Files
beauty-bookings/src/client/components/admin-availability.tsx

859 lines
36 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { queryClient } from "@/client/rpc-client";
export function AdminAvailability() {
const today = new Date().toISOString().split("T")[0];
const [selectedDate, setSelectedDate] = useState<string>(today);
const [time, setTime] = useState<string>("09:00");
const [duration, setDuration] = useState<number>(30);
const [selectedTreatmentId, setSelectedTreatmentId] = useState<string>("");
const [slotType, setSlotType] = useState<"treatment" | "manual">("treatment");
// Neue State-Variablen für Tab-Navigation
const [activeSubTab, setActiveSubTab] = useState<"slots" | "recurring" | "timeoff">("slots");
// States für Recurring Rules
const [selectedDayOfWeek, setSelectedDayOfWeek] = useState<number>(1); // 1=Montag
const [ruleStartTime, setRuleStartTime] = useState<string>("13:00");
const [ruleEndTime, setRuleEndTime] = useState<string>("18:00");
const [editingRuleId, setEditingRuleId] = useState<string>("");
// States für Time-Off
const [timeOffStartDate, setTimeOffStartDate] = useState<string>("");
const [timeOffEndDate, setTimeOffEndDate] = useState<string>("");
const [timeOffReason, setTimeOffReason] = useState<string>("");
const [editingTimeOffId, setEditingTimeOffId] = useState<string>("");
const { data: allSlots, refetch: refetchSlots } = useQuery(
queryClient.availability.live.list.experimental_liveOptions()
);
const { data: treatments } = useQuery(
queryClient.treatments.live.list.experimental_liveOptions()
);
// Neue Queries für wiederkehrende Verfügbarkeiten (mit Authentifizierung)
const { data: recurringRules } = useQuery(
queryClient.recurringAvailability.live.adminListRules.experimental_liveOptions({
input: { sessionId: localStorage.getItem("sessionId") || "" }
})
);
const { data: timeOffPeriods } = useQuery(
queryClient.recurringAvailability.live.adminListTimeOff.experimental_liveOptions({
input: { sessionId: localStorage.getItem("sessionId") || "" }
})
);
const [errorMsg, setErrorMsg] = useState<string>("");
const [successMsg, setSuccessMsg] = useState<string>("");
// Auto-clear messages after 5 seconds
useEffect(() => {
if (errorMsg) {
const timer = setTimeout(() => setErrorMsg(""), 5000);
return () => clearTimeout(timer);
}
}, [errorMsg]);
useEffect(() => {
if (successMsg) {
const timer = setTimeout(() => setSuccessMsg(""), 5000);
return () => clearTimeout(timer);
}
}, [successMsg]);
const { mutate: createSlot, isPending: isCreating } = useMutation(
queryClient.availability.create.mutationOptions()
);
const { mutate: removeSlot } = useMutation(
queryClient.availability.remove.mutationOptions()
);
const { mutate: cleanupPastSlots } = useMutation(
queryClient.availability.cleanupPastSlots.mutationOptions()
);
// Neue Mutations für wiederkehrende Verfügbarkeiten
const { mutate: createRule } = useMutation(
queryClient.recurringAvailability.createRule.mutationOptions()
);
const { mutate: updateRule } = useMutation(
queryClient.recurringAvailability.updateRule.mutationOptions()
);
const { mutate: deleteRule } = useMutation(
queryClient.recurringAvailability.deleteRule.mutationOptions()
);
const { mutate: toggleRuleActive } = useMutation(
queryClient.recurringAvailability.toggleRuleActive.mutationOptions()
);
const { mutate: createTimeOff } = useMutation(
queryClient.recurringAvailability.createTimeOff.mutationOptions()
);
const { mutate: updateTimeOff } = useMutation(
queryClient.recurringAvailability.updateTimeOff.mutationOptions()
);
const { mutate: deleteTimeOff } = useMutation(
queryClient.recurringAvailability.deleteTimeOff.mutationOptions()
);
// Auto-update duration when treatment is selected
useEffect(() => {
if (selectedTreatmentId && treatments) {
const treatment = treatments.find(t => t.id === selectedTreatmentId);
if (treatment) {
setDuration(treatment.duration);
}
}
}, [selectedTreatmentId, treatments]);
// Get selected treatment details
const selectedTreatment = treatments?.find(t => t.id === selectedTreatmentId);
// Get treatment name for display
const getTreatmentName = (treatmentId: string) => {
return treatments?.find(t => t.id === treatmentId)?.name || "Unbekannte Behandlung";
};
// Helper-Funktion für Wochentag-Namen
const getDayName = (dayOfWeek: number): string => {
const days = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"];
return days[dayOfWeek];
};
const addSlot = () => {
setErrorMsg("");
setSuccessMsg("");
// Validation based on slot type
if (slotType === "treatment" && !selectedTreatmentId) {
setErrorMsg("Bitte eine Behandlung auswählen.");
return;
}
if (!selectedDate || !time || !duration) {
setErrorMsg("Bitte Datum, Uhrzeit und Dauer angeben.");
return;
}
const sessionId = localStorage.getItem("sessionId") || "";
if (!sessionId) {
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
return;
}
createSlot(
{ sessionId, date: selectedDate, time, durationMinutes: duration },
{
onSuccess: () => {
const slotDescription = slotType === "treatment"
? `${getTreatmentName(selectedTreatmentId)} (${duration} Min)`
: `Manueller Slot (${duration} Min)`;
setSuccessMsg(`${slotDescription} angelegt.`);
// Manually refetch slots to ensure live updates
refetchSlots();
// advance time by the duration of the slot
const [hStr, mStr] = time.split(":");
let h = parseInt(hStr, 10);
let m = parseInt(mStr, 10);
m += duration;
if (m >= 60) { h += Math.floor(m / 60); m = m % 60; }
if (h >= 24) { h = h % 24; }
const next = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
setTime(next);
},
onError: (err: any) => {
const msg = (err && (err.message || (err as any).toString())) || "Fehler beim Anlegen.";
setErrorMsg(msg);
},
}
);
};
return (
<div className="max-w-4xl mx-auto space-y-6">
{/* Tab-Navigation */}
<div className="bg-white rounded-lg shadow">
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8 px-6">
<button
onClick={() => setActiveSubTab("slots")}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeSubTab === "slots"
? "border-pink-500 text-pink-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
📅 Slots
</button>
<button
onClick={() => setActiveSubTab("recurring")}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeSubTab === "recurring"
? "border-pink-500 text-pink-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
🔄 Wiederkehrende Zeiten
</button>
<button
onClick={() => setActiveSubTab("timeoff")}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeSubTab === "timeoff"
? "border-pink-500 text-pink-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
🏖 Urlaubszeiten
</button>
</nav>
</div>
</div>
{/* Tab Inhalt */}
{activeSubTab === "slots" && (
<>
{/* Slot Type Selection */}
<div className="bg-white rounded-lg shadow p-4">
<h3 className="text-lg font-semibold mb-4">Neuen Slot anlegen</h3>
<div className="flex flex-wrap gap-2 mb-4">
<button
onClick={() => setSlotType("treatment")}
className={`px-4 py-2 rounded-md font-medium transition-colors ${
slotType === "treatment"
? "bg-pink-600 text-white"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
💅 Behandlungs-Slot
</button>
<button
onClick={() => setSlotType("manual")}
className={`px-4 py-2 rounded-md font-medium transition-colors ${
slotType === "manual"
? "bg-pink-600 text-white"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
Manueller Slot
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Datum
</label>
<input
type="date"
value={selectedDate}
onChange={(e) => setSelectedDate(e.target.value)}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Startzeit
</label>
<input
type="time"
value={time}
onChange={(e) => setTime(e.target.value)}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500"
/>
</div>
{slotType === "treatment" ? (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Behandlung
</label>
<select
value={selectedTreatmentId}
onChange={(e) => setSelectedTreatmentId(e.target.value)}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500"
>
<option value="">Behandlung wählen...</option>
{treatments?.map((treatment) => (
<option key={treatment.id} value={treatment.id}>
{treatment.name} ({treatment.duration} Min)
</option>
))}
</select>
</div>
) : (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Dauer (Minuten)
</label>
<input
type="number"
min={5}
step={5}
value={duration}
onChange={(e) => setDuration(Number(e.target.value))}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500"
/>
</div>
)}
<div className="flex items-end">
<button
onClick={addSlot}
disabled={isCreating || (slotType === "treatment" && !selectedTreatmentId)}
className="w-full bg-pink-600 text-white px-4 py-2 rounded-md hover:bg-pink-700 disabled:bg-gray-400 disabled:cursor-not-allowed font-medium transition-colors"
>
{isCreating ? "Anlegen..." : "Slot hinzufügen"}
</button>
</div>
</div>
{/* Treatment Info Display */}
{slotType === "treatment" && selectedTreatment && (
<div className="mt-4 p-3 bg-pink-50 rounded-md border border-pink-200">
<div className="flex items-center justify-between">
<div>
<h4 className="font-medium text-pink-900">{selectedTreatment.name}</h4>
<p className="text-sm text-pink-700">{selectedTreatment.description}</p>
</div>
<div className="text-right">
<div className="text-lg font-semibold text-pink-900">
{(selectedTreatment.price / 100).toFixed(2)}
</div>
<div className="text-sm text-pink-700">
{selectedTreatment.duration} Minuten
</div>
</div>
</div>
</div>
)}
</div>
{(errorMsg || successMsg) && (
<div className="fixed top-4 right-4 z-50 max-w-md">
{errorMsg && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-lg shadow-lg mb-2">
<div className="flex items-center">
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
<span className="font-medium">Fehler:</span>
<span className="ml-1">{errorMsg}</span>
</div>
</div>
)}
{successMsg && (
<div className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded-lg shadow-lg">
<div className="flex items-center">
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<span className="font-medium">Erfolg:</span>
<span className="ml-1">{successMsg}</span>
</div>
</div>
)}
</div>
)}
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-white rounded-lg shadow p-4">
<div className="text-2xl font-bold text-green-600">
{allSlots?.filter(s => s.status === "free").length || 0}
</div>
<div className="text-sm text-gray-600">Freie Slots</div>
</div>
<div className="bg-white rounded-lg shadow p-4">
<div className="text-2xl font-bold text-yellow-600">
{allSlots?.filter(s => s.status === "reserved").length || 0}
</div>
<div className="text-sm text-gray-600">Reservierte Slots</div>
</div>
<div className="bg-white rounded-lg shadow p-4">
<div className="text-2xl font-bold text-blue-600">
{allSlots?.length || 0}
</div>
<div className="text-sm text-gray-600">Slots gesamt</div>
</div>
</div>
{/* Cleanup Button */}
<div className="bg-white rounded-lg shadow p-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-medium text-gray-900">Bereinigung</h3>
<p className="text-sm text-gray-600">Vergangene Slots automatisch löschen</p>
</div>
<button
onClick={() => {
const sessionId = localStorage.getItem("sessionId") || "";
if (!sessionId) {
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
return;
}
cleanupPastSlots(
{ sessionId },
{
onSuccess: (result: any) => {
setSuccessMsg(result.message || "Bereinigung abgeschlossen.");
refetchSlots();
},
onError: (err: any) => {
setErrorMsg(err?.message || "Fehler bei der Bereinigung.");
}
}
);
}}
className="px-4 py-2 bg-orange-600 text-white rounded-md hover:bg-orange-700 transition-colors"
>
Vergangene Slots löschen
</button>
</div>
</div>
{/* All Slots List */}
<div className="bg-white rounded-lg shadow">
<div className="p-4 border-b">
<h3 className="text-lg font-semibold">Alle Slots</h3>
</div>
<div className="divide-y">
{allSlots
?.sort((a, b) => (a.date === b.date ? a.time.localeCompare(b.time) : a.date.localeCompare(b.date)))
.map((slot) => {
// Try to find matching treatment based on duration
const matchingTreatments = treatments?.filter(t => t.duration === slot.durationMinutes) || [];
return (
<div key={slot.id} className="p-4 hover:bg-gray-50 transition-colors">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="text-center">
<div className="text-sm text-gray-500">Datum</div>
<div className="font-medium">{new Date(slot.date).toLocaleDateString('de-DE')}</div>
</div>
<div className="text-center">
<div className="text-sm text-gray-500">Zeit</div>
<div className="font-mono text-lg">{slot.time}</div>
</div>
<div className="text-center">
<div className="text-sm text-gray-500">Dauer</div>
<div className="font-medium">{slot.durationMinutes} Min</div>
</div>
{matchingTreatments.length > 0 && (
<div className="text-center">
<div className="text-sm text-gray-500">Passende Behandlungen</div>
<div className="text-sm">
{matchingTreatments.length === 1
? matchingTreatments[0].name
: `${matchingTreatments.length} Behandlungen`
}
</div>
</div>
)}
<span className={`px-3 py-1 rounded-full text-sm font-medium ${
slot.status === "free"
? "bg-green-100 text-green-800"
: "bg-yellow-100 text-yellow-800"
}`}>
{slot.status === "free" ? "Frei" : "Reserviert"}
</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => {
const sessionId = localStorage.getItem("sessionId");
if (!sessionId) {
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
return;
}
removeSlot(
{ sessionId, id: slot.id },
{
onSuccess: () => {
setSuccessMsg("Slot erfolgreich gelöscht.");
// Manually refetch slots to ensure live updates
refetchSlots();
},
onError: (err: any) => {
const msg = (err && (err.message || (err as any).toString())) || "Fehler beim Löschen des Slots.";
setErrorMsg(msg);
}
}
);
}}
className="px-3 py-1 text-red-600 hover:bg-red-50 rounded-md transition-colors text-sm"
disabled={slot.status === "reserved"}
title={slot.status === "reserved" ? "Slot ist reserviert" : "Slot löschen"}
>
Löschen
</button>
</div>
</div>
{/* Show matching treatments if multiple */}
{matchingTreatments.length > 1 && (
<div className="mt-2 ml-20">
<div className="text-xs text-gray-500 mb-1">Passende Behandlungen:</div>
<div className="flex flex-wrap gap-1">
{matchingTreatments.map(treatment => (
<span key={treatment.id} className="px-2 py-1 bg-pink-100 text-pink-700 rounded text-xs">
{treatment.name}
</span>
))}
</div>
</div>
)}
</div>
);
})}
{allSlots?.length === 0 && (
<div className="p-8 text-center text-gray-500">
<div className="text-lg font-medium mb-2">Keine Slots vorhanden</div>
<div className="text-sm">Legen Sie den ersten Slot an, um zu beginnen.</div>
</div>
)}
</div>
</div>
</>
)}
{/* Tab "Wiederkehrende Zeiten" */}
{activeSubTab === "recurring" && (
<div className="space-y-6">
{/* Neue Regel erstellen */}
<div className="bg-white rounded-lg shadow p-4">
<h3 className="text-lg font-semibold mb-4">Neue wiederkehrende Regel erstellen</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Wochentag
</label>
<select
value={selectedDayOfWeek}
onChange={(e) => setSelectedDayOfWeek(Number(e.target.value))}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500"
>
<option value={1}>Montag</option>
<option value={2}>Dienstag</option>
<option value={3}>Mittwoch</option>
<option value={4}>Donnerstag</option>
<option value={5}>Freitag</option>
<option value={6}>Samstag</option>
<option value={0}>Sonntag</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Startzeit
</label>
<input
type="time"
value={ruleStartTime}
onChange={(e) => setRuleStartTime(e.target.value)}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Endzeit
</label>
<input
type="time"
value={ruleEndTime}
onChange={(e) => setRuleEndTime(e.target.value)}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500"
/>
</div>
</div>
<div className="mt-4">
<button
onClick={() => {
setErrorMsg("");
setSuccessMsg("");
if (ruleStartTime >= ruleEndTime) {
setErrorMsg("Startzeit muss vor der Endzeit liegen.");
return;
}
const sessionId = localStorage.getItem("sessionId") || "";
if (!sessionId) {
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
return;
}
createRule(
{ sessionId, dayOfWeek: selectedDayOfWeek, startTime: ruleStartTime, endTime: ruleEndTime },
{
onSuccess: () => {
setSuccessMsg(`Regel für ${getDayName(selectedDayOfWeek)} erstellt.`);
},
onError: (err: any) => {
setErrorMsg(err?.message || "Fehler beim Erstellen der Regel.");
}
}
);
}}
className="bg-pink-600 text-white px-4 py-2 rounded-md hover:bg-pink-700 font-medium transition-colors"
>
Regel hinzufügen
</button>
</div>
</div>
{/* Bestehende Regeln */}
<div className="bg-white rounded-lg shadow">
<div className="p-4 border-b">
<h3 className="text-lg font-semibold">Bestehende Regeln</h3>
</div>
<div className="divide-y">
{recurringRules?.length === 0 && (
<div className="p-8 text-center text-gray-500">
<div className="text-lg font-medium mb-2">Noch keine wiederkehrenden Regeln definiert</div>
<div className="text-sm">Erstellen Sie Ihre erste Regel, um automatisch Slots zu generieren.</div>
</div>
)}
{recurringRules?.map((rule) => (
<div key={rule.id} className="p-4 hover:bg-gray-50 transition-colors">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="font-medium">{getDayName(rule.dayOfWeek)}</div>
<div className="text-gray-600">{rule.startTime} - {rule.endTime} Uhr</div>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
rule.isActive
? "bg-green-100 text-green-800"
: "bg-gray-100 text-gray-800"
}`}>
{rule.isActive ? "Aktiv" : "Inaktiv"}
</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => {
const sessionId = localStorage.getItem("sessionId");
if (!sessionId) {
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
return;
}
toggleRuleActive(
{ sessionId, id: rule.id },
{
onSuccess: () => {
setSuccessMsg(`Regel ${rule.isActive ? "deaktiviert" : "aktiviert"}.`);
},
onError: (err: any) => {
setErrorMsg(err?.message || "Fehler beim Umschalten der Regel.");
}
}
);
}}
className="px-3 py-1 text-blue-600 hover:bg-blue-50 rounded-md transition-colors text-sm"
>
{rule.isActive ? "Deaktivieren" : "Aktivieren"}
</button>
<button
onClick={() => {
const sessionId = localStorage.getItem("sessionId");
if (!sessionId) {
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
return;
}
deleteRule(
{ sessionId, id: rule.id },
{
onSuccess: () => {
setSuccessMsg("Regel gelöscht.");
},
onError: (err: any) => {
setErrorMsg(err?.message || "Fehler beim Löschen der Regel.");
}
}
);
}}
className="px-3 py-1 text-red-600 hover:bg-red-50 rounded-md transition-colors text-sm"
>
Löschen
</button>
</div>
</div>
</div>
))}
</div>
</div>
</div>
)}
{/* Tab "Urlaubszeiten" */}
{activeSubTab === "timeoff" && (
<div className="space-y-6">
{/* Neue Urlaubszeit erstellen */}
<div className="bg-white rounded-lg shadow p-4">
<h3 className="text-lg font-semibold mb-4">Neue Urlaubszeit erstellen</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Von Datum
</label>
<input
type="date"
value={timeOffStartDate}
onChange={(e) => setTimeOffStartDate(e.target.value)}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Bis Datum
</label>
<input
type="date"
value={timeOffEndDate}
onChange={(e) => setTimeOffEndDate(e.target.value)}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Grund/Notiz
</label>
<input
type="text"
placeholder="z.B. Sommerurlaub, Feiertag"
value={timeOffReason}
onChange={(e) => setTimeOffReason(e.target.value)}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500"
/>
</div>
</div>
<div className="mt-4">
<button
onClick={() => {
setErrorMsg("");
setSuccessMsg("");
if (!timeOffStartDate || !timeOffEndDate || !timeOffReason) {
setErrorMsg("Bitte alle Felder ausfüllen.");
return;
}
if (timeOffStartDate > timeOffEndDate) {
setErrorMsg("Startdatum muss vor dem Enddatum liegen.");
return;
}
const sessionId = localStorage.getItem("sessionId") || "";
if (!sessionId) {
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
return;
}
createTimeOff(
{ sessionId, startDate: timeOffStartDate, endDate: timeOffEndDate, reason: timeOffReason },
{
onSuccess: () => {
setSuccessMsg("Urlaubszeit hinzugefügt.");
setTimeOffStartDate("");
setTimeOffEndDate("");
setTimeOffReason("");
},
onError: (err: any) => {
setErrorMsg(err?.message || "Fehler beim Hinzufügen der Urlaubszeit.");
}
}
);
}}
className="bg-pink-600 text-white px-4 py-2 rounded-md hover:bg-pink-700 font-medium transition-colors"
>
Urlaubszeit hinzufügen
</button>
</div>
</div>
{/* Bestehende Urlaubszeiten */}
<div className="bg-white rounded-lg shadow">
<div className="p-4 border-b">
<h3 className="text-lg font-semibold">Bestehende Urlaubszeiten</h3>
</div>
<div className="divide-y">
{timeOffPeriods?.length === 0 && (
<div className="p-8 text-center text-gray-500">
<div className="text-lg font-medium mb-2">Keine Urlaubszeiten eingetragen</div>
<div className="text-sm">Fügen Sie Urlaubszeiten hinzu, um automatisch Slots zu blockieren.</div>
</div>
)}
{timeOffPeriods?.map((period) => {
const today = new Date().toISOString().split("T")[0];
const isPast = period.endDate < today;
const isCurrent = period.startDate <= today && period.endDate >= today;
const isFuture = period.startDate > today;
return (
<div key={period.id} className="p-4 hover:bg-gray-50 transition-colors">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="font-medium">
{new Date(period.startDate).toLocaleDateString('de-DE')} - {new Date(period.endDate).toLocaleDateString('de-DE')}
</div>
<div className="text-gray-600">{period.reason}</div>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
isPast
? "bg-gray-100 text-gray-800"
: isCurrent
? "bg-red-100 text-red-800"
: "bg-blue-100 text-blue-800"
}`}>
{isPast ? "Vergangen" : isCurrent ? "Aktuell" : "Geplant"}
</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => {
const sessionId = localStorage.getItem("sessionId");
if (!sessionId) {
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
return;
}
deleteTimeOff(
{ sessionId, id: period.id },
{
onSuccess: () => {
setSuccessMsg("Urlaubszeit gelöscht.");
},
onError: (err: any) => {
setErrorMsg(err?.message || "Fehler beim Löschen der Urlaubszeit.");
}
}
);
}}
className="px-3 py-1 text-red-600 hover:bg-red-50 rounded-md transition-colors text-sm"
>
Löschen
</button>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
)}
</div>
);
}