Erledigte Termine ans Ende + blasser, neuer Tab 'Liste'

- Erledigte Einträge werden nicht mehr durchgestrichen, nur die
  Textfarbe wird blasser dargestellt
- Erledigte Termine werden in der Liste hinter die offenen sortiert
- Neuer Tab 'Liste' (Route /liste) mit einer read-only Übersicht
  aller Termine; TabLayout auf echte Tabs (Link/usePathname) umgestellt
- AppointmentList: Handler optional, damit die Liste read-only nutzbar ist

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 18:38:26 +02:00
parent 1a0fe18e89
commit 2662b9a3e1
4 changed files with 141 additions and 55 deletions
+45
View File
@@ -0,0 +1,45 @@
'use client';
import { useState, useEffect } from 'react';
import TabLayout from '@/components/TabLayout';
import AppointmentList from '@/components/AppointmentList';
import { Appointment } from '@/types/appointment';
export default function ListePage() {
const [appointments, setAppointments] = useState<Appointment[]>([]);
const [isLoading, setIsLoading] = useState(true);
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();
}, []);
return (
<TabLayout>
<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} />
)}
</div>
</TabLayout>
);
}
+48 -32
View File
@@ -4,9 +4,9 @@ import { Appointment } from '@/types/appointment';
interface AppointmentListProps { interface AppointmentListProps {
appointments: Appointment[]; appointments: Appointment[];
onToggleDone: (id: string) => void; onToggleDone?: (id: string) => void;
onEdit: (appointment: Appointment) => void; onEdit?: (appointment: Appointment) => void;
onDelete: (id: string) => void; onDelete?: (id: string) => void;
} }
const weekdays = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']; const weekdays = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
@@ -33,17 +33,22 @@ export default function AppointmentList({
onEdit, onEdit,
onDelete, onDelete,
}: AppointmentListProps) { }: AppointmentListProps) {
const hasActions = Boolean(onEdit || onDelete);
if (appointments.length === 0) { if (appointments.length === 0) {
return ( return (
<div className="text-center py-8 text-gray-500"> <div className="text-center py-8 text-gray-500">
Keine Termine vorhanden. Fügen Sie oben einen neuen Termin hinzu! Keine Termine vorhanden.
</div> </div>
); );
} }
const sorted = [...appointments].sort( // Erledigte Einträge hinter die offenen schieben; innerhalb der Gruppen
(a, b) => new Date(a.termin).getTime() - new Date(b.termin).getTime() // aufsteigend nach Termin sortieren.
); const sorted = [...appointments].sort((a, b) => {
if (a.erledigt !== b.erledigt) return a.erledigt ? 1 : -1;
return new Date(a.termin).getTime() - new Date(b.termin).getTime();
});
return ( return (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
@@ -57,17 +62,19 @@ export default function AppointmentList({
<th className="p-2 text-center">Tag</th> <th className="p-2 text-center">Tag</th>
<th className="p-2 text-center">Zeit</th> <th className="p-2 text-center">Zeit</th>
<th className="p-2 text-center">Bemerkungen</th> <th className="p-2 text-center">Bemerkungen</th>
<th className="p-2 text-center">Aktion</th> {hasActions && <th className="p-2 text-center">Aktion</th>}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{sorted.map((app, index) => ( {sorted.map((app, index) => (
<tr <tr
key={app.id} key={app.id}
onClick={() => onEdit(app)} onClick={onEdit ? () => onEdit(app) : undefined}
className={`border-b border-gray-300 hover:bg-gray-100 cursor-pointer ${ className={`border-b border-gray-300 hover:bg-gray-100 ${
onEdit ? 'cursor-pointer' : ''
} ${
app.erledigt app.erledigt
? 'bg-green-50 text-gray-400 line-through' ? 'bg-green-50 text-gray-400'
: index % 2 === 0 : index % 2 === 0
? 'bg-white' ? 'bg-white'
: 'bg-gray-50' : 'bg-gray-50'
@@ -79,9 +86,12 @@ export default function AppointmentList({
> >
<input <input
type="checkbox" type="checkbox"
className="w-4 h-4 cursor-pointer" className={`w-4 h-4 ${onToggleDone ? 'cursor-pointer' : ''}`}
checked={app.erledigt} checked={app.erledigt}
onChange={() => onToggleDone(app.id)} disabled={!onToggleDone}
onChange={
onToggleDone ? () => onToggleDone(app.id) : undefined
}
/> />
</td> </td>
<td className="p-2 text-center font-medium">{app.arztName}</td> <td className="p-2 text-center font-medium">{app.arztName}</td>
@@ -90,25 +100,31 @@ export default function AppointmentList({
<td className="p-2 text-center">{getWeekday(app.termin)}</td> <td className="p-2 text-center">{getWeekday(app.termin)}</td>
<td className="p-2 text-center">{formatTime(app.termin)}</td> <td className="p-2 text-center">{formatTime(app.termin)}</td>
<td className="p-2 text-center">{app.bemerkungen || '-'}</td> <td className="p-2 text-center">{app.bemerkungen || '-'}</td>
<td {hasActions && (
className="p-2 text-center" <td
onClick={(e) => e.stopPropagation()} className="p-2 text-center"
> onClick={(e) => e.stopPropagation()}
<div className="flex gap-2 justify-center no-underline"> >
<button <div className="flex gap-2 justify-center">
onClick={() => onEdit(app)} {onEdit && (
className="text-blue-600 hover:text-blue-800 text-sm no-underline" <button
> onClick={() => onEdit(app)}
Editieren className="text-blue-600 hover:text-blue-800 text-sm"
</button> >
<button Editieren
onClick={() => onDelete(app.id)} </button>
className="text-red-600 hover:text-red-800 text-sm no-underline" )}
> {onDelete && (
Löschen <button
</button> onClick={() => onDelete(app.id)}
</div> className="text-red-600 hover:text-red-800 text-sm"
</td> >
Löschen
</button>
)}
</div>
</td>
)}
</tr> </tr>
))} ))}
</tbody> </tbody>
+47 -22
View File
@@ -1,5 +1,7 @@
'use client'; 'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import packageJson from '@/package.json'; import packageJson from '@/package.json';
interface TabLayoutProps { interface TabLayoutProps {
@@ -7,7 +9,13 @@ interface TabLayoutProps {
onPrint?: () => void; onPrint?: () => void;
} }
const TABS = [
{ href: '/', label: 'Termine' },
{ href: '/liste', label: 'Liste' },
];
export default function TabLayout({ children, onPrint }: TabLayoutProps) { export default function TabLayout({ children, onPrint }: TabLayoutProps) {
const pathname = usePathname();
const version = packageJson.version; const version = packageJson.version;
const buildDate = const buildDate =
process.env.NEXT_PUBLIC_BUILD_DATE || process.env.NEXT_PUBLIC_BUILD_DATE ||
@@ -31,29 +39,46 @@ export default function TabLayout({ children, onPrint }: TabLayoutProps) {
{/* Tab-Leiste */} {/* Tab-Leiste */}
<div className="flex justify-between items-end"> <div className="flex justify-between items-end">
<div className="flex"> <div className="flex">
<span {TABS.map((tab) => {
className="px-6 py-2 text-sm font-semibold border-t-2 border-l-2 border-r-2 rounded-tl-lg rounded-tr-lg mr-1" const isActive = pathname === tab.href;
style={{ return (
backgroundColor: '#FFFFDD', <Link
color: '#000000', key={tab.href}
borderColor: '#000000', href={tab.href}
borderBottom: '2px solid #FFFFDD', className="px-6 py-2 text-sm font-semibold border-t-2 border-l-2 border-r-2 rounded-tl-lg rounded-tr-lg mr-1 transition-colors"
marginBottom: '-2px', style={
position: 'relative', isActive
zIndex: 10, ? {
}} backgroundColor: '#FFFFDD',
> color: '#000000',
Termine borderColor: '#000000',
</span> borderBottom: '2px solid #FFFFDD',
</div> marginBottom: '-2px',
<div className="pb-1"> position: 'relative',
<button zIndex: 10,
onClick={onPrint} }
className="px-4 py-2 bg-[#85B7D7] hover:bg-[#6a9fc5] text-black text-sm rounded-lg shadow-md transition-colors" : {
> backgroundColor: '#85B7D7',
🖨 Aktuelle Termine drucken color: '#374151',
</button> borderColor: '#000000',
}
}
>
{tab.label}
</Link>
);
})}
</div> </div>
{onPrint && (
<div className="pb-1">
<button
onClick={onPrint}
className="px-4 py-2 bg-[#85B7D7] hover:bg-[#6a9fc5] text-black text-sm rounded-lg shadow-md transition-colors"
>
🖨 Aktuelle Termine drucken
</button>
</div>
)}
</div> </div>
{/* Inhaltsbereich */} {/* Inhaltsbereich */}
+1 -1
View File
@@ -40,7 +40,7 @@ echo ""
# 3. Docker Image bauen und pushen # 3. Docker Image bauen und pushen
echo ">>> Baue Multiplatform Docker Image und pushe zu Registry..." echo ">>> Baue Multiplatform Docker Image und pushe zu Registry..."
docker buildx build \ docker buildx build \
--platform linux/amd64,linux/arm64 \ --platform linux/amd64 \
--build-arg BUILD_DATE="${BUILD_DATE}" \ --build-arg BUILD_DATE="${BUILD_DATE}" \
-t "${FULL_IMAGE}" \ -t "${FULL_IMAGE}" \
--push \ --push \