@open-mercato/core 0.6.8-develop.7057.1.61440fc3bc → 0.6.8-develop.7063.1.664341cf65

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.
Files changed (31) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/acl.js +30 -5
  3. package/dist/modules/auth/acl.js.map +2 -2
  4. package/dist/modules/customers/backend/customers/companies/page.js +1 -1
  5. package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
  6. package/dist/modules/customers/backend/customers/people/page.js +1 -1
  7. package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
  8. package/dist/modules/sales/components/documents/ItemsSection.js +37 -15
  9. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  10. package/dist/modules/sales/components/documents/LineItemDialog.js +76 -12
  11. package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
  12. package/dist/modules/sales/components/documents/SalesDocumentForm.js +1 -0
  13. package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
  14. package/dist/modules/sales/components/documents/SalesOrderDraftLines.js +1 -1
  15. package/dist/modules/sales/components/documents/SalesOrderDraftLines.js.map +2 -2
  16. package/dist/modules/sales/components/documents/lineItemShipmentLock.js +54 -0
  17. package/dist/modules/sales/components/documents/lineItemShipmentLock.js.map +7 -0
  18. package/package.json +7 -7
  19. package/src/modules/auth/acl.ts +30 -6
  20. package/src/modules/customers/backend/customers/companies/page.tsx +1 -1
  21. package/src/modules/customers/backend/customers/people/page.tsx +1 -1
  22. package/src/modules/sales/components/documents/ItemsSection.tsx +48 -17
  23. package/src/modules/sales/components/documents/LineItemDialog.tsx +129 -13
  24. package/src/modules/sales/components/documents/SalesDocumentForm.tsx +1 -0
  25. package/src/modules/sales/components/documents/SalesOrderDraftLines.tsx +1 -1
  26. package/src/modules/sales/components/documents/lineItemShipmentLock.ts +74 -0
  27. package/src/modules/sales/i18n/de.json +3 -0
  28. package/src/modules/sales/i18n/en.json +3 -0
  29. package/src/modules/sales/i18n/es.json +3 -0
  30. package/src/modules/sales/i18n/ko.json +3 -0
  31. package/src/modules/sales/i18n/pl.json +3 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/sales/components/documents/SalesOrderDraftLines.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { Plus } from 'lucide-react'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { LineItemDialog } from './LineItemDialog'\nimport type { SalesLineRecord } from './lineItemTypes'\nimport { formatMoney, normalizeNumber } from './lineItemUtils'\n\nexport type SalesOrderLineDraft = {\n id: string\n payload: Record<string, unknown>\n record: SalesLineRecord\n}\n\ntype SalesOrderDraftLinesProps = {\n currencyCode: string | null | undefined\n organizationId: string | null\n tenantId: string | null\n lines: SalesOrderLineDraft[]\n error?: string | null\n onChange: (lines: SalesOrderLineDraft[]) => void\n}\n\nfunction draftId(): string {\n return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'\n ? crypto.randomUUID()\n : `draft-${Date.now()}-${Math.round(performance.now())}`\n}\n\nexport function createSalesOrderLineDraft(\n payload: Record<string, unknown>,\n id = draftId(),\n): SalesOrderLineDraft {\n const quantity = normalizeNumber(payload.quantity, 0)\n const unitPriceNet = normalizeNumber(payload.unitPriceNet, 0)\n const unitPriceGross = normalizeNumber(payload.unitPriceGross, unitPriceNet)\n const taxRate = normalizeNumber(payload.taxRate, 0)\n const totalNet = normalizeNumber(payload.totalNetAmount, unitPriceNet * quantity)\n const totalGross = normalizeNumber(payload.totalGrossAmount, unitPriceGross * quantity)\n const metadata = payload.metadata && typeof payload.metadata === 'object'\n ? payload.metadata as Record<string, unknown>\n : null\n const catalogSnapshot = payload.catalogSnapshot && typeof payload.catalogSnapshot === 'object'\n ? payload.catalogSnapshot as Record<string, unknown>\n : null\n\n return {\n id,\n payload: { ...payload },\n record: {\n id,\n name: typeof payload.name === 'string' ? payload.name : null,\n productId: typeof payload.productId === 'string' ? payload.productId : null,\n productVariantId: typeof payload.productVariantId === 'string' ? payload.productVariantId : null,\n quantity,\n quantityUnit: typeof payload.quantityUnit === 'string' ? payload.quantityUnit : null,\n normalizedQuantity: normalizeNumber(payload.normalizedQuantity, quantity),\n normalizedUnit: typeof payload.normalizedUnit === 'string' ? payload.normalizedUnit : null,\n currencyCode: typeof payload.currencyCode === 'string' ? payload.currencyCode : null,\n unitPriceNet,\n unitPriceGross,\n discountAmount: normalizeNumber(payload.discountAmount, 0) * quantity,\n discountPercent: normalizeNumber(payload.discountPercent, 0),\n taxRate,\n totalNet,\n totalGross,\n priceMode: payload.priceMode === 'net' ? 'net' : 'gross',\n uomSnapshot: null,\n metadata,\n catalogSnapshot,\n customFieldSetId: typeof payload.customFieldSetId === 'string' ? payload.customFieldSetId : null,\n customFields: payload.customFields && typeof payload.customFields === 'object'\n ? payload.customFields as Record<string, unknown>\n : null,\n status: null,\n statusEntryId: typeof payload.statusEntryId === 'string' ? payload.statusEntryId : null,\n },\n }\n}\n\nexport function SalesOrderDraftLines({\n currencyCode,\n organizationId,\n tenantId,\n lines,\n error,\n onChange,\n}: SalesOrderDraftLinesProps) {\n const t = useT()\n const [dialogOpen, setDialogOpen] = React.useState(false)\n const [editing, setEditing] = React.useState<SalesOrderLineDraft | null>(null)\n\n const columns = React.useMemo<ColumnDef<SalesOrderLineDraft>[]>(() => [\n {\n id: 'name',\n header: t('sales.documents.items.table.product', 'Product'),\n cell: ({ row }) => row.original.record.name ?? t('sales.documents.items.untitled', 'Untitled'),\n },\n {\n id: 'quantity',\n header: t('sales.documents.items.table.quantity', 'Qty'),\n cell: ({ row }) => row.original.record.quantityUnit\n ? `${row.original.record.quantity} ${row.original.record.quantityUnit}`\n : row.original.record.quantity,\n },\n {\n id: 'unitPrice',\n header: t('sales.documents.items.table.unit', 'Unit price'),\n cell: ({ row }) => formatMoney(row.original.record.unitPriceGross, row.original.record.currencyCode ?? currencyCode ?? undefined),\n },\n {\n id: 'total',\n header: t('sales.documents.items.table.total', 'Total'),\n cell: ({ row }) => formatMoney(row.original.record.totalGross, row.original.record.currencyCode ?? currencyCode ?? undefined),\n },\n ], [currencyCode, t])\n\n const openCreate = React.useCallback(() => {\n setEditing(null)\n setDialogOpen(true)\n }, [])\n\n const rowActions = React.useCallback((line: SalesOrderLineDraft) => (\n <RowActions\n items={[\n {\n id: 'edit',\n label: t('ui.actions.edit', 'Edit'),\n onSelect: () => {\n setEditing(line)\n setDialogOpen(true)\n },\n },\n {\n id: 'delete',\n label: t('ui.actions.delete', 'Delete'),\n destructive: true,\n onSelect: () => onChange(lines.filter((candidate) => candidate.id !== line.id)),\n },\n ]}\n />\n ), [lines, onChange, t])\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between gap-4\">\n <div>\n <h3 className=\"text-sm font-medium\">{t('sales.orders.form.lines', 'Line Items')}</h3>\n <p className=\"text-sm text-muted-foreground\">\n {t('sales.orders.linesRequired', 'Add at least one line item before creating the order.')}\n </p>\n </div>\n <Button type=\"button\" variant=\"outline\" onClick={openCreate}>\n <Plus className=\"h-4 w-4\" aria-hidden=\"true\" />\n {t('sales.documents.items.add', 'Add item')}\n </Button>\n </div>\n {error ? <p className=\"text-sm text-destructive\" role=\"alert\">{error}</p> : null}\n <DataTable\n columns={columns}\n data={lines}\n embedded\n disableRowClick\n rowActions={rowActions}\n emptyState={(\n <EmptyState\n size=\"sm\"\n title={t('sales.documents.items.empty', 'No items yet.')}\n description={t('sales.orders.linesRequired', 'Add at least one line item before creating the order.')}\n actions={<Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={openCreate}>{t('sales.documents.items.add', 'Add item')}</Button>}\n />\n )}\n />\n <LineItemDialog\n open={dialogOpen}\n onOpenChange={(open) => {\n setDialogOpen(open)\n if (!open) setEditing(null)\n }}\n kind=\"order\"\n currencyCode={currencyCode}\n organizationId={organizationId}\n tenantId={tenantId}\n initialLine={editing?.record ?? null}\n onDraftSaved={(payload, lineId) => {\n const draft = createSalesOrderLineDraft(payload, lineId ?? undefined)\n onChange(lineId\n ? lines.map((line) => line.id === lineId ? draft : line)\n : [...lines, draft])\n }}\n />\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { Plus } from 'lucide-react'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { LineItemDialog } from './LineItemDialog'\nimport type { SalesLineRecord } from './lineItemTypes'\nimport { formatMoney, normalizeNumber } from './lineItemUtils'\n\nexport type SalesOrderLineDraft = {\n id: string\n payload: Record<string, unknown>\n record: SalesLineRecord\n}\n\ntype SalesOrderDraftLinesProps = {\n currencyCode: string | null | undefined\n organizationId: string | null\n tenantId: string | null\n lines: SalesOrderLineDraft[]\n error?: string | null\n onChange: (lines: SalesOrderLineDraft[]) => void\n}\n\nfunction draftId(): string {\n return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'\n ? crypto.randomUUID()\n : `draft-${Date.now()}-${Math.round(performance.now())}`\n}\n\nexport function createSalesOrderLineDraft(\n payload: Record<string, unknown>,\n id = draftId(),\n): SalesOrderLineDraft {\n const quantity = normalizeNumber(payload.quantity, 0)\n const unitPriceNet = normalizeNumber(payload.unitPriceNet, 0)\n const unitPriceGross = normalizeNumber(payload.unitPriceGross, unitPriceNet)\n const taxRate = normalizeNumber(payload.taxRate, 0)\n const totalNet = normalizeNumber(payload.totalNetAmount, unitPriceNet * quantity)\n const totalGross = normalizeNumber(payload.totalGrossAmount, unitPriceGross * quantity)\n const metadata = payload.metadata && typeof payload.metadata === 'object'\n ? payload.metadata as Record<string, unknown>\n : null\n const catalogSnapshot = payload.catalogSnapshot && typeof payload.catalogSnapshot === 'object'\n ? payload.catalogSnapshot as Record<string, unknown>\n : null\n\n return {\n id,\n payload: { ...payload },\n record: {\n id,\n name: typeof payload.name === 'string' ? payload.name : null,\n productId: typeof payload.productId === 'string' ? payload.productId : null,\n productVariantId: typeof payload.productVariantId === 'string' ? payload.productVariantId : null,\n quantity,\n quantityUnit: typeof payload.quantityUnit === 'string' ? payload.quantityUnit : null,\n normalizedQuantity: normalizeNumber(payload.normalizedQuantity, quantity),\n normalizedUnit: typeof payload.normalizedUnit === 'string' ? payload.normalizedUnit : null,\n currencyCode: typeof payload.currencyCode === 'string' ? payload.currencyCode : null,\n unitPriceNet,\n unitPriceGross,\n discountAmount: normalizeNumber(payload.discountAmount, 0) * quantity,\n discountPercent: normalizeNumber(payload.discountPercent, 0),\n taxRate,\n totalNet,\n totalGross,\n priceMode: payload.priceMode === 'net' ? 'net' : 'gross',\n uomSnapshot: null,\n metadata,\n catalogSnapshot,\n customFieldSetId: typeof payload.customFieldSetId === 'string' ? payload.customFieldSetId : null,\n customFields: payload.customFields && typeof payload.customFields === 'object'\n ? payload.customFields as Record<string, unknown>\n : null,\n status: null,\n statusEntryId: typeof payload.statusEntryId === 'string' ? payload.statusEntryId : null,\n },\n }\n}\n\nexport function SalesOrderDraftLines({\n currencyCode,\n organizationId,\n tenantId,\n lines,\n error,\n onChange,\n}: SalesOrderDraftLinesProps) {\n const t = useT()\n const [dialogOpen, setDialogOpen] = React.useState(false)\n const [editing, setEditing] = React.useState<SalesOrderLineDraft | null>(null)\n\n const columns = React.useMemo<ColumnDef<SalesOrderLineDraft>[]>(() => [\n {\n id: 'name',\n header: t('sales.documents.items.table.product', 'Product'),\n cell: ({ row }) => row.original.record.name ?? t('sales.documents.items.untitled', 'Untitled'),\n },\n {\n id: 'quantity',\n header: t('sales.documents.items.table.quantity', 'Qty'),\n cell: ({ row }) => row.original.record.quantityUnit\n ? `${row.original.record.quantity} ${row.original.record.quantityUnit}`\n : row.original.record.quantity,\n },\n {\n id: 'unitPrice',\n header: t('sales.documents.items.table.unit', 'Unit price'),\n cell: ({ row }) => formatMoney(row.original.record.unitPriceGross, row.original.record.currencyCode ?? currencyCode ?? undefined),\n },\n {\n id: 'total',\n header: t('sales.documents.items.table.total', 'Total'),\n cell: ({ row }) => formatMoney(row.original.record.totalGross, row.original.record.currencyCode ?? currencyCode ?? undefined),\n },\n ], [currencyCode, t])\n\n const openCreate = React.useCallback(() => {\n setEditing(null)\n setDialogOpen(true)\n }, [])\n\n const rowActions = React.useCallback((line: SalesOrderLineDraft) => (\n <RowActions\n items={[\n {\n id: 'edit',\n label: t('ui.actions.edit', 'Edit'),\n onSelect: () => {\n setEditing(line)\n setDialogOpen(true)\n },\n },\n {\n id: 'delete',\n label: t('ui.actions.delete', 'Delete'),\n destructive: true,\n onSelect: () => onChange(lines.filter((candidate) => candidate.id !== line.id)),\n },\n ]}\n />\n ), [lines, onChange, t])\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between gap-4\">\n <div>\n <h3 className=\"text-sm font-medium\">{t('sales.orders.form.lines', 'Line Items')}</h3>\n <p className=\"text-sm text-muted-foreground\">\n {t('sales.orders.linesRequired', 'Add at least one line item before creating the order.')}\n </p>\n </div>\n <Button type=\"button\" variant=\"outline\" onClick={openCreate}>\n <Plus className=\"h-4 w-4\" aria-hidden=\"true\" />\n {t('sales.documents.items.add', 'Add item')}\n </Button>\n </div>\n {error ? <p className=\"text-sm text-status-error-text\" role=\"alert\">{error}</p> : null}\n <DataTable\n columns={columns}\n data={lines}\n embedded\n disableRowClick\n rowActions={rowActions}\n emptyState={(\n <EmptyState\n size=\"sm\"\n title={t('sales.documents.items.empty', 'No items yet.')}\n description={t('sales.orders.linesRequired', 'Add at least one line item before creating the order.')}\n actions={<Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={openCreate}>{t('sales.documents.items.add', 'Add item')}</Button>}\n />\n )}\n />\n <LineItemDialog\n open={dialogOpen}\n onOpenChange={(open) => {\n setDialogOpen(open)\n if (!open) setEditing(null)\n }}\n kind=\"order\"\n currencyCode={currencyCode}\n organizationId={organizationId}\n tenantId={tenantId}\n initialLine={editing?.record ?? null}\n onDraftSaved={(payload, lineId) => {\n const draft = createSalesOrderLineDraft(payload, lineId ?? undefined)\n onChange(lineId\n ? lines.map((line) => line.id === lineId ? draft : line)\n : [...lines, draft])\n }}\n />\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,54 @@
1
+ const SHIPPED_LINE_IMMUTABLE_PAYLOAD_FIELDS = [
2
+ "kind",
3
+ "productId",
4
+ "productVariantId",
5
+ "quantityUnit",
6
+ "unitPriceNet",
7
+ "unitPriceGross",
8
+ "priceId",
9
+ "priceMode",
10
+ "taxRateId",
11
+ "taxRate",
12
+ "taxAmount",
13
+ "discountAmount",
14
+ "discountPercent",
15
+ "catalogSnapshot",
16
+ "metadata"
17
+ ];
18
+ function scaleTotal(total, previousQuantity, nextQuantity) {
19
+ if (!Number.isFinite(total) || !Number.isFinite(previousQuantity) || previousQuantity <= 0) {
20
+ return void 0;
21
+ }
22
+ return total * (nextQuantity / previousQuantity);
23
+ }
24
+ function prepareShippedLineUpdatePayload(payload, currentLine) {
25
+ if (!currentLine) return payload;
26
+ const nextPayload = { ...payload };
27
+ for (const field of SHIPPED_LINE_IMMUTABLE_PAYLOAD_FIELDS) {
28
+ delete nextPayload[field];
29
+ }
30
+ delete nextPayload.totalNetAmount;
31
+ delete nextPayload.totalGrossAmount;
32
+ const nextQuantity = Number(nextPayload.quantity);
33
+ if (!Number.isFinite(nextQuantity) || nextQuantity === currentLine.quantity) {
34
+ return nextPayload;
35
+ }
36
+ const scaledNetTotal = scaleTotal(
37
+ currentLine.totalNetAmount,
38
+ currentLine.quantity,
39
+ nextQuantity
40
+ );
41
+ const scaledGrossTotal = scaleTotal(
42
+ currentLine.totalGrossAmount,
43
+ currentLine.quantity,
44
+ nextQuantity
45
+ );
46
+ if (scaledNetTotal !== void 0) nextPayload.totalNetAmount = scaledNetTotal;
47
+ if (scaledGrossTotal !== void 0)
48
+ nextPayload.totalGrossAmount = scaledGrossTotal;
49
+ return nextPayload;
50
+ }
51
+ export {
52
+ prepareShippedLineUpdatePayload
53
+ };
54
+ //# sourceMappingURL=lineItemShipmentLock.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/sales/components/documents/lineItemShipmentLock.ts"],
4
+ "sourcesContent": ["type ShippedLineSnapshot = {\n quantity: number;\n totalNetAmount?: number | null;\n totalGrossAmount?: number | null;\n};\n\nconst SHIPPED_LINE_IMMUTABLE_PAYLOAD_FIELDS = [\n \"kind\",\n \"productId\",\n \"productVariantId\",\n \"quantityUnit\",\n \"unitPriceNet\",\n \"unitPriceGross\",\n \"priceId\",\n \"priceMode\",\n \"taxRateId\",\n \"taxRate\",\n \"taxAmount\",\n \"discountAmount\",\n \"discountPercent\",\n \"catalogSnapshot\",\n \"metadata\",\n] as const;\n\nfunction scaleTotal(\n total: number | null | undefined,\n previousQuantity: number,\n nextQuantity: number,\n): number | undefined {\n if (\n !Number.isFinite(total) ||\n !Number.isFinite(previousQuantity) ||\n previousQuantity <= 0\n ) {\n return undefined;\n }\n return (total as number) * (nextQuantity / previousQuantity);\n}\n\nexport function prepareShippedLineUpdatePayload(\n payload: Record<string, unknown>,\n currentLine: ShippedLineSnapshot | null,\n): Record<string, unknown> {\n if (!currentLine) return payload;\n\n const nextPayload = { ...payload };\n for (const field of SHIPPED_LINE_IMMUTABLE_PAYLOAD_FIELDS) {\n delete nextPayload[field];\n }\n\n delete nextPayload.totalNetAmount;\n delete nextPayload.totalGrossAmount;\n\n const nextQuantity = Number(nextPayload.quantity);\n if (!Number.isFinite(nextQuantity) || nextQuantity === currentLine.quantity) {\n return nextPayload;\n }\n\n const scaledNetTotal = scaleTotal(\n currentLine.totalNetAmount,\n currentLine.quantity,\n nextQuantity,\n );\n const scaledGrossTotal = scaleTotal(\n currentLine.totalGrossAmount,\n currentLine.quantity,\n nextQuantity,\n );\n if (scaledNetTotal !== undefined) nextPayload.totalNetAmount = scaledNetTotal;\n if (scaledGrossTotal !== undefined)\n nextPayload.totalGrossAmount = scaledGrossTotal;\n\n return nextPayload;\n}\n"],
5
+ "mappings": "AAMA,MAAM,wCAAwC;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,SAAS,WACP,OACA,kBACA,cACoB;AACpB,MACE,CAAC,OAAO,SAAS,KAAK,KACtB,CAAC,OAAO,SAAS,gBAAgB,KACjC,oBAAoB,GACpB;AACA,WAAO;AAAA,EACT;AACA,SAAQ,SAAoB,eAAe;AAC7C;AAEO,SAAS,gCACd,SACA,aACyB;AACzB,MAAI,CAAC,YAAa,QAAO;AAEzB,QAAM,cAAc,EAAE,GAAG,QAAQ;AACjC,aAAW,SAAS,uCAAuC;AACzD,WAAO,YAAY,KAAK;AAAA,EAC1B;AAEA,SAAO,YAAY;AACnB,SAAO,YAAY;AAEnB,QAAM,eAAe,OAAO,YAAY,QAAQ;AAChD,MAAI,CAAC,OAAO,SAAS,YAAY,KAAK,iBAAiB,YAAY,UAAU;AAC3E,WAAO;AAAA,EACT;AAEA,QAAM,iBAAiB;AAAA,IACrB,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ;AAAA,EACF;AACA,QAAM,mBAAmB;AAAA,IACvB,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ;AAAA,EACF;AACA,MAAI,mBAAmB,OAAW,aAAY,iBAAiB;AAC/D,MAAI,qBAAqB;AACvB,gBAAY,mBAAmB;AAEjC,SAAO;AACT;",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.8-develop.7057.1.61440fc3bc",
3
+ "version": "0.6.8-develop.7063.1.664341cf65",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -252,16 +252,16 @@
252
252
  "zod": "^4.4.3"
253
253
  },
