From 0ec0b92705b6531720a60f7d11c391e03806d88a Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Sun, 12 Jul 2026 14:39:03 +0330 Subject: [PATCH] feat(representation): add KPI tiles and enhance doctor representation details in admin view --- .../admin/pages/RepresentationDetailPage.tsx | 132 ++++++++++++------ src/Admin/Controller/AdminApiController.php | 1 + src/Auth/Entity/User.php | 1 + 3 files changed, 88 insertions(+), 46 deletions(-) diff --git a/assets/admin/pages/RepresentationDetailPage.tsx b/assets/admin/pages/RepresentationDetailPage.tsx index 55775401..cacd297f 100644 --- a/assets/admin/pages/RepresentationDetailPage.tsx +++ b/assets/admin/pages/RepresentationDetailPage.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { PencilIcon, TrashIcon, CheckCircleIcon, XCircleIcon } from '@heroicons/react/24/outline'; +import { PencilIcon, TrashIcon, CheckCircleIcon, XCircleIcon, UserGroupIcon, CalendarDaysIcon, ClockIcon, ReceiptPercentIcon, PlusIcon } from '@heroicons/react/24/outline'; import { toast } from 'sonner'; import { api } from '../lib/api'; import type { ApiResponse, PaginatedResponse } from '../lib/api'; @@ -52,6 +52,23 @@ function DetailRow({ label, value }: { label: string; value: React.ReactNode }) ); } +function StatTile({ icon: Icon, label, value, tone }: { + icon: React.ComponentType<{ className?: string }>; + label: string; value: React.ReactNode; tone: 'violet' | 'green' | 'amber' | 'pink'; +}) { + return ( +
+
+ +
+
+
{label}
+
{value}
+
+
+ ); +} + export default function RepresentationDetailPage() { const { uuid } = useParams<{ uuid: string }>(); const navigate = useNavigate(); @@ -85,6 +102,8 @@ export default function RepresentationDetailPage() { }); const repDoctors: RepDoctor[] = doctorsQuery.data?.data ?? []; const repDoctorsTotal = doctorsQuery.data?.meta?.totalRecords ?? repDoctors.length; + const upcomingTotal = repDoctors.reduce((s, d) => s + (d.upcoming_count ?? 0), 0); + const pastTotal = repDoctors.reduce((s, d) => s + (d.past_count ?? 0), 0); const appointmentsQuery = useQuery({ queryKey: ['representation-appointments', uuid, apptScope], @@ -244,6 +263,14 @@ export default function RepresentationDetailPage() { } /> + {/* KPI tiles */} +
+ + + + +
+
{/* Basic Info */}
@@ -284,24 +311,33 @@ export default function RepresentationDetailPage() {
{/* Doctors of this representation */} -
-
+
+
+

پزشکان این نماینده

- {formatNumber(repDoctorsTotal)} پزشک + {formatNumber(repDoctorsTotal)}
- +
{doctorsQuery.isLoading ? ( -

در حال بارگذاری…

+

در حال بارگذاری…

) : repDoctors.length === 0 ? ( -

این نماینده پزشکی ندارد

+
+ +

این نماینده هنوز پزشکی ندارد

+ +
) : ( -
- +
+
- + @@ -312,9 +348,9 @@ export default function RepresentationDetailPage() { {repDoctors.map((d) => ( navigate(`/admin/doctors/${d.uuid}`)}> - + - + ))} @@ -325,46 +361,50 @@ export default function RepresentationDetailPage() { {/* Appointments of this representation */} -
-
-

نوبت‌های این نماینده

-
- - +
+
+
+ +

نوبت‌های این نماینده

+ {!appointmentsQuery.isLoading && {formatNumber(repAppointmentsTotal)}} +
+
+ +
{appointmentsQuery.isLoading ? ( -

در حال بارگذاری…

+

در حال بارگذاری…

) : repAppointments.length === 0 ? ( -

- {apptScope === 'upcoming' ? 'نوبت آینده‌ای ثبت نشده' : 'نوبت گذشته‌ای وجود ندارد'} -

+
+ +

+ {apptScope === 'upcoming' ? 'نوبت آینده‌ای ثبت نشده است' : 'نوبت گذشته‌ای وجود ندارد'} +

+
) : ( - <> -
{formatNumber(repAppointmentsTotal)} نوبت
-
-
نامنام پزشک کد نظام نوبت گذشته نوبت آینده
{d.name}{d.medical_code ?? '—'}{d.medical_code ?? '—'} {formatNumber(d.past_count)}{formatNumber(d.upcoming_count)}{d.upcoming_count > 0 ? {formatNumber(d.upcoming_count)} : ۰}
- - - - - - +
+
پزشکبیمارزمانوضعیت
+ + + + + + + + + + {repAppointments.map((a) => ( + navigate(`/admin/doctors/${a.doctor_uuid}`)}> + + + + - - - {repAppointments.map((a) => ( - navigate(`/admin/doctors/${a.doctor_uuid}`)}> - - - - - - ))} - -
پزشکبیمارزمانوضعیت
{a.doctor_name}{a.patient_name ?? '—'}{formatDateTime(a.slot_start)}{a.status}
{a.doctor_name}{a.patient_name ?? '—'}{formatDateTime(a.slot_start)}{a.status}
-
- + ))} + + +
)}
diff --git a/src/Admin/Controller/AdminApiController.php b/src/Admin/Controller/AdminApiController.php index 6036d2d9..7cd9e9e8 100644 --- a/src/Admin/Controller/AdminApiController.php +++ b/src/Admin/Controller/AdminApiController.php @@ -14,6 +14,7 @@ use App\Payment\Entity\Payment; use App\Rating\Entity\Comment; use App\Rating\Entity\Rate; use App\Representation\Entity\Representation; +use App\Representation\Repository\RepresentationRepository; use App\Secretary\Entity\DoctorSecretary; use App\Settlement\Entity\FinancialBreakdown; use App\Settlement\Entity\Settlement; diff --git a/src/Auth/Entity/User.php b/src/Auth/Entity/User.php index 63624988..055d9ead 100644 --- a/src/Auth/Entity/User.php +++ b/src/Auth/Entity/User.php @@ -122,6 +122,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface || $this->hasRole('ROLE_CLINIC') || $this->hasRole('ROLE_SECRETARY') || $this->hasRole('ROLE_ADMIN') + || $this->hasRole('ROLE_REPRESENTATION') // نماینده — لاگین با نام‌کاربری/رمز مجاز است || $this->hasRole('ROLE_IMPORTER'); // کاربر سیستمی کرالر — لاگین با رمز؛ دسترسی فقط اندپوینت ایمپورت } }