import { useCallback, useLayoutEffect, useMemo, useRef, useState, type Dispatch, type SetStateAction } from 'react'; import { ArrowLeft, X } from 'lucide-react'; import type { FlowDetailItem, FlowType } from '../api'; import type { FlowSelection } from '../model'; import { FLOW_META } from './FlowDetailModal'; import { orderDimensionGroups, dimensionRowSpans, dimensionParentTotals, dimensionChildCounts, dimensionCountUnit, visibleDimensionRowSpans } from '../dimension-layout'; import { useDialogFocus } from './useDialogFocus'; import { DimensionPicker, type DimensionPickerOption } from './analysis/DimensionPicker'; import { ProgressiveRowsStatus } from './analysis/ProgressiveRowsStatus'; import { useProgressiveRows } from './analysis/useProgressiveRows'; import { AnalysisExportButton } from './analysis/AnalysisExportButton'; import { downloadAnalysisWorkbook, statisticsExportRows } from '../analysis-export'; import './analysis/asset-analysis.css'; import './flow-analysis-modal.css'; import './analysis/mobile-analysis.css'; import { MobileAnalysisControls } from './analysis/MobileAnalysisControls'; import { MobileGroupedTable } from './analysis/MobileGroupedTable'; import { useMobileLayout } from '../useMobileLayout'; interface FlowAnalysisModalProps { selectedFlow: FlowSelection | null; selectedFlowDetails: FlowDetailItem[]; setSelectedFlow: Dispatch>; } type Dimension = 'date' | 'department' | 'manager' | 'type'; const DIMENSIONS: { key: Dimension; label: string }[] = [ { key: 'date', label: '日期' }, { key: 'department', label: '部门' }, { key: 'manager', label: '业务负责人' }, { key: 'type', label: '业务类型' }, ]; const DIMENSION_LABELS: Record = { date: '日期', department: '部门', manager: '业务负责人', type: '业务类型' }; const EMPTY_FLOW_ROWS: FlowDetailItem[] = []; const countLabel = (count: number) => count.toLocaleString('zh-CN'); const dimensionValue = (row: FlowDetailItem, dimension: Dimension) => row[dimension].trim(); const dimensionLabel = (dimension: Dimension, value: string) => dimension === 'type' ? FLOW_META[value as FlowType]?.label || value : value || `未填写${DIMENSION_LABELS[dimension]}`; interface FlowGroup { key: string; values: string[]; rows: FlowDetailItem[] } /** Each business event belongs to exactly one actual type; replacements also belong to the delivery scope. */ function matchesType(row: FlowDetailItem, type: FlowType): boolean { return row.type === type || (type === 'delivered' && row.type === 'replaced'); } function groupRows(rows: FlowDetailItem[], dimensions: Dimension[]): FlowGroup[] { const groups = new Map(); for (const row of rows) { const values = dimensions.map(dimension => dimensionValue(row, dimension)); const key = JSON.stringify(values); const group = groups.get(key) ?? { key, values, rows: [] }; group.rows.push(row); groups.set(key, group); } 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) { if (!props.selectedFlow) return null; return ; } function FlowAnalysisContent({ selectedFlow, selectedFlowDetails, setSelectedFlow }: Omit & { selectedFlow: FlowSelection }) { const mobile = useMobileLayout(); const [dimensions, setDimensions] = useState(['date']); const [mergeParents, setMergeParents] = useState(true); const mergeActive = mergeParents && dimensions.length > 1; const [filters, setFilters] = useState>>({}); const [detailKey, setDetailKey] = useState(null); const dialog = useRef(null); const scrollRoot = useRef(null); const close = useCallback(() => setSelectedFlow(null), [setSelectedFlow]); useDialogFocus(true, dialog, close); 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)) .sort((a, b) => b.date.localeCompare(a.date) || (b.eventTime || '').localeCompare(a.eventTime || '')), [selectedFlowDetails, selectedFlow]); 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 counts = new Map(); for (const row of eligible) { const value = dimensionValue(row, key); counts.set(value, (counts.get(value) || 0) + 1); } for (const value of filters[key] || []) if (!counts.has(value)) counts.set(value, 0); return [key, [...counts].map(([value, count]) => ({ value, label: dimensionLabel(key, value), count })).sort((a, b) => b.count - a.count || a.label.localeCompare(b.label, 'zh-CN'))]; })) as Record, [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 flat = groupRows(filteredRows, dimensions); 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]); const detailGroup = useMemo(() => detailKey === null ? null : groups.find(group => group.key === detailKey), [groups, detailKey]); const detailRows = detailGroup?.rows ?? EMPTY_FLOW_ROWS; const showDetails = detailKey !== null; const overviewScroll = useRef(0); const openDetails = (key: string) => { overviewScroll.current = scrollRoot.current?.scrollTop || 0; setDetailKey(key); }; useLayoutEffect(() => { if (scrollRoot.current) scrollRoot.current.scrollTop = showDetails ? 0 : overviewScroll.current; }, [showDetails]); const detailLabel = detailGroup && dimensions.length ? detailGroup.values.map((value, i) => `${DIMENSION_LABELS[dimensions[i]]}:${dimensionLabel(dimensions[i], value)}`).join(' · ') : '当前筛选范围'; const selectedRows = showDetails ? detailRows : filteredRows; const { replacedCount, departmentCount } = useMemo(() => ({ replacedCount: selectedRows.reduce((sum, row) => sum + Number(row.type === 'replaced'), 0), departmentCount: new Set(selectedRows.map(row => dimensionValue(row, 'department'))).size, }), [selectedRows]); const rangeLabel = 'date' in selectedFlow ? selectedFlow.date : `${selectedFlow.start} 至 ${selectedFlow.end}`; const parentTotals = useMemo(() => mergeActive ? dimensionParentTotals(groups, dimensions.length, group => group.values, group => group.rows.length) : [], [groups, dimensions.length, mergeActive]); const childCounts = useMemo(() => mergeActive ? dimensionChildCounts(groups, dimensions.length, group => group.values) : [], [groups, dimensions.length, mergeActive]); const groupProgress = useProgressiveRows({ total: groups.length, rootRef: scrollRoot, resetKey: groups, enabled: !showDetails, preserveOnDisable: true }); const detailProgress = useProgressiveRows({ total: detailRows.length, rootRef: scrollRoot, resetKey: detailRows, enabled: showDetails }); const shownGroups = useMemo(() => groups.slice(0, groupProgress.visibleCount), [groups, groupProgress.visibleCount]); const completeSpans = useMemo(() => mergeActive ? dimensionRowSpans(groups, dimensions.length, group => group.values) : [], [groups, dimensions.length, mergeActive]); const groupSpans = useMemo(() => visibleDimensionRowSpans(completeSpans, groupProgress.visibleCount), [completeSpans, groupProgress.visibleCount]); const shownDetails = useMemo(() => detailRows.slice(0, detailProgress.visibleCount), [detailRows, detailProgress.visibleCount]); const activeFilters = DIMENSIONS.filter(({ key }) => filters[key]?.length); const resetResult = () => { setDetailKey(null); }; const toggleDimension = (dimension: Dimension) => { setDimensions(current => current.includes(dimension) ? current.filter(value => value !== dimension) : [...current, dimension]); resetResult(); }; const changeFilter = (dimension: Dimension, value: string[] | undefined) => { setFilters(current => { const next = { ...current }; if (value === undefined) delete next[dimension]; else next[dimension] = value; return next; }); resetResult(); }; const exportWorkbook = () => { const statistics = statisticsExportRows(dimensions.map(d => DIMENSION_LABELS[d]), (detailGroup ? [detailGroup] : groups).map(group => ({ values: group.values.map((v, i) => dimensionLabel(dimensions[i], v)), count: group.rows.length })), selectedRows.length, '车次'); const details = selectedRows.map(item => ({ 业务日期: item.date, 类型: item.typeLabel, 车牌: item.plateNumber, 运维签章时间: item.eventTime, 建单时间: item.submitTime, 部门: item.department, 业务负责人: item.manager, 客户: item.customerName, 合同: item.contractNo, 项目名称: item.projectName })); return downloadAnalysisWorkbook(statistics, details, `${FLOW_META[selectedFlow.type].label}-${rangeLabel}-${showDetails ? detailLabel : '当前筛选范围'}`); }; return
{ if (event.target === event.currentTarget) close(); }}>
{mobile && }

