ci/woodpecker/push/woodpecker Pipeline failed
Use operations signing times and delivery completion statuses; include replacement deliveries in delivery totals with range drilldowns and exports. Separate abnormal vehicles from inventory, add a date range picker, and enable CI for feature/asset-statistics.
283 lines
19 KiB
TypeScript
283 lines
19 KiB
TypeScript
import React from 'react';
|
|
import { ChevronDown, Filter, Loader2, PlusCircle, Truck } from 'lucide-react';
|
|
import { AnimatePresence, motion } from 'motion/react';
|
|
import Blur from '../../../components/Blur';
|
|
import { SearchSelect } from '../../../components/SearchSelect';
|
|
import type { WeeklyDetailItem } from '../api';
|
|
import type { ModalVehicleFilters, VehicleModalSelection } from '../model';
|
|
import type { VehicleListItem } from '../types';
|
|
|
|
interface VehicleDetailModalProps {
|
|
selection: VehicleModalSelection | null;
|
|
setSelection: React.Dispatch<React.SetStateAction<VehicleModalSelection | null>>;
|
|
isFilterExpanded: boolean;
|
|
setIsFilterExpanded: React.Dispatch<React.SetStateAction<boolean>>;
|
|
filters: ModalVehicleFilters;
|
|
setFilters: React.Dispatch<React.SetStateAction<ModalVehicleFilters>>;
|
|
plates: string[];
|
|
models: string[];
|
|
brands: string[];
|
|
locations: string[];
|
|
loading: boolean;
|
|
weeklyDetails: WeeklyDetailItem[];
|
|
filteredWeeklyDetails: WeeklyDetailItem[];
|
|
filteredVehicles: VehicleListItem[];
|
|
}
|
|
export function VehicleDetailModal({
|
|
selection: showPlateNumbers,
|
|
setSelection: setShowPlateNumbers,
|
|
isFilterExpanded: isModalFilterExpanded,
|
|
setIsFilterExpanded: setIsModalFilterExpanded,
|
|
filters: modalFilters,
|
|
setFilters: setModalFilters,
|
|
plates: uniqueModalPlates,
|
|
models: uniqueModalModels,
|
|
brands: uniqueModalBrands,
|
|
locations: uniqueModalLocations,
|
|
loading: modalLoading,
|
|
weeklyDetails: modalWeeklyDetail,
|
|
filteredWeeklyDetails: filteredModalWeeklyDetail,
|
|
filteredVehicles: filteredModalVehicles,
|
|
}: VehicleDetailModalProps) {
|
|
return (
|
|
<>
|
|
{/* Vehicle Detail Modal */}
|
|
<AnimatePresence>
|
|
{showPlateNumbers && (
|
|
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
|
<motion.div
|
|
initial={{ scale: 0.9, opacity: 0, y: 20 }}
|
|
animate={{ scale: 1, opacity: 1, y: 0 }}
|
|
className={`bg-white rounded-xl shadow-2xl w-full max-w-[95vw] ${showPlateNumbers.source === 'customer' ? 'lg:max-w-6xl' : 'lg:max-w-4xl'} overflow-hidden flex flex-col max-h-[85vh] sm:max-h-[90vh] min-h-[40vh]`}
|
|
>
|
|
<div className="p-4 border-b border-gray-100 flex justify-between items-center bg-slate-800 text-white shrink-0">
|
|
<div>
|
|
<h3 className="font-bold text-base flex items-center gap-2">
|
|
<Truck size={18} className="text-blue-400" />
|
|
{showPlateNumbers.title || (
|
|
(showPlateNumbers.manager ? `${showPlateNumbers.manager} 的${showPlateNumbers.type || ''}车辆` :
|
|
showPlateNumbers.customer ? `${showPlateNumbers.customer} 的${showPlateNumbers.type || ''}车辆` :
|
|
showPlateNumbers.batch === 'All' ? '全量批次' : `${showPlateNumbers.batch} 批次`) + ' - 运营明细'
|
|
)}
|
|
</h3>
|
|
<p className="text-[10px] opacity-60 mt-0.5">
|
|
{showPlateNumbers.model === 'All' ? '全量型号' : showPlateNumbers.model} |
|
|
{showPlateNumbers.category === 'Pending' ? '待交车' :
|
|
showPlateNumbers.category === 'Delivered' ? '本周已交车' :
|
|
showPlateNumbers.category === 'Returned' ? '已还车' :
|
|
showPlateNumbers.category === 'Replaced' ? '已替换' :
|
|
showPlateNumbers.category === 'Abnormal' ? '异动' :
|
|
showPlateNumbers.category === 'Inventory' ? `${showPlateNumbers.location}库存` :
|
|
showPlateNumbers.category === 'Operating' ? '正在运营' : '全部状态'}
|
|
</p>
|
|
</div>
|
|
<button onClick={() => setShowPlateNumbers(null)} className="hover:bg-white/10 p-2 rounded-full transition-colors">
|
|
<PlusCircle className="rotate-45" size={24} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Modal Filters */}
|
|
<div className="px-4 py-2 bg-slate-50 border-b border-gray-200 shrink-0">
|
|
<div
|
|
className="flex justify-between items-center cursor-pointer py-1"
|
|
onClick={() => setIsModalFilterExpanded(!isModalFilterExpanded)}
|
|
>
|
|
<div className="flex items-center gap-2 text-slate-600">
|
|
<Filter size={14} />
|
|
<span className="text-xs font-bold">筛选条件</span>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
{/* Quick Search always visible when collapsed */}
|
|
{!isModalFilterExpanded && (
|
|
<div className="w-40 sm:w-64" onClick={(e) => e.stopPropagation()}>
|
|
<SearchSelect value={modalFilters.plateNumber} onChange={(v) => setModalFilters({...modalFilters, plateNumber: v})} options={uniqueModalPlates} placeholder="快速搜索车牌..." className="text-[11px] py-1 px-2" />
|
|
</div>
|
|
)}
|
|
<motion.div
|
|
animate={{ rotate: isModalFilterExpanded ? 180 : 0 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<ChevronDown size={16} className="text-slate-400" />
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
|
|
<AnimatePresence>
|
|
{isModalFilterExpanded && (
|
|
<motion.div
|
|
initial={{ height: 0, opacity: 0 }}
|
|
animate={{ height: 'auto', opacity: 1 }}
|
|
exit={{ height: 0, opacity: 0 }}
|
|
className="overflow-hidden"
|
|
>
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 py-3 border-t border-gray-100 mt-1">
|
|
<div className="space-y-1">
|
|
<label className="block text-[10px] text-gray-500 font-medium">车牌号码</label>
|
|
<SearchSelect value={modalFilters.plateNumber} onChange={(v) => setModalFilters({...modalFilters, plateNumber: v})} options={uniqueModalPlates} placeholder="全部车牌" className="text-[11px] py-1.5 px-2" />
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="block text-[10px] text-gray-500 font-medium">车型型号</label>
|
|
<select value={modalFilters.model} onChange={(e) => setModalFilters({...modalFilters, model: e.target.value})} className="w-full text-[11px] px-2 py-1.5 bg-white border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm cursor-pointer">
|
|
<option value="">全部车型</option>
|
|
{uniqueModalModels.map(m => <option key={m} value={m}>{m}</option>)}
|
|
</select>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="block text-[10px] text-gray-500 font-medium">品牌名称</label>
|
|
<select value={modalFilters.brand} onChange={(e) => setModalFilters({...modalFilters, brand: e.target.value})} className="w-full text-[11px] px-2 py-1.5 bg-white border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm cursor-pointer">
|
|
<option value="">全部品牌</option>
|
|
{uniqueModalBrands.map(b => <option key={b} value={b}>{b}</option>)}
|
|
</select>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="block text-[10px] text-gray-500 font-medium">运营所在地</label>
|
|
<select value={modalFilters.location} onChange={(e) => setModalFilters({...modalFilters, location: e.target.value})} className="w-full text-[11px] px-2 py-1.5 bg-white border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm cursor-pointer">
|
|
<option value="">全部地区</option>
|
|
{uniqueModalLocations.map(l => <option key={l} value={l}>{l}</option>)}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-end pb-2">
|
|
<button
|
|
onClick={() => setModalFilters({ plateNumber: '', model: '', brand: '', location: '' })}
|
|
className="text-[10px] text-blue-500 hover:text-blue-600 font-medium"
|
|
>
|
|
重置筛选
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-auto p-0 sm:p-4 bg-gray-50 min-h-0 overscroll-contain">
|
|
{modalLoading ? (
|
|
<div className="flex items-center justify-center py-16">
|
|
<Loader2 className="animate-spin text-blue-500" size={32} />
|
|
</div>
|
|
) : modalWeeklyDetail.length > 0 ? (
|
|
<div className="bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden w-full">
|
|
<table className="w-full text-left border-collapse">
|
|
<thead>
|
|
<tr className="bg-slate-700 text-white text-[10px] uppercase tracking-wider sticky top-0 z-20 shadow-sm">
|
|
<th className="p-2 font-semibold border-r border-slate-600 text-center">车牌</th>
|
|
<th className="p-2 font-semibold border-r border-slate-600 text-center">客户名称</th>
|
|
<th className="p-2 font-semibold text-center">日期</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="text-[11px]">
|
|
{filteredModalWeeklyDetail.map((v, i) => (
|
|
<tr key={`${v.truck_id}-${i}`} className={`border-b border-gray-100 hover:bg-blue-50/50 transition-colors ${i % 2 === 0 ? 'bg-white' : 'bg-gray-50/30'}`}>
|
|
<td className="p-2 border-r border-gray-100 font-mono font-bold text-blue-700 text-center"><Blur>{v.plate_number}</Blur></td>
|
|
<td className="p-2 border-r border-gray-100 text-gray-600 text-center"><Blur>{v.customer_name || '—'}</Blur></td>
|
|
<td className="p-2 text-gray-500 text-center">{v.handover_date ? v.handover_date.slice(0, 10) : '—'}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
) : (
|
|
<div className="bg-white rounded-lg border border-gray-200 shadow-sm w-full">
|
|
<table className="min-w-full w-max text-left border-collapse">
|
|
<thead className="sticky top-0 z-20 shadow-sm">
|
|
<tr className="bg-slate-700 text-white text-[10px] uppercase tracking-wider whitespace-nowrap">
|
|
{showPlateNumbers.source === 'customer' ? (
|
|
<>
|
|
<th className="p-2 font-semibold border-r border-slate-600 w-24">业务部门</th>
|
|
<th className="p-2 font-semibold border-r border-slate-600 w-24">业务负责人</th>
|
|
<th className="p-2 font-semibold border-r border-slate-600 w-16">品牌</th>
|
|
<th className="p-2 font-semibold border-r border-slate-600 w-16">车型</th>
|
|
<th className="p-2 font-semibold border-r border-slate-600 w-48">资产归属</th>
|
|
<th className="p-2 font-semibold border-r border-slate-600 w-48">客户名称</th>
|
|
<th className="p-2 font-semibold border-r border-slate-600 w-24">车牌</th>
|
|
<th className="p-2 font-semibold border-r border-slate-600 w-16 text-center">状态</th>
|
|
<th className="p-2 font-semibold border-r border-slate-600 w-24 text-center">提车时间</th>
|
|
<th className="p-2 font-semibold border-r border-slate-600 w-24 text-center">合同到期时间</th>
|
|
<th className="p-2 font-semibold border-r border-slate-600 w-24">运营区域</th>
|
|
<th className="p-2 font-semibold border-r border-slate-600 w-20 text-center">离到期</th>
|
|
<th className="p-2 font-semibold w-48">签约公司</th>
|
|
</>
|
|
) : (
|
|
<>
|
|
<th className="p-2 font-semibold border-r border-slate-600 text-center">车牌</th>
|
|
{showPlateNumbers.source !== 'asset' && showPlateNumbers.category !== 'Inventory' && (
|
|
<th className="p-2 font-semibold border-r border-slate-600 text-center">客户名称</th>
|
|
)}
|
|
<th className="p-2 font-semibold border-r border-slate-600 text-center">品牌</th>
|
|
<th className="p-2 font-semibold border-r border-slate-600 text-center">车型</th>
|
|
<th className="p-2 font-semibold text-center">所在地</th>
|
|
</>
|
|
)}
|
|
</tr>
|
|
</thead>
|
|
<tbody className="text-[11px]">
|
|
{filteredModalVehicles.map((v, idx) => (
|
|
<tr key={v.id} className={`border-b border-gray-100 hover:bg-blue-50/50 transition-colors whitespace-nowrap ${idx % 2 === 0 ? 'bg-white' : 'bg-gray-50/30'}`}>
|
|
{showPlateNumbers.source === 'customer' ? (
|
|
<>
|
|
<td className="p-2 border-r border-gray-100 text-gray-600">{v.departmentName || '—'}</td>
|
|
<td className="p-2 border-r border-gray-100 font-medium text-gray-700"><Blur>{v.customerManager || '—'}</Blur></td>
|
|
<td className="p-2 border-r border-gray-100 text-gray-600">{v.brandLabel || '—'}</td>
|
|
<td className="p-2 border-r border-gray-100 text-gray-600">{v.type}</td>
|
|
<td className="p-2 border-r border-gray-100 text-gray-500 text-[10px]"><Blur>{v.subjectOrg || '—'}</Blur></td>
|
|
<td className="p-2 border-r border-gray-100 font-bold text-gray-800"><Blur>{v.customerName || '—'}</Blur></td>
|
|
<td className={`p-2 border-r border-gray-100 font-mono font-bold ${v.plateNumber ? 'text-blue-700' : 'text-orange-500'}`}><Blur>{v.plateNumber || v.vin || '—'}</Blur></td>
|
|
<td className="p-2 border-r border-gray-100 text-center">
|
|
<span className={`px-1.5 py-0.5 rounded-full text-[9px] font-bold ${
|
|
v.status === 'Operating' ? 'bg-green-100 text-green-700' :
|
|
v.status === 'Inventory' ? 'bg-blue-100 text-blue-700' : 'bg-red-100 text-red-700'
|
|
}`}>
|
|
{v.status === 'Operating' ? '在租' : v.status === 'Inventory' ? '库存' : '异常'}
|
|
</span>
|
|
</td>
|
|
<td className="p-2 border-r border-gray-100 text-center text-gray-500">{'—'}</td>
|
|
<td className="p-2 border-r border-gray-100 text-center text-gray-500">{'—'}</td>
|
|
<td className="p-2 border-r border-gray-100 text-gray-600">{v.location === '其他' ? '对接中' : v.location}</td>
|
|
<td className="p-2 border-r border-gray-100 text-center font-bold text-orange-600">{'—'}</td>
|
|
<td className="p-2 text-gray-500 text-[10px]"><Blur>{v.orgName || '—'}</Blur></td>
|
|
</>
|
|
) : (
|
|
<>
|
|
<td className={`p-2 border-r border-gray-100 font-mono font-bold text-center ${v.plateNumber ? 'text-blue-700' : 'text-orange-500'}`}><Blur>{v.plateNumber || v.vin || '—'}</Blur></td>
|
|
{showPlateNumbers.source !== 'asset' && showPlateNumbers.category !== 'Inventory' && (
|
|
<td className="p-2 border-r border-gray-100 font-bold text-gray-800 text-center"><Blur>{v.customerName || '—'}</Blur></td>
|
|
)}
|
|
<td className="p-2 border-r border-gray-100 text-gray-600 text-center">{v.brandLabel || '—'}</td>
|
|
<td className="p-2 border-r border-gray-100 text-gray-600 text-center">{v.type}</td>
|
|
<td className="p-2 text-gray-600 text-center">{v.location === '其他' ? '对接中' : v.location}</td>
|
|
</>
|
|
)}
|
|
</tr>
|
|
))}
|
|
{filteredModalVehicles.length === 0 && (
|
|
<tr>
|
|
<td colSpan={showPlateNumbers.source === 'customer' ? 13 : ((showPlateNumbers.source === 'asset' || showPlateNumbers.category === 'Inventory') ? 4 : 5)} className="p-8 text-center text-gray-400 italic">
|
|
暂无符合条件的车辆数据
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="p-4 bg-white border-t border-gray-100 flex justify-between items-center shrink-0">
|
|
<div className="text-xs text-gray-500">
|
|
共计 <span className="font-bold text-blue-600">{filteredModalWeeklyDetail.length > 0 ? filteredModalWeeklyDetail.length : filteredModalVehicles.length}</span> 台车辆
|
|
</div>
|
|
<button
|
|
onClick={() => setShowPlateNumbers(null)}
|
|
className="px-6 py-2 bg-slate-800 text-white rounded-lg text-xs font-bold hover:bg-slate-700 transition-colors shadow-sm"
|
|
>
|
|
确认并关闭
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
)}
|
|
</AnimatePresence>
|
|
</>
|
|
);
|
|
}
|