@fayz-ai/plugin-agenda 0.3.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/bridges/create-financial-bridge.d.ts.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/index.cjs +77 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +77 -65
- package/dist/index.js.map +1 -1
- package/dist/migrations/index.d.ts +8 -1
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/public/createPublicBookingPlugin.d.ts.map +1 -1
- package/dist/public/index.cjs +16 -16
- package/dist/public/index.cjs.map +1 -1
- package/dist/public/index.js +17 -17
- package/dist/public/index.js.map +1 -1
- package/dist/registries.d.ts.map +1 -1
- package/dist/types.d.ts +3 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +20 -14
- package/src/bridges/create-financial-bridge.ts +6 -8
- package/src/components/AppointmentModal.tsx +4 -4
- package/src/config.ts +2 -2
- package/src/data/supabase.ts +26 -25
- package/src/index.ts +1 -1
- package/src/integrations/google-calendar/index.ts +1 -1
- package/src/integrations/google-calendar/migrations/001_google_calendar.sql +6 -6
- package/src/migrations/000b_v_bookings_rename.sql +21 -0
- package/src/migrations/000c_dedupe_customer_phones.sql +43 -0
- package/src/migrations/001_public_booking.sql +40 -27
- package/src/migrations/002_v_bookings_compat.sql +14 -0
- package/src/migrations/index.ts +134 -29
- package/src/public/createPublicBookingPlugin.ts +23 -20
- package/src/public/data.supabase.ts +4 -4
- package/src/registries.ts +1 -2
- package/src/types.ts +4 -4
package/dist/registries.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registries.d.ts","sourceRoot":"","sources":["../src/registries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"registries.d.ts","sourceRoot":"","sources":["../src/registries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AA2BtD,eAAO,MAAM,gBAAgB,EAAE,iBAAiB,EAO/C,CAAA"}
|
package/dist/types.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface BookingTypeConfig {
|
|
|
23
23
|
requiresServices: boolean;
|
|
24
24
|
requiresClient: boolean;
|
|
25
25
|
}
|
|
26
|
-
/** A booking service line item (from
|
|
26
|
+
/** A booking service line item (from public.appointment_items) */
|
|
27
27
|
export interface BookingService {
|
|
28
28
|
id: string;
|
|
29
29
|
serviceId: string | null;
|
|
@@ -32,7 +32,7 @@ export interface BookingService {
|
|
|
32
32
|
price: number;
|
|
33
33
|
assigneeId: string | null;
|
|
34
34
|
}
|
|
35
|
-
/** Calendar booking — the main entity rendered on the calendar (from
|
|
35
|
+
/** Calendar booking — the main entity rendered on the calendar (from v_appointments view on orders) */
|
|
36
36
|
export interface CalendarBooking {
|
|
37
37
|
id: string;
|
|
38
38
|
tenantId: string;
|
|
@@ -76,7 +76,7 @@ export interface Professional {
|
|
|
76
76
|
locationName: string | null;
|
|
77
77
|
isActive: boolean;
|
|
78
78
|
}
|
|
79
|
-
/** Working hours schedule (from
|
|
79
|
+
/** Working hours schedule (from public.schedules) */
|
|
80
80
|
export interface Schedule {
|
|
81
81
|
id: string;
|
|
82
82
|
tenantId: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AASA,+BAA+B;AAC/B,MAAM,MAAM,aAAa,GACrB,WAAW,GACX,WAAW,GACX,aAAa,GACb,WAAW,GACX,WAAW,GACX,SAAS,CAAA;AAEb,mCAAmC;AACnC,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,MAAM,GAAG,WAAW,GAAG,UAAU,CAAA;AAE9E,oEAAoE;AACpE,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,MAAM,GAAG,eAAe,GAAG,OAAO,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAE9F,oDAAoD;AACpD,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,aAAa,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE;QACN;+EACuE;QACvE,KAAK,CAAC,EAAE,OAAO,CAAA;QACf,MAAM,EAAE,OAAO,CAAA;QACf,YAAY,EAAE,OAAO,CAAA;QACrB,QAAQ,EAAE,OAAO,CAAA;QACjB,QAAQ,EAAE,OAAO,CAAA;QACjB,MAAM,EAAE,OAAO,CAAA;KAChB,CAAA;IACD,gBAAgB,EAAE,OAAO,CAAA;IACzB,cAAc,EAAE,OAAO,CAAA;CACxB;AAMD,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fayz-ai/plugin-agenda",
|
|
3
|
-
"
|
|
3
|
+
"fayz": {
|
|
4
|
+
"status": "beta"
|
|
5
|
+
},
|
|
6
|
+
"version": "0.8.0",
|
|
4
7
|
"description": "Fayz SDK — plugin-agenda plugin",
|
|
5
8
|
"type": "module",
|
|
6
9
|
"main": "./dist/index.cjs",
|
|
@@ -29,29 +32,29 @@
|
|
|
29
32
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
30
33
|
},
|
|
31
34
|
"dependencies": {
|
|
35
|
+
"lucide-react": "^0.400.0",
|
|
36
|
+
"zustand": "^4.5.0",
|
|
32
37
|
"@fullcalendar/core": "^6.1.20",
|
|
33
38
|
"@fullcalendar/daygrid": "^6.1.20",
|
|
34
|
-
"@fullcalendar/interaction": "^6.1.20",
|
|
35
|
-
"@fullcalendar/list": "^6.1.0",
|
|
36
|
-
"@fullcalendar/react": "^6.1.20",
|
|
37
|
-
"@fullcalendar/resource-timegrid": "^6.1.0",
|
|
38
39
|
"@fullcalendar/timegrid": "^6.1.20",
|
|
40
|
+
"@fullcalendar/react": "^6.1.20",
|
|
41
|
+
"@fullcalendar/interaction": "^6.1.20",
|
|
39
42
|
"@tanstack/react-table": "^8.20.0",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"@fayz-ai/
|
|
43
|
-
"@fayz-ai/
|
|
44
|
-
"@fayz-ai/
|
|
45
|
-
"@fayz-ai/
|
|
43
|
+
"@fullcalendar/resource-timegrid": "^6.1.0",
|
|
44
|
+
"@fullcalendar/list": "^6.1.0",
|
|
45
|
+
"@fayz-ai/saas": "^0.8.0",
|
|
46
|
+
"@fayz-ai/ui": "^0.8.0",
|
|
47
|
+
"@fayz-ai/core": "^0.8.0",
|
|
48
|
+
"@fayz-ai/plugin-financial": "^0.8.0"
|
|
46
49
|
},
|
|
47
50
|
"devDependencies": {
|
|
48
51
|
"@types/react": "^18.3.0",
|
|
49
|
-
"@types/react-dom": "^18.3.0",
|
|
50
52
|
"react": "^18.3.0",
|
|
51
|
-
"react-dom": "^18.3.0",
|
|
52
53
|
"tsup": "^8.2.0",
|
|
53
54
|
"typescript": "^5.5.0",
|
|
54
|
-
"
|
|
55
|
+
"@types/react-dom": "^18.3.0",
|
|
56
|
+
"vitest": "^2.1.9",
|
|
57
|
+
"react-dom": "^18.3.0"
|
|
55
58
|
},
|
|
56
59
|
"license": "MIT",
|
|
57
60
|
"keywords": [
|
|
@@ -59,6 +62,9 @@
|
|
|
59
62
|
"fayz-plugin",
|
|
60
63
|
"fayz-sdk"
|
|
61
64
|
],
|
|
65
|
+
"publishConfig": {
|
|
66
|
+
"access": "public"
|
|
67
|
+
},
|
|
62
68
|
"scripts": {
|
|
63
69
|
"build": "tsup && tsc --emitDeclarationOnly --declaration --declarationMap --noEmit false",
|
|
64
70
|
"dev": "tsup --watch",
|
|
@@ -54,12 +54,11 @@ export function createFinancialBridge(provider: FinancialDataProvider): AgendaFi
|
|
|
54
54
|
try {
|
|
55
55
|
const [{ data: orders }, { data: movements }] = await Promise.all([
|
|
56
56
|
supabase
|
|
57
|
-
.schema('saas_core')
|
|
58
57
|
.from('orders')
|
|
59
58
|
.select('id, kind, status, total, reference_number, metadata')
|
|
60
59
|
.in('id', orderIds),
|
|
61
60
|
supabase
|
|
62
|
-
.from('
|
|
61
|
+
.from('plg_financial_movements')
|
|
63
62
|
.select('invoice_id, status, paid_amount')
|
|
64
63
|
.in('invoice_id', orderIds),
|
|
65
64
|
])
|
|
@@ -97,8 +96,8 @@ export function createFinancialBridge(provider: FinancialDataProvider): AgendaFi
|
|
|
97
96
|
|
|
98
97
|
// Single query: get order + movements in parallel
|
|
99
98
|
const [orderRes, movRes] = await Promise.all([
|
|
100
|
-
supabase.
|
|
101
|
-
supabase.from('
|
|
99
|
+
supabase.from('orders').select('id, status, total, reference_number, metadata').eq('id', orderId).single(),
|
|
100
|
+
supabase.from('plg_financial_movements').select('*').eq('invoice_id', orderId).order('due_date'),
|
|
102
101
|
])
|
|
103
102
|
|
|
104
103
|
const order = orderRes.data
|
|
@@ -109,7 +108,7 @@ export function createFinancialBridge(provider: FinancialDataProvider): AgendaFi
|
|
|
109
108
|
const typeIds = [...new Set((movRes.data ?? []).map((m: any) => m.payment_method_type_id).filter(Boolean))]
|
|
110
109
|
let typeNames: Record<string, string> = {}
|
|
111
110
|
if (typeIds.length > 0) {
|
|
112
|
-
const { data: types } = await supabase.from('
|
|
111
|
+
const { data: types } = await supabase.from('plg_financial_payment_method_types').select('id, name').in('id', typeIds)
|
|
113
112
|
typeNames = Object.fromEntries((types ?? []).map((t: any) => [t.id, t.name]))
|
|
114
113
|
}
|
|
115
114
|
|
|
@@ -176,7 +175,6 @@ export function createFinancialBridge(provider: FinancialDataProvider): AgendaFi
|
|
|
176
175
|
|
|
177
176
|
// Order carries the assigned professional (assignee_id) and realized total.
|
|
178
177
|
const { data: order } = await supabase
|
|
179
|
-
.schema('saas_core')
|
|
180
178
|
.from('orders')
|
|
181
179
|
.select('id, tenant_id, assignee_id, total')
|
|
182
180
|
.eq('id', orderId)
|
|
@@ -197,7 +195,7 @@ export function createFinancialBridge(provider: FinancialDataProvider): AgendaFi
|
|
|
197
195
|
|
|
198
196
|
// Idempotent: one commission accrual per order. Reuse if already present.
|
|
199
197
|
const { data: existing } = await supabase
|
|
200
|
-
.from('
|
|
198
|
+
.from('plg_financial_movements')
|
|
201
199
|
.select('id')
|
|
202
200
|
.eq('invoice_id', orderId)
|
|
203
201
|
.eq('movement_kind', 'commission')
|
|
@@ -207,7 +205,7 @@ export function createFinancialBridge(provider: FinancialDataProvider): AgendaFi
|
|
|
207
205
|
const dueDate = options?.dueDate ?? new Date().toISOString().slice(0, 10)
|
|
208
206
|
|
|
209
207
|
const { data: inserted } = await supabase
|
|
210
|
-
.from('
|
|
208
|
+
.from('plg_financial_movements')
|
|
211
209
|
.insert({
|
|
212
210
|
tenant_id: order.tenant_id,
|
|
213
211
|
invoice_id: orderId,
|
|
@@ -724,7 +724,7 @@ export function AppointmentModal({ open, mode, bookingId, prefill, initialTab, e
|
|
|
724
724
|
for (const [k, v] of Object.entries(data)) {
|
|
725
725
|
dbData[k.replace(/[A-Z]/g, (m) => '_' + m.toLowerCase())] = v
|
|
726
726
|
}
|
|
727
|
-
await supabase.
|
|
727
|
+
await supabase.from('people').update(dbData).eq('id', clientId)
|
|
728
728
|
setClientData({ ...clientData, ...data })
|
|
729
729
|
setClientEditing(false)
|
|
730
730
|
} catch { /* toast */ }
|
|
@@ -851,8 +851,8 @@ export function AppointmentModal({ open, mode, bookingId, prefill, initialTab, e
|
|
|
851
851
|
const tenantId = getAgendaTenantId()
|
|
852
852
|
if (!supabase || !tenantId) throw new Error('Not initialized')
|
|
853
853
|
|
|
854
|
-
// 1. Insert into
|
|
855
|
-
const { data: personRow, error: personError } = await supabase.
|
|
854
|
+
// 1. Insert into public.people (archetype base table)
|
|
855
|
+
const { data: personRow, error: personError } = await supabase.from('people').insert({
|
|
856
856
|
tenant_id: tenantId, kind: config.clientKind, name: newClientName.trim(),
|
|
857
857
|
phone: newClientPhone.trim() || null, email: newClientEmail.trim() || null,
|
|
858
858
|
}).select('id, name').single()
|
|
@@ -864,7 +864,7 @@ export function AppointmentModal({ open, mode, bookingId, prefill, initialTab, e
|
|
|
864
864
|
})
|
|
865
865
|
if (clientError) {
|
|
866
866
|
// Rollback person if extension insert fails
|
|
867
|
-
await supabase.
|
|
867
|
+
await supabase.from('people').delete().eq('id', personRow.id)
|
|
868
868
|
throw clientError
|
|
869
869
|
}
|
|
870
870
|
|
package/src/config.ts
CHANGED
|
@@ -70,13 +70,13 @@ export interface AgendaPluginOptions {
|
|
|
70
70
|
labels?: Partial<AgendaPluginLabels>
|
|
71
71
|
currency?: { code?: string; locale?: string; symbol?: string }
|
|
72
72
|
|
|
73
|
-
/** Booking kind used in
|
|
73
|
+
/** Booking kind used in public.appointments.kind (default: 'appointment') */
|
|
74
74
|
bookingKind?: string
|
|
75
75
|
|
|
76
76
|
/** Order kind for financial records (default: 'service_order') */
|
|
77
77
|
orderKind?: string
|
|
78
78
|
|
|
79
|
-
/** Schedule kind in
|
|
79
|
+
/** Schedule kind in public.schedules.kind (default: 'working_hours') */
|
|
80
80
|
scheduleKind?: string
|
|
81
81
|
|
|
82
82
|
/** Booking type tabs shown in the appointment modal */
|
package/src/data/supabase.ts
CHANGED
|
@@ -28,7 +28,8 @@ function snakeToCamel(obj: Record<string, unknown>): Record<string, unknown> {
|
|
|
28
28
|
function getClients() {
|
|
29
29
|
const supabase = getSupabaseClientOptional() as any
|
|
30
30
|
if (!supabase) throw new Error('Supabase not initialized')
|
|
31
|
-
|
|
31
|
+
// Core v1 lives in PUBLIC — a single client serves both core and view reads.
|
|
32
|
+
return { core: supabase, pub: supabase }
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
function mapBooking(row: Record<string, unknown>): CalendarBooking {
|
|
@@ -97,7 +98,7 @@ async function hydrateCanonicalBookings(core: any, bookings: any[]): Promise<Cal
|
|
|
97
98
|
{ data: items, error: itemsError },
|
|
98
99
|
] = await Promise.all([
|
|
99
100
|
personIds.length
|
|
100
|
-
? core.from('
|
|
101
|
+
? core.from('people').select('id, name, phone, email, avatar_url').in('id', personIds)
|
|
101
102
|
: Promise.resolve({ data: [], error: null }),
|
|
102
103
|
locationIds.length
|
|
103
104
|
? core.from('locations').select('id, name').in('id', locationIds)
|
|
@@ -170,7 +171,7 @@ async function hydrateCanonicalBookings(core: any, bookings: any[]): Promise<Cal
|
|
|
170
171
|
}
|
|
171
172
|
|
|
172
173
|
async function getCanonicalBookings(core: any, query: BookingQuery): Promise<CalendarBooking[]> {
|
|
173
|
-
let q = core.from('
|
|
174
|
+
let q = core.from('appointments')
|
|
174
175
|
.select('id, tenant_id, kind, starts_at, ends_at, status, notes, order_id, location_id, metadata, party_id, assignee_id, created_at, updated_at')
|
|
175
176
|
.gte('starts_at', query.dateRange.start)
|
|
176
177
|
.lte('starts_at', query.dateRange.end)
|
|
@@ -187,11 +188,11 @@ async function getCanonicalBookings(core: any, query: BookingQuery): Promise<Cal
|
|
|
187
188
|
|
|
188
189
|
async function getCanonicalBookingById(core: any, id: string): Promise<CalendarBooking | null> {
|
|
189
190
|
const columns = 'id, tenant_id, kind, starts_at, ends_at, status, notes, order_id, location_id, metadata, party_id, assignee_id, created_at, updated_at'
|
|
190
|
-
let { data, error } = await core.from('
|
|
191
|
+
let { data, error } = await core.from('appointments').select(columns).eq('id', id).maybeSingle()
|
|
191
192
|
if (error) throw error
|
|
192
193
|
|
|
193
194
|
if (!data) {
|
|
194
|
-
const fallback = await core.from('
|
|
195
|
+
const fallback = await core.from('appointments').select(columns).eq('order_id', id).maybeSingle()
|
|
195
196
|
if (fallback.error) throw fallback.error
|
|
196
197
|
data = fallback.data
|
|
197
198
|
}
|
|
@@ -207,10 +208,10 @@ async function getCanonicalBookingById(core: any, id: string): Promise<CalendarB
|
|
|
207
208
|
|
|
208
209
|
export function createSupabaseAgendaProvider(): AgendaDataProvider {
|
|
209
210
|
return {
|
|
210
|
-
//
|
|
211
|
+
// v_appointments is in public schema (PostgREST default)
|
|
211
212
|
async getBookings(query: BookingQuery): Promise<CalendarBooking[]> {
|
|
212
213
|
const { core, pub } = getClients()
|
|
213
|
-
let q = pub.from('
|
|
214
|
+
let q = pub.from('v_appointments').select('*')
|
|
214
215
|
.gte('starts_at', query.dateRange.start)
|
|
215
216
|
.lte('starts_at', query.dateRange.end)
|
|
216
217
|
|
|
@@ -230,7 +231,7 @@ export function createSupabaseAgendaProvider(): AgendaDataProvider {
|
|
|
230
231
|
|
|
231
232
|
async getBookingById(id: string): Promise<CalendarBooking | null> {
|
|
232
233
|
const { core, pub } = getClients()
|
|
233
|
-
const { data, error } = await pub.from('
|
|
234
|
+
const { data, error } = await pub.from('v_appointments').select('*').eq('id', id).maybeSingle()
|
|
234
235
|
if (error) throw error
|
|
235
236
|
if (data) return mapBooking(data as Record<string, unknown>)
|
|
236
237
|
return getCanonicalBookingById(core, id)
|
|
@@ -250,7 +251,7 @@ export function createSupabaseAgendaProvider(): AgendaDataProvider {
|
|
|
250
251
|
// Resolve client name for metadata (used by financial detail view)
|
|
251
252
|
let contactName: string | undefined
|
|
252
253
|
if (input.clientId) {
|
|
253
|
-
const { data: person } = await core.from('
|
|
254
|
+
const { data: person } = await core.from('people').select('name').eq('id', input.clientId).single()
|
|
254
255
|
contactName = person?.name as string | undefined
|
|
255
256
|
}
|
|
256
257
|
|
|
@@ -277,7 +278,7 @@ export function createSupabaseAgendaProvider(): AgendaDataProvider {
|
|
|
277
278
|
}).select('id').single()
|
|
278
279
|
if (orderErr) throw orderErr
|
|
279
280
|
|
|
280
|
-
const { data: booking, error: bookingErr } = await core.from('
|
|
281
|
+
const { data: booking, error: bookingErr } = await core.from('appointments').insert({
|
|
281
282
|
tenant_id: tenantId,
|
|
282
283
|
kind: input.kind ?? 'appointment',
|
|
283
284
|
party_id: input.clientId || null,
|
|
@@ -321,7 +322,7 @@ export function createSupabaseAgendaProvider(): AgendaDataProvider {
|
|
|
321
322
|
|
|
322
323
|
async updateBooking(id: string, data: UpdateBookingInput): Promise<CalendarBooking> {
|
|
323
324
|
const { core } = getClients()
|
|
324
|
-
const { data: existing, error: existingErr } = await core.from('
|
|
325
|
+
const { data: existing, error: existingErr } = await core.from('appointments')
|
|
325
326
|
.select('id, order_id, starts_at, assignee_id, metadata')
|
|
326
327
|
.eq('id', id)
|
|
327
328
|
.single()
|
|
@@ -357,7 +358,7 @@ export function createSupabaseAgendaProvider(): AgendaDataProvider {
|
|
|
357
358
|
|
|
358
359
|
const effectiveAssigneeId = data.professionalId ?? (existing?.assignee_id as string | null | undefined) ?? null
|
|
359
360
|
|
|
360
|
-
const { error } = await core.from('
|
|
361
|
+
const { error } = await core.from('appointments').update(updates).eq('id', id)
|
|
361
362
|
if (error) throw error
|
|
362
363
|
|
|
363
364
|
// S4: services are updated only in order_items below (booking_items deprecated).
|
|
@@ -405,7 +406,7 @@ export function createSupabaseAgendaProvider(): AgendaDataProvider {
|
|
|
405
406
|
|
|
406
407
|
async deleteBooking(id: string): Promise<void> {
|
|
407
408
|
const { core, pub } = getClients()
|
|
408
|
-
const { data: booking, error: bookingErr } = await core.from('
|
|
409
|
+
const { data: booking, error: bookingErr } = await core.from('appointments')
|
|
409
410
|
.select('order_id')
|
|
410
411
|
.eq('id', id)
|
|
411
412
|
.single()
|
|
@@ -413,22 +414,22 @@ export function createSupabaseAgendaProvider(): AgendaDataProvider {
|
|
|
413
414
|
|
|
414
415
|
const orderId = booking?.order_id as string | null | undefined
|
|
415
416
|
const { count, error: movementErr } = orderId
|
|
416
|
-
? await pub.from('
|
|
417
|
+
? await pub.from('plg_financial_movements').select('id', { count: 'exact', head: true }).eq('invoice_id', orderId)
|
|
417
418
|
: { count: 0, error: null }
|
|
418
419
|
if (movementErr) throw movementErr
|
|
419
420
|
|
|
420
421
|
if (orderId && (count ?? 0) > 0) {
|
|
421
422
|
// Cancel movements + order (don't delete financial records)
|
|
422
|
-
await pub.from('
|
|
423
|
+
await pub.from('plg_financial_movements').update({ status: 'cancelled' }).eq('invoice_id', orderId)
|
|
423
424
|
await core.from('orders').update({ status: 'cancelled' }).eq('id', orderId)
|
|
424
|
-
const { error } = await core.from('
|
|
425
|
+
const { error } = await core.from('appointments').update({ status: 'cancelled' }).eq('id', id)
|
|
425
426
|
if (error) throw error
|
|
426
427
|
} else {
|
|
427
428
|
// Safe to delete — remove any movements first (FK constraint)
|
|
428
429
|
if (orderId) {
|
|
429
|
-
await pub.from('
|
|
430
|
+
await pub.from('plg_financial_movements').delete().eq('invoice_id', orderId)
|
|
430
431
|
}
|
|
431
|
-
const { error } = await core.from('
|
|
432
|
+
const { error } = await core.from('appointments').delete().eq('id', id)
|
|
432
433
|
if (error) throw error
|
|
433
434
|
if (orderId) {
|
|
434
435
|
const { error: orderErr } = await core.from('orders').delete().eq('id', orderId)
|
|
@@ -443,7 +444,7 @@ export function createSupabaseAgendaProvider(): AgendaDataProvider {
|
|
|
443
444
|
|
|
444
445
|
async checkConflict(params: ConflictCheckParams): Promise<boolean> {
|
|
445
446
|
const { core } = getClients()
|
|
446
|
-
let q = core.from('
|
|
447
|
+
let q = core.from('appointments').select('id', { count: 'exact', head: true })
|
|
447
448
|
.eq('assignee_id', params.assigneeId)
|
|
448
449
|
.eq('kind', 'appointment')
|
|
449
450
|
.not('status', 'in', '("cancelled","no_show")')
|
|
@@ -513,7 +514,7 @@ export function createSupabaseAgendaProvider(): AgendaDataProvider {
|
|
|
513
514
|
|
|
514
515
|
async getProfessionals(): Promise<Professional[]> {
|
|
515
516
|
const { core } = getClients()
|
|
516
|
-
const { data, error } = await core.from('
|
|
517
|
+
const { data, error } = await core.from('people')
|
|
517
518
|
.select('id, name, avatar_url, is_active')
|
|
518
519
|
.eq('kind', 'staff')
|
|
519
520
|
.eq('is_active', true)
|
|
@@ -541,7 +542,7 @@ export function createSupabaseAgendaProvider(): AgendaDataProvider {
|
|
|
541
542
|
const cutoff = new Date()
|
|
542
543
|
cutoff.setDate(cutoff.getDate() + daysAhead)
|
|
543
544
|
|
|
544
|
-
const { data, error } = await pub.from('
|
|
545
|
+
const { data, error } = await pub.from('v_appointments').select('*')
|
|
545
546
|
.eq('status', 'scheduled')
|
|
546
547
|
.gte('starts_at', now)
|
|
547
548
|
.lte('starts_at', cutoff.toISOString())
|
|
@@ -552,22 +553,22 @@ export function createSupabaseAgendaProvider(): AgendaDataProvider {
|
|
|
552
553
|
|
|
553
554
|
async sendConfirmation(bookingId: string, channel: string): Promise<void> {
|
|
554
555
|
const { core } = getClients()
|
|
555
|
-
const { data: booking } = await core.from('
|
|
556
|
+
const { data: booking } = await core.from('appointments').select('metadata').eq('id', bookingId).single()
|
|
556
557
|
const metadata = (booking?.metadata ?? {}) as Record<string, unknown>
|
|
557
558
|
const confirmations = (metadata.confirmations as any[] ?? [])
|
|
558
559
|
confirmations.push({ channel, sentAt: new Date().toISOString(), status: 'sent' })
|
|
559
|
-
await core.from('
|
|
560
|
+
await core.from('appointments').update({ metadata: { ...metadata, confirmations } }).eq('id', bookingId)
|
|
560
561
|
},
|
|
561
562
|
|
|
562
563
|
async completeBooking(bookingId: string): Promise<{ booking: CalendarBooking; orderId: string }> {
|
|
563
564
|
const { core } = getClients()
|
|
564
|
-
const { data: existing, error: existingErr } = await core.from('
|
|
565
|
+
const { data: existing, error: existingErr } = await core.from('appointments')
|
|
565
566
|
.select('order_id')
|
|
566
567
|
.eq('id', bookingId)
|
|
567
568
|
.single()
|
|
568
569
|
if (existingErr) throw existingErr
|
|
569
570
|
|
|
570
|
-
const { error } = await core.from('
|
|
571
|
+
const { error } = await core.from('appointments')
|
|
571
572
|
.update({ status: 'completed' })
|
|
572
573
|
.eq('id', bookingId)
|
|
573
574
|
if (error) throw error
|
package/src/index.ts
CHANGED
|
@@ -186,7 +186,7 @@ export function createAgendaPlugin(options?: AgendaPluginOptions): PluginManifes
|
|
|
186
186
|
version: '0.3.0',
|
|
187
187
|
sql: MIGRATION_001_PUBLIC_BOOKING,
|
|
188
188
|
description:
|
|
189
|
-
'Booking read model (
|
|
189
|
+
'Booking read model (v_appointments) + anon public booking surface (v_public_services, get_available_slots, create_public_booking) + order_items duration/assignee columns',
|
|
190
190
|
},
|
|
191
191
|
],
|
|
192
192
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Google) is automatic via a DB trigger; inbound (Google → booking) via pg_cron
|
|
6
6
|
// or a Google watch webhook. The data plane is the google-calendar-sync edge
|
|
7
7
|
// function (see ./functions/google-calendar-sync). The booking↔event link lives
|
|
8
|
-
// on
|
|
8
|
+
// on public.appointments.metadata.googleCalendarEventId.
|
|
9
9
|
//
|
|
10
10
|
// Graduation path: extract to its own `@fayz-ai/plugin-google-calendar` package.
|
|
11
11
|
import type { PluginManifest } from '@fayz-ai/core'
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
-- ---------------------------------------------------------------------------
|
|
2
2
|
-- Google Calendar integration (bidirectional) — connection + audit + triggers
|
|
3
3
|
-- ---------------------------------------------------------------------------
|
|
4
|
-
-- Outbound (Fayz booking → Google event) fires from a trigger on
|
|
4
|
+
-- Outbound (Fayz booking → Google event) fires from a trigger on public.appointments
|
|
5
5
|
-- via pg_net to the google-calendar-sync edge function (push_event). Inbound
|
|
6
6
|
-- (Google → booking) runs on a pg_cron schedule (pull_events) and/or a Google
|
|
7
7
|
-- watch-channel webhook. The booking↔event link is stored on
|
|
8
|
-
--
|
|
8
|
+
-- public.appointments.metadata.googleCalendarEventId (no schema change there).
|
|
9
9
|
|
|
10
10
|
CREATE TABLE IF NOT EXISTS public.calendar_integrations (
|
|
11
11
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
12
|
-
tenant_id uuid NOT NULL REFERENCES
|
|
12
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
13
13
|
provider text NOT NULL DEFAULT 'google',
|
|
14
14
|
oauth_refresh_token text,
|
|
15
15
|
oauth_access_token text,
|
|
@@ -31,7 +31,7 @@ CREATE INDEX IF NOT EXISTS idx_calendar_integrations_tenant ON public.calendar_i
|
|
|
31
31
|
|
|
32
32
|
CREATE TABLE IF NOT EXISTS public.calendar_sync_log (
|
|
33
33
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
34
|
-
tenant_id uuid NOT NULL REFERENCES
|
|
34
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
35
35
|
direction text NOT NULL, -- inbound | outbound
|
|
36
36
|
trigger text, -- on-write | scheduled | webhook | manual
|
|
37
37
|
status text NOT NULL DEFAULT 'success', -- success | partial | error
|
|
@@ -80,9 +80,9 @@ BEGIN
|
|
|
80
80
|
RETURN COALESCE(NEW, OLD);
|
|
81
81
|
END $$;
|
|
82
82
|
|
|
83
|
-
DROP TRIGGER IF EXISTS trg_gcal_push ON
|
|
83
|
+
DROP TRIGGER IF EXISTS trg_gcal_push ON public.appointments;
|
|
84
84
|
CREATE TRIGGER trg_gcal_push
|
|
85
|
-
AFTER INSERT OR UPDATE OR DELETE ON
|
|
85
|
+
AFTER INSERT OR UPDATE OR DELETE ON public.appointments
|
|
86
86
|
FOR EACH ROW EXECUTE FUNCTION public.fn_gcal_push_booking();
|
|
87
87
|
|
|
88
88
|
-- Inbound schedule (optional): pull external changes every 10 minutes.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- Salon-only unblock: the pre-pools beauty deployment built rep_* report
|
|
2
|
+
-- views on top of its (wider) v_bookings view, so 001's cleanup DROP cannot
|
|
3
|
+
-- run there. Renaming keeps the dependents attached (they track the OID) and
|
|
4
|
+
-- frees the name for 001; 002_v_bookings_compat re-creates v_bookings as a
|
|
5
|
+
-- thin alias for live pre-M5 clients. No-op on fresh pools and on pools whose
|
|
6
|
+
-- v_bookings has no dependents (001's DROP handles those).
|
|
7
|
+
|
|
8
|
+
DO $rename$
|
|
9
|
+
BEGIN
|
|
10
|
+
IF to_regclass('public.v_bookings') IS NOT NULL
|
|
11
|
+
AND to_regclass('public.v_bookings_legacy_beauty') IS NULL
|
|
12
|
+
AND EXISTS (
|
|
13
|
+
SELECT 1 FROM pg_depend d
|
|
14
|
+
JOIN pg_rewrite rw ON rw.oid = d.objid
|
|
15
|
+
JOIN pg_class dep ON dep.oid = rw.ev_class
|
|
16
|
+
JOIN pg_class ref ON ref.oid = d.refobjid
|
|
17
|
+
WHERE ref.relname = 'v_bookings' AND dep.relname <> 'v_bookings'
|
|
18
|
+
) THEN
|
|
19
|
+
EXECUTE 'ALTER VIEW public.v_bookings RENAME TO v_bookings_legacy_beauty';
|
|
20
|
+
END IF;
|
|
21
|
+
END $rename$;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
-- 001 needs the customer-phone upsert key UNIQUE(tenant_id, phone). Converted
|
|
2
|
+
-- pools can carry duplicates (found live on salon: 9 dogfood test records all
|
|
3
|
+
-- sharing the founder's phone). Non-destructive dedupe: per (tenant_id, phone)
|
|
4
|
+
-- customer group keep the row with most appointments (tie: oldest), NULL the
|
|
5
|
+
-- phone on the rest and stash the original in metadata->'dedupedPhone'.
|
|
6
|
+
-- Idempotent: reruns find no groups > 1.
|
|
7
|
+
|
|
8
|
+
DO $dedupe$
|
|
9
|
+
DECLARE
|
|
10
|
+
r record;
|
|
11
|
+
BEGIN
|
|
12
|
+
IF to_regclass('public.people') IS NULL THEN
|
|
13
|
+
RETURN;
|
|
14
|
+
END IF;
|
|
15
|
+
|
|
16
|
+
FOR r IN
|
|
17
|
+
SELECT p.id, p.phone
|
|
18
|
+
FROM public.people p
|
|
19
|
+
JOIN LATERAL (
|
|
20
|
+
SELECT count(*) AS appts
|
|
21
|
+
FROM public.appointments a WHERE a.party_id = p.id
|
|
22
|
+
) ac ON true
|
|
23
|
+
WHERE p.kind = 'customer' AND p.phone IS NOT NULL
|
|
24
|
+
AND p.id NOT IN (
|
|
25
|
+
SELECT DISTINCT ON (p2.tenant_id, p2.phone) p2.id
|
|
26
|
+
FROM public.people p2
|
|
27
|
+
WHERE p2.kind = 'customer' AND p2.phone IS NOT NULL
|
|
28
|
+
ORDER BY p2.tenant_id, p2.phone,
|
|
29
|
+
(SELECT count(*) FROM public.appointments a2 WHERE a2.party_id = p2.id) DESC,
|
|
30
|
+
p2.created_at ASC
|
|
31
|
+
)
|
|
32
|
+
AND (p.tenant_id, p.phone) IN (
|
|
33
|
+
SELECT tenant_id, phone FROM public.people
|
|
34
|
+
WHERE kind = 'customer' AND phone IS NOT NULL
|
|
35
|
+
GROUP BY tenant_id, phone HAVING count(*) > 1
|
|
36
|
+
)
|
|
37
|
+
LOOP
|
|
38
|
+
UPDATE public.people
|
|
39
|
+
SET metadata = COALESCE(metadata, '{}'::jsonb) || jsonb_build_object('dedupedPhone', r.phone),
|
|
40
|
+
phone = NULL
|
|
41
|
+
WHERE id = r.id;
|
|
42
|
+
END LOOP;
|
|
43
|
+
END $dedupe$;
|