@wealthx/shadcn 1.2.2 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +193 -149
- package/CHANGELOG.md +28 -0
- package/dist/{chunk-4Y6R4WEC.mjs → chunk-2A5RRQGG.mjs} +9 -22
- package/dist/{chunk-TS2ZX2VS.mjs → chunk-2UM72RJ7.mjs} +11 -15
- package/dist/{chunk-A56YQQHG.mjs → chunk-3NCUZIFP.mjs} +2 -2
- package/dist/chunk-3OYFOX3X.mjs +79 -0
- package/dist/{chunk-RP3SQYA3.mjs → chunk-3TTACBDP.mjs} +9 -4
- package/dist/chunk-4GAWMKMI.mjs +710 -0
- package/dist/{chunk-VGSESELX.mjs → chunk-5FQIKDKP.mjs} +5 -5
- package/dist/{chunk-K3JYD4IU.mjs → chunk-5IS7G74I.mjs} +11 -4
- package/dist/chunk-6AW4KJHE.mjs +235 -0
- package/dist/chunk-6CR5N2JW.mjs +302 -0
- package/dist/{chunk-XIRTEFKH.mjs → chunk-6DZEXFNB.mjs} +36 -8
- package/dist/chunk-6O6KD7CE.mjs +271 -0
- package/dist/chunk-7PV3IWCN.mjs +33 -0
- package/dist/{chunk-SPJ5KXW7.mjs → chunk-7S5AESZO.mjs} +5 -5
- package/dist/{chunk-RYCLWMZ7.mjs → chunk-ABFDMHOR.mjs} +9 -7
- package/dist/{chunk-SWGT756Z.mjs → chunk-AMQZRHEZ.mjs} +10 -4
- package/dist/{chunk-WAZD7NFU.mjs → chunk-BKNFWEH2.mjs} +6 -6
- package/dist/{chunk-CLIN5525.mjs → chunk-C7CQJNMR.mjs} +1 -1
- package/dist/{chunk-D4ILTPOG.mjs → chunk-CFMQP5QS.mjs} +5 -4
- package/dist/{chunk-VPBN3WOO.mjs → chunk-DGHAXJBN.mjs} +9 -7
- package/dist/chunk-DOEO3CDL.mjs +27 -0
- package/dist/{chunk-5MEWU56Z.mjs → chunk-DUJTAXMH.mjs} +11 -6
- package/dist/{chunk-GGM2UYGG.mjs → chunk-EBXQWIYG.mjs} +10 -4
- package/dist/chunk-EWRB4PAD.mjs +468 -0
- package/dist/{chunk-ZSHYDDRB.mjs → chunk-FAKPBKLT.mjs} +6 -2
- package/dist/{chunk-A6AAWBPF.mjs → chunk-GHC7LLUX.mjs} +13 -4
- package/dist/chunk-HBZLGDIN.mjs +507 -0
- package/dist/{chunk-SIZMLSRU.mjs → chunk-HISNT2MG.mjs} +8 -6
- package/dist/{chunk-CGH4DRNG.mjs → chunk-HVY6KCCF.mjs} +10 -7
- package/dist/chunk-I3RZS7V2.mjs +136 -0
- package/dist/chunk-IAE3F7DR.mjs +1962 -0
- package/dist/{chunk-UT4KJR7V.mjs → chunk-IHMFS7NZ.mjs} +35 -74
- package/dist/{chunk-PCPLO5HT.mjs → chunk-IOJRDS6V.mjs} +96 -14
- package/dist/{chunk-LHYCMLVA.mjs → chunk-JKGDCQTZ.mjs} +11 -4
- package/dist/{chunk-H45TKD34.mjs → chunk-JMHR3YGZ.mjs} +1 -1
- package/dist/{chunk-4MN6UQHG.mjs → chunk-K5A5L6T2.mjs} +17 -39
- package/dist/chunk-LV35NGVG.mjs +272 -0
- package/dist/{chunk-FZIXGLMV.mjs → chunk-M3FV7LOK.mjs} +5 -12
- package/dist/{chunk-FMAXJ2SI.mjs → chunk-MBON7YRJ.mjs} +1 -1
- package/dist/chunk-MIZQHHUO.mjs +441 -0
- package/dist/chunk-MLNEWRWV.mjs +449 -0
- package/dist/chunk-MN5NYQCL.mjs +29 -0
- package/dist/chunk-NL3ZO62D.mjs +31 -0
- package/dist/{chunk-Q76O3RIQ.mjs → chunk-NMOI6CQD.mjs} +1 -1
- package/dist/{chunk-P6AM5V7O.mjs → chunk-OODBHKG7.mjs} +1 -1
- package/dist/chunk-PBL4OQV2.mjs +283 -0
- package/dist/{chunk-Y4QFWRNR.mjs → chunk-PU4YZQXV.mjs} +17 -18
- package/dist/chunk-Q2BGOAMG.mjs +202 -0
- package/dist/chunk-QMY3AZJH.mjs +80 -0
- package/dist/{chunk-BL3DXM2X.mjs → chunk-QZ4RE6NA.mjs} +11 -4
- package/dist/{chunk-VACKZOMY.mjs → chunk-R3VSPKNP.mjs} +3 -3
- package/dist/{chunk-OPNQAVVH.mjs → chunk-RJI6GKVF.mjs} +8 -6
- package/dist/{chunk-WG6JGJXB.mjs → chunk-T4BJLT57.mjs} +1 -1
- package/dist/chunk-UMTOX62O.mjs +415 -0
- package/dist/{chunk-7MMXNK3C.mjs → chunk-VLARHE5V.mjs} +8 -6
- package/dist/{chunk-2I5S2AMY.mjs → chunk-XREGSKX3.mjs} +2 -2
- package/dist/{chunk-JNQORUPP.mjs → chunk-YJG55G2H.mjs} +14 -11
- package/dist/components/ui/add-column-modal.js +42 -14
- package/dist/components/ui/add-column-modal.mjs +5 -5
- package/dist/components/ui/add-lead-modal.js +42 -11
- package/dist/components/ui/add-lead-modal.mjs +3 -3
- package/dist/components/ui/advisor-card.js +530 -0
- package/dist/components/ui/advisor-card.mjs +15 -0
- package/dist/components/ui/ai-assistant-drawer.js +11 -10
- package/dist/components/ui/ai-assistant-drawer.mjs +3 -3
- package/dist/components/ui/alert-dialog.js +2 -2
- package/dist/components/ui/alert-dialog.mjs +2 -2
- package/dist/components/ui/appointment-action-dialogs.js +1160 -0
- package/dist/components/ui/appointment-action-dialogs.mjs +23 -0
- package/dist/components/ui/appointment-availability-settings.js +1590 -0
- package/dist/components/ui/appointment-availability-settings.mjs +23 -0
- package/dist/components/ui/appointment-book-dialog.js +1744 -0
- package/dist/components/ui/appointment-book-dialog.mjs +27 -0
- package/dist/components/ui/appointment-calendar-view.js +833 -0
- package/dist/components/ui/appointment-calendar-view.mjs +14 -0
- package/dist/components/ui/appointment-detail-sheet.js +1517 -0
- package/dist/components/ui/appointment-detail-sheet.mjs +24 -0
- package/dist/components/ui/appointment-gmail-connect.js +467 -0
- package/dist/components/ui/appointment-gmail-connect.mjs +14 -0
- package/dist/components/ui/appointment-mini-card.js +345 -0
- package/dist/components/ui/appointment-mini-card.mjs +11 -0
- package/dist/components/ui/appointment-time-slot-picker.js +311 -0
- package/dist/components/ui/appointment-time-slot-picker.mjs +13 -0
- package/dist/components/ui/appointment-upcoming-card.js +1268 -0
- package/dist/components/ui/appointment-upcoming-card.mjs +21 -0
- package/dist/components/ui/backoffice-alert-history-chart.js +11 -5
- package/dist/components/ui/backoffice-alert-history-chart.mjs +5 -4
- package/dist/components/ui/backoffice-alerts-chart.js +786 -0
- package/dist/components/ui/backoffice-alerts-chart.mjs +19 -0
- package/dist/components/ui/backoffice-connections-chart.js +817 -0
- package/dist/components/ui/backoffice-connections-chart.mjs +19 -0
- package/dist/components/ui/backoffice-contact-history-chart.js +11 -5
- package/dist/components/ui/backoffice-contact-history-chart.mjs +5 -4
- package/dist/components/ui/badge.js +6 -6
- package/dist/components/ui/badge.mjs +1 -1
- package/dist/components/ui/borrowing-capacity-line-chart.js +30 -21
- package/dist/components/ui/borrowing-capacity-line-chart.mjs +5 -4
- package/dist/components/ui/button.js +2 -2
- package/dist/components/ui/button.mjs +1 -1
- package/dist/components/ui/calendar.js +2 -2
- package/dist/components/ui/calendar.mjs +2 -2
- package/dist/components/ui/card.js +1 -1
- package/dist/components/ui/card.mjs +1 -1
- package/dist/components/ui/cash-balance-line-chart.js +31 -23
- package/dist/components/ui/cash-balance-line-chart.mjs +5 -4
- package/dist/components/ui/cashflow-bar-chart.js +12 -5
- package/dist/components/ui/cashflow-bar-chart.mjs +5 -4
- package/dist/components/ui/chip.js +97 -18
- package/dist/components/ui/chip.mjs +3 -2
- package/dist/components/ui/color-picker.js +158 -28
- package/dist/components/ui/color-picker.mjs +3 -1
- package/dist/components/ui/data-table.js +140 -119
- package/dist/components/ui/data-table.mjs +3 -2
- package/dist/components/ui/date-picker.js +48 -27
- package/dist/components/ui/date-picker.mjs +4 -3
- package/dist/components/ui/dialog.js +37 -9
- package/dist/components/ui/dialog.mjs +2 -2
- package/dist/components/ui/expense-bar-chart.js +12 -5
- package/dist/components/ui/expense-bar-chart.mjs +5 -4
- package/dist/components/ui/field.mjs +2 -2
- package/dist/components/ui/financial-cards.js +322 -155
- package/dist/components/ui/financial-cards.mjs +5 -3
- package/dist/components/ui/financial-drawers.js +2 -2
- package/dist/components/ui/financial-drawers.mjs +3 -3
- package/dist/components/ui/financial-sections.js +14 -10
- package/dist/components/ui/financial-sections.mjs +6 -5
- package/dist/components/ui/income-bar-chart.js +12 -5
- package/dist/components/ui/income-bar-chart.mjs +5 -4
- package/dist/components/ui/input-group.js +2 -2
- package/dist/components/ui/input-group.mjs +2 -2
- package/dist/components/ui/kanban-column.js +52 -44
- package/dist/components/ui/kanban-column.mjs +7 -5
- package/dist/components/ui/opportunity-card.js +52 -44
- package/dist/components/ui/opportunity-card.mjs +6 -4
- package/dist/components/ui/opportunity-edit-modals.js +1367 -1263
- package/dist/components/ui/opportunity-edit-modals.mjs +8 -8
- package/dist/components/ui/opportunity-summary-tab.js +2744 -2157
- package/dist/components/ui/opportunity-summary-tab.mjs +14 -14
- package/dist/components/ui/page-header.js +92 -0
- package/dist/components/ui/page-header.mjs +8 -0
- package/dist/components/ui/page-top-bar.js +88 -0
- package/dist/components/ui/page-top-bar.mjs +8 -0
- package/dist/components/ui/pagination.js +303 -19
- package/dist/components/ui/pagination.mjs +11 -4
- package/dist/components/ui/pipeline-board.js +205 -191
- package/dist/components/ui/pipeline-board.mjs +9 -7
- package/dist/components/ui/pipeline-dialogs.js +114 -65
- package/dist/components/ui/pipeline-dialogs.mjs +7 -6
- package/dist/components/ui/pipeline-primitives.js +6 -6
- package/dist/components/ui/pipeline-primitives.mjs +2 -2
- package/dist/components/ui/property-cashflow-doughnut-chart.js +14 -12
- package/dist/components/ui/property-cashflow-doughnut-chart.mjs +5 -4
- package/dist/components/ui/property-debt-equity-doughnut-chart.js +14 -12
- package/dist/components/ui/property-debt-equity-doughnut-chart.mjs +5 -4
- package/dist/components/ui/property-mobile-estimate-line-chart.js +16 -14
- package/dist/components/ui/property-mobile-estimate-line-chart.mjs +5 -4
- package/dist/components/ui/sidebar-nav.js +426 -191
- package/dist/components/ui/sidebar-nav.mjs +5 -1
- package/dist/components/ui/stage-timeline.js +6 -6
- package/dist/components/ui/stage-timeline.mjs +3 -3
- package/dist/components/ui/transactions-expense-categories-doughnut-chart.js +18 -16
- package/dist/components/ui/transactions-expense-categories-doughnut-chart.mjs +5 -4
- package/dist/components/ui/transactions-income-expense-bar-chart.js +28 -12
- package/dist/components/ui/transactions-income-expense-bar-chart.mjs +5 -4
- package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.js +18 -16
- package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.mjs +5 -4
- package/dist/index.js +12258 -8611
- package/dist/index.mjs +258 -190
- package/dist/styles.css +1 -1
- package/package.json +71 -1
- package/src/components/index.tsx +115 -9
- package/src/components/ui/add-column-modal.tsx +7 -7
- package/src/components/ui/add-lead-modal.tsx +6 -3
- package/src/components/ui/advisor-card.tsx +284 -0
- package/src/components/ui/ai-assistant-drawer.tsx +4 -3
- package/src/components/ui/appointment-action-dialogs.tsx +297 -0
- package/src/components/ui/appointment-availability-settings.tsx +645 -0
- package/src/components/ui/appointment-book-dialog.tsx +618 -0
- package/src/components/ui/appointment-calendar-view.tsx +510 -0
- package/src/components/ui/appointment-detail-sheet.tsx +415 -0
- package/src/components/ui/appointment-gmail-connect.tsx +188 -0
- package/src/components/ui/appointment-mini-card.tsx +104 -0
- package/src/components/ui/appointment-time-slot-picker.tsx +123 -0
- package/src/components/ui/appointment-upcoming-card.tsx +635 -0
- package/src/components/ui/backoffice-alert-history-chart.tsx +10 -2
- package/src/components/ui/backoffice-alerts-chart.tsx +312 -0
- package/src/components/ui/backoffice-connections-chart.tsx +339 -0
- package/src/components/ui/backoffice-contact-history-chart.tsx +10 -2
- package/src/components/ui/badge.tsx +12 -6
- package/src/components/ui/borrowing-capacity-line-chart.tsx +4 -11
- package/src/components/ui/button.tsx +2 -2
- package/src/components/ui/card.tsx +1 -1
- package/src/components/ui/cash-balance-line-chart.tsx +4 -23
- package/src/components/ui/cashflow-bar-chart.tsx +9 -2
- package/src/components/ui/chart-shared.tsx +4 -11
- package/src/components/ui/chip.tsx +23 -19
- package/src/components/ui/color-picker.tsx +4 -2
- package/src/components/ui/data-table.tsx +28 -74
- package/src/components/ui/date-picker.tsx +42 -37
- package/src/components/ui/dialog.tsx +72 -6
- package/src/components/ui/expense-bar-chart.tsx +11 -2
- package/src/components/ui/financial-cards.tsx +99 -10
- package/src/components/ui/income-bar-chart.tsx +11 -2
- package/src/components/ui/opportunity-card.tsx +10 -39
- package/src/components/ui/opportunity-edit-modals.tsx +98 -36
- package/src/components/ui/opportunity-summary-tab.tsx +548 -232
- package/src/components/ui/page-header.tsx +57 -0
- package/src/components/ui/page-top-bar.tsx +48 -0
- package/src/components/ui/pagination.tsx +171 -22
- package/src/components/ui/pipeline-board.tsx +12 -5
- package/src/components/ui/property-cashflow-doughnut-chart.tsx +3 -1
- package/src/components/ui/property-debt-equity-doughnut-chart.tsx +3 -1
- package/src/components/ui/property-mobile-estimate-line-chart.tsx +3 -1
- package/src/components/ui/sidebar-nav.tsx +213 -157
- package/src/components/ui/transactions-expense-categories-doughnut-chart.tsx +3 -1
- package/src/components/ui/transactions-income-expense-bar-chart.tsx +12 -9
- package/src/components/ui/transactions-liabilities-breakdown-doughnut-chart.tsx +3 -1
- package/src/lib/format-currency.ts +44 -0
- package/src/lib/format-date.ts +50 -0
- package/src/lib/opportunity-constants.ts +12 -0
- package/src/styles/globals.css +17 -15
- package/src/styles/styles-css.ts +1 -1
- package/tsup.config.ts +14 -0
- package/dist/chunk-S4QRUQNW.mjs +0 -475
- package/dist/chunk-URGMJAE3.mjs +0 -1885
- package/dist/chunk-WNGWBVLV.mjs +0 -148
- package/dist/chunk-ZRSDX6OW.mjs +0 -385
- package/dist/{chunk-LLVQKSU3.mjs → chunk-GD4BJDJR.mjs} +3 -3
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Button } from "./button";
|
|
3
|
+
import { DatePicker } from "./date-picker";
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
DialogFooter,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
} from "./dialog";
|
|
12
|
+
import { Input } from "./input";
|
|
13
|
+
import { Label } from "./label";
|
|
14
|
+
import {
|
|
15
|
+
Select,
|
|
16
|
+
SelectContent,
|
|
17
|
+
SelectItem,
|
|
18
|
+
SelectTrigger,
|
|
19
|
+
SelectValue,
|
|
20
|
+
} from "./select";
|
|
21
|
+
import { Separator } from "./separator";
|
|
22
|
+
import { Switch } from "./switch";
|
|
23
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
|
|
24
|
+
import { cn } from "@/lib/utils";
|
|
25
|
+
import { formatDateWithWeekday } from "@/lib/format-date";
|
|
26
|
+
import { Plus, X } from "lucide-react";
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Types
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
export interface AppointmentDaySchedule {
|
|
33
|
+
day: "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" | "Sun";
|
|
34
|
+
enabled: boolean;
|
|
35
|
+
/** 24h format — "09:00" */
|
|
36
|
+
startTime: string;
|
|
37
|
+
/** 24h format — "17:00" */
|
|
38
|
+
endTime: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AppointmentAvailabilityPrefs {
|
|
42
|
+
meetingDuration: string;
|
|
43
|
+
schedulingBuffer: string;
|
|
44
|
+
maxSlotsPerDay: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AppointmentBlockedDate {
|
|
48
|
+
/** ISO date string — "2026-04-25" */
|
|
49
|
+
date: string;
|
|
50
|
+
/** Human-readable label — "ANZAC Day" */
|
|
51
|
+
label?: string;
|
|
52
|
+
/** Partial day — start time in 24h format "09:00" */
|
|
53
|
+
timeStart?: string;
|
|
54
|
+
/** Partial day — end time in 24h format "17:00" */
|
|
55
|
+
timeEnd?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface AppointmentAvailabilitySettingsProps {
|
|
59
|
+
schedule: AppointmentDaySchedule[];
|
|
60
|
+
blockedDates?: AppointmentBlockedDate[];
|
|
61
|
+
onSave?: (
|
|
62
|
+
schedule: AppointmentDaySchedule[],
|
|
63
|
+
prefs: AppointmentAvailabilityPrefs,
|
|
64
|
+
) => void;
|
|
65
|
+
onBlockedDatesChange?: (dates: AppointmentBlockedDate[]) => void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// AU Public Holidays 2026
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
const AU_PUBLIC_HOLIDAYS_2026: AppointmentBlockedDate[] = [
|
|
73
|
+
{ date: "2026-01-01", label: "New Year's Day" },
|
|
74
|
+
{ date: "2026-01-26", label: "Australia Day" },
|
|
75
|
+
{ date: "2026-04-03", label: "Good Friday" },
|
|
76
|
+
{ date: "2026-04-04", label: "Easter Saturday" },
|
|
77
|
+
{ date: "2026-04-06", label: "Easter Monday" },
|
|
78
|
+
{ date: "2026-04-25", label: "ANZAC Day" },
|
|
79
|
+
{ date: "2026-06-08", label: "King's Birthday" },
|
|
80
|
+
{ date: "2026-12-25", label: "Christmas Day" },
|
|
81
|
+
{ date: "2026-12-26", label: "Boxing Day" },
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Internal type — Time Off entries with switch state
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
interface TimeOffEntry {
|
|
89
|
+
date: string;
|
|
90
|
+
label?: string;
|
|
91
|
+
enabled: boolean;
|
|
92
|
+
isHoliday: boolean;
|
|
93
|
+
timeStart?: string;
|
|
94
|
+
timeEnd?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Time options — 30-min increments from 6:00 to 21:30
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
const TIME_OPTIONS: { value: string; label: string }[] = (() => {
|
|
102
|
+
const opts: { value: string; label: string }[] = [];
|
|
103
|
+
for (let h = 6; h <= 21; h++) {
|
|
104
|
+
for (const m of [0, 30]) {
|
|
105
|
+
const hh = String(h).padStart(2, "0");
|
|
106
|
+
const mm = String(m).padStart(2, "0");
|
|
107
|
+
const hour12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
|
|
108
|
+
const ampm = h < 12 ? "AM" : "PM";
|
|
109
|
+
opts.push({ value: `${hh}:${mm}`, label: `${hour12}:${mm} ${ampm}` });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return opts;
|
|
113
|
+
})();
|
|
114
|
+
|
|
115
|
+
const timeLabel = (v: string) =>
|
|
116
|
+
TIME_OPTIONS.find((o) => o.value === v)?.label ?? v;
|
|
117
|
+
|
|
118
|
+
function TimeSelect({
|
|
119
|
+
value,
|
|
120
|
+
onChange,
|
|
121
|
+
disabled,
|
|
122
|
+
}: {
|
|
123
|
+
value: string;
|
|
124
|
+
onChange: (v: string) => void;
|
|
125
|
+
disabled?: boolean;
|
|
126
|
+
}) {
|
|
127
|
+
return (
|
|
128
|
+
<Select
|
|
129
|
+
value={value}
|
|
130
|
+
onValueChange={(v) => onChange(v as string)}
|
|
131
|
+
disabled={disabled}
|
|
132
|
+
>
|
|
133
|
+
<SelectTrigger className="w-36">
|
|
134
|
+
<SelectValue />
|
|
135
|
+
</SelectTrigger>
|
|
136
|
+
<SelectContent>
|
|
137
|
+
{TIME_OPTIONS.map((opt) => (
|
|
138
|
+
<SelectItem key={opt.value} value={opt.value}>
|
|
139
|
+
{opt.label}
|
|
140
|
+
</SelectItem>
|
|
141
|
+
))}
|
|
142
|
+
</SelectContent>
|
|
143
|
+
</Select>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// "Add More" dialog
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
function AddTimeOffDialog({
|
|
153
|
+
open,
|
|
154
|
+
onOpenChange,
|
|
155
|
+
onAdd,
|
|
156
|
+
}: {
|
|
157
|
+
open: boolean;
|
|
158
|
+
onOpenChange: (v: boolean) => void;
|
|
159
|
+
onAdd: (entry: {
|
|
160
|
+
date: string;
|
|
161
|
+
label?: string;
|
|
162
|
+
timeStart?: string;
|
|
163
|
+
timeEnd?: string;
|
|
164
|
+
}) => void;
|
|
165
|
+
}) {
|
|
166
|
+
const [label, setLabel] = React.useState("");
|
|
167
|
+
const [date, setDate] = React.useState<Date | undefined>(undefined);
|
|
168
|
+
const [includeTime, setIncludeTime] = React.useState(false);
|
|
169
|
+
const [timeStart, setTimeStart] = React.useState("09:00");
|
|
170
|
+
const [timeEnd, setTimeEnd] = React.useState("17:00");
|
|
171
|
+
|
|
172
|
+
const reset = () => {
|
|
173
|
+
setLabel("");
|
|
174
|
+
setDate(undefined);
|
|
175
|
+
setIncludeTime(false);
|
|
176
|
+
setTimeStart("09:00");
|
|
177
|
+
setTimeEnd("17:00");
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const handleAdd = () => {
|
|
181
|
+
if (!date) return;
|
|
182
|
+
// Convert Date to ISO date string "YYYY-MM-DD"
|
|
183
|
+
const isoDate = [
|
|
184
|
+
date.getFullYear(),
|
|
185
|
+
String(date.getMonth() + 1).padStart(2, "0"),
|
|
186
|
+
String(date.getDate()).padStart(2, "0"),
|
|
187
|
+
].join("-");
|
|
188
|
+
onAdd({
|
|
189
|
+
date: isoDate,
|
|
190
|
+
label: label.trim() || undefined,
|
|
191
|
+
timeStart: includeTime ? timeStart : undefined,
|
|
192
|
+
timeEnd: includeTime ? timeEnd : undefined,
|
|
193
|
+
});
|
|
194
|
+
reset();
|
|
195
|
+
onOpenChange(false);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const handleCancel = () => {
|
|
199
|
+
reset();
|
|
200
|
+
onOpenChange(false);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
205
|
+
<DialogContent size="auto" minWidth="26rem" align="top">
|
|
206
|
+
<DialogHeader>
|
|
207
|
+
<DialogTitle>Add time off</DialogTitle>
|
|
208
|
+
<DialogDescription>
|
|
209
|
+
Block a date when you are unavailable. Clients cannot book on this
|
|
210
|
+
date.
|
|
211
|
+
</DialogDescription>
|
|
212
|
+
</DialogHeader>
|
|
213
|
+
|
|
214
|
+
<div className="flex flex-col gap-4 py-1">
|
|
215
|
+
{/* Label */}
|
|
216
|
+
<div className="flex flex-col gap-1.5">
|
|
217
|
+
<Label htmlFor="toff-label">
|
|
218
|
+
Name{" "}
|
|
219
|
+
<span className="font-normal text-muted-foreground">
|
|
220
|
+
(optional)
|
|
221
|
+
</span>
|
|
222
|
+
</Label>
|
|
223
|
+
<Input
|
|
224
|
+
id="toff-label"
|
|
225
|
+
placeholder="e.g. Conference, Personal day…"
|
|
226
|
+
value={label}
|
|
227
|
+
onChange={(e) => setLabel(e.target.value)}
|
|
228
|
+
/>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
{/* Date */}
|
|
232
|
+
<div className="flex flex-col gap-1.5">
|
|
233
|
+
<Label>Date</Label>
|
|
234
|
+
<DatePicker
|
|
235
|
+
value={date}
|
|
236
|
+
onChange={setDate}
|
|
237
|
+
placeholder="Pick a date"
|
|
238
|
+
calendarProps={{ fromDate: new Date() }}
|
|
239
|
+
/>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
{/* Add time switch */}
|
|
243
|
+
<div className="flex flex-col gap-3">
|
|
244
|
+
<div className="flex items-center gap-3">
|
|
245
|
+
<Switch
|
|
246
|
+
id="toff-include-time"
|
|
247
|
+
checked={includeTime}
|
|
248
|
+
onCheckedChange={setIncludeTime}
|
|
249
|
+
/>
|
|
250
|
+
<Label
|
|
251
|
+
htmlFor="toff-include-time"
|
|
252
|
+
className="cursor-pointer text-sm"
|
|
253
|
+
>
|
|
254
|
+
Specify hours (partial day)
|
|
255
|
+
</Label>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
{includeTime && (
|
|
259
|
+
<div className="flex items-center gap-3 pl-10">
|
|
260
|
+
<TimeSelect value={timeStart} onChange={setTimeStart} />
|
|
261
|
+
<span className="text-sm text-muted-foreground">to</span>
|
|
262
|
+
<TimeSelect value={timeEnd} onChange={setTimeEnd} />
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
<DialogFooter>
|
|
269
|
+
<Button variant="outline" onClick={handleCancel}>
|
|
270
|
+
Cancel
|
|
271
|
+
</Button>
|
|
272
|
+
<Button onClick={handleAdd} disabled={!date}>
|
|
273
|
+
Add
|
|
274
|
+
</Button>
|
|
275
|
+
</DialogFooter>
|
|
276
|
+
</DialogContent>
|
|
277
|
+
</Dialog>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// Component
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
export function AppointmentAvailabilitySettings({
|
|
286
|
+
schedule: initialSchedule,
|
|
287
|
+
blockedDates: blockedDatesProp,
|
|
288
|
+
onSave,
|
|
289
|
+
onBlockedDatesChange,
|
|
290
|
+
}: AppointmentAvailabilitySettingsProps) {
|
|
291
|
+
// Track first render to skip auto-save on mount
|
|
292
|
+
const hasMounted = React.useRef(false);
|
|
293
|
+
React.useEffect(() => {
|
|
294
|
+
hasMounted.current = true;
|
|
295
|
+
}, []);
|
|
296
|
+
|
|
297
|
+
// --- Weekly Availability tab ---
|
|
298
|
+
const [schedule, setSchedule] =
|
|
299
|
+
React.useState<AppointmentDaySchedule[]>(initialSchedule);
|
|
300
|
+
|
|
301
|
+
// --- Booking Preferences tab ---
|
|
302
|
+
const [meetingDuration, setMeetingDuration] = React.useState("30");
|
|
303
|
+
const [schedulingBuffer, setSchedulingBuffer] = React.useState("0");
|
|
304
|
+
const [maxSlotsPerDay, setMaxSlotsPerDay] = React.useState("8");
|
|
305
|
+
|
|
306
|
+
// --- Time Off tab ---
|
|
307
|
+
const [timeOffEntries, setTimeOffEntries] = React.useState<TimeOffEntry[]>(
|
|
308
|
+
() => {
|
|
309
|
+
const holidayDates = new Set(AU_PUBLIC_HOLIDAYS_2026.map((h) => h.date));
|
|
310
|
+
const entries: TimeOffEntry[] = AU_PUBLIC_HOLIDAYS_2026.map((h) => ({
|
|
311
|
+
...h,
|
|
312
|
+
enabled: true,
|
|
313
|
+
isHoliday: true,
|
|
314
|
+
}));
|
|
315
|
+
blockedDatesProp?.forEach((b) => {
|
|
316
|
+
if (!holidayDates.has(b.date)) {
|
|
317
|
+
entries.push({
|
|
318
|
+
date: b.date,
|
|
319
|
+
label: b.label,
|
|
320
|
+
enabled: true,
|
|
321
|
+
isHoliday: false,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
return entries.sort((a, b) => a.date.localeCompare(b.date));
|
|
326
|
+
},
|
|
327
|
+
);
|
|
328
|
+
const [addMoreOpen, setAddMoreOpen] = React.useState(false);
|
|
329
|
+
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
// Auto-save — fires after each deliberate state change (skip initial mount)
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
const currentPrefs = React.useMemo(
|
|
335
|
+
() => ({ meetingDuration, schedulingBuffer, maxSlotsPerDay }),
|
|
336
|
+
[meetingDuration, schedulingBuffer, maxSlotsPerDay],
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
React.useEffect(() => {
|
|
340
|
+
if (!hasMounted.current) return;
|
|
341
|
+
onSave?.(schedule, currentPrefs);
|
|
342
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
343
|
+
}, [schedule]);
|
|
344
|
+
|
|
345
|
+
React.useEffect(() => {
|
|
346
|
+
if (!hasMounted.current) return;
|
|
347
|
+
onSave?.(schedule, currentPrefs);
|
|
348
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
349
|
+
}, [meetingDuration, schedulingBuffer, maxSlotsPerDay]);
|
|
350
|
+
|
|
351
|
+
React.useEffect(() => {
|
|
352
|
+
if (!hasMounted.current) return;
|
|
353
|
+
const blocked = timeOffEntries
|
|
354
|
+
.filter((e) => e.enabled)
|
|
355
|
+
.map(({ date, label, timeStart, timeEnd }) => ({
|
|
356
|
+
date,
|
|
357
|
+
label,
|
|
358
|
+
timeStart,
|
|
359
|
+
timeEnd,
|
|
360
|
+
}));
|
|
361
|
+
onBlockedDatesChange?.(blocked);
|
|
362
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
363
|
+
}, [timeOffEntries]);
|
|
364
|
+
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
// Weekly handlers
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
|
|
369
|
+
const toggleDay = (index: number) => {
|
|
370
|
+
setSchedule((prev) =>
|
|
371
|
+
prev.map((d, i) => (i === index ? { ...d, enabled: !d.enabled } : d)),
|
|
372
|
+
);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const updateTime = (
|
|
376
|
+
index: number,
|
|
377
|
+
field: "startTime" | "endTime",
|
|
378
|
+
value: string,
|
|
379
|
+
) => {
|
|
380
|
+
setSchedule((prev) =>
|
|
381
|
+
prev.map((d, i) => (i === index ? { ...d, [field]: value } : d)),
|
|
382
|
+
);
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
// Time Off handlers
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
const toggleTimeOff = (date: string, enabled: boolean) => {
|
|
390
|
+
setTimeOffEntries((prev) =>
|
|
391
|
+
prev.map((e) => (e.date === date ? { ...e, enabled } : e)),
|
|
392
|
+
);
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const removeCustomDate = (date: string) => {
|
|
396
|
+
setTimeOffEntries((prev) => prev.filter((e) => e.date !== date));
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const handleAddMore = (entry: {
|
|
400
|
+
date: string;
|
|
401
|
+
label?: string;
|
|
402
|
+
timeStart?: string;
|
|
403
|
+
timeEnd?: string;
|
|
404
|
+
}) => {
|
|
405
|
+
if (timeOffEntries.some((e) => e.date === entry.date)) return;
|
|
406
|
+
setTimeOffEntries((prev) =>
|
|
407
|
+
[
|
|
408
|
+
...prev,
|
|
409
|
+
{
|
|
410
|
+
date: entry.date,
|
|
411
|
+
label: entry.label,
|
|
412
|
+
enabled: true,
|
|
413
|
+
isHoliday: false,
|
|
414
|
+
timeStart: entry.timeStart,
|
|
415
|
+
timeEnd: entry.timeEnd,
|
|
416
|
+
},
|
|
417
|
+
].sort((a, b) => a.date.localeCompare(b.date)),
|
|
418
|
+
);
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
return (
|
|
422
|
+
<div className="border border-border bg-card">
|
|
423
|
+
<Tabs defaultValue="weekly" className="gap-0">
|
|
424
|
+
{/* Tab strip */}
|
|
425
|
+
<div className="border-b border-border p-1">
|
|
426
|
+
<TabsList variant="default" className="w-full">
|
|
427
|
+
<TabsTrigger value="weekly">Weekly Availability</TabsTrigger>
|
|
428
|
+
<TabsTrigger value="booking">Booking Preferences</TabsTrigger>
|
|
429
|
+
<TabsTrigger value="time-off">Time Off</TabsTrigger>
|
|
430
|
+
</TabsList>
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
{/* ---------------------------------------------------------------- */}
|
|
434
|
+
{/* Tab 1: Weekly Availability */}
|
|
435
|
+
{/* ---------------------------------------------------------------- */}
|
|
436
|
+
<TabsContent value="weekly">
|
|
437
|
+
<div className="flex flex-col divide-y divide-border">
|
|
438
|
+
{schedule.map((day, index) => (
|
|
439
|
+
<div
|
|
440
|
+
key={day.day}
|
|
441
|
+
className="flex min-h-[68px] items-center gap-5 px-6 py-4"
|
|
442
|
+
>
|
|
443
|
+
<div className="flex w-28 items-center gap-3">
|
|
444
|
+
<Switch
|
|
445
|
+
id={`day-${day.day}`}
|
|
446
|
+
checked={day.enabled}
|
|
447
|
+
onCheckedChange={() => toggleDay(index)}
|
|
448
|
+
/>
|
|
449
|
+
<Label
|
|
450
|
+
htmlFor={`day-${day.day}`}
|
|
451
|
+
className="cursor-pointer text-sm font-medium"
|
|
452
|
+
>
|
|
453
|
+
{day.day}
|
|
454
|
+
</Label>
|
|
455
|
+
</div>
|
|
456
|
+
|
|
457
|
+
{day.enabled ? (
|
|
458
|
+
<div className="flex flex-1 items-center gap-3">
|
|
459
|
+
<TimeSelect
|
|
460
|
+
value={day.startTime}
|
|
461
|
+
onChange={(v) => updateTime(index, "startTime", v)}
|
|
462
|
+
/>
|
|
463
|
+
<span className="text-sm text-muted-foreground">to</span>
|
|
464
|
+
<TimeSelect
|
|
465
|
+
value={day.endTime}
|
|
466
|
+
onChange={(v) => updateTime(index, "endTime", v)}
|
|
467
|
+
/>
|
|
468
|
+
</div>
|
|
469
|
+
) : (
|
|
470
|
+
<p className="flex-1 text-sm text-muted-foreground">
|
|
471
|
+
Unavailable
|
|
472
|
+
</p>
|
|
473
|
+
)}
|
|
474
|
+
</div>
|
|
475
|
+
))}
|
|
476
|
+
</div>
|
|
477
|
+
</TabsContent>
|
|
478
|
+
|
|
479
|
+
{/* ---------------------------------------------------------------- */}
|
|
480
|
+
{/* Tab 2: Booking Preferences */}
|
|
481
|
+
{/* ---------------------------------------------------------------- */}
|
|
482
|
+
<TabsContent value="booking">
|
|
483
|
+
<div className="flex flex-col divide-y divide-border">
|
|
484
|
+
<div className="flex min-h-[68px] items-center justify-between px-6 py-4">
|
|
485
|
+
<div className="flex flex-col gap-0.5">
|
|
486
|
+
<p className="text-sm font-medium">Meeting Duration</p>
|
|
487
|
+
<p className="text-xs text-muted-foreground">
|
|
488
|
+
Length of each appointment slot
|
|
489
|
+
</p>
|
|
490
|
+
</div>
|
|
491
|
+
<Select
|
|
492
|
+
value={meetingDuration}
|
|
493
|
+
onValueChange={(v) => setMeetingDuration(v as string)}
|
|
494
|
+
>
|
|
495
|
+
<SelectTrigger className="w-40">
|
|
496
|
+
<SelectValue />
|
|
497
|
+
</SelectTrigger>
|
|
498
|
+
<SelectContent>
|
|
499
|
+
<SelectItem value="15">15 minutes</SelectItem>
|
|
500
|
+
<SelectItem value="30">30 minutes</SelectItem>
|
|
501
|
+
<SelectItem value="45">45 minutes</SelectItem>
|
|
502
|
+
<SelectItem value="60">60 minutes</SelectItem>
|
|
503
|
+
<SelectItem value="90">90 minutes</SelectItem>
|
|
504
|
+
</SelectContent>
|
|
505
|
+
</Select>
|
|
506
|
+
</div>
|
|
507
|
+
|
|
508
|
+
<div className="flex min-h-[68px] items-center justify-between px-6 py-4">
|
|
509
|
+
<div className="flex flex-col gap-0.5">
|
|
510
|
+
<p className="text-sm font-medium">Scheduling Buffer</p>
|
|
511
|
+
<p className="text-xs text-muted-foreground">
|
|
512
|
+
Gap between consecutive bookings
|
|
513
|
+
</p>
|
|
514
|
+
</div>
|
|
515
|
+
<Select
|
|
516
|
+
value={schedulingBuffer}
|
|
517
|
+
onValueChange={(v) => setSchedulingBuffer(v as string)}
|
|
518
|
+
>
|
|
519
|
+
<SelectTrigger className="w-40">
|
|
520
|
+
<SelectValue />
|
|
521
|
+
</SelectTrigger>
|
|
522
|
+
<SelectContent>
|
|
523
|
+
<SelectItem value="0">No buffer</SelectItem>
|
|
524
|
+
<SelectItem value="5">5 minutes</SelectItem>
|
|
525
|
+
<SelectItem value="10">10 minutes</SelectItem>
|
|
526
|
+
<SelectItem value="15">15 minutes</SelectItem>
|
|
527
|
+
<SelectItem value="30">30 minutes</SelectItem>
|
|
528
|
+
</SelectContent>
|
|
529
|
+
</Select>
|
|
530
|
+
</div>
|
|
531
|
+
|
|
532
|
+
<div className="flex min-h-[68px] items-center justify-between px-6 py-4">
|
|
533
|
+
<div className="flex flex-col gap-0.5">
|
|
534
|
+
<p className="text-sm font-medium">Daily Slot Limit</p>
|
|
535
|
+
<p className="text-xs text-muted-foreground">
|
|
536
|
+
Maximum bookings accepted per day
|
|
537
|
+
</p>
|
|
538
|
+
</div>
|
|
539
|
+
<Select
|
|
540
|
+
value={maxSlotsPerDay}
|
|
541
|
+
onValueChange={(v) => setMaxSlotsPerDay(v as string)}
|
|
542
|
+
>
|
|
543
|
+
<SelectTrigger className="w-40">
|
|
544
|
+
<SelectValue />
|
|
545
|
+
</SelectTrigger>
|
|
546
|
+
<SelectContent>
|
|
547
|
+
<SelectItem value="2">2 per day</SelectItem>
|
|
548
|
+
<SelectItem value="4">4 per day</SelectItem>
|
|
549
|
+
<SelectItem value="6">6 per day</SelectItem>
|
|
550
|
+
<SelectItem value="8">8 per day</SelectItem>
|
|
551
|
+
<SelectItem value="10">10 per day</SelectItem>
|
|
552
|
+
<SelectItem value="unlimited">Unlimited</SelectItem>
|
|
553
|
+
</SelectContent>
|
|
554
|
+
</Select>
|
|
555
|
+
</div>
|
|
556
|
+
</div>
|
|
557
|
+
</TabsContent>
|
|
558
|
+
|
|
559
|
+
{/* ---------------------------------------------------------------- */}
|
|
560
|
+
{/* Tab 3: Time Off */}
|
|
561
|
+
{/* ---------------------------------------------------------------- */}
|
|
562
|
+
<TabsContent value="time-off">
|
|
563
|
+
{/* Description + Add more button */}
|
|
564
|
+
<div className="flex items-center justify-between px-6 py-4">
|
|
565
|
+
<p className="text-sm text-muted-foreground">
|
|
566
|
+
Toggle dates when you are unavailable. Clients cannot book on
|
|
567
|
+
switched-on dates.
|
|
568
|
+
</p>
|
|
569
|
+
<Button
|
|
570
|
+
variant="outline"
|
|
571
|
+
size="sm"
|
|
572
|
+
className="ml-4 shrink-0 gap-1.5"
|
|
573
|
+
onClick={() => setAddMoreOpen(true)}
|
|
574
|
+
>
|
|
575
|
+
<Plus className="h-3.5 w-3.5" />
|
|
576
|
+
Add more
|
|
577
|
+
</Button>
|
|
578
|
+
</div>
|
|
579
|
+
|
|
580
|
+
<Separator />
|
|
581
|
+
|
|
582
|
+
{/* Holiday + custom date rows */}
|
|
583
|
+
<div className="flex flex-col divide-y divide-border">
|
|
584
|
+
{timeOffEntries.map((entry) => {
|
|
585
|
+
const formattedDate = formatDateWithWeekday(entry.date);
|
|
586
|
+
const hasTimeRange = entry.timeStart && entry.timeEnd;
|
|
587
|
+
return (
|
|
588
|
+
<div
|
|
589
|
+
key={entry.date}
|
|
590
|
+
className="flex min-h-[68px] items-center gap-5 px-6 py-4"
|
|
591
|
+
>
|
|
592
|
+
<Switch
|
|
593
|
+
id={`toff-${entry.date}`}
|
|
594
|
+
checked={entry.enabled}
|
|
595
|
+
onCheckedChange={(v) => toggleTimeOff(entry.date, v)}
|
|
596
|
+
/>
|
|
597
|
+
<div className="min-w-0 flex-1">
|
|
598
|
+
<Label
|
|
599
|
+
htmlFor={`toff-${entry.date}`}
|
|
600
|
+
className={cn(
|
|
601
|
+
"cursor-pointer text-sm font-medium",
|
|
602
|
+
!entry.enabled && "text-muted-foreground",
|
|
603
|
+
)}
|
|
604
|
+
>
|
|
605
|
+
{entry.label ?? formattedDate}
|
|
606
|
+
</Label>
|
|
607
|
+
<p className="text-xs text-muted-foreground">
|
|
608
|
+
{entry.label ? formattedDate : null}
|
|
609
|
+
{hasTimeRange && (
|
|
610
|
+
<>
|
|
611
|
+
{entry.label ? " · " : ""}
|
|
612
|
+
{timeLabel(entry.timeStart!)} –{" "}
|
|
613
|
+
{timeLabel(entry.timeEnd!)}
|
|
614
|
+
</>
|
|
615
|
+
)}
|
|
616
|
+
</p>
|
|
617
|
+
</div>
|
|
618
|
+
{/* Only custom (non-holiday) dates can be removed */}
|
|
619
|
+
{!entry.isHoliday && (
|
|
620
|
+
<Button
|
|
621
|
+
type="button"
|
|
622
|
+
variant="ghost"
|
|
623
|
+
size="icon"
|
|
624
|
+
className="h-7 w-7 text-muted-foreground hover:text-destructive"
|
|
625
|
+
onClick={() => removeCustomDate(entry.date)}
|
|
626
|
+
>
|
|
627
|
+
<X className="h-3.5 w-3.5" />
|
|
628
|
+
</Button>
|
|
629
|
+
)}
|
|
630
|
+
</div>
|
|
631
|
+
);
|
|
632
|
+
})}
|
|
633
|
+
</div>
|
|
634
|
+
</TabsContent>
|
|
635
|
+
</Tabs>
|
|
636
|
+
|
|
637
|
+
{/* Add More dialog */}
|
|
638
|
+
<AddTimeOffDialog
|
|
639
|
+
open={addMoreOpen}
|
|
640
|
+
onOpenChange={setAddMoreOpen}
|
|
641
|
+
onAdd={handleAddMore}
|
|
642
|
+
/>
|
|
643
|
+
</div>
|
|
644
|
+
);
|
|
645
|
+
}
|