@akinon/pz-theme 2.0.154-rc.6 → 2.0.154
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -46
- package/package.json +4 -4
- package/src/__tests__/account-slot-coverage.test.ts +1 -0
- package/src/__tests__/custom-css.test.ts +12 -0
- package/src/__tests__/route-slug.test.ts +7 -0
- package/src/blocks/__tests__/group-order-items.test.ts +87 -0
- package/src/blocks/__tests__/order-detail-return.test.tsx +208 -0
- package/src/blocks/item-template.tsx +19 -25
- package/src/blocks/order-detail-block.tsx +273 -82
- package/src/blocks/order-list-block.tsx +2 -25
- package/src/blocks/product-block-utils.tsx +49 -13
- package/src/chrome/__tests__/page-context-builders.test.ts +1 -14
- package/src/chrome/page-context.ts +0 -4
- package/src/chrome/placeholder-slugs.ts +4 -0
- package/src/chrome/route-slug.ts +7 -3
- package/src/custom/README.md +0 -20
- package/src/utils/custom-css.ts +35 -2
- package/src/__tests__/row-scope.test.tsx +0 -118
- package/src/blocks/__tests__/order-line-actions.test.tsx +0 -54
- package/src/blocks/__tests__/order-list-rows.test.tsx +0 -112
- package/src/blocks/order-line-actions.ts +0 -45
- package/src/blocks/row-scope.tsx +0 -35
|
@@ -9,7 +9,6 @@ import { getResponsiveValue, partAttrs } from '../utils';
|
|
|
9
9
|
import { formatDate, formatMoney } from './commerce-format';
|
|
10
10
|
import { readMoneyStyle } from './money-style';
|
|
11
11
|
import { hasItemTemplate, ItemTemplateList } from './item-template';
|
|
12
|
-
import { renderOrderLineActions } from './order-line-actions';
|
|
13
12
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
14
13
|
import {
|
|
15
14
|
DesignerPlaceholder,
|
|
@@ -17,7 +16,7 @@ import {
|
|
|
17
16
|
readToggle
|
|
18
17
|
} from './product-block-utils';
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
interface OrderItemLike {
|
|
21
20
|
id?: number;
|
|
22
21
|
price?: string;
|
|
23
22
|
quantity?: string | number;
|
|
@@ -27,6 +26,7 @@ type OrderItemLike = {
|
|
|
27
26
|
tracking_number?: string;
|
|
28
27
|
estimated_delivery_date?: string;
|
|
29
28
|
price_currency?: { value?: string; label?: string };
|
|
29
|
+
is_refundable?: boolean;
|
|
30
30
|
active_cancellation_request?: unknown;
|
|
31
31
|
product?: {
|
|
32
32
|
pk?: number;
|
|
@@ -35,7 +35,7 @@ type OrderItemLike = {
|
|
|
35
35
|
absolute_url?: string;
|
|
36
36
|
attributes?: Record<string, unknown>;
|
|
37
37
|
} | null;
|
|
38
|
-
}
|
|
38
|
+
}
|
|
39
39
|
|
|
40
40
|
interface AddressLike {
|
|
41
41
|
first_name?: string;
|
|
@@ -66,6 +66,7 @@ interface OrderLike {
|
|
|
66
66
|
orderitem_set?: OrderItemLike[];
|
|
67
67
|
discountitem_set?: DiscountItemLike[];
|
|
68
68
|
is_cancellable?: boolean;
|
|
69
|
+
is_refundable?: boolean;
|
|
69
70
|
e_archive_url?: string | null;
|
|
70
71
|
shipping_address?: AddressLike | null;
|
|
71
72
|
billing_address?: AddressLike | null;
|
|
@@ -113,6 +114,37 @@ const firstString = (
|
|
|
113
114
|
return '';
|
|
114
115
|
};
|
|
115
116
|
|
|
117
|
+
const hasOpenRequest = (request: unknown): boolean =>
|
|
118
|
+
Array.isArray(request) ? request.length > 0 : Boolean(request);
|
|
119
|
+
|
|
120
|
+
/** Commerce's "Teslim edildi". */
|
|
121
|
+
const DELIVERED = '550';
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Whether the order still has a line to return: delivered, refundable, with
|
|
125
|
+
* no request open on it. Commerce also marks a shipped line refundable, and
|
|
126
|
+
* keeps `is_refundable` true on the order and the line while a return request
|
|
127
|
+
* is open, so the order flag alone would offer a return before the parcel
|
|
128
|
+
* arrives and again after the shopper has asked. Lines that carry no flag at
|
|
129
|
+
* all (an older payload) leave the decision to the order's flag and status.
|
|
130
|
+
*/
|
|
131
|
+
const hasReturnableLine = (order: OrderLike): boolean => {
|
|
132
|
+
if (!order.is_refundable) return false;
|
|
133
|
+
|
|
134
|
+
const lines = order.orderitem_set ?? [];
|
|
135
|
+
|
|
136
|
+
if (!lines.some((item) => item && 'is_refundable' in item)) {
|
|
137
|
+
return String(order.status?.value ?? '') === DELIVERED;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return lines.some(
|
|
141
|
+
(item) =>
|
|
142
|
+
String(item?.status?.value ?? '') === DELIVERED &&
|
|
143
|
+
item?.is_refundable &&
|
|
144
|
+
!hasOpenRequest(item.active_cancellation_request)
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
|
|
116
148
|
/** Positive integers only; anything else falls back to the shipped size. */
|
|
117
149
|
const readSize = (value: unknown, fallback: number): number => {
|
|
118
150
|
const numeric = Math.round(Number(value));
|
|
@@ -146,6 +178,7 @@ const DESIGNER_SAMPLE_ORDER: OrderLike = {
|
|
|
146
178
|
status: { value: '10', label: 'Approved' },
|
|
147
179
|
currency: { value: 'try', label: 'TRY' },
|
|
148
180
|
is_cancellable: true,
|
|
181
|
+
is_refundable: true,
|
|
149
182
|
e_archive_url: '#',
|
|
150
183
|
installment_count: 3,
|
|
151
184
|
orderitem_set: [
|
|
@@ -159,7 +192,16 @@ const DESIGNER_SAMPLE_ORDER: OrderLike = {
|
|
|
159
192
|
tracking_number: '1Z999AA10123456784',
|
|
160
193
|
tracking_url: '#',
|
|
161
194
|
active_cancellation_request: [
|
|
162
|
-
{
|
|
195
|
+
{
|
|
196
|
+
easy_return: {
|
|
197
|
+
code: '12345678912345',
|
|
198
|
+
shipping_company: { value: 'carrier', label: 'Carrier' }
|
|
199
|
+
},
|
|
200
|
+
pickup_date: {
|
|
201
|
+
date: '2026-04-11',
|
|
202
|
+
hour_range: { start: '09:00:00', end: '18:00:00' }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
163
205
|
],
|
|
164
206
|
product: {
|
|
165
207
|
pk: 1,
|
|
@@ -250,6 +292,63 @@ const resolveReturnCode = (value: unknown): string => {
|
|
|
250
292
|
return typeof code === 'string' ? code : '';
|
|
251
293
|
};
|
|
252
294
|
|
|
295
|
+
interface ReturnRequestLike {
|
|
296
|
+
pickup_date?: { date?: string } | null;
|
|
297
|
+
easy_return?: {
|
|
298
|
+
code?: string;
|
|
299
|
+
shipping_company?: { label?: string } | null;
|
|
300
|
+
} | null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* How a returned line goes back: `appointment` when the request booked a
|
|
305
|
+
* pickup (it carries a pickup day), `standard` for an easy return dropped off
|
|
306
|
+
* at a branch, nothing for a plain cancellation. The carrier and the pickup
|
|
307
|
+
* day come from the same request; both shapes of the field are probed, as
|
|
308
|
+
* for the return code.
|
|
309
|
+
*/
|
|
310
|
+
const resolveReturnDetails = (
|
|
311
|
+
value: unknown
|
|
312
|
+
): {
|
|
313
|
+
method: 'appointment' | 'standard' | null;
|
|
314
|
+
carrier: string;
|
|
315
|
+
pickupDate: string;
|
|
316
|
+
} => {
|
|
317
|
+
const entry = (Array.isArray(value) ? value[0] : value) as
|
|
318
|
+
| ReturnRequestLike
|
|
319
|
+
| null
|
|
320
|
+
| undefined;
|
|
321
|
+
const pickupDate = entry?.pickup_date?.date;
|
|
322
|
+
const carrier = entry?.easy_return?.shipping_company?.label;
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
method: pickupDate ? 'appointment' : entry?.easy_return ? 'standard' : null,
|
|
326
|
+
carrier: typeof carrier === 'string' ? carrier : '',
|
|
327
|
+
pickupDate: typeof pickupDate === 'string' ? pickupDate : ''
|
|
328
|
+
};
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
/** "2026-04-11" read as a calendar day in the page's locale (no time zone shift). */
|
|
332
|
+
const formatPickupDate = (value: string, locale?: string): string => {
|
|
333
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(value);
|
|
334
|
+
if (!match) return value;
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
return new Intl.DateTimeFormat(locale || undefined, {
|
|
338
|
+
timeZone: 'UTC',
|
|
339
|
+
day: '2-digit',
|
|
340
|
+
month: '2-digit',
|
|
341
|
+
year: 'numeric'
|
|
342
|
+
}).format(
|
|
343
|
+
new Date(
|
|
344
|
+
Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]), 12)
|
|
345
|
+
)
|
|
346
|
+
);
|
|
347
|
+
} catch {
|
|
348
|
+
return value;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
253
352
|
const formatAddress = (address: AddressLike | null | undefined): string[] => {
|
|
254
353
|
if (!address) return [];
|
|
255
354
|
const name = [address.first_name, address.last_name]
|
|
@@ -273,16 +372,16 @@ const formatAddress = (address: AddressLike | null | undefined): string[] => {
|
|
|
273
372
|
* order key is read from the URL; the editor owns which sections are exposed,
|
|
274
373
|
* every caption and the styling.
|
|
275
374
|
* A designer can mark one child as the row layout for the order lines; without
|
|
276
|
-
* one the built-in line renders.
|
|
277
|
-
* built-in line through `registerOrderLineActions`; they render inside the
|
|
278
|
-
* line's `item-actions` part.
|
|
375
|
+
* one the built-in line renders.
|
|
279
376
|
* Properties: numberLabel, numberTag, dateLabel, headerTotalLabel,
|
|
280
377
|
* showInvoiceLink, invoiceLinkLabel, showCancelLink, cancelLinkLabel,
|
|
281
|
-
* cancelHrefBase, showShipmentStatus, showItems, showImage, imageWidth,
|
|
378
|
+
* showReturnLink, returnLinkLabel, cancelHrefBase, showShipmentStatus, showItems, showImage, imageWidth,
|
|
282
379
|
* imageHeight, showQuantity, quantityLabel, showColor,
|
|
283
380
|
* colorLabel, colorAttributeKey, showSize, sizeLabel, sizeAttributeKey,
|
|
284
381
|
* showLineTracking, lineTrackingLabel, showReturnCode,
|
|
285
|
-
* returnCodeLabel,
|
|
382
|
+
* returnCodeLabel, showReturnMethod, returnMethodLabel,
|
|
383
|
+
* appointmentReturnLabel, standardReturnLabel, showReturnCarrier,
|
|
384
|
+
* returnCarrierLabel, showPickupDate, pickupDateLabel, showShippingAddress, shippingAddressLabel,
|
|
286
385
|
* showBillingAddress, billingAddressLabel, showTotals, showPieceCount,
|
|
287
386
|
* pieceCountLabel, showDiscountItems, showInstallmentInfo,
|
|
288
387
|
* installmentInfoLabel, subtotalLabel, discountLabel, shippingLabel,
|
|
@@ -290,8 +389,9 @@ const formatAddress = (address: AddressLike | null | undefined): string[] => {
|
|
|
290
389
|
* showPaymentInfo, paymentInfoLabel, decimals, currencyDisplay,
|
|
291
390
|
* currencyText, currencyPosition.
|
|
292
391
|
* Parts: detail, header, number, date, header-total, status,
|
|
293
|
-
* invoice-link, cancel-link, list, item, image, name, quantity,
|
|
294
|
-
* item-status, item-tracking, item-return-code,
|
|
392
|
+
* invoice-link, cancel-link, return-link, list, item, image, name, quantity,
|
|
393
|
+
* color, size, item-status, item-tracking, item-return-code,
|
|
394
|
+
* item-return-method, item-return-carrier, item-pickup-date, price, shipment,
|
|
295
395
|
* delivery, tracking, addresses, address, address-label, totals, total-row,
|
|
296
396
|
* total-label, total-value, grand-total, payment-info, payment-info-label.
|
|
297
397
|
*/
|
|
@@ -402,6 +502,38 @@ const OrderDetailBlock = ({
|
|
|
402
502
|
false
|
|
403
503
|
);
|
|
404
504
|
const returnCodeLabel = text('returnCodeLabel', 'Return code:');
|
|
505
|
+
// ZERO-5183: the easy-return screens start from here, and a returned line
|
|
506
|
+
// says how it goes back. The cancel link only shows while the order can
|
|
507
|
+
// still be cancelled; a delivered order needs its own way in.
|
|
508
|
+
const showReturnLink = readToggle(
|
|
509
|
+
block.properties?.showReturnLink,
|
|
510
|
+
currentBreakpoint,
|
|
511
|
+
false
|
|
512
|
+
);
|
|
513
|
+
const returnLinkLabel = text('returnLinkLabel', 'Easy return');
|
|
514
|
+
const showReturnMethod = readToggle(
|
|
515
|
+
block.properties?.showReturnMethod,
|
|
516
|
+
currentBreakpoint,
|
|
517
|
+
false
|
|
518
|
+
);
|
|
519
|
+
const returnMethodLabel = text('returnMethodLabel', 'Return method:');
|
|
520
|
+
const appointmentReturnLabel = text(
|
|
521
|
+
'appointmentReturnLabel',
|
|
522
|
+
'Scheduled pickup'
|
|
523
|
+
);
|
|
524
|
+
const standardReturnLabel = text('standardReturnLabel', 'Standard');
|
|
525
|
+
const showReturnCarrier = readToggle(
|
|
526
|
+
block.properties?.showReturnCarrier,
|
|
527
|
+
currentBreakpoint,
|
|
528
|
+
false
|
|
529
|
+
);
|
|
530
|
+
const returnCarrierLabel = text('returnCarrierLabel', 'Carrier:');
|
|
531
|
+
const showPickupDate = readToggle(
|
|
532
|
+
block.properties?.showPickupDate,
|
|
533
|
+
currentBreakpoint,
|
|
534
|
+
false
|
|
535
|
+
);
|
|
536
|
+
const pickupDateLabel = text('pickupDateLabel', 'Pickup date:');
|
|
405
537
|
|
|
406
538
|
const showShippingAddress = readToggle(
|
|
407
539
|
block.properties?.showShippingAddress,
|
|
@@ -489,6 +621,12 @@ const OrderDetailBlock = ({
|
|
|
489
621
|
const money = (value: unknown) =>
|
|
490
622
|
formatMoney(value, { locale, currency, ...moneyStyle });
|
|
491
623
|
|
|
624
|
+
// The sample order is not a delivered one; it shows the link for styling.
|
|
625
|
+
const offersReturn =
|
|
626
|
+
order === DESIGNER_SAMPLE_ORDER
|
|
627
|
+
? Boolean(order.is_refundable)
|
|
628
|
+
: hasReturnableLine(order);
|
|
629
|
+
|
|
492
630
|
// Navigating inside the designer iframe would drop the author out of the page
|
|
493
631
|
// they are editing, so every link is inert there.
|
|
494
632
|
const guard = (event: React.MouseEvent) => {
|
|
@@ -667,7 +805,7 @@ const OrderDetailBlock = ({
|
|
|
667
805
|
{headerTotalLabel} {money(order.amount)}
|
|
668
806
|
</span>
|
|
669
807
|
)}
|
|
670
|
-
{(showInvoiceLink || showCancelLink) && (
|
|
808
|
+
{(showInvoiceLink || showCancelLink || showReturnLink) && (
|
|
671
809
|
<div
|
|
672
810
|
style={{
|
|
673
811
|
display: 'flex',
|
|
@@ -707,6 +845,21 @@ const OrderDetailBlock = ({
|
|
|
707
845
|
→ {cancelLinkLabel}
|
|
708
846
|
</NextLink>
|
|
709
847
|
)}
|
|
848
|
+
{showReturnLink && offersReturn && cancelHref && (
|
|
849
|
+
<NextLink
|
|
850
|
+
href={isDesigner ? '#' : cancelHref}
|
|
851
|
+
onClick={guard}
|
|
852
|
+
data-order-detail-return-link=""
|
|
853
|
+
{...partAttrs('return-link')}
|
|
854
|
+
style={{
|
|
855
|
+
color: 'inherit',
|
|
856
|
+
textDecoration: 'none',
|
|
857
|
+
whiteSpace: 'nowrap'
|
|
858
|
+
}}
|
|
859
|
+
>
|
|
860
|
+
→ {returnLinkLabel}
|
|
861
|
+
</NextLink>
|
|
862
|
+
)}
|
|
710
863
|
</div>
|
|
711
864
|
)}
|
|
712
865
|
</div>
|
|
@@ -761,14 +914,22 @@ const OrderDetailBlock = ({
|
|
|
761
914
|
const returnCodeValue = showReturnCode
|
|
762
915
|
? resolveReturnCode(item.active_cancellation_request)
|
|
763
916
|
: '';
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
917
|
+
const returnDetails = resolveReturnDetails(
|
|
918
|
+
item.active_cancellation_request
|
|
919
|
+
);
|
|
920
|
+
const returnMethodValue =
|
|
921
|
+
showReturnMethod && returnDetails.method
|
|
922
|
+
? returnDetails.method === 'appointment'
|
|
923
|
+
? appointmentReturnLabel
|
|
924
|
+
: standardReturnLabel
|
|
925
|
+
: '';
|
|
926
|
+
const returnCarrierValue = showReturnCarrier
|
|
927
|
+
? returnDetails.carrier
|
|
928
|
+
: '';
|
|
929
|
+
const pickupDateValue =
|
|
930
|
+
showPickupDate && returnDetails.pickupDate
|
|
931
|
+
? formatPickupDate(returnDetails.pickupDate, locale)
|
|
932
|
+
: '';
|
|
772
933
|
|
|
773
934
|
return (
|
|
774
935
|
<li
|
|
@@ -884,68 +1045,98 @@ const OrderDetailBlock = ({
|
|
|
884
1045
|
</div>
|
|
885
1046
|
</div>
|
|
886
1047
|
|
|
887
|
-
{
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
</
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1048
|
+
{showShipmentStatus &&
|
|
1049
|
+
(item.status?.label ||
|
|
1050
|
+
trackingValue ||
|
|
1051
|
+
returnCodeValue ||
|
|
1052
|
+
returnMethodValue ||
|
|
1053
|
+
returnCarrierValue ||
|
|
1054
|
+
pickupDateValue) && (
|
|
1055
|
+
<div
|
|
1056
|
+
style={{
|
|
1057
|
+
display: 'flex',
|
|
1058
|
+
flexDirection: 'column',
|
|
1059
|
+
alignItems: isMobile ? 'flex-start' : 'flex-end',
|
|
1060
|
+
textAlign: isMobile ? 'left' : 'right',
|
|
1061
|
+
gap: '4px',
|
|
1062
|
+
flex: isMobile ? '1 1 100%' : '0 0 auto'
|
|
1063
|
+
}}
|
|
1064
|
+
>
|
|
1065
|
+
{item.status?.label && (
|
|
1066
|
+
<span
|
|
1067
|
+
data-order-detail-item-status=""
|
|
1068
|
+
data-order-status-value={item.status.value}
|
|
1069
|
+
{...partAttrs('item-status')}
|
|
1070
|
+
style={{ display: 'inline-block' }}
|
|
1071
|
+
>
|
|
1072
|
+
{item.status.label}
|
|
1073
|
+
</span>
|
|
1074
|
+
)}
|
|
1075
|
+
{/* Label and value in their own spans, so a
|
|
1076
|
+
composition can colour them apart. */}
|
|
1077
|
+
{returnMethodValue && (
|
|
1078
|
+
<span
|
|
1079
|
+
data-order-detail-item-return-method=""
|
|
1080
|
+
{...partAttrs('item-return-method')}
|
|
1081
|
+
style={{ opacity: 0.8 }}
|
|
1082
|
+
>
|
|
1083
|
+
<span data-label="">{returnMethodLabel}</span>{' '}
|
|
1084
|
+
<span data-value="">{returnMethodValue}</span>
|
|
1085
|
+
</span>
|
|
1086
|
+
)}
|
|
1087
|
+
{returnCarrierValue && (
|
|
1088
|
+
<span
|
|
1089
|
+
data-order-detail-item-return-carrier=""
|
|
1090
|
+
{...partAttrs('item-return-carrier')}
|
|
1091
|
+
style={{ opacity: 0.8 }}
|
|
1092
|
+
>
|
|
1093
|
+
<span data-label="">{returnCarrierLabel}</span>{' '}
|
|
1094
|
+
<span data-value="">{returnCarrierValue}</span>
|
|
1095
|
+
</span>
|
|
1096
|
+
)}
|
|
1097
|
+
{pickupDateValue && (
|
|
1098
|
+
<span
|
|
1099
|
+
data-order-detail-item-pickup-date=""
|
|
1100
|
+
{...partAttrs('item-pickup-date')}
|
|
1101
|
+
style={{ opacity: 0.8 }}
|
|
1102
|
+
>
|
|
1103
|
+
<span data-label="">{pickupDateLabel}</span>{' '}
|
|
1104
|
+
<span data-value="">{pickupDateValue}</span>
|
|
1105
|
+
</span>
|
|
1106
|
+
)}
|
|
1107
|
+
{trackingValue && (
|
|
1108
|
+
<span
|
|
1109
|
+
data-order-detail-item-tracking=""
|
|
1110
|
+
{...partAttrs('item-tracking')}
|
|
1111
|
+
style={{ opacity: 0.8 }}
|
|
1112
|
+
>
|
|
1113
|
+
{lineTrackingLabel}{' '}
|
|
1114
|
+
{item.tracking_url ? (
|
|
1115
|
+
<a
|
|
1116
|
+
href={isDesigner ? '#' : item.tracking_url}
|
|
1117
|
+
onClick={guard}
|
|
1118
|
+
target={isDesigner ? undefined : '_blank'}
|
|
1119
|
+
rel="noreferrer"
|
|
1120
|
+
style={{ color: 'inherit' }}
|
|
1121
|
+
>
|
|
1122
|
+
{item.tracking_number || item.tracking_url}
|
|
1123
|
+
</a>
|
|
1124
|
+
) : (
|
|
1125
|
+
trackingValue
|
|
1126
|
+
)}
|
|
1127
|
+
</span>
|
|
1128
|
+
)}
|
|
1129
|
+
{returnCodeValue && (
|
|
1130
|
+
<span
|
|
1131
|
+
data-order-detail-item-return-code=""
|
|
1132
|
+
{...partAttrs('item-return-code')}
|
|
1133
|
+
style={{ opacity: 0.8 }}
|
|
1134
|
+
>
|
|
1135
|
+
{returnCodeLabel} {returnCodeValue}
|
|
1136
|
+
</span>
|
|
1137
|
+
)}
|
|
1138
|
+
</div>
|
|
1139
|
+
)}
|
|
949
1140
|
</div>
|
|
950
1141
|
</li>
|
|
951
1142
|
);
|
|
@@ -13,14 +13,7 @@ import { readToggle } from './product-block-utils';
|
|
|
13
13
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
14
14
|
|
|
15
15
|
interface OrderItemLike {
|
|
16
|
-
|
|
17
|
-
status?: { value?: string | number; label?: string };
|
|
18
|
-
product?: {
|
|
19
|
-
image?: string;
|
|
20
|
-
name?: string;
|
|
21
|
-
sku?: string;
|
|
22
|
-
base_code?: string;
|
|
23
|
-
} | null;
|
|
16
|
+
product?: { image?: string; name?: string } | null;
|
|
24
17
|
}
|
|
25
18
|
|
|
26
19
|
interface OrderLike {
|
|
@@ -255,23 +248,7 @@ const OrderListBlock = ({
|
|
|
255
248
|
maxImages
|
|
256
249
|
),
|
|
257
250
|
url: hrefFor(order),
|
|
258
|
-
href: hrefFor(order)
|
|
259
|
-
createdDate: order.created_date || '',
|
|
260
|
-
items: (Array.isArray(order.orderitem_set) ? order.orderitem_set : []).map(
|
|
261
|
-
(item, itemIndex) => ({
|
|
262
|
-
id: item?.id ?? itemIndex,
|
|
263
|
-
name: item?.product?.name || '',
|
|
264
|
-
image: item?.product?.image || '',
|
|
265
|
-
status: item?.status?.label || '',
|
|
266
|
-
statusValue: String(item?.status?.value ?? ''),
|
|
267
|
-
product: {
|
|
268
|
-
sku: item?.product?.sku || '',
|
|
269
|
-
base_code: item?.product?.base_code || '',
|
|
270
|
-
name: item?.product?.name || '',
|
|
271
|
-
image: item?.product?.image || ''
|
|
272
|
-
}
|
|
273
|
-
})
|
|
274
|
-
)
|
|
251
|
+
href: hrefFor(order)
|
|
275
252
|
}));
|
|
276
253
|
|
|
277
254
|
// A designer who marked a child as the row layout gets that layout repeated
|
|
@@ -240,6 +240,8 @@ export interface OrderLineLike {
|
|
|
240
240
|
price?: string | number;
|
|
241
241
|
quantity?: string | number;
|
|
242
242
|
total_amount?: string | number;
|
|
243
|
+
status?: { value?: string | number; label?: string } | null;
|
|
244
|
+
active_cancellation_request?: unknown;
|
|
243
245
|
product?: {
|
|
244
246
|
pk?: number;
|
|
245
247
|
name?: string;
|
|
@@ -278,7 +280,10 @@ export const getLineImage = (
|
|
|
278
280
|
};
|
|
279
281
|
|
|
280
282
|
export interface GroupedOrderLine<T extends OrderLineLike> {
|
|
281
|
-
/**
|
|
283
|
+
/**
|
|
284
|
+
* Stable React key: the product pk when the payload carries one (with a
|
|
285
|
+
* `-2`, `-3`… suffix for the product's further rows, see groupOrderItems).
|
|
286
|
+
*/
|
|
282
287
|
key: string;
|
|
283
288
|
name: string;
|
|
284
289
|
image: string;
|
|
@@ -301,21 +306,28 @@ export interface GroupedOrderLine<T extends OrderLineLike> {
|
|
|
301
306
|
* `quantity` is summed rather than counted so a payload that does carry a line
|
|
302
307
|
* quantity (older commerce versions, and brands that merge lines server-side)
|
|
303
308
|
* is respected instead of being flattened back to one.
|
|
309
|
+
*
|
|
310
|
+
* Units of one product only share a row while nothing sets them apart: once
|
|
311
|
+
* one of them is cancelled or returned (its status moves on, or it carries a
|
|
312
|
+
* cancellation / return request) it gets a row of its own. A shared row shows
|
|
313
|
+
* its first unit's status and return details, so "return 1 of 2" would
|
|
314
|
+
* otherwise read as both units returned.
|
|
304
315
|
*/
|
|
305
316
|
export const groupOrderItems = <T extends OrderLineLike>(
|
|
306
317
|
items: T[] | null | undefined
|
|
307
318
|
): GroupedOrderLine<T>[] => {
|
|
308
319
|
const source = Array.isArray(items) ? items : [];
|
|
320
|
+
const groups: GroupedOrderLine<T>[] = [];
|
|
321
|
+
const byIdentity = new Map<string, GroupedOrderLine<T>>();
|
|
322
|
+
const rowsPerKey = new Map<string, number>();
|
|
309
323
|
|
|
310
|
-
|
|
324
|
+
source.forEach((item) => {
|
|
311
325
|
const pk = item.product?.pk;
|
|
312
|
-
const key = String(pk ?? item.id ?? accumulator.length);
|
|
313
326
|
// Lines without a product pk cannot be proven identical, so they stay
|
|
314
327
|
// separate rows instead of collapsing into an arbitrary neighbour.
|
|
315
|
-
const
|
|
316
|
-
pk === undefined || pk === null
|
|
317
|
-
|
|
318
|
-
: accumulator.find((entry) => entry.key === key);
|
|
328
|
+
const identity =
|
|
329
|
+
pk === undefined || pk === null ? null : `${pk}#${lineState(item)}`;
|
|
330
|
+
const existing = identity === null ? undefined : byIdentity.get(identity);
|
|
319
331
|
const quantity = parseAmount(item.quantity) ?? 1;
|
|
320
332
|
const lineTotal = parseAmount(item.total_amount || item.price);
|
|
321
333
|
|
|
@@ -324,11 +336,15 @@ export const groupOrderItems = <T extends OrderLineLike>(
|
|
|
324
336
|
existing.total =
|
|
325
337
|
lineTotal === null ? existing.total : (existing.total ?? 0) + lineTotal;
|
|
326
338
|
|
|
327
|
-
return
|
|
339
|
+
return;
|
|
328
340
|
}
|
|
329
341
|
|
|
330
|
-
|
|
331
|
-
|
|
342
|
+
const baseKey = String(pk ?? item.id ?? groups.length);
|
|
343
|
+
const rows = (rowsPerKey.get(baseKey) ?? 0) + 1;
|
|
344
|
+
rowsPerKey.set(baseKey, rows);
|
|
345
|
+
|
|
346
|
+
const group: GroupedOrderLine<T> = {
|
|
347
|
+
key: rows === 1 ? baseKey : `${baseKey}-${rows}`,
|
|
332
348
|
name: item.product?.name || '',
|
|
333
349
|
image: getLineImage(item),
|
|
334
350
|
href: item.product?.absolute_url || '',
|
|
@@ -336,10 +352,30 @@ export const groupOrderItems = <T extends OrderLineLike>(
|
|
|
336
352
|
total: lineTotal,
|
|
337
353
|
quantity,
|
|
338
354
|
item
|
|
339
|
-
}
|
|
355
|
+
};
|
|
356
|
+
groups.push(group);
|
|
357
|
+
if (identity !== null) byIdentity.set(identity, group);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
return groups;
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* What tells one unit of a product apart from another in the same order: its
|
|
365
|
+
* status and the cancellation / return request it carries (one object or a
|
|
366
|
+
* one-item array, depending on the pz version).
|
|
367
|
+
*/
|
|
368
|
+
const lineState = (item: OrderLineLike): string => {
|
|
369
|
+
const value = item.active_cancellation_request;
|
|
370
|
+
const request = (Array.isArray(value) ? value[0] : value) as
|
|
371
|
+
| { id?: unknown; easy_return?: { code?: unknown } | null }
|
|
372
|
+
| null
|
|
373
|
+
| undefined;
|
|
374
|
+
const requestKey = request
|
|
375
|
+
? String(request.id ?? request.easy_return?.code ?? 'request')
|
|
376
|
+
: '';
|
|
340
377
|
|
|
341
|
-
|
|
342
|
-
}, []);
|
|
378
|
+
return `${item.status?.value ?? ''}|${requestKey}`;
|
|
343
379
|
};
|
|
344
380
|
|
|
345
381
|
/**
|
|
@@ -41,7 +41,7 @@ const orderPayload = {
|
|
|
41
41
|
orderitem_set: [
|
|
42
42
|
{
|
|
43
43
|
price: '649.95',
|
|
44
|
-
status: {
|
|
44
|
+
status: { label: 'Shipped' },
|
|
45
45
|
product: {
|
|
46
46
|
name: 'Spring Sneaker',
|
|
47
47
|
image: '/i.jpg',
|
|
@@ -63,19 +63,6 @@ describe('buildOrderPageContext', () => {
|
|
|
63
63
|
expect(context.order.currency).toBe('TRY');
|
|
64
64
|
expect(context.order.items).toHaveLength(1);
|
|
65
65
|
expect(context.order.items[0].title).toBe('Spring Sneaker');
|
|
66
|
-
expect(context.order.items[0].status).toBe('Shipped');
|
|
67
|
-
expect(context.order.items[0].statusValue).toBe('500');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('stringifies a numeric order item status code', () => {
|
|
71
|
-
const context = buildOrderPageContext({
|
|
72
|
-
order: {
|
|
73
|
-
...orderPayload,
|
|
74
|
-
orderitem_set: [{ status: { value: 550, label: 'Delivered' } }]
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
expect(context.order.items[0].statusValue).toBe('550');
|
|
79
66
|
});
|
|
80
67
|
|
|
81
68
|
it('does not publish the identity fields the raw payload carries', () => {
|
|
@@ -59,9 +59,6 @@ const toRecord = (value: unknown): UnknownRecord =>
|
|
|
59
59
|
const toStringValue = (value: unknown, fallback = ''): string =>
|
|
60
60
|
typeof value === 'string' ? value : fallback;
|
|
61
61
|
|
|
62
|
-
const toCodeValue = (value: unknown): string =>
|
|
63
|
-
typeof value === 'number' ? String(value) : toStringValue(value);
|
|
64
|
-
|
|
65
62
|
const humanizeSlug = (value: string) =>
|
|
66
63
|
value
|
|
67
64
|
.split('-')
|
|
@@ -482,7 +479,6 @@ export const buildOrderPageContext = ({
|
|
|
482
479
|
unitPrice: toStringValue(item.price),
|
|
483
480
|
quantity: 1,
|
|
484
481
|
status: toStringValue(toRecord(item.status).label),
|
|
485
|
-
statusValue: toCodeValue(toRecord(item.status).value),
|
|
486
482
|
currency
|
|
487
483
|
};
|
|
488
484
|
});
|