Files
aerzte/app/page.tsx
T
admin 1a0fe18e89 Umstellung auf Next.js (App Router), Design analog werte-next
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>
2026-07-01 14:24:34 +02:00

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)}
/>
</>
);
}