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