Add edit/delete functionality for Wasserzähler entries in admin dashboard
Admins can now edit all fields of a Wasserzähler entry (name, address, customer number, meter number, readings) and delete entries with a confirmation dialog. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,16 @@ export default function AdminDashboardPage() {
|
|||||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||||
const [confirmMsg, setConfirmMsg] = useState('');
|
const [confirmMsg, setConfirmMsg] = useState('');
|
||||||
|
|
||||||
|
// Wasserzähler edit/delete
|
||||||
|
const [editZaehler, setEditZaehler] = useState<Wasserzaehler | null>(null);
|
||||||
|
const [editForm, setEditForm] = useState({
|
||||||
|
haushalt_name: '', adresse: '', kundennummer: '', zaehlernummer: '',
|
||||||
|
alter_stand: '', neuer_stand: '', ablesedatum: '',
|
||||||
|
});
|
||||||
|
const [editError, setEditError] = useState('');
|
||||||
|
const [editSaving, setEditSaving] = useState(false);
|
||||||
|
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
supabase.auth.getUser().then(({ data: { user } }) => {
|
supabase.auth.getUser().then(({ data: { user } }) => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -138,6 +148,65 @@ export default function AdminDashboardPage() {
|
|||||||
loadSettings();
|
loadSettings();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openEditZaehler = (z: Wasserzaehler) => {
|
||||||
|
setEditZaehler(z);
|
||||||
|
setEditForm({
|
||||||
|
haushalt_name: z.haushalt_name || '',
|
||||||
|
adresse: z.adresse || '',
|
||||||
|
kundennummer: z.kundennummer || '',
|
||||||
|
zaehlernummer: z.zaehlernummer || '',
|
||||||
|
alter_stand: z.alter_stand != null ? String(z.alter_stand) : '',
|
||||||
|
neuer_stand: z.neuer_stand != null ? String(z.neuer_stand) : '',
|
||||||
|
ablesedatum: z.ablesedatum || '',
|
||||||
|
});
|
||||||
|
setEditError('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditSave = async () => {
|
||||||
|
if (!editZaehler) return;
|
||||||
|
setEditError('');
|
||||||
|
setEditSaving(true);
|
||||||
|
|
||||||
|
const alterStand = editForm.alter_stand ? parseFloat(editForm.alter_stand) : null;
|
||||||
|
const neuerStand = editForm.neuer_stand ? parseFloat(editForm.neuer_stand) : null;
|
||||||
|
const verbrauch = neuerStand != null && alterStand != null ? neuerStand - alterStand : null;
|
||||||
|
|
||||||
|
const { error } = await supabase.from('wasserzaehler').update({
|
||||||
|
haushalt_name: editForm.haushalt_name,
|
||||||
|
adresse: editForm.adresse,
|
||||||
|
kundennummer: editForm.kundennummer,
|
||||||
|
zaehlernummer: editForm.zaehlernummer,
|
||||||
|
alter_stand: alterStand,
|
||||||
|
neuer_stand: neuerStand,
|
||||||
|
verbrauch,
|
||||||
|
ablesedatum: editForm.ablesedatum || null,
|
||||||
|
}).eq('id', editZaehler.id);
|
||||||
|
|
||||||
|
setEditSaving(false);
|
||||||
|
if (error) {
|
||||||
|
setEditError(`Fehler: ${error.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setEditZaehler(null);
|
||||||
|
setConfirmMsg('Wasserzähler wurde erfolgreich aktualisiert.');
|
||||||
|
setShowConfirmation(true);
|
||||||
|
loadZaehler();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteZaehler = async (id: string) => {
|
||||||
|
const { error } = await supabase.from('wasserzaehler').delete().eq('id', id);
|
||||||
|
setDeleteConfirm(null);
|
||||||
|
if (error) {
|
||||||
|
setConfirmMsg(`Fehler beim Löschen: ${error.message}`);
|
||||||
|
setShowConfirmation(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setEditZaehler(null);
|
||||||
|
setConfirmMsg('Wasserzähler wurde gelöscht.');
|
||||||
|
setShowConfirmation(true);
|
||||||
|
loadZaehler();
|
||||||
|
};
|
||||||
|
|
||||||
// Today stats
|
// Today stats
|
||||||
const todayStr = new Date().toISOString().split('T')[0];
|
const todayStr = new Date().toISOString().split('T')[0];
|
||||||
const todayStats = useMemo(() => {
|
const todayStats = useMemo(() => {
|
||||||
@@ -420,12 +489,13 @@ export default function AdminDashboardPage() {
|
|||||||
<th className="px-4 py-3 text-right font-semibold">Verbrauch</th>
|
<th className="px-4 py-3 text-right font-semibold">Verbrauch</th>
|
||||||
<th className="px-4 py-3 text-left font-semibold">Ablesedatum</th>
|
<th className="px-4 py-3 text-left font-semibold">Ablesedatum</th>
|
||||||
<th className="px-4 py-3 text-left font-semibold">Token</th>
|
<th className="px-4 py-3 text-left font-semibold">Token</th>
|
||||||
|
<th className="px-4 py-3 text-left font-semibold">Aktion</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-border/50">
|
<tbody className="divide-y divide-border/50">
|
||||||
{zaehler.length === 0 ? (
|
{zaehler.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={9} className="px-4 py-12 text-center text-text-muted">
|
<td colSpan={10} className="px-4 py-12 text-center text-text-muted">
|
||||||
Keine Wasserzähler gefunden.
|
Keine Wasserzähler gefunden.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -455,6 +525,20 @@ export default function AdminDashboardPage() {
|
|||||||
{z.access_token.slice(0, 8)}...
|
{z.access_token.slice(0, 8)}...
|
||||||
</code>
|
</code>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap">
|
||||||
|
<button
|
||||||
|
onClick={() => openEditZaehler(z)}
|
||||||
|
className="text-accent text-xs font-medium hover:underline mr-2"
|
||||||
|
>
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setDeleteConfirm(z.id)}
|
||||||
|
className="text-danger text-xs font-medium hover:underline"
|
||||||
|
>
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -632,6 +716,177 @@ export default function AdminDashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Wasserzähler Edit Modal */}
|
||||||
|
{editZaehler && (
|
||||||
|
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-end sm:items-center justify-center z-50 p-0 sm:p-4 animate-fade-in">
|
||||||
|
<div className="bg-white rounded-t-2xl sm:rounded-2xl shadow-2xl max-w-lg w-full p-6 pb-8 max-h-[90vh] overflow-y-auto animate-slide-up">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-bold text-primary">
|
||||||
|
Wasserzähler bearbeiten
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setEditZaehler(null)}
|
||||||
|
className="w-8 h-8 rounded-full flex items-center justify-center hover:bg-bg transition-colors text-text-muted"
|
||||||
|
aria-label="Schließen"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Readonly fields */}
|
||||||
|
<div className="mb-4 p-3 bg-bg/50 rounded-xl space-y-1">
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span className="text-text-muted">Access Token</span>
|
||||||
|
<code className="font-mono select-all">{editZaehler.access_token}</code>
|
||||||
|
</div>
|
||||||
|
{editForm.neuer_stand && editForm.alter_stand && (
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span className="text-text-muted">Verbrauch (berechnet)</span>
|
||||||
|
<span className="font-semibold text-accent">
|
||||||
|
{(parseFloat(editForm.neuer_stand) - parseFloat(editForm.alter_stand)).toFixed(1)} m³
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium mb-1">Haushalt Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editForm.haushalt_name}
|
||||||
|
onChange={(e) => setEditForm(f => ({ ...f, haushalt_name: e.target.value }))}
|
||||||
|
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium mb-1">Adresse</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editForm.adresse}
|
||||||
|
onChange={(e) => setEditForm(f => ({ ...f, adresse: e.target.value }))}
|
||||||
|
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium mb-1">Kundennummer</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editForm.kundennummer}
|
||||||
|
onChange={(e) => setEditForm(f => ({ ...f, kundennummer: e.target.value }))}
|
||||||
|
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium mb-1">Zählernummer</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editForm.zaehlernummer}
|
||||||
|
onChange={(e) => setEditForm(f => ({ ...f, zaehlernummer: e.target.value }))}
|
||||||
|
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium mb-1">Alter Stand (m³)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
inputMode="decimal"
|
||||||
|
step="0.01"
|
||||||
|
value={editForm.alter_stand}
|
||||||
|
onChange={(e) => setEditForm(f => ({ ...f, alter_stand: e.target.value }))}
|
||||||
|
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium mb-1">Neuer Stand (m³)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
inputMode="decimal"
|
||||||
|
step="0.01"
|
||||||
|
value={editForm.neuer_stand}
|
||||||
|
onChange={(e) => setEditForm(f => ({ ...f, neuer_stand: e.target.value }))}
|
||||||
|
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium mb-1">Ablesedatum</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={editForm.ablesedatum}
|
||||||
|
onChange={(e) => setEditForm(f => ({ ...f, ablesedatum: e.target.value }))}
|
||||||
|
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{editError && (
|
||||||
|
<div className="p-2.5 bg-danger/5 border border-danger/20 rounded-xl text-danger text-sm">
|
||||||
|
{editError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-2 pt-1">
|
||||||
|
<button
|
||||||
|
onClick={() => setEditZaehler(null)}
|
||||||
|
className="flex-1 border border-border py-3 rounded-xl font-semibold text-sm hover:bg-bg transition-colors"
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleEditSave}
|
||||||
|
disabled={editSaving}
|
||||||
|
className="flex-1 bg-accent text-white py-3 rounded-xl font-semibold text-sm hover:bg-accent-light active:scale-[0.98] transition-all disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{editSaving ? 'Speichern...' : 'Speichern'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setDeleteConfirm(editZaehler.id)}
|
||||||
|
className="w-full py-2.5 rounded-xl text-sm font-medium text-danger hover:bg-danger/5 transition-colors"
|
||||||
|
>
|
||||||
|
Eintrag löschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Delete Confirmation */}
|
||||||
|
{deleteConfirm && (
|
||||||
|
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center z-[60] p-4 animate-fade-in">
|
||||||
|
<div className="bg-white rounded-2xl shadow-2xl max-w-sm w-full p-6 animate-slide-up text-center">
|
||||||
|
<div className="w-12 h-12 rounded-full bg-danger/10 flex items-center justify-center mx-auto mb-3">
|
||||||
|
<svg className="w-6 h-6 text-danger" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-bold text-primary mb-1">Wirklich löschen?</h3>
|
||||||
|
<p className="text-sm text-text-muted mb-5">
|
||||||
|
Der Wasserzähler-Eintrag wird unwiderruflich gelöscht.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setDeleteConfirm(null)}
|
||||||
|
className="flex-1 border border-border py-3 rounded-xl font-semibold text-sm hover:bg-bg transition-colors"
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteZaehler(deleteConfirm)}
|
||||||
|
className="flex-1 bg-danger text-white py-3 rounded-xl font-semibold text-sm hover:bg-danger/90 active:scale-[0.98] transition-all"
|
||||||
|
>
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{showConfirmation && (
|
{showConfirmation && (
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
title="Erfolgreich!"
|
title="Erfolgreich!"
|
||||||
|
|||||||
Reference in New Issue
Block a user