getAddress()->getTimezone()); $startDay = $this->midnight($from, $timezone); $endDay = $this->midnight($to, $timezone); // شیفت‌ها و ساعت شعبه یک بار خوانده می‌شوند، نه per روز. $shiftsByDay = $this->shiftsByDay($resource); $holidayMap = $this->holidays->mapForRange($startDay, $endDay); $overrideMap = $this->overrides->mapForRange( $resource->getEntityType(), $resource->getEntityId(), $startDay, $endDay, ); $exceptions = $this->exceptions->findOverlapping( $resource, $startDay, $endDay + self::DAY_SECONDS, ); $days = []; for ($day = $startDay; $day <= $endDay; $day = $this->nextMidnight($day, $timezone)) { $days[] = $this->buildDay( $resource, $day, $timezone, $shiftsByDay, $holidayMap, $overrideMap, $exceptions, ); } return $days; } /** * همان `rawAvailability` برای چند منبع، ولی با خواندنِ دسته‌ای. * * تعطیلات و استثناهای محیط و ساعت شعبه برای همهٔ منابع یکی‌اند و بیرون حلقه خوانده * می‌شوند؛ شیفت و استثنای هر منبع هم با یک کوئری برای همه می‌آید. بدون این، گزارشِ * چهل منبع دویست کوئری می‌زد. * * @param ClinicResource[] $resources همهٔ آن‌ها باید یک شعبه داشته باشند * @return array> کلید: شناسهٔ منبع */ public function rawAvailabilityForAll(array $resources, int $from, int $to): array { if ($resources === []) { return []; } $first = $resources[array_key_first($resources)]; $timezone = new \DateTimeZone($first->getAddress()->getTimezone()); $startDay = $this->midnight($from, $timezone); $endDay = $this->midnight($to, $timezone); $holidayMap = $this->holidays->mapForRange($startDay, $endDay); $overrideMap = $this->overrides->mapForRange( $first->getEntityType(), $first->getEntityId(), $startDay, $endDay, ); $ids = array_map(static fn (ClinicResource $r): int => (int) $r->getId(), $resources); $shiftsById = $this->calendars->findForResources($ids); $exceptionsById = $this->exceptions->findOverlappingForResources($ids, $startDay, $endDay + self::DAY_SECONDS); $out = []; foreach ($resources as $resource) { $id = (int) $resource->getId(); $byDay = []; foreach ($shiftsById[$id] ?? [] as $shift) { if ($shift->isActive()) { $byDay[$shift->getDayOfWeek()][] = new TimeInterval($shift->getStartMinute(), $shift->getEndMinute()); } } $shiftsByDay = array_map(TimeInterval::mergeAll(...), $byDay); $days = []; for ($day = $startDay; $day <= $endDay; $day = $this->nextMidnight($day, $timezone)) { $days[] = $this->buildDay( $resource, $day, $timezone, $shiftsByDay, $holidayMap, $overrideMap, $exceptionsById[$id] ?? [], ); } $out[$id] = $days; } return $out; } /** * @param array> $shiftsByDay * @param array $holidayMap * @param array $overrideMap * @param ResourceException[] $exceptions */ private function buildDay( ClinicResource $resource, int $midnight, \DateTimeZone $timezone, array $shiftsByDay, array $holidayMap, array $overrideMap, array $exceptions, ): DayAvailability { $reasons = []; $dayOfWeek = $this->dayOfWeek($midnight, $timezone); if (!$resource->isActive()) { return new DayAvailability($midnight, $dayOfWeek, [], ['resource_inactive']); } /** * فعال‌بودنِ آدرس اینجا سنجیده **نمی‌شود**. * * منابع دامنهٔ شعبه ندارند: آدرس فقط حاملِ منطقهٔ زمانی و جفتِ محیط است و هیچ * جای پنل هم روشن/خاموشش نمی‌کند. وقتی می‌شد، یک ردیفِ قدیمیِ `active = 0` کلِ * دستگاه‌های کلینیک را با پیامی خاموش می‌کرد که کاربر راهی برای رفعش نداشت. */ $override = $overrideMap[$midnight] ?? null; $holiday = $holidayMap[$midnight] ?? null; // استثنای محیط بر تقویم رسمی مقدم است — در هر دو جهت. $closedByHoliday = $override !== null ? !$override->isWorking() : $holiday !== null; if ($closedByHoliday) { $reasons[] = $override !== null ? 'tenant_holiday' : 'national_holiday'; return new DayAvailability($midnight, $dayOfWeek, [], $reasons); } $shifts = $shiftsByDay[$dayOfWeek] ?? []; if ($shifts === []) { return new DayAvailability($midnight, $dayOfWeek, [], ['no_shift']); } $absolute = array_map( static fn (TimeInterval $i): TimeInterval => $i->minutesToAbsolute($midnight), $shifts, ); $blocking = []; foreach ($exceptions as $exception) { if ($exception->getStartsAt() < $this->endOfDay($midnight, $timezone) && $exception->getEndsAt() > $midnight ) { $blocking[] = new TimeInterval($exception->getStartsAt(), $exception->getEndsAt()); } } if ($blocking !== []) { $before = $absolute; $absolute = TimeInterval::subtractAll($absolute, $blocking); if ($absolute !== $before) { $reasons[] = 'exception'; } } return new DayAvailability($midnight, $dayOfWeek, $absolute, $reasons); } /** @return array> روز هفته => بازه‌های دقیقه‌ای */ private function shiftsByDay(ClinicResource $resource): array { $byDay = []; foreach ($this->calendars->findForResource($resource) as $shift) { if (!$shift->isActive()) { continue; } $byDay[$shift->getDayOfWeek()][] = new TimeInterval($shift->getStartMinute(), $shift->getEndMinute()); } return array_map(TimeInterval::mergeAll(...), $byDay); } /** ۰=شنبه … ۶=جمعه — همان قرارداد بقیهٔ سامانه، نه `w` استاندارد PHP. */ public function dayOfWeek(int $timestamp, \DateTimeZone $timezone): int { $date = (new \DateTimeImmutable('@' . $timestamp))->setTimezone($timezone); return ((int) $date->format('w') + 1) % 7; } public function midnight(int $timestamp, \DateTimeZone $timezone): int { return (new \DateTimeImmutable('@' . $timestamp)) ->setTimezone($timezone) ->setTime(0, 0) ->getTimestamp(); } /** * روز بعد از روی تقویم گرفته می‌شود نه با `+86400`: در منطقه‌هایی که ساعت تابستانی * دارند، روز ۲۳ یا ۲۵ ساعته می‌شود و جمعِ ثابت، نیمه‌شب را جابه‌جا می‌کند. */ private function nextMidnight(int $midnight, \DateTimeZone $timezone): int { return (new \DateTimeImmutable('@' . $midnight)) ->setTimezone($timezone) ->modify('+1 day') ->setTime(0, 0) ->getTimestamp(); } private function endOfDay(int $midnight, \DateTimeZone $timezone): int { return $this->nextMidnight($midnight, $timezone); } }