@fayz-ai/plugin-agenda 0.17.0 → 0.18.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.
Files changed (69) hide show
  1. package/dist/bridges/create-financial-bridge.d.ts.map +1 -1
  2. package/dist/{chunk-PDYBAVXU.js → chunk-RVO5ILXN.js} +39 -11
  3. package/dist/chunk-RVO5ILXN.js.map +1 -0
  4. package/dist/components/AgendaFilterRail.d.ts +18 -0
  5. package/dist/components/AgendaFilterRail.d.ts.map +1 -0
  6. package/dist/components/AgendaGeneralSettings.d.ts.map +1 -1
  7. package/dist/components/AgendaMobileList.d.ts.map +1 -1
  8. package/dist/components/AppointmentModal.d.ts.map +1 -1
  9. package/dist/components/AttendanceRuler.d.ts +21 -0
  10. package/dist/components/AttendanceRuler.d.ts.map +1 -0
  11. package/dist/components/BlockDialog.d.ts +26 -0
  12. package/dist/components/BlockDialog.d.ts.map +1 -0
  13. package/dist/components/BookingCard.d.ts +73 -0
  14. package/dist/components/BookingCard.d.ts.map +1 -0
  15. package/dist/components/BookingInterestsPanel.d.ts +17 -0
  16. package/dist/components/BookingInterestsPanel.d.ts.map +1 -0
  17. package/dist/components/CheckinDialog.d.ts +9 -2
  18. package/dist/components/CheckinDialog.d.ts.map +1 -1
  19. package/dist/components/CheckinScoreboard.d.ts +14 -0
  20. package/dist/components/CheckinScoreboard.d.ts.map +1 -0
  21. package/dist/components/ClientHistoryPanel.d.ts +7 -0
  22. package/dist/components/ClientHistoryPanel.d.ts.map +1 -0
  23. package/dist/components/LostSlotsCard.d.ts +10 -0
  24. package/dist/components/LostSlotsCard.d.ts.map +1 -0
  25. package/dist/components/PersonAttendanceBlock.d.ts +18 -0
  26. package/dist/components/PersonAttendanceBlock.d.ts.map +1 -0
  27. package/dist/components/PersonAttendancesWidget.d.ts +7 -0
  28. package/dist/components/PersonAttendancesWidget.d.ts.map +1 -0
  29. package/dist/components/PersonServicesWidget.d.ts +5 -0
  30. package/dist/components/PersonServicesWidget.d.ts.map +1 -0
  31. package/dist/components/ScheduleGridDialog.d.ts +9 -0
  32. package/dist/components/ScheduleGridDialog.d.ts.map +1 -0
  33. package/dist/config.d.ts +7 -0
  34. package/dist/config.d.ts.map +1 -1
  35. package/dist/data/mock.d.ts.map +1 -1
  36. package/dist/data/registries.d.ts.map +1 -1
  37. package/dist/data/supabase.d.ts.map +1 -1
  38. package/dist/data/types.d.ts +394 -2
  39. package/dist/data/types.d.ts.map +1 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +7536 -1694
  42. package/dist/index.js.map +1 -1
  43. package/dist/lib/phone.d.ts +11 -0
  44. package/dist/lib/phone.d.ts.map +1 -0
  45. package/dist/lib/professional-scope.d.ts +41 -0
  46. package/dist/lib/professional-scope.d.ts.map +1 -0
  47. package/dist/lib/schedule-engine.d.ts +46 -0
  48. package/dist/lib/schedule-engine.d.ts.map +1 -0
  49. package/dist/locales/en.d.ts.map +1 -1
  50. package/dist/locales/pt-BR.d.ts.map +1 -1
  51. package/dist/migrations/index.d.ts +5 -0
  52. package/dist/migrations/index.d.ts.map +1 -1
  53. package/dist/public/createPublicBookingPlugin.d.ts.map +1 -1
  54. package/dist/public/index.js +3 -4
  55. package/dist/public/index.js.map +1 -1
  56. package/dist/store.d.ts +44 -3
  57. package/dist/store.d.ts.map +1 -1
  58. package/dist/styles/calendar-styles.d.ts.map +1 -1
  59. package/dist/types.d.ts +164 -2
  60. package/dist/types.d.ts.map +1 -1
  61. package/dist/views/AgendaPage.d.ts +11 -1
  62. package/dist/views/AgendaPage.d.ts.map +1 -1
  63. package/dist/views/CalendarView.d.ts +0 -16
  64. package/dist/views/CalendarView.d.ts.map +1 -1
  65. package/dist/views/CheckinView.d.ts +3 -1
  66. package/dist/views/CheckinView.d.ts.map +1 -1
  67. package/dist/views/ConfirmationsView.d.ts.map +1 -1
  68. package/package.json +10 -9
  69. package/dist/chunk-PDYBAVXU.js.map +0 -1
