@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/README.md
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@fayz-ai/plugin-agenda)
|
|
6
6
|
[](https://github.com/FayaLabs/fayz-sdk/blob/main/LICENSE)
|
|
7
7
|
|
|
8
|
+
**Status:** beta — pre-1.0. Core surface is stable; some backend facets (see `PLUGIN_PATTERNS.md`) are still landing. APIs may change before 1.0.
|
|
9
|
+
|
|
8
10
|
Every service business runs on a calendar. Salons book stylists, clinics book doctors, studios book rooms — the same primitive, rebuilt badly a thousand times. `plugin-agenda` is that primitive done once: a resource-aware calendar with appointments, blocks, working hours, conflict detection, and drag-and-drop, all driven by config instead of forks.
|
|
9
11
|
|
|
10
12
|
It snaps into any `defineSaas` app and reshapes itself per vertical — booking types, statuses, professional/client/service lookups, and locations are all options. Pair it with `@fayz-ai/plugin-financial` and a booking can auto-create an order, so scheduling and revenue stay in sync from the first appointment. One engine, every vertical, no rewrite.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-financial-bridge.d.ts","sourceRoot":"","sources":["../../src/bridges/create-financial-bridge.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAA;AACtE,OAAO,KAAK,EAAE,qBAAqB,EAAqE,MAAM,qBAAqB,CAAA;AAGnI;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAKlF;AAeD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,qBAAqB,
|
|
1
|
+
{"version":3,"file":"create-financial-bridge.d.ts","sourceRoot":"","sources":["../../src/bridges/create-financial-bridge.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAA;AACtE,OAAO,KAAK,EAAE,qBAAqB,EAAqE,MAAM,qBAAqB,CAAA;AAGnI;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAKlF;AAeD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,qBAAqB,CA0O5F"}
|
package/dist/config.d.ts
CHANGED
|
@@ -62,11 +62,11 @@ export interface AgendaPluginOptions {
|
|
|
62
62
|
locale?: string;
|
|
63
63
|
symbol?: string;
|
|
64
64
|
};
|
|
65
|
-
/** Booking kind used in
|
|
65
|
+
/** Booking kind used in public.appointments.kind (default: 'appointment') */
|
|
66
66
|
bookingKind?: string;
|
|
67
67
|
/** Order kind for financial records (default: 'service_order') */
|
|
68
68
|
orderKind?: string;
|
|
69
|
-
/** Schedule kind in
|
|
69
|
+
/** Schedule kind in public.schedules.kind (default: 'working_hours') */
|
|
70
70
|
scheduleKind?: string;
|
|
71
71
|
/** Booking type tabs shown in the appointment modal */
|
|
72
72
|
bookingTypes?: BookingTypeConfig[];
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC/E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAC9D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAM9C,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,wBAAwB;IACvC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAED,MAAM,WAAW,2BAA2B;IAC1C,WAAW,IAAI,OAAO,CAAC,wBAAwB,EAAE,CAAC,CAAA;IAClD,uBAAuB,CAAC,KAAK,EAAE;QAC7B,SAAS,EAAE,MAAM,CAAA;QACjB,QAAQ,EAAE,MAAM,CAAA;QAChB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,WAAW,CAAC,EAAE,MAAM,CAAA;KACrB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE;QACR,YAAY,CAAC,EAAE,OAAO,CAAA;QACtB,aAAa,CAAC,EAAE,OAAO,CAAA;QACvB,iBAAiB,CAAC,EAAE,OAAO,CAAA;QAC3B,WAAW,CAAC,EAAE,OAAO,CAAA;QACrB,iBAAiB,CAAC,EAAE,OAAO,CAAA;QAC3B,mFAAmF;QACnF,SAAS,CAAC,EAAE,OAAO,CAAA;KACpB,CAAA;IACD,MAAM,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAA;IACpC,QAAQ,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAE9D,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"supabase.d.ts","sourceRoot":"","sources":["../../src/data/supabase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"supabase.d.ts","sourceRoot":"","sources":["../../src/data/supabase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAgNjD,wBAAgB,4BAA4B,IAAI,kBAAkB,CAqXjE"}
|
package/dist/index.cjs
CHANGED
|
@@ -1733,7 +1733,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1733
1733
|
for (const [k, v] of Object.entries(data)) {
|
|
1734
1734
|
dbData[k.replace(/[A-Z]/g, (m) => "_" + m.toLowerCase())] = v;
|
|
1735
1735
|
}
|
|
1736
|
-
await supabase.
|
|
1736
|
+
await supabase.from("people").update(dbData).eq("id", clientId);
|
|
1737
1737
|
setClientData({ ...clientData, ...data });
|
|
1738
1738
|
setClientEditing(false);
|
|
1739
1739
|
} catch {
|
|
@@ -1893,7 +1893,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1893
1893
|
const supabase = getSupabaseClientOptional4();
|
|
1894
1894
|
const tenantId = getAgendaTenantId2();
|
|
1895
1895
|
if (!supabase || !tenantId) throw new Error("Not initialized");
|
|
1896
|
-
const { data: personRow, error: personError } = await supabase.
|
|
1896
|
+
const { data: personRow, error: personError } = await supabase.from("people").insert({
|
|
1897
1897
|
tenant_id: tenantId,
|
|
1898
1898
|
kind: config.clientKind,
|
|
1899
1899
|
name: newClientName.trim(),
|
|
@@ -1906,7 +1906,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1906
1906
|
tenant_id: tenantId
|
|
1907
1907
|
});
|
|
1908
1908
|
if (clientError) {
|
|
1909
|
-
await supabase.
|
|
1909
|
+
await supabase.from("people").delete().eq("id", personRow.id);
|
|
1910
1910
|
throw clientError;
|
|
1911
1911
|
}
|
|
1912
1912
|
setClientId(personRow.id);
|
|
@@ -3486,7 +3486,7 @@ function snakeToCamel(obj) {
|
|
|
3486
3486
|
function getClients() {
|
|
3487
3487
|
const supabase = core.getSupabaseClientOptional();
|
|
3488
3488
|
if (!supabase) throw new Error("Supabase not initialized");
|
|
3489
|
-
return { core: supabase
|
|
3489
|
+
return { core: supabase, pub: supabase };
|
|
3490
3490
|
}
|
|
3491
3491
|
function mapBooking(row) {
|
|
3492
3492
|
return snakeToCamel(row);
|
|
@@ -3543,7 +3543,7 @@ async function hydrateCanonicalBookings(core, bookings) {
|
|
|
3543
3543
|
{ data: orders, error: ordersError },
|
|
3544
3544
|
{ data: items, error: itemsError }
|
|
3545
3545
|
] = await Promise.all([
|
|
3546
|
-
personIds.length ? core.from("
|
|
3546
|
+
personIds.length ? core.from("people").select("id, name, phone, email, avatar_url").in("id", personIds) : Promise.resolve({ data: [], error: null }),
|
|
3547
3547
|
locationIds.length ? core.from("locations").select("id, name").in("id", locationIds) : Promise.resolve({ data: [], error: null }),
|
|
3548
3548
|
orderIds.length ? core.from("orders").select("id, total, status").in("id", orderIds) : Promise.resolve({ data: [], error: null }),
|
|
3549
3549
|
orderIds.length ? core.from("order_items").select("id, order_id, service_id, name, duration_minutes, unit_price, assignee_id, sort_order").in("order_id", orderIds) : Promise.resolve({ data: [], error: null })
|
|
@@ -3601,7 +3601,7 @@ async function hydrateCanonicalBookings(core, bookings) {
|
|
|
3601
3601
|
});
|
|
3602
3602
|
}
|
|
3603
3603
|
async function getCanonicalBookings(core, query) {
|
|
3604
|
-
let q = core.from("
|
|
3604
|
+
let q = core.from("appointments").select("id, tenant_id, kind, starts_at, ends_at, status, notes, order_id, location_id, metadata, party_id, assignee_id, created_at, updated_at").gte("starts_at", query.dateRange.start).lte("starts_at", query.dateRange.end);
|
|
3605
3605
|
if (query.kind) q = q.eq("kind", query.kind);
|
|
3606
3606
|
if (query.statuses?.length) q = q.in("status", query.statuses);
|
|
3607
3607
|
if (query.professionalIds?.length) q = q.in("assignee_id", query.professionalIds);
|
|
@@ -3612,10 +3612,10 @@ async function getCanonicalBookings(core, query) {
|
|
|
3612
3612
|
}
|
|
3613
3613
|
async function getCanonicalBookingById(core, id) {
|
|
3614
3614
|
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";
|
|
3615
|
-
let { data, error } = await core.from("
|
|
3615
|
+
let { data, error } = await core.from("appointments").select(columns).eq("id", id).maybeSingle();
|
|
3616
3616
|
if (error) throw error;
|
|
3617
3617
|
if (!data) {
|
|
3618
|
-
const fallback = await core.from("
|
|
3618
|
+
const fallback = await core.from("appointments").select(columns).eq("order_id", id).maybeSingle();
|
|
3619
3619
|
if (fallback.error) throw fallback.error;
|
|
3620
3620
|
data = fallback.data;
|
|
3621
3621
|
}
|
|
@@ -3625,10 +3625,10 @@ async function getCanonicalBookingById(core, id) {
|
|
|
3625
3625
|
}
|
|
3626
3626
|
function createSupabaseAgendaProvider() {
|
|
3627
3627
|
return {
|
|
3628
|
-
//
|
|
3628
|
+
// v_appointments is in public schema (PostgREST default)
|
|
3629
3629
|
async getBookings(query) {
|
|
3630
3630
|
const { core, pub } = getClients();
|
|
3631
|
-
let q = pub.from("
|
|
3631
|
+
let q = pub.from("v_appointments").select("*").gte("starts_at", query.dateRange.start).lte("starts_at", query.dateRange.end);
|
|
3632
3632
|
if (query.kind) q = q.eq("kind", query.kind);
|
|
3633
3633
|
if (query.statuses?.length) q = q.in("status", query.statuses);
|
|
3634
3634
|
if (query.professionalIds?.length) q = q.in("professional_id", query.professionalIds);
|
|
@@ -3642,7 +3642,7 @@ function createSupabaseAgendaProvider() {
|
|
|
3642
3642
|
},
|
|
3643
3643
|
async getBookingById(id) {
|
|
3644
3644
|
const { core, pub } = getClients();
|
|
3645
|
-
const { data, error } = await pub.from("
|
|
3645
|
+
const { data, error } = await pub.from("v_appointments").select("*").eq("id", id).maybeSingle();
|
|
3646
3646
|
if (error) throw error;
|
|
3647
3647
|
if (data) return mapBooking(data);
|
|
3648
3648
|
return getCanonicalBookingById(core, id);
|
|
@@ -3658,7 +3658,7 @@ function createSupabaseAgendaProvider() {
|
|
|
3658
3658
|
const serviceNames = input.services.map((s) => s.name).join(", ");
|
|
3659
3659
|
let contactName;
|
|
3660
3660
|
if (input.clientId) {
|
|
3661
|
-
const { data: person } = await core.from("
|
|
3661
|
+
const { data: person } = await core.from("people").select("name").eq("id", input.clientId).single();
|
|
3662
3662
|
contactName = person?.name;
|
|
3663
3663
|
}
|
|
3664
3664
|
const eventTitle = input.title?.trim() || void 0;
|
|
@@ -3680,7 +3680,7 @@ function createSupabaseAgendaProvider() {
|
|
|
3680
3680
|
}
|
|
3681
3681
|
}).select("id").single();
|
|
3682
3682
|
if (orderErr) throw orderErr;
|
|
3683
|
-
const { data: booking, error: bookingErr } = await core.from("
|
|
3683
|
+
const { data: booking, error: bookingErr } = await core.from("appointments").insert({
|
|
3684
3684
|
tenant_id: tenantId,
|
|
3685
3685
|
kind: input.kind ?? "appointment",
|
|
3686
3686
|
party_id: input.clientId || null,
|
|
@@ -3720,7 +3720,7 @@ function createSupabaseAgendaProvider() {
|
|
|
3720
3720
|
},
|
|
3721
3721
|
async updateBooking(id, data) {
|
|
3722
3722
|
const { core } = getClients();
|
|
3723
|
-
const { data: existing, error: existingErr } = await core.from("
|
|
3723
|
+
const { data: existing, error: existingErr } = await core.from("appointments").select("id, order_id, starts_at, assignee_id, metadata").eq("id", id).single();
|
|
3724
3724
|
if (existingErr) throw existingErr;
|
|
3725
3725
|
const updates = {};
|
|
3726
3726
|
if (data.startsAt) updates.starts_at = data.startsAt;
|
|
@@ -3745,7 +3745,7 @@ function createSupabaseAgendaProvider() {
|
|
|
3745
3745
|
}
|
|
3746
3746
|
}
|
|
3747
3747
|
const effectiveAssigneeId = data.professionalId ?? existing?.assignee_id ?? null;
|
|
3748
|
-
const { error } = await core.from("
|
|
3748
|
+
const { error } = await core.from("appointments").update(updates).eq("id", id);
|
|
3749
3749
|
if (error) throw error;
|
|
3750
3750
|
const orderId = existing?.order_id;
|
|
3751
3751
|
if (orderId) {
|
|
@@ -3786,21 +3786,21 @@ function createSupabaseAgendaProvider() {
|
|
|
3786
3786
|
},
|
|
3787
3787
|
async deleteBooking(id) {
|
|
3788
3788
|
const { core, pub } = getClients();
|
|
3789
|
-
const { data: booking, error: bookingErr } = await core.from("
|
|
3789
|
+
const { data: booking, error: bookingErr } = await core.from("appointments").select("order_id").eq("id", id).single();
|
|
3790
3790
|
if (bookingErr) throw bookingErr;
|
|
3791
3791
|
const orderId = booking?.order_id;
|
|
3792
|
-
const { count, error: movementErr } = orderId ? await pub.from("
|
|
3792
|
+
const { count, error: movementErr } = orderId ? await pub.from("plg_financial_movements").select("id", { count: "exact", head: true }).eq("invoice_id", orderId) : { count: 0, error: null };
|
|
3793
3793
|
if (movementErr) throw movementErr;
|
|
3794
3794
|
if (orderId && (count ?? 0) > 0) {
|
|
3795
|
-
await pub.from("
|
|
3795
|
+
await pub.from("plg_financial_movements").update({ status: "cancelled" }).eq("invoice_id", orderId);
|
|
3796
3796
|
await core.from("orders").update({ status: "cancelled" }).eq("id", orderId);
|
|
3797
|
-
const { error } = await core.from("
|
|
3797
|
+
const { error } = await core.from("appointments").update({ status: "cancelled" }).eq("id", id);
|
|
3798
3798
|
if (error) throw error;
|
|
3799
3799
|
} else {
|
|
3800
3800
|
if (orderId) {
|
|
3801
|
-
await pub.from("
|
|
3801
|
+
await pub.from("plg_financial_movements").delete().eq("invoice_id", orderId);
|
|
3802
3802
|
}
|
|
3803
|
-
const { error } = await core.from("
|
|
3803
|
+
const { error } = await core.from("appointments").delete().eq("id", id);
|
|
3804
3804
|
if (error) throw error;
|
|
3805
3805
|
if (orderId) {
|
|
3806
3806
|
const { error: orderErr } = await core.from("orders").delete().eq("id", orderId);
|
|
@@ -3813,7 +3813,7 @@ function createSupabaseAgendaProvider() {
|
|
|
3813
3813
|
},
|
|
3814
3814
|
async checkConflict(params) {
|
|
3815
3815
|
const { core } = getClients();
|
|
3816
|
-
let q = core.from("
|
|
3816
|
+
let q = core.from("appointments").select("id", { count: "exact", head: true }).eq("assignee_id", params.assigneeId).eq("kind", "appointment").not("status", "in", '("cancelled","no_show")').not("starts_at", "is", null).lt("starts_at", params.endsAt).gt("ends_at", params.startsAt);
|
|
3817
3817
|
if (params.excludeBookingId) {
|
|
3818
3818
|
q = q.neq("id", params.excludeBookingId);
|
|
3819
3819
|
}
|
|
@@ -3868,7 +3868,7 @@ function createSupabaseAgendaProvider() {
|
|
|
3868
3868
|
},
|
|
3869
3869
|
async getProfessionals() {
|
|
3870
3870
|
const { core } = getClients();
|
|
3871
|
-
const { data, error } = await core.from("
|
|
3871
|
+
const { data, error } = await core.from("people").select("id, name, avatar_url, is_active").eq("kind", "staff").eq("is_active", true).order("name");
|
|
3872
3872
|
if (error) throw error;
|
|
3873
3873
|
return (data ?? []).map((p) => ({
|
|
3874
3874
|
id: p.id,
|
|
@@ -3890,23 +3890,23 @@ function createSupabaseAgendaProvider() {
|
|
|
3890
3890
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3891
3891
|
const cutoff = /* @__PURE__ */ new Date();
|
|
3892
3892
|
cutoff.setDate(cutoff.getDate() + daysAhead);
|
|
3893
|
-
const { data, error } = await pub.from("
|
|
3893
|
+
const { data, error } = await pub.from("v_appointments").select("*").eq("status", "scheduled").gte("starts_at", now).lte("starts_at", cutoff.toISOString()).order("starts_at");
|
|
3894
3894
|
if (error) throw error;
|
|
3895
3895
|
return (data ?? []).map(mapBooking);
|
|
3896
3896
|
},
|
|
3897
3897
|
async sendConfirmation(bookingId, channel) {
|
|
3898
3898
|
const { core } = getClients();
|
|
3899
|
-
const { data: booking } = await core.from("
|
|
3899
|
+
const { data: booking } = await core.from("appointments").select("metadata").eq("id", bookingId).single();
|
|
3900
3900
|
const metadata = booking?.metadata ?? {};
|
|
3901
3901
|
const confirmations = metadata.confirmations ?? [];
|
|
3902
3902
|
confirmations.push({ channel, sentAt: (/* @__PURE__ */ new Date()).toISOString(), status: "sent" });
|
|
3903
|
-
await core.from("
|
|
3903
|
+
await core.from("appointments").update({ metadata: { ...metadata, confirmations } }).eq("id", bookingId);
|
|
3904
3904
|
},
|
|
3905
3905
|
async completeBooking(bookingId) {
|
|
3906
3906
|
const { core } = getClients();
|
|
3907
|
-
const { data: existing, error: existingErr } = await core.from("
|
|
3907
|
+
const { data: existing, error: existingErr } = await core.from("appointments").select("order_id").eq("id", bookingId).single();
|
|
3908
3908
|
if (existingErr) throw existingErr;
|
|
3909
|
-
const { error } = await core.from("
|
|
3909
|
+
const { error } = await core.from("appointments").update({ status: "completed" }).eq("id", bookingId);
|
|
3910
3910
|
if (error) throw error;
|
|
3911
3911
|
const orderId = existing?.order_id;
|
|
3912
3912
|
const booking = await this.getBookingById(bookingId);
|
|
@@ -4135,7 +4135,6 @@ var holidayEntity = {
|
|
|
4135
4135
|
],
|
|
4136
4136
|
data: {
|
|
4137
4137
|
table: "holidays",
|
|
4138
|
-
schema: "saas_core",
|
|
4139
4138
|
tenantScoped: true
|
|
4140
4139
|
}
|
|
4141
4140
|
};
|
|
@@ -4669,36 +4668,49 @@ var MIGRATION_001_PUBLIC_BOOKING = `-- =========================================
|
|
|
4669
4668
|
-- plugin-agenda 001: booking read model + public (anon) booking surface
|
|
4670
4669
|
--
|
|
4671
4670
|
-- Ships the objects the agenda providers reference but the spine (packages/db
|
|
4672
|
-
-- migrations 001\
|
|
4671
|
+
-- migrations 001\u2013010) does not define:
|
|
4672
|
+
-- \xA70 converted-pool cleanup \u2014 drop the pre-rename v_bookings view / indexes
|
|
4673
4673
|
-- \xA71 order_items columns the admin provider writes (duration/assignee)
|
|
4674
|
-
-- \xA72 public.
|
|
4674
|
+
-- \xA72 public.v_appointments \u2014 admin read model (authenticated, RLS applies)
|
|
4675
4675
|
-- \xA73 public.v_public_services \u2014 anon-readable service catalog
|
|
4676
4676
|
-- \xA74 public.get_available_slots \u2014 anon RPC (mirrors the mock algorithm)
|
|
4677
4677
|
-- \xA75 public.create_public_booking \u2014 anon RPC (validated, race-safe write)
|
|
4678
4678
|
--
|
|
4679
|
+
-- Core v1 lives directly in PUBLIC (no separate core schema): people (was
|
|
4680
|
+
-- persons), appointments (was bookings). This migration is written against public
|
|
4681
|
+
-- names and is idempotent + safe on both fresh and converted pools.
|
|
4682
|
+
--
|
|
4679
4683
|
-- Anon-access design: public booking pages run with the publishable key and no
|
|
4680
4684
|
-- session, so the canonical tenant RLS yields nothing. Anon access is granted
|
|
4681
4685
|
-- ONLY via the column-whitelisted view (\xA73) and SECURITY DEFINER RPCs (\xA74, \xA75)
|
|
4682
4686
|
-- with tight input validation and hardcoded statuses.
|
|
4683
4687
|
-- ============================================================================
|
|
4684
4688
|
|
|
4689
|
+
-- \xA70 \u2014 converted-pool cleanup. The pre-industry-pool shape shipped a v_bookings
|
|
4690
|
+
-- view and persons_/bookings_-named indexes; drop them so this file re-emits the
|
|
4691
|
+
-- canonical v_appointments view + people_/appointments_ indexes without leaving
|
|
4692
|
+
-- duplicates. All guarded with IF EXISTS \u2192 no-ops on a fresh pool.
|
|
4693
|
+
DROP VIEW IF EXISTS public.v_bookings;
|
|
4694
|
+
DROP INDEX IF EXISTS public.persons_tenant_customer_phone;
|
|
4695
|
+
DROP INDEX IF EXISTS public.bookings_assignee_time;
|
|
4696
|
+
|
|
4685
4697
|
-- \xA71 \u2014 columns the agenda provider writes but 004_archetypes lacks
|
|
4686
|
-
ALTER TABLE
|
|
4698
|
+
ALTER TABLE public.order_items
|
|
4687
4699
|
ADD COLUMN IF NOT EXISTS duration_minutes integer,
|
|
4688
|
-
ADD COLUMN IF NOT EXISTS assignee_id uuid REFERENCES
|
|
4700
|
+
ADD COLUMN IF NOT EXISTS assignee_id uuid REFERENCES public.people(id) ON DELETE SET NULL;
|
|
4689
4701
|
|
|
4690
4702
|
-- Upsert key for public customers (per tenant, by normalized phone)
|
|
4691
|
-
CREATE UNIQUE INDEX IF NOT EXISTS
|
|
4692
|
-
ON
|
|
4703
|
+
CREATE UNIQUE INDEX IF NOT EXISTS people_tenant_customer_phone
|
|
4704
|
+
ON public.people(tenant_id, phone)
|
|
4693
4705
|
WHERE kind = 'customer' AND phone IS NOT NULL;
|
|
4694
4706
|
|
|
4695
4707
|
-- Slot-conflict scan
|
|
4696
|
-
CREATE INDEX IF NOT EXISTS
|
|
4697
|
-
ON
|
|
4708
|
+
CREATE INDEX IF NOT EXISTS appointments_assignee_time
|
|
4709
|
+
ON public.appointments(assignee_id, starts_at, ends_at)
|
|
4698
4710
|
WHERE status NOT IN ('cancelled', 'no_show');
|
|
4699
4711
|
|
|
4700
4712
|
-- \xA72 \u2014 admin read model (security_invoker \u2192 underlying RLS applies; anon sees 0 rows)
|
|
4701
|
-
CREATE OR REPLACE VIEW public.
|
|
4713
|
+
CREATE OR REPLACE VIEW public.v_appointments
|
|
4702
4714
|
WITH (security_invoker = true) AS
|
|
4703
4715
|
SELECT
|
|
4704
4716
|
b.id, b.tenant_id, b.kind, b.starts_at, b.ends_at, b.status, b.notes,
|
|
@@ -4716,14 +4728,14 @@ SELECT
|
|
|
4716
4728
|
o.status AS order_status,
|
|
4717
4729
|
(EXTRACT(EPOCH FROM (COALESCE(b.ends_at, b.starts_at) - b.starts_at))::int / 60)
|
|
4718
4730
|
AS total_duration_minutes
|
|
4719
|
-
FROM
|
|
4720
|
-
LEFT JOIN
|
|
4721
|
-
LEFT JOIN
|
|
4722
|
-
LEFT JOIN
|
|
4723
|
-
LEFT JOIN
|
|
4731
|
+
FROM public.appointments b
|
|
4732
|
+
LEFT JOIN public.people pc ON pc.id = b.party_id
|
|
4733
|
+
LEFT JOIN public.people ps ON ps.id = b.assignee_id
|
|
4734
|
+
LEFT JOIN public.locations l ON l.id = b.location_id
|
|
4735
|
+
LEFT JOIN public.orders o ON o.id = b.order_id;
|
|
4724
4736
|
|
|
4725
|
-
GRANT SELECT ON public.
|
|
4726
|
-
REVOKE SELECT ON public.
|
|
4737
|
+
GRANT SELECT ON public.v_appointments TO authenticated, service_role;
|
|
4738
|
+
REVOKE SELECT ON public.v_appointments FROM anon;
|
|
4727
4739
|
|
|
4728
4740
|
-- \xA73 \u2014 anon-readable public catalog (owner-rights view = deliberate, whitelisted)
|
|
4729
4741
|
CREATE OR REPLACE VIEW public.v_public_services AS
|
|
@@ -4732,7 +4744,7 @@ SELECT
|
|
|
4732
4744
|
COALESCE(s.duration_minutes, 30) AS duration_minutes,
|
|
4733
4745
|
s.image_url,
|
|
4734
4746
|
COALESCE((s.metadata->>'sort_order')::int, 0) AS sort_order
|
|
4735
|
-
FROM
|
|
4747
|
+
FROM public.services s
|
|
4736
4748
|
WHERE s.is_active AND s.status = 'active';
|
|
4737
4749
|
|
|
4738
4750
|
GRANT SELECT ON public.v_public_services TO anon, authenticated, service_role;
|
|
@@ -4746,7 +4758,7 @@ CREATE OR REPLACE FUNCTION public.get_available_slots(
|
|
|
4746
4758
|
p_slot_interval integer DEFAULT 30
|
|
4747
4759
|
) RETURNS TABLE (slot_start timestamptz, slot_end timestamptz)
|
|
4748
4760
|
LANGUAGE plpgsql STABLE SECURITY DEFINER
|
|
4749
|
-
SET search_path =
|
|
4761
|
+
SET search_path = public
|
|
4750
4762
|
AS $$
|
|
4751
4763
|
DECLARE
|
|
4752
4764
|
v_tz text;
|
|
@@ -4794,7 +4806,7 @@ BEGIN
|
|
|
4794
4806
|
SELECT DISTINCT c.s_start, c.s_end
|
|
4795
4807
|
FROM candidate c
|
|
4796
4808
|
WHERE NOT EXISTS (
|
|
4797
|
-
SELECT 1 FROM
|
|
4809
|
+
SELECT 1 FROM appointments b
|
|
4798
4810
|
WHERE b.tenant_id = p_tenant_id
|
|
4799
4811
|
AND b.assignee_id = c.assignee_id
|
|
4800
4812
|
AND b.status NOT IN ('cancelled', 'no_show')
|
|
@@ -4820,7 +4832,7 @@ CREATE OR REPLACE FUNCTION public.create_public_booking(
|
|
|
4820
4832
|
p_notes text DEFAULT NULL
|
|
4821
4833
|
) RETURNS TABLE (booking_id uuid, starts_at timestamptz, ends_at timestamptz)
|
|
4822
4834
|
LANGUAGE plpgsql SECURITY DEFINER
|
|
4823
|
-
SET search_path =
|
|
4835
|
+
SET search_path = public
|
|
4824
4836
|
AS $$
|
|
4825
4837
|
DECLARE
|
|
4826
4838
|
v_service record;
|
|
@@ -4882,7 +4894,7 @@ BEGIN
|
|
|
4882
4894
|
PERFORM pg_advisory_xact_lock(hashtextextended(p_tenant_id::text || v_assignee::text, 0));
|
|
4883
4895
|
|
|
4884
4896
|
IF EXISTS (
|
|
4885
|
-
SELECT 1 FROM
|
|
4897
|
+
SELECT 1 FROM appointments b
|
|
4886
4898
|
WHERE b.tenant_id = p_tenant_id
|
|
4887
4899
|
AND b.assignee_id = v_assignee
|
|
4888
4900
|
AND b.status NOT IN ('cancelled', 'no_show')
|
|
@@ -4894,8 +4906,8 @@ BEGIN
|
|
|
4894
4906
|
|
|
4895
4907
|
-- anti-spam: cap open future public bookings per phone per tenant
|
|
4896
4908
|
IF (
|
|
4897
|
-
SELECT count(*) FROM
|
|
4898
|
-
JOIN
|
|
4909
|
+
SELECT count(*) FROM appointments b
|
|
4910
|
+
JOIN people p ON p.id = b.party_id
|
|
4899
4911
|
WHERE b.tenant_id = p_tenant_id
|
|
4900
4912
|
AND p.phone = v_phone
|
|
4901
4913
|
AND b.starts_at > now()
|
|
@@ -4904,14 +4916,14 @@ BEGIN
|
|
|
4904
4916
|
RAISE EXCEPTION 'too many open bookings for this phone';
|
|
4905
4917
|
END IF;
|
|
4906
4918
|
|
|
4907
|
-
-- upsert the customer by (tenant, phone) \u2014 matches
|
|
4908
|
-
INSERT INTO
|
|
4919
|
+
-- upsert the customer by (tenant, phone) \u2014 matches people_tenant_customer_phone
|
|
4920
|
+
INSERT INTO people (tenant_id, kind, name, phone, email, metadata)
|
|
4909
4921
|
VALUES (p_tenant_id, 'customer', trim(p_name), v_phone, p_email,
|
|
4910
4922
|
jsonb_build_object('source', 'public_booking'))
|
|
4911
4923
|
ON CONFLICT (tenant_id, phone) WHERE kind = 'customer' AND phone IS NOT NULL
|
|
4912
4924
|
DO UPDATE SET
|
|
4913
4925
|
name = EXCLUDED.name,
|
|
4914
|
-
email = COALESCE(EXCLUDED.email,
|
|
4926
|
+
email = COALESCE(EXCLUDED.email, people.email),
|
|
4915
4927
|
updated_at = now()
|
|
4916
4928
|
RETURNING id INTO v_person_id;
|
|
4917
4929
|
|
|
@@ -4924,7 +4936,7 @@ BEGIN
|
|
|
4924
4936
|
'contactName', trim(p_name)))
|
|
4925
4937
|
RETURNING id INTO v_order;
|
|
4926
4938
|
|
|
4927
|
-
INSERT INTO
|
|
4939
|
+
INSERT INTO appointments (tenant_id, kind, party_id, assignee_id, order_id,
|
|
4928
4940
|
starts_at, ends_at, status, notes, metadata)
|
|
4929
4941
|
VALUES (p_tenant_id, 'appointment', v_person_id, v_assignee, v_order,
|
|
4930
4942
|
p_starts_at, v_ends, 'scheduled', v_notes,
|
|
@@ -4971,8 +4983,8 @@ function createFinancialBridge(provider2) {
|
|
|
4971
4983
|
if (!supabase) return result;
|
|
4972
4984
|
try {
|
|
4973
4985
|
const [{ data: orders }, { data: movements }] = await Promise.all([
|
|
4974
|
-
supabase.
|
|
4975
|
-
supabase.from("
|
|
4986
|
+
supabase.from("orders").select("id, kind, status, total, reference_number, metadata").in("id", orderIds),
|
|
4987
|
+
supabase.from("plg_financial_movements").select("invoice_id, status, paid_amount").in("invoice_id", orderIds)
|
|
4976
4988
|
]);
|
|
4977
4989
|
const movementsByOrderId = /* @__PURE__ */ new Map();
|
|
4978
4990
|
for (const movement of movements ?? []) {
|
|
@@ -5000,8 +5012,8 @@ function createFinancialBridge(provider2) {
|
|
|
5000
5012
|
const supabase = core.getSupabaseClientOptional();
|
|
5001
5013
|
if (!supabase) return null;
|
|
5002
5014
|
const [orderRes, movRes] = await Promise.all([
|
|
5003
|
-
supabase.
|
|
5004
|
-
supabase.from("
|
|
5015
|
+
supabase.from("orders").select("id, status, total, reference_number, metadata").eq("id", orderId).single(),
|
|
5016
|
+
supabase.from("plg_financial_movements").select("*").eq("invoice_id", orderId).order("due_date")
|
|
5005
5017
|
]);
|
|
5006
5018
|
const order = orderRes.data;
|
|
5007
5019
|
if (!order) return null;
|
|
@@ -5009,7 +5021,7 @@ function createFinancialBridge(provider2) {
|
|
|
5009
5021
|
const typeIds = [...new Set((movRes.data ?? []).map((m) => m.payment_method_type_id).filter(Boolean))];
|
|
5010
5022
|
let typeNames = {};
|
|
5011
5023
|
if (typeIds.length > 0) {
|
|
5012
|
-
const { data: types } = await supabase.from("
|
|
5024
|
+
const { data: types } = await supabase.from("plg_financial_payment_method_types").select("id, name").in("id", typeIds);
|
|
5013
5025
|
typeNames = Object.fromEntries((types ?? []).map((t) => [t.id, t.name]));
|
|
5014
5026
|
}
|
|
5015
5027
|
const movements = (movRes.data ?? []).map((m) => ({
|
|
@@ -5061,17 +5073,17 @@ function createFinancialBridge(provider2) {
|
|
|
5061
5073
|
async createCommissionMovement(orderId, options) {
|
|
5062
5074
|
const supabase = core.getSupabaseClientOptional();
|
|
5063
5075
|
if (!supabase) return null;
|
|
5064
|
-
const { data: order } = await supabase.
|
|
5076
|
+
const { data: order } = await supabase.from("orders").select("id, tenant_id, assignee_id, total").eq("id", orderId).single();
|
|
5065
5077
|
if (!order || !order.assignee_id) return null;
|
|
5066
5078
|
const { data: staff } = await supabase.from("v_staff").select("commission_rate").eq("id", order.assignee_id).single();
|
|
5067
5079
|
const rate = Number(staff?.commission_rate) || 0;
|
|
5068
5080
|
const base = Number(order.total) || 0;
|
|
5069
5081
|
const amount = computeCommissionAmount(base, rate);
|
|
5070
5082
|
if (amount <= 0) return null;
|
|
5071
|
-
const { data: existing } = await supabase.from("
|
|
5083
|
+
const { data: existing } = await supabase.from("plg_financial_movements").select("id").eq("invoice_id", orderId).eq("movement_kind", "commission").limit(1);
|
|
5072
5084
|
if (existing && existing.length > 0) return existing[0].id;
|
|
5073
5085
|
const dueDate = options?.dueDate ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
5074
|
-
const { data: inserted } = await supabase.from("
|
|
5086
|
+
const { data: inserted } = await supabase.from("plg_financial_movements").insert({
|
|
5075
5087
|
tenant_id: order.tenant_id,
|
|
5076
5088
|
invoice_id: orderId,
|
|
5077
5089
|
direction: "debit",
|
|
@@ -5508,7 +5520,7 @@ function createAgendaPlugin(options) {
|
|
|
5508
5520
|
id: "agenda-001-public-booking",
|
|
5509
5521
|
version: "0.3.0",
|
|
5510
5522
|
sql: MIGRATION_001_PUBLIC_BOOKING,
|
|
5511
|
-
description: "Booking read model (
|
|
5523
|
+
description: "Booking read model (v_appointments) + anon public booking surface (v_public_services, get_available_slots, create_public_booking) + order_items duration/assignee columns"
|
|
5512
5524
|
}
|
|
5513
5525
|
],
|
|
5514
5526
|
locales: agendaLocales
|