E164PhoneNumberCast::class . ':FR' ]; /** * Map attribute names to enum classes. * * @var array */ protected $enumCasts = [ 'country' => Country::class ]; /** * The model's attributes. * * @var array */ protected $attributes = [ 'phone_country' => 'FR' ]; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'name', 'address', 'postal_code', 'city', 'country', 'phone', 'gmaps_embed_href', ]; /** * Perform any actions required after the model boots. * * @return void */ protected static function booted() { // * EVENT CLOSURES. static::created(function (self $model) { // * CREATE ASSOCIATED MODELS foreach (WeekDay::toArray() as $weekday) { $model->addTimeSlotSettingFor($weekday); } }); static::retrieved(function (self $model) { /** @var \Illuminate\Support\Collection<\App\Enums\TimeSlot\WeekDay> */ $existingTimeslotDays = $model->timeslotSettings->pluck('day')->keyBy(function ($enum) { return $enum->value; }); foreach (WeekDay::toArray() as $weekday) { // * If day is missing. if (!$existingTimeslotDays->has($weekday->value)) { $model->addTimeSlotSettingFor($weekday); } } }); } // * RELATIONSHIPS /** * hasMany timeslot settings relation * * @return \Illuminate\Database\Eloquent\Relations\HasMany */ public function timeslotSettings() { return $this->hasMany(TimeslotSetting::class); } /** * hasMany collection exceptions relation * * @return \Illuminate\Database\Eloquent\Relations\HasMany */ public function collectionExceptions() { return $this->hasMany(CollectionException::class); } /** * hasMany timeslots relation * * @return \Illuminate\Database\Eloquent\Relations\HasMany */ public function timeslots() { return $this->hasMany(Timeslot::class); } // * METHODS /** * Check if creneau is available * * @param \Carbon\Carbon $creneau * @return boolean */ public function isCreneauAvailable(Carbon $creneau): bool { $days_to_come = Carbon::now()->diffInDays($creneau) + 1; $creneaux = $this->getAvailableCreneaux($days_to_come); return $creneaux[$creneau->format('d/m/Y')][$creneau->format('H:i')] ?? false; } /** * Récupérer tout les créneaux disponible à partir de * maintenant pour un magasin sur les jours à venir. * * @param integer $days_to_come Sur quelle période proposer des créneaux à partir de maintenant. * @return array * @throws \RuntimeException Si la période demandé est incohérente. */ public function getAvailableCreneaux(int $days_to_come = 1): array { if ($days_to_come < 1) { throw new \RuntimeException('Impossible de proposer des créneaux avec une période inférieure a 1 jour.'); } /** @var integer $days_to_come */ $days_to_come = \min($days_to_come, 15); /** @var \Illuminate\Database\Eloquent\Collection<\App\Models\Shop\TimeslotSetting> */ $timeslotSettings = $this->timeslotSettings; $plage = []; foreach ($timeslotSettings as $timeslot) { $timeslotSetting = (object)(new TimeslotSettingResource($timeslot))->toArray(request()); $plage[$timeslot->day->value] = $timeslotSetting; } // * Génération des plages. /** @var \Carbon\CarbonImmutable $today Le jour de la semaine actuelle a minuit. */ $today = CarbonImmutable::now()->startOfDay(); /** @var \Carbon\CarbonPeriod La période pour laquelle il faut fournir les crénaux. */ $period = CarbonPeriod::dates($today, $today->add($days_to_come, 'day')); /** @var \Illuminate\Database\Eloquent\Collection<\Carbon\Carbon> Jours de fermeture exceptionnels.*/ $noCollectDays = $this->collectionExceptions() ->whereDate('date', '>=', $period->start) ->whereDate('date', '<=', $period->end) ->orderBy('date')->pluck('date')->keyBy(function (Carbon $date) { return $date->format('d/m/Y'); }); $availCreneaux = []; // * Délais de préparation minimal pour aujourd'hui (à l'heure de maintenant). /** @var string $min_order_preparation A string like "hh:ii". Durée de préparation de commande */ $min_order_preparation = $plage[$today->weekday() ?: 7]->min_order_preparation; /** @var \Carbon\CarbonInterval $min_order_preparation_carbon Heures minute de délais de préparation */ $min_order_preparation_carbon = CarbonInterval::createFromFormat( 'h:i', $min_order_preparation )->addHours(now()->format('H'))->addMinutes(now()->format('i')); /** @var \Carbon\CarbonInterface|null $day_to_consider Le jour pour lequel il faut obtenir les créneaux. */ foreach ($period->days() as $i => $day_to_consider) { // * Remove a day on `min_order_preparation_carbon` if ($i and $min_order_preparation_carbon->totalMinutes) { $min_order_preparation_carbon->subDays(1); } // * Convert carbon weekday to FR /** @var integer */ $day_to_consider_week_day = $day_to_consider->weekday() ?: 7; // * Ignorer jours de fermeture. if ($noCollectDays->has($day_to_consider->format('d/m/Y'))) { continue; } /** @var integer|null $max_orders_per_interval . Nb de réservations maximum par créneau */ $max_orders_per_interval = $plage[$day_to_consider_week_day]->max_orders_per_interval; /** @var \Carbon\CarbonInterval $baseInterval Durée d'un créneau */ $baseInterval = CarbonInterval::createFromFormat('h:i', $plage[$day_to_consider_week_day]->interval); $availCreneaux[$day_to_consider->format('d/m/Y')] = []; foreach (['morning', 'afternoon'] as $half_day_period) { if (!empty($plage[$day_to_consider_week_day]->{"{$half_day_period}_start"})) { /** @var \Carbon\Carbon Début de la plage de demi journée */ $start_half_day = Carbon::createFromTimeString( $plage[$day_to_consider_week_day]->{"{$half_day_period}_start"} ); /** @var \Carbon\Carbon Fin de la plage de demi journée */ $end_half_day = Carbon::createFromTimeString( $plage[$day_to_consider_week_day]->{"{$half_day_period}_end"} ); /** @var \Carbon\CarbonPeriod La période de demi journée a considérer */ $half_day_period = CarbonPeriod::dates($start_half_day, $end_half_day); /** @var array $crenaux Les créneaux et leur disponibilité. */ $crenaux = $this->addCreneaux( CarbonImmutable::parse($day_to_consider), $half_day_period, $min_order_preparation_carbon, $max_orders_per_interval, $baseInterval ); $availCreneaux[$day_to_consider->format('d/m/Y')] = array_merge( $availCreneaux[$day_to_consider->format('d/m/Y')], $crenaux ); } //end if } //end foreach } //end foreach return $availCreneaux; } /** * Calcule les disponibilités de créneaux d'un magasin * pour une période et un jour donné. * * @param \Carbon\CarbonImmutable $actual_day Le jour à considérer. * @param \Carbon\CarbonPeriod $half_day_period La période de demi journée à considérer. * @param \Carbon\CarbonInterval $min_order_preparation Durée de préparation d'une commande. * @param integer|null $max_orders_per_interval Nombre de commandes max par intervalles. * @param \Carbon\CarbonInterval $interval_duration La durée d'un intervalle. * @return array Tableau avec en index l'heure minute et une valeur booléenne de disponibilité. */ private function addCreneaux( CarbonImmutable $actual_day, CarbonPeriod $half_day_period, CarbonInterval $min_order_preparation, $max_orders_per_interval, CarbonInterval $interval_duration ): array { /** @var array */ $out = []; foreach ($half_day_period->interval($interval_duration) as $creneau) { /** @var integer Le nombre de créneaux déja réservés */ $nbtakens = $this->timeslots() ->where('slot', Carbon::createFromFormat( 'd/m/Y H:i', "{$actual_day->format('d/m/Y')} {$creneau->format('H:i')}" ))->count(); // * Le créneau est-il disponible ? $is_dispo = true; if (!\is_null($max_orders_per_interval)) { $is_dispo = $nbtakens <= $max_orders_per_interval; } // * Délais minimal avant qu'ils soit possible de proposer un créneau. $minimal_prepartion_offset_time = $actual_day->setHour(0)->setMinute(0)->add($min_order_preparation); // * Le créneaux n'est pas disponible si il dépasse le temp minimal de préparation à partir de maintenant. $is_dispo = $minimal_prepartion_offset_time->isBefore( $actual_day ->addHours($creneau->format('H')) ->addMinutes($creneau->format('i')) ); $out[$creneau->format('H:i')] = $is_dispo; } //end foreach return $out; } /** * Create a Timeslot Setting for a specific model and day. * * @param \App\Enums\TimeSlot\WeekDay $weekDay * @return void */ public function addTimeSlotSettingFor(WeekDay $weekDay): void { $dayset = new TimeslotSetting(); $dayset->day = $weekDay; $dayset->shop()->associate($this); $dayset->saveOrFail(); } }