194 lines
8.1 KiB
PHP
194 lines
8.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Repositories\AppointmentRepository;
|
|
use App\Repositories\InstructorRepository;
|
|
use App\Repositories\ReferenceDataRepository;
|
|
use App\Repositories\StudentRepository;
|
|
use App\Services\CalendarService;
|
|
use App\Services\RequirementService;
|
|
use App\Support\Auth;
|
|
use App\Support\Request;
|
|
use App\Support\Response;
|
|
|
|
final class AppointmentsController
|
|
{
|
|
public function index(Request $request): void
|
|
{
|
|
$user = Auth::requireUser();
|
|
$from = (string) $request->query('from');
|
|
$to = (string) $request->query('to');
|
|
$instructorId = null;
|
|
if ($user['role'] === 'instructor') {
|
|
$instructor = (new InstructorRepository())->findByUserId((int) $user['tenant_id'], (int) $user['id']);
|
|
$instructorId = $instructor ? (int) $instructor['id'] : null;
|
|
}
|
|
$data = (new AppointmentRepository())->listForTenant(
|
|
(int) $user['tenant_id'],
|
|
$from,
|
|
$to,
|
|
$instructorId
|
|
);
|
|
Response::json(['data' => $data]);
|
|
}
|
|
|
|
public function validate(Request $request): void
|
|
{
|
|
$user = Auth::requireUser();
|
|
$payload = $this->normalizePayload((int) $user['tenant_id'], $request->input());
|
|
$warnings = (new CalendarService())->validate((int) $user['tenant_id'], $payload);
|
|
Response::json(['warnings' => $warnings, 'payload' => $payload]);
|
|
}
|
|
|
|
public function store(Request $request): void
|
|
{
|
|
$user = Auth::requireRole(['tenant_admin', 'dispatcher', 'instructor']);
|
|
$payload = $this->normalizePayload((int) $user['tenant_id'], $request->input());
|
|
$warnings = (new CalendarService())->validate((int) $user['tenant_id'], $payload);
|
|
|
|
if ($this->hasWarnings($warnings) && empty($payload['warning_acknowledged'])) {
|
|
Response::json(['message' => 'Warnings require acknowledgement', 'warnings' => $warnings], 409);
|
|
}
|
|
|
|
$record = (new AppointmentRepository())->create((int) $user['tenant_id'], $payload, (int) $user['id']);
|
|
Response::json([
|
|
'data' => $record,
|
|
'warnings' => $warnings,
|
|
'progress' => $payload['student_id'] ? (new RequirementService())->progressForStudent((int) $payload['student_id']) : [],
|
|
], 201);
|
|
}
|
|
|
|
public function update(Request $request, array $params): void
|
|
{
|
|
$user = Auth::requireRole(['tenant_admin', 'dispatcher', 'instructor']);
|
|
$payload = $this->normalizePayload((int) $user['tenant_id'], $request->input());
|
|
$warnings = (new CalendarService())->validate((int) $user['tenant_id'], $payload, (int) $params['id']);
|
|
|
|
if ($this->hasWarnings($warnings) && empty($payload['warning_acknowledged'])) {
|
|
Response::json(['message' => 'Warnings require acknowledgement', 'warnings' => $warnings], 409);
|
|
}
|
|
|
|
$record = (new AppointmentRepository())->update((int) $user['tenant_id'], (int) $params['id'], $payload, (int) $user['id']);
|
|
Response::json([
|
|
'data' => $record,
|
|
'warnings' => $warnings,
|
|
'progress' => $payload['student_id'] ? (new RequirementService())->progressForStudent((int) $payload['student_id']) : [],
|
|
]);
|
|
}
|
|
|
|
public function destroy(Request $request, array $params): void
|
|
{
|
|
$user = Auth::requireRole(['tenant_admin', 'dispatcher']);
|
|
(new AppointmentRepository())->delete((int) $user['tenant_id'], (int) $params['id']);
|
|
Response::noContent();
|
|
}
|
|
|
|
private function normalizePayload(int $tenantId, array $payload): array
|
|
{
|
|
$referenceRepository = new ReferenceDataRepository();
|
|
$lessonType = $referenceRepository->findLessonType($tenantId, (int) $payload['lesson_type_id']);
|
|
if ($lessonType === null) {
|
|
Response::json(['message' => 'Unbekannte Stundenart'], 422);
|
|
}
|
|
|
|
$student = null;
|
|
if (!empty($payload['student_id'])) {
|
|
// Reject student_id if lesson_type does not allow it
|
|
if ((int) $lessonType['allows_student'] !== 1) {
|
|
Response::json(['message' => 'Bei dieser Stundenart kann kein Fahrschüler ausgewählt werden'], 422);
|
|
}
|
|
|
|
$student = (new StudentRepository())->find($tenantId, (int) $payload['student_id']);
|
|
if ($student === null) {
|
|
Response::json(['message' => 'Unbekannter Fahrschueler'], 422);
|
|
}
|
|
|
|
if (
|
|
$lessonType['category'] === 'student'
|
|
&& !$referenceRepository->isLessonTypeVisibleForTemplate($tenantId, (int) $lessonType['id'], (int) $student['template_id'])
|
|
) {
|
|
Response::json(['message' => 'Diese Stundenart ist fuer die Fuehrerscheinklasse nicht freigegeben'], 422);
|
|
}
|
|
}
|
|
|
|
$startAt = new \DateTimeImmutable((string) $payload['start_at']);
|
|
$baseDuration = max(1, (int) $lessonType['default_duration']);
|
|
$providedEndAt = !empty($payload['end_at']) ? new \DateTimeImmutable((string) $payload['end_at']) : null;
|
|
|
|
if ($providedEndAt instanceof \DateTimeImmutable && $providedEndAt > $startAt) {
|
|
$endAt = $providedEndAt;
|
|
$duration = max(1, (int) round(($endAt->getTimestamp() - $startAt->getTimestamp()) / 60));
|
|
$units = (int) $lessonType['fixed_duration'] === 1
|
|
? 1
|
|
: max(1, (int) round($duration / $baseDuration));
|
|
} else {
|
|
$units = max(1, (int) ($payload['units'] ?? 1));
|
|
$duration = (int) $lessonType['fixed_duration'] === 1
|
|
? $baseDuration
|
|
: $baseDuration * $units;
|
|
$endAt = $startAt->modify(sprintf('+%d minutes', $duration));
|
|
}
|
|
|
|
$title = $payload['title'] ?? $lessonType['name'] ?? 'Termin';
|
|
$unitsBreakdown = [];
|
|
|
|
if ($lessonType && (int) $lessonType['is_counted'] === 1 && !empty($payload['student_id'])) {
|
|
$requirementKey = $lessonType['effective_requirement_key'] ?? $lessonType['requirement_key'];
|
|
if ($student !== null && (int) ($student['template_is_combination'] ?? 0) === 1) {
|
|
$requirementKey = $this->requirementKeyForPhase($student['requirements'] ?? [], $requirementKey, (string) ($payload['requirement_phase'] ?? ''));
|
|
}
|
|
|
|
$unitsBreakdown[] = [
|
|
'requirement_key' => $requirementKey,
|
|
'label' => $lessonType['name'],
|
|
'units_counted' => $units,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'student_id' => $payload['student_id'] ?? null,
|
|
'instructor_id' => (int) $payload['instructor_id'],
|
|
'lesson_type_id' => (int) $payload['lesson_type_id'],
|
|
'title' => $title,
|
|
'category' => $lessonType['category'] ?? ($payload['category'] ?? 'student'),
|
|
'start_at' => $startAt->format(DATE_ATOM),
|
|
'end_at' => $endAt->format(DATE_ATOM),
|
|
'units' => $units,
|
|
'status' => $payload['status'] ?? 'planned',
|
|
'notes' => $payload['notes'] ?? '',
|
|
'notes_private' => $payload['notes_private'] ?? '',
|
|
'warning_acknowledged' => !empty($payload['warning_acknowledged']),
|
|
'units_breakdown' => $unitsBreakdown,
|
|
];
|
|
}
|
|
|
|
private function hasWarnings(array $warnings): bool
|
|
{
|
|
return !empty($warnings['conflicts']) || !empty($warnings['rest_period_warnings']);
|
|
}
|
|
|
|
private function requirementKeyForPhase(array $requirements, string $baseRequirementKey, string $phaseLabel): string
|
|
{
|
|
$matchingRows = array_values(array_filter(
|
|
$requirements,
|
|
static fn (array $row): bool => str_ends_with((string) $row['requirement_key'], '::' . $baseRequirementKey)
|
|
|| (string) $row['requirement_key'] === $baseRequirementKey
|
|
));
|
|
|
|
if ($matchingRows === []) {
|
|
return $baseRequirementKey;
|
|
}
|
|
|
|
foreach ($matchingRows as $row) {
|
|
if ((string) ($row['phase_label'] ?? '') === $phaseLabel) {
|
|
return (string) $row['requirement_key'];
|
|
}
|
|
}
|
|
|
|
return (string) $matchingRows[0]['requirement_key'];
|
|
}
|
|
}
|