From 8fccf0e4069479bd8bbca16b2e407ddd8e4514c5 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 20 May 2026 16:36:12 +0200 Subject: [PATCH] Backup: old custom PHP API code removed from fahrschultermin.de --- .../Controllers/AppointmentsController.php | 193 +++++++ .../app/Http/Controllers/AuthController.php | 49 ++ .../Http/Controllers/BootstrapController.php | 76 +++ .../Controllers/InstructorsController.php | 33 ++ .../Controllers/ReferenceDataController.php | 90 +++ .../Http/Controllers/StudentsController.php | 78 +++ .../Http/Controllers/TenantsController.php | 40 ++ .../app/Http/Controllers/UsersController.php | 51 ++ .../Repositories/AppointmentRepository.php | 292 ++++++++++ .../AppointmentRequestRepository.php | 104 ++++ .../app/Repositories/BaseRepository.php | 28 + .../InstructorAvailabilityRepository.php | 65 +++ .../InstructorBreakRuleRepository.php | 96 ++++ .../InstructorHomeToTenantRepository.php | 56 ++ .../app/Repositories/InstructorRepository.php | 119 ++++ .../InstructorTenantRepository.php | 70 +++ .../InstructorTravelTimeRepository.php | 66 +++ .../InstructorVacationRepository.php | 54 ++ .../PrivateAppointmentRepository.php | 56 ++ .../Repositories/ReferenceDataRepository.php | 515 +++++++++++++++++ .../app/Repositories/StudentRepository.php | 294 ++++++++++ .../Repositories/StudentTenantRepository.php | 77 +++ .../TenantRegistrationCodeRepository.php | 57 ++ .../app/Repositories/TenantRepository.php | 86 +++ .../app/Repositories/UserRepository.php | 112 ++++ .../app/Services/CalendarService.php | 93 ++++ .../app/Services/HolidayService.php | 109 ++++ .../app/Services/RequirementService.php | 56 ++ .../app/Services/SunTimesService.php | 55 ++ backup/old_api_20260520/app/Support/Auth.php | 42 ++ .../old_api_20260520/app/Support/Database.php | 66 +++ .../old_api_20260520/app/Support/Request.php | 40 ++ .../old_api_20260520/app/Support/Response.php | 22 + .../old_api_20260520/app/Support/Router.php | 63 +++ backup/old_api_20260520/app/bootstrap.php | 44 ++ backup/old_api_20260520/bin/migrate.php | 27 + backup/old_api_20260520/bin/seed.php | 208 +++++++ backup/old_api_20260520/config/app.php | 38 ++ .../database/import_pgsql.sql | 518 ++++++++++++++++++ .../database/migrations/001_initial.sql | 145 +++++ .../migrations/002_instructors_user_link.sql | 1 + .../003_backfill_instructor_user_links.sql | 11 + .../004_lesson_type_template_visibility.sql | 13 + .../migrations/005_tenant_location_sun.sql | 3 + .../migrations/006_tenant_federal_state.sql | 1 + .../007_student_prior_completed_units.sql | 1 + .../migrations/008_lesson_type_system_key.sql | 2 + .../009_requirement_phase_labels.sql | 2 + .../database/migrations/010_student_email.sql | 1 + .../migrations/010_students_contact_flags.sql | 2 + .../migrations/011_student_needs_glasses.sql | 1 + .../012_split_combination_templates.sql | 183 +++++++ .../013_combo_generic_key_cleanup.sql | 42 ++ ...tudent_key_aliases_to_lesson_type_keys.sql | 170 ++++++ .../015_instructor_pre_start_buffer.sql | 1 + ..._type_allows_student_and_notes_private.sql | 22 + .../016_consolidate_lesson_types.sql | 39 ++ .../migrations/017_student_booking_schema.sql | 202 +++++++ .../database/schema_pgsql.sql | 166 ++++++ 59 files changed, 5146 insertions(+) create mode 100644 backup/old_api_20260520/app/Http/Controllers/AppointmentsController.php create mode 100644 backup/old_api_20260520/app/Http/Controllers/AuthController.php create mode 100644 backup/old_api_20260520/app/Http/Controllers/BootstrapController.php create mode 100644 backup/old_api_20260520/app/Http/Controllers/InstructorsController.php create mode 100644 backup/old_api_20260520/app/Http/Controllers/ReferenceDataController.php create mode 100644 backup/old_api_20260520/app/Http/Controllers/StudentsController.php create mode 100644 backup/old_api_20260520/app/Http/Controllers/TenantsController.php create mode 100644 backup/old_api_20260520/app/Http/Controllers/UsersController.php create mode 100644 backup/old_api_20260520/app/Repositories/AppointmentRepository.php create mode 100644 backup/old_api_20260520/app/Repositories/AppointmentRequestRepository.php create mode 100644 backup/old_api_20260520/app/Repositories/BaseRepository.php create mode 100644 backup/old_api_20260520/app/Repositories/InstructorAvailabilityRepository.php create mode 100644 backup/old_api_20260520/app/Repositories/InstructorBreakRuleRepository.php create mode 100644 backup/old_api_20260520/app/Repositories/InstructorHomeToTenantRepository.php create mode 100644 backup/old_api_20260520/app/Repositories/InstructorRepository.php create mode 100644 backup/old_api_20260520/app/Repositories/InstructorTenantRepository.php create mode 100644 backup/old_api_20260520/app/Repositories/InstructorTravelTimeRepository.php create mode 100644 backup/old_api_20260520/app/Repositories/InstructorVacationRepository.php create mode 100644 backup/old_api_20260520/app/Repositories/PrivateAppointmentRepository.php create mode 100644 backup/old_api_20260520/app/Repositories/ReferenceDataRepository.php create mode 100644 backup/old_api_20260520/app/Repositories/StudentRepository.php create mode 100644 backup/old_api_20260520/app/Repositories/StudentTenantRepository.php create mode 100644 backup/old_api_20260520/app/Repositories/TenantRegistrationCodeRepository.php create mode 100644 backup/old_api_20260520/app/Repositories/TenantRepository.php create mode 100644 backup/old_api_20260520/app/Repositories/UserRepository.php create mode 100644 backup/old_api_20260520/app/Services/CalendarService.php create mode 100644 backup/old_api_20260520/app/Services/HolidayService.php create mode 100644 backup/old_api_20260520/app/Services/RequirementService.php create mode 100644 backup/old_api_20260520/app/Services/SunTimesService.php create mode 100644 backup/old_api_20260520/app/Support/Auth.php create mode 100644 backup/old_api_20260520/app/Support/Database.php create mode 100644 backup/old_api_20260520/app/Support/Request.php create mode 100644 backup/old_api_20260520/app/Support/Response.php create mode 100644 backup/old_api_20260520/app/Support/Router.php create mode 100644 backup/old_api_20260520/app/bootstrap.php create mode 100644 backup/old_api_20260520/bin/migrate.php create mode 100644 backup/old_api_20260520/bin/seed.php create mode 100644 backup/old_api_20260520/config/app.php create mode 100644 backup/old_api_20260520/database/import_pgsql.sql create mode 100644 backup/old_api_20260520/database/migrations/001_initial.sql create mode 100644 backup/old_api_20260520/database/migrations/002_instructors_user_link.sql create mode 100644 backup/old_api_20260520/database/migrations/003_backfill_instructor_user_links.sql create mode 100644 backup/old_api_20260520/database/migrations/004_lesson_type_template_visibility.sql create mode 100644 backup/old_api_20260520/database/migrations/005_tenant_location_sun.sql create mode 100644 backup/old_api_20260520/database/migrations/006_tenant_federal_state.sql create mode 100644 backup/old_api_20260520/database/migrations/007_student_prior_completed_units.sql create mode 100644 backup/old_api_20260520/database/migrations/008_lesson_type_system_key.sql create mode 100644 backup/old_api_20260520/database/migrations/009_requirement_phase_labels.sql create mode 100644 backup/old_api_20260520/database/migrations/010_student_email.sql create mode 100644 backup/old_api_20260520/database/migrations/010_students_contact_flags.sql create mode 100644 backup/old_api_20260520/database/migrations/011_student_needs_glasses.sql create mode 100644 backup/old_api_20260520/database/migrations/012_split_combination_templates.sql create mode 100644 backup/old_api_20260520/database/migrations/013_combo_generic_key_cleanup.sql create mode 100644 backup/old_api_20260520/database/migrations/014_legacy_student_key_aliases_to_lesson_type_keys.sql create mode 100644 backup/old_api_20260520/database/migrations/015_instructor_pre_start_buffer.sql create mode 100644 backup/old_api_20260520/database/migrations/015_lesson_type_allows_student_and_notes_private.sql create mode 100644 backup/old_api_20260520/database/migrations/016_consolidate_lesson_types.sql create mode 100644 backup/old_api_20260520/database/migrations/017_student_booking_schema.sql create mode 100644 backup/old_api_20260520/database/schema_pgsql.sql diff --git a/backup/old_api_20260520/app/Http/Controllers/AppointmentsController.php b/backup/old_api_20260520/app/Http/Controllers/AppointmentsController.php new file mode 100644 index 0000000..059c31e --- /dev/null +++ b/backup/old_api_20260520/app/Http/Controllers/AppointmentsController.php @@ -0,0 +1,193 @@ +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']; + } +} diff --git a/backup/old_api_20260520/app/Http/Controllers/AuthController.php b/backup/old_api_20260520/app/Http/Controllers/AuthController.php new file mode 100644 index 0000000..a71b131 --- /dev/null +++ b/backup/old_api_20260520/app/Http/Controllers/AuthController.php @@ -0,0 +1,49 @@ +input(); + $user = (new UserRepository())->findByEmail(trim((string) ($payload['email'] ?? ''))); + + if ($user === null || !password_verify((string) ($payload['password'] ?? ''), $user['password_hash'])) { + Response::json(['message' => 'Ungueltige Zugangsdaten'], 422); + } + + if (!(int) $user['is_active'] || ((int) ($user['tenant_is_active'] ?? 1) !== 1 && $user['role'] !== 'superadmin')) { + Response::json(['message' => 'Benutzer oder Mandant ist deaktiviert'], 403); + } + + $_SESSION['user_id'] = (int) $user['id']; + + Response::json(['user' => $this->sanitizeUser($user)]); + } + + public function logout(): void + { + session_destroy(); + Response::noContent(); + } + + public function me(): void + { + $user = Auth::user(); + Response::json(['user' => $user ? $this->sanitizeUser($user) : null]); + } + + private function sanitizeUser(array $user): array + { + unset($user['password_hash']); + return $user; + } +} diff --git a/backup/old_api_20260520/app/Http/Controllers/BootstrapController.php b/backup/old_api_20260520/app/Http/Controllers/BootstrapController.php new file mode 100644 index 0000000..0a9bf53 --- /dev/null +++ b/backup/old_api_20260520/app/Http/Controllers/BootstrapController.php @@ -0,0 +1,76 @@ + ['user' => $user]]; + + if ($user['role'] === 'superadmin') { + $payload['tenants'] = (new TenantRepository())->all(); + $payload['users'] = (new UserRepository())->allUsers(); + Response::json($payload); + } + + $tenantId = (int) $user['tenant_id']; + $studentRepository = new StudentRepository(); + $students = $studentRepository->allForTenant($tenantId); + $requirementService = new RequirementService(); + + foreach ($students as &$student) { + $student['progress'] = $requirementService->progressForStudent((int) $student['id']); + } + + $payload['tenant'] = (new TenantRepository())->find($tenantId); + $payload['students'] = $students; + $payload['templates'] = (new ReferenceDataRepository())->templates($tenantId); + $payload['lessonTypes'] = (new ReferenceDataRepository())->lessonTypes($tenantId); + $payload['instructors'] = (new InstructorRepository())->allForTenant($tenantId); + $payload['users'] = $user['role'] === 'tenant_admin' + ? (new UserRepository())->allForTenant($tenantId) + : []; + $instructorId = null; + if ($user['role'] === 'instructor') { + $instructor = (new InstructorRepository())->findByUserId($tenantId, (int) $user['id']); + $instructorId = $instructor ? (int) $instructor['id'] : null; + } + + $from = $request->query('from', gmdate('Y-m-d\T00:00:00\Z', strtotime('monday this week'))); + $to = $request->query('to', gmdate('Y-m-d\T00:00:00\Z', strtotime('+7 day', strtotime($from)))); + $payload['sunTimes'] = (new SunTimesService())->forRange($payload['tenant'], (string) $from, (string) $to); + $payload['dayMeta'] = (new HolidayService())->forRange($payload['tenant'], (string) $from, (string) $to); + $payload['appointments'] = (new AppointmentRepository())->listForTenant( + $tenantId, + (string) $from, + (string) $to, + $instructorId + ); + $payload['nextAppointment'] = (new AppointmentRepository())->nextForTenant( + $tenantId, + gmdate(DATE_ATOM), + $instructorId + ); + + Response::json($payload); + } +} diff --git a/backup/old_api_20260520/app/Http/Controllers/InstructorsController.php b/backup/old_api_20260520/app/Http/Controllers/InstructorsController.php new file mode 100644 index 0000000..b72c60a --- /dev/null +++ b/backup/old_api_20260520/app/Http/Controllers/InstructorsController.php @@ -0,0 +1,33 @@ + (new InstructorRepository())->allForTenant((int) $user['tenant_id'])]); + } + + public function store(Request $request): void + { + $user = Auth::requireRole('tenant_admin'); + $record = (new InstructorRepository())->create((int) $user['tenant_id'], $request->input()); + Response::json(['data' => $record], 201); + } + + public function update(Request $request, array $params): void + { + $user = Auth::requireRole('tenant_admin'); + $record = (new InstructorRepository())->update((int) $user['tenant_id'], (int) $params['id'], $request->input()); + Response::json(['data' => $record]); + } +} diff --git a/backup/old_api_20260520/app/Http/Controllers/ReferenceDataController.php b/backup/old_api_20260520/app/Http/Controllers/ReferenceDataController.php new file mode 100644 index 0000000..71b091c --- /dev/null +++ b/backup/old_api_20260520/app/Http/Controllers/ReferenceDataController.php @@ -0,0 +1,90 @@ + (new ReferenceDataRepository())->lessonTypes((int) $user['tenant_id'])]); + } + + public function storeLessonType(Request $request): void + { + $user = Auth::requireRole('tenant_admin'); + $record = (new ReferenceDataRepository())->createLessonType((int) $user['tenant_id'], $request->input()); + Response::json(['data' => $record], 201); + } + + public function updateLessonType(Request $request, array $params): void + { + $user = Auth::requireRole('tenant_admin'); + $record = (new ReferenceDataRepository())->updateLessonType((int) $user['tenant_id'], (int) $params['id'], $request->input()); + Response::json(['data' => $record]); + } + + public function destroyLessonType(Request $request, array $params): void + { + $user = Auth::requireRole('tenant_admin'); + + try { + $deleted = (new ReferenceDataRepository())->deleteLessonType((int) $user['tenant_id'], (int) $params['id']); + } catch (\RuntimeException $exception) { + Response::json(['message' => $exception->getMessage()], 409); + } + + if (!$deleted) { + Response::json(['message' => 'Not found'], 404); + } + + Response::noContent(); + } + + public function reorderLessonTypes(Request $request): void + { + $user = Auth::requireRole('tenant_admin'); + $records = (new ReferenceDataRepository())->reorderLessonTypes( + (int) $user['tenant_id'], + (array) ($request->input()['lesson_type_ids'] ?? []) + ); + Response::json(['data' => $records]); + } + + public function updateLessonTypeMatrix(Request $request): void + { + $user = Auth::requireRole('tenant_admin'); + $records = (new ReferenceDataRepository())->updateLessonTypeMatrix( + (int) $user['tenant_id'], + (array) ($request->input()['rows'] ?? []) + ); + Response::json(['data' => $records]); + } + + public function templates(): void + { + $user = Auth::requireUser(); + Response::json(['data' => (new ReferenceDataRepository())->templates((int) $user['tenant_id'])]); + } + + public function storeTemplate(Request $request): void + { + $user = Auth::requireRole('tenant_admin'); + $record = (new ReferenceDataRepository())->createTemplate((int) $user['tenant_id'], $request->input()); + Response::json(['data' => $record], 201); + } + + public function updateTemplate(Request $request, array $params): void + { + $user = Auth::requireRole('tenant_admin'); + $record = (new ReferenceDataRepository())->updateTemplate((int) $user['tenant_id'], (int) $params['id'], $request->input()); + Response::json(['data' => $record]); + } +} diff --git a/backup/old_api_20260520/app/Http/Controllers/StudentsController.php b/backup/old_api_20260520/app/Http/Controllers/StudentsController.php new file mode 100644 index 0000000..8323549 --- /dev/null +++ b/backup/old_api_20260520/app/Http/Controllers/StudentsController.php @@ -0,0 +1,78 @@ +allForTenant((int) $user['tenant_id']); + $requirements = new RequirementService(); + + foreach ($students as &$student) { + $student['progress'] = $requirements->progressForStudent((int) $student['id']); + } + + Response::json(['data' => $students]); + } + + public function store(Request $request): void + { + $user = Auth::requireRole(['tenant_admin', 'dispatcher']); + $student = (new StudentRepository())->create((int) $user['tenant_id'], $request->input()); + $student['progress'] = (new RequirementService())->progressForStudent((int) $student['id']); + + Response::json(['data' => $student], 201); + } + + public function update(Request $request, array $params): void + { + $user = Auth::requireRole(['tenant_admin', 'dispatcher']); + $student = (new StudentRepository())->update((int) $user['tenant_id'], (int) $params['id'], $request->input()); + + if ($student === null) { + Response::json(['message' => 'Not found'], 404); + } + + $student['progress'] = (new RequirementService())->progressForStudent((int) $student['id']); + Response::json(['data' => $student]); + } + + public function updateTraining(Request $request, array $params): void + { + $user = Auth::requireRole(['tenant_admin', 'dispatcher']); + $student = (new StudentRepository())->updatePriorCompletedUnits( + (int) $user['tenant_id'], + (int) $params['id'], + $request->input()['rows'] ?? [] + ); + + if ($student === null) { + Response::json(['message' => 'Not found'], 404); + } + + $student['progress'] = (new RequirementService())->progressForStudent((int) $student['id']); + Response::json(['data' => $student]); + } + + public function destroy(Request $request, array $params): void + { + $user = Auth::requireRole(['tenant_admin', 'dispatcher']); + $deleted = (new StudentRepository())->delete((int) $user['tenant_id'], (int) $params['id']); + + if (!$deleted) { + Response::json(['message' => 'Not found'], 404); + } + + Response::noContent(); + } +} diff --git a/backup/old_api_20260520/app/Http/Controllers/TenantsController.php b/backup/old_api_20260520/app/Http/Controllers/TenantsController.php new file mode 100644 index 0000000..cf238c6 --- /dev/null +++ b/backup/old_api_20260520/app/Http/Controllers/TenantsController.php @@ -0,0 +1,40 @@ + (new TenantRepository())->all()]); + } + + public function store(Request $request): void + { + Auth::requireRole('superadmin'); + $tenant = (new TenantRepository())->create($request->input()); + Response::json(['data' => $tenant], 201); + } + + public function update(Request $request, array $params): void + { + Auth::requireRole('superadmin'); + $tenant = (new TenantRepository())->update((int) $params['id'], $request->input()); + Response::json(['data' => $tenant]); + } + + public function updateOwn(Request $request): void + { + $user = Auth::requireRole('tenant_admin'); + $tenant = (new TenantRepository())->updateForTenantAdmin((int) $user['tenant_id'], $request->input()); + Response::json(['data' => $tenant]); + } +} diff --git a/backup/old_api_20260520/app/Http/Controllers/UsersController.php b/backup/old_api_20260520/app/Http/Controllers/UsersController.php new file mode 100644 index 0000000..364acc5 --- /dev/null +++ b/backup/old_api_20260520/app/Http/Controllers/UsersController.php @@ -0,0 +1,51 @@ + (new UserRepository())->allUsers()]); + } + + Response::json(['data' => (new UserRepository())->allForTenant((int) $user['tenant_id'])]); + } + + public function store(Request $request): void + { + $user = Auth::requireRole(['tenant_admin', 'superadmin']); + $payload = $request->input(); + if ($user['role'] !== 'superadmin') { + $payload['tenant_id'] = (int) $user['tenant_id']; + } + $record = (new UserRepository())->create($payload); + unset($record['password_hash']); + Response::json(['data' => $record], 201); + } + + public function update(Request $request, array $params): void + { + $user = Auth::requireRole(['tenant_admin', 'superadmin']); + $payload = $request->input(); + if ($user['role'] !== 'superadmin') { + $payload['tenant_id'] = (int) $user['tenant_id']; + } + $record = (new UserRepository())->update((int) $params['id'], $payload); + if ($record === null) { + Response::json(['message' => 'Not found'], 404); + } + unset($record['password_hash']); + Response::json(['data' => $record]); + } +} diff --git a/backup/old_api_20260520/app/Repositories/AppointmentRepository.php b/backup/old_api_20260520/app/Repositories/AppointmentRepository.php new file mode 100644 index 0000000..afe0f47 --- /dev/null +++ b/backup/old_api_20260520/app/Repositories/AppointmentRepository.php @@ -0,0 +1,292 @@ + :from'; + $params = [ + 'tenant_id' => $tenantId, + 'from' => $from, + 'to' => $to, + ]; + + if ($instructorId !== null) { + $sql .= ' AND a.instructor_id = :instructor_id'; + $params['instructor_id'] = $instructorId; + } + + $sql .= ' ORDER BY a.start_at'; + $statement = $this->db->prepare($sql); + $statement->execute($params); + + return $statement->fetchAll(); + } + + public function find(int $tenantId, int $id): ?array + { + $statement = $this->db->prepare('SELECT * FROM appointments WHERE tenant_id = :tenant_id AND id = :id'); + $statement->execute(['tenant_id' => $tenantId, 'id' => $id]); + $appointment = $statement->fetch() ?: null; + + if ($appointment !== null) { + $appointment['units_breakdown'] = $this->units((int) $appointment['id']); + } + + return $appointment; + } + + public function create(int $tenantId, array $data, int $actorId): array + { + $timestamp = $this->now(); + $statement = $this->db->prepare( + 'INSERT INTO appointments + (tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, notes_private, warning_acknowledged, created_by, updated_by, created_at, updated_at) + VALUES + (:tenant_id, :student_id, :instructor_id, :lesson_type_id, :title, :category, :start_at, :end_at, :units, :status, :notes, :notes_private, :warning_acknowledged, :created_by, :updated_by, :created_at, :updated_at)' + ); + $statement->execute([ + 'tenant_id' => $tenantId, + 'student_id' => $data['student_id'] ?: null, + 'instructor_id' => $data['instructor_id'], + 'lesson_type_id' => $data['lesson_type_id'], + 'title' => $data['title'], + 'category' => $data['category'], + 'start_at' => $data['start_at'], + 'end_at' => $data['end_at'], + 'units' => (int) $data['units'], + 'status' => $data['status'], + 'notes' => $data['notes'] ?? '', + 'notes_private' => $data['notes_private'] ?? '', + 'warning_acknowledged' => !empty($data['warning_acknowledged']) ? 1 : 0, + 'created_by' => $actorId, + 'updated_by' => $actorId, + 'created_at' => $timestamp, + 'updated_at' => $timestamp, + ]); + + $appointmentId = (int) $this->lastInsertId(); + $this->replaceUnits($appointmentId, $data['units_breakdown'] ?? []); + + return $this->find($tenantId, $appointmentId); + } + + public function update(int $tenantId, int $id, array $data, int $actorId): ?array + { + $statement = $this->db->prepare( + 'UPDATE appointments + SET student_id = :student_id, instructor_id = :instructor_id, lesson_type_id = :lesson_type_id, + title = :title, category = :category, start_at = :start_at, end_at = :end_at, units = :units, + status = :status, notes = :notes, notes_private = :notes_private, warning_acknowledged = :warning_acknowledged, + updated_by = :updated_by, updated_at = :updated_at + WHERE tenant_id = :tenant_id AND id = :id' + ); + $statement->execute([ + 'tenant_id' => $tenantId, + 'id' => $id, + 'student_id' => $data['student_id'] ?: null, + 'instructor_id' => $data['instructor_id'], + 'lesson_type_id' => $data['lesson_type_id'], + 'title' => $data['title'], + 'category' => $data['category'], + 'start_at' => $data['start_at'], + 'end_at' => $data['end_at'], + 'units' => (int) $data['units'], + 'status' => $data['status'], + 'notes' => $data['notes'] ?? '', + 'notes_private' => $data['notes_private'] ?? '', + 'warning_acknowledged' => !empty($data['warning_acknowledged']) ? 1 : 0, + 'updated_by' => $actorId, + 'updated_at' => $this->now(), + ]); + + $this->replaceUnits($id, $data['units_breakdown'] ?? []); + + return $this->find($tenantId, $id); + } + + public function delete(int $tenantId, int $id): void + { + $statement = $this->db->prepare('DELETE FROM appointments WHERE tenant_id = :tenant_id AND id = :id'); + $statement->execute(['tenant_id' => $tenantId, 'id' => $id]); + } + + public function units(int $appointmentId): array + { + $statement = $this->db->prepare('SELECT * FROM appointment_units WHERE appointment_id = :appointment_id'); + $statement->execute(['appointment_id' => $appointmentId]); + + return $statement->fetchAll(); + } + + public function overlappingForInstructor(int $tenantId, int $instructorId, string $from, string $to, ?int $ignoreId = null): array + { + $sql = 'SELECT * FROM appointments + WHERE tenant_id = :tenant_id AND instructor_id = :instructor_id + AND start_at < :to AND end_at > :from'; + $params = [ + 'tenant_id' => $tenantId, + 'instructor_id' => $instructorId, + 'from' => $from, + 'to' => $to, + ]; + + if ($ignoreId !== null) { + $sql .= ' AND id != :ignore_id'; + $params['ignore_id'] = $ignoreId; + } + + $statement = $this->db->prepare($sql); + $statement->execute($params); + + return $statement->fetchAll(); + } + + public function overlappingForStudent(int $tenantId, int $studentId, string $from, string $to, ?int $ignoreId = null): array + { + $sql = 'SELECT * FROM appointments + WHERE tenant_id = :tenant_id AND student_id = :student_id + AND start_at < :to AND end_at > :from'; + $params = [ + 'tenant_id' => $tenantId, + 'student_id' => $studentId, + 'from' => $from, + 'to' => $to, + ]; + + if ($ignoreId !== null) { + $sql .= ' AND id != :ignore_id'; + $params['ignore_id'] = $ignoreId; + } + + $statement = $this->db->prepare($sql); + $statement->execute($params); + + return $statement->fetchAll(); + } + + public function lastRestRelevantForInstructor(int $tenantId, int $instructorId, string $beforeStart, ?int $ignoreId = null): ?array + { + $sql = 'SELECT a.*, lt.name AS lesson_type_name + FROM appointments a + JOIN lesson_types lt ON lt.id = a.lesson_type_id + WHERE a.tenant_id = :tenant_id + AND a.instructor_id = :instructor_id + AND lt.is_rest_relevant = 1 + AND a.end_at <= :before_start'; + $params = [ + 'tenant_id' => $tenantId, + 'instructor_id' => $instructorId, + 'before_start' => $beforeStart, + ]; + + if ($ignoreId !== null) { + $sql .= ' AND a.id != :ignore_id'; + $params['ignore_id'] = $ignoreId; + } + + $sql .= ' ORDER BY a.end_at DESC LIMIT 1'; + $statement = $this->db->prepare($sql); + $statement->execute($params); + + return $statement->fetch() ?: null; + } + + public function hasRestRelevantAfterStartOnDay(int $tenantId, int $instructorId, string $dayStart, string $dayEnd, string $startAt): bool + { + $statement = $this->db->prepare( + 'SELECT 1 + FROM appointments a + JOIN lesson_types lt ON lt.id = a.lesson_type_id + WHERE a.tenant_id = :tenant_id + AND a.instructor_id = :instructor_id + AND lt.is_rest_relevant = 1 + AND a.start_at < :day_end + AND a.end_at > :day_start + AND a.end_at > :start_at + LIMIT 1' + ); + $statement->execute([ + 'tenant_id' => $tenantId, + 'instructor_id' => $instructorId, + 'day_start' => $dayStart, + 'day_end' => $dayEnd, + 'start_at' => $startAt, + ]); + + return (bool) $statement->fetchColumn(); + } + + public function nextForTenant(int $tenantId, string $from, ?int $instructorId = null): ?array + { + $sql = 'SELECT a.*, s.first_name AS student_first_name, s.last_name AS student_last_name, + i.first_name AS instructor_first_name, i.last_name AS instructor_last_name, + lt.name AS lesson_type_name + FROM appointments a + JOIN instructors i ON i.id = a.instructor_id + LEFT JOIN students s ON s.id = a.student_id + JOIN lesson_types lt ON lt.id = a.lesson_type_id + WHERE a.tenant_id = :tenant_id + AND a.start_at >= :from'; + $params = [ + 'tenant_id' => $tenantId, + 'from' => $from, + ]; + + if ($instructorId !== null) { + $sql .= ' AND a.instructor_id = :instructor_id'; + $params['instructor_id'] = $instructorId; + } + + $sql .= ' ORDER BY a.start_at ASC LIMIT 1'; + $statement = $this->db->prepare($sql); + $statement->execute($params); + + return $statement->fetch() ?: null; + } + + private function replaceUnits(int $appointmentId, array $rows): void + { + $delete = $this->db->prepare('DELETE FROM appointment_units WHERE appointment_id = :appointment_id'); + $delete->execute(['appointment_id' => $appointmentId]); + + $insert = $this->db->prepare( + 'INSERT INTO appointment_units (appointment_id, requirement_key, label, units_counted, created_at) + VALUES (:appointment_id, :requirement_key, :label, :units_counted, :created_at)' + ); + + foreach ($rows as $row) { + if ((int) ($row['units_counted'] ?? 0) <= 0) { + continue; + } + + $insert->execute([ + 'appointment_id' => $appointmentId, + 'requirement_key' => $row['requirement_key'] ?? null, + 'label' => $row['label'] ?? null, + 'units_counted' => (int) $row['units_counted'], + 'created_at' => $this->now(), + ]); + } + } +} diff --git a/backup/old_api_20260520/app/Repositories/AppointmentRequestRepository.php b/backup/old_api_20260520/app/Repositories/AppointmentRequestRepository.php new file mode 100644 index 0000000..d7ce147 --- /dev/null +++ b/backup/old_api_20260520/app/Repositories/AppointmentRequestRepository.php @@ -0,0 +1,104 @@ +db->prepare( + 'INSERT INTO appointment_requests + (student_user_id, instructor_id, lesson_type_id, requested_start, requested_end, notes) + VALUES (:student_user_id, :instructor_id, :lesson_type_id, :start, :end, :notes) + RETURNING *' + ); + $stmt->execute([ + 'student_user_id' => $studentUserId, + 'instructor_id' => $instructorId, + 'lesson_type_id' => $lessonTypeId, + 'start' => $start, + 'end' => $end, + 'notes' => $notes, + ]); + return $stmt->fetch(); + } + + public function findById(int $id): ?array + { + $stmt = $this->db->prepare('SELECT * FROM appointment_requests WHERE id = :id'); + $stmt->execute(['id' => $id]); + $result = $stmt->fetch(); + return $result ?: null; + } + + public function getPendingForInstructor(int $instructorId): array + { + $stmt = $this->db->prepare( + 'SELECT ar.*, + u.first_name AS student_first_name, u.last_name AS student_last_name, + lt.name AS lesson_type_name, lt.category + FROM appointment_requests ar + JOIN users u ON u.id = ar.student_user_id + JOIN lesson_types lt ON lt.id = ar.lesson_type_id + WHERE ar.instructor_id = :instructor_id AND ar.status = \'pending\' + ORDER BY ar.requested_start ASC' + ); + $stmt->execute(['instructor_id' => $instructorId]); + return $stmt->fetchAll(); + } + + public function getForStudent(int $studentUserId): array + { + $stmt = $this->db->prepare( + 'SELECT ar.*, lt.name AS lesson_type_name + FROM appointment_requests ar + JOIN lesson_types lt ON lt.id = ar.lesson_type_id + WHERE ar.student_user_id = :student_user_id + ORDER BY ar.requested_start DESC' + ); + $stmt->execute(['student_user_id' => $studentUserId]); + return $stmt->fetchAll(); + } + + public function confirm(int $id, int $responseBy, int $appointmentId): bool + { + $stmt = $this->db->prepare( + 'UPDATE appointment_requests + SET status = \'confirmed\', responded_at = NOW(), response_by = :response_by, + confirmed_appointment_id = :appointment_id + WHERE id = :id AND status = \'pending\' + RETURNING id' + ); + $stmt->execute(['id' => $id, 'response_by' => $responseBy, 'appointment_id' => $appointmentId]); + return (bool) $stmt->fetch(); + } + + public function reject(int $id, int $responseBy): bool + { + $stmt = $this->db->prepare( + 'UPDATE appointment_requests + SET status = \'rejected\', responded_at = NOW(), response_by = :response_by + WHERE id = :id AND status = \'pending\' + RETURNING id' + ); + $stmt->execute(['id' => $id, 'response_by' => $responseBy]); + return (bool) $stmt->fetch(); + } + + public function cancel(int $id): bool + { + $stmt = $this->db->prepare( + 'UPDATE appointment_requests SET status = \'cancelled\' WHERE id = :id RETURNING id' + ); + $stmt->execute(['id' => $id]); + return (bool) $stmt->fetch(); + } +} \ No newline at end of file diff --git a/backup/old_api_20260520/app/Repositories/BaseRepository.php b/backup/old_api_20260520/app/Repositories/BaseRepository.php new file mode 100644 index 0000000..6e356f9 --- /dev/null +++ b/backup/old_api_20260520/app/Repositories/BaseRepository.php @@ -0,0 +1,28 @@ +db = Database::connection(); + } + + protected function lastInsertId(): string + { + return Database::lastInsertId(); + } + + protected function now(): string + { + return gmdate('c'); + } +} diff --git a/backup/old_api_20260520/app/Repositories/InstructorAvailabilityRepository.php b/backup/old_api_20260520/app/Repositories/InstructorAvailabilityRepository.php new file mode 100644 index 0000000..ac371d8 --- /dev/null +++ b/backup/old_api_20260520/app/Repositories/InstructorAvailabilityRepository.php @@ -0,0 +1,65 @@ +db->prepare( + 'SELECT * FROM instructor_availability + WHERE instructor_id = :instructor_id + ORDER BY weekday, time_from' + ); + $stmt->execute(['instructor_id' => $instructorId]); + return $stmt->fetchAll(); + } + + public function getActiveForInstructor(int $instructorId): array + { + $stmt = $this->db->prepare( + 'SELECT * FROM instructor_availability + WHERE instructor_id = :instructor_id AND is_active = 1 + ORDER BY weekday, time_from' + ); + $stmt->execute(['instructor_id' => $instructorId]); + return $stmt->fetchAll(); + } + + public function upsert(int $instructorId, array $entries): void + { + // Delete all existing, then insert new (full replace) + $this->db->beginTransaction(); + try { + $stmt = $this->db->prepare('DELETE FROM instructor_availability WHERE instructor_id = :instructor_id'); + $stmt->execute(['instructor_id' => $instructorId]); + + $insert = $this->db->prepare( + 'INSERT INTO instructor_availability (instructor_id, weekday, time_from, time_to, is_active) + VALUES (:instructor_id, :weekday, :time_from, :time_to, :is_active)' + ); + foreach ($entries as $e) { + $insert->execute([ + 'instructor_id' => $instructorId, + 'weekday' => (int) $e['weekday'], + 'time_from' => $e['time_from'], + 'time_to' => $e['time_to'], + 'is_active' => (int) ($e['is_active'] ?? 1), + ]); + } + $this->db->commit(); + } catch (\Throwable $e) { + $this->db->rollBack(); + throw $e; + } + } + + public function delete(int $id): bool + { + $stmt = $this->db->prepare('DELETE FROM instructor_availability WHERE id = :id RETURNING id'); + $stmt->execute(['id' => $id]); + return (bool) $stmt->fetch(); + } +} \ No newline at end of file diff --git a/backup/old_api_20260520/app/Repositories/InstructorBreakRuleRepository.php b/backup/old_api_20260520/app/Repositories/InstructorBreakRuleRepository.php new file mode 100644 index 0000000..779e036 --- /dev/null +++ b/backup/old_api_20260520/app/Repositories/InstructorBreakRuleRepository.php @@ -0,0 +1,96 @@ +db->prepare( + 'SELECT * FROM instructor_break_rules + WHERE instructor_id = :instructor_id + ORDER BY condition_type, threshold' + ); + $stmt->execute(['instructor_id' => $instructorId]); + return $stmt->fetchAll(); + } + + public function getActiveForInstructor(int $instructorId): array + { + $stmt = $this->db->prepare( + 'SELECT * FROM instructor_break_rules + WHERE instructor_id = :instructor_id AND is_active = 1 + ORDER BY condition_type, threshold' + ); + $stmt->execute(['instructor_id' => $instructorId]); + return $stmt->fetchAll(); + } + + public function create( + int $instructorId, + string $conditionType, + string $operator, + float $threshold, + int $breakMinutes, + bool $isActive = true + ): array { + $stmt = $this->db->prepare( + 'INSERT INTO instructor_break_rules + (instructor_id, condition_type, operator, threshold, break_minutes, is_active) + VALUES (:instructor_id, :condition_type, :operator, :threshold, :break_minutes, :is_active) + RETURNING *' + ); + $stmt->execute([ + 'instructor_id' => $instructorId, + 'condition_type' => $conditionType, + 'operator' => $operator, + 'threshold' => $threshold, + 'break_minutes' => $breakMinutes, + 'is_active' => $isActive ? 1 : 0, + ]); + return $stmt->fetch(); + } + + public function update( + int $id, + string $conditionType, + string $operator, + float $threshold, + int $breakMinutes, + bool $isActive + ): bool { + $stmt = $this->db->prepare( + 'UPDATE instructor_break_rules + SET condition_type = :condition_type, operator = :operator, + threshold = :threshold, break_minutes = :break_minutes, is_active = :is_active + WHERE id = :id RETURNING id' + ); + $stmt->execute([ + 'id' => $id, + 'condition_type' => $conditionType, + 'operator' => $operator, + 'threshold' => $threshold, + 'break_minutes' => $breakMinutes, + 'is_active' => $isActive ? 1 : 0, + ]); + return (bool) $stmt->fetch(); + } + + public function delete(int $id): bool + { + $stmt = $this->db->prepare('DELETE FROM instructor_break_rules WHERE id = :id RETURNING id'); + $stmt->execute(['id' => $id]); + return (bool) $stmt->fetch(); + } + + public function setActive(int $id, bool $isActive): bool + { + $stmt = $this->db->prepare( + 'UPDATE instructor_break_rules SET is_active = :is_active WHERE id = :id RETURNING id' + ); + $stmt->execute(['id' => $id, 'is_active' => $isActive ? 1 : 0]); + return (bool) $stmt->fetch(); + } +} \ No newline at end of file diff --git a/backup/old_api_20260520/app/Repositories/InstructorHomeToTenantRepository.php b/backup/old_api_20260520/app/Repositories/InstructorHomeToTenantRepository.php new file mode 100644 index 0000000..1eede00 --- /dev/null +++ b/backup/old_api_20260520/app/Repositories/InstructorHomeToTenantRepository.php @@ -0,0 +1,56 @@ +db->prepare( + 'SELECT h.*, t.name AS tenant_name + FROM instructor_home_to_tenant h + JOIN tenants t ON t.id = h.tenant_id + WHERE h.instructor_id = :instructor_id + ORDER BY t.name' + ); + $stmt->execute(['instructor_id' => $instructorId]); + return $stmt->fetchAll(); + } + + public function getMinutes(int $instructorId, int $tenantId): ?int + { + $stmt = $this->db->prepare( + 'SELECT minutes FROM instructor_home_to_tenant + WHERE instructor_id = :instructor_id AND tenant_id = :tenant_id' + ); + $stmt->execute(['instructor_id' => $instructorId, 'tenant_id' => $tenantId]); + $result = $stmt->fetch(); + return $result ? (int) $result['minutes'] : null; + } + + public function upsert(int $instructorId, int $tenantId, int $minutes): array + { + $stmt = $this->db->prepare( + 'INSERT INTO instructor_home_to_tenant (instructor_id, tenant_id, minutes) + VALUES (:instructor_id, :tenant_id, :minutes) + ON CONFLICT (instructor_id, tenant_id) + DO UPDATE SET minutes = :minutes + RETURNING *' + ); + $stmt->execute([ + 'instructor_id' => $instructorId, + 'tenant_id' => $tenantId, + 'minutes' => $minutes, + ]); + return $stmt->fetch(); + } + + public function delete(int $id): bool + { + $stmt = $this->db->prepare('DELETE FROM instructor_home_to_tenant WHERE id = :id RETURNING id'); + $stmt->execute(['id' => $id]); + return (bool) $stmt->fetch(); + } +} \ No newline at end of file diff --git a/backup/old_api_20260520/app/Repositories/InstructorRepository.php b/backup/old_api_20260520/app/Repositories/InstructorRepository.php new file mode 100644 index 0000000..b4d90cf --- /dev/null +++ b/backup/old_api_20260520/app/Repositories/InstructorRepository.php @@ -0,0 +1,119 @@ +db->prepare('SELECT * FROM instructors WHERE tenant_id = :tenant_id ORDER BY last_name, first_name'); + $statement->execute(['tenant_id' => $tenantId]); + + return $statement->fetchAll(); + } + + public function findByUserId(int $tenantId, int $userId): ?array + { + $statement = $this->db->prepare( + 'SELECT * FROM instructors WHERE tenant_id = :tenant_id AND user_id = :user_id LIMIT 1' + ); + $statement->execute(['tenant_id' => $tenantId, 'user_id' => $userId]); + + return $statement->fetch() ?: null; + } + + public function create(int $tenantId, array $data): array + { + $timestamp = $this->now(); + $statement = $this->db->prepare( + 'INSERT INTO instructors + (tenant_id, user_id, first_name, last_name, color, notes, is_active, pre_start_buffer_minutes, created_at, updated_at) + VALUES (:tenant_id, :user_id, :first_name, :last_name, :color, :notes, :is_active, :pre_start_buffer_minutes, :created_at, :updated_at)' + ); + $statement->execute([ + 'tenant_id' => $tenantId, + 'user_id' => $data['user_id'] ?? null, + 'first_name' => $data['first_name'], + 'last_name' => $data['last_name'], + 'color' => $data['color'], + 'notes' => $data['notes'] ?? '', + 'is_active' => !empty($data['is_active']) ? 1 : 0, + 'pre_start_buffer_minutes' => max(0, (int) ($data['pre_start_buffer_minutes'] ?? 0)), + 'created_at' => $timestamp, + 'updated_at' => $timestamp, + ]); + + return $this->find($tenantId, (int) $this->lastInsertId()); + } + + public function update(int $tenantId, int $id, array $data): ?array + { + $statement = $this->db->prepare( + 'UPDATE instructors + SET user_id = :user_id, first_name = :first_name, last_name = :last_name, color = :color, + notes = :notes, is_active = :is_active, pre_start_buffer_minutes = :pre_start_buffer_minutes, updated_at = :updated_at + WHERE tenant_id = :tenant_id AND id = :id' + ); + $statement->execute([ + 'tenant_id' => $tenantId, + 'id' => $id, + 'user_id' => $data['user_id'] ?? null, + 'first_name' => $data['first_name'], + 'last_name' => $data['last_name'], + 'color' => $data['color'], + 'notes' => $data['notes'] ?? '', + 'is_active' => !empty($data['is_active']) ? 1 : 0, + 'pre_start_buffer_minutes' => max(0, (int) ($data['pre_start_buffer_minutes'] ?? 0)), + 'updated_at' => $this->now(), + ]); + + return $this->find($tenantId, $id); + } + + public function find(int $tenantId, int $id): ?array + { + $statement = $this->db->prepare('SELECT * FROM instructors WHERE tenant_id = :tenant_id AND id = :id'); + $statement->execute(['tenant_id' => $tenantId, 'id' => $id]); + + return $statement->fetch() ?: null; + } + + public function findById(int $id): ?array + { + $statement = $this->db->prepare('SELECT * FROM instructors WHERE id = :id'); + $statement->execute(['id' => $id]); + + return $statement->fetch() ?: null; + } + + public function listForTenant(int $tenantId): array + { + $statement = $this->db->prepare('SELECT * FROM instructors WHERE tenant_id = :tenant_id AND is_active = 1 ORDER BY last_name, first_name'); + $statement->execute(['tenant_id' => $tenantId]); + + return $statement->fetchAll(); + } + + public function updateSettings(int $instructorId, array $settings): bool + { + $allowed = ['max_daily_units', 'max_daily_override', 'max_weekly_units', 'max_weekly_override', 'booking_enabled']; + $sets = []; + $params = ['id' => $instructorId]; + foreach ($allowed as $key) { + if (array_key_exists($key, $settings)) { + $sets[] = "{$key} = :{$key}"; + $params[$key] = $settings[$key]; + } + } + if (empty($sets)) { + return false; + } + $sql = 'UPDATE instructors SET ' . implode(', ', $sets) . ', updated_at = :updated_at WHERE id = :id'; + $params['updated_at'] = $this->now(); + $statement = $this->db->prepare($sql); + $statement->execute($params); + return (bool) $statement->rowCount(); + } +} diff --git a/backup/old_api_20260520/app/Repositories/InstructorTenantRepository.php b/backup/old_api_20260520/app/Repositories/InstructorTenantRepository.php new file mode 100644 index 0000000..6e4a0a8 --- /dev/null +++ b/backup/old_api_20260520/app/Repositories/InstructorTenantRepository.php @@ -0,0 +1,70 @@ +db->prepare( + 'SELECT it.*, t.name AS tenant_name + FROM instructor_tenants it + JOIN tenants t ON t.id = it.tenant_id + WHERE it.instructor_id = :instructor_id + ORDER BY it.is_primary DESC, t.name' + ); + $stmt->execute(['instructor_id' => $instructorId]); + return $stmt->fetchAll(); + } + + public function isPrimary(int $instructorId, int $tenantId): bool + { + $stmt = $this->db->prepare( + 'SELECT 1 FROM instructor_tenants WHERE instructor_id = :instructor_id AND tenant_id = :tenant_id AND is_primary = 1' + ); + $stmt->execute(['instructor_id' => $instructorId, 'tenant_id' => $tenantId]); + return (bool) $stmt->fetch(); + } + + public function add(int $instructorId, int $tenantId, bool $isPrimary = false): array + { + $stmt = $this->db->prepare( + 'INSERT INTO instructor_tenants (instructor_id, tenant_id, is_primary) + VALUES (:instructor_id, :tenant_id, :is_primary) + ON CONFLICT (instructor_id, tenant_id) DO UPDATE SET is_primary = :is_primary + RETURNING *' + ); + $stmt->execute([ + 'instructor_id' => $instructorId, + 'tenant_id' => $tenantId, + 'is_primary' => $isPrimary ? 1 : 0, + ]); + return $stmt->fetch(); + } + + public function remove(int $instructorId, int $tenantId): bool + { + $stmt = $this->db->prepare( + 'DELETE FROM instructor_tenants WHERE instructor_id = :instructor_id AND tenant_id = :tenant_id RETURNING id' + ); + $stmt->execute(['instructor_id' => $instructorId, 'tenant_id' => $tenantId]); + return (bool) $stmt->fetch(); + } + + public function setPrimary(int $instructorId, int $tenantId): void + { + $this->db->beginTransaction(); + try { + $stmt = $this->db->prepare('UPDATE instructor_tenants SET is_primary = 0 WHERE instructor_id = :instructor_id'); + $stmt->execute(['instructor_id' => $instructorId]); + $stmt = $this->db->prepare('UPDATE instructor_tenants SET is_primary = 1 WHERE instructor_id = :instructor_id AND tenant_id = :tenant_id'); + $stmt->execute(['instructor_id' => $instructorId, 'tenant_id' => $tenantId]); + $this->db->commit(); + } catch (\Throwable $e) { + $this->db->rollBack(); + throw $e; + } + } +} \ No newline at end of file diff --git a/backup/old_api_20260520/app/Repositories/InstructorTravelTimeRepository.php b/backup/old_api_20260520/app/Repositories/InstructorTravelTimeRepository.php new file mode 100644 index 0000000..5d345df --- /dev/null +++ b/backup/old_api_20260520/app/Repositories/InstructorTravelTimeRepository.php @@ -0,0 +1,66 @@ +db->prepare( + 'SELECT t.*, + ft.name AS from_tenant_name, + tt.name AS to_tenant_name + FROM instructor_travel_times t + JOIN tenants ft ON ft.id = t.from_tenant_id + JOIN tenants tt ON tt.id = t.to_tenant_id + WHERE t.instructor_id = :instructor_id + ORDER BY ft.name, tt.name' + ); + $stmt->execute(['instructor_id' => $instructorId]); + return $stmt->fetchAll(); + } + + public function getMinutes(int $instructorId, int $fromTenantId, int $toTenantId): ?int + { + $stmt = $this->db->prepare( + 'SELECT minutes FROM instructor_travel_times + WHERE instructor_id = :instructor_id + AND from_tenant_id = :from_tenant_id + AND to_tenant_id = :to_tenant_id' + ); + $stmt->execute([ + 'instructor_id' => $instructorId, + 'from_tenant_id' => $fromTenantId, + 'to_tenant_id' => $toTenantId, + ]); + $result = $stmt->fetch(); + return $result ? (int) $result['minutes'] : null; + } + + public function upsert(int $instructorId, int $fromTenantId, int $toTenantId, int $minutes): array + { + $stmt = $this->db->prepare( + 'INSERT INTO instructor_travel_times (instructor_id, from_tenant_id, to_tenant_id, minutes) + VALUES (:instructor_id, :from_tenant_id, :to_tenant_id, :minutes) + ON CONFLICT (instructor_id, from_tenant_id, to_tenant_id) + DO UPDATE SET minutes = :minutes + RETURNING *' + ); + $stmt->execute([ + 'instructor_id' => $instructorId, + 'from_tenant_id' => $fromTenantId, + 'to_tenant_id' => $toTenantId, + 'minutes' => $minutes, + ]); + return $stmt->fetch(); + } + + public function delete(int $id): bool + { + $stmt = $this->db->prepare('DELETE FROM instructor_travel_times WHERE id = :id RETURNING id'); + $stmt->execute(['id' => $id]); + return (bool) $stmt->fetch(); + } +} \ No newline at end of file diff --git a/backup/old_api_20260520/app/Repositories/InstructorVacationRepository.php b/backup/old_api_20260520/app/Repositories/InstructorVacationRepository.php new file mode 100644 index 0000000..0824592 --- /dev/null +++ b/backup/old_api_20260520/app/Repositories/InstructorVacationRepository.php @@ -0,0 +1,54 @@ +db->prepare( + 'SELECT * FROM instructor_vacations + WHERE instructor_id = :instructor_id + ORDER BY date_from ASC' + ); + $stmt->execute(['instructor_id' => $instructorId]); + return $stmt->fetchAll(); + } + + public function getForInstructorInRange(int $instructorId, string $from, string $to): array + { + $stmt = $this->db->prepare( + 'SELECT * FROM instructor_vacations + WHERE instructor_id = :instructor_id + AND date_from <= :to AND date_to >= :from + ORDER BY date_from ASC' + ); + $stmt->execute(['instructor_id' => $instructorId, 'from' => $from, 'to' => $to]); + return $stmt->fetchAll(); + } + + public function create(int $instructorId, string $dateFrom, string $dateTo, string $notes = ''): array + { + $stmt = $this->db->prepare( + 'INSERT INTO instructor_vacations (instructor_id, date_from, date_to, notes) + VALUES (:instructor_id, :date_from, :date_to, :notes) + RETURNING *' + ); + $stmt->execute([ + 'instructor_id' => $instructorId, + 'date_from' => $dateFrom, + 'date_to' => $dateTo, + 'notes' => $notes, + ]); + return $stmt->fetch(); + } + + public function delete(int $id): bool + { + $stmt = $this->db->prepare('DELETE FROM instructor_vacations WHERE id = :id RETURNING id'); + $stmt->execute(['id' => $id]); + return (bool) $stmt->fetch(); + } +} \ No newline at end of file diff --git a/backup/old_api_20260520/app/Repositories/PrivateAppointmentRepository.php b/backup/old_api_20260520/app/Repositories/PrivateAppointmentRepository.php new file mode 100644 index 0000000..f2040ec --- /dev/null +++ b/backup/old_api_20260520/app/Repositories/PrivateAppointmentRepository.php @@ -0,0 +1,56 @@ +db->prepare( + 'SELECT * FROM private_appointments + WHERE instructor_id = :instructor_id + ORDER BY date ASC, time_from ASC' + ); + $stmt->execute(['instructor_id' => $instructorId]); + return $stmt->fetchAll(); + } + + public function getForInstructorInRange(int $instructorId, string $from, string $to): array + { + $stmt = $this->db->prepare( + 'SELECT * FROM private_appointments + WHERE instructor_id = :instructor_id + AND date >= :from AND date <= :to + ORDER BY date ASC, time_from ASC' + ); + $stmt->execute(['instructor_id' => $instructorId, 'from' => $from, 'to' => $to]); + return $stmt->fetchAll(); + } + + public function create(int $instructorId, string $title, string $date, string $timeFrom, string $timeTo, string $color = '#9ca3af'): array + { + $stmt = $this->db->prepare( + 'INSERT INTO private_appointments (instructor_id, title, date, time_from, time_to, color) + VALUES (:instructor_id, :title, :date, :time_from, :time_to, :color) + RETURNING *' + ); + $stmt->execute([ + 'instructor_id' => $instructorId, + 'title' => $title, + 'date' => $date, + 'time_from' => $timeFrom, + 'time_to' => $timeTo, + 'color' => $color, + ]); + return $stmt->fetch(); + } + + public function delete(int $id): bool + { + $stmt = $this->db->prepare('DELETE FROM private_appointments WHERE id = :id RETURNING id'); + $stmt->execute(['id' => $id]); + return (bool) $stmt->fetch(); + } +} \ No newline at end of file diff --git a/backup/old_api_20260520/app/Repositories/ReferenceDataRepository.php b/backup/old_api_20260520/app/Repositories/ReferenceDataRepository.php new file mode 100644 index 0000000..715930a --- /dev/null +++ b/backup/old_api_20260520/app/Repositories/ReferenceDataRepository.php @@ -0,0 +1,515 @@ + 'private_block', 'name' => 'Privat', 'color' => '#6B7280', 'default_duration' => 60, 'category' => 'private', 'sort_order' => 900], + ['system_key' => 'work_vehicle_care', 'name' => 'Fahrzeugpflege', 'color' => '#0E5D80', 'default_duration' => 60, 'category' => 'work', 'sort_order' => 910], + ['system_key' => 'work_office', 'name' => 'Buero', 'color' => '#8B5A2B', 'default_duration' => 60, 'category' => 'work', 'sort_order' => 920], + ['system_key' => 'work_transfer_drive', 'name' => 'Wechselfahrt', 'color' => '#3B7A57', 'default_duration' => 45, 'category' => 'work', 'sort_order' => 930], + ['system_key' => 'work_other', 'name' => 'Sonstige Taetigkeit', 'color' => '#7C3AED', 'default_duration' => 60, 'category' => 'work', 'sort_order' => 940], + ['system_key' => 'theory_a', 'name' => 'Theorie A', 'color' => '#C2410C', 'default_duration' => 90, 'category' => 'theory', 'sort_order' => 950], + ['system_key' => 'theory_b', 'name' => 'Theorie B', 'color' => '#A74826', 'default_duration' => 90, 'category' => 'theory', 'sort_order' => 960], + ['system_key' => 'theory_c', 'name' => 'Theorie C', 'color' => '#B45309', 'default_duration' => 90, 'category' => 'theory', 'sort_order' => 970], + ['system_key' => 'theory_d', 'name' => 'Theorie D', 'color' => '#BE123C', 'default_duration' => 90, 'category' => 'theory', 'sort_order' => 980], + ['system_key' => 'theory_t', 'name' => 'Theorie T', 'color' => '#047857', 'default_duration' => 90, 'category' => 'theory', 'sort_order' => 990], + ['system_key' => 'theory_bkf', 'name' => 'Theorie BKF', 'color' => '#1D4ED8', 'default_duration' => 90, 'category' => 'theory', 'sort_order' => 1000], + ]; + + public function requirementKeyForLessonType(array $lessonType): string + { + $key = trim((string) ($lessonType['requirement_key'] ?? '')); + return $key !== '' ? $key : 'lesson_type_' . (int) $lessonType['id']; + } + + public function lessonTypes(int $tenantId): array + { + $this->ensureDefaultPlanningTypes($tenantId); + $statement = $this->db->prepare('SELECT * FROM lesson_types WHERE tenant_id = :tenant_id ORDER BY sort_order, name'); + $statement->execute(['tenant_id' => $tenantId]); + $lessonTypes = $statement->fetchAll(); + $visibilityMap = $this->lessonTypeVisibilityMap($tenantId); + + foreach ($lessonTypes as &$lessonType) { + $lessonType['visible_template_ids'] = $visibilityMap[(int) $lessonType['id']] ?? []; + $lessonType['effective_requirement_key'] = $this->requirementKeyForLessonType($lessonType); + } + + return $lessonTypes; + } + + public function createLessonType(int $tenantId, array $data): array + { + $timestamp = $this->now(); + $statement = $this->db->prepare( + 'INSERT INTO lesson_types + (tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at) + VALUES + (:tenant_id, :name, :color, :default_duration, :category, :requirement_key, :is_billable, :is_counted, :is_rest_relevant, :fixed_duration, :sort_order, :created_at, :updated_at)' + ); + $statement->execute([ + 'tenant_id' => $tenantId, + 'name' => $data['name'], + 'color' => $data['color'], + 'default_duration' => (int) $data['default_duration'], + 'category' => $data['category'], + 'requirement_key' => $data['requirement_key'] ?: null, + 'is_billable' => !empty($data['is_billable']) ? 1 : 0, + 'is_counted' => !empty($data['is_counted']) ? 1 : 0, + 'is_rest_relevant' => !empty($data['is_rest_relevant']) ? 1 : 0, + 'fixed_duration' => !empty($data['fixed_duration']) ? 1 : 0, + 'sort_order' => (int) ($data['sort_order'] ?? 0), + 'created_at' => $timestamp, + 'updated_at' => $timestamp, + ]); + $lessonTypeId = (int) $this->lastInsertId(); + $this->replaceLessonTypeVisibility($tenantId, $lessonTypeId, $data['visible_template_ids'] ?? null); + + return $this->findLessonType($tenantId, $lessonTypeId); + } + + public function updateLessonType(int $tenantId, int $id, array $data): ?array + { + $statement = $this->db->prepare( + 'UPDATE lesson_types + SET name = :name, color = :color, default_duration = :default_duration, category = :category, + requirement_key = :requirement_key, is_billable = :is_billable, is_counted = :is_counted, + is_rest_relevant = :is_rest_relevant, fixed_duration = :fixed_duration, sort_order = :sort_order, + updated_at = :updated_at + WHERE id = :id AND tenant_id = :tenant_id' + ); + $statement->execute([ + 'id' => $id, + 'tenant_id' => $tenantId, + 'name' => $data['name'], + 'color' => $data['color'], + 'default_duration' => (int) $data['default_duration'], + 'category' => $data['category'], + 'requirement_key' => $data['requirement_key'] ?: null, + 'is_billable' => !empty($data['is_billable']) ? 1 : 0, + 'is_counted' => !empty($data['is_counted']) ? 1 : 0, + 'is_rest_relevant' => !empty($data['is_rest_relevant']) ? 1 : 0, + 'fixed_duration' => !empty($data['fixed_duration']) ? 1 : 0, + 'sort_order' => (int) ($data['sort_order'] ?? 0), + 'updated_at' => $this->now(), + ]); + + $this->replaceLessonTypeVisibility($tenantId, $id, $data['visible_template_ids'] ?? null); + + return $this->findLessonType($tenantId, $id); + } + + public function deleteLessonType(int $tenantId, int $id): bool + { + $lessonType = $this->findLessonTypeRecord($tenantId, $id); + if ($lessonType === null) { + return false; + } + + if (($lessonType['system_key'] ?? null) === 'private_block') { + throw new \RuntimeException('Der feste Privat-Baustein kann nicht geloescht werden.'); + } + + $usage = $this->db->prepare('SELECT COUNT(*) AS count FROM appointments WHERE tenant_id = :tenant_id AND lesson_type_id = :lesson_type_id'); + $usage->execute(['tenant_id' => $tenantId, 'lesson_type_id' => $id]); + if ((int) ($usage->fetch()['count'] ?? 0) > 0) { + throw new \RuntimeException('Diese Stundenart wird bereits in Terminen verwendet und kann nicht geloescht werden.'); + } + + $this->db->beginTransaction(); + try { + $visibility = $this->db->prepare('DELETE FROM lesson_type_template_visibility WHERE lesson_type_id = :lesson_type_id'); + $visibility->execute(['lesson_type_id' => $id]); + + $delete = $this->db->prepare('DELETE FROM lesson_types WHERE tenant_id = :tenant_id AND id = :id'); + $delete->execute(['tenant_id' => $tenantId, 'id' => $id]); + + $this->db->commit(); + return true; + } catch (\Throwable $exception) { + $this->db->rollBack(); + throw $exception; + } + } + + public function reorderLessonTypes(int $tenantId, array $lessonTypeIds): array + { + $allowedIds = array_map( + static fn (array $lessonType): int => (int) $lessonType['id'], + $this->lessonTypes($tenantId) + ); + + $statement = $this->db->prepare( + 'UPDATE lesson_types + SET sort_order = :sort_order, updated_at = :updated_at + WHERE tenant_id = :tenant_id AND id = :id' + ); + + $sortOrder = 1; + foreach ($lessonTypeIds as $lessonTypeId) { + $lessonTypeId = (int) $lessonTypeId; + if (!in_array($lessonTypeId, $allowedIds, true)) { + continue; + } + + $statement->execute([ + 'sort_order' => $sortOrder, + 'updated_at' => $this->now(), + 'tenant_id' => $tenantId, + 'id' => $lessonTypeId, + ]); + $sortOrder++; + } + + return $this->lessonTypes($tenantId); + } + + public function findLessonType(int $tenantId, int $id): ?array + { + $statement = $this->db->prepare('SELECT * FROM lesson_types WHERE tenant_id = :tenant_id AND id = :id'); + $statement->execute(['tenant_id' => $tenantId, 'id' => $id]); + $lessonType = $statement->fetch() ?: null; + if ($lessonType === null) { + return null; + } + + $lessonType['visible_template_ids'] = $this->visibleTemplateIdsForLessonType($tenantId, $id); + $lessonType['effective_requirement_key'] = $this->requirementKeyForLessonType($lessonType); + + return $lessonType; + } + + public function templates(int $tenantId): array + { + $statement = $this->db->prepare( + 'SELECT * FROM license_class_templates WHERE tenant_id = :tenant_id ORDER BY name' + ); + $statement->execute(['tenant_id' => $tenantId]); + $templates = $statement->fetchAll(); + + foreach ($templates as &$template) { + $template['requirements'] = $this->templateRequirements((int) $template['id']); + $template['lesson_type_ids'] = $this->lessonTypeIdsForTemplate((int) $template['id']); + } + + return $templates; + } + + public function templateRequirements(int $templateId): array + { + $statement = $this->db->prepare( + 'SELECT * FROM license_template_requirements WHERE template_id = :template_id ORDER BY sort_order, phase_label, label' + ); + $statement->execute(['template_id' => $templateId]); + + return $statement->fetchAll(); + } + + public function createTemplate(int $tenantId, array $data): array + { + $timestamp = $this->now(); + $statement = $this->db->prepare( + 'INSERT INTO license_class_templates + (tenant_id, code, name, is_combination, created_at, updated_at) + VALUES (:tenant_id, :code, :name, :is_combination, :created_at, :updated_at)' + ); + $statement->execute([ + 'tenant_id' => $tenantId, + 'code' => $data['code'], + 'name' => $data['name'], + 'is_combination' => !empty($data['is_combination']) ? 1 : 0, + 'created_at' => $timestamp, + 'updated_at' => $timestamp, + ]); + $templateId = (int) $this->lastInsertId(); + + $this->replaceTemplateRequirements($templateId, $data['requirements'] ?? []); + $this->attachTemplateToExistingStudentLessonTypes($tenantId, $templateId); + + return $this->findTemplate($tenantId, $templateId); + } + + public function updateTemplate(int $tenantId, int $id, array $data): ?array + { + $statement = $this->db->prepare( + 'UPDATE license_class_templates + SET code = :code, name = :name, is_combination = :is_combination, updated_at = :updated_at + WHERE id = :id AND tenant_id = :tenant_id' + ); + $statement->execute([ + 'id' => $id, + 'tenant_id' => $tenantId, + 'code' => $data['code'], + 'name' => $data['name'], + 'is_combination' => !empty($data['is_combination']) ? 1 : 0, + 'updated_at' => $this->now(), + ]); + + $this->replaceTemplateRequirements($id, $data['requirements'] ?? []); + + return $this->findTemplate($tenantId, $id); + } + + public function updateLessonTypeMatrix(int $tenantId, array $rows): array + { + $templates = $this->templates($tenantId); + $templateIds = array_map(static fn (array $template): int => (int) $template['id'], $templates); + $allowedLessonTypeIds = array_map( + static fn (array $lessonType): int => (int) $lessonType['id'], + array_filter($this->lessonTypes($tenantId), static fn (array $lessonType): bool => $lessonType['category'] === 'student') + ); + + $delete = $this->db->prepare( + 'DELETE FROM lesson_type_template_visibility + WHERE template_id = :template_id + AND lesson_type_id IN ( + SELECT id FROM lesson_types WHERE tenant_id = :tenant_id AND category = \'student\' + )' + ); + $insert = $this->db->prepare( + 'INSERT OR IGNORE INTO lesson_type_template_visibility (lesson_type_id, template_id) + VALUES (:lesson_type_id, :template_id)' + ); + + foreach ($rows as $row) { + $templateId = (int) ($row['template_id'] ?? 0); + if (!in_array($templateId, $templateIds, true)) { + continue; + } + + $delete->execute([ + 'template_id' => $templateId, + 'tenant_id' => $tenantId, + ]); + + foreach ((array) ($row['lesson_type_ids'] ?? []) as $lessonTypeId) { + $lessonTypeId = (int) $lessonTypeId; + if (!in_array($lessonTypeId, $allowedLessonTypeIds, true)) { + continue; + } + + $insert->execute([ + 'lesson_type_id' => $lessonTypeId, + 'template_id' => $templateId, + ]); + } + } + + return $this->templates($tenantId); + } + + public function isLessonTypeVisibleForTemplate(int $tenantId, int $lessonTypeId, int $templateId): bool + { + $lessonType = $this->findLessonType($tenantId, $lessonTypeId); + if ($lessonType === null) { + return false; + } + + if ($lessonType['category'] !== 'student') { + return true; + } + + return in_array($templateId, array_map('intval', $lessonType['visible_template_ids'] ?? []), true); + } + + private function replaceTemplateRequirements(int $templateId, array $requirements): void + { + $delete = $this->db->prepare('DELETE FROM license_template_requirements WHERE template_id = :template_id'); + $delete->execute(['template_id' => $templateId]); + + $insert = $this->db->prepare( + 'INSERT INTO license_template_requirements + (template_id, requirement_key, label, required_units, sort_order, phase_label) + VALUES (:template_id, :requirement_key, :label, :required_units, :sort_order, :phase_label)' + ); + + foreach ($requirements as $index => $requirement) { + if (($requirement['label'] ?? '') === '') { + continue; + } + + $insert->execute([ + 'template_id' => $templateId, + 'requirement_key' => $requirement['requirement_key'], + 'label' => $requirement['label'], + 'required_units' => (int) $requirement['required_units'], + 'sort_order' => (int) ($requirement['sort_order'] ?? $index), + 'phase_label' => trim((string) ($requirement['phase_label'] ?? '')), + ]); + } + } + + public function findTemplate(int $tenantId, int $id): ?array + { + $statement = $this->db->prepare( + 'SELECT * FROM license_class_templates WHERE tenant_id = :tenant_id AND id = :id' + ); + $statement->execute(['tenant_id' => $tenantId, 'id' => $id]); + $template = $statement->fetch() ?: null; + + if ($template !== null) { + $template['requirements'] = $this->templateRequirements($id); + $template['lesson_type_ids'] = $this->lessonTypeIdsForTemplate($id); + } + + return $template; + } + + private function replaceLessonTypeVisibility(int $tenantId, int $lessonTypeId, ?array $visibleTemplateIds): void + { + $lessonType = $this->findLessonTypeRecord($tenantId, $lessonTypeId); + if ($lessonType === null) { + return; + } + + $delete = $this->db->prepare('DELETE FROM lesson_type_template_visibility WHERE lesson_type_id = :lesson_type_id'); + $delete->execute(['lesson_type_id' => $lessonTypeId]); + + if ($lessonType['category'] !== 'student') { + return; + } + + $templateIds = $visibleTemplateIds ?? $this->templateIdsForTenant($tenantId); + $templateIds = array_values(array_unique(array_map('intval', $templateIds))); + if ($templateIds === []) { + return; + } + + $allowedTemplateIds = $this->templateIdsForTenant($tenantId); + $insert = $this->db->prepare( + 'INSERT OR IGNORE INTO lesson_type_template_visibility (lesson_type_id, template_id) + VALUES (:lesson_type_id, :template_id)' + ); + + foreach ($templateIds as $templateId) { + if (!in_array($templateId, $allowedTemplateIds, true)) { + continue; + } + + $insert->execute([ + 'lesson_type_id' => $lessonTypeId, + 'template_id' => $templateId, + ]); + } + } + + private function lessonTypeVisibilityMap(int $tenantId): array + { + $statement = $this->db->prepare( + 'SELECT lt.id AS lesson_type_id, vis.template_id + FROM lesson_types lt + LEFT JOIN lesson_type_template_visibility vis ON vis.lesson_type_id = lt.id + WHERE lt.tenant_id = :tenant_id AND lt.category = \'student\' + ORDER BY vis.template_id' + ); + $statement->execute(['tenant_id' => $tenantId]); + + $map = []; + foreach ($statement->fetchAll() as $row) { + $lessonTypeId = (int) $row['lesson_type_id']; + $map[$lessonTypeId] ??= []; + if ($row['template_id'] !== null) { + $map[$lessonTypeId][] = (int) $row['template_id']; + } + } + + return $map; + } + + private function visibleTemplateIdsForLessonType(int $tenantId, int $lessonTypeId): array + { + $lessonType = $this->findLessonTypeRecord($tenantId, $lessonTypeId); + if ($lessonType === null || $lessonType['category'] !== 'student') { + return []; + } + + $statement = $this->db->prepare( + 'SELECT template_id FROM lesson_type_template_visibility + WHERE lesson_type_id = :lesson_type_id + ORDER BY template_id' + ); + $statement->execute(['lesson_type_id' => $lessonTypeId]); + + return array_map( + static fn (array $row): int => (int) $row['template_id'], + $statement->fetchAll() + ); + } + + private function lessonTypeIdsForTemplate(int $templateId): array + { + $statement = $this->db->prepare( + 'SELECT lesson_type_id FROM lesson_type_template_visibility + WHERE template_id = :template_id + ORDER BY lesson_type_id' + ); + $statement->execute(['template_id' => $templateId]); + + return array_map( + static fn (array $row): int => (int) $row['lesson_type_id'], + $statement->fetchAll() + ); + } + + private function templateIdsForTenant(int $tenantId): array + { + $statement = $this->db->prepare('SELECT id FROM license_class_templates WHERE tenant_id = :tenant_id ORDER BY id'); + $statement->execute(['tenant_id' => $tenantId]); + + return array_map( + static fn (array $row): int => (int) $row['id'], + $statement->fetchAll() + ); + } + + private function attachTemplateToExistingStudentLessonTypes(int $tenantId, int $templateId): void + { + $statement = $this->db->prepare( + 'INSERT OR IGNORE INTO lesson_type_template_visibility (lesson_type_id, template_id) + SELECT id, :template_id FROM lesson_types + WHERE tenant_id = :tenant_id AND category = \'student\'' + ); + $statement->execute([ + 'template_id' => $templateId, + 'tenant_id' => $tenantId, + ]); + } + + private function findLessonTypeRecord(int $tenantId, int $id): ?array + { + $statement = $this->db->prepare('SELECT * FROM lesson_types WHERE tenant_id = :tenant_id AND id = :id'); + $statement->execute(['tenant_id' => $tenantId, 'id' => $id]); + + return $statement->fetch() ?: null; + } + + private function ensureDefaultPlanningTypes(int $tenantId): void + { + $statement = $this->db->prepare( + 'INSERT OR IGNORE INTO lesson_types + (tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, system_key, created_at, updated_at) + VALUES + (:tenant_id, :name, :color, :default_duration, :category, NULL, 0, 0, :is_rest_relevant, 0, :sort_order, :system_key, :created_at, :updated_at)' + ); + + $timestamp = $this->now(); + foreach (self::DEFAULT_PLANNING_TYPES as $row) { + $statement->execute([ + 'tenant_id' => $tenantId, + 'name' => $row['name'], + 'color' => $row['color'], + 'default_duration' => $row['default_duration'], + 'category' => $row['category'], + 'is_rest_relevant' => $row['category'] === 'private' ? 0 : 1, + 'sort_order' => $row['sort_order'], + 'system_key' => $row['system_key'], + 'created_at' => $timestamp, + 'updated_at' => $timestamp, + ]); + } + } +} diff --git a/backup/old_api_20260520/app/Repositories/StudentRepository.php b/backup/old_api_20260520/app/Repositories/StudentRepository.php new file mode 100644 index 0000000..8042efa --- /dev/null +++ b/backup/old_api_20260520/app/Repositories/StudentRepository.php @@ -0,0 +1,294 @@ +db->prepare( + 'SELECT s.*, t.code AS template_code, t.name AS template_name, t.is_combination AS template_is_combination + FROM students s + JOIN license_class_templates t ON t.id = s.template_id + WHERE s.tenant_id = :tenant_id + ORDER BY s.last_name, s.first_name' + ); + $statement->execute(['tenant_id' => $tenantId]); + + return $statement->fetchAll(); + } + + public function find(int $tenantId, int $id): ?array + { + $statement = $this->db->prepare( + 'SELECT s.*, t.code AS template_code, t.name AS template_name, t.is_combination AS template_is_combination + FROM students s + JOIN license_class_templates t ON t.id = s.template_id + WHERE s.tenant_id = :tenant_id AND s.id = :id' + ); + $statement->execute(['tenant_id' => $tenantId, 'id' => $id]); + $student = $statement->fetch() ?: null; + + if ($student !== null) { + $student['requirements'] = $this->requirements($id); + } + + return $student; + } + + public function create(int $tenantId, array $data): array + { + $timestamp = $this->now(); + $statement = $this->db->prepare( + 'INSERT INTO students + (tenant_id, template_id, first_name, last_name, phone, email, needs_glasses, notes, status, created_at, updated_at) + VALUES (:tenant_id, :template_id, :first_name, :last_name, :phone, :email, :needs_glasses, :notes, :status, :created_at, :updated_at)' + ); + $statement->execute([ + 'tenant_id' => $tenantId, + 'template_id' => $data['template_id'], + 'first_name' => $data['first_name'], + 'last_name' => $data['last_name'], + 'phone' => $data['phone'] ?? '', + 'email' => $data['email'] ?? '', + 'needs_glasses' => !empty($data['needs_glasses']) ? 1 : 0, + 'notes' => $data['notes'] ?? '', + 'status' => $data['status'] ?? 'active', + 'created_at' => $timestamp, + 'updated_at' => $timestamp, + ]); + + $studentId = (int) $this->lastInsertId(); + $this->snapshotRequirements($studentId, (int) $data['template_id']); + + return $this->find($tenantId, $studentId); + } + + public function update(int $tenantId, int $id, array $data): ?array + { + $statement = $this->db->prepare( + 'UPDATE students + SET template_id = :template_id, first_name = :first_name, last_name = :last_name, phone = :phone, email = :email, needs_glasses = :needs_glasses, + notes = :notes, status = :status, updated_at = :updated_at + WHERE tenant_id = :tenant_id AND id = :id' + ); + $statement->execute([ + 'tenant_id' => $tenantId, + 'id' => $id, + 'template_id' => $data['template_id'], + 'first_name' => $data['first_name'], + 'last_name' => $data['last_name'], + 'phone' => $data['phone'] ?? '', + 'email' => $data['email'] ?? '', + 'needs_glasses' => !empty($data['needs_glasses']) ? 1 : 0, + 'notes' => $data['notes'] ?? '', + 'status' => $data['status'] ?? 'active', + 'updated_at' => $this->now(), + ]); + + return $this->find($tenantId, $id); + } + + public function delete(int $tenantId, int $id): bool + { + $student = $this->find($tenantId, $id); + if ($student === null) { + return false; + } + + $this->db->beginTransaction(); + try { + $updateAppointments = $this->db->prepare( + 'UPDATE appointments SET student_id = NULL, updated_at = :updated_at WHERE tenant_id = :tenant_id AND student_id = :student_id' + ); + $updateAppointments->execute([ + 'tenant_id' => $tenantId, + 'student_id' => $id, + 'updated_at' => $this->now(), + ]); + + $deleteSnapshots = $this->db->prepare('DELETE FROM student_requirement_snapshots WHERE student_id = :student_id'); + $deleteSnapshots->execute(['student_id' => $id]); + + $deleteStudent = $this->db->prepare('DELETE FROM students WHERE tenant_id = :tenant_id AND id = :id'); + $deleteStudent->execute(['tenant_id' => $tenantId, 'id' => $id]); + + $this->db->commit(); + return true; + } catch (\Throwable $exception) { + $this->db->rollBack(); + throw $exception; + } + } + + public function requirements(int $studentId): array + { + $this->syncRequirementSnapshots($studentId); + + $statement = $this->db->prepare( + 'SELECT * FROM student_requirement_snapshots WHERE student_id = :student_id ORDER BY sort_order, phase_label, label' + ); + $statement->execute(['student_id' => $studentId]); + + return $statement->fetchAll(); + } + + public function updatePriorCompletedUnits(int $tenantId, int $studentId, array $rows): ?array + { + $student = $this->find($tenantId, $studentId); + if ($student === null) { + return null; + } + + $statement = $this->db->prepare( + 'UPDATE student_requirement_snapshots + SET prior_completed_units = :prior_completed_units + WHERE student_id = :student_id AND requirement_key = :requirement_key' + ); + + foreach ($rows as $row) { + $statement->execute([ + 'student_id' => $studentId, + 'requirement_key' => (string) ($row['requirement_key'] ?? ''), + 'prior_completed_units' => max(0, (int) ($row['prior_completed_units'] ?? 0)), + ]); + } + + return $this->find($tenantId, $studentId); + } + + private function snapshotRequirements(int $studentId, int $templateId): void + { + $this->syncRequirementSnapshots($studentId, $templateId); + } + + private function syncRequirementSnapshots(int $studentId, ?int $templateId = null): void + { + if ($templateId === null) { + $student = $this->db->prepare('SELECT template_id FROM students WHERE id = :student_id'); + $student->execute(['student_id' => $studentId]); + $templateId = (int) ($student->fetch()['template_id'] ?? 0); + } + + if ($templateId <= 0) { + return; + } + + $requirements = $this->db->prepare( + 'SELECT requirement_key, label, required_units, sort_order, phase_label + FROM license_template_requirements WHERE template_id = :template_id' + ); + $requirements->execute(['template_id' => $templateId]); + $rows = []; + + foreach ($requirements->fetchAll() as $row) { + $rows[$row['requirement_key']] = $row; + } + $hasPhasedRequirements = array_reduce( + $rows, + static fn (bool $carry, array $row): bool => $carry || trim((string) ($row['phase_label'] ?? '')) !== '', + false + ); + + $lessonTypes = $this->db->prepare( + 'SELECT COALESCE(NULLIF(TRIM(lt.requirement_key), \'\'), \'lesson_type_\' || lt.id) AS requirement_key, + lt.name AS label, + lt.sort_order + FROM lesson_types lt + JOIN lesson_type_template_visibility vis ON vis.lesson_type_id = lt.id + WHERE vis.template_id = :template_id + AND lt.category = \'student\' + AND lt.is_counted = 1 + ORDER BY lt.sort_order, lt.name' + ); + $lessonTypes->execute(['template_id' => $templateId]); + + foreach ($lessonTypes->fetchAll() as $row) { + if (isset($rows[$row['requirement_key']])) { + continue; + } + + if ($hasPhasedRequirements) { + $hasMatchingPhasedRequirement = false; + foreach ($rows as $existingRow) { + $phaseLabel = trim((string) ($existingRow['phase_label'] ?? '')); + if ($phaseLabel === '') { + continue; + } + + if (str_ends_with((string) $existingRow['requirement_key'], '::' . $row['requirement_key'])) { + $hasMatchingPhasedRequirement = true; + break; + } + } + + if ($hasMatchingPhasedRequirement) { + continue; + } + } + + $rows[$row['requirement_key']] = [ + 'requirement_key' => $row['requirement_key'], + 'label' => $row['label'], + 'required_units' => 0, + 'sort_order' => $row['sort_order'], + ]; + } + + $insert = $this->db->prepare( + 'INSERT INTO student_requirement_snapshots + (student_id, requirement_key, label, required_units, prior_completed_units, sort_order, phase_label) + SELECT :student_id, :requirement_key, :label, :required_units, 0, :sort_order, :phase_label + WHERE NOT EXISTS ( + SELECT 1 FROM student_requirement_snapshots + WHERE student_id = :student_id AND requirement_key = :requirement_key + )' + ); + $update = $this->db->prepare( + 'UPDATE student_requirement_snapshots + SET label = :label, + required_units = :required_units, + sort_order = :sort_order, + phase_label = :phase_label + WHERE student_id = :student_id AND requirement_key = :requirement_key' + ); + + foreach ($rows as $row) { + $insert->execute([ + 'student_id' => $studentId, + 'requirement_key' => $row['requirement_key'], + 'label' => $row['label'], + 'required_units' => $row['required_units'], + 'sort_order' => $row['sort_order'], + 'phase_label' => $row['phase_label'] ?? '', + ]); + $update->execute([ + 'student_id' => $studentId, + 'requirement_key' => $row['requirement_key'], + 'label' => $row['label'], + 'required_units' => $row['required_units'], + 'sort_order' => $row['sort_order'], + 'phase_label' => $row['phase_label'] ?? '', + ]); + } + + if ($rows !== []) { + $placeholders = implode(', ', array_fill(0, count($rows), '?')); + $delete = $this->db->prepare( + "DELETE FROM student_requirement_snapshots + WHERE student_id = ? + AND requirement_key NOT IN ($placeholders) + AND prior_completed_units = 0 + AND NOT EXISTS ( + SELECT 1 FROM appointment_units au + JOIN appointments a ON a.id = au.appointment_id + WHERE a.student_id = student_requirement_snapshots.student_id + AND au.requirement_key = student_requirement_snapshots.requirement_key + )" + ); + $delete->execute(array_merge([$studentId], array_keys($rows))); + } + } +} diff --git a/backup/old_api_20260520/app/Repositories/StudentTenantRepository.php b/backup/old_api_20260520/app/Repositories/StudentTenantRepository.php new file mode 100644 index 0000000..d4135de --- /dev/null +++ b/backup/old_api_20260520/app/Repositories/StudentTenantRepository.php @@ -0,0 +1,77 @@ +db->prepare( + 'SELECT * FROM student_tenants WHERE student_user_id = :user_id AND tenant_id = :tenant_id' + ); + $stmt->execute(['user_id' => $userId, 'tenant_id' => $tenantId]); + $result = $stmt->fetch(); + return $result ?: null; + } + + public function create(int $userId, int $tenantId, string $status = 'pending'): array + { + $stmt = $this->db->prepare( + 'INSERT INTO student_tenants (student_user_id, tenant_id, status) + VALUES (:user_id, :tenant_id, :status) RETURNING *' + ); + $stmt->execute([ + 'user_id' => $userId, + 'tenant_id' => $tenantId, + 'status' => $status, + ]); + return $stmt->fetch(); + } + + public function confirm(int $id, int $confirmedBy): bool + { + $stmt = $this->db->prepare( + 'UPDATE student_tenants + SET status = \'active\', confirmed_at = NOW(), confirmed_by = :confirmed_by + WHERE id = :id RETURNING id' + ); + $stmt->execute(['id' => $id, 'confirmed_by' => $confirmedBy]); + return (bool) $stmt->fetch(); + } + + public function suspend(int $id): bool + { + $stmt = $this->db->prepare( + 'UPDATE student_tenants SET status = \'suspended\' WHERE id = :id RETURNING id' + ); + $stmt->execute(['id' => $id]); + return (bool) $stmt->fetch(); + } + + public function getPendingForTenant(int $tenantId): array + { + $stmt = $this->db->prepare( + 'SELECT st.*, u.email, u.first_name, u.last_name + FROM student_tenants st + JOIN users u ON u.id = st.student_user_id + WHERE st.tenant_id = :tenant_id AND st.status = \'pending\' + ORDER BY st.registered_at DESC' + ); + $stmt->execute(['tenant_id' => $tenantId]); + return $stmt->fetchAll(); + } + + public function getActiveForUser(int $userId): array + { + $stmt = $this->db->prepare( + 'SELECT st.*, t.name AS tenant_name + FROM student_tenants st + JOIN tenants t ON t.id = st.tenant_id + WHERE st.student_user_id = :user_id AND st.status = \'active\'' + ); + $stmt->execute(['user_id' => $userId]); + return $stmt->fetchAll(); + } +} \ No newline at end of file diff --git a/backup/old_api_20260520/app/Repositories/TenantRegistrationCodeRepository.php b/backup/old_api_20260520/app/Repositories/TenantRegistrationCodeRepository.php new file mode 100644 index 0000000..fdb988b --- /dev/null +++ b/backup/old_api_20260520/app/Repositories/TenantRegistrationCodeRepository.php @@ -0,0 +1,57 @@ +db->prepare( + 'SELECT trc.*, t.name AS tenant_name + FROM tenant_registration_codes trc + JOIN tenants t ON t.id = trc.tenant_id + WHERE trc.code = :code AND trc.is_active = 1' + ); + $stmt->execute(['code' => $code]); + $result = $stmt->fetch(); + return $result ?: null; + } + + public function findByTenant(int $tenantId): ?array + { + $stmt = $this->db->prepare( + 'SELECT * FROM tenant_registration_codes WHERE tenant_id = :tenant_id AND is_active = 1' + ); + $stmt->execute(['tenant_id' => $tenantId]); + $result = $stmt->fetch(); + return $result ?: null; + } + + public function create(int $tenantId, string $code): array + { + $stmt = $this->db->prepare( + 'INSERT INTO tenant_registration_codes (tenant_id, code) VALUES (:tenant_id, :code) RETURNING *' + ); + $stmt->execute(['tenant_id' => $tenantId, 'code' => $code]); + return $stmt->fetch(); + } + + public function update(int $tenantId, string $code): bool + { + $stmt = $this->db->prepare( + 'UPDATE tenant_registration_codes SET code = :code WHERE tenant_id = :tenant_id RETURNING id' + ); + $stmt->execute(['tenant_id' => $tenantId, 'code' => $code]); + return (bool) $stmt->fetch(); + } + + public function incrementUsedCount(int $tenantId): void + { + $stmt = $this->db->prepare( + 'UPDATE tenant_registration_codes SET used_count = used_count + 1 WHERE tenant_id = :tenant_id' + ); + $stmt->execute(['tenant_id' => $tenantId]); + } +} \ No newline at end of file diff --git a/backup/old_api_20260520/app/Repositories/TenantRepository.php b/backup/old_api_20260520/app/Repositories/TenantRepository.php new file mode 100644 index 0000000..bc47c4f --- /dev/null +++ b/backup/old_api_20260520/app/Repositories/TenantRepository.php @@ -0,0 +1,86 @@ +db->query('SELECT * FROM tenants ORDER BY name')->fetchAll(); + } + + public function find(int $id): ?array + { + $statement = $this->db->prepare('SELECT * FROM tenants WHERE id = :id'); + $statement->execute(['id' => $id]); + + return $statement->fetch() ?: null; + } + + public function create(array $data): array + { + $timestamp = $this->now(); + $statement = $this->db->prepare( + 'INSERT INTO tenants (name, timezone, federal_state, location_label, latitude, longitude, is_active, created_at, updated_at) + VALUES (:name, :timezone, :federal_state, :location_label, :latitude, :longitude, :is_active, :created_at, :updated_at)' + ); + $statement->execute([ + 'name' => $data['name'], + 'timezone' => $data['timezone'] ?? 'Europe/Berlin', + 'federal_state' => $data['federal_state'] ?? 'BE', + 'location_label' => $data['location_label'] ?? '', + 'latitude' => $data['latitude'] !== '' ? $data['latitude'] : null, + 'longitude' => $data['longitude'] !== '' ? $data['longitude'] : null, + 'is_active' => !empty($data['is_active']) ? 1 : 0, + 'created_at' => $timestamp, + 'updated_at' => $timestamp, + ]); + + return $this->find((int) $this->lastInsertId()); + } + + public function update(int $id, array $data): ?array + { + $statement = $this->db->prepare( + 'UPDATE tenants + SET name = :name, timezone = :timezone, federal_state = :federal_state, location_label = :location_label, + latitude = :latitude, longitude = :longitude, is_active = :is_active, updated_at = :updated_at + WHERE id = :id' + ); + $statement->execute([ + 'id' => $id, + 'name' => $data['name'], + 'timezone' => $data['timezone'], + 'federal_state' => $data['federal_state'] ?? 'BE', + 'location_label' => $data['location_label'] ?? '', + 'latitude' => $data['latitude'] !== '' ? $data['latitude'] : null, + 'longitude' => $data['longitude'] !== '' ? $data['longitude'] : null, + 'is_active' => !empty($data['is_active']) ? 1 : 0, + 'updated_at' => $this->now(), + ]); + + return $this->find($id); + } + + public function updateForTenantAdmin(int $id, array $data): ?array + { + $current = $this->find($id); + if ($current === null) { + return null; + } + + return $this->update($id, [ + 'name' => $data['name'] ?? $current['name'], + 'timezone' => $data['timezone'] ?? $current['timezone'], + 'federal_state' => $data['federal_state'] ?? ($current['federal_state'] ?? 'BE'), + 'location_label' => $data['location_label'] ?? ($current['location_label'] ?? ''), + 'latitude' => array_key_exists('latitude', $data) ? $data['latitude'] : ($current['latitude'] ?? null), + 'longitude' => array_key_exists('longitude', $data) ? $data['longitude'] : ($current['longitude'] ?? null), + 'is_active' => $current['is_active'], + ]); + } +} diff --git a/backup/old_api_20260520/app/Repositories/UserRepository.php b/backup/old_api_20260520/app/Repositories/UserRepository.php new file mode 100644 index 0000000..bce3629 --- /dev/null +++ b/backup/old_api_20260520/app/Repositories/UserRepository.php @@ -0,0 +1,112 @@ +db->prepare( + 'SELECT id, tenant_id, role, email, first_name, last_name, is_active, created_at, updated_at + FROM users WHERE tenant_id = :tenant_id ORDER BY role, last_name, first_name' + ); + $statement->execute(['tenant_id' => $tenantId]); + + return $statement->fetchAll(); + } + + public function allUsers(): array + { + return $this->db->query( + 'SELECT u.id, u.tenant_id, u.role, u.email, u.first_name, u.last_name, u.is_active, + u.created_at, u.updated_at, t.name AS tenant_name + FROM users u + LEFT JOIN tenants t ON t.id = u.tenant_id + ORDER BY COALESCE(t.name, "System"), u.role, u.last_name, u.first_name' + )->fetchAll(); + } + + public function findByEmail(string $email): ?array + { + $statement = $this->db->prepare( + 'SELECT u.*, t.name AS tenant_name, t.timezone AS tenant_timezone, t.is_active AS tenant_is_active + FROM users u + LEFT JOIN tenants t ON t.id = u.tenant_id + WHERE u.email = :email' + ); + $statement->execute(['email' => $email]); + + return $statement->fetch() ?: null; + } + + public function findById(int $id): ?array + { + $statement = $this->db->prepare( + 'SELECT u.*, t.name AS tenant_name, t.timezone AS tenant_timezone, t.is_active AS tenant_is_active + FROM users u + LEFT JOIN tenants t ON t.id = u.tenant_id + WHERE u.id = :id' + ); + $statement->execute(['id' => $id]); + + return $statement->fetch() ?: null; + } + + public function create(array $data): array + { + $timestamp = $this->now(); + $statement = $this->db->prepare( + 'INSERT INTO users + (tenant_id, role, email, password_hash, first_name, last_name, is_active, created_at, updated_at) + VALUES (:tenant_id, :role, :email, :password_hash, :first_name, :last_name, :is_active, :created_at, :updated_at)' + ); + $statement->execute([ + 'tenant_id' => $data['tenant_id'], + 'role' => $data['role'], + 'email' => $data['email'], + 'password_hash' => password_hash($data['password'], PASSWORD_DEFAULT), + 'first_name' => $data['first_name'], + 'last_name' => $data['last_name'], + 'is_active' => !empty($data['is_active']) ? 1 : 0, + 'created_at' => $timestamp, + 'updated_at' => $timestamp, + ]); + + return $this->findById((int) $this->lastInsertId()); + } + + public function update(int $id, array $data): ?array + { + $current = $this->findById($id); + if ($current === null) { + return null; + } + + $passwordHash = $current['password_hash']; + if (!empty($data['password'])) { + $passwordHash = password_hash($data['password'], PASSWORD_DEFAULT); + } + + $statement = $this->db->prepare( + 'UPDATE users + SET tenant_id = :tenant_id, role = :role, email = :email, password_hash = :password_hash, + first_name = :first_name, last_name = :last_name, is_active = :is_active, updated_at = :updated_at + WHERE id = :id' + ); + $statement->execute([ + 'id' => $id, + 'tenant_id' => $data['tenant_id'], + 'role' => $data['role'], + 'email' => $data['email'], + 'password_hash' => $passwordHash, + 'first_name' => $data['first_name'], + 'last_name' => $data['last_name'], + 'is_active' => !empty($data['is_active']) ? 1 : 0, + 'updated_at' => $this->now(), + ]); + + return $this->findById($id); + } +} diff --git a/backup/old_api_20260520/app/Services/CalendarService.php b/backup/old_api_20260520/app/Services/CalendarService.php new file mode 100644 index 0000000..76fcbd0 --- /dev/null +++ b/backup/old_api_20260520/app/Services/CalendarService.php @@ -0,0 +1,93 @@ +findLessonType($tenantId, (int) $payload['lesson_type_id']); + $warnings = [ + 'conflicts' => [], + 'rest_period_warnings' => [], + ]; + + $conflicts = $appointmentRepository->overlappingForInstructor( + $tenantId, + (int) $payload['instructor_id'], + $payload['start_at'], + $payload['end_at'], + $appointmentId + ); + + foreach ($conflicts as $conflict) { + $warnings['conflicts'][] = [ + 'type' => 'instructor_overlap', + 'message' => 'Fahrlehrer ist in diesem Zeitraum bereits verplant.', + 'appointment_id' => (int) $conflict['id'], + ]; + } + + if (!empty($payload['student_id'])) { + $studentConflicts = $appointmentRepository->overlappingForStudent( + $tenantId, + (int) $payload['student_id'], + $payload['start_at'], + $payload['end_at'], + $appointmentId + ); + foreach ($studentConflicts as $conflict) { + $warnings['conflicts'][] = [ + 'type' => 'student_overlap', + 'message' => 'Fahrschueler ist in diesem Zeitraum bereits verplant.', + 'appointment_id' => (int) $conflict['id'], + ]; + } + } + + if ($lessonType !== null && (int) $lessonType['is_rest_relevant'] === 1) { + $newStart = new DateTimeImmutable($payload['start_at']); + $dayStart = $newStart->setTime(0, 0, 0); + $dayEnd = $dayStart->modify('+1 day'); + $hasLaterRelevantOnDay = $appointmentRepository->hasRestRelevantAfterStartOnDay( + $tenantId, + (int) $payload['instructor_id'], + $dayStart->format(DATE_ATOM), + $dayEnd->format(DATE_ATOM), + $payload['start_at'] + ); + + if ($hasLaterRelevantOnDay) { + return $warnings; + } + + $lastRelevant = $appointmentRepository->lastRestRelevantForInstructor( + $tenantId, + (int) $payload['instructor_id'], + $payload['start_at'], + $appointmentId + ); + if ($lastRelevant !== null) { + $restEnds = (new DateTimeImmutable($lastRelevant['end_at']))->add(new DateInterval('PT11H')); + if ($newStart < $restEnds) { + $warnings['rest_period_warnings'][] = [ + 'type' => 'rest_period', + 'message' => 'Achtung: Dieser Termin liegt innerhalb der 11-Stunden-Ruhezeit.', + 'rest_until' => $restEnds->format(DATE_ATOM), + 'blocking_appointment_id' => (int) $lastRelevant['id'], + ]; + } + } + } + + return $warnings; + } +} diff --git a/backup/old_api_20260520/app/Services/HolidayService.php b/backup/old_api_20260520/app/Services/HolidayService.php new file mode 100644 index 0000000..1b6d66b --- /dev/null +++ b/backup/old_api_20260520/app/Services/HolidayService.php @@ -0,0 +1,109 @@ +setTimezone($timezone)->setTime(0, 0); + $end = (new DateTimeImmutable($to, new DateTimeZone('UTC')))->setTimezone($timezone)->setTime(0, 0); + + $years = []; + $day = $start; + while ($day <= $end) { + $years[(int) $day->format('Y')] = true; + $day = $day->add(new DateInterval('P1D')); + } + + $holidaysByDate = []; + foreach (array_keys($years) as $year) { + foreach ($this->holidaysForYear((int) $year, $state, $timezone) as $date => $name) { + $holidaysByDate[$date] = $name; + } + } + + $result = []; + $day = $start; + while ($day < $end) { + $key = $day->format('Y-m-d'); + $result[$key] = [ + 'isSunday' => (int) $day->format('N') === 7, + 'holidayName' => $holidaysByDate[$key] ?? null, + ]; + $day = $day->add(new DateInterval('P1D')); + } + + return $result; + } + + private function holidaysForYear(int $year, string $state, DateTimeZone $timezone): array + { + $easter = (new DateTimeImmutable('@' . (string) easter_date($year)))->setTimezone($timezone)->setTime(0, 0); + $holidays = [ + $this->dateKey($year, 1, 1, $timezone) => 'Neujahr', + $easter->sub(new DateInterval('P2D'))->format('Y-m-d') => 'Karfreitag', + $easter->add(new DateInterval('P1D'))->format('Y-m-d') => 'Ostermontag', + $this->dateKey($year, 5, 1, $timezone) => 'Tag der Arbeit', + $easter->add(new DateInterval('P39D'))->format('Y-m-d') => 'Christi Himmelfahrt', + $easter->add(new DateInterval('P50D'))->format('Y-m-d') => 'Pfingstmontag', + $this->dateKey($year, 10, 3, $timezone) => 'Tag der Deutschen Einheit', + $this->dateKey($year, 12, 25, $timezone) => '1. Weihnachtstag', + $this->dateKey($year, 12, 26, $timezone) => '2. Weihnachtstag', + ]; + + if (in_array($state, self::STATES_WITH_EPIPHANY, true)) { + $holidays[$this->dateKey($year, 1, 6, $timezone)] = 'Heilige Drei Koenige'; + } + if (in_array($state, self::STATES_WITH_WOMENS_DAY, true)) { + $holidays[$this->dateKey($year, 3, 8, $timezone)] = 'Internationaler Frauentag'; + } + if (in_array($state, self::STATES_WITH_CORPUS_CHRISTI, true)) { + $holidays[$easter->add(new DateInterval('P60D'))->format('Y-m-d')] = 'Fronleichnam'; + } + if (in_array($state, self::STATES_WITH_ASSUMPTION, true)) { + $holidays[$this->dateKey($year, 8, 15, $timezone)] = 'Mariae Himmelfahrt'; + } + if (in_array($state, self::STATES_WITH_REFORMATION, true)) { + $holidays[$this->dateKey($year, 10, 31, $timezone)] = 'Reformationstag'; + } + if (in_array($state, self::STATES_WITH_ALL_SAINTS, true)) { + $holidays[$this->dateKey($year, 11, 1, $timezone)] = 'Allerheiligen'; + } + if (in_array($state, self::STATES_WITH_REPENTANCE, true)) { + $holidays[$this->repentanceDay($year, $timezone)->format('Y-m-d')] = 'Buss- und Bettag'; + } + + return $holidays; + } + + private function dateKey(int $year, int $month, int $day, DateTimeZone $timezone): string + { + return (new DateTimeImmutable(sprintf('%04d-%02d-%02d', $year, $month, $day), $timezone))->format('Y-m-d'); + } + + private function repentanceDay(int $year, DateTimeZone $timezone): DateTimeImmutable + { + $day = new DateTimeImmutable(sprintf('%04d-11-23', $year), $timezone); + while ((int) $day->format('N') !== 3) { + $day = $day->sub(new DateInterval('P1D')); + } + return $day; + } +} diff --git a/backup/old_api_20260520/app/Services/RequirementService.php b/backup/old_api_20260520/app/Services/RequirementService.php new file mode 100644 index 0000000..fa646fc --- /dev/null +++ b/backup/old_api_20260520/app/Services/RequirementService.php @@ -0,0 +1,56 @@ +requirements($studentId); + + $statement = $db->prepare( + 'SELECT au.requirement_key, + SUM(CASE WHEN a.status = "planned" THEN au.units_counted ELSE 0 END) AS planned_units, + SUM(CASE WHEN a.status = "completed" THEN au.units_counted ELSE 0 END) AS completed_units + FROM appointment_units au + JOIN appointments a ON a.id = au.appointment_id + WHERE a.student_id = :student_id + GROUP BY au.requirement_key' + ); + $statement->execute(['student_id' => $studentId]); + $totals = []; + foreach ($statement->fetchAll() as $row) { + $totals[$row['requirement_key']] = [ + 'planned' => (int) $row['planned_units'], + 'completed' => (int) $row['completed_units'], + ]; + } + + return array_map(static function (array $snapshot) use ($totals): array { + $row = $totals[$snapshot['requirement_key']] ?? ['planned' => 0, 'completed' => 0]; + $priorCompleted = (int) ($snapshot['prior_completed_units'] ?? 0); + $appointmentCompleted = (int) $row['completed']; + $completed = $priorCompleted + $appointmentCompleted; + $open = (int) $snapshot['required_units'] - $row['planned'] - $completed; + + return [ + 'requirement_key' => $snapshot['requirement_key'], + 'phase_label' => $snapshot['phase_label'] ?? '', + 'label' => $snapshot['label'], + 'required_units' => (int) $snapshot['required_units'], + 'planned_units' => $row['planned'], + 'completed_units' => $completed, + 'prior_completed_units' => $priorCompleted, + 'appointment_completed_units' => $appointmentCompleted, + 'open_units' => $open, + ]; + }, $snapshots); + } +} diff --git a/backup/old_api_20260520/app/Services/SunTimesService.php b/backup/old_api_20260520/app/Services/SunTimesService.php new file mode 100644 index 0000000..041fef0 --- /dev/null +++ b/backup/old_api_20260520/app/Services/SunTimesService.php @@ -0,0 +1,55 @@ +setTimezone($timezone)->setTime(0, 0); + $lastDay = $end->setTimezone($timezone)->setTime(0, 0); + + $result = []; + while ($day < $lastDay) { + $solar = date_sun_info($day->setTime(12, 0)->getTimestamp(), $latitude, $longitude); + $key = $day->format('Y-m-d'); + + $result[$key] = [ + 'sunrise' => $this->formatSolarTimestamp($solar['sunrise'] ?? false, $timezone), + 'sunset' => $this->formatSolarTimestamp($solar['sunset'] ?? false, $timezone), + ]; + + $day = $day->add(new DateInterval('P1D')); + } + + return $result; + } + + private function formatSolarTimestamp(int|float|bool $timestamp, DateTimeZone $timezone): ?string + { + if (!is_int($timestamp) && !is_float($timestamp)) { + return null; + } + + return (new DateTimeImmutable('@' . (string) (int) $timestamp)) + ->setTimezone($timezone) + ->format('H:i'); + } +} diff --git a/backup/old_api_20260520/app/Support/Auth.php b/backup/old_api_20260520/app/Support/Auth.php new file mode 100644 index 0000000..488c54f --- /dev/null +++ b/backup/old_api_20260520/app/Support/Auth.php @@ -0,0 +1,42 @@ +findById((int) $userId); + } + + public static function requireUser(): array + { + $user = self::user(); + if ($user === null) { + Response::json(['message' => 'Unauthenticated'], 401); + } + + return $user; + } + + public static function requireRole(array|string $roles): array + { + $user = self::requireUser(); + $roles = (array) $roles; + + if (!in_array($user['role'], $roles, true)) { + Response::json(['message' => 'Forbidden'], 403); + } + + return $user; + } +} diff --git a/backup/old_api_20260520/app/Support/Database.php b/backup/old_api_20260520/app/Support/Database.php new file mode 100644 index 0000000..a60ee17 --- /dev/null +++ b/backup/old_api_20260520/app/Support/Database.php @@ -0,0 +1,66 @@ +exec('PRAGMA foreign_keys = ON'); + } + + self::$connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + self::$connection->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); + + return self::$connection; + } + + public static function isPostgres(): bool + { + return (self::$config['db_driver'] ?? 'sqlite') === 'pgsql'; + } + + public static function lastInsertId(): string + { + if (self::isPostgres()) { + return self::$connection->query('SELECT lastval()')->fetchColumn(); + } + return self::$connection->lastInsertId(); + } +} diff --git a/backup/old_api_20260520/app/Support/Request.php b/backup/old_api_20260520/app/Support/Request.php new file mode 100644 index 0000000..ca66565 --- /dev/null +++ b/backup/old_api_20260520/app/Support/Request.php @@ -0,0 +1,40 @@ +map('GET', $path, $handler); + } + + public function post(string $path, callable $handler): void + { + $this->map('POST', $path, $handler); + } + + public function patch(string $path, callable $handler): void + { + $this->map('PATCH', $path, $handler); + } + + public function delete(string $path, callable $handler): void + { + $this->map('DELETE', $path, $handler); + } + + public function dispatch(): void + { + $method = $this->request->method(); + $path = $this->request->path(); + + foreach ($this->routes[$method] ?? [] as $route) { + $pattern = preg_replace('#\{([a-zA-Z_][a-zA-Z0-9_]*)\}#', '(?P<$1>[^/]+)', $route['path']); + $pattern = '#^' . $pattern . '$#'; + + if (!preg_match($pattern, $path, $matches)) { + continue; + } + + $params = array_filter($matches, static fn (string|int $key): bool => is_string($key), ARRAY_FILTER_USE_KEY); + $route['handler']($this->request, $params); + return; + } + + Response::json(['message' => 'Route not found', 'path' => $path], 404); + } + + private function map(string $method, string $path, callable $handler): void + { + $this->routes[$method][] = [ + 'path' => rtrim($path, '/') ?: '/', + 'handler' => $handler, + ]; + } +} diff --git a/backup/old_api_20260520/app/bootstrap.php b/backup/old_api_20260520/app/bootstrap.php new file mode 100644 index 0000000..591c80e --- /dev/null +++ b/backup/old_api_20260520/app/bootstrap.php @@ -0,0 +1,44 @@ + $config, + 'router' => new Router(new Request()), +]; diff --git a/backup/old_api_20260520/bin/migrate.php b/backup/old_api_20260520/bin/migrate.php new file mode 100644 index 0000000..33bc9d5 --- /dev/null +++ b/backup/old_api_20260520/bin/migrate.php @@ -0,0 +1,27 @@ +query('PRAGMA table_info(' . $matches[1] . ')')->fetchAll(); + $columnNames = array_map(static fn (array $column): string => strtolower((string) $column['name']), $columns); + if (in_array(strtolower($matches[2]), $columnNames, true)) { + continue; + } + } + + $db->exec($statement); + } + fwrite(STDOUT, "Applied: " . basename($migration) . PHP_EOL); +} diff --git a/backup/old_api_20260520/bin/seed.php b/backup/old_api_20260520/bin/seed.php new file mode 100644 index 0000000..38babcb --- /dev/null +++ b/backup/old_api_20260520/bin/seed.php @@ -0,0 +1,208 @@ +query('SELECT COUNT(*) AS count FROM users')->fetch(); +if ((int) $existing['count'] > 0) { + fwrite(STDOUT, "Seed skipped: data already exists." . PHP_EOL); + exit(0); +} + +$tenant = $tenantRepository->create([ + 'name' => 'Fahrschule Nordring', + 'timezone' => 'Europe/Berlin', + 'is_active' => true, +]); + +$db->prepare('INSERT INTO settings (tenant_id, key_name, value) VALUES (:tenant_id, :key_name, :value)') + ->execute([ + 'tenant_id' => $tenant['id'], + 'key_name' => 'app.theme', + 'value' => 'signal', + ]); + +$superadmin = $userRepository->create([ + 'tenant_id' => null, + 'role' => 'superadmin', + 'email' => 'superadmin@example.com', + 'password' => 'secret123', + 'first_name' => 'System', + 'last_name' => 'Admin', + 'is_active' => true, +]); + +$tenantAdmin = $userRepository->create([ + 'tenant_id' => $tenant['id'], + 'role' => 'tenant_admin', + 'email' => 'admin@nordring.test', + 'password' => 'secret123', + 'first_name' => 'Jana', + 'last_name' => 'Koester', + 'is_active' => true, +]); + +$dispatcher = $userRepository->create([ + 'tenant_id' => $tenant['id'], + 'role' => 'dispatcher', + 'email' => 'planung@nordring.test', + 'password' => 'secret123', + 'first_name' => 'Mara', + 'last_name' => 'Dispo', + 'is_active' => true, +]); + +$instructorUser = $userRepository->create([ + 'tenant_id' => $tenant['id'], + 'role' => 'instructor', + 'email' => 'fahrlehrer@nordring.test', + 'password' => 'secret123', + 'first_name' => 'Tobias', + 'last_name' => 'Fahr', + 'is_active' => true, +]); + +$instructors = [ + ['user_id' => $instructorUser['id'], 'first_name' => 'Tobias', 'last_name' => 'Fahr', 'color' => '#0E5D80'], + ['user_id' => null, 'first_name' => 'Leonie', 'last_name' => 'Kraft', 'color' => '#A74826'], +]; + +foreach ($instructors as $instructor) { + $instructorRepository->create($tenant['id'], $instructor + ['notes' => '', 'is_active' => true]); +} + +$lessonTypes = [ + ['name' => 'Einweisung', 'color' => '#0E5D80', 'default_duration' => 45, 'category' => 'student', 'requirement_key' => 'intro', 'is_billable' => 1, 'is_counted' => 1, 'is_rest_relevant' => 1, 'fixed_duration' => 0, 'sort_order' => 1], + ['name' => 'Uebungsstunde', 'color' => '#2C7A4B', 'default_duration' => 45, 'category' => 'student', 'requirement_key' => 'practice', 'is_billable' => 1, 'is_counted' => 1, 'is_rest_relevant' => 1, 'fixed_duration' => 0, 'sort_order' => 2], + ['name' => 'Bundes- / Landstrasse', 'color' => '#D99A1A', 'default_duration' => 45, 'category' => 'student', 'requirement_key' => 'rural', 'is_billable' => 1, 'is_counted' => 1, 'is_rest_relevant' => 1, 'fixed_duration' => 0, 'sort_order' => 3], + ['name' => 'Autobahn', 'color' => '#C86C1E', 'default_duration' => 45, 'category' => 'student', 'requirement_key' => 'highway', 'is_billable' => 1, 'is_counted' => 1, 'is_rest_relevant' => 1, 'fixed_duration' => 0, 'sort_order' => 4], + ['name' => 'Daemmerung / Dunkelheit', 'color' => '#4B356B', 'default_duration' => 45, 'category' => 'student', 'requirement_key' => 'night', 'is_billable' => 1, 'is_counted' => 1, 'is_rest_relevant' => 1, 'fixed_duration' => 0, 'sort_order' => 5], + ['name' => 'Pruefungsfahrt', 'color' => '#A83333', 'default_duration' => 70, 'category' => 'student', 'requirement_key' => null, 'is_billable' => 1, 'is_counted' => 0, 'is_rest_relevant' => 1, 'fixed_duration' => 1, 'sort_order' => 6], + ['name' => 'Theorieunterricht', 'color' => '#2B8793', 'default_duration' => 90, 'category' => 'other', 'requirement_key' => null, 'is_billable' => 0, 'is_counted' => 0, 'is_rest_relevant' => 0, 'fixed_duration' => 1, 'sort_order' => 7], + ['name' => 'Sonstige Arbeiten', 'color' => '#70747C', 'default_duration' => 60, 'category' => 'other', 'requirement_key' => null, 'is_billable' => 0, 'is_counted' => 0, 'is_rest_relevant' => 0, 'fixed_duration' => 1, 'sort_order' => 8], + ['name' => 'Privater Termin', 'color' => '#44444A', 'default_duration' => 60, 'category' => 'private', 'requirement_key' => null, 'is_billable' => 0, 'is_counted' => 0, 'is_rest_relevant' => 0, 'fixed_duration' => 1, 'sort_order' => 9], + ['name' => 'Sperrzeit / Blockierung', 'color' => '#111111', 'default_duration' => 60, 'category' => 'blocked', 'requirement_key' => null, 'is_billable' => 0, 'is_counted' => 0, 'is_rest_relevant' => 0, 'fixed_duration' => 1, 'sort_order' => 10], +]; + +foreach ($lessonTypes as $lessonType) { + $referenceRepository->createLessonType($tenant['id'], $lessonType); +} + +$templates = [ + [ + 'code' => 'C', + 'name' => 'Klasse C', + 'is_combination' => false, + 'requirements' => [ + ['requirement_key' => 'practice', 'label' => 'Uebungsstunden', 'required_units' => 8], + ['requirement_key' => 'rural', 'label' => 'Ueberland', 'required_units' => 5], + ['requirement_key' => 'highway', 'label' => 'Autobahn', 'required_units' => 4], + ['requirement_key' => 'night', 'label' => 'Nachtfahrt', 'required_units' => 3], + ], + ], + [ + 'code' => 'CE', + 'name' => 'Klasse CE', + 'is_combination' => false, + 'requirements' => [ + ['requirement_key' => 'practice', 'label' => 'Uebungsstunden', 'required_units' => 10], + ['requirement_key' => 'rural', 'label' => 'Ueberland', 'required_units' => 5], + ['requirement_key' => 'highway', 'label' => 'Autobahn', 'required_units' => 5], + ['requirement_key' => 'night', 'label' => 'Nachtfahrt', 'required_units' => 3], + ], + ], + [ + 'code' => 'C+CE', + 'name' => 'Klasse C + CE', + 'is_combination' => true, + 'requirements' => [ + ['requirement_key' => 'C::practice', 'phase_label' => 'C', 'label' => 'Uebungsstunden', 'required_units' => 8], + ['requirement_key' => 'C::rural', 'phase_label' => 'C', 'label' => 'Ueberland', 'required_units' => 5], + ['requirement_key' => 'C::highway', 'phase_label' => 'C', 'label' => 'Autobahn', 'required_units' => 4], + ['requirement_key' => 'C::night', 'phase_label' => 'C', 'label' => 'Nachtfahrt', 'required_units' => 3], + ['requirement_key' => 'CE::practice', 'phase_label' => 'CE', 'label' => 'Uebungsstunden', 'required_units' => 6], + ['requirement_key' => 'CE::rural', 'phase_label' => 'CE', 'label' => 'Ueberland', 'required_units' => 3], + ['requirement_key' => 'CE::highway', 'phase_label' => 'CE', 'label' => 'Autobahn', 'required_units' => 3], + ['requirement_key' => 'CE::night', 'phase_label' => 'CE', 'label' => 'Nachtfahrt', 'required_units' => 2], + ], + ], + [ + 'code' => 'C1', + 'name' => 'Klasse C1', + 'is_combination' => false, + 'requirements' => [ + ['requirement_key' => 'practice', 'label' => 'Uebungsstunden', 'required_units' => 6], + ['requirement_key' => 'rural', 'label' => 'Ueberland', 'required_units' => 4], + ['requirement_key' => 'highway', 'label' => 'Autobahn', 'required_units' => 3], + ['requirement_key' => 'night', 'label' => 'Nachtfahrt', 'required_units' => 2], + ], + ], + [ + 'code' => 'C1E', + 'name' => 'Klasse C1E', + 'is_combination' => false, + 'requirements' => [ + ['requirement_key' => 'practice', 'label' => 'Uebungsstunden', 'required_units' => 7], + ['requirement_key' => 'rural', 'label' => 'Ueberland', 'required_units' => 4], + ['requirement_key' => 'highway', 'label' => 'Autobahn', 'required_units' => 3], + ['requirement_key' => 'night', 'label' => 'Nachtfahrt', 'required_units' => 2], + ], + ], + [ + 'code' => 'C1+C1E', + 'name' => 'Klasse C1 + C1E', + 'is_combination' => true, + 'requirements' => [ + ['requirement_key' => 'C1::practice', 'phase_label' => 'C1', 'label' => 'Uebungsstunden', 'required_units' => 6], + ['requirement_key' => 'C1::rural', 'phase_label' => 'C1', 'label' => 'Ueberland', 'required_units' => 4], + ['requirement_key' => 'C1::highway', 'phase_label' => 'C1', 'label' => 'Autobahn', 'required_units' => 3], + ['requirement_key' => 'C1::night', 'phase_label' => 'C1', 'label' => 'Nachtfahrt', 'required_units' => 2], + ['requirement_key' => 'C1E::practice', 'phase_label' => 'C1E', 'label' => 'Uebungsstunden', 'required_units' => 4], + ['requirement_key' => 'C1E::rural', 'phase_label' => 'C1E', 'label' => 'Ueberland', 'required_units' => 2], + ['requirement_key' => 'C1E::highway', 'phase_label' => 'C1E', 'label' => 'Autobahn', 'required_units' => 2], + ['requirement_key' => 'C1E::night', 'phase_label' => 'C1E', 'label' => 'Nachtfahrt', 'required_units' => 1], + ], + ], +]; + +$templateIds = []; +foreach ($templates as $template) { + $record = $referenceRepository->createTemplate($tenant['id'], $template); + $templateIds[$record['code']] = $record['id']; +} + +$studentRepository->create($tenant['id'], [ + 'template_id' => $templateIds['C+CE'], + 'first_name' => 'Mila', + 'last_name' => 'Hansen', + 'phone' => '0170 1234567', + 'notes' => 'Startet naechste Woche mit Nachtfahrten.', + 'status' => 'active', +]); + +$studentRepository->create($tenant['id'], [ + 'template_id' => $templateIds['C1'], + 'first_name' => 'Jonas', + 'last_name' => 'Winter', + 'phone' => '0172 999000', + 'notes' => 'Braucht fruehe Termine.', + 'status' => 'active', +]); + +fwrite(STDOUT, "Seed complete." . PHP_EOL); diff --git a/backup/old_api_20260520/config/app.php b/backup/old_api_20260520/config/app.php new file mode 100644 index 0000000..40dcdfe --- /dev/null +++ b/backup/old_api_20260520/config/app.php @@ -0,0 +1,38 @@ + 'DriveTime Planner', + 'env' => 'production', + 'debug' => false, + + // Database: set db_driver to 'pgsql' or 'sqlite' + 'db_driver' => 'pgsql', + 'db_host' => '127.0.0.1', + 'db_port' => '5433', + 'db_database' => 'fahrschultermin', + 'db_username' => 'fahrschultermin_api', + 'db_password' => 'X9wL3pN7kRtHvMbZs4cGfYu2Dj6qAe8t', + + // Legacy SQLite path (used when db_driver = 'sqlite') + 'db_path' => __DIR__ . '/../storage/database/app.sqlite', + + // Session + 'session_path' => __DIR__ . '/../storage/sessions', + 'session_name' => 'drive_time_session', + 'session_domain' => '.fahrschultermin.de', + + // API + 'api_base_url' => 'https://api.fahrschultermin.de', + 'frontend_url' => 'https://fahrschultermin.de', + + // CORS + 'cors_allowed_origins' => [ + 'https://fahrschultermin.de', + 'https://www.fahrschultermin.de', + ], + + // Frontend asset path (for fahrschultermin.de frontend) + 'frontend_entry' => __DIR__ . '/../public/assets/index.js', +]; diff --git a/backup/old_api_20260520/database/import_pgsql.sql b/backup/old_api_20260520/database/import_pgsql.sql new file mode 100644 index 0000000..64e7a28 --- /dev/null +++ b/backup/old_api_20260520/database/import_pgsql.sql @@ -0,0 +1,518 @@ +INSERT INTO tenants (id, name, timezone, is_active, created_at, updated_at, location_label, latitude, longitude, federal_state) VALUES (1, 'Fahrschule Nordring', 'Europe/Berlin', 1, '2026-04-22T18:35:38+00:00', '2026-04-22T18:35:38+00:00', NULL, NULL, NULL, NULL); +INSERT INTO tenants (id, name, timezone, is_active, created_at, updated_at, location_label, latitude, longitude, federal_state) VALUES (2, 'madgerm', 'Europe/Berlin', 1, '2026-04-22 18:40:04', '2026-04-22T22:13:13+00:00', 'Dorsten', 51.656, 6.9643, 'NW'); +INSERT INTO users (id, tenant_id, role, email, password_hash, first_name, last_name, is_active, created_at, updated_at) VALUES (2, 1, 'tenant_admin', 'admin@nordring.test', '$2y$12$IIbaLmgMr9IH0pXxtuGUNeS73tyK7v/pfR0sMw5Edz4eDflX4W9RG', 'Jana', 'Koester', 1, '2026-04-22T18:35:38+00:00', '2026-04-22T18:35:38+00:00'); +INSERT INTO users (id, tenant_id, role, email, password_hash, first_name, last_name, is_active, created_at, updated_at) VALUES (3, 1, 'dispatcher', 'planung@nordring.test', '$2y$12$EdKSdiQwqoMz6btxR2nZBuo59F6EqdqdQboI06IeHUAkfpqjuIa8q', 'Mara', 'Dispo', 1, '2026-04-22T18:35:38+00:00', '2026-04-22T18:35:38+00:00'); +INSERT INTO users (id, tenant_id, role, email, password_hash, first_name, last_name, is_active, created_at, updated_at) VALUES (4, 1, 'instructor', 'fahrlehrer@nordring.test', '$2y$12$JGNkoZrs/Ga2qgz0r2BZy.LJLv4WZm/EqkfauWe1Avsn7n8lymXGy', 'Tobias', 'Fahr', 1, '2026-04-22T18:35:39+00:00', '2026-04-22T18:35:39+00:00'); +INSERT INTO users (id, tenant_id, role, email, password_hash, first_name, last_name, is_active, created_at, updated_at) VALUES (5, NULL, 'superadmin', 'armin.ploeger@live.de', '$2y$12$oZmKClGFa380ugvANeYX1OcVpjRWs9gNiJtWE6KhfzqTBMMgJlTFS', 'Armin', 'Ploeger', 1, '2026-04-22 18:40:04', '2026-04-22T18:40:49+00:00'); +INSERT INTO users (id, tenant_id, role, email, password_hash, first_name, last_name, is_active, created_at, updated_at) VALUES (6, 2, 'tenant_admin', 'madgerm@msn.com', '$2y$12$c6VKMJBHSdMHNZoxKB8UP.6i3zPVvAURudyOPMKmVn000TTADGZLC', 'madgerm', '', 1, '2026-04-22 18:40:04', '2026-04-22T19:20:46+00:00'); +INSERT INTO instructors (id, tenant_id, first_name, last_name, color, notes, is_active, created_at, updated_at, user_id, pre_start_buffer_minutes) VALUES (1, 1, 'Tobias', 'Fahr', '#0E5D80', '', 1, '2026-04-22T18:35:39+00:00', '2026-04-22T18:35:39+00:00', 4, 0); +INSERT INTO instructors (id, tenant_id, first_name, last_name, color, notes, is_active, created_at, updated_at, user_id, pre_start_buffer_minutes) VALUES (2, 1, 'Leonie', 'Kraft', '#A74826', '', 1, '2026-04-22T18:35:39+00:00', '2026-04-22T18:35:39+00:00', NULL, 0); +INSERT INTO instructors (id, tenant_id, first_name, last_name, color, notes, is_active, created_at, updated_at, user_id, pre_start_buffer_minutes) VALUES (3, 2, 'Armin', '', '#0e5d80', '', 1, '2026-04-22T19:37:18+00:00', '2026-04-24T09:00:39+00:00', NULL, 60); +INSERT INTO license_class_templates (id, tenant_id, code, name, is_combination, created_at, updated_at) VALUES (1, 1, 'C', 'Klasse C', 0, '2026-04-22T18:35:39+00:00', '2026-04-22T18:35:39+00:00'); +INSERT INTO license_class_templates (id, tenant_id, code, name, is_combination, created_at, updated_at) VALUES (2, 1, 'CE', 'Klasse CE', 0, '2026-04-22T18:35:39+00:00', '2026-04-22T18:35:39+00:00'); +INSERT INTO license_class_templates (id, tenant_id, code, name, is_combination, created_at, updated_at) VALUES (3, 1, 'C+CE', 'Klasse C + CE', 1, '2026-04-22T18:35:39+00:00', '2026-04-22T18:35:39+00:00'); +INSERT INTO license_class_templates (id, tenant_id, code, name, is_combination, created_at, updated_at) VALUES (4, 1, 'C1', 'Klasse C1', 0, '2026-04-22T18:35:39+00:00', '2026-04-22T18:35:39+00:00'); +INSERT INTO license_class_templates (id, tenant_id, code, name, is_combination, created_at, updated_at) VALUES (5, 1, 'C1E', 'Klasse C1E', 0, '2026-04-22T18:35:39+00:00', '2026-04-22T18:35:39+00:00'); +INSERT INTO license_class_templates (id, tenant_id, code, name, is_combination, created_at, updated_at) VALUES (6, 1, 'C1+C1E', 'Klasse C1 + C1E', 1, '2026-04-22T18:35:39+00:00', '2026-04-22T18:35:39+00:00'); +INSERT INTO license_class_templates (id, tenant_id, code, name, is_combination, created_at, updated_at) VALUES (7, 2, 'Feuerwehren C', 'Feuerwehren C', 0, '2026-04-22T18:43:17+00:00', '2026-04-24T07:39:17+00:00'); +INSERT INTO license_class_templates (id, tenant_id, code, name, is_combination, created_at, updated_at) VALUES (8, 2, 'Feuerwehr C+CE', 'Feuerwehr C+CE', 1, '2026-04-22T19:19:55+00:00', '2026-04-24T06:50:58+00:00'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (1, 1, 'practice', 'Uebungsstunden', 8, 0, ''); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (2, 1, 'rural', 'Ueberland', 5, 1, ''); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (3, 1, 'highway', 'Autobahn', 4, 2, ''); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (4, 1, 'night', 'Nachtfahrt', 3, 3, ''); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (5, 2, 'practice', 'Uebungsstunden', 10, 0, ''); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (6, 2, 'rural', 'Ueberland', 5, 1, ''); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (7, 2, 'highway', 'Autobahn', 5, 2, ''); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (8, 2, 'night', 'Nachtfahrt', 3, 3, ''); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (13, 4, 'practice', 'Uebungsstunden', 6, 0, ''); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (14, 4, 'rural', 'Ueberland', 4, 1, ''); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (15, 4, 'highway', 'Autobahn', 3, 2, ''); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (16, 4, 'night', 'Nachtfahrt', 2, 3, ''); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (17, 5, 'practice', 'Uebungsstunden', 7, 0, ''); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (18, 5, 'rural', 'Ueberland', 4, 1, ''); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (19, 5, 'highway', 'Autobahn', 3, 2, ''); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (20, 5, 'night', 'Nachtfahrt', 2, 3, ''); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (56, 8, 'C::lesson_type_14', 'Einweisung', 1, 0, 'C'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (57, 8, 'CE::lesson_type_14', 'Einweisung', 0, 1000, 'CE'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (58, 8, 'C::lesson_type_15', 'Übungsstunde', 7, 1, 'C'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (59, 8, 'CE::lesson_type_15', 'Übungsstunde', 8, 1001, 'CE'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (60, 8, 'C::lesson_type_12', 'Überlandfahrt', 5, 2, 'C'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (61, 8, 'CE::lesson_type_12', 'Überlandfahrt', 3, 1002, 'CE'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (62, 8, 'C::lesson_type_13', 'Autobahnfahrt', 2, 3, 'C'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (63, 8, 'CE::lesson_type_13', 'Autobahnfahrt', 1, 1003, 'CE'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (64, 8, 'C::lesson_type_11', 'Nachtfahrten', 3, 4, 'C'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (65, 8, 'CE::lesson_type_11', 'Nachtfahrten', 0, 1004, 'CE'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (66, 8, 'C::lesson_type_16', 'Praktische Prüfung', 1, 5, 'C'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (67, 8, 'CE::lesson_type_16', 'Praktische Prüfung', 1, 1005, 'CE'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (100, 7, 'lesson_type_14', 'Einweisung', 1, 0, ''); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (101, 7, 'lesson_type_15', 'Übungsstunde', 8, 1, ''); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (102, 7, 'lesson_type_12', 'Überlandfahrt', 5, 2, ''); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (103, 7, 'lesson_type_13', 'Autobahnfahrt', 2, 3, ''); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (104, 7, 'lesson_type_11', 'Nachtfahrten', 3, 4, ''); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (105, 7, 'lesson_type_16', 'Praktische Prüfung', 1, 5, ''); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (138, 3, 'C::practice', 'Uebungsstunden', 8, 0, 'C'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (139, 3, 'C::rural', 'Ueberland', 5, 1, 'C'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (140, 3, 'C::highway', 'Autobahn', 4, 2, 'C'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (141, 3, 'C::night', 'Nachtfahrt', 3, 3, 'C'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (142, 3, 'CE::practice', 'Uebungsstunden', 6, 100, 'CE'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (143, 3, 'CE::rural', 'Ueberland', 3, 101, 'CE'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (144, 3, 'CE::highway', 'Autobahn', 3, 102, 'CE'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (145, 3, 'CE::night', 'Nachtfahrt', 2, 103, 'CE'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (146, 6, 'C1::practice', 'Uebungsstunden', 6, 0, 'C1'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (147, 6, 'C1::rural', 'Ueberland', 4, 1, 'C1'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (148, 6, 'C1::highway', 'Autobahn', 3, 2, 'C1'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (149, 6, 'C1::night', 'Nachtfahrt', 2, 3, 'C1'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (150, 6, 'C1E::practice', 'Uebungsstunden', 4, 100, 'C1E'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (151, 6, 'C1E::rural', 'Ueberland', 2, 101, 'C1E'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (152, 6, 'C1E::highway', 'Autobahn', 2, 102, 'C1E'); +INSERT INTO license_template_requirements (id, template_id, requirement_key, label, required_units, sort_order, phase_label) VALUES (153, 6, 'C1E::night', 'Nachtfahrt', 1, 103, 'C1E'); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (1, 1, 'Einweisung', '#0E5D80', 45, 'student', 'intro', 1, 1, 1, 0, 1, '2026-04-22T18:35:39+00:00', '2026-04-22T18:35:39+00:00', NULL); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (2, 1, 'Uebungsstunde', '#2C7A4B', 45, 'student', 'practice', 1, 1, 1, 0, 2, '2026-04-22T18:35:39+00:00', '2026-04-22T18:35:39+00:00', NULL); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (3, 1, 'Bundes- / Landstrasse', '#D99A1A', 45, 'student', 'rural', 1, 1, 1, 0, 3, '2026-04-22T18:35:39+00:00', '2026-04-22T18:35:39+00:00', NULL); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (4, 1, 'Autobahn', '#C86C1E', 45, 'student', 'highway', 1, 1, 1, 0, 4, '2026-04-22T18:35:39+00:00', '2026-04-22T18:35:39+00:00', NULL); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (5, 1, 'Daemmerung / Dunkelheit', '#4B356B', 45, 'student', 'night', 1, 1, 1, 0, 5, '2026-04-22T18:35:39+00:00', '2026-04-22T18:35:39+00:00', NULL); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (6, 1, 'Pruefungsfahrt', '#A83333', 70, 'student', NULL, 1, 0, 1, 1, 6, '2026-04-22T18:35:39+00:00', '2026-04-22T18:35:39+00:00', NULL); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (7, 1, 'Theorieunterricht', '#2B8793', 90, 'work', NULL, 0, 0, 0, 1, 7, '2026-04-22T18:35:39+00:00', '2026-04-22T18:35:39+00:00', NULL); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (8, 1, 'Sonstige Arbeiten', '#70747C', 60, 'work', NULL, 0, 0, 0, 1, 8, '2026-04-22T18:35:39+00:00', '2026-04-22T18:35:39+00:00', NULL); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (9, 1, 'Privater Termin', '#44444A', 60, 'private', NULL, 0, 0, 0, 1, 9, '2026-04-22T18:35:39+00:00', '2026-04-22T18:35:39+00:00', NULL); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (10, 1, 'Sperrzeit / Blockierung', '#111111', 60, 'blocked', NULL, 0, 0, 0, 1, 10, '2026-04-22T18:35:39+00:00', '2026-04-22T18:35:39+00:00', NULL); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (11, 2, 'Nachtfahrten', '#000000', 45, 'student', NULL, 1, 1, 1, 0, 5, '2026-04-22T18:49:50+00:00', '2026-04-22T19:23:23+00:00', NULL); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (12, 2, 'Überlandfahrt', '#02882a', 45, 'student', NULL, 1, 1, 1, 0, 3, '2026-04-22T19:01:19+00:00', '2026-04-22T19:23:23+00:00', NULL); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (13, 2, 'Autobahnfahrt', '#c4660e', 45, 'student', NULL, 1, 1, 1, 0, 4, '2026-04-22T19:05:14+00:00', '2026-04-22T19:23:23+00:00', NULL); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (14, 2, 'Einweisung', '#e65c4c', 45, 'student', NULL, 1, 1, 1, 0, 1, '2026-04-22T19:06:35+00:00', '2026-04-22T19:23:23+00:00', NULL); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (15, 2, 'Übungsstunde', '#e65c4c', 45, 'student', NULL, 1, 1, 1, 0, 2, '2026-04-22T19:06:36+00:00', '2026-04-22T19:23:23+00:00', NULL); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (16, 2, 'Praktische Prüfung', '#11ff00', 85, 'student', NULL, 1, 1, 1, 0, 6, '2026-04-23T05:14:13+00:00', '2026-05-05T19:03:08+00:00', NULL); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (17, 2, 'Privat', '#6B7280', 60, 'private', NULL, 0, 0, 0, 0, 900, '2026-04-23T07:26:51+00:00', '2026-04-23T07:26:51+00:00', 'private_block'); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (18, 2, 'Fahrzeugpflege', '#0E5D80', 60, 'work', NULL, 0, 0, 1, 0, 910, '2026-04-23T07:26:51+00:00', '2026-04-23T07:26:51+00:00', 'work_vehicle_care'); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (19, 2, 'Buero', '#8B5A2B', 60, 'work', NULL, 0, 0, 1, 0, 920, '2026-04-23T07:26:51+00:00', '2026-04-23T07:26:51+00:00', 'work_office'); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (20, 2, 'Wechselfahrt', '#3B7A57', 45, 'work', NULL, 0, 0, 1, 0, 930, '2026-04-23T07:26:51+00:00', '2026-04-23T07:26:51+00:00', 'work_transfer_drive'); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (21, 2, 'Sonstige Taetigkeit', '#7C3AED', 60, 'work', NULL, 0, 0, 1, 0, 940, '2026-04-23T07:26:51+00:00', '2026-04-23T07:26:51+00:00', 'work_other'); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (22, 2, 'Theorie A', '#C2410C', 90, 'theory', NULL, 0, 0, 1, 0, 950, '2026-04-23T07:26:51+00:00', '2026-04-23T07:26:51+00:00', 'theory_a'); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (23, 2, 'Theorie B', '#A74826', 90, 'theory', NULL, 0, 0, 1, 0, 960, '2026-04-23T07:26:51+00:00', '2026-04-23T07:26:51+00:00', 'theory_b'); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (24, 2, 'Theorie C', '#B45309', 90, 'theory', NULL, 0, 0, 1, 0, 970, '2026-04-23T07:26:51+00:00', '2026-04-23T07:26:51+00:00', 'theory_c'); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (25, 2, 'Theorie D', '#BE123C', 90, 'theory', NULL, 0, 0, 1, 0, 980, '2026-04-23T07:26:51+00:00', '2026-04-23T07:26:51+00:00', 'theory_d'); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (26, 2, 'Theorie T', '#047857', 90, 'theory', NULL, 0, 0, 1, 0, 990, '2026-04-23T07:26:51+00:00', '2026-04-23T07:26:51+00:00', 'theory_t'); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (27, 2, 'Theorie BKF', '#1D4ED8', 90, 'theory', NULL, 0, 0, 1, 0, 1000, '2026-04-23T07:26:51+00:00', '2026-04-23T07:26:51+00:00', 'theory_bkf'); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (2327, 1, 'Privat', '#6B7280', 60, 'private', NULL, 0, 0, 0, 0, 900, '2026-04-24T06:39:44+00:00', '2026-04-24T06:39:44+00:00', 'private_block'); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (2328, 1, 'Fahrzeugpflege', '#0E5D80', 60, 'work', NULL, 0, 0, 1, 0, 910, '2026-04-24T06:39:44+00:00', '2026-04-24T06:39:44+00:00', 'work_vehicle_care'); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (2329, 1, 'Buero', '#8B5A2B', 60, 'work', NULL, 0, 0, 1, 0, 920, '2026-04-24T06:39:44+00:00', '2026-04-24T06:39:44+00:00', 'work_office'); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (2330, 1, 'Wechselfahrt', '#3B7A57', 45, 'work', NULL, 0, 0, 1, 0, 930, '2026-04-24T06:39:44+00:00', '2026-04-24T06:39:44+00:00', 'work_transfer_drive'); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (2331, 1, 'Sonstige Taetigkeit', '#7C3AED', 60, 'work', NULL, 0, 0, 1, 0, 940, '2026-04-24T06:39:44+00:00', '2026-04-24T06:39:44+00:00', 'work_other'); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (2332, 1, 'Theorie A', '#C2410C', 90, 'theory', NULL, 0, 0, 1, 0, 950, '2026-04-24T06:39:44+00:00', '2026-04-24T06:39:44+00:00', 'theory_a'); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (2333, 1, 'Theorie B', '#A74826', 90, 'theory', NULL, 0, 0, 1, 0, 960, '2026-04-24T06:39:44+00:00', '2026-04-24T06:39:44+00:00', 'theory_b'); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (2334, 1, 'Theorie C', '#B45309', 90, 'theory', NULL, 0, 0, 1, 0, 970, '2026-04-24T06:39:44+00:00', '2026-04-24T06:39:44+00:00', 'theory_c'); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (2335, 1, 'Theorie D', '#BE123C', 90, 'theory', NULL, 0, 0, 1, 0, 980, '2026-04-24T06:39:44+00:00', '2026-04-24T06:39:44+00:00', 'theory_d'); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (2336, 1, 'Theorie T', '#047857', 90, 'theory', NULL, 0, 0, 1, 0, 990, '2026-04-24T06:39:44+00:00', '2026-04-24T06:39:44+00:00', 'theory_t'); +INSERT INTO lesson_types (id, tenant_id, name, color, default_duration, category, requirement_key, is_billable, is_counted, is_rest_relevant, fixed_duration, sort_order, created_at, updated_at, system_key) VALUES (2337, 1, 'Theorie BKF', '#1D4ED8', 90, 'theory', NULL, 0, 0, 1, 0, 1000, '2026-04-24T06:39:44+00:00', '2026-04-24T06:39:44+00:00', 'theory_bkf'); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (1, 1); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (1, 3); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (1, 4); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (1, 6); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (1, 5); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (1, 2); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (2, 1); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (2, 3); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (2, 4); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (2, 6); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (2, 5); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (2, 2); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (3, 1); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (3, 3); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (3, 4); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (3, 6); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (3, 5); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (3, 2); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (4, 1); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (4, 3); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (4, 4); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (4, 6); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (4, 5); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (4, 2); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (5, 1); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (5, 3); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (5, 4); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (5, 6); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (5, 5); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (5, 2); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (6, 1); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (6, 3); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (6, 4); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (6, 6); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (6, 5); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (6, 2); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (11, 7); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (12, 7); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (13, 7); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (14, 7); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (15, 7); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (11, 8); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (12, 8); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (13, 8); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (14, 8); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (15, 8); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (16, 7); +INSERT INTO lesson_type_template_visibility (lesson_type_id, template_id) VALUES (16, 8); +INSERT INTO students (id, tenant_id, template_id, first_name, last_name, phone, notes, status, created_at, updated_at, email, needs_glasses) VALUES (1, 1, 3, 'Mila', 'Hansen', '0170 1234567', 'Startet naechste Woche mit Nachtfahrten.', 'active', '2026-04-22T18:35:39+00:00', '2026-04-22T18:35:39+00:00', '', 0); +INSERT INTO students (id, tenant_id, template_id, first_name, last_name, phone, notes, status, created_at, updated_at, email, needs_glasses) VALUES (2, 1, 4, 'Jonas', 'Winter', '0172 999000', 'Braucht fruehe Termine.', 'active', '2026-04-22T18:35:39+00:00', '2026-04-22T18:35:39+00:00', '', 0); +INSERT INTO students (id, tenant_id, template_id, first_name, last_name, phone, notes, status, created_at, updated_at, email, needs_glasses) VALUES (3, 2, 7, '-', 'Nöllecke', '', '', 'active', '2026-04-22T18:48:52+00:00', '2026-04-23T07:37:52+00:00', '', 0); +INSERT INTO students (id, tenant_id, template_id, first_name, last_name, phone, notes, status, created_at, updated_at, email, needs_glasses) VALUES (4, 2, 7, '-', 'Nappenfeld', '', '', 'active', '2026-04-22T19:31:04+00:00', '2026-04-23T07:37:12+00:00', '', 0); +INSERT INTO students (id, tenant_id, template_id, first_name, last_name, phone, notes, status, created_at, updated_at, email, needs_glasses) VALUES (6, 2, 7, '-', 'Storck', '', '', 'active', '2026-04-22T19:31:45+00:00', '2026-04-23T07:38:05+00:00', '', 0); +INSERT INTO students (id, tenant_id, template_id, first_name, last_name, phone, notes, status, created_at, updated_at, email, needs_glasses) VALUES (7, 2, 7, '-', 'Ronneburger', '', '', 'active', '2026-04-22T19:31:58+00:00', '2026-04-23T07:38:02+00:00', '', 0); +INSERT INTO students (id, tenant_id, template_id, first_name, last_name, phone, notes, status, created_at, updated_at, email, needs_glasses) VALUES (9, 2, 8, '-', 'Mrowitzki', '', '', 'active', '2026-04-23T05:20:18+00:00', '2026-04-23T07:37:46+00:00', '', 0); +INSERT INTO students (id, tenant_id, template_id, first_name, last_name, phone, notes, status, created_at, updated_at, email, needs_glasses) VALUES (10, 2, 8, '-', 'Klaas', '', '', 'active', '2026-04-23T05:20:58+00:00', '2026-04-23T08:37:10+00:00', '', 0); +INSERT INTO students (id, tenant_id, template_id, first_name, last_name, phone, notes, status, created_at, updated_at, email, needs_glasses) VALUES (11, 2, 8, '-', 'Wioliczkp', '', '', 'active', '2026-04-23T05:21:33+00:00', '2026-04-23T08:36:13+00:00', '', 0); +INSERT INTO students (id, tenant_id, template_id, first_name, last_name, phone, notes, status, created_at, updated_at, email, needs_glasses) VALUES (13, 2, 8, '-', 'Pölling', '', '', 'active', '2026-04-23T05:22:18+00:00', '2026-04-24T07:22:23+00:00', '', 0); +INSERT INTO students (id, tenant_id, template_id, first_name, last_name, phone, notes, status, created_at, updated_at, email, needs_glasses) VALUES (14, 2, 8, '-', 'Hardameck', '', '', 'active', '2026-04-23T05:22:26+00:00', '2026-04-23T07:37:28+00:00', '', 0); +INSERT INTO students (id, tenant_id, template_id, first_name, last_name, phone, notes, status, created_at, updated_at, email, needs_glasses) VALUES (15, 2, 7, '-', 'Lawniczek', '', '', 'active', '2026-04-23T05:22:52+00:00', '2026-04-23T08:37:22+00:00', '', 0); +INSERT INTO students (id, tenant_id, template_id, first_name, last_name, phone, notes, status, created_at, updated_at, email, needs_glasses) VALUES (17, 2, 8, '-', 'Lackmann', '', '', 'active', '2026-04-23T07:34:23+00:00', '2026-04-23T07:34:23+00:00', '', 0); +INSERT INTO students (id, tenant_id, template_id, first_name, last_name, phone, notes, status, created_at, updated_at, email, needs_glasses) VALUES (19, 2, 8, 'egal', 'bowens', '', '', 'active', '2026-05-07T12:00:21+00:00', '2026-05-07T12:00:21+00:00', '', 0); +INSERT INTO students (id, tenant_id, template_id, first_name, last_name, phone, notes, status, created_at, updated_at, email, needs_glasses) VALUES (20, 2, 8, 'egal', 'gröning', '', '', 'active', '2026-05-07T12:00:38+00:00', '2026-05-07T12:00:38+00:00', '', 0); +INSERT INTO students (id, tenant_id, template_id, first_name, last_name, phone, notes, status, created_at, updated_at, email, needs_glasses) VALUES (21, 2, 7, 'egal', 'kobitzsch', '', '', 'active', '2026-05-07T12:00:52+00:00', '2026-05-07T12:01:20+00:00', '', 0); +INSERT INTO students (id, tenant_id, template_id, first_name, last_name, phone, notes, status, created_at, updated_at, email, needs_glasses) VALUES (22, 2, 8, 'egal', 'gregor schlosser', '', '', 'active', '2026-05-07T12:01:02+00:00', '2026-05-07T12:01:02+00:00', '', 0); +INSERT INTO students (id, tenant_id, template_id, first_name, last_name, phone, notes, status, created_at, updated_at, email, needs_glasses) VALUES (23, 2, 8, '-', 'Kuckel', '', '', 'active', '2026-05-11T17:44:52+00:00', '2026-05-11T17:44:52+00:00', '', 0); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (13, 4, 'lesson_type_14', 'Einweisung', 1, 0, 3, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (14, 4, 'lesson_type_15', 'Übungsstunde', 8, 1, 6, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (15, 4, 'lesson_type_12', 'Überlandfahrt', 5, 2, 0, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (16, 4, 'lesson_type_13', 'Autobahnfahrt', 2, 3, 0, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (17, 4, 'lesson_type_11', 'Nachtfahrten', 3, 4, 0, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (23, 6, 'lesson_type_14', 'Einweisung', 1, 0, 0, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (24, 6, 'lesson_type_15', 'Übungsstunde', 8, 1, 4, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (25, 6, 'lesson_type_12', 'Überlandfahrt', 5, 2, 3, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (26, 6, 'lesson_type_13', 'Autobahnfahrt', 2, 3, 2, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (27, 6, 'lesson_type_11', 'Nachtfahrten', 3, 4, 3, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (28, 7, 'lesson_type_14', 'Einweisung', 1, 0, 1, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (29, 7, 'lesson_type_15', 'Übungsstunde', 8, 1, 3, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (30, 7, 'lesson_type_12', 'Überlandfahrt', 5, 2, 3, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (31, 7, 'lesson_type_13', 'Autobahnfahrt', 2, 3, 0, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (32, 7, 'lesson_type_11', 'Nachtfahrten', 3, 4, 3, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (68, 15, 'lesson_type_14', 'Einweisung', 1, 0, 1, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (69, 15, 'lesson_type_11', 'Nachtfahrten', 3, 4, 0, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (70, 15, 'lesson_type_13', 'Autobahnfahrt', 2, 3, 2, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (71, 15, 'lesson_type_12', 'Überlandfahrt', 5, 2, 3, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (72, 15, 'lesson_type_15', 'Übungsstunde', 8, 1, 3, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (86, 15, 'lesson_type_16', 'Praktische Prüfung', 1, 5, 0, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (88, 4, 'lesson_type_16', 'Praktische Prüfung', 1, 5, 0, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (89, 3, 'lesson_type_14', 'Einweisung', 1, 0, 1, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (90, 3, 'lesson_type_15', 'Übungsstunde', 8, 1, 3, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (91, 3, 'lesson_type_12', 'Überlandfahrt', 5, 2, 0, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (92, 3, 'lesson_type_13', 'Autobahnfahrt', 2, 3, 0, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (93, 3, 'lesson_type_11', 'Nachtfahrten', 3, 4, 3, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (94, 3, 'lesson_type_16', 'Praktische Prüfung', 1, 5, 0, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (96, 7, 'lesson_type_16', 'Praktische Prüfung', 1, 5, 0, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (97, 6, 'lesson_type_16', 'Praktische Prüfung', 1, 5, 0, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (101, 14, 'C::lesson_type_14', 'Einweisung', 1, 0, 1, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (102, 14, 'CE::lesson_type_14', 'Einweisung', 0, 1000, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (103, 14, 'C::lesson_type_15', 'Übungsstunde', 7, 1, 2, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (104, 14, 'CE::lesson_type_15', 'Übungsstunde', 8, 1001, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (105, 14, 'C::lesson_type_12', 'Überlandfahrt', 5, 2, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (106, 14, 'CE::lesson_type_12', 'Überlandfahrt', 3, 1002, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (107, 14, 'C::lesson_type_13', 'Autobahnfahrt', 2, 3, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (108, 14, 'CE::lesson_type_13', 'Autobahnfahrt', 1, 1003, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (109, 14, 'C::lesson_type_11', 'Nachtfahrten', 3, 4, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (110, 14, 'CE::lesson_type_11', 'Nachtfahrten', 0, 1004, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (111, 14, 'C::lesson_type_16', 'Praktische Prüfung', 1, 5, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (112, 14, 'CE::lesson_type_16', 'Praktische Prüfung', 1, 1005, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (113, 10, 'C::lesson_type_14', 'Einweisung', 1, 0, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (114, 10, 'CE::lesson_type_14', 'Einweisung', 0, 1000, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (115, 10, 'C::lesson_type_15', 'Übungsstunde', 7, 1, 3, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (116, 10, 'CE::lesson_type_15', 'Übungsstunde', 8, 1001, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (117, 10, 'C::lesson_type_12', 'Überlandfahrt', 5, 2, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (118, 10, 'CE::lesson_type_12', 'Überlandfahrt', 3, 1002, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (119, 10, 'C::lesson_type_13', 'Autobahnfahrt', 2, 3, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (120, 10, 'CE::lesson_type_13', 'Autobahnfahrt', 1, 1003, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (121, 10, 'C::lesson_type_11', 'Nachtfahrten', 3, 4, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (122, 10, 'CE::lesson_type_11', 'Nachtfahrten', 0, 1004, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (123, 10, 'C::lesson_type_16', 'Praktische Prüfung', 1, 5, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (124, 10, 'CE::lesson_type_16', 'Praktische Prüfung', 1, 1005, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (125, 17, 'C::lesson_type_14', 'Einweisung', 1, 0, 1, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (126, 17, 'CE::lesson_type_14', 'Einweisung', 0, 1000, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (127, 17, 'C::lesson_type_15', 'Übungsstunde', 7, 1, 6, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (128, 17, 'CE::lesson_type_15', 'Übungsstunde', 8, 1001, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (129, 17, 'C::lesson_type_12', 'Überlandfahrt', 5, 2, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (130, 17, 'CE::lesson_type_12', 'Überlandfahrt', 3, 1002, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (131, 17, 'C::lesson_type_13', 'Autobahnfahrt', 2, 3, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (132, 17, 'CE::lesson_type_13', 'Autobahnfahrt', 1, 1003, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (133, 17, 'C::lesson_type_11', 'Nachtfahrten', 3, 4, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (134, 17, 'CE::lesson_type_11', 'Nachtfahrten', 0, 1004, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (135, 17, 'C::lesson_type_16', 'Praktische Prüfung', 1, 5, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (136, 17, 'CE::lesson_type_16', 'Praktische Prüfung', 1, 1005, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (137, 9, 'C::lesson_type_14', 'Einweisung', 1, 0, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (138, 9, 'CE::lesson_type_14', 'Einweisung', 0, 1000, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (139, 9, 'C::lesson_type_15', 'Übungsstunde', 7, 1, 5, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (140, 9, 'CE::lesson_type_15', 'Übungsstunde', 8, 1001, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (141, 9, 'C::lesson_type_12', 'Überlandfahrt', 5, 2, 1, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (142, 9, 'CE::lesson_type_12', 'Überlandfahrt', 3, 1002, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (143, 9, 'C::lesson_type_13', 'Autobahnfahrt', 2, 3, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (144, 9, 'CE::lesson_type_13', 'Autobahnfahrt', 1, 1003, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (145, 9, 'C::lesson_type_11', 'Nachtfahrten', 3, 4, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (146, 9, 'CE::lesson_type_11', 'Nachtfahrten', 0, 1004, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (147, 9, 'C::lesson_type_16', 'Praktische Prüfung', 1, 5, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (148, 9, 'CE::lesson_type_16', 'Praktische Prüfung', 1, 1005, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (149, 13, 'C::lesson_type_14', 'Einweisung', 1, 0, 1, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (150, 13, 'CE::lesson_type_14', 'Einweisung', 0, 1000, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (151, 13, 'C::lesson_type_15', 'Übungsstunde', 7, 1, 4, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (152, 13, 'CE::lesson_type_15', 'Übungsstunde', 8, 1001, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (153, 13, 'C::lesson_type_12', 'Überlandfahrt', 5, 2, 2, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (154, 13, 'CE::lesson_type_12', 'Überlandfahrt', 3, 1002, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (155, 13, 'C::lesson_type_13', 'Autobahnfahrt', 2, 3, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (156, 13, 'CE::lesson_type_13', 'Autobahnfahrt', 1, 1003, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (157, 13, 'C::lesson_type_11', 'Nachtfahrten', 3, 4, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (158, 13, 'CE::lesson_type_11', 'Nachtfahrten', 0, 1004, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (159, 13, 'C::lesson_type_16', 'Praktische Prüfung', 1, 5, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (160, 13, 'CE::lesson_type_16', 'Praktische Prüfung', 1, 1005, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (161, 11, 'C::lesson_type_14', 'Einweisung', 1, 0, 1, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (162, 11, 'CE::lesson_type_14', 'Einweisung', 0, 1000, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (163, 11, 'C::lesson_type_15', 'Übungsstunde', 7, 1, 6, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (164, 11, 'CE::lesson_type_15', 'Übungsstunde', 8, 1001, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (165, 11, 'C::lesson_type_12', 'Überlandfahrt', 5, 2, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (166, 11, 'CE::lesson_type_12', 'Überlandfahrt', 3, 1002, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (167, 11, 'C::lesson_type_13', 'Autobahnfahrt', 2, 3, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (168, 11, 'CE::lesson_type_13', 'Autobahnfahrt', 1, 1003, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (169, 11, 'C::lesson_type_11', 'Nachtfahrten', 3, 4, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (170, 11, 'CE::lesson_type_11', 'Nachtfahrten', 0, 1004, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (171, 11, 'C::lesson_type_16', 'Praktische Prüfung', 1, 5, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (172, 11, 'CE::lesson_type_16', 'Praktische Prüfung', 1, 1005, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (205, 1, 'C::highway', 'Autobahn', 4, 2, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (206, 1, 'C::night', 'Nachtfahrt', 3, 3, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (207, 1, 'C::practice', 'Uebungsstunden', 8, 0, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (208, 1, 'C::rural', 'Ueberland', 5, 1, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (209, 1, 'CE::highway', 'Autobahn', 3, 102, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (210, 1, 'CE::night', 'Nachtfahrt', 2, 103, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (211, 1, 'CE::practice', 'Uebungsstunden', 6, 100, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (212, 1, 'CE::rural', 'Ueberland', 3, 101, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (225, 19, 'C::lesson_type_14', 'Einweisung', 1, 0, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (226, 19, 'CE::lesson_type_14', 'Einweisung', 0, 1000, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (227, 19, 'C::lesson_type_15', 'Übungsstunde', 7, 1, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (228, 19, 'CE::lesson_type_15', 'Übungsstunde', 8, 1001, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (229, 19, 'C::lesson_type_12', 'Überlandfahrt', 5, 2, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (230, 19, 'CE::lesson_type_12', 'Überlandfahrt', 3, 1002, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (231, 19, 'C::lesson_type_13', 'Autobahnfahrt', 2, 3, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (232, 19, 'CE::lesson_type_13', 'Autobahnfahrt', 1, 1003, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (233, 19, 'C::lesson_type_11', 'Nachtfahrten', 3, 4, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (234, 19, 'CE::lesson_type_11', 'Nachtfahrten', 0, 1004, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (235, 19, 'C::lesson_type_16', 'Praktische Prüfung', 1, 5, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (236, 19, 'CE::lesson_type_16', 'Praktische Prüfung', 1, 1005, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (237, 20, 'C::lesson_type_14', 'Einweisung', 1, 0, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (238, 20, 'CE::lesson_type_14', 'Einweisung', 0, 1000, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (239, 20, 'C::lesson_type_15', 'Übungsstunde', 7, 1, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (240, 20, 'CE::lesson_type_15', 'Übungsstunde', 8, 1001, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (241, 20, 'C::lesson_type_12', 'Überlandfahrt', 5, 2, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (242, 20, 'CE::lesson_type_12', 'Überlandfahrt', 3, 1002, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (243, 20, 'C::lesson_type_13', 'Autobahnfahrt', 2, 3, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (244, 20, 'CE::lesson_type_13', 'Autobahnfahrt', 1, 1003, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (245, 20, 'C::lesson_type_11', 'Nachtfahrten', 3, 4, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (246, 20, 'CE::lesson_type_11', 'Nachtfahrten', 0, 1004, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (247, 20, 'C::lesson_type_16', 'Praktische Prüfung', 1, 5, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (248, 20, 'CE::lesson_type_16', 'Praktische Prüfung', 1, 1005, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (261, 22, 'C::lesson_type_14', 'Einweisung', 1, 0, 1, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (262, 22, 'CE::lesson_type_14', 'Einweisung', 0, 1000, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (263, 22, 'C::lesson_type_15', 'Übungsstunde', 7, 1, 7, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (264, 22, 'CE::lesson_type_15', 'Übungsstunde', 8, 1001, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (265, 22, 'C::lesson_type_12', 'Überlandfahrt', 5, 2, 2, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (266, 22, 'CE::lesson_type_12', 'Überlandfahrt', 3, 1002, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (267, 22, 'C::lesson_type_13', 'Autobahnfahrt', 2, 3, 2, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (268, 22, 'CE::lesson_type_13', 'Autobahnfahrt', 1, 1003, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (269, 22, 'C::lesson_type_11', 'Nachtfahrten', 3, 4, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (270, 22, 'CE::lesson_type_11', 'Nachtfahrten', 0, 1004, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (271, 22, 'C::lesson_type_16', 'Praktische Prüfung', 1, 5, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (272, 22, 'CE::lesson_type_16', 'Praktische Prüfung', 1, 1005, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (273, 21, 'lesson_type_14', 'Einweisung', 1, 0, 0, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (274, 21, 'lesson_type_15', 'Übungsstunde', 8, 1, 0, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (275, 21, 'lesson_type_12', 'Überlandfahrt', 5, 2, 0, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (276, 21, 'lesson_type_13', 'Autobahnfahrt', 2, 3, 0, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (277, 21, 'lesson_type_11', 'Nachtfahrten', 3, 4, 0, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (278, 21, 'lesson_type_16', 'Praktische Prüfung', 1, 5, 0, ''); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (279, 23, 'C::lesson_type_14', 'Einweisung', 1, 0, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (280, 23, 'CE::lesson_type_14', 'Einweisung', 0, 1000, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (281, 23, 'C::lesson_type_15', 'Übungsstunde', 7, 1, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (282, 23, 'CE::lesson_type_15', 'Übungsstunde', 8, 1001, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (283, 23, 'C::lesson_type_12', 'Überlandfahrt', 5, 2, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (284, 23, 'CE::lesson_type_12', 'Überlandfahrt', 3, 1002, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (285, 23, 'C::lesson_type_13', 'Autobahnfahrt', 2, 3, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (286, 23, 'CE::lesson_type_13', 'Autobahnfahrt', 1, 1003, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (287, 23, 'C::lesson_type_11', 'Nachtfahrten', 3, 4, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (288, 23, 'CE::lesson_type_11', 'Nachtfahrten', 0, 1004, 0, 'CE'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (289, 23, 'C::lesson_type_16', 'Praktische Prüfung', 1, 5, 0, 'C'); +INSERT INTO student_requirement_snapshots (id, student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) VALUES (290, 23, 'CE::lesson_type_16', 'Praktische Prüfung', 1, 1005, 0, 'CE'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (11, 2, 7, 3, 15, 'Übungsstunde', 'student', '2026-04-23T12:00:00+00:00', '2026-04-23T13:30:00+00:00', 2, 'planned', '', 0, 6, 6, '2026-04-23T07:15:35+00:00', '2026-04-23T07:38:40+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (14, 2, 9, 3, 15, 'Übungsstunde', 'student', '2026-04-23T13:45:00+00:00', '2026-04-23T16:00:00+00:00', 3, 'planned', '', 1, 6, 6, '2026-04-23T07:19:46+00:00', '2026-04-23T07:19:46+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (15, 2, 10, 3, 14, 'Einweisung', 'student', '2026-04-23T16:15:00+00:00', '2026-04-23T17:00:00+00:00', 1, 'planned', 'Erste Fahrstunde', 1, 6, 6, '2026-04-23T07:20:34+00:00', '2026-04-23T08:34:15+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (16, 2, 11, 3, 11, 'Nachtfahrten', 'student', '2026-04-23T19:15:00+00:00', '2026-04-23T21:30:00+00:00', 3, 'planned', '', 0, 6, 6, '2026-04-23T07:21:23+00:00', '2026-04-24T09:12:56+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (17, 2, 6, 3, 15, 'Übungsstunde', 'student', '2026-04-24T13:45:00+00:00', '2026-04-24T16:00:00+00:00', 3, 'planned', '', 0, 6, 6, '2026-04-23T07:22:07+00:00', '2026-04-24T08:19:09+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (18, 2, 10, 3, 15, 'Übungsstunde', 'student', '2026-04-24T16:15:00+00:00', '2026-04-24T18:30:00+00:00', 3, 'planned', '', 0, 6, 6, '2026-04-23T07:22:58+00:00', '2026-04-24T07:43:59+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (19, 2, 13, 3, 11, 'Nachtfahrten', 'student', '2026-04-24T18:45:00+00:00', '2026-04-24T21:00:00+00:00', 3, 'planned', '', 0, 6, 6, '2026-04-23T07:23:26+00:00', '2026-04-24T06:56:17+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (20, 2, 14, 3, 11, 'Nachtfahrten', 'student', '2026-04-24T21:00:00+00:00', '2026-04-24T23:15:00+00:00', 3, 'planned', '', 0, 6, 6, '2026-04-23T07:24:01+00:00', '2026-04-24T07:30:07+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (22, 2, 3, 3, 15, 'Übungsstunde', 'student', '2026-04-27T07:45:00+00:00', '2026-04-27T10:45:00+00:00', 4, 'planned', '', 0, 6, 6, '2026-04-23T07:26:26+00:00', '2026-04-24T07:36:25+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (23, 2, 13, 3, 12, 'Überlandfahrt', 'student', '2026-04-28T06:00:00+00:00', '2026-04-28T08:15:00+00:00', 3, 'planned', '', 0, 6, 6, '2026-04-23T07:26:51+00:00', '2026-04-24T07:24:01+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (24, 2, 3, 3, 15, 'Übungsstunde', 'student', '2026-04-28T14:15:00+00:00', '2026-04-28T15:00:00+00:00', 1, 'planned', '', 0, 6, 6, '2026-04-23T07:29:01+00:00', '2026-04-24T07:42:24+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (25, 2, 9, 3, 12, 'Überlandfahrt', 'student', '2026-04-29T09:00:00+00:00', '2026-04-29T10:30:00+00:00', 2, 'planned', '', 0, 6, 6, '2026-04-23T07:29:48+00:00', '2026-04-24T07:47:37+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (26, 2, 11, 3, 13, 'Autobahnfahrt', 'student', '2026-04-29T06:30:00+00:00', '2026-04-29T08:00:00+00:00', 2, 'planned', '', 0, 6, 6, '2026-04-23T07:31:31+00:00', '2026-04-24T08:10:04+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (27, 2, 3, 3, 12, 'Überlandfahrt', 'student', '2026-04-29T12:15:00+00:00', '2026-04-29T13:45:00+00:00', 2, 'planned', '', 0, 6, 6, '2026-04-23T07:32:13+00:00', '2026-04-24T07:41:27+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (28, 2, 17, 3, 12, 'Überlandfahrt', 'student', '2026-04-28T08:30:00+00:00', '2026-04-28T10:45:00+00:00', 3, 'planned', '', 0, 6, 6, '2026-04-23T07:36:17+00:00', '2026-04-24T08:02:02+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (29, 2, NULL, 3, 21, 'Sonstige Taetigkeit', 'work', '2026-04-23T10:00:00+00:00', '2026-04-23T11:45:00+00:00', 2, 'completed', '', 0, 6, 6, '2026-04-23T07:38:58+00:00', '2026-04-24T09:12:18+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (39, 2, 13, 3, 13, 'Autobahnfahrt', 'student', '2026-04-30T08:15:00+00:00', '2026-04-30T09:45:00+00:00', 2, 'planned', '', 0, 6, 6, '2026-04-23T08:05:22+00:00', '2026-04-24T07:24:28+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (40, 2, 14, 3, 15, 'Übungsstunde', 'student', '2026-04-30T10:30:00+00:00', '2026-04-30T13:30:00+00:00', 4, 'planned', '', 0, 6, 6, '2026-04-23T08:05:59+00:00', '2026-04-24T07:33:13+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (41, 2, 17, 3, 13, 'Autobahnfahrt', 'student', '2026-04-30T13:30:00+00:00', '2026-04-30T15:00:00+00:00', 2, 'planned', '', 0, 6, 6, '2026-04-23T08:06:20+00:00', '2026-04-24T08:01:06+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (43, 2, 6, 3, 12, 'Überlandfahrt', 'student', '2026-05-04T11:15:00+00:00', '2026-05-04T14:15:00+00:00', 4, 'planned', '', 1, 6, 6, '2026-04-23T08:08:46+00:00', '2026-04-27T13:38:49+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (44, 2, 3, 3, 13, 'Autobahnfahrt', 'student', '2026-05-04T14:30:00+00:00', '2026-05-04T16:00:00+00:00', 2, 'planned', '', 1, 6, 6, '2026-04-23T08:08:57+00:00', '2026-04-23T08:16:03+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (46, 2, 17, 3, 11, 'Nachtfahrten', 'student', '2026-05-02T18:15:00+00:00', '2026-05-02T20:30:00+00:00', 3, 'planned', '', 0, 6, 6, '2026-04-23T08:09:52+00:00', '2026-04-24T08:39:51+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (49, 2, 7, 3, 13, 'Autobahnfahrt', 'student', '2026-05-04T16:15:00+00:00', '2026-05-04T17:45:00+00:00', 2, 'planned', '', 1, 6, 6, '2026-04-23T08:16:18+00:00', '2026-04-23T08:30:07+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (51, 2, 14, 3, 12, 'Überlandfahrt', 'student', '2026-05-05T07:00:00+00:00', '2026-05-05T07:45:00+00:00', 1, 'planned', '', 0, 6, 6, '2026-04-23T08:17:45+00:00', '2026-05-04T06:37:04+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (52, 2, 14, 3, 12, 'Überlandfahrt', 'student', '2026-05-08T11:30:00+00:00', '2026-05-08T14:30:00+00:00', 4, 'planned', '', 0, 6, 6, '2026-04-23T08:18:12+00:00', '2026-05-06T08:09:09+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (54, 2, 15, 3, 15, 'Übungsstunde', 'student', '2026-05-12T06:00:00+00:00', '2026-05-12T07:30:00+00:00', 2, 'planned', '', 0, 6, 6, '2026-04-23T08:21:26+00:00', '2026-04-23T08:21:32+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (55, 2, 7, 3, 12, 'Überlandfahrt', 'student', '2026-04-28T12:30:00+00:00', '2026-04-28T14:00:00+00:00', 2, 'planned', '', 1, 6, 6, '2026-04-23T08:29:26+00:00', '2026-04-23T08:29:26+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (56, 2, 7, 3, 15, 'Übungsstunde', 'student', '2026-05-04T17:45:00+00:00', '2026-05-04T18:30:00+00:00', 1, 'planned', '', 1, 6, 6, '2026-04-23T08:30:16+00:00', '2026-04-23T08:30:16+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (57, 2, 7, 3, 15, 'Übungsstunde', 'student', '2026-04-28T11:00:00+00:00', '2026-04-28T12:30:00+00:00', 2, 'planned', '', 1, 6, 6, '2026-04-23T08:30:35+00:00', '2026-04-23T08:30:35+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (58, 2, 10, 3, 15, 'Übungsstunde', 'student', '2026-04-23T17:00:00+00:00', '2026-04-23T18:30:00+00:00', 2, 'planned', '', 1, 6, 6, '2026-04-23T08:34:06+00:00', '2026-04-23T08:34:10+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (59, 2, 15, 3, 16, 'Praktische Prüfung', 'student', '2026-05-13T08:50:00+00:00', '2026-05-13T10:05:00+00:00', 1, 'planned', '', 0, 6, 6, '2026-04-23T09:04:23+00:00', '2026-05-07T11:54:19+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (61, 2, 3, 3, 16, 'Praktische Prüfung', 'student', '2026-05-05T12:15:00+00:00', '2026-05-05T13:30:00+00:00', 1, 'planned', '', 0, 6, 6, '2026-04-23T09:06:24+00:00', '2026-05-04T06:36:24+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (62, 2, 7, 3, 16, 'Praktische Prüfung', 'student', '2026-05-05T11:00:00+00:00', '2026-05-05T12:15:00+00:00', 1, 'planned', '', 0, 6, 6, '2026-04-23T09:06:47+00:00', '2026-05-04T18:38:20+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (63, 2, 11, 3, 12, 'Überlandfahrt', 'student', '2026-04-27T11:00:00+00:00', '2026-04-27T12:30:00+00:00', 2, 'planned', '', 0, 6, 6, '2026-04-23T21:47:43+00:00', '2026-04-27T11:00:51+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (67, 2, 13, 3, 15, 'Übungsstunde', 'student', '2026-04-30T09:45:00+00:00', '2026-04-30T10:30:00+00:00', 1, 'planned', '', 0, 6, 6, '2026-04-24T07:24:36+00:00', '2026-04-24T07:24:43+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (68, 2, 14, 3, 13, 'Autobahnfahrt', 'student', '2026-05-05T07:45:00+00:00', '2026-05-05T09:15:00+00:00', 2, 'planned', '', 0, 6, 6, '2026-04-24T07:29:55+00:00', '2026-05-04T06:36:52+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (69, 2, 14, 3, 16, 'Praktische Prüfung', 'student', '2026-05-11T11:40:00+00:00', '2026-05-11T13:05:00+00:00', 1, 'planned', '', 1, 6, 6, '2026-04-24T07:31:52+00:00', '2026-05-08T07:28:05+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (70, 2, 3, 3, 12, 'Überlandfahrt', 'student', '2026-04-28T15:00:00+00:00', '2026-04-28T15:45:00+00:00', 1, 'planned', '', 1, 6, 6, '2026-04-24T07:42:39+00:00', '2026-04-24T07:42:39+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (72, 2, 13, 3, 16, 'Praktische Prüfung', 'student', '2026-05-06T11:20:00+00:00', '2026-05-06T12:35:00+00:00', 1, 'planned', '', 0, 6, 6, '2026-04-24T07:44:24+00:00', '2026-05-04T06:34:51+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (73, 2, 9, 3, 13, 'Autobahnfahrt', 'student', '2026-04-29T10:30:00+00:00', '2026-04-29T12:00:00+00:00', 2, 'planned', '', 0, 6, 6, '2026-04-24T07:47:49+00:00', '2026-04-24T07:47:52+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (74, 2, 11, 3, 16, 'Praktische Prüfung', 'student', '2026-05-06T10:15:00+00:00', '2026-05-06T11:40:00+00:00', 1, 'planned', '', 1, 6, 6, '2026-04-24T07:48:36+00:00', '2026-05-07T11:48:54+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (76, 2, 11, 3, 15, 'Übungsstunde', 'student', '2026-04-27T12:30:00+00:00', '2026-04-27T13:15:00+00:00', 1, 'planned', '', 0, 6, 6, '2026-04-24T07:49:49+00:00', '2026-04-27T12:05:20+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (78, 2, NULL, 3, 17, 'Privat', 'private', '2026-04-30T06:00:00+00:00', '2026-04-30T08:15:00+00:00', 2, 'planned', '', 0, 6, 6, '2026-04-24T07:59:59+00:00', '2026-04-24T08:02:57+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (80, 2, 17, 3, 12, 'Überlandfahrt', 'student', '2026-04-30T15:00:00+00:00', '2026-04-30T16:30:00+00:00', 2, 'planned', '', 0, 6, 6, '2026-04-24T08:01:22+00:00', '2026-04-24T08:42:10+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (81, 2, 11, 3, 12, 'Überlandfahrt', 'student', '2026-04-29T08:00:00+00:00', '2026-04-29T08:45:00+00:00', 1, 'planned', '', 0, 6, 6, '2026-04-24T08:10:10+00:00', '2026-04-24T08:10:20+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (82, 2, 9, 3, 16, 'Praktische Prüfung', 'student', '2026-05-06T08:50:00+00:00', '2026-05-06T10:15:00+00:00', 1, 'planned', '', 1, 6, 6, '2026-04-24T08:10:58+00:00', '2026-05-07T11:48:41+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (83, 2, 11, 3, 12, 'Überlandfahrt', 'student', '2026-05-05T14:00:00+00:00', '2026-05-05T15:30:00+00:00', 2, 'planned', '', 0, 6, 6, '2026-04-24T08:11:27+00:00', '2026-05-05T07:13:35+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (86, 2, 6, 3, 16, 'Praktische Prüfung', 'student', '2026-05-05T09:45:00+00:00', '2026-05-05T11:00:00+00:00', 1, 'planned', '', 0, 6, 6, '2026-04-24T08:18:47+00:00', '2026-05-04T06:35:48+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (87, 2, 9, 3, 11, 'Nachtfahrten', 'student', '2026-04-25T18:30:00+00:00', '2026-04-25T20:45:00+00:00', 3, 'planned', '', 0, 6, 6, '2026-04-24T08:25:38+00:00', '2026-04-24T08:39:40+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (88, 2, NULL, 3, 20, 'Wechselfahrt', 'work', '2026-04-23T11:45:00+00:00', '2026-04-23T12:00:00+00:00', 1, 'planned', '', 0, 6, 6, '2026-04-24T09:12:32+00:00', '2026-04-24T09:12:32+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (89, 2, NULL, 3, 21, 'Sonstige Taetigkeit', 'work', '2026-04-23T18:30:00+00:00', '2026-04-23T19:30:00+00:00', 1, 'planned', '', 1, 6, 6, '2026-04-24T09:13:12+00:00', '2026-04-24T09:13:12+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (91, 2, 15, 3, 15, 'Übungsstunde', 'student', '2026-05-11T08:30:00+00:00', '2026-05-11T10:45:00+00:00', 3, 'planned', '', 0, 6, 6, '2026-04-27T07:08:36+00:00', '2026-05-08T07:28:16+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (92, 2, 15, 3, 12, 'Überlandfahrt', 'student', '2026-05-12T07:30:00+00:00', '2026-05-12T09:00:00+00:00', 2, 'planned', '', 1, 6, 6, '2026-04-27T07:09:19+00:00', '2026-04-27T07:09:19+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (95, 2, 17, 3, 15, 'Übungsstunde', 'student', '2026-05-02T17:30:00+00:00', '2026-05-02T18:15:00+00:00', 1, 'planned', '', 0, 6, 6, '2026-04-28T10:51:37+00:00', '2026-04-28T10:51:37+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (96, 2, 9, 3, 12, 'Überlandfahrt', 'student', '2026-05-04T09:30:00+00:00', '2026-05-04T11:00:00+00:00', 2, 'planned', '', 0, 6, 6, '2026-04-29T19:22:31+00:00', '2026-04-29T19:22:48+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (97, 2, NULL, 3, 17, 'Privat', 'private', '2026-05-08T00:15:00+00:00', '2026-05-08T11:30:00+00:00', 11, 'planned', '', 1, 6, 6, '2026-05-05T19:28:33+00:00', '2026-05-05T19:28:33+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (99, 2, NULL, 3, 17, 'Privat', 'private', '2026-05-19T09:00:00+00:00', '2026-05-19T10:00:00+00:00', 1, 'planned', '', 0, 6, 6, '2026-05-06T16:05:05+00:00', '2026-05-06T16:05:05+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (100, 2, 22, 3, 11, 'Nachtfahrten', 'student', '2026-05-07T18:30:00+00:00', '2026-05-07T20:45:00+00:00', 3, 'planned', '', 0, 6, 6, '2026-05-07T11:36:23+00:00', '2026-05-07T12:03:24+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (101, 2, 20, 3, 14, 'Einweisung', 'student', '2026-05-07T15:45:00+00:00', '2026-05-07T16:30:00+00:00', 1, 'planned', '', 0, 6, 6, '2026-05-07T11:36:40+00:00', '2026-05-07T12:12:40+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (102, 2, 20, 3, 15, 'Übungsstunde', 'student', '2026-05-08T15:00:00+00:00', '2026-05-08T17:15:00+00:00', 3, 'planned', '', 0, 6, 6, '2026-05-07T11:37:51+00:00', '2026-05-13T10:31:47+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (103, 2, 17, 3, 16, 'Praktische Prüfung', 'student', '2026-05-06T12:30:00+00:00', '2026-05-06T13:55:00+00:00', 1, 'planned', '', 1, 6, 6, '2026-05-07T11:48:01+00:00', '2026-05-07T11:48:31+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (104, 2, 20, 3, 15, 'Übungsstunde', 'student', '2026-05-07T16:30:00+00:00', '2026-05-07T18:00:00+00:00', 2, 'planned', '', 0, 6, 6, '2026-05-07T12:12:48+00:00', '2026-05-07T12:13:08+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (108, 2, NULL, 3, 20, 'Wechselfahrt', 'work', '2026-05-11T13:00:00+00:00', '2026-05-11T14:00:00+00:00', 1, 'planned', '', 1, 6, 6, '2026-05-08T07:29:03+00:00', '2026-05-08T07:29:03+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (110, 2, NULL, 3, 20, 'Wechselfahrt', 'work', '2026-05-13T10:00:00+00:00', '2026-05-13T11:00:00+00:00', 1, 'planned', '', 1, 6, 6, '2026-05-08T07:30:18+00:00', '2026-05-08T07:30:18+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (116, 2, 20, 3, 15, 'Übungsstunde', 'student', '2026-05-13T11:15:00+00:00', '2026-05-13T12:45:00+00:00', 2, 'planned', '', 0, 6, 6, '2026-05-08T07:39:55+00:00', '2026-05-13T10:32:15+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (119, 2, 21, 3, 14, 'Einweisung', 'student', '2026-05-12T09:15:00+00:00', '2026-05-12T10:00:00+00:00', 1, 'planned', '', 0, 6, 6, '2026-05-08T07:45:42+00:00', '2026-05-09T13:43:04+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (120, 2, 21, 3, 14, 'Einweisung', 'student', '2026-05-12T10:00:00+00:00', '2026-05-12T11:30:00+00:00', 2, 'planned', '', 0, 6, 6, '2026-05-08T07:45:50+00:00', '2026-05-09T13:43:18+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (123, 2, 19, 3, 14, 'Einweisung', 'student', '2026-05-12T12:00:00+00:00', '2026-05-12T12:45:00+00:00', 1, 'planned', '', 1, 6, 6, '2026-05-09T13:45:05+00:00', '2026-05-09T13:45:05+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (124, 2, 19, 3, 15, 'Übungsstunde', 'student', '2026-05-12T12:45:00+00:00', '2026-05-12T14:15:00+00:00', 2, 'planned', '', 1, 6, 6, '2026-05-09T13:45:20+00:00', '2026-05-09T13:45:20+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (125, 2, 23, 3, 14, 'Einweisung', 'student', '2026-05-13T05:30:00+00:00', '2026-05-13T06:15:00+00:00', 1, 'planned', '', 0, 6, 6, '2026-05-11T17:45:00+00:00', '2026-05-11T17:47:29+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (126, 2, 23, 3, 15, 'Übungsstunde', 'student', '2026-05-13T06:15:00+00:00', '2026-05-13T07:45:00+00:00', 2, 'planned', '', 0, 6, 6, '2026-05-11T17:47:35+00:00', '2026-05-13T07:03:13+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (128, 2, 20, 3, 11, 'Nachtfahrten', 'student', '2026-05-15T18:00:00+00:00', '2026-05-15T20:15:00+00:00', 3, 'planned', '', 0, 6, 6, '2026-05-11T17:50:12+00:00', '2026-05-13T14:12:08+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (130, 2, NULL, 3, 17, 'Privat', 'private', '2026-05-16T09:00:00+00:00', '2026-05-16T10:30:00+00:00', 2, 'planned', '', 0, 6, 6, '2026-05-11T17:52:04+00:00', '2026-05-11T17:52:24+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (131, 2, NULL, 3, 17, 'Privat', 'private', '2026-05-16T10:45:00+00:00', '2026-05-16T12:15:00+00:00', 2, 'planned', '', 0, 6, 6, '2026-05-11T17:52:33+00:00', '2026-05-11T17:52:33+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (133, 2, NULL, 3, 21, 'Sonstige Taetigkeit', 'work', '2026-05-11T10:45:00+00:00', '2026-05-11T11:30:00+00:00', 1, 'planned', '', 0, 6, 6, '2026-05-11T18:09:30+00:00', '2026-05-11T18:09:30+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (134, 2, NULL, 3, 21, 'Sonstige Taetigkeit', 'work', '2026-05-13T08:00:00+00:00', '2026-05-13T08:45:00+00:00', 1, 'planned', '', 0, 6, 6, '2026-05-11T18:09:58+00:00', '2026-05-11T18:10:10+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (136, 2, 20, 3, 12, 'Überlandfahrt', 'student', '2026-05-13T12:45:00+00:00', '2026-05-13T14:15:00+00:00', 2, 'planned', '', 1, 6, 6, '2026-05-13T10:32:27+00:00', '2026-05-13T10:32:27+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (137, 2, 19, 3, 15, 'Übungsstunde', 'student', '2026-05-15T14:45:00+00:00', '2026-05-15T17:45:00+00:00', 4, 'planned', '', 0, 6, 6, '2026-05-13T10:34:50+00:00', '2026-05-14T12:29:05+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (138, 2, 19, 3, 11, 'Nachtfahrten', 'student', '2026-05-22T18:00:00+00:00', '2026-05-22T20:15:00+00:00', 3, 'planned', '', 0, 6, 6, '2026-05-14T12:15:48+00:00', '2026-05-14T12:15:48+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (139, 2, NULL, 3, 17, 'Privat', 'private', '2026-05-18T15:00:00+00:00', '2026-05-18T19:00:00+00:00', 4, 'planned', '', 0, 6, 6, '2026-05-14T12:16:59+00:00', '2026-05-14T12:17:16+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (140, 2, NULL, 3, 17, 'Privat', 'private', '2026-05-20T15:00:00+00:00', '2026-05-20T19:00:00+00:00', 4, 'planned', '', 0, 6, 6, '2026-05-14T12:17:10+00:00', '2026-05-14T12:17:13+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (141, 2, 23, 3, 15, 'Übungsstunde', 'student', '2026-05-19T05:30:00+00:00', '2026-05-19T08:30:00+00:00', 4, 'planned', '', 0, 6, 6, '2026-05-14T12:21:43+00:00', '2026-05-14T12:22:57+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (142, 2, 22, 3, 14, 'Einweisung', 'student', '2026-05-22T11:30:00+00:00', '2026-05-22T14:30:00+00:00', 4, 'planned', '', 0, 6, 6, '2026-05-14T12:24:02+00:00', '2026-05-14T12:25:48+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (144, 2, 19, 3, 15, 'Übungsstunde', 'student', '2026-05-27T12:00:00+00:00', '2026-05-27T12:45:00+00:00', 1, 'planned', '', 0, 6, 6, '2026-05-14T12:31:05+00:00', '2026-05-14T12:41:13+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (145, 2, 19, 3, 12, 'Überlandfahrt', 'student', '2026-05-27T12:45:00+00:00', '2026-05-27T15:00:00+00:00', 3, 'planned', '', 1, 6, 6, '2026-05-14T12:31:42+00:00', '2026-05-14T12:41:18+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (146, 2, 19, 3, 16, 'Praktische Prüfung', 'student', '2026-06-01T06:00:00+00:00', '2026-06-01T07:25:00+00:00', 1, 'planned', '', 0, 6, 6, '2026-05-14T12:31:59+00:00', '2026-05-14T12:31:59+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (147, 2, 22, 3, 16, 'Praktische Prüfung', 'student', '2026-06-01T07:45:00+00:00', '2026-06-01T09:10:00+00:00', 1, 'planned', '', 1, 6, 6, '2026-05-14T12:32:51+00:00', '2026-05-14T12:32:51+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (148, 2, 19, 3, 12, 'Überlandfahrt', 'student', '2026-05-28T12:00:00+00:00', '2026-05-28T13:30:00+00:00', 2, 'planned', '', 0, 6, 6, '2026-05-14T12:33:31+00:00', '2026-05-14T12:33:31+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (149, 2, 19, 3, 13, 'Autobahnfahrt', 'student', '2026-05-28T13:30:00+00:00', '2026-05-28T15:00:00+00:00', 2, 'planned', '', 1, 6, 6, '2026-05-14T12:33:43+00:00', '2026-05-14T12:33:43+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (150, 2, 3, 3, 15, 'Übungsstunde', 'student', '2026-05-26T11:00:00+00:00', '2026-05-26T14:00:00+00:00', 4, 'planned', '', 0, 6, 6, '2026-05-14T12:37:39+00:00', '2026-05-14T12:41:57+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (151, 2, NULL, 3, 17, 'Privat', 'private', '2026-05-26T15:00:00+00:00', '2026-05-26T16:00:00+00:00', 1, 'planned', '', 0, 6, 6, '2026-05-14T12:41:38+00:00', '2026-05-14T12:41:38+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (152, 2, 15, 3, 14, 'Einweisung', 'student', '2026-05-22T14:45:00+00:00', '2026-05-22T17:45:00+00:00', 4, 'planned', '', 0, 6, 6, '2026-05-14T12:46:48+00:00', '2026-05-14T12:46:48+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (154, 2, 22, 3, 15, 'Übungsstunde', 'student', '2026-05-29T06:30:00+00:00', '2026-05-29T09:30:00+00:00', 4, 'planned', '', 0, 6, 6, '2026-05-15T10:50:45+00:00', '2026-05-15T10:51:24+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (155, 2, 20, 3, 12, 'Überlandfahrt', 'student', '2026-05-19T10:30:00+00:00', '2026-05-19T12:45:00+00:00', 3, 'planned', '', 0, 6, 6, '2026-05-15T20:50:25+00:00', '2026-05-15T20:50:36+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (156, 2, 20, 3, 13, 'Autobahnfahrt', 'student', '2026-05-21T11:45:00+00:00', '2026-05-21T13:15:00+00:00', 2, 'planned', '', 1, 6, 6, '2026-05-15T20:50:59+00:00', '2026-05-15T20:54:45+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (157, 2, 20, 3, 15, 'Übungsstunde', 'student', '2026-05-21T06:00:00+00:00', '2026-05-21T09:00:00+00:00', 4, 'planned', '', 0, 6, 6, '2026-05-15T20:51:35+00:00', '2026-05-15T20:54:04+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (158, 2, 20, 3, 15, 'Übungsstunde', 'student', '2026-05-21T09:15:00+00:00', '2026-05-21T11:30:00+00:00', 3, 'planned', '', 0, 6, 6, '2026-05-15T20:51:41+00:00', '2026-05-15T20:54:19+00:00'); +INSERT INTO appointments (id, tenant_id, student_id, instructor_id, lesson_type_id, title, category, start_at, end_at, units, status, notes, warning_acknowledged, created_by, updated_by, created_at, updated_at) VALUES (159, 2, 14, 3, 14, 'Einweisung', 'student', '2026-05-18T11:30:00+00:00', '2026-05-18T14:00:00+00:00', 3, 'planned', '', 0, 6, 6, '2026-05-18T14:07:24+00:00', '2026-05-18T14:07:24+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (16, 14, 'C::lesson_type_15', 'Übungsstunde', 3, '2026-04-23T07:19:46+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (44, 11, 'lesson_type_15', 'Übungsstunde', 2, '2026-04-23T07:38:40+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (59, 44, 'lesson_type_13', 'Autobahnfahrt', 2, '2026-04-23T08:16:03+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (66, 54, 'lesson_type_15', 'Übungsstunde', 2, '2026-04-23T08:21:32+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (68, 55, 'lesson_type_12', 'Überlandfahrt', 2, '2026-04-23T08:29:26+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (69, 49, 'lesson_type_13', 'Autobahnfahrt', 2, '2026-04-23T08:30:07+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (70, 56, 'lesson_type_15', 'Übungsstunde', 1, '2026-04-23T08:30:16+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (71, 57, 'lesson_type_15', 'Übungsstunde', 2, '2026-04-23T08:30:35+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (74, 58, 'C::lesson_type_15', 'Übungsstunde', 2, '2026-04-23T08:34:10+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (75, 15, 'C::lesson_type_14', 'Einweisung', 1, '2026-04-23T08:34:15+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (88, 19, 'C::lesson_type_11', 'Nachtfahrten', 3, '2026-04-24T06:56:17+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (101, 23, 'C::lesson_type_12', 'Überlandfahrt', 3, '2026-04-24T07:24:01+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (103, 39, 'C::lesson_type_13', 'Autobahnfahrt', 2, '2026-04-24T07:24:28+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (105, 67, 'C::lesson_type_15', 'Übungsstunde', 1, '2026-04-24T07:24:43+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (109, 20, 'C::lesson_type_11', 'Nachtfahrten', 3, '2026-04-24T07:30:07+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (114, 40, 'C::lesson_type_15', 'Übungsstunde', 4, '2026-04-24T07:33:13+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (116, 22, 'lesson_type_15', 'Übungsstunde', 4, '2026-04-24T07:36:25+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (118, 27, 'lesson_type_12', 'Überlandfahrt', 2, '2026-04-24T07:41:27+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (119, 24, 'lesson_type_15', 'Übungsstunde', 1, '2026-04-24T07:42:24+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (120, 70, 'lesson_type_12', 'Überlandfahrt', 1, '2026-04-24T07:42:39+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (123, 18, 'C::lesson_type_15', 'Übungsstunde', 3, '2026-04-24T07:43:59+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (125, 25, 'C::lesson_type_12', 'Überlandfahrt', 2, '2026-04-24T07:47:37+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (127, 73, 'C::lesson_type_13', 'Autobahnfahrt', 2, '2026-04-24T07:47:52+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (134, 41, 'C::lesson_type_13', 'Autobahnfahrt', 2, '2026-04-24T08:01:06+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (137, 28, 'C::lesson_type_12', 'Überlandfahrt', 3, '2026-04-24T08:02:02+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (139, 26, 'C::lesson_type_13', 'Autobahnfahrt', 2, '2026-04-24T08:10:04+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (141, 81, 'C::lesson_type_12', 'Überlandfahrt', 1, '2026-04-24T08:10:20+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (150, 17, 'lesson_type_15', 'Übungsstunde', 3, '2026-04-24T08:19:09+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (165, 87, 'C::lesson_type_11', 'Nachtfahrten', 3, '2026-04-24T08:39:40+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (166, 46, 'C::lesson_type_11', 'Nachtfahrten', 3, '2026-04-24T08:39:51+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (168, 80, 'C::lesson_type_12', 'Überlandfahrt', 2, '2026-04-24T08:42:10+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (169, 16, 'C::lesson_type_11', 'Nachtfahrten', 3, '2026-04-24T09:12:56+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (175, 92, 'lesson_type_12', 'Überlandfahrt', 2, '2026-04-27T07:09:19+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (176, 63, 'C::lesson_type_12', 'Überlandfahrt', 2, '2026-04-27T11:00:51+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (179, 76, 'C::lesson_type_15', 'Übungsstunde', 1, '2026-04-27T12:05:20+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (183, 43, 'lesson_type_12', 'Überlandfahrt', 4, '2026-04-27T13:38:49+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (188, 95, 'C::lesson_type_15', 'Übungsstunde', 1, '2026-04-28T10:51:37+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (191, 96, 'C::lesson_type_12', 'Überlandfahrt', 2, '2026-04-29T19:22:48+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (218, 72, 'C::lesson_type_16', 'Praktische Prüfung', 1, '2026-05-04T06:34:51+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (221, 86, 'lesson_type_16', 'Praktische Prüfung', 1, '2026-05-04T06:35:48+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (224, 61, 'lesson_type_16', 'Praktische Prüfung', 1, '2026-05-04T06:36:24+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (226, 68, 'C::lesson_type_13', 'Autobahnfahrt', 2, '2026-05-04T06:36:52+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (227, 51, 'C::lesson_type_12', 'Überlandfahrt', 1, '2026-05-04T06:37:04+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (230, 62, 'lesson_type_16', 'Praktische Prüfung', 1, '2026-05-04T18:38:20+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (231, 83, 'C::lesson_type_12', 'Überlandfahrt', 2, '2026-05-05T07:13:35+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (234, 52, 'C::lesson_type_12', 'Überlandfahrt', 4, '2026-05-06T08:09:09+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (242, 103, 'C::lesson_type_16', 'Praktische Prüfung', 1, '2026-05-07T11:48:31+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (243, 82, 'C::lesson_type_16', 'Praktische Prüfung', 1, '2026-05-07T11:48:41+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (244, 74, 'C::lesson_type_16', 'Praktische Prüfung', 1, '2026-05-07T11:48:54+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (250, 59, 'lesson_type_16', 'Praktische Prüfung', 1, '2026-05-07T11:54:19+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (253, 100, 'C::lesson_type_11', 'Nachtfahrten', 3, '2026-05-07T12:03:24+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (255, 101, 'C::lesson_type_14', 'Einweisung', 1, '2026-05-07T12:12:40+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (257, 104, 'C::lesson_type_15', 'Übungsstunde', 2, '2026-05-07T12:13:08+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (258, 69, 'C::lesson_type_16', 'Praktische Prüfung', 1, '2026-05-08T07:28:05+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (259, 91, 'lesson_type_15', 'Übungsstunde', 3, '2026-05-08T07:28:16+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (284, 119, 'lesson_type_14', 'Einweisung', 1, '2026-05-09T13:43:04+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (285, 120, 'lesson_type_14', 'Einweisung', 2, '2026-05-09T13:43:18+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (286, 123, 'C::lesson_type_14', 'Einweisung', 1, '2026-05-09T13:45:05+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (287, 124, 'C::lesson_type_15', 'Übungsstunde', 2, '2026-05-09T13:45:20+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (291, 125, 'C::lesson_type_14', 'Einweisung', 1, '2026-05-11T17:47:29+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (301, 126, 'C::lesson_type_15', 'Übungsstunde', 2, '2026-05-13T07:03:13+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (304, 102, 'C::lesson_type_15', 'Übungsstunde', 3, '2026-05-13T10:31:47+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (305, 116, 'C::lesson_type_15', 'Übungsstunde', 2, '2026-05-13T10:32:15+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (306, 136, 'C::lesson_type_12', 'Überlandfahrt', 2, '2026-05-13T10:32:27+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (309, 128, 'C::lesson_type_11', 'Nachtfahrten', 3, '2026-05-13T14:12:08+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (313, 138, 'C::lesson_type_11', 'Nachtfahrten', 3, '2026-05-14T12:15:48+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (319, 141, 'C::lesson_type_15', 'Übungsstunde', 4, '2026-05-14T12:22:57+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (328, 142, 'C::lesson_type_14', 'Einweisung', 4, '2026-05-14T12:25:48+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (330, 137, 'C::lesson_type_15', 'Übungsstunde', 4, '2026-05-14T12:29:05+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (335, 146, 'C::lesson_type_16', 'Praktische Prüfung', 1, '2026-05-14T12:31:59+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (336, 147, 'C::lesson_type_16', 'Praktische Prüfung', 1, '2026-05-14T12:32:51+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (337, 148, 'C::lesson_type_12', 'Überlandfahrt', 2, '2026-05-14T12:33:31+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (338, 149, 'C::lesson_type_13', 'Autobahnfahrt', 2, '2026-05-14T12:33:43+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (341, 144, 'C::lesson_type_15', 'Übungsstunde', 1, '2026-05-14T12:41:13+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (342, 145, 'C::lesson_type_12', 'Überlandfahrt', 3, '2026-05-14T12:41:18+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (344, 150, 'lesson_type_15', 'Übungsstunde', 4, '2026-05-14T12:41:57+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (345, 152, 'lesson_type_14', 'Einweisung', 4, '2026-05-14T12:46:48+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (349, 154, 'C::lesson_type_15', 'Übungsstunde', 4, '2026-05-15T10:51:24+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (352, 155, 'C::lesson_type_12', 'Überlandfahrt', 3, '2026-05-15T20:50:36+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (359, 157, 'C::lesson_type_15', 'Übungsstunde', 4, '2026-05-15T20:54:04+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (360, 158, 'C::lesson_type_15', 'Übungsstunde', 3, '2026-05-15T20:54:19+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (362, 156, 'C::lesson_type_13', 'Autobahnfahrt', 2, '2026-05-15T20:54:45+00:00'); +INSERT INTO appointment_units (id, appointment_id, requirement_key, label, units_counted, created_at) VALUES (363, 159, 'C::lesson_type_14', 'Einweisung', 3, '2026-05-18T14:07:24+00:00'); +INSERT INTO settings (id, tenant_id, key_name, value) VALUES (1, 1, 'app.theme', 'signal'); \ No newline at end of file diff --git a/backup/old_api_20260520/database/migrations/001_initial.sql b/backup/old_api_20260520/database/migrations/001_initial.sql new file mode 100644 index 0000000..c9ec8b6 --- /dev/null +++ b/backup/old_api_20260520/database/migrations/001_initial.sql @@ -0,0 +1,145 @@ +CREATE TABLE IF NOT EXISTS tenants ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + timezone TEXT NOT NULL DEFAULT 'Europe/Berlin', + is_active INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tenant_id INTEGER NULL, + role TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + is_active INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS instructors ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tenant_id INTEGER NOT NULL, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + color TEXT NOT NULL, + notes TEXT DEFAULT '', + is_active INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS lesson_types ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tenant_id INTEGER NOT NULL, + name TEXT NOT NULL, + color TEXT NOT NULL, + default_duration INTEGER NOT NULL, + category TEXT NOT NULL, + requirement_key TEXT DEFAULT NULL, + is_billable INTEGER NOT NULL DEFAULT 0, + is_counted INTEGER NOT NULL DEFAULT 0, + is_rest_relevant INTEGER NOT NULL DEFAULT 0, + fixed_duration INTEGER NOT NULL DEFAULT 0, + sort_order INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS license_class_templates ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tenant_id INTEGER NOT NULL, + code TEXT NOT NULL, + name TEXT NOT NULL, + is_combination INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + UNIQUE (tenant_id, code), + FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS license_template_requirements ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + template_id INTEGER NOT NULL, + requirement_key TEXT NOT NULL, + label TEXT NOT NULL, + required_units INTEGER NOT NULL, + sort_order INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (template_id) REFERENCES license_class_templates (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS students ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tenant_id INTEGER NOT NULL, + template_id INTEGER NOT NULL, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + phone TEXT DEFAULT '', + notes TEXT DEFAULT '', + status TEXT NOT NULL DEFAULT 'active', + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE, + FOREIGN KEY (template_id) REFERENCES license_class_templates (id) +); + +CREATE TABLE IF NOT EXISTS student_requirement_snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + student_id INTEGER NOT NULL, + requirement_key TEXT NOT NULL, + label TEXT NOT NULL, + required_units INTEGER NOT NULL, + sort_order INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (student_id) REFERENCES students (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS appointments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tenant_id INTEGER NOT NULL, + student_id INTEGER DEFAULT NULL, + instructor_id INTEGER NOT NULL, + lesson_type_id INTEGER NOT NULL, + title TEXT NOT NULL, + category TEXT NOT NULL, + start_at TEXT NOT NULL, + end_at TEXT NOT NULL, + units INTEGER NOT NULL DEFAULT 1, + status TEXT NOT NULL DEFAULT 'planned', + notes TEXT DEFAULT '', + warning_acknowledged INTEGER NOT NULL DEFAULT 0, + created_by INTEGER NOT NULL, + updated_by INTEGER NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE, + FOREIGN KEY (student_id) REFERENCES students (id) ON DELETE SET NULL, + FOREIGN KEY (instructor_id) REFERENCES instructors (id), + FOREIGN KEY (lesson_type_id) REFERENCES lesson_types (id), + FOREIGN KEY (created_by) REFERENCES users (id), + FOREIGN KEY (updated_by) REFERENCES users (id) +); + +CREATE TABLE IF NOT EXISTS appointment_units ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + appointment_id INTEGER NOT NULL, + requirement_key TEXT DEFAULT NULL, + label TEXT DEFAULT NULL, + units_counted INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL, + FOREIGN KEY (appointment_id) REFERENCES appointments (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS settings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tenant_id INTEGER NOT NULL, + key_name TEXT NOT NULL, + value TEXT NOT NULL, + UNIQUE (tenant_id, key_name), + FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE +); diff --git a/backup/old_api_20260520/database/migrations/002_instructors_user_link.sql b/backup/old_api_20260520/database/migrations/002_instructors_user_link.sql new file mode 100644 index 0000000..d1a755e --- /dev/null +++ b/backup/old_api_20260520/database/migrations/002_instructors_user_link.sql @@ -0,0 +1 @@ +ALTER TABLE instructors ADD COLUMN user_id INTEGER DEFAULT NULL; diff --git a/backup/old_api_20260520/database/migrations/003_backfill_instructor_user_links.sql b/backup/old_api_20260520/database/migrations/003_backfill_instructor_user_links.sql new file mode 100644 index 0000000..c41d2fd --- /dev/null +++ b/backup/old_api_20260520/database/migrations/003_backfill_instructor_user_links.sql @@ -0,0 +1,11 @@ +UPDATE instructors +SET user_id = ( + SELECT u.id + FROM users u + WHERE u.tenant_id = instructors.tenant_id + AND u.role = 'instructor' + AND u.first_name = instructors.first_name + AND u.last_name = instructors.last_name + LIMIT 1 +) +WHERE user_id IS NULL; diff --git a/backup/old_api_20260520/database/migrations/004_lesson_type_template_visibility.sql b/backup/old_api_20260520/database/migrations/004_lesson_type_template_visibility.sql new file mode 100644 index 0000000..084a4f3 --- /dev/null +++ b/backup/old_api_20260520/database/migrations/004_lesson_type_template_visibility.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS lesson_type_template_visibility ( + lesson_type_id INTEGER NOT NULL, + template_id INTEGER NOT NULL, + PRIMARY KEY (lesson_type_id, template_id), + FOREIGN KEY (lesson_type_id) REFERENCES lesson_types (id) ON DELETE CASCADE, + FOREIGN KEY (template_id) REFERENCES license_class_templates (id) ON DELETE CASCADE +); + +INSERT OR IGNORE INTO lesson_type_template_visibility (lesson_type_id, template_id) +SELECT lt.id, tpl.id +FROM lesson_types lt +JOIN license_class_templates tpl ON tpl.tenant_id = lt.tenant_id +WHERE lt.category = 'student'; diff --git a/backup/old_api_20260520/database/migrations/005_tenant_location_sun.sql b/backup/old_api_20260520/database/migrations/005_tenant_location_sun.sql new file mode 100644 index 0000000..154f13a --- /dev/null +++ b/backup/old_api_20260520/database/migrations/005_tenant_location_sun.sql @@ -0,0 +1,3 @@ +ALTER TABLE tenants ADD COLUMN location_label TEXT DEFAULT ''; +ALTER TABLE tenants ADD COLUMN latitude REAL DEFAULT NULL; +ALTER TABLE tenants ADD COLUMN longitude REAL DEFAULT NULL; diff --git a/backup/old_api_20260520/database/migrations/006_tenant_federal_state.sql b/backup/old_api_20260520/database/migrations/006_tenant_federal_state.sql new file mode 100644 index 0000000..7af0b1b --- /dev/null +++ b/backup/old_api_20260520/database/migrations/006_tenant_federal_state.sql @@ -0,0 +1 @@ +ALTER TABLE tenants ADD COLUMN federal_state TEXT DEFAULT 'BE'; diff --git a/backup/old_api_20260520/database/migrations/007_student_prior_completed_units.sql b/backup/old_api_20260520/database/migrations/007_student_prior_completed_units.sql new file mode 100644 index 0000000..0d181c8 --- /dev/null +++ b/backup/old_api_20260520/database/migrations/007_student_prior_completed_units.sql @@ -0,0 +1 @@ +ALTER TABLE student_requirement_snapshots ADD COLUMN prior_completed_units INTEGER NOT NULL DEFAULT 0; diff --git a/backup/old_api_20260520/database/migrations/008_lesson_type_system_key.sql b/backup/old_api_20260520/database/migrations/008_lesson_type_system_key.sql new file mode 100644 index 0000000..623f0ff --- /dev/null +++ b/backup/old_api_20260520/database/migrations/008_lesson_type_system_key.sql @@ -0,0 +1,2 @@ +ALTER TABLE lesson_types ADD COLUMN system_key TEXT DEFAULT NULL; +CREATE UNIQUE INDEX IF NOT EXISTS idx_lesson_types_tenant_system_key ON lesson_types (tenant_id, system_key) WHERE system_key IS NOT NULL; diff --git a/backup/old_api_20260520/database/migrations/009_requirement_phase_labels.sql b/backup/old_api_20260520/database/migrations/009_requirement_phase_labels.sql new file mode 100644 index 0000000..8d403ba --- /dev/null +++ b/backup/old_api_20260520/database/migrations/009_requirement_phase_labels.sql @@ -0,0 +1,2 @@ +ALTER TABLE license_template_requirements ADD COLUMN phase_label TEXT NOT NULL DEFAULT ''; +ALTER TABLE student_requirement_snapshots ADD COLUMN phase_label TEXT NOT NULL DEFAULT ''; diff --git a/backup/old_api_20260520/database/migrations/010_student_email.sql b/backup/old_api_20260520/database/migrations/010_student_email.sql new file mode 100644 index 0000000..64fbf40 --- /dev/null +++ b/backup/old_api_20260520/database/migrations/010_student_email.sql @@ -0,0 +1 @@ +ALTER TABLE students ADD COLUMN email TEXT NOT NULL DEFAULT ''; diff --git a/backup/old_api_20260520/database/migrations/010_students_contact_flags.sql b/backup/old_api_20260520/database/migrations/010_students_contact_flags.sql new file mode 100644 index 0000000..1c1d76f --- /dev/null +++ b/backup/old_api_20260520/database/migrations/010_students_contact_flags.sql @@ -0,0 +1,2 @@ +ALTER TABLE students ADD COLUMN email TEXT DEFAULT ''; +ALTER TABLE students ADD COLUMN needs_glasses INTEGER NOT NULL DEFAULT 0; diff --git a/backup/old_api_20260520/database/migrations/011_student_needs_glasses.sql b/backup/old_api_20260520/database/migrations/011_student_needs_glasses.sql new file mode 100644 index 0000000..9e31849 --- /dev/null +++ b/backup/old_api_20260520/database/migrations/011_student_needs_glasses.sql @@ -0,0 +1 @@ +ALTER TABLE students ADD COLUMN needs_glasses INTEGER NOT NULL DEFAULT 0; diff --git a/backup/old_api_20260520/database/migrations/012_split_combination_templates.sql b/backup/old_api_20260520/database/migrations/012_split_combination_templates.sql new file mode 100644 index 0000000..9e37494 --- /dev/null +++ b/backup/old_api_20260520/database/migrations/012_split_combination_templates.sql @@ -0,0 +1,183 @@ +DROP TABLE IF EXISTS student_requirement_snapshots_old; +CREATE TEMP TABLE student_requirement_snapshots_old AS +SELECT * +FROM student_requirement_snapshots +WHERE student_id IN ( + SELECT s.id + FROM students s + JOIN license_class_templates t ON t.id = s.template_id + WHERE t.code IN ('C+CE', 'C1+C1E') +); + +DELETE FROM license_template_requirements +WHERE template_id IN ( + SELECT id FROM license_class_templates WHERE code IN ('C+CE', 'C1+C1E') +); + +INSERT INTO license_template_requirements (template_id, requirement_key, label, required_units, sort_order, phase_label) +SELECT id, 'C::practice', 'Uebungsstunden', 8, 0, 'C' +FROM license_class_templates +WHERE code = 'C+CE'; + +INSERT INTO license_template_requirements (template_id, requirement_key, label, required_units, sort_order, phase_label) +SELECT id, 'C::rural', 'Ueberland', 5, 1, 'C' +FROM license_class_templates +WHERE code = 'C+CE'; + +INSERT INTO license_template_requirements (template_id, requirement_key, label, required_units, sort_order, phase_label) +SELECT id, 'C::highway', 'Autobahn', 4, 2, 'C' +FROM license_class_templates +WHERE code = 'C+CE'; + +INSERT INTO license_template_requirements (template_id, requirement_key, label, required_units, sort_order, phase_label) +SELECT id, 'C::night', 'Nachtfahrt', 3, 3, 'C' +FROM license_class_templates +WHERE code = 'C+CE'; + +INSERT INTO license_template_requirements (template_id, requirement_key, label, required_units, sort_order, phase_label) +SELECT id, 'CE::practice', 'Uebungsstunden', 6, 100, 'CE' +FROM license_class_templates +WHERE code = 'C+CE'; + +INSERT INTO license_template_requirements (template_id, requirement_key, label, required_units, sort_order, phase_label) +SELECT id, 'CE::rural', 'Ueberland', 3, 101, 'CE' +FROM license_class_templates +WHERE code = 'C+CE'; + +INSERT INTO license_template_requirements (template_id, requirement_key, label, required_units, sort_order, phase_label) +SELECT id, 'CE::highway', 'Autobahn', 3, 102, 'CE' +FROM license_class_templates +WHERE code = 'C+CE'; + +INSERT INTO license_template_requirements (template_id, requirement_key, label, required_units, sort_order, phase_label) +SELECT id, 'CE::night', 'Nachtfahrt', 2, 103, 'CE' +FROM license_class_templates +WHERE code = 'C+CE'; + +INSERT INTO license_template_requirements (template_id, requirement_key, label, required_units, sort_order, phase_label) +SELECT id, 'C1::practice', 'Uebungsstunden', 6, 0, 'C1' +FROM license_class_templates +WHERE code = 'C1+C1E'; + +INSERT INTO license_template_requirements (template_id, requirement_key, label, required_units, sort_order, phase_label) +SELECT id, 'C1::rural', 'Ueberland', 4, 1, 'C1' +FROM license_class_templates +WHERE code = 'C1+C1E'; + +INSERT INTO license_template_requirements (template_id, requirement_key, label, required_units, sort_order, phase_label) +SELECT id, 'C1::highway', 'Autobahn', 3, 2, 'C1' +FROM license_class_templates +WHERE code = 'C1+C1E'; + +INSERT INTO license_template_requirements (template_id, requirement_key, label, required_units, sort_order, phase_label) +SELECT id, 'C1::night', 'Nachtfahrt', 2, 3, 'C1' +FROM license_class_templates +WHERE code = 'C1+C1E'; + +INSERT INTO license_template_requirements (template_id, requirement_key, label, required_units, sort_order, phase_label) +SELECT id, 'C1E::practice', 'Uebungsstunden', 4, 100, 'C1E' +FROM license_class_templates +WHERE code = 'C1+C1E'; + +INSERT INTO license_template_requirements (template_id, requirement_key, label, required_units, sort_order, phase_label) +SELECT id, 'C1E::rural', 'Ueberland', 2, 101, 'C1E' +FROM license_class_templates +WHERE code = 'C1+C1E'; + +INSERT INTO license_template_requirements (template_id, requirement_key, label, required_units, sort_order, phase_label) +SELECT id, 'C1E::highway', 'Autobahn', 2, 102, 'C1E' +FROM license_class_templates +WHERE code = 'C1+C1E'; + +INSERT INTO license_template_requirements (template_id, requirement_key, label, required_units, sort_order, phase_label) +SELECT id, 'C1E::night', 'Nachtfahrt', 1, 103, 'C1E' +FROM license_class_templates +WHERE code = 'C1+C1E'; + +UPDATE appointment_units +SET requirement_key = 'C::' || requirement_key +WHERE requirement_key IN ('practice', 'rural', 'highway', 'night') + AND appointment_id IN ( + SELECT a.id + FROM appointments a + JOIN students s ON s.id = a.student_id + JOIN license_class_templates t ON t.id = s.template_id + WHERE t.code = 'C+CE' + ); + +UPDATE appointment_units +SET requirement_key = 'C1::' || requirement_key +WHERE requirement_key IN ('practice', 'rural', 'highway', 'night') + AND appointment_id IN ( + SELECT a.id + FROM appointments a + JOIN students s ON s.id = a.student_id + JOIN license_class_templates t ON t.id = s.template_id + WHERE t.code = 'C1+C1E' + ); + +DELETE FROM student_requirement_snapshots +WHERE student_id IN ( + SELECT s.id + FROM students s + JOIN license_class_templates t ON t.id = s.template_id + WHERE t.code IN ('C+CE', 'C1+C1E') +); + +INSERT INTO student_requirement_snapshots (student_id, requirement_key, label, required_units, sort_order, prior_completed_units, phase_label) +SELECT + s.id, + r.requirement_key, + r.label, + r.required_units, + r.sort_order, + CASE + WHEN r.requirement_key = 'C::practice' THEN COALESCE(( + SELECT prior_completed_units + FROM student_requirement_snapshots_old old + WHERE old.student_id = s.id AND old.requirement_key = 'practice' + ), 0) + WHEN r.requirement_key = 'C::rural' THEN COALESCE(( + SELECT prior_completed_units + FROM student_requirement_snapshots_old old + WHERE old.student_id = s.id AND old.requirement_key = 'rural' + ), 0) + WHEN r.requirement_key = 'C::highway' THEN COALESCE(( + SELECT prior_completed_units + FROM student_requirement_snapshots_old old + WHERE old.student_id = s.id AND old.requirement_key = 'highway' + ), 0) + WHEN r.requirement_key = 'C::night' THEN COALESCE(( + SELECT prior_completed_units + FROM student_requirement_snapshots_old old + WHERE old.student_id = s.id AND old.requirement_key = 'night' + ), 0) + WHEN r.requirement_key = 'C1::practice' THEN COALESCE(( + SELECT prior_completed_units + FROM student_requirement_snapshots_old old + WHERE old.student_id = s.id AND old.requirement_key = 'practice' + ), 0) + WHEN r.requirement_key = 'C1::rural' THEN COALESCE(( + SELECT prior_completed_units + FROM student_requirement_snapshots_old old + WHERE old.student_id = s.id AND old.requirement_key = 'rural' + ), 0) + WHEN r.requirement_key = 'C1::highway' THEN COALESCE(( + SELECT prior_completed_units + FROM student_requirement_snapshots_old old + WHERE old.student_id = s.id AND old.requirement_key = 'highway' + ), 0) + WHEN r.requirement_key = 'C1::night' THEN COALESCE(( + SELECT prior_completed_units + FROM student_requirement_snapshots_old old + WHERE old.student_id = s.id AND old.requirement_key = 'night' + ), 0) + ELSE 0 + END, + r.phase_label +FROM students s +JOIN license_class_templates t ON t.id = s.template_id +JOIN license_template_requirements r ON r.template_id = t.id +WHERE t.code IN ('C+CE', 'C1+C1E'); + +DROP TABLE IF EXISTS student_requirement_snapshots_old; diff --git a/backup/old_api_20260520/database/migrations/013_combo_generic_key_cleanup.sql b/backup/old_api_20260520/database/migrations/013_combo_generic_key_cleanup.sql new file mode 100644 index 0000000..2cc654a --- /dev/null +++ b/backup/old_api_20260520/database/migrations/013_combo_generic_key_cleanup.sql @@ -0,0 +1,42 @@ +UPDATE appointment_units +SET requirement_key = COALESCE( + ( + SELECT r.requirement_key + FROM appointments a + JOIN students s ON s.id = a.student_id + JOIN license_class_templates t ON t.id = s.template_id + JOIN license_template_requirements r ON r.template_id = t.id + WHERE a.id = appointment_units.appointment_id + AND t.is_combination = 1 + AND COALESCE(r.phase_label, '') <> '' + AND substr(r.requirement_key, instr(r.requirement_key, '::') + 2) = appointment_units.requirement_key + ORDER BY r.sort_order + LIMIT 1 + ), + requirement_key +) +WHERE requirement_key NOT LIKE '%::%' + AND appointment_id IN ( + SELECT a.id + FROM appointments a + JOIN students s ON s.id = a.student_id + JOIN license_class_templates t ON t.id = s.template_id + WHERE t.is_combination = 1 + ); + +DELETE FROM student_requirement_snapshots +WHERE requirement_key NOT LIKE '%::%' + AND student_id IN ( + SELECT s.id + FROM students s + JOIN license_class_templates t ON t.id = s.template_id + WHERE t.is_combination = 1 + ) + AND EXISTS ( + SELECT 1 + FROM students s + JOIN license_template_requirements r ON r.template_id = s.template_id + WHERE s.id = student_requirement_snapshots.student_id + AND COALESCE(r.phase_label, '') <> '' + AND substr(r.requirement_key, instr(r.requirement_key, '::') + 2) = student_requirement_snapshots.requirement_key + ); diff --git a/backup/old_api_20260520/database/migrations/014_legacy_student_key_aliases_to_lesson_type_keys.sql b/backup/old_api_20260520/database/migrations/014_legacy_student_key_aliases_to_lesson_type_keys.sql new file mode 100644 index 0000000..94a52ef --- /dev/null +++ b/backup/old_api_20260520/database/migrations/014_legacy_student_key_aliases_to_lesson_type_keys.sql @@ -0,0 +1,170 @@ +UPDATE appointment_units +SET requirement_key = COALESCE(( + SELECT 'lesson_type_' || lt.id + FROM appointments a + JOIN students s ON s.id = a.student_id + JOIN lesson_type_template_visibility vis ON vis.template_id = s.template_id + JOIN lesson_types lt ON lt.id = vis.lesson_type_id + WHERE a.id = appointment_units.appointment_id + AND lt.category = 'student' + AND lt.is_counted = 1 + AND ( + (appointment_units.requirement_key = 'intro' AND lt.name = 'Einweisung') + OR (appointment_units.requirement_key = 'practice' AND lt.name IN ('Uebungsstunde', 'Übungsstunde', 'Uebungsstunden', 'Übungsstunden')) + OR (appointment_units.requirement_key = 'rural' AND lt.name IN ('Ueberlandfahrt', 'Überlandfahrt', 'Bundes- / Landstrasse')) + OR (appointment_units.requirement_key = 'highway' AND lt.name IN ('Autobahn', 'Autobahnfahrt')) + OR (appointment_units.requirement_key = 'night' AND lt.name IN ('Nachtfahrt', 'Nachtfahrten', 'Daemmerung / Dunkelheit')) + ) + ORDER BY lt.sort_order, lt.id + LIMIT 1 +), requirement_key) +WHERE requirement_key IN ('intro', 'practice', 'rural', 'highway', 'night'); + +UPDATE student_requirement_snapshots AS target +SET prior_completed_units = prior_completed_units + COALESCE(( + SELECT SUM(src.prior_completed_units) + FROM student_requirement_snapshots src + WHERE src.student_id = target.student_id + AND src.requirement_key = 'intro' +), 0) +WHERE target.requirement_key = COALESCE(( + SELECT 'lesson_type_' || lt.id + FROM students s + JOIN lesson_type_template_visibility vis ON vis.template_id = s.template_id + JOIN lesson_types lt ON lt.id = vis.lesson_type_id + WHERE s.id = target.student_id + AND lt.category = 'student' + AND lt.is_counted = 1 + AND lt.name = 'Einweisung' + ORDER BY lt.sort_order, lt.id + LIMIT 1 +), target.requirement_key) + AND EXISTS ( + SELECT 1 + FROM student_requirement_snapshots src + WHERE src.student_id = target.student_id + AND src.requirement_key = 'intro' + ); + +UPDATE student_requirement_snapshots AS target +SET prior_completed_units = prior_completed_units + COALESCE(( + SELECT SUM(src.prior_completed_units) + FROM student_requirement_snapshots src + WHERE src.student_id = target.student_id + AND src.requirement_key = 'practice' +), 0) +WHERE target.requirement_key = COALESCE(( + SELECT 'lesson_type_' || lt.id + FROM students s + JOIN lesson_type_template_visibility vis ON vis.template_id = s.template_id + JOIN lesson_types lt ON lt.id = vis.lesson_type_id + WHERE s.id = target.student_id + AND lt.category = 'student' + AND lt.is_counted = 1 + AND lt.name IN ('Uebungsstunde', 'Übungsstunde', 'Uebungsstunden', 'Übungsstunden') + ORDER BY lt.sort_order, lt.id + LIMIT 1 +), target.requirement_key) + AND EXISTS ( + SELECT 1 + FROM student_requirement_snapshots src + WHERE src.student_id = target.student_id + AND src.requirement_key = 'practice' + ); + +UPDATE student_requirement_snapshots AS target +SET prior_completed_units = prior_completed_units + COALESCE(( + SELECT SUM(src.prior_completed_units) + FROM student_requirement_snapshots src + WHERE src.student_id = target.student_id + AND src.requirement_key = 'rural' +), 0) +WHERE target.requirement_key = COALESCE(( + SELECT 'lesson_type_' || lt.id + FROM students s + JOIN lesson_type_template_visibility vis ON vis.template_id = s.template_id + JOIN lesson_types lt ON lt.id = vis.lesson_type_id + WHERE s.id = target.student_id + AND lt.category = 'student' + AND lt.is_counted = 1 + AND lt.name IN ('Ueberlandfahrt', 'Überlandfahrt', 'Bundes- / Landstrasse') + ORDER BY lt.sort_order, lt.id + LIMIT 1 +), target.requirement_key) + AND EXISTS ( + SELECT 1 + FROM student_requirement_snapshots src + WHERE src.student_id = target.student_id + AND src.requirement_key = 'rural' + ); + +UPDATE student_requirement_snapshots AS target +SET prior_completed_units = prior_completed_units + COALESCE(( + SELECT SUM(src.prior_completed_units) + FROM student_requirement_snapshots src + WHERE src.student_id = target.student_id + AND src.requirement_key = 'highway' +), 0) +WHERE target.requirement_key = COALESCE(( + SELECT 'lesson_type_' || lt.id + FROM students s + JOIN lesson_type_template_visibility vis ON vis.template_id = s.template_id + JOIN lesson_types lt ON lt.id = vis.lesson_type_id + WHERE s.id = target.student_id + AND lt.category = 'student' + AND lt.is_counted = 1 + AND lt.name IN ('Autobahn', 'Autobahnfahrt') + ORDER BY lt.sort_order, lt.id + LIMIT 1 +), target.requirement_key) + AND EXISTS ( + SELECT 1 + FROM student_requirement_snapshots src + WHERE src.student_id = target.student_id + AND src.requirement_key = 'highway' + ); + +UPDATE student_requirement_snapshots AS target +SET prior_completed_units = prior_completed_units + COALESCE(( + SELECT SUM(src.prior_completed_units) + FROM student_requirement_snapshots src + WHERE src.student_id = target.student_id + AND src.requirement_key = 'night' +), 0) +WHERE target.requirement_key = COALESCE(( + SELECT 'lesson_type_' || lt.id + FROM students s + JOIN lesson_type_template_visibility vis ON vis.template_id = s.template_id + JOIN lesson_types lt ON lt.id = vis.lesson_type_id + WHERE s.id = target.student_id + AND lt.category = 'student' + AND lt.is_counted = 1 + AND lt.name IN ('Nachtfahrt', 'Nachtfahrten', 'Daemmerung / Dunkelheit') + ORDER BY lt.sort_order, lt.id + LIMIT 1 +), target.requirement_key) + AND EXISTS ( + SELECT 1 + FROM student_requirement_snapshots src + WHERE src.student_id = target.student_id + AND src.requirement_key = 'night' + ); + +DELETE FROM student_requirement_snapshots +WHERE requirement_key IN ('intro', 'practice', 'rural', 'highway', 'night') + AND EXISTS ( + SELECT 1 + FROM students s + JOIN lesson_type_template_visibility vis ON vis.template_id = s.template_id + JOIN lesson_types lt ON lt.id = vis.lesson_type_id + WHERE s.id = student_requirement_snapshots.student_id + AND lt.category = 'student' + AND lt.is_counted = 1 + AND ( + (student_requirement_snapshots.requirement_key = 'intro' AND lt.name = 'Einweisung') + OR (student_requirement_snapshots.requirement_key = 'practice' AND lt.name IN ('Uebungsstunde', 'Übungsstunde', 'Uebungsstunden', 'Übungsstunden')) + OR (student_requirement_snapshots.requirement_key = 'rural' AND lt.name IN ('Ueberlandfahrt', 'Überlandfahrt', 'Bundes- / Landstrasse')) + OR (student_requirement_snapshots.requirement_key = 'highway' AND lt.name IN ('Autobahn', 'Autobahnfahrt')) + OR (student_requirement_snapshots.requirement_key = 'night' AND lt.name IN ('Nachtfahrt', 'Nachtfahrten', 'Daemmerung / Dunkelheit')) + ) + ); diff --git a/backup/old_api_20260520/database/migrations/015_instructor_pre_start_buffer.sql b/backup/old_api_20260520/database/migrations/015_instructor_pre_start_buffer.sql new file mode 100644 index 0000000..60c1107 --- /dev/null +++ b/backup/old_api_20260520/database/migrations/015_instructor_pre_start_buffer.sql @@ -0,0 +1 @@ +ALTER TABLE instructors ADD COLUMN pre_start_buffer_minutes INTEGER NOT NULL DEFAULT 0; diff --git a/backup/old_api_20260520/database/migrations/015_lesson_type_allows_student_and_notes_private.sql b/backup/old_api_20260520/database/migrations/015_lesson_type_allows_student_and_notes_private.sql new file mode 100644 index 0000000..6954809 --- /dev/null +++ b/backup/old_api_20260520/database/migrations/015_lesson_type_allows_student_and_notes_private.sql @@ -0,0 +1,22 @@ +-- Migration: 015_lesson_type_allows_student_and_notes_private.sql +-- Adds allows_student column to lesson_types and notes_private to appointments + +-- 1. lesson_types.allows_student: which lesson types can be assigned a student +ALTER TABLE lesson_types ADD COLUMN IF NOT EXISTS allows_student integer NOT NULL DEFAULT 0; +COMMENT ON COLUMN lesson_types.allows_student IS '1 = lesson type can have a student_id (e.g. driving lessons), 0 = no student (theory, work, private)'; + +-- 2. appointments.notes_private: private note only visible to the instructor (not chef/office) +ALTER TABLE appointments ADD COLUMN IF NOT EXISTS notes_private text DEFAULT ''; + +-- Default: only 'student' category allows student_id, all other categories = 0 +UPDATE lesson_types SET allows_student = 1 WHERE category = 'student'; +UPDATE lesson_types SET allows_student = 0 WHERE category != 'student'; + +-- For existing appointments, clear student_id if the lesson_type doesn't allow it +-- (data cleanup - not normally needed but defensive) +UPDATE appointments +SET student_id = NULL +WHERE student_id IS NOT NULL + AND lesson_type_id IN ( + SELECT id FROM lesson_types WHERE allows_student = 0 + ); \ No newline at end of file diff --git a/backup/old_api_20260520/database/migrations/016_consolidate_lesson_types.sql b/backup/old_api_20260520/database/migrations/016_consolidate_lesson_types.sql new file mode 100644 index 0000000..5d4334f --- /dev/null +++ b/backup/old_api_20260520/database/migrations/016_consolidate_lesson_types.sql @@ -0,0 +1,39 @@ +-- Migration: 016_consolidate_lesson_types.sql +-- Reihen: tenant 1 und tenant 2 bereinigen, konsistente Einstellungen + +-- ══════════════════════════════════════════════════════ +-- TENANT 1 +-- ══════════════════════════════════════════════════════ + +-- Tenant 1: Doppelte Einweisung (id=1, id=14 → id=14 renamed to avoid conflict via rename approach) +-- Already unique (no action needed) + +-- Tenant 1: Doppelte Übungsstunde (id=2, id=15 → id=15 is in tenant 1 but was already unique) +-- Actually tenant 1 has only id=2 (Uebungsstunde) - no duplicate + +-- Tenant 1 work type fixes: +-- Sonstige Arbeiten (id=8): is_rest_relevant=1 (zählt für Ruhezeit) +UPDATE lesson_types SET is_rest_relevant = 1, fixed_duration = 0 WHERE id = 8 AND tenant_id = 1; + +-- Theorieunterricht (id=7): is_rest_relevant=1, is_billable=1, fixed_duration=0 +UPDATE lesson_types SET is_rest_relevant = 1, fixed_duration = 0, is_billable = 1 WHERE id = 7 AND tenant_id = 1; + +-- ══════════════════════════════════════════════════════ +-- TENANT 2 +-- ══════════════════════════════════════════════════════ + +-- Tenant 2: Doppelte Namen konsolidieren +-- Autobahnfahrt → Autobahn (id=13 → name='Autobahn') +UPDATE lesson_types SET name = 'Autobahn' WHERE id = 13 AND tenant_id = 2; +-- Übungsstunde (id=15 already named correctly) + +-- Verify tenant 2 student types +-- Note: id=11 Nachtfahrten stays (different from tenant 1 which has no Nachtfahrten) +-- id=16 Praktische Prüfung stays +-- id=12 Überlandfahrt stays + +-- ══════════════════════════════════════════════════════ +-- ALL TENANTS: allows_student defaults +-- ══════════════════════════════════════════════════════ +-- already done in migration 015 +-- student=1, all others=0 \ No newline at end of file diff --git a/backup/old_api_20260520/database/migrations/017_student_booking_schema.sql b/backup/old_api_20260520/database/migrations/017_student_booking_schema.sql new file mode 100644 index 0000000..b3053bf --- /dev/null +++ b/backup/old_api_20260520/database/migrations/017_student_booking_schema.sql @@ -0,0 +1,202 @@ +-- Migration: 017_student_booking_schema.sql +-- Fahrschüler-Buchungssystem + Multi-Tenant Instructor + Break-Rules + +BEGIN; + +-- ══════════════════════════════════════════════════════ +-- 1. users.role ist bereits text (kein ENUM), keine Änderung nötig +-- ══════════════════════════════════════════════════════ + +-- ══════════════════════════════════════════════════════ +-- 2. instructors erweitern: UE-Limits + Booking-Settings +-- ══════════════════════════════════════════════════════ +ALTER TABLE instructors ADD COLUMN IF NOT EXISTS max_daily_units integer DEFAULT NULL; +ALTER TABLE instructors ADD COLUMN IF NOT EXISTS max_daily_override integer DEFAULT 0; +ALTER TABLE instructors ADD COLUMN IF NOT EXISTS max_weekly_units integer DEFAULT NULL; +ALTER TABLE instructors ADD COLUMN IF NOT EXISTS max_weekly_override integer DEFAULT 0; +ALTER TABLE instructors ADD COLUMN IF NOT EXISTS booking_enabled integer DEFAULT 1; + +-- ══════════════════════════════════════════════════════ +-- 3. students erweitern: user_id FK +-- ══════════════════════════════════════════════════════ +ALTER TABLE students ADD COLUMN IF NOT EXISTS user_id bigint REFERENCES users(id) NULL; + +-- ══════════════════════════════════════════════════════ +-- 4. tenants erweitern: free_cancellation_hours +-- ══════════════════════════════════════════════════════ +ALTER TABLE tenants ADD COLUMN IF NOT EXISTS free_cancellation_hours integer DEFAULT 24; + +-- ══════════════════════════════════════════════════════ +-- 5. tenant_registration_codes +-- ══════════════════════════════════════════════════════ +CREATE TABLE IF NOT EXISTS tenant_registration_codes ( + id bigserial PRIMARY KEY, + tenant_id bigint NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + code text NOT NULL UNIQUE, + is_active integer DEFAULT 1, + used_count integer DEFAULT 0, + created_at timestamptz DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_tenant_reg_codes_code ON tenant_registration_codes(code); +CREATE INDEX IF NOT EXISTS idx_tenant_reg_codes_tenant ON tenant_registration_codes(tenant_id); + +-- ══════════════════════════════════════════════════════ +-- 6. student_tenants +-- ══════════════════════════════════════════════════════ +CREATE TABLE IF NOT EXISTS student_tenants ( + id bigserial PRIMARY KEY, + student_user_id bigint NOT NULL REFERENCES users(id) ON DELETE CASCADE, + tenant_id bigint NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'active', 'suspended')), + registered_at timestamptz DEFAULT NOW(), + confirmed_at timestamptz NULL, + confirmed_by bigint NULL REFERENCES users(id), + UNIQUE(student_user_id, tenant_id) +); +CREATE INDEX IF NOT EXISTS idx_student_tenants_user ON student_tenants(student_user_id); +CREATE INDEX IF NOT EXISTS idx_student_tenants_tenant ON student_tenants(tenant_id); + +-- ══════════════════════════════════════════════════════ +-- 7. instructor_availability +-- ══════════════════════════════════════════════════════ +CREATE TABLE IF NOT EXISTS instructor_availability ( + id bigserial PRIMARY KEY, + instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE, + weekday integer NOT NULL CHECK (weekday BETWEEN 0 AND 6), + time_from text NOT NULL, + time_to text NOT NULL, + is_active integer DEFAULT 1, + UNIQUE(instructor_id, weekday, time_from) +); +CREATE INDEX IF NOT EXISTS idx_instructor_avail_instructor ON instructor_availability(instructor_id); + +-- ══════════════════════════════════════════════════════ +-- 8. appointment_requests +-- ══════════════════════════════════════════════════════ +CREATE TABLE IF NOT EXISTS appointment_requests ( + id bigserial PRIMARY KEY, + student_user_id bigint NOT NULL REFERENCES users(id) ON DELETE CASCADE, + instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE, + lesson_type_id bigint NOT NULL REFERENCES lesson_types(id), + requested_start timestamptz NOT NULL, + requested_end timestamptz NOT NULL, + status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'rejected', 'cancelled')), + requested_at timestamptz DEFAULT NOW(), + responded_at timestamptz NULL, + response_by bigint NULL REFERENCES users(id), + notes text DEFAULT '', + confirmed_appointment_id bigint NULL REFERENCES appointments(id) +); +CREATE INDEX IF NOT EXISTS idx_appt_req_student ON appointment_requests(student_user_id); +CREATE INDEX IF NOT EXISTS idx_appt_req_instructor ON appointment_requests(instructor_id); +CREATE INDEX IF NOT EXISTS idx_appt_req_status ON appointment_requests(status); + +-- ══════════════════════════════════════════════════════ +-- 9. instructor_vacations +-- ══════════════════════════════════════════════════════ +CREATE TABLE IF NOT EXISTS instructor_vacations ( + id bigserial PRIMARY KEY, + instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE, + date_from date NOT NULL, + date_to date NOT NULL, + notes text DEFAULT '', + created_at timestamptz DEFAULT NOW(), + UNIQUE(instructor_id, date_from, date_to) +); +CREATE INDEX IF NOT EXISTS idx_instructor_vacations_instructor ON instructor_vacations(instructor_id); +CREATE INDEX IF NOT EXISTS idx_instructor_vacations_dates ON instructor_vacations(date_from, date_to); + +-- ══════════════════════════════════════════════════════ +-- 10. private_appointments +-- ══════════════════════════════════════════════════════ +CREATE TABLE IF NOT EXISTS private_appointments ( + id bigserial PRIMARY KEY, + instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE, + title text NOT NULL DEFAULT 'Privat', + date date NOT NULL, + time_from text NOT NULL, + time_to text NOT NULL, + color text DEFAULT '#9ca3af', + created_at timestamptz DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_private_appts_instructor ON private_appointments(instructor_id); +CREATE INDEX IF NOT EXISTS idx_private_appts_date ON private_appointments(date); + +-- ══════════════════════════════════════════════════════ +-- 11. instructor_tenants +-- ══════════════════════════════════════════════════════ +CREATE TABLE IF NOT EXISTS instructor_tenants ( + id bigserial PRIMARY KEY, + instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE, + tenant_id bigint NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + is_primary integer DEFAULT 0, + created_at timestamptz DEFAULT NOW(), + UNIQUE(instructor_id, tenant_id) +); +CREATE INDEX IF NOT EXISTS idx_instructor_tenants_instructor ON instructor_tenants(instructor_id); +CREATE INDEX IF NOT EXISTS idx_instructor_tenants_tenant ON instructor_tenants(tenant_id); + +-- ══════════════════════════════════════════════════════ +-- 12. instructor_travel_times +-- ══════════════════════════════════════════════════════ +CREATE TABLE IF NOT EXISTS instructor_travel_times ( + id bigserial PRIMARY KEY, + instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE, + from_tenant_id bigint NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + to_tenant_id bigint NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + minutes integer NOT NULL, + UNIQUE(instructor_id, from_tenant_id, to_tenant_id) +); +CREATE INDEX IF NOT EXISTS idx_travel_times_instructor ON instructor_travel_times(instructor_id); + +-- ══════════════════════════════════════════════════════ +-- 13. instructor_home_to_tenant +-- ══════════════════════════════════════════════════════ +CREATE TABLE IF NOT EXISTS instructor_home_to_tenant ( + id bigserial PRIMARY KEY, + instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE, + tenant_id bigint NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + minutes integer NOT NULL, + UNIQUE(instructor_id, tenant_id) +); +CREATE INDEX IF NOT EXISTS idx_home_to_tenant_instructor ON instructor_home_to_tenant(instructor_id); + +-- ══════════════════════════════════════════════════════ +-- 14. instructor_break_rules +-- ══════════════════════════════════════════════════════ +CREATE TABLE IF NOT EXISTS instructor_break_rules ( + id bigserial PRIMARY KEY, + instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE, + condition_type text NOT NULL CHECK (condition_type IN ('daily_units', 'weekly_units', 'hours_daily', 'between_appointments', 'continuous_minutes')), + operator text NOT NULL CHECK (operator IN ('greater_than', 'equals', 'at_least')), + threshold numeric NOT NULL, + break_minutes integer NOT NULL, + is_active integer DEFAULT 1, + created_at timestamptz DEFAULT NOW(), + UNIQUE(instructor_id, condition_type, operator, threshold) +); +CREATE INDEX IF NOT EXISTS idx_break_rules_instructor ON instructor_break_rules(instructor_id); + +-- ══════════════════════════════════════════════════════ +-- 15. Default Registration-Codes für bestehende Tenants +-- ══════════════════════════════════════════════════════ +INSERT INTO tenant_registration_codes (tenant_id, code, is_active) +SELECT id, LPAD(id::text, 8, '0') || '-' || UPPER(SUBSTRING(MD5(id::text || 'salt') FROM 1 FOR 4)), 1 +FROM tenants +WHERE id NOT IN (SELECT tenant_id FROM tenant_registration_codes) +ON CONFLICT (code) DO NOTHING; + +-- ══════════════════════════════════════════════════════ +-- 16. Default Availability für alle Instructors (Mo-So 08:00-19:00) +-- ══════════════════════════════════════════════════════ +INSERT INTO instructor_availability (instructor_id, weekday, time_from, time_to, is_active) +SELECT i.id, w.day, '08:00', '19:00', 1 +FROM instructors i +CROSS JOIN (VALUES (0),(1),(2),(3),(4),(5),(6)) AS w(day) +WHERE i.is_active = 1 +AND NOT EXISTS ( + SELECT 1 FROM instructor_availability ia + WHERE ia.instructor_id = i.id AND ia.weekday = w.day +); + +COMMIT; \ No newline at end of file diff --git a/backup/old_api_20260520/database/schema_pgsql.sql b/backup/old_api_20260520/database/schema_pgsql.sql new file mode 100644 index 0000000..34204d1 --- /dev/null +++ b/backup/old_api_20260520/database/schema_pgsql.sql @@ -0,0 +1,166 @@ +-- PostgreSQL Schema for fahrschultermin.de +-- Run this BEFORE importing data + +BEGIN; + +-- ── Tenants ─────────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS tenants ( + id BIGSERIAL PRIMARY KEY, + name TEXT NOT NULL, + timezone TEXT NOT NULL DEFAULT 'Europe/Berlin', + is_active INTEGER NOT NULL DEFAULT 1, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + location_label TEXT DEFAULT '', + latitude DOUBLE PRECISION DEFAULT NULL, + longitude DOUBLE PRECISION DEFAULT NULL, + federal_state TEXT DEFAULT 'BE' +); + +-- ── Users ───────────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS users ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT REFERENCES tenants(id) ON DELETE CASCADE, + role TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + is_active INTEGER NOT NULL DEFAULT 1, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL +); + +-- ── Instructors ──────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS instructors ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + color TEXT NOT NULL, + notes TEXT DEFAULT '', + is_active INTEGER NOT NULL DEFAULT 1, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + user_id BIGINT REFERENCES users(id) ON DELETE SET NULL, + pre_start_buffer_minutes INTEGER NOT NULL DEFAULT 0 +); + +-- ── License Class Templates ─────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS license_class_templates ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + code TEXT NOT NULL, + name TEXT NOT NULL, + is_combination INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + UNIQUE (tenant_id, code) +); + +-- ── License Template Requirements ───────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS license_template_requirements ( + id BIGSERIAL PRIMARY KEY, + template_id BIGINT NOT NULL REFERENCES license_class_templates(id) ON DELETE CASCADE, + requirement_key TEXT NOT NULL, + label TEXT NOT NULL, + required_units INTEGER NOT NULL, + sort_order INTEGER NOT NULL DEFAULT 0, + phase_label TEXT NOT NULL DEFAULT '' +); + +-- ── Lesson Types ────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS lesson_types ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + name TEXT NOT NULL, + color TEXT NOT NULL, + default_duration INTEGER NOT NULL, + category TEXT NOT NULL, + requirement_key TEXT DEFAULT NULL, + is_billable INTEGER NOT NULL DEFAULT 0, + is_counted INTEGER NOT NULL DEFAULT 0, + is_rest_relevant INTEGER NOT NULL DEFAULT 0, + fixed_duration INTEGER NOT NULL DEFAULT 0, + sort_order INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + system_key TEXT DEFAULT NULL +); + +-- ── Lesson Type ↔ Template Visibility ────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS lesson_type_template_visibility ( + lesson_type_id BIGINT NOT NULL REFERENCES lesson_types(id) ON DELETE CASCADE, + template_id BIGINT NOT NULL REFERENCES license_class_templates(id) ON DELETE CASCADE, + PRIMARY KEY (lesson_type_id, template_id) +); + +-- ── Students ────────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS students ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + template_id BIGINT NOT NULL REFERENCES license_class_templates(id), + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + phone TEXT DEFAULT '', + notes TEXT DEFAULT '', + status TEXT NOT NULL DEFAULT 'active', + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + email TEXT NOT NULL DEFAULT '', + needs_glasses INTEGER NOT NULL DEFAULT 0 +); + +-- ── Student Requirement Snapshots ────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS student_requirement_snapshots ( + id BIGSERIAL PRIMARY KEY, + student_id BIGINT NOT NULL REFERENCES students(id) ON DELETE CASCADE, + requirement_key TEXT NOT NULL, + label TEXT NOT NULL, + required_units INTEGER NOT NULL, + sort_order INTEGER NOT NULL DEFAULT 0, + prior_completed_units INTEGER NOT NULL DEFAULT 0, + phase_label TEXT NOT NULL DEFAULT '' +); + +-- ── Appointments ────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS appointments ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + student_id BIGINT REFERENCES students(id) ON DELETE SET NULL, + instructor_id BIGINT NOT NULL REFERENCES instructors(id), + lesson_type_id BIGINT NOT NULL REFERENCES lesson_types(id), + title TEXT NOT NULL, + category TEXT NOT NULL, + start_at TIMESTAMPTZ NOT NULL, + end_at TIMESTAMPTZ NOT NULL, + units INTEGER NOT NULL DEFAULT 1, + status TEXT NOT NULL DEFAULT 'planned', + notes TEXT DEFAULT '', + warning_acknowledged INTEGER NOT NULL DEFAULT 0, + created_by BIGINT NOT NULL REFERENCES users(id), + updated_by BIGINT NOT NULL REFERENCES users(id), + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL +); + +-- ── Appointment Units ───────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS appointment_units ( + id BIGSERIAL PRIMARY KEY, + appointment_id BIGINT NOT NULL REFERENCES appointments(id) ON DELETE CASCADE, + requirement_key TEXT DEFAULT NULL, + label TEXT DEFAULT NULL, + units_counted INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL +); + +-- ── Settings ─────────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS settings ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + key_name TEXT NOT NULL, + value TEXT NOT NULL, + UNIQUE (tenant_id, key_name) +); + +COMMIT; \ No newline at end of file