denyUnlessGranted($user, 'view'); $resource = $this->context->resource($user, $uuid); return $this->success([ 'resource_uuid' => $resource->getUuid(), 'timezone' => $resource->getAddress()->getTimezone(), 'defined' => $this->calendar->isDefined($resource), 'days' => (object) $this->calendar->read($resource), ]); } /** جایگزینی کامل هفت روز؛ آرایهٔ خالی یعنی منبع هیچ شیفتی ندارد. */ #[Route('/api/v1/resource/{uuid}/calendar', name: 'resource_calendar_replace', methods: ['PUT'])] public function replace(#[CurrentUser] User $user, string $uuid, Request $request): JsonResponse { $this->denyUnlessGranted($user, 'update'); $data = json_decode($request->getContent(), true); if (!is_array($data) || !is_array($data['days'] ?? null)) { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'فیلد days الزامی است', 422, 'days'); } $resource = $this->context->resource($user, $uuid); $days = $this->calendar->replace($resource, $data['days']); return $this->success([ 'resource_uuid' => $resource->getUuid(), 'timezone' => $resource->getAddress()->getTimezone(), 'defined' => $days !== array_fill_keys(ResourceCalendarService::DAYS, []), 'days' => (object) $days, ]); } #[Route('/api/v1/resource/{uuid}/exceptions', name: 'resource_exceptions_list', methods: ['GET'])] public function listExceptions(#[CurrentUser] User $user, string $uuid, Request $request): JsonResponse { $this->denyUnlessGranted($user, 'view'); $resource = $this->context->resource($user, $uuid); $from = $request->query->get('from'); $to = $request->query->get('to'); // بدون بازه، همهٔ استثناها از ابتدای زمان تا انتهای آن — عمداً محدود نمی‌شود // چون فهرست مرخصیِ یک منبع کوچک است و صفحه‌بندی‌اش سود ندارد. $rows = $this->exceptions->findOverlapping( $resource, is_numeric($from) ? (int) $from : 0, is_numeric($to) ? (int) $to : PHP_INT_MAX, ); return $this->success(array_map( static fn (ResourceException $e): array => $e->toArray(), $rows, )); } #[Route('/api/v1/resource/{uuid}/exception', name: 'resource_exception_create', methods: ['POST'])] public function createException(#[CurrentUser] User $user, string $uuid, Request $request): JsonResponse { $this->denyUnlessGranted($user, 'update'); $data = json_decode($request->getContent(), true); if (!is_array($data)) { return $this->error(ErrorCodes::ERR_VALIDATION_001, 'بدنهٔ درخواست نامعتبر است', 422); } $resource = $this->context->resource($user, $uuid); $exception = $this->calendar->createException($resource, $data); return $this->success($exception->toArray(), 201); } #[Route('/api/v1/resource-exception/{uuid}', name: 'resource_exception_update', methods: ['PATCH'])] public function updateException(#[CurrentUser] User $user, string $uuid, Request $request): JsonResponse { $this->denyUnlessGranted($user, 'update'); $data = json_decode($request->getContent(), true); if (!is_array($data)) { return $this->error(ErrorCodes::ERR_VALIDATION_001, 'بدنهٔ درخواست نامعتبر است', 422); } return $this->success($this->calendar->updateException($this->requireException($user, $uuid), $data)->toArray()); } #[Route('/api/v1/resource-exception/{uuid}', name: 'resource_exception_delete', methods: ['DELETE'])] public function deleteException(#[CurrentUser] User $user, string $uuid): JsonResponse { $this->denyUnlessGranted($user, 'update'); $this->calendar->deleteException($this->requireException($user, $uuid)); return $this->success(null); } /** * ساعت آزاد **خام**: ساعت شعبه ∩ شیفت منبع − تعطیلات − استثناها. * نوبت‌های ثبت‌شده اینجا کسر نمی‌شوند — آن کارِ تسک ۰۶/۰۷ است. */ #[Route('/api/v1/resource/{uuid}/availability', name: 'resource_availability', methods: ['GET'])] public function availabilityFor(#[CurrentUser] User $user, string $uuid, Request $request): JsonResponse { $this->denyUnlessGranted($user, 'view'); $resource = $this->context->resource($user, $uuid); $from = $request->query->get('from'); $to = $request->query->get('to'); if (!is_numeric($from) || !is_numeric($to)) { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'پارامترهای from و to الزامی‌اند', 422, 'from'); } if ((int) $to < (int) $from) { return $this->error(ErrorCodes::ERR_VALIDATION_001, 'to باید بعد از from باشد', 422, 'to'); } $days = intdiv((int) $to - (int) $from, ResourceAvailabilityService::DAY_SECONDS) + 1; if ($days > ResourceAvailabilityService::MAX_DAYS) { return $this->error( ErrorCodes::ERR_VALIDATION_001, sprintf('بازهٔ درخواستی حداکثر %d روز است', ResourceAvailabilityService::MAX_DAYS), 422, 'to', ); } return $this->success([ 'resource_uuid' => $resource->getUuid(), 'timezone' => $resource->getAddress()->getTimezone(), 'days' => array_map( static fn (DayAvailability $d): array => $d->toArray(), $this->availability->rawAvailability($resource, (int) $from, (int) $to), ), ]); } private function requireException(User $user, string $uuid): ResourceException { $exception = $this->exceptions->findByUuid($uuid); [$entityType, $entityId] = $this->context->pair($user); if ($exception === null || !$this->ownership->belongsToPair($entityType, $entityId, $exception)) { throw new AppException(ErrorCodes::ERR_NOT_FOUND_001, 'استثنا یافت نشد', 404); } return $exception; } }