334 lines
11 KiB
TypeScript
334 lines
11 KiB
TypeScript
import { inventoryColor } from "../../../shared/assets/inventory";
|
||
import { matchesInventorySelection, inventoryModel } from "./inventory-view";
|
||
import { ArrowLeft, X } from "lucide-react";
|
||
import { Button } from "@/components/ui/button";
|
||
import { useEffect, useRef, useState } from "react";
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
DialogDescription,
|
||
} from "@/components/ui/dialog";
|
||
import { Skeleton } from "@/components/ui/skeleton";
|
||
import {
|
||
fetchVehicleList,
|
||
fetchWeeklyDetail,
|
||
type WeeklyDetailItem,
|
||
} from "../api";
|
||
import { buildVehicleModalRequest, type VehicleModalSelection } from "../model";
|
||
import type { VehicleListItem } from "../types";
|
||
import { DataGrid, type Column } from "./DataGrid";
|
||
import { ExportButton } from "./export";
|
||
import { Failure, FilterFields, PrivateText, unique } from "./shared";
|
||
const titles = {
|
||
Inventory: "库存车辆",
|
||
Operating: "运营车辆",
|
||
Abnormal: "异动车辆",
|
||
Pending: "待交车车辆",
|
||
Delivered: "本周交车",
|
||
Returned: "本周还车",
|
||
Replaced: "本周替换交车",
|
||
};
|
||
export function VehicleDetails({
|
||
selection,
|
||
subject,
|
||
close,
|
||
returnLabel = "返回统计",
|
||
}: {
|
||
selection: VehicleModalSelection | null;
|
||
subject: string | null;
|
||
close: () => void;
|
||
returnLabel?: string;
|
||
}) {
|
||
const returnFocus = useRef<HTMLElement | null>(null);
|
||
const [vehicles, setVehicles] = useState<VehicleListItem[]>([]);
|
||
const [weekly, setWeekly] = useState<WeeklyDetailItem[]>([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState("");
|
||
const [retry, setRetry] = useState(0);
|
||
const [filters, setFilters] = useState({
|
||
model: "",
|
||
brandLabel: "",
|
||
location: "",
|
||
});
|
||
useEffect(() => {
|
||
let active = true;
|
||
setFilters({ model: "", brandLabel: "", location: "" });
|
||
setVehicles([]);
|
||
setWeekly([]);
|
||
setError("");
|
||
if (!selection) return;
|
||
setLoading(true);
|
||
const request = buildVehicleModalRequest(selection, subject);
|
||
const run = async () => {
|
||
try {
|
||
if (request.kind === "weekly") {
|
||
const rows = await fetchWeeklyDetail(request.type, {
|
||
...request.filters,
|
||
subject,
|
||
});
|
||
if (active) setWeekly(rows);
|
||
} else {
|
||
const rows = await fetchVehicleList(request.params);
|
||
if (active)
|
||
setVehicles(
|
||
rows.filter((row) => matchesInventorySelection(row, selection)),
|
||
);
|
||
}
|
||
} catch (e) {
|
||
if (active) setError(e instanceof Error ? e.message : "加载失败");
|
||
} finally {
|
||
if (active) setLoading(false);
|
||
}
|
||
};
|
||
void run();
|
||
return () => {
|
||
active = false;
|
||
};
|
||
}, [selection, subject, retry]);
|
||
const rows = vehicles
|
||
.filter((r) =>
|
||
Object.entries(filters).every(
|
||
([k, v]) => !v || r[k as keyof VehicleListItem] === v,
|
||
),
|
||
)
|
||
.map((r) => ({
|
||
...r,
|
||
color: inventoryColor(r.color),
|
||
displayModel: inventoryModel(r.model, r.brandLabel || "").name,
|
||
operator: inventoryModel(r.model, r.brandLabel || "").operator || "—",
|
||
pendingContract: r.pendingDelivery?.contractNo || "—",
|
||
pendingCustomer: r.pendingDelivery?.customerName || "—",
|
||
moveType: r.abnormalMove?.type || "—",
|
||
work: r.abnormalMove?.work || "—",
|
||
target: r.abnormalMove?.targetCompletionTime || "—",
|
||
actual: r.abnormalMove?.actualCompletionTime || "—",
|
||
initiating: r.abnormalMove?.initiatingDepartment || "—",
|
||
executing: r.abnormalMove?.executingDepartment || "—",
|
||
responsible: r.abnormalMove?.responsiblePerson || "—",
|
||
driver: r.abnormalMove?.driver || "—",
|
||
}));
|
||
type Row = (typeof rows)[number];
|
||
const columns: Column<Row>[] = [
|
||
{ key: "plateNumber", label: "车牌", private: true, width: 140 },
|
||
{ key: "brandLabel", label: "品牌" },
|
||
{ key: "type", label: "车型" },
|
||
{ key: "displayModel", label: "型号", width: 160 },
|
||
{ key: "operator", label: "所属公司" },
|
||
{ key: "color", label: "颜色" },
|
||
{ key: "location", label: "所在地" },
|
||
];
|
||
if (selection?.category === "Pending")
|
||
columns.push(
|
||
{
|
||
key: "pendingContract",
|
||
primary: true,
|
||
label: "待交车合同",
|
||
width: 180,
|
||
},
|
||
{
|
||
key: "pendingCustomer",
|
||
primary: true,
|
||
label: "客户(已脱敏)",
|
||
width: 180,
|
||
private: true,
|
||
},
|
||
);
|
||
if (selection?.category === "Abnormal")
|
||
columns.push(
|
||
...([
|
||
{ key: "moveType", primary: true, label: "异动类型" },
|
||
{ key: "target", primary: true, label: "预计结束时间", width: 180 },
|
||
{ key: "work", label: "具体工作", width: 220 },
|
||
{ key: "initiating", label: "发起部门" },
|
||
{ key: "executing", label: "执行部门" },
|
||
{ key: "responsible", label: "负责人", private: true },
|
||
{ key: "driver", label: "操作司机", private: true },
|
||
{ key: "actual", label: "实际完成时间", width: 180 },
|
||
] as Column<Row>[]),
|
||
);
|
||
columns.push(
|
||
{
|
||
key: "operationStatus",
|
||
label: "运营状态",
|
||
primary: true,
|
||
format: (value) =>
|
||
({ "1": "租赁", "2": "自营", "3": "可运营", "4": "待运营" })[
|
||
String(value)
|
||
] || String(value || "未标注"),
|
||
},
|
||
{
|
||
key: "status",
|
||
label: "业务状态",
|
||
primary: true,
|
||
format: (value) =>
|
||
({
|
||
Inventory: "库存",
|
||
Operating: "运营",
|
||
Abnormal: "异动",
|
||
Pending: "待交车",
|
||
})[String(value)] || String(value),
|
||
},
|
||
{ key: "departmentName", label: "部门" },
|
||
{ key: "customerManager", label: "客户经理", private: true },
|
||
{ key: "vin", label: "车架号", private: true, width: 200 },
|
||
);
|
||
const isWeekly =
|
||
selection &&
|
||
["Delivered", "Returned", "Replaced"].includes(selection.category || "");
|
||
const weeklyColumns: Column<WeeklyDetailItem>[] = [
|
||
{ key: "plate_number", label: "车牌", private: true },
|
||
{ key: "handover_date", label: "签章日期", width: 180 },
|
||
{ key: "contract_type", label: "合同类型" },
|
||
{ key: "customer_name", label: "客户", private: true },
|
||
];
|
||
const title =
|
||
selection?.title ||
|
||
(selection?.category ? titles[selection.category] : "车辆明细");
|
||
const context = [
|
||
["城市 / 区域", selection?.location],
|
||
["品牌", selection?.brand],
|
||
["车型", selection?.type || selection?.vehicleType],
|
||
["型号", selection?.model],
|
||
["批次", selection?.batch],
|
||
["部门", selection?.department],
|
||
].filter(([, value]) => value && value !== "All");
|
||
return (
|
||
<Dialog open={!!selection} onOpenChange={(v) => !v && close()}>
|
||
<DialogContent
|
||
className="asset-ui asset-detail"
|
||
showCloseButton={false}
|
||
onOpenAutoFocus={() => {
|
||
returnFocus.current =
|
||
document.activeElement instanceof HTMLElement
|
||
? document.activeElement
|
||
: null;
|
||
}}
|
||
onCloseAutoFocus={(event) => {
|
||
if (returnFocus.current?.isConnected) {
|
||
event.preventDefault();
|
||
returnFocus.current.focus({ preventScroll: true });
|
||
}
|
||
}}
|
||
>
|
||
<DialogHeader className="asset-detail-header">
|
||
<div className="flex items-center gap-3">
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={close}
|
||
aria-label={returnLabel}
|
||
>
|
||
<ArrowLeft />
|
||
</Button>
|
||
<div className="min-w-0 flex-1">
|
||
<p className="mb-1 text-xs text-[var(--label-secondary)]">
|
||
资产管理 /{" "}
|
||
{selection?.source === "department"
|
||
? "部门运营"
|
||
: selection?.source === "region"
|
||
? "区域分布"
|
||
: selection?.source === "customer"
|
||
? "客户资产"
|
||
: "资产统计"}
|
||
</p>
|
||
<DialogTitle className="text-lg font-semibold">
|
||
{title}
|
||
</DialogTitle>
|
||
</div>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
className="hidden md:inline-flex"
|
||
onClick={close}
|
||
aria-label="关闭车辆明细"
|
||
>
|
||
<X />
|
||
</Button>
|
||
</div>
|
||
<DialogDescription>
|
||
{subject || "全部公司"} ·{" "}
|
||
{loading
|
||
? "正在加载车辆…"
|
||
: `${isWeekly ? weekly.length : vehicles.length} 条记录`}
|
||
</DialogDescription>
|
||
{selection?.scopeLabel && (
|
||
<p className="text-sm">{selection.scopeLabel}</p>
|
||
)}
|
||
{context.length > 0 && (
|
||
<div className="asset-context" aria-label="当前下钻范围">
|
||
{context.map(([label, value]) => (
|
||
<span key={label}>
|
||
{label}:{value}
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
{selection?.customer && (
|
||
<p className="text-xs text-[var(--label-secondary)]">
|
||
客户:<PrivateText>{selection.customer}</PrivateText>
|
||
</p>
|
||
)}
|
||
{selection?.manager && (
|
||
<p className="text-xs text-[var(--label-secondary)]">
|
||
负责人:<PrivateText>{selection.manager}</PrivateText>
|
||
</p>
|
||
)}
|
||
</DialogHeader>
|
||
<div className="asset-detail-body" aria-busy={loading}>
|
||
{error ? (
|
||
<Failure message={error} retry={() => setRetry((n) => n + 1)} />
|
||
) : loading ? (
|
||
<Skeleton className="h-64" />
|
||
) : isWeekly ? (
|
||
<>
|
||
<ExportButton
|
||
rows={weekly}
|
||
columns={weeklyColumns}
|
||
filename="交还车车辆"
|
||
sheetName="交还车车辆"
|
||
/>
|
||
<DataGrid
|
||
rows={weekly}
|
||
label="交还车车辆"
|
||
columns={weeklyColumns}
|
||
/>
|
||
</>
|
||
) : (
|
||
<>
|
||
<FilterFields
|
||
fields={(["model", "brandLabel", "location"] as const).map(
|
||
(key, i) => ({
|
||
label: ["型号", "品牌", "所在地"][i],
|
||
value: filters[key],
|
||
values: unique(vehicles.map((r) => r[key])),
|
||
change: (value) =>
|
||
setFilters((f) => ({ ...f, [key]: value })),
|
||
}),
|
||
)}
|
||
onReset={() =>
|
||
setFilters({ model: "", brandLabel: "", location: "" })
|
||
}
|
||
/>
|
||
<ExportButton
|
||
rows={rows}
|
||
columns={columns}
|
||
filename="车辆明细"
|
||
sheetName="车辆"
|
||
/>
|
||
<DataGrid rows={rows} columns={columns} label="车辆" />
|
||
</>
|
||
)}
|
||
</div>
|
||
<footer className="asset-detail-footer">
|
||
<span>关闭后返回原来的筛选与浏览位置</span>
|
||
<Button variant="outline" onClick={close}>
|
||
{returnLabel}
|
||
</Button>
|
||
</footer>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
}
|