Files
ln-bi/src/modules/assets/library/VehicleDetails.tsx
T

334 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
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.
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>
);
}