Phase 3: Appointment model & booking API with timezone fix

This commit is contained in:
counterweight 2025-12-21 00:03:34 +01:00
parent f6cf093cb1
commit 06817875f7
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
9 changed files with 946 additions and 9 deletions

View file

@ -15,9 +15,12 @@ type AvailabilityDay = components["schemas"]["AvailabilityDay"];
type AvailabilityResponse = components["schemas"]["AvailabilityResponse"];
type TimeSlot = components["schemas"]["TimeSlot"];
// Helper to format date as YYYY-MM-DD
// Helper to format date as YYYY-MM-DD in local timezone
function formatDate(d: Date): string {
return d.toISOString().split("T")[0];
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
// Helper to get next N days starting from tomorrow

View file

@ -356,6 +356,46 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/booking/slots": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Get Available Slots
* @description Get available booking slots for a specific date.
*/
get: operations["get_available_slots_api_booking_slots_get"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/booking": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Create Booking
* @description Book an appointment slot.
*/
post: operations["create_booking_api_booking_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/meta/constants": {
parameters: {
query?: never;
@ -390,6 +430,39 @@ export interface components {
/** Email */
email: string;
};
/**
* AppointmentResponse
* @description Response model for an appointment.
*/
AppointmentResponse: {
/** Id */
id: number;
/** User Id */
user_id: number;
/** User Email */
user_email: string;
/**
* Slot Start
* Format: date-time
*/
slot_start: string;
/**
* Slot End
* Format: date-time
*/
slot_end: string;
/** Note */
note: string | null;
/** Status */
status: string;
/**
* Created At
* Format: date-time
*/
created_at: string;
/** Cancelled At */
cancelled_at: string | null;
};
/**
* AvailabilityDay
* @description Availability for a single day.
@ -411,6 +484,48 @@ export interface components {
/** Days */
days: components["schemas"]["AvailabilityDay"][];
};
/**
* AvailableSlotsResponse
* @description Response for available slots on a given date.
*/
AvailableSlotsResponse: {
/**
* Date
* Format: date
*/
date: string;
/** Slots */
slots: components["schemas"]["BookableSlot"][];
};
/**
* BookableSlot
* @description A bookable 15-minute slot.
*/
BookableSlot: {
/**
* Start Time
* Format: date-time
*/
start_time: string;
/**
* End Time
* Format: date-time
*/
end_time: string;
};
/**
* BookingRequest
* @description Request to book an appointment.
*/
BookingRequest: {
/**
* Slot Start
* Format: date-time
*/
slot_start: string;
/** Note */
note?: string | null;
};
/**
* ConstantsResponse
* @description Response model for shared constants.
@ -1304,6 +1419,71 @@ export interface operations {
};
};
};
get_available_slots_api_booking_slots_get: {
parameters: {
query: {
/** @description Date to get slots for */
date: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["AvailableSlotsResponse"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
create_booking_api_booking_post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["BookingRequest"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["AppointmentResponse"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
get_constants_api_meta_constants_get: {
parameters: {
query?: never;

View file

@ -45,6 +45,20 @@ function getTomorrowDisplay(): string {
return tomorrow.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });
}
// Helper to get a date string in YYYY-MM-DD format using local timezone
function formatDateLocal(d: Date): string {
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
function getTomorrowDateStr(): string {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
return formatDateLocal(tomorrow);
}
test.describe("Availability Page - Admin Access", () => {
test.beforeEach(async ({ page }) => {
await clearAuth(page);
@ -204,9 +218,7 @@ test.describe("Availability API", () => {
const authCookie = cookies.find(c => c.name === "auth_token");
if (authCookie) {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const dateStr = tomorrow.toISOString().split("T")[0];
const dateStr = getTomorrowDateStr();
const response = await request.put(`${API_URL}/api/admin/availability`, {
headers: {
@ -234,9 +246,7 @@ test.describe("Availability API", () => {
const authCookie = cookies.find(c => c.name === "auth_token");
if (authCookie) {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const dateStr = tomorrow.toISOString().split("T")[0];
const dateStr = getTomorrowDateStr();
const response = await request.get(
`${API_URL}/api/admin/availability?from=${dateStr}&to=${dateStr}`,