ci/woodpecker/push/woodpecker Pipeline was successful
Refactor asset drilldowns with combined dimension filters, parent summaries and progressive loading. Add the hidden /who read-only permission preview with scoped authorization, identity mapping and audit logging. Refine inventory counts and AI问数 presentation.
177 lines
9.4 KiB
TypeScript
177 lines
9.4 KiB
TypeScript
import { InventoryStatusCounts } from './InventoryStatusCounts';
|
|
import { inventorySelection, type InventoryDisplayGroup } from './inventory-presentation';
|
|
import type { InventoryFilters } from '../../model';
|
|
import type { RegionalInventoryStats } from '../../types';
|
|
import { inventorySubtotalSelection } from './inventory-presentation';
|
|
import React from 'react';
|
|
import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
import { AnimatePresence, motion } from 'motion/react';
|
|
import type { InventoryStatisticsSectionProps } from './types';
|
|
|
|
type InventoryDesktopTableProps = Pick<
|
|
InventoryStatisticsSectionProps,
|
|
| 'inventoryTab'
|
|
| 'inventoryByRegion'
|
|
| 'expandedInventoryRegions'
|
|
| 'toggleInventoryRegion'
|
|
| 'inventoryByModel'
|
|
| 'expandedInventoryTypes'
|
|
| 'toggleInventoryType'
|
|
| 'setVehicleSelection'
|
|
>;
|
|
|
|
export function InventoryDesktopTable({
|
|
inventoryTab,
|
|
inventoryByRegion,
|
|
expandedInventoryRegions,
|
|
toggleInventoryRegion,
|
|
inventoryByModel,
|
|
expandedInventoryTypes,
|
|
toggleInventoryType,
|
|
setVehicleSelection: setShowPlateNumbers,
|
|
}: InventoryDesktopTableProps) {
|
|
return (
|
|
<div className="hidden lg:block overflow-x-auto">
|
|
<table className="w-full text-left border-collapse">
|
|
<thead>
|
|
<tr className="bg-slate-50 text-[11px] text-slate-500 uppercase tracking-wider border-b border-slate-100">
|
|
<th className="p-3 font-semibold w-64">{inventoryTab === 'region' ? '区域 / 城市' : '车型分类 / 型号'}</th>
|
|
<th className="p-3 font-semibold">{inventoryTab === 'region' ? '品牌' : '品牌'}</th>
|
|
<th className="p-3 font-semibold">{inventoryTab === 'region' ? '车型' : '所在区域/城市'}</th>
|
|
<th className="p-3 font-semibold text-center w-32">库存数量</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="text-xs">
|
|
{inventoryTab === 'region' ? (
|
|
Object.entries(inventoryByRegion).map(([region, cities]) => (
|
|
<React.Fragment key={region}>
|
|
<tr
|
|
className="bg-slate-50/30 border-b border-slate-100 cursor-pointer hover:bg-slate-50 transition-colors"
|
|
onClick={() => toggleInventoryRegion(region)}
|
|
>
|
|
<td colSpan={4} className="p-3 font-bold text-slate-700">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
{expandedInventoryRegions.has(region) ? <ChevronDown size={14} className="text-slate-400" /> : <ChevronRight size={14} className="text-slate-400" />}
|
|
<span>{region}</span>
|
|
<span className="text-[10px] font-normal text-slate-400 ml-2 cursor-pointer hover:text-blue-500 transition-colors"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setShowPlateNumbers(inventorySubtotalSelection(Object.values(cities).flat(), `库存统计 - ${region}`));
|
|
}}>
|
|
(共 {Object.values(cities).flat().reduce((acc, s) => acc + s.quantity, 0)} 台)
|
|
</span>
|
|
<InventoryStatusCounts onSelect={setShowPlateNumbers} rows={Object.values(cities).flat()} />
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<AnimatePresence>
|
|
{expandedInventoryRegions.has(region) && Object.entries(cities).map(([city, stats]) => (
|
|
<React.Fragment key={city}>
|
|
{stats.map((stat, idx) => (
|
|
<motion.tr
|
|
key={`${city}-${stat.model}-${idx}`}
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
className="border-b border-slate-50 hover:bg-slate-50/20 transition-colors"
|
|
>
|
|
<td className="p-3 pl-8 text-slate-500 border-r border-slate-50">
|
|
{idx === 0 ? <span className="font-medium text-slate-600">{city}</span> : ''}
|
|
</td>
|
|
<td className="p-3 text-slate-600 border-r border-slate-50">{stat.brand}</td>
|
|
<td className="p-3 text-slate-600 border-r border-slate-50">{stat.model}</td>
|
|
<td className="p-3 text-center font-bold text-blue-600">
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setShowPlateNumbers(inventorySubtotalSelection([stat], `库存统计 - ${stat.model} - ${stat.city}`));
|
|
}}
|
|
className="text-blue-600 hover:underline font-bold"
|
|
>
|
|
{stat.quantity}
|
|
</button>
|
|
{((stat.pending ?? 0) > 0 || (stat.abnormal ?? 0) > 0) && (
|
|
<div className="mt-1"><InventoryStatusCounts onSelect={setShowPlateNumbers} rows={[stat]} /></div>
|
|
)}
|
|
</td>
|
|
</motion.tr>
|
|
))}
|
|
</React.Fragment>
|
|
))}
|
|
</AnimatePresence>
|
|
</React.Fragment>
|
|
))
|
|
) : (
|
|
Object.entries(inventoryByModel).map(([type, models]) => (
|
|
<React.Fragment key={type}>
|
|
<tr
|
|
className="bg-slate-50/30 border-b border-slate-100 cursor-pointer hover:bg-slate-50 transition-colors"
|
|
onClick={() => toggleInventoryType(type)}
|
|
>
|
|
<td colSpan={4} className="p-3 font-bold text-slate-700">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
{expandedInventoryTypes.has(type) ? <ChevronDown size={14} className="text-slate-400" /> : <ChevronRight size={14} className="text-slate-400" />}
|
|
<span>{type}</span>
|
|
<span className="text-[10px] font-normal text-slate-400 ml-2 cursor-pointer hover:text-blue-500 transition-colors"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setShowPlateNumbers(inventorySubtotalSelection(Object.values(models).flat(), `库存统计 - ${type}`));
|
|
}}>
|
|
(共 {Object.values(models).flat().reduce((acc, s) => acc + s.quantity, 0)} 台)
|
|
</span>
|
|
<InventoryStatusCounts onSelect={setShowPlateNumbers} rows={Object.values(models).flat()} />
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<AnimatePresence>
|
|
{expandedInventoryTypes.has(type) && Object.entries(models).map(([model, stats]) => (
|
|
<React.Fragment key={model}>
|
|
{stats.map((stat, idx) => (
|
|
<motion.tr
|
|
key={`${model}-${stat.region}-${stat.city}-${idx}`}
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
className="border-b border-slate-50 hover:bg-slate-50/20 transition-colors"
|
|
>
|
|
<td className="p-3 pl-8 text-slate-500 border-r border-slate-50">
|
|
{idx === 0 ? <span className="font-medium text-slate-600">{model}</span> : ''}
|
|
</td>
|
|
<td className="p-3 text-slate-600 border-r border-slate-50">{stat.brand}</td>
|
|
<td className="p-3 text-slate-600 border-r border-slate-50">{stat.region} / {stat.city}</td>
|
|
<td className="p-3 text-center font-bold text-blue-600">
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setShowPlateNumbers(inventorySubtotalSelection([stat], `库存统计 - ${stat.model} - ${stat.city}`));
|
|
}}
|
|
className="text-blue-600 hover:underline font-bold"
|
|
>
|
|
{stat.quantity}
|
|
</button>
|
|
{((stat.pending ?? 0) > 0 || (stat.abnormal ?? 0) > 0) && (
|
|
<div className="mt-1"><InventoryStatusCounts onSelect={setShowPlateNumbers} rows={[stat]} /></div>
|
|
)}
|
|
</td>
|
|
</motion.tr>
|
|
))}
|
|
</React.Fragment>
|
|
))}
|
|
</AnimatePresence>
|
|
</React.Fragment>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function selectInventoryRow(row: RegionalInventoryStats, select: InventoryStatisticsSectionProps['setVehicleSelection']) {
|
|
select(inventorySelection(row, `库存统计 · ${row.city} · ${row.model}`, { vehicleType: row.type }));
|
|
}
|
|
|
|
export function selectInventoryGroup(group: InventoryDisplayGroup, { tab, filters, select }: { tab: 'region' | 'model'; filters: InventoryFilters; select: InventoryStatisticsSectionProps['setVehicleSelection'] }) {
|
|
select(inventorySelection(tab === 'model' ? { ...filters, type: group.name } : filters, `库存统计 · ${group.name}`, tab === 'region'
|
|
? { location: filters.city || group.name }
|
|
: {}));
|
|
}
|