Mist, jetzt vielleicht

This commit is contained in:
rxf
2026-03-11 20:33:19 +01:00
parent bc235e4e32
commit a949ebcdc8
28 changed files with 1666 additions and 74 deletions

View File

@@ -0,0 +1,208 @@
'use client';
import { useEffect, useState, useCallback } from 'react';
import { Tablette } from '@/types/tablette';
const EMPTY: Omit<Tablette, 'akt' | 'until' | 'warn'> = {
tab: '',
pday: 1,
cnt: 0,
at: '',
rem: '',
order: '',
};
type FormData = Omit<Tablette, 'akt' | 'until' | 'warn'>;
export default function TablettenTable() {
const [rows, setRows] = useState<Tablette[]>([]);
const [sortField, setSortField] = useState('until');
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
const [modal, setModal] = useState<null | 'add' | 'edit' | 'del'>(null);
const [selected, setSelected] = useState<Tablette | null>(null);
const [form, setForm] = useState<FormData>({ ...EMPTY });
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const fetchData = useCallback(async () => {
setLoading(true);
try {
const res = await fetch(`/api/data?sidx=${sortField}&sord=${sortDir}`);
const json = await res.json();
setRows(json.values || []);
} catch {
setError('Fehler beim Laden der Daten.');
} finally {
setLoading(false);
}
}, [sortField, sortDir]);
useEffect(() => {
fetchData();
}, [fetchData]);
function handleSort(field: string) {
if (field === sortField) {
setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
} else {
setSortField(field);
setSortDir('asc');
}
}
function openAdd() {
setForm({ ...EMPTY });
setModal('add');
}
function openEdit(row: Tablette) {
setSelected(row);
setForm({ tab: row.tab, pday: row.pday, cnt: row.cnt, at: row.at, rem: row.rem, order: row.order });
setModal('edit');
}
function openDel(row: Tablette) {
setSelected(row);
setModal('del');
}
async function handleSave() {
const payload = { ...form, oper: modal === 'add' ? 'add' : 'edit' };
await fetch('/api/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
setModal(null);
fetchData();
}
async function handleDelete() {
if (!selected) return;
await fetch('/api/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ oper: 'del', tab: selected.tab }),
});
setModal(null);
fetchData();
}
const SortIndicator = ({ field }: { field: string }) =>
sortField === field ? (sortDir === 'asc' ? ' ▲' : ' ▼') : '';
const colHeader = (label: string, field: string) => (
<th
key={field}
onClick={() => handleSort(field)}
className="sortable-header"
>
{label}
<SortIndicator field={field} />
</th>
);
return (
<div className="table-container">
{error && <p className="error-msg">{error}</p>}
<div className="toolbar">
<button onClick={openAdd} className="btn btn-add">+ Hinzufügen</button>
<button onClick={fetchData} className="btn btn-refresh"> Aktualisieren</button>
</div>
<table className="main-table">
<thead>
<tr>
{colHeader('Tabletten', 'tab')}
{colHeader('pro Tag', 'pday')}
{colHeader('aktuell', 'akt')}
{colHeader('reicht bis', 'until')}
{colHeader('Anzahl…', 'cnt')}
{colHeader('…am', 'at')}
{colHeader('Bemerkungen', 'rem')}
{colHeader('bestellt am', 'order')}
<th>Aktion</th>
</tr>
</thead>
<tbody>
{loading && (
<tr><td colSpan={9} style={{ textAlign: 'center' }}>Lade</td></tr>
)}
{!loading && rows.map((row) => (
<tr
key={row.tab}
className={
row.warn ? 'row-warn' : row.rem === 'abgesetzt' ? 'row-abgesetzt' : ''
}
>
<td className="col-tab">{row.tab}</td>
<td className="col-pday">{row.pday}</td>
<td className="cell-center">{row.akt}</td>
<td className={`col-date ${row.warn ? 'cell-warn' : ''}`}>{row.until}</td>
<td className="cell-center">{row.cnt}</td>
<td className="col-date">{row.at}</td>
<td>{row.rem}</td>
<td className="col-date">{row.order}</td>
<td className="cell-center action-cell">
<button onClick={() => openEdit(row)} className="btn-icon" title="Bearbeiten"></button>
<button onClick={() => openDel(row)} className="btn-icon" title="Löschen">🗑</button>
</td>
</tr>
))}
</tbody>
</table>
{/* Add/Edit Modal */}
{(modal === 'add' || modal === 'edit') && (
<div className="modal-overlay" onClick={() => setModal(null)}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<h2>{modal === 'add' ? 'Neuer Eintrag' : 'Bearbeiten'}</h2>
<label>Tabletten Name
<input value={form.tab} onChange={(e) => setForm({ ...form, tab: e.target.value })}
disabled={modal === 'edit'} />
</label>
<label>pro Tag
<input type="number" step="0.25" value={form.pday}
onChange={(e) => setForm({ ...form, pday: parseFloat(e.target.value) || 0 })} />
</label>
<label>Anzahl
<input type="number" value={form.cnt}
onChange={(e) => setForm({ ...form, cnt: parseInt(e.target.value, 10) || 0 })} />
</label>
<label>Kaufdatum (am)
<input type="text" value={form.at} placeholder="YYYY-MM-DD"
pattern="\d{4}-\d{2}-\d{2}"
onChange={(e) => setForm({ ...form, at: e.target.value })} />
</label>
<label>Bemerkungen
<input value={form.rem}
onChange={(e) => setForm({ ...form, rem: e.target.value })} />
</label>
<label>Bestellt am
<input type="text" value={form.order} placeholder="YYYY-MM-DD"
pattern="\d{4}-\d{2}-\d{2}"
onChange={(e) => setForm({ ...form, order: e.target.value })} />
</label>
<div className="modal-buttons">
<button onClick={handleSave} className="btn btn-add">Speichern</button>
<button onClick={() => setModal(null)} className="btn btn-refresh">Abbrechen</button>
</div>
</div>
</div>
)}
{/* Delete Modal */}
{modal === 'del' && selected && (
<div className="modal-overlay" onClick={() => setModal(null)}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<h2>Eintrag löschen</h2>
<p>Möchtest du <strong>{selected.tab}</strong> wirklich löschen?</p>
<div className="modal-buttons">
<button onClick={handleDelete} className="btn btn-delete">Löschen</button>
<button onClick={() => setModal(null)} className="btn btn-refresh">Abbrechen</button>
</div>
</div>
</div>
)}
</div>
);
}