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:
parent
6184f5aeed
commit
5867272873
|
|
@ -863,6 +863,12 @@ export const OrderDetailPanel = ({
|
||||||
<p className="text-xs text-[var(--color-text-muted)]">{isPickupOrder ? "Статус самовывоза" : "Статус доставки"}</p>
|
<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>
|
<p className="font-medium !text-[var(--color-text)]">{getOrderGroupDeliveryStatusLabel(order.deliveryStatus || order.delivery_status)}</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
|
|
@ -1087,14 +1093,11 @@ export const OrderDetailPanel = ({
|
||||||
|
|
||||||
return statusOptions.map((statusOption) => {
|
return statusOptions.map((statusOption) => {
|
||||||
const isSelected = pendingStatus?.value === statusOption.value;
|
const isSelected = pendingStatus?.value === statusOption.value;
|
||||||
const isDeliveredBtn = statusOption.value === "delivered";
|
|
||||||
const deliveryBlocked = isDeliveredBtn && shipmentState && !shipmentState.canMarkDelivered;
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
key={statusOption.value}
|
key={statusOption.value}
|
||||||
variant={isSelected ? "primary" : "secondary"}
|
variant={isSelected ? "primary" : "secondary"}
|
||||||
disabled={deliveryBlocked}
|
disabled={isSavingStatusChange}
|
||||||
title={deliveryBlocked ? "Сначала отметьте все позиции как отгруженные" : undefined}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (statusOption.value === "problem") {
|
if (statusOption.value === "problem") {
|
||||||
setProblemReason("selecting");
|
setProblemReason("selecting");
|
||||||
|
|
@ -1104,11 +1107,6 @@ export const OrderDetailPanel = ({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{statusOption.label}
|
{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>
|
</Button>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -1120,7 +1118,6 @@ export const OrderDetailPanel = ({
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={isSavingStatusChange}
|
disabled={isSavingStatusChange}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (pendingStatus.value === "delivered" && shipmentState && !shipmentState.canMarkDelivered) return;
|
|
||||||
onChangeDeliveryStatus({
|
onChangeDeliveryStatus({
|
||||||
orderGroupId: order.id,
|
orderGroupId: order.id,
|
||||||
status: pendingStatus.value,
|
status: pendingStatus.value,
|
||||||
|
|
|
||||||
|
|
@ -378,6 +378,13 @@ export const ClientDeliveryPage = () => {
|
||||||
<p className="text-sm leading-6 text-[var(--color-text-muted)]">
|
<p className="text-sm leading-6 text-[var(--color-text-muted)]">
|
||||||
Статус: {invitation?.deliveryType === "pickup" ? "самовывоз" : "доставка"} уже согласован. При повторном открытии этой ссылки будет показан тот же выбор.
|
Статус: {invitation?.deliveryType === "pickup" ? "самовывоз" : "доставка"} уже согласован. При повторном открытии этой ссылки будет показан тот же выбор.
|
||||||
</p>
|
</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>
|
</Panel>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -223,6 +223,7 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
|
||||||
driverShipmentData: row.driver_shipment_data || null,
|
driverShipmentData: row.driver_shipment_data || null,
|
||||||
hasDeliveryProblem: row.has_delivery_problem || false,
|
hasDeliveryProblem: row.has_delivery_problem || false,
|
||||||
deliveryProblemNote: row.delivery_problem_note || null,
|
deliveryProblemNote: row.delivery_problem_note || null,
|
||||||
|
pickupCode: row.pickup_code || null,
|
||||||
syncedTo1cAt: row.synced_to_1c_at || null,
|
syncedTo1cAt: row.synced_to_1c_at || null,
|
||||||
deliveryHalfDay: getOrderGroupDeliveryHalfDay({
|
deliveryHalfDay: getOrderGroupDeliveryHalfDay({
|
||||||
deliveryHalfDay: rawDeliveryHalfDay,
|
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 ({
|
export const updateOrderGroupDeliveryChoice = async ({
|
||||||
orderGroupId,
|
orderGroupId,
|
||||||
|
|
@ -292,6 +293,16 @@ export const updateOrderGroupDeliveryChoice = async ({
|
||||||
updatePayload.pickup_time_slot = pickupTimeSlot || null;
|
updatePayload.pickup_time_slot = pickupTimeSlot || null;
|
||||||
// Pickup orders don't need a driver — clear assignment
|
// Pickup orders don't need a driver — clear assignment
|
||||||
updatePayload.assigned_driver_id = null;
|
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 {
|
} else {
|
||||||
updatePayload.pickup_date = null;
|
updatePayload.pickup_date = null;
|
||||||
updatePayload.pickup_time_slot = null;
|
updatePayload.pickup_time_slot = null;
|
||||||
|
|
@ -393,22 +404,14 @@ export const assignDriverToOrderGroup = async ({
|
||||||
export const saveShipmentData = async ({ orderGroupId, shipmentData }) => {
|
export const saveShipmentData = async ({ orderGroupId, shipmentData }) => {
|
||||||
return safeSupabaseCall(async () => {
|
return safeSupabaseCall(async () => {
|
||||||
const client = requireSupabase();
|
const client = requireSupabase();
|
||||||
const unshipped = shipmentData.filter((i) => !i.shipped);
|
const hasAnyShipped = shipmentData.some((i) => i.shipped);
|
||||||
const allShipped = unshipped.length === 0 && shipmentData.length > 0;
|
const hasProblem = shipmentData.some((i) => !i.shipped);
|
||||||
const unshippedWithoutComment = unshipped.filter((i) => !i.comment?.trim()).length;
|
|
||||||
const canMarkDelivered = allShipped || unshippedWithoutComment === 0;
|
|
||||||
const hasProblem = unshipped.length > 0;
|
|
||||||
const problemNote = hasProblem
|
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;
|
: null;
|
||||||
|
|
||||||
// Auto-update delivery status based on shipment
|
// Any shipment data saved = delivered status. Problems go into has_delivery_problem/delivery_problem_note columns.
|
||||||
let newDeliveryStatus = undefined;
|
const newDeliveryStatus = hasAnyShipped ? "delivered" : undefined;
|
||||||
if (canMarkDelivered) {
|
|
||||||
newDeliveryStatus = "delivered";
|
|
||||||
} else if (hasProblem) {
|
|
||||||
newDeliveryStatus = "problem";
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePayload = {
|
const updatePayload = {
|
||||||
driver_shipment_data: shipmentData,
|
driver_shipment_data: shipmentData,
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,16 @@ Deno.serve(async (request) => {
|
||||||
groupUpdateData.pickup_time_slot = body.pickupTimeSlot || requestedSlot.deliveryTime || null;
|
groupUpdateData.pickup_time_slot = body.pickupTimeSlot || requestedSlot.deliveryTime || null;
|
||||||
// Pickup orders don't need a driver — clear assignment
|
// Pickup orders don't need a driver — clear assignment
|
||||||
groupUpdateData.assigned_driver_id = null;
|
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
|
const { error: groupUpdateError } = await supabase
|
||||||
|
|
@ -258,6 +268,7 @@ Deno.serve(async (request) => {
|
||||||
ok: true,
|
ok: true,
|
||||||
orderGroupId: invitation.order_group_id,
|
orderGroupId: invitation.order_group_id,
|
||||||
deliveryStatus: effectiveDeliveryStatus,
|
deliveryStatus: effectiveDeliveryStatus,
|
||||||
|
pickupCode: groupUpdateData.pickup_code || null,
|
||||||
},
|
},
|
||||||
200,
|
200,
|
||||||
corsHeaders,
|
corsHeaders,
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@ Deno.serve(async (request) => {
|
||||||
|
|
||||||
const { data: currentGroup, error: currentGroupError } = await supabase
|
const { data: currentGroup, error: currentGroupError } = await supabase
|
||||||
.from("order_groups")
|
.from("order_groups")
|
||||||
.select("id, delivery_status, delivery_invitation_id")
|
.select("id, delivery_status, delivery_invitation_id, pickup_code")
|
||||||
.eq("id", orderGroupId)
|
.eq("id", orderGroupId)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
|
|
@ -175,6 +175,11 @@ Deno.serve(async (request) => {
|
||||||
updateData.pickup_date = body.pickupDate || deliveryDate || null;
|
updateData.pickup_date = body.pickupDate || deliveryDate || null;
|
||||||
updateData.pickup_time_slot = body.pickupTimeSlot || deliveryTime || null;
|
updateData.pickup_time_slot = body.pickupTimeSlot || deliveryTime || null;
|
||||||
updateData.assigned_driver_id = 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 {
|
} else {
|
||||||
updateData.pickup_date = null;
|
updateData.pickup_date = null;
|
||||||
updateData.pickup_time_slot = null;
|
updateData.pickup_time_slot = null;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue