fix(portal): send preferredTime as HH:MM:SS and format booking slot labels (GRO-2211) (#51)
CI / Test (push) Successful in 18s
CI / Lint & Typecheck (push) Successful in 25s
CI / Build & Push Docker Image (push) Successful in 12s
CI / Test (pull_request) Successful in 22s
CI / Lint & Typecheck (pull_request) Successful in 28s
CI / Build & Push Docker Image (pull_request) Successful in 12s
CI / Test (push) Successful in 18s
CI / Lint & Typecheck (push) Successful in 25s
CI / Build & Push Docker Image (push) Successful in 12s
CI / Test (pull_request) Successful in 22s
CI / Lint & Typecheck (pull_request) Successful in 28s
CI / Build & Push Docker Image (pull_request) Successful in 12s
This commit was merged in pull request #51.
This commit is contained in:
@@ -110,6 +110,41 @@ export function parseTimeTo24Hour(time: string | null | undefined): string {
|
||||
return `${hours24.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:00`;
|
||||
}
|
||||
|
||||
// A booking slot is the canonical UTC ISO instant returned by
|
||||
// `/api/book/availability` (e.g. "2026-06-09T10:00:00.000Z" is the 10:00 UTC
|
||||
// business slot — see api `src/lib/slots.ts`, which builds them with
|
||||
// `setUTCHours`). Display label and submit payload both derive from the slot via
|
||||
// these helpers so they never desync. Always format/extract in UTC: slots are
|
||||
// generated as UTC business hours, so a browser-local conversion would mislabel
|
||||
// the slot and diverge from the stored Postgres `time` column.
|
||||
export function formatSlotLabel(slot: string): string {
|
||||
const d = new Date(slot);
|
||||
// Non-ISO input (e.g. an already-formatted "10:00 AM" label) — show as-is.
|
||||
if (Number.isNaN(d.getTime())) return slot;
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
timeZone: 'UTC',
|
||||
}).format(d);
|
||||
}
|
||||
|
||||
// Extracts the UTC `HH:MM:SS` time component the api stores in the Postgres
|
||||
// `time` column. The api inserts this verbatim, so a full ISO datetime here is
|
||||
// an `invalid input syntax for type time` 500 (GRO-2211).
|
||||
export function slotToTime(slot: string): string {
|
||||
if (/^\d{2}:\d{2}:\d{2}$/.test(slot)) return slot; // already HH:MM:SS
|
||||
const d = new Date(slot);
|
||||
if (!Number.isNaN(d.getTime())) {
|
||||
const hh = String(d.getUTCHours()).padStart(2, '0');
|
||||
const mm = String(d.getUTCMinutes()).padStart(2, '0');
|
||||
const ss = String(d.getUTCSeconds()).padStart(2, '0');
|
||||
return `${hh}:${mm}:${ss}`;
|
||||
}
|
||||
// "10:00 AM"-style label fallback.
|
||||
return parseTimeTo24Hour(slot);
|
||||
}
|
||||
|
||||
export function isUpcoming(appt: Appointment): boolean {
|
||||
const now = new Date();
|
||||
// Prefer the absolute ISO `startTime` from the API; fall back to the
|
||||
@@ -860,7 +895,7 @@ interface BookingFlowProps {
|
||||
sessionId: string | null;
|
||||
}
|
||||
|
||||
function BookingFlow({ onClose, sessionId }: BookingFlowProps) {
|
||||
export function BookingFlow({ onClose, sessionId }: BookingFlowProps) {
|
||||
const [step, setStep] = useState(1);
|
||||
const [pets, setPets] = useState<Pet[]>([]);
|
||||
const [services, setServices] = useState<Service[]>([]);
|
||||
@@ -972,7 +1007,7 @@ function BookingFlow({ onClose, sessionId }: BookingFlowProps) {
|
||||
addOnIds: selectedAddOns.map((s) => s.id),
|
||||
groomerId: selectedGroomer === 'first-available' ? null : selectedGroomer,
|
||||
preferredDate: selectedDate,
|
||||
preferredTime: selectedTime,
|
||||
preferredTime: slotToTime(selectedTime),
|
||||
notes: notes || undefined,
|
||||
recurring: recurring || undefined,
|
||||
}),
|
||||
@@ -1035,7 +1070,7 @@ function BookingFlow({ onClose, sessionId }: BookingFlowProps) {
|
||||
Appointment Requested!
|
||||
</h3>
|
||||
<p className="text-sm text-stone-500 mb-4">
|
||||
{selectedPet?.name} on {formatDate(selectedDate)} at {selectedTime}
|
||||
{selectedPet?.name} on {formatDate(selectedDate)} at {formatSlotLabel(selectedTime)}
|
||||
</p>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -1255,7 +1290,7 @@ function BookingFlow({ onClose, sessionId }: BookingFlowProps) {
|
||||
: 'border-stone-200 hover:border-stone-300'
|
||||
}`}
|
||||
>
|
||||
{time}
|
||||
{formatSlotLabel(time)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -1325,7 +1360,7 @@ function BookingFlow({ onClose, sessionId }: BookingFlowProps) {
|
||||
<div className="flex justify-between">
|
||||
<span className="text-stone-500">Date & Time</span>
|
||||
<span className="font-medium">
|
||||
{formatDate(selectedDate)} at {selectedTime}
|
||||
{formatDate(selectedDate)} at {formatSlotLabel(selectedTime)}
|
||||
</span>
|
||||
</div>
|
||||
{recurring && (
|
||||
|
||||
Reference in New Issue
Block a user