feat(holidays): one official calendar, inherited everywhere
The holiday model was already right — national holidays global, a per-tenant override in both directions, per-doctor and per-resource exceptions — but nothing could create a national holiday. The only writer was an import command, so the calendar the whole product inherits from had no owner. Three admin-only routes give it one. POST upserts, because `date` is unique and re-sending a day should rename it rather than surface a raw database error; PATCH takes only the title, because moving a date means a different holiday. The system admin has no work environment, so the list endpoint now returns the calendar with an empty `overrides` for that role instead of the 403 `pair()` would raise — the person who maintains the calendar has to be able to read it. Both holiday tabs — the doctor's and the resource's — now open with the official calendar above their own exceptions, from one shared card rather than two copies that would drift. Each row can be opted out of with a single click, which is the existing holiday-override endpoint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ namespace App\Resource\Controller;
|
||||
use App\Auth\Entity\User;
|
||||
use App\Resource\Entity\NationalHoliday;
|
||||
use App\Resource\Entity\TenantHolidayOverride;
|
||||
use App\Resource\Repository\NationalHolidayRepository;
|
||||
use App\Resource\Repository\TenantHolidayOverrideRepository;
|
||||
use App\Resource\Service\HolidayService;
|
||||
use App\Resource\Service\ResourceContext;
|
||||
@@ -29,6 +30,7 @@ class HolidayController extends BaseController
|
||||
private readonly ResourceContext $context,
|
||||
private readonly TenantHolidayOverrideRepository $overrides,
|
||||
private readonly TenantOwnershipChecker $ownership,
|
||||
private readonly NationalHolidayRepository $nationalHolidays,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -42,8 +44,11 @@ class HolidayController extends BaseController
|
||||
|
||||
$year = $this->holidays->assertJalaliYear($request->query->get('year'));
|
||||
|
||||
[$entityType, $entityId] = $this->context->pair($user);
|
||||
$overrides = $this->holidays->overridesForYear($entityType, $entityId, $year);
|
||||
// مدیر سیستم محیط کاری ندارد و `pair()` برایش ۴۰۳ میدهد؛ ولی تقویم رسمی را او
|
||||
// نگه میدارد، پس باید ببیندش. استثنا فقط استثناهاست: کسی که محیط ندارد، ندارد.
|
||||
$overrides = $user->hasRole('ROLE_ADMIN')
|
||||
? []
|
||||
: $this->holidays->overridesForYear(...[...$this->context->pair($user), $year]);
|
||||
|
||||
$overrideByDate = [];
|
||||
foreach ($overrides as $override) {
|
||||
@@ -112,6 +117,71 @@ class HolidayController extends BaseController
|
||||
|
||||
$this->holidays->deleteOverride($override);
|
||||
|
||||
return $this->success(null);
|
||||
}
|
||||
// ── مدیر سیستم: تقویم تعطیلات رسمی ─────────────────────────────────────
|
||||
//
|
||||
// تعطیل رسمی به هیچ محیطی تعلق ندارد، پس ساختنش هم کارِ هیچ کلینیکی نیست: یک بار
|
||||
// مرکزی ثبت میشود و همهٔ محیطها از آن ارث میبرند. کلینیکی که خلافش کار میکند
|
||||
// با `POST /holiday-overrides` استثنا میزند، نه با دستکاری خودِ تقویم.
|
||||
|
||||
#[Route('/api/v1/admin/national-holidays', name: 'admin_national_holiday_create', methods: ['POST'])]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
public function createNationalHoliday(Request $request): JsonResponse
|
||||
{
|
||||
$data = json_decode($request->getContent(), true);
|
||||
|
||||
if (!is_array($data) || !is_string($data['jalali_date'] ?? null)
|
||||
|| preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $data['jalali_date'], $m) !== 1
|
||||
) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'jalali_date باید به شکل ۱۴۰۵-۰۱-۱۳ باشد', 422, 'jalali_date');
|
||||
}
|
||||
|
||||
if (!is_string($data['title'] ?? null) || trim($data['title']) === '') {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'فیلد title الزامی است', 422, 'title');
|
||||
}
|
||||
|
||||
// upsert است نه insert: ثبت روی `date` کلید یکتا دارد و دوبارهفرستادن همان
|
||||
// روز باید عنوان را عوض کند، نه با خطای خام دیتابیس بشکند.
|
||||
$holiday = $this->holidays->upsertNational((int) $m[1], (int) $m[2], (int) $m[3], trim($data['title']));
|
||||
|
||||
return $this->success($holiday->toArray(), 201);
|
||||
}
|
||||
|
||||
#[Route('/api/v1/admin/national-holiday/{uuid}', name: 'admin_national_holiday_update', methods: ['PATCH'])]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
public function updateNationalHoliday(string $uuid, Request $request): JsonResponse
|
||||
{
|
||||
$holiday = $this->nationalHolidays->findByUuid($uuid);
|
||||
|
||||
if ($holiday === null) {
|
||||
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'تعطیلی یافت نشد', 404);
|
||||
}
|
||||
|
||||
$data = json_decode($request->getContent(), true);
|
||||
|
||||
if (!is_array($data) || !is_string($data['title'] ?? null) || trim($data['title']) === '') {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'فیلد title الزامی است', 422, 'title');
|
||||
}
|
||||
|
||||
// فقط عنوان: تاریخ کلید یکتاست و «جابهجایی» یعنی یک تعطیلِ دیگر — حذف و ثبت دوباره.
|
||||
$this->holidays->renameNational($holiday, trim($data['title']));
|
||||
|
||||
return $this->success($holiday->toArray());
|
||||
}
|
||||
|
||||
#[Route('/api/v1/admin/national-holiday/{uuid}', name: 'admin_national_holiday_delete', methods: ['DELETE'])]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
public function deleteNationalHoliday(string $uuid): JsonResponse
|
||||
{
|
||||
$holiday = $this->nationalHolidays->findByUuid($uuid);
|
||||
|
||||
if ($holiday === null) {
|
||||
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'تعطیلی یافت نشد', 404);
|
||||
}
|
||||
|
||||
$this->holidays->deleteNational($holiday);
|
||||
|
||||
return $this->success(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,11 @@ class NationalHolidayRepository extends ServiceEntityRepository
|
||||
return $map;
|
||||
}
|
||||
|
||||
public function findByUuid(string $uuid): ?NationalHoliday
|
||||
{
|
||||
return $this->findOneBy(['uuid' => $uuid]);
|
||||
}
|
||||
|
||||
public function findByDate(int $date): ?NationalHoliday
|
||||
{
|
||||
return $this->findOneBy(['date' => $date]);
|
||||
|
||||
@@ -76,6 +76,20 @@ final class HolidayService
|
||||
return $holiday;
|
||||
}
|
||||
|
||||
/** تغییر عنوان یک تعطیل رسمی؛ تاریخ کلید یکتاست و اینجا دست نمیخورد. */
|
||||
public function renameNational(NationalHoliday $holiday, string $title): void
|
||||
{
|
||||
$holiday->setTitle($title);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
/** حذف یک تعطیل رسمی — مسیر پنل مدیر سیستم، در برابر `purgeYear` که کل سال را میبرد. */
|
||||
public function deleteNational(NationalHoliday $holiday): void
|
||||
{
|
||||
$this->em->remove($holiday);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* همهٔ تعطیلات یک سال را پاک میکند — تنها راه تصحیحِ ردیفی که با تاریخ غلط
|
||||
* نوشته شده، چون upsert روی `date` کلید میخورد و ردیف غلط را نمیشناسد.
|
||||
|
||||
Reference in New Issue
Block a user