- Introduced clinic_id to weekly_schedules, date_overrides, and holidays to differentiate between personal and clinic schedules. - Updated unique constraints and indexes to accommodate the new clinic context. feat(command): create AssignScheduleClinicCommand to move schedules - Added a command to move a doctor's personal weekly schedule into a clinic context. - Implemented checks to ensure sessions align with the target clinic. feat(context): implement EntityContext and EntityContextResolver - Created EntityContext to represent the effective working environment of a request (doctor or clinic). - Developed EntityContextResolver to determine the execution context based on user roles and active contexts. test: add ServiceModeContextTest for appointment scheduling - Implemented tests to ensure service booking respects clinic and personal contexts. - Verified that financial data is omitted in clinic contexts in InvitedDoctorDashboardScopeTest.
535 lines
24 KiB
PHP
535 lines
24 KiB
PHP
<?php
|
|
|
|
namespace App\Dashboard\Controller;
|
|
|
|
use App\Auth\Entity\User;
|
|
use App\Auth\Repository\UserActiveContextRepository;
|
|
use App\Clinic\Repository\ClinicRepository;
|
|
use App\Doctor\Repository\DoctorRepository;
|
|
use App\Patient\Repository\PatientRecordRepository;
|
|
use App\Patient\Repository\PatientSessionRepository;
|
|
use App\Secretary\Entity\DoctorSecretary;
|
|
use App\Secretary\Repository\DoctorSecretaryRepository;
|
|
use App\Shared\Constant\ErrorCodes;
|
|
use App\Shared\Context\EntityContextResolver;
|
|
use App\Shared\Controller\BaseController;
|
|
use App\Sms\Service\SmsWalletService;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\Routing\Attribute\Route;
|
|
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|
use OpenApi\Attributes as OA;
|
|
|
|
#[OA\Tag(name: 'Dashboard')]
|
|
class DashboardController extends BaseController
|
|
{
|
|
public function __construct(
|
|
private readonly ClinicRepository $clinicRepo,
|
|
private readonly DoctorRepository $doctorRepo,
|
|
private readonly DoctorSecretaryRepository $secretaryRepo,
|
|
private readonly EntityManagerInterface $em,
|
|
private readonly SmsWalletService $smsWalletService,
|
|
private readonly PatientRecordRepository $patientRecordRepo,
|
|
private readonly PatientSessionRepository $patientSessionRepo,
|
|
private readonly UserActiveContextRepository $contextRepo,
|
|
private readonly EntityContextResolver $contextResolver,
|
|
private readonly \App\Clinic\Security\ClinicDoctorPermissionChecker $permChecker,
|
|
private readonly \App\Doctor\Repository\DoctorAddressRepository $addressRepo,
|
|
) {}
|
|
|
|
// ── Clinic Dashboard ────────────────────────────────────────────────────
|
|
|
|
#[Route('/api/v1/dashboard/clinic', methods: ['GET'])]
|
|
#[IsGranted('ROLE_CLINIC')]
|
|
public function clinic(Request $request, #[CurrentUser] User $user): JsonResponse
|
|
{
|
|
$clinic = $this->clinicRepo->findByUser($user);
|
|
if ($clinic === null) {
|
|
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'کلینیک یافت نشد', 404);
|
|
}
|
|
|
|
$clinicId = $clinic->getId();
|
|
$todayStart = strtotime('today midnight');
|
|
$todayEnd = strtotime('tomorrow midnight') - 1;
|
|
$monthStart = strtotime('first day of this month midnight');
|
|
|
|
$from = $request->query->get('from') ? (int) $request->query->get('from') : $monthStart;
|
|
$to = $request->query->get('to') ? (int) $request->query->get('to') : time();
|
|
|
|
// آمار نوبتهای امروز و این ماه
|
|
$stats = $this->em->createQuery('
|
|
SELECT
|
|
COUNT(a.id) AS today_appointments,
|
|
SUM(CASE WHEN a.slotStart >= :monthStart THEN 1 ELSE 0 END) AS this_month_appointments
|
|
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 >= :todayStart AND a.slotStart <= :todayEnd
|
|
')->setParameters([
|
|
'clinicId' => $clinicId,
|
|
'todayStart' => $todayStart,
|
|
'todayEnd' => $todayEnd,
|
|
'monthStart' => $monthStart,
|
|
])->getOneOrNullResult() ?? [];
|
|
|
|
// شمارش کل نوبتهای این ماه (query جداگانه)
|
|
$monthCount = (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 >= :monthStart
|
|
')->setParameters([
|
|
'clinicId' => $clinicId,
|
|
'monthStart' => $monthStart,
|
|
])->getSingleScalarResult();
|
|
|
|
// تعداد دعوتنامههای در انتظار
|
|
$pendingInvitations = (int) $this->em->createQuery('
|
|
SELECT COUNT(i.id)
|
|
FROM App\ClinicInvitation\Entity\ClinicDoctorInvitation i
|
|
WHERE i.clinic = :clinic AND i.status = :status
|
|
')->setParameters([
|
|
'clinic' => $clinic,
|
|
'status' => 'pending',
|
|
])->getSingleScalarResult();
|
|
|
|
// ۵ نوبت امروز این کلینیک
|
|
$todayAppts = $this->em->createQuery('
|
|
SELECT a.uuid, u.realName AS patient_name, u.mobileNumber AS patient_mobile,
|
|
d.name AS doctor_name, si.name AS service_name,
|
|
a.slotStart AS slot_start, a.slotEnd AS slot_end, a.status
|
|
FROM App\Appointment\Entity\Appointment a
|
|
JOIN a.doctor d
|
|
JOIN a.user u
|
|
LEFT JOIN a.serviceItem si
|
|
JOIN App\Clinic\Entity\Clinic c WITH d MEMBER OF c.doctors
|
|
WHERE c.id = :clinicId
|
|
AND a.slotStart >= :todayStart AND a.slotStart <= :todayEnd
|
|
ORDER BY a.slotStart ASC
|
|
')->setMaxResults(5)->setParameters([
|
|
'clinicId' => $clinicId,
|
|
'todayStart' => $todayStart,
|
|
'todayEnd' => $todayEnd,
|
|
])->getArrayResult();
|
|
|
|
// لیست پزشکان با شمارش نوبت امروز
|
|
$doctors = $this->em->createQuery('
|
|
SELECT d.uuid, d.name,
|
|
COUNT(a.id) AS today_count
|
|
FROM App\Doctor\Entity\Doctor d
|
|
JOIN App\Clinic\Entity\Clinic c WITH d MEMBER OF c.doctors
|
|
LEFT JOIN App\Appointment\Entity\Appointment a
|
|
WITH a.doctor = d
|
|
AND a.slotStart >= :todayStart
|
|
AND a.slotStart <= :todayEnd
|
|
WHERE c.id = :clinicId
|
|
GROUP BY d.id
|
|
')->setParameters([
|
|
'clinicId' => $clinicId,
|
|
'todayStart' => $todayStart,
|
|
'todayEnd' => $todayEnd,
|
|
])->getArrayResult();
|
|
|
|
$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' => [
|
|
'uuid' => $clinic->getUuid(),
|
|
'name' => $clinic->getName(),
|
|
'is_active' => $clinic->isActive(),
|
|
'logo' => $clinic->getClinicLogo(),
|
|
],
|
|
'stats' => [
|
|
'total_doctors' => count($doctors),
|
|
'today_appointments' => (int) ($stats['today_appointments'] ?? 0),
|
|
'this_month_appointments' => $monthCount,
|
|
'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,
|
|
'doctors' => $doctors,
|
|
]);
|
|
}
|
|
|
|
// ── Doctor Dashboard ─────────────────────────────────────────────────────
|
|
|
|
#[Route('/api/v1/dashboard/doctor', methods: ['GET'])]
|
|
#[IsGranted('ROLE_DOCTOR')]
|
|
public function doctor(Request $request, #[CurrentUser] User $user): JsonResponse
|
|
{
|
|
$doctor = $this->doctorRepo->findByUser($user);
|
|
if ($doctor === null) {
|
|
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'دکتر یافت نشد', 404);
|
|
}
|
|
|
|
// محیط فعال تعیین میکند این داشبورد شخصی است یا داخل یک کلینیک. بدون این،
|
|
// پزشکِ دعوتشده در محیط کلینیک آمار و درآمد مطب شخصی خودش را میدید.
|
|
$context = $this->contextResolver->tryResolve($user, $request->query->get('clinic_uuid'));
|
|
$clinic = $context?->clinic;
|
|
|
|
$doctorId = $doctor->getId();
|
|
$todayStart = strtotime('today midnight');
|
|
$todayEnd = strtotime('tomorrow midnight') - 1;
|
|
$tmrStart = strtotime('tomorrow midnight');
|
|
$tmrEnd = strtotime('tomorrow midnight') + 86399;
|
|
$monthStart = strtotime('first day of this month midnight');
|
|
|
|
$from = $request->query->get('from') ? (int) $request->query->get('from') : $monthStart;
|
|
$to = $request->query->get('to') ? (int) $request->query->get('to') : time();
|
|
|
|
// آمار
|
|
$todayCount = $this->countAppointments($doctor, $clinic, $todayStart, $todayEnd);
|
|
$tmrCount = $this->countAppointments($doctor, $clinic, $tmrStart, $tmrEnd);
|
|
$monthCount = $this->countAppointments($doctor, $clinic, $monthStart, PHP_INT_MAX);
|
|
|
|
// میانگین و تعداد امتیاز
|
|
$ratingRow = $this->em->createQuery('
|
|
SELECT AVG((r.waitingTimeAtClinic + r.accuracyOfDiagnosis + r.doctorBehavior + r.clinicCleanliness + r.doctorExpertise) / 5.0) AS avg_score,
|
|
COUNT(r.id) AS total
|
|
FROM App\Rating\Entity\Rate r WHERE r.doctor = :doctor
|
|
')->setParameter('doctor', $doctor)->getOneOrNullResult() ?? [];
|
|
|
|
// نوبتهای امروز
|
|
$todayApptsQb = $this->em->createQueryBuilder()
|
|
->select('a.uuid, u.realName AS patient_name, u.mobileNumber AS patient_mobile,
|
|
d.name AS doctor_name, si.name AS service_name,
|
|
a.slotStart AS slot_start, a.slotEnd AS slot_end, a.status')
|
|
->from(\App\Appointment\Entity\Appointment::class, 'a')
|
|
->join('a.user', 'u')
|
|
->join('a.doctor', 'd')
|
|
->leftJoin('a.serviceItem', 'si')
|
|
->where('a.doctor = :doctor')
|
|
->andWhere('a.slotStart >= :s AND a.slotStart <= :e')
|
|
->setParameter('doctor', $doctor)
|
|
->setParameter('s', $todayStart)
|
|
->setParameter('e', $todayEnd)
|
|
->orderBy('a.slotStart', 'ASC')
|
|
->setMaxResults(10);
|
|
|
|
$this->restrictToClinicAddresses($todayApptsQb, $clinic);
|
|
$todayAppts = $todayApptsQb->getQuery()->getArrayResult();
|
|
|
|
// کلینیکهای عضو
|
|
$clinics = $this->em->createQuery('
|
|
SELECT c.uuid, c.name, c.clinicLogo AS logo
|
|
FROM App\Clinic\Entity\Clinic c
|
|
JOIN c.doctors d
|
|
WHERE d.id = :doctorId
|
|
')->setParameter('doctorId', $doctorId)->getArrayResult();
|
|
|
|
$apptByDay = $this->appointmentsDaily(
|
|
fn(int $ds, int $de): int => $this->countAppointments($doctor, $clinic, $ds, $de)
|
|
);
|
|
|
|
$stats = [
|
|
'today_appointments' => $todayCount,
|
|
'tomorrow_appointments' => $tmrCount,
|
|
'this_month_appointments' => $monthCount,
|
|
'avg_rating' => $ratingRow['avg_score'] ? round((float) $ratingRow['avg_score'], 1) : null,
|
|
'total_ratings' => (int) ($ratingRow['total'] ?? 0),
|
|
];
|
|
|
|
$charts = ['appointments_by_day' => $apptByDay];
|
|
|
|
// ارقام مالی و کیف پول پیامک به مطب شخصی تعلق دارند. در محیط کلینیک اصلاً
|
|
// برگردانده نمیشوند مگر کاربر مالک همان کلینیک باشد — مخفیکردن در UI کافی
|
|
// نیست، چون endpoint مستقیماً قابل صدا زدن است.
|
|
if ($this->maySeeFinancials($user, $clinic)) {
|
|
$rev = $this->revenueDaily('doctor', $doctorId);
|
|
|
|
$stats['sms_wallet_balance'] = $this->smsWalletService->getBalance('doctor', $doctorId);
|
|
$stats['unique_patients_count'] = $this->patientRecordRepo->countUnique('doctor', $doctorId, $from, $to);
|
|
$stats['total_patients'] = $this->patientRecordRepo->countUnique('doctor', $doctorId, 0, time());
|
|
$stats['revenue_period_rials'] = $this->patientSessionRepo->sumRevenue('doctor', $doctorId, $from, $to);
|
|
$stats['today_payments_rials'] = $rev['today_payments_rials'];
|
|
$stats['week_payments_rials'] = $rev['week_payments_rials'];
|
|
|
|
$charts['revenue_by_day'] = $rev['revenue'];
|
|
}
|
|
|
|
return $this->success([
|
|
'doctor' => [
|
|
'uuid' => $doctor->getUuid(),
|
|
'name' => $doctor->getName(),
|
|
'degree' => $doctor->getDegree(),
|
|
],
|
|
'context' => [
|
|
'type' => $clinic === null ? 'personal' : 'clinic',
|
|
'clinic_uuid' => $clinic?->getUuid(),
|
|
'clinic_name' => $clinic?->getName(),
|
|
],
|
|
'stats' => $stats,
|
|
'charts' => $charts,
|
|
'period' => ['from' => $from, 'to' => $to],
|
|
'today_appointments' => $todayAppts,
|
|
// فهرست کلینیکها فقط در محیط شخصی معنا دارد.
|
|
'clinics' => $clinic === null ? $clinics : [],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* نوبتهای پزشک در یک بازه، محدود به محیط جاری. در محیط کلینیک فقط نوبتهایی
|
|
* شمرده میشوند که آدرسشان متعلق به همان کلینیک است.
|
|
*/
|
|
private function countAppointments(\App\Doctor\Entity\Doctor $doctor, ?\App\Clinic\Entity\Clinic $clinic, int $from, int $to): int
|
|
{
|
|
$qb = $this->em->createQueryBuilder()
|
|
->select('COUNT(a.id)')
|
|
->from(\App\Appointment\Entity\Appointment::class, 'a')
|
|
->where('a.doctor = :doctor')
|
|
->andWhere('a.slotStart >= :s AND a.slotStart <= :e')
|
|
->setParameter('doctor', $doctor)
|
|
->setParameter('s', $from)
|
|
->setParameter('e', $to);
|
|
|
|
$this->restrictToClinicAddresses($qb, $clinic);
|
|
|
|
return (int) $qb->getQuery()->getSingleScalarResult();
|
|
}
|
|
|
|
private function restrictToClinicAddresses(\Doctrine\ORM\QueryBuilder $qb, ?\App\Clinic\Entity\Clinic $clinic): void
|
|
{
|
|
if ($clinic === null) {
|
|
return;
|
|
}
|
|
|
|
$addressIds = array_map(
|
|
fn(\App\Doctor\Entity\DoctorAddress $a): int => (int) $a->getId(),
|
|
$this->addressRepo->findForContext($qb->getParameter('doctor')->getValue(), $clinic->getId())
|
|
);
|
|
|
|
$qb->andWhere('a.addressId IN (:addressIds)')
|
|
->setParameter('addressIds', $addressIds ?: [0]);
|
|
}
|
|
|
|
/** فقط محیط شخصی خود پزشک، یا مالک همان کلینیک. */
|
|
private function maySeeFinancials(User $user, ?\App\Clinic\Entity\Clinic $clinic): bool
|
|
{
|
|
if ($clinic === null) {
|
|
return true;
|
|
}
|
|
|
|
return $clinic->getUser()->getId() === $user->getId()
|
|
&& $this->permChecker->can($user, $clinic, 'payments', 'view');
|
|
}
|
|
|
|
/**
|
|
* سری ۷ روز اخیر درآمد (بر اساس 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'])]
|
|
#[IsGranted('ROLE_SECRETARY')]
|
|
public function secretary(#[CurrentUser] User $user): JsonResponse
|
|
{
|
|
$activeCtx = $this->contextRepo->findByUser($user);
|
|
$dbUuid = $activeCtx?->getDbUuid();
|
|
|
|
if ($dbUuid === null) {
|
|
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'دسترسی منشی تنظیم نشده', 403);
|
|
}
|
|
|
|
// تعیین scope بر اساس db_uuid فعال
|
|
$clinic = $this->clinicRepo->findByUuid($dbUuid);
|
|
if ($clinic !== null) {
|
|
return $this->secretaryClinicDashboard($user, $clinic);
|
|
}
|
|
|
|
$doctor = $this->doctorRepo->findByUuid($dbUuid);
|
|
if ($doctor !== null) {
|
|
$rel = $this->secretaryRepo->findActiveBySecretaryForDoctor($user, $doctor);
|
|
if ($rel === null) {
|
|
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'دسترسی منشی تنظیم نشده', 403);
|
|
}
|
|
return $this->secretaryDoctorDashboard($user, $rel);
|
|
}
|
|
|
|
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'context نامعتبر است', 403);
|
|
}
|
|
|
|
private function secretaryDoctorDashboard(User $user, DoctorSecretary $rel): JsonResponse
|
|
{
|
|
$doctor = $rel->getDoctor();
|
|
$permissions = $rel->getPermissions();
|
|
$canView = (bool) ($permissions['resources']['appointments']['view'] ?? false);
|
|
|
|
$todayStart = strtotime('today midnight');
|
|
$todayEnd = strtotime('tomorrow midnight') - 1;
|
|
$tmrStart = strtotime('tomorrow midnight');
|
|
$tmrEnd = strtotime('tomorrow midnight') + 86399;
|
|
|
|
$todayCount = (int) $this->em->createQuery('
|
|
SELECT COUNT(a.id) FROM App\Appointment\Entity\Appointment a
|
|
WHERE a.doctor = :doctor AND a.slotStart >= :s AND a.slotStart <= :e
|
|
')->setParameters(['doctor' => $doctor, 's' => $todayStart, 'e' => $todayEnd])
|
|
->getSingleScalarResult();
|
|
|
|
$tmrCount = (int) $this->em->createQuery('
|
|
SELECT COUNT(a.id) FROM App\Appointment\Entity\Appointment a
|
|
WHERE a.doctor = :doctor AND a.slotStart >= :s AND a.slotStart <= :e
|
|
')->setParameters(['doctor' => $doctor, 's' => $tmrStart, 'e' => $tmrEnd])
|
|
->getSingleScalarResult();
|
|
|
|
$todayAppts = [];
|
|
if ($canView) {
|
|
$todayAppts = $this->em->createQuery('
|
|
SELECT a.uuid, u.realName AS patient_name, u.mobileNumber AS patient_mobile,
|
|
a.slotStart AS slot_start, a.status
|
|
FROM App\Appointment\Entity\Appointment a
|
|
JOIN a.user u
|
|
WHERE a.doctor = :doctor AND a.slotStart >= :s AND a.slotStart <= :e
|
|
ORDER BY a.slotStart ASC
|
|
')->setMaxResults(10)->setParameters([
|
|
'doctor' => $doctor,
|
|
's' => $todayStart,
|
|
'e' => $todayEnd,
|
|
])->getArrayResult();
|
|
}
|
|
|
|
return $this->success([
|
|
'scope' => 'doctor',
|
|
'doctor' => [
|
|
'uuid' => $doctor->getUuid(),
|
|
'name' => $doctor->getName(),
|
|
'degree' => $doctor->getDegree(),
|
|
],
|
|
'permissions' => $permissions,
|
|
'stats' => [
|
|
'today_appointments' => $todayCount,
|
|
'tomorrow_appointments' => $tmrCount,
|
|
],
|
|
'today_appointments' => $todayAppts,
|
|
]);
|
|
}
|
|
|
|
private function secretaryClinicDashboard(User $user, \App\Clinic\Entity\Clinic $clinic): JsonResponse
|
|
{
|
|
$rel = $this->secretaryRepo->findActiveBySecretaryForClinic($user, $clinic);
|
|
if ($rel === null) {
|
|
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'دسترسی منشی تنظیم نشده', 403);
|
|
}
|
|
|
|
$permissions = $rel->getPermissions();
|
|
$canView = (bool) ($permissions['resources']['appointments']['view'] ?? false);
|
|
|
|
$todayStart = strtotime('today midnight');
|
|
$todayEnd = strtotime('tomorrow midnight') - 1;
|
|
$tmrStart = strtotime('tomorrow midnight');
|
|
$tmrEnd = strtotime('tomorrow midnight') + 86399;
|
|
|
|
$doctors = $this->secretaryRepo->findDoctorsBySecretaryInClinic($user, $clinic);
|
|
|
|
$todayCount = 0;
|
|
$tmrCount = 0;
|
|
$todayAppts = [];
|
|
|
|
if (!empty($doctors)) {
|
|
$todayCount = (int) $this->em->createQuery('
|
|
SELECT COUNT(a.id) FROM App\Appointment\Entity\Appointment a
|
|
WHERE a.doctor IN (:doctors) AND a.slotStart >= :s AND a.slotStart <= :e
|
|
')->setParameters(['doctors' => $doctors, 's' => $todayStart, 'e' => $todayEnd])
|
|
->getSingleScalarResult();
|
|
|
|
$tmrCount = (int) $this->em->createQuery('
|
|
SELECT COUNT(a.id) FROM App\Appointment\Entity\Appointment a
|
|
WHERE a.doctor IN (:doctors) AND a.slotStart >= :s AND a.slotStart <= :e
|
|
')->setParameters(['doctors' => $doctors, 's' => $tmrStart, 'e' => $tmrEnd])
|
|
->getSingleScalarResult();
|
|
|
|
if ($canView) {
|
|
$todayAppts = $this->em->createQuery('
|
|
SELECT a.uuid, u.realName AS patient_name, u.mobileNumber AS patient_mobile,
|
|
a.slotStart AS slot_start, a.status, d.name AS doctor_name
|
|
FROM App\Appointment\Entity\Appointment a
|
|
JOIN a.user u
|
|
JOIN a.doctor d
|
|
WHERE a.doctor IN (:doctors) AND a.slotStart >= :s AND a.slotStart <= :e
|
|
ORDER BY a.slotStart ASC
|
|
')->setMaxResults(20)->setParameters([
|
|
'doctors' => $doctors,
|
|
's' => $todayStart,
|
|
'e' => $todayEnd,
|
|
])->getArrayResult();
|
|
}
|
|
}
|
|
|
|
return $this->success([
|
|
'scope' => 'clinic',
|
|
'clinic' => [
|
|
'uuid' => $clinic->getUuid(),
|
|
'name' => $clinic->getName(),
|
|
],
|
|
'permissions' => $permissions,
|
|
'stats' => [
|
|
'today_appointments' => $todayCount,
|
|
'tomorrow_appointments' => $tmrCount,
|
|
],
|
|
'today_appointments' => $todayAppts,
|
|
]);
|
|
}
|
|
}
|
|
|