{FLOW_META[selectedFlow.type].label}分析

{rangeLabel} · 运维签章时间 · 车次

{!mobile &&

交车包含替换交车;业务类型按实际记录拆分,同车多笔业务分别计次。

}
{!mobile && (!showDetails ?
分析维度
{DIMENSIONS.map(({ key, label }) => toggleDimension(key)} onApplyFilter={values => changeFilter(key, values)} />)} {activeFilters.length ? : null}

点击维度名切换分组,点击右侧箭头筛选;同维度多选为“或”,不同维度为“且”。

:
{detailLabel}
)}
{showDetails ? '当前分组' : '当前筛选'}{FLOW_META[selectedFlow.type].label}{countLabel(selectedRows.length)}车次
涉及部门{departmentCount}个
{selectedFlow.type === 'delivered' ?
其中:替换交车{countLabel(replacedCount)}车次
:
{showDetails ? '占当前筛选' : '统计分组'}{showDetails ? `${filteredRows.length ? (selectedRows.length / filteredRows.length * 100).toFixed(1) : '0.0'}%` : groups.length}{!showDetails ? 组 : null}
}
{mobile && !showDetails && (filteredRows.length > 0 || activeFilters.length > 0) && ({ ...d, options: filterOptions[d.key] }))} dimensions={dimensions} filters={filters} defaults={['date']} apply={(nextDimensions, nextFilters) => { setDimensions(nextDimensions as Dimension[]); setFilters(nextFilters); resetResult(); }} />} {mobile && showDetails &&

{detailLabel}

}
{(!mobile || showDetails) &&

{showDetails ? '业务明细' : dimensions.length ? '组合统计' : '筛选汇总'}

}

