@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/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, Settings, Pencil, Trash2, Clock, User, MapPin, FileText, AlertTriangle, Check, DollarSign, HandCoins, Ban, CheckSquare, Calendar, Briefcase, CircleDot, FileCheck, ArrowRightLeft, CreditCard, QrCode, Banknote, ExternalLink } from 'lucide-react';
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, ConfirmDialog, Modal, ModalContent, ModalTitle, SearchCombobox, cn, TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@fayz-ai/ui';
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
- body: "Oi {{customer_name}}! Seu hor\xE1rio est\xE1 marcado \u2705\n\n*{{service_name}}*\n{{booking_date}} \xE0s {{booking_time}}\ncom {{professional_name}}\n\nQualquer coisa \xE9 s\xF3 chamar aqui. \u2014 {{business_name}}"
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
- body: "Oi {{customer_name}}! Passando para lembrar do seu hor\xE1rio amanh\xE3 \u{1F487}\n\n*{{service_name}}*\n{{booking_date}} \xE0s {{booking_time}}\ncom {{professional_name}}\n\nConsegue vir? Se precisar remarcar, me avisa por aqui. \u2014 {{business_name}}"
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\n*{{service_name}}*\nAgora: {{booking_date}} \xE0s {{booking_time}}\ncom {{professional_name}}\n\n\u2014 {{business_name}}"
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
- body: "Oi {{customer_name}}, seu hor\xE1rio de {{booking_date}} \xE0s {{booking_time}} foi cancelado.\n\nQuer que eu veja outra data para voc\xEA? \u2014 {{business_name}}"
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.\n\n\u2014 {{business_name}}"
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. \u2014 {{business_name}}"
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-xl border bg-card shadow-2xl mx-4 transition-all duration-200 max-h-[90vh] overflow-y-auto",
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, mode, bookingId]);
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
- return /* @__PURE__ */ jsx(Modal, { open, onOpenChange: (v) => {
1517
- if (!v) onClose();
1518
- }, children: /* @__PURE__ */ jsxs(ModalContent, { size: "lg", noPadding: true, children: [
1519
- /* @__PURE__ */ jsxs("div", { className: "px-5 pt-4 pb-3 shrink-0 space-y-3", children: [
1520
- /* @__PURE__ */ jsx(ModalTitle, { className: "sr-only", children: mode === "create" ? t2("agenda.appointment.new") : t2("agenda.appointment.edit") }),
1521
- showTabs && mode === "create" ? /* @__PURE__ */ jsx("div", { className: "flex gap-1 border-b fayz-glass-divider", children: config.bookingTypes.map((bt) => {
1522
- const label = t2(`agenda.bookingType.${bt.id}`) || bt.label;
1523
- const isActive = bookingType === bt.id;
1524
- const BtIcon = BOOKING_TYPE_ICONS[bt.icon] ?? Calendar;
1525
- return /* @__PURE__ */ jsxs(
1526
- "button",
1527
- {
1528
- type: "button",
1529
- onClick: () => setBookingType(bt.id),
1530
- 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"}`,
1531
- children: [
1532
- /* @__PURE__ */ jsx(BtIcon, { className: "h-3.5 w-3.5" }),
1533
- label,
1534
- isActive && /* @__PURE__ */ jsx("span", { className: "absolute bottom-0 left-0 right-0 h-0.5 rounded-full bg-foreground" })
1535
- ]
1536
- },
1537
- bt.id
1538
- );
1539
- }) }) : mode === "edit" && (config.clientEntityDef && clientId || config.modules.financial && editOrderId ? /* @__PURE__ */ jsx("div", { className: "flex gap-1 border-b fayz-glass-divider", children: [
1540
- { id: "appointment", label: t2(`agenda.bookingType.${bookingType}`) || activeType.label, icon: Calendar, show: true },
1541
- { id: "client", label: t2("agenda.appointment.clientTab"), icon: User, show: !!config.clientEntityDef && !!clientId },
1542
- { id: "financial", label: t2("agenda.payment.title"), icon: DollarSign, show: config.modules.financial && !!editOrderId }
1543
- ].filter((tab) => tab.show).map((tab) => {
1544
- const isActive = modalTab === tab.id;
1545
- return /* @__PURE__ */ jsxs(
1546
- "button",
1547
- {
1548
- type: "button",
1549
- onClick: () => setModalTab(tab.id),
1550
- 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"}`,
1551
- children: [
1552
- /* @__PURE__ */ jsx(tab.icon, { className: "h-3.5 w-3.5" }),
1553
- tab.label,
1554
- tab.id === "financial" && editPaymentStatus === "paid" && /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-success" }),
1555
- 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" }),
1556
- isActive && /* @__PURE__ */ jsx("span", { className: "absolute bottom-0 left-0 right-0 h-0.5 rounded-full bg-foreground" })
1557
- ]
1558
- },
1559
- tab.id
1560
- );
1561
- }) }) : /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold", children: t2(`agenda.bookingType.${bookingType}`) || activeType.label })),
1562
- modalTab === "appointment" && /* @__PURE__ */ jsxs(Fragment, { children: [
1563
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
1564
- /* @__PURE__ */ jsx(Clock, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
1565
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-sm flex-1 min-w-0", children: [
1566
- editingDate ? /* @__PURE__ */ jsx(
1567
- "input",
1568
- {
1569
- type: "date",
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: "text",
1695
- value: title,
1696
- onChange: (e) => setTitle(e.target.value),
1697
- placeholder: t2("agenda.appointment.addTitle"),
1698
- autoFocus: mode === "create",
1699
- 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"
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
- activeType.fields.client && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1", children: [
1704
- /* @__PURE__ */ jsx(User, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
1705
- /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx(
1706
- ContactPicker,
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
- value: clientId || clientSearch ? { id: clientId || void 0, name: clientSearch, phone: clientPhone } : null,
1709
- onChange: (contact) => {
1710
- setClientId(contact?.id ?? "");
1711
- setClientSearch(contact?.name ?? "");
1712
- setClientPhone(contact?.phone ?? "");
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
- kind: config.clientKind,
1715
- extensionTable: "clients",
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
- value: locationId,
1730
- onChange: setLocationId,
1731
- locations: storeLocations.length > 0 ? storeLocations : config.locations
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
- activeType.fields.services && /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 py-1", children: [
1736
- /* @__PURE__ */ jsx(Briefcase, { className: "h-4 w-4 mt-1.5 text-muted-foreground shrink-0" }),
1737
- /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
1738
- 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: [
1739
- /* @__PURE__ */ jsx("span", { className: "flex-1 truncate min-w-0", children: svc.name }),
1740
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 shrink-0 ml-3", children: [
1741
- /* @__PURE__ */ jsx(
1742
- InlineNumber,
1743
- {
1744
- value: svc.durationMinutes,
1745
- onChange: (v) => setServices((p) => p.map((s, i) => i === idx ? { ...s, durationMinutes: Math.round(v) } : s)),
1746
- min: 5,
1747
- step: 5,
1748
- className: "w-10 shrink-0",
1749
- disabled: isPaidBooking,
1750
- tooltip: t2("agenda.appointment.duration")
1751
- }
1752
- ),
1753
- /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: "min" })
1754
- ] }),
1755
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 shrink-0 ml-3", children: [
1756
- /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: currency.symbol }),
1757
- /* @__PURE__ */ jsx(
1758
- InlineNumber,
1759
- {
1760
- value: svc.price,
1761
- onChange: (v) => setServices((p) => p.map((s, i) => i === idx ? { ...s, price: v } : s)),
1762
- min: 0,
1763
- step: 0.01,
1764
- className: "w-14 shrink-0",
1765
- disabled: isPaidBooking,
1766
- tooltip: t2("agenda.appointment.price")
1767
- }
1768
- )
1769
- ] }),
1770
- !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" }) })
1771
- ] }, idx)) }),
1772
- !isPaidBooking && /* @__PURE__ */ jsx(
1773
- SearchCombobox,
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
- value: serviceSearch,
1776
- onChange: setServiceSearch,
1777
- onSelect: addService,
1778
- options: serviceOptions,
1779
- placeholder: t2("agenda.appointment.searchService"),
1780
- onBlurEmpty: () => {
1781
- },
1782
- onFocus: () => {
1783
- if (!serviceSearch && serviceOptions.length === 0) setServiceOptions(initialServiceOptions);
1784
- },
1785
- renderRight: (opt) => {
1786
- const dur = opt.data?.duration_minutes || 0;
1787
- const hasPrice = opt.price != null;
1788
- if (!hasPrice && !dur) return null;
1789
- return /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground text-xs whitespace-nowrap", children: [
1790
- hasPrice && fmtCurrency(opt.price, currency.locale, currency.code),
1791
- hasPrice && dur > 0 && " \xB7 ",
1792
- dur > 0 && `${dur}min`
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
- minimal: true,
1796
- inlineLabel: services.length > 0 ? t2("agenda.appointment.addService") : t2("agenda.appointment.addServiceFirst"),
1797
- inlineIcon: services.length > 0 ? "plus" : "search"
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
- activeType.fields.status && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1", children: [
1803
- /* @__PURE__ */ jsx(CircleDot, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
1804
- /* @__PURE__ */ jsx(StatusSelect, { value: status, onChange: setStatus, statuses: config.statuses, bookingDate: date })
1805
- ] }),
1806
- needsCancellationReason && /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 py-1", children: [
1807
- /* @__PURE__ */ jsx(Ban, { className: "h-4 w-4 mt-2 text-muted-foreground shrink-0" }),
1808
- /* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-2", children: [
1809
- /* @__PURE__ */ jsxs(
1810
- "select",
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
- value: cancellationReasonId,
1813
- onChange: (event) => setCancellationReasonId(event.target.value),
1814
- disabled: cancellationReasonsLoading,
1815
- 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",
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: cancellationNotes,
1829
- onChange: (event) => setCancellationNotes(event.target.value),
1830
- placeholder: needsCancellationNotes ? t2("agenda.cancellation.notesRequired") : t2("agenda.cancellation.notesOptional"),
1919
+ value: notes,
1920
+ onChange: (e) => setNotes(e.target.value),
1921
+ placeholder: t2("agenda.appointment.addDescription"),
1831
1922
  rows: 2,
1832
- 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"
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 gap-2", children: [
1919
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: onClose, className: "hidden sm:inline-flex", children: t2("agenda.appointment.cancel") }),
1920
- /* @__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") })
1921
- ] })
1922
- ] }) })
1923
- ] }),
1924
- 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: /* @__PURE__ */ jsx("div", { className: "flex items-center", children: /* @__PURE__ */ jsxs(
1925
- "button",
1926
- {
1927
- type: "button",
1928
- onClick: () => {
1929
- setModalTab("appointment");
1930
- setClientEditing(false);
1931
- },
1932
- className: "inline-flex items-center gap-1 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors",
1933
- children: [
1934
- /* @__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" }) }),
1935
- t2("agenda.payment.backToAppointment")
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 AgendaMobileList({ bookings, statusColors, currencyCode, onSelect }) {
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__ */ jsx("div", { className: "flex-1 flex flex-col items-center justify-center px-6 py-16 text-center text-muted-foreground", children: /* @__PURE__ */ jsx("p", { className: "text-sm", children: t2("agenda.list.empty") }) });
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-lg border bg-card px-3 py-2.5 text-left shadow-sm transition-colors active:bg-muted/50",
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-14 md:px-4 h-12", children: [
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("h1", { className: "text-base sm:text-lg font-normal truncate", children: isMobile && !mobileGridActive ? t2("agenda.calendar.viewAgenda") : calendarTitle })
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
- title: "Agenda Settings",
3188
- className: "ml-1",
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
- className: `shrink-0 rounded-md px-3 py-1 text-sm transition-colors ${mobileView === opt.key ? "bg-primary/10 text-primary font-medium" : "text-muted-foreground hover:bg-muted/50"}`,
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 ? /* @__PURE__ */ jsx(
3205
- AgendaMobileList,
3206
- {
3207
- bookings,
3208
- statusColors,
3209
- currencyCode: config.currency.code,
3210
- onSelect: (b) => openModal("edit", { bookingId: b.id })
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 { data, error } = await core.from("people").select("id, name, avatar_url, is_active").eq("kind", professionalKind).eq("is_active", true).order("name");
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
- -- Upsert key for public customers (per tenant, by normalized phone)
4665
- CREATE UNIQUE INDEX IF NOT EXISTS people_tenant_customer_phone
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
- -- upsert the customer by (tenant, phone) \u2014 matches people_tenant_customer_phone
4887
- INSERT INTO people (tenant_id, kind, name, phone, email, metadata)
4888
- VALUES (p_tenant_id, 'customer', trim(p_name), v_phone, p_email,
4889
- jsonb_build_object('source', 'public_booking'))
4890
- ON CONFLICT (tenant_id, phone) WHERE kind = 'customer' AND phone IS NOT NULL
4891
- DO UPDATE SET
4892
- name = EXCLUDED.name,
4893
- email = COALESCE(EXCLUDED.email, people.email),
4894
- updated_at = now()
4895
- RETURNING id INTO v_person_id;
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("v_staff").select("commission_rate").eq("id", order.assignee_id).single();
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-md border bg-card", children: [
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(),