repository->options();} public function report(string $section,array $input):array{ if(!in_array($section,['overview','resources','cancellations'],true))Entitlements::current()->require('statistics_advanced'); foreach(['membership_type_id','customer_type','search','payment_method']as $key)if(!empty($input[$key]))Entitlements::current()->require(in_array($key,['payment_method'],true)?'financial_reporting':'statistics_advanced'); if(($input['sort']??null)==='final_amount')Entitlements::current()->require('financial_reporting'); $f=new S\StatisticsFilter($input);$resources=$this->repository->resources($f);$summary=['booking_count'=>0,'venue_seconds'=>0,'cancelled'=>0,'no_show'=>0,'cancelled_seconds'=>0,'no_show_seconds'=>0];$rows=[];$trend=[];$details=[]; foreach($resources as $r)$rows[(int)$r['id']]=['id'=>(int)$r['id'],'name'=>$r['name'],'sport_name'=>$r['sport_name'],'venue_seconds'=>0,'booking_count'=>0,'cancelled'=>0,'no_show'=>0]; $granularity=$f->granularity();$bucket=static function(string $date)use($granularity){if($granularity==='month')return substr($date,0,7).'-01';if($granularity==='week'){$d=Time::day($date);return $d->modify('-'.(((int)$d->format('N')-(int)get_option('start_of_week',1)+7)%7).' days')->format('Y-m-d');}return $date;}; foreach($this->repository->bookings($f)as $b){if(!S\StatisticsBookingClassifier::included($b['status']))continue;$rid=(int)$b['resource_id'];$a=Time::fromSql($b['starts_at']);$z=Time::fromSql($b['ends_at']);$day=Time::timestamp($a)->format('Y-m-d'); if($a>=$f->from&&$a<$f->to){$summary['booking_count']++;$rows[$rid]['booking_count']++;if(in_array($b['status'],['cancelled','no_show'],true)){$summary[$b['status']]++;$rows[$rid][$b['status']]++;$details[]=['id'=>(int)$b['id'],'reference'=>$b['reference'],'date'=>$day,'resource_name'=>$rows[$rid]['name'],'status'=>$b['status']];}if($b['status']==='cancelled')$summary['cancelled_seconds']+=max(0,$z-$a);} $overlap=max(0,min($z,$f->to)-max($a,$f->from));if(!S\StatisticsBookingClassifier::occupied($b['status'])||!$overlap)continue;$summary['venue_seconds']+=$overlap;$rows[$rid]['venue_seconds']+=$overlap;if($b['status']==='no_show')$summary['no_show_seconds']+=$overlap; S\StatisticsUtilizationService::hourParts(max($a,$f->from),min($z,$f->to),static function($w,$h,$seconds,$date)use(&$trend,$bucket){$key=$bucket($date);$trend[$key]=($trend[$key]??0)+$seconds;}); } for($day=Time::day($f->values['date_from']);$day->format('Y-m-d')<=$f->values['date_to'];$day=Time::nextDay($day->format('Y-m-d')))$trend[$bucket($day->format('Y-m-d'))]??=0;ksort($trend); $capacity=$this->utilization->calculate($resources,$this->repository->schedule($f),$f);$available=0;$warnings=[];foreach($rows as $rid=>&$r){$r+=$capacity['resources'][$rid];$available+=$r['available_seconds'];$r['utilization']=$r['available_seconds']?$r['venue_seconds']/$r['available_seconds']*100:null;$r['availability_warning']=$r['venue_seconds']>$r['available_seconds'];if($r['availability_warning'])$warnings[]=$r['name'];}unset($r);$summary['available_seconds']=$available;$summary['utilization']=$available?$summary['venue_seconds']/$available*100:null; $page=static fn(array $items)=>['items'=>array_slice($items,($f->values['page']-1)*$f->values['per_page'],$f->values['per_page']),'total'=>count($items),'page'=>$f->values['page'],'pages'=>max(1,(int)ceil(count($items)/$f->values['per_page']))]; $result=['filters'=>$f->values,'timezone'=>wp_timezone()->getName(),'generated_at'=>wp_date('Y-m-d H:i:s'),'financial_access'=>false,'summary'=>$summary,'warnings'=>$warnings]; return $result+match($section){'resources'=>['resources'=>$page(array_values($rows))],'cancellations'=>['cancellations'=>$page($details)],default=>['trend'=>array_map(static fn($date,$seconds)=>['date'=>$date,'venue_seconds'=>$seconds],array_keys($trend),array_values($trend)),'granularity'=>$granularity,'resource_chart'=>array_map(static fn($r)=>array_intersect_key($r,array_flip(['id','name','utilization'])),array_values($rows))]}; } }