diff --git a/src/components/driver/DriverDeliveryPlanner.jsx b/src/components/driver/DriverDeliveryPlanner.jsx index 467788c..af4240f 100644 --- a/src/components/driver/DriverDeliveryPlanner.jsx +++ b/src/components/driver/DriverDeliveryPlanner.jsx @@ -6,6 +6,7 @@ import { DRIVER_VISIBLE_DELIVERY_STATUSES, isOrderGroupVisibleToDriver, groupOrderGroupsByDate, + parseGroupDate, } from "../../services/orderGroupViews"; import { Badge } from "../UI/Badge"; import { Input } from "../UI/Input"; @@ -149,7 +150,7 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs : "border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)] hover:border-[var(--color-accent)]", ].join(" ")} > - {new Date(`${date}T12:00:00`).toLocaleDateString("ru-RU", { day: "numeric", month: "short" })} + {parseGroupDate(date)?.toLocaleDateString("ru-RU", { day: "numeric", month: "short" }) || "—"} {count > 0 && ( {count} @@ -169,17 +170,26 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs

- {new Date(`${group.date}T12:00:00`).toLocaleDateString("ru-RU", { + {parseGroupDate(group.date)?.toLocaleDateString("ru-RU", { day: "numeric", month: "long", weekday: "long", - })} + }) || "Без даты"}

{group.items.length} {group.items.length === 1 ? "группа" : "группы"}

- {group.date} + + {(() => { + const d = parseGroupDate(group.date); + if (!d) return group.date || "—"; + const day = String(d.getDate()).padStart(2, "0"); + const month = String(d.getMonth() + 1).padStart(2, "0"); + const year = d.getFullYear(); + return `${day}.${month}.${year}`; + })()} +
diff --git a/src/components/logistics/LogisticsReadinessBoard.jsx b/src/components/logistics/LogisticsReadinessBoard.jsx index 45558eb..b86221e 100644 --- a/src/components/logistics/LogisticsReadinessBoard.jsx +++ b/src/components/logistics/LogisticsReadinessBoard.jsx @@ -31,6 +31,7 @@ const renderOrderNumbers = (group) => { export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusOptions = ORDER_GROUP_DISPLAY_STATUS_OPTIONS }) => { const [filters, setFilters] = React.useState({ query: "", displayStatus: "all" }); + const [collapsedSections, setCollapsedSections] = React.useState(new Set()); const filteredGroups = React.useMemo( () => filterOrderGroups(orderGroups, filters), @@ -78,15 +79,42 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
{Array.from(statusGroups.entries()).map(([statusValue, { label, groups }]) => { if (!groups.length) return null; + const isCollapsed = collapsedSections.has(statusValue); return (
-
-

{label}

- {groups.length} -
+ - {groups.map((group) => ( + {!isCollapsed && groups.map((group) => ( + + ); + } + + return ( + +
+ Платное хранение +

+ Переведите заказ в статус платного хранения, если клиент не забрал товар в срок. +

+
+ + {showConfirm ? ( +
+

Перевести заказ в платное хранение? Клиент получит уведомление.

+
+ + +
+
+ ) : ( + + )} +
+ ); + })() + ) : null} + {canManageDelivery ? (
@@ -779,6 +876,10 @@ export const OrderDetailPanel = ({

Ручное согласование выполнено

{order.manualConfirmationAt ? formatDateTime(order.manualConfirmationAt) : "Нет"}

+
+

Платное хранение

+

{order.paidStorageAt ? formatDateTime(order.paidStorageAt) : "Нет"}

+
{order.createdFromExchangeAt ? (

Создано из обмена

diff --git a/src/services/orderGroupViews.js b/src/services/orderGroupViews.js index e00acb4..65d7a39 100644 --- a/src/services/orderGroupViews.js +++ b/src/services/orderGroupViews.js @@ -11,6 +11,7 @@ export const DELIVERY_GROUP_STATUS_LABELS = { on_route: "В пути", delivered: "Доставлено", problem: "Проблема", + paid_storage: "Платное хранение", cancelled: "Отменено", }; @@ -168,7 +169,7 @@ export const isOrderGroupVisibleToDriver = (group) => { return DRIVER_VISIBLE_DELIVERY_STATUSES.includes(deliveryStatus); }; -const parseGroupDate = (value) => { +export const parseGroupDate = (value) => { const normalized = normalizeDate(value); if (!normalized) { @@ -299,6 +300,7 @@ export const ORDER_GROUP_DISPLAY_STATUS_OPTIONS = [ { value: "delivery:on_route", label: DELIVERY_GROUP_STATUS_LABELS.on_route }, { value: "delivery:delivered", label: DELIVERY_GROUP_STATUS_LABELS.delivered }, { value: "delivery:problem", label: DELIVERY_GROUP_STATUS_LABELS.problem }, + { value: "delivery:paid_storage", label: DELIVERY_GROUP_STATUS_LABELS.paid_storage }, { value: "delivery:cancelled", label: DELIVERY_GROUP_STATUS_LABELS.cancelled }, ]; @@ -321,6 +323,8 @@ export const getOrderGroupDeliveryStatusTone = (status) => { return "accent"; case "delivered": return "accent"; + case "paid_storage": + return "warning"; case "problem": return "danger"; case "cancelled": diff --git a/src/services/supabase/orderGroupRepository.js b/src/services/supabase/orderGroupRepository.js index e1268c2..847a2a3 100644 --- a/src/services/supabase/orderGroupRepository.js +++ b/src/services/supabase/orderGroupRepository.js @@ -117,6 +117,7 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => { firstSmsSentAt: row.first_sms_sent_at || null, secondSmsSentAt: row.second_sms_sent_at || null, manualConfirmationAt: row.manual_confirmation_at || null, + paidStorageAt: row.paid_storage_at || null, notificationStatus: normalizeText(row.notification_status), createdFromExchangeAt: row.created_from_exchange_at || null, sourceKey: row.source_key || null, @@ -199,7 +200,7 @@ export const updateOrderGroupDeliveryChoice = async ({ const { data, error } = await client .from("order_groups") - .select("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, manual_confirmation_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name)") + .select("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, manual_confirmation_at, paid_storage_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name)") .eq("id", orderGroupId) .single(); @@ -282,7 +283,7 @@ export const fetchOrderGroups = async () => { const client = requireSupabase(); const { data, error } = await client .from("order_groups") - .select("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, manual_confirmation_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name)") + .select("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, manual_confirmation_at, paid_storage_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name)") .order("updated_at", { ascending: false }); if (error) { diff --git a/src/services/supabase/orderGroupRepository.test.js b/src/services/supabase/orderGroupRepository.test.js index 6e83e24..a2d796a 100644 --- a/src/services/supabase/orderGroupRepository.test.js +++ b/src/services/supabase/orderGroupRepository.test.js @@ -166,7 +166,7 @@ describe("updateOrderGroupDeliveryChoice", () => { updated_at: expect.any(String), }); expect(eqMock).toHaveBeenCalledWith("id", "group-id"); - expect(selectMock).toHaveBeenCalledWith("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, manual_confirmation_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name)"); + expect(selectMock).toHaveBeenCalledWith("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, manual_confirmation_at, paid_storage_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name)"); expect(singleMock).toHaveBeenCalledTimes(1); }); }); diff --git a/supabase/paid-storage-status.sql b/supabase/paid-storage-status.sql new file mode 100644 index 0000000..302dfd6 --- /dev/null +++ b/supabase/paid-storage-status.sql @@ -0,0 +1,56 @@ +-- Patch: Add paid_storage status support for order_groups + +-- 1. Add paid_storage_at column to order_groups if not exists +alter table public.order_groups add column if not exists paid_storage_at timestamptz; + +-- 2. Update update_delivery_status to allow paid_storage without assigned driver +create or replace function public.update_delivery_status( + p_order_group_id uuid, + p_status text +) +returns boolean +language plpgsql +security definer +set search_path = public +as $$ +declare + v_assigned_driver_id uuid; + v_current_status text; +begin + select assigned_driver_id, delivery_status + into v_assigned_driver_id, v_current_status + from public.order_groups + where id = p_order_group_id; + + -- Allow paid_storage status for any authenticated user with proper role + -- (checked by RLS policy) + if p_status = 'paid_storage' then + update public.order_groups + set delivery_status = p_status, + paid_storage_at = timezone('utc', now()), + updated_at = timezone('utc', now()) + where id = p_order_group_id; + return true; + end if; + + if v_assigned_driver_id is null then + raise exception 'Группа не назначена водителю'; + end if; + + if v_assigned_driver_id != auth.uid() then + raise exception 'Вы не назначены на эту доставку'; + end if; + + update public.order_groups + set delivery_status = p_status, + updated_at = timezone('utc', now()) + where id = p_order_group_id; + + return true; +end; +$$; + +-- 3. Ensure proper grants +revoke execute on function public.update_delivery_status(uuid, text) from anon; +grant execute on function public.update_delivery_status(uuid, text) to authenticated; +