fix: unify table styles — no rounded rows, wider type col, horizontal scroll, pagination by filtered groups
This commit is contained in:
parent
c0c549adca
commit
581239cc73
|
|
@ -7,7 +7,6 @@ import {
|
||||||
ORDER_GROUP_DISPLAY_STATUS_OPTIONS,
|
ORDER_GROUP_DISPLAY_STATUS_OPTIONS,
|
||||||
} from "../../services/orderGroupViews";
|
} from "../../services/orderGroupViews";
|
||||||
import { Badge } from "../UI/Badge";
|
import { Badge } from "../UI/Badge";
|
||||||
import { Button } from "../UI/Button";
|
|
||||||
import { Panel } from "../UI/Panel";
|
import { Panel } from "../UI/Panel";
|
||||||
import { SkeletonPage } from "../UI/Loading";
|
import { SkeletonPage } from "../UI/Loading";
|
||||||
import { Pagination } from "../UI/Pagination";
|
import { Pagination } from "../UI/Pagination";
|
||||||
|
|
@ -33,12 +32,11 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
|
||||||
[filters, orderGroups],
|
[filters, orderGroups],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Paginate
|
const totalPages = Math.ceil(filteredGroups.length / PAGE_SIZE);
|
||||||
const paginatedGroups = React.useMemo(() => {
|
const paginatedGroups = React.useMemo(() => {
|
||||||
const start = (page - 1) * PAGE_SIZE;
|
const start = (page - 1) * PAGE_SIZE;
|
||||||
return filteredGroups.slice(start, start + PAGE_SIZE);
|
return filteredGroups.slice(start, start + PAGE_SIZE);
|
||||||
}, [filteredGroups, page]);
|
}, [filteredGroups, page]);
|
||||||
const totalPages = Math.ceil(filteredGroups.length / PAGE_SIZE);
|
|
||||||
|
|
||||||
const statusGroups = React.useMemo(() => {
|
const statusGroups = React.useMemo(() => {
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
|
|
@ -71,17 +69,18 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
|
||||||
|
|
||||||
const totalGroups = filteredGroups.length;
|
const totalGroups = filteredGroups.length;
|
||||||
|
|
||||||
const COLS = "grid-cols-[minmax(140px,2fr)_minmax(80px,1fr)_minmax(70px,0.7fr)_minmax(100px,1.2fr)_minmax(80px,1fr)_minmax(100px,1fr)_minmax(120px,1fr)]";
|
// Same column layout as OrdersTable + Тип
|
||||||
|
const COLS = "grid-cols-[minmax(130px,2fr)_minmax(80px,1fr)_minmax(100px,0.8fr)_minmax(100px,1fr)_minmax(100px,1fr)_minmax(90px,0.8fr)_minmax(110px,1fr)]";
|
||||||
|
|
||||||
const TableHeader = () => (
|
const TableHeader = () => (
|
||||||
<div className={`grid ${COLS} gap-0 border-b border-[var(--color-border)] bg-[var(--color-surface-strong)] text-xs uppercase tracking-[0.14em] text-[var(--color-text-muted)]`}>
|
<div className={`grid ${COLS} gap-0 border-b border-[var(--color-border)] bg-[var(--color-surface-strong)] text-[10px] font-semibold uppercase tracking-wider text-[var(--color-text-muted)]`}>
|
||||||
<div className="px-4 py-3 font-medium">Клиент</div>
|
<div className="px-3 py-2.5">Клиент</div>
|
||||||
<div className="px-4 py-3 font-medium">Город</div>
|
<div className="px-3 py-2.5">Город</div>
|
||||||
<div className="px-4 py-3 font-medium">Тип</div>
|
<div className="px-3 py-2.5">Тип</div>
|
||||||
<div className="px-4 py-3 font-medium">Дата доставки</div>
|
<div className="px-3 py-2.5">Дата доставки</div>
|
||||||
<div className="px-4 py-3 font-medium">Водитель</div>
|
<div className="px-3 py-2.5">Водитель</div>
|
||||||
<div className="px-4 py-3 font-medium">Статус</div>
|
<div className="px-3 py-2.5">Статус</div>
|
||||||
<div className="px-4 py-3 font-medium">Обновлён</div>
|
<div className="px-3 py-2.5">Обновлён</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -112,7 +111,7 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
|
||||||
По этому поиску ничего не найдено.
|
По этому поиску ничего не найдено.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-0">
|
||||||
{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);
|
||||||
|
|
@ -124,10 +123,10 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
|
||||||
const isCollapsed = collapsedSections.has(statusValue);
|
const isCollapsed = collapsedSections.has(statusValue);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel key={statusValue} className="overflow-hidden p-0">
|
<div key={statusValue} className="border-b border-[var(--color-border)] last:border-b-0">
|
||||||
<Button
|
{/* Section header — same style as OrdersTable */}
|
||||||
variant="ghost"
|
<button
|
||||||
className="flex w-full items-center justify-between px-5 py-3 text-left"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCollapsedSections((prev) => {
|
setCollapsedSections((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
|
|
@ -139,65 +138,65 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
className="flex w-full items-center gap-3 px-4 py-2.5 text-left transition hover:bg-[var(--color-accent-soft)]"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h3 className="font-semibold">{label}</h3>
|
|
||||||
<Badge tone={groups.length > 0 ? "neutral" : "muted"}>{groups.length}</Badge>
|
|
||||||
</div>
|
|
||||||
<svg
|
<svg
|
||||||
className="h-4 w-4 text-[var(--color-text-muted)] transition-transform"
|
className="h-4 w-4 shrink-0 text-[var(--color-text-muted)] transition-transform"
|
||||||
style={{ transform: isCollapsed ? "rotate(-90deg)" : "rotate(0deg)" }}
|
style={{ transform: isCollapsed ? "rotate(0deg)" : "rotate(90deg)" }}
|
||||||
fill="none"
|
fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={2}
|
|
||||||
>
|
>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
<Badge tone={groups.length > 0 ? "neutral" : "muted"}>{label}</Badge>
|
||||||
|
<span className="text-xs text-[var(--color-text-muted)]">{groups.length}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
|
{/* Horizontal scroll wrapper — min-w so columns don't squish */}
|
||||||
|
<div className="min-w-[960px]">
|
||||||
<TableHeader />
|
<TableHeader />
|
||||||
{groups.map((group) => (
|
{groups.map((group) => (
|
||||||
<Button
|
<button
|
||||||
key={group.id}
|
key={group.id}
|
||||||
variant="ghost"
|
type="button"
|
||||||
className={`grid ${COLS} gap-0 w-full border-t border-[var(--color-border)] text-left`}
|
className={`grid ${COLS} gap-0 w-full border-t border-[var(--color-border)] text-left transition hover:bg-[var(--color-accent-soft)]`}
|
||||||
onClick={() => { if (onSelectSet) onSelectSet(group.id); }}
|
onClick={() => { if (onSelectSet) onSelectSet(group.id); }}
|
||||||
>
|
>
|
||||||
<div className="px-4 py-2.5">
|
<div className="min-w-0 px-3 py-1.5">
|
||||||
<div className="font-medium">{group.displayTitle || group.customerName || group.groupKey}</div>
|
<div className="text-xs font-medium leading-snug break-words" style={{display:"-webkit-box",WebkitLineClamp:2,WebkitBoxOrient:"vertical",overflow:"hidden"}}>{group.displayTitle || group.customerName || group.groupKey}</div>
|
||||||
<div className="text-xs text-[var(--color-text-muted)]">{group.customerPhone || "—"}</div>
|
<div className="mt-0.5 text-[11px] text-[var(--color-text-muted)]">{group.customerPhone || "—"}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 py-2.5 text-sm">
|
<div className="px-3 py-1.5 text-xs text-[var(--color-text-muted)]">
|
||||||
{group.city || group.customerAddress || "—"}
|
{group.city || "—"}
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 py-2.5">
|
<div className="px-3 py-1.5 text-xs">
|
||||||
<span className={`inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-semibold ${group.deliveryType === "pickup" ? "bg-[var(--color-accent-soft)] text-[var(--color-accent)]" : "bg-[var(--color-surface-strong)] text-[var(--color-text-muted)]"}`}>
|
<span className="inline-flex items-center gap-1">
|
||||||
{group.deliveryType === "pickup" ? "🏪 Самовывоз" : "🚚 Доставка"}
|
{group.deliveryType === "pickup" ? "🏪" : "🚚"}
|
||||||
|
<span className="text-[var(--color-text-muted)]">{group.deliveryType === "pickup" ? "Самовывоз" : "Доставка"}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 py-2.5 text-sm">
|
<div className="px-3 py-1.5 text-xs">
|
||||||
{group.deliveryDate
|
{group.deliveryDate
|
||||||
? <span>{formatDate(group.deliveryDate)}{group.deliveryTime ? <span className="text-[var(--color-text-muted)]"> · {group.deliveryTime}</span> : ""}</span>
|
? <span>{formatDate(group.deliveryDate)}{group.deliveryTime ? <span className="text-[var(--color-text-muted)]"> · {group.deliveryTime}</span> : ""}</span>
|
||||||
: <span className="text-[var(--color-text-muted)]">—</span>
|
: <span className="text-[var(--color-text-muted)]">—</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 py-2.5 text-sm">
|
<div className="px-3 py-1.5 text-xs">
|
||||||
{group.assignedDriverName || <span className="text-[var(--color-text-muted)]">—</span>}
|
{group.assignedDriverName || <span className="text-[var(--color-text-muted)]">—</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 py-2.5">
|
<div className="px-3 py-1.5 flex items-center">
|
||||||
<Badge tone={getOrderGroupStatusTone(group)}>{getOrderGroupDisplayStatusLabel(group)}</Badge>
|
<Badge tone={getOrderGroupStatusTone(group)}>{getOrderGroupDisplayStatusLabel(group)}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 py-2.5 text-sm text-[var(--color-text-muted)]">
|
<div className="px-3 py-1.5 text-xs text-[var(--color-text-muted)]">
|
||||||
{formatDateTime(group.updatedAt)}
|
{formatDateTime(group.updatedAt)}
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Panel>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ const StatusSection = ({ statusKey, groups, isOpen, onToggle, selectedOrderGroup
|
||||||
{groups.map((group) => {
|
{groups.map((group) => {
|
||||||
const hasProblem = group.hasDeliveryProblem;
|
const hasProblem = group.hasDeliveryProblem;
|
||||||
const isAgreedRow = isAgreedDelivery(group);
|
const isAgreedRow = isAgreedDelivery(group);
|
||||||
const rowClassName = `grid grid-cols-[minmax(130px,2fr)_minmax(90px,1fr)_minmax(80px,0.8fr)_minmax(80px,1fr)_minmax(100px,1fr)_minmax(70px,0.7fr)_minmax(80px,0.8fr)] gap-0 w-full border-t text-left transition ${
|
const rowClassName = `grid grid-cols-[minmax(130px,2fr)_minmax(90px,1fr)_minmax(80px,0.8fr)_minmax(80px,1fr)_minmax(100px,1fr)_minmax(100px,0.8fr)_minmax(80px,0.8fr)] gap-0 w-full border-t text-left transition ${
|
||||||
isAgreedRow
|
isAgreedRow
|
||||||
? "border-[rgba(18,128,92,0.25)] bg-[rgba(18,128,92,0.07)] hover:bg-[rgba(18,128,92,0.12)]"
|
? "border-[rgba(18,128,92,0.25)] bg-[rgba(18,128,92,0.07)] hover:bg-[rgba(18,128,92,0.12)]"
|
||||||
: hasProblem
|
: hasProblem
|
||||||
|
|
@ -265,14 +265,13 @@ export const OrdersTable = ({
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const PAGE_SIZE = 30;
|
const PAGE_SIZE = 30;
|
||||||
|
|
||||||
// Paginate groups before grouping
|
// Paginate filtered groups (after filter, before grouping)
|
||||||
|
const totalPages = Math.ceil(orderGroups.length / PAGE_SIZE);
|
||||||
const paginatedGroups = useMemo(() => {
|
const paginatedGroups = useMemo(() => {
|
||||||
const start = (page - 1) * PAGE_SIZE;
|
const start = (page - 1) * PAGE_SIZE;
|
||||||
return orderGroups.slice(start, start + PAGE_SIZE);
|
return orderGroups.slice(start, start + PAGE_SIZE);
|
||||||
}, [orderGroups, page]);
|
}, [orderGroups, page]);
|
||||||
|
|
||||||
const totalPages = Math.ceil(orderGroups.length / PAGE_SIZE);
|
|
||||||
|
|
||||||
// Group by status
|
// Group by status
|
||||||
const grouped = useMemo(() => {
|
const grouped = useMemo(() => {
|
||||||
const map = {};
|
const map = {};
|
||||||
|
|
@ -354,7 +353,7 @@ export const OrdersTable = ({
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<div className="min-w-[920px]">
|
<div className="min-w-[1000px]">
|
||||||
{grouped.map(({ key, groups }) => (
|
{grouped.map(({ key, groups }) => (
|
||||||
<StatusSection
|
<StatusSection
|
||||||
key={key}
|
key={key}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue