Reorganize into monorepo layout, move mitarbeiter-app to legacy reference

Consolidates the previously separate omsorgapp and omsorgCore repos
(each had their own nested .git with GitHub history) plus the old
root-level website/mitarbeiter-app into a single monorepo, matching
the structure already documented in the root CLAUDE.md. Also moves
the PHP employee app aside as omsorgWeb/mitarbeiter-app-legacy/ to
serve as a template for a ground-up rewrite.

Fixes .gitignore in the same pass: the config-secrets/uploads/data
patterns were unanchored (relative to repo root, not depth-agnostic),
so they silently stopped matching once the app moved under omsorgWeb/.
Patterns are now **/-prefixed and cover both mitarbeiter-app and
mitarbeiter-app-legacy, keeping DB/SMTP credentials and uploaded
employee documents out of version control.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Felix Kemmler
2026-08-07 14:21:37 +02:00
co-authored by Claude Sonnet 5
parent 8beb0fcf52
commit b6c1389c55
355 changed files with 24348 additions and 361 deletions
@@ -0,0 +1,184 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/db.php';
require_once __DIR__ . '/../lib/layout.php';
require_login();
$user = current_user();
$user_id = $user['id'];
$now = new DateTimeImmutable('today');
$year = isset($_GET['year']) ? (int)$_GET['year'] : (int)$now->format('Y');
$month = isset($_GET['month']) ? (int)$_GET['month'] : (int)$now->format('n');
$year = max(2020, min(2099, $year));
$month = max(1, min(12, $month));
$first = new DateTimeImmutable(sprintf('%04d-%02d-01', $year, $month));
$month_start = $first->format('Y-m-d');
$month_end = $first->modify('last day of this month')->format('Y-m-d');
$days = (int)$first->format('t');
$prev = $first->modify('-1 month');
$next = $first->modify('+1 month');
$prev_url = '?year=' . $prev->format('Y') . '&month=' . $prev->format('n');
$next_url = '?year=' . $next->format('Y') . '&month=' . $next->format('n');
$stmt = db()->prepare(
'SELECT date, schicht FROM dienstplan WHERE user_id = ? AND date >= ? AND date <= ?'
);
$stmt->execute([$user_id, $month_start, $month_end]);
$schichten = [];
foreach ($stmt->fetchAll() as $row) {
$schichten[$row['date']] = $row['schicht'];
}
$stmt = db()->prepare("
SELECT von, bis, 'urlaub' AS typ
FROM requests_urlaubsantrag
WHERE user_id = ? AND status = 'accepted' AND bis >= ? AND von <= ?
UNION ALL
SELECT von, bis, 'abwesenheit' AS typ
FROM requests_abwesenheitsantrag
WHERE user_id = ? AND status = 'accepted' AND bis >= ? AND von <= ?
");
$stmt->execute([$user_id, $month_start, $month_end, $user_id, $month_start, $month_end]);
$overlays = [];
foreach ($stmt->fetchAll() as $row) {
$cursor = new DateTimeImmutable($row['von']);
$end = new DateTimeImmutable($row['bis']);
while ($cursor <= $end) {
$key = $cursor->format('Y-m-d');
if ($key >= $month_start && $key <= $month_end) {
$overlays[$key] = $row['typ'];
}
$cursor = $cursor->modify('+1 day');
}
}
$today_str = $now->format('Y-m-d');
$ea_stmt = db()->prepare('SELECT ort FROM einsatzanweisung WHERE user_id = ?');
$ea_stmt->execute([$user_id]);
$ea_row = $ea_stmt->fetch();
$ea_ort = ($ea_row && $ea_row['ort'] !== '') ? $ea_row['ort'] : '';
$schicht_labels = ['frueh' => 'Früh', 'spaet' => 'Spät', 'nacht' => 'Nacht'];
$weekdays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
$months_de = ['', 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'];
layout_start('Dienstplan', 'dienstplan');
?>
<h1 class="main-title">Dienstplan</h1>
<?php if ($ea_ort !== ''): ?>
<p class="dp-ort">&#128205; Einsatzort: <strong><?= e($ea_ort) ?></strong></p>
<?php endif; ?>
<div class="dp-nav">
<div style="display:flex;gap:10px;align-items:center">
<a href="<?= e($prev_url) ?>" class="dp-nav-btn">&larr;</a>
<h2 class="dp-nav-title"><?= e($months_de[$month] . ' ' . $year) ?></h2>
<a href="<?= e($next_url) ?>" class="dp-nav-btn">&rarr;</a>
</div>
<span id="dp-status" style="font-size:.85rem;color:var(--muted);transition:opacity .4s"></span>
</div>
<div class="dp-grid-wrap">
<div class="dp-grid">
<?php foreach ($weekdays as $wd): ?>
<div class="dp-weekday"><?= e($wd) ?></div>
<?php endforeach; ?>
<?php
$lead = (int)$first->format('N') - 1;
for ($i = 0; $i < $lead; $i++): ?>
<div class="dp-cell dp-cell-empty"></div>
<?php endfor; ?>
<?php for ($d = 1; $d <= $days; $d++):
$date_key = sprintf('%04d-%02d-%02d', $year, $month, $d);
$is_today = ($date_key === $today_str);
$overlay = $overlays[$date_key] ?? null;
$schicht = $schichten[$date_key] ?? 'leer';
$cell_cls = 'dp-cell';
if (!$overlay && $schicht !== 'leer') $cell_cls .= ' dp-cell-' . $schicht;
if ($is_today) $cell_cls .= ' dp-cell-today';
?>
<div class="<?= $cell_cls ?>">
<span class="dp-day-num"><?= $d ?></span>
<?php if ($overlay): ?>
<span class="dp-badge dp-overlay-<?= e($overlay) ?>">
<?= $overlay === 'urlaub' ? 'Urlaub' : 'Abwesenheit' ?>
</span>
<?php if ($schicht !== 'leer'): ?>
<span class="dp-badge dp-badge-<?= e($schicht) ?>" style="opacity:.45">
<?= e($schicht_labels[$schicht]) ?>
</span>
<?php endif; ?>
<?php else: ?>
<select data-date="<?= e($date_key) ?>" class="dp-select">
<option value="leer" <?= $schicht === 'leer' ? 'selected' : '' ?>>Frei</option>
<option value="frueh" <?= $schicht === 'frueh' ? 'selected' : '' ?>>Früh</option>
<option value="spaet" <?= $schicht === 'spaet' ? 'selected' : '' ?>>Spät</option>
<option value="nacht" <?= $schicht === 'nacht' ? 'selected' : '' ?>>Nacht</option>
</select>
<?php endif; ?>
</div>
<?php endfor; ?>
<?php
$total = $lead + $days;
$trail = (7 - ($total % 7)) % 7;
for ($i = 0; $i < $trail; $i++): ?>
<div class="dp-cell dp-cell-empty"></div>
<?php endfor; ?>
</div>
</div>
<script>
(function(){
var YEAR = <?= (int)$year ?>;
var MONTH = <?= (int)$month ?>;
function colorCell(sel){
var cell = sel.closest('.dp-cell');
cell.classList.remove('dp-cell-frueh','dp-cell-spaet','dp-cell-nacht');
if(sel.value !== 'leer') cell.classList.add('dp-cell-' + sel.value);
}
var statusEl = document.getElementById('dp-status');
var statusTimer;
function showStatus(ok){
clearTimeout(statusTimer);
statusEl.style.opacity = '1';
statusEl.style.color = ok ? '#6eff9a' : '#ff9a9a';
statusEl.textContent = ok ? 'Gespeichert' : 'Fehler beim Speichern';
statusTimer = setTimeout(function(){ statusEl.style.opacity = '0'; }, 1800);
}
document.querySelectorAll('.dp-select').forEach(function(sel){
colorCell(sel);
sel.addEventListener('change', function(){
var date = this.dataset.date;
var schicht = this.value;
colorCell(this);
var body = new URLSearchParams();
body.append('year', YEAR);
body.append('month', MONTH);
body.append('schicht[' + date + ']', schicht);
body.append('_ajax', '1');
body.append('csrf_token', document.querySelector('meta[name="csrf-token"]').content);
fetch('../actions/save-dienstplan.php', {method:'POST', body:body})
.then(function(r){ return r.json(); })
.then(function(d){ showStatus(d.ok); })
.catch(function(){ showStatus(false); });
});
});
})();
</script>
<?php
layout_end();