feat(appointment): public month-availability endpoint

Add GET /api/v1/appointment-settings/month-availability/{doctorUuid}
?year=&month= (Gregorian) returning disabled_dates / enabled_dates for
the month plus the doctor's online_booking flag and window. Lives in
AppointmentController (per-method guards) so it is genuinely public —
the class-level IsGranted on AppointmentSettingsController would have
forced auth. Whitelisted in security.yaml (firewall + access_control).
Uses SlotCalculator::hasAnyAvailability per day, so holidays, closed
overrides, non-working days and out-of-window dates all come back
disabled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-06-15 16:23:12 +03:30
co-authored by Claude Opus 4.8
parent f82d92b3ed
commit f443beb615
2 changed files with 48 additions and 1 deletions
@@ -3,7 +3,9 @@
namespace App\Appointment\Controller;
use App\Appointment\Entity\Appointment;
use App\Appointment\Entity\WeeklySchedule;
use App\Appointment\Repository\AppointmentRepository;
use App\Appointment\Repository\WeeklyScheduleRepository;
use App\Appointment\Service\SlotCalculatorService;
use App\Auth\Entity\User;
use App\Doctor\Repository\DoctorRepository;
@@ -26,6 +28,7 @@ class AppointmentController extends BaseController
private readonly DoctorRepository $doctorRepo,
private readonly SlotCalculatorService $slotCalculator,
private readonly PatientService $patientService,
private readonly WeeklyScheduleRepository $scheduleRepo,
) {}
// ── Public: available slots ───────────────────────────────────────────────
@@ -126,6 +129,49 @@ class AppointmentController extends BaseController
]);
}
#[Route('/api/v1/appointment-settings/month-availability/{doctorUuid}', methods: ['GET'])]
public function monthAvailability(string $doctorUuid, Request $request): JsonResponse
{
$doctor = $this->doctorRepo->findByUuid($doctorUuid);
if ($doctor === null) {
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404);
}
$year = (int) $request->query->get('year');
$month = (int) $request->query->get('month');
if ($year < 1970 || $month < 1 || $month > 12) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'سال یا ماه نامعتبر است', 422, 'month');
}
$daysInMonth = (int) date('t', (int) strtotime(sprintf('%04d-%02d-01', $year, $month)));
$disabled = [];
$enabled = [];
for ($day = 1; $day <= $daysInMonth; $day++) {
$date = sprintf('%04d-%02d-%02d', $year, $month, $day);
if ($this->slotCalculator->hasAnyAvailability($doctor, $date)) {
$enabled[] = $date;
} else {
$disabled[] = $date;
}
}
$schedule = $this->scheduleRepo->findByDoctor($doctor);
$meta = $schedule ? $schedule->getMeta() : WeeklySchedule::DEFAULT_META;
return $this->success([
'year' => $year,
'month' => $month,
'disabled_dates' => $disabled,
'enabled_dates' => $enabled,
'online_booking_enabled' => (bool) $meta['online_booking_enabled'],
'booking_window' => [
'value' => (int) $meta['booking_window_value'],
'unit' => $meta['booking_window_unit'],
],
]);
}
// ── Authenticated: book / manage ─────────────────────────────────────────
#[OA\Post(