Extract duplicate date formatting functions to shared utilities

- Created frontend/app/utils/date.ts with formatDate, formatTime, formatDateTime, formatDisplayDate
- Created frontend/e2e/helpers/date.ts with formatDateLocal, getTomorrowDateStr
- Updated all frontend pages and e2e tests to use shared utilities
- Removed duplicate date formatting code from 6 files
This commit is contained in:
counterweight 2025-12-21 17:48:17 +01:00
parent eefdfd714f
commit 6ff3c0a133
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
9 changed files with 78 additions and 98 deletions

View file

@ -7,23 +7,11 @@ import { api } from "../../api";
import { Header } from "../../components/Header"; import { Header } from "../../components/Header";
import { useRequireAuth } from "../../hooks/useRequireAuth"; import { useRequireAuth } from "../../hooks/useRequireAuth";
import { components } from "../../generated/api"; import { components } from "../../generated/api";
import { formatDateTime } from "../../utils/date";
type AppointmentResponse = components["schemas"]["AppointmentResponse"]; type AppointmentResponse = components["schemas"]["AppointmentResponse"];
type PaginatedAppointments = components["schemas"]["PaginatedResponse_AppointmentResponse_"]; type PaginatedAppointments = components["schemas"]["PaginatedResponse_AppointmentResponse_"];
// Helper to format datetime
function formatDateTime(isoString: string): string {
const d = new Date(isoString);
return d.toLocaleString("en-US", {
weekday: "short",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
}
// Helper to get status display // Helper to get status display
function getStatusDisplay(status: string): { text: string; bgColor: string; textColor: string } { function getStatusDisplay(status: string): { text: string; bgColor: string; textColor: string } {
switch (status) { switch (status) {

View file

@ -8,6 +8,7 @@ import { Header } from "../../components/Header";
import { useRequireAuth } from "../../hooks/useRequireAuth"; import { useRequireAuth } from "../../hooks/useRequireAuth";
import { components } from "../../generated/api"; import { components } from "../../generated/api";
import constants from "../../../../shared/constants.json"; import constants from "../../../../shared/constants.json";
import { formatDate, formatDisplayDate } from "../../utils/date";
const { slotDurationMinutes, maxAdvanceDays } = constants.booking; const { slotDurationMinutes, maxAdvanceDays } = constants.booking;
@ -15,14 +16,6 @@ type AvailabilityDay = components["schemas"]["AvailabilityDay"];
type AvailabilityResponse = components["schemas"]["AvailabilityResponse"]; type AvailabilityResponse = components["schemas"]["AvailabilityResponse"];
type TimeSlot = components["schemas"]["TimeSlot"]; type TimeSlot = components["schemas"]["TimeSlot"];
// Helper to format date as YYYY-MM-DD in local timezone
function formatDate(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}`;
}
// Helper to get next N days starting from tomorrow // Helper to get next N days starting from tomorrow
function getDateRange(): Date[] { function getDateRange(): Date[] {
const dates: Date[] = []; const dates: Date[] = [];
@ -223,10 +216,6 @@ export default function AdminAvailabilityPage() {
} }
}; };
const formatDisplayDate = (d: Date): string => {
return d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });
};
const formatSlotTime = (slot: TimeSlot): string => { const formatSlotTime = (slot: TimeSlot): string => {
return `${slot.start_time.slice(0, 5)} - ${slot.end_time.slice(0, 5)}`; return `${slot.start_time.slice(0, 5)} - ${slot.end_time.slice(0, 5)}`;
}; };

View file

@ -7,22 +7,10 @@ import { api } from "../api";
import { Header } from "../components/Header"; import { Header } from "../components/Header";
import { useRequireAuth } from "../hooks/useRequireAuth"; import { useRequireAuth } from "../hooks/useRequireAuth";
import { components } from "../generated/api"; import { components } from "../generated/api";
import { formatDateTime } from "../utils/date";
type AppointmentResponse = components["schemas"]["AppointmentResponse"]; type AppointmentResponse = components["schemas"]["AppointmentResponse"];
// Helper to format datetime
function formatDateTime(isoString: string): string {
const d = new Date(isoString);
return d.toLocaleString("en-US", {
weekday: "short",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
}
// Helper to get status display // Helper to get status display
function getStatusDisplay(status: string): { text: string; bgColor: string; textColor: string } { function getStatusDisplay(status: string): { text: string; bgColor: string; textColor: string } {
switch (status) { switch (status) {

View file

@ -8,6 +8,7 @@ import { Header } from "../components/Header";
import { useRequireAuth } from "../hooks/useRequireAuth"; import { useRequireAuth } from "../hooks/useRequireAuth";
import { components } from "../generated/api"; import { components } from "../generated/api";
import constants from "../../../shared/constants.json"; import constants from "../../../shared/constants.json";
import { formatDate, formatTime } from "../utils/date";
const { slotDurationMinutes, maxAdvanceDays, minAdvanceDays, noteMaxLength } = constants.booking; const { slotDurationMinutes, maxAdvanceDays, minAdvanceDays, noteMaxLength } = constants.booking;
@ -15,24 +16,6 @@ type BookableSlot = components["schemas"]["BookableSlot"];
type AvailableSlotsResponse = components["schemas"]["AvailableSlotsResponse"]; type AvailableSlotsResponse = components["schemas"]["AvailableSlotsResponse"];
type AppointmentResponse = components["schemas"]["AppointmentResponse"]; type AppointmentResponse = components["schemas"]["AppointmentResponse"];
// Helper to format date as YYYY-MM-DD in local timezone
function formatDate(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}`;
}
// Helper to format time from ISO string
function formatTime(isoString: string): string {
const d = new Date(isoString);
return d.toLocaleTimeString("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
}
// Get date range for booking (tomorrow to +30 days) // Get date range for booking (tomorrow to +30 days)
function getBookableDates(): Date[] { function getBookableDates(): Date[] {
const dates: Date[] = []; const dates: Date[] = [];

View file

@ -0,0 +1,48 @@
/**
* Shared date formatting utilities.
*/
/**
* Format date as YYYY-MM-DD in local timezone.
*/
export function formatDate(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}`;
}
/**
* Format time from ISO string to HH:MM format.
*/
export function formatTime(isoString: string): string {
const d = new Date(isoString);
return d.toLocaleTimeString("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
}
/**
* Format datetime from ISO string to a readable format.
*/
export function formatDateTime(isoString: string): string {
const d = new Date(isoString);
return d.toLocaleString("en-US", {
weekday: "short",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
}
/**
* Format date for display (e.g., "Mon, Jan 15").
*/
export function formatDisplayDate(d: Date): string {
return d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });
}

View file

@ -1,4 +1,5 @@
import { test, expect, Page } from "@playwright/test"; import { test, expect, Page } from "@playwright/test";
import { formatDateLocal, getTomorrowDateStr } from "./helpers/date";
/** /**
* Appointments Page E2E Tests * Appointments Page E2E Tests
@ -38,20 +39,6 @@ async function loginUser(page: Page, email: string, password: string) {
await page.waitForURL((url) => !url.pathname.includes("/login"), { timeout: 10000 }); await page.waitForURL((url) => !url.pathname.includes("/login"), { timeout: 10000 });
} }
// Helper to format date as YYYY-MM-DD in 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);
}
// Set up availability and create a booking // Set up availability and create a booking
async function createTestBooking(page: Page) { async function createTestBooking(page: Page) {
const dateStr = getTomorrowDateStr(); const dateStr = getTomorrowDateStr();

View file

@ -1,4 +1,5 @@
import { test, expect, Page } from "@playwright/test"; import { test, expect, Page } from "@playwright/test";
import { formatDateLocal, getTomorrowDateStr } from "./helpers/date";
/** /**
* Availability Page E2E Tests * Availability Page E2E Tests
@ -45,20 +46,6 @@ function getTomorrowDisplay(): string {
return tomorrow.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" }); 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.describe("Availability Page - Admin Access", () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await clearAuth(page); await clearAuth(page);

View file

@ -1,4 +1,5 @@
import { test, expect, Page } from "@playwright/test"; import { test, expect, Page } from "@playwright/test";
import { formatDateLocal, getTomorrowDateStr } from "./helpers/date";
/** /**
* Booking Page E2E Tests * Booking Page E2E Tests
@ -38,20 +39,6 @@ async function loginUser(page: Page, email: string, password: string) {
await page.waitForURL((url) => !url.pathname.includes("/login"), { timeout: 10000 }); await page.waitForURL((url) => !url.pathname.includes("/login"), { timeout: 10000 });
} }
// Helper to format date as YYYY-MM-DD in 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);
}
// Set up availability for a date using the API with retry logic // Set up availability for a date using the API with retry logic
async function setAvailability(page: Page, dateStr: string, maxRetries = 3) { async function setAvailability(page: Page, dateStr: string, maxRetries = 3) {
const cookies = await page.context().cookies(); const cookies = await page.context().cookies();

View file

@ -0,0 +1,23 @@
/**
* Date formatting helpers for e2e tests.
*/
/**
* Format date as YYYY-MM-DD in local timezone.
*/
export 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}`;
}
/**
* Get tomorrow's date string in YYYY-MM-DD format.
*/
export function getTomorrowDateStr(): string {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
return formatDateLocal(tomorrow);
}