@fayz-ai/plugin-agenda 0.9.0 → 0.10.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 (196) hide show
  1. package/README.md +3 -3
  2. package/dist/bridges/create-financial-bridge.d.ts +1 -1
  3. package/dist/bridges/create-financial-bridge.d.ts.map +1 -1
  4. package/dist/bridges/financial-bridge.d.ts.map +1 -0
  5. package/dist/{chunk-Q2PAWBPR.js → chunk-ISLJXP7O.js} +7 -2
  6. package/dist/chunk-ISLJXP7O.js.map +1 -0
  7. package/dist/components/AppointmentModal.d.ts.map +1 -1
  8. package/dist/components/PaymentStatusBadge.d.ts +1 -1
  9. package/dist/components/PaymentStatusBadge.d.ts.map +1 -1
  10. package/dist/config.d.ts +2 -2
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/{AgendaContext.d.ts → context.d.ts} +1 -1
  13. package/dist/context.d.ts.map +1 -0
  14. package/dist/data/mock.d.ts.map +1 -1
  15. package/dist/data/realtime.test.d.ts +2 -0
  16. package/dist/data/realtime.test.d.ts.map +1 -0
  17. package/dist/data/registries.d.ts.map +1 -0
  18. package/dist/data/supabase.d.ts.map +1 -1
  19. package/dist/data/types.d.ts +7 -0
  20. package/dist/data/types.d.ts.map +1 -1
  21. package/dist/index.d.ts +2 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2515 -187
  24. package/dist/index.js.map +1 -1
  25. package/dist/integrations/google-calendar/conflict.d.ts +25 -0
  26. package/dist/integrations/google-calendar/conflict.d.ts.map +1 -0
  27. package/dist/integrations/google-calendar/connectorDef.d.ts +12 -0
  28. package/dist/integrations/google-calendar/connectorDef.d.ts.map +1 -1
  29. package/dist/integrations/google-calendar/credentials.d.ts +27 -0
  30. package/dist/integrations/google-calendar/credentials.d.ts.map +1 -0
  31. package/dist/integrations/google-calendar/data/supabase.d.ts +10 -2
  32. package/dist/integrations/google-calendar/data/supabase.d.ts.map +1 -1
  33. package/dist/integrations/google-calendar/data/tables.d.ts +2 -0
  34. package/dist/integrations/google-calendar/data/tables.d.ts.map +1 -1
  35. package/dist/integrations/google-calendar/health.d.ts +23 -0
  36. package/dist/integrations/google-calendar/health.d.ts.map +1 -0
  37. package/dist/integrations/google-calendar/health.test.d.ts +2 -0
  38. package/dist/integrations/google-calendar/health.test.d.ts.map +1 -0
  39. package/dist/integrations/google-calendar/index.d.ts +6 -0
  40. package/dist/integrations/google-calendar/index.d.ts.map +1 -1
  41. package/dist/integrations/google-calendar/migrations/index.d.ts +12 -0
  42. package/dist/integrations/google-calendar/migrations/index.d.ts.map +1 -0
  43. package/dist/integrations/google-calendar/panel-data.d.ts +26 -0
  44. package/dist/integrations/google-calendar/panel-data.d.ts.map +1 -0
  45. package/dist/integrations/google-calendar/panel-data.test.d.ts +2 -0
  46. package/dist/integrations/google-calendar/panel-data.test.d.ts.map +1 -0
  47. package/dist/integrations/google-calendar/types.d.ts +17 -0
  48. package/dist/integrations/google-calendar/types.d.ts.map +1 -1
  49. package/dist/lib/booking-events.d.ts +5 -0
  50. package/dist/lib/booking-events.d.ts.map +1 -0
  51. package/dist/lib/calendar-events.d.ts +8 -0
  52. package/dist/lib/calendar-events.d.ts.map +1 -0
  53. package/dist/lib/calendar-events.test.d.ts +2 -0
  54. package/dist/lib/calendar-events.test.d.ts.map +1 -0
  55. package/dist/lib/message-events.d.ts +12 -0
  56. package/dist/lib/message-events.d.ts.map +1 -0
  57. package/dist/locales/en.d.ts.map +1 -1
  58. package/dist/locales/pt-BR.d.ts.map +1 -1
  59. package/dist/public/{data.d.ts → data/index.d.ts} +4 -4
  60. package/dist/public/data/index.d.ts.map +1 -0
  61. package/dist/public/{data.supabase.d.ts → data/supabase.d.ts} +2 -2
  62. package/dist/public/data/supabase.d.ts.map +1 -0
  63. package/dist/public/{hooks.d.ts → hooks/index.d.ts} +4 -4
  64. package/dist/public/hooks/index.d.ts.map +1 -0
  65. package/dist/public/index.d.ts +4 -4
  66. package/dist/public/index.d.ts.map +1 -1
  67. package/dist/public/index.js +3 -3
  68. package/dist/public/index.js.map +1 -1
  69. package/dist/public/{format.d.ts → lib/format.d.ts} +1 -1
  70. package/dist/public/lib/format.d.ts.map +1 -0
  71. package/dist/public/views/BookingWidget.d.ts.map +1 -0
  72. package/dist/public/{steps → views/steps}/ContactStep.d.ts +1 -1
  73. package/dist/public/views/steps/ContactStep.d.ts.map +1 -0
  74. package/dist/public/{steps → views/steps}/DateTimeStep.d.ts +3 -3
  75. package/dist/public/views/steps/DateTimeStep.d.ts.map +1 -0
  76. package/dist/public/{steps → views/steps}/OverviewStep.d.ts +1 -1
  77. package/dist/public/views/steps/OverviewStep.d.ts.map +1 -0
  78. package/dist/public/{steps → views/steps}/PaymentStep.d.ts +1 -1
  79. package/dist/public/views/steps/PaymentStep.d.ts.map +1 -0
  80. package/dist/store.d.ts +1 -1
  81. package/dist/store.d.ts.map +1 -1
  82. package/dist/styles/calendar-styles.d.ts.map +1 -0
  83. package/dist/types.d.ts +1 -1
  84. package/dist/types.d.ts.map +1 -1
  85. package/dist/{AgendaPage.d.ts → views/AgendaPage.d.ts} +3 -3
  86. package/dist/views/AgendaPage.d.ts.map +1 -0
  87. package/dist/views/CalendarView.d.ts.map +1 -1
  88. package/package.json +9 -11
  89. package/dist/AgendaContext.d.ts.map +0 -1
  90. package/dist/AgendaPage.d.ts.map +0 -1
  91. package/dist/calendar-styles.d.ts.map +0 -1
  92. package/dist/chunk-Q2PAWBPR.js.map +0 -1
  93. package/dist/financial-bridge.d.ts.map +0 -1
  94. package/dist/integrations/google-calendar/GoogleCalendarSettings.d.ts +0 -3
  95. package/dist/integrations/google-calendar/GoogleCalendarSettings.d.ts.map +0 -1
  96. package/dist/public/BookingWidget.d.ts.map +0 -1
  97. package/dist/public/data.d.ts.map +0 -1
  98. package/dist/public/data.supabase.d.ts.map +0 -1
  99. package/dist/public/format.d.ts.map +0 -1
  100. package/dist/public/hooks.d.ts.map +0 -1
  101. package/dist/public/steps/ContactStep.d.ts.map +0 -1
  102. package/dist/public/steps/DateTimeStep.d.ts.map +0 -1
  103. package/dist/public/steps/OverviewStep.d.ts.map +0 -1
  104. package/dist/public/steps/PaymentStep.d.ts.map +0 -1
  105. package/dist/registries.d.ts.map +0 -1
  106. package/src/AgendaContext.tsx +0 -11
  107. package/src/AgendaPage.tsx +0 -33
  108. package/src/bridges/create-financial-bridge.ts +0 -296
  109. package/src/calendar-styles.css +0 -245
  110. package/src/calendar-styles.ts +0 -285
  111. package/src/components/AgendaGeneralSettings.tsx +0 -86
  112. package/src/components/AgendaMobileList.tsx +0 -103
  113. package/src/components/AppointmentModal.tsx +0 -1050
  114. package/src/components/AppointmentPopover.tsx +0 -231
  115. package/src/components/BookingPaymentPanel.tsx +0 -394
  116. package/src/components/CalendarSidebar.tsx +0 -119
  117. package/src/components/CalendarToolbar.tsx +0 -86
  118. package/src/components/EventContextMenu.tsx +0 -125
  119. package/src/components/FloatingPanel.tsx +0 -123
  120. package/src/components/MiniCalendar.tsx +0 -120
  121. package/src/components/PaymentStatusBadge.tsx +0 -62
  122. package/src/components/StatusBadge.tsx +0 -23
  123. package/src/config.ts +0 -368
  124. package/src/data/index.ts +0 -4
  125. package/src/data/mock.ts +0 -344
  126. package/src/data/supabase.ts +0 -636
  127. package/src/data/types.ts +0 -37
  128. package/src/financial-bridge.ts +0 -96
  129. package/src/hooks/useAgendaSettings.ts +0 -17
  130. package/src/index.ts +0 -297
  131. package/src/integrations/google-calendar/GoogleCalendarSettings.tsx +0 -136
  132. package/src/integrations/google-calendar/components/CalendarColorDot.tsx +0 -23
  133. package/src/integrations/google-calendar/components/ChannelMappingList.tsx +0 -329
  134. package/src/integrations/google-calendar/components/ImportEventsWizard.tsx +0 -215
  135. package/src/integrations/google-calendar/components/useAgendaLookups.ts +0 -100
  136. package/src/integrations/google-calendar/connector.ts +0 -20
  137. package/src/integrations/google-calendar/connectorDef.tsx +0 -185
  138. package/src/integrations/google-calendar/data/supabase.ts +0 -170
  139. package/src/integrations/google-calendar/data/tables.ts +0 -12
  140. package/src/integrations/google-calendar/index.ts +0 -45
  141. package/src/integrations/google-calendar/integration.test.ts +0 -199
  142. package/src/integrations/google-calendar/mapping.ts +0 -183
  143. package/src/integrations/google-calendar/migrations/001_google_calendar.sql +0 -97
  144. package/src/integrations/google-calendar/migrations/002_smart_sync.sql +0 -517
  145. package/src/integrations/google-calendar/migrations/003_plg_rename.sql +0 -273
  146. package/src/integrations/google-calendar/types.ts +0 -142
  147. package/src/lib/tenant.ts +0 -3
  148. package/src/locales/en.ts +0 -220
  149. package/src/locales/index.ts +0 -7
  150. package/src/locales/pt-BR.ts +0 -220
  151. package/src/migrations/000b_v_bookings_rename.sql +0 -21
  152. package/src/migrations/000c_dedupe_customer_phones.sql +0 -43
  153. package/src/migrations/001_public_booking.sql +0 -296
  154. package/src/migrations/002_v_bookings_compat.sql +0 -14
  155. package/src/migrations/003_public_client_kind.sql +0 -175
  156. package/src/migrations/004_public_booking_assignee.sql +0 -176
  157. package/src/migrations/005_agent_rpcs.sql +0 -168
  158. package/src/migrations/006_agent_rpc_localtime.sql +0 -181
  159. package/src/migrations/007_schedule_exception_precedence.sql +0 -290
  160. package/src/migrations/008_agent_rpc_exception_precedence.sql +0 -172
  161. package/src/migrations/009_public_booking_location.sql +0 -162
  162. package/src/migrations/010_public_service_curation.sql +0 -31
  163. package/src/migrations/011_default_assignee.sql +0 -167
  164. package/src/migrations/012_drop_legacy_booking_overload.sql +0 -29
  165. package/src/migrations/013_normalize_v_appointments.sql +0 -199
  166. package/src/migrations/014_agent_rpc_update_appointment.sql +0 -199
  167. package/src/migrations/index.ts +0 -2377
  168. package/src/public/BookingWidget.tsx +0 -324
  169. package/src/public/components/BrandHeader.tsx +0 -27
  170. package/src/public/components/ServiceCard.tsx +0 -39
  171. package/src/public/components/SidebarExtras.tsx +0 -30
  172. package/src/public/components/StepRailItem.tsx +0 -27
  173. package/src/public/components/SuccessPanel.tsx +0 -27
  174. package/src/public/components/SummaryRail.tsx +0 -90
  175. package/src/public/components/defaults.ts +0 -14
  176. package/src/public/context.tsx +0 -103
  177. package/src/public/createPublicBookingPlugin.ts +0 -281
  178. package/src/public/data.supabase.ts +0 -97
  179. package/src/public/data.ts +0 -105
  180. package/src/public/format.ts +0 -96
  181. package/src/public/hooks.ts +0 -110
  182. package/src/public/index.ts +0 -69
  183. package/src/public/steps/ContactStep.tsx +0 -184
  184. package/src/public/steps/DateTimeStep.tsx +0 -138
  185. package/src/public/steps/OverviewStep.tsx +0 -81
  186. package/src/public/steps/PaymentStep.tsx +0 -180
  187. package/src/public/types.ts +0 -250
  188. package/src/registries.ts +0 -35
  189. package/src/store.ts +0 -311
  190. package/src/types.ts +0 -273
  191. package/src/views/CalendarView.tsx +0 -1112
  192. package/src/views/ConfirmationsView.tsx +0 -155
  193. /package/dist/{financial-bridge.d.ts → bridges/financial-bridge.d.ts} +0 -0
  194. /package/dist/{registries.d.ts → data/registries.d.ts} +0 -0
  195. /package/dist/public/{BookingWidget.d.ts → views/BookingWidget.d.ts} +0 -0
  196. /package/dist/{calendar-styles.d.ts → styles/calendar-styles.d.ts} +0 -0
