@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.
- package/.turbo/turbo-build.log +1 -1
- 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/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
package/.turbo/turbo-build.log
CHANGED
|
@@ -24,7 +24,7 @@ function readErrorKey(value) {
|
|
|
24
24
|
}
|
|
25
25
|
function toApiError(status, result, fallbackKey, t) {
|
|
26
26
|
const key = readErrorKey(result) ?? fallbackKey;
|
|
27
|
-
return Object.assign(new Error(t(key, key)), { status });
|
|
27
|
+
return Object.assign(new Error(t(key, key)), { status, body: result });
|
|
28
28
|
}
|
|
29
29
|
function formatLineTitle(line, t) {
|
|
30
30
|
const identifier = line.lineNo !== null ? `#${line.lineNo}` : t("warranty_claims.form.lines.unnamed", "Unnamed line");
|
|
@@ -97,7 +97,10 @@ function ReceivingPanel({
|
|
|
97
97
|
setDialogLine(null);
|
|
98
98
|
await onRefresh();
|
|
99
99
|
} catch (err) {
|
|
100
|
-
if (surfaceRecordConflict(err, t, { onRefresh }))
|
|
100
|
+
if (surfaceRecordConflict(err, t, { onRefresh })) {
|
|
101
|
+
setDialogLine(null);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
101
104
|
const message = err instanceof Error && err.message ? err.message : t("warranty_claims.receiving.error.save", "Failed to save receiving details.");
|
|
102
105
|
setError(message);
|
|
103
106
|
flash(message, "error");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/warranty_claims/backend/components/ReceivingPanel.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@open-mercato/ui/primitives/select'\nimport { StatusBadge } from '@open-mercato/ui/primitives/status-badge'\nimport { Textarea } from '@open-mercato/ui/primitives/textarea'\nimport { ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\n\ntype ConditionGrade = 'A' | 'B' | 'C' | 'D'\n\ntype ReceivingLine = {\n id: string\n lineNo: number | null\n productName: string | null\n sku: string | null\n serialNumber: string | null\n conditionGrade: string | null\n quarantineStatus: string | null\n inspectionNotes: string | null\n updatedAt: string | null\n}\n\ntype MutationContext = {\n formId: string\n resourceKind: string\n resourceId?: string\n retryLastMutation: () => Promise<boolean>\n}\n\ntype ReceivingPanelProps = {\n claimId: string\n lines: ReceivingLine[]\n canManage: boolean\n receivingCapable: boolean\n onRefresh: () => Promise<void>\n}\n\nconst CONDITION_GRADES: ConditionGrade[] = ['A', 'B', 'C', 'D']\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction readErrorKey(value: unknown): string | null {\n if (!isRecord(value)) return null\n return typeof value.error === 'string' && value.error.trim().length ? value.error.trim() : null\n}\n\nfunction toApiError(status: number, result: unknown, fallbackKey: string, t: TranslateFn): Error & { status?: number } {\n const key = readErrorKey(result) ?? fallbackKey\n return Object.assign(new Error(t(key, key)), { status })\n}\n\nfunction formatLineTitle(line: ReceivingLine, t: TranslateFn): string {\n const identifier = line.lineNo !== null\n ? `#${line.lineNo}`\n : t('warranty_claims.form.lines.unnamed', 'Unnamed line')\n const product = line.productName ?? line.sku ?? line.serialNumber\n return product ? `${identifier} ${product}` : identifier\n}\n\nfunction gradeVariant(grade: string | null): 'success' | 'info' | 'warning' | 'error' | 'neutral' {\n if (grade === 'A') return 'success'\n if (grade === 'B') return 'info'\n if (grade === 'C') return 'warning'\n if (grade === 'D') return 'error'\n return 'neutral'\n}\n\nfunction quarantineVariant(status: string | null): 'success' | 'warning' | 'neutral' {\n if (status === 'held') return 'warning'\n if (status === 'released') return 'success'\n return 'neutral'\n}\n\nexport function ReceivingPanel({\n claimId,\n lines,\n canManage,\n receivingCapable,\n onRefresh,\n}: ReceivingPanelProps) {\n const t = useT()\n const [dialogLine, setDialogLine] = React.useState<ReceivingLine | null>(null)\n const [conditionGrade, setConditionGrade] = React.useState<ConditionGrade>('A')\n const [inspectionNotes, setInspectionNotes] = React.useState('')\n const [saving, setSaving] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const { runMutation, retryLastMutation } = useGuardedMutation<MutationContext>({\n contextId: `warranty-claim-receiving:${claimId}`,\n blockedMessage: t('warranty_claims.common.saveBlocked', 'Save blocked by a mutation guard.'),\n })\n\n React.useEffect(() => {\n if (!dialogLine) return\n const nextGrade = CONDITION_GRADES.includes(dialogLine.conditionGrade as ConditionGrade)\n ? dialogLine.conditionGrade as ConditionGrade\n : 'A'\n setConditionGrade(nextGrade)\n setInspectionNotes(dialogLine.inspectionNotes ?? '')\n setError(null)\n }, [dialogLine])\n\n if (!receivingCapable || lines.length === 0) return null\n\n const runReceivingAction = async (line: ReceivingLine, body: Record<string, unknown>, successKey: string) => {\n setSaving(true)\n setError(null)\n try {\n await runMutation({\n operation: async () => {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(line.updatedAt),\n () => apiCall('/api/warranty_claims/receiving', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ ...body, updatedAt: line.updatedAt }),\n }),\n )\n if (!call.ok) throw toApiError(call.status, call.result, 'warranty_claims.receiving.error.save', t)\n return call\n },\n context: {\n formId: `warranty-claim-receiving:${claimId}:${line.id}`,\n resourceKind: 'warranty_claims.claim_line',\n resourceId: line.id,\n retryLastMutation,\n },\n mutationPayload: body,\n })\n flash(t(successKey, 'Receiving action saved.'), 'success')\n setDialogLine(null)\n await onRefresh()\n } catch (err) {\n if (surfaceRecordConflict(err, t, { onRefresh })) return\n const message = err instanceof Error && err.message\n ? err.message\n : t('warranty_claims.receiving.error.save', 'Failed to save receiving details.')\n setError(message)\n flash(message, 'error')\n } finally {\n setSaving(false)\n }\n }\n\n const submitGrade = async () => {\n if (!dialogLine) return\n await runReceivingAction(\n dialogLine,\n {\n lineId: dialogLine.id,\n conditionGrade,\n inspectionNotes: inspectionNotes.trim() || undefined,\n },\n 'warranty_claims.receiving.flash.saved',\n )\n }\n\n const releaseLine = async (line: ReceivingLine) => {\n await runReceivingAction(\n line,\n { lineId: line.id, action: 'release' },\n 'warranty_claims.receiving.flash.released',\n )\n }\n\n return (\n <div className=\"rounded-lg border border-border bg-card p-4 shadow-sm\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h3 className=\"text-sm font-semibold\">{t('warranty_claims.receiving.title', 'Receiving and grading')}</h3>\n <p className=\"text-xs text-muted-foreground\">\n {t('warranty_claims.receiving.description', 'Capture received condition grades and quarantine status per line.')}\n </p>\n </div>\n {!canManage ? (\n <StatusBadge variant=\"neutral\">\n {t('warranty_claims.receiving.readOnly', 'Read only')}\n </StatusBadge>\n ) : null}\n </div>\n {error ? (\n <div className=\"mt-3\">\n <ErrorMessage label={error} />\n </div>\n ) : null}\n <div className=\"mt-3 space-y-3\">\n {lines.map((line) => (\n <div key={line.id} className=\"flex flex-wrap items-start justify-between gap-3 rounded-md border border-border p-3\">\n <div className=\"min-w-0 space-y-2\">\n <div className=\"text-sm font-medium\">{formatLineTitle(line, t)}</div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge variant={gradeVariant(line.conditionGrade)}>\n {line.conditionGrade\n ? t(`warranty_claims.receiving.grade.${line.conditionGrade}`, `Grade ${line.conditionGrade}`)\n : t('warranty_claims.receiving.grade.none', 'No grade')}\n </StatusBadge>\n <StatusBadge variant={quarantineVariant(line.quarantineStatus)}>\n {t(\n `warranty_claims.receiving.quarantine.${line.quarantineStatus ?? 'none'}`,\n line.quarantineStatus ?? 'none',\n )}\n </StatusBadge>\n </div>\n {line.inspectionNotes ? (\n <p className=\"text-xs text-muted-foreground\">{line.inspectionNotes}</p>\n ) : null}\n </div>\n {canManage ? (\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => setDialogLine(line)}>\n {t('warranty_claims.receiving.actions.grade', 'Grade / Receive')}\n </Button>\n {line.quarantineStatus === 'held' ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n disabled={saving}\n onClick={() => { void releaseLine(line) }}\n >\n {t('warranty_claims.receiving.actions.release', 'Release')}\n </Button>\n ) : null}\n </div>\n ) : null}\n </div>\n ))}\n </div>\n\n <Dialog open={dialogLine !== null} onOpenChange={(open) => { if (!open) setDialogLine(null) }}>\n <DialogContent\n className=\"max-w-lg\"\n onKeyDown={(event) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void submitGrade()\n }\n if (event.key === 'Escape') {\n event.preventDefault()\n setDialogLine(null)\n }\n }}\n >\n <DialogHeader>\n <DialogTitle>{t('warranty_claims.receiving.dialog.title', 'Grade received line')}</DialogTitle>\n </DialogHeader>\n <div className=\"space-y-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"warranty-claim-condition-grade\">\n {t('warranty_claims.receiving.fields.conditionGrade', 'Condition grade')}\n </Label>\n <Select value={conditionGrade} onValueChange={(value) => setConditionGrade(value as ConditionGrade)}>\n <SelectTrigger id=\"warranty-claim-condition-grade\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {CONDITION_GRADES.map((grade) => (\n <SelectItem key={grade} value={grade}>\n {t(`warranty_claims.receiving.grade.${grade}`, `Grade ${grade}`)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"warranty-claim-inspection-notes\">\n {t('warranty_claims.receiving.fields.inspectionNotes', 'Inspection notes')}\n </Label>\n <Textarea\n id=\"warranty-claim-inspection-notes\"\n value={inspectionNotes}\n onChange={(event) => setInspectionNotes(event.target.value)}\n rows={4}\n />\n </div>\n </div>\n <DialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={() => setDialogLine(null)}>\n {t('common.cancel', 'Cancel')}\n </Button>\n <Button type=\"button\" disabled={saving} onClick={() => { void submitGrade() }}>\n {saving ? t('warranty_claims.common.saving', 'Saving...') : t('warranty_claims.form.submit', 'Save')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@open-mercato/ui/primitives/select'\nimport { StatusBadge } from '@open-mercato/ui/primitives/status-badge'\nimport { Textarea } from '@open-mercato/ui/primitives/textarea'\nimport { ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\n\ntype ConditionGrade = 'A' | 'B' | 'C' | 'D'\n\ntype ReceivingLine = {\n id: string\n lineNo: number | null\n productName: string | null\n sku: string | null\n serialNumber: string | null\n conditionGrade: string | null\n quarantineStatus: string | null\n inspectionNotes: string | null\n updatedAt: string | null\n}\n\ntype MutationContext = {\n formId: string\n resourceKind: string\n resourceId?: string\n retryLastMutation: () => Promise<boolean>\n}\n\ntype ReceivingPanelProps = {\n claimId: string\n lines: ReceivingLine[]\n canManage: boolean\n receivingCapable: boolean\n onRefresh: () => Promise<void>\n}\n\nconst CONDITION_GRADES: ConditionGrade[] = ['A', 'B', 'C', 'D']\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction readErrorKey(value: unknown): string | null {\n if (!isRecord(value)) return null\n return typeof value.error === 'string' && value.error.trim().length ? value.error.trim() : null\n}\n\nfunction toApiError(status: number, result: unknown, fallbackKey: string, t: TranslateFn): Error & { status: number; body: unknown } {\n const key = readErrorKey(result) ?? fallbackKey\n return Object.assign(new Error(t(key, key)), { status, body: result })\n}\n\nfunction formatLineTitle(line: ReceivingLine, t: TranslateFn): string {\n const identifier = line.lineNo !== null\n ? `#${line.lineNo}`\n : t('warranty_claims.form.lines.unnamed', 'Unnamed line')\n const product = line.productName ?? line.sku ?? line.serialNumber\n return product ? `${identifier} ${product}` : identifier\n}\n\nfunction gradeVariant(grade: string | null): 'success' | 'info' | 'warning' | 'error' | 'neutral' {\n if (grade === 'A') return 'success'\n if (grade === 'B') return 'info'\n if (grade === 'C') return 'warning'\n if (grade === 'D') return 'error'\n return 'neutral'\n}\n\nfunction quarantineVariant(status: string | null): 'success' | 'warning' | 'neutral' {\n if (status === 'held') return 'warning'\n if (status === 'released') return 'success'\n return 'neutral'\n}\n\nexport function ReceivingPanel({\n claimId,\n lines,\n canManage,\n receivingCapable,\n onRefresh,\n}: ReceivingPanelProps) {\n const t = useT()\n const [dialogLine, setDialogLine] = React.useState<ReceivingLine | null>(null)\n const [conditionGrade, setConditionGrade] = React.useState<ConditionGrade>('A')\n const [inspectionNotes, setInspectionNotes] = React.useState('')\n const [saving, setSaving] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const { runMutation, retryLastMutation } = useGuardedMutation<MutationContext>({\n contextId: `warranty-claim-receiving:${claimId}`,\n blockedMessage: t('warranty_claims.common.saveBlocked', 'Save blocked by a mutation guard.'),\n })\n\n React.useEffect(() => {\n if (!dialogLine) return\n const nextGrade = CONDITION_GRADES.includes(dialogLine.conditionGrade as ConditionGrade)\n ? dialogLine.conditionGrade as ConditionGrade\n : 'A'\n setConditionGrade(nextGrade)\n setInspectionNotes(dialogLine.inspectionNotes ?? '')\n setError(null)\n }, [dialogLine])\n\n if (!receivingCapable || lines.length === 0) return null\n\n const runReceivingAction = async (line: ReceivingLine, body: Record<string, unknown>, successKey: string) => {\n setSaving(true)\n setError(null)\n try {\n await runMutation({\n operation: async () => {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(line.updatedAt),\n () => apiCall('/api/warranty_claims/receiving', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ ...body, updatedAt: line.updatedAt }),\n }),\n )\n if (!call.ok) throw toApiError(call.status, call.result, 'warranty_claims.receiving.error.save', t)\n return call\n },\n context: {\n formId: `warranty-claim-receiving:${claimId}:${line.id}`,\n resourceKind: 'warranty_claims.claim_line',\n resourceId: line.id,\n retryLastMutation,\n },\n mutationPayload: body,\n })\n flash(t(successKey, 'Receiving action saved.'), 'success')\n setDialogLine(null)\n await onRefresh()\n } catch (err) {\n if (surfaceRecordConflict(err, t, { onRefresh })) {\n setDialogLine(null)\n return\n }\n const message = err instanceof Error && err.message\n ? err.message\n : t('warranty_claims.receiving.error.save', 'Failed to save receiving details.')\n setError(message)\n flash(message, 'error')\n } finally {\n setSaving(false)\n }\n }\n\n const submitGrade = async () => {\n if (!dialogLine) return\n await runReceivingAction(\n dialogLine,\n {\n lineId: dialogLine.id,\n conditionGrade,\n inspectionNotes: inspectionNotes.trim() || undefined,\n },\n 'warranty_claims.receiving.flash.saved',\n )\n }\n\n const releaseLine = async (line: ReceivingLine) => {\n await runReceivingAction(\n line,\n { lineId: line.id, action: 'release' },\n 'warranty_claims.receiving.flash.released',\n )\n }\n\n return (\n <div className=\"rounded-lg border border-border bg-card p-4 shadow-sm\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h3 className=\"text-sm font-semibold\">{t('warranty_claims.receiving.title', 'Receiving and grading')}</h3>\n <p className=\"text-xs text-muted-foreground\">\n {t('warranty_claims.receiving.description', 'Capture received condition grades and quarantine status per line.')}\n </p>\n </div>\n {!canManage ? (\n <StatusBadge variant=\"neutral\">\n {t('warranty_claims.receiving.readOnly', 'Read only')}\n </StatusBadge>\n ) : null}\n </div>\n {error ? (\n <div className=\"mt-3\">\n <ErrorMessage label={error} />\n </div>\n ) : null}\n <div className=\"mt-3 space-y-3\">\n {lines.map((line) => (\n <div key={line.id} className=\"flex flex-wrap items-start justify-between gap-3 rounded-md border border-border p-3\">\n <div className=\"min-w-0 space-y-2\">\n <div className=\"text-sm font-medium\">{formatLineTitle(line, t)}</div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge variant={gradeVariant(line.conditionGrade)}>\n {line.conditionGrade\n ? t(`warranty_claims.receiving.grade.${line.conditionGrade}`, `Grade ${line.conditionGrade}`)\n : t('warranty_claims.receiving.grade.none', 'No grade')}\n </StatusBadge>\n <StatusBadge variant={quarantineVariant(line.quarantineStatus)}>\n {t(\n `warranty_claims.receiving.quarantine.${line.quarantineStatus ?? 'none'}`,\n line.quarantineStatus ?? 'none',\n )}\n </StatusBadge>\n </div>\n {line.inspectionNotes ? (\n <p className=\"text-xs text-muted-foreground\">{line.inspectionNotes}</p>\n ) : null}\n </div>\n {canManage ? (\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => setDialogLine(line)}>\n {t('warranty_claims.receiving.actions.grade', 'Grade / Receive')}\n </Button>\n {line.quarantineStatus === 'held' ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n disabled={saving}\n onClick={() => { void releaseLine(line) }}\n >\n {t('warranty_claims.receiving.actions.release', 'Release')}\n </Button>\n ) : null}\n </div>\n ) : null}\n </div>\n ))}\n </div>\n\n <Dialog open={dialogLine !== null} onOpenChange={(open) => { if (!open) setDialogLine(null) }}>\n <DialogContent\n className=\"max-w-lg\"\n onKeyDown={(event) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void submitGrade()\n }\n if (event.key === 'Escape') {\n event.preventDefault()\n setDialogLine(null)\n }\n }}\n >\n <DialogHeader>\n <DialogTitle>{t('warranty_claims.receiving.dialog.title', 'Grade received line')}</DialogTitle>\n </DialogHeader>\n <div className=\"space-y-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"warranty-claim-condition-grade\">\n {t('warranty_claims.receiving.fields.conditionGrade', 'Condition grade')}\n </Label>\n <Select value={conditionGrade} onValueChange={(value) => setConditionGrade(value as ConditionGrade)}>\n <SelectTrigger id=\"warranty-claim-condition-grade\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {CONDITION_GRADES.map((grade) => (\n <SelectItem key={grade} value={grade}>\n {t(`warranty_claims.receiving.grade.${grade}`, `Grade ${grade}`)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"warranty-claim-inspection-notes\">\n {t('warranty_claims.receiving.fields.inspectionNotes', 'Inspection notes')}\n </Label>\n <Textarea\n id=\"warranty-claim-inspection-notes\"\n value={inspectionNotes}\n onChange={(event) => setInspectionNotes(event.target.value)}\n rows={4}\n />\n </div>\n </div>\n <DialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={() => setDialogLine(null)}>\n {t('common.cancel', 'Cancel')}\n </Button>\n <Button type=\"button\" disabled={saving} onClick={() => { void submitGrade() }}>\n {saving ? t('warranty_claims.common.saving', 'Saving...') : t('warranty_claims.form.submit', 'Save')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAsLQ,SACE,KADF;AApLR,YAAY,WAAW;AAEvB,SAAS,YAAY;AACrB,SAAS,cAAc;AACvB,SAAS,QAAQ,eAAe,cAAc,cAAc,mBAAmB;AAC/E,SAAS,aAAa;AACtB,SAAS,QAAQ,eAAe,YAAY,eAAe,mBAAmB;AAC9E,SAAS,mBAAmB;AAC5B,SAAS,gBAAgB;AACzB,SAAS,oBAAoB;AAC7B,SAAS,aAAa;AACtB,SAAS,0BAA0B;AACnC,SAAS,SAAS,mCAAmC;AACrD,SAAS,iCAAiC;AAC1C,SAAS,6BAA6B;AA+BtC,MAAM,mBAAqC,CAAC,KAAK,KAAK,KAAK,GAAG;AAE9D,SAAS,SAAS,OAAkD;AAClE,SAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,aAAa,OAA+B;AACnD,MAAI,CAAC,SAAS,KAAK,EAAG,QAAO;AAC7B,SAAO,OAAO,MAAM,UAAU,YAAY,MAAM,MAAM,KAAK,EAAE,SAAS,MAAM,MAAM,KAAK,IAAI;AAC7F;AAEA,SAAS,WAAW,QAAgB,QAAiB,aAAqB,GAA2D;AACnI,QAAM,MAAM,aAAa,MAAM,KAAK;AACpC,SAAO,OAAO,OAAO,IAAI,MAAM,EAAE,KAAK,GAAG,CAAC,GAAG,EAAE,QAAQ,MAAM,OAAO,CAAC;AACvE;AAEA,SAAS,gBAAgB,MAAqB,GAAwB;AACpE,QAAM,aAAa,KAAK,WAAW,OAC/B,IAAI,KAAK,MAAM,KACf,EAAE,sCAAsC,cAAc;AAC1D,QAAM,UAAU,KAAK,eAAe,KAAK,OAAO,KAAK;AACrD,SAAO,UAAU,GAAG,UAAU,IAAI,OAAO,KAAK;AAChD;AAEA,SAAS,aAAa,OAA4E;AAChG,MAAI,UAAU,IAAK,QAAO;AAC1B,MAAI,UAAU,IAAK,QAAO;AAC1B,MAAI,UAAU,IAAK,QAAO;AAC1B,MAAI,UAAU,IAAK,QAAO;AAC1B,SAAO;AACT;AAEA,SAAS,kBAAkB,QAA0D;AACnF,MAAI,WAAW,OAAQ,QAAO;AAC9B,MAAI,WAAW,WAAY,QAAO;AAClC,SAAO;AACT;AAEO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAwB;AACtB,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAA+B,IAAI;AAC7E,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,MAAM,SAAyB,GAAG;AAC9E,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,MAAM,SAAS,EAAE;AAC/D,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAS,KAAK;AAChD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAC5D,QAAM,EAAE,aAAa,kBAAkB,IAAI,mBAAoC;AAAA,IAC7E,WAAW,4BAA4B,OAAO;AAAA,IAC9C,gBAAgB,EAAE,sCAAsC,mCAAmC;AAAA,EAC7F,CAAC;AAED,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,WAAY;AACjB,UAAM,YAAY,iBAAiB,SAAS,WAAW,cAAgC,IACnF,WAAW,iBACX;AACJ,sBAAkB,SAAS;AAC3B,uBAAmB,WAAW,mBAAmB,EAAE;AACnD,aAAS,IAAI;AAAA,EACf,GAAG,CAAC,UAAU,CAAC;AAEf,MAAI,CAAC,oBAAoB,MAAM,WAAW,EAAG,QAAO;AAEpD,QAAM,qBAAqB,OAAO,MAAqB,MAA+B,eAAuB;AAC3G,cAAU,IAAI;AACd,aAAS,IAAI;AACb,QAAI;AACF,YAAM,YAAY;AAAA,QAChB,WAAW,YAAY;AACrB,gBAAM,OAAO,MAAM;AAAA,YACjB,0BAA0B,KAAK,SAAS;AAAA,YACxC,MAAM,QAAQ,kCAAkC;AAAA,cAC9C,QAAQ;AAAA,cACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,cAC9C,MAAM,KAAK,UAAU,EAAE,GAAG,MAAM,WAAW,KAAK,UAAU,CAAC;AAAA,YAC7D,CAAC;AAAA,UACH;AACA,cAAI,CAAC,KAAK,GAAI,OAAM,WAAW,KAAK,QAAQ,KAAK,QAAQ,wCAAwC,CAAC;AAClG,iBAAO;AAAA,QACT;AAAA,QACA,SAAS;AAAA,UACP,QAAQ,4BAA4B,OAAO,IAAI,KAAK,EAAE;AAAA,UACtD,cAAc;AAAA,UACd,YAAY,KAAK;AAAA,UACjB;AAAA,QACF;AAAA,QACA,iBAAiB;AAAA,MACnB,CAAC;AACD,YAAM,EAAE,YAAY,yBAAyB,GAAG,SAAS;AACzD,oBAAc,IAAI;AAClB,YAAM,UAAU;AAAA,IAClB,SAAS,KAAK;AACZ,UAAI,sBAAsB,KAAK,GAAG,EAAE,UAAU,CAAC,GAAG;AAChD,sBAAc,IAAI;AAClB;AAAA,MACF;AACA,YAAM,UAAU,eAAe,SAAS,IAAI,UACxC,IAAI,UACJ,EAAE,wCAAwC,mCAAmC;AACjF,eAAS,OAAO;AAChB,YAAM,SAAS,OAAO;AAAA,IACxB,UAAE;AACA,gBAAU,KAAK;AAAA,IACjB;AAAA,EACF;AAEA,QAAM,cAAc,YAAY;AAC9B,QAAI,CAAC,WAAY;AACjB,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,QACE,QAAQ,WAAW;AAAA,QACnB;AAAA,QACA,iBAAiB,gBAAgB,KAAK,KAAK;AAAA,MAC7C;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,cAAc,OAAO,SAAwB;AACjD,UAAM;AAAA,MACJ;AAAA,MACA,EAAE,QAAQ,KAAK,IAAI,QAAQ,UAAU;AAAA,MACrC;AAAA,IACF;AAAA,EACF;AAEA,SACE,qBAAC,SAAI,WAAU,yDACb;AAAA,yBAAC,SAAI,WAAU,oDACb;AAAA,2BAAC,SACC;AAAA,4BAAC,QAAG,WAAU,yBAAyB,YAAE,mCAAmC,uBAAuB,GAAE;AAAA,QACrG,oBAAC,OAAE,WAAU,iCACV,YAAE,yCAAyC,mEAAmE,GACjH;AAAA,SACF;AAAA,MACC,CAAC,YACA,oBAAC,eAAY,SAAQ,WAClB,YAAE,sCAAsC,WAAW,GACtD,IACE;AAAA,OACN;AAAA,IACC,QACC,oBAAC,SAAI,WAAU,QACb,8BAAC,gBAAa,OAAO,OAAO,GAC9B,IACE;AAAA,IACJ,oBAAC,SAAI,WAAU,kBACZ,gBAAM,IAAI,CAAC,SACV,qBAAC,SAAkB,WAAU,wFAC3B;AAAA,2BAAC,SAAI,WAAU,qBACb;AAAA,4BAAC,SAAI,WAAU,uBAAuB,0BAAgB,MAAM,CAAC,GAAE;AAAA,QAC/D,qBAAC,SAAI,WAAU,qCACb;AAAA,8BAAC,eAAY,SAAS,aAAa,KAAK,cAAc,GACnD,eAAK,iBACF,EAAE,mCAAmC,KAAK,cAAc,IAAI,SAAS,KAAK,cAAc,EAAE,IAC1F,EAAE,wCAAwC,UAAU,GAC1D;AAAA,UACA,oBAAC,eAAY,SAAS,kBAAkB,KAAK,gBAAgB,GAC1D;AAAA,YACC,wCAAwC,KAAK,oBAAoB,MAAM;AAAA,YACvE,KAAK,oBAAoB;AAAA,UAC3B,GACF;AAAA,WACF;AAAA,QACC,KAAK,kBACJ,oBAAC,OAAE,WAAU,iCAAiC,eAAK,iBAAgB,IACjE;AAAA,SACN;AAAA,MACC,YACC,qBAAC,SAAI,WAAU,qCACb;AAAA,4BAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,SAAS,MAAM,cAAc,IAAI,GAChF,YAAE,2CAA2C,iBAAiB,GACjE;AAAA,QACC,KAAK,qBAAqB,SACzB;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,UAAU;AAAA,YACV,SAAS,MAAM;AAAE,mBAAK,YAAY,IAAI;AAAA,YAAE;AAAA,YAEvC,YAAE,6CAA6C,SAAS;AAAA;AAAA,QAC3D,IACE;AAAA,SACN,IACE;AAAA,SArCI,KAAK,EAsCf,CACD,GACH;AAAA,IAEA,oBAAC,UAAO,MAAM,eAAe,MAAM,cAAc,CAAC,SAAS;AAAE,UAAI,CAAC,KAAM,eAAc,IAAI;AAAA,IAAE,GAC1F;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,WAAW,CAAC,UAAU;AACpB,eAAK,MAAM,WAAW,MAAM,YAAY,MAAM,QAAQ,SAAS;AAC7D,kBAAM,eAAe;AACrB,iBAAK,YAAY;AAAA,UACnB;AACA,cAAI,MAAM,QAAQ,UAAU;AAC1B,kBAAM,eAAe;AACrB,0BAAc,IAAI;AAAA,UACpB;AAAA,QACF;AAAA,QAEA;AAAA,8BAAC,gBACC,8BAAC,eAAa,YAAE,0CAA0C,qBAAqB,GAAE,GACnF;AAAA,UACA,qBAAC,SAAI,WAAU,aACb;AAAA,iCAAC,SAAI,WAAU,aACb;AAAA,kCAAC,SAAM,SAAQ,kCACZ,YAAE,mDAAmD,iBAAiB,GACzE;AAAA,cACA,qBAAC,UAAO,OAAO,gBAAgB,eAAe,CAAC,UAAU,kBAAkB,KAAuB,GAChG;AAAA,oCAAC,iBAAc,IAAG,kCAChB,8BAAC,eAAY,GACf;AAAA,gBACA,oBAAC,iBACE,2BAAiB,IAAI,CAAC,UACrB,oBAAC,cAAuB,OAAO,OAC5B,YAAE,mCAAmC,KAAK,IAAI,SAAS,KAAK,EAAE,KADhD,KAEjB,CACD,GACH;AAAA,iBACF;AAAA,eACF;AAAA,YACA,qBAAC,SAAI,WAAU,aACb;AAAA,kCAAC,SAAM,SAAQ,mCACZ,YAAE,oDAAoD,kBAAkB,GAC3E;AAAA,cACA;AAAA,gBAAC;AAAA;AAAA,kBACC,IAAG;AAAA,kBACH,OAAO;AAAA,kBACP,UAAU,CAAC,UAAU,mBAAmB,MAAM,OAAO,KAAK;AAAA,kBAC1D,MAAM;AAAA;AAAA,cACR;AAAA,eACF;AAAA,aACF;AAAA,UACA,qBAAC,gBACC;AAAA,gCAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,SAAS,MAAM,cAAc,IAAI,GACtE,YAAE,iBAAiB,QAAQ,GAC9B;AAAA,YACA,oBAAC,UAAO,MAAK,UAAS,UAAU,QAAQ,SAAS,MAAM;AAAE,mBAAK,YAAY;AAAA,YAAE,GACzE,mBAAS,EAAE,iCAAiC,WAAW,IAAI,EAAE,+BAA+B,MAAM,GACrG;AAAA,aACF;AAAA;AAAA;AAAA,IACF,GACF;AAAA,KACF;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -22,7 +22,7 @@ function readErrorKey(value) {
|
|
|
22
22
|
}
|
|
23
23
|
function toApiError(status, result, fallbackKey, t) {
|
|
24
24
|
const key = readErrorKey(result) ?? fallbackKey;
|
|
25
|
-
return Object.assign(new Error(t(key, key)), { status });
|
|
25
|
+
return Object.assign(new Error(t(key, key)), { status, body: result });
|
|
26
26
|
}
|
|
27
27
|
function isReturnLabelStatus(value) {
|
|
28
28
|
return isRecord(value) && (value.status === "created" || value.status === "notConfigured");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/warranty_claims/backend/components/ReturnLabelPanel.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { StatusBadge } from '@open-mercato/ui/primitives/status-badge'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\n\ntype ReturnLabelClaim = {\n id: string\n status: string | null\n updatedAt: string | null\n returnLabelUrl: string | null\n returnTrackingNumber: string | null\n returnCarrier: string | null\n}\n\ntype ReturnLabelResponse =\n | {\n status: 'created'\n labelUrl: string | null\n trackingNumber: string | null\n carrier: string | null\n }\n | { status: 'notConfigured' }\n\ntype MutationContext = {\n formId: string\n resourceKind: string\n resourceId?: string\n retryLastMutation: () => Promise<boolean>\n}\n\ntype ReturnLabelPanelProps = {\n claim: ReturnLabelClaim\n canManage: boolean\n onRefresh: () => Promise<void>\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction readErrorKey(value: unknown): string | null {\n if (!isRecord(value)) return null\n return typeof value.error === 'string' && value.error.trim().length ? value.error.trim() : null\n}\n\nfunction toApiError(status: number, result: unknown, fallbackKey: string, t: TranslateFn): Error & { status?: number } {\n const key = readErrorKey(result) ?? fallbackKey\n return Object.assign(new Error(t(key, key)), { status })\n}\n\nfunction isReturnLabelStatus(value: unknown): value is ReturnLabelResponse {\n return isRecord(value) && (value.status === 'created' || value.status === 'notConfigured')\n}\n\nfunction cleanText(value: string): string | undefined {\n const trimmed = value.trim()\n return trimmed.length ? trimmed : undefined\n}\n\nexport function ReturnLabelPanel({ claim, canManage, onRefresh }: ReturnLabelPanelProps) {\n const t = useT()\n const [manualVisible, setManualVisible] = React.useState(false)\n const [autoHidden, setAutoHidden] = React.useState(false)\n const [labelUrl, setLabelUrl] = React.useState(claim.returnLabelUrl ?? '')\n const [trackingNumber, setTrackingNumber] = React.useState(claim.returnTrackingNumber ?? '')\n const [carrier, setCarrier] = React.useState(claim.returnCarrier ?? '')\n const [busy, setBusy] = React.useState<'generate' | 'manual' | null>(null)\n const [error, setError] = React.useState<string | null>(null)\n const { runMutation, retryLastMutation } = useGuardedMutation<MutationContext>({\n contextId: `warranty-claim-return-label:${claim.id}`,\n blockedMessage: t('warranty_claims.common.saveBlocked', 'Save blocked by a mutation guard.'),\n })\n\n React.useEffect(() => {\n setLabelUrl(claim.returnLabelUrl ?? '')\n setTrackingNumber(claim.returnTrackingNumber ?? '')\n setCarrier(claim.returnCarrier ?? '')\n }, [claim.returnCarrier, claim.returnLabelUrl, claim.returnTrackingNumber])\n\n if (claim.status !== 'approved' && claim.status !== 'awaiting_return') return null\n\n const hasLabel = Boolean(claim.returnLabelUrl || claim.returnTrackingNumber || claim.returnCarrier)\n\n const runReturnLabelMutation = async (\n payload: Record<string, unknown>,\n mode: 'generate' | 'manual',\n ): Promise<ReturnLabelResponse | null> => {\n setBusy(mode)\n setError(null)\n try {\n const result = await runMutation({\n operation: async () => {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(claim.updatedAt),\n () => apiCall<ReturnLabelResponse>('/api/warranty_claims/return-label', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ ...payload, updatedAt: claim.updatedAt }),\n }),\n )\n if (!call.ok) throw toApiError(call.status, call.result, 'warranty_claims.returnLabel.error.save', t)\n if (!isReturnLabelStatus(call.result)) {\n throw new Error(t('warranty_claims.returnLabel.error.save', 'Failed to save return label.'))\n }\n return call.result\n },\n context: {\n formId: `warranty-claim-return-label:${claim.id}`,\n resourceKind: 'warranty_claims.claim',\n resourceId: claim.id,\n retryLastMutation,\n },\n mutationPayload: payload,\n })\n return result\n } catch (err) {\n if (surfaceRecordConflict(err, t, { onRefresh })) return null\n const message = err instanceof Error && err.message\n ? err.message\n : t('warranty_claims.returnLabel.error.save', 'Failed to save return label.')\n setError(message)\n flash(message, 'error')\n return null\n } finally {\n setBusy(null)\n }\n }\n\n const generateLabel = async () => {\n const result = await runReturnLabelMutation({ claimId: claim.id }, 'generate')\n if (!result) return\n if (result.status === 'notConfigured') {\n setAutoHidden(true)\n setManualVisible(true)\n flash(t('warranty_claims.returnLabel.flash.notConfigured', 'No return label provider is configured. Enter label details manually.'), 'info')\n return\n }\n flash(t('warranty_claims.returnLabel.flash.created', 'Return label created.'), 'success')\n await onRefresh()\n }\n\n const submitManual = async () => {\n const payload = {\n claimId: claim.id,\n manual: true,\n labelUrl: cleanText(labelUrl),\n trackingNumber: cleanText(trackingNumber),\n carrier: cleanText(carrier),\n }\n if (!payload.labelUrl && !payload.trackingNumber && !payload.carrier) {\n const message = t('warranty_claims.returnLabel.error.manualRequired', 'Enter at least one return label detail.')\n setError(message)\n flash(message, 'error')\n return\n }\n const result = await runReturnLabelMutation(payload, 'manual')\n if (!result || result.status !== 'created') return\n setManualVisible(false)\n flash(t('warranty_claims.returnLabel.flash.saved', 'Return label details saved.'), 'success')\n await onRefresh()\n }\n\n return (\n <div className=\"rounded-lg border border-border bg-card p-4 shadow-sm\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h3 className=\"text-sm font-semibold\">{t('warranty_claims.returnLabel.title', 'Return label')}</h3>\n <p className=\"text-xs text-muted-foreground\">\n {t('warranty_claims.returnLabel.description', 'Generate or record customer return shipping details.')}\n </p>\n </div>\n {hasLabel ? (\n <StatusBadge variant=\"success\">{t('warranty_claims.returnLabel.status.created', 'Label recorded')}</StatusBadge>\n ) : (\n <StatusBadge variant=\"neutral\">{t('warranty_claims.returnLabel.status.missing', 'No label')}</StatusBadge>\n )}\n </div>\n\n {error ? (\n <div className=\"mt-3\">\n <ErrorMessage label={error} />\n </div>\n ) : null}\n\n {hasLabel ? (\n <div className=\"mt-3 grid gap-3 sm:grid-cols-3\">\n <div className=\"rounded-md border border-border p-3\">\n <div className=\"text-xs text-muted-foreground\">{t('warranty_claims.returnLabel.fields.carrier', 'Carrier')}</div>\n <div className=\"text-sm font-medium\">{claim.returnCarrier ?? t('warranty_claims.common.noValue', 'No value')}</div>\n </div>\n <div className=\"rounded-md border border-border p-3\">\n <div className=\"text-xs text-muted-foreground\">{t('warranty_claims.returnLabel.fields.trackingNumber', 'Tracking number')}</div>\n <div className=\"text-sm font-medium\">{claim.returnTrackingNumber ?? t('warranty_claims.common.noValue', 'No value')}</div>\n </div>\n <div className=\"rounded-md border border-border p-3\">\n <div className=\"text-xs text-muted-foreground\">{t('warranty_claims.returnLabel.fields.labelUrl', 'Label')}</div>\n {claim.returnLabelUrl ? (\n <a\n href={claim.returnLabelUrl}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"text-sm font-medium text-foreground underline-offset-4 hover:underline\"\n >\n {t('warranty_claims.returnLabel.actions.openLabel', 'Open label')}\n </a>\n ) : (\n <div className=\"text-sm font-medium\">{t('warranty_claims.common.noValue', 'No value')}</div>\n )}\n </div>\n </div>\n ) : null}\n\n {canManage ? (\n <div className=\"mt-4 flex flex-wrap items-center gap-2\">\n {!autoHidden ? (\n <Button type=\"button\" variant=\"outline\" disabled={busy !== null} onClick={() => { void generateLabel() }}>\n {busy === 'generate' ? <Spinner size=\"sm\" /> : null}\n {t('warranty_claims.returnLabel.actions.generate', 'Generate return label')}\n </Button>\n ) : null}\n <Button type=\"button\" variant=\"outline\" disabled={busy !== null} onClick={() => setManualVisible((value) => !value)}>\n {manualVisible\n ? t('warranty_claims.returnLabel.actions.hideManual', 'Hide manual entry')\n : t('warranty_claims.returnLabel.actions.manual', 'Enter manually')}\n </Button>\n </div>\n ) : null}\n\n {canManage && manualVisible ? (\n <div\n className=\"mt-4 grid gap-3 sm:grid-cols-3\"\n onKeyDown={(event) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void submitManual()\n }\n if (event.key === 'Escape') {\n event.preventDefault()\n setManualVisible(false)\n }\n }}\n >\n <div className=\"space-y-2\">\n <Label htmlFor=\"warranty-return-label-url\">{t('warranty_claims.returnLabel.fields.labelUrl', 'Label URL')}</Label>\n <Input\n id=\"warranty-return-label-url\"\n value={labelUrl}\n onChange={(event) => setLabelUrl(event.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"warranty-return-tracking-number\">{t('warranty_claims.returnLabel.fields.trackingNumber', 'Tracking number')}</Label>\n <Input\n id=\"warranty-return-tracking-number\"\n value={trackingNumber}\n onChange={(event) => setTrackingNumber(event.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"warranty-return-carrier\">{t('warranty_claims.returnLabel.fields.carrier', 'Carrier')}</Label>\n <Input\n id=\"warranty-return-carrier\"\n value={carrier}\n onChange={(event) => setCarrier(event.target.value)}\n />\n </div>\n <div className=\"flex flex-wrap items-center gap-2 sm:col-span-3\">\n <Button type=\"button\" disabled={busy !== null} onClick={() => { void submitManual() }}>\n {busy === 'manual' ? <Spinner size=\"sm\" /> : null}\n {t('warranty_claims.returnLabel.actions.saveManual', 'Save return label')}\n </Button>\n <Button type=\"button\" variant=\"outline\" onClick={() => setManualVisible(false)}>\n {t('common.cancel', 'Cancel')}\n </Button>\n </div>\n </div>\n ) : null}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";AAiLQ,SACE,KADF;AA/KR,YAAY,WAAW;AAEvB,SAAS,YAAY;AACrB,SAAS,cAAc;AACvB,SAAS,aAAa;AACtB,SAAS,aAAa;AACtB,SAAS,mBAAmB;AAC5B,SAAS,eAAe;AACxB,SAAS,oBAAoB;AAC7B,SAAS,aAAa;AACtB,SAAS,0BAA0B;AACnC,SAAS,SAAS,mCAAmC;AACrD,SAAS,iCAAiC;AAC1C,SAAS,6BAA6B;AAiCtC,SAAS,SAAS,OAAkD;AAClE,SAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,aAAa,OAA+B;AACnD,MAAI,CAAC,SAAS,KAAK,EAAG,QAAO;AAC7B,SAAO,OAAO,MAAM,UAAU,YAAY,MAAM,MAAM,KAAK,EAAE,SAAS,MAAM,MAAM,KAAK,IAAI;AAC7F;AAEA,SAAS,WAAW,QAAgB,QAAiB,aAAqB,
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { StatusBadge } from '@open-mercato/ui/primitives/status-badge'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\n\ntype ReturnLabelClaim = {\n id: string\n status: string | null\n updatedAt: string | null\n returnLabelUrl: string | null\n returnTrackingNumber: string | null\n returnCarrier: string | null\n}\n\ntype ReturnLabelResponse =\n | {\n status: 'created'\n labelUrl: string | null\n trackingNumber: string | null\n carrier: string | null\n }\n | { status: 'notConfigured' }\n\ntype MutationContext = {\n formId: string\n resourceKind: string\n resourceId?: string\n retryLastMutation: () => Promise<boolean>\n}\n\ntype ReturnLabelPanelProps = {\n claim: ReturnLabelClaim\n canManage: boolean\n onRefresh: () => Promise<void>\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction readErrorKey(value: unknown): string | null {\n if (!isRecord(value)) return null\n return typeof value.error === 'string' && value.error.trim().length ? value.error.trim() : null\n}\n\nfunction toApiError(status: number, result: unknown, fallbackKey: string, t: TranslateFn): Error & { status: number; body: unknown } {\n const key = readErrorKey(result) ?? fallbackKey\n return Object.assign(new Error(t(key, key)), { status, body: result })\n}\n\nfunction isReturnLabelStatus(value: unknown): value is ReturnLabelResponse {\n return isRecord(value) && (value.status === 'created' || value.status === 'notConfigured')\n}\n\nfunction cleanText(value: string): string | undefined {\n const trimmed = value.trim()\n return trimmed.length ? trimmed : undefined\n}\n\nexport function ReturnLabelPanel({ claim, canManage, onRefresh }: ReturnLabelPanelProps) {\n const t = useT()\n const [manualVisible, setManualVisible] = React.useState(false)\n const [autoHidden, setAutoHidden] = React.useState(false)\n const [labelUrl, setLabelUrl] = React.useState(claim.returnLabelUrl ?? '')\n const [trackingNumber, setTrackingNumber] = React.useState(claim.returnTrackingNumber ?? '')\n const [carrier, setCarrier] = React.useState(claim.returnCarrier ?? '')\n const [busy, setBusy] = React.useState<'generate' | 'manual' | null>(null)\n const [error, setError] = React.useState<string | null>(null)\n const { runMutation, retryLastMutation } = useGuardedMutation<MutationContext>({\n contextId: `warranty-claim-return-label:${claim.id}`,\n blockedMessage: t('warranty_claims.common.saveBlocked', 'Save blocked by a mutation guard.'),\n })\n\n React.useEffect(() => {\n setLabelUrl(claim.returnLabelUrl ?? '')\n setTrackingNumber(claim.returnTrackingNumber ?? '')\n setCarrier(claim.returnCarrier ?? '')\n }, [claim.returnCarrier, claim.returnLabelUrl, claim.returnTrackingNumber])\n\n if (claim.status !== 'approved' && claim.status !== 'awaiting_return') return null\n\n const hasLabel = Boolean(claim.returnLabelUrl || claim.returnTrackingNumber || claim.returnCarrier)\n\n const runReturnLabelMutation = async (\n payload: Record<string, unknown>,\n mode: 'generate' | 'manual',\n ): Promise<ReturnLabelResponse | null> => {\n setBusy(mode)\n setError(null)\n try {\n const result = await runMutation({\n operation: async () => {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(claim.updatedAt),\n () => apiCall<ReturnLabelResponse>('/api/warranty_claims/return-label', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ ...payload, updatedAt: claim.updatedAt }),\n }),\n )\n if (!call.ok) throw toApiError(call.status, call.result, 'warranty_claims.returnLabel.error.save', t)\n if (!isReturnLabelStatus(call.result)) {\n throw new Error(t('warranty_claims.returnLabel.error.save', 'Failed to save return label.'))\n }\n return call.result\n },\n context: {\n formId: `warranty-claim-return-label:${claim.id}`,\n resourceKind: 'warranty_claims.claim',\n resourceId: claim.id,\n retryLastMutation,\n },\n mutationPayload: payload,\n })\n return result\n } catch (err) {\n if (surfaceRecordConflict(err, t, { onRefresh })) return null\n const message = err instanceof Error && err.message\n ? err.message\n : t('warranty_claims.returnLabel.error.save', 'Failed to save return label.')\n setError(message)\n flash(message, 'error')\n return null\n } finally {\n setBusy(null)\n }\n }\n\n const generateLabel = async () => {\n const result = await runReturnLabelMutation({ claimId: claim.id }, 'generate')\n if (!result) return\n if (result.status === 'notConfigured') {\n setAutoHidden(true)\n setManualVisible(true)\n flash(t('warranty_claims.returnLabel.flash.notConfigured', 'No return label provider is configured. Enter label details manually.'), 'info')\n return\n }\n flash(t('warranty_claims.returnLabel.flash.created', 'Return label created.'), 'success')\n await onRefresh()\n }\n\n const submitManual = async () => {\n const payload = {\n claimId: claim.id,\n manual: true,\n labelUrl: cleanText(labelUrl),\n trackingNumber: cleanText(trackingNumber),\n carrier: cleanText(carrier),\n }\n if (!payload.labelUrl && !payload.trackingNumber && !payload.carrier) {\n const message = t('warranty_claims.returnLabel.error.manualRequired', 'Enter at least one return label detail.')\n setError(message)\n flash(message, 'error')\n return\n }\n const result = await runReturnLabelMutation(payload, 'manual')\n if (!result || result.status !== 'created') return\n setManualVisible(false)\n flash(t('warranty_claims.returnLabel.flash.saved', 'Return label details saved.'), 'success')\n await onRefresh()\n }\n\n return (\n <div className=\"rounded-lg border border-border bg-card p-4 shadow-sm\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h3 className=\"text-sm font-semibold\">{t('warranty_claims.returnLabel.title', 'Return label')}</h3>\n <p className=\"text-xs text-muted-foreground\">\n {t('warranty_claims.returnLabel.description', 'Generate or record customer return shipping details.')}\n </p>\n </div>\n {hasLabel ? (\n <StatusBadge variant=\"success\">{t('warranty_claims.returnLabel.status.created', 'Label recorded')}</StatusBadge>\n ) : (\n <StatusBadge variant=\"neutral\">{t('warranty_claims.returnLabel.status.missing', 'No label')}</StatusBadge>\n )}\n </div>\n\n {error ? (\n <div className=\"mt-3\">\n <ErrorMessage label={error} />\n </div>\n ) : null}\n\n {hasLabel ? (\n <div className=\"mt-3 grid gap-3 sm:grid-cols-3\">\n <div className=\"rounded-md border border-border p-3\">\n <div className=\"text-xs text-muted-foreground\">{t('warranty_claims.returnLabel.fields.carrier', 'Carrier')}</div>\n <div className=\"text-sm font-medium\">{claim.returnCarrier ?? t('warranty_claims.common.noValue', 'No value')}</div>\n </div>\n <div className=\"rounded-md border border-border p-3\">\n <div className=\"text-xs text-muted-foreground\">{t('warranty_claims.returnLabel.fields.trackingNumber', 'Tracking number')}</div>\n <div className=\"text-sm font-medium\">{claim.returnTrackingNumber ?? t('warranty_claims.common.noValue', 'No value')}</div>\n </div>\n <div className=\"rounded-md border border-border p-3\">\n <div className=\"text-xs text-muted-foreground\">{t('warranty_claims.returnLabel.fields.labelUrl', 'Label')}</div>\n {claim.returnLabelUrl ? (\n <a\n href={claim.returnLabelUrl}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"text-sm font-medium text-foreground underline-offset-4 hover:underline\"\n >\n {t('warranty_claims.returnLabel.actions.openLabel', 'Open label')}\n </a>\n ) : (\n <div className=\"text-sm font-medium\">{t('warranty_claims.common.noValue', 'No value')}</div>\n )}\n </div>\n </div>\n ) : null}\n\n {canManage ? (\n <div className=\"mt-4 flex flex-wrap items-center gap-2\">\n {!autoHidden ? (\n <Button type=\"button\" variant=\"outline\" disabled={busy !== null} onClick={() => { void generateLabel() }}>\n {busy === 'generate' ? <Spinner size=\"sm\" /> : null}\n {t('warranty_claims.returnLabel.actions.generate', 'Generate return label')}\n </Button>\n ) : null}\n <Button type=\"button\" variant=\"outline\" disabled={busy !== null} onClick={() => setManualVisible((value) => !value)}>\n {manualVisible\n ? t('warranty_claims.returnLabel.actions.hideManual', 'Hide manual entry')\n : t('warranty_claims.returnLabel.actions.manual', 'Enter manually')}\n </Button>\n </div>\n ) : null}\n\n {canManage && manualVisible ? (\n <div\n className=\"mt-4 grid gap-3 sm:grid-cols-3\"\n onKeyDown={(event) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void submitManual()\n }\n if (event.key === 'Escape') {\n event.preventDefault()\n setManualVisible(false)\n }\n }}\n >\n <div className=\"space-y-2\">\n <Label htmlFor=\"warranty-return-label-url\">{t('warranty_claims.returnLabel.fields.labelUrl', 'Label URL')}</Label>\n <Input\n id=\"warranty-return-label-url\"\n value={labelUrl}\n onChange={(event) => setLabelUrl(event.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"warranty-return-tracking-number\">{t('warranty_claims.returnLabel.fields.trackingNumber', 'Tracking number')}</Label>\n <Input\n id=\"warranty-return-tracking-number\"\n value={trackingNumber}\n onChange={(event) => setTrackingNumber(event.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"warranty-return-carrier\">{t('warranty_claims.returnLabel.fields.carrier', 'Carrier')}</Label>\n <Input\n id=\"warranty-return-carrier\"\n value={carrier}\n onChange={(event) => setCarrier(event.target.value)}\n />\n </div>\n <div className=\"flex flex-wrap items-center gap-2 sm:col-span-3\">\n <Button type=\"button\" disabled={busy !== null} onClick={() => { void submitManual() }}>\n {busy === 'manual' ? <Spinner size=\"sm\" /> : null}\n {t('warranty_claims.returnLabel.actions.saveManual', 'Save return label')}\n </Button>\n <Button type=\"button\" variant=\"outline\" onClick={() => setManualVisible(false)}>\n {t('common.cancel', 'Cancel')}\n </Button>\n </div>\n </div>\n ) : null}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -7,6 +7,7 @@ import { Copy, Hash, Info, MessageSquare, Pencil, RefreshCw, Trash2, UserRound }
|
|
|
7
7
|
import { hasFeature } from "@open-mercato/shared/security/features";
|
|
8
8
|
import { parseBooleanFromUnknown } from "@open-mercato/shared/lib/boolean";
|
|
9
9
|
import { useLocale, useT } from "@open-mercato/shared/lib/i18n/context";
|
|
10
|
+
import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
|
|
10
11
|
import { formatDateTime, formatRelativeTime } from "@open-mercato/shared/lib/time";
|
|
11
12
|
import { Page, PageBody } from "@open-mercato/ui/backend/Page";
|
|
12
13
|
import { DataTable } from "@open-mercato/ui/backend/DataTable";
|
|
@@ -59,6 +60,7 @@ import {
|
|
|
59
60
|
import { AiAssessButtons } from "../../components/AiAssessButtons.js";
|
|
60
61
|
import { useUserDisplayNames } from "../../components/useUserDisplayNames.js";
|
|
61
62
|
import { ReceivingPanel } from "../../components/ReceivingPanel.js";
|
|
63
|
+
import { loadClaimLines } from "../../../lib/claimLines.js";
|
|
62
64
|
import { ReturnLabelPanel } from "../../components/ReturnLabelPanel.js";
|
|
63
65
|
import { VendorRecoverySuggestionsPanel } from "../../components/VendorRecoverySuggestionsPanel.js";
|
|
64
66
|
import { ClaimStageProgress } from "../../components/ClaimStageProgress.js";
|
|
@@ -636,11 +638,7 @@ function WarrantyClaimDetailPage({ params }) {
|
|
|
636
638
|
void 0,
|
|
637
639
|
{ fallback: { items: [] }, errorMessage: t("warranty_claims.detail.error.load") }
|
|
638
640
|
),
|
|
639
|
-
|
|
640
|
-
`/api/warranty_claims/lines?claimId=${encodeURIComponent(id)}&page=1&pageSize=100`,
|
|
641
|
-
void 0,
|
|
642
|
-
{ fallback: { items: [] }, errorMessage: t("warranty_claims.detail.error.loadLines") }
|
|
643
|
-
),
|
|
641
|
+
loadClaimLines(id, t("warranty_claims.detail.error.loadLines")),
|
|
644
642
|
readApiResultOrThrow(
|
|
645
643
|
`/api/warranty_claims/events?claimId=${encodeURIComponent(id)}`,
|
|
646
644
|
void 0,
|
|
@@ -1089,6 +1087,38 @@ function WarrantyClaimDetailPage({ params }) {
|
|
|
1089
1087
|
flash(translateErrorMessage(err, t, "warranty_claims.edit.error.delete"), "error");
|
|
1090
1088
|
}
|
|
1091
1089
|
}, [claim, confirm, currentStatus, featureAccess.claimDelete, loadData, mutationContext, router, runMutation, t]);
|
|
1090
|
+
const deleteLine = React.useCallback(async (line) => {
|
|
1091
|
+
if (!claim || !linesInlineEditable) return;
|
|
1092
|
+
const confirmed = await confirm({
|
|
1093
|
+
title: t("warranty_claims.form.lines.remove"),
|
|
1094
|
+
variant: "destructive"
|
|
1095
|
+
});
|
|
1096
|
+
if (!confirmed) return;
|
|
1097
|
+
const payload = { id: line.id, claimId: claim.id };
|
|
1098
|
+
try {
|
|
1099
|
+
await runMutation({
|
|
1100
|
+
operation: () => withScopedApiRequestHeaders(
|
|
1101
|
+
buildOptimisticLockHeader(line.updatedAt),
|
|
1102
|
+
() => deleteCrud("warranty_claims/lines", {
|
|
1103
|
+
body: payload,
|
|
1104
|
+
errorMessage: t("warranty_claims.detail.lines.error.delete")
|
|
1105
|
+
})
|
|
1106
|
+
),
|
|
1107
|
+
context: { ...mutationContext, resourceKind: "warranty_claims.claim_line", resourceId: line.id },
|
|
1108
|
+
mutationPayload: payload
|
|
1109
|
+
});
|
|
1110
|
+
flash(t("warranty_claims.detail.lines.flash.deleted"), "success");
|
|
1111
|
+
await loadData();
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
if (surfaceRecordConflict(err, t, { onRefresh: loadData })) return;
|
|
1114
|
+
getTelemetryRuntime()?.reportError(err, {
|
|
1115
|
+
module: "warranty_claims",
|
|
1116
|
+
code: "warranty_claims.line_delete_failed",
|
|
1117
|
+
attributes: { claimId: claim.id, lineId: line.id }
|
|
1118
|
+
});
|
|
1119
|
+
flash(translateErrorMessage(err, t, "warranty_claims.detail.lines.error.delete"), "error");
|
|
1120
|
+
}
|
|
1121
|
+
}, [claim, confirm, linesInlineEditable, loadData, mutationContext, runMutation, t]);
|
|
1092
1122
|
const headerMoreActions = React.useMemo(() => {
|
|
1093
1123
|
if (!claim) return [];
|
|
1094
1124
|
const items = [
|
|
@@ -1683,6 +1713,14 @@ function WarrantyClaimDetailPage({ params }) {
|
|
|
1683
1713
|
id: "edit",
|
|
1684
1714
|
label: t("warranty_claims.detail.lines.edit"),
|
|
1685
1715
|
onSelect: () => setLineDialog({ mode: "edit", line })
|
|
1716
|
+
},
|
|
1717
|
+
{
|
|
1718
|
+
id: "delete",
|
|
1719
|
+
label: t("warranty_claims.form.lines.remove"),
|
|
1720
|
+
destructive: true,
|
|
1721
|
+
onSelect: () => {
|
|
1722
|
+
void deleteLine(line);
|
|
1723
|
+
}
|
|
1686
1724
|
}
|
|
1687
1725
|
]
|
|
1688
1726
|
}
|
|
@@ -2033,16 +2071,7 @@ function WarrantyClaimDetailPage({ params }) {
|
|
|
2033
2071
|
}
|
|
2034
2072
|
setLineDialog(null);
|
|
2035
2073
|
await loadData();
|
|
2036
|
-
}
|
|
2037
|
-
onDelete: lineDialog?.mode === "edit" ? async () => {
|
|
2038
|
-
await deleteCrud("warranty_claims/lines", {
|
|
2039
|
-
body: { id: lineDialog.line.id, claimId: claim.id },
|
|
2040
|
-
errorMessage: t("warranty_claims.detail.lines.error.delete")
|
|
2041
|
-
});
|
|
2042
|
-
flash(t("warranty_claims.detail.lines.flash.deleted"), "success");
|
|
2043
|
-
setLineDialog(null);
|
|
2044
|
-
await loadData();
|
|
2045
|
-
} : void 0
|
|
2074
|
+
}
|
|
2046
2075
|
}
|
|
2047
2076
|
)
|
|
2048
2077
|
] }) }),
|