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:
2026-07-01 14:24:34 +02:00
parent 91a31f04b1
commit 1a0fe18e89
55 changed files with 8216 additions and 1756 deletions
+226
View File
@@ -0,0 +1,226 @@
'use client';
import { useState } from 'react';
import { Appointment, AppointmentFormData, AppointmentPayload } from '@/types/appointment';
interface AppointmentFormProps {
onAdd: (payload: AppointmentPayload) => void;
onUpdate: (id: string, payload: AppointmentPayload) => void;
editingAppointment: Appointment | null;
onCancelEdit: () => void;
}
const emptyForm: AppointmentFormData = {
arztName: '',
arztArt: '',
terminDatum: '',
terminUhrzeit: '',
bemerkungen: '',
};
// Hilfsfunktionen: ISO-String -> Formularfelder
const toDateInput = (iso: string) => {
const d = new Date(iso);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
};
const toTimeInput = (iso: string) => {
const d = new Date(iso);
const h = String(d.getHours()).padStart(2, '0');
const min = String(d.getMinutes()).padStart(2, '0');
return `${h}:${min}`;
};
// Leitet die Formularfelder aus dem zu bearbeitenden Termin ab (oder leer).
const buildInitialForm = (
editing: Appointment | null
): AppointmentFormData =>
editing
? {
arztName: editing.arztName,
arztArt: editing.arztArt ?? '',
terminDatum: toDateInput(editing.termin),
terminUhrzeit: toTimeInput(editing.termin),
bemerkungen: editing.bemerkungen ?? '',
}
: emptyForm;
export default function AppointmentForm({
onAdd,
onUpdate,
editingAppointment,
onCancelEdit,
}: AppointmentFormProps) {
// Der Anfangszustand wird aus den Props abgeleitet. Wechselt der zu
// bearbeitende Termin, wird die Komponente über einen `key` im Parent
// neu gemountet dadurch entfällt ein setState im Effect.
const [formData, setFormData] = useState<AppointmentFormData>(() =>
buildInitialForm(editingAppointment)
);
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!formData.arztName || !formData.terminDatum || !formData.terminUhrzeit) {
alert('Bitte füllen Sie Name, Datum und Uhrzeit aus.');
return;
}
const termin = new Date(
`${formData.terminDatum}T${formData.terminUhrzeit}`
).toISOString();
const payload: AppointmentPayload = {
arztName: formData.arztName,
arztArt: formData.arztArt,
termin,
erledigt: editingAppointment ? editingAppointment.erledigt : false,
bemerkungen: formData.bemerkungen,
};
if (editingAppointment) {
onUpdate(editingAppointment.id, payload);
} else {
onAdd(payload);
}
};
const inputClass =
'w-full px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none';
return (
<div className="bg-[#CCCCFF] border border-black p-6 rounded-lg mb-6">
{editingAppointment && (
<div className="mb-4 p-3 bg-blue-100 border border-blue-400 rounded text-sm text-blue-800">
<strong>Bearbeitungsmodus:</strong> Sie bearbeiten einen bestehenden
Termin. Klicken Sie auf &quot;Aktualisieren&quot;, um die Änderungen zu
speichern, oder &quot;Abbrechen&quot;, um zur Neuerfassung
zurückzukehren.
</div>
)}
<form onSubmit={handleSubmit}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label
htmlFor="arztName"
className="block text-sm font-medium text-gray-800 mb-1"
>
Name des Arztes
</label>
<input
type="text"
id="arztName"
name="arztName"
className={inputClass}
value={formData.arztName}
onChange={handleChange}
required
/>
</div>
<div>
<label
htmlFor="arztArt"
className="block text-sm font-medium text-gray-800 mb-1"
>
Art des Arztes
</label>
<input
type="text"
id="arztArt"
name="arztArt"
className={inputClass}
placeholder="z.B. Zahnarzt, Kardiologe"
value={formData.arztArt}
onChange={handleChange}
/>
</div>
<div>
<label
htmlFor="terminDatum"
className="block text-sm font-medium text-gray-800 mb-1"
>
Datum
</label>
<input
type="date"
id="terminDatum"
name="terminDatum"
className={inputClass}
value={formData.terminDatum}
onChange={handleChange}
required
/>
</div>
<div>
<label
htmlFor="terminUhrzeit"
className="block text-sm font-medium text-gray-800 mb-1"
>
Uhrzeit
</label>
<input
type="time"
id="terminUhrzeit"
name="terminUhrzeit"
className={inputClass}
value={formData.terminUhrzeit}
onChange={handleChange}
required
/>
</div>
<div className="md:col-span-2">
<label
htmlFor="bemerkungen"
className="block text-sm font-medium text-gray-800 mb-1"
>
Bemerkungen
</label>
<textarea
id="bemerkungen"
name="bemerkungen"
rows={3}
className={`${inputClass} resize-y`}
value={formData.bemerkungen}
onChange={handleChange}
/>
</div>
</div>
<div className="flex justify-center gap-4 mt-6">
<button
type="submit"
className="bg-[#85B7D7] hover:bg-[#6a9fc5] text-black font-medium py-2 px-8 rounded-lg transition-colors"
>
{editingAppointment ? 'Aktualisieren' : 'Speichern'}
</button>
<button
type="button"
onClick={() => {
if (editingAppointment) {
onCancelEdit();
} else {
setFormData(emptyForm);
}
}}
className="bg-[#85B7D7] hover:bg-[#6a9fc5] text-black font-medium py-2 px-8 rounded-lg transition-colors"
>
{editingAppointment ? 'Abbrechen' : 'Löschen'}
</button>
</div>
</form>
</div>
);
}