@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
package/src/financial-bridge.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// Agenda × Financial Bridge — Cross-plugin integration types
|
|
3
|
-
// Pure types only. Runtime implementation in bridges/create-financial-bridge.ts
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
|
|
6
|
-
/** Payment status for a booking's linked invoice */
|
|
7
|
-
export type BookingPaymentStatus = 'none' | 'pending' | 'partial' | 'paid' | 'overdue' | 'cancelled'
|
|
8
|
-
|
|
9
|
-
/** Compact payment summary for calendar enrichment */
|
|
10
|
-
export interface BookingPaymentSummary {
|
|
11
|
-
invoiceId: string
|
|
12
|
-
referenceNumber?: string
|
|
13
|
-
status: BookingPaymentStatus
|
|
14
|
-
totalAmount: number
|
|
15
|
-
paidAmount: number
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Detailed payment info with installment breakdown */
|
|
19
|
-
export interface BookingPaymentDetail extends BookingPaymentSummary {
|
|
20
|
-
movements: Array<{
|
|
21
|
-
id: string
|
|
22
|
-
amount: number
|
|
23
|
-
paidAmount: number
|
|
24
|
-
status: string
|
|
25
|
-
dueDate: string
|
|
26
|
-
installmentNumber?: number
|
|
27
|
-
paymentDate?: string
|
|
28
|
-
paymentMethodTypeName?: string
|
|
29
|
-
cardBrand?: string
|
|
30
|
-
cardInstallments?: number
|
|
31
|
-
}>
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Bridge interface for agenda → financial plugin integration.
|
|
36
|
-
* Injected via `AgendaPluginOptions.financialBridge`.
|
|
37
|
-
* Follows the same DI pattern as `EntityLookup`.
|
|
38
|
-
*/
|
|
39
|
-
export interface AgendaFinancialBridge {
|
|
40
|
-
/** Batch-resolve payment status for multiple order IDs (for calendar enrichment) */
|
|
41
|
-
resolvePaymentStatuses(orderIds: string[]): Promise<Map<string, BookingPaymentSummary>>
|
|
42
|
-
|
|
43
|
-
/** Get detailed payment info for a single booking's order */
|
|
44
|
-
getPaymentDetail(orderId: string): Promise<BookingPaymentDetail | null>
|
|
45
|
-
|
|
46
|
-
/** Create a receivable invoice from the booking's service_order. Returns invoice ID */
|
|
47
|
-
createInvoiceFromOrder(orderId: string, options?: {
|
|
48
|
-
installments?: number
|
|
49
|
-
dueDate?: string
|
|
50
|
-
paymentMethodTypeId?: string
|
|
51
|
-
}): Promise<string>
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Accrue the assigned professional's commission for a booking's order as a
|
|
55
|
-
* payable `financial_movements` row (direction='debit', movement_kind='commission').
|
|
56
|
-
* Commission = order.total × professional commission_rate%. Resolves the rate
|
|
57
|
-
* from the `v_staff` bridge view via the order's `assignee_id`.
|
|
58
|
-
*
|
|
59
|
-
* Idempotent per order: returns the existing commission movement id if one was
|
|
60
|
-
* already accrued. Returns null when there is no professional, no configured
|
|
61
|
-
* rate, a zero base, or Supabase is unavailable (nothing to accrue).
|
|
62
|
-
*/
|
|
63
|
-
createCommissionMovement?(orderId: string, options?: {
|
|
64
|
-
dueDate?: string
|
|
65
|
-
}): Promise<string | null>
|
|
66
|
-
|
|
67
|
-
/** Record a payment against a movement (installment) */
|
|
68
|
-
payMovement(input: {
|
|
69
|
-
movementId: string
|
|
70
|
-
amount: number
|
|
71
|
-
paymentDate: string
|
|
72
|
-
paymentMethodTypeId?: string
|
|
73
|
-
paymentMethodId?: string
|
|
74
|
-
bankAccountId?: string
|
|
75
|
-
cardBrand?: string
|
|
76
|
-
cardInstallments?: number
|
|
77
|
-
}): Promise<void>
|
|
78
|
-
|
|
79
|
-
/** One-step POS checkout: create invoice + pay in full. Returns paid detail. */
|
|
80
|
-
checkout(orderId: string, input: {
|
|
81
|
-
paymentMethodTypeId: string
|
|
82
|
-
paymentMethodId?: string
|
|
83
|
-
bankAccountId?: string
|
|
84
|
-
cardBrand?: string
|
|
85
|
-
cardInstallments?: number
|
|
86
|
-
}): Promise<BookingPaymentDetail>
|
|
87
|
-
|
|
88
|
-
/** Get payment methods filtered by type */
|
|
89
|
-
getPaymentMethods?(paymentMethodTypeId: string): Promise<Array<{ id: string; name: string }>>
|
|
90
|
-
|
|
91
|
-
/** Get payment method types for the payment UI */
|
|
92
|
-
getPaymentMethodTypes(): Promise<Array<{ id: string; name: string; transactionType?: string }>>
|
|
93
|
-
|
|
94
|
-
/** Get bank accounts for payment destination */
|
|
95
|
-
getBankAccounts(): Promise<Array<{ id: string; name: string; accountType: string }>>
|
|
96
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { usePluginPrefs } from '@fayz-ai/saas'
|
|
2
|
-
|
|
3
|
-
const DEFAULTS = {
|
|
4
|
-
startTime: '07:00',
|
|
5
|
-
endTime: '21:00',
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function useAgendaSettings() {
|
|
9
|
-
const prefs = usePluginPrefs('agenda', DEFAULTS)
|
|
10
|
-
|
|
11
|
-
return {
|
|
12
|
-
startTime: prefs.get('startTime'),
|
|
13
|
-
endTime: prefs.get('endTime'),
|
|
14
|
-
setStartTime: (v: string) => prefs.set('startTime', v),
|
|
15
|
-
setEndTime: (v: string) => prefs.set('endTime', v),
|
|
16
|
-
}
|
|
17
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import type { PluginManifest } from '@fayz-ai/core'
|
|
3
|
-
import { createSafeDataProvider, registerTranslations } from '@fayz-ai/core'
|
|
4
|
-
import type { AgendaPluginOptions } from './config'
|
|
5
|
-
import { resolveConfig } from './config'
|
|
6
|
-
import { AgendaPage } from './AgendaPage'
|
|
7
|
-
import { createSupabaseAgendaProvider } from './data/supabase'
|
|
8
|
-
import { createMockAgendaProvider } from './data/mock'
|
|
9
|
-
import { setAgendaTenantId } from './lib/tenant'
|
|
10
|
-
import { createAgendaStore } from './store'
|
|
11
|
-
import { agendaRegistries } from './registries'
|
|
12
|
-
import { AgendaGeneralSettings } from './components/AgendaGeneralSettings'
|
|
13
|
-
import { agendaLocales } from './locales'
|
|
14
|
-
import { MIGRATION_001_PUBLIC_BOOKING } from './migrations'
|
|
15
|
-
import { setScheduleBlockConfig, getScheduleBlockConfig, PluginSettingsPanel } from '@fayz-ai/saas'
|
|
16
|
-
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// Factory
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
export function createAgendaPlugin(options?: AgendaPluginOptions): PluginManifest {
|
|
22
|
-
const config = resolveConfig(options)
|
|
23
|
-
registerTranslations(agendaLocales)
|
|
24
|
-
const provider = options?.dataProvider ?? createSafeDataProvider(
|
|
25
|
-
() => createSupabaseAgendaProvider({ professionalKind: config.professionalKind }),
|
|
26
|
-
() => createMockAgendaProvider(),
|
|
27
|
-
)
|
|
28
|
-
const store = createAgendaStore(provider, options?.financialBridge)
|
|
29
|
-
const registries = [...agendaRegistries, ...(options?.settingsRegistries ?? [])]
|
|
30
|
-
|
|
31
|
-
// Register schedule block config globally so ScheduleEditor can read it
|
|
32
|
-
setScheduleBlockConfig({
|
|
33
|
-
defaults: config.scheduleBlockDefaults,
|
|
34
|
-
showServices: !!config.serviceLookup,
|
|
35
|
-
showConcurrent: true,
|
|
36
|
-
showBookingWindow: true,
|
|
37
|
-
showLocations: config.modules.locationSelection,
|
|
38
|
-
locations: config.locations.map((l) => ({ id: l.id, name: l.name })),
|
|
39
|
-
// Lazy fetcher — called by ScheduleEditor when locations are needed but empty
|
|
40
|
-
fetchLocations: config.modules.locationSelection && provider.getLocations
|
|
41
|
-
? async () => {
|
|
42
|
-
const locations = await provider.getLocations!()
|
|
43
|
-
const current = getScheduleBlockConfig()
|
|
44
|
-
if (current) setScheduleBlockConfig({ ...current, locations })
|
|
45
|
-
return locations
|
|
46
|
-
}
|
|
47
|
-
: undefined,
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
const PageComponent: React.FC<any> = () =>
|
|
51
|
-
React.createElement(AgendaPage, { config, provider, store })
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
id: 'agenda',
|
|
55
|
-
name: config.labels.pageTitle,
|
|
56
|
-
icon: 'Calendar',
|
|
57
|
-
version: '1.0.0',
|
|
58
|
-
scope: options?.scope ?? 'universal',
|
|
59
|
-
verticalId: options?.verticalId,
|
|
60
|
-
// Booking/scheduling is business-ops (clinic, salon, services) — targets
|
|
61
|
-
// the saas/admin Panel world, not the ecommerce storefront.
|
|
62
|
-
scaffolds: ['saas'],
|
|
63
|
-
defaultEnabled: true,
|
|
64
|
-
dependencies: [],
|
|
65
|
-
declaredFeatures: [
|
|
66
|
-
{ id: 'appointments', label: config.labels.pageTitle, group: config.labels.pageTitle },
|
|
67
|
-
{ id: 'agenda.schedules', label: 'Schedule Management', group: config.labels.pageTitle },
|
|
68
|
-
],
|
|
69
|
-
// Recurring monthly quota — counts appointment rows created this month.
|
|
70
|
-
// (Clients/patients are the app's CRUD entity, so their cap is declared by
|
|
71
|
-
// the app's EntityDef.limitKey, not here.)
|
|
72
|
-
declaredLimits: [
|
|
73
|
-
{ key: 'bookings_month', label: 'Appointments this month', table: 'appointments', period: 'month' },
|
|
74
|
-
],
|
|
75
|
-
queryEntities: [
|
|
76
|
-
{
|
|
77
|
-
key: 'agenda:appointments',
|
|
78
|
-
entity: {
|
|
79
|
-
name: 'Appointment',
|
|
80
|
-
namePlural: 'Appointments',
|
|
81
|
-
icon: 'Calendar',
|
|
82
|
-
permission: { feature: 'appointments', action: 'read' },
|
|
83
|
-
// Keys are the PROVIDER's output shape (camelCase) — what filters/
|
|
84
|
-
// dateField/groupBy match against, not the raw DB columns.
|
|
85
|
-
fields: [
|
|
86
|
-
{ key: 'startsAt', label: 'Starts at', type: 'text' },
|
|
87
|
-
{ key: 'endsAt', label: 'Ends at', type: 'text' },
|
|
88
|
-
{ key: 'status', label: 'Status', type: 'text' },
|
|
89
|
-
{ key: 'clientName', label: 'Client', type: 'text', searchable: true },
|
|
90
|
-
{ key: 'professionalName', label: 'Professional', type: 'text', searchable: true },
|
|
91
|
-
{ key: 'orderTotal', label: 'Order total', type: 'number' },
|
|
92
|
-
{ key: 'totalDurationMinutes', label: 'Duration (min)', type: 'number' },
|
|
93
|
-
{ key: 'createdAt', label: 'Created at', type: 'text' },
|
|
94
|
-
],
|
|
95
|
-
data: {
|
|
96
|
-
table: 'v_appointments',
|
|
97
|
-
tenantScoped: true,
|
|
98
|
-
searchColumns: ['client_name', 'professional_name'],
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
],
|
|
103
|
-
declaredRpcs: [
|
|
104
|
-
{
|
|
105
|
-
name: 'agent_agenda_create_appointment',
|
|
106
|
-
kind: 'write' as const,
|
|
107
|
-
description:
|
|
108
|
-
'Guarded appointment create: agent_guard (role→plan→bookings_month cap), working hours, race-safe conflict check, audited.',
|
|
109
|
-
audits: true,
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
name: 'agent_agenda_update_appointment',
|
|
113
|
-
kind: 'write' as const,
|
|
114
|
-
description:
|
|
115
|
-
'Guarded appointment reschedule/reassign/cancel: agent_guard (role→plan, no cap), working hours, race-safe conflict check that skips the appointment itself, audited.',
|
|
116
|
-
audits: true,
|
|
117
|
-
},
|
|
118
|
-
],
|
|
119
|
-
|
|
120
|
-
navigation: [
|
|
121
|
-
{
|
|
122
|
-
section: options?.navSection ?? 'main',
|
|
123
|
-
position: options?.navPosition ?? 2,
|
|
124
|
-
label: config.labels.pageTitle,
|
|
125
|
-
route: '/agenda',
|
|
126
|
-
icon: 'Calendar',
|
|
127
|
-
permission: { feature: 'appointments', action: 'read' as const },
|
|
128
|
-
},
|
|
129
|
-
],
|
|
130
|
-
|
|
131
|
-
routes: [
|
|
132
|
-
{
|
|
133
|
-
path: '/agenda',
|
|
134
|
-
component: PageComponent,
|
|
135
|
-
permission: { feature: 'appointments', action: 'read' as const },
|
|
136
|
-
// Matches the ref agent_agenda_create_appointment returns.
|
|
137
|
-
entityRoutes: ['schedule:appointment'],
|
|
138
|
-
},
|
|
139
|
-
],
|
|
140
|
-
|
|
141
|
-
widgets: [],
|
|
142
|
-
|
|
143
|
-
aiTools: [
|
|
144
|
-
{
|
|
145
|
-
id: 'agenda.list-appointments',
|
|
146
|
-
name: 'listAppointments',
|
|
147
|
-
description: 'Lists upcoming appointments for a given date or date range, optionally filtered by professional.',
|
|
148
|
-
icon: 'Calendar',
|
|
149
|
-
mode: 'read' as const,
|
|
150
|
-
category: 'Scheduling',
|
|
151
|
-
parameters: {
|
|
152
|
-
type: 'object' as const,
|
|
153
|
-
properties: {
|
|
154
|
-
date: { type: 'string' as const, description: 'Date (YYYY-MM-DD) or "today", "tomorrow", "this week"' },
|
|
155
|
-
professional: { type: 'string' as const, description: 'Professional name to filter by' },
|
|
156
|
-
},
|
|
157
|
-
},
|
|
158
|
-
suggestions: [
|
|
159
|
-
{ label: "What's on the agenda today?" },
|
|
160
|
-
{ label: 'Show me tomorrow\'s appointments' },
|
|
161
|
-
{ label: 'Who has the most bookings this week?' },
|
|
162
|
-
],
|
|
163
|
-
permission: { feature: 'appointments', action: 'read' as const },
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
id: 'agenda.create-appointment',
|
|
167
|
-
name: 'createAppointment',
|
|
168
|
-
description:
|
|
169
|
-
'Creates a new appointment. Takes IDS, not names: resolve the client/service/professional ids first via findAnything/searchRecords, then call this with an ISO start time in the tenant timezone. The user confirms before booking.',
|
|
170
|
-
icon: 'CalendarPlus',
|
|
171
|
-
mode: 'persist' as const,
|
|
172
|
-
limitKey: 'bookings_month',
|
|
173
|
-
execution: { plane: 'server' as const, kind: 'rpc' as const, rpc: 'agent_agenda_create_appointment' },
|
|
174
|
-
category: 'Scheduling',
|
|
175
|
-
parameters: {
|
|
176
|
-
type: 'object' as const,
|
|
177
|
-
properties: {
|
|
178
|
-
client_id: { type: 'string' as const, description: 'Client id (from a search)' },
|
|
179
|
-
service_id: { type: 'string' as const, description: 'Service id (from a search)' },
|
|
180
|
-
professional_id: { type: 'string' as const, description: 'Professional id (optional — auto-assigned when omitted)' },
|
|
181
|
-
starts_at: { type: 'string' as const, description: "LOCAL date-time of the business, NO timezone suffix — e.g. '2026-07-27T10:00' for 10h. The system applies the tenant timezone." },
|
|
182
|
-
notes: { type: 'string' as const, description: 'Optional notes' },
|
|
183
|
-
},
|
|
184
|
-
required: ['client_id', 'service_id', 'starts_at'],
|
|
185
|
-
},
|
|
186
|
-
suggestions: [
|
|
187
|
-
{ label: 'Book a haircut for Sarah tomorrow at 10am' },
|
|
188
|
-
],
|
|
189
|
-
permission: { feature: 'appointments', action: 'create' as const },
|
|
190
|
-
},
|
|
191
|
-
{
|
|
192
|
-
id: 'agenda.update-appointment',
|
|
193
|
-
name: 'updateAppointment',
|
|
194
|
-
description:
|
|
195
|
-
'Changes an appointment that ALREADY EXISTS: reschedule (starts_at), reassign (professional_id), cancel or confirm (status), or annotate (notes). ' +
|
|
196
|
-
'This is the ONLY way to change an appointment — the appointments entity is a read-only view, so updateRecord cannot touch it. ' +
|
|
197
|
-
'NEVER answer a reschedule or a cancellation by creating a new appointment: that leaves the original standing and double-books the professional. ' +
|
|
198
|
-
'Takes the appointment id from a previous search. The duration is preserved automatically. The user confirms before anything changes.',
|
|
199
|
-
icon: 'CalendarClock',
|
|
200
|
-
mode: 'persist' as const,
|
|
201
|
-
execution: { plane: 'server' as const, kind: 'rpc' as const, rpc: 'agent_agenda_update_appointment' },
|
|
202
|
-
category: 'Scheduling',
|
|
203
|
-
parameters: {
|
|
204
|
-
type: 'object' as const,
|
|
205
|
-
properties: {
|
|
206
|
-
appointment_id: { type: 'string' as const, description: 'Id of the appointment to change (from a search)' },
|
|
207
|
-
starts_at: { type: 'string' as const, description: "New LOCAL date-time of the business, NO timezone suffix — e.g. '2026-07-27T16:30'. Omit to keep the current time." },
|
|
208
|
-
professional_id: { type: 'string' as const, description: 'Move to another professional (optional)' },
|
|
209
|
-
status: {
|
|
210
|
-
type: 'string' as const,
|
|
211
|
-
description: 'New status — use "cancelled" to cancel',
|
|
212
|
-
enum: ['scheduled', 'confirmed', 'in_progress', 'completed', 'cancelled', 'no_show'],
|
|
213
|
-
},
|
|
214
|
-
notes: { type: 'string' as const, description: 'Optional notes' },
|
|
215
|
-
},
|
|
216
|
-
required: ['appointment_id'],
|
|
217
|
-
},
|
|
218
|
-
suggestions: [
|
|
219
|
-
{ label: 'Remarca o Gustavo para 16:30' },
|
|
220
|
-
{ label: 'Cancela o atendimento das 10h' },
|
|
221
|
-
],
|
|
222
|
-
permission: { feature: 'appointments', action: 'edit' as const },
|
|
223
|
-
},
|
|
224
|
-
{
|
|
225
|
-
id: 'agenda.check-availability',
|
|
226
|
-
name: 'checkAvailability',
|
|
227
|
-
description: 'Checks available time slots for a professional on a given date.',
|
|
228
|
-
icon: 'Clock',
|
|
229
|
-
mode: 'read' as const,
|
|
230
|
-
category: 'Scheduling',
|
|
231
|
-
parameters: {
|
|
232
|
-
type: 'object' as const,
|
|
233
|
-
properties: {
|
|
234
|
-
professional: { type: 'string' as const, description: 'Professional name' },
|
|
235
|
-
date: { type: 'string' as const, description: 'Date (YYYY-MM-DD) or "today", "tomorrow"' },
|
|
236
|
-
duration: { type: 'number' as const, description: 'Required duration in minutes' },
|
|
237
|
-
},
|
|
238
|
-
required: ['professional', 'date'],
|
|
239
|
-
},
|
|
240
|
-
suggestions: [
|
|
241
|
-
{ label: 'When is Ana available tomorrow?' },
|
|
242
|
-
{ label: 'Find a 90-minute slot for Carlos this week' },
|
|
243
|
-
],
|
|
244
|
-
permission: { feature: 'appointments', action: 'read' as const },
|
|
245
|
-
},
|
|
246
|
-
],
|
|
247
|
-
|
|
248
|
-
registries,
|
|
249
|
-
|
|
250
|
-
settings: [
|
|
251
|
-
{
|
|
252
|
-
id: 'agenda',
|
|
253
|
-
label: 'Agenda',
|
|
254
|
-
icon: 'Calendar',
|
|
255
|
-
// Standard layout: General | Integrations (Google Calendar addon) | Properties.
|
|
256
|
-
component: (() => {
|
|
257
|
-
const AgendaSettingsTab: React.FC = () => React.createElement(PluginSettingsPanel, {
|
|
258
|
-
title: 'Agenda',
|
|
259
|
-
generalSettings: React.createElement(AgendaGeneralSettings),
|
|
260
|
-
registries,
|
|
261
|
-
hostPluginId: 'agenda',
|
|
262
|
-
routeBase: '/settings/agenda',
|
|
263
|
-
})
|
|
264
|
-
AgendaSettingsTab.displayName = 'AgendaSettingsTab'
|
|
265
|
-
return AgendaSettingsTab as unknown as React.ComponentType<unknown>
|
|
266
|
-
})(),
|
|
267
|
-
order: 5,
|
|
268
|
-
permission: { feature: 'appointments', action: 'read' as const },
|
|
269
|
-
},
|
|
270
|
-
],
|
|
271
|
-
migrations: [
|
|
272
|
-
{
|
|
273
|
-
id: 'agenda-001-public-booking',
|
|
274
|
-
version: '0.3.0',
|
|
275
|
-
sql: MIGRATION_001_PUBLIC_BOOKING,
|
|
276
|
-
description:
|
|
277
|
-
'Booking read model (v_appointments) + anon public booking surface (v_public_services, get_available_slots, create_public_booking) + order_items duration/assignee columns',
|
|
278
|
-
},
|
|
279
|
-
],
|
|
280
|
-
|
|
281
|
-
locales: agendaLocales,
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Re-export types for consumers
|
|
286
|
-
export type { AgendaPluginOptions } from './config'
|
|
287
|
-
export type { ResolvedAgendaConfig } from './config'
|
|
288
|
-
export type { AgendaDataProvider } from './data/types'
|
|
289
|
-
// Seedable in-memory provider — apps pass a seed to render the calendar with
|
|
290
|
-
// app-specific events instead of the built-in salon sample.
|
|
291
|
-
export { createMockAgendaProvider } from './data/mock'
|
|
292
|
-
export type { MockAgendaSeed, MockAgendaProviderOptions } from './data/mock'
|
|
293
|
-
export type { AgendaFinancialBridge, BookingPaymentStatus, BookingPaymentSummary, BookingPaymentDetail } from './financial-bridge'
|
|
294
|
-
export { createFinancialBridge, computeCommissionAmount } from './bridges/create-financial-bridge'
|
|
295
|
-
|
|
296
|
-
// Google Calendar integration (official addon) — settings-only plugin + helpers
|
|
297
|
-
export { createGoogleCalendarPlugin, createGoogleCalendarProvider, googleCalendarConnector, bookingToEvent, eventToBookingPatch } from './integrations/google-calendar'
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
// Google Calendar integration — settings tab (control plane UI).
|
|
2
|
-
//
|
|
3
|
-
// Connect/disconnect, pick the target calendar, run a manual inbound sync, and
|
|
4
|
-
// see recent runs. Outbound (booking → Google) is automatic via the DB trigger;
|
|
5
|
-
// this screen is the only UI surface the integration needs.
|
|
6
|
-
import React, { useEffect, useState } from 'react'
|
|
7
|
-
import { Calendar, Plug, Power, RefreshCw, CheckCircle2, Loader2, History, AlertCircle } from 'lucide-react'
|
|
8
|
-
import { Button, toast } from '@fayz-ai/ui'
|
|
9
|
-
import { createGoogleCalendarProvider } from './data/supabase'
|
|
10
|
-
import type { CalendarIntegration, CalendarSyncLogEntry } from './types'
|
|
11
|
-
|
|
12
|
-
const provider = createGoogleCalendarProvider()
|
|
13
|
-
|
|
14
|
-
export function GoogleCalendarSettings() {
|
|
15
|
-
const [integration, setIntegration] = useState<CalendarIntegration | null>(null)
|
|
16
|
-
const [loading, setLoading] = useState(true)
|
|
17
|
-
const [calendarId, setCalendarId] = useState('primary')
|
|
18
|
-
const [connecting, setConnecting] = useState(false)
|
|
19
|
-
const [syncing, setSyncing] = useState(false)
|
|
20
|
-
const [log, setLog] = useState<CalendarSyncLogEntry[]>([])
|
|
21
|
-
|
|
22
|
-
async function load() {
|
|
23
|
-
setLoading(true)
|
|
24
|
-
try {
|
|
25
|
-
const found = await provider.getIntegration()
|
|
26
|
-
setIntegration(found)
|
|
27
|
-
if (found) { setCalendarId(found.calendarId); void provider.getSyncLog().then(setLog) }
|
|
28
|
-
} finally { setLoading(false) }
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
useEffect(() => { void load() }, [])
|
|
32
|
-
|
|
33
|
-
async function handleConnect() {
|
|
34
|
-
setConnecting(true)
|
|
35
|
-
try {
|
|
36
|
-
const url = await provider.getConnectUrl()
|
|
37
|
-
window.location.href = url // → Google consent, then back to the app
|
|
38
|
-
} catch (e: any) {
|
|
39
|
-
toast.error(e?.message ?? 'Connect failed')
|
|
40
|
-
setConnecting(false)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function handleDisconnect() {
|
|
45
|
-
await provider.disconnect()
|
|
46
|
-
toast.success('Disconnected')
|
|
47
|
-
void load()
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function handleSaveCalendar() {
|
|
51
|
-
await provider.setCalendar(calendarId)
|
|
52
|
-
toast.success('Saved')
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function handleSyncNow() {
|
|
56
|
-
setSyncing(true)
|
|
57
|
-
try {
|
|
58
|
-
const res = await provider.syncNow()
|
|
59
|
-
toast.success(`Synced — ${res.written} updated`)
|
|
60
|
-
void provider.getSyncLog().then(setLog)
|
|
61
|
-
void load()
|
|
62
|
-
} catch (e: any) {
|
|
63
|
-
toast.error(e?.message ?? 'Sync failed')
|
|
64
|
-
} finally { setSyncing(false) }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const connected = !!integration?.connected
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<div className="space-y-6 max-w-3xl">
|
|
71
|
-
<div className="flex items-center gap-2">
|
|
72
|
-
<Calendar className="h-5 w-5 text-muted-foreground" />
|
|
73
|
-
<div>
|
|
74
|
-
<h2 className="text-lg font-semibold tracking-tight">Google Calendar</h2>
|
|
75
|
-
<p className="text-sm text-muted-foreground">Two-way sync between your bookings and Google Calendar.</p>
|
|
76
|
-
</div>
|
|
77
|
-
{connected && (
|
|
78
|
-
<span className="ml-auto inline-flex items-center gap-1 text-xs text-success">
|
|
79
|
-
<CheckCircle2 className="h-3.5 w-3.5" /> Connected
|
|
80
|
-
</span>
|
|
81
|
-
)}
|
|
82
|
-
</div>
|
|
83
|
-
|
|
84
|
-
<section className="rounded-lg border bg-card shadow-sm p-5 space-y-4">
|
|
85
|
-
{loading ? (
|
|
86
|
-
<div className="py-6 text-center text-sm text-muted-foreground"><Loader2 className="h-4 w-4 animate-spin inline" /></div>
|
|
87
|
-
) : !connected ? (
|
|
88
|
-
<div className="text-center py-6">
|
|
89
|
-
<p className="text-sm text-muted-foreground mb-3">Connect a Google account to sync bookings both ways.</p>
|
|
90
|
-
<Button onClick={handleConnect} disabled={connecting}>
|
|
91
|
-
{connecting ? <Loader2 className="h-4 w-4 animate-spin" /> : <Plug className="h-4 w-4" />} Connect Google Calendar
|
|
92
|
-
</Button>
|
|
93
|
-
</div>
|
|
94
|
-
) : (
|
|
95
|
-
<>
|
|
96
|
-
<div className="flex flex-wrap items-end gap-3">
|
|
97
|
-
<label className="block flex-1 min-w-[200px]">
|
|
98
|
-
<span className="text-xs font-medium text-muted-foreground">Calendar ID</span>
|
|
99
|
-
<input value={calendarId} onChange={(e) => setCalendarId(e.target.value)} placeholder="primary"
|
|
100
|
-
className="w-full mt-1 rounded-input border border-input bg-card px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" />
|
|
101
|
-
</label>
|
|
102
|
-
<Button variant="outline" size="sm" onClick={handleSaveCalendar}>Save</Button>
|
|
103
|
-
<Button variant="outline" size="sm" onClick={handleSyncNow} disabled={syncing}>
|
|
104
|
-
<RefreshCw className={`h-3.5 w-3.5 ${syncing ? 'animate-spin' : ''}`} /> Sync now
|
|
105
|
-
</Button>
|
|
106
|
-
<Button variant="outline" size="sm" onClick={handleDisconnect}>
|
|
107
|
-
<Power className="h-3.5 w-3.5" /> Disconnect
|
|
108
|
-
</Button>
|
|
109
|
-
</div>
|
|
110
|
-
{integration?.lastSyncAt && (
|
|
111
|
-
<p className="text-[11px] text-muted-foreground">Last sync: {new Date(integration.lastSyncAt).toLocaleString()}</p>
|
|
112
|
-
)}
|
|
113
|
-
</>
|
|
114
|
-
)}
|
|
115
|
-
</section>
|
|
116
|
-
|
|
117
|
-
{log.length > 0 && (
|
|
118
|
-
<section className="rounded-lg border bg-card shadow-sm p-5 space-y-2">
|
|
119
|
-
<div className="flex items-center gap-2"><History className="h-4 w-4 text-muted-foreground" /><h3 className="text-sm font-semibold">History</h3></div>
|
|
120
|
-
<div className="rounded-md border divide-y text-sm">
|
|
121
|
-
{log.map((r) => (
|
|
122
|
-
<div key={r.id} className="flex items-center gap-3 px-3 py-2">
|
|
123
|
-
<span className="text-muted-foreground text-xs">{new Date(r.createdAt).toLocaleString()}</span>
|
|
124
|
-
<span className="text-[10px] uppercase tracking-wide px-1 py-px rounded bg-muted text-muted-foreground">{r.direction}</span>
|
|
125
|
-
<span className="ml-auto text-xs">{r.written} written / {r.fetched} fetched</span>
|
|
126
|
-
{r.status === 'success'
|
|
127
|
-
? <CheckCircle2 className="h-3.5 w-3.5 text-success" />
|
|
128
|
-
: <AlertCircle className="h-3.5 w-3.5 text-warning" />}
|
|
129
|
-
</div>
|
|
130
|
-
))}
|
|
131
|
-
</div>
|
|
132
|
-
</section>
|
|
133
|
-
)}
|
|
134
|
-
</div>
|
|
135
|
-
)
|
|
136
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
// A small round swatch tinted with a Google calendar's backgroundColor.
|
|
2
|
-
//
|
|
3
|
-
// Google's calendarList entries carry a `backgroundColor` hex; we render it as a
|
|
4
|
-
// dot next to the calendar summary in the de-para list and as the leading dot of
|
|
5
|
-
// the origin badge in the import wizard. Falls back to the muted border when a
|
|
6
|
-
// calendar has no color (rare, e.g. some shared calendars).
|
|
7
|
-
import React from 'react'
|
|
8
|
-
|
|
9
|
-
export function CalendarColorDot({
|
|
10
|
-
color,
|
|
11
|
-
className = '',
|
|
12
|
-
}: {
|
|
13
|
-
color?: string | null
|
|
14
|
-
className?: string
|
|
15
|
-
}) {
|
|
16
|
-
return (
|
|
17
|
-
<span
|
|
18
|
-
className={`inline-block h-2.5 w-2.5 shrink-0 rounded-full border border-black/10 ${className}`}
|
|
19
|
-
style={{ backgroundColor: color || 'hsl(var(--muted-foreground))' }}
|
|
20
|
-
aria-hidden
|
|
21
|
-
/>
|
|
22
|
-
)
|
|
23
|
-
}
|