@fayz-ai/plugin-agenda 0.11.0 → 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/index.d.ts.map +1 -1
- package/dist/index.js +165 -46
- 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/migrations/index.d.ts +7 -3
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/views/CalendarView.d.ts.map +1 -1
- package/package.json +7 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-financial-bridge.d.ts","sourceRoot":"","sources":["../../src/bridges/create-financial-bridge.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAA;AACtE,OAAO,KAAK,EAAE,qBAAqB,EAAqE,MAAM,oBAAoB,CAAA;AAGlI;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAKlF;AAeD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,qBAAqB,
|
|
1
|
+
{"version":3,"file":"create-financial-bridge.d.ts","sourceRoot":"","sources":["../../src/bridges/create-financial-bridge.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAA;AACtE,OAAO,KAAK,EAAE,qBAAqB,EAAqE,MAAM,oBAAoB,CAAA;AAGlI;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAKlF;AAeD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,qBAAqB,CA4O5F"}
|
|
@@ -5,6 +5,15 @@ export interface AgendaMobileListProps {
|
|
|
5
5
|
statusColors: Record<string, string>;
|
|
6
6
|
currencyCode: string;
|
|
7
7
|
onSelect: (booking: CalendarBooking) => void;
|
|
8
|
+
/** Lets the empty state offer the one thing the screen is for. */
|
|
9
|
+
onCreate?: () => void;
|
|
8
10
|
}
|
|
9
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Loading state for the mobile list. Shaped like the list it replaces — a day
|
|
13
|
+
* heading and a couple of rows — so the screen does not jump when real data
|
|
14
|
+
* lands. A blank pane while the query runs reads as "nothing scheduled", which
|
|
15
|
+
* is the one answer this screen must never give wrongly.
|
|
16
|
+
*/
|
|
17
|
+
export declare function AgendaMobileListSkeleton(): React.JSX.Element;
|
|
18
|
+
export declare function AgendaMobileList({ bookings, statusColors, currencyCode, onSelect, onCreate }: AgendaMobileListProps): React.JSX.Element;
|
|
10
19
|
//# sourceMappingURL=AgendaMobileList.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AgendaMobileList.d.ts","sourceRoot":"","sources":["../../src/components/AgendaMobileList.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkB,MAAM,OAAO,CAAA;AAEtC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAoB/C,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,eAAe,EAAE,CAAA;IAC3B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACpC,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"AgendaMobileList.d.ts","sourceRoot":"","sources":["../../src/components/AgendaMobileList.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkB,MAAM,OAAO,CAAA;AAEtC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAoB/C,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,eAAe,EAAE,CAAA;IAC3B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACpC,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,CAAA;IAC5C,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,sBAsBvC;AAED,wBAAgB,gBAAgB,CAAC,EAAE,QAAQ,EAAE,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,qBAAqB,qBAkFnH"}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAGnD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAA;AAiBnD,wBAAgB,kBAAkB,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAGnD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAA;AAiBnD,wBAAgB,kBAAkB,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,cAAc,CA0RhF;AAGD,YAAY,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAA;AACnD,YAAY,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AACpD,YAAY,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAGtD,OAAO,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAA;AACtD,YAAY,EAAE,cAAc,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAA;AAC5E,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAA;AAC1I,OAAO,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAA;AAGlG,OAAO,EAAE,0BAA0B,EAAE,4BAA4B,EAAE,uBAAuB,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAA;AAIvK,OAAO,EAAE,uBAAuB,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAA"}
|
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, useLookupCreate, RightRailPage, 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, ConfirmDialog, 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
|
-
|
|
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
|
}
|
|
@@ -2406,7 +2433,20 @@ function timeLabel(iso) {
|
|
|
2406
2433
|
const d = new Date(iso);
|
|
2407
2434
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
2408
2435
|
}
|
|
2409
|
-
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 }) {
|
|
2410
2450
|
const t2 = useTranslation();
|
|
2411
2451
|
const locale = getCurrentLocale() === "pt-BR" ? "pt-BR" : "en";
|
|
2412
2452
|
const groups = useMemo(() => {
|
|
@@ -2431,7 +2471,18 @@ function AgendaMobileList({ bookings, statusColors, currencyCode, onSelect }) {
|
|
|
2431
2471
|
return date.toLocaleDateString(locale, { weekday: "long", day: "numeric", month: "long" });
|
|
2432
2472
|
}
|
|
2433
2473
|
if (groups.length === 0) {
|
|
2434
|
-
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
|
+
] });
|
|
2435
2486
|
}
|
|
2436
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: [
|
|
2437
2488
|
/* @__PURE__ */ jsx("h3", { className: "px-1 pb-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: dayLabel(g.date) }),
|
|
@@ -2632,7 +2683,7 @@ function CalendarView() {
|
|
|
2632
2683
|
endTime: agendaSettings.endTime
|
|
2633
2684
|
};
|
|
2634
2685
|
const bookings = useAgendaStore((s) => s.bookings);
|
|
2635
|
-
useAgendaStore((s) => s.bookingsLoading);
|
|
2686
|
+
const bookingsLoading = useAgendaStore((s) => s.bookingsLoading);
|
|
2636
2687
|
const professionals = useAgendaStore((s) => s.professionals);
|
|
2637
2688
|
const professionalsLoading = useAgendaStore((s) => s.professionalsLoading);
|
|
2638
2689
|
const currentView = useAgendaStore((s) => s.currentView);
|
|
@@ -3178,18 +3229,37 @@ function CalendarView() {
|
|
|
3178
3229
|
}) })
|
|
3179
3230
|
] })
|
|
3180
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
|
+
) }),
|
|
3181
3259
|
/* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col min-w-0 overflow-hidden", children: [
|
|
3182
3260
|
/* @__PURE__ */ jsxs("div", { className: "shrink-0 border-b", children: [
|
|
3183
|
-
/* @__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: [
|
|
3184
3262
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [
|
|
3185
|
-
/* @__PURE__ */ jsx(
|
|
3186
|
-
"button",
|
|
3187
|
-
{
|
|
3188
|
-
onClick: () => openModal("create"),
|
|
3189
|
-
className: "md:hidden flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground",
|
|
3190
|
-
children: /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" })
|
|
3191
|
-
}
|
|
3192
|
-
),
|
|
3193
3263
|
/* @__PURE__ */ jsxs("div", { className: `${mobileGridActive ? "flex" : "hidden"} md:flex items-center gap-3`, children: [
|
|
3194
3264
|
/* @__PURE__ */ jsx(
|
|
3195
3265
|
"button",
|
|
@@ -3204,7 +3274,7 @@ function CalendarView() {
|
|
|
3204
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" }) })
|
|
3205
3275
|
] })
|
|
3206
3276
|
] }),
|
|
3207
|
-
/* @__PURE__ */ jsx("
|
|
3277
|
+
!(isMobile && !mobileGridActive) && /* @__PURE__ */ jsx("h2", { className: "truncate text-base font-normal sm:text-lg", children: calendarTitle })
|
|
3208
3278
|
] }),
|
|
3209
3279
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [
|
|
3210
3280
|
/* @__PURE__ */ jsx("div", { className: "hidden md:flex items-center gap-1", children: viewOptions.map((opt) => /* @__PURE__ */ jsx(
|
|
@@ -3222,8 +3292,9 @@ function CalendarView() {
|
|
|
3222
3292
|
variant: "outline",
|
|
3223
3293
|
size: "icon",
|
|
3224
3294
|
onClick: () => window.location.hash = "/settings/agenda",
|
|
3225
|
-
|
|
3226
|
-
|
|
3295
|
+
"aria-label": t2("agenda.settings.title"),
|
|
3296
|
+
title: t2("agenda.settings.title"),
|
|
3297
|
+
className: "ml-1 hidden md:inline-flex",
|
|
3227
3298
|
children: /* @__PURE__ */ jsx(Settings, { className: "h-4 w-4" })
|
|
3228
3299
|
}
|
|
3229
3300
|
)
|
|
@@ -3233,20 +3304,27 @@ function CalendarView() {
|
|
|
3233
3304
|
"button",
|
|
3234
3305
|
{
|
|
3235
3306
|
onClick: () => handleMobileViewChange(opt.key),
|
|
3236
|
-
|
|
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"}`,
|
|
3237
3309
|
children: t2(opt.labelKey)
|
|
3238
3310
|
},
|
|
3239
3311
|
opt.key
|
|
3240
3312
|
)) })
|
|
3241
3313
|
] }),
|
|
3242
|
-
isMobile && !mobileGridActive ?
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
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
|
+
)
|
|
3250
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(
|
|
3251
3329
|
FullCalendar,
|
|
3252
3330
|
{
|
|
@@ -3885,6 +3963,12 @@ function bookingPayload(booking) {
|
|
|
3885
3963
|
const starts = new Date(booking.startsAt);
|
|
3886
3964
|
return {
|
|
3887
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 ?? "",
|
|
3888
3972
|
service_name: (booking.services ?? []).map((s) => s.name).join(" + "),
|
|
3889
3973
|
professional_name: booking.professionalName ?? "",
|
|
3890
3974
|
booking_date: starts.toLocaleDateString("pt-BR"),
|
|
@@ -4707,8 +4791,14 @@ ALTER TABLE public.order_items
|
|
|
4707
4791
|
ADD COLUMN IF NOT EXISTS duration_minutes integer,
|
|
4708
4792
|
ADD COLUMN IF NOT EXISTS assignee_id uuid REFERENCES public.people(id) ON DELETE SET NULL;
|
|
4709
4793
|
|
|
4710
|
-
--
|
|
4711
|
-
|
|
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
|
|
4712
4802
|
ON public.people(tenant_id, phone)
|
|
4713
4803
|
WHERE kind = 'customer' AND phone IS NOT NULL;
|
|
4714
4804
|
|
|
@@ -4720,6 +4810,8 @@ CREATE INDEX IF NOT EXISTS appointments_assignee_time
|
|
|
4720
4810
|
-- \xA72 \u2014 admin read model (security_invoker \u2192 underlying RLS applies; anon sees 0 rows)
|
|
4721
4811
|
-- DROP first: 013 widens this view and CREATE OR REPLACE cannot narrow a live
|
|
4722
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');
|
|
4723
4815
|
DROP VIEW IF EXISTS public.v_appointments;
|
|
4724
4816
|
CREATE VIEW public.v_appointments
|
|
4725
4817
|
WITH (security_invoker = true) AS
|
|
@@ -4929,16 +5021,32 @@ BEGIN
|
|
|
4929
5021
|
RAISE EXCEPTION 'too many open bookings for this phone';
|
|
4930
5022
|
END IF;
|
|
4931
5023
|
|
|
4932
|
-
--
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
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;
|
|
4942
5050
|
|
|
4943
5051
|
-- order + booking + order_item (statuses hardcoded \u2014 caller cannot set)
|
|
4944
5052
|
INSERT INTO orders (tenant_id, kind, status, party_id, assignee_id, subtotal, total, notes, metadata)
|
|
@@ -4968,6 +5076,8 @@ $$;
|
|
|
4968
5076
|
REVOKE ALL ON FUNCTION public.create_public_booking(uuid, uuid, timestamptz, text, text, text, text) FROM public;
|
|
4969
5077
|
GRANT EXECUTE ON FUNCTION public.create_public_booking(uuid, uuid, timestamptz, text, text, text, text)
|
|
4970
5078
|
TO anon, authenticated, service_role;
|
|
5079
|
+
|
|
5080
|
+
SELECT app.reattach_view_dependents();
|
|
4971
5081
|
`;
|
|
4972
5082
|
function computeCommissionAmount(total, ratePercent) {
|
|
4973
5083
|
const base = Number(total) || 0;
|
|
@@ -5088,7 +5198,7 @@ function createFinancialBridge(provider2) {
|
|
|
5088
5198
|
if (!supabase) return null;
|
|
5089
5199
|
const { data: order } = await supabase.from("orders").select("id, tenant_id, assignee_id, total").eq("id", orderId).single();
|
|
5090
5200
|
if (!order || !order.assignee_id) return null;
|
|
5091
|
-
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();
|
|
5092
5202
|
const rate = Number(staff?.commission_rate) || 0;
|
|
5093
5203
|
const base = Number(order.total) || 0;
|
|
5094
5204
|
const amount = computeCommissionAmount(base, rate);
|
|
@@ -8056,6 +8166,15 @@ function createAgendaPlugin(options) {
|
|
|
8056
8166
|
// the saas/admin Panel world, not the ecommerce storefront.
|
|
8057
8167
|
scaffolds: ["saas"],
|
|
8058
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
|
+
],
|
|
8059
8178
|
dependencies: [],
|
|
8060
8179
|
// Os FATOS da agenda — só expostos enquanto o módulo está ATIVO.
|
|
8061
8180
|
events: buildAgendaDomainEvents(),
|