192 lines
7.9 KiB
JavaScript
192 lines
7.9 KiB
JavaScript
import { formatDateTime } from "../../utils/formatters";
|
|
import { Badge } from "../UI/Badge";
|
|
import { Panel } from "../UI/Panel";
|
|
import { SkeletonTable } from "../UI/Loading";
|
|
import { OrderFilters } from "./OrderFilters";
|
|
import {
|
|
getOrderGroupDisplayStatusLabel,
|
|
getOrderGroupStatusTone,
|
|
} from "../../services/orderGroupViews";
|
|
|
|
const MAX_VISIBLE_INVOICES = 2;
|
|
|
|
const buildGroupSummary = (group) => {
|
|
const orderCountLabel = `${group.ordersCount || 0} ${group.ordersCount === 1 ? "заказ" : group.ordersCount < 5 ? "заказа" : "заказов"}`;
|
|
const parts = [orderCountLabel];
|
|
if (group.deliveryDate) {
|
|
const datePart = group.deliveryTime ? `${group.deliveryDate} · ${group.deliveryTime}` : group.deliveryDate;
|
|
parts.push(datePart);
|
|
}
|
|
if (group.assignedDriverName) {
|
|
parts.push(group.assignedDriverName);
|
|
}
|
|
|
|
return parts.join(" · ");
|
|
};
|
|
|
|
const renderOrderNumbers = (group) => {
|
|
const numbers = group.allBillNumbers || group.orderNumbers;
|
|
if (!Array.isArray(numbers) || !numbers.length) {
|
|
return "Номера не указаны";
|
|
}
|
|
|
|
if (numbers.length <= MAX_VISIBLE_INVOICES) {
|
|
return numbers.join(", ");
|
|
}
|
|
const visible = numbers.slice(0, MAX_VISIBLE_INVOICES);
|
|
const remaining = numbers.length - MAX_VISIBLE_INVOICES;
|
|
return `${visible.join(", ")} +${remaining}`;
|
|
};
|
|
|
|
const renderMobileOrderNumbers = (group) => {
|
|
const numbers = group.allBillNumbers || group.orderNumbers;
|
|
if (!Array.isArray(numbers) || !numbers.length) {
|
|
return "Номера не указаны";
|
|
}
|
|
|
|
if (numbers.length <= MAX_VISIBLE_INVOICES) {
|
|
return numbers.join(", ");
|
|
}
|
|
const visible = numbers.slice(0, MAX_VISIBLE_INVOICES);
|
|
const remaining = numbers.length - MAX_VISIBLE_INVOICES;
|
|
return (
|
|
<>
|
|
{visible.join(", ")}
|
|
<span className="ml-1 rounded-full bg-[var(--color-accent-soft)] px-1.5 py-0.5 text-xs font-medium text-[var(--color-accent)]">+{remaining}</span>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const OrdersTable = ({
|
|
orderGroups = [],
|
|
selectedOrderGroupId,
|
|
onOpenOrder,
|
|
filters,
|
|
setFilters,
|
|
statusOptions,
|
|
cities = [],
|
|
isLoading = false,
|
|
}) => {
|
|
if (isLoading) {
|
|
return <SkeletonTable rows={5} cols={5} />;
|
|
}
|
|
|
|
return (
|
|
<Panel className="p-0">
|
|
<div className="space-y-4 border-b border-[var(--color-border)] px-5 py-4">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div>
|
|
<h2 className="text-lg font-semibold">Группы доставки</h2>
|
|
<p className="text-sm text-[var(--color-text-muted)]">
|
|
Поиск по группе, клиенту, телефону и дате доставки.
|
|
</p>
|
|
</div>
|
|
<Badge tone="neutral">{orderGroups.length}</Badge>
|
|
</div>
|
|
|
|
{filters && setFilters ? (
|
|
<OrderFilters filters={filters} setFilters={setFilters} statusOptions={statusOptions} cities={cities} />
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="space-y-3 p-4 md:hidden">
|
|
{!orderGroups.length ? (
|
|
<div className="rounded-[28px] border border-dashed border-[var(--color-border)] bg-[var(--color-surface-strong)] p-4 text-sm text-[var(--color-text-muted)]">
|
|
Группы не найдены. Попробуйте изменить поиск или статус.
|
|
</div>
|
|
) : null}
|
|
{orderGroups.map((group) => (
|
|
<button
|
|
key={group.id}
|
|
type="button"
|
|
onClick={() => onOpenOrder(group.id)}
|
|
className={[
|
|
"w-full rounded-[22px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-4 text-left transition",
|
|
selectedOrderGroupId === group.id ? "bg-[var(--color-accent-soft)]" : "",
|
|
].join(" ")}
|
|
>
|
|
<div className="space-y-1">
|
|
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3">
|
|
<div className="min-w-0 truncate font-medium">
|
|
{group.displayTitle || group.customerName || group.groupKey}
|
|
</div>
|
|
<Badge className="self-start" tone={getOrderGroupStatusTone(group)}>
|
|
{getOrderGroupDisplayStatusLabel(group)}
|
|
</Badge>
|
|
</div>
|
|
<div className="text-sm text-[var(--color-text-muted)]">
|
|
{group.displaySubtitle || [group.customerPhone, group.customerDate].filter(Boolean).join(" · ")}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-3 text-sm text-[var(--color-text-muted)]">{buildGroupSummary(group)}</div>
|
|
<div className="mt-2 text-sm text-[var(--color-text-muted)]">{renderMobileOrderNumbers(group)}</div>
|
|
<div className="mt-3 text-xs text-[var(--color-text-muted)]">
|
|
{formatDateTime(group.updatedAt)}
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="hidden overflow-x-auto md:block">
|
|
{!orderGroups.length ? (
|
|
<div className="px-5 py-6 text-sm text-[var(--color-text-muted)]">
|
|
Группы не найдены. Попробуйте изменить поиск или статус.
|
|
</div>
|
|
) : (
|
|
<table className="min-w-full border-collapse">
|
|
<thead className="bg-[var(--color-surface-strong)] text-left text-xs uppercase tracking-[0.16em] text-[var(--color-text-muted)]">
|
|
<tr>
|
|
<th className="px-5 py-4 font-medium">Группа / Клиент</th>
|
|
<th className="px-5 py-4 font-medium">Счёта</th>
|
|
<th className="px-5 py-4 font-medium">Статус</th>
|
|
<th className="px-5 py-4 font-medium">Водитель</th>
|
|
<th className="px-5 py-4 font-medium">Дата доставки</th>
|
|
<th className="px-5 py-4 font-medium">Обновлён</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{orderGroups.map((group) => (
|
|
<tr
|
|
key={group.id}
|
|
className={[
|
|
"cursor-pointer border-t border-[var(--color-border)] transition hover:bg-[var(--color-accent-soft)]",
|
|
selectedOrderGroupId === group.id ? "bg-[var(--color-accent-soft)]" : "",
|
|
].join(" ")}
|
|
onClick={() => onOpenOrder(group.id)}
|
|
>
|
|
<td className="px-5 py-4">
|
|
<div className="font-medium">{group.displayTitle || group.customerName || group.groupKey}</div>
|
|
<div className="mt-1 text-sm text-[var(--color-text-muted)]">
|
|
{[group.customerName, group.customerPhone].filter(Boolean).join(" · ")}
|
|
</div>
|
|
<div className="text-xs text-[var(--color-text-muted)]">{group.groupKey}</div>
|
|
</td>
|
|
<td className="max-w-[260px] px-5 py-4 text-sm text-[var(--color-text-muted)]">
|
|
{renderOrderNumbers(group)}
|
|
</td>
|
|
<td className="px-5 py-4">
|
|
<Badge tone={getOrderGroupStatusTone(group)}>{getOrderGroupDisplayStatusLabel(group)}</Badge>
|
|
</td>
|
|
<td className="px-5 py-4 text-sm">
|
|
{group.assignedDriverName || <span className="text-[var(--color-text-muted)]">—</span>}
|
|
</td>
|
|
<td className="px-5 py-4 text-sm">
|
|
{group.deliveryDate ? (
|
|
<span>{group.deliveryDate}{group.deliveryTime ? <span className="text-[var(--color-text-muted)]"> · {group.deliveryTime}</span> : ""}</span>
|
|
) : (
|
|
<span className="text-[var(--color-text-muted)]">—</span>
|
|
)}
|
|
</td>
|
|
<td className="px-5 py-4 text-sm text-[var(--color-text-muted)]">
|
|
{formatDateTime(group.updatedAt)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</div>
|
|
</Panel>
|
|
);
|
|
}; |