Compare commits
2
Commits
7dd7e67602
...
5751d1af15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5751d1af15 | ||
|
|
24984fe033 |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ln-bi",
|
"name": "ln-bi",
|
||||||
"version": "1.1.18",
|
"version": "1.1.19",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ln-bi",
|
"name": "ln-bi",
|
||||||
"version": "1.1.18",
|
"version": "1.1.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@amap/amap-jsapi-loader": "^1.0.1",
|
"@amap/amap-jsapi-loader": "^1.0.1",
|
||||||
"@hono/node-server": "^1.13.0",
|
"@hono/node-server": "^1.13.0",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ln-bi",
|
"name": "ln-bi",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.1.18",
|
"version": "1.1.19",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently -n server,client -c blue,green \"npm run dev:server\" \"npm run dev:client\"",
|
"dev": "concurrently -n server,client -c blue,green \"npm run dev:server\" \"npm run dev:client\"",
|
||||||
|
|||||||
@@ -41,3 +41,13 @@ test('one Excel workbook contains statistics and all details in two named sheets
|
|||||||
const empty = await createAnalysisWorkbook([], []);
|
const empty = await createAnalysisWorkbook([], []);
|
||||||
assert.deepEqual(empty.SheetNames, ['统计表', '详情表']);
|
assert.deepEqual(empty.SheetNames, ['统计表', '详情表']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('inventory export uses assigned operations managers and a dash when unset', () => {
|
||||||
|
const vehicles = ['张三、李四', null, ' '].map(opsManagers => ({
|
||||||
|
opsManagers, customerManager: '业务负责人', customerName: '客户', status: 'Inventory',
|
||||||
|
} as VehicleListItem));
|
||||||
|
const rows = vehicleExportRows(vehicles, true);
|
||||||
|
assert.deepEqual(rows.map(row => row.运维负责人), ['张三、李四', '-', '-']);
|
||||||
|
assert.ok(rows.every(row => !('业务负责人' in row) && !('客户' in row)));
|
||||||
|
assert.equal(vehicleExportRows(vehicles)[0].业务负责人, '业务负责人');
|
||||||
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function statisticsExportRows(labels: string[], groups: { values: string[
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function vehicleExportRows(vehicles: VehicleListItem[]): ExportRow[] {
|
export function vehicleExportRows(vehicles: VehicleListItem[], inventory = false): ExportRow[] {
|
||||||
const status: Record<string, string> = { Abnormal: '异动', Pending: '待交车', Inventory: '在库', Operating: '运营中' };
|
const status: Record<string, string> = { Abnormal: '异动', Pending: '待交车', Inventory: '在库', Operating: '运营中' };
|
||||||
const operation: Record<string, string> = { '1': '租赁', '2': '自营', '3': '可运营', '4': '待运营' };
|
const operation: Record<string, string> = { '1': '租赁', '2': '自营', '3': '可运营', '4': '待运营' };
|
||||||
const hasPending = vehicles.some(v => v.pendingDelivery);
|
const hasPending = vehicles.some(v => v.pendingDelivery);
|
||||||
@@ -21,7 +21,7 @@ export function vehicleExportRows(vehicles: VehicleListItem[]): ExportRow[] {
|
|||||||
车牌: v.plateNumber, VIN: v.vin, 品牌: v.brandLabel, 车型: v.type, 型号: v.model,
|
车牌: v.plateNumber, VIN: v.vin, 品牌: v.brandLabel, 车型: v.type, 型号: v.model,
|
||||||
省份: v.province, 城市: v.city || v.location,
|
省份: v.province, 城市: v.city || v.location,
|
||||||
状态: status[v.status] || v.status, 运营状态: operation[v.operationStatus || ''] || v.operationStatus,
|
状态: status[v.status] || v.status, 运营状态: operation[v.operationStatus || ''] || v.operationStatus,
|
||||||
客户: v.customerName, 签约公司: v.orgName, 部门: v.departmentName, 业务负责人: v.customerManager,
|
...(inventory ? { 运维负责人: v.opsManagers?.trim() || '-' } : { 客户: v.customerName, 签约公司: v.orgName, 部门: v.departmentName, 业务负责人: v.customerManager }),
|
||||||
所属公司: v.subjectOrg, 合同: v.contractNo,
|
所属公司: v.subjectOrg, 合同: v.contractNo,
|
||||||
...(hasPending ? { 待交车合同: v.pendingDelivery?.contractNo, 待交车客户: v.pendingDelivery?.customerName } : {}),
|
...(hasPending ? { 待交车合同: v.pendingDelivery?.contractNo, 待交车客户: v.pendingDelivery?.customerName } : {}),
|
||||||
...(hasMovement ? {
|
...(hasMovement ? {
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ function groupRows(rows: FlowDetailItem[], dimensions: Dimension[]): FlowGroup[]
|
|||||||
group.rows.push(row);
|
group.rows.push(row);
|
||||||
groups.set(key, group);
|
groups.set(key, group);
|
||||||
}
|
}
|
||||||
return Array.from(groups.values()).sort((a, b) => b.rows.length - a.rows.length || a.key.localeCompare(b.key, 'zh-CN'));
|
const dateIndex = dimensions.indexOf('date');
|
||||||
|
return Array.from(groups.values()).sort((a, b) => (dateIndex >= 0 ? b.values[dateIndex].localeCompare(a.values[dateIndex]) : 0) || b.rows.length - a.rows.length || a.key.localeCompare(b.key, 'zh-CN'));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FlowAnalysisModal(props: FlowAnalysisModalProps) {
|
export function FlowAnalysisModal(props: FlowAnalysisModalProps) {
|
||||||
@@ -68,7 +69,8 @@ function FlowAnalysisContent({ selectedFlow, selectedFlowDetails, setSelectedFlo
|
|||||||
useDialogFocus(true, dialog, close);
|
useDialogFocus(true, dialog, close);
|
||||||
|
|
||||||
const rows = useMemo(() => selectedFlowDetails.filter(row => matchesType(row, selectedFlow.type)
|
const rows = useMemo(() => selectedFlowDetails.filter(row => matchesType(row, selectedFlow.type)
|
||||||
&& ('date' in selectedFlow ? row.date === selectedFlow.date : row.date >= selectedFlow.start && row.date <= selectedFlow.end)), [selectedFlowDetails, selectedFlow]);
|
&& ('date' in selectedFlow ? row.date === selectedFlow.date : row.date >= selectedFlow.start && row.date <= selectedFlow.end))
|
||||||
|
.sort((a, b) => b.date.localeCompare(a.date) || (b.eventTime || '').localeCompare(a.eventTime || '')), [selectedFlowDetails, selectedFlow]);
|
||||||
const filterOptions = useMemo(() => Object.fromEntries(DIMENSIONS.map(({ key }) => {
|
const filterOptions = useMemo(() => Object.fromEntries(DIMENSIONS.map(({ key }) => {
|
||||||
const eligible = rows.filter(row => DIMENSIONS.every(({ key: other }) => other === key || !filters[other]?.length || filters[other]!.includes(dimensionValue(row, other))));
|
const eligible = rows.filter(row => DIMENSIONS.every(({ key: other }) => other === key || !filters[other]?.length || filters[other]!.includes(dimensionValue(row, other))));
|
||||||
const counts = new Map<string, number>();
|
const counts = new Map<string, number>();
|
||||||
@@ -79,7 +81,8 @@ function FlowAnalysisContent({ selectedFlow, selectedFlowDetails, setSelectedFlo
|
|||||||
const filteredRows = useMemo(() => rows.filter(row => DIMENSIONS.every(({ key }) => !filters[key]?.length || filters[key]!.includes(dimensionValue(row, key)))), [rows, filters]);
|
const filteredRows = useMemo(() => rows.filter(row => DIMENSIONS.every(({ key }) => !filters[key]?.length || filters[key]!.includes(dimensionValue(row, key)))), [rows, filters]);
|
||||||
const groups = useMemo(() => {
|
const groups = useMemo(() => {
|
||||||
const flat = groupRows(filteredRows, dimensions);
|
const flat = groupRows(filteredRows, dimensions);
|
||||||
return mergeActive ? orderDimensionGroups(flat, dimensions.length, group => group.values, group => group.rows.length) : flat;
|
return mergeActive ? orderDimensionGroups(flat, dimensions.length, group => group.values, group => group.rows.length, dimensions.includes('date') ? 'label' : 'count-desc',
|
||||||
|
(a, b, index) => dimensions[index] === 'date' ? b.values[index].localeCompare(a.values[index]) : a.values[index].localeCompare(b.values[index], 'zh-CN')) : flat;
|
||||||
}, [filteredRows, dimensions, mergeActive]);
|
}, [filteredRows, dimensions, mergeActive]);
|
||||||
const detailGroup = useMemo(() => detailKey === null ? null : groups.find(group => group.key === detailKey), [groups, detailKey]);
|
const detailGroup = useMemo(() => detailKey === null ? null : groups.find(group => group.key === detailKey), [groups, detailKey]);
|
||||||
const detailRows = detailGroup?.rows ?? EMPTY_FLOW_ROWS;
|
const detailRows = detailGroup?.rows ?? EMPTY_FLOW_ROWS;
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ function AnalysisDialog({ selection, subject, close }: Props & { selection: Vehi
|
|||||||
extra: view === 'inventory' ? { 待交车: row.totals.pending, 异动: row.totals.abnormal } : undefined })),
|
extra: view === 'inventory' ? { 待交车: row.totals.pending, 异动: row.totals.abnormal } : undefined })),
|
||||||
showDetails ? rows.length : analysis.total,
|
showDetails ? rows.length : analysis.total,
|
||||||
);
|
);
|
||||||
await downloadAnalysisWorkbook(statistics, vehicleExportRows(rows), `${title}-${scopeLabel}${detailGroup ? `-${detailGroup.label}` : ''}`);
|
await downloadAnalysisWorkbook(statistics, vehicleExportRows(rows, view === 'inventory'), `${title}-${scopeLabel}${detailGroup ? `-${detailGroup.label}` : ''}`);
|
||||||
};
|
};
|
||||||
return <div className="asset-analysis-overlay">
|
return <div className="asset-analysis-overlay">
|
||||||
<div ref={dialog} role="dialog" aria-modal="true" aria-label={title} tabIndex={-1} className={`asset-analysis-dialog ${view === 'abnormal' ? 'analysis-amber' : ''}`}>
|
<div ref={dialog} role="dialog" aria-modal="true" aria-label={title} tabIndex={-1} className={`asset-analysis-dialog ${view === 'abnormal' ? 'analysis-amber' : ''}`}>
|
||||||
@@ -164,7 +164,7 @@ function AnalysisDialog({ selection, subject, close }: Props & { selection: Vehi
|
|||||||
<div className="analysis-table-tools"><h3>车辆明细 <small>{number(visibleVehicles.length)} 台</small></h3><AnalysisExportButton disabled={detailBusy || !!detailError || !visibleVehicles.length} onExport={exportWorkbook} /><label className="analysis-search"><Search size={15} /><input aria-label="搜索车辆明细" placeholder="搜索车牌、VIN、型号、城市" value={search} onChange={event => { setSearch(event.target.value); }} /></label></div>
|
<div className="analysis-table-tools"><h3>车辆明细 <small>{number(visibleVehicles.length)} 台</small></h3><AnalysisExportButton disabled={detailBusy || !!detailError || !visibleVehicles.length} onExport={exportWorkbook} /><label className="analysis-search"><Search size={15} /><input aria-label="搜索车辆明细" placeholder="搜索车牌、VIN、型号、城市" value={search} onChange={event => { setSearch(event.target.value); }} /></label></div>
|
||||||
{detailBusy ? <div className="analysis-message" role="status"><Loader2 className="animate-spin" />正在加载车辆明细…</div> : detailError ? <div className="analysis-message" role="alert"><p>{detailError}</p><button className="analysis-button" onClick={() => setDetailRetry(detailRetry + 1)}>重新加载明细</button></div> : <>
|
{detailBusy ? <div className="analysis-message" role="status"><Loader2 className="animate-spin" />正在加载车辆明细…</div> : detailError ? <div className="analysis-message" role="alert"><p>{detailError}</p><button className="analysis-button" onClick={() => setDetailRetry(detailRetry + 1)}>重新加载明细</button></div> : <>
|
||||||
{vehicles.length !== detailGroup.count && <p className="analysis-definition">车辆状态可能已更新:统计快照 {detailGroup.count} 台,当前明细 {vehicles.length} 台。</p>}
|
{vehicles.length !== detailGroup.count && <p className="analysis-definition">车辆状态可能已更新:统计快照 {detailGroup.count} 台,当前明细 {vehicles.length} 台。</p>}
|
||||||
{(view === 'abnormal' || detailGroup.values.inventoryState === 'abnormal' || (filterValues.inventoryState?.length === 1 && filterValues.inventoryState[0] === 'abnormal')) ? <AbnormalVehicleDetails vehicles={shownVehicles} /> : (view === 'pending' || detailGroup.values.inventoryState === 'pending' || (filterValues.inventoryState?.length === 1 && filterValues.inventoryState[0] === 'pending')) ? <PendingVehicleDetails vehicles={shownVehicles} /> : <VehicleRows vehicles={shownVehicles} />}
|
{(view === 'abnormal' || detailGroup.values.inventoryState === 'abnormal' || (filterValues.inventoryState?.length === 1 && filterValues.inventoryState[0] === 'abnormal')) ? <AbnormalVehicleDetails vehicles={shownVehicles} /> : (view === 'pending' || detailGroup.values.inventoryState === 'pending' || (filterValues.inventoryState?.length === 1 && filterValues.inventoryState[0] === 'pending')) ? <PendingVehicleDetails vehicles={shownVehicles} /> : <VehicleRows vehicles={shownVehicles} inventory={view === 'inventory'} />}
|
||||||
<ProgressiveRowsStatus {...detailProgress} unit="台" />
|
<ProgressiveRowsStatus {...detailProgress} unit="台" />
|
||||||
</>}
|
</>}
|
||||||
</div>}
|
</div>}
|
||||||
@@ -175,14 +175,14 @@ function AnalysisDialog({ selection, subject, close }: Props & { selection: Vehi
|
|||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function VehicleRows({ vehicles }: { vehicles: VehicleListItem[] }) {
|
function VehicleRows({ vehicles, inventory = false }: { vehicles: VehicleListItem[]; inventory?: boolean }) {
|
||||||
if (!vehicles.length) return <div className="analysis-message">暂无匹配车辆</div>;
|
if (!vehicles.length) return <div className="analysis-message">暂无匹配车辆</div>;
|
||||||
const stateLabel = (vehicle: VehicleListItem) => {
|
const stateLabel = (vehicle: VehicleListItem) => {
|
||||||
const operation = ({ '1': '租赁', '2': '自营', '3': '可运营', '4': '待运营' } as Record<string, string>)[vehicle.operationStatus || ''] || vehicle.operationStatus;
|
const operation = ({ '1': '租赁', '2': '自营', '3': '可运营', '4': '待运营' } as Record<string, string>)[vehicle.operationStatus || ''] || vehicle.operationStatus;
|
||||||
const business = ({ Abnormal: '异动', Pending: '待交车', Inventory: '在库', Operating: '运营中' } as Record<string, string>)[vehicle.status] || vehicle.status;
|
const business = ({ Abnormal: '异动', Pending: '待交车', Inventory: '在库', Operating: '运营中' } as Record<string, string>)[vehicle.status] || vehicle.status;
|
||||||
return [business, operation].filter(Boolean).join(' · ');
|
return [business, operation].filter(Boolean).join(' · ');
|
||||||
};
|
};
|
||||||
return <div className="analysis-detail-table" tabIndex={0} role="region" aria-label="车辆明细表"><table><thead><tr>{['车牌 / VIN', '品牌 / 车型 / 型号', '省份 / 城市', '状态', '客户 / 签约公司', '部门 / 业务负责人', '所属公司'].map(label => <th key={label}>{label}</th>)}</tr></thead><tbody>{vehicles.map(vehicle => <tr key={vehicle.id}><td><Blur>{vehicle.plateNumber || vehicle.vin || '未提供'}</Blur></td><td>{vehicle.brandLabel || '—'} · {vehicle.type}<small>{vehicle.model}</small></td><td>{vehicle.province || '未标注'} / {vehicle.city || vehicle.location || '未标注'}</td><td>{stateLabel(vehicle)}</td><td><Blur>{vehicle.customerName || '—'}</Blur><small><Blur>{vehicle.orgName || '—'}</Blur></small></td><td><Blur>{vehicle.departmentName || '—'}</Blur><small><Blur>{vehicle.customerManager || '—'}</Blur></small></td><td>{vehicle.subjectOrg || '—'}</td></tr>)}</tbody></table></div>;
|
return <div className="analysis-detail-table" tabIndex={0} role="region" aria-label="车辆明细表"><table><thead><tr>{['车牌 / VIN', '品牌 / 车型 / 型号', '省份 / 城市', '状态', ...(inventory ? ['运维负责人'] : ['客户 / 签约公司', '部门 / 业务负责人']), '所属公司'].map(label => <th key={label}>{label}</th>)}</tr></thead><tbody>{vehicles.map(vehicle => <tr key={vehicle.id}><td><Blur>{vehicle.plateNumber || vehicle.vin || '未提供'}</Blur></td><td>{vehicle.brandLabel || '—'} · {vehicle.type}<small>{vehicle.model}</small></td><td>{vehicle.province || '未标注'} / {vehicle.city || vehicle.location || '未标注'}</td><td>{stateLabel(vehicle)}</td>{inventory ? <td><Blur>{vehicle.opsManagers?.trim() || '-'}</Blur></td> : <><td><Blur>{vehicle.customerName || '—'}</Blur><small><Blur>{vehicle.orgName || '—'}</Blur></small></td><td><Blur>{vehicle.departmentName || '—'}</Blur><small><Blur>{vehicle.customerManager || '—'}</Blur></small></td></>}<td>{vehicle.subjectOrg || '—'}</td></tr>)}</tbody></table></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type WeeklyDimension = 'date' | 'contract_type';
|
type WeeklyDimension = 'date' | 'contract_type';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CalendarDays, ChevronDown, Download, Loader2 } from 'lucide-react';
|
import { CalendarDays, ChevronDown, Download, ListFilter, Loader2 } from 'lucide-react';
|
||||||
import { AnimatePresence, motion } from 'motion/react';
|
import { AnimatePresence, motion } from 'motion/react';
|
||||||
import type { FlowType } from '../../api';
|
import type { FlowType } from '../../api';
|
||||||
import { FLOW_META } from '../FlowDetailModal';
|
import { FLOW_META } from '../FlowDetailModal';
|
||||||
@@ -65,18 +65,24 @@ export function FlowStatisticsCard({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-testid="asset-flow-daily-toggle"
|
data-testid="asset-flow-daily-toggle"
|
||||||
|
aria-expanded={flowDailyExpanded}
|
||||||
|
aria-controls="asset-flow-daily-details"
|
||||||
onClick={() => setFlowDailyExpanded((prev) => !prev)}
|
onClick={() => setFlowDailyExpanded((prev) => !prev)}
|
||||||
className="mt-2 flex w-full items-center justify-between rounded-xl border border-slate-100 bg-white px-3 py-1.5 text-[12px] font-black text-slate-500 transition hover:border-blue-100 hover:bg-blue-50/50 hover:text-blue-600"
|
className="mt-3 flex min-h-16 w-full items-center justify-between gap-3 rounded-xl border border-blue-200 bg-blue-50 px-4 py-3 text-left text-blue-700 shadow-sm transition hover:border-blue-400 hover:bg-blue-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<span>{flowDailyExpanded ? '收起每日明细' : '展开每日明细'}</span>
|
<span className="flex min-w-0 items-center gap-3">
|
||||||
<span className="flex items-center gap-2 text-[11px] text-slate-400">
|
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-white text-blue-600"><ListFilter size={21} /></span>
|
||||||
{flowStats?.daily.length ?? 0} 天
|
<span><span className="block text-sm font-black">{flowDailyExpanded ? '收起每日明细' : '展开每日明细'}</span><span className="mt-0.5 block text-[11px] font-medium text-blue-600">查看每日交还车数量 · 最新日期在前</span></span>
|
||||||
<ChevronDown size={15} className={`transition-transform ${flowDailyExpanded ? 'rotate-180' : ''}`} />
|
</span>
|
||||||
|
<span className="flex shrink-0 items-center gap-2 text-xs font-bold">
|
||||||
|
<span className="rounded-lg bg-white px-2 py-1">{flowStats?.daily.length ?? 0} 天</span>
|
||||||
|
<ChevronDown size={20} className={`transition-transform ${flowDailyExpanded ? 'rotate-180' : ''}`} />
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<AnimatePresence initial={false}>
|
<AnimatePresence initial={false}>
|
||||||
{flowDailyExpanded && (
|
{flowDailyExpanded && (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
id="asset-flow-daily-details"
|
||||||
initial={{ height: 0, opacity: 0 }}
|
initial={{ height: 0, opacity: 0 }}
|
||||||
animate={{ height: 'auto', opacity: 1 }}
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
exit={{ height: 0, opacity: 0 }}
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ export interface InventoryTypeSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface VehicleListItem {
|
export interface VehicleListItem {
|
||||||
|
opsManagers?: string | null;
|
||||||
color?: string;
|
color?: string;
|
||||||
operationStatus?: string | null;
|
operationStatus?: string | null;
|
||||||
inventoryKey?: string;
|
inventoryKey?: string;
|
||||||
|
|||||||
@@ -544,6 +544,7 @@ function vehicleListItem(v: Vehicle) {
|
|||||||
subjectOrg: v.subjectOrg,
|
subjectOrg: v.subjectOrg,
|
||||||
departmentName: v.departmentName,
|
departmentName: v.departmentName,
|
||||||
customerManager: v.customerManager,
|
customerManager: v.customerManager,
|
||||||
|
opsManagers: v.opsManagers || null,
|
||||||
brandLabel: v.brandLabel,
|
brandLabel: v.brandLabel,
|
||||||
orgName: v.orgName,
|
orgName: v.orgName,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ test('transforms database rows without changing the route model contract', () =>
|
|||||||
subjectOrg: '主体',
|
subjectOrg: '主体',
|
||||||
projectName: '项目',
|
projectName: '项目',
|
||||||
customerManager: '经理',
|
customerManager: '经理',
|
||||||
|
opsManagers: null,
|
||||||
managerId: '9',
|
managerId: '9',
|
||||||
brandLabel: '现代',
|
brandLabel: '现代',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -255,6 +255,7 @@ export function transformRow(row: VehicleRow): Vehicle {
|
|||||||
subjectOrg: row.主体,
|
subjectOrg: row.主体,
|
||||||
projectName: row.项目名称,
|
projectName: row.项目名称,
|
||||||
customerManager: row.客户经理,
|
customerManager: row.客户经理,
|
||||||
|
opsManagers: row.ops_managers?.trim() || null,
|
||||||
managerId: row.经理ID || null,
|
managerId: row.经理ID || null,
|
||||||
brandLabel: row.车辆品牌Label,
|
brandLabel: row.车辆品牌Label,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -100,6 +100,11 @@ export const MAIN_SQL = `SELECT
|
|||||||
NULLIF(vi.registered_ownership, '') AS 主体,
|
NULLIF(vi.registered_ownership, '') AS 主体,
|
||||||
vor.project_name AS 项目名称,
|
vor.project_name AS 项目名称,
|
||||||
vor.business_manager AS 客户经理,
|
vor.business_manager AS 客户经理,
|
||||||
|
(SELECT GROUP_CONCAT(
|
||||||
|
COALESCE(NULLIF(TRIM(ops.nick_name), ''), NULLIF(TRIM(ops.user_name), ''), CAST(ops.user_id AS CHAR))
|
||||||
|
ORDER BY ops.id SEPARATOR '、')
|
||||||
|
FROM vehicle_ops_manager_rel ops
|
||||||
|
WHERE ops.vehicle_id = vi.id AND ops.del_flag = '0') AS ops_managers,
|
||||||
CAST(COALESCE(c.business_manager_id, vi.business_id) AS CHAR) AS 经理ID
|
CAST(COALESCE(c.business_manager_id, vi.business_id) AS CHAR) AS 经理ID
|
||||||
FROM vehicle_info vi
|
FROM vehicle_info vi
|
||||||
LEFT JOIN vehicle_status vs
|
LEFT JOIN vehicle_status vs
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { AbnormalMoveDetail } from '../shared/assets/abnormal-move.js';
|
import type { AbnormalMoveDetail } from '../shared/assets/abnormal-move.js';
|
||||||
|
|
||||||
export interface VehicleRow {
|
export interface VehicleRow {
|
||||||
|
ops_managers?: string | null;
|
||||||
pending_delivery_id?: string | null;
|
pending_delivery_id?: string | null;
|
||||||
pending_contract_no?: string | null;
|
pending_contract_no?: string | null;
|
||||||
pending_customer_name?: string | null;
|
pending_customer_name?: string | null;
|
||||||
@@ -42,6 +43,7 @@ export interface VehicleRow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Vehicle {
|
export interface Vehicle {
|
||||||
|
opsManagers?: string | null;
|
||||||
pendingDelivery?: { contractNo: string | null; customerName: string | null } | null;
|
pendingDelivery?: { contractNo: string | null; customerName: string | null } | null;
|
||||||
abnormalMove?: AbnormalMoveDetail | null;
|
abnormalMove?: AbnormalMoveDetail | null;
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user