1a0fe18e89
Führt das getrennte React/Vite-Frontend und Express/MongoDB-Backend in eine einzelne Next.js-Anwendung zusammen: - UI: React-Client-Komponenten (TabLayout, AppointmentForm, -List, ConfirmationModal) im Look von werte-next (Farben #CCCCFF/#85B7D7/ #FFFFDD, Rahmenbox, Tabellen-Liste, Footer mit Version/Datum) - API: Next.js Route Handler unter /api/appointments (GET/POST/PUT/DELETE, Soft-Delete beibehalten) - DB: MongoDB via lib/db.ts (gecachter Client, Standard appointmentsdb) - Tailwind 4 + TypeScript, Standalone-Docker-Build wie werte-next - Druckansicht der aktuellen offenen Termine beibehalten - Deployment: Dockerfile (multi-stage), docker-compose(.prod), deploy.sh Entfernt die alten frontend/- und backend/-Verzeichnisse. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
213 lines
6.9 KiB
TypeScript
213 lines
6.9 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import TabLayout from '@/components/TabLayout';
|
|
import AppointmentForm from '@/components/AppointmentForm';
|
|
import AppointmentList from '@/components/AppointmentList';
|
|
import ConfirmationModal from '@/components/ConfirmationModal';
|
|
import { Appointment, AppointmentPayload } from '@/types/appointment';
|
|
|
|
export default function Home() {
|
|
const [appointments, setAppointments] = useState<Appointment[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [editingAppointment, setEditingAppointment] =
|
|
useState<Appointment | null>(null);
|
|
const [deleteId, setDeleteId] = useState<string | null>(null);
|
|
|
|
const fetchAppointments = async () => {
|
|
try {
|
|
const res = await fetch('/api/appointments', { cache: 'no-store' });
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
setAppointments(data.data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Termine:', error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
// Termine einmalig beim Mount laden.
|
|
useEffect(() => {
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
fetchAppointments();
|
|
}, []);
|
|
|
|
// CREATE
|
|
const addAppointment = async (payload: AppointmentPayload) => {
|
|
try {
|
|
const res = await fetch('/api/appointments', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
if (!res.ok) throw new Error('Fehler beim Hinzufügen.');
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
setAppointments((prev) => [...prev, data.data]);
|
|
}
|
|
} catch (error) {
|
|
console.error('Fehler beim Speichern des Termins:', error);
|
|
alert('Fehler beim Speichern!');
|
|
}
|
|
};
|
|
|
|
// UPDATE (vollständig, vom Formular)
|
|
const updateAppointment = async (id: string, payload: AppointmentPayload) => {
|
|
try {
|
|
const res = await fetch(`/api/appointments/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
if (!res.ok) throw new Error('Fehler beim Aktualisieren.');
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
setAppointments((prev) =>
|
|
prev.map((a) => (a.id === id ? data.data : a))
|
|
);
|
|
setEditingAppointment(null);
|
|
}
|
|
} catch (error) {
|
|
console.error('Fehler beim Aktualisieren des Termins:', error);
|
|
alert('Fehler beim Aktualisieren!');
|
|
}
|
|
};
|
|
|
|
// Statuswechsel (erledigt)
|
|
const toggleDone = async (id: string) => {
|
|
const current = appointments.find((a) => a.id === id);
|
|
if (!current) return;
|
|
const newStatus = !current.erledigt;
|
|
// Optimistisches Update
|
|
setAppointments((prev) =>
|
|
prev.map((a) => (a.id === id ? { ...a, erledigt: newStatus } : a))
|
|
);
|
|
try {
|
|
const res = await fetch(`/api/appointments/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ erledigt: newStatus }),
|
|
});
|
|
if (!res.ok) throw new Error('Fehler beim Statuswechsel.');
|
|
} catch (error) {
|
|
console.error('Fehler beim Statuswechsel:', error);
|
|
// Rückgängig machen bei Fehler
|
|
setAppointments((prev) =>
|
|
prev.map((a) => (a.id === id ? { ...a, erledigt: current.erledigt } : a))
|
|
);
|
|
}
|
|
};
|
|
|
|
// DELETE (Soft-Delete) mit Bestätigung
|
|
const confirmDelete = async () => {
|
|
const id = deleteId;
|
|
setDeleteId(null);
|
|
if (!id) return;
|
|
try {
|
|
const res = await fetch(`/api/appointments/${id}`, { method: 'DELETE' });
|
|
if (!res.ok) throw new Error('Fehler beim Löschen.');
|
|
setAppointments((prev) => prev.filter((a) => a.id !== id));
|
|
if (editingAppointment && editingAppointment.id === id) {
|
|
setEditingAppointment(null);
|
|
}
|
|
} catch (error) {
|
|
console.error('Fehler beim Löschen des Termins:', error);
|
|
alert('Fehler beim Löschen!');
|
|
}
|
|
};
|
|
|
|
const startEdit = (appointment: Appointment) => {
|
|
setEditingAppointment(appointment);
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
};
|
|
|
|
const handlePrint = () => window.print();
|
|
|
|
// Aktuelle (offene) Termine ab heute für die Druckansicht
|
|
const startOfToday = new Date();
|
|
startOfToday.setHours(0, 0, 0, 0);
|
|
const currentAppointments = appointments
|
|
.filter((a) => !a.erledigt && new Date(a.termin) >= startOfToday)
|
|
.sort(
|
|
(a, b) => new Date(a.termin).getTime() - new Date(b.termin).getTime()
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{/* Bildschirmansicht */}
|
|
<div className="screen-only">
|
|
<TabLayout onPrint={handlePrint}>
|
|
<div className="mb-8">
|
|
<h2 className="text-xl font-semibold mb-4">
|
|
{editingAppointment
|
|
? '✍️ Termin bearbeiten'
|
|
: '🗓️ Neuen Termin hinzufügen'}
|
|
</h2>
|
|
<AppointmentForm
|
|
key={editingAppointment?.id ?? 'new'}
|
|
onAdd={addAppointment}
|
|
onUpdate={updateAppointment}
|
|
editingAppointment={editingAppointment}
|
|
onCancelEdit={() => setEditingAppointment(null)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="bg-white border border-black rounded-lg shadow-md p-6">
|
|
<h2 className="text-xl font-semibold mb-4">Alle Termine</h2>
|
|
{isLoading ? (
|
|
<div className="text-center py-4">Lade Daten...</div>
|
|
) : (
|
|
<AppointmentList
|
|
appointments={appointments}
|
|
onToggleDone={toggleDone}
|
|
onEdit={startEdit}
|
|
onDelete={setDeleteId}
|
|
/>
|
|
)}
|
|
</div>
|
|
</TabLayout>
|
|
</div>
|
|
|
|
{/* Druckansicht: nur aktuelle (offene) Termine ab heute */}
|
|
<div className="print-only">
|
|
<h1>Aktuelle Arzt-Termine</h1>
|
|
{currentAppointments.length === 0 ? (
|
|
<p>Keine aktuellen Termine vorhanden.</p>
|
|
) : (
|
|
<table className="print-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Arzt</th>
|
|
<th>Fachrichtung</th>
|
|
<th>Datum</th>
|
|
<th>Bemerkungen</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{currentAppointments.map((app) => (
|
|
<tr key={app.id}>
|
|
<td>{app.arztName}</td>
|
|
<td>{app.arztArt || ''}</td>
|
|
<td>{new Date(app.termin).toLocaleString('de-DE')}</td>
|
|
<td>{app.bemerkungen || ''}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</div>
|
|
|
|
<ConfirmationModal
|
|
isOpen={deleteId !== null}
|
|
title="Termin löschen"
|
|
message="Möchten Sie diesen Termin wirklich löschen?"
|
|
onConfirm={confirmDelete}
|
|
onCancel={() => setDeleteId(null)}
|
|
/>
|
|
</>
|
|
);
|
|
}
|