@fayz-ai/plugin-agenda 0.8.1 → 0.8.3

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 (117) hide show
  1. package/dist/{chunk-LFUELUIT.cjs → chunk-3KWNHV5A.cjs} +60 -2
  2. package/dist/chunk-3KWNHV5A.cjs.map +1 -0
  3. package/dist/{chunk-DRZF6CV2.js → chunk-YQO6B4G3.js} +60 -3
  4. package/dist/chunk-YQO6B4G3.js.map +1 -0
  5. package/dist/components/AgendaGeneralSettings.d.ts.map +1 -1
  6. package/dist/components/AppointmentModal.d.ts.map +1 -1
  7. package/dist/components/CalendarToolbar.d.ts.map +1 -1
  8. package/dist/data/supabase.d.ts +5 -1
  9. package/dist/data/supabase.d.ts.map +1 -1
  10. package/dist/index.cjs +1071 -766
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +914 -609
  14. package/dist/index.js.map +1 -1
  15. package/dist/integrations/google-calendar/components/CalendarColorDot.d.ts +6 -0
  16. package/dist/integrations/google-calendar/components/CalendarColorDot.d.ts.map +1 -0
  17. package/dist/integrations/google-calendar/components/ChannelMappingList.d.ts +9 -0
  18. package/dist/integrations/google-calendar/components/ChannelMappingList.d.ts.map +1 -0
  19. package/dist/integrations/google-calendar/components/ImportEventsWizard.d.ts +12 -0
  20. package/dist/integrations/google-calendar/components/ImportEventsWizard.d.ts.map +1 -0
  21. package/dist/integrations/google-calendar/components/useAgendaLookups.d.ts +26 -0
  22. package/dist/integrations/google-calendar/components/useAgendaLookups.d.ts.map +1 -0
  23. package/dist/integrations/google-calendar/connectorDef.d.ts.map +1 -1
  24. package/dist/integrations/google-calendar/data/supabase.d.ts +33 -1
  25. package/dist/integrations/google-calendar/data/supabase.d.ts.map +1 -1
  26. package/dist/integrations/google-calendar/data/tables.d.ts +6 -0
  27. package/dist/integrations/google-calendar/data/tables.d.ts.map +1 -0
  28. package/dist/integrations/google-calendar/index.d.ts +1 -1
  29. package/dist/integrations/google-calendar/index.d.ts.map +1 -1
  30. package/dist/integrations/google-calendar/mapping.d.ts +56 -4
  31. package/dist/integrations/google-calendar/mapping.d.ts.map +1 -1
  32. package/dist/integrations/google-calendar/types.d.ts +101 -0
  33. package/dist/integrations/google-calendar/types.d.ts.map +1 -1
  34. package/dist/locales/en.d.ts.map +1 -1
  35. package/dist/locales/pt-BR.d.ts.map +1 -1
  36. package/dist/migrations/index.d.ts +11 -0
  37. package/dist/migrations/index.d.ts.map +1 -1
  38. package/dist/public/BookingWidget.d.ts.map +1 -1
  39. package/dist/public/context.d.ts +3 -1
  40. package/dist/public/context.d.ts.map +1 -1
  41. package/dist/public/createPublicBookingPlugin.d.ts +7 -1
  42. package/dist/public/createPublicBookingPlugin.d.ts.map +1 -1
  43. package/dist/public/data.d.ts +6 -0
  44. package/dist/public/data.d.ts.map +1 -1
  45. package/dist/public/data.supabase.d.ts.map +1 -1
  46. package/dist/public/format.d.ts +22 -1
  47. package/dist/public/format.d.ts.map +1 -1
  48. package/dist/public/hooks.d.ts.map +1 -1
  49. package/dist/public/index.cjs +31 -63
  50. package/dist/public/index.cjs.map +1 -1
  51. package/dist/public/index.d.ts +1 -1
  52. package/dist/public/index.d.ts.map +1 -1
  53. package/dist/public/index.js +27 -61
  54. package/dist/public/index.js.map +1 -1
  55. package/dist/public/steps/ContactStep.d.ts +4 -2
  56. package/dist/public/steps/ContactStep.d.ts.map +1 -1
  57. package/dist/public/types.d.ts +16 -0
  58. package/dist/public/types.d.ts.map +1 -1
  59. package/dist/views/CalendarView.d.ts.map +1 -1
  60. package/package.json +5 -5
  61. package/src/components/AgendaGeneralSettings.tsx +38 -16
  62. package/src/components/AppointmentModal.tsx +50 -108
  63. package/src/components/CalendarToolbar.tsx +10 -7
  64. package/src/data/supabase.ts +67 -13
  65. package/src/index.ts +56 -8
  66. package/src/integrations/google-calendar/components/CalendarColorDot.tsx +23 -0
  67. package/src/integrations/google-calendar/components/ChannelMappingList.tsx +329 -0
  68. package/src/integrations/google-calendar/components/ImportEventsWizard.tsx +215 -0
  69. package/src/integrations/google-calendar/components/useAgendaLookups.ts +100 -0
  70. package/src/integrations/google-calendar/connectorDef.tsx +123 -40
  71. package/src/integrations/google-calendar/data/supabase.ts +109 -13
  72. package/src/integrations/google-calendar/data/tables.ts +12 -0
  73. package/src/integrations/google-calendar/index.ts +10 -1
  74. package/src/integrations/google-calendar/integration.test.ts +126 -1
  75. package/src/integrations/google-calendar/mapping.ts +149 -9
  76. package/src/integrations/google-calendar/migrations/001_google_calendar.sql +9 -6
  77. package/src/integrations/google-calendar/migrations/002_smart_sync.sql +517 -0
  78. package/src/integrations/google-calendar/migrations/003_plg_rename.sql +273 -0
  79. package/src/integrations/google-calendar/types.ts +111 -0
  80. package/src/locales/en.ts +1 -0
  81. package/src/locales/pt-BR.ts +1 -0
  82. package/src/migrations/003_public_client_kind.sql +175 -0
  83. package/src/migrations/004_public_booking_assignee.sql +176 -0
  84. package/src/migrations/005_agent_rpcs.sql +168 -0
  85. package/src/migrations/006_agent_rpc_localtime.sql +181 -0
  86. package/src/migrations/007_schedule_exception_precedence.sql +290 -0
  87. package/src/migrations/008_agent_rpc_exception_precedence.sql +172 -0
  88. package/src/migrations/009_public_booking_location.sql +162 -0
  89. package/src/migrations/010_public_service_curation.sql +31 -0
  90. package/src/migrations/011_default_assignee.sql +167 -0
  91. package/src/migrations/012_drop_legacy_booking_overload.sql +29 -0
  92. package/src/migrations/013_normalize_v_appointments.sql +161 -0
  93. package/src/migrations/index.ts +1746 -1
  94. package/src/public/BookingWidget.tsx +10 -1
  95. package/src/public/context.tsx +3 -0
  96. package/src/public/createPublicBookingPlugin.ts +14 -1
  97. package/src/public/data.supabase.ts +3 -0
  98. package/src/public/data.ts +6 -0
  99. package/src/public/format.ts +36 -1
  100. package/src/public/hooks.ts +7 -2
  101. package/src/public/index.ts +1 -0
  102. package/src/public/steps/ContactStep.tsx +10 -5
  103. package/src/public/types.ts +21 -0
  104. package/src/views/CalendarView.tsx +15 -1
  105. package/dist/chunk-3LJKZQK4.js +0 -12
  106. package/dist/chunk-3LJKZQK4.js.map +0 -1
  107. package/dist/chunk-DRZF6CV2.js.map +0 -1
  108. package/dist/chunk-LFUELUIT.cjs.map +0 -1
  109. package/dist/chunk-SYDBDI3Q.cjs +0 -15
  110. package/dist/chunk-SYDBDI3Q.cjs.map +0 -1
  111. package/dist/components/SearchCombobox.d.ts +0 -38
  112. package/dist/components/SearchCombobox.d.ts.map +0 -1
  113. package/dist/tenant-222EHYUC.cjs +0 -16
  114. package/dist/tenant-222EHYUC.cjs.map +0 -1
  115. package/dist/tenant-LAUWVUYB.js +0 -3
  116. package/dist/tenant-LAUWVUYB.js.map +0 -1
  117. package/src/components/SearchCombobox.tsx +0 -216
