@fayz-ai/plugin-agenda 0.27.10 → 0.27.12
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/AgendaGeneralSettings-YBI43RB6.js +96 -0
- package/dist/AgendaGeneralSettings-YBI43RB6.js.map +1 -0
- package/dist/AgendaMessageTemplates-46ZLTFLM.js +31 -0
- package/dist/AgendaMessageTemplates-46ZLTFLM.js.map +1 -0
- package/dist/AgendaPage-43OXQDZE.js +3174 -0
- package/dist/AgendaPage-43OXQDZE.js.map +1 -0
- package/dist/CheckinPage-SGWK2TQZ.js +904 -0
- package/dist/CheckinPage-SGWK2TQZ.js.map +1 -0
- package/dist/ConfirmationsView-DZJSYBXA.js +361 -0
- package/dist/ConfirmationsView-DZJSYBXA.js.map +1 -0
- package/dist/MyClientsTodayWidget-ZGTXJJKN.js +127 -0
- package/dist/MyClientsTodayWidget-ZGTXJJKN.js.map +1 -0
- package/dist/PersonAttendanceBlock-PHZVHFQ4.js +105 -0
- package/dist/PersonAttendanceBlock-PHZVHFQ4.js.map +1 -0
- package/dist/PersonAttendancesWidget-3NTWBGCU.js +1278 -0
- package/dist/PersonAttendancesWidget-3NTWBGCU.js.map +1 -0
- package/dist/PersonServicesWidget-4RJGKX3K.js +80 -0
- package/dist/PersonServicesWidget-4RJGKX3K.js.map +1 -0
- package/dist/chunk-37INOUNW.js +20 -0
- package/dist/chunk-37INOUNW.js.map +1 -0
- package/dist/chunk-A2VD5DVN.js +12 -0
- package/dist/chunk-A2VD5DVN.js.map +1 -0
- package/dist/chunk-HGJI2JR6.js +169 -0
- package/dist/chunk-HGJI2JR6.js.map +1 -0
- package/dist/chunk-VFQPXKXP.js +11 -0
- package/dist/chunk-VFQPXKXP.js.map +1 -0
- package/dist/chunk-WNANVT4F.js +2386 -0
- package/dist/chunk-WNANVT4F.js.map +1 -0
- package/dist/chunk-X7PKHXT6.js +18 -0
- package/dist/chunk-X7PKHXT6.js.map +1 -0
- package/dist/chunk-XTNSK4RJ.js +12 -0
- package/dist/chunk-XTNSK4RJ.js.map +1 -0
- package/dist/components/PersonAttendanceBlock.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2530 -19553
- package/dist/index.js.map +1 -1
- package/dist/migrations/integrations-google-calendar.browser.js +15 -0
- package/dist/migrations/integrations-google-calendar.browser.js.map +1 -0
- package/dist/migrations/integrations-google-calendar.js +1992 -0
- package/dist/migrations/integrations-google-calendar.js.map +1 -0
- package/dist/migrations/main.browser.js +29 -0
- package/dist/migrations/main.browser.js.map +1 -0
- package/dist/migrations/main.js +6440 -0
- package/dist/migrations/main.js.map +1 -0
- package/package.json +13 -7
|
@@ -0,0 +1,3174 @@
|
|
|
1
|
+
import { StatusFilterList, AppointmentModal, isStatusAvailable } from './chunk-WNANVT4F.js';
|
|
2
|
+
import { AgendaRailColumn, AgendaFilterRail, INLINE_FILTER_MAX, AdaptiveFilter } from './chunk-HGJI2JR6.js';
|
|
3
|
+
import { useAgendaSettings } from './chunk-37INOUNW.js';
|
|
4
|
+
import { getTurnQueueLeader, subscribeTurnQueueLeader } from './chunk-X7PKHXT6.js';
|
|
5
|
+
import { setAgendaTenantId } from './chunk-A2VD5DVN.js';
|
|
6
|
+
import { AgendaContextProvider, useAgendaConfig, useAgendaProvider, useAgendaStore } from './chunk-XTNSK4RJ.js';
|
|
7
|
+
import React, { useRef, useState, useLayoutEffect, useMemo, useCallback, useEffect } from 'react';
|
|
8
|
+
import { usePluginRuntimeOptional, useTranslation, getCurrentLocale, useActiveUnitId, localDateKey, useDataChanged, currentAppPath, appHref, navigateApp, formatCurrency, todayLocal } from '@fayz-ai/core';
|
|
9
|
+
import FullCalendar from '@fullcalendar/react';
|
|
10
|
+
import dayGridPlugin from '@fullcalendar/daygrid';
|
|
11
|
+
import timeGridPlugin from '@fullcalendar/timegrid';
|
|
12
|
+
import resourceTimeGridPlugin from '@fullcalendar/resource-timegrid';
|
|
13
|
+
import interactionPlugin from '@fullcalendar/interaction';
|
|
14
|
+
import listPlugin from '@fullcalendar/list';
|
|
15
|
+
import ptBrLocale from '@fullcalendar/core/locales/pt-br';
|
|
16
|
+
import { Ban, Clock3, Plus, Settings, ChevronLeft, ChevronRight, CalendarX, UserX, Search, CalendarOff, Pencil, Trash2, X, Clock, User, MapPin, FileText, AlertTriangle, Check, DollarSign, HandCoins } from 'lucide-react';
|
|
17
|
+
import { usePluginPrefs, useTenantPluginSettings, useAdminPath, adminNavigateTo, PersonLink, PageActions, ScheduleEditor } from '@fayz-ai/admin';
|
|
18
|
+
import { toast, PersonAvatar, abbreviatePersonName, Badge, PersonMultiPicker, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SegmentedControl, Button, Switch, MiniCalendar, PageHeaderActions, ConfirmDialog, Skeleton, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, Input, PersonPicker, Textarea, Tabs, TabsList, TabsTrigger, DatePicker, TimePicker, ModalFooter } from '@fayz-ai/ui';
|
|
19
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
20
|
+
import { createPortal } from 'react-dom';
|
|
21
|
+
|
|
22
|
+
// src/styles/calendar-styles.ts
|
|
23
|
+
var STYLE_ID = "saas-agenda-calendar-styles";
|
|
24
|
+
var css = (
|
|
25
|
+
/* css */
|
|
26
|
+
`
|
|
27
|
+
/* Dark mode content background \u2014 uses the content token */
|
|
28
|
+
.dark .agenda-calendar,
|
|
29
|
+
.dark .agenda-calendar .fc,
|
|
30
|
+
.dark .agenda-calendar .fc-view-harness,
|
|
31
|
+
.dark .agenda-calendar .fc-timegrid,
|
|
32
|
+
.dark .agenda-calendar .fc-scrollgrid,
|
|
33
|
+
.dark .agenda-calendar .fc-timegrid-col,
|
|
34
|
+
.dark .agenda-calendar .fc-daygrid-body,
|
|
35
|
+
.dark .agenda-calendar .fc-timegrid-slot-lane {
|
|
36
|
+
background-color: hsl(var(--content));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* FullCalendar \u2014 Google Calendar inspired theme */
|
|
40
|
+
|
|
41
|
+
.agenda-calendar {
|
|
42
|
+
--fc-border-color: hsl(var(--border));
|
|
43
|
+
--fc-today-bg-color: transparent;
|
|
44
|
+
--fc-now-indicator-color: hsl(var(--destructive));
|
|
45
|
+
--fc-highlight-color: hsl(var(--primary) / 0.06);
|
|
46
|
+
--fc-non-business-color: hsl(var(--muted) / 0.15);
|
|
47
|
+
--fc-neutral-bg-color: transparent;
|
|
48
|
+
--fc-page-bg-color: transparent;
|
|
49
|
+
--fc-event-border-color: transparent;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.agenda-calendar .fc {
|
|
53
|
+
font-family: inherit;
|
|
54
|
+
background: transparent;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.agenda-calendar .fc .fc-view-harness {
|
|
58
|
+
background: transparent;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* \u2500\u2500 Column headers \u2500\u2500 */
|
|
62
|
+
.agenda-calendar .fc-col-header-cell {
|
|
63
|
+
background: transparent;
|
|
64
|
+
border-bottom: 1px solid var(--fc-border-color);
|
|
65
|
+
border-left: none;
|
|
66
|
+
border-right: none;
|
|
67
|
+
border-top: none;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.agenda-calendar .fc-col-header-cell-cushion {
|
|
71
|
+
padding: 8px 4px;
|
|
72
|
+
font-weight: 400;
|
|
73
|
+
font-size: 0.7rem;
|
|
74
|
+
color: hsl(var(--muted-foreground));
|
|
75
|
+
text-decoration: none;
|
|
76
|
+
text-transform: uppercase;
|
|
77
|
+
letter-spacing: 0.03em;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Today's column header */
|
|
81
|
+
.agenda-calendar .fc-day-today .fc-col-header-cell-cushion {
|
|
82
|
+
color: hsl(var(--primary-ink, var(--primary)));
|
|
83
|
+
font-weight: 600;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Resource header (professional columns) */
|
|
87
|
+
.agenda-calendar .fc .fc-resource-header {
|
|
88
|
+
background: transparent;
|
|
89
|
+
border-bottom: 1px solid var(--fc-border-color);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* \u2500\u2500 Time slots \u2014 compact \u2500\u2500 */
|
|
93
|
+
.agenda-calendar .fc-timegrid-slot {
|
|
94
|
+
height: 1.5rem;
|
|
95
|
+
border-bottom: 1px solid hsl(var(--border) / 0.4);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.agenda-calendar .fc-timegrid-slot-label {
|
|
99
|
+
vertical-align: top;
|
|
100
|
+
border: none;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.agenda-calendar .fc-timegrid-slot-label-cushion {
|
|
104
|
+
font-size: 0.65rem;
|
|
105
|
+
color: hsl(var(--muted-foreground) / 0.7);
|
|
106
|
+
padding: 1px 8px 0 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Half-hour lines lighter */
|
|
110
|
+
.agenda-calendar .fc-timegrid-slot-minor {
|
|
111
|
+
border-bottom-style: none;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* Column dividers */
|
|
115
|
+
.agenda-calendar .fc-timegrid-col {
|
|
116
|
+
border-right: 1px solid hsl(var(--border) / 0.5);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* \u2500\u2500 Events \u2500\u2500 */
|
|
120
|
+
.agenda-calendar .fc-event {
|
|
121
|
+
border-radius: 6px;
|
|
122
|
+
border-width: 0;
|
|
123
|
+
border-left: 4px solid;
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
transition: opacity 0.15s, box-shadow 0.15s, backdrop-filter 0.15s;
|
|
126
|
+
font-size: 0.7rem;
|
|
127
|
+
line-height: 1.3;
|
|
128
|
+
margin: 0 2px;
|
|
129
|
+
overflow: hidden;
|
|
130
|
+
backdrop-filter: blur(8px) saturate(140%);
|
|
131
|
+
-webkit-backdrop-filter: blur(8px) saturate(140%);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.agenda-calendar .fc-event .fc-event-main {
|
|
135
|
+
background: inherit;
|
|
136
|
+
border-radius: inherit;
|
|
137
|
+
overflow: hidden;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.agenda-calendar .fc-event:not(.fc-bg-event):hover {
|
|
141
|
+
opacity: 0.92;
|
|
142
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.12);
|
|
143
|
+
backdrop-filter: blur(12px) saturate(160%);
|
|
144
|
+
-webkit-backdrop-filter: blur(12px) saturate(160%);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.agenda-calendar .fc-bg-event {
|
|
148
|
+
pointer-events: none;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* \u2500\u2500 Now indicator \u2500\u2500 */
|
|
152
|
+
.agenda-calendar .fc-timegrid-now-indicator-line {
|
|
153
|
+
border-color: hsl(var(--destructive));
|
|
154
|
+
border-width: 2px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.agenda-calendar .fc-timegrid-now-indicator-arrow {
|
|
158
|
+
border-top-color: hsl(var(--destructive));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* \u2500\u2500 Month view \u2500\u2500 */
|
|
162
|
+
.agenda-calendar .fc-daygrid-day-number {
|
|
163
|
+
font-size: 0.8rem;
|
|
164
|
+
padding: 4px 8px;
|
|
165
|
+
text-decoration: none;
|
|
166
|
+
color: hsl(var(--foreground));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.agenda-calendar .fc-daygrid-day.fc-day-today .fc-daygrid-day-number {
|
|
170
|
+
background-color: hsl(var(--primary));
|
|
171
|
+
color: hsl(var(--primary-foreground));
|
|
172
|
+
border-radius: 50%;
|
|
173
|
+
width: 1.5rem;
|
|
174
|
+
height: 1.5rem;
|
|
175
|
+
display: flex;
|
|
176
|
+
align-items: center;
|
|
177
|
+
justify-content: center;
|
|
178
|
+
font-size: 0.75rem;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* \u2500\u2500 Selection \u2500\u2500 */
|
|
182
|
+
.agenda-calendar .fc-highlight {
|
|
183
|
+
background: hsl(var(--primary) / 0.08);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* \u2500\u2500 Links \u2500\u2500 */
|
|
187
|
+
.agenda-calendar .fc a {
|
|
188
|
+
text-decoration: none;
|
|
189
|
+
color: inherit;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* \u2500\u2500 Scrollbar \u2500\u2500 */
|
|
193
|
+
.agenda-calendar .fc-scroller {
|
|
194
|
+
scrollbar-width: thin;
|
|
195
|
+
scrollbar-color: hsl(var(--muted-foreground) / 0.12) transparent;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.agenda-calendar .fc-scroller::-webkit-scrollbar {
|
|
199
|
+
width: 6px;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.agenda-calendar .fc-scroller::-webkit-scrollbar-track {
|
|
203
|
+
background: transparent;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.agenda-calendar .fc-scroller::-webkit-scrollbar-thumb {
|
|
207
|
+
background: hsl(var(--muted-foreground) / 0.12);
|
|
208
|
+
border-radius: 3px;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* \u2500\u2500 Clean up borders \u2500\u2500 */
|
|
212
|
+
.agenda-calendar .fc .fc-timegrid {
|
|
213
|
+
border: none;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.agenda-calendar .fc-scrollgrid {
|
|
217
|
+
border: none !important;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.agenda-calendar .fc-scrollgrid td:last-child {
|
|
221
|
+
border-right: none;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* \u2500\u2500 Unavailable time background \u2014 opaque/disabled feel \u2500\u2500 */
|
|
225
|
+
/* Fora da grade \xE9 FECHADO, e tem de se ler como fechado \xE0 dist\xE2ncia de um
|
|
226
|
+
bra\xE7o. A 8% o hachurado sumia contra o branco da coluna aberta, e as duas
|
|
227
|
+
faixas viravam a mesma \u2014 quem marca em cima de um hor\xE1rio que n\xE3o existe
|
|
228
|
+
descobre no erro, n\xE3o no olho. */
|
|
229
|
+
.agenda-calendar .fc-unavailable-bg {
|
|
230
|
+
background: hsl(var(--muted-foreground) / 0.2) !important;
|
|
231
|
+
background-image: repeating-linear-gradient(
|
|
232
|
+
45deg,
|
|
233
|
+
transparent,
|
|
234
|
+
transparent 4px,
|
|
235
|
+
hsl(var(--muted-foreground) / 0.12) 4px,
|
|
236
|
+
hsl(var(--muted-foreground) / 0.12) 8px
|
|
237
|
+
) !important;
|
|
238
|
+
border-radius: 0;
|
|
239
|
+
border-left: none !important;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* \u2500\u2500 Location label on schedule blocks \u2500\u2500 */
|
|
243
|
+
.agenda-calendar .fc-location-label {
|
|
244
|
+
background: transparent !important;
|
|
245
|
+
border: none !important;
|
|
246
|
+
border-left-width: 0 !important;
|
|
247
|
+
cursor: default !important;
|
|
248
|
+
pointer-events: none;
|
|
249
|
+
box-shadow: none !important;
|
|
250
|
+
z-index: 1 !important;
|
|
251
|
+
margin: 0 4px !important;
|
|
252
|
+
font-weight: 500 !important;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.agenda-calendar .fc-location-label .fc-event-main {
|
|
256
|
+
padding: 0 !important;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* Week view day header with date */
|
|
260
|
+
.agenda-calendar .fc-timegrid .fc-col-header-cell {
|
|
261
|
+
padding: 4px 0;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* \u2500\u2500 Alternating resource column backgrounds \u2500\u2500 */
|
|
265
|
+
.agenda-calendar .fc-resource-alt {
|
|
266
|
+
background-color: hsl(var(--muted) / 0.4) !important;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.dark .agenda-calendar .fc-resource-alt {
|
|
270
|
+
background-color: hsl(var(--muted-foreground) / 0.08) !important;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/* Resource group header separator \u2014 stronger border between professionals */
|
|
274
|
+
.agenda-calendar .fc-resource-group {
|
|
275
|
+
border-left: 2px solid hsl(var(--border));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* Also stripe the resource header label for the alt group */
|
|
279
|
+
.agenda-calendar th.fc-resource-alt {
|
|
280
|
+
background-color: hsl(var(--muted) / 0.4) !important;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.dark .agenda-calendar th.fc-resource-alt {
|
|
284
|
+
background-color: hsl(var(--muted-foreground) / 0.06) !important;
|
|
285
|
+
}
|
|
286
|
+
/* \u2500\u2500 Semana agrupada por DATA \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
287
|
+
O cabe\xE7alho de cima \xE9 o dia e as colunas de baixo s\xE3o as agendas daquele
|
|
288
|
+
dia. Para o bloco existir aos olhos ele precisa de tr\xEAs coisas: o t\xEDtulo do
|
|
289
|
+
dia lendo como t\xEDtulo, uma junta vis\xEDvel na virada do dia, e uma faixa
|
|
290
|
+
alternando um dia sim outro n\xE3o. Sem elas quarenta e duas colunas viram uma
|
|
291
|
+
r\xE9gua sem marca nenhuma.
|
|
292
|
+
|
|
293
|
+
Sutil de prop\xF3sito: a junta \xE9 UMA linha de cor cheia contra as linhas a meio
|
|
294
|
+
tom que separam as agendas. Uma barra grossa gritaria mais alto que os
|
|
295
|
+
pr\xF3prios agendamentos, que \xE9 o que a tela veio mostrar. */
|
|
296
|
+
|
|
297
|
+
/* O t\xEDtulo do dia: \xE9 o grupo, ent\xE3o l\xEA como grupo. */
|
|
298
|
+
.agenda-calendar .fc-col-header-cell[data-date] > .fc-scrollgrid-sync-inner {
|
|
299
|
+
background: hsl(var(--muted) / 0.4);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.dark .agenda-calendar .fc-col-header-cell[data-date] > .fc-scrollgrid-sync-inner {
|
|
303
|
+
background: hsl(var(--muted-foreground) / 0.08);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.agenda-calendar .fc-col-header-cell[data-date] .fc-col-header-cell-cushion {
|
|
307
|
+
font-weight: 600;
|
|
308
|
+
font-size: 0.68rem;
|
|
309
|
+
letter-spacing: 0.08em;
|
|
310
|
+
color: hsl(var(--primary-ink, var(--primary)));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.agenda-calendar .fc-day-today.fc-col-header-cell[data-date] > .fc-scrollgrid-sync-inner {
|
|
314
|
+
background: hsl(var(--primary) / 0.1);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* A junta do bloco: a primeira agenda de cada dia fecha o dia anterior. */
|
|
318
|
+
.agenda-calendar th.fc-day-start,
|
|
319
|
+
.agenda-calendar td.fc-day-start {
|
|
320
|
+
border-left: 1px solid hsl(var(--border)) !important;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* \u2026e a mesma junta sobe at\xE9 o t\xEDtulo, para o corte atravessar a tela inteira. */
|
|
324
|
+
.agenda-calendar .fc-col-header-cell[data-date] + .fc-col-header-cell[data-date] {
|
|
325
|
+
border-left: 1px solid hsl(var(--border));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* O nome do profissional \xE9 a FOLHA agora: cabe pouco, ent\xE3o n\xE3o pode vazar. */
|
|
329
|
+
.agenda-calendar th[data-resource-id] {
|
|
330
|
+
overflow: hidden;
|
|
331
|
+
border-left: 1px solid hsl(var(--border) / 0.4);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.agenda-calendar th[data-resource-id] .fc-col-header-cell-cushion {
|
|
335
|
+
padding: 0;
|
|
336
|
+
text-transform: none;
|
|
337
|
+
letter-spacing: 0;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/* The day header fills its column, so a holiday name ellipses inside it
|
|
341
|
+
instead of pushing the day out. */
|
|
342
|
+
.agenda-calendar .fc-col-header-cell[data-date] .fc-col-header-cell-cushion {
|
|
343
|
+
display: block;
|
|
344
|
+
max-width: 100%;
|
|
345
|
+
overflow: hidden;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* A day of the week opens that day. */
|
|
349
|
+
.agenda-calendar .agenda-day-link {
|
|
350
|
+
cursor: pointer;
|
|
351
|
+
border-radius: 0.5rem;
|
|
352
|
+
padding: 2px 4px;
|
|
353
|
+
transition: background-color 120ms ease-out;
|
|
354
|
+
}
|
|
355
|
+
.agenda-calendar .agenda-day-link:hover,
|
|
356
|
+
.agenda-calendar .agenda-day-link:focus-visible {
|
|
357
|
+
background: hsl(var(--foreground) / 0.07);
|
|
358
|
+
outline: none;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* No schedule in the period: the whole grid reads as closed, in the same
|
|
362
|
+
hatch as time outside a schedule \u2014 a blank grid looks bookable. On the slot
|
|
363
|
+
rows, because with no schedule there may be no column to paint. */
|
|
364
|
+
.agenda-calendar.agenda-unscheduled .fc-timegrid-slots td.fc-timegrid-slot-lane {
|
|
365
|
+
background-color: hsl(var(--muted-foreground) / 0.2);
|
|
366
|
+
background-image: repeating-linear-gradient(
|
|
367
|
+
45deg,
|
|
368
|
+
transparent,
|
|
369
|
+
transparent 4px,
|
|
370
|
+
hsl(var(--muted-foreground) / 0.12) 4px,
|
|
371
|
+
hsl(var(--muted-foreground) / 0.12) 8px
|
|
372
|
+
);
|
|
373
|
+
background-attachment: fixed;
|
|
374
|
+
}
|
|
375
|
+
`
|
|
376
|
+
);
|
|
377
|
+
var injected = false;
|
|
378
|
+
function injectCalendarStyles() {
|
|
379
|
+
if (injected) return;
|
|
380
|
+
if (typeof document === "undefined") return;
|
|
381
|
+
const existing = document.getElementById(STYLE_ID);
|
|
382
|
+
if (existing) {
|
|
383
|
+
if (existing.textContent !== css) existing.textContent = css;
|
|
384
|
+
injected = true;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const style = document.createElement("style");
|
|
388
|
+
style.id = STYLE_ID;
|
|
389
|
+
style.textContent = css;
|
|
390
|
+
document.head.appendChild(style);
|
|
391
|
+
injected = true;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/lib/agenda-url.ts
|
|
395
|
+
var VIEWS = ["day", "week", "month", "list"];
|
|
396
|
+
function parseAgendaPath(path) {
|
|
397
|
+
const parts = (path.split("?")[0] ?? "").split("/").filter(Boolean);
|
|
398
|
+
if (parts[0] !== "agenda") return null;
|
|
399
|
+
const view = parts[1];
|
|
400
|
+
if (!view || !VIEWS.includes(view)) return null;
|
|
401
|
+
if (parts.length === 2) return { view };
|
|
402
|
+
if (parts.length !== 5) return null;
|
|
403
|
+
const [y, m, d] = parts.slice(2).map(Number);
|
|
404
|
+
const date = new Date(y, m - 1, d);
|
|
405
|
+
if (date.getFullYear() !== y || date.getMonth() !== m - 1 || date.getDate() !== d) return null;
|
|
406
|
+
return { view, date };
|
|
407
|
+
}
|
|
408
|
+
function agendaPath(view, date) {
|
|
409
|
+
if (!date) return `/agenda/${view}`;
|
|
410
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
411
|
+
return `/agenda/${view}/${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())}`;
|
|
412
|
+
}
|
|
413
|
+
function urlViewOf(viewType) {
|
|
414
|
+
if (viewType.startsWith("list")) return "list";
|
|
415
|
+
if (viewType.endsWith("Day")) return "day";
|
|
416
|
+
if (viewType.endsWith("Week")) return "week";
|
|
417
|
+
return "month";
|
|
418
|
+
}
|
|
419
|
+
function calendarViewFor(view, resourceCapable) {
|
|
420
|
+
if (view === "day") return resourceCapable ? "resourceTimeGridDay" : "timeGridDay";
|
|
421
|
+
if (view === "week") return resourceCapable ? "resourceTimeGridWeek" : "timeGridWeek";
|
|
422
|
+
if (view === "list") return resourceCapable ? "listWeek" : "listMonth";
|
|
423
|
+
return "dayGridMonth";
|
|
424
|
+
}
|
|
425
|
+
var DIAS = [
|
|
426
|
+
{ v: 1, label: "Seg" },
|
|
427
|
+
{ v: 2, label: "Ter" },
|
|
428
|
+
{ v: 3, label: "Qua" },
|
|
429
|
+
{ v: 4, label: "Qui" },
|
|
430
|
+
{ v: 5, label: "Sex" },
|
|
431
|
+
{ v: 6, label: "S\xE1b" },
|
|
432
|
+
{ v: 0, label: "Dom" }
|
|
433
|
+
];
|
|
434
|
+
var REPETICAO = [
|
|
435
|
+
{ v: "0", label: "Toda semana" },
|
|
436
|
+
{ v: "1", label: "Semana sim, semana n\xE3o" },
|
|
437
|
+
{ v: "2", label: "A cada 3 semanas" },
|
|
438
|
+
{ v: "3", label: "A cada 4 semanas" }
|
|
439
|
+
];
|
|
440
|
+
var hoje = () => todayLocal();
|
|
441
|
+
function BlockDialog({
|
|
442
|
+
open,
|
|
443
|
+
onClose,
|
|
444
|
+
professionalId: prefillProfessional,
|
|
445
|
+
date,
|
|
446
|
+
editing,
|
|
447
|
+
onSaved
|
|
448
|
+
}) {
|
|
449
|
+
const professionals = useAgendaStore((s) => s.agendaProfessionals);
|
|
450
|
+
const provider = useAgendaProvider();
|
|
451
|
+
const [assigneeId, setAssigneeId] = React.useState(prefillProfessional ?? "");
|
|
452
|
+
const [title, setTitle] = React.useState("");
|
|
453
|
+
const [description, setDescription] = React.useState("");
|
|
454
|
+
const [mode, setMode] = React.useState("single");
|
|
455
|
+
const [specificDate, setSpecificDate] = React.useState(date ?? hoje());
|
|
456
|
+
const [allDay, setAllDay] = React.useState(false);
|
|
457
|
+
const [startsAt, setStartsAt] = React.useState("12:00");
|
|
458
|
+
const [endsAt, setEndsAt] = React.useState("13:00");
|
|
459
|
+
const [days2, setDays] = React.useState([]);
|
|
460
|
+
const [validFrom, setValidFrom] = React.useState(date ?? hoje());
|
|
461
|
+
const [validTo, setValidTo] = React.useState("");
|
|
462
|
+
const [weekInterval, setWeekInterval] = React.useState("0");
|
|
463
|
+
const [saving, setSaving] = React.useState(false);
|
|
464
|
+
React.useEffect(() => {
|
|
465
|
+
if (!open) return;
|
|
466
|
+
if (editing) {
|
|
467
|
+
setAssigneeId(editing.assigneeId);
|
|
468
|
+
setTitle(editing.title);
|
|
469
|
+
setDescription(editing.description);
|
|
470
|
+
setMode(editing.daysOfWeek.length ? "recurring" : "single");
|
|
471
|
+
setSpecificDate(editing.specificDate ?? hoje());
|
|
472
|
+
setDays(editing.daysOfWeek);
|
|
473
|
+
setValidFrom(editing.validFrom ?? hoje());
|
|
474
|
+
setValidTo(editing.validTo ?? "");
|
|
475
|
+
setWeekInterval(String(editing.weekInterval));
|
|
476
|
+
setAllDay(editing.startsAt === "00:00" && editing.endsAt === "23:59");
|
|
477
|
+
setStartsAt(editing.startsAt);
|
|
478
|
+
setEndsAt(editing.endsAt);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
setAssigneeId(prefillProfessional ?? "");
|
|
482
|
+
setTitle("");
|
|
483
|
+
setDescription("");
|
|
484
|
+
setMode("single");
|
|
485
|
+
setDays([]);
|
|
486
|
+
setValidTo("");
|
|
487
|
+
setSpecificDate(date ?? hoje());
|
|
488
|
+
setValidFrom(date ?? hoje());
|
|
489
|
+
}, [open, prefillProfessional, date, editing]);
|
|
490
|
+
const toggleDay = (d) => setDays((cur) => cur.includes(d) ? cur.filter((x) => x !== d) : [...cur, d]);
|
|
491
|
+
const falta = !assigneeId ? "Escolha o profissional" : !title.trim() ? "D\xEA um t\xEDtulo ao bloqueio" : mode === "recurring" && days2.length === 0 ? "Escolha ao menos um dia da semana" : null;
|
|
492
|
+
async function salvar() {
|
|
493
|
+
if (falta || !provider.createBlock) return;
|
|
494
|
+
setSaving(true);
|
|
495
|
+
try {
|
|
496
|
+
if (editing && provider.deleteBlock) await provider.deleteBlock(editing.ids);
|
|
497
|
+
await provider.createBlock({
|
|
498
|
+
assigneeId,
|
|
499
|
+
title: title.trim(),
|
|
500
|
+
description: description.trim() || void 0,
|
|
501
|
+
startsAt: allDay ? "00:00" : startsAt,
|
|
502
|
+
endsAt: allDay ? "23:59" : endsAt,
|
|
503
|
+
...mode === "single" ? { specificDate } : {
|
|
504
|
+
daysOfWeek: days2,
|
|
505
|
+
validFrom,
|
|
506
|
+
validTo: validTo || null,
|
|
507
|
+
weekInterval: Number(weekInterval)
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
toast.success(editing ? "Bloqueio atualizado" : "Bloqueio salvo");
|
|
511
|
+
onSaved?.();
|
|
512
|
+
onClose();
|
|
513
|
+
} catch (err) {
|
|
514
|
+
toast.error("N\xE3o foi poss\xEDvel salvar o bloqueio", { description: err?.message });
|
|
515
|
+
} finally {
|
|
516
|
+
setSaving(false);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return /* @__PURE__ */ jsx(Modal, { open, onOpenChange: (o) => {
|
|
520
|
+
if (!o) onClose();
|
|
521
|
+
}, children: /* @__PURE__ */ jsxs(ModalContent, { size: "md", children: [
|
|
522
|
+
/* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsxs(ModalTitle, { className: "flex items-center gap-2", children: [
|
|
523
|
+
/* @__PURE__ */ jsx(CalendarOff, { className: "h-4 w-4", "aria-hidden": true }),
|
|
524
|
+
editing ? "Editar bloqueio" : "Novo bloqueio / compromisso"
|
|
525
|
+
] }) }),
|
|
526
|
+
/* @__PURE__ */ jsxs(ModalBody, { className: "space-y-4", children: [
|
|
527
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
528
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: "Profissional *" }),
|
|
529
|
+
/* @__PURE__ */ jsx(
|
|
530
|
+
PersonPicker,
|
|
531
|
+
{
|
|
532
|
+
options: professionals.map((p) => ({ id: p.id, name: p.name, avatarUrl: p.avatarUrl })),
|
|
533
|
+
value: assigneeId,
|
|
534
|
+
onChange: setAssigneeId,
|
|
535
|
+
placeholder: "Selecione o profissional",
|
|
536
|
+
searchPlaceholder: "Buscar profissional\u2026"
|
|
537
|
+
}
|
|
538
|
+
)
|
|
539
|
+
] }),
|
|
540
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
541
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: "T\xEDtulo *" }),
|
|
542
|
+
/* @__PURE__ */ jsx(
|
|
543
|
+
Input,
|
|
544
|
+
{
|
|
545
|
+
value: title,
|
|
546
|
+
onChange: (e) => setTitle(e.target.value),
|
|
547
|
+
placeholder: "Ex.: Almo\xE7o, Curso, Consulta m\xE9dica"
|
|
548
|
+
}
|
|
549
|
+
)
|
|
550
|
+
] }),
|
|
551
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
552
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: "Descri\xE7\xE3o" }),
|
|
553
|
+
/* @__PURE__ */ jsx(
|
|
554
|
+
Textarea,
|
|
555
|
+
{
|
|
556
|
+
value: description,
|
|
557
|
+
onChange: (e) => setDescription(e.target.value),
|
|
558
|
+
rows: 2,
|
|
559
|
+
className: "min-h-0"
|
|
560
|
+
}
|
|
561
|
+
)
|
|
562
|
+
] }),
|
|
563
|
+
/* @__PURE__ */ jsx(Tabs, { value: mode, onValueChange: (v) => setMode(v), children: /* @__PURE__ */ jsxs(TabsList, { className: "w-full", children: [
|
|
564
|
+
/* @__PURE__ */ jsx(TabsTrigger, { value: "single", className: "flex-1", children: "\xDAnico" }),
|
|
565
|
+
/* @__PURE__ */ jsx(TabsTrigger, { value: "recurring", className: "flex-1", children: "Recorrente" })
|
|
566
|
+
] }) }),
|
|
567
|
+
mode === "single" ? /* @__PURE__ */ jsxs("div", { children: [
|
|
568
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: "Data *" }),
|
|
569
|
+
/* @__PURE__ */ jsx(DatePicker, { value: specificDate, format: "numeric", onChange: setSpecificDate })
|
|
570
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
571
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
572
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: "Dias da semana *" }),
|
|
573
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: DIAS.map((d) => {
|
|
574
|
+
const on = days2.includes(d.v);
|
|
575
|
+
return /* @__PURE__ */ jsx(
|
|
576
|
+
Button,
|
|
577
|
+
{
|
|
578
|
+
variant: "bare",
|
|
579
|
+
size: "bare",
|
|
580
|
+
type: "button",
|
|
581
|
+
onClick: () => toggleDay(d.v),
|
|
582
|
+
"aria-pressed": on,
|
|
583
|
+
className: "rounded-button border px-2.5 py-1 text-xs transition-colors " + (on ? "border-primary bg-primary/10 font-medium text-foreground" : "border-input font-normal text-muted-foreground hover:bg-muted"),
|
|
584
|
+
children: d.label
|
|
585
|
+
},
|
|
586
|
+
d.v
|
|
587
|
+
);
|
|
588
|
+
}) })
|
|
589
|
+
] }),
|
|
590
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
|
|
591
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
592
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: "De *" }),
|
|
593
|
+
/* @__PURE__ */ jsx(DatePicker, { value: validFrom, format: "numeric", onChange: setValidFrom })
|
|
594
|
+
] }),
|
|
595
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
596
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: "At\xE9" }),
|
|
597
|
+
/* @__PURE__ */ jsx(DatePicker, { value: validTo, format: "numeric", onChange: setValidTo }),
|
|
598
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-[11px] text-muted-foreground", children: "Vazio = sem fim" })
|
|
599
|
+
] })
|
|
600
|
+
] }),
|
|
601
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
602
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: "Repeti\xE7\xE3o" }),
|
|
603
|
+
/* @__PURE__ */ jsxs(Select, { value: weekInterval, onValueChange: setWeekInterval, children: [
|
|
604
|
+
/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
605
|
+
/* @__PURE__ */ jsx(SelectContent, { children: REPETICAO.map((r) => /* @__PURE__ */ jsx(SelectItem, { value: r.v, children: r.label }, r.v)) })
|
|
606
|
+
] })
|
|
607
|
+
] })
|
|
608
|
+
] }),
|
|
609
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between rounded-lg border px-3 py-2", children: [
|
|
610
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm", children: "Dia inteiro" }),
|
|
611
|
+
/* @__PURE__ */ jsx(Switch, { size: "sm", checked: allDay, onCheckedChange: setAllDay })
|
|
612
|
+
] }),
|
|
613
|
+
!allDay && /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
|
|
614
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
615
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: "In\xEDcio *" }),
|
|
616
|
+
/* @__PURE__ */ jsx(TimePicker, { value: startsAt, onChange: setStartsAt, interval: 15 })
|
|
617
|
+
] }),
|
|
618
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
619
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: "Fim *" }),
|
|
620
|
+
/* @__PURE__ */ jsx(TimePicker, { value: endsAt, onChange: setEndsAt, interval: 15, min: startsAt })
|
|
621
|
+
] })
|
|
622
|
+
] })
|
|
623
|
+
] }),
|
|
624
|
+
/* @__PURE__ */ jsxs(ModalFooter, { className: "items-center", children: [
|
|
625
|
+
editing && /* @__PURE__ */ jsx(
|
|
626
|
+
Button,
|
|
627
|
+
{
|
|
628
|
+
variant: "bare",
|
|
629
|
+
size: "bare",
|
|
630
|
+
type: "button",
|
|
631
|
+
disabled: saving,
|
|
632
|
+
onClick: async () => {
|
|
633
|
+
if (!provider.deleteBlock) return;
|
|
634
|
+
setSaving(true);
|
|
635
|
+
try {
|
|
636
|
+
await provider.deleteBlock(editing.ids);
|
|
637
|
+
toast.success(editing.ids.length > 1 ? `Bloqueio removido de ${editing.ids.length} dias` : "Bloqueio removido");
|
|
638
|
+
onSaved?.();
|
|
639
|
+
onClose();
|
|
640
|
+
} catch (err) {
|
|
641
|
+
toast.error("N\xE3o foi poss\xEDvel remover o bloqueio", { description: err?.message });
|
|
642
|
+
} finally {
|
|
643
|
+
setSaving(false);
|
|
644
|
+
}
|
|
645
|
+
},
|
|
646
|
+
className: "mr-auto text-xs font-medium text-destructive hover:underline disabled:opacity-60",
|
|
647
|
+
children: "Excluir bloqueio"
|
|
648
|
+
}
|
|
649
|
+
),
|
|
650
|
+
falta && /* @__PURE__ */ jsx("span", { className: "mr-auto text-xs text-muted-foreground", children: falta }),
|
|
651
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: onClose, children: "Cancelar" }),
|
|
652
|
+
/* @__PURE__ */ jsx(Button, { onClick: salvar, disabled: !!falta || saving, children: saving ? "Salvando\u2026" : editing ? "Salvar altera\xE7\xF5es" : "Salvar bloqueio" })
|
|
653
|
+
] })
|
|
654
|
+
] }) });
|
|
655
|
+
}
|
|
656
|
+
function LostSlotsCard({ range, selectedStatuses, onPick }) {
|
|
657
|
+
const provider = useAgendaProvider();
|
|
658
|
+
const [counts, setCounts] = React.useState(null);
|
|
659
|
+
const [falhou, setFalhou] = React.useState(false);
|
|
660
|
+
const key = range ? `${range.start}|${range.end}` : null;
|
|
661
|
+
React.useEffect(() => {
|
|
662
|
+
if (!range || !provider.getLostCounts) {
|
|
663
|
+
setCounts(null);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
let cancelled = false;
|
|
667
|
+
setCounts(null);
|
|
668
|
+
setFalhou(false);
|
|
669
|
+
provider.getLostCounts(range).then((c) => {
|
|
670
|
+
if (!cancelled) setCounts(c);
|
|
671
|
+
}).catch(() => {
|
|
672
|
+
if (!cancelled) setFalhou(true);
|
|
673
|
+
});
|
|
674
|
+
return () => {
|
|
675
|
+
cancelled = true;
|
|
676
|
+
};
|
|
677
|
+
}, [key, provider]);
|
|
678
|
+
if (falhou) return null;
|
|
679
|
+
if (!provider.getLostCounts) return null;
|
|
680
|
+
if (counts === null) {
|
|
681
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5 py-1", children: [
|
|
682
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-24" }),
|
|
683
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-7 w-full" }),
|
|
684
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-7 w-full" })
|
|
685
|
+
] });
|
|
686
|
+
}
|
|
687
|
+
if (counts.cancelled === 0 && counts.noShow === 0) return null;
|
|
688
|
+
const linhas = [
|
|
689
|
+
{ status: "cancelled", label: "Cancelados", n: counts.cancelled, Icon: CalendarX, tone: "text-destructive" },
|
|
690
|
+
{ status: "no_show", label: "N\xE3o compareceu", n: counts.noShow, Icon: UserX, tone: "text-muted-foreground" }
|
|
691
|
+
].filter((l) => l.n > 0);
|
|
692
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
693
|
+
/* @__PURE__ */ jsx("p", { className: "px-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: "Perdidos no per\xEDodo" }),
|
|
694
|
+
linhas.map(({ status, label, n, Icon, tone }) => {
|
|
695
|
+
const on = selectedStatuses.includes(status);
|
|
696
|
+
return /* @__PURE__ */ jsxs(
|
|
697
|
+
Button,
|
|
698
|
+
{
|
|
699
|
+
variant: "bare",
|
|
700
|
+
size: "bare",
|
|
701
|
+
type: "button",
|
|
702
|
+
onClick: () => onPick(status),
|
|
703
|
+
"aria-pressed": on,
|
|
704
|
+
className: "flex w-full items-center justify-start gap-2 rounded-md px-1.5 py-1.5 text-left text-xs transition-colors " + (on ? "bg-muted font-medium text-foreground" : "font-normal text-muted-foreground hover:bg-muted/50"),
|
|
705
|
+
children: [
|
|
706
|
+
/* @__PURE__ */ jsx(Icon, { className: `h-3.5 w-3.5 shrink-0 ${tone}`, "aria-hidden": true }),
|
|
707
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: label }),
|
|
708
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 tabular-nums font-semibold text-foreground", children: n })
|
|
709
|
+
]
|
|
710
|
+
},
|
|
711
|
+
status
|
|
712
|
+
);
|
|
713
|
+
})
|
|
714
|
+
] });
|
|
715
|
+
}
|
|
716
|
+
function ScheduleGridDialog({ open, onClose, professionalId }) {
|
|
717
|
+
const t = useTranslation();
|
|
718
|
+
const professionals = useAgendaStore((s) => s.professionals);
|
|
719
|
+
const invalidateSchedules = useAgendaStore((s) => s.invalidateSchedules);
|
|
720
|
+
const [search, setSearch] = React.useState("");
|
|
721
|
+
const [selectedId, setSelectedId] = React.useState(null);
|
|
722
|
+
React.useEffect(() => {
|
|
723
|
+
if (!open) return;
|
|
724
|
+
setSelectedId((current) => professionalId ?? current ?? professionals[0]?.id ?? null);
|
|
725
|
+
}, [open, professionalId, professionals]);
|
|
726
|
+
const selectedRef = React.useRef(null);
|
|
727
|
+
React.useEffect(() => {
|
|
728
|
+
if (!open || !selectedId) return;
|
|
729
|
+
const id = window.setTimeout(() => {
|
|
730
|
+
selectedRef.current?.scrollIntoView({ block: "nearest" });
|
|
731
|
+
}, 0);
|
|
732
|
+
return () => window.clearTimeout(id);
|
|
733
|
+
}, [open, selectedId]);
|
|
734
|
+
const term = search.trim().toLowerCase();
|
|
735
|
+
const shown = term ? professionals.filter((p) => p.name.toLowerCase().includes(term)) : professionals;
|
|
736
|
+
return /* @__PURE__ */ jsx(Modal, { open, onOpenChange: (o) => {
|
|
737
|
+
if (!o) onClose();
|
|
738
|
+
}, children: /* @__PURE__ */ jsxs(ModalContent, { size: "full", noPadding: true, className: "gap-0", children: [
|
|
739
|
+
/* @__PURE__ */ jsx(ModalHeader, { className: "border-b px-5 py-3.5", children: /* @__PURE__ */ jsx(ModalTitle, { className: "text-base", children: t("agenda.schedules.title") }) }),
|
|
740
|
+
/* @__PURE__ */ jsxs(ModalBody, { className: "flex flex-col overflow-hidden p-0 sm:h-full sm:max-h-[78vh] sm:flex-row", children: [
|
|
741
|
+
/* @__PURE__ */ jsxs("div", { className: "flex max-h-44 shrink-0 flex-col border-b sm:max-h-none sm:w-56 sm:border-b-0 sm:border-r", children: [
|
|
742
|
+
/* @__PURE__ */ jsxs("div", { className: "relative p-2", children: [
|
|
743
|
+
/* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-4 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }),
|
|
744
|
+
/* @__PURE__ */ jsx(
|
|
745
|
+
Input,
|
|
746
|
+
{
|
|
747
|
+
type: "search",
|
|
748
|
+
value: search,
|
|
749
|
+
onChange: (e) => setSearch(e.target.value),
|
|
750
|
+
placeholder: t("agenda.schedules.searchProfessional"),
|
|
751
|
+
className: "h-auto rounded-md border-border bg-background py-1.5 pl-7 pr-2 text-xs shadow-none focus:border-primary focus-visible:ring-0 focus-visible:ring-offset-0"
|
|
752
|
+
}
|
|
753
|
+
)
|
|
754
|
+
] }),
|
|
755
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto pb-2", children: [
|
|
756
|
+
shown.map((prof) => /* @__PURE__ */ jsxs(
|
|
757
|
+
Button,
|
|
758
|
+
{
|
|
759
|
+
variant: "bare",
|
|
760
|
+
size: "bare",
|
|
761
|
+
ref: selectedId === prof.id ? selectedRef : void 0,
|
|
762
|
+
type: "button",
|
|
763
|
+
onClick: () => setSelectedId(prof.id),
|
|
764
|
+
className: (
|
|
765
|
+
// Selecionado com o fundo de ativo da casa e barra à
|
|
766
|
+
// esquerda: numa lista de 277 nomes, um cinza a mais que o
|
|
767
|
+
// vizinho não diz de quem é a grade aberta ao lado.
|
|
768
|
+
//
|
|
769
|
+
// O NOME fica em `text-foreground`, não na cor primária: o
|
|
770
|
+
// primário desta marca é um dourado claro, e dourado sobre
|
|
771
|
+
// fundo claro é o texto que ninguém lê. A cor identifica a
|
|
772
|
+
// linha (fundo e barra); quem carrega a informação é o
|
|
773
|
+
// nome, e ele precisa de contraste.
|
|
774
|
+
"flex w-full items-center justify-start gap-2 border-l-2 px-3 py-2 text-left text-sm transition-colors " + (selectedId === prof.id ? "border-primary bg-primary/10 font-semibold text-foreground" : "border-transparent font-normal text-muted-foreground hover:bg-muted/50 hover:text-foreground")
|
|
775
|
+
),
|
|
776
|
+
children: [
|
|
777
|
+
/* @__PURE__ */ jsx(PersonAvatar, { name: prof.name, src: prof.avatarUrl, size: "sm" }),
|
|
778
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: prof.name })
|
|
779
|
+
]
|
|
780
|
+
},
|
|
781
|
+
prof.id
|
|
782
|
+
)),
|
|
783
|
+
shown.length === 0 && /* @__PURE__ */ jsx("p", { className: "px-3 py-2 text-xs text-muted-foreground", children: "\u2014" })
|
|
784
|
+
] })
|
|
785
|
+
] }),
|
|
786
|
+
/* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1 overflow-y-auto px-4 pt-4", children: selectedId ? /* @__PURE__ */ jsx(ScheduleEditor, { assigneeId: selectedId, onSaved: invalidateSchedules }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("agenda.schedules.pickProfessional") }) })
|
|
787
|
+
] })
|
|
788
|
+
] }) });
|
|
789
|
+
}
|
|
790
|
+
var STYLE_ID2 = "saas-floating-panel";
|
|
791
|
+
if (typeof document !== "undefined" && !document.getElementById(STYLE_ID2)) {
|
|
792
|
+
const s = document.createElement("style");
|
|
793
|
+
s.id = STYLE_ID2;
|
|
794
|
+
s.textContent = `
|
|
795
|
+
@keyframes _fp-dismiss { from { opacity:1; transform:scale(1) } to { opacity:0; transform:scale(.96) } }
|
|
796
|
+
@keyframes _fp-expand { from { opacity:1; transform:scale(1) } to { opacity:0; transform:scale(1.08) } }
|
|
797
|
+
.saas-fp-dismissing { animation: _fp-dismiss 150ms ease-in forwards; pointer-events: none }
|
|
798
|
+
.saas-fp-expanding { animation: _fp-expand 200ms ease-in forwards; pointer-events: none }
|
|
799
|
+
`;
|
|
800
|
+
document.head.appendChild(s);
|
|
801
|
+
}
|
|
802
|
+
function FloatingPanel({
|
|
803
|
+
position,
|
|
804
|
+
width,
|
|
805
|
+
height,
|
|
806
|
+
offset,
|
|
807
|
+
onClose,
|
|
808
|
+
className,
|
|
809
|
+
children,
|
|
810
|
+
zIndex = 100
|
|
811
|
+
}) {
|
|
812
|
+
const ref = useRef(null);
|
|
813
|
+
const [phase, setPhase] = useState("visible");
|
|
814
|
+
const phaseRef = useRef("visible");
|
|
815
|
+
phaseRef.current = phase;
|
|
816
|
+
const onCloseRef = useRef(onClose);
|
|
817
|
+
onCloseRef.current = onClose;
|
|
818
|
+
const expandCallbackRef = useRef(null);
|
|
819
|
+
const dismiss = useCallback(() => {
|
|
820
|
+
if (phaseRef.current !== "visible") return;
|
|
821
|
+
setPhase("dismissing");
|
|
822
|
+
setTimeout(() => onCloseRef.current(), 150);
|
|
823
|
+
}, []);
|
|
824
|
+
const expandThen = useCallback((callback) => {
|
|
825
|
+
if (phaseRef.current !== "visible") return;
|
|
826
|
+
expandCallbackRef.current = callback;
|
|
827
|
+
setPhase("expanding");
|
|
828
|
+
setTimeout(() => {
|
|
829
|
+
callback();
|
|
830
|
+
onCloseRef.current();
|
|
831
|
+
}, 220);
|
|
832
|
+
}, []);
|
|
833
|
+
useEffect(() => {
|
|
834
|
+
function handleClick(e) {
|
|
835
|
+
if (phaseRef.current !== "visible") return;
|
|
836
|
+
if (ref.current && !ref.current.contains(e.target)) {
|
|
837
|
+
const target = e.target;
|
|
838
|
+
if (target.closest(".fc-event")) return;
|
|
839
|
+
dismiss();
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
function handleKey(e) {
|
|
843
|
+
if (e.key === "Escape") dismiss();
|
|
844
|
+
}
|
|
845
|
+
document.addEventListener("mousedown", handleClick);
|
|
846
|
+
document.addEventListener("keydown", handleKey);
|
|
847
|
+
return () => {
|
|
848
|
+
document.removeEventListener("mousedown", handleClick);
|
|
849
|
+
document.removeEventListener("keydown", handleKey);
|
|
850
|
+
};
|
|
851
|
+
}, [dismiss]);
|
|
852
|
+
const ox = offset?.x ?? 0;
|
|
853
|
+
const oy = offset?.y ?? 0;
|
|
854
|
+
let x = position.x + ox;
|
|
855
|
+
let y = position.y + oy;
|
|
856
|
+
if (x + width > window.innerWidth - 12) x = x - width - ox * 2;
|
|
857
|
+
if (y + height > window.innerHeight - 12) y = y - height - oy * 2;
|
|
858
|
+
if (x < 12) x = 12;
|
|
859
|
+
if (y < 12) y = 12;
|
|
860
|
+
const panelRef = { dismiss, expandThen };
|
|
861
|
+
return createPortal(
|
|
862
|
+
/* @__PURE__ */ jsx(
|
|
863
|
+
"div",
|
|
864
|
+
{
|
|
865
|
+
ref,
|
|
866
|
+
style: { position: "fixed", left: x, top: y, zIndex, transition: phase === "visible" ? "left 200ms ease-out, top 200ms ease-out" : void 0 },
|
|
867
|
+
className: `fayz-glass-surface rounded-modal border bg-popover shadow-lg overflow-hidden ${phase === "expanding" ? "saas-fp-expanding" : phase === "dismissing" ? "saas-fp-dismissing" : "animate-in fade-in zoom-in-95 duration-150"} ${className ?? ""}`,
|
|
868
|
+
children: children(panelRef)
|
|
869
|
+
}
|
|
870
|
+
),
|
|
871
|
+
document.body
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
function useSeriesDelete(booking, enabled, onDone) {
|
|
875
|
+
const provider = useAgendaProvider();
|
|
876
|
+
const deleteBooking = useAgendaStore((s) => s.deleteBooking);
|
|
877
|
+
const deleteBookingSeries = useAgendaStore((s) => s.deleteBookingSeries);
|
|
878
|
+
const [seriesSize, setSeriesSize] = useState(null);
|
|
879
|
+
const [deleting, setDeleting] = useState(null);
|
|
880
|
+
const seriesId = booking.seriesId ?? null;
|
|
881
|
+
useEffect(() => {
|
|
882
|
+
if (!enabled || !seriesId || !provider.countSeriesOccurrences) return;
|
|
883
|
+
let alive = true;
|
|
884
|
+
provider.countSeriesOccurrences(seriesId).then((n) => {
|
|
885
|
+
if (alive) setSeriesSize(n);
|
|
886
|
+
}).catch(() => {
|
|
887
|
+
if (alive) setSeriesSize(0);
|
|
888
|
+
});
|
|
889
|
+
return () => {
|
|
890
|
+
alive = false;
|
|
891
|
+
};
|
|
892
|
+
}, [enabled, seriesId, provider]);
|
|
893
|
+
const isSeries = Boolean(seriesId) && seriesSize !== 1 && seriesSize !== 0;
|
|
894
|
+
async function remove(scope) {
|
|
895
|
+
setDeleting(scope);
|
|
896
|
+
try {
|
|
897
|
+
if (scope === "series" && seriesId) await deleteBookingSeries(seriesId);
|
|
898
|
+
else await deleteBooking(booking.id);
|
|
899
|
+
onDone();
|
|
900
|
+
} catch {
|
|
901
|
+
setDeleting(null);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return { isSeries, seriesSize, deleting, remove };
|
|
905
|
+
}
|
|
906
|
+
function SeriesDeleteChoice({ seriesSize, deleting, onPick, onCancel }) {
|
|
907
|
+
const t = useTranslation();
|
|
908
|
+
const allLabel = t("agenda.recurrence.scopeSeriesShort").replace("{{count}}", seriesSize ? String(seriesSize) : "\u2026");
|
|
909
|
+
const btn = "flex-1 min-w-0 whitespace-normal rounded border border-destructive/30 px-2 py-1 text-center text-[11px] font-medium text-destructive hover:bg-destructive/10 disabled:opacity-40 transition-colors";
|
|
910
|
+
return /* @__PURE__ */ jsxs("div", { className: "px-3 py-2 space-y-1.5", children: [
|
|
911
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
912
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-destructive", children: t("agenda.recurrence.scopeDeleteTitle") }),
|
|
913
|
+
/* @__PURE__ */ jsx(
|
|
914
|
+
Button,
|
|
915
|
+
{
|
|
916
|
+
variant: "bare",
|
|
917
|
+
size: "bare",
|
|
918
|
+
type: "button",
|
|
919
|
+
onClick: onCancel,
|
|
920
|
+
disabled: deleting !== null,
|
|
921
|
+
className: "rounded px-1.5 py-0.5 text-[11px] font-medium text-muted-foreground hover:bg-muted/50 transition-colors",
|
|
922
|
+
children: t("agenda.appointment.cancel")
|
|
923
|
+
}
|
|
924
|
+
)
|
|
925
|
+
] }),
|
|
926
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-stretch gap-1.5", children: [
|
|
927
|
+
/* @__PURE__ */ jsx(Button, { variant: "bare", size: "bare", type: "button", className: btn, disabled: deleting !== null, onClick: () => onPick("this"), children: deleting === "this" ? t("agenda.appointment.deleting") : t("agenda.recurrence.scopeThisShort") }),
|
|
928
|
+
/* @__PURE__ */ jsx(Button, { variant: "bare", size: "bare", type: "button", className: btn, disabled: deleting !== null, onClick: () => onPick("series"), children: deleting === "series" ? t("agenda.appointment.deleting") : allLabel })
|
|
929
|
+
] })
|
|
930
|
+
] });
|
|
931
|
+
}
|
|
932
|
+
function fmtTime(iso) {
|
|
933
|
+
return new Date(iso).toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit" });
|
|
934
|
+
}
|
|
935
|
+
function fmtDate(iso) {
|
|
936
|
+
return new Date(iso).toLocaleDateString(void 0, { weekday: "long", month: "long", day: "numeric" });
|
|
937
|
+
}
|
|
938
|
+
function PopoverStatusSelect({ value, statuses, bookingStartsAt, onChange }) {
|
|
939
|
+
const [open, setOpen] = useState(false);
|
|
940
|
+
const ref = useRef(null);
|
|
941
|
+
const current = statuses.find((s) => s.value === value);
|
|
942
|
+
useEffect(() => {
|
|
943
|
+
if (!open) return;
|
|
944
|
+
const h = (e) => {
|
|
945
|
+
if (ref.current && !ref.current.contains(e.target)) setOpen(false);
|
|
946
|
+
};
|
|
947
|
+
document.addEventListener("mousedown", h);
|
|
948
|
+
return () => document.removeEventListener("mousedown", h);
|
|
949
|
+
}, [open]);
|
|
950
|
+
return /* @__PURE__ */ jsxs("div", { ref, className: "relative inline-block", children: [
|
|
951
|
+
/* @__PURE__ */ jsxs(
|
|
952
|
+
Button,
|
|
953
|
+
{
|
|
954
|
+
variant: "bare",
|
|
955
|
+
size: "bare",
|
|
956
|
+
type: "button",
|
|
957
|
+
onClick: () => setOpen((p) => !p),
|
|
958
|
+
className: "inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium transition-colors hover:opacity-80",
|
|
959
|
+
style: { backgroundColor: (current?.color ?? "#6b7280") + "18", color: current?.color ?? "#6b7280" },
|
|
960
|
+
children: [
|
|
961
|
+
/* @__PURE__ */ jsx("span", { className: "h-2 w-2 rounded-full", style: { backgroundColor: current?.color } }),
|
|
962
|
+
current?.label ?? value,
|
|
963
|
+
/* @__PURE__ */ jsx("svg", { className: "h-3 w-3 opacity-50", viewBox: "0 0 16 16", fill: "none", children: /* @__PURE__ */ jsx("path", { d: "M4 6l4 4 4-4", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) })
|
|
964
|
+
]
|
|
965
|
+
}
|
|
966
|
+
),
|
|
967
|
+
open && /* @__PURE__ */ jsx("div", { className: "fayz-glass-surface absolute bottom-full left-0 z-50 mb-1 rounded-lg border bg-popover shadow-lg py-1 animate-in fade-in zoom-in-95 duration-100 min-w-[140px]", children: statuses.map((s) => {
|
|
968
|
+
const available = isStatusAvailable(s, bookingStartsAt);
|
|
969
|
+
return /* @__PURE__ */ jsxs(
|
|
970
|
+
Button,
|
|
971
|
+
{
|
|
972
|
+
variant: "bare",
|
|
973
|
+
size: "bare",
|
|
974
|
+
type: "button",
|
|
975
|
+
disabled: !available,
|
|
976
|
+
onClick: () => {
|
|
977
|
+
if (available) {
|
|
978
|
+
onChange(s.value);
|
|
979
|
+
setOpen(false);
|
|
980
|
+
}
|
|
981
|
+
},
|
|
982
|
+
className: `font-normal justify-start whitespace-normal flex w-full items-center gap-2 px-3 py-1.5 text-xs transition-colors ${!available ? "disabled:pointer-events-auto disabled:cursor-not-allowed disabled:opacity-30" : s.value === value ? "bg-muted/30" : "hover:bg-muted/50"}`,
|
|
983
|
+
children: [
|
|
984
|
+
/* @__PURE__ */ jsx("span", { className: "h-2 w-2 rounded-full shrink-0", style: { backgroundColor: s.color } }),
|
|
985
|
+
/* @__PURE__ */ jsx("span", { children: s.label })
|
|
986
|
+
]
|
|
987
|
+
},
|
|
988
|
+
s.value
|
|
989
|
+
);
|
|
990
|
+
}) })
|
|
991
|
+
] });
|
|
992
|
+
}
|
|
993
|
+
function AppointmentPopover({ booking, position, onClose, onEdit }) {
|
|
994
|
+
const t = useTranslation();
|
|
995
|
+
const config = useAgendaConfig();
|
|
996
|
+
const updateStatus = useAgendaStore((s) => s.updateBookingStatus);
|
|
997
|
+
const openModal = useAgendaStore((s) => s.openAppointmentModal);
|
|
998
|
+
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
999
|
+
const series = useSeriesDelete(booking, confirmingDelete, onClose);
|
|
1000
|
+
const deleting = series.deleting !== null;
|
|
1001
|
+
const statusConfig = config.statuses.find((s) => s.value === booking.status);
|
|
1002
|
+
const color = statusConfig?.color ?? "#6b7280";
|
|
1003
|
+
const timeRange = `${fmtTime(booking.startsAt)}${booking.endsAt ? " \u2013 " + fmtTime(booking.endsAt) : ""}`;
|
|
1004
|
+
return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(FloatingPanel, { position, width: 320, height: 340, onClose, children: (panel) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1005
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center justify-end gap-0.5 px-3 pt-3 pb-1", children: confirmingDelete && series.isSeries ? /* @__PURE__ */ jsx("div", { className: "w-full", children: /* @__PURE__ */ jsx(
|
|
1006
|
+
SeriesDeleteChoice,
|
|
1007
|
+
{
|
|
1008
|
+
seriesSize: series.seriesSize,
|
|
1009
|
+
deleting: series.deleting,
|
|
1010
|
+
onPick: (scope) => {
|
|
1011
|
+
void series.remove(scope);
|
|
1012
|
+
},
|
|
1013
|
+
onCancel: () => setConfirmingDelete(false)
|
|
1014
|
+
}
|
|
1015
|
+
) }) : confirmingDelete ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1016
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-destructive font-medium mr-auto pl-1", children: t("agenda.appointment.deleteConfirm") }),
|
|
1017
|
+
/* @__PURE__ */ jsx(
|
|
1018
|
+
Button,
|
|
1019
|
+
{
|
|
1020
|
+
variant: "bare",
|
|
1021
|
+
size: "bare",
|
|
1022
|
+
onClick: () => setConfirmingDelete(false),
|
|
1023
|
+
className: "rounded px-2 py-0.5 text-[11px] font-medium hover:bg-muted/50 transition-colors",
|
|
1024
|
+
children: t("agenda.appointment.cancel")
|
|
1025
|
+
}
|
|
1026
|
+
),
|
|
1027
|
+
/* @__PURE__ */ jsx(
|
|
1028
|
+
Button,
|
|
1029
|
+
{
|
|
1030
|
+
variant: "bare",
|
|
1031
|
+
size: "bare",
|
|
1032
|
+
onClick: () => {
|
|
1033
|
+
void series.remove("this");
|
|
1034
|
+
},
|
|
1035
|
+
disabled: deleting,
|
|
1036
|
+
className: "rounded bg-destructive border border-destructive px-2 py-0.5 text-[11px] font-medium text-destructive-foreground hover:bg-destructive/90 shadow-button-primary active:shadow-button-inset disabled:opacity-40 transition-colors",
|
|
1037
|
+
children: deleting ? t("agenda.appointment.deleting") : t("agenda.appointment.confirmDelete")
|
|
1038
|
+
}
|
|
1039
|
+
)
|
|
1040
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1041
|
+
/* @__PURE__ */ jsx(
|
|
1042
|
+
Button,
|
|
1043
|
+
{
|
|
1044
|
+
variant: "bare",
|
|
1045
|
+
size: "bare",
|
|
1046
|
+
onClick: () => panel.expandThen(() => onEdit(booking.id)),
|
|
1047
|
+
className: "p-1.5 rounded-full hover:bg-muted transition-colors",
|
|
1048
|
+
title: t("common.edit"),
|
|
1049
|
+
children: /* @__PURE__ */ jsx(Pencil, { className: "h-4 w-4 text-muted-foreground" })
|
|
1050
|
+
}
|
|
1051
|
+
),
|
|
1052
|
+
/* @__PURE__ */ jsx(
|
|
1053
|
+
Button,
|
|
1054
|
+
{
|
|
1055
|
+
variant: "bare",
|
|
1056
|
+
size: "bare",
|
|
1057
|
+
onClick: () => setConfirmingDelete(true),
|
|
1058
|
+
className: "p-1.5 rounded-full hover:bg-destructive/10 transition-colors",
|
|
1059
|
+
title: t("common.delete"),
|
|
1060
|
+
children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4 text-muted-foreground hover:text-destructive" })
|
|
1061
|
+
}
|
|
1062
|
+
),
|
|
1063
|
+
/* @__PURE__ */ jsx(Button, { variant: "bare", size: "bare", onClick: panel.dismiss, className: "p-1.5 rounded-full hover:bg-muted transition-colors", children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4 text-muted-foreground" }) })
|
|
1064
|
+
] }) }),
|
|
1065
|
+
/* @__PURE__ */ jsxs("div", { className: "px-5 pb-5 space-y-3", children: [
|
|
1066
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
|
|
1067
|
+
/* @__PURE__ */ jsx("span", { className: "mt-1.5 h-3 w-3 rounded shrink-0", style: { backgroundColor: color } }),
|
|
1068
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
1069
|
+
config.capabilities.client ? /* @__PURE__ */ jsx(PersonLink, { personId: booking.clientId, name: booking.clientName ?? "Unknown", className: "text-base" }) : /* @__PURE__ */ jsx("p", { className: "text-base font-medium truncate", children: booking.clientName || t("agenda.appointment.untitled") }),
|
|
1070
|
+
/* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground mt-0.5", children: [
|
|
1071
|
+
fmtDate(booking.startsAt),
|
|
1072
|
+
" \xB7 ",
|
|
1073
|
+
timeRange
|
|
1074
|
+
] })
|
|
1075
|
+
] })
|
|
1076
|
+
] }),
|
|
1077
|
+
booking.services && booking.services.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
|
|
1078
|
+
/* @__PURE__ */ jsx(Clock, { className: "h-4 w-4 mt-0.5 text-muted-foreground shrink-0" }),
|
|
1079
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm space-y-0.5", children: booking.services.map((svc) => /* @__PURE__ */ jsxs("p", { children: [
|
|
1080
|
+
svc.name,
|
|
1081
|
+
" ",
|
|
1082
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
1083
|
+
"(",
|
|
1084
|
+
svc.durationMinutes,
|
|
1085
|
+
"min)"
|
|
1086
|
+
] })
|
|
1087
|
+
] }, svc.id)) })
|
|
1088
|
+
] }),
|
|
1089
|
+
config.capabilities.professional && booking.professionalName && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1090
|
+
/* @__PURE__ */ jsx(User, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
1091
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm", children: booking.professionalName })
|
|
1092
|
+
] }),
|
|
1093
|
+
booking.locationName && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1094
|
+
/* @__PURE__ */ jsx(MapPin, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
1095
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm", children: booking.locationName })
|
|
1096
|
+
] }),
|
|
1097
|
+
booking.notes && /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
|
|
1098
|
+
/* @__PURE__ */ jsx(FileText, { className: "h-4 w-4 mt-0.5 text-muted-foreground shrink-0" }),
|
|
1099
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: booking.notes })
|
|
1100
|
+
] }),
|
|
1101
|
+
booking.orderTotal != null && booking.orderTotal > 0 && (() => {
|
|
1102
|
+
const total = formatCurrency(booking.orderTotal, config.currency.code);
|
|
1103
|
+
const ps = booking.paymentStatus ?? "none";
|
|
1104
|
+
const hasFinancial = config.modules.financial && booking.orderId;
|
|
1105
|
+
if (!hasFinancial) {
|
|
1106
|
+
return /* @__PURE__ */ jsxs("div", { className: "pt-2 border-t fayz-glass-divider flex items-center justify-between text-sm", children: [
|
|
1107
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("agenda.appointment.total") }),
|
|
1108
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: total })
|
|
1109
|
+
] });
|
|
1110
|
+
}
|
|
1111
|
+
const badgeConfig = {
|
|
1112
|
+
none: { icon: HandCoins, bg: "bg-warning/10 hover:bg-warning/15", text: "text-warning", label: `${total} \xB7 ${t("agenda.payment.collect")}` },
|
|
1113
|
+
pending: { icon: Clock, bg: "bg-warning/10 hover:bg-warning/15", text: "text-warning", label: `${total} \xB7 ${t("agenda.payment.pending")}` },
|
|
1114
|
+
partial: { icon: DollarSign, bg: "bg-warning/10 hover:bg-warning/15", text: "text-warning", label: `${total} \xB7 ${t("agenda.payment.partial")}` },
|
|
1115
|
+
paid: { icon: Check, bg: "bg-success/10 hover:bg-success/15", text: "text-success", label: `${total} \xB7 ${t("agenda.payment.paid")}` },
|
|
1116
|
+
overdue: { icon: AlertTriangle, bg: "bg-destructive/10 hover:bg-destructive/15", text: "text-destructive", label: `${total} \xB7 ${t("agenda.payment.overdue")}` },
|
|
1117
|
+
cancelled: { icon: X, bg: "bg-muted hover:bg-muted/80", text: "text-muted-foreground", label: `${total} \xB7 ${t("agenda.payment.cancelled")}` }
|
|
1118
|
+
};
|
|
1119
|
+
const cfg = badgeConfig[ps] ?? badgeConfig.none;
|
|
1120
|
+
const StatusIcon = cfg.icon;
|
|
1121
|
+
return /* @__PURE__ */ jsxs("div", { className: "pt-2 border-t fayz-glass-divider flex items-center justify-between", children: [
|
|
1122
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("agenda.appointment.total") }),
|
|
1123
|
+
/* @__PURE__ */ jsxs(
|
|
1124
|
+
Button,
|
|
1125
|
+
{
|
|
1126
|
+
variant: "bare",
|
|
1127
|
+
size: "bare",
|
|
1128
|
+
type: "button",
|
|
1129
|
+
onClick: () => {
|
|
1130
|
+
onClose();
|
|
1131
|
+
openModal("edit", { bookingId: booking.id, initialTab: "financial" });
|
|
1132
|
+
},
|
|
1133
|
+
className: `inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-medium transition-colors ${cfg.bg} ${cfg.text}`,
|
|
1134
|
+
children: [
|
|
1135
|
+
/* @__PURE__ */ jsx(StatusIcon, { className: "h-3 w-3" }),
|
|
1136
|
+
cfg.label
|
|
1137
|
+
]
|
|
1138
|
+
}
|
|
1139
|
+
)
|
|
1140
|
+
] });
|
|
1141
|
+
})(),
|
|
1142
|
+
config.capabilities.status && /* @__PURE__ */ jsx("div", { className: "pt-2 border-t fayz-glass-divider", children: /* @__PURE__ */ jsx(
|
|
1143
|
+
PopoverStatusSelect,
|
|
1144
|
+
{
|
|
1145
|
+
value: booking.status,
|
|
1146
|
+
statuses: config.statuses,
|
|
1147
|
+
bookingStartsAt: booking.startsAt,
|
|
1148
|
+
onChange: (v) => {
|
|
1149
|
+
updateStatus(booking.id, v);
|
|
1150
|
+
onClose();
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
) })
|
|
1154
|
+
] })
|
|
1155
|
+
] }) }) });
|
|
1156
|
+
}
|
|
1157
|
+
function Tooltip({ anchor, label }) {
|
|
1158
|
+
return createPortal(
|
|
1159
|
+
/* @__PURE__ */ jsx(
|
|
1160
|
+
"div",
|
|
1161
|
+
{
|
|
1162
|
+
style: { position: "fixed", left: anchor.left + anchor.width / 2, top: anchor.bottom + 6, transform: "translateX(-50%)", zIndex: 120 },
|
|
1163
|
+
className: "pointer-events-none",
|
|
1164
|
+
children: /* @__PURE__ */ jsx("div", { className: "rounded-md bg-foreground text-background px-2 py-0.5 text-[11px] font-medium whitespace-nowrap shadow-lg", children: label })
|
|
1165
|
+
}
|
|
1166
|
+
),
|
|
1167
|
+
document.body
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
function StatusDot({ color, label, isActive, disabled, onClick }) {
|
|
1171
|
+
const [hover, setHover] = useState(false);
|
|
1172
|
+
const btnRef = useRef(null);
|
|
1173
|
+
const [rect, setRect] = useState(null);
|
|
1174
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1175
|
+
/* @__PURE__ */ jsx(
|
|
1176
|
+
Button,
|
|
1177
|
+
{
|
|
1178
|
+
variant: "bare",
|
|
1179
|
+
size: "bare",
|
|
1180
|
+
ref: btnRef,
|
|
1181
|
+
type: "button",
|
|
1182
|
+
onClick: disabled ? void 0 : onClick,
|
|
1183
|
+
disabled,
|
|
1184
|
+
onMouseEnter: () => {
|
|
1185
|
+
setHover(true);
|
|
1186
|
+
if (btnRef.current) setRect(btnRef.current.getBoundingClientRect());
|
|
1187
|
+
},
|
|
1188
|
+
onMouseLeave: () => setHover(false),
|
|
1189
|
+
className: `flex h-7 w-7 items-center justify-center rounded-full transition-all ${disabled ? "disabled:pointer-events-auto disabled:cursor-not-allowed disabled:opacity-25" : isActive ? "ring-2 ring-offset-2 ring-offset-popover" : "hover:scale-110"}`,
|
|
1190
|
+
style: { backgroundColor: color, ["--tw-ring-color"]: isActive ? color : void 0 },
|
|
1191
|
+
children: isActive && /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5 text-white", strokeWidth: 2.5 })
|
|
1192
|
+
}
|
|
1193
|
+
),
|
|
1194
|
+
hover && rect && /* @__PURE__ */ jsx(Tooltip, { anchor: rect, label })
|
|
1195
|
+
] });
|
|
1196
|
+
}
|
|
1197
|
+
function EventContextMenu({ booking, position, onClose, onEdit }) {
|
|
1198
|
+
const t = useTranslation();
|
|
1199
|
+
const config = useAgendaConfig();
|
|
1200
|
+
const updateStatus = useAgendaStore((s) => s.updateBookingStatus);
|
|
1201
|
+
const [confirming, setConfirming] = useState(false);
|
|
1202
|
+
const series = useSeriesDelete(booking, confirming, onClose);
|
|
1203
|
+
const deleting = series.deleting !== null;
|
|
1204
|
+
return /* @__PURE__ */ jsx(FloatingPanel, { position, width: 230, height: 180, offset: { x: 4, y: 4 }, onClose, zIndex: 110, children: (panel) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1205
|
+
/* @__PURE__ */ jsxs("div", { className: "py-1", children: [
|
|
1206
|
+
/* @__PURE__ */ jsxs(
|
|
1207
|
+
Button,
|
|
1208
|
+
{
|
|
1209
|
+
variant: "bare",
|
|
1210
|
+
size: "bare",
|
|
1211
|
+
onClick: () => panel.expandThen(() => onEdit(booking.id)),
|
|
1212
|
+
className: "font-normal justify-start rounded-none whitespace-normal flex w-full items-center gap-3 px-4 py-2 text-sm hover:bg-muted/50 transition-colors",
|
|
1213
|
+
children: [
|
|
1214
|
+
/* @__PURE__ */ jsx(Pencil, { className: "h-4 w-4 text-muted-foreground" }),
|
|
1215
|
+
" ",
|
|
1216
|
+
t("agenda.contextMenu.edit")
|
|
1217
|
+
]
|
|
1218
|
+
}
|
|
1219
|
+
),
|
|
1220
|
+
confirming && series.isSeries ? /* @__PURE__ */ jsx(
|
|
1221
|
+
SeriesDeleteChoice,
|
|
1222
|
+
{
|
|
1223
|
+
seriesSize: series.seriesSize,
|
|
1224
|
+
deleting: series.deleting,
|
|
1225
|
+
onPick: (scope) => {
|
|
1226
|
+
void series.remove(scope);
|
|
1227
|
+
},
|
|
1228
|
+
onCancel: () => setConfirming(false)
|
|
1229
|
+
}
|
|
1230
|
+
) : confirming ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-2", children: [
|
|
1231
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-destructive font-medium", children: t("agenda.appointment.deleteConfirm") }),
|
|
1232
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
1233
|
+
/* @__PURE__ */ jsx(
|
|
1234
|
+
Button,
|
|
1235
|
+
{
|
|
1236
|
+
variant: "bare",
|
|
1237
|
+
size: "bare",
|
|
1238
|
+
onClick: () => setConfirming(false),
|
|
1239
|
+
className: "rounded px-2 py-0.5 text-[11px] font-medium hover:bg-muted/50 transition-colors",
|
|
1240
|
+
children: t("agenda.appointment.cancel")
|
|
1241
|
+
}
|
|
1242
|
+
),
|
|
1243
|
+
/* @__PURE__ */ jsx(
|
|
1244
|
+
Button,
|
|
1245
|
+
{
|
|
1246
|
+
variant: "bare",
|
|
1247
|
+
size: "bare",
|
|
1248
|
+
onClick: () => {
|
|
1249
|
+
void series.remove("this");
|
|
1250
|
+
},
|
|
1251
|
+
disabled: deleting,
|
|
1252
|
+
className: "rounded bg-destructive border border-destructive px-2 py-0.5 text-[11px] font-medium text-destructive-foreground hover:bg-destructive/90 shadow-button-primary active:shadow-button-inset disabled:opacity-40 transition-colors",
|
|
1253
|
+
children: deleting ? t("agenda.appointment.deleting") : t("agenda.appointment.confirmDelete")
|
|
1254
|
+
}
|
|
1255
|
+
)
|
|
1256
|
+
] })
|
|
1257
|
+
] }) : /* @__PURE__ */ jsxs(
|
|
1258
|
+
Button,
|
|
1259
|
+
{
|
|
1260
|
+
variant: "bare",
|
|
1261
|
+
size: "bare",
|
|
1262
|
+
onClick: () => setConfirming(true),
|
|
1263
|
+
className: "font-normal justify-start rounded-none whitespace-normal flex w-full items-center gap-3 px-4 py-2 text-sm hover:bg-muted/50 transition-colors text-destructive",
|
|
1264
|
+
children: [
|
|
1265
|
+
/* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" }),
|
|
1266
|
+
" ",
|
|
1267
|
+
t("agenda.contextMenu.delete")
|
|
1268
|
+
]
|
|
1269
|
+
}
|
|
1270
|
+
)
|
|
1271
|
+
] }),
|
|
1272
|
+
config.capabilities.status && /* @__PURE__ */ jsxs("div", { className: "border-t px-4 py-3", children: [
|
|
1273
|
+
/* @__PURE__ */ jsx("p", { className: "text-[11px] font-medium text-muted-foreground mb-2", children: t("agenda.contextMenu.changeStatus") }),
|
|
1274
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-1.5", children: config.statuses.map((s) => {
|
|
1275
|
+
const available = isStatusAvailable(s, booking.startsAt);
|
|
1276
|
+
return /* @__PURE__ */ jsx(
|
|
1277
|
+
StatusDot,
|
|
1278
|
+
{
|
|
1279
|
+
color: s.color,
|
|
1280
|
+
label: available ? s.label : `${s.label} ${t("agenda.contextMenu.notAvailable")}`,
|
|
1281
|
+
isActive: booking.status === s.value,
|
|
1282
|
+
disabled: !available,
|
|
1283
|
+
onClick: () => {
|
|
1284
|
+
if (available) {
|
|
1285
|
+
updateStatus(booking.id, s.value);
|
|
1286
|
+
panel.dismiss();
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
},
|
|
1290
|
+
s.value
|
|
1291
|
+
);
|
|
1292
|
+
}) })
|
|
1293
|
+
] })
|
|
1294
|
+
] }) });
|
|
1295
|
+
}
|
|
1296
|
+
function startOfDay(d) {
|
|
1297
|
+
const x = new Date(d);
|
|
1298
|
+
x.setHours(0, 0, 0, 0);
|
|
1299
|
+
return x;
|
|
1300
|
+
}
|
|
1301
|
+
function timeLabel(iso) {
|
|
1302
|
+
const d = new Date(iso);
|
|
1303
|
+
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
1304
|
+
}
|
|
1305
|
+
function AgendaMobileListSkeleton() {
|
|
1306
|
+
return /* @__PURE__ */ jsx("div", { className: "flex-1 space-y-5 overflow-hidden px-3 py-3", "aria-busy": "true", "aria-live": "polite", children: [0, 1].map((g) => /* @__PURE__ */ jsxs("div", { className: "animate-pulse", children: [
|
|
1307
|
+
/* @__PURE__ */ jsx("div", { className: "mb-2 ml-1 h-3 w-20 rounded bg-muted" }),
|
|
1308
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-1.5", children: [0, 1].map((r) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 rounded-card border border-border bg-card px-3 py-3", children: [
|
|
1309
|
+
/* @__PURE__ */ jsx("div", { className: "h-2 w-2 shrink-0 rounded-full bg-muted" }),
|
|
1310
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [
|
|
1311
|
+
/* @__PURE__ */ jsx("div", { className: "h-3.5 w-2/3 rounded bg-muted" }),
|
|
1312
|
+
/* @__PURE__ */ jsx("div", { className: "h-3 w-12 rounded bg-muted" })
|
|
1313
|
+
] }),
|
|
1314
|
+
/* @__PURE__ */ jsx("div", { className: "h-3.5 w-14 shrink-0 rounded bg-muted" })
|
|
1315
|
+
] }, r)) })
|
|
1316
|
+
] }, g)) });
|
|
1317
|
+
}
|
|
1318
|
+
function AgendaMobileList({ bookings, statusColors, currencyCode, onSelect, onCreate }) {
|
|
1319
|
+
const t = useTranslation();
|
|
1320
|
+
const locale = getCurrentLocale() === "pt-BR" ? "pt-BR" : "en";
|
|
1321
|
+
const groups = useMemo(() => {
|
|
1322
|
+
const todayStart = startOfDay(/* @__PURE__ */ new Date()).getTime();
|
|
1323
|
+
const sorted = bookings.filter((b) => !!b.startsAt).slice().sort((a, b) => a.startsAt < b.startsAt ? -1 : a.startsAt > b.startsAt ? 1 : 0);
|
|
1324
|
+
const map = /* @__PURE__ */ new Map();
|
|
1325
|
+
for (const b of sorted) {
|
|
1326
|
+
const day = startOfDay(new Date(b.startsAt));
|
|
1327
|
+
if (day.getTime() < todayStart) continue;
|
|
1328
|
+
const key = localDateKey(day);
|
|
1329
|
+
const list = map.get(key) ?? [];
|
|
1330
|
+
list.push(b);
|
|
1331
|
+
map.set(key, list);
|
|
1332
|
+
}
|
|
1333
|
+
return Array.from(map.entries()).map(([key, items]) => ({ key, date: /* @__PURE__ */ new Date(key + "T00:00:00"), items }));
|
|
1334
|
+
}, [bookings]);
|
|
1335
|
+
function dayLabel(date) {
|
|
1336
|
+
const today = startOfDay(/* @__PURE__ */ new Date());
|
|
1337
|
+
const diff = Math.round((startOfDay(date).getTime() - today.getTime()) / 864e5);
|
|
1338
|
+
if (diff === 0) return t("agenda.list.today");
|
|
1339
|
+
if (diff === 1) return t("agenda.list.tomorrow");
|
|
1340
|
+
return date.toLocaleDateString(locale, { weekday: "long", day: "numeric", month: "long" });
|
|
1341
|
+
}
|
|
1342
|
+
if (groups.length === 0) {
|
|
1343
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-4 px-6 py-16 text-center", children: [
|
|
1344
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("agenda.list.empty") }),
|
|
1345
|
+
onCreate && /* @__PURE__ */ jsx(
|
|
1346
|
+
Button,
|
|
1347
|
+
{
|
|
1348
|
+
variant: "bare",
|
|
1349
|
+
size: "bare",
|
|
1350
|
+
type: "button",
|
|
1351
|
+
onClick: onCreate,
|
|
1352
|
+
className: "fz-tap inline-flex items-center rounded-full bg-primary px-5 text-sm font-medium text-primary-foreground active:scale-[0.98]",
|
|
1353
|
+
children: t("agenda.calendar.newAppointment")
|
|
1354
|
+
}
|
|
1355
|
+
)
|
|
1356
|
+
] });
|
|
1357
|
+
}
|
|
1358
|
+
return /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto px-3 py-3 space-y-5", children: groups.map((g) => /* @__PURE__ */ jsxs("div", { children: [
|
|
1359
|
+
/* @__PURE__ */ jsx("h3", { className: "px-1 pb-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: dayLabel(g.date) }),
|
|
1360
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-1.5", children: g.items.map((b) => {
|
|
1361
|
+
const color = statusColors[b.status] ?? "#6b7280";
|
|
1362
|
+
const hasAmount = typeof b.orderTotal === "number" && b.orderTotal > 0;
|
|
1363
|
+
return /* @__PURE__ */ jsxs(
|
|
1364
|
+
Button,
|
|
1365
|
+
{
|
|
1366
|
+
variant: "bare",
|
|
1367
|
+
size: "bare",
|
|
1368
|
+
onClick: () => onSelect(b),
|
|
1369
|
+
className: "font-normal justify-start whitespace-normal flex w-full items-center gap-3 rounded-card border bg-card px-3 py-2.5 text-left shadow-sm transition-colors active:bg-muted/50",
|
|
1370
|
+
children: [
|
|
1371
|
+
/* @__PURE__ */ jsx("span", { className: "h-2.5 w-2.5 shrink-0 rounded-full", style: { backgroundColor: color } }),
|
|
1372
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
1373
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium text-foreground", children: b.clientName ?? "\u2014" }),
|
|
1374
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: timeLabel(b.startsAt) })
|
|
1375
|
+
] }),
|
|
1376
|
+
hasAmount && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm font-semibold tabular-nums text-foreground", children: formatCurrency(b.orderTotal, currencyCode) })
|
|
1377
|
+
]
|
|
1378
|
+
},
|
|
1379
|
+
b.id
|
|
1380
|
+
);
|
|
1381
|
+
}) })
|
|
1382
|
+
] }, g.key)) });
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// src/lib/schedule-engine.ts
|
|
1386
|
+
function dow(iso) {
|
|
1387
|
+
const [y, m, d] = iso.slice(0, 10).split("-").map(Number);
|
|
1388
|
+
return new Date(Date.UTC(y, m - 1, d)).getUTCDay();
|
|
1389
|
+
}
|
|
1390
|
+
function days(iso) {
|
|
1391
|
+
const [y, m, d] = iso.slice(0, 10).split("-").map(Number);
|
|
1392
|
+
return Math.round(Date.UTC(y, m - 1, d) / 864e5);
|
|
1393
|
+
}
|
|
1394
|
+
function gridAppliesOn(grid, iso) {
|
|
1395
|
+
const dia = iso.slice(0, 10);
|
|
1396
|
+
if (grid.validFrom && dia < grid.validFrom.slice(0, 10)) return false;
|
|
1397
|
+
if (grid.validTo && dia > grid.validTo.slice(0, 10)) return false;
|
|
1398
|
+
return true;
|
|
1399
|
+
}
|
|
1400
|
+
function weekParityActive(validFrom, iso, weekInterval) {
|
|
1401
|
+
const passo = weekInterval ?? 0;
|
|
1402
|
+
if (passo <= 0 || !validFrom) return true;
|
|
1403
|
+
const dia = iso.slice(0, 10);
|
|
1404
|
+
const inicio = validFrom.slice(0, 10);
|
|
1405
|
+
const semanas = (days(dia) - dow(dia) - (days(inicio) - dow(inicio))) / 7 - (dow(dia) < dow(inicio) ? 1 : 0);
|
|
1406
|
+
return semanas >= 0 && semanas % (passo + 1) === 0;
|
|
1407
|
+
}
|
|
1408
|
+
function effectiveWindows(rows, iso) {
|
|
1409
|
+
const dia = iso.slice(0, 10);
|
|
1410
|
+
const semana = dow(dia);
|
|
1411
|
+
const vigentes = rows.filter((r) => gridAppliesOn(r, dia));
|
|
1412
|
+
const doDia = vigentes.filter((r) => r.specificDate?.slice(0, 10) === dia);
|
|
1413
|
+
if (doDia.length > 0) return doDia.filter((r) => r.isActive !== false);
|
|
1414
|
+
const semanais = vigentes.filter(
|
|
1415
|
+
(r) => !r.specificDate && r.isActive !== false && r.dayOfWeek === semana
|
|
1416
|
+
);
|
|
1417
|
+
const temExcecao = semanais.some((r) => r.gridKind === "exception");
|
|
1418
|
+
const comParidade = semanais.filter((r) => weekParityActive(r.validFrom, dia, r.weekInterval));
|
|
1419
|
+
return temExcecao ? comParidade.filter((r) => r.gridKind === "exception") : comParidade;
|
|
1420
|
+
}
|
|
1421
|
+
function opensOnDate(rows, iso) {
|
|
1422
|
+
return effectiveWindows(rows, iso).length > 0;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// src/lib/calendar-events.ts
|
|
1426
|
+
var REVISION_CARD_COLOR = "#111827";
|
|
1427
|
+
var DEFAULT_SERVICE_CARD_COLOR = "#3b82f6";
|
|
1428
|
+
function bookingCardColor(booking, looks) {
|
|
1429
|
+
const own = (booking.services ?? []).map((s) => s.serviceId ? looks[s.serviceId] : void 0).filter(Boolean);
|
|
1430
|
+
if (own.length === 0) return null;
|
|
1431
|
+
if (own.some((l) => l.kind === "return")) return REVISION_CARD_COLOR;
|
|
1432
|
+
return own.find((l) => l.color)?.color ?? DEFAULT_SERVICE_CARD_COLOR;
|
|
1433
|
+
}
|
|
1434
|
+
function readableTextOn(hex) {
|
|
1435
|
+
let h = hex.replace("#", "");
|
|
1436
|
+
if (h.length === 3) h = h.split("").map((c) => c + c).join("");
|
|
1437
|
+
const [r, g, b] = [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) / 255);
|
|
1438
|
+
const lin = (c) => c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
1439
|
+
const L = 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
|
|
1440
|
+
return (L + 0.05) / 0.05 > 1.05 / (L + 0.05) ? "#111827" : "#ffffff";
|
|
1441
|
+
}
|
|
1442
|
+
function bookingToEvent(booking, statusColors, visibleResourceIds = [], serviceLooks) {
|
|
1443
|
+
const color = statusColors[booking.status] ?? "#6b7280";
|
|
1444
|
+
const kindColor = serviceLooks ? bookingCardColor(booking, serviceLooks) : null;
|
|
1445
|
+
const serviceNames = booking.services?.map((service) => service.name).join(", ") ?? "";
|
|
1446
|
+
return {
|
|
1447
|
+
id: booking.id,
|
|
1448
|
+
// A tenant-wide block (an imported whole-agenda Google event, for example)
|
|
1449
|
+
// has no assignee. Availability treats it as busy for every professional, so
|
|
1450
|
+
// resource views must render it in every visible lane instead of dropping it.
|
|
1451
|
+
...booking.professionalId ? { resourceId: booking.professionalId } : visibleResourceIds.length > 0 ? { resourceIds: visibleResourceIds } : {},
|
|
1452
|
+
title: booking.clientName ?? "Sem cliente",
|
|
1453
|
+
start: booking.startsAt,
|
|
1454
|
+
end: booking.endsAt ?? void 0,
|
|
1455
|
+
backgroundColor: kindColor ?? color + "30",
|
|
1456
|
+
borderColor: kindColor ?? color,
|
|
1457
|
+
textColor: kindColor ? readableTextOn(kindColor) : color,
|
|
1458
|
+
extendedProps: { booking, serviceNames, ...kindColor ? { statusStripe: color } : {} }
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
function localDateTimeString(d) {
|
|
1462
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1463
|
+
return `${localDateKey(d)}T${pad(d.getHours())}:${pad(d.getMinutes())}:00`;
|
|
1464
|
+
}
|
|
1465
|
+
function generateAvailabilityEvents(schedules, professionals, dateRange, businessHours, locationMap, defaultLocationName) {
|
|
1466
|
+
if (!dateRange || schedules.length === 0) return [];
|
|
1467
|
+
const bgEvents = [];
|
|
1468
|
+
const startDate = new Date(dateRange.start);
|
|
1469
|
+
const endDate = new Date(dateRange.end);
|
|
1470
|
+
const bizStart = businessHours?.startTime ?? "08:00";
|
|
1471
|
+
const bizEnd = businessHours?.endTime ?? "20:00";
|
|
1472
|
+
const byAssignee = /* @__PURE__ */ new Map();
|
|
1473
|
+
for (const s of schedules) {
|
|
1474
|
+
const aid = s.assigneeId;
|
|
1475
|
+
if (!aid) continue;
|
|
1476
|
+
const list = byAssignee.get(aid) ?? [];
|
|
1477
|
+
list.push(s);
|
|
1478
|
+
byAssignee.set(aid, list);
|
|
1479
|
+
}
|
|
1480
|
+
function timeToMinutes(t) {
|
|
1481
|
+
const [h, m] = t.split(":").map(Number);
|
|
1482
|
+
return h * 60 + m;
|
|
1483
|
+
}
|
|
1484
|
+
function minutesToTime(m) {
|
|
1485
|
+
return `${String(Math.floor(m / 60)).padStart(2, "0")}:${String(m % 60).padStart(2, "0")}`;
|
|
1486
|
+
}
|
|
1487
|
+
const bizStartMin = timeToMinutes(bizStart);
|
|
1488
|
+
const bizEndMin = timeToMinutes(bizEnd);
|
|
1489
|
+
for (const prof of professionals) {
|
|
1490
|
+
const profSchedules = byAssignee.get(prof.id) ?? [];
|
|
1491
|
+
const current = new Date(startDate);
|
|
1492
|
+
while (current < endDate) {
|
|
1493
|
+
const dateStr = localDateKey(current);
|
|
1494
|
+
const daySchedules = effectiveWindows(profSchedules, dateStr);
|
|
1495
|
+
const workRanges = daySchedules.map((s) => {
|
|
1496
|
+
const locId = s.locationId ?? s.metadata?.locationId;
|
|
1497
|
+
return {
|
|
1498
|
+
start: timeToMinutes(s.startsAt),
|
|
1499
|
+
end: timeToMinutes(s.endsAt),
|
|
1500
|
+
// A grade sem local É uma grade, e no V1 ela se anuncia igual: a
|
|
1501
|
+
// faixa aberta leva o nome do local em cima. Cair para o nome
|
|
1502
|
+
// padrão em vez de não escrever nada é o que faz a coluna dizer
|
|
1503
|
+
// "aqui abre" — sem a etiqueta, grade aberta e grade vazia se
|
|
1504
|
+
// parecem, e a recepção descobre a diferença clicando.
|
|
1505
|
+
locationName: (locId && locationMap ? locationMap.get(locId) : void 0) ?? defaultLocationName
|
|
1506
|
+
};
|
|
1507
|
+
}).sort((a, b) => a.start - b.start);
|
|
1508
|
+
let cursor = bizStartMin;
|
|
1509
|
+
for (const range of workRanges) {
|
|
1510
|
+
const availStart = Math.max(range.start, bizStartMin);
|
|
1511
|
+
const availEnd = Math.min(range.end, bizEndMin);
|
|
1512
|
+
if (cursor < availStart) {
|
|
1513
|
+
bgEvents.push({
|
|
1514
|
+
resourceId: prof.id,
|
|
1515
|
+
start: dateStr + "T" + minutesToTime(cursor) + ":00",
|
|
1516
|
+
end: dateStr + "T" + minutesToTime(availStart) + ":00",
|
|
1517
|
+
display: "background",
|
|
1518
|
+
classNames: ["fc-unavailable-bg"]
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
if (availStart < availEnd && range.locationName) {
|
|
1522
|
+
bgEvents.push({
|
|
1523
|
+
id: `loc-${prof.id}-${dateStr}-${minutesToTime(availStart)}`,
|
|
1524
|
+
resourceId: prof.id,
|
|
1525
|
+
start: dateStr + "T" + minutesToTime(availStart) + ":00",
|
|
1526
|
+
end: dateStr + "T" + minutesToTime(availStart + 20) + ":00",
|
|
1527
|
+
title: range.locationName,
|
|
1528
|
+
display: "auto",
|
|
1529
|
+
editable: false,
|
|
1530
|
+
backgroundColor: "transparent",
|
|
1531
|
+
borderColor: "transparent",
|
|
1532
|
+
textColor: "#a3a3a3",
|
|
1533
|
+
classNames: ["fc-location-label"],
|
|
1534
|
+
extendedProps: { isLocationLabel: true }
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
cursor = Math.max(cursor, availEnd);
|
|
1538
|
+
}
|
|
1539
|
+
if (cursor < bizEndMin) {
|
|
1540
|
+
bgEvents.push({
|
|
1541
|
+
resourceId: prof.id,
|
|
1542
|
+
start: dateStr + "T" + minutesToTime(cursor) + ":00",
|
|
1543
|
+
end: dateStr + "T" + minutesToTime(bizEndMin) + ":00",
|
|
1544
|
+
display: "background",
|
|
1545
|
+
classNames: ["fc-unavailable-bg"]
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
current.setDate(current.getDate() + 1);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
return bgEvents;
|
|
1552
|
+
}
|
|
1553
|
+
function CalendarSkeleton({ profCount = 2 }) {
|
|
1554
|
+
const days2 = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
|
|
1555
|
+
const hours = ["07:00", "08:00", "09:00", "10:00", "11:00", "12:00", "13:00", "14:00", "15:00", "16:00", "17:00", "18:00", "19:00", "20:00"];
|
|
1556
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-hidden animate-pulse", children: [
|
|
1557
|
+
/* @__PURE__ */ jsx("div", { className: "flex border-b", style: { paddingLeft: 56 }, children: Array.from({ length: profCount }).map((_, pi) => /* @__PURE__ */ jsx("div", { className: "flex-1 border-r last:border-r-0 py-2 px-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-2", children: [
|
|
1558
|
+
/* @__PURE__ */ jsx("div", { className: "h-6 w-6 rounded-full bg-muted" }),
|
|
1559
|
+
/* @__PURE__ */ jsx("div", { className: "h-3 w-20 rounded bg-muted" })
|
|
1560
|
+
] }) }, pi)) }),
|
|
1561
|
+
/* @__PURE__ */ jsx("div", { className: "flex border-b", style: { paddingLeft: 56 }, children: Array.from({ length: profCount }).map((_, pi) => /* @__PURE__ */ jsx("div", { className: "flex-1 flex border-r last:border-r-0", children: days2.map((d) => /* @__PURE__ */ jsx("div", { className: "flex-1 py-1.5 text-center", children: /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground/40", children: d }) }, d)) }, pi)) }),
|
|
1562
|
+
/* @__PURE__ */ jsx("div", { className: "relative", children: hours.map((h) => /* @__PURE__ */ jsxs("div", { className: "flex border-b border-border/30", style: { height: 50 }, children: [
|
|
1563
|
+
/* @__PURE__ */ jsx("div", { className: "w-14 shrink-0 pr-2 text-right", children: /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground/30", children: h }) }),
|
|
1564
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 flex", children: Array.from({ length: profCount * 7 }).map((_, ci) => /* @__PURE__ */ jsx("div", { className: `flex-1 border-r border-border/20 ${Math.floor(ci / 7) % 2 === 1 ? "bg-muted/20" : ""}` }, ci)) })
|
|
1565
|
+
] }, h)) })
|
|
1566
|
+
] });
|
|
1567
|
+
}
|
|
1568
|
+
function toEditingBlock(clicado, todos) {
|
|
1569
|
+
const meta = clicado.metadata?.agenda ?? {};
|
|
1570
|
+
const mesmoGrupo = (s) => s.assigneeId === clicado.assigneeId && s.startsAt === clicado.startsAt && s.endsAt === clicado.endsAt && (s.metadata?.agenda?.title ?? null) === (meta.title ?? null) && s.validFrom === clicado.validFrom && s.validTo === clicado.validTo;
|
|
1571
|
+
const grupo = clicado.specificDate ? [clicado] : todos.filter(mesmoGrupo);
|
|
1572
|
+
return {
|
|
1573
|
+
ids: grupo.map((s) => s.id),
|
|
1574
|
+
assigneeId: clicado.assigneeId ?? "",
|
|
1575
|
+
title: meta.title ?? "Bloqueio",
|
|
1576
|
+
description: meta.description ?? "",
|
|
1577
|
+
startsAt: clicado.startsAt.slice(0, 5),
|
|
1578
|
+
endsAt: clicado.endsAt.slice(0, 5),
|
|
1579
|
+
specificDate: clicado.specificDate,
|
|
1580
|
+
daysOfWeek: clicado.specificDate ? [] : [...new Set(grupo.map((s) => s.dayOfWeek).filter((d) => d != null))],
|
|
1581
|
+
validFrom: clicado.validFrom ?? null,
|
|
1582
|
+
validTo: clicado.validTo ?? null,
|
|
1583
|
+
weekInterval: clicado.weekInterval ?? 0
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
var MIN_COLUMN_PX = 48;
|
|
1587
|
+
var RESOURCE_COLUMN_MIN_PX = 96;
|
|
1588
|
+
var MIN_SHELL_HEIGHT = 360;
|
|
1589
|
+
var MAX_SELECTED_AGENDAS = 10;
|
|
1590
|
+
var RESOURCE_VIEW_OPTIONS = [
|
|
1591
|
+
{ key: "resourceTimeGridDay", labelKey: "agenda.calendar.viewDay" },
|
|
1592
|
+
{ key: "resourceTimeGridWeek", labelKey: "agenda.calendar.viewWeek" },
|
|
1593
|
+
{ key: "dayGridMonth", labelKey: "agenda.calendar.viewMonth" },
|
|
1594
|
+
// A QUARTA VISÃO DO V1, que faltava no salão: a lista.
|
|
1595
|
+
//
|
|
1596
|
+
// A grade responde "como está o dia" e exige varrer colunas; a lista responde
|
|
1597
|
+
// "o que acontece, em que ordem" e se lê de cima para baixo — é a visão de
|
|
1598
|
+
// quem confere a véspera, imprime o dia ou procura um nome sem saber a hora.
|
|
1599
|
+
// O V1 a chama de "Agenda", e é esse o nome aqui: "Lista" descreve a forma,
|
|
1600
|
+
// "Agenda" descreve o que ela mostra.
|
|
1601
|
+
//
|
|
1602
|
+
// Sem recursos de propósito: uma lista não tem colunas, e o profissional
|
|
1603
|
+
// aparece na linha, junto do cliente e do serviço.
|
|
1604
|
+
{ key: "listWeek", labelKey: "agenda.calendar.viewAgenda" }
|
|
1605
|
+
];
|
|
1606
|
+
var PLAIN_VIEW_OPTIONS = [
|
|
1607
|
+
{ key: "timeGridDay", labelKey: "agenda.calendar.viewDay" },
|
|
1608
|
+
{ key: "timeGridWeek", labelKey: "agenda.calendar.viewWeek" },
|
|
1609
|
+
{ key: "dayGridMonth", labelKey: "agenda.calendar.viewMonth" },
|
|
1610
|
+
{ key: "listMonth", labelKey: "agenda.calendar.viewList" }
|
|
1611
|
+
];
|
|
1612
|
+
var MOBILE_LIST_VIEW = "__mobileList__";
|
|
1613
|
+
var AGENDA_POLL_INTERVAL_MS = 3e4;
|
|
1614
|
+
var AGENDA_REALTIME_DEBOUNCE_MS = 300;
|
|
1615
|
+
function viewForNoResources(view) {
|
|
1616
|
+
return view.startsWith("resource") ? "dayGridMonth" : view;
|
|
1617
|
+
}
|
|
1618
|
+
function viewForResources(view) {
|
|
1619
|
+
if (view === "timeGridWeek") return "resourceTimeGridWeek";
|
|
1620
|
+
if (view === "timeGridDay") return "resourceTimeGridDay";
|
|
1621
|
+
return view;
|
|
1622
|
+
}
|
|
1623
|
+
function useIsMobile() {
|
|
1624
|
+
const [isMobile, setIsMobile] = useState(() => typeof window !== "undefined" && window.matchMedia("(max-width: 767px)").matches);
|
|
1625
|
+
useEffect(() => {
|
|
1626
|
+
if (typeof window === "undefined") return;
|
|
1627
|
+
const mq = window.matchMedia("(max-width: 767px)");
|
|
1628
|
+
const onChange = () => setIsMobile(mq.matches);
|
|
1629
|
+
mq.addEventListener("change", onChange);
|
|
1630
|
+
return () => mq.removeEventListener("change", onChange);
|
|
1631
|
+
}, []);
|
|
1632
|
+
return isMobile;
|
|
1633
|
+
}
|
|
1634
|
+
function CalendarView() {
|
|
1635
|
+
injectCalendarStyles();
|
|
1636
|
+
const config = useAgendaConfig();
|
|
1637
|
+
const provider = useAgendaProvider();
|
|
1638
|
+
const t = useTranslation();
|
|
1639
|
+
const currentLocale = getCurrentLocale();
|
|
1640
|
+
const fcLocale = currentLocale === "pt-BR" ? ptBrLocale : void 0;
|
|
1641
|
+
const calendarRef = useRef(null);
|
|
1642
|
+
const prefs = usePluginPrefs("agenda", {
|
|
1643
|
+
calendarView: config.defaultCalendarView ?? "resourceTimeGridDay",
|
|
1644
|
+
// Como a SEMANA se agrupa: 'day' põe o dia em cima e as pessoas dentro
|
|
1645
|
+
// dele; 'professional' inverte. 'auto' é o padrão e deixa a agenda decidir
|
|
1646
|
+
// pelo tamanho da equipe — ver `weekGrouping` mais abaixo.
|
|
1647
|
+
weekGrouping: "auto",
|
|
1648
|
+
// Which weekdays the WEEK draws: 'auto', or the shown days (0 = Sunday) — see `shownWeekDays`.
|
|
1649
|
+
weekDays: "auto"
|
|
1650
|
+
});
|
|
1651
|
+
const weekGroupingPref = prefs.get("weekGrouping") ?? "auto";
|
|
1652
|
+
const agendaSettings = useAgendaSettings();
|
|
1653
|
+
const effectiveBusinessHours = {
|
|
1654
|
+
startTime: agendaSettings.startTime,
|
|
1655
|
+
endTime: agendaSettings.endTime
|
|
1656
|
+
};
|
|
1657
|
+
const bookings = useAgendaStore((s) => s.bookings);
|
|
1658
|
+
const bookingsLoading = useAgendaStore((s) => s.bookingsLoading);
|
|
1659
|
+
const professionals = useAgendaStore((s) => s.professionals);
|
|
1660
|
+
const professionalsLoading = useAgendaStore((s) => s.professionalsLoading);
|
|
1661
|
+
const professionalsTruncated = useAgendaStore((s) => s.professionalsTruncated);
|
|
1662
|
+
const currentView = useAgendaStore((s) => s.currentView);
|
|
1663
|
+
const selectedProfIds = useAgendaStore((s) => s.selectedProfessionalIds);
|
|
1664
|
+
const selectedStatuses = useAgendaStore((s) => s.selectedStatuses);
|
|
1665
|
+
const schedules = useAgendaStore((s) => s.schedules);
|
|
1666
|
+
const schedulesLoading = useAgendaStore((s) => s.schedulesLoading);
|
|
1667
|
+
const fetchBookings = useAgendaStore((s) => s.fetchBookings);
|
|
1668
|
+
const fetchProfessionals = useAgendaStore((s) => s.fetchProfessionals);
|
|
1669
|
+
const fetchLocations = useAgendaStore((s) => s.fetchLocations);
|
|
1670
|
+
const storeLocations = useAgendaStore((s) => s.locations);
|
|
1671
|
+
const selectedLocationId = useAgendaStore((s) => s.selectedLocationId);
|
|
1672
|
+
const fetchSchedules = useAgendaStore((s) => s.fetchSchedules);
|
|
1673
|
+
const invalidateSchedules = useAgendaStore((s) => s.invalidateSchedules);
|
|
1674
|
+
const rescheduleBooking = useAgendaStore((s) => s.rescheduleBooking);
|
|
1675
|
+
const setView = useAgendaStore((s) => s.setView);
|
|
1676
|
+
const setFilters = useAgendaStore((s) => s.setFilters);
|
|
1677
|
+
const _openModal = useAgendaStore((s) => s.openAppointmentModal);
|
|
1678
|
+
const closeModal = useAgendaStore((s) => s.closeAppointmentModal);
|
|
1679
|
+
const modalState = useAgendaStore((s) => s.appointmentModal);
|
|
1680
|
+
const [serviceFilter, setServiceFilter] = useState([]);
|
|
1681
|
+
const [hideEmptyGrids, setHideEmptyGrids] = useState(false);
|
|
1682
|
+
const [schedulesOpen, setSchedulesOpen] = useState(false);
|
|
1683
|
+
const [blockFor, setBlockFor] = React.useState(null);
|
|
1684
|
+
const [schedulesFor, setSchedulesFor] = React.useState(null);
|
|
1685
|
+
const [editingBlock, setEditingBlock] = React.useState(null);
|
|
1686
|
+
const isMobile = useIsMobile();
|
|
1687
|
+
const resourceCapable = professionals.length > 0;
|
|
1688
|
+
const viewOptions = resourceCapable ? RESOURCE_VIEW_OPTIONS : PLAIN_VIEW_OPTIONS;
|
|
1689
|
+
const shellRef = useRef(null);
|
|
1690
|
+
const [shellHeight, setShellHeight] = useState(void 0);
|
|
1691
|
+
useLayoutEffect(() => {
|
|
1692
|
+
const measure = () => {
|
|
1693
|
+
const el = shellRef.current;
|
|
1694
|
+
if (!el) return;
|
|
1695
|
+
const top = el.getBoundingClientRect().top;
|
|
1696
|
+
const parent = el.parentElement;
|
|
1697
|
+
const pad = parent ? parseFloat(getComputedStyle(parent).paddingBottom) || 0 : 0;
|
|
1698
|
+
setShellHeight(Math.max(MIN_SHELL_HEIGHT, window.innerHeight - top - pad));
|
|
1699
|
+
};
|
|
1700
|
+
measure();
|
|
1701
|
+
window.addEventListener("resize", measure);
|
|
1702
|
+
const ro = new ResizeObserver(measure);
|
|
1703
|
+
if (document.body) ro.observe(document.body);
|
|
1704
|
+
return () => {
|
|
1705
|
+
window.removeEventListener("resize", measure);
|
|
1706
|
+
ro.disconnect();
|
|
1707
|
+
};
|
|
1708
|
+
}, []);
|
|
1709
|
+
const [mobileView, setMobileView] = useState(MOBILE_LIST_VIEW);
|
|
1710
|
+
const mobileViewOptions = useMemo(
|
|
1711
|
+
() => [
|
|
1712
|
+
...viewOptions.filter((o) => o.key !== "listMonth"),
|
|
1713
|
+
{ key: MOBILE_LIST_VIEW, labelKey: "agenda.calendar.viewList" }
|
|
1714
|
+
],
|
|
1715
|
+
[viewOptions]
|
|
1716
|
+
);
|
|
1717
|
+
const mobileGridActive = isMobile && mobileView !== MOBILE_LIST_VIEW;
|
|
1718
|
+
const [selectedBooking, setSelectedBooking] = useState(null);
|
|
1719
|
+
const [popoverAnchor, setPopoverAnchor] = useState(null);
|
|
1720
|
+
const [popoverVisible, _setPopoverVisible] = useState(false);
|
|
1721
|
+
const popoverVisibleRef = useRef(false);
|
|
1722
|
+
const setPopoverVisible = useCallback((v) => {
|
|
1723
|
+
popoverVisibleRef.current = v;
|
|
1724
|
+
_setPopoverVisible(v);
|
|
1725
|
+
}, []);
|
|
1726
|
+
const [contextMenu, setContextMenu] = useState(null);
|
|
1727
|
+
const [contextMenuVisible, _setContextMenuVisible] = useState(false);
|
|
1728
|
+
const contextMenuVisibleRef = useRef(false);
|
|
1729
|
+
const setContextMenuVisible = useCallback((v) => {
|
|
1730
|
+
contextMenuVisibleRef.current = v;
|
|
1731
|
+
_setContextMenuVisible(v);
|
|
1732
|
+
}, []);
|
|
1733
|
+
const [scheduleConfirm, setScheduleConfirm] = useState(null);
|
|
1734
|
+
const [calendarTitle, setCalendarTitle] = useState("");
|
|
1735
|
+
const [selectedDate, setSelectedDate] = useState(/* @__PURE__ */ new Date());
|
|
1736
|
+
const [visibleRange, setVisibleRange] = useState(null);
|
|
1737
|
+
useEffect(() => () => closeModal(), [closeModal]);
|
|
1738
|
+
const openModal = useCallback((...args) => {
|
|
1739
|
+
setPopoverVisible(false);
|
|
1740
|
+
setSelectedBooking(null);
|
|
1741
|
+
setPopoverAnchor(null);
|
|
1742
|
+
setContextMenuVisible(false);
|
|
1743
|
+
setContextMenu(null);
|
|
1744
|
+
_openModal(...args);
|
|
1745
|
+
}, [_openModal]);
|
|
1746
|
+
const statusColors = useMemo(() => {
|
|
1747
|
+
const map = {};
|
|
1748
|
+
for (const s of config.statuses) map[s.value] = s.color;
|
|
1749
|
+
return map;
|
|
1750
|
+
}, [config.statuses]);
|
|
1751
|
+
const activeUnitId = useActiveUnitId();
|
|
1752
|
+
const [nextProfessionalId, setNextProfessionalId] = useState(() => getTurnQueueLeader(activeUnitId));
|
|
1753
|
+
useEffect(() => {
|
|
1754
|
+
setNextProfessionalId(getTurnQueueLeader(activeUnitId));
|
|
1755
|
+
return subscribeTurnQueueLeader((unitId, professionalId) => {
|
|
1756
|
+
if (unitId === activeUnitId) setNextProfessionalId(professionalId);
|
|
1757
|
+
});
|
|
1758
|
+
}, [activeUnitId]);
|
|
1759
|
+
useEffect(() => {
|
|
1760
|
+
fetchProfessionals();
|
|
1761
|
+
if (config.modules.locationSelection) fetchLocations();
|
|
1762
|
+
}, [activeUnitId]);
|
|
1763
|
+
const schedulesKey = useAgendaStore((s) => s.schedulesKey);
|
|
1764
|
+
const agendaProfessionalsKey = useAgendaStore((s) => s.agendaProfessionalsKey);
|
|
1765
|
+
const scheduleQuery = useMemo(() => {
|
|
1766
|
+
if (!visibleRange) return null;
|
|
1767
|
+
const days2 = /* @__PURE__ */ new Set();
|
|
1768
|
+
const cursor = new Date(visibleRange.start);
|
|
1769
|
+
const end = new Date(visibleRange.end);
|
|
1770
|
+
if (currentView.endsWith("Week")) {
|
|
1771
|
+
cursor.setDate(cursor.getDate() - cursor.getDay());
|
|
1772
|
+
end.setTime(cursor.getTime());
|
|
1773
|
+
end.setDate(end.getDate() + 7);
|
|
1774
|
+
return {
|
|
1775
|
+
daysOfWeek: [0, 1, 2, 3, 4, 5, 6],
|
|
1776
|
+
from: localDateKey(cursor),
|
|
1777
|
+
to: localDateKey(end)
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
for (let i = 0; cursor < end && i < 62; i++) {
|
|
1781
|
+
days2.add(cursor.getDay());
|
|
1782
|
+
cursor.setDate(cursor.getDate() + 1);
|
|
1783
|
+
}
|
|
1784
|
+
return {
|
|
1785
|
+
daysOfWeek: [...days2],
|
|
1786
|
+
from: visibleRange.start.slice(0, 10),
|
|
1787
|
+
to: visibleRange.end.slice(0, 10)
|
|
1788
|
+
};
|
|
1789
|
+
}, [visibleRange, currentView]);
|
|
1790
|
+
const wantedSchedulesKey = useMemo(
|
|
1791
|
+
() => scheduleQuery ? "agenda:schedules:" + JSON.stringify(scheduleQuery) : null,
|
|
1792
|
+
[scheduleQuery]
|
|
1793
|
+
);
|
|
1794
|
+
useEffect(() => {
|
|
1795
|
+
if (!scheduleQuery) return;
|
|
1796
|
+
if (schedulesKey === wantedSchedulesKey) return;
|
|
1797
|
+
void fetchSchedules(scheduleQuery);
|
|
1798
|
+
}, [scheduleQuery, wantedSchedulesKey, schedulesKey, activeUnitId, fetchSchedules]);
|
|
1799
|
+
const blocks = useAgendaStore((s) => s.blocks);
|
|
1800
|
+
const fetchBlocks = useAgendaStore((s) => s.fetchBlocks);
|
|
1801
|
+
useEffect(() => {
|
|
1802
|
+
if (!visibleRange) return;
|
|
1803
|
+
void fetchBlocks({ from: visibleRange.start.slice(0, 10), to: visibleRange.end.slice(0, 10) });
|
|
1804
|
+
}, [visibleRange, activeUnitId, fetchBlocks]);
|
|
1805
|
+
const [holidays, setHolidays] = useState([]);
|
|
1806
|
+
const holidayRangeKey = visibleRange ? `${visibleRange.start.slice(0, 10)}|${visibleRange.end.slice(0, 10)}` : null;
|
|
1807
|
+
useEffect(() => {
|
|
1808
|
+
if (!visibleRange || !provider.getHolidays) {
|
|
1809
|
+
setHolidays([]);
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
let cancelado = false;
|
|
1813
|
+
provider.getHolidays({ from: visibleRange.start.slice(0, 10), to: visibleRange.end.slice(0, 10) }, activeUnitId).then((rows) => {
|
|
1814
|
+
if (!cancelado) setHolidays(rows);
|
|
1815
|
+
}).catch(() => {
|
|
1816
|
+
if (!cancelado) setHolidays([]);
|
|
1817
|
+
});
|
|
1818
|
+
return () => {
|
|
1819
|
+
cancelado = true;
|
|
1820
|
+
};
|
|
1821
|
+
}, [holidayRangeKey, activeUnitId, provider]);
|
|
1822
|
+
const holidayByDate = useMemo(() => {
|
|
1823
|
+
const map = /* @__PURE__ */ new Map();
|
|
1824
|
+
for (const h of holidays) {
|
|
1825
|
+
const atual = map.get(h.date);
|
|
1826
|
+
if (atual) {
|
|
1827
|
+
atual.closes = atual.closes || h.closes;
|
|
1828
|
+
if (!atual.names.includes(h.name)) atual.names.push(h.name);
|
|
1829
|
+
} else {
|
|
1830
|
+
map.set(h.date, { closes: h.closes, names: [h.name] });
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
return map;
|
|
1834
|
+
}, [holidays]);
|
|
1835
|
+
const scheduledProfIds = useMemo(
|
|
1836
|
+
() => new Set(schedules.filter((sch) => sch.isActive !== false).map((sch) => sch.assigneeId).filter(Boolean)),
|
|
1837
|
+
[schedules]
|
|
1838
|
+
);
|
|
1839
|
+
const scheduleIndex = useMemo(() => {
|
|
1840
|
+
const byProf = /* @__PURE__ */ new Map();
|
|
1841
|
+
for (const sch of schedules) {
|
|
1842
|
+
if (!sch.assigneeId) continue;
|
|
1843
|
+
const list = byProf.get(sch.assigneeId) ?? [];
|
|
1844
|
+
list.push(sch);
|
|
1845
|
+
byProf.set(sch.assigneeId, list);
|
|
1846
|
+
}
|
|
1847
|
+
return byProf;
|
|
1848
|
+
}, [schedules]);
|
|
1849
|
+
const visibleDates = useMemo(() => {
|
|
1850
|
+
if (!visibleRange) return [];
|
|
1851
|
+
const out = [];
|
|
1852
|
+
const cursor = new Date(visibleRange.start);
|
|
1853
|
+
const end = new Date(visibleRange.end);
|
|
1854
|
+
for (let i = 0; cursor < end && i < 62; i++) {
|
|
1855
|
+
out.push({ iso: localDateKey(cursor), dow: cursor.getDay() });
|
|
1856
|
+
cursor.setDate(cursor.getDate() + 1);
|
|
1857
|
+
}
|
|
1858
|
+
return out;
|
|
1859
|
+
}, [visibleRange]);
|
|
1860
|
+
const opensOn = useCallback((profId, iso, _dow) => {
|
|
1861
|
+
const rows = scheduleIndex.get(profId);
|
|
1862
|
+
return !!rows && opensOnDate(rows, iso);
|
|
1863
|
+
}, [scheduleIndex]);
|
|
1864
|
+
const weekDaysPref = prefs.get("weekDays");
|
|
1865
|
+
const lastAutoWeekDays = useRef("0123456");
|
|
1866
|
+
const shownWeekDaysKey = useMemo(() => {
|
|
1867
|
+
const all = [0, 1, 2, 3, 4, 5, 6];
|
|
1868
|
+
if (Array.isArray(weekDaysPref)) {
|
|
1869
|
+
const chosen = all.filter((d) => weekDaysPref.includes(d));
|
|
1870
|
+
return (chosen.length > 0 ? chosen : all).join("");
|
|
1871
|
+
}
|
|
1872
|
+
const settled = currentView.endsWith("Week") && !schedulesLoading && schedulesKey === wantedSchedulesKey;
|
|
1873
|
+
if (!settled) return lastAutoWeekDays.current;
|
|
1874
|
+
if (!visibleRange || scheduleIndex.size === 0) return lastAutoWeekDays.current = all.join("");
|
|
1875
|
+
const sunday = new Date(visibleRange.start);
|
|
1876
|
+
sunday.setDate(sunday.getDate() - sunday.getDay());
|
|
1877
|
+
const works = (dow2) => {
|
|
1878
|
+
const day = new Date(sunday);
|
|
1879
|
+
day.setDate(day.getDate() + dow2);
|
|
1880
|
+
const iso = localDateKey(day);
|
|
1881
|
+
for (const [profId] of scheduleIndex) if (opensOn(profId, iso, dow2)) return true;
|
|
1882
|
+
return false;
|
|
1883
|
+
};
|
|
1884
|
+
return lastAutoWeekDays.current = all.filter((d) => d !== 0 && d !== 6 || works(d)).join("");
|
|
1885
|
+
}, [weekDaysPref, visibleRange, scheduleIndex, opensOn, currentView, schedulesLoading, schedulesKey, wantedSchedulesKey]);
|
|
1886
|
+
const shownWeekDays = useMemo(() => shownWeekDaysKey.split("").map(Number), [shownWeekDaysKey]);
|
|
1887
|
+
const weekViewOptions = useMemo(() => {
|
|
1888
|
+
const hiddenDays = [0, 1, 2, 3, 4, 5, 6].filter((d) => !shownWeekDays.includes(d));
|
|
1889
|
+
return { resourceTimeGridWeek: { hiddenDays }, timeGridWeek: { hiddenDays } };
|
|
1890
|
+
}, [shownWeekDays]);
|
|
1891
|
+
const toggleWeekDay = useCallback((dow2) => {
|
|
1892
|
+
const next = shownWeekDays.includes(dow2) ? shownWeekDays.filter((d) => d !== dow2) : [...shownWeekDays, dow2].sort();
|
|
1893
|
+
if (next.length > 0) prefs.set("weekDays", next);
|
|
1894
|
+
}, [shownWeekDays, prefs]);
|
|
1895
|
+
const relevantProfIds = useMemo(() => {
|
|
1896
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1897
|
+
for (const [profId] of scheduleIndex) {
|
|
1898
|
+
if (visibleDates.some((d) => opensOn(profId, d.iso, d.dow))) ids.add(profId);
|
|
1899
|
+
}
|
|
1900
|
+
for (const booking of bookings) {
|
|
1901
|
+
if (booking.professionalId) ids.add(booking.professionalId);
|
|
1902
|
+
}
|
|
1903
|
+
return [...ids];
|
|
1904
|
+
}, [scheduleIndex, visibleDates, opensOn, bookings]);
|
|
1905
|
+
const relevantSet = useMemo(() => new Set(relevantProfIds), [relevantProfIds]);
|
|
1906
|
+
const fetchAgendaProfessionals = useAgendaStore((s) => s.fetchAgendaProfessionals);
|
|
1907
|
+
const agendaProfessionals = useAgendaStore((s) => s.agendaProfessionals);
|
|
1908
|
+
useEffect(() => {
|
|
1909
|
+
void fetchAgendaProfessionals(relevantProfIds);
|
|
1910
|
+
}, [relevantProfIds, fetchAgendaProfessionals]);
|
|
1911
|
+
const pickerProfessionals = useMemo(() => {
|
|
1912
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1913
|
+
for (const p of agendaProfessionals) byId.set(p.id, p);
|
|
1914
|
+
for (const p of professionals) if (!byId.has(p.id)) byId.set(p.id, p);
|
|
1915
|
+
const all = [...byId.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
1916
|
+
return [...all.filter((p) => relevantSet.has(p.id)), ...all.filter((p) => !relevantSet.has(p.id))];
|
|
1917
|
+
}, [agendaProfessionals, professionals, relevantSet]);
|
|
1918
|
+
useEffect(() => {
|
|
1919
|
+
if (selectedProfIds.length) setFilters({ professionalIds: [] });
|
|
1920
|
+
}, [activeUnitId]);
|
|
1921
|
+
const [profSearch, setProfSearch] = useState("");
|
|
1922
|
+
useEffect(() => {
|
|
1923
|
+
if (!professionalsTruncated) return;
|
|
1924
|
+
const term = profSearch.trim();
|
|
1925
|
+
const timer = setTimeout(() => {
|
|
1926
|
+
void fetchProfessionals(term ? { search: term } : void 0);
|
|
1927
|
+
}, 250);
|
|
1928
|
+
return () => clearTimeout(timer);
|
|
1929
|
+
}, [profSearch, professionalsTruncated]);
|
|
1930
|
+
useEffect(() => {
|
|
1931
|
+
const api = calendarRef.current?.getApi();
|
|
1932
|
+
if (!api) return;
|
|
1933
|
+
if (!resourceCapable && currentView.startsWith("resource")) {
|
|
1934
|
+
api.changeView("dayGridMonth");
|
|
1935
|
+
} else if (resourceCapable && (currentView === "timeGridWeek" || currentView === "timeGridDay")) {
|
|
1936
|
+
api.changeView(viewForResources(currentView));
|
|
1937
|
+
}
|
|
1938
|
+
}, [resourceCapable, currentView]);
|
|
1939
|
+
useEffect(() => {
|
|
1940
|
+
if (!isMobile) return;
|
|
1941
|
+
const start = /* @__PURE__ */ new Date();
|
|
1942
|
+
start.setHours(0, 0, 0, 0);
|
|
1943
|
+
start.setDate(start.getDate() - 1);
|
|
1944
|
+
const end = new Date(start);
|
|
1945
|
+
end.setDate(end.getDate() + 91);
|
|
1946
|
+
fetchBookings({ start: start.toISOString(), end: end.toISOString() });
|
|
1947
|
+
}, [isMobile, fetchBookings]);
|
|
1948
|
+
const refreshVisibleBookings = useCallback(() => {
|
|
1949
|
+
if (visibleRange) {
|
|
1950
|
+
void fetchBookings(visibleRange);
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
const start = /* @__PURE__ */ new Date();
|
|
1954
|
+
start.setHours(0, 0, 0, 0);
|
|
1955
|
+
start.setDate(start.getDate() - 1);
|
|
1956
|
+
const end = new Date(start);
|
|
1957
|
+
end.setDate(end.getDate() + 91);
|
|
1958
|
+
void fetchBookings({ start: start.toISOString(), end: end.toISOString() });
|
|
1959
|
+
}, [visibleRange, fetchBookings]);
|
|
1960
|
+
useDataChanged({}, refreshVisibleBookings, [refreshVisibleBookings]);
|
|
1961
|
+
useEffect(() => {
|
|
1962
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
1963
|
+
let refreshTimer;
|
|
1964
|
+
let unsubscribe;
|
|
1965
|
+
const scheduleRefresh = (delay = 0) => {
|
|
1966
|
+
if (document.visibilityState === "hidden") return;
|
|
1967
|
+
if (refreshTimer) clearTimeout(refreshTimer);
|
|
1968
|
+
refreshTimer = setTimeout(refreshVisibleBookings, delay);
|
|
1969
|
+
};
|
|
1970
|
+
try {
|
|
1971
|
+
unsubscribe = provider.subscribeToBookingChanges?.(() => scheduleRefresh(AGENDA_REALTIME_DEBOUNCE_MS));
|
|
1972
|
+
} catch (error) {
|
|
1973
|
+
console.warn("[agenda] Realtime unavailable; using the polling fallback.", error);
|
|
1974
|
+
}
|
|
1975
|
+
const pollTimer = window.setInterval(() => scheduleRefresh(), AGENDA_POLL_INTERVAL_MS);
|
|
1976
|
+
const onVisibilityChange = () => {
|
|
1977
|
+
if (document.visibilityState === "visible") scheduleRefresh();
|
|
1978
|
+
};
|
|
1979
|
+
const onFocus = () => scheduleRefresh();
|
|
1980
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
1981
|
+
window.addEventListener("focus", onFocus);
|
|
1982
|
+
return () => {
|
|
1983
|
+
if (refreshTimer) clearTimeout(refreshTimer);
|
|
1984
|
+
window.clearInterval(pollTimer);
|
|
1985
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
1986
|
+
window.removeEventListener("focus", onFocus);
|
|
1987
|
+
unsubscribe?.();
|
|
1988
|
+
};
|
|
1989
|
+
}, [provider, refreshVisibleBookings]);
|
|
1990
|
+
const [catalogServices, setCatalogServices] = useState([]);
|
|
1991
|
+
useEffect(() => {
|
|
1992
|
+
const lookup = config.serviceLookup;
|
|
1993
|
+
if (!lookup?.list) {
|
|
1994
|
+
setCatalogServices([]);
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
let alive = true;
|
|
1998
|
+
void lookup.list().then((rows) => {
|
|
1999
|
+
if (!alive) return;
|
|
2000
|
+
setCatalogServices(rows.map((r) => ({ value: r.id, label: r.label })));
|
|
2001
|
+
}).catch(() => {
|
|
2002
|
+
if (alive) setCatalogServices([]);
|
|
2003
|
+
});
|
|
2004
|
+
return () => {
|
|
2005
|
+
alive = false;
|
|
2006
|
+
};
|
|
2007
|
+
}, [config.serviceLookup]);
|
|
2008
|
+
const serviceOptions = useMemo(() => {
|
|
2009
|
+
const map = /* @__PURE__ */ new Map();
|
|
2010
|
+
for (const option of catalogServices) map.set(option.value, option.label);
|
|
2011
|
+
for (const booking of bookings) {
|
|
2012
|
+
for (const service of booking.services ?? []) {
|
|
2013
|
+
if (service.serviceId) map.set(service.serviceId, service.name);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
return [...map].map(([value, label]) => ({ value, label })).sort((a, b) => a.label.localeCompare(b.label));
|
|
2017
|
+
}, [catalogServices, bookings]);
|
|
2018
|
+
const filteredBookings = useMemo(() => {
|
|
2019
|
+
if (serviceFilter.length === 0) return bookings;
|
|
2020
|
+
const wanted = new Set(serviceFilter);
|
|
2021
|
+
return bookings.filter((b) => (b.services ?? []).some((sv) => sv.serviceId && wanted.has(sv.serviceId)));
|
|
2022
|
+
}, [bookings, serviceFilter]);
|
|
2023
|
+
const drawnProfessionals = useMemo(() => {
|
|
2024
|
+
const byName = (a, b) => a.name.localeCompare(b.name);
|
|
2025
|
+
if (selectedProfIds.length === 0) {
|
|
2026
|
+
const period = [...agendaProfessionals].filter((p) => relevantSet.has(p.id)).sort(byName);
|
|
2027
|
+
if (!hideEmptyGrids) return period;
|
|
2028
|
+
const busyNow = new Set(filteredBookings.map((b) => b.professionalId).filter(Boolean));
|
|
2029
|
+
return period.filter((p) => busyNow.has(p.id));
|
|
2030
|
+
}
|
|
2031
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2032
|
+
for (const p of agendaProfessionals) byId.set(p.id, p);
|
|
2033
|
+
for (const p of professionals) if (!byId.has(p.id)) byId.set(p.id, p);
|
|
2034
|
+
const picked = [...byId.values()].filter((p) => selectedProfIds.includes(p.id)).sort(byName);
|
|
2035
|
+
if (!hideEmptyGrids) return picked;
|
|
2036
|
+
const busy = new Set(filteredBookings.map((b) => b.professionalId).filter(Boolean));
|
|
2037
|
+
return picked.filter((p) => busy.has(p.id));
|
|
2038
|
+
}, [agendaProfessionals, professionals, selectedProfIds, relevantSet, hideEmptyGrids, filteredBookings]);
|
|
2039
|
+
const columnsSettled = useMemo(() => {
|
|
2040
|
+
if (selectedProfIds.length > 0) return true;
|
|
2041
|
+
if (!scheduleQuery) return false;
|
|
2042
|
+
if (schedulesKey !== wantedSchedulesKey) return false;
|
|
2043
|
+
return agendaProfessionalsKey === "agenda:columns:" + [...relevantProfIds].sort().join(",");
|
|
2044
|
+
}, [selectedProfIds, scheduleQuery, schedulesKey, wantedSchedulesKey, agendaProfessionalsKey, relevantProfIds]);
|
|
2045
|
+
const semGradeNoPeriodo = useMemo(() => {
|
|
2046
|
+
if (!columnsSettled || schedulesLoading || visibleDates.length === 0) return false;
|
|
2047
|
+
for (const [profId] of scheduleIndex) {
|
|
2048
|
+
if (visibleDates.some((d) => opensOn(profId, d.iso, d.dow))) return false;
|
|
2049
|
+
}
|
|
2050
|
+
return true;
|
|
2051
|
+
}, [columnsSettled, schedulesLoading, visibleDates, scheduleIndex, opensOn]);
|
|
2052
|
+
const resources = useMemo(() => drawnProfessionals.map((p) => ({
|
|
2053
|
+
id: p.id,
|
|
2054
|
+
title: p.name,
|
|
2055
|
+
// Sem `locationName`: ele SEMPRE foi nulo (o mapeador nunca o preencheu),
|
|
2056
|
+
// então a segunda linha do cabeçalho da coluna nunca desenhou. E agora que
|
|
2057
|
+
// a pessoa pode atender em VÁRIAS unidades não há um nome para pôr ali —
|
|
2058
|
+
// além de a grade já estar recortada por uma unidade, o que faria o
|
|
2059
|
+
// subtítulo repetir a mesma palavra em toda coluna.
|
|
2060
|
+
extendedProps: { avatarUrl: p.avatarUrl }
|
|
2061
|
+
})), [drawnProfessionals]);
|
|
2062
|
+
const visibleResourceIds = useMemo(() => resources.map((resource) => resource.id), [resources]);
|
|
2063
|
+
const weekGrouping = weekGroupingPref === "auto" ? resources.length <= 1 ? "professional" : "day" : weekGroupingPref;
|
|
2064
|
+
const gridMinWidth = useMemo(() => {
|
|
2065
|
+
if (!currentView.startsWith("resource")) return void 0;
|
|
2066
|
+
const daysInView = currentView.endsWith("Day") ? 1 : 7;
|
|
2067
|
+
const columns = Math.max(1, resources.length) * daysInView;
|
|
2068
|
+
const folhaEPessoa = currentView.endsWith("Day") || weekGrouping === "day";
|
|
2069
|
+
const floor = folhaEPessoa ? Math.max(MIN_COLUMN_PX, RESOURCE_COLUMN_MIN_PX) : MIN_COLUMN_PX;
|
|
2070
|
+
return columns * floor + 64;
|
|
2071
|
+
}, [currentView, resources.length, weekGrouping]);
|
|
2072
|
+
const blockEvents = useMemo(() => {
|
|
2073
|
+
if (!visibleRange || blocks.length === 0) return [];
|
|
2074
|
+
const out = [];
|
|
2075
|
+
const semanasEntre = (de, ate) => Math.floor(((/* @__PURE__ */ new Date(ate + "T00:00:00")).getTime() - (/* @__PURE__ */ new Date(de + "T00:00:00")).getTime()) / 6048e5);
|
|
2076
|
+
for (const b of blocks) {
|
|
2077
|
+
if (!b.assigneeId) continue;
|
|
2078
|
+
const titulo = b.metadata?.agenda?.title ?? "Bloqueio";
|
|
2079
|
+
for (const { iso, dow: dow2 } of visibleDates) {
|
|
2080
|
+
const dentroDaVigencia = (!b.validFrom || b.validFrom <= iso) && (!b.validTo || iso <= b.validTo);
|
|
2081
|
+
let vale = false;
|
|
2082
|
+
if (b.specificDate) vale = b.specificDate === iso;
|
|
2083
|
+
else if (b.dayOfWeek == null) vale = dentroDaVigencia;
|
|
2084
|
+
else if (b.dayOfWeek === dow2 && dentroDaVigencia) {
|
|
2085
|
+
const passo = (b.weekInterval ?? 0) + 1;
|
|
2086
|
+
vale = passo <= 1 || !b.validFrom || semanasEntre(b.validFrom, iso) % passo === 0;
|
|
2087
|
+
}
|
|
2088
|
+
if (!vale) continue;
|
|
2089
|
+
out.push({
|
|
2090
|
+
id: `block-${b.id}-${iso}`,
|
|
2091
|
+
resourceId: b.assigneeId,
|
|
2092
|
+
title: titulo,
|
|
2093
|
+
start: `${iso}T${b.startsAt}`,
|
|
2094
|
+
end: `${iso}T${b.endsAt}`,
|
|
2095
|
+
// Arrastável: mudar o almoço de 12h para 12h30 é o ajuste mais comum
|
|
2096
|
+
// que existe nesta tela, e obrigar a abrir um diálogo para quinze
|
|
2097
|
+
// minutos é o tipo de caminho que faz a pessoa deixar errado.
|
|
2098
|
+
editable: true,
|
|
2099
|
+
// A cor vai no EVENTO, com as variáveis do tema: o FullCalendar
|
|
2100
|
+
// pinta o fundo inline a partir da cor do calendário, e uma regra de
|
|
2101
|
+
// folha externa perdia a corrida em recarga quente — o bloqueio saía
|
|
2102
|
+
// azul, idêntico a um atendimento. Inline é determinístico, e as
|
|
2103
|
+
// `var()` continuam respondendo ao tema claro/escuro.
|
|
2104
|
+
backgroundColor: "hsl(var(--muted-foreground) / 0.16)",
|
|
2105
|
+
borderColor: "hsl(var(--muted-foreground) / 0.4)",
|
|
2106
|
+
textColor: "hsl(var(--foreground))",
|
|
2107
|
+
classNames: ["fc-agenda-block"],
|
|
2108
|
+
extendedProps: { isBlock: true, blockId: b.id }
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
return out;
|
|
2113
|
+
}, [blocks, visibleDates, visibleRange]);
|
|
2114
|
+
const cardPrefs = useTenantPluginSettings("agenda", { cardColorByServiceKind: false });
|
|
2115
|
+
const colorByKind = cardPrefs.get("cardColorByServiceKind");
|
|
2116
|
+
const serviceIdsKey = useMemo(() => {
|
|
2117
|
+
if (!colorByKind) return "";
|
|
2118
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2119
|
+
for (const b of filteredBookings) for (const sv of b.services ?? []) if (sv.serviceId) ids.add(sv.serviceId);
|
|
2120
|
+
return [...ids].sort().join(",");
|
|
2121
|
+
}, [colorByKind, filteredBookings]);
|
|
2122
|
+
const [serviceLooks, setServiceLooks] = useState(void 0);
|
|
2123
|
+
useEffect(() => {
|
|
2124
|
+
if (!serviceIdsKey || !provider.getServiceAppearance) {
|
|
2125
|
+
setServiceLooks(void 0);
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
let alive = true;
|
|
2129
|
+
provider.getServiceAppearance(serviceIdsKey.split(",")).then((m) => {
|
|
2130
|
+
if (alive) setServiceLooks(m);
|
|
2131
|
+
}).catch(() => {
|
|
2132
|
+
if (alive) setServiceLooks(void 0);
|
|
2133
|
+
});
|
|
2134
|
+
return () => {
|
|
2135
|
+
alive = false;
|
|
2136
|
+
};
|
|
2137
|
+
}, [serviceIdsKey, provider]);
|
|
2138
|
+
const bookingEvents = useMemo(
|
|
2139
|
+
() => filteredBookings.map((b) => bookingToEvent(b, statusColors, visibleResourceIds, colorByKind ? serviceLooks : void 0)),
|
|
2140
|
+
[filteredBookings, statusColors, visibleResourceIds, colorByKind, serviceLooks]
|
|
2141
|
+
);
|
|
2142
|
+
const allLocations = useMemo(() => {
|
|
2143
|
+
const map = /* @__PURE__ */ new Map();
|
|
2144
|
+
for (const loc of config.locations) map.set(loc.id, loc.name);
|
|
2145
|
+
for (const loc of storeLocations) map.set(loc.id, loc.name);
|
|
2146
|
+
return map;
|
|
2147
|
+
}, [config.locations, storeLocations]);
|
|
2148
|
+
const locationMap = useMemo(() => {
|
|
2149
|
+
if (!config.modules.locationSelection || allLocations.size === 0) return void 0;
|
|
2150
|
+
return allLocations;
|
|
2151
|
+
}, [config.modules.locationSelection, allLocations]);
|
|
2152
|
+
const defaultLocationName = useMemo(() => {
|
|
2153
|
+
if (allLocations.size === 1) return [...allLocations.values()][0];
|
|
2154
|
+
return t("agenda.calendar.defaultLocation");
|
|
2155
|
+
}, [allLocations, t]);
|
|
2156
|
+
const availabilityEvents = useMemo(
|
|
2157
|
+
() => generateAvailabilityEvents(
|
|
2158
|
+
schedules,
|
|
2159
|
+
drawnProfessionals,
|
|
2160
|
+
visibleRange,
|
|
2161
|
+
effectiveBusinessHours,
|
|
2162
|
+
locationMap,
|
|
2163
|
+
defaultLocationName
|
|
2164
|
+
),
|
|
2165
|
+
[schedules, drawnProfessionals, visibleRange, effectiveBusinessHours, locationMap, defaultLocationName]
|
|
2166
|
+
);
|
|
2167
|
+
const events = useMemo(() => {
|
|
2168
|
+
const ehLista = currentView.startsWith("list");
|
|
2169
|
+
const disponibilidade = ehLista ? availabilityEvents.filter((e) => !e.extendedProps?.isLocationLabel && e.display !== "background") : availabilityEvents;
|
|
2170
|
+
return [...bookingEvents, ...disponibilidade, ...blockEvents];
|
|
2171
|
+
}, [bookingEvents, availabilityEvents, blockEvents, currentView]);
|
|
2172
|
+
const storedView = prefs.get("calendarView");
|
|
2173
|
+
const adminPath = useAdminPath();
|
|
2174
|
+
const urlTarget = useMemo(() => parseAgendaPath(adminPath), [adminPath]);
|
|
2175
|
+
const effectiveInitialView = urlTarget ? calendarViewFor(urlTarget.view, resourceCapable) : resourceCapable ? viewForResources(storedView) : viewForNoResources(storedView);
|
|
2176
|
+
const handleDatesSet = useCallback((arg) => {
|
|
2177
|
+
setCalendarTitle(arg.view.title);
|
|
2178
|
+
setView(arg.view.type);
|
|
2179
|
+
setVisibleRange({ start: arg.startStr, end: arg.endStr });
|
|
2180
|
+
fetchBookings({ start: arg.startStr, end: arg.endStr });
|
|
2181
|
+
const here = currentAppPath();
|
|
2182
|
+
if (parseAgendaPath(here)) {
|
|
2183
|
+
const api = calendarRef.current?.getApi();
|
|
2184
|
+
const next = agendaPath(urlViewOf(arg.view.type), api?.getDate());
|
|
2185
|
+
if (next !== here) {
|
|
2186
|
+
try {
|
|
2187
|
+
window.history.replaceState(null, "", appHref(next));
|
|
2188
|
+
} catch {
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
}, [fetchBookings, setView]);
|
|
2193
|
+
useEffect(() => {
|
|
2194
|
+
if (!urlTarget) return;
|
|
2195
|
+
const api = calendarRef.current?.getApi();
|
|
2196
|
+
if (!api) return;
|
|
2197
|
+
const view = calendarViewFor(urlTarget.view, resourceCapable);
|
|
2198
|
+
const sameDay = !urlTarget.date || api.getDate().toDateString() === urlTarget.date.toDateString();
|
|
2199
|
+
if (api.view.type === view && sameDay) return;
|
|
2200
|
+
api.changeView(view);
|
|
2201
|
+
if (urlTarget.date) {
|
|
2202
|
+
api.gotoDate(urlTarget.date);
|
|
2203
|
+
setSelectedDate(urlTarget.date);
|
|
2204
|
+
}
|
|
2205
|
+
}, [urlTarget, resourceCapable]);
|
|
2206
|
+
const openDay = useCallback((date) => {
|
|
2207
|
+
const api = calendarRef.current?.getApi();
|
|
2208
|
+
if (api && !parseAgendaPath(currentAppPath())) {
|
|
2209
|
+
try {
|
|
2210
|
+
window.history.replaceState(null, "", appHref(agendaPath(urlViewOf(api.view.type), api.getDate())));
|
|
2211
|
+
} catch {
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
adminNavigateTo(agendaPath("day", date));
|
|
2215
|
+
}, []);
|
|
2216
|
+
const handleEventDidMount = useCallback((arg) => {
|
|
2217
|
+
const booking = arg.event.extendedProps?.booking;
|
|
2218
|
+
if (booking) arg.el.setAttribute("data-booking-id", booking.id);
|
|
2219
|
+
const temAgendamento = () => !!arg.event.extendedProps?.booking;
|
|
2220
|
+
arg.el.addEventListener("dblclick", (e) => {
|
|
2221
|
+
if (!temAgendamento()) return;
|
|
2222
|
+
e.preventDefault();
|
|
2223
|
+
e.stopPropagation();
|
|
2224
|
+
skipClickRef.current = true;
|
|
2225
|
+
const booking2 = arg.event.extendedProps.booking;
|
|
2226
|
+
setSelectedBooking(null);
|
|
2227
|
+
setPopoverAnchor(null);
|
|
2228
|
+
setContextMenu(null);
|
|
2229
|
+
openModal("edit", { bookingId: booking2.id });
|
|
2230
|
+
});
|
|
2231
|
+
arg.el.addEventListener("contextmenu", (e) => {
|
|
2232
|
+
if (!temAgendamento()) return;
|
|
2233
|
+
e.preventDefault();
|
|
2234
|
+
const booking2 = arg.event.extendedProps.booking;
|
|
2235
|
+
const menuW = 210, menuH = 200;
|
|
2236
|
+
let x = e.clientX + 4;
|
|
2237
|
+
let y = e.clientY + 4;
|
|
2238
|
+
if (x + menuW > window.innerWidth - 12) x = x - menuW;
|
|
2239
|
+
if (y + menuH > window.innerHeight - 12) y = y - menuH;
|
|
2240
|
+
setContextMenu({ booking: booking2, x, y });
|
|
2241
|
+
setContextMenuVisible(true);
|
|
2242
|
+
setSelectedBooking(null);
|
|
2243
|
+
setPopoverAnchor(null);
|
|
2244
|
+
});
|
|
2245
|
+
}, []);
|
|
2246
|
+
const skipClickRef = useRef(false);
|
|
2247
|
+
const hoverTimerRef = useRef(null);
|
|
2248
|
+
const handleEventMouseEnter = useCallback((arg) => {
|
|
2249
|
+
if (arg.event.display === "background") return;
|
|
2250
|
+
if (arg.event.extendedProps?.isLocationLabel) return;
|
|
2251
|
+
const booking = arg.event.extendedProps.booking;
|
|
2252
|
+
if (!booking) return;
|
|
2253
|
+
hoverTimerRef.current = setTimeout(() => {
|
|
2254
|
+
const rect = arg.el.getBoundingClientRect();
|
|
2255
|
+
const popW = 320, popH = 340;
|
|
2256
|
+
let x = rect.right + 8;
|
|
2257
|
+
let y = rect.top + rect.height / 2 - popH / 2;
|
|
2258
|
+
if (x + popW > window.innerWidth - 16) x = rect.left - popW - 8;
|
|
2259
|
+
if (x < 16) x = Math.max(16, rect.left + rect.width / 2 - popW / 2);
|
|
2260
|
+
if (y + popH > window.innerHeight - 16) y = window.innerHeight - popH - 16;
|
|
2261
|
+
if (y < 16) y = 16;
|
|
2262
|
+
setSelectedBooking(booking);
|
|
2263
|
+
setPopoverAnchor({ x, y });
|
|
2264
|
+
setPopoverVisible(true);
|
|
2265
|
+
}, 3e3);
|
|
2266
|
+
}, []);
|
|
2267
|
+
const handleEventMouseLeave = useCallback(() => {
|
|
2268
|
+
if (hoverTimerRef.current) {
|
|
2269
|
+
clearTimeout(hoverTimerRef.current);
|
|
2270
|
+
hoverTimerRef.current = null;
|
|
2271
|
+
}
|
|
2272
|
+
}, []);
|
|
2273
|
+
const handleEventClick = useCallback((arg) => {
|
|
2274
|
+
if (skipClickRef.current) {
|
|
2275
|
+
skipClickRef.current = false;
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
if (arg.event.display === "background") return;
|
|
2279
|
+
if (arg.event.extendedProps?.isLocationLabel) return;
|
|
2280
|
+
if (arg.event.extendedProps?.isBlock) {
|
|
2281
|
+
setContextMenu(null);
|
|
2282
|
+
setContextMenuVisible(false);
|
|
2283
|
+
const clicado = blocks.find((b) => b.id === arg.event.extendedProps.blockId);
|
|
2284
|
+
if (clicado) setEditingBlock(toEditingBlock(clicado, blocks));
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
setContextMenu(null);
|
|
2288
|
+
setContextMenuVisible(false);
|
|
2289
|
+
const booking = arg.event.extendedProps.booking;
|
|
2290
|
+
if (!booking) return;
|
|
2291
|
+
const rect = arg.el.getBoundingClientRect();
|
|
2292
|
+
const popW = 320, popH = 340;
|
|
2293
|
+
let x = rect.right + 8;
|
|
2294
|
+
let y = rect.top + rect.height / 2 - popH / 2;
|
|
2295
|
+
if (x + popW > window.innerWidth - 16) x = rect.left - popW - 8;
|
|
2296
|
+
if (x < 16) x = Math.max(16, rect.left + rect.width / 2 - popW / 2);
|
|
2297
|
+
if (y + popH > window.innerHeight - 16) y = window.innerHeight - popH - 16;
|
|
2298
|
+
if (y < 16) y = 16;
|
|
2299
|
+
setSelectedBooking(booking);
|
|
2300
|
+
setPopoverAnchor({ x, y });
|
|
2301
|
+
setPopoverVisible(true);
|
|
2302
|
+
}, [blocks]);
|
|
2303
|
+
const findScheduleForSlot = useCallback((professionalId, dateTimeStr) => {
|
|
2304
|
+
if (!professionalId) return null;
|
|
2305
|
+
const dt = new Date(dateTimeStr);
|
|
2306
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
2307
|
+
const dateStr = `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())}`;
|
|
2308
|
+
const timeMin = dt.getHours() * 60 + dt.getMinutes();
|
|
2309
|
+
const profSchedules = schedules.filter((s) => s.assigneeId === professionalId);
|
|
2310
|
+
const daySchedules = effectiveWindows(profSchedules, dateStr);
|
|
2311
|
+
for (const s of daySchedules) {
|
|
2312
|
+
const [sh, sm] = s.startsAt.split(":").map(Number);
|
|
2313
|
+
const [eh, em] = s.endsAt.split(":").map(Number);
|
|
2314
|
+
if (timeMin >= sh * 60 + sm && timeMin < eh * 60 + em) return s;
|
|
2315
|
+
}
|
|
2316
|
+
return null;
|
|
2317
|
+
}, [schedules]);
|
|
2318
|
+
const lastSlotOpenRef = useRef(0);
|
|
2319
|
+
const openCreateForSlot = useCallback((startStr, endStr, professionalId) => {
|
|
2320
|
+
calendarRef.current?.getApi().unselect();
|
|
2321
|
+
const now = Date.now();
|
|
2322
|
+
if (now - lastSlotOpenRef.current < 500) return;
|
|
2323
|
+
lastSlotOpenRef.current = now;
|
|
2324
|
+
if (popoverVisibleRef.current || contextMenuVisibleRef.current) {
|
|
2325
|
+
setPopoverVisible(false);
|
|
2326
|
+
setSelectedBooking(null);
|
|
2327
|
+
setPopoverAnchor(null);
|
|
2328
|
+
setContextMenuVisible(false);
|
|
2329
|
+
setContextMenu(null);
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2332
|
+
const schedule = findScheduleForSlot(professionalId, startStr);
|
|
2333
|
+
const proceed = () => {
|
|
2334
|
+
const locId = schedule?.locationId ?? schedule?.metadata?.locationId;
|
|
2335
|
+
openModal("create", { prefill: { startsAt: startStr, endsAt: endStr, professionalId, locationId: locId || void 0 } });
|
|
2336
|
+
};
|
|
2337
|
+
if (config.modules.workingHours && !schedule) {
|
|
2338
|
+
setScheduleConfirm({ onConfirm: proceed });
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
proceed();
|
|
2342
|
+
}, [openModal, findScheduleForSlot, config.modules.workingHours]);
|
|
2343
|
+
const handleSelect = useCallback((arg) => {
|
|
2344
|
+
openCreateForSlot(arg.startStr, arg.endStr, arg.resource?.id);
|
|
2345
|
+
}, [openCreateForSlot]);
|
|
2346
|
+
const handleDateClick = useCallback((arg) => {
|
|
2347
|
+
const professionalId = arg.resource?.id;
|
|
2348
|
+
if (arg.allDay) {
|
|
2349
|
+
openCreateForSlot(arg.dateStr, localDateKey(new Date(arg.date.getTime() + 864e5)), professionalId);
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
const end = new Date(arg.date.getTime() + config.slotDuration * 6e4);
|
|
2353
|
+
openCreateForSlot(localDateTimeString(arg.date), localDateTimeString(end), professionalId);
|
|
2354
|
+
}, [openCreateForSlot, config.slotDuration]);
|
|
2355
|
+
const handleEventDrop = useCallback(async (arg) => {
|
|
2356
|
+
if (arg.event.extendedProps?.isBlock) {
|
|
2357
|
+
const clicado = blocks.find((b) => b.id === arg.event.extendedProps.blockId);
|
|
2358
|
+
if (!clicado || !provider.createBlock || !provider.deleteBlock) {
|
|
2359
|
+
arg.revert();
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
const grupo = toEditingBlock(clicado, blocks);
|
|
2363
|
+
const hhmm = (iso) => new Date(iso).toTimeString().slice(0, 5);
|
|
2364
|
+
try {
|
|
2365
|
+
await provider.deleteBlock(grupo.ids);
|
|
2366
|
+
await provider.createBlock({
|
|
2367
|
+
assigneeId: arg.newResource?.id ?? grupo.assigneeId,
|
|
2368
|
+
title: grupo.title,
|
|
2369
|
+
description: grupo.description || void 0,
|
|
2370
|
+
startsAt: hhmm(arg.event.startStr),
|
|
2371
|
+
endsAt: hhmm(arg.event.endStr),
|
|
2372
|
+
...grupo.specificDate ? { specificDate: arg.event.startStr.slice(0, 10) } : {
|
|
2373
|
+
daysOfWeek: grupo.daysOfWeek,
|
|
2374
|
+
validFrom: grupo.validFrom ?? void 0,
|
|
2375
|
+
validTo: grupo.validTo,
|
|
2376
|
+
weekInterval: grupo.weekInterval
|
|
2377
|
+
}
|
|
2378
|
+
});
|
|
2379
|
+
toast.success(grupo.ids.length > 1 ? `${grupo.title} movido em ${grupo.ids.length} dias` : `${grupo.title} movido`);
|
|
2380
|
+
invalidateSchedules();
|
|
2381
|
+
} catch (err) {
|
|
2382
|
+
arg.revert();
|
|
2383
|
+
toast.error("N\xE3o foi poss\xEDvel mover o bloqueio", { description: err?.message });
|
|
2384
|
+
}
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
const booking = arg.event.extendedProps.booking;
|
|
2388
|
+
try {
|
|
2389
|
+
await rescheduleBooking(booking.id, arg.event.startStr, arg.event.endStr, arg.newResource?.id ?? void 0);
|
|
2390
|
+
} catch {
|
|
2391
|
+
arg.revert();
|
|
2392
|
+
}
|
|
2393
|
+
}, [rescheduleBooking, blocks, provider, invalidateSchedules]);
|
|
2394
|
+
const handleViewChange = useCallback((v) => {
|
|
2395
|
+
calendarRef.current?.getApi().changeView(v);
|
|
2396
|
+
prefs.set("calendarView", v);
|
|
2397
|
+
}, [prefs]);
|
|
2398
|
+
const handleMobileViewChange = useCallback((v) => {
|
|
2399
|
+
setMobileView(v);
|
|
2400
|
+
if (v === MOBILE_LIST_VIEW) return;
|
|
2401
|
+
setView(v);
|
|
2402
|
+
calendarRef.current?.getApi()?.changeView(v);
|
|
2403
|
+
}, [setView]);
|
|
2404
|
+
const handlePrev = useCallback(() => calendarRef.current?.getApi().prev(), []);
|
|
2405
|
+
const handleNext = useCallback(() => calendarRef.current?.getApi().next(), []);
|
|
2406
|
+
const handleToday = useCallback(() => {
|
|
2407
|
+
calendarRef.current?.getApi().today();
|
|
2408
|
+
setSelectedDate(/* @__PURE__ */ new Date());
|
|
2409
|
+
}, []);
|
|
2410
|
+
function handleMiniDateSelect(date) {
|
|
2411
|
+
setSelectedDate(date);
|
|
2412
|
+
calendarRef.current?.getApi().gotoDate(date);
|
|
2413
|
+
}
|
|
2414
|
+
function toggleStatus(value) {
|
|
2415
|
+
const next = selectedStatuses.includes(value) ? selectedStatuses.filter((s) => s !== value) : [...selectedStatuses, value];
|
|
2416
|
+
setFilters({ statuses: next });
|
|
2417
|
+
}
|
|
2418
|
+
const wrapperRef = useRef(null);
|
|
2419
|
+
useEffect(() => {
|
|
2420
|
+
const container = wrapperRef.current;
|
|
2421
|
+
if (!container) return;
|
|
2422
|
+
let frame = 0;
|
|
2423
|
+
let lastWidth = container.clientWidth;
|
|
2424
|
+
let lastHeight = container.clientHeight;
|
|
2425
|
+
const ro = new ResizeObserver(() => {
|
|
2426
|
+
const width = container.clientWidth;
|
|
2427
|
+
const height = container.clientHeight;
|
|
2428
|
+
if (width === lastWidth && height === lastHeight) return;
|
|
2429
|
+
lastWidth = width;
|
|
2430
|
+
lastHeight = height;
|
|
2431
|
+
cancelAnimationFrame(frame);
|
|
2432
|
+
frame = requestAnimationFrame(() => calendarRef.current?.getApi()?.updateSize());
|
|
2433
|
+
});
|
|
2434
|
+
ro.observe(container);
|
|
2435
|
+
return () => {
|
|
2436
|
+
cancelAnimationFrame(frame);
|
|
2437
|
+
ro.disconnect();
|
|
2438
|
+
};
|
|
2439
|
+
}, [isMobile, mobileGridActive, professionalsLoading, professionals.length]);
|
|
2440
|
+
const [dayHeaderCompact, setDayHeaderCompact] = useState(false);
|
|
2441
|
+
useEffect(() => {
|
|
2442
|
+
const container = wrapperRef.current;
|
|
2443
|
+
if (!container) return;
|
|
2444
|
+
let frame = 0;
|
|
2445
|
+
const measure = () => {
|
|
2446
|
+
const cell = container.querySelector(".fc-col-header-cell[data-date]");
|
|
2447
|
+
if (!cell) return;
|
|
2448
|
+
const colWidth = cell.getBoundingClientRect().width;
|
|
2449
|
+
const cushion = cell.querySelector(".fc-col-header-cell-cushion") ?? cell;
|
|
2450
|
+
const cs = getComputedStyle(cushion);
|
|
2451
|
+
const probe = document.createElement("span");
|
|
2452
|
+
probe.style.cssText = "position:absolute;visibility:hidden;white-space:nowrap;top:-9999px";
|
|
2453
|
+
probe.style.fontSize = cs.fontSize;
|
|
2454
|
+
probe.style.fontWeight = cs.fontWeight;
|
|
2455
|
+
probe.style.fontFamily = cs.fontFamily;
|
|
2456
|
+
probe.style.letterSpacing = cs.letterSpacing;
|
|
2457
|
+
probe.style.textTransform = cs.textTransform;
|
|
2458
|
+
document.body.appendChild(probe);
|
|
2459
|
+
probe.textContent = "Wed";
|
|
2460
|
+
const weekdayWidth = probe.getBoundingClientRect().width;
|
|
2461
|
+
probe.textContent = "12/14";
|
|
2462
|
+
const dateWidth = probe.getBoundingClientRect().width;
|
|
2463
|
+
probe.remove();
|
|
2464
|
+
const needed = Math.max(weekdayWidth, dateWidth) + 12;
|
|
2465
|
+
setDayHeaderCompact(colWidth < needed);
|
|
2466
|
+
};
|
|
2467
|
+
const timers = [];
|
|
2468
|
+
const schedule = () => {
|
|
2469
|
+
cancelAnimationFrame(frame);
|
|
2470
|
+
frame = requestAnimationFrame(measure);
|
|
2471
|
+
timers.forEach(clearTimeout);
|
|
2472
|
+
timers.length = 0;
|
|
2473
|
+
timers.push(setTimeout(measure, 130), setTimeout(measure, 330));
|
|
2474
|
+
};
|
|
2475
|
+
schedule();
|
|
2476
|
+
const ro = new ResizeObserver(schedule);
|
|
2477
|
+
ro.observe(container);
|
|
2478
|
+
return () => {
|
|
2479
|
+
cancelAnimationFrame(frame);
|
|
2480
|
+
timers.forEach(clearTimeout);
|
|
2481
|
+
ro.disconnect();
|
|
2482
|
+
};
|
|
2483
|
+
}, [resources, currentView, visibleRange]);
|
|
2484
|
+
const applyResourceStriping = useCallback(() => {
|
|
2485
|
+
const container = wrapperRef.current;
|
|
2486
|
+
if (!container) return;
|
|
2487
|
+
const resourceIds = resources.map((r) => r.id);
|
|
2488
|
+
const cells = Array.from(container.querySelectorAll("[data-resource-id]"));
|
|
2489
|
+
if (cells.length === 0) return;
|
|
2490
|
+
const dates = [];
|
|
2491
|
+
for (const el of cells) {
|
|
2492
|
+
const d = el.getAttribute("data-date");
|
|
2493
|
+
if (d && !dates.includes(d)) dates.push(d);
|
|
2494
|
+
}
|
|
2495
|
+
const groupByDate = dates.length > 1;
|
|
2496
|
+
const firstResource = resourceIds[0];
|
|
2497
|
+
for (const el of cells) {
|
|
2498
|
+
const rid = el.getAttribute("data-resource-id");
|
|
2499
|
+
const date = el.getAttribute("data-date");
|
|
2500
|
+
if (groupByDate) {
|
|
2501
|
+
const idx = date ? dates.indexOf(date) : -1;
|
|
2502
|
+
el.classList.toggle("fc-resource-alt", idx % 2 === 1);
|
|
2503
|
+
el.classList.toggle("fc-day-start", rid === firstResource);
|
|
2504
|
+
} else {
|
|
2505
|
+
const idx = rid ? resourceIds.indexOf(rid) : -1;
|
|
2506
|
+
el.classList.toggle("fc-resource-alt", resourceIds.length > 1 && idx % 2 === 1);
|
|
2507
|
+
el.classList.remove("fc-day-start");
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
}, [resources]);
|
|
2511
|
+
useEffect(() => {
|
|
2512
|
+
applyResourceStriping();
|
|
2513
|
+
const t1 = setTimeout(applyResourceStriping, 100);
|
|
2514
|
+
const t2 = setTimeout(applyResourceStriping, 300);
|
|
2515
|
+
return () => {
|
|
2516
|
+
clearTimeout(t1);
|
|
2517
|
+
clearTimeout(t2);
|
|
2518
|
+
};
|
|
2519
|
+
}, [applyResourceStriping, currentView, visibleRange, bookings]);
|
|
2520
|
+
useEffect(() => {
|
|
2521
|
+
function handleOpenBooking(e) {
|
|
2522
|
+
const detail = e.detail ?? {};
|
|
2523
|
+
const externalSource = detail.source ? { type: String(detail.source), id: detail.waitlistId, waitlistId: detail.waitlistId } : void 0;
|
|
2524
|
+
if (detail.bookingId) {
|
|
2525
|
+
openModal("edit", { bookingId: detail.bookingId, externalSource });
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
openModal("create", {
|
|
2529
|
+
prefill: detail.prefill ?? {
|
|
2530
|
+
clientId: detail.clientId,
|
|
2531
|
+
professionalId: detail.professionalId,
|
|
2532
|
+
locationId: detail.locationId,
|
|
2533
|
+
startsAt: detail.startsAt
|
|
2534
|
+
},
|
|
2535
|
+
externalSource
|
|
2536
|
+
});
|
|
2537
|
+
}
|
|
2538
|
+
const handleOpenSchedules = (e) => {
|
|
2539
|
+
const detail = e.detail ?? {};
|
|
2540
|
+
if (detail.professionalId) setFilters({ professionalIds: [detail.professionalId] });
|
|
2541
|
+
setSchedulesOpen(true);
|
|
2542
|
+
setSchedulesFor((e.detail ?? {}).professionalId ?? null);
|
|
2543
|
+
};
|
|
2544
|
+
const handleOpenBlock = (e) => {
|
|
2545
|
+
const detail = e.detail ?? {};
|
|
2546
|
+
setBlockFor({ professionalId: detail.professionalId ?? null, date: detail.date ?? null });
|
|
2547
|
+
};
|
|
2548
|
+
window.addEventListener("agenda:open-booking", handleOpenBooking);
|
|
2549
|
+
window.addEventListener("agenda:open-schedules", handleOpenSchedules);
|
|
2550
|
+
window.addEventListener("agenda:open-block", handleOpenBlock);
|
|
2551
|
+
return () => {
|
|
2552
|
+
window.removeEventListener("agenda:open-booking", handleOpenBooking);
|
|
2553
|
+
window.removeEventListener("agenda:open-schedules", handleOpenSchedules);
|
|
2554
|
+
window.removeEventListener("agenda:open-block", handleOpenBlock);
|
|
2555
|
+
};
|
|
2556
|
+
}, [openModal, setFilters]);
|
|
2557
|
+
const dayHeaderContent = useCallback((arg) => {
|
|
2558
|
+
const date = arg.date;
|
|
2559
|
+
const dateLocale = currentLocale === "pt-BR" ? "pt-BR" : "en";
|
|
2560
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
2561
|
+
const iso = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
2562
|
+
const noMes = arg.view?.type === "dayGridMonth";
|
|
2563
|
+
const feriado = noMes ? void 0 : holidayByDate.get(iso);
|
|
2564
|
+
const wrapStyle = { display: "flex", flexDirection: "column", alignItems: "center", lineHeight: 1.15, width: "fit-content", maxWidth: "100%", minWidth: 0, margin: "0 auto", boxSizing: "border-box" };
|
|
2565
|
+
const dayLink = String(arg.view?.type ?? "").endsWith("Week") ? {
|
|
2566
|
+
role: "link",
|
|
2567
|
+
tabIndex: 0,
|
|
2568
|
+
className: "agenda-day-link",
|
|
2569
|
+
onClick: () => openDay(date),
|
|
2570
|
+
onKeyDown: (e) => {
|
|
2571
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
2572
|
+
e.preventDefault();
|
|
2573
|
+
openDay(date);
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
} : {};
|
|
2577
|
+
const dateStyle = { fontSize: "0.65rem", opacity: 0.75 };
|
|
2578
|
+
if (dayHeaderCompact) {
|
|
2579
|
+
const letter = date.toLocaleDateString(dateLocale, { weekday: "narrow" });
|
|
2580
|
+
return /* @__PURE__ */ jsxs("div", { style: wrapStyle, title: feriado ? feriado.names.join(" \xB7 ") : void 0, ...dayLink, children: [
|
|
2581
|
+
/* @__PURE__ */ jsx("span", { style: { fontWeight: 600 }, children: letter }),
|
|
2582
|
+
/* @__PURE__ */ jsx("span", { style: dateStyle, children: date.getDate() }),
|
|
2583
|
+
feriado && /* @__PURE__ */ jsx(
|
|
2584
|
+
"span",
|
|
2585
|
+
{
|
|
2586
|
+
"aria-label": feriado.names.join(" \xB7 "),
|
|
2587
|
+
style: {
|
|
2588
|
+
width: 4,
|
|
2589
|
+
height: 4,
|
|
2590
|
+
borderRadius: 999,
|
|
2591
|
+
marginTop: 2,
|
|
2592
|
+
background: feriado.closes ? "hsl(var(--destructive))" : "hsl(var(--muted-foreground))"
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
)
|
|
2596
|
+
] });
|
|
2597
|
+
}
|
|
2598
|
+
const weekday = date.toLocaleDateString(dateLocale, { weekday: "short" });
|
|
2599
|
+
if (noMes) return /* @__PURE__ */ jsx("div", { style: wrapStyle, children: /* @__PURE__ */ jsx("span", { children: arg.text }) });
|
|
2600
|
+
const diaMes = date.toLocaleDateString(dateLocale, { day: "numeric", month: "numeric" });
|
|
2601
|
+
return /* @__PURE__ */ jsxs("div", { style: wrapStyle, ...dayLink, children: [
|
|
2602
|
+
/* @__PURE__ */ jsx("span", { children: weekday }),
|
|
2603
|
+
/* @__PURE__ */ jsx("span", { style: dateStyle, children: diaMes }),
|
|
2604
|
+
feriado && /* @__PURE__ */ jsxs(
|
|
2605
|
+
"span",
|
|
2606
|
+
{
|
|
2607
|
+
title: feriado.names.join(" \xB7 "),
|
|
2608
|
+
style: {
|
|
2609
|
+
fontSize: "0.6rem",
|
|
2610
|
+
lineHeight: 1.1,
|
|
2611
|
+
marginTop: 1,
|
|
2612
|
+
maxWidth: "100%",
|
|
2613
|
+
overflow: "hidden",
|
|
2614
|
+
textOverflow: "ellipsis",
|
|
2615
|
+
whiteSpace: "nowrap",
|
|
2616
|
+
fontWeight: feriado.closes ? 600 : 400,
|
|
2617
|
+
color: feriado.closes ? "hsl(var(--destructive))" : "hsl(var(--muted-foreground))"
|
|
2618
|
+
},
|
|
2619
|
+
children: [
|
|
2620
|
+
feriado.names[0],
|
|
2621
|
+
feriado.names.length > 1 ? ` +${feriado.names.length - 1}` : ""
|
|
2622
|
+
]
|
|
2623
|
+
}
|
|
2624
|
+
)
|
|
2625
|
+
] });
|
|
2626
|
+
}, [dayHeaderCompact, currentLocale, holidayByDate, openDay]);
|
|
2627
|
+
const dayCellContent = useCallback((arg) => {
|
|
2628
|
+
const date = arg.date;
|
|
2629
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
2630
|
+
const feriado = holidayByDate.get(`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`);
|
|
2631
|
+
return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", alignItems: "flex-end", maxWidth: "100%", minWidth: 0 }, children: [
|
|
2632
|
+
/* @__PURE__ */ jsx("span", { children: arg.dayNumberText }),
|
|
2633
|
+
feriado && /* @__PURE__ */ jsxs(
|
|
2634
|
+
"span",
|
|
2635
|
+
{
|
|
2636
|
+
title: feriado.names.join(" \xB7 "),
|
|
2637
|
+
style: {
|
|
2638
|
+
fontSize: "0.6rem",
|
|
2639
|
+
lineHeight: 1.1,
|
|
2640
|
+
maxWidth: "100%",
|
|
2641
|
+
overflow: "hidden",
|
|
2642
|
+
textOverflow: "ellipsis",
|
|
2643
|
+
whiteSpace: "nowrap",
|
|
2644
|
+
fontWeight: feriado.closes ? 600 : 400,
|
|
2645
|
+
color: feriado.closes ? "hsl(var(--destructive))" : "hsl(var(--muted-foreground))"
|
|
2646
|
+
},
|
|
2647
|
+
children: [
|
|
2648
|
+
feriado.names[0],
|
|
2649
|
+
feriado.names.length > 1 ? ` +${feriado.names.length - 1}` : ""
|
|
2650
|
+
]
|
|
2651
|
+
}
|
|
2652
|
+
)
|
|
2653
|
+
] });
|
|
2654
|
+
}, [holidayByDate]);
|
|
2655
|
+
const resourceLabelContent = useCallback((arg) => {
|
|
2656
|
+
const { id, title, extendedProps } = arg.resource;
|
|
2657
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 px-1.5 py-1.5 min-w-0", children: [
|
|
2658
|
+
/* @__PURE__ */ jsx(PersonAvatar, { name: title, src: extendedProps?.avatarUrl, size: "xs" }),
|
|
2659
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-1 overflow-hidden", children: [
|
|
2660
|
+
/* @__PURE__ */ jsx(
|
|
2661
|
+
PersonLink,
|
|
2662
|
+
{
|
|
2663
|
+
personId: id,
|
|
2664
|
+
name: abbreviatePersonName(title),
|
|
2665
|
+
avatarUrl: extendedProps?.avatarUrl,
|
|
2666
|
+
size: "sm",
|
|
2667
|
+
tab: "schedule",
|
|
2668
|
+
profileHref: appHref(`/registry/staff/${id}/schedule`)
|
|
2669
|
+
}
|
|
2670
|
+
),
|
|
2671
|
+
id === nextProfessionalId ? /* @__PURE__ */ jsx(Badge, { className: "shrink-0 border-foreground/10 bg-foreground px-1.5 py-0 text-[9px] leading-4 text-background", children: "Pr\xF3ximo" }) : null
|
|
2672
|
+
] })
|
|
2673
|
+
] });
|
|
2674
|
+
}, [nextProfessionalId]);
|
|
2675
|
+
const eventContent = useCallback((arg) => {
|
|
2676
|
+
if (arg.event.display === "background") return null;
|
|
2677
|
+
if (arg.event.extendedProps?.isLocationLabel) {
|
|
2678
|
+
return /* @__PURE__ */ jsx("div", { className: "px-1.5 py-0.5 overflow-hidden", children: /* @__PURE__ */ jsx("p", { className: "text-[10px] font-medium truncate", children: arg.event.title }) });
|
|
2679
|
+
}
|
|
2680
|
+
const serviceNames = arg.event.extendedProps?.serviceNames;
|
|
2681
|
+
const start = arg.event.start;
|
|
2682
|
+
const end = arg.event.end;
|
|
2683
|
+
const fmt = (d) => `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
2684
|
+
const timeRange = start && end ? `${fmt(start)} \u2013 ${fmt(end)}` : start ? fmt(start) : "";
|
|
2685
|
+
const stripe = arg.event.extendedProps?.statusStripe;
|
|
2686
|
+
return /* @__PURE__ */ jsxs("div", { className: `relative py-0.5 overflow-hidden leading-tight h-full ${stripe ? "pl-2.5 pr-1.5" : "px-1.5"}`, children: [
|
|
2687
|
+
stripe && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "absolute inset-y-0 left-0 w-1.5", style: { backgroundColor: stripe } }),
|
|
2688
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-1", children: [
|
|
2689
|
+
/* @__PURE__ */ jsx("p", { className: "text-[11px] font-semibold truncate", children: arg.event.title }),
|
|
2690
|
+
serviceNames && /* @__PURE__ */ jsx("p", { className: "text-[10px] opacity-60 truncate shrink-0 max-w-[45%] text-right", children: serviceNames })
|
|
2691
|
+
] }),
|
|
2692
|
+
timeRange && /* @__PURE__ */ jsx("p", { className: "text-[9px] opacity-50", children: timeRange })
|
|
2693
|
+
] });
|
|
2694
|
+
}, []);
|
|
2695
|
+
return /* @__PURE__ */ jsxs("div", { ref: shellRef, className: "flex", style: { height: shellHeight ?? void 0 }, children: [
|
|
2696
|
+
/* @__PURE__ */ jsx(AgendaRailColumn, { children: /* @__PURE__ */ jsx(
|
|
2697
|
+
AgendaFilterRail,
|
|
2698
|
+
{
|
|
2699
|
+
quickActions: [
|
|
2700
|
+
{ id: "block", label: t("agenda.sidebar.block"), icon: Ban, onClick: () => setBlockFor({ professionalId: null, date: null }) },
|
|
2701
|
+
...config.capabilities.professional ? [{ id: "working-hours", label: t("agenda.sidebar.workingHours"), icon: Clock3, onClick: () => setSchedulesOpen(true) }] : []
|
|
2702
|
+
],
|
|
2703
|
+
beforeMark: { label: "Calend\xE1rio" },
|
|
2704
|
+
before: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2705
|
+
/* @__PURE__ */ jsx(MiniCalendar, { selectedDate, onDateSelect: handleMiniDateSelect }),
|
|
2706
|
+
/* @__PURE__ */ jsx(
|
|
2707
|
+
LostSlotsCard,
|
|
2708
|
+
{
|
|
2709
|
+
range: visibleRange,
|
|
2710
|
+
selectedStatuses,
|
|
2711
|
+
onPick: toggleStatus
|
|
2712
|
+
}
|
|
2713
|
+
)
|
|
2714
|
+
] }),
|
|
2715
|
+
sections: [
|
|
2716
|
+
{
|
|
2717
|
+
id: "professionals",
|
|
2718
|
+
title: t("agenda.sidebar.professionals"),
|
|
2719
|
+
count: selectedProfIds.length,
|
|
2720
|
+
hidden: !config.capabilities.professional,
|
|
2721
|
+
// O mesmo primitivo do "quem atende" do agendamento, na versão de
|
|
2722
|
+
// marcar várias: o retrato é o que separa a Ana Caroline da Ana
|
|
2723
|
+
// Luiza numa coluna de 220px, onde o nome trunca antes do
|
|
2724
|
+
// sobrenome chegar.
|
|
2725
|
+
// Seis agendas ou menos: caixinhas, que se leem de uma vez. Mais
|
|
2726
|
+
// que isso, o campo com retrato e busca — é o que separa a Ana
|
|
2727
|
+
// Caroline da Ana Luiza numa coluna de 220px.
|
|
2728
|
+
content: pickerProfessionals.length > 0 && pickerProfessionals.length <= INLINE_FILTER_MAX && !professionalsTruncated ? /* @__PURE__ */ jsx(
|
|
2729
|
+
AdaptiveFilter,
|
|
2730
|
+
{
|
|
2731
|
+
options: pickerProfessionals.map((p) => ({
|
|
2732
|
+
value: p.id,
|
|
2733
|
+
// Abreviado para caber, ordenado e identificado pelo inteiro.
|
|
2734
|
+
label: p.name,
|
|
2735
|
+
avatarUrl: p.avatarUrl ?? null,
|
|
2736
|
+
hint: scheduledProfIds.has(p.id) ? void 0 : t("agenda.sidebar.noGrid")
|
|
2737
|
+
})),
|
|
2738
|
+
value: selectedProfIds,
|
|
2739
|
+
onChange: (ids) => setFilters({ professionalIds: ids }),
|
|
2740
|
+
allLabel: t("agenda.sidebar.allProfessionals")
|
|
2741
|
+
}
|
|
2742
|
+
) : /* @__PURE__ */ jsx(
|
|
2743
|
+
PersonMultiPicker,
|
|
2744
|
+
{
|
|
2745
|
+
options: pickerProfessionals.map((p) => ({
|
|
2746
|
+
id: p.id,
|
|
2747
|
+
name: p.name,
|
|
2748
|
+
avatarUrl: p.avatarUrl,
|
|
2749
|
+
hint: scheduledProfIds.has(p.id) ? void 0 : t("agenda.sidebar.noGrid")
|
|
2750
|
+
})),
|
|
2751
|
+
value: selectedProfIds,
|
|
2752
|
+
onChange: (ids) => setFilters({ professionalIds: ids }),
|
|
2753
|
+
max: MAX_SELECTED_AGENDAS,
|
|
2754
|
+
allLabel: t("agenda.sidebar.allProfessionals"),
|
|
2755
|
+
searchPlaceholder: t("agenda.sidebar.searchAgendas"),
|
|
2756
|
+
onSearchChange: professionalsTruncated ? setProfSearch : void 0
|
|
2757
|
+
}
|
|
2758
|
+
)
|
|
2759
|
+
},
|
|
2760
|
+
{
|
|
2761
|
+
id: "unit",
|
|
2762
|
+
title: t("agenda.sidebar.unit"),
|
|
2763
|
+
count: selectedLocationId ? 1 : 0,
|
|
2764
|
+
hidden: !config.modules.locationSelection || allLocations.size <= 1,
|
|
2765
|
+
content: /* @__PURE__ */ jsxs(
|
|
2766
|
+
Select,
|
|
2767
|
+
{
|
|
2768
|
+
value: selectedLocationId ?? "__all__",
|
|
2769
|
+
onValueChange: (v) => setFilters({ locationId: v === "__all__" ? null : v }),
|
|
2770
|
+
children: [
|
|
2771
|
+
/* @__PURE__ */ jsx(SelectTrigger, { className: "h-8 text-sm", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
2772
|
+
/* @__PURE__ */ jsxs(SelectContent, { children: [
|
|
2773
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "__all__", children: t("agenda.sidebar.allUnits") }),
|
|
2774
|
+
[...allLocations].map(([id, name]) => /* @__PURE__ */ jsx(SelectItem, { value: id, children: name }, id))
|
|
2775
|
+
] })
|
|
2776
|
+
]
|
|
2777
|
+
}
|
|
2778
|
+
)
|
|
2779
|
+
},
|
|
2780
|
+
{
|
|
2781
|
+
id: "service",
|
|
2782
|
+
title: t("agenda.sidebar.service"),
|
|
2783
|
+
count: serviceFilter.length,
|
|
2784
|
+
hidden: !config.capabilities.services || serviceOptions.length === 0,
|
|
2785
|
+
// Três serviços agendáveis viram três caixinhas; trezentos viram
|
|
2786
|
+
// um campo com busca. Quem chama não escolhe a forma.
|
|
2787
|
+
content: /* @__PURE__ */ jsx(
|
|
2788
|
+
AdaptiveFilter,
|
|
2789
|
+
{
|
|
2790
|
+
options: serviceOptions,
|
|
2791
|
+
value: serviceFilter,
|
|
2792
|
+
onChange: setServiceFilter,
|
|
2793
|
+
allLabel: t("agenda.sidebar.allServices")
|
|
2794
|
+
}
|
|
2795
|
+
)
|
|
2796
|
+
},
|
|
2797
|
+
{
|
|
2798
|
+
id: "status",
|
|
2799
|
+
title: t("agenda.sidebar.status"),
|
|
2800
|
+
count: selectedStatuses.length,
|
|
2801
|
+
// Fora do ar quando nenhum tipo de reserva tem fluxo de status
|
|
2802
|
+
// (eventMode:'simple').
|
|
2803
|
+
hidden: !config.capabilities.status,
|
|
2804
|
+
content: /* @__PURE__ */ jsx(
|
|
2805
|
+
StatusFilterList,
|
|
2806
|
+
{
|
|
2807
|
+
statuses: config.statuses,
|
|
2808
|
+
selected: selectedStatuses,
|
|
2809
|
+
onToggle: toggleStatus
|
|
2810
|
+
}
|
|
2811
|
+
)
|
|
2812
|
+
},
|
|
2813
|
+
{
|
|
2814
|
+
id: "week-grouping",
|
|
2815
|
+
title: "Agrupar semana por",
|
|
2816
|
+
// Só na SEMANA: no dia existe uma data só, e um seletor que não
|
|
2817
|
+
// muda nada é um seletor que ensina a desconfiar dos outros.
|
|
2818
|
+
hidden: !config.capabilities.professional || !currentView.endsWith("Week"),
|
|
2819
|
+
defaultOpen: true,
|
|
2820
|
+
content: /* @__PURE__ */ jsx(
|
|
2821
|
+
SegmentedControl,
|
|
2822
|
+
{
|
|
2823
|
+
size: "sm",
|
|
2824
|
+
value: weekGrouping,
|
|
2825
|
+
onChange: (v) => prefs.set("weekGrouping", v),
|
|
2826
|
+
"aria-label": "Agrupar a semana por dia ou por profissional",
|
|
2827
|
+
options: [
|
|
2828
|
+
{ value: "day", label: "Dia" },
|
|
2829
|
+
{ value: "professional", label: "Profissional" }
|
|
2830
|
+
]
|
|
2831
|
+
}
|
|
2832
|
+
)
|
|
2833
|
+
},
|
|
2834
|
+
{
|
|
2835
|
+
id: "week-days",
|
|
2836
|
+
title: "Dias da semana",
|
|
2837
|
+
// A choice away from the automatic one is something in force.
|
|
2838
|
+
count: Array.isArray(weekDaysPref) ? 1 : 0,
|
|
2839
|
+
hidden: !currentView.endsWith("Week") || currentView.startsWith("list"),
|
|
2840
|
+
defaultOpen: true,
|
|
2841
|
+
content: /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
2842
|
+
/* @__PURE__ */ jsx("div", { className: "flex gap-1", role: "group", "aria-label": "Dias que a semana mostra", children: [1, 2, 3, 4, 5, 6, 0].map((dow2) => {
|
|
2843
|
+
const on = shownWeekDays.includes(dow2);
|
|
2844
|
+
const name = new Date(2024, 0, 7 + dow2).toLocaleDateString(currentLocale === "pt-BR" ? "pt-BR" : "en", { weekday: "long" });
|
|
2845
|
+
return /* @__PURE__ */ jsx(
|
|
2846
|
+
Button,
|
|
2847
|
+
{
|
|
2848
|
+
variant: "bare",
|
|
2849
|
+
size: "bare",
|
|
2850
|
+
type: "button",
|
|
2851
|
+
"aria-pressed": on,
|
|
2852
|
+
title: name,
|
|
2853
|
+
"aria-label": name,
|
|
2854
|
+
"data-testid": `week-day-${dow2}`,
|
|
2855
|
+
onClick: () => toggleWeekDay(dow2),
|
|
2856
|
+
className: `font-normal whitespace-normal flex h-7 w-7 items-center justify-center rounded-full text-xs font-medium uppercase transition-colors ${on ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground hover:text-foreground"}`,
|
|
2857
|
+
children: name.charAt(0)
|
|
2858
|
+
},
|
|
2859
|
+
dow2
|
|
2860
|
+
);
|
|
2861
|
+
}) }),
|
|
2862
|
+
Array.isArray(weekDaysPref) && /* @__PURE__ */ jsx(
|
|
2863
|
+
Button,
|
|
2864
|
+
{
|
|
2865
|
+
variant: "bare",
|
|
2866
|
+
size: "bare",
|
|
2867
|
+
type: "button",
|
|
2868
|
+
onClick: () => prefs.set("weekDays", "auto"),
|
|
2869
|
+
className: "font-normal whitespace-normal text-xs text-muted-foreground underline-offset-2 hover:text-foreground hover:underline",
|
|
2870
|
+
children: "Voltar ao autom\xE1tico"
|
|
2871
|
+
}
|
|
2872
|
+
)
|
|
2873
|
+
] })
|
|
2874
|
+
},
|
|
2875
|
+
{
|
|
2876
|
+
id: "hide-empty",
|
|
2877
|
+
title: t("agenda.sidebar.hideEmptyGrids"),
|
|
2878
|
+
count: hideEmptyGrids ? 1 : 0,
|
|
2879
|
+
hidden: !config.capabilities.professional,
|
|
2880
|
+
content: /* @__PURE__ */ jsx(
|
|
2881
|
+
Switch,
|
|
2882
|
+
{
|
|
2883
|
+
size: "sm",
|
|
2884
|
+
checked: hideEmptyGrids,
|
|
2885
|
+
onCheckedChange: setHideEmptyGrids,
|
|
2886
|
+
label: /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("agenda.sidebar.hideEmptyGrids") })
|
|
2887
|
+
}
|
|
2888
|
+
)
|
|
2889
|
+
}
|
|
2890
|
+
]
|
|
2891
|
+
}
|
|
2892
|
+
) }),
|
|
2893
|
+
config.capabilities.professional && /* @__PURE__ */ jsx(ScheduleGridDialog, { open: schedulesOpen, professionalId: schedulesFor, onClose: () => {
|
|
2894
|
+
setSchedulesOpen(false);
|
|
2895
|
+
setSchedulesFor(null);
|
|
2896
|
+
} }),
|
|
2897
|
+
blockFor && /* @__PURE__ */ jsx(
|
|
2898
|
+
BlockDialog,
|
|
2899
|
+
{
|
|
2900
|
+
open: true,
|
|
2901
|
+
professionalId: blockFor.professionalId,
|
|
2902
|
+
date: blockFor.date,
|
|
2903
|
+
onClose: () => setBlockFor(null),
|
|
2904
|
+
onSaved: invalidateSchedules
|
|
2905
|
+
}
|
|
2906
|
+
),
|
|
2907
|
+
editingBlock && /* @__PURE__ */ jsx(
|
|
2908
|
+
BlockDialog,
|
|
2909
|
+
{
|
|
2910
|
+
open: true,
|
|
2911
|
+
editing: editingBlock,
|
|
2912
|
+
onClose: () => setEditingBlock(null),
|
|
2913
|
+
onSaved: invalidateSchedules
|
|
2914
|
+
}
|
|
2915
|
+
),
|
|
2916
|
+
/* @__PURE__ */ jsx(PageHeaderActions, { children: /* @__PURE__ */ jsx(
|
|
2917
|
+
PageActions,
|
|
2918
|
+
{
|
|
2919
|
+
actions: [
|
|
2920
|
+
{
|
|
2921
|
+
id: "new-booking",
|
|
2922
|
+
label: t("agenda.calendar.newAppointment"),
|
|
2923
|
+
icon: Plus,
|
|
2924
|
+
intent: "primary",
|
|
2925
|
+
creates: true,
|
|
2926
|
+
onSelect: () => openModal("create")
|
|
2927
|
+
},
|
|
2928
|
+
{
|
|
2929
|
+
id: "agenda-settings",
|
|
2930
|
+
label: t("agenda.settings.title"),
|
|
2931
|
+
icon: Settings,
|
|
2932
|
+
// Utility, not an alternative way to book: the shell gives it
|
|
2933
|
+
// its own quiet circle above the "+" instead of hiding it in a
|
|
2934
|
+
// create menu.
|
|
2935
|
+
intent: "utility",
|
|
2936
|
+
onSelect: () => {
|
|
2937
|
+
navigateApp("/settings/agenda");
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
]
|
|
2941
|
+
}
|
|
2942
|
+
) }),
|
|
2943
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col min-w-0 min-h-0 overflow-hidden", children: [
|
|
2944
|
+
/* @__PURE__ */ jsxs("div", { className: "shrink-0 border-b", children: [
|
|
2945
|
+
/* @__PURE__ */ jsxs("div", { className: `${mobileGridActive ? "flex" : "hidden"} md:flex h-12 items-center justify-between pl-4 pr-4`, children: [
|
|
2946
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [
|
|
2947
|
+
/* @__PURE__ */ jsxs("div", { className: `${mobileGridActive ? "flex" : "hidden"} md:flex items-center gap-3`, children: [
|
|
2948
|
+
/* @__PURE__ */ jsx(
|
|
2949
|
+
Button,
|
|
2950
|
+
{
|
|
2951
|
+
variant: "bare",
|
|
2952
|
+
size: "bare",
|
|
2953
|
+
onClick: handleToday,
|
|
2954
|
+
className: "rounded-md border px-3 py-1 text-sm font-medium hover:bg-muted bg-card shadow-button active:shadow-button-inset transition-colors",
|
|
2955
|
+
children: t("agenda.toolbar.today")
|
|
2956
|
+
}
|
|
2957
|
+
),
|
|
2958
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5", children: [
|
|
2959
|
+
/* @__PURE__ */ jsx(Button, { variant: "bare", size: "bare", onClick: handlePrev, className: "flex h-7 w-7 items-center justify-center rounded-full hover:bg-muted transition-colors", children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4" }) }),
|
|
2960
|
+
/* @__PURE__ */ jsx(Button, { variant: "bare", size: "bare", onClick: handleNext, className: "flex h-7 w-7 items-center justify-center rounded-full hover:bg-muted transition-colors", children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" }) })
|
|
2961
|
+
] })
|
|
2962
|
+
] }),
|
|
2963
|
+
!(isMobile && !mobileGridActive) && /* @__PURE__ */ jsx("h2", { className: "truncate text-base font-normal sm:text-lg", children: calendarTitle })
|
|
2964
|
+
] }),
|
|
2965
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-1 shrink-0", children: /* @__PURE__ */ jsx("div", { className: "hidden md:flex items-center gap-1", children: viewOptions.map((opt) => /* @__PURE__ */ jsx(
|
|
2966
|
+
Button,
|
|
2967
|
+
{
|
|
2968
|
+
variant: "bare",
|
|
2969
|
+
size: "bare",
|
|
2970
|
+
onClick: () => handleViewChange(opt.key),
|
|
2971
|
+
className: `font-normal rounded-md px-3 py-1 text-sm transition-colors ${currentView === opt.key ? "bg-primary/10 text-primary font-medium" : "text-muted-foreground hover:bg-muted/50"}`,
|
|
2972
|
+
children: t(opt.labelKey)
|
|
2973
|
+
},
|
|
2974
|
+
opt.key
|
|
2975
|
+
)) }) })
|
|
2976
|
+
] }),
|
|
2977
|
+
/* @__PURE__ */ jsx("div", { className: "flex md:hidden items-center gap-1 overflow-x-auto px-3 pb-2", children: mobileViewOptions.map((opt) => /* @__PURE__ */ jsx(
|
|
2978
|
+
Button,
|
|
2979
|
+
{
|
|
2980
|
+
variant: "bare",
|
|
2981
|
+
size: "bare",
|
|
2982
|
+
onClick: () => handleMobileViewChange(opt.key),
|
|
2983
|
+
"aria-pressed": mobileView === opt.key,
|
|
2984
|
+
className: `font-normal flex min-h-[2.75rem] shrink-0 items-center rounded-full px-4 text-sm transition-colors active:scale-[0.98] ${mobileView === opt.key ? "bg-primary/10 font-medium text-primary" : "text-muted-foreground hover:bg-muted/50"}`,
|
|
2985
|
+
children: t(opt.labelKey)
|
|
2986
|
+
},
|
|
2987
|
+
opt.key
|
|
2988
|
+
)) })
|
|
2989
|
+
] }),
|
|
2990
|
+
isMobile && !mobileGridActive ? (
|
|
2991
|
+
// The mobile branch used to sit in front of the loading check, so the
|
|
2992
|
+
// list rendered empty while the query ran — which on this screen reads
|
|
2993
|
+
// as "nothing scheduled", the one wrong answer it must not give.
|
|
2994
|
+
bookingsLoading && bookings.length === 0 ? /* @__PURE__ */ jsx(AgendaMobileListSkeleton, {}) : /* @__PURE__ */ jsx(
|
|
2995
|
+
AgendaMobileList,
|
|
2996
|
+
{
|
|
2997
|
+
bookings,
|
|
2998
|
+
statusColors,
|
|
2999
|
+
currencyCode: config.currency.code,
|
|
3000
|
+
onSelect: (b) => openModal("edit", { bookingId: b.id }),
|
|
3001
|
+
onCreate: () => openModal("create")
|
|
3002
|
+
}
|
|
3003
|
+
)
|
|
3004
|
+
) : professionalsLoading && professionals.length === 0 ? /* @__PURE__ */ jsx(CalendarSkeleton, { profCount: 2 }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3005
|
+
semGradeNoPeriodo && /* @__PURE__ */ jsxs("div", { className: "mx-3 mb-2 flex flex-wrap items-center gap-x-3 gap-y-1.5 rounded-lg border border-warning/40 bg-warning/10 px-3 py-2", children: [
|
|
3006
|
+
/* @__PURE__ */ jsx(Clock3, { className: "h-4 w-4 shrink-0 text-warning-foreground", "aria-hidden": true }),
|
|
3007
|
+
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1 text-xs text-foreground", children: [
|
|
3008
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: t("agenda.calendar.noGridTitle") }),
|
|
3009
|
+
" \u2014 ",
|
|
3010
|
+
t("agenda.calendar.noGridBody")
|
|
3011
|
+
] }),
|
|
3012
|
+
/* @__PURE__ */ jsx(
|
|
3013
|
+
Button,
|
|
3014
|
+
{
|
|
3015
|
+
variant: "bare",
|
|
3016
|
+
size: "bare",
|
|
3017
|
+
type: "button",
|
|
3018
|
+
onClick: () => setSchedulesOpen(true),
|
|
3019
|
+
className: "shrink-0 rounded-button border bg-card px-2.5 py-1 text-xs font-medium shadow-button transition-all hover:bg-muted active:scale-95",
|
|
3020
|
+
children: t("agenda.calendar.noGridCta")
|
|
3021
|
+
}
|
|
3022
|
+
)
|
|
3023
|
+
] }),
|
|
3024
|
+
/* @__PURE__ */ jsx("div", { ref: wrapperRef, className: `min-h-0 min-w-0 flex-1 overflow-x-auto overflow-y-hidden agenda-calendar${semGradeNoPeriodo ? " agenda-unscheduled" : ""}`, children: /* @__PURE__ */ jsx("div", { className: "h-full", style: gridMinWidth ? { minWidth: gridMinWidth } : void 0, children: /* @__PURE__ */ jsx(
|
|
3025
|
+
FullCalendar,
|
|
3026
|
+
{
|
|
3027
|
+
ref: calendarRef,
|
|
3028
|
+
plugins: [dayGridPlugin, timeGridPlugin, resourceTimeGridPlugin, interactionPlugin, listPlugin],
|
|
3029
|
+
initialView: mobileGridActive ? mobileView : effectiveInitialView,
|
|
3030
|
+
initialDate: urlTarget?.date,
|
|
3031
|
+
schedulerLicenseKey: "CC-Attribution-NonCommercial-NoDerivatives",
|
|
3032
|
+
views: weekViewOptions,
|
|
3033
|
+
resources: resourceCapable ? columnsSettled ? resources : [] : void 0,
|
|
3034
|
+
events,
|
|
3035
|
+
headerToolbar: false,
|
|
3036
|
+
locale: fcLocale,
|
|
3037
|
+
height: "100%",
|
|
3038
|
+
slotMinTime: effectiveBusinessHours.startTime + ":00",
|
|
3039
|
+
slotMaxTime: effectiveBusinessHours.endTime + ":00",
|
|
3040
|
+
slotDuration: { minutes: config.slotDuration },
|
|
3041
|
+
allDaySlot: false,
|
|
3042
|
+
nowIndicator: true,
|
|
3043
|
+
editable: config.modules.dragAndDrop,
|
|
3044
|
+
selectable: true,
|
|
3045
|
+
selectMirror: true,
|
|
3046
|
+
eventOverlap: false,
|
|
3047
|
+
datesSet: handleDatesSet,
|
|
3048
|
+
eventDidMount: handleEventDidMount,
|
|
3049
|
+
eventClick: handleEventClick,
|
|
3050
|
+
eventMouseEnter: handleEventMouseEnter,
|
|
3051
|
+
eventMouseLeave: handleEventMouseLeave,
|
|
3052
|
+
select: handleSelect,
|
|
3053
|
+
dateClick: handleDateClick,
|
|
3054
|
+
selectLongPressDelay: 400,
|
|
3055
|
+
eventDrop: handleEventDrop,
|
|
3056
|
+
resourceLabelContent,
|
|
3057
|
+
dayHeaderContent,
|
|
3058
|
+
dayCellContent,
|
|
3059
|
+
eventContent,
|
|
3060
|
+
resourceOrder: "title",
|
|
3061
|
+
datesAboveResources: weekGrouping === "day",
|
|
3062
|
+
stickyHeaderDates: true,
|
|
3063
|
+
expandRows: false,
|
|
3064
|
+
eventTimeFormat: { hour: "2-digit", minute: "2-digit", hour12: false },
|
|
3065
|
+
slotLabelFormat: { hour: "2-digit", minute: "2-digit", hour12: false }
|
|
3066
|
+
}
|
|
3067
|
+
) }) })
|
|
3068
|
+
] }),
|
|
3069
|
+
config.capabilities.status && !isMobile && /* @__PURE__ */ jsx("div", { className: "hidden md:flex shrink-0 flex-wrap items-center gap-x-4 gap-y-1.5 border-t px-4 py-2", children: config.statuses.map((status) => /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-[11px] text-muted-foreground", children: [
|
|
3070
|
+
/* @__PURE__ */ jsx("span", { className: "h-2 w-2 shrink-0 rounded-full", style: { backgroundColor: status.color } }),
|
|
3071
|
+
status.label
|
|
3072
|
+
] }, status.value)) })
|
|
3073
|
+
] }),
|
|
3074
|
+
selectedBooking && popoverAnchor && popoverVisible && /* @__PURE__ */ jsx(
|
|
3075
|
+
AppointmentPopover,
|
|
3076
|
+
{
|
|
3077
|
+
booking: selectedBooking,
|
|
3078
|
+
position: popoverAnchor,
|
|
3079
|
+
onClose: () => {
|
|
3080
|
+
setPopoverVisible(false);
|
|
3081
|
+
setSelectedBooking(null);
|
|
3082
|
+
setPopoverAnchor(null);
|
|
3083
|
+
},
|
|
3084
|
+
onEdit: (id) => {
|
|
3085
|
+
setPopoverVisible(false);
|
|
3086
|
+
setSelectedBooking(null);
|
|
3087
|
+
setPopoverAnchor(null);
|
|
3088
|
+
openModal("edit", { bookingId: id });
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
),
|
|
3092
|
+
contextMenu && /* @__PURE__ */ jsx(
|
|
3093
|
+
EventContextMenu,
|
|
3094
|
+
{
|
|
3095
|
+
booking: contextMenu.booking,
|
|
3096
|
+
position: { x: contextMenu.x, y: contextMenu.y },
|
|
3097
|
+
onClose: () => {
|
|
3098
|
+
setContextMenuVisible(false);
|
|
3099
|
+
setContextMenu(null);
|
|
3100
|
+
},
|
|
3101
|
+
onEdit: (id) => {
|
|
3102
|
+
setContextMenuVisible(false);
|
|
3103
|
+
setContextMenu(null);
|
|
3104
|
+
openModal("edit", { bookingId: id });
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
),
|
|
3108
|
+
/* @__PURE__ */ jsx(
|
|
3109
|
+
ConfirmDialog,
|
|
3110
|
+
{
|
|
3111
|
+
open: !!scheduleConfirm,
|
|
3112
|
+
variant: "warning",
|
|
3113
|
+
title: t("agenda.calendar.noScheduleConfirm"),
|
|
3114
|
+
confirmLabel: t("common.confirm"),
|
|
3115
|
+
cancelLabel: t("common.cancel"),
|
|
3116
|
+
onConfirm: () => {
|
|
3117
|
+
const cb = scheduleConfirm?.onConfirm;
|
|
3118
|
+
setScheduleConfirm(null);
|
|
3119
|
+
cb?.();
|
|
3120
|
+
},
|
|
3121
|
+
onCancel: () => setScheduleConfirm(null)
|
|
3122
|
+
}
|
|
3123
|
+
),
|
|
3124
|
+
/* @__PURE__ */ jsx(
|
|
3125
|
+
AppointmentModal,
|
|
3126
|
+
{
|
|
3127
|
+
open: modalState.open,
|
|
3128
|
+
mode: modalState.mode,
|
|
3129
|
+
bookingId: modalState.bookingId,
|
|
3130
|
+
prefill: modalState.prefill,
|
|
3131
|
+
initialTab: modalState.initialTab,
|
|
3132
|
+
externalSource: modalState.externalSource,
|
|
3133
|
+
onClose: closeModal,
|
|
3134
|
+
onCreated: (newId, source) => {
|
|
3135
|
+
if (config.onBookingCreated) {
|
|
3136
|
+
void Promise.resolve(config.onBookingCreated({ bookingId: newId, source })).catch((error) => {
|
|
3137
|
+
console.error("Failed to run agenda booking-created bridge", error);
|
|
3138
|
+
});
|
|
3139
|
+
}
|
|
3140
|
+
setTimeout(() => {
|
|
3141
|
+
const booking = bookings.find((b) => b.id === newId);
|
|
3142
|
+
if (!booking) return;
|
|
3143
|
+
const el = document.querySelector(`[data-booking-id="${newId}"]`);
|
|
3144
|
+
if (el) {
|
|
3145
|
+
const rect = el.getBoundingClientRect();
|
|
3146
|
+
const popW = 320, popH = 340;
|
|
3147
|
+
let x = rect.right + 8;
|
|
3148
|
+
let y = rect.top + rect.height / 2 - popH / 2;
|
|
3149
|
+
if (x + popW > window.innerWidth - 16) x = rect.left - popW - 8;
|
|
3150
|
+
if (y + popH > window.innerHeight - 16) y = window.innerHeight - popH - 16;
|
|
3151
|
+
if (y < 16) y = 16;
|
|
3152
|
+
setSelectedBooking(booking);
|
|
3153
|
+
setPopoverAnchor({ x, y });
|
|
3154
|
+
setPopoverVisible(true);
|
|
3155
|
+
}
|
|
3156
|
+
}, 400);
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
)
|
|
3160
|
+
] });
|
|
3161
|
+
}
|
|
3162
|
+
function AgendaPage({ config, provider, store, children }) {
|
|
3163
|
+
const runtime = usePluginRuntimeOptional();
|
|
3164
|
+
const tenantId = runtime?.context.tenant?.id;
|
|
3165
|
+
React.useEffect(() => {
|
|
3166
|
+
setAgendaTenantId(tenantId);
|
|
3167
|
+
return () => setAgendaTenantId(void 0);
|
|
3168
|
+
}, [tenantId]);
|
|
3169
|
+
return /* @__PURE__ */ jsx(AgendaContextProvider, { config, provider, store, children: /* @__PURE__ */ jsx("div", { className: "h-full min-h-0 space-y-3 overflow-y-auto p-2 md:space-y-4 md:px-3 md:pb-3 md:pt-2", children: children ?? /* @__PURE__ */ jsx(CalendarView, {}) }) });
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
export { AgendaPage };
|
|
3173
|
+
//# sourceMappingURL=AgendaPage-43OXQDZE.js.map
|
|
3174
|
+
//# sourceMappingURL=AgendaPage-43OXQDZE.js.map
|