feat: колонка Отгрузка в таблице + подсветка проблем

- Новая колонка 'Отгрузка' в таблице групп доставки (десктоп и мобайл)
- Показывает ✓ N/N для полностью отгруженных, ⚠ X/N для частичной отгрузки
- В мобильных карточках: блок с перечислением проблемных товаров и причин
- Подсветка warning-тоном для групп с неотгруженными позициями
- Восстановление состояния чекбоксов из driverShipmentData
This commit is contained in:
root 2026-06-15 15:27:50 +00:00
parent a687d0b8a8
commit 3847625005
2 changed files with 63 additions and 2 deletions

View File

@ -17,6 +17,14 @@ const fmtDate = (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];
@ -128,6 +136,29 @@ export const OrdersTable = ({
<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>
{(() => {
const issues = getShipmentIssues(group);
const data = group?.driverShipmentData;
if (!Array.isArray(data) || data.length === 0) return null;
const shippedCount = data.filter((i) => i.shipped).length;
const total = data.length;
if (issues && issues.length > 0) {
return (
<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)]"> Отгрузка: {shippedCount}/{total}</span>
<span className="text-[var(--color-text-muted)]"> {issues.slice(0, 2).map((i) => i.name).join(", ")}{issues.length > 2 ? ` +${issues.length - 2}` : ""}</span>
{issues.some((i) => i.comment) && (
<div className="mt-0.5 text-[var(--color-text-muted)]">{issues.filter((i) => i.comment).slice(0, 1).map((i) => i.comment)}</div>
)}
</div>
);
}
return (
<div className="mt-2 text-xs text-[var(--color-accent)]">
Отгружено: {total}/{total}
</div>
);
})()}
<div className="mt-3 text-xs text-[var(--color-text-muted)]">
{formatDateTime(group.updatedAt)}
</div>
@ -142,19 +173,20 @@ export const OrdersTable = ({
</div>
) : (
<div>
<div className="grid grid-cols-[minmax(180px,2.5fr)_minmax(100px,1.2fr)_minmax(100px,1fr)_minmax(80px,1fr)_minmax(120px,1.2fr)_minmax(130px,1.2fr)] gap-0 border-b border-[var(--color-border)] bg-[var(--color-surface-strong)] text-xs uppercase tracking-[0.16em] text-[var(--color-text-muted)]">
<div className="grid grid-cols-[minmax(180px,2.5fr)_minmax(100px,1.2fr)_minmax(100px,1fr)_minmax(80px,1fr)_minmax(120px,1.2fr)_minmax(80px,0.8fr)_minmax(130px,1.2fr)] gap-0 border-b border-[var(--color-border)] bg-[var(--color-surface-strong)] text-xs uppercase tracking-[0.16em] text-[var(--color-text-muted)]">
<div className="px-5 py-4 font-medium">Группа / Клиент</div>
<div className="px-5 py-4 font-medium">Счёта</div>
<div className="px-5 py-4 font-medium">Статус</div>
<div className="px-5 py-4 font-medium">Водитель</div>
<div className="px-5 py-4 font-medium">Дата доставки</div>
<div className="px-5 py-4 font-medium">Отгрузка</div>
<div className="px-5 py-4 font-medium">Обновлён</div>
</div>
{orderGroups.map((group) => (
<button
key={group.id}
type="button"
className={`grid grid-cols-[minmax(180px,2.5fr)_minmax(100px,1.2fr)_minmax(100px,1fr)_minmax(80px,1fr)_minmax(120px,1.2fr)_minmax(130px,1.2fr)] gap-0 w-full border-t border-[var(--color-border)] text-left transition hover:bg-[var(--color-accent-soft)] ${selectedOrderGroupId === group.id ? "bg-[var(--color-accent-soft)]" : ""}`}
className={`grid grid-cols-[minmax(180px,2.5fr)_minmax(100px,1.2fr)_minmax(100px,1fr)_minmax(80px,1fr)_minmax(120px,1.2fr)_minmax(80px,0.8fr)_minmax(130px,1.2fr)] gap-0 w-full border-t border-[var(--color-border)] text-left transition hover:bg-[var(--color-accent-soft)] ${selectedOrderGroupId === group.id ? "bg-[var(--color-accent-soft)]" : ""}`}
onClick={() => onOpenOrder(group.id)}
>
<div className="px-5 py-4">
@ -180,6 +212,28 @@ export const OrdersTable = ({
<span className="text-[var(--color-text-muted)]"></span>
)}
</div>
<div className="px-5 py-4 text-sm">
{(() => {
const issues = getShipmentIssues(group);
const data = group?.driverShipmentData;
if (!Array.isArray(data) || data.length === 0) {
return <span className="text-[var(--color-text-muted)]"></span>;
}
const shippedCount = data.filter((i) => i.shipped).length;
const total = data.length;
if (issues && issues.length > 0) {
return (
<div>
<span className="text-[var(--color-warning)] font-medium"> {shippedCount}/{total}</span>
<div className="mt-0.5 text-xs text-[var(--color-text-muted)]">
{issues.slice(0, 2).map((i) => i.name).join(", ")}{issues.length > 2 ? ` +${issues.length - 2}` : ""}
</div>
</div>
);
}
return <span className="text-[var(--color-accent)]"> {total}/{total}</span>;
})()}
</div>
<div className="px-5 py-4 text-sm text-[var(--color-text-muted)]">
{formatDateTime(group.updatedAt)}
</div>

View File

@ -461,6 +461,13 @@ export const buildOrderGroupBuckets = (groups) => {
export const getOrderGroupStatusTone = (group) => {
const deliveryStatus = group?.deliveryStatus || group?.delivery_status;
// Highlight groups with shipment issues (partial delivery)
const shipmentData = group?.driverShipmentData;
if (Array.isArray(shipmentData) && shipmentData.length > 0) {
const hasIssues = shipmentData.some((i) => !i.shipped);
if (hasIssues) return "warning";
}
if (deliveryStatus && deliveryStatus !== "pending_confirmation") {
return getOrderGroupDeliveryStatusTone(deliveryStatus);
}