@@ -1,5 +1,5 @@
1
1
  import { type CountryDef } from '@fayz-ai/core';
2
- import type { PublicBookingLabels, ResolvedPayment } from '../types';
2
+ import type { PhoneVerificationMode, PublicBookingLabels, ResolvedPayment } from '../types';
3
3
  export interface ContactDraft {
4
4
  name: string;
5
5
  phone: string;
@@ -11,6 +11,8 @@ interface ContactStepProps {
11
11
  contact: ContactDraft;
12
12
  onContactChange: (contact: ContactDraft) => void;
13
13
  contactStep: 'phone' | 'code' | 'details';
14
+ /** 'none' → no code is sent, so the copy drops every OTP promise. */
15
+ phoneVerification: PhoneVerificationMode;
14
16
  country: CountryDef;
15
17
  onCountryChange: (iso2: string) => void;
16
18
  code: string;
@@ -25,6 +27,6 @@ interface ContactStepProps {
25
27
  error: Error | null;
26
28
  }
27
29
  /** Contact step — phone verification first (Quaddro-style), then details. */
28
- export declare function ContactStep({ labels, contact, onContactChange, contactStep, country, onCountryChange, code, onCodeChange, codeError, onSendCode, onConfirmCode, onBackToPhone, onSubmitDetails, payment, submitting, error, }: ContactStepProps): import("react").JSX.Element;
30
+ export declare function ContactStep({ labels, contact, onContactChange, contactStep, phoneVerification, country, onCountryChange, code, onCodeChange, codeError, onSendCode, onConfirmCode, onBackToPhone, onSubmitDetails, payment, submitting, error, }: ContactStepProps): import("react").JSX.Element;
29
31
  export {};
30
32
  //# sourceMappingURL=ContactStep.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ContactStep.d.ts","sourceRoot":"","sources":["../../../src/public/steps/ContactStep.tsx"],"names":[],"mappings":"AACA,OAAO,EAAqC,KAAK,UAAU,EAAE,MAAM,eAAe,CAAA;AAClF,OAAO,KAAK,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAEpE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,UAAU,gBAAgB;IACxB,MAAM,EAAE,mBAAmB,CAAA;IAC3B,OAAO,EAAE,YAAY,CAAA;IACrB,eAAe,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAA;IAChD,WAAW,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAA;IACzC,OAAO,EAAE,UAAU,CAAA;IACnB,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,SAAS,EAAE,OAAO,CAAA;IAClB,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB,aAAa,EAAE,MAAM,IAAI,CAAA;IACzB,aAAa,EAAE,MAAM,IAAI,CAAA;IACzB,eAAe,EAAE,MAAM,IAAI,CAAA;IAC3B,OAAO,EAAE,eAAe,GAAG,IAAI,CAAA;IAC/B,UAAU,EAAE,OAAO,CAAA;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;CACpB;AAED,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EAAE,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,OAAO,EAAE,eAAe,EACvE,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,aAAa,EAAE,aAAa,EACvE,eAAe,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,GAC5C,EAAE,gBAAgB,+BA+IlB"}
1
+ {"version":3,"file":"ContactStep.d.ts","sourceRoot":"","sources":["../../../src/public/steps/ContactStep.tsx"],"names":[],"mappings":"AACA,OAAO,EAAqC,KAAK,UAAU,EAAE,MAAM,eAAe,CAAA;AAClF,OAAO,KAAK,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAE3F,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,UAAU,gBAAgB;IACxB,MAAM,EAAE,mBAAmB,CAAA;IAC3B,OAAO,EAAE,YAAY,CAAA;IACrB,eAAe,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAA;IAChD,WAAW,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAA;IACzC,qEAAqE;IACrE,iBAAiB,EAAE,qBAAqB,CAAA;IACxC,OAAO,EAAE,UAAU,CAAA;IACnB,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,SAAS,EAAE,OAAO,CAAA;IAClB,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB,aAAa,EAAE,MAAM,IAAI,CAAA;IACzB,aAAa,EAAE,MAAM,IAAI,CAAA;IACzB,eAAe,EAAE,MAAM,IAAI,CAAA;IAC3B,OAAO,EAAE,eAAe,GAAG,IAAI,CAAA;IAC/B,UAAU,EAAE,OAAO,CAAA;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;CACpB;AAED,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EAAE,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,iBAAiB,EAAE,OAAO,EAAE,eAAe,EAC1F,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,aAAa,EAAE,aAAa,EACvE,eAAe,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,GAC5C,EAAE,gBAAgB,+BAkJlB"}
@@ -40,6 +40,18 @@ export interface ResolvedPayment {
40
40
  reservationFeePercent: number;
41
41
  method: 'pix';
42
42
  }
43
+ /**
44
+ * How the customer's phone number is verified before the details sub-step.
45
+ *
46
+ * • 'otp' — show the code modal (POC: the code is the literal '0000'; no
47
+ * message is actually sent). Use only where a real sender exists.
48
+ * • 'none' — trust the typed number: the flow goes straight to the details
49
+ * sub-step and the identity is emitted as-is.
50
+ *
51
+ * Default is 'none': until a real WhatsApp sender is wired, an OTP nobody can
52
+ * receive is a dead end for real customers.
53
+ */
54
+ export type PhoneVerificationMode = 'otp' | 'none';
43
55
  /** One icon+text bullet on the overview sidebar. */
44
56
  export interface BookingHighlight {
45
57
  icon?: ComponentType<{
@@ -157,6 +169,10 @@ export interface PublicBookingLabels {
157
169
  phoneLabel: string;
158
170
  phoneHint: string;
159
171
  sendCodeCta: string;
172
+ /** Phone hint shown when phoneVerification is 'none' (no code is sent). */
173
+ phoneHintNoVerification: string;
174
+ /** Phone CTA shown when phoneVerification is 'none'. */
175
+ phoneContinueCta: string;
160
176
  codeTitle: string;
161
177
  codeBody: string;
162
178
  codeInvalid: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/public/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/public/types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CalendarView.d.ts","sourceRoot":"","sources":["../../src/views/CalendarView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAA;AAwRhF,wBAAgB,YAAY,sBAqxB3B"}
1
+ {"version":3,"file":"CalendarView.d.ts","sourceRoot":"","sources":["../../src/views/CalendarView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAA;AAwRhF,wBAAgB,YAAY,sBAmyB3B"}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "fayz": {
4
4
  "status": "beta"
5
5
  },
6
- "version": "0.8.1",
6
+ "version": "0.8.3",
7
7
  "description": "Fayz SDK — plugin-agenda plugin",
8
8
  "type": "module",
9
9
  "main": "./dist/index.cjs",
@@ -43,10 +43,10 @@
43
43
  "@tanstack/react-table": "^8.20.0",
44
44
  "@fullcalendar/resource-timegrid": "^6.1.0",
45
45
  "@fullcalendar/list": "^6.1.0",
46
- "@fayz-ai/core": "^0.8.0",
47
- "@fayz-ai/ui": "^0.8.0",
48
- "@fayz-ai/saas": "^0.8.0",
49
- "@fayz-ai/plugin-financial": "^0.8.0"
46
+ "@fayz-ai/core": "^0.8.3",
47
+ "@fayz-ai/ui": "^0.8.3",
48
+ "@fayz-ai/saas": "^0.8.3",
49
+ "@fayz-ai/plugin-financial": "^0.8.3"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/react": "^18.3.0",
@@ -1,5 +1,5 @@
1
1
  import React from 'react'
2
- import { SettingsGroup, ToggleRow, SelectRow } from '@fayz-ai/saas'
2
+ import { SettingsGroup, ToggleRow, SelectRow, useTenantPluginSettings } from '@fayz-ai/saas'
3
3
  import { useTranslation } from '@fayz-ai/core'
4
4
  import { useAgendaSettings } from '../hooks/useAgendaSettings'
5
5
 
@@ -8,30 +8,52 @@ const HOUR_OPTIONS = Array.from({ length: 18 }, (_, i) => {
8
8
  return { value: `${h}:00`, label: `${h}:00` }
9
9
  })
10
10
 
11
+ // Per-tenant toggle defaults = the values that used to be hardcoded here.
12
+ // NOTE: these are persisted + re-hydrated per tenant; actually consuming them to
13
+ // change agenda behaviour is a follow-up (see useTenantPluginSettings docs).
14
+ const AGENDA_TOGGLE_DEFAULTS = {
15
+ conflictDetection: true,
16
+ dragDrop: true,
17
+ autoCreateOrder: true,
18
+ enableLocation: false,
19
+ enableWorkingHours: true,
20
+ blockOutside: false,
21
+ enableConfirmations: true,
22
+ autoConfirm: false,
23
+ reminder: true,
24
+ showCancelled: false,
25
+ showNoShow: true,
26
+ compact: false,
27
+ newBookingNotif: true,
28
+ cancelAlerts: true,
29
+ noShowTracking: false,
30
+ }
31
+
11
32
  export function AgendaGeneralSettings() {
12
33
  const t = useTranslation()
13
34
  const { startTime, endTime, setStartTime, setEndTime } = useAgendaSettings()
35
+ const s = useTenantPluginSettings('agenda', AGENDA_TOGGLE_DEFAULTS)
14
36
  return (
15
37
  <div className="space-y-4">
16
38
  <SettingsGroup title={t('agenda.settings.scheduling')} description={t('agenda.settings.schedulingDesc')}>
17
- <ToggleRow label={t('agenda.settings.conflictDetection')} description={t('agenda.settings.conflictDetectionDesc')} checked={true} onChange={() => {}} />
18
- <ToggleRow label={t('agenda.settings.dragDrop')} description={t('agenda.settings.dragDropDesc')} checked={true} onChange={() => {}} />
19
- <ToggleRow label={t('agenda.settings.autoCreateOrder')} description={t('agenda.settings.autoCreateOrderDesc')} checked={true} onChange={() => {}} />
39
+ <ToggleRow label={t('agenda.settings.conflictDetection')} description={t('agenda.settings.conflictDetectionDesc')} checked={s.get('conflictDetection')} onChange={(v) => s.set('conflictDetection', v)} />
40
+ <ToggleRow label={t('agenda.settings.dragDrop')} description={t('agenda.settings.dragDropDesc')} checked={s.get('dragDrop')} onChange={(v) => s.set('dragDrop', v)} />
41
+ <ToggleRow label={t('agenda.settings.autoCreateOrder')} description={t('agenda.settings.autoCreateOrderDesc')} checked={s.get('autoCreateOrder')} onChange={(v) => s.set('autoCreateOrder', v)} />
20
42
  </SettingsGroup>
21
43
 
22
44
  <SettingsGroup title={t('agenda.settings.locations')} description={t('agenda.settings.locationsDesc')}>
23
- <ToggleRow label={t('agenda.settings.enableLocation')} description={t('agenda.settings.enableLocationDesc')} checked={false} onChange={() => {}} />
45
+ <ToggleRow label={t('agenda.settings.enableLocation')} description={t('agenda.settings.enableLocationDesc')} checked={s.get('enableLocation')} onChange={(v) => s.set('enableLocation', v)} />
24
46
  </SettingsGroup>
25
47
 
26
48
  <SettingsGroup title={t('agenda.settings.workingHours')} description={t('agenda.settings.workingHoursDesc')}>
27
- <ToggleRow label={t('agenda.settings.enableWorkingHours')} description={t('agenda.settings.enableWorkingHoursDesc')} checked={true} onChange={() => {}} />
28
- <ToggleRow label={t('agenda.settings.blockOutside')} description={t('agenda.settings.blockOutsideDesc')} checked={false} onChange={() => {}} />
49
+ <ToggleRow label={t('agenda.settings.enableWorkingHours')} description={t('agenda.settings.enableWorkingHoursDesc')} checked={s.get('enableWorkingHours')} onChange={(v) => s.set('enableWorkingHours', v)} />
50
+ <ToggleRow label={t('agenda.settings.blockOutside')} description={t('agenda.settings.blockOutsideDesc')} checked={s.get('blockOutside')} onChange={(v) => s.set('blockOutside', v)} />
29
51
  </SettingsGroup>
30
52
 
31
53
  <SettingsGroup title={t('agenda.settings.confirmations')} description={t('agenda.settings.confirmationsDesc')}>
32
- <ToggleRow label={t('agenda.settings.enableConfirmations')} description={t('agenda.settings.enableConfirmationsDesc')} checked={true} onChange={() => {}} />
33
- <ToggleRow label={t('agenda.settings.autoConfirm')} description={t('agenda.settings.autoConfirmDesc')} checked={false} onChange={() => {}} />
34
- <ToggleRow label={t('agenda.settings.reminder')} description={t('agenda.settings.reminderDesc')} checked={true} onChange={() => {}} />
54
+ <ToggleRow label={t('agenda.settings.enableConfirmations')} description={t('agenda.settings.enableConfirmationsDesc')} checked={s.get('enableConfirmations')} onChange={(v) => s.set('enableConfirmations', v)} />
55
+ <ToggleRow label={t('agenda.settings.autoConfirm')} description={t('agenda.settings.autoConfirmDesc')} checked={s.get('autoConfirm')} onChange={(v) => s.set('autoConfirm', v)} />
56
+ <ToggleRow label={t('agenda.settings.reminder')} description={t('agenda.settings.reminderDesc')} checked={s.get('reminder')} onChange={(v) => s.set('reminder', v)} />
35
57
  </SettingsGroup>
36
58
 
37
59
  <SettingsGroup title={t('agenda.settings.calendarDisplay')} description={t('agenda.settings.calendarDisplayDesc')}>
@@ -49,15 +71,15 @@ export function AgendaGeneralSettings() {
49
71
  options={HOUR_OPTIONS}
50
72
  onChange={setEndTime}
51
73
  />
52
- <ToggleRow label={t('agenda.settings.showCancelled')} description={t('agenda.settings.showCancelledDesc')} checked={false} onChange={() => {}} />
53
- <ToggleRow label={t('agenda.settings.showNoShow')} description={t('agenda.settings.showNoShowDesc')} checked={true} onChange={() => {}} />
54
- <ToggleRow label={t('agenda.settings.compact')} description={t('agenda.settings.compactDesc')} checked={false} onChange={() => {}} />
74
+ <ToggleRow label={t('agenda.settings.showCancelled')} description={t('agenda.settings.showCancelledDesc')} checked={s.get('showCancelled')} onChange={(v) => s.set('showCancelled', v)} />
75
+ <ToggleRow label={t('agenda.settings.showNoShow')} description={t('agenda.settings.showNoShowDesc')} checked={s.get('showNoShow')} onChange={(v) => s.set('showNoShow', v)} />
76
+ <ToggleRow label={t('agenda.settings.compact')} description={t('agenda.settings.compactDesc')} checked={s.get('compact')} onChange={(v) => s.set('compact', v)} />
55
77
  </SettingsGroup>
56
78
 
57
79
  <SettingsGroup title={t('agenda.settings.notifications')} description={t('agenda.settings.notificationsDesc')}>
58
- <ToggleRow label={t('agenda.settings.newBookingNotif')} description={t('agenda.settings.newBookingNotifDesc')} checked={true} onChange={() => {}} />
59
- <ToggleRow label={t('agenda.settings.cancelAlerts')} description={t('agenda.settings.cancelAlertsDesc')} checked={true} onChange={() => {}} />
60
- <ToggleRow label={t('agenda.settings.noShowTracking')} description={t('agenda.settings.noShowTrackingDesc')} checked={false} onChange={() => {}} />
80
+ <ToggleRow label={t('agenda.settings.newBookingNotif')} description={t('agenda.settings.newBookingNotifDesc')} checked={s.get('newBookingNotif')} onChange={(v) => s.set('newBookingNotif', v)} />
81
+ <ToggleRow label={t('agenda.settings.cancelAlerts')} description={t('agenda.settings.cancelAlertsDesc')} checked={s.get('cancelAlerts')} onChange={(v) => s.set('cancelAlerts', v)} />
82
+ <ToggleRow label={t('agenda.settings.noShowTracking')} description={t('agenda.settings.noShowTrackingDesc')} checked={s.get('noShowTracking')} onChange={(v) => s.set('noShowTracking', v)} />
61
83
  </SettingsGroup>
62
84
  </div>
63
85
  )
@@ -10,11 +10,12 @@ import {
10
10
  } from '@fayz-ai/ui'
11
11
  import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@fayz-ai/ui'
12
12
  import { cn } from '@fayz-ai/ui'
13
- import { SearchCombobox, type ComboboxOption } from './SearchCombobox'
14
- import { PersonLink } from '@fayz-ai/saas'
13
+ import { SearchCombobox, type ComboboxOption } from '@fayz-ai/ui'
14
+ import { ContactPicker, type ContactPickerValue } from '@fayz-ai/saas'
15
15
  import { BookingPaymentPanel } from './BookingPaymentPanel'
16
16
  import { CrudDetailPage } from '@fayz-ai/saas'
17
17
  import { CrudFormPage } from '@fayz-ai/saas'
18
+ import { useLimitGuard, invalidateLimit } from '@fayz-ai/saas'
18
19
  import { Button } from '@fayz-ai/ui'
19
20
  import { useAgendaConfig, useAgendaProvider, useAgendaStore } from '../AgendaContext'
20
21
  import { useTranslation } from '@fayz-ai/core'
@@ -280,6 +281,7 @@ export function AppointmentModal({ open, mode, bookingId, prefill, initialTab, e
280
281
  const provider = useAgendaProvider()
281
282
  const initialSnapshot = useRef<string>('')
282
283
  const createBooking = useAgendaStore((s) => s.createBooking)
284
+ const guardBookings = useLimitGuard('bookings_month')
283
285
  const updateBooking = useAgendaStore((s) => s.updateBooking)
284
286
  const deleteBooking = useAgendaStore((s) => s.deleteBooking)
285
287
  const fetchBookings = useAgendaStore((s) => s.fetchBookings)
@@ -317,13 +319,7 @@ export function AppointmentModal({ open, mode, bookingId, prefill, initialTab, e
317
319
  const [showNotes, setShowNotes] = useState(false)
318
320
  const [saving, setSaving] = useState(false)
319
321
  const [conflict, setConflict] = useState(false)
320
- const [quickCreate, setQuickCreate] = useState(false)
321
- const [newClientName, setNewClientName] = useState('')
322
- const [newClientPhone, setNewClientPhone] = useState('')
323
- const [newClientEmail, setNewClientEmail] = useState('')
324
- const [creatingClient, setCreatingClient] = useState(false)
325
- const [clientOptions, setClientOptions] = useState<ComboboxOption[]>([])
326
- const [clientSearching, setClientSearching] = useState(false)
322
+ // Client search/quick-create state now lives inside <ContactPicker>.
327
323
  const [serviceOptions, setServiceOptions] = useState<ComboboxOption[]>([])
328
324
  const [initialServiceOptions, setInitialServiceOptions] = useState<ComboboxOption[]>([])
329
325
  const [editingDate, setEditingDate] = useState(false)
@@ -353,7 +349,6 @@ export function AppointmentModal({ open, mode, bookingId, prefill, initialTab, e
353
349
  setProfessionalId(prefill?.professionalId ?? ''); setLocationId(prefill?.locationId ?? ''); setServices([])
354
350
  setServiceSearch(''); setNotes(''); setStatus('scheduled'); setShowNotes(false)
355
351
  setConflict(false); setLoading(mode === 'edit')
356
- setQuickCreate(false); setNewClientName(''); setNewClientPhone(''); setNewClientEmail(''); setCreatingClient(false)
357
352
  setBookingType(prefill?.kind ?? defaultBookingType); setTitle('')
358
353
  setEditingDate(false); setEditingTime(false); setConfirmingDelete(false); setDeleting(false)
359
354
  setEditOrderId(null); setEditOrderTotal(0); setEditPaymentStatus('none'); setPaymentStatusLoading(false)
@@ -449,20 +444,7 @@ export function AppointmentModal({ open, mode, bookingId, prefill, initialTab, e
449
444
  }).catch(() => setInitialServiceOptions([]))
450
445
  }, [open])
451
446
 
452
- // Search effects
453
- useEffect(() => {
454
- if (!config.contactLookup || clientSearch.length < 1 || clientId) { setClientOptions([]); setClientSearching(false); return }
455
- setClientSearching(true)
456
- const timer = setTimeout(async () => {
457
- try {
458
- const results = await config.contactLookup!.search(clientSearch)
459
- setClientOptions(results.map((r: any) => ({ id: r.id, label: r.label, subtitle: r.subtitle, price: r.price, data: r.data })))
460
- } catch { setClientOptions([]) }
461
- setClientSearching(false)
462
- }, 250)
463
- return () => clearTimeout(timer)
464
- }, [clientSearch, clientId])
465
-
447
+ // Search effects (client search now lives inside <ContactPicker>)
466
448
  useEffect(() => {
467
449
  if (!config.serviceLookup || serviceSearch.length < 1) { setServiceOptions([]); return }
468
450
  const timer = setTimeout(async () => {
@@ -501,10 +483,6 @@ export function AppointmentModal({ open, mode, bookingId, prefill, initialTab, e
501
483
  return () => clearTimeout(timer)
502
484
  }, [professionalId, date, startTime, endTime, bookingId])
503
485
 
504
- function selectClient(opt: ComboboxOption) {
505
- setClientId(opt.id); setClientSearch(opt.label); setClientOptions([])
506
- setClientPhone((opt.data?.phone as string) ?? '')
507
- }
508
486
  function addService(opt: ComboboxOption) {
509
487
  const duration = (opt.data?.duration_minutes as number) || 60
510
488
  setServices((p) => [...p, { serviceId: opt.id, name: opt.label, durationMinutes: duration, price: opt.price ?? 0 }])
@@ -515,6 +493,9 @@ export function AppointmentModal({ open, mode, bookingId, prefill, initialTab, e
515
493
  async function handleSubmit(e: React.FormEvent) {
516
494
  e.preventDefault()
517
495
  if (!canSubmit) return
496
+ // Plan quantity guard (client-side, before the store call): opens the global
497
+ // UpgradeModal and aborts when the monthly booking cap is reached. CREATE only.
498
+ if (mode === 'create' && (await guardBookings()) === 'blocked') return
518
499
  setSaving(true)
519
500
  try {
520
501
  const startsAt = new Date(`${date}T${startTime}:00`).toISOString()
@@ -522,6 +503,7 @@ export function AppointmentModal({ open, mode, bookingId, prefill, initialTab, e
522
503
  const newBooking = await createBooking({ kind: bookingType, clientId, professionalId, locationId: locationId || undefined, startsAt, notes: notes || undefined,
523
504
  title: activeType.fields.title ? title.trim() : undefined,
524
505
  services: services.map((s: any) => ({ serviceId: s.serviceId, name: s.name, durationMinutes: s.durationMinutes, price: s.price })) })
506
+ invalidateLimit('bookings_month')
525
507
  onClose()
526
508
  onCreated?.(newBooking.id, externalSource)
527
509
  } else if (bookingId) {
@@ -547,8 +529,17 @@ export function AppointmentModal({ open, mode, bookingId, prefill, initialTab, e
547
529
  async function handleDelete() {
548
530
  if (!bookingId) return
549
531
  setDeleting(true)
550
- try { await deleteBooking(bookingId); onClose() } catch { /* toast */ }
551
- setDeleting(false)
532
+ try {
533
+ await deleteBooking(bookingId)
534
+ onClose()
535
+ } catch {
536
+ // deleteBooking surfaces its own toast. Return the footer to its normal
537
+ // state (out of the confirm step) so a failure is clear feedback rather
538
+ // than a modal that silently stays stuck on "Confirm delete".
539
+ setConfirmingDelete(false)
540
+ } finally {
541
+ setDeleting(false)
542
+ }
552
543
  }
553
544
 
554
545
  const isPaidBooking = editPaymentStatus === 'paid'
@@ -802,84 +793,35 @@ export function AppointmentModal({ open, mode, bookingId, prefill, initialTab, e
802
793
  <div className="flex items-center gap-3 py-1">
803
794
  <User className="h-4 w-4 text-muted-foreground shrink-0" />
804
795
  <div className="flex-1">
805
- {clientId && !quickCreate ? (
806
- <div className="flex items-center gap-2 rounded-md bg-muted/30 px-2.5 py-1.5 text-sm">
807
- <PersonLink personId={clientId} name={clientSearch} size="default" className="flex-1 min-w-0" />
808
- {clientPhone && <span className="text-[10px] text-muted-foreground shrink-0">{clientPhone}</span>}
809
- {!isPaidBooking && (
810
- <button type="button" onClick={() => { setClientId(''); setClientSearch(''); setClientPhone('') }}
811
- className="p-0.5 text-muted-foreground hover:text-destructive shrink-0">
812
- <X className="h-3 w-3" />
813
- </button>
814
- )}
815
- </div>
816
- ) : !quickCreate ? (
817
- <SearchCombobox
818
- value={clientSearch}
819
- onChange={(v) => { setClientSearch(v); setClientId('') }}
820
- onSelect={selectClient}
821
- options={clientOptions}
822
- loading={clientSearching}
823
- placeholder={t('agenda.appointment.searchClient')}
824
- allowCreate
825
- createLabel={t('agenda.appointment.newClient')}
826
- onCreateNew={(name) => { setQuickCreate(true); setNewClientName(name); setNewClientPhone(''); setNewClientEmail('') }}
827
- minimal
828
- inlineLabel={t('agenda.appointment.addClient')}
829
- />
830
- ) : (
831
- <div className="rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] p-2.5 space-y-2">
832
- <div className="flex items-center gap-2">
833
- <input type="text" value={newClientName} onChange={(e) => setNewClientName(e.target.value)}
834
- placeholder={t('agenda.appointment.name')} className="flex-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-ring" autoFocus />
835
- <button type="button" onClick={() => setQuickCreate(false)}
836
- className="p-1 text-muted-foreground hover:text-foreground shrink-0"><X className="h-3.5 w-3.5" /></button>
837
- </div>
838
- <div className="grid grid-cols-2 gap-2">
839
- <input type="tel" value={newClientPhone} onChange={(e) => setNewClientPhone(e.target.value)}
840
- placeholder={t('agenda.appointment.phone')} className="rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring" />
841
- <input type="email" value={newClientEmail} onChange={(e) => setNewClientEmail(e.target.value)}
842
- placeholder={t('agenda.appointment.emailOptional')} className="rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring" />
843
- </div>
844
- <button type="button" disabled={creatingClient || !newClientName.trim()}
845
- onClick={async () => {
846
- setCreatingClient(true)
847
- try {
848
- const { getSupabaseClientOptional } = await import('@fayz-ai/core')
849
- const { getAgendaTenantId } = await import('../lib/tenant')
850
- const supabase = getSupabaseClientOptional() as any
851
- const tenantId = getAgendaTenantId()
852
- if (!supabase || !tenantId) throw new Error('Not initialized')
853
-
854
- // 1. Insert into public.people (archetype base table)
855
- const { data: personRow, error: personError } = await supabase.from('people').insert({
856
- tenant_id: tenantId, kind: config.clientKind, name: newClientName.trim(),
857
- phone: newClientPhone.trim() || null, email: newClientEmail.trim() || null,
858
- }).select('id, name').single()
859
- if (personError) throw personError
860
-
861
- // 2. Insert into public.clients (project extension table)
862
- const { error: clientError } = await supabase.from('clients').insert({
863
- person_id: personRow.id, tenant_id: tenantId,
864
- })
865
- if (clientError) {
866
- // Rollback person if extension insert fails
867
- await supabase.from('people').delete().eq('id', personRow.id)
868
- throw clientError
869
- }
870
-
871
- setClientId(personRow.id); setClientSearch(personRow.name); setQuickCreate(false)
872
- } catch (err: any) {
873
- const { toast } = await import('@fayz-ai/ui')
874
- toast.error(t('agenda.appointment.createClientFailed'), { description: err?.message })
875
- }
876
- setCreatingClient(false)
877
- }}
878
- className="w-full rounded-md bg-primary border border-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 shadow-button-primary active:shadow-button-inset disabled:opacity-40 transition-colors">
879
- {creatingClient ? t('agenda.appointment.creating') : t('agenda.appointment.create')}
880
- </button>
881
- </div>
882
- )}
796
+ {/* Shared find-or-create contact flow (@fayz-ai/saas). The
797
+ quick-create that used to live inline here now writes the
798
+ SAME people(+clients) rows for every plugin — see
799
+ `createPerson`. `clientKind`/`clientExtensionTable` keep the
800
+ vertical's discriminator and extension table.
801
+ `variant="inline"` keeps this modal's grammar: an icon
802
+ column and borderless controls (like Scheduled / Add
803
+ notes). Same component and same flow as the conversations
804
+ composer — only the input's chrome differs. */}
805
+ <ContactPicker
806
+ value={clientId || clientSearch ? { id: clientId || undefined, name: clientSearch, phone: clientPhone } : null}
807
+ onChange={(contact) => {
808
+ setClientId(contact?.id ?? '')
809
+ setClientSearch(contact?.name ?? '')
810
+ setClientPhone(contact?.phone ?? '')
811
+ }}
812
+ kind={config.clientKind}
813
+ extensionTable="clients"
814
+ lookup={config.contactLookup}
815
+ clearable={!isPaidBooking}
816
+ variant="inline"
817
+ placeholder={t('agenda.appointment.searchClient')}
818
+ createLabel={t('agenda.appointment.newClient')}
819
+ hint={
820
+ <p className="text-[11px] text-warning">
821
+ {t('agenda.appointment.selectClientHint')}
822
+ </p>
823
+ }
824
+ />
883
825
  </div>
884
826
  </div>
885
827
  )}
@@ -1,6 +1,7 @@
1
1
  import React from 'react'
2
2
  import { ChevronLeft, ChevronRight, Plus, Calendar } from 'lucide-react'
3
3
  import { SegmentedControl } from '@fayz-ai/ui'
4
+ import { PermissionGate } from '@fayz-ai/saas'
4
5
  import { useAgendaConfig } from '../AgendaContext'
5
6
  import { useTranslation } from '@fayz-ai/core'
6
7
 
@@ -70,13 +71,15 @@ export function CalendarToolbar({
70
71
  aria-label="Calendar view"
71
72
  />
72
73
 
73
- <button
74
- onClick={onNewAppointment}
75
- className="flex items-center gap-2 rounded-lg bg-primary border border-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 shadow-button-primary active:shadow-button-inset transition-colors"
76
- >
77
- <Plus className="h-4 w-4" />
78
- <span className="hidden sm:inline">{config.labels.newAppointment}</span>
79
- </button>
74
+ <PermissionGate feature="appointments" action="create">
75
+ <button
76
+ onClick={onNewAppointment}
77
+ className="flex items-center gap-2 rounded-lg bg-primary border border-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 shadow-button-primary active:shadow-button-inset transition-colors"
78
+ >
79
+ <Plus className="h-4 w-4" />
80
+ <span className="hidden sm:inline">{config.labels.newAppointment}</span>
81
+ </button>
82
+ </PermissionGate>
80
83
  </div>
81
84
  </div>
82
85
  )
@@ -151,8 +151,14 @@ async function hydrateCanonicalBookings(core: any, bookings: any[]): Promise<Cal
151
151
  locationId: booking.location_id,
152
152
  metadata: booking.metadata ?? {},
153
153
  clientId: booking.party_id,
154
- // Simple events (no party) fall back to the title stored in metadata.
155
- clientName: client?.name ?? (booking.metadata?.title as string | undefined) ?? null,
154
+ // Simple events (no party) fall back to the title stored in metadata;
155
+ // partyless blocks/tasks (e.g. imported Google events) get their notes or
156
+ // a kind label so the calendar never renders an unnamed chip.
157
+ clientName: client?.name
158
+ ?? (booking.metadata?.title as string | undefined)
159
+ ?? (booking.kind === 'appointment'
160
+ ? null
161
+ : booking.notes || (booking.kind === 'task' ? 'Tarefa' : 'Bloqueio')),
156
162
  clientPhone: client?.phone ?? null,
157
163
  clientEmail: client?.email ?? null,
158
164
  clientAvatarUrl: client?.avatar_url ?? null,
@@ -206,7 +212,13 @@ async function getCanonicalBookingById(core: any, id: string): Promise<CalendarB
206
212
  // Supabase Agenda Provider
207
213
  // ---------------------------------------------------------------------------
208
214
 
209
- export function createSupabaseAgendaProvider(): AgendaDataProvider {
215
+ export interface SupabaseAgendaProviderOptions {
216
+ /** people.kind used for calendar resources/professionals (default 'staff' — schools use 'teacher'). */
217
+ professionalKind?: string
218
+ }
219
+
220
+ export function createSupabaseAgendaProvider(providerOptions?: SupabaseAgendaProviderOptions): AgendaDataProvider {
221
+ const professionalKind = providerOptions?.professionalKind ?? 'staff'
210
222
  return {
211
223
  // v_appointments is in public schema (PostgREST default)
212
224
  async getBookings(query: BookingQuery): Promise<CalendarBooking[]> {
@@ -233,7 +245,37 @@ export function createSupabaseAgendaProvider(): AgendaDataProvider {
233
245
  const { core, pub } = getClients()
234
246
  const { data, error } = await pub.from('v_appointments').select('*').eq('id', id).maybeSingle()
235
247
  if (error) throw error
236
- if (data) return mapBooking(data as Record<string, unknown>)
248
+ if (data) {
249
+ const booking = mapBooking(data as Record<string, unknown>)
250
+ // v_appointments is a FLAT read model — it exposes no line items. The
251
+ // calendar list path merges in the canonical hydration (which carries
252
+ // `services` from order_items), but this single-record lookup used by the
253
+ // edit modal previously returned the bare view row. That dropped
254
+ // `services`, so a booking whose type has `requiresServices` reopened with
255
+ // an empty service list and kept the "Update" button permanently disabled
256
+ // (canSubmit stayed false). Hydrate the line items here to stay consistent
257
+ // with getBookings.
258
+ if (booking.orderId) {
259
+ const { data: items } = await core.from('order_items')
260
+ .select('id, service_id, name, duration_minutes, unit_price, assignee_id, sort_order')
261
+ .eq('order_id', booking.orderId)
262
+ .order('sort_order', { ascending: true })
263
+ if (items && items.length) {
264
+ const services = items.map((item: any) => ({
265
+ id: item.id,
266
+ serviceId: item.service_id,
267
+ name: item.name,
268
+ durationMinutes: item.duration_minutes,
269
+ price: item.unit_price,
270
+ assigneeId: item.assignee_id,
271
+ }))
272
+ booking.services = services
273
+ const itemDuration = services.reduce((sum: number, s: any) => sum + (s.durationMinutes ?? 0), 0)
274
+ if (itemDuration) booking.totalDurationMinutes = itemDuration
275
+ }
276
+ }
277
+ return booking
278
+ }
237
279
  return getCanonicalBookingById(core, id)
238
280
  },
239
281
 
@@ -264,7 +306,7 @@ export function createSupabaseAgendaProvider(): AgendaDataProvider {
264
306
  kind: input.kind ?? 'appointment',
265
307
  status: 'scheduled',
266
308
  party_id: input.clientId || null,
267
- assignee_id: input.professionalId,
309
+ assignee_id: input.professionalId || null,
268
310
  location_id: input.locationId ?? null,
269
311
  subtotal: totalPrice,
270
312
  total: totalPrice,
@@ -282,7 +324,7 @@ export function createSupabaseAgendaProvider(): AgendaDataProvider {
282
324
  tenant_id: tenantId,
283
325
  kind: input.kind ?? 'appointment',
284
326
  party_id: input.clientId || null,
285
- assignee_id: input.professionalId,
327
+ assignee_id: input.professionalId || null,
286
328
  location_id: input.locationId ?? null,
287
329
  order_id: order.id,
288
330
  starts_at: input.startsAt,
@@ -309,7 +351,7 @@ export function createSupabaseAgendaProvider(): AgendaDataProvider {
309
351
  total: s.price,
310
352
  sort_order: i,
311
353
  duration_minutes: s.durationMinutes,
312
- assignee_id: s.assigneeId ?? input.professionalId,
354
+ assignee_id: s.assigneeId || input.professionalId || null,
313
355
  }))
314
356
  const { error: oiErr } = await core.from('order_items').insert(orderItems)
315
357
  if (oiErr) throw oiErr
@@ -413,12 +455,24 @@ export function createSupabaseAgendaProvider(): AgendaDataProvider {
413
455
  if (bookingErr) throw bookingErr
414
456
 
415
457
  const orderId = booking?.order_id as string | null | undefined
416
- const { count, error: movementErr } = orderId
417
- ? await pub.from('plg_financial_movements').select('id', { count: 'exact', head: true }).eq('invoice_id', orderId)
418
- : { count: 0, error: null }
419
- if (movementErr) throw movementErr
458
+ // Financial movements live in the financial plugin's tables. Pools that
459
+ // don't provision that plugin (e.g. school/event agendas) have no
460
+ // plg_financial_movements table, and the probe below returns a PostgREST
461
+ // "relation does not exist" error. Previously that error was rethrown,
462
+ // which aborted the ENTIRE delete — the modal caught it and left the
463
+ // booking in place, so "Delete" looked like a silent no-op (works in
464
+ // beauty, which has the table; fails in school, which doesn't). Treat any
465
+ // probe failure as "no linked movements" and fall through to a plain delete.
466
+ let movementCount = 0
467
+ if (orderId) {
468
+ const { count, error: movementErr } = await pub
469
+ .from('plg_financial_movements')
470
+ .select('id', { count: 'exact', head: true })
471
+ .eq('invoice_id', orderId)
472
+ if (!movementErr) movementCount = count ?? 0
473
+ }
420
474
 
421
- if (orderId && (count ?? 0) > 0) {
475
+ if (orderId && movementCount > 0) {
422
476
  // Cancel movements + order (don't delete financial records)
423
477
  await pub.from('plg_financial_movements').update({ status: 'cancelled' }).eq('invoice_id', orderId)
424
478
  await core.from('orders').update({ status: 'cancelled' }).eq('id', orderId)
@@ -516,7 +570,7 @@ export function createSupabaseAgendaProvider(): AgendaDataProvider {
516
570
  const { core } = getClients()
517
571
  const { data, error } = await core.from('people')
518
572
  .select('id, name, avatar_url, is_active')
519
- .eq('kind', 'staff')
573
+ .eq('kind', professionalKind)
520
574
  .eq('is_active', true)
521
575
  .order('name')
522
576
  if (error) throw error