import React, { useEffect, useMemo, useRef, useState } from "react"; import { Edit, MapPin, MoreHorizontal, Trash2 } from "lucide-react"; import { Button } from "../ui/button"; import { Checkbox } from "../ui/checkbox"; import { Input } from "../ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../ui/table"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "../ui/select"; import { Label } from "../ui/label"; import { Badge } from "../ui/badge"; import { cn } from "../ui/utils"; import { Switch } from "../ui/switch"; import { toast } from "sonner"; import { skipCountForPage } from "../../lib/paginationQuery"; import { useCategoryScopeAuth } from "../../hooks/useCategoryScopeAuth"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, } from "../ui/pagination"; import { createLocation, deleteLocation, downloadLocationImportTemplate, exportLocationsExcel, getLocations, importLocationsBatch, updateLocation, } from "../../services/locationService"; import { getPartners } from "../../services/partnerService"; import { getGroups } from "../../services/groupService"; import type { LocationCreateInput, LocationDto } from "../../types/location"; import type { GroupListItem } from "../../types/group"; import type { PartnerListItem } from "../../types/partner"; import { BatchImportDialog } from "../bulk/batch-import-dialog"; import { LocationBulkEditPage } from "./location-bulk-edit-page"; const LOCATION_PG_NONE = "__none__"; async function loadActivePartnerOptions(signal: AbortSignal): Promise { const out: PartnerListItem[] = []; let page = 1; const size = 100; for (;;) { const res = await getPartners({ skipCount: page, maxResultCount: size, state: true }, signal); out.push(...(res.items ?? [])); if (!res.items || res.items.length < size) break; page += 1; if (page > 200) break; } const m = new Map(); for (const p of out) if (p.id && !m.has(p.id)) m.set(p.id, p); return Array.from(m.values()); } async function loadActiveGroupsForPartner(partnerId: string, signal: AbortSignal): Promise { const out: GroupListItem[] = []; let page = 1; const size = 100; for (;;) { const res = await getGroups( { skipCount: page, maxResultCount: size, partnerId, state: true }, signal, ); out.push(...(res.items ?? [])); if (!res.items || res.items.length < size) break; page += 1; if (page > 200) break; } const m = new Map(); for (const g of out) if (g.id && !m.has(g.id)) m.set(g.id, g); return Array.from(m.values()); } function toDisplay(v: string | null | undefined): string { const s = (v ?? "").trim(); return s ? s : "N/A"; } function formatGps(lat: number | null | undefined, lng: number | null | undefined): string { if (lat === null || lat === undefined || lng === null || lng === undefined) return "N/A"; if (!Number.isFinite(lat) || !Number.isFinite(lng)) return "N/A"; return `${lat}, ${lng}`; } /** * 门店表单红框区域:Location ID / Name、地址、联系方式(Company、Region、Active、GPS 不要求) * 仅校验是否填写,不校验邮箱/电话格式。 */ function getLocationRedBoxValidationErrors(form: LocationCreateInput): string[] { const errs: string[] = []; if (!form.locationCode.trim()) errs.push("Location ID"); if (!form.locationName.trim()) errs.push("Location Name"); if (!(form.street ?? "").trim()) errs.push("Street"); if (!(form.city ?? "").trim()) errs.push("City"); if (!(form.stateCode ?? "").trim()) errs.push("State"); if (!(form.country ?? "").trim()) errs.push("Country"); if (!(form.zipCode ?? "").trim()) errs.push("Zip Code"); if (!(form.phone ?? "").trim()) errs.push("Phone Number"); if (!(form.email ?? "").trim()) errs.push("Email"); return errs; } export type LocationsViewProps = { /** * 嵌入 Account Management 时:将「搜索/筛选/操作」条交给父组件排版(例如放在 Tab 上方)。 * 传入的 `toolbar` 为门店管理原有工具栏节点,父组件应将其与 Tab 等一并渲染。 */ renderBeforeTabs?: (toolbar: React.ReactNode) => React.ReactNode; /** * 为 false 时隐藏新建、批量导入/批量编辑、行勾选及编辑/删除(非平台管理员)。 * @default true */ canMutateLocations?: boolean; }; export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: LocationsViewProps = {}) { const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [editingLocation, setEditingLocation] = useState(null); const [deletingLocation, setDeletingLocation] = useState(null); const [locations, setLocations] = useState([]); const [loading, setLoading] = useState(false); const [total, setTotal] = useState(0); const [refreshSeq, setRefreshSeq] = useState(0); const [actionsOpenForId, setActionsOpenForId] = useState(null); const [selectedIds, setSelectedIds] = useState>(() => new Set()); const [bulkImportOpen, setBulkImportOpen] = useState(false); const [locationBulkEditPage, setLocationBulkEditPage] = useState(false); const [bulkEditSeed, setBulkEditSeed] = useState([]); const [tmplDownloading, setTmplDownloading] = useState(false); const [excelExporting, setExcelExporting] = useState(false); const [keyword, setKeyword] = useState(""); const [partner, setPartner] = useState("all"); const [groupName, setGroupName] = useState("all"); const [locationPick, setLocationPick] = useState("all"); const [pageIndex, setPageIndex] = useState(1); const [pageSize, setPageSize] = useState(10); const abortRef = useRef(null); const keywordTimerRef = useRef(null); const [debouncedKeyword, setDebouncedKeyword] = useState(""); useEffect(() => { if (keywordTimerRef.current) window.clearTimeout(keywordTimerRef.current); keywordTimerRef.current = window.setTimeout(() => setDebouncedKeyword(keyword.trim()), 300); return () => { if (keywordTimerRef.current) window.clearTimeout(keywordTimerRef.current); }; }, [keyword]); const listKeyword = useMemo( () => (locationPick !== "all" ? locationPick : debouncedKeyword.trim()), [locationPick, debouncedKeyword], ); // Options derived from current result set (no dedicated endpoints provided in doc). const partnerOptions = useMemo(() => { const s = new Set(); for (const x of locations) { const v = (x.partner ?? "").trim(); if (v) s.add(v); } return ["all", ...Array.from(s).sort((a, b) => a.localeCompare(b))]; }, [locations]); const groupOptions = useMemo(() => { const s = new Set(); for (const x of locations) { const v = (x.groupName ?? "").trim(); if (v) s.add(v); } return ["all", ...Array.from(s).sort((a, b) => a.localeCompare(b))]; }, [locations]); const locationOptions = useMemo(() => { const s = new Set(); for (const x of locations) { const v = (x.locationCode ?? "").trim(); if (v) s.add(v); } return ["all", ...Array.from(s).sort((a, b) => a.localeCompare(b))]; }, [locations]); const totalPages = Math.max(1, Math.ceil(total / pageSize)); useEffect(() => { // When filter changes, reset to first page. setPageIndex(1); // eslint-disable-next-line react-hooks/exhaustive-deps }, [debouncedKeyword, partner, groupName, locationPick, pageSize]); useEffect(() => { const run = async () => { abortRef.current?.abort(); const ac = new AbortController(); abortRef.current = ac; setLoading(true); try { const skipCount = skipCountForPage(pageIndex); const res = await getLocations( { skipCount, maxResultCount: pageSize, keyword: listKeyword || undefined, partner: partner !== "all" ? partner : undefined, groupName: groupName !== "all" ? groupName : undefined, }, ac.signal, ); setLocations(res.items ?? []); setTotal(res.totalCount ?? 0); } catch (e: any) { if (e?.name === "AbortError") return; toast.error("Failed to load locations.", { description: e?.message ? String(e.message) : "Please try again.", }); setLocations([]); setTotal(0); } finally { setLoading(false); } }; run(); return () => abortRef.current?.abort(); }, [listKeyword, partner, groupName, locationPick, pageIndex, pageSize, refreshSeq]); useEffect(() => { setSelectedIds(new Set()); }, [debouncedKeyword, partner, groupName, locationPick, pageIndex]); useEffect(() => { if (!canMutateLocations) { setLocationBulkEditPage(false); setBulkEditSeed([]); setSelectedIds(new Set()); } }, [canMutateLocations]); const refreshList = () => setRefreshSeq((x) => x + 1); const openEdit = (loc: LocationDto) => { setActionsOpenForId(null); setEditingLocation(loc); setIsEditDialogOpen(true); }; const openDelete = (loc: LocationDto) => { setActionsOpenForId(null); setDeletingLocation(loc); setIsDeleteDialogOpen(true); }; const toolbarSection = (
setKeyword(e.target.value)} style={{ height: 40, boxSizing: "border-box" }} className="border border-gray-300 rounded-md w-40 shrink-0 bg-white placeholder:text-gray-500" />
{canMutateLocations && ( <> )} {canMutateLocations && ( <> )}
); const locationTableColSpan = canMutateLocations ? 15 : 14; const tableAndPagination = ( <>
{canMutateLocations ? ( 0 && locations.every((l) => selectedIds.has(l.id))} onCheckedChange={(c) => { if (c === true) setSelectedIds(new Set(locations.map((l) => l.id))); else setSelectedIds(new Set()); }} aria-label="Select all on page" /> ) : null} Company Region Location ID Location Name Street City State Country Zip Code Phone Email GPS Active Actions {loading ? ( Loading... ) : locations.length === 0 ? ( No results. ) : ( locations.map((loc) => ( {canMutateLocations ? ( { setSelectedIds((prev) => { const n = new Set(prev); if (c === true) n.add(loc.id); else n.delete(loc.id); return n; }); }} aria-label="Select row" /> ) : null} {toDisplay(loc.partner)} {toDisplay(loc.groupName)} {toDisplay(loc.locationCode ?? loc.id)} {toDisplay(loc.locationName)} {toDisplay(loc.street)} {toDisplay(loc.city)} {toDisplay(loc.stateCode)} {toDisplay(loc.country)} {toDisplay(loc.zipCode)} {toDisplay(loc.phone)} {toDisplay(loc.email)} {formatGps(loc.latitude, loc.longitude)} {loc.state ? "Yes" : "No"} {canMutateLocations ? ( setActionsOpenForId(open ? loc.id : null)} > ) : ( "—" )} )) )}
Showing {total === 0 ? 0 : (pageIndex - 1) * pageSize + 1}- {Math.min(pageIndex * pageSize, total)} of {total}
{ e.preventDefault(); setPageIndex((p) => Math.max(1, p - 1)); }} aria-disabled={pageIndex <= 1} className={pageIndex <= 1 ? "pointer-events-none opacity-50" : ""} /> e.preventDefault()} > Page {pageIndex} / {totalPages} { e.preventDefault(); setPageIndex((p) => Math.min(totalPages, p + 1)); }} aria-disabled={pageIndex >= totalPages} className={pageIndex >= totalPages ? "pointer-events-none opacity-50" : ""} />
); const listOrBulkMain = locationBulkEditPage ? (
{ setLocationBulkEditPage(false); setBulkEditSeed([]); }} onSaved={() => { setSelectedIds(new Set()); refreshList(); }} />
) : ( tableAndPagination ); const dialogs = ( <> { // 新增后强制刷新一次列表;如果当前已在第一页也能刷新。 setPageIndex(1); refreshList(); }} /> { setIsEditDialogOpen(open); if (!open) setEditingLocation(null); }} onUpdated={() => { // 编辑后强制刷新一次列表 refreshList(); }} /> { setIsDeleteDialogOpen(open); if (!open) setDeletingLocation(null); }} onDeleted={() => { refreshList(); }} /> { setTmplDownloading(true); try { await downloadLocationImportTemplate(); toast.success("Template downloaded."); } catch (e: unknown) { const msg = e instanceof Error ? e.message : "Download failed."; toast.error("Template download failed", { description: msg }); } finally { setTmplDownloading(false); } }} onImportFile={async (file) => { const r = await importLocationsBatch(file); refreshList(); return { successCount: r.successCount, failCount: r.failCount }; }} /> ); if (renderBeforeTabs) { return (
{renderBeforeTabs(toolbarSection)}
{listOrBulkMain}
{dialogs}
); } return (
{toolbarSection}
{listOrBulkMain}
{dialogs}
); } // --- Sub-components --- function CreateLocationDialog({ open, onOpenChange, onCreated, }: { open: boolean; onOpenChange: (open: boolean) => void; onCreated: () => void; }) { const scopeAuth = useCategoryScopeAuth(); const [submitting, setSubmitting] = useState(false); const [form, setForm] = useState({ partner: "", groupName: "", locationCode: "", locationName: "", street: "", city: "", stateCode: "", country: "", zipCode: "", phone: "", email: "", operatingHours: "", latitude: null, longitude: null, state: true, }); const [partnerPickId, setPartnerPickId] = useState(LOCATION_PG_NONE); const [groupPickId, setGroupPickId] = useState(LOCATION_PG_NONE); const [partnerOptions, setPartnerOptions] = useState([]); const [groupOptions, setGroupOptions] = useState([]); const [loadingPartners, setLoadingPartners] = useState(false); const [loadingGroups, setLoadingGroups] = useState(false); const createAbortRef = useRef(null); const resetForm = () => { setForm({ partner: "", groupName: "", locationCode: "", locationName: "", street: "", city: "", stateCode: "", country: "", zipCode: "", phone: "", email: "", operatingHours: "", latitude: null, longitude: null, state: true, }); setPartnerPickId(LOCATION_PG_NONE); setGroupPickId(LOCATION_PG_NONE); setPartnerOptions([]); setGroupOptions([]); }; useEffect(() => { if (!open) { createAbortRef.current?.abort(); resetForm(); setSubmitting(false); } }, [open]); useEffect(() => { if (!open) return; createAbortRef.current?.abort(); const ac = new AbortController(); createAbortRef.current = ac; setLoadingPartners(true); (async () => { try { const list = await loadActivePartnerOptions(ac.signal); setPartnerOptions(list); if (!scopeAuth.requireCompanySelection) { const pid = scopeAuth.fixedPartnerId.trim(); if (pid && list.some((p) => p.id === pid)) { setPartnerPickId(pid); } } } catch (e: any) { if (e?.name === "AbortError") return; toast.error("Failed to load companies.", { description: e?.message ? String(e.message) : "Please try again.", }); setPartnerOptions([]); } finally { setLoadingPartners(false); } })(); return () => ac.abort(); }, [open, scopeAuth.requireCompanySelection, scopeAuth.fixedPartnerId]); useEffect(() => { if (!open || scopeAuth.requireCompanySelection || scopeAuth.loadingPartner) return; const pid = scopeAuth.fixedPartnerId.trim(); if (pid && partnerOptions.some((p) => p.id === pid)) { setPartnerPickId(pid); } }, [open, scopeAuth.requireCompanySelection, scopeAuth.fixedPartnerId, scopeAuth.loadingPartner, partnerOptions]); useEffect(() => { if (!open || partnerPickId === LOCATION_PG_NONE) { setGroupOptions([]); setGroupPickId(LOCATION_PG_NONE); setLoadingGroups(false); return; } const ac = new AbortController(); setLoadingGroups(true); (async () => { try { const list = await loadActiveGroupsForPartner(partnerPickId, ac.signal); setGroupOptions(list); setGroupPickId(LOCATION_PG_NONE); } catch (e: any) { if (e?.name === "AbortError") return; toast.error("Failed to load regions.", { description: e?.message ? String(e.message) : "Please try again.", }); setGroupOptions([]); } finally { setLoadingGroups(false); } })(); return () => ac.abort(); }, [open, partnerPickId]); const submit = async () => { const errs = getLocationRedBoxValidationErrors(form); if (errs.length) { toast.error("Please fill in required fields.", { description: `Missing: ${errs.join(", ")}.`, }); return; } const resolvedPartnerPickId = partnerPickId !== LOCATION_PG_NONE ? partnerPickId : !scopeAuth.requireCompanySelection ? scopeAuth.fixedPartnerId.trim() : ""; const p = resolvedPartnerPickId ? partnerOptions.find((x) => x.id === resolvedPartnerPickId) : undefined; const g = groupPickId !== LOCATION_PG_NONE ? groupOptions.find((x) => x.id === groupPickId) : undefined; setSubmitting(true); try { await createLocation({ ...form, locationCode: form.locationCode.trim(), locationName: form.locationName.trim(), partner: p?.partnerName?.trim() ? p.partnerName.trim() : null, groupName: g?.groupName?.trim() ? g.groupName.trim() : null, street: (form.street ?? "").trim(), city: (form.city ?? "").trim(), stateCode: (form.stateCode ?? "").trim(), country: (form.country ?? "").trim(), zipCode: (form.zipCode ?? "").trim(), phone: (form.phone ?? "").trim(), email: (form.email ?? "").trim(), operatingHours: (form.operatingHours ?? "").trim() || null, latitude: form.latitude ?? null, longitude: form.longitude ?? null, }); toast.success("Location created.", { description: "The location has been added successfully.", }); onOpenChange(false); onCreated(); } catch (e: any) { toast.error("Failed to create location.", { description: e?.message ? String(e.message) : "Please try again.", }); } finally { setSubmitting(false); } }; return ( Add New Location Enter the details for the new store location.
setForm((p) => ({ ...p, locationCode: e.target.value }))} />
setForm((p) => ({ ...p, locationName: e.target.value }))} />
setForm((p) => ({ ...p, street: e.target.value }))} />
setForm((p) => ({ ...p, city: e.target.value }))} />
setForm((p) => ({ ...p, stateCode: e.target.value }))} />
setForm((p) => ({ ...p, country: e.target.value }))} />
setForm((p) => ({ ...p, zipCode: e.target.value }))} />
setForm((p) => ({ ...p, phone: e.target.value }))} />
setForm((p) => ({ ...p, email: e.target.value }))} />
setForm((p) => ({ ...p, operatingHours: e.target.value }))} />
{ const raw = e.target.value.trim(); setForm((p) => ({ ...p, latitude: raw ? Number(raw) : null })); }} /> { const raw = e.target.value.trim(); setForm((p) => ({ ...p, longitude: raw ? Number(raw) : null })); }} />
setForm((p) => ({ ...p, state: v }))} />
); } function fromDtoToForm(loc: LocationDto): LocationCreateInput { return { partner: loc.partner ?? "", groupName: loc.groupName ?? "", locationCode: loc.locationCode ?? "", locationName: loc.locationName ?? "", street: loc.street ?? "", city: loc.city ?? "", stateCode: loc.stateCode ?? "", country: loc.country ?? "", zipCode: loc.zipCode ?? "", phone: loc.phone ?? "", email: loc.email ?? "", operatingHours: loc.operatingHours ?? "", latitude: loc.latitude ?? null, longitude: loc.longitude ?? null, state: !!loc.state, }; } function EditLocationDialog({ open, location, onOpenChange, onUpdated, }: { open: boolean; location: LocationDto | null; onOpenChange: (open: boolean) => void; onUpdated: () => void; }) { const [submitting, setSubmitting] = useState(false); const [form, setForm] = useState({ partner: "", groupName: "", locationCode: "", locationName: "", street: "", city: "", stateCode: "", country: "", zipCode: "", phone: "", email: "", operatingHours: "", latitude: null, longitude: null, state: true, }); const [partnerPickId, setPartnerPickId] = useState(LOCATION_PG_NONE); const [groupPickId, setGroupPickId] = useState(LOCATION_PG_NONE); const [partnerOptions, setPartnerOptions] = useState([]); const [groupOptions, setGroupOptions] = useState([]); const [loadingPartners, setLoadingPartners] = useState(false); const [loadingGroups, setLoadingGroups] = useState(false); const initialPartnerIdRef = useRef(LOCATION_PG_NONE); useEffect(() => { if (!open) { setSubmitting(false); setPartnerPickId(LOCATION_PG_NONE); setGroupPickId(LOCATION_PG_NONE); setPartnerOptions([]); setGroupOptions([]); initialPartnerIdRef.current = LOCATION_PG_NONE; return; } if (!location) return; setForm(fromDtoToForm(location)); setSubmitting(false); initialPartnerIdRef.current = LOCATION_PG_NONE; setPartnerPickId(LOCATION_PG_NONE); setGroupPickId(LOCATION_PG_NONE); const ac = new AbortController(); setLoadingPartners(true); (async () => { try { const plist = await loadActivePartnerOptions(ac.signal); setPartnerOptions(plist); const pname = (location.partner ?? "").trim().toLowerCase(); let pid: string | typeof LOCATION_PG_NONE = LOCATION_PG_NONE; if (pname) { const hit = plist.find((p) => (p.partnerName ?? "").trim().toLowerCase() === pname); if (hit) pid = hit.id; } initialPartnerIdRef.current = pid; setPartnerPickId(pid); } catch (e: any) { if (e?.name === "AbortError") return; toast.error("Failed to load companies.", { description: e?.message ? String(e.message) : "Please try again.", }); setPartnerOptions([]); } finally { setLoadingPartners(false); } })(); return () => ac.abort(); }, [open, location?.id]); useEffect(() => { if (!open || partnerPickId === LOCATION_PG_NONE) { setGroupOptions([]); setGroupPickId(LOCATION_PG_NONE); setLoadingGroups(false); return; } const ac = new AbortController(); setLoadingGroups(true); (async () => { try { const list = await loadActiveGroupsForPartner(partnerPickId, ac.signal); setGroupOptions(list); let nextG = LOCATION_PG_NONE; const refPid = initialPartnerIdRef.current; const canAuto = !!location && refPid !== "__user__" && refPid !== LOCATION_PG_NONE && partnerPickId === refPid; if (canAuto) { const gname = (location!.groupName ?? "").trim().toLowerCase(); if (gname) { const hit = list.find((g) => (g.groupName ?? "").trim().toLowerCase() === gname); if (hit) nextG = hit.id; } } setGroupPickId(nextG); } catch (e: any) { if (e?.name === "AbortError") return; toast.error("Failed to load regions.", { description: e?.message ? String(e.message) : "Please try again.", }); setGroupOptions([]); } finally { setLoadingGroups(false); } })(); return () => ac.abort(); }, [open, partnerPickId, location?.groupName, location?.id]); const submit = async () => { if (!location?.id) return; const errs = getLocationRedBoxValidationErrors(form); if (errs.length) { toast.error("Please fill in required fields.", { description: `Missing: ${errs.join(", ")}.`, }); return; } const p = partnerPickId !== LOCATION_PG_NONE ? partnerOptions.find((x) => x.id === partnerPickId) : undefined; const g = groupPickId !== LOCATION_PG_NONE ? groupOptions.find((x) => x.id === groupPickId) : undefined; setSubmitting(true); try { await updateLocation(location.id, { ...form, locationCode: form.locationCode.trim(), locationName: form.locationName.trim(), partner: p?.partnerName?.trim() ? p.partnerName.trim() : null, groupName: g?.groupName?.trim() ? g.groupName.trim() : null, street: (form.street ?? "").trim(), city: (form.city ?? "").trim(), stateCode: (form.stateCode ?? "").trim(), country: (form.country ?? "").trim(), zipCode: (form.zipCode ?? "").trim(), phone: (form.phone ?? "").trim(), email: (form.email ?? "").trim(), operatingHours: (form.operatingHours ?? "").trim() || null, latitude: form.latitude ?? null, longitude: form.longitude ?? null, }); toast.success("Location updated.", { description: "The changes have been saved successfully.", }); onOpenChange(false); onUpdated(); } catch (e: any) { toast.error("Failed to update location.", { description: e?.message ? String(e.message) : "Please try again.", }); } finally { setSubmitting(false); } }; return ( Edit Location Update the details for this store location.
setForm((p) => ({ ...p, locationCode: e.target.value }))} />
setForm((p) => ({ ...p, locationName: e.target.value }))} />
setForm((p) => ({ ...p, street: e.target.value }))} />
setForm((p) => ({ ...p, city: e.target.value }))} />
setForm((p) => ({ ...p, stateCode: e.target.value }))} />
setForm((p) => ({ ...p, country: e.target.value }))} />
setForm((p) => ({ ...p, zipCode: e.target.value }))} />
setForm((p) => ({ ...p, phone: e.target.value }))} />
setForm((p) => ({ ...p, email: e.target.value }))} />
setForm((p) => ({ ...p, operatingHours: e.target.value }))} />
{ const raw = e.target.value.trim(); setForm((p) => ({ ...p, latitude: raw ? Number(raw) : null })); }} /> { const raw = e.target.value.trim(); setForm((p) => ({ ...p, longitude: raw ? Number(raw) : null })); }} />
setForm((p) => ({ ...p, state: v }))} />
); } function DeleteLocationDialog({ open, location, onOpenChange, onDeleted, }: { open: boolean; location: LocationDto | null; onOpenChange: (open: boolean) => void; onDeleted: () => void; }) { const [submitting, setSubmitting] = useState(false); const name = useMemo(() => { const code = (location?.locationCode ?? "").trim(); const n = (location?.locationName ?? "").trim(); if (code && n) return `${code} - ${n}`; return code || n || "this location"; }, [location?.locationCode, location?.locationName]); const submit = async () => { if (!location?.id) return; setSubmitting(true); try { await deleteLocation(location.id); toast.success("Location deleted.", { description: "The location has been removed successfully.", }); onOpenChange(false); onDeleted(); } catch (e: any) { toast.error("Failed to delete location.", { description: e?.message ? String(e.message) : "Please try again.", }); } finally { setSubmitting(false); } }; return ( Delete Location This action cannot be undone.
Are you sure you want to delete {name}?
); }