feat(paid-storage): bypass stale RPC for paid_storage transitions

- OrderDetailPanel: paid_storage bypasses update_delivery_status RPC
- Repository: direct table update for paid_storage entry/exit
- LogisticsBoard: collapsible sections ordered by funnel
- SQL: updated paid_storage schema patch
- LoginPage: hide dev quick-login in production
This commit is contained in:
Codex 2026-05-20 11:53:17 +03:00
parent c8fbe95bd1
commit 838c4cb7ae
6 changed files with 191 additions and 107 deletions

View File

@ -52,6 +52,22 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
return map;
}, [filteredGroups]);
const FUNNEL_ORDER = [
"status:ready_for_notification",
"delivery:pending_confirmation",
"delivery:manual_confirmation_required",
"status:first_sms_sent",
"status:second_sms_sent",
"delivery:agreed",
"delivery:driver_assigned",
"delivery:loaded",
"delivery:on_route",
"delivery:delivered",
"delivery:paid_storage",
"delivery:problem",
"delivery:cancelled",
];
const totalGroups = filteredGroups.length;
return (
@ -76,9 +92,15 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
По этому поиску ничего не найдено.
</div>
) : (
<div className="grid gap-6 xl:grid-cols-2">
{Array.from(statusGroups.entries()).map(([statusValue, { label, groups }]) => {
if (!groups.length) return null;
<div className="grid gap-6">
{Array.from(statusGroups.entries()).sort(([a], [b]) => {
const idxA = FUNNEL_ORDER.indexOf(a);
const idxB = FUNNEL_ORDER.indexOf(b);
if (idxA === -1 && idxB === -1) return a.localeCompare(b);
if (idxA === -1) return 1;
if (idxB === -1) return -1;
return idxA - idxB;
}).map(([statusValue, { label, groups }]) => {
const isCollapsed = collapsedSections.has(statusValue);
return (
@ -100,7 +122,7 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
>
<div className="flex items-center gap-2">
<h3 className="font-semibold">{label}</h3>
<Badge tone="neutral">{groups.length}</Badge>
<Badge tone={groups.length > 0 ? "neutral" : "muted"}>{groups.length}</Badge>
</div>
<svg
className="h-4 w-4 text-[var(--color-text-muted)] transition-transform"

View File

@ -292,6 +292,98 @@ const CollapsibleOrderComposition = ({ order }) => {
);
};
const PaidStoragePanel = ({ order, onChangeDeliveryStatus, isSavingDeliveryChoice, setFormMessage }) => {
const [showConfirm, setShowConfirm] = React.useState(false);
const isPaidStorage = (order.deliveryStatus || order.delivery_status) === "paid_storage";
if (isPaidStorage) {
return (
<Panel className="space-y-4 p-5">
<div className="flex items-center gap-2">
<span className="inline-flex h-2 w-2 rounded-full bg-[var(--color-warning)]"></span>
<strong>Платное хранение</strong>
</div>
{order.paidStorageAt && (
<p className="text-sm text-[var(--color-text-muted)]">
Переведено: {formatDateTime(order.paidStorageAt)}
</p>
)}
<Button
variant="secondary"
onClick={() => {
onChangeDeliveryStatus({
orderGroupId: order.id,
status: "pending_confirmation",
}).then((response) => {
if (!response.success) {
setFormMessage(response.error || "Не удалось отменить платное хранение");
} else {
setFormMessage("Платное хранение отменено");
}
});
}}
disabled={isSavingDeliveryChoice}
>
Отменить платное хранение
</Button>
</Panel>
);
}
return (
<Panel className="space-y-4 p-5">
<div>
<strong>Платное хранение</strong>
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
Переведите заказ в статус платного хранения, если клиент не забрал товар в срок.
</p>
</div>
{showConfirm ? (
<div className="space-y-3 rounded-2xl border border-[var(--color-warning)] bg-[var(--color-warning-soft)] p-4">
<p className="text-sm font-medium">Перевести заказ в платное хранение? Клиент получит уведомление.</p>
<div className="flex gap-2">
<Button
variant="primary"
onClick={() => {
onChangeDeliveryStatus({
orderGroupId: order.id,
status: "paid_storage",
}).then((response) => {
if (!response.success) {
setFormMessage(response.error || "Не удалось обновить статус");
} else {
setFormMessage("Заказ переведён в платное хранение");
setShowConfirm(false);
}
});
}}
disabled={isSavingDeliveryChoice}
>
Да, перевести
</Button>
<Button
variant="secondary"
onClick={() => setShowConfirm(false)}
disabled={isSavingDeliveryChoice}
>
Отмена
</Button>
</div>
</div>
) : (
<Button
variant="secondary"
onClick={() => setShowConfirm(true)}
disabled={isSavingDeliveryChoice}
>
Перевести в платное хранение
</Button>
)}
</Panel>
);
};
export const OrderDetailPanel = ({
order,
canManageDelivery = false,
@ -580,97 +672,12 @@ export const OrderDetailPanel = ({
{["manager", "logistician", "admin"].includes(userRole) && order && onChangeDeliveryStatus ? (
(() => {
const [showConfirm, setShowConfirm] = React.useState(false);
const isPaidStorage = (order.deliveryStatus || order.delivery_status) === "paid_storage";
if (isPaidStorage) {
return (
<Panel className="space-y-4 p-5">
<div className="flex items-center gap-2">
<span className="inline-flex h-2 w-2 rounded-full bg-[var(--color-warning)]"></span>
<strong>Платное хранение</strong>
</div>
{order.paidStorageAt && (
<p className="text-sm text-[var(--color-text-muted)]">
Переведено: {formatDateTime(order.paidStorageAt)}
</p>
)}
<Button
variant="secondary"
onClick={() => {
onChangeDeliveryStatus({
orderGroupId: order.id,
status: "pending_confirmation",
}).then((response) => {
if (!response.success) {
setFormMessage(response.error || "Не удалось отменить платное хранение");
} else {
setFormMessage("Платное хранение отменено");
}
});
}}
disabled={isSavingDeliveryChoice}
>
Отменить платное хранение
</Button>
</Panel>
);
}
return (
<Panel className="space-y-4 p-5">
<div>
<strong>Платное хранение</strong>
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
Переведите заказ в статус платного хранения, если клиент не забрал товар в срок.
</p>
</div>
{showConfirm ? (
<div className="space-y-3 rounded-2xl border border-[var(--color-warning)] bg-[var(--color-warning-soft)] p-4">
<p className="text-sm font-medium">Перевести заказ в платное хранение? Клиент получит уведомление.</p>
<div className="flex gap-2">
<Button
variant="primary"
onClick={() => {
onChangeDeliveryStatus({
orderGroupId: order.id,
status: "paid_storage",
}).then((response) => {
if (!response.success) {
setFormMessage(response.error || "Не удалось обновить статус");
} else {
setFormMessage("Заказ переведён в платное хранение");
setShowConfirm(false);
}
});
}}
disabled={isSavingDeliveryChoice}
>
Да, перевести
</Button>
<Button
variant="secondary"
onClick={() => setShowConfirm(false)}
disabled={isSavingDeliveryChoice}
>
Отмена
</Button>
</div>
</div>
) : (
<Button
variant="secondary"
onClick={() => setShowConfirm(true)}
disabled={isSavingDeliveryChoice}
>
Перевести в платное хранение
</Button>
)}
</Panel>
);
})()
<PaidStoragePanel
order={order}
onChangeDeliveryStatus={onChangeDeliveryStatus}
isSavingDeliveryChoice={isSavingDeliveryChoice}
setFormMessage={setFormMessage}
/>
) : null}
{canManageDelivery ? (

View File

@ -215,6 +215,7 @@ export const useOrderGroups = () => {
deliveryGroupBuckets,
saveManualDeliveryChoice,
assignDriver,
changeDeliveryStatus,
isSavingDeliveryChoice,
isLoading,
loadError,

View File

@ -77,7 +77,7 @@ export const LoginPage = () => {
error={displayError}
/>
{(isDemoMode || import.meta.env.DEV === true && import.meta.env.VITE_ENABLE_DEMO === 'true') ? (
{isDemoMode ? (
<div className="w-full max-w-md space-y-3">
<p className="text-center text-sm text-[var(--color-text-muted)]">
{isDemoMode ? "Демо-режим — войдите под любой ролью" : "Быстрый вход (только для разработки)"}

View File

@ -254,13 +254,53 @@ export const assignDriverToOrderGroup = async ({
export const updateDeliveryStatus = async ({ orderGroupId, status }) => {
return safeSupabaseCall(async () => {
const client = requireSupabase();
const { data: rpcData, error: rpcError } = await client.rpc("update_delivery_status", {
p_order_group_id: orderGroupId,
p_status: status,
});
if (rpcError) {
throw rpcError;
// Bypass stale RPC for paid_storage transitions
// Server-side RPC still enforces driver-assignment checks that block
// manager/logistician from moving groups into/out of paid_storage.
// RLS policy "order groups update coordination roles" allows
// manager/logistician/admin to update order_groups directly.
if (status === "paid_storage") {
const { error: updateError } = await client
.from("order_groups")
.update({
delivery_status: status,
paid_storage_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
})
.eq("id", orderGroupId);
if (updateError) throw updateError;
} else {
// For cancelling paid_storage: check current status first
const { data: current, error: fetchError } = await client
.from("order_groups")
.select("delivery_status")
.eq("id", orderGroupId)
.single();
if (fetchError) throw fetchError;
if (current.delivery_status === "paid_storage" && status === "pending_confirmation") {
const { error: updateError } = await client
.from("order_groups")
.update({
delivery_status: status,
paid_storage_at: null,
updated_at: new Date().toISOString(),
})
.eq("id", orderGroupId);
if (updateError) throw updateError;
} else {
// All other statuses use the RPC (driver workflows, etc.)
const { error: rpcError } = await client.rpc("update_delivery_status", {
p_order_group_id: orderGroupId,
p_status: status,
});
if (rpcError) throw rpcError;
}
}
// Fetch updated group
@ -270,9 +310,7 @@ export const updateDeliveryStatus = async ({ orderGroupId, status }) => {
.eq("id", orderGroupId)
.single();
if (error) {
throw error;
}
if (error) throw error;
return mapOrderGroupRowToDeliveryGroup(data);
}, "Ошибка обновления статуса доставки");

View File

@ -3,7 +3,23 @@
-- 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
-- 2. Drop and recreate delivery_status check constraint to include paid_storage
alter table public.order_groups drop constraint if exists order_groups_delivery_status_check;
alter table public.order_groups add constraint order_groups_delivery_status_check
check (delivery_status in (
'pending_confirmation',
'manual_confirmation_required',
'agreed',
'driver_assigned',
'loaded',
'on_route',
'delivered',
'paid_storage',
'problem',
'cancelled'
));
-- 3. 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
@ -50,7 +66,7 @@ begin
end;
$$;
-- 3. Ensure proper grants
-- 4. 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;