Files
ln-bi/src/modules/assets/components/overview/InventoryStatisticsSection.tsx
T

122 lines
9.1 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 { useId, useRef, useState } from 'react';
import { ArrowUpRight, Boxes, ChevronDown, ChevronsDownUp, ChevronsUpDown, Filter, MapPin, SearchX, Truck, X } from 'lucide-react';
import { InventoryDesktopTable } from './InventoryDesktopTable';
import { InventoryMobileList } from './InventoryMobileList';
import { inventoryGroups, inventorySelection, inventoryTotal } from './inventory-presentation';
import type { InventoryFilters } from '../../model';
import type { InventoryStatisticsSectionProps } from './types';
import './inventory-statistics.css';
const emptyFilters: InventoryFilters = { region: '', city: '', brand: '', type: '', model: '' };
const filterLabels: Record<keyof InventoryFilters, string> = { region: '区域', city: '城市', brand: '品牌', type: '车型', model: '型号 / 批次' };
export function InventoryStatisticsSection(props: InventoryStatisticsSectionProps) {
const {
inventoryFilters: filters, setInventoryFilters: setFilters,
draftInventoryFilters: draft, setDraftInventoryFilters: setDraft,
isInventoryFilterOpen: filterOpen, setIsInventoryFilterOpen: setFilterOpen,
filteredInventoryStats: rows, inventoryTab: tab, setInventoryTab: setTab,
setVehicleSelection: select,
} = props;
const id = useId();
const filterTrigger = useRef<HTMLButtonElement>(null);
const [sort, setSort] = useState<'default' | 'quantity'>('default');
const total = inventoryTotal(rows);
const abnormal = rows.reduce((sum, row) => sum + (row.abnormal || 0), 0);
const pending = rows.reduce((sum, row) => sum + (row.pending || 0), 0);
const activeFilters = (Object.keys(filters) as (keyof InventoryFilters)[]).filter(key => filters[key]);
const groups = inventoryGroups(tab === 'region' ? props.inventoryByRegion : props.inventoryByModel, sort, tab);
const expanded = tab === 'region' ? props.expandedInventoryRegions : props.expandedInventoryTypes;
const toggle = tab === 'region' ? props.toggleInventoryRegion : props.toggleInventoryType;
const allExpanded = groups.length > 0 && groups.every(group => expanded.has(group.name));
const closeFilters = () => { setFilterOpen(false); filterTrigger.current?.focus(); };
const resetFilters = () => { setFilters({ ...emptyFilters }); setDraft({ ...emptyFilters }); };
const options = {
region: props.uniqueInventoryRegions, city: props.uniqueInventoryCities,
brand: props.uniqueInventoryBrands, type: props.uniqueInventoryTypes,
model: props.uniqueInventoryModelsForType,
};
const listProps = { groups, tab, expanded, toggle, total, filters, select };
return (
<section data-testid="inventory-statistics" className="inventory-statistics" aria-labelledby={`${id}-heading`}>
<header className="inventory-heading">
<div className="inventory-title">
<span className="inventory-title-icon"><Boxes size={20} aria-hidden="true" /></span>
<div><h2 id={`${id}-heading`}>库存统计</h2><p>按区域、车型核对库存分布</p></div>
</div>
<button ref={filterTrigger} className="inventory-control" aria-expanded={filterOpen} aria-controls={`${id}-filters`}
onClick={() => { if (!filterOpen) setDraft({ ...filters }); setFilterOpen(!filterOpen); }}>
<Filter size={16} aria-hidden="true" /> 筛选{activeFilters.length > 0 && <span className="inventory-filter-count">{activeFilters.length}</span>}
<ChevronDown size={14} aria-hidden="true" />
</button>
</header>
<div className="inventory-metrics" data-testid="inventory-status-summary">
<button className="inventory-metric inventory-metric-primary" disabled={!total}
onClick={() => select(inventorySelection(filters, activeFilters.length ? '库存统计 · 筛选结果' : '库存统计 · 全部库存'))}>
<span>{activeFilters.length ? '筛选后库存' : '库存总数'}<ArrowUpRight size={16} aria-hidden="true" /></span>
<strong>{total.toLocaleString()}<small>台</small></strong>
</button>
<button className="inventory-metric inventory-metric-pending" disabled={!pending}
onClick={() => select(inventorySelection(filters, '库存统计 · 待交车', { category: 'Pending' }))}>
<span>其中待交车<ArrowUpRight size={16} aria-hidden="true" /></span>
<strong>{pending.toLocaleString()}<small>台</small></strong>
</button>
<button className="inventory-metric inventory-metric-warning" disabled={!abnormal}
onClick={() => select(inventorySelection(filters, '库存统计 · 异动', { category: 'Abnormal' }))}>
<span>其中异动<ArrowUpRight size={16} aria-hidden="true" /></span>
<strong>{abnormal.toLocaleString()}<small>台</small></strong>
</button>
</div>
<div className="inventory-summary-note"><span>{new Set(rows.map(row => row.region)).size} 个区域 · {new Set(rows.map(row => row.city)).size} 个城市</span><span>待交车、异动已包含在库存总数中</span></div>
{filterOpen && <form id={`${id}-filters`} className="inventory-filters" aria-label="库存筛选"
onKeyDown={event => { if (event.key === 'Escape') { event.preventDefault(); closeFilters(); } }}
onSubmit={event => { event.preventDefault(); setFilters({ ...draft }); closeFilters(); }}>
<div className="inventory-filter-heading"><strong>筛选库存</strong><button type="button" className="inventory-icon-button" aria-label="关闭库存筛选" onClick={closeFilters}><X size={18} aria-hidden="true" /></button></div>
<div className="inventory-filter-fields">
{(Object.keys(filterLabels) as (keyof InventoryFilters)[]).map(key => <label key={key}>
{filterLabels[key]}
<select value={draft[key]} onChange={event => setDraft({ ...draft, [key]: event.target.value, ...(key === 'type' ? { model: '' } : {}), ...(key === 'region' ? { city: '' } : {}) })}>
<option value="">全部{filterLabels[key]}</option>
{options[key].filter(Boolean).map(value => <option key={value} value={value}>{value}</option>)}
</select>
</label>)}
</div>
<div className="inventory-filter-actions">
<button type="button" className="inventory-text-button" onClick={() => setDraft({ ...emptyFilters })}>重置条件</button>
<button type="button" className="inventory-control" onClick={closeFilters}>取消</button>
<button type="submit" className="inventory-control inventory-control-primary">应用筛选</button>
</div>
</form>}
{activeFilters.length > 0 && <div className="inventory-active-filters" aria-label="已应用的库存筛选">
<span>已筛选</span>
{activeFilters.map(key => <button key={key} onClick={() => setFilters({ ...filters, [key]: '', ...(key === 'type' ? { model: '' } : {}), ...(key === 'region' ? { city: '' } : {}) })}
aria-label={`移除${filterLabels[key]}筛选:${filters[key]}`}>
{filterLabels[key]}:{filters[key]}<X size={13} aria-hidden="true" />
</button>)}
<button className="inventory-clear" onClick={resetFilters}>清除全部</button>
</div>}
<div className="inventory-toolbar">
<div className="inventory-view-switch" role="group" aria-label="库存分组方式">
<button aria-pressed={tab === 'region'} onClick={() => setTab('region')}><MapPin size={15} aria-hidden="true" />按区域</button>
<button aria-pressed={tab === 'model'} onClick={() => setTab('model')}><Truck size={15} aria-hidden="true" />按车型</button>
</div>
<div className="inventory-toolbar-actions">
<label className="inventory-sort"><span className="sr-only">库存分组排序</span><select value={sort} onChange={event => setSort(event.target.value as typeof sort)}><option value="default">默认排序</option><option value="quantity">库存从多到少</option></select></label>
<button className="inventory-text-button inventory-expand-all" disabled={!groups.length} onClick={() => groups.forEach(group => { if (expanded.has(group.name) === allExpanded) toggle(group.name); })}>
{allExpanded ? <ChevronsDownUp size={16} aria-hidden="true" /> : <ChevronsUpDown size={16} aria-hidden="true" />}{allExpanded ? '收起全部' : '展开全部'}
</button>
</div>
</div>
{groups.length ? <><InventoryDesktopTable {...listProps} /><InventoryMobileList key={JSON.stringify([tab, filters])} {...listProps} /></> :
<div className="inventory-empty"><SearchX size={32} aria-hidden="true" /><h3>{activeFilters.length ? '没有符合条件的库存' : '暂无库存数据'}</h3><p>{activeFilters.length ? '试试减少筛选条件,或清除筛选查看全部库存。' : '当前所属公司范围内暂无库存记录。'}</p>{activeFilters.length > 0 && <button className="inventory-control" onClick={resetFilters}>清除筛选</button>}</div>}
<footer className="inventory-footer"><span role="status" aria-live="polite">共 {groups.length} 个{tab === 'region' ? '区域' : '车型'} · {rows.length} 条记录 · {total.toLocaleString()} 台库存</span><span>占比基于当前筛选结果 · 单位:台</span></footer>
</section>
);
}