@fayz-ai/plugin-agenda 0.1.3 → 0.1.6

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 (34) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.cjs +412 -201
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +254 -48
  7. package/dist/index.js.map +1 -1
  8. package/dist/integrations/google-calendar/GoogleCalendarSettings.d.ts +3 -0
  9. package/dist/integrations/google-calendar/GoogleCalendarSettings.d.ts.map +1 -0
  10. package/dist/integrations/google-calendar/connector.d.ts +3 -0
  11. package/dist/integrations/google-calendar/connector.d.ts.map +1 -0
  12. package/dist/integrations/google-calendar/connectorDef.d.ts +3 -0
  13. package/dist/integrations/google-calendar/connectorDef.d.ts.map +1 -0
  14. package/dist/integrations/google-calendar/data/supabase.d.ts +16 -0
  15. package/dist/integrations/google-calendar/data/supabase.d.ts.map +1 -0
  16. package/dist/integrations/google-calendar/index.d.ts +8 -0
  17. package/dist/integrations/google-calendar/index.d.ts.map +1 -0
  18. package/dist/integrations/google-calendar/integration.test.d.ts +2 -0
  19. package/dist/integrations/google-calendar/integration.test.d.ts.map +1 -0
  20. package/dist/integrations/google-calendar/mapping.d.ts +25 -0
  21. package/dist/integrations/google-calendar/mapping.d.ts.map +1 -0
  22. package/dist/integrations/google-calendar/types.d.ts +36 -0
  23. package/dist/integrations/google-calendar/types.d.ts.map +1 -0
  24. package/package.json +10 -7
  25. package/src/index.ts +12 -2
  26. package/src/integrations/google-calendar/GoogleCalendarSettings.tsx +136 -0
  27. package/src/integrations/google-calendar/connector.ts +20 -0
  28. package/src/integrations/google-calendar/connectorDef.tsx +94 -0
  29. package/src/integrations/google-calendar/data/supabase.ts +74 -0
  30. package/src/integrations/google-calendar/index.ts +36 -0
  31. package/src/integrations/google-calendar/integration.test.ts +74 -0
  32. package/src/integrations/google-calendar/mapping.ts +43 -0
  33. package/src/integrations/google-calendar/migrations/001_google_calendar.sql +94 -0
  34. package/src/integrations/google-calendar/types.ts +31 -0
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ export declare function GoogleCalendarSettings(): React.JSX.Element;
3
+ //# sourceMappingURL=GoogleCalendarSettings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GoogleCalendarSettings.d.ts","sourceRoot":"","sources":["../../../src/integrations/google-calendar/GoogleCalendarSettings.tsx"],"names":[],"mappings":"AAKA,OAAO,KAA8B,MAAM,OAAO,CAAA;AAQlD,wBAAgB,sBAAsB,sBA0HrC"}
@@ -0,0 +1,3 @@
1
+ import type { Connector } from '@fayz-ai/core';
2
+ export declare const googleCalendarConnector: Connector;
3
+ //# sourceMappingURL=connector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connector.d.ts","sourceRoot":"","sources":["../../../src/integrations/google-calendar/connector.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAE9C,eAAO,MAAM,uBAAuB,EAAE,SAYrC,CAAA"}
@@ -0,0 +1,3 @@
1
+ import type { ConnectorDefinition } from '@fayz-ai/core';
2
+ export declare const googleCalendarConnectorDef: ConnectorDefinition;
3
+ //# sourceMappingURL=connectorDef.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connectorDef.d.ts","sourceRoot":"","sources":["../../../src/integrations/google-calendar/connectorDef.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AAsExD,eAAO,MAAM,0BAA0B,EAAE,mBAcxC,CAAA"}
@@ -0,0 +1,16 @@
1
+ import type { CalendarIntegration, CalendarSyncLogEntry } from '../types';
2
+ export interface GoogleCalendarProvider {
3
+ getIntegration(): Promise<CalendarIntegration | null>;
4
+ /** Start the OAuth connect flow — returns the Google consent URL to redirect to. */
5
+ getConnectUrl(redirectTo?: string): Promise<string>;
6
+ setCalendar(calendarId: string): Promise<void>;
7
+ disconnect(): Promise<void>;
8
+ /** Manually pull external changes now (inbound). */
9
+ syncNow(): Promise<{
10
+ fetched: number;
11
+ written: number;
12
+ }>;
13
+ getSyncLog(): Promise<CalendarSyncLogEntry[]>;
14
+ }
15
+ export declare function createGoogleCalendarProvider(): GoogleCalendarProvider;
16
+ //# sourceMappingURL=supabase.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supabase.d.ts","sourceRoot":"","sources":["../../../../src/integrations/google-calendar/data/supabase.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAUzE,MAAM,WAAW,sBAAsB;IACrC,cAAc,IAAI,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAA;IACrD,oFAAoF;IACpF,aAAa,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IACnD,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9C,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3B,oDAAoD;IACpD,OAAO,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACxD,UAAU,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAAA;CAC9C;AAED,wBAAgB,4BAA4B,IAAI,sBAAsB,CA8CrE"}
@@ -0,0 +1,8 @@
1
+ import type { PluginManifest } from '@fayz-ai/core';
2
+ export { createGoogleCalendarProvider } from './data/supabase';
3
+ export { googleCalendarConnector } from './connector';
4
+ export { googleCalendarConnectorDef } from './connectorDef';
5
+ export { bookingToEvent, eventToBookingPatch, bookingSummary } from './mapping';
6
+ export * from './types';
7
+ export declare function createGoogleCalendarPlugin(): PluginManifest;
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/integrations/google-calendar/index.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAGnD,OAAO,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAA;AAC9D,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;AACrD,OAAO,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAA;AAC3D,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAC/E,cAAc,SAAS,CAAA;AAKvB,wBAAgB,0BAA0B,IAAI,cAAc,CAa3D"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=integration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration.test.d.ts","sourceRoot":"","sources":["../../../src/integrations/google-calendar/integration.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,25 @@
1
+ import type { CalendarBooking } from '../../types';
2
+ import type { GCalEvent } from './types';
3
+ export interface GCalEventInput {
4
+ summary: string;
5
+ description?: string;
6
+ start: {
7
+ dateTime: string;
8
+ timeZone?: string;
9
+ };
10
+ end: {
11
+ dateTime: string;
12
+ timeZone?: string;
13
+ };
14
+ }
15
+ /** Build the event title from the booking's services + client. */
16
+ export declare function bookingSummary(b: Pick<CalendarBooking, 'services' | 'clientName'>): string;
17
+ /** Map a Fayz booking to a Google Calendar event input. Returns null when the booking has no end. */
18
+ export declare function bookingToEvent(b: CalendarBooking, timeZone?: string): GCalEventInput | null;
19
+ /** Map an inbound Google event change to a partial booking update (reschedule/cancel). */
20
+ export declare function eventToBookingPatch(ev: GCalEvent): {
21
+ startsAt?: string;
22
+ endsAt?: string;
23
+ status?: 'cancelled';
24
+ };
25
+ //# sourceMappingURL=mapping.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mapping.d.ts","sourceRoot":"","sources":["../../../src/integrations/google-calendar/mapping.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAExC,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC9C,GAAG,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAC7C;AAED,kEAAkE;AAClE,wBAAgB,cAAc,CAAC,CAAC,EAAE,IAAI,CAAC,eAAe,EAAE,UAAU,GAAG,YAAY,CAAC,GAAG,MAAM,CAK1F;AAED,qGAAqG;AACrG,wBAAgB,cAAc,CAAC,CAAC,EAAE,eAAe,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAU3F;AAED,0FAA0F;AAC1F,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,SAAS,GAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,CAM/G"}
@@ -0,0 +1,36 @@
1
+ export interface CalendarIntegration {
2
+ id: string;
3
+ provider: string;
4
+ calendarId: string;
5
+ active: boolean;
6
+ connected: boolean;
7
+ lastSyncAt?: string;
8
+ }
9
+ export interface CalendarSyncLogEntry {
10
+ id: string;
11
+ direction: string;
12
+ trigger?: string;
13
+ status: string;
14
+ fetched: number;
15
+ written: number;
16
+ error?: string;
17
+ createdAt: string;
18
+ }
19
+ /** Minimal Google Calendar event shape we read/write (subset of the API). */
20
+ export interface GCalEvent {
21
+ id: string;
22
+ summary?: string;
23
+ description?: string;
24
+ start: {
25
+ dateTime?: string;
26
+ date?: string;
27
+ timeZone?: string;
28
+ };
29
+ end: {
30
+ dateTime?: string;
31
+ date?: string;
32
+ timeZone?: string;
33
+ };
34
+ status?: string;
35
+ }
36
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/integrations/google-calendar/types.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,OAAO,CAAA;IACf,SAAS,EAAE,OAAO,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,6EAA6E;AAC7E,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC9D,GAAG,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fayz-ai/plugin-agenda",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
4
4
  "description": "Fayz SDK — plugin-agenda plugin",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -33,17 +33,19 @@
33
33
  "@tanstack/react-table": "^8.20.0",
34
34
  "@fullcalendar/resource-timegrid": "^6.1.0",
35
35
  "@fullcalendar/list": "^6.1.0",
36
- "@fayz-ai/core": "^0.1.7",
37
- "@fayz-ai/ui": "^0.1.7",
38
- "@fayz-ai/saas": "^0.1.7",
39
- "@fayz-ai/plugin-financial": "^0.1.2"
36
+ "@fayz-ai/core": "^0.2.0",
37
+ "@fayz-ai/ui": "^0.2.1",
38
+ "@fayz-ai/saas": "^0.3.0",
39
+ "@fayz-ai/plugin-financial": "^0.1.5"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/react": "^18.3.0",
43
43
  "react": "^18.3.0",
44
44
  "tsup": "^8.2.0",
45
45
  "typescript": "^5.5.0",
46
- "@types/react-dom": "^18.3.0"
46
+ "@types/react-dom": "^18.3.0",
47
+ "vitest": "^2.1.9",
48
+ "react-dom": "^18.3.0"
47
49
  },
