- api.fahrschultermin.de: full API entry point with CORS + session auth - fahrschultermin.de: frontend only, no more /api/v1 routing - Database.php: PostgreSQL support (pgsql driver + lastval()) - All repositories: use Database::lastInsertId() for PG compatibility - config/app.php: PostgreSQL connection settings + CORS config - bootstrap.php: pass full $config to Database::configure() - public/index.php: fetch-patch to redirect /api/v1 to api.fahrschultermin.de - database/schema_pgsql.sql: full PostgreSQL schema (BIGSERIAL) - database/import_pgsql.sql: 518 rows migrated from SQLite
295 lines
11 KiB
PHP
295 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Repositories;
|
|
|
|
final class StudentRepository extends BaseRepository
|
|
{
|
|
public function allForTenant(int $tenantId): 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
|
|
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)));
|
|
}
|
|
}
|
|
}
|