fix: pickup display — detect from source_orders.ship, show correct labels, hide placeholder address

- orderGroupRepository: detect pickup from source_orders.ship='САМОВЫВОЗ' and address='САМОВЫВОЗ'
- orderGroupRepository: set effectiveDeliveryType='pickup' when source data indicates pickup even if DB says 'delivery'
- orderGroupRepository: clear deliveryAddress when it's just 'САМОВЫВОЗ' placeholder
- OrderDetailPanel: dynamic header 'Карточка группы самовывоза' vs 'Карточка группы доставки'
- OrderDetailPanel: subtitle now includes orderNumbers for visibility
- OrderDetailPanel: label changed from 'Номер счёта' to 'Заказ' with '+N сч.' for sub-bills
- GroupDetailPage: neutral 'Группа не найдена' instead of 'Группа доставки не найдена'
- Added pickup-specific test case
This commit is contained in:
root 2026-06-12 08:14:20 +00:00
parent ec9b28fa6f
commit 9aef4d49c0
4 changed files with 137 additions and 28 deletions

View File

@ -642,7 +642,7 @@ export const OrderDetailPanel = ({
});
if (result?.success) {
setFormMessage("Доставка согласована вручную.");
setFormMessage(deliveryType === "pickup" ? "Самовывоз согласован вручную." : "Доставка согласована вручную.");
return;
}
@ -682,36 +682,57 @@ export const OrderDetailPanel = ({
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<p className="text-sm uppercase tracking-[0.2em] text-[var(--color-text-muted)]">
Карточка группы доставки
{(order.deliveryType === "pickup" || order.deliveryStatus === "pickup" || order.delivery_status === "pickup") ? "Карточка группы самовывоза" : "Карточка группы доставки"}
</p>
<h2 className="mt-2 text-2xl font-semibold">
{order.displayTitle || order.customerName || order.groupKey}
</h2>
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
{order.displaySubtitle || [order.customerPhone, order.customerDate].filter(Boolean).join(" · ") || "Не указано"}
{(() => {
const parts = [];
if (order.orderNumbers && order.orderNumbers.length > 0) parts.push(order.orderNumbers.join(", "));
const sub = order.displaySubtitle || [order.customerPhone, order.customerDate].filter(Boolean).join(" · ");
if (sub) parts.push(sub);
return parts.join(" · ") || "Не указано";
})()}
</p>
</div>
<Badge tone={getOrderGroupStatusTone(order)}>{getOrderGroupDisplayStatusLabel(order)}</Badge>
</div>
{(() => {
const isPickup = order.deliveryType === "pickup" || order.deliveryStatus === "pickup" || order.delivery_status === "pickup";
const deliveryTypeLabel = isPickup
? "Самовывоз"
: (order.deliveryStatus === "requires_address" || order.delivery_status === "requires_address")
? "Доставка (требуется адрес)"
: "Доставка";
const dateLabel = isPickup ? "Дата самовывоза" : "Дата доставки";
const timeLabel = isPickup ? "Время самовывоза" : "Время доставки";
const addressLabel = isPickup ? "Адрес самовывоза" : "Адрес доставки";
// For pickup orders, hide the delivery address if it's just a placeholder like "Самовывоз"
const effectiveAddress = isPickup
? (order.deliveryAddress && order.deliveryAddress !== "Самовывоз" && order.deliveryAddress !== "самовывоз" ? order.deliveryAddress : "")
: order.deliveryAddress;
return (
<div className="grid gap-3 rounded-[24px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-4 md:grid-cols-4">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
Дата доставки
{dateLabel}
</p>
<p className="mt-1 text-base font-medium !text-[var(--color-text)]">{formatDeliveryDateDisplay(order.deliveryDate)}</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
Время доставки
{timeLabel}
</p>
<p className="mt-1 text-base font-medium !text-[var(--color-text)]">{renderValue(order.deliveryTime || order.deliveryHalfDay)}</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
Тип доставки
Тип
</p>
<p className="mt-1 text-base font-medium !text-[var(--color-text)]">{order.deliveryType === "pickup" ? "Самовывоз" : order.deliveryStatus === "requires_address" || order.delivery_status === "requires_address" ? "Доставка (требуется адрес)" : "Доставка"}</p>
<p className="mt-1 text-base font-medium !text-[var(--color-text)]">{deliveryTypeLabel}</p>
{(order.deliveryStatus === "requires_address" || order.delivery_status === "requires_address") && (
<div className="mt-2 flex items-start gap-2 rounded-xl border border-[rgba(239,68,68,0.3)] bg-[rgba(239,68,68,0.08)] p-3">
<span className="text-lg">📍</span>
@ -726,7 +747,7 @@ export const OrderDetailPanel = ({
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
Водитель
</p>
<p className="mt-1 text-base font-medium !text-[var(--color-text)]">{order.assignedDriverId ? renderValue(order.assignedDriverName) : "Не назначен"}</p>
<p className="mt-1 text-base font-medium !text-[var(--color-text)]">{order.assignedDriverId ? renderValue(order.assignedDriverName) : (isPickup ? "Не нужен" : "Не назначен")}</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
@ -739,18 +760,38 @@ export const OrderDetailPanel = ({
{renderValue(order.customerPhone)}
</a>
</div>
<div className="">
{!isPickup || effectiveAddress ? (
<div>
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
Адрес доставки
{addressLabel}
</p>
<p className="mt-1 text-base font-medium !text-[var(--color-text)]">{renderValue(order.deliveryAddress)}</p>
<p className="mt-1 text-base font-medium !text-[var(--color-text)]">{renderValue(effectiveAddress)}</p>
</div>
) : null}
</div>
);
})()}
<div className="grid gap-x-4 gap-y-2 grid-cols-2 md:grid-cols-4">
<div>
<p className="text-xs text-[var(--color-text-muted)]">Номер счёта</p>
<p className="font-medium !text-[var(--color-text)]">{renderValue(order.orderNumberSummary)}</p>
<p className="text-xs text-[var(--color-text-muted)]">Заказ</p>
<p className="font-medium !text-[var(--color-text)]">{(() => {
const mainNumbers = order.orderNumbers || [];
const allNumbers = order.allBillNumbers || [];
const mainSet = new Set(mainNumbers.map(String));
const extraNumbers = allNumbers.filter((n) => !mainSet.has(String(n)));
if (mainNumbers.length > 0) {
return (
<span>
<span className="font-bold">{mainNumbers.join(", ")}</span>
{extraNumbers.length > 0 && (
<span className="text-[var(--color-text-muted)]"> +{extraNumbers.length} сч.</span>
)}
</span>
);
}
return renderValue(order.orderNumberSummary);
})()}</p>
</div>
<div>
<p className="text-xs text-[var(--color-text-muted)]">Клиент</p>
@ -777,7 +818,7 @@ export const OrderDetailPanel = ({
<p className="font-medium !text-[var(--color-text)]">{formatDateTime(order.updatedAt)}</p>
</div>
<div>
<p className="text-xs text-[var(--color-text-muted)]">Статус доставки</p>
<p className="text-xs text-[var(--color-text-muted)]">{(order.deliveryType === "pickup" || order.deliveryStatus === "pickup" || order.delivery_status === "pickup") ? "Статус самовывоза" : "Статус доставки"}</p>
<p className="font-medium !text-[var(--color-text)]">{getOrderGroupDeliveryStatusLabel(order.deliveryStatus || order.delivery_status)}</p>
</div>
</div>
@ -786,11 +827,11 @@ export const OrderDetailPanel = ({
{canManageDelivery ? (
<Panel className="space-y-4 p-5">
<div>
<strong>Ручное согласование доставки</strong>
<strong>Ручное согласование</strong>
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
{isDeliveryAgreed
? "Дата и половина дня доставки уже зафиксированы."
: "Если клиент согласовал доставку по телефону, сохраните дату и половину дня здесь."}
? "Дата и время уже зафиксированы."
: "Если клиент согласовал доставку или самовывоз по телефону, сохраните дату и время здесь."}
</p>
</div>
{/* Delivery type tabs */}
@ -831,7 +872,7 @@ export const OrderDetailPanel = ({
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-accent)]">
Доставка согласована
{(order.deliveryType === "pickup" || order.deliveryStatus === "pickup" || order.delivery_status === "pickup") ? "Самовывоз согласован" : "Доставка согласована"}
</p>
<p className="mt-1 text-lg font-semibold">
{agreedDeliveryLabel || "Дата и время сохранены"}
@ -847,7 +888,7 @@ export const OrderDetailPanel = ({
disabled={isSavingDeliveryChoice}
className="text-sm"
>
Изменить дату доставки
Изменить {(order.deliveryType === "pickup" || order.deliveryStatus === "pickup" || order.delivery_status === "pickup") ? "дату самовывоза" : "дату доставки"}
</Button>
) : null}
</div>
@ -1292,7 +1333,39 @@ export const OrderDetailPanel = ({
<Panel className="space-y-4 p-5">
<strong>Счета</strong>
{renderList(getAllBillNumbers(order))}
{(() => {
const mainNumbers = order.orderNumbers || [];
const allNumbers = getAllBillNumbers(order);
const mainSet = new Set(mainNumbers.map(String));
const extraNumbers = allNumbers.filter((n) => !mainSet.has(String(n)));
return (
<div className="space-y-2">
{mainNumbers.length > 0 && (
<div>
<p className="text-xs text-[var(--color-text-muted)]">Основной счёт</p>
<div className="flex flex-wrap gap-2">
{mainNumbers.map((num, idx) => (
<span key={idx} className="rounded-full bg-[var(--color-accent-soft)] px-3 py-1 text-sm font-semibold text-[var(--color-accent)]">{num}</span>
))}
</div>
</div>
)}
{extraNumbers.length > 0 && (
<div>
<p className="text-xs text-[var(--color-text-muted)]">{mainNumbers.length > 0 ? "Составные заказы" : "Все счета"}</p>
<div className="flex flex-wrap gap-2">
{extraNumbers.map((num, idx) => (
<span key={idx} className="rounded-full bg-[var(--color-surface)] px-3 py-1 text-xs text-[var(--color-text-muted)]">{num}</span>
))}
</div>
</div>
)}
{mainNumbers.length === 0 && extraNumbers.length === 0 && (
<p className="text-sm text-[var(--color-text-muted)]">Нет данных</p>
)}
</div>
);
})()}
</Panel>
<Panel className="space-y-4 p-5">

View File

@ -40,7 +40,7 @@ describe("OrderDetailPanel", () => {
<OrderDetailPanel order={order} />,
);
expect(markup).toContain("Карточка группы доставки");
expect(markup).toContain("Карточка группы");
expect(markup).toContain("Мария Волкова");
expect(markup).toContain("Адрес доставки");
expect(markup).toContain("Симферополь, ул. Ленина, 10");
@ -109,4 +109,22 @@ describe("OrderDetailPanel", () => {
it("skips weekends when selecting the default manual delivery date", () => {
expect(getNextSelectableDateKey(new Date("2026-05-15T12:00:00Z"))).toBe("2026-05-18");
});
it("shows pickup labels for pickup orders", () => {
const markup = renderToStaticMarkup(
<OrderDetailPanel
order={{
...order,
deliveryType: "pickup",
deliveryStatus: "pickup",
deliveryAddress: "",
}}
/>,
);
expect(markup).toContain("Карточка группы самовывоза");
expect(markup).toContain("Самовывоз");
expect(markup).toContain("Дата самовывоза");
expect(markup).toContain("Статус самовывоза");
expect(markup).not.toContain("Адрес доставки");
});
});

View File

@ -109,7 +109,7 @@ export const GroupDetailPage = () => {
/>
) : (
<Panel className="p-6 text-sm text-[var(--color-text-muted)]">
Группа доставки не найдена.
Группа не найдена.
</Panel>
)}
</div>

View File

@ -110,6 +110,24 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
};
const deliveryAddress = normalizeText(row.delivery_address) || extractAddressFromSourceOrders(row.source_orders);
// Detect pickup from source_orders ship field (1C sends "САМОВЫВОЗ")
const isPickupFromSource = Array.isArray(row.source_orders) && row.source_orders.length > 0
&& normalizeText(row.source_orders[0].ship || "").toUpperCase() === "САМОВЫВОЗ";
// Also treat address equal to "САМОВЫВОЗ" as pickup indicator
const isPickupAddress = deliveryAddress.toUpperCase() === "САМОВЫВОЗ";
// Resolve effective delivery type: DB field takes precedence, but if it says "delivery"
// while source data clearly indicates pickup, treat as pickup
const effectiveDeliveryType = (row.delivery_type === "pickup" || deliveryStatus === "pickup" || isPickupFromSource || isPickupAddress)
? "pickup"
: (row.delivery_type || "delivery");
// Clear placeholder pickup address
const resolvedDeliveryAddress = (effectiveDeliveryType === "pickup" && (deliveryAddress.toUpperCase() === "САМОВЫВОЗ" || !deliveryAddress))
? ""
: deliveryAddress;
const customerAddress = normalizeText(row.customer_address) || "";
const extractCity = (addr) => {
@ -151,7 +169,7 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
customerPhone,
customerPhoneNormalized: parsedKey.phone || normalizePhone(customerPhone),
customerDate,
deliveryAddress,
deliveryAddress: resolvedDeliveryAddress,
customerAddress,
city,
assignedDriverId: row.assigned_driver_id || null,
@ -189,7 +207,7 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
deliveryDate,
deliveryTime,
deliveryDateSource: row.delivery_date_source || null,
deliveryType: row.delivery_type || "delivery",
deliveryType: effectiveDeliveryType,
pickupDate: row.pickup_date || null,
pickupTimeSlot: row.pickup_time_slot || null,
driverShipmentData: row.driver_shipment_data || null,
@ -205,7 +223,7 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
customerName,
customerPhone,
customerDate,
deliveryAddress,
resolvedDeliveryAddress,
customerAddress,
city,
rawDeliveryHalfDay,