@fayz-ai/admin 0.29.1 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ScheduleEditor-QC6OYBUE.js +7 -0
- package/dist/{ScheduleEditor-KU4UUG6Q.js.map → ScheduleEditor-QC6OYBUE.js.map} +1 -1
- package/dist/SubscriptionPage-TVAEOFNC.js +9 -0
- package/dist/{SubscriptionPage-2JPALG53.js.map → SubscriptionPage-TVAEOFNC.js.map} +1 -1
- package/dist/app/AdminShell.d.ts.map +1 -1
- package/dist/app/admin-app.d.ts.map +1 -1
- package/dist/app/bottom-nav.d.ts.map +1 -1
- package/dist/app/rail-preset.d.ts +40 -2
- package/dist/app/rail-preset.d.ts.map +1 -1
- package/dist/app/useAppPreset.d.ts +27 -0
- package/dist/app/useAppPreset.d.ts.map +1 -1
- package/dist/chunk-3SREEFHN.js +1685 -0
- package/dist/chunk-3SREEFHN.js.map +1 -0
- package/dist/{chunk-R4HQ2HS2.js → chunk-KSUIF56I.js} +7 -2
- package/dist/chunk-KSUIF56I.js.map +1 -0
- package/dist/{chunk-OVVTLRDD.js → chunk-U6LV4GIH.js} +3 -3
- package/dist/{chunk-OVVTLRDD.js.map → chunk-U6LV4GIH.js.map} +1 -1
- package/dist/{chunk-XJB76FHA.js → chunk-ZUM3Y7VB.js} +50 -17
- package/dist/chunk-ZUM3Y7VB.js.map +1 -0
- package/dist/components/notifications/NotificationInbox.d.ts.map +1 -1
- package/dist/components/notifications/UpdateReady.d.ts +8 -0
- package/dist/components/notifications/UpdateReady.d.ts.map +1 -0
- package/dist/components/settings/SettingsPage.d.ts.map +1 -1
- package/dist/components/shared/PersonLink.d.ts.map +1 -1
- package/dist/components/shared/person-actions.d.ts +48 -0
- package/dist/components/shared/person-actions.d.ts.map +1 -0
- package/dist/crud/CrudDetailPage.d.ts +6 -1
- package/dist/crud/CrudDetailPage.d.ts.map +1 -1
- package/dist/crud/CrudListView.d.ts.map +1 -1
- package/dist/crud/archetype-lookup.d.ts +24 -0
- package/dist/crud/archetype-lookup.d.ts.map +1 -1
- package/dist/crud/archetypes/AddressesTab.d.ts.map +1 -1
- package/dist/crud/archetypes/EntityLink.d.ts.map +1 -1
- package/dist/crud/archetypes/KanbanBoard.d.ts +11 -1
- package/dist/crud/archetypes/KanbanBoard.d.ts.map +1 -1
- package/dist/crud/archetypes/OrderDetailView.d.ts +3 -0
- package/dist/crud/archetypes/OrderDetailView.d.ts.map +1 -1
- package/dist/crud/archetypes/PersonDetailTabs.d.ts +22 -0
- package/dist/crud/archetypes/PersonDetailTabs.d.ts.map +1 -1
- package/dist/crud/archetypes/ScheduleDateExceptions.d.ts +6 -0
- package/dist/crud/archetypes/ScheduleDateExceptions.d.ts.map +1 -0
- package/dist/crud/archetypes/ScheduleEditor.d.ts +8 -1
- package/dist/crud/archetypes/ScheduleEditor.d.ts.map +1 -1
- package/dist/crud/archetypes/ScheduleGridPanel.d.ts +10 -0
- package/dist/crud/archetypes/ScheduleGridPanel.d.ts.map +1 -0
- package/dist/hooks/useNotifications.d.ts.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1510 -1141
- package/dist/index.js.map +1 -1
- package/dist/lib/dedup.d.ts.map +1 -1
- package/dist/lib/schedule-config.d.ts +15 -0
- package/dist/lib/schedule-config.d.ts.map +1 -1
- package/dist/lib/schedule-service.d.ts +92 -0
- package/dist/lib/schedule-service.d.ts.map +1 -1
- package/dist/org/OrgMark.d.ts.map +1 -1
- package/dist/org/WorkspaceUnitSheet.d.ts +15 -0
- package/dist/org/WorkspaceUnitSheet.d.ts.map +1 -0
- package/dist/org/context.d.ts.map +1 -1
- package/dist/org/index.js +2 -2
- package/dist/units/UnitMenuSection.d.ts.map +1 -1
- package/dist/units/UnitPickerDialog.d.ts +4 -0
- package/dist/units/UnitPickerDialog.d.ts.map +1 -0
- package/dist/units/index.d.ts +2 -0
- package/dist/units/index.d.ts.map +1 -1
- package/dist/units/picker.store.d.ts +10 -0
- package/dist/units/picker.store.d.ts.map +1 -0
- package/package.json +7 -7
- package/dist/ScheduleEditor-KU4UUG6Q.js +0 -7
- package/dist/SubscriptionPage-2JPALG53.js +0 -9
- package/dist/chunk-NXFFJY2X.js +0 -1077
- package/dist/chunk-NXFFJY2X.js.map +0 -1
- package/dist/chunk-R4HQ2HS2.js.map +0 -1
- package/dist/chunk-XJB76FHA.js.map +0 -1
|
@@ -0,0 +1,1685 @@
|
|
|
1
|
+
import { getFayzSupabaseClient } from './chunk-ZUM3Y7VB.js';
|
|
2
|
+
import { useOrganizationStore } from './chunk-W5IBKM3K.js';
|
|
3
|
+
import React4, { useState, useCallback, useEffect, useSyncExternalStore, useRef } from 'react';
|
|
4
|
+
import { toast, Skeleton, Button, TooltipProvider, Badge, DatePicker, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Input, Modal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalBody, ModalFooter, Tabs, TabsList, TabsTrigger, TimePicker, Tooltip, TooltipTrigger, TooltipContent, Card, CardContent, TabsContent, Switch } from '@fayz-ai/ui';
|
|
5
|
+
import { getSupabaseClientOptional, stableKey, globalCache, useTranslation, todayLocal, localDateKey } from '@fayz-ai/core';
|
|
6
|
+
import { Plus, Copy, Users, CalendarOff, Trash2, Check, CalendarPlus, Clock, Save, Settings, CalendarCheck, X, ChevronDown } from 'lucide-react';
|
|
7
|
+
import { createPortal } from 'react-dom';
|
|
8
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
9
|
+
|
|
10
|
+
function parseBlockSettings(metadata) {
|
|
11
|
+
return {
|
|
12
|
+
bufferMinutes: typeof metadata.bufferMinutes === "number" ? metadata.bufferMinutes : void 0,
|
|
13
|
+
allowedServiceIds: Array.isArray(metadata.allowedServiceIds) ? metadata.allowedServiceIds : void 0,
|
|
14
|
+
maxConcurrent: typeof metadata.maxConcurrent === "number" ? metadata.maxConcurrent : void 0,
|
|
15
|
+
minAdvanceHours: typeof metadata.minAdvanceHours === "number" ? metadata.minAdvanceHours : void 0,
|
|
16
|
+
maxAdvanceDays: typeof metadata.maxAdvanceDays === "number" ? metadata.maxAdvanceDays : void 0,
|
|
17
|
+
label: typeof metadata.label === "string" ? metadata.label : void 0,
|
|
18
|
+
locationId: typeof metadata.locationId === "string" ? metadata.locationId : void 0
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function mergeBlockSettings(metadata, settings) {
|
|
22
|
+
const merged = { ...metadata };
|
|
23
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
24
|
+
if (value !== void 0 && value !== null && !(Array.isArray(value) && value.length === 0)) {
|
|
25
|
+
merged[key] = value;
|
|
26
|
+
} else {
|
|
27
|
+
delete merged[key];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return merged;
|
|
31
|
+
}
|
|
32
|
+
function getTenantId() {
|
|
33
|
+
const id = useOrganizationStore.getState().currentOrg?.id;
|
|
34
|
+
if (!id) throw new Error("No active tenant");
|
|
35
|
+
return id;
|
|
36
|
+
}
|
|
37
|
+
function mapRow(row) {
|
|
38
|
+
const r = {};
|
|
39
|
+
for (const [key, value] of Object.entries(row)) {
|
|
40
|
+
r[key.replace(/_([a-z])/g, (_, c) => c.toUpperCase())] = value;
|
|
41
|
+
}
|
|
42
|
+
return r;
|
|
43
|
+
}
|
|
44
|
+
async function getSchedules(assigneeId, kind = "working_hours") {
|
|
45
|
+
const core = getFayzSupabaseClient();
|
|
46
|
+
const { data, error } = await core.from("schedules").select("*").eq("kind", kind).eq("assignee_id", assigneeId).order("day_of_week").order("starts_at");
|
|
47
|
+
if (error) throw error;
|
|
48
|
+
return (data ?? []).map(mapRow);
|
|
49
|
+
}
|
|
50
|
+
async function saveSchedule(input) {
|
|
51
|
+
const core = getFayzSupabaseClient();
|
|
52
|
+
const tenantId = getTenantId();
|
|
53
|
+
const { data, error } = await core.from("schedules").insert({
|
|
54
|
+
tenant_id: tenantId,
|
|
55
|
+
kind: input.kind ?? "working_hours",
|
|
56
|
+
assignee_id: input.assigneeId,
|
|
57
|
+
location_id: input.locationId ?? null,
|
|
58
|
+
day_of_week: input.dayOfWeek ?? null,
|
|
59
|
+
specific_date: input.specificDate ?? null,
|
|
60
|
+
starts_at: input.startsAt,
|
|
61
|
+
ends_at: input.endsAt,
|
|
62
|
+
is_active: input.isActive ?? true
|
|
63
|
+
}).select("*").single();
|
|
64
|
+
if (error) throw error;
|
|
65
|
+
return mapRow(data);
|
|
66
|
+
}
|
|
67
|
+
async function deleteSchedule(id) {
|
|
68
|
+
const core = getFayzSupabaseClient();
|
|
69
|
+
const { error } = await core.from("schedules").delete().eq("id", id);
|
|
70
|
+
if (error) throw error;
|
|
71
|
+
}
|
|
72
|
+
async function replaceWeeklySchedules(assigneeId, week, kind = "working_hours") {
|
|
73
|
+
const core = getFayzSupabaseClient();
|
|
74
|
+
const tenantId = getTenantId();
|
|
75
|
+
const { error: delErr } = await core.from("schedules").delete().eq("assignee_id", assigneeId).eq("kind", kind).not("day_of_week", "is", null).is("specific_date", null);
|
|
76
|
+
if (delErr) throw delErr;
|
|
77
|
+
const rows = [];
|
|
78
|
+
for (const [dayStr, day] of Object.entries(week)) {
|
|
79
|
+
if (!day.enabled) continue;
|
|
80
|
+
for (const period of day.periods) {
|
|
81
|
+
const metadata = period.settings ? mergeBlockSettings({}, period.settings) : {};
|
|
82
|
+
rows.push({
|
|
83
|
+
tenant_id: tenantId,
|
|
84
|
+
kind,
|
|
85
|
+
assignee_id: assigneeId,
|
|
86
|
+
day_of_week: Number(dayStr),
|
|
87
|
+
specific_date: null,
|
|
88
|
+
starts_at: period.startsAt,
|
|
89
|
+
ends_at: period.endsAt,
|
|
90
|
+
is_active: true,
|
|
91
|
+
metadata
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (rows.length > 0) {
|
|
96
|
+
const { error: insErr } = await core.from("schedules").insert(rows);
|
|
97
|
+
if (insErr) throw insErr;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function eachDate(startDate, endDate) {
|
|
101
|
+
const dates = [];
|
|
102
|
+
const current = /* @__PURE__ */ new Date(startDate + "T00:00:00Z");
|
|
103
|
+
const end = /* @__PURE__ */ new Date(endDate + "T00:00:00Z");
|
|
104
|
+
while (current <= end) {
|
|
105
|
+
dates.push(localDateKey(current));
|
|
106
|
+
current.setUTCDate(current.getUTCDate() + 1);
|
|
107
|
+
}
|
|
108
|
+
return dates;
|
|
109
|
+
}
|
|
110
|
+
async function saveException(input) {
|
|
111
|
+
const core = getFayzSupabaseClient();
|
|
112
|
+
const tenantId = getTenantId();
|
|
113
|
+
const kind = input.kind ?? "working_hours";
|
|
114
|
+
const dates = eachDate(input.startDate, input.endDate);
|
|
115
|
+
const isRange = dates.length > 1;
|
|
116
|
+
const meta = mergeBlockSettings(
|
|
117
|
+
{
|
|
118
|
+
...input.notes ? { notes: input.notes } : {},
|
|
119
|
+
...isRange ? { rangeStart: input.startDate, rangeEnd: input.endDate } : {},
|
|
120
|
+
exceptionType: input.type
|
|
121
|
+
},
|
|
122
|
+
input.settings ?? {}
|
|
123
|
+
);
|
|
124
|
+
const { error: delErr } = await core.from("schedules").delete().eq("assignee_id", input.assigneeId).eq("kind", kind).in("specific_date", dates);
|
|
125
|
+
if (delErr) throw delErr;
|
|
126
|
+
const rows = [];
|
|
127
|
+
if (input.type === "unavailable") {
|
|
128
|
+
for (const date of dates) {
|
|
129
|
+
rows.push({
|
|
130
|
+
tenant_id: tenantId,
|
|
131
|
+
kind,
|
|
132
|
+
assignee_id: input.assigneeId,
|
|
133
|
+
day_of_week: null,
|
|
134
|
+
specific_date: date,
|
|
135
|
+
starts_at: "00:00",
|
|
136
|
+
ends_at: "00:00",
|
|
137
|
+
is_active: false,
|
|
138
|
+
metadata: meta
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
for (const date of dates) {
|
|
143
|
+
for (const p of input.periods) {
|
|
144
|
+
rows.push({
|
|
145
|
+
tenant_id: tenantId,
|
|
146
|
+
kind,
|
|
147
|
+
assignee_id: input.assigneeId,
|
|
148
|
+
day_of_week: null,
|
|
149
|
+
specific_date: date,
|
|
150
|
+
starts_at: p.startsAt,
|
|
151
|
+
ends_at: p.endsAt,
|
|
152
|
+
is_active: true,
|
|
153
|
+
metadata: meta
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (rows.length > 0) {
|
|
159
|
+
const { error } = await core.from("schedules").insert(rows);
|
|
160
|
+
if (error) throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function deleteException(assigneeId, startDate, endDate, kind = "working_hours") {
|
|
164
|
+
const core = getFayzSupabaseClient();
|
|
165
|
+
const dates = eachDate(startDate, endDate ?? startDate);
|
|
166
|
+
const { error } = await core.from("schedules").delete().eq("assignee_id", assigneeId).eq("kind", kind).in("specific_date", dates);
|
|
167
|
+
if (error) throw error;
|
|
168
|
+
}
|
|
169
|
+
var emptyDays = () => ({ 0: [], 1: [], 2: [], 3: [], 4: [], 5: [], 6: [] });
|
|
170
|
+
var gridNameOf = (metadata) => {
|
|
171
|
+
const agenda = metadata?.agenda;
|
|
172
|
+
return typeof agenda?.grid_name === "string" ? agenda.grid_name : null;
|
|
173
|
+
};
|
|
174
|
+
var gridKeyOf = (g) => [g.kind, g.validFrom ?? "", g.validTo ?? "", g.name ?? ""].join("|");
|
|
175
|
+
function isGridCurrent(g, today2 = todayLocal()) {
|
|
176
|
+
if (g.validFrom && g.validFrom > today2) return false;
|
|
177
|
+
if (g.validTo && g.validTo < today2) return false;
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
async function listGrids(assigneeId, kind = "working_hours") {
|
|
181
|
+
const rows = (await getSchedules(assigneeId, kind)).filter(
|
|
182
|
+
(r) => r.dayOfWeek != null && r.specificDate == null && r.isActive
|
|
183
|
+
);
|
|
184
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
185
|
+
for (const r of rows) {
|
|
186
|
+
const name = gridNameOf(r.metadata ?? {});
|
|
187
|
+
const kindOf = r.gridKind === "exception" ? "exception" : "standard";
|
|
188
|
+
const key = gridKeyOf({ kind: kindOf, validFrom: r.validFrom, validTo: r.validTo, name });
|
|
189
|
+
let grid = byKey.get(key);
|
|
190
|
+
if (!grid) {
|
|
191
|
+
grid = {
|
|
192
|
+
key,
|
|
193
|
+
kind: kindOf,
|
|
194
|
+
name,
|
|
195
|
+
validFrom: r.validFrom,
|
|
196
|
+
validTo: r.validTo,
|
|
197
|
+
slotIntervalMinutes: r.slotIntervalMinutes,
|
|
198
|
+
weekInterval: r.weekInterval ?? 0,
|
|
199
|
+
unitId: r.unitId ?? r.locationId,
|
|
200
|
+
days: emptyDays(),
|
|
201
|
+
rowIds: []
|
|
202
|
+
};
|
|
203
|
+
byKey.set(key, grid);
|
|
204
|
+
}
|
|
205
|
+
const settings = parseBlockSettings(r.metadata ?? {});
|
|
206
|
+
const hasSettings = Object.values(settings).some((v) => v !== void 0);
|
|
207
|
+
grid.days[r.dayOfWeek].push({
|
|
208
|
+
id: r.id,
|
|
209
|
+
startsAt: r.startsAt,
|
|
210
|
+
endsAt: r.endsAt,
|
|
211
|
+
resourceId: r.resourceId,
|
|
212
|
+
settings: hasSettings ? settings : void 0
|
|
213
|
+
});
|
|
214
|
+
grid.rowIds.push(r.id);
|
|
215
|
+
}
|
|
216
|
+
const grids = [...byKey.values()];
|
|
217
|
+
for (const g of grids) for (const d of Object.values(g.days)) d.sort((a, b) => a.startsAt.localeCompare(b.startsAt));
|
|
218
|
+
return grids.sort((a, b) => {
|
|
219
|
+
const cur = Number(isGridCurrent(b)) - Number(isGridCurrent(a));
|
|
220
|
+
if (cur !== 0) return cur;
|
|
221
|
+
return (b.validFrom ?? "").localeCompare(a.validFrom ?? "");
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
async function saveGrid(assigneeId, grid, kind = "working_hours") {
|
|
225
|
+
const core = getFayzSupabaseClient();
|
|
226
|
+
const tenantId = getTenantId();
|
|
227
|
+
if (grid.rowIds.length > 0) {
|
|
228
|
+
const { error } = await core.from("schedules").delete().in("id", grid.rowIds);
|
|
229
|
+
if (error) throw error;
|
|
230
|
+
}
|
|
231
|
+
const rows = [];
|
|
232
|
+
for (const [dayStr, periods] of Object.entries(grid.days)) {
|
|
233
|
+
for (const p of periods) {
|
|
234
|
+
const metadata = mergeBlockSettings({}, p.settings ?? {});
|
|
235
|
+
if (grid.name) metadata.agenda = { grid_name: grid.name };
|
|
236
|
+
rows.push({
|
|
237
|
+
tenant_id: tenantId,
|
|
238
|
+
kind,
|
|
239
|
+
assignee_id: assigneeId,
|
|
240
|
+
day_of_week: Number(dayStr),
|
|
241
|
+
specific_date: null,
|
|
242
|
+
starts_at: p.startsAt,
|
|
243
|
+
ends_at: p.endsAt,
|
|
244
|
+
is_active: true,
|
|
245
|
+
metadata,
|
|
246
|
+
valid_from: grid.validFrom,
|
|
247
|
+
valid_to: grid.validTo,
|
|
248
|
+
grid_kind: grid.kind,
|
|
249
|
+
week_interval: grid.weekInterval ?? 0,
|
|
250
|
+
slot_interval_minutes: grid.slotIntervalMinutes,
|
|
251
|
+
resource_id: p.resourceId,
|
|
252
|
+
unit_id: grid.unitId
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (rows.length > 0) {
|
|
257
|
+
const { error } = await core.from("schedules").insert(rows);
|
|
258
|
+
if (error) throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function assigneesWithCurrentGrid(assigneeIds, kind = "working_hours") {
|
|
262
|
+
if (assigneeIds.length === 0) return /* @__PURE__ */ new Set();
|
|
263
|
+
const core = getFayzSupabaseClient();
|
|
264
|
+
const hoje = todayLocal();
|
|
265
|
+
const { data, error } = await core.from("schedules").select("assignee_id").eq("kind", kind).eq("is_active", true).is("specific_date", null).in("assignee_id", assigneeIds).or(`valid_to.is.null,valid_to.gte.${hoje}`);
|
|
266
|
+
if (error) throw error;
|
|
267
|
+
return new Set((data ?? []).map((r) => r.assignee_id).filter(Boolean));
|
|
268
|
+
}
|
|
269
|
+
async function copyGridTo(assigneeIds, grid, kind = "working_hours") {
|
|
270
|
+
for (const id of assigneeIds) {
|
|
271
|
+
await saveGrid(id, { ...grid, rowIds: [] }, kind);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async function endGridValidity(grid, endDate) {
|
|
275
|
+
if (grid.rowIds.length === 0) return;
|
|
276
|
+
const core = getFayzSupabaseClient();
|
|
277
|
+
const { error } = await core.from("schedules").update({ valid_to: endDate }).in("id", grid.rowIds);
|
|
278
|
+
if (error) throw error;
|
|
279
|
+
}
|
|
280
|
+
async function deleteGrid(grid) {
|
|
281
|
+
if (grid.rowIds.length === 0) return;
|
|
282
|
+
const core = getFayzSupabaseClient();
|
|
283
|
+
const { error } = await core.from("schedules").delete().in("id", grid.rowIds);
|
|
284
|
+
if (error) throw error;
|
|
285
|
+
}
|
|
286
|
+
var PERSON_LOOKUPS = {};
|
|
287
|
+
function registerPersonLookup(kind, lookup) {
|
|
288
|
+
PERSON_LOOKUPS[kind] = lookup;
|
|
289
|
+
}
|
|
290
|
+
function getPersonLookup(kind) {
|
|
291
|
+
return PERSON_LOOKUPS[kind] ?? null;
|
|
292
|
+
}
|
|
293
|
+
var ARCHETYPE_DISPLAY = {
|
|
294
|
+
person: { table: "people", labelField: "name", subtitleFields: ["phone"], groupField: "kind" },
|
|
295
|
+
product: { table: "products", labelField: "name", subtitleFields: ["sku", "description"], priceField: "price" },
|
|
296
|
+
service: { table: "products", labelField: "name", subtitleFields: ["duration_minutes", "description"], priceField: "price" },
|
|
297
|
+
location: { table: "v_units", labelField: "name", subtitleFields: ["city", "address"], groupField: "kind" }
|
|
298
|
+
};
|
|
299
|
+
var ARCHETYPE_ENTITY_DEFS = {
|
|
300
|
+
person: {
|
|
301
|
+
name: "Contato",
|
|
302
|
+
namePlural: "Contatos",
|
|
303
|
+
icon: "User",
|
|
304
|
+
fields: [
|
|
305
|
+
{ key: "name", label: "Nome", type: "text", required: true },
|
|
306
|
+
{ key: "phone", label: "Telefone", type: "phone" },
|
|
307
|
+
{ key: "email", label: "E-mail", type: "email" },
|
|
308
|
+
{ key: "document_number", label: "CPF / CNPJ", type: "text" },
|
|
309
|
+
{ key: "notes", label: "Observa\xE7\xF5es", type: "textarea" }
|
|
310
|
+
]
|
|
311
|
+
},
|
|
312
|
+
product: {
|
|
313
|
+
name: "Produto",
|
|
314
|
+
namePlural: "Produtos",
|
|
315
|
+
icon: "Package",
|
|
316
|
+
fields: [
|
|
317
|
+
{ key: "name", label: "Nome", type: "text", required: true },
|
|
318
|
+
{ key: "price", label: "Pre\xE7o", type: "currency" },
|
|
319
|
+
{ key: "sku", label: "SKU", type: "text" },
|
|
320
|
+
{ key: "unit", label: "Unidade", type: "text" },
|
|
321
|
+
{ key: "description", label: "Descri\xE7\xE3o", type: "textarea" }
|
|
322
|
+
]
|
|
323
|
+
},
|
|
324
|
+
service: {
|
|
325
|
+
name: "Servi\xE7o",
|
|
326
|
+
namePlural: "Servi\xE7os",
|
|
327
|
+
icon: "Sparkles",
|
|
328
|
+
fields: [
|
|
329
|
+
{ key: "name", label: "Nome", type: "text", required: true },
|
|
330
|
+
{ key: "price", label: "Pre\xE7o", type: "currency" },
|
|
331
|
+
{ key: "duration_minutes", label: "Dura\xE7\xE3o (min)", type: "number" },
|
|
332
|
+
{ key: "description", label: "Descri\xE7\xE3o", type: "textarea" }
|
|
333
|
+
]
|
|
334
|
+
},
|
|
335
|
+
location: {
|
|
336
|
+
name: "Local",
|
|
337
|
+
namePlural: "Locais",
|
|
338
|
+
icon: "MapPin",
|
|
339
|
+
fields: [
|
|
340
|
+
{ key: "name", label: "Nome", type: "text", required: true },
|
|
341
|
+
{ key: "phone", label: "Telefone", type: "phone" },
|
|
342
|
+
{ key: "address", label: "Endere\xE7o", type: "text" },
|
|
343
|
+
{ key: "city", label: "Cidade", type: "text" }
|
|
344
|
+
]
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
function searchTargets(archetype, labelField, query, declarados) {
|
|
348
|
+
const aspas = (v) => `"${v.replace(/["\\]/g, "")}"`;
|
|
349
|
+
if (declarados?.length) return declarados.map((c) => `${c}.ilike.${aspas(`*${query}*`)}`);
|
|
350
|
+
const alvos = [`${labelField}.ilike.${aspas(`*${query}*`)}`];
|
|
351
|
+
if (archetype !== "person") return alvos;
|
|
352
|
+
const digitos = query.replace(/\D+/g, "");
|
|
353
|
+
if (digitos.length >= 3) {
|
|
354
|
+
alvos.push(`phone_digits.ilike.${aspas(`*${digitos}*`)}`);
|
|
355
|
+
alvos.push(`document_number.ilike.${aspas(`*${digitos}*`)}`);
|
|
356
|
+
}
|
|
357
|
+
return alvos;
|
|
358
|
+
}
|
|
359
|
+
function toResult(row, display, kindLabels, kinds) {
|
|
360
|
+
const label = String(row[display.labelField] ?? row.name ?? "");
|
|
361
|
+
const subtitleParts = display.subtitleFields.map((f) => row[f]).filter(Boolean).map((v) => {
|
|
362
|
+
if (typeof v === "number") return `${v}min`;
|
|
363
|
+
return String(v);
|
|
364
|
+
});
|
|
365
|
+
let group;
|
|
366
|
+
if (display.groupField && row[display.groupField]) {
|
|
367
|
+
const rawGroup = String(row[display.groupField]);
|
|
368
|
+
const labelled = kindLabels?.[rawGroup];
|
|
369
|
+
group = labelled ?? (kinds && kinds.length > 1 ? rawGroup : void 0);
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
id: String(row.id),
|
|
373
|
+
label,
|
|
374
|
+
subtitle: subtitleParts.length > 0 ? subtitleParts.join(" \xB7 ") : void 0,
|
|
375
|
+
group,
|
|
376
|
+
price: display.priceField ? Number(row[display.priceField]) || void 0 : void 0,
|
|
377
|
+
data: row
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function createArchetypeLookup(config) {
|
|
381
|
+
const baseDisplay = ARCHETYPE_DISPLAY[config.archetype];
|
|
382
|
+
const display = {
|
|
383
|
+
...baseDisplay,
|
|
384
|
+
...config.subtitleFields ? { subtitleFields: config.subtitleFields } : {},
|
|
385
|
+
...config.table ? { table: config.table } : {}
|
|
386
|
+
};
|
|
387
|
+
const limit = config.limit ?? 20;
|
|
388
|
+
const kinds = config.kind ? Array.isArray(config.kind) ? config.kind : [config.kind] : config.archetype === "service" ? ["service"] : void 0;
|
|
389
|
+
function applyKindFilter(qb) {
|
|
390
|
+
if (kinds && kinds.length === 1) return qb.eq("kind", kinds[0]);
|
|
391
|
+
if (kinds && kinds.length > 1) return qb.in("kind", kinds);
|
|
392
|
+
return qb;
|
|
393
|
+
}
|
|
394
|
+
const defaultKind = kinds?.[0];
|
|
395
|
+
const createKinds = config.createKinds ?? kinds;
|
|
396
|
+
const baseDef = config.entityDef === null ? void 0 : config.entityDef ?? ARCHETYPE_ENTITY_DEFS[config.archetype];
|
|
397
|
+
const namedDef = baseDef && !config.entityDef && defaultKind && config.kindLabels?.[defaultKind] ? { ...baseDef, name: config.kindLabels[defaultKind] } : baseDef;
|
|
398
|
+
const entityDef = namedDef && createKinds && createKinds.length > 1 ? {
|
|
399
|
+
...namedDef,
|
|
400
|
+
fields: [
|
|
401
|
+
namedDef.fields[0],
|
|
402
|
+
{
|
|
403
|
+
key: "kind",
|
|
404
|
+
label: "Tipo",
|
|
405
|
+
type: "select",
|
|
406
|
+
required: true,
|
|
407
|
+
options: createKinds.map((k) => ({ value: k, label: config.kindLabels?.[k] ?? k })),
|
|
408
|
+
defaultValue: defaultKind ?? createKinds[0]
|
|
409
|
+
},
|
|
410
|
+
...namedDef.fields.slice(1)
|
|
411
|
+
]
|
|
412
|
+
} : namedDef;
|
|
413
|
+
return {
|
|
414
|
+
entityDef,
|
|
415
|
+
async search(query) {
|
|
416
|
+
const supabase = getSupabaseClientOptional();
|
|
417
|
+
if (!supabase) return [];
|
|
418
|
+
let qb = supabase.from(display.table).select("*").order(display.labelField, { ascending: true }).limit(limit);
|
|
419
|
+
qb = config.archetype === "service" ? qb.not("is_active", "is", false) : qb.eq("is_active", true);
|
|
420
|
+
if (config.bookableOnly) qb = qb.not("is_bookable", "is", false);
|
|
421
|
+
if (query) {
|
|
422
|
+
const alvos = searchTargets(config.archetype, display.labelField, query, config.searchFields);
|
|
423
|
+
qb = alvos.length > 1 ? qb.or(alvos.join(",")) : qb.ilike(config.searchFields?.[0] ?? display.labelField, `%${query}%`);
|
|
424
|
+
}
|
|
425
|
+
qb = applyKindFilter(qb);
|
|
426
|
+
const { data } = await qb;
|
|
427
|
+
return (data ?? []).map(
|
|
428
|
+
(row) => toResult(row, display, config.kindLabels, kinds)
|
|
429
|
+
);
|
|
430
|
+
},
|
|
431
|
+
/**
|
|
432
|
+
* O catálogo inteiro — e ele quase nunca muda entre duas perguntas.
|
|
433
|
+
*
|
|
434
|
+
* Toda abertura do formulário de agendamento relia a lista de serviços do
|
|
435
|
+
* banco, e fechar e reabrir relia de novo. Numa tela que a recepção abre
|
|
436
|
+
* dezenas de vezes por turno isso é uma consulta por clique para devolver
|
|
437
|
+
* a MESMA resposta.
|
|
438
|
+
*
|
|
439
|
+
* Um minuto de TTL: tempo de um turno de balcão inteiro sem repetir a
|
|
440
|
+
* pergunta, e curto o bastante para um serviço cadastrado agora aparecer
|
|
441
|
+
* sozinho no próximo agendamento. A chave leva conta e unidade — duas
|
|
442
|
+
* casas não compartilham catálogo, e sob RLS a mesma URL devolve linhas
|
|
443
|
+
* diferentes.
|
|
444
|
+
*/
|
|
445
|
+
async list() {
|
|
446
|
+
const supabase = getSupabaseClientOptional();
|
|
447
|
+
if (!supabase) return [];
|
|
448
|
+
const cacheKey = stableKey(
|
|
449
|
+
"lookup:list",
|
|
450
|
+
display.table,
|
|
451
|
+
kinds ?? [],
|
|
452
|
+
limit,
|
|
453
|
+
config.bookableOnly ? "bookable" : "",
|
|
454
|
+
useOrganizationStore.getState().currentOrg?.id ?? ""
|
|
455
|
+
);
|
|
456
|
+
const hit = globalCache.get(cacheKey);
|
|
457
|
+
if (hit) return hit;
|
|
458
|
+
let qb = supabase.from(display.table).select("*").order(display.labelField, { ascending: true }).limit(limit);
|
|
459
|
+
qb = config.archetype === "service" ? qb.or("is_active.eq.true,is_active.is.null") : qb.eq("is_active", true);
|
|
460
|
+
if (config.bookableOnly) qb = qb.or("is_bookable.eq.true,is_bookable.is.null");
|
|
461
|
+
qb = applyKindFilter(qb);
|
|
462
|
+
const { data } = await qb;
|
|
463
|
+
const rows = (data ?? []).map(
|
|
464
|
+
(row) => toResult(row, display, config.kindLabels, kinds)
|
|
465
|
+
);
|
|
466
|
+
globalCache.set(cacheKey, rows);
|
|
467
|
+
return rows;
|
|
468
|
+
},
|
|
469
|
+
// Create on the spot: what the form gathered, under the search's `kind`.
|
|
470
|
+
async create(draft) {
|
|
471
|
+
const supabase = getSupabaseClientOptional();
|
|
472
|
+
if (!supabase) throw new Error("No database connection");
|
|
473
|
+
const tenantId = useOrganizationStore.getState().currentOrg?.id;
|
|
474
|
+
if (!tenantId) throw new Error("Nenhuma organiza\xE7\xE3o ativa \u2014 tente de novo em um instante.");
|
|
475
|
+
const { name, ...rest } = draft;
|
|
476
|
+
const filled = Object.fromEntries(
|
|
477
|
+
Object.entries(rest).filter(([, v]) => v !== "" && v !== null && v !== void 0)
|
|
478
|
+
);
|
|
479
|
+
const row = {
|
|
480
|
+
...filled,
|
|
481
|
+
[display.labelField]: name,
|
|
482
|
+
tenant_id: tenantId,
|
|
483
|
+
is_active: true
|
|
484
|
+
};
|
|
485
|
+
if (!row.kind && defaultKind) row.kind = defaultKind;
|
|
486
|
+
const { data, error } = await supabase.from(display.table).insert(row).select("*").single();
|
|
487
|
+
if (error) throw new Error(error.message);
|
|
488
|
+
return toResult(data, display, config.kindLabels, kinds);
|
|
489
|
+
},
|
|
490
|
+
async getById(id) {
|
|
491
|
+
const supabase = getSupabaseClientOptional();
|
|
492
|
+
if (!supabase) return null;
|
|
493
|
+
const { data } = await supabase.from(display.table).select("*").eq("id", id).single();
|
|
494
|
+
return data ? toResult(data, display, config.kindLabels, kinds) : null;
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/lib/schedule-config.ts
|
|
500
|
+
var globalConfig = null;
|
|
501
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
502
|
+
function setScheduleBlockConfig(config) {
|
|
503
|
+
globalConfig = config;
|
|
504
|
+
listeners.forEach((fn) => fn());
|
|
505
|
+
}
|
|
506
|
+
function getScheduleBlockConfig() {
|
|
507
|
+
return globalConfig;
|
|
508
|
+
}
|
|
509
|
+
function subscribeScheduleBlockConfig(listener) {
|
|
510
|
+
listeners.add(listener);
|
|
511
|
+
return () => {
|
|
512
|
+
listeners.delete(listener);
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
function BlockSettingsPopover({
|
|
516
|
+
settings,
|
|
517
|
+
onChange,
|
|
518
|
+
config,
|
|
519
|
+
triggerRef,
|
|
520
|
+
open,
|
|
521
|
+
onClose
|
|
522
|
+
}) {
|
|
523
|
+
const t = useTranslation();
|
|
524
|
+
const popoverRef = useRef(null);
|
|
525
|
+
const [pos, setPos] = useState({ top: 0, left: 0 });
|
|
526
|
+
useEffect(() => {
|
|
527
|
+
if (!open || !triggerRef.current) return;
|
|
528
|
+
const rect = triggerRef.current.getBoundingClientRect();
|
|
529
|
+
const popW = 280;
|
|
530
|
+
let left = rect.left + rect.width / 2 - popW / 2;
|
|
531
|
+
if (left + popW > window.innerWidth - 12) left = window.innerWidth - popW - 12;
|
|
532
|
+
if (left < 8) left = 8;
|
|
533
|
+
setPos({ top: rect.bottom + 6, left });
|
|
534
|
+
}, [open, triggerRef]);
|
|
535
|
+
useEffect(() => {
|
|
536
|
+
if (!open) return;
|
|
537
|
+
const handler = (e) => {
|
|
538
|
+
if (popoverRef.current?.contains(e.target)) return;
|
|
539
|
+
if (triggerRef.current?.contains(e.target)) return;
|
|
540
|
+
onClose();
|
|
541
|
+
};
|
|
542
|
+
document.addEventListener("mousedown", handler);
|
|
543
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
544
|
+
}, [open, onClose, triggerRef]);
|
|
545
|
+
if (!open) return null;
|
|
546
|
+
const showServices = config.showServices !== false && config.services && config.services.length > 0;
|
|
547
|
+
const showConcurrent = config.showConcurrent !== false;
|
|
548
|
+
const showBookingWindow = config.showBookingWindow !== false;
|
|
549
|
+
const showLocations = config.showLocations === true && config.locations && config.locations.length > 0;
|
|
550
|
+
function update(patch) {
|
|
551
|
+
onChange({ ...settings, ...patch });
|
|
552
|
+
}
|
|
553
|
+
return createPortal(
|
|
554
|
+
/* @__PURE__ */ jsxs(
|
|
555
|
+
"div",
|
|
556
|
+
{
|
|
557
|
+
ref: popoverRef,
|
|
558
|
+
style: { position: "fixed", top: pos.top, left: pos.left, zIndex: 9999 },
|
|
559
|
+
className: "fayz-glass-surface w-[280px] rounded-xl border bg-popover shadow-xl",
|
|
560
|
+
children: [
|
|
561
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-3 py-2.5 border-b", children: [
|
|
562
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-semibold", children: t("schedule.blockSettings.title") }),
|
|
563
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: onClose, className: "p-0.5 rounded text-muted-foreground hover:text-foreground", children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" }) })
|
|
564
|
+
] }),
|
|
565
|
+
/* @__PURE__ */ jsxs("div", { className: "p-3 space-y-3", children: [
|
|
566
|
+
showLocations && /* @__PURE__ */ jsx(
|
|
567
|
+
LocationSelect,
|
|
568
|
+
{
|
|
569
|
+
locations: config.locations,
|
|
570
|
+
value: settings.locationId,
|
|
571
|
+
onChange: (id) => update({ locationId: id || void 0 })
|
|
572
|
+
}
|
|
573
|
+
),
|
|
574
|
+
showServices && /* @__PURE__ */ jsx(
|
|
575
|
+
ServiceSelect,
|
|
576
|
+
{
|
|
577
|
+
services: config.services,
|
|
578
|
+
selected: settings.allowedServiceIds ?? [],
|
|
579
|
+
onChange: (ids) => update({ allowedServiceIds: ids.length > 0 ? ids : void 0 })
|
|
580
|
+
}
|
|
581
|
+
),
|
|
582
|
+
showConcurrent && /* @__PURE__ */ jsxs("div", { children: [
|
|
583
|
+
/* @__PURE__ */ jsx("label", { className: "text-[11px] font-medium text-muted-foreground", children: t("schedule.blockSettings.maxConcurrent") }),
|
|
584
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mt-1", children: [
|
|
585
|
+
/* @__PURE__ */ jsx(
|
|
586
|
+
"button",
|
|
587
|
+
{
|
|
588
|
+
type: "button",
|
|
589
|
+
onClick: () => update({ maxConcurrent: Math.max(1, (settings.maxConcurrent ?? config.defaults.maxConcurrent) - 1) }),
|
|
590
|
+
className: "flex h-7 w-7 items-center justify-center rounded-md border text-sm hover:bg-muted bg-card shadow-button active:shadow-button-inset transition-colors",
|
|
591
|
+
children: "-"
|
|
592
|
+
}
|
|
593
|
+
),
|
|
594
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium w-6 text-center", children: settings.maxConcurrent ?? config.defaults.maxConcurrent }),
|
|
595
|
+
/* @__PURE__ */ jsx(
|
|
596
|
+
"button",
|
|
597
|
+
{
|
|
598
|
+
type: "button",
|
|
599
|
+
onClick: () => update({ maxConcurrent: (settings.maxConcurrent ?? config.defaults.maxConcurrent) + 1 }),
|
|
600
|
+
className: "flex h-7 w-7 items-center justify-center rounded-md border text-sm hover:bg-muted bg-card shadow-button active:shadow-button-inset transition-colors",
|
|
601
|
+
children: "+"
|
|
602
|
+
}
|
|
603
|
+
)
|
|
604
|
+
] })
|
|
605
|
+
] }),
|
|
606
|
+
showBookingWindow && /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
|
|
607
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
608
|
+
/* @__PURE__ */ jsx("label", { className: "text-[11px] font-medium text-muted-foreground", children: t("schedule.blockSettings.minAdvance") }),
|
|
609
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 mt-1", children: [
|
|
610
|
+
/* @__PURE__ */ jsx(
|
|
611
|
+
"input",
|
|
612
|
+
{
|
|
613
|
+
type: "number",
|
|
614
|
+
min: 0,
|
|
615
|
+
value: settings.minAdvanceHours ?? config.defaults.minAdvanceHours,
|
|
616
|
+
onChange: (e) => update({ minAdvanceHours: Number(e.target.value) || 0 }),
|
|
617
|
+
className: "w-full rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2 py-1 text-xs tabular-nums"
|
|
618
|
+
}
|
|
619
|
+
),
|
|
620
|
+
/* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground shrink-0", children: t("schedule.blockSettings.hrs") })
|
|
621
|
+
] })
|
|
622
|
+
] }),
|
|
623
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
624
|
+
/* @__PURE__ */ jsx("label", { className: "text-[11px] font-medium text-muted-foreground", children: t("schedule.blockSettings.maxAdvance") }),
|
|
625
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 mt-1", children: [
|
|
626
|
+
/* @__PURE__ */ jsx(
|
|
627
|
+
"input",
|
|
628
|
+
{
|
|
629
|
+
type: "number",
|
|
630
|
+
min: 1,
|
|
631
|
+
value: settings.maxAdvanceDays ?? config.defaults.maxAdvanceDays,
|
|
632
|
+
onChange: (e) => update({ maxAdvanceDays: Number(e.target.value) || 30 }),
|
|
633
|
+
className: "w-full rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2 py-1 text-xs tabular-nums"
|
|
634
|
+
}
|
|
635
|
+
),
|
|
636
|
+
/* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground shrink-0", children: t("schedule.blockSettings.days") })
|
|
637
|
+
] })
|
|
638
|
+
] })
|
|
639
|
+
] }),
|
|
640
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
641
|
+
/* @__PURE__ */ jsx("label", { className: "text-[11px] font-medium text-muted-foreground", children: t("schedule.blockSettings.label") }),
|
|
642
|
+
/* @__PURE__ */ jsx(
|
|
643
|
+
"input",
|
|
644
|
+
{
|
|
645
|
+
type: "text",
|
|
646
|
+
value: settings.label ?? "",
|
|
647
|
+
onChange: (e) => update({ label: e.target.value || void 0 }),
|
|
648
|
+
placeholder: t("schedule.blockSettings.labelPlaceholder"),
|
|
649
|
+
className: "mt-1 w-full rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2 py-1 text-xs placeholder:text-muted-foreground/40"
|
|
650
|
+
}
|
|
651
|
+
)
|
|
652
|
+
] })
|
|
653
|
+
] })
|
|
654
|
+
]
|
|
655
|
+
}
|
|
656
|
+
),
|
|
657
|
+
document.body
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
function LocationSelect({
|
|
661
|
+
locations,
|
|
662
|
+
value,
|
|
663
|
+
onChange
|
|
664
|
+
}) {
|
|
665
|
+
const t = useTranslation();
|
|
666
|
+
const [open, setOpen] = useState(false);
|
|
667
|
+
const ref = useRef(null);
|
|
668
|
+
useEffect(() => {
|
|
669
|
+
if (!open) return;
|
|
670
|
+
const handler = (e) => {
|
|
671
|
+
if (ref.current && !ref.current.contains(e.target)) setOpen(false);
|
|
672
|
+
};
|
|
673
|
+
document.addEventListener("mousedown", handler);
|
|
674
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
675
|
+
}, [open]);
|
|
676
|
+
const current = value ? locations.find((l) => l.id === value) : null;
|
|
677
|
+
const label = current ? current.name : t("schedule.blockSettings.noLocation");
|
|
678
|
+
return /* @__PURE__ */ jsxs("div", { ref, className: "relative", children: [
|
|
679
|
+
/* @__PURE__ */ jsx("label", { className: "text-[11px] font-medium text-muted-foreground", children: t("schedule.blockSettings.location") }),
|
|
680
|
+
/* @__PURE__ */ jsxs(
|
|
681
|
+
"button",
|
|
682
|
+
{
|
|
683
|
+
type: "button",
|
|
684
|
+
onClick: () => setOpen((p) => !p),
|
|
685
|
+
className: "mt-1 flex items-center justify-between w-full rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2 py-1.5 text-xs hover:bg-muted/30 transition-colors",
|
|
686
|
+
children: [
|
|
687
|
+
/* @__PURE__ */ jsx("span", { className: current ? "" : "text-muted-foreground", children: label }),
|
|
688
|
+
/* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 text-muted-foreground" })
|
|
689
|
+
]
|
|
690
|
+
}
|
|
691
|
+
),
|
|
692
|
+
open && /* @__PURE__ */ jsxs("div", { className: "fayz-glass-surface absolute top-full left-0 z-10 mt-1 w-full max-h-40 overflow-y-auto rounded-lg border bg-popover shadow-lg py-1", children: [
|
|
693
|
+
/* @__PURE__ */ jsx(
|
|
694
|
+
"button",
|
|
695
|
+
{
|
|
696
|
+
type: "button",
|
|
697
|
+
onClick: () => {
|
|
698
|
+
onChange("");
|
|
699
|
+
setOpen(false);
|
|
700
|
+
},
|
|
701
|
+
className: `block w-full px-2.5 py-1.5 text-xs text-left transition-colors ${!value ? "bg-primary/10 text-primary font-medium" : "hover:bg-muted/50"}`,
|
|
702
|
+
children: t("schedule.blockSettings.noLocation")
|
|
703
|
+
}
|
|
704
|
+
),
|
|
705
|
+
locations.map((l) => /* @__PURE__ */ jsx(
|
|
706
|
+
"button",
|
|
707
|
+
{
|
|
708
|
+
type: "button",
|
|
709
|
+
onClick: () => {
|
|
710
|
+
onChange(l.id);
|
|
711
|
+
setOpen(false);
|
|
712
|
+
},
|
|
713
|
+
className: `block w-full px-2.5 py-1.5 text-xs text-left transition-colors ${l.id === value ? "bg-primary/10 text-primary font-medium" : "hover:bg-muted/50"}`,
|
|
714
|
+
children: l.name
|
|
715
|
+
},
|
|
716
|
+
l.id
|
|
717
|
+
))
|
|
718
|
+
] })
|
|
719
|
+
] });
|
|
720
|
+
}
|
|
721
|
+
function ServiceSelect({
|
|
722
|
+
services,
|
|
723
|
+
selected,
|
|
724
|
+
onChange
|
|
725
|
+
}) {
|
|
726
|
+
const t = useTranslation();
|
|
727
|
+
const [open, setOpen] = useState(false);
|
|
728
|
+
const ref = useRef(null);
|
|
729
|
+
useEffect(() => {
|
|
730
|
+
if (!open) return;
|
|
731
|
+
const handler = (e) => {
|
|
732
|
+
if (ref.current && !ref.current.contains(e.target)) setOpen(false);
|
|
733
|
+
};
|
|
734
|
+
document.addEventListener("mousedown", handler);
|
|
735
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
736
|
+
}, [open]);
|
|
737
|
+
const allSelected = selected.length === 0;
|
|
738
|
+
const label = allSelected ? t("schedule.blockSettings.allServices") : selected.length === 1 ? services.find((s) => s.id === selected[0])?.name ?? t("schedule.blockSettings.oneService") : t("schedule.blockSettings.nServices", { count: selected.length });
|
|
739
|
+
function toggle(id) {
|
|
740
|
+
onChange(selected.includes(id) ? selected.filter((s) => s !== id) : [...selected, id]);
|
|
741
|
+
}
|
|
742
|
+
return /* @__PURE__ */ jsxs("div", { ref, className: "relative", children: [
|
|
743
|
+
/* @__PURE__ */ jsx("label", { className: "text-[11px] font-medium text-muted-foreground", children: t("schedule.blockSettings.allowedServices") }),
|
|
744
|
+
/* @__PURE__ */ jsxs(
|
|
745
|
+
"button",
|
|
746
|
+
{
|
|
747
|
+
type: "button",
|
|
748
|
+
onClick: () => setOpen((p) => !p),
|
|
749
|
+
className: "mt-1 flex items-center justify-between w-full rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2 py-1.5 text-xs hover:bg-muted/30 transition-colors",
|
|
750
|
+
children: [
|
|
751
|
+
/* @__PURE__ */ jsx("span", { className: allSelected ? "text-muted-foreground" : "", children: label }),
|
|
752
|
+
/* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 text-muted-foreground" })
|
|
753
|
+
]
|
|
754
|
+
}
|
|
755
|
+
),
|
|
756
|
+
open && /* @__PURE__ */ jsxs("div", { className: "fayz-glass-surface absolute top-full left-0 z-10 mt-1 w-full max-h-40 overflow-y-auto rounded-lg border bg-popover shadow-lg py-1", children: [
|
|
757
|
+
/* @__PURE__ */ jsx(
|
|
758
|
+
"button",
|
|
759
|
+
{
|
|
760
|
+
type: "button",
|
|
761
|
+
onClick: () => onChange([]),
|
|
762
|
+
className: `block w-full px-2.5 py-1.5 text-xs text-left transition-colors ${allSelected ? "bg-primary/10 text-primary font-medium" : "hover:bg-muted/50"}`,
|
|
763
|
+
children: t("schedule.blockSettings.allServices")
|
|
764
|
+
}
|
|
765
|
+
),
|
|
766
|
+
services.map((s) => {
|
|
767
|
+
const checked = selected.includes(s.id);
|
|
768
|
+
return /* @__PURE__ */ jsxs(
|
|
769
|
+
"button",
|
|
770
|
+
{
|
|
771
|
+
type: "button",
|
|
772
|
+
onClick: () => toggle(s.id),
|
|
773
|
+
className: `flex items-center gap-2 w-full px-2.5 py-1.5 text-xs text-left transition-colors ${checked ? "bg-primary/5" : "hover:bg-muted/50"}`,
|
|
774
|
+
children: [
|
|
775
|
+
/* @__PURE__ */ jsx("span", { className: `flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-sm border ${checked ? "border-primary bg-primary text-primary-foreground" : "border-muted-foreground/30"}`, children: checked && /* @__PURE__ */ jsx("svg", { className: "h-2.5 w-2.5", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ jsx("path", { d: "M2 6l3 3 5-5", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }),
|
|
776
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: s.name })
|
|
777
|
+
]
|
|
778
|
+
},
|
|
779
|
+
s.id
|
|
780
|
+
);
|
|
781
|
+
})
|
|
782
|
+
] })
|
|
783
|
+
] });
|
|
784
|
+
}
|
|
785
|
+
var WEEK_ORDER = [1, 2, 3, 4, 5, 6, 0];
|
|
786
|
+
var DAY_KEYS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
|
787
|
+
var SLOT_OPTIONS = [5, 10, 15, 20, 30, 45, 60];
|
|
788
|
+
var DEFAULT_SLOT = 30;
|
|
789
|
+
var today = () => todayLocal();
|
|
790
|
+
function formatDay(date) {
|
|
791
|
+
if (!date) return "";
|
|
792
|
+
const [y, m, d] = date.split("-");
|
|
793
|
+
return `${d}/${m}/${y}`;
|
|
794
|
+
}
|
|
795
|
+
var emptyDays2 = () => ({ 0: [], 1: [], 2: [], 3: [], 4: [], 5: [], 6: [] });
|
|
796
|
+
function newGrid(kind) {
|
|
797
|
+
return {
|
|
798
|
+
key: `novo:${kind}`,
|
|
799
|
+
kind,
|
|
800
|
+
validFrom: today(),
|
|
801
|
+
validTo: null,
|
|
802
|
+
slotIntervalMinutes: DEFAULT_SLOT,
|
|
803
|
+
weekInterval: 0,
|
|
804
|
+
name: null,
|
|
805
|
+
unitId: null,
|
|
806
|
+
days: emptyDays2(),
|
|
807
|
+
rowIds: []
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
function cloneGrid(g) {
|
|
811
|
+
const days = emptyDays2();
|
|
812
|
+
for (const [d, periods] of Object.entries(g.days)) {
|
|
813
|
+
days[Number(d)] = periods.map((p) => ({ ...p, settings: p.settings ? { ...p.settings } : void 0 }));
|
|
814
|
+
}
|
|
815
|
+
return { ...g, days };
|
|
816
|
+
}
|
|
817
|
+
function useResources() {
|
|
818
|
+
const config = useSyncExternalStore(subscribeScheduleBlockConfig, getScheduleBlockConfig, () => null);
|
|
819
|
+
const asked = useRef(false);
|
|
820
|
+
useEffect(() => {
|
|
821
|
+
if (asked.current) return;
|
|
822
|
+
if (config?.resources?.length || !config?.fetchResources) return;
|
|
823
|
+
asked.current = true;
|
|
824
|
+
void config.fetchResources();
|
|
825
|
+
}, [config]);
|
|
826
|
+
return config?.resources ?? [];
|
|
827
|
+
}
|
|
828
|
+
function ConfirmDialog({
|
|
829
|
+
open,
|
|
830
|
+
title,
|
|
831
|
+
description,
|
|
832
|
+
confirmLabel,
|
|
833
|
+
destructive,
|
|
834
|
+
onConfirm,
|
|
835
|
+
onCancel
|
|
836
|
+
}) {
|
|
837
|
+
const t = useTranslation();
|
|
838
|
+
return /* @__PURE__ */ jsx(Modal, { open, onOpenChange: (o) => {
|
|
839
|
+
if (!o) onCancel();
|
|
840
|
+
}, children: /* @__PURE__ */ jsxs(ModalContent, { size: "sm", children: [
|
|
841
|
+
/* @__PURE__ */ jsxs(ModalHeader, { children: [
|
|
842
|
+
/* @__PURE__ */ jsx(ModalTitle, { children: title }),
|
|
843
|
+
/* @__PURE__ */ jsx(ModalDescription, { children: description })
|
|
844
|
+
] }),
|
|
845
|
+
/* @__PURE__ */ jsx(ModalBody, {}),
|
|
846
|
+
/* @__PURE__ */ jsxs(ModalFooter, { children: [
|
|
847
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: onCancel, children: t("common.cancel") }),
|
|
848
|
+
/* @__PURE__ */ jsx(Button, { variant: destructive ? "destructive" : "default", onClick: onConfirm, children: confirmLabel })
|
|
849
|
+
] })
|
|
850
|
+
] }) });
|
|
851
|
+
}
|
|
852
|
+
function PeriodBox({
|
|
853
|
+
period,
|
|
854
|
+
resources,
|
|
855
|
+
onChange,
|
|
856
|
+
onRemove,
|
|
857
|
+
onCopyToAll
|
|
858
|
+
}) {
|
|
859
|
+
const t = useTranslation();
|
|
860
|
+
const gearRef = useRef(null);
|
|
861
|
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
862
|
+
const blockConfig = useSyncExternalStore(subscribeScheduleBlockConfig, getScheduleBlockConfig, () => null);
|
|
863
|
+
const hasSettings = period.settings && Object.values(period.settings).some((v) => v !== void 0);
|
|
864
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5 rounded-md border border-border/60 bg-background p-1.5", children: [
|
|
865
|
+
/* @__PURE__ */ jsx(TimePicker, { value: period.startsAt, onChange: (v) => onChange({ startsAt: v }), interval: 15, className: "w-full" }),
|
|
866
|
+
/* @__PURE__ */ jsx(TimePicker, { value: period.endsAt, onChange: (v) => onChange({ endsAt: v }), interval: 15, min: period.startsAt, className: "w-full" }),
|
|
867
|
+
resources.length > 1 && /* @__PURE__ */ jsxs(Select, { value: period.resourceId ?? "", onValueChange: (v) => onChange({ resourceId: v || null }), children: [
|
|
868
|
+
/* @__PURE__ */ jsx(SelectTrigger, { className: "h-8 w-full text-xs", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: t("schedule.grid.room") }) }),
|
|
869
|
+
/* @__PURE__ */ jsx(SelectContent, { children: resources.map((r) => /* @__PURE__ */ jsx(SelectItem, { value: r.id, children: r.name }, r.id)) })
|
|
870
|
+
] }),
|
|
871
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-0.5 pt-0.5", children: [
|
|
872
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
873
|
+
blockConfig && /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
874
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
875
|
+
"button",
|
|
876
|
+
{
|
|
877
|
+
ref: gearRef,
|
|
878
|
+
type: "button",
|
|
879
|
+
onClick: () => setSettingsOpen((v) => !v),
|
|
880
|
+
"aria-label": t("schedule.grid.configure"),
|
|
881
|
+
className: "flex flex-1 items-center gap-1 rounded px-1 py-0.5 text-[11px] transition-colors " + (hasSettings ? "text-primary" : "text-muted-foreground hover:bg-muted hover:text-foreground"),
|
|
882
|
+
children: [
|
|
883
|
+
/* @__PURE__ */ jsx(Settings, { className: "h-3 w-3 shrink-0" }),
|
|
884
|
+
t("schedule.grid.configure")
|
|
885
|
+
]
|
|
886
|
+
}
|
|
887
|
+
) }),
|
|
888
|
+
/* @__PURE__ */ jsx(TooltipContent, { children: t("schedule.blockSettings.title") })
|
|
889
|
+
] }),
|
|
890
|
+
/* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
891
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
892
|
+
"button",
|
|
893
|
+
{
|
|
894
|
+
type: "button",
|
|
895
|
+
onClick: onCopyToAll,
|
|
896
|
+
"aria-label": t("schedule.editor.copyToAll"),
|
|
897
|
+
className: "ml-auto shrink-0 rounded p-1 text-muted-foreground/50 transition-colors hover:bg-muted hover:text-foreground",
|
|
898
|
+
children: /* @__PURE__ */ jsx(Copy, { className: "h-3 w-3" })
|
|
899
|
+
}
|
|
900
|
+
) }),
|
|
901
|
+
/* @__PURE__ */ jsx(TooltipContent, { children: t("schedule.editor.copyToAll") })
|
|
902
|
+
] })
|
|
903
|
+
] }),
|
|
904
|
+
/* @__PURE__ */ jsxs(
|
|
905
|
+
"button",
|
|
906
|
+
{
|
|
907
|
+
type: "button",
|
|
908
|
+
onClick: onRemove,
|
|
909
|
+
className: "flex w-full items-center gap-1 rounded px-1 py-0.5 text-[11px] text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive",
|
|
910
|
+
children: [
|
|
911
|
+
/* @__PURE__ */ jsx(Trash2, { className: "h-3 w-3 shrink-0" }),
|
|
912
|
+
t("schedule.grid.removePeriod")
|
|
913
|
+
]
|
|
914
|
+
}
|
|
915
|
+
)
|
|
916
|
+
] }),
|
|
917
|
+
blockConfig && /* @__PURE__ */ jsx(
|
|
918
|
+
BlockSettingsPopover,
|
|
919
|
+
{
|
|
920
|
+
settings: period.settings ?? {},
|
|
921
|
+
onChange: (s) => onChange({ settings: s }),
|
|
922
|
+
config: blockConfig,
|
|
923
|
+
triggerRef: gearRef,
|
|
924
|
+
open: settingsOpen,
|
|
925
|
+
onClose: () => setSettingsOpen(false)
|
|
926
|
+
}
|
|
927
|
+
)
|
|
928
|
+
] });
|
|
929
|
+
}
|
|
930
|
+
function ScheduleGridPanel({ assigneeId, kind, onSaved }) {
|
|
931
|
+
const t = useTranslation();
|
|
932
|
+
const resources = useResources();
|
|
933
|
+
const [grids, setGrids] = useState(null);
|
|
934
|
+
const [draft, setDraft] = useState(null);
|
|
935
|
+
const [dirty, setDirty] = useState(false);
|
|
936
|
+
const [saving, setSaving] = useState(false);
|
|
937
|
+
const [confirm, setConfirm] = useState(null);
|
|
938
|
+
const [pendingSelect, setPendingSelect] = useState(null);
|
|
939
|
+
const load = useCallback(async () => {
|
|
940
|
+
setGrids(null);
|
|
941
|
+
try {
|
|
942
|
+
const all = await listGrids(assigneeId);
|
|
943
|
+
const mine = all.filter((g) => g.kind === kind);
|
|
944
|
+
setGrids(mine);
|
|
945
|
+
setDraft(mine[0] ? cloneGrid(mine[0]) : null);
|
|
946
|
+
setDirty(false);
|
|
947
|
+
} catch (err) {
|
|
948
|
+
setGrids([]);
|
|
949
|
+
toast.error(t("schedule.editor.failedToLoad"), { description: err?.message });
|
|
950
|
+
}
|
|
951
|
+
}, [assigneeId, kind, t]);
|
|
952
|
+
useEffect(() => {
|
|
953
|
+
void load();
|
|
954
|
+
}, [load]);
|
|
955
|
+
const patch = (p) => {
|
|
956
|
+
setDraft((d) => d ? { ...d, ...p } : d);
|
|
957
|
+
setDirty(true);
|
|
958
|
+
};
|
|
959
|
+
const patchDay = (dow, periods) => {
|
|
960
|
+
setDraft((d) => d ? { ...d, days: { ...d.days, [dow]: periods } } : d);
|
|
961
|
+
setDirty(true);
|
|
962
|
+
};
|
|
963
|
+
const addPeriod = (dow) => {
|
|
964
|
+
const periods = draft?.days[dow] ?? [];
|
|
965
|
+
const last = periods[periods.length - 1];
|
|
966
|
+
patchDay(dow, [...periods, { startsAt: last?.endsAt ?? "08:00", endsAt: "18:00", resourceId: last?.resourceId ?? resources[0]?.id ?? null }]);
|
|
967
|
+
};
|
|
968
|
+
const copyToAll = (period) => {
|
|
969
|
+
if (!draft) return;
|
|
970
|
+
const days = emptyDays2();
|
|
971
|
+
for (const d of WEEK_ORDER) days[d] = [{ ...period, id: void 0, settings: period.settings ? { ...period.settings } : void 0 }];
|
|
972
|
+
setDraft({ ...draft, days });
|
|
973
|
+
setDirty(true);
|
|
974
|
+
};
|
|
975
|
+
const selectGrid = (g) => {
|
|
976
|
+
if (dirty) {
|
|
977
|
+
setPendingSelect(g);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
setDraft(cloneGrid(g));
|
|
981
|
+
setDirty(false);
|
|
982
|
+
};
|
|
983
|
+
const startNew = () => {
|
|
984
|
+
setDraft(newGrid(kind));
|
|
985
|
+
setDirty(true);
|
|
986
|
+
};
|
|
987
|
+
const duplicate = () => {
|
|
988
|
+
if (!draft) return;
|
|
989
|
+
const copy = cloneGrid(draft);
|
|
990
|
+
copy.key = `novo:${kind}`;
|
|
991
|
+
copy.rowIds = [];
|
|
992
|
+
copy.validFrom = today();
|
|
993
|
+
copy.validTo = null;
|
|
994
|
+
copy.name = draft.name ? `${draft.name} (${t("common.copy")})` : null;
|
|
995
|
+
for (const d of Object.keys(copy.days)) copy.days[Number(d)] = copy.days[Number(d)].map((p) => ({ ...p, id: void 0 }));
|
|
996
|
+
setDraft(copy);
|
|
997
|
+
setDirty(true);
|
|
998
|
+
};
|
|
999
|
+
const [copyOpen, setCopyOpen] = useState(false);
|
|
1000
|
+
const [people, setPeople] = useState(null);
|
|
1001
|
+
const [picked, setPicked] = useState([]);
|
|
1002
|
+
const [copyTerm, setCopyTerm] = useState("");
|
|
1003
|
+
const [copying, setCopying] = useState(false);
|
|
1004
|
+
const openCopy = async () => {
|
|
1005
|
+
setCopyOpen(true);
|
|
1006
|
+
setPicked([]);
|
|
1007
|
+
setCopyTerm("");
|
|
1008
|
+
setPeople(null);
|
|
1009
|
+
try {
|
|
1010
|
+
const lookup = createArchetypeLookup({ archetype: "person", bookableOnly: true, limit: 200 });
|
|
1011
|
+
const rows = (await lookup.list?.() ?? []).filter((r) => r.id !== assigneeId);
|
|
1012
|
+
const comGrade = await assigneesWithCurrentGrid(rows.map((r) => r.id));
|
|
1013
|
+
setPeople(rows.map((r) => ({ id: r.id, name: r.label, hasGrid: comGrade.has(r.id) })));
|
|
1014
|
+
} catch (err) {
|
|
1015
|
+
setPeople([]);
|
|
1016
|
+
toast.error("N\xE3o foi poss\xEDvel listar as pessoas", { description: err?.message });
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
const runCopy = async () => {
|
|
1020
|
+
if (!draft || picked.length === 0) return;
|
|
1021
|
+
setCopying(true);
|
|
1022
|
+
try {
|
|
1023
|
+
await copyGridTo(picked, draft);
|
|
1024
|
+
toast.success(picked.length === 1 ? "Grade copiada para 1 pessoa" : `Grade copiada para ${picked.length} pessoas`);
|
|
1025
|
+
setCopyOpen(false);
|
|
1026
|
+
onSaved?.();
|
|
1027
|
+
} catch (err) {
|
|
1028
|
+
toast.error("N\xE3o foi poss\xEDvel copiar a grade", { description: err?.message });
|
|
1029
|
+
} finally {
|
|
1030
|
+
setCopying(false);
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
const save = async () => {
|
|
1034
|
+
if (!draft) return;
|
|
1035
|
+
if (!draft.validFrom) {
|
|
1036
|
+
toast.error(t("schedule.grid.needsStart"));
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
if (draft.validTo && draft.validTo < draft.validFrom) {
|
|
1040
|
+
toast.error(t("schedule.editor.endDateAfterStart"));
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
const total = Object.values(draft.days).reduce((n, p) => n + p.length, 0);
|
|
1044
|
+
if (total === 0) {
|
|
1045
|
+
toast.error(t("schedule.grid.needsOneDay"));
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
setSaving(true);
|
|
1049
|
+
try {
|
|
1050
|
+
await saveGrid(assigneeId, draft);
|
|
1051
|
+
toast.success(t("schedule.editor.saved"));
|
|
1052
|
+
onSaved?.();
|
|
1053
|
+
await load();
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
toast.error(t("schedule.editor.failedToSave"), { description: err?.message });
|
|
1056
|
+
} finally {
|
|
1057
|
+
setSaving(false);
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
const endValidity = async () => {
|
|
1061
|
+
if (!draft || draft.rowIds.length === 0) return;
|
|
1062
|
+
setConfirm(null);
|
|
1063
|
+
try {
|
|
1064
|
+
await endGridValidity(draft, today());
|
|
1065
|
+
toast.success(t("schedule.grid.ended"));
|
|
1066
|
+
onSaved?.();
|
|
1067
|
+
await load();
|
|
1068
|
+
} catch (err) {
|
|
1069
|
+
toast.error(t("schedule.editor.failedToSave"), { description: err?.message });
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
const removeGrid = async () => {
|
|
1073
|
+
if (!draft) return;
|
|
1074
|
+
if (draft.rowIds.length === 0) {
|
|
1075
|
+
setDraft(grids?.[0] ? cloneGrid(grids[0]) : null);
|
|
1076
|
+
setDirty(false);
|
|
1077
|
+
setConfirm(null);
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
setConfirm(null);
|
|
1081
|
+
try {
|
|
1082
|
+
await deleteGrid(draft);
|
|
1083
|
+
toast.success(t("schedule.grid.deleted"));
|
|
1084
|
+
onSaved?.();
|
|
1085
|
+
await load();
|
|
1086
|
+
} catch (err) {
|
|
1087
|
+
toast.error(t("schedule.editor.failedToDelete"), { description: err?.message });
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
if (grids === null) {
|
|
1091
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
1092
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-16 w-56" }),
|
|
1093
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-3", children: [
|
|
1094
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-14 w-36" }),
|
|
1095
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-14 w-36" }),
|
|
1096
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-14 w-40" })
|
|
1097
|
+
] }),
|
|
1098
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2 lg:grid-cols-7", children: WEEK_ORDER.map((d) => /* @__PURE__ */ jsx(Skeleton, { className: "h-36 w-full" }, d)) })
|
|
1099
|
+
] });
|
|
1100
|
+
}
|
|
1101
|
+
if (!draft) {
|
|
1102
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center gap-3 py-16 text-center", children: [
|
|
1103
|
+
/* @__PURE__ */ jsx("p", { className: "max-w-md text-sm text-muted-foreground", children: kind === "standard" ? t("schedule.grid.emptyStandard") : t("schedule.grid.emptyException") }),
|
|
1104
|
+
/* @__PURE__ */ jsxs(Button, { onClick: startNew, className: "gap-1.5", children: [
|
|
1105
|
+
/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
|
|
1106
|
+
kind === "standard" ? t("schedule.grid.createFirst") : t("schedule.grid.createException")
|
|
1107
|
+
] })
|
|
1108
|
+
] });
|
|
1109
|
+
}
|
|
1110
|
+
const isNew = draft.rowIds.length === 0;
|
|
1111
|
+
return /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 300, children: /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
1112
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-stretch gap-2", children: [
|
|
1113
|
+
grids.map((g) => {
|
|
1114
|
+
const active = gridKeyOf(g) === gridKeyOf(draft) && !isNew;
|
|
1115
|
+
return /* @__PURE__ */ jsxs(
|
|
1116
|
+
"button",
|
|
1117
|
+
{
|
|
1118
|
+
type: "button",
|
|
1119
|
+
onClick: () => selectGrid(g),
|
|
1120
|
+
className: "min-w-[13rem] rounded-lg border px-3 py-2 text-left transition-colors " + (active ? "border-primary bg-primary/5" : "border-border hover:bg-muted/50"),
|
|
1121
|
+
children: [
|
|
1122
|
+
/* @__PURE__ */ jsxs("span", { className: "block text-sm font-medium tabular-nums", children: [
|
|
1123
|
+
formatDay(g.validFrom),
|
|
1124
|
+
" \u2192 ",
|
|
1125
|
+
g.validTo ? formatDay(g.validTo) : t("schedule.grid.noEnd")
|
|
1126
|
+
] }),
|
|
1127
|
+
/* @__PURE__ */ jsxs("span", { className: "mt-1 flex items-center gap-1.5", children: [
|
|
1128
|
+
isGridCurrent(g) && /* @__PURE__ */ jsx(Badge, { variant: "success", className: "text-[10px]", children: t("schedule.grid.current") }),
|
|
1129
|
+
g.name && /* @__PURE__ */ jsx("span", { className: "truncate text-xs text-muted-foreground", children: g.name })
|
|
1130
|
+
] })
|
|
1131
|
+
]
|
|
1132
|
+
},
|
|
1133
|
+
g.key
|
|
1134
|
+
);
|
|
1135
|
+
}),
|
|
1136
|
+
isNew && /* @__PURE__ */ jsxs("div", { className: "min-w-[13rem] rounded-lg border border-primary bg-primary/5 px-3 py-2", children: [
|
|
1137
|
+
/* @__PURE__ */ jsx("span", { className: "block text-sm font-medium", children: t("schedule.grid.newGrid") }),
|
|
1138
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("schedule.grid.notSavedYet") })
|
|
1139
|
+
] }),
|
|
1140
|
+
/* @__PURE__ */ jsxs(
|
|
1141
|
+
"button",
|
|
1142
|
+
{
|
|
1143
|
+
type: "button",
|
|
1144
|
+
onClick: startNew,
|
|
1145
|
+
className: "flex min-w-[9rem] items-center justify-center gap-1.5 rounded-lg border border-dashed px-3 py-2 text-sm text-muted-foreground transition-colors hover:border-primary hover:text-foreground",
|
|
1146
|
+
children: [
|
|
1147
|
+
/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
|
|
1148
|
+
t("schedule.grid.newGrid")
|
|
1149
|
+
]
|
|
1150
|
+
}
|
|
1151
|
+
),
|
|
1152
|
+
/* @__PURE__ */ jsxs(
|
|
1153
|
+
"button",
|
|
1154
|
+
{
|
|
1155
|
+
type: "button",
|
|
1156
|
+
onClick: duplicate,
|
|
1157
|
+
className: "flex min-w-[9rem] items-center justify-center gap-1.5 rounded-lg border border-dashed px-3 py-2 text-sm text-muted-foreground transition-colors hover:border-primary hover:text-foreground",
|
|
1158
|
+
children: [
|
|
1159
|
+
/* @__PURE__ */ jsx(Copy, { className: "h-4 w-4" }),
|
|
1160
|
+
t("schedule.grid.duplicate")
|
|
1161
|
+
]
|
|
1162
|
+
}
|
|
1163
|
+
),
|
|
1164
|
+
!isNew && /* @__PURE__ */ jsxs(
|
|
1165
|
+
"button",
|
|
1166
|
+
{
|
|
1167
|
+
type: "button",
|
|
1168
|
+
onClick: openCopy,
|
|
1169
|
+
className: "flex min-w-[9rem] items-center justify-center gap-1.5 rounded-lg border border-dashed px-3 py-2 text-sm text-muted-foreground transition-colors hover:border-primary hover:text-foreground",
|
|
1170
|
+
children: [
|
|
1171
|
+
/* @__PURE__ */ jsx(Users, { className: "h-4 w-4" }),
|
|
1172
|
+
"Copiar para\u2026"
|
|
1173
|
+
]
|
|
1174
|
+
}
|
|
1175
|
+
)
|
|
1176
|
+
] }),
|
|
1177
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4", children: [
|
|
1178
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1179
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: t("schedule.grid.validFrom") }),
|
|
1180
|
+
/* @__PURE__ */ jsx(DatePicker, { value: draft.validFrom ?? "", format: "numeric", onChange: (v) => patch({ validFrom: v || null }) })
|
|
1181
|
+
] }),
|
|
1182
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1183
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: t("schedule.grid.validTo") }),
|
|
1184
|
+
/* @__PURE__ */ jsx(DatePicker, { value: draft.validTo ?? "", format: "numeric", onChange: (v) => patch({ validTo: v || null }) }),
|
|
1185
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-[11px] text-muted-foreground", children: t("schedule.grid.validToHint") })
|
|
1186
|
+
] }),
|
|
1187
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1188
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: t("schedule.grid.slotInterval") }),
|
|
1189
|
+
/* @__PURE__ */ jsxs(
|
|
1190
|
+
Select,
|
|
1191
|
+
{
|
|
1192
|
+
value: String(draft.slotIntervalMinutes ?? DEFAULT_SLOT),
|
|
1193
|
+
onValueChange: (v) => patch({ slotIntervalMinutes: Number(v) }),
|
|
1194
|
+
children: [
|
|
1195
|
+
/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
1196
|
+
/* @__PURE__ */ jsx(SelectContent, { children: SLOT_OPTIONS.map((m) => /* @__PURE__ */ jsx(SelectItem, { value: String(m), children: t("schedule.grid.minutes", { n: m }) }, m)) })
|
|
1197
|
+
]
|
|
1198
|
+
}
|
|
1199
|
+
),
|
|
1200
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-[11px] text-muted-foreground", children: t("schedule.grid.slotHint") })
|
|
1201
|
+
] }),
|
|
1202
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1203
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: t("schedule.grid.name") }),
|
|
1204
|
+
/* @__PURE__ */ jsx(
|
|
1205
|
+
Input,
|
|
1206
|
+
{
|
|
1207
|
+
value: draft.name ?? "",
|
|
1208
|
+
onChange: (e) => patch({ name: e.target.value || null }),
|
|
1209
|
+
placeholder: t("schedule.grid.namePlaceholder")
|
|
1210
|
+
}
|
|
1211
|
+
)
|
|
1212
|
+
] })
|
|
1213
|
+
] }),
|
|
1214
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-7", children: WEEK_ORDER.map((dow) => {
|
|
1215
|
+
const periods = draft.days[dow] ?? [];
|
|
1216
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-muted/20 p-2", children: [
|
|
1217
|
+
/* @__PURE__ */ jsx("p", { className: "mb-2 text-center text-xs font-semibold", children: t(`schedule.daysFull.${DAY_KEYS[dow]}`) }),
|
|
1218
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-2", children: periods.map((p, i) => /* @__PURE__ */ jsx(
|
|
1219
|
+
PeriodBox,
|
|
1220
|
+
{
|
|
1221
|
+
period: p,
|
|
1222
|
+
resources,
|
|
1223
|
+
onChange: (pp) => patchDay(dow, periods.map((x, j) => j === i ? { ...x, ...pp } : x)),
|
|
1224
|
+
onRemove: () => patchDay(dow, periods.filter((_, j) => j !== i)),
|
|
1225
|
+
onCopyToAll: () => copyToAll(p)
|
|
1226
|
+
},
|
|
1227
|
+
p.id ?? `novo-${i}`
|
|
1228
|
+
)) }),
|
|
1229
|
+
/* @__PURE__ */ jsxs(
|
|
1230
|
+
"button",
|
|
1231
|
+
{
|
|
1232
|
+
type: "button",
|
|
1233
|
+
onClick: () => addPeriod(dow),
|
|
1234
|
+
className: "mt-2 flex w-full items-center justify-center gap-1 rounded-md border border-dashed py-1.5 text-xs text-muted-foreground transition-colors hover:border-primary hover:text-foreground",
|
|
1235
|
+
children: [
|
|
1236
|
+
/* @__PURE__ */ jsx(Plus, { className: "h-3 w-3" }),
|
|
1237
|
+
t("schedule.grid.add")
|
|
1238
|
+
]
|
|
1239
|
+
}
|
|
1240
|
+
)
|
|
1241
|
+
] }, dow);
|
|
1242
|
+
}) }),
|
|
1243
|
+
/* @__PURE__ */ jsxs("div", { className: "sticky bottom-0 z-10 -mx-1 flex flex-wrap items-center gap-2 border-t bg-background px-1 py-3", children: [
|
|
1244
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: () => setConfirm("end"), disabled: isNew, className: "gap-1.5", children: [
|
|
1245
|
+
/* @__PURE__ */ jsx(CalendarOff, { className: "h-3.5 w-3.5" }),
|
|
1246
|
+
t("schedule.grid.endValidity")
|
|
1247
|
+
] }),
|
|
1248
|
+
/* @__PURE__ */ jsxs(
|
|
1249
|
+
Button,
|
|
1250
|
+
{
|
|
1251
|
+
variant: "ghost",
|
|
1252
|
+
size: "sm",
|
|
1253
|
+
onClick: () => isNew ? removeGrid() : setConfirm("delete"),
|
|
1254
|
+
className: "gap-1.5 text-destructive hover:bg-destructive/10 hover:text-destructive",
|
|
1255
|
+
children: [
|
|
1256
|
+
/* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" }),
|
|
1257
|
+
t("schedule.grid.deleteGrid")
|
|
1258
|
+
]
|
|
1259
|
+
}
|
|
1260
|
+
),
|
|
1261
|
+
/* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-2", children: [
|
|
1262
|
+
dirty && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("schedule.grid.unsaved") }),
|
|
1263
|
+
/* @__PURE__ */ jsxs(Button, { size: "sm", onClick: save, disabled: saving || !dirty, className: "gap-1.5", children: [
|
|
1264
|
+
/* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" }),
|
|
1265
|
+
saving ? t("common.saving") : t("schedule.grid.save")
|
|
1266
|
+
] })
|
|
1267
|
+
] })
|
|
1268
|
+
] }),
|
|
1269
|
+
/* @__PURE__ */ jsx(Modal, { open: copyOpen, onOpenChange: (o) => {
|
|
1270
|
+
if (!o) setCopyOpen(false);
|
|
1271
|
+
}, children: /* @__PURE__ */ jsxs(ModalContent, { size: "sm", children: [
|
|
1272
|
+
/* @__PURE__ */ jsxs(ModalHeader, { children: [
|
|
1273
|
+
/* @__PURE__ */ jsx(ModalTitle, { children: "Copiar esta grade" }),
|
|
1274
|
+
/* @__PURE__ */ jsx(ModalDescription, { children: "A mesma semana vira uma grade NOVA para quem voc\xEA escolher. Quem j\xE1 tem grade vigente fica de fora: copiar por cima n\xE3o substitui a dela, soma \u2014 e a agenda passaria a desenhar as duas." })
|
|
1275
|
+
] }),
|
|
1276
|
+
/* @__PURE__ */ jsxs(ModalBody, { children: [
|
|
1277
|
+
/* @__PURE__ */ jsx(
|
|
1278
|
+
Input,
|
|
1279
|
+
{
|
|
1280
|
+
value: copyTerm,
|
|
1281
|
+
onChange: (e) => setCopyTerm(e.target.value),
|
|
1282
|
+
placeholder: "Buscar pessoa\u2026",
|
|
1283
|
+
className: "mb-2"
|
|
1284
|
+
}
|
|
1285
|
+
),
|
|
1286
|
+
people === null ? /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
1287
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-full" }),
|
|
1288
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-full" }),
|
|
1289
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-full" })
|
|
1290
|
+
] }) : people.length === 0 ? /* @__PURE__ */ jsx("p", { className: "py-6 text-center text-sm text-muted-foreground", children: "Nenhuma outra pessoa agend\xE1vel." }) : /* @__PURE__ */ jsx("ul", { className: "max-h-72 space-y-0.5 overflow-y-auto", children: people.filter((pe) => !copyTerm.trim() || pe.name.toLowerCase().includes(copyTerm.trim().toLowerCase())).map((pe) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 rounded-md px-2 py-1.5 text-sm " + (pe.hasGrid ? "opacity-50" : "cursor-pointer hover:bg-muted"), children: [
|
|
1291
|
+
/* @__PURE__ */ jsx(
|
|
1292
|
+
"input",
|
|
1293
|
+
{
|
|
1294
|
+
type: "checkbox",
|
|
1295
|
+
disabled: pe.hasGrid,
|
|
1296
|
+
checked: picked.includes(pe.id),
|
|
1297
|
+
onChange: () => setPicked((cur) => cur.includes(pe.id) ? cur.filter((x) => x !== pe.id) : [...cur, pe.id])
|
|
1298
|
+
}
|
|
1299
|
+
),
|
|
1300
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: pe.name }),
|
|
1301
|
+
pe.hasGrid && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[11px] text-muted-foreground", children: "j\xE1 tem grade" })
|
|
1302
|
+
] }) }, pe.id)) })
|
|
1303
|
+
] }),
|
|
1304
|
+
/* @__PURE__ */ jsxs(ModalFooter, { children: [
|
|
1305
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: () => setCopyOpen(false), children: t("common.cancel") }),
|
|
1306
|
+
/* @__PURE__ */ jsx(Button, { onClick: runCopy, disabled: picked.length === 0 || copying, children: copying ? "Copiando\u2026" : picked.length === 0 ? "Escolha ao menos uma pessoa" : `Copiar para ${picked.length}` })
|
|
1307
|
+
] })
|
|
1308
|
+
] }) }),
|
|
1309
|
+
/* @__PURE__ */ jsx(
|
|
1310
|
+
ConfirmDialog,
|
|
1311
|
+
{
|
|
1312
|
+
open: confirm === "end",
|
|
1313
|
+
title: t("schedule.grid.endValidity"),
|
|
1314
|
+
description: t("schedule.grid.confirmEnd"),
|
|
1315
|
+
confirmLabel: t("schedule.grid.endValidity"),
|
|
1316
|
+
onConfirm: endValidity,
|
|
1317
|
+
onCancel: () => setConfirm(null)
|
|
1318
|
+
}
|
|
1319
|
+
),
|
|
1320
|
+
/* @__PURE__ */ jsx(
|
|
1321
|
+
ConfirmDialog,
|
|
1322
|
+
{
|
|
1323
|
+
open: confirm === "delete",
|
|
1324
|
+
title: t("schedule.grid.deleteGrid"),
|
|
1325
|
+
description: t("schedule.grid.confirmDelete"),
|
|
1326
|
+
confirmLabel: t("schedule.grid.deleteGrid"),
|
|
1327
|
+
destructive: true,
|
|
1328
|
+
onConfirm: removeGrid,
|
|
1329
|
+
onCancel: () => setConfirm(null)
|
|
1330
|
+
}
|
|
1331
|
+
),
|
|
1332
|
+
/* @__PURE__ */ jsx(
|
|
1333
|
+
ConfirmDialog,
|
|
1334
|
+
{
|
|
1335
|
+
open: pendingSelect !== null,
|
|
1336
|
+
title: t("schedule.grid.discardTitle"),
|
|
1337
|
+
description: t("schedule.grid.discardChanges"),
|
|
1338
|
+
confirmLabel: t("schedule.grid.discard"),
|
|
1339
|
+
destructive: true,
|
|
1340
|
+
onConfirm: () => {
|
|
1341
|
+
if (pendingSelect) {
|
|
1342
|
+
setDraft(cloneGrid(pendingSelect));
|
|
1343
|
+
setDirty(false);
|
|
1344
|
+
}
|
|
1345
|
+
setPendingSelect(null);
|
|
1346
|
+
},
|
|
1347
|
+
onCancel: () => setPendingSelect(null)
|
|
1348
|
+
}
|
|
1349
|
+
)
|
|
1350
|
+
] }) });
|
|
1351
|
+
}
|
|
1352
|
+
function schedulesToExceptions(schedules) {
|
|
1353
|
+
const map = /* @__PURE__ */ new Map();
|
|
1354
|
+
for (const s of schedules) {
|
|
1355
|
+
if (!s.specificDate) continue;
|
|
1356
|
+
const meta = s.metadata ?? {};
|
|
1357
|
+
const rangeStart = meta.rangeStart ?? s.specificDate;
|
|
1358
|
+
const rangeEnd = meta.rangeEnd ?? s.specificDate;
|
|
1359
|
+
const key = `${rangeStart}__${rangeEnd}`;
|
|
1360
|
+
const exType = meta.exceptionType === "available" ? "available" : "unavailable";
|
|
1361
|
+
const notes = meta.notes ?? "";
|
|
1362
|
+
const existing = map.get(key);
|
|
1363
|
+
if (!existing) {
|
|
1364
|
+
map.set(key, {
|
|
1365
|
+
startDate: rangeStart,
|
|
1366
|
+
endDate: rangeEnd,
|
|
1367
|
+
type: exType,
|
|
1368
|
+
periods: s.isActive ? [{ startsAt: s.startsAt, endsAt: s.endsAt }] : [],
|
|
1369
|
+
notes
|
|
1370
|
+
});
|
|
1371
|
+
} else if (s.isActive && s.specificDate === rangeStart) {
|
|
1372
|
+
existing.periods.push({ startsAt: s.startsAt, endsAt: s.endsAt });
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return Array.from(map.values()).sort((a, b) => a.startDate.localeCompare(b.startDate));
|
|
1376
|
+
}
|
|
1377
|
+
function formatDate(dateStr) {
|
|
1378
|
+
const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
|
|
1379
|
+
return d.toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" });
|
|
1380
|
+
}
|
|
1381
|
+
function ScheduleDateExceptions({ assigneeId, onChanged }) {
|
|
1382
|
+
const t = useTranslation();
|
|
1383
|
+
const [exceptions, setExceptions] = useState(null);
|
|
1384
|
+
const [showAdd, setShowAdd] = useState(false);
|
|
1385
|
+
const [startDate, setStartDate] = useState("");
|
|
1386
|
+
const [endDate, setEndDate] = useState("");
|
|
1387
|
+
const [type, setType] = useState("unavailable");
|
|
1388
|
+
const [periods, setPeriods] = useState([{ startsAt: "09:00", endsAt: "17:00" }]);
|
|
1389
|
+
const [notes, setNotes] = useState("");
|
|
1390
|
+
const [saving, setSaving] = useState(false);
|
|
1391
|
+
const load = useCallback(async () => {
|
|
1392
|
+
try {
|
|
1393
|
+
setExceptions(schedulesToExceptions(await getSchedules(assigneeId)));
|
|
1394
|
+
} catch {
|
|
1395
|
+
setExceptions([]);
|
|
1396
|
+
toast.error(t("schedule.editor.failedToLoad"));
|
|
1397
|
+
}
|
|
1398
|
+
}, [assigneeId, t]);
|
|
1399
|
+
useEffect(() => {
|
|
1400
|
+
void load();
|
|
1401
|
+
}, [load]);
|
|
1402
|
+
async function add() {
|
|
1403
|
+
if (!startDate) {
|
|
1404
|
+
toast.error(t("schedule.editor.selectStartDate"));
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
const end = endDate || startDate;
|
|
1408
|
+
if (end < startDate) {
|
|
1409
|
+
toast.error(t("schedule.editor.endDateAfterStart"));
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
setSaving(true);
|
|
1413
|
+
try {
|
|
1414
|
+
await saveException({
|
|
1415
|
+
assigneeId,
|
|
1416
|
+
startDate,
|
|
1417
|
+
endDate: end,
|
|
1418
|
+
type,
|
|
1419
|
+
periods: type === "unavailable" ? [] : periods,
|
|
1420
|
+
notes: notes || void 0
|
|
1421
|
+
});
|
|
1422
|
+
toast.success(t("schedule.editor.exceptionAdded"));
|
|
1423
|
+
setShowAdd(false);
|
|
1424
|
+
setStartDate("");
|
|
1425
|
+
setEndDate("");
|
|
1426
|
+
setType("unavailable");
|
|
1427
|
+
setPeriods([{ startsAt: "09:00", endsAt: "17:00" }]);
|
|
1428
|
+
setNotes("");
|
|
1429
|
+
onChanged?.();
|
|
1430
|
+
await load();
|
|
1431
|
+
} catch (err) {
|
|
1432
|
+
toast.error(t("schedule.editor.failedToSaveException"), { description: err?.message });
|
|
1433
|
+
}
|
|
1434
|
+
setSaving(false);
|
|
1435
|
+
}
|
|
1436
|
+
async function remove(from, to) {
|
|
1437
|
+
try {
|
|
1438
|
+
await deleteException(assigneeId, from, to);
|
|
1439
|
+
toast.success(t("schedule.editor.exceptionRemoved"));
|
|
1440
|
+
onChanged?.();
|
|
1441
|
+
await load();
|
|
1442
|
+
} catch (err) {
|
|
1443
|
+
toast.error(t("schedule.editor.failedToDelete"), { description: err?.message });
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
return /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 300, children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
1447
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
1448
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1449
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: t("schedule.editor.exceptionsTitle") }),
|
|
1450
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("schedule.editor.exceptionsDescription") })
|
|
1451
|
+
] }),
|
|
1452
|
+
/* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", onClick: () => setShowAdd(true), className: "gap-1.5", children: [
|
|
1453
|
+
/* @__PURE__ */ jsx(CalendarPlus, { className: "h-4 w-4" }),
|
|
1454
|
+
t("schedule.editor.addException")
|
|
1455
|
+
] })
|
|
1456
|
+
] }),
|
|
1457
|
+
/* @__PURE__ */ jsx(Modal, { open: showAdd, onOpenChange: setShowAdd, children: /* @__PURE__ */ jsxs(ModalContent, { size: "md", children: [
|
|
1458
|
+
/* @__PURE__ */ jsxs(ModalHeader, { children: [
|
|
1459
|
+
/* @__PURE__ */ jsx(ModalTitle, { children: t("schedule.editor.addException") }),
|
|
1460
|
+
/* @__PURE__ */ jsx(ModalDescription, { children: t("schedule.editor.exceptionsDescription") })
|
|
1461
|
+
] }),
|
|
1462
|
+
/* @__PURE__ */ jsxs(ModalBody, { className: "space-y-5", children: [
|
|
1463
|
+
/* @__PURE__ */ jsxs("div", { className: "fz-fields fz-fields-2", children: [
|
|
1464
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1465
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1.5 block text-xs font-medium text-muted-foreground", children: t("schedule.editor.startDate") }),
|
|
1466
|
+
/* @__PURE__ */ jsx(DatePicker, { value: startDate, onChange: (v) => {
|
|
1467
|
+
setStartDate(v);
|
|
1468
|
+
if (!endDate || v > endDate) setEndDate(v);
|
|
1469
|
+
} })
|
|
1470
|
+
] }),
|
|
1471
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1472
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1.5 block text-xs font-medium text-muted-foreground", children: t("schedule.editor.endDate") }),
|
|
1473
|
+
/* @__PURE__ */ jsx(DatePicker, { value: endDate || startDate, onChange: setEndDate })
|
|
1474
|
+
] })
|
|
1475
|
+
] }),
|
|
1476
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1477
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1.5 block text-xs font-medium text-muted-foreground", children: t("schedule.editor.type") }),
|
|
1478
|
+
/* @__PURE__ */ jsx(Tabs, { value: type, onValueChange: (v) => setType(v), children: /* @__PURE__ */ jsxs(TabsList, { className: "h-9 w-full", children: [
|
|
1479
|
+
/* @__PURE__ */ jsxs(TabsTrigger, { value: "unavailable", className: "flex-1 gap-1.5 text-xs", children: [
|
|
1480
|
+
/* @__PURE__ */ jsx(CalendarOff, { className: "h-3.5 w-3.5" }),
|
|
1481
|
+
t("schedule.editor.unavailable")
|
|
1482
|
+
] }),
|
|
1483
|
+
/* @__PURE__ */ jsxs(TabsTrigger, { value: "available", className: "flex-1 gap-1.5 text-xs", children: [
|
|
1484
|
+
/* @__PURE__ */ jsx(Clock, { className: "h-3.5 w-3.5" }),
|
|
1485
|
+
t("schedule.editor.customHours")
|
|
1486
|
+
] })
|
|
1487
|
+
] }) })
|
|
1488
|
+
] }),
|
|
1489
|
+
type === "available" && /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
1490
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("schedule.editor.workingHours") }),
|
|
1491
|
+
periods.map((period, idx) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1492
|
+
/* @__PURE__ */ jsx(
|
|
1493
|
+
TimePicker,
|
|
1494
|
+
{
|
|
1495
|
+
value: period.startsAt,
|
|
1496
|
+
interval: 30,
|
|
1497
|
+
className: "flex-1",
|
|
1498
|
+
onChange: (v) => setPeriods(periods.map((p, i) => i === idx ? { ...p, startsAt: v } : p))
|
|
1499
|
+
}
|
|
1500
|
+
),
|
|
1501
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: t("schedule.editor.to") }),
|
|
1502
|
+
/* @__PURE__ */ jsx(
|
|
1503
|
+
TimePicker,
|
|
1504
|
+
{
|
|
1505
|
+
value: period.endsAt,
|
|
1506
|
+
interval: 30,
|
|
1507
|
+
min: period.startsAt,
|
|
1508
|
+
className: "flex-1",
|
|
1509
|
+
onChange: (v) => setPeriods(periods.map((p, i) => i === idx ? { ...p, endsAt: v } : p))
|
|
1510
|
+
}
|
|
1511
|
+
),
|
|
1512
|
+
periods.length > 1 && /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
1513
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
1514
|
+
"button",
|
|
1515
|
+
{
|
|
1516
|
+
type: "button",
|
|
1517
|
+
"aria-label": t("schedule.editor.removeInterval"),
|
|
1518
|
+
onClick: () => setPeriods(periods.filter((_, i) => i !== idx)),
|
|
1519
|
+
className: "shrink-0 rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-destructive/5 hover:text-destructive",
|
|
1520
|
+
children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
|
|
1521
|
+
}
|
|
1522
|
+
) }),
|
|
1523
|
+
/* @__PURE__ */ jsx(TooltipContent, { children: t("schedule.editor.removeInterval") })
|
|
1524
|
+
] })
|
|
1525
|
+
] }, idx)),
|
|
1526
|
+
/* @__PURE__ */ jsxs(
|
|
1527
|
+
"button",
|
|
1528
|
+
{
|
|
1529
|
+
type: "button",
|
|
1530
|
+
className: "flex items-center gap-1 text-xs text-primary hover:underline",
|
|
1531
|
+
onClick: () => {
|
|
1532
|
+
const last = periods[periods.length - 1];
|
|
1533
|
+
setPeriods([...periods, { startsAt: last?.endsAt ?? "13:00", endsAt: "18:00" }]);
|
|
1534
|
+
},
|
|
1535
|
+
children: [
|
|
1536
|
+
/* @__PURE__ */ jsx(Plus, { className: "h-3 w-3" }),
|
|
1537
|
+
" ",
|
|
1538
|
+
t("schedule.editor.addTimeBlock")
|
|
1539
|
+
]
|
|
1540
|
+
}
|
|
1541
|
+
)
|
|
1542
|
+
] }),
|
|
1543
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1544
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1.5 block text-xs font-medium text-muted-foreground", children: t("schedule.editor.notes") }),
|
|
1545
|
+
/* @__PURE__ */ jsx(
|
|
1546
|
+
"input",
|
|
1547
|
+
{
|
|
1548
|
+
type: "text",
|
|
1549
|
+
value: notes,
|
|
1550
|
+
onChange: (e) => setNotes(e.target.value),
|
|
1551
|
+
placeholder: t("schedule.editor.notesPlaceholder"),
|
|
1552
|
+
className: "block w-full rounded-input border border-input bg-card px-3 py-2 text-sm shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] placeholder:text-muted-foreground/40 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1"
|
|
1553
|
+
}
|
|
1554
|
+
)
|
|
1555
|
+
] })
|
|
1556
|
+
] }),
|
|
1557
|
+
/* @__PURE__ */ jsxs(ModalFooter, { children: [
|
|
1558
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: () => setShowAdd(false), children: t("common.cancel") }),
|
|
1559
|
+
/* @__PURE__ */ jsxs(Button, { onClick: add, disabled: saving, className: "gap-1.5", children: [
|
|
1560
|
+
/* @__PURE__ */ jsx(Save, { className: "h-3.5 w-3.5" }),
|
|
1561
|
+
saving ? t("common.saving") : t("schedule.editor.saveException")
|
|
1562
|
+
] })
|
|
1563
|
+
] })
|
|
1564
|
+
] }) }),
|
|
1565
|
+
exceptions === null ? /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "space-y-2 p-4", children: [
|
|
1566
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-64" }),
|
|
1567
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-48" })
|
|
1568
|
+
] }) }) : exceptions.length > 0 ? /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, { className: "divide-y p-0", children: exceptions.map((ex) => {
|
|
1569
|
+
const label = ex.startDate !== ex.endDate ? `${formatDate(ex.startDate)} \u2014 ${formatDate(ex.endDate)}` : formatDate(ex.startDate);
|
|
1570
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-3", children: [
|
|
1571
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [
|
|
1572
|
+
/* @__PURE__ */ jsx("span", { className: `h-2.5 w-2.5 shrink-0 rounded-full ${ex.type === "unavailable" ? "bg-destructive" : "bg-success"}` }),
|
|
1573
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
1574
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
1575
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: label }),
|
|
1576
|
+
/* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "text-[10px]", children: ex.type === "unavailable" ? t("schedule.editor.unavailable") : t("schedule.editor.customHours") }),
|
|
1577
|
+
ex.type === "available" && ex.periods.length > 0 && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: ex.periods.map((p) => `${p.startsAt} \u2014 ${p.endsAt}`).join(", ") })
|
|
1578
|
+
] }),
|
|
1579
|
+
ex.notes && /* @__PURE__ */ jsx("p", { className: "mt-0.5 truncate text-xs text-muted-foreground", children: ex.notes })
|
|
1580
|
+
] })
|
|
1581
|
+
] }),
|
|
1582
|
+
/* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
1583
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
1584
|
+
"button",
|
|
1585
|
+
{
|
|
1586
|
+
type: "button",
|
|
1587
|
+
onClick: () => remove(ex.startDate, ex.endDate),
|
|
1588
|
+
"aria-label": t("schedule.editor.removeException"),
|
|
1589
|
+
className: "ml-2 shrink-0 rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-destructive/5 hover:text-destructive",
|
|
1590
|
+
children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
|
|
1591
|
+
}
|
|
1592
|
+
) }),
|
|
1593
|
+
/* @__PURE__ */ jsx(TooltipContent, { children: t("schedule.editor.removeException") })
|
|
1594
|
+
] })
|
|
1595
|
+
] }, ex.startDate + ex.endDate);
|
|
1596
|
+
}) }) }) : /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, { className: "flex items-center justify-center py-6 text-sm text-muted-foreground", children: t("schedule.editor.noExceptions") }) })
|
|
1597
|
+
] }) });
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// src/lib/bookable-service.ts
|
|
1601
|
+
var UNDEFINED_COLUMN = "42703";
|
|
1602
|
+
async function getPersonBookable(personId) {
|
|
1603
|
+
const core = getFayzSupabaseClient();
|
|
1604
|
+
const { data, error } = await core.from("people").select("is_bookable").eq("id", personId).maybeSingle();
|
|
1605
|
+
if (error) {
|
|
1606
|
+
if (error.code === UNDEFINED_COLUMN) return null;
|
|
1607
|
+
throw error;
|
|
1608
|
+
}
|
|
1609
|
+
return data?.is_bookable ?? null;
|
|
1610
|
+
}
|
|
1611
|
+
async function setPersonBookable(personId, isBookable) {
|
|
1612
|
+
const core = getFayzSupabaseClient();
|
|
1613
|
+
const { error } = await core.from("people").update({ is_bookable: isBookable }).eq("id", personId);
|
|
1614
|
+
if (error) throw error;
|
|
1615
|
+
}
|
|
1616
|
+
function BookableToggle({ personId }) {
|
|
1617
|
+
const t = useTranslation();
|
|
1618
|
+
const [value, setValue] = React4.useState(null);
|
|
1619
|
+
const [loading, setLoading] = React4.useState(true);
|
|
1620
|
+
const [saving, setSaving] = React4.useState(false);
|
|
1621
|
+
React4.useEffect(() => {
|
|
1622
|
+
let cancelled = false;
|
|
1623
|
+
setLoading(true);
|
|
1624
|
+
getPersonBookable(personId).then((v) => {
|
|
1625
|
+
if (!cancelled) setValue(v);
|
|
1626
|
+
}).catch(() => {
|
|
1627
|
+
if (!cancelled) setValue(null);
|
|
1628
|
+
}).finally(() => {
|
|
1629
|
+
if (!cancelled) setLoading(false);
|
|
1630
|
+
});
|
|
1631
|
+
return () => {
|
|
1632
|
+
cancelled = true;
|
|
1633
|
+
};
|
|
1634
|
+
}, [personId]);
|
|
1635
|
+
async function toggle(next) {
|
|
1636
|
+
const previous = value;
|
|
1637
|
+
setValue(next);
|
|
1638
|
+
setSaving(true);
|
|
1639
|
+
try {
|
|
1640
|
+
await setPersonBookable(personId, next);
|
|
1641
|
+
toast.success(next ? t("crud.archetype.bookable.on") : t("crud.archetype.bookable.off"));
|
|
1642
|
+
} catch {
|
|
1643
|
+
setValue(previous);
|
|
1644
|
+
toast.error(t("crud.archetype.bookable.saveFailed"));
|
|
1645
|
+
} finally {
|
|
1646
|
+
setSaving(false);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
if (loading) return /* @__PURE__ */ jsx(Skeleton, { className: "h-16 w-full" });
|
|
1650
|
+
if (value === null) return null;
|
|
1651
|
+
const Icon = value ? CalendarCheck : CalendarOff;
|
|
1652
|
+
return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "flex items-center justify-between gap-4 py-4", children: [
|
|
1653
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
|
|
1654
|
+
/* @__PURE__ */ jsx("div", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-muted", children: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 text-muted-foreground" }) }),
|
|
1655
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1656
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: t("crud.archetype.bookable.title") }),
|
|
1657
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mt-0.5 max-w-md", children: value ? t("crud.archetype.bookable.descriptionOn") : t("crud.archetype.bookable.descriptionOff") })
|
|
1658
|
+
] })
|
|
1659
|
+
] }),
|
|
1660
|
+
/* @__PURE__ */ jsx(Switch, { size: "md", checked: value, onCheckedChange: toggle, disabled: saving })
|
|
1661
|
+
] }) });
|
|
1662
|
+
}
|
|
1663
|
+
function ScheduleEditor({ assigneeId, onSaved, hideBookable }) {
|
|
1664
|
+
const t = useTranslation();
|
|
1665
|
+
const [tab, setTab] = useState("standard");
|
|
1666
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
1667
|
+
!hideBookable && /* @__PURE__ */ jsx(BookableToggle, { personId: assigneeId }),
|
|
1668
|
+
/* @__PURE__ */ jsxs(Tabs, { value: tab, onValueChange: (v) => setTab(v), className: "space-y-4", children: [
|
|
1669
|
+
/* @__PURE__ */ jsxs(TabsList, { children: [
|
|
1670
|
+
/* @__PURE__ */ jsx(TabsTrigger, { value: "standard", children: t("schedule.grid.standardTab") }),
|
|
1671
|
+
/* @__PURE__ */ jsx(TabsTrigger, { value: "exception", children: t("schedule.grid.exceptionTab") })
|
|
1672
|
+
] }),
|
|
1673
|
+
/* @__PURE__ */ jsx(TabsContent, { value: "standard", children: /* @__PURE__ */ jsx(ScheduleGridPanel, { assigneeId, kind: "standard", onSaved }) }),
|
|
1674
|
+
/* @__PURE__ */ jsxs(TabsContent, { value: "exception", className: "space-y-6", children: [
|
|
1675
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("schedule.grid.exceptionHint") }),
|
|
1676
|
+
/* @__PURE__ */ jsx(ScheduleGridPanel, { assigneeId, kind: "exception", onSaved }),
|
|
1677
|
+
/* @__PURE__ */ jsx("div", { className: "border-t pt-6", children: /* @__PURE__ */ jsx(ScheduleDateExceptions, { assigneeId, onChanged: onSaved }) })
|
|
1678
|
+
] })
|
|
1679
|
+
] })
|
|
1680
|
+
] });
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
export { ScheduleDateExceptions, ScheduleEditor, ScheduleGridPanel, createArchetypeLookup, deleteSchedule, getPersonBookable, getPersonLookup, getScheduleBlockConfig, getSchedules, mergeBlockSettings, parseBlockSettings, registerPersonLookup, replaceWeeklySchedules, saveSchedule, setPersonBookable, setScheduleBlockConfig, subscribeScheduleBlockConfig };
|
|
1684
|
+
//# sourceMappingURL=chunk-3SREEFHN.js.map
|
|
1685
|
+
//# sourceMappingURL=chunk-3SREEFHN.js.map
|