import type { AssetAnalysisDimension } from '../../../shared/assets/analytics'; import type { FaultAnalysisRecord } from '../../../shared/assets/faults'; import { DIMENSION_LABELS, getAvailableDimensions } from '../analytics-model'; export type FaultAnalysisDimension = AssetAnalysisDimension | 'handler' | 'plate' | 'faultStatus' | 'level'; export type FaultAnalysisFilters = Partial>; export const FAULT_ANALYSIS_DIMENSIONS: FaultAnalysisDimension[] = getAvailableDimensions('assets').flatMap(d => d === 'type' ? [d, 'handler', 'plate', 'faultStatus', 'level'] : [d]); export const FAULT_ANALYSIS_LABELS = { ...DIMENSION_LABELS, handler: '运维处置人', plate: '车牌', faultStatus: '故障工单状态', level: '故障程度' }; export const FAULT_CLOSURE_LABELS: Record = { closed: '已闭环', unclosed: '未闭环' }; export interface FaultAnalysisGroup { key: string; values: Partial>; count: number; share: number; recordIds: string[]; } export function faultDimensionValues(row: FaultAnalysisRecord, dimension: FaultAnalysisDimension): string[] { if (dimension === 'plate') return [row.plate?.trim() || '未关联车辆']; if (dimension === 'faultStatus') return [row.status === '已归档' ? 'closed' : 'unclosed']; if (dimension === 'level') return [row.level?.trim() || '未标注']; return dimension === 'handler' ? [...new Set(row.handlers.length ? row.handlers.map(h => h.value) : ['unassigned'])] : [row.cell[dimension]]; } export function filterFaultRecords(rows: FaultAnalysisRecord[], filters: FaultAnalysisFilters = {}): FaultAnalysisRecord[] { return [...new Map(rows.filter(row => FAULT_ANALYSIS_DIMENSIONS.every(d => !filters[d]?.length || faultDimensionValues(row, d).some(value => filters[d]!.includes(value)))).map(row => [row.id, row])).values()]; } /** Expand only the grouped handler dimension; totals and each group remain distinct fault IDs. */ export function analyzeFaultRecords(rows: FaultAnalysisRecord[], dimensions: FaultAnalysisDimension[], filters: FaultAnalysisFilters = {}) { const selected = filterFaultRecords(rows, filters); const groups = new Map }>(); const validDimensions = [...new Set(dimensions.filter(d => FAULT_ANALYSIS_DIMENSIONS.includes(d)))]; for (const row of selected) { let combinations: [FaultAnalysisDimension, string][][] = [[]]; for (const d of validDimensions) { const values = faultDimensionValues(row, d).filter(value => !filters[d]?.length || filters[d]!.includes(value)); combinations = combinations.flatMap(parts => values.map(value => [...parts, [d, value] as [FaultAnalysisDimension, string]])); } for (const entries of combinations) { const key = JSON.stringify(entries); const group = groups.get(key) || { values: Object.fromEntries(entries), ids: new Set() }; group.ids.add(row.id); groups.set(key, group); } } return { total: selected.length, rows: [...groups].map(([key, group]): FaultAnalysisGroup => ({ key, values: group.values, count: group.ids.size, share: selected.length ? group.ids.size / selected.length * 100 : 0, recordIds: [...group.ids] })) }; } /** Multi-handler child rows overlap: parent subtotals must use the union, never sum children. */ export function faultParentTotals(rows: FaultAnalysisGroup[], dimensions: FaultAnalysisDimension[]): number[][] { const totals = new Map>(); const keys = rows.map(row => dimensions.slice(0, -1).map((_, depth) => JSON.stringify(dimensions.slice(0, depth + 1).map(d => row.values[d])))); rows.forEach((row, i) => keys[i].forEach(key => { const ids = totals.get(key) || new Set(); row.recordIds.forEach(id => ids.add(id)); totals.set(key, ids); })); return keys.map(row => row.map(key => totals.get(key)!.size)); }