Migrate omsorgapp to browser SPA, add Docker/CI build setup
Docker-Images bauen und veröffentlichen / build (, omsorgCore/Dockerfile, omsorgcore) (push) Successful in 3s
Docker-Images bauen und veröffentlichen / build (, omsorgWeb/Dockerfile, omsorgweb) (push) Failing after 4s
Docker-Images bauen und veröffentlichen / build (VITE_OMSORG_CORE_URL=${{ vars.OMSORG_CORE_PUBLIC_URL }}, omsorgapp/Dockerfile, omsorgapp) (push) Successful in 3s

- omsorgapp: drop Electron, run as a plain Vite/React browser app; refresh
  token moves to an HttpOnly cookie (omsorgCore), CORS added for the new
  browser origin, document download/preview switched to Blob-based browser
  APIs.
- Add Dockerfiles for omsorgCore, omsorgapp, and omsorgWeb, a docker-compose.yml
  wiring Postgres/MySQL/all three apps together, and a Gitea Actions workflow
  that builds and pushes images to the repo's container registry on push to
  main and on version tags.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Felix Kemmler
2026-08-10 17:42:45 +02:00
co-authored by Claude Sonnet 5
parent e9e96a57dc
commit 598dfcd38a
461 changed files with 59753 additions and 2859 deletions
@@ -0,0 +1,235 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/layout.php';
require_login();
if (!has_permission('Absences', 'View')) {
header('Location: dashboard.php');
exit;
}
$canCreate = has_permission('Absences', 'Create');
$canEdit = has_permission('Absences', 'Edit');
$error = '';
$success = false;
$editId = null;
$typesResult = omsorgcore_value_list_items(_omsorgcore_config(), $_SESSION['omsorgcore_access_token'], 'AbsenceType');
$types = $typesResult['ok'] ? $typesResult['data'] : [];
// Nicht auf den Anzeigetext "Eingereicht" hartkodieren - der ist über die Status-Verwaltung in
// omsorgapp umbenennbar (siehe omsorgCore/CLAUDE.md, AbsenceService.GetInitialStatusValueAsync).
$statusesResult = omsorgcore_value_list_items(_omsorgcore_config(), $_SESSION['omsorgcore_access_token'], 'AbsenceStatus');
$statuses = $statusesResult['ok'] ? $statusesResult['data'] : [];
$initialStatus = null;
foreach ($statuses as $statusItem) {
if (!empty($statusItem['isInitial'])) {
$initialStatus = $statusItem['value'];
break;
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$mode = $_POST['mode'] ?? 'create';
$payload = [
'type' => $_POST['type'] ?? '',
'startDate' => $_POST['start_date'] ?? '',
'endDate' => $_POST['end_date'] ?? '',
'reason' => trim($_POST['reason'] ?? ''),
'substitute' => trim($_POST['substitute'] ?? ''),
'note' => trim($_POST['note'] ?? ''),
];
if ($payload['type'] === '' || $payload['startDate'] === '' || $payload['endDate'] === '') {
$error = 'Art, Beginn und Ende sind erforderlich.';
$editId = $mode === 'edit' ? ($_POST['absence_id'] ?? null) : null;
} elseif ($payload['endDate'] < $payload['startDate']) {
$error = 'Das Ende darf nicht vor dem Beginn liegen.';
$editId = $mode === 'edit' ? ($_POST['absence_id'] ?? null) : null;
} elseif ($mode === 'edit') {
if (!$canEdit) {
// Server lehnt das ohnehin über [RequirePermission(Absences, Edit)] ab - dieser Zweig
// greift nur, wenn jemand das Formular manuell postet, obwohl es clientseitig gar nicht
// angezeigt wurde.
$error = 'Keine Berechtigung, diesen Antrag zu bearbeiten.';
} else {
$absenceId = $_POST['absence_id'] ?? '';
$result = omsorgcore_absences_update(_omsorgcore_config(), $_SESSION['omsorgcore_access_token'], $absenceId, $payload);
if ($result['ok']) {
header('Location: urlaubsantrag.php?updated=1');
exit;
}
$editId = $absenceId;
$error = $result['status'] === 403
? 'Keine Berechtigung, diesen Antrag zu bearbeiten.'
: (is_string($result['data'] ?? null) ? $result['data'] : 'Antrag konnte nicht gespeichert werden.');
}
} elseif (!$canCreate) {
$error = 'Keine Berechtigung, einen Antrag zu stellen.';
} else {
$result = omsorgcore_absences_create(_omsorgcore_config(), $_SESSION['omsorgcore_access_token'], $payload);
if ($result['ok']) {
$success = true;
} elseif ($result['status'] === 403) {
$error = 'Keine Berechtigung, einen Antrag zu stellen.';
} else {
$error = is_string($result['data'] ?? null) ? $result['data'] : 'Antrag konnte nicht gespeichert werden.';
}
}
} elseif (isset($_GET['edit'])) {
$editId = $_GET['edit'];
}
$listResult = omsorgcore_absences_list(_omsorgcore_config(), $_SESSION['omsorgcore_access_token']);
$absences = $listResult['ok'] ? ($listResult['data']['items'] ?? []) : [];
$editAbsence = null;
if ($editId !== null) {
foreach ($absences as $absence) {
if ($absence['id'] === $editId) {
$editAbsence = $absence;
break;
}
}
// Fremder/ungültiger/schon entschiedener Antrag - Own-Scope-Filterung passiert serverseitig
// in omsorgcore_absences_list, hier nur zusätzlich: nicht (mehr) bearbeitbar zurück zu "Neuer Antrag".
if ($editAbsence === null || $editAbsence['status'] !== $initialStatus) {
$editId = null;
$editAbsence = null;
}
}
layout_start('Urlaub & Abwesenheit Mitarbeiter-App', 'urlaubsantrag');
?>
<h1 class="main-title">Urlaub &amp; Abwesenheit</h1>
<div class="split-layout">
<div class="glass" style="padding:24px">
<h2 style="font-family:'KindelSerif',Georgia,serif;font-size:1.3rem;margin:0 0 18px">Meine Anträge</h2>
<?php if (empty($absences)): ?>
<p>Noch keine Anträge eingereicht.</p>
<?php else: ?>
<?php foreach ($absences as $absence): ?>
<div style="padding:12px 0;border-bottom:1px solid rgba(0,0,0,.08)">
<b><?= e($absence['type']) ?></b> — <?= e($absence['status']) ?>
<div class="hint">
<?= e(substr($absence['startDate'], 0, 10)) ?> <?= e(substr($absence['endDate'], 0, 10)) ?>
</div>
<?php if (!empty($absence['adminNote'])): ?>
<div class="hint">Kommentar: <?= e($absence['adminNote']) ?></div>
<?php endif; ?>
<?php if ($canEdit && $absence['status'] === $initialStatus): ?>
<div style="margin-top:6px">
<a href="urlaubsantrag.php?edit=<?= e($absence['id']) ?>">Bearbeiten</a>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<div class="glass" style="padding:24px">
<h2 style="font-family:'KindelSerif',Georgia,serif;font-size:1.3rem;margin:0 0 18px">
<?= $editAbsence ? 'Antrag bearbeiten' : 'Neuer Antrag' ?>
</h2>
<?php if ($success): ?>
<div class="hint" style="margin-bottom:16px">Antrag wurde eingereicht.</div>
<?php endif; ?>
<?php if (isset($_GET['updated'])): ?>
<div class="hint" style="margin-bottom:16px">Antrag wurde aktualisiert.</div>
<?php endif; ?>
<?php if ($error): ?>
<div class="error" style="margin-bottom:16px"><?= e($error) ?></div>
<?php endif; ?>
<?php if ($editAbsence): ?>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="mode" value="edit">
<input type="hidden" name="absence_id" value="<?= e($editAbsence['id']) ?>">
<label>
Art
<select name="type" required>
<option value="">— auswählen —</option>
<?php foreach ($types as $type): ?>
<option value="<?= e($type['value']) ?>" <?= $type['value'] === $editAbsence['type'] ? 'selected' : '' ?>><?= e($type['value']) ?></option>
<?php endforeach; ?>
</select>
</label>
<label>
Beginn
<input type="date" name="start_date" value="<?= e(substr($editAbsence['startDate'], 0, 10)) ?>" required>
</label>
<label>
Ende
<input type="date" name="end_date" value="<?= e(substr($editAbsence['endDate'], 0, 10)) ?>" required>
</label>
<label>
Grund (optional)
<input type="text" name="reason" maxlength="500" value="<?= e($editAbsence['reason'] ?? '') ?>">
</label>
<label>
Vertretung (optional)
<input type="text" name="substitute" maxlength="200" value="<?= e($editAbsence['substitute'] ?? '') ?>">
</label>
<label>
Nachricht (optional)
<textarea name="note" maxlength="500"><?= e($editAbsence['note'] ?? '') ?></textarea>
</label>
<button type="submit" class="btn" style="width:100%;margin-top:6px">Änderungen speichern</button>
</form>
<p class="hint" style="text-align:center;margin-top:14px">
<a href="urlaubsantrag.php">Abbrechen</a>
</p>
<?php elseif ($canCreate): ?>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="mode" value="create">
<label>
Art
<select name="type" required>
<option value="">— auswählen —</option>
<?php foreach ($types as $type): ?>
<option value="<?= e($type['value']) ?>"><?= e($type['value']) ?></option>
<?php endforeach; ?>
</select>
</label>
<label>
Beginn
<input type="date" name="start_date" required>
</label>
<label>
Ende
<input type="date" name="end_date" required>
</label>
<label>
Grund (optional)
<input type="text" name="reason" maxlength="500">
</label>
<label>
Vertretung (optional)
<input type="text" name="substitute" maxlength="200">
</label>
<label>
Nachricht (optional)
<textarea name="note" maxlength="500"></textarea>
</label>
<button type="submit" class="btn" style="width:100%;margin-top:6px">Antrag einreichen</button>
</form>
<?php else: ?>
<p class="hint">Keine Berechtigung, einen Antrag zu stellen.</p>
<?php endif; ?>
</div>
</div>
<?php
layout_end();