mirror of
https://github.com/open-reception/appointment-booking-software.git
synced 2026-08-17 21:25:52 +02:00
160 book appointment confirm appointment requests (#179)
* Extension of tenant schema * Notification service * Notification endpoints * Format errors * Update src/lib/server/db/tenant-schema.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/lib/server/services/notification-service.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Explicit where clause in delete notification * Update tenant-migrations/0008_neat_swarm.sql Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Sort notifivcations by creation date. Do not explicitly check for existance in mark as read. * Fixed tests * Mail templates appointment cancellation * Appointment cancellation notification texts * Delete appointment by staff * Deletion endpoint for staff * My Appointments endpoint for clients * Delete appointment by client * Copilot review fixes * Fixes after notification changes and updates. * Fixed limiting errors * Rejection email template * Send out request, confirmed notifications, send out confirmed, denied emails. * Fix test * MIgration files --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -940,6 +940,11 @@
|
||||
"introduction": "Ihr Termin wurde angefragt. Sie werden benachrichtigt, sobald der Termin bestätigt wurde.",
|
||||
"reason": "Sie erhalten diese E-Mail, weil jemand mit Ihrer E-Mail Adresse einen Termin bei angefragt hat."
|
||||
},
|
||||
"appointmentRejected": {
|
||||
"subject": "Ihre Anfrage für {channel} bei {tenant} wurde abgelehnt",
|
||||
"introduction": "Ihr Termin wurde abgelehnt.",
|
||||
"reason": "Sie erhalten diese E-Mail, weil jemand mit Ihrer E-Mail Adresse einen Termin bei angefragt hat."
|
||||
},
|
||||
"confirmation": {
|
||||
"subject": "E-Mail Adresse bestätigen",
|
||||
"introduction": "willkommen bei unserem Terminbuchungsportal. Bitte bestätige Deine E-Mail Adresse.",
|
||||
|
||||
@@ -944,6 +944,11 @@
|
||||
"action": "Cancel appointment",
|
||||
"reason": "You are receiving this email because someone booked an appointment with your e-mail address."
|
||||
},
|
||||
"appointmentRejected": {
|
||||
"subject": "Your request for {channel} at {tenant} was rejected",
|
||||
"introduction": "Your appointment was rejected.",
|
||||
"reason": "You are receiving this email because someone requested an appointment with your e-mail address."
|
||||
},
|
||||
"appointmentRequest": {
|
||||
"subject": "{channel} with {tenant} requested",
|
||||
"introduction": "your appointment was requested. You will be notified once it is confirmed.",
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<script lang="ts">
|
||||
import { m } from "$i18n/messages";
|
||||
import { setLocale } from "$i18n/runtime";
|
||||
import type { SupportedLocale } from "$lib/const/locales";
|
||||
import type { SelectTenant } from "$lib/server/db/central-schema";
|
||||
import { type SelectAppointment } from "$lib/server/db/tenant-schema";
|
||||
import type { SelectClient } from "$lib/server/email/email-service";
|
||||
import EmailHeadline from "./components/EmailHeadline.svelte";
|
||||
import EmailLayout from "./components/EmailLayout.svelte";
|
||||
import EmailText from "./components/EmailText.svelte";
|
||||
import { renderAppointmentDate, renderAppointmentTime } from "./utils";
|
||||
|
||||
let {
|
||||
locale,
|
||||
user,
|
||||
tenant,
|
||||
channel,
|
||||
appointment,
|
||||
address,
|
||||
}: {
|
||||
locale: SupportedLocale;
|
||||
user: SelectClient;
|
||||
tenant: SelectTenant;
|
||||
channel: string;
|
||||
appointment: SelectAppointment & { agentName: string };
|
||||
address: {
|
||||
street: string;
|
||||
number: string;
|
||||
additionalAddressInfo?: string;
|
||||
zip: string;
|
||||
city: string;
|
||||
};
|
||||
} = $props();
|
||||
|
||||
setLocale(locale);
|
||||
</script>
|
||||
|
||||
<EmailLayout>
|
||||
<EmailText variant="md">{m["emails.greeting"]({ name: user.email })}</EmailText>
|
||||
<EmailText variant="md">
|
||||
{m["emails.appointmentRejected.introduction"]()}
|
||||
</EmailText>
|
||||
<EmailHeadline>{channel}</EmailHeadline>
|
||||
<EmailText variant="md">
|
||||
{appointment.agentName}<br />
|
||||
{renderAppointmentDate(appointment.appointmentDate, locale)}<br />
|
||||
{renderAppointmentTime(appointment.appointmentDate, locale)}
|
||||
{m["emails.oclock"]()}
|
||||
</EmailText>
|
||||
<EmailHeadline>{tenant.longName}</EmailHeadline>
|
||||
<EmailText variant="md">
|
||||
{address.street}
|
||||
{address.number}<br />
|
||||
{#if address.additionalAddressInfo}{address.additionalAddressInfo}<br />{/if}
|
||||
{address.zip}
|
||||
{address.city}
|
||||
</EmailText>
|
||||
<EmailText variant="md" color="text-light">
|
||||
{m["emails.appointmentRejected.reason"]()}
|
||||
</EmailText>
|
||||
</EmailLayout>
|
||||
@@ -26,7 +26,11 @@ export const appointmentStatusEnum = pgEnum("appointment_status", [
|
||||
"NO_SHOW",
|
||||
]);
|
||||
|
||||
export const notificationTypes = ["APPOINTMENT_CONFIRMED", "APPOINTMENT_CANCELLED"] as const;
|
||||
export const notificationTypes = [
|
||||
"APPOINTMENT_CONFIRMED",
|
||||
"APPOINTMENT_CANCELLED",
|
||||
"APPOINTMENT_REQUESTED",
|
||||
] as const;
|
||||
export type NotificationType = (typeof notificationTypes)[number];
|
||||
|
||||
export const notificationTypeEnum = pgEnum("notification_type", notificationTypes);
|
||||
|
||||
@@ -14,6 +14,7 @@ import { setLocale } from "$i18n/runtime";
|
||||
import { m } from "$i18n/messages";
|
||||
import { render } from "svelte/server";
|
||||
import AppointmentBooked from "$lib/emails/AppointmentBooked.svelte";
|
||||
import AppointmentRejected from "$lib/emails/AppointmentRejected.svelte";
|
||||
import { htmlToText, renderOutputToHtml } from "$lib/emails/utils";
|
||||
import { AgentService } from "../services/agent-service";
|
||||
import { TenantService } from "../db/tenant-service";
|
||||
@@ -232,6 +233,50 @@ const getAddressFromTenant = async (tenantId: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Send appointment rejection email for newly created appointments
|
||||
* @param {SelectClient | SelectUser} user - Database user object or client data
|
||||
* @param {SelectTenant} tenant - Tenant information for branding
|
||||
* @param {SelectAppointment} appointment - Appointment details
|
||||
* @param {string} [channelTitle] - Optional channel title/name
|
||||
* @param {string} [cancelUrl] - Optional URL to cancel appointment
|
||||
* @throws {Error} When email sending fails
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function sendAppointmentRejectedEmail(
|
||||
user: SelectClient | SelectUser,
|
||||
tenant: SelectTenant,
|
||||
appointment: SelectAppointment,
|
||||
channelTitle?: string,
|
||||
): Promise<void> {
|
||||
// Create recipient directly for SelectClient type, use helper for SelectUser
|
||||
|
||||
// Set language
|
||||
const agentService = await AgentService.forTenant(tenant.id);
|
||||
const agent = await agentService.getAgentById(appointment.agentId);
|
||||
const { recipient, locale } = await getRecipient(user);
|
||||
|
||||
// Generate email
|
||||
const subject = m["emails.appointmentRejected.subject"]({
|
||||
channel: channelTitle || appointment.channelId,
|
||||
tenant: tenant.longName,
|
||||
});
|
||||
const emailRender = render(AppointmentRejected, {
|
||||
props: {
|
||||
locale,
|
||||
channel: channelTitle || appointment.channelId,
|
||||
user,
|
||||
tenant,
|
||||
appointment: { ...appointment, agentName: agent?.name ?? "---" },
|
||||
address: await getAddressFromTenant(tenant.id),
|
||||
},
|
||||
});
|
||||
const html = renderOutputToHtml(emailRender);
|
||||
const text = htmlToText(html);
|
||||
|
||||
await sendEmail(recipient, subject, html, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send appointment confirmation email for newly created appointments
|
||||
* @param {SelectClient | SelectUser} user - Database user object or client data
|
||||
|
||||
@@ -25,6 +25,16 @@ vi.mock("../challenge-throttle", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../notification-service", () => ({
|
||||
NotificationService: {
|
||||
forTenant: vi.fn().mockResolvedValue({
|
||||
sendAppointmentConfirmationEmail: vi.fn().mockResolvedValue(undefined),
|
||||
sendAppointmentCancellationEmail: vi.fn().mockResolvedValue(undefined),
|
||||
createNotification: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockAppointment = {
|
||||
id: "appointment-123",
|
||||
tunnelId: "tunnel-123",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
sendAppointmentRequestEmail,
|
||||
sendAppointmentCancelledEmail,
|
||||
getChannelTitle,
|
||||
sendAppointmentRejectedEmail,
|
||||
} from "../email/email-service";
|
||||
import { TenantAdminService } from "./tenant-admin-service";
|
||||
import { NotificationService } from "./notification-service";
|
||||
@@ -127,6 +128,8 @@ export class AppointmentService {
|
||||
appointmentId: appointment.id,
|
||||
tenantId: this.tenantId,
|
||||
});
|
||||
} else if (appointment.status === "REJECTED") {
|
||||
await sendAppointmentRejectedEmail(clientData, tenant, appointment, channelTitle);
|
||||
} else {
|
||||
await sendAppointmentCreatedEmail(clientData, tenant, appointment, channelTitle);
|
||||
log.info("Appointment confirmation email sent", {
|
||||
@@ -206,9 +209,77 @@ export class AppointmentService {
|
||||
});
|
||||
}
|
||||
|
||||
const notificationService = await NotificationService.forTenant(this.tenantId);
|
||||
await notificationService.createNotification({
|
||||
channelId: row.channelId,
|
||||
type: "APPOINTMENT_CONFIRMED",
|
||||
metaData: {
|
||||
appointmentId: row.id,
|
||||
},
|
||||
});
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
public async denyAppointment(
|
||||
id: string,
|
||||
clientEmail?: string,
|
||||
clientLanguage?: string,
|
||||
): Promise<void> {
|
||||
const log = logger.setContext("AppointmentService");
|
||||
log.debug("Denying appointment by ID", { appointmentId: id, tenantId: this.tenantId });
|
||||
const db = await this.getDb();
|
||||
|
||||
const result = await db
|
||||
.update(tenantSchema.appointment)
|
||||
.set({ status: "REJECTED" })
|
||||
.where(and(eq(tenantSchema.appointment.id, id), eq(tenantSchema.appointment.status, "NEW")))
|
||||
.returning();
|
||||
|
||||
if (result.length === 0) {
|
||||
log.warn("Appointment not found or in wrong state", {
|
||||
appointmentId: id,
|
||||
state: result[0] ? result[0].status : undefined,
|
||||
tenantId: this.tenantId,
|
||||
});
|
||||
throw new ValidationError("Appointment not found or in wrong state");
|
||||
}
|
||||
|
||||
const row = result[0];
|
||||
|
||||
// Send rejection email to client if email is provided (async, don't wait)
|
||||
if (clientEmail) {
|
||||
this.sendAppointmentNotification(
|
||||
row.id,
|
||||
row.channelId,
|
||||
clientEmail,
|
||||
clientLanguage || "de",
|
||||
false,
|
||||
).catch((error) => {
|
||||
log.error("Failed to send appointment rejection email", {
|
||||
appointmentId: row.id,
|
||||
error: String(error),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
db.delete(tenantSchema.appointment)
|
||||
.where(eq(tenantSchema.appointment.id, id))
|
||||
.then(() => {
|
||||
log.debug("Appointment deleted after rejection", {
|
||||
appointmentId: id,
|
||||
tenantId: this.tenantId,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("Failed to delete appointment after rejection", {
|
||||
appointmentId: id,
|
||||
error: String(error),
|
||||
tenantId: this.tenantId,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async cancelAppointment(id: string): Promise<SelectAppointment> {
|
||||
const log = logger.setContext("AppointmentService");
|
||||
log.debug("Confirming appointment by ID", { appointmentId: id, tenantId: this.tenantId });
|
||||
@@ -522,6 +593,17 @@ export class AppointmentService {
|
||||
updatedAt: tenantSchema.appointment.updatedAt,
|
||||
});
|
||||
|
||||
if (initialStatus === "NEW") {
|
||||
const notificationService = await NotificationService.forTenant(this.tenantId);
|
||||
await notificationService.createNotification({
|
||||
channelId: clientData.channelId,
|
||||
type: "APPOINTMENT_REQUESTED",
|
||||
metaData: {
|
||||
appointmentId: appointmentResult[0].id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (appointmentResult.length === 0) {
|
||||
throw new InternalError("Failed to create appointment");
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
getChannelTitle,
|
||||
} from "$lib/server/email/email-service";
|
||||
import { TenantAdminService } from "$lib/server/services/tenant-admin-service";
|
||||
import { NotificationService } from "$lib/server/services/notification-service";
|
||||
|
||||
const requestSchema = z.object({
|
||||
emailHash: z.string(),
|
||||
@@ -316,6 +317,15 @@ export const POST: RequestHandler = async ({ request, params }) => {
|
||||
);
|
||||
|
||||
// Send appropriate email based on whether confirmation is required
|
||||
if (requiresConfirmation) {
|
||||
// send notification to tenant staff about new appointment request
|
||||
const notificationService = NotificationService.forTenant(tenantId);
|
||||
(await notificationService).createNotification({
|
||||
type: "APPOINTMENT_REQUESTED",
|
||||
channelId: validatedData.channelId,
|
||||
metaData: { appointmentId: result.id },
|
||||
});
|
||||
}
|
||||
if (validatedData.clientEmail) {
|
||||
// Create client data object from request
|
||||
const clientData = {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TYPE "public"."notification_type" ADD VALUE 'APPOINTMENT_REQUESTED';
|
||||
@@ -0,0 +1,898 @@
|
||||
{
|
||||
"id": "a189259c-e77c-4561-8a6c-c2f2fe5c9eb4",
|
||||
"prevId": "b141a31e-20fd-4247-bb21-e403af1809bc",
|
||||
"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
|
||||
},
|
||||
"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.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": {}
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,13 @@
|
||||
"when": 1768926072442,
|
||||
"tag": "0011_kind_starbolt",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 12,
|
||||
"version": "7",
|
||||
"when": 1769078158060,
|
||||
"tag": "0012_military_puppet_master",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user