@fayz-ai/plugin-agenda 0.9.0 → 0.9.1

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 (153) 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-76ZOHRBM.js} +2 -2
  6. package/dist/{chunk-Q2PAWBPR.js.map → chunk-76ZOHRBM.js.map} +1 -1
  7. package/dist/components/PaymentStatusBadge.d.ts +1 -1
  8. package/dist/components/PaymentStatusBadge.d.ts.map +1 -1
  9. package/dist/config.d.ts +2 -2
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/{AgendaContext.d.ts → context.d.ts} +1 -1
  12. package/dist/context.d.ts.map +1 -0
  13. package/dist/data/registries.d.ts.map +1 -0
  14. package/dist/index.d.ts +2 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +170 -7
  17. package/dist/index.js.map +1 -1
  18. package/dist/lib/message-events.d.ts +12 -0
  19. package/dist/lib/message-events.d.ts.map +1 -0
  20. package/dist/public/{data.d.ts → data/index.d.ts} +4 -4
  21. package/dist/public/data/index.d.ts.map +1 -0
  22. package/dist/public/{data.supabase.d.ts → data/supabase.d.ts} +2 -2
  23. package/dist/public/data/supabase.d.ts.map +1 -0
  24. package/dist/public/{hooks.d.ts → hooks/index.d.ts} +4 -4
  25. package/dist/public/hooks/index.d.ts.map +1 -0
  26. package/dist/public/index.d.ts +4 -4
  27. package/dist/public/index.d.ts.map +1 -1
  28. package/dist/public/index.js +3 -3
  29. package/dist/public/index.js.map +1 -1
  30. package/dist/public/{format.d.ts → lib/format.d.ts} +1 -1
  31. package/dist/public/lib/format.d.ts.map +1 -0
  32. package/dist/public/views/BookingWidget.d.ts.map +1 -0
  33. package/dist/public/{steps → views/steps}/ContactStep.d.ts +1 -1
  34. package/dist/public/views/steps/ContactStep.d.ts.map +1 -0
  35. package/dist/public/{steps → views/steps}/DateTimeStep.d.ts +3 -3
  36. package/dist/public/views/steps/DateTimeStep.d.ts.map +1 -0
  37. package/dist/public/{steps → views/steps}/OverviewStep.d.ts +1 -1
  38. package/dist/public/views/steps/OverviewStep.d.ts.map +1 -0
  39. package/dist/public/{steps → views/steps}/PaymentStep.d.ts +1 -1
  40. package/dist/public/views/steps/PaymentStep.d.ts.map +1 -0
  41. package/dist/store.d.ts +1 -1
  42. package/dist/store.d.ts.map +1 -1
  43. package/dist/styles/calendar-styles.d.ts.map +1 -0
  44. package/dist/types.d.ts +1 -1
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/{AgendaPage.d.ts → views/AgendaPage.d.ts} +3 -3
  47. package/dist/views/AgendaPage.d.ts.map +1 -0
  48. package/package.json +6 -9
  49. package/dist/AgendaContext.d.ts.map +0 -1
  50. package/dist/AgendaPage.d.ts.map +0 -1
  51. package/dist/calendar-styles.d.ts.map +0 -1
  52. package/dist/financial-bridge.d.ts.map +0 -1
  53. package/dist/public/BookingWidget.d.ts.map +0 -1
  54. package/dist/public/data.d.ts.map +0 -1
  55. package/dist/public/data.supabase.d.ts.map +0 -1
  56. package/dist/public/format.d.ts.map +0 -1
  57. package/dist/public/hooks.d.ts.map +0 -1
  58. package/dist/public/steps/ContactStep.d.ts.map +0 -1
  59. package/dist/public/steps/DateTimeStep.d.ts.map +0 -1
  60. package/dist/public/steps/OverviewStep.d.ts.map +0 -1
  61. package/dist/public/steps/PaymentStep.d.ts.map +0 -1
  62. package/dist/registries.d.ts.map +0 -1
  63. package/src/AgendaContext.tsx +0 -11
  64. package/src/AgendaPage.tsx +0 -33
  65. package/src/bridges/create-financial-bridge.ts +0 -296
  66. package/src/calendar-styles.css +0 -245
  67. package/src/calendar-styles.ts +0 -285
  68. package/src/components/AgendaGeneralSettings.tsx +0 -86
  69. package/src/components/AgendaMobileList.tsx +0 -103
  70. package/src/components/AppointmentModal.tsx +0 -1050
  71. package/src/components/AppointmentPopover.tsx +0 -231
  72. package/src/components/BookingPaymentPanel.tsx +0 -394
  73. package/src/components/CalendarSidebar.tsx +0 -119
  74. package/src/components/CalendarToolbar.tsx +0 -86
  75. package/src/components/EventContextMenu.tsx +0 -125
  76. package/src/components/FloatingPanel.tsx +0 -123
  77. package/src/components/MiniCalendar.tsx +0 -120
  78. package/src/components/PaymentStatusBadge.tsx +0 -62
  79. package/src/components/StatusBadge.tsx +0 -23
  80. package/src/config.ts +0 -368
  81. package/src/data/index.ts +0 -4
  82. package/src/data/mock.ts +0 -344
  83. package/src/data/supabase.ts +0 -636
  84. package/src/data/types.ts +0 -37
  85. package/src/financial-bridge.ts +0 -96
  86. package/src/hooks/useAgendaSettings.ts +0 -17
  87. package/src/index.ts +0 -297
  88. package/src/integrations/google-calendar/GoogleCalendarSettings.tsx +0 -136
  89. package/src/integrations/google-calendar/components/CalendarColorDot.tsx +0 -23
  90. package/src/integrations/google-calendar/components/ChannelMappingList.tsx +0 -329
  91. package/src/integrations/google-calendar/components/ImportEventsWizard.tsx +0 -215
  92. package/src/integrations/google-calendar/components/useAgendaLookups.ts +0 -100
  93. package/src/integrations/google-calendar/connector.ts +0 -20
  94. package/src/integrations/google-calendar/connectorDef.tsx +0 -185
  95. package/src/integrations/google-calendar/data/supabase.ts +0 -170
  96. package/src/integrations/google-calendar/data/tables.ts +0 -12
  97. package/src/integrations/google-calendar/index.ts +0 -45
  98. package/src/integrations/google-calendar/integration.test.ts +0 -199
  99. package/src/integrations/google-calendar/mapping.ts +0 -183
  100. package/src/integrations/google-calendar/migrations/001_google_calendar.sql +0 -97
  101. package/src/integrations/google-calendar/migrations/002_smart_sync.sql +0 -517
  102. package/src/integrations/google-calendar/migrations/003_plg_rename.sql +0 -273
  103. package/src/integrations/google-calendar/types.ts +0 -142
  104. package/src/lib/tenant.ts +0 -3
  105. package/src/locales/en.ts +0 -220
  106. package/src/locales/index.ts +0 -7
  107. package/src/locales/pt-BR.ts +0 -220
  108. package/src/migrations/000b_v_bookings_rename.sql +0 -21
  109. package/src/migrations/000c_dedupe_customer_phones.sql +0 -43
  110. package/src/migrations/001_public_booking.sql +0 -296
  111. package/src/migrations/002_v_bookings_compat.sql +0 -14
  112. package/src/migrations/003_public_client_kind.sql +0 -175
  113. package/src/migrations/004_public_booking_assignee.sql +0 -176
  114. package/src/migrations/005_agent_rpcs.sql +0 -168
  115. package/src/migrations/006_agent_rpc_localtime.sql +0 -181
  116. package/src/migrations/007_schedule_exception_precedence.sql +0 -290
  117. package/src/migrations/008_agent_rpc_exception_precedence.sql +0 -172
  118. package/src/migrations/009_public_booking_location.sql +0 -162
  119. package/src/migrations/010_public_service_curation.sql +0 -31
  120. package/src/migrations/011_default_assignee.sql +0 -167
  121. package/src/migrations/012_drop_legacy_booking_overload.sql +0 -29
  122. package/src/migrations/013_normalize_v_appointments.sql +0 -199
  123. package/src/migrations/014_agent_rpc_update_appointment.sql +0 -199
  124. package/src/migrations/index.ts +0 -2377
  125. package/src/public/BookingWidget.tsx +0 -324
  126. package/src/public/components/BrandHeader.tsx +0 -27
  127. package/src/public/components/ServiceCard.tsx +0 -39
  128. package/src/public/components/SidebarExtras.tsx +0 -30
  129. package/src/public/components/StepRailItem.tsx +0 -27
  130. package/src/public/components/SuccessPanel.tsx +0 -27
  131. package/src/public/components/SummaryRail.tsx +0 -90
  132. package/src/public/components/defaults.ts +0 -14
  133. package/src/public/context.tsx +0 -103
  134. package/src/public/createPublicBookingPlugin.ts +0 -281
  135. package/src/public/data.supabase.ts +0 -97
  136. package/src/public/data.ts +0 -105
  137. package/src/public/format.ts +0 -96
  138. package/src/public/hooks.ts +0 -110
  139. package/src/public/index.ts +0 -69
  140. package/src/public/steps/ContactStep.tsx +0 -184
  141. package/src/public/steps/DateTimeStep.tsx +0 -138
  142. package/src/public/steps/OverviewStep.tsx +0 -81
  143. package/src/public/steps/PaymentStep.tsx +0 -180
  144. package/src/public/types.ts +0 -250
  145. package/src/registries.ts +0 -35
  146. package/src/store.ts +0 -311
  147. package/src/types.ts +0 -273
  148. package/src/views/CalendarView.tsx +0 -1112
  149. package/src/views/ConfirmationsView.tsx +0 -155
  150. /package/dist/{financial-bridge.d.ts → bridges/financial-bridge.d.ts} +0 -0
  151. /package/dist/{registries.d.ts → data/registries.d.ts} +0 -0
  152. /package/dist/public/{BookingWidget.d.ts → views/BookingWidget.d.ts} +0 -0
  153. /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
- }