feat: logistics board as grouped tables with column headers
This commit is contained in:
parent
f8c6c538b7
commit
a764213a77
|
|
@ -9,16 +9,7 @@ import {
|
||||||
import { Badge } from "../UI/Badge";
|
import { Badge } from "../UI/Badge";
|
||||||
import { Panel } from "../UI/Panel";
|
import { Panel } from "../UI/Panel";
|
||||||
import { OrderFilters } from "../orders/OrderFilters";
|
import { OrderFilters } from "../orders/OrderFilters";
|
||||||
|
import { formatDateTime } from "../../utils/formatters";
|
||||||
const renderOrderNumbersCompact = (group) => {
|
|
||||||
if (!Array.isArray(group.orderNumbers) || !group.orderNumbers.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const text = group.orderNumbers.length <= 3
|
|
||||||
? group.orderNumbers.join(", ")
|
|
||||||
: group.orderNumbers.slice(0, 3).join(", ") + ` +${group.orderNumbers.length - 3}`;
|
|
||||||
return <span className="text-xs text-[var(--color-text-muted)]">{text}</span>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusOptions = ORDER_GROUP_DISPLAY_STATUS_OPTIONS }) => {
|
export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusOptions = ORDER_GROUP_DISPLAY_STATUS_OPTIONS }) => {
|
||||||
const [filters, setFilters] = React.useState({ query: "", displayStatus: "all" });
|
const [filters, setFilters] = React.useState({ query: "", displayStatus: "all" });
|
||||||
|
|
@ -60,6 +51,19 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
|
||||||
|
|
||||||
const totalGroups = filteredGroups.length;
|
const totalGroups = filteredGroups.length;
|
||||||
|
|
||||||
|
const TableHeader = () => (
|
||||||
|
<thead className="bg-[var(--color-surface-strong)] text-left text-xs uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 font-medium">Клиент</th>
|
||||||
|
<th className="px-4 py-3 font-medium hidden sm:table-cell">Телефон</th>
|
||||||
|
<th className="px-4 py-3 font-medium hidden md:table-cell">Дата доставки</th>
|
||||||
|
<th className="px-4 py-3 font-medium hidden lg:table-cell">Заказы</th>
|
||||||
|
<th className="px-4 py-3 font-medium">Статус</th>
|
||||||
|
<th className="px-4 py-3 font-medium hidden md:table-cell">Обновлён</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Panel className="space-y-4 p-5">
|
<Panel className="space-y-4 p-5">
|
||||||
|
|
@ -82,7 +86,7 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
|
||||||
По этому поиску ничего не найдено.
|
По этому поиску ничего не найдено.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-6">
|
<div className="grid gap-4">
|
||||||
{Array.from(statusGroups.entries()).sort(([a], [b]) => {
|
{Array.from(statusGroups.entries()).sort(([a], [b]) => {
|
||||||
const idxA = FUNNEL_ORDER.indexOf(a);
|
const idxA = FUNNEL_ORDER.indexOf(a);
|
||||||
const idxB = FUNNEL_ORDER.indexOf(b);
|
const idxB = FUNNEL_ORDER.indexOf(b);
|
||||||
|
|
@ -94,10 +98,10 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
|
||||||
const isCollapsed = collapsedSections.has(statusValue);
|
const isCollapsed = collapsedSections.has(statusValue);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={statusValue} className="space-y-2">
|
<Panel key={statusValue} className="overflow-hidden p-0">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex w-full items-center justify-between text-left"
|
className="flex w-full items-center justify-between px-5 py-3 text-left transition hover:bg-[var(--color-accent-soft)]"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCollapsedSections((prev) => {
|
setCollapsedSections((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
|
|
@ -126,47 +130,47 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{!isCollapsed && groups.map((group) => (
|
{!isCollapsed && (
|
||||||
<button
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full border-collapse border-t border-[var(--color-border)]">
|
||||||
|
<TableHeader />
|
||||||
|
<tbody>
|
||||||
|
{groups.map((group) => (
|
||||||
|
<tr
|
||||||
key={group.id}
|
key={group.id}
|
||||||
className="w-full rounded-[16px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] px-4 py-2 !text-left !text-[var(--color-text)] transition hover:bg-[var(--color-accent-soft)]"
|
className="cursor-pointer border-t border-[var(--color-border)] transition hover:bg-[var(--color-accent-soft)]"
|
||||||
onClick={() => {
|
onClick={() => { if (onSelectSet) onSelectSet(group.id); }}
|
||||||
if (onSelectSet) {
|
|
||||||
onSelectSet(group.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
{/* Desktop: single-line compact row */}
|
<td className="px-4 py-2.5">
|
||||||
<div className="hidden md:flex md:items-center md:gap-3 md:text-sm">
|
<div className="font-medium">{group.displayTitle || group.customerName || group.groupKey}</div>
|
||||||
<span className="min-w-0 truncate font-semibold">{group.displayTitle || group.customerName || group.groupKey}</span>
|
<div className="text-xs text-[var(--color-text-muted)] sm:hidden">{group.customerPhone || "—"}</div>
|
||||||
<span className="shrink-0 text-[var(--color-text-muted)]">{group.customerDate || "—"}</span>
|
<div className="text-xs text-[var(--color-text-muted)] md:hidden">{group.deliveryDate || "—"}</div>
|
||||||
<span className="shrink-0 text-[var(--color-text-muted)]">{group.customerPhone || "—"}</span>
|
</td>
|
||||||
<span className="shrink-0">{group.ordersCount || 0} {group.ordersCount === 1 ? "заказ" : group.ordersCount < 5 ? "заказа" : "заказов"}</span>
|
<td className="px-4 py-2.5 text-sm hidden sm:table-cell">
|
||||||
{renderOrderNumbersCompact(group)}
|
{group.customerPhone || "—"}
|
||||||
{group.assignedDriverName && <span className="text-[var(--color-accent)]">{group.assignedDriverName}</span>}
|
</td>
|
||||||
<span className="ml-auto shrink-0"><Badge tone={getOrderGroupStatusTone(group)}>{getOrderGroupDisplayStatusLabel(group)}</Badge></span>
|
<td className="px-4 py-2.5 text-sm hidden md:table-cell">
|
||||||
</div>
|
{group.deliveryDate
|
||||||
{/* Mobile: stacked */}
|
? <span>{group.deliveryDate}{group.deliveryTime ? <span className="text-[var(--color-text-muted)]"> · {group.deliveryTime}</span> : ""}</span>
|
||||||
<div className="space-y-1 md:hidden">
|
: <span className="text-[var(--color-text-muted)]">—</span>
|
||||||
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-2">
|
}
|
||||||
<div className="break-words font-semibold leading-tight !text-[var(--color-text)]">
|
</td>
|
||||||
{group.displayTitle || group.customerName || group.groupKey}
|
<td className="px-4 py-2.5 text-sm hidden lg:table-cell">
|
||||||
</div>
|
{group.ordersCount || 0} {group.ordersCount === 1 ? "заказ" : group.ordersCount < 5 ? "заказа" : "заказов"}
|
||||||
<Badge className="self-start" tone={getOrderGroupStatusTone(group)}>
|
</td>
|
||||||
{getOrderGroupDisplayStatusLabel(group)}
|
<td className="px-4 py-2.5">
|
||||||
</Badge>
|
<Badge tone={getOrderGroupStatusTone(group)}>{getOrderGroupDisplayStatusLabel(group)}</Badge>
|
||||||
</div>
|
</td>
|
||||||
<div className="text-sm leading-5 text-[var(--color-text-muted)]">
|
<td className="px-4 py-2.5 text-sm text-[var(--color-text-muted)] hidden md:table-cell">
|
||||||
{group.customerDate || "—"} · {group.customerPhone || "—"} · {group.ordersCount || 0}{" "}
|
{formatDateTime(group.updatedAt)}
|
||||||
{group.ordersCount === 1 ? "заказ" : group.ordersCount < 5 ? "заказа" : "заказов"}
|
</td>
|
||||||
{group.assignedDriverName ? ` · ${group.assignedDriverName}` : ""}
|
</tr>
|
||||||
</div>
|
|
||||||
{renderOrderNumbersCompact(group)}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</Panel>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue