feat(appointment): log cancellation events and show them in a Timeline

Introduce the first per-appointment event system. On cancel (via the status
or general update endpoints) an AppointmentEvent (type=cancelled, «نوبت لغو
شد») is recorded with the actor, cancel time, and an optional cancel_reason,
plus a warning-level app_log entry. New GET /appointment/{uuid}/events
returns the ordered event list. The admin appointment detail page renders a
Timeline section and the cancel dialog now collects an optional reason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-17 11:13:23 +03:30
co-authored by Claude Fable 5
parent 36d7fe0303
commit 49b2ca60d7
8 changed files with 311 additions and 8 deletions
@@ -0,0 +1,31 @@
<?php
namespace App\Appointment\Repository;
use App\Appointment\Entity\AppointmentEvent;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class AppointmentEventRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry) { parent::__construct($registry, AppointmentEvent::class); }
public function save(AppointmentEvent $e, bool $flush = true): void
{
$this->getEntityManager()->persist($e);
if ($flush) {
$this->getEntityManager()->flush();
}
}
/** رویدادهای یک نوبت به‌ترتیب زمان (قدیمی → جدید)، به‌صورت آرایه. */
public function findByAppointmentUuid(string $appointmentUuid): array
{
return $this->createQueryBuilder('e')
->select('e.type AS type', 'e.title AS title', 'e.actorName AS actor_name', 'e.reason AS reason', 'e.createdAt AS created_at')
->join('e.appointment', 'a')
->where('a.uuid = :uuid')->setParameter('uuid', $appointmentUuid)
->orderBy('e.createdAt', 'ASC')
->getQuery()->getArrayResult();
}
}