From 0658e59d9b7bf63c54936e3ed654cba558635ac2 Mon Sep 17 00:00:00 2001 From: m7amedez5511 Date: Sun, 23 Aug 2026 15:34:18 +0300 Subject: [PATCH] edit location info --- src/Components/Branch/BranchFormModal.tsx | 257 +++++++++++++---- src/Components/Order/OrderFormModal.tsx | 2 +- src/Components/Order/OrderTable.tsx | 4 +- src/data/location.ts | 54 ++++ src/data/locationHierarchy.ts | 18 ++ src/data/saudiLocationHierarchy.ts | 323 ++++++++++++++++++++++ src/hooks/useBranch.ts | 7 +- src/lib/locationHierarchy.ts | 127 +++++++++ src/validations/branch.validator.ts | 96 +++++-- 9 files changed, 809 insertions(+), 79 deletions(-) create mode 100644 src/data/location.ts create mode 100644 src/data/locationHierarchy.ts create mode 100644 src/data/saudiLocationHierarchy.ts create mode 100644 src/lib/locationHierarchy.ts diff --git a/src/Components/Branch/BranchFormModal.tsx b/src/Components/Branch/BranchFormModal.tsx index 6f58d05..b88d323 100644 --- a/src/Components/Branch/BranchFormModal.tsx +++ b/src/Components/Branch/BranchFormModal.tsx @@ -1,54 +1,144 @@ "use client"; -import { useForm } from "react-hook-form"; +import { useEffect, useMemo, useState } from "react"; +import { useForm, Controller } from "react-hook-form"; import { yupResolver } from "@hookform/resolvers/yup"; -import { Alert, Button, Input, Modal } from "../UI"; +import { Alert, Button, Input, Modal, Select } from "../UI"; import { createBranchSchema, updateBranchSchema } from "@/src/validations/branch.validator"; +import { + getRegions, + getCitiesByRegion, + getDistrictsByCity, + resolveLocation, + findLocationByNames, +} from "@/src/lib/locationHierarchy"; import type { Branch, BranchFormData } from "@/src/types/branch"; const FORM_ID = "branch-form"; +// Step 1: Extend the plain form-values shape with the three hierarchy ids. +// BranchFormData itself stays untouched (see src/types/branch.ts) — city/ +// state/district there are still plain strings, since that's the API +// contract. This local type is what react-hook-form actually manages. +interface BranchFormValues extends BranchFormData { + regionId: string; + cityId: string; + districtId: string; +} + // ── Props ───────────────────────────────────────────────────────────────────── interface BranchFormModalProps { editBranch: Branch | null; - onClose: () => void; - onSubmit: (data: BranchFormData, isNew: boolean) => Promise; + onClose: () => void; + onSubmit: (data: BranchFormData, isNew: boolean) => Promise; } // ── main component ──────────────────────────────────────────────────────────── export function BranchFormModal({ editBranch, onClose, onSubmit }: BranchFormModalProps) { const isNew = editBranch === null; + // Step 2: On edit, try to resolve the branch's stored city/state/district + // NAMES back into ids against the current dataset. If nothing matches + // (the value is a legacy/unknown location no longer in the hierarchy), + // resolved stays null and the form falls back to empty selects — the + // person must actively re-pick a valid Region -> City -> District chain + // before they can save, rather than silently keeping a stale value. + const resolvedEdit = useMemo( + () => (editBranch ? findLocationByNames(editBranch.state, editBranch.city, editBranch.district) : null), + [editBranch], + ); + const isLegacyLocation = !isNew && !resolvedEdit && !!(editBranch?.city || editBranch?.district); + const { register, handleSubmit, setError, + setValue, + watch, + control, formState: { errors, isSubmitting }, - } = useForm({ - // Cast the schema itself and pin the generic explicitly on yupResolver — - // create/update schemas differ structurally in which fields are required - // (e.g. city/street are optional on update), so yup can't unify them - // into the single flat BranchFormData shape on its own. - resolver: yupResolver((isNew ? createBranchSchema : updateBranchSchema) as any), + } = useForm({ + resolver: yupResolver((isNew ? createBranchSchema : updateBranchSchema) as any), defaultValues: { - name: editBranch?.name ?? "", - email: editBranch?.email ?? "", - phone: editBranch?.phone ?? "", - country: editBranch?.country ?? "SA", - city: editBranch?.city ?? "", - state: editBranch?.state ?? "", - district: editBranch?.district ?? "", - street: editBranch?.street ?? "", + name: editBranch?.name ?? "", + email: editBranch?.email ?? "", + phone: editBranch?.phone ?? "", + country: editBranch?.country ?? "SA", + regionId: resolvedEdit?.regionId ?? "", + cityId: resolvedEdit?.cityId ?? "", + districtId: resolvedEdit?.districtId ?? "", + city: editBranch?.city ?? "", + state: editBranch?.state ?? "", + district: editBranch?.district ?? "", + street: editBranch?.street ?? "", buildingNo: editBranch?.buildingNo ?? "", - unitNo: editBranch?.unitNo ?? "", - zipCode: editBranch?.zipCode ?? "", - latitude: editBranch?.latitude != null ? String(editBranch.latitude) : "", - longitude: editBranch?.longitude != null ? String(editBranch.longitude) : "", + unitNo: editBranch?.unitNo ?? "", + zipCode: editBranch?.zipCode ?? "", + latitude: resolvedEdit?.latitude != null ? String(resolvedEdit.latitude) : editBranch?.latitude != null ? String(editBranch.latitude) : "", + longitude: resolvedEdit?.longitude != null ? String(resolvedEdit.longitude) : editBranch?.longitude != null ? String(editBranch.longitude) : "", }, }); - const submitHandler = async (data: BranchFormData) => { - const ok = await onSubmit(data, isNew); + // Step 3: Watch the two parent selects so the child dropdowns can filter + // themselves reactively (cascading Region -> City -> District). + const watchedRegionId = watch("regionId"); + const watchedCityId = watch("cityId"); + const watchedDistrictId = watch("districtId"); + + const regions = useMemo(() => getRegions(), []); + const cities = useMemo(() => getCitiesByRegion(watchedRegionId), [watchedRegionId]); + const districts = useMemo( + () => getDistrictsByCity(watchedRegionId, watchedCityId), + [watchedRegionId, watchedCityId], + ); + + // Step 4: Whenever the region changes, clear any city/district selection + // that no longer belongs to it — prevents the exact "Mohandessin under + // Cairo" class of bug from ever reaching submit. + useEffect(() => { + if (!watchedCityId) return; + const stillValid = cities.some((c) => c.id === watchedCityId); + if (!stillValid) { + setValue("cityId", ""); + setValue("districtId", ""); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [watchedRegionId]); + + useEffect(() => { + if (!watchedDistrictId) return; + const stillValid = districts.some((d) => d.id === watchedDistrictId); + if (!stillValid) setValue("districtId", ""); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [watchedCityId]); + + // Step 5: Whenever the fully-resolved district changes, auto-populate + // latitude/longitude from the dataset — no manual coordinate entry. + useEffect(() => { + const resolved = resolveLocation(watchedRegionId, watchedCityId, watchedDistrictId); + if (resolved) { + setValue("latitude", String(resolved.latitude)); + setValue("longitude", String(resolved.longitude)); + } + }, [watchedRegionId, watchedCityId, watchedDistrictId, setValue]); + + const submitHandler = async (data: BranchFormValues) => { + // Step 6: Resolve the chosen ids into display names right before + // building the payload, so the API keeps receiving plain strings for + // city/state/district exactly as it does today — this UI change is + // fully transparent to the backend contract. + const resolved = resolveLocation(data.regionId, data.cityId, data.districtId); + + const payload: BranchFormData = { + ...data, + state: resolved?.regionName ?? data.state, + city: resolved?.cityName ?? data.city, + district: resolved?.districtName ?? data.district, + latitude: resolved ? String(resolved.latitude) : data.latitude, + longitude: resolved ? String(resolved.longitude) : data.longitude, + }; + + const ok = await onSubmit(payload, isNew); if (ok) { onClose(); } else { @@ -84,6 +174,17 @@ export function BranchFormModal({ editBranch, onClose, onSubmit }: BranchFormMod {}} /> )} + {/* Step 7: Warn once if the stored location no longer matches any + valid Region -> City -> District chain, and require a fresh + selection instead of silently keeping the legacy value. */} + {isLegacyLocation && ( + {}} + /> + )} + {/* name */} - {/* city + street */} + {/* Step 8: Cascading Region -> City -> District selects. Each + child ( + + )} /> + + ( + + )} + /> + + +
+ ( + + )} + /> +
- {/* state + district */} -
- - -
- {/* buildingNo + unitNo + zipCode */}
- {/* latitude + longitude */} + {/* Step 9: latitude/longitude are now READ-ONLY — they are derived + automatically from the selected district (Step 5 above), so + there is no manual entry and therefore no coordinate-vs-district + mismatch possible from this form. */}
diff --git a/src/Components/Order/OrderFormModal.tsx b/src/Components/Order/OrderFormModal.tsx index 4426782..ad573ee 100644 --- a/src/Components/Order/OrderFormModal.tsx +++ b/src/Components/Order/OrderFormModal.tsx @@ -373,7 +373,7 @@ export function OrderFormModal({