feat: колонка has_delivery_problem + delivery_problem_note

- saveShipmentData автоматически заполняет has_delivery_problem (boolean) и delivery_problem_note (текст с перечислением неотгруженных позиций и причин)
- OrdersTable: колонка 'Проблема' показывает ⚠ Проблема + детали из delivery_problem_note
- orderGroupViews: warning-тон для групп с hasDeliveryProblem
- n8n/1C: просто читать has_delivery_problem (0/1) и delivery_problem_note
This commit is contained in:
root 2026-06-15 17:32:14 +00:00
parent 187ef90c96
commit b888eba09b
3 changed files with 37 additions and 50 deletions

View File

@ -136,29 +136,17 @@ export const OrdersTable = ({
<div className="mt-3 text-sm text-[var(--color-text-muted)]">{buildGroupSummary(group)}</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-2 text-sm text-[var(--color-text-muted)]">{renderMobileOrderNumbers(group)}</div>
{(() => { {group.hasDeliveryProblem && (
const issues = getShipmentIssues(group); <div className="mt-2 rounded-lg border border-[var(--color-warning)] bg-[var(--color-warning-soft)] px-2 py-1 text-xs">
const data = group?.driverShipmentData; <span className="font-medium text-[var(--color-warning)]"> Проблема с доставкой</span>
if (!Array.isArray(data) || data.length === 0) return null; {group.deliveryProblemNote && (
const shippedCount = data.filter((i) => i.shipped).length; <div className="mt-0.5 text-[var(--color-text-muted)]">{group.deliveryProblemNote}</div>
const total = data.length; )}
if (issues && issues.length > 0) { </div>
return ( )}
<div className="mt-2 rounded-lg border border-[var(--color-warning)] bg-[var(--color-warning-soft)] px-2 py-1 text-xs"> {!group.hasDeliveryProblem && Array.isArray(group?.driverShipmentData) && group.driverShipmentData.length > 0 && (
<span className="font-medium text-[var(--color-warning)]"> Отгрузка: {shippedCount}/{total}</span> <div className="mt-2 text-xs text-[var(--color-accent)]"> Доставка без проблем</div>
<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)]"> <div className="mt-3 text-xs text-[var(--color-text-muted)]">
{formatDateTime(group.updatedAt)} {formatDateTime(group.updatedAt)}
</div> </div>
@ -179,7 +167,7 @@ export const OrdersTable = ({
<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 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> </div>
{orderGroups.map((group) => ( {orderGroups.map((group) => (
@ -213,26 +201,20 @@ export const OrdersTable = ({
)} )}
</div> </div>
<div className="px-5 py-4 text-sm"> <div className="px-5 py-4 text-sm">
{(() => { {group.hasDeliveryProblem ? (
const issues = getShipmentIssues(group); <div>
const data = group?.driverShipmentData; <span className="text-[var(--color-warning)] font-medium"> Проблема</span>
if (!Array.isArray(data) || data.length === 0) { {group.deliveryProblemNote && (
return <span className="text-[var(--color-text-muted)]"></span>; <div className="mt-0.5 text-xs text-[var(--color-text-muted)] line-clamp-2">
} {group.deliveryProblemNote}
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> </div>
); )}
} </div>
return <span className="text-[var(--color-accent)]"> {total}/{total}</span>; ) : Array.isArray(group?.driverShipmentData) && group.driverShipmentData.length > 0 ? (
})()} <span className="text-[var(--color-accent)]"> Ок</span>
) : (
<span className="text-[var(--color-text-muted)]"></span>
)}
</div> </div>
<div className="px-5 py-4 text-sm text-[var(--color-text-muted)]"> <div className="px-5 py-4 text-sm text-[var(--color-text-muted)]">
{formatDateTime(group.updatedAt)} {formatDateTime(group.updatedAt)}

View File

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

View File

@ -221,6 +221,8 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
pickupDate: row.pickup_date || null, pickupDate: row.pickup_date || null,
pickupTimeSlot: row.pickup_time_slot || null, pickupTimeSlot: row.pickup_time_slot || null,
driverShipmentData: row.driver_shipment_data || null, driverShipmentData: row.driver_shipment_data || null,
hasDeliveryProblem: row.has_delivery_problem || false,
deliveryProblemNote: row.delivery_problem_note || null,
syncedTo1cAt: row.synced_to_1c_at || null, syncedTo1cAt: row.synced_to_1c_at || null,
deliveryHalfDay: getOrderGroupDeliveryHalfDay({ deliveryHalfDay: getOrderGroupDeliveryHalfDay({
deliveryHalfDay: rawDeliveryHalfDay, deliveryHalfDay: rawDeliveryHalfDay,
@ -262,7 +264,7 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
}; };
}; };
const ORDER_GROUP_SELECT_FIELDS = `id, group_key, order_numbers, status, delivery_status, sms_sent_at, created_at, updated_at, created_from_exchange_at, source_key, customer_name, customer_phone, customer_phone_normalized, customer_date, orders_total, orders_ready, orders_not_ready, source_orders, order_list, order_list_structured, delivery_invitation_id, delivery_link, notification_status, sms_attempts, first_sms_sent_at, second_sms_sent_at, last_sms_error, next_notification_check_at, delivery_date, delivery_time, delivery_address, customer_address, delivery_date_source, manual_confirmation_at, paid_storage_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name), driver_shipment_data, delivery_type, pickup_date, pickup_time_slot, synced_to_1c_at`; const ORDER_GROUP_SELECT_FIELDS = `id, group_key, order_numbers, status, delivery_status, sms_sent_at, created_at, updated_at, created_from_exchange_at, source_key, customer_name, customer_phone, customer_phone_normalized, customer_date, orders_total, orders_ready, orders_not_ready, source_orders, order_list, order_list_structured, delivery_invitation_id, delivery_link, notification_status, sms_attempts, first_sms_sent_at, second_sms_sent_at, last_sms_error, next_notification_check_at, delivery_date, delivery_time, delivery_address, customer_address, delivery_date_source, manual_confirmation_at, paid_storage_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name), driver_shipment_data, delivery_type, pickup_date, pickup_time_slot, has_delivery_problem, delivery_problem_note, synced_to_1c_at`;
export const updateOrderGroupDeliveryChoice = async ({ export const updateOrderGroupDeliveryChoice = async ({
orderGroupId, orderGroupId,
@ -391,11 +393,18 @@ export const assignDriverToOrderGroup = async ({
export const saveShipmentData = async ({ orderGroupId, shipmentData }) => { export const saveShipmentData = async ({ orderGroupId, shipmentData }) => {
return safeSupabaseCall(async () => { return safeSupabaseCall(async () => {
const client = requireSupabase(); const client = requireSupabase();
const unshipped = shipmentData.filter((i) => !i.shipped);
const hasProblem = unshipped.length > 0;
const problemNote = hasProblem
? unshipped.map((i) => `${i.name}${i.quantity ? ` (${i.quantity}${i.unit ? ` ${i.unit}` : ""})` : ""}${i.comment ? `${i.comment}` : ""}`).join("; ")
: null;
const { error: updateError } = await client const { error: updateError } = await client
.from("order_groups") .from("order_groups")
.update({ .update({
driver_shipment_data: shipmentData, driver_shipment_data: shipmentData,
has_delivery_problem: hasProblem,
delivery_problem_note: problemNote,
updated_at: new Date().toISOString(), updated_at: new Date().toISOString(),
}) })
.eq("id", orderGroupId); .eq("id", orderGroupId);