@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.
Files changed (81) hide show
  1. package/README.md +161 -1
  2. package/dist/admin/availability-index-host.d.ts +12 -0
  3. package/dist/admin/availability-index-host.d.ts.map +1 -0
  4. package/dist/admin/availability-index-host.js +125 -0
  5. package/dist/admin/availability-page-data.d.ts +9 -0
  6. package/dist/admin/availability-page-data.d.ts.map +1 -0
  7. package/dist/admin/availability-page-data.js +25 -0
  8. package/dist/admin/index.d.ts +72 -0
  9. package/dist/admin/index.d.ts.map +1 -0
  10. package/dist/admin/index.js +132 -0
  11. package/dist/admin/option-resource-templates-panel.d.ts +22 -0
  12. package/dist/admin/option-resource-templates-panel.d.ts.map +1 -0
  13. package/dist/admin/option-resource-templates-panel.js +251 -0
  14. package/dist/admin/pages/availability-rule-detail-page.d.ts +9 -0
  15. package/dist/admin/pages/availability-rule-detail-page.d.ts.map +1 -0
  16. package/dist/admin/pages/availability-rule-detail-page.js +11 -0
  17. package/dist/admin/pages/availability-slot-detail-page.d.ts +9 -0
  18. package/dist/admin/pages/availability-slot-detail-page.d.ts.map +1 -0
  19. package/dist/admin/pages/availability-slot-detail-page.js +11 -0
  20. package/dist/admin/pages/availability-start-time-detail-page.d.ts +9 -0
  21. package/dist/admin/pages/availability-start-time-detail-page.d.ts.map +1 -0
  22. package/dist/admin/pages/availability-start-time-detail-page.js +11 -0
  23. package/dist/admin/rule-detail-host.d.ts +14 -0
  24. package/dist/admin/rule-detail-host.d.ts.map +1 -0
  25. package/dist/admin/rule-detail-host.js +27 -0
  26. package/dist/admin/slot-detail-host.d.ts +29 -0
  27. package/dist/admin/slot-detail-host.d.ts.map +1 -0
  28. package/dist/admin/slot-detail-host.js +110 -0
  29. package/dist/admin/start-time-detail-host.d.ts +15 -0
  30. package/dist/admin/start-time-detail-host.d.ts.map +1 -0
  31. package/dist/admin/start-time-detail-host.js +37 -0
  32. package/dist/components/availability-columns.d.ts +42 -0
  33. package/dist/components/availability-columns.d.ts.map +1 -0
  34. package/dist/components/availability-columns.js +182 -0
  35. package/dist/components/availability-dialogs.d.ts +236 -0
  36. package/dist/components/availability-dialogs.d.ts.map +1 -0
  37. package/dist/components/availability-dialogs.js +369 -0
  38. package/dist/components/availability-overview.d.ts +54 -0
  39. package/dist/components/availability-overview.d.ts.map +1 -0
  40. package/dist/components/availability-overview.js +50 -0
  41. package/dist/components/availability-page.d.ts +32 -0
  42. package/dist/components/availability-page.d.ts.map +1 -0
  43. package/dist/components/availability-page.js +128 -0
  44. package/dist/components/availability-rule-detail-page.d.ts +251 -0
  45. package/dist/components/availability-rule-detail-page.d.ts.map +1 -0
  46. package/dist/components/availability-rule-detail-page.js +74 -0
  47. package/dist/components/availability-section-header.d.ts +8 -0
  48. package/dist/components/availability-section-header.d.ts.map +1 -0
  49. package/dist/components/availability-section-header.js +7 -0
  50. package/dist/components/availability-skeletons.d.ts +6 -0
  51. package/dist/components/availability-skeletons.d.ts.map +1 -0
  52. package/dist/components/availability-skeletons.js +34 -0
  53. package/dist/components/availability-slot-detail-page.d.ts +974 -0
  54. package/dist/components/availability-slot-detail-page.d.ts.map +1 -0
  55. package/dist/components/availability-slot-detail-page.js +383 -0
  56. package/dist/components/availability-start-time-detail-page.d.ts +246 -0
  57. package/dist/components/availability-start-time-detail-page.d.ts.map +1 -0
  58. package/dist/components/availability-start-time-detail-page.js +83 -0
  59. package/dist/components/availability-tabs.d.ts +152 -0
  60. package/dist/components/availability-tabs.d.ts.map +1 -0
  61. package/dist/components/availability-tabs.js +192 -0
  62. package/dist/components/slot-status-tone.d.ts +15 -0
  63. package/dist/components/slot-status-tone.d.ts.map +1 -0
  64. package/dist/components/slot-status-tone.js +18 -0
  65. package/dist/form-resolver.d.ts +4 -0
  66. package/dist/form-resolver.d.ts.map +1 -0
  67. package/dist/form-resolver.js +40 -0
  68. package/dist/i18n/index.d.ts +2 -0
  69. package/dist/i18n/index.d.ts.map +1 -0
  70. package/dist/i18n/index.js +1 -0
  71. package/dist/i18n/provider.d.ts +2003 -0
  72. package/dist/i18n/provider.d.ts.map +1 -0
  73. package/dist/i18n/provider.js +102 -0
  74. package/dist/ui.d.ts +13 -0
  75. package/dist/ui.d.ts.map +1 -0
  76. package/dist/ui.js +12 -0
  77. package/dist/utils.d.ts +1 -0
  78. package/dist/utils.d.ts.map +1 -1
  79. package/dist/utils.js +3 -0
  80. package/package.json +92 -9
  81. 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"}