src/Repository/ReservationRepository.php line 286
<?phpnamespace App\Repository;use App\Entity\Habitat;use App\Entity\Property;use App\Entity\Reservation;use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;use Doctrine\ORM\NoResultException;use Doctrine\Persistence\ManagerRegistry;/*** @extends ServiceEntityRepository<Reservation>** @method Reservation|null find($id, $lockMode = null, $lockVersion = null)* @method Reservation|null findOneBy(array $criteria, array $orderBy = null)* @method Reservation[] findAll()* @method Reservation[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)*/class ReservationRepository extends ServiceEntityRepository{public function __construct(ManagerRegistry $registry){parent::__construct($registry, Reservation::class);}public function add(Reservation $entity, bool $flush = false): void{$this->getEntityManager()->persist($entity);if ($flush) {$this->getEntityManager()->flush();}}public function remove(Reservation $entity, bool $flush = false): void{$this->getEntityManager()->remove($entity);if ($flush) {$this->getEntityManager()->flush();}}/*** Return booked intervals for a habitat in a window (optional).** @param int $habitatId* @param \DateTimeInterface|null $from* @param \DateTimeInterface|null $to* @param int $marchandId* @param string[] $statuses* @return array<array{checkin:\DateTimeInterface, checkout:\DateTimeInterface}>*/public function findBookedRanges(int $habitatId, ?\DateTimeInterface $from = null, ?\DateTimeInterface $to = null, int $marchandId, array $statuses = []): array{$qb = $this->createQueryBuilder('r')->join('r.marchand', 'm')->join('r.habitat', 'h')->andWhere('m.id = :mid')->setParameter('mid', $marchandId)->andWhere('h.id = :hid')->setParameter('hid', $habitatId);// Optional status filter (only certain ones block dates)if (!empty($statuses)) {$qb->andWhere('r.status IN (:statuses)')->setParameter('statuses', $statuses);}// Optional window optimizationif ($from) {$qb->andWhere('r.checkOutDate > :from')->setParameter('from', $from);}if ($to) {$qb->andWhere('r.checkInDate < :to')->setParameter('to', $to);}$qb->orderBy('r.checkInDate', 'ASC');$rows = $qb->getQuery()->getResult();return array_map(function(Reservation $r) {return ['checkin' => $r->getCheckInDate(),'checkout' => $r->getCheckOutDate(),];}, $rows);}/*** True if any reservation overlaps with [checkin, checkout) for the given habitat.* Overlap rule (half-open interval):* [checkIn, checkOut) overlaps [checkin, checkout) <=> checkIn < checkout AND checkOut > checkin** @param int $habitatId* @param \DateTimeInterface $checkin inclusive* @param \DateTimeInterface $checkout exclusive* @param string[]|null $statuses null => default blocking statuses (tentative, confirmed, arrived);* [] => no status filter; custom array to override*/public function hasOverlap(int $habitatId,\DateTimeInterface $checkin,\DateTimeInterface $checkout,?array $statuses = null): bool {if ($checkout <= $checkin) {throw new \InvalidArgumentException('checkout must be strictly after checkin.');}// Normalize (use DATE at midnight if your columns are DATE)$ci = $checkin instanceof \DateTimeImmutable ? \DateTime::createFromImmutable($checkin) : (clone $checkin);$co = $checkout instanceof \DateTimeImmutable ? \DateTime::createFromImmutable($checkout) : (clone $checkout);$ci->setTime(0, 0, 0);$co->setTime(0, 0, 0);// Default statuses that block dates if none providedif ($statuses === null) {$statuses = [Reservation::STATUS_TENTATIVE,Reservation::STATUS_CONFIRMED,Reservation::STATUS_ARRIVED,];}$qb = $this->createQueryBuilder('r')->select('COUNT(r.id)')->join('r.habitat', 'h')->andWhere('h.id = :hid')// half-open overlap condition:->andWhere('r.checkInDate < :reqCheckout')->andWhere('r.checkOutDate > :reqCheckin')->setParameter('hid', $habitatId)->setParameter('reqCheckin', $ci)->setParameter('reqCheckout', $co);// Optional status filterif ($statuses !== []) {$qb->andWhere('r.status IN (:statuses)')->setParameter('statuses', $statuses);}$count = (int) $qb->getQuery()->getSingleScalarResult();return $count > 0;}public function listData(array $filters, int $marchandId): array{$from = $filters['from'] ?? null; // \DateTimeInterface|null$to = $filters['to'] ?? null; // \DateTimeInterface|null$status = $filters['status'] ?? 'all'; // string$termFilter = $filters['term'] ?? 'all'; // 'all'|'night'|'week'|'month'$propertyId = $filters['propertyId'] ?? null; // int|null// Base QB$qb = $this->createQueryBuilder('r')->join('r.marchand', 'm')->leftJoin('r.habitat', 'h')->leftJoin('r.locataire', 't')->addSelect('h.libelle AS property')->addSelect("CONCAT(COALESCE(t.name,''),' ',COALESCE(t.lastname,'')) AS locataire")// include term and totalAmount explicitly->addSelect('r.id, r.code, r.status, r.term, r.checkInDate, r.checkOutDate, r.totalAmount')->andWhere('m.id = :mid')->setParameter('mid', $marchandId);// Filtersif ($from instanceof \DateTimeInterface) {$from0 = $from instanceof \DateTimeImmutable ? $from : \DateTimeImmutable::createFromMutable($from);$qb->andWhere('r.checkInDate >= :from')->setParameter('from', $from0->setTime(0,0));}if ($to instanceof \DateTimeInterface) {$to2359 = $to instanceof \DateTimeImmutable ? $to : \DateTimeImmutable::createFromMutable($to);$qb->andWhere('r.checkOutDate <= :to')->setParameter('to', $to2359->setTime(23,59,59));}if ($status !== 'all') {$qb->andWhere('r.status = :st')->setParameter('st', $status);}if ($termFilter !== 'all') {$qb->andWhere('r.term = :tm')->setParameter('tm', $termFilter);}if ($propertyId) {$qb->andWhere('h.id = :pid')->setParameter('pid', $propertyId);}$rows = $qb->orderBy('r.checkInDate', 'DESC')->getQuery()->getArrayResult();// Helpers$toImmutable = static function ($v): \DateTimeImmutable {if ($v instanceof \DateTimeImmutable) return $v;if ($v instanceof \DateTime) return \DateTimeImmutable::createFromMutable($v);return new \DateTimeImmutable((string)$v);};// Compute deriveds$active = 0;$upcoming = 0;$revenue = 0.0;$count = count($rows);$today = new \DateTimeImmutable('today');$revenueStatuses = ['confirmed', 'confirmée', 'completed'];foreach ($rows as &$r) {$ci = $toImmutable($r['checkInDate'] ?? null);$co = $toImmutable($r['checkOutDate'] ?? null);$r['nights'] = max(0, (int)$co->diff($ci)->format('%a'));if ($ci <= $today && $today < $co) {$active++;}if ($ci > $today && $ci <= $today->modify('+7 days')) {$upcoming++;}if (in_array(($r['status'] ?? ''), $revenueStatuses, true)) {$revenue += (float)($r['totalAmount'] ?? 0);}}unset($r);// Occupancy$occupancyRate = $count > 0 ? min(100.0, ($active / max(1, $count)) * 100.0) : 0.0;// Series (last 12 months)$seriesReservations = [];$seriesRevenue = [];$now = new \DateTimeImmutable('first day of this month');for ($i = 11; $i >= 0; $i--) {$m = $now->modify("-{$i} months");$label = $m->format('Y-m');$seriesReservations[$label] = 0;$seriesRevenue[$label] = 0.0;}foreach ($rows as $r) {$ci = $toImmutable($r['checkInDate'] ?? null);$label = $ci->format('Y-m');if (isset($seriesReservations[$label])) {$seriesReservations[$label]++;if (in_array(($r['status'] ?? ''), $revenueStatuses, true)) {$seriesRevenue[$label] += (float)($r['totalAmount'] ?? 0);}}}return ['totals' => ['count' => $count,'active' => $active,'upcoming' => $upcoming,'revenue' => round($revenue, 2),'occupancyRate' => $occupancyRate,],'table' => array_map(static function(array $r) {return ['id' => $r['id'],'code' => $r['code'],'status' => $r['status'],'term' => $r['term'],'checkInDate' => $r['checkInDate'],'checkOutDate'=> $r['checkOutDate'],'nights' => $r['nights'] ?? 0,'total' => $r['totalAmount'] ?? 0,'property' => $r['property'] ?? '','locataire' => $r['locataire'] ?? '',];}, $rows),'series' => ['reservations' => array_map(fn($k,$v)=>['label'=>$k,'value'=>$v],array_keys($seriesReservations),array_values($seriesReservations)),'revenue' => array_map(fn($k,$v)=>['label'=>$k,'value'=>round($v,2)],array_keys($seriesRevenue),array_values($seriesRevenue)),],];}public function dashboardData(array $filters, int $totalActiveUnits = 0, int $marchandId): array{$from = $filters['from'] ?? null; // \DateTimeInterface|null$to = $filters['to'] ?? null; // \DateTimeInterface|null$status = $filters['status'] ?? 'all'; // 'all' or a status$termFilter = $filters['term'] ?? 'all'; // 'all'|'night'|'week'|'month'$propertyId = $filters['propertyId'] ?? null; // int|null$today = new \DateTimeImmutable('today');$tomorrow = $today->modify('+1 day');$in7 = $today->modify('+7 days');// ---------- Base rows for the main table (respecting filters) ----------$qb = $this->createQueryBuilder('r')->join('r.marchand', 'm')->leftJoin('r.habitat', 'h')->leftJoin('r.locataire', 't')->addSelect('h.libelle AS property')->andWhere('m.id = :mid')->setParameter('mid', $marchandId)->addSelect("CONCAT(COALESCE(t.name,''),' ',COALESCE(t.lastname,'')) AS tenant")->addSelect('r.id, r.code, r.status, r.term, r.checkInDate, r.checkOutDate, r.totalAmount');if ($from) { $qb->andWhere('r.checkInDate >= :from')->setParameter('from', (clone $from)->setTime(0,0)); }if ($to) { $qb->andWhere('r.checkOutDate <= :to')->setParameter('to', (clone $to)->setTime(23,59,59)); }if ($status !== 'all') { $qb->andWhere('r.status = :st')->setParameter('st', $status); }if ($termFilter !== 'all') { $qb->andWhere('r.term = :tm')->setParameter('tm', $termFilter); }if ($propertyId) { $qb->andWhere('h.id = :pid')->setParameter('pid', (int) $propertyId); }if ($marchandId) { $qb->andWhere('h.marchand = :mid')->setParameter('mid', (int) $marchandId); }$rows = $qb->orderBy('r.checkInDate', 'DESC')->getQuery()->getArrayResult();// ---------- KPIs + series ----------$totalReservations = \count($rows);$activeNow = 0;$upcomingCheckins = 0;$revenue = 0.0;// Lists$listCurrent = []; // stays currently in-house$listTodayCI = []; // check-ins today$toImmutable = static fn($v): \DateTimeImmutable =>$v instanceof \DateTimeImmutable ? $v :($v instanceof \DateTime ? \DateTimeImmutable::createFromMutable($v) : new \DateTimeImmutable((string)$v));foreach ($rows as &$r) {$ci = $toImmutable($r['checkInDate']);$co = $toImmutable($r['checkOutDate']);// Active now (checkIn <= today < checkOut) and not canceledif ($ci <= $today && $today < $co && !in_array(($r['status'] ?? ''), ['canceled','annulée'], true)) {$activeNow++;$listCurrent[] = ['code' => $r['code'],'property' => $r['property'],'tenant' => $r['tenant'],'from' => $ci,'to' => $co,'status' => $r['status'],];}// Upcoming arrivals in next 7 days (excluding today)if ($ci >= $tomorrow && $ci <= $in7 && !in_array(($r['status'] ?? ''), ['canceled','annulée'], true)) {$upcomingCheckins++;}// Today check-ins listif ($ci->format('Y-m-d') === $today->format('Y-m-d') && !in_array(($r['status'] ?? ''), ['canceled','annulée'], true)) {$listTodayCI[] = ['code' => $r['code'],'property' => $r['property'],'tenant' => $r['tenant'],'from' => $ci,'to' => $co,'status' => $r['status'],];}// Revenue (confirmed/arrived/departed i.e., realized)if (in_array(($r['status'] ?? ''), ['confirmed','confirmée','arrived','arrivée','departed','parti'], true)) {$revenue += (float)($r['totalAmount'] ?? 0);}// Derived nights for table$r['nights'] = max(0, (int)$co->diff($ci)->format('%a'));$r['checkin'] = $ci;$r['checkout'] = $co;}unset($r);// Current occupancy rate$occupancyRate = 0.0;if ($totalActiveUnits > 0) {$occupancyRate = min(100.0, ($activeNow / $totalActiveUnits) * 100.0);}// Series for last 12 months (reservations count + revenue)$seriesReservations = [];$seriesRevenue = [];$nowFirst = new \DateTimeImmutable('first day of this month');for ($i = 11; $i >= 0; $i--) {$m = $nowFirst->modify("-{$i} months")->format('Y-m');$seriesReservations[$m] = 0;$seriesRevenue[$m] = 0.0;}foreach ($rows as $r) {$label = $toImmutable($r['checkInDate'])->format('Y-m');if (isset($seriesReservations[$label])) {$seriesReservations[$label]++;if (in_array(($r['status'] ?? ''), ['confirmed','confirmée','arrived','arrivée','departed','parti'], true)) {$seriesRevenue[$label] += (float)($r['totalAmount'] ?? 0);}}}return ['stats' => ['total' => $totalReservations,'active' => $activeNow,'upcomingCheckins' => $upcomingCheckins,'occupancyRate' => round($occupancyRate, 1),'revenue' => round($revenue, 2),'currencySymbol' => $filters['currencySymbol'] ?? '$',],'series' => ['reservations' => array_map(fn($k,$v)=>['label'=>$k,'value'=>$v], array_keys($seriesReservations), array_values($seriesReservations)),'revenue' => array_map(fn($k,$v)=>['label'=>$k,'value'=>round($v,2)], array_keys($seriesRevenue), array_values($seriesRevenue)),],// Keep your previous table shape but normalize field names a bit'table' => ['rows' => array_map(static function(array $r) {return ['code' => $r['code'],'property' => $r['property'],'tenant' => $r['tenant'],'checkin' => $r['checkin'],'checkout' => $r['checkout'],'term' => $r['term'],'nights' => $r['nights'],'total' => (float)($r['totalAmount'] ?? 0),'status' => $r['status'],];}, $rows),'currencySymbol' => $filters['currencySymbol'] ?? '$',],'lists' => ['current' => $listCurrent,'todayCheckins' => $listTodayCI,],];}/*** Returns true if $habitat has NO non-canceled reservations overlapping [from, to).** Overlap rule (half-open interval):* [checkIn, checkOut) overlaps [from, to) <=> checkIn < to AND checkOut > from** @param Habitat $habitat* @param \DateTimeInterface $from inclusive (start of stay)* @param \DateTimeInterface $to exclusive (end of stay)* @param int|null $excludeReservationId reservation ID to ignore (e.g. when modifying/moving)*/public function isHabitatAvailable(Habitat $habitat,\DateTimeInterface $from,\DateTimeInterface $to,?int $excludeReservationId = null, int $marchandId): bool {// Basic guardif ($to <= $from) {throw new \InvalidArgumentException('The "to" date must be strictly after the "from" date.');}// Normalize to mutable DateTime for DBAL (columns are DATE, but DateTime works fine)$fromDT = $from instanceof \DateTimeImmutable ? \DateTime::createFromImmutable($from) : clone $from;$toDT = $to instanceof \DateTimeImmutable ? \DateTime::createFromImmutable($to) : clone $to;// If you store DATE (no time), setting time to midnight is harmless/explicit$fromDT->setTime(0, 0, 0);$toDT->setTime(0, 0, 0);$qb = $this->createQueryBuilder('r')->select('COUNT(r.id)')->join('r.marchand', 'm')->where('r.habitat = :habitat')->andWhere('m.id = :mid')->setParameter('mid', $marchandId)// ignore canceled reservations->andWhere('r.status <> :canceled')// half-open overlap condition:->andWhere('r.checkInDate < :to')->andWhere('r.checkOutDate > :from')->setParameter('habitat', $habitat)->setParameter('canceled', Reservation::STATUS_CANCELED)->setParameter('from', $fromDT)->setParameter('to', $toDT);if ($excludeReservationId !== null) {$qb->andWhere('r.id <> :excludeId')->setParameter('excludeId', $excludeReservationId);}$conflicts = (int) $qb->getQuery()->getSingleScalarResult();return $conflicts === 0;}public function loadReservationsForPeriodForSingleAppartment($startDate, $period, Habitat $property){$start = date('Y-m-d', $startDate);$end = date('Y-m-d', $startDate + ($period * 3600 * 24));$q = $this->createQueryBuilder('u')->select('u')->where('u.habitat = :app ')->andWhere('((u.checkInDate >= :start AND u.checkOutDate <= :end) OR'.'(u.checkInDate < :start AND u.checkOutDate >= :start) OR'.'(u.checkInDate <= :end AND u.checkOutDate > :end) OR'.'(u.checkInDate < :start AND u.checkOutDate > :end))')// ->andWhere('u.confirmed = :confirmed')->setParameter('start', $start)->setParameter('end', $end)->setParameter('app', $property)// ->setParameter('confirmed', true)->addOrderBy('u.checkOutDate', 'ASC')->getQuery();$reservations = null;try {$reservations = $q->getResult();} catch (NoResultException $e) {}return $reservations;}}