254
254
  "peerDependencies": {
255
- "@open-mercato/ai-assistant": "0.6.8-develop.7057.1.61440fc3bc",
256
- "@open-mercato/shared": "0.6.8-develop.7057.1.61440fc3bc",
257
- "@open-mercato/ui": "0.6.8-develop.7057.1.61440fc3bc",
255
+ "@open-mercato/ai-assistant": "0.6.8-develop.7063.1.664341cf65",
256
+ "@open-mercato/shared": "0.6.8-develop.7063.1.664341cf65",
257
+ "@open-mercato/ui": "0.6.8-develop.7063.1.664341cf65",
258
258
  "react": "^19.0.0",
259
259
  "react-dom": "^19.0.0"
260
260
  },
261
261
  "devDependencies": {
262
- "@open-mercato/ai-assistant": "0.6.8-develop.7057.1.61440fc3bc",
263
- "@open-mercato/shared": "0.6.8-develop.7057.1.61440fc3bc",
264
- "@open-mercato/ui": "0.6.8-develop.7057.1.61440fc3bc",
262
+ "@open-mercato/ai-assistant": "0.6.8-develop.7063.1.664341cf65",
263
+ "@open-mercato/shared": "0.6.8-develop.7063.1.664341cf65",
264
+ "@open-mercato/ui": "0.6.8-develop.7063.1.664341cf65",
265
265
  "@testing-library/dom": "^10.4.1",
266
266
  "@testing-library/jest-dom": "^7.0.0",
267
267
  "@testing-library/react": "^16.3.1",
@@ -1,14 +1,38 @@
1
1
  // Module-level features declaration for RBAC
2
2
  export const features = [
3
3
  { id: 'auth.users.list', title: 'List users', module: 'auth' },
4
- { id: 'auth.users.create', title: 'Create users', module: 'auth' },
5
- { id: 'auth.users.edit', title: 'Edit users', module: 'auth' },
6
- { id: 'auth.users.delete', title: 'Delete users', module: 'auth' },
4
+ {
5
+ id: 'auth.users.create',
6
+ title: 'Create users',
7
+ module: 'auth',
8
+ dependsOn: ['auth.users.list', 'auth.roles.list', 'directory.organizations.view'],
9
+ },
10
+ {
11
+ id: 'auth.users.edit',
12
+ title: 'Edit users',
13
+ module: 'auth',
14
+ dependsOn: ['auth.users.list', 'auth.roles.list'],
15
+ },
16
+ {
17
+ id: 'auth.users.delete',
18
+ title: 'Delete users',
19
+ module: 'auth',
20
+ dependsOn: ['auth.users.list'],
21
+ },
7
22
  { id: 'auth.roles.list', title: 'List roles', module: 'auth' },
8
- { id: 'auth.roles.manage', title: 'Manage roles', module: 'auth' },
9
- { id: 'auth.acl.manage', title: 'Manage ACLs', module: 'auth' },
23
+ {
24
+ id: 'auth.roles.manage',
25
+ title: 'Manage roles',
26
+ module: 'auth',
27
+ dependsOn: ['auth.roles.list'],
28
+ },
29
+ {
30
+ id: 'auth.acl.manage',
31
+ title: 'Manage ACLs',
32
+ module: 'auth',
33
+ dependsOn: ['auth.users.list', 'auth.roles.list'],
34
+ },
10
35
  { id: 'auth.sidebar.manage', title: 'Manage sidebar presets', module: 'auth' },
11
36
  ]
12
37
 
13
38
  export default features
14
-
@@ -200,9 +200,9 @@ export default function CustomersCompaniesPage() {
200
200
  const [total, setTotal] = React.useState(0)
201
201
  const [totalPages, setTotalPages] = React.useState(1)
202
202
  const [totalIsCapped, setTotalIsCapped] = React.useState(false)
203
- const [search, setSearch] = React.useState('')
204
203
  const pathname = usePathname()
205
204
  const searchParams = useSearchParams()
205
+ const [search, setSearch] = React.useState(() => searchParams?.get('search')?.trim() ?? '')
206
206
  // One-shot URL hydration used as the hook's initial value. The hook is the
207
207
  // single source of truth from this point on — the page MUST NOT keep a
208
208
  // parallel `useState<AdvancedFilterTree>` (see spec "Migration & Backward
@@ -208,9 +208,9 @@ export default function CustomersPeoplePage() {
208
208
  const [total, setTotal] = React.useState(0)
209
209
  const [totalPages, setTotalPages] = React.useState(1)
210
210
  const [totalIsCapped, setTotalIsCapped] = React.useState(false)
211
- const [search, setSearch] = React.useState('')
212
211
  const pathname = usePathname()
213
212
  const searchParams = useSearchParams()
213
+ const [search, setSearch] = React.useState(() => searchParams?.get('search')?.trim() ?? '')
214
214
  // One-shot URL hydration used as the hook's initial value. The hook is the
215
215
  // single source of truth from this point on — the page MUST NOT keep a
216
216
  // parallel `useState<AdvancedFilterTree>` (see spec "Migration & Backward
@@ -131,6 +131,11 @@ function resolveInjectedColumnValue(
131
131
  return current;
132
132
  }
133
133
 
134
+ const SHIPMENTS_PAGE_SIZE = 100;
135
+ // A single order beyond this many shipment pages is pathological; stopping there
136
+ // keeps the shipped state explicitly unresolved rather than silently partial.
137
+ const SHIPMENTS_MAX_PAGES = 50;
138
+
134
139
  type SalesDocumentItemsSectionProps = {
135
140
  documentId: string;
136
141
  kind: "order" | "quote";
@@ -168,6 +173,12 @@ export function SalesDocumentItemsSection({
168
173
  const [shippedTotals, setShippedTotals] = React.useState<Map<string, number>>(
169
174
  new Map(),
170
175
  );
176
+ // Quotes have no shipments at all, so their shipped state is known up front.
177
+ // For orders it stays unknown until every shipment page has been read — an
178
+ // empty map from a pending or failed load must never read as "nothing shipped".
179
+ const [shippedTotalsResolved, setShippedTotalsResolved] = React.useState(
180
+ kind !== "order",
181
+ );
171
182
 
172
183
  const { widgets: allColumnWidgets } = useInjectionDataWidgets(
173
184
  extensionPoints.hosts.orderItemColumns.spotId,
@@ -379,22 +390,31 @@ export function SalesDocumentItemsSection({
379
390
  const loadShippedTotals = React.useCallback(async () => {
380
391
  if (kind !== "order") {
381
392
  setShippedTotals(new Map());
393
+ setShippedTotalsResolved(true);
382
394
  return;
383
395
  }
396
+ setShippedTotals(new Map());
397
+ setShippedTotalsResolved(false);
384
398
  try {
385
- const params = new URLSearchParams({
386
- page: "1",
387
- pageSize: "100",
388
- orderId: documentId,
389
- });
390
- const response = await apiCall<{
391
- items?: Array<Record<string, unknown>>;
392
- }>(`/api/sales/shipments?${params.toString()}`, undefined, {
393
- fallback: { items: [] },
394
- });
395
- if (response.ok && Array.isArray(response.result?.items)) {
396
- const totals = new Map<string, number>();
397
- response.result.items.forEach((shipment) => {
399
+ const totals = new Map<string, number>();
400
+ let page = 1;
401
+ let collected = 0;
402
+ let complete = false;
403
+ while (page <= SHIPMENTS_MAX_PAGES) {
404
+ const params = new URLSearchParams({
405
+ page: String(page),
406
+ pageSize: String(SHIPMENTS_PAGE_SIZE),
407
+ orderId: documentId,
408
+ });
409
+ const response = await apiCall<{
410
+ items?: Array<Record<string, unknown>>;
411
+ total?: unknown;
412
+ }>(`/api/sales/shipments?${params.toString()}`, undefined, {
413
+ fallback: { items: [] },
414
+ });
415
+ if (!response.ok || !Array.isArray(response.result?.items)) return;
416
+ const shipments = response.result.items;
417
+ shipments.forEach((shipment) => {
398
418
  const entries = Array.isArray(shipment.items)
399
419
  ? (shipment.items as Array<Record<string, unknown>>)
400
420
  : [];
@@ -412,13 +432,22 @@ export function SalesDocumentItemsSection({
412
432
  totals.set(lineId, current + quantity);
413
433
  });
414
434
  });
415
- setShippedTotals(totals);
416
- } else {
417
- setShippedTotals(new Map());
435
+ collected += shipments.length;
436
+ const reportedTotal = normalizeNumber(response.result?.total, Number.NaN);
437
+ const hasMore =
438
+ shipments.length >= SHIPMENTS_PAGE_SIZE &&
439
+ (!Number.isFinite(reportedTotal) || collected < reportedTotal);
440
+ if (!hasMore) {
441
+ complete = true;
442
+ break;
443
+ }
444
+ page += 1;
418
445
  }
446
+ if (!complete) return;
447
+ setShippedTotals(totals);
448
+ setShippedTotalsResolved(true);
419
449
  } catch (err) {
420
450
  logger.error('sales.document.shipments.load', { err });
421
- setShippedTotals(new Map());
422
451
  }
423
452
  }, [documentId, kind]);
424
453
 
@@ -439,6 +468,7 @@ export function SalesDocumentItemsSection({
439
468
  if (kind !== "order") {
440
469
  shipmentsLoadedForDocument.current = null;
441
470
  setShippedTotals(new Map());
471
+ setShippedTotalsResolved(true);
442
472
  return;
443
473
  }
444
474
  const key = `${kind}:${documentId}`;
@@ -964,6 +994,7 @@ export function SalesDocumentItemsSection({
964
994
  ? Math.max(0, shippedTotals.get(lineForEdit.id) ?? 0)
965
995
  : 0
966
996
  }
997
+ shippedQuantityResolved={shippedTotalsResolved}
967
998
  onSaved={async () => {
968
999
  await loadItems();
969
1000
  emitSalesDocumentTotalsRefresh({ documentId, kind });
@@ -50,6 +50,7 @@ import { useT } from "@open-mercato/shared/lib/i18n/context";
50
50
  import { useOrganizationScopeDetail } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
51
51
  import { formatMoney, normalizeNumber } from "./lineItemUtils";
52
52
  import type { SalesLineRecord } from "./lineItemTypes";
53
+ import { prepareShippedLineUpdatePayload } from "./lineItemShipmentLock";
53
54
  import {
54
55
  normalizeCustomFieldSubmitValue,
55
56
  extractCustomFieldValues,
@@ -279,6 +280,13 @@ type SalesLineDialogProps = {
279
280
  tenantId: string | null;
280
281
  initialLine?: SalesLineRecord | null;
281
282
  shippedQuantity?: number;
283
+ /**
284
+ * Whether `shippedQuantity` reflects a fully resolved shipment state. Defaults
285
+ * to `true` so callers that genuinely know the value keep working; pass `false`
286
+ * while the host is still loading shipments or after the load failed, and the
287
+ * dialog locks pricing instead of assuming the line is unshipped.
288
+ */
289
+ shippedQuantityResolved?: boolean;
282
290
  onOpenChange: (open: boolean) => void;
283
291
  onSaved?: () => Promise<void> | void;
284
292
  onDraftSaved?: (payload: Record<string, unknown>, lineId: string | null) => Promise<void> | void;
@@ -475,6 +483,7 @@ export function LineItemDialog({
475
483
  tenantId,
476
484
  initialLine,
477
485
  shippedQuantity = 0,
486
+ shippedQuantityResolved = true,
478
487
  onOpenChange,
479
488
  onSaved,
480
489
  onDraftSaved,
@@ -513,6 +522,23 @@ export function LineItemDialog({
513
522
  () => (kind === "order" ? "sales/order-lines" : "sales/quote-lines"),
514
523
  [kind],
515
524
  );
525
+ // A line the caller has not resolved shipment state for is treated as shipped:
526
+ // the server rejects pricing changes on shipped lines, so guessing "unshipped"
527
+ // from a pending or failed shipments load hands the user an edit that cannot
528
+ // be saved. A brand-new line has no shipments by construction.
529
+ const hasExistingLine = Boolean(initialLine);
530
+ const shipmentStateUnknown = kind === "order" && hasExistingLine && !shippedQuantityResolved;
531
+ const isShippedOrderLine =
532
+ kind === "order" &&
533
+ hasExistingLine &&
534
+ (shipmentStateUnknown || shippedQuantity > 0);
535
+ // While the shipments read is unresolved the shipped quantity is unknown, so the
536
+ // only safe floor for a quantity edit is the quantity already stored on the line:
537
+ // whatever turns out to be shipped can never exceed it. Raising stays allowed,
538
+ // exactly as it is once the state resolves.
539
+ const storedQuantity = Number(initialLine?.quantity ?? 0);
540
+ const safeStoredQuantity = Number.isFinite(storedQuantity) ? storedQuantity : 0;
541
+ const quantityFloor = shipmentStateUnknown ? safeStoredQuantity : shippedQuantity;
516
542
  const documentKey = kind === "order" ? "orderId" : "quoteId";
517
543
  const customFieldEntityId =
518
544
  kind === "order" ? E.sales.sales_order_line : E.sales.sales_quote_line;
@@ -1330,12 +1356,17 @@ export function LineItemDialog({
1330
1356
  },
1331
1357
  );
1332
1358
  }
1333
- if (shippedQuantity > 0 && qtyNumber < shippedQuantity) {
1334
- const message = t(
1335
- "sales.documents.items.errorQuantityBelowShipped",
1336
- "You cannot lower the quantity below the {{shipped}} already shipped.",
1337
- { shipped: shippedQuantity },
1338
- );
1359
+ if (quantityFloor > 0 && qtyNumber < quantityFloor) {
1360
+ const message = shipmentStateUnknown
1361
+ ? t(
1362
+ "sales.documents.items.errorQuantityShipmentsUnknown",
1363
+ "The quantity cannot be lowered until this order's shipments have been read. Reopen the order to try again.",
1364
+ )
1365
+ : t(
1366
+ "sales.documents.items.errorQuantityBelowShipped",
1367
+ "You cannot lower the quantity below the {{shipped}} already shipped.",
1368
+ { shipped: shippedQuantity },
1369
+ );
1339
1370
  throw createCrudFormError(message, { quantity: message });
1340
1371
  }
1341
1372
  const resolvedQuantityUnit = (() => {
@@ -1509,9 +1540,20 @@ export function LineItemDialog({
1509
1540
  }
1510
1541
  if (resolvedName) payload.name = resolvedName;
1511
1542
 
1543
+ const submittedPayload = prepareShippedLineUpdatePayload(
1544
+ payload,
1545
+ isShippedOrderLine && initialLine
1546
+ ? {
1547
+ quantity: initialLine.quantity,
1548
+ totalNetAmount: initialLine.totalNet,
1549
+ totalGrossAmount: initialLine.totalGross,
1550
+ }
1551
+ : null,
1552
+ );
1553
+
1512
1554
  try {
1513
1555
  if (onDraftSaved) {
1514
- await onDraftSaved(payload, editingId);
1556
+ await onDraftSaved(submittedPayload, editingId);
1515
1557
  closeDialog();
1516
1558
  return;
1517
1559
  }
@@ -1521,7 +1563,9 @@ export function LineItemDialog({
1521
1563
  () =>
1522
1564
  action(
1523
1565
  resourcePath,
1524
- editingId ? { id: editingId, ...payload } : payload,
1566
+ editingId
1567
+ ? { id: editingId, ...submittedPayload }
1568
+ : submittedPayload,
1525
1569
  {
1526
1570
  errorMessage: t(
1527
1571
  "sales.documents.items.errorSave",
@@ -1552,10 +1596,15 @@ export function LineItemDialog({
1552
1596
  documentKey,
1553
1597
  documentUpdatedAt,
1554
1598
  editingId,
1599
+ initialLine,
1600
+ isShippedOrderLine,
1555
1601
  onDraftSaved,
1556
1602
  priceOptions,
1557
1603
  productOption,
1604
+ quantityFloor,
1558
1605
  resourcePath,
1606
+ shipmentStateUnknown,
1607
+ shippedQuantity,
1559
1608
  t,
1560
1609
  variantOption,
1561
1610
  onSaved,
@@ -1613,6 +1662,7 @@ export function LineItemDialog({
1613
1662
  type="button"
1614
1663
  size="sm"
1615
1664
  variant={mode === "catalog" ? "default" : "ghost"}
1665
+ disabled={isShippedOrderLine}
1616
1666
  onClick={() => switchMode("catalog")}
1617
1667
  >
1618
1668
  {t("sales.documents.items.lineMode.catalog", "Catalog item")}
@@ -1621,6 +1671,7 @@ export function LineItemDialog({
1621
1671
  type="button"
1622
1672
  size="sm"
1623
1673
  variant={mode === "custom" ? "default" : "ghost"}
1674
+ disabled={isShippedOrderLine}
1624
1675
  onClick={() => switchMode("custom")}
1625
1676
  >
1626
1677
  {t("sales.documents.items.lineMode.custom", "Custom line")}
@@ -1801,6 +1852,7 @@ export function LineItemDialog({
1801
1852
  },
1802
1853
  )
1803
1854
  }
1855
+ disabled={isShippedOrderLine}
1804
1856
  />
1805
1857
  ),
1806
1858
  } satisfies CrudField,
@@ -1960,7 +2012,7 @@ export function LineItemDialog({
1960
2012
  },
1961
2013
  )
1962
2014
  }
1963
- disabled={!productId}
2015
+ disabled={isShippedOrderLine || !productId}
1964
2016
  />
1965
2017
  );
1966
2018
  },
@@ -1984,6 +2036,51 @@ export function LineItemDialog({
1984
2036
  typeof values?.variantId === "string"
1985
2037
  ? values.variantId
1986
2038
  : null;
2039
+ const selectedPriceId =
2040
+ typeof value === "string" ? value : null;
2041
+ const selectedPrice = selectedPriceId
2042
+ ? (priceOptions.find(
2043
+ (entry) => entry.id === selectedPriceId,
2044
+ ) ?? null)
2045
+ : null;
2046
+ if (isShippedOrderLine) {
2047
+ const lockedAmount = normalizeNumber(
2048
+ values?.unitPrice,
2049
+ Number.NaN,
2050
+ );
2051
+ const lockedCurrency =
2052
+ selectedPrice?.currencyCode ??
2053
+ (typeof values?.currencyCode === "string"
2054
+ ? values.currencyCode
2055
+ : currencyCode) ??
2056
+ undefined;
2057
+ const lockedModeLabel =
2058
+ values?.priceMode === "net"
2059
+ ? t("sales.documents.items.priceNet", "Net")
2060
+ : t("sales.documents.items.priceGross", "Gross");
2061
+ const lockedAmountLabel = Number.isFinite(lockedAmount)
2062
+ ? `${formatMoney(lockedAmount, lockedCurrency)} — ${lockedModeLabel}`
2063
+ : lockedModeLabel;
2064
+ const lockedPriceDetail =
2065
+ selectedPrice?.priceKindTitle ??
2066
+ selectedPrice?.priceKindCode ??
2067
+ null;
2068
+ return (
2069
+ <div className="space-y-2">
2070
+ <Input
2071
+ readOnly
2072
+ disabled
2073
+ value={lockedAmountLabel}
2074
+ aria-label={t("sales.documents.items.price", "Price")}
2075
+ />
2076
+ {lockedPriceDetail ? (
2077
+ <p className="text-xs text-muted-foreground">
2078
+ {lockedPriceDetail}
2079
+ </p>
2080
+ ) : null}
2081
+ </div>
2082
+ );
2083
+ }
1987
2084
  return (
1988
2085
  <LookupSelect
1989
2086
  key={
@@ -1991,7 +2088,7 @@ export function LineItemDialog({
1991
2088
  ? `${productId}-${variantId ?? "no-variant"}`
1992
2089
  : "price"
1993
2090
  }
1994
- value={typeof value === "string" ? value : null}
2091
+ value={selectedPriceId}
1995
2092
  onChange={(next) => {
1996
2093
  setValue(next ?? null);
1997
2094
  const selected = next
@@ -2133,9 +2230,11 @@ export function LineItemDialog({
2133
2230
  }
2134
2231
  onChange={(event) => setValue(event.target.value)}
2135
2232
  placeholder="0.00"
2233
+ disabled={isShippedOrderLine}
2136
2234
  />
2137
2235
  <Select
2138
2236
  value={mode}
2237
+ disabled={isShippedOrderLine}
2139
2238
  onValueChange={(value) => {
2140
2239
  const nextMode = value === "net" ? "net" : "gross";
2141
2240
  setFormValue?.("priceMode", nextMode);
@@ -2222,7 +2321,7 @@ export function LineItemDialog({
2222
2321
  <Select
2223
2322
  value={resolvedValue || undefined}
2224
2323
  onValueChange={(value) => handleChange({ target: { value } } as React.ChangeEvent<HTMLSelectElement>)}
2225
- disabled={!taxRates.length}
2324
+ disabled={isShippedOrderLine || !taxRates.length}
2226
2325
  >
2227
2326
  <SelectTrigger>
2228
2327
  <SelectValue
@@ -2292,6 +2391,7 @@ export function LineItemDialog({
2292
2391
  <Input
2293
2392
  value={typeof value === "string" ? value : ""}
2294
2393
  onChange={(event) => setValue(event.target.value || null)}
2394
+ disabled={isShippedOrderLine}
2295
2395
  placeholder={t(
2296
2396
  "sales.documents.items.quantityUnitPlaceholder",
2297
2397
  "e.g. pc",
@@ -2349,7 +2449,7 @@ export function LineItemDialog({
2349
2449
  });
2350
2450
  }
2351
2451
  }}
2352
- disabled={!productId}
2452
+ disabled={isShippedOrderLine || !productId}
2353
2453
  >
2354
2454
  <SelectTrigger>
2355
2455
  <SelectValue
@@ -2394,7 +2494,7 @@ export function LineItemDialog({
2394
2494
  typeof values?.quantityUnit === "string"
2395
2495
  ? values.quantityUnit
2396
2496
  : null;
2397
- if (productId) {
2497
+ if (productId && !isShippedOrderLine) {
2398
2498
  const selectedPriceId =
2399
2499
  typeof values?.priceId === "string" ? values.priceId : null;
2400
2500
  const selectedPriceKindId =
@@ -2549,6 +2649,7 @@ export function LineItemDialog({
2549
2649
  resolveTaxSelection,
2550
2650
  selectPriceAfterRefresh,
2551
2651
  hasTaxMetadata,
2652
+ isShippedOrderLine,
2552
2653
  ]);
2553
2654
 
2554
2655
  const groups = React.useMemo<CrudFormGroup[]>(() => {
@@ -2914,6 +3015,21 @@ export function LineItemDialog({
2914
3015
  : t("sales.documents.items.addTitle", "Add line")}
2915
3016
  </DialogTitle>
2916
3017
  </DialogHeader>
3018
+ {isShippedOrderLine ? (
3019
+ <Alert status="information" style="lighter">
3020
+ <AlertDescription>
3021
+ {shipmentStateUnknown
3022
+ ? t(
3023
+ "sales.documents.items.shippedLineLockPending",
3024
+ "Pricing is locked until this order's shipments have been read. Reopen the order to try again — you can still edit the name and raise the quantity.",
3025
+ )
3026
+ : t(
3027
+ "sales.documents.items.shippedLineLocked",
3028
+ "Pricing is locked on this line because it already has shipped items. You can still edit the name and quantity.",
3029
+ )}
3030
+ </AlertDescription>
3031
+ </Alert>
3032
+ ) : null}
2917
3033
  <CrudForm<LineFormState>
2918
3034
  key={formResetKey}
2919
3035
  embedded
@@ -1311,6 +1311,7 @@ export function SalesDocumentForm({ onCreated, isSubmitting = false, initialKind
1311
1311
  id: 'lines',
1312
1312
  label: '',
1313
1313
  type: 'custom',
1314
+ rendersOwnError: true,
1314
1315
  component: ({ value, error, values, setValue }) => values?.documentKind === 'order' ? (
1315
1316
  <SalesOrderDraftLines
1316
1317
  currencyCode={typeof values.currencyCode === 'string' ? values.currencyCode : defaultCurrency}
@@ -161,7 +161,7 @@ export function SalesOrderDraftLines({
161
161
  {t('sales.documents.items.add', 'Add item')}
162
162
  </Button>
163
163
  </div>
164
- {error ? <p className="text-sm text-destructive" role="alert">{error}</p> : null}
164
+ {error ? <p className="text-sm text-status-error-text" role="alert">{error}</p> : null}
165
165
  <DataTable
166
166
  columns={columns}
167
167
  data={lines}