From 2048e99bf219c2682185204759a67c4970701871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afra=20=E2=80=8C?= Date: Wed, 2 Sep 2026 15:14:43 +0330 Subject: [PATCH 1/4] feat: C08 page implemented --- .../(shop)/checkout/[store]/confirm/page.tsx | 14 ++++- src/components/customer/checkout-address.tsx | 54 +++++++++++++++++++ .../customer/checkout-fulfillment.tsx | 9 ++-- src/components/customer/checkout-payment.tsx | 14 +++-- src/lib/api/gooyal-accounts.ts | 8 +-- 5 files changed, 87 insertions(+), 12 deletions(-) create mode 100644 src/components/customer/checkout-address.tsx diff --git a/src/app/(customer)/(shop)/checkout/[store]/confirm/page.tsx b/src/app/(customer)/(shop)/checkout/[store]/confirm/page.tsx index 9411ca7..9a6b8d2 100644 --- a/src/app/(customer)/(shop)/checkout/[store]/confirm/page.tsx +++ b/src/app/(customer)/(shop)/checkout/[store]/confirm/page.tsx @@ -4,10 +4,16 @@ import { ArrowRight } from "lucide-react"; import { getCart } from "@/lib/api/cart"; import { getAddresses } from "@/lib/api/addresses"; import { requireSession } from "@/lib/auth/session"; +import { CheckoutAddress } from "@/components/customer/checkout-address"; import { CheckoutPayment } from "@/components/customer/checkout-payment"; -export default async function ConfirmPaymentPage({ params }: PageProps<"/checkout/[store]/confirm">) { +export default async function ConfirmPaymentPage({ + params, + searchParams, +}: PageProps<"/checkout/[store]/confirm">) { const { store } = await params; + const { fulfillment } = await searchParams; + const isDelivery = fulfillment === "delivery"; await requireSession(`/checkout/${store}/confirm`); const [cart, addresses] = await Promise.all([ @@ -19,6 +25,8 @@ export default async function ConfirmPaymentPage({ params }: PageProps<"/checkou const group = cart.groups.find((g) => g.store.uuid === store); if (!group) notFound(); + const address = addresses.find((a) => a.is_default) ?? addresses[0] ?? null; + return (
{/* Same header pattern as /cart and /checkout/[store]. */} @@ -29,7 +37,9 @@ export default async function ConfirmPaymentPage({ params }: PageProps<"/checkou

تأیید و پرداخت

- + {isDelivery && } + +
); } diff --git a/src/components/customer/checkout-address.tsx b/src/components/customer/checkout-address.tsx new file mode 100644 index 0000000..5210fc9 --- /dev/null +++ b/src/components/customer/checkout-address.tsx @@ -0,0 +1,54 @@ +import Link from "next/link"; +import { MapPin } from "lucide-react"; +import type { Address, AddressLabel } from "@/types/api"; + +const ADDRESS_LABEL_FA: Record = { + home: "خانه", + work: "محل کار", + other: "سایر", +}; + +/** + * "افزودن آدرس" / "تغییر آدرس" both just go to /addresses for now — the picker step + * in between (per the design) is a later task. + */ +export function CheckoutAddress({ address }: { address: Address | null }) { + return ( +
+

آدرس تحویل

+ + {address ? ( +
+ {/* DOM order intentionally mirrors the screenshot's left→right layout — see the + inherited dir="rtl" note on the /cart page. */} +
+ + تغییر آدرس + +
+ + {address.title || ADDRESS_LABEL_FA[address.label]} + + +
+
+

{address.full_address}

+
+ ) : ( +
+ + + +

+ هنوز آدرسی ثبت نکرده‌اید. برای ادامه سفارش، لطفاً یک آدرس اضافه کنید. +

+ + + +
+ )} +
+ ); +} diff --git a/src/components/customer/checkout-fulfillment.tsx b/src/components/customer/checkout-fulfillment.tsx index a3e638a..202b38e 100644 --- a/src/components/customer/checkout-fulfillment.tsx +++ b/src/components/customer/checkout-fulfillment.tsx @@ -94,9 +94,12 @@ export function CheckoutFulfillment({ group }: { group: CartGroup }) {
- {/* Pickup has its own confirm+pay screen (no address needed); delivery still - goes to the general /checkout form until that flow's design is provided. */} - + {/* Same confirm+pay screen either way — it shows the address section only for + delivery, and folds the delivery fee into the total only for delivery. */} +
- {isDelivery && } + {isDelivery && ( + + )} diff --git a/src/components/customer/address-details-form.tsx b/src/components/customer/address-details-form.tsx new file mode 100644 index 0000000..c745c81 --- /dev/null +++ b/src/components/customer/address-details-form.tsx @@ -0,0 +1,240 @@ +"use client"; + +import { useMemo, useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; +import { createAddress, type AddressInput } from "@/lib/api/addresses"; +import type { AddressLabel, City, Neighborhood } from "@/types/api"; + +const LABEL_PRESETS: { value: AddressLabel; text: string }[] = [ + { value: "other", text: "سایر" }, + { value: "work", text: "محل کار" }, + { value: "home", text: "خانه جدید" }, +]; + +export function AddressDetailsForm({ + fullAddress, + latitude, + longitude, + cityUuid, + neighborhoodUuid, + cities, + neighborhoods, + next, +}: { + fullAddress: string; + latitude: number; + longitude: number; + /** Inferred from the customer's current location cookie, if they'd set one manually. */ + cityUuid: string | null; + neighborhoodUuid: string | null; + cities: City[]; + neighborhoods: Neighborhood[]; + /** Set when this add-address detour started from checkout — see handleSubmit. */ + next: string | null; +}) { + const router = useRouter(); + const [address, setAddress] = useState(fullAddress); + const [label, setLabel] = useState("home"); + const [title, setTitle] = useState("خانه جدید"); + const [plaque, setPlaque] = useState(""); + const [floor, setFloor] = useState(""); + const [unit, setUnit] = useState(""); + const [recipientName, setRecipientName] = useState(""); + const [recipientPhone, setRecipientPhone] = useState(""); + const [manualCityUuid, setManualCityUuid] = useState(cityUuid ?? ""); + const [manualNeighborhoodUuid, setManualNeighborhoodUuid] = useState(neighborhoodUuid ?? ""); + const [error, setError] = useState(null); + const [isPending, startTransition] = useTransition(); + + // city_uuid is a hard requirement on the backend (OpenAPI: Address.city_uuid is + // required for writes) but isn't part of this screen's design — it's normally + // inferred from the customer's current location cookie. Only show a picker for + // it when that inference failed (e.g. they're in "geo" mode, or never set one). + const needsCityPicker = !cityUuid; + const filteredNeighborhoods = useMemo( + () => neighborhoods.filter((n) => n.city.uuid === manualCityUuid), + [neighborhoods, manualCityUuid], + ); + + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + + const finalCityUuid = cityUuid ?? manualCityUuid; + if (!finalCityUuid) { + setError("شهر مشخص نیست — لطفاً محله خود را از صفحه اصلی انتخاب کنید یا شهر را زیر مشخص کنید."); + return; + } + + startTransition(async () => { + const input: AddressInput = { + label, + title, + full_address: address, + plaque, + floor, + unit, + recipient_name: recipientName, + recipient_phone: recipientPhone, + city_uuid: finalCityUuid, + neighborhood_uuid: (neighborhoodUuid ?? manualNeighborhoodUuid) || undefined, + latitude, + longitude, + // Arriving here from checkout means they had no address at all yet — make + // this one the default immediately so it's actually used for the order, + // rather than sending them on to /addresses to select it a second time. + // A plain profile-driven "add another address" leaves the existing default alone. + is_default: next !== null, + }; + const result = await createAddress(input); + if (result.ok) { + router.push(next ?? "/addresses"); + } else { + setError(result.message); + } + }); + } + + function selectPreset(preset: (typeof LABEL_PRESETS)[number]) { + setLabel(preset.value); + setTitle(preset.text); + } + + return ( +
+
+ آدرس کامل + setAddress(e.target.value)} + required + className="h-12 rounded-xl border border-gray-300 bg-white px-4 text-[14px] text-foreground outline-none focus:border-primary-action" + /> +
+ +
+ عنوان آدرس +
+ {LABEL_PRESETS.map((preset) => ( + + ))} +
+
+ +
+
+ پلاک + setPlaque(e.target.value)} + className="h-12 rounded-xl border border-gray-300 bg-white px-3 text-center text-[14px] text-foreground outline-none focus:border-primary-action" + /> +
+
+ طبقه + setFloor(e.target.value)} + className="h-12 rounded-xl border border-gray-300 bg-white px-3 text-center text-[14px] text-foreground outline-none focus:border-primary-action" + /> +
+
+ واحد + setUnit(e.target.value)} + className="h-12 rounded-xl border border-gray-300 bg-white px-3 text-center text-[14px] text-foreground outline-none focus:border-primary-action" + /> +
+
+ +
+ نام گیرنده + setRecipientName(e.target.value)} + required + className="h-12 rounded-xl border border-gray-300 bg-white px-4 text-[14px] text-foreground outline-none focus:border-primary-action" + /> +
+ +
+ شماره تماس + setRecipientPhone(e.target.value)} + placeholder="09123456789" + required + className="h-12 rounded-xl border border-gray-300 bg-white px-4 text-[14px] text-foreground outline-none placeholder:text-gray-400 focus:border-primary-action" + /> +
+ + {needsCityPicker && ( +
+

+ شهر شما مشخص نیست — این فقط لازم است چون هنوز محله‌ای از صفحه اصلی انتخاب نکرده‌اید. +

+
+ + +
+
+ )} + + {error &&

{error}

} + + +
+ ); +} diff --git a/src/components/customer/address-list.tsx b/src/components/customer/address-list.tsx deleted file mode 100644 index c80af60..0000000 --- a/src/components/customer/address-list.tsx +++ /dev/null @@ -1,198 +0,0 @@ -"use client"; - -import { useMemo, useState, useTransition } from "react"; -import { useRouter } from "next/navigation"; -import { createAddress, deleteAddress, type AddressInput } from "@/lib/api/addresses"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import type { Address, AddressLabel, City, Neighborhood } from "@/types/api"; - -const LABEL_OPTIONS: { value: AddressLabel; label: string }[] = [ - { value: "home", label: "خانه" }, - { value: "work", label: "محل کار" }, - { value: "other", label: "سایر" }, -]; - -const emptyForm = { - label: "home" as AddressLabel, - title: "", - full_address: "", - plaque: "", - floor: "", - unit: "", - recipient_name: "", - recipient_phone: "", - city_uuid: "", - neighborhood_uuid: "", - is_default: false, -}; - -export function AddressList({ - addresses, - cities, - neighborhoods, -}: { - addresses: Address[]; - cities: City[]; - neighborhoods: Neighborhood[]; -}) { - const router = useRouter(); - const [isAdding, setIsAdding] = useState(false); - const [form, setForm] = useState(emptyForm); - const [error, setError] = useState(null); - const [isPending, startTransition] = useTransition(); - - const filteredNeighborhoods = useMemo( - () => neighborhoods.filter((n) => n.city.uuid === form.city_uuid), - [neighborhoods, form.city_uuid], - ); - - function handleDelete(uuid: string) { - startTransition(async () => { - await deleteAddress(uuid); - router.refresh(); - }); - } - - function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - setError(null); - startTransition(async () => { - const input: AddressInput = { - ...form, - neighborhood_uuid: form.neighborhood_uuid || undefined, - latitude: 0, - longitude: 0, - }; - const result = await createAddress(input); - if (result.ok) { - setForm(emptyForm); - setIsAdding(false); - router.refresh(); - } else { - setError(result.message); - } - }); - } - - return ( -
-
- {addresses.map((address) => ( -
-
- - {LABEL_OPTIONS.find((o) => o.value === address.label)?.label} {address.is_default && "· پیش‌فرض"} - - {address.full_address} - - {address.recipient_name} — {address.recipient_phone} - -
- -
- ))} - {addresses.length === 0 && !isAdding &&

آدرسی ثبت نشده است.

} -
- - {isAdding ? ( -
-
- {LABEL_OPTIONS.map((opt) => ( - - ))} -
- - setForm((f) => ({ ...f, full_address: e.target.value }))} - required - /> -
- setForm((f) => ({ ...f, plaque: e.target.value }))} /> - setForm((f) => ({ ...f, floor: e.target.value }))} /> - setForm((f) => ({ ...f, unit: e.target.value }))} /> -
- -
- - -
- - setForm((f) => ({ ...f, recipient_name: e.target.value }))} - required - /> - setForm((f) => ({ ...f, recipient_phone: e.target.value }))} - required - /> - - - - {error &&

{error}

} - -
- - -
-
- ) : ( - - )} -
- ); -} diff --git a/src/components/customer/address-selector.tsx b/src/components/customer/address-selector.tsx new file mode 100644 index 0000000..b718482 --- /dev/null +++ b/src/components/customer/address-selector.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; +import { updateAddress } from "@/lib/api/addresses"; +import type { Address, AddressLabel } from "@/types/api"; + +const ADDRESS_LABEL_FA: Record = { + home: "خانه", + work: "محل کار", + other: "سایر", +}; + +/** + * Doubles as both a plain address-management screen (reached from the profile menu) + * and a picker (reached from checkout's "تغییر آدرس" with a `next` return path) — + * the radio always reflects/sets which address is the default, "تأیید" just persists + * that choice and, if there's somewhere to return to, goes back there. + */ +export function AddressSelector({ addresses, next }: { addresses: Address[]; next: string | null }) { + const router = useRouter(); + const [selectedUuid, setSelectedUuid] = useState( + () => addresses.find((a) => a.is_default)?.uuid ?? addresses[0]?.uuid ?? null, + ); + const [error, setError] = useState(null); + const [isPending, startTransition] = useTransition(); + + function handleConfirm() { + setError(null); + const currentDefaultUuid = addresses.find((a) => a.is_default)?.uuid; + if (!selectedUuid || selectedUuid === currentDefaultUuid) { + if (next) router.push(next); + return; + } + startTransition(async () => { + const result = await updateAddress(selectedUuid, { is_default: true }); + if (result.ok) { + if (next) router.push(next); + else router.refresh(); + } else { + setError(result.message); + } + }); + } + + return ( +
+
+ {addresses.map((address) => { + const isSelected = address.uuid === selectedUuid; + return ( + + ); + })} + + {addresses.length === 0 && ( +

هنوز آدرسی ثبت نکرده‌اید.

+ )} + + +
+ + {error &&

{error}

} + + {addresses.length > 0 && ( + + )} +
+ ); +} diff --git a/src/components/customer/checkout-address.tsx b/src/components/customer/checkout-address.tsx index 5210fc9..5c64ed9 100644 --- a/src/components/customer/checkout-address.tsx +++ b/src/components/customer/checkout-address.tsx @@ -9,30 +9,45 @@ const ADDRESS_LABEL_FA: Record = { }; /** - * "افزودن آدرس" / "تغییر آدرس" both just go to /addresses for now — the picker step - * in between (per the design) is a later task. + * Both actions carry `returnTo` (the checkout confirm page's own URL) so the whole + * address-management chain — map → details form → address list — comes back here + * instead of stranding the customer on /addresses mid-checkout. */ -export function CheckoutAddress({ address }: { address: Address | null }) { +export function CheckoutAddress({ + address, + returnTo, +}: { + address: Address | null; + returnTo: string; +}) { + const next = encodeURIComponent(returnTo); return (
-

آدرس تحویل

+

+ آدرس تحویل +

{address ? ( -
+
{/* DOM order intentionally mirrors the screenshot's left→right layout — see the inherited dir="rtl" note on the /cart page. */} -
- - تغییر آدرس - +
+ {address.title || ADDRESS_LABEL_FA[address.label]} -
+

+ {address.full_address} +

-

{address.full_address}

+ + تغییر آدرس +
) : (
@@ -40,10 +55,14 @@ export function CheckoutAddress({ address }: { address: Address | null }) {

- هنوز آدرسی ثبت نکرده‌اید. برای ادامه سفارش، لطفاً یک آدرس اضافه کنید. + هنوز آدرسی ثبت نکرده‌اید. برای ادامه سفارش، لطفاً یک آدرس اضافه + کنید.

- - diff --git a/src/components/location/address-map-leaflet.tsx b/src/components/location/address-map-leaflet.tsx new file mode 100644 index 0000000..d36e365 --- /dev/null +++ b/src/components/location/address-map-leaflet.tsx @@ -0,0 +1,98 @@ +"use client"; + +import "leaflet/dist/leaflet.css"; +import { useState } from "react"; +import { MapContainer, TileLayer, ZoomControl, useMap, useMapEvents } from "react-leaflet"; +import { LocateFixed } from "lucide-react"; + +const ADDWIN_TILE_URL = "https://maps.addwin.ir/styles/basic/{z}/{x}/{y}.png"; + +/** + * Reports the map's center whenever panning/zooming settles — this drives the + * "pin fixed at screen-center, map moves underneath" UX (see the CSS pin in + * AddressMapLeaflet below), not a draggable react-leaflet . + */ +function CenterTracker({ onMove }: { onMove: (lat: number, lng: number) => void }) { + useMapEvents({ + moveend: (e) => { + const c = e.target.getCenter(); + onMove(c.lat, c.lng); + }, + }); + return null; +} + +/** Recenters the map on the browser's reported GPS position — same navigator.geolocation + * pattern as the location-onboarding flow (permission/error screens). */ +function RecenterButton() { + const map = useMap(); + const [locating, setLocating] = useState(false); + + function handleClick() { + setLocating(true); + navigator.geolocation.getCurrentPosition( + (position) => { + map.flyTo([position.coords.latitude, position.coords.longitude], 16); + setLocating(false); + }, + () => setLocating(false), + { timeout: 10000 }, + ); + } + + return ( + + ); +} + +export function AddressMapLeaflet({ + initialCenter, + onCenterChange, +}: { + initialCenter: [number, number]; + onCenterChange: (lat: number, lng: number) => void; +}) { + return ( +
+ + + + + + + + {/* Fixed screen-center pin — its tip (not its center) marks the selected point, + so it's offset up by its own height rather than centered on both axes. */} +
+
+ +
+
+
+ ); +} diff --git a/src/components/location/address-map-picker.tsx b/src/components/location/address-map-picker.tsx new file mode 100644 index 0000000..f32f0bc --- /dev/null +++ b/src/components/location/address-map-picker.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { useState } from "react"; +import dynamic from "next/dynamic"; +import { useRouter, useSearchParams } from "next/navigation"; + +// Leaflet touches `window` at import time — this component can only ever run in +// the browser, so it's dynamically imported with ssr:false from this client +// component (Server Components/pages can't pass ssr:false themselves in the +// App Router, hence this wrapper layer). +const AddressMapLeaflet = dynamic( + () => import("./address-map-leaflet").then((m) => m.AddressMapLeaflet), + { + ssr: false, + loading: () =>
, + }, +); + +// Central Tehran — used only when there's no prior location to center on. +const DEFAULT_CENTER: [number, number] = [35.7219, 51.339]; + +export function AddressMapPicker({ initialCenter }: { initialCenter: [number, number] | null }) { + const router = useRouter(); + const searchParams = useSearchParams(); + const next = searchParams.get("next"); + const [center, setCenter] = useState(initialCenter ?? DEFAULT_CENTER); + const [address, setAddress] = useState(""); + + function handleConfirm() { + const params = new URLSearchParams({ + lat: String(center[0]), + lng: String(center[1]), + address, + }); + if (next) params.set("next", next); + router.push(`/addresses/new?${params.toString()}`); + } + + return ( +
+
+ setCenter([lat, lng])} /> +
+ +
+
+ موقعیت انتخابی + {/* No reverse-geocoding endpoint is documented for Addwin or Winofy (checked + the OpenAPI spec — Address only ever takes latitude/longitude and a + separate free-text full_address, never derives one from the other), so + this is a real editable field styled to match the design, not an + auto-filled label. */} + setAddress(e.target.value)} + placeholder="آدرس را وارد کنید — مثلاً خیابان، کوچه، پلاک" + className="border-0 bg-transparent p-0 text-[15px] font-semibold leading-normal text-foreground outline-none placeholder:font-normal placeholder:text-gray-400" + /> +
+ + +
+
+ ); +} -- 2.45.3 From c2b4c4a69050ad1d816bd1160cd6f7509c912aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afra=20=E2=80=8C?= Date: Sat, 5 Sep 2026 14:00:55 +0330 Subject: [PATCH 3/4] fix: minor modification on cart page's design --- src/app/(customer)/(shop)/cart/page.tsx | 37 ++++++++++++++----------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/app/(customer)/(shop)/cart/page.tsx b/src/app/(customer)/(shop)/cart/page.tsx index 56ee906..a33af69 100644 --- a/src/app/(customer)/(shop)/cart/page.tsx +++ b/src/app/(customer)/(shop)/cart/page.tsx @@ -26,10 +26,7 @@ export default async function CartPage() { ); return ( -
- {/* DOM order intentionally mirrors the screenshot's left→right layout, not reading - order — under this app's inherited dir="rtl", the first child renders at the - right edge and the last at the left. */} +
@@ -49,7 +46,11 @@ export default async function CartPage() { )} @@ -65,19 +66,23 @@ export default async function CartPage() { ))}
-
-
- - جمع کل سبد ({cart.groups.length.toLocaleString("fa-IR")} فروشگاه،{" "} - {totalItemLines.toLocaleString("fa-IR")} کالا) +
+
+
+ + جمع کل سبد ({cart.groups.length.toLocaleString("fa-IR")} فروشگاه،{" "} + {totalItemLines.toLocaleString("fa-IR")} کالا) + + +

