@@ -135,7 +127,7 @@
- {toDisplayDateTime(new Date(cancelling.appointmentDate), {
+ {toDisplayDateTime(utcToLocalWithoutDST(new Date(cancelling.appointmentDate)), {
year: "numeric",
month: "long",
day: "numeric",
diff --git a/src/routes/(pages)/dashboard/+layout.svelte b/src/routes/(pages)/dashboard/+layout.svelte
index 5b16fef..acde2c7 100644
--- a/src/routes/(pages)/dashboard/+layout.svelte
+++ b/src/routes/(pages)/dashboard/+layout.svelte
@@ -23,12 +23,11 @@
updateStores();
if (!intervalData) {
- refreshSession();
intervalData = setInterval(updateStores, 2 * 60 * 1000); // 2 minutes
}
if (!intervalSession) {
refreshSession();
- intervalSession = setInterval(refreshSession, 5 * 60 * 1000); // 5 minutes
+ intervalSession = setInterval(refreshSession, 10 * 60 * 1000); // 10 minutes
}
const unsubscribe = () => {
diff --git a/src/routes/(pages)/dashboard/calendar/(components)/AppointmentDetail.svelte b/src/routes/(pages)/dashboard/calendar/(components)/AppointmentDetail.svelte
index c0636cc..71df577 100644
--- a/src/routes/(pages)/dashboard/calendar/(components)/AppointmentDetail.svelte
+++ b/src/routes/(pages)/dashboard/calendar/(components)/AppointmentDetail.svelte
@@ -1,13 +1,12 @@
+
+ {m["settings.title"]()} - OpenReception
+
+
{
channelId: validatedData.channelId,
agentId: validatedData.agentId,
appointmentDate: new Date(validatedData.appointmentDate),
+ timezone: validatedData.appointmentTimeZone,
duration: validatedData.duration,
encryptedPayload: validatedData.encryptedAppointment.encryptedPayload,
iv: validatedData.encryptedAppointment.iv,
@@ -266,6 +268,7 @@ export const POST: RequestHandler = async ({ request, params }) => {
channelId: appointment.channelId,
agentId: appointment.agentId,
appointmentDate: appointment.appointmentDate,
+ timezone: appointment.timezone,
expiryDate: appointment.expiryDate,
status: appointment.status,
encryptedPayload: appointment.encryptedPayload,
@@ -287,6 +290,7 @@ export const POST: RequestHandler = async ({ request, params }) => {
const response: AppointmentResponse = {
id: result.id,
appointmentDate: result.appointmentDate.toISOString(),
+ appointmentTimeZone: result.timezone,
status: result.status,
};
diff --git a/src/routes/api/tenants/[id]/appointments/create-new-client/+server.ts b/src/routes/api/tenants/[id]/appointments/create-new-client/+server.ts
index 9061d68..5d89eff 100644
--- a/src/routes/api/tenants/[id]/appointments/create-new-client/+server.ts
+++ b/src/routes/api/tenants/[id]/appointments/create-new-client/+server.ts
@@ -22,6 +22,7 @@ const requestSchema = z.object({
channelId: z.string(),
agentId: z.string(),
appointmentDate: z.string(),
+ appointmentTimeZone: z.string(),
duration: z.number().int().positive(),
emailHash: z.string(),
clientEmail: z.email().optional(),
@@ -136,6 +137,11 @@ registerOpenAPIRoute("/tenants/{id}/appointments/create-new-client", "POST", {
format: "date-time",
description: "Appointment date and time (ISO 8601)",
},
+ appointmentTimezone: {
+ type: "string",
+ format: "IANA Timezone",
+ description: "Time zone of the appointment (e.g. Europe/Berlin)",
+ },
emailHash: {
type: "string",
description: "SHA-256 hash of client email",
diff --git a/src/routes/api/tenants/[id]/appointments/create-new-client/__tests__/create-new-client-api.test.ts b/src/routes/api/tenants/[id]/appointments/create-new-client/__tests__/create-new-client-api.test.ts
index c2be121..2376c4f 100644
--- a/src/routes/api/tenants/[id]/appointments/create-new-client/__tests__/create-new-client-api.test.ts
+++ b/src/routes/api/tenants/[id]/appointments/create-new-client/__tests__/create-new-client-api.test.ts
@@ -36,6 +36,7 @@ describe("Create New Client API Route", () => {
channelId: mockChannelId,
agentId: "agent-123", // Missing field added
appointmentDate: "2024-12-25T14:30:00.000Z",
+ appointmentTimeZone: "Europe/Berlin",
duration: 10,
emailHash: "test-email-hash",
clientEmail: "test@example.com",
diff --git a/src/routes/api/tenants/[id]/appointments/staff-create/+server.ts b/src/routes/api/tenants/[id]/appointments/staff-create/+server.ts
index e31dbe7..fe85812 100644
--- a/src/routes/api/tenants/[id]/appointments/staff-create/+server.ts
+++ b/src/routes/api/tenants/[id]/appointments/staff-create/+server.ts
@@ -16,6 +16,7 @@ const requestSchema = z
// Appointment details
appointmentDate: z.string(),
+ appointmentTimeZone: z.string(),
duration: z.number().int().positive(),
channelId: z.string(),
agentId: z.string(),
@@ -315,6 +316,7 @@ export const POST: RequestHandler = async ({ params, request, locals }) => {
channelId: validatedData.channelId,
agentId: validatedData.agentId,
appointmentDate: validatedData.appointmentDate,
+ appointmentTimeZone: validatedData.appointmentTimeZone,
duration: validatedData.duration,
clientEmail: validatedData.clientEmail || "",
clientLanguage: validatedData.clientLanguage,
@@ -349,6 +351,7 @@ export const POST: RequestHandler = async ({ params, request, locals }) => {
channelId: validatedData.channelId,
agentId: validatedData.agentId,
appointmentDate: validatedData.appointmentDate,
+ appointmentTimeZone: validatedData.appointmentTimeZone,
duration: validatedData.duration,
emailHash: validatedData.emailHash,
clientEmail: validatedData.clientEmail || "",
diff --git a/src/routes/api/tenants/[id]/appointments/staff-create/__tests__/staff-create.test.ts b/src/routes/api/tenants/[id]/appointments/staff-create/__tests__/staff-create.test.ts
index 4dd43b6..12b8633 100644
--- a/src/routes/api/tenants/[id]/appointments/staff-create/__tests__/staff-create.test.ts
+++ b/src/routes/api/tenants/[id]/appointments/staff-create/__tests__/staff-create.test.ts
@@ -68,6 +68,7 @@ describe("POST /api/tenants/[id]/appointments/staff-create", () => {
clientEmail: "test@example.com",
emailHash,
appointmentDate: "2026-01-15T14:00:00.000Z",
+ appointmentTimeZone: "Europe/Berlin",
duration: 30,
channelId: "channel-123",
agentId: "agent-123",
@@ -108,6 +109,7 @@ describe("POST /api/tenants/[id]/appointments/staff-create", () => {
channelId: "channel-123",
agentId: "agent-123",
appointmentDate: "2026-01-15T14:00:00.000Z",
+ appointmentTimeZone: "Europe/Berlin",
duration: 30,
emailHash,
clientEmail: "test@example.com",
@@ -151,6 +153,7 @@ describe("POST /api/tenants/[id]/appointments/staff-create", () => {
hasNoEmail: true,
emailHash: "unique-hash-for-no-email",
appointmentDate: "2026-01-15T14:00:00.000Z",
+ appointmentTimeZone: "Europe/Berlin",
duration: 30,
channelId: "channel-123",
agentId: "agent-123",
@@ -214,6 +217,7 @@ describe("POST /api/tenants/[id]/appointments/staff-create", () => {
clientEmail: "existing@example.com",
emailHash,
appointmentDate: "2026-01-15T14:00:00.000Z",
+ appointmentTimeZone: "Europe/Berlin",
duration: 30,
channelId: "channel-123",
agentId: "agent-123",
@@ -245,6 +249,7 @@ describe("POST /api/tenants/[id]/appointments/staff-create", () => {
channelId: "channel-123",
agentId: "agent-123",
appointmentDate: "2026-01-15T14:00:00.000Z",
+ appointmentTimeZone: "Europe/Berlin",
duration: 30,
clientEmail: "existing@example.com",
clientLanguage: "de",
@@ -291,6 +296,7 @@ describe("POST /api/tenants/[id]/appointments/staff-create", () => {
clientEmail: "test@example.com",
emailHash,
appointmentDate: "2026-01-15T14:00:00.000Z",
+ appointmentTimeZone: "Europe/Berlin",
duration: 30,
channelId: "channel-123",
agentId: "agent-123",
@@ -387,6 +393,7 @@ describe("POST /api/tenants/[id]/appointments/staff-create", () => {
body: JSON.stringify({
emailHash,
appointmentDate: "2026-01-15T14:00:00.000Z",
+ appointmentTimeZone: "Europe/Berlin",
duration: 30,
channelId: "channel-123",
agentId: "agent-123",
diff --git a/src/routes/api/tenants/[id]/calendar/+server.ts b/src/routes/api/tenants/[id]/calendar/+server.ts
index ff0cac9..b0c8402 100644
--- a/src/routes/api/tenants/[id]/calendar/+server.ts
+++ b/src/routes/api/tenants/[id]/calendar/+server.ts
@@ -33,6 +33,14 @@ registerOpenAPIRoute("/tenants/{id}/calendar", "GET", {
schema: { type: "string", format: "date-time" },
description: "End date for the calendar range (ISO 8601 format with timezone)",
},
+ {
+ name: "timeZone",
+ in: "query",
+ required: false,
+ schema: { type: "string" },
+ description:
+ "IANA timezone name for schedule calculations (e.g., 'Europe/Berlin', 'America/New_York'). Defaults to 'UTC' if not provided. This ensures slots are calculated with the correct daylight saving time offset.",
+ },
],
responses: {
"200": {
@@ -124,6 +132,7 @@ export const GET: RequestHandler = async ({ params, url, locals }) => {
// Parse query parameters for date range
const startDateParam = url.searchParams.get("startDate");
const endDateParam = url.searchParams.get("endDate");
+ const timeZoneParam = url.searchParams.get("timeZone") || "UTC";
if (!startDateParam || !endDateParam) {
throw new ValidationError("Both startDate and endDate query parameters are required");
@@ -153,6 +162,7 @@ export const GET: RequestHandler = async ({ params, url, locals }) => {
tenantId,
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
+ timeZone: timeZoneParam,
});
const scheduleService = await ScheduleService.forTenant(tenantId);
@@ -164,6 +174,7 @@ export const GET: RequestHandler = async ({ params, url, locals }) => {
tenantId,
startDate: startDateParam,
endDate: endDateParam,
+ timeZone: timeZoneParam,
staffUserId,
});
diff --git a/src/routes/api/tenants/[id]/calendar/__tests__/calendar-api.test.ts b/src/routes/api/tenants/[id]/calendar/__tests__/calendar-api.test.ts
index 7fb7e61..c39f778 100644
--- a/src/routes/api/tenants/[id]/calendar/__tests__/calendar-api.test.ts
+++ b/src/routes/api/tenants/[id]/calendar/__tests__/calendar-api.test.ts
@@ -142,6 +142,7 @@ describe("Calendar API", () => {
tenantId: "tenant-123",
startDate: "2024-01-01T00:00:00.000Z",
endDate: "2024-01-02T00:00:00.000Z",
+ timeZone: "UTC",
staffUserId: undefined,
});
});
diff --git a/src/routes/api/tenants/[id]/schedule/+server.ts b/src/routes/api/tenants/[id]/schedule/+server.ts
index ae6bec3..1cb1564 100644
--- a/src/routes/api/tenants/[id]/schedule/+server.ts
+++ b/src/routes/api/tenants/[id]/schedule/+server.ts
@@ -138,6 +138,7 @@ export const GET: RequestHandler = async ({ params, url }) => {
// Parse query parameters for date range
const startDateParam = url.searchParams.get("startDate");
const endDateParam = url.searchParams.get("endDate");
+ const timeZoneParam = url.searchParams.get("timeZone") || "UTC";
const channelIdParam = url.searchParams.get("channel");
const agentIdParam = url.searchParams.get("agent");
@@ -169,6 +170,7 @@ export const GET: RequestHandler = async ({ params, url }) => {
tenantId,
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
+ timeZone: timeZoneParam,
});
const scheduleService = await ScheduleService.forTenant(tenantId);
@@ -178,6 +180,7 @@ export const GET: RequestHandler = async ({ params, url }) => {
endDate: endDateParam,
channelId: channelIdParam || undefined,
agentId: agentIdParam || undefined,
+ timeZone: timeZoneParam,
});
// Transform schedule to client-friendly format (remove appointments, simplify available slots)
diff --git a/src/routes/api/tenants/[id]/schedule/__tests__/schedule-api.test.ts b/src/routes/api/tenants/[id]/schedule/__tests__/schedule-api.test.ts
index 2f5cf8a..2fd3686 100644
--- a/src/routes/api/tenants/[id]/schedule/__tests__/schedule-api.test.ts
+++ b/src/routes/api/tenants/[id]/schedule/__tests__/schedule-api.test.ts
@@ -126,6 +126,7 @@ describe("Schedule API Route", () => {
tenantId: mockTenantId,
startDate: "2024-01-01T00:00:00.000Z",
endDate: "2024-01-07T23:59:59.999Z",
+ timeZone: "UTC",
});
});
@@ -305,6 +306,7 @@ describe("Schedule API Route", () => {
tenantId: mockTenantId,
startDate,
endDate,
+ timeZone: "UTC",
});
});
});
diff --git a/src/server-hooks/secHeaderHandle.ts b/src/server-hooks/secHeaderHandle.ts
index c1fb1e4..7033c6d 100644
--- a/src/server-hooks/secHeaderHandle.ts
+++ b/src/server-hooks/secHeaderHandle.ts
@@ -32,7 +32,7 @@ export const secHeaderHandle: Handle = async ({ event, resolve }) => {
"font-src 'self' data: https://unpkg.com",
"connect-src 'self'",
"media-src 'self'",
- "object-src 'none'",
+ "object-src 'self'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
@@ -40,7 +40,7 @@ export const secHeaderHandle: Handle = async ({ event, resolve }) => {
];
if (!dev) {
cspDirectives.unshift("default-src 'self' 'wasm-unsafe-eval'");
- cspDirectives.push("img-src 'self' data: https:");
+ cspDirectives.push("img-src 'self' data: blob: https:");
}
response.headers.set("Content-Security-Policy", cspDirectives.join("; "));
diff --git a/svelte.config.js b/svelte.config.js
index b5d398f..258012b 100644
--- a/svelte.config.js
+++ b/svelte.config.js
@@ -7,6 +7,7 @@ const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
+ checkOrigin: true,
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
@@ -16,6 +17,18 @@ const config = {
alias: {
$i18n: path.resolve("./src/i18n"),
},
+ csp: {
+ mode: "hash",
+ directives: {
+ "script-src": [
+ "'self'",
+ "'unsafe-inline'",
+ "'wasm-unsafe-eval'",
+ "'unsafe-eval'",
+ "https://unpkg.com",
+ ],
+ },
+ },
},
};
diff --git a/tenant-migrations/0014_fluffy_ezekiel.sql b/tenant-migrations/0014_fluffy_ezekiel.sql
new file mode 100644
index 0000000..870ed9c
--- /dev/null
+++ b/tenant-migrations/0014_fluffy_ezekiel.sql
@@ -0,0 +1,2 @@
+ALTER TABLE "appointment" ADD COLUMN "timezone" text NOT NULL DEFAULT 'UTC';
+ALTER TABLE "appointment" ALTER COLUMN "timezone" DROP DEFAULT;
\ No newline at end of file
diff --git a/tenant-migrations/meta/0014_snapshot.json b/tenant-migrations/meta/0014_snapshot.json
new file mode 100644
index 0000000..ddc4cad
--- /dev/null
+++ b/tenant-migrations/meta/0014_snapshot.json
@@ -0,0 +1,973 @@
+{
+ "id": "7e551bff-abaf-48b5-bb08-0ba14cb3292d",
+ "prevId": "726f8502-95ab-458f-83e5-9897a8b46a03",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.agent": {
+ "name": "agent",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "descriptions": {
+ "name": "descriptions",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "image": {
+ "name": "image",
+ "type": "varchar(250000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "archived": {
+ "name": "archived",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.agent_absence": {
+ "name": "agent_absence",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "agent_id": {
+ "name": "agent_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "start_date": {
+ "name": "start_date",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "end_date": {
+ "name": "end_date",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "absence_type": {
+ "name": "absence_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "agent_absence_agent_id_agent_id_fk": {
+ "name": "agent_absence_agent_id_agent_id_fk",
+ "tableFrom": "agent_absence",
+ "tableTo": "agent",
+ "columnsFrom": ["agent_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.appointment": {
+ "name": "appointment",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "tunnel_id": {
+ "name": "tunnel_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "channel_id": {
+ "name": "channel_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "agent_id": {
+ "name": "agent_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "appointment_date": {
+ "name": "appointment_date",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "duration": {
+ "name": "duration",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "timezone": {
+ "name": "timezone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expiry_date": {
+ "name": "expiry_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "appointment_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "encrypted_data": {
+ "name": "encrypted_data",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "data_key": {
+ "name": "data_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "encrypted_payload": {
+ "name": "encrypted_payload",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "iv": {
+ "name": "iv",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "auth_tag": {
+ "name": "auth_tag",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "appointment_tunnel_id_client_appointment_tunnel_id_fk": {
+ "name": "appointment_tunnel_id_client_appointment_tunnel_id_fk",
+ "tableFrom": "appointment",
+ "tableTo": "client_appointment_tunnel",
+ "columnsFrom": ["tunnel_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "appointment_channel_id_channel_id_fk": {
+ "name": "appointment_channel_id_channel_id_fk",
+ "tableFrom": "appointment",
+ "tableTo": "channel",
+ "columnsFrom": ["channel_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "appointment_agent_id_agent_id_fk": {
+ "name": "appointment_agent_id_agent_id_fk",
+ "tableFrom": "appointment",
+ "tableTo": "agent",
+ "columnsFrom": ["agent_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.appointment_key_share": {
+ "name": "appointment_key_share",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "appointment_id": {
+ "name": "appointment_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "encrypted_key": {
+ "name": "encrypted_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "appointment_key_share_appointment_id_appointment_id_fk": {
+ "name": "appointment_key_share_appointment_id_appointment_id_fk",
+ "tableFrom": "appointment_key_share",
+ "tableTo": "appointment",
+ "columnsFrom": ["appointment_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.auth_challenge": {
+ "name": "auth_challenge",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "challenge": {
+ "name": "challenge",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_hash": {
+ "name": "email_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "consumed": {
+ "name": "consumed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.booking_access_token": {
+ "name": "booking_access_token",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tenant_id": {
+ "name": "tenant_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_hash": {
+ "name": "email_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tunnel_id": {
+ "name": "tunnel_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_public_key": {
+ "name": "client_public_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "consumed": {
+ "name": "consumed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.channel": {
+ "name": "channel",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "names": {
+ "name": "names",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "paused": {
+ "name": "paused",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "descriptions": {
+ "name": "descriptions",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_public": {
+ "name": "is_public",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "requires_confirmation": {
+ "name": "requires_confirmation",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "archived": {
+ "name": "archived",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.channel_agent": {
+ "name": "channel_agent",
+ "schema": "",
+ "columns": {
+ "channel_id": {
+ "name": "channel_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "agent_id": {
+ "name": "agent_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "channel_agent_channel_id_channel_id_fk": {
+ "name": "channel_agent_channel_id_channel_id_fk",
+ "tableFrom": "channel_agent",
+ "tableTo": "channel",
+ "columnsFrom": ["channel_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "channel_agent_agent_id_agent_id_fk": {
+ "name": "channel_agent_agent_id_agent_id_fk",
+ "tableFrom": "channel_agent",
+ "tableTo": "agent",
+ "columnsFrom": ["agent_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.channel_slot_template": {
+ "name": "channel_slot_template",
+ "schema": "",
+ "columns": {
+ "channel_id": {
+ "name": "channel_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slot_template_id": {
+ "name": "slot_template_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "channel_slot_template_channel_id_channel_id_fk": {
+ "name": "channel_slot_template_channel_id_channel_id_fk",
+ "tableFrom": "channel_slot_template",
+ "tableTo": "channel",
+ "columnsFrom": ["channel_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "channel_slot_template_slot_template_id_slotTemplate_id_fk": {
+ "name": "channel_slot_template_slot_template_id_slotTemplate_id_fk",
+ "tableFrom": "channel_slot_template",
+ "tableTo": "slotTemplate",
+ "columnsFrom": ["slot_template_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.channel_staff": {
+ "name": "channel_staff",
+ "schema": "",
+ "columns": {
+ "channel_id": {
+ "name": "channel_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "staff_id": {
+ "name": "staff_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "channel_staff_channel_id_channel_id_fk": {
+ "name": "channel_staff_channel_id_channel_id_fk",
+ "tableFrom": "channel_staff",
+ "tableTo": "channel",
+ "columnsFrom": ["channel_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.client_appointment_tunnel": {
+ "name": "client_appointment_tunnel",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "email_hash": {
+ "name": "email_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_public_key": {
+ "name": "client_public_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "private_key_share": {
+ "name": "private_key_share",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_key_share": {
+ "name": "client_key_share",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "client_appointment_tunnel_email_hash_unique": {
+ "name": "client_appointment_tunnel_email_hash_unique",
+ "nullsNotDistinct": false,
+ "columns": ["email_hash"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.client_pin_reset_token": {
+ "name": "client_pin_reset_token",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "token": {
+ "name": "token",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "email_hash": {
+ "name": "email_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "used": {
+ "name": "used",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "client_pin_reset_token_token_unique": {
+ "name": "client_pin_reset_token_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.client_tunnel_staff_key_share": {
+ "name": "client_tunnel_staff_key_share",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "tunnel_id": {
+ "name": "tunnel_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "encrypted_tunnel_key": {
+ "name": "encrypted_tunnel_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "client_tunnel_staff_key_share_tunnel_id_client_appointment_tunnel_id_fk": {
+ "name": "client_tunnel_staff_key_share_tunnel_id_client_appointment_tunnel_id_fk",
+ "tableFrom": "client_tunnel_staff_key_share",
+ "tableTo": "client_appointment_tunnel",
+ "columnsFrom": ["tunnel_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.notification": {
+ "name": "notification",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "staff_id": {
+ "name": "staff_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "notification_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'APPOINTMENT_CONFIRMED'"
+ },
+ "meta_data": {
+ "name": "meta_data",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_read": {
+ "name": "is_read",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.slotTemplate": {
+ "name": "slotTemplate",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "weekdays": {
+ "name": "weekdays",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "from": {
+ "name": "from",
+ "type": "time",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "to": {
+ "name": "to",
+ "type": "time",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "duration": {
+ "name": "duration",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.staff_crypto": {
+ "name": "staff_crypto",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "public_key": {
+ "name": "public_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "private_key_share": {
+ "name": "private_key_share",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "passkey_id": {
+ "name": "passkey_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.appointment_status": {
+ "name": "appointment_status",
+ "schema": "public",
+ "values": ["NEW", "CONFIRMED", "HELD", "REJECTED", "NO_SHOW"]
+ },
+ "public.notification_type": {
+ "name": "notification_type",
+ "schema": "public",
+ "values": ["APPOINTMENT_CONFIRMED", "APPOINTMENT_CANCELLED", "APPOINTMENT_REQUESTED"]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/tenant-migrations/meta/_journal.json b/tenant-migrations/meta/_journal.json
index 312ab27..fa1c97d 100644
--- a/tenant-migrations/meta/_journal.json
+++ b/tenant-migrations/meta/_journal.json
@@ -99,6 +99,13 @@
"when": 1773231146366,
"tag": "0013_youthful_grim_reaper",
"breakpoints": true
+ },
+ {
+ "idx": 14,
+ "version": "7",
+ "when": 1775654963847,
+ "tag": "0014_fluffy_ezekiel",
+ "breakpoints": true
}
]
}