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);