diff --git a/src/app/booking/BookingPageClient.tsx b/src/app/booking/BookingPageClient.tsx index 6af5230..09f558f 100644 --- a/src/app/booking/BookingPageClient.tsx +++ b/src/app/booking/BookingPageClient.tsx @@ -6,9 +6,8 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; import { RoomSelectBooking } from "@/components/RoomSelectBooking"; import { useBooking } from "@/context/BookingContext"; -import { rooms } from "@/lib/mocks/rooms"; -import { siteConfig } from "@/lib/mocks/site"; -import { submitBookingHold } from "@/lib/mocks/api"; +import { siteConfig } from "@/lib/site-config"; +import { createPublicBooking, ensurePropertyId } from "@/lib/public-hotel-api"; export function BookingPageClient() { const searchParams = useSearchParams(); @@ -24,6 +23,9 @@ export function BookingPageClient() { setPayLaterHold, selectedRoom, nights, + rooms, + couponCode, + setLastCreatedBooking, } = useBooking(); const [pending, setPending] = useState(null); @@ -32,33 +34,55 @@ export function BookingPageClient() { useEffect(() => { const r = searchParams.get("room"); if (r && rooms.some((x) => x.id === r)) setRoomId(r); - }, [searchParams, setRoomId]); + }, [searchParams, setRoomId, rooms]); const canContinue = selectedRoom && guest.firstName.trim() && guest.lastName.trim() && guest.email.trim() && - guest.phone.trim() && - guest.flightBookingNumber.trim() && - guest.arrivalTime.trim(); + guest.phone.trim() + // guest.flightBookingNumber.trim() && + // guest.arrivalTime.trim(); async function placeHold(mode: "payment" | "reserve") { if (!canContinue || !selectedRoom) return; setError(null); setPending(mode); try { - const { reference } = await submitBookingHold({ + const propertyId = await ensurePropertyId(); + const booking = await createPublicBooking(propertyId, { roomId: selectedRoom.id, - email: guest.email, - flightBookingNumber: guest.flightBookingNumber.trim(), + checkIn, + checkOut, + guestCount: guests, + firstName: guest.firstName.trim(), + lastName: guest.lastName.trim(), + email: guest.email.trim().toLowerCase(), + phone: guest.phone.trim(), + flightPnr: guest.flightBookingNumber.trim(), arrivalTime: guest.arrivalTime.trim(), + discountCode: couponCode.trim() || undefined, + payLaterHold: mode === "reserve", }); - setHoldReference(reference); + const code = booking.bookingCode ?? ""; + setHoldReference(code); setPayLaterHold(mode === "reserve"); + const tp = + booking.totalPrice != null + ? typeof booking.totalPrice === "string" + ? Number.parseFloat(booking.totalPrice) + : booking.totalPrice + : 0; + setLastCreatedBooking({ + id: booking.id, + bookingCode: booking.bookingCode, + totalPrice: Number.isFinite(tp) ? tp : 0, + currency: booking.currency ?? "ETB", + }); router.push(mode === "payment" ? "/payment" : "/reserve-held"); - } catch { - setError("Something went wrong. Please try again."); + } catch (e) { + setError(e instanceof Error ? e.message : "Something went wrong. Please try again."); } finally { setPending(null); } @@ -69,11 +93,9 @@ export function BookingPageClient() {

Book your stay

-

- It only takes a moment -

+

It only takes a moment

- Pay now, or reserve first and complete payment later in this session — mock only. + Live rates from the hotel. You'll receive a booking code to sign in and manage your stay.

