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:
parent
c8fbe95bd1
commit
838c4cb7ae
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -215,6 +215,7 @@ export const useOrderGroups = () => {
|
|||
deliveryGroupBuckets,
|
||||
saveManualDeliveryChoice,
|
||||
assignDriver,
|
||||
changeDeliveryStatus,
|
||||
isSavingDeliveryChoice,
|
||||
isLoading,
|
||||
loadError,
|
||||
|
|
|
|||
|
|
@ -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 ? "Демо-режим — войдите под любой ролью" : "Быстрый вход (только для разработки)"}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}, "Ошибка обновления статуса доставки");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue