Files
appointment-booking-software/src/lib/server/db/tenant-schema.ts
T
5965fada38 Make descriptions multilingual (#89)
* Make descriptions multilingual

* Format and test fixes

* Update src/lib/server/db/tenant-schema.ts

* Fix tenant-switcher to update instantly

* Fix sql migrations for description

* Add languages to tenant table

* Fix tenant schema

* Use objects for translated fields

* Fix format

* Fix lint

* Allow longer descriptions for channels

* Field Localizations are based on tenant locales not supportedLocales

* Fix agent image list preview

* Improve language tab keyboard navigation

* Fix falsy redirect after store init

---------

Co-authored-by: Karl Ludwig Weise <office@ludwigweise.de>
Co-authored-by: Karl Ludwig Weise <ludwig@ludwigweise.de>
2025-10-01 18:43:01 +02:00

225 lines
8.1 KiB
TypeScript

import type { InferSelectModel } from "drizzle-orm";
import {
pgTable,
boolean,
uuid,
text,
date,
pgEnum,
time,
integer,
json,
timestamp,
varchar,
} from "drizzle-orm/pg-core";
/**
* Database enums for tenant-specific entities
*/
/** Appointment status enumeration - tracks the lifecycle of appointments */
export const appointmentStatusEnum = pgEnum("appointment_status", [
"NEW",
"CONFIRMED",
"HELD",
"REJECTED",
"NO_SHOW",
]);
/**
* Agent table - represents personnel or staff members who can be assigned to channels
* Agents are the people who provide services and can be associated with multiple channels
* Stored in tenant-specific database
* @table agent
*/
export const agent = pgTable("agent", {
/** Primary key - unique identifier */
id: uuid("id").primaryKey().defaultRandom(),
/** Agent's display name */
name: text("name").notNull(),
/** Optional description of the agent's role or specialties */
descriptions: json("descriptions").$type<{ [key: string]: string }>().notNull(),
/** Optional logo/profile image for the agent (PNG, JPEG, GIF, or WEBP) */
image: varchar("image", { length: 250_000 }),
});
/**
* Channel table - represents bookable resources (rooms, machines, personnel)
* Channels define what can be booked and under what conditions
* Can be associated with multiple agents and slot templates
* Stored in tenant-specific database
* @table channel
*/
export const channel = pgTable("channel", {
/** Primary key - unique identifier */
id: uuid("id").primaryKey().defaultRandom(),
/** Channel display names in multiple languages (array of strings in same order as languages) */
names: json("names").$type<{ [key: string]: string }>().notNull(),
/** Optional color for UI display (hex code) */
color: text("color"),
/** Whether the channel is paused and does not offer nor accept new appointments */
pause: boolean("paused").notNull().default(false),
/** Optional descriptions in multiple languages (array of strings in same order as languages) */
descriptions: json("descriptions").$type<{ [key: string]: string }>().notNull(),
/** Whether channel is publicly bookable or requires internal access */
isPublic: boolean("is_public"),
/** Whether appointments must be explicitly confirmed by staff */
requiresConfirmation: boolean("requires_confirmation"),
});
/**
* Slot Template table - defines recurring time slots for appointment booking
* Templates specify when appointments can be scheduled on specific weekdays
* Can be associated with multiple channels to define their availability
* Stored in tenant-specific database
* @table slotTemplate
*/
export const slotTemplate = pgTable("slotTemplate", {
/** Primary key - unique identifier */
id: uuid("id").primaryKey().defaultRandom(),
/** Bitmask for weekdays (1=Monday, 2=Tuesday, 4=Wednesday, etc.) */
weekdays: integer("weekdays"),
/** Start time for the slot template */
from: time("from").notNull(),
/** End time for the slot template */
to: time("to").notNull(),
/** Duration of individual appointment slots in minutes */
duration: integer("duration").notNull(),
});
/**
* Channel-Agent junction table - establishes many-to-many relationship
* Links channels with the agents who can provide services for that channel
* Stored in tenant-specific database
* @table channelAgent
*/
export const channelAgent = pgTable("channel_agent", {
/** Foreign key to channel */
channelId: uuid("channel_id")
.notNull()
.references(() => channel.id),
/** Foreign key to agent */
agentId: uuid("agent_id")
.notNull()
.references(() => agent.id),
});
/**
* Channel-SlotTemplate junction table - establishes many-to-many relationship
* Links channels with the slot templates that define their availability
* Stored in tenant-specific database
* @table channelSlotTemplate
*/
export const channelSlotTemplate = pgTable("channel_slot_template", {
/** Foreign key to channel */
channelId: uuid("channel_id")
.notNull()
.references(() => channel.id),
/** Foreign key to slot template */
slotTemplateId: uuid("slot_template_id")
.notNull()
.references(() => slotTemplate.id),
});
/**
* Client table - represents end users who book appointments
* Uses end-to-end encryption for privacy protection
* Stored in tenant-specific database
* @table client
*/
export const client = pgTable("client", {
/** Primary key - unique identifier */
id: uuid("id").primaryKey().defaultRandom(),
/** Hash of client email for identification without storing plaintext */
hashKey: text("hash_key").notNull().unique(),
/** Client's public key for end-to-end encryption */
publicKey: text("public_key").notNull(),
/** Server-side share of client's private key for recovery */
privateKeyShare: text("private_key_share").notNull(),
/** Client email address (optional for privacy) */
email: text("email"),
/** Preferred language for communications (de/en) */
language: text("language"),
});
/**
* Appointment table - represents scheduled appointments between clients and channels
* Contains encrypted appointment data for privacy protection
* Stored in tenant-specific database
* @table appointment
*/
export const appointment = pgTable("appointment", {
/** Primary key - unique identifier */
id: uuid("id").primaryKey().defaultRandom(),
/** Foreign key to client who booked the appointment */
clientId: uuid("client_id")
.notNull()
.references(() => client.id),
/** Foreign key to channel/resource being booked */
channelId: uuid("channel_id")
.notNull()
.references(() => channel.id),
/** Date and time of the appointment */
appointmentDate: date("appointment_date").notNull(),
/** When appointment data expires and can be auto-deleted */
expiryDate: date("expiry_date").notNull(),
/** Current status of the appointment */
status: appointmentStatusEnum("status").notNull().default("NEW"),
/** Appointment title/subject */
name: text("name").notNull(),
/** Optional detailed description of the appointment */
phone: text("phone"), // TODO Sensible information will be removed in future (appointment) branch since it will be stored in an encrypted blob
});
/**
* Agent Absence table - represents periods when agents are unavailable
* Used to block agent availability during vacation, training, meetings, etc.
* Stored in tenant-specific database
* @table agentAbsence
*/
export const agentAbsence = pgTable("agent_absence", {
/** Primary key - unique identifier */
id: uuid("id").primaryKey().defaultRandom(),
/** Foreign key to agent who is absent */
agentId: uuid("agent_id")
.notNull()
.references(() => agent.id),
/** Start date and time of absence */
startDate: timestamp("start_date").notNull(),
/** End date and time of absence */
endDate: timestamp("end_date").notNull(),
/** Type of absence (free text: Urlaub, Krankheit, Fortbildung, etc.) */
absenceType: text("absence_type").notNull().default(""),
/** Optional description/reason for absence */
description: text("description"),
});
/**
* TypeScript type exports for use in application code
* These types represent the shape of data when queried from the database
*/
/** Client record type for database queries */
export type SelectClient = InferSelectModel<typeof client>;
/** Channel record type for database queries */
export type SelectChannel = InferSelectModel<typeof channel>;
/** Appointment record type for database queries */
export type SelectAppointment = InferSelectModel<typeof appointment>;
/** Agent record type for database queries */
export type SelectAgent = InferSelectModel<typeof agent>;
/** Slot template record type for database queries */
export type SelectSlotTemplate = InferSelectModel<typeof slotTemplate>;
/** Channel-Agent junction record type for database queries */
export type SelectChannelAgent = InferSelectModel<typeof channelAgent>;
/** Channel-SlotTemplate junction record type for database queries */
export type SelectChannelSlotTemplate = InferSelectModel<typeof channelSlotTemplate>;
/** Agent absence record type for database queries */
export type SelectAgentAbsence = InferSelectModel<typeof agentAbsence>;