feat(dashboard): doctor/clinic dashboard matching Figma layout

Restructure to Figma: 4 stat cards (today appts, today/week payments,
total patients) -> two charts (revenue area + patient-count vertical
bars) -> today appointments table. Add today/week payments, total
patients, and 7-day revenue/appointments series to doctor + clinic
dashboard endpoints. Add SvgVBars chart. Update dashboard.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-13 09:23:38 +03:30
co-authored by Claude Opus 4.8
parent 9e491ae6b9
commit 599382fae4
3 changed files with 180 additions and 24 deletions
@@ -132,6 +132,17 @@ class DashboardController extends BaseController
$smsBalance = $this->smsWalletService->getBalance('clinic', $clinicId);
$uniquePatients = $this->patientRecordRepo->countUnique('clinic', $clinicId, $from, $to);
$revenuePeriod = $this->patientSessionRepo->sumRevenue('clinic', $clinicId, $from, $to);
$totalPatients = $this->patientRecordRepo->countUnique('clinic', $clinicId, 0, time());
$rev = $this->revenueDaily('clinic', $clinicId);
$apptByDay = $this->appointmentsDaily(function (int $ds, int $de) use ($clinicId): int {
return (int) $this->em->createQuery('
SELECT COUNT(a.id) FROM App\Appointment\Entity\Appointment a
JOIN App\Doctor\Entity\Doctor d WITH a.doctor = d
JOIN App\Clinic\Entity\Clinic c WITH d MEMBER OF c.doctors
WHERE c.id = :clinicId AND a.slotStart >= :s AND a.slotStart <= :e
')->setParameters(['clinicId' => $clinicId, 's' => $ds, 'e' => $de])->getSingleScalarResult();
});
return $this->success([
'clinic' => [
@@ -147,7 +158,14 @@ class DashboardController extends BaseController
'pending_invitations' => $pendingInvitations,
'sms_wallet_balance' => $smsBalance,
'unique_patients_count' => $uniquePatients,
'total_patients' => $totalPatients,
'revenue_period_rials' => $revenuePeriod,
'today_payments_rials' => $rev['today_payments_rials'],
'week_payments_rials' => $rev['week_payments_rials'],
],
'charts' => [
'revenue_by_day' => $rev['revenue'],
'appointments_by_day' => $apptByDay,
],
'period' => ['from' => $from, 'to' => $to],
'today_appointments' => $todayAppts,
@@ -227,6 +245,15 @@ class DashboardController extends BaseController
$smsBalance = $this->smsWalletService->getBalance('doctor', $doctorId);
$uniquePatients = $this->patientRecordRepo->countUnique('doctor', $doctorId, $from, $to);
$revenuePeriod = $this->patientSessionRepo->sumRevenue('doctor', $doctorId, $from, $to);
$totalPatients = $this->patientRecordRepo->countUnique('doctor', $doctorId, 0, time());
$rev = $this->revenueDaily('doctor', $doctorId);
$apptByDay = $this->appointmentsDaily(function (int $ds, int $de) use ($doctor): int {
return (int) $this->em->createQuery('
SELECT COUNT(a.id) FROM App\Appointment\Entity\Appointment a
WHERE a.doctor = :d AND a.slotStart >= :s AND a.slotStart <= :e
')->setParameters(['d' => $doctor, 's' => $ds, 'e' => $de])->getSingleScalarResult();
});
return $this->success([
'doctor' => [
@@ -242,7 +269,14 @@ class DashboardController extends BaseController
'total_ratings' => (int) ($ratingRow['total'] ?? 0),
'sms_wallet_balance' => $smsBalance,
'unique_patients_count' => $uniquePatients,
'total_patients' => $totalPatients,
'revenue_period_rials' => $revenuePeriod,
'today_payments_rials' => $rev['today_payments_rials'],
'week_payments_rials' => $rev['week_payments_rials'],
],
'charts' => [
'revenue_by_day' => $rev['revenue'],
'appointments_by_day' => $apptByDay,
],
'period' => ['from' => $from, 'to' => $to],
'today_appointments' => $todayAppts,
@@ -250,6 +284,44 @@ class DashboardController extends BaseController
]);
}
/**
* سری ۷ روز اخیر درآمد (بر اساس PatientSession) + پرداختی امروز و هفته.
* @return array{revenue: array<int, array{label:string, amount_rials:int}>, today_payments_rials:int, week_payments_rials:int}
*/
private function revenueDaily(string $entityType, int $entityId): array
{
$fmt = new \IntlDateFormatter('fa_IR@calendar=persian', \IntlDateFormatter::NONE, \IntlDateFormatter::NONE, 'Asia/Tehran', \IntlDateFormatter::TRADITIONAL, 'd MMMM');
$series = [];
$todayPay = 0;
$weekPay = 0;
for ($i = 6; $i >= 0; $i--) {
$ds = strtotime('today midnight') - $i * 86400;
$de = $ds + 86399;
$rev = (int) $this->patientSessionRepo->sumRevenue($entityType, $entityId, $ds, $de);
$series[] = ['label' => $fmt->format($ds), 'amount_rials' => $rev];
$weekPay += $rev;
if ($i === 0) { $todayPay = $rev; }
}
return ['revenue' => $series, 'today_payments_rials' => $todayPay, 'week_payments_rials' => $weekPay];
}
/**
* سری ۷ روز اخیر تعداد نوبت با شمارنده‌ی دلخواه (doctor/clinic).
* @param callable(int $dayStart, int $dayEnd): int $counter
* @return array<int, array{label:string, count:int}>
*/
private function appointmentsDaily(callable $counter): array
{
$fmt = new \IntlDateFormatter('fa_IR@calendar=persian', \IntlDateFormatter::NONE, \IntlDateFormatter::NONE, 'Asia/Tehran', \IntlDateFormatter::TRADITIONAL, 'd MMMM');
$series = [];
for ($i = 6; $i >= 0; $i--) {
$ds = strtotime('today midnight') - $i * 86400;
$de = $ds + 86399;
$series[] = ['label' => $fmt->format($ds), 'count' => $counter($ds, $de)];
}
return $series;
}
// ── Secretary Dashboard ──────────────────────────────────────────────────
#[Route('/api/v1/dashboard/secretary', methods: ['GET'])]