@@ -1,329 +0,0 @@
1
- // De-para (mapping) list — one row per Google calendar.
2
- //
3
- // Reads the user's Google calendars (listGoogleCalendars) and this tenant's
4
- // existing channels (listChannels), joins them by google_calendar_id, and lets
5
- // the user, per calendar:
6
- // • toggle sync on/off → creates/updates the channel (is_active)
7
- // • pick a direction → Two-way / Import only / Export only / Off
8
- // • pick a target → Whole agenda / Professional / Service / Location
9
- // • pick an import mode → Block / Appointment
10
- // Every change is persisted immediately via saveChannel (RLS upsert on
11
- // integration_id + google_calendar_id), snapshotting the calendar summary+color
12
- // onto the channel. Target ids come from real host lookups (useAgendaLookups);
13
- // if a lookup is empty for the tenant the row degrades to a raw uuid input.
14
- import React, { useCallback, useEffect, useState } from 'react'
15
- import { Loader2, AlertCircle, RefreshCw } from 'lucide-react'
16
- import {
17
- Button,
18
- Checkbox,
19
- Badge,
20
- Select,
21
- SelectTrigger,
22
- SelectValue,
23
- SelectContent,
24
- SelectItem,
25
- toast,
26
- } from '@fayz-ai/ui'
27
- import type { GoogleCalendarProvider } from '../data/supabase'
28
- import type {
29
- CalendarChannel,
30
- ChannelDirection,
31
- ChannelTargetKind,
32
- ChannelImportMode,
33
- GoogleCalendarListEntry,
34
- } from '../types'
35
- import { CalendarColorDot } from './CalendarColorDot'
36
- import { useAgendaLookups, optionsForKind } from './useAgendaLookups'
37
-
38
- const DIRECTION_LABELS: Record<ChannelDirection, string> = {
39
- bidirectional: 'Two-way',
40
- inbound: 'Import only',
41
- outbound: 'Export only',
42
- off: 'Off',
43
- }
44
- const IMPORT_MODE_LABELS: Record<ChannelImportMode, string> = {
45
- block: 'Block',
46
- appointment: 'Appointment',
47
- }
48
- // 'agenda' is the UI sentinel for the whole-agenda target (target_kind = null).
49
- const TARGET_KIND_LABELS: Record<'agenda' | 'assignee' | 'service' | 'location', string> = {
50
- agenda: 'Whole agenda',
51
- assignee: 'Professional',
52
- service: 'Service',
53
- location: 'Location',
54
- }
55
- const NO_TARGET = '__none__'
56
-
57
- /** Defaults for a freshly-enabled calendar (mirror the migration defaults). */
58
- function defaultChannel(): {
59
- direction: ChannelDirection
60
- targetKind: ChannelTargetKind
61
- targetId: string | null
62
- importMode: ChannelImportMode
63
- } {
64
- return { direction: 'bidirectional', targetKind: null, targetId: null, importMode: 'block' }
65
- }
66
-
67
- export function ChannelMappingList({
68
- provider,
69
- onChannelsChange,
70
- }: {
71
- provider: GoogleCalendarProvider
72
- /** Bubble the current channels up so the parent can gate the import wizard. */
73
- onChannelsChange?: (channels: CalendarChannel[]) => void
74
- }) {
75
- const [calendars, setCalendars] = useState<GoogleCalendarListEntry[]>([])
76
- const [byGcalId, setByGcalId] = useState<Record<string, CalendarChannel>>({})
77
- const [loading, setLoading] = useState(true)
78
- const [error, setError] = useState<string | null>(null)
79
- const [savingId, setSavingId] = useState<string | null>(null)
80
-
81
- const { lookups, hasAny: hasLookups } = useAgendaLookups(true)
82
-
83
- const load = useCallback(async () => {
84
- setLoading(true)
85
- setError(null)
86
- try {
87
- const [cals, channels] = await Promise.all([
88
- provider.listGoogleCalendars(),
89
- provider.listChannels(),
90
- ])
91
- setCalendars(cals)
92
- const map: Record<string, CalendarChannel> = {}
93
- for (const ch of channels) map[ch.googleCalendarId] = ch
94
- setByGcalId(map)
95
- onChannelsChange?.(channels)
96
- } catch (e: any) {
97
- setError(e?.message ?? 'Could not load Google calendars')
98
- } finally {
99
- setLoading(false)
100
- }
101
- }, [provider, onChannelsChange])
102
-
103
- useEffect(() => {
104
- void load()
105
- }, [load])
106
-
107
- /** Persist a patch for one calendar, then refresh channels (keeps ids correct). */
108
- const persist = useCallback(
109
- async (
110
- cal: GoogleCalendarListEntry,
111
- patch: {
112
- direction?: ChannelDirection
113
- targetKind?: ChannelTargetKind
114
- targetId?: string | null
115
- importMode?: ChannelImportMode
116
- isActive?: boolean
117
- },
118
- ) => {
119
- const existing = byGcalId[cal.id]
120
- const base = existing ?? { ...defaultChannel(), isActive: true }
121
- setSavingId(cal.id)
122
- try {
123
- await provider.saveChannel({
124
- id: existing?.id,
125
- googleCalendarId: cal.id,
126
- summary: cal.summary,
127
- color: cal.backgroundColor ?? null,
128
- direction: patch.direction ?? base.direction,
129
- targetKind: patch.targetKind !== undefined ? patch.targetKind : base.targetKind,
130
- targetId: patch.targetId !== undefined ? patch.targetId : base.targetId ?? null,
131
- importMode: patch.importMode ?? base.importMode,
132
- isActive: patch.isActive !== undefined ? patch.isActive : base.isActive,
133
- })
134
- const channels = await provider.listChannels()
135
- const map: Record<string, CalendarChannel> = {}
136
- for (const ch of channels) map[ch.googleCalendarId] = ch
137
- setByGcalId(map)
138
- onChannelsChange?.(channels)
139
- } catch (e: any) {
140
- toast.error(e?.message ?? 'Could not save calendar mapping')
141
- } finally {
142
- setSavingId(null)
143
- }
144
- },
145
- [byGcalId, provider, onChannelsChange],
146
- )
147
-
148
- if (loading) {
149
- return (
150
- <div className="py-6 text-center text-sm text-muted-foreground">
151
- <Loader2 className="h-4 w-4 animate-spin inline" />
152
- </div>
153
- )
154
- }
155
-
156
- if (error) {
157
- return (
158
- <div className="rounded-md border border-warning/40 bg-warning-soft/40 px-3 py-3 text-sm">
159
- <div className="flex items-center gap-2 text-warning-soft-foreground">
160
- <AlertCircle className="h-4 w-4" /> {error}
161
- </div>
162
- <Button variant="outline" size="sm" className="mt-2" onClick={() => void load()}>
163
- <RefreshCw className="h-3.5 w-3.5" /> Retry
164
- </Button>
165
- </div>
166
- )
167
- }
168
-
169
- if (calendars.length === 0) {
170
- return (
171
- <p className="py-6 text-center text-sm text-muted-foreground">
172
- No Google calendars found on this account.
173
- </p>
174
- )
175
- }
176
-
177
- return (
178
- <div className="rounded-md border divide-y">
179
- {calendars.map((cal) => {
180
- const ch = byGcalId[cal.id]
181
- const active = !!ch?.isActive
182
- const saving = savingId === cal.id
183
- const direction: ChannelDirection = ch?.direction ?? 'bidirectional'
184
- const targetKind: ChannelTargetKind = ch?.targetKind ?? null
185
- const importMode: ChannelImportMode = ch?.importMode ?? 'block'
186
- // Import mode only matters when Google can write into the agenda.
187
- const canImport = direction === 'inbound' || direction === 'bidirectional'
188
- const kindOptions = optionsForKind(lookups, targetKind)
189
-
190
- return (
191
- <div key={cal.id} className="px-3 py-3 space-y-3">
192
- <div className="flex items-center gap-2.5">
193
- <Checkbox
194
- checked={active}
195
- disabled={saving}
196
- onChange={(next) => void persist(cal, { isActive: next })}
197
- />
198
- <CalendarColorDot color={cal.backgroundColor} />
199
- <span className="text-sm font-medium truncate">{cal.summary}</span>
200
- {cal.primary && (
201
- <Badge variant="outline" className="shrink-0">
202
- Primary
203
- </Badge>
204
- )}
205
- {saving && <Loader2 className="ml-auto h-3.5 w-3.5 animate-spin text-muted-foreground" />}
206
- </div>
207
-
208
- {active && (
209
- <div className="grid gap-2 pl-[26px] sm:grid-cols-2 lg:grid-cols-4">
210
- {/* Direction */}
211
- <label className="block">
212
- <span className="text-[11px] font-medium text-muted-foreground">Direction</span>
213
- <Select
214
- value={direction}
215
- onValueChange={(v) => void persist(cal, { direction: v as ChannelDirection })}
216
- >
217
- <SelectTrigger className="mt-1 h-8 text-sm">
218
- <SelectValue />
219
- </SelectTrigger>
220
- <SelectContent>
221
- {(Object.keys(DIRECTION_LABELS) as ChannelDirection[]).map((d) => (
222
- <SelectItem key={d} value={d}>
223
- {DIRECTION_LABELS[d]}
224
- </SelectItem>
225
- ))}
226
- </SelectContent>
227
- </Select>
228
- </label>
229
-
230
- {/* Target kind */}
231
- <label className="block">
232
- <span className="text-[11px] font-medium text-muted-foreground">Target</span>
233
- <Select
234
- value={targetKind ?? 'agenda'}
235
- onValueChange={(v) =>
236
- void persist(cal, {
237
- targetKind: v === 'agenda' ? null : (v as ChannelTargetKind),
238
- // Reset the id whenever the kind changes.
239
- targetId: null,
240
- })
241
- }
242
- >
243
- <SelectTrigger className="mt-1 h-8 text-sm">
244
- <SelectValue />
245
- </SelectTrigger>
246
- <SelectContent>
247
- {(Object.keys(TARGET_KIND_LABELS) as Array<keyof typeof TARGET_KIND_LABELS>).map(
248
- (k) => (
249
- <SelectItem key={k} value={k}>
250
- {TARGET_KIND_LABELS[k]}
251
- </SelectItem>
252
- ),
253
- )}
254
- </SelectContent>
255
- </Select>
256
- </label>
257
-
258
- {/* Target id — real picker when we have lookups, uuid input otherwise */}
259
- {targetKind && (
260
- <label className="block">
261
- <span className="text-[11px] font-medium text-muted-foreground">
262
- {TARGET_KIND_LABELS[targetKind]}
263
- </span>
264
- {kindOptions.length > 0 ? (
265
- <Select
266
- value={ch?.targetId ?? NO_TARGET}
267
- onValueChange={(v) =>
268
- void persist(cal, { targetId: v === NO_TARGET ? null : v })
269
- }
270
- >
271
- <SelectTrigger className="mt-1 h-8 text-sm">
272
- <SelectValue placeholder="Select…" />
273
- </SelectTrigger>
274
- <SelectContent>
275
- {kindOptions.map((o) => (
276
- <SelectItem key={o.id} value={o.id}>
277
- {o.name}
278
- </SelectItem>
279
- ))}
280
- </SelectContent>
281
- </Select>
282
- ) : (
283
- <input
284
- defaultValue={ch?.targetId ?? ''}
285
- placeholder="uuid"
286
- onBlur={(e) =>
287
- void persist(cal, { targetId: e.target.value.trim() || null })
288
- }
289
- className="w-full mt-1 h-8 rounded-input border border-input bg-card px-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
290
- />
291
- )}
292
- </label>
293
- )}
294
-
295
- {/* Import mode — only when inbound */}
296
- {canImport && (
297
- <label className="block">
298
- <span className="text-[11px] font-medium text-muted-foreground">Import as</span>
299
- <Select
300
- value={importMode}
301
- onValueChange={(v) => void persist(cal, { importMode: v as ChannelImportMode })}
302
- >
303
- <SelectTrigger className="mt-1 h-8 text-sm">
304
- <SelectValue />
305
- </SelectTrigger>
306
- <SelectContent>
307
- {(Object.keys(IMPORT_MODE_LABELS) as ChannelImportMode[]).map((m) => (
308
- <SelectItem key={m} value={m}>
309
- {IMPORT_MODE_LABELS[m]}
310
- </SelectItem>
311
- ))}
312
- </SelectContent>
313
- </Select>
314
- </label>
315
- )}
316
- </div>
317
- )}
318
- </div>
319
- )
320
- })}
321
- {!hasLookups && (
322
- <p className="px-3 py-2 text-[11px] text-muted-foreground">
323
- No professional/service/location lookups available — target ids fall back to manual uuid
324
- entry.
325
- </p>
326
- )}
327
- </div>
328
- )
329
- }
@@ -1,215 +0,0 @@
1
- // Import wizard — pull FUTURE Google events into the agenda as blocks/appointments.
2
- //
3
- // listExternalEvents() returns events on the active inbound channels that are not
4
- // yet linked to an appointment. The user ticks the ones to bring in and confirms;
5
- // importEvents() runs them through gcal_import_event (honouring each channel's
6
- // import_mode + target) and returns a per-item tally {imported, skipped, errors}.
7
- // Events show their source calendar as a colored badge and an "All-day" badge
8
- // when applicable; times are formatted pt-BR.
9
- import React, { useCallback, useEffect, useMemo, useState } from 'react'
10
- import { Loader2, CalendarPlus, CheckCircle2, AlertCircle, RefreshCw, X } from 'lucide-react'
11
- import { Button, Checkbox, Badge, toast } from '@fayz-ai/ui'
12
- import type { GoogleCalendarProvider } from '../data/supabase'
13
- import type { CalendarChannel, ExternalEventPreview, ImportEventsResult } from '../types'
14
- import { CalendarColorDot } from './CalendarColorDot'
15
-
16
- /** pt-BR datetime label, all-day aware (date only for all-day events). */
17
- function formatWhen(ev: ExternalEventPreview): string {
18
- const start = new Date(ev.startsAt)
19
- if (ev.allDay) {
20
- return start.toLocaleDateString('pt-BR', { day: '2-digit', month: 'short', year: 'numeric' })
21
- }
22
- return start.toLocaleString('pt-BR', {
23
- day: '2-digit',
24
- month: 'short',
25
- hour: '2-digit',
26
- minute: '2-digit',
27
- })
28
- }
29
-
30
- export function ImportEventsWizard({
31
- provider,
32
- channels,
33
- onClose,
34
- onImported,
35
- }: {
36
- provider: GoogleCalendarProvider
37
- /** Active channels — used to label each event with its source calendar + color. */
38
- channels: CalendarChannel[]
39
- onClose: () => void
40
- /** Fired after a successful import so the parent can refresh status/history. */
41
- onImported?: (result: ImportEventsResult) => void
42
- }) {
43
- const [events, setEvents] = useState<ExternalEventPreview[]>([])
44
- const [selected, setSelected] = useState<Set<string>>(new Set())
45
- const [loading, setLoading] = useState(true)
46
- const [error, setError] = useState<string | null>(null)
47
- const [importing, setImporting] = useState(false)
48
- const [result, setResult] = useState<ImportEventsResult | null>(null)
49
-
50
- const channelById = useMemo(() => {
51
- const m: Record<string, CalendarChannel> = {}
52
- for (const c of channels) m[c.id] = c
53
- return m
54
- }, [channels])
55
-
56
- const load = useCallback(async () => {
57
- setLoading(true)
58
- setError(null)
59
- setResult(null)
60
- try {
61
- const rows = await provider.listExternalEvents()
62
- setEvents(rows)
63
- setSelected(new Set(rows.map((r) => r.eventId)))
64
- } catch (e: any) {
65
- setError(e?.message ?? 'Could not load upcoming events')
66
- } finally {
67
- setLoading(false)
68
- }
69
- }, [provider])
70
-
71
- useEffect(() => {
72
- void load()
73
- }, [load])
74
-
75
- const toggle = (eventId: string) =>
76
- setSelected((prev) => {
77
- const next = new Set(prev)
78
- if (next.has(eventId)) next.delete(eventId)
79
- else next.add(eventId)
80
- return next
81
- })
82
-
83
- const allSelected = events.length > 0 && selected.size === events.length
84
- const toggleAll = () =>
85
- setSelected(allSelected ? new Set() : new Set(events.map((e) => e.eventId)))
86
-
87
- async function handleImport() {
88
- const items = events
89
- .filter((e) => selected.has(e.eventId))
90
- .map((e) => ({ channelId: e.channelId, eventId: e.eventId }))
91
- if (items.length === 0) return
92
- setImporting(true)
93
- try {
94
- const res = await provider.importEvents(items)
95
- setResult(res)
96
- onImported?.(res)
97
- toast.success(`Imported ${res.imported} · skipped ${res.skipped}`)
98
- // Drop imported events from the list so the wizard reflects the new state.
99
- await load()
100
- } catch (e: any) {
101
- toast.error(e?.message ?? 'Import failed')
102
- } finally {
103
- setImporting(false)
104
- }
105
- }
106
-
107
- return (
108
- <div className="rounded-md border bg-card">
109
- <div className="flex items-center gap-2 border-b px-3 py-2">
110
- <CalendarPlus className="h-4 w-4 text-muted-foreground" />
111
- <h4 className="text-sm font-semibold">Import future events</h4>
112
- <button
113
- onClick={onClose}
114
- className="ml-auto rounded p-1 text-muted-foreground hover:bg-muted/50"
115
- aria-label="Close"
116
- >
117
- <X className="h-3.5 w-3.5" />
118
- </button>
119
- </div>
120
-
121
- {loading ? (
122
- <div className="py-8 text-center text-sm text-muted-foreground">
123
- <Loader2 className="h-4 w-4 animate-spin inline" />
124
- </div>
125
- ) : error ? (
126
- <div className="p-3">
127
- <div className="flex items-center gap-2 text-sm text-warning-soft-foreground">
128
- <AlertCircle className="h-4 w-4" /> {error}
129
- </div>
130
- <Button variant="outline" size="sm" className="mt-2" onClick={() => void load()}>
131
- <RefreshCw className="h-3.5 w-3.5" /> Retry
132
- </Button>
133
- </div>
134
- ) : events.length === 0 ? (
135
- <div className="py-8 px-4 text-center space-y-1">
136
- <p className="text-sm font-medium">You're all caught up</p>
137
- <p className="text-xs text-muted-foreground">
138
- No upcoming Google events left to import on your active calendars.
139
- </p>
140
- {result && (
141
- <p className="text-xs text-success">
142
- Imported {result.imported} · skipped {result.skipped}
143
- {result.errors.length > 0 ? ` · ${result.errors.length} errors` : ''}
144
- </p>
145
- )}
146
- </div>
147
- ) : (
148
- <>
149
- <div className="flex items-center gap-2.5 border-b px-3 py-2">
150
- <Checkbox checked={allSelected} onChange={toggleAll} />
151
- <span className="text-xs text-muted-foreground">
152
- {selected.size} of {events.length} selected
153
- </span>
154
- </div>
155
- <div className="max-h-[320px] overflow-y-auto divide-y">
156
- {events.map((ev) => {
157
- const ch = channelById[ev.channelId]
158
- return (
159
- <label
160
- key={ev.eventId}
161
- className="flex items-start gap-2.5 px-3 py-2.5 cursor-pointer hover:bg-muted/30"
162
- >
163
- <Checkbox
164
- checked={selected.has(ev.eventId)}
165
- onChange={() => toggle(ev.eventId)}
166
- />
167
- <div className="min-w-0 flex-1 space-y-1">
168
- <div className="flex items-center gap-2">
169
- <span className="text-sm font-medium truncate">
170
- {ev.summary || '(no title)'}
171
- </span>
172
- {ev.allDay && (
173
- <Badge variant="secondary" className="shrink-0">
174
- All-day
175
- </Badge>
176
- )}
177
- </div>
178
- <div className="flex items-center gap-2 text-xs text-muted-foreground">
179
- <span>{formatWhen(ev)}</span>
180
- <span className="inline-flex items-center gap-1">
181
- <CalendarColorDot color={ch?.color} />
182
- {ch?.summary ?? ev.calendarId}
183
- </span>
184
- </div>
185
- </div>
186
- </label>
187
- )
188
- })}
189
- </div>
190
- <div className="flex items-center gap-2 border-t px-3 py-2">
191
- {result && (
192
- <span className="text-xs text-muted-foreground">
193
- Last run: {result.imported} imported
194
- {result.errors.length > 0 ? `, ${result.errors.length} errors` : ''}
195
- </span>
196
- )}
197
- <Button
198
- size="sm"
199
- className="ml-auto"
200
- disabled={importing || selected.size === 0}
201
- onClick={handleImport}
202
- >
203
- {importing ? (
204
- <Loader2 className="h-3.5 w-3.5 animate-spin" />
205
- ) : (
206
- <CheckCircle2 className="h-3.5 w-3.5" />
207
- )}
208
- Import {selected.size > 0 ? selected.size : ''}
209
- </Button>
210
- </div>
211
- </>
212
- )}
213
- </div>
214
- )
215
- }
@@ -1,100 +0,0 @@
1
- // Host-plugin lookups for the smart-sync de-para target picker.
2
- //
3
- // A channel can target one professional (assignee), one service, or one location
4
- // instead of the whole agenda. To offer real pickers (not raw uuid inputs) the
5
- // connector reuses plugin-agenda's own data providers — it lives inside the same
6
- // package, so these are in-process reads under the same RLS:
7
- // • professionals + locations → createSupabaseAgendaProvider()
8
- // • services → createSupabasePublicBookingProvider() (v_public_services)
9
- // Everything is loaded once, lazily, and degrades to an empty list (the row then
10
- // falls back to a plain uuid input) if a lookup is unavailable for the tenant.
11
- import { useEffect, useState } from 'react'
12
- import { getActiveTenantId } from '@fayz-ai/core'
13
- import { createSupabaseAgendaProvider } from '../../../data'
14
- import { createSupabasePublicBookingProvider } from '../../../public/data.supabase'
15
- import type { ChannelTargetKind } from '../types'
16
-
17
- /** A pickable target option (assignee/service/location) — id + display name. */
18
- export interface LookupOption {
19
- id: string
20
- name: string
21
- }
22
-
23
- export interface AgendaLookups {
24
- assignee: LookupOption[]
25
- service: LookupOption[]
26
- location: LookupOption[]
27
- }
28
-
29
- const EMPTY: AgendaLookups = { assignee: [], service: [], location: [] }
30
-
31
- export interface UseAgendaLookupsResult {
32
- lookups: AgendaLookups
33
- loading: boolean
34
- /** True when at least one of the three lookups returned rows. */
35
- hasAny: boolean
36
- }
37
-
38
- /**
39
- * Load professionals / services / locations for the target picker. Failures are
40
- * swallowed per-lookup (each falls back to an empty list) so a missing table or
41
- * a vertical without services never breaks the whole de-para screen.
42
- */
43
- export function useAgendaLookups(enabled: boolean): UseAgendaLookupsResult {
44
- const [lookups, setLookups] = useState<AgendaLookups>(EMPTY)
45
- const [loading, setLoading] = useState(false)
46
-
47
- useEffect(() => {
48
- if (!enabled) return
49
- let cancelled = false
50
- setLoading(true)
51
- void (async () => {
52
- const agenda = createSupabaseAgendaProvider()
53
- const tenantId = getActiveTenantId()
54
- const publicProvider = tenantId
55
- ? createSupabasePublicBookingProvider({ tenantId })
56
- : null
57
-
58
- const [assignee, location, service] = await Promise.all([
59
- agenda.getProfessionals().then(
60
- (rows) => rows.map((p) => ({ id: p.id, name: p.name })),
61
- () => [] as LookupOption[],
62
- ),
63
- (agenda.getLocations?.() ?? Promise.resolve([])).then(
64
- (rows) => rows.map((l) => ({ id: l.id, name: l.name })),
65
- () => [] as LookupOption[],
66
- ),
67
- (publicProvider?.getServices() ?? Promise.resolve([])).then(
68
- (rows) => rows.map((s) => ({ id: s.id, name: s.name })),
69
- () => [] as LookupOption[],
70
- ),
71
- ])
72
-
73
- if (cancelled) return
74
- setLookups({ assignee, service, location })
75
- setLoading(false)
76
- })()
77
-
78
- return () => {
79
- cancelled = true
80
- }
81
- }, [enabled])
82
-
83
- const hasAny =
84
- lookups.assignee.length > 0 ||
85
- lookups.service.length > 0 ||
86
- lookups.location.length > 0
87
-
88
- return { lookups, loading, hasAny }
89
- }
90
-
91
- /** Options available for a given target kind (empty for whole-agenda / null). */
92
- export function optionsForKind(
93
- lookups: AgendaLookups,
94
- kind: ChannelTargetKind,
95
- ): LookupOption[] {
96
- if (kind === 'assignee') return lookups.assignee
97
- if (kind === 'service') return lookups.service
98
- if (kind === 'location') return lookups.location
99
- return []
100
- }
@@ -1,20 +0,0 @@
1
- // Google Calendar connector descriptor (control-plane metadata).
2
- //
3
- // Declares the connector against the SDK integration spine so the platform can
4
- // reason about what it syncs. The actual data-plane verbs run in the
5
- // google-calendar-sync edge function; this is the typed contract + capabilities.
6
- import type { Connector } from '@fayz-ai/core'
7
-
8
- export const googleCalendarConnector: Connector = {
9
- id: 'google-calendar',
10
- provider: 'google',
11
- pluginId: 'agenda',
12
- authKind: 'oauth',
13
- capabilities: [
14
- {
15
- entity: 'booking',
16
- direction: 'bidirectional',
17
- triggers: ['on-write', 'scheduled', 'webhook'],
18
- },
19
- ],
20
- }