feat: preserve mileage assessment years

This commit is contained in:
kkfluous
2026-08-07 17:37:05 +08:00
parent 52d79ba260
commit 67a269651e
4 changed files with 91 additions and 16 deletions
+71 -13
View File
@@ -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>
+11 -3
View File
@@ -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'] },
+8
View File
@@ -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);