refactor: Phase 1 stabilization — hooks ordering, deduplicate utils, extract shared constants

- P0-1: Move all hooks (useState, useCallback, useMemo) before early return to fix Rules of Hooks violation
- P0-3: Replace 7 inline isPickupOrder checks with single computed variable
- P1: Extract getErrorMessage, normalizeNom to shared utils/deliveryUtils.js
- P1: Replace duplicate STATUS_LABELS with DELIVERY_GROUP_STATUS_LABELS import
- P1: Add requires_address to DELIVERY_GROUP_STATUS_LABELS
- Remove duplicate getErrorMessage from useOrderGroups.js and OrderDetailPanel.jsx
- Remove duplicate normalizeNom from OrderDetailPanel.jsx and orderGroupRepository.js
This commit is contained in:
root 2026-06-12 14:50:56 +00:00
parent 1a665b5165
commit 4dde64ff5a
5 changed files with 72 additions and 49 deletions

View File

@ -50,11 +50,13 @@ import {
getOrderGroupDeliveryStatusLabel,
getOrderGroupDisplayStatusLabel,
getOrderGroupStatusTone,
DELIVERY_GROUP_STATUS_LABELS,
} from "../../services/orderGroupViews";
import { getErrorMessage, normalizeNom } from "../../utils/deliveryUtils";
const DELIVERY_TIME_OPTIONS = ["Первая половина дня", "Вторая половина дня"];
const WEEK_DAY_LABELS = ["ПН", "ВТ", "СР", "ЧТ", "ПТ", "СБ", "ВС"];
const STATUS_LABELS = { pending_confirmation: 'Ожидает согласования', agreed: 'Согласовано', driver_assigned: 'Назначен водитель', loaded: 'Загружено', on_route: 'В пути', delivered: 'Доставлено', pickup: 'Самовывоз', requires_address: 'Требуется адрес', problem: 'Проблема', cancelled: 'Отменено' };
const STATUS_LABELS = DELIVERY_GROUP_STATUS_LABELS;
const ConfirmModal = ({ open, title, message, onConfirm, onCancel }) => {
if (!open) return null;
@ -125,12 +127,6 @@ const renderList = (values) => {
const renderValue = (value) => value || "Нет данных";
const normalizeNom = (nom) => {
if (!nom) return '';
// 1C escapes backslashes: "СФ Т\\ЕА-33584" normalize for comparison
return String(nom).replace(/\\\\/g, '\\').trim();
};
const getAllBillNumbers = (order) => {
const orders = parseOrderList(order);
if (!orders.length) return order.orderNumbers || [];
@ -194,22 +190,6 @@ const parseOrderList = (order) => {
return [];
};
const getErrorMessage = (error, fallbackMessage) => {
if (!error) {
return fallbackMessage;
}
if (error instanceof Error) {
return error.message || fallbackMessage;
}
if (typeof error === "string") {
return error || fallbackMessage;
}
return error?.message || fallbackMessage;
};
const normalizeDeliveryTimeChoice = (value) => {
const normalized = value ? String(value).trim() : "";
const deliveryTime = DELIVERY_TIME_ALIASES[normalized] || normalized;
@ -604,6 +584,10 @@ export const OrderDetailPanel = ({
const [pickupTimeSlot, setPickupTimeSlot] = React.useState(DELIVERY_TIME_OPTIONS[0]);
const [deliveryAddress, setDeliveryAddress] = React.useState(order?.originalDeliveryAddress || order?.deliveryAddress || order?.customerAddress || "");
const [confirmAction, setConfirmAction] = React.useState(null);
const [isEditingDate, setIsEditingDate] = React.useState(false);
const handleShipmentChange = React.useCallback((state) => {
setShipmentState(state);
}, []);
const minSelectableDateKey = React.useMemo(() => getNextSelectableDateKey(), []);
const [currentMonth, setCurrentMonth] = React.useState(() => {
const existingDeliveryDate = fromDateKey(order?.deliveryDate);
@ -655,7 +639,6 @@ export const OrderDetailPanel = ({
const isDeliveryAgreed = ["agreed", "driver_assigned", "loaded", "on_route", "delivered"].includes(order.deliveryStatus || order.delivery_status);
const isPickupOrder = order.deliveryType === "pickup" || order.deliveryStatus === "pickup" || order.delivery_status === "pickup";
const [isEditingDate, setIsEditingDate] = React.useState(false);
// Show "agreed" banner only when selected tab matches the already-agreed type
const agreedTypeMatchesTab = isDeliveryAgreed && !isEditingDate && (
(deliveryType === "pickup" && isPickupOrder)
@ -667,10 +650,6 @@ export const OrderDetailPanel = ({
order.deliveryTime || order.deliveryHalfDay,
].filter((value) => value && value !== "Нет данных").join(" · ");
const handleShipmentChange = React.useCallback((state) => {
setShipmentState(state);
}, []);
const handleSaveDeliveryChoice = async () => {
const effectiveDate = deliveryType === "pickup" ? pickupDate : deliveryDate;
const effectiveTime = deliveryType === "pickup" ? pickupTimeSlot : deliveryTime;
@ -735,7 +714,7 @@ 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") ? "Карточка группы самовывоза" : "Карточка группы доставки"}
{isPickupOrder ? "Карточка группы самовывоза" : "Карточка группы доставки"}
</p>
<h2 className="mt-2 text-2xl font-semibold">
{order.displayTitle || order.customerName || order.groupKey}
@ -754,7 +733,7 @@ export const OrderDetailPanel = ({
</div>
{(() => {
const isPickup = order.deliveryType === "pickup" || order.deliveryStatus === "pickup" || order.delivery_status === "pickup";
const isPickup = isPickupOrder;
const deliveryTypeLabel = isPickup
? "Самовывоз"
: (order.deliveryStatus === "requires_address" || order.delivery_status === "requires_address")
@ -882,7 +861,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)]">{(order.deliveryType === "pickup" || order.deliveryStatus === "pickup" || order.delivery_status === "pickup") ? "Статус самовывоза" : "Статус доставки"}</p>
<p className="text-xs text-[var(--color-text-muted)]">{isPickupOrder ? "Статус самовывоза" : "Статус доставки"}</p>
<p className="font-medium !text-[var(--color-text)]">{getOrderGroupDeliveryStatusLabel(order.deliveryStatus || order.delivery_status)}</p>
</div>
</div>
@ -1173,7 +1152,7 @@ export const OrderDetailPanel = ({
) : null}
{canManageDelivery && ["manager", "logistician", "admin", "mega_admin"].includes(userRole) && (order.deliveryType !== "pickup" && order.deliveryStatus !== "pickup" && order.delivery_status !== "pickup") ? (
{canManageDelivery && ["manager", "logistician", "admin", "mega_admin"].includes(userRole) && !isPickupOrder ? (
<Panel className="space-y-4 p-5">
<div>
<strong>Назначение водителя</strong>

View File

@ -7,22 +7,7 @@ import {
ORDER_GROUP_DISPLAY_STATUS_OPTIONS,
getOrderGroupDisplayStatusValue,
} from "../services/orderGroupViews";
const getErrorMessage = (error, fallbackMessage) => {
if (!error) {
return fallbackMessage;
}
if (error instanceof Error) {
return error.message || fallbackMessage;
}
if (typeof error === "string") {
return error || fallbackMessage;
}
return error?.message || fallbackMessage;
};
import { getErrorMessage } from "../utils/deliveryUtils";
export const useOrderGroups = () => {
const [orderGroups, setOrderGroups] = React.useState(() => []);

View File

@ -5,6 +5,7 @@ const getDeliveryDate = (group) => normalizeDate(group.deliveryDate || group.cus
export const DELIVERY_GROUP_STATUS_LABELS = {
pending_confirmation: "Ожидает согласования",
manual_confirmation_required: "Взят в ручное управление",
requires_address: "Требуется адрес",
agreed: "Согласовано",
driver_assigned: "Назначен водитель",
loaded: "Загружено",

View File

@ -8,6 +8,7 @@ import {
getOrderGroupDeliveryStatusLabel,
getOrderGroupStatusLabel,
} from "../orderGroupViews";
import { normalizeNom } from "../../utils/deliveryUtils";
const requireSupabase = () => {
if (!hasSupabaseConfig || !supabase) {
@ -68,7 +69,6 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
if (!Array.isArray(srcOrders) || !srcOrders.length) return orderNumbers;
const seen = new Set();
const result = [];
const normalizeNom = (nom) => String(nom || '').replace(/\\\\/g, '\\').trim();
for (const src of srcOrders) {
if (src && Array.isArray(src.orderList)) {
for (const ol of src.orderList) {

View File

@ -0,0 +1,58 @@
/**
* Shared delivery/delivery-group utilities.
* Single source of truth for isPickupOrder, getErrorMessage, normalizeNom, and STATUS_LABELS.
*/
// ── Pickup detection ──────────────────────────────────────────────────────
/**
* Returns true if the order group is a pickup (self-pickup) order.
* Uses canonical deliveryStatus field (mapper normalizes both delivery_status variants).
*/
export const isPickupOrder = (order) =>
order?.deliveryType === "pickup" ||
order?.deliveryStatus === "pickup" ||
order?.delivery_status === "pickup";
// ── Error messages ────────────────────────────────────────────────────────
export const getErrorMessage = (error, fallbackMessage) => {
if (!error) {
return fallbackMessage;
}
if (error instanceof Error) {
return error.message || fallbackMessage;
}
if (typeof error === "string") {
return error || fallbackMessage;
}
return error?.message || fallbackMessage;
};
// ── Nom normalisation ──────────────────────────────────────────────────────
/**
* 1C escapes backslashes: "СФ Т\\ЕА-33584" normalise for comparison.
*/
export const normalizeNom = (nom) => {
if (!nom) return "";
return String(nom).replace(/\\\\/g, "\\").trim();
};
// ── Status labels ─────────────────────────────────────────────────────────
export const DELIVERY_STATUS_LABELS = {
pending_confirmation: "Ожидает подтверждения",
requires_address: "Требуется адрес",
agreed: "Согласовано",
driver_assigned: "Водитель назначен",
loaded: "Загружен",
on_route: "В пути",
delivered: "Доставлено",
problem: "Проблема",
cancelled: "Отменено",
pickup: "Самовывоз",
};
export const getOrderGroupDeliveryStatusLabel = (status) =>
DELIVERY_STATUS_LABELS[status] || status || "Ожидает подтверждения";