feat: show order composition collapsed with product list from source_orders
- OrderCompositionPanel now collapsed by default with position count badge - Flattens all products from source_orders into a single list - RPC migration to extract items from source_orders (needs DB deploy) - Edge Function updated to include source_orders items - Removed Edge Function enrichment hack from fetchDeliveryInvitation
This commit is contained in:
parent
e6dc1972fb
commit
9abfbff654
|
|
@ -0,0 +1,199 @@
|
|||
-- Migration: add source_orders items to get_delivery_invitation_by_token
|
||||
-- This replaces ONLY the orderItems building section for the group path.
|
||||
-- Apply AFTER the base function is restored.
|
||||
|
||||
-- Step 1: First restore the original function (run restore-rpc-original.sql if needed)
|
||||
-- Step 2: Then run this migration
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.get_delivery_invitation_by_token(p_token text)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public, extensions
|
||||
AS $$
|
||||
DECLARE
|
||||
v_invitation public.delivery_invitations%rowtype;
|
||||
v_group public.order_groups%rowtype;
|
||||
v_order record;
|
||||
v_token_hash text;
|
||||
v_state text;
|
||||
v_order_number text;
|
||||
v_customer_name text;
|
||||
v_customer_phone text;
|
||||
v_order_items jsonb;
|
||||
v_order_numbers jsonb;
|
||||
v_now timestamptz := timezone('utc', now());
|
||||
BEGIN
|
||||
IF nullif(trim(coalesce(p_token, '')), '') IS NULL THEN
|
||||
RAISE EXCEPTION 'token is required';
|
||||
END IF;
|
||||
|
||||
v_token_hash := encode(digest(p_token, 'sha256'), 'hex');
|
||||
|
||||
SELECT *
|
||||
INTO v_invitation
|
||||
FROM public.delivery_invitations
|
||||
WHERE token_hash = v_token_hash;
|
||||
|
||||
IF NOT found THEN
|
||||
RAISE EXCEPTION 'Invitation not found';
|
||||
END IF;
|
||||
|
||||
IF v_invitation.revoked_at IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'Invitation expired';
|
||||
END IF;
|
||||
|
||||
IF v_invitation.expires_at IS NOT NULL AND v_invitation.expires_at <= v_now THEN
|
||||
RAISE EXCEPTION 'Invitation expired';
|
||||
END IF;
|
||||
|
||||
IF v_invitation.order_group_id IS NOT NULL THEN
|
||||
SELECT *
|
||||
INTO v_group
|
||||
FROM public.order_groups
|
||||
WHERE id = v_invitation.order_group_id;
|
||||
|
||||
IF NOT found THEN
|
||||
RAISE EXCEPTION 'Order group not found';
|
||||
END IF;
|
||||
|
||||
v_state := CASE
|
||||
WHEN v_group.delivery_status = 'agreed' THEN 'agreed'
|
||||
WHEN v_group.delivery_status = 'delivered' THEN 'delivered'
|
||||
WHEN v_invitation.state IN ('awaiting_choice', 'opened', 'reminder_sent') THEN v_invitation.state
|
||||
ELSE 'default'
|
||||
END;
|
||||
|
||||
UPDATE public.delivery_invitations
|
||||
SET
|
||||
opened_at = CASE
|
||||
WHEN v_state IN ('awaiting_choice', 'opened', 'reminder_sent') AND opened_at IS NULL THEN v_now
|
||||
ELSE opened_at
|
||||
END,
|
||||
access_count = COALESCE(access_count, 0) + 1,
|
||||
last_accessed_at = v_now
|
||||
WHERE id = v_invitation.id
|
||||
RETURNING * INTO v_invitation;
|
||||
|
||||
v_order_number := COALESCE(
|
||||
NULLIF(v_invitation.order_number, ''),
|
||||
to_jsonb(v_group.order_numbers) ->> 0,
|
||||
NULLIF(v_group.group_key, '')
|
||||
);
|
||||
v_customer_name := COALESCE(
|
||||
NULLIF(v_group.customer_name, ''),
|
||||
NULLIF(v_invitation.customer_name, '')
|
||||
);
|
||||
v_customer_phone := COALESCE(
|
||||
NULLIF(v_group.customer_phone, ''),
|
||||
NULLIF(v_group.customer_phone_normalized, ''),
|
||||
NULLIF(v_invitation.customer_phone, '')
|
||||
);
|
||||
|
||||
-- Build orderItems: use source_orders for real product lines if available,
|
||||
-- otherwise fall back to invoice numbers from order_numbers.
|
||||
v_order_items := CASE
|
||||
WHEN v_group.source_orders IS NOT NULL
|
||||
AND jsonb_typeof(v_group.source_orders) = 'array'
|
||||
AND jsonb_array_length(v_group.source_orders) > 0
|
||||
THEN COALESCE(
|
||||
(SELECT jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'name', COALESCE(src ->> 'nom', src ->> 'name', ''),
|
||||
'quantity', '',
|
||||
'items', COALESCE(src -> 'orderList', src -> 'items', '[]'::jsonb)
|
||||
)
|
||||
) FROM jsonb_array_elements(v_group.source_orders) AS src),
|
||||
'[]'::jsonb
|
||||
)
|
||||
ELSE COALESCE(
|
||||
(SELECT jsonb_agg(jsonb_build_object('name', order_number, 'quantity', ''))
|
||||
FROM jsonb_array_elements_text(
|
||||
CASE
|
||||
WHEN jsonb_typeof(to_jsonb(v_group.order_numbers)) = 'array' THEN to_jsonb(v_group.order_numbers)
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
) AS order_number),
|
||||
'[]'::jsonb
|
||||
)
|
||||
END;
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'ok', TRUE,
|
||||
'invitation', jsonb_build_object(
|
||||
'orderId', COALESCE(v_invitation.order_group_id, v_group.id)::text,
|
||||
'orderGroupId', COALESCE(v_invitation.order_group_id, v_group.id)::text,
|
||||
'state', v_state,
|
||||
'token', p_token,
|
||||
'orderNumber', v_order_number,
|
||||
'customerName', v_customer_name,
|
||||
'customerPhone', v_customer_phone,
|
||||
'orderItems', v_order_items,
|
||||
'availableSlots', COALESCE(to_jsonb(v_invitation.available_slots), '[]'::jsonb),
|
||||
'deliveryDate', v_invitation.delivery_date,
|
||||
'deliveryTime', v_invitation.delivery_time,
|
||||
'orderStatus', NULL,
|
||||
'deliveryAgreementStatus', NULL
|
||||
)
|
||||
);
|
||||
END IF;
|
||||
|
||||
SELECT id, order_number, status, delivery_agreement_status, customer
|
||||
INTO v_order
|
||||
FROM public.orders
|
||||
WHERE id = v_invitation.order_id;
|
||||
|
||||
IF NOT found THEN
|
||||
RAISE EXCEPTION 'Order not found';
|
||||
END IF;
|
||||
|
||||
v_state := CASE v_order.status
|
||||
WHEN 'Ожидает ответа клиента' THEN 'awaiting_choice'
|
||||
WHEN 'Ожидает согласования доставки' THEN 'opened'
|
||||
WHEN 'Напоминание отправлено' THEN 'reminder_sent'
|
||||
WHEN 'Переход отправлен' THEN 'reminder_sent'
|
||||
WHEN 'Передан логисту' THEN 'transferred_to_logistics'
|
||||
WHEN 'Платное хранение' THEN 'paid_storage'
|
||||
WHEN 'Доставлен' THEN 'delivered'
|
||||
WHEN 'Доставка согласована' THEN 'agreed'
|
||||
ELSE 'default'
|
||||
END;
|
||||
|
||||
UPDATE public.delivery_invitations
|
||||
SET
|
||||
opened_at = CASE
|
||||
WHEN v_state IN ('awaiting_choice', 'opened', 'reminder_sent') AND opened_at IS NULL THEN v_now
|
||||
ELSE opened_at
|
||||
END,
|
||||
access_count = COALESCE(access_count, 0) + 1,
|
||||
last_accessed_at = v_now
|
||||
WHERE id = v_invitation.id
|
||||
RETURNING * INTO v_invitation;
|
||||
|
||||
v_order_items := CASE
|
||||
WHEN jsonb_typeof(v_order.customer -> 'items') = 'array' THEN v_order.customer -> 'items'
|
||||
ELSE '[]'::jsonb
|
||||
END;
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'ok', TRUE,
|
||||
'invitation', jsonb_build_object(
|
||||
'orderId', v_invitation.order_id::text,
|
||||
'state', v_state,
|
||||
'token', p_token,
|
||||
'orderNumber', COALESCE(NULLIF(v_order.order_number, ''), NULLIF(v_invitation.order_number, '')),
|
||||
'customerName', COALESCE(NULLIF(v_order.customer ->> 'name', ''), NULLIF(v_invitation.customer_name, '')),
|
||||
'customerPhone', COALESCE(NULLIF(v_order.customer ->> 'phone', ''), NULLIF(v_invitation.customer_phone, '')),
|
||||
'orderItems', v_order_items,
|
||||
'availableSlots', COALESCE(to_jsonb(v_invitation.available_slots), '[]'::jsonb),
|
||||
'deliveryDate', v_invitation.delivery_date,
|
||||
'deliveryTime', v_invitation.delivery_time,
|
||||
'orderStatus', v_order.status,
|
||||
'deliveryAgreementStatus', v_order.delivery_agreement_status
|
||||
)
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.get_delivery_invitation_by_token(text) FROM public;
|
||||
GRANT EXECUTE ON FUNCTION public.get_delivery_invitation_by_token(text) TO anon, authenticated;
|
||||
|
|
@ -1,12 +1,17 @@
|
|||
create extension if not exists pgcrypto;
|
||||
-- Migration: add source_orders items to get_delivery_invitation_by_token
|
||||
-- This replaces ONLY the orderItems building section for the group path.
|
||||
-- Apply AFTER the base function is restored.
|
||||
|
||||
create or replace function public.get_delivery_invitation_by_token(p_token text)
|
||||
returns jsonb
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public, extensions
|
||||
as $$
|
||||
declare
|
||||
-- Step 1: First restore the original function (run restore-rpc-original.sql if needed)
|
||||
-- Step 2: Then run this migration
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.get_delivery_invitation_by_token(p_token text)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public, extensions
|
||||
AS $$
|
||||
DECLARE
|
||||
v_invitation public.delivery_invitations%rowtype;
|
||||
v_group public.order_groups%rowtype;
|
||||
v_order record;
|
||||
|
|
@ -18,291 +23,275 @@ declare
|
|||
v_order_items jsonb;
|
||||
v_order_numbers jsonb;
|
||||
v_now timestamptz := timezone('utc', now());
|
||||
begin
|
||||
if nullif(trim(coalesce(p_token, '')), '') is null then
|
||||
raise exception 'token is required';
|
||||
end if;
|
||||
BEGIN
|
||||
IF nullif(trim(coalesce(p_token, '')), '') IS NULL THEN
|
||||
RAISE EXCEPTION 'token is required';
|
||||
END IF;
|
||||
|
||||
v_token_hash := encode(digest(p_token, 'sha256'), 'hex');
|
||||
|
||||
select *
|
||||
into v_invitation
|
||||
from public.delivery_invitations
|
||||
where token_hash = v_token_hash;
|
||||
SELECT *
|
||||
INTO v_invitation
|
||||
FROM public.delivery_invitations
|
||||
WHERE token_hash = v_token_hash;
|
||||
|
||||
if not found then
|
||||
raise exception 'Invitation not found';
|
||||
end if;
|
||||
IF NOT found THEN
|
||||
RAISE EXCEPTION 'Invitation not found';
|
||||
END IF;
|
||||
|
||||
if v_invitation.revoked_at is not null then
|
||||
raise exception 'Invitation expired';
|
||||
end if;
|
||||
IF v_invitation.revoked_at IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'Invitation expired';
|
||||
END IF;
|
||||
|
||||
if v_invitation.expires_at is not null and v_invitation.expires_at <= v_now then
|
||||
raise exception 'Invitation expired';
|
||||
end if;
|
||||
IF v_invitation.expires_at IS NOT NULL AND v_invitation.expires_at <= v_now THEN
|
||||
RAISE EXCEPTION 'Invitation expired';
|
||||
END IF;
|
||||
|
||||
if v_invitation.order_group_id is not null then
|
||||
select *
|
||||
into v_group
|
||||
from public.order_groups
|
||||
where id = v_invitation.order_group_id;
|
||||
IF v_invitation.order_group_id IS NOT NULL THEN
|
||||
SELECT *
|
||||
INTO v_group
|
||||
FROM public.order_groups
|
||||
WHERE id = v_invitation.order_group_id;
|
||||
|
||||
if not found then
|
||||
raise exception 'Order group not found';
|
||||
end if;
|
||||
IF NOT found THEN
|
||||
RAISE EXCEPTION 'Order group not found';
|
||||
END IF;
|
||||
|
||||
v_state := case
|
||||
when v_group.delivery_status = 'agreed' then 'agreed'
|
||||
when v_group.delivery_status = 'delivered' then 'delivered'
|
||||
when v_invitation.state in ('awaiting_choice', 'opened', 'reminder_sent') then v_invitation.state
|
||||
else 'default'
|
||||
end;
|
||||
v_state := CASE
|
||||
WHEN v_group.delivery_status = 'agreed' THEN 'agreed'
|
||||
WHEN v_group.delivery_status = 'delivered' THEN 'delivered'
|
||||
WHEN v_invitation.state IN ('awaiting_choice', 'opened', 'reminder_sent') THEN v_invitation.state
|
||||
ELSE 'default'
|
||||
END;
|
||||
|
||||
update public.delivery_invitations
|
||||
set
|
||||
opened_at = case
|
||||
when v_state in ('awaiting_choice', 'opened', 'reminder_sent') and opened_at is null then v_now
|
||||
else opened_at
|
||||
end,
|
||||
access_count = coalesce(access_count, 0) + 1,
|
||||
UPDATE public.delivery_invitations
|
||||
SET
|
||||
opened_at = CASE
|
||||
WHEN v_state IN ('awaiting_choice', 'opened', 'reminder_sent') AND opened_at IS NULL THEN v_now
|
||||
ELSE opened_at
|
||||
END,
|
||||
access_count = COALESCE(access_count, 0) + 1,
|
||||
last_accessed_at = v_now
|
||||
where id = v_invitation.id
|
||||
returning * into v_invitation;
|
||||
WHERE id = v_invitation.id
|
||||
RETURNING * INTO v_invitation;
|
||||
|
||||
v_order_number := coalesce(
|
||||
nullif(v_invitation.order_number, ''),
|
||||
v_order_number := COALESCE(
|
||||
NULLIF(v_invitation.order_number, ''),
|
||||
to_jsonb(v_group.order_numbers) ->> 0,
|
||||
nullif(v_group.group_key, '')
|
||||
NULLIF(v_group.group_key, '')
|
||||
);
|
||||
v_customer_name := coalesce(
|
||||
nullif(v_group.customer_name, ''),
|
||||
nullif(v_invitation.customer_name, '')
|
||||
v_customer_name := COALESCE(
|
||||
NULLIF(v_group.customer_name, ''),
|
||||
NULLIF(v_invitation.customer_name, '')
|
||||
);
|
||||
v_customer_phone := coalesce(
|
||||
nullif(v_group.customer_phone, ''),
|
||||
nullif(v_group.customer_phone_normalized, ''),
|
||||
nullif(v_invitation.customer_phone, '')
|
||||
v_customer_phone := COALESCE(
|
||||
NULLIF(v_group.customer_phone, ''),
|
||||
NULLIF(v_group.customer_phone_normalized, ''),
|
||||
NULLIF(v_invitation.customer_phone, '')
|
||||
);
|
||||
-- Build orderItems from source_orders if available (real product lines),
|
||||
-- otherwise fall back to invoice numbers from order_numbers.
|
||||
v_order_items := case
|
||||
when v_group.source_orders is not null
|
||||
and jsonb_typeof(v_group.source_orders) = 'array'
|
||||
and jsonb_array_length(v_group.source_orders) > 0
|
||||
then
|
||||
coalesce(
|
||||
(select jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'name', coalesce(src ->> 'nom', src ->> 'name', onum),
|
||||
'quantity', '',
|
||||
'items', coalesce(
|
||||
src -> 'orderList',
|
||||
src -> 'items',
|
||||
'[]'::jsonb
|
||||
)
|
||||
)
|
||||
)
|
||||
from jsonb_array_elements(v_group.source_orders) with ordinality as t(src, idx)
|
||||
cross join lateral (
|
||||
select coalesce(
|
||||
nullif(v_group.order_numbers[idx], ''),
|
||||
src ->> 'nom',
|
||||
src ->> 'name',
|
||||
''
|
||||
) as onum
|
||||
) as names
|
||||
),
|
||||
'[]'::jsonb
|
||||
)
|
||||
else
|
||||
coalesce(
|
||||
(select jsonb_agg(jsonb_build_object('name', order_number, 'quantity', ''))
|
||||
from jsonb_array_elements_text(
|
||||
case
|
||||
when jsonb_typeof(to_jsonb(v_group.order_numbers)) = 'array' then to_jsonb(v_group.order_numbers)
|
||||
else '[]'::jsonb
|
||||
end
|
||||
) as order_number),
|
||||
'[]'::jsonb
|
||||
)
|
||||
end;
|
||||
|
||||
return jsonb_build_object(
|
||||
'ok', true,
|
||||
-- Build orderItems: use source_orders for real product lines if available,
|
||||
-- otherwise fall back to invoice numbers from order_numbers.
|
||||
v_order_items := CASE
|
||||
WHEN v_group.source_orders IS NOT NULL
|
||||
AND jsonb_typeof(v_group.source_orders) = 'array'
|
||||
AND jsonb_array_length(v_group.source_orders) > 0
|
||||
THEN COALESCE(
|
||||
(SELECT jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'name', COALESCE(src ->> 'nom', src ->> 'name', ''),
|
||||
'quantity', '',
|
||||
'items', COALESCE(src -> 'orderList', src -> 'items', '[]'::jsonb)
|
||||
)
|
||||
) FROM jsonb_array_elements(v_group.source_orders) AS src),
|
||||
'[]'::jsonb
|
||||
)
|
||||
ELSE COALESCE(
|
||||
(SELECT jsonb_agg(jsonb_build_object('name', onum, 'quantity', ''))
|
||||
FROM unnest(v_group.order_numbers) AS onum
|
||||
WHERE onum IS NOT NULL AND onum <> ''),
|
||||
'[]'::jsonb
|
||||
)
|
||||
END;
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'ok', TRUE,
|
||||
'invitation', jsonb_build_object(
|
||||
'orderId', coalesce(v_invitation.order_group_id, v_group.id)::text,
|
||||
'orderGroupId', coalesce(v_invitation.order_group_id, v_group.id)::text,
|
||||
'orderId', COALESCE(v_invitation.order_group_id, v_group.id)::text,
|
||||
'orderGroupId', COALESCE(v_invitation.order_group_id, v_group.id)::text,
|
||||
'state', v_state,
|
||||
'token', p_token,
|
||||
'orderNumber', v_order_number,
|
||||
'customerName', v_customer_name,
|
||||
'customerPhone', v_customer_phone,
|
||||
'orderItems', v_order_items,
|
||||
'availableSlots', coalesce(to_jsonb(v_invitation.available_slots), '[]'::jsonb),
|
||||
'availableSlots', COALESCE(to_jsonb(v_invitation.available_slots), '[]'::jsonb),
|
||||
'deliveryDate', v_invitation.delivery_date,
|
||||
'deliveryTime', v_invitation.delivery_time,
|
||||
'orderStatus', null,
|
||||
'deliveryAgreementStatus', null
|
||||
'orderStatus', NULL,
|
||||
'deliveryAgreementStatus', NULL
|
||||
)
|
||||
);
|
||||
end if;
|
||||
END IF;
|
||||
|
||||
select id, order_number, status, delivery_agreement_status, customer
|
||||
into v_order
|
||||
from public.orders
|
||||
where id = v_invitation.order_id;
|
||||
SELECT id, order_number, status, delivery_agreement_status, customer
|
||||
INTO v_order
|
||||
FROM public.orders
|
||||
WHERE id = v_invitation.order_id;
|
||||
|
||||
if not found then
|
||||
raise exception 'Order not found';
|
||||
end if;
|
||||
IF NOT found THEN
|
||||
RAISE EXCEPTION 'Order not found';
|
||||
END IF;
|
||||
|
||||
v_state := case v_order.status
|
||||
when 'Ожидает ответа клиента' then 'awaiting_choice'
|
||||
when 'Ожидает согласования доставки' then 'opened'
|
||||
when 'Напоминание отправлено' then 'reminder_sent'
|
||||
when 'Переход отправлен' then 'reminder_sent'
|
||||
when 'Передан логисту' then 'transferred_to_logistics'
|
||||
when 'Платное хранение' then 'paid_storage'
|
||||
when 'Доставлен' then 'delivered'
|
||||
when 'Доставка согласована' then 'agreed'
|
||||
else 'default'
|
||||
end;
|
||||
v_state := CASE v_order.status
|
||||
WHEN 'Ожидает ответа клиента' THEN 'awaiting_choice'
|
||||
WHEN 'Ожидает согласования доставки' THEN 'opened'
|
||||
WHEN 'Напоминание отправлено' THEN 'reminder_sent'
|
||||
WHEN 'Переход отправлен' THEN 'reminder_sent'
|
||||
WHEN 'Передан логисту' THEN 'transferred_to_logistics'
|
||||
WHEN 'Платное хранение' THEN 'paid_storage'
|
||||
WHEN 'Доставлен' THEN 'delivered'
|
||||
WHEN 'Доставка согласована' THEN 'agreed'
|
||||
ELSE 'default'
|
||||
END;
|
||||
|
||||
update public.delivery_invitations
|
||||
set
|
||||
opened_at = case
|
||||
when v_state in ('awaiting_choice', 'opened', 'reminder_sent') and opened_at is null then v_now
|
||||
else opened_at
|
||||
end,
|
||||
access_count = coalesce(access_count, 0) + 1,
|
||||
UPDATE public.delivery_invitations
|
||||
SET
|
||||
opened_at = CASE
|
||||
WHEN v_state IN ('awaiting_choice', 'opened', 'reminder_sent') AND opened_at IS NULL THEN v_now
|
||||
ELSE opened_at
|
||||
END,
|
||||
access_count = COALESCE(access_count, 0) + 1,
|
||||
last_accessed_at = v_now
|
||||
where id = v_invitation.id
|
||||
returning * into v_invitation;
|
||||
WHERE id = v_invitation.id
|
||||
RETURNING * INTO v_invitation;
|
||||
|
||||
v_order_items := case
|
||||
when jsonb_typeof(v_order.customer -> 'items') = 'array' then v_order.customer -> 'items'
|
||||
else '[]'::jsonb
|
||||
end;
|
||||
v_order_items := CASE
|
||||
WHEN jsonb_typeof(v_order.customer -> 'items') = 'array' THEN v_order.customer -> 'items'
|
||||
ELSE '[]'::jsonb
|
||||
END;
|
||||
|
||||
return jsonb_build_object(
|
||||
'ok', true,
|
||||
RETURN jsonb_build_object(
|
||||
'ok', TRUE,
|
||||
'invitation', jsonb_build_object(
|
||||
'orderId', v_invitation.order_id::text,
|
||||
'state', v_state,
|
||||
'token', p_token,
|
||||
'orderNumber', coalesce(nullif(v_order.order_number, ''), nullif(v_invitation.order_number, '')),
|
||||
'customerName', coalesce(nullif(v_order.customer ->> 'name', ''), nullif(v_invitation.customer_name, '')),
|
||||
'customerPhone', coalesce(nullif(v_order.customer ->> 'phone', ''), nullif(v_invitation.customer_phone, '')),
|
||||
'orderNumber', COALESCE(NULLIF(v_order.order_number, ''), NULLIF(v_invitation.order_number, '')),
|
||||
'customerName', COALESCE(NULLIF(v_order.customer ->> 'name', ''), NULLIF(v_invitation.customer_name, '')),
|
||||
'customerPhone', COALESCE(NULLIF(v_order.customer ->> 'phone', ''), NULLIF(v_invitation.customer_phone, '')),
|
||||
'orderItems', v_order_items,
|
||||
'availableSlots', coalesce(to_jsonb(v_invitation.available_slots), '[]'::jsonb),
|
||||
'availableSlots', COALESCE(to_jsonb(v_invitation.available_slots), '[]'::jsonb),
|
||||
'deliveryDate', v_invitation.delivery_date,
|
||||
'deliveryTime', v_invitation.delivery_time,
|
||||
'orderStatus', v_order.status,
|
||||
'deliveryAgreementStatus', v_order.delivery_agreement_status
|
||||
)
|
||||
);
|
||||
end;
|
||||
END;
|
||||
$$;
|
||||
|
||||
create or replace function public.confirm_delivery_choice_by_token(
|
||||
REVOKE ALL ON FUNCTION public.get_delivery_invitation_by_token(text) FROM public;
|
||||
GRANT EXECUTE ON FUNCTION public.get_delivery_invitation_by_token(text) TO anon, authenticated;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.confirm_delivery_choice_by_token(
|
||||
p_token text,
|
||||
p_delivery_date date,
|
||||
p_delivery_time text
|
||||
)
|
||||
returns jsonb
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public, extensions
|
||||
as $$
|
||||
declare
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public, extensions
|
||||
AS $$
|
||||
DECLARE
|
||||
v_invitation public.delivery_invitations%rowtype;
|
||||
v_group public.order_groups%rowtype;
|
||||
v_order record;
|
||||
v_token_hash text;
|
||||
v_slot_label text;
|
||||
v_now timestamptz := timezone('utc', now());
|
||||
begin
|
||||
if nullif(trim(coalesce(p_token, '')), '') is null then
|
||||
raise exception 'token is required';
|
||||
end if;
|
||||
BEGIN
|
||||
IF nullif(trim(coalesce(p_token, '')), '') IS NULL THEN
|
||||
RAISE EXCEPTION 'token is required';
|
||||
END IF;
|
||||
|
||||
if p_delivery_date is null or nullif(trim(coalesce(p_delivery_time, '')), '') is null then
|
||||
raise exception 'Selected slot is not available';
|
||||
end if;
|
||||
IF p_delivery_date IS NULL OR nullif(trim(coalesce(p_delivery_time, '')), '') IS NULL THEN
|
||||
RAISE EXCEPTION 'Selected slot is not available';
|
||||
END IF;
|
||||
|
||||
v_token_hash := encode(digest(p_token, 'sha256'), 'hex');
|
||||
v_slot_label := concat(p_delivery_date::text, ', ', trim(p_delivery_time));
|
||||
|
||||
select *
|
||||
into v_invitation
|
||||
from public.delivery_invitations
|
||||
where token_hash = v_token_hash
|
||||
for update;
|
||||
SELECT *
|
||||
INTO v_invitation
|
||||
FROM public.delivery_invitations
|
||||
WHERE token_hash = v_token_hash
|
||||
FOR UPDATE;
|
||||
|
||||
if not found then
|
||||
raise exception 'Invitation not found';
|
||||
end if;
|
||||
IF NOT found THEN
|
||||
RAISE EXCEPTION 'Invitation not found';
|
||||
END IF;
|
||||
|
||||
if v_invitation.revoked_at is not null then
|
||||
raise exception 'Invitation expired';
|
||||
end if;
|
||||
IF v_invitation.revoked_at IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'Invitation expired';
|
||||
END IF;
|
||||
|
||||
if v_invitation.expires_at is not null and v_invitation.expires_at <= v_now then
|
||||
raise exception 'Invitation expired';
|
||||
end if;
|
||||
IF v_invitation.expires_at IS NOT NULL AND v_invitation.expires_at <= v_now THEN
|
||||
RAISE EXCEPTION 'Invitation expired';
|
||||
END IF;
|
||||
|
||||
if v_invitation.state not in ('awaiting_choice', 'opened', 'reminder_sent') then
|
||||
raise exception 'Invitation is no longer active';
|
||||
end if;
|
||||
IF v_invitation.state NOT IN ('awaiting_choice', 'opened', 'reminder_sent') THEN
|
||||
RAISE EXCEPTION 'Invitation is no longer active';
|
||||
END IF;
|
||||
|
||||
if cardinality(v_invitation.available_slots) > 0 and not (v_slot_label = any(v_invitation.available_slots)) then
|
||||
raise exception 'Selected slot is not available';
|
||||
end if;
|
||||
IF cardinality(v_invitation.available_slots) > 0 AND NOT (v_slot_label = ANY(v_invitation.available_slots)) THEN
|
||||
RAISE EXCEPTION 'Selected slot is not available';
|
||||
END IF;
|
||||
|
||||
if v_invitation.order_group_id is not null then
|
||||
select *
|
||||
into v_group
|
||||
from public.order_groups
|
||||
where id = v_invitation.order_group_id
|
||||
for update;
|
||||
IF v_invitation.order_group_id IS NOT NULL THEN
|
||||
SELECT *
|
||||
INTO v_group
|
||||
FROM public.order_groups
|
||||
WHERE id = v_invitation.order_group_id
|
||||
FOR UPDATE;
|
||||
|
||||
if not found then
|
||||
raise exception 'Order group not found';
|
||||
end if;
|
||||
IF NOT found THEN
|
||||
RAISE EXCEPTION 'Order group not found';
|
||||
END IF;
|
||||
|
||||
if v_group.delivery_status <> 'pending_confirmation' then
|
||||
raise exception 'Invitation is no longer active';
|
||||
end if;
|
||||
IF v_group.delivery_status <> 'pending_confirmation' THEN
|
||||
RAISE EXCEPTION 'Invitation is no longer active';
|
||||
END IF;
|
||||
|
||||
update public.delivery_invitations
|
||||
set
|
||||
UPDATE public.delivery_invitations
|
||||
SET
|
||||
state = 'agreed',
|
||||
delivery_date = p_delivery_date,
|
||||
delivery_time = trim(p_delivery_time),
|
||||
confirmed_at = v_now,
|
||||
access_count = coalesce(access_count, 0) + 1,
|
||||
access_count = COALESCE(access_count, 0) + 1,
|
||||
last_accessed_at = v_now
|
||||
where id = v_invitation.id;
|
||||
WHERE id = v_invitation.id;
|
||||
|
||||
update public.order_groups
|
||||
set
|
||||
UPDATE public.order_groups
|
||||
SET
|
||||
delivery_status = 'agreed',
|
||||
delivery_date = p_delivery_date,
|
||||
delivery_time = trim(p_delivery_time),
|
||||
notification_status = 'confirmed',
|
||||
updated_at = v_now
|
||||
where id = v_group.id;
|
||||
WHERE id = v_group.id;
|
||||
|
||||
insert into public.integration_events (
|
||||
INSERT INTO public.integration_events (
|
||||
order_id,
|
||||
event_type,
|
||||
direction,
|
||||
status,
|
||||
payload
|
||||
)
|
||||
values (
|
||||
null,
|
||||
VALUES (
|
||||
NULL,
|
||||
'delivery_choice_confirmed',
|
||||
'inbound',
|
||||
'success',
|
||||
|
|
@ -314,66 +303,66 @@ begin
|
|||
)
|
||||
);
|
||||
|
||||
return jsonb_build_object(
|
||||
'ok', true,
|
||||
RETURN jsonb_build_object(
|
||||
'ok', TRUE,
|
||||
'orderGroupId', v_group.id,
|
||||
'deliveryStatus', 'agreed'
|
||||
);
|
||||
end if;
|
||||
END IF;
|
||||
|
||||
select id, status, delivery_agreement_status
|
||||
into v_order
|
||||
from public.orders
|
||||
where id = v_invitation.order_id
|
||||
for update;
|
||||
SELECT id, status, delivery_agreement_status
|
||||
INTO v_order
|
||||
FROM public.orders
|
||||
WHERE id = v_invitation.order_id
|
||||
FOR UPDATE;
|
||||
|
||||
if not found then
|
||||
raise exception 'Order not found';
|
||||
end if;
|
||||
IF NOT found THEN
|
||||
RAISE EXCEPTION 'Order not found';
|
||||
END IF;
|
||||
|
||||
if v_order.status not in ('Ожидает ответа клиента', 'Ожидает согласования доставки') then
|
||||
raise exception 'Invitation is no longer active';
|
||||
end if;
|
||||
IF v_order.status NOT IN ('Ожидает ответа клиента', 'Ожидает согласования доставки') THEN
|
||||
RAISE EXCEPTION 'Invitation is no longer active';
|
||||
END IF;
|
||||
|
||||
update public.delivery_invitations
|
||||
set
|
||||
UPDATE public.delivery_invitations
|
||||
SET
|
||||
state = 'agreed',
|
||||
delivery_date = p_delivery_date,
|
||||
delivery_time = trim(p_delivery_time),
|
||||
confirmed_at = v_now,
|
||||
access_count = coalesce(access_count, 0) + 1,
|
||||
access_count = COALESCE(access_count, 0) + 1,
|
||||
last_accessed_at = v_now
|
||||
where id = v_invitation.id;
|
||||
WHERE id = v_invitation.id;
|
||||
|
||||
update public.orders
|
||||
set
|
||||
UPDATE public.orders
|
||||
SET
|
||||
status = 'Доставка согласована',
|
||||
delivery_agreement_status = 'Подтверждено клиентом'
|
||||
where id = v_order.id;
|
||||
WHERE id = v_order.id;
|
||||
|
||||
insert into public.delivery_slots (
|
||||
INSERT INTO public.delivery_slots (
|
||||
order_id,
|
||||
delivery_date,
|
||||
delivery_time,
|
||||
logistician_id,
|
||||
status
|
||||
)
|
||||
values (
|
||||
VALUES (
|
||||
v_order.id,
|
||||
p_delivery_date,
|
||||
trim(p_delivery_time),
|
||||
null,
|
||||
NULL,
|
||||
'confirmed_by_client'
|
||||
);
|
||||
|
||||
insert into public.order_history (
|
||||
INSERT INTO public.order_history (
|
||||
order_id,
|
||||
action,
|
||||
old_status,
|
||||
new_status,
|
||||
metadata
|
||||
)
|
||||
values (
|
||||
VALUES (
|
||||
v_order.id,
|
||||
'Подтверждение выбора доставки клиентом',
|
||||
v_order.status,
|
||||
|
|
@ -386,14 +375,14 @@ begin
|
|||
)
|
||||
);
|
||||
|
||||
insert into public.integration_events (
|
||||
INSERT INTO public.integration_events (
|
||||
order_id,
|
||||
event_type,
|
||||
direction,
|
||||
status,
|
||||
payload
|
||||
)
|
||||
values (
|
||||
VALUES (
|
||||
v_order.id,
|
||||
'delivery_choice_confirmed',
|
||||
'inbound',
|
||||
|
|
@ -404,17 +393,14 @@ begin
|
|||
)
|
||||
);
|
||||
|
||||
return jsonb_build_object(
|
||||
'ok', true,
|
||||
RETURN jsonb_build_object(
|
||||
'ok', TRUE,
|
||||
'orderId', v_order.id,
|
||||
'status', 'Доставка согласована',
|
||||
'deliveryAgreementStatus', 'Подтверждено клиентом'
|
||||
);
|
||||
end;
|
||||
END;
|
||||
$$;
|
||||
|
||||
revoke all on function public.get_delivery_invitation_by_token(text) from public;
|
||||
grant execute on function public.get_delivery_invitation_by_token(text) to anon, authenticated;
|
||||
|
||||
revoke all on function public.confirm_delivery_choice_by_token(text, date, text) from public;
|
||||
grant execute on function public.confirm_delivery_choice_by_token(text, date, text) to anon, authenticated;
|
||||
REVOKE ALL ON FUNCTION public.confirm_delivery_choice_by_token(text, date, text) FROM public;
|
||||
GRANT EXECUTE ON FUNCTION public.confirm_delivery_choice_by_token(text, date, text) TO anon, authenticated;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,199 @@
|
|||
-- Migration: add source_orders items to get_delivery_invitation_by_token
|
||||
-- This replaces ONLY the orderItems building section for the group path.
|
||||
-- Apply AFTER the base function is restored.
|
||||
|
||||
-- Step 1: First restore the original function (run restore-rpc-original.sql if needed)
|
||||
-- Step 2: Then run this migration
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.get_delivery_invitation_by_token(p_token text)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public, extensions
|
||||
AS $$
|
||||
DECLARE
|
||||
v_invitation public.delivery_invitations%rowtype;
|
||||
v_group public.order_groups%rowtype;
|
||||
v_order record;
|
||||
v_token_hash text;
|
||||
v_state text;
|
||||
v_order_number text;
|
||||
v_customer_name text;
|
||||
v_customer_phone text;
|
||||
v_order_items jsonb;
|
||||
v_order_numbers jsonb;
|
||||
v_now timestamptz := timezone('utc', now());
|
||||
BEGIN
|
||||
IF nullif(trim(coalesce(p_token, '')), '') IS NULL THEN
|
||||
RAISE EXCEPTION 'token is required';
|
||||
END IF;
|
||||
|
||||
v_token_hash := encode(digest(p_token, 'sha256'), 'hex');
|
||||
|
||||
SELECT *
|
||||
INTO v_invitation
|
||||
FROM public.delivery_invitations
|
||||
WHERE token_hash = v_token_hash;
|
||||
|
||||
IF NOT found THEN
|
||||
RAISE EXCEPTION 'Invitation not found';
|
||||
END IF;
|
||||
|
||||
IF v_invitation.revoked_at IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'Invitation expired';
|
||||
END IF;
|
||||
|
||||
IF v_invitation.expires_at IS NOT NULL AND v_invitation.expires_at <= v_now THEN
|
||||
RAISE EXCEPTION 'Invitation expired';
|
||||
END IF;
|
||||
|
||||
IF v_invitation.order_group_id IS NOT NULL THEN
|
||||
SELECT *
|
||||
INTO v_group
|
||||
FROM public.order_groups
|
||||
WHERE id = v_invitation.order_group_id;
|
||||
|
||||
IF NOT found THEN
|
||||
RAISE EXCEPTION 'Order group not found';
|
||||
END IF;
|
||||
|
||||
v_state := CASE
|
||||
WHEN v_group.delivery_status = 'agreed' THEN 'agreed'
|
||||
WHEN v_group.delivery_status = 'delivered' THEN 'delivered'
|
||||
WHEN v_invitation.state IN ('awaiting_choice', 'opened', 'reminder_sent') THEN v_invitation.state
|
||||
ELSE 'default'
|
||||
END;
|
||||
|
||||
UPDATE public.delivery_invitations
|
||||
SET
|
||||
opened_at = CASE
|
||||
WHEN v_state IN ('awaiting_choice', 'opened', 'reminder_sent') AND opened_at IS NULL THEN v_now
|
||||
ELSE opened_at
|
||||
END,
|
||||
access_count = COALESCE(access_count, 0) + 1,
|
||||
last_accessed_at = v_now
|
||||
WHERE id = v_invitation.id
|
||||
RETURNING * INTO v_invitation;
|
||||
|
||||
v_order_number := COALESCE(
|
||||
NULLIF(v_invitation.order_number, ''),
|
||||
to_jsonb(v_group.order_numbers) ->> 0,
|
||||
NULLIF(v_group.group_key, '')
|
||||
);
|
||||
v_customer_name := COALESCE(
|
||||
NULLIF(v_group.customer_name, ''),
|
||||
NULLIF(v_invitation.customer_name, '')
|
||||
);
|
||||
v_customer_phone := COALESCE(
|
||||
NULLIF(v_group.customer_phone, ''),
|
||||
NULLIF(v_group.customer_phone_normalized, ''),
|
||||
NULLIF(v_invitation.customer_phone, '')
|
||||
);
|
||||
|
||||
-- Build orderItems: use source_orders for real product lines if available,
|
||||
-- otherwise fall back to invoice numbers from order_numbers.
|
||||
v_order_items := CASE
|
||||
WHEN v_group.source_orders IS NOT NULL
|
||||
AND jsonb_typeof(v_group.source_orders) = 'array'
|
||||
AND jsonb_array_length(v_group.source_orders) > 0
|
||||
THEN COALESCE(
|
||||
(SELECT jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'name', COALESCE(src ->> 'nom', src ->> 'name', ''),
|
||||
'quantity', '',
|
||||
'items', COALESCE(src -> 'orderList', src -> 'items', '[]'::jsonb)
|
||||
)
|
||||
) FROM jsonb_array_elements(v_group.source_orders) AS src),
|
||||
'[]'::jsonb
|
||||
)
|
||||
ELSE COALESCE(
|
||||
(SELECT jsonb_agg(jsonb_build_object('name', order_number, 'quantity', ''))
|
||||
FROM jsonb_array_elements_text(
|
||||
CASE
|
||||
WHEN jsonb_typeof(to_jsonb(v_group.order_numbers)) = 'array' THEN to_jsonb(v_group.order_numbers)
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
) AS order_number),
|
||||
'[]'::jsonb
|
||||
)
|
||||
END;
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'ok', TRUE,
|
||||
'invitation', jsonb_build_object(
|
||||
'orderId', COALESCE(v_invitation.order_group_id, v_group.id)::text,
|
||||
'orderGroupId', COALESCE(v_invitation.order_group_id, v_group.id)::text,
|
||||
'state', v_state,
|
||||
'token', p_token,
|
||||
'orderNumber', v_order_number,
|
||||
'customerName', v_customer_name,
|
||||
'customerPhone', v_customer_phone,
|
||||
'orderItems', v_order_items,
|
||||
'availableSlots', COALESCE(to_jsonb(v_invitation.available_slots), '[]'::jsonb),
|
||||
'deliveryDate', v_invitation.delivery_date,
|
||||
'deliveryTime', v_invitation.delivery_time,
|
||||
'orderStatus', NULL,
|
||||
'deliveryAgreementStatus', NULL
|
||||
)
|
||||
);
|
||||
END IF;
|
||||
|
||||
SELECT id, order_number, status, delivery_agreement_status, customer
|
||||
INTO v_order
|
||||
FROM public.orders
|
||||
WHERE id = v_invitation.order_id;
|
||||
|
||||
IF NOT found THEN
|
||||
RAISE EXCEPTION 'Order not found';
|
||||
END IF;
|
||||
|
||||
v_state := CASE v_order.status
|
||||
WHEN 'Ожидает ответа клиента' THEN 'awaiting_choice'
|
||||
WHEN 'Ожидает согласования доставки' THEN 'opened'
|
||||
WHEN 'Напоминание отправлено' THEN 'reminder_sent'
|
||||
WHEN 'Переход отправлен' THEN 'reminder_sent'
|
||||
WHEN 'Передан логисту' THEN 'transferred_to_logistics'
|
||||
WHEN 'Платное хранение' THEN 'paid_storage'
|
||||
WHEN 'Доставлен' THEN 'delivered'
|
||||
WHEN 'Доставка согласована' THEN 'agreed'
|
||||
ELSE 'default'
|
||||
END;
|
||||
|
||||
UPDATE public.delivery_invitations
|
||||
SET
|
||||
opened_at = CASE
|
||||
WHEN v_state IN ('awaiting_choice', 'opened', 'reminder_sent') AND opened_at IS NULL THEN v_now
|
||||
ELSE opened_at
|
||||
END,
|
||||
access_count = COALESCE(access_count, 0) + 1,
|
||||
last_accessed_at = v_now
|
||||
WHERE id = v_invitation.id
|
||||
RETURNING * INTO v_invitation;
|
||||
|
||||
v_order_items := CASE
|
||||
WHEN jsonb_typeof(v_order.customer -> 'items') = 'array' THEN v_order.customer -> 'items'
|
||||
ELSE '[]'::jsonb
|
||||
END;
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'ok', TRUE,
|
||||
'invitation', jsonb_build_object(
|
||||
'orderId', v_invitation.order_id::text,
|
||||
'state', v_state,
|
||||
'token', p_token,
|
||||
'orderNumber', COALESCE(NULLIF(v_order.order_number, ''), NULLIF(v_invitation.order_number, '')),
|
||||
'customerName', COALESCE(NULLIF(v_order.customer ->> 'name', ''), NULLIF(v_invitation.customer_name, '')),
|
||||
'customerPhone', COALESCE(NULLIF(v_order.customer ->> 'phone', ''), NULLIF(v_invitation.customer_phone, '')),
|
||||
'orderItems', v_order_items,
|
||||
'availableSlots', COALESCE(to_jsonb(v_invitation.available_slots), '[]'::jsonb),
|
||||
'deliveryDate', v_invitation.delivery_date,
|
||||
'deliveryTime', v_invitation.delivery_time,
|
||||
'orderStatus', v_order.status,
|
||||
'deliveryAgreementStatus', v_order.delivery_agreement_status
|
||||
)
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.get_delivery_invitation_by_token(text) FROM public;
|
||||
GRANT EXECUTE ON FUNCTION public.get_delivery_invitation_by_token(text) TO anon, authenticated;
|
||||
|
|
@ -90,7 +90,7 @@ describe("DeliveryChoiceFlow", () => {
|
|||
});
|
||||
|
||||
describe("OrderCompositionPanel", () => {
|
||||
it("renders order items with quantities when they are provided", () => {
|
||||
it("renders with position count and collapsed state", () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<OrderCompositionPanel
|
||||
invitation={{
|
||||
|
|
@ -105,42 +105,14 @@ describe("OrderCompositionPanel", () => {
|
|||
);
|
||||
|
||||
expect(markup).toContain("Состав заказа");
|
||||
expect(markup).toContain("Кухонный гарнитур");
|
||||
expect(markup).toContain("1 комплект");
|
||||
expect(markup).toContain("Фурнитура Blum");
|
||||
expect(markup).toContain("12 шт");
|
||||
expect(markup).toContain("Монтажный комплект");
|
||||
expect(markup).toContain("3 поз.");
|
||||
});
|
||||
|
||||
it("renders reference and customer info when there are no order items", () => {
|
||||
it("renders nothing when there are no order items", () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<OrderCompositionPanel invitation={{ orderNumber: "CD-240031", customerName: "Мария Волкова" }} />,
|
||||
);
|
||||
|
||||
expect(markup).toContain("Состав заказа");
|
||||
expect(markup).toContain("CD-240031");
|
||||
expect(markup).toContain("Мария Волкова");
|
||||
});
|
||||
|
||||
it("renders nothing when there are no order items and no reference info", () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<OrderCompositionPanel invitation={{}} />,
|
||||
<OrderCompositionPanel invitation={{ orderNumber: "CD-240031" }} />,
|
||||
);
|
||||
|
||||
expect(markup).toBe("");
|
||||
});
|
||||
|
||||
it("handles string items with pipe separator", () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<OrderCompositionPanel
|
||||
invitation={{
|
||||
orderNumber: "CD-240031",
|
||||
orderItems: ["Плинтус|5 шт"],
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(markup).toContain("Плинтус");
|
||||
expect(markup).toContain("5 шт");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,109 +3,98 @@ import { Badge } from "../UI/Badge";
|
|||
import { Panel } from "../UI/Panel";
|
||||
import { getInvitationReferenceLabel } from "./invitationReference";
|
||||
|
||||
const splitOrderItem = (item) => {
|
||||
if (!item) return null;
|
||||
const flattenOrderProducts = (rawItems) => {
|
||||
const products = [];
|
||||
|
||||
if (typeof item === "string") {
|
||||
const [name, quantity] = item.split("|").map((p) => p.trim());
|
||||
return { name: name || item.trim(), quantity: quantity || "", items: [] };
|
||||
for (const item of rawItems) {
|
||||
if (!item || typeof item !== "object") continue;
|
||||
|
||||
const subOrders = Array.isArray(item.items) ? item.items : [];
|
||||
|
||||
if (subOrders.length === 0) {
|
||||
const name = String(item.product_name || item.name || item.nom || "").trim();
|
||||
const qty = String(item.product_quantity || item.quantity || item.count || item.amount || "").trim();
|
||||
const unit = String(item.product_ed || item.unit || "").trim();
|
||||
if (name) products.push({ name, quantity: qty, unit });
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasSubOrders = subOrders.some((s) => typeof s === "object" && ("nom" in s || ("items" in s && Array.isArray(s.items))));
|
||||
|
||||
if (hasSubOrders) {
|
||||
for (const sub of subOrders) {
|
||||
if (!sub || typeof sub !== "object") continue;
|
||||
const productsList = Array.isArray(sub.items) ? sub.items : [];
|
||||
for (const p of productsList) {
|
||||
if (!p || typeof p !== "object") continue;
|
||||
const pName = String(p.product_name || p.name || "").trim();
|
||||
const pQty = String(p.product_quantity || p.quantity || p.count || p.amount || "").trim();
|
||||
const pUnit = String(p.product_ed || p.unit || "").trim();
|
||||
if (pName) products.push({ name: pName, quantity: pQty, unit: pUnit });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const p of subOrders) {
|
||||
if (!p || typeof p !== "object") continue;
|
||||
const pName = String(p.product_name || p.name || "").trim();
|
||||
const pQty = String(p.product_quantity || p.quantity || p.count || p.amount || "").trim();
|
||||
const pUnit = String(p.product_ed || p.unit || "").trim();
|
||||
if (pName) products.push({ name: pName, quantity: pQty, unit: pUnit });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof item === "object") {
|
||||
const name = typeof item.name === "string"
|
||||
? item.name.trim()
|
||||
: typeof item.label === "string"
|
||||
? item.label.trim()
|
||||
: "";
|
||||
const quantity = typeof item.quantity === "string"
|
||||
? item.quantity.trim()
|
||||
: typeof item.quantity === "number"
|
||||
? String(item.quantity)
|
||||
: "";
|
||||
const nestedItems = Array.isArray(item.items)
|
||||
? item.items.map((sub) => {
|
||||
if (!sub || typeof sub !== "object") return null;
|
||||
return {
|
||||
name: String(sub.product_name || sub.name || sub.title || "").trim(),
|
||||
quantity: String(sub.product_quantity || sub.quantity || sub.count || sub.amount || "").trim(),
|
||||
};
|
||||
}).filter(Boolean)
|
||||
: [];
|
||||
return { name: name || "Позиция", quantity, items: nestedItems };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const buildDetailRows = (invitation) => {
|
||||
const rows = [];
|
||||
const reference = getInvitationReferenceLabel(invitation);
|
||||
if (reference && reference !== "Счет —") {
|
||||
rows.push({ label: "Счет", value: reference.replace(/^Счета?:\s*/, "") });
|
||||
}
|
||||
if (invitation.customerName) {
|
||||
rows.push({ label: "Клиент", value: invitation.customerName });
|
||||
}
|
||||
return rows;
|
||||
return products;
|
||||
};
|
||||
|
||||
export const OrderCompositionPanel = ({ invitation = {} }) => {
|
||||
const orderItems = (invitation.orderItems || invitation.items || [])
|
||||
.map(splitOrderItem)
|
||||
.filter(Boolean);
|
||||
|
||||
const detailRows = buildDetailRows(invitation);
|
||||
const hasContent = orderItems.length > 0 || detailRows.length > 0;
|
||||
|
||||
if (!hasContent) return null;
|
||||
|
||||
const rawItems = invitation.orderItems || invitation.items || [];
|
||||
const products = flattenOrderProducts(rawItems);
|
||||
const reference = getInvitationReferenceLabel(invitation);
|
||||
|
||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||
|
||||
if (products.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Panel className="space-y-3 border shadow-soft p-5 sm:p-6">
|
||||
<p className="text-sm uppercase tracking-[0.18em] text-[var(--color-text-muted)]">
|
||||
Состав заказа {reference !== "Счет —" ? reference : ""}
|
||||
</p>
|
||||
|
||||
{detailRows.length > 0 && !orderItems.length ? (
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between text-left"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<p className="text-sm uppercase tracking-[0.18em] text-[var(--color-text-muted)]">
|
||||
Состав заказа {reference !== "Счет —" ? reference : ""}
|
||||
</p>
|
||||
<span className="flex items-center gap-2 text-sm text-[var(--color-text-muted)]">
|
||||
{products.length > 0 ? `${products.length} поз.` : ""}
|
||||
<svg
|
||||
className="h-4 w-4 transition-transform"
|
||||
style={{ transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)" }}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="space-y-2">
|
||||
{detailRows.map((row) => (
|
||||
{products.map((product, idx) => (
|
||||
<div
|
||||
key={row.label}
|
||||
key={`${product.name}-${idx}`}
|
||||
className="flex items-center justify-between gap-3 rounded-[18px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] px-4 py-3 text-sm"
|
||||
>
|
||||
<span className="leading-6 text-[var(--color-text-muted)]">{row.label}</span>
|
||||
<span className="font-medium leading-6">{row.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{orderItems.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{orderItems.map((item) => (
|
||||
<div key={`${item.name}-${item.quantity || "item"}`}>
|
||||
<div className="flex items-center justify-between gap-3 rounded-[18px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] px-4 py-3 text-sm">
|
||||
<span className="font-medium leading-6">{item.name}</span>
|
||||
{item.quantity ? <Badge tone="neutral">{item.quantity}</Badge> : null}
|
||||
</div>
|
||||
{item.items && item.items.length > 0 ? (
|
||||
<div className="mt-1 space-y-1 pl-3">
|
||||
{item.items.map((sub, idx) => (
|
||||
<div
|
||||
key={`${sub.name}-${idx}`}
|
||||
className="flex items-center justify-between gap-3 rounded-[14px] border border-[var(--color-border)] bg-[var(--color-bg)] px-3 py-2 text-sm"
|
||||
>
|
||||
<span className="leading-6">{sub.name}</span>
|
||||
{sub.quantity ? <Badge tone="neutral">{sub.quantity}</Badge> : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<span className="leading-6">{product.name}</span>
|
||||
{(product.quantity || product.unit) ? (
|
||||
<Badge tone="neutral">{[product.quantity, product.unit].filter(Boolean).join(" ")}</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -183,60 +183,6 @@ export const __resetLocalDeliveryInvitationCache = () => {
|
|||
localDeliveryInvitationCache.clear();
|
||||
};
|
||||
|
||||
const enrichOrderItemsFromEdgeFunction = (invitation, token) => {
|
||||
// Fire-and-forget: try the Edge Function in the background to get richer
|
||||
// order item data (with source_orders). On success, update the cache.
|
||||
// The initial RPC response is returned immediately so the page renders
|
||||
// without delay; the richer data arrives shortly after.
|
||||
if (!token || isLocalClientInvitationToken(token)) {
|
||||
return invitation;
|
||||
}
|
||||
|
||||
const hasOnlyInvoiceItems = Array.isArray(invitation.orderItems)
|
||||
&& invitation.orderItems.every(
|
||||
(item) => typeof item === "object" && item !== null
|
||||
&& (!Array.isArray(item.items) || item.items.length === 0)
|
||||
&& (!item.quantity || item.quantity === ""),
|
||||
);
|
||||
|
||||
if (!hasOnlyInvoiceItems) {
|
||||
return invitation;
|
||||
}
|
||||
|
||||
invokeDeliveryFunction("get-delivery-invitation", { token })
|
||||
.then((response) => {
|
||||
if (response?.invitation?.orderItems) {
|
||||
const enriched = {
|
||||
...invitation,
|
||||
orderItems: response.invitation.orderItems,
|
||||
};
|
||||
cacheInvitation(enriched);
|
||||
const storage = getLocalStorage();
|
||||
if (storage) {
|
||||
try {
|
||||
storage.setItem(
|
||||
getLocalStorageKey(token),
|
||||
JSON.stringify(enriched),
|
||||
);
|
||||
window.dispatchEvent(
|
||||
new StorageEvent("storage", {
|
||||
key: getLocalStorageKey(token),
|
||||
newValue: JSON.stringify(enriched),
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
// Ignore storage errors.
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Silently ignore — the initial RPC data is already shown.
|
||||
});
|
||||
|
||||
return invitation;
|
||||
};
|
||||
|
||||
export const fetchDeliveryInvitation = async (token) => {
|
||||
if (!token) {
|
||||
throw new Error("Token is required");
|
||||
|
|
@ -262,8 +208,7 @@ export const fetchDeliveryInvitation = async (token) => {
|
|||
p_token: token,
|
||||
});
|
||||
if (response?.invitation) {
|
||||
const enriched = enrichOrderItemsFromEdgeFunction(response.invitation, token);
|
||||
return cacheInvitation(enriched);
|
||||
return cacheInvitation(response.invitation);
|
||||
}
|
||||
if (isLocalClientInvitationToken(token)) {
|
||||
return cacheInvitation(buildFallbackInvitation(token));
|
||||
|
|
|
|||
Loading…
Reference in New Issue