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>
This commit is contained in:
+212
@@ -0,0 +1,212 @@
|
||||
'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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user