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:
@@ -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 "Aktualisieren", um die Änderungen zu
|
||||
speichern, oder "Abbrechen", 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user