@open-mercato/core 0.10.1-develop.7879.1.80d3740285 → 0.10.1-develop.7888.1.96c8c2bd5f
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/attachments/lib/transferClient.js +72 -0
- package/dist/modules/attachments/lib/transferClient.js.map +7 -0
- package/dist/modules/attachments/lib/transferLimits.js +5 -0
- package/dist/modules/attachments/lib/transferLimits.js.map +7 -0
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js +23 -26
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/create/page.js +31 -23
- package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
- package/dist/modules/catalog/components/products/mediaTransfer.js +27 -0
- package/dist/modules/catalog/components/products/mediaTransfer.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js +5 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +2 -1
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/warranty_claims/backend/components/ReceivingPanel.js +5 -2
- package/dist/modules/warranty_claims/backend/components/ReceivingPanel.js.map +2 -2
- package/dist/modules/warranty_claims/backend/components/ReturnLabelPanel.js +1 -1
- package/dist/modules/warranty_claims/backend/components/ReturnLabelPanel.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js +44 -15
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js.map +2 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js +31 -5
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js.map +3 -3
- package/dist/modules/warranty_claims/lib/claimLines.js +20 -0
- package/dist/modules/warranty_claims/lib/claimLines.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/attachments/lib/transferClient.ts +87 -0
- package/src/modules/attachments/lib/transferLimits.ts +1 -0
- package/src/modules/catalog/backend/catalog/products/[productId]/variants/create/page.tsx +23 -32
- package/src/modules/catalog/backend/catalog/products/create/page.tsx +31 -28
- package/src/modules/catalog/components/products/mediaTransfer.ts +40 -0
- package/src/modules/catalog/i18n/de.json +2 -0
- package/src/modules/catalog/i18n/en.json +2 -0
- package/src/modules/catalog/i18n/es.json +2 -0
- package/src/modules/catalog/i18n/ko.json +2 -0
- package/src/modules/catalog/i18n/pl.json +2 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/Lane.tsx +6 -2
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +3 -2
- package/src/modules/warranty_claims/backend/components/ReceivingPanel.tsx +6 -3
- package/src/modules/warranty_claims/backend/components/ReturnLabelPanel.tsx +2 -2
- package/src/modules/warranty_claims/backend/warranty_claims/[id]/page.tsx +42 -16
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.tsx +33 -5
- package/src/modules/warranty_claims/lib/claimLines.ts +22 -0
|
@@ -35,10 +35,10 @@ import {
|
|
|
35
35
|
VariantPricesSection,
|
|
36
36
|
VariantMediaSection,
|
|
37
37
|
} from '@open-mercato/core/modules/catalog/components/products/VariantBuilder'
|
|
38
|
-
import type { ProductMediaItem } from '@open-mercato/core/modules/catalog/components/products/ProductMediaManager'
|
|
39
38
|
import { buildAttachmentImageUrl, slugifyAttachmentFileName } from '@open-mercato/core/modules/attachments/lib/imageUrls'
|
|
40
39
|
import { fetchOptionSchemaTemplate } from '../../../optionSchemaClient'
|
|
41
40
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
41
|
+
import { transferDraftMedia } from '@open-mercato/core/modules/catalog/components/products/mediaTransfer'
|
|
42
42
|
|
|
43
43
|
const logger = createLogger('catalog')
|
|
44
44
|
|
|
@@ -393,11 +393,19 @@ export default function CreateVariantPage({ params }: { params?: { productId?: s
|
|
|
393
393
|
if (!variantId) {
|
|
394
394
|
throw createCrudFormError(t('catalog.variants.form.errors.idMissing', 'Variant id missing after create.'))
|
|
395
395
|
}
|
|
396
|
-
await
|
|
396
|
+
const mediaTransfer = await transferDraftMedia({
|
|
397
|
+
entityId: E.catalog.catalog_product_variant,
|
|
397
398
|
draftId: values.mediaDraftId,
|
|
398
|
-
variantId,
|
|
399
|
+
recordId: variantId,
|
|
399
400
|
mediaItems: Array.isArray(values.mediaItems) ? values.mediaItems : [],
|
|
400
401
|
})
|
|
402
|
+
if (mediaTransfer.failedCount > 0) {
|
|
403
|
+
logger.error('attachments.transfer.failed', {
|
|
404
|
+
failed: mediaTransfer.failedCount,
|
|
405
|
+
requested: mediaTransfer.requestedCount,
|
|
406
|
+
errors: mediaTransfer.errors,
|
|
407
|
+
})
|
|
408
|
+
}
|
|
401
409
|
await syncVariantPrices({
|
|
402
410
|
priceKinds,
|
|
403
411
|
priceDrafts: values.prices ?? {},
|
|
@@ -408,7 +416,18 @@ export default function CreateVariantPage({ params }: { params?: { productId?: s
|
|
|
408
416
|
productTaxRateId,
|
|
409
417
|
productTaxRate,
|
|
410
418
|
})
|
|
411
|
-
|
|
419
|
+
if (mediaTransfer.failedCount > 0) {
|
|
420
|
+
flash(
|
|
421
|
+
t(
|
|
422
|
+
'catalog.variants.form.mediaTransferIncomplete',
|
|
423
|
+
'Variant created, but {failed} of {total} media files could not be attached.',
|
|
424
|
+
{ failed: mediaTransfer.failedCount, total: mediaTransfer.requestedCount },
|
|
425
|
+
),
|
|
426
|
+
'warning',
|
|
427
|
+
)
|
|
428
|
+
} else {
|
|
429
|
+
flash(t('catalog.variants.form.createSuccess', 'Variant created.'), 'success')
|
|
430
|
+
}
|
|
412
431
|
router.push(`/backend/catalog/products/${productId}#variants`)
|
|
413
432
|
}}
|
|
414
433
|
/>
|
|
@@ -417,34 +436,6 @@ export default function CreateVariantPage({ params }: { params?: { productId?: s
|
|
|
417
436
|
)
|
|
418
437
|
}
|
|
419
438
|
|
|
420
|
-
async function transferVariantMedia({
|
|
421
|
-
draftId,
|
|
422
|
-
variantId,
|
|
423
|
-
mediaItems,
|
|
424
|
-
}: {
|
|
425
|
-
draftId?: string | null
|
|
426
|
-
variantId: string
|
|
427
|
-
mediaItems: ProductMediaItem[]
|
|
428
|
-
}): Promise<void> {
|
|
429
|
-
if (!draftId || !variantId) return
|
|
430
|
-
const attachmentIds = mediaItems.map((item) => item.id).filter((id): id is string => typeof id === 'string' && id.length > 0)
|
|
431
|
-
if (!attachmentIds.length) return
|
|
432
|
-
await apiCall(
|
|
433
|
-
'/api/attachments/transfer',
|
|
434
|
-
{
|
|
435
|
-
method: 'POST',
|
|
436
|
-
headers: { 'content-type': 'application/json' },
|
|
437
|
-
body: JSON.stringify({
|
|
438
|
-
entityId: E.catalog.catalog_product_variant,
|
|
439
|
-
attachmentIds,
|
|
440
|
-
fromRecordId: draftId,
|
|
441
|
-
toRecordId: variantId,
|
|
442
|
-
}),
|
|
443
|
-
},
|
|
444
|
-
{ fallback: null },
|
|
445
|
-
)
|
|
446
|
-
}
|
|
447
|
-
|
|
448
439
|
async function syncVariantPrices({
|
|
449
440
|
priceKinds,
|
|
450
441
|
priceDrafts,
|
|
@@ -98,6 +98,7 @@ import {
|
|
|
98
98
|
type ProductUnitConversionInput,
|
|
99
99
|
} from "@open-mercato/core/modules/catalog/components/products/productFormUtils";
|
|
100
100
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
101
|
+
import { transferDraftMedia } from "@open-mercato/core/modules/catalog/components/products/mediaTransfer";
|
|
101
102
|
|
|
102
103
|
const logger = createLogger('catalog')
|
|
103
104
|
|
|
@@ -419,9 +420,6 @@ export default function CreateCatalogProductPage() {
|
|
|
419
420
|
const mediaItems = Array.isArray(formValues.mediaItems)
|
|
420
421
|
? formValues.mediaItems
|
|
421
422
|
: [];
|
|
422
|
-
const attachmentIds = mediaItems
|
|
423
|
-
.map((item) => (typeof item.id === "string" ? item.id : null))
|
|
424
|
-
.filter((value): value is string => !!value);
|
|
425
423
|
const mediaDraftId =
|
|
426
424
|
typeof formValues.mediaDraftId === "string"
|
|
427
425
|
? formValues.mediaDraftId
|
|
@@ -768,27 +766,18 @@ export default function CreateCatalogProductPage() {
|
|
|
768
766
|
await createCrud("catalog/prices", pricePayload);
|
|
769
767
|
}
|
|
770
768
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
fromRecordId: mediaDraftId,
|
|
784
|
-
toRecordId: productId,
|
|
785
|
-
}),
|
|
786
|
-
},
|
|
787
|
-
{ fallback: null },
|
|
788
|
-
);
|
|
789
|
-
if (!transfer.ok) {
|
|
790
|
-
logger.error("attachments.transfer.failed", { err: transfer.result?.error });
|
|
791
|
-
}
|
|
769
|
+
const mediaTransfer = await transferDraftMedia({
|
|
770
|
+
entityId: E.catalog.catalog_product,
|
|
771
|
+
draftId: mediaDraftId,
|
|
772
|
+
recordId: productId,
|
|
773
|
+
mediaItems,
|
|
774
|
+
});
|
|
775
|
+
if (mediaTransfer.failedCount > 0) {
|
|
776
|
+
logger.error("attachments.transfer.failed", {
|
|
777
|
+
failed: mediaTransfer.failedCount,
|
|
778
|
+
requested: mediaTransfer.requestedCount,
|
|
779
|
+
errors: mediaTransfer.errors,
|
|
780
|
+
});
|
|
792
781
|
}
|
|
793
782
|
|
|
794
783
|
if (inboxDraft) {
|
|
@@ -817,10 +806,24 @@ export default function CreateCatalogProductPage() {
|
|
|
817
806
|
}
|
|
818
807
|
}
|
|
819
808
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
809
|
+
if (mediaTransfer.failedCount > 0) {
|
|
810
|
+
flash(
|
|
811
|
+
t(
|
|
812
|
+
"catalog.products.create.mediaTransferIncomplete",
|
|
813
|
+
"Product created, but {failed} of {total} media files could not be attached.",
|
|
814
|
+
{
|
|
815
|
+
failed: mediaTransfer.failedCount,
|
|
816
|
+
total: mediaTransfer.requestedCount,
|
|
817
|
+
},
|
|
818
|
+
),
|
|
819
|
+
"warning",
|
|
820
|
+
);
|
|
821
|
+
} else {
|
|
822
|
+
flash(
|
|
823
|
+
t("catalog.products.create.success", "Product created."),
|
|
824
|
+
"success",
|
|
825
|
+
);
|
|
826
|
+
}
|
|
824
827
|
if (inboxDraft) {
|
|
825
828
|
router.push(
|
|
826
829
|
`/backend/inbox-ops/proposals/${encodeURIComponent(inboxDraft.proposalId)}`,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { transferAttachmentsInChunks } from '@open-mercato/core/modules/attachments/lib/transferClient'
|
|
2
|
+
import type { ProductMediaItem } from './ProductMediaManager'
|
|
3
|
+
|
|
4
|
+
export type DraftMediaTransferInput = {
|
|
5
|
+
entityId: string
|
|
6
|
+
draftId?: string | null
|
|
7
|
+
recordId: string
|
|
8
|
+
mediaItems: ProductMediaItem[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type DraftMediaTransferResult = {
|
|
12
|
+
requestedCount: number
|
|
13
|
+
failedCount: number
|
|
14
|
+
errors: string[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function transferDraftMedia({
|
|
18
|
+
entityId,
|
|
19
|
+
draftId,
|
|
20
|
+
recordId,
|
|
21
|
+
mediaItems,
|
|
22
|
+
}: DraftMediaTransferInput): Promise<DraftMediaTransferResult> {
|
|
23
|
+
const attachmentIds = mediaItems
|
|
24
|
+
.map((item) => item.id)
|
|
25
|
+
.filter((id): id is string => typeof id === 'string' && id.length > 0)
|
|
26
|
+
if (!draftId || !recordId || !attachmentIds.length) {
|
|
27
|
+
return { requestedCount: 0, failedCount: 0, errors: [] }
|
|
28
|
+
}
|
|
29
|
+
const result = await transferAttachmentsInChunks({
|
|
30
|
+
entityId,
|
|
31
|
+
attachmentIds,
|
|
32
|
+
fromRecordId: draftId,
|
|
33
|
+
toRecordId: recordId,
|
|
34
|
+
})
|
|
35
|
+
return {
|
|
36
|
+
requestedCount: attachmentIds.length,
|
|
37
|
+
failedCount: result.failedIds.length + result.unconfirmedCount,
|
|
38
|
+
errors: result.errors,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -416,6 +416,7 @@
|
|
|
416
416
|
"catalog.products.create.errors.variant": "Variante konnte nicht erstellt werden.",
|
|
417
417
|
"catalog.products.create.fields.currency": "Hauptwährung",
|
|
418
418
|
"catalog.products.create.handleHelp": "Handle wird für URLs verwendet und muss eindeutig sein.",
|
|
419
|
+
"catalog.products.create.mediaTransferIncomplete": "Produkt erstellt, aber {failed} von {total} Mediendateien konnten nicht angehängt werden.",
|
|
419
420
|
"catalog.products.create.meta.description": "Verwalte Untertitel, Handle und Währung für deine Storefronts.",
|
|
420
421
|
"catalog.products.create.meta.handleLabel": "Handle",
|
|
421
422
|
"catalog.products.create.meta.title": "Produkt-Metadaten",
|
|
@@ -882,6 +883,7 @@
|
|
|
882
883
|
"catalog.variants.form.isDefaultLabel": "Standardvariante",
|
|
883
884
|
"catalog.variants.form.loading": "Formular wird geladen...",
|
|
884
885
|
"catalog.variants.form.media": "Medien",
|
|
886
|
+
"catalog.variants.form.mediaTransferIncomplete": "Variante erstellt, aber {failed} von {total} Mediendateien konnten nicht angehängt werden.",
|
|
885
887
|
"catalog.variants.form.nameLabel": "Name",
|
|
886
888
|
"catalog.variants.form.namePlaceholder": "z. B. Blau / Klein",
|
|
887
889
|
"catalog.variants.form.nameRequired": "Geben Sie den Variantennamen an.",
|
|
@@ -416,6 +416,7 @@
|
|
|
416
416
|
"catalog.products.create.errors.variant": "Failed to create variant.",
|
|
417
417
|
"catalog.products.create.fields.currency": "Primary currency",
|
|
418
418
|
"catalog.products.create.handleHelp": "Handle is used for URLs and must be unique.",
|
|
419
|
+
"catalog.products.create.mediaTransferIncomplete": "Product created, but {failed} of {total} media files could not be attached.",
|
|
419
420
|
"catalog.products.create.meta.description": "Manage subtitle, handle, and currency for storefronts.",
|
|
420
421
|
"catalog.products.create.meta.handleLabel": "Handle",
|
|
421
422
|
"catalog.products.create.meta.title": "Product meta",
|
|
@@ -882,6 +883,7 @@
|
|
|
882
883
|
"catalog.variants.form.isDefaultLabel": "Default variant",
|
|
883
884
|
"catalog.variants.form.loading": "Loading form...",
|
|
884
885
|
"catalog.variants.form.media": "Media",
|
|
886
|
+
"catalog.variants.form.mediaTransferIncomplete": "Variant created, but {failed} of {total} media files could not be attached.",
|
|
885
887
|
"catalog.variants.form.nameLabel": "Name",
|
|
886
888
|
"catalog.variants.form.namePlaceholder": "e.g., Blue / Small",
|
|
887
889
|
"catalog.variants.form.nameRequired": "Provide the variant name.",
|
|
@@ -416,6 +416,7 @@
|
|
|
416
416
|
"catalog.products.create.errors.variant": "No se pudo crear la variante.",
|
|
417
417
|
"catalog.products.create.fields.currency": "Moneda principal",
|
|
418
418
|
"catalog.products.create.handleHelp": "El identificador se usa en URLs y debe ser único.",
|
|
419
|
+
"catalog.products.create.mediaTransferIncomplete": "Producto creado, pero no se pudieron adjuntar {failed} de {total} archivos multimedia.",
|
|
419
420
|
"catalog.products.create.meta.description": "Administra el subtítulo, el identificador y la moneda para las vitrinas.",
|
|
420
421
|
"catalog.products.create.meta.handleLabel": "Identificador",
|
|
421
422
|
"catalog.products.create.meta.title": "Metadatos del producto",
|
|
@@ -882,6 +883,7 @@
|
|
|
882
883
|
"catalog.variants.form.isDefaultLabel": "Variante predeterminada",
|
|
883
884
|
"catalog.variants.form.loading": "Cargando formulario...",
|
|
884
885
|
"catalog.variants.form.media": "Medios",
|
|
886
|
+
"catalog.variants.form.mediaTransferIncomplete": "Variante creada, pero no se pudieron adjuntar {failed} de {total} archivos multimedia.",
|
|
885
887
|
"catalog.variants.form.nameLabel": "Nombre",
|
|
886
888
|
"catalog.variants.form.namePlaceholder": "ej., Azul / Pequeño",
|
|
887
889
|
"catalog.variants.form.nameRequired": "Ingrese el nombre de la variante.",
|
|
@@ -416,6 +416,7 @@
|
|
|
416
416
|
"catalog.products.create.errors.variant": "변형 생성에 실패했습니다.",
|
|
417
417
|
"catalog.products.create.fields.currency": "기본 통화",
|
|
418
418
|
"catalog.products.create.handleHelp": "핸들은 URL에 사용되며 고유해야 합니다.",
|
|
419
|
+
"catalog.products.create.mediaTransferIncomplete": "상품이 생성되었지만 미디어 파일 {total}개 중 {failed}개를 첨부하지 못했습니다.",
|
|
419
420
|
"catalog.products.create.meta.description": "스토어프론트에 사용할 부제목, 핸들, 통화를 관리합니다.",
|
|
420
421
|
"catalog.products.create.meta.handleLabel": "핸들",
|
|
421
422
|
"catalog.products.create.meta.title": "상품 메타 정보",
|
|
@@ -882,6 +883,7 @@
|
|
|
882
883
|
"catalog.variants.form.isDefaultLabel": "기본 변형",
|
|
883
884
|
"catalog.variants.form.loading": "폼을 불러오는 중...",
|
|
884
885
|
"catalog.variants.form.media": "미디어",
|
|
886
|
+
"catalog.variants.form.mediaTransferIncomplete": "변형이 생성되었지만 미디어 파일 {total}개 중 {failed}개를 첨부하지 못했습니다.",
|
|
885
887
|
"catalog.variants.form.nameLabel": "이름",
|
|
886
888
|
"catalog.variants.form.namePlaceholder": "예: 블루 / 스몰",
|
|
887
889
|
"catalog.variants.form.nameRequired": "변형 이름을 입력하세요.",
|
|
@@ -416,6 +416,7 @@
|
|
|
416
416
|
"catalog.products.create.errors.variant": "Nie udało się utworzyć wariantu.",
|
|
417
417
|
"catalog.products.create.fields.currency": "Główna waluta",
|
|
418
418
|
"catalog.products.create.handleHelp": "Identyfikator jest używany w adresach URL i musi być unikalny.",
|
|
419
|
+
"catalog.products.create.mediaTransferIncomplete": "Produkt utworzony, ale nie udało się dołączyć {failed} z {total} plików multimedialnych.",
|
|
419
420
|
"catalog.products.create.meta.description": "Zarządzaj podtytułem, identyfikatorem i walutą dla witryn.",
|
|
420
421
|
"catalog.products.create.meta.handleLabel": "Identyfikator",
|
|
421
422
|
"catalog.products.create.meta.title": "Metadane produktu",
|
|
@@ -882,6 +883,7 @@
|
|
|
882
883
|
"catalog.variants.form.isDefaultLabel": "Domyślny wariant",
|
|
883
884
|
"catalog.variants.form.loading": "Ładowanie formularza...",
|
|
884
885
|
"catalog.variants.form.media": "Multimedia",
|
|
886
|
+
"catalog.variants.form.mediaTransferIncomplete": "Wariant utworzony, ale nie udało się dołączyć {failed} z {total} plików multimedialnych.",
|
|
885
887
|
"catalog.variants.form.nameLabel": "Nazwa",
|
|
886
888
|
"catalog.variants.form.namePlaceholder": "np. Niebieski / Mały",
|
|
887
889
|
"catalog.variants.form.nameRequired": "Podaj nazwę wariantu.",
|
|
@@ -36,6 +36,7 @@ type LaneProps = {
|
|
|
36
36
|
deals: DealCardData[]
|
|
37
37
|
/** Server-side aggregate for accurate counts/totals across all deals in this stage, not just loaded ones */
|
|
38
38
|
aggregate?: LaneAggregateProp | null
|
|
39
|
+
paginationTotal?: number
|
|
39
40
|
selectedDealIds: Set<string>
|
|
40
41
|
buildMenuItems: (deal: DealCardData) => RowActionItem[]
|
|
41
42
|
activeDragDealId?: string | null
|
|
@@ -176,6 +177,7 @@ function LaneImpl({
|
|
|
176
177
|
stage,
|
|
177
178
|
deals,
|
|
178
179
|
aggregate,
|
|
180
|
+
paginationTotal,
|
|
179
181
|
selectedDealIds,
|
|
180
182
|
buildMenuItems,
|
|
181
183
|
activeDragDealId,
|
|
@@ -212,7 +214,8 @@ function LaneImpl({
|
|
|
212
214
|
: null
|
|
213
215
|
const visibleCount = deals.length
|
|
214
216
|
const totalCount = stats.count
|
|
215
|
-
const
|
|
217
|
+
const remainingCount = (paginationTotal ?? totalCount) - visibleCount
|
|
218
|
+
const hasMoreDeals = remainingCount > 0
|
|
216
219
|
const accentClass = getAccentClass(stage.tone)
|
|
217
220
|
const countBadgeClass = getCountBadgeClass(stage.tone)
|
|
218
221
|
|
|
@@ -336,7 +339,7 @@ function LaneImpl({
|
|
|
336
339
|
t,
|
|
337
340
|
'customers.deals.kanban.lane.loadMore',
|
|
338
341
|
'Show more ({remaining})',
|
|
339
|
-
{ remaining:
|
|
342
|
+
{ remaining: remainingCount },
|
|
340
343
|
)}
|
|
341
344
|
</DashedTileButton>
|
|
342
345
|
) : null}
|
|
@@ -358,6 +361,7 @@ export const Lane = React.memo(LaneImpl, (prev, next) => {
|
|
|
358
361
|
if (prev.stage !== next.stage) return false
|
|
359
362
|
if (prev.deals !== next.deals) return false
|
|
360
363
|
if (prev.aggregate !== next.aggregate) return false
|
|
364
|
+
if (prev.paginationTotal !== next.paginationTotal) return false
|
|
361
365
|
if (prev.selectedDealIds !== next.selectedDealIds) return false
|
|
362
366
|
if (prev.buildMenuItems !== next.buildMenuItems) return false
|
|
363
367
|
if (prev.activeDragDealId !== next.activeDragDealId) return false
|
|
@@ -631,7 +631,7 @@ export default function DealsKanbanPage(): React.ReactElement {
|
|
|
631
631
|
)
|
|
632
632
|
|
|
633
633
|
// Aggregate query — gives accurate per-stage counts + totals (in base currency) across the entire pipeline,
|
|
634
|
-
// not just the loaded slice. Drives
|
|
634
|
+
// not just the loaded slice. Drives lane headers and currency breakdowns.
|
|
635
635
|
const aggregateQuery = useQuery<StageAggregateResponse | null>({
|
|
636
636
|
queryKey: [
|
|
637
637
|
'customers',
|
|
@@ -2853,7 +2853,7 @@ export default function DealsKanbanPage(): React.ReactElement {
|
|
|
2853
2853
|
/>
|
|
2854
2854
|
) : (
|
|
2855
2855
|
<>
|
|
2856
|
-
{lanes.map((stage) => {
|
|
2856
|
+
{lanes.map((stage, stageIndex) => {
|
|
2857
2857
|
const laneDeals = sortedDealsByStage.get(stage.id) ?? EMPTY_DEAL_ARRAY
|
|
2858
2858
|
return (
|
|
2859
2859
|
<Lane
|
|
@@ -2861,6 +2861,7 @@ export default function DealsKanbanPage(): React.ReactElement {
|
|
|
2861
2861
|
stage={stage}
|
|
2862
2862
|
deals={laneDeals}
|
|
2863
2863
|
aggregate={aggregateByStage.get(stage.id) ?? null}
|
|
2864
|
+
paginationTotal={laneQueries[stageIndex]?.data?.total ?? 0}
|
|
2864
2865
|
selectedDealIds={selectedDealIds}
|
|
2865
2866
|
buildMenuItems={buildMenuItems}
|
|
2866
2867
|
activeDragDealId={activeDragDealId}
|
|
@@ -56,9 +56,9 @@ function readErrorKey(value: unknown): string | null {
|
|
|
56
56
|
return typeof value.error === 'string' && value.error.trim().length ? value.error.trim() : null
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
function toApiError(status: number, result: unknown, fallbackKey: string, t: TranslateFn): Error & { status
|
|
59
|
+
function toApiError(status: number, result: unknown, fallbackKey: string, t: TranslateFn): Error & { status: number; body: unknown } {
|
|
60
60
|
const key = readErrorKey(result) ?? fallbackKey
|
|
61
|
-
return Object.assign(new Error(t(key, key)), { status })
|
|
61
|
+
return Object.assign(new Error(t(key, key)), { status, body: result })
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
function formatLineTitle(line: ReceivingLine, t: TranslateFn): string {
|
|
@@ -142,7 +142,10 @@ export function ReceivingPanel({
|
|
|
142
142
|
setDialogLine(null)
|
|
143
143
|
await onRefresh()
|
|
144
144
|
} catch (err) {
|
|
145
|
-
if (surfaceRecordConflict(err, t, { onRefresh }))
|
|
145
|
+
if (surfaceRecordConflict(err, t, { onRefresh })) {
|
|
146
|
+
setDialogLine(null)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
146
149
|
const message = err instanceof Error && err.message
|
|
147
150
|
? err.message
|
|
148
151
|
: t('warranty_claims.receiving.error.save', 'Failed to save receiving details.')
|
|
@@ -55,9 +55,9 @@ function readErrorKey(value: unknown): string | null {
|
|
|
55
55
|
return typeof value.error === 'string' && value.error.trim().length ? value.error.trim() : null
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function toApiError(status: number, result: unknown, fallbackKey: string, t: TranslateFn): Error & { status
|
|
58
|
+
function toApiError(status: number, result: unknown, fallbackKey: string, t: TranslateFn): Error & { status: number; body: unknown } {
|
|
59
59
|
const key = readErrorKey(result) ?? fallbackKey
|
|
60
|
-
return Object.assign(new Error(t(key, key)), { status })
|
|
60
|
+
return Object.assign(new Error(t(key, key)), { status, body: result })
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
function isReturnLabelStatus(value: unknown): value is ReturnLabelResponse {
|
|
@@ -9,6 +9,7 @@ import { hasFeature } from '@open-mercato/shared/security/features'
|
|
|
9
9
|
import { parseBooleanFromUnknown } from '@open-mercato/shared/lib/boolean'
|
|
10
10
|
import type { TranslateFn, TranslateParams } from '@open-mercato/shared/lib/i18n/context'
|
|
11
11
|
import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
|
|
12
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
12
13
|
import { formatDateTime, formatRelativeTime } from '@open-mercato/shared/lib/time'
|
|
13
14
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
14
15
|
import { DataTable } from '@open-mercato/ui/backend/DataTable'
|
|
@@ -67,6 +68,7 @@ import {
|
|
|
67
68
|
import { AiAssessButtons } from '../../components/AiAssessButtons'
|
|
68
69
|
import { useUserDisplayNames } from '../../components/useUserDisplayNames'
|
|
69
70
|
import { ReceivingPanel } from '../../components/ReceivingPanel'
|
|
71
|
+
import { loadClaimLines } from '../../../lib/claimLines'
|
|
70
72
|
import { ReturnLabelPanel } from '../../components/ReturnLabelPanel'
|
|
71
73
|
import { VendorRecoverySuggestionsPanel } from '../../components/VendorRecoverySuggestionsPanel'
|
|
72
74
|
import { ClaimStageProgress } from '../../components/ClaimStageProgress'
|
|
@@ -929,11 +931,7 @@ export default function WarrantyClaimDetailPage({ params }: { params?: { id?: st
|
|
|
929
931
|
undefined,
|
|
930
932
|
{ fallback: { items: [] }, errorMessage: t('warranty_claims.detail.error.load') },
|
|
931
933
|
),
|
|
932
|
-
|
|
933
|
-
`/api/warranty_claims/lines?claimId=${encodeURIComponent(id)}&page=1&pageSize=100`,
|
|
934
|
-
undefined,
|
|
935
|
-
{ fallback: { items: [] }, errorMessage: t('warranty_claims.detail.error.loadLines') },
|
|
936
|
-
),
|
|
934
|
+
loadClaimLines(id, t('warranty_claims.detail.error.loadLines')),
|
|
937
935
|
readApiResultOrThrow<{ items?: unknown[] }>(
|
|
938
936
|
`/api/warranty_claims/events?claimId=${encodeURIComponent(id)}`,
|
|
939
937
|
undefined,
|
|
@@ -1427,6 +1425,39 @@ export default function WarrantyClaimDetailPage({ params }: { params?: { id?: st
|
|
|
1427
1425
|
}
|
|
1428
1426
|
}, [claim, confirm, currentStatus, featureAccess.claimDelete, loadData, mutationContext, router, runMutation, t])
|
|
1429
1427
|
|
|
1428
|
+
const deleteLine = React.useCallback(async (line: ClaimLine) => {
|
|
1429
|
+
if (!claim || !linesInlineEditable) return
|
|
1430
|
+
const confirmed = await confirm({
|
|
1431
|
+
title: t('warranty_claims.form.lines.remove'),
|
|
1432
|
+
variant: 'destructive',
|
|
1433
|
+
})
|
|
1434
|
+
if (!confirmed) return
|
|
1435
|
+
const payload = { id: line.id, claimId: claim.id }
|
|
1436
|
+
try {
|
|
1437
|
+
await runMutation({
|
|
1438
|
+
operation: () => withScopedApiRequestHeaders(
|
|
1439
|
+
buildOptimisticLockHeader(line.updatedAt),
|
|
1440
|
+
() => deleteCrud('warranty_claims/lines', {
|
|
1441
|
+
body: payload,
|
|
1442
|
+
errorMessage: t('warranty_claims.detail.lines.error.delete'),
|
|
1443
|
+
}),
|
|
1444
|
+
),
|
|
1445
|
+
context: { ...mutationContext, resourceKind: 'warranty_claims.claim_line', resourceId: line.id },
|
|
1446
|
+
mutationPayload: payload,
|
|
1447
|
+
})
|
|
1448
|
+
flash(t('warranty_claims.detail.lines.flash.deleted'), 'success')
|
|
1449
|
+
await loadData()
|
|
1450
|
+
} catch (err) {
|
|
1451
|
+
if (surfaceRecordConflict(err, t, { onRefresh: loadData })) return
|
|
1452
|
+
getTelemetryRuntime()?.reportError(err, {
|
|
1453
|
+
module: 'warranty_claims',
|
|
1454
|
+
code: 'warranty_claims.line_delete_failed',
|
|
1455
|
+
attributes: { claimId: claim.id, lineId: line.id },
|
|
1456
|
+
})
|
|
1457
|
+
flash(translateErrorMessage(err, t, 'warranty_claims.detail.lines.error.delete'), 'error')
|
|
1458
|
+
}
|
|
1459
|
+
}, [claim, confirm, linesInlineEditable, loadData, mutationContext, runMutation, t])
|
|
1460
|
+
|
|
1430
1461
|
const headerMoreActions = React.useMemo(() => {
|
|
1431
1462
|
if (!claim) return []
|
|
1432
1463
|
const items = [
|
|
@@ -2090,6 +2121,12 @@ export default function WarrantyClaimDetailPage({ params }: { params?: { id?: st
|
|
|
2090
2121
|
label: t('warranty_claims.detail.lines.edit'),
|
|
2091
2122
|
onSelect: () => setLineDialog({ mode: 'edit', line }),
|
|
2092
2123
|
},
|
|
2124
|
+
{
|
|
2125
|
+
id: 'delete',
|
|
2126
|
+
label: t('warranty_claims.form.lines.remove'),
|
|
2127
|
+
destructive: true,
|
|
2128
|
+
onSelect: () => { void deleteLine(line) },
|
|
2129
|
+
},
|
|
2093
2130
|
]}
|
|
2094
2131
|
/>
|
|
2095
2132
|
) : undefined}
|
|
@@ -2533,17 +2570,6 @@ export default function WarrantyClaimDetailPage({ params }: { params?: { id?: st
|
|
|
2533
2570
|
setLineDialog(null)
|
|
2534
2571
|
await loadData()
|
|
2535
2572
|
}}
|
|
2536
|
-
onDelete={lineDialog?.mode === 'edit'
|
|
2537
|
-
? async () => {
|
|
2538
|
-
await deleteCrud('warranty_claims/lines', {
|
|
2539
|
-
body: { id: lineDialog.line.id, claimId: claim.id },
|
|
2540
|
-
errorMessage: t('warranty_claims.detail.lines.error.delete'),
|
|
2541
|
-
})
|
|
2542
|
-
flash(t('warranty_claims.detail.lines.flash.deleted'), 'success')
|
|
2543
|
-
setLineDialog(null)
|
|
2544
|
-
await loadData()
|
|
2545
|
-
}
|
|
2546
|
-
: undefined}
|
|
2547
2573
|
/>
|
|
2548
2574
|
</DialogContent>
|
|
2549
2575
|
</Dialog>
|
|
@@ -5,6 +5,7 @@ import Link from 'next/link'
|
|
|
5
5
|
import { ArrowLeft, Check, CircleAlert, Info, Paperclip, RefreshCw, ShieldCheck, Trash2, TriangleAlert } from 'lucide-react'
|
|
6
6
|
import { replaceWithPageReload } from '@open-mercato/core/modules/portal/lib/navigation'
|
|
7
7
|
import { useLocale, useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
8
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
8
9
|
import { localizeDictionaryLabel } from '@open-mercato/core/modules/warranty_claims/lib/dictionaryLabels'
|
|
9
10
|
import { formatQuantity } from '@open-mercato/core/modules/warranty_claims/lib/quantity'
|
|
10
11
|
import {
|
|
@@ -30,6 +31,7 @@ import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/u
|
|
|
30
31
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
31
32
|
import { usePortalContext } from '@open-mercato/ui/portal/PortalContext'
|
|
32
33
|
import { PortalEmptyState } from '@open-mercato/ui/portal/components/PortalEmptyState'
|
|
34
|
+
import { usePortalAppEvent } from '@open-mercato/ui/portal/hooks/usePortalAppEvent'
|
|
33
35
|
|
|
34
36
|
type Props = { params: { orgSlug: string; id: string } }
|
|
35
37
|
|
|
@@ -357,6 +359,8 @@ export default function WarrantyClaimPortalDetailPage({ params }: Props) {
|
|
|
357
359
|
const [actionSubmitting, setActionSubmitting] = React.useState(false)
|
|
358
360
|
const replacementInputRef = React.useRef<HTMLInputElement | null>(null)
|
|
359
361
|
const replacementTargetRef = React.useRef<PortalAttachment | null>(null)
|
|
362
|
+
const claimRequestSequenceRef = React.useRef(0)
|
|
363
|
+
const eventsRequestSequenceRef = React.useRef(0)
|
|
360
364
|
|
|
361
365
|
// Leaving an authenticated page for the login page crosses the public/authenticated
|
|
362
366
|
// boundary, so it must be a full page load — see `replaceWithPageReload`.
|
|
@@ -367,8 +371,11 @@ export default function WarrantyClaimPortalDetailPage({ params }: Props) {
|
|
|
367
371
|
}, [loading, user, params.orgSlug])
|
|
368
372
|
|
|
369
373
|
const refreshEvents = React.useCallback(async (claimId: string) => {
|
|
374
|
+
const requestSequence = ++eventsRequestSequenceRef.current
|
|
370
375
|
const result = await apiCall<EventResponse>(`/api/warranty_claims/portal/events?claimId=${encodeURIComponent(claimId)}`)
|
|
371
|
-
if (result.ok && result.result)
|
|
376
|
+
if (requestSequence === eventsRequestSequenceRef.current && result.ok && result.result) {
|
|
377
|
+
setEvents(result.result.items)
|
|
378
|
+
}
|
|
372
379
|
}, [])
|
|
373
380
|
|
|
374
381
|
const refreshAttachments = React.useCallback(async (claimId: string) => {
|
|
@@ -376,12 +383,14 @@ export default function WarrantyClaimPortalDetailPage({ params }: Props) {
|
|
|
376
383
|
if (result.ok && result.result) setAttachments(result.result.items)
|
|
377
384
|
}, [])
|
|
378
385
|
|
|
379
|
-
const loadClaim = React.useCallback(async () => {
|
|
380
|
-
|
|
386
|
+
const loadClaim = React.useCallback(async (showLoading = true) => {
|
|
387
|
+
const requestSequence = ++claimRequestSequenceRef.current
|
|
388
|
+
if (showLoading) setIsLoading(true)
|
|
381
389
|
setError(null)
|
|
382
390
|
setNotFound(false)
|
|
383
391
|
try {
|
|
384
392
|
const detail = await apiCall<ClaimResponse>(`/api/warranty_claims/portal/claims/${encodeURIComponent(params.id)}`)
|
|
393
|
+
if (requestSequence !== claimRequestSequenceRef.current) return
|
|
385
394
|
if (detail.status === 404) {
|
|
386
395
|
setClaim(null)
|
|
387
396
|
setNotFound(true)
|
|
@@ -397,9 +406,11 @@ export default function WarrantyClaimPortalDetailPage({ params }: Props) {
|
|
|
397
406
|
refreshAttachments(detail.result.item.id),
|
|
398
407
|
])
|
|
399
408
|
} catch {
|
|
400
|
-
|
|
409
|
+
if (requestSequence === claimRequestSequenceRef.current) {
|
|
410
|
+
setError(t('warranty_claims.portal.detail.loadError'))
|
|
411
|
+
}
|
|
401
412
|
} finally {
|
|
402
|
-
setIsLoading(false)
|
|
413
|
+
if (requestSequence === claimRequestSequenceRef.current) setIsLoading(false)
|
|
403
414
|
}
|
|
404
415
|
}, [params.id, refreshAttachments, refreshEvents, t])
|
|
405
416
|
|
|
@@ -407,6 +418,23 @@ export default function WarrantyClaimPortalDetailPage({ params }: Props) {
|
|
|
407
418
|
if (user) void loadClaim()
|
|
408
419
|
}, [loadClaim, user])
|
|
409
420
|
|
|
421
|
+
usePortalAppEvent('warranty_claims.claim.portal_status_changed', (event) => {
|
|
422
|
+
if (!user || event.payload.claimId !== params.id) return
|
|
423
|
+
void loadClaim(false)
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
usePortalAppEvent('warranty_claims.claim.comment_added', (event) => {
|
|
427
|
+
if (!user || event.payload.claimId !== params.id) return
|
|
428
|
+
void refreshEvents(params.id).catch((error) => {
|
|
429
|
+
getTelemetryRuntime()?.reportError(error, {
|
|
430
|
+
module: 'warranty_claims',
|
|
431
|
+
code: 'warranty_claims.portal_timeline_refresh_failed',
|
|
432
|
+
attributes: { claimId: params.id },
|
|
433
|
+
})
|
|
434
|
+
setError(t('warranty_claims.portal.detail.loadError'))
|
|
435
|
+
})
|
|
436
|
+
})
|
|
437
|
+
|
|
410
438
|
React.useEffect(() => {
|
|
411
439
|
if (!user) return
|
|
412
440
|
let cancelled = false
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
2
|
+
|
|
3
|
+
type ClaimLinesPage = {
|
|
4
|
+
items?: unknown[]
|
|
5
|
+
totalPages?: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function loadClaimLines(claimId: string, errorMessage: string): Promise<{ items: unknown[] }> {
|
|
9
|
+
const readPage = (page: number) => readApiResultOrThrow<ClaimLinesPage>(
|
|
10
|
+
`/api/warranty_claims/lines?claimId=${encodeURIComponent(claimId)}&page=${page}&pageSize=100&sortField=lineNo&sortDir=asc`,
|
|
11
|
+
undefined,
|
|
12
|
+
{ fallback: { items: [] }, errorMessage },
|
|
13
|
+
)
|
|
14
|
+
const firstPage = await readPage(1)
|
|
15
|
+
const items = [...(firstPage.items ?? [])]
|
|
16
|
+
const totalPages = firstPage.totalPages ?? 1
|
|
17
|
+
for (let page = 2; page <= totalPages; page += 1) {
|
|
18
|
+
const payload = await readPage(page)
|
|
19
|
+
items.push(...(payload.items ?? []))
|
|
20
|
+
}
|
|
21
|
+
return { items }
|
|
22
|
+
}
|