@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.
- package/README.md +3 -3
- package/dist/bridges/create-financial-bridge.d.ts +1 -1
- package/dist/bridges/create-financial-bridge.d.ts.map +1 -1
- package/dist/bridges/financial-bridge.d.ts.map +1 -0
- package/dist/{chunk-Q2PAWBPR.js → chunk-76ZOHRBM.js} +2 -2
- package/dist/{chunk-Q2PAWBPR.js.map → chunk-76ZOHRBM.js.map} +1 -1
- package/dist/components/PaymentStatusBadge.d.ts +1 -1
- package/dist/components/PaymentStatusBadge.d.ts.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/{AgendaContext.d.ts → context.d.ts} +1 -1
- package/dist/context.d.ts.map +1 -0
- package/dist/data/registries.d.ts.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +170 -7
- package/dist/index.js.map +1 -1
- package/dist/lib/message-events.d.ts +12 -0
- package/dist/lib/message-events.d.ts.map +1 -0
- package/dist/public/{data.d.ts → data/index.d.ts} +4 -4
- package/dist/public/data/index.d.ts.map +1 -0
- package/dist/public/{data.supabase.d.ts → data/supabase.d.ts} +2 -2
- package/dist/public/data/supabase.d.ts.map +1 -0
- package/dist/public/{hooks.d.ts → hooks/index.d.ts} +4 -4
- package/dist/public/hooks/index.d.ts.map +1 -0
- package/dist/public/index.d.ts +4 -4
- package/dist/public/index.d.ts.map +1 -1
- package/dist/public/index.js +3 -3
- package/dist/public/index.js.map +1 -1
- package/dist/public/{format.d.ts → lib/format.d.ts} +1 -1
- package/dist/public/lib/format.d.ts.map +1 -0
- package/dist/public/views/BookingWidget.d.ts.map +1 -0
- package/dist/public/{steps → views/steps}/ContactStep.d.ts +1 -1
- package/dist/public/views/steps/ContactStep.d.ts.map +1 -0
- package/dist/public/{steps → views/steps}/DateTimeStep.d.ts +3 -3
- package/dist/public/views/steps/DateTimeStep.d.ts.map +1 -0
- package/dist/public/{steps → views/steps}/OverviewStep.d.ts +1 -1
- package/dist/public/views/steps/OverviewStep.d.ts.map +1 -0
- package/dist/public/{steps → views/steps}/PaymentStep.d.ts +1 -1
- package/dist/public/views/steps/PaymentStep.d.ts.map +1 -0
- package/dist/store.d.ts +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/styles/calendar-styles.d.ts.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/{AgendaPage.d.ts → views/AgendaPage.d.ts} +3 -3
- package/dist/views/AgendaPage.d.ts.map +1 -0
- package/package.json +6 -9
- package/dist/AgendaContext.d.ts.map +0 -1
- package/dist/AgendaPage.d.ts.map +0 -1
- package/dist/calendar-styles.d.ts.map +0 -1
- package/dist/financial-bridge.d.ts.map +0 -1
- package/dist/public/BookingWidget.d.ts.map +0 -1
- package/dist/public/data.d.ts.map +0 -1
- package/dist/public/data.supabase.d.ts.map +0 -1
- package/dist/public/format.d.ts.map +0 -1
- package/dist/public/hooks.d.ts.map +0 -1
- package/dist/public/steps/ContactStep.d.ts.map +0 -1
- package/dist/public/steps/DateTimeStep.d.ts.map +0 -1
- package/dist/public/steps/OverviewStep.d.ts.map +0 -1
- package/dist/public/steps/PaymentStep.d.ts.map +0 -1
- package/dist/registries.d.ts.map +0 -1
- package/src/AgendaContext.tsx +0 -11
- package/src/AgendaPage.tsx +0 -33
- package/src/bridges/create-financial-bridge.ts +0 -296
- package/src/calendar-styles.css +0 -245
- package/src/calendar-styles.ts +0 -285
- package/src/components/AgendaGeneralSettings.tsx +0 -86
- package/src/components/AgendaMobileList.tsx +0 -103
- package/src/components/AppointmentModal.tsx +0 -1050
- package/src/components/AppointmentPopover.tsx +0 -231
- package/src/components/BookingPaymentPanel.tsx +0 -394
- package/src/components/CalendarSidebar.tsx +0 -119
- package/src/components/CalendarToolbar.tsx +0 -86
- package/src/components/EventContextMenu.tsx +0 -125
- package/src/components/FloatingPanel.tsx +0 -123
- package/src/components/MiniCalendar.tsx +0 -120
- package/src/components/PaymentStatusBadge.tsx +0 -62
- package/src/components/StatusBadge.tsx +0 -23
- package/src/config.ts +0 -368
- package/src/data/index.ts +0 -4
- package/src/data/mock.ts +0 -344
- package/src/data/supabase.ts +0 -636
- package/src/data/types.ts +0 -37
- package/src/financial-bridge.ts +0 -96
- package/src/hooks/useAgendaSettings.ts +0 -17
- package/src/index.ts +0 -297
- package/src/integrations/google-calendar/GoogleCalendarSettings.tsx +0 -136
- package/src/integrations/google-calendar/components/CalendarColorDot.tsx +0 -23
- package/src/integrations/google-calendar/components/ChannelMappingList.tsx +0 -329
- package/src/integrations/google-calendar/components/ImportEventsWizard.tsx +0 -215
- package/src/integrations/google-calendar/components/useAgendaLookups.ts +0 -100
- package/src/integrations/google-calendar/connector.ts +0 -20
- package/src/integrations/google-calendar/connectorDef.tsx +0 -185
- package/src/integrations/google-calendar/data/supabase.ts +0 -170
- package/src/integrations/google-calendar/data/tables.ts +0 -12
- package/src/integrations/google-calendar/index.ts +0 -45
- package/src/integrations/google-calendar/integration.test.ts +0 -199
- package/src/integrations/google-calendar/mapping.ts +0 -183
- package/src/integrations/google-calendar/migrations/001_google_calendar.sql +0 -97
- package/src/integrations/google-calendar/migrations/002_smart_sync.sql +0 -517
- package/src/integrations/google-calendar/migrations/003_plg_rename.sql +0 -273
- package/src/integrations/google-calendar/types.ts +0 -142
- package/src/lib/tenant.ts +0 -3
- package/src/locales/en.ts +0 -220
- package/src/locales/index.ts +0 -7
- package/src/locales/pt-BR.ts +0 -220
- package/src/migrations/000b_v_bookings_rename.sql +0 -21
- package/src/migrations/000c_dedupe_customer_phones.sql +0 -43
- package/src/migrations/001_public_booking.sql +0 -296
- package/src/migrations/002_v_bookings_compat.sql +0 -14
- package/src/migrations/003_public_client_kind.sql +0 -175
- package/src/migrations/004_public_booking_assignee.sql +0 -176
- package/src/migrations/005_agent_rpcs.sql +0 -168
- package/src/migrations/006_agent_rpc_localtime.sql +0 -181
- package/src/migrations/007_schedule_exception_precedence.sql +0 -290
- package/src/migrations/008_agent_rpc_exception_precedence.sql +0 -172
- package/src/migrations/009_public_booking_location.sql +0 -162
- package/src/migrations/010_public_service_curation.sql +0 -31
- package/src/migrations/011_default_assignee.sql +0 -167
- package/src/migrations/012_drop_legacy_booking_overload.sql +0 -29
- package/src/migrations/013_normalize_v_appointments.sql +0 -199
- package/src/migrations/014_agent_rpc_update_appointment.sql +0 -199
- package/src/migrations/index.ts +0 -2377
- package/src/public/BookingWidget.tsx +0 -324
- package/src/public/components/BrandHeader.tsx +0 -27
- package/src/public/components/ServiceCard.tsx +0 -39
- package/src/public/components/SidebarExtras.tsx +0 -30
- package/src/public/components/StepRailItem.tsx +0 -27
- package/src/public/components/SuccessPanel.tsx +0 -27
- package/src/public/components/SummaryRail.tsx +0 -90
- package/src/public/components/defaults.ts +0 -14
- package/src/public/context.tsx +0 -103
- package/src/public/createPublicBookingPlugin.ts +0 -281
- package/src/public/data.supabase.ts +0 -97
- package/src/public/data.ts +0 -105
- package/src/public/format.ts +0 -96
- package/src/public/hooks.ts +0 -110
- package/src/public/index.ts +0 -69
- package/src/public/steps/ContactStep.tsx +0 -184
- package/src/public/steps/DateTimeStep.tsx +0 -138
- package/src/public/steps/OverviewStep.tsx +0 -81
- package/src/public/steps/PaymentStep.tsx +0 -180
- package/src/public/types.ts +0 -250
- package/src/registries.ts +0 -35
- package/src/store.ts +0 -311
- package/src/types.ts +0 -273
- package/src/views/CalendarView.tsx +0 -1112
- package/src/views/ConfirmationsView.tsx +0 -155
- /package/dist/{financial-bridge.d.ts → bridges/financial-bridge.d.ts} +0 -0
- /package/dist/{registries.d.ts → data/registries.d.ts} +0 -0
- /package/dist/public/{BookingWidget.d.ts → views/BookingWidget.d.ts} +0 -0
- /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
|
-
}
|