feat: 出勤率数据源切换为tab_truck_remote_sync_realtime_info
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- dept-stats和/list的attendance过滤改为查询realtime表的day_mileage - day_mileage>0为出勤,=0为闲置 - 移除旧的ln_vehicle_day_mileage依赖 - 前端恢复出勤率/出勤/闲置的数据显示和下钻功能 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
46
src/App.tsx
46
src/App.tsx
@@ -1357,17 +1357,25 @@ export default function App() {
|
|||||||
<span className="text-[9px] opacity-50 uppercase font-bold tracking-widest mb-0.5">总运营车辆</span>
|
<span className="text-[9px] opacity-50 uppercase font-bold tracking-widest mb-0.5">总运营车辆</span>
|
||||||
<span className="text-xl font-black">{deptData.reduce((s, d) => s + d.totalAssets, 0)}</span>
|
<span className="text-xl font-black">{deptData.reduce((s, d) => s + d.totalAssets, 0)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col cursor-pointer hover:opacity-80 transition-opacity"
|
||||||
|
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', attendance: 'active', source: 'department', title: '部门运营统计 - 出勤车辆' })}>
|
||||||
<span className="text-[9px] opacity-50 uppercase font-bold tracking-widest mb-0.5 text-green-400">出勤车辆</span>
|
<span className="text-[9px] opacity-50 uppercase font-bold tracking-widest mb-0.5 text-green-400">出勤车辆</span>
|
||||||
<span className="text-xl font-black text-green-400">-</span>
|
<span className="text-xl font-black text-green-400">
|
||||||
|
{deptData.reduce((acc, d) => acc + d.operatingCount, 0)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col cursor-pointer hover:opacity-80 transition-opacity"
|
||||||
|
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', attendance: 'idle', source: 'department', title: '部门运营统计 - 闲置车辆' })}>
|
||||||
<span className="text-[9px] opacity-50 uppercase font-bold tracking-widest mb-0.5 text-slate-400">闲置车辆</span>
|
<span className="text-[9px] opacity-50 uppercase font-bold tracking-widest mb-0.5 text-slate-400">闲置车辆</span>
|
||||||
<span className="text-xl font-black text-slate-400">-</span>
|
<span className="text-xl font-black text-slate-400">
|
||||||
|
{deptData.reduce((acc, d) => acc + d.idleCount, 0)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-[9px] opacity-50 uppercase font-bold tracking-widest mb-0.5 text-blue-400">平均出勤</span>
|
<span className="text-[9px] opacity-50 uppercase font-bold tracking-widest mb-0.5 text-blue-400">平均出勤</span>
|
||||||
<span className="text-xl font-black text-blue-400">-</span>
|
<span className="text-xl font-black text-blue-400">
|
||||||
|
{deptData.length > 0 ? (deptData.reduce((acc, d) => acc + d.attendanceRate, 0) / deptData.length).toFixed(1) : 0}%
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1453,7 +1461,9 @@ export default function App() {
|
|||||||
{dept.department}
|
{dept.department}
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2 border-r border-gray-100 text-center">
|
<td className="p-2 border-r border-gray-100 text-center">
|
||||||
<span className="bg-gray-50 text-gray-400 text-[10px] font-bold px-2 py-0.5 rounded-full">-</span>
|
<span className="bg-blue-50 text-blue-600 text-[10px] font-bold px-2 py-0.5 rounded-full">
|
||||||
|
{dept.attendanceRate}%
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2 border-r border-gray-100 text-center font-black text-gray-800 text-sm">
|
<td className="p-2 border-r border-gray-100 text-center font-black text-gray-800 text-sm">
|
||||||
<button
|
<button
|
||||||
@@ -1466,8 +1476,12 @@ export default function App() {
|
|||||||
{dept.totalAssets}
|
{dept.totalAssets}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2 border-r border-gray-100 text-center font-black text-gray-400 text-sm">-</td>
|
<td className="p-2 border-r border-gray-100 text-center font-black text-green-500 text-sm">
|
||||||
<td className="p-2 border-r border-gray-100 text-center font-black text-gray-400 text-sm">-</td>
|
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', department: dept.department, attendance: 'active', source: 'department', title: `部门运营统计 - ${dept.department} - 出勤车辆` }); }} className="text-green-500 hover:underline font-black">{dept.operatingCount}</button>
|
||||||
|
</td>
|
||||||
|
<td className="p-2 border-r border-gray-100 text-center font-black text-gray-400 text-sm">
|
||||||
|
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', department: dept.department, attendance: 'idle', source: 'department', title: `部门运营统计 - ${dept.department} - 闲置车辆` }); }} className="text-gray-400 hover:underline font-black">{dept.idleCount}</button>
|
||||||
|
</td>
|
||||||
<td className="p-2 text-center">
|
<td className="p-2 text-center">
|
||||||
{isExpanded ? <ChevronDown size={16} className="text-blue-500 inline" /> : <ChevronRight size={16} className="text-gray-300 inline" />}
|
{isExpanded ? <ChevronDown size={16} className="text-blue-500 inline" /> : <ChevronRight size={16} className="text-gray-300 inline" />}
|
||||||
</td>
|
</td>
|
||||||
@@ -1651,8 +1665,8 @@ export default function App() {
|
|||||||
>
|
>
|
||||||
<div className="flex justify-between items-center mb-2">
|
<div className="flex justify-between items-center mb-2">
|
||||||
<h3 className="text-sm font-bold text-gray-800">{dept.department}</h3>
|
<h3 className="text-sm font-bold text-gray-800">{dept.department}</h3>
|
||||||
<span className="bg-gray-50 text-gray-400 text-[9px] font-bold px-2 py-0.5 rounded-full">
|
<span className="bg-blue-50 text-blue-600 text-[9px] font-bold px-2 py-0.5 rounded-full">
|
||||||
出勤率: -
|
出勤率: {dept.attendanceRate}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
@@ -1661,13 +1675,15 @@ export default function App() {
|
|||||||
<div className="text-[8px] text-gray-400 uppercase font-bold mb-0.5">总运营</div>
|
<div className="text-[8px] text-gray-400 uppercase font-bold mb-0.5">总运营</div>
|
||||||
<div className="text-xs font-black text-gray-800">{dept.totalAssets}</div>
|
<div className="text-xs font-black text-gray-800">{dept.totalAssets}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center rounded p-1">
|
<div className="text-center cursor-pointer hover:bg-green-50 rounded p-1"
|
||||||
<div className="text-[8px] text-gray-400 uppercase font-bold mb-0.5">出勤</div>
|
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', department: dept.department, attendance: 'active', source: 'department', title: `部门运营统计 - ${dept.department} - 出勤车辆` }); }}>
|
||||||
<div className="text-xs font-black text-gray-400">-</div>
|
<div className="text-[8px] text-green-500 uppercase font-bold mb-0.5">出勤</div>
|
||||||
|
<div className="text-xs font-black text-green-500">{dept.operatingCount}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center rounded p-1">
|
<div className="text-center cursor-pointer hover:bg-gray-100 rounded p-1"
|
||||||
|
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', department: dept.department, attendance: 'idle', source: 'department', title: `部门运营统计 - ${dept.department} - 闲置车辆` }); }}>
|
||||||
<div className="text-[8px] text-gray-400 uppercase font-bold mb-0.5">闲置</div>
|
<div className="text-[8px] text-gray-400 uppercase font-bold mb-0.5">闲置</div>
|
||||||
<div className="text-xs font-black text-gray-400">-</div>
|
<div className="text-xs font-black text-gray-400">{dept.idleCount}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 flex justify-center">
|
<div className="mt-1 flex justify-center">
|
||||||
|
|||||||
@@ -652,35 +652,16 @@ app.get('/dept-stats', async (c) => {
|
|||||||
const vehicles = await getVehicles();
|
const vehicles = await getVehicles();
|
||||||
const withManager = vehicles.filter((v) => v.status === 'Operating');
|
const withManager = vehicles.filter((v) => v.status === 'Operating');
|
||||||
|
|
||||||
// Query mileage data: last 30 days attendance & avg mileage per plate
|
// Query realtime day_mileage from tab_truck_remote_sync_realtime_info
|
||||||
// + today's mileage for idle detection
|
const [realtimeRows] = await pool.query<any[]>(`
|
||||||
const [[mileageRows], [todayRows]] = await Promise.all([
|
SELECT plate_number, day_mileage
|
||||||
pool.query<any[]>(`
|
FROM tab_truck_remote_sync_realtime_info
|
||||||
SELECT plateNumber,
|
WHERE is_deleted = 0 AND plate_number IS NOT NULL
|
||||||
COUNT(CASE WHEN dayMileage > 0 THEN 1 END) AS activeDays,
|
`);
|
||||||
COUNT(*) AS totalDays,
|
|
||||||
AVG(dayMileage) AS avgMileage
|
|
||||||
FROM ln_vehicle_day_mileage
|
|
||||||
WHERE dates >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
|
||||||
GROUP BY plateNumber
|
|
||||||
`),
|
|
||||||
pool.query<any[]>(`
|
|
||||||
SELECT plateNumber, dayMileage
|
|
||||||
FROM ln_vehicle_day_mileage
|
|
||||||
WHERE dates = CURDATE()
|
|
||||||
`),
|
|
||||||
]);
|
|
||||||
const mileageMap = new Map<string, { activeDays: number; totalDays: number; avgMileage: number }>();
|
|
||||||
for (const row of mileageRows as any[]) {
|
|
||||||
mileageMap.set(row.plateNumber, {
|
|
||||||
activeDays: Number(row.activeDays),
|
|
||||||
totalDays: Number(row.totalDays),
|
|
||||||
avgMileage: Number(row.avgMileage),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const todayMileageMap = new Map<string, number>();
|
const todayMileageMap = new Map<string, number>();
|
||||||
for (const row of todayRows as any[]) {
|
for (const row of realtimeRows as any[]) {
|
||||||
todayMileageMap.set(row.plateNumber, Number(row.dayMileage));
|
const plate = (row.plate_number || '').trim();
|
||||||
|
if (plate) todayMileageMap.set(plate, Number(row.day_mileage) || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const deptMap = new Map<string, Map<string, Vehicle[]>>();
|
const deptMap = new Map<string, Map<string, Vehicle[]>>();
|
||||||
@@ -693,15 +674,15 @@ app.get('/dept-stats', async (c) => {
|
|||||||
mgrMap.get(mgr)!.push(v);
|
mgrMap.get(mgr)!.push(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute attendance (today) & avg mileage (30d) for a set of vehicles
|
// Compute attendance & avg mileage from realtime data
|
||||||
const getMileageStats = (vList: Vehicle[]) => {
|
const getMileageStats = (vList: Vehicle[]) => {
|
||||||
const todayActive = vList.filter((v) => (todayMileageMap.get(v.plateNumber) || 0) > 0).length;
|
const todayActive = vList.filter((v) => (todayMileageMap.get(v.plateNumber) || 0) > 0).length;
|
||||||
let totalMileage = 0;
|
let totalMileage = 0;
|
||||||
let count = 0;
|
let count = 0;
|
||||||
for (const v of vList) {
|
for (const v of vList) {
|
||||||
const m = mileageMap.get(v.plateNumber);
|
const m = todayMileageMap.get(v.plateNumber);
|
||||||
if (m) {
|
if (m !== undefined && m > 0) {
|
||||||
totalMileage += m.avgMileage;
|
totalMileage += m;
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -859,11 +840,11 @@ app.get('/list', async (c) => {
|
|||||||
|
|
||||||
let filtered = vehicles;
|
let filtered = vehicles;
|
||||||
|
|
||||||
// attendance filter: active = today mileage > 0, idle = today mileage = 0 (only for Operating vehicles)
|
// attendance filter: active = day_mileage > 0, idle = day_mileage = 0 (only for Operating vehicles)
|
||||||
if (attendance === 'active' || attendance === 'idle') {
|
if (attendance === 'active' || attendance === 'idle') {
|
||||||
const [todayRows] = await pool.query<any[]>(`SELECT plateNumber, dayMileage FROM ln_vehicle_day_mileage WHERE dates = CURDATE()`);
|
const [realtimeRows] = await pool.query<any[]>(`SELECT plate_number, day_mileage FROM tab_truck_remote_sync_realtime_info WHERE is_deleted = 0 AND plate_number IS NOT NULL`);
|
||||||
const todayMap = new Map<string, number>();
|
const todayMap = new Map<string, number>();
|
||||||
for (const row of todayRows as any[]) todayMap.set(row.plateNumber, Number(row.dayMileage));
|
for (const row of realtimeRows as any[]) todayMap.set((row.plate_number || '').trim(), Number(row.day_mileage) || 0);
|
||||||
filtered = filtered.filter((v) => v.status === 'Operating');
|
filtered = filtered.filter((v) => v.status === 'Operating');
|
||||||
if (attendance === 'active') {
|
if (attendance === 'active') {
|
||||||
filtered = filtered.filter((v) => (todayMap.get(v.plateNumber) || 0) > 0);
|
filtered = filtered.filter((v) => (todayMap.get(v.plateNumber) || 0) > 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user