Files
anjuma-dashboard/termine.php
T
2026-07-22 01:12:45 +02:00

436 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once "config/session.php";
if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
header("location: index.php");
exit;
}
include('navbar.php');
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ANJUMA • Termine</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.10/css/dataTables.bootstrap5.min.css">
<style>
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
.kpi-card { min-height: 92px; }
.badge-status { text-transform: uppercase; letter-spacing: .04em; }
.table-responsive { overflow-x: auto; }
@media (max-width: 576px) {
.hide-xs { display:none !important; }
.kpi-hint { display:none !important; }
}
.mobile-card .meta { font-size: .9rem; }
.mobile-card .title { font-weight: 600; }
.mobile-card .sub { color: #6c757d; }
</style>
</head>
<body class="bg-light">
<div class="container-fluid py-4">
<div class="d-flex align-items-center justify-content-between mb-3">
<div>
<h3 class="mb-0">Termine</h3>
</div>
<div class="d-flex gap-2 flex-wrap">
<button id="btnRefresh" class="btn btn-outline-secondary btn-sm">↻ Refresh</button>
<button id="btnAdd" class="btn btn-primary btn-sm">+ Termin</button>
</div>
</div>
<!-- Filter -->
<div class="card shadow-sm mb-3">
<div class="card-body">
<div class="row g-2 align-items-end">
<div class="col-12 col-md-3">
<label class="form-label mb-1">Von</label>
<input type="date" class="form-control" id="fromDate">
</div>
<div class="col-12 col-md-3">
<label class="form-label mb-1">Bis</label>
<input type="date" class="form-control" id="toDate">
</div>
<div class="col-12 col-md-3">
<label class="form-label mb-1">Status</label>
<select class="form-select" id="statusFilter">
<option value="">Alle</option>
<option value="0">Offen</option>
<option value="1">Bereit</option>
<option value="2">Abgeschlossen</option>
<option value="3">Storniert</option>
</select>
</div>
<div class="col-12 col-md-3 d-flex gap-2">
<button id="btnThisMonth" class="btn btn-outline-primary w-100">Dieser Monat</button>
<button id="btnThisYear" class="btn btn-outline-primary w-100">Dieses Jahr</button>
</div>
</div>
</div>
</div>
<!-- KPI -->
<div class="row g-3 mb-3">
<div class="col-12 col-md-4">
<div class="card shadow-sm kpi-card">
<div class="card-body">
<div class="text-muted small">Heute</div>
<div class="fs-3 fw-semibold" id="kpiToday"></div>
<div class="text-muted small kpi-hint" id="kpiTodayHint"></div>
</div>
</div>
</div>
<div class="col-12 col-md-4">
<div class="card shadow-sm kpi-card">
<div class="card-body">
<div class="text-muted small">Diese Woche</div>
<div class="fs-3 fw-semibold" id="kpiWeek"></div>
<div class="text-muted small kpi-hint" id="kpiWeekHint"></div>
</div>
</div>
</div>
<div class="col-12 col-md-4">
<div class="card shadow-sm kpi-card">
<div class="card-body">
<div class="text-muted small">Dieser Monat</div>
<div class="fs-3 fw-semibold" id="kpiMonth"></div>
<div class="text-muted small kpi-hint" id="kpiMonthHint"></div>
</div>
</div>
</div>
</div>
<!-- Table -->
<div class="card shadow-sm">
<div class="card-body">
<!-- Mobile Cards (nur xs/sm) -->
<div id="mobileCards" class="d-block d-md-none"></div>
<!-- Desktop Table (ab md) -->
<div class="table-responsive d-none d-md-block">
<table id="tblTermine" class="table table-striped table-hover align-middle w-100">
<thead>
<tr>
<th>Datum</th>
<th class="hide-xs">Zeit</th>
<th>Location</th>
<th>Status</th>
<th>Gage</th>
<th class="d-none d-md-table-cell">Beschreibung</th>
<th>Aktion</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="text-muted small mt-2" id="tableInfo"></div>
</div>
</div>
</div>
<!-- Toast -->
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 1080">
<div id="toast" class="toast align-items-center text-bg-dark border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body" id="toastBody">...</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/datatables.net@1.13.10/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.10/js/dataTables.bootstrap5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
const API_LIST = '/api/termine_list.php';
let dt;
const toast = new bootstrap.Toast(document.getElementById('toast'), { delay: 3500 });
function showToast(msg){ document.getElementById('toastBody').textContent = msg; toast.show(); }
function escapeHtml(s){
return String(s ?? '')
.replaceAll('&','&amp;').replaceAll('<','&lt;')
.replaceAll('>','&gt;').replaceAll('"','&quot;')
.replaceAll("'","&#039;");
}
function renderMobileCards(items){
const el = document.getElementById('mobileCards');
if (!el) return;
if (!items.length) {
el.innerHTML = `<div class="text-muted">Keine Termine im Zeitraum.</div>`;
return;
}
el.innerHTML = items.map(r => {
const date = fmtDateDEFromTs(r.startzeit);
const time = `${fmtTimeFromTs(r.startzeit)}${fmtTimeFromTs(r.endzeit)}`;
const loc = `${(r.locationtype||'').trim()} ${(r.location||'').trim()}`.trim();
const desc = (r.beschreibung || '').toString().trim();
const descShort = desc.length > 90 ? desc.slice(0, 90) + '…' : desc;
return `
<div class="card shadow-sm mb-2 mobile-card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start gap-2">
<div class="title">${escapeHtml(loc || '—')}</div>
<div class="text-end">
${statusBadge(r.status)}
<div class="mono mt-1">${eur(r.gage || 0)}</div>
</div>
</div>
<div class="meta sub mt-1">
<span class="mono">${date}</span> · <span class="mono">${time}</span>
</div>
${desc ? `<div class="mt-2">${escapeHtml(descShort)}</div>` : ''}
<div class="d-flex gap-2 mt-3">
<button class="btn btn-outline-primary btn-sm actEdit" data-id="${r.id}">Edit</button>
</div>
</div>
</div>
`;
}).join('');
}
function isoLocal(d){
const y=d.getFullYear(), m=String(d.getMonth()+1).padStart(2,'0'), day=String(d.getDate()).padStart(2,'0');
return `${y}-${m}-${day}`;
}
function thisMonthRange(){
const d=new Date();
return [isoLocal(new Date(d.getFullYear(), d.getMonth(), 1)), isoLocal(new Date(d.getFullYear(), d.getMonth()+1, 0))];
}
function thisYearRange(){
const d=new Date();
return [isoLocal(new Date(d.getFullYear(), 0, 1)), isoLocal(new Date(d.getFullYear(), 11, 31))];
}
function fmtDateDEFromTs(ts){
const d=new Date(Number(ts)*1000);
const dd=String(d.getDate()).padStart(2,'0');
const mm=String(d.getMonth()+1).padStart(2,'0');
const yy=d.getFullYear();
return `${dd}.${mm}.${yy}`;
}
function fmtTimeFromTs(ts){
const d=new Date(Number(ts)*1000);
const hh=String(d.getHours()).padStart(2,'0');
const mm=String(d.getMinutes()).padStart(2,'0');
return `${hh}:${mm}`;
}
function eur(n){
const v = Number(n)||0;
return v.toLocaleString('de-DE', { style:'currency', currency:'EUR', maximumFractionDigits:0 });
}
function statusBadge(s){
s = String(s ?? '').trim();
if (s === '2') return '<span class="badge text-bg-success badge-status">ABGESCHLOSSEN</span>';
if (s === '0') return '<span class="badge text-bg-secondary badge-status">OFFEN</span>';
if (s === '1') return '<span class="badge text-bg-primary badge-status">BEREIT</span>';
if (s === '3') return '<span class="badge text-bg-danger badge-status">STORNIERT</span>';
return '<span class="badge text-bg-warning badge-status">UNBEKANNT</span>';
}
function actionsHtml(row){
return `
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-primary actEdit" data-id="${row.id}">Edit</button>
</div>
`;
}
function getQS(){
const from = document.getElementById('fromDate').value;
const to = document.getElementById('toDate').value;
const status = document.getElementById('statusFilter').value;
const qs = new URLSearchParams({ from, to });
if (status !== '') qs.set('status', status);
return qs.toString();
}
async function fetchJSON(url){
const res = await fetch(url, { cache:'no-store' });
const text = await res.text();
try {
const json = JSON.parse(text);
if (!json.ok) throw new Error(json.error || 'API error');
return json;
} catch(e){
throw new Error(`API liefert kein JSON (${res.status}). ${text.slice(0,200)}`);
}
}
// ✅ Sort: Zukunft zuerst (nächster Termin oben), dann Vergangenheit
function sortNextFirst(items){
const nowTs = Math.floor(Date.now() / 1000);
return items.slice().sort((a, b) => {
const as = Number(a.startzeit) || 0;
const bs = Number(b.startzeit) || 0;
const aFuture = as >= nowTs;
const bFuture = bs >= nowTs;
if (aFuture && !bFuture) return -1;
if (!aFuture && bFuture) return 1;
// Beide Zukunft -> ASC (nächster zuerst)
if (aFuture && bFuture) return as - bs;
// Beide Vergangenheit -> DESC (letzter vergangener oben)
return bs - as;
});
}
async function refresh(){
try{
const qs = getQS();
const json = await fetchJSON(`${API_LIST}?${qs}`);
// ✅ sortierte items nutzen (für Mobile & Desktop identisch)
const items = sortNextFirst(json.items || []);
const isMobile = window.matchMedia("(max-width: 767.98px)").matches;
// Desktop: DataTable füllen
if (!isMobile) {
dt.clear().rows.add(items).draw(false);
}
// Mobile: Cards füllen
if (isMobile) {
renderMobileCards(items);
}
document.getElementById('tableInfo').textContent =
`Treffer: ${items.length} • Zeitraum: ${json.from} bis ${json.to}`;
// KPI (clientseitig)
const now = new Date();
const todayKey = isoLocal(now);
// week start (Mon)
const day = (now.getDay()+6)%7; // 0=Mon
const weekStart = new Date(now);
weekStart.setDate(now.getDate()-day);
weekStart.setHours(0,0,0,0);
const [mFrom, mTo] = thisMonthRange();
const mFromMs = new Date(mFrom+'T00:00:00').getTime();
const mToMs = new Date(mTo+'T23:59:59').getTime();
let todayCount=0;
let weekCount=0;
let monthCount = 0;
for(const r of items){
const startMs = Number(r.startzeit)*1000;
const dIso = isoLocal(new Date(startMs));
if (dIso === todayKey) todayCount++;
if (startMs >= weekStart.getTime()) weekCount++;
if (startMs >= mFromMs && startMs <= mToMs) monthCount++;
}
document.getElementById('kpiToday').textContent = todayCount;
document.getElementById('kpiTodayHint').textContent = 'Termine heute';
document.getElementById('kpiWeek').textContent = weekCount;
document.getElementById('kpiWeekHint').textContent = 'Termine seit Montag';
document.getElementById('kpiMonth').textContent = monthCount;
document.getElementById('kpiMonthHint').textContent = 'Termine diesen Monat';
} catch(e){
showToast('Fehler: ' + e.message);
}
}
// Init
$(function(){
const [f,t] = thisMonthRange();
document.getElementById('fromDate').value = f;
document.getElementById('toDate').value = t;
dt = $('#tblTermine').DataTable({
pageLength: 25,
// ✅ wichtig: keine Default-Sortierung, wir sortieren im JS für "nächster Termin oben"
order: [],
columns: [
{
data: 'startzeit',
className:'mono',
render: (d, type) => {
const ts = parseInt(d, 10) || 0;
if (type === 'sort' || type === 'type') return ts;
return fmtDateDEFromTs(ts);
}
},
{ data: null, className:'mono hide-xs', render: (row)=> `${fmtTimeFromTs(row.startzeit)}${fmtTimeFromTs(row.endzeit)}` },
{ data: null, render: (row)=> `${(row.locationtype||'').trim()} ${(row.location||'').trim()}`.trim() },
{ data: 'status', render: (d)=>statusBadge(d) },
{ data: 'gage', className:'mono', render: (d)=> eur(d) },
{ data: 'beschreibung', className:'d-none d-md-table-cell', render: (d)=> (d||'').toString().slice(0,80) },
{ data: null, orderable:false, searchable:false, render: (row)=>actionsHtml(row) },
],
});
['fromDate','toDate','statusFilter'].forEach(id => {
document.getElementById(id).addEventListener('change', refresh);
});
document.getElementById('btnThisMonth').addEventListener('click', () => {
const [a,b] = thisMonthRange();
document.getElementById('fromDate').value = a;
document.getElementById('toDate').value = b;
refresh();
});
document.getElementById('btnThisYear').addEventListener('click', () => {
const [a,b] = thisYearRange();
document.getElementById('fromDate').value = a;
document.getElementById('toDate').value = b;
refresh();
});
document.getElementById('btnRefresh').addEventListener('click', refresh);
refresh();
});
// Hooks für View/Edit
document.addEventListener('click', (ev) => {
const e = ev.target.closest('.actEdit');
if (e) {
const id = e.dataset.id;
window.location = `/termin_show.php?id=${id}`;
return;
}
});
document.getElementById('btnAdd').addEventListener('click', () => {
window.location = `/termin_create.php`;
});
</script>
</body>
</html>