feat: preserve mileage assessment years
This commit is contained in:
@@ -49,6 +49,10 @@ function getTargetAssessment(target: TargetSummary, selectedYear?: number): Targ
|
||||
return target.yearlyAssessments.find(item => item.yearNumber === selectedYear) || target.yearlyAssessments[0];
|
||||
}
|
||||
|
||||
function resolveAssessmentYear(target: TargetSummary, requestedYear?: number): number | undefined {
|
||||
return getTargetAssessment(target, requestedYear)?.yearNumber;
|
||||
}
|
||||
|
||||
function fmtDateLabel(date: string | null): string {
|
||||
if (!date) return '';
|
||||
const [year, month, day] = date.split('-');
|
||||
@@ -96,10 +100,12 @@ export default function StatisticsView() {
|
||||
const [viewAllLoading, setViewAllLoading] = useState(false);
|
||||
const [viewAllHistoricalVehicles, setViewAllHistoricalVehicles] = useState<TargetVehicle[] | null>(null);
|
||||
const targetVehicleRequestsRef = useRef(new Set<number>());
|
||||
const initialDrillContextRef = useRef(drillContext);
|
||||
|
||||
const selectedTarget = targets.find(t => t.id === selectedTargetId);
|
||||
const selectedTargetVehicles = selectedTargetId === null ? [] : targetVehiclesMap[selectedTargetId] || [];
|
||||
const selectedAssessment = selectedTarget ? getTargetAssessment(selectedTarget, assessmentYearMap[selectedTarget.id]) : null;
|
||||
const selectedAssessmentYear = selectedAssessment?.yearNumber;
|
||||
const selectedCompletion = selectedAssessment?.completionRate ?? selectedTarget?.avgCompletion ?? 0;
|
||||
const selectedRemaining = selectedAssessment?.remaining ?? 0;
|
||||
const selectedDaysLeft = selectedAssessment?.daysLeft ?? selectedTarget?.daysLeft ?? 0;
|
||||
@@ -136,6 +142,10 @@ export default function StatisticsView() {
|
||||
}, []);
|
||||
|
||||
const selectTarget = useCallback((targetId: number) => {
|
||||
const target = targets.find(item => item.id === targetId);
|
||||
const assessmentYear = target
|
||||
? resolveAssessmentYear(target, assessmentYearMap[targetId])
|
||||
: undefined;
|
||||
setSelectedTargetId(targetId);
|
||||
setExpandedTargetId(targetId);
|
||||
setViewAllTargetId(null);
|
||||
@@ -143,9 +153,10 @@ export default function StatisticsView() {
|
||||
commitDrillContext({
|
||||
level: 'overview',
|
||||
targetId,
|
||||
assessmentYear,
|
||||
sourcePriority: ['instrument'],
|
||||
}, 'replace');
|
||||
}, [commitDrillContext]);
|
||||
}, [assessmentYearMap, commitDrillContext, targets]);
|
||||
|
||||
const openAttributionGroup = useCallback((value: string) => {
|
||||
if (selectedTargetId === null) return;
|
||||
@@ -157,11 +168,29 @@ export default function StatisticsView() {
|
||||
commitDrillContext({
|
||||
level: 'breakdown',
|
||||
targetId: selectedTargetId,
|
||||
assessmentYear: selectedAssessmentYear,
|
||||
dimension: attributionDimension,
|
||||
dimensionValue: value,
|
||||
sourcePriority: ['instrument'],
|
||||
}, 'push');
|
||||
}, [attributionDimension, commitDrillContext, selectedTarget?.targetName, selectedTargetId]);
|
||||
}, [attributionDimension, commitDrillContext, selectedAssessmentYear, selectedTarget?.targetName, selectedTargetId]);
|
||||
|
||||
const openTargetVehicles = useCallback((target: TargetSummary) => {
|
||||
const assessmentYear = resolveAssessmentYear(target, assessmentYearMap[target.id]);
|
||||
setViewAllTargetId(target.id);
|
||||
setViewAllTargetName(target.targetName);
|
||||
setViewAllSearch('');
|
||||
setViewAllDate(getDefaultDate());
|
||||
setViewAllHistoricalVehicles(null);
|
||||
commitDrillContext({
|
||||
level: 'breakdown',
|
||||
targetId: target.id,
|
||||
assessmentYear,
|
||||
dimension: 'assessment-target',
|
||||
dimensionValue: target.targetName,
|
||||
sourcePriority: ['instrument'],
|
||||
}, 'push');
|
||||
}, [assessmentYearMap, commitDrillContext]);
|
||||
|
||||
const closeVehiclePanel = useCallback(() => {
|
||||
setViewAllTargetId(null);
|
||||
@@ -170,10 +199,11 @@ export default function StatisticsView() {
|
||||
commitDrillContext({
|
||||
level: 'overview',
|
||||
targetId: selectedTargetId || undefined,
|
||||
assessmentYear: selectedAssessmentYear,
|
||||
sourcePriority: ['instrument'],
|
||||
}, 'replace');
|
||||
}
|
||||
}, [commitDrillContext, drillContext.level, selectedTargetId]);
|
||||
}, [commitDrillContext, drillContext.level, selectedAssessmentYear, selectedTargetId]);
|
||||
|
||||
const openVehicleDiagnostic = useCallback((vehicle: TargetVehicle) => {
|
||||
const url = buildMileageDrillUrl(
|
||||
@@ -181,6 +211,7 @@ export default function StatisticsView() {
|
||||
{
|
||||
level: 'vehicle',
|
||||
targetId: viewAllTargetId || selectedTargetId || undefined,
|
||||
assessmentYear: selectedAssessmentYear,
|
||||
plate: vehicle.plateNumber,
|
||||
startDate: viewAllDate,
|
||||
endDate: viewAllDate,
|
||||
@@ -189,11 +220,12 @@ export default function StatisticsView() {
|
||||
);
|
||||
window.history.pushState(null, '', url);
|
||||
window.dispatchEvent(new HashChangeEvent('hashchange'));
|
||||
}, [selectedTargetId, viewAllDate, viewAllTargetId]);
|
||||
}, [selectedAssessmentYear, selectedTargetId, viewAllDate, viewAllTargetId]);
|
||||
|
||||
// Load targets on mount
|
||||
useEffect(() => {
|
||||
fetchTargets().then(data => {
|
||||
const initialContext = initialDrillContextRef.current;
|
||||
const focused = data.find(item => item.targetName.includes('羚牛136')) || data[0];
|
||||
const ordered = focused
|
||||
? [focused, ...data.filter(item => item.id !== focused.id)]
|
||||
@@ -203,7 +235,20 @@ export default function StatisticsView() {
|
||||
if (initialTarget) {
|
||||
setSelectedTargetId(initialTarget.id);
|
||||
setExpandedTargetId(initialTarget.id);
|
||||
setAssessmentYearMap(Object.fromEntries(ordered.map(item => [item.id, item.yearlyAssessments[0]?.yearNumber || 1])));
|
||||
const initialYears = Object.fromEntries(ordered.map(item => [
|
||||
item.id,
|
||||
resolveAssessmentYear(item, item.id === initialContext.targetId ? initialContext.assessmentYear : undefined) || 1,
|
||||
]));
|
||||
setAssessmentYearMap(initialYears);
|
||||
|
||||
if (initialContext.targetId === initialTarget.id) {
|
||||
const resolvedYear = initialYears[initialTarget.id];
|
||||
if (resolvedYear && resolvedYear !== initialContext.assessmentYear) {
|
||||
const normalizedContext = { ...initialContext, assessmentYear: resolvedYear };
|
||||
window.history.replaceState(null, '', buildMileageDrillUrl(window.location, normalizedContext));
|
||||
setDrillContext(normalizedContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
@@ -236,7 +281,14 @@ export default function StatisticsView() {
|
||||
const handlePopState = () => {
|
||||
const next = parseMileageDrillContext(window.location.search);
|
||||
setDrillContext(next);
|
||||
if (next.targetId) setSelectedTargetId(next.targetId);
|
||||
if (next.targetId) {
|
||||
setSelectedTargetId(next.targetId);
|
||||
const target = targets.find(item => item.id === next.targetId);
|
||||
const resolvedYear = target ? resolveAssessmentYear(target, next.assessmentYear) : undefined;
|
||||
if (resolvedYear) {
|
||||
setAssessmentYearMap(prev => ({ ...prev, [next.targetId!]: resolvedYear }));
|
||||
}
|
||||
}
|
||||
if (next.level === 'breakdown' && next.targetId && next.dimensionValue) {
|
||||
setAttributionDimension(next.dimension === 'customer' ? 'customer' : 'department');
|
||||
setViewAllTargetId(next.targetId);
|
||||
@@ -246,7 +298,7 @@ export default function StatisticsView() {
|
||||
};
|
||||
window.addEventListener('popstate', handlePopState);
|
||||
return () => window.removeEventListener('popstate', handlePopState);
|
||||
}, []);
|
||||
}, [targets]);
|
||||
|
||||
const viewAllVehicles = useMemo(() => {
|
||||
const vehicles = viewAllHistoricalVehicles
|
||||
@@ -536,7 +588,13 @@ export default function StatisticsView() {
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
setAssessmentYearMap(prev => ({ ...prev, [target.id]: Number(e.target.value) }));
|
||||
const assessmentYear = Number(e.target.value);
|
||||
setAssessmentYearMap(prev => ({ ...prev, [target.id]: assessmentYear }));
|
||||
commitDrillContext({
|
||||
...drillContext,
|
||||
targetId: target.id,
|
||||
assessmentYear,
|
||||
}, 'replace');
|
||||
}}
|
||||
className="bg-white border border-blue-100 rounded-lg px-2 py-1 text-[10px] font-bold text-blue-700 outline-none"
|
||||
>
|
||||
@@ -625,9 +683,7 @@ export default function StatisticsView() {
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setViewAllTargetId(target.id);
|
||||
setViewAllTargetName(target.targetName);
|
||||
setViewAllDate(getDefaultDate());
|
||||
openTargetVehicles(target);
|
||||
}}
|
||||
className="text-[8px] text-blue-500 font-bold hover:underline"
|
||||
>
|
||||
@@ -803,8 +859,10 @@ export default function StatisticsView() {
|
||||
<h3 className="text-lg font-black text-slate-900">
|
||||
{viewAllTargetName || selectedTarget?.targetName || '考核车辆'}
|
||||
</h3>
|
||||
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest mt-1">车辆实时明细清单</p>
|
||||
{selectedBreakdownValue && (
|
||||
<p className="mt-1 text-xs font-bold uppercase tracking-widest text-slate-400">
|
||||
{selectedAssessment ? `${selectedAssessment.label} · ` : ''}车辆实时明细清单
|
||||
</p>
|
||||
{selectedBreakdownValue && drillContext.dimension !== 'assessment-target' && (
|
||||
<p className="mt-1 text-[10px] font-bold text-blue-600">{selectedBreakdownValue}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -24,10 +24,11 @@ test('requires complete context for breakdown and vehicle levels', () => {
|
||||
|
||||
test('parses a target-scoped breakdown context', () => {
|
||||
assert.deepEqual(
|
||||
parseMileageDrillContext('?mileageLevel=breakdown&mileageTarget=5&mileageDimension=department&mileageValue=%E4%B8%9A%E5%8A%A1%E4%B8%80%E9%83%A8'),
|
||||
parseMileageDrillContext('?mileageLevel=breakdown&mileageTarget=5&mileageYear=2&mileageDimension=department&mileageValue=%E4%B8%9A%E5%8A%A1%E4%B8%80%E9%83%A8'),
|
||||
{
|
||||
level: 'breakdown',
|
||||
targetId: 5,
|
||||
assessmentYear: 2,
|
||||
dimension: 'department',
|
||||
dimensionValue: '业务一部',
|
||||
sourcePriority: ['instrument'],
|
||||
@@ -35,12 +36,19 @@ test('parses a target-scoped breakdown context', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('accepts assessment years only with a target', () => {
|
||||
assert.equal(parseMileageDrillContext('?mileageTarget=5&mileageYear=3').assessmentYear, 3);
|
||||
assert.equal(parseMileageDrillContext('?mileageYear=3').assessmentYear, undefined);
|
||||
assert.equal(parseMileageDrillContext('?mileageTarget=5&mileageYear=0').assessmentYear, undefined);
|
||||
});
|
||||
|
||||
test('builds a namespaced URL and preserves unrelated parameters', () => {
|
||||
const url = buildMileageDrillUrl(
|
||||
{ pathname: '/asset', search: '?company=5&legacy=1', hash: '#mileage' },
|
||||
{
|
||||
level: 'vehicle',
|
||||
targetId: 5,
|
||||
assessmentYear: 2,
|
||||
plate: '粤A08190F',
|
||||
startDate: '2026-08-01',
|
||||
endDate: '2026-08-07',
|
||||
@@ -50,7 +58,7 @@ test('builds a namespaced URL and preserves unrelated parameters', () => {
|
||||
|
||||
assert.equal(
|
||||
url,
|
||||
'/asset?company=5&legacy=1&mileageLevel=vehicle&mileageTarget=5&mileagePlate=%E7%B2%A4A08190F&mileageStart=2026-08-01&mileageEnd=2026-08-07&mileageSource=instrument#mileage',
|
||||
'/asset?company=5&legacy=1&mileageLevel=vehicle&mileageTarget=5&mileageYear=2&mileagePlate=%E7%B2%A4A08190F&mileageStart=2026-08-01&mileageEnd=2026-08-07&mileageSource=instrument#mileage',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -58,7 +66,7 @@ test('clears entity drill parameters when returning to overview', () => {
|
||||
const url = buildMileageDrillUrl(
|
||||
{
|
||||
pathname: '/asset',
|
||||
search: '?mileageLevel=vehicle&mileagePlate=%E7%B2%A4A08190F&company=5',
|
||||
search: '?mileageLevel=vehicle&mileageTarget=5&mileageYear=2&mileagePlate=%E7%B2%A4A08190F&company=5',
|
||||
hash: '#mileage',
|
||||
},
|
||||
{ level: 'overview', sourcePriority: ['instrument'] },
|
||||
|
||||
@@ -11,6 +11,7 @@ export type MileageBreakdownDimension =
|
||||
export interface MileageDrillContext {
|
||||
level: MileageDrillLevel;
|
||||
targetId?: number;
|
||||
assessmentYear?: number;
|
||||
dimension?: MileageBreakdownDimension;
|
||||
dimensionValue?: string;
|
||||
plate?: string;
|
||||
@@ -54,6 +55,10 @@ export function parseMileageDrillContext(search: string): MileageDrillContext {
|
||||
const params = new URLSearchParams(search);
|
||||
const rawTargetId = Number(params.get('mileageTarget'));
|
||||
const targetId = Number.isInteger(rawTargetId) && rawTargetId > 0 ? rawTargetId : undefined;
|
||||
const rawAssessmentYear = Number(params.get('mileageYear'));
|
||||
const assessmentYear = Number.isInteger(rawAssessmentYear) && rawAssessmentYear > 0
|
||||
? rawAssessmentYear
|
||||
: undefined;
|
||||
const plate = params.get('mileagePlate')?.trim() || undefined;
|
||||
const rawDimension = params.get('mileageDimension');
|
||||
const dimension = rawDimension && BREAKDOWN_DIMENSIONS.has(rawDimension as MileageBreakdownDimension)
|
||||
@@ -71,6 +76,7 @@ export function parseMileageDrillContext(search: string): MileageDrillContext {
|
||||
sourcePriority: parseSourcePriority(params.get('mileageSource')),
|
||||
};
|
||||
if (targetId) context.targetId = targetId;
|
||||
if (targetId && assessmentYear) context.assessmentYear = assessmentYear;
|
||||
if (level === 'breakdown') {
|
||||
context.dimension = dimension;
|
||||
context.dimensionValue = dimensionValue;
|
||||
@@ -91,6 +97,7 @@ export function buildMileageDrillUrl(
|
||||
for (const key of [
|
||||
'mileageLevel',
|
||||
'mileageTarget',
|
||||
'mileageYear',
|
||||
'mileageDimension',
|
||||
'mileageValue',
|
||||
'mileagePlate',
|
||||
@@ -103,6 +110,7 @@ export function buildMileageDrillUrl(
|
||||
|
||||
if (context.level !== 'overview') params.set('mileageLevel', context.level);
|
||||
if (context.targetId) params.set('mileageTarget', String(context.targetId));
|
||||
if (context.targetId && context.assessmentYear) params.set('mileageYear', String(context.assessmentYear));
|
||||
if (context.level === 'breakdown' && context.dimension && context.dimensionValue) {
|
||||
params.set('mileageDimension', context.dimension);
|
||||
params.set('mileageValue', context.dimensionValue);
|
||||
|
||||
Reference in New Issue
Block a user