Entferne Slots-Tab und Slot-RPCs; bereinige recurring-availability; Texte angepasst
This commit is contained in:
@@ -304,7 +304,7 @@ function App() {
|
|||||||
Verfügbarkeiten verwalten
|
Verfügbarkeiten verwalten
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-lg text-gray-600">
|
<p className="text-lg text-gray-600">
|
||||||
Lege freie Slots an und entferne sie bei Bedarf.
|
Verwalte wiederkehrende Zeiten und Urlaubszeiten.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<AdminAvailability />
|
<AdminAvailability />
|
||||||
|
@@ -4,14 +4,9 @@ import { queryClient } from "@/client/rpc-client";
|
|||||||
|
|
||||||
export function AdminAvailability() {
|
export function AdminAvailability() {
|
||||||
const today = new Date().toISOString().split("T")[0];
|
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
|
// Tab-Navigation (Slots entfernt)
|
||||||
const [activeSubTab, setActiveSubTab] = useState<"slots" | "recurring" | "timeoff">("slots");
|
const [activeSubTab, setActiveSubTab] = useState<"recurring" | "timeoff">("recurring");
|
||||||
|
|
||||||
// States für Recurring Rules
|
// States für Recurring Rules
|
||||||
const [selectedDayOfWeek, setSelectedDayOfWeek] = useState<number>(1); // 1=Montag
|
const [selectedDayOfWeek, setSelectedDayOfWeek] = useState<number>(1); // 1=Montag
|
||||||
@@ -26,14 +21,6 @@ export function AdminAvailability() {
|
|||||||
const [editingTimeOffId, setEditingTimeOffId] = 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)
|
// Neue Queries für wiederkehrende Verfügbarkeiten (mit Authentifizierung)
|
||||||
const { data: recurringRules } = useQuery(
|
const { data: recurringRules } = useQuery(
|
||||||
queryClient.recurringAvailability.live.adminListRules.experimental_liveOptions({
|
queryClient.recurringAvailability.live.adminListRules.experimental_liveOptions({
|
||||||
@@ -64,16 +51,6 @@ export function AdminAvailability() {
|
|||||||
}
|
}
|
||||||
}, [successMsg]);
|
}, [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
|
// Neue Mutations für wiederkehrende Verfügbarkeiten
|
||||||
const { mutate: createRule } = useMutation(
|
const { mutate: createRule } = useMutation(
|
||||||
queryClient.recurringAvailability.createRule.mutationOptions()
|
queryClient.recurringAvailability.createRule.mutationOptions()
|
||||||
@@ -97,23 +74,6 @@ export function AdminAvailability() {
|
|||||||
queryClient.recurringAvailability.deleteTimeOff.mutationOptions()
|
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
|
// Helper-Funktion für Wochentag-Namen
|
||||||
const getDayName = (dayOfWeek: number): string => {
|
const getDayName = (dayOfWeek: number): string => {
|
||||||
@@ -121,72 +81,13 @@ export function AdminAvailability() {
|
|||||||
return days[dayOfWeek];
|
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 (
|
return (
|
||||||
<div className="max-w-4xl mx-auto space-y-6">
|
<div className="max-w-4xl mx-auto space-y-6">
|
||||||
{/* Tab-Navigation */}
|
{/* Tab-Navigation (Slots entfernt) */}
|
||||||
<div className="bg-white rounded-lg shadow">
|
<div className="bg-white rounded-lg shadow">
|
||||||
<div className="border-b border-gray-200">
|
<div className="border-b border-gray-200">
|
||||||
<nav className="-mb-px flex space-x-8 px-6">
|
<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
|
<button
|
||||||
onClick={() => setActiveSubTab("recurring")}
|
onClick={() => setActiveSubTab("recurring")}
|
||||||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||||
@@ -211,316 +112,7 @@ export function AdminAvailability() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tab Inhalt */}
|
{/* Slot-Tab und Slot-UI entfernt */}
|
||||||
{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" */}
|
{/* Tab "Wiederkehrende Zeiten" */}
|
||||||
{activeSubTab === "recurring" && (
|
{activeSubTab === "recurring" && (
|
||||||
|
@@ -1,250 +0,0 @@
|
|||||||
import { call, os } from "@orpc/server";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { randomUUID } from "crypto";
|
|
||||||
import { createKV } from "../lib/create-kv.js";
|
|
||||||
|
|
||||||
const AvailabilitySchema = z.object({
|
|
||||||
id: z.string(),
|
|
||||||
date: z.string(), // YYYY-MM-DD
|
|
||||||
time: z.string(), // HH:MM
|
|
||||||
durationMinutes: z.number().int().positive(),
|
|
||||||
status: z.enum(["free", "reserved"]),
|
|
||||||
reservedByBookingId: z.string().optional(),
|
|
||||||
createdAt: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Availability = z.output<typeof AvailabilitySchema>;
|
|
||||||
|
|
||||||
const kv = createKV<Availability>("availability");
|
|
||||||
|
|
||||||
// Minimal Owner-Prüfung über Sessions/Users KV
|
|
||||||
type Session = { id: string; userId: string; expiresAt: string; createdAt: string };
|
|
||||||
type User = { id: string; username: string; email: string; passwordHash: string; role: "customer" | "owner"; createdAt: string };
|
|
||||||
const sessionsKV = createKV<Session>("sessions");
|
|
||||||
const usersKV = createKV<User>("users");
|
|
||||||
|
|
||||||
async function assertOwner(sessionId: string): Promise<void> {
|
|
||||||
const session = await sessionsKV.getItem(sessionId);
|
|
||||||
if (!session) throw new Error("Invalid session");
|
|
||||||
if (new Date(session.expiresAt) < new Date()) throw new Error("Session expired");
|
|
||||||
const user = await usersKV.getItem(session.userId);
|
|
||||||
if (!user || user.role !== "owner") throw new Error("Forbidden");
|
|
||||||
}
|
|
||||||
|
|
||||||
const create = os
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
sessionId: z.string(),
|
|
||||||
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
|
||||||
time: z.string().regex(/^\d{2}:\d{2}$/),
|
|
||||||
durationMinutes: z.number().int().positive(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.handler(async ({ input }) => {
|
|
||||||
try {
|
|
||||||
await assertOwner(input.sessionId);
|
|
||||||
// Prevent duplicate slot on same date+time
|
|
||||||
const existing = await kv.getAllItems();
|
|
||||||
const conflict = existing.some((s) => s.date === input.date && s.time === input.time);
|
|
||||||
if (conflict) {
|
|
||||||
throw new Error("Es existiert bereits ein Slot zu diesem Datum und dieser Uhrzeit.");
|
|
||||||
}
|
|
||||||
const id = randomUUID();
|
|
||||||
const slot: Availability = {
|
|
||||||
id,
|
|
||||||
date: input.date,
|
|
||||||
time: input.time,
|
|
||||||
durationMinutes: input.durationMinutes,
|
|
||||||
status: "free",
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
await kv.setItem(id, slot);
|
|
||||||
return slot;
|
|
||||||
} catch (err) {
|
|
||||||
console.error("availability.create error", err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const update = os
|
|
||||||
.input(AvailabilitySchema.extend({ sessionId: z.string() }))
|
|
||||||
.handler(async ({ input }) => {
|
|
||||||
await assertOwner(input.sessionId);
|
|
||||||
const { sessionId, ...rest } = input as any;
|
|
||||||
await kv.setItem(rest.id, rest as Availability);
|
|
||||||
return rest as Availability;
|
|
||||||
});
|
|
||||||
|
|
||||||
const remove = os
|
|
||||||
.input(z.object({ sessionId: z.string(), id: z.string() }))
|
|
||||||
.handler(async ({ input }) => {
|
|
||||||
await assertOwner(input.sessionId);
|
|
||||||
const slot = await kv.getItem(input.id);
|
|
||||||
if (slot && slot.status === "reserved") throw new Error("Cannot delete reserved slot");
|
|
||||||
await kv.removeItem(input.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
const list = os.handler(async () => {
|
|
||||||
const allSlots = await kv.getAllItems();
|
|
||||||
|
|
||||||
// Auto-delete past slots
|
|
||||||
const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
|
||||||
const now = new Date();
|
|
||||||
const currentTime = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
|
|
||||||
|
|
||||||
let deletedCount = 0;
|
|
||||||
const slotsToDelete: string[] = [];
|
|
||||||
|
|
||||||
// Identify past slots for deletion
|
|
||||||
allSlots.forEach(slot => {
|
|
||||||
const isPastDate = slot.date < today;
|
|
||||||
const isPastTime = slot.date === today && slot.time <= currentTime;
|
|
||||||
|
|
||||||
if (isPastDate || isPastTime) {
|
|
||||||
slotsToDelete.push(slot.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete past slots (only if not reserved)
|
|
||||||
for (const slotId of slotsToDelete) {
|
|
||||||
const slot = await kv.getItem(slotId);
|
|
||||||
if (slot && slot.status !== "reserved") {
|
|
||||||
await kv.removeItem(slotId);
|
|
||||||
deletedCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deletedCount > 0) {
|
|
||||||
console.log(`Auto-deleted ${deletedCount} past availability slots`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return remaining slots (all are now current/future)
|
|
||||||
const remainingSlots = allSlots.filter(slot => !slotsToDelete.includes(slot.id));
|
|
||||||
|
|
||||||
return remainingSlots;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Non-mutating function that returns all slots without auto-cleanup
|
|
||||||
const peekAll = os.handler(async () => {
|
|
||||||
const allSlots = await kv.getAllItems();
|
|
||||||
return allSlots;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Helper function to check if a date is in a time-off period
|
|
||||||
function isDateInTimeOffPeriod(date: string, periods: Array<{startDate: string, endDate: string}>): boolean {
|
|
||||||
return periods.some(period => date >= period.startDate && date <= period.endDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtered list that excludes slots in time-off periods (for customer-facing endpoints)
|
|
||||||
const listFiltered = os.handler(async () => {
|
|
||||||
const allSlots = await kv.getAllItems();
|
|
||||||
|
|
||||||
// Auto-delete past slots
|
|
||||||
const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
|
||||||
const now = new Date();
|
|
||||||
const currentTime = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
|
|
||||||
|
|
||||||
let deletedCount = 0;
|
|
||||||
const slotsToDelete: string[] = [];
|
|
||||||
|
|
||||||
// Identify past slots for deletion
|
|
||||||
allSlots.forEach(slot => {
|
|
||||||
const isPastDate = slot.date < today;
|
|
||||||
const isPastTime = slot.date === today && slot.time <= currentTime;
|
|
||||||
|
|
||||||
if (isPastDate || isPastTime) {
|
|
||||||
slotsToDelete.push(slot.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete past slots (only if not reserved)
|
|
||||||
for (const slotId of slotsToDelete) {
|
|
||||||
const slot = await kv.getItem(slotId);
|
|
||||||
if (slot && slot.status !== "reserved") {
|
|
||||||
await kv.removeItem(slotId);
|
|
||||||
deletedCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deletedCount > 0) {
|
|
||||||
console.log(`Auto-deleted ${deletedCount} past availability slots`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get remaining slots (all are now current/future)
|
|
||||||
let remainingSlots = allSlots.filter(slot => !slotsToDelete.includes(slot.id));
|
|
||||||
|
|
||||||
// Note: Time-off filtering would be added here if we had access to time-off periods
|
|
||||||
// For now, we'll rely on the slot generation logic to not create slots during time-off periods
|
|
||||||
|
|
||||||
return remainingSlots;
|
|
||||||
});
|
|
||||||
|
|
||||||
const get = os.input(z.string()).handler(async ({ input }) => {
|
|
||||||
return kv.getItem(input);
|
|
||||||
});
|
|
||||||
|
|
||||||
const getByDate = os
|
|
||||||
.input(z.string()) // YYYY-MM-DD
|
|
||||||
.handler(async ({ input }) => {
|
|
||||||
const all = await kv.getAllItems();
|
|
||||||
return all.filter((s) => s.date === input);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cleanup function to manually delete past slots
|
|
||||||
const cleanupPastSlots = os
|
|
||||||
.input(z.object({ sessionId: z.string() }))
|
|
||||||
.handler(async ({ input }) => {
|
|
||||||
await assertOwner(input.sessionId);
|
|
||||||
|
|
||||||
const allSlots = await kv.getAllItems();
|
|
||||||
const today = new Date().toISOString().split("T")[0];
|
|
||||||
const now = new Date();
|
|
||||||
const currentTime = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
|
|
||||||
|
|
||||||
let deletedCount = 0;
|
|
||||||
|
|
||||||
for (const slot of allSlots) {
|
|
||||||
const isPastDate = slot.date < today;
|
|
||||||
const isPastTime = slot.date === today && slot.time <= currentTime;
|
|
||||||
|
|
||||||
if ((isPastDate || isPastTime) && slot.status !== "reserved") {
|
|
||||||
await kv.removeItem(slot.id);
|
|
||||||
deletedCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Manual cleanup: deleted ${deletedCount} past availability slots`);
|
|
||||||
return { deletedCount, message: `${deletedCount} vergangene Slots wurden gelöscht.` };
|
|
||||||
});
|
|
||||||
|
|
||||||
const live = {
|
|
||||||
list: os.handler(async function* ({ signal }) {
|
|
||||||
yield call(list, {}, { signal });
|
|
||||||
for await (const _ of kv.subscribe()) {
|
|
||||||
yield call(list, {}, { signal });
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
byDate: os
|
|
||||||
.input(z.string())
|
|
||||||
.handler(async function* ({ input, signal }) {
|
|
||||||
yield call(getByDate, input, { signal });
|
|
||||||
for await (const _ of kv.subscribe()) {
|
|
||||||
yield call(getByDate, input, { signal });
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const router = {
|
|
||||||
create,
|
|
||||||
update,
|
|
||||||
remove,
|
|
||||||
list,
|
|
||||||
listFiltered,
|
|
||||||
peekAll,
|
|
||||||
get,
|
|
||||||
getByDate,
|
|
||||||
cleanupPastSlots,
|
|
||||||
live,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
@@ -2,7 +2,6 @@ import { demo } from "./demo/index.js";
|
|||||||
import { router as treatments } from "./treatments.js";
|
import { router as treatments } from "./treatments.js";
|
||||||
import { router as bookings } from "./bookings.js";
|
import { router as bookings } from "./bookings.js";
|
||||||
import { router as auth } from "./auth.js";
|
import { router as auth } from "./auth.js";
|
||||||
import { router as availability } from "./availability.js";
|
|
||||||
import { router as recurringAvailability } from "./recurring-availability.js";
|
import { router as recurringAvailability } from "./recurring-availability.js";
|
||||||
import { router as cancellation } from "./cancellation.js";
|
import { router as cancellation } from "./cancellation.js";
|
||||||
import { router as legal } from "./legal.js";
|
import { router as legal } from "./legal.js";
|
||||||
@@ -12,7 +11,6 @@ export const router = {
|
|||||||
treatments,
|
treatments,
|
||||||
bookings,
|
bookings,
|
||||||
auth,
|
auth,
|
||||||
availability,
|
|
||||||
recurringAvailability,
|
recurringAvailability,
|
||||||
cancellation,
|
cancellation,
|
||||||
legal,
|
legal,
|
||||||
|
@@ -30,14 +30,12 @@ export type TimeOffPeriod = z.output<typeof TimeOffPeriodSchema>;
|
|||||||
const recurringRulesKV = createKV<RecurringRule>("recurringRules");
|
const recurringRulesKV = createKV<RecurringRule>("recurringRules");
|
||||||
const timeOffPeriodsKV = createKV<TimeOffPeriod>("timeOffPeriods");
|
const timeOffPeriodsKV = createKV<TimeOffPeriod>("timeOffPeriods");
|
||||||
|
|
||||||
// Import existing availability KV
|
|
||||||
import { router as availabilityRouter } from "./availability.js";
|
|
||||||
|
|
||||||
// Import bookings and treatments KV stores for getAvailableTimes endpoint
|
// Import bookings and treatments KV stores for getAvailableTimes endpoint
|
||||||
const bookingsKV = createKV<any>("bookings");
|
const bookingsKV = createKV<any>("bookings");
|
||||||
const treatmentsKV = createKV<any>("treatments");
|
const treatmentsKV = createKV<any>("treatments");
|
||||||
|
|
||||||
// Owner-Authentifizierung (kopiert aus availability.ts)
|
// Owner-Authentifizierung
|
||||||
type Session = { id: string; userId: string; expiresAt: string; createdAt: string };
|
type Session = { id: string; userId: string; expiresAt: string; createdAt: string };
|
||||||
type User = { id: string; username: string; email: string; passwordHash: string; role: "customer" | "owner"; createdAt: string };
|
type User = { id: string; username: string; email: string; passwordHash: string; role: "customer" | "owner"; createdAt: string };
|
||||||
const sessionsKV = createKV<Session>("sessions");
|
const sessionsKV = createKV<Session>("sessions");
|
||||||
@@ -236,23 +234,8 @@ const createTimeOff = os
|
|||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Blockiere bestehende Slots in diesem Zeitraum
|
|
||||||
const existingSlots = await call(availabilityRouter.peekAll, {}, {});
|
|
||||||
let blockedCount = 0;
|
|
||||||
|
|
||||||
for (const slot of existingSlots) {
|
|
||||||
if (slot.date >= input.startDate && slot.date <= input.endDate && slot.status === "free") {
|
|
||||||
await call(availabilityRouter.remove, { sessionId: input.sessionId, id: slot.id }, {});
|
|
||||||
blockedCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (blockedCount > 0) {
|
|
||||||
console.log(`Blocked ${blockedCount} existing slots for time-off period: ${input.reason}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await timeOffPeriodsKV.setItem(id, timeOff);
|
await timeOffPeriodsKV.setItem(id, timeOff);
|
||||||
return { ...timeOff, blockedSlots: blockedCount };
|
return timeOff;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("recurring-availability.createTimeOff error", err);
|
console.error("recurring-availability.createTimeOff error", err);
|
||||||
throw err;
|
throw err;
|
||||||
@@ -294,153 +277,6 @@ const adminListTimeOff = os
|
|||||||
return allTimeOff.sort((a, b) => a.startDate.localeCompare(b.startDate));
|
return allTimeOff.sort((a, b) => a.startDate.localeCompare(b.startDate));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Slot-Generator-Endpoint
|
|
||||||
// DEPRECATED: This endpoint will be removed in a future version.
|
|
||||||
// The system is transitioning to dynamic availability calculation with 15-minute intervals.
|
|
||||||
// Slots are no longer pre-generated based on recurring rules.
|
|
||||||
const generateSlots = os
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
sessionId: z.string(),
|
|
||||||
startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
|
||||||
endDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
|
||||||
overwriteExisting: z.boolean().default(false),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.handler(async ({ input }) => {
|
|
||||||
try {
|
|
||||||
await assertOwner(input.sessionId);
|
|
||||||
|
|
||||||
// Validierung: startDate <= endDate
|
|
||||||
if (input.startDate > input.endDate) {
|
|
||||||
throw new Error("Startdatum muss vor oder am Enddatum liegen.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validierung: maximal 12 Wochen Zeitraum
|
|
||||||
const start = new Date(input.startDate);
|
|
||||||
const end = new Date(input.endDate);
|
|
||||||
const daysDiff = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
|
|
||||||
if (daysDiff > 84) { // 12 Wochen = 84 Tage
|
|
||||||
throw new Error("Zeitraum darf maximal 12 Wochen betragen.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lade alle aktiven Regeln
|
|
||||||
const allRules = await recurringRulesKV.getAllItems();
|
|
||||||
const activeRules = allRules.filter(rule => rule.isActive);
|
|
||||||
|
|
||||||
// Lade alle Urlaubszeiten
|
|
||||||
const timeOffPeriods = await timeOffPeriodsKV.getAllItems();
|
|
||||||
|
|
||||||
// Lade bestehende Slots (ohne Auto-Cleanup)
|
|
||||||
const existingSlots = await call(availabilityRouter.peekAll, {}, {});
|
|
||||||
|
|
||||||
// Erstelle Set für effiziente Duplikat-Prüfung
|
|
||||||
const existing = new Set(existingSlots.map(s => `${s.date}T${s.time}`));
|
|
||||||
|
|
||||||
let created = 0;
|
|
||||||
let skipped = 0;
|
|
||||||
let updated = 0;
|
|
||||||
|
|
||||||
// Iteriere über jeden Tag im Zeitraum
|
|
||||||
const currentDate = new Date(start);
|
|
||||||
while (currentDate <= end) {
|
|
||||||
const dateStr = formatDate(currentDate);
|
|
||||||
// Verwende lokale Datumskomponenten für korrekte Wochentag-Berechnung
|
|
||||||
const [y, m, d] = dateStr.split('-').map(Number);
|
|
||||||
const localDate = new Date(y, m - 1, d);
|
|
||||||
const dayOfWeek = localDate.getDay(); // 0=Sonntag, 1=Montag, ...
|
|
||||||
|
|
||||||
// Prüfe, ob Datum in einer Urlaubszeit liegt
|
|
||||||
if (isDateInTimeOffPeriod(dateStr, timeOffPeriods)) {
|
|
||||||
currentDate.setDate(currentDate.getDate() + 1);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finde alle Regeln für diesen Wochentag
|
|
||||||
const matchingRules = activeRules.filter(rule => rule.dayOfWeek === dayOfWeek);
|
|
||||||
|
|
||||||
for (const rule of matchingRules) {
|
|
||||||
// Skip rules without slotDurationMinutes (legacy field for deprecated generateSlots)
|
|
||||||
if (!rule.slotDurationMinutes) {
|
|
||||||
console.log(`Skipping rule ${rule.id} - no slotDurationMinutes defined (legacy field)`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startMinutes = parseTime(rule.startTime);
|
|
||||||
const endMinutes = parseTime(rule.endTime);
|
|
||||||
|
|
||||||
// Generiere Slots in slotDurationMinutes-Schritten
|
|
||||||
let currentMinutes = startMinutes;
|
|
||||||
while (currentMinutes + rule.slotDurationMinutes <= endMinutes) {
|
|
||||||
const timeStr = formatTime(currentMinutes);
|
|
||||||
const key = `${dateStr}T${timeStr}`;
|
|
||||||
|
|
||||||
// Prüfe, ob bereits ein Slot für dieses Datum+Zeit existiert
|
|
||||||
if (existing.has(key)) {
|
|
||||||
if (input.overwriteExisting) {
|
|
||||||
// Finde den bestehenden Slot für Update
|
|
||||||
const existingSlot = existingSlots.find(
|
|
||||||
slot => slot.date === dateStr && slot.time === timeStr
|
|
||||||
);
|
|
||||||
if (existingSlot && existingSlot.status === "free") {
|
|
||||||
// Überschreibe Dauer des bestehenden Slots
|
|
||||||
const updatedSlot = {
|
|
||||||
...existingSlot,
|
|
||||||
durationMinutes: rule.slotDurationMinutes,
|
|
||||||
};
|
|
||||||
await call(availabilityRouter.update, {
|
|
||||||
sessionId: input.sessionId,
|
|
||||||
...updatedSlot
|
|
||||||
}, {});
|
|
||||||
updated++;
|
|
||||||
} else {
|
|
||||||
skipped++;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
skipped++;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Erstelle neuen Slot mit try/catch für Duplikat-Konflikte
|
|
||||||
try {
|
|
||||||
await call(availabilityRouter.create, {
|
|
||||||
sessionId: input.sessionId,
|
|
||||||
date: dateStr,
|
|
||||||
time: timeStr,
|
|
||||||
durationMinutes: rule.slotDurationMinutes,
|
|
||||||
}, {});
|
|
||||||
existing.add(key);
|
|
||||||
created++;
|
|
||||||
} catch (err: any) {
|
|
||||||
// Behandle bekannte Duplikat-Fehler
|
|
||||||
if (err.message && err.message.includes("bereits ein Slot")) {
|
|
||||||
skipped++;
|
|
||||||
} else {
|
|
||||||
throw err; // Re-throw unbekannte Fehler
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentMinutes += rule.slotDurationMinutes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentDate.setDate(currentDate.getDate() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = `${created} Slots erstellt, ${updated} aktualisiert, ${skipped} übersprungen.`;
|
|
||||||
console.log(`Slot generation completed: ${message}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
created,
|
|
||||||
updated,
|
|
||||||
skipped,
|
|
||||||
message,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
console.error("recurring-availability.generateSlots error", err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get Available Times Endpoint
|
// Get Available Times Endpoint
|
||||||
const getAvailableTimes = os
|
const getAvailableTimes = os
|
||||||
@@ -643,9 +479,6 @@ export const router = {
|
|||||||
listTimeOff,
|
listTimeOff,
|
||||||
adminListTimeOff,
|
adminListTimeOff,
|
||||||
|
|
||||||
// Generator
|
|
||||||
generateSlots,
|
|
||||||
|
|
||||||
// Availability
|
// Availability
|
||||||
getAvailableTimes,
|
getAvailableTimes,
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user