@fayz-ai/plugin-agenda 0.10.2 → 0.12.0
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/bridges/create-financial-bridge.d.ts.map +1 -1
- package/dist/components/AgendaMobileList.d.ts +10 -1
- package/dist/components/AgendaMobileList.d.ts.map +1 -1
- package/dist/components/AppointmentModal.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +619 -453
- package/dist/index.js.map +1 -1
- package/dist/lib/booking-events.d.ts.map +1 -1
- package/dist/lib/message-events.d.ts.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +7 -2
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/views/CalendarView.d.ts.map +1 -1
- package/package.json +7 -6
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createMockAgendaProvider, createSupabasePublicBookingProvider } from '.
|
|
|
2
2
|
export { createMockAgendaProvider } from './chunk-ISLJXP7O.js';
|
|
3
3
|
import React13, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
4
4
|
import { connectionStatus, createConnectionStore, getSupabaseClientOptional, getActiveTenantId, registerTranslations, registerMessageDefaults, createSafeDataProvider, connectorRuntimeToken, CONNECTOR_RUNTIME_TOKEN_HEADER, emitDataChanged, usePluginRuntimeOptional, useTranslation, emitDomainEvent, getCurrentLocale, useDataChanged, formatCurrency } from '@fayz-ai/core';
|
|
5
|
-
import { createPluginContext, fayzConnectorOAuth, setScheduleBlockConfig, getScheduleBlockConfig, PluginSettingsPanel, dedup, useTenantPluginSettings, SettingsGroup, ToggleRow, SelectRow, usePluginPrefs, PersonLink, useLimitGuard, CrudFormPage, CrudDetailPage, ContactPicker, invalidateLimit } from '@fayz-ai/admin';
|
|
5
|
+
import { createPluginContext, fayzConnectorOAuth, setScheduleBlockConfig, getScheduleBlockConfig, PluginSettingsPanel, dedup, useTenantPluginSettings, SettingsGroup, ToggleRow, SelectRow, usePluginPrefs, PersonLink, PageActions, useLimitGuard, useLookupCreate, RightRailPage, CrudFormPage, CrudDetailPage, ContactPicker, invalidateLimit } from '@fayz-ai/admin';
|
|
6
6
|
import FullCalendar from '@fullcalendar/react';
|
|
7
7
|
import dayGridPlugin from '@fullcalendar/daygrid';
|
|
8
8
|
import timeGridPlugin from '@fullcalendar/timegrid';
|
|
@@ -10,20 +10,29 @@ import resourceTimeGridPlugin from '@fullcalendar/resource-timegrid';
|
|
|
10
10
|
import interactionPlugin from '@fullcalendar/interaction';
|
|
11
11
|
import listPlugin from '@fullcalendar/list';
|
|
12
12
|
import ptBrLocale from '@fullcalendar/core/locales/pt-br';
|
|
13
|
-
import { Loader2, AlertCircle, RefreshCw, CalendarPlus, X, CheckCircle2, Plus, ChevronDown, ChevronLeft, ChevronRight,
|
|
13
|
+
import { Loader2, AlertCircle, RefreshCw, CalendarPlus, X, CheckCircle2, Plus, ChevronDown, Settings, ChevronLeft, ChevronRight, Pencil, Trash2, Clock, User, MapPin, FileText, AlertTriangle, Check, DollarSign, HandCoins, Ban, CheckSquare, Calendar, Briefcase, CircleDot, FileCheck, ArrowRightLeft, CreditCard, QrCode, Banknote, ExternalLink } from 'lucide-react';
|
|
14
14
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
15
15
|
import { createPortal } from 'react-dom';
|
|
16
|
-
import { toast, Button, Checkbox, Badge, Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
|
|
16
|
+
import { toast, Button, Checkbox, Badge, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, PageHeaderActions, ConfirmDialog, SearchCombobox, cn, TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@fayz-ai/ui';
|
|
17
17
|
import { createStore } from 'zustand/vanilla';
|
|
18
18
|
|
|
19
19
|
// src/lib/message-events.ts
|
|
20
20
|
var BOOKING_FIELDS = [
|
|
21
21
|
{ name: "customer_name", label: "Cliente", type: "string", example: "Maria Silva" },
|
|
22
|
+
// Para ONDE a mensagem vai — não é texto de mensagem nenhuma, é o endereço.
|
|
23
|
+
// A automação de WhatsApp lê este campo (`recipientField`, padrão
|
|
24
|
+
// 'customer_phone') e pula o disparo quando ele vem vazio: sem número não há
|
|
25
|
+
// envio, e um evento sem ele é um lembrete que nunca sai.
|
|
26
|
+
{ name: "customer_phone", label: "Telefone do cliente", type: "string", example: "(21) 99889-9889" },
|
|
22
27
|
{ name: "service_name", label: "Servi\xE7o", type: "string", example: "Corte + Escova" },
|
|
23
28
|
{ name: "professional_name", label: "Profissional", type: "string", example: "Camila Duarte" },
|
|
24
29
|
{ name: "booking_date", label: "Data", type: "date", example: "12/08/2026" },
|
|
25
30
|
{ name: "booking_time", label: "Hor\xE1rio", type: "string", example: "14:30" },
|
|
26
31
|
{ name: "location_name", label: "Local", type: "string", example: "Unidade Centro" },
|
|
32
|
+
// Not in bookingPayload on purpose: the browser has no reliable source for
|
|
33
|
+
// the tenant's public name, so the SENDER fills this slot (messaging-send
|
|
34
|
+
// reads the tenants row). The field stays declared because templates use it
|
|
35
|
+
// and the editor needs its placeholder.
|
|
27
36
|
{ name: "business_name", label: "Seu neg\xF3cio", type: "string", example: "Seu Neg\xF3cio" }
|
|
28
37
|
];
|
|
29
38
|
function buildAgendaDomainEvents() {
|
|
@@ -84,6 +93,10 @@ function buildAgendaDomainEvents() {
|
|
|
84
93
|
}
|
|
85
94
|
];
|
|
86
95
|
}
|
|
96
|
+
var CONFIRM_OR_CANCEL = [
|
|
97
|
+
{ id: "confirm", label: "Confirma", kind: "reply" },
|
|
98
|
+
{ id: "cancel", label: "Cancela", kind: "reply" }
|
|
99
|
+
];
|
|
87
100
|
function buildAgendaMessageDefaults() {
|
|
88
101
|
return [
|
|
89
102
|
{
|
|
@@ -94,7 +107,13 @@ function buildAgendaMessageDefaults() {
|
|
|
94
107
|
channels: ["whatsapp", "email"],
|
|
95
108
|
defaults: {
|
|
96
109
|
whatsapp: {
|
|
97
|
-
|
|
110
|
+
// Curto porque os botões dizem o resto: a linha "qualquer coisa é só
|
|
111
|
+
// chamar" virou Confirmar/Cancelar, que além de ser menos texto já
|
|
112
|
+
// atualiza o agendamento sozinho.
|
|
113
|
+
// Sem botões, de propósito (decisão do founder 2026-08-24): esta é a
|
|
114
|
+
// confirmação informativa de que a reserva EXISTE. Confirmar presença é
|
|
115
|
+
// trabalho do lembrete de véspera, onde a resposta ainda muda o dia.
|
|
116
|
+
body: "Oi {{customer_name}}! Tudo certo \u2014 seu hor\xE1rio foi reservado na nossa agenda \u2705\n\nServi\xE7o: *{{service_name}}*\nData: {{booking_date}} \xE0s {{booking_time}}\nProfissional: {{professional_name}}\n\nSe precisar ajustar alguma coisa, \xE9 s\xF3 responder esta mensagem.\n\nEquipe {{business_name}} agradece a prefer\xEAncia!"
|
|
98
117
|
},
|
|
99
118
|
email: {
|
|
100
119
|
subject: "Hor\xE1rio confirmado \u2014 {{booking_date}} \xE0s {{booking_time}}",
|
|
@@ -112,7 +131,10 @@ function buildAgendaMessageDefaults() {
|
|
|
112
131
|
channels: ["whatsapp", "email"],
|
|
113
132
|
defaults: {
|
|
114
133
|
whatsapp: {
|
|
115
|
-
|
|
134
|
+
// AQUI moram Confirma/Cancela: na véspera a resposta ainda muda o dia —
|
|
135
|
+
// libera o horário ou garante a presença.
|
|
136
|
+
body: "Oi {{customer_name}}! Passando para lembrar do seu hor\xE1rio amanh\xE3 \u{1F487}\n\nServi\xE7o: *{{service_name}}*\nData: {{booking_date}} \xE0s {{booking_time}}\nProfissional: {{professional_name}}\n\nConsegue vir? \xC9 s\xF3 tocar em Confirma ou Cancela aqui embaixo \u2014 e se precisar de outro hor\xE1rio, responde esta mensagem.\n\nEquipe {{business_name}} te espera!",
|
|
137
|
+
actions: CONFIRM_OR_CANCEL
|
|
116
138
|
},
|
|
117
139
|
email: {
|
|
118
140
|
subject: "Lembrete: seu hor\xE1rio \xE9 amanh\xE3, {{booking_time}}",
|
|
@@ -127,7 +149,10 @@ function buildAgendaMessageDefaults() {
|
|
|
127
149
|
channels: ["whatsapp", "email"],
|
|
128
150
|
defaults: {
|
|
129
151
|
whatsapp: {
|
|
130
|
-
body: "Oi {{customer_name}}! Seu hor\xE1rio foi remarcado \u{1F5D3}\n\
|
|
152
|
+
body: "Oi {{customer_name}}! Seu hor\xE1rio foi remarcado \u{1F5D3}\n\nServi\xE7o: *{{service_name}}*\nNova data: {{booking_date}} \xE0s {{booking_time}}\nProfissional: {{professional_name}}\n\nSe a nova data n\xE3o funcionar para voc\xEA, \xE9 s\xF3 responder esta mensagem ou usar os bot\xF5es abaixo.\n\nEquipe {{business_name}} agradece a compreens\xE3o!",
|
|
153
|
+
// A data mudou, então a confirmação vale de novo: é o momento em que
|
|
154
|
+
// mais gente some sem avisar.
|
|
155
|
+
actions: CONFIRM_OR_CANCEL
|
|
131
156
|
},
|
|
132
157
|
email: {
|
|
133
158
|
subject: "Seu hor\xE1rio mudou para {{booking_date}}, {{booking_time}}",
|
|
@@ -142,7 +167,9 @@ function buildAgendaMessageDefaults() {
|
|
|
142
167
|
channels: ["whatsapp", "email"],
|
|
143
168
|
defaults: {
|
|
144
169
|
whatsapp: {
|
|
145
|
-
|
|
170
|
+
// Sem botões, de propósito: não há o que confirmar num horário que já
|
|
171
|
+
// foi desmarcado, e um "Confirmar" aqui só poderia dar erro.
|
|
172
|
+
body: "Oi {{customer_name}}, seu hor\xE1rio de {{booking_date}} \xE0s {{booking_time}} foi cancelado.\n\nQuer que eu veja outra data? A equipe {{business_name}} fica \xE0 disposi\xE7\xE3o."
|
|
146
173
|
},
|
|
147
174
|
email: {
|
|
148
175
|
subject: "Hor\xE1rio de {{booking_date}} cancelado",
|
|
@@ -159,7 +186,7 @@ function buildAgendaMessageDefaults() {
|
|
|
159
186
|
channels: ["whatsapp"],
|
|
160
187
|
defaults: {
|
|
161
188
|
whatsapp: {
|
|
162
|
-
body: "Oi {{customer_name}}! Foi \xF3timo te atender hoje \u{1F49B}\n\nComo foi a experi\xEAncia com {{professional_name}}? Sua opini\xE3o ajuda demais
|
|
189
|
+
body: "Oi {{customer_name}}! Foi \xF3timo te atender hoje \u{1F49B}\n\nComo foi a experi\xEAncia com {{professional_name}}? Sua opini\xE3o ajuda demais a equipe {{business_name}} a melhorar sempre."
|
|
163
190
|
}
|
|
164
191
|
}
|
|
165
192
|
},
|
|
@@ -169,7 +196,7 @@ function buildAgendaMessageDefaults() {
|
|
|
169
196
|
channels: ["whatsapp"],
|
|
170
197
|
defaults: {
|
|
171
198
|
whatsapp: {
|
|
172
|
-
body: "Oi {{customer_name}}, senti sua falta no hor\xE1rio de {{booking_date}} \u{1F614}\n\nAconteceu alguma coisa? Se quiser, j\xE1 deixo outro hor\xE1rio separado para voc\xEA
|
|
199
|
+
body: "Oi {{customer_name}}, senti sua falta no hor\xE1rio de {{booking_date}} \u{1F614}\n\nAconteceu alguma coisa? Se quiser, j\xE1 deixo outro hor\xE1rio separado para voc\xEA aqui na {{business_name}}, \xE9 s\xF3 responder."
|
|
173
200
|
}
|
|
174
201
|
}
|
|
175
202
|
}
|
|
@@ -962,7 +989,7 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
|
|
|
962
989
|
children: /* @__PURE__ */ jsxs(
|
|
963
990
|
"div",
|
|
964
991
|
{
|
|
965
|
-
className: "w-full max-w-md rounded-
|
|
992
|
+
className: "w-full max-w-md rounded-modal border bg-card shadow-2xl mx-4 transition-all duration-200 max-h-[90vh] overflow-y-auto",
|
|
966
993
|
style: { opacity: visible ? 1 : 0, transform: visible ? "scale(1) translateY(0)" : "scale(0.95) translateY(8px)" },
|
|
967
994
|
onClick: (e) => e.stopPropagation(),
|
|
968
995
|
children: [
|
|
@@ -1269,6 +1296,17 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1269
1296
|
const [cancellationNotes, setCancellationNotes] = useState("");
|
|
1270
1297
|
const [cancellationReasonsLoading, setCancellationReasonsLoading] = useState(false);
|
|
1271
1298
|
const activeType = config.bookingTypes.find((bt) => bt.id === bookingType) ?? config.bookingTypes[0];
|
|
1299
|
+
const requestKey = [
|
|
1300
|
+
mode,
|
|
1301
|
+
bookingId ?? "",
|
|
1302
|
+
initialTab ?? "",
|
|
1303
|
+
prefill?.startsAt ?? "",
|
|
1304
|
+
prefill?.endsAt ?? "",
|
|
1305
|
+
prefill?.professionalId ?? "",
|
|
1306
|
+
prefill?.locationId ?? "",
|
|
1307
|
+
prefill?.clientId ?? "",
|
|
1308
|
+
prefill?.kind ?? ""
|
|
1309
|
+
].join("|");
|
|
1272
1310
|
useEffect(() => {
|
|
1273
1311
|
if (!open) return;
|
|
1274
1312
|
setClientId(prefill?.clientId ?? "");
|
|
@@ -1312,7 +1350,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1312
1350
|
}
|
|
1313
1351
|
return "09:00";
|
|
1314
1352
|
});
|
|
1315
|
-
}, [open]);
|
|
1353
|
+
}, [open, requestKey]);
|
|
1316
1354
|
useEffect(() => {
|
|
1317
1355
|
if (!open || mode !== "edit" || !bookingId) return;
|
|
1318
1356
|
setLoading(true);
|
|
@@ -1369,7 +1407,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1369
1407
|
}).finally(() => setPaymentStatusLoading(false));
|
|
1370
1408
|
}
|
|
1371
1409
|
}).catch(() => setLoading(false));
|
|
1372
|
-
}, [open,
|
|
1410
|
+
}, [open, requestKey]);
|
|
1373
1411
|
useEffect(() => {
|
|
1374
1412
|
if (modalTab !== "client" || !clientId || !config.contactLookup || clientData) return;
|
|
1375
1413
|
setClientDataLoading(true);
|
|
@@ -1444,6 +1482,21 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1444
1482
|
function removeService(idx) {
|
|
1445
1483
|
setServices((p) => p.filter((_, i) => i !== idx));
|
|
1446
1484
|
}
|
|
1485
|
+
const serviceCreate = useLookupCreate(
|
|
1486
|
+
config.serviceLookup,
|
|
1487
|
+
(created) => {
|
|
1488
|
+
const duration = created.data?.duration_minutes || 60;
|
|
1489
|
+
setServices((p) => [...p, {
|
|
1490
|
+
serviceId: created.id,
|
|
1491
|
+
name: created.label,
|
|
1492
|
+
durationMinutes: duration,
|
|
1493
|
+
price: created.price ?? 0
|
|
1494
|
+
}]);
|
|
1495
|
+
setServiceSearch("");
|
|
1496
|
+
setServiceOptions([]);
|
|
1497
|
+
},
|
|
1498
|
+
{ variant: "inline" }
|
|
1499
|
+
);
|
|
1447
1500
|
async function handleSubmit(e) {
|
|
1448
1501
|
e.preventDefault();
|
|
1449
1502
|
if (!canSubmit) return;
|
|
@@ -1513,431 +1566,442 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1513
1566
|
const isDirty = mode === "create" || formSnapshot !== initialSnapshot.current;
|
|
1514
1567
|
const canSubmit = (!activeType.fields.title || title.trim()) && (!activeType.requiresClient || clientId) && (!activeType.fields.professional || professionalId) && (!activeType.requiresServices || services.length > 0) && (!needsCancellationReason || !!cancellationReasonId) && (!needsCancellationNotes || !!cancellationNotes.trim()) && !conflict && !saving;
|
|
1515
1568
|
const showTabs = config.bookingTypes.length > 1;
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
value: date,
|
|
1571
|
-
autoFocus: true,
|
|
1572
|
-
onChange: (e) => {
|
|
1573
|
-
setDate(e.target.value);
|
|
1574
|
-
setEditingDate(false);
|
|
1575
|
-
},
|
|
1576
|
-
onBlur: () => setEditingDate(false),
|
|
1577
|
-
className: "bg-transparent text-sm font-medium focus:outline-none border-b border-foreground w-auto shrink-0"
|
|
1578
|
-
}
|
|
1579
|
-
) : /* @__PURE__ */ jsx(
|
|
1580
|
-
"button",
|
|
1581
|
-
{
|
|
1582
|
-
type: "button",
|
|
1583
|
-
onClick: () => setEditingDate(true),
|
|
1584
|
-
className: "px-0.5 py-0.5 font-medium border-b border-transparent hover:border-foreground transition-colors shrink-0",
|
|
1585
|
-
children: formatCompactDate(date, locale)
|
|
1586
|
-
}
|
|
1587
|
-
),
|
|
1588
|
-
editingTime ? /* @__PURE__ */ jsx(
|
|
1589
|
-
"input",
|
|
1590
|
-
{
|
|
1591
|
-
type: "time",
|
|
1592
|
-
value: startTime,
|
|
1593
|
-
autoFocus: true,
|
|
1594
|
-
onChange: (e) => {
|
|
1595
|
-
if (e.target.value) {
|
|
1596
|
-
setStartTime(e.target.value);
|
|
1597
|
-
setEditingTime(false);
|
|
1598
|
-
}
|
|
1599
|
-
},
|
|
1600
|
-
onBlur: () => setEditingTime(false),
|
|
1601
|
-
className: "bg-transparent text-xs text-muted-foreground focus:outline-none border-b border-foreground w-auto shrink-0"
|
|
1602
|
-
}
|
|
1603
|
-
) : /* @__PURE__ */ jsx(
|
|
1604
|
-
"button",
|
|
1605
|
-
{
|
|
1606
|
-
type: "button",
|
|
1607
|
-
onClick: () => setEditingTime(true),
|
|
1608
|
-
className: "px-0.5 py-0.5 text-xs text-muted-foreground border-b border-transparent hover:border-foreground transition-colors shrink-0",
|
|
1609
|
-
children: startTime
|
|
1610
|
-
}
|
|
1611
|
-
),
|
|
1612
|
-
/* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground/60", children: "\u2014" }),
|
|
1613
|
-
/* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground/60", children: endTime }),
|
|
1614
|
-
activeType.fields.professional && /* @__PURE__ */ jsx("div", { className: "ml-auto shrink-0", children: /* @__PURE__ */ jsx(ProfessionalSelect, { value: professionalId, onChange: setProfessionalId, professionals }) })
|
|
1615
|
-
] })
|
|
1616
|
-
] }),
|
|
1617
|
-
conflict && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-destructive/10 px-3 py-1.5 text-xs text-destructive", children: [
|
|
1618
|
-
/* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5 shrink-0" }),
|
|
1619
|
-
t2("agenda.appointment.conflict")
|
|
1620
|
-
] })
|
|
1621
|
-
] })
|
|
1622
|
-
] }),
|
|
1623
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-col flex-1 min-h-0 overflow-hidden", children: [
|
|
1624
|
-
modalTab === "client" && clientId && config.clientEntityDef && /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto px-5 py-4", children: clientDataLoading ? /* @__PURE__ */ jsx("div", { className: "space-y-3 py-2", children: [...Array(4)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-8 rounded-lg bg-muted animate-pulse" }, i)) }) : clientData ? clientEditing ? /* @__PURE__ */ jsx(
|
|
1625
|
-
CrudFormPage,
|
|
1626
|
-
{
|
|
1627
|
-
embedded: true,
|
|
1628
|
-
entityDef: config.clientEntityDef,
|
|
1629
|
-
mode: "edit",
|
|
1630
|
-
initialData: clientData,
|
|
1631
|
-
namePlural: "",
|
|
1632
|
-
onCancel: () => setClientEditing(false),
|
|
1633
|
-
onSubmit: async (data) => {
|
|
1634
|
-
try {
|
|
1635
|
-
const { getSupabaseClientOptional: getSupabaseClientOptional4 } = await import('@fayz-ai/core');
|
|
1636
|
-
const supabase = getSupabaseClientOptional4();
|
|
1637
|
-
if (!supabase) return;
|
|
1638
|
-
const dbData = {};
|
|
1639
|
-
for (const [k, v] of Object.entries(data)) {
|
|
1640
|
-
dbData[k.replace(/[A-Z]/g, (m) => "_" + m.toLowerCase())] = v;
|
|
1641
|
-
}
|
|
1642
|
-
await supabase.from("people").update(dbData).eq("id", clientId);
|
|
1643
|
-
setClientData({ ...clientData, ...data });
|
|
1644
|
-
setClientEditing(false);
|
|
1645
|
-
} catch {
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
) : /* @__PURE__ */ jsx(
|
|
1650
|
-
CrudDetailPage,
|
|
1651
|
-
{
|
|
1652
|
-
embedded: true,
|
|
1653
|
-
entityDef: config.clientEntityDef,
|
|
1654
|
-
item: clientData,
|
|
1655
|
-
namePlural: "",
|
|
1656
|
-
basePath: "",
|
|
1657
|
-
onBack: () => setModalTab("appointment"),
|
|
1658
|
-
onEdit: () => setClientEditing(true)
|
|
1659
|
-
}
|
|
1660
|
-
) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground py-4 text-center", children: t2("agenda.appointment.noClientData") }) }),
|
|
1661
|
-
modalTab === "financial" && editOrderId && config.financialBridge && /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto", children: /* @__PURE__ */ jsx(
|
|
1662
|
-
BookingPaymentPanel,
|
|
1663
|
-
{
|
|
1664
|
-
orderId: editOrderId,
|
|
1665
|
-
orderTotal: editOrderTotal,
|
|
1666
|
-
services: services.map((s) => ({ name: s.name, price: s.price, durationMinutes: s.durationMinutes })),
|
|
1667
|
-
formatCurrency: (v) => fmtCurrency(v, currency.locale, currency.code),
|
|
1668
|
-
inline: true,
|
|
1669
|
-
onClose: () => setModalTab("appointment"),
|
|
1670
|
-
onPaymentChange: () => {
|
|
1671
|
-
config.financialBridge.getPaymentDetail(editOrderId).then((d) => {
|
|
1672
|
-
if (d) {
|
|
1673
|
-
setEditPaymentStatus(d.status);
|
|
1674
|
-
setEditOrderTotal(d.totalAmount);
|
|
1675
|
-
}
|
|
1676
|
-
}).catch(() => {
|
|
1677
|
-
});
|
|
1678
|
-
if (agendaDateRange.start) fetchBookings(agendaDateRange);
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
) }),
|
|
1682
|
-
/* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col flex-1 min-h-0 overflow-hidden", style: { display: modalTab === "appointment" ? void 0 : "none" }, children: [
|
|
1683
|
-
/* @__PURE__ */ jsxs("div", { className: "border-t fayz-glass-divider px-5 py-3 space-y-1 flex-1 overflow-y-auto", children: [
|
|
1684
|
-
loading && /* @__PURE__ */ jsx("div", { className: "space-y-2 py-2", children: [...Array(4)].map((_, i) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1.5", children: [
|
|
1685
|
-
/* @__PURE__ */ jsx("div", { className: "h-4 w-4 rounded bg-muted animate-pulse shrink-0" }),
|
|
1686
|
-
/* @__PURE__ */ jsx("div", { className: "h-8 flex-1 rounded-lg bg-muted animate-pulse" })
|
|
1687
|
-
] }, i)) }),
|
|
1688
|
-
!loading && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1689
|
-
activeType.fields.title && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1", children: [
|
|
1690
|
-
/* @__PURE__ */ jsx(Calendar, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
1691
|
-
/* @__PURE__ */ jsx(
|
|
1569
|
+
const railTitle = mode === "create" ? t2("agenda.appointment.new") : title.trim() || clientSearch || t2("agenda.appointment.edit");
|
|
1570
|
+
return (
|
|
1571
|
+
// A booking is written next to the calendar it belongs to, not on top of
|
|
1572
|
+
// it: the dialog used to dim the week you were reading while you filled in
|
|
1573
|
+
// a slot from it, and a second panel opening from the same edge as the
|
|
1574
|
+
// assistant meant two vocabularies for one side of the screen.
|
|
1575
|
+
/* @__PURE__ */ jsxs(RightRailPage, { title: railTitle, open, onClose, width: "max-w-lg", children: [
|
|
1576
|
+
/* @__PURE__ */ jsxs("div", { className: "px-5 pt-3 pb-3 shrink-0 space-y-3", children: [
|
|
1577
|
+
showTabs && mode === "create" ? /* @__PURE__ */ jsx("div", { className: "flex gap-1 border-b fayz-glass-divider", children: config.bookingTypes.map((bt) => {
|
|
1578
|
+
const label = t2(`agenda.bookingType.${bt.id}`) || bt.label;
|
|
1579
|
+
const isActive = bookingType === bt.id;
|
|
1580
|
+
const BtIcon = BOOKING_TYPE_ICONS[bt.icon] ?? Calendar;
|
|
1581
|
+
return /* @__PURE__ */ jsxs(
|
|
1582
|
+
"button",
|
|
1583
|
+
{
|
|
1584
|
+
type: "button",
|
|
1585
|
+
onClick: () => setBookingType(bt.id),
|
|
1586
|
+
className: `relative flex items-center gap-1.5 pb-2 px-2 text-xs font-medium transition-colors ${isActive ? "text-foreground" : "text-muted-foreground hover:text-foreground"}`,
|
|
1587
|
+
children: [
|
|
1588
|
+
/* @__PURE__ */ jsx(BtIcon, { className: "h-3.5 w-3.5" }),
|
|
1589
|
+
label,
|
|
1590
|
+
isActive && /* @__PURE__ */ jsx("span", { className: "absolute bottom-0 left-0 right-0 h-0.5 rounded-full bg-foreground" })
|
|
1591
|
+
]
|
|
1592
|
+
},
|
|
1593
|
+
bt.id
|
|
1594
|
+
);
|
|
1595
|
+
}) }) : mode === "edit" && (config.clientEntityDef && clientId || config.modules.financial && editOrderId ? /* @__PURE__ */ jsx("div", { className: "flex gap-1 border-b fayz-glass-divider", children: [
|
|
1596
|
+
{ id: "appointment", label: t2(`agenda.bookingType.${bookingType}`) || activeType.label, icon: Calendar, show: true },
|
|
1597
|
+
{ id: "client", label: t2("agenda.appointment.clientTab"), icon: User, show: !!config.clientEntityDef && !!clientId },
|
|
1598
|
+
{ id: "financial", label: t2("agenda.payment.title"), icon: DollarSign, show: config.modules.financial && !!editOrderId }
|
|
1599
|
+
].filter((tab) => tab.show).map((tab) => {
|
|
1600
|
+
const isActive = modalTab === tab.id;
|
|
1601
|
+
return /* @__PURE__ */ jsxs(
|
|
1602
|
+
"button",
|
|
1603
|
+
{
|
|
1604
|
+
type: "button",
|
|
1605
|
+
onClick: () => setModalTab(tab.id),
|
|
1606
|
+
className: `relative flex items-center gap-1.5 pb-2 px-2 text-xs font-medium transition-colors ${isActive ? "text-foreground" : "text-muted-foreground hover:text-foreground"}`,
|
|
1607
|
+
children: [
|
|
1608
|
+
/* @__PURE__ */ jsx(tab.icon, { className: "h-3.5 w-3.5" }),
|
|
1609
|
+
tab.label,
|
|
1610
|
+
tab.id === "financial" && editPaymentStatus === "paid" && /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-success" }),
|
|
1611
|
+
tab.id === "financial" && editPaymentStatus !== "paid" && (editPaymentStatus !== "none" || totalPrice > 0 && date <= (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) && /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-warning" }),
|
|
1612
|
+
isActive && /* @__PURE__ */ jsx("span", { className: "absolute bottom-0 left-0 right-0 h-0.5 rounded-full bg-foreground" })
|
|
1613
|
+
]
|
|
1614
|
+
},
|
|
1615
|
+
tab.id
|
|
1616
|
+
);
|
|
1617
|
+
}) }) : null),
|
|
1618
|
+
modalTab === "appointment" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1619
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1620
|
+
/* @__PURE__ */ jsx(Clock, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
1621
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-1.5 gap-y-1 text-sm flex-1 min-w-0", children: [
|
|
1622
|
+
editingDate ? /* @__PURE__ */ jsx(
|
|
1692
1623
|
"input",
|
|
1693
1624
|
{
|
|
1694
|
-
type: "
|
|
1695
|
-
value:
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1625
|
+
type: "date",
|
|
1626
|
+
value: date,
|
|
1627
|
+
autoFocus: true,
|
|
1628
|
+
onChange: (e) => {
|
|
1629
|
+
setDate(e.target.value);
|
|
1630
|
+
setEditingDate(false);
|
|
1631
|
+
},
|
|
1632
|
+
onBlur: () => setEditingDate(false),
|
|
1633
|
+
className: "bg-transparent text-sm font-medium focus:outline-none border-b border-foreground w-auto shrink-0"
|
|
1700
1634
|
}
|
|
1701
|
-
)
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1635
|
+
) : /* @__PURE__ */ jsx(
|
|
1636
|
+
"button",
|
|
1637
|
+
{
|
|
1638
|
+
type: "button",
|
|
1639
|
+
onClick: () => setEditingDate(true),
|
|
1640
|
+
className: "px-0.5 py-0.5 font-medium border-b border-transparent hover:border-foreground transition-colors shrink-0",
|
|
1641
|
+
children: formatCompactDate(date, locale)
|
|
1642
|
+
}
|
|
1643
|
+
),
|
|
1644
|
+
editingTime ? /* @__PURE__ */ jsx(
|
|
1645
|
+
"input",
|
|
1707
1646
|
{
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1647
|
+
type: "time",
|
|
1648
|
+
value: startTime,
|
|
1649
|
+
autoFocus: true,
|
|
1650
|
+
onChange: (e) => {
|
|
1651
|
+
if (e.target.value) {
|
|
1652
|
+
setStartTime(e.target.value);
|
|
1653
|
+
setEditingTime(false);
|
|
1654
|
+
}
|
|
1713
1655
|
},
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
lookup: config.contactLookup,
|
|
1717
|
-
clearable: !isPaidBooking,
|
|
1718
|
-
variant: "inline",
|
|
1719
|
-
placeholder: t2("agenda.appointment.searchClient"),
|
|
1720
|
-
createLabel: t2("agenda.appointment.newClient")
|
|
1656
|
+
onBlur: () => setEditingTime(false),
|
|
1657
|
+
className: "bg-transparent text-xs text-muted-foreground focus:outline-none border-b border-foreground w-auto shrink-0"
|
|
1721
1658
|
}
|
|
1722
|
-
)
|
|
1723
|
-
|
|
1724
|
-
activeType.fields.location && config.modules.locationSelection && (storeLocations.length > 0 || config.locations.length > 0) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1", children: [
|
|
1725
|
-
/* @__PURE__ */ jsx(MapPin, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
1726
|
-
/* @__PURE__ */ jsx(
|
|
1727
|
-
LocationSelect,
|
|
1659
|
+
) : /* @__PURE__ */ jsx(
|
|
1660
|
+
"button",
|
|
1728
1661
|
{
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1662
|
+
type: "button",
|
|
1663
|
+
onClick: () => setEditingTime(true),
|
|
1664
|
+
className: "px-0.5 py-0.5 text-xs text-muted-foreground border-b border-transparent hover:border-foreground transition-colors shrink-0",
|
|
1665
|
+
children: startTime
|
|
1732
1666
|
}
|
|
1733
|
-
)
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
/* @__PURE__ */ jsx(
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1667
|
+
),
|
|
1668
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground/60", children: "\u2014" }),
|
|
1669
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground/60", children: endTime }),
|
|
1670
|
+
activeType.fields.professional && /* @__PURE__ */ jsx("div", { className: "ml-auto shrink-0", children: /* @__PURE__ */ jsx(ProfessionalSelect, { value: professionalId, onChange: setProfessionalId, professionals }) })
|
|
1671
|
+
] })
|
|
1672
|
+
] }),
|
|
1673
|
+
conflict && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-destructive/10 px-3 py-1.5 text-xs text-destructive", children: [
|
|
1674
|
+
/* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5 shrink-0" }),
|
|
1675
|
+
t2("agenda.appointment.conflict")
|
|
1676
|
+
] })
|
|
1677
|
+
] })
|
|
1678
|
+
] }),
|
|
1679
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col flex-1 min-h-0 overflow-hidden", children: [
|
|
1680
|
+
modalTab === "client" && clientId && config.clientEntityDef && /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto px-5 py-4", children: clientDataLoading ? /* @__PURE__ */ jsx("div", { className: "space-y-3 py-2", children: [...Array(4)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-8 rounded-lg bg-muted animate-pulse" }, i)) }) : clientData ? clientEditing ? /* @__PURE__ */ jsx(
|
|
1681
|
+
CrudFormPage,
|
|
1682
|
+
{
|
|
1683
|
+
embedded: true,
|
|
1684
|
+
entityDef: config.clientEntityDef,
|
|
1685
|
+
mode: "edit",
|
|
1686
|
+
initialData: clientData,
|
|
1687
|
+
namePlural: "",
|
|
1688
|
+
onCancel: () => setClientEditing(false),
|
|
1689
|
+
onSubmit: async (data) => {
|
|
1690
|
+
try {
|
|
1691
|
+
const { getSupabaseClientOptional: getSupabaseClientOptional4 } = await import('@fayz-ai/core');
|
|
1692
|
+
const supabase = getSupabaseClientOptional4();
|
|
1693
|
+
if (!supabase) return;
|
|
1694
|
+
const dbData = {};
|
|
1695
|
+
for (const [k, v] of Object.entries(data)) {
|
|
1696
|
+
dbData[k.replace(/[A-Z]/g, (m) => "_" + m.toLowerCase())] = v;
|
|
1697
|
+
}
|
|
1698
|
+
await supabase.from("people").update(dbData).eq("id", clientId);
|
|
1699
|
+
setClientData({ ...clientData, ...data });
|
|
1700
|
+
setClientEditing(false);
|
|
1701
|
+
} catch {
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
) : /* @__PURE__ */ jsx(
|
|
1706
|
+
CrudDetailPage,
|
|
1707
|
+
{
|
|
1708
|
+
embedded: true,
|
|
1709
|
+
entityDef: config.clientEntityDef,
|
|
1710
|
+
item: clientData,
|
|
1711
|
+
namePlural: "",
|
|
1712
|
+
basePath: "",
|
|
1713
|
+
onBack: () => setModalTab("appointment"),
|
|
1714
|
+
onEdit: () => setClientEditing(true)
|
|
1715
|
+
}
|
|
1716
|
+
) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground py-4 text-center", children: t2("agenda.appointment.noClientData") }) }),
|
|
1717
|
+
modalTab === "financial" && editOrderId && config.financialBridge && /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto", children: /* @__PURE__ */ jsx(
|
|
1718
|
+
BookingPaymentPanel,
|
|
1719
|
+
{
|
|
1720
|
+
orderId: editOrderId,
|
|
1721
|
+
orderTotal: editOrderTotal,
|
|
1722
|
+
services: services.map((s) => ({ name: s.name, price: s.price, durationMinutes: s.durationMinutes })),
|
|
1723
|
+
formatCurrency: (v) => fmtCurrency(v, currency.locale, currency.code),
|
|
1724
|
+
inline: true,
|
|
1725
|
+
onClose: () => setModalTab("appointment"),
|
|
1726
|
+
onPaymentChange: () => {
|
|
1727
|
+
config.financialBridge.getPaymentDetail(editOrderId).then((d) => {
|
|
1728
|
+
if (d) {
|
|
1729
|
+
setEditPaymentStatus(d.status);
|
|
1730
|
+
setEditOrderTotal(d.totalAmount);
|
|
1731
|
+
}
|
|
1732
|
+
}).catch(() => {
|
|
1733
|
+
});
|
|
1734
|
+
if (agendaDateRange.start) fetchBookings(agendaDateRange);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
) }),
|
|
1738
|
+
/* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col flex-1 min-h-0 overflow-hidden", style: { display: modalTab === "appointment" ? void 0 : "none" }, children: [
|
|
1739
|
+
/* @__PURE__ */ jsxs("div", { className: "border-t fayz-glass-divider px-5 py-3 space-y-1 flex-1 overflow-y-auto", children: [
|
|
1740
|
+
loading && /* @__PURE__ */ jsx("div", { className: "space-y-2 py-2", children: [...Array(4)].map((_, i) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1.5", children: [
|
|
1741
|
+
/* @__PURE__ */ jsx("div", { className: "h-4 w-4 rounded bg-muted animate-pulse shrink-0" }),
|
|
1742
|
+
/* @__PURE__ */ jsx("div", { className: "h-8 flex-1 rounded-lg bg-muted animate-pulse" })
|
|
1743
|
+
] }, i)) }),
|
|
1744
|
+
!loading && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1745
|
+
activeType.fields.title && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1", children: [
|
|
1746
|
+
/* @__PURE__ */ jsx(Calendar, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
1747
|
+
/* @__PURE__ */ jsx(
|
|
1748
|
+
"input",
|
|
1774
1749
|
{
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1750
|
+
type: "text",
|
|
1751
|
+
value: title,
|
|
1752
|
+
onChange: (e) => setTitle(e.target.value),
|
|
1753
|
+
placeholder: t2("agenda.appointment.addTitle"),
|
|
1754
|
+
autoFocus: mode === "create",
|
|
1755
|
+
"aria-required": true,
|
|
1756
|
+
className: "w-full flex-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-ring"
|
|
1757
|
+
}
|
|
1758
|
+
)
|
|
1759
|
+
] }),
|
|
1760
|
+
activeType.fields.client && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1", children: [
|
|
1761
|
+
/* @__PURE__ */ jsx(User, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
1762
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx(
|
|
1763
|
+
ContactPicker,
|
|
1764
|
+
{
|
|
1765
|
+
value: clientId || clientSearch ? { id: clientId || void 0, name: clientSearch, phone: clientPhone } : null,
|
|
1766
|
+
onChange: (contact) => {
|
|
1767
|
+
setClientId(contact?.id ?? "");
|
|
1768
|
+
setClientSearch(contact?.name ?? "");
|
|
1769
|
+
setClientPhone(contact?.phone ?? "");
|
|
1794
1770
|
},
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1771
|
+
kind: config.clientKind,
|
|
1772
|
+
lookup: config.contactLookup,
|
|
1773
|
+
createEntityDef: config.clientEntityDef,
|
|
1774
|
+
clearable: !isPaidBooking,
|
|
1775
|
+
variant: "inline",
|
|
1776
|
+
placeholder: t2("agenda.appointment.searchClient"),
|
|
1777
|
+
createLabel: t2("agenda.appointment.newClient"),
|
|
1778
|
+
required: activeType.requiresClient
|
|
1779
|
+
}
|
|
1780
|
+
) })
|
|
1781
|
+
] }),
|
|
1782
|
+
activeType.fields.location && config.modules.locationSelection && (storeLocations.length > 0 || config.locations.length > 0) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1", children: [
|
|
1783
|
+
/* @__PURE__ */ jsx(MapPin, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
1784
|
+
/* @__PURE__ */ jsx(
|
|
1785
|
+
LocationSelect,
|
|
1786
|
+
{
|
|
1787
|
+
value: locationId,
|
|
1788
|
+
onChange: setLocationId,
|
|
1789
|
+
locations: storeLocations.length > 0 ? storeLocations : config.locations
|
|
1798
1790
|
}
|
|
1799
1791
|
)
|
|
1800
|
-
] })
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1792
|
+
] }),
|
|
1793
|
+
activeType.fields.services && /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 py-1", children: [
|
|
1794
|
+
/* @__PURE__ */ jsx(Briefcase, { className: "h-4 w-4 mt-1.5 text-muted-foreground shrink-0" }),
|
|
1795
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
|
|
1796
|
+
services.length > 0 && /* @__PURE__ */ jsx("div", { className: "space-y-0.5 mb-1", children: services.map((svc, idx) => /* @__PURE__ */ jsxs("div", { className: "flex items-center rounded-md bg-muted/30 px-2.5 py-1.5 text-sm", children: [
|
|
1797
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1 truncate min-w-0", children: svc.name }),
|
|
1798
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 shrink-0 ml-3", children: [
|
|
1799
|
+
/* @__PURE__ */ jsx(
|
|
1800
|
+
InlineNumber,
|
|
1801
|
+
{
|
|
1802
|
+
value: svc.durationMinutes,
|
|
1803
|
+
onChange: (v) => setServices((p) => p.map((s, i) => i === idx ? { ...s, durationMinutes: Math.round(v) } : s)),
|
|
1804
|
+
min: 5,
|
|
1805
|
+
step: 5,
|
|
1806
|
+
className: "w-10 shrink-0",
|
|
1807
|
+
disabled: isPaidBooking,
|
|
1808
|
+
tooltip: t2("agenda.appointment.duration")
|
|
1809
|
+
}
|
|
1810
|
+
),
|
|
1811
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: "min" })
|
|
1812
|
+
] }),
|
|
1813
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 shrink-0 ml-3", children: [
|
|
1814
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: currency.symbol }),
|
|
1815
|
+
/* @__PURE__ */ jsx(
|
|
1816
|
+
InlineNumber,
|
|
1817
|
+
{
|
|
1818
|
+
value: svc.price,
|
|
1819
|
+
onChange: (v) => setServices((p) => p.map((s, i) => i === idx ? { ...s, price: v } : s)),
|
|
1820
|
+
min: 0,
|
|
1821
|
+
step: 0.01,
|
|
1822
|
+
className: "w-14 shrink-0",
|
|
1823
|
+
disabled: isPaidBooking,
|
|
1824
|
+
tooltip: t2("agenda.appointment.price")
|
|
1825
|
+
}
|
|
1826
|
+
)
|
|
1827
|
+
] }),
|
|
1828
|
+
!isPaidBooking && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => removeService(idx), className: "p-0.5 ml-2 text-muted-foreground hover:text-destructive shrink-0", children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" }) })
|
|
1829
|
+
] }, idx)) }),
|
|
1830
|
+
serviceCreate.creating && serviceCreate.form,
|
|
1831
|
+
!isPaidBooking && !serviceCreate.creating && /* @__PURE__ */ jsx(
|
|
1832
|
+
SearchCombobox,
|
|
1833
|
+
{
|
|
1834
|
+
value: serviceSearch,
|
|
1835
|
+
onChange: setServiceSearch,
|
|
1836
|
+
onSelect: addService,
|
|
1837
|
+
options: serviceOptions,
|
|
1838
|
+
placeholder: t2("agenda.appointment.searchService"),
|
|
1839
|
+
onBlurEmpty: () => {
|
|
1840
|
+
},
|
|
1841
|
+
onFocus: () => {
|
|
1842
|
+
if (!serviceSearch && serviceOptions.length === 0) setServiceOptions(initialServiceOptions);
|
|
1843
|
+
},
|
|
1844
|
+
renderRight: (opt) => {
|
|
1845
|
+
const dur = opt.data?.duration_minutes || 0;
|
|
1846
|
+
const hasPrice = opt.price != null;
|
|
1847
|
+
if (!hasPrice && !dur) return null;
|
|
1848
|
+
return /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground text-xs whitespace-nowrap", children: [
|
|
1849
|
+
hasPrice && fmtCurrency(opt.price, currency.locale, currency.code),
|
|
1850
|
+
hasPrice && dur > 0 && " \xB7 ",
|
|
1851
|
+
dur > 0 && `${dur}min`
|
|
1852
|
+
] });
|
|
1853
|
+
},
|
|
1854
|
+
minimal: true,
|
|
1855
|
+
chevron: true,
|
|
1856
|
+
required: activeType.requiresServices && services.length === 0,
|
|
1857
|
+
allowCreate: !!serviceCreate.onCreate,
|
|
1858
|
+
onCreateNew: serviceCreate.onCreate,
|
|
1859
|
+
createLabel: t2("agenda.appointment.newService"),
|
|
1860
|
+
inlineLabel: services.length > 0 ? t2("agenda.appointment.addService") : t2("agenda.appointment.addServiceFirst"),
|
|
1861
|
+
inlineIcon: services.length > 0 ? "plus" : "search"
|
|
1862
|
+
}
|
|
1863
|
+
)
|
|
1864
|
+
] })
|
|
1865
|
+
] }),
|
|
1866
|
+
activeType.fields.status && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1", children: [
|
|
1867
|
+
/* @__PURE__ */ jsx(CircleDot, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
1868
|
+
/* @__PURE__ */ jsx(StatusSelect, { value: status, onChange: setStatus, statuses: config.statuses, bookingDate: date })
|
|
1869
|
+
] }),
|
|
1870
|
+
needsCancellationReason && /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 py-1", children: [
|
|
1871
|
+
/* @__PURE__ */ jsx(Ban, { className: "h-4 w-4 mt-2 text-muted-foreground shrink-0" }),
|
|
1872
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-2", children: [
|
|
1873
|
+
/* @__PURE__ */ jsxs(
|
|
1874
|
+
"select",
|
|
1875
|
+
{
|
|
1876
|
+
value: cancellationReasonId,
|
|
1877
|
+
onChange: (event) => setCancellationReasonId(event.target.value),
|
|
1878
|
+
disabled: cancellationReasonsLoading,
|
|
1879
|
+
"aria-required": true,
|
|
1880
|
+
className: "w-full rounded-input border border-input bg-card px-2.5 py-1.5 text-sm shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] focus:outline-none focus:ring-2 focus:ring-ring",
|
|
1881
|
+
children: [
|
|
1882
|
+
/* @__PURE__ */ jsx("option", { value: "", children: cancellationReasonsLoading ? t2("agenda.cancellation.loadingReasons") : t2("agenda.cancellation.selectReason") }),
|
|
1883
|
+
cancellationReasons.map((reason) => /* @__PURE__ */ jsxs("option", { value: reason.id, children: [
|
|
1884
|
+
reason.label,
|
|
1885
|
+
reason.requiresNotes ? " *" : ""
|
|
1886
|
+
] }, reason.id))
|
|
1887
|
+
]
|
|
1888
|
+
}
|
|
1889
|
+
),
|
|
1890
|
+
/* @__PURE__ */ jsx(
|
|
1891
|
+
"textarea",
|
|
1892
|
+
{
|
|
1893
|
+
value: cancellationNotes,
|
|
1894
|
+
onChange: (event) => setCancellationNotes(event.target.value),
|
|
1895
|
+
placeholder: needsCancellationNotes ? t2("agenda.cancellation.notesRequired") : t2("agenda.cancellation.notesOptional"),
|
|
1896
|
+
rows: 2,
|
|
1897
|
+
"aria-required": needsCancellationNotes || void 0,
|
|
1898
|
+
className: "w-full rounded-input border border-input bg-card px-2.5 py-1.5 text-sm resize-none shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] focus:outline-none focus:ring-2 focus:ring-ring"
|
|
1899
|
+
}
|
|
1900
|
+
)
|
|
1901
|
+
] })
|
|
1902
|
+
] }),
|
|
1903
|
+
!showNotes ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1", children: [
|
|
1904
|
+
/* @__PURE__ */ jsx(FileText, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
1905
|
+
/* @__PURE__ */ jsx(
|
|
1906
|
+
"button",
|
|
1811
1907
|
{
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
children: [
|
|
1817
|
-
/* @__PURE__ */ jsx("option", { value: "", children: cancellationReasonsLoading ? t2("agenda.cancellation.loadingReasons") : t2("agenda.cancellation.selectReason") }),
|
|
1818
|
-
cancellationReasons.map((reason) => /* @__PURE__ */ jsxs("option", { value: reason.id, children: [
|
|
1819
|
-
reason.label,
|
|
1820
|
-
reason.requiresNotes ? " *" : ""
|
|
1821
|
-
] }, reason.id))
|
|
1822
|
-
]
|
|
1908
|
+
type: "button",
|
|
1909
|
+
onClick: () => setShowNotes(true),
|
|
1910
|
+
className: "text-sm text-muted-foreground hover:text-foreground transition-colors text-left py-1.5 pl-2",
|
|
1911
|
+
children: t2("agenda.appointment.addNotes")
|
|
1823
1912
|
}
|
|
1824
|
-
)
|
|
1913
|
+
)
|
|
1914
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 py-1", children: [
|
|
1915
|
+
/* @__PURE__ */ jsx(FileText, { className: "h-4 w-4 mt-2 text-muted-foreground shrink-0" }),
|
|
1825
1916
|
/* @__PURE__ */ jsx(
|
|
1826
1917
|
"textarea",
|
|
1827
1918
|
{
|
|
1828
|
-
value:
|
|
1829
|
-
onChange: (
|
|
1830
|
-
placeholder:
|
|
1919
|
+
value: notes,
|
|
1920
|
+
onChange: (e) => setNotes(e.target.value),
|
|
1921
|
+
placeholder: t2("agenda.appointment.addDescription"),
|
|
1831
1922
|
rows: 2,
|
|
1832
|
-
|
|
1923
|
+
autoFocus: true,
|
|
1924
|
+
className: "w-full flex-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
|
|
1833
1925
|
}
|
|
1834
1926
|
)
|
|
1835
1927
|
] })
|
|
1836
|
-
] }),
|
|
1837
|
-
!showNotes ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1", children: [
|
|
1838
|
-
/* @__PURE__ */ jsx(FileText, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
1839
|
-
/* @__PURE__ */ jsx(
|
|
1840
|
-
"button",
|
|
1841
|
-
{
|
|
1842
|
-
type: "button",
|
|
1843
|
-
onClick: () => setShowNotes(true),
|
|
1844
|
-
className: "text-sm text-muted-foreground hover:text-foreground transition-colors text-left py-1.5 pl-2",
|
|
1845
|
-
children: t2("agenda.appointment.addNotes")
|
|
1846
|
-
}
|
|
1847
|
-
)
|
|
1848
|
-
] }) : /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 py-1", children: [
|
|
1849
|
-
/* @__PURE__ */ jsx(FileText, { className: "h-4 w-4 mt-2 text-muted-foreground shrink-0" }),
|
|
1850
|
-
/* @__PURE__ */ jsx(
|
|
1851
|
-
"textarea",
|
|
1852
|
-
{
|
|
1853
|
-
value: notes,
|
|
1854
|
-
onChange: (e) => setNotes(e.target.value),
|
|
1855
|
-
placeholder: t2("agenda.appointment.addDescription"),
|
|
1856
|
-
rows: 2,
|
|
1857
|
-
autoFocus: true,
|
|
1858
|
-
className: "w-full flex-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
|
|
1859
|
-
}
|
|
1860
|
-
)
|
|
1861
|
-
] })
|
|
1862
|
-
] })
|
|
1863
|
-
] }),
|
|
1864
|
-
modalTab === "appointment" && !clientEditing && /* @__PURE__ */ jsx("div", { className: "px-5 py-2.5 border-t fayz-glass-divider bg-transparent shrink-0 sm:rounded-b-2xl", children: confirmingDelete ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
1865
|
-
/* @__PURE__ */ jsx("span", { className: "text-xs text-destructive font-medium", children: t2("agenda.appointment.deleteConfirm") }),
|
|
1866
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1867
|
-
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setConfirmingDelete(false), children: t2("agenda.appointment.cancel") }),
|
|
1868
|
-
/* @__PURE__ */ jsx(Button, { type: "button", variant: "destructive", size: "sm", onClick: handleDelete, disabled: deleting, children: deleting ? t2("agenda.appointment.deleting") : t2("agenda.appointment.confirmDelete") })
|
|
1869
|
-
] })
|
|
1870
|
-
] }) : /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
1871
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1872
|
-
mode === "edit" && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setConfirmingDelete(true), className: "text-xs text-destructive font-medium hover:underline", children: t2("agenda.appointment.delete") }),
|
|
1873
|
-
services.length > 0 && /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
1874
|
-
/* @__PURE__ */ jsx(Clock, { className: "h-3 w-3" }),
|
|
1875
|
-
totalDuration,
|
|
1876
|
-
"min",
|
|
1877
|
-
config.modules.financial && totalPrice > 0 && mode === "edit" ? paymentStatusLoading ? /* @__PURE__ */ jsx("span", { className: "h-4 w-16 rounded-full bg-muted animate-pulse" }) : isPaidBooking ? /* @__PURE__ */ jsxs(
|
|
1878
|
-
"button",
|
|
1879
|
-
{
|
|
1880
|
-
type: "button",
|
|
1881
|
-
onClick: (e) => {
|
|
1882
|
-
e.stopPropagation();
|
|
1883
|
-
setModalTab("financial");
|
|
1884
|
-
},
|
|
1885
|
-
className: "inline-flex items-center gap-1 rounded-full bg-success/10 px-2 py-0.5 text-[10px] font-medium text-success hover:bg-success/20 transition-colors",
|
|
1886
|
-
children: [
|
|
1887
|
-
/* @__PURE__ */ jsx(Check, { className: "h-2.5 w-2.5" }),
|
|
1888
|
-
fmtCurrency(totalPrice, currency.locale, currency.code),
|
|
1889
|
-
" \xB7 ",
|
|
1890
|
-
t2("agenda.payment.paid")
|
|
1891
|
-
]
|
|
1892
|
-
}
|
|
1893
|
-
) : date <= (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) ? /* @__PURE__ */ jsxs(
|
|
1894
|
-
"button",
|
|
1895
|
-
{
|
|
1896
|
-
type: "button",
|
|
1897
|
-
onClick: (e) => {
|
|
1898
|
-
e.stopPropagation();
|
|
1899
|
-
setModalTab("financial");
|
|
1900
|
-
},
|
|
1901
|
-
className: "inline-flex items-center gap-1 rounded-full bg-warning/10 px-2 py-0.5 text-[10px] font-medium text-warning hover:bg-warning/20 transition-colors",
|
|
1902
|
-
children: [
|
|
1903
|
-
/* @__PURE__ */ jsx(HandCoins, { className: "h-3 w-3" }),
|
|
1904
|
-
fmtCurrency(totalPrice, currency.locale, currency.code),
|
|
1905
|
-
" \xB7 ",
|
|
1906
|
-
t2("agenda.payment.collect")
|
|
1907
|
-
]
|
|
1908
|
-
}
|
|
1909
|
-
) : /* @__PURE__ */ jsxs("span", { children: [
|
|
1910
|
-
"\xB7 ",
|
|
1911
|
-
fmtCurrency(totalPrice, currency.locale, currency.code)
|
|
1912
|
-
] }) : /* @__PURE__ */ jsxs("span", { children: [
|
|
1913
|
-
"\xB7 ",
|
|
1914
|
-
fmtCurrency(totalPrice, currency.locale, currency.code)
|
|
1915
|
-
] })
|
|
1916
1928
|
] })
|
|
1917
1929
|
] }),
|
|
1918
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center
|
|
1919
|
-
/* @__PURE__ */ jsx(
|
|
1920
|
-
/* @__PURE__ */
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1930
|
+
modalTab === "appointment" && !clientEditing && /* @__PURE__ */ jsx("div", { className: "px-5 py-2.5 border-t fayz-glass-divider bg-transparent shrink-0", children: confirmingDelete ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
1931
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-destructive font-medium", children: t2("agenda.appointment.deleteConfirm") }),
|
|
1932
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1933
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setConfirmingDelete(false), children: t2("agenda.appointment.cancel") }),
|
|
1934
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "destructive", size: "sm", onClick: handleDelete, disabled: deleting, children: deleting ? t2("agenda.appointment.deleting") : t2("agenda.appointment.confirmDelete") })
|
|
1935
|
+
] })
|
|
1936
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
1937
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1938
|
+
mode === "edit" && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setConfirmingDelete(true), className: "text-xs text-destructive font-medium hover:underline", children: t2("agenda.appointment.delete") }),
|
|
1939
|
+
services.length > 0 && /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
1940
|
+
/* @__PURE__ */ jsx(Clock, { className: "h-3 w-3" }),
|
|
1941
|
+
totalDuration,
|
|
1942
|
+
"min",
|
|
1943
|
+
config.modules.financial && totalPrice > 0 && mode === "edit" ? paymentStatusLoading ? /* @__PURE__ */ jsx("span", { className: "h-4 w-16 rounded-full bg-muted animate-pulse" }) : isPaidBooking ? /* @__PURE__ */ jsxs(
|
|
1944
|
+
"button",
|
|
1945
|
+
{
|
|
1946
|
+
type: "button",
|
|
1947
|
+
onClick: (e) => {
|
|
1948
|
+
e.stopPropagation();
|
|
1949
|
+
setModalTab("financial");
|
|
1950
|
+
},
|
|
1951
|
+
className: "inline-flex items-center gap-1 rounded-full bg-success/10 px-2 py-0.5 text-[10px] font-medium text-success hover:bg-success/20 transition-colors",
|
|
1952
|
+
children: [
|
|
1953
|
+
/* @__PURE__ */ jsx(Check, { className: "h-2.5 w-2.5" }),
|
|
1954
|
+
fmtCurrency(totalPrice, currency.locale, currency.code),
|
|
1955
|
+
" \xB7 ",
|
|
1956
|
+
t2("agenda.payment.paid")
|
|
1957
|
+
]
|
|
1958
|
+
}
|
|
1959
|
+
) : date <= (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) ? /* @__PURE__ */ jsxs(
|
|
1960
|
+
"button",
|
|
1961
|
+
{
|
|
1962
|
+
type: "button",
|
|
1963
|
+
onClick: (e) => {
|
|
1964
|
+
e.stopPropagation();
|
|
1965
|
+
setModalTab("financial");
|
|
1966
|
+
},
|
|
1967
|
+
className: "inline-flex items-center gap-1 rounded-full bg-warning/10 px-2 py-0.5 text-[10px] font-medium text-warning hover:bg-warning/20 transition-colors",
|
|
1968
|
+
children: [
|
|
1969
|
+
/* @__PURE__ */ jsx(HandCoins, { className: "h-3 w-3" }),
|
|
1970
|
+
fmtCurrency(totalPrice, currency.locale, currency.code),
|
|
1971
|
+
" \xB7 ",
|
|
1972
|
+
t2("agenda.payment.collect")
|
|
1973
|
+
]
|
|
1974
|
+
}
|
|
1975
|
+
) : /* @__PURE__ */ jsxs("span", { children: [
|
|
1976
|
+
"\xB7 ",
|
|
1977
|
+
fmtCurrency(totalPrice, currency.locale, currency.code)
|
|
1978
|
+
] }) : /* @__PURE__ */ jsxs("span", { children: [
|
|
1979
|
+
"\xB7 ",
|
|
1980
|
+
fmtCurrency(totalPrice, currency.locale, currency.code)
|
|
1981
|
+
] })
|
|
1982
|
+
] })
|
|
1983
|
+
] }),
|
|
1984
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsx(Button, { type: "submit", variant: "default", size: "sm", disabled: !canSubmit || !isDirty, children: saving ? t2("agenda.appointment.saving") : mode === "create" ? t2("agenda.appointment.save") : t2("agenda.appointment.update") }) })
|
|
1985
|
+
] }) })
|
|
1986
|
+
] }),
|
|
1987
|
+
modalTab !== "appointment" && !clientEditing && /* @__PURE__ */ jsx("div", { className: "px-5 py-2.5 border-t fayz-glass-divider bg-transparent shrink-0", children: /* @__PURE__ */ jsx("div", { className: "flex items-center", children: /* @__PURE__ */ jsxs(
|
|
1988
|
+
"button",
|
|
1989
|
+
{
|
|
1990
|
+
type: "button",
|
|
1991
|
+
onClick: () => {
|
|
1992
|
+
setModalTab("appointment");
|
|
1993
|
+
setClientEditing(false);
|
|
1994
|
+
},
|
|
1995
|
+
className: "inline-flex items-center gap-1 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors",
|
|
1996
|
+
children: [
|
|
1997
|
+
/* @__PURE__ */ jsx("svg", { className: "h-3 w-3", viewBox: "0 0 16 16", fill: "none", children: /* @__PURE__ */ jsx("path", { d: "M10 12L6 8l4-4", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }),
|
|
1998
|
+
t2("agenda.payment.backToAppointment")
|
|
1999
|
+
]
|
|
2000
|
+
}
|
|
2001
|
+
) }) })
|
|
2002
|
+
] })
|
|
1939
2003
|
] })
|
|
1940
|
-
|
|
2004
|
+
);
|
|
1941
2005
|
}
|
|
1942
2006
|
var STYLE_ID2 = "saas-floating-panel";
|
|
1943
2007
|
if (typeof document !== "undefined" && !document.getElementById(STYLE_ID2)) {
|
|
@@ -2369,7 +2433,20 @@ function timeLabel(iso) {
|
|
|
2369
2433
|
const d = new Date(iso);
|
|
2370
2434
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
2371
2435
|
}
|
|
2372
|
-
function
|
|
2436
|
+
function AgendaMobileListSkeleton() {
|
|
2437
|
+
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: [
|
|
2438
|
+
/* @__PURE__ */ jsx("div", { className: "mb-2 ml-1 h-3 w-20 rounded bg-muted" }),
|
|
2439
|
+
/* @__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: [
|
|
2440
|
+
/* @__PURE__ */ jsx("div", { className: "h-2 w-2 shrink-0 rounded-full bg-muted" }),
|
|
2441
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [
|
|
2442
|
+
/* @__PURE__ */ jsx("div", { className: "h-3.5 w-2/3 rounded bg-muted" }),
|
|
2443
|
+
/* @__PURE__ */ jsx("div", { className: "h-3 w-12 rounded bg-muted" })
|
|
2444
|
+
] }),
|
|
2445
|
+
/* @__PURE__ */ jsx("div", { className: "h-3.5 w-14 shrink-0 rounded bg-muted" })
|
|
2446
|
+
] }, r)) })
|
|
2447
|
+
] }, g)) });
|
|
2448
|
+
}
|
|
2449
|
+
function AgendaMobileList({ bookings, statusColors, currencyCode, onSelect, onCreate }) {
|
|
2373
2450
|
const t2 = useTranslation();
|
|
2374
2451
|
const locale = getCurrentLocale() === "pt-BR" ? "pt-BR" : "en";
|
|
2375
2452
|
const groups = useMemo(() => {
|
|
@@ -2394,7 +2471,18 @@ function AgendaMobileList({ bookings, statusColors, currencyCode, onSelect }) {
|
|
|
2394
2471
|
return date.toLocaleDateString(locale, { weekday: "long", day: "numeric", month: "long" });
|
|
2395
2472
|
}
|
|
2396
2473
|
if (groups.length === 0) {
|
|
2397
|
-
return /* @__PURE__ */
|
|
2474
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-4 px-6 py-16 text-center", children: [
|
|
2475
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t2("agenda.list.empty") }),
|
|
2476
|
+
onCreate && /* @__PURE__ */ jsx(
|
|
2477
|
+
"button",
|
|
2478
|
+
{
|
|
2479
|
+
type: "button",
|
|
2480
|
+
onClick: onCreate,
|
|
2481
|
+
className: "fz-tap inline-flex items-center rounded-full bg-primary px-5 text-sm font-medium text-primary-foreground active:scale-[0.98]",
|
|
2482
|
+
children: t2("agenda.calendar.newAppointment")
|
|
2483
|
+
}
|
|
2484
|
+
)
|
|
2485
|
+
] });
|
|
2398
2486
|
}
|
|
2399
2487
|
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: [
|
|
2400
2488
|
/* @__PURE__ */ jsx("h3", { className: "px-1 pb-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: dayLabel(g.date) }),
|
|
@@ -2405,7 +2493,7 @@ function AgendaMobileList({ bookings, statusColors, currencyCode, onSelect }) {
|
|
|
2405
2493
|
"button",
|
|
2406
2494
|
{
|
|
2407
2495
|
onClick: () => onSelect(b),
|
|
2408
|
-
className: "flex w-full items-center gap-3 rounded-
|
|
2496
|
+
className: "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",
|
|
2409
2497
|
children: [
|
|
2410
2498
|
/* @__PURE__ */ jsx("span", { className: "h-2.5 w-2.5 shrink-0 rounded-full", style: { backgroundColor: color } }),
|
|
2411
2499
|
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
@@ -2595,7 +2683,7 @@ function CalendarView() {
|
|
|
2595
2683
|
endTime: agendaSettings.endTime
|
|
2596
2684
|
};
|
|
2597
2685
|
const bookings = useAgendaStore((s) => s.bookings);
|
|
2598
|
-
useAgendaStore((s) => s.bookingsLoading);
|
|
2686
|
+
const bookingsLoading = useAgendaStore((s) => s.bookingsLoading);
|
|
2599
2687
|
const professionals = useAgendaStore((s) => s.professionals);
|
|
2600
2688
|
const professionalsLoading = useAgendaStore((s) => s.professionalsLoading);
|
|
2601
2689
|
const currentView = useAgendaStore((s) => s.currentView);
|
|
@@ -2646,6 +2734,7 @@ function CalendarView() {
|
|
|
2646
2734
|
const [profSectionOpen, setProfSectionOpen] = useState(true);
|
|
2647
2735
|
const [statusSectionOpen, setStatusSectionOpen] = useState(false);
|
|
2648
2736
|
const [visibleRange, setVisibleRange] = useState(null);
|
|
2737
|
+
useEffect(() => () => closeModal(), [closeModal]);
|
|
2649
2738
|
const openModal = useCallback((...args) => {
|
|
2650
2739
|
setPopoverVisible(false);
|
|
2651
2740
|
setSelectedBooking(null);
|
|
@@ -3140,18 +3229,37 @@ function CalendarView() {
|
|
|
3140
3229
|
}) })
|
|
3141
3230
|
] })
|
|
3142
3231
|
] }),
|
|
3232
|
+
isMobile && /* @__PURE__ */ jsx(PageHeaderActions, { children: /* @__PURE__ */ jsx(
|
|
3233
|
+
PageActions,
|
|
3234
|
+
{
|
|
3235
|
+
actions: [
|
|
3236
|
+
{
|
|
3237
|
+
id: "new-booking",
|
|
3238
|
+
label: t2("agenda.calendar.newAppointment"),
|
|
3239
|
+
icon: Plus,
|
|
3240
|
+
intent: "primary",
|
|
3241
|
+
creates: true,
|
|
3242
|
+
onSelect: () => openModal("create")
|
|
3243
|
+
},
|
|
3244
|
+
{
|
|
3245
|
+
id: "agenda-settings",
|
|
3246
|
+
label: t2("agenda.settings.title"),
|
|
3247
|
+
icon: Settings,
|
|
3248
|
+
// Utility, not an alternative way to book: the shell gives it
|
|
3249
|
+
// its own quiet circle above the "+" instead of hiding it in a
|
|
3250
|
+
// create menu.
|
|
3251
|
+
intent: "utility",
|
|
3252
|
+
onSelect: () => {
|
|
3253
|
+
window.location.hash = "/settings/agenda";
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
]
|
|
3257
|
+
}
|
|
3258
|
+
) }),
|
|
3143
3259
|
/* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col min-w-0 overflow-hidden", children: [
|
|
3144
3260
|
/* @__PURE__ */ jsxs("div", { className: "shrink-0 border-b", children: [
|
|
3145
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between pl-4 pr-
|
|
3261
|
+
/* @__PURE__ */ jsxs("div", { className: `${mobileGridActive ? "flex" : "hidden"} md:flex h-12 items-center justify-between pl-4 pr-4`, children: [
|
|
3146
3262
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [
|
|
3147
|
-
/* @__PURE__ */ jsx(
|
|
3148
|
-
"button",
|
|
3149
|
-
{
|
|
3150
|
-
onClick: () => openModal("create"),
|
|
3151
|
-
className: "md:hidden flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground",
|
|
3152
|
-
children: /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" })
|
|
3153
|
-
}
|
|
3154
|
-
),
|
|
3155
3263
|
/* @__PURE__ */ jsxs("div", { className: `${mobileGridActive ? "flex" : "hidden"} md:flex items-center gap-3`, children: [
|
|
3156
3264
|
/* @__PURE__ */ jsx(
|
|
3157
3265
|
"button",
|
|
@@ -3166,7 +3274,7 @@ function CalendarView() {
|
|
|
3166
3274
|
/* @__PURE__ */ jsx("button", { 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" }) })
|
|
3167
3275
|
] })
|
|
3168
3276
|
] }),
|
|
3169
|
-
/* @__PURE__ */ jsx("
|
|
3277
|
+
!(isMobile && !mobileGridActive) && /* @__PURE__ */ jsx("h2", { className: "truncate text-base font-normal sm:text-lg", children: calendarTitle })
|
|
3170
3278
|
] }),
|
|
3171
3279
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [
|
|
3172
3280
|
/* @__PURE__ */ jsx("div", { className: "hidden md:flex items-center gap-1", children: viewOptions.map((opt) => /* @__PURE__ */ jsx(
|
|
@@ -3184,8 +3292,9 @@ function CalendarView() {
|
|
|
3184
3292
|
variant: "outline",
|
|
3185
3293
|
size: "icon",
|
|
3186
3294
|
onClick: () => window.location.hash = "/settings/agenda",
|
|
3187
|
-
|
|
3188
|
-
|
|
3295
|
+
"aria-label": t2("agenda.settings.title"),
|
|
3296
|
+
title: t2("agenda.settings.title"),
|
|
3297
|
+
className: "ml-1 hidden md:inline-flex",
|
|
3189
3298
|
children: /* @__PURE__ */ jsx(Settings, { className: "h-4 w-4" })
|
|
3190
3299
|
}
|
|
3191
3300
|
)
|
|
@@ -3195,20 +3304,27 @@ function CalendarView() {
|
|
|
3195
3304
|
"button",
|
|
3196
3305
|
{
|
|
3197
3306
|
onClick: () => handleMobileViewChange(opt.key),
|
|
3198
|
-
|
|
3307
|
+
"aria-pressed": mobileView === opt.key,
|
|
3308
|
+
className: `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"}`,
|
|
3199
3309
|
children: t2(opt.labelKey)
|
|
3200
3310
|
},
|
|
3201
3311
|
opt.key
|
|
3202
3312
|
)) })
|
|
3203
3313
|
] }),
|
|
3204
|
-
isMobile && !mobileGridActive ?
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3314
|
+
isMobile && !mobileGridActive ? (
|
|
3315
|
+
// The mobile branch used to sit in front of the loading check, so the
|
|
3316
|
+
// list rendered empty while the query ran — which on this screen reads
|
|
3317
|
+
// as "nothing scheduled", the one wrong answer it must not give.
|
|
3318
|
+
bookingsLoading && bookings.length === 0 ? /* @__PURE__ */ jsx(AgendaMobileListSkeleton, {}) : /* @__PURE__ */ jsx(
|
|
3319
|
+
AgendaMobileList,
|
|
3320
|
+
{
|
|
3321
|
+
bookings,
|
|
3322
|
+
statusColors,
|
|
3323
|
+
currencyCode: config.currency.code,
|
|
3324
|
+
onSelect: (b) => openModal("edit", { bookingId: b.id }),
|
|
3325
|
+
onCreate: () => openModal("create")
|
|
3326
|
+
}
|
|
3327
|
+
)
|
|
3212
3328
|
) : professionalsLoading && professionals.length === 0 ? /* @__PURE__ */ jsx(CalendarSkeleton, { profCount: 2 }) : /* @__PURE__ */ jsx("div", { ref: wrapperRef, className: "flex-1 overflow-hidden agenda-calendar", children: /* @__PURE__ */ jsx(
|
|
3213
3329
|
FullCalendar,
|
|
3214
3330
|
{
|
|
@@ -3784,7 +3900,11 @@ function createSupabaseAgendaProvider(providerOptions) {
|
|
|
3784
3900
|
},
|
|
3785
3901
|
async getProfessionals() {
|
|
3786
3902
|
const { core } = getClients();
|
|
3787
|
-
const
|
|
3903
|
+
const base = () => core.from("people").select("id, name, avatar_url, is_active").eq("kind", professionalKind).eq("is_active", true);
|
|
3904
|
+
let { data, error } = await base().eq("is_bookable", true).order("name");
|
|
3905
|
+
if (error && error.code === "42703") {
|
|
3906
|
+
({ data, error } = await base().order("name"));
|
|
3907
|
+
}
|
|
3788
3908
|
if (error) throw error;
|
|
3789
3909
|
return (data ?? []).map((p) => ({
|
|
3790
3910
|
id: p.id,
|
|
@@ -3843,6 +3963,12 @@ function bookingPayload(booking) {
|
|
|
3843
3963
|
const starts = new Date(booking.startsAt);
|
|
3844
3964
|
return {
|
|
3845
3965
|
customer_name: booking.clientName ?? "",
|
|
3966
|
+
// O destino do WhatsApp, exatamente como está na ficha do cliente — máscara,
|
|
3967
|
+
// DDD solto, o que houver. Normalizar é trabalho de quem envia
|
|
3968
|
+
// (normalizeBrPhoneToE164), e adivinhar aqui só criaria uma quinta versão da
|
|
3969
|
+
// mesma regra. Vazio quando a ficha não tem telefone: a automação pula, e um
|
|
3970
|
+
// envio pulado é melhor que um envio para o número de outra pessoa.
|
|
3971
|
+
customer_phone: booking.clientPhone ?? "",
|
|
3846
3972
|
service_name: (booking.services ?? []).map((s) => s.name).join(" + "),
|
|
3847
3973
|
professional_name: booking.professionalName ?? "",
|
|
3848
3974
|
booking_date: starts.toLocaleDateString("pt-BR"),
|
|
@@ -4201,6 +4327,8 @@ var en = {
|
|
|
4201
4327
|
"agenda.appointment.name": "Name",
|
|
4202
4328
|
"agenda.appointment.new": "New Appointment",
|
|
4203
4329
|
"agenda.appointment.newClient": "New client",
|
|
4330
|
+
"agenda.appointment.required": "Required",
|
|
4331
|
+
"agenda.appointment.newService": "New service",
|
|
4204
4332
|
"agenda.appointment.notes": "Notes",
|
|
4205
4333
|
"agenda.appointment.phone": "Phone",
|
|
4206
4334
|
"agenda.appointment.professional": "Professional",
|
|
@@ -4421,6 +4549,8 @@ var ptBR = {
|
|
|
4421
4549
|
"agenda.appointment.name": "Nome",
|
|
4422
4550
|
"agenda.appointment.new": "Novo Agendamento",
|
|
4423
4551
|
"agenda.appointment.newClient": "Novo cliente",
|
|
4552
|
+
"agenda.appointment.required": "Obrigat\xF3rio",
|
|
4553
|
+
"agenda.appointment.newService": "Novo servi\xE7o",
|
|
4424
4554
|
"agenda.appointment.notes": "Observa\xE7\xF5es",
|
|
4425
4555
|
"agenda.appointment.phone": "Telefone",
|
|
4426
4556
|
"agenda.appointment.professional": "Profissional",
|
|
@@ -4661,8 +4791,14 @@ ALTER TABLE public.order_items
|
|
|
4661
4791
|
ADD COLUMN IF NOT EXISTS duration_minutes integer,
|
|
4662
4792
|
ADD COLUMN IF NOT EXISTS assignee_id uuid REFERENCES public.people(id) ON DELETE SET NULL;
|
|
4663
4793
|
|
|
4664
|
-
--
|
|
4665
|
-
|
|
4794
|
+
-- Lookup key for public customers (per tenant, by normalized phone).
|
|
4795
|
+
-- NOT UNIQUE. It was, and spine/110 says why it cannot be: \`kind\` became a
|
|
4796
|
+
-- derived column, so an imported 'client' turns into a 'customer' and a
|
|
4797
|
+
-- placeholder phone shared by eight real people lands under one key. The upsert
|
|
4798
|
+
-- that used to lean on it takes a per-(tenant, phone) advisory lock instead
|
|
4799
|
+
-- (003, 009) \u2014 the index is here for the lookup that lock guards.
|
|
4800
|
+
DROP INDEX IF EXISTS public.people_tenant_customer_phone;
|
|
4801
|
+
CREATE INDEX IF NOT EXISTS people_tenant_customer_phone
|
|
4666
4802
|
ON public.people(tenant_id, phone)
|
|
4667
4803
|
WHERE kind = 'customer' AND phone IS NOT NULL;
|
|
4668
4804
|
|
|
@@ -4674,6 +4810,8 @@ CREATE INDEX IF NOT EXISTS appointments_assignee_time
|
|
|
4674
4810
|
-- \xA72 \u2014 admin read model (security_invoker \u2192 underlying RLS applies; anon sees 0 rows)
|
|
4675
4811
|
-- DROP first: 013 widens this view and CREATE OR REPLACE cannot narrow a live
|
|
4676
4812
|
-- view on replay ("cannot drop columns from view"). 013 replays right after.
|
|
4813
|
+
-- 013 builds on this one, so the drop needs its dependents out and back (143).
|
|
4814
|
+
SELECT app.detach_view_dependents('public.v_appointments');
|
|
4677
4815
|
DROP VIEW IF EXISTS public.v_appointments;
|
|
4678
4816
|
CREATE VIEW public.v_appointments
|
|
4679
4817
|
WITH (security_invoker = true) AS
|
|
@@ -4883,16 +5021,32 @@ BEGIN
|
|
|
4883
5021
|
RAISE EXCEPTION 'too many open bookings for this phone';
|
|
4884
5022
|
END IF;
|
|
4885
5023
|
|
|
4886
|
-
--
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
5024
|
+
-- Upsert the customer by (tenant, phone). NOT ON CONFLICT: the partial unique
|
|
5025
|
+
-- index this used to infer is gone (spine/110 \u2014 a placeholder phone is shared
|
|
5026
|
+
-- by real, distinct people). 003 already settled the replacement: take a
|
|
5027
|
+
-- per-(tenant, phone) advisory lock so two first-bookings serialise, then look
|
|
5028
|
+
-- before inserting. Oldest match wins, so a returning caller keeps the record
|
|
5029
|
+
-- that carries their history.
|
|
5030
|
+
PERFORM pg_advisory_xact_lock(hashtextextended(p_tenant_id::text || ':' || v_phone, 1));
|
|
5031
|
+
|
|
5032
|
+
SELECT p.id INTO v_person_id
|
|
5033
|
+
FROM people p
|
|
5034
|
+
WHERE p.tenant_id = p_tenant_id AND p.kind = 'customer' AND p.phone = v_phone
|
|
5035
|
+
ORDER BY p.created_at
|
|
5036
|
+
LIMIT 1;
|
|
5037
|
+
|
|
5038
|
+
IF v_person_id IS NOT NULL THEN
|
|
5039
|
+
UPDATE people SET
|
|
5040
|
+
name = trim(p_name),
|
|
5041
|
+
email = COALESCE(p_email, people.email),
|
|
5042
|
+
updated_at = now()
|
|
5043
|
+
WHERE id = v_person_id;
|
|
5044
|
+
ELSE
|
|
5045
|
+
INSERT INTO people (tenant_id, kind, name, phone, email, metadata)
|
|
5046
|
+
VALUES (p_tenant_id, 'customer', trim(p_name), v_phone, p_email,
|
|
5047
|
+
jsonb_build_object('source', 'public_booking'))
|
|
5048
|
+
RETURNING id INTO v_person_id;
|
|
5049
|
+
END IF;
|
|
4896
5050
|
|
|
4897
5051
|
-- order + booking + order_item (statuses hardcoded \u2014 caller cannot set)
|
|
4898
5052
|
INSERT INTO orders (tenant_id, kind, status, party_id, assignee_id, subtotal, total, notes, metadata)
|
|
@@ -4922,6 +5076,8 @@ $$;
|
|
|
4922
5076
|
REVOKE ALL ON FUNCTION public.create_public_booking(uuid, uuid, timestamptz, text, text, text, text) FROM public;
|
|
4923
5077
|
GRANT EXECUTE ON FUNCTION public.create_public_booking(uuid, uuid, timestamptz, text, text, text, text)
|
|
4924
5078
|
TO anon, authenticated, service_role;
|
|
5079
|
+
|
|
5080
|
+
SELECT app.reattach_view_dependents();
|
|
4925
5081
|
`;
|
|
4926
5082
|
function computeCommissionAmount(total, ratePercent) {
|
|
4927
5083
|
const base = Number(total) || 0;
|
|
@@ -5042,7 +5198,7 @@ function createFinancialBridge(provider2) {
|
|
|
5042
5198
|
if (!supabase) return null;
|
|
5043
5199
|
const { data: order } = await supabase.from("orders").select("id, tenant_id, assignee_id, total").eq("id", orderId).single();
|
|
5044
5200
|
if (!order || !order.assignee_id) return null;
|
|
5045
|
-
const { data: staff } = await supabase.from("
|
|
5201
|
+
const { data: staff } = await supabase.from("v_professional_rates").select("commission_rate").eq("id", order.assignee_id).single();
|
|
5046
5202
|
const rate = Number(staff?.commission_rate) || 0;
|
|
5047
5203
|
const base = Number(order.total) || 0;
|
|
5048
5204
|
const amount = computeCommissionAmount(base, rate);
|
|
@@ -5715,7 +5871,7 @@ function ImportEventsWizard({
|
|
|
5715
5871
|
setImporting(false);
|
|
5716
5872
|
}
|
|
5717
5873
|
}
|
|
5718
|
-
return /* @__PURE__ */ jsxs("div", { className: "rounded-
|
|
5874
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-card border bg-card", children: [
|
|
5719
5875
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b px-3 py-2", children: [
|
|
5720
5876
|
/* @__PURE__ */ jsx(CalendarPlus, { className: "h-4 w-4 text-muted-foreground" }),
|
|
5721
5877
|
/* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Import future events" }),
|
|
@@ -8000,6 +8156,7 @@ function createAgendaPlugin(options) {
|
|
|
8000
8156
|
const PageComponent = () => React13.createElement(AgendaPage, { config, provider: provider2, store });
|
|
8001
8157
|
return {
|
|
8002
8158
|
id: "agenda",
|
|
8159
|
+
defaultAgentRole: "frontdesk",
|
|
8003
8160
|
name: config.labels.pageTitle,
|
|
8004
8161
|
icon: "Calendar",
|
|
8005
8162
|
version: "1.0.0",
|
|
@@ -8009,6 +8166,15 @@ function createAgendaPlugin(options) {
|
|
|
8009
8166
|
// the saas/admin Panel world, not the ecommerce storefront.
|
|
8010
8167
|
scaffolds: ["saas"],
|
|
8011
8168
|
defaultEnabled: true,
|
|
8169
|
+
// The parts a plan can permit separately (#281, ADR 0025). This is the
|
|
8170
|
+
// plugin ADR 0018 named: it ships open with the "suggest a slot" button,
|
|
8171
|
+
// and the intelligence behind that button is a service we host — so the
|
|
8172
|
+
// facet is what says whether this tenant contracted it.
|
|
8173
|
+
facets: [
|
|
8174
|
+
{ id: "scheduling", label: "Agenda", description: "Hor\xE1rios, profissionais e reservas.", includedWithPlugin: true },
|
|
8175
|
+
{ id: "public_booking", label: "Agendamento online", description: "A p\xE1gina p\xFAblica onde o cliente marca sozinho." },
|
|
8176
|
+
{ id: "smart_fit", label: "Encaixe inteligente", description: "Sugere o melhor hor\xE1rio lendo a agenda inteira." }
|
|
8177
|
+
],
|
|
8012
8178
|
dependencies: [],
|
|
8013
8179
|
// Os FATOS da agenda — só expostos enquanto o módulo está ATIVO.
|
|
8014
8180
|
events: buildAgendaDomainEvents(),
|