Mist, jetzt vielleicht
This commit is contained in:
44
components/AppLayout.tsx
Normal file
44
components/AppLayout.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
'use client';
|
||||
|
||||
import LogoutButton from '@/components/LogoutButton';
|
||||
import packageJson from '@/package.json';
|
||||
|
||||
interface AppLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function AppLayout({ children }: AppLayoutProps) {
|
||||
const version = packageJson.version;
|
||||
const buildDate = process.env.NEXT_PUBLIC_BUILD_DATE || new Date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
|
||||
return (
|
||||
<div className="min-h-screen py-8 px-4">
|
||||
<div className="max-w-316 mx-auto border-2 border-black rounded-xl bg-gray-200 p-6">
|
||||
|
||||
{/* Seitentitel */}
|
||||
<h1 className="text-4xl font-bold text-center mb-6 tracking-tight">Tabletten-Check</h1>
|
||||
|
||||
<div className="max-w-6xl mx-auto">
|
||||
|
||||
{/* Logout-Button oben rechts */}
|
||||
<div className="flex justify-end -mb-0.5">
|
||||
<LogoutButton className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm rounded-lg shadow-md" />
|
||||
</div>
|
||||
|
||||
{/* Inhaltsbereich */}
|
||||
<main className="border-2 border-black rounded-lg p-6 bg-[#FFFFDD]">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
<footer className="mt-8 flex justify-between items-center text-sm text-gray-600 px-4">
|
||||
<a href="mailto:rxf@gmx.de" className="hover:underline">
|
||||
mailto:rxf@gmx.de
|
||||
</a>
|
||||
<div>Version {version} – {buildDate}</div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
components/LogoutButton.tsx
Normal file
23
components/LogoutButton.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { logout } from '@/app/login/actions';
|
||||
|
||||
interface LogoutButtonProps {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function LogoutButton({ className, children }: LogoutButtonProps) {
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className={className || 'px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors'}
|
||||
>
|
||||
{children || 'Abmelden'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
208
components/TablettenTable.tsx
Normal file
208
components/TablettenTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user