Files
ln-bi/src/modules/mileage/daily-report/GroupRow.tsx
T

193 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ReactNode } from 'react';
import { AnimatePresence, motion } from 'motion/react';
import {
ChevronDown,
CircleGauge,
MapPin,
Truck,
} from 'lucide-react';
import type { MileageReportGroup } from '../api';
import VehicleTable from './VehicleTable';
import {
currentAssessmentYearLabel,
dailyMileageStatus,
fmtKm,
} from './utils';
interface GroupRowProps {
group: MileageReportGroup;
reportDate: string;
expanded: boolean;
onToggle: () => void;
}
function BreakdownList({
title,
icon,
items,
showAverage = true,
}: {
title: string;
icon: ReactNode;
items: MileageReportGroup['departments'];
showAverage?: boolean;
}) {
const max = Math.max(1, ...items.map(item => item.vehicleCount));
return (
<div className="min-w-0">
<div className="mb-3 flex items-center gap-2 text-xs font-black text-slate-700">
<span className="text-slate-400">{icon}</span>
{title}
</div>
{items.length === 0 ? (
<div className="rounded-lg bg-slate-50 px-3 py-4 text-center text-[11px] font-bold text-slate-400">暂无数据</div>
) : (
<div className="space-y-2.5">
{items.map(item => (
<div key={item.name}>
<div className="mb-1 flex items-center justify-between gap-3 text-[11px] font-bold">
<span className="truncate text-slate-600">{item.name}</span>
<span className="shrink-0 tabular-nums text-slate-500">
{item.vehicleCount} {showAverage ? ` · 均 ${fmtKm(item.averageMileage)} km` : ''}
</span>
</div>
<div className="h-1.5 overflow-hidden rounded-full bg-slate-100">
<div
className="h-full rounded-full bg-blue-500"
style={{ width: `${Math.max(4, (item.vehicleCount / max) * 100)}%` }}
/>
</div>
</div>
))}
</div>
)}
</div>
);
}
export default function GroupRow({
group,
reportDate,
expanded,
onToggle,
}: GroupRowProps) {
const highRate = group.operatingCount > 0 ? (group.highMileageCount / group.operatingCount) * 100 : 0;
const qualifiedRate = group.vehicleCount > 0 ? (group.qualifiedCount / group.vehicleCount) * 100 : 0;
const assessmentYearLabel = currentAssessmentYearLabel(group.assessmentPeriods, reportDate);
const dailyStatus = dailyMileageStatus(group);
const dailyStatusClass = dailyStatus.tone === 'positive'
? 'border-emerald-100 bg-emerald-50/70 text-emerald-700'
: dailyStatus.tone === 'warning'
? 'border-amber-100 bg-amber-50/70 text-amber-700'
: 'border-slate-100 bg-slate-50 text-slate-600';
return (
<div className="border-b border-slate-100 last:border-0">
<button
type="button"
onClick={onToggle}
className="grid w-full grid-cols-[minmax(170px,1.3fr)_82px_100px_88px_100px_96px_24px] items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-slate-50 max-lg:grid-cols-[minmax(150px,1fr)_82px_96px_24px]"
aria-expanded={expanded}
>
<span className="min-w-0">
<span className="flex min-w-0 items-center gap-1.5">
<span className="truncate text-xs font-black text-slate-900">{group.displayName}</span>
{assessmentYearLabel ? (
<span
className="shrink-0 rounded-md bg-blue-50 px-1.5 py-0.5 text-[9px] font-black text-blue-700 ring-1 ring-blue-100"
title={`当前考核年度:${assessmentYearLabel}`}
>
{assessmentYearLabel}
</span>
) : null}
</span>
<span className="mt-1 flex flex-wrap items-center gap-x-1 text-[10px] font-bold text-slate-400">
<span>{group.vehicleCount} · 运营 {group.operatingCount} · 库存 {group.inventoryCount}</span>
<span className="text-emerald-600">· 达标 {group.qualifiedCount} </span>
</span>
</span>
<span className="text-right">
<span className="block text-xs font-black tabular-nums text-blue-600">{group.dailyMileage}</span>
<span className="text-[9px] font-bold text-slate-400">km</span>
</span>
<span className="text-right max-lg:hidden">
<span className={`block text-xs font-black tabular-nums ${group.dayOverDayDelta >= 0 ? 'text-emerald-600' : 'text-rose-600'}`}>
{group.dayOverDayDelta >= 0 ? '+' : ''}{fmtKm(group.dayOverDayDelta)}
</span>
<span className="text-[9px] font-bold text-slate-400">环比变化</span>
</span>
<span className="text-right max-lg:hidden">
<span className="block text-xs font-black tabular-nums text-slate-700">{group.lowMileageCount}/{group.highMileageCount}</span>
<span className="text-[9px] font-bold text-slate-400">/高里程</span>
</span>
<span className="text-right">
<span className="block text-xs font-black tabular-nums text-slate-700">{highRate.toFixed(1)}%</span>
<span className="text-[9px] font-bold text-slate-400">高里程占比</span>
</span>
<span className="text-right max-lg:hidden">
<span className="block text-xs font-black tabular-nums text-slate-700">{group.qualifiedCount}</span>
<span className="text-[9px] font-bold text-slate-400">年度达标</span>
</span>
<ChevronDown size={16} className={`text-slate-400 transition-transform ${expanded ? 'rotate-180' : ''}`} />
</button>
<AnimatePresence initial={false}>
{expanded ? (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="overflow-hidden"
>
<div className="bg-slate-50/70 px-4 py-4">
<div className="grid gap-4 xl:grid-cols-[1fr_1fr_1.1fr]">
<BreakdownList title="业务部门" icon={<Truck size={15} />} items={group.departments} />
<BreakdownList title="库存地区" icon={<MapPin size={15} />} items={group.inventoryRegions} showAverage={false} />
<div>
<div className="mb-3 flex items-center gap-2 text-xs font-black text-slate-700">
<CircleGauge size={15} className="text-slate-400" />考核进度
</div>
<div className="grid grid-cols-2 gap-2">
<div className="rounded-lg border border-slate-100 bg-white p-3">
<div className="text-[10px] font-bold text-slate-400">累计完成率</div>
<div className="mt-1 text-base font-black tabular-nums text-slate-900">{group.completionRate.toFixed(1)}%</div>
</div>
<div className="rounded-lg border border-slate-100 bg-white p-3">
<div className="text-[10px] font-bold text-slate-400">达标50%</div>
<div className="mt-1 text-base font-black tabular-nums text-slate-900">{group.halfQualifiedCount} </div>
</div>
<div className="rounded-lg border border-slate-100 bg-white p-3">
<div className="text-[10px] font-bold text-slate-400">剩余任务</div>
<div className="mt-1 text-base font-black tabular-nums text-slate-900">{fmtKm(group.remainingMileage)} km</div>
</div>
<div className="rounded-lg border border-slate-100 bg-white p-3">
<div className="text-[10px] font-bold text-slate-400">每日应完成</div>
<div className="mt-1 text-base font-black tabular-nums text-blue-600">{fmtKm(group.dailyRequiredMileage)} km</div>
</div>
<div className={`col-span-2 flex flex-wrap items-end justify-between gap-x-4 gap-y-2 rounded-lg border p-3 ${dailyStatusClass}`}>
<div>
<div className="text-[10px] font-black">当日目标执行 · {dailyStatus.label}</div>
<div className="mt-1 text-[10px] font-bold opacity-80">当日实际 {fmtKm(group.dailyMileage)} km / 应完成 {fmtKm(group.dailyRequiredMileage)} km</div>
</div>
<div className="max-w-[260px] text-[10px] font-bold leading-relaxed opacity-80">{dailyStatus.detail}</div>
</div>
<div className="col-span-2 flex items-end justify-between gap-3 rounded-lg border border-blue-100 bg-blue-50/50 p-3">
<div>
<div className="text-[10px] font-bold text-slate-500">累计达标率</div>
<div className="mt-1 text-[10px] font-bold text-slate-500">已达标 {group.qualifiedCount} / {group.vehicleCount} </div>
</div>
<div className="text-lg font-black tabular-nums text-blue-700">{qualifiedRate.toFixed(1)}%</div>
</div>
</div>
<div className="mt-2 text-[10px] font-bold leading-relaxed text-slate-400">{group.assessmentPeriods.join('')}</div>
</div>
</div>
<VehicleTable group={group} />
</div>
</motion.div>
) : null}
</AnimatePresence>
</div>
);
}