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 (
+
+ );
+}
+
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}`)}>
| {d.name} |
- {d.medical_code ?? '—'} |
+ {d.medical_code ?? '—'} |
{formatNumber(d.past_count)} |
- {formatNumber(d.upcoming_count)} |
+ {d.upcoming_count > 0 ? {formatNumber(d.upcoming_count)} : ۰} |
|
))}
@@ -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)} نوبت
-
-
-
-
- | پزشک |
- بیمار |
- زمان |
- وضعیت |
+
+
+
+
+ | پزشک |
+ بیمار |
+ زمان |
+ وضعیت |
+
+
+
+ {repAppointments.map((a) => (
+ navigate(`/admin/doctors/${a.doctor_uuid}`)}>
+ | {a.doctor_name} |
+ {a.patient_name ?? '—'} |
+ {formatDateTime(a.slot_start)} |
+ {a.status} |
-
-
- {repAppointments.map((a) => (
- navigate(`/admin/doctors/${a.doctor_uuid}`)}>
- | {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'); // کاربر سیستمی کرالر — لاگین با رمز؛ دسترسی فقط اندپوینت ایمپورت
}
}