48
50
  "license": "MIT",
49
51
  "keywords": [
@@ -58,6 +60,7 @@
58
60
  "build": "tsup && tsc --emitDeclarationOnly --declaration --declarationMap --noEmit false",
59
61
  "dev": "tsup --watch",
60
62
  "typecheck": "tsc --noEmit",
61
- "clean": "rm -rf dist"
63
+ "clean": "rm -rf dist",
64
+ "test": "vitest run"
62
65
  }
63
66
  }
package/src/index.ts CHANGED
@@ -11,7 +11,7 @@ import { createAgendaStore } from './store'
11
11
  import { agendaRegistries } from './registries'
12
12
  import { AgendaGeneralSettings } from './components/AgendaGeneralSettings'
13
13
  import { agendaLocales } from './locales'
14
- import { setScheduleBlockConfig, getScheduleBlockConfig } from '@fayz-ai/saas'
14
+ import { setScheduleBlockConfig, getScheduleBlockConfig, PluginSettingsPanel } from '@fayz-ai/saas'
15
15
 
16
16
  // ---------------------------------------------------------------------------
17
17
  // Factory
@@ -159,8 +159,15 @@ export function createAgendaPlugin(options?: AgendaPluginOptions): PluginManifes
159
159
  id: 'agenda',
160
160
  label: 'Agenda',
