intervalsFromRows($resource_id, $date, $this->hours->forDay($resource_id, (int) Time::day($date)?->format('N')), $this->exceptions->forDate($resource_id, $date)); } /** The same scheduling rules for batched reports; no per-resource/day SQL. */ public function intervalsFromRows(int $resource_id, string $date, array $hours, array $exceptions): array { $day = Time::day( $date ); if ( ! $day ) { return []; } $next_day = Time::nextDay( $date ); if ( ! $next_day ) { return []; } $rows = $this->scope( $exceptions, $resource_id ); $is_exception = (bool) $rows; if ( ! $rows ) { $rows = $this->scope( $hours, $resource_id ); } foreach ( $rows as $row ) { if ( $row['is_closed'] ) { return []; } } $intervals = []; foreach ( $rows as $row ) { if ( ! $is_exception && 'sun' === $row['opening_mode'] ) { $events = $this->sun->events( $date ); if ( ! $events ) { continue; } $start = $events['sunrise'] + ( $row['sunrise_offset_minutes'] ?? 0 ) * 60; $end = $events['sunset'] + ( $row['sunset_offset_minutes'] ?? 0 ) * 60; foreach ( [ 'min_open_time', 'max_close_time' ] as $field ) { if ( null === $row[ $field ] ) { continue; } $limit = Time::local( $date, $row[ $field ] ); if ( ! $limit ) { $end = $start; break; } if ( 'min_open_time' === $field ) { $start = max( $start, $limit->getTimestamp() ); } else { $end = min( $end, $limit->getTimestamp() ); } } } else { $open = '00:00:00' === ( $row['opens_at'] ?? null ) ? $day : Time::local( $date, $row['opens_at'] ?? '' ); $close = Time::local( $date, $row['closes_at'] ?? '' ); if ( ! $open || ! $close ) { continue; } $start = $open->getTimestamp(); $end = $close->getTimestamp(); } $start = max( $start, $day->getTimestamp() ); $end = min( $end, $next_day->getTimestamp() ); if ( $start < $end ) { $intervals[] = [ $start, $end ]; } } usort( $intervals, static fn( $a, $b ) => $a[0] <=> $b[0] ); $merged = []; foreach ( $intervals as $interval ) { $last = count( $merged ) - 1; if ( $last >= 0 && $interval[0] <= $merged[ $last ][1] ) { $merged[ $last ][1] = max( $merged[ $last ][1], $interval[1] ); } else { $merged[] = $interval; } } return $merged; } public function publicTitle(int $resource_id,string $date):?string { $rows=$this->scope($this->exceptions->forDate($resource_id,$date),$resource_id);$titles=array_values(array_unique(array_filter(array_column($rows,'public_title'))));return $titles?implode(' ยท ',$titles):null; } private function scope( array $rows, int $resource_id ): array { $specific = array_values( array_filter( $rows, static fn( $row ) => $row['resource_id'] === $resource_id ) ); return $specific ?: array_values( array_filter( $rows, static fn( $row ) => null === $row['resource_id'] ) ); } }