@@ -0,0 +1,11 @@
1
+ /** Só dígitos, com o país na frente — o formato que o `wa.me` e o `tel:` pedem. */
2
+ export declare function whatsappNumber(raw: string): string;
3
+ /**
4
+ * O número como se lê em voz alta, SEM o país.
5
+ *
6
+ * "+55 (21) 99788-1675" num botão de 180px empurra o DDD para fora e gasta
7
+ * quatro caracteres dizendo o que toda linha desta tela tem em comum. Quem
8
+ * confere um telefone confere o DDD e o final; o +55 é do `tel:`, não do olho.
9
+ */
10
+ export declare function phoneDisplay(raw: string | null | undefined): string;
11
+ //# sourceMappingURL=phone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"phone.d.ts","sourceRoot":"","sources":["../../src/lib/phone.ts"],"names":[],"mappings":"AAWA,mFAAmF;AACnF,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAKlD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAGnE"}
@@ -0,0 +1,41 @@
1
+ export interface ScopedPerson {
2
+ id: string;
3
+ name: string;
4
+ /**
5
+ * AS UNIDADES em que a pessoa atende — plural, e é isso que a regra errava.
6
+ *
7
+ * O tipo dizia `locationId: string | null`, uma unidade só, e o mapeador
8
+ * gravava `null` em toda linha porque a fonte (`v_bookable_people.unit_ids`)
9
+ * é um CONJUNTO: quem atende na Tijuca e no Leme está nas duas. Com todo
10
+ * mundo em `null`, a regra abaixo lia "sem unidade" para o quadro inteiro e
11
+ * nunca estreitava nada — o filtro existia e não filtrava.
12
+ *
13
+ * `null` continua querendo dizer A REDE INTEIRA, que é a semântica de
14
+ * `public.scoped_resources` para pessoa: quem não foi amarrado a uma unidade
15
+ * atende em qualquer uma.
16
+ */
17
+ unitIds?: string[] | null;
18
+ avatarUrl?: string | null;
19
+ }
20
+ /**
21
+ * A lista que o seletor mostra.
22
+ *
23
+ * Três regras, nesta ordem:
24
+ *
25
+ * sem local escolhido todo mundo — não há o que estreitar
26
+ * sem unidade na ficha FICA. Ausência de unidade não é "não atende aqui";
27
+ * 117 dos 299 agendáveis do Espaço Facial estão assim,
28
+ * e sumir com eles trocaria um erro raro por uma lista
29
+ * vazia.
30
+ * já escolhido NUNCA sai, mesmo fora do local: o valor está gravado,
31
+ * e ausência no seletor não pode virar ausência na tela.
32
+ */
33
+ export declare function professionalsForLocation<T extends ScopedPerson>(people: T[], locationId: string | null | undefined, selectedId?: string | null): T[];
34
+ /**
35
+ * O campo de profissional deve se esvaziar ao trocar de local?
36
+ *
37
+ * Só quando a pessoa TEM unidade e ela é outra. Manter o nome ali afirmaria que
38
+ * ela atende num lugar onde não está.
39
+ */
40
+ export declare function shouldClearProfessional<T extends ScopedPerson>(people: T[], locationId: string | null | undefined, selectedId: string | null | undefined): T | null;
41
+ //# sourceMappingURL=professional-scope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"professional-scope.d.ts","sourceRoot":"","sources":["../../src/lib/professional-scope.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,SAAS,YAAY,EAC7D,MAAM,EAAE,CAAC,EAAE,EACX,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACrC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,GACzB,CAAC,EAAE,CAKL;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,YAAY,EAC5D,MAAM,EAAE,CAAC,EAAE,EACX,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACrC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACpC,CAAC,GAAG,IAAI,CAKV"}
@@ -0,0 +1,46 @@
1
+ export interface GridRow {
2
+ dayOfWeek?: number | null;
3
+ specificDate?: string | null;
4
+ gridKind?: string | null;
5
+ weekInterval?: number | null;
6
+ validFrom?: string | null;
7
+ validTo?: string | null;
8
+ isActive?: boolean;
9
+ }
10
+ /**
11
+ * Esta grade vale no dia `iso`?
12
+ *
13
+ * Comparação de TEXTO: `YYYY-MM-DD` ordena igual como string, e
14
+ * `new Date('2025-04-30')` é meia-noite UTC — no Brasil, o dia anterior. Uma
15
+ * vigência que começa no dia 30 abriria no dia 29.
16
+ *
17
+ * Ponta ausente é ponta aberta, como o V1 escreve ("Vazio = sem data final").
18
+ */
19
+ export declare function gridAppliesOn(grid: Pick<GridRow, 'validFrom' | 'validTo'>, iso: string): boolean;
20
+ /**
21
+ * Esta é "semana sim"?
22
+ *
23
+ * Espelho de `public.agenda_week_parity_active`. A âncora é a primeira
24
+ * ocorrência DAQUELE weekday dentro da vigência — por isso o desconto de uma
25
+ * semana quando o weekday cai antes do dia em que a vigência começou: uma
26
+ * grade que começa numa quarta e define segunda só tem a primeira segunda na
27
+ * semana seguinte.
28
+ *
29
+ * Sem `validFrom` não há âncora, e sem âncora não há paridade: vale sempre.
30
+ */
31
+ export declare function weekParityActive(validFrom: string | null | undefined, iso: string, weekInterval: number | null | undefined): boolean;
32
+ /**
33
+ * As janelas efetivas de UM profissional no dia `iso`.
34
+ *
35
+ * Espelho de `public.agenda_working_windows`, na mesma ordem de decisão:
36
+ *
37
+ * 1. fora da vigência não participa de nada — nem como exceção que fecha;
38
+ * 2. a exceção de DATA manda em tudo, e conta MESMO INATIVA: é assim que o
39
+ * "dia de folga" apaga a semana em vez de deixar a padrão voltar;
40
+ * 3. a grade de exceção que define este weekday tira a padrão do dia,
41
+ * mesmo quando a paridade dela pula a semana.
42
+ */
43
+ export declare function effectiveWindows<T extends GridRow>(rows: T[], iso: string): T[];
44
+ /** Atalho para a pergunta que a coluna faz: "esta pessoa abre neste dia?". */
45
+ export declare function opensOnDate(rows: GridRow[], iso: string): boolean;
46
+ //# sourceMappingURL=schedule-engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule-engine.d.ts","sourceRoot":"","sources":["../../src/lib/schedule-engine.ts"],"names":[],"mappings":"AA8BA,MAAM,WAAW,OAAO;IACtB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAcD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,SAAS,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAKhG;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACpC,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACtC,OAAO,CAQT;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,EAAE,CAsB/E;AAED,8EAA8E;AAC9E,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAEjE"}
@@ -1 +1 @@
1
- {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAkOrC,CAAA"}
1
+ {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAkRrC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"pt-BR.d.ts","sourceRoot":"","sources":["../../src/locales/pt-BR.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAkOvC,CAAA"}
1
+ {"version":3,"file":"pt-BR.d.ts","sourceRoot":"","sources":["../../src/locales/pt-BR.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAkRvC,CAAA"}
@@ -8,6 +8,11 @@ export declare const MIGRATION_006_THE_AGENDA_STOPS_PRICING_EVERY_INVOICE = "--
8
8
  export declare const MIGRATION_007_O_CASCADE_LEVOU_A_AGENDA_JUNTO = "-- O CASCADE levou a agenda junto.\n--\n-- Sintoma: a tela de check-in (`#/agenda/checkin`) demora e n\u00E3o abre. A causa\n-- n\u00E3o est\u00E1 na agenda \u2014 est\u00E1 numa migration do FINANCEIRO, escrita tr\u00EAs dias\n-- antes.\n--\n-- As migrations 044 e 045 do plugin-financial come\u00E7am com:\n--\n-- DROP VIEW IF EXISTS public.v_financial_invoice_balances CASCADE;\n--\n-- e recriam, logo abaixo, `v_financial_invoice_balances` e `v_invoice_balances`\n-- \u2014 as duas que o autor (eu) sabia que dependiam. O CASCADE, por\u00E9m, n\u00E3o se\n-- limita ao que o autor conhece: ele derruba a \u00E1rvore inteira. Debaixo de\n-- `v_invoice_balances` moravam mais seis views, de dois plugins:\n--\n-- v_appointments (agenda \u2014 o read model do agendamento)\n-- \u251C\u2500\u2500 rep_appointments_by_period\n-- \u2502 \u2514\u2500\u2500 plg_agenda_rep_bookings\n-- \u251C\u2500\u2500 rep_confirmation_queue\n-- \u251C\u2500\u2500 rep_revenue_by_professional\n-- \u2514\u2500\u2500 rep_revenue_by_service\n-- plg_financial_rep_receivables (financeiro \u2014 restaurada na 053 de l\u00E1)\n--\n-- `v_appointments` l\u00EA `v_invoice_balances` desde a migration 006 desta corrente\n-- (o LATERAL que fez a agenda parar de apurar 8.134 faturas por agendamento).\n-- Essa depend\u00EAncia foi o conserto de um problema real e continua certa; o que\n-- ela criou, sem ningu\u00E9m notar, foi uma aresta do financeiro para a agenda \u2014\n-- e \u00E9 por ela que o CASCADE passou.\n--\n-- Nada gritou. O `DROP` n\u00E3o avisa o que levou, o `apply` das duas migrations\n-- passou verde, a lista de contas a pagar (o que eu estava consertando) ficou\n-- correta, e a agenda s\u00F3 falha quando algu\u00E9m a abre. O erro que chega ao\n-- navegador \u00E9 um 404 do PostgREST sobre uma rela\u00E7\u00E3o que o c\u00F3digo tem certeza\n-- de que existe \u2014 pista que aponta para o lugar errado.\n--\n-- \u2500\u2500 O que esta migration faz \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Recria as cinco views da agenda, com o corpo EXATO em que estavam: o de\n-- `v_appointments` vem da migration 006 (a forma com LATERAL, n\u00E3o a do\n-- baseline), e os quatro relat\u00F3rios v\u00EAm do baseline, que n\u00E3o os alterou desde\n-- ent\u00E3o. Nenhuma coluna muda, nenhuma regra muda \u2014 o que muda \u00E9 existirem.\n--\n-- `WITH (security_invoker = true)` expl\u00EDcito em todas: uma view recriada n\u00E3o\n-- herda op\u00E7\u00E3o nenhuma, e sem isso ela passa a rodar com os direitos do dono \u2014\n-- a RLS de quem l\u00EA deixa de valer. Os GRANTs tamb\u00E9m s\u00E3o reescritos, porque\n-- morreram com as views.\n--\n-- \u2500\u2500 A li\u00E7\u00E3o, para a pr\u00F3xima \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Antes de um `DROP \u2026 CASCADE`, pergunte ao banco quem depende:\n--\n-- SELECT DISTINCT dependente.relname\n-- FROM pg_depend d\n-- JOIN pg_rewrite r ON r.oid = d.objid\n-- JOIN pg_class dependente ON dependente.oid = r.ev_class\n-- WHERE d.refobjid = 'public.v_financial_invoice_balances'::regclass\n-- AND dependente.relname <> 'v_financial_invoice_balances';\n--\n-- A lista que voltar \u00E9 a lista que a migration tem que recriar. E depois de\n-- aplicar, o diff entre as views que o c\u00F3digo declara e as que o banco tem \u00E9 o\n-- que pega o que escapou \u2014 foi ele que encontrou estas seis.\n--\n-- Idempotente.\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 1. v_appointments \u2014 corpo da migration 006 (LATERAL sobre v_invoice_balances)\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE OR REPLACE VIEW public.v_appointments WITH (security_invoker = true) AS\n SELECT b.id,\n b.tenant_id,\n b.kind,\n b.starts_at,\n b.ends_at,\n b.status,\n b.notes,\n b.order_id,\n b.location_id,\n b.metadata,\n b.created_at,\n b.updated_at,\n b.party_id AS client_id,\n COALESCE(pc.name, b.metadata ->> 'title'::text) AS client_name,\n pc.phone AS client_phone,\n pc.email AS client_email,\n pc.avatar_url AS client_avatar_url,\n b.assignee_id AS professional_id,\n ps.name AS professional_name,\n ps.avatar_url AS professional_avatar_url,\n l.name AS location_name,\n o.total AS order_total,\n o.status AS order_status,\n EXTRACT(epoch FROM COALESCE(b.ends_at, b.starts_at) - b.starts_at)::integer / 60 AS total_duration_minutes,\n o.reference_number,\n o.status AS stage,\n o.metadata ->> 'direction'::text AS direction,\n ( SELECT json_agg(json_build_object('id', oi.id, 'serviceId', oi.product_id, 'name', oi.name, 'durationMinutes', oi.duration_minutes, 'price', oi.unit_price, 'assigneeId', oi.assignee_id) ORDER BY oi.sort_order) AS json_agg\n FROM items oi\n WHERE oi.order_id = b.order_id) AS services,\n inv.status AS payment_status,\n inv.paid AS order_paid,\n inv.balance AS order_balance\n FROM appointments b\n LEFT JOIN people pc ON pc.id = b.party_id\n LEFT JOIN people ps ON ps.id = b.assignee_id\n LEFT JOIN app.units l ON l.id = b.location_id\n LEFT JOIN orders o ON o.id = b.order_id\n LEFT JOIN LATERAL (\n SELECT vb.status, vb.paid, vb.balance\n FROM v_invoice_balances vb\n WHERE vb.invoice_id = b.order_id\n LIMIT 1\n ) inv ON true;\n\nCOMMENT ON VIEW public.v_appointments IS\n 'O read model do agendamento: a linha do calend\u00E1rio com cliente, '\n 'profissional, unidade, itens e o estado de pagamento da Venda. L\u00EA '\n 'v_invoice_balances por LATERAL para que o filtro des\u00E7a por baixo do '\n 'agregado \u2014 como JOIN, a agenda apurava as faturas do tenant inteiro por '\n 'agendamento.';\n\nGRANT SELECT ON TABLE public.v_appointments TO authenticated, service_role;\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 2. Os quatro relat\u00F3rios que penduram nela, e o quinto que pendura no primeiro\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE OR REPLACE VIEW public.rep_appointments_by_period WITH (security_invoker = true) AS\n SELECT tenant_id,\n id AS booking_id,\n (starts_at)::date AS date,\n starts_at,\n client_id,\n client_name,\n professional_id,\n professional_name,\n COALESCE(((services -> 0) ->> 'name'::text), ((services -> 0) ->> 'serviceName'::text)) AS service_name,\n status,\n order_total AS revenue,\n created_at,\n updated_at\n FROM public.v_appointments b;\n\nGRANT SELECT ON TABLE public.rep_appointments_by_period TO authenticated, service_role;\n\nCREATE OR REPLACE VIEW public.plg_agenda_rep_bookings WITH (security_invoker = true) AS\n SELECT tenant_id,\n booking_id,\n date,\n starts_at,\n client_id,\n client_name,\n professional_id,\n professional_name,\n service_name,\n status,\n revenue AS booked_revenue,\n CASE\n WHEN (status = ANY (ARRAY['cancelled'::text, 'no_show'::text])) THEN (0)::numeric\n ELSE COALESCE(revenue, (0)::numeric)\n END AS effective_revenue,\n CASE\n WHEN (status = ANY (ARRAY['cancelled'::text, 'no_show'::text])) THEN COALESCE(revenue, (0)::numeric)\n ELSE (0)::numeric\n END AS lost_revenue,\n CASE\n WHEN (status = 'completed'::text) THEN 1\n ELSE 0\n END AS is_completed,\n CASE\n WHEN (status = 'cancelled'::text) THEN 1\n ELSE 0\n END AS is_cancelled,\n CASE\n WHEN (status = 'no_show'::text) THEN 1\n ELSE 0\n END AS is_no_show,\n created_at,\n updated_at\n FROM public.rep_appointments_by_period b;\n\nGRANT SELECT ON TABLE public.plg_agenda_rep_bookings TO authenticated, service_role;\n\nCREATE OR REPLACE VIEW public.rep_confirmation_queue WITH (security_invoker = true) AS\n SELECT tenant_id,\n id AS booking_id,\n (starts_at)::date AS date,\n starts_at,\n client_id,\n client_name,\n professional_id,\n professional_name,\n COALESCE(((services -> 0) ->> 'name'::text), ((services -> 0) ->> 'serviceName'::text)) AS service_name,\n 'pending'::text AS confirmation_status,\n NULL::text AS confirmation_channel,\n NULL::timestamp with time zone AS confirmation_sent_at,\n NULL::timestamp with time zone AS confirmed_at,\n created_at,\n updated_at\n FROM public.v_appointments b\n WHERE ((status <> ALL (ARRAY['cancelled'::text, 'no_show'::text])) AND (starts_at >= (now() - '1 day'::interval)));\n\nGRANT SELECT ON TABLE public.rep_confirmation_queue TO authenticated, service_role;\n\nCREATE OR REPLACE VIEW public.rep_revenue_by_professional WITH (security_invoker = true) AS\n SELECT tenant_id,\n (starts_at)::date AS date,\n professional_id,\n COALESCE(professional_name, 'Sem profissional'::text) AS professional_name,\n (count(*))::integer AS appointment_count,\n (COALESCE(sum(NULLIF(order_total, (0)::numeric)), (0)::numeric))::numeric(14,2) AS total_revenue,\n (COALESCE(avg(NULLIF(order_total, (0)::numeric)), (0)::numeric))::numeric(14,2) AS avg_ticket,\n (0)::numeric(14,2) AS commission,\n max(updated_at) AS updated_at\n FROM public.v_appointments b\n WHERE (status <> ALL (ARRAY['cancelled'::text, 'no_show'::text]))\n GROUP BY tenant_id, ((starts_at)::date), professional_id, COALESCE(professional_name, 'Sem profissional'::text);\n\nGRANT SELECT ON TABLE public.rep_revenue_by_professional TO authenticated, service_role;\n\nCREATE OR REPLACE VIEW public.rep_revenue_by_service WITH (security_invoker = true) AS\n SELECT b.tenant_id,\n (b.starts_at)::date AS date,\n NULLIF((service_item.value ->> 'serviceId'::text), ''::text) AS service_id,\n COALESCE((service_item.value ->> 'name'::text), (service_item.value ->> 'serviceName'::text), 'Sem servico'::text) AS service_name,\n (count(DISTINCT b.id))::integer AS quantity,\n (COALESCE(sum((NULLIF((service_item.value ->> 'price'::text), ''::text))::numeric), (0)::numeric))::numeric(14,2) AS total_revenue,\n (COALESCE(avg((NULLIF((service_item.value ->> 'price'::text), ''::text))::numeric), (0)::numeric))::numeric(14,2) AS avg_ticket,\n max(b.updated_at) AS updated_at\n FROM (public.v_appointments b\n CROSS JOIN LATERAL jsonb_array_elements(COALESCE((b.services)::jsonb, '[]'::jsonb)) service_item(value))\n WHERE (b.status <> ALL (ARRAY['cancelled'::text, 'no_show'::text]))\n GROUP BY b.tenant_id, ((b.starts_at)::date), NULLIF((service_item.value ->> 'serviceId'::text), ''::text), COALESCE((service_item.value ->> 'name'::text), (service_item.value ->> 'serviceName'::text), 'Sem servico'::text);\n\nGRANT SELECT ON TABLE public.rep_revenue_by_service TO authenticated, service_role;\n";
9
9
  export declare const MIGRATION_008_A_AGENDA_DO_V1_ATRAVESSA = "-- A agenda do V1 atravessa.\n--\n-- 354.206 agendamentos e 314.310 servi\u00E7os agendados do Espa\u00E7o Facial n\u00E3o tinham\n-- para onde ir: `appointments` e `appointment_services` n\u00E3o estavam nem na\n-- `migration.allowlist` nem na `migration.excluded_tables`. N\u00E3o era uma decis\u00E3o\n-- de deixar de fora \u2014 era a aus\u00EAncia de decis\u00E3o, e a tela de check-in abria\n-- vazia por isso.\n--\n-- \u2500\u2500 O de-para \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- V1 appointments \u2192 public.appointments\n-- V1 appointment_services \u2192 public.appointment_items\n--\n-- Tr\u00EAs refer\u00EAncias o ledger resolve, porque as tr\u00EAs j\u00E1 atravessaram:\n-- `client_id` \u2192 `public.people` (123.304 linhas), `professional_id` \u2192\n-- `public.people` (300) e `service_id` \u2192 `public.products` (122).\n--\n-- \u2500\u2500 A unidade, que o V1 n\u00E3o guarda no agendamento \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `public.appointments` do V2 quer `unit_id` \u2014 \u00E9 por ela que a rede de 63\n-- unidades filtra a agenda. O V1 n\u00E3o tem essa coluna: ele guarda\n-- `service_location_id`, a SALA, e \u00E9 a sala que sabe a unidade\n-- (`service_locations.unit_id`, um uuid que casa com `companies.uuid`).\n--\n-- A ponte \u00E9 feita no extrator (`augment` do Step), n\u00E3o aqui, porque ela \u00E9\n-- conhecimento da ORIGEM. Medido em 19/09/2026, tenant 1:\n--\n-- agendamentos 354.206\n-- com sala que resolve a unidade 352.315 (99,5%)\n-- sem sala nenhuma 1.891\n--\n-- Os 1.891 sem sala entram com `unit_id` NULL \u2014 \u00E9 o que o V1 tem, e inventar\n-- unidade para eles seria coloc\u00E1-los na agenda de algu\u00E9m.\n--\n-- \u2500\u2500 O estado \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- O vocabul\u00E1rio do V1 tem sete palavras; o de `appointment_kinds.appointment`\n-- tem seis. Cinco s\u00E3o a mesma palavra. Sobram duas, e o mapa \u00E9 declarado no\n-- `writer_args` \u2014 dado, n\u00E3o c\u00F3digo:\n--\n-- unmarked \u2192 cancelled desmarcado \u00E9 cancelado; o V2 n\u00E3o tem a terceira\n-- palavra, e 'unmarked' n\u00E3o \u00E9 um estado, \u00E9 a\n-- aus\u00EAncia de um.\n-- waiting \u2192 confirmed a cliente chegou e espera. O V2 diz isso com\n-- `checked_in_at` (migration 003 desta corrente),\n-- que o pr\u00F3prio V1 j\u00E1 preenche \u2014 ent\u00E3o a palavra\n-- vira 'confirmed' e o REL\u00D3GIO carrega o resto.\n--\n-- O original fica em `metadata.migration.source_status`, como em toda tradu\u00E7\u00E3o\n-- de vocabul\u00E1rio deste motor.\n--\n-- \u2500\u2500 O que N\u00C3O vem \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- O agendamento do V1 n\u00E3o tem Venda. `order_id` fica NULL, e \u00E9 por isso que\n-- `v_appointments` traz `order_total`/`payment_status` vazios para o que\n-- migrou: o financeiro do V1 chega pela fatura (`invoices`), que \u00E9 outra\n-- tabela e outro caminho. Costurar as duas \u00E9 trabalho de reconcilia\u00E7\u00E3o, n\u00E3o de\n-- transporte, e faz\u00EA-lo aqui inventaria uma Venda que n\u00E3o existe.\n--\n-- Idempotente.\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 1. O vocabul\u00E1rio fechado aprende duas palavras\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n-- A lista era LITERAL aqui, e literal \u00E9 o que quebra a corrente: a restri\u00E7\u00E3o \u00E9\n-- um vocabul\u00E1rio fechado, e reescrev\u00EA-la por extenso APAGA toda palavra que um\n-- passo anterior tenha acrescentado depois de algu\u00E9m escrever esta lista. Foi o\n-- que aconteceu com `fin_cash_session`, registrado por `financial/072`, e com\n-- `crm_pipeline`, que o spine acrescenta antes das cadeias de plugin: a linha j\u00E1\n-- existia no de-para e a restaurada n\u00E3o a admitia \u2014\n-- `check constraint \"allowlist_writer_check\" is violated by some row`, e o\n-- encadeamento do zero parava aqui.\n--\n-- Vira acr\u00E9scimo, que \u00E9 como `inventory/004` j\u00E1 fazia: l\u00EA a defini\u00E7\u00E3o vigente,\n-- costura as duas palavras novas dentro dela e devolve. O que veio antes\n-- sobrevive, e o passo seguinte herda tudo.\nDO $$\nDECLARE v_def text;\nBEGIN\n SELECT pg_get_constraintdef(oid) INTO v_def\n FROM pg_constraint WHERE conname = 'allowlist_writer_check';\n IF v_def IS NULL OR strpos(v_def, 'appointment_item') > 0 THEN RETURN; END IF;\n ALTER TABLE migration.allowlist DROP CONSTRAINT allowlist_writer_check;\n EXECUTE replace(\n 'ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check ' || v_def,\n '''person''::text',\n '''person''::text, ''appointment''::text, ''appointment_item''::text');\nEND $$;\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 2. O writer do agendamento\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE OR REPLACE FUNCTION migration.upsert_appointment(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_map jsonb := coalesce(p_args -> 'status_map', '{}'::jsonb);\n v_kind text := coalesce(p_args ->> 'kind', 'appointment');\n v_src_status text := p_row ->> 'status';\n v_status text;\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_starts timestamptz;\n v_ends timestamptz;\n v_dur integer;\n v_party uuid;\n v_assignee uuid;\n v_unit uuid;\n v_existing uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'an appointment needs an id');\n END IF;\n\n -- A hora \u00E9 o agendamento. Sem ela n\u00E3o h\u00E1 linha no calend\u00E1rio, e uma linha\n -- sem hora n\u00E3o \u00E9 um agendamento incompleto: \u00E9 outra coisa.\n v_starts := (p_row ->> 'starts_at')::timestamptz;\n IF v_starts IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', 'an appointment needs starts_at');\n END IF;\n\n IF NOT EXISTS (SELECT 1 FROM public.appointment_kinds k WHERE k.kind = v_kind) THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('appointment kind %L is not declared in public.appointment_kinds', v_kind));\n END IF;\n\n v_dur := nullif(p_row ->> 'duration_minutes', '')::integer;\n v_ends := v_starts + make_interval(mins => coalesce(v_dur, 0));\n\n v_status := coalesce(v_map ->> coalesce(v_src_status, ''), v_map ->> '*', v_src_status, 'scheduled');\n -- `appointment_kinds.statuses` \u00E9 text[], n\u00E3o jsonb: o vocabul\u00E1rio do tipo \u00E9\n -- uma lista de palavras, e `= ANY (array)` \u00E9 como se pergunta a ela.\n IF NOT EXISTS (SELECT 1 FROM public.appointment_kinds k\n WHERE k.kind = v_kind AND v_status = ANY (k.statuses)) THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('status %L is not one of the statuses declared for kind %L', v_status, v_kind));\n END IF;\n v_meta := migration._deep_set(v_meta, ARRAY['migration', 'source_status'], to_jsonb(v_src_status));\n\n BEGIN v_party := (p_row ->> 'party_id')::uuid; EXCEPTION WHEN OTHERS THEN v_party := NULL; END;\n BEGIN v_assignee := (p_row ->> 'assignee_id')::uuid; EXCEPTION WHEN OTHERS THEN v_assignee := NULL; END;\n BEGIN v_unit := (p_row ->> 'unit_id')::uuid; EXCEPTION WHEN OTHERS THEN v_unit := NULL; END;\n\n SELECT tenant_id INTO v_existing FROM public.appointments WHERE id = p_id;\n IF v_existing IS NOT NULL AND v_existing <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant',\n 'reason', format('appointment %s already exists in another tenant', p_id));\n END IF;\n\n IF v_existing IS NULL THEN\n INSERT INTO public.appointments\n (id, tenant_id, kind, party_id, assignee_id, location_id, unit_id,\n starts_at, ends_at, status, notes, metadata, checked_in_at, created_at, updated_at)\n VALUES\n (p_id, p_tenant, v_kind, v_party, v_assignee, v_unit, v_unit,\n v_starts, v_ends, v_status, p_row ->> 'notes', v_meta,\n nullif(p_row ->> 'checked_in_at', '')::timestamptz,\n coalesce(nullif(p_row ->> 'created_at', '')::timestamptz, now()),\n coalesce(nullif(p_row ->> 'updated_at', '')::timestamptz, now()));\n v_created := true;\n ELSE\n UPDATE public.appointments SET\n kind = v_kind,\n party_id = coalesce(v_party, party_id),\n assignee_id = coalesce(v_assignee, assignee_id),\n -- `location_id` \u00E9 a unidade na leitura (v_appointments junta app.units\n -- por ela); `unit_id` \u00E9 a unidade na RLS. As duas apontam para a mesma\n -- coisa e \u00E9 isso que as mant\u00E9m verdadeiras juntas.\n location_id = coalesce(v_unit, location_id),\n starts_at = v_starts,\n ends_at = v_ends,\n status = CASE WHEN p_row ? 'status' THEN v_status ELSE status END,\n notes = CASE WHEN p_row ? 'notes' THEN p_row ->> 'notes' ELSE notes END,\n checked_in_at = CASE WHEN p_row ? 'checked_in_at'\n THEN nullif(p_row ->> 'checked_in_at', '')::timestamptz ELSE checked_in_at END,\n metadata = migration._merge_meta(metadata, v_meta),\n updated_at = coalesce(nullif(p_row ->> 'updated_at', '')::timestamptz, now())\n WHERE id = p_id;\n END IF;\n\n PERFORM migration._set_unit('public.appointments', p_id, v_unit);\n\n IF v_standalone THEN\n PERFORM migration._audit(p_tenant, 'public.appointments', p_id,\n jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END));\n END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 3. O writer do servi\u00E7o agendado\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE OR REPLACE FUNCTION migration.upsert_appointment_item(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_booking uuid;\n v_service uuid;\n v_name text;\n v_unit uuid;\n v_existing uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'an appointment item needs an id');\n END IF;\n\n BEGIN v_booking := (p_row ->> 'booking_id')::uuid; EXCEPTION WHEN OTHERS THEN v_booking := NULL; END;\n -- Um item sem agendamento n\u00E3o \u00E9 um item solto, \u00E9 um item perdido. Recusa em\n -- vez de escrever uma linha que nenhuma tela alcan\u00E7a.\n IF v_booking IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', 'an appointment item needs the appointment it belongs to');\n END IF;\n\n BEGIN v_service := (p_row ->> 'service_id')::uuid; EXCEPTION WHEN OTHERS THEN v_service := NULL; END;\n\n -- O nome \u00E9 NOT NULL no destino. A origem guarda o nome no SERVI\u00C7O, n\u00E3o no\n -- item, ent\u00E3o ele vem pela ponte do extrator; se faltar, o nome do produto\n -- que a refer\u00EAncia resolveu serve, e s\u00F3 ent\u00E3o o r\u00F3tulo gen\u00E9rico.\n v_name := coalesce(\n nullif(p_row ->> 'name', ''),\n (SELECT pr.name FROM public.products pr WHERE pr.id = v_service),\n 'Servi\u00E7o');\n\n SELECT a.unit_id INTO v_unit FROM public.appointments a WHERE a.id = v_booking;\n\n SELECT tenant_id INTO v_existing FROM public.appointment_items WHERE id = p_id;\n IF v_existing IS NOT NULL AND v_existing <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant',\n 'reason', format('appointment item %s already exists in another tenant', p_id));\n END IF;\n\n IF v_existing IS NULL THEN\n INSERT INTO public.appointment_items\n (id, tenant_id, booking_id, service_id, name, duration_minutes, price,\n sort_order, unit_id, metadata, created_at, updated_at)\n VALUES\n (p_id, p_tenant, v_booking, v_service, v_name,\n nullif(p_row ->> 'duration_minutes', '')::integer,\n coalesce(nullif(p_row ->> 'price', '')::numeric, 0),\n coalesce(nullif(p_row ->> 'sort_order', '')::integer, 0),\n v_unit, v_meta,\n coalesce(nullif(p_row ->> 'created_at', '')::timestamptz, now()),\n coalesce(nullif(p_row ->> 'updated_at', '')::timestamptz, now()));\n v_created := true;\n ELSE\n UPDATE public.appointment_items SET\n booking_id = v_booking,\n service_id = coalesce(v_service, service_id),\n name = v_name,\n duration_minutes = CASE WHEN p_row ? 'duration_minutes'\n THEN nullif(p_row ->> 'duration_minutes', '')::integer ELSE duration_minutes END,\n unit_id = coalesce(v_unit, unit_id),\n metadata = migration._merge_meta(metadata, v_meta),\n updated_at = coalesce(nullif(p_row ->> 'updated_at', '')::timestamptz, now())\n WHERE id = p_id;\n END IF;\n\n IF v_standalone THEN\n PERFORM migration._audit(p_tenant, 'public.appointment_items', p_id,\n jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END));\n END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 4. As duas linhas do de-para\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- A allowlist congela a linha depois de escrita \u2014 mudar o de-para \u00E9 um arquivo\n-- de migration, n\u00E3o um UPDATE avulso, e o gatilho `trg_allowlist_frozen` \u00E9 quem\n-- cobra isso. Este \u00C9 o arquivo de migration, ent\u00E3o ele destrava; `set_local`\n-- para a destrava morrer com a transa\u00E7\u00E3o, tenha ela terminado bem ou mal.\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\nSELECT migration._allow(\n 'appointments', 'public.appointments', 'agenda', 'appointment',\n jsonb_build_object(\n 'kind', 'appointment',\n 'status_map', jsonb_build_object(\n 'scheduled', 'scheduled',\n 'confirmed', 'confirmed',\n 'in_progress', 'in_progress',\n 'completed', 'completed',\n 'no_show', 'no_show',\n 'unmarked', 'cancelled',\n 'waiting', 'confirmed')),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'appointment_date', 'starts_at',\n 'duration_minutes', 'duration_minutes',\n 'status', 'status',\n 'notes', 'notes',\n 'checked_in_at', 'checked_in_at',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'client_id', 'party_id@clients',\n 'professional_id', 'assignee_id@professionals',\n 'unit_company_id', 'unit_id@companies',\n 'tenant_id', '-',\n 'checkout_confirmed_by', '-',\n 'price', 'jsonb:agenda.v1_price',\n 'is_prepaid', 'jsonb:agenda.is_prepaid',\n 'prepaid_invoice_item_id', 'jsonb:agenda.v1_prepaid_invoice_item_ref',\n 'checkout_confirmed_at', 'jsonb:agenda.checkout_confirmed_at',\n 'service_location_id', 'jsonb:agenda.v1_service_location_ref',\n 'opportunity_id', 'jsonb:crm.v1_opportunity_ref',\n 'whatsapp_sent', 'jsonb:agenda.whatsapp_sent',\n 'whatsapp_confirmed', 'jsonb:agenda.whatsapp_confirmed'),\n 'O Compromisso do V1. A unidade n\u00E3o vem de uma coluna: vem da SALA '\n '(service_location_id \u2192 service_locations.unit_id \u2192 companies.uuid), e a '\n 'ponte \u00E9 feita no extrator. 99,5% resolvem; os 1.891 sem sala entram sem '\n 'unidade, que \u00E9 o que a origem tem. Sem Venda: order_id fica NULL porque o '\n 'financeiro do V1 chega pela fatura, que \u00E9 outro caminho.'\n);\n\nSELECT migration._allow(\n 'appointment_services', 'public.appointment_items', 'agenda', 'appointment_item',\n '{}'::jsonb,\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'appointment_id', 'booking_id@appointments',\n 'service_id', 'service_id@services',\n 'service_name', 'name',\n 'duration_minutes', 'duration_minutes',\n 'price', 'price',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'is_revision', 'jsonb:agenda.is_revision',\n 'revision_number', 'jsonb:agenda.revision_number',\n 'parent_journey_id', 'jsonb:agenda.v1_parent_journey_ref',\n 'added_during_execution', 'jsonb:agenda.added_during_execution',\n 'source_evaluation_execution_id','jsonb:agenda.v1_source_evaluation_ref'),\n 'O servi\u00E7o dentro do Compromisso. `service_name` n\u00E3o existe no V1 \u2014 vem da '\n 'ponte do extrator, porque o destino exige nome e a origem guarda o nome no '\n 'servi\u00E7o, n\u00E3o na linha. O pre\u00E7o \u00E9 o pre\u00E7o praticado NAQUELE agendamento, e '\n 'por isso vem cru e n\u00E3o da tabela de pre\u00E7os.'\n);\n\nDELETE FROM migration.excluded_tables WHERE source_table IN ('appointments', 'appointment_services');\n\nRESET migration.allowlist_unlock;\n\n\n-- \u2500\u2500 quem pode executar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Fun\u00E7\u00E3o nova nasce execut\u00E1vel por PUBLIC, e `anon`/`authenticated` herdam\n-- dali. Estas s\u00E3o de PORTE: SECURITY DEFINER, escrevem dados de qualquer\n-- inquilino. S\u00F3 o service_role, que \u00E9 quem roda a onda.\nREVOKE ALL ON FUNCTION migration.upsert_appointment(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_appointment(uuid, uuid, jsonb, jsonb) TO service_role;\nREVOKE ALL ON FUNCTION migration.upsert_appointment_item(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_appointment_item(uuid, uuid, jsonb, jsonb) TO service_role;\n";
10
10
  export declare const MIGRATION_009_O_EXECUTOR_SE_ACHA_PELO_PEDIDO = "-- O executor se acha pelo pedido.\n--\n-- `trg_financial_seed_item_participant` dispara em todo INSERT de\n-- `public.items` e pergunta uma coisa s\u00F3:\n--\n-- SELECT a.assignee_id, a.unit_id\n-- FROM public.appointments a\n-- WHERE a.order_id = NEW.order_id AND a.assignee_id IS NOT NULL\n-- ORDER BY a.created_at LIMIT 1\n--\n-- \u00C9 a pergunta certa \u2014 \"quem executou o servi\u00E7o desta linha?\" \u2014, e \u00E9 o que\n-- alimenta a comiss\u00E3o. O que faltava era o \u00EDndice que a responde:\n-- `public.appointments` tem dez \u00EDndices e **nenhum por `order_id`**.\n--\n-- \u2500\u2500 Medido \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- EXPLAIN ANALYZE de um INSERT em `public.items`, tenant Espa\u00E7o Facial,\n-- 20/09/2026, com 179.613 agendamentos na tabela:\n--\n-- Trigger items_financial_seed_participant: time=56.799\n-- ... os outros dezesseis gatilhos somados: time= 5.268\n-- Execution Time: 62.067 ms\n--\n-- **91% do custo de inserir um item.** E o n\u00FAmero cresce com a agenda: o mesmo\n-- INSERT vai custar o dobro quando os 354.208 compromissos do V1 tiverem\n-- entrado, porque a varredura \u00E9 da tabela inteira.\n--\n-- \u2500\u2500 N\u00E3o \u00E9 s\u00F3 da migra\u00E7\u00E3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Vale registrar, porque o \u00EDndice se paga duas vezes: este gatilho roda toda\n-- vez que algu\u00E9m acrescenta uma linha a uma venda no app \u2014 a comanda, o\n-- carrinho, o atendimento que ganha um produto no fim. Numa rede com agenda\n-- cheia, cada item somava dezenas de milissegundos de varredura que ningu\u00E9m\n-- pediu. O porte s\u00F3 foi quem gritou primeiro: 220.462 itens a 62 ms s\u00E3o quatro\n-- horas.\n--\n-- \u2500\u2500 Por que PARCIAL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- O compromisso do V1 n\u00E3o tem Venda: ele chega com `order_id` NULL, e s\u00E3o\n-- centenas de milhares assim. Um \u00EDndice completo carregaria todos eles para\n-- responder sobre os poucos que t\u00EAm pedido. O parcial indexa s\u00F3 as linhas que\n-- a consulta pode encontrar \u2014 e o planejador o usa porque o predicado\n-- (`order_id = \u2026`) implica a condi\u00E7\u00E3o do \u00EDndice.\n--\n-- `CONCURRENTLY` porque o app est\u00E1 no ar: a forma normal tranca a escrita em\n-- `appointments` enquanto constr\u00F3i, e uma recep\u00E7\u00E3o marcando hor\u00E1rio n\u00E3o tem\n-- por que esperar por um \u00EDndice.\n--\n-- Idempotente.\n\n-- Sem CONCURRENTLY: \u00E9 o \u00FAnico arquivo do reposit\u00F3rio que o usava, e ele n\u00E3o\n-- roda dentro de transa\u00E7\u00E3o \u2014 o encadeamento aplica cada migra\u00E7\u00E3o numa. A\n-- tabela \u00E9 pequena o bastante para o bloqueio curto de um CREATE INDEX comum.\nCREATE INDEX IF NOT EXISTS appointments_order_idx\n ON public.appointments (order_id)\n WHERE order_id IS NOT NULL;\n\nCOMMENT ON INDEX public.appointments_order_idx IS\n 'O caminho de \"quem executou o servi\u00E7o desta linha da venda\", que '\n 'trg_financial_seed_item_participant percorre em todo INSERT de items. Sem '\n 'ele a pergunta era varredura da agenda inteira: 56,8 ms dos 62 ms de '\n 'inserir um item, medido com 179.613 compromissos.';\n";
11
+ export declare const MIGRATION_010_A_GRADE_DO_V1_ATRAVESSA = "-- A grade do V1 atravessa.\n--\n-- 008 trouxe o COMPROMISSO (353.895) e o servi\u00E7o agendado (314.051). Faltava o\n-- que diz QUANDO se pode marcar: a grade. `public.schedules` existe desde o\n-- baseline e estava com ZERO linhas no Espa\u00E7o Facial \u2014 por isso\n-- `get_available_slots` n\u00E3o ofertava hor\u00E1rio nenhum, em nenhuma das 63\n-- unidades.\n--\n-- \u2500\u2500 O de-para \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- V1 service_locations \u2192 public.plg_agenda_resources (a sala)\n-- V1 schedule_grid_blocks \u2192 public.schedules kind='working_hours'\n-- V1 schedule_blocks \u2192 public.schedules kind='block'\n-- V1 schedule_grids \u2192 n\u00E3o vira linha: desce inteira para o bloco\n--\n-- Medido em 21/09/2026 contra o V1 de produ\u00E7\u00E3o (somente leitura), tenant 1:\n--\n-- service_locations 223 (219 ativas, 127 unidades)\n-- schedule_grids 482 (271 standard \u00B7 211 exception)\n-- schedule_grid_blocks 1.331 (nenhum \u00F3rf\u00E3o de sala ou profissional)\n-- schedule_blocks 8\n--\n-- \u2500\u2500 Por que a grade n\u00E3o vira linha \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- A grade do V1 \u00E9 (profissional, vig\u00EAncia, tipo, intervalo de slot) e os BLOCOS\n-- s\u00E3o (weekday, faixa de hor\u00E1rio, sala). O destino \u00E9 um arqu\u00E9tipo de linha\n-- \u00FAnica: `schedules` descreve UMA janela. Ent\u00E3o os atributos da grade descem\n-- para cada bloco, e a grade n\u00E3o tem linha pr\u00F3pria.\n--\n-- A alternativa era expandir cada bloco em uma linha por data. D\u00E1 44.493 linhas\n-- contra 1.331, n\u00E3o representa a grade ETERNA (248 das 482 t\u00EAm valid_to nulo), e\n-- mata o espelho D-1: um UPDATE na grade viraria milhares de INSERT/DELETE em\n-- vez de um UPDATE.\n--\n-- \u2500\u2500 A sala, e por que ela N\u00C3O se chama location \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `service_locations` do V1 \u00E9 a SALA. `plg_agenda_locations` seria a tradu\u00E7\u00E3o\n-- literal e seria errada: neste SDK `location` j\u00E1 significa FILIAL \u2014\n-- `packages/core/src/data/archetype.ts` declara\n-- `location: { table: 'v_units', fkColumn: 'location_id' }`, `scoped_resources`\n-- usa `location_id` como a coluna de unidade de `appointments` e `schedules`, e\n-- 352.005 compromissos migrados j\u00E1 gravaram a unidade ali.\n--\n-- `resources` \u00E9 o nome que Fresha, Mindbody, Booksy, Square e o Google Calendar\n-- d\u00E3o para \"sala, cadeira, maca, aparelho\" \u2014 uma tabela para tudo que se reserva\n-- e n\u00E3o \u00E9 gente.\n--\n-- O outro padr\u00E3o da ind\u00FAstria \u00E9 o do FHIR: `Location` com `physicalType`\n-- (room/bed/wing) e `partOf` aninhando sala dentro de pr\u00E9dio, numa tabela s\u00F3.\n-- N\u00E3o serve aqui porque a unidade n\u00E3o \u00E9 uma linha vizinha: \u00E9 `app.units`, a\n-- cerca da franquia. A migration 091 j\u00E1 tentou apontar `service_locations` para\n-- `app.units` e foi revertida \u2014 o motivo est\u00E1 escrito na `excluded_tables`:\n-- transformaria cada sala de atendimento numa filial.\n--\n-- \u2500\u2500 O que a fun\u00E7\u00E3o de disponibilidade n\u00E3o sabia \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `agenda_working_windows` tinha preced\u00EAncia de DIA: qualquer linha com\n-- `specific_date` apagava as semanais daquele profissional. O V1 tem tr\u00EAs regras\n-- que isso n\u00E3o cobre, e todas as tr\u00EAs mudam o hor\u00E1rio que a tela oferta:\n--\n-- VIG\u00CANCIA a grade vale de valid_from a valid_to. Sem ela, grade encerrada\n-- em janeiro continua ofertando e grade que come\u00E7a em 2027 j\u00E1\n-- oferta hoje. 234 das 482 grades t\u00EAm vig\u00EAncia fechada.\n--\n-- PRECED\u00CANCIA durante a vig\u00EAncia de uma grade `exception`, cada weekday que\n-- ela define SUBSTITUI a padr\u00E3o naquele dia \u2014 e s\u00F3 naquele. Os\n-- weekdays que a exce\u00E7\u00E3o n\u00E3o define seguem a padr\u00E3o.\n--\n-- PARIDADE week_interval: 0 = toda semana; N = atende uma e pula N,\n-- ancorado na PRIMEIRA ocorr\u00EAncia daquele weekday dentro da\n-- vig\u00EAncia. E quando a paridade da exce\u00E7\u00E3o pula a semana, o dia\n-- fica FECHADO \u2014 n\u00E3o volta para a padr\u00E3o.\n--\n-- A fonte \u00E9 o motor puro do V1, compartilhado verbatim entre o frontend e as\n-- edge functions: ~/dev/beautyplace/supabase/functions/_shared/schedule-engine.ts\n-- (`resolveEffectiveWindows`, `blockActiveOnDate`).\n--\n-- O BLOQUEIO n\u00E3o entra como janela negativa: no V1 ele \u00E9 tempo OCUPADO na hora\n-- de gerar o slot, igual a um agendamento (`bloqueiosAsBusy`). Aqui \u00E9 a mesma\n-- coisa \u2014 `get_available_slots` passa a subtrair `agenda_busy_blocks`, do mesmo\n-- jeito que j\u00E1 subtra\u00EDa `appointments`.\n--\n-- Idempotente.\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 1. A sala\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nCREATE TABLE IF NOT EXISTS public.plg_agenda_resources (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n -- A sala \u00E9 SEMPRE de uma unidade: 223 de 223 no V1 t\u00EAm unit_id. NOT NULL \u00E9\n -- a trava da franquia \u2014 uma sala sem unidade apareceria na agenda de todas.\n unit_id uuid NOT NULL,\n name text NOT NULL,\n -- Sala, cadeira, maca, aparelho. Vocabul\u00E1rio DO PLUGIN, num CHECK: `kind`\n -- com registry \u00E9 contrato de arqu\u00E9tipo, e `plg_*` n\u00E3o \u00E9 arqu\u00E9tipo.\n kind text DEFAULT 'room'::text NOT NULL,\n -- `service_locations.max_simultaneous_clients` \u2014 1 ou 2 no V1. \u00C9 o que\n -- permite duas clientes na mesma sala ao mesmo tempo.\n capacity integer DEFAULT 1 NOT NULL,\n is_active boolean DEFAULT true NOT NULL,\n sort_order integer DEFAULT 0 NOT NULL,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n owner_id uuid,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT plg_agenda_resources_kind_check\n CHECK (kind = ANY (ARRAY['room'::text, 'chair'::text, 'equipment'::text, 'other'::text])),\n CONSTRAINT plg_agenda_resources_capacity_check CHECK (capacity >= 1)\n);\n\nCOMMENT ON TABLE public.plg_agenda_resources IS\n 'A sala (ou cadeira, maca, aparelho) onde o atendimento acontece, DENTRO de uma unidade. V1: service_locations. N\u00E3o se chama location porque location_id, neste SDK, \u00E9 a unidade.';\nCOMMENT ON COLUMN public.plg_agenda_resources.capacity IS\n 'V1: service_locations.max_simultaneous_clients \u2014 quantas pessoas cabem ao mesmo tempo.';\n\nALTER TABLE public.plg_agenda_resources FORCE ROW LEVEL SECURITY;\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'public.plg_agenda_resources'::regclass AND contype = 'p') THEN\n ALTER TABLE ONLY public.plg_agenda_resources ADD CONSTRAINT plg_agenda_resources_pkey PRIMARY KEY (id);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_agenda_resources_tenant_id_id_key') THEN\n ALTER TABLE ONLY public.plg_agenda_resources\n ADD CONSTRAINT plg_agenda_resources_tenant_id_id_key UNIQUE (tenant_id, id);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_agenda_resources_tenant_fkey') THEN\n ALTER TABLE ONLY public.plg_agenda_resources\n ADD CONSTRAINT plg_agenda_resources_tenant_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n END IF;\n -- A FK composta, e n\u00E3o s\u00F3 `unit_id`: \u00E9 ela que impede a sala de uma casa\n -- apontar para a filial de outra.\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_agenda_resources_unit_fkey') THEN\n ALTER TABLE ONLY public.plg_agenda_resources\n ADD CONSTRAINT plg_agenda_resources_unit_fkey FOREIGN KEY (tenant_id, unit_id)\n REFERENCES app.units(tenant_id, id) ON DELETE CASCADE;\n END IF;\nEND $$;\n\nCREATE INDEX IF NOT EXISTS idx_plg_agenda_resources_tenant_unit\n ON public.plg_agenda_resources USING btree (tenant_id, unit_id) WHERE is_active;\n\nDROP TRIGGER IF EXISTS plg_agenda_resources_updated_at ON public.plg_agenda_resources;\nCREATE TRIGGER plg_agenda_resources_updated_at\n BEFORE UPDATE ON public.plg_agenda_resources\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nALTER TABLE public.plg_agenda_resources ENABLE ROW LEVEL SECURITY;\n\n-- Escopo de UNIDADE, sem escopo de dono: uma sala n\u00E3o pertence a uma pessoa.\n-- Quem enxerga a agenda da filial enxerga as salas dela, e nenhuma outra.\nDROP POLICY IF EXISTS plg_agenda_resources_authz_select ON public.plg_agenda_resources;\nCREATE POLICY plg_agenda_resources_authz_select ON public.plg_agenda_resources\n FOR SELECT TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_unit(plg_agenda_resources.unit_id))\n AND (SELECT app.has_permission('agenda.read', plg_agenda_resources.unit_id)));\n\nDROP POLICY IF EXISTS plg_agenda_resources_authz_insert ON public.plg_agenda_resources;\nCREATE POLICY plg_agenda_resources_authz_insert ON public.plg_agenda_resources\n FOR INSERT TO authenticated WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_unit(plg_agenda_resources.unit_id))\n AND (SELECT app.has_permission('agenda.create', plg_agenda_resources.unit_id)));\n\nDROP POLICY IF EXISTS plg_agenda_resources_authz_update ON public.plg_agenda_resources;\nCREATE POLICY plg_agenda_resources_authz_update ON public.plg_agenda_resources\n FOR UPDATE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_unit(plg_agenda_resources.unit_id))\n AND (SELECT app.has_permission('agenda.edit', plg_agenda_resources.unit_id)))\n WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_unit(plg_agenda_resources.unit_id))\n AND (SELECT app.has_permission('agenda.edit', plg_agenda_resources.unit_id)));\n\nDROP POLICY IF EXISTS plg_agenda_resources_authz_delete ON public.plg_agenda_resources;\nCREATE POLICY plg_agenda_resources_authz_delete ON public.plg_agenda_resources\n FOR DELETE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_unit(plg_agenda_resources.unit_id))\n AND (SELECT app.has_permission('agenda.delete', plg_agenda_resources.unit_id)));\n\nGRANT SELECT, INSERT, UPDATE, DELETE ON TABLE public.plg_agenda_resources TO authenticated;\nGRANT ALL ON TABLE public.plg_agenda_resources TO service_role;\n\n-- `unit_column` \u00E9 `unit_id` aqui, e `location_id` em appointments/schedules.\n-- N\u00E3o \u00E9 inconsist\u00EAncia: aquelas duas carregam `location_id` desde a 004 e o\n-- SDK inteiro l\u00EA por ele; uma tabela NOVA n\u00E3o precisa herdar o nome amb\u00EDguo.\nINSERT INTO public.scoped_resources (resource_table, unit_column, owner_column, shareable, default_visibility, label)\nVALUES ('plg_agenda_resources', 'unit_id', 'owner_id', false, 'unit', 'Salas')\nON CONFLICT (resource_table) DO UPDATE\n SET unit_column = EXCLUDED.unit_column, default_visibility = EXCLUDED.default_visibility, label = EXCLUDED.label;\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 2. O que a grade tem e o arqu\u00E9tipo n\u00E3o tinha\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nALTER TABLE public.schedules ADD COLUMN IF NOT EXISTS valid_from date;\nALTER TABLE public.schedules ADD COLUMN IF NOT EXISTS valid_to date;\nALTER TABLE public.schedules ADD COLUMN IF NOT EXISTS grid_kind text;\nALTER TABLE public.schedules ADD COLUMN IF NOT EXISTS week_interval smallint DEFAULT 0 NOT NULL;\nALTER TABLE public.schedules ADD COLUMN IF NOT EXISTS slot_interval_minutes integer;\nALTER TABLE public.schedules ADD COLUMN IF NOT EXISTS resource_id uuid;\nALTER TABLE public.appointments ADD COLUMN IF NOT EXISTS resource_id uuid;\n\nCOMMENT ON COLUMN public.schedules.valid_from IS 'V1: schedule_grids.valid_from \u2014 a janela s\u00F3 vale a partir desta data.';\nCOMMENT ON COLUMN public.schedules.valid_to IS 'V1: schedule_grids.valid_to \u2014 NULL = eterna (248 das 482 grades do V1).';\nCOMMENT ON COLUMN public.schedules.grid_kind IS 'V1: schedule_grids.kind \u2014 standard | exception. Durante a vig\u00EAncia, a exce\u00E7\u00E3o substitui a padr\u00E3o naquele weekday, e s\u00F3 naquele.';\nCOMMENT ON COLUMN public.schedules.week_interval IS 'V1: 0 = toda semana; N = atende uma e pula N, ancorado na primeira ocorr\u00EAncia do weekday dentro da vig\u00EAncia.';\nCOMMENT ON COLUMN public.schedules.slot_interval_minutes IS 'V1: schedule_grids.slot_interval_minutes \u2014 o passo de oferta DESTA grade (5/15/30), n\u00E3o o do par\u00E2metro da chamada.';\nCOMMENT ON COLUMN public.schedules.resource_id IS 'V1: schedule_grid_blocks.service_location_id \u2014 a sala em que o profissional atende nesta janela.';\nCOMMENT ON COLUMN public.appointments.resource_id IS 'V1: appointments.service_location_id \u2014 a sala do atendimento.';\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'schedules_grid_kind_check') THEN\n ALTER TABLE public.schedules ADD CONSTRAINT schedules_grid_kind_check\n CHECK (grid_kind IS NULL OR grid_kind = ANY (ARRAY['standard'::text, 'exception'::text]));\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'schedules_week_interval_check') THEN\n ALTER TABLE public.schedules ADD CONSTRAINT schedules_week_interval_check\n CHECK (week_interval >= 0 AND week_interval <= 52);\n END IF;\n -- Vig\u00EAncia invertida n\u00E3o \u00E9 uma grade estranha, \u00E9 uma grade que n\u00E3o existe.\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'schedules_validity_check') THEN\n ALTER TABLE public.schedules ADD CONSTRAINT schedules_validity_check\n CHECK (valid_from IS NULL OR valid_to IS NULL OR valid_from <= valid_to);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'schedules_resource_fkey') THEN\n ALTER TABLE public.schedules ADD CONSTRAINT schedules_resource_fkey\n FOREIGN KEY (resource_id) REFERENCES public.plg_agenda_resources(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'appointments_resource_fkey') THEN\n ALTER TABLE public.appointments ADD CONSTRAINT appointments_resource_fkey\n FOREIGN KEY (resource_id) REFERENCES public.plg_agenda_resources(id) ON DELETE SET NULL;\n END IF;\nEND $$;\n\n-- A consulta que a agenda faz o dia inteiro: \"quem trabalha neste weekday,\n-- nesta casa, nesta data\".\nCREATE INDEX IF NOT EXISTS idx_schedules_tenant_kind_dow\n ON public.schedules USING btree (tenant_id, kind, day_of_week)\n WHERE specific_date IS NULL;\nCREATE INDEX IF NOT EXISTS idx_schedules_tenant_kind_date\n ON public.schedules USING btree (tenant_id, kind, specific_date)\n WHERE specific_date IS NOT NULL;\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 3. A paridade de semana\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n--\n-- Espelho de `blockActiveOnDate` do motor do V1. A \u00E2ncora \u00E9 a primeira\n-- ocorr\u00EAncia daquele weekday DENTRO da vig\u00EAncia: quando `valid_from` cai depois\n-- desse dia na pr\u00F3pria semana, a \u00E2ncora desliza para a semana seguinte, e por\n-- isso o `-1`. Sem ele a primeira semana contaria como \"semana sim\" ou \"semana\n-- n\u00E3o\" conforme o dia em que a vig\u00EAncia come\u00E7ou, que \u00E9 o defeito que faria a\n-- quinzena inverter.\nCREATE OR REPLACE FUNCTION public.agenda_week_parity_active(\n p_valid_from date, p_date date, p_week_interval smallint\n) RETURNS boolean\nLANGUAGE sql IMMUTABLE SET search_path TO ''\nAS $function$\n SELECT CASE\n WHEN coalesce(p_week_interval, 0) <= 0 THEN true\n WHEN p_valid_from IS NULL THEN true\n ELSE (\n SELECT w.wi >= 0 AND (w.wi % (p_week_interval + 1)) = 0\n FROM (SELECT\n ((p_date - extract(dow from p_date)::int)\n - (p_valid_from - extract(dow from p_valid_from)::int)) / 7\n - CASE WHEN extract(dow from p_date) < extract(dow from p_valid_from)\n THEN 1 ELSE 0 END AS wi) w)\n END\n$function$;\n\nCOMMENT ON FUNCTION public.agenda_week_parity_active(date, date, smallint) IS\n 'A semana \u00E9 \"semana sim\"? Espelho de blockActiveOnDate do motor de grades do V1.';\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 4. As janelas efetivas do dia\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n--\n-- DROP e n\u00E3o REPLACE: o retorno ganha `resource_id` e `slot_interval_minutes`, e\n-- `CREATE OR REPLACE FUNCTION` n\u00E3o muda tipo de retorno. Os quatro chamadores\n-- s\u00E3o corpos plpgsql que nomeiam colunas (`w.assignee_id`, `w.win_start`,\n-- `w.win_end`), ent\u00E3o n\u00E3o s\u00E3o depend\u00EAncias do cat\u00E1logo e continuam v\u00E1lidos.\n\nDROP FUNCTION IF EXISTS public.agenda_working_windows(uuid, date, uuid);\n\nCREATE FUNCTION public.agenda_working_windows(\n p_tenant_id uuid, p_date date, p_assignee_id uuid DEFAULT NULL::uuid\n) RETURNS TABLE(assignee_id uuid, win_start time without time zone, win_end time without time zone,\n resource_id uuid, slot_interval_minutes integer)\nLANGUAGE sql STABLE SET search_path TO 'public'\nAS $function$\n WITH vigente AS (\n -- Vig\u00EAncia primeiro: o que n\u00E3o vale hoje n\u00E3o participa de decis\u00E3o nenhuma,\n -- nem como exce\u00E7\u00E3o que fecha o dia.\n SELECT sc.assignee_id, sc.starts_at, sc.ends_at, sc.is_active, sc.specific_date,\n sc.day_of_week, sc.grid_kind, sc.week_interval, sc.valid_from,\n sc.resource_id, sc.slot_interval_minutes\n FROM schedules sc\n WHERE sc.tenant_id = p_tenant_id\n AND sc.kind = 'working_hours'\n AND (p_assignee_id IS NULL OR sc.assignee_id = p_assignee_id)\n AND (sc.valid_from IS NULL OR sc.valid_from <= p_date)\n AND (sc.valid_to IS NULL OR p_date <= sc.valid_to)\n ),\n -- A exce\u00E7\u00E3o de DIA: a linha com specific_date nesta data. Preced\u00EAncia m\u00E1xima,\n -- e conta mesmo INATIVA \u2014 \u00E9 assim que o \"dia de folga\" apaga a semana.\n exc_dia AS (\n SELECT v.* FROM vigente v WHERE v.specific_date = p_date\n ),\n -- As semanais deste weekday, j\u00E1 com a paridade aplicada.\n semanal AS (\n SELECT v.* FROM vigente v\n WHERE v.specific_date IS NULL\n AND v.is_active\n AND v.day_of_week = extract(dow from p_date)::smallint\n AND public.agenda_week_parity_active(v.valid_from, p_date, v.week_interval)\n ),\n -- Quem tem grade de EXCE\u00C7\u00C3O definindo este weekday \u2014 ainda que a paridade\n -- tenha pulado a semana. \u00C9 por isso que este CTE n\u00E3o filtra por paridade: no\n -- V1, exce\u00E7\u00E3o que pula a semana FECHA o dia, n\u00E3o devolve a padr\u00E3o.\n tem_excecao AS (\n SELECT DISTINCT v.assignee_id FROM vigente v\n WHERE v.grid_kind = 'exception'\n AND v.specific_date IS NULL\n AND v.is_active\n AND v.day_of_week = extract(dow from p_date)::smallint\n ),\n grade AS (\n SELECT s.assignee_id, s.starts_at, s.ends_at, s.resource_id, s.slot_interval_minutes\n FROM semanal s\n WHERE s.grid_kind = 'exception'\n UNION ALL\n SELECT s.assignee_id, s.starts_at, s.ends_at, s.resource_id, s.slot_interval_minutes\n FROM semanal s\n WHERE coalesce(s.grid_kind, 'standard') <> 'exception'\n AND NOT EXISTS (SELECT 1 FROM tem_excecao t\n WHERE t.assignee_id IS NOT DISTINCT FROM s.assignee_id)\n )\n SELECT e.assignee_id, e.starts_at, e.ends_at, e.resource_id, e.slot_interval_minutes\n FROM exc_dia e WHERE e.is_active\n UNION ALL\n SELECT g.assignee_id, g.starts_at, g.ends_at, g.resource_id, g.slot_interval_minutes\n FROM grade g\n WHERE NOT EXISTS (SELECT 1 FROM exc_dia e\n WHERE e.assignee_id IS NOT DISTINCT FROM g.assignee_id)\n$function$;\n\nCOMMENT ON FUNCTION public.agenda_working_windows(uuid, date, uuid) IS\n 'Janelas de atendimento efetivas na data: vig\u00EAncia, preced\u00EAncia da exce\u00E7\u00E3o por weekday e paridade de semana. Espelho de resolveEffectiveWindows do motor do V1.';\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 5. O bloqueio \u00E9 tempo ocupado\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n--\n-- N\u00E3o \u00E9 janela negativa: no V1 o bloqueio entra como BUSY na gera\u00E7\u00E3o do slot,\n-- igual a um agendamento. `assignee_id` nulo \u00E9 bloqueio da casa inteira \u2014 um\n-- feriado \u2014 e o V1 tem exatamente essa forma.\nCREATE OR REPLACE FUNCTION public.agenda_busy_blocks(\n p_tenant_id uuid, p_date date, p_assignee_id uuid DEFAULT NULL::uuid\n) RETURNS TABLE(assignee_id uuid, busy_start time without time zone, busy_end time without time zone)\nLANGUAGE sql STABLE SET search_path TO 'public'\nAS $function$\n SELECT sc.assignee_id, sc.starts_at, sc.ends_at\n FROM schedules sc\n WHERE sc.tenant_id = p_tenant_id\n AND sc.kind = 'block'\n AND sc.is_active\n AND (p_assignee_id IS NULL OR sc.assignee_id IS NULL OR sc.assignee_id = p_assignee_id)\n AND (sc.valid_from IS NULL OR sc.valid_from <= p_date)\n AND (sc.valid_to IS NULL OR p_date <= sc.valid_to)\n AND (\n -- ocorr\u00EAncia \u00FAnica\n sc.specific_date = p_date\n -- faixa de datas sem weekday: bloqueia todo dia da faixa\n OR (sc.specific_date IS NULL AND sc.day_of_week IS NULL)\n -- recorrente por weekday, com a mesma paridade da grade\n OR (sc.specific_date IS NULL\n AND sc.day_of_week = extract(dow from p_date)::smallint\n AND public.agenda_week_parity_active(sc.valid_from, p_date, sc.week_interval))\n )\n$function$;\n\nCOMMENT ON FUNCTION public.agenda_busy_blocks(uuid, date, uuid) IS\n 'Tempo ocupado por bloqueio (almo\u00E7o, feriado, compromisso) na data. V1: schedule_blocks.';\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 6. A oferta de hor\u00E1rio passa a obedecer \u00E0 grade\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n--\n-- Duas mudan\u00E7as. O passo sai da JANELA quando ela tem um \u2014 o V1 guarda o\n-- intervalo na grade (5, 15 ou 30 conforme o profissional), e 34 das 482 grades\n-- n\u00E3o s\u00E3o de 30; ofertar de 30 em 30 para elas \u00E9 ofertar hor\u00E1rio que a casa n\u00E3o\n-- atende. E o bloqueio passa a ocupar, do mesmo jeito que o agendamento j\u00E1\n-- ocupava.\nCREATE OR REPLACE FUNCTION public.get_available_slots(\n p_tenant_id uuid, p_date date, p_duration_minutes integer,\n p_assignee_id uuid DEFAULT NULL::uuid, p_slot_interval integer DEFAULT 30\n) RETURNS TABLE(slot_start timestamp with time zone, slot_end timestamp with time zone)\nLANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'\nAS $function$\nDECLARE\n v_tz text;\nBEGIN\n -- tight validation: this function is anon-callable\n IF p_duration_minutes IS NULL OR p_duration_minutes NOT BETWEEN 5 AND 480 THEN\n RAISE EXCEPTION 'invalid duration';\n END IF;\n IF p_slot_interval IS NULL OR p_slot_interval NOT BETWEEN 5 AND 240 THEN\n RAISE EXCEPTION 'invalid interval';\n END IF;\n IF p_date IS NULL OR p_date < current_date OR p_date > current_date + 90 THEN\n RETURN;\n END IF;\n\n SELECT COALESCE(t.settings->>'timezone', 'America/Sao_Paulo') INTO v_tz\n FROM tenants t WHERE t.id = p_tenant_id;\n IF v_tz IS NULL THEN RETURN; END IF; -- unknown tenant \u2192 empty, not an error\n\n RETURN QUERY\n WITH day_schedules AS (\n SELECT w.assignee_id, w.win_start, w.win_end,\n -- O passo da grade, com o par\u00E2metro como queda. Preso \u00E0 mesma faixa\n -- que a valida\u00E7\u00E3o de cima cobra do par\u00E2metro: uma grade com passo\n -- absurdo n\u00E3o pode virar um generate_series infinito numa fun\u00E7\u00E3o\n -- que o an\u00F4nimo chama.\n CASE WHEN w.slot_interval_minutes BETWEEN 5 AND 240\n THEN w.slot_interval_minutes ELSE p_slot_interval END AS step\n FROM agenda_working_windows(p_tenant_id, p_date, p_assignee_id) w\n ),\n candidate AS (\n SELECT\n ds.assignee_id,\n gs AS s_start,\n gs + make_interval(mins => p_duration_minutes) AS s_end\n FROM day_schedules ds\n CROSS JOIN LATERAL generate_series(\n (p_date + ds.win_start) AT TIME ZONE v_tz,\n ((p_date + ds.win_end) AT TIME ZONE v_tz) - make_interval(mins => p_duration_minutes),\n make_interval(mins => ds.step)\n ) AS gs\n )\n SELECT DISTINCT c.s_start, c.s_end\n FROM candidate c\n WHERE NOT EXISTS (\n SELECT 1 FROM appointments b\n WHERE b.tenant_id = p_tenant_id\n -- SMART SYNC (gcal 002): this professional's own bookings OR any\n -- tenant-wide BLOCK (assignee_id IS NULL AND kind='block') busy the slot.\n AND (b.assignee_id = c.assignee_id\n OR (b.assignee_id IS NULL AND b.kind = 'block'))\n AND b.status NOT IN ('cancelled', 'no_show')\n AND b.starts_at < c.s_end\n AND COALESCE(b.ends_at, b.starts_at) > c.s_start\n )\n -- O bloqueio da grade do V1 (almo\u00E7o, feriado). `assignee_id` nulo \u00E9 da casa.\n AND NOT EXISTS (\n SELECT 1 FROM agenda_busy_blocks(p_tenant_id, p_date, p_assignee_id) bb\n WHERE (bb.assignee_id IS NULL OR bb.assignee_id = c.assignee_id)\n AND (p_date + bb.busy_start) AT TIME ZONE v_tz < c.s_end\n AND (p_date + bb.busy_end) AT TIME ZONE v_tz > c.s_start\n )\n ORDER BY 1;\nEND;\n$function$;\n\nREVOKE ALL ON FUNCTION public.agenda_working_windows(uuid, date, uuid) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.agenda_working_windows(uuid, date, uuid) TO authenticated, service_role;\nREVOKE ALL ON FUNCTION public.agenda_busy_blocks(uuid, date, uuid) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.agenda_busy_blocks(uuid, date, uuid) TO authenticated, service_role;\nREVOKE ALL ON FUNCTION public.agenda_week_parity_active(date, date, smallint) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.agenda_week_parity_active(date, date, smallint) TO authenticated, anon, service_role;\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 7. O vocabul\u00E1rio fechado aprende duas palavras\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n--\n-- Por ACR\u00C9SCIMO, lendo a defini\u00E7\u00E3o vigente: a lista literal apaga toda palavra\n-- que um passo anterior tenha acrescentado depois de algu\u00E9m escrever a lista. \u00C9\n-- a mesma armadilha registrada em financial/072 e agenda/008.\nDO $$\nDECLARE v_def text;\nBEGIN\n SELECT pg_get_constraintdef(oid) INTO v_def\n FROM pg_constraint WHERE conname = 'allowlist_writer_check';\n IF v_def IS NULL OR strpos(v_def, 'agenda_schedule') > 0 THEN RETURN; END IF;\n ALTER TABLE migration.allowlist DROP CONSTRAINT allowlist_writer_check;\n EXECUTE replace(\n 'ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check ' || v_def,\n '''person''::text',\n '''person''::text, ''agenda_resource''::text, ''agenda_schedule''::text');\nEND $$;\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 8. O writer da sala\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nCREATE OR REPLACE FUNCTION migration.upsert_agenda_resource(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_unit uuid;\n v_name text;\n v_cap integer;\n v_existing uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'a resource needs an id');\n END IF;\n\n BEGIN v_unit := (p_row ->> 'unit_id')::uuid; EXCEPTION WHEN OTHERS THEN v_unit := NULL; END;\n -- Sala sem unidade n\u00E3o \u00E9 sala incompleta: \u00E9 sala que apareceria na agenda de\n -- todas as 63 filiais. A coluna \u00E9 NOT NULL por isso, e a recusa \u00E9 aqui para\n -- a linha ir para a quarentena com o motivo escrito em vez de estourar.\n IF v_unit IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', 'a resource needs the unit it lives in');\n END IF;\n\n v_name := nullif(trim(coalesce(p_row ->> 'name', '')), '');\n IF v_name IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', 'a resource needs a name');\n END IF;\n\n -- `max_simultaneous_clients` vem 1 ou 2 no V1; zero ou nulo vira 1, porque\n -- capacidade zero descreveria uma sala onde ningu\u00E9m entra.\n v_cap := greatest(coalesce(nullif(p_row ->> 'capacity', '')::integer, 1), 1);\n\n SELECT tenant_id INTO v_existing FROM public.plg_agenda_resources WHERE id = p_id;\n IF v_existing IS NOT NULL AND v_existing <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant',\n 'reason', format('resource %s already exists in another tenant', p_id));\n END IF;\n\n IF v_existing IS NULL THEN\n INSERT INTO public.plg_agenda_resources\n (id, tenant_id, unit_id, name, kind, capacity, is_active, sort_order, metadata, created_at, updated_at)\n VALUES\n (p_id, p_tenant, v_unit, v_name, coalesce(p_args ->> 'kind', 'room'), v_cap,\n coalesce((p_row ->> 'is_active')::boolean, true),\n coalesce(nullif(p_row ->> 'sort_order', '')::integer, 0), v_meta,\n coalesce(nullif(p_row ->> 'created_at', '')::timestamptz, now()),\n coalesce(nullif(p_row ->> 'updated_at', '')::timestamptz, now()));\n v_created := true;\n ELSE\n UPDATE public.plg_agenda_resources SET\n unit_id = v_unit,\n name = v_name,\n capacity = v_cap,\n is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END,\n metadata = migration._merge_meta(metadata, v_meta),\n updated_at = coalesce(nullif(p_row ->> 'updated_at', '')::timestamptz, now())\n WHERE id = p_id;\n END IF;\n\n IF v_standalone THEN\n PERFORM migration._audit(p_tenant, 'public.plg_agenda_resources', p_id,\n jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END));\n END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 9. O writer da janela \u2014 grade e bloqueio pela mesma porta\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n--\n-- Duas tabelas do V1 e um destino s\u00F3, porque o que as duas descrevem \u00E9 a mesma\n-- coisa: uma faixa de hor\u00E1rio num dia, com vig\u00EAncia. O que muda \u00E9 o sinal \u2014\n-- `working_hours` abre, `block` fecha \u2014 e isso \u00E9 o `kind`, que vem do\n-- `writer_args`.\n\nCREATE OR REPLACE FUNCTION migration.upsert_agenda_schedule(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_kind text := coalesce(p_args ->> 'kind', 'working_hours');\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_assignee uuid;\n v_unit uuid;\n v_resource uuid;\n v_starts time;\n v_ends time;\n v_dow smallint;\n v_specific date;\n v_from date;\n v_to date;\n v_grid text;\n v_weekdays jsonb;\n v_existing uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'a schedule needs an id');\n END IF;\n\n IF NOT EXISTS (SELECT 1 FROM public.schedule_kinds k WHERE k.kind = v_kind) THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('schedule kind %L is not declared in public.schedule_kinds', v_kind));\n END IF;\n\n BEGIN v_assignee := (p_row ->> 'assignee_id')::uuid; EXCEPTION WHEN OTHERS THEN v_assignee := NULL; END;\n BEGIN v_unit := (p_row ->> 'unit_id')::uuid; EXCEPTION WHEN OTHERS THEN v_unit := NULL; END;\n BEGIN v_resource := (p_row ->> 'resource_id')::uuid; EXCEPTION WHEN OTHERS THEN v_resource := NULL; END;\n\n -- Uma janela sem dono n\u00E3o oferta hor\u00E1rio de ningu\u00E9m. O bloqueio da CASA \u00E9\n -- outra coisa e chega com assignee nulo de prop\u00F3sito \u2014 por isso a recusa s\u00F3\n -- vale para a grade.\n IF v_assignee IS NULL AND v_kind = 'working_hours' THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', 'a working-hours window needs the professional it belongs to');\n END IF;\n\n -- Recorr\u00EAncia por lista de dias: uma linha do V1 viraria N no destino, e o\n -- ledger \u00E9 1:1. Zero linhas assim no Espa\u00E7o Facial; quando existir, a forma\n -- certa \u00E9 o extrator explodir a lista, n\u00E3o o writer inventar um dia.\n v_weekdays := p_row -> 'v1_weekdays';\n IF v_weekdays IS NOT NULL AND jsonb_typeof(v_weekdays) = 'array' AND jsonb_array_length(v_weekdays) > 0 THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', 'a block recurring by weekday list needs one destination row per weekday; the extractor has to split it');\n END IF;\n\n -- O dia inteiro \u00E9 uma faixa como outra qualquer no destino. 00:00\u201323:59 e n\u00E3o\n -- 24:00 porque `time` n\u00E3o tem 24:00, e um minuto de folga \u00E0 meia-noite n\u00E3o\n -- muda nenhuma decis\u00E3o de agenda.\n IF coalesce((p_row ->> 'all_day')::boolean, false) THEN\n v_starts := '00:00'::time; v_ends := '23:59'::time;\n ELSE\n BEGIN v_starts := (p_row ->> 'starts_at')::time; EXCEPTION WHEN OTHERS THEN v_starts := NULL; END;\n BEGIN v_ends := (p_row ->> 'ends_at')::time; EXCEPTION WHEN OTHERS THEN v_ends := NULL; END;\n END IF;\n IF v_starts IS NULL OR v_ends IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', 'a schedule needs a start and an end');\n END IF;\n IF v_ends <= v_starts THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('a schedule ends before it starts: %s\u2013%s', v_starts, v_ends));\n END IF;\n\n BEGIN v_dow := (p_row ->> 'day_of_week')::smallint; EXCEPTION WHEN OTHERS THEN v_dow := NULL; END;\n BEGIN v_specific := (p_row ->> 'specific_date')::date; EXCEPTION WHEN OTHERS THEN v_specific := NULL; END;\n BEGIN v_from := (p_row ->> 'valid_from')::date; EXCEPTION WHEN OTHERS THEN v_from := NULL; END;\n BEGIN v_to := (p_row ->> 'valid_to')::date; EXCEPTION WHEN OTHERS THEN v_to := NULL; END;\n\n IF v_dow IS NULL AND v_specific IS NULL AND v_kind = 'working_hours' THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', 'a working-hours window needs a weekday or a date');\n END IF;\n IF v_dow IS NOT NULL AND v_dow NOT BETWEEN 0 AND 6 THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('weekday %s is outside 0..6', v_dow));\n END IF;\n -- Vig\u00EAncia invertida \u00E9 recusada aqui e n\u00E3o pela CHECK, para a linha ir para a\n -- quarentena com o motivo em vez de derrubar o lote inteiro.\n IF v_from IS NOT NULL AND v_to IS NOT NULL AND v_from > v_to THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('validity ends before it begins: %s\u2013%s', v_from, v_to));\n END IF;\n\n v_grid := nullif(p_row ->> 'grid_kind', '');\n IF v_grid IS NOT NULL AND v_grid NOT IN ('standard', 'exception') THEN\n v_meta := migration._deep_set(v_meta, ARRAY['migration', 'source_grid_kind'], to_jsonb(v_grid));\n v_grid := 'standard';\n END IF;\n\n -- A sala sabe a unidade quando o extrator n\u00E3o soube diz\u00EA-la.\n IF v_unit IS NULL AND v_resource IS NOT NULL THEN\n SELECT r.unit_id INTO v_unit FROM public.plg_agenda_resources r\n WHERE r.id = v_resource AND r.tenant_id = p_tenant;\n END IF;\n\n SELECT tenant_id INTO v_existing FROM public.schedules WHERE id = p_id;\n IF v_existing IS NOT NULL AND v_existing <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant',\n 'reason', format('schedule %s already exists in another tenant', p_id));\n END IF;\n\n IF v_existing IS NULL THEN\n INSERT INTO public.schedules\n (id, tenant_id, kind, assignee_id, location_id, unit_id, resource_id,\n day_of_week, specific_date, starts_at, ends_at, is_active,\n valid_from, valid_to, grid_kind, week_interval, slot_interval_minutes,\n metadata, created_at, updated_at)\n VALUES\n -- `location_id` E `unit_id`: a primeira \u00E9 a coluna de unidade que\n -- `scoped_resources` declara para `schedules`, a segunda \u00E9 a da RLS. As\n -- duas apontam para a mesma filial, e \u00E9 isso que as mant\u00E9m verdadeiras.\n (p_id, p_tenant, v_kind, v_assignee, v_unit, v_unit, v_resource,\n v_dow, v_specific, v_starts, v_ends,\n coalesce((p_row ->> 'is_active')::boolean, true),\n v_from, v_to, v_grid,\n coalesce(nullif(p_row ->> 'week_interval', '')::smallint, 0),\n nullif(p_row ->> 'slot_interval_minutes', '')::integer,\n v_meta,\n coalesce(nullif(p_row ->> 'created_at', '')::timestamptz, now()),\n coalesce(nullif(p_row ->> 'updated_at', '')::timestamptz, now()));\n v_created := true;\n ELSE\n UPDATE public.schedules SET\n kind = v_kind,\n assignee_id = coalesce(v_assignee, assignee_id),\n location_id = coalesce(v_unit, location_id),\n unit_id = coalesce(v_unit, unit_id),\n resource_id = coalesce(v_resource, resource_id),\n day_of_week = v_dow,\n specific_date = v_specific,\n starts_at = v_starts,\n ends_at = v_ends,\n is_active = coalesce((p_row ->> 'is_active')::boolean, is_active),\n valid_from = v_from,\n valid_to = v_to,\n grid_kind = v_grid,\n week_interval = coalesce(nullif(p_row ->> 'week_interval', '')::smallint, 0),\n slot_interval_minutes = nullif(p_row ->> 'slot_interval_minutes', '')::integer,\n metadata = migration._merge_meta(metadata, v_meta),\n updated_at = coalesce(nullif(p_row ->> 'updated_at', '')::timestamptz, now())\n WHERE id = p_id;\n END IF;\n\n PERFORM migration._set_unit('public.schedules', p_id, v_unit);\n\n IF v_standalone THEN\n PERFORM migration._audit(p_tenant, 'public.schedules', p_id,\n jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END));\n END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 10. As tr\u00EAs linhas do de-para\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\nSELECT migration._allow(\n 'service_locations', 'public.plg_agenda_resources', 'agenda', 'agenda_resource',\n jsonb_build_object('kind', 'room'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'name', 'name',\n 'active', 'is_active',\n 'max_simultaneous_clients', 'capacity',\n 'unit_company_id', 'unit_id@companies',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'unit_id', '-',\n 'id', 'jsonb:agenda.v1_service_location_ref',\n 'group_id', 'jsonb:agenda.v1_location_group_ref'),\n 'A SALA, n\u00E3o a filial. `service_locations.unit_id` \u00E9 um uuid que casa com '\n '`companies.uuid`; o de-para pede o id NUM\u00C9RICO de companies, que \u00E9 o que o '\n 'ledger resolve, e a ponte \u00E9 feita no extrator. 223 salas no tenant 1, 219 '\n 'ativas, em 127 unidades. Ver a migration 091: apontar esta tabela para '\n 'app.units transformaria cada sala numa filial.'\n);\n\nSELECT migration._allow(\n 'schedule_grid_blocks', 'public.schedules', 'agenda', 'agenda_schedule',\n jsonb_build_object('kind', 'working_hours'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'weekday', 'day_of_week',\n 'start_time', 'starts_at',\n 'end_time', 'ends_at',\n 'week_interval', 'week_interval',\n 'professional_id', 'assignee_id@professionals',\n 'service_location_id', 'resource_id@service_locations',\n 'unit_company_id', 'unit_id@companies',\n 'grid_valid_from', 'valid_from',\n 'grid_valid_to', 'valid_to',\n 'grid_kind', 'grid_kind',\n 'grid_slot_interval', 'slot_interval_minutes',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'grid_id', '-',\n 'grid_name', 'jsonb:agenda.grid_name',\n 'allowed_service_ids', 'jsonb:agenda.v1_allowed_service_ids'),\n 'A grade de atendimento. A GRADE (schedule_grids) n\u00E3o tem linha no destino: '\n 'vig\u00EAncia, tipo e intervalo de slot descem para cada bloco, porque `schedules` '\n 'descreve UMA janela. 1.331 blocos no tenant 1 contra 44.493 se fossem '\n 'expandidos por data \u2014 e expandir perderia a grade eterna e mataria o espelho '\n 'D-1. O cursor do extrator \u00E9 greatest(bloco, grade): mexer s\u00F3 na grade n\u00E3o '\n 'move o updated_at do bloco. `allowed_service_ids` vai para metadata porque '\n 'est\u00E1 NULL em 1.331 de 1.331 \u2014 o recurso existe no V1 e ningu\u00E9m usa.'\n);\n\nSELECT migration._allow(\n 'schedule_blocks', 'public.schedules', 'agenda', 'agenda_schedule',\n jsonb_build_object('kind', 'block'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'professional_id', 'assignee_id@professionals',\n 'start_time', 'starts_at',\n 'end_time', 'ends_at',\n 'date_from', 'valid_from',\n 'date_to', 'valid_to',\n 'v1_specific_date', 'specific_date',\n 'all_day', 'all_day',\n 'week_interval', 'week_interval',\n 'weekdays', 'v1_weekdays',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'created_by', '-',\n 'title', 'jsonb:agenda.title',\n 'description', 'jsonb:agenda.description'),\n 'O bloqueio: almo\u00E7o, feriado, \"a dra. vai atender\". Vai para `schedules` '\n 'kind=block e n\u00E3o para `appointments` kind=block porque o bloqueio do V1 pode '\n 'ser RECORRENTE (weekdays + week_interval + faixa de datas) e um appointment '\n '\u00E9 evento \u00FAnico. Sem unidade de prop\u00F3sito: o bloqueio \u00E9 da PESSOA, e unit_id '\n 'nulo significa \"vale onde ela atende\". 8 linhas no tenant 1, todas de um dia.'\n);\n\n-- A grade sai da excluded_tables com o motivo REESCRITO. N\u00E3o \u00E9 \"sem destino\":\n-- ela atravessa inteira, dentro do bloco. Sem isto o dashboard de cobertura\n-- marcaria 482 linhas em vermelho que na verdade est\u00E3o no destino.\nDELETE FROM migration.excluded_tables\n WHERE source_table IN ('schedule_grid_blocks', 'service_locations');\n\nINSERT INTO migration.excluded_tables (source_table, reason)\nVALUES ('schedule_grids',\n 'N\u00E3o tem linha pr\u00F3pria no destino, e isso \u00E9 a decis\u00E3o \u2014 n\u00E3o a aus\u00EAncia '\n 'dela. Vig\u00EAncia, tipo e intervalo de slot descem para cada '\n 'schedule_grid_blocks, que vira public.schedules. Ver plugin-agenda/010.')\nON CONFLICT (source_table) WHERE tenant_id IS NULL\nDO UPDATE SET reason = EXCLUDED.reason;\n\nRESET migration.allowlist_unlock;\n\nREVOKE ALL ON FUNCTION migration.upsert_agenda_resource(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_agenda_resource(uuid, uuid, jsonb, jsonb) TO service_role;\nREVOKE ALL ON FUNCTION migration.upsert_agenda_schedule(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_agenda_schedule(uuid, uuid, jsonb, jsonb) TO service_role;\n";
12
+ export declare const MIGRATION_011_A_SALA_ENTRA_NO_COMPROMISSO = "-- A sala entra no compromisso.\n--\n-- 010 criou `plg_agenda_resources` e `appointments.resource_id`, e trouxe as\n-- 159 salas. Os 353.895 compromissos j\u00E1 migrados continuam apontando para a\n-- sala por um N\u00DAMERO cru em `metadata.agenda.v1_service_location_ref` \u2014 que era\n-- tudo o que existia quando 008 rodou.\n--\n-- Duas coisas aqui, e a ordem importa:\n--\n-- 1. O de-para passa a mapear `service_location_id` para `resource_id`, para\n-- a pr\u00F3xima onda preencher a coluna sozinha.\n-- 2. O que J\u00C1 atravessou \u00E9 costurado pelo ledger, sem reler a origem. S\u00E3o\n-- 352.002 linhas com a refer\u00EAncia guardada; reoferec\u00EA-las ao motor custaria\n-- uma varredura de 354 mil linhas no V1 de produ\u00E7\u00E3o para escrever uma\n-- coluna que o pr\u00F3prio destino j\u00E1 sabe deduzir.\n--\n-- A costura \u00E9 pelo LEDGER e n\u00E3o por adivinha\u00E7\u00E3o: `migration.ledger` sabe que o\n-- `service_locations` n\u00FAmero N daquele projeto virou o uuid X naquele tenant. \u00C9\n-- a mesma tabela que o writer consultaria, s\u00F3 que consultada uma vez para todos.\n--\n-- Idempotente: o UPDATE s\u00F3 toca linha com `resource_id` nulo.\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\n-- `service_location_id` deixa de ir s\u00F3 para metadata e passa a resolver pelo\n-- ledger. A entrada de metadata continua existindo nas linhas antigas \u2014 e isso\n-- n\u00E3o \u00E9 sujeira: \u00E9 o n\u00FAmero da origem, que \u00E9 o que permite auditar esta costura\n-- depois de ela ter acontecido.\nUPDATE migration.allowlist\n SET columns = columns || jsonb_build_object('service_location_id', 'resource_id@service_locations')\n WHERE source_table = 'appointments';\n\nRESET migration.allowlist_unlock;\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- A costura\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Em lotes, e n\u00E3o num UPDATE s\u00F3: 352 mil linhas numa transa\u00E7\u00E3o seguram a tabela\n-- que a agenda l\u00EA o dia inteiro. O la\u00E7o para quando uma passada n\u00E3o move nada,\n-- que \u00E9 tamb\u00E9m o que torna o arquivo replay-safe \u2014 na segunda vez a primeira\n-- passada j\u00E1 n\u00E3o acha linha.\nDO $$\nDECLARE\n v_moved integer;\n v_total bigint := 0;\nBEGIN\n IF to_regclass('migration.ledger') IS NULL THEN RETURN; END IF;\n\n LOOP\n WITH alvo AS (\n SELECT a.id, l.target_id AS resource_id\n FROM public.appointments a\n JOIN migration.ledger l\n ON l.tenant_id = a.tenant_id\n AND l.source_table = 'service_locations'\n AND l.status = 'migrated'\n AND l.source_numeric_id = (a.metadata -> 'agenda' ->> 'v1_service_location_ref')::bigint\n WHERE a.resource_id IS NULL\n AND a.metadata -> 'agenda' ? 'v1_service_location_ref'\n LIMIT 5000\n )\n UPDATE public.appointments a\n SET resource_id = alvo.resource_id\n FROM alvo\n WHERE a.id = alvo.id;\n\n GET DIAGNOSTICS v_moved = ROW_COUNT;\n v_total := v_total + v_moved;\n EXIT WHEN v_moved = 0;\n END LOOP;\n\n RAISE NOTICE 'appointments.resource_id costurado em % linha(s)', v_total;\nEND $$;\n\n-- A agenda por sala: \"o que acontece na Sala 2 amanh\u00E3\".\nCREATE INDEX IF NOT EXISTS idx_appointments_tenant_resource_starts\n ON public.appointments USING btree (tenant_id, resource_id, starts_at)\n WHERE resource_id IS NOT NULL;\n";
13
+ export declare const MIGRATION_012_A_SALA_SEM_NOME_TAMBEM_E_UMA_SALA = "-- A sala sem nome tamb\u00E9m \u00E9 uma sala.\n--\n-- 010 recusou a sala cujo `name` vem vazio: sem nome, o writer mandava para a\n-- quarentena. Uma linha de 223 \u2014 e 17 compromissos apontam para ela.\n--\n-- Recusar estava errado pelo mesmo motivo que `upsert_appointment_item` j\u00E1\n-- tinha resolvido para o servi\u00E7o: o nome \u00E9 uma ETIQUETA, e a falta dela n\u00E3o\n-- torna a linha amb\u00EDgua. O que se perde recusando \u00E9 o V\u00CDNCULO \u2014 17 atendimentos\n-- ficam sem saber em que sala aconteceram, para poupar um r\u00F3tulo feio.\n--\n-- Ent\u00E3o cai para o n\u00FAmero da origem, que \u00E9 sempre verdadeiro e diz na cara que\n-- \u00E9 um remendo: \"Sala 576\". Quem for arrumar o cadastro no V1 v\u00EA a linha e sabe\n-- o que procurar.\n--\n-- Idempotente.\n\nCREATE OR REPLACE FUNCTION migration.upsert_agenda_resource(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_unit uuid;\n v_name text;\n v_ref text;\n v_cap integer;\n v_existing uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'a resource needs an id');\n END IF;\n\n BEGIN v_unit := (p_row ->> 'unit_id')::uuid; EXCEPTION WHEN OTHERS THEN v_unit := NULL; END;\n -- Sala sem unidade continua recusada, e a diferen\u00E7a com o nome \u00E9 essa: uma\n -- sala sem unidade apareceria na agenda de todas as 63 filiais. N\u00E3o \u00E9 r\u00F3tulo,\n -- \u00E9 escopo.\n IF v_unit IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', 'a resource needs the unit it lives in');\n END IF;\n\n v_ref := nullif(v_meta -> 'agenda' ->> 'v1_service_location_ref', '');\n v_name := coalesce(\n nullif(trim(coalesce(p_row ->> 'name', '')), ''),\n CASE WHEN v_ref IS NOT NULL THEN 'Sala ' || v_ref END,\n 'Sala');\n\n v_cap := greatest(coalesce(nullif(p_row ->> 'capacity', '')::integer, 1), 1);\n\n SELECT tenant_id INTO v_existing FROM public.plg_agenda_resources WHERE id = p_id;\n IF v_existing IS NOT NULL AND v_existing <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant',\n 'reason', format('resource %s already exists in another tenant', p_id));\n END IF;\n\n IF v_existing IS NULL THEN\n INSERT INTO public.plg_agenda_resources\n (id, tenant_id, unit_id, name, kind, capacity, is_active, sort_order, metadata, created_at, updated_at)\n VALUES\n (p_id, p_tenant, v_unit, v_name, coalesce(p_args ->> 'kind', 'room'), v_cap,\n coalesce((p_row ->> 'is_active')::boolean, true),\n coalesce(nullif(p_row ->> 'sort_order', '')::integer, 0), v_meta,\n coalesce(nullif(p_row ->> 'created_at', '')::timestamptz, now()),\n coalesce(nullif(p_row ->> 'updated_at', '')::timestamptz, now()));\n v_created := true;\n ELSE\n UPDATE public.plg_agenda_resources SET\n unit_id = v_unit,\n name = v_name,\n capacity = v_cap,\n is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END,\n metadata = migration._merge_meta(metadata, v_meta),\n updated_at = coalesce(nullif(p_row ->> 'updated_at', '')::timestamptz, now())\n WHERE id = p_id;\n END IF;\n\n IF v_standalone THEN\n PERFORM migration._audit(p_tenant, 'public.plg_agenda_resources', p_id,\n jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END));\n END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\nREVOKE ALL ON FUNCTION migration.upsert_agenda_resource(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_agenda_resource(uuid, uuid, jsonb, jsonb) TO service_role;\n";
14
+ export declare const MIGRATION_013_O_SELO_DO_ATENDIMENTO_TEM_ONDE_MORAR = "-- O selo do atendimento tem onde morar.\n--\n-- A grade, a sala e o bloqueio atravessaram nas 010..012: 1.331 janelas, 160\n-- salas, 352.002 agendamentos costurados \u00E0 sala. O que ficou de fora s\u00E3o os\n-- V\u00CDNCULO que a tela do atendimento l\u00EA para pintar selo \u2014 pouca linha, feature\n-- inteira.\n--\n-- appointment_prepaid_items 53 o selo \"Compra antecipada\"\n--\n-- \u2500\u2500 Por que 53 linhas merecem uma tabela \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- O V2 j\u00E1 tem o DESFECHO do item pr\u00E9-pago: `plg_clinical_prepaid_outcomes`, 30\n-- linhas migradas. N\u00E3o tem a PERGUNTA \u2014 o que este agendamento veio consumir.\n-- \u00C9 `PrepaidOutcomeGate.tsx` no V1, e o coment\u00E1rio do arquivo diz a fonte com\n-- todas as letras: \"Fonte: appointment_prepaid_items (o que o agendamento veio\n-- validar)\". Sem ela o port\u00E3o do V2 n\u00E3o tem o que perguntar, e o desfecho fica\n-- sendo a resposta de uma pergunta que sumiu.\n--\n-- \u2500\u2500 Por que a FK para public.items \u00E9 legal aqui \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `appointment_prepaid_items.invoice_item_id` aponta para a parcela do pacote.\n-- No V2 isso \u00E9 `public.items`, tabela da BASE \u2014 e financial roda ANTES de\n-- agenda na chain. Apontar para `plg_financial_*` seria o contr\u00E1rio e quebraria\n-- a ordem; apontar para `items` n\u00E3o.\n--\n-- \u2500\u2500 O que N\u00C3O est\u00E1 aqui \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `appointment_interests` (762 linhas) \u00E9 o selo \"Interesses\", e ele N\u00C3O mora\n-- aqui: aponta para `opportunity_interests`, que \u00E9 a cesta do funil. Uma tabela\n-- de agenda n\u00E3o pode ter FK numa de crm, porque crm roda DEPOIS. As duas vivem\n-- em plugin-crm, na migration da cesta.\n--\n-- `online_booking_configs` tamb\u00E9m n\u00E3o: \u00E9 CONFIGURA\u00C7\u00C3O de uma linha s\u00F3, e\n-- configura\u00E7\u00E3o no V2 n\u00E3o \u00E9 tabela \u2014 mora em `tenants.settings.plugins.agenda`.\n-- Ver a 014, que desfaz a tabela que esta migration chegou a criar.\n--\n-- `schedules.slot_interval_minutes` j\u00E1 est\u00E1 certo: 448 grades em 30min, 33 em\n-- 15min, 1 em 5min no V1; 1.280 / 50 / 1 no V2. A queixa \"intervalo por grade\"\n-- \u00E9 tela que n\u00E3o l\u00EA a coluna, n\u00E3o porte. Nada a fazer aqui.\n--\n-- Idempotente.\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 1. O que este agendamento veio consumir\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nCREATE TABLE IF NOT EXISTS public.plg_agenda_appointment_prepaid_items (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n -- A cerca da franquia. Herdada do agendamento: n\u00E3o h\u00E1 item pr\u00E9-pago sem\n -- agendamento, e o agendamento sempre tem unidade.\n unit_id uuid NOT NULL,\n appointment_id uuid NOT NULL,\n -- A parcela do pacote que este atendimento consome.\n item_id uuid NOT NULL,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT plg_agenda_appointment_prepaid_items_pkey PRIMARY KEY (id)\n);\n\nDO $$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_agenda_prepaid_tenant_fk') THEN\n ALTER TABLE public.plg_agenda_appointment_prepaid_items\n ADD CONSTRAINT plg_agenda_prepaid_tenant_fk\n FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_agenda_prepaid_appointment_fk') THEN\n ALTER TABLE public.plg_agenda_appointment_prepaid_items\n ADD CONSTRAINT plg_agenda_prepaid_appointment_fk\n FOREIGN KEY (appointment_id) REFERENCES public.appointments(id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_agenda_prepaid_item_fk') THEN\n ALTER TABLE public.plg_agenda_appointment_prepaid_items\n ADD CONSTRAINT plg_agenda_prepaid_item_fk\n FOREIGN KEY (item_id) REFERENCES public.items(id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_agenda_prepaid_unit_fk') THEN\n ALTER TABLE public.plg_agenda_appointment_prepaid_items\n ADD CONSTRAINT plg_agenda_prepaid_unit_fk\n FOREIGN KEY (unit_id) REFERENCES app.units(id) ON DELETE CASCADE;\n END IF;\nEND $$;\n\n-- O mesmo item n\u00E3o se consome duas vezes no mesmo agendamento.\nCREATE UNIQUE INDEX IF NOT EXISTS plg_agenda_prepaid_uq\n ON public.plg_agenda_appointment_prepaid_items (appointment_id, item_id);\n-- A pergunta da tela: o que ESTE agendamento veio validar.\nCREATE INDEX IF NOT EXISTS plg_agenda_prepaid_appointment_idx\n ON public.plg_agenda_appointment_prepaid_items (tenant_id, appointment_id);\n-- A pergunta do port\u00E3o: este item j\u00E1 foi consumido em algum lugar?\nCREATE INDEX IF NOT EXISTS plg_agenda_prepaid_item_idx\n ON public.plg_agenda_appointment_prepaid_items (tenant_id, item_id);\n\nCOMMENT ON TABLE public.plg_agenda_appointment_prepaid_items IS\n 'O que o agendamento veio consumir de um pacote j\u00E1 pago \u2014 o selo \"Compra antecipada\". \u2190 appointment_prepaid_items';\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 3. updated_at\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nDO $$\nDECLARE t text;\nBEGIN\n FOREACH t IN ARRAY ARRAY['plg_agenda_appointment_prepaid_items'] LOOP\n EXECUTE format('DROP TRIGGER IF EXISTS %I ON public.%I', t || '_updated_at', t);\n EXECUTE format('CREATE TRIGGER %I BEFORE UPDATE ON public.%I FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at()', t || '_updated_at', t);\n END LOOP;\nEND $$;\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 4. Quem enxerga\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n--\n-- O item pr\u00E9-pago \u00E9 de UNIDADE, como a sala da 010: quem v\u00EA a agenda da filial\n-- v\u00EA o que os agendamentos dela vieram consumir.\n--\n-- A configura\u00E7\u00E3o do agendamento online \u00E9 de TENANT e exige `agenda.manage`:\n-- \u00E9 a p\u00E1gina que o p\u00FAblico v\u00EA, e mud\u00E1-la n\u00E3o \u00E9 tarefa de recep\u00E7\u00E3o.\n\nALTER TABLE public.plg_agenda_appointment_prepaid_items ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_agenda_appointment_prepaid_items FORCE ROW LEVEL SECURITY;\nGRANT SELECT, INSERT, UPDATE, DELETE ON TABLE public.plg_agenda_appointment_prepaid_items TO authenticated;\nGRANT ALL ON TABLE public.plg_agenda_appointment_prepaid_items TO service_role;\n\nDROP POLICY IF EXISTS plg_agenda_prepaid_authz_select ON public.plg_agenda_appointment_prepaid_items;\nCREATE POLICY plg_agenda_prepaid_authz_select ON public.plg_agenda_appointment_prepaid_items\n FOR SELECT TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_unit(plg_agenda_appointment_prepaid_items.unit_id))\n AND (SELECT app.has_permission('agenda.read', plg_agenda_appointment_prepaid_items.unit_id)));\n\nDROP POLICY IF EXISTS plg_agenda_prepaid_authz_insert ON public.plg_agenda_appointment_prepaid_items;\nCREATE POLICY plg_agenda_prepaid_authz_insert ON public.plg_agenda_appointment_prepaid_items\n FOR INSERT TO authenticated WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_unit(plg_agenda_appointment_prepaid_items.unit_id))\n AND (SELECT app.has_permission('agenda.create', plg_agenda_appointment_prepaid_items.unit_id)));\n\nDROP POLICY IF EXISTS plg_agenda_prepaid_authz_update ON public.plg_agenda_appointment_prepaid_items;\nCREATE POLICY plg_agenda_prepaid_authz_update ON public.plg_agenda_appointment_prepaid_items\n FOR UPDATE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_unit(plg_agenda_appointment_prepaid_items.unit_id))\n AND (SELECT app.has_permission('agenda.edit', plg_agenda_appointment_prepaid_items.unit_id)))\n WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_unit(plg_agenda_appointment_prepaid_items.unit_id))\n AND (SELECT app.has_permission('agenda.edit', plg_agenda_appointment_prepaid_items.unit_id)));\n\nDROP POLICY IF EXISTS plg_agenda_prepaid_authz_delete ON public.plg_agenda_appointment_prepaid_items;\nCREATE POLICY plg_agenda_prepaid_authz_delete ON public.plg_agenda_appointment_prepaid_items\n FOR DELETE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_unit(plg_agenda_appointment_prepaid_items.unit_id))\n AND (SELECT app.has_permission('agenda.delete', plg_agenda_appointment_prepaid_items.unit_id)));\n\n-- N\u00C3O entra em `scoped_resources`, e isso \u00E9 decis\u00E3o. Aquele registro existe para\n-- o RECURSO que a tela de visibilidade oferece \u2014 e exige `owner_id`, porque a\n-- pergunta que ele responde \u00E9 \"quem \u00E9 o dono desta linha\". `plg_agenda_appointment_prepaid_items`\n-- \u00E9 uma PONTE: ela n\u00E3o tem dono, herda a unidade de quem ela liga, e a cerca\n-- dela j\u00E1 est\u00E1 nas pol\u00EDticas acima. Registr\u00E1-la ali pediria uma coluna\n-- `owner_id` inventada s\u00F3 para satisfazer um NOT NULL.\n";
15
+ export declare const MIGRATION_014_A_CONFIGURACAO_NAO_E_UMA_TABELA = "-- A configura\u00E7\u00E3o n\u00E3o \u00E9 uma tabela.\n--\n-- A 013 criou `public.plg_agenda_booking_configs` para receber\n-- `online_booking_configs` do V1. Era uma tabela de UMA linha, e foi erro meu.\n-- Esta migration a desfaz.\n--\n-- \u2500\u2500 Como o SDK guarda configura\u00E7\u00E3o de plugin \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `packages/admin/src/hooks/useTenantPluginSettings.ts`, e est\u00E1 escrito no\n-- cabe\u00E7alho do pr\u00F3prio arquivo:\n--\n-- tenants.settings = { ..., plugins: { [pluginId]: { key: value, ... } } }\n--\n-- A leitura sai de `currentOrg.settings.plugins[pluginId]` e a escrita de\n-- `adapter.updateOrg(orgId, { settings })`. \u00C9 por a\u00ED que passam as telas de\n-- /settings de agenda, crm, inventory, financial, tasks e courses.\n--\n-- E a agenda J\u00C1 usa: `AgendaGeneralSettings.tsx` importa\n-- `useTenantPluginSettings` e declara `AGENDA_TOGGLE_DEFAULTS`. Criar\n-- `plg_agenda_booking_configs` abriria um SEGUNDO lugar para a configura\u00E7\u00E3o da\n-- agenda \u2014 a tela lendo de um, o porte escrevendo no outro.\n--\n-- \u2500\u2500 Mas existem plg_*_settings no cluster \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Existem, e n\u00E3o contradizem: `plg_shop_settings`, `plg_tables_settings`,\n-- `plg_reservations_settings`, `plg_crm_engagement_settings`,\n-- `plg_inventory_product_settings`. Todas t\u00EAm `unit_id` e existem porque a\n-- configura\u00E7\u00E3o delas varia POR UNIDADE e \u00E9 lida por RPC no caminho quente\n-- (fechar comanda, calcular frete) \u2014 n\u00E3o por uma tela de toggles.\n--\n-- `online_booking_configs` n\u00E3o \u00E9 nenhum dos dois casos: UMA linha no V1\n-- inteiro, tenant 1, e ningu\u00E9m a l\u00EA num caminho quente. \u00C9 toggle.\n--\n-- A troco de registro, se um dia ela precisar variar por unidade, o lugar certo\n-- \u00E9 `plg_agenda_settings` com `unit_id`, no formato das outras cinco \u2014 e n\u00E3o\n-- `_configs`, que n\u00E3o \u00E9 nome que este SDK usa.\n--\n-- \u2500\u2500 O que se perde, e \u00E9 de prop\u00F3sito \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `allowed_units`, `allowed_services` e `allowed_professionals` viram arrays de\n-- uuid DENTRO do jsonb, sem chave estrangeira. Apagar uma unidade deixa o id\n-- \u00F3rf\u00E3o na lista. \u00C9 a mesma troca que `tenants.settings.branding` j\u00E1 faz, e\n-- para uma linha ela paga: integridade referencial num array de configura\u00E7\u00E3o\n-- custaria uma tabela de liga\u00E7\u00E3o para resolver um problema que o V1 n\u00E3o tem em\n-- nenhum tenant.\n--\n-- Idempotente. A tabela nunca recebeu linha: o porte dela ainda n\u00E3o rodou.\n\nDO $$\nBEGIN\n IF to_regclass('public.plg_agenda_booking_configs') IS NOT NULL THEN\n IF EXISTS (SELECT 1 FROM public.plg_agenda_booking_configs) THEN\n RAISE EXCEPTION\n 'plg_agenda_booking_configs tem linha \u2014 mover para tenants.settings.plugins.agenda antes de derrubar';\n END IF;\n DROP TABLE public.plg_agenda_booking_configs;\n END IF;\nEND $$;\n";
11
16
  export declare const MIGRATIONS: Array<{
12
17
  id: string;
13
18
  sql: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,05vEAg2ClC,CAAA;AAED,eAAO,MAAM,uDAAuD,mvEAgDnE,CAAA;AAED,eAAO,MAAM,4CAA4C,w6EAqDyB,CAAA;AAElF,eAAO,MAAM,oDAAoD,61CAyBhE,CAAA;AAED,eAAO,MAAM,8CAA8C,w7EA4C1D,CAAA;AAED,eAAO,MAAM,iDAAiD,woFAiD7D,CAAA;AAED,eAAO,MAAM,oDAAoD,kxOAmHhE,CAAA;AAED,eAAO,MAAM,4CAA4C,mrZA2OxD,CAAA;AAED,eAAO,MAAM,sCAAsC,8tuBAoXlD,CAAA;AAED,eAAO,MAAM,4CAA4C,wkIA8DxD,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAWzD,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,05vEAg2ClC,CAAA;AAED,eAAO,MAAM,uDAAuD,mvEAgDnE,CAAA;AAED,eAAO,MAAM,4CAA4C,w6EAqDyB,CAAA;AAElF,eAAO,MAAM,oDAAoD,61CAyBhE,CAAA;AAED,eAAO,MAAM,8CAA8C,w7EA4C1D,CAAA;AAED,eAAO,MAAM,iDAAiD,woFAiD7D,CAAA;AAED,eAAO,MAAM,oDAAoD,kxOAmHhE,CAAA;AAED,eAAO,MAAM,4CAA4C,mrZA2OxD,CAAA;AAED,eAAO,MAAM,sCAAsC,8tuBAoXlD,CAAA;AAED,eAAO,MAAM,4CAA4C,wkIA8DxD,CAAA;AAED,eAAO,MAAM,qCAAqC,q5pDAo1BjD,CAAA;AAED,eAAO,MAAM,yCAAyC,0rIA+ErD,CAAA;AAED,eAAO,MAAM,+CAA+C,+sIA0F3D,CAAA;AAED,eAAO,MAAM,kDAAkD,ogYAoK9D,CAAA;AAED,eAAO,MAAM,6CAA6C,w/GA0DzD,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAgBzD,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"createPublicBookingPlugin.d.ts","sourceRoot":"","sources":["../../src/public/createPublicBookingPlugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,EAAE,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAE9D,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAE7F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAClD,OAAO,EAAmC,KAAK,yBAAyB,EAAE,MAAM,QAAQ,CAAA;AAExF,OAAO,EAAmD,KAAK,kBAAkB,EAAE,MAAM,WAAW,CAAA;AAGpG,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,qBAAqB,EACrB,mBAAmB,EACnB,aAAa,EAGb,YAAY,EACb,MAAM,SAAS,CAAA;AAEhB,MAAM,WAAW,oBAAoB;IACnC,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,YAAY,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC7C;;;OAGG;IACH,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAA;IAC1B,gFAAgF;IAChF,YAAY,CAAC,EAAE,YAAY,CAAA;IAC3B,sDAAsD;IACtD,MAAM,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAA;IAC/B,wDAAwD;IACxD,MAAM,CAAC,EAAE,OAAO,CAAC,mBAAmB,CAAC,CAAA;IACrC,wEAAwE;IACxE,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAA;IACvC;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,qBAAqB,CAAA;IACzC,uFAAuF;IACvF,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,qFAAqF;IACrF,eAAe,CAAC,EAAE,eAAe,CAAA;IACjC,oFAAoF;IACpF,kBAAkB,CAAC,EAAE,kBAAkB,CAAA;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,4EAA4E;IAC5E,YAAY,CAAC,EAAE,yBAAyB,CAAA;IACxC,8CAA8C;IAC9C,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,CAAA;CACxB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,cAAc,CAAA;IACxB,QAAQ,EAAE,EAAE,CAAC;QAAE,QAAQ,EAAE,SAAS,CAAA;KAAE,CAAC,CAAA;IACrC,YAAY,EAAE,yBAAyB,CAAA;CACxC;AAqGD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,oBAAoB,GAAG,mBAAmB,CAqG5F"}
1
+ {"version":3,"file":"createPublicBookingPlugin.d.ts","sourceRoot":"","sources":["../../src/public/createPublicBookingPlugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,EAAE,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAE9D,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAE7F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAClD,OAAO,EAAmC,KAAK,yBAAyB,EAAE,MAAM,QAAQ,CAAA;AAExF,OAAO,EAAmD,KAAK,kBAAkB,EAAE,MAAM,WAAW,CAAA;AAGpG,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,qBAAqB,EACrB,mBAAmB,EACnB,aAAa,EAGb,YAAY,EACb,MAAM,SAAS,CAAA;AAEhB,MAAM,WAAW,oBAAoB;IACnC,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,YAAY,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC7C;;;OAGG;IACH,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAA;IAC1B,gFAAgF;IAChF,YAAY,CAAC,EAAE,YAAY,CAAA;IAC3B,sDAAsD;IACtD,MAAM,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAA;IAC/B,wDAAwD;IACxD,MAAM,CAAC,EAAE,OAAO,CAAC,mBAAmB,CAAC,CAAA;IACrC,wEAAwE;IACxE,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAA;IACvC;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,qBAAqB,CAAA;IACzC,uFAAuF;IACvF,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,qFAAqF;IACrF,eAAe,CAAC,EAAE,eAAe,CAAA;IACjC,oFAAoF;IACpF,kBAAkB,CAAC,EAAE,kBAAkB,CAAA;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,4EAA4E;IAC5E,YAAY,CAAC,EAAE,yBAAyB,CAAA;IACxC,8CAA8C;IAC9C,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,CAAA;CACxB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,cAAc,CAAA;IACxB,QAAQ,EAAE,EAAE,CAAC;QAAE,QAAQ,EAAE,SAAS,CAAA;KAAE,CAAC,CAAA;IACrC,YAAY,EAAE,yBAAyB,CAAA;CACxC;AAqGD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,oBAAoB,GAAG,mBAAmB,CAoG5F"}
@@ -1,5 +1,5 @@
1
- import { createMockAgendaProvider, createSupabasePublicBookingProvider } from '../chunk-PDYBAVXU.js';
2
- export { createSupabasePublicBookingProvider } from '../chunk-PDYBAVXU.js';
1
+ import { createMockAgendaProvider, createSupabasePublicBookingProvider } from '../chunk-RVO5ILXN.js';
2
+ export { createSupabasePublicBookingProvider } from '../chunk-RVO5ILXN.js';
3
3
  import { createContext, useState, useEffect, useMemo, useContext, useRef, createElement } from 'react';
4
4
  import { ChevronLeft, ShieldCheck, Clock, ChevronRight, User, Check, Calendar, ChevronDown, Loader2, CreditCard, Plus, QrCode, CalendarCheck, Copy, Video } from 'lucide-react';
5
5
  import { jsx, jsxs } from 'react/jsx-runtime';
@@ -1292,8 +1292,7 @@ function createPublicBookingPlugin(options) {
1292
1292
  id: mockProfessionalId,
1293
1293
  name: professionalName,
1294
1294
  avatarUrl: null,
1295
- locationId: null,
1296
- locationName: null,
1295
+ unitIds: null,
1297
1296
  isActive: true
1298
1297
  };
1299
1298
  const mockSeed = options.seed ?? {