@adatechnology/scheduling-ui 0.1.0-rc.4 → 0.1.0-rc.5
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 +14 -1
- package/dist/index.js +981 -667
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -316,7 +316,8 @@ var DEFAULT_SCHEDULING_WORKSPACE_LABELS = {
|
|
|
316
316
|
import { useState as useState13 } from "react";
|
|
317
317
|
|
|
318
318
|
// src/workspace/AgendaArea.tsx
|
|
319
|
-
import {
|
|
319
|
+
import { CalendarDays, ChevronLeft, ChevronRight } from "lucide-react";
|
|
320
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
320
321
|
import { MAX_PAGE_SIZE } from "@adatechnology/scheduling-contracts";
|
|
321
322
|
|
|
322
323
|
// src/locales/pt-BR.json
|
|
@@ -410,7 +411,20 @@ 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."
|
|
414
428
|
};
|
|
415
429
|
|
|
416
430
|
// src/locales/en.json
|
|
@@ -504,7 +518,20 @@ var en_default = {
|
|
|
504
518
|
"datetime.year": "Year",
|
|
505
519
|
"datetime.month": "Month",
|
|
506
520
|
"datetime.day": "Day",
|
|
507
|
-
"datetime.time": "Time"
|
|
521
|
+
"datetime.time": "Time",
|
|
522
|
+
"common.emptyHint": "When there is something here, it shows up in this list.",
|
|
523
|
+
"agenda.allResources": "All",
|
|
524
|
+
"agenda.viewLabel": "View",
|
|
525
|
+
"agenda.now": "Now",
|
|
526
|
+
"agenda.emptyTitle": "Nothing scheduled",
|
|
527
|
+
"resource.emptyTitle": "No resources yet",
|
|
528
|
+
"resource.emptyHint": "Resources are who or what serves \u2014 people, rooms, equipment.",
|
|
529
|
+
"service.emptyTitle": "No services yet",
|
|
530
|
+
"service.emptyHint": "Services define what gets booked and how long it takes.",
|
|
531
|
+
"booking.emptyTitle": "No bookings",
|
|
532
|
+
"booking.emptyHint": "Adjust the status filters or wait for new requests.",
|
|
533
|
+
"availability.selectResourceTitle": "Pick a resource",
|
|
534
|
+
"availability.selectResourceHint": "Availability rules are defined per resource."
|
|
508
535
|
};
|
|
509
536
|
|
|
510
537
|
// src/locales/index.ts
|
|
@@ -517,6 +544,47 @@ function resolveSchedulingMessages(locale) {
|
|
|
517
544
|
return MESSAGES_BY_LOCALE[locale] ?? MESSAGES_BY_LOCALE[DEFAULT_SCHEDULING_LOCALE];
|
|
518
545
|
}
|
|
519
546
|
|
|
547
|
+
// src/components/BookingStatusBadge.tsx
|
|
548
|
+
import { BOOKING_STATUS } from "@adatechnology/scheduling-contracts";
|
|
549
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
550
|
+
var BOOKING_STATUS_TONE = {
|
|
551
|
+
[BOOKING_STATUS.REQUESTED]: {
|
|
552
|
+
badge: "bg-amber-100 text-amber-800 dark:bg-amber-900/50 dark:text-amber-200",
|
|
553
|
+
block: "border-amber-400 bg-amber-50 text-amber-900 dark:border-amber-600 dark:bg-amber-900/40 dark:text-amber-100"
|
|
554
|
+
},
|
|
555
|
+
[BOOKING_STATUS.CONFIRMED]: {
|
|
556
|
+
badge: "bg-brand-100 text-brand-800 dark:bg-brand-900/50 dark:text-brand-200",
|
|
557
|
+
block: "border-brand-400 bg-brand-50 text-brand-900 dark:border-brand-600 dark:bg-brand-900/40 dark:text-brand-100"
|
|
558
|
+
},
|
|
559
|
+
[BOOKING_STATUS.CANCELLED]: {
|
|
560
|
+
badge: "bg-gray-200 text-gray-600 line-through dark:bg-gray-700 dark:text-gray-400",
|
|
561
|
+
block: "border-gray-300 bg-gray-100 text-gray-500 line-through dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400"
|
|
562
|
+
},
|
|
563
|
+
[BOOKING_STATUS.COMPLETED]: {
|
|
564
|
+
badge: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/50 dark:text-emerald-200",
|
|
565
|
+
block: "border-emerald-400 bg-emerald-50 text-emerald-900 dark:border-emerald-600 dark:bg-emerald-900/40 dark:text-emerald-100"
|
|
566
|
+
},
|
|
567
|
+
[BOOKING_STATUS.NO_SHOW]: {
|
|
568
|
+
badge: "bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-200",
|
|
569
|
+
block: "border-red-400 bg-red-50 text-red-900 dark:border-red-600 dark:bg-red-900/40 dark:text-red-100"
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
function bookingStatusLabel(messages, status) {
|
|
573
|
+
const key = status === BOOKING_STATUS.NO_SHOW ? "booking.status.noShow" : `booking.status.${status}`;
|
|
574
|
+
return messages[key];
|
|
575
|
+
}
|
|
576
|
+
function BookingStatusBadge({ status }) {
|
|
577
|
+
const { locale } = useSchedulingConfig();
|
|
578
|
+
const messages = resolveSchedulingMessages(locale);
|
|
579
|
+
return /* @__PURE__ */ jsx2(
|
|
580
|
+
"span",
|
|
581
|
+
{
|
|
582
|
+
className: `inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${BOOKING_STATUS_TONE[status].badge}`,
|
|
583
|
+
children: bookingStatusLabel(messages, status)
|
|
584
|
+
}
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
520
588
|
// src/components/agendaLayout.util.ts
|
|
521
589
|
function groupOverlappingBookings(sortedBookings) {
|
|
522
590
|
const clusters = [];
|
|
@@ -565,7 +633,9 @@ function startOfDay(date) {
|
|
|
565
633
|
}
|
|
566
634
|
|
|
567
635
|
// src/components/AgendaGrid.tsx
|
|
568
|
-
import { jsx as
|
|
636
|
+
import { jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
637
|
+
var HOUR_HEIGHT_REM = 4;
|
|
638
|
+
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
639
|
function buildHours(startHour, endHour) {
|
|
570
640
|
return Array.from({ length: endHour - startHour }, (_hour, index) => startHour + index);
|
|
571
641
|
}
|
|
@@ -575,14 +645,18 @@ function bookingsOnDay(bookings, day, startHour, endHour) {
|
|
|
575
645
|
const visibleEnd = new Date(dayStart.getTime() + endHour * 60 * 6e4);
|
|
576
646
|
return bookings.filter((booking) => booking.startsAt < visibleEnd && booking.endsAt > visibleStart);
|
|
577
647
|
}
|
|
578
|
-
function
|
|
648
|
+
function isSameDay(left, right) {
|
|
649
|
+
return startOfDay(left).getTime() === startOfDay(right).getTime();
|
|
650
|
+
}
|
|
651
|
+
function AgendaGrid({ bookings, days, now, onSelect }) {
|
|
579
652
|
const { locale, agendaStartHour, agendaEndHour } = useSchedulingConfig();
|
|
580
653
|
const messages = resolveSchedulingMessages(locale);
|
|
581
654
|
const hours = buildHours(agendaStartHour, agendaEndHour);
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
655
|
+
const columnHeight = `${hours.length * HOUR_HEIGHT_REM}rem`;
|
|
656
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-1 min-h-0 overflow-auto rounded-xl border border-gray-200 dark:border-gray-800", children: [
|
|
657
|
+
/* @__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: [
|
|
658
|
+
/* @__PURE__ */ jsx3("div", { className: `${HEADER_CLASS} border-b border-gray-200 dark:border-gray-800` }),
|
|
659
|
+
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
660
|
String(hour).padStart(2, "0"),
|
|
587
661
|
":00"
|
|
588
662
|
] }, hour))
|
|
@@ -591,47 +665,498 @@ function AgendaGrid({ bookings, days }) {
|
|
|
591
665
|
const dayBookings = bookingsOnDay(bookings, day, agendaStartHour, agendaEndHour);
|
|
592
666
|
const positioned = layoutDayBookings(dayBookings);
|
|
593
667
|
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(
|
|
668
|
+
const isToday = isSameDay(day, now);
|
|
669
|
+
const nowPercent = toVisiblePercent({
|
|
670
|
+
minutesSinceDayStart: minutesFromDayStart(now, dayStart),
|
|
671
|
+
startHour: agendaStartHour,
|
|
672
|
+
endHour: agendaEndHour
|
|
673
|
+
});
|
|
674
|
+
const showNowLine = isToday && nowPercent >= 0 && nowPercent <= 100;
|
|
675
|
+
return /* @__PURE__ */ jsxs(
|
|
676
|
+
"div",
|
|
677
|
+
{
|
|
678
|
+
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" : ""}`,
|
|
679
|
+
children: [
|
|
680
|
+
/* @__PURE__ */ jsxs(
|
|
611
681
|
"div",
|
|
612
682
|
{
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
683
|
+
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" : ""}`,
|
|
684
|
+
children: [
|
|
685
|
+
day.toLocaleDateString(locale, { weekday: "short", day: "2-digit", month: "2-digit" }),
|
|
686
|
+
isToday && /* @__PURE__ */ jsx3("span", { className: "h-1.5 w-1.5 rounded-full bg-brand-600", "aria-hidden": "true" })
|
|
687
|
+
]
|
|
688
|
+
}
|
|
689
|
+
),
|
|
690
|
+
/* @__PURE__ */ jsxs("div", { className: "relative", style: { height: columnHeight }, children: [
|
|
691
|
+
hours.map((hour, index) => /* @__PURE__ */ jsx3(
|
|
692
|
+
"div",
|
|
693
|
+
{
|
|
694
|
+
"aria-hidden": "true",
|
|
695
|
+
className: "absolute inset-x-0 border-b border-gray-100 dark:border-gray-800/60",
|
|
696
|
+
style: { top: `${index / hours.length * 100}%`, height: `${100 / hours.length}%` }
|
|
620
697
|
},
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
698
|
+
hour
|
|
699
|
+
)),
|
|
700
|
+
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"] }),
|
|
701
|
+
positioned.map(({ booking, columnIndex, columnCount }) => {
|
|
702
|
+
const top = toVisiblePercent({
|
|
703
|
+
minutesSinceDayStart: minutesFromDayStart(booking.startsAt, dayStart),
|
|
704
|
+
startHour: agendaStartHour,
|
|
705
|
+
endHour: agendaEndHour
|
|
706
|
+
});
|
|
707
|
+
const bottom = toVisiblePercent({
|
|
708
|
+
minutesSinceDayStart: minutesFromDayStart(booking.endsAt, dayStart),
|
|
709
|
+
startHour: agendaStartHour,
|
|
710
|
+
endHour: agendaEndHour
|
|
711
|
+
});
|
|
712
|
+
const width = 100 / columnCount;
|
|
713
|
+
const startLabel = booking.startsAt.toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" });
|
|
714
|
+
return /* @__PURE__ */ jsxs(
|
|
715
|
+
"button",
|
|
716
|
+
{
|
|
717
|
+
type: "button",
|
|
718
|
+
onClick: () => onSelect?.(booking),
|
|
719
|
+
title: `${startLabel} \xB7 ${booking.title}`,
|
|
720
|
+
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}`,
|
|
721
|
+
style: {
|
|
722
|
+
top: `${top}%`,
|
|
723
|
+
height: `${Math.max(bottom - top, 2)}%`,
|
|
724
|
+
left: `${columnIndex * width}%`,
|
|
725
|
+
width: `${width}%`
|
|
726
|
+
},
|
|
727
|
+
children: [
|
|
728
|
+
/* @__PURE__ */ jsx3("span", { className: "block font-medium tabular-nums opacity-80", children: startLabel }),
|
|
729
|
+
/* @__PURE__ */ jsx3("span", { className: "block truncate", children: booking.title })
|
|
730
|
+
]
|
|
731
|
+
},
|
|
732
|
+
booking.id
|
|
733
|
+
);
|
|
734
|
+
}),
|
|
735
|
+
showNowLine && /* @__PURE__ */ jsxs(
|
|
736
|
+
"div",
|
|
737
|
+
{
|
|
738
|
+
"aria-label": messages["agenda.now"],
|
|
739
|
+
className: "pointer-events-none absolute inset-x-0 z-10 flex items-center",
|
|
740
|
+
style: { top: `${nowPercent}%` },
|
|
741
|
+
children: [
|
|
742
|
+
/* @__PURE__ */ jsx3("span", { className: "h-2 w-2 shrink-0 rounded-full bg-red-500" }),
|
|
743
|
+
/* @__PURE__ */ jsx3("span", { className: "h-px flex-1 bg-red-500" })
|
|
744
|
+
]
|
|
745
|
+
}
|
|
746
|
+
)
|
|
747
|
+
] })
|
|
748
|
+
]
|
|
749
|
+
},
|
|
750
|
+
day.toISOString()
|
|
751
|
+
);
|
|
628
752
|
})
|
|
629
753
|
] });
|
|
630
754
|
}
|
|
631
755
|
|
|
756
|
+
// src/components/BookingDrawer.tsx
|
|
757
|
+
import { XCircle } from "lucide-react";
|
|
758
|
+
import { useState } from "react";
|
|
759
|
+
import { BOOKING_STATUS as BOOKING_STATUS2 } from "@adatechnology/scheduling-contracts";
|
|
760
|
+
|
|
761
|
+
// src/components/DateTimeField.tsx
|
|
762
|
+
import { useId } from "react";
|
|
763
|
+
|
|
764
|
+
// src/components/dateTimeParts.util.ts
|
|
765
|
+
var DATE_TIME_LOCAL_PATTERN = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/;
|
|
766
|
+
var MONTHS_IN_YEAR = 12;
|
|
767
|
+
function parseDateTimeParts(value) {
|
|
768
|
+
const match = DATE_TIME_LOCAL_PATTERN.exec(value);
|
|
769
|
+
if (!match) return void 0;
|
|
770
|
+
const [, year, month, day, hour, minute] = match;
|
|
771
|
+
return { year: Number(year), month: Number(month), day: Number(day), time: `${hour}:${minute}` };
|
|
772
|
+
}
|
|
773
|
+
function daysInMonth(params) {
|
|
774
|
+
return new Date(Date.UTC(params.year, params.month, 0)).getUTCDate();
|
|
775
|
+
}
|
|
776
|
+
function pad(value) {
|
|
777
|
+
return String(value).padStart(2, "0");
|
|
778
|
+
}
|
|
779
|
+
function formatDateTimeParts(parts) {
|
|
780
|
+
const lastDay = daysInMonth({ year: parts.year, month: parts.month });
|
|
781
|
+
const day = Math.min(Math.max(parts.day, 1), lastDay);
|
|
782
|
+
return `${String(parts.year).padStart(4, "0")}-${pad(parts.month)}-${pad(day)}T${parts.time}`;
|
|
783
|
+
}
|
|
784
|
+
function buildYearOptions(params) {
|
|
785
|
+
const current = params.today.getFullYear();
|
|
786
|
+
const first = Math.min(current - params.past, params.year);
|
|
787
|
+
const last = Math.max(current + params.future, params.year);
|
|
788
|
+
return Array.from({ length: last - first + 1 }, (_, index) => first + index);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/components/DateTimeField.tsx
|
|
792
|
+
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
793
|
+
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";
|
|
794
|
+
var YEAR_WINDOW = { PAST: 1, FUTURE: 2 };
|
|
795
|
+
function partsOf(value, fallback) {
|
|
796
|
+
return parseDateTimeParts(value) ?? {
|
|
797
|
+
year: fallback.getFullYear(),
|
|
798
|
+
month: fallback.getMonth() + 1,
|
|
799
|
+
day: fallback.getDate(),
|
|
800
|
+
time: "09:00"
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
function monthLabels(locale) {
|
|
804
|
+
const format = new Intl.DateTimeFormat(locale, { month: "long", timeZone: "UTC" });
|
|
805
|
+
return Array.from(
|
|
806
|
+
{ length: MONTHS_IN_YEAR },
|
|
807
|
+
(_, index) => format.format(new Date(Date.UTC(2026, index, 1)))
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
function DateTimeField({ label, value, onChange, emptyDefault }) {
|
|
811
|
+
const { locale } = useSchedulingConfig();
|
|
812
|
+
const messages = resolveSchedulingMessages(locale);
|
|
813
|
+
const groupId = useId();
|
|
814
|
+
const today = emptyDefault ?? /* @__PURE__ */ new Date();
|
|
815
|
+
const parts = partsOf(value, today);
|
|
816
|
+
const years = buildYearOptions({
|
|
817
|
+
year: parts.year,
|
|
818
|
+
today,
|
|
819
|
+
past: YEAR_WINDOW.PAST,
|
|
820
|
+
future: YEAR_WINDOW.FUTURE
|
|
821
|
+
});
|
|
822
|
+
const days = daysInMonth({ year: parts.year, month: parts.month });
|
|
823
|
+
const months = monthLabels(locale);
|
|
824
|
+
function change(patch) {
|
|
825
|
+
onChange(formatDateTimeParts({ ...parts, ...patch }));
|
|
826
|
+
}
|
|
827
|
+
return /* @__PURE__ */ jsxs2("div", { role: "group", "aria-labelledby": groupId, className: "flex flex-wrap items-center gap-2", children: [
|
|
828
|
+
/* @__PURE__ */ jsx4("span", { id: groupId, className: "sr-only", children: label }),
|
|
829
|
+
/* @__PURE__ */ jsx4(
|
|
830
|
+
"select",
|
|
831
|
+
{
|
|
832
|
+
"aria-label": `${label} \u2014 ${messages["datetime.year"]}`,
|
|
833
|
+
value: parts.year,
|
|
834
|
+
onChange: (event) => change({ year: Number(event.target.value) }),
|
|
835
|
+
className: FIELD_CLASS,
|
|
836
|
+
children: years.map((year) => /* @__PURE__ */ jsx4("option", { value: year, children: year }, year))
|
|
837
|
+
}
|
|
838
|
+
),
|
|
839
|
+
/* @__PURE__ */ jsx4(
|
|
840
|
+
"select",
|
|
841
|
+
{
|
|
842
|
+
"aria-label": `${label} \u2014 ${messages["datetime.month"]}`,
|
|
843
|
+
value: parts.month,
|
|
844
|
+
onChange: (event) => change({ month: Number(event.target.value) }),
|
|
845
|
+
className: FIELD_CLASS,
|
|
846
|
+
children: months.map((name, index) => /* @__PURE__ */ jsx4("option", { value: index + 1, children: name }, name))
|
|
847
|
+
}
|
|
848
|
+
),
|
|
849
|
+
/* @__PURE__ */ jsx4(
|
|
850
|
+
"select",
|
|
851
|
+
{
|
|
852
|
+
"aria-label": `${label} \u2014 ${messages["datetime.day"]}`,
|
|
853
|
+
value: Math.min(parts.day, days),
|
|
854
|
+
onChange: (event) => change({ day: Number(event.target.value) }),
|
|
855
|
+
className: FIELD_CLASS,
|
|
856
|
+
children: Array.from({ length: days }, (_, index) => index + 1).map((day) => /* @__PURE__ */ jsx4("option", { value: day, children: day }, day))
|
|
857
|
+
}
|
|
858
|
+
),
|
|
859
|
+
/* @__PURE__ */ jsx4(
|
|
860
|
+
"input",
|
|
861
|
+
{
|
|
862
|
+
type: "time",
|
|
863
|
+
"aria-label": `${label} \u2014 ${messages["datetime.time"]}`,
|
|
864
|
+
value: parts.time,
|
|
865
|
+
onChange: (event) => change({ time: event.target.value }),
|
|
866
|
+
className: FIELD_CLASS
|
|
867
|
+
}
|
|
868
|
+
)
|
|
869
|
+
] });
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// src/components/datetimeLocal.util.ts
|
|
873
|
+
function getTimeZoneOffsetMs(instant, timeZone) {
|
|
874
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
875
|
+
timeZone,
|
|
876
|
+
year: "numeric",
|
|
877
|
+
month: "2-digit",
|
|
878
|
+
day: "2-digit",
|
|
879
|
+
hour: "2-digit",
|
|
880
|
+
minute: "2-digit",
|
|
881
|
+
second: "2-digit",
|
|
882
|
+
hourCycle: "h23"
|
|
883
|
+
}).formatToParts(instant);
|
|
884
|
+
const get = (type) => Number(parts.find((part) => part.type === type)?.value ?? 0);
|
|
885
|
+
const asUtcOfWallClock = Date.UTC(
|
|
886
|
+
get("year"),
|
|
887
|
+
get("month") - 1,
|
|
888
|
+
get("day"),
|
|
889
|
+
get("hour") % 24,
|
|
890
|
+
get("minute"),
|
|
891
|
+
get("second")
|
|
892
|
+
);
|
|
893
|
+
return asUtcOfWallClock - instant.getTime();
|
|
894
|
+
}
|
|
895
|
+
function parseDateTimeLocalInTimeZone(value, timeZone) {
|
|
896
|
+
const naiveUtc = /* @__PURE__ */ new Date(`${value}:00.000Z`);
|
|
897
|
+
const firstOffset = getTimeZoneOffsetMs(naiveUtc, timeZone);
|
|
898
|
+
const firstPass = new Date(naiveUtc.getTime() - firstOffset);
|
|
899
|
+
const secondOffset = getTimeZoneOffsetMs(firstPass, timeZone);
|
|
900
|
+
return new Date(naiveUtc.getTime() - secondOffset);
|
|
901
|
+
}
|
|
902
|
+
function formatDateTimeLocalInTimeZone(instant, timeZone) {
|
|
903
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
904
|
+
timeZone,
|
|
905
|
+
year: "numeric",
|
|
906
|
+
month: "2-digit",
|
|
907
|
+
day: "2-digit",
|
|
908
|
+
hour: "2-digit",
|
|
909
|
+
minute: "2-digit",
|
|
910
|
+
hourCycle: "h23"
|
|
911
|
+
}).formatToParts(instant);
|
|
912
|
+
const get = (type) => parts.find((part) => part.type === type)?.value ?? "00";
|
|
913
|
+
const hour = get("hour") === "24" ? "00" : get("hour");
|
|
914
|
+
return `${get("year")}-${get("month")}-${get("day")}T${hour}:${get("minute")}`;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// src/components/SidePanel.tsx
|
|
918
|
+
import { X } from "lucide-react";
|
|
919
|
+
|
|
920
|
+
// src/components/ui.constant.ts
|
|
921
|
+
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";
|
|
922
|
+
var BUTTON_PRIMARY = `${BUTTON_BASE} bg-brand-600 text-white hover:bg-brand-700`;
|
|
923
|
+
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`;
|
|
924
|
+
var BUTTON_GHOST = `${BUTTON_BASE} text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800`;
|
|
925
|
+
var BUTTON_DANGER = `${BUTTON_BASE} text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-950/40`;
|
|
926
|
+
var ICON_BUTTON = `${BUTTON_SECONDARY} min-w-11 px-0`;
|
|
927
|
+
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";
|
|
928
|
+
var FIELD_LABEL = "text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400";
|
|
929
|
+
var SURFACE_BORDER = "rounded-xl border border-gray-200 dark:border-gray-800";
|
|
930
|
+
var ROW_STRIPE = "bg-gray-50 dark:bg-gray-800/40";
|
|
931
|
+
|
|
932
|
+
// src/components/SidePanel.tsx
|
|
933
|
+
import { useEffect } from "react";
|
|
934
|
+
import { Fragment, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
935
|
+
function SidePanel({ title, closeLabel, onClose, headerActions, children }) {
|
|
936
|
+
useEffect(() => {
|
|
937
|
+
function handleKeyDown(event) {
|
|
938
|
+
if (event.key === "Escape") onClose();
|
|
939
|
+
}
|
|
940
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
941
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
942
|
+
}, [onClose]);
|
|
943
|
+
return /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
944
|
+
/* @__PURE__ */ jsx5(
|
|
945
|
+
"button",
|
|
946
|
+
{
|
|
947
|
+
type: "button",
|
|
948
|
+
"aria-hidden": "true",
|
|
949
|
+
tabIndex: -1,
|
|
950
|
+
onClick: onClose,
|
|
951
|
+
className: "absolute inset-0 z-10 bg-gray-900/30 backdrop-blur-[1px] wide:hidden"
|
|
952
|
+
}
|
|
953
|
+
),
|
|
954
|
+
/* @__PURE__ */ jsxs3(
|
|
955
|
+
"section",
|
|
956
|
+
{
|
|
957
|
+
"aria-label": title,
|
|
958
|
+
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",
|
|
959
|
+
children: [
|
|
960
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 border-b border-gray-200 px-4 py-3 dark:border-gray-700", children: [
|
|
961
|
+
/* @__PURE__ */ jsx5("h2", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100 mr-auto", children: title }),
|
|
962
|
+
headerActions,
|
|
963
|
+
/* @__PURE__ */ jsxs3(
|
|
964
|
+
"button",
|
|
965
|
+
{
|
|
966
|
+
type: "button",
|
|
967
|
+
onClick: onClose,
|
|
968
|
+
className: BUTTON_GHOST,
|
|
969
|
+
children: [
|
|
970
|
+
/* @__PURE__ */ jsx5(X, { "aria-hidden": "true", className: "w-4 h-4" }),
|
|
971
|
+
closeLabel
|
|
972
|
+
]
|
|
973
|
+
}
|
|
974
|
+
)
|
|
975
|
+
] }),
|
|
976
|
+
/* @__PURE__ */ jsx5("div", { className: "min-h-0 flex-1 overflow-y-auto p-4", children })
|
|
977
|
+
]
|
|
978
|
+
}
|
|
979
|
+
)
|
|
980
|
+
] });
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// src/components/BookingDrawer.tsx
|
|
984
|
+
import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
985
|
+
var BROWSER_TIME_ZONE = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
986
|
+
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";
|
|
987
|
+
var PRIMARY_BUTTON_CLASS = "min-h-11 px-3 rounded-lg text-sm font-medium bg-brand-600 text-white hover:bg-brand-700";
|
|
988
|
+
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";
|
|
989
|
+
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";
|
|
990
|
+
function BookingDrawer({ booking, resourceTimezone, onClose }) {
|
|
991
|
+
const { locale } = useSchedulingConfig();
|
|
992
|
+
const messages = resolveSchedulingMessages(locale);
|
|
993
|
+
const confirmBooking = useConfirmBooking();
|
|
994
|
+
const completeBooking = useCompleteBooking();
|
|
995
|
+
const markNoShow = useMarkNoShow();
|
|
996
|
+
const cancelBooking = useCancelBooking();
|
|
997
|
+
const rescheduleBooking = useRescheduleBooking();
|
|
998
|
+
const timezone = resourceTimezone ?? BROWSER_TIME_ZONE;
|
|
999
|
+
const [isCancelling, setIsCancelling] = useState(false);
|
|
1000
|
+
const [cancelledBy, setCancelledBy] = useState("");
|
|
1001
|
+
const [cancellationReason, setCancellationReason] = useState("");
|
|
1002
|
+
const [isRescheduling, setIsRescheduling] = useState(false);
|
|
1003
|
+
const [start, setStart] = useState(() => formatDateTimeLocalInTimeZone(booking.startsAt, timezone));
|
|
1004
|
+
const [end, setEnd] = useState(() => formatDateTimeLocalInTimeZone(booking.endsAt, timezone));
|
|
1005
|
+
async function handleCancel() {
|
|
1006
|
+
if (!cancelledBy) return;
|
|
1007
|
+
try {
|
|
1008
|
+
await cancelBooking.mutateAsync({
|
|
1009
|
+
id: booking.id,
|
|
1010
|
+
input: { cancelledBy, ...cancellationReason ? { cancellationReason } : {} }
|
|
1011
|
+
});
|
|
1012
|
+
onClose();
|
|
1013
|
+
} catch {
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
async function handleReschedule() {
|
|
1017
|
+
try {
|
|
1018
|
+
await rescheduleBooking.mutateAsync({
|
|
1019
|
+
id: booking.id,
|
|
1020
|
+
input: {
|
|
1021
|
+
during: {
|
|
1022
|
+
start: parseDateTimeLocalInTimeZone(start, timezone),
|
|
1023
|
+
end: parseDateTimeLocalInTimeZone(end, timezone)
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
setIsRescheduling(false);
|
|
1028
|
+
} catch {
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
const isTerminal = booking.status === BOOKING_STATUS2.CANCELLED || booking.status === BOOKING_STATUS2.COMPLETED || booking.status === BOOKING_STATUS2.NO_SHOW;
|
|
1032
|
+
return /* @__PURE__ */ jsx6(SidePanel, { title: messages["booking.detailTitle"], closeLabel: messages["common.close"], onClose, children: /* @__PURE__ */ jsxs4("div", { className: "space-y-4", children: [
|
|
1033
|
+
/* @__PURE__ */ jsxs4("div", { children: [
|
|
1034
|
+
/* @__PURE__ */ jsx6("p", { className: "text-base font-semibold text-gray-900 dark:text-gray-100", children: booking.title }),
|
|
1035
|
+
/* @__PURE__ */ jsx6("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: messages[`booking.status.${booking.status === "no_show" ? "noShow" : booking.status}`] }),
|
|
1036
|
+
/* @__PURE__ */ jsxs4("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: [
|
|
1037
|
+
booking.startsAt.toLocaleString(locale),
|
|
1038
|
+
" \u2192 ",
|
|
1039
|
+
booking.endsAt.toLocaleString(locale)
|
|
1040
|
+
] })
|
|
1041
|
+
] }),
|
|
1042
|
+
(confirmBooking.isError || completeBooking.isError || markNoShow.isError || cancelBooking.isError || rescheduleBooking.isError) && /* @__PURE__ */ jsx6("p", { role: "alert", className: "text-sm text-red-700 bg-red-50 rounded-lg px-3 py-2", children: messages["common.actionFailure"] }),
|
|
1043
|
+
!isTerminal && /* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap gap-2", children: [
|
|
1044
|
+
booking.status === BOOKING_STATUS2.REQUESTED && /* @__PURE__ */ jsx6("button", { type: "button", onClick: () => confirmBooking.mutate(booking.id), className: PRIMARY_BUTTON_CLASS, children: messages["booking.confirm"] }),
|
|
1045
|
+
booking.status === BOOKING_STATUS2.CONFIRMED && /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
1046
|
+
/* @__PURE__ */ jsx6("button", { type: "button", onClick: () => completeBooking.mutate(booking.id), className: ACTION_BUTTON_CLASS, children: messages["booking.complete"] }),
|
|
1047
|
+
/* @__PURE__ */ jsx6("button", { type: "button", onClick: () => markNoShow.mutate(booking.id), className: ACTION_BUTTON_CLASS, children: messages["booking.markNoShow"] }),
|
|
1048
|
+
/* @__PURE__ */ jsx6("button", { type: "button", onClick: () => setIsRescheduling(true), className: ACTION_BUTTON_CLASS, children: messages["booking.reschedule"] })
|
|
1049
|
+
] }),
|
|
1050
|
+
/* @__PURE__ */ jsxs4("button", { type: "button", onClick: () => setIsCancelling(true), className: DANGER_BUTTON_CLASS, children: [
|
|
1051
|
+
/* @__PURE__ */ jsx6(XCircle, { "aria-hidden": "true", className: "w-4 h-4" }),
|
|
1052
|
+
messages["booking.cancel"]
|
|
1053
|
+
] })
|
|
1054
|
+
] }),
|
|
1055
|
+
isRescheduling && /* @__PURE__ */ jsxs4("div", { className: "space-y-2 rounded-lg border border-gray-200 dark:border-gray-700 p-3", children: [
|
|
1056
|
+
/* @__PURE__ */ jsx6(
|
|
1057
|
+
DateTimeField,
|
|
1058
|
+
{
|
|
1059
|
+
label: messages["booking.rescheduleStart"],
|
|
1060
|
+
value: start,
|
|
1061
|
+
onChange: setStart
|
|
1062
|
+
}
|
|
1063
|
+
),
|
|
1064
|
+
/* @__PURE__ */ jsx6(DateTimeField, { label: messages["booking.rescheduleEnd"], value: end, onChange: setEnd }),
|
|
1065
|
+
/* @__PURE__ */ jsx6("button", { type: "button", onClick: handleReschedule, disabled: rescheduleBooking.isPending, className: PRIMARY_BUTTON_CLASS, children: messages["common.save"] })
|
|
1066
|
+
] }),
|
|
1067
|
+
isCancelling && /* @__PURE__ */ jsxs4("div", { className: "space-y-2 rounded-lg border border-gray-200 dark:border-gray-700 p-3", children: [
|
|
1068
|
+
/* @__PURE__ */ jsx6(
|
|
1069
|
+
"input",
|
|
1070
|
+
{
|
|
1071
|
+
type: "text",
|
|
1072
|
+
placeholder: messages["booking.cancelledBy"],
|
|
1073
|
+
value: cancelledBy,
|
|
1074
|
+
onChange: (event) => setCancelledBy(event.target.value),
|
|
1075
|
+
className: INPUT_CLASS
|
|
1076
|
+
}
|
|
1077
|
+
),
|
|
1078
|
+
/* @__PURE__ */ jsx6(
|
|
1079
|
+
"input",
|
|
1080
|
+
{
|
|
1081
|
+
type: "text",
|
|
1082
|
+
placeholder: messages["booking.cancelReason"],
|
|
1083
|
+
value: cancellationReason,
|
|
1084
|
+
onChange: (event) => setCancellationReason(event.target.value),
|
|
1085
|
+
className: INPUT_CLASS
|
|
1086
|
+
}
|
|
1087
|
+
),
|
|
1088
|
+
/* @__PURE__ */ jsxs4("button", { type: "button", onClick: handleCancel, disabled: cancelBooking.isPending, className: DANGER_BUTTON_CLASS, children: [
|
|
1089
|
+
/* @__PURE__ */ jsx6(XCircle, { "aria-hidden": "true", className: "w-4 h-4" }),
|
|
1090
|
+
messages["booking.cancel"]
|
|
1091
|
+
] })
|
|
1092
|
+
] })
|
|
1093
|
+
] }) });
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// src/components/ResourceSelect.tsx
|
|
1097
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1098
|
+
function ResourceSelect({ label, emptyOptionLabel, resources, value, onChange }) {
|
|
1099
|
+
return /* @__PURE__ */ jsxs5("label", { className: "flex flex-col gap-1", children: [
|
|
1100
|
+
/* @__PURE__ */ jsx7("span", { className: FIELD_LABEL, children: label }),
|
|
1101
|
+
/* @__PURE__ */ jsxs5(
|
|
1102
|
+
"select",
|
|
1103
|
+
{
|
|
1104
|
+
value,
|
|
1105
|
+
onChange: (event) => onChange(event.target.value),
|
|
1106
|
+
className: `${FIELD_CONTROL} min-w-44`,
|
|
1107
|
+
children: [
|
|
1108
|
+
/* @__PURE__ */ jsx7("option", { value: "", children: emptyOptionLabel }),
|
|
1109
|
+
resources.map((resource) => /* @__PURE__ */ jsx7("option", { value: resource.id, children: resource.name }, resource.id))
|
|
1110
|
+
]
|
|
1111
|
+
}
|
|
1112
|
+
)
|
|
1113
|
+
] });
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// src/components/StateFeedback.tsx
|
|
1117
|
+
import { AlertTriangle } from "lucide-react";
|
|
1118
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1119
|
+
function ErrorBanner({ message }) {
|
|
1120
|
+
return /* @__PURE__ */ jsxs6(
|
|
1121
|
+
"p",
|
|
1122
|
+
{
|
|
1123
|
+
role: "alert",
|
|
1124
|
+
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",
|
|
1125
|
+
children: [
|
|
1126
|
+
/* @__PURE__ */ jsx8(AlertTriangle, { "aria-hidden": "true", className: "mt-0.5 h-4 w-4 shrink-0" }),
|
|
1127
|
+
message
|
|
1128
|
+
]
|
|
1129
|
+
}
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
function EmptyState({ icon: Icon, title, hint, action }) {
|
|
1133
|
+
return /* @__PURE__ */ jsxs6("div", { className: `${SURFACE_BORDER} flex flex-col items-center gap-2 px-6 py-12 text-center`, children: [
|
|
1134
|
+
/* @__PURE__ */ jsx8("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__ */ jsx8(Icon, { "aria-hidden": "true", className: "h-6 w-6" }) }),
|
|
1135
|
+
/* @__PURE__ */ jsx8("p", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: title }),
|
|
1136
|
+
hint && /* @__PURE__ */ jsx8("p", { className: "max-w-sm text-sm text-gray-500 dark:text-gray-400", children: hint }),
|
|
1137
|
+
action && /* @__PURE__ */ jsx8("div", { className: "mt-2", children: action })
|
|
1138
|
+
] });
|
|
1139
|
+
}
|
|
1140
|
+
var SKELETON_ROW = "h-11 animate-pulse rounded-lg bg-gray-100 dark:bg-gray-800";
|
|
1141
|
+
function ListSkeleton({ label, rows = 5 }) {
|
|
1142
|
+
return /* @__PURE__ */ jsx8("div", { "aria-busy": "true", "aria-label": label, role: "status", className: "space-y-2", children: Array.from({ length: rows }, (_row, index) => /* @__PURE__ */ jsx8("div", { className: SKELETON_ROW }, index)) });
|
|
1143
|
+
}
|
|
1144
|
+
function BlockSkeleton({ label }) {
|
|
1145
|
+
return /* @__PURE__ */ jsx8(
|
|
1146
|
+
"div",
|
|
1147
|
+
{
|
|
1148
|
+
"aria-busy": "true",
|
|
1149
|
+
"aria-label": label,
|
|
1150
|
+
role: "status",
|
|
1151
|
+
className: "min-h-64 flex-1 animate-pulse rounded-xl bg-gray-100 dark:bg-gray-800"
|
|
1152
|
+
}
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
632
1156
|
// src/workspace/AgendaArea.tsx
|
|
633
|
-
import {
|
|
1157
|
+
import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
634
1158
|
var DAY_IN_MS = 24 * 60 * 6e4;
|
|
1159
|
+
var NOW_TICK_MS = 6e4;
|
|
635
1160
|
function startOfWeek(date, weekStartsOn) {
|
|
636
1161
|
const start = startOfDay(date);
|
|
637
1162
|
const offset = (start.getDay() - weekStartsOn + 7) % 7;
|
|
@@ -642,14 +1167,32 @@ function buildVisibleDays(anchorDate, view, weekStartsOn) {
|
|
|
642
1167
|
const weekStart = startOfWeek(anchorDate, weekStartsOn);
|
|
643
1168
|
return Array.from({ length: 7 }, (_day, index) => new Date(weekStart.getTime() + index * DAY_IN_MS));
|
|
644
1169
|
}
|
|
645
|
-
|
|
1170
|
+
function formatPeriod(days, locale) {
|
|
1171
|
+
const first = days[0];
|
|
1172
|
+
if (days.length === 1) {
|
|
1173
|
+
return first.toLocaleDateString(locale, { weekday: "long", day: "2-digit", month: "long", year: "numeric" });
|
|
1174
|
+
}
|
|
1175
|
+
const last = days[days.length - 1];
|
|
1176
|
+
const short = { day: "2-digit", month: "short" };
|
|
1177
|
+
return `${first.toLocaleDateString(locale, short)} \u2013 ${last.toLocaleDateString(locale, { ...short, year: "numeric" })}`;
|
|
1178
|
+
}
|
|
1179
|
+
function useNow() {
|
|
1180
|
+
const [now, setNow] = useState2(() => /* @__PURE__ */ new Date());
|
|
1181
|
+
useEffect2(() => {
|
|
1182
|
+
const timer = window.setInterval(() => setNow(/* @__PURE__ */ new Date()), NOW_TICK_MS);
|
|
1183
|
+
return () => window.clearInterval(timer);
|
|
1184
|
+
}, []);
|
|
1185
|
+
return now;
|
|
1186
|
+
}
|
|
646
1187
|
function AgendaArea() {
|
|
647
1188
|
const { locale, weekStartsOn } = useSchedulingConfig();
|
|
648
1189
|
const messages = resolveSchedulingMessages(locale);
|
|
1190
|
+
const now = useNow();
|
|
649
1191
|
const { data: resourcesPage } = useResources({ active: true, pageSize: MAX_PAGE_SIZE });
|
|
650
|
-
const [resourceId, setResourceId] =
|
|
651
|
-
const [view, setView] =
|
|
652
|
-
const [anchorDate, setAnchorDate] =
|
|
1192
|
+
const [resourceId, setResourceId] = useState2("");
|
|
1193
|
+
const [view, setView] = useState2("day");
|
|
1194
|
+
const [anchorDate, setAnchorDate] = useState2(() => /* @__PURE__ */ new Date());
|
|
1195
|
+
const [selectedBookingId, setSelectedBookingId] = useState2(void 0);
|
|
653
1196
|
const resources = resourcesPage?.data ?? [];
|
|
654
1197
|
const days = buildVisibleDays(anchorDate, view, weekStartsOn);
|
|
655
1198
|
const from = days[0];
|
|
@@ -657,70 +1200,115 @@ function AgendaArea() {
|
|
|
657
1200
|
const { data: bookingsPage, isLoading, isError } = useBookings(
|
|
658
1201
|
resourceId ? { resourceId, from, until, pageSize: MAX_PAGE_SIZE } : { from, until, pageSize: MAX_PAGE_SIZE }
|
|
659
1202
|
);
|
|
660
|
-
|
|
661
|
-
|
|
1203
|
+
const visibleBookings = bookingsPage?.data ?? [];
|
|
1204
|
+
const selectedBooking = visibleBookings.find((booking) => booking.id === selectedBookingId);
|
|
1205
|
+
const selectedResourceTimezone = resources.find(
|
|
1206
|
+
(resource) => resource.id === selectedBooking?.resourceIds[0]
|
|
1207
|
+
)?.timezone;
|
|
1208
|
+
function shiftAnchor(direction) {
|
|
1209
|
+
const step = view === "day" ? 1 : 7;
|
|
1210
|
+
setAnchorDate((current) => new Date(current.getTime() + direction * step * DAY_IN_MS));
|
|
1211
|
+
}
|
|
1212
|
+
function renderViewOption(value, label) {
|
|
1213
|
+
const isActive = view === value;
|
|
1214
|
+
return /* @__PURE__ */ jsx9(
|
|
1215
|
+
"button",
|
|
1216
|
+
{
|
|
1217
|
+
type: "button",
|
|
1218
|
+
onClick: () => setView(value),
|
|
1219
|
+
"aria-pressed": isActive,
|
|
1220
|
+
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"}`,
|
|
1221
|
+
children: label
|
|
1222
|
+
}
|
|
1223
|
+
);
|
|
662
1224
|
}
|
|
663
|
-
return /* @__PURE__ */
|
|
664
|
-
/* @__PURE__ */
|
|
665
|
-
/* @__PURE__ */
|
|
666
|
-
/* @__PURE__ */
|
|
667
|
-
|
|
668
|
-
"select",
|
|
1225
|
+
return /* @__PURE__ */ jsxs7("div", { className: "flex flex-1 min-h-0 min-w-0", children: [
|
|
1226
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex flex-1 min-h-0 min-w-0 flex-col gap-4 overflow-y-auto p-4", children: [
|
|
1227
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex flex-wrap items-end gap-3", children: [
|
|
1228
|
+
/* @__PURE__ */ jsx9(
|
|
1229
|
+
ResourceSelect,
|
|
669
1230
|
{
|
|
1231
|
+
label: messages["agenda.resourceLabel"],
|
|
1232
|
+
emptyOptionLabel: messages["agenda.allResources"],
|
|
1233
|
+
resources,
|
|
670
1234
|
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"]
|
|
1235
|
+
onChange: setResourceId
|
|
694
1236
|
}
|
|
695
1237
|
),
|
|
696
|
-
/* @__PURE__ */
|
|
697
|
-
|
|
1238
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-1", children: [
|
|
1239
|
+
/* @__PURE__ */ jsx9(
|
|
1240
|
+
"button",
|
|
1241
|
+
{
|
|
1242
|
+
type: "button",
|
|
1243
|
+
"aria-label": messages["agenda.previous"],
|
|
1244
|
+
onClick: () => shiftAnchor(-1),
|
|
1245
|
+
className: ICON_BUTTON,
|
|
1246
|
+
children: /* @__PURE__ */ jsx9(ChevronLeft, { "aria-hidden": "true", className: "h-4 w-4" })
|
|
1247
|
+
}
|
|
1248
|
+
),
|
|
1249
|
+
/* @__PURE__ */ jsx9("button", { type: "button", onClick: () => setAnchorDate(/* @__PURE__ */ new Date()), className: BUTTON_SECONDARY, children: messages["agenda.today"] }),
|
|
1250
|
+
/* @__PURE__ */ jsx9(
|
|
1251
|
+
"button",
|
|
1252
|
+
{
|
|
1253
|
+
type: "button",
|
|
1254
|
+
"aria-label": messages["agenda.next"],
|
|
1255
|
+
onClick: () => shiftAnchor(1),
|
|
1256
|
+
className: ICON_BUTTON,
|
|
1257
|
+
children: /* @__PURE__ */ jsx9(ChevronRight, { "aria-hidden": "true", className: "h-4 w-4" })
|
|
1258
|
+
}
|
|
1259
|
+
)
|
|
1260
|
+
] }),
|
|
1261
|
+
/* @__PURE__ */ jsx9(
|
|
1262
|
+
"p",
|
|
698
1263
|
{
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
className: `${NAV_BUTTON_CLASS} ${view === "week" ? "bg-brand-600 text-white border-brand-600" : ""}`,
|
|
703
|
-
children: messages["agenda.viewWeek"]
|
|
1264
|
+
"aria-live": "polite",
|
|
1265
|
+
className: "min-w-0 flex-1 truncate text-sm font-semibold text-gray-900 first-letter:uppercase dark:text-gray-100",
|
|
1266
|
+
children: formatPeriod(days, locale)
|
|
704
1267
|
}
|
|
705
|
-
)
|
|
706
|
-
|
|
1268
|
+
),
|
|
1269
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex flex-col gap-1", children: [
|
|
1270
|
+
/* @__PURE__ */ jsx9("span", { className: FIELD_LABEL, children: messages["agenda.viewLabel"] }),
|
|
1271
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex gap-1 rounded-lg bg-gray-100 p-1 dark:bg-gray-800", children: [
|
|
1272
|
+
renderViewOption("day", messages["agenda.viewDay"]),
|
|
1273
|
+
renderViewOption("week", messages["agenda.viewWeek"])
|
|
1274
|
+
] })
|
|
1275
|
+
] })
|
|
1276
|
+
] }),
|
|
1277
|
+
isError && /* @__PURE__ */ jsx9(ErrorBanner, { message: messages["common.loadFailure"] }),
|
|
1278
|
+
bookingsPage && bookingsPage.total > bookingsPage.data.length && /* @__PURE__ */ jsx9("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"] }),
|
|
1279
|
+
isLoading && /* @__PURE__ */ jsx9(BlockSkeleton, { label: messages["common.loading"] }),
|
|
1280
|
+
!isLoading && !isError && visibleBookings.length === 0 && /* @__PURE__ */ jsx9(EmptyState, { icon: CalendarDays, title: messages["agenda.emptyTitle"], hint: messages["agenda.empty"] }),
|
|
1281
|
+
!isLoading && visibleBookings.length > 0 && /* @__PURE__ */ jsx9(
|
|
1282
|
+
AgendaGrid,
|
|
1283
|
+
{
|
|
1284
|
+
bookings: visibleBookings,
|
|
1285
|
+
days,
|
|
1286
|
+
now,
|
|
1287
|
+
onSelect: (booking) => setSelectedBookingId(booking.id)
|
|
1288
|
+
}
|
|
1289
|
+
)
|
|
707
1290
|
] }),
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
1291
|
+
selectedBooking && /* @__PURE__ */ jsx9(
|
|
1292
|
+
BookingDrawer,
|
|
1293
|
+
{
|
|
1294
|
+
booking: selectedBooking,
|
|
1295
|
+
resourceTimezone: selectedResourceTimezone,
|
|
1296
|
+
onClose: () => setSelectedBookingId(void 0)
|
|
1297
|
+
},
|
|
1298
|
+
selectedBooking.id
|
|
1299
|
+
)
|
|
713
1300
|
] });
|
|
714
1301
|
}
|
|
715
1302
|
|
|
716
1303
|
// src/workspace/AvailabilityArea.tsx
|
|
717
|
-
import {
|
|
1304
|
+
import { Clock } from "lucide-react";
|
|
1305
|
+
import { useState as useState5 } from "react";
|
|
718
1306
|
import { MAX_PAGE_SIZE as MAX_PAGE_SIZE2 } from "@adatechnology/scheduling-contracts";
|
|
719
1307
|
|
|
720
1308
|
// src/components/WeeklyRulesEditor.tsx
|
|
721
1309
|
import { Plus, Trash2 } from "lucide-react";
|
|
722
|
-
import { useState as
|
|
723
|
-
import { jsx as
|
|
1310
|
+
import { useState as useState3 } from "react";
|
|
1311
|
+
import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
724
1312
|
var WEEKDAYS = [0, 1, 2, 3, 4, 5, 6];
|
|
725
1313
|
function toDraft(rule) {
|
|
726
1314
|
return { weekday: rule.weekday, startsAtLocal: rule.startsAtLocal, endsAtLocal: rule.endsAtLocal };
|
|
@@ -733,7 +1321,7 @@ function WeeklyRulesEditor({ resourceId }) {
|
|
|
733
1321
|
const messages = resolveSchedulingMessages(locale);
|
|
734
1322
|
const { data, isSuccess: isRulesLoaded, isError: isRulesLoadError } = useAvailabilityRules(resourceId);
|
|
735
1323
|
const setAvailabilityRules = useSetAvailabilityRules();
|
|
736
|
-
const [draft, setDraft] =
|
|
1324
|
+
const [draft, setDraft] = useState3(void 0);
|
|
737
1325
|
const rules = draft ?? (data ? data.map(toDraft) : []);
|
|
738
1326
|
function updateRule(index, patch) {
|
|
739
1327
|
setDraft(rules.map((rule, ruleIndex) => ruleIndex === index ? { ...rule, ...patch } : rule));
|
|
@@ -755,32 +1343,32 @@ function WeeklyRulesEditor({ resourceId }) {
|
|
|
755
1343
|
}
|
|
756
1344
|
}
|
|
757
1345
|
if (isRulesLoadError) {
|
|
758
|
-
return /* @__PURE__ */
|
|
759
|
-
/* @__PURE__ */
|
|
760
|
-
/* @__PURE__ */
|
|
1346
|
+
return /* @__PURE__ */ jsxs8("section", { className: "space-y-3", children: [
|
|
1347
|
+
/* @__PURE__ */ jsx10("h3", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: messages["availability.weeklyRulesTitle"] }),
|
|
1348
|
+
/* @__PURE__ */ jsx10("p", { role: "alert", className: "text-sm text-red-700 bg-red-50 rounded-lg px-3 py-2", children: messages["common.loadFailure"] })
|
|
761
1349
|
] });
|
|
762
1350
|
}
|
|
763
1351
|
if (!isRulesLoaded) {
|
|
764
|
-
return /* @__PURE__ */
|
|
765
|
-
/* @__PURE__ */
|
|
766
|
-
/* @__PURE__ */
|
|
1352
|
+
return /* @__PURE__ */ jsxs8("section", { className: "space-y-3", children: [
|
|
1353
|
+
/* @__PURE__ */ jsx10("h3", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: messages["availability.weeklyRulesTitle"] }),
|
|
1354
|
+
/* @__PURE__ */ jsx10("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: messages["common.loading"] })
|
|
767
1355
|
] });
|
|
768
1356
|
}
|
|
769
|
-
return /* @__PURE__ */
|
|
770
|
-
/* @__PURE__ */
|
|
771
|
-
setAvailabilityRules.isError && /* @__PURE__ */
|
|
772
|
-
/* @__PURE__ */
|
|
773
|
-
/* @__PURE__ */
|
|
1357
|
+
return /* @__PURE__ */ jsxs8("section", { className: "space-y-3", children: [
|
|
1358
|
+
/* @__PURE__ */ jsx10("h3", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: messages["availability.weeklyRulesTitle"] }),
|
|
1359
|
+
setAvailabilityRules.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"] }),
|
|
1360
|
+
/* @__PURE__ */ jsx10("ul", { className: "space-y-2", children: rules.map((rule, index) => /* @__PURE__ */ jsxs8("li", { className: "flex items-center gap-2", children: [
|
|
1361
|
+
/* @__PURE__ */ jsx10(
|
|
774
1362
|
"select",
|
|
775
1363
|
{
|
|
776
1364
|
"aria-label": messages["availability.weekdayLabel"],
|
|
777
1365
|
value: rule.weekday,
|
|
778
1366
|
onChange: (event) => updateRule(index, { weekday: Number(event.target.value) }),
|
|
779
1367
|
className: "min-h-11 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 text-sm",
|
|
780
|
-
children: WEEKDAYS.map((weekday) => /* @__PURE__ */
|
|
1368
|
+
children: WEEKDAYS.map((weekday) => /* @__PURE__ */ jsx10("option", { value: weekday, children: messages[`availability.weekday.${weekday}`] }, weekday))
|
|
781
1369
|
}
|
|
782
1370
|
),
|
|
783
|
-
/* @__PURE__ */
|
|
1371
|
+
/* @__PURE__ */ jsx10(
|
|
784
1372
|
"input",
|
|
785
1373
|
{
|
|
786
1374
|
"aria-label": messages["availability.startsAtLocal"],
|
|
@@ -790,7 +1378,7 @@ function WeeklyRulesEditor({ resourceId }) {
|
|
|
790
1378
|
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
1379
|
}
|
|
792
1380
|
),
|
|
793
|
-
/* @__PURE__ */
|
|
1381
|
+
/* @__PURE__ */ jsx10(
|
|
794
1382
|
"input",
|
|
795
1383
|
{
|
|
796
1384
|
"aria-label": messages["availability.endsAtLocal"],
|
|
@@ -800,207 +1388,49 @@ function WeeklyRulesEditor({ resourceId }) {
|
|
|
800
1388
|
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
1389
|
}
|
|
802
1390
|
),
|
|
803
|
-
/* @__PURE__ */
|
|
1391
|
+
/* @__PURE__ */ jsx10(
|
|
804
1392
|
"button",
|
|
805
1393
|
{
|
|
806
1394
|
type: "button",
|
|
807
1395
|
onClick: () => removeRule(index),
|
|
808
1396
|
"aria-label": messages["availability.removeRule"],
|
|
809
1397
|
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__ */
|
|
1398
|
+
children: /* @__PURE__ */ jsx10(Trash2, { "aria-hidden": "true", className: "w-4 h-4" })
|
|
811
1399
|
}
|
|
812
1400
|
)
|
|
813
1401
|
] }, index)) }),
|
|
814
|
-
/* @__PURE__ */
|
|
815
|
-
/* @__PURE__ */
|
|
1402
|
+
/* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [
|
|
1403
|
+
/* @__PURE__ */ jsxs8(
|
|
816
1404
|
"button",
|
|
817
1405
|
{
|
|
818
1406
|
type: "button",
|
|
819
1407
|
onClick: addRule,
|
|
820
1408
|
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
1409
|
children: [
|
|
822
|
-
/* @__PURE__ */
|
|
1410
|
+
/* @__PURE__ */ jsx10(Plus, { "aria-hidden": "true", className: "w-4 h-4" }),
|
|
823
1411
|
messages["availability.addRule"]
|
|
824
1412
|
]
|
|
825
1413
|
}
|
|
826
1414
|
),
|
|
827
|
-
/* @__PURE__ */
|
|
1415
|
+
/* @__PURE__ */ jsx10(
|
|
828
1416
|
"button",
|
|
829
1417
|
{
|
|
830
1418
|
type: "button",
|
|
831
1419
|
onClick: handleSave,
|
|
832
|
-
disabled: setAvailabilityRules.isPending,
|
|
833
|
-
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
|
-
children: messages["common.save"]
|
|
835
|
-
}
|
|
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
|
-
)
|
|
1420
|
+
disabled: setAvailabilityRules.isPending,
|
|
1421
|
+
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",
|
|
1422
|
+
children: messages["common.save"]
|
|
1423
|
+
}
|
|
1424
|
+
)
|
|
1425
|
+
] })
|
|
954
1426
|
] });
|
|
955
1427
|
}
|
|
956
1428
|
|
|
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
1429
|
// src/components/AvailabilityExceptionsEditor.tsx
|
|
1003
|
-
import {
|
|
1430
|
+
import { Plus as Plus2, Trash2 as Trash22 } from "lucide-react";
|
|
1431
|
+
import { useState as useState4 } from "react";
|
|
1432
|
+
import { AVAILABILITY_EXCEPTION_KIND } from "@adatechnology/scheduling-contracts";
|
|
1433
|
+
import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1004
1434
|
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
1435
|
function AvailabilityExceptionsEditor({ resourceId, timezone }) {
|
|
1006
1436
|
const { locale } = useSchedulingConfig();
|
|
@@ -1009,10 +1439,10 @@ function AvailabilityExceptionsEditor({ resourceId, timezone }) {
|
|
|
1009
1439
|
const addException = useAddAvailabilityException();
|
|
1010
1440
|
const removeException = useRemoveAvailabilityException();
|
|
1011
1441
|
const emptyValue = () => formatDateTimeLocalInTimeZone(/* @__PURE__ */ new Date(), timezone);
|
|
1012
|
-
const [from, setFrom] =
|
|
1013
|
-
const [until, setUntil] =
|
|
1014
|
-
const [kind, setKind] =
|
|
1015
|
-
const [reason, setReason] =
|
|
1442
|
+
const [from, setFrom] = useState4(emptyValue);
|
|
1443
|
+
const [until, setUntil] = useState4(emptyValue);
|
|
1444
|
+
const [kind, setKind] = useState4(AVAILABILITY_EXCEPTION_KIND.BLOCK);
|
|
1445
|
+
const [reason, setReason] = useState4("");
|
|
1016
1446
|
async function handleAdd() {
|
|
1017
1447
|
if (!from || !until) return;
|
|
1018
1448
|
try {
|
|
@@ -1031,13 +1461,13 @@ function AvailabilityExceptionsEditor({ resourceId, timezone }) {
|
|
|
1031
1461
|
} catch {
|
|
1032
1462
|
}
|
|
1033
1463
|
}
|
|
1034
|
-
return /* @__PURE__ */
|
|
1035
|
-
/* @__PURE__ */
|
|
1036
|
-
(addException.isError || removeException.isError) && /* @__PURE__ */
|
|
1037
|
-
isLoadError && /* @__PURE__ */
|
|
1038
|
-
isLoading && /* @__PURE__ */
|
|
1039
|
-
/* @__PURE__ */
|
|
1040
|
-
/* @__PURE__ */
|
|
1464
|
+
return /* @__PURE__ */ jsxs9("section", { className: "space-y-3", children: [
|
|
1465
|
+
/* @__PURE__ */ jsx11("h3", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: messages["availability.exceptionsTitle"] }),
|
|
1466
|
+
(addException.isError || removeException.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"] }),
|
|
1467
|
+
isLoadError && /* @__PURE__ */ jsx11("p", { role: "alert", className: "text-sm text-red-700 bg-red-50 rounded-lg px-3 py-2", children: messages["common.loadFailure"] }),
|
|
1468
|
+
isLoading && /* @__PURE__ */ jsx11("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: messages["common.loading"] }),
|
|
1469
|
+
/* @__PURE__ */ jsx11("ul", { className: "space-y-2", children: (data ?? []).map((exception) => /* @__PURE__ */ jsxs9("li", { className: "flex items-center gap-2 text-sm", children: [
|
|
1470
|
+
/* @__PURE__ */ jsxs9("span", { className: "flex-1", children: [
|
|
1041
1471
|
messages[`availability.exceptionKind.${exception.kind === "block" ? "blocked" : "extra"}`],
|
|
1042
1472
|
" \u2014",
|
|
1043
1473
|
" ",
|
|
@@ -1046,19 +1476,19 @@ function AvailabilityExceptionsEditor({ resourceId, timezone }) {
|
|
|
1046
1476
|
exception.during.end.toLocaleString(locale),
|
|
1047
1477
|
exception.reason ? ` (${exception.reason})` : ""
|
|
1048
1478
|
] }),
|
|
1049
|
-
/* @__PURE__ */
|
|
1479
|
+
/* @__PURE__ */ jsx11(
|
|
1050
1480
|
"button",
|
|
1051
1481
|
{
|
|
1052
1482
|
type: "button",
|
|
1053
1483
|
onClick: () => removeException.mutate({ id: exception.id, resourceId }),
|
|
1054
1484
|
"aria-label": messages["availability.removeException"],
|
|
1055
1485
|
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__ */
|
|
1486
|
+
children: /* @__PURE__ */ jsx11(Trash22, { "aria-hidden": "true", className: "w-4 h-4" })
|
|
1057
1487
|
}
|
|
1058
1488
|
)
|
|
1059
1489
|
] }, exception.id)) }),
|
|
1060
|
-
/* @__PURE__ */
|
|
1061
|
-
/* @__PURE__ */
|
|
1490
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
1491
|
+
/* @__PURE__ */ jsx11(
|
|
1062
1492
|
DateTimeField,
|
|
1063
1493
|
{
|
|
1064
1494
|
label: messages["availability.exceptionFrom"],
|
|
@@ -1066,7 +1496,7 @@ function AvailabilityExceptionsEditor({ resourceId, timezone }) {
|
|
|
1066
1496
|
onChange: setFrom
|
|
1067
1497
|
}
|
|
1068
1498
|
),
|
|
1069
|
-
/* @__PURE__ */
|
|
1499
|
+
/* @__PURE__ */ jsx11(
|
|
1070
1500
|
DateTimeField,
|
|
1071
1501
|
{
|
|
1072
1502
|
label: messages["availability.exceptionUntil"],
|
|
@@ -1074,7 +1504,7 @@ function AvailabilityExceptionsEditor({ resourceId, timezone }) {
|
|
|
1074
1504
|
onChange: setUntil
|
|
1075
1505
|
}
|
|
1076
1506
|
),
|
|
1077
|
-
/* @__PURE__ */
|
|
1507
|
+
/* @__PURE__ */ jsxs9(
|
|
1078
1508
|
"select",
|
|
1079
1509
|
{
|
|
1080
1510
|
"aria-label": messages["availability.exceptionKind.blocked"],
|
|
@@ -1082,12 +1512,12 @@ function AvailabilityExceptionsEditor({ resourceId, timezone }) {
|
|
|
1082
1512
|
onChange: (event) => setKind(event.target.value),
|
|
1083
1513
|
className: SELECT_CLASS,
|
|
1084
1514
|
children: [
|
|
1085
|
-
/* @__PURE__ */
|
|
1086
|
-
/* @__PURE__ */
|
|
1515
|
+
/* @__PURE__ */ jsx11("option", { value: AVAILABILITY_EXCEPTION_KIND.BLOCK, children: messages["availability.exceptionKind.blocked"] }),
|
|
1516
|
+
/* @__PURE__ */ jsx11("option", { value: AVAILABILITY_EXCEPTION_KIND.EXTRA, children: messages["availability.exceptionKind.extra"] })
|
|
1087
1517
|
]
|
|
1088
1518
|
}
|
|
1089
1519
|
),
|
|
1090
|
-
/* @__PURE__ */
|
|
1520
|
+
/* @__PURE__ */ jsx11(
|
|
1091
1521
|
"input",
|
|
1092
1522
|
{
|
|
1093
1523
|
"aria-label": messages["availability.exceptionReason"],
|
|
@@ -1098,7 +1528,7 @@ function AvailabilityExceptionsEditor({ resourceId, timezone }) {
|
|
|
1098
1528
|
className: SELECT_CLASS
|
|
1099
1529
|
}
|
|
1100
1530
|
),
|
|
1101
|
-
/* @__PURE__ */
|
|
1531
|
+
/* @__PURE__ */ jsxs9(
|
|
1102
1532
|
"button",
|
|
1103
1533
|
{
|
|
1104
1534
|
type: "button",
|
|
@@ -1106,7 +1536,7 @@ function AvailabilityExceptionsEditor({ resourceId, timezone }) {
|
|
|
1106
1536
|
disabled: addException.isPending,
|
|
1107
1537
|
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
1538
|
children: [
|
|
1109
|
-
/* @__PURE__ */
|
|
1539
|
+
/* @__PURE__ */ jsx11(Plus2, { "aria-hidden": "true", className: "w-4 h-4" }),
|
|
1110
1540
|
messages["availability.addException"]
|
|
1111
1541
|
]
|
|
1112
1542
|
}
|
|
@@ -1116,51 +1546,54 @@ function AvailabilityExceptionsEditor({ resourceId, timezone }) {
|
|
|
1116
1546
|
}
|
|
1117
1547
|
|
|
1118
1548
|
// src/components/AvailabilityEditor.tsx
|
|
1119
|
-
import { jsx as
|
|
1549
|
+
import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1120
1550
|
function AvailabilityEditor({ resourceId, timezone }) {
|
|
1121
1551
|
const { locale } = useSchedulingConfig();
|
|
1122
1552
|
const messages = resolveSchedulingMessages(locale);
|
|
1123
|
-
return /* @__PURE__ */
|
|
1124
|
-
/* @__PURE__ */
|
|
1125
|
-
/* @__PURE__ */
|
|
1553
|
+
return /* @__PURE__ */ jsxs10("div", { className: "space-y-6", children: [
|
|
1554
|
+
/* @__PURE__ */ jsxs10("p", { className: "inline-flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400", children: [
|
|
1555
|
+
/* @__PURE__ */ jsxs10("span", { className: "font-medium", children: [
|
|
1126
1556
|
messages["availability.resourceTimezone"],
|
|
1127
1557
|
":"
|
|
1128
1558
|
] }),
|
|
1129
1559
|
timezone
|
|
1130
1560
|
] }),
|
|
1131
|
-
/* @__PURE__ */
|
|
1132
|
-
/* @__PURE__ */
|
|
1561
|
+
/* @__PURE__ */ jsx12(WeeklyRulesEditor, { resourceId }),
|
|
1562
|
+
/* @__PURE__ */ jsx12(AvailabilityExceptionsEditor, { resourceId, timezone })
|
|
1133
1563
|
] });
|
|
1134
1564
|
}
|
|
1135
1565
|
|
|
1136
1566
|
// src/workspace/AvailabilityArea.tsx
|
|
1137
|
-
import { jsx as
|
|
1567
|
+
import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1138
1568
|
function AvailabilityArea() {
|
|
1139
1569
|
const { locale } = useSchedulingConfig();
|
|
1140
1570
|
const messages = resolveSchedulingMessages(locale);
|
|
1141
1571
|
const { data, isLoading, isError } = useResources({ active: true, pageSize: MAX_PAGE_SIZE2 });
|
|
1142
|
-
const [resourceId, setResourceId] =
|
|
1572
|
+
const [resourceId, setResourceId] = useState5("");
|
|
1143
1573
|
const resources = data?.data ?? [];
|
|
1144
1574
|
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
|
-
|
|
1575
|
+
return /* @__PURE__ */ jsxs11("div", { className: "flex flex-1 min-h-0 min-w-0 flex-col gap-4 overflow-y-auto p-4", children: [
|
|
1576
|
+
/* @__PURE__ */ jsx13(
|
|
1577
|
+
ResourceSelect,
|
|
1578
|
+
{
|
|
1579
|
+
label: messages["agenda.resourceLabel"],
|
|
1580
|
+
emptyOptionLabel: messages["availability.selectResourceTitle"],
|
|
1581
|
+
resources,
|
|
1582
|
+
value: resourceId,
|
|
1583
|
+
onChange: setResourceId
|
|
1584
|
+
}
|
|
1585
|
+
),
|
|
1586
|
+
isError && /* @__PURE__ */ jsx13(ErrorBanner, { message: messages["common.loadFailure"] }),
|
|
1587
|
+
isLoading && /* @__PURE__ */ jsx13(ListSkeleton, { label: messages["common.loading"], rows: 3 }),
|
|
1588
|
+
!isLoading && !isError && !selectedResource && /* @__PURE__ */ jsx13(
|
|
1589
|
+
EmptyState,
|
|
1590
|
+
{
|
|
1591
|
+
icon: Clock,
|
|
1592
|
+
title: messages["availability.selectResourceTitle"],
|
|
1593
|
+
hint: messages["availability.selectResourceHint"]
|
|
1594
|
+
}
|
|
1595
|
+
),
|
|
1596
|
+
selectedResource && /* @__PURE__ */ jsx13(
|
|
1164
1597
|
AvailabilityEditor,
|
|
1165
1598
|
{
|
|
1166
1599
|
resourceId: selectedResource.id,
|
|
@@ -1172,194 +1605,24 @@ function AvailabilityArea() {
|
|
|
1172
1605
|
}
|
|
1173
1606
|
|
|
1174
1607
|
// src/workspace/BookingsArea.tsx
|
|
1608
|
+
import { ClipboardList } from "lucide-react";
|
|
1175
1609
|
import { useState as useState8 } from "react";
|
|
1176
1610
|
import { MAX_PAGE_SIZE as MAX_PAGE_SIZE3 } from "@adatechnology/scheduling-contracts";
|
|
1177
1611
|
|
|
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
1612
|
// src/components/BookingsTable.tsx
|
|
1350
1613
|
import { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, ArrowUpDown } from "lucide-react";
|
|
1351
1614
|
import { useState as useState6 } from "react";
|
|
1352
|
-
import { BOOKING_STATUS as
|
|
1615
|
+
import { BOOKING_STATUS as BOOKING_STATUS4 } from "@adatechnology/scheduling-contracts";
|
|
1353
1616
|
|
|
1354
1617
|
// src/components/bookingsTableState.util.ts
|
|
1355
|
-
import { BOOKING_STATUS as
|
|
1618
|
+
import { BOOKING_STATUS as BOOKING_STATUS3 } from "@adatechnology/scheduling-contracts";
|
|
1356
1619
|
var DEFAULT_BOOKINGS_TABLE_STATE = {
|
|
1357
1620
|
sortDirection: "asc",
|
|
1358
1621
|
statusFilters: [],
|
|
1359
1622
|
page: 1
|
|
1360
1623
|
};
|
|
1361
1624
|
var SORT_COLUMNS = ["title", "status", "startsAt", "endsAt"];
|
|
1362
|
-
var BOOKING_STATUSES = Object.values(
|
|
1625
|
+
var BOOKING_STATUSES = Object.values(BOOKING_STATUS3);
|
|
1363
1626
|
function isBookingSortColumn(value) {
|
|
1364
1627
|
return SORT_COLUMNS.includes(value);
|
|
1365
1628
|
}
|
|
@@ -1398,8 +1661,8 @@ function filterBookingsByStatus(bookings, statusFilters) {
|
|
|
1398
1661
|
}
|
|
1399
1662
|
|
|
1400
1663
|
// src/components/BookingsTable.tsx
|
|
1401
|
-
import { jsx as
|
|
1402
|
-
var ALL_STATUSES = Object.values(
|
|
1664
|
+
import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1665
|
+
var ALL_STATUSES = Object.values(BOOKING_STATUS4);
|
|
1403
1666
|
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
1667
|
var CHECKBOX_CELL_CLASS = "px-3 py-2";
|
|
1405
1668
|
function nextSortState(column, currentColumn, currentDirection) {
|
|
@@ -1447,53 +1710,67 @@ function BookingsTable({
|
|
|
1447
1710
|
setSelected(next);
|
|
1448
1711
|
}
|
|
1449
1712
|
function renderSortIcon(column) {
|
|
1450
|
-
if (state.sortColumn !== column) return /* @__PURE__ */
|
|
1451
|
-
return state.sortDirection === "asc" ? /* @__PURE__ */
|
|
1713
|
+
if (state.sortColumn !== column) return /* @__PURE__ */ jsx14(ArrowUpDown, { "aria-hidden": "true", className: "w-3 h-3" });
|
|
1714
|
+
return state.sortDirection === "asc" ? /* @__PURE__ */ jsx14(ArrowUp, { "aria-hidden": "true", className: "w-3 h-3" }) : /* @__PURE__ */ jsx14(ArrowDown, { "aria-hidden": "true", className: "w-3 h-3" });
|
|
1452
1715
|
}
|
|
1453
1716
|
function renderHeader(column, labelKey) {
|
|
1454
1717
|
const ariaSort = state.sortColumn !== column ? "none" : state.sortDirection === "asc" ? "ascending" : "descending";
|
|
1455
|
-
return /* @__PURE__ */
|
|
1718
|
+
return /* @__PURE__ */ jsx14("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
1719
|
messages[labelKey],
|
|
1457
1720
|
renderSortIcon(column)
|
|
1458
1721
|
] }) });
|
|
1459
1722
|
}
|
|
1460
|
-
return /* @__PURE__ */
|
|
1461
|
-
/* @__PURE__ */
|
|
1462
|
-
|
|
1463
|
-
/* @__PURE__ */
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1723
|
+
return /* @__PURE__ */ jsxs12("div", { className: "space-y-3", children: [
|
|
1724
|
+
/* @__PURE__ */ jsxs12("div", { className: "flex flex-wrap items-center gap-3", children: [
|
|
1725
|
+
/* @__PURE__ */ jsxs12("fieldset", { className: "flex flex-wrap items-center gap-2", children: [
|
|
1726
|
+
/* @__PURE__ */ jsx14("legend", { className: "sr-only", children: messages["booking.filterByStatus"] }),
|
|
1727
|
+
ALL_STATUSES.map((status) => {
|
|
1728
|
+
const isActive = state.statusFilters.includes(status);
|
|
1729
|
+
return /* @__PURE__ */ jsxs12(
|
|
1730
|
+
"label",
|
|
1731
|
+
{
|
|
1732
|
+
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"}`,
|
|
1733
|
+
children: [
|
|
1734
|
+
/* @__PURE__ */ jsx14(
|
|
1735
|
+
"input",
|
|
1736
|
+
{
|
|
1737
|
+
type: "checkbox",
|
|
1738
|
+
className: "sr-only",
|
|
1739
|
+
checked: isActive,
|
|
1740
|
+
onChange: () => toggleStatusFilter(status)
|
|
1741
|
+
}
|
|
1742
|
+
),
|
|
1743
|
+
bookingStatusLabel(messages, status)
|
|
1744
|
+
]
|
|
1745
|
+
},
|
|
1746
|
+
status
|
|
1747
|
+
);
|
|
1748
|
+
})
|
|
1749
|
+
] }),
|
|
1750
|
+
!isBookingsTableStateDefault(state) && /* @__PURE__ */ jsx14(
|
|
1474
1751
|
"button",
|
|
1475
1752
|
{
|
|
1476
1753
|
type: "button",
|
|
1477
1754
|
onClick: () => onStateChange?.(DEFAULT_BOOKINGS_TABLE_STATE),
|
|
1478
|
-
className: "min-h-11 px-3 text-sm font-medium text-brand-700 hover:underline",
|
|
1755
|
+
className: "min-h-11 px-3 text-sm font-medium text-brand-700 hover:underline dark:text-brand-300",
|
|
1479
1756
|
children: messages["common.clearFilters"]
|
|
1480
1757
|
}
|
|
1481
1758
|
)
|
|
1482
1759
|
] }),
|
|
1483
|
-
selected.size > 0 && bulkActions.length > 0 && /* @__PURE__ */
|
|
1760
|
+
selected.size > 0 && bulkActions.length > 0 && /* @__PURE__ */ jsx14("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__ */ jsx14(
|
|
1484
1761
|
"button",
|
|
1485
1762
|
{
|
|
1486
1763
|
type: "button",
|
|
1487
1764
|
onClick: () => action.onRun(Array.from(selected)),
|
|
1488
|
-
className:
|
|
1765
|
+
className: BUTTON_PRIMARY,
|
|
1489
1766
|
children: action.label
|
|
1490
1767
|
},
|
|
1491
1768
|
action.key
|
|
1492
1769
|
)) }),
|
|
1493
|
-
/* @__PURE__ */
|
|
1494
|
-
/* @__PURE__ */
|
|
1495
|
-
/* @__PURE__ */
|
|
1496
|
-
/* @__PURE__ */
|
|
1770
|
+
/* @__PURE__ */ jsxs12("div", { className: `${SURFACE_BORDER} overflow-x-auto`, children: [
|
|
1771
|
+
/* @__PURE__ */ jsxs12("table", { className: "min-w-full text-sm", children: [
|
|
1772
|
+
/* @__PURE__ */ jsx14("thead", { className: "border-b border-gray-200 bg-gray-50 dark:border-gray-800 dark:bg-gray-800/60", children: /* @__PURE__ */ jsxs12("tr", { children: [
|
|
1773
|
+
/* @__PURE__ */ jsx14("th", { scope: "col", className: CHECKBOX_CELL_CLASS, children: /* @__PURE__ */ jsx14(
|
|
1497
1774
|
"input",
|
|
1498
1775
|
{
|
|
1499
1776
|
type: "checkbox",
|
|
@@ -1507,12 +1784,12 @@ function BookingsTable({
|
|
|
1507
1784
|
renderHeader("startsAt", "booking.column.startsAt"),
|
|
1508
1785
|
renderHeader("endsAt", "booking.column.endsAt")
|
|
1509
1786
|
] }) }),
|
|
1510
|
-
/* @__PURE__ */
|
|
1787
|
+
/* @__PURE__ */ jsx14("tbody", { children: visibleBookings.map((booking, index) => /* @__PURE__ */ jsxs12(
|
|
1511
1788
|
"tr",
|
|
1512
1789
|
{
|
|
1513
|
-
className: index % 2 === 1 ? "bg-
|
|
1790
|
+
className: `${index % 2 === 1 ? ROW_STRIPE : ""} hover:bg-brand-50/50 dark:hover:bg-brand-900/10`,
|
|
1514
1791
|
children: [
|
|
1515
|
-
/* @__PURE__ */
|
|
1792
|
+
/* @__PURE__ */ jsx14("td", { className: CHECKBOX_CELL_CLASS, children: /* @__PURE__ */ jsx14(
|
|
1516
1793
|
"input",
|
|
1517
1794
|
{
|
|
1518
1795
|
type: "checkbox",
|
|
@@ -1521,43 +1798,43 @@ function BookingsTable({
|
|
|
1521
1798
|
onChange: () => toggleRowSelected(booking.id)
|
|
1522
1799
|
}
|
|
1523
1800
|
) }),
|
|
1524
|
-
/* @__PURE__ */
|
|
1525
|
-
/* @__PURE__ */
|
|
1526
|
-
/* @__PURE__ */
|
|
1527
|
-
/* @__PURE__ */
|
|
1801
|
+
/* @__PURE__ */ jsx14("td", { className: "px-3 py-2", children: /* @__PURE__ */ jsx14("button", { type: "button", onClick: () => onRowClick?.(booking), className: "min-h-11 text-left hover:underline", children: booking.title }) }),
|
|
1802
|
+
/* @__PURE__ */ jsx14("td", { className: "px-3 py-2", children: /* @__PURE__ */ jsx14(BookingStatusBadge, { status: booking.status }) }),
|
|
1803
|
+
/* @__PURE__ */ jsx14("td", { className: "px-3 py-2 tabular-nums whitespace-nowrap", children: booking.startsAt.toLocaleString(locale) }),
|
|
1804
|
+
/* @__PURE__ */ jsx14("td", { className: "px-3 py-2 tabular-nums whitespace-nowrap", children: booking.endsAt.toLocaleString(locale) })
|
|
1528
1805
|
]
|
|
1529
1806
|
},
|
|
1530
1807
|
booking.id
|
|
1531
1808
|
)) })
|
|
1532
1809
|
] }),
|
|
1533
|
-
visibleBookings.length === 0 && /* @__PURE__ */
|
|
1810
|
+
visibleBookings.length === 0 && /* @__PURE__ */ jsx14("p", { className: "px-3 py-8 text-center text-sm text-gray-500 dark:text-gray-400", children: messages["common.empty"] })
|
|
1534
1811
|
] }),
|
|
1535
|
-
pagination && pagination.totalPages > 1 && /* @__PURE__ */
|
|
1536
|
-
/* @__PURE__ */
|
|
1812
|
+
pagination && pagination.totalPages > 1 && /* @__PURE__ */ jsxs12("div", { className: "flex items-center justify-end gap-3", children: [
|
|
1813
|
+
/* @__PURE__ */ jsx14(
|
|
1537
1814
|
"button",
|
|
1538
1815
|
{
|
|
1539
1816
|
type: "button",
|
|
1540
1817
|
"aria-label": messages["common.previousPage"],
|
|
1541
1818
|
disabled: state.page <= 1,
|
|
1542
1819
|
onClick: () => goToPage(state.page - 1),
|
|
1543
|
-
className:
|
|
1544
|
-
children: /* @__PURE__ */
|
|
1820
|
+
className: ICON_BUTTON,
|
|
1821
|
+
children: /* @__PURE__ */ jsx14(ArrowLeft, { "aria-hidden": "true", className: "w-4 h-4" })
|
|
1545
1822
|
}
|
|
1546
1823
|
),
|
|
1547
|
-
/* @__PURE__ */
|
|
1824
|
+
/* @__PURE__ */ jsxs12("span", { className: "text-sm tabular-nums text-gray-500 dark:text-gray-400", children: [
|
|
1548
1825
|
state.page,
|
|
1549
1826
|
" / ",
|
|
1550
1827
|
pagination.totalPages
|
|
1551
1828
|
] }),
|
|
1552
|
-
/* @__PURE__ */
|
|
1829
|
+
/* @__PURE__ */ jsx14(
|
|
1553
1830
|
"button",
|
|
1554
1831
|
{
|
|
1555
1832
|
type: "button",
|
|
1556
1833
|
"aria-label": messages["common.nextPage"],
|
|
1557
1834
|
disabled: state.page >= pagination.totalPages,
|
|
1558
1835
|
onClick: () => goToPage(state.page + 1),
|
|
1559
|
-
className:
|
|
1560
|
-
children: /* @__PURE__ */
|
|
1836
|
+
className: ICON_BUTTON,
|
|
1837
|
+
children: /* @__PURE__ */ jsx14(ArrowRight, { "aria-hidden": "true", className: "w-4 h-4" })
|
|
1561
1838
|
}
|
|
1562
1839
|
)
|
|
1563
1840
|
] })
|
|
@@ -1565,7 +1842,7 @@ function BookingsTable({
|
|
|
1565
1842
|
}
|
|
1566
1843
|
|
|
1567
1844
|
// src/hooks/useBookingsTableState.hook.ts
|
|
1568
|
-
import { useEffect as
|
|
1845
|
+
import { useEffect as useEffect3, useState as useState7 } from "react";
|
|
1569
1846
|
var OWNED_PARAM_KEYS = ["sortBy", "sortDirection", "status", "page"];
|
|
1570
1847
|
function readInitialState() {
|
|
1571
1848
|
if (typeof window === "undefined") return DEFAULT_BOOKINGS_TABLE_STATE;
|
|
@@ -1573,7 +1850,7 @@ function readInitialState() {
|
|
|
1573
1850
|
}
|
|
1574
1851
|
function useBookingsTableState() {
|
|
1575
1852
|
const [state, setState] = useState7(readInitialState);
|
|
1576
|
-
|
|
1853
|
+
useEffect3(() => {
|
|
1577
1854
|
if (typeof window === "undefined") return;
|
|
1578
1855
|
const params = new URLSearchParams(window.location.search);
|
|
1579
1856
|
for (const key of OWNED_PARAM_KEYS) params.delete(key);
|
|
@@ -1586,7 +1863,7 @@ function useBookingsTableState() {
|
|
|
1586
1863
|
}
|
|
1587
1864
|
|
|
1588
1865
|
// src/workspace/BookingsArea.tsx
|
|
1589
|
-
import { jsx as
|
|
1866
|
+
import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
1590
1867
|
var PAGE_SIZE = 20;
|
|
1591
1868
|
function BookingsArea() {
|
|
1592
1869
|
const { locale } = useSchedulingConfig();
|
|
@@ -1610,21 +1887,32 @@ function BookingsArea() {
|
|
|
1610
1887
|
function bulkConfirm(ids) {
|
|
1611
1888
|
for (const id of ids) confirmBooking.mutate(id);
|
|
1612
1889
|
}
|
|
1613
|
-
return /* @__PURE__ */
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1890
|
+
return /* @__PURE__ */ jsxs13("div", { className: "flex flex-1 min-h-0 min-w-0", children: [
|
|
1891
|
+
/* @__PURE__ */ jsxs13("div", { className: "flex flex-1 min-h-0 min-w-0 flex-col gap-4 overflow-y-auto p-4", children: [
|
|
1892
|
+
isError && /* @__PURE__ */ jsx15(ErrorBanner, { message: messages["common.loadFailure"] }),
|
|
1893
|
+
confirmBooking.isError && /* @__PURE__ */ jsx15(ErrorBanner, { message: messages["common.actionFailure"] }),
|
|
1894
|
+
isLoading && /* @__PURE__ */ jsx15(ListSkeleton, { label: messages["common.loading"], rows: 8 }),
|
|
1895
|
+
!isLoading && !isError && (data?.data.length ?? 0) === 0 && tableState.statusFilters.length === 0 && /* @__PURE__ */ jsx15(
|
|
1896
|
+
EmptyState,
|
|
1897
|
+
{
|
|
1898
|
+
icon: ClipboardList,
|
|
1899
|
+
title: messages["booking.emptyTitle"],
|
|
1900
|
+
hint: messages["booking.emptyHint"]
|
|
1901
|
+
}
|
|
1902
|
+
),
|
|
1903
|
+
!isLoading && ((data?.data.length ?? 0) > 0 || tableState.statusFilters.length > 0) && /* @__PURE__ */ jsx15(
|
|
1904
|
+
BookingsTable,
|
|
1905
|
+
{
|
|
1906
|
+
bookings: data?.data ?? [],
|
|
1907
|
+
state: tableState,
|
|
1908
|
+
onStateChange: setTableState,
|
|
1909
|
+
pagination: data ? { totalPages: data.totalPages } : void 0,
|
|
1910
|
+
onRowClick: (booking) => setSelectedBookingId(booking.id),
|
|
1911
|
+
bulkActions: [{ key: "confirm", label: messages["booking.confirm"], onRun: bulkConfirm }]
|
|
1912
|
+
}
|
|
1913
|
+
)
|
|
1914
|
+
] }),
|
|
1915
|
+
selectedBooking && /* @__PURE__ */ jsx15(
|
|
1628
1916
|
BookingDrawer,
|
|
1629
1917
|
{
|
|
1630
1918
|
booking: selectedBooking,
|
|
@@ -1637,17 +1925,17 @@ function BookingsArea() {
|
|
|
1637
1925
|
}
|
|
1638
1926
|
|
|
1639
1927
|
// src/workspace/ResourcesArea.tsx
|
|
1640
|
-
import { Plus as Plus3, Trash2 as Trash23 } from "lucide-react";
|
|
1928
|
+
import { Plus as Plus3, Trash2 as Trash23, Users } from "lucide-react";
|
|
1641
1929
|
import { useState as useState10 } from "react";
|
|
1642
1930
|
import { MAX_PAGE_SIZE as MAX_PAGE_SIZE4 } from "@adatechnology/scheduling-contracts";
|
|
1643
1931
|
|
|
1644
1932
|
// src/components/ResourceForm.tsx
|
|
1645
1933
|
import { useId as useId2, useState as useState9 } from "react";
|
|
1646
1934
|
import { RESOURCE_KIND } from "@adatechnology/scheduling-contracts";
|
|
1647
|
-
import { jsx as
|
|
1935
|
+
import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
1648
1936
|
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
1937
|
var LABEL_CLASS = "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1";
|
|
1650
|
-
var
|
|
1938
|
+
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";
|
|
1651
1939
|
function ResourceForm({ initialValues, onSubmit }) {
|
|
1652
1940
|
const { locale } = useSchedulingConfig();
|
|
1653
1941
|
const messages = resolveSchedulingMessages(locale);
|
|
@@ -1669,10 +1957,10 @@ function ResourceForm({ initialValues, onSubmit }) {
|
|
|
1669
1957
|
setSubmitting(false);
|
|
1670
1958
|
}
|
|
1671
1959
|
}
|
|
1672
|
-
return /* @__PURE__ */
|
|
1673
|
-
/* @__PURE__ */
|
|
1674
|
-
/* @__PURE__ */
|
|
1675
|
-
/* @__PURE__ */
|
|
1960
|
+
return /* @__PURE__ */ jsxs14("form", { onSubmit: handleSubmit, className: "space-y-4", children: [
|
|
1961
|
+
/* @__PURE__ */ jsxs14("div", { children: [
|
|
1962
|
+
/* @__PURE__ */ jsx16("label", { htmlFor: nameId, className: LABEL_CLASS, children: messages["resource.name"] }),
|
|
1963
|
+
/* @__PURE__ */ jsx16(
|
|
1676
1964
|
"input",
|
|
1677
1965
|
{
|
|
1678
1966
|
id: nameId,
|
|
@@ -1684,9 +1972,9 @@ function ResourceForm({ initialValues, onSubmit }) {
|
|
|
1684
1972
|
}
|
|
1685
1973
|
)
|
|
1686
1974
|
] }),
|
|
1687
|
-
/* @__PURE__ */
|
|
1688
|
-
/* @__PURE__ */
|
|
1689
|
-
/* @__PURE__ */
|
|
1975
|
+
/* @__PURE__ */ jsxs14("div", { children: [
|
|
1976
|
+
/* @__PURE__ */ jsx16("label", { htmlFor: kindId, className: LABEL_CLASS, children: messages["resource.kind"] }),
|
|
1977
|
+
/* @__PURE__ */ jsxs14(
|
|
1690
1978
|
"select",
|
|
1691
1979
|
{
|
|
1692
1980
|
id: kindId,
|
|
@@ -1694,16 +1982,16 @@ function ResourceForm({ initialValues, onSubmit }) {
|
|
|
1694
1982
|
onChange: (event) => setKind(event.target.value),
|
|
1695
1983
|
className: INPUT_CLASS2,
|
|
1696
1984
|
children: [
|
|
1697
|
-
/* @__PURE__ */
|
|
1698
|
-
/* @__PURE__ */
|
|
1699
|
-
/* @__PURE__ */
|
|
1985
|
+
/* @__PURE__ */ jsx16("option", { value: RESOURCE_KIND.PERSON, children: messages["resource.kind.person"] }),
|
|
1986
|
+
/* @__PURE__ */ jsx16("option", { value: RESOURCE_KIND.ROOM, children: messages["resource.kind.room"] }),
|
|
1987
|
+
/* @__PURE__ */ jsx16("option", { value: RESOURCE_KIND.EQUIPMENT, children: messages["resource.kind.equipment"] })
|
|
1700
1988
|
]
|
|
1701
1989
|
}
|
|
1702
1990
|
)
|
|
1703
1991
|
] }),
|
|
1704
|
-
/* @__PURE__ */
|
|
1705
|
-
/* @__PURE__ */
|
|
1706
|
-
/* @__PURE__ */
|
|
1992
|
+
/* @__PURE__ */ jsxs14("div", { children: [
|
|
1993
|
+
/* @__PURE__ */ jsx16("label", { htmlFor: timezoneId, className: LABEL_CLASS, children: messages["resource.timezone"] }),
|
|
1994
|
+
/* @__PURE__ */ jsx16(
|
|
1707
1995
|
"input",
|
|
1708
1996
|
{
|
|
1709
1997
|
id: timezoneId,
|
|
@@ -1716,43 +2004,40 @@ function ResourceForm({ initialValues, onSubmit }) {
|
|
|
1716
2004
|
}
|
|
1717
2005
|
)
|
|
1718
2006
|
] }),
|
|
1719
|
-
initialValues && /* @__PURE__ */
|
|
1720
|
-
/* @__PURE__ */
|
|
2007
|
+
initialValues && /* @__PURE__ */ jsxs14("label", { className: "flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 min-h-11", children: [
|
|
2008
|
+
/* @__PURE__ */ jsx16("input", { type: "checkbox", checked: active, onChange: (event) => setActive(event.target.checked) }),
|
|
1721
2009
|
messages["resource.active"]
|
|
1722
2010
|
] }),
|
|
1723
|
-
/* @__PURE__ */
|
|
2011
|
+
/* @__PURE__ */ jsx16("button", { type: "submit", disabled: submitting, className: BUTTON_PRIMARY2, children: messages["common.save"] })
|
|
1724
2012
|
] });
|
|
1725
2013
|
}
|
|
1726
2014
|
|
|
1727
2015
|
// src/components/ResourceList.tsx
|
|
1728
|
-
import { jsx as
|
|
2016
|
+
import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
1729
2017
|
function ResourceList({ resources, onSelect }) {
|
|
1730
2018
|
const { locale } = useSchedulingConfig();
|
|
1731
2019
|
const messages = resolveSchedulingMessages(locale);
|
|
1732
2020
|
if (resources.length === 0) {
|
|
1733
|
-
return /* @__PURE__ */
|
|
2021
|
+
return /* @__PURE__ */ jsx17("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: messages["common.empty"] });
|
|
1734
2022
|
}
|
|
1735
|
-
return /* @__PURE__ */
|
|
2023
|
+
return /* @__PURE__ */ jsx17("ul", { className: `${SURFACE_BORDER} divide-y divide-gray-200 overflow-hidden dark:divide-gray-800`, children: resources.map((resource, index) => /* @__PURE__ */ jsx17("li", { className: index % 2 === 1 ? ROW_STRIPE : void 0, children: /* @__PURE__ */ jsxs15(
|
|
1736
2024
|
"button",
|
|
1737
2025
|
{
|
|
1738
2026
|
type: "button",
|
|
1739
2027
|
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-
|
|
2028
|
+
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
2029
|
children: [
|
|
1742
|
-
/* @__PURE__ */
|
|
1743
|
-
/* @__PURE__ */
|
|
1744
|
-
/* @__PURE__ */
|
|
1745
|
-
!resource.active && /* @__PURE__ */
|
|
2030
|
+
/* @__PURE__ */ jsx17("span", { className: "flex-1 font-medium text-gray-900 dark:text-gray-100", children: resource.name }),
|
|
2031
|
+
/* @__PURE__ */ jsx17("span", { className: "text-gray-500 dark:text-gray-400", children: messages[`resource.kind.${resource.kind}`] }),
|
|
2032
|
+
/* @__PURE__ */ jsx17("span", { className: "text-gray-500 dark:text-gray-400", children: resource.timezone }),
|
|
2033
|
+
!resource.active && /* @__PURE__ */ jsx17("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
2034
|
]
|
|
1747
2035
|
}
|
|
1748
2036
|
) }, resource.id)) });
|
|
1749
2037
|
}
|
|
1750
2038
|
|
|
1751
2039
|
// 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`;
|
|
2040
|
+
import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
1756
2041
|
function ResourcesArea() {
|
|
1757
2042
|
const { locale } = useSchedulingConfig();
|
|
1758
2043
|
const messages = resolveSchedulingMessages(locale);
|
|
@@ -1763,6 +2048,7 @@ function ResourcesArea() {
|
|
|
1763
2048
|
const [draft, setDraft] = useState10(void 0);
|
|
1764
2049
|
const isDraftOpen = draft !== void 0;
|
|
1765
2050
|
const isEditing = Boolean(draft);
|
|
2051
|
+
const resources = data?.data ?? [];
|
|
1766
2052
|
async function handleSubmit(input) {
|
|
1767
2053
|
try {
|
|
1768
2054
|
if (draft) {
|
|
@@ -1774,21 +2060,36 @@ function ResourcesArea() {
|
|
|
1774
2060
|
} catch {
|
|
1775
2061
|
}
|
|
1776
2062
|
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
/* @__PURE__ */
|
|
2063
|
+
function renderCreateButton() {
|
|
2064
|
+
return /* @__PURE__ */ jsxs16("button", { type: "button", onClick: () => setDraft(null), className: BUTTON_PRIMARY, children: [
|
|
2065
|
+
/* @__PURE__ */ jsx18(Plus3, { "aria-hidden": "true", className: "h-4 w-4" }),
|
|
1780
2066
|
messages["resource.newResource"]
|
|
1781
|
-
] })
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
2067
|
+
] });
|
|
2068
|
+
}
|
|
2069
|
+
return /* @__PURE__ */ jsxs16("div", { className: "flex flex-1 min-h-0 min-w-0", children: [
|
|
2070
|
+
/* @__PURE__ */ jsxs16("div", { className: "flex flex-1 min-h-0 min-w-0 flex-col gap-4 overflow-y-auto p-4", children: [
|
|
2071
|
+
/* @__PURE__ */ jsx18("div", { className: "flex justify-end", children: renderCreateButton() }),
|
|
2072
|
+
isError && /* @__PURE__ */ jsx18(ErrorBanner, { message: messages["common.loadFailure"] }),
|
|
2073
|
+
(createResource.isError || updateResource.isError || deleteResource.isError) && /* @__PURE__ */ jsx18(ErrorBanner, { message: messages["common.actionFailure"] }),
|
|
2074
|
+
isLoading && /* @__PURE__ */ jsx18(ListSkeleton, { label: messages["common.loading"] }),
|
|
2075
|
+
!isLoading && !isError && resources.length === 0 && /* @__PURE__ */ jsx18(
|
|
2076
|
+
EmptyState,
|
|
2077
|
+
{
|
|
2078
|
+
icon: Users,
|
|
2079
|
+
title: messages["resource.emptyTitle"],
|
|
2080
|
+
hint: messages["resource.emptyHint"],
|
|
2081
|
+
action: renderCreateButton()
|
|
2082
|
+
}
|
|
2083
|
+
),
|
|
2084
|
+
!isLoading && resources.length > 0 && /* @__PURE__ */ jsx18(ResourceList, { resources, onSelect: setDraft })
|
|
2085
|
+
] }),
|
|
2086
|
+
isDraftOpen && /* @__PURE__ */ jsx18(
|
|
1786
2087
|
SidePanel,
|
|
1787
2088
|
{
|
|
1788
2089
|
title: isEditing ? messages["resource.editTitle"] : messages["resource.createTitle"],
|
|
1789
2090
|
closeLabel: messages["common.close"],
|
|
1790
2091
|
onClose: () => setDraft(void 0),
|
|
1791
|
-
headerActions: isEditing ? /* @__PURE__ */
|
|
2092
|
+
headerActions: isEditing ? /* @__PURE__ */ jsxs16(
|
|
1792
2093
|
"button",
|
|
1793
2094
|
{
|
|
1794
2095
|
type: "button",
|
|
@@ -1798,12 +2099,12 @@ function ResourcesArea() {
|
|
|
1798
2099
|
},
|
|
1799
2100
|
className: BUTTON_DANGER,
|
|
1800
2101
|
children: [
|
|
1801
|
-
/* @__PURE__ */
|
|
2102
|
+
/* @__PURE__ */ jsx18(Trash23, { "aria-hidden": "true", className: "h-4 w-4" }),
|
|
1802
2103
|
messages["common.remove"]
|
|
1803
2104
|
]
|
|
1804
2105
|
}
|
|
1805
2106
|
) : void 0,
|
|
1806
|
-
children: /* @__PURE__ */
|
|
2107
|
+
children: /* @__PURE__ */ jsx18(
|
|
1807
2108
|
ResourceForm,
|
|
1808
2109
|
{
|
|
1809
2110
|
...draft ? { initialValues: draft } : {},
|
|
@@ -1817,13 +2118,13 @@ function ResourcesArea() {
|
|
|
1817
2118
|
}
|
|
1818
2119
|
|
|
1819
2120
|
// src/workspace/ServicesArea.tsx
|
|
1820
|
-
import { Plus as Plus4, Trash2 as Trash24 } from "lucide-react";
|
|
2121
|
+
import { Plus as Plus4, Trash2 as Trash24, Wrench } from "lucide-react";
|
|
1821
2122
|
import { useState as useState12 } from "react";
|
|
1822
2123
|
import { MAX_PAGE_SIZE as MAX_PAGE_SIZE5 } from "@adatechnology/scheduling-contracts";
|
|
1823
2124
|
|
|
1824
2125
|
// src/components/ServiceForm.tsx
|
|
1825
2126
|
import { useState as useState11 } from "react";
|
|
1826
|
-
import { jsx as
|
|
2127
|
+
import { jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
1827
2128
|
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
2129
|
var LABEL_CLASS2 = "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1";
|
|
1829
2130
|
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";
|
|
@@ -1845,10 +2146,10 @@ function ServiceForm({ initialValues, onSubmit }) {
|
|
|
1845
2146
|
setSubmitting(false);
|
|
1846
2147
|
}
|
|
1847
2148
|
}
|
|
1848
|
-
return /* @__PURE__ */
|
|
1849
|
-
/* @__PURE__ */
|
|
1850
|
-
/* @__PURE__ */
|
|
1851
|
-
/* @__PURE__ */
|
|
2149
|
+
return /* @__PURE__ */ jsxs17("form", { onSubmit: handleSubmit, className: "space-y-4", children: [
|
|
2150
|
+
/* @__PURE__ */ jsxs17("div", { children: [
|
|
2151
|
+
/* @__PURE__ */ jsx19("label", { htmlFor: "scheduling-service-name", className: LABEL_CLASS2, children: messages["service.name"] }),
|
|
2152
|
+
/* @__PURE__ */ jsx19(
|
|
1852
2153
|
"input",
|
|
1853
2154
|
{
|
|
1854
2155
|
id: "scheduling-service-name",
|
|
@@ -1860,9 +2161,9 @@ function ServiceForm({ initialValues, onSubmit }) {
|
|
|
1860
2161
|
}
|
|
1861
2162
|
)
|
|
1862
2163
|
] }),
|
|
1863
|
-
/* @__PURE__ */
|
|
1864
|
-
/* @__PURE__ */
|
|
1865
|
-
/* @__PURE__ */
|
|
2164
|
+
/* @__PURE__ */ jsxs17("div", { children: [
|
|
2165
|
+
/* @__PURE__ */ jsx19("label", { htmlFor: "scheduling-service-duration", className: LABEL_CLASS2, children: messages["service.durationMinutes"] }),
|
|
2166
|
+
/* @__PURE__ */ jsx19(
|
|
1866
2167
|
"input",
|
|
1867
2168
|
{
|
|
1868
2169
|
id: "scheduling-service-duration",
|
|
@@ -1875,10 +2176,10 @@ function ServiceForm({ initialValues, onSubmit }) {
|
|
|
1875
2176
|
}
|
|
1876
2177
|
)
|
|
1877
2178
|
] }),
|
|
1878
|
-
/* @__PURE__ */
|
|
1879
|
-
/* @__PURE__ */
|
|
1880
|
-
/* @__PURE__ */
|
|
1881
|
-
/* @__PURE__ */
|
|
2179
|
+
/* @__PURE__ */ jsxs17("div", { className: "grid grid-cols-2 gap-3", children: [
|
|
2180
|
+
/* @__PURE__ */ jsxs17("div", { children: [
|
|
2181
|
+
/* @__PURE__ */ jsx19("label", { htmlFor: "scheduling-service-buffer-before", className: LABEL_CLASS2, children: messages["service.bufferBeforeMinutes"] }),
|
|
2182
|
+
/* @__PURE__ */ jsx19(
|
|
1882
2183
|
"input",
|
|
1883
2184
|
{
|
|
1884
2185
|
id: "scheduling-service-buffer-before",
|
|
@@ -1890,9 +2191,9 @@ function ServiceForm({ initialValues, onSubmit }) {
|
|
|
1890
2191
|
}
|
|
1891
2192
|
)
|
|
1892
2193
|
] }),
|
|
1893
|
-
/* @__PURE__ */
|
|
1894
|
-
/* @__PURE__ */
|
|
1895
|
-
/* @__PURE__ */
|
|
2194
|
+
/* @__PURE__ */ jsxs17("div", { children: [
|
|
2195
|
+
/* @__PURE__ */ jsx19("label", { htmlFor: "scheduling-service-buffer-after", className: LABEL_CLASS2, children: messages["service.bufferAfterMinutes"] }),
|
|
2196
|
+
/* @__PURE__ */ jsx19(
|
|
1896
2197
|
"input",
|
|
1897
2198
|
{
|
|
1898
2199
|
id: "scheduling-service-buffer-after",
|
|
@@ -1905,45 +2206,42 @@ function ServiceForm({ initialValues, onSubmit }) {
|
|
|
1905
2206
|
)
|
|
1906
2207
|
] })
|
|
1907
2208
|
] }),
|
|
1908
|
-
initialValues && /* @__PURE__ */
|
|
1909
|
-
/* @__PURE__ */
|
|
2209
|
+
initialValues && /* @__PURE__ */ jsxs17("label", { className: "flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 min-h-11", children: [
|
|
2210
|
+
/* @__PURE__ */ jsx19("input", { type: "checkbox", checked: active, onChange: (event) => setActive(event.target.checked) }),
|
|
1910
2211
|
messages["service.active"]
|
|
1911
2212
|
] }),
|
|
1912
|
-
/* @__PURE__ */
|
|
2213
|
+
/* @__PURE__ */ jsx19("button", { type: "submit", disabled: submitting, className: BUTTON_PRIMARY3, children: messages["common.save"] })
|
|
1913
2214
|
] });
|
|
1914
2215
|
}
|
|
1915
2216
|
|
|
1916
2217
|
// src/components/ServiceList.tsx
|
|
1917
|
-
import { jsx as
|
|
2218
|
+
import { jsx as jsx20, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
1918
2219
|
function ServiceList({ services, onSelect }) {
|
|
1919
2220
|
const { locale } = useSchedulingConfig();
|
|
1920
2221
|
const messages = resolveSchedulingMessages(locale);
|
|
1921
2222
|
if (services.length === 0) {
|
|
1922
|
-
return /* @__PURE__ */
|
|
2223
|
+
return /* @__PURE__ */ jsx20("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: messages["common.empty"] });
|
|
1923
2224
|
}
|
|
1924
|
-
return /* @__PURE__ */
|
|
2225
|
+
return /* @__PURE__ */ jsx20("ul", { className: `${SURFACE_BORDER} divide-y divide-gray-200 overflow-hidden dark:divide-gray-800`, children: services.map((service, index) => /* @__PURE__ */ jsx20("li", { className: index % 2 === 1 ? ROW_STRIPE : void 0, children: /* @__PURE__ */ jsxs18(
|
|
1925
2226
|
"button",
|
|
1926
2227
|
{
|
|
1927
2228
|
type: "button",
|
|
1928
2229
|
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-
|
|
2230
|
+
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
2231
|
children: [
|
|
1931
|
-
/* @__PURE__ */
|
|
1932
|
-
/* @__PURE__ */
|
|
2232
|
+
/* @__PURE__ */ jsx20("span", { className: "flex-1 font-medium text-gray-900 dark:text-gray-100", children: service.name }),
|
|
2233
|
+
/* @__PURE__ */ jsxs18("span", { className: "text-gray-500 dark:text-gray-400", children: [
|
|
1933
2234
|
service.durationMinutes,
|
|
1934
2235
|
" min"
|
|
1935
2236
|
] }),
|
|
1936
|
-
!service.active && /* @__PURE__ */
|
|
2237
|
+
!service.active && /* @__PURE__ */ jsx20("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
2238
|
]
|
|
1938
2239
|
}
|
|
1939
2240
|
) }, service.id)) });
|
|
1940
2241
|
}
|
|
1941
2242
|
|
|
1942
2243
|
// 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`;
|
|
2244
|
+
import { jsx as jsx21, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
1947
2245
|
function ServicesArea() {
|
|
1948
2246
|
const { locale } = useSchedulingConfig();
|
|
1949
2247
|
const messages = resolveSchedulingMessages(locale);
|
|
@@ -1954,6 +2252,7 @@ function ServicesArea() {
|
|
|
1954
2252
|
const [draft, setDraft] = useState12(void 0);
|
|
1955
2253
|
const isDraftOpen = draft !== void 0;
|
|
1956
2254
|
const isEditing = Boolean(draft);
|
|
2255
|
+
const services = data?.data ?? [];
|
|
1957
2256
|
async function handleSubmit(input) {
|
|
1958
2257
|
try {
|
|
1959
2258
|
if (draft) {
|
|
@@ -1965,21 +2264,36 @@ function ServicesArea() {
|
|
|
1965
2264
|
} catch {
|
|
1966
2265
|
}
|
|
1967
2266
|
}
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
/* @__PURE__ */
|
|
2267
|
+
function renderCreateButton() {
|
|
2268
|
+
return /* @__PURE__ */ jsxs19("button", { type: "button", onClick: () => setDraft(null), className: BUTTON_PRIMARY, children: [
|
|
2269
|
+
/* @__PURE__ */ jsx21(Plus4, { "aria-hidden": "true", className: "h-4 w-4" }),
|
|
1971
2270
|
messages["service.newService"]
|
|
1972
|
-
] })
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2271
|
+
] });
|
|
2272
|
+
}
|
|
2273
|
+
return /* @__PURE__ */ jsxs19("div", { className: "flex flex-1 min-h-0 min-w-0", children: [
|
|
2274
|
+
/* @__PURE__ */ jsxs19("div", { className: "flex flex-1 min-h-0 min-w-0 flex-col gap-4 overflow-y-auto p-4", children: [
|
|
2275
|
+
/* @__PURE__ */ jsx21("div", { className: "flex justify-end", children: renderCreateButton() }),
|
|
2276
|
+
isError && /* @__PURE__ */ jsx21(ErrorBanner, { message: messages["common.loadFailure"] }),
|
|
2277
|
+
(createService.isError || updateService.isError || deleteService.isError) && /* @__PURE__ */ jsx21(ErrorBanner, { message: messages["common.actionFailure"] }),
|
|
2278
|
+
isLoading && /* @__PURE__ */ jsx21(ListSkeleton, { label: messages["common.loading"] }),
|
|
2279
|
+
!isLoading && !isError && services.length === 0 && /* @__PURE__ */ jsx21(
|
|
2280
|
+
EmptyState,
|
|
2281
|
+
{
|
|
2282
|
+
icon: Wrench,
|
|
2283
|
+
title: messages["service.emptyTitle"],
|
|
2284
|
+
hint: messages["service.emptyHint"],
|
|
2285
|
+
action: renderCreateButton()
|
|
2286
|
+
}
|
|
2287
|
+
),
|
|
2288
|
+
!isLoading && services.length > 0 && /* @__PURE__ */ jsx21(ServiceList, { services, onSelect: setDraft })
|
|
2289
|
+
] }),
|
|
2290
|
+
isDraftOpen && /* @__PURE__ */ jsx21(
|
|
1977
2291
|
SidePanel,
|
|
1978
2292
|
{
|
|
1979
2293
|
title: isEditing ? messages["service.editTitle"] : messages["service.createTitle"],
|
|
1980
2294
|
closeLabel: messages["common.close"],
|
|
1981
2295
|
onClose: () => setDraft(void 0),
|
|
1982
|
-
headerActions: isEditing ? /* @__PURE__ */
|
|
2296
|
+
headerActions: isEditing ? /* @__PURE__ */ jsxs19(
|
|
1983
2297
|
"button",
|
|
1984
2298
|
{
|
|
1985
2299
|
type: "button",
|
|
@@ -1987,14 +2301,14 @@ function ServicesArea() {
|
|
|
1987
2301
|
if (draft) void deleteService.mutateAsync(draft.id).then(() => setDraft(void 0)).catch(() => {
|
|
1988
2302
|
});
|
|
1989
2303
|
},
|
|
1990
|
-
className:
|
|
2304
|
+
className: BUTTON_DANGER,
|
|
1991
2305
|
children: [
|
|
1992
|
-
/* @__PURE__ */
|
|
2306
|
+
/* @__PURE__ */ jsx21(Trash24, { "aria-hidden": "true", className: "h-4 w-4" }),
|
|
1993
2307
|
messages["common.remove"]
|
|
1994
2308
|
]
|
|
1995
2309
|
}
|
|
1996
2310
|
) : void 0,
|
|
1997
|
-
children: /* @__PURE__ */
|
|
2311
|
+
children: /* @__PURE__ */ jsx21(
|
|
1998
2312
|
ServiceForm,
|
|
1999
2313
|
{
|
|
2000
2314
|
...draft ? { initialValues: draft } : {},
|
|
@@ -2008,7 +2322,7 @@ function ServicesArea() {
|
|
|
2008
2322
|
}
|
|
2009
2323
|
|
|
2010
2324
|
// src/workspace/WorkspaceAreaNav.tsx
|
|
2011
|
-
import { CalendarDays, Clock, ClipboardList, Users, Wrench } from "lucide-react";
|
|
2325
|
+
import { CalendarDays as CalendarDays2, Clock as Clock2, ClipboardList as ClipboardList2, Users as Users2, Wrench as Wrench2 } from "lucide-react";
|
|
2012
2326
|
|
|
2013
2327
|
// src/workspace/workspace.constant.ts
|
|
2014
2328
|
var SCHEDULING_WORKSPACE_AREA = {
|
|
@@ -2024,16 +2338,16 @@ function isSchedulingWorkspaceArea(value) {
|
|
|
2024
2338
|
}
|
|
2025
2339
|
|
|
2026
2340
|
// 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-
|
|
2341
|
+
import { jsx as jsx22, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
2342
|
+
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";
|
|
2343
|
+
var ITEM_ACTIVE = "border-brand-600 text-brand-700 dark:text-brand-300";
|
|
2030
2344
|
var ITEM_IDLE = "border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100";
|
|
2031
2345
|
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]:
|
|
2346
|
+
[SCHEDULING_WORKSPACE_AREA.AGENDA]: CalendarDays2,
|
|
2347
|
+
[SCHEDULING_WORKSPACE_AREA.BOOKINGS]: ClipboardList2,
|
|
2348
|
+
[SCHEDULING_WORKSPACE_AREA.RESOURCES]: Users2,
|
|
2349
|
+
[SCHEDULING_WORKSPACE_AREA.SERVICES]: Wrench2,
|
|
2350
|
+
[SCHEDULING_WORKSPACE_AREA.AVAILABILITY]: Clock2
|
|
2037
2351
|
};
|
|
2038
2352
|
function WorkspaceAreaNav({ area, labels, onSelect }) {
|
|
2039
2353
|
const items = [
|
|
@@ -2043,9 +2357,9 @@ function WorkspaceAreaNav({ area, labels, onSelect }) {
|
|
|
2043
2357
|
[SCHEDULING_WORKSPACE_AREA.SERVICES, labels.servicesTab],
|
|
2044
2358
|
[SCHEDULING_WORKSPACE_AREA.AVAILABILITY, labels.availabilityTab]
|
|
2045
2359
|
];
|
|
2046
|
-
return /* @__PURE__ */
|
|
2360
|
+
return /* @__PURE__ */ jsx22("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
2361
|
const Icon = AREA_ICON[value];
|
|
2048
|
-
return /* @__PURE__ */
|
|
2362
|
+
return /* @__PURE__ */ jsxs20(
|
|
2049
2363
|
"button",
|
|
2050
2364
|
{
|
|
2051
2365
|
type: "button",
|
|
@@ -2053,7 +2367,7 @@ function WorkspaceAreaNav({ area, labels, onSelect }) {
|
|
|
2053
2367
|
"aria-current": value === area ? "page" : void 0,
|
|
2054
2368
|
className: `${ITEM_BASE} ${value === area ? ITEM_ACTIVE : ITEM_IDLE} whitespace-nowrap`,
|
|
2055
2369
|
children: [
|
|
2056
|
-
/* @__PURE__ */
|
|
2370
|
+
/* @__PURE__ */ jsx22(Icon, { "aria-hidden": "true", className: "w-4 h-4 shrink-0" }),
|
|
2057
2371
|
label
|
|
2058
2372
|
]
|
|
2059
2373
|
},
|
|
@@ -2063,7 +2377,7 @@ function WorkspaceAreaNav({ area, labels, onSelect }) {
|
|
|
2063
2377
|
}
|
|
2064
2378
|
|
|
2065
2379
|
// src/workspace/SchedulingWorkspace.tsx
|
|
2066
|
-
import { jsx as
|
|
2380
|
+
import { jsx as jsx23, jsxs as jsxs21 } from "react/jsx-runtime";
|
|
2067
2381
|
var AREA_COMPONENT = {
|
|
2068
2382
|
[SCHEDULING_WORKSPACE_AREA.AGENDA]: AgendaArea,
|
|
2069
2383
|
[SCHEDULING_WORKSPACE_AREA.BOOKINGS]: BookingsArea,
|
|
@@ -2085,13 +2399,13 @@ function SchedulingWorkspace({
|
|
|
2085
2399
|
setInternalArea(next);
|
|
2086
2400
|
onAreaChange?.(next);
|
|
2087
2401
|
}
|
|
2088
|
-
return /* @__PURE__ */
|
|
2089
|
-
/* @__PURE__ */
|
|
2090
|
-
/* @__PURE__ */
|
|
2402
|
+
return /* @__PURE__ */ jsxs21("div", { className: "flex h-full flex-col bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100", children: [
|
|
2403
|
+
/* @__PURE__ */ jsxs21("header", { className: "flex shrink-0 flex-wrap items-center gap-3 px-4 pb-2 pt-4", children: [
|
|
2404
|
+
/* @__PURE__ */ jsx23("h1", { className: "mr-auto text-xl font-semibold tracking-tight text-gray-900 dark:text-gray-100", children: labels.title }),
|
|
2091
2405
|
renderHeaderActions?.()
|
|
2092
2406
|
] }),
|
|
2093
|
-
/* @__PURE__ */
|
|
2094
|
-
/* @__PURE__ */
|
|
2407
|
+
/* @__PURE__ */ jsx23(WorkspaceAreaNav, { area, labels, onSelect: handleSelectArea }),
|
|
2408
|
+
/* @__PURE__ */ jsx23("div", { className: "relative flex flex-1 min-h-0 overflow-hidden", children: /* @__PURE__ */ jsx23(AreaComponent, {}) })
|
|
2095
2409
|
] });
|
|
2096
2410
|
}
|
|
2097
2411
|
export {
|