winofy-front/src/components/customer/address-details-form.tsx
2026-09-05 11:37:15 +03:30

240 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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<AddressLabel>("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<string | null>(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 (
<form onSubmit={handleSubmit} className="flex flex-1 flex-col gap-6 px-4 pb-6">
<div className="flex flex-col gap-2">
<span className="text-[13px] font-semibold leading-none text-gray-700">آدرس کامل</span>
<input
type="text"
value={address}
onChange={(e) => 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"
/>
</div>
<div className="flex flex-col gap-2">
<span className="text-[13px] font-semibold leading-none text-gray-700">عنوان آدرس</span>
<div className="flex items-center justify-start gap-2">
{LABEL_PRESETS.map((preset) => (
<button
key={preset.value}
type="button"
onClick={() => selectPreset(preset)}
className={`rounded-full border px-4 py-2 text-[13px] font-semibold whitespace-nowrap ${
label === preset.value
? "border-primary-action bg-blue-bg text-primary-action"
: "border-gray-300 bg-white text-gray-700"
}`}
>
{preset.text}
</button>
))}
</div>
</div>
<div className="grid grid-cols-3 gap-3">
<div className="flex flex-col gap-2">
<span className="text-[13px] font-semibold leading-none text-gray-700">پلاک</span>
<input
type="text"
value={plaque}
onChange={(e) => 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"
/>
</div>
<div className="flex flex-col gap-2">
<span className="text-[13px] font-semibold leading-none text-gray-700">طبقه</span>
<input
type="text"
value={floor}
onChange={(e) => 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"
/>
</div>
<div className="flex flex-col gap-2">
<span className="text-[13px] font-semibold leading-none text-gray-700">واحد</span>
<input
type="text"
value={unit}
onChange={(e) => 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"
/>
</div>
</div>
<div className="flex flex-col gap-2">
<span className="text-[13px] font-semibold leading-none text-gray-700">نام گیرنده</span>
<input
type="text"
value={recipientName}
onChange={(e) => 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"
/>
</div>
<div className="flex flex-col gap-2">
<span className="text-[13px] font-semibold leading-none text-gray-700">شماره تماس</span>
<input
type="tel"
dir="ltr"
value={recipientPhone}
onChange={(e) => 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"
/>
</div>
{needsCityPicker && (
<div className="flex flex-col gap-3 rounded-xl border border-gray-300 bg-gray-50 p-4">
<p className="text-[12px] leading-normal text-gray-700">
شهر شما مشخص نیست — این فقط لازم است چون هنوز محله‌ای از صفحه اصلی انتخاب نکرده‌اید.
</p>
<div className="grid grid-cols-2 gap-2">
<select
value={manualCityUuid}
onChange={(e) => {
setManualCityUuid(e.target.value);
setManualNeighborhoodUuid("");
}}
className="h-11 rounded-xl border border-gray-300 bg-white px-3 text-[13px]"
required
>
<option value="">شهر</option>
{cities.map((city) => (
<option key={city.uuid} value={city.uuid}>
{city.name}
</option>
))}
</select>
<select
value={manualNeighborhoodUuid}
onChange={(e) => setManualNeighborhoodUuid(e.target.value)}
className="h-11 rounded-xl border border-gray-300 bg-white px-3 text-[13px]"
disabled={!manualCityUuid}
>
<option value="">محله (اختیاری)</option>
{filteredNeighborhoods.map((n) => (
<option key={n.uuid} value={n.uuid}>
{n.name}
</option>
))}
</select>
</div>
</div>
)}
{error && <p className="text-[13px] text-error">{error}</p>}
<button
type="submit"
disabled={isPending}
className="mt-auto w-full rounded-2xl bg-primary-action py-4 text-[15px] font-bold text-white disabled:opacity-40"
>
{isPending ? "در حال ذخیره..." : "ذخیره آدرس"}
</button>
</form>
);
}