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
+15
View File
@@ -132,6 +132,21 @@ function current_role(): string {
return $_SESSION['omsorgcore_profile']['role'] ?? '';
}
// Serverseitig ist jeder Endpunkt ohnehin über [RequirePermission] gegated (siehe
// omsorgCore/CLAUDE.md, "Rechtesystem") - das hier ist nur die UI-Seite davon (Tab/Formular
// ausblenden statt nur eine 403-Antwort abzuwarten), analog zu hasPermission() in
// omsorgapp/src/app/AuthContext.jsx. $_SESSION['omsorgcore_profile']['permissions'] kommt aus
// GET /api/auth/me (PermissionDto[] { module, action, scope }, siehe MeResponse in omsorgCore).
function has_permission(string $module, string $action): bool {
$permissions = $_SESSION['omsorgcore_profile']['permissions'] ?? [];
foreach ($permissions as $permission) {
if (($permission['module'] ?? null) === $module && ($permission['action'] ?? null) === $action) {
return true;
}
}
return false;
}
function e(string $s): string {
return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
+10
View File
@@ -35,6 +35,16 @@ function layout_start(string $title, string $currentPage = 'dashboard'): void {
<a href="dashboard.php" class="<?= $currentPage === 'dashboard' ? 'active' : '' ?>">
<span class="nav-icon">⊞</span><span class="nav-label"> Dashboard</span>
</a>
<?php if (has_permission('Absences', 'View')): ?>
<a href="urlaubsantrag.php" class="<?= $currentPage === 'urlaubsantrag' ? 'active' : '' ?>">
<span class="nav-icon">🗓</span><span class="nav-label"> Urlaub &amp; Abwesenheit</span>
</a>
<?php endif; ?>
<?php if (has_permission('TimeEntries', 'View')): ?>
<a href="stundenerfassung.php" class="<?= $currentPage === 'stundenerfassung' ? 'active' : '' ?>">
<span class="nav-icon">⏱</span><span class="nav-label"> Zeiterfassung</span>
</a>
<?php endif; ?>
<a href="settings.php" class="<?= $currentPage === 'settings' ? 'active' : '' ?>">
<span class="nav-icon">⚙</span><span class="nav-label"> Einstellungen</span>
</a>
@@ -3,8 +3,9 @@
// analog zu httpClient.cjs/authClient.cjs in omsorgapp. Alle Aufrufer bekommen ein einheitliches
// Ergebnis-Array: ['ok' => bool, 'status' => int, 'data' => array|null].
//
// Nur Auth-Endpunkte - Connect verwaltet keine anderen Nutzer/Rollen/Mitarbeiter (das bleibt
// OMSORG Desktop vorbehalten), deshalb fehlen hier bewusst omsorgcore_users_*/roles_*/employees_*.
// Auth-Endpunkte plus die für den Außendienst freigegebenen Absences-/ValueLists-Endpunkte -
// Connect verwaltet weiterhin keine anderen Nutzer/Rollen/Mitarbeiter (das bleibt OMSORG Desktop
// vorbehalten), deshalb fehlen hier bewusst omsorgcore_users_*/roles_*/employees_*.
//
// Transport läuft über den generierten Client in ../api-client-php/ (openapi-generator, siehe
// ../api-client-php/ANLEITUNG.md) statt über rohes cURL.
@@ -110,3 +111,150 @@ function omsorgcore_password_policy(array $config): array
return omsorgcore_call($config, fn($cfg) => (new \OmsorgCoreClient\Api\AuthApi(new \GuzzleHttp\Client(), $cfg))
->apiAuthPasswordPolicyGetWithHttpInfo());
}
// --- ValueLists (nur lesend - Dropdown-Optionen, z. B. "AbsenceType") ---
function omsorgcore_value_list_items(array $config, string $accessToken, string $key): array
{
return omsorgcore_call($config, function ($cfg) use ($accessToken, $key) {
$cfg->setAccessToken($accessToken);
return (new \OmsorgCoreClient\Api\ValueListsApi(new \GuzzleHttp\Client(), $cfg))
->apiValueListsKeyItemsGetWithHttpInfo($key);
});
}
// --- Absences (Urlaub/Krankmeldung/Sonstige, FR-CON-1) ---
// Own-Scope: die EmployeeId wird serverseitig aus dem JWT aufgelöst (siehe AbsenceService in
// omsorgCore), Connect schickt hier bewusst nie eine employeeId mit.
function omsorgcore_absences_list(array $config, string $accessToken, ?string $status = null): array
{
return omsorgcore_call($config, function ($cfg) use ($accessToken, $status) {
$cfg->setAccessToken($accessToken);
return (new \OmsorgCoreClient\Api\AbsencesApi(new \GuzzleHttp\Client(), $cfg))
->apiAbsencesGetWithHttpInfo($status);
});
}
function omsorgcore_absences_create(array $config, string $accessToken, array $payload): array
{
$createAbsenceRequest = new \OmsorgCoreClient\Model\CreateAbsenceRequest([
'type' => $payload['type'],
'start_date' => new \DateTime($payload['startDate']),
'end_date' => new \DateTime($payload['endDate']),
'reason' => $payload['reason'] !== '' ? $payload['reason'] : null,
'substitute' => $payload['substitute'] !== '' ? $payload['substitute'] : null,
'note' => $payload['note'] !== '' ? $payload['note'] : null,
]);
return omsorgcore_call($config, function ($cfg) use ($accessToken, $createAbsenceRequest) {
$cfg->setAccessToken($accessToken);
return (new \OmsorgCoreClient\Api\AbsencesApi(new \GuzzleHttp\Client(), $cfg))
->apiAbsencesPostWithHttpInfo($createAbsenceRequest);
});
}
// Nur solange der Antrag noch "Eingereicht" ist (serverseitig erzwungen, siehe
// AbsenceService.UpdateAsync in omsorgCore) - Genehmigt/Abgelehnt sind über diesen Endpoint
// bewusst nicht mehr änderbar.
function omsorgcore_absences_update(array $config, string $accessToken, string $id, array $payload): array
{
$updateAbsenceRequest = new \OmsorgCoreClient\Model\UpdateAbsenceRequest([
'type' => $payload['type'],
'start_date' => new \DateTime($payload['startDate']),
'end_date' => new \DateTime($payload['endDate']),
'reason' => $payload['reason'] !== '' ? $payload['reason'] : null,
'substitute' => $payload['substitute'] !== '' ? $payload['substitute'] : null,
'note' => $payload['note'] !== '' ? $payload['note'] : null,
]);
return omsorgcore_call($config, function ($cfg) use ($accessToken, $id, $updateAbsenceRequest) {
$cfg->setAccessToken($accessToken);
return (new \OmsorgCoreClient\Api\AbsencesApi(new \GuzzleHttp\Client(), $cfg))
->apiAbsencesIdPutWithHttpInfo($id, $updateAbsenceRequest);
});
}
// --- Orders (nur lesend - Auftrags-Dropdown fürs Zeiterfassungsformular) ---
// Order hat noch keine Mitarbeiter-Zuweisung (FR-EM-3 offen), daher zeigt das Dropdown alle
// aktiven Aufträge - siehe omsorgCore/CLAUDE.md, Abschnitt "Zeiterfassung", dokumentierte Einschränkung.
function omsorgcore_orders_list(array $config, string $accessToken): array
{
return omsorgcore_call($config, function ($cfg) use ($accessToken) {
$cfg->setAccessToken($accessToken);
return (new \OmsorgCoreClient\Api\OrdersApi(new \GuzzleHttp\Client(), $cfg))
->apiOrdersGetWithHttpInfo(null, null, null, null, null, null, 1, 200);
});
}
// --- TimeEntries (Zeiterfassung pro Schicht, FR-ZE-1/FR-ZE-2) ---
// Own-Scope: die EmployeeId wird serverseitig aus dem JWT aufgelöst (siehe TimeEntryService in
// omsorgCore), Connect schickt hier bewusst nie eine employeeId mit.
function omsorgcore_time_entries_list(array $config, string $accessToken, ?string $statusId = null): array
{
return omsorgcore_call($config, function ($cfg) use ($accessToken, $statusId) {
$cfg->setAccessToken($accessToken);
return (new \OmsorgCoreClient\Api\TimeEntriesApi(new \GuzzleHttp\Client(), $cfg))
->apiTimeEntriesGetWithHttpInfo($statusId);
});
}
function omsorgcore_time_entries_create(array $config, string $accessToken, array $payload): array
{
$createTimeEntryRequest = new \OmsorgCoreClient\Model\CreateTimeEntryRequest([
'order_id' => $payload['orderId'],
'date' => new \DateTime($payload['date']),
'start' => $payload['start'],
'end' => $payload['end'],
'break_duration' => $payload['breakDuration'],
'night_hours' => $payload['nightHours'],
'saturday_hours' => $payload['saturdayHours'],
'sunday_hours' => $payload['sundayHours'],
'holiday_hours' => $payload['holidayHours'],
]);
return omsorgcore_call($config, function ($cfg) use ($accessToken, $createTimeEntryRequest) {
$cfg->setAccessToken($accessToken);
return (new \OmsorgCoreClient\Api\TimeEntriesApi(new \GuzzleHttp\Client(), $cfg))
->apiTimeEntriesPostWithHttpInfo($createTimeEntryRequest);
});
}
// Nur solange status.isEditableByOwner (serverseitig erzwungen, siehe TimeEntryService.UpdateAsync
// in omsorgCore) - kein statusId-Feld hier, Statuswechsel laufen ausschließlich über submit/decision.
function omsorgcore_time_entries_update(array $config, string $accessToken, string $id, array $payload): array
{
$updateTimeEntryRequest = new \OmsorgCoreClient\Model\UpdateTimeEntryRequest([
'order_id' => $payload['orderId'],
'date' => new \DateTime($payload['date']),
'start' => $payload['start'],
'end' => $payload['end'],
'break_duration' => $payload['breakDuration'],
'night_hours' => $payload['nightHours'],
'saturday_hours' => $payload['saturdayHours'],
'sunday_hours' => $payload['sundayHours'],
'holiday_hours' => $payload['holidayHours'],
]);
return omsorgcore_call($config, function ($cfg) use ($accessToken, $id, $updateTimeEntryRequest) {
$cfg->setAccessToken($accessToken);
return (new \OmsorgCoreClient\Api\TimeEntriesApi(new \GuzzleHttp\Client(), $cfg))
->apiTimeEntriesIdPutWithHttpInfo($id, $updateTimeEntryRequest);
});
}
function omsorgcore_value_list_transitions(array $config, string $accessToken, string $key): array
{
return omsorgcore_call($config, function ($cfg) use ($accessToken, $key) {
$cfg->setAccessToken($accessToken);
return (new \OmsorgCoreClient\Api\ValueListsApi(new \GuzzleHttp\Client(), $cfg))
->apiValueListsKeyTransitionsGetWithHttpInfo($key);
});
}
function omsorgcore_time_entries_submit(array $config, string $accessToken, string $id): array
{
return omsorgcore_call($config, function ($cfg) use ($accessToken, $id) {
$cfg->setAccessToken($accessToken);
return (new \OmsorgCoreClient\Api\TimeEntriesApi(new \GuzzleHttp\Client(), $cfg))
->apiTimeEntriesIdSubmitPostWithHttpInfo($id);
});
}