diff --git a/src/components/driver/DriverShipmentPanel.jsx b/src/components/driver/DriverShipmentPanel.jsx index cf8e3ef..6e0b7f2 100644 --- a/src/components/driver/DriverShipmentPanel.jsx +++ b/src/components/driver/DriverShipmentPanel.jsx @@ -89,7 +89,7 @@ const parseOrderItems = (order) => { return []; }; -export const DriverShipmentPanel = ({ order, onShipmentChange }) => { +export const DriverShipmentPanel = ({ order, onShipmentChange, onSaveShipment, isSavingShipment }) => { const [stopWords, setStopWords] = React.useState([]); const [scopeActive, setScopeActive] = React.useState(true); @@ -261,6 +261,29 @@ export const DriverShipmentPanel = ({ order, onShipmentChange }) => { )} )} + + {onSaveShipment && items.length > 0 && ( +
+ + {shippedCount > 0 && shippedCount < items.length && ( + + Частичная отгрузка: {shippedCount}/{items.length} + + )} +
+ )} ); }; diff --git a/src/components/orders/OrderDetailPanel.jsx b/src/components/orders/OrderDetailPanel.jsx index d3958a1..fd3a28d 100644 --- a/src/components/orders/OrderDetailPanel.jsx +++ b/src/components/orders/OrderDetailPanel.jsx @@ -544,7 +544,8 @@ export const OrderDetailPanel = ({ drivers = [], onAssignDriver, onChangeDeliveryStatus, - userRole, + onSaveShipmentData, + userRole = "driver", }) => { const [problemReason, setProblemReason] = 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 [formMessage, setFormMessage] = React.useState(""); const [shipmentState, setShipmentState] = React.useState(null); + const [isSavingShipment, setIsSavingShipment] = React.useState(false); const [isCalendarOpen, setIsCalendarOpen] = React.useState(false); const [driverMessage, setDriverMessage] = React.useState(""); const [selectedDriverId, setSelectedDriverId] = React.useState(order?.assignedDriverId || ""); @@ -564,6 +566,26 @@ export const OrderDetailPanel = ({ const handleShipmentChange = React.useCallback((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 [currentMonth, setCurrentMonth] = React.useState(() => { const existingDeliveryDate = fromDateKey(order?.deliveryDate); @@ -1024,7 +1046,7 @@ export const OrderDetailPanel = ({ ) : null} {userRole === "driver" && order ? ( - + ) : null} {userRole === "driver" && order && onChangeDeliveryStatus ? ( diff --git a/src/hooks/useOrderGroups.js b/src/hooks/useOrderGroups.js index 0a63e33..ec4c73d 100644 --- a/src/hooks/useOrderGroups.js +++ b/src/hooks/useOrderGroups.js @@ -1,5 +1,5 @@ import React from "react"; -import { assignDriverToOrderGroup, fetchOrderGroups, updateDeliveryStatus, updateOrderGroupDeliveryChoice } from "../services/supabase/orderGroupRepository"; +import { assignDriverToOrderGroup, fetchOrderGroups, saveShipmentData, updateDeliveryStatus, updateOrderGroupDeliveryChoice } from "../services/supabase/orderGroupRepository"; import { buildOrderGroupBuckets, 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); try { - const result = await updateDeliveryStatus({ orderGroupId, status, details }); + const result = await updateDeliveryStatus({ orderGroupId, status, details, shipmentData }); if (result.error) { return { 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 { orderGroups, allOrderGroups: orderGroups, @@ -211,6 +232,7 @@ export const useOrderGroups = () => { saveManualDeliveryChoice, assignDriver, changeDeliveryStatus, + saveShipmentData: onSaveShipmentData, isSavingDeliveryChoice, isSavingDriverAssignment, isSavingStatusChange, diff --git a/src/pages/GroupDetailPage.jsx b/src/pages/GroupDetailPage.jsx index dd2be64..43803fb 100644 --- a/src/pages/GroupDetailPage.jsx +++ b/src/pages/GroupDetailPage.jsx @@ -35,6 +35,7 @@ export const GroupDetailPage = () => { isSavingStatusChange, assignDriver, changeDeliveryStatus, + saveShipmentData, isLoading, } = useOrderGroups(); @@ -111,6 +112,7 @@ export const GroupDetailPage = () => { drivers={drivers} onAssignDriver={assignDriver} onChangeDeliveryStatus={changeDeliveryStatus} + onSaveShipmentData={saveShipmentData} userRole={userRole} /> diff --git a/src/services/supabase/orderGroupRepository.js b/src/services/supabase/orderGroupRepository.js index e464e0b..8dc7795 100644 --- a/src/services/supabase/orderGroupRepository.js +++ b/src/services/supabase/orderGroupRepository.js @@ -117,11 +117,18 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => { // 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) + // Resolve effective delivery type: + // - If DB explicitly says "pickup" → pickup + // - 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" - : (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) const originalDeliveryAddress = deliveryAddress; @@ -214,6 +221,7 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => { pickupDate: row.pickup_date || null, pickupTimeSlot: row.pickup_time_slot || null, driverShipmentData: row.driver_shipment_data || null, + syncedTo1cAt: row.synced_to_1c_at || null, deliveryHalfDay: getOrderGroupDeliveryHalfDay({ deliveryHalfDay: rawDeliveryHalfDay, 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 ({ orderGroupId, @@ -280,6 +288,8 @@ export const updateOrderGroupDeliveryChoice = async ({ if (deliveryType === "pickup") { updatePayload.pickup_date = pickupDate || null; updatePayload.pickup_time_slot = pickupTimeSlot || null; + // Pickup orders don't need a driver — clear assignment + updatePayload.assigned_driver_id = null; } else { updatePayload.pickup_date = 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 () => { const client = requireSupabase(); @@ -428,6 +471,22 @@ export const updateDeliveryStatus = async ({ orderGroupId, status, details } = { 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 const { data, error } = await client .from("order_groups") diff --git a/supabase/schema.sql b/supabase/schema.sql index 7fd68fe..b0962c0 100644 --- a/supabase/schema.sql +++ b/supabase/schema.sql @@ -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 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 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_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_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_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_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);