diff --git a/src/lib/client/appointment-crypto.ts b/src/lib/client/appointment-crypto.ts
index c46ffec..6483f5d 100644
--- a/src/lib/client/appointment-crypto.ts
+++ b/src/lib/client/appointment-crypto.ts
@@ -444,6 +444,8 @@ export class UnifiedAppointmentCrypto {
? `/api/tenants/${tenantId}/appointments/create-new-client`
: `/api/tenants/${tenantId}/appointments/add-to-tunnel`;
+ const appointmentTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
+
const requestData = isFirstAppointment
? {
// New client
@@ -451,6 +453,7 @@ export class UnifiedAppointmentCrypto {
agentId,
channelId,
appointmentDate,
+ appointmentTimeZone,
duration,
emailHash: this.emailHash,
clientEmail: appointmentData.shareEmail ? appointmentData.email : undefined,
@@ -470,6 +473,7 @@ export class UnifiedAppointmentCrypto {
agentId,
channelId,
appointmentDate,
+ appointmentTimeZone,
duration,
clientEmail: appointmentData.shareEmail ? appointmentData.email : undefined,
clientLanguage,
diff --git a/src/lib/emails/AppointmentBooked.svelte b/src/lib/emails/AppointmentBooked.svelte
index 76ef184..b683f98 100644
--- a/src/lib/emails/AppointmentBooked.svelte
+++ b/src/lib/emails/AppointmentBooked.svelte
@@ -48,8 +48,8 @@
{channel}
{appointment.agentName}
- {renderAppointmentDate(appointment.appointmentDate, locale)}
- {renderAppointmentTime(appointment.appointmentDate, locale)}
+ {renderAppointmentDate(appointment.appointmentDate, locale, appointment.timezone)}
+ {renderAppointmentTime(appointment.appointmentDate, locale, appointment.timezone)}
{m["emails.oclock"]()}
{tenant.longName}
diff --git a/src/lib/emails/AppointmentCancelled.svelte b/src/lib/emails/AppointmentCancelled.svelte
index 3052fd1..df8ea47 100644
--- a/src/lib/emails/AppointmentCancelled.svelte
+++ b/src/lib/emails/AppointmentCancelled.svelte
@@ -45,8 +45,8 @@
{channel}
{appointment.agentName}
- {renderAppointmentDate(appointment.appointmentDate, locale)}
- {renderAppointmentTime(appointment.appointmentDate, locale)}
+ {renderAppointmentDate(appointment.appointmentDate, locale, appointment.timezone)}
+ {renderAppointmentTime(appointment.appointmentDate, locale, appointment.timezone)}
{m["emails.oclock"]()}
{tenant.longName}
diff --git a/src/lib/emails/AppointmentRejected.svelte b/src/lib/emails/AppointmentRejected.svelte
index 9df32f8..a2b496a 100644
--- a/src/lib/emails/AppointmentRejected.svelte
+++ b/src/lib/emails/AppointmentRejected.svelte
@@ -45,8 +45,8 @@
{channel}
{appointment.agentName}
- {renderAppointmentDate(appointment.appointmentDate, locale)}
- {renderAppointmentTime(appointment.appointmentDate, locale)}
+ {renderAppointmentDate(appointment.appointmentDate, locale, appointment.timezone)}
+ {renderAppointmentTime(appointment.appointmentDate, locale, appointment.timezone)}
{m["emails.oclock"]()}
{tenant.longName}
diff --git a/src/lib/emails/AppointmentReminder.svelte b/src/lib/emails/AppointmentReminder.svelte
index 0d51729..ca17a30 100644
--- a/src/lib/emails/AppointmentReminder.svelte
+++ b/src/lib/emails/AppointmentReminder.svelte
@@ -48,8 +48,8 @@
{channel}
{appointment.agentName}
- {renderAppointmentDate(appointment.appointmentDate, locale)}
- {renderAppointmentTime(appointment.appointmentDate, locale)}
+ {renderAppointmentDate(appointment.appointmentDate, locale, appointment.timezone)}
+ {renderAppointmentTime(appointment.appointmentDate, locale, appointment.timezone)}
{m["emails.oclock"]()}
{tenant.longName}
diff --git a/src/lib/emails/AppointmentRequest.svelte b/src/lib/emails/AppointmentRequest.svelte
index dcc96a0..404be16 100644
--- a/src/lib/emails/AppointmentRequest.svelte
+++ b/src/lib/emails/AppointmentRequest.svelte
@@ -45,8 +45,8 @@
{channel}
{appointment.agentName}
- {renderAppointmentDate(appointment.appointmentDate, locale)}
- {renderAppointmentTime(appointment.appointmentDate, locale)}
+ {renderAppointmentDate(appointment.appointmentDate, locale, appointment.timezone)}
+ {renderAppointmentTime(appointment.appointmentDate, locale, appointment.timezone)}
{m["emails.oclock"]()}
{tenant.longName}
diff --git a/src/lib/emails/utils.ts b/src/lib/emails/utils.ts
index 73f4154..768f285 100644
--- a/src/lib/emails/utils.ts
+++ b/src/lib/emails/utils.ts
@@ -1,6 +1,4 @@
import type { SupportedLocale } from "$lib/const/locales";
-import { format, type Locale } from "date-fns";
-import { de, enUS } from "date-fns/locale";
// is not exported from svelte
export type RenderOutput = {
@@ -68,15 +66,29 @@ export const htmlToText = (html: string) => {
return text;
};
-const localeMap: { [key: string]: Locale } = {
- en: enUS,
- de: de,
+export const renderAppointmentDate = (date: Date, locale: SupportedLocale, timezone: string) => {
+ return new Intl.DateTimeFormat(locale, {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ timeZone: timezone,
+ }).format(date);
};
-export const renderAppointmentDate = (date: Date, locale: SupportedLocale) => {
- return format(date, "PPP", { locale: localeMap[locale] as unknown as Locale });
-};
+export const renderAppointmentTime = (date: Date, locale: SupportedLocale, timezone: string) => {
+ const time = new Intl.DateTimeFormat(locale, {
+ hour: "numeric",
+ minute: "2-digit",
+ timeZone: timezone,
+ }).format(date);
-export const renderAppointmentTime = (date: Date, locale: SupportedLocale) => {
- return format(date, "p", { locale: localeMap[locale] as unknown as Locale });
+ const timeZoneName =
+ new Intl.DateTimeFormat(locale, {
+ timeZone: timezone,
+ timeZoneName: "long",
+ })
+ .formatToParts(date)
+ .find((part) => part.type === "timeZoneName")?.value ?? timezone;
+
+ return `${time}, ${timeZoneName}`;
};
diff --git a/src/lib/server/db/tenant-schema.ts b/src/lib/server/db/tenant-schema.ts
index 64eaf7a..2f345bb 100644
--- a/src/lib/server/db/tenant-schema.ts
+++ b/src/lib/server/db/tenant-schema.ts
@@ -171,6 +171,8 @@ export const appointment = pgTable("appointment", {
appointmentDate: timestamp("appointment_date").notNull(),
/** Duration of the appointment in minutes */
duration: integer("duration").notNull(),
+ /** Timezone of client booking appointment */
+ timezone: text("timezone").notNull(),
/** When appointment data expires and can be auto-deleted */
expiryDate: date("expiry_date"),
/** Current status of the appointment - defaults depend on channel's requiresConfirmation setting */
diff --git a/src/lib/server/services/appointment-service.ts b/src/lib/server/services/appointment-service.ts
index 5a03571..e0be8a6 100644
--- a/src/lib/server/services/appointment-service.ts
+++ b/src/lib/server/services/appointment-service.ts
@@ -24,6 +24,7 @@ export interface ClientTunnelData {
channelId: string;
agentId: string;
appointmentDate: string;
+ appointmentTimeZone: string;
duration: number;
emailHash: string;
clientEmail?: string;
@@ -500,6 +501,7 @@ export class AppointmentService {
channelId: string;
agentId: string;
appointmentDate: string;
+ appointmentTimeZone: string;
duration: number;
clientEmail: string;
clientLanguage?: string;
@@ -567,6 +569,7 @@ export class AppointmentService {
channelId: appointmentData.channelId,
agentId: appointmentData.agentId,
appointmentDate: new Date(appointmentData.appointmentDate),
+ timezone: appointmentData.appointmentTimeZone,
duration: appointmentData.duration,
encryptedPayload: appointmentData.encryptedAppointment.encryptedPayload,
iv: appointmentData.encryptedAppointment.iv,
@@ -588,6 +591,7 @@ export class AppointmentService {
const response: AppointmentResponse = {
id: result.id,
appointmentDate: result.appointmentDate.toISOString(),
+ appointmentTimeZone: appointmentData.appointmentTimeZone,
status: result.status,
requiresConfirmation,
};
@@ -716,6 +720,7 @@ export class AppointmentService {
channelId: clientData.channelId,
agentId: clientData.agentId,
appointmentDate: new Date(clientData.appointmentDate),
+ timezone: clientData.appointmentTimeZone,
duration: clientData.duration,
encryptedPayload: clientData.encryptedAppointment.encryptedPayload,
iv: clientData.encryptedAppointment.iv,
@@ -725,6 +730,7 @@ export class AppointmentService {
.returning({
id: tenantSchema.appointment.id,
appointmentDate: tenantSchema.appointment.appointmentDate,
+ timezone: tenantSchema.appointment.timezone,
status: tenantSchema.appointment.status,
tunnelId: tenantSchema.appointment.tunnelId,
channelId: tenantSchema.appointment.channelId,
@@ -758,6 +764,7 @@ export class AppointmentService {
const response: AppointmentResponse = {
id: result.appointment.id,
appointmentDate: result.appointment.appointmentDate.toISOString(),
+ appointmentTimeZone: result.appointment.timezone,
status: result.appointment.status,
requiresConfirmation: result.requiresConfirmation,
};
diff --git a/src/lib/types/appointment.ts b/src/lib/types/appointment.ts
index 0539dc0..44ed87d 100644
--- a/src/lib/types/appointment.ts
+++ b/src/lib/types/appointment.ts
@@ -91,6 +91,7 @@ export interface AddAppointmentToTunnelRequest {
emailHash: string;
tunnelId: string;
appointmentDate: string;
+ appointmentTimeZone: string;
encryptedAppointment: EncryptedAppointmentData;
}
@@ -132,6 +133,7 @@ export interface ClientAppointmentsTunnel {
export interface AppointmentResponse {
id: string;
appointmentDate: string;
+ appointmentTimeZone: string;
status: "NEW" | "CONFIRMED" | "HELD" | "REJECTED" | "NO_SHOW";
requiresConfirmation?: boolean;
}
diff --git a/src/routes/api/tenants/[id]/appointments/add-to-tunnel/+server.ts b/src/routes/api/tenants/[id]/appointments/add-to-tunnel/+server.ts
index 5259796..615dd2f 100644
--- a/src/routes/api/tenants/[id]/appointments/add-to-tunnel/+server.ts
+++ b/src/routes/api/tenants/[id]/appointments/add-to-tunnel/+server.ts
@@ -27,6 +27,7 @@ const requestSchema = z.object({
channelId: z.string(),
agentId: z.string(),
appointmentDate: z.string(),
+ appointmentTimeZone: z.string(),
duration: z.number().int().positive(),
clientEmail: z.email().optional(),
clientLanguage: z.string().optional().default("en"),
@@ -254,6 +255,7 @@ export const POST: RequestHandler = async ({ request, params }) => {
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/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..2b2e839
--- /dev/null
+++ b/tenant-migrations/meta/0014_snapshot.json
@@ -0,0 +1,1031 @@
+{
+ "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": {}
+ }
+}
\ No newline at end of file
diff --git a/tenant-migrations/meta/_journal.json b/tenant-migrations/meta/_journal.json
index 312ab27..5b44ca7 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
}
]
-}
+}
\ No newline at end of file