@voyantjs/availability-ui 0.21.1
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 +81 -0
- package/dist/components/availability-columns.d.ts +39 -0
- package/dist/components/availability-columns.d.ts.map +1 -0
- package/dist/components/availability-columns.js +160 -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 +341 -0
- package/dist/components/availability-overview.d.ts +39 -0
- package/dist/components/availability-overview.d.ts.map +1 -0
- package/dist/components/availability-overview.js +12 -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 +32 -0
- package/dist/components/availability-tabs.d.ts +141 -0
- package/dist/components/availability-tabs.d.ts.map +1 -0
- package/dist/components/availability-tabs.js +167 -0
- package/dist/form-resolver.d.ts +4 -0
- package/dist/form-resolver.d.ts.map +1 -0
- package/dist/form-resolver.js +39 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +3 -0
- package/package.json +75 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { booleanOptions, formatDateTime, NONE_VALUE, nullableNumber, nullableString, slotStatusOptions, toIsoDateTime, toLocalDateTimeInput, } from "@voyantjs/availability-react";
|
|
4
|
+
import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, Textarea, } from "@voyantjs/ui/components";
|
|
5
|
+
import { DatePicker } from "@voyantjs/ui/components/date-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
|
+
function getRuleFormSchema(messages) {
|
|
12
|
+
return z.object({
|
|
13
|
+
productId: z.string().min(1, messages.dialogs.rule.validationProductRequired),
|
|
14
|
+
timezone: z.string().min(1, messages.dialogs.rule.validationTimezoneRequired),
|
|
15
|
+
recurrenceRule: z.string().min(1, messages.dialogs.rule.validationRecurrenceRequired),
|
|
16
|
+
maxCapacity: z.coerce.number().int().min(0),
|
|
17
|
+
maxPickupCapacity: z.string().optional(),
|
|
18
|
+
minTotalPax: z.string().optional(),
|
|
19
|
+
cutoffMinutes: z.string().optional(),
|
|
20
|
+
earlyBookingLimitMinutes: z.string().optional(),
|
|
21
|
+
active: z.boolean(),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export function AvailabilityRuleDialog(props) {
|
|
25
|
+
const ruleMessages = props.messages.dialogs.rule;
|
|
26
|
+
const ruleFormSchema = getRuleFormSchema(props.messages);
|
|
27
|
+
const form = useForm({
|
|
28
|
+
resolver: zodResolver(ruleFormSchema),
|
|
29
|
+
defaultValues: {
|
|
30
|
+
productId: "",
|
|
31
|
+
timezone: "Europe/Bucharest",
|
|
32
|
+
recurrenceRule: "FREQ=DAILY;INTERVAL=1",
|
|
33
|
+
maxCapacity: 0,
|
|
34
|
+
maxPickupCapacity: "",
|
|
35
|
+
minTotalPax: "",
|
|
36
|
+
cutoffMinutes: "",
|
|
37
|
+
earlyBookingLimitMinutes: "",
|
|
38
|
+
active: true,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (props.open && props.rule) {
|
|
43
|
+
form.reset({
|
|
44
|
+
productId: props.rule.productId,
|
|
45
|
+
timezone: props.rule.timezone,
|
|
46
|
+
recurrenceRule: props.rule.recurrenceRule,
|
|
47
|
+
maxCapacity: props.rule.maxCapacity,
|
|
48
|
+
maxPickupCapacity: props.rule.maxPickupCapacity?.toString() ?? "",
|
|
49
|
+
minTotalPax: "",
|
|
50
|
+
cutoffMinutes: props.rule.cutoffMinutes?.toString() ?? "",
|
|
51
|
+
earlyBookingLimitMinutes: "",
|
|
52
|
+
active: props.rule.active,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
else if (props.open) {
|
|
56
|
+
form.reset();
|
|
57
|
+
}
|
|
58
|
+
}, [form, props.open, props.rule]);
|
|
59
|
+
const isEditing = Boolean(props.rule);
|
|
60
|
+
const onSubmit = async (values) => {
|
|
61
|
+
await props.onSubmit({
|
|
62
|
+
productId: values.productId,
|
|
63
|
+
timezone: values.timezone,
|
|
64
|
+
recurrenceRule: values.recurrenceRule,
|
|
65
|
+
maxCapacity: values.maxCapacity,
|
|
66
|
+
maxPickupCapacity: nullableNumber(values.maxPickupCapacity),
|
|
67
|
+
minTotalPax: nullableNumber(values.minTotalPax),
|
|
68
|
+
cutoffMinutes: nullableNumber(values.cutoffMinutes),
|
|
69
|
+
earlyBookingLimitMinutes: nullableNumber(values.earlyBookingLimitMinutes),
|
|
70
|
+
active: values.active,
|
|
71
|
+
}, { isEditing, id: props.rule?.id });
|
|
72
|
+
props.onSuccess();
|
|
73
|
+
};
|
|
74
|
+
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) })] })] }) }));
|
|
75
|
+
}
|
|
76
|
+
function getStartTimeFormSchema(messages) {
|
|
77
|
+
return z.object({
|
|
78
|
+
productId: z.string().min(1, messages.dialogs.startTime.validationProductRequired),
|
|
79
|
+
label: z.string().optional(),
|
|
80
|
+
startTimeLocal: z.string().min(1, messages.dialogs.startTime.validationStartTimeRequired),
|
|
81
|
+
durationMinutes: z.string().optional(),
|
|
82
|
+
sortOrder: z.coerce.number().int(),
|
|
83
|
+
active: z.boolean(),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
export function AvailabilityStartTimeDialog(props) {
|
|
87
|
+
const startTimeMessages = props.messages.dialogs.startTime;
|
|
88
|
+
const startTimeFormSchema = getStartTimeFormSchema(props.messages);
|
|
89
|
+
const form = useForm({
|
|
90
|
+
resolver: zodResolver(startTimeFormSchema),
|
|
91
|
+
defaultValues: {
|
|
92
|
+
productId: "",
|
|
93
|
+
label: "",
|
|
94
|
+
startTimeLocal: "09:00",
|
|
95
|
+
durationMinutes: "",
|
|
96
|
+
sortOrder: 0,
|
|
97
|
+
active: true,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (props.open && props.startTime) {
|
|
102
|
+
form.reset({
|
|
103
|
+
productId: props.startTime.productId,
|
|
104
|
+
label: props.startTime.label ?? "",
|
|
105
|
+
startTimeLocal: props.startTime.startTimeLocal,
|
|
106
|
+
durationMinutes: props.startTime.durationMinutes?.toString() ?? "",
|
|
107
|
+
sortOrder: props.startTime.sortOrder,
|
|
108
|
+
active: props.startTime.active,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
else if (props.open) {
|
|
112
|
+
form.reset();
|
|
113
|
+
}
|
|
114
|
+
}, [form, props.open, props.startTime]);
|
|
115
|
+
const isEditing = Boolean(props.startTime);
|
|
116
|
+
const onSubmit = async (values) => {
|
|
117
|
+
await props.onSubmit({
|
|
118
|
+
productId: values.productId,
|
|
119
|
+
label: nullableString(values.label),
|
|
120
|
+
startTimeLocal: values.startTimeLocal,
|
|
121
|
+
durationMinutes: nullableNumber(values.durationMinutes),
|
|
122
|
+
sortOrder: values.sortOrder,
|
|
123
|
+
active: values.active,
|
|
124
|
+
}, { isEditing, id: props.startTime?.id });
|
|
125
|
+
props.onSuccess();
|
|
126
|
+
};
|
|
127
|
+
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) })] })] }) }));
|
|
128
|
+
}
|
|
129
|
+
function getSlotFormSchema(messages) {
|
|
130
|
+
return z.object({
|
|
131
|
+
productId: z.string().min(1, messages.dialogs.slot.validationProductRequired),
|
|
132
|
+
availabilityRuleId: z.string().optional(),
|
|
133
|
+
startTimeId: z.string().optional(),
|
|
134
|
+
dateLocal: z.string().min(1, messages.dialogs.slot.validationDateRequired),
|
|
135
|
+
startsAt: z.string().min(1, messages.dialogs.slot.validationStartsAtRequired),
|
|
136
|
+
endsAt: z.string().optional(),
|
|
137
|
+
timezone: z.string().min(1, messages.dialogs.slot.validationTimezoneRequired),
|
|
138
|
+
status: z.enum(["open", "closed", "sold_out", "cancelled"]),
|
|
139
|
+
unlimited: z.boolean(),
|
|
140
|
+
initialPax: z.string().optional(),
|
|
141
|
+
remainingPax: z.string().optional(),
|
|
142
|
+
initialPickups: z.string().optional(),
|
|
143
|
+
remainingPickups: z.string().optional(),
|
|
144
|
+
remainingResources: z.string().optional(),
|
|
145
|
+
pastCutoff: z.boolean(),
|
|
146
|
+
tooEarly: z.boolean(),
|
|
147
|
+
notes: z.string().optional(),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
export function AvailabilitySlotDialog(props) {
|
|
151
|
+
const slotMessages = props.messages.dialogs.slot;
|
|
152
|
+
const slotFormSchema = getSlotFormSchema(props.messages);
|
|
153
|
+
const form = useForm({
|
|
154
|
+
resolver: zodResolver(slotFormSchema),
|
|
155
|
+
defaultValues: {
|
|
156
|
+
productId: "",
|
|
157
|
+
availabilityRuleId: NONE_VALUE,
|
|
158
|
+
startTimeId: NONE_VALUE,
|
|
159
|
+
dateLocal: "",
|
|
160
|
+
startsAt: "",
|
|
161
|
+
endsAt: "",
|
|
162
|
+
timezone: "Europe/Bucharest",
|
|
163
|
+
status: "open",
|
|
164
|
+
unlimited: false,
|
|
165
|
+
initialPax: "",
|
|
166
|
+
remainingPax: "",
|
|
167
|
+
initialPickups: "",
|
|
168
|
+
remainingPickups: "",
|
|
169
|
+
remainingResources: "",
|
|
170
|
+
pastCutoff: false,
|
|
171
|
+
tooEarly: false,
|
|
172
|
+
notes: "",
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (props.open && props.slot) {
|
|
177
|
+
form.reset({
|
|
178
|
+
productId: props.slot.productId,
|
|
179
|
+
availabilityRuleId: props.slot.availabilityRuleId ?? NONE_VALUE,
|
|
180
|
+
startTimeId: props.slot.startTimeId ?? NONE_VALUE,
|
|
181
|
+
dateLocal: props.slot.dateLocal,
|
|
182
|
+
startsAt: toLocalDateTimeInput(props.slot.startsAt),
|
|
183
|
+
endsAt: toLocalDateTimeInput(props.slot.endsAt),
|
|
184
|
+
timezone: props.slot.timezone,
|
|
185
|
+
status: props.slot.status,
|
|
186
|
+
unlimited: props.slot.unlimited,
|
|
187
|
+
initialPax: props.slot.initialPax?.toString() ?? "",
|
|
188
|
+
remainingPax: props.slot.remainingPax?.toString() ?? "",
|
|
189
|
+
initialPickups: "",
|
|
190
|
+
remainingPickups: "",
|
|
191
|
+
remainingResources: "",
|
|
192
|
+
pastCutoff: false,
|
|
193
|
+
tooEarly: false,
|
|
194
|
+
notes: props.slot.notes ?? "",
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
else if (props.open) {
|
|
198
|
+
form.reset();
|
|
199
|
+
}
|
|
200
|
+
}, [form, props.open, props.slot]);
|
|
201
|
+
const selectedProductId = form.watch("productId");
|
|
202
|
+
const filteredRules = props.rules.filter((rule) => rule.productId === selectedProductId);
|
|
203
|
+
const filteredStartTimes = props.startTimes.filter((startTime) => startTime.productId === selectedProductId);
|
|
204
|
+
const isEditing = Boolean(props.slot);
|
|
205
|
+
const onSubmit = async (values) => {
|
|
206
|
+
await props.onSubmit({
|
|
207
|
+
productId: values.productId,
|
|
208
|
+
availabilityRuleId: values.availabilityRuleId === NONE_VALUE ? null : (values.availabilityRuleId ?? null),
|
|
209
|
+
startTimeId: values.startTimeId === NONE_VALUE ? null : (values.startTimeId ?? null),
|
|
210
|
+
dateLocal: values.dateLocal,
|
|
211
|
+
startsAt: new Date(values.startsAt).toISOString(),
|
|
212
|
+
endsAt: toIsoDateTime(values.endsAt),
|
|
213
|
+
timezone: values.timezone,
|
|
214
|
+
status: values.status,
|
|
215
|
+
unlimited: values.unlimited,
|
|
216
|
+
initialPax: nullableNumber(values.initialPax),
|
|
217
|
+
remainingPax: nullableNumber(values.remainingPax),
|
|
218
|
+
initialPickups: nullableNumber(values.initialPickups),
|
|
219
|
+
remainingPickups: nullableNumber(values.remainingPickups),
|
|
220
|
+
remainingResources: nullableNumber(values.remainingResources),
|
|
221
|
+
pastCutoff: values.pastCutoff,
|
|
222
|
+
tooEarly: values.tooEarly,
|
|
223
|
+
notes: nullableString(values.notes),
|
|
224
|
+
}, { isEditing, id: props.slot?.id });
|
|
225
|
+
props.onSuccess();
|
|
226
|
+
};
|
|
227
|
+
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(Input, { ...form.register("dateLocal"), type: "date" })] }), _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(Input, { ...form.register("startsAt"), type: "datetime-local" })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: slotMessages.endsAtLabel }), _jsx(Input, { ...form.register("endsAt"), type: "datetime-local" })] })] }), _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: {
|
|
228
|
+
open: props.messages.statusOpen,
|
|
229
|
+
closed: props.messages.statusClosed,
|
|
230
|
+
sold_out: props.messages.statusSoldOut,
|
|
231
|
+
cancelled: props.messages.statusCancelled,
|
|
232
|
+
}[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) })] })] }) }));
|
|
233
|
+
}
|
|
234
|
+
function getCloseoutFormSchema(messages) {
|
|
235
|
+
return z.object({
|
|
236
|
+
productId: z.string().min(1, messages.dialogs.closeout.validationProductRequired),
|
|
237
|
+
slotId: z.string().optional(),
|
|
238
|
+
dateLocal: z.string().min(1, messages.dialogs.closeout.validationDateRequired),
|
|
239
|
+
reason: z.string().optional(),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
export function AvailabilityCloseoutDialog(props) {
|
|
243
|
+
const closeoutMessages = props.messages.dialogs.closeout;
|
|
244
|
+
const closeoutFormSchema = getCloseoutFormSchema(props.messages);
|
|
245
|
+
const form = useForm({
|
|
246
|
+
resolver: zodResolver(closeoutFormSchema),
|
|
247
|
+
defaultValues: {
|
|
248
|
+
productId: "",
|
|
249
|
+
slotId: NONE_VALUE,
|
|
250
|
+
dateLocal: "",
|
|
251
|
+
reason: "",
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
useEffect(() => {
|
|
255
|
+
if (props.open && props.closeout) {
|
|
256
|
+
form.reset({
|
|
257
|
+
productId: props.closeout.productId,
|
|
258
|
+
slotId: props.closeout.slotId ?? NONE_VALUE,
|
|
259
|
+
dateLocal: props.closeout.dateLocal,
|
|
260
|
+
reason: props.closeout.reason ?? "",
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
else if (props.open) {
|
|
264
|
+
form.reset();
|
|
265
|
+
}
|
|
266
|
+
}, [form, props.closeout, props.open]);
|
|
267
|
+
const selectedProductId = form.watch("productId");
|
|
268
|
+
const filteredSlots = props.slots.filter((slot) => slot.productId === selectedProductId);
|
|
269
|
+
const isEditing = Boolean(props.closeout);
|
|
270
|
+
const onSubmit = async (values) => {
|
|
271
|
+
await props.onSubmit({
|
|
272
|
+
productId: values.productId,
|
|
273
|
+
slotId: values.slotId === NONE_VALUE ? null : (values.slotId ?? null),
|
|
274
|
+
dateLocal: values.dateLocal,
|
|
275
|
+
reason: nullableString(values.reason),
|
|
276
|
+
}, { isEditing, id: props.closeout?.id });
|
|
277
|
+
props.onSuccess();
|
|
278
|
+
};
|
|
279
|
+
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) => (_jsxs(SelectItem, { value: slot.id, children: [slot.dateLocal, " \u00B7 ", formatDateTime(slot.startsAt)] }, 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 ?? "", {
|
|
280
|
+
shouldDirty: true,
|
|
281
|
+
shouldValidate: true,
|
|
282
|
+
}), 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) })] })] }) }));
|
|
283
|
+
}
|
|
284
|
+
function getPickupPointFormSchema(messages) {
|
|
285
|
+
return z.object({
|
|
286
|
+
productId: z.string().min(1, messages.dialogs.pickupPoint.validationProductRequired),
|
|
287
|
+
name: z.string().min(1, messages.dialogs.pickupPoint.validationNameRequired),
|
|
288
|
+
description: z.string().optional(),
|
|
289
|
+
locationText: z.string().optional(),
|
|
290
|
+
active: z.boolean(),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
export function AvailabilityPickupPointDialog(props) {
|
|
294
|
+
const pickupPointMessages = props.messages.dialogs.pickupPoint;
|
|
295
|
+
const pickupPointFormSchema = getPickupPointFormSchema(props.messages);
|
|
296
|
+
const form = useForm({
|
|
297
|
+
resolver: zodResolver(pickupPointFormSchema),
|
|
298
|
+
defaultValues: {
|
|
299
|
+
productId: "",
|
|
300
|
+
name: "",
|
|
301
|
+
description: "",
|
|
302
|
+
locationText: "",
|
|
303
|
+
active: true,
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
if (props.open && props.pickupPoint) {
|
|
308
|
+
form.reset({
|
|
309
|
+
productId: props.pickupPoint.productId,
|
|
310
|
+
name: props.pickupPoint.name,
|
|
311
|
+
description: props.pickupPoint.description ?? "",
|
|
312
|
+
locationText: props.pickupPoint.locationText ?? "",
|
|
313
|
+
active: props.pickupPoint.active,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
else if (props.open) {
|
|
317
|
+
form.reset();
|
|
318
|
+
}
|
|
319
|
+
}, [form, props.open, props.pickupPoint]);
|
|
320
|
+
const isEditing = Boolean(props.pickupPoint);
|
|
321
|
+
const onSubmit = async (values) => {
|
|
322
|
+
await props.onSubmit({
|
|
323
|
+
productId: values.productId,
|
|
324
|
+
name: values.name,
|
|
325
|
+
description: nullableString(values.description),
|
|
326
|
+
locationText: nullableString(values.locationText),
|
|
327
|
+
active: values.active,
|
|
328
|
+
}, { isEditing, id: props.pickupPoint?.id });
|
|
329
|
+
props.onSuccess();
|
|
330
|
+
};
|
|
331
|
+
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) })] })] }) }));
|
|
332
|
+
}
|
|
333
|
+
function ProductSelect({ label, placeholder, products, value, onValueChange, }) {
|
|
334
|
+
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))) })] })] }));
|
|
335
|
+
}
|
|
336
|
+
function SwitchField({ title, description, checked, onCheckedChange, }) {
|
|
337
|
+
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 })] }));
|
|
338
|
+
}
|
|
339
|
+
function DialogActions({ cancel, save, create, isEditing, isSubmitting, onCancel, }) {
|
|
340
|
+
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] })] }));
|
|
341
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AvailabilityPickupPointRow, AvailabilityRuleRow, AvailabilitySlotRow, ProductOption } from "@voyantjs/availability-react";
|
|
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
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export declare function AvailabilityOverview({ messages, products, constrainedSlots, filteredRules, filteredPickupPoints, productsWithoutActiveRules, search, setSearch, productFilter, setProductFilter, hasFilters, onClearFilters, onOpenSlot, onOpenProduct, }: {
|
|
24
|
+
messages: AvailabilityOverviewMessages;
|
|
25
|
+
products: ProductOption[];
|
|
26
|
+
constrainedSlots: AvailabilitySlotRow[];
|
|
27
|
+
filteredRules: AvailabilityRuleRow[];
|
|
28
|
+
filteredPickupPoints: AvailabilityPickupPointRow[];
|
|
29
|
+
productsWithoutActiveRules: ProductOption[];
|
|
30
|
+
search: string;
|
|
31
|
+
setSearch: (value: string) => void;
|
|
32
|
+
productFilter: string;
|
|
33
|
+
setProductFilter: (value: string) => void;
|
|
34
|
+
hasFilters: boolean;
|
|
35
|
+
onClearFilters: () => void;
|
|
36
|
+
onOpenSlot: (slotId: string) => void;
|
|
37
|
+
onOpenProduct: (productId: string) => void;
|
|
38
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
39
|
+
//# 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":"AAEA,OAAO,KAAK,EACV,0BAA0B,EAC1B,mBAAmB,EACnB,mBAAmB,EACnB,aAAa,EACd,MAAM,8BAA8B,CAAA;AAiBrC,OAAO,EAAE,KAAK,2BAA2B,EAAsB,MAAM,2BAA2B,CAAA;AAEhG,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;KAC/B,CAAA;CACF;AAED,wBAAgB,oBAAoB,CAAC,EACnC,QAAQ,EACR,QAAQ,EACR,gBAAgB,EAChB,aAAa,EACb,oBAAoB,EACpB,0BAA0B,EAC1B,MAAM,EACN,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,UAAU,EACV,cAAc,EACd,UAAU,EACV,aAAa,GACd,EAAE;IACD,QAAQ,EAAE,4BAA4B,CAAA;IACtC,QAAQ,EAAE,aAAa,EAAE,CAAA;IACzB,gBAAgB,EAAE,mBAAmB,EAAE,CAAA;IACvC,aAAa,EAAE,mBAAmB,EAAE,CAAA;IACpC,oBAAoB,EAAE,0BAA0B,EAAE,CAAA;IAClD,0BAA0B,EAAE,aAAa,EAAE,CAAA;IAC3C,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;CAC3C,2CA4HA"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { formatDateTime, productNameById } from "@voyantjs/availability-react";
|
|
4
|
+
import { Button, Card, CardContent, CardHeader, CardTitle, Input, OverviewMetric, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components";
|
|
5
|
+
import { CalendarDays, Clock3, Package, Search, Truck } from "lucide-react";
|
|
6
|
+
import { getSlotStatusLabel } from "./availability-columns.js";
|
|
7
|
+
export function AvailabilityOverview({ messages, products, constrainedSlots, filteredRules, filteredPickupPoints, productsWithoutActiveRules, search, setSearch, productFilter, setProductFilter, hasFilters, onClearFilters, onOpenSlot, onOpenProduct, }) {
|
|
8
|
+
const openSlotsCount = constrainedSlots.filter((slot) => slot.status === "open").length;
|
|
9
|
+
const activeRulesCount = filteredRules.filter((rule) => rule.active).length;
|
|
10
|
+
const activePickupPointsCount = filteredPickupPoints.filter((pickupPoint) => pickupPoint.active).length;
|
|
11
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "grid gap-4 md: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: constrainedSlots.length, 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("div", { className: "grid gap-4 xl:grid-cols-2", children: [_jsxs(Card, { size: "sm", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: messages.overview.capacityWatchlistTitle }) }), _jsx(CardContent, { className: "space-y-3 text-sm", children: constrainedSlots.length === 0 ? (_jsx("p", { className: "text-muted-foreground", children: messages.overview.capacityWatchlistEmpty })) : (constrainedSlots.slice(0, 4).map((slot) => (_jsxs("button", { type: "button", className: "block w-full rounded-md border p-3 text-left hover:bg-muted/40", onClick: () => onOpenSlot(slot.id), children: [_jsxs("div", { className: "font-medium", children: [productNameById(products, slot.productId, slot.productName), " \u00B7 ", slot.dateLocal] }), _jsxs("div", { className: "text-muted-foreground", children: [formatDateTime(slot.startsAt), " \u00B7 ", getSlotStatusLabel(slot.status, messages), " \u00B7", " ", messages.remainingPaxLabel, ": ", slot.remainingPax ?? messages.details.noValue] })] }, slot.id)))) })] }), _jsxs(Card, { size: "sm", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: messages.overview.coverageGapsTitle }) }), _jsx(CardContent, { className: "space-y-3 text-sm", children: productsWithoutActiveRules.length === 0 ? (_jsx("p", { className: "text-muted-foreground", children: messages.overview.coverageGapsEmpty })) : (productsWithoutActiveRules.slice(0, 4).map((product) => (_jsxs("button", { type: "button", className: "block w-full rounded-md border p-3 text-left hover:bg-muted/40", onClick: () => onOpenProduct(product.id), children: [_jsx("div", { className: "font-medium", children: product.name }), _jsx("div", { className: "text-muted-foreground", children: messages.overview.coverageGapDescription })] }, product.id)))) })] })] }), _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] })] }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface AvailabilitySectionHeaderProps {
|
|
2
|
+
actionLabel: string;
|
|
3
|
+
description: string;
|
|
4
|
+
onAction: () => void;
|
|
5
|
+
title: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function AvailabilitySectionHeader({ actionLabel, description, onAction, title, }: AvailabilitySectionHeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=availability-section-header.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"availability-section-header.d.ts","sourceRoot":"","sources":["../../src/components/availability-section-header.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,8BAA8B;IAC7C,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;CACd;AAED,wBAAgB,yBAAyB,CAAC,EACxC,WAAW,EACX,WAAW,EACX,QAAQ,EACR,KAAK,GACN,EAAE,8BAA8B,2CAahC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Button } from "@voyantjs/ui/components";
|
|
4
|
+
import { Plus } from "lucide-react";
|
|
5
|
+
export function AvailabilitySectionHeader({ actionLabel, description, onAction, title, }) {
|
|
6
|
+
return (_jsxs("div", { className: "flex items-center justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-lg font-semibold", children: title }), _jsx("p", { className: "text-sm text-muted-foreground", children: description })] }), _jsxs(Button, { onClick: onAction, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), actionLabel] })] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function AvailabilityBodySkeleton(): import("react/jsx-runtime").JSX.Element;
|
|
2
|
+
export declare function AvailabilityPageSkeleton(): import("react/jsx-runtime").JSX.Element;
|
|
3
|
+
export declare function AvailabilityRuleDetailSkeleton(): import("react/jsx-runtime").JSX.Element;
|
|
4
|
+
export declare function AvailabilityStartTimeDetailSkeleton(): import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
export declare function AvailabilitySlotDetailSkeleton(): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
//# sourceMappingURL=availability-skeletons.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"availability-skeletons.d.ts","sourceRoot":"","sources":["../../src/components/availability-skeletons.tsx"],"names":[],"mappings":"AAWA,wBAAgB,wBAAwB,4CAqEvC;AAED,wBAAgB,wBAAwB,4CAUvC;AAED,wBAAgB,8BAA8B,4CAmE7C;AAED,wBAAgB,mCAAmC,4CAqDlD;AAED,wBAAgB,8BAA8B,4CA6D7C"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Card, CardContent, CardHeader } from "@voyantjs/ui/components/card";
|
|
3
|
+
import { Skeleton } from "@voyantjs/ui/components/skeleton";
|
|
4
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@voyantjs/ui/components/table";
|
|
5
|
+
export function AvailabilityBodySkeleton() {
|
|
6
|
+
return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Skeleton, { className: "h-9 w-full max-w-sm" }), _jsx(Skeleton, { className: "h-9 w-44" })] }), _jsx("div", { className: "grid gap-4 md:grid-cols-2 lg:grid-cols-4", children: Array.from({ length: 4 }).map((_, i) => (_jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-2", children: _jsx(Skeleton, { className: "h-3.5 w-28" }) }), _jsxs(CardContent, { className: "space-y-2", children: [_jsx(Skeleton, { className: "h-7 w-20" }), _jsx(Skeleton, { className: "h-3 w-40" })] })] }, i))) }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(Skeleton, { className: "h-5 w-52" }) }), _jsx(CardContent, { className: "space-y-2", children: Array.from({ length: 3 }).map((_, i) => (_jsxs("div", { className: "flex items-center justify-between rounded-md border px-3 py-2", children: [_jsx(Skeleton, { className: "h-4 w-48" }), _jsx(Skeleton, { className: "h-4 w-32" })] }, i))) })] }), _jsxs("div", { className: "flex items-center gap-1 border-b", children: [_jsx(Skeleton, { className: "h-9 w-20" }), _jsx(Skeleton, { className: "h-9 w-20" }), _jsx(Skeleton, { className: "h-9 w-28" }), _jsx(Skeleton, { className: "h-9 w-24" }), _jsx(Skeleton, { className: "h-9 w-32" })] }), _jsx("div", { className: "rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Date" }), _jsx(TableHead, { children: "Product" }), _jsx(TableHead, { children: "Status" }), _jsx(TableHead, { children: "Remaining" }), _jsx(TableHead, { children: "Capacity" })] }) }), _jsx(SkeletonTableRows, { rows: 6, columns: 5, columnWidths: ["w-24", "w-40", "w-16", "w-16", "w-16"] })] }) })] }));
|
|
7
|
+
}
|
|
8
|
+
export function AvailabilityPageSkeleton() {
|
|
9
|
+
return (_jsxs("div", { className: "flex flex-col gap-6 p-6", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Skeleton, { className: "h-7 w-40" }), _jsx(Skeleton, { className: "h-4 w-96" })] }), _jsx(AvailabilityBodySkeleton, {})] }));
|
|
10
|
+
}
|
|
11
|
+
export function AvailabilityRuleDetailSkeleton() {
|
|
12
|
+
return (_jsxs("div", { className: "flex flex-col gap-6 p-6", children: [_jsxs("div", { className: "flex items-center gap-4", children: [_jsx(Skeleton, { className: "h-9 w-9 rounded-md" }), _jsxs("div", { className: "flex-1 space-y-2", children: [_jsx(Skeleton, { className: "h-7 w-48" }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Skeleton, { className: "h-5 w-14 rounded-full" }), _jsx(Skeleton, { className: "h-5 w-24 rounded" })] })] }), _jsx(Skeleton, { className: "h-9 w-32" }), _jsx(Skeleton, { className: "h-9 w-24" })] }), _jsxs("div", { className: "grid gap-6 md:grid-cols-2", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(Skeleton, { className: "h-5 w-24" }) }), _jsxs(CardContent, { className: "grid gap-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Skeleton, { className: "h-3.5 w-20" }), _jsx(Skeleton, { className: "h-3.5 w-40" })] }), _jsx(Skeleton, { className: "h-3.5 w-24" }), _jsx(Skeleton, { className: "h-24 w-full rounded-md" })] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(Skeleton, { className: "h-5 w-32" }) }), _jsx(CardContent, { className: "grid gap-3", children: Array.from({ length: 5 }).map((_, i) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Skeleton, { className: "h-3.5 w-32" }), _jsx(Skeleton, { className: "h-3.5 w-16" })] }, i))) })] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center gap-2", children: [_jsx(Skeleton, { className: "h-4 w-4" }), _jsx(Skeleton, { className: "h-5 w-36" })] }), _jsx(CardContent, { className: "space-y-2", children: Array.from({ length: 4 }).map((_, i) => (_jsxs("div", { className: "flex items-center justify-between rounded-md border px-3 py-2", children: [_jsx(Skeleton, { className: "h-4 w-48" }), _jsx(Skeleton, { className: "h-5 w-16 rounded-full" })] }, i))) })] })] }));
|
|
13
|
+
}
|
|
14
|
+
export function AvailabilityStartTimeDetailSkeleton() {
|
|
15
|
+
return (_jsxs("div", { className: "flex flex-col gap-6 p-6", children: [_jsxs("div", { className: "flex items-center gap-4", children: [_jsx(Skeleton, { className: "h-9 w-9 rounded-md" }), _jsxs("div", { className: "flex-1 space-y-2", children: [_jsx(Skeleton, { className: "h-7 w-56" }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Skeleton, { className: "h-5 w-16 rounded" }), _jsx(Skeleton, { className: "h-5 w-14 rounded-full" })] })] }), _jsx(Skeleton, { className: "h-9 w-32" }), _jsx(Skeleton, { className: "h-9 w-24" })] }), _jsxs("div", { className: "grid gap-6 md:grid-cols-2", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(Skeleton, { className: "h-5 w-40" }) }), _jsx(CardContent, { className: "grid gap-3", children: Array.from({ length: 6 }).map((_, i) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Skeleton, { className: "h-3.5 w-24" }), _jsx(Skeleton, { className: "h-3.5 w-32" })] }, i))) })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center gap-2", children: [_jsx(Skeleton, { className: "h-4 w-4" }), _jsx(Skeleton, { className: "h-5 w-36" })] }), _jsx(CardContent, { className: "space-y-3", children: Array.from({ length: 3 }).map((_, i) => (_jsxs("div", { className: "rounded-md border p-3 space-y-1.5", children: [_jsx(Skeleton, { className: "h-4 w-48" }), _jsx(Skeleton, { className: "h-3 w-32" })] }, i))) })] })] })] }));
|
|
16
|
+
}
|
|
17
|
+
export function AvailabilitySlotDetailSkeleton() {
|
|
18
|
+
return (_jsxs("div", { className: "flex flex-col gap-6 p-6", children: [_jsxs("div", { className: "flex items-center gap-4", children: [_jsx(Skeleton, { className: "h-9 w-9 rounded-md" }), _jsxs("div", { className: "flex-1 space-y-2", children: [_jsx(Skeleton, { className: "h-7 w-72" }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Skeleton, { className: "h-5 w-16 rounded-full" }), _jsx(Skeleton, { className: "h-5 w-24 rounded" })] })] }), _jsx(Skeleton, { className: "h-9 w-32" }), _jsx(Skeleton, { className: "h-9 w-24" })] }), _jsxs("div", { className: "grid gap-6 md:grid-cols-2", children: [_jsx(PairListCard, { titleWidth: "w-24", rows: 7 }), _jsx(PairListCard, { titleWidth: "w-28", rows: 7 })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center gap-2", children: [_jsx(Skeleton, { className: "h-4 w-4" }), _jsx(Skeleton, { className: "h-5 w-36" })] }), _jsx(CardContent, { className: "space-y-3", children: Array.from({ length: 3 }).map((_, i) => (_jsxs("div", { className: "rounded-md border p-3 space-y-1.5", children: [_jsx(Skeleton, { className: "h-4 w-40" }), _jsx(Skeleton, { className: "h-3 w-56" }), _jsx(Skeleton, { className: "h-3 w-44" })] }, i))) })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center gap-2", children: [_jsx(Skeleton, { className: "h-4 w-4" }), _jsx(Skeleton, { className: "h-5 w-44" })] }), _jsx(CardContent, { className: "space-y-2", children: Array.from({ length: 2 }).map((_, i) => (_jsxs("div", { className: "flex items-center justify-between rounded-md border px-3 py-2", children: [_jsx(Skeleton, { className: "h-4 w-48" }), _jsx(Skeleton, { className: "h-5 w-16 rounded-full" })] }, i))) })] })] }));
|
|
19
|
+
}
|
|
20
|
+
function PairListCard({ titleWidth, rows }) {
|
|
21
|
+
return (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(Skeleton, { className: `h-5 ${titleWidth}` }) }), _jsx(CardContent, { className: "grid gap-3", children: Array.from({ length: rows }).map((_, i) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Skeleton, { className: "h-3.5 w-28" }), _jsx(Skeleton, { className: "h-3.5 w-32" })] }, i))) })] }));
|
|
22
|
+
}
|
|
23
|
+
function SkeletonTableRows({ rows = 6, columns = 4, columnWidths, }) {
|
|
24
|
+
return (_jsx(TableBody, { children: Array.from({ length: rows }).map((_, r) => (_jsx(TableRow
|
|
25
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: stable placeholders
|
|
26
|
+
, { children: Array.from({ length: columns }).map((__, c) => {
|
|
27
|
+
const width = columnWidths?.[c] ?? (c === 0 ? "w-2/3" : c === columns - 1 ? "w-16" : "w-1/2");
|
|
28
|
+
return (_jsx(TableCell
|
|
29
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: stable placeholders
|
|
30
|
+
, { children: _jsx(Skeleton, { className: `h-4 ${width}` }) }, c));
|
|
31
|
+
}) }, r))) }));
|
|
32
|
+
}
|