161
161
  icon: 'Calendar',
162
+ // Standard layout: General | Integrations (Google Calendar addon) | Properties (holidays).
162
163
  component: (() => {
163
- const AgendaSettingsTab: React.FC = () => React.createElement(AgendaGeneralSettings)
164
+ const AgendaSettingsTab: React.FC = () => React.createElement(PluginSettingsPanel, {
165
+ title: 'Agenda',
166
+ generalSettings: React.createElement(AgendaGeneralSettings),
167
+ registries: agendaRegistries,
168
+ hostPluginId: 'agenda',
169
+ routeBase: '/settings/agenda',
170
+ })
164
171
  AgendaSettingsTab.displayName = 'AgendaSettingsTab'
165
172
  return AgendaSettingsTab as unknown as React.ComponentType<unknown>
166
173
  })(),
@@ -178,3 +185,6 @@ export type { ResolvedAgendaConfig } from './config'
178
185
  export type { AgendaDataProvider } from './data/types'
179
186
  export type { AgendaFinancialBridge, BookingPaymentStatus, BookingPaymentSummary, BookingPaymentDetail } from './financial-bridge'
180
187
  export { createFinancialBridge, computeCommissionAmount } from './bridges/create-financial-bridge'
188
+
189
+ // Google Calendar integration (official addon) — settings-only plugin + helpers
190
+ export { createGoogleCalendarPlugin, createGoogleCalendarProvider, googleCalendarConnector, bookingToEvent, eventToBookingPatch } from './integrations/google-calendar'
@@ -0,0 +1,136 @@
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
+ }
@@ -0,0 +1,20 @@
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
+ }
@@ -0,0 +1,94 @@
1
+ // Google Calendar as a unified ConnectorDefinition.
2
+ //
3
+ // Surfaces inside plugin-agenda's settings → Integrations tab (the host),
4
+ // rendered by the shared ConnectorsHub. The hub handles the OAuth connect/
5
+ // disconnect; the ExtraPanel below adds the calendar picker, manual sync and
6
+ // run history (only meaningful once connected).
7
+ import React, { useEffect, useState } from 'react'
8
+ import { RefreshCw, History, CheckCircle2, AlertCircle, Loader2 } from 'lucide-react'
9
+ import { Button, toast } from '@fayz-ai/ui'
10
+ import type { ConnectorDefinition } from '@fayz-ai/core'
11
+ import { createGoogleCalendarProvider } from './data/supabase'
12
+ import type { CalendarSyncLogEntry } from './types'
13
+
14
+ const provider = createGoogleCalendarProvider()
15
+
16
+ function GoogleCalendarExtraPanel() {
17
+ const [connected, setConnected] = useState(false)
18
+ const [calendarId, setCalendarId] = useState('primary')
19
+ const [syncing, setSyncing] = useState(false)
20
+ const [log, setLog] = useState<CalendarSyncLogEntry[]>([])
21
+ const [loaded, setLoaded] = useState(false)
22
+
23
+ async function load() {
24
+ const integ = await provider.getIntegration()
25
+ setConnected(!!integ?.connected)
26
+ if (integ) { setCalendarId(integ.calendarId); void provider.getSyncLog().then(setLog) }
27
+ setLoaded(true)
28
+ }
29
+ useEffect(() => { void load() }, [])
30
+
31
+ // Before connecting there's nothing to configure here.
32
+ if (!loaded || !connected) return null
33
+
34
+ async function handleSaveCalendar() {
35
+ await provider.setCalendar(calendarId)
36
+ toast.success('Saved')
37
+ }
38
+ async function handleSyncNow() {
39
+ setSyncing(true)
40
+ try { const r = await provider.syncNow(); toast.success(`Synced — ${r.written} updated`); void provider.getSyncLog().then(setLog) }
41
+ catch (e: any) { toast.error(e?.message ?? 'Sync failed') }
42
+ finally { setSyncing(false) }
43
+ }
44
+
45
+ return (
46
+ <div className="space-y-4 border-t pt-4">
47
+ <div className="flex flex-wrap items-end gap-3">
48
+ <label className="block flex-1 min-w-[200px]">
49
+ <span className="text-xs font-medium text-muted-foreground">Calendar ID</span>
50
+ <input value={calendarId} onChange={(e) => setCalendarId(e.target.value)} placeholder="primary"
51
+ 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" />
52
+ </label>
53
+ <Button variant="outline" size="sm" onClick={handleSaveCalendar}>Save</Button>
54
+ <Button variant="outline" size="sm" onClick={handleSyncNow} disabled={syncing}>
55
+ <RefreshCw className={`h-3.5 w-3.5 ${syncing ? 'animate-spin' : ''}`} /> Sync now
56
+ </Button>
57
+ </div>
58
+
59
+ {log.length > 0 && (
60
+ <div className="space-y-2">
61
+ <div className="flex items-center gap-2"><History className="h-4 w-4 text-muted-foreground" /><h4 className="text-sm font-semibold">History</h4></div>
62
+ <div className="rounded-md border divide-y text-sm">
63
+ {log.map((r) => (
64
+ <div key={r.id} className="flex items-center gap-3 px-3 py-2">
65
+ <span className="text-muted-foreground text-xs">{new Date(r.createdAt).toLocaleString()}</span>
66
+ <span className="text-[10px] uppercase tracking-wide px-1 py-px rounded bg-muted text-muted-foreground">{r.direction}</span>
67
+ <span className="ml-auto text-xs">{r.written} written / {r.fetched} fetched</span>
68
+ {r.status === 'success'
69
+ ? <CheckCircle2 className="h-3.5 w-3.5 text-success" />
70
+ : <AlertCircle className="h-3.5 w-3.5 text-warning" />}
71
+ </div>
72
+ ))}
73
+ </div>
74
+ </div>
75
+ )}
76
+ </div>
77
+ )
78
+ }
79
+
80
+ export const googleCalendarConnectorDef: ConnectorDefinition = {
81
+ id: 'google-calendar',
82
+ hostPluginId: 'agenda',
83
+ name: 'Google Calendar',
84
+ description: 'Two-way sync between your bookings and Google Calendar.',
85
+ icon: 'Calendar',
86
+ authKind: 'oauth',
87
+ async getStatus() {
88
+ const integ = await provider.getIntegration()
89
+ return { connected: !!integ?.connected }
90
+ },
91
+ startOAuth: (redirectTo) => provider.getConnectUrl(redirectTo),
92
+ disconnect: () => provider.disconnect(),
93
+ ExtraPanel: GoogleCalendarExtraPanel,
94
+ }
@@ -0,0 +1,74 @@
1
+ // Google Calendar integration (control plane) — Supabase client.
2
+ //
3
+ // Thin client: reads/writes the calendar_integrations row and invokes the
4
+ // google-calendar-sync edge function for OAuth connect + manual sync. The
5
+ // function holds the OAuth client secret and does the Calendar API work.
6
+ import { getSupabaseClientOptional } from '@fayz-ai/core'
7
+ import type { CalendarIntegration, CalendarSyncLogEntry } from '../types'
8
+
9
+ const FUNCTION = 'google-calendar-sync'
10
+
11
+ function sb() {
12
+ const supabase = getSupabaseClientOptional() as any
13
+ if (!supabase) throw new Error('Supabase not initialized')
14
+ return supabase
15
+ }
16
+
17
+ export interface GoogleCalendarProvider {
18
+ getIntegration(): Promise<CalendarIntegration | null>
19
+ /** Start the OAuth connect flow — returns the Google consent URL to redirect to. */
20
+ getConnectUrl(redirectTo?: string): Promise<string>
21
+ setCalendar(calendarId: string): Promise<void>
22
+ disconnect(): Promise<void>
23
+ /** Manually pull external changes now (inbound). */
24
+ syncNow(): Promise<{ fetched: number; written: number }>
25
+ getSyncLog(): Promise<CalendarSyncLogEntry[]>
26
+ }
27
+
28
+ export function createGoogleCalendarProvider(): GoogleCalendarProvider {
29
+ return {
30
+ async getIntegration() {
31
+ const { data } = await sb().from('calendar_integrations').select('*').eq('provider', 'google').limit(1).maybeSingle()
32
+ if (!data) return null
33
+ return {
34
+ id: data.id,
35
+ provider: data.provider,
36
+ calendarId: data.calendar_id ?? 'primary',
37
+ active: data.active ?? true,
38
+ connected: !!data.oauth_refresh_token,
39
+ lastSyncAt: data.last_sync_at ?? undefined,
40
+ }
41
+ },
42
+
43
+ async getConnectUrl(redirectTo) {
44
+ const { data, error } = await sb().functions.invoke(FUNCTION, {
45
+ body: { action: 'oauth_start', redirectTo: redirectTo ?? window.location.href },
46
+ })
47
+ if (error) throw new Error(error.message)
48
+ return (data as { url: string }).url
49
+ },
50
+
51
+ async setCalendar(calendarId) {
52
+ await sb().from('calendar_integrations').update({ calendar_id: calendarId, updated_at: new Date().toISOString() }).eq('provider', 'google')
53
+ },
54
+
55
+ async disconnect() {
56
+ await sb().from('calendar_integrations').update({ active: false, oauth_refresh_token: null, oauth_access_token: null }).eq('provider', 'google')
57
+ },
58
+
59
+ async syncNow() {
60
+ const { data, error } = await sb().functions.invoke(FUNCTION, { body: { action: 'pull_events', trigger: 'manual' } })
61
+ if (error) throw new Error(error.message)
62
+ return data as { fetched: number; written: number }
63
+ },
64
+
65
+ async getSyncLog() {
66
+ const { data } = await sb().from('calendar_sync_log').select('*').order('created_at', { ascending: false }).limit(20)
67
+ return (data ?? []).map((r: any) => ({
68
+ id: r.id, direction: r.direction, trigger: r.trigger ?? undefined,
69
+ status: r.status, fetched: r.fetched ?? 0, written: r.written ?? 0,
70
+ error: r.error ?? undefined, createdAt: r.created_at,
71
+ }))
72
+ },
73
+ }
74
+ }
@@ -0,0 +1,36 @@
1
+ // Google Calendar integration — official (in-SDK) addon for plugin-agenda.
2
+ //
3
+ // Settings-only PluginManifest: contributes a "Google Calendar" tab to /settings
4
+ // for connecting an account and running a manual sync. Outbound (booking →
5
+ // Google) is automatic via a DB trigger; inbound (Google → booking) via pg_cron
6
+ // or a Google watch webhook. The data plane is the google-calendar-sync edge
7
+ // function (see ./functions/google-calendar-sync). The booking↔event link lives
8
+ // on saas_core.bookings.metadata.googleCalendarEventId.
9
+ //
10
+ // Graduation path: extract to its own `@fayz-ai/plugin-google-calendar` package.
11
+ import type { PluginManifest } from '@fayz-ai/core'
12
+ import { googleCalendarConnectorDef } from './connectorDef'
13
+
14
+ export { createGoogleCalendarProvider } from './data/supabase'
15
+ export { googleCalendarConnector } from './connector'
16
+ export { googleCalendarConnectorDef } from './connectorDef'
17
+ export { bookingToEvent, eventToBookingPatch, bookingSummary } from './mapping'
18
+ export * from './types'
19
+
20
+ // Addon plugin: contributes the Google Calendar connector into its host
21
+ // (plugin-agenda). It appears in Agenda settings → Integrations — no top-level
22
+ // settings tab of its own. See packages/saas ConnectorsHub.
23
+ export function createGoogleCalendarPlugin(): PluginManifest {
24
+ return {
25
+ id: 'google-calendar',
26
+ name: 'Google Calendar',
27
+ icon: 'Calendar',
28
+ version: '0.1.0',
29
+ scope: 'addon',
30
+ defaultEnabled: true,
31
+ dependencies: ['agenda'],
32
+ navigation: [],
33
+ routes: [],
34
+ connectors: [googleCalendarConnectorDef],
35
+ }
36
+ }