feat(platform): sync dashboard customer time window
This commit is contained in:
@@ -600,7 +600,7 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pages: Record<PageKey, JSX.Element> = {
|
const pages: Record<PageKey, JSX.Element> = {
|
||||||
dashboard: <Dashboard onOpenVehicle={openVehicle} onOpenQuality={openQuality} onOpenMap={openMap} onOpenRealtime={openRealtime} onOpenVehicles={openVehicles} onOpenHistory={openHistoryWithFilters} onOpenMileage={openMileageWithFilters} />,
|
dashboard: <Dashboard onOpenVehicle={openVehicle} onOpenQuality={openQuality} onOpenMap={openMap} onOpenRealtime={openRealtime} onOpenVehicles={openVehicles} onOpenHistory={openHistoryWithFilters} onOpenMileage={openMileageWithFilters} customerTimeWindow={customerTimeWindow} />,
|
||||||
vehicles: <Vehicles onOpenVehicle={openVehicle} onOpenQuality={openQuality} onOpenMap={openMap} onOpenRealtime={openRealtime} onOpenHistory={openHistoryWithFilters} onOpenMileage={openMileageWithFilters} onFiltersChange={updateVehicleFilters} initialFilters={vehicleFilters} />,
|
vehicles: <Vehicles onOpenVehicle={openVehicle} onOpenQuality={openQuality} onOpenMap={openMap} onOpenRealtime={openRealtime} onOpenHistory={openHistoryWithFilters} onOpenMileage={openMileageWithFilters} onFiltersChange={updateVehicleFilters} initialFilters={vehicleFilters} />,
|
||||||
map: <Realtime mode="map" title="车辆实时地图" description="以车辆为中心查看位置分布、在线状态、关注车辆和轨迹入口" onOpenVehicle={openVehicle} onOpenHistory={openHistoryForVehicle} onOpenQuality={openQuality} onFiltersChange={updateMapFilters} initialFilters={realtimeFilters} />,
|
map: <Realtime mode="map" title="车辆实时地图" description="以车辆为中心查看位置分布、在线状态、关注车辆和轨迹入口" onOpenVehicle={openVehicle} onOpenHistory={openHistoryForVehicle} onOpenQuality={openQuality} onFiltersChange={updateMapFilters} initialFilters={realtimeFilters} />,
|
||||||
realtime: <Realtime onOpenVehicle={openVehicle} onOpenHistory={openHistoryForVehicle} onOpenQuality={openQuality} onFiltersChange={updateRealtimeFilters} initialFilters={realtimeFilters} />,
|
realtime: <Realtime onOpenVehicle={openVehicle} onOpenHistory={openHistoryForVehicle} onOpenQuality={openQuality} onFiltersChange={updateRealtimeFilters} initialFilters={realtimeFilters} />,
|
||||||
|
|||||||
@@ -154,6 +154,16 @@ function dayRangeText(dateFrom?: string, dateTo?: string) {
|
|||||||
return days <= 0 ? '同日或倒置时间窗' : `${days} 天窗口`;
|
return days <= 0 ? '同日或倒置时间窗' : `${days} 天窗口`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function naturalDayRangeText(dateFrom?: string, dateTo?: string) {
|
||||||
|
if (!dateFrom && !dateTo) return '未限定时间';
|
||||||
|
if (!dateFrom || !dateTo) return '单边时间窗';
|
||||||
|
const start = new Date(`${dateFrom}T00:00:00+08:00`).getTime();
|
||||||
|
const end = new Date(`${dateTo}T00:00:00+08:00`).getTime();
|
||||||
|
if (!Number.isFinite(start) || !Number.isFinite(end)) return '时间格式待确认';
|
||||||
|
const days = Math.max(0, Math.round((end - start) / 86400000)) + 1;
|
||||||
|
return days <= 0 ? '同日或倒置时间窗' : `${days} 天窗口`;
|
||||||
|
}
|
||||||
|
|
||||||
function priorityIssueVehicleLabel(row: QualityIssueRow) {
|
function priorityIssueVehicleLabel(row: QualityIssueRow) {
|
||||||
const identity = row.vin?.trim() && row.vin !== 'unknown' ? row.vin.trim() : row.phone?.trim();
|
const identity = row.vin?.trim() && row.vin !== 'unknown' ? row.vin.trim() : row.phone?.trim();
|
||||||
return [row.plate?.trim(), identity].filter(Boolean).join(' / ') || row.sourceEndpoint || '-';
|
return [row.plate?.trim(), identity].filter(Boolean).join(' / ') || row.sourceEndpoint || '-';
|
||||||
@@ -261,6 +271,14 @@ function sourceConsistencyAction(row: VehicleCoverageRow, onFilter: (filters: Re
|
|||||||
return <Tag color={color}>{label}</Tag>;
|
return <Tag color={color}>{label}</Tag>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function defaultTimeMonitorFilters() {
|
||||||
|
const today = todayDateString();
|
||||||
|
return {
|
||||||
|
dateFrom: previousDateString(today),
|
||||||
|
dateTo: today
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function Dashboard({
|
export function Dashboard({
|
||||||
onOpenVehicle,
|
onOpenVehicle,
|
||||||
onOpenQuality,
|
onOpenQuality,
|
||||||
@@ -268,7 +286,8 @@ export function Dashboard({
|
|||||||
onOpenRealtime,
|
onOpenRealtime,
|
||||||
onOpenVehicles,
|
onOpenVehicles,
|
||||||
onOpenHistory,
|
onOpenHistory,
|
||||||
onOpenMileage
|
onOpenMileage,
|
||||||
|
customerTimeWindow
|
||||||
}: {
|
}: {
|
||||||
onOpenVehicle: (vin: string, protocol?: string) => void;
|
onOpenVehicle: (vin: string, protocol?: string) => void;
|
||||||
onOpenQuality: (filters?: Record<string, string>) => void;
|
onOpenQuality: (filters?: Record<string, string>) => void;
|
||||||
@@ -277,6 +296,7 @@ export function Dashboard({
|
|||||||
onOpenVehicles: (filters?: Record<string, string>) => void;
|
onOpenVehicles: (filters?: Record<string, string>) => void;
|
||||||
onOpenHistory: (filters?: Record<string, string>) => void;
|
onOpenHistory: (filters?: Record<string, string>) => void;
|
||||||
onOpenMileage: (filters?: Record<string, string>) => void;
|
onOpenMileage: (filters?: Record<string, string>) => void;
|
||||||
|
customerTimeWindow?: Record<string, string>;
|
||||||
}) {
|
}) {
|
||||||
const [summary, setSummary] = useState<DashboardSummary | null>(null);
|
const [summary, setSummary] = useState<DashboardSummary | null>(null);
|
||||||
const [serviceSummary, setServiceSummary] = useState<VehicleServiceSummary | null>(null);
|
const [serviceSummary, setServiceSummary] = useState<VehicleServiceSummary | null>(null);
|
||||||
@@ -284,13 +304,7 @@ export function Dashboard({
|
|||||||
const [locations, setLocations] = useState<VehicleRealtimeRow[]>([]);
|
const [locations, setLocations] = useState<VehicleRealtimeRow[]>([]);
|
||||||
const [qualityIssues, setQualityIssues] = useState<QualityIssueRow[]>([]);
|
const [qualityIssues, setQualityIssues] = useState<QualityIssueRow[]>([]);
|
||||||
const [opsHealth, setOpsHealth] = useState<OpsHealth | null>(null);
|
const [opsHealth, setOpsHealth] = useState<OpsHealth | null>(null);
|
||||||
const [timeMonitorFilters, setTimeMonitorFilters] = useState<Record<string, string>>(() => {
|
const [timeMonitorFilters, setTimeMonitorFilters] = useState<Record<string, string>>(defaultTimeMonitorFilters);
|
||||||
const today = todayDateString();
|
|
||||||
return {
|
|
||||||
dateFrom: previousDateString(today),
|
|
||||||
dateTo: today
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [coverageLoading, setCoverageLoading] = useState(false);
|
const [coverageLoading, setCoverageLoading] = useState(false);
|
||||||
const [coverageServiceStatusTitle, setCoverageServiceStatusTitle] = useState('');
|
const [coverageServiceStatusTitle, setCoverageServiceStatusTitle] = useState('');
|
||||||
@@ -343,6 +357,25 @@ export function Dashboard({
|
|||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const dateFrom = customerTimeWindow?.dateFrom?.trim();
|
||||||
|
const dateTo = customerTimeWindow?.dateTo?.trim();
|
||||||
|
if (!dateFrom && !dateTo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeMonitorFilters((previous) => {
|
||||||
|
const next = {
|
||||||
|
...previous,
|
||||||
|
dateFrom: dateFrom ?? '',
|
||||||
|
dateTo: dateTo ?? ''
|
||||||
|
};
|
||||||
|
if (previous.dateFrom === next.dateFrom && previous.dateTo === next.dateTo) {
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, [customerTimeWindow?.dateFrom, customerTimeWindow?.dateTo]);
|
||||||
|
|
||||||
const serviceActionQueue = useMemo<Array<{ label: string; count: number; filters: Record<string, string>; detail: string }>>(() => {
|
const serviceActionQueue = useMemo<Array<{ label: string; count: number; filters: Record<string, string>; detail: string }>>(() => {
|
||||||
const items: Array<{ label: string; count: number; filters: Record<string, string>; detail: string }> = [];
|
const items: Array<{ label: string; count: number; filters: Record<string, string>; detail: string }> = [];
|
||||||
if ((serviceSummary?.noDataVehicles ?? 0) > 0) {
|
if ((serviceSummary?.noDataVehicles ?? 0) > 0) {
|
||||||
@@ -873,7 +906,14 @@ export function Dashboard({
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
const timeMonitorScopeValue = timeMonitorScope();
|
const timeMonitorScopeValue = timeMonitorScope();
|
||||||
const timeMonitorWindowLabel = dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo);
|
const usesCustomerTimeWindow = Boolean(
|
||||||
|
(customerTimeWindow?.dateFrom || customerTimeWindow?.dateTo)
|
||||||
|
&& customerTimeWindow?.dateFrom === timeMonitorScopeValue.dateFrom
|
||||||
|
&& customerTimeWindow?.dateTo === timeMonitorScopeValue.dateTo
|
||||||
|
);
|
||||||
|
const timeMonitorWindowLabel = usesCustomerTimeWindow
|
||||||
|
? naturalDayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo)
|
||||||
|
: dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo);
|
||||||
const timeMonitorChecklist = [
|
const timeMonitorChecklist = [
|
||||||
{
|
{
|
||||||
label: '范围可解释',
|
label: '范围可解释',
|
||||||
@@ -1003,7 +1043,7 @@ export function Dashboard({
|
|||||||
{
|
{
|
||||||
step: '1',
|
step: '1',
|
||||||
label: '锁定范围',
|
label: '锁定范围',
|
||||||
value: dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo),
|
value: timeMonitorWindowLabel,
|
||||||
detail: timeMonitorScopeValue.keyword ? `单车 ${timeMonitorScopeValue.keyword}` : '先确认车辆或车辆池,再应用同一时间窗。',
|
detail: timeMonitorScopeValue.keyword ? `单车 ${timeMonitorScopeValue.keyword}` : '先确认车辆或车辆池,再应用同一时间窗。',
|
||||||
action: '应用时间窗',
|
action: '应用时间窗',
|
||||||
color: timeMonitorHasRange ? 'green' as const : 'orange' as const,
|
color: timeMonitorHasRange ? 'green' as const : 'orange' as const,
|
||||||
|
|||||||
@@ -7324,6 +7324,7 @@ test('topbar customer time window presets support common monitoring ranges', asy
|
|||||||
fireEvent.click(screen.getByRole('button', { name: '顶部时间窗 近7天' }));
|
fireEvent.click(screen.getByRole('button', { name: '顶部时间窗 近7天' }));
|
||||||
expect(screen.getByLabelText('顶部时间窗开始')).toHaveValue('2026-06-29');
|
expect(screen.getByLabelText('顶部时间窗开始')).toHaveValue('2026-06-29');
|
||||||
expect(screen.getByLabelText('顶部时间窗结束')).toHaveValue('2026-07-05');
|
expect(screen.getByLabelText('顶部时间窗结束')).toHaveValue('2026-07-05');
|
||||||
|
expect(await screen.findByRole('button', { name: /时间窗服务路径 1 锁定范围 7 天窗口 应用时间窗/ })).toBeInTheDocument();
|
||||||
|
|
||||||
fireEvent.change(screen.getByPlaceholderText('搜索 VIN / 车牌 / 手机号'), { target: { value: '粤A快捷窗' } });
|
fireEvent.change(screen.getByPlaceholderText('搜索 VIN / 车牌 / 手机号'), { target: { value: '粤A快捷窗' } });
|
||||||
fireEvent.click(screen.getByRole('button', { name: '查询车辆' }));
|
fireEvent.click(screen.getByRole('button', { name: '查询车辆' }));
|
||||||
|
|||||||
Reference in New Issue
Block a user