{showDetails ? `共 ${countLabel(detailRows.length)} 条业务记录` : mobile ? `共 ${countLabel(groups.length)} 组 · ${countLabel(filteredRows.length)} 车次` : `共 ${countLabel(groups.length)} 组 · 占比以当前筛选的 ${countLabel(filteredRows.length)} 车次为基数`}

{!mobile && !showDetails && dimensions.length > 1 ? : null}
{(!mobile || selectedRows.length > 0) && }
{!showDetails ? groups.length ? <> {mobile ? DIMENSION_LABELS[d])} unit="车次" merged={mergeActive} setMerged={setMergeParents} spans={groupSpans} parentTotals={parentTotals} rows={shownGroups.map(group => ({ key: group.key, label: group.values.map((value, i) => dimensionLabel(dimensions[i], value)).join('、') || '当前筛选范围', values: group.values.map((value, i) => dimensionLabel(dimensions[i], value)), count: group.rows.length, share: group.rows.length / filteredRows.length * 100 }))} onDetails={index => openDetails(shownGroups[index].key)} /> :
{dimensions.length ? dimensions.map(dimension => ) : } {shownGroups.map((group, rowIndex) => 0 ? 'bi-flow-modal-group-start' : undefined}> {dimensions.length ? group.values.map((value, index) => { const span = mergeActive ? groupSpans[rowIndex][index] : 1; return span === 0 ? null : ; }) : } )}
{DIMENSION_LABELS[dimension]}范围车次占比操作
1 ? span : undefined} className={span > 1 ? 'bi-flow-modal-parent-cell' : undefined}>{dimensionLabel(dimensions[index], value)}{mergeActive && index < dimensions.length - 1 && childCounts[rowIndex][index] > 1 && <>合计 {countLabel(parentTotals[rowIndex][index])} 车次{DIMENSION_LABELS[dimensions[index + 1]]}:{countLabel(childCounts[rowIndex][index])} {dimensionCountUnit(dimensions[index + 1])}}当前筛选范围{countLabel(group.rows.length)}
{(group.rows.length / filteredRows.length * 100).toFixed(1)}%
} :
{activeFilters.length ? '当前筛选范围暂无交还车数据,可调整筛选。' : '当前时段暂无交还车记录。'}
: detailRows.length ? <> {mobile ?
{shownDetails.map(item =>

{item.plateNumber || '未登记车牌'}

{FLOW_META[item.type].label}

{item.eventTime || item.date} · {item.department || '未填写部门'}

{item.customerName || '未填写客户'}

业务资料
{[['运维签章时间', item.eventTime], ['建单时间', item.submitTime], ['业务负责人', item.manager], ['合同', item.contractNo], ['项目名称', item.projectName]].map(([label,value]) =>
{label}
{value || '未提供'}
)}
)}
:
{shownDetails.map(item => )}
车牌业务类型运维签章时间建单时间部门业务负责人客户合同项目名称
{item.plateNumber || '未登记车牌'}{FLOW_META[item.type].label}{item.eventTime || '—'}{item.submitTime || '—'}{dimensionLabel('department', dimensionValue(item, 'department'))}{item.manager || '—'}{item.customerName || '—'}{item.contractNo || '—'}{item.projectName || '—'}
} :
当前分组暂无业务明细,请返回统计调整条件。
}
{mobile && }
; }