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:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
@@ -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 */}
|
||||||
|
|||||||
@@ -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 \
|
||||||
|
|||||||
Reference in New Issue
Block a user