@@ -206,8 +228,8 @@ export function BookingPageClient() { {pending === "reserve" ? "Saving your hold…" : "Reserve now — pay later"}

- Pay later keeps your details and hold reference; finish checkout from the next screen - whenever you're ready. + Pay later keeps your hold; you'll get a booking code. Payment is completed at the hotel unless + you add card checkout later.

diff --git a/src/app/guest/laundry/LaundryClient.tsx b/src/app/guest/laundry/LaundryClient.tsx index 22dd9e2..63db670 100644 --- a/src/app/guest/laundry/LaundryClient.tsx +++ b/src/app/guest/laundry/LaundryClient.tsx @@ -1,10 +1,12 @@ "use client"; import Link from "next/link"; -import { useMemo, useState } from "react"; +import { useState } from "react"; import { RequireAuth } from "@/components/RequireAuth"; import { useAuth } from "@/context/AuthContext"; -import { laundryItems } from "@/lib/mocks/laundryCatalog"; +import { guestPlaceLaundry } from "@/lib/guest-hotel-api"; +import { formatEtb } from "@/lib/format-etb"; +import { useGuestActiveBooking } from "@/lib/useGuestActiveBooking"; export function LaundryClient() { return ( @@ -15,58 +17,46 @@ export function LaundryClient() { } function LaundryInner() { - const { addOrder } = useAuth(); - const [qty, setQty] = useState>({}); - const [express, setExpress] = useState(false); + const { accessToken } = useAuth(); + const { bookingId, loading: bookingLoading, propertyId } = useGuestActiveBooking(); + const [notes, setNotes] = useState(""); + const [pickupAt, setPickupAt] = useState(""); + const [deliverAt, setDeliverAt] = useState(""); + const [estimateEtb, setEstimateEtb] = useState(""); const [sent, setSent] = useState(false); + const [submitErr, setSubmitErr] = useState(null); + const [submitting, setSubmitting] = useState(false); - function bump(id: string, delta: number) { - setQty((prev) => { - const next = { ...prev }; - const n = Math.max(0, (next[id] ?? 0) + delta); - if (n === 0) delete next[id]; - else next[id] = n; - return next; - }); - } + const canUseApi = !!(propertyId && accessToken && bookingId); - const lines = useMemo(() => { - const out: { id: string; name: string; count: number; unitUsd: number }[] = []; - for (const row of laundryItems) { - const q = qty[row.id]; - if (q && q > 0) { - out.push({ id: row.id, name: row.name, count: q, unitUsd: row.priceUsd }); - } + async function submit() { + if (!canUseApi) return; + setSubmitErr(null); + setSubmitting(true); + try { + await guestPlaceLaundry(propertyId!, accessToken!, { + bookingId: bookingId!, + items: [], + notes: notes.trim() || undefined, + pickupAt: pickupAt || undefined, + deliverAt: deliverAt || undefined, + total: estimateEtb.trim() || undefined, + }); + } catch (e) { + setSubmitErr(e instanceof Error ? e.message : "Could not submit laundry request"); + setSubmitting(false); + return; } - return out; - }, [qty]); - - const subtotal = useMemo(() => { - let s = lines.reduce((a, l) => a + l.unitUsd * l.count, 0); - if (express) s += 15; - return s; - }, [lines, express]); - - function submit() { - if (lines.length === 0 && !express) return; - const detail = [ - ...lines.map((l) => `${l.name} ×${l.count}`), - express ? "Express same-day (+$15)" : null, - ] - .filter(Boolean) - .join("; "); - addOrder({ - category: "laundry", - title: "Laundry · " + (lines.length ? `${lines.length} item type(s)` : "Express only"), - detail, - totalUsd: Math.round(subtotal * 100) / 100, - status: "pending", - }); - setQty({}); - setExpress(false); + setSubmitting(false); + setNotes(""); + setPickupAt(""); + setDeliverAt(""); + setEstimateEtb(""); setSent(true); } + const needBooking = !bookingLoading && !bookingId; + return (
@@ -88,62 +78,78 @@ function LaundryInner() { Laundry service

- Select pieces and optional express surcharge. Mock request — pickup at reception. + Submit a real laundry request attached to your active booking.

- + View profile →
+ {needBooking ? ( +
+ Sign in with a booking code or use a reservation on your account to sync laundry with the + hotel. +
+ ) : null} + + {submitErr ? ( +
+ {submitErr} +
+ ) : null} + {sent ? (
- Request logged (demo). Our team will confirm timing by phone. + Laundry request submitted successfully.
) : null}
-
- {laundryItems.map((row) => ( -
-
-

{row.name}

-

- {row.description} · ${row.priceUsd}/{row.unit} -

-
-
- - {qty[row.id] ?? 0} - -
-
- ))} -