feat(assets): migrate asset views and reconcile inventory drilldowns
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user