fix: 3 бага доставки — самовывоз/водитель/отгрузка
Bug 1: deliveryType='delivery' перезаписывался на 'pickup' маппером из-за адреса/источника. Теперь если БД явно содержит delivery_type='delivery', маппер это уважает. Bug 2: водитель не мог сохранить частичную отгрузку — добавлена кнопка 'Сохранить отгрузку' в DriverShipmentPanel + saveShipmentData() в репозитории. Bug 3: при смене типа на самовывоз assigned_driver_id не очищался — добавлено assigned_driver_id: null в updatePayload.
This commit is contained in:
parent
efa2a83634
commit
ae1e0a2e0e
|
|
@ -89,7 +89,7 @@ const parseOrderItems = (order) => {
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DriverShipmentPanel = ({ order, onShipmentChange }) => {
|
export const DriverShipmentPanel = ({ order, onShipmentChange, onSaveShipment, isSavingShipment }) => {
|
||||||
const [stopWords, setStopWords] = React.useState([]);
|
const [stopWords, setStopWords] = React.useState([]);
|
||||||
const [scopeActive, setScopeActive] = React.useState(true);
|
const [scopeActive, setScopeActive] = React.useState(true);
|
||||||
|
|
||||||
|
|
@ -261,6 +261,29 @@ export const DriverShipmentPanel = ({ order, onShipmentChange }) => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{onSaveShipment && items.length > 0 && (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Button
|
||||||
|
onClick={() => onSaveShipment(items.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
quantity: item.quantity,
|
||||||
|
unit: item.unit,
|
||||||
|
shipped: shippedItems.has(item.id),
|
||||||
|
comment: shippedItems.has(item.id) ? "" : (comments[item.id] || ""),
|
||||||
|
})))}
|
||||||
|
disabled={isSavingShipment || shippedCount === 0}
|
||||||
|
>
|
||||||
|
{isSavingShipment ? "Сохраняем..." : "Сохранить отгрузку"}
|
||||||
|
</Button>
|
||||||
|
{shippedCount > 0 && shippedCount < items.length && (
|
||||||
|
<span className="text-xs text-[var(--color-text-muted)]">
|
||||||
|
Частичная отгрузка: {shippedCount}/{items.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -544,7 +544,8 @@ export const OrderDetailPanel = ({
|
||||||
drivers = [],
|
drivers = [],
|
||||||
onAssignDriver,
|
onAssignDriver,
|
||||||
onChangeDeliveryStatus,
|
onChangeDeliveryStatus,
|
||||||
userRole,
|
onSaveShipmentData,
|
||||||
|
userRole = "driver",
|
||||||
}) => {
|
}) => {
|
||||||
const [problemReason, setProblemReason] = React.useState(null);
|
const [problemReason, setProblemReason] = React.useState(null);
|
||||||
const [pendingStatus, setPendingStatus] = React.useState(null);
|
const [pendingStatus, setPendingStatus] = React.useState(null);
|
||||||
|
|
@ -552,6 +553,7 @@ export const OrderDetailPanel = ({
|
||||||
const [deliveryTime, setDeliveryTime] = React.useState(DELIVERY_TIME_OPTIONS[0]);
|
const [deliveryTime, setDeliveryTime] = React.useState(DELIVERY_TIME_OPTIONS[0]);
|
||||||
const [formMessage, setFormMessage] = React.useState("");
|
const [formMessage, setFormMessage] = React.useState("");
|
||||||
const [shipmentState, setShipmentState] = React.useState(null);
|
const [shipmentState, setShipmentState] = React.useState(null);
|
||||||
|
const [isSavingShipment, setIsSavingShipment] = React.useState(false);
|
||||||
const [isCalendarOpen, setIsCalendarOpen] = React.useState(false);
|
const [isCalendarOpen, setIsCalendarOpen] = React.useState(false);
|
||||||
const [driverMessage, setDriverMessage] = React.useState("");
|
const [driverMessage, setDriverMessage] = React.useState("");
|
||||||
const [selectedDriverId, setSelectedDriverId] = React.useState(order?.assignedDriverId || "");
|
const [selectedDriverId, setSelectedDriverId] = React.useState(order?.assignedDriverId || "");
|
||||||
|
|
@ -564,6 +566,26 @@ export const OrderDetailPanel = ({
|
||||||
const handleShipmentChange = React.useCallback((state) => {
|
const handleShipmentChange = React.useCallback((state) => {
|
||||||
setShipmentState(state);
|
setShipmentState(state);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleSaveShipment = React.useCallback(async (shipmentData) => {
|
||||||
|
if (!onSaveShipmentData) return;
|
||||||
|
setIsSavingShipment(true);
|
||||||
|
try {
|
||||||
|
const result = await onSaveShipmentData({
|
||||||
|
orderGroupId: order.id,
|
||||||
|
shipmentData,
|
||||||
|
});
|
||||||
|
if (!result.success) {
|
||||||
|
setFormMessage(result.error || "Не удалось сохранить данные отгрузки");
|
||||||
|
} else {
|
||||||
|
setFormMessage("Данные отгрузки сохранены");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setFormMessage("Не удалось сохранить данные отгрузки");
|
||||||
|
} finally {
|
||||||
|
setIsSavingShipment(false);
|
||||||
|
}
|
||||||
|
}, [onSaveShipmentData, order?.id]);
|
||||||
const minSelectableDateKey = React.useMemo(() => getNextSelectableDateKey(), []);
|
const minSelectableDateKey = React.useMemo(() => getNextSelectableDateKey(), []);
|
||||||
const [currentMonth, setCurrentMonth] = React.useState(() => {
|
const [currentMonth, setCurrentMonth] = React.useState(() => {
|
||||||
const existingDeliveryDate = fromDateKey(order?.deliveryDate);
|
const existingDeliveryDate = fromDateKey(order?.deliveryDate);
|
||||||
|
|
@ -1024,7 +1046,7 @@ export const OrderDetailPanel = ({
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{userRole === "driver" && order ? (
|
{userRole === "driver" && order ? (
|
||||||
<DriverShipmentPanel order={order} onShipmentChange={handleShipmentChange} />
|
<DriverShipmentPanel order={order} onShipmentChange={handleShipmentChange} onSaveShipment={handleSaveShipment} isSavingShipment={isSavingShipment} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{userRole === "driver" && order && onChangeDeliveryStatus ? (
|
{userRole === "driver" && order && onChangeDeliveryStatus ? (
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { assignDriverToOrderGroup, fetchOrderGroups, updateDeliveryStatus, updateOrderGroupDeliveryChoice } from "../services/supabase/orderGroupRepository";
|
import { assignDriverToOrderGroup, fetchOrderGroups, saveShipmentData, updateDeliveryStatus, updateOrderGroupDeliveryChoice } from "../services/supabase/orderGroupRepository";
|
||||||
import {
|
import {
|
||||||
buildOrderGroupBuckets,
|
buildOrderGroupBuckets,
|
||||||
filterOrderGroups,
|
filterOrderGroups,
|
||||||
|
|
@ -171,10 +171,10 @@ export const useOrderGroups = () => {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const changeDeliveryStatus = React.useCallback(async ({ orderGroupId, status, details }) => {
|
const changeDeliveryStatus = React.useCallback(async ({ orderGroupId, status, details, shipmentData }) => {
|
||||||
setIsSavingStatusChange(true);
|
setIsSavingStatusChange(true);
|
||||||
try {
|
try {
|
||||||
const result = await updateDeliveryStatus({ orderGroupId, status, details });
|
const result = await updateDeliveryStatus({ orderGroupId, status, details, shipmentData });
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
|
@ -195,6 +195,27 @@ export const useOrderGroups = () => {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onSaveShipmentData = React.useCallback(async ({ orderGroupId, shipmentData }) => {
|
||||||
|
try {
|
||||||
|
const result = await saveShipmentData({ orderGroupId, shipmentData });
|
||||||
|
if (result.error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: getErrorMessage(result.error, "Не удалось сохранить данные отгрузки"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
setOrderGroups((currentGroups) =>
|
||||||
|
currentGroups.map((group) => (group.id === orderGroupId ? result.data : group)),
|
||||||
|
);
|
||||||
|
return { success: true, data: result.data };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: getErrorMessage(error, "Не удалось сохранить данные отгрузки"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
orderGroups,
|
orderGroups,
|
||||||
allOrderGroups: orderGroups,
|
allOrderGroups: orderGroups,
|
||||||
|
|
@ -211,6 +232,7 @@ export const useOrderGroups = () => {
|
||||||
saveManualDeliveryChoice,
|
saveManualDeliveryChoice,
|
||||||
assignDriver,
|
assignDriver,
|
||||||
changeDeliveryStatus,
|
changeDeliveryStatus,
|
||||||
|
saveShipmentData: onSaveShipmentData,
|
||||||
isSavingDeliveryChoice,
|
isSavingDeliveryChoice,
|
||||||
isSavingDriverAssignment,
|
isSavingDriverAssignment,
|
||||||
isSavingStatusChange,
|
isSavingStatusChange,
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ export const GroupDetailPage = () => {
|
||||||
isSavingStatusChange,
|
isSavingStatusChange,
|
||||||
assignDriver,
|
assignDriver,
|
||||||
changeDeliveryStatus,
|
changeDeliveryStatus,
|
||||||
|
saveShipmentData,
|
||||||
isLoading,
|
isLoading,
|
||||||
} = useOrderGroups();
|
} = useOrderGroups();
|
||||||
|
|
||||||
|
|
@ -111,6 +112,7 @@ export const GroupDetailPage = () => {
|
||||||
drivers={drivers}
|
drivers={drivers}
|
||||||
onAssignDriver={assignDriver}
|
onAssignDriver={assignDriver}
|
||||||
onChangeDeliveryStatus={changeDeliveryStatus}
|
onChangeDeliveryStatus={changeDeliveryStatus}
|
||||||
|
onSaveShipmentData={saveShipmentData}
|
||||||
userRole={userRole}
|
userRole={userRole}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
|
||||||
|
|
@ -117,11 +117,18 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
|
||||||
// Also treat address equal to "САМОВЫВОЗ" as pickup indicator
|
// Also treat address equal to "САМОВЫВОЗ" as pickup indicator
|
||||||
const isPickupAddress = deliveryAddress.toUpperCase() === "САМОВЫВОЗ";
|
const isPickupAddress = deliveryAddress.toUpperCase() === "САМОВЫВОЗ";
|
||||||
|
|
||||||
// Resolve effective delivery type: DB field takes precedence, but if it says "delivery"
|
// Resolve effective delivery type:
|
||||||
// while source data clearly indicates pickup, treat as pickup
|
// - If DB explicitly says "pickup" → pickup
|
||||||
const effectiveDeliveryType = (row.delivery_type === "pickup" || deliveryStatus === "pickup" || isPickupFromSource || isPickupAddress)
|
// - If status is "pickup" → pickup
|
||||||
|
// - If DB explicitly says "delivery" (manually confirmed by logistician) → honor it, don't override
|
||||||
|
// - Otherwise fall back to source/address detection
|
||||||
|
const effectiveDeliveryType = (row.delivery_type === "pickup" || deliveryStatus === "pickup")
|
||||||
? "pickup"
|
? "pickup"
|
||||||
: (row.delivery_type || "delivery");
|
: row.delivery_type === "delivery"
|
||||||
|
? "delivery"
|
||||||
|
: (isPickupFromSource || isPickupAddress)
|
||||||
|
? "pickup"
|
||||||
|
: (row.delivery_type || "delivery");
|
||||||
|
|
||||||
// Preserve original address for pre-filling delivery form (don't clear for pickup)
|
// Preserve original address for pre-filling delivery form (don't clear for pickup)
|
||||||
const originalDeliveryAddress = deliveryAddress;
|
const originalDeliveryAddress = deliveryAddress;
|
||||||
|
|
@ -214,6 +221,7 @@ 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,
|
||||||
|
syncedTo1cAt: row.synced_to_1c_at || null,
|
||||||
deliveryHalfDay: getOrderGroupDeliveryHalfDay({
|
deliveryHalfDay: getOrderGroupDeliveryHalfDay({
|
||||||
deliveryHalfDay: rawDeliveryHalfDay,
|
deliveryHalfDay: rawDeliveryHalfDay,
|
||||||
deliveryTime: rawDeliveryTime,
|
deliveryTime: rawDeliveryTime,
|
||||||
|
|
@ -254,7 +262,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`;
|
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`;
|
||||||
|
|
||||||
export const updateOrderGroupDeliveryChoice = async ({
|
export const updateOrderGroupDeliveryChoice = async ({
|
||||||
orderGroupId,
|
orderGroupId,
|
||||||
|
|
@ -280,6 +288,8 @@ export const updateOrderGroupDeliveryChoice = async ({
|
||||||
if (deliveryType === "pickup") {
|
if (deliveryType === "pickup") {
|
||||||
updatePayload.pickup_date = pickupDate || null;
|
updatePayload.pickup_date = pickupDate || null;
|
||||||
updatePayload.pickup_time_slot = pickupTimeSlot || null;
|
updatePayload.pickup_time_slot = pickupTimeSlot || null;
|
||||||
|
// Pickup orders don't need a driver — clear assignment
|
||||||
|
updatePayload.assigned_driver_id = null;
|
||||||
} else {
|
} else {
|
||||||
updatePayload.pickup_date = null;
|
updatePayload.pickup_date = null;
|
||||||
updatePayload.pickup_time_slot = null;
|
updatePayload.pickup_time_slot = null;
|
||||||
|
|
@ -378,7 +388,40 @@ export const assignDriverToOrderGroup = async ({
|
||||||
}, "Ошибка назначения водителя");
|
}, "Ошибка назначения водителя");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateDeliveryStatus = async ({ orderGroupId, status, details } = {}) => {
|
export const saveShipmentData = async ({ orderGroupId, shipmentData }) => {
|
||||||
|
return safeSupabaseCall(async () => {
|
||||||
|
const client = requireSupabase();
|
||||||
|
|
||||||
|
const { error: updateError } = await client
|
||||||
|
.from("order_groups")
|
||||||
|
.update({
|
||||||
|
driver_shipment_data: shipmentData,
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
.eq("id", orderGroupId);
|
||||||
|
|
||||||
|
if (updateError) throw updateError;
|
||||||
|
|
||||||
|
const { data, error } = await client
|
||||||
|
.from("order_groups")
|
||||||
|
.select(ORDER_GROUP_SELECT_FIELDS)
|
||||||
|
.eq("id", orderGroupId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
await logAction({
|
||||||
|
orderGroupId,
|
||||||
|
action: "shipment_data_saved",
|
||||||
|
newValue: "shipment_data_updated",
|
||||||
|
details: { itemCount: shipmentData?.length || 0 },
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
|
return mapOrderGroupRowToDeliveryGroup(data);
|
||||||
|
}, "Ошибка сохранения данных отгрузки");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateDeliveryStatus = async ({ orderGroupId, status, details, shipmentData } = {}) => {
|
||||||
return safeSupabaseCall(async () => {
|
return safeSupabaseCall(async () => {
|
||||||
const client = requireSupabase();
|
const client = requireSupabase();
|
||||||
|
|
||||||
|
|
@ -428,6 +471,22 @@ export const updateDeliveryStatus = async ({ orderGroupId, status, details } = {
|
||||||
if (rpcError) throw rpcError;
|
if (rpcError) throw rpcError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save shipment data if provided (e.g. partial delivery info)
|
||||||
|
if (shipmentData) {
|
||||||
|
const { error: shipmentUpdateError } = await client
|
||||||
|
.from("order_groups")
|
||||||
|
.update({
|
||||||
|
driver_shipment_data: shipmentData,
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
.eq("id", orderGroupId);
|
||||||
|
|
||||||
|
if (shipmentUpdateError) {
|
||||||
|
// Log but don't fail the status update
|
||||||
|
console.error("[updateDeliveryStatus] Failed to save shipment data:", shipmentUpdateError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch updated group
|
// Fetch updated group
|
||||||
const { data, error } = await client
|
const { data, error } = await client
|
||||||
.from("order_groups")
|
.from("order_groups")
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,7 @@ alter table public.order_groups add column if not exists last_sms_error text;
|
||||||
alter table public.order_groups add column if not exists next_notification_check_at timestamptz;
|
alter table public.order_groups add column if not exists next_notification_check_at timestamptz;
|
||||||
alter table public.order_groups add column if not exists delivery_date date;
|
alter table public.order_groups add column if not exists delivery_date date;
|
||||||
alter table public.order_groups add column if not exists delivery_time text;
|
alter table public.order_groups add column if not exists delivery_time text;
|
||||||
|
alter table public.order_groups add column if not exists synced_to_1c_at timestamptz;
|
||||||
|
|
||||||
alter table public.orders add column if not exists source_order_number text;
|
alter table public.orders add column if not exists source_order_number text;
|
||||||
alter table public.orders add column if not exists source_order_date date;
|
alter table public.orders add column if not exists source_order_date date;
|
||||||
|
|
@ -465,6 +466,7 @@ create index if not exists idx_delivery_invitations_expires_at on public.deliver
|
||||||
create index if not exists idx_order_groups_status on public.order_groups (status);
|
create index if not exists idx_order_groups_status on public.order_groups (status);
|
||||||
create index if not exists idx_order_groups_delivery_status on public.order_groups (delivery_status);
|
create index if not exists idx_order_groups_delivery_status on public.order_groups (delivery_status);
|
||||||
create index if not exists idx_order_groups_notification_status on public.order_groups (notification_status, next_notification_check_at);
|
create index if not exists idx_order_groups_notification_status on public.order_groups (notification_status, next_notification_check_at);
|
||||||
|
create index if not exists idx_order_groups_synced_to_1c on public.order_groups (synced_to_1c_at);
|
||||||
create index if not exists idx_integration_events_order_id on public.integration_events (order_id, created_at desc);
|
create index if not exists idx_integration_events_order_id on public.integration_events (order_id, created_at desc);
|
||||||
create index if not exists idx_integration_events_event_type on public.integration_events (event_type);
|
create index if not exists idx_integration_events_event_type on public.integration_events (event_type);
|
||||||
create index if not exists idx_rate_limits_scope_key_window on public.rate_limits (scope, rate_key, window_start desc);
|
create index if not exists idx_rate_limits_scope_key_window on public.rate_limits (scope, rate_key, window_start desc);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue