@voyantjs/availability-react 0.106.0 → 0.107.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 +69 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +73 -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/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,369 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, Textarea, } from "@voyantjs/ui/components";
|
|
4
|
+
import { DatePicker } from "@voyantjs/ui/components/date-picker";
|
|
5
|
+
import { DateTimePicker } from "@voyantjs/ui/components/date-time-picker";
|
|
6
|
+
import { Loader2 } from "lucide-react";
|
|
7
|
+
import { useEffect } from "react";
|
|
8
|
+
import { useForm } from "react-hook-form";
|
|
9
|
+
import { z } from "zod/v4";
|
|
10
|
+
import { zodResolver } from "../form-resolver.js";
|
|
11
|
+
import { useAvailabilityUiMessagesOrDefault } from "../i18n/index.js";
|
|
12
|
+
import { booleanOptions, instantToSlotLocal, localToInstant, NONE_VALUE, nullableNumber, nullableString, slotLocalStart, slotStatusOptions, } from "../index.js";
|
|
13
|
+
function getRuleFormSchema(messages) {
|
|
14
|
+
return z.object({
|
|
15
|
+
productId: z.string().min(1, messages.dialogs.rule.validationProductRequired),
|
|
16
|
+
timezone: z.string().min(1, messages.dialogs.rule.validationTimezoneRequired),
|
|
17
|
+
recurrenceRule: z.string().min(1, messages.dialogs.rule.validationRecurrenceRequired),
|
|
18
|
+
maxCapacity: z.coerce.number().int().min(0),
|
|
19
|
+
maxPickupCapacity: z.string().optional(),
|
|
20
|
+
minTotalPax: z.string().optional(),
|
|
21
|
+
cutoffMinutes: z.string().optional(),
|
|
22
|
+
earlyBookingLimitMinutes: z.string().optional(),
|
|
23
|
+
active: z.boolean(),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export function AvailabilityRuleDialog(props) {
|
|
27
|
+
useAvailabilityUiMessagesOrDefault();
|
|
28
|
+
const ruleMessages = props.messages.dialogs.rule;
|
|
29
|
+
const ruleFormSchema = getRuleFormSchema(props.messages);
|
|
30
|
+
const form = useForm({
|
|
31
|
+
resolver: zodResolver(ruleFormSchema),
|
|
32
|
+
defaultValues: {
|
|
33
|
+
productId: "",
|
|
34
|
+
timezone: "Europe/Bucharest", // i18n-literal-ok IANA timezone default
|
|
35
|
+
recurrenceRule: "FREQ=DAILY;INTERVAL=1", // i18n-literal-ok RRULE default
|
|
36
|
+
maxCapacity: 0,
|
|
37
|
+
maxPickupCapacity: "",
|
|
38
|
+
minTotalPax: "",
|
|
39
|
+
cutoffMinutes: "",
|
|
40
|
+
earlyBookingLimitMinutes: "",
|
|
41
|
+
active: true,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (props.open && props.rule) {
|
|
46
|
+
form.reset({
|
|
47
|
+
productId: props.rule.productId,
|
|
48
|
+
timezone: props.rule.timezone,
|
|
49
|
+
recurrenceRule: props.rule.recurrenceRule,
|
|
50
|
+
maxCapacity: props.rule.maxCapacity,
|
|
51
|
+
maxPickupCapacity: props.rule.maxPickupCapacity?.toString() ?? "",
|
|
52
|
+
minTotalPax: "",
|
|
53
|
+
cutoffMinutes: props.rule.cutoffMinutes?.toString() ?? "",
|
|
54
|
+
earlyBookingLimitMinutes: "",
|
|
55
|
+
active: props.rule.active,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
else if (props.open) {
|
|
59
|
+
form.reset();
|
|
60
|
+
}
|
|
61
|
+
}, [form, props.open, props.rule]);
|
|
62
|
+
const isEditing = Boolean(props.rule);
|
|
63
|
+
const onSubmit = async (values) => {
|
|
64
|
+
await props.onSubmit({
|
|
65
|
+
productId: values.productId,
|
|
66
|
+
timezone: values.timezone,
|
|
67
|
+
recurrenceRule: values.recurrenceRule,
|
|
68
|
+
maxCapacity: values.maxCapacity,
|
|
69
|
+
maxPickupCapacity: nullableNumber(values.maxPickupCapacity),
|
|
70
|
+
minTotalPax: nullableNumber(values.minTotalPax),
|
|
71
|
+
cutoffMinutes: nullableNumber(values.cutoffMinutes),
|
|
72
|
+
earlyBookingLimitMinutes: nullableNumber(values.earlyBookingLimitMinutes),
|
|
73
|
+
active: values.active,
|
|
74
|
+
}, { isEditing, id: props.rule?.id });
|
|
75
|
+
props.onSuccess();
|
|
76
|
+
};
|
|
77
|
+
return (_jsx(Dialog, { open: props.open, onOpenChange: props.onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? ruleMessages.editTitle : ruleMessages.newTitle }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsx(ProductSelect, { label: ruleMessages.productLabel, placeholder: ruleMessages.selectProductPlaceholder, products: props.products, value: form.watch("productId"), onValueChange: (value) => form.setValue("productId", value ?? "") }), form.formState.errors.productId ? (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.productId.message })) : null, _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: ruleMessages.timezoneLabel }), _jsx(Input, { ...form.register("timezone"), placeholder: ruleMessages.timezonePlaceholder })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: ruleMessages.maxCapacityLabel }), _jsx(Input, { ...form.register("maxCapacity"), type: "number", min: 0 })] })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: ruleMessages.recurrenceRuleLabel }), _jsx(Textarea, { ...form.register("recurrenceRule"), placeholder: ruleMessages.recurrenceRulePlaceholder, className: "font-mono text-xs" })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: ruleMessages.maxPickupCapacityLabel }), _jsx(Input, { ...form.register("maxPickupCapacity"), type: "number", min: 0 })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: ruleMessages.minimumTotalPaxLabel }), _jsx(Input, { ...form.register("minTotalPax"), type: "number", min: 0 })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: ruleMessages.cutoffMinutesLabel }), _jsx(Input, { ...form.register("cutoffMinutes"), type: "number", min: 0 })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: ruleMessages.earlyBookingLimitMinutesLabel }), _jsx(Input, { ...form.register("earlyBookingLimitMinutes"), type: "number", min: 0 })] })] }), _jsx(SwitchField, { title: ruleMessages.activeTitle, description: ruleMessages.activeDescription, checked: form.watch("active"), onCheckedChange: (checked) => form.setValue("active", checked) })] }), _jsx(DialogActions, { cancel: ruleMessages.cancel, save: ruleMessages.save, create: ruleMessages.create, isEditing: isEditing, isSubmitting: form.formState.isSubmitting, onCancel: () => props.onOpenChange(false) })] })] }) }));
|
|
78
|
+
}
|
|
79
|
+
function getStartTimeFormSchema(messages) {
|
|
80
|
+
return z.object({
|
|
81
|
+
productId: z.string().min(1, messages.dialogs.startTime.validationProductRequired),
|
|
82
|
+
label: z.string().optional(),
|
|
83
|
+
startTimeLocal: z.string().min(1, messages.dialogs.startTime.validationStartTimeRequired),
|
|
84
|
+
durationMinutes: z.string().optional(),
|
|
85
|
+
sortOrder: z.coerce.number().int(),
|
|
86
|
+
active: z.boolean(),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export function AvailabilityStartTimeDialog(props) {
|
|
90
|
+
const startTimeMessages = props.messages.dialogs.startTime;
|
|
91
|
+
const startTimeFormSchema = getStartTimeFormSchema(props.messages);
|
|
92
|
+
const form = useForm({
|
|
93
|
+
resolver: zodResolver(startTimeFormSchema),
|
|
94
|
+
defaultValues: {
|
|
95
|
+
productId: "",
|
|
96
|
+
label: "",
|
|
97
|
+
startTimeLocal: "09:00",
|
|
98
|
+
durationMinutes: "",
|
|
99
|
+
sortOrder: 0,
|
|
100
|
+
active: true,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (props.open && props.startTime) {
|
|
105
|
+
form.reset({
|
|
106
|
+
productId: props.startTime.productId,
|
|
107
|
+
label: props.startTime.label ?? "",
|
|
108
|
+
startTimeLocal: props.startTime.startTimeLocal,
|
|
109
|
+
durationMinutes: props.startTime.durationMinutes?.toString() ?? "",
|
|
110
|
+
sortOrder: props.startTime.sortOrder,
|
|
111
|
+
active: props.startTime.active,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
else if (props.open) {
|
|
115
|
+
form.reset();
|
|
116
|
+
}
|
|
117
|
+
}, [form, props.open, props.startTime]);
|
|
118
|
+
const isEditing = Boolean(props.startTime);
|
|
119
|
+
const onSubmit = async (values) => {
|
|
120
|
+
await props.onSubmit({
|
|
121
|
+
productId: values.productId,
|
|
122
|
+
label: nullableString(values.label),
|
|
123
|
+
startTimeLocal: values.startTimeLocal,
|
|
124
|
+
durationMinutes: nullableNumber(values.durationMinutes),
|
|
125
|
+
sortOrder: values.sortOrder,
|
|
126
|
+
active: values.active,
|
|
127
|
+
}, { isEditing, id: props.startTime?.id });
|
|
128
|
+
props.onSuccess();
|
|
129
|
+
};
|
|
130
|
+
return (_jsx(Dialog, { open: props.open, onOpenChange: props.onOpenChange, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? startTimeMessages.editTitle : startTimeMessages.newTitle }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsx(ProductSelect, { label: startTimeMessages.productLabel, placeholder: startTimeMessages.selectProductPlaceholder, products: props.products, value: form.watch("productId"), onValueChange: (value) => form.setValue("productId", value ?? "") }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: startTimeMessages.labelLabel }), _jsx(Input, { ...form.register("label"), placeholder: startTimeMessages.labelPlaceholder })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: startTimeMessages.startTimeLabel }), _jsx(Input, { ...form.register("startTimeLocal"), type: "time" })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: startTimeMessages.durationMinutesLabel }), _jsx(Input, { ...form.register("durationMinutes"), type: "number", min: 0 })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: startTimeMessages.sortOrderLabel }), _jsx(Input, { ...form.register("sortOrder"), type: "number" })] })] }), _jsx(SwitchField, { title: startTimeMessages.activeTitle, description: startTimeMessages.activeDescription, checked: form.watch("active"), onCheckedChange: (checked) => form.setValue("active", checked) })] }), _jsx(DialogActions, { cancel: startTimeMessages.cancel, save: startTimeMessages.save, create: startTimeMessages.create, isEditing: isEditing, isSubmitting: form.formState.isSubmitting, onCancel: () => props.onOpenChange(false) })] })] }) }));
|
|
131
|
+
}
|
|
132
|
+
function getSlotFormSchema(messages) {
|
|
133
|
+
return z.object({
|
|
134
|
+
productId: z.string().min(1, messages.dialogs.slot.validationProductRequired),
|
|
135
|
+
availabilityRuleId: z.string().optional(),
|
|
136
|
+
startTimeId: z.string().optional(),
|
|
137
|
+
dateLocal: z.string().min(1, messages.dialogs.slot.validationDateRequired),
|
|
138
|
+
startsAt: z.string().min(1, messages.dialogs.slot.validationStartsAtRequired),
|
|
139
|
+
endsAt: z.string().optional(),
|
|
140
|
+
timezone: z.string().min(1, messages.dialogs.slot.validationTimezoneRequired),
|
|
141
|
+
status: z.enum(["open", "closed", "sold_out", "cancelled"]),
|
|
142
|
+
unlimited: z.boolean(),
|
|
143
|
+
initialPax: z.string().optional(),
|
|
144
|
+
remainingPax: z.string().optional(),
|
|
145
|
+
initialPickups: z.string().optional(),
|
|
146
|
+
remainingPickups: z.string().optional(),
|
|
147
|
+
remainingResources: z.string().optional(),
|
|
148
|
+
pastCutoff: z.boolean(),
|
|
149
|
+
tooEarly: z.boolean(),
|
|
150
|
+
notes: z.string().optional(),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function toLocalDateTimeInput(instant, timezone) {
|
|
154
|
+
const local = instantToSlotLocal(instant, timezone);
|
|
155
|
+
return `${local.date}T${local.time}`;
|
|
156
|
+
}
|
|
157
|
+
function localDateTimeInputToInstant(value, timezone) {
|
|
158
|
+
const [date, time] = value.split("T");
|
|
159
|
+
if (!date || !time) {
|
|
160
|
+
throw new RangeError("Local date-time input must use YYYY-MM-DDTHH:mm");
|
|
161
|
+
}
|
|
162
|
+
return localToInstant({ date, time, timezone });
|
|
163
|
+
}
|
|
164
|
+
function formatSlotLocalDateTime(value) {
|
|
165
|
+
return `${value.date} ${value.time}`;
|
|
166
|
+
}
|
|
167
|
+
export function AvailabilitySlotDialog(props) {
|
|
168
|
+
const slotMessages = props.messages.dialogs.slot;
|
|
169
|
+
const slotFormSchema = getSlotFormSchema(props.messages);
|
|
170
|
+
const form = useForm({
|
|
171
|
+
resolver: zodResolver(slotFormSchema),
|
|
172
|
+
defaultValues: {
|
|
173
|
+
productId: "",
|
|
174
|
+
availabilityRuleId: NONE_VALUE,
|
|
175
|
+
startTimeId: NONE_VALUE,
|
|
176
|
+
dateLocal: "",
|
|
177
|
+
startsAt: "",
|
|
178
|
+
endsAt: "",
|
|
179
|
+
timezone: "Europe/Bucharest", // i18n-literal-ok IANA timezone default
|
|
180
|
+
status: "open",
|
|
181
|
+
unlimited: false,
|
|
182
|
+
initialPax: "",
|
|
183
|
+
remainingPax: "",
|
|
184
|
+
initialPickups: "",
|
|
185
|
+
remainingPickups: "",
|
|
186
|
+
remainingResources: "",
|
|
187
|
+
pastCutoff: false,
|
|
188
|
+
tooEarly: false,
|
|
189
|
+
notes: "",
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
if (props.open && props.slot) {
|
|
194
|
+
form.reset({
|
|
195
|
+
productId: props.slot.productId,
|
|
196
|
+
availabilityRuleId: props.slot.availabilityRuleId ?? NONE_VALUE,
|
|
197
|
+
startTimeId: props.slot.startTimeId ?? NONE_VALUE,
|
|
198
|
+
dateLocal: props.slot.dateLocal,
|
|
199
|
+
startsAt: toLocalDateTimeInput(props.slot.startsAt, props.slot.timezone),
|
|
200
|
+
endsAt: props.slot.endsAt
|
|
201
|
+
? toLocalDateTimeInput(props.slot.endsAt, props.slot.timezone)
|
|
202
|
+
: "",
|
|
203
|
+
timezone: props.slot.timezone,
|
|
204
|
+
status: props.slot.status,
|
|
205
|
+
unlimited: props.slot.unlimited,
|
|
206
|
+
initialPax: props.slot.initialPax?.toString() ?? "",
|
|
207
|
+
remainingPax: props.slot.remainingPax?.toString() ?? "",
|
|
208
|
+
initialPickups: "",
|
|
209
|
+
remainingPickups: "",
|
|
210
|
+
remainingResources: "",
|
|
211
|
+
pastCutoff: false,
|
|
212
|
+
tooEarly: false,
|
|
213
|
+
notes: props.slot.notes ?? "",
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
else if (props.open) {
|
|
217
|
+
form.reset();
|
|
218
|
+
}
|
|
219
|
+
}, [form, props.open, props.slot]);
|
|
220
|
+
const selectedProductId = form.watch("productId");
|
|
221
|
+
const filteredRules = props.rules.filter((rule) => rule.productId === selectedProductId);
|
|
222
|
+
const filteredStartTimes = props.startTimes.filter((startTime) => startTime.productId === selectedProductId);
|
|
223
|
+
const isEditing = Boolean(props.slot);
|
|
224
|
+
const onSubmit = async (values) => {
|
|
225
|
+
await props.onSubmit({
|
|
226
|
+
productId: values.productId,
|
|
227
|
+
availabilityRuleId: values.availabilityRuleId === NONE_VALUE ? null : (values.availabilityRuleId ?? null),
|
|
228
|
+
startTimeId: values.startTimeId === NONE_VALUE ? null : (values.startTimeId ?? null),
|
|
229
|
+
dateLocal: values.dateLocal,
|
|
230
|
+
startsAt: localDateTimeInputToInstant(values.startsAt, values.timezone),
|
|
231
|
+
endsAt: values.endsAt ? localDateTimeInputToInstant(values.endsAt, values.timezone) : null,
|
|
232
|
+
timezone: values.timezone,
|
|
233
|
+
status: values.status,
|
|
234
|
+
unlimited: values.unlimited,
|
|
235
|
+
initialPax: nullableNumber(values.initialPax),
|
|
236
|
+
remainingPax: nullableNumber(values.remainingPax),
|
|
237
|
+
initialPickups: nullableNumber(values.initialPickups),
|
|
238
|
+
remainingPickups: nullableNumber(values.remainingPickups),
|
|
239
|
+
remainingResources: nullableNumber(values.remainingResources),
|
|
240
|
+
pastCutoff: values.pastCutoff,
|
|
241
|
+
tooEarly: values.tooEarly,
|
|
242
|
+
notes: nullableString(values.notes),
|
|
243
|
+
}, { isEditing, id: props.slot?.id });
|
|
244
|
+
props.onSuccess();
|
|
245
|
+
};
|
|
246
|
+
return (_jsx(Dialog, { open: props.open, onOpenChange: props.onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? slotMessages.editTitle : slotMessages.newTitle }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsx(ProductSelect, { label: slotMessages.productLabel, placeholder: slotMessages.selectProductPlaceholder, products: props.products, value: form.watch("productId"), onValueChange: (value) => form.setValue("productId", value ?? "") }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: slotMessages.ruleLabel }), _jsxs(Select, { value: form.watch("availabilityRuleId") ?? NONE_VALUE, onValueChange: (value) => form.setValue("availabilityRuleId", value ?? NONE_VALUE), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: slotMessages.optionalRulePlaceholder }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: NONE_VALUE, children: slotMessages.noRule }), filteredRules.map((rule) => (_jsxs(SelectItem, { value: rule.id, children: [rule.timezone, " \u00B7 ", rule.recurrenceRule] }, rule.id)))] })] })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: slotMessages.startTimeLabel }), _jsxs(Select, { value: form.watch("startTimeId") ?? NONE_VALUE, onValueChange: (value) => form.setValue("startTimeId", value ?? NONE_VALUE), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: slotMessages.optionalStartTimePlaceholder }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: NONE_VALUE, children: slotMessages.noStartTime }), filteredStartTimes.map((startTime) => (_jsx(SelectItem, { value: startTime.id, children: startTime.label ?? startTime.startTimeLocal }, startTime.id)))] })] })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: slotMessages.dateLabel }), _jsx(DatePicker, { value: form.watch("dateLocal") || null, onChange: (nextValue) => form.setValue("dateLocal", nextValue ?? "", {
|
|
247
|
+
shouldDirty: true,
|
|
248
|
+
shouldValidate: true,
|
|
249
|
+
}) })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: slotMessages.timezoneLabel }), _jsx(Input, { ...form.register("timezone"), placeholder: slotMessages.timezonePlaceholder })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: slotMessages.startsAtLabel }), _jsx(DateTimePicker, { value: form.watch("startsAt") || null, onChange: (nextValue) => form.setValue("startsAt", nextValue ?? "", {
|
|
250
|
+
shouldDirty: true,
|
|
251
|
+
shouldValidate: true,
|
|
252
|
+
}) })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: slotMessages.endsAtLabel }), _jsx(DateTimePicker, { value: form.watch("endsAt") || null, onChange: (nextValue) => form.setValue("endsAt", nextValue ?? "", {
|
|
253
|
+
shouldDirty: true,
|
|
254
|
+
shouldValidate: true,
|
|
255
|
+
}) })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: slotMessages.statusLabel }), _jsxs(Select, { items: slotStatusOptions, value: form.watch("status"), onValueChange: (value) => form.setValue("status", value), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: slotStatusOptions.map((option) => (_jsx(SelectItem, { value: option.value, children: {
|
|
256
|
+
open: props.messages.statusOpen,
|
|
257
|
+
closed: props.messages.statusClosed,
|
|
258
|
+
sold_out: props.messages.statusSoldOut,
|
|
259
|
+
cancelled: props.messages.statusCancelled,
|
|
260
|
+
}[option.value] }, option.value))) })] })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: slotMessages.unlimitedLabel }), _jsxs(Select, { items: booleanOptions, value: String(form.watch("unlimited")), onValueChange: (value) => form.setValue("unlimited", value === "true"), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: booleanOptions.map((option) => (_jsx(SelectItem, { value: option.value, children: option.value === "true" ? slotMessages.yes : slotMessages.no }, option.value))) })] })] })] }), _jsxs("div", { className: "grid grid-cols-3 gap-4", children: [_jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: slotMessages.initialPaxLabel }), _jsx(Input, { ...form.register("initialPax"), type: "number", min: 0 })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: slotMessages.remainingPaxLabel }), _jsx(Input, { ...form.register("remainingPax"), type: "number", min: 0 })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: slotMessages.remainingResourcesLabel }), _jsx(Input, { ...form.register("remainingResources"), type: "number", min: 0 })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: slotMessages.initialPickupsLabel }), _jsx(Input, { ...form.register("initialPickups"), type: "number", min: 0 })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: slotMessages.remainingPickupsLabel }), _jsx(Input, { ...form.register("remainingPickups"), type: "number", min: 0 })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsx(SwitchField, { title: slotMessages.pastCutoffTitle, description: slotMessages.pastCutoffDescription, checked: form.watch("pastCutoff"), onCheckedChange: (checked) => form.setValue("pastCutoff", checked) }), _jsx(SwitchField, { title: slotMessages.tooEarlyTitle, description: slotMessages.tooEarlyDescription, checked: form.watch("tooEarly"), onCheckedChange: (checked) => form.setValue("tooEarly", checked) })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: slotMessages.notesLabel }), _jsx(Textarea, { ...form.register("notes"), placeholder: slotMessages.notesPlaceholder })] })] }), _jsx(DialogActions, { cancel: slotMessages.cancel, save: slotMessages.save, create: slotMessages.create, isEditing: isEditing, isSubmitting: form.formState.isSubmitting, onCancel: () => props.onOpenChange(false) })] })] }) }));
|
|
261
|
+
}
|
|
262
|
+
function getCloseoutFormSchema(messages) {
|
|
263
|
+
return z.object({
|
|
264
|
+
productId: z.string().min(1, messages.dialogs.closeout.validationProductRequired),
|
|
265
|
+
slotId: z.string().optional(),
|
|
266
|
+
dateLocal: z.string().min(1, messages.dialogs.closeout.validationDateRequired),
|
|
267
|
+
reason: z.string().optional(),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
export function AvailabilityCloseoutDialog(props) {
|
|
271
|
+
const closeoutMessages = props.messages.dialogs.closeout;
|
|
272
|
+
const closeoutFormSchema = getCloseoutFormSchema(props.messages);
|
|
273
|
+
const form = useForm({
|
|
274
|
+
resolver: zodResolver(closeoutFormSchema),
|
|
275
|
+
defaultValues: {
|
|
276
|
+
productId: "",
|
|
277
|
+
slotId: NONE_VALUE,
|
|
278
|
+
dateLocal: "",
|
|
279
|
+
reason: "",
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
if (props.open && props.closeout) {
|
|
284
|
+
form.reset({
|
|
285
|
+
productId: props.closeout.productId,
|
|
286
|
+
slotId: props.closeout.slotId ?? NONE_VALUE,
|
|
287
|
+
dateLocal: props.closeout.dateLocal,
|
|
288
|
+
reason: props.closeout.reason ?? "",
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
else if (props.open) {
|
|
292
|
+
form.reset();
|
|
293
|
+
}
|
|
294
|
+
}, [form, props.closeout, props.open]);
|
|
295
|
+
const selectedProductId = form.watch("productId");
|
|
296
|
+
const filteredSlots = props.slots.filter((slot) => slot.productId === selectedProductId);
|
|
297
|
+
const isEditing = Boolean(props.closeout);
|
|
298
|
+
const onSubmit = async (values) => {
|
|
299
|
+
await props.onSubmit({
|
|
300
|
+
productId: values.productId,
|
|
301
|
+
slotId: values.slotId === NONE_VALUE ? null : (values.slotId ?? null),
|
|
302
|
+
dateLocal: values.dateLocal,
|
|
303
|
+
reason: nullableString(values.reason),
|
|
304
|
+
}, { isEditing, id: props.closeout?.id });
|
|
305
|
+
props.onSuccess();
|
|
306
|
+
};
|
|
307
|
+
return (_jsx(Dialog, { open: props.open, onOpenChange: props.onOpenChange, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? closeoutMessages.editTitle : closeoutMessages.newTitle }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsx(ProductSelect, { label: closeoutMessages.productLabel, placeholder: closeoutMessages.selectProductPlaceholder, products: props.products, value: form.watch("productId"), onValueChange: (value) => form.setValue("productId", value ?? "") }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: closeoutMessages.slotLabel }), _jsxs(Select, { value: form.watch("slotId") ?? NONE_VALUE, onValueChange: (value) => form.setValue("slotId", value ?? NONE_VALUE), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: closeoutMessages.optionalSlotPlaceholder }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: NONE_VALUE, children: closeoutMessages.productLevelOption }), filteredSlots.map((slot) => (_jsx(SelectItem, { value: slot.id, children: formatSlotLocalDateTime(slotLocalStart(slot)) }, slot.id)))] })] })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: closeoutMessages.dateLabel }), _jsx(DatePicker, { value: form.watch("dateLocal") || null, onChange: (nextValue) => form.setValue("dateLocal", nextValue ?? "", {
|
|
308
|
+
shouldDirty: true,
|
|
309
|
+
shouldValidate: true,
|
|
310
|
+
}), placeholder: closeoutMessages.datePlaceholder, className: "w-full" })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: closeoutMessages.reasonLabel }), _jsx(Textarea, { ...form.register("reason"), placeholder: closeoutMessages.reasonPlaceholder })] })] }), _jsx(DialogActions, { cancel: closeoutMessages.cancel, save: closeoutMessages.save, create: closeoutMessages.create, isEditing: isEditing, isSubmitting: form.formState.isSubmitting, onCancel: () => props.onOpenChange(false) })] })] }) }));
|
|
311
|
+
}
|
|
312
|
+
function getPickupPointFormSchema(messages) {
|
|
313
|
+
return z.object({
|
|
314
|
+
productId: z.string().min(1, messages.dialogs.pickupPoint.validationProductRequired),
|
|
315
|
+
name: z.string().min(1, messages.dialogs.pickupPoint.validationNameRequired),
|
|
316
|
+
description: z.string().optional(),
|
|
317
|
+
locationText: z.string().optional(),
|
|
318
|
+
active: z.boolean(),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
export function AvailabilityPickupPointDialog(props) {
|
|
322
|
+
const pickupPointMessages = props.messages.dialogs.pickupPoint;
|
|
323
|
+
const pickupPointFormSchema = getPickupPointFormSchema(props.messages);
|
|
324
|
+
const form = useForm({
|
|
325
|
+
resolver: zodResolver(pickupPointFormSchema),
|
|
326
|
+
defaultValues: {
|
|
327
|
+
productId: "",
|
|
328
|
+
name: "",
|
|
329
|
+
description: "",
|
|
330
|
+
locationText: "",
|
|
331
|
+
active: true,
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
useEffect(() => {
|
|
335
|
+
if (props.open && props.pickupPoint) {
|
|
336
|
+
form.reset({
|
|
337
|
+
productId: props.pickupPoint.productId,
|
|
338
|
+
name: props.pickupPoint.name,
|
|
339
|
+
description: props.pickupPoint.description ?? "",
|
|
340
|
+
locationText: props.pickupPoint.locationText ?? "",
|
|
341
|
+
active: props.pickupPoint.active,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
else if (props.open) {
|
|
345
|
+
form.reset();
|
|
346
|
+
}
|
|
347
|
+
}, [form, props.open, props.pickupPoint]);
|
|
348
|
+
const isEditing = Boolean(props.pickupPoint);
|
|
349
|
+
const onSubmit = async (values) => {
|
|
350
|
+
await props.onSubmit({
|
|
351
|
+
productId: values.productId,
|
|
352
|
+
name: values.name,
|
|
353
|
+
description: nullableString(values.description),
|
|
354
|
+
locationText: nullableString(values.locationText),
|
|
355
|
+
active: values.active,
|
|
356
|
+
}, { isEditing, id: props.pickupPoint?.id });
|
|
357
|
+
props.onSuccess();
|
|
358
|
+
};
|
|
359
|
+
return (_jsx(Dialog, { open: props.open, onOpenChange: props.onOpenChange, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? pickupPointMessages.editTitle : pickupPointMessages.newTitle }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsx(ProductSelect, { label: pickupPointMessages.productLabel, placeholder: pickupPointMessages.selectProductPlaceholder, products: props.products, value: form.watch("productId"), onValueChange: (value) => form.setValue("productId", value ?? "") }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: pickupPointMessages.nameLabel }), _jsx(Input, { ...form.register("name"), placeholder: pickupPointMessages.namePlaceholder })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: pickupPointMessages.locationTextLabel }), _jsx(Input, { ...form.register("locationText"), placeholder: pickupPointMessages.locationTextPlaceholder })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: pickupPointMessages.descriptionLabel }), _jsx(Textarea, { ...form.register("description"), placeholder: pickupPointMessages.descriptionPlaceholder })] }), _jsx(SwitchField, { title: pickupPointMessages.activeTitle, description: pickupPointMessages.activeDescription, checked: form.watch("active"), onCheckedChange: (checked) => form.setValue("active", checked) })] }), _jsx(DialogActions, { cancel: pickupPointMessages.cancel, save: pickupPointMessages.save, create: pickupPointMessages.create, isEditing: isEditing, isSubmitting: form.formState.isSubmitting, onCancel: () => props.onOpenChange(false) })] })] }) }));
|
|
360
|
+
}
|
|
361
|
+
function ProductSelect({ label, placeholder, products, value, onValueChange, }) {
|
|
362
|
+
return (_jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: label }), _jsxs(Select, { items: products.map((product) => ({ label: product.name, value: product.id })), value: value, onValueChange: onValueChange, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: placeholder }) }), _jsx(SelectContent, { children: products.map((product) => (_jsx(SelectItem, { value: product.id, children: product.name }, product.id))) })] })] }));
|
|
363
|
+
}
|
|
364
|
+
function SwitchField({ title, description, checked, onCheckedChange, }) {
|
|
365
|
+
return (_jsxs("div", { className: "flex items-center justify-between rounded-md border px-3 py-2", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium", children: title }), _jsx("p", { className: "text-xs text-muted-foreground", children: description })] }), _jsx(Switch, { checked: checked, onCheckedChange: onCheckedChange })] }));
|
|
366
|
+
}
|
|
367
|
+
function DialogActions({ cancel, save, create, isEditing, isSubmitting, onCancel, }) {
|
|
368
|
+
return (_jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: onCancel, children: cancel }), _jsxs(Button, { type: "submit", disabled: isSubmitting, children: [isSubmitting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), isEditing ? save : create] })] }));
|
|
369
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { AvailabilityPickupPointRow, AvailabilityRuleRow, AvailabilitySlotRow, ProductOption } from "../index.js";
|
|
2
|
+
import { type AvailabilityColumnsMessages } from "./availability-columns.js";
|
|
3
|
+
export interface AvailabilityOverviewMessages extends AvailabilityColumnsMessages {
|
|
4
|
+
allProducts: string;
|
|
5
|
+
clearFilters: string;
|
|
6
|
+
searchPlaceholder: string;
|
|
7
|
+
overview: {
|
|
8
|
+
openSlotsTitle: string;
|
|
9
|
+
openSlotsDescription: string;
|
|
10
|
+
constrainedSlotsTitle: string;
|
|
11
|
+
constrainedSlotsDescription: string;
|
|
12
|
+
activeRulesTitle: string;
|
|
13
|
+
activeRulesDescription: string;
|
|
14
|
+
pickupPointsTitle: string;
|
|
15
|
+
pickupPointsDescription: string;
|
|
16
|
+
capacityWatchlistTitle: string;
|
|
17
|
+
capacityWatchlistEmpty: string;
|
|
18
|
+
coverageGapsTitle: string;
|
|
19
|
+
coverageGapsEmpty: string;
|
|
20
|
+
coverageGapDescription: string;
|
|
21
|
+
actionRequiredTitle: string;
|
|
22
|
+
actionRequiredBody: string;
|
|
23
|
+
actionRequiredCta: string;
|
|
24
|
+
attentionTitle: string;
|
|
25
|
+
attentionEmpty: string;
|
|
26
|
+
severityCoverageGap: string;
|
|
27
|
+
severityClosed: string;
|
|
28
|
+
severitySoldOut: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export declare function AvailabilityOverview({ messages, products, constrainedSlots, constrainedSlotsCount: providedConstrainedSlotsCount, openSlotsCount: providedOpenSlotsCount, activeRulesCount: providedActiveRulesCount, activePickupPointsCount: providedActivePickupPointsCount, filteredRules, filteredPickupPoints, productsWithoutUpcomingDepartures, productsWithoutUpcomingDeparturesCount: providedProductsWithoutUpcomingDeparturesCount, search, setSearch, productFilter, setProductFilter, hasFilters, onClearFilters, onOpenSlot, onOpenProduct, onJumpToSlots, showFilters, }: {
|
|
32
|
+
messages: AvailabilityOverviewMessages;
|
|
33
|
+
products: ProductOption[];
|
|
34
|
+
constrainedSlots: AvailabilitySlotRow[];
|
|
35
|
+
constrainedSlotsCount?: number;
|
|
36
|
+
openSlotsCount?: number;
|
|
37
|
+
activeRulesCount?: number;
|
|
38
|
+
activePickupPointsCount?: number;
|
|
39
|
+
filteredRules: AvailabilityRuleRow[];
|
|
40
|
+
filteredPickupPoints: AvailabilityPickupPointRow[];
|
|
41
|
+
productsWithoutUpcomingDepartures: ProductOption[];
|
|
42
|
+
productsWithoutUpcomingDeparturesCount?: number;
|
|
43
|
+
search: string;
|
|
44
|
+
setSearch: (value: string) => void;
|
|
45
|
+
productFilter: string;
|
|
46
|
+
setProductFilter: (value: string) => void;
|
|
47
|
+
hasFilters: boolean;
|
|
48
|
+
onClearFilters: () => void;
|
|
49
|
+
onOpenSlot: (slotId: string) => void;
|
|
50
|
+
onOpenProduct: (productId: string) => void;
|
|
51
|
+
onJumpToSlots?: () => void;
|
|
52
|
+
showFilters?: boolean;
|
|
53
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
54
|
+
//# sourceMappingURL=availability-overview.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"availability-overview.d.ts","sourceRoot":"","sources":["../../src/components/availability-overview.tsx"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EACV,0BAA0B,EAC1B,mBAAmB,EACnB,mBAAmB,EACnB,aAAa,EACd,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,KAAK,2BAA2B,EAAsB,MAAM,2BAA2B,CAAA;AAShG,MAAM,WAAW,4BAA6B,SAAQ,2BAA2B;IAC/E,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,iBAAiB,EAAE,MAAM,CAAA;IACzB,QAAQ,EAAE;QACR,cAAc,EAAE,MAAM,CAAA;QACtB,oBAAoB,EAAE,MAAM,CAAA;QAC5B,qBAAqB,EAAE,MAAM,CAAA;QAC7B,2BAA2B,EAAE,MAAM,CAAA;QACnC,gBAAgB,EAAE,MAAM,CAAA;QACxB,sBAAsB,EAAE,MAAM,CAAA;QAC9B,iBAAiB,EAAE,MAAM,CAAA;QACzB,uBAAuB,EAAE,MAAM,CAAA;QAC/B,sBAAsB,EAAE,MAAM,CAAA;QAC9B,sBAAsB,EAAE,MAAM,CAAA;QAC9B,iBAAiB,EAAE,MAAM,CAAA;QACzB,iBAAiB,EAAE,MAAM,CAAA;QACzB,sBAAsB,EAAE,MAAM,CAAA;QAC9B,mBAAmB,EAAE,MAAM,CAAA;QAC3B,kBAAkB,EAAE,MAAM,CAAA;QAC1B,iBAAiB,EAAE,MAAM,CAAA;QACzB,cAAc,EAAE,MAAM,CAAA;QACtB,cAAc,EAAE,MAAM,CAAA;QACtB,mBAAmB,EAAE,MAAM,CAAA;QAC3B,cAAc,EAAE,MAAM,CAAA;QACtB,eAAe,EAAE,MAAM,CAAA;KACxB,CAAA;CACF;AAED,wBAAgB,oBAAoB,CAAC,EACnC,QAAQ,EACR,QAAQ,EACR,gBAAgB,EAChB,qBAAqB,EAAE,6BAA6B,EACpD,cAAc,EAAE,sBAAsB,EACtC,gBAAgB,EAAE,wBAAwB,EAC1C,uBAAuB,EAAE,+BAA+B,EACxD,aAAa,EACb,oBAAoB,EACpB,iCAAiC,EACjC,sCAAsC,EAAE,8CAA8C,EACtF,MAAM,EACN,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,UAAU,EACV,cAAc,EACd,UAAU,EACV,aAAa,EACb,aAAa,EACb,WAAkB,GACnB,EAAE;IACD,QAAQ,EAAE,4BAA4B,CAAA;IACtC,QAAQ,EAAE,aAAa,EAAE,CAAA;IACzB,gBAAgB,EAAE,mBAAmB,EAAE,CAAA;IACvC,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC,aAAa,EAAE,mBAAmB,EAAE,CAAA;IACpC,oBAAoB,EAAE,0BAA0B,EAAE,CAAA;IAClD,iCAAiC,EAAE,aAAa,EAAE,CAAA;IAClD,sCAAsC,CAAC,EAAE,MAAM,CAAA;IAC/C,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,aAAa,EAAE,MAAM,CAAA;IACrB,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACzC,UAAU,EAAE,OAAO,CAAA;IACnB,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C,aAAa,CAAC,EAAE,MAAM,IAAI,CAAA;IAC1B,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB,2CA0KA"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, cn, Input, OverviewMetric, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components";
|
|
4
|
+
import { AlertTriangle, ArrowRight, CalendarDays, CheckCircle2, Clock3, Package, Search, Truck, } from "lucide-react";
|
|
5
|
+
import { useAvailabilityUiMessagesOrDefault } from "../i18n/index.js";
|
|
6
|
+
import { productNameById, slotLocalStart } from "../index.js";
|
|
7
|
+
import { getSlotStatusLabel } from "./availability-columns.js";
|
|
8
|
+
function interpolate(template, values) {
|
|
9
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => {
|
|
10
|
+
const value = values[key];
|
|
11
|
+
return value === undefined ? "" : String(value);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export function AvailabilityOverview({ messages, products, constrainedSlots, constrainedSlotsCount: providedConstrainedSlotsCount, openSlotsCount: providedOpenSlotsCount, activeRulesCount: providedActiveRulesCount, activePickupPointsCount: providedActivePickupPointsCount, filteredRules, filteredPickupPoints, productsWithoutUpcomingDepartures, productsWithoutUpcomingDeparturesCount: providedProductsWithoutUpcomingDeparturesCount, search, setSearch, productFilter, setProductFilter, hasFilters, onClearFilters, onOpenSlot, onOpenProduct, onJumpToSlots, showFilters = true, }) {
|
|
15
|
+
useAvailabilityUiMessagesOrDefault();
|
|
16
|
+
const openSlotsCount = providedOpenSlotsCount ?? constrainedSlots.filter((slot) => slot.status === "open").length;
|
|
17
|
+
const constrainedSlotsCount = providedConstrainedSlotsCount ?? constrainedSlots.length;
|
|
18
|
+
const activeRulesCount = providedActiveRulesCount ?? filteredRules.filter((rule) => rule.active).length;
|
|
19
|
+
const activePickupPointsCount = providedActivePickupPointsCount ??
|
|
20
|
+
filteredPickupPoints.filter((pickupPoint) => pickupPoint.active).length;
|
|
21
|
+
const noDeparturesCount = providedProductsWithoutUpcomingDeparturesCount ?? productsWithoutUpcomingDepartures.length;
|
|
22
|
+
const hasNoDeparturesProducts = noDeparturesCount > 0;
|
|
23
|
+
const hasConstrainedSlots = constrainedSlotsCount > 0;
|
|
24
|
+
const hasAttention = hasNoDeparturesProducts || hasConstrainedSlots;
|
|
25
|
+
return (_jsxs(_Fragment, { children: [hasNoDeparturesProducts ? (_jsxs("div", { className: "flex flex-col gap-3 rounded-lg border border-amber-300 bg-amber-50 p-4 text-amber-900 sm:flex-row sm:items-center sm:justify-between dark:border-amber-700 dark:bg-amber-950/40 dark:text-amber-100", children: [_jsxs("div", { className: "flex items-start gap-3", children: [_jsx(AlertTriangle, { className: "mt-0.5 h-5 w-5 shrink-0" }), _jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "font-medium", children: messages.overview.actionRequiredTitle }), _jsx("p", { className: "text-sm text-amber-900/80 dark:text-amber-100/80", children: interpolate(messages.overview.actionRequiredBody, { count: noDeparturesCount }) })] })] }), onJumpToSlots ? (_jsx(Button, { size: "sm", className: "self-start sm:self-auto", onClick: onJumpToSlots, children: messages.overview.actionRequiredCta })) : null] })) : null, _jsxs("div", { className: "grid gap-3 grid-cols-2 xl:grid-cols-4", children: [_jsx(OverviewMetric, { title: messages.overview.openSlotsTitle, value: openSlotsCount, description: messages.overview.openSlotsDescription, icon: CalendarDays }), _jsx(OverviewMetric, { title: messages.overview.constrainedSlotsTitle, value: constrainedSlotsCount, description: messages.overview.constrainedSlotsDescription, icon: Clock3 }), _jsx(OverviewMetric, { title: messages.overview.activeRulesTitle, value: activeRulesCount, description: messages.overview.activeRulesDescription, icon: Package }), _jsx(OverviewMetric, { title: messages.overview.pickupPointsTitle, value: activePickupPointsCount, description: messages.overview.pickupPointsDescription, icon: Truck })] }), _jsxs(Card, { size: "sm", children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between gap-3", children: [_jsxs(CardTitle, { className: "flex items-center gap-2 text-sm", children: [messages.overview.attentionTitle, hasAttention ? (_jsx(Badge, { variant: "secondary", className: "tabular-nums", children: noDeparturesCount + constrainedSlotsCount })) : null] }), !hasAttention ? (_jsxs("span", { className: "flex items-center gap-1.5 text-xs text-emerald-600 dark:text-emerald-400", children: [_jsx(CheckCircle2, { className: "h-4 w-4" }), messages.overview.attentionEmpty.split(".")[0]] })) : null] }), _jsx(CardContent, { className: "grid gap-3 lg:grid-cols-2", children: hasAttention ? (_jsxs(_Fragment, { children: [_jsx(AttentionColumn, { title: messages.overview.coverageGapsTitle, count: noDeparturesCount, items: productsWithoutUpcomingDepartures.slice(0, 4).map((product) => ({
|
|
26
|
+
id: product.id,
|
|
27
|
+
primary: product.name,
|
|
28
|
+
secondary: messages.overview.coverageGapDescription,
|
|
29
|
+
severityLabel: messages.overview.severityCoverageGap,
|
|
30
|
+
severityTone: "destructive",
|
|
31
|
+
onClick: () => onOpenProduct(product.id),
|
|
32
|
+
})), emptyMessage: messages.overview.coverageGapsEmpty }), _jsx(AttentionColumn, { title: messages.overview.capacityWatchlistTitle, count: constrainedSlotsCount, items: constrainedSlots.slice(0, 4).map((slot) => ({
|
|
33
|
+
id: slot.id,
|
|
34
|
+
primary: `${productNameById(products, slot.productId, slot.productName)} · ${slot.dateLocal}`,
|
|
35
|
+
secondary: `${formatSlotLocalDateTime(slotLocalStart(slot))} · ${messages.remainingPaxLabel}: ${slot.remainingPax ?? messages.details.noValue}`,
|
|
36
|
+
severityLabel: slot.status === "sold_out"
|
|
37
|
+
? messages.overview.severitySoldOut
|
|
38
|
+
: slot.status === "closed"
|
|
39
|
+
? messages.overview.severityClosed
|
|
40
|
+
: getSlotStatusLabel(slot.status, messages),
|
|
41
|
+
severityTone: slot.status === "sold_out" ? "default" : "outline",
|
|
42
|
+
onClick: () => onOpenSlot(slot.id),
|
|
43
|
+
})), emptyMessage: messages.overview.capacityWatchlistEmpty })] })) : (_jsx("p", { className: "col-span-full py-6 text-center text-sm text-muted-foreground", children: messages.overview.attentionEmpty })) })] }), showFilters ? (_jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [_jsxs("div", { className: "flex flex-1 flex-col gap-3 md:flex-row md:items-center", children: [_jsxs("div", { className: "relative w-full max-w-sm", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { placeholder: messages.searchPlaceholder, value: search, onChange: (event) => setSearch(event.target.value), className: "pl-9" })] }), _jsxs(Select, { value: productFilter, onValueChange: (value) => setProductFilter(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-full md:w-56", children: _jsx(SelectValue, { placeholder: messages.allProducts }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: messages.allProducts }), products.map((product) => (_jsx(SelectItem, { value: product.id, children: product.name }, product.id)))] })] })] }), hasFilters ? (_jsx(Button, { variant: "outline", onClick: onClearFilters, children: messages.clearFilters })) : null] })) : null] }));
|
|
44
|
+
}
|
|
45
|
+
function formatSlotLocalDateTime(value) {
|
|
46
|
+
return `${value.date} ${value.time}`;
|
|
47
|
+
}
|
|
48
|
+
function AttentionColumn({ title, count, items, emptyMessage, }) {
|
|
49
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center gap-2 px-1", children: [_jsx("span", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: title }), count > 0 ? (_jsx(Badge, { variant: "outline", className: "tabular-nums", children: count })) : null] }), items.length === 0 ? (_jsx("p", { className: "rounded-md border border-dashed px-3 py-4 text-center text-xs text-muted-foreground", children: emptyMessage })) : (_jsx("ul", { className: "flex flex-col gap-1.5", children: items.map((item) => (_jsx("li", { children: _jsxs("button", { type: "button", onClick: item.onClick, className: cn("group flex w-full items-start justify-between gap-3 rounded-md border bg-card px-3 py-2 text-left transition-colors", "hover:border-foreground/30 hover:bg-muted/40", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"), children: [_jsxs("div", { className: "min-w-0 flex-1 space-y-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Badge, { variant: item.severityTone, className: "text-[10px] uppercase", children: item.severityLabel }), _jsx("span", { className: "truncate text-sm font-medium", children: item.primary })] }), _jsx("p", { className: "truncate text-xs text-muted-foreground", children: item.secondary })] }), _jsx(ArrowRight, { className: "mt-1 h-4 w-4 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5" })] }) }, item.id))) }))] }));
|
|
50
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type AvailabilitySlotRow } from "../index.js";
|
|
3
|
+
import { type AvailabilitySlotSubmitPayload } from "./availability-dialogs.js";
|
|
4
|
+
import { type AvailabilityBulkDeleteFn, type AvailabilityBulkUpdateFn } from "./availability-tabs.js";
|
|
5
|
+
export type AvailabilityPageView = "list" | "calendar";
|
|
6
|
+
export type AvailabilityPageSlotStatusFilter = "all" | AvailabilitySlotRow["status"];
|
|
7
|
+
export type AvailabilityPageBulkUpdateHandler = AvailabilityBulkUpdateFn;
|
|
8
|
+
export type AvailabilityPageBulkDeleteHandler = AvailabilityBulkDeleteFn;
|
|
9
|
+
type DialogSubmitContext = {
|
|
10
|
+
isEditing: boolean;
|
|
11
|
+
id?: string;
|
|
12
|
+
};
|
|
13
|
+
export type AvailabilityPageSlotSubmitHandler = (payload: AvailabilitySlotSubmitPayload, context: DialogSubmitContext) => Promise<void>;
|
|
14
|
+
export interface AvailabilityPageSlots {
|
|
15
|
+
headerEnd?: ReactNode;
|
|
16
|
+
beforeFilters?: ReactNode;
|
|
17
|
+
afterFilters?: ReactNode;
|
|
18
|
+
dialogs?: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
export interface AvailabilityPageProps {
|
|
21
|
+
className?: string;
|
|
22
|
+
defaultView?: AvailabilityPageView;
|
|
23
|
+
bulkActionTarget?: string | null;
|
|
24
|
+
onBulkUpdate: AvailabilityPageBulkUpdateHandler;
|
|
25
|
+
onBulkDelete: AvailabilityPageBulkDeleteHandler;
|
|
26
|
+
onSlotOpen?: (slotId: string) => void;
|
|
27
|
+
onSlotSubmit?: AvailabilityPageSlotSubmitHandler;
|
|
28
|
+
slots?: AvailabilityPageSlots;
|
|
29
|
+
}
|
|
30
|
+
export declare function AvailabilityPage({ className, defaultView, bulkActionTarget, onBulkUpdate, onBulkDelete, onSlotOpen, onSlotSubmit, slots: pageSlots, }: AvailabilityPageProps): import("react/jsx-runtime").JSX.Element;
|
|
31
|
+
export {};
|
|
32
|
+
//# sourceMappingURL=availability-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"availability-page.d.ts","sourceRoot":"","sources":["../../src/components/availability-page.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,OAAO,EACL,KAAK,mBAAmB,EAUzB,MAAM,aAAa,CAAA;AACpB,OAAO,EAEL,KAAK,6BAA6B,EACnC,MAAM,2BAA2B,CAAA;AAElC,OAAO,EACL,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,EAE9B,MAAM,wBAAwB,CAAA;AAE/B,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,UAAU,CAAA;AACtD,MAAM,MAAM,gCAAgC,GAAG,KAAK,GAAG,mBAAmB,CAAC,QAAQ,CAAC,CAAA;AACpF,MAAM,MAAM,iCAAiC,GAAG,wBAAwB,CAAA;AACxE,MAAM,MAAM,iCAAiC,GAAG,wBAAwB,CAAA;AAExE,KAAK,mBAAmB,GAAG;IAAE,SAAS,EAAE,OAAO,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9D,MAAM,MAAM,iCAAiC,GAAG,CAC9C,OAAO,EAAE,6BAA6B,EACtC,OAAO,EAAE,mBAAmB,KACzB,OAAO,CAAC,IAAI,CAAC,CAAA;AAElB,MAAM,WAAW,qBAAqB;IACpC,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,aAAa,CAAC,EAAE,SAAS,CAAA;IACzB,YAAY,CAAC,EAAE,SAAS,CAAA;IACxB,OAAO,CAAC,EAAE,SAAS,CAAA;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,oBAAoB,CAAA;IAClC,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,YAAY,EAAE,iCAAiC,CAAA;IAC/C,YAAY,EAAE,iCAAiC,CAAA;IAC/C,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,YAAY,CAAC,EAAE,iCAAiC,CAAA;IAChD,KAAK,CAAC,EAAE,qBAAqB,CAAA;CAC9B;AAID,wBAAgB,gBAAgB,CAAC,EAC/B,SAAS,EACT,WAAoB,EACpB,gBAAuB,EACvB,YAAY,EACZ,YAAY,EACZ,UAAmB,EACnB,YAAY,EACZ,KAAK,EAAE,SAAS,GACjB,EAAE,qBAAqB,2CAkRvB"}
|