@open-mercato/core 0.10.1-develop.7883.1.44ac2125f5 → 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.
@@ -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
- readApiResultOrThrow<{ items?: unknown[] }>(
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) setEvents(result.result.items)
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
- setIsLoading(true)
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
- setError(t('warranty_claims.portal.detail.loadError'))
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
+ }