+ هر فروشگاه جداگانه پرداخت و ارسال می‌شود +

+
+ + + {cart.grand_total.toLocaleString("fa-IR")} تومان -

- هر فروشگاه جداگانه پرداخت و ارسال می‌شود -

- - {cart.grand_total.toLocaleString("fa-IR")} تومان -
); -- 2.45.3 From b548c5948482167c9143d173bbf2406b3872f741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afra=20=E2=80=8C?= Date: Sat, 5 Sep 2026 16:24:45 +0330 Subject: [PATCH 4/4] fix: single order checkout added --- src/app/(customer)/(shop)/checkout/page.tsx | 19 --- src/components/customer/checkout-form.tsx | 158 ------------------- src/components/customer/checkout-payment.tsx | 6 +- src/lib/api/checkout.ts | 16 +- 4 files changed, 19 insertions(+), 180 deletions(-) delete mode 100644 src/app/(customer)/(shop)/checkout/page.tsx delete mode 100644 src/components/customer/checkout-form.tsx diff --git a/src/app/(customer)/(shop)/checkout/page.tsx b/src/app/(customer)/(shop)/checkout/page.tsx deleted file mode 100644 index 2182a1b..0000000 --- a/src/app/(customer)/(shop)/checkout/page.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { redirect } from "next/navigation"; -import { getCart } from "@/lib/api/cart"; -import { requireSession } from "@/lib/auth/session"; -import { getAddresses } from "@/lib/api/addresses"; -import { CheckoutForm } from "@/components/customer/checkout-form"; - -export default async function CheckoutPage() { - await requireSession("/checkout"); - const [cart, addresses] = await Promise.all([getCart(), getAddresses()]); - - if (cart.groups.length === 0) redirect("/cart"); - - return ( -
-

تکمیل سفارش

- -
- ); -} diff --git a/src/components/customer/checkout-form.tsx b/src/components/customer/checkout-form.tsx deleted file mode 100644 index a8af62d..0000000 --- a/src/components/customer/checkout-form.tsx +++ /dev/null @@ -1,158 +0,0 @@ -"use client"; - -import { useState, useTransition } from "react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { submitCheckout } from "@/lib/api/checkout"; -import { Button } from "@/components/ui/button"; -import type { Address, Cart, DeliveryType, PaymentMethod } from "@/types/api"; - -const PAYMENT_LABELS_FA: Record = { - wallet: "کیف پول وینوفای", - online: "درگاه بانکی", - cash_on_delivery: "پرداخت در محل", -}; - -export function CheckoutForm({ cart, addresses }: { cart: Cart; addresses: Address[] }) { - const router = useRouter(); - const [addressUuid, setAddressUuid] = useState(addresses.find((a) => a.is_default)?.uuid ?? addresses[0]?.uuid ?? ""); - const [deliveryType, setDeliveryType] = useState("express"); - const [scheduledAt, setScheduledAt] = useState(""); - const [paymentMethod, setPaymentMethod] = useState("cash_on_delivery"); - const [notes, setNotes] = useState(""); - const [error, setError] = useState(null); - const [isPending, startTransition] = useTransition(); - - function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - setError(null); - - if (!addressUuid) { - setError("لطفاً یک آدرس انتخاب کنید."); - return; - } - if (deliveryType === "scheduled" && !scheduledAt) { - setError("لطفاً زمان ارسال را مشخص کنید."); - return; - } - - startTransition(async () => { - const result = await submitCheckout({ - address_uuid: addressUuid, - delivery_type: deliveryType, - scheduled_at: deliveryType === "scheduled" ? new Date(scheduledAt).toISOString() : null, - payment_method: paymentMethod, - notes, - }); - if (result.ok) { - router.push(`/order-groups/${result.data.uuid}`); - } else { - setError(result.message); - } - }); - } - - if (addresses.length === 0) { - return ( -
-

برای ثبت سفارش ابتدا یک آدرس اضافه کنید.

- - - -
- ); - } - - return ( -
-
-

آدرس تحویل

- {addresses.map((address) => ( - - ))} - - مدیریت آدرس‌ها - -
- -
-

زمان ارسال

-
- - -
- {deliveryType === "scheduled" && ( - setScheduledAt(e.target.value)} - className="h-11 rounded-xl border border-border bg-background px-3 text-sm" - /> - )} -
- -
-

روش پرداخت

-
- {(Object.keys(PAYMENT_LABELS_FA) as PaymentMethod[]).map((method) => ( - - ))} -
-
- -
-

توضیحات (اختیاری)

-