257 lines
12 KiB
JavaScript
257 lines
12 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 fmtDate = (d) => {
|
||
if (!d) return '';
|
||
const [y, m, day] = d.split('-');
|
||
if (!y || !m || !day) return d;
|
||
return `${day}.${m}.${y}`;
|
||
};
|
||
|
||
const getShipmentIssues = (group) => {
|
||
const data = group?.driverShipmentData;
|
||
if (!Array.isArray(data) || data.length === 0) return null;
|
||
const unshipped = data.filter((i) => !i.shipped);
|
||
if (unshipped.length === 0) return null;
|
||
return unshipped;
|
||
};
|
||
|
||
const buildGroupSummary = (group) => {
|
||
const orderCountLabel = `${group.ordersCount || 0} ${group.ordersCount === 1 ? "заказ" : group.ordersCount < 5 ? "заказа" : "заказов"}`;
|
||
const parts = [orderCountLabel];
|
||
if (group.deliveryDate) {
|
||
const datePart = group.deliveryTime ? `${fmtDate(group.deliveryDate)} · ${group.deliveryTime}` : fmtDate(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 getDeliveryTypeLabel = (deliveryType) => {
|
||
if (deliveryType === "pickup") return "Самовывоз";
|
||
if (deliveryType === "delivery") return "Доставка";
|
||
return "—";
|
||
};
|
||
|
||
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 fs-zone-table">
|
||
<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) => {
|
||
const hasProblem = group.hasDeliveryProblem;
|
||
const baseClass = "w-full rounded-[22px] border text-left transition";
|
||
const selectedClass = selectedOrderGroupId === group.id
|
||
? "border-[var(--color-accent)] bg-[var(--color-accent-soft)]"
|
||
: hasProblem
|
||
? "border-[var(--color-danger)] bg-[rgba(201,61,61,0.1)]"
|
||
: "border-[var(--color-border)] bg-[var(--color-surface-strong)]";
|
||
|
||
const allNumbers = group.allBillNumbers || group.orderNumbers || [];
|
||
const primaryBill = allNumbers[0] || "—";
|
||
const totalCount = allNumbers.length;
|
||
|
||
return (
|
||
<div key={group.id}>
|
||
<div className="mb-1 flex items-center gap-2 px-1 text-xs">
|
||
<span className="font-medium text-[var(--color-text)]">№ {primaryBill}</span>
|
||
{totalCount > 1 && (
|
||
<span className="rounded-full bg-[var(--color-accent-soft)] px-1.5 py-0.5 text-[10px] font-medium text-[var(--color-accent)]">
|
||
{totalCount} сч.
|
||
</span>
|
||
)}
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => onOpenOrder(group.id)}
|
||
className={[baseClass, selectedClass, "flex flex-col items-start justify-start p-4"].join(" ")}
|
||
>
|
||
<div className="min-w-0 w-full font-medium leading-snug break-words">
|
||
{group.displayTitle || group.customerName || group.groupKey}
|
||
</div>
|
||
<div className="mt-1.5 flex flex-wrap items-center gap-1.5">
|
||
<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)]"}`}>
|
||
{group.deliveryType === "pickup" ? "🏪 Самовывоз" : "🚚 Доставка"}
|
||
</span>
|
||
<Badge tone={getOrderGroupStatusTone(group)}>
|
||
{getOrderGroupDisplayStatusLabel(group)}
|
||
</Badge>
|
||
</div>
|
||
|
||
{group.hasDeliveryProblem && (
|
||
<div className="mt-2 rounded-lg border border-[var(--color-warning)] bg-[var(--color-warning-soft)] px-2 py-1 text-xs">
|
||
<span className="font-medium text-[var(--color-warning)]">⚠ Проблема с доставкой</span>
|
||
{group.deliveryProblemNote && (
|
||
<div className="mt-0.5 text-[var(--color-text-muted)]">{group.deliveryProblemNote}</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</button>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
<div className="hidden md:block">
|
||
{!orderGroups.length ? (
|
||
<div className="px-5 py-6 text-sm text-[var(--color-text-muted)]">
|
||
Группы не найдены. Попробуйте изменить поиск или статус.
|
||
</div>
|
||
) : (
|
||
<div className="overflow-x-auto">
|
||
<div className="min-w-[1080px]">
|
||
<div className="grid grid-cols-[minmax(130px,2fr)_minmax(90px,1fr)_minmax(100px,0.8fr)_minmax(100px,1fr)_minmax(100px,1fr)_minmax(100px,0.8fr)_minmax(90px,0.8fr)] gap-0 border-b border-[var(--color-border)] bg-[var(--color-surface-strong)] text-xs uppercase tracking-[0.12em] text-[var(--color-text-muted)]">
|
||
<div className="px-3 py-1.5 font-medium">Группа / Клиент</div>
|
||
<div className="px-3 py-1.5 font-medium">Счета</div>
|
||
<div className="px-3 py-1.5 font-medium">Город</div>
|
||
<div className="px-3 py-1.5 font-medium">Статус</div>
|
||
<div className="px-3 py-1.5 font-medium">Дата доставки</div>
|
||
<div className="px-3 py-1.5 font-medium">Тип</div>
|
||
<div className="px-3 py-1.5 font-medium">Водитель</div>
|
||
</div>
|
||
{orderGroups.map((group) => {
|
||
const hasProblem = group.hasDeliveryProblem;
|
||
const rowClassName = `grid grid-cols-[minmax(130px,2fr)_minmax(90px,1fr)_minmax(100px,0.8fr)_minmax(100px,1fr)_minmax(100px,1fr)_minmax(100px,0.8fr)_minmax(90px,0.8fr)] gap-0 w-full border-t border-[var(--color-border)] text-left transition ${
|
||
hasProblem
|
||
? "bg-[rgba(201,61,61,0.1)] hover:bg-[rgba(201,61,61,0.15)]"
|
||
: "hover:bg-[var(--color-accent-soft)]"
|
||
} ${selectedOrderGroupId === group.id ? "bg-[var(--color-accent-soft)]" : ""}`;
|
||
|
||
const billNumbers = group.allBillNumbers || group.orderNumbers || [];
|
||
const primaryBill = billNumbers[0] || "—";
|
||
const totalBills = billNumbers.length;
|
||
|
||
return (
|
||
<button
|
||
key={group.id}
|
||
type="button"
|
||
className={rowClassName}
|
||
onClick={() => onOpenOrder(group.id)}
|
||
>
|
||
<div className="min-w-0 px-3 py-1.5">
|
||
<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="mt-0.5 text-[11px] text-[var(--color-text-muted)]">
|
||
{group.customerPhone || ""}
|
||
</div>
|
||
</div>
|
||
<div className="px-3 py-1.5">
|
||
<div className="text-xs text-[var(--color-text)]">{primaryBill}</div>
|
||
{totalBills > 1 && (
|
||
<span className="inline-block mt-0.5 rounded-full bg-[var(--color-accent-soft)] px-1.5 py-0.5 text-[10px] font-medium text-[var(--color-accent)]">
|
||
{totalBills} сч.
|
||
</span>
|
||
)}
|
||
</div>
|
||
<div className="px-3 py-1.5 text-xs text-[var(--color-text-muted)]">
|
||
{group.city || "—"}
|
||
</div>
|
||
<div className="px-3 py-1.5">
|
||
<Badge tone={getOrderGroupStatusTone(group)}>
|
||
{getOrderGroupDisplayStatusLabel(group)}
|
||
</Badge>
|
||
</div>
|
||
<div className="px-3 py-1.5 text-xs">
|
||
{group.deliveryDate ? (
|
||
<span>{fmtDate(group.deliveryDate)}{group.deliveryTime ? <span className="text-[var(--color-text-muted)]"> · {group.deliveryTime}</span> : ""}</span>
|
||
) : (
|
||
<span className="text-[var(--color-text-muted)]">—</span>
|
||
)}
|
||
</div>
|
||
<div className="px-3 py-1.5 text-xs">
|
||
<span className="inline-flex items-center gap-1">
|
||
{group.deliveryType === "pickup" ? "🏪" : "🚚"}
|
||
<span className="text-[var(--color-text-muted)]">{group.deliveryType === "pickup" ? "Самовывоз" : "Доставка"}</span>
|
||
</span>
|
||
</div>
|
||
<div className="px-3 py-1.5 text-xs">
|
||
{group.assignedDriverName || <span className="text-[var(--color-text-muted)]">—</span>}
|
||
</div>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</Panel>
|
||
);
|
||
}; |