feat: pickup_code для самовывоза + упрощение статуса отгрузки

- pickup_code: автогенерация 6-значного кода при выборе самовывоза
- Колонка pickup_code в order_groups + SELECT
- Отображение кода выдачи в карточках (логист, клиент)
- Упрощение: любая отгрузка → delivered (проблемы в has_delivery_problem)
- RPC: добавлены deliveryType, pickupCode, pickupDate, pickupTimeSlot
This commit is contained in:
root 2026-06-16 06:53:12 +00:00
parent 6184f5aeed
commit 5867272873
5 changed files with 48 additions and 25 deletions

View File

@ -863,6 +863,12 @@ export const OrderDetailPanel = ({
<p className="text-xs text-[var(--color-text-muted)]">{isPickupOrder ? "Статус самовывоза" : "Статус доставки"}</p>
<p className="font-medium !text-[var(--color-text)]">{getOrderGroupDeliveryStatusLabel(order.deliveryStatus || order.delivery_status)}</p>
</div>
{(order.pickupCode || order.pickup_code) ? (
<div>
<p className="text-xs text-[var(--color-text-muted)]">Код выдачи</p>
<p className="text-2xl font-bold tracking-[0.3em] text-[var(--color-accent)]">{order.pickupCode || order.pickup_code}</p>
</div>
) : null}
</div>
</Panel>
@ -1087,14 +1093,11 @@ export const OrderDetailPanel = ({
return statusOptions.map((statusOption) => {
const isSelected = pendingStatus?.value === statusOption.value;
const isDeliveredBtn = statusOption.value === "delivered";
const deliveryBlocked = isDeliveredBtn && shipmentState && !shipmentState.canMarkDelivered;
return (
<Button
key={statusOption.value}
variant={isSelected ? "primary" : "secondary"}
disabled={deliveryBlocked}
title={deliveryBlocked ? "Сначала отметьте все позиции как отгруженные" : undefined}
disabled={isSavingStatusChange}
onClick={() => {
if (statusOption.value === "problem") {
setProblemReason("selecting");
@ -1104,11 +1107,6 @@ export const OrderDetailPanel = ({
}}
>
{statusOption.label}
{isDeliveredBtn && shipmentState && !shipmentState.canMarkDelivered ? (
<span className="ml-1.5 text-xs text-[var(--color-text-muted)]">
({shipmentState.shipped}/{shipmentState.total})
</span>
) : null}
</Button>
);
});
@ -1120,7 +1118,6 @@ export const OrderDetailPanel = ({
variant="primary"
disabled={isSavingStatusChange}
onClick={() => {
if (pendingStatus.value === "delivered" && shipmentState && !shipmentState.canMarkDelivered) return;
onChangeDeliveryStatus({
orderGroupId: order.id,
status: pendingStatus.value,

View File

@ -378,6 +378,13 @@ export const ClientDeliveryPage = () => {
<p className="text-sm leading-6 text-[var(--color-text-muted)]">
Статус: {invitation?.deliveryType === "pickup" ? "самовывоз" : "доставка"} уже согласован. При повторном открытии этой ссылки будет показан тот же выбор.
</p>
{(invitation?.pickupCode || invitation?.pickup_code) ? (
<div className="mt-3 rounded-2xl border-2 border-[var(--color-accent)] bg-[var(--color-accent-soft)] p-4 text-center">
<p className="text-xs uppercase tracking-[0.2em] text-[var(--color-text-muted)]">Код выдачи</p>
<p className="mt-1 text-3xl font-bold tracking-[0.4em] text-[var(--color-accent)]">{invitation.pickupCode || invitation.pickup_code}</p>
<p className="mt-2 text-xs text-[var(--color-text-muted)]">Покажите этот код при получении заказа</p>
</div>
) : null}
</Panel>
) : null}

View File

@ -223,6 +223,7 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
driverShipmentData: row.driver_shipment_data || null,
hasDeliveryProblem: row.has_delivery_problem || false,
deliveryProblemNote: row.delivery_problem_note || null,
pickupCode: row.pickup_code || null,
syncedTo1cAt: row.synced_to_1c_at || null,
deliveryHalfDay: getOrderGroupDeliveryHalfDay({
deliveryHalfDay: rawDeliveryHalfDay,
@ -264,7 +265,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, has_delivery_problem, delivery_problem_note, synced_to_1c_at`;
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, has_delivery_problem, delivery_problem_note, pickup_code, synced_to_1c_at`;
export const updateOrderGroupDeliveryChoice = async ({
orderGroupId,
@ -292,6 +293,16 @@ export const updateOrderGroupDeliveryChoice = async ({
updatePayload.pickup_time_slot = pickupTimeSlot || null;
// Pickup orders don't need a driver — clear assignment
updatePayload.assigned_driver_id = null;
// Generate pickup code if not already set
const { data: existing } = await client
.from("order_groups")
.select("pickup_code")
.eq("id", orderGroupId)
.single();
if (!existing?.pickup_code) {
const code = Math.random().toString(36).substring(2, 8).toUpperCase();
updatePayload.pickup_code = code;
}
} else {
updatePayload.pickup_date = null;
updatePayload.pickup_time_slot = null;
@ -393,22 +404,14 @@ export const assignDriverToOrderGroup = async ({
export const saveShipmentData = async ({ orderGroupId, shipmentData }) => {
return safeSupabaseCall(async () => {
const client = requireSupabase();
const unshipped = shipmentData.filter((i) => !i.shipped);
const allShipped = unshipped.length === 0 && shipmentData.length > 0;
const unshippedWithoutComment = unshipped.filter((i) => !i.comment?.trim()).length;
const canMarkDelivered = allShipped || unshippedWithoutComment === 0;
const hasProblem = unshipped.length > 0;
const hasAnyShipped = shipmentData.some((i) => i.shipped);
const hasProblem = shipmentData.some((i) => !i.shipped);
const problemNote = hasProblem
? unshipped.map((i) => `${i.name}${i.quantity ? ` (${i.quantity}${i.unit ? ` ${i.unit}` : ""})` : ""}${i.comment ? `${i.comment}` : ""}`).join("; ")
? shipmentData.filter((i) => !i.shipped).map((i) => `${i.name}${i.quantity ? ` (${i.quantity}${i.unit ? ` ${i.unit}` : ""})` : ""}${i.comment ? `${i.comment}` : ""}`).join("; ")
: null;
// Auto-update delivery status based on shipment
let newDeliveryStatus = undefined;
if (canMarkDelivered) {
newDeliveryStatus = "delivered";
} else if (hasProblem) {
newDeliveryStatus = "problem";
}
// Any shipment data saved = delivered status. Problems go into has_delivery_problem/delivery_problem_note columns.
const newDeliveryStatus = hasAnyShipped ? "delivered" : undefined;
const updatePayload = {
driver_shipment_data: shipmentData,

View File

@ -210,6 +210,16 @@ Deno.serve(async (request) => {
groupUpdateData.pickup_time_slot = body.pickupTimeSlot || requestedSlot.deliveryTime || null;
// Pickup orders don't need a driver — clear assignment
groupUpdateData.assigned_driver_id = null;
// Generate pickup code if not already set
const { data: existingGroup } = await supabase
.from("order_groups")
.select("pickup_code")
.eq("id", invitation.order_group_id)
.single();
if (!existingGroup?.pickup_code) {
const code = Math.random().toString(36).substring(2, 8).toUpperCase();
groupUpdateData.pickup_code = code;
}
}
const { error: groupUpdateError } = await supabase
@ -258,6 +268,7 @@ Deno.serve(async (request) => {
ok: true,
orderGroupId: invitation.order_group_id,
deliveryStatus: effectiveDeliveryStatus,
pickupCode: groupUpdateData.pickup_code || null,
},
200,
corsHeaders,

View File

@ -152,7 +152,7 @@ Deno.serve(async (request) => {
const { data: currentGroup, error: currentGroupError } = await supabase
.from("order_groups")
.select("id, delivery_status, delivery_invitation_id")
.select("id, delivery_status, delivery_invitation_id, pickup_code")
.eq("id", orderGroupId)
.single();
@ -175,6 +175,11 @@ Deno.serve(async (request) => {
updateData.pickup_date = body.pickupDate || deliveryDate || null;
updateData.pickup_time_slot = body.pickupTimeSlot || deliveryTime || null;
updateData.assigned_driver_id = null;
// Generate pickup code if not already set
if (!currentGroup.pickup_code) {
const code = Math.random().toString(36).substring(2, 8).toUpperCase();
updateData.pickup_code = code;
}
} else {
updateData.pickup_date = null;
updateData.pickup_time_slot = null;