fix(registry): дата и время самовывоза в реестре групп

Владелец: «в таблице всех доставок и самовывоза, если согласована доставка, дата
и время указаны, а если самовывоз — нет; нужно и самовывоз выводить».

Причина: колонка «Дата» (LogisticsReadinessBoard) читала только delivery_date/
delivery_time, а у самовывоза дата и половина дня лежат в pickup_date/
pickup_time_slot — поэтому строка самовывоза показывала «—», хотя слот согласован.
Тот же пропуск был в фильтре по периоду и группировке по датам: getDeliveryDate()
смотрела только на delivery_date, и при выбранном периоде заказы самовывоза
пропадали из реестра (падали в дату заказа).

Введены общие хелперы в orderGroupViews: isPickupGroup, getGroupSlotDate,
getGroupSlotTime (камелкеис и snake_case). Реестр показывает дату и время слота
по типу заказа, сортировка — по этой же показанной дате, фильтр по периоду и
группировка — тоже по ней. Подпись колонки «Доставка» → «Дата» (в колонке теперь
и дата выдачи).

Попутно поправлено устаревшее ожидание в тесте (getOrderGroupDisplayStatusLabel
для черновика ждало «draft», подпись локализована в «Черновик») — тест падал и до
этой правки.

SW: v121 -> v122.
This commit is contained in:
Hermes 2026-09-28 23:17:39 +02:00
parent 6944386b3b
commit 3b90b79f9f
4 changed files with 93 additions and 7 deletions

View File

@ -1,7 +1,7 @@
const isLocalhost = self.location.hostname === "localhost" || self.location.hostname === "127.0.0.1"; const isLocalhost = self.location.hostname === "localhost" || self.location.hostname === "127.0.0.1";
if (!isLocalhost) { if (!isLocalhost) {
const STATIC_CACHE = "construction-delivery-static-v121"; const STATIC_CACHE = "construction-delivery-static-v122";
const RUNTIME_CACHE = "construction-delivery-runtime-v119"; const RUNTIME_CACHE = "construction-delivery-runtime-v119";
const APP_SHELL_URLS = ["/", "/client", "/index.html", "/manifest.webmanifest", "/icons/icon-192.png", "/icons/icon-512.png"]; const APP_SHELL_URLS = ["/", "/client", "/index.html", "/manifest.webmanifest", "/icons/icon-192.png", "/icons/icon-512.png"];

View File

@ -15,6 +15,8 @@ import {
import { CSS } from "@dnd-kit/utilities"; import { CSS } from "@dnd-kit/utilities";
import { import {
filterOrderGroups, filterOrderGroups,
getGroupSlotDate,
getGroupSlotTime,
getOrderGroupDisplayStatusLabel, getOrderGroupDisplayStatusLabel,
getOrderGroupDisplayStatusValue, getOrderGroupDisplayStatusValue,
getOrderGroupStatusTone, getOrderGroupStatusTone,
@ -109,7 +111,7 @@ const TABLE_COLUMNS = [
{ key: "customer", label: "Клиент" }, { key: "customer", label: "Клиент" },
{ key: "city", label: "Город" }, { key: "city", label: "Город" },
{ key: "deliveryType", label: "Тип" }, { key: "deliveryType", label: "Тип" },
{ key: "deliveryDate", label: "Доставка" }, { key: "deliveryDate", label: "Дата" },
{ key: "driver", label: "Водитель" }, { key: "driver", label: "Водитель" },
{ key: "status", label: "Статус" }, { key: "status", label: "Статус" },
{ key: "updatedAt", label: "Обновлён" }, { key: "updatedAt", label: "Обновлён" },
@ -189,6 +191,9 @@ const renderRow = (g, onSelectSet) => {
const smsTone = getSmsProblemTone(g); const smsTone = getSmsProblemTone(g);
const smsTag = smsTone !== "ok" ? SMS_TAG[smsTone] : null; const smsTag = smsTone !== "ok" ? SMS_TAG[smsTone] : null;
const notAgreedHighlight = getNotAgreedHighlight(g); const notAgreedHighlight = getNotAgreedHighlight(g);
// Дата и время слота: у самовывоза — выдача, у доставки — доставка (владелец: показывать оба типа).
const slotDate = getGroupSlotDate(g);
const slotTime = getGroupSlotTime(g);
return ( return (
<button <button
type="button" type="button"
@ -228,8 +233,8 @@ const renderRow = (g, onSelectSet) => {
</span> </span>
</div> </div>
<div className="px-3 py-2.5 text-sm text-[var(--color-text-muted)]"> <div className="px-3 py-2.5 text-sm text-[var(--color-text-muted)]">
{g.deliveryDate ? fmtDate(g.deliveryDate) : "—"} {slotDate ? fmtDate(slotDate) : "—"}
{g.deliveryTime ? <span className="ml-1 text-xs text-[var(--color-text-muted)]">{g.deliveryTime}</span> : null} {slotTime ? <span className="ml-1 text-xs text-[var(--color-text-muted)]">{slotTime}</span> : null}
</div> </div>
<div className="px-3 py-2.5 text-sm text-[var(--color-text-muted)]">{g.assignedDriverName || "—"}</div> <div className="px-3 py-2.5 text-sm text-[var(--color-text-muted)]">{g.assignedDriverName || "—"}</div>
<div className="px-3 py-2.5"> <div className="px-3 py-2.5">
@ -382,7 +387,7 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], totalGroups: totalGr
case "customer": return text(group.customerName || group.groupKey); case "customer": return text(group.customerName || group.groupKey);
case "city": return text(group.city); case "city": return text(group.city);
case "deliveryType": return text(group.deliveryType === "pickup" ? "Самовывоз" : "Доставка"); case "deliveryType": return text(group.deliveryType === "pickup" ? "Самовывоз" : "Доставка");
case "deliveryDate": return time(group.deliveryDate); case "deliveryDate": return time(getGroupSlotDate(group));
case "driver": return text(group.assignedDriverName); case "driver": return text(group.assignedDriverName);
case "status": return text(getOrderGroupDisplayStatusLabel(group)); case "status": return text(getOrderGroupDisplayStatusLabel(group));
case "updatedAt": return time(group.updatedAt); case "updatedAt": return time(group.updatedAt);

View File

@ -1,6 +1,26 @@
const normalizeDate = (value) => (value ? String(value) : ""); const normalizeDate = (value) => (value ? String(value) : "");
const getDeliveryDate = (group) => normalizeDate(group.deliveryDate || group.customerDate || ""); // Слот заказа: у самовывоза дата и время выдачи лежат в pickup_*, у доставки — в delivery_*.
// Панели и фильтры раньше смотрели только на delivery_*, поэтому у самовывоза колонка даты
// пустовала, а фильтр по периоду выкидывал такие заказы.
export const isPickupGroup = (group) =>
(group?.deliveryType || group?.delivery_type) === "pickup" ||
(group?.deliveryStatus || group?.delivery_status) === "pickup";
export const getGroupSlotDate = (group) =>
normalizeDate(
isPickupGroup(group)
? group?.pickupDate || group?.pickup_date || ""
: group?.deliveryDate || group?.delivery_date || "",
);
export const getGroupSlotTime = (group) =>
(isPickupGroup(group)
? group?.pickupTimeSlot || group?.pickup_time_slot || ""
: group?.deliveryTime || group?.delivery_time || "") || "";
// Дата, по которой группируют и фильтруют реестр: согласованный слот, иначе дата заказа.
const getDeliveryDate = (group) => normalizeDate(getGroupSlotDate(group) || group.customerDate || "");
export const DELIVERY_GROUP_STATUS_LABELS = { export const DELIVERY_GROUP_STATUS_LABELS = {
pending_confirmation: "Ожидает согласования", pending_confirmation: "Ожидает согласования",

View File

@ -2,6 +2,9 @@ import { describe, expect, it } from "vitest";
import { import {
buildOrderGroupBuckets, buildOrderGroupBuckets,
filterOrderGroups, filterOrderGroups,
getGroupSlotDate,
getGroupSlotTime,
isPickupGroup,
getOrderGroupDisplayStatusLabel, getOrderGroupDisplayStatusLabel,
getOrderGroupDisplayStatusValue, getOrderGroupDisplayStatusValue,
getOrderGroupDeliveryStatusLabel, getOrderGroupDeliveryStatusLabel,
@ -104,7 +107,7 @@ describe("orderGroupViews", () => {
expect(getOrderGroupDisplayStatusLabel(groups[0])).toBe("Согласовано"); expect(getOrderGroupDisplayStatusLabel(groups[0])).toBe("Согласовано");
expect(getOrderGroupDisplayStatusValue(groups[0])).toBe("delivery:agreed"); expect(getOrderGroupDisplayStatusValue(groups[0])).toBe("delivery:agreed");
expect(getOrderGroupStatusTone(groups[0])).toBe("accent"); expect(getOrderGroupStatusTone(groups[0])).toBe("accent");
expect(getOrderGroupDisplayStatusLabel(groups[2])).toBe("draft"); expect(getOrderGroupDisplayStatusLabel(groups[2])).toBe("Черновик"); // подпись локализована
}); });
it("filters by the visible card status", () => { it("filters by the visible card status", () => {
@ -115,4 +118,62 @@ describe("orderGroupViews", () => {
expect(agreedGroups.map((group) => group.id)).toEqual(["g-1"]); expect(agreedGroups.map((group) => group.id)).toEqual(["g-1"]);
expect(deliveredGroups.map((group) => group.id)).toEqual(["g-2"]); expect(deliveredGroups.map((group) => group.id)).toEqual(["g-2"]);
}); });
it("берёт дату и время самовывоза из pickup_*, а не из delivery_*", () => {
const pickupGroup = {
id: "p-1",
deliveryType: "pickup",
deliveryStatus: "agreed",
deliveryDate: null,
deliveryTime: null,
pickupDate: "2026-09-30",
pickupTimeSlot: "Вторая половина дня",
customerDate: "28.09.26",
orderNumbers: ["P-1"],
status: "ready_for_notification",
smsSentAt: null,
ordersCount: 1,
readyCount: 1,
notReadyCount: 0,
createdAt: "2026-09-28T07:00:00Z",
updatedAt: "2026-09-28T07:00:00Z",
};
const deliveryGroup = { ...groups[0], deliveryType: "delivery", deliveryDate: "2026-09-30", deliveryTime: "Первая половина дня" };
expect(isPickupGroup(pickupGroup)).toBe(true);
expect(getGroupSlotDate(pickupGroup)).toBe("2026-09-30");
expect(getGroupSlotTime(pickupGroup)).toBe("Вторая половина дня");
expect(getGroupSlotDate(deliveryGroup)).toBe("2026-09-30");
expect(getGroupSlotTime(deliveryGroup)).toBe("Первая половина дня");
// snake_case из сырой строки БД тоже поддержан
expect(getGroupSlotDate({ delivery_type: "pickup", pickup_date: "2026-10-01" })).toBe("2026-10-01");
expect(getGroupSlotTime({ delivery_type: "pickup", pickup_time_slot: "Первая половина дня" })).toBe("Первая половина дня");
});
it("фильтр по периоду ловит самовывоз по дате выдачи", () => {
const pickupGroup = {
id: "p-2",
deliveryType: "pickup",
deliveryStatus: "agreed",
deliveryDate: null,
pickupDate: "2026-09-30",
pickupTimeSlot: "Вторая половина дня",
customerDate: "28.09.26",
orderNumbers: ["P-2"],
status: "ready_for_notification",
smsSentAt: null,
ordersCount: 1,
readyCount: 1,
notReadyCount: 0,
createdAt: "2026-09-28T07:00:00Z",
updatedAt: "2026-09-28T07:00:00Z",
};
expect(filterOrderGroups([pickupGroup], { dateFrom: "2026-09-30", dateTo: "2026-09-30" })).toHaveLength(1);
expect(filterOrderGroups([pickupGroup], { dateFrom: "2026-10-01", dateTo: "2026-10-05" })).toHaveLength(0);
const grouped = groupOrderGroupsByDate([pickupGroup]);
expect(grouped[0].date).toBe("2026-09-30");
});
}); });