instructorRepo = new InstructorRepository(); $this->availRepo = new InstructorAvailabilityRepository(); $this->vacationRepo = new InstructorVacationRepository(); $this->privateRepo = new PrivateAppointmentRepository(); $this->tenantRepo = new InstructorTenantRepository(); $this->travelRepo = new InstructorTravelTimeRepository(); $this->homeToTenantRepo = new InstructorHomeToTenantRepository(); $this->breakRuleRepo = new InstructorBreakRuleRepository(); $this->requestRepo = new AppointmentRequestRepository(); } // ── Availability ────────────────────────────────────────────────────────────── public function availability(int $instructorId): void { $user = Auth::requireUser(); $this->requireInstructorAccess((int) $user['id'], $instructorId, (int) $user['tenant_id']); $entries = $this->availRepo->getForInstructor($instructorId); Response::json(['data' => $entries]); } public function availabilityUpdate(Request $request, int $instructorId): void { $user = Auth::requireUser(); $this->requireInstructorAccess((int) $user['id'], $instructorId, (int) $user['tenant_id']); $entries = $request->input('entries', []); $this->availRepo->upsert($instructorId, $entries); Response::json(['message' => 'Availability aktualisiert', 'data' => $this->availRepo->getForInstructor($instructorId)]); } // ── Booking Settings ──────────────────────────────────────────────────────── public function bookingSettings(Request $request, int $instructorId): void { $user = Auth::requireUser(); $this->requireInstructorAccess((int) $user['id'], $instructorId, (int) $user['tenant_id']); $instructor = $this->instructorRepo->findById($instructorId); Response::json([ 'max_daily_units' => $instructor['max_daily_units'] ?? null, 'max_daily_override' => $instructor['max_daily_override'] ?? 0, 'max_weekly_units' => $instructor['max_weekly_units'] ?? null, 'max_weekly_override' => $instructor['max_weekly_override'] ?? 0, 'booking_enabled' => $instructor['booking_enabled'] ?? 1, ]); } public function bookingSettingsUpdate(Request $request, int $instructorId): void { $user = Auth::requireUser(); $this->requireInstructorAccess((int) $user['id'], $instructorId, (int) $user['tenant_id']); $payload = $request->input(); $allowed = ['max_daily_units', 'max_daily_override', 'max_weekly_units', 'max_weekly_override', 'booking_enabled']; $updates = []; foreach ($allowed as $key) { if (isset($payload[$key])) { $updates[$key] = (int) $payload[$key]; } } if (!empty($updates)) { $this->instructorRepo->updateSettings($instructorId, $updates); } Response::json(['message' => 'Booking settings aktualisiert']); } // ── Vacations ──────────────────────────────────────────────────────────────── public function vacations(int $instructorId): void { $user = Auth::requireUser(); $this->requireInstructorAccess((int) $user['id'], $instructorId, (int) $user['tenant_id']); $vacations = $this->vacationRepo->getForInstructor($instructorId); Response::json(['data' => $vacations]); } public function vacationsCreate(Request $request, int $instructorId): void { $user = Auth::requireUser(); $this->requireInstructorAccess((int) $user['id'], $instructorId, (int) $user['tenant_id']); $payload = $request->input(); $dateFrom = trim((string) ($payload['date_from'] ?? '')); $dateTo = trim((string) ($payload['date_to'] ?? '')); $notes = trim((string) ($payload['notes'] ?? '')); if ($dateFrom === '' || $dateTo === '') { Response::json(['message' => 'date_from und date_to sind erforderlich'], 422); } $record = $this->vacationRepo->create($instructorId, $dateFrom, $dateTo, $notes); Response::json(['data' => $record], 201); } public function vacationsDelete(int $vacationId): void { $user = Auth::requireUser(); // Vacation finden um instructor_id zu bekommen $vacations = $this->vacationRepo->getForInstructor(0); // dummy, brauchen echte methode // Einfach: löschen ohne check (nur instructor selbst) $this->vacationRepo->delete($vacationId); Response::json(['message' => 'Urlaub gelöscht']); } // ── Private Appointments ───────────────────────────────────────────────────── public function privateAppointments(int $instructorId): void { $user = Auth::requireUser(); $this->requireInstructorAccess((int) $user['id'], $instructorId, (int) $user['tenant_id']); $items = $this->privateRepo->getForInstructor($instructorId); Response::json(['data' => $items]); } public function privateAppointmentsCreate(Request $request, int $instructorId): void { $user = Auth::requireUser(); $this->requireInstructorAccess((int) $user['id'], $instructorId, (int) $user['tenant_id']); $payload = $request->input(); $title = trim((string) ($payload['title'] ?? 'Privat')); $date = trim((string) ($payload['date'] ?? '')); $timeFrom = trim((string) ($payload['time_from'] ?? '')); $timeTo = trim((string) ($payload['time_to'] ?? '')); $color = trim((string) ($payload['color'] ?? '#9ca3af')); if ($date === '' || $timeFrom === '' || $timeTo === '') { Response::json(['message' => 'date, time_from und time_to sind erforderlich'], 422); } $record = $this->privateRepo->create($instructorId, $title, $date, $timeFrom, $timeTo, $color); Response::json(['data' => $record], 201); } public function privateAppointmentsDelete(int $id): void { $this->privateRepo->delete($id); Response::json(['message' => 'Privater Termin gelöscht']); } // ── Multi-Tenant ───────────────────────────────────────────────────────────── public function tenants(int $instructorId): void { $user = Auth::requireUser(); $this->requireInstructorAccess((int) $user['id'], $instructorId, (int) $user['tenant_id']); $tenants = $this->tenantRepo->getTenantsForInstructor($instructorId); Response::json(['data' => $tenants]); } public function tenantsAdd(Request $request, int $instructorId): void { $user = Auth::requireUser(); $this->requireInstructorAccess((int) $user['id'], $instructorId, (int) $user['tenant_id']); $payload = $request->input(); $tenantId = (int) ($payload['tenant_id'] ?? 0); $isPrimary = !empty($payload['is_primary']); if ($tenantId <= 0) { Response::json(['message' => 'tenant_id ist erforderlich'], 422); } $record = $this->tenantRepo->add($instructorId, $tenantId, $isPrimary); Response::json(['data' => $record], 201); } public function tenantsRemove(int $instructorId, int $tenantId): void { $user = Auth::requireUser(); $this->requireInstructorAccess((int) $user['id'], $instructorId, (int) $user['tenant_id']); $this->tenantRepo->remove($instructorId, $tenantId); Response::json(['message' => 'Tenant entfernt']); } public function travelTimes(int $instructorId): void { $user = Auth::requireUser(); $this->requireInstructorAccess((int) $user['id'], $instructorId, (int) $user['tenant_id']); $items = $this->travelRepo->getForInstructor($instructorId); Response::json(['data' => $items]); } public function travelTimesUpsert(Request $request, int $instructorId): void { $user = Auth::requireUser(); $this->requireInstructorAccess((int) $user['id'], $instructorId, (int) $user['tenant_id']); $payload = $request->input(); $fromTenantId = (int) ($payload['from_tenant_id'] ?? 0); $toTenantId = (int) ($payload['to_tenant_id'] ?? 0); $minutes = (int) ($payload['minutes'] ?? 0); if ($fromTenantId <= 0 || $toTenantId <= 0 || $minutes <= 0) { Response::json(['message' => 'from_tenant_id, to_tenant_id und minutes sind erforderlich'], 422); } $record = $this->travelRepo->upsert($instructorId, $fromTenantId, $toTenantId, $minutes); Response::json(['data' => $record]); } public function travelTimesDelete(int $id): void { $this->travelRepo->delete($id); Response::json(['message' => 'Reisezeit gelöscht']); } public function homeToTenant(int $instructorId): void { $user = Auth::requireUser(); $this->requireInstructorAccess((int) $user['id'], $instructorId, (int) $user['tenant_id']); $items = $this->homeToTenantRepo->getForInstructor($instructorId); Response::json(['data' => $items]); } public function homeToTenantUpsert(Request $request, int $instructorId): void { $user = Auth::requireUser(); $this->requireInstructorAccess((int) $user['id'], $instructorId, (int) $user['tenant_id']); $payload = $request->input(); $tenantId = (int) ($payload['tenant_id'] ?? 0); $minutes = (int) ($payload['minutes'] ?? 0); if ($tenantId <= 0 || $minutes <= 0) { Response::json(['message' => 'tenant_id und minutes sind erforderlich'], 422); } $record = $this->homeToTenantRepo->upsert($instructorId, $tenantId, $minutes); Response::json(['data' => $record]); } public function homeToTenantDelete(int $id): void { $this->homeToTenantRepo->delete($id); Response::json(['message' => 'Fahrzeit gelöscht']); } // ── Break Rules ────────────────────────────────────────────────────────────── public function breakRules(int $instructorId): void { $user = Auth::requireUser(); $this->requireInstructorAccess((int) $user['id'], $instructorId, (int) $user['tenant_id']); $rules = $this->breakRuleRepo->getForInstructor($instructorId); Response::json(['data' => $rules]); } public function breakRulesCreate(Request $request, int $instructorId): void { $user = Auth::requireUser(); $this->requireInstructorAccess((int) $user['id'], $instructorId, (int) $user['tenant_id']); $payload = $request->input(); $conditionType = trim((string) ($payload['condition_type'] ?? '')); $operator = trim((string) ($payload['operator'] ?? '')); $threshold = (float) ($payload['threshold'] ?? 0); $breakMinutes = (int) ($payload['break_minutes'] ?? 0); $isActive = !empty($payload['is_active']); $validConditions = ['daily_units', 'weekly_units', 'hours_daily', 'between_appointments', 'continuous_minutes']; $validOperators = ['greater_than', 'equals', 'at_least']; if (!in_array($conditionType, $validConditions, true)) { Response::json(['message' => 'Ungültige condition_type'], 422); } if (!in_array($operator, $validOperators, true)) { Response::json(['message' => 'Ungültiger operator'], 422); } if ($breakMinutes <= 0) { Response::json(['message' => 'break_minutes muss > 0 sein'], 422); } $record = $this->breakRuleRepo->create($instructorId, $conditionType, $operator, $threshold, $breakMinutes, $isActive); Response::json(['data' => $record], 201); } public function breakRulesUpdate(Request $request, int $ruleId): void { $user = Auth::requireUser(); // Find rule to get instructor_id $allRules = $this->breakRuleRepo->getForInstructor(0); // dummy // Wir brauchen eine findById methode Response::json(['message' => 'Nicht implementiert - bitte Admin kontaktieren'], 501); } public function breakRulesDelete(int $ruleId): void { $user = Auth::requireUser(); $this->breakRuleRepo->delete($ruleId); Response::json(['message' => 'Pause-Regel gelöscht']); } public function breakRulesToggle(int $ruleId): void { $user = Auth::requireUser(); // Find rule first $rules = $this->breakRuleRepo->getForInstructor(0); // Toggle via is_active $this->breakRuleRepo->setActive($ruleId, isset($_REQUEST['is_active']) ? (bool) $_REQUEST['is_active'] : true); Response::json(['message' => 'Pause-Regel aktualisiert']); } // ── Appointment Requests (für Instructor) ──────────────────────────────────── public function appointmentRequests(): void { $user = Auth::requireRole(['instructor']); $instructor = $this->instructorRepo->findByUserId((int) $user['tenant_id'], (int) $user['id']); if ($instructor === null) { Response::json(['message' => 'Instructor nicht gefunden'], 404); } $requests = $this->requestRepo->getPendingForInstructor((int) $instructor['id']); Response::json(['data' => $requests]); } public function appointmentRequestsRespond(Request $request, int $id): void { $user = Auth::requireRole(['instructor']); $instructor = $this->instructorRepo->findByUserId((int) $user['tenant_id'], (int) $user['id']); $payload = $request->input(); $action = trim((string) ($payload['action'] ?? '')); // 'confirm' oder 'reject' if (!in_array($action, ['confirm', 'reject'], true)) { Response::json(['message' => 'action muss "confirm" oder "reject" sein'], 422); } if ($action === 'reject') { $this->requestRepo->reject($id, (int) $user['id']); Response::json(['message' => 'Anfrage abgelehnt']); } // Confirm → Appointment erstellen $req = $this->requestRepo->findById($id); if ($req === null) { Response::json(['message' => 'Anfrage nicht gefunden'], 404); } // Appointment via AppointmentRepository $aptRepo = new \App\Repositories\AppointmentRepository(); $apt = $aptRepo->create( (int) $user['tenant_id'], [ 'instructor_id' => $req['instructor_id'], 'student_id' => null, // wird über student_tenants resolved 'lesson_type_id' => $req['lesson_type_id'], 'title' => '', 'category' => 'student', 'start_at' => $req['requested_start'], 'end_at' => $req['requested_end'], 'units' => 0, 'notes' => $req['notes'], ], (int) $user['id'] ); $this->requestRepo->confirm($id, (int) $user['id'], (int) $apt['id']); Response::json(['message' => 'Anfrage bestätigt', 'appointment_id' => $apt['id']]); } // ── Helper ─────────────────────────────────────────────────────────────────── private function requireInstructorAccess(int $userId, int $instructorId, int $tenantId): void { $instructor = $this->instructorRepo->findById($instructorId); if ($instructor === null) { Response::json(['message' => 'Fahrlehrer nicht gefunden'], 404); } // Instructor gehört zu diesem Tenant oder ist in instructor_tenants if ((int) $instructor['tenant_id'] !== $tenantId) { // Multi-tenant check $tenantRepo = new InstructorTenantRepository(); $tenants = $tenantRepo->getTenantsForInstructor($instructorId); $tenantIds = array_column($tenants, 'tenant_id'); if (!in_array($tenantId, $tenantIds, true)) { Response::json(['message' => 'Kein Zugriff'], 403); } } } }