@voyantjs/availability-react 0.106.0 → 0.108.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -1
- package/dist/admin/availability-index-host.d.ts +12 -0
- package/dist/admin/availability-index-host.d.ts.map +1 -0
- package/dist/admin/availability-index-host.js +125 -0
- package/dist/admin/availability-page-data.d.ts +9 -0
- package/dist/admin/availability-page-data.d.ts.map +1 -0
- package/dist/admin/availability-page-data.js +25 -0
- package/dist/admin/index.d.ts +72 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +132 -0
- package/dist/admin/option-resource-templates-panel.d.ts +22 -0
- package/dist/admin/option-resource-templates-panel.d.ts.map +1 -0
- package/dist/admin/option-resource-templates-panel.js +251 -0
- package/dist/admin/pages/availability-rule-detail-page.d.ts +9 -0
- package/dist/admin/pages/availability-rule-detail-page.d.ts.map +1 -0
- package/dist/admin/pages/availability-rule-detail-page.js +11 -0
- package/dist/admin/pages/availability-slot-detail-page.d.ts +9 -0
- package/dist/admin/pages/availability-slot-detail-page.d.ts.map +1 -0
- package/dist/admin/pages/availability-slot-detail-page.js +11 -0
- package/dist/admin/pages/availability-start-time-detail-page.d.ts +9 -0
- package/dist/admin/pages/availability-start-time-detail-page.d.ts.map +1 -0
- package/dist/admin/pages/availability-start-time-detail-page.js +11 -0
- package/dist/admin/rule-detail-host.d.ts +14 -0
- package/dist/admin/rule-detail-host.d.ts.map +1 -0
- package/dist/admin/rule-detail-host.js +27 -0
- package/dist/admin/slot-detail-host.d.ts +29 -0
- package/dist/admin/slot-detail-host.d.ts.map +1 -0
- package/dist/admin/slot-detail-host.js +110 -0
- package/dist/admin/start-time-detail-host.d.ts +15 -0
- package/dist/admin/start-time-detail-host.d.ts.map +1 -0
- package/dist/admin/start-time-detail-host.js +37 -0
- package/dist/components/availability-columns.d.ts +42 -0
- package/dist/components/availability-columns.d.ts.map +1 -0
- package/dist/components/availability-columns.js +182 -0
- package/dist/components/availability-dialogs.d.ts +236 -0
- package/dist/components/availability-dialogs.d.ts.map +1 -0
- package/dist/components/availability-dialogs.js +369 -0
- package/dist/components/availability-overview.d.ts +54 -0
- package/dist/components/availability-overview.d.ts.map +1 -0
- package/dist/components/availability-overview.js +50 -0
- package/dist/components/availability-page.d.ts +32 -0
- package/dist/components/availability-page.d.ts.map +1 -0
- package/dist/components/availability-page.js +128 -0
- package/dist/components/availability-rule-detail-page.d.ts +251 -0
- package/dist/components/availability-rule-detail-page.d.ts.map +1 -0
- package/dist/components/availability-rule-detail-page.js +74 -0
- package/dist/components/availability-section-header.d.ts +8 -0
- package/dist/components/availability-section-header.d.ts.map +1 -0
- package/dist/components/availability-section-header.js +7 -0
- package/dist/components/availability-skeletons.d.ts +6 -0
- package/dist/components/availability-skeletons.d.ts.map +1 -0
- package/dist/components/availability-skeletons.js +34 -0
- package/dist/components/availability-slot-detail-page.d.ts +974 -0
- package/dist/components/availability-slot-detail-page.d.ts.map +1 -0
- package/dist/components/availability-slot-detail-page.js +383 -0
- package/dist/components/availability-start-time-detail-page.d.ts +246 -0
- package/dist/components/availability-start-time-detail-page.d.ts.map +1 -0
- package/dist/components/availability-start-time-detail-page.js +83 -0
- package/dist/components/availability-tabs.d.ts +152 -0
- package/dist/components/availability-tabs.d.ts.map +1 -0
- package/dist/components/availability-tabs.js +192 -0
- package/dist/components/slot-status-tone.d.ts +15 -0
- package/dist/components/slot-status-tone.d.ts.map +1 -0
- package/dist/components/slot-status-tone.js +18 -0
- package/dist/form-resolver.d.ts +4 -0
- package/dist/form-resolver.d.ts.map +1 -0
- package/dist/form-resolver.js +40 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +1 -0
- package/dist/i18n/provider.d.ts +2003 -0
- package/dist/i18n/provider.d.ts.map +1 -0
- package/dist/i18n/provider.js +102 -0
- package/dist/ui.d.ts +13 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +12 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +3 -0
- package/package.json +92 -9
- package/src/styles.css +11 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useOperatorAdminMessages } from "@voyantjs/admin";
|
|
4
|
+
import { SeatMapBuilder } from "@voyantjs/allocation-ui";
|
|
5
|
+
import { formatMessage } from "@voyantjs/i18n";
|
|
6
|
+
import { useOptionUnits } from "@voyantjs/products-react";
|
|
7
|
+
import { Badge, Button, Collapsible, CollapsibleContent, CollapsibleTrigger, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components";
|
|
8
|
+
import { Armchair, Bed, CalendarCheck, ChevronDown, ChevronRight, Loader2, Pencil, Plus, Sparkles, Trash2, } from "lucide-react";
|
|
9
|
+
import { useMemo, useState } from "react";
|
|
10
|
+
import { seatLayoutSpecSchema, useMaterializeOpenSlotsMutation, useProductResourceTemplates, useResourceTemplateMutation, } from "../index.js";
|
|
11
|
+
// Defaults populate the namePattern field when the operator picks a kind. They
|
|
12
|
+
// are starting points the operator edits before saving, not display labels —
|
|
13
|
+
// the localized prompt copy lives in the dialog's namePatternPlaceholder.
|
|
14
|
+
const COMMON_KINDS = [
|
|
15
|
+
// i18n-literal-ok
|
|
16
|
+
{ value: "room", defaultPattern: "Room {sequence}" },
|
|
17
|
+
// i18n-literal-ok
|
|
18
|
+
{ value: "vehicle_seat", defaultPattern: "Seat {sequence}" },
|
|
19
|
+
// i18n-literal-ok
|
|
20
|
+
{ value: "cabin", defaultPattern: "Cabin {sequence}" },
|
|
21
|
+
// i18n-literal-ok
|
|
22
|
+
{ value: "flight_seat", defaultPattern: "Seat {sequence}" },
|
|
23
|
+
];
|
|
24
|
+
export function OptionResourceTemplatesPanel({ productId, optionId, }) {
|
|
25
|
+
const adminMessages = useOperatorAdminMessages();
|
|
26
|
+
const t = adminMessages.availability.details.resourceTemplates;
|
|
27
|
+
const { data, isPending, isError } = useProductResourceTemplates({ productId });
|
|
28
|
+
const { upsert, remove } = useResourceTemplateMutation(productId);
|
|
29
|
+
const materializeOpenSlots = useMaterializeOpenSlotsMutation(productId);
|
|
30
|
+
const templates = useMemo(() => {
|
|
31
|
+
const option = (data?.data ?? []).find((entry) => entry.id === optionId);
|
|
32
|
+
return option?.templates ?? [];
|
|
33
|
+
}, [data?.data, optionId]);
|
|
34
|
+
const totalPerDeparture = useMemo(() => templates.reduce((sum, template) => sum + (template.defaultCount ?? 0), 0), [templates]);
|
|
35
|
+
// The option's room units already carry quantity (maxQuantity) and occupancy
|
|
36
|
+
// — generate departure inventory straight from them instead of re-typing it.
|
|
37
|
+
const { data: unitsData } = useOptionUnits({ optionId, limit: 100 });
|
|
38
|
+
const roomUnits = useMemo(() => (unitsData?.data ?? []).filter((unit) => unit.unitType === "room"), [unitsData?.data]);
|
|
39
|
+
const [open, setOpen] = useState(false);
|
|
40
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
41
|
+
const [seatMapOpen, setSeatMapOpen] = useState(false);
|
|
42
|
+
const [editingKind, setEditingKind] = useState(null);
|
|
43
|
+
// The ref of the template being edited, so submit updates that exact
|
|
44
|
+
// (option, kind, refId) row rather than colliding with siblings of the same
|
|
45
|
+
// kind. Null for ref-less templates (the manual "Add" path).
|
|
46
|
+
const [editingRefType, setEditingRefType] = useState(null);
|
|
47
|
+
const [editingRefId, setEditingRefId] = useState(null);
|
|
48
|
+
const [kindValue, setKindValue] = useState("room");
|
|
49
|
+
const [capacityValue, setCapacityValue] = useState(2);
|
|
50
|
+
const [defaultCountValue, setDefaultCountValue] = useState(1);
|
|
51
|
+
const [namePatternValue, setNamePatternValue] = useState("Room {sequence}");
|
|
52
|
+
const [layoutSpec, setLayoutSpec] = useState(null);
|
|
53
|
+
const [error, setError] = useState(null);
|
|
54
|
+
const [applyResult, setApplyResult] = useState(null);
|
|
55
|
+
const derivedSeatCount = useMemo(() => countSeats(layoutSpec), [layoutSpec]);
|
|
56
|
+
const usingSeatMap = kindValue === "vehicle_seat" && layoutSpec !== null;
|
|
57
|
+
function openCreate() {
|
|
58
|
+
setEditingKind(null);
|
|
59
|
+
setEditingRefType(null);
|
|
60
|
+
setEditingRefId(null);
|
|
61
|
+
setKindValue("room");
|
|
62
|
+
setCapacityValue(2);
|
|
63
|
+
setDefaultCountValue(1);
|
|
64
|
+
setNamePatternValue("Room {sequence}");
|
|
65
|
+
setLayoutSpec(null);
|
|
66
|
+
setError(null);
|
|
67
|
+
setDialogOpen(true);
|
|
68
|
+
}
|
|
69
|
+
function openEdit(template) {
|
|
70
|
+
setEditingKind(template.kind);
|
|
71
|
+
setEditingRefType(template.refType);
|
|
72
|
+
setEditingRefId(template.refId);
|
|
73
|
+
setKindValue(template.kind);
|
|
74
|
+
setCapacityValue(template.capacity);
|
|
75
|
+
setDefaultCountValue(template.defaultCount ?? 0);
|
|
76
|
+
setNamePatternValue(template.namePattern);
|
|
77
|
+
setLayoutSpec(extractLayoutSpec(template.flags));
|
|
78
|
+
setError(null);
|
|
79
|
+
setDialogOpen(true);
|
|
80
|
+
}
|
|
81
|
+
async function submit(event) {
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
setError(null);
|
|
84
|
+
const trimmedKind = kindValue.trim();
|
|
85
|
+
const trimmedPattern = namePatternValue.trim();
|
|
86
|
+
const effectiveCapacity = usingSeatMap ? derivedSeatCount : capacityValue;
|
|
87
|
+
if (!trimmedKind || !trimmedPattern || effectiveCapacity < 1) {
|
|
88
|
+
setError(t.validation);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const flags = {};
|
|
92
|
+
if (trimmedKind === "vehicle_seat" && layoutSpec) {
|
|
93
|
+
flags.layoutSpec = layoutSpec;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
await upsert.mutateAsync({
|
|
97
|
+
optionId,
|
|
98
|
+
kind: trimmedKind,
|
|
99
|
+
input: {
|
|
100
|
+
capacity: effectiveCapacity,
|
|
101
|
+
defaultCount: defaultCountValue > 0 ? defaultCountValue : null,
|
|
102
|
+
namePattern: trimmedPattern,
|
|
103
|
+
// Preserve the edited template's ref so the upsert targets its exact
|
|
104
|
+
// (option, kind, refId) row instead of clobbering a sibling.
|
|
105
|
+
refType: editingRefType,
|
|
106
|
+
refId: editingRefId,
|
|
107
|
+
flags,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
setDialogOpen(false);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
setError(err instanceof Error ? err.message : t.saveFailed);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function handleRemove(kind, refId, label) {
|
|
117
|
+
if (!globalThis.confirm?.(formatMessage(t.deleteConfirm, { kind: label })))
|
|
118
|
+
return;
|
|
119
|
+
try {
|
|
120
|
+
await remove.mutateAsync({ optionId, kind, refId });
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
setError(err instanceof Error ? err.message : t.deleteFailed);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function generateFromRooms() {
|
|
127
|
+
setError(null);
|
|
128
|
+
try {
|
|
129
|
+
for (const unit of roomUnits) {
|
|
130
|
+
await upsert.mutateAsync({
|
|
131
|
+
optionId,
|
|
132
|
+
// All room types share kind "room"; they're distinguished — and
|
|
133
|
+
// travelers are constrained to their booked type — by the option_unit
|
|
134
|
+
// ref (allocator's groupUnitMatchScore: refType "option_unit" + refId
|
|
135
|
+
// === bookedOptionUnitId). The widened (option, kind, ref) unique
|
|
136
|
+
// index lets one option carry a "room" template per unit.
|
|
137
|
+
kind: "room",
|
|
138
|
+
input: {
|
|
139
|
+
capacity: unit.occupancyMax ?? unit.occupancyMin ?? 1,
|
|
140
|
+
defaultCount: unit.maxQuantity ?? null,
|
|
141
|
+
// {index} numbers each room type from 1 (Double 1…20), not the
|
|
142
|
+
// global {sequence}, so the shared "room" pool reads cleanly.
|
|
143
|
+
namePattern: `${unit.name} {index}`,
|
|
144
|
+
refType: "option_unit",
|
|
145
|
+
refId: unit.id,
|
|
146
|
+
flags: {},
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
setOpen(true);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
setError(err instanceof Error ? err.message : t.saveFailed);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async function applyToOpenDepartures() {
|
|
157
|
+
if (!globalThis.confirm?.(t.applyToOpenConfirm))
|
|
158
|
+
return;
|
|
159
|
+
setError(null);
|
|
160
|
+
setApplyResult(null);
|
|
161
|
+
try {
|
|
162
|
+
const result = await materializeOpenSlots.mutateAsync({ optionId });
|
|
163
|
+
setApplyResult(result.slots === 0
|
|
164
|
+
? t.applyToOpenEmpty
|
|
165
|
+
: formatMessage(t.applyToOpenResult, {
|
|
166
|
+
created: result.created,
|
|
167
|
+
slots: result.slots,
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
setError(err instanceof Error ? err.message : t.applyToOpenFailed);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const matchingCommon = COMMON_KINDS.find((entry) => entry.value === kindValue);
|
|
175
|
+
const isExtendedKind = !matchingCommon && kindValue.trim().length > 0;
|
|
176
|
+
return (_jsxs(Collapsible, { open: open, onOpenChange: setOpen, children: [_jsxs("div", { className: "rounded-md border bg-background/60", children: [_jsxs(CollapsibleTrigger, { className: "flex w-full items-center gap-2 px-3 py-2.5 text-left", children: [_jsx(Bed, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": "true" }), _jsx("span", { className: "font-medium text-sm", children: t.title }), _jsx(Badge, { variant: "secondary", className: "font-normal", children: templates.length === 0
|
|
177
|
+
? t.collapsedEmpty
|
|
178
|
+
: formatMessage(t.collapsedSummary, {
|
|
179
|
+
count: templates.length,
|
|
180
|
+
total: totalPerDeparture,
|
|
181
|
+
}) }), _jsx("span", { className: "flex-1" }), open ? (_jsx(ChevronDown, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": "true" })) : (_jsx(ChevronRight, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": "true" }))] }), _jsx(CollapsibleContent, { children: _jsxs("div", { className: "flex flex-col gap-3 border-t p-3", children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsx("p", { className: "text-muted-foreground text-xs", children: t.description }), _jsxs("div", { className: "flex shrink-0 flex-wrap items-center justify-end gap-2", children: [templates.length > 0 ? (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => void applyToOpenDepartures(), disabled: materializeOpenSlots.isPending, children: [_jsx(CalendarCheck, { className: "mr-1 size-4", "aria-hidden": "true" }), t.applyToOpenButton] })) : null, roomUnits.length > 0 ? (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => void generateFromRooms(), disabled: upsert.isPending, children: [_jsx(Sparkles, { className: "mr-1 size-4", "aria-hidden": "true" }), t.generateFromRooms] })) : null, _jsxs(Button, { size: "sm", variant: "outline", onClick: openCreate, children: [_jsx(Plus, { className: "mr-1 size-4", "aria-hidden": "true" }), t.addButton] })] })] }), applyResult ? (_jsx("p", { className: "rounded-md bg-muted/50 px-3 py-2 text-muted-foreground text-xs", children: applyResult })) : null, error ? _jsx("p", { className: "text-destructive text-xs", children: error }) : null, isPending ? (_jsx("p", { className: "flex items-center justify-center gap-2 py-4 text-muted-foreground text-sm", children: _jsx(Loader2, { className: "size-3.5 animate-spin" }) })) : isError ? (_jsx("p", { className: "py-4 text-center text-destructive text-sm", children: t.loadFailed })) : templates.length === 0 ? (_jsx("p", { className: "rounded-md border border-dashed px-3 py-4 text-center text-muted-foreground text-xs", children: roomUnits.length > 0 ? t.generateFromRoomsHint : t.emptyMessage })) : (_jsx("ul", { className: "divide-y overflow-hidden rounded-md border", children: templates.map((template) => {
|
|
182
|
+
const kindLabel = t.kinds[template.kind] ?? template.kind;
|
|
183
|
+
// Several "room" templates share a kind but map to different
|
|
184
|
+
// option_units — surface the unit name so the operator can
|
|
185
|
+
// tell Single / Double / Triple rows apart.
|
|
186
|
+
const unit = template.refId
|
|
187
|
+
? roomUnits.find((entry) => entry.id === template.refId)
|
|
188
|
+
: undefined;
|
|
189
|
+
const displayLabel = unit?.name ?? kindLabel;
|
|
190
|
+
return (_jsxs("li", { className: "flex items-center justify-between gap-3 px-3 py-2.5", children: [_jsx("div", { className: "min-w-0 flex-1", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Badge, { variant: "outline", className: "text-[10px]", children: displayLabel }), _jsx("span", { className: "text-sm", children: formatMessage(t.capacitySummary, {
|
|
191
|
+
capacity: template.capacity,
|
|
192
|
+
count: template.defaultCount ?? 0,
|
|
193
|
+
pattern: template.namePattern,
|
|
194
|
+
}) }), template.kind === "vehicle_seat" ? (_jsx(SeatMapSummaryBadge, { flags: template.flags, messages: t })) : null] }) }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { size: "sm", variant: "ghost", className: "h-7 px-2", onClick: () => openEdit({
|
|
195
|
+
kind: template.kind,
|
|
196
|
+
refType: template.refType,
|
|
197
|
+
refId: template.refId,
|
|
198
|
+
capacity: template.capacity,
|
|
199
|
+
defaultCount: template.defaultCount,
|
|
200
|
+
namePattern: template.namePattern,
|
|
201
|
+
flags: template.flags,
|
|
202
|
+
}), children: _jsx(Pencil, { className: "size-3.5", "aria-hidden": "true" }) }), _jsx(Button, { size: "sm", variant: "ghost", className: "h-7 px-2 text-destructive hover:text-destructive", onClick: () => void handleRemove(template.kind, template.refId, displayLabel), disabled: remove.isPending, children: _jsx(Trash2, { className: "size-3.5", "aria-hidden": "true" }) })] })] }, template.id));
|
|
203
|
+
}) }))] }) })] }), _jsx(Dialog, { open: dialogOpen, onOpenChange: setDialogOpen, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: editingKind ? formatMessage(t.editTitle, { kind: editingKind }) : t.newTitle }) }), _jsxs("form", { onSubmit: submit, children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "resource-template-kind", children: t.kindLabel }), _jsxs(Select, { value: isExtendedKind ? "__custom__" : kindValue, onValueChange: (value) => {
|
|
204
|
+
if (!value || value === "__custom__") {
|
|
205
|
+
setKindValue("");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
setKindValue(value);
|
|
209
|
+
const found = COMMON_KINDS.find((entry) => entry.value === value);
|
|
210
|
+
if (found && namePatternValue === "Room {sequence}") {
|
|
211
|
+
setNamePatternValue(found.defaultPattern);
|
|
212
|
+
}
|
|
213
|
+
}, disabled: editingKind !== null, children: [_jsx(SelectTrigger, { id: "resource-template-kind", className: "w-full", children: _jsx(SelectValue, { placeholder: t.kindPlaceholder }) }), _jsxs(SelectContent, { children: [COMMON_KINDS.map((entry) => (_jsx(SelectItem, { value: entry.value, children: t.kinds[entry.value] }, entry.value))), _jsx(SelectItem, { value: "__custom__", children: t.kindCustomOption })] })] }), isExtendedKind || editingKind ? (_jsx(Input, { value: kindValue, onChange: (event) => setKindValue(event.target.value), placeholder: t.kindCustomInputPlaceholder, disabled: editingKind !== null })) : null] }), kindValue === "vehicle_seat" ? (_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { children: t.seatMapLabel }), layoutSpec ? (_jsxs("div", { className: "flex items-center gap-2 rounded-md border p-3", children: [_jsx(Armchair, { className: "size-4 text-muted-foreground", "aria-hidden": "true" }), _jsx("div", { className: "min-w-0 flex-1 text-sm", children: formatMessage(t.seatMapSummary, {
|
|
214
|
+
rows: layoutSpec.rows.length,
|
|
215
|
+
count: derivedSeatCount,
|
|
216
|
+
}) }), _jsxs(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => setSeatMapOpen(true), children: [_jsx(Pencil, { className: "mr-1 size-3.5", "aria-hidden": "true" }), t.seatMapEditButton] })] })) : (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border border-dashed p-3 text-sm", children: [_jsx("p", { className: "text-muted-foreground", children: t.seatMapEmpty }), _jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: () => setSeatMapOpen(true), children: [_jsx(Armchair, { className: "mr-1 size-3.5", "aria-hidden": "true" }), t.seatMapEditButton] })] }))] })) : null, _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "resource-template-capacity", children: t.capacityLabel }), _jsx(Input, { id: "resource-template-capacity", type: "number", min: 1, value: usingSeatMap ? derivedSeatCount : capacityValue, onChange: (event) => setCapacityValue(Number(event.target.value) || 1), disabled: usingSeatMap }), usingSeatMap ? (_jsx("p", { className: "text-muted-foreground text-xs", children: formatMessage(t.capacityDerivedHint, { count: derivedSeatCount }) })) : null] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "resource-template-count", children: t.defaultCountLabel }), _jsx(Input, { id: "resource-template-count", type: "number", min: 0, value: defaultCountValue, onChange: (event) => setDefaultCountValue(Number(event.target.value) || 0) }), _jsx("p", { className: "text-muted-foreground text-xs", children: t.defaultCountHint })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "resource-template-pattern", children: t.namePatternLabel }), _jsx(Input, { id: "resource-template-pattern", value: namePatternValue, onChange: (event) => setNamePatternValue(event.target.value), placeholder: t.namePatternPlaceholder }), _jsx("p", { className: "text-muted-foreground text-xs", children: (() => {
|
|
217
|
+
const [before, after] = t.namePatternHint.split("{placeholder}");
|
|
218
|
+
return (_jsxs(_Fragment, { children: [before, _jsx("code", { children: "{sequence}" }), after] }));
|
|
219
|
+
})() })] }), error ? _jsx("p", { className: "text-destructive text-xs", children: error }) : null] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => setDialogOpen(false), children: t.cancel }), _jsxs(Button, { type: "submit", disabled: upsert.isPending, children: [upsert.isPending ? (_jsx(Loader2, { className: "mr-1 size-3.5 animate-spin", "aria-hidden": "true" })) : null, editingKind ? t.save : t.createButton] })] })] })] }) }), _jsx(Dialog, { open: seatMapOpen, onOpenChange: setSeatMapOpen, children: _jsxs(DialogContent, { className: "max-w-3xl", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: t.seatMapDialogTitle }) }), _jsx(DialogBody, { children: _jsx(SeatMapBuilder, { value: layoutSpec, onChange: setLayoutSpec }) }), _jsx(DialogFooter, { children: _jsx(Button, { type: "button", onClick: () => setSeatMapOpen(false), children: t.seatMapDialogDone }) })] }) })] }));
|
|
220
|
+
}
|
|
221
|
+
function SeatMapSummaryBadge({ flags, messages, }) {
|
|
222
|
+
const spec = extractLayoutSpec(flags);
|
|
223
|
+
if (!spec)
|
|
224
|
+
return null;
|
|
225
|
+
return (_jsx(Badge, { variant: "outline", className: "text-[10px]", children: formatMessage(messages.seatMapSummary, {
|
|
226
|
+
rows: spec.rows.length,
|
|
227
|
+
count: countSeats(spec),
|
|
228
|
+
}) }));
|
|
229
|
+
}
|
|
230
|
+
function extractLayoutSpec(flags) {
|
|
231
|
+
const raw = flags?.layoutSpec;
|
|
232
|
+
if (!raw)
|
|
233
|
+
return null;
|
|
234
|
+
// Validate against the schema rather than trust the shape — the server
|
|
235
|
+
// stores flags as opaque JSON, so a malformed value (e.g. a row missing
|
|
236
|
+
// `cells`) could otherwise reach `countSeats` and throw at runtime.
|
|
237
|
+
const parsed = seatLayoutSpecSchema.safeParse(raw);
|
|
238
|
+
return parsed.success ? parsed.data : null;
|
|
239
|
+
}
|
|
240
|
+
function countSeats(spec) {
|
|
241
|
+
if (!spec)
|
|
242
|
+
return 0;
|
|
243
|
+
let count = 0;
|
|
244
|
+
for (const row of spec.rows) {
|
|
245
|
+
for (const cell of row.cells) {
|
|
246
|
+
if (cell === "seat")
|
|
247
|
+
count += 1;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return count;
|
|
251
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AdminRoutePageProps } from "@voyantjs/admin";
|
|
2
|
+
/**
|
|
3
|
+
* Param-taking page for the `availability-rule-detail` contribution: reads
|
|
4
|
+
* the rule id off {@link AdminRoutePageProps} and binds it onto the packaged
|
|
5
|
+
* host. Resolved lazily through the contribution's `page` loader so the
|
|
6
|
+
* detail page lands in its own chunk.
|
|
7
|
+
*/
|
|
8
|
+
export default function AvailabilityRuleDetailRoutePage({ params }: AdminRoutePageProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
//# sourceMappingURL=availability-rule-detail-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"availability-rule-detail-page.d.ts","sourceRoot":"","sources":["../../../src/admin/pages/availability-rule-detail-page.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAI1D;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,+BAA+B,CAAC,EAAE,MAAM,EAAE,EAAE,mBAAmB,2CAEtF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { AvailabilityRuleDetailHost } from "../rule-detail-host.js";
|
|
3
|
+
/**
|
|
4
|
+
* Param-taking page for the `availability-rule-detail` contribution: reads
|
|
5
|
+
* the rule id off {@link AdminRoutePageProps} and binds it onto the packaged
|
|
6
|
+
* host. Resolved lazily through the contribution's `page` loader so the
|
|
7
|
+
* detail page lands in its own chunk.
|
|
8
|
+
*/
|
|
9
|
+
export default function AvailabilityRuleDetailRoutePage({ params }) {
|
|
10
|
+
return _jsx(AvailabilityRuleDetailHost, { ruleId: params.id ?? "" });
|
|
11
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AdminRoutePageProps } from "@voyantjs/admin";
|
|
2
|
+
/**
|
|
3
|
+
* Param-taking page for the `availability-slot-detail` contribution: reads
|
|
4
|
+
* the slot id off {@link AdminRoutePageProps} and binds it onto the packaged
|
|
5
|
+
* host. Resolved lazily through the contribution's `page` loader so the
|
|
6
|
+
* detail page lands in its own chunk.
|
|
7
|
+
*/
|
|
8
|
+
export default function AvailabilitySlotDetailRoutePage({ params }: AdminRoutePageProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
//# sourceMappingURL=availability-slot-detail-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"availability-slot-detail-page.d.ts","sourceRoot":"","sources":["../../../src/admin/pages/availability-slot-detail-page.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAI1D;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,+BAA+B,CAAC,EAAE,MAAM,EAAE,EAAE,mBAAmB,2CAEtF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { AvailabilitySlotDetailHost } from "../slot-detail-host.js";
|
|
3
|
+
/**
|
|
4
|
+
* Param-taking page for the `availability-slot-detail` contribution: reads
|
|
5
|
+
* the slot id off {@link AdminRoutePageProps} and binds it onto the packaged
|
|
6
|
+
* host. Resolved lazily through the contribution's `page` loader so the
|
|
7
|
+
* detail page lands in its own chunk.
|
|
8
|
+
*/
|
|
9
|
+
export default function AvailabilitySlotDetailRoutePage({ params }) {
|
|
10
|
+
return _jsx(AvailabilitySlotDetailHost, { slotId: params.id ?? "" });
|
|
11
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AdminRoutePageProps } from "@voyantjs/admin";
|
|
2
|
+
/**
|
|
3
|
+
* Param-taking page for the `availability-start-time-detail` contribution:
|
|
4
|
+
* reads the start time id off {@link AdminRoutePageProps} and binds it onto
|
|
5
|
+
* the packaged host. Resolved lazily through the contribution's `page`
|
|
6
|
+
* loader so the detail page lands in its own chunk.
|
|
7
|
+
*/
|
|
8
|
+
export default function AvailabilityStartTimeDetailRoutePage({ params }: AdminRoutePageProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
//# sourceMappingURL=availability-start-time-detail-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"availability-start-time-detail-page.d.ts","sourceRoot":"","sources":["../../../src/admin/pages/availability-start-time-detail-page.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAI1D;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,oCAAoC,CAAC,EAAE,MAAM,EAAE,EAAE,mBAAmB,2CAE3F"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { AvailabilityStartTimeDetailHost } from "../start-time-detail-host.js";
|
|
3
|
+
/**
|
|
4
|
+
* Param-taking page for the `availability-start-time-detail` contribution:
|
|
5
|
+
* reads the start time id off {@link AdminRoutePageProps} and binds it onto
|
|
6
|
+
* the packaged host. Resolved lazily through the contribution's `page`
|
|
7
|
+
* loader so the detail page lands in its own chunk.
|
|
8
|
+
*/
|
|
9
|
+
export default function AvailabilityStartTimeDetailRoutePage({ params }) {
|
|
10
|
+
return _jsx(AvailabilityStartTimeDetailHost, { startTimeId: params.id ?? "" });
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface AvailabilityRuleDetailHostProps {
|
|
2
|
+
/** The availability rule id (route param, bound by the host route file). */
|
|
3
|
+
ruleId: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Packaged admin host for the availability rule detail page (packaged-admin
|
|
7
|
+
* RFC Phase 3). Data wiring runs through the shared availability provider
|
|
8
|
+
* context; breadcrumbs through the admin chrome; cross-route links through
|
|
9
|
+
* the semantic destinations `availabilitySlot.list`, `availabilitySlot.detail`
|
|
10
|
+
* and `product.detail` (RFC §4.7). The SSR prefetch loader stays in the host
|
|
11
|
+
* route file with the app's cookie-forwarding fetcher.
|
|
12
|
+
*/
|
|
13
|
+
export declare function AvailabilityRuleDetailHost({ ruleId }: AvailabilityRuleDetailHostProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
//# sourceMappingURL=rule-detail-host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rule-detail-host.d.ts","sourceRoot":"","sources":["../../src/admin/rule-detail-host.tsx"],"names":[],"mappings":"AAeA,MAAM,WAAW,+BAA+B;IAC9C,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,EAAE,MAAM,EAAE,EAAE,+BAA+B,2CAsBrF"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { useAdminBreadcrumbs, useAdminHref, useAdminNavigate, useOperatorAdminMessages, } from "@voyantjs/admin";
|
|
5
|
+
import { AvailabilityRuleDetailPage, getAvailabilityRuleDetailQueryOptions, } from "../components/availability-rule-detail-page.js";
|
|
6
|
+
import { useVoyantAvailabilityContext } from "../index.js";
|
|
7
|
+
/**
|
|
8
|
+
* Packaged admin host for the availability rule detail page (packaged-admin
|
|
9
|
+
* RFC Phase 3). Data wiring runs through the shared availability provider
|
|
10
|
+
* context; breadcrumbs through the admin chrome; cross-route links through
|
|
11
|
+
* the semantic destinations `availabilitySlot.list`, `availabilitySlot.detail`
|
|
12
|
+
* and `product.detail` (RFC §4.7). The SSR prefetch loader stays in the host
|
|
13
|
+
* route file with the app's cookie-forwarding fetcher.
|
|
14
|
+
*/
|
|
15
|
+
export function AvailabilityRuleDetailHost({ ruleId }) {
|
|
16
|
+
const messages = useOperatorAdminMessages();
|
|
17
|
+
const resolveHref = useAdminHref();
|
|
18
|
+
const navigateTo = useAdminNavigate();
|
|
19
|
+
const client = useVoyantAvailabilityContext();
|
|
20
|
+
const ruleQuery = useQuery(getAvailabilityRuleDetailQueryOptions(client, ruleId));
|
|
21
|
+
const rule = ruleQuery.data?.data;
|
|
22
|
+
useAdminBreadcrumbs([
|
|
23
|
+
{ label: messages.availability.title, href: resolveHref("availabilitySlot.list", {}) },
|
|
24
|
+
...(rule ? [{ label: rule.productName ?? `Rule ${rule.id.slice(-6)}` }] : []),
|
|
25
|
+
]);
|
|
26
|
+
return (_jsx(AvailabilityRuleDetailPage, { id: ruleId, onBack: () => navigateTo("availabilitySlot.list", {}), onDeleted: () => navigateTo("availabilitySlot.list", {}), onOpenProduct: (productId) => navigateTo("product.detail", { productId }), onOpenSlot: (slotId) => navigateTo("availabilitySlot.detail", { slotId }) }));
|
|
27
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface AvailabilitySlotDetailHostProps {
|
|
2
|
+
/** The availability slot id (route param, bound by the host route file). */
|
|
3
|
+
slotId: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Packaged admin host for the availability slot detail page (packaged-admin
|
|
7
|
+
* RFC Phase 3). Owns everything package-clean:
|
|
8
|
+
*
|
|
9
|
+
* - Data wiring through the shared availability provider context
|
|
10
|
+
* (`useVoyantAvailabilityContext`) — the workspace shell mounts
|
|
11
|
+
* `VoyantAvailabilityProvider`, so no per-route provider or app env
|
|
12
|
+
* helper is needed.
|
|
13
|
+
* - Admin chrome breadcrumbs (`useAdminBreadcrumbs`).
|
|
14
|
+
* - Cross-route links resolve through semantic destinations (RFC §4.7):
|
|
15
|
+
* `availabilitySlot.list`, `availabilityStartTime.detail`,
|
|
16
|
+
* `booking.detail`, `product.detail` — no host route tree import.
|
|
17
|
+
* - The cross-domain composition the operator route previously assembled:
|
|
18
|
+
* the Allocation tab (`@voyantjs/allocation-ui`), the Extras manifest tab
|
|
19
|
+
* (`@voyantjs/extras-react/ui`), the booking create/quick-view sheets
|
|
20
|
+
* (`@voyantjs/bookings-react/ui`, lazy) and the product quick-view sheet
|
|
21
|
+
* (`@voyantjs/products-react/ui`).
|
|
22
|
+
* - The slot edit dialog, submitting through the package mutation
|
|
23
|
+
* (`useAvailabilitySlotMutation`) instead of an app RPC client.
|
|
24
|
+
*
|
|
25
|
+
* The SSR prefetch loader stays in the host route file (it runs outside the
|
|
26
|
+
* React tree with the app's cookie-forwarding fetcher).
|
|
27
|
+
*/
|
|
28
|
+
export declare function AvailabilitySlotDetailHost({ slotId }: AvailabilitySlotDetailHostProps): import("react/jsx-runtime").JSX.Element;
|
|
29
|
+
//# sourceMappingURL=slot-detail-host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slot-detail-host.d.ts","sourceRoot":"","sources":["../../src/admin/slot-detail-host.tsx"],"names":[],"mappings":"AAyCA,MAAM,WAAW,+BAA+B;IAC9C,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,0BAA0B,CAAC,EAAE,MAAM,EAAE,EAAE,+BAA+B,2CAqIrF"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { useAdminBreadcrumbs, useAdminHref, useAdminNavigate, useOperatorAdminMessages, } from "@voyantjs/admin";
|
|
5
|
+
import { SlotAllocationPage } from "@voyantjs/allocation-ui";
|
|
6
|
+
import { SlotExtrasManifestPanel, useExtrasUiMessagesOrDefault } from "@voyantjs/extras-react/ui";
|
|
7
|
+
import { ProductQuickViewSheet } from "@voyantjs/products-react/ui";
|
|
8
|
+
import { lazy, Suspense, useState } from "react";
|
|
9
|
+
import { AvailabilitySlotDialog } from "../components/availability-dialogs.js";
|
|
10
|
+
import { AvailabilitySlotDetailPage, getAvailabilitySlotDetailQueryOptions, getAvailabilitySlotProductQueryOptions, } from "../components/availability-slot-detail-page.js";
|
|
11
|
+
import { useAvailabilitySlotMutation, useRules, useStartTimes, useVoyantAvailabilityContext, } from "../index.js";
|
|
12
|
+
// Lazy: the booking sheets pull the bookings-ui bundle; only operators who
|
|
13
|
+
// actually create/preview a booking from a slot pay for it.
|
|
14
|
+
const BookingCreateSheet = lazy(() => import("@voyantjs/bookings-react/components/booking-create-sheet").then((module) => ({
|
|
15
|
+
default: module.BookingCreateSheet,
|
|
16
|
+
})));
|
|
17
|
+
const BookingQuickViewSheet = lazy(() => import("@voyantjs/bookings-react/components/booking-quick-view-sheet").then((module) => ({
|
|
18
|
+
default: module.BookingQuickViewSheet,
|
|
19
|
+
})));
|
|
20
|
+
/**
|
|
21
|
+
* Packaged admin host for the availability slot detail page (packaged-admin
|
|
22
|
+
* RFC Phase 3). Owns everything package-clean:
|
|
23
|
+
*
|
|
24
|
+
* - Data wiring through the shared availability provider context
|
|
25
|
+
* (`useVoyantAvailabilityContext`) — the workspace shell mounts
|
|
26
|
+
* `VoyantAvailabilityProvider`, so no per-route provider or app env
|
|
27
|
+
* helper is needed.
|
|
28
|
+
* - Admin chrome breadcrumbs (`useAdminBreadcrumbs`).
|
|
29
|
+
* - Cross-route links resolve through semantic destinations (RFC §4.7):
|
|
30
|
+
* `availabilitySlot.list`, `availabilityStartTime.detail`,
|
|
31
|
+
* `booking.detail`, `product.detail` — no host route tree import.
|
|
32
|
+
* - The cross-domain composition the operator route previously assembled:
|
|
33
|
+
* the Allocation tab (`@voyantjs/allocation-ui`), the Extras manifest tab
|
|
34
|
+
* (`@voyantjs/extras-react/ui`), the booking create/quick-view sheets
|
|
35
|
+
* (`@voyantjs/bookings-react/ui`, lazy) and the product quick-view sheet
|
|
36
|
+
* (`@voyantjs/products-react/ui`).
|
|
37
|
+
* - The slot edit dialog, submitting through the package mutation
|
|
38
|
+
* (`useAvailabilitySlotMutation`) instead of an app RPC client.
|
|
39
|
+
*
|
|
40
|
+
* The SSR prefetch loader stays in the host route file (it runs outside the
|
|
41
|
+
* React tree with the app's cookie-forwarding fetcher).
|
|
42
|
+
*/
|
|
43
|
+
export function AvailabilitySlotDetailHost({ slotId }) {
|
|
44
|
+
const messages = useOperatorAdminMessages();
|
|
45
|
+
const extrasMessages = useExtrasUiMessagesOrDefault();
|
|
46
|
+
const resolveHref = useAdminHref();
|
|
47
|
+
const navigateTo = useAdminNavigate();
|
|
48
|
+
const client = useVoyantAvailabilityContext();
|
|
49
|
+
const slotMutation = useAvailabilitySlotMutation();
|
|
50
|
+
const slotQuery = useQuery(getAvailabilitySlotDetailQueryOptions(client, slotId));
|
|
51
|
+
const slot = slotQuery.data?.data;
|
|
52
|
+
const productQuery = useQuery({
|
|
53
|
+
...getAvailabilitySlotProductQueryOptions(client, slot?.productId ?? null),
|
|
54
|
+
enabled: Boolean(slot?.productId),
|
|
55
|
+
});
|
|
56
|
+
const productName = productQuery.data?.data?.name ?? null;
|
|
57
|
+
const [bookingPreviewId, setBookingPreviewId] = useState(null);
|
|
58
|
+
const [productPreviewId, setProductPreviewId] = useState(null);
|
|
59
|
+
const [bookingCreateDefaults, setBookingCreateDefaults] = useState(null);
|
|
60
|
+
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
|
61
|
+
// Lazy-load rules + start times only when the edit dialog opens —
|
|
62
|
+
// the slot detail view itself doesn't need them. Scope to the slot's
|
|
63
|
+
// product so the dialog only suggests recurring rules / start times
|
|
64
|
+
// that already belong to this product.
|
|
65
|
+
const rulesQuery = useRules({ productId: slot?.productId, enabled: editDialogOpen });
|
|
66
|
+
const startTimesQuery = useStartTimes({
|
|
67
|
+
productId: slot?.productId,
|
|
68
|
+
enabled: editDialogOpen,
|
|
69
|
+
});
|
|
70
|
+
useAdminBreadcrumbs([
|
|
71
|
+
{ label: messages.availability.title, href: resolveHref("availabilitySlot.list", {}) },
|
|
72
|
+
...(slot
|
|
73
|
+
? [
|
|
74
|
+
{
|
|
75
|
+
label: productName ? `${productName} · ${slot.dateLocal}` : `Slot · ${slot.dateLocal}`,
|
|
76
|
+
},
|
|
77
|
+
]
|
|
78
|
+
: []),
|
|
79
|
+
]);
|
|
80
|
+
return (_jsxs(_Fragment, { children: [_jsx(AvailabilitySlotDetailPage, { id: slotId, onBack: () => navigateTo("availabilitySlot.list", {}), onDeleted: () => navigateTo("availabilitySlot.list", {}), onOpenProduct: (productId) => setProductPreviewId(productId), onOpenStartTime: (startTimeId) => navigateTo("availabilityStartTime.detail", { startTimeId }), onCreateBooking: (input) => setBookingCreateDefaults(input), onEdit: () => setEditDialogOpen(true), renderAllocation: ({ slotId: allocationSlotId }) => (_jsx(SlotAllocationPage, { slotId: allocationSlotId, embed: true, onBookingOpen: (bookingId) => setBookingPreviewId(bookingId) })), renderExtras: ({ slotId: extrasSlotId }) => (_jsx(SlotExtrasManifestPanel, { slotId: extrasSlotId })), extrasTabLabel: extrasMessages.slotManifest.title }), _jsx(Suspense, { fallback: null, children: _jsx(BookingCreateSheet, { open: Boolean(bookingCreateDefaults), onOpenChange: (open) => {
|
|
81
|
+
if (!open)
|
|
82
|
+
setBookingCreateDefaults(null);
|
|
83
|
+
}, defaultProductId: bookingCreateDefaults?.productId, defaultSlotId: bookingCreateDefaults?.slotId, onCreated: (booking) => setBookingPreviewId(booking.id) }) }), _jsx(Suspense, { fallback: null, children: _jsx(BookingQuickViewSheet, { bookingId: bookingPreviewId, open: bookingPreviewId !== null, onOpenChange: (open) => {
|
|
84
|
+
if (!open)
|
|
85
|
+
setBookingPreviewId(null);
|
|
86
|
+
}, onViewFull: (booking) => {
|
|
87
|
+
setBookingPreviewId(null);
|
|
88
|
+
navigateTo("booking.detail", { bookingId: booking.id });
|
|
89
|
+
} }) }), _jsx(ProductQuickViewSheet, { productId: productPreviewId, open: productPreviewId !== null, onOpenChange: (open) => {
|
|
90
|
+
if (!open)
|
|
91
|
+
setProductPreviewId(null);
|
|
92
|
+
}, onViewFull: (product) => {
|
|
93
|
+
setProductPreviewId(null);
|
|
94
|
+
navigateTo("product.detail", { productId: product.id });
|
|
95
|
+
} }), slot && productQuery.data?.data ? (_jsx(AvailabilitySlotDialog, { messages: messages.availability, open: editDialogOpen, onOpenChange: setEditDialogOpen, slot: slot, products: [productQuery.data.data], rules: rulesQuery.data?.data ?? [], startTimes: startTimesQuery.data?.data ?? [], onSubmit: async (payload, context) => {
|
|
96
|
+
if (context.isEditing) {
|
|
97
|
+
if (!context.id)
|
|
98
|
+
throw new Error("Slot edit requires an id.");
|
|
99
|
+
await slotMutation.update.mutateAsync({
|
|
100
|
+
id: context.id,
|
|
101
|
+
input: payload,
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
await slotMutation.create.mutateAsync(payload);
|
|
106
|
+
}, onSuccess: () => {
|
|
107
|
+
setEditDialogOpen(false);
|
|
108
|
+
void slotQuery.refetch();
|
|
109
|
+
} })) : null] }));
|
|
110
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface AvailabilityStartTimeDetailHostProps {
|
|
2
|
+
/** The availability start time id (route param, bound by the host route file). */
|
|
3
|
+
startTimeId: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Packaged admin host for the availability start time detail page
|
|
7
|
+
* (packaged-admin RFC Phase 3). Data wiring runs through the shared
|
|
8
|
+
* availability provider context; breadcrumbs through the admin chrome;
|
|
9
|
+
* cross-route links through the semantic destinations
|
|
10
|
+
* `availabilitySlot.list`, `availabilitySlot.detail` and `product.detail`
|
|
11
|
+
* (RFC §4.7). The SSR prefetch loader stays in the host route file with the
|
|
12
|
+
* app's cookie-forwarding fetcher.
|
|
13
|
+
*/
|
|
14
|
+
export declare function AvailabilityStartTimeDetailHost({ startTimeId, }: AvailabilityStartTimeDetailHostProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
//# sourceMappingURL=start-time-detail-host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"start-time-detail-host.d.ts","sourceRoot":"","sources":["../../src/admin/start-time-detail-host.tsx"],"names":[],"mappings":"AAeA,MAAM,WAAW,oCAAoC;IACnD,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAA;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,+BAA+B,CAAC,EAC9C,WAAW,GACZ,EAAE,oCAAoC,2CAgCtC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { useAdminBreadcrumbs, useAdminHref, useAdminNavigate, useOperatorAdminMessages, } from "@voyantjs/admin";
|
|
5
|
+
import { AvailabilityStartTimeDetailPage, getAvailabilityStartTimeDetailQueryOptions, } from "../components/availability-start-time-detail-page.js";
|
|
6
|
+
import { useVoyantAvailabilityContext } from "../index.js";
|
|
7
|
+
/**
|
|
8
|
+
* Packaged admin host for the availability start time detail page
|
|
9
|
+
* (packaged-admin RFC Phase 3). Data wiring runs through the shared
|
|
10
|
+
* availability provider context; breadcrumbs through the admin chrome;
|
|
11
|
+
* cross-route links through the semantic destinations
|
|
12
|
+
* `availabilitySlot.list`, `availabilitySlot.detail` and `product.detail`
|
|
13
|
+
* (RFC §4.7). The SSR prefetch loader stays in the host route file with the
|
|
14
|
+
* app's cookie-forwarding fetcher.
|
|
15
|
+
*/
|
|
16
|
+
export function AvailabilityStartTimeDetailHost({ startTimeId, }) {
|
|
17
|
+
const messages = useOperatorAdminMessages();
|
|
18
|
+
const resolveHref = useAdminHref();
|
|
19
|
+
const navigateTo = useAdminNavigate();
|
|
20
|
+
const client = useVoyantAvailabilityContext();
|
|
21
|
+
const startTimeQuery = useQuery(getAvailabilityStartTimeDetailQueryOptions(client, startTimeId));
|
|
22
|
+
const startTime = startTimeQuery.data?.data;
|
|
23
|
+
const startTimeFallback = messages.availability.details.startTime.fallbackTitle;
|
|
24
|
+
useAdminBreadcrumbs([
|
|
25
|
+
{ label: messages.availability.title, href: resolveHref("availabilitySlot.list", {}) },
|
|
26
|
+
...(startTime
|
|
27
|
+
? [
|
|
28
|
+
{
|
|
29
|
+
label: startTime.label
|
|
30
|
+
? `${startTime.productName ?? startTimeFallback} · ${startTime.label}`
|
|
31
|
+
: (startTime.productName ?? `${startTimeFallback} ${startTime.startTimeLocal}`),
|
|
32
|
+
},
|
|
33
|
+
]
|
|
34
|
+
: []),
|
|
35
|
+
]);
|
|
36
|
+
return (_jsx(AvailabilityStartTimeDetailPage, { id: startTimeId, onBack: () => navigateTo("availabilitySlot.list", {}), onDeleted: () => navigateTo("availabilitySlot.list", {}), onOpenProduct: (productId) => navigateTo("product.detail", { productId }), onOpenSlot: (slotId) => navigateTo("availabilitySlot.detail", { slotId }) }));
|
|
37
|
+
}
|