@adatechnology/scheduling-ui 0.1.0-rc.4 → 0.1.0-rc.6
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/dist/index.d.ts +16 -1
- package/dist/index.js +1303 -724
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -313,10 +313,11 @@ var DEFAULT_SCHEDULING_WORKSPACE_LABELS = {
|
|
|
313
313
|
};
|
|
314
314
|
|
|
315
315
|
// src/workspace/SchedulingWorkspace.tsx
|
|
316
|
-
import { useState as
|
|
316
|
+
import { useState as useState14 } from "react";
|
|
317
317
|
|
|
318
318
|
// src/workspace/AgendaArea.tsx
|
|
319
|
-
import {
|
|
319
|
+
import { CalendarDays, ChevronLeft, ChevronRight } from "lucide-react";
|
|
320
|
+
import { useEffect as useEffect3, useState as useState3 } from "react";
|
|
320
321
|
import { MAX_PAGE_SIZE } from "@adatechnology/scheduling-contracts";
|
|
321
322
|
|
|
322
323
|
// src/locales/pt-BR.json
|
|
@@ -410,7 +411,22 @@ var pt_BR_default = {
|
|
|
410
411
|
"datetime.year": "Ano",
|
|
411
412
|
"datetime.month": "M\xEAs",
|
|
412
413
|
"datetime.day": "Dia",
|
|
413
|
-
"datetime.time": "Hora"
|
|
414
|
+
"datetime.time": "Hora",
|
|
415
|
+
"common.emptyHint": "Quando houver algo aqui, ele aparece nesta lista.",
|
|
416
|
+
"agenda.allResources": "Todos",
|
|
417
|
+
"agenda.viewLabel": "Visualiza\xE7\xE3o",
|
|
418
|
+
"agenda.now": "Agora",
|
|
419
|
+
"agenda.emptyTitle": "Nada agendado",
|
|
420
|
+
"resource.emptyTitle": "Nenhum recurso cadastrado",
|
|
421
|
+
"resource.emptyHint": "Recursos s\xE3o quem ou o que atende \u2014 pessoas, salas, equipamentos.",
|
|
422
|
+
"service.emptyTitle": "Nenhum servi\xE7o cadastrado",
|
|
423
|
+
"service.emptyHint": "Servi\xE7os definem o que \xE9 agendado e quanto tempo dura.",
|
|
424
|
+
"booking.emptyTitle": "Nenhuma reserva",
|
|
425
|
+
"booking.emptyHint": "Ajuste os filtros de status ou aguarde novas solicita\xE7\xF5es.",
|
|
426
|
+
"availability.selectResourceTitle": "Escolha um recurso",
|
|
427
|
+
"availability.selectResourceHint": "As regras de disponibilidade s\xE3o definidas por recurso.",
|
|
428
|
+
"select.search": "Buscar\u2026",
|
|
429
|
+
"select.noResults": "Nenhum resultado"
|
|
414
430
|
};
|
|
415
431
|
|
|
416
432
|
// src/locales/en.json
|
|
@@ -504,7 +520,22 @@ var en_default = {
|
|
|
504
520
|
"datetime.year": "Year",
|
|
505
521
|
"datetime.month": "Month",
|
|
506
522
|
"datetime.day": "Day",
|
|
507
|
-
"datetime.time": "Time"
|
|
523
|
+
"datetime.time": "Time",
|
|
524
|
+
"common.emptyHint": "When there is something here, it shows up in this list.",
|
|
525
|
+
"agenda.allResources": "All",
|
|
526
|
+
"agenda.viewLabel": "View",
|
|
527
|
+
"agenda.now": "Now",
|
|
528
|
+
"agenda.emptyTitle": "Nothing scheduled",
|
|
529
|
+
"resource.emptyTitle": "No resources yet",
|
|
530
|
+
"resource.emptyHint": "Resources are who or what serves \u2014 people, rooms, equipment.",
|
|
531
|
+
"service.emptyTitle": "No services yet",
|
|
532
|
+
"service.emptyHint": "Services define what gets booked and how long it takes.",
|
|
533
|
+
"booking.emptyTitle": "No bookings",
|
|
534
|
+
"booking.emptyHint": "Adjust the status filters or wait for new requests.",
|
|
535
|
+
"availability.selectResourceTitle": "Pick a resource",
|
|
536
|
+
"availability.selectResourceHint": "Availability rules are defined per resource.",
|
|
537
|
+
"select.search": "Search\u2026",
|
|
538
|
+
"select.noResults": "No results"
|
|
508
539
|
};
|
|
509
540
|
|
|
510
541
|
// src/locales/index.ts
|
|
@@ -517,6 +548,47 @@ function resolveSchedulingMessages(locale) {
|
|
|
517
548
|
return MESSAGES_BY_LOCALE[locale] ?? MESSAGES_BY_LOCALE[DEFAULT_SCHEDULING_LOCALE];
|
|
518
549
|
}
|
|
519
550
|
|
|
551
|
+
// src/components/BookingStatusBadge.tsx
|
|
552
|
+
import { BOOKING_STATUS } from "@adatechnology/scheduling-contracts";
|
|
553
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
554
|
+
var BOOKING_STATUS_TONE = {
|
|
555
|
+
[BOOKING_STATUS.REQUESTED]: {
|
|
556
|
+
badge: "bg-amber-100 text-amber-800 dark:bg-amber-900/50 dark:text-amber-200",
|
|
557
|
+
block: "border-amber-400 bg-amber-50 text-amber-900 dark:border-amber-600 dark:bg-amber-900/40 dark:text-amber-100"
|
|
558
|
+
},
|
|
559
|
+
[BOOKING_STATUS.CONFIRMED]: {
|
|
560
|
+
badge: "bg-brand-100 text-brand-800 dark:bg-brand-900/50 dark:text-brand-200",
|
|
561
|
+
block: "border-brand-400 bg-brand-50 text-brand-900 dark:border-brand-600 dark:bg-brand-900/40 dark:text-brand-100"
|
|
562
|
+
},
|
|
563
|
+
[BOOKING_STATUS.CANCELLED]: {
|
|
564
|
+
badge: "bg-gray-200 text-gray-600 line-through dark:bg-gray-700 dark:text-gray-400",
|
|
565
|
+
block: "border-gray-300 bg-gray-100 text-gray-500 line-through dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400"
|
|
566
|
+
},
|
|
567
|
+
[BOOKING_STATUS.COMPLETED]: {
|
|
568
|
+
badge: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/50 dark:text-emerald-200",
|
|
569
|
+
block: "border-emerald-400 bg-emerald-50 text-emerald-900 dark:border-emerald-600 dark:bg-emerald-900/40 dark:text-emerald-100"
|
|
570
|
+
},
|
|
571
|
+
[BOOKING_STATUS.NO_SHOW]: {
|
|
572
|
+
badge: "bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-200",
|
|
573
|
+
block: "border-red-400 bg-red-50 text-red-900 dark:border-red-600 dark:bg-red-900/40 dark:text-red-100"
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
function bookingStatusLabel(messages, status) {
|
|
577
|
+
const key = status === BOOKING_STATUS.NO_SHOW ? "booking.status.noShow" : `booking.status.${status}`;
|
|
578
|
+
return messages[key];
|
|
579
|
+
}
|
|
580
|
+
function BookingStatusBadge({ status }) {
|
|
581
|
+
const { locale } = useSchedulingConfig();
|
|
582
|
+
const messages = resolveSchedulingMessages(locale);
|
|
583
|
+
return /* @__PURE__ */ jsx2(
|
|
584
|
+
"span",
|
|
585
|
+
{
|
|
586
|
+
className: `inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${BOOKING_STATUS_TONE[status].badge}`,
|
|
587
|
+
children: bookingStatusLabel(messages, status)
|
|
588
|
+
}
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
520
592
|
// src/components/agendaLayout.util.ts
|
|
521
593
|
function groupOverlappingBookings(sortedBookings) {
|
|
522
594
|
const clusters = [];
|
|
@@ -565,7 +637,9 @@ function startOfDay(date) {
|
|
|
565
637
|
}
|
|
566
638
|
|
|
567
639
|
// src/components/AgendaGrid.tsx
|
|
568
|
-
import { jsx as
|
|
640
|
+
import { jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
641
|
+
var HOUR_HEIGHT_REM = 4;
|
|
642
|
+
var HEADER_CLASS = "sticky top-0 z-10 h-10 bg-white px-2 text-xs font-medium text-gray-700 dark:bg-gray-900 dark:text-gray-300";
|
|
569
643
|
function buildHours(startHour, endHour) {
|
|
570
644
|
return Array.from({ length: endHour - startHour }, (_hour, index) => startHour + index);
|
|
571
645
|
}
|
|
@@ -575,14 +649,18 @@ function bookingsOnDay(bookings, day, startHour, endHour) {
|
|
|
575
649
|
const visibleEnd = new Date(dayStart.getTime() + endHour * 60 * 6e4);
|
|
576
650
|
return bookings.filter((booking) => booking.startsAt < visibleEnd && booking.endsAt > visibleStart);
|
|
577
651
|
}
|
|
578
|
-
function
|
|
652
|
+
function isSameDay(left, right) {
|
|
653
|
+
return startOfDay(left).getTime() === startOfDay(right).getTime();
|
|
654
|
+
}
|
|
655
|
+
function AgendaGrid({ bookings, days, now, onSelect }) {
|
|
579
656
|
const { locale, agendaStartHour, agendaEndHour } = useSchedulingConfig();
|
|
580
657
|
const messages = resolveSchedulingMessages(locale);
|
|
581
658
|
const hours = buildHours(agendaStartHour, agendaEndHour);
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
659
|
+
const columnHeight = `${hours.length * HOUR_HEIGHT_REM}rem`;
|
|
660
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-1 min-h-0 overflow-auto rounded-xl border border-gray-200 dark:border-gray-800", children: [
|
|
661
|
+
/* @__PURE__ */ jsxs("div", { className: "sticky left-0 z-20 flex shrink-0 flex-col border-r border-gray-200 bg-white text-xs text-gray-500 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400", children: [
|
|
662
|
+
/* @__PURE__ */ jsx3("div", { className: `${HEADER_CLASS} border-b border-gray-200 dark:border-gray-800` }),
|
|
663
|
+
hours.map((hour) => /* @__PURE__ */ jsxs("div", { className: "h-16 border-b border-gray-100 px-2 pt-1 tabular-nums dark:border-gray-800/60", children: [
|
|
586
664
|
String(hour).padStart(2, "0"),
|
|
587
665
|
":00"
|
|
588
666
|
] }, hour))
|
|
@@ -591,47 +669,748 @@ function AgendaGrid({ bookings, days }) {
|
|
|
591
669
|
const dayBookings = bookingsOnDay(bookings, day, agendaStartHour, agendaEndHour);
|
|
592
670
|
const positioned = layoutDayBookings(dayBookings);
|
|
593
671
|
const dayStart = startOfDay(day);
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
endHour: agendaEndHour
|
|
608
|
-
});
|
|
609
|
-
const width = 100 / columnCount;
|
|
610
|
-
return /* @__PURE__ */ jsx2(
|
|
672
|
+
const isToday = isSameDay(day, now);
|
|
673
|
+
const nowPercent = toVisiblePercent({
|
|
674
|
+
minutesSinceDayStart: minutesFromDayStart(now, dayStart),
|
|
675
|
+
startHour: agendaStartHour,
|
|
676
|
+
endHour: agendaEndHour
|
|
677
|
+
});
|
|
678
|
+
const showNowLine = isToday && nowPercent >= 0 && nowPercent <= 100;
|
|
679
|
+
return /* @__PURE__ */ jsxs(
|
|
680
|
+
"div",
|
|
681
|
+
{
|
|
682
|
+
className: `flex-1 min-w-40 border-r border-gray-200 last:border-r-0 dark:border-gray-800 ${isToday ? "bg-brand-50/40 dark:bg-brand-900/10" : ""}`,
|
|
683
|
+
children: [
|
|
684
|
+
/* @__PURE__ */ jsxs(
|
|
611
685
|
"div",
|
|
612
686
|
{
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
687
|
+
className: `${HEADER_CLASS} flex items-center gap-1 border-b border-gray-200 dark:border-gray-800 ${isToday ? "text-brand-700 dark:text-brand-300" : ""}`,
|
|
688
|
+
children: [
|
|
689
|
+
day.toLocaleDateString(locale, { weekday: "short", day: "2-digit", month: "2-digit" }),
|
|
690
|
+
isToday && /* @__PURE__ */ jsx3("span", { className: "h-1.5 w-1.5 rounded-full bg-brand-600", "aria-hidden": "true" })
|
|
691
|
+
]
|
|
692
|
+
}
|
|
693
|
+
),
|
|
694
|
+
/* @__PURE__ */ jsxs("div", { className: "relative", style: { height: columnHeight }, children: [
|
|
695
|
+
hours.map((hour, index) => /* @__PURE__ */ jsx3(
|
|
696
|
+
"div",
|
|
697
|
+
{
|
|
698
|
+
"aria-hidden": "true",
|
|
699
|
+
className: "absolute inset-x-0 border-b border-gray-100 dark:border-gray-800/60",
|
|
700
|
+
style: { top: `${index / hours.length * 100}%`, height: `${100 / hours.length}%` }
|
|
620
701
|
},
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
702
|
+
hour
|
|
703
|
+
)),
|
|
704
|
+
positioned.length === 0 && /* @__PURE__ */ jsx3("p", { className: "absolute inset-x-2 top-2 text-xs text-gray-400 dark:text-gray-500", children: messages["agenda.empty"] }),
|
|
705
|
+
positioned.map(({ booking, columnIndex, columnCount }) => {
|
|
706
|
+
const top = toVisiblePercent({
|
|
707
|
+
minutesSinceDayStart: minutesFromDayStart(booking.startsAt, dayStart),
|
|
708
|
+
startHour: agendaStartHour,
|
|
709
|
+
endHour: agendaEndHour
|
|
710
|
+
});
|
|
711
|
+
const bottom = toVisiblePercent({
|
|
712
|
+
minutesSinceDayStart: minutesFromDayStart(booking.endsAt, dayStart),
|
|
713
|
+
startHour: agendaStartHour,
|
|
714
|
+
endHour: agendaEndHour
|
|
715
|
+
});
|
|
716
|
+
const width = 100 / columnCount;
|
|
717
|
+
const startLabel = booking.startsAt.toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" });
|
|
718
|
+
return /* @__PURE__ */ jsxs(
|
|
719
|
+
"button",
|
|
720
|
+
{
|
|
721
|
+
type: "button",
|
|
722
|
+
onClick: () => onSelect?.(booking),
|
|
723
|
+
title: `${startLabel} \xB7 ${booking.title}`,
|
|
724
|
+
className: `absolute overflow-hidden rounded-md border-l-4 px-1.5 py-0.5 text-left text-xs shadow-sm transition-shadow hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 ${BOOKING_STATUS_TONE[booking.status].block}`,
|
|
725
|
+
style: {
|
|
726
|
+
top: `${top}%`,
|
|
727
|
+
height: `${Math.max(bottom - top, 2)}%`,
|
|
728
|
+
left: `${columnIndex * width}%`,
|
|
729
|
+
width: `${width}%`
|
|
730
|
+
},
|
|
731
|
+
children: [
|
|
732
|
+
/* @__PURE__ */ jsx3("span", { className: "block font-medium tabular-nums opacity-80", children: startLabel }),
|
|
733
|
+
/* @__PURE__ */ jsx3("span", { className: "block truncate", children: booking.title })
|
|
734
|
+
]
|
|
735
|
+
},
|
|
736
|
+
booking.id
|
|
737
|
+
);
|
|
738
|
+
}),
|
|
739
|
+
showNowLine && /* @__PURE__ */ jsxs(
|
|
740
|
+
"div",
|
|
741
|
+
{
|
|
742
|
+
"aria-label": messages["agenda.now"],
|
|
743
|
+
className: "pointer-events-none absolute inset-x-0 z-10 flex items-center",
|
|
744
|
+
style: { top: `${nowPercent}%` },
|
|
745
|
+
children: [
|
|
746
|
+
/* @__PURE__ */ jsx3("span", { className: "h-2 w-2 shrink-0 rounded-full bg-red-500" }),
|
|
747
|
+
/* @__PURE__ */ jsx3("span", { className: "h-px flex-1 bg-red-500" })
|
|
748
|
+
]
|
|
749
|
+
}
|
|
750
|
+
)
|
|
751
|
+
] })
|
|
752
|
+
]
|
|
753
|
+
},
|
|
754
|
+
day.toISOString()
|
|
755
|
+
);
|
|
628
756
|
})
|
|
629
757
|
] });
|
|
630
758
|
}
|
|
631
759
|
|
|
760
|
+
// src/components/BookingDrawer.tsx
|
|
761
|
+
import { XCircle } from "lucide-react";
|
|
762
|
+
import { useState as useState2 } from "react";
|
|
763
|
+
import { BOOKING_STATUS as BOOKING_STATUS2 } from "@adatechnology/scheduling-contracts";
|
|
764
|
+
|
|
765
|
+
// src/components/DateTimeField.tsx
|
|
766
|
+
import { useId as useId2, useMemo as useMemo3 } from "react";
|
|
767
|
+
|
|
768
|
+
// src/components/SelectField.tsx
|
|
769
|
+
import { Check, ChevronDown, Search } from "lucide-react";
|
|
770
|
+
import { useEffect, useId, useMemo as useMemo2, useRef, useState } from "react";
|
|
771
|
+
|
|
772
|
+
// src/components/selectFilter.util.ts
|
|
773
|
+
var COMBINING_MARKS = /[̀-ͯ]/g;
|
|
774
|
+
function normalizeForSearch(text) {
|
|
775
|
+
return text.normalize("NFD").replace(COMBINING_MARKS, "").toLowerCase().trim();
|
|
776
|
+
}
|
|
777
|
+
function filterSelectOptions(options, query) {
|
|
778
|
+
const needle = normalizeForSearch(query);
|
|
779
|
+
if (needle === "") return options;
|
|
780
|
+
return options.filter((option) => normalizeForSearch(option.label).includes(needle));
|
|
781
|
+
}
|
|
782
|
+
function findByPrefix(options, prefix) {
|
|
783
|
+
const needle = normalizeForSearch(prefix);
|
|
784
|
+
if (needle === "") return -1;
|
|
785
|
+
return options.findIndex((option) => normalizeForSearch(option.label).startsWith(needle));
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// src/components/ui.constant.ts
|
|
789
|
+
var BUTTON_BASE = "inline-flex items-center justify-center gap-2 rounded-lg px-3 text-sm font-medium min-h-11 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-gray-900 disabled:opacity-50 disabled:pointer-events-none";
|
|
790
|
+
var BUTTON_PRIMARY = `${BUTTON_BASE} bg-brand-600 text-white hover:bg-brand-700`;
|
|
791
|
+
var BUTTON_SECONDARY = `${BUTTON_BASE} border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-800`;
|
|
792
|
+
var BUTTON_GHOST = `${BUTTON_BASE} text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800`;
|
|
793
|
+
var BUTTON_DANGER = `${BUTTON_BASE} text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-950/40`;
|
|
794
|
+
var ICON_BUTTON = `${BUTTON_SECONDARY} min-w-11 px-0`;
|
|
795
|
+
var FIELD_CONTROL = "min-h-11 rounded-lg border border-gray-300 bg-white px-3 text-sm text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100";
|
|
796
|
+
var FIELD_LABEL = "text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400";
|
|
797
|
+
var SURFACE_BORDER = "rounded-xl border border-gray-200 dark:border-gray-800";
|
|
798
|
+
var ROW_STRIPE = "bg-gray-50 dark:bg-gray-800/40";
|
|
799
|
+
|
|
800
|
+
// src/components/SelectField.tsx
|
|
801
|
+
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
802
|
+
var SEARCHABLE_THRESHOLD = 8;
|
|
803
|
+
var POPUP_MAX_HEIGHT = 288;
|
|
804
|
+
var TYPEAHEAD_RESET_MS = 700;
|
|
805
|
+
var OPTION_BASE = "flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-left text-sm";
|
|
806
|
+
function SelectField({
|
|
807
|
+
label,
|
|
808
|
+
value,
|
|
809
|
+
options,
|
|
810
|
+
onChange,
|
|
811
|
+
emptyOptionLabel,
|
|
812
|
+
searchable,
|
|
813
|
+
hideLabel,
|
|
814
|
+
className
|
|
815
|
+
}) {
|
|
816
|
+
const { locale } = useSchedulingConfig();
|
|
817
|
+
const messages = resolveSchedulingMessages(locale);
|
|
818
|
+
const fieldId = useId();
|
|
819
|
+
const listboxId = `${fieldId}-listbox`;
|
|
820
|
+
const containerRef = useRef(null);
|
|
821
|
+
const triggerRef = useRef(null);
|
|
822
|
+
const searchRef = useRef(null);
|
|
823
|
+
const listRef = useRef(null);
|
|
824
|
+
const typeahead = useRef({ text: "", at: 0 });
|
|
825
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
826
|
+
const [openUpward, setOpenUpward] = useState(false);
|
|
827
|
+
const [query, setQuery] = useState("");
|
|
828
|
+
const [highlighted, setHighlighted] = useState(0);
|
|
829
|
+
const allOptions = useMemo2(
|
|
830
|
+
() => emptyOptionLabel === void 0 ? options : [{ value: "", label: emptyOptionLabel }, ...options],
|
|
831
|
+
[emptyOptionLabel, options]
|
|
832
|
+
);
|
|
833
|
+
const withSearch = searchable ?? allOptions.length >= SEARCHABLE_THRESHOLD;
|
|
834
|
+
const visibleOptions = useMemo2(
|
|
835
|
+
() => withSearch ? filterSelectOptions(allOptions, query) : allOptions,
|
|
836
|
+
[allOptions, query, withSearch]
|
|
837
|
+
);
|
|
838
|
+
const selected = allOptions.find((option) => option.value === value);
|
|
839
|
+
function open() {
|
|
840
|
+
const rect = triggerRef.current?.getBoundingClientRect();
|
|
841
|
+
if (rect) {
|
|
842
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
843
|
+
setOpenUpward(spaceBelow < POPUP_MAX_HEIGHT && rect.top > spaceBelow);
|
|
844
|
+
}
|
|
845
|
+
setQuery("");
|
|
846
|
+
setHighlighted(Math.max(allOptions.findIndex((option) => option.value === value), 0));
|
|
847
|
+
setIsOpen(true);
|
|
848
|
+
}
|
|
849
|
+
function close(returnFocus = true) {
|
|
850
|
+
setIsOpen(false);
|
|
851
|
+
if (returnFocus) triggerRef.current?.focus();
|
|
852
|
+
}
|
|
853
|
+
function commit(option) {
|
|
854
|
+
onChange(option.value);
|
|
855
|
+
close();
|
|
856
|
+
}
|
|
857
|
+
useEffect(() => {
|
|
858
|
+
if (!isOpen) return;
|
|
859
|
+
function handlePointerDown(event) {
|
|
860
|
+
if (!containerRef.current?.contains(event.target)) setIsOpen(false);
|
|
861
|
+
}
|
|
862
|
+
document.addEventListener("pointerdown", handlePointerDown);
|
|
863
|
+
return () => document.removeEventListener("pointerdown", handlePointerDown);
|
|
864
|
+
}, [isOpen]);
|
|
865
|
+
useEffect(() => {
|
|
866
|
+
if (isOpen && withSearch) searchRef.current?.focus();
|
|
867
|
+
}, [isOpen, withSearch]);
|
|
868
|
+
useEffect(() => {
|
|
869
|
+
if (!isOpen) return;
|
|
870
|
+
listRef.current?.children[highlighted]?.scrollIntoView({ block: "nearest" });
|
|
871
|
+
}, [highlighted, isOpen]);
|
|
872
|
+
function jumpByTypeahead(key) {
|
|
873
|
+
if (withSearch || key.length !== 1 || !/\S/.test(key)) return false;
|
|
874
|
+
const now = Date.now();
|
|
875
|
+
const text = now - typeahead.current.at > TYPEAHEAD_RESET_MS ? key : typeahead.current.text + key;
|
|
876
|
+
typeahead.current = { text, at: now };
|
|
877
|
+
const index = findByPrefix(visibleOptions, text);
|
|
878
|
+
if (index >= 0) setHighlighted(index);
|
|
879
|
+
return index >= 0;
|
|
880
|
+
}
|
|
881
|
+
function handleKeyDown(event) {
|
|
882
|
+
if (!isOpen) {
|
|
883
|
+
if (["ArrowDown", "ArrowUp", "Enter", " "].includes(event.key)) {
|
|
884
|
+
event.preventDefault();
|
|
885
|
+
open();
|
|
886
|
+
}
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
switch (event.key) {
|
|
890
|
+
case "Escape":
|
|
891
|
+
event.preventDefault();
|
|
892
|
+
close();
|
|
893
|
+
return;
|
|
894
|
+
case "Tab":
|
|
895
|
+
close(false);
|
|
896
|
+
return;
|
|
897
|
+
case "ArrowDown":
|
|
898
|
+
event.preventDefault();
|
|
899
|
+
setHighlighted((current) => Math.min(current + 1, visibleOptions.length - 1));
|
|
900
|
+
return;
|
|
901
|
+
case "ArrowUp":
|
|
902
|
+
event.preventDefault();
|
|
903
|
+
setHighlighted((current) => Math.max(current - 1, 0));
|
|
904
|
+
return;
|
|
905
|
+
case "Home":
|
|
906
|
+
event.preventDefault();
|
|
907
|
+
setHighlighted(0);
|
|
908
|
+
return;
|
|
909
|
+
case "End":
|
|
910
|
+
event.preventDefault();
|
|
911
|
+
setHighlighted(visibleOptions.length - 1);
|
|
912
|
+
return;
|
|
913
|
+
case "Enter": {
|
|
914
|
+
event.preventDefault();
|
|
915
|
+
const option = visibleOptions[highlighted];
|
|
916
|
+
if (option) commit(option);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
default:
|
|
920
|
+
if (jumpByTypeahead(event.key)) event.preventDefault();
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return /* @__PURE__ */ jsxs2("div", { className: `flex flex-col gap-1 ${className ?? ""}`, children: [
|
|
924
|
+
/* @__PURE__ */ jsx4("span", { id: fieldId, className: hideLabel ? "sr-only" : FIELD_LABEL, children: label }),
|
|
925
|
+
/* @__PURE__ */ jsxs2("div", { ref: containerRef, className: "relative", children: [
|
|
926
|
+
/* @__PURE__ */ jsxs2(
|
|
927
|
+
"button",
|
|
928
|
+
{
|
|
929
|
+
ref: triggerRef,
|
|
930
|
+
type: "button",
|
|
931
|
+
role: "combobox",
|
|
932
|
+
"aria-controls": listboxId,
|
|
933
|
+
"aria-expanded": isOpen,
|
|
934
|
+
"aria-haspopup": "listbox",
|
|
935
|
+
"aria-labelledby": fieldId,
|
|
936
|
+
onClick: () => isOpen ? close(false) : open(),
|
|
937
|
+
onKeyDown: handleKeyDown,
|
|
938
|
+
className: `${FIELD_CONTROL} flex w-full items-center gap-2 text-left`,
|
|
939
|
+
children: [
|
|
940
|
+
/* @__PURE__ */ jsx4("span", { className: `flex-1 truncate ${selected ? "" : "text-gray-500 dark:text-gray-400"}`, children: selected?.label ?? emptyOptionLabel ?? "" }),
|
|
941
|
+
/* @__PURE__ */ jsx4(
|
|
942
|
+
ChevronDown,
|
|
943
|
+
{
|
|
944
|
+
"aria-hidden": "true",
|
|
945
|
+
className: `h-4 w-4 shrink-0 text-gray-400 transition-transform ${isOpen ? "rotate-180" : ""}`
|
|
946
|
+
}
|
|
947
|
+
)
|
|
948
|
+
]
|
|
949
|
+
}
|
|
950
|
+
),
|
|
951
|
+
isOpen && /* @__PURE__ */ jsxs2(
|
|
952
|
+
"div",
|
|
953
|
+
{
|
|
954
|
+
className: `absolute z-30 w-full min-w-max overflow-hidden rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-900 ${openUpward ? "bottom-full mb-1" : "top-full mt-1"}`,
|
|
955
|
+
children: [
|
|
956
|
+
withSearch && /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 border-b border-gray-200 px-3 dark:border-gray-700", children: [
|
|
957
|
+
/* @__PURE__ */ jsx4(Search, { "aria-hidden": "true", className: "h-4 w-4 shrink-0 text-gray-400" }),
|
|
958
|
+
/* @__PURE__ */ jsx4(
|
|
959
|
+
"input",
|
|
960
|
+
{
|
|
961
|
+
ref: searchRef,
|
|
962
|
+
type: "text",
|
|
963
|
+
value: query,
|
|
964
|
+
onChange: (event) => {
|
|
965
|
+
setQuery(event.target.value);
|
|
966
|
+
setHighlighted(0);
|
|
967
|
+
},
|
|
968
|
+
onKeyDown: handleKeyDown,
|
|
969
|
+
"aria-label": messages["select.search"],
|
|
970
|
+
placeholder: messages["select.search"],
|
|
971
|
+
className: "min-h-11 w-full bg-transparent text-sm text-gray-900 outline-none placeholder:text-gray-400 dark:text-gray-100"
|
|
972
|
+
}
|
|
973
|
+
)
|
|
974
|
+
] }),
|
|
975
|
+
/* @__PURE__ */ jsxs2(
|
|
976
|
+
"ul",
|
|
977
|
+
{
|
|
978
|
+
ref: listRef,
|
|
979
|
+
id: listboxId,
|
|
980
|
+
role: "listbox",
|
|
981
|
+
"aria-labelledby": fieldId,
|
|
982
|
+
className: "max-h-72 overflow-y-auto py-1",
|
|
983
|
+
children: [
|
|
984
|
+
visibleOptions.map((option, index) => {
|
|
985
|
+
const isSelected = option.value === value;
|
|
986
|
+
return /* @__PURE__ */ jsxs2(
|
|
987
|
+
"li",
|
|
988
|
+
{
|
|
989
|
+
role: "option",
|
|
990
|
+
"aria-selected": isSelected,
|
|
991
|
+
onPointerEnter: () => setHighlighted(index),
|
|
992
|
+
onClick: () => commit(option),
|
|
993
|
+
className: `${OPTION_BASE} ${index === highlighted ? "bg-brand-50 dark:bg-brand-900/30" : ""} ${isSelected ? "font-medium text-brand-800 dark:text-brand-200" : "text-gray-800 dark:text-gray-200"}`,
|
|
994
|
+
children: [
|
|
995
|
+
/* @__PURE__ */ jsx4("span", { className: "flex-1 truncate", children: option.label }),
|
|
996
|
+
isSelected && /* @__PURE__ */ jsx4(Check, { "aria-hidden": "true", className: "h-4 w-4 shrink-0" })
|
|
997
|
+
]
|
|
998
|
+
},
|
|
999
|
+
option.value
|
|
1000
|
+
);
|
|
1001
|
+
}),
|
|
1002
|
+
visibleOptions.length === 0 && /* @__PURE__ */ jsx4("li", { className: "px-3 py-3 text-sm text-gray-500 dark:text-gray-400", children: messages["select.noResults"] })
|
|
1003
|
+
]
|
|
1004
|
+
}
|
|
1005
|
+
)
|
|
1006
|
+
]
|
|
1007
|
+
}
|
|
1008
|
+
)
|
|
1009
|
+
] })
|
|
1010
|
+
] });
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// src/components/dateTimeParts.util.ts
|
|
1014
|
+
var DATE_TIME_LOCAL_PATTERN = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/;
|
|
1015
|
+
var MONTHS_IN_YEAR = 12;
|
|
1016
|
+
function parseDateTimeParts(value) {
|
|
1017
|
+
const match = DATE_TIME_LOCAL_PATTERN.exec(value);
|
|
1018
|
+
if (!match) return void 0;
|
|
1019
|
+
const [, year, month, day, hour, minute] = match;
|
|
1020
|
+
return { year: Number(year), month: Number(month), day: Number(day), time: `${hour}:${minute}` };
|
|
1021
|
+
}
|
|
1022
|
+
function daysInMonth(params) {
|
|
1023
|
+
return new Date(Date.UTC(params.year, params.month, 0)).getUTCDate();
|
|
1024
|
+
}
|
|
1025
|
+
function pad(value) {
|
|
1026
|
+
return String(value).padStart(2, "0");
|
|
1027
|
+
}
|
|
1028
|
+
function formatDateTimeParts(parts) {
|
|
1029
|
+
const lastDay = daysInMonth({ year: parts.year, month: parts.month });
|
|
1030
|
+
const day = Math.min(Math.max(parts.day, 1), lastDay);
|
|
1031
|
+
return `${String(parts.year).padStart(4, "0")}-${pad(parts.month)}-${pad(day)}T${parts.time}`;
|
|
1032
|
+
}
|
|
1033
|
+
function buildYearOptions(params) {
|
|
1034
|
+
const current = params.today.getFullYear();
|
|
1035
|
+
const first = Math.min(current - params.past, params.year);
|
|
1036
|
+
const last = Math.max(current + params.future, params.year);
|
|
1037
|
+
return Array.from({ length: last - first + 1 }, (_, index) => first + index);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// src/components/DateTimeField.tsx
|
|
1041
|
+
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1042
|
+
var FIELD_CLASS = "min-h-11 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 text-sm";
|
|
1043
|
+
var YEAR_WINDOW = { PAST: 1, FUTURE: 2 };
|
|
1044
|
+
function partsOf(value, fallback) {
|
|
1045
|
+
return parseDateTimeParts(value) ?? {
|
|
1046
|
+
year: fallback.getFullYear(),
|
|
1047
|
+
month: fallback.getMonth() + 1,
|
|
1048
|
+
day: fallback.getDate(),
|
|
1049
|
+
time: "09:00"
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
function monthLabels(locale) {
|
|
1053
|
+
const format = new Intl.DateTimeFormat(locale, { month: "long", timeZone: "UTC" });
|
|
1054
|
+
return Array.from(
|
|
1055
|
+
{ length: MONTHS_IN_YEAR },
|
|
1056
|
+
(_, index) => format.format(new Date(Date.UTC(2026, index, 1)))
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
function DateTimeField({ label, value, onChange, emptyDefault }) {
|
|
1060
|
+
const { locale } = useSchedulingConfig();
|
|
1061
|
+
const messages = resolveSchedulingMessages(locale);
|
|
1062
|
+
const groupId = useId2();
|
|
1063
|
+
const today = emptyDefault ?? /* @__PURE__ */ new Date();
|
|
1064
|
+
const parts = partsOf(value, today);
|
|
1065
|
+
const years = buildYearOptions({
|
|
1066
|
+
year: parts.year,
|
|
1067
|
+
today,
|
|
1068
|
+
past: YEAR_WINDOW.PAST,
|
|
1069
|
+
future: YEAR_WINDOW.FUTURE
|
|
1070
|
+
});
|
|
1071
|
+
const days = daysInMonth({ year: parts.year, month: parts.month });
|
|
1072
|
+
const months = monthLabels(locale);
|
|
1073
|
+
const yearOptions = useMemo3(
|
|
1074
|
+
() => years.map((year) => ({ value: String(year), label: String(year) })),
|
|
1075
|
+
[years]
|
|
1076
|
+
);
|
|
1077
|
+
const monthOptions = useMemo3(
|
|
1078
|
+
() => months.map((name, index) => ({ value: String(index + 1), label: name })),
|
|
1079
|
+
[months]
|
|
1080
|
+
);
|
|
1081
|
+
const dayOptions = useMemo3(
|
|
1082
|
+
() => Array.from({ length: days }, (_, index) => ({ value: String(index + 1), label: String(index + 1) })),
|
|
1083
|
+
[days]
|
|
1084
|
+
);
|
|
1085
|
+
function change(patch) {
|
|
1086
|
+
onChange(formatDateTimeParts({ ...parts, ...patch }));
|
|
1087
|
+
}
|
|
1088
|
+
return /* @__PURE__ */ jsxs3("div", { role: "group", "aria-labelledby": groupId, className: "flex flex-wrap items-center gap-2", children: [
|
|
1089
|
+
/* @__PURE__ */ jsx5("span", { id: groupId, className: "sr-only", children: label }),
|
|
1090
|
+
/* @__PURE__ */ jsx5(
|
|
1091
|
+
SelectField,
|
|
1092
|
+
{
|
|
1093
|
+
hideLabel: true,
|
|
1094
|
+
label: `${label} \u2014 ${messages["datetime.year"]}`,
|
|
1095
|
+
value: String(parts.year),
|
|
1096
|
+
options: yearOptions,
|
|
1097
|
+
onChange: (next) => change({ year: Number(next) }),
|
|
1098
|
+
className: "min-w-24"
|
|
1099
|
+
}
|
|
1100
|
+
),
|
|
1101
|
+
/* @__PURE__ */ jsx5(
|
|
1102
|
+
SelectField,
|
|
1103
|
+
{
|
|
1104
|
+
hideLabel: true,
|
|
1105
|
+
label: `${label} \u2014 ${messages["datetime.month"]}`,
|
|
1106
|
+
value: String(parts.month),
|
|
1107
|
+
options: monthOptions,
|
|
1108
|
+
onChange: (next) => change({ month: Number(next) }),
|
|
1109
|
+
searchable: false,
|
|
1110
|
+
className: "min-w-36"
|
|
1111
|
+
}
|
|
1112
|
+
),
|
|
1113
|
+
/* @__PURE__ */ jsx5(
|
|
1114
|
+
SelectField,
|
|
1115
|
+
{
|
|
1116
|
+
hideLabel: true,
|
|
1117
|
+
label: `${label} \u2014 ${messages["datetime.day"]}`,
|
|
1118
|
+
value: String(Math.min(parts.day, days)),
|
|
1119
|
+
options: dayOptions,
|
|
1120
|
+
onChange: (next) => change({ day: Number(next) }),
|
|
1121
|
+
searchable: false,
|
|
1122
|
+
className: "min-w-20"
|
|
1123
|
+
}
|
|
1124
|
+
),
|
|
1125
|
+
/* @__PURE__ */ jsx5(
|
|
1126
|
+
"input",
|
|
1127
|
+
{
|
|
1128
|
+
type: "time",
|
|
1129
|
+
"aria-label": `${label} \u2014 ${messages["datetime.time"]}`,
|
|
1130
|
+
value: parts.time,
|
|
1131
|
+
onChange: (event) => change({ time: event.target.value }),
|
|
1132
|
+
className: FIELD_CLASS
|
|
1133
|
+
}
|
|
1134
|
+
)
|
|
1135
|
+
] });
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// src/components/datetimeLocal.util.ts
|
|
1139
|
+
function getTimeZoneOffsetMs(instant, timeZone) {
|
|
1140
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
1141
|
+
timeZone,
|
|
1142
|
+
year: "numeric",
|
|
1143
|
+
month: "2-digit",
|
|
1144
|
+
day: "2-digit",
|
|
1145
|
+
hour: "2-digit",
|
|
1146
|
+
minute: "2-digit",
|
|
1147
|
+
second: "2-digit",
|
|
1148
|
+
hourCycle: "h23"
|
|
1149
|
+
}).formatToParts(instant);
|
|
1150
|
+
const get = (type) => Number(parts.find((part) => part.type === type)?.value ?? 0);
|
|
1151
|
+
const asUtcOfWallClock = Date.UTC(
|
|
1152
|
+
get("year"),
|
|
1153
|
+
get("month") - 1,
|
|
1154
|
+
get("day"),
|
|
1155
|
+
get("hour") % 24,
|
|
1156
|
+
get("minute"),
|
|
1157
|
+
get("second")
|
|
1158
|
+
);
|
|
1159
|
+
return asUtcOfWallClock - instant.getTime();
|
|
1160
|
+
}
|
|
1161
|
+
function parseDateTimeLocalInTimeZone(value, timeZone) {
|
|
1162
|
+
const naiveUtc = /* @__PURE__ */ new Date(`${value}:00.000Z`);
|
|
1163
|
+
const firstOffset = getTimeZoneOffsetMs(naiveUtc, timeZone);
|
|
1164
|
+
const firstPass = new Date(naiveUtc.getTime() - firstOffset);
|
|
1165
|
+
const secondOffset = getTimeZoneOffsetMs(firstPass, timeZone);
|
|
1166
|
+
return new Date(naiveUtc.getTime() - secondOffset);
|
|
1167
|
+
}
|
|
1168
|
+
function formatDateTimeLocalInTimeZone(instant, timeZone) {
|
|
1169
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
1170
|
+
timeZone,
|
|
1171
|
+
year: "numeric",
|
|
1172
|
+
month: "2-digit",
|
|
1173
|
+
day: "2-digit",
|
|
1174
|
+
hour: "2-digit",
|
|
1175
|
+
minute: "2-digit",
|
|
1176
|
+
hourCycle: "h23"
|
|
1177
|
+
}).formatToParts(instant);
|
|
1178
|
+
const get = (type) => parts.find((part) => part.type === type)?.value ?? "00";
|
|
1179
|
+
const hour = get("hour") === "24" ? "00" : get("hour");
|
|
1180
|
+
return `${get("year")}-${get("month")}-${get("day")}T${hour}:${get("minute")}`;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// src/components/SidePanel.tsx
|
|
1184
|
+
import { X } from "lucide-react";
|
|
1185
|
+
import { useEffect as useEffect2 } from "react";
|
|
1186
|
+
import { Fragment, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1187
|
+
function SidePanel({ title, closeLabel, onClose, headerActions, children }) {
|
|
1188
|
+
useEffect2(() => {
|
|
1189
|
+
function handleKeyDown(event) {
|
|
1190
|
+
if (event.key === "Escape") onClose();
|
|
1191
|
+
}
|
|
1192
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
1193
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
1194
|
+
}, [onClose]);
|
|
1195
|
+
return /* @__PURE__ */ jsxs4(Fragment, { children: [
|
|
1196
|
+
/* @__PURE__ */ jsx6(
|
|
1197
|
+
"button",
|
|
1198
|
+
{
|
|
1199
|
+
type: "button",
|
|
1200
|
+
"aria-hidden": "true",
|
|
1201
|
+
tabIndex: -1,
|
|
1202
|
+
onClick: onClose,
|
|
1203
|
+
className: "absolute inset-0 z-10 bg-gray-900/30 backdrop-blur-[1px] wide:hidden"
|
|
1204
|
+
}
|
|
1205
|
+
),
|
|
1206
|
+
/* @__PURE__ */ jsxs4(
|
|
1207
|
+
"section",
|
|
1208
|
+
{
|
|
1209
|
+
"aria-label": title,
|
|
1210
|
+
className: "absolute inset-y-0 right-0 z-20 flex w-full max-w-full flex-col bg-white shadow-2xl desktop:w-[28rem] dark:bg-gray-900 wide:static wide:z-auto wide:shrink-0 wide:border-l wide:border-gray-200 wide:shadow-none wide:dark:border-gray-700",
|
|
1211
|
+
children: [
|
|
1212
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2 border-b border-gray-200 px-4 py-3 dark:border-gray-700", children: [
|
|
1213
|
+
/* @__PURE__ */ jsx6("h2", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100 mr-auto", children: title }),
|
|
1214
|
+
headerActions,
|
|
1215
|
+
/* @__PURE__ */ jsxs4(
|
|
1216
|
+
"button",
|
|
1217
|
+
{
|
|
1218
|
+
type: "button",
|
|
1219
|
+
onClick: onClose,
|
|
1220
|
+
className: BUTTON_GHOST,
|
|
1221
|
+
children: [
|
|
1222
|
+
/* @__PURE__ */ jsx6(X, { "aria-hidden": "true", className: "w-4 h-4" }),
|
|
1223
|
+
closeLabel
|
|
1224
|
+
]
|
|
1225
|
+
}
|
|
1226
|
+
)
|
|
1227
|
+
] }),
|
|
1228
|
+
/* @__PURE__ */ jsx6("div", { className: "min-h-0 flex-1 overflow-y-auto p-4", children })
|
|
1229
|
+
]
|
|
1230
|
+
}
|
|
1231
|
+
)
|
|
1232
|
+
] });
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// src/components/BookingDrawer.tsx
|
|
1236
|
+
import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1237
|
+
var BROWSER_TIME_ZONE = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1238
|
+
var ACTION_BUTTON_CLASS = "min-h-11 px-3 rounded-lg text-sm font-medium border border-gray-300 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800";
|
|
1239
|
+
var PRIMARY_BUTTON_CLASS = "min-h-11 px-3 rounded-lg text-sm font-medium bg-brand-600 text-white hover:bg-brand-700";
|
|
1240
|
+
var DANGER_BUTTON_CLASS = "inline-flex items-center gap-2 min-h-11 px-3 rounded-lg text-sm font-medium text-red-700 hover:bg-red-50";
|
|
1241
|
+
var INPUT_CLASS = "min-h-11 w-full px-3 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-sm";
|
|
1242
|
+
function BookingDrawer({ booking, resourceTimezone, onClose }) {
|
|
1243
|
+
const { locale } = useSchedulingConfig();
|
|
1244
|
+
const messages = resolveSchedulingMessages(locale);
|
|
1245
|
+
const confirmBooking = useConfirmBooking();
|
|
1246
|
+
const completeBooking = useCompleteBooking();
|
|
1247
|
+
const markNoShow = useMarkNoShow();
|
|
1248
|
+
const cancelBooking = useCancelBooking();
|
|
1249
|
+
const rescheduleBooking = useRescheduleBooking();
|
|
1250
|
+
const timezone = resourceTimezone ?? BROWSER_TIME_ZONE;
|
|
1251
|
+
const [isCancelling, setIsCancelling] = useState2(false);
|
|
1252
|
+
const [cancelledBy, setCancelledBy] = useState2("");
|
|
1253
|
+
const [cancellationReason, setCancellationReason] = useState2("");
|
|
1254
|
+
const [isRescheduling, setIsRescheduling] = useState2(false);
|
|
1255
|
+
const [start, setStart] = useState2(() => formatDateTimeLocalInTimeZone(booking.startsAt, timezone));
|
|
1256
|
+
const [end, setEnd] = useState2(() => formatDateTimeLocalInTimeZone(booking.endsAt, timezone));
|
|
1257
|
+
async function handleCancel() {
|
|
1258
|
+
if (!cancelledBy) return;
|
|
1259
|
+
try {
|
|
1260
|
+
await cancelBooking.mutateAsync({
|
|
1261
|
+
id: booking.id,
|
|
1262
|
+
input: { cancelledBy, ...cancellationReason ? { cancellationReason } : {} }
|
|
1263
|
+
});
|
|
1264
|
+
onClose();
|
|
1265
|
+
} catch {
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
async function handleReschedule() {
|
|
1269
|
+
try {
|
|
1270
|
+
await rescheduleBooking.mutateAsync({
|
|
1271
|
+
id: booking.id,
|
|
1272
|
+
input: {
|
|
1273
|
+
during: {
|
|
1274
|
+
start: parseDateTimeLocalInTimeZone(start, timezone),
|
|
1275
|
+
end: parseDateTimeLocalInTimeZone(end, timezone)
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
setIsRescheduling(false);
|
|
1280
|
+
} catch {
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
const isTerminal = booking.status === BOOKING_STATUS2.CANCELLED || booking.status === BOOKING_STATUS2.COMPLETED || booking.status === BOOKING_STATUS2.NO_SHOW;
|
|
1284
|
+
return /* @__PURE__ */ jsx7(SidePanel, { title: messages["booking.detailTitle"], closeLabel: messages["common.close"], onClose, children: /* @__PURE__ */ jsxs5("div", { className: "space-y-4", children: [
|
|
1285
|
+
/* @__PURE__ */ jsxs5("div", { children: [
|
|
1286
|
+
/* @__PURE__ */ jsx7("p", { className: "text-base font-semibold text-gray-900 dark:text-gray-100", children: booking.title }),
|
|
1287
|
+
/* @__PURE__ */ jsx7("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: messages[`booking.status.${booking.status === "no_show" ? "noShow" : booking.status}`] }),
|
|
1288
|
+
/* @__PURE__ */ jsxs5("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: [
|
|
1289
|
+
booking.startsAt.toLocaleString(locale),
|
|
1290
|
+
" \u2192 ",
|
|
1291
|
+
booking.endsAt.toLocaleString(locale)
|
|
1292
|
+
] })
|
|
1293
|
+
] }),
|
|
1294
|
+
(confirmBooking.isError || completeBooking.isError || markNoShow.isError || cancelBooking.isError || rescheduleBooking.isError) && /* @__PURE__ */ jsx7("p", { role: "alert", className: "text-sm text-red-700 bg-red-50 rounded-lg px-3 py-2", children: messages["common.actionFailure"] }),
|
|
1295
|
+
!isTerminal && /* @__PURE__ */ jsxs5("div", { className: "flex flex-wrap gap-2", children: [
|
|
1296
|
+
booking.status === BOOKING_STATUS2.REQUESTED && /* @__PURE__ */ jsx7("button", { type: "button", onClick: () => confirmBooking.mutate(booking.id), className: PRIMARY_BUTTON_CLASS, children: messages["booking.confirm"] }),
|
|
1297
|
+
booking.status === BOOKING_STATUS2.CONFIRMED && /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
1298
|
+
/* @__PURE__ */ jsx7("button", { type: "button", onClick: () => completeBooking.mutate(booking.id), className: ACTION_BUTTON_CLASS, children: messages["booking.complete"] }),
|
|
1299
|
+
/* @__PURE__ */ jsx7("button", { type: "button", onClick: () => markNoShow.mutate(booking.id), className: ACTION_BUTTON_CLASS, children: messages["booking.markNoShow"] }),
|
|
1300
|
+
/* @__PURE__ */ jsx7("button", { type: "button", onClick: () => setIsRescheduling(true), className: ACTION_BUTTON_CLASS, children: messages["booking.reschedule"] })
|
|
1301
|
+
] }),
|
|
1302
|
+
/* @__PURE__ */ jsxs5("button", { type: "button", onClick: () => setIsCancelling(true), className: DANGER_BUTTON_CLASS, children: [
|
|
1303
|
+
/* @__PURE__ */ jsx7(XCircle, { "aria-hidden": "true", className: "w-4 h-4" }),
|
|
1304
|
+
messages["booking.cancel"]
|
|
1305
|
+
] })
|
|
1306
|
+
] }),
|
|
1307
|
+
isRescheduling && /* @__PURE__ */ jsxs5("div", { className: "space-y-2 rounded-lg border border-gray-200 dark:border-gray-700 p-3", children: [
|
|
1308
|
+
/* @__PURE__ */ jsx7(
|
|
1309
|
+
DateTimeField,
|
|
1310
|
+
{
|
|
1311
|
+
label: messages["booking.rescheduleStart"],
|
|
1312
|
+
value: start,
|
|
1313
|
+
onChange: setStart
|
|
1314
|
+
}
|
|
1315
|
+
),
|
|
1316
|
+
/* @__PURE__ */ jsx7(DateTimeField, { label: messages["booking.rescheduleEnd"], value: end, onChange: setEnd }),
|
|
1317
|
+
/* @__PURE__ */ jsx7("button", { type: "button", onClick: handleReschedule, disabled: rescheduleBooking.isPending, className: PRIMARY_BUTTON_CLASS, children: messages["common.save"] })
|
|
1318
|
+
] }),
|
|
1319
|
+
isCancelling && /* @__PURE__ */ jsxs5("div", { className: "space-y-2 rounded-lg border border-gray-200 dark:border-gray-700 p-3", children: [
|
|
1320
|
+
/* @__PURE__ */ jsx7(
|
|
1321
|
+
"input",
|
|
1322
|
+
{
|
|
1323
|
+
type: "text",
|
|
1324
|
+
placeholder: messages["booking.cancelledBy"],
|
|
1325
|
+
value: cancelledBy,
|
|
1326
|
+
onChange: (event) => setCancelledBy(event.target.value),
|
|
1327
|
+
className: INPUT_CLASS
|
|
1328
|
+
}
|
|
1329
|
+
),
|
|
1330
|
+
/* @__PURE__ */ jsx7(
|
|
1331
|
+
"input",
|
|
1332
|
+
{
|
|
1333
|
+
type: "text",
|
|
1334
|
+
placeholder: messages["booking.cancelReason"],
|
|
1335
|
+
value: cancellationReason,
|
|
1336
|
+
onChange: (event) => setCancellationReason(event.target.value),
|
|
1337
|
+
className: INPUT_CLASS
|
|
1338
|
+
}
|
|
1339
|
+
),
|
|
1340
|
+
/* @__PURE__ */ jsxs5("button", { type: "button", onClick: handleCancel, disabled: cancelBooking.isPending, className: DANGER_BUTTON_CLASS, children: [
|
|
1341
|
+
/* @__PURE__ */ jsx7(XCircle, { "aria-hidden": "true", className: "w-4 h-4" }),
|
|
1342
|
+
messages["booking.cancel"]
|
|
1343
|
+
] })
|
|
1344
|
+
] })
|
|
1345
|
+
] }) });
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// src/components/ResourceSelect.tsx
|
|
1349
|
+
import { useMemo as useMemo4 } from "react";
|
|
1350
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
1351
|
+
function ResourceSelect({ label, emptyOptionLabel, resources, value, onChange }) {
|
|
1352
|
+
const options = useMemo4(
|
|
1353
|
+
() => resources.map((resource) => ({ value: resource.id, label: resource.name })),
|
|
1354
|
+
[resources]
|
|
1355
|
+
);
|
|
1356
|
+
return /* @__PURE__ */ jsx8(
|
|
1357
|
+
SelectField,
|
|
1358
|
+
{
|
|
1359
|
+
label,
|
|
1360
|
+
emptyOptionLabel,
|
|
1361
|
+
options,
|
|
1362
|
+
value,
|
|
1363
|
+
onChange,
|
|
1364
|
+
searchable: true,
|
|
1365
|
+
className: "min-w-52"
|
|
1366
|
+
}
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// src/components/StateFeedback.tsx
|
|
1371
|
+
import { AlertTriangle } from "lucide-react";
|
|
1372
|
+
import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1373
|
+
function ErrorBanner({ message }) {
|
|
1374
|
+
return /* @__PURE__ */ jsxs6(
|
|
1375
|
+
"p",
|
|
1376
|
+
{
|
|
1377
|
+
role: "alert",
|
|
1378
|
+
className: "flex items-start gap-2 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800 dark:border-red-900 dark:bg-red-950/50 dark:text-red-200",
|
|
1379
|
+
children: [
|
|
1380
|
+
/* @__PURE__ */ jsx9(AlertTriangle, { "aria-hidden": "true", className: "mt-0.5 h-4 w-4 shrink-0" }),
|
|
1381
|
+
message
|
|
1382
|
+
]
|
|
1383
|
+
}
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
function EmptyState({ icon: Icon, title, hint, action }) {
|
|
1387
|
+
return /* @__PURE__ */ jsxs6("div", { className: `${SURFACE_BORDER} flex flex-col items-center gap-2 px-6 py-12 text-center`, children: [
|
|
1388
|
+
/* @__PURE__ */ jsx9("span", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-gray-100 text-gray-400 dark:bg-gray-800 dark:text-gray-500", children: /* @__PURE__ */ jsx9(Icon, { "aria-hidden": "true", className: "h-6 w-6" }) }),
|
|
1389
|
+
/* @__PURE__ */ jsx9("p", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: title }),
|
|
1390
|
+
hint && /* @__PURE__ */ jsx9("p", { className: "max-w-sm text-sm text-gray-500 dark:text-gray-400", children: hint }),
|
|
1391
|
+
action && /* @__PURE__ */ jsx9("div", { className: "mt-2", children: action })
|
|
1392
|
+
] });
|
|
1393
|
+
}
|
|
1394
|
+
var SKELETON_ROW = "h-11 animate-pulse rounded-lg bg-gray-100 dark:bg-gray-800";
|
|
1395
|
+
function ListSkeleton({ label, rows = 5 }) {
|
|
1396
|
+
return /* @__PURE__ */ jsx9("div", { "aria-busy": "true", "aria-label": label, role: "status", className: "space-y-2", children: Array.from({ length: rows }, (_row, index) => /* @__PURE__ */ jsx9("div", { className: SKELETON_ROW }, index)) });
|
|
1397
|
+
}
|
|
1398
|
+
function BlockSkeleton({ label }) {
|
|
1399
|
+
return /* @__PURE__ */ jsx9(
|
|
1400
|
+
"div",
|
|
1401
|
+
{
|
|
1402
|
+
"aria-busy": "true",
|
|
1403
|
+
"aria-label": label,
|
|
1404
|
+
role: "status",
|
|
1405
|
+
className: "min-h-64 flex-1 animate-pulse rounded-xl bg-gray-100 dark:bg-gray-800"
|
|
1406
|
+
}
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
632
1410
|
// src/workspace/AgendaArea.tsx
|
|
633
|
-
import {
|
|
1411
|
+
import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
634
1412
|
var DAY_IN_MS = 24 * 60 * 6e4;
|
|
1413
|
+
var NOW_TICK_MS = 6e4;
|
|
635
1414
|
function startOfWeek(date, weekStartsOn) {
|
|
636
1415
|
const start = startOfDay(date);
|
|
637
1416
|
const offset = (start.getDay() - weekStartsOn + 7) % 7;
|
|
@@ -642,14 +1421,32 @@ function buildVisibleDays(anchorDate, view, weekStartsOn) {
|
|
|
642
1421
|
const weekStart = startOfWeek(anchorDate, weekStartsOn);
|
|
643
1422
|
return Array.from({ length: 7 }, (_day, index) => new Date(weekStart.getTime() + index * DAY_IN_MS));
|
|
644
1423
|
}
|
|
645
|
-
|
|
1424
|
+
function formatPeriod(days, locale) {
|
|
1425
|
+
const first = days[0];
|
|
1426
|
+
if (days.length === 1) {
|
|
1427
|
+
return first.toLocaleDateString(locale, { weekday: "long", day: "2-digit", month: "long", year: "numeric" });
|
|
1428
|
+
}
|
|
1429
|
+
const last = days[days.length - 1];
|
|
1430
|
+
const short = { day: "2-digit", month: "short" };
|
|
1431
|
+
return `${first.toLocaleDateString(locale, short)} \u2013 ${last.toLocaleDateString(locale, { ...short, year: "numeric" })}`;
|
|
1432
|
+
}
|
|
1433
|
+
function useNow() {
|
|
1434
|
+
const [now, setNow] = useState3(() => /* @__PURE__ */ new Date());
|
|
1435
|
+
useEffect3(() => {
|
|
1436
|
+
const timer = window.setInterval(() => setNow(/* @__PURE__ */ new Date()), NOW_TICK_MS);
|
|
1437
|
+
return () => window.clearInterval(timer);
|
|
1438
|
+
}, []);
|
|
1439
|
+
return now;
|
|
1440
|
+
}
|
|
646
1441
|
function AgendaArea() {
|
|
647
1442
|
const { locale, weekStartsOn } = useSchedulingConfig();
|
|
648
1443
|
const messages = resolveSchedulingMessages(locale);
|
|
1444
|
+
const now = useNow();
|
|
649
1445
|
const { data: resourcesPage } = useResources({ active: true, pageSize: MAX_PAGE_SIZE });
|
|
650
|
-
const [resourceId, setResourceId] =
|
|
651
|
-
const [view, setView] =
|
|
652
|
-
const [anchorDate, setAnchorDate] =
|
|
1446
|
+
const [resourceId, setResourceId] = useState3("");
|
|
1447
|
+
const [view, setView] = useState3("day");
|
|
1448
|
+
const [anchorDate, setAnchorDate] = useState3(() => /* @__PURE__ */ new Date());
|
|
1449
|
+
const [selectedBookingId, setSelectedBookingId] = useState3(void 0);
|
|
653
1450
|
const resources = resourcesPage?.data ?? [];
|
|
654
1451
|
const days = buildVisibleDays(anchorDate, view, weekStartsOn);
|
|
655
1452
|
const from = days[0];
|
|
@@ -657,70 +1454,115 @@ function AgendaArea() {
|
|
|
657
1454
|
const { data: bookingsPage, isLoading, isError } = useBookings(
|
|
658
1455
|
resourceId ? { resourceId, from, until, pageSize: MAX_PAGE_SIZE } : { from, until, pageSize: MAX_PAGE_SIZE }
|
|
659
1456
|
);
|
|
660
|
-
|
|
661
|
-
|
|
1457
|
+
const visibleBookings = bookingsPage?.data ?? [];
|
|
1458
|
+
const selectedBooking = visibleBookings.find((booking) => booking.id === selectedBookingId);
|
|
1459
|
+
const selectedResourceTimezone = resources.find(
|
|
1460
|
+
(resource) => resource.id === selectedBooking?.resourceIds[0]
|
|
1461
|
+
)?.timezone;
|
|
1462
|
+
function shiftAnchor(direction) {
|
|
1463
|
+
const step = view === "day" ? 1 : 7;
|
|
1464
|
+
setAnchorDate((current) => new Date(current.getTime() + direction * step * DAY_IN_MS));
|
|
1465
|
+
}
|
|
1466
|
+
function renderViewOption(value, label) {
|
|
1467
|
+
const isActive = view === value;
|
|
1468
|
+
return /* @__PURE__ */ jsx10(
|
|
1469
|
+
"button",
|
|
1470
|
+
{
|
|
1471
|
+
type: "button",
|
|
1472
|
+
onClick: () => setView(value),
|
|
1473
|
+
"aria-pressed": isActive,
|
|
1474
|
+
className: `${BUTTON_BASE} flex-1 ${isActive ? "bg-white text-brand-700 shadow-sm dark:bg-gray-900 dark:text-brand-300" : "text-gray-600 dark:text-gray-400"}`,
|
|
1475
|
+
children: label
|
|
1476
|
+
}
|
|
1477
|
+
);
|
|
662
1478
|
}
|
|
663
|
-
return /* @__PURE__ */
|
|
664
|
-
/* @__PURE__ */
|
|
665
|
-
/* @__PURE__ */
|
|
666
|
-
/* @__PURE__ */
|
|
667
|
-
|
|
668
|
-
"select",
|
|
1479
|
+
return /* @__PURE__ */ jsxs7("div", { className: "flex flex-1 min-h-0 min-w-0", children: [
|
|
1480
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex flex-1 min-h-0 min-w-0 flex-col gap-4 overflow-y-auto p-4", children: [
|
|
1481
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex flex-wrap items-end gap-3", children: [
|
|
1482
|
+
/* @__PURE__ */ jsx10(
|
|
1483
|
+
ResourceSelect,
|
|
669
1484
|
{
|
|
1485
|
+
label: messages["agenda.resourceLabel"],
|
|
1486
|
+
emptyOptionLabel: messages["agenda.allResources"],
|
|
1487
|
+
resources,
|
|
670
1488
|
value: resourceId,
|
|
671
|
-
onChange:
|
|
672
|
-
className: "min-h-11 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 text-sm",
|
|
673
|
-
children: [
|
|
674
|
-
/* @__PURE__ */ jsx3("option", { value: "", children: "\u2014" }),
|
|
675
|
-
resources.map((resource) => /* @__PURE__ */ jsx3("option", { value: resource.id, children: resource.name }, resource.id))
|
|
676
|
-
]
|
|
677
|
-
}
|
|
678
|
-
)
|
|
679
|
-
] }),
|
|
680
|
-
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1 ml-auto", children: [
|
|
681
|
-
/* @__PURE__ */ jsx3("button", { type: "button", onClick: () => shiftAnchor(view === "day" ? -1 : -7), className: NAV_BUTTON_CLASS, children: messages["agenda.previous"] }),
|
|
682
|
-
/* @__PURE__ */ jsx3("button", { type: "button", onClick: () => setAnchorDate(/* @__PURE__ */ new Date()), className: NAV_BUTTON_CLASS, children: messages["agenda.today"] }),
|
|
683
|
-
/* @__PURE__ */ jsx3("button", { type: "button", onClick: () => shiftAnchor(view === "day" ? 1 : 7), className: NAV_BUTTON_CLASS, children: messages["agenda.next"] })
|
|
684
|
-
] }),
|
|
685
|
-
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1", children: [
|
|
686
|
-
/* @__PURE__ */ jsx3(
|
|
687
|
-
"button",
|
|
688
|
-
{
|
|
689
|
-
type: "button",
|
|
690
|
-
onClick: () => setView("day"),
|
|
691
|
-
"aria-current": view === "day" ? "true" : void 0,
|
|
692
|
-
className: `${NAV_BUTTON_CLASS} ${view === "day" ? "bg-brand-600 text-white border-brand-600" : ""}`,
|
|
693
|
-
children: messages["agenda.viewDay"]
|
|
1489
|
+
onChange: setResourceId
|
|
694
1490
|
}
|
|
695
1491
|
),
|
|
696
|
-
/* @__PURE__ */
|
|
697
|
-
|
|
1492
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-1", children: [
|
|
1493
|
+
/* @__PURE__ */ jsx10(
|
|
1494
|
+
"button",
|
|
1495
|
+
{
|
|
1496
|
+
type: "button",
|
|
1497
|
+
"aria-label": messages["agenda.previous"],
|
|
1498
|
+
onClick: () => shiftAnchor(-1),
|
|
1499
|
+
className: ICON_BUTTON,
|
|
1500
|
+
children: /* @__PURE__ */ jsx10(ChevronLeft, { "aria-hidden": "true", className: "h-4 w-4" })
|
|
1501
|
+
}
|
|
1502
|
+
),
|
|
1503
|
+
/* @__PURE__ */ jsx10("button", { type: "button", onClick: () => setAnchorDate(/* @__PURE__ */ new Date()), className: BUTTON_SECONDARY, children: messages["agenda.today"] }),
|
|
1504
|
+
/* @__PURE__ */ jsx10(
|
|
1505
|
+
"button",
|
|
1506
|
+
{
|
|
1507
|
+
type: "button",
|
|
1508
|
+
"aria-label": messages["agenda.next"],
|
|
1509
|
+
onClick: () => shiftAnchor(1),
|
|
1510
|
+
className: ICON_BUTTON,
|
|
1511
|
+
children: /* @__PURE__ */ jsx10(ChevronRight, { "aria-hidden": "true", className: "h-4 w-4" })
|
|
1512
|
+
}
|
|
1513
|
+
)
|
|
1514
|
+
] }),
|
|
1515
|
+
/* @__PURE__ */ jsx10(
|
|
1516
|
+
"p",
|
|
698
1517
|
{
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
className: `${NAV_BUTTON_CLASS} ${view === "week" ? "bg-brand-600 text-white border-brand-600" : ""}`,
|
|
703
|
-
children: messages["agenda.viewWeek"]
|
|
1518
|
+
"aria-live": "polite",
|
|
1519
|
+
className: "min-w-0 flex-1 truncate text-sm font-semibold text-gray-900 first-letter:uppercase dark:text-gray-100",
|
|
1520
|
+
children: formatPeriod(days, locale)
|
|
704
1521
|
}
|
|
705
|
-
)
|
|
706
|
-
|
|
1522
|
+
),
|
|
1523
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex flex-col gap-1", children: [
|
|
1524
|
+
/* @__PURE__ */ jsx10("span", { className: FIELD_LABEL, children: messages["agenda.viewLabel"] }),
|
|
1525
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex gap-1 rounded-lg bg-gray-100 p-1 dark:bg-gray-800", children: [
|
|
1526
|
+
renderViewOption("day", messages["agenda.viewDay"]),
|
|
1527
|
+
renderViewOption("week", messages["agenda.viewWeek"])
|
|
1528
|
+
] })
|
|
1529
|
+
] })
|
|
1530
|
+
] }),
|
|
1531
|
+
isError && /* @__PURE__ */ jsx10(ErrorBanner, { message: messages["common.loadFailure"] }),
|
|
1532
|
+
bookingsPage && bookingsPage.total > bookingsPage.data.length && /* @__PURE__ */ jsx10("p", { className: "rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800 dark:border-amber-900 dark:bg-amber-950/50 dark:text-amber-200", children: messages["agenda.moreResults"] }),
|
|
1533
|
+
isLoading && /* @__PURE__ */ jsx10(BlockSkeleton, { label: messages["common.loading"] }),
|
|
1534
|
+
!isLoading && !isError && visibleBookings.length === 0 && /* @__PURE__ */ jsx10(EmptyState, { icon: CalendarDays, title: messages["agenda.emptyTitle"], hint: messages["agenda.empty"] }),
|
|
1535
|
+
!isLoading && visibleBookings.length > 0 && /* @__PURE__ */ jsx10(
|
|
1536
|
+
AgendaGrid,
|
|
1537
|
+
{
|
|
1538
|
+
bookings: visibleBookings,
|
|
1539
|
+
days,
|
|
1540
|
+
now,
|
|
1541
|
+
onSelect: (booking) => setSelectedBookingId(booking.id)
|
|
1542
|
+
}
|
|
1543
|
+
)
|
|
707
1544
|
] }),
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
1545
|
+
selectedBooking && /* @__PURE__ */ jsx10(
|
|
1546
|
+
BookingDrawer,
|
|
1547
|
+
{
|
|
1548
|
+
booking: selectedBooking,
|
|
1549
|
+
resourceTimezone: selectedResourceTimezone,
|
|
1550
|
+
onClose: () => setSelectedBookingId(void 0)
|
|
1551
|
+
},
|
|
1552
|
+
selectedBooking.id
|
|
1553
|
+
)
|
|
713
1554
|
] });
|
|
714
1555
|
}
|
|
715
1556
|
|
|
716
1557
|
// src/workspace/AvailabilityArea.tsx
|
|
717
|
-
import {
|
|
1558
|
+
import { Clock } from "lucide-react";
|
|
1559
|
+
import { useState as useState6 } from "react";
|
|
718
1560
|
import { MAX_PAGE_SIZE as MAX_PAGE_SIZE2 } from "@adatechnology/scheduling-contracts";
|
|
719
1561
|
|
|
720
1562
|
// src/components/WeeklyRulesEditor.tsx
|
|
721
1563
|
import { Plus, Trash2 } from "lucide-react";
|
|
722
|
-
import { useState as
|
|
723
|
-
import { jsx as
|
|
1564
|
+
import { useState as useState4, useMemo as useMemo5 } from "react";
|
|
1565
|
+
import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
724
1566
|
var WEEKDAYS = [0, 1, 2, 3, 4, 5, 6];
|
|
725
1567
|
function toDraft(rule) {
|
|
726
1568
|
return { weekday: rule.weekday, startsAtLocal: rule.startsAtLocal, endsAtLocal: rule.endsAtLocal };
|
|
@@ -731,9 +1573,16 @@ function createEmptyDraft() {
|
|
|
731
1573
|
function WeeklyRulesEditor({ resourceId }) {
|
|
732
1574
|
const { locale } = useSchedulingConfig();
|
|
733
1575
|
const messages = resolveSchedulingMessages(locale);
|
|
1576
|
+
const weekdayOptions = useMemo5(
|
|
1577
|
+
() => WEEKDAYS.map((weekday) => ({
|
|
1578
|
+
value: String(weekday),
|
|
1579
|
+
label: messages[`availability.weekday.${weekday}`]
|
|
1580
|
+
})),
|
|
1581
|
+
[messages]
|
|
1582
|
+
);
|
|
734
1583
|
const { data, isSuccess: isRulesLoaded, isError: isRulesLoadError } = useAvailabilityRules(resourceId);
|
|
735
1584
|
const setAvailabilityRules = useSetAvailabilityRules();
|
|
736
|
-
const [draft, setDraft] =
|
|
1585
|
+
const [draft, setDraft] = useState4(void 0);
|
|
737
1586
|
const rules = draft ?? (data ? data.map(toDraft) : []);
|
|
738
1587
|
function updateRule(index, patch) {
|
|
739
1588
|
setDraft(rules.map((rule, ruleIndex) => ruleIndex === index ? { ...rule, ...patch } : rule));
|
|
@@ -755,32 +1604,32 @@ function WeeklyRulesEditor({ resourceId }) {
|
|
|
755
1604
|
}
|
|
756
1605
|
}
|
|
757
1606
|
if (isRulesLoadError) {
|
|
758
|
-
return /* @__PURE__ */
|
|
759
|
-
/* @__PURE__ */
|
|
760
|
-
/* @__PURE__ */
|
|
1607
|
+
return /* @__PURE__ */ jsxs8("section", { className: "space-y-3", children: [
|
|
1608
|
+
/* @__PURE__ */ jsx11("h3", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: messages["availability.weeklyRulesTitle"] }),
|
|
1609
|
+
/* @__PURE__ */ jsx11("p", { role: "alert", className: "text-sm text-red-700 bg-red-50 rounded-lg px-3 py-2", children: messages["common.loadFailure"] })
|
|
761
1610
|
] });
|
|
762
1611
|
}
|
|
763
1612
|
if (!isRulesLoaded) {
|
|
764
|
-
return /* @__PURE__ */
|
|
765
|
-
/* @__PURE__ */
|
|
766
|
-
/* @__PURE__ */
|
|
1613
|
+
return /* @__PURE__ */ jsxs8("section", { className: "space-y-3", children: [
|
|
1614
|
+
/* @__PURE__ */ jsx11("h3", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: messages["availability.weeklyRulesTitle"] }),
|
|
1615
|
+
/* @__PURE__ */ jsx11("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: messages["common.loading"] })
|
|
767
1616
|
] });
|
|
768
1617
|
}
|
|
769
|
-
return /* @__PURE__ */
|
|
770
|
-
/* @__PURE__ */
|
|
771
|
-
setAvailabilityRules.isError && /* @__PURE__ */
|
|
772
|
-
/* @__PURE__ */
|
|
773
|
-
/* @__PURE__ */
|
|
774
|
-
|
|
1618
|
+
return /* @__PURE__ */ jsxs8("section", { className: "space-y-3", children: [
|
|
1619
|
+
/* @__PURE__ */ jsx11("h3", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: messages["availability.weeklyRulesTitle"] }),
|
|
1620
|
+
setAvailabilityRules.isError && /* @__PURE__ */ jsx11("p", { role: "alert", className: "text-sm text-red-700 bg-red-50 rounded-lg px-3 py-2", children: messages["common.actionFailure"] }),
|
|
1621
|
+
/* @__PURE__ */ jsx11("ul", { className: "space-y-2", children: rules.map((rule, index) => /* @__PURE__ */ jsxs8("li", { className: "flex items-center gap-2", children: [
|
|
1622
|
+
/* @__PURE__ */ jsx11(
|
|
1623
|
+
SelectField,
|
|
775
1624
|
{
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1625
|
+
hideLabel: true,
|
|
1626
|
+
label: messages["availability.weekdayLabel"],
|
|
1627
|
+
value: String(rule.weekday),
|
|
1628
|
+
options: weekdayOptions,
|
|
1629
|
+
onChange: (next) => updateRule(index, { weekday: Number(next) })
|
|
781
1630
|
}
|
|
782
1631
|
),
|
|
783
|
-
/* @__PURE__ */
|
|
1632
|
+
/* @__PURE__ */ jsx11(
|
|
784
1633
|
"input",
|
|
785
1634
|
{
|
|
786
1635
|
"aria-label": messages["availability.startsAtLocal"],
|
|
@@ -790,7 +1639,7 @@ function WeeklyRulesEditor({ resourceId }) {
|
|
|
790
1639
|
className: "min-h-11 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 text-sm"
|
|
791
1640
|
}
|
|
792
1641
|
),
|
|
793
|
-
/* @__PURE__ */
|
|
1642
|
+
/* @__PURE__ */ jsx11(
|
|
794
1643
|
"input",
|
|
795
1644
|
{
|
|
796
1645
|
"aria-label": messages["availability.endsAtLocal"],
|
|
@@ -800,31 +1649,31 @@ function WeeklyRulesEditor({ resourceId }) {
|
|
|
800
1649
|
className: "min-h-11 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 text-sm"
|
|
801
1650
|
}
|
|
802
1651
|
),
|
|
803
|
-
/* @__PURE__ */
|
|
1652
|
+
/* @__PURE__ */ jsx11(
|
|
804
1653
|
"button",
|
|
805
1654
|
{
|
|
806
1655
|
type: "button",
|
|
807
1656
|
onClick: () => removeRule(index),
|
|
808
1657
|
"aria-label": messages["availability.removeRule"],
|
|
809
1658
|
className: "min-h-11 min-w-11 inline-flex items-center justify-center rounded-lg text-red-700 hover:bg-red-50",
|
|
810
|
-
children: /* @__PURE__ */
|
|
1659
|
+
children: /* @__PURE__ */ jsx11(Trash2, { "aria-hidden": "true", className: "w-4 h-4" })
|
|
811
1660
|
}
|
|
812
1661
|
)
|
|
813
1662
|
] }, index)) }),
|
|
814
|
-
/* @__PURE__ */
|
|
815
|
-
/* @__PURE__ */
|
|
1663
|
+
/* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [
|
|
1664
|
+
/* @__PURE__ */ jsxs8(
|
|
816
1665
|
"button",
|
|
817
1666
|
{
|
|
818
1667
|
type: "button",
|
|
819
1668
|
onClick: addRule,
|
|
820
1669
|
className: "inline-flex items-center gap-2 min-h-11 px-3 py-2 rounded-lg text-sm font-medium border border-gray-300 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800",
|
|
821
1670
|
children: [
|
|
822
|
-
/* @__PURE__ */
|
|
1671
|
+
/* @__PURE__ */ jsx11(Plus, { "aria-hidden": "true", className: "w-4 h-4" }),
|
|
823
1672
|
messages["availability.addRule"]
|
|
824
1673
|
]
|
|
825
1674
|
}
|
|
826
1675
|
),
|
|
827
|
-
/* @__PURE__ */
|
|
1676
|
+
/* @__PURE__ */ jsx11(
|
|
828
1677
|
"button",
|
|
829
1678
|
{
|
|
830
1679
|
type: "button",
|
|
@@ -833,186 +1682,35 @@ function WeeklyRulesEditor({ resourceId }) {
|
|
|
833
1682
|
className: "min-h-11 px-3 py-2 rounded-lg text-sm font-medium bg-brand-600 text-white hover:bg-brand-700 disabled:opacity-50",
|
|
834
1683
|
children: messages["common.save"]
|
|
835
1684
|
}
|
|
836
|
-
)
|
|
837
|
-
] })
|
|
838
|
-
] });
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
// src/components/AvailabilityExceptionsEditor.tsx
|
|
842
|
-
import { Plus as Plus2, Trash2 as Trash22 } from "lucide-react";
|
|
843
|
-
import { useState as useState3 } from "react";
|
|
844
|
-
import { AVAILABILITY_EXCEPTION_KIND } from "@adatechnology/scheduling-contracts";
|
|
845
|
-
|
|
846
|
-
// src/components/DateTimeField.tsx
|
|
847
|
-
import { useId } from "react";
|
|
848
|
-
|
|
849
|
-
// src/components/dateTimeParts.util.ts
|
|
850
|
-
var DATE_TIME_LOCAL_PATTERN = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/;
|
|
851
|
-
var MONTHS_IN_YEAR = 12;
|
|
852
|
-
function parseDateTimeParts(value) {
|
|
853
|
-
const match = DATE_TIME_LOCAL_PATTERN.exec(value);
|
|
854
|
-
if (!match) return void 0;
|
|
855
|
-
const [, year, month, day, hour, minute] = match;
|
|
856
|
-
return { year: Number(year), month: Number(month), day: Number(day), time: `${hour}:${minute}` };
|
|
857
|
-
}
|
|
858
|
-
function daysInMonth(params) {
|
|
859
|
-
return new Date(Date.UTC(params.year, params.month, 0)).getUTCDate();
|
|
860
|
-
}
|
|
861
|
-
function pad(value) {
|
|
862
|
-
return String(value).padStart(2, "0");
|
|
863
|
-
}
|
|
864
|
-
function formatDateTimeParts(parts) {
|
|
865
|
-
const lastDay = daysInMonth({ year: parts.year, month: parts.month });
|
|
866
|
-
const day = Math.min(Math.max(parts.day, 1), lastDay);
|
|
867
|
-
return `${String(parts.year).padStart(4, "0")}-${pad(parts.month)}-${pad(day)}T${parts.time}`;
|
|
868
|
-
}
|
|
869
|
-
function buildYearOptions(params) {
|
|
870
|
-
const current = params.today.getFullYear();
|
|
871
|
-
const first = Math.min(current - params.past, params.year);
|
|
872
|
-
const last = Math.max(current + params.future, params.year);
|
|
873
|
-
return Array.from({ length: last - first + 1 }, (_, index) => first + index);
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
// src/components/DateTimeField.tsx
|
|
877
|
-
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
878
|
-
var FIELD_CLASS = "min-h-11 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 text-sm";
|
|
879
|
-
var YEAR_WINDOW = { PAST: 1, FUTURE: 2 };
|
|
880
|
-
function partsOf(value, fallback) {
|
|
881
|
-
return parseDateTimeParts(value) ?? {
|
|
882
|
-
year: fallback.getFullYear(),
|
|
883
|
-
month: fallback.getMonth() + 1,
|
|
884
|
-
day: fallback.getDate(),
|
|
885
|
-
time: "09:00"
|
|
886
|
-
};
|
|
887
|
-
}
|
|
888
|
-
function monthLabels(locale) {
|
|
889
|
-
const format = new Intl.DateTimeFormat(locale, { month: "long", timeZone: "UTC" });
|
|
890
|
-
return Array.from(
|
|
891
|
-
{ length: MONTHS_IN_YEAR },
|
|
892
|
-
(_, index) => format.format(new Date(Date.UTC(2026, index, 1)))
|
|
893
|
-
);
|
|
894
|
-
}
|
|
895
|
-
function DateTimeField({ label, value, onChange, emptyDefault }) {
|
|
896
|
-
const { locale } = useSchedulingConfig();
|
|
897
|
-
const messages = resolveSchedulingMessages(locale);
|
|
898
|
-
const groupId = useId();
|
|
899
|
-
const today = emptyDefault ?? /* @__PURE__ */ new Date();
|
|
900
|
-
const parts = partsOf(value, today);
|
|
901
|
-
const years = buildYearOptions({
|
|
902
|
-
year: parts.year,
|
|
903
|
-
today,
|
|
904
|
-
past: YEAR_WINDOW.PAST,
|
|
905
|
-
future: YEAR_WINDOW.FUTURE
|
|
906
|
-
});
|
|
907
|
-
const days = daysInMonth({ year: parts.year, month: parts.month });
|
|
908
|
-
const months = monthLabels(locale);
|
|
909
|
-
function change(patch) {
|
|
910
|
-
onChange(formatDateTimeParts({ ...parts, ...patch }));
|
|
911
|
-
}
|
|
912
|
-
return /* @__PURE__ */ jsxs4("div", { role: "group", "aria-labelledby": groupId, className: "flex flex-wrap items-center gap-2", children: [
|
|
913
|
-
/* @__PURE__ */ jsx5("span", { id: groupId, className: "sr-only", children: label }),
|
|
914
|
-
/* @__PURE__ */ jsx5(
|
|
915
|
-
"select",
|
|
916
|
-
{
|
|
917
|
-
"aria-label": `${label} \u2014 ${messages["datetime.year"]}`,
|
|
918
|
-
value: parts.year,
|
|
919
|
-
onChange: (event) => change({ year: Number(event.target.value) }),
|
|
920
|
-
className: FIELD_CLASS,
|
|
921
|
-
children: years.map((year) => /* @__PURE__ */ jsx5("option", { value: year, children: year }, year))
|
|
922
|
-
}
|
|
923
|
-
),
|
|
924
|
-
/* @__PURE__ */ jsx5(
|
|
925
|
-
"select",
|
|
926
|
-
{
|
|
927
|
-
"aria-label": `${label} \u2014 ${messages["datetime.month"]}`,
|
|
928
|
-
value: parts.month,
|
|
929
|
-
onChange: (event) => change({ month: Number(event.target.value) }),
|
|
930
|
-
className: FIELD_CLASS,
|
|
931
|
-
children: months.map((name, index) => /* @__PURE__ */ jsx5("option", { value: index + 1, children: name }, name))
|
|
932
|
-
}
|
|
933
|
-
),
|
|
934
|
-
/* @__PURE__ */ jsx5(
|
|
935
|
-
"select",
|
|
936
|
-
{
|
|
937
|
-
"aria-label": `${label} \u2014 ${messages["datetime.day"]}`,
|
|
938
|
-
value: Math.min(parts.day, days),
|
|
939
|
-
onChange: (event) => change({ day: Number(event.target.value) }),
|
|
940
|
-
className: FIELD_CLASS,
|
|
941
|
-
children: Array.from({ length: days }, (_, index) => index + 1).map((day) => /* @__PURE__ */ jsx5("option", { value: day, children: day }, day))
|
|
942
|
-
}
|
|
943
|
-
),
|
|
944
|
-
/* @__PURE__ */ jsx5(
|
|
945
|
-
"input",
|
|
946
|
-
{
|
|
947
|
-
type: "time",
|
|
948
|
-
"aria-label": `${label} \u2014 ${messages["datetime.time"]}`,
|
|
949
|
-
value: parts.time,
|
|
950
|
-
onChange: (event) => change({ time: event.target.value }),
|
|
951
|
-
className: FIELD_CLASS
|
|
952
|
-
}
|
|
953
|
-
)
|
|
1685
|
+
)
|
|
1686
|
+
] })
|
|
954
1687
|
] });
|
|
955
1688
|
}
|
|
956
1689
|
|
|
957
|
-
// src/components/datetimeLocal.util.ts
|
|
958
|
-
function getTimeZoneOffsetMs(instant, timeZone) {
|
|
959
|
-
const parts = new Intl.DateTimeFormat("en-US", {
|
|
960
|
-
timeZone,
|
|
961
|
-
year: "numeric",
|
|
962
|
-
month: "2-digit",
|
|
963
|
-
day: "2-digit",
|
|
964
|
-
hour: "2-digit",
|
|
965
|
-
minute: "2-digit",
|
|
966
|
-
second: "2-digit",
|
|
967
|
-
hourCycle: "h23"
|
|
968
|
-
}).formatToParts(instant);
|
|
969
|
-
const get = (type) => Number(parts.find((part) => part.type === type)?.value ?? 0);
|
|
970
|
-
const asUtcOfWallClock = Date.UTC(
|
|
971
|
-
get("year"),
|
|
972
|
-
get("month") - 1,
|
|
973
|
-
get("day"),
|
|
974
|
-
get("hour") % 24,
|
|
975
|
-
get("minute"),
|
|
976
|
-
get("second")
|
|
977
|
-
);
|
|
978
|
-
return asUtcOfWallClock - instant.getTime();
|
|
979
|
-
}
|
|
980
|
-
function parseDateTimeLocalInTimeZone(value, timeZone) {
|
|
981
|
-
const naiveUtc = /* @__PURE__ */ new Date(`${value}:00.000Z`);
|
|
982
|
-
const firstOffset = getTimeZoneOffsetMs(naiveUtc, timeZone);
|
|
983
|
-
const firstPass = new Date(naiveUtc.getTime() - firstOffset);
|
|
984
|
-
const secondOffset = getTimeZoneOffsetMs(firstPass, timeZone);
|
|
985
|
-
return new Date(naiveUtc.getTime() - secondOffset);
|
|
986
|
-
}
|
|
987
|
-
function formatDateTimeLocalInTimeZone(instant, timeZone) {
|
|
988
|
-
const parts = new Intl.DateTimeFormat("en-US", {
|
|
989
|
-
timeZone,
|
|
990
|
-
year: "numeric",
|
|
991
|
-
month: "2-digit",
|
|
992
|
-
day: "2-digit",
|
|
993
|
-
hour: "2-digit",
|
|
994
|
-
minute: "2-digit",
|
|
995
|
-
hourCycle: "h23"
|
|
996
|
-
}).formatToParts(instant);
|
|
997
|
-
const get = (type) => parts.find((part) => part.type === type)?.value ?? "00";
|
|
998
|
-
const hour = get("hour") === "24" ? "00" : get("hour");
|
|
999
|
-
return `${get("year")}-${get("month")}-${get("day")}T${hour}:${get("minute")}`;
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
1690
|
// src/components/AvailabilityExceptionsEditor.tsx
|
|
1003
|
-
import {
|
|
1691
|
+
import { Plus as Plus2, Trash2 as Trash22 } from "lucide-react";
|
|
1692
|
+
import { useState as useState5, useMemo as useMemo6 } from "react";
|
|
1693
|
+
import { AVAILABILITY_EXCEPTION_KIND } from "@adatechnology/scheduling-contracts";
|
|
1694
|
+
import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1004
1695
|
var SELECT_CLASS = "min-h-11 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 text-sm";
|
|
1005
1696
|
function AvailabilityExceptionsEditor({ resourceId, timezone }) {
|
|
1006
1697
|
const { locale } = useSchedulingConfig();
|
|
1007
1698
|
const messages = resolveSchedulingMessages(locale);
|
|
1699
|
+
const kindOptions = useMemo6(
|
|
1700
|
+
() => [
|
|
1701
|
+
{ value: AVAILABILITY_EXCEPTION_KIND.BLOCK, label: messages["availability.exceptionKind.blocked"] },
|
|
1702
|
+
{ value: AVAILABILITY_EXCEPTION_KIND.EXTRA, label: messages["availability.exceptionKind.extra"] }
|
|
1703
|
+
],
|
|
1704
|
+
[messages]
|
|
1705
|
+
);
|
|
1008
1706
|
const { data, isLoading, isError: isLoadError } = useAvailabilityExceptions(resourceId);
|
|
1009
1707
|
const addException = useAddAvailabilityException();
|
|
1010
1708
|
const removeException = useRemoveAvailabilityException();
|
|
1011
1709
|
const emptyValue = () => formatDateTimeLocalInTimeZone(/* @__PURE__ */ new Date(), timezone);
|
|
1012
|
-
const [from, setFrom] =
|
|
1013
|
-
const [until, setUntil] =
|
|
1014
|
-
const [kind, setKind] =
|
|
1015
|
-
const [reason, setReason] =
|
|
1710
|
+
const [from, setFrom] = useState5(emptyValue);
|
|
1711
|
+
const [until, setUntil] = useState5(emptyValue);
|
|
1712
|
+
const [kind, setKind] = useState5(AVAILABILITY_EXCEPTION_KIND.BLOCK);
|
|
1713
|
+
const [reason, setReason] = useState5("");
|
|
1016
1714
|
async function handleAdd() {
|
|
1017
1715
|
if (!from || !until) return;
|
|
1018
1716
|
try {
|
|
@@ -1031,13 +1729,13 @@ function AvailabilityExceptionsEditor({ resourceId, timezone }) {
|
|
|
1031
1729
|
} catch {
|
|
1032
1730
|
}
|
|
1033
1731
|
}
|
|
1034
|
-
return /* @__PURE__ */
|
|
1035
|
-
/* @__PURE__ */
|
|
1036
|
-
(addException.isError || removeException.isError) && /* @__PURE__ */
|
|
1037
|
-
isLoadError && /* @__PURE__ */
|
|
1038
|
-
isLoading && /* @__PURE__ */
|
|
1039
|
-
/* @__PURE__ */
|
|
1040
|
-
/* @__PURE__ */
|
|
1732
|
+
return /* @__PURE__ */ jsxs9("section", { className: "space-y-3", children: [
|
|
1733
|
+
/* @__PURE__ */ jsx12("h3", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: messages["availability.exceptionsTitle"] }),
|
|
1734
|
+
(addException.isError || removeException.isError) && /* @__PURE__ */ jsx12("p", { role: "alert", className: "text-sm text-red-700 bg-red-50 rounded-lg px-3 py-2", children: messages["common.actionFailure"] }),
|
|
1735
|
+
isLoadError && /* @__PURE__ */ jsx12("p", { role: "alert", className: "text-sm text-red-700 bg-red-50 rounded-lg px-3 py-2", children: messages["common.loadFailure"] }),
|
|
1736
|
+
isLoading && /* @__PURE__ */ jsx12("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: messages["common.loading"] }),
|
|
1737
|
+
/* @__PURE__ */ jsx12("ul", { className: "space-y-2", children: (data ?? []).map((exception) => /* @__PURE__ */ jsxs9("li", { className: "flex items-center gap-2 text-sm", children: [
|
|
1738
|
+
/* @__PURE__ */ jsxs9("span", { className: "flex-1", children: [
|
|
1041
1739
|
messages[`availability.exceptionKind.${exception.kind === "block" ? "blocked" : "extra"}`],
|
|
1042
1740
|
" \u2014",
|
|
1043
1741
|
" ",
|
|
@@ -1046,19 +1744,19 @@ function AvailabilityExceptionsEditor({ resourceId, timezone }) {
|
|
|
1046
1744
|
exception.during.end.toLocaleString(locale),
|
|
1047
1745
|
exception.reason ? ` (${exception.reason})` : ""
|
|
1048
1746
|
] }),
|
|
1049
|
-
/* @__PURE__ */
|
|
1747
|
+
/* @__PURE__ */ jsx12(
|
|
1050
1748
|
"button",
|
|
1051
1749
|
{
|
|
1052
1750
|
type: "button",
|
|
1053
1751
|
onClick: () => removeException.mutate({ id: exception.id, resourceId }),
|
|
1054
1752
|
"aria-label": messages["availability.removeException"],
|
|
1055
1753
|
className: "min-h-11 min-w-11 inline-flex items-center justify-center rounded-lg text-red-700 hover:bg-red-50",
|
|
1056
|
-
children: /* @__PURE__ */
|
|
1754
|
+
children: /* @__PURE__ */ jsx12(Trash22, { "aria-hidden": "true", className: "w-4 h-4" })
|
|
1057
1755
|
}
|
|
1058
1756
|
)
|
|
1059
1757
|
] }, exception.id)) }),
|
|
1060
|
-
/* @__PURE__ */
|
|
1061
|
-
/* @__PURE__ */
|
|
1758
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
1759
|
+
/* @__PURE__ */ jsx12(
|
|
1062
1760
|
DateTimeField,
|
|
1063
1761
|
{
|
|
1064
1762
|
label: messages["availability.exceptionFrom"],
|
|
@@ -1066,7 +1764,7 @@ function AvailabilityExceptionsEditor({ resourceId, timezone }) {
|
|
|
1066
1764
|
onChange: setFrom
|
|
1067
1765
|
}
|
|
1068
1766
|
),
|
|
1069
|
-
/* @__PURE__ */
|
|
1767
|
+
/* @__PURE__ */ jsx12(
|
|
1070
1768
|
DateTimeField,
|
|
1071
1769
|
{
|
|
1072
1770
|
label: messages["availability.exceptionUntil"],
|
|
@@ -1074,20 +1772,17 @@ function AvailabilityExceptionsEditor({ resourceId, timezone }) {
|
|
|
1074
1772
|
onChange: setUntil
|
|
1075
1773
|
}
|
|
1076
1774
|
),
|
|
1077
|
-
/* @__PURE__ */
|
|
1078
|
-
|
|
1775
|
+
/* @__PURE__ */ jsx12(
|
|
1776
|
+
SelectField,
|
|
1079
1777
|
{
|
|
1080
|
-
|
|
1778
|
+
hideLabel: true,
|
|
1779
|
+
label: messages["availability.exceptionsTitle"],
|
|
1081
1780
|
value: kind,
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
children: [
|
|
1085
|
-
/* @__PURE__ */ jsx6("option", { value: AVAILABILITY_EXCEPTION_KIND.BLOCK, children: messages["availability.exceptionKind.blocked"] }),
|
|
1086
|
-
/* @__PURE__ */ jsx6("option", { value: AVAILABILITY_EXCEPTION_KIND.EXTRA, children: messages["availability.exceptionKind.extra"] })
|
|
1087
|
-
]
|
|
1781
|
+
options: kindOptions,
|
|
1782
|
+
onChange: (next) => setKind(next)
|
|
1088
1783
|
}
|
|
1089
1784
|
),
|
|
1090
|
-
/* @__PURE__ */
|
|
1785
|
+
/* @__PURE__ */ jsx12(
|
|
1091
1786
|
"input",
|
|
1092
1787
|
{
|
|
1093
1788
|
"aria-label": messages["availability.exceptionReason"],
|
|
@@ -1098,7 +1793,7 @@ function AvailabilityExceptionsEditor({ resourceId, timezone }) {
|
|
|
1098
1793
|
className: SELECT_CLASS
|
|
1099
1794
|
}
|
|
1100
1795
|
),
|
|
1101
|
-
/* @__PURE__ */
|
|
1796
|
+
/* @__PURE__ */ jsxs9(
|
|
1102
1797
|
"button",
|
|
1103
1798
|
{
|
|
1104
1799
|
type: "button",
|
|
@@ -1106,7 +1801,7 @@ function AvailabilityExceptionsEditor({ resourceId, timezone }) {
|
|
|
1106
1801
|
disabled: addException.isPending,
|
|
1107
1802
|
className: "inline-flex items-center gap-2 min-h-11 px-3 py-2 rounded-lg text-sm font-medium bg-brand-600 text-white hover:bg-brand-700 disabled:opacity-50",
|
|
1108
1803
|
children: [
|
|
1109
|
-
/* @__PURE__ */
|
|
1804
|
+
/* @__PURE__ */ jsx12(Plus2, { "aria-hidden": "true", className: "w-4 h-4" }),
|
|
1110
1805
|
messages["availability.addException"]
|
|
1111
1806
|
]
|
|
1112
1807
|
}
|
|
@@ -1116,51 +1811,54 @@ function AvailabilityExceptionsEditor({ resourceId, timezone }) {
|
|
|
1116
1811
|
}
|
|
1117
1812
|
|
|
1118
1813
|
// src/components/AvailabilityEditor.tsx
|
|
1119
|
-
import { jsx as
|
|
1814
|
+
import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1120
1815
|
function AvailabilityEditor({ resourceId, timezone }) {
|
|
1121
1816
|
const { locale } = useSchedulingConfig();
|
|
1122
1817
|
const messages = resolveSchedulingMessages(locale);
|
|
1123
|
-
return /* @__PURE__ */
|
|
1124
|
-
/* @__PURE__ */
|
|
1125
|
-
/* @__PURE__ */
|
|
1818
|
+
return /* @__PURE__ */ jsxs10("div", { className: "space-y-6", children: [
|
|
1819
|
+
/* @__PURE__ */ jsxs10("p", { className: "inline-flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400", children: [
|
|
1820
|
+
/* @__PURE__ */ jsxs10("span", { className: "font-medium", children: [
|
|
1126
1821
|
messages["availability.resourceTimezone"],
|
|
1127
1822
|
":"
|
|
1128
1823
|
] }),
|
|
1129
1824
|
timezone
|
|
1130
1825
|
] }),
|
|
1131
|
-
/* @__PURE__ */
|
|
1132
|
-
/* @__PURE__ */
|
|
1826
|
+
/* @__PURE__ */ jsx13(WeeklyRulesEditor, { resourceId }),
|
|
1827
|
+
/* @__PURE__ */ jsx13(AvailabilityExceptionsEditor, { resourceId, timezone })
|
|
1133
1828
|
] });
|
|
1134
1829
|
}
|
|
1135
1830
|
|
|
1136
1831
|
// src/workspace/AvailabilityArea.tsx
|
|
1137
|
-
import { jsx as
|
|
1832
|
+
import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1138
1833
|
function AvailabilityArea() {
|
|
1139
1834
|
const { locale } = useSchedulingConfig();
|
|
1140
1835
|
const messages = resolveSchedulingMessages(locale);
|
|
1141
1836
|
const { data, isLoading, isError } = useResources({ active: true, pageSize: MAX_PAGE_SIZE2 });
|
|
1142
|
-
const [resourceId, setResourceId] =
|
|
1837
|
+
const [resourceId, setResourceId] = useState6("");
|
|
1143
1838
|
const resources = data?.data ?? [];
|
|
1144
1839
|
const selectedResource = resources.find((resource) => resource.id === resourceId);
|
|
1145
|
-
return /* @__PURE__ */
|
|
1146
|
-
/* @__PURE__ */
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
"
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1840
|
+
return /* @__PURE__ */ jsxs11("div", { className: "flex flex-1 min-h-0 min-w-0 flex-col gap-4 overflow-y-auto p-4", children: [
|
|
1841
|
+
/* @__PURE__ */ jsx14(
|
|
1842
|
+
ResourceSelect,
|
|
1843
|
+
{
|
|
1844
|
+
label: messages["agenda.resourceLabel"],
|
|
1845
|
+
emptyOptionLabel: messages["availability.selectResourceTitle"],
|
|
1846
|
+
resources,
|
|
1847
|
+
value: resourceId,
|
|
1848
|
+
onChange: setResourceId
|
|
1849
|
+
}
|
|
1850
|
+
),
|
|
1851
|
+
isError && /* @__PURE__ */ jsx14(ErrorBanner, { message: messages["common.loadFailure"] }),
|
|
1852
|
+
isLoading && /* @__PURE__ */ jsx14(ListSkeleton, { label: messages["common.loading"], rows: 3 }),
|
|
1853
|
+
!isLoading && !isError && !selectedResource && /* @__PURE__ */ jsx14(
|
|
1854
|
+
EmptyState,
|
|
1855
|
+
{
|
|
1856
|
+
icon: Clock,
|
|
1857
|
+
title: messages["availability.selectResourceTitle"],
|
|
1858
|
+
hint: messages["availability.selectResourceHint"]
|
|
1859
|
+
}
|
|
1860
|
+
),
|
|
1861
|
+
selectedResource && /* @__PURE__ */ jsx14(
|
|
1164
1862
|
AvailabilityEditor,
|
|
1165
1863
|
{
|
|
1166
1864
|
resourceId: selectedResource.id,
|
|
@@ -1172,194 +1870,24 @@ function AvailabilityArea() {
|
|
|
1172
1870
|
}
|
|
1173
1871
|
|
|
1174
1872
|
// src/workspace/BookingsArea.tsx
|
|
1175
|
-
import {
|
|
1873
|
+
import { ClipboardList } from "lucide-react";
|
|
1874
|
+
import { useState as useState9 } from "react";
|
|
1176
1875
|
import { MAX_PAGE_SIZE as MAX_PAGE_SIZE3 } from "@adatechnology/scheduling-contracts";
|
|
1177
1876
|
|
|
1178
|
-
// src/components/BookingDrawer.tsx
|
|
1179
|
-
import { XCircle } from "lucide-react";
|
|
1180
|
-
import { useState as useState5 } from "react";
|
|
1181
|
-
import { BOOKING_STATUS } from "@adatechnology/scheduling-contracts";
|
|
1182
|
-
|
|
1183
|
-
// src/components/SidePanel.tsx
|
|
1184
|
-
import { X } from "lucide-react";
|
|
1185
|
-
import { useEffect } from "react";
|
|
1186
|
-
import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1187
|
-
var BUTTON_CLASS = "inline-flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-colors min-h-11";
|
|
1188
|
-
function SidePanel({ title, closeLabel, onClose, headerActions, children }) {
|
|
1189
|
-
useEffect(() => {
|
|
1190
|
-
function handleKeyDown(event) {
|
|
1191
|
-
if (event.key === "Escape") onClose();
|
|
1192
|
-
}
|
|
1193
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
1194
|
-
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
1195
|
-
}, [onClose]);
|
|
1196
|
-
return /* @__PURE__ */ jsxs8(Fragment2, { children: [
|
|
1197
|
-
/* @__PURE__ */ jsx9(
|
|
1198
|
-
"button",
|
|
1199
|
-
{
|
|
1200
|
-
type: "button",
|
|
1201
|
-
"aria-hidden": "true",
|
|
1202
|
-
tabIndex: -1,
|
|
1203
|
-
onClick: onClose,
|
|
1204
|
-
className: "absolute inset-0 z-10 bg-gray-900/20 wide:hidden"
|
|
1205
|
-
}
|
|
1206
|
-
),
|
|
1207
|
-
/* @__PURE__ */ jsxs8(
|
|
1208
|
-
"section",
|
|
1209
|
-
{
|
|
1210
|
-
"aria-label": title,
|
|
1211
|
-
className: "absolute inset-y-0 right-0 z-20 flex w-full max-w-full flex-col bg-white shadow-2xl desktop:w-[28rem] dark:bg-gray-900 wide:static wide:z-auto wide:shrink-0 wide:border-l wide:border-gray-200 wide:shadow-none wide:dark:border-gray-700",
|
|
1212
|
-
children: [
|
|
1213
|
-
/* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2 border-b border-gray-200 px-4 py-3 dark:border-gray-700", children: [
|
|
1214
|
-
/* @__PURE__ */ jsx9("h2", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100 mr-auto", children: title }),
|
|
1215
|
-
headerActions,
|
|
1216
|
-
/* @__PURE__ */ jsxs8(
|
|
1217
|
-
"button",
|
|
1218
|
-
{
|
|
1219
|
-
type: "button",
|
|
1220
|
-
onClick: onClose,
|
|
1221
|
-
className: `${BUTTON_CLASS} text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800`,
|
|
1222
|
-
children: [
|
|
1223
|
-
/* @__PURE__ */ jsx9(X, { "aria-hidden": "true", className: "w-4 h-4" }),
|
|
1224
|
-
closeLabel
|
|
1225
|
-
]
|
|
1226
|
-
}
|
|
1227
|
-
)
|
|
1228
|
-
] }),
|
|
1229
|
-
/* @__PURE__ */ jsx9("div", { className: "min-h-0 flex-1 overflow-y-auto p-4", children })
|
|
1230
|
-
]
|
|
1231
|
-
}
|
|
1232
|
-
)
|
|
1233
|
-
] });
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
// src/components/BookingDrawer.tsx
|
|
1237
|
-
import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1238
|
-
var BROWSER_TIME_ZONE = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1239
|
-
var ACTION_BUTTON_CLASS = "min-h-11 px-3 rounded-lg text-sm font-medium border border-gray-300 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800";
|
|
1240
|
-
var PRIMARY_BUTTON_CLASS = "min-h-11 px-3 rounded-lg text-sm font-medium bg-brand-600 text-white hover:bg-brand-700";
|
|
1241
|
-
var DANGER_BUTTON_CLASS = "inline-flex items-center gap-2 min-h-11 px-3 rounded-lg text-sm font-medium text-red-700 hover:bg-red-50";
|
|
1242
|
-
var INPUT_CLASS = "min-h-11 w-full px-3 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-sm";
|
|
1243
|
-
function BookingDrawer({ booking, resourceTimezone, onClose }) {
|
|
1244
|
-
const { locale } = useSchedulingConfig();
|
|
1245
|
-
const messages = resolveSchedulingMessages(locale);
|
|
1246
|
-
const confirmBooking = useConfirmBooking();
|
|
1247
|
-
const completeBooking = useCompleteBooking();
|
|
1248
|
-
const markNoShow = useMarkNoShow();
|
|
1249
|
-
const cancelBooking = useCancelBooking();
|
|
1250
|
-
const rescheduleBooking = useRescheduleBooking();
|
|
1251
|
-
const timezone = resourceTimezone ?? BROWSER_TIME_ZONE;
|
|
1252
|
-
const [isCancelling, setIsCancelling] = useState5(false);
|
|
1253
|
-
const [cancelledBy, setCancelledBy] = useState5("");
|
|
1254
|
-
const [cancellationReason, setCancellationReason] = useState5("");
|
|
1255
|
-
const [isRescheduling, setIsRescheduling] = useState5(false);
|
|
1256
|
-
const [start, setStart] = useState5(() => formatDateTimeLocalInTimeZone(booking.startsAt, timezone));
|
|
1257
|
-
const [end, setEnd] = useState5(() => formatDateTimeLocalInTimeZone(booking.endsAt, timezone));
|
|
1258
|
-
async function handleCancel() {
|
|
1259
|
-
if (!cancelledBy) return;
|
|
1260
|
-
try {
|
|
1261
|
-
await cancelBooking.mutateAsync({
|
|
1262
|
-
id: booking.id,
|
|
1263
|
-
input: { cancelledBy, ...cancellationReason ? { cancellationReason } : {} }
|
|
1264
|
-
});
|
|
1265
|
-
onClose();
|
|
1266
|
-
} catch {
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
async function handleReschedule() {
|
|
1270
|
-
try {
|
|
1271
|
-
await rescheduleBooking.mutateAsync({
|
|
1272
|
-
id: booking.id,
|
|
1273
|
-
input: {
|
|
1274
|
-
during: {
|
|
1275
|
-
start: parseDateTimeLocalInTimeZone(start, timezone),
|
|
1276
|
-
end: parseDateTimeLocalInTimeZone(end, timezone)
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
});
|
|
1280
|
-
setIsRescheduling(false);
|
|
1281
|
-
} catch {
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
const isTerminal = booking.status === BOOKING_STATUS.CANCELLED || booking.status === BOOKING_STATUS.COMPLETED || booking.status === BOOKING_STATUS.NO_SHOW;
|
|
1285
|
-
return /* @__PURE__ */ jsx10(SidePanel, { title: messages["booking.detailTitle"], closeLabel: messages["common.close"], onClose, children: /* @__PURE__ */ jsxs9("div", { className: "space-y-4", children: [
|
|
1286
|
-
/* @__PURE__ */ jsxs9("div", { children: [
|
|
1287
|
-
/* @__PURE__ */ jsx10("p", { className: "text-base font-semibold text-gray-900 dark:text-gray-100", children: booking.title }),
|
|
1288
|
-
/* @__PURE__ */ jsx10("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: messages[`booking.status.${booking.status === "no_show" ? "noShow" : booking.status}`] }),
|
|
1289
|
-
/* @__PURE__ */ jsxs9("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: [
|
|
1290
|
-
booking.startsAt.toLocaleString(locale),
|
|
1291
|
-
" \u2192 ",
|
|
1292
|
-
booking.endsAt.toLocaleString(locale)
|
|
1293
|
-
] })
|
|
1294
|
-
] }),
|
|
1295
|
-
(confirmBooking.isError || completeBooking.isError || markNoShow.isError || cancelBooking.isError || rescheduleBooking.isError) && /* @__PURE__ */ jsx10("p", { role: "alert", className: "text-sm text-red-700 bg-red-50 rounded-lg px-3 py-2", children: messages["common.actionFailure"] }),
|
|
1296
|
-
!isTerminal && /* @__PURE__ */ jsxs9("div", { className: "flex flex-wrap gap-2", children: [
|
|
1297
|
-
booking.status === BOOKING_STATUS.REQUESTED && /* @__PURE__ */ jsx10("button", { type: "button", onClick: () => confirmBooking.mutate(booking.id), className: PRIMARY_BUTTON_CLASS, children: messages["booking.confirm"] }),
|
|
1298
|
-
booking.status === BOOKING_STATUS.CONFIRMED && /* @__PURE__ */ jsxs9(Fragment3, { children: [
|
|
1299
|
-
/* @__PURE__ */ jsx10("button", { type: "button", onClick: () => completeBooking.mutate(booking.id), className: ACTION_BUTTON_CLASS, children: messages["booking.complete"] }),
|
|
1300
|
-
/* @__PURE__ */ jsx10("button", { type: "button", onClick: () => markNoShow.mutate(booking.id), className: ACTION_BUTTON_CLASS, children: messages["booking.markNoShow"] }),
|
|
1301
|
-
/* @__PURE__ */ jsx10("button", { type: "button", onClick: () => setIsRescheduling(true), className: ACTION_BUTTON_CLASS, children: messages["booking.reschedule"] })
|
|
1302
|
-
] }),
|
|
1303
|
-
/* @__PURE__ */ jsxs9("button", { type: "button", onClick: () => setIsCancelling(true), className: DANGER_BUTTON_CLASS, children: [
|
|
1304
|
-
/* @__PURE__ */ jsx10(XCircle, { "aria-hidden": "true", className: "w-4 h-4" }),
|
|
1305
|
-
messages["booking.cancel"]
|
|
1306
|
-
] })
|
|
1307
|
-
] }),
|
|
1308
|
-
isRescheduling && /* @__PURE__ */ jsxs9("div", { className: "space-y-2 rounded-lg border border-gray-200 dark:border-gray-700 p-3", children: [
|
|
1309
|
-
/* @__PURE__ */ jsx10(
|
|
1310
|
-
DateTimeField,
|
|
1311
|
-
{
|
|
1312
|
-
label: messages["booking.rescheduleStart"],
|
|
1313
|
-
value: start,
|
|
1314
|
-
onChange: setStart
|
|
1315
|
-
}
|
|
1316
|
-
),
|
|
1317
|
-
/* @__PURE__ */ jsx10(DateTimeField, { label: messages["booking.rescheduleEnd"], value: end, onChange: setEnd }),
|
|
1318
|
-
/* @__PURE__ */ jsx10("button", { type: "button", onClick: handleReschedule, disabled: rescheduleBooking.isPending, className: PRIMARY_BUTTON_CLASS, children: messages["common.save"] })
|
|
1319
|
-
] }),
|
|
1320
|
-
isCancelling && /* @__PURE__ */ jsxs9("div", { className: "space-y-2 rounded-lg border border-gray-200 dark:border-gray-700 p-3", children: [
|
|
1321
|
-
/* @__PURE__ */ jsx10(
|
|
1322
|
-
"input",
|
|
1323
|
-
{
|
|
1324
|
-
type: "text",
|
|
1325
|
-
placeholder: messages["booking.cancelledBy"],
|
|
1326
|
-
value: cancelledBy,
|
|
1327
|
-
onChange: (event) => setCancelledBy(event.target.value),
|
|
1328
|
-
className: INPUT_CLASS
|
|
1329
|
-
}
|
|
1330
|
-
),
|
|
1331
|
-
/* @__PURE__ */ jsx10(
|
|
1332
|
-
"input",
|
|
1333
|
-
{
|
|
1334
|
-
type: "text",
|
|
1335
|
-
placeholder: messages["booking.cancelReason"],
|
|
1336
|
-
value: cancellationReason,
|
|
1337
|
-
onChange: (event) => setCancellationReason(event.target.value),
|
|
1338
|
-
className: INPUT_CLASS
|
|
1339
|
-
}
|
|
1340
|
-
),
|
|
1341
|
-
/* @__PURE__ */ jsxs9("button", { type: "button", onClick: handleCancel, disabled: cancelBooking.isPending, className: DANGER_BUTTON_CLASS, children: [
|
|
1342
|
-
/* @__PURE__ */ jsx10(XCircle, { "aria-hidden": "true", className: "w-4 h-4" }),
|
|
1343
|
-
messages["booking.cancel"]
|
|
1344
|
-
] })
|
|
1345
|
-
] })
|
|
1346
|
-
] }) });
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
1877
|
// src/components/BookingsTable.tsx
|
|
1350
1878
|
import { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, ArrowUpDown } from "lucide-react";
|
|
1351
|
-
import { useState as
|
|
1352
|
-
import { BOOKING_STATUS as
|
|
1879
|
+
import { useState as useState7 } from "react";
|
|
1880
|
+
import { BOOKING_STATUS as BOOKING_STATUS4 } from "@adatechnology/scheduling-contracts";
|
|
1353
1881
|
|
|
1354
1882
|
// src/components/bookingsTableState.util.ts
|
|
1355
|
-
import { BOOKING_STATUS as
|
|
1883
|
+
import { BOOKING_STATUS as BOOKING_STATUS3 } from "@adatechnology/scheduling-contracts";
|
|
1356
1884
|
var DEFAULT_BOOKINGS_TABLE_STATE = {
|
|
1357
1885
|
sortDirection: "asc",
|
|
1358
1886
|
statusFilters: [],
|
|
1359
1887
|
page: 1
|
|
1360
1888
|
};
|
|
1361
1889
|
var SORT_COLUMNS = ["title", "status", "startsAt", "endsAt"];
|
|
1362
|
-
var BOOKING_STATUSES = Object.values(
|
|
1890
|
+
var BOOKING_STATUSES = Object.values(BOOKING_STATUS3);
|
|
1363
1891
|
function isBookingSortColumn(value) {
|
|
1364
1892
|
return SORT_COLUMNS.includes(value);
|
|
1365
1893
|
}
|
|
@@ -1398,8 +1926,8 @@ function filterBookingsByStatus(bookings, statusFilters) {
|
|
|
1398
1926
|
}
|
|
1399
1927
|
|
|
1400
1928
|
// src/components/BookingsTable.tsx
|
|
1401
|
-
import { jsx as
|
|
1402
|
-
var ALL_STATUSES = Object.values(
|
|
1929
|
+
import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1930
|
+
var ALL_STATUSES = Object.values(BOOKING_STATUS4);
|
|
1403
1931
|
var HEADER_BUTTON_CLASS = "inline-flex items-center gap-1 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400";
|
|
1404
1932
|
var CHECKBOX_CELL_CLASS = "px-3 py-2";
|
|
1405
1933
|
function nextSortState(column, currentColumn, currentDirection) {
|
|
@@ -1417,7 +1945,7 @@ function BookingsTable({
|
|
|
1417
1945
|
}) {
|
|
1418
1946
|
const { locale } = useSchedulingConfig();
|
|
1419
1947
|
const messages = resolveSchedulingMessages(locale);
|
|
1420
|
-
const [selected, setSelected] =
|
|
1948
|
+
const [selected, setSelected] = useState7(/* @__PURE__ */ new Set());
|
|
1421
1949
|
const visibleBookings = filterBookingsByStatus(bookings, state.statusFilters);
|
|
1422
1950
|
const allSelected = visibleBookings.length > 0 && visibleBookings.every((booking) => selected.has(booking.id));
|
|
1423
1951
|
function toggleSort(column) {
|
|
@@ -1447,53 +1975,67 @@ function BookingsTable({
|
|
|
1447
1975
|
setSelected(next);
|
|
1448
1976
|
}
|
|
1449
1977
|
function renderSortIcon(column) {
|
|
1450
|
-
if (state.sortColumn !== column) return /* @__PURE__ */
|
|
1451
|
-
return state.sortDirection === "asc" ? /* @__PURE__ */
|
|
1978
|
+
if (state.sortColumn !== column) return /* @__PURE__ */ jsx15(ArrowUpDown, { "aria-hidden": "true", className: "w-3 h-3" });
|
|
1979
|
+
return state.sortDirection === "asc" ? /* @__PURE__ */ jsx15(ArrowUp, { "aria-hidden": "true", className: "w-3 h-3" }) : /* @__PURE__ */ jsx15(ArrowDown, { "aria-hidden": "true", className: "w-3 h-3" });
|
|
1452
1980
|
}
|
|
1453
1981
|
function renderHeader(column, labelKey) {
|
|
1454
1982
|
const ariaSort = state.sortColumn !== column ? "none" : state.sortDirection === "asc" ? "ascending" : "descending";
|
|
1455
|
-
return /* @__PURE__ */
|
|
1983
|
+
return /* @__PURE__ */ jsx15("th", { scope: "col", className: "px-3 py-2", "aria-sort": ariaSort, children: /* @__PURE__ */ jsxs12("button", { type: "button", onClick: () => toggleSort(column), className: HEADER_BUTTON_CLASS, children: [
|
|
1456
1984
|
messages[labelKey],
|
|
1457
1985
|
renderSortIcon(column)
|
|
1458
1986
|
] }) });
|
|
1459
1987
|
}
|
|
1460
|
-
return /* @__PURE__ */
|
|
1461
|
-
/* @__PURE__ */
|
|
1462
|
-
|
|
1463
|
-
/* @__PURE__ */
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1988
|
+
return /* @__PURE__ */ jsxs12("div", { className: "space-y-3", children: [
|
|
1989
|
+
/* @__PURE__ */ jsxs12("div", { className: "flex flex-wrap items-center gap-3", children: [
|
|
1990
|
+
/* @__PURE__ */ jsxs12("fieldset", { className: "flex flex-wrap items-center gap-2", children: [
|
|
1991
|
+
/* @__PURE__ */ jsx15("legend", { className: "sr-only", children: messages["booking.filterByStatus"] }),
|
|
1992
|
+
ALL_STATUSES.map((status) => {
|
|
1993
|
+
const isActive = state.statusFilters.includes(status);
|
|
1994
|
+
return /* @__PURE__ */ jsxs12(
|
|
1995
|
+
"label",
|
|
1996
|
+
{
|
|
1997
|
+
className: `flex cursor-pointer items-center gap-1.5 rounded-full border px-3 py-1.5 text-sm transition-colors ${isActive ? "border-brand-500 bg-brand-50 text-brand-800 dark:bg-brand-900/40 dark:text-brand-200" : "border-gray-300 text-gray-600 hover:bg-gray-50 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-800"}`,
|
|
1998
|
+
children: [
|
|
1999
|
+
/* @__PURE__ */ jsx15(
|
|
2000
|
+
"input",
|
|
2001
|
+
{
|
|
2002
|
+
type: "checkbox",
|
|
2003
|
+
className: "sr-only",
|
|
2004
|
+
checked: isActive,
|
|
2005
|
+
onChange: () => toggleStatusFilter(status)
|
|
2006
|
+
}
|
|
2007
|
+
),
|
|
2008
|
+
bookingStatusLabel(messages, status)
|
|
2009
|
+
]
|
|
2010
|
+
},
|
|
2011
|
+
status
|
|
2012
|
+
);
|
|
2013
|
+
})
|
|
2014
|
+
] }),
|
|
2015
|
+
!isBookingsTableStateDefault(state) && /* @__PURE__ */ jsx15(
|
|
1474
2016
|
"button",
|
|
1475
2017
|
{
|
|
1476
2018
|
type: "button",
|
|
1477
2019
|
onClick: () => onStateChange?.(DEFAULT_BOOKINGS_TABLE_STATE),
|
|
1478
|
-
className: "min-h-11 px-3 text-sm font-medium text-brand-700 hover:underline",
|
|
2020
|
+
className: "min-h-11 px-3 text-sm font-medium text-brand-700 hover:underline dark:text-brand-300",
|
|
1479
2021
|
children: messages["common.clearFilters"]
|
|
1480
2022
|
}
|
|
1481
2023
|
)
|
|
1482
2024
|
] }),
|
|
1483
|
-
selected.size > 0 && bulkActions.length > 0 && /* @__PURE__ */
|
|
2025
|
+
selected.size > 0 && bulkActions.length > 0 && /* @__PURE__ */ jsx15("div", { className: "flex items-center gap-2 rounded-lg bg-brand-50 dark:bg-brand-900/30 px-3 py-2", children: bulkActions.map((action) => /* @__PURE__ */ jsx15(
|
|
1484
2026
|
"button",
|
|
1485
2027
|
{
|
|
1486
2028
|
type: "button",
|
|
1487
2029
|
onClick: () => action.onRun(Array.from(selected)),
|
|
1488
|
-
className:
|
|
2030
|
+
className: BUTTON_PRIMARY,
|
|
1489
2031
|
children: action.label
|
|
1490
2032
|
},
|
|
1491
2033
|
action.key
|
|
1492
2034
|
)) }),
|
|
1493
|
-
/* @__PURE__ */
|
|
1494
|
-
/* @__PURE__ */
|
|
1495
|
-
/* @__PURE__ */
|
|
1496
|
-
/* @__PURE__ */
|
|
2035
|
+
/* @__PURE__ */ jsxs12("div", { className: `${SURFACE_BORDER} overflow-x-auto`, children: [
|
|
2036
|
+
/* @__PURE__ */ jsxs12("table", { className: "min-w-full text-sm", children: [
|
|
2037
|
+
/* @__PURE__ */ jsx15("thead", { className: "border-b border-gray-200 bg-gray-50 dark:border-gray-800 dark:bg-gray-800/60", children: /* @__PURE__ */ jsxs12("tr", { children: [
|
|
2038
|
+
/* @__PURE__ */ jsx15("th", { scope: "col", className: CHECKBOX_CELL_CLASS, children: /* @__PURE__ */ jsx15(
|
|
1497
2039
|
"input",
|
|
1498
2040
|
{
|
|
1499
2041
|
type: "checkbox",
|
|
@@ -1507,12 +2049,12 @@ function BookingsTable({
|
|
|
1507
2049
|
renderHeader("startsAt", "booking.column.startsAt"),
|
|
1508
2050
|
renderHeader("endsAt", "booking.column.endsAt")
|
|
1509
2051
|
] }) }),
|
|
1510
|
-
/* @__PURE__ */
|
|
2052
|
+
/* @__PURE__ */ jsx15("tbody", { children: visibleBookings.map((booking, index) => /* @__PURE__ */ jsxs12(
|
|
1511
2053
|
"tr",
|
|
1512
2054
|
{
|
|
1513
|
-
className: index % 2 === 1 ? "bg-
|
|
2055
|
+
className: `${index % 2 === 1 ? ROW_STRIPE : ""} hover:bg-brand-50/50 dark:hover:bg-brand-900/10`,
|
|
1514
2056
|
children: [
|
|
1515
|
-
/* @__PURE__ */
|
|
2057
|
+
/* @__PURE__ */ jsx15("td", { className: CHECKBOX_CELL_CLASS, children: /* @__PURE__ */ jsx15(
|
|
1516
2058
|
"input",
|
|
1517
2059
|
{
|
|
1518
2060
|
type: "checkbox",
|
|
@@ -1521,43 +2063,43 @@ function BookingsTable({
|
|
|
1521
2063
|
onChange: () => toggleRowSelected(booking.id)
|
|
1522
2064
|
}
|
|
1523
2065
|
) }),
|
|
1524
|
-
/* @__PURE__ */
|
|
1525
|
-
/* @__PURE__ */
|
|
1526
|
-
/* @__PURE__ */
|
|
1527
|
-
/* @__PURE__ */
|
|
2066
|
+
/* @__PURE__ */ jsx15("td", { className: "px-3 py-2", children: /* @__PURE__ */ jsx15("button", { type: "button", onClick: () => onRowClick?.(booking), className: "min-h-11 text-left hover:underline", children: booking.title }) }),
|
|
2067
|
+
/* @__PURE__ */ jsx15("td", { className: "px-3 py-2", children: /* @__PURE__ */ jsx15(BookingStatusBadge, { status: booking.status }) }),
|
|
2068
|
+
/* @__PURE__ */ jsx15("td", { className: "px-3 py-2 tabular-nums whitespace-nowrap", children: booking.startsAt.toLocaleString(locale) }),
|
|
2069
|
+
/* @__PURE__ */ jsx15("td", { className: "px-3 py-2 tabular-nums whitespace-nowrap", children: booking.endsAt.toLocaleString(locale) })
|
|
1528
2070
|
]
|
|
1529
2071
|
},
|
|
1530
2072
|
booking.id
|
|
1531
2073
|
)) })
|
|
1532
2074
|
] }),
|
|
1533
|
-
visibleBookings.length === 0 && /* @__PURE__ */
|
|
2075
|
+
visibleBookings.length === 0 && /* @__PURE__ */ jsx15("p", { className: "px-3 py-8 text-center text-sm text-gray-500 dark:text-gray-400", children: messages["common.empty"] })
|
|
1534
2076
|
] }),
|
|
1535
|
-
pagination && pagination.totalPages > 1 && /* @__PURE__ */
|
|
1536
|
-
/* @__PURE__ */
|
|
2077
|
+
pagination && pagination.totalPages > 1 && /* @__PURE__ */ jsxs12("div", { className: "flex items-center justify-end gap-3", children: [
|
|
2078
|
+
/* @__PURE__ */ jsx15(
|
|
1537
2079
|
"button",
|
|
1538
2080
|
{
|
|
1539
2081
|
type: "button",
|
|
1540
2082
|
"aria-label": messages["common.previousPage"],
|
|
1541
2083
|
disabled: state.page <= 1,
|
|
1542
2084
|
onClick: () => goToPage(state.page - 1),
|
|
1543
|
-
className:
|
|
1544
|
-
children: /* @__PURE__ */
|
|
2085
|
+
className: ICON_BUTTON,
|
|
2086
|
+
children: /* @__PURE__ */ jsx15(ArrowLeft, { "aria-hidden": "true", className: "w-4 h-4" })
|
|
1545
2087
|
}
|
|
1546
2088
|
),
|
|
1547
|
-
/* @__PURE__ */
|
|
2089
|
+
/* @__PURE__ */ jsxs12("span", { className: "text-sm tabular-nums text-gray-500 dark:text-gray-400", children: [
|
|
1548
2090
|
state.page,
|
|
1549
2091
|
" / ",
|
|
1550
2092
|
pagination.totalPages
|
|
1551
2093
|
] }),
|
|
1552
|
-
/* @__PURE__ */
|
|
2094
|
+
/* @__PURE__ */ jsx15(
|
|
1553
2095
|
"button",
|
|
1554
2096
|
{
|
|
1555
2097
|
type: "button",
|
|
1556
2098
|
"aria-label": messages["common.nextPage"],
|
|
1557
2099
|
disabled: state.page >= pagination.totalPages,
|
|
1558
2100
|
onClick: () => goToPage(state.page + 1),
|
|
1559
|
-
className:
|
|
1560
|
-
children: /* @__PURE__ */
|
|
2101
|
+
className: ICON_BUTTON,
|
|
2102
|
+
children: /* @__PURE__ */ jsx15(ArrowRight, { "aria-hidden": "true", className: "w-4 h-4" })
|
|
1561
2103
|
}
|
|
1562
2104
|
)
|
|
1563
2105
|
] })
|
|
@@ -1565,15 +2107,15 @@ function BookingsTable({
|
|
|
1565
2107
|
}
|
|
1566
2108
|
|
|
1567
2109
|
// src/hooks/useBookingsTableState.hook.ts
|
|
1568
|
-
import { useEffect as
|
|
2110
|
+
import { useEffect as useEffect4, useState as useState8 } from "react";
|
|
1569
2111
|
var OWNED_PARAM_KEYS = ["sortBy", "sortDirection", "status", "page"];
|
|
1570
2112
|
function readInitialState() {
|
|
1571
2113
|
if (typeof window === "undefined") return DEFAULT_BOOKINGS_TABLE_STATE;
|
|
1572
2114
|
return parseBookingsTableState(window.location.search);
|
|
1573
2115
|
}
|
|
1574
2116
|
function useBookingsTableState() {
|
|
1575
|
-
const [state, setState] =
|
|
1576
|
-
|
|
2117
|
+
const [state, setState] = useState8(readInitialState);
|
|
2118
|
+
useEffect4(() => {
|
|
1577
2119
|
if (typeof window === "undefined") return;
|
|
1578
2120
|
const params = new URLSearchParams(window.location.search);
|
|
1579
2121
|
for (const key of OWNED_PARAM_KEYS) params.delete(key);
|
|
@@ -1586,7 +2128,7 @@ function useBookingsTableState() {
|
|
|
1586
2128
|
}
|
|
1587
2129
|
|
|
1588
2130
|
// src/workspace/BookingsArea.tsx
|
|
1589
|
-
import { jsx as
|
|
2131
|
+
import { jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
1590
2132
|
var PAGE_SIZE = 20;
|
|
1591
2133
|
function BookingsArea() {
|
|
1592
2134
|
const { locale } = useSchedulingConfig();
|
|
@@ -1602,7 +2144,7 @@ function BookingsArea() {
|
|
|
1602
2144
|
});
|
|
1603
2145
|
const confirmBooking = useConfirmBooking();
|
|
1604
2146
|
const { data: resourcesData } = useResources({ pageSize: MAX_PAGE_SIZE3 });
|
|
1605
|
-
const [selectedBookingId, setSelectedBookingId] =
|
|
2147
|
+
const [selectedBookingId, setSelectedBookingId] = useState9(void 0);
|
|
1606
2148
|
const selectedBooking = data?.data.find((booking) => booking.id === selectedBookingId);
|
|
1607
2149
|
const selectedBookingResourceTimezone = resourcesData?.data.find(
|
|
1608
2150
|
(resource) => resource.id === selectedBooking?.resourceIds[0]
|
|
@@ -1610,21 +2152,32 @@ function BookingsArea() {
|
|
|
1610
2152
|
function bulkConfirm(ids) {
|
|
1611
2153
|
for (const id of ids) confirmBooking.mutate(id);
|
|
1612
2154
|
}
|
|
1613
|
-
return /* @__PURE__ */
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
2155
|
+
return /* @__PURE__ */ jsxs13("div", { className: "flex flex-1 min-h-0 min-w-0", children: [
|
|
2156
|
+
/* @__PURE__ */ jsxs13("div", { className: "flex flex-1 min-h-0 min-w-0 flex-col gap-4 overflow-y-auto p-4", children: [
|
|
2157
|
+
isError && /* @__PURE__ */ jsx16(ErrorBanner, { message: messages["common.loadFailure"] }),
|
|
2158
|
+
confirmBooking.isError && /* @__PURE__ */ jsx16(ErrorBanner, { message: messages["common.actionFailure"] }),
|
|
2159
|
+
isLoading && /* @__PURE__ */ jsx16(ListSkeleton, { label: messages["common.loading"], rows: 8 }),
|
|
2160
|
+
!isLoading && !isError && (data?.data.length ?? 0) === 0 && tableState.statusFilters.length === 0 && /* @__PURE__ */ jsx16(
|
|
2161
|
+
EmptyState,
|
|
2162
|
+
{
|
|
2163
|
+
icon: ClipboardList,
|
|
2164
|
+
title: messages["booking.emptyTitle"],
|
|
2165
|
+
hint: messages["booking.emptyHint"]
|
|
2166
|
+
}
|
|
2167
|
+
),
|
|
2168
|
+
!isLoading && ((data?.data.length ?? 0) > 0 || tableState.statusFilters.length > 0) && /* @__PURE__ */ jsx16(
|
|
2169
|
+
BookingsTable,
|
|
2170
|
+
{
|
|
2171
|
+
bookings: data?.data ?? [],
|
|
2172
|
+
state: tableState,
|
|
2173
|
+
onStateChange: setTableState,
|
|
2174
|
+
pagination: data ? { totalPages: data.totalPages } : void 0,
|
|
2175
|
+
onRowClick: (booking) => setSelectedBookingId(booking.id),
|
|
2176
|
+
bulkActions: [{ key: "confirm", label: messages["booking.confirm"], onRun: bulkConfirm }]
|
|
2177
|
+
}
|
|
2178
|
+
)
|
|
2179
|
+
] }),
|
|
2180
|
+
selectedBooking && /* @__PURE__ */ jsx16(
|
|
1628
2181
|
BookingDrawer,
|
|
1629
2182
|
{
|
|
1630
2183
|
booking: selectedBooking,
|
|
@@ -1637,29 +2190,42 @@ function BookingsArea() {
|
|
|
1637
2190
|
}
|
|
1638
2191
|
|
|
1639
2192
|
// src/workspace/ResourcesArea.tsx
|
|
1640
|
-
import { Plus as Plus3, Trash2 as Trash23 } from "lucide-react";
|
|
1641
|
-
import { useState as
|
|
2193
|
+
import { Plus as Plus3, Trash2 as Trash23, Users } from "lucide-react";
|
|
2194
|
+
import { useState as useState11 } from "react";
|
|
1642
2195
|
import { MAX_PAGE_SIZE as MAX_PAGE_SIZE4 } from "@adatechnology/scheduling-contracts";
|
|
1643
2196
|
|
|
1644
2197
|
// src/components/ResourceForm.tsx
|
|
1645
|
-
import { useId as
|
|
2198
|
+
import { useId as useId3, useMemo as useMemo7, useState as useState10 } from "react";
|
|
1646
2199
|
import { RESOURCE_KIND } from "@adatechnology/scheduling-contracts";
|
|
1647
|
-
import { jsx as
|
|
2200
|
+
import { jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
1648
2201
|
var INPUT_CLASS2 = "w-full min-h-11 px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500 bg-white dark:bg-gray-900";
|
|
1649
2202
|
var LABEL_CLASS = "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1";
|
|
1650
|
-
var
|
|
2203
|
+
var BUTTON_PRIMARY2 = "inline-flex items-center justify-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-colors min-h-11 bg-brand-600 text-white hover:bg-brand-700 disabled:opacity-50";
|
|
2204
|
+
var FALLBACK_TIMEZONES = ["America/Sao_Paulo", "America/Manaus", "America/Belem", "UTC"];
|
|
2205
|
+
function buildTimezoneOptions(current) {
|
|
2206
|
+
const supported = Intl.supportedValuesOf?.("timeZone") ?? FALLBACK_TIMEZONES;
|
|
2207
|
+
const zones = supported.includes(current) ? supported : [current, ...supported];
|
|
2208
|
+
return zones.map((zone) => ({ value: zone, label: zone.replace(/_/g, " ") }));
|
|
2209
|
+
}
|
|
1651
2210
|
function ResourceForm({ initialValues, onSubmit }) {
|
|
1652
2211
|
const { locale } = useSchedulingConfig();
|
|
1653
2212
|
const messages = resolveSchedulingMessages(locale);
|
|
1654
|
-
const formId =
|
|
2213
|
+
const formId = useId3();
|
|
1655
2214
|
const nameId = `${formId}-name`;
|
|
1656
|
-
const
|
|
1657
|
-
const
|
|
1658
|
-
const [
|
|
1659
|
-
const [
|
|
1660
|
-
const [
|
|
1661
|
-
const
|
|
1662
|
-
|
|
2215
|
+
const [name, setName] = useState10(initialValues?.name ?? "");
|
|
2216
|
+
const [kind, setKind] = useState10(initialValues?.kind ?? RESOURCE_KIND.PERSON);
|
|
2217
|
+
const [timezone, setTimezone] = useState10(initialValues?.timezone ?? "America/Sao_Paulo");
|
|
2218
|
+
const [active, setActive] = useState10(initialValues?.active ?? true);
|
|
2219
|
+
const [submitting, setSubmitting] = useState10(false);
|
|
2220
|
+
const kindOptions = useMemo7(
|
|
2221
|
+
() => [
|
|
2222
|
+
{ value: RESOURCE_KIND.PERSON, label: messages["resource.kind.person"] },
|
|
2223
|
+
{ value: RESOURCE_KIND.ROOM, label: messages["resource.kind.room"] },
|
|
2224
|
+
{ value: RESOURCE_KIND.EQUIPMENT, label: messages["resource.kind.equipment"] }
|
|
2225
|
+
],
|
|
2226
|
+
[messages]
|
|
2227
|
+
);
|
|
2228
|
+
const timezoneOptions = useMemo7(() => buildTimezoneOptions(timezone), [timezone]);
|
|
1663
2229
|
async function handleSubmit(event) {
|
|
1664
2230
|
event.preventDefault();
|
|
1665
2231
|
setSubmitting(true);
|
|
@@ -1669,10 +2235,10 @@ function ResourceForm({ initialValues, onSubmit }) {
|
|
|
1669
2235
|
setSubmitting(false);
|
|
1670
2236
|
}
|
|
1671
2237
|
}
|
|
1672
|
-
return /* @__PURE__ */
|
|
1673
|
-
/* @__PURE__ */
|
|
1674
|
-
/* @__PURE__ */
|
|
1675
|
-
/* @__PURE__ */
|
|
2238
|
+
return /* @__PURE__ */ jsxs14("form", { onSubmit: handleSubmit, className: "space-y-4", children: [
|
|
2239
|
+
/* @__PURE__ */ jsxs14("div", { children: [
|
|
2240
|
+
/* @__PURE__ */ jsx17("label", { htmlFor: nameId, className: LABEL_CLASS, children: messages["resource.name"] }),
|
|
2241
|
+
/* @__PURE__ */ jsx17(
|
|
1676
2242
|
"input",
|
|
1677
2243
|
{
|
|
1678
2244
|
id: nameId,
|
|
@@ -1684,75 +2250,59 @@ function ResourceForm({ initialValues, onSubmit }) {
|
|
|
1684
2250
|
}
|
|
1685
2251
|
)
|
|
1686
2252
|
] }),
|
|
1687
|
-
/* @__PURE__ */
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
"
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
{
|
|
1709
|
-
id: timezoneId,
|
|
1710
|
-
type: "text",
|
|
1711
|
-
required: true,
|
|
1712
|
-
value: timezone,
|
|
1713
|
-
onChange: (event) => setTimezone(event.target.value),
|
|
1714
|
-
placeholder: "America/Sao_Paulo",
|
|
1715
|
-
className: INPUT_CLASS2
|
|
1716
|
-
}
|
|
1717
|
-
)
|
|
1718
|
-
] }),
|
|
1719
|
-
initialValues && /* @__PURE__ */ jsxs12("label", { className: "flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 min-h-11", children: [
|
|
1720
|
-
/* @__PURE__ */ jsx13("input", { type: "checkbox", checked: active, onChange: (event) => setActive(event.target.checked) }),
|
|
2253
|
+
/* @__PURE__ */ jsx17(
|
|
2254
|
+
SelectField,
|
|
2255
|
+
{
|
|
2256
|
+
label: messages["resource.kind"],
|
|
2257
|
+
value: kind,
|
|
2258
|
+
options: kindOptions,
|
|
2259
|
+
onChange: (next) => setKind(next)
|
|
2260
|
+
}
|
|
2261
|
+
),
|
|
2262
|
+
/* @__PURE__ */ jsx17(
|
|
2263
|
+
SelectField,
|
|
2264
|
+
{
|
|
2265
|
+
label: messages["resource.timezone"],
|
|
2266
|
+
value: timezone,
|
|
2267
|
+
options: timezoneOptions,
|
|
2268
|
+
onChange: setTimezone,
|
|
2269
|
+
searchable: true
|
|
2270
|
+
}
|
|
2271
|
+
),
|
|
2272
|
+
initialValues && /* @__PURE__ */ jsxs14("label", { className: "flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 min-h-11", children: [
|
|
2273
|
+
/* @__PURE__ */ jsx17("input", { type: "checkbox", checked: active, onChange: (event) => setActive(event.target.checked) }),
|
|
1721
2274
|
messages["resource.active"]
|
|
1722
2275
|
] }),
|
|
1723
|
-
/* @__PURE__ */
|
|
2276
|
+
/* @__PURE__ */ jsx17("button", { type: "submit", disabled: submitting, className: BUTTON_PRIMARY2, children: messages["common.save"] })
|
|
1724
2277
|
] });
|
|
1725
2278
|
}
|
|
1726
2279
|
|
|
1727
2280
|
// src/components/ResourceList.tsx
|
|
1728
|
-
import { jsx as
|
|
2281
|
+
import { jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
1729
2282
|
function ResourceList({ resources, onSelect }) {
|
|
1730
2283
|
const { locale } = useSchedulingConfig();
|
|
1731
2284
|
const messages = resolveSchedulingMessages(locale);
|
|
1732
2285
|
if (resources.length === 0) {
|
|
1733
|
-
return /* @__PURE__ */
|
|
2286
|
+
return /* @__PURE__ */ jsx18("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: messages["common.empty"] });
|
|
1734
2287
|
}
|
|
1735
|
-
return /* @__PURE__ */
|
|
2288
|
+
return /* @__PURE__ */ jsx18("ul", { className: `${SURFACE_BORDER} divide-y divide-gray-200 overflow-hidden dark:divide-gray-800`, children: resources.map((resource, index) => /* @__PURE__ */ jsx18("li", { className: index % 2 === 1 ? ROW_STRIPE : void 0, children: /* @__PURE__ */ jsxs15(
|
|
1736
2289
|
"button",
|
|
1737
2290
|
{
|
|
1738
2291
|
type: "button",
|
|
1739
2292
|
onClick: () => onSelect(resource),
|
|
1740
|
-
className: "flex w-full items-center gap-3 px-4 py-3 min-h-11 text-left text-sm hover:bg-
|
|
2293
|
+
className: "flex w-full items-center gap-3 px-4 py-3 min-h-11 text-left text-sm transition-colors hover:bg-brand-50/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand-500 dark:hover:bg-brand-900/20",
|
|
1741
2294
|
children: [
|
|
1742
|
-
/* @__PURE__ */
|
|
1743
|
-
/* @__PURE__ */
|
|
1744
|
-
/* @__PURE__ */
|
|
1745
|
-
!resource.active && /* @__PURE__ */
|
|
2295
|
+
/* @__PURE__ */ jsx18("span", { className: "flex-1 font-medium text-gray-900 dark:text-gray-100", children: resource.name }),
|
|
2296
|
+
/* @__PURE__ */ jsx18("span", { className: "text-gray-500 dark:text-gray-400", children: messages[`resource.kind.${resource.kind}`] }),
|
|
2297
|
+
/* @__PURE__ */ jsx18("span", { className: "text-gray-500 dark:text-gray-400", children: resource.timezone }),
|
|
2298
|
+
!resource.active && /* @__PURE__ */ jsx18("span", { className: "rounded-full bg-gray-200 px-2 py-0.5 text-xs font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-300", children: messages["resource.inactive"] })
|
|
1746
2299
|
]
|
|
1747
2300
|
}
|
|
1748
2301
|
) }, resource.id)) });
|
|
1749
2302
|
}
|
|
1750
2303
|
|
|
1751
2304
|
// src/workspace/ResourcesArea.tsx
|
|
1752
|
-
import { jsx as
|
|
1753
|
-
var BUTTON_CLASS2 = "inline-flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-colors min-h-11";
|
|
1754
|
-
var BUTTON_PRIMARY2 = `${BUTTON_CLASS2} bg-brand-600 text-white hover:bg-brand-700`;
|
|
1755
|
-
var BUTTON_DANGER = `${BUTTON_CLASS2} text-red-700 hover:bg-red-50`;
|
|
2305
|
+
import { jsx as jsx19, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
1756
2306
|
function ResourcesArea() {
|
|
1757
2307
|
const { locale } = useSchedulingConfig();
|
|
1758
2308
|
const messages = resolveSchedulingMessages(locale);
|
|
@@ -1760,9 +2310,10 @@ function ResourcesArea() {
|
|
|
1760
2310
|
const createResource = useCreateResource();
|
|
1761
2311
|
const updateResource = useUpdateResource();
|
|
1762
2312
|
const deleteResource = useDeleteResource();
|
|
1763
|
-
const [draft, setDraft] =
|
|
2313
|
+
const [draft, setDraft] = useState11(void 0);
|
|
1764
2314
|
const isDraftOpen = draft !== void 0;
|
|
1765
2315
|
const isEditing = Boolean(draft);
|
|
2316
|
+
const resources = data?.data ?? [];
|
|
1766
2317
|
async function handleSubmit(input) {
|
|
1767
2318
|
try {
|
|
1768
2319
|
if (draft) {
|
|
@@ -1774,21 +2325,36 @@ function ResourcesArea() {
|
|
|
1774
2325
|
} catch {
|
|
1775
2326
|
}
|
|
1776
2327
|
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
/* @__PURE__ */
|
|
2328
|
+
function renderCreateButton() {
|
|
2329
|
+
return /* @__PURE__ */ jsxs16("button", { type: "button", onClick: () => setDraft(null), className: BUTTON_PRIMARY, children: [
|
|
2330
|
+
/* @__PURE__ */ jsx19(Plus3, { "aria-hidden": "true", className: "h-4 w-4" }),
|
|
1780
2331
|
messages["resource.newResource"]
|
|
1781
|
-
] })
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
2332
|
+
] });
|
|
2333
|
+
}
|
|
2334
|
+
return /* @__PURE__ */ jsxs16("div", { className: "flex flex-1 min-h-0 min-w-0", children: [
|
|
2335
|
+
/* @__PURE__ */ jsxs16("div", { className: "flex flex-1 min-h-0 min-w-0 flex-col gap-4 overflow-y-auto p-4", children: [
|
|
2336
|
+
/* @__PURE__ */ jsx19("div", { className: "flex justify-end", children: renderCreateButton() }),
|
|
2337
|
+
isError && /* @__PURE__ */ jsx19(ErrorBanner, { message: messages["common.loadFailure"] }),
|
|
2338
|
+
(createResource.isError || updateResource.isError || deleteResource.isError) && /* @__PURE__ */ jsx19(ErrorBanner, { message: messages["common.actionFailure"] }),
|
|
2339
|
+
isLoading && /* @__PURE__ */ jsx19(ListSkeleton, { label: messages["common.loading"] }),
|
|
2340
|
+
!isLoading && !isError && resources.length === 0 && /* @__PURE__ */ jsx19(
|
|
2341
|
+
EmptyState,
|
|
2342
|
+
{
|
|
2343
|
+
icon: Users,
|
|
2344
|
+
title: messages["resource.emptyTitle"],
|
|
2345
|
+
hint: messages["resource.emptyHint"],
|
|
2346
|
+
action: renderCreateButton()
|
|
2347
|
+
}
|
|
2348
|
+
),
|
|
2349
|
+
!isLoading && resources.length > 0 && /* @__PURE__ */ jsx19(ResourceList, { resources, onSelect: setDraft })
|
|
2350
|
+
] }),
|
|
2351
|
+
isDraftOpen && /* @__PURE__ */ jsx19(
|
|
1786
2352
|
SidePanel,
|
|
1787
2353
|
{
|
|
1788
2354
|
title: isEditing ? messages["resource.editTitle"] : messages["resource.createTitle"],
|
|
1789
2355
|
closeLabel: messages["common.close"],
|
|
1790
2356
|
onClose: () => setDraft(void 0),
|
|
1791
|
-
headerActions: isEditing ? /* @__PURE__ */
|
|
2357
|
+
headerActions: isEditing ? /* @__PURE__ */ jsxs16(
|
|
1792
2358
|
"button",
|
|
1793
2359
|
{
|
|
1794
2360
|
type: "button",
|
|
@@ -1798,12 +2364,12 @@ function ResourcesArea() {
|
|
|
1798
2364
|
},
|
|
1799
2365
|
className: BUTTON_DANGER,
|
|
1800
2366
|
children: [
|
|
1801
|
-
/* @__PURE__ */
|
|
2367
|
+
/* @__PURE__ */ jsx19(Trash23, { "aria-hidden": "true", className: "h-4 w-4" }),
|
|
1802
2368
|
messages["common.remove"]
|
|
1803
2369
|
]
|
|
1804
2370
|
}
|
|
1805
2371
|
) : void 0,
|
|
1806
|
-
children: /* @__PURE__ */
|
|
2372
|
+
children: /* @__PURE__ */ jsx19(
|
|
1807
2373
|
ResourceForm,
|
|
1808
2374
|
{
|
|
1809
2375
|
...draft ? { initialValues: draft } : {},
|
|
@@ -1817,25 +2383,25 @@ function ResourcesArea() {
|
|
|
1817
2383
|
}
|
|
1818
2384
|
|
|
1819
2385
|
// src/workspace/ServicesArea.tsx
|
|
1820
|
-
import { Plus as Plus4, Trash2 as Trash24 } from "lucide-react";
|
|
1821
|
-
import { useState as
|
|
2386
|
+
import { Plus as Plus4, Trash2 as Trash24, Wrench } from "lucide-react";
|
|
2387
|
+
import { useState as useState13 } from "react";
|
|
1822
2388
|
import { MAX_PAGE_SIZE as MAX_PAGE_SIZE5 } from "@adatechnology/scheduling-contracts";
|
|
1823
2389
|
|
|
1824
2390
|
// src/components/ServiceForm.tsx
|
|
1825
|
-
import { useState as
|
|
1826
|
-
import { jsx as
|
|
2391
|
+
import { useState as useState12 } from "react";
|
|
2392
|
+
import { jsx as jsx20, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
1827
2393
|
var INPUT_CLASS3 = "w-full min-h-11 px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500 bg-white dark:bg-gray-900";
|
|
1828
2394
|
var LABEL_CLASS2 = "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1";
|
|
1829
2395
|
var BUTTON_PRIMARY3 = "inline-flex items-center justify-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-colors min-h-11 bg-brand-600 text-white hover:bg-brand-700 disabled:opacity-50";
|
|
1830
2396
|
function ServiceForm({ initialValues, onSubmit }) {
|
|
1831
2397
|
const { locale } = useSchedulingConfig();
|
|
1832
2398
|
const messages = resolveSchedulingMessages(locale);
|
|
1833
|
-
const [name, setName] =
|
|
1834
|
-
const [durationMinutes, setDurationMinutes] =
|
|
1835
|
-
const [bufferBeforeMinutes, setBufferBeforeMinutes] =
|
|
1836
|
-
const [bufferAfterMinutes, setBufferAfterMinutes] =
|
|
1837
|
-
const [active, setActive] =
|
|
1838
|
-
const [submitting, setSubmitting] =
|
|
2399
|
+
const [name, setName] = useState12(initialValues?.name ?? "");
|
|
2400
|
+
const [durationMinutes, setDurationMinutes] = useState12(initialValues?.durationMinutes ?? 30);
|
|
2401
|
+
const [bufferBeforeMinutes, setBufferBeforeMinutes] = useState12(initialValues?.bufferBeforeMinutes ?? 0);
|
|
2402
|
+
const [bufferAfterMinutes, setBufferAfterMinutes] = useState12(initialValues?.bufferAfterMinutes ?? 0);
|
|
2403
|
+
const [active, setActive] = useState12(initialValues?.active ?? true);
|
|
2404
|
+
const [submitting, setSubmitting] = useState12(false);
|
|
1839
2405
|
async function handleSubmit(event) {
|
|
1840
2406
|
event.preventDefault();
|
|
1841
2407
|
setSubmitting(true);
|
|
@@ -1845,10 +2411,10 @@ function ServiceForm({ initialValues, onSubmit }) {
|
|
|
1845
2411
|
setSubmitting(false);
|
|
1846
2412
|
}
|
|
1847
2413
|
}
|
|
1848
|
-
return /* @__PURE__ */
|
|
1849
|
-
/* @__PURE__ */
|
|
1850
|
-
/* @__PURE__ */
|
|
1851
|
-
/* @__PURE__ */
|
|
2414
|
+
return /* @__PURE__ */ jsxs17("form", { onSubmit: handleSubmit, className: "space-y-4", children: [
|
|
2415
|
+
/* @__PURE__ */ jsxs17("div", { children: [
|
|
2416
|
+
/* @__PURE__ */ jsx20("label", { htmlFor: "scheduling-service-name", className: LABEL_CLASS2, children: messages["service.name"] }),
|
|
2417
|
+
/* @__PURE__ */ jsx20(
|
|
1852
2418
|
"input",
|
|
1853
2419
|
{
|
|
1854
2420
|
id: "scheduling-service-name",
|
|
@@ -1860,9 +2426,9 @@ function ServiceForm({ initialValues, onSubmit }) {
|
|
|
1860
2426
|
}
|
|
1861
2427
|
)
|
|
1862
2428
|
] }),
|
|
1863
|
-
/* @__PURE__ */
|
|
1864
|
-
/* @__PURE__ */
|
|
1865
|
-
/* @__PURE__ */
|
|
2429
|
+
/* @__PURE__ */ jsxs17("div", { children: [
|
|
2430
|
+
/* @__PURE__ */ jsx20("label", { htmlFor: "scheduling-service-duration", className: LABEL_CLASS2, children: messages["service.durationMinutes"] }),
|
|
2431
|
+
/* @__PURE__ */ jsx20(
|
|
1866
2432
|
"input",
|
|
1867
2433
|
{
|
|
1868
2434
|
id: "scheduling-service-duration",
|
|
@@ -1875,10 +2441,10 @@ function ServiceForm({ initialValues, onSubmit }) {
|
|
|
1875
2441
|
}
|
|
1876
2442
|
)
|
|
1877
2443
|
] }),
|
|
1878
|
-
/* @__PURE__ */
|
|
1879
|
-
/* @__PURE__ */
|
|
1880
|
-
/* @__PURE__ */
|
|
1881
|
-
/* @__PURE__ */
|
|
2444
|
+
/* @__PURE__ */ jsxs17("div", { className: "grid grid-cols-2 gap-3", children: [
|
|
2445
|
+
/* @__PURE__ */ jsxs17("div", { children: [
|
|
2446
|
+
/* @__PURE__ */ jsx20("label", { htmlFor: "scheduling-service-buffer-before", className: LABEL_CLASS2, children: messages["service.bufferBeforeMinutes"] }),
|
|
2447
|
+
/* @__PURE__ */ jsx20(
|
|
1882
2448
|
"input",
|
|
1883
2449
|
{
|
|
1884
2450
|
id: "scheduling-service-buffer-before",
|
|
@@ -1890,9 +2456,9 @@ function ServiceForm({ initialValues, onSubmit }) {
|
|
|
1890
2456
|
}
|
|
1891
2457
|
)
|
|
1892
2458
|
] }),
|
|
1893
|
-
/* @__PURE__ */
|
|
1894
|
-
/* @__PURE__ */
|
|
1895
|
-
/* @__PURE__ */
|
|
2459
|
+
/* @__PURE__ */ jsxs17("div", { children: [
|
|
2460
|
+
/* @__PURE__ */ jsx20("label", { htmlFor: "scheduling-service-buffer-after", className: LABEL_CLASS2, children: messages["service.bufferAfterMinutes"] }),
|
|
2461
|
+
/* @__PURE__ */ jsx20(
|
|
1896
2462
|
"input",
|
|
1897
2463
|
{
|
|
1898
2464
|
id: "scheduling-service-buffer-after",
|
|
@@ -1905,45 +2471,42 @@ function ServiceForm({ initialValues, onSubmit }) {
|
|
|
1905
2471
|
)
|
|
1906
2472
|
] })
|
|
1907
2473
|
] }),
|
|
1908
|
-
initialValues && /* @__PURE__ */
|
|
1909
|
-
/* @__PURE__ */
|
|
2474
|
+
initialValues && /* @__PURE__ */ jsxs17("label", { className: "flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 min-h-11", children: [
|
|
2475
|
+
/* @__PURE__ */ jsx20("input", { type: "checkbox", checked: active, onChange: (event) => setActive(event.target.checked) }),
|
|
1910
2476
|
messages["service.active"]
|
|
1911
2477
|
] }),
|
|
1912
|
-
/* @__PURE__ */
|
|
2478
|
+
/* @__PURE__ */ jsx20("button", { type: "submit", disabled: submitting, className: BUTTON_PRIMARY3, children: messages["common.save"] })
|
|
1913
2479
|
] });
|
|
1914
2480
|
}
|
|
1915
2481
|
|
|
1916
2482
|
// src/components/ServiceList.tsx
|
|
1917
|
-
import { jsx as
|
|
2483
|
+
import { jsx as jsx21, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
1918
2484
|
function ServiceList({ services, onSelect }) {
|
|
1919
2485
|
const { locale } = useSchedulingConfig();
|
|
1920
2486
|
const messages = resolveSchedulingMessages(locale);
|
|
1921
2487
|
if (services.length === 0) {
|
|
1922
|
-
return /* @__PURE__ */
|
|
2488
|
+
return /* @__PURE__ */ jsx21("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: messages["common.empty"] });
|
|
1923
2489
|
}
|
|
1924
|
-
return /* @__PURE__ */
|
|
2490
|
+
return /* @__PURE__ */ jsx21("ul", { className: `${SURFACE_BORDER} divide-y divide-gray-200 overflow-hidden dark:divide-gray-800`, children: services.map((service, index) => /* @__PURE__ */ jsx21("li", { className: index % 2 === 1 ? ROW_STRIPE : void 0, children: /* @__PURE__ */ jsxs18(
|
|
1925
2491
|
"button",
|
|
1926
2492
|
{
|
|
1927
2493
|
type: "button",
|
|
1928
2494
|
onClick: () => onSelect(service),
|
|
1929
|
-
className: "flex w-full items-center gap-3 px-4 py-3 min-h-11 text-left text-sm hover:bg-
|
|
2495
|
+
className: "flex w-full items-center gap-3 px-4 py-3 min-h-11 text-left text-sm transition-colors hover:bg-brand-50/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand-500 dark:hover:bg-brand-900/20",
|
|
1930
2496
|
children: [
|
|
1931
|
-
/* @__PURE__ */
|
|
1932
|
-
/* @__PURE__ */
|
|
2497
|
+
/* @__PURE__ */ jsx21("span", { className: "flex-1 font-medium text-gray-900 dark:text-gray-100", children: service.name }),
|
|
2498
|
+
/* @__PURE__ */ jsxs18("span", { className: "text-gray-500 dark:text-gray-400", children: [
|
|
1933
2499
|
service.durationMinutes,
|
|
1934
2500
|
" min"
|
|
1935
2501
|
] }),
|
|
1936
|
-
!service.active && /* @__PURE__ */
|
|
2502
|
+
!service.active && /* @__PURE__ */ jsx21("span", { className: "rounded-full bg-gray-200 px-2 py-0.5 text-xs font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-300", children: messages["service.inactive"] })
|
|
1937
2503
|
]
|
|
1938
2504
|
}
|
|
1939
2505
|
) }, service.id)) });
|
|
1940
2506
|
}
|
|
1941
2507
|
|
|
1942
2508
|
// src/workspace/ServicesArea.tsx
|
|
1943
|
-
import { jsx as
|
|
1944
|
-
var BUTTON_CLASS3 = "inline-flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-colors min-h-11";
|
|
1945
|
-
var BUTTON_PRIMARY4 = `${BUTTON_CLASS3} bg-brand-600 text-white hover:bg-brand-700`;
|
|
1946
|
-
var BUTTON_DANGER2 = `${BUTTON_CLASS3} text-red-700 hover:bg-red-50`;
|
|
2509
|
+
import { jsx as jsx22, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
1947
2510
|
function ServicesArea() {
|
|
1948
2511
|
const { locale } = useSchedulingConfig();
|
|
1949
2512
|
const messages = resolveSchedulingMessages(locale);
|
|
@@ -1951,9 +2514,10 @@ function ServicesArea() {
|
|
|
1951
2514
|
const createService = useCreateService();
|
|
1952
2515
|
const updateService = useUpdateService();
|
|
1953
2516
|
const deleteService = useDeleteService();
|
|
1954
|
-
const [draft, setDraft] =
|
|
2517
|
+
const [draft, setDraft] = useState13(void 0);
|
|
1955
2518
|
const isDraftOpen = draft !== void 0;
|
|
1956
2519
|
const isEditing = Boolean(draft);
|
|
2520
|
+
const services = data?.data ?? [];
|
|
1957
2521
|
async function handleSubmit(input) {
|
|
1958
2522
|
try {
|
|
1959
2523
|
if (draft) {
|
|
@@ -1965,21 +2529,36 @@ function ServicesArea() {
|
|
|
1965
2529
|
} catch {
|
|
1966
2530
|
}
|
|
1967
2531
|
}
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
/* @__PURE__ */
|
|
2532
|
+
function renderCreateButton() {
|
|
2533
|
+
return /* @__PURE__ */ jsxs19("button", { type: "button", onClick: () => setDraft(null), className: BUTTON_PRIMARY, children: [
|
|
2534
|
+
/* @__PURE__ */ jsx22(Plus4, { "aria-hidden": "true", className: "h-4 w-4" }),
|
|
1971
2535
|
messages["service.newService"]
|
|
1972
|
-
] })
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2536
|
+
] });
|
|
2537
|
+
}
|
|
2538
|
+
return /* @__PURE__ */ jsxs19("div", { className: "flex flex-1 min-h-0 min-w-0", children: [
|
|
2539
|
+
/* @__PURE__ */ jsxs19("div", { className: "flex flex-1 min-h-0 min-w-0 flex-col gap-4 overflow-y-auto p-4", children: [
|
|
2540
|
+
/* @__PURE__ */ jsx22("div", { className: "flex justify-end", children: renderCreateButton() }),
|
|
2541
|
+
isError && /* @__PURE__ */ jsx22(ErrorBanner, { message: messages["common.loadFailure"] }),
|
|
2542
|
+
(createService.isError || updateService.isError || deleteService.isError) && /* @__PURE__ */ jsx22(ErrorBanner, { message: messages["common.actionFailure"] }),
|
|
2543
|
+
isLoading && /* @__PURE__ */ jsx22(ListSkeleton, { label: messages["common.loading"] }),
|
|
2544
|
+
!isLoading && !isError && services.length === 0 && /* @__PURE__ */ jsx22(
|
|
2545
|
+
EmptyState,
|
|
2546
|
+
{
|
|
2547
|
+
icon: Wrench,
|
|
2548
|
+
title: messages["service.emptyTitle"],
|
|
2549
|
+
hint: messages["service.emptyHint"],
|
|
2550
|
+
action: renderCreateButton()
|
|
2551
|
+
}
|
|
2552
|
+
),
|
|
2553
|
+
!isLoading && services.length > 0 && /* @__PURE__ */ jsx22(ServiceList, { services, onSelect: setDraft })
|
|
2554
|
+
] }),
|
|
2555
|
+
isDraftOpen && /* @__PURE__ */ jsx22(
|
|
1977
2556
|
SidePanel,
|
|
1978
2557
|
{
|
|
1979
2558
|
title: isEditing ? messages["service.editTitle"] : messages["service.createTitle"],
|
|
1980
2559
|
closeLabel: messages["common.close"],
|
|
1981
2560
|
onClose: () => setDraft(void 0),
|
|
1982
|
-
headerActions: isEditing ? /* @__PURE__ */
|
|
2561
|
+
headerActions: isEditing ? /* @__PURE__ */ jsxs19(
|
|
1983
2562
|
"button",
|
|
1984
2563
|
{
|
|
1985
2564
|
type: "button",
|
|
@@ -1987,14 +2566,14 @@ function ServicesArea() {
|
|
|
1987
2566
|
if (draft) void deleteService.mutateAsync(draft.id).then(() => setDraft(void 0)).catch(() => {
|
|
1988
2567
|
});
|
|
1989
2568
|
},
|
|
1990
|
-
className:
|
|
2569
|
+
className: BUTTON_DANGER,
|
|
1991
2570
|
children: [
|
|
1992
|
-
/* @__PURE__ */
|
|
2571
|
+
/* @__PURE__ */ jsx22(Trash24, { "aria-hidden": "true", className: "h-4 w-4" }),
|
|
1993
2572
|
messages["common.remove"]
|
|
1994
2573
|
]
|
|
1995
2574
|
}
|
|
1996
2575
|
) : void 0,
|
|
1997
|
-
children: /* @__PURE__ */
|
|
2576
|
+
children: /* @__PURE__ */ jsx22(
|
|
1998
2577
|
ServiceForm,
|
|
1999
2578
|
{
|
|
2000
2579
|
...draft ? { initialValues: draft } : {},
|
|
@@ -2008,7 +2587,7 @@ function ServicesArea() {
|
|
|
2008
2587
|
}
|
|
2009
2588
|
|
|
2010
2589
|
// src/workspace/WorkspaceAreaNav.tsx
|
|
2011
|
-
import { CalendarDays, Clock, ClipboardList, Users, Wrench } from "lucide-react";
|
|
2590
|
+
import { CalendarDays as CalendarDays2, Clock as Clock2, ClipboardList as ClipboardList2, Users as Users2, Wrench as Wrench2 } from "lucide-react";
|
|
2012
2591
|
|
|
2013
2592
|
// src/workspace/workspace.constant.ts
|
|
2014
2593
|
var SCHEDULING_WORKSPACE_AREA = {
|
|
@@ -2024,16 +2603,16 @@ function isSchedulingWorkspaceArea(value) {
|
|
|
2024
2603
|
}
|
|
2025
2604
|
|
|
2026
2605
|
// src/workspace/WorkspaceAreaNav.tsx
|
|
2027
|
-
import { jsx as
|
|
2028
|
-
var ITEM_BASE = "flex items-center gap-2 px-3 py-2 -mb-px border-b-2 text-sm font-medium transition-colors min-h-11";
|
|
2029
|
-
var ITEM_ACTIVE = "border-brand-600 text-brand-700 dark:text-brand-
|
|
2606
|
+
import { jsx as jsx23, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
2607
|
+
var ITEM_BASE = "flex items-center gap-2 px-3 py-2 -mb-px border-b-2 text-sm font-medium transition-colors min-h-11 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand-500";
|
|
2608
|
+
var ITEM_ACTIVE = "border-brand-600 text-brand-700 dark:text-brand-300";
|
|
2030
2609
|
var ITEM_IDLE = "border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100";
|
|
2031
2610
|
var AREA_ICON = {
|
|
2032
|
-
[SCHEDULING_WORKSPACE_AREA.AGENDA]:
|
|
2033
|
-
[SCHEDULING_WORKSPACE_AREA.BOOKINGS]:
|
|
2034
|
-
[SCHEDULING_WORKSPACE_AREA.RESOURCES]:
|
|
2035
|
-
[SCHEDULING_WORKSPACE_AREA.SERVICES]:
|
|
2036
|
-
[SCHEDULING_WORKSPACE_AREA.AVAILABILITY]:
|
|
2611
|
+
[SCHEDULING_WORKSPACE_AREA.AGENDA]: CalendarDays2,
|
|
2612
|
+
[SCHEDULING_WORKSPACE_AREA.BOOKINGS]: ClipboardList2,
|
|
2613
|
+
[SCHEDULING_WORKSPACE_AREA.RESOURCES]: Users2,
|
|
2614
|
+
[SCHEDULING_WORKSPACE_AREA.SERVICES]: Wrench2,
|
|
2615
|
+
[SCHEDULING_WORKSPACE_AREA.AVAILABILITY]: Clock2
|
|
2037
2616
|
};
|
|
2038
2617
|
function WorkspaceAreaNav({ area, labels, onSelect }) {
|
|
2039
2618
|
const items = [
|
|
@@ -2043,9 +2622,9 @@ function WorkspaceAreaNav({ area, labels, onSelect }) {
|
|
|
2043
2622
|
[SCHEDULING_WORKSPACE_AREA.SERVICES, labels.servicesTab],
|
|
2044
2623
|
[SCHEDULING_WORKSPACE_AREA.AVAILABILITY, labels.availabilityTab]
|
|
2045
2624
|
];
|
|
2046
|
-
return /* @__PURE__ */
|
|
2625
|
+
return /* @__PURE__ */ jsx23("nav", { "aria-label": labels.areaNav, className: "flex shrink-0 gap-1 overflow-x-auto border-b border-gray-200 px-4 dark:border-gray-800", children: items.map(([value, label]) => {
|
|
2047
2626
|
const Icon = AREA_ICON[value];
|
|
2048
|
-
return /* @__PURE__ */
|
|
2627
|
+
return /* @__PURE__ */ jsxs20(
|
|
2049
2628
|
"button",
|
|
2050
2629
|
{
|
|
2051
2630
|
type: "button",
|
|
@@ -2053,7 +2632,7 @@ function WorkspaceAreaNav({ area, labels, onSelect }) {
|
|
|
2053
2632
|
"aria-current": value === area ? "page" : void 0,
|
|
2054
2633
|
className: `${ITEM_BASE} ${value === area ? ITEM_ACTIVE : ITEM_IDLE} whitespace-nowrap`,
|
|
2055
2634
|
children: [
|
|
2056
|
-
/* @__PURE__ */
|
|
2635
|
+
/* @__PURE__ */ jsx23(Icon, { "aria-hidden": "true", className: "w-4 h-4 shrink-0" }),
|
|
2057
2636
|
label
|
|
2058
2637
|
]
|
|
2059
2638
|
},
|
|
@@ -2063,7 +2642,7 @@ function WorkspaceAreaNav({ area, labels, onSelect }) {
|
|
|
2063
2642
|
}
|
|
2064
2643
|
|
|
2065
2644
|
// src/workspace/SchedulingWorkspace.tsx
|
|
2066
|
-
import { jsx as
|
|
2645
|
+
import { jsx as jsx24, jsxs as jsxs21 } from "react/jsx-runtime";
|
|
2067
2646
|
var AREA_COMPONENT = {
|
|
2068
2647
|
[SCHEDULING_WORKSPACE_AREA.AGENDA]: AgendaArea,
|
|
2069
2648
|
[SCHEDULING_WORKSPACE_AREA.BOOKINGS]: BookingsArea,
|
|
@@ -2078,20 +2657,20 @@ function SchedulingWorkspace({
|
|
|
2078
2657
|
onAreaChange
|
|
2079
2658
|
}) {
|
|
2080
2659
|
const labels = { ...DEFAULT_SCHEDULING_WORKSPACE_LABELS, ...labelsOverride };
|
|
2081
|
-
const [internalArea, setInternalArea] =
|
|
2660
|
+
const [internalArea, setInternalArea] = useState14(SCHEDULING_WORKSPACE_AREA.AGENDA);
|
|
2082
2661
|
const area = areaProp ?? internalArea;
|
|
2083
2662
|
const AreaComponent = AREA_COMPONENT[area];
|
|
2084
2663
|
function handleSelectArea(next) {
|
|
2085
2664
|
setInternalArea(next);
|
|
2086
2665
|
onAreaChange?.(next);
|
|
2087
2666
|
}
|
|
2088
|
-
return /* @__PURE__ */
|
|
2089
|
-
/* @__PURE__ */
|
|
2090
|
-
/* @__PURE__ */
|
|
2667
|
+
return /* @__PURE__ */ jsxs21("div", { className: "flex h-full flex-col bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100", children: [
|
|
2668
|
+
/* @__PURE__ */ jsxs21("header", { className: "flex shrink-0 flex-wrap items-center gap-3 px-4 pb-2 pt-4", children: [
|
|
2669
|
+
/* @__PURE__ */ jsx24("h1", { className: "mr-auto text-xl font-semibold tracking-tight text-gray-900 dark:text-gray-100", children: labels.title }),
|
|
2091
2670
|
renderHeaderActions?.()
|
|
2092
2671
|
] }),
|
|
2093
|
-
/* @__PURE__ */
|
|
2094
|
-
/* @__PURE__ */
|
|
2672
|
+
/* @__PURE__ */ jsx24(WorkspaceAreaNav, { area, labels, onSelect: handleSelectArea }),
|
|
2673
|
+
/* @__PURE__ */ jsx24("div", { className: "relative flex flex-1 min-h-0 overflow-hidden", children: /* @__PURE__ */ jsx24(AreaComponent, {}) })
|
|
2095
2674
|
] });
|
|
2096
2675
|
}
|
|
2097
2676
|
export {
|