859 lines
36 KiB
TypeScript
859 lines
36 KiB
TypeScript
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>
|
||
);
|
||
}
|
||
|
||
|