From c1f34619524f2daa6a000d90700a23f1df165f1d Mon Sep 17 00:00:00 2001 From: brooktewabe Date: Wed, 15 Apr 2026 10:45:13 +0300 Subject: [PATCH] amenities and room service apis --- next.config.ts | 1 + src/app/guest/laundry/LaundryClient.tsx | 238 ++++++----- .../guest/room-service/RoomServiceClient.tsx | 2 +- src/app/services/ServicesPageClient.tsx | 376 +++++++----------- src/app/services/page.tsx | 2 +- src/components/RoomSelectBooking.tsx | 23 +- src/components/StoreHydration.tsx | 15 + src/context/CurrencyContext.tsx | 88 +--- src/lib/currency.ts | 7 +- src/lib/data/bookingReviews.ts | 62 +++ src/lib/data/guestData.ts | 71 ++++ src/lib/data/laundryCatalog.ts | 55 +-- src/lib/guest-hotel-api.ts | 5 +- src/lib/public-hotel-api.ts | 65 +++ src/lib/room-mapper.ts | 46 +++ src/stores/guest-ui-store.ts | 14 + 16 files changed, 606 insertions(+), 464 deletions(-) create mode 100644 src/components/StoreHydration.tsx create mode 100644 src/lib/data/bookingReviews.ts create mode 100644 src/lib/data/guestData.ts create mode 100644 src/lib/public-hotel-api.ts create mode 100644 src/lib/room-mapper.ts create mode 100644 src/stores/guest-ui-store.ts diff --git a/next.config.ts b/next.config.ts index 637fb97..1283376 100644 --- a/next.config.ts +++ b/next.config.ts @@ -4,6 +4,7 @@ const nextConfig: NextConfig = { images: { remotePatterns: [ { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" }, + { protocol: "http", hostname: "82.112.253.199", pathname: "/**" }, { protocol: "https", hostname: "images.pexels.com", pathname: "/**" }, { protocol: "https", hostname: "cf.bstatic.com", pathname: "/**" }, ], diff --git a/src/app/guest/laundry/LaundryClient.tsx b/src/app/guest/laundry/LaundryClient.tsx index 63db670..dd21a75 100644 --- a/src/app/guest/laundry/LaundryClient.tsx +++ b/src/app/guest/laundry/LaundryClient.tsx @@ -1,12 +1,13 @@ "use client"; import Link from "next/link"; -import { useState } from "react"; +import { useState, useEffect, useCallback } from "react"; import { RequireAuth } from "@/components/RequireAuth"; import { useAuth } from "@/context/AuthContext"; import { guestPlaceLaundry } from "@/lib/guest-hotel-api"; import { formatEtb } from "@/lib/format-etb"; import { useGuestActiveBooking } from "@/lib/useGuestActiveBooking"; +import { laundryItems, type LaundryCartItem, SAME_DAY_SURCHARGE } from "@/lib/data/laundryCatalog"; export function LaundryClient() { return ( @@ -19,87 +20,115 @@ export function LaundryClient() { function LaundryInner() { const { accessToken } = useAuth(); const { bookingId, loading: bookingLoading, propertyId } = useGuestActiveBooking(); - const [notes, setNotes] = useState(""); + + // Form states + const [cart, setCart] = useState>({}); + const [sameDay, setSameDay] = useState(false); const [pickupAt, setPickupAt] = useState(""); const [deliverAt, setDeliverAt] = useState(""); - const [estimateEtb, setEstimateEtb] = useState(""); + const [notes, setNotes] = useState(""); const [sent, setSent] = useState(false); const [submitErr, setSubmitErr] = useState(null); const [submitting, setSubmitting] = useState(false); const canUseApi = !!(propertyId && accessToken && bookingId); + // Compute total + const total = useCallback(() => { + let sum = 0; + for (const [label, qty] of Object.entries(cart)) { + if (qty > 0) { + const price = laundryItems.find(item => item.label.toLowerCase() === label.toLowerCase())?.price || 0; + sum += price * qty; + } + } + if (sameDay) sum += SAME_DAY_SURCHARGE; + return sum; + }, [cart, sameDay]); + + const [displayTotal, setDisplayTotal] = useState(0); + useEffect(() => { + setDisplayTotal(total()); + }, [total]); + + // Build items Json + const buildItems = (): LaundryCartItem[] => + Object.entries(cart) + .filter(([, qty]) => qty > 0) + .map(([label, quantity]) => ({ label: laundryItems.find(i => i.label.toLowerCase() === label.toLowerCase())?.label || label, quantity })); + async function submit() { - if (!canUseApi) return; + if (!canUseApi || buildItems().length === 0) { + setSubmitErr("Please select at least one item."); + return; + } setSubmitErr(null); setSubmitting(true); try { await guestPlaceLaundry(propertyId!, accessToken!, { bookingId: bookingId!, - items: [], + items: buildItems(), + sameDay, + total: displayTotal, + // currency: "ETB", notes: notes.trim() || undefined, pickupAt: pickupAt || undefined, deliverAt: deliverAt || undefined, - total: estimateEtb.trim() || undefined, }); + setSubmitting(false); + setCart({}); + setSameDay(false); + setPickupAt(""); + setDeliverAt(""); + setNotes(""); + setSent(true); } catch (e) { setSubmitErr(e instanceof Error ? e.message : "Could not submit laundry request"); setSubmitting(false); - return; } - setSubmitting(false); - setNotes(""); - setPickupAt(""); - setDeliverAt(""); - setEstimateEtb(""); - setSent(true); } const needBooking = !bookingLoading && !bookingId; + const hasItems = buildItems().length > 0; + + const updateQty = (label: string, delta: number) => { + setCart(prev => { + const current = prev[label] || 0; + const newQty = Math.max(0, current + delta); + const newCart = { ...prev }; + if (newQty === 0) delete newCart[label]; + else newCart[label] = newQty; + return newCart; + }); + }; return (
-

- Laundry service -

-

- Submit a real laundry request attached to your active booking. -

+

Laundry service

+

Submit a laundry request attached to your active booking.

- - View profile → - + View profile →
{needBooking ? (
- Sign in with a booking code or use a reservation on your account to sync laundry with the - hotel. + Sign in with a booking code or use a reservation to sync laundry with the hotel.
) : null} {submitErr ? ( -
- {submitErr} -
+
{submitErr}
) : null} {sent ? ( @@ -108,70 +137,97 @@ function LaundryInner() {
) : null} -
-
-
- + {!sent && ( +
+ {/* Items Selection */} +
+ +
+ {laundryItems.map((item) => { + const qty = cart[item.label.toLowerCase()] || 0; + return ( +
+
{item.label}
+
{formatEtb(item.price)} / each
+
+ + {qty} + +
+
+ ); + })} +
+
+ + {/* Summary & Form */} +
+
+ {formatEtb(displayTotal)} {sameDay && (incl. same-day)} +
+ + + +
+
+ + setPickupAt(e.target.value)} + className="w-full rounded-xl border border-[var(--color-border)] bg-[var(--color-surface-muted)] px-3 py-2 text-sm" + /> +
+
+ + setDeliverAt(e.target.value)} + className="w-full rounded-xl border border-[var(--color-border)] bg-[var(--color-surface-muted)] px-3 py-2 text-sm" + /> +
+
+