Problem: Frontend JS bundle used credentials:'same-origin' which dropped cookies on cross-domain requests. Login worked (200) but the subsequent /auth/me check returned 401, leaving the user stuck on the login screen. Fix: - AuthController now sets a dtp_jwt HttpOnly cookie on login/refresh - Cookie uses Domain=.fahrschultermin.de (shared between frontend and api subdomain), Secure, SameSite=Lax, Max-Age=8h - JwtMiddleware reads JWT from Authorization header OR cookie - Added AuthController::me() endpoint (was missing, caused 500) - Logout endpoint clears the cookie - Frontend index.php patches fetch() to use credentials:'include' for all /api/v1/* calls
428 lines
18 KiB
PHP
Executable File
428 lines
18 KiB
PHP
Executable File
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Repositories\InstructorRepository;
|
|
use App\Repositories\InstructorAvailabilityRepository;
|
|
use App\Repositories\InstructorVacationRepository;
|
|
use App\Repositories\PrivateAppointmentRepository;
|
|
use App\Repositories\InstructorTenantRepository;
|
|
use App\Repositories\InstructorTravelTimeRepository;
|
|
use App\Repositories\InstructorHomeToTenantRepository;
|
|
use App\Repositories\InstructorBreakRuleRepository;
|
|
use App\Repositories\AppointmentRequestRepository;
|
|
use App\Support\Auth;
|
|
use App\Support\Request;
|
|
use App\Support\Response;
|
|
|
|
final class InstructorController
|
|
{
|
|
private InstructorRepository $instructorRepo;
|
|
private InstructorAvailabilityRepository $availRepo;
|
|
private InstructorVacationRepository $vacationRepo;
|
|
private PrivateAppointmentRepository $privateRepo;
|
|
private InstructorTenantRepository $tenantRepo;
|
|
private InstructorTravelTimeRepository $travelRepo;
|
|
private InstructorHomeToTenantRepository $homeToTenantRepo;
|
|
private InstructorBreakRuleRepository $breakRuleRepo;
|
|
private AppointmentRequestRepository $requestRepo;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->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);
|
|
}
|
|
}
|
|
}
|
|
} |