mirror of
https://github.com/open-reception/appointment-booking-software.git
synced 2026-08-17 21:25:52 +02:00
Save timezone, use timezone in appointment mails.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
<EmailHeadline>{channel}</EmailHeadline>
|
||||
<EmailText variant="md">
|
||||
{appointment.agentName}<br />
|
||||
{renderAppointmentDate(appointment.appointmentDate, locale)}<br />
|
||||
{renderAppointmentTime(appointment.appointmentDate, locale)}
|
||||
{renderAppointmentDate(appointment.appointmentDate, locale, appointment.timezone)}<br />
|
||||
{renderAppointmentTime(appointment.appointmentDate, locale, appointment.timezone)}
|
||||
{m["emails.oclock"]()}
|
||||
</EmailText>
|
||||
<EmailHeadline>{tenant.longName}</EmailHeadline>
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
<EmailHeadline>{channel}</EmailHeadline>
|
||||
<EmailText variant="md">
|
||||
{appointment.agentName}<br />
|
||||
{renderAppointmentDate(appointment.appointmentDate, locale)}<br />
|
||||
{renderAppointmentTime(appointment.appointmentDate, locale)}
|
||||
{renderAppointmentDate(appointment.appointmentDate, locale, appointment.timezone)}<br />
|
||||
{renderAppointmentTime(appointment.appointmentDate, locale, appointment.timezone)}
|
||||
{m["emails.oclock"]()}
|
||||
</EmailText>
|
||||
<EmailHeadline>{tenant.longName}</EmailHeadline>
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
<EmailHeadline>{channel}</EmailHeadline>
|
||||
<EmailText variant="md">
|
||||
{appointment.agentName}<br />
|
||||
{renderAppointmentDate(appointment.appointmentDate, locale)}<br />
|
||||
{renderAppointmentTime(appointment.appointmentDate, locale)}
|
||||
{renderAppointmentDate(appointment.appointmentDate, locale, appointment.timezone)}<br />
|
||||
{renderAppointmentTime(appointment.appointmentDate, locale, appointment.timezone)}
|
||||
{m["emails.oclock"]()}
|
||||
</EmailText>
|
||||
<EmailHeadline>{tenant.longName}</EmailHeadline>
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
<EmailHeadline>{channel}</EmailHeadline>
|
||||
<EmailText variant="md">
|
||||
{appointment.agentName}<br />
|
||||
{renderAppointmentDate(appointment.appointmentDate, locale)}<br />
|
||||
{renderAppointmentTime(appointment.appointmentDate, locale)}
|
||||
{renderAppointmentDate(appointment.appointmentDate, locale, appointment.timezone)}<br />
|
||||
{renderAppointmentTime(appointment.appointmentDate, locale, appointment.timezone)}
|
||||
{m["emails.oclock"]()}
|
||||
</EmailText>
|
||||
<EmailHeadline>{tenant.longName}</EmailHeadline>
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
<EmailHeadline>{channel}</EmailHeadline>
|
||||
<EmailText variant="md">
|
||||
{appointment.agentName}<br />
|
||||
{renderAppointmentDate(appointment.appointmentDate, locale)}<br />
|
||||
{renderAppointmentTime(appointment.appointmentDate, locale)}
|
||||
{renderAppointmentDate(appointment.appointmentDate, locale, appointment.timezone)}<br />
|
||||
{renderAppointmentTime(appointment.appointmentDate, locale, appointment.timezone)}
|
||||
{m["emails.oclock"]()}
|
||||
</EmailText>
|
||||
<EmailHeadline>{tenant.longName}</EmailHeadline>
|
||||
|
||||
+22
-10
@@ -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}`;
|
||||
};
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "appointment" ADD COLUMN "timezone" text NOT NULL DEFAULT 'UTC';
|
||||
ALTER TABLE "appointment" ALTER COLUMN "timezone" DROP DEFAULT;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user