@open-mercato/core 0.7.1-develop.7153.1.7145d295e6 → 0.7.1-develop.7170.1.d95074d7ba
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/AGENTS.md +2 -2
- package/dist/modules/auth/commands/users.js +14 -1
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/auth/data/entities.js +2 -2
- package/dist/modules/auth/data/entities.js.map +2 -2
- package/dist/modules/auth/lib/backendChrome.js +26 -3
- package/dist/modules/auth/lib/backendChrome.js.map +2 -2
- package/dist/modules/auth/lib/profile-sections.js +1 -1
- package/dist/modules/auth/lib/profile-sections.js.map +2 -2
- package/dist/modules/auth/migrations/Migration20260907120000_auth.js +19 -0
- package/dist/modules/auth/migrations/Migration20260907120000_auth.js.map +7 -0
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.meta.js +2 -2
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.meta.js.map +1 -1
- package/dist/modules/customer_accounts/api/admin/demo-accounts.js +118 -0
- package/dist/modules/customer_accounts/api/admin/demo-accounts.js.map +7 -0
- package/dist/modules/customer_accounts/api/invitations/accept.js +23 -6
- package/dist/modules/customer_accounts/api/invitations/accept.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/CustomerAccountsSettingsPageClient.js +10 -13
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/CustomerAccountsSettingsPageClient.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/useDemoPortalAccounts.js +28 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/useDemoPortalAccounts.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js +6 -1
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js.map +2 -2
- package/dist/modules/customer_accounts/data/entities.js +4 -1
- package/dist/modules/customer_accounts/data/entities.js.map +2 -2
- package/dist/modules/customer_accounts/lib/exampleAccounts.js +9 -0
- package/dist/modules/customer_accounts/lib/exampleAccounts.js.map +7 -0
- package/dist/modules/customer_accounts/migrations/Migration20260827160000_customer_accounts.js +16 -0
- package/dist/modules/customer_accounts/migrations/Migration20260827160000_customer_accounts.js.map +7 -0
- package/dist/modules/customer_accounts/services/customerInvitationService.js +53 -6
- package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
- package/dist/modules/customer_accounts/setup.js +2 -6
- package/dist/modules/customer_accounts/setup.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +55 -40
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
- package/dist/modules/customers/commands/people.js +16 -1
- package/dist/modules/customers/commands/people.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +18 -36
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-schedule-service.js +25 -23
- package/dist/modules/data_sync/lib/sync-schedule-service.js.map +2 -2
- package/dist/modules/entities/api/records.js +18 -15
- package/dist/modules/entities/api/records.js.map +3 -3
- package/dist/modules/entities/lib/scoped-field-defs.js +52 -0
- package/dist/modules/entities/lib/scoped-field-defs.js.map +7 -0
- package/dist/modules/entities/lib/validation.js +4 -35
- package/dist/modules/entities/lib/validation.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +32 -54
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +12 -29
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
- package/dist/modules/notifications/backend/profile/notification-preferences/page.meta.js +2 -2
- package/dist/modules/notifications/backend/profile/notification-preferences/page.meta.js.map +1 -1
- package/dist/modules/progress/lib/progressServiceImpl.js +14 -3
- package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +9 -9
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +50 -10
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/PaymentsSection.js +8 -3
- package/dist/modules/sales/components/documents/PaymentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/ReturnEditDialog.js +2 -7
- package/dist/modules/sales/components/documents/ReturnEditDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/ReturnsSection.js +2 -7
- package/dist/modules/sales/components/documents/ReturnsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentsSection.js +3 -8
- package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/commands/users.ts +14 -1
- package/src/modules/auth/data/entities.ts +2 -2
- package/src/modules/auth/lib/backendChrome.tsx +46 -2
- package/src/modules/auth/lib/profile-sections.tsx +14 -3
- package/src/modules/auth/migrations/.snapshot-open-mercato.json +4 -2
- package/src/modules/auth/migrations/Migration20260907120000_auth.ts +21 -0
- package/src/modules/communication_channels/backend/profile/communication-channels/page.meta.ts +2 -2
- package/src/modules/customer_accounts/AGENTS.md +3 -1
- package/src/modules/customer_accounts/api/admin/demo-accounts.ts +149 -0
- package/src/modules/customer_accounts/api/invitations/accept.ts +23 -6
- package/src/modules/customer_accounts/backend/customer_accounts/settings/CustomerAccountsSettingsPageClient.tsx +43 -41
- package/src/modules/customer_accounts/backend/customer_accounts/useDemoPortalAccounts.ts +42 -0
- package/src/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.tsx +10 -3
- package/src/modules/customer_accounts/data/entities.ts +5 -1
- package/src/modules/customer_accounts/i18n/de.json +4 -2
- package/src/modules/customer_accounts/i18n/en.json +4 -2
- package/src/modules/customer_accounts/i18n/es.json +4 -2
- package/src/modules/customer_accounts/i18n/ko.json +4 -2
- package/src/modules/customer_accounts/i18n/pl.json +4 -2
- package/src/modules/customer_accounts/lib/exampleAccounts.ts +18 -0
- package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +5 -7
- package/src/modules/customer_accounts/migrations/Migration20260827160000_customer_accounts.ts +23 -0
- package/src/modules/customer_accounts/services/customerInvitationService.ts +89 -5
- package/src/modules/customer_accounts/setup.ts +2 -6
- package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +74 -41
- package/src/modules/customers/commands/people.ts +15 -0
- package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +19 -44
- package/src/modules/data_sync/lib/sync-schedule-service.ts +32 -26
- package/src/modules/entities/api/records.ts +24 -15
- package/src/modules/entities/lib/scoped-field-defs.ts +76 -0
- package/src/modules/entities/lib/validation.ts +5 -45
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +33 -62
- package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +13 -37
- package/src/modules/notifications/backend/profile/notification-preferences/page.meta.ts +2 -2
- package/src/modules/progress/AGENTS.md +1 -1
- package/src/modules/progress/lib/progressServiceImpl.ts +26 -3
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +19 -25
- package/src/modules/sales/components/documents/LineItemDialog.tsx +53 -9
- package/src/modules/sales/components/documents/PaymentsSection.tsx +8 -8
- package/src/modules/sales/components/documents/ReturnEditDialog.tsx +2 -8
- package/src/modules/sales/components/documents/ReturnsSection.tsx +8 -7
- package/src/modules/sales/components/documents/ShipmentsSection.tsx +9 -8
- package/src/modules/sales/i18n/de.json +2 -0
- package/src/modules/sales/i18n/en.json +2 -0
- package/src/modules/sales/i18n/es.json +2 -0
- package/src/modules/sales/i18n/ko.json +2 -0
- package/src/modules/sales/i18n/pl.json +2 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/sales/components/documents/ShipmentsSection.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Pencil, Plus, Trash2, Truck } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { ErrorMessage, LoadingMessage, TabEmptyState } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { deleteCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { useOrganizationScopeDetail } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport {\n emitSalesDocumentTotalsRefresh,\n subscribeSalesDocumentTotalsRefresh,\n} from '@open-mercato/core/modules/sales/lib/frontend/documentTotalsEvents'\nimport type { SectionAction } from '@open-mercato/core/modules/customers/components/detail/types'\nimport { generateTempId } from '@open-mercato/core/modules/customers/lib/detailHelpers'\nimport { formatAddressString, type AddressValue } from '@open-mercato/core/modules/customers/utils/addressFormat'\nimport { ShipmentDialog } from './ShipmentDialog'\nimport { handleSectionMutationError, readRowUpdatedAt } from './optimisticLock'\nimport { extractCustomFieldValues } from './customFieldHelpers'\nimport type { OrderLine, ShipmentRow, ShipmentItem } from './shipmentTypes'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sales')\n\nconst ADDRESS_SNAPSHOT_KEY = 'shipmentAddressSnapshot'\nconst ADDRESS_FORMAT: 'line_first' = 'line_first'\n\ntype SalesShipmentsSectionProps = {\n orderId: string\n currencyCode?: string | null\n shippingAddressSnapshot?: Record<string, unknown> | null\n organizationId?: string | null\n tenantId?: string | null\n documentUpdatedAt?: string | null\n onActionChange?: (action: SectionAction | null) => void\n onAddComment?: (body: string) => Promise<void>\n}\n\nexport function formatDisplayDate(value: string | null | undefined, locale?: string): string | null {\n if (!value) return null\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return null\n return new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(date)\n}\n\nconst formatShipmentAddress = (metadata?: Record<string, unknown> | null): string | null => {\n if (!metadata || typeof metadata !== 'object') return null\n const snapshot = (metadata as any)[ADDRESS_SNAPSHOT_KEY]\n if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) return null\n const read = (key: string): string | null => {\n const value = (snapshot as any)[key]\n return typeof value === 'string' && value.trim().length ? value.trim() : null\n }\n const addressValue: AddressValue = {\n addressLine1: read('addressLine1') ?? read('address_line1'),\n addressLine2: read('addressLine2') ?? read('address_line2'),\n buildingNumber: read('buildingNumber') ?? read('building_number'),\n flatNumber: read('flatNumber') ?? read('flat_number'),\n city: read('city'),\n region: read('region'),\n postalCode: read('postalCode') ?? read('postal_code'),\n country: read('country'),\n companyName: read('companyName') ?? read('company_name'),\n }\n const summary = formatAddressString(addressValue, ADDRESS_FORMAT)\n return summary && summary.trim().length ? summary : null\n}\n\nconst resolveLineThumbnail = (item: Record<string, unknown>): string | null => {\n const pickThumbnail = (...candidates: Array<unknown>): string | null => {\n const found = candidates.find(\n (candidate): candidate is string =>\n typeof candidate === 'string' && candidate.trim().length > 0,\n )\n return found ?? null\n }\n\n const metadata =\n typeof (item as any).metadata === 'object' && (item as any).metadata !== null\n ? ((item as any).metadata as Record<string, unknown>)\n : null\n const snapshot =\n typeof (item as any).catalog_snapshot === 'object' && (item as any).catalog_snapshot !== null\n ? ((item as any).catalog_snapshot as Record<string, unknown>)\n : null\n const productSnapshot =\n snapshot && typeof (snapshot as any).product === 'object' ? ((snapshot as any).product as Record<string, unknown>) : null\n const variantSnapshot =\n snapshot && typeof (snapshot as any).variant === 'object' ? ((snapshot as any).variant as Record<string, unknown>) : null\n\n const productThumb = pickThumbnail(\n metadata ? (metadata as any).productThumbnail : null,\n productSnapshot ? (productSnapshot as any).thumbnailUrl : null,\n productSnapshot ? (productSnapshot as any).thumbnail_url : null,\n )\n const variantThumb = pickThumbnail(\n metadata ? (metadata as any).variantThumbnail : null,\n variantSnapshot ? (variantSnapshot as any).thumbnailUrl : null,\n variantSnapshot ? (variantSnapshot as any).thumbnail_url : null,\n )\n\n const snapshotThumb = pickThumbnail(\n snapshot ? (snapshot as any).thumbnailUrl : null,\n snapshot ? (snapshot as any).thumbnail_url : null,\n )\n\n return variantThumb ?? productThumb ?? snapshotThumb ?? null\n}\n\nexport function SalesShipmentsSection({\n orderId,\n currencyCode,\n shippingAddressSnapshot,\n organizationId: organizationIdProp,\n tenantId: tenantIdProp,\n documentUpdatedAt,\n onActionChange,\n onAddComment,\n}: SalesShipmentsSectionProps) {\n const t = useT()\n const locale = useLocale()\n const { organizationId, tenantId } = useOrganizationScopeDetail()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const resolvedOrganizationId = organizationIdProp ?? organizationId ?? null\n const resolvedTenantId = tenantIdProp ?? tenantId ?? null\n const addShipmentLabel = React.useMemo(\n () => t('sales.documents.shipments.add', 'Add shipment'),\n [t]\n )\n const [shipments, setShipments] = React.useState<ShipmentRow[]>([])\n const [lines, setLines] = React.useState<OrderLine[]>([])\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const [dialogState, setDialogState] = React.useState<{ mode: 'create' | 'edit'; shipment: ShipmentRow | null } | null>(null)\n\n const lineMap = React.useMemo(() => new Map(lines.map((line) => [line.id, line])), [lines])\n\n const shippedTotals = React.useMemo(() => {\n const totals = new Map<string, number>()\n shipments.forEach((shipment) => {\n shipment.items.forEach((item) => {\n const current = totals.get(item.orderLineId) ?? 0\n totals.set(item.orderLineId, current + (Number.isFinite(item.quantity) ? item.quantity : 0))\n })\n })\n return totals\n }, [shipments])\n\n const computeAvailable = React.useCallback(\n (lineId: string, excludeShipmentId?: string | null) => {\n const line = lineMap.get(lineId)\n if (!line) return 0\n const shipped = shippedTotals.get(lineId) ?? 0\n const editingShipment = excludeShipmentId ? shipments.find((entry) => entry.id === excludeShipmentId) : null\n const editingQty =\n editingShipment?.items\n .filter((item) => item.orderLineId === lineId)\n .reduce((acc, item) => acc + (Number.isFinite(item.quantity) ? item.quantity : 0), 0) ?? 0\n const remaining = line.quantity - (shipped - editingQty)\n return remaining < 0 ? 0 : remaining\n },\n [lineMap, shipments, shippedTotals]\n )\n\n const loadLines = React.useCallback(async () => {\n const params = new URLSearchParams({ page: '1', pageSize: '100', orderId })\n const response = await apiCall<{ items?: Array<Record<string, unknown>> }>(\n `/api/sales/order-lines?${params.toString()}`,\n undefined,\n { fallback: { items: [] } }\n )\n const items = Array.isArray(response.result?.items) ? response.result?.items ?? [] : []\n const mapped: OrderLine[] = items\n .map((item) => {\n const id = typeof item.id === 'string' ? item.id : null\n if (!id) return null\n const name =\n typeof item.name === 'string'\n ? item.name\n : typeof (item as any).catalog_snapshot === 'object' &&\n (item as any).catalog_snapshot &&\n typeof (item as any).catalog_snapshot.name === 'string'\n ? (item as any).catalog_snapshot.name\n : null\n const lineNumber =\n typeof (item as any).line_number === 'number'\n ? (item as any).line_number\n : typeof (item as any).lineNumber === 'number'\n ? (item as any).lineNumber\n : null\n const quantity =\n typeof item.quantity === 'number'\n ? item.quantity\n : typeof (item as any).quantity === 'string'\n ? Number((item as any).quantity)\n : 0\n return {\n id,\n title: name ?? id,\n lineNumber,\n quantity: Number.isFinite(quantity) ? quantity : 0,\n thumbnail: resolveLineThumbnail(item as Record<string, unknown>),\n }\n })\n .filter((entry): entry is OrderLine => Boolean(entry?.id))\n setLines(mapped)\n }, [orderId])\n\n const loadShipments = React.useCallback(async () => {\n setLoading(true)\n setError(null)\n try {\n const params = new URLSearchParams({ page: '1', pageSize: '100', orderId })\n const response = await apiCall<{ items?: Array<Record<string, unknown>> }>(\n `/api/sales/shipments?${params.toString()}`,\n undefined,\n { fallback: { items: [] } }\n )\n const items = Array.isArray(response.result?.items) ? response.result?.items ?? [] : []\n const mapped: ShipmentRow[] = items\n .map((item) => {\n const id = typeof item.id === 'string' ? item.id : null\n if (!id) return null\n const snapshotItemsRaw = Array.isArray((item as any).items_snapshot)\n ? ((item as any).items_snapshot as Array<Record<string, unknown>>)\n : []\n const responseItems = Array.isArray((item as any).items)\n ? ((item as any).items as Array<Record<string, unknown>>)\n : []\n const itemsRaw = responseItems.length ? responseItems : snapshotItemsRaw\n const shipmentItems: ShipmentItem[] = itemsRaw\n .map((entry) => {\n const lineId =\n typeof entry.orderLineId === 'string'\n ? entry.orderLineId\n : typeof (entry as any).order_line_id === 'string'\n ? (entry as any).order_line_id\n : null\n if (!lineId) return null\n const quantity =\n typeof entry.quantity === 'number'\n ? entry.quantity\n : typeof (entry as any).quantity === 'string'\n ? Number((entry as any).quantity)\n : 0\n return {\n id: typeof entry.id === 'string' ? entry.id : generateTempId(),\n orderLineId: lineId,\n orderLineName:\n typeof entry.orderLineName === 'string'\n ? entry.orderLineName\n : typeof (entry as any).order_line_name === 'string'\n ? (entry as any).order_line_name\n : null,\n orderLineNumber:\n typeof entry.orderLineNumber === 'number'\n ? entry.orderLineNumber\n : typeof (entry as any).order_line_number === 'number'\n ? (entry as any).order_line_number\n : null,\n quantity: Number.isFinite(quantity) ? quantity : 0,\n metadata: (entry.metadata as Record<string, unknown> | undefined | null) ?? null,\n }\n })\n .filter((entry): entry is ShipmentItem => Boolean(entry))\n const tracking =\n Array.isArray((item as any).tracking_numbers) && (item as any).tracking_numbers.length\n ? ((item as any).tracking_numbers as string[])\n : []\n const customValues = extractCustomFieldValues(item as Record<string, unknown>)\n return {\n id,\n updatedAt: readRowUpdatedAt(item),\n shipmentNumber:\n typeof (item as any).shipment_number === 'string'\n ? (item as any).shipment_number\n : typeof (item as any).shipmentNumber === 'string'\n ? (item as any).shipmentNumber\n : null,\n shippingMethodId:\n typeof (item as any).shipping_method_id === 'string'\n ? (item as any).shipping_method_id\n : typeof (item as any).shippingMethodId === 'string'\n ? (item as any).shippingMethodId\n : null,\n shippingMethodCode:\n typeof (item as any).shipping_method_code === 'string'\n ? (item as any).shipping_method_code\n : typeof (item as any).shippingMethodCode === 'string'\n ? (item as any).shippingMethodCode\n : null,\n shippingMethodName:\n typeof (item as any).shipping_method_name === 'string'\n ? (item as any).shipping_method_name\n : typeof (item as any).shippingMethodName === 'string'\n ? (item as any).shippingMethodName\n : null,\n status:\n typeof item.status === 'string'\n ? item.status\n : typeof (item as any).status === 'string'\n ? (item as any).status\n : null,\n statusLabel:\n typeof (item as any).status_label === 'string'\n ? (item as any).status_label\n : typeof (item as any).statusLabel === 'string'\n ? (item as any).statusLabel\n : null,\n statusEntryId:\n typeof (item as any).status_entry_id === 'string'\n ? (item as any).status_entry_id\n : typeof (item as any).statusEntryId === 'string'\n ? (item as any).statusEntryId\n : null,\n carrierName:\n typeof (item as any).carrier_name === 'string'\n ? (item as any).carrier_name\n : typeof (item as any).carrierName === 'string'\n ? (item as any).carrierName\n : null,\n trackingNumbers: tracking,\n shippedAt:\n typeof (item as any).shipped_at === 'string'\n ? (item as any).shipped_at\n : typeof (item as any).shippedAt === 'string'\n ? (item as any).shippedAt\n : null,\n deliveredAt:\n typeof (item as any).delivered_at === 'string'\n ? (item as any).delivered_at\n : typeof (item as any).deliveredAt === 'string'\n ? (item as any).deliveredAt\n : null,\n notes:\n typeof (item as any).notes === 'string'\n ? (item as any).notes\n : typeof (item as any).notesText === 'string'\n ? (item as any).notesText\n : null,\n metadata: (item as Record<string, unknown> | null | undefined)?.metadata ?? null,\n customValues: Object.keys(customValues).length ? customValues : null,\n items: shipmentItems,\n createdAt:\n typeof (item as any).created_at === 'string'\n ? (item as any).created_at\n : typeof (item as any).createdAt === 'string'\n ? (item as any).createdAt\n : null,\n }\n })\n .filter((entry): entry is ShipmentRow => Boolean(entry))\n setShipments(mapped)\n } catch (err) {\n logger.error('sales.shipments.load', { err })\n setError(t('sales.documents.shipments.errorLoad', 'Failed to load shipments.'))\n } finally {\n setLoading(false)\n }\n }, [orderId, t])\n\n React.useEffect(() => {\n void loadLines()\n void loadShipments()\n }, [loadLines, loadShipments])\n\n React.useEffect(\n () =>\n subscribeSalesDocumentTotalsRefresh((detail) => {\n if (detail.documentId !== orderId) return\n if (detail.kind && detail.kind !== 'order') return\n void loadLines()\n void loadShipments()\n }),\n [loadLines, loadShipments, orderId],\n )\n\n const handleOpenCreate = React.useCallback(() => {\n setDialogState({ mode: 'create', shipment: null })\n }, [])\n\n React.useEffect(() => {\n if (!onActionChange) return\n onActionChange({\n label: addShipmentLabel,\n onClick: handleOpenCreate,\n disabled: false,\n })\n return () => onActionChange(null)\n }, [addShipmentLabel, handleOpenCreate, onActionChange])\n\n const handleEdit = React.useCallback((shipment: ShipmentRow) => {\n setDialogState({ mode: 'edit', shipment })\n }, [])\n\n const handleDelete = React.useCallback(\n async (shipment: ShipmentRow) => {\n const confirmed = await confirm({\n title: t('sales.documents.shipments.confirmDelete', 'Delete this shipment?'),\n variant: 'default',\n })\n if (!confirmed) return\n try {\n const result = await withScopedApiRequestHeaders(\n // The server guards the PARENT order's aggregate version (Gap B), so\n // send the order's `updated_at`, not the shipment row's.\n buildOptimisticLockHeader(documentUpdatedAt ?? undefined),\n () =>\n deleteCrud('sales/shipments', {\n body: {\n id: shipment.id,\n orderId,\n organizationId: resolvedOrganizationId,\n tenantId: resolvedTenantId,\n },\n errorMessage: t('sales.documents.shipments.errorDelete', 'Failed to delete shipment.'),\n })\n )\n if (result.ok) {\n await loadShipments()\n emitSalesDocumentTotalsRefresh({ documentId: orderId, kind: 'order' })\n }\n } catch (err) {\n if (handleSectionMutationError(err, t, () => void loadShipments())) {\n return\n }\n logger.error('sales.shipments.delete', { err })\n flash(t('sales.documents.shipments.errorDelete', 'Failed to delete shipment.'), 'error')\n }\n },\n [confirm, documentUpdatedAt, loadShipments, orderId, resolvedOrganizationId, resolvedTenantId, t]\n )\n\n const renderItemList = (items: ShipmentItem[]) => (\n <ul className=\"space-y-1 text-sm text-muted-foreground\">\n {items.map((item) => {\n const label = item.orderLineNumber ? `#${item.orderLineNumber}` : ''\n return (\n <li key={item.id} className=\"flex items-center justify-between gap-2\">\n <span className=\"truncate\">\n {label ? `${label} \u00B7 ` : null}\n {item.orderLineName ?? item.orderLineId}\n </span>\n <Badge variant=\"secondary\">{item.quantity}</Badge>\n </li>\n )\n })}\n </ul>\n )\n\n if (loading) {\n return (\n <LoadingMessage\n label={t('sales.documents.shipments.loading', 'Loading shipments\u2026')}\n className=\"border-0 bg-transparent p-0 py-8 justify-center\"\n />\n )\n }\n\n if (error) {\n return <ErrorMessage label={error} />\n }\n\n const empty = !shipments.length\n\n return (\n <div className=\"space-y-4\">\n {empty ? (\n <TabEmptyState\n title={t('sales.documents.shipments.empty.title', 'No shipments yet.')}\n description={t(\n 'sales.documents.shipments.empty.description',\n 'Add shipments for this document to let the user track the order.'\n )}\n action={{\n label: addShipmentLabel,\n onClick: handleOpenCreate,\n icon: <Plus className=\"h-4 w-4\" aria-hidden />,\n disabled: loading,\n }}\n />\n ) : (\n <div className=\"grid gap-3 md:grid-cols-2\">\n {shipments.map((shipment) => {\n const shippedAt = formatDisplayDate(shipment.shippedAt, locale)\n const deliveredAt = formatDisplayDate(shipment.deliveredAt, locale)\n const addressSummary = formatShipmentAddress(shipment.metadata)\n const statusLabel =\n shipment.statusLabel ??\n shipment.status ??\n t('sales.documents.shipments.statusMissing', 'Status pending')\n return (\n <div key={shipment.id} className=\"rounded-lg border bg-card p-4 shadow-sm\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex items-center gap-2\">\n <div className=\"flex h-9 w-9 items-center justify-center rounded-full bg-primary/10 text-primary\">\n <Truck className=\"h-5 w-5\" />\n </div>\n <div>\n <p className=\"text-sm font-semibold\">\n {shipment.shipmentNumber\n ? t('sales.documents.shipments.numberLabel', 'Shipment {{number}}', {\n number: shipment.shipmentNumber,\n })\n : t('sales.documents.shipments.fallbackNumber', 'Shipment {{id}}', {\n id: shipment.id.slice(0, 6),\n })}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {statusLabel}\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-1\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8\"\n onClick={() => handleEdit(shipment)}\n >\n <Pencil className=\"h-4 w-4\" />\n </Button>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8 text-destructive\"\n onClick={() => void handleDelete(shipment)}\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n <div className=\"mt-3 space-y-2 text-sm\">\n <div className=\"flex flex-wrap items-center gap-2 text-muted-foreground\">\n {shipment.carrierName ? (\n <Badge variant=\"outline\">{shipment.carrierName}</Badge>\n ) : null}\n {shipment.trackingNumbers.length ? (\n <span className=\"truncate\">\n {t('sales.documents.shipments.tracking', 'Tracking')}: {shipment.trackingNumbers.join(', ')}\n </span>\n ) : null}\n </div>\n {shippedAt ? (\n <p className=\"text-muted-foreground\">\n {t('sales.documents.shipments.shippedOn', 'Shipped on {{date}}', { date: shippedAt })}\n </p>\n ) : null}\n {deliveredAt ? (\n <p className=\"text-muted-foreground\">\n {t('sales.documents.shipments.deliveredOn', 'Delivered on {{date}}', { date: deliveredAt })}\n </p>\n ) : null}\n {addressSummary ? (\n <p className=\"text-xs text-muted-foreground\">{addressSummary}</p>\n ) : null}\n {shipment.notes ? (\n <p className=\"rounded-md bg-muted px-3 py-2 text-muted-foreground\">\n {shipment.notes}\n </p>\n ) : null}\n {renderItemList(shipment.items)}\n </div>\n </div>\n )\n })}\n </div>\n )}\n\n <ShipmentDialog\n open={dialogState !== null}\n mode={dialogState?.mode ?? 'create'}\n shipment={dialogState?.shipment ?? null}\n lines={lines}\n orderId={orderId}\n currencyCode={currencyCode}\n organizationId={resolvedOrganizationId}\n tenantId={resolvedTenantId}\n documentUpdatedAt={documentUpdatedAt ?? null}\n computeAvailable={computeAvailable}\n shippingAddressSnapshot={shippingAddressSnapshot}\n onClose={() => setDialogState(null)}\n onSaved={async () => {\n setDialogState(null)\n await loadShipments()\n emitSalesDocumentTotalsRefresh({ documentId: orderId, kind: 'order' })\n }}\n onAddComment={onAddComment}\n />\n {ConfirmDialogElement}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { formatDisplayDate, toUtcDateInputValue } from '@open-mercato/ui/primitives/date-format'\nimport { Pencil, Plus, Trash2, Truck } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { ErrorMessage, LoadingMessage, TabEmptyState } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { deleteCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { useOrganizationScopeDetail } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport {\n emitSalesDocumentTotalsRefresh,\n subscribeSalesDocumentTotalsRefresh,\n} from '@open-mercato/core/modules/sales/lib/frontend/documentTotalsEvents'\nimport type { SectionAction } from '@open-mercato/core/modules/customers/components/detail/types'\nimport { generateTempId } from '@open-mercato/core/modules/customers/lib/detailHelpers'\nimport { formatAddressString, type AddressValue } from '@open-mercato/core/modules/customers/utils/addressFormat'\nimport { ShipmentDialog } from './ShipmentDialog'\nimport { handleSectionMutationError, readRowUpdatedAt } from './optimisticLock'\nimport { extractCustomFieldValues } from './customFieldHelpers'\nimport type { OrderLine, ShipmentRow, ShipmentItem } from './shipmentTypes'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sales')\n\nconst ADDRESS_SNAPSHOT_KEY = 'shipmentAddressSnapshot'\nconst ADDRESS_FORMAT: 'line_first' = 'line_first'\n\ntype SalesShipmentsSectionProps = {\n orderId: string\n currencyCode?: string | null\n shippingAddressSnapshot?: Record<string, unknown> | null\n organizationId?: string | null\n tenantId?: string | null\n documentUpdatedAt?: string | null\n onActionChange?: (action: SectionAction | null) => void\n onAddComment?: (body: string) => Promise<void>\n}\n\n// One date formatter for this page rather than three with incompatible semantics.\nexport { formatDisplayDate }\n\nconst formatShipmentAddress = (metadata?: Record<string, unknown> | null): string | null => {\n if (!metadata || typeof metadata !== 'object') return null\n const snapshot = (metadata as any)[ADDRESS_SNAPSHOT_KEY]\n if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) return null\n const read = (key: string): string | null => {\n const value = (snapshot as any)[key]\n return typeof value === 'string' && value.trim().length ? value.trim() : null\n }\n const addressValue: AddressValue = {\n addressLine1: read('addressLine1') ?? read('address_line1'),\n addressLine2: read('addressLine2') ?? read('address_line2'),\n buildingNumber: read('buildingNumber') ?? read('building_number'),\n flatNumber: read('flatNumber') ?? read('flat_number'),\n city: read('city'),\n region: read('region'),\n postalCode: read('postalCode') ?? read('postal_code'),\n country: read('country'),\n companyName: read('companyName') ?? read('company_name'),\n }\n const summary = formatAddressString(addressValue, ADDRESS_FORMAT)\n return summary && summary.trim().length ? summary : null\n}\n\nconst resolveLineThumbnail = (item: Record<string, unknown>): string | null => {\n const pickThumbnail = (...candidates: Array<unknown>): string | null => {\n const found = candidates.find(\n (candidate): candidate is string =>\n typeof candidate === 'string' && candidate.trim().length > 0,\n )\n return found ?? null\n }\n\n const metadata =\n typeof (item as any).metadata === 'object' && (item as any).metadata !== null\n ? ((item as any).metadata as Record<string, unknown>)\n : null\n const snapshot =\n typeof (item as any).catalog_snapshot === 'object' && (item as any).catalog_snapshot !== null\n ? ((item as any).catalog_snapshot as Record<string, unknown>)\n : null\n const productSnapshot =\n snapshot && typeof (snapshot as any).product === 'object' ? ((snapshot as any).product as Record<string, unknown>) : null\n const variantSnapshot =\n snapshot && typeof (snapshot as any).variant === 'object' ? ((snapshot as any).variant as Record<string, unknown>) : null\n\n const productThumb = pickThumbnail(\n metadata ? (metadata as any).productThumbnail : null,\n productSnapshot ? (productSnapshot as any).thumbnailUrl : null,\n productSnapshot ? (productSnapshot as any).thumbnail_url : null,\n )\n const variantThumb = pickThumbnail(\n metadata ? (metadata as any).variantThumbnail : null,\n variantSnapshot ? (variantSnapshot as any).thumbnailUrl : null,\n variantSnapshot ? (variantSnapshot as any).thumbnail_url : null,\n )\n\n const snapshotThumb = pickThumbnail(\n snapshot ? (snapshot as any).thumbnailUrl : null,\n snapshot ? (snapshot as any).thumbnail_url : null,\n )\n\n return variantThumb ?? productThumb ?? snapshotThumb ?? null\n}\n\nexport function SalesShipmentsSection({\n orderId,\n currencyCode,\n shippingAddressSnapshot,\n organizationId: organizationIdProp,\n tenantId: tenantIdProp,\n documentUpdatedAt,\n onActionChange,\n onAddComment,\n}: SalesShipmentsSectionProps) {\n const t = useT()\n const locale = useLocale()\n const { organizationId, tenantId } = useOrganizationScopeDetail()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const resolvedOrganizationId = organizationIdProp ?? organizationId ?? null\n const resolvedTenantId = tenantIdProp ?? tenantId ?? null\n const addShipmentLabel = React.useMemo(\n () => t('sales.documents.shipments.add', 'Add shipment'),\n [t]\n )\n const [shipments, setShipments] = React.useState<ShipmentRow[]>([])\n const [lines, setLines] = React.useState<OrderLine[]>([])\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const [dialogState, setDialogState] = React.useState<{ mode: 'create' | 'edit'; shipment: ShipmentRow | null } | null>(null)\n\n const lineMap = React.useMemo(() => new Map(lines.map((line) => [line.id, line])), [lines])\n\n const shippedTotals = React.useMemo(() => {\n const totals = new Map<string, number>()\n shipments.forEach((shipment) => {\n shipment.items.forEach((item) => {\n const current = totals.get(item.orderLineId) ?? 0\n totals.set(item.orderLineId, current + (Number.isFinite(item.quantity) ? item.quantity : 0))\n })\n })\n return totals\n }, [shipments])\n\n const computeAvailable = React.useCallback(\n (lineId: string, excludeShipmentId?: string | null) => {\n const line = lineMap.get(lineId)\n if (!line) return 0\n const shipped = shippedTotals.get(lineId) ?? 0\n const editingShipment = excludeShipmentId ? shipments.find((entry) => entry.id === excludeShipmentId) : null\n const editingQty =\n editingShipment?.items\n .filter((item) => item.orderLineId === lineId)\n .reduce((acc, item) => acc + (Number.isFinite(item.quantity) ? item.quantity : 0), 0) ?? 0\n const remaining = line.quantity - (shipped - editingQty)\n return remaining < 0 ? 0 : remaining\n },\n [lineMap, shipments, shippedTotals]\n )\n\n const loadLines = React.useCallback(async () => {\n const params = new URLSearchParams({ page: '1', pageSize: '100', orderId })\n const response = await apiCall<{ items?: Array<Record<string, unknown>> }>(\n `/api/sales/order-lines?${params.toString()}`,\n undefined,\n { fallback: { items: [] } }\n )\n const items = Array.isArray(response.result?.items) ? response.result?.items ?? [] : []\n const mapped: OrderLine[] = items\n .map((item) => {\n const id = typeof item.id === 'string' ? item.id : null\n if (!id) return null\n const name =\n typeof item.name === 'string'\n ? item.name\n : typeof (item as any).catalog_snapshot === 'object' &&\n (item as any).catalog_snapshot &&\n typeof (item as any).catalog_snapshot.name === 'string'\n ? (item as any).catalog_snapshot.name\n : null\n const lineNumber =\n typeof (item as any).line_number === 'number'\n ? (item as any).line_number\n : typeof (item as any).lineNumber === 'number'\n ? (item as any).lineNumber\n : null\n const quantity =\n typeof item.quantity === 'number'\n ? item.quantity\n : typeof (item as any).quantity === 'string'\n ? Number((item as any).quantity)\n : 0\n return {\n id,\n title: name ?? id,\n lineNumber,\n quantity: Number.isFinite(quantity) ? quantity : 0,\n thumbnail: resolveLineThumbnail(item as Record<string, unknown>),\n }\n })\n .filter((entry): entry is OrderLine => Boolean(entry?.id))\n setLines(mapped)\n }, [orderId])\n\n const loadShipments = React.useCallback(async () => {\n setLoading(true)\n setError(null)\n try {\n const params = new URLSearchParams({ page: '1', pageSize: '100', orderId })\n const response = await apiCall<{ items?: Array<Record<string, unknown>> }>(\n `/api/sales/shipments?${params.toString()}`,\n undefined,\n { fallback: { items: [] } }\n )\n const items = Array.isArray(response.result?.items) ? response.result?.items ?? [] : []\n const mapped: ShipmentRow[] = items\n .map((item) => {\n const id = typeof item.id === 'string' ? item.id : null\n if (!id) return null\n const snapshotItemsRaw = Array.isArray((item as any).items_snapshot)\n ? ((item as any).items_snapshot as Array<Record<string, unknown>>)\n : []\n const responseItems = Array.isArray((item as any).items)\n ? ((item as any).items as Array<Record<string, unknown>>)\n : []\n const itemsRaw = responseItems.length ? responseItems : snapshotItemsRaw\n const shipmentItems: ShipmentItem[] = itemsRaw\n .map((entry) => {\n const lineId =\n typeof entry.orderLineId === 'string'\n ? entry.orderLineId\n : typeof (entry as any).order_line_id === 'string'\n ? (entry as any).order_line_id\n : null\n if (!lineId) return null\n const quantity =\n typeof entry.quantity === 'number'\n ? entry.quantity\n : typeof (entry as any).quantity === 'string'\n ? Number((entry as any).quantity)\n : 0\n return {\n id: typeof entry.id === 'string' ? entry.id : generateTempId(),\n orderLineId: lineId,\n orderLineName:\n typeof entry.orderLineName === 'string'\n ? entry.orderLineName\n : typeof (entry as any).order_line_name === 'string'\n ? (entry as any).order_line_name\n : null,\n orderLineNumber:\n typeof entry.orderLineNumber === 'number'\n ? entry.orderLineNumber\n : typeof (entry as any).order_line_number === 'number'\n ? (entry as any).order_line_number\n : null,\n quantity: Number.isFinite(quantity) ? quantity : 0,\n metadata: (entry.metadata as Record<string, unknown> | undefined | null) ?? null,\n }\n })\n .filter((entry): entry is ShipmentItem => Boolean(entry))\n const tracking =\n Array.isArray((item as any).tracking_numbers) && (item as any).tracking_numbers.length\n ? ((item as any).tracking_numbers as string[])\n : []\n const customValues = extractCustomFieldValues(item as Record<string, unknown>)\n return {\n id,\n updatedAt: readRowUpdatedAt(item),\n shipmentNumber:\n typeof (item as any).shipment_number === 'string'\n ? (item as any).shipment_number\n : typeof (item as any).shipmentNumber === 'string'\n ? (item as any).shipmentNumber\n : null,\n shippingMethodId:\n typeof (item as any).shipping_method_id === 'string'\n ? (item as any).shipping_method_id\n : typeof (item as any).shippingMethodId === 'string'\n ? (item as any).shippingMethodId\n : null,\n shippingMethodCode:\n typeof (item as any).shipping_method_code === 'string'\n ? (item as any).shipping_method_code\n : typeof (item as any).shippingMethodCode === 'string'\n ? (item as any).shippingMethodCode\n : null,\n shippingMethodName:\n typeof (item as any).shipping_method_name === 'string'\n ? (item as any).shipping_method_name\n : typeof (item as any).shippingMethodName === 'string'\n ? (item as any).shippingMethodName\n : null,\n status:\n typeof item.status === 'string'\n ? item.status\n : typeof (item as any).status === 'string'\n ? (item as any).status\n : null,\n statusLabel:\n typeof (item as any).status_label === 'string'\n ? (item as any).status_label\n : typeof (item as any).statusLabel === 'string'\n ? (item as any).statusLabel\n : null,\n statusEntryId:\n typeof (item as any).status_entry_id === 'string'\n ? (item as any).status_entry_id\n : typeof (item as any).statusEntryId === 'string'\n ? (item as any).statusEntryId\n : null,\n carrierName:\n typeof (item as any).carrier_name === 'string'\n ? (item as any).carrier_name\n : typeof (item as any).carrierName === 'string'\n ? (item as any).carrierName\n : null,\n trackingNumbers: tracking,\n shippedAt:\n typeof (item as any).shipped_at === 'string'\n ? (item as any).shipped_at\n : typeof (item as any).shippedAt === 'string'\n ? (item as any).shippedAt\n : null,\n deliveredAt:\n typeof (item as any).delivered_at === 'string'\n ? (item as any).delivered_at\n : typeof (item as any).deliveredAt === 'string'\n ? (item as any).deliveredAt\n : null,\n notes:\n typeof (item as any).notes === 'string'\n ? (item as any).notes\n : typeof (item as any).notesText === 'string'\n ? (item as any).notesText\n : null,\n metadata: (item as Record<string, unknown> | null | undefined)?.metadata ?? null,\n customValues: Object.keys(customValues).length ? customValues : null,\n items: shipmentItems,\n createdAt:\n typeof (item as any).created_at === 'string'\n ? (item as any).created_at\n : typeof (item as any).createdAt === 'string'\n ? (item as any).createdAt\n : null,\n }\n })\n .filter((entry): entry is ShipmentRow => Boolean(entry))\n setShipments(mapped)\n } catch (err) {\n logger.error('sales.shipments.load', { err })\n setError(t('sales.documents.shipments.errorLoad', 'Failed to load shipments.'))\n } finally {\n setLoading(false)\n }\n }, [orderId, t])\n\n React.useEffect(() => {\n void loadLines()\n void loadShipments()\n }, [loadLines, loadShipments])\n\n React.useEffect(\n () =>\n subscribeSalesDocumentTotalsRefresh((detail) => {\n if (detail.documentId !== orderId) return\n if (detail.kind && detail.kind !== 'order') return\n void loadLines()\n void loadShipments()\n }),\n [loadLines, loadShipments, orderId],\n )\n\n const handleOpenCreate = React.useCallback(() => {\n setDialogState({ mode: 'create', shipment: null })\n }, [])\n\n React.useEffect(() => {\n if (!onActionChange) return\n onActionChange({\n label: addShipmentLabel,\n onClick: handleOpenCreate,\n disabled: false,\n })\n return () => onActionChange(null)\n }, [addShipmentLabel, handleOpenCreate, onActionChange])\n\n const handleEdit = React.useCallback((shipment: ShipmentRow) => {\n setDialogState({ mode: 'edit', shipment })\n }, [])\n\n const handleDelete = React.useCallback(\n async (shipment: ShipmentRow) => {\n const confirmed = await confirm({\n title: t('sales.documents.shipments.confirmDelete', 'Delete this shipment?'),\n variant: 'default',\n })\n if (!confirmed) return\n try {\n const result = await withScopedApiRequestHeaders(\n // The server guards the PARENT order's aggregate version (Gap B), so\n // send the order's `updated_at`, not the shipment row's.\n buildOptimisticLockHeader(documentUpdatedAt ?? undefined),\n () =>\n deleteCrud('sales/shipments', {\n body: {\n id: shipment.id,\n orderId,\n organizationId: resolvedOrganizationId,\n tenantId: resolvedTenantId,\n },\n errorMessage: t('sales.documents.shipments.errorDelete', 'Failed to delete shipment.'),\n })\n )\n if (result.ok) {\n await loadShipments()\n emitSalesDocumentTotalsRefresh({ documentId: orderId, kind: 'order' })\n }\n } catch (err) {\n if (handleSectionMutationError(err, t, () => void loadShipments())) {\n return\n }\n logger.error('sales.shipments.delete', { err })\n flash(t('sales.documents.shipments.errorDelete', 'Failed to delete shipment.'), 'error')\n }\n },\n [confirm, documentUpdatedAt, loadShipments, orderId, resolvedOrganizationId, resolvedTenantId, t]\n )\n\n const renderItemList = (items: ShipmentItem[]) => (\n <ul className=\"space-y-1 text-sm text-muted-foreground\">\n {items.map((item) => {\n const label = item.orderLineNumber ? `#${item.orderLineNumber}` : ''\n return (\n <li key={item.id} className=\"flex items-center justify-between gap-2\">\n <span className=\"truncate\">\n {label ? `${label} \u00B7 ` : null}\n {item.orderLineName ?? item.orderLineId}\n </span>\n <Badge variant=\"secondary\">{item.quantity}</Badge>\n </li>\n )\n })}\n </ul>\n )\n\n if (loading) {\n return (\n <LoadingMessage\n label={t('sales.documents.shipments.loading', 'Loading shipments\u2026')}\n className=\"border-0 bg-transparent p-0 py-8 justify-center\"\n />\n )\n }\n\n if (error) {\n return <ErrorMessage label={error} />\n }\n\n const empty = !shipments.length\n\n return (\n <div className=\"space-y-4\">\n {empty ? (\n <TabEmptyState\n title={t('sales.documents.shipments.empty.title', 'No shipments yet.')}\n description={t(\n 'sales.documents.shipments.empty.description',\n 'Add shipments for this document to let the user track the order.'\n )}\n action={{\n label: addShipmentLabel,\n onClick: handleOpenCreate,\n icon: <Plus className=\"h-4 w-4\" aria-hidden />,\n disabled: loading,\n }}\n />\n ) : (\n <div className=\"grid gap-3 md:grid-cols-2\">\n {shipments.map((shipment) => {\n // `shippedAt` / `deliveredAt` are written from date inputs (`ShipmentDialog`), coerced\n // by `z.coerce.date()` and stored as UTC midnight, and the dialog seeds itself back from\n // `.slice(0, 10)` \u2014 the UTC day. Reading them locally names the previous day west of UTC\n // and disagrees with that dialog on the same row.\n const shippedAt = formatDisplayDate(toUtcDateInputValue(shipment.shippedAt), locale)\n const deliveredAt = formatDisplayDate(toUtcDateInputValue(shipment.deliveredAt), locale)\n const addressSummary = formatShipmentAddress(shipment.metadata)\n const statusLabel =\n shipment.statusLabel ??\n shipment.status ??\n t('sales.documents.shipments.statusMissing', 'Status pending')\n return (\n <div key={shipment.id} className=\"rounded-lg border bg-card p-4 shadow-sm\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex items-center gap-2\">\n <div className=\"flex h-9 w-9 items-center justify-center rounded-full bg-primary/10 text-primary\">\n <Truck className=\"h-5 w-5\" />\n </div>\n <div>\n <p className=\"text-sm font-semibold\">\n {shipment.shipmentNumber\n ? t('sales.documents.shipments.numberLabel', 'Shipment {{number}}', {\n number: shipment.shipmentNumber,\n })\n : t('sales.documents.shipments.fallbackNumber', 'Shipment {{id}}', {\n id: shipment.id.slice(0, 6),\n })}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {statusLabel}\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-1\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8\"\n onClick={() => handleEdit(shipment)}\n >\n <Pencil className=\"h-4 w-4\" />\n </Button>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8 text-destructive\"\n onClick={() => void handleDelete(shipment)}\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n <div className=\"mt-3 space-y-2 text-sm\">\n <div className=\"flex flex-wrap items-center gap-2 text-muted-foreground\">\n {shipment.carrierName ? (\n <Badge variant=\"outline\">{shipment.carrierName}</Badge>\n ) : null}\n {shipment.trackingNumbers.length ? (\n <span className=\"truncate\">\n {t('sales.documents.shipments.tracking', 'Tracking')}: {shipment.trackingNumbers.join(', ')}\n </span>\n ) : null}\n </div>\n {shippedAt ? (\n <p className=\"text-muted-foreground\">\n {t('sales.documents.shipments.shippedOn', 'Shipped on {{date}}', { date: shippedAt })}\n </p>\n ) : null}\n {deliveredAt ? (\n <p className=\"text-muted-foreground\">\n {t('sales.documents.shipments.deliveredOn', 'Delivered on {{date}}', { date: deliveredAt })}\n </p>\n ) : null}\n {addressSummary ? (\n <p className=\"text-xs text-muted-foreground\">{addressSummary}</p>\n ) : null}\n {shipment.notes ? (\n <p className=\"rounded-md bg-muted px-3 py-2 text-muted-foreground\">\n {shipment.notes}\n </p>\n ) : null}\n {renderItemList(shipment.items)}\n </div>\n </div>\n )\n })}\n </div>\n )}\n\n <ShipmentDialog\n open={dialogState !== null}\n mode={dialogState?.mode ?? 'create'}\n shipment={dialogState?.shipment ?? null}\n lines={lines}\n orderId={orderId}\n currencyCode={currencyCode}\n organizationId={resolvedOrganizationId}\n tenantId={resolvedTenantId}\n documentUpdatedAt={documentUpdatedAt ?? null}\n computeAvailable={computeAvailable}\n shippingAddressSnapshot={shippingAddressSnapshot}\n onClose={() => setDialogState(null)}\n onSaved={async () => {\n setDialogState(null)\n await loadShipments()\n emitSalesDocumentTotalsRefresh({ documentId: orderId, kind: 'order' })\n }}\n onAddComment={onAddComment}\n />\n {ConfirmDialogElement}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAybY,SAIA,KAJA;AAvbZ,YAAY,WAAW;AACvB,SAAS,mBAAmB,2BAA2B;AACvD,SAAS,QAAQ,MAAM,QAAQ,aAAa;AAC5C,SAAS,cAAc;AACvB,SAAS,aAAa;AACtB,SAAS,cAAc,gBAAgB,qBAAqB;AAC5D,SAAS,aAAa;AACtB,SAAS,SAAS,mCAAmC;AACrD,SAAS,iCAAiC;AAC1C,SAAS,kBAAkB;AAC3B,SAAS,kCAAkC;AAC3C,SAAS,MAAM,iBAAiB;AAChC,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEP,SAAS,sBAAsB;AAC/B,SAAS,2BAA8C;AACvD,SAAS,sBAAsB;AAC/B,SAAS,4BAA4B,wBAAwB;AAC7D,SAAS,gCAAgC;AAEzC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,OAAO;AAEnC,MAAM,uBAAuB;AAC7B,MAAM,iBAA+B;AAgBrC,MAAM,wBAAwB,CAAC,aAA6D;AAC1F,MAAI,CAAC,YAAY,OAAO,aAAa,SAAU,QAAO;AACtD,QAAM,WAAY,SAAiB,oBAAoB;AACvD,MAAI,CAAC,YAAY,OAAO,aAAa,YAAY,MAAM,QAAQ,QAAQ,EAAG,QAAO;AACjF,QAAM,OAAO,CAAC,QAA+B;AAC3C,UAAM,QAAS,SAAiB,GAAG;AACnC,WAAO,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,MAAM,KAAK,IAAI;AAAA,EAC3E;AACA,QAAM,eAA6B;AAAA,IACjC,cAAc,KAAK,cAAc,KAAK,KAAK,eAAe;AAAA,IAC1D,cAAc,KAAK,cAAc,KAAK,KAAK,eAAe;AAAA,IAC1D,gBAAgB,KAAK,gBAAgB,KAAK,KAAK,iBAAiB;AAAA,IAChE,YAAY,KAAK,YAAY,KAAK,KAAK,aAAa;AAAA,IACpD,MAAM,KAAK,MAAM;AAAA,IACjB,QAAQ,KAAK,QAAQ;AAAA,IACrB,YAAY,KAAK,YAAY,KAAK,KAAK,aAAa;AAAA,IACpD,SAAS,KAAK,SAAS;AAAA,IACvB,aAAa,KAAK,aAAa,KAAK,KAAK,cAAc;AAAA,EACzD;AACA,QAAM,UAAU,oBAAoB,cAAc,cAAc;AAChE,SAAO,WAAW,QAAQ,KAAK,EAAE,SAAS,UAAU;AACtD;AAEA,MAAM,uBAAuB,CAAC,SAAiD;AAC7E,QAAM,gBAAgB,IAAI,eAA8C;AACtE,UAAM,QAAQ,WAAW;AAAA,MACvB,CAAC,cACC,OAAO,cAAc,YAAY,UAAU,KAAK,EAAE,SAAS;AAAA,IAC/D;AACA,WAAO,SAAS;AAAA,EAClB;AAEA,QAAM,WACJ,OAAQ,KAAa,aAAa,YAAa,KAAa,aAAa,OACnE,KAAa,WACf;AACN,QAAM,WACJ,OAAQ,KAAa,qBAAqB,YAAa,KAAa,qBAAqB,OACnF,KAAa,mBACf;AACN,QAAM,kBACJ,YAAY,OAAQ,SAAiB,YAAY,WAAa,SAAiB,UAAsC;AACvH,QAAM,kBACJ,YAAY,OAAQ,SAAiB,YAAY,WAAa,SAAiB,UAAsC;AAEvH,QAAM,eAAe;AAAA,IACnB,WAAY,SAAiB,mBAAmB;AAAA,IAChD,kBAAmB,gBAAwB,eAAe;AAAA,IAC1D,kBAAmB,gBAAwB,gBAAgB;AAAA,EAC7D;AACA,QAAM,eAAe;AAAA,IACnB,WAAY,SAAiB,mBAAmB;AAAA,IAChD,kBAAmB,gBAAwB,eAAe;AAAA,IAC1D,kBAAmB,gBAAwB,gBAAgB;AAAA,EAC7D;AAEA,QAAM,gBAAgB;AAAA,IACpB,WAAY,SAAiB,eAAe;AAAA,IAC5C,WAAY,SAAiB,gBAAgB;AAAA,EAC/C;AAEA,SAAO,gBAAgB,gBAAgB,iBAAiB;AAC1D;AAEO,SAAS,sBAAsB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AACF,GAA+B;AAC7B,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,UAAU;AACzB,QAAM,EAAE,gBAAgB,SAAS,IAAI,2BAA2B;AAChE,QAAM,EAAE,SAAS,qBAAqB,IAAI,iBAAiB;AAC3D,QAAM,yBAAyB,sBAAsB,kBAAkB;AACvE,QAAM,mBAAmB,gBAAgB,YAAY;AACrD,QAAM,mBAAmB,MAAM;AAAA,IAC7B,MAAM,EAAE,iCAAiC,cAAc;AAAA,IACvD,CAAC,CAAC;AAAA,EACJ;AACA,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAwB,CAAC,CAAC;AAClE,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAsB,CAAC,CAAC;AACxD,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;AAClD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAC5D,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAA2E,IAAI;AAE3H,QAAM,UAAU,MAAM,QAAQ,MAAM,IAAI,IAAI,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC;AAE1F,QAAM,gBAAgB,MAAM,QAAQ,MAAM;AACxC,UAAM,SAAS,oBAAI,IAAoB;AACvC,cAAU,QAAQ,CAAC,aAAa;AAC9B,eAAS,MAAM,QAAQ,CAAC,SAAS;AAC/B,cAAM,UAAU,OAAO,IAAI,KAAK,WAAW,KAAK;AAChD,eAAO,IAAI,KAAK,aAAa,WAAW,OAAO,SAAS,KAAK,QAAQ,IAAI,KAAK,WAAW,EAAE;AAAA,MAC7F,CAAC;AAAA,IACH,CAAC;AACD,WAAO;AAAA,EACT,GAAG,CAAC,SAAS,CAAC;AAEd,QAAM,mBAAmB,MAAM;AAAA,IAC7B,CAAC,QAAgB,sBAAsC;AACrD,YAAM,OAAO,QAAQ,IAAI,MAAM;AAC/B,UAAI,CAAC,KAAM,QAAO;AAClB,YAAM,UAAU,cAAc,IAAI,MAAM,KAAK;AAC7C,YAAM,kBAAkB,oBAAoB,UAAU,KAAK,CAAC,UAAU,MAAM,OAAO,iBAAiB,IAAI;AACxG,YAAM,aACJ,iBAAiB,MACd,OAAO,CAAC,SAAS,KAAK,gBAAgB,MAAM,EAC5C,OAAO,CAAC,KAAK,SAAS,OAAO,OAAO,SAAS,KAAK,QAAQ,IAAI,KAAK,WAAW,IAAI,CAAC,KAAK;AAC7F,YAAM,YAAY,KAAK,YAAY,UAAU;AAC7C,aAAO,YAAY,IAAI,IAAI;AAAA,IAC7B;AAAA,IACA,CAAC,SAAS,WAAW,aAAa;AAAA,EACpC;AAEA,QAAM,YAAY,MAAM,YAAY,YAAY;AAC9C,UAAM,SAAS,IAAI,gBAAgB,EAAE,MAAM,KAAK,UAAU,OAAO,QAAQ,CAAC;AAC1E,UAAM,WAAW,MAAM;AAAA,MACrB,0BAA0B,OAAO,SAAS,CAAC;AAAA,MAC3C;AAAA,MACA,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE;AAAA,IAC5B;AACA,UAAM,QAAQ,MAAM,QAAQ,SAAS,QAAQ,KAAK,IAAI,SAAS,QAAQ,SAAS,CAAC,IAAI,CAAC;AACtF,UAAM,SAAsB,MACzB,IAAI,CAAC,SAAS;AACb,YAAM,KAAK,OAAO,KAAK,OAAO,WAAW,KAAK,KAAK;AACnD,UAAI,CAAC,GAAI,QAAO;AAChB,YAAM,OACJ,OAAO,KAAK,SAAS,WACjB,KAAK,OACL,OAAQ,KAAa,qBAAqB,YACvC,KAAa,oBACd,OAAQ,KAAa,iBAAiB,SAAS,WAC9C,KAAa,iBAAiB,OAC/B;AACR,YAAM,aACJ,OAAQ,KAAa,gBAAgB,WAChC,KAAa,cACd,OAAQ,KAAa,eAAe,WACjC,KAAa,aACd;AACR,YAAM,WACJ,OAAO,KAAK,aAAa,WACrB,KAAK,WACL,OAAQ,KAAa,aAAa,WAChC,OAAQ,KAAa,QAAQ,IAC7B;AACR,aAAO;AAAA,QACL;AAAA,QACA,OAAO,QAAQ;AAAA,QACf;AAAA,QACA,UAAU,OAAO,SAAS,QAAQ,IAAI,WAAW;AAAA,QACjD,WAAW,qBAAqB,IAA+B;AAAA,MACjE;AAAA,IACF,CAAC,EACA,OAAO,CAAC,UAA8B,QAAQ,OAAO,EAAE,CAAC;AAC3D,aAAS,MAAM;AAAA,EACjB,GAAG,CAAC,OAAO,CAAC;AAEZ,QAAM,gBAAgB,MAAM,YAAY,YAAY;AAClD,eAAW,IAAI;AACf,aAAS,IAAI;AACb,QAAI;AACF,YAAM,SAAS,IAAI,gBAAgB,EAAE,MAAM,KAAK,UAAU,OAAO,QAAQ,CAAC;AAC1E,YAAM,WAAW,MAAM;AAAA,QACrB,wBAAwB,OAAO,SAAS,CAAC;AAAA,QACzC;AAAA,QACA,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE;AAAA,MAC5B;AACA,YAAM,QAAQ,MAAM,QAAQ,SAAS,QAAQ,KAAK,IAAI,SAAS,QAAQ,SAAS,CAAC,IAAI,CAAC;AACtF,YAAM,SAAwB,MAC3B,IAAI,CAAC,SAAS;AACb,cAAM,KAAK,OAAO,KAAK,OAAO,WAAW,KAAK,KAAK;AACnD,YAAI,CAAC,GAAI,QAAO;AAChB,cAAM,mBAAmB,MAAM,QAAS,KAAa,cAAc,IAC7D,KAAa,iBACf,CAAC;AACL,cAAM,gBAAgB,MAAM,QAAS,KAAa,KAAK,IACjD,KAAa,QACf,CAAC;AACL,cAAM,WAAW,cAAc,SAAS,gBAAgB;AACxD,cAAM,gBAAgC,SACnC,IAAI,CAAC,UAAU;AACd,gBAAM,SACJ,OAAO,MAAM,gBAAgB,WACzB,MAAM,cACN,OAAQ,MAAc,kBAAkB,WACrC,MAAc,gBACf;AACR,cAAI,CAAC,OAAQ,QAAO;AACpB,gBAAM,WACJ,OAAO,MAAM,aAAa,WACtB,MAAM,WACN,OAAQ,MAAc,aAAa,WACjC,OAAQ,MAAc,QAAQ,IAC9B;AACR,iBAAO;AAAA,YACL,IAAI,OAAO,MAAM,OAAO,WAAW,MAAM,KAAK,eAAe;AAAA,YAC7D,aAAa;AAAA,YACb,eACE,OAAO,MAAM,kBAAkB,WAC3B,MAAM,gBACN,OAAQ,MAAc,oBAAoB,WACvC,MAAc,kBACf;AAAA,YACR,iBACE,OAAO,MAAM,oBAAoB,WAC7B,MAAM,kBACN,OAAQ,MAAc,sBAAsB,WACzC,MAAc,oBACf;AAAA,YACR,UAAU,OAAO,SAAS,QAAQ,IAAI,WAAW;AAAA,YACjD,UAAW,MAAM,YAA2D;AAAA,UAC9E;AAAA,QACF,CAAC,EACA,OAAO,CAAC,UAAiC,QAAQ,KAAK,CAAC;AAC1D,cAAM,WACJ,MAAM,QAAS,KAAa,gBAAgB,KAAM,KAAa,iBAAiB,SAC1E,KAAa,mBACf,CAAC;AACP,cAAM,eAAe,yBAAyB,IAA+B;AAC7E,eAAO;AAAA,UACL;AAAA,UACA,WAAW,iBAAiB,IAAI;AAAA,UAChC,gBACE,OAAQ,KAAa,oBAAoB,WACpC,KAAa,kBACd,OAAQ,KAAa,mBAAmB,WACrC,KAAa,iBACd;AAAA,UACR,kBACE,OAAQ,KAAa,uBAAuB,WACvC,KAAa,qBACd,OAAQ,KAAa,qBAAqB,WACvC,KAAa,mBACd;AAAA,UACR,oBACE,OAAQ,KAAa,yBAAyB,WACzC,KAAa,uBACd,OAAQ,KAAa,uBAAuB,WACzC,KAAa,qBACd;AAAA,UACR,oBACE,OAAQ,KAAa,yBAAyB,WACzC,KAAa,uBACd,OAAQ,KAAa,uBAAuB,WACzC,KAAa,qBACd;AAAA,UACR,QACE,OAAO,KAAK,WAAW,WACnB,KAAK,SACL,OAAQ,KAAa,WAAW,WAC7B,KAAa,SACd;AAAA,UACR,aACE,OAAQ,KAAa,iBAAiB,WACjC,KAAa,eACd,OAAQ,KAAa,gBAAgB,WAClC,KAAa,cACd;AAAA,UACR,eACE,OAAQ,KAAa,oBAAoB,WACpC,KAAa,kBACd,OAAQ,KAAa,kBAAkB,WACpC,KAAa,gBACd;AAAA,UACR,aACE,OAAQ,KAAa,iBAAiB,WACjC,KAAa,eACd,OAAQ,KAAa,gBAAgB,WAClC,KAAa,cACd;AAAA,UACR,iBAAiB;AAAA,UACjB,WACE,OAAQ,KAAa,eAAe,WAC/B,KAAa,aACd,OAAQ,KAAa,cAAc,WAChC,KAAa,YACd;AAAA,UACR,aACE,OAAQ,KAAa,iBAAiB,WACjC,KAAa,eACd,OAAQ,KAAa,gBAAgB,WAClC,KAAa,cACd;AAAA,UACR,OACE,OAAQ,KAAa,UAAU,WAC1B,KAAa,QACd,OAAQ,KAAa,cAAc,WAChC,KAAa,YACd;AAAA,UACR,UAAW,MAAqD,YAAY;AAAA,UAC5E,cAAc,OAAO,KAAK,YAAY,EAAE,SAAS,eAAe;AAAA,UAChE,OAAO;AAAA,UACP,WACE,OAAQ,KAAa,eAAe,WAC/B,KAAa,aACd,OAAQ,KAAa,cAAc,WAChC,KAAa,YACd;AAAA,QACV;AAAA,MACF,CAAC,EACA,OAAO,CAAC,UAAgC,QAAQ,KAAK,CAAC;AACzD,mBAAa,MAAM;AAAA,IACrB,SAAS,KAAK;AACZ,aAAO,MAAM,wBAAwB,EAAE,IAAI,CAAC;AAC5C,eAAS,EAAE,uCAAuC,2BAA2B,CAAC;AAAA,IAChF,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,SAAS,CAAC,CAAC;AAEf,QAAM,UAAU,MAAM;AACpB,SAAK,UAAU;AACf,SAAK,cAAc;AAAA,EACrB,GAAG,CAAC,WAAW,aAAa,CAAC;AAE7B,QAAM;AAAA,IACJ,MACE,oCAAoC,CAAC,WAAW;AAC9C,UAAI,OAAO,eAAe,QAAS;AACnC,UAAI,OAAO,QAAQ,OAAO,SAAS,QAAS;AAC5C,WAAK,UAAU;AACf,WAAK,cAAc;AAAA,IACrB,CAAC;AAAA,IACH,CAAC,WAAW,eAAe,OAAO;AAAA,EACpC;AAEA,QAAM,mBAAmB,MAAM,YAAY,MAAM;AAC/C,mBAAe,EAAE,MAAM,UAAU,UAAU,KAAK,CAAC;AAAA,EACnD,GAAG,CAAC,CAAC;AAEL,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,eAAgB;AACrB,mBAAe;AAAA,MACb,OAAO;AAAA,MACP,SAAS;AAAA,MACT,UAAU;AAAA,IACZ,CAAC;AACD,WAAO,MAAM,eAAe,IAAI;AAAA,EAClC,GAAG,CAAC,kBAAkB,kBAAkB,cAAc,CAAC;AAEvD,QAAM,aAAa,MAAM,YAAY,CAAC,aAA0B;AAC9D,mBAAe,EAAE,MAAM,QAAQ,SAAS,CAAC;AAAA,EAC3C,GAAG,CAAC,CAAC;AAEL,QAAM,eAAe,MAAM;AAAA,IACzB,OAAO,aAA0B;AAC/B,YAAM,YAAY,MAAM,QAAQ;AAAA,QAC9B,OAAO,EAAE,2CAA2C,uBAAuB;AAAA,QAC3E,SAAS;AAAA,MACX,CAAC;AACD,UAAI,CAAC,UAAW;AAChB,UAAI;AACF,cAAM,SAAS,MAAM;AAAA;AAAA;AAAA,UAGnB,0BAA0B,qBAAqB,MAAS;AAAA,UACxD,MACE,WAAW,mBAAmB;AAAA,YAC5B,MAAM;AAAA,cACJ,IAAI,SAAS;AAAA,cACb;AAAA,cACA,gBAAgB;AAAA,cAChB,UAAU;AAAA,YACZ;AAAA,YACA,cAAc,EAAE,yCAAyC,4BAA4B;AAAA,UACvF,CAAC;AAAA,QACL;AACA,YAAI,OAAO,IAAI;AACb,gBAAM,cAAc;AACpB,yCAA+B,EAAE,YAAY,SAAS,MAAM,QAAQ,CAAC;AAAA,QACvE;AAAA,MACF,SAAS,KAAK;AACZ,YAAI,2BAA2B,KAAK,GAAG,MAAM,KAAK,cAAc,CAAC,GAAG;AAClE;AAAA,QACF;AACA,eAAO,MAAM,0BAA0B,EAAE,IAAI,CAAC;AAC9C,cAAM,EAAE,yCAAyC,4BAA4B,GAAG,OAAO;AAAA,MACzF;AAAA,IACF;AAAA,IACA,CAAC,SAAS,mBAAmB,eAAe,SAAS,wBAAwB,kBAAkB,CAAC;AAAA,EAClG;AAEA,QAAM,iBAAiB,CAAC,UACtB,oBAAC,QAAG,WAAU,2CACX,gBAAM,IAAI,CAAC,SAAS;AACnB,UAAM,QAAQ,KAAK,kBAAkB,IAAI,KAAK,eAAe,KAAK;AAClE,WACE,qBAAC,QAAiB,WAAU,2CAC1B;AAAA,2BAAC,UAAK,WAAU,YACb;AAAA,gBAAQ,GAAG,KAAK,WAAQ;AAAA,QACxB,KAAK,iBAAiB,KAAK;AAAA,SAC9B;AAAA,MACA,oBAAC,SAAM,SAAQ,aAAa,eAAK,UAAS;AAAA,SALnC,KAAK,EAMd;AAAA,EAEJ,CAAC,GACH;AAGF,MAAI,SAAS;AACX,WACE;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,EAAE,qCAAqC,yBAAoB;AAAA,QAClE,WAAU;AAAA;AAAA,IACZ;AAAA,EAEJ;AAEA,MAAI,OAAO;AACT,WAAO,oBAAC,gBAAa,OAAO,OAAO;AAAA,EACrC;AAEA,QAAM,QAAQ,CAAC,UAAU;AAEzB,SACE,qBAAC,SAAI,WAAU,aACZ;AAAA,YACC;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,EAAE,yCAAyC,mBAAmB;AAAA,QACrE,aAAa;AAAA,UACX;AAAA,UACA;AAAA,QACF;AAAA,QACA,QAAQ;AAAA,UACN,OAAO;AAAA,UACP,SAAS;AAAA,UACT,MAAM,oBAAC,QAAK,WAAU,WAAU,eAAW,MAAC;AAAA,UAC5C,UAAU;AAAA,QACZ;AAAA;AAAA,IACF,IAEA,oBAAC,SAAI,WAAU,6BACZ,oBAAU,IAAI,CAAC,aAAa;AAK3B,YAAM,YAAY,kBAAkB,oBAAoB,SAAS,SAAS,GAAG,MAAM;AACnF,YAAM,cAAc,kBAAkB,oBAAoB,SAAS,WAAW,GAAG,MAAM;AACvF,YAAM,iBAAiB,sBAAsB,SAAS,QAAQ;AAC9D,YAAM,cACJ,SAAS,eACT,SAAS,UACT,EAAE,2CAA2C,gBAAgB;AAC/D,aACE,qBAAC,SAAsB,WAAU,2CAC/B;AAAA,6BAAC,SAAI,WAAU,0CACb;AAAA,+BAAC,SAAI,WAAU,2BACb;AAAA,gCAAC,SAAI,WAAU,oFACb,8BAAC,SAAM,WAAU,WAAU,GAC7B;AAAA,YACA,qBAAC,SACC;AAAA,kCAAC,OAAE,WAAU,yBACV,mBAAS,iBACN,EAAE,yCAAyC,uBAAuB;AAAA,gBAChE,QAAQ,SAAS;AAAA,cACnB,CAAC,IACD,EAAE,4CAA4C,mBAAmB;AAAA,gBAC/D,IAAI,SAAS,GAAG,MAAM,GAAG,CAAC;AAAA,cAC5B,CAAC,GACP;AAAA,cACA,oBAAC,OAAE,WAAU,iCACV,uBACH;AAAA,eACF;AAAA,aACF;AAAA,UACA,qBAAC,SAAI,WAAU,2BACb;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,SAAQ;AAAA,gBACR,MAAK;AAAA,gBACL,WAAU;AAAA,gBACV,SAAS,MAAM,WAAW,QAAQ;AAAA,gBAElC,8BAAC,UAAO,WAAU,WAAU;AAAA;AAAA,YAC9B;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,SAAQ;AAAA,gBACR,MAAK;AAAA,gBACL,WAAU;AAAA,gBACV,SAAS,MAAM,KAAK,aAAa,QAAQ;AAAA,gBAEzC,8BAAC,UAAO,WAAU,WAAU;AAAA;AAAA,YAC9B;AAAA,aACF;AAAA,WACF;AAAA,QACA,qBAAC,SAAI,WAAU,0BACb;AAAA,+BAAC,SAAI,WAAU,2DACZ;AAAA,qBAAS,cACR,oBAAC,SAAM,SAAQ,WAAW,mBAAS,aAAY,IAC7C;AAAA,YACH,SAAS,gBAAgB,SACxB,qBAAC,UAAK,WAAU,YACb;AAAA,gBAAE,sCAAsC,UAAU;AAAA,cAAE;AAAA,cAAG,SAAS,gBAAgB,KAAK,IAAI;AAAA,eAC5F,IACE;AAAA,aACN;AAAA,UACC,YACC,oBAAC,OAAE,WAAU,yBACV,YAAE,uCAAuC,uBAAuB,EAAE,MAAM,UAAU,CAAC,GACtF,IACE;AAAA,UACH,cACC,oBAAC,OAAE,WAAU,yBACV,YAAE,yCAAyC,yBAAyB,EAAE,MAAM,YAAY,CAAC,GAC5F,IACE;AAAA,UACH,iBACC,oBAAC,OAAE,WAAU,iCAAiC,0BAAe,IAC3D;AAAA,UACH,SAAS,QACR,oBAAC,OAAE,WAAU,uDACV,mBAAS,OACZ,IACE;AAAA,UACH,eAAe,SAAS,KAAK;AAAA,WAChC;AAAA,WAtEQ,SAAS,EAuEnB;AAAA,IAEJ,CAAC,GACH;AAAA,IAGF;AAAA,MAAC;AAAA;AAAA,QACC,MAAM,gBAAgB;AAAA,QACtB,MAAM,aAAa,QAAQ;AAAA,QAC3B,UAAU,aAAa,YAAY;AAAA,QACnC;AAAA,QACA;AAAA,QACA;AAAA,QACA,gBAAgB;AAAA,QAChB,UAAU;AAAA,QACV,mBAAmB,qBAAqB;AAAA,QACxC;AAAA,QACA;AAAA,QACA,SAAS,MAAM,eAAe,IAAI;AAAA,QAClC,SAAS,YAAY;AACnB,yBAAe,IAAI;AACnB,gBAAM,cAAc;AACpB,yCAA+B,EAAE,YAAY,SAAS,MAAM,QAAQ,CAAC;AAAA,QACvE;AAAA,QACA;AAAA;AAAA,IACF;AAAA,IACC;AAAA,KACH;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.7.1-develop.
|
|
3
|
+
"version": "0.7.1-develop.7170.1.d95074d7ba",
|
|
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.7.1-develop.
|
|
256
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
257
|
-
"@open-mercato/ui": "0.7.1-develop.
|
|
255
|
+
"@open-mercato/ai-assistant": "0.7.1-develop.7170.1.d95074d7ba",
|
|
256
|
+
"@open-mercato/shared": "0.7.1-develop.7170.1.d95074d7ba",
|
|
257
|
+
"@open-mercato/ui": "0.7.1-develop.7170.1.d95074d7ba",
|
|
258
258
|
"react": "^19.0.0",
|
|
259
259
|
"react-dom": "^19.0.0"
|
|
260
260
|
},
|
|
261
261
|
"devDependencies": {
|
|
262
|
-
"@open-mercato/ai-assistant": "0.7.1-develop.
|
|
263
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
264
|
-
"@open-mercato/ui": "0.7.1-develop.
|
|
262
|
+
"@open-mercato/ai-assistant": "0.7.1-develop.7170.1.d95074d7ba",
|
|
263
|
+
"@open-mercato/shared": "0.7.1-develop.7170.1.d95074d7ba",
|
|
264
|
+
"@open-mercato/ui": "0.7.1-develop.7170.1.d95074d7ba",
|
|
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",
|
|
@@ -15,7 +15,16 @@ import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
|
15
15
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
16
16
|
import { UniqueConstraintViolationException, LockMode } from '@mikro-orm/core'
|
|
17
17
|
import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
User,
|
|
20
|
+
UserRole,
|
|
21
|
+
Role,
|
|
22
|
+
UserAcl,
|
|
23
|
+
Session,
|
|
24
|
+
PasswordReset,
|
|
25
|
+
UserSidebarPreference,
|
|
26
|
+
SidebarVariant,
|
|
27
|
+
} from '@open-mercato/core/modules/auth/data/entities'
|
|
19
28
|
import { Organization } from '@open-mercato/core/modules/directory/data/entities'
|
|
20
29
|
import { resolveOrganizationScope } from '@open-mercato/core/modules/directory/utils/organizationScope'
|
|
21
30
|
import { E } from '#generated/entities.ids.generated'
|
|
@@ -358,6 +367,8 @@ const createUserCommand: CommandHandler<Record<string, unknown>, CreateUserResul
|
|
|
358
367
|
await em.nativeDelete(UserRole, { user: userId })
|
|
359
368
|
await em.nativeDelete(Session, { user: userId })
|
|
360
369
|
await em.nativeDelete(PasswordReset, { user: userId })
|
|
370
|
+
await em.nativeDelete(UserSidebarPreference, { user: userId })
|
|
371
|
+
await em.nativeDelete(SidebarVariant, { user: userId })
|
|
361
372
|
|
|
362
373
|
if (snapshot?.custom && Object.keys(snapshot.custom).length) {
|
|
363
374
|
const reset = buildCustomFieldResetMap(undefined, snapshot.custom)
|
|
@@ -937,6 +948,8 @@ const deleteUserCommand: CommandHandler<{ body?: Record<string, unknown>; query?
|
|
|
937
948
|
await em.nativeDelete(UserRole, { user: id })
|
|
938
949
|
await em.nativeDelete(Session, { user: id })
|
|
939
950
|
await em.nativeDelete(PasswordReset, { user: id })
|
|
951
|
+
await em.nativeDelete(UserSidebarPreference, { user: id })
|
|
952
|
+
await em.nativeDelete(SidebarVariant, { user: id })
|
|
940
953
|
const removed = await de.deleteOrmEntity({
|
|
941
954
|
entity: User,
|
|
942
955
|
where: deleteWhere as FilterQuery<User>,
|
|
@@ -84,7 +84,7 @@ export class UserSidebarPreference {
|
|
|
84
84
|
@PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
|
|
85
85
|
id!: string
|
|
86
86
|
|
|
87
|
-
@ManyToOne(() => User)
|
|
87
|
+
@ManyToOne(() => User, { deleteRule: 'cascade' })
|
|
88
88
|
user!: User
|
|
89
89
|
|
|
90
90
|
@Property({ name: 'tenant_id', type: 'uuid', nullable: true })
|
|
@@ -149,7 +149,7 @@ export class SidebarVariant {
|
|
|
149
149
|
@PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
|
|
150
150
|
id!: string
|
|
151
151
|
|
|
152
|
-
@ManyToOne(() => User)
|
|
152
|
+
@ManyToOne(() => User, { deleteRule: 'cascade' })
|
|
153
153
|
user!: User
|
|
154
154
|
|
|
155
155
|
@Property({ name: 'tenant_id', type: 'uuid', nullable: true })
|
|
@@ -13,10 +13,14 @@ import type {
|
|
|
13
13
|
} from '@open-mercato/shared/modules/navigation/backendChrome'
|
|
14
14
|
import {
|
|
15
15
|
buildAdminNav,
|
|
16
|
+
buildProfileSections,
|
|
16
17
|
buildSettingsSections,
|
|
17
18
|
computeSettingsPathPrefixes,
|
|
18
19
|
convertToSectionNavGroups,
|
|
20
|
+
mergeSectionsWithDiscovered,
|
|
19
21
|
type AdminNavItem,
|
|
22
|
+
type SettingsSection,
|
|
23
|
+
type SettingsSectionItem,
|
|
20
24
|
} from '@open-mercato/ui/backend/utils/nav'
|
|
21
25
|
import { resolveRegisteredLucideIconNode } from '@open-mercato/ui/backend/icons/lucideRegistry'
|
|
22
26
|
import { profilePathPrefixes, profileSections } from './profile-sections'
|
|
@@ -106,6 +110,36 @@ export const settingsSectionOrder: Record<string, number> = {
|
|
|
106
110
|
'settings.sections.featureToggles': 8,
|
|
107
111
|
}
|
|
108
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Profile section weights, keyed by the untranslated group id, exactly like `settingsSectionOrder`.
|
|
115
|
+
*
|
|
116
|
+
* Only the built-in account section is ranked; a module contributing its own profile group sorts
|
|
117
|
+
* after it on the shared 999 fallback rather than displacing the account pages.
|
|
118
|
+
*/
|
|
119
|
+
export const profileSectionOrder: Record<string, number> = {
|
|
120
|
+
'profile.sections.account': 1,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Path prefixes that put the shell into profile mode: the static list plus every href the resolved
|
|
125
|
+
* profile sections actually link to.
|
|
126
|
+
*
|
|
127
|
+
* `computeSettingsPathPrefixes` is deliberately not reused — it also registers each item's parent
|
|
128
|
+
* directory, which is harmless for `/backend/settings/*` but would hand profile mode a whole
|
|
129
|
+
* unrelated subtree if a module hosts its profile page outside `/backend/profile/`.
|
|
130
|
+
*/
|
|
131
|
+
function resolveProfilePathPrefixes(sections: SettingsSection[]): string[] {
|
|
132
|
+
const prefixes = new Set(profilePathPrefixes)
|
|
133
|
+
const visitItems = (items: SettingsSectionItem[]) => {
|
|
134
|
+
for (const item of items) {
|
|
135
|
+
prefixes.add(item.href)
|
|
136
|
+
if (item.children?.length) visitItems(item.children)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const section of sections) visitItems(section.items)
|
|
140
|
+
return Array.from(prefixes)
|
|
141
|
+
}
|
|
142
|
+
|
|
109
143
|
type NavGroupWithWeight = Omit<BackendChromeNavGroup, 'id' | 'defaultName' | 'items'> & {
|
|
110
144
|
id: string
|
|
111
145
|
defaultName: string
|
|
@@ -460,6 +494,14 @@ export async function resolveBackendChromePayload({
|
|
|
460
494
|
),
|
|
461
495
|
)
|
|
462
496
|
|
|
497
|
+
// The profile sidebar mirrors the profile dropdown: the static baseline covers the `navHidden`
|
|
498
|
+
// pages route discovery drops, and every other `pageContext: 'profile'` page is derived from the
|
|
499
|
+
// manifest so a module adding one lands in both surfaces at once (#5594).
|
|
500
|
+
const resolvedProfileSections = mergeSectionsWithDiscovered(
|
|
501
|
+
profileSections,
|
|
502
|
+
buildProfileSections(entries, profileSectionOrder),
|
|
503
|
+
)
|
|
504
|
+
|
|
463
505
|
const requestOrganizationId = request ? getSelectedOrganizationFromRequest(request) : null
|
|
464
506
|
const fallbackOrganizationId = selectedOrganizationId ?? requestOrganizationId ?? auth.orgId ?? null
|
|
465
507
|
const brandOrganizationId = scopedOrganizationId
|
|
@@ -507,8 +549,10 @@ export async function resolveBackendChromePayload({
|
|
|
507
549
|
groups: appliedGroups.map(({ weight: _weight, ...group }) => group),
|
|
508
550
|
settingsSections,
|
|
509
551
|
settingsPathPrefixes: computeSettingsPathPrefixes(buildSettingsSections(entries, settingsSectionOrder)),
|
|
510
|
-
profileSections: await serializeSectionGroups(
|
|
511
|
-
|
|
552
|
+
profileSections: await serializeSectionGroups(
|
|
553
|
+
convertToSectionNavGroups(resolvedProfileSections, translate),
|
|
554
|
+
),
|
|
555
|
+
profilePathPrefixes: resolveProfilePathPrefixes(resolvedProfileSections),
|
|
512
556
|
grantedFeatures,
|
|
513
557
|
roles: Array.isArray(auth.roles) ? auth.roles : [],
|
|
514
558
|
brand,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
|
-
import type {
|
|
2
|
+
import type { SettingsSection } from '@open-mercato/ui/backend/utils/nav'
|
|
3
3
|
|
|
4
4
|
const KeyIcon = (
|
|
5
5
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
@@ -7,9 +7,20 @@ const KeyIcon = (
|
|
|
7
7
|
</svg>
|
|
8
8
|
)
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Baseline profile sidebar sections for pages route discovery cannot see.
|
|
12
|
+
*
|
|
13
|
+
* `/backend/profile/change-password` is `navHidden`, so `buildAdminNav` drops it and
|
|
14
|
+
* `buildProfileSections` can never derive it. Everything else in the profile sidebar comes from
|
|
15
|
+
* pages declaring `pageContext: 'profile'` and is merged in by `resolveBackendChromePayload`.
|
|
16
|
+
*
|
|
17
|
+
* The section id is the untranslated group id (`profile.sections.account`) — the same convention the
|
|
18
|
+
* settings sidebar adopted in 0.6.8 — so a page declaring `pageGroupKey: 'profile.sections.account'`
|
|
19
|
+
* lands in this section instead of opening a duplicate one.
|
|
20
|
+
*/
|
|
21
|
+
export const profileSections: SettingsSection[] = [
|
|
11
22
|
{
|
|
12
|
-
id: 'account',
|
|
23
|
+
id: 'profile.sections.account',
|
|
13
24
|
label: 'Account',
|
|
14
25
|
labelKey: 'profile.sections.account',
|
|
15
26
|
order: 1,
|
|
@@ -913,7 +913,8 @@
|
|
|
913
913
|
"referencedColumnNames": [
|
|
914
914
|
"id"
|
|
915
915
|
],
|
|
916
|
-
"referencedTableName": "public.users"
|
|
916
|
+
"referencedTableName": "public.users",
|
|
917
|
+
"deleteRule": "cascade"
|
|
917
918
|
}
|
|
918
919
|
},
|
|
919
920
|
"nativeEnums": {}
|
|
@@ -1978,7 +1979,8 @@
|
|
|
1978
1979
|
"referencedColumnNames": [
|
|
1979
1980
|
"id"
|
|
1980
1981
|
],
|
|
1981
|
-
"referencedTableName": "public.users"
|
|
1982
|
+
"referencedTableName": "public.users",
|
|
1983
|
+
"deleteRule": "cascade"
|
|
1982
1984
|
}
|
|
1983
1985
|
},
|
|
1984
1986
|
"nativeEnums": {}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
export class Migration20260907120000_auth extends Migration {
|
|
4
|
+
|
|
5
|
+
override up(): void | Promise<void> {
|
|
6
|
+
this.addSql(`alter table "user_sidebar_preferences" drop constraint "user_sidebar_preferences_user_id_foreign";`);
|
|
7
|
+
this.addSql(`alter table "user_sidebar_preferences" add constraint "user_sidebar_preferences_user_id_foreign" foreign key ("user_id") references "users" ("id") on update cascade on delete cascade;`);
|
|
8
|
+
|
|
9
|
+
this.addSql(`alter table "sidebar_variants" drop constraint "sidebar_variants_user_id_foreign";`);
|
|
10
|
+
this.addSql(`alter table "sidebar_variants" add constraint "sidebar_variants_user_id_foreign" foreign key ("user_id") references "users" ("id") on update cascade on delete cascade;`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override down(): void | Promise<void> {
|
|
14
|
+
this.addSql(`alter table "user_sidebar_preferences" drop constraint "user_sidebar_preferences_user_id_foreign";`);
|
|
15
|
+
this.addSql(`alter table "user_sidebar_preferences" add constraint "user_sidebar_preferences_user_id_foreign" foreign key ("user_id") references "users" ("id") on update cascade;`);
|
|
16
|
+
|
|
17
|
+
this.addSql(`alter table "sidebar_variants" drop constraint "sidebar_variants_user_id_foreign";`);
|
|
18
|
+
this.addSql(`alter table "sidebar_variants" add constraint "sidebar_variants_user_id_foreign" foreign key ("user_id") references "users" ("id");`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
}
|
package/src/modules/communication_channels/backend/profile/communication-channels/page.meta.ts
CHANGED
|
@@ -21,8 +21,8 @@ export const metadata = {
|
|
|
21
21
|
requireFeatures: ['communication_channels.connect_user_channel'],
|
|
22
22
|
pageTitle: 'My communication channels',
|
|
23
23
|
pageTitleKey: 'communication_channels.profile.title',
|
|
24
|
-
pageGroup: '
|
|
25
|
-
pageGroupKey: '
|
|
24
|
+
pageGroup: 'Account',
|
|
25
|
+
pageGroupKey: 'profile.sections.account',
|
|
26
26
|
pageOrder: 20,
|
|
27
27
|
icon: channelsIcon,
|
|
28
28
|
pageContext: 'profile' as const,
|
|
@@ -371,7 +371,9 @@ Configurable via environment variables (`CUSTOMER_LOGIN_POINTS`, `CUSTOMER_LOGIN
|
|
|
371
371
|
### Email Privacy
|
|
372
372
|
|
|
373
373
|
- Emails stored in plaintext but lookups use `hashForLookup` (deterministic hash)
|
|
374
|
-
-
|
|
374
|
+
- Partial unique index on `(tenantId, emailHash) WHERE deleted_at IS NULL` prevents duplicate **active** accounts — a soft-deleted row no longer blocks re-inviting its email (#5532)
|
|
375
|
+
- Every `CustomerUser` email lookup MUST therefore filter `deletedAt: null`; that predicate is load-bearing, not defensive, because two rows can legitimately share `(tenantId, emailHash)` once one is soft-deleted
|
|
376
|
+
- Match lookups against `lookupHashCandidates(email)` rather than a single `hashForLookup(email)`, so rows still carrying the legacy digest are found
|
|
375
377
|
- Error messages never confirm whether an email is registered
|
|
376
378
|
|
|
377
379
|
## Lib Utilities
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { compare as bcryptCompare } from 'bcryptjs'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'
|
|
5
|
+
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
6
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
7
|
+
import { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
8
|
+
import { CustomerUser, CustomerUserRole } from '@open-mercato/core/modules/customer_accounts/data/entities'
|
|
9
|
+
import { EXAMPLE_PORTAL_ACCOUNTS } from '@open-mercato/core/modules/customer_accounts/lib/exampleAccounts'
|
|
10
|
+
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
11
|
+
import { lookupHashCandidates } from '@open-mercato/shared/lib/encryption/aes'
|
|
12
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
13
|
+
|
|
14
|
+
const FEATURE = 'customer_accounts.view'
|
|
15
|
+
|
|
16
|
+
export const metadata = {
|
|
17
|
+
GET: { requireAuth: true, requireFeatures: [FEATURE] },
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Reports which of the `seedExamples` portal accounts can actually be logged into
|
|
22
|
+
* in the caller's organization. An installation initialized with `--no-examples`,
|
|
23
|
+
* or an organization the examples were never seeded into, gets an empty list so
|
|
24
|
+
* the admin UI can hide the demo-credentials sections entirely (#5669).
|
|
25
|
+
*
|
|
26
|
+
* The surfaces this feeds claim "these credentials work", so mere existence is not
|
|
27
|
+
* enough: the lookup applies the same guards `api/login.ts` enforces (active,
|
|
28
|
+
* email-verified, not soft-deleted) and the advertised password is compared
|
|
29
|
+
* against the stored hash, so an account that was deactivated or whose password
|
|
30
|
+
* was changed since seeding drops out instead of being advertised.
|
|
31
|
+
*/
|
|
32
|
+
export async function GET(req: Request) {
|
|
33
|
+
const auth = await getAuthFromRequest(req)
|
|
34
|
+
if (!auth) {
|
|
35
|
+
return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const container = await createRequestContainer()
|
|
39
|
+
const rbacService = container.resolve('rbacService') as RbacService
|
|
40
|
+
const hasAccess = await rbacService.userHasAllFeatures(auth.sub, [FEATURE], { tenantId: auth.tenantId, organizationId: auth.orgId })
|
|
41
|
+
if (!hasAccess) {
|
|
42
|
+
return NextResponse.json({ ok: false, error: 'Insufficient permissions' }, { status: 403 })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const em = container.resolve('em') as EntityManager
|
|
46
|
+
|
|
47
|
+
// Emails are stored encrypted, so the seeded addresses are matched through the
|
|
48
|
+
// deterministic lookup hash (both the current and the legacy candidate).
|
|
49
|
+
const accountByHash = new Map<string, (typeof EXAMPLE_PORTAL_ACCOUNTS)[number]>()
|
|
50
|
+
for (const account of EXAMPLE_PORTAL_ACCOUNTS) {
|
|
51
|
+
for (const candidate of lookupHashCandidates(account.email)) {
|
|
52
|
+
accountByHash.set(candidate, account)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const seededUsers = await findWithDecryption(
|
|
57
|
+
em,
|
|
58
|
+
CustomerUser,
|
|
59
|
+
{
|
|
60
|
+
emailHash: { $in: Array.from(accountByHash.keys()) },
|
|
61
|
+
tenantId: auth.tenantId,
|
|
62
|
+
organizationId: auth.orgId,
|
|
63
|
+
isActive: true,
|
|
64
|
+
emailVerifiedAt: { $ne: null },
|
|
65
|
+
deletedAt: null,
|
|
66
|
+
} as any,
|
|
67
|
+
undefined,
|
|
68
|
+
{ tenantId: auth.tenantId, organizationId: auth.orgId },
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const loginableUsers: typeof seededUsers = []
|
|
72
|
+
for (const user of seededUsers) {
|
|
73
|
+
const account = accountByHash.get(user.emailHash as string)
|
|
74
|
+
if (!account || !user.passwordHash) continue
|
|
75
|
+
if (await bcryptCompare(account.password, user.passwordHash)) loginableUsers.push(user)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (loginableUsers.length === 0) {
|
|
79
|
+
return NextResponse.json({ ok: true, items: [] })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const roleLinks = await findWithDecryption(
|
|
83
|
+
em,
|
|
84
|
+
CustomerUserRole,
|
|
85
|
+
{ user: { $in: loginableUsers.map((user) => user.id) } as any, deletedAt: null } as any,
|
|
86
|
+
{ populate: ['role'] },
|
|
87
|
+
{ tenantId: auth.tenantId, organizationId: auth.orgId },
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
const rolesByUserId = new Map<string, Array<{ id: string; name: string; slug: string }>>()
|
|
91
|
+
for (const link of roleLinks) {
|
|
92
|
+
const role = link.role
|
|
93
|
+
if (!role || role.deletedAt) continue
|
|
94
|
+
const entry = { id: role.id, name: role.name, slug: role.slug }
|
|
95
|
+
const bucket = rolesByUserId.get(link.user.id)
|
|
96
|
+
if (bucket) bucket.push(entry)
|
|
97
|
+
else rolesByUserId.set(link.user.id, [entry])
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const seenEmails = new Set<string>()
|
|
101
|
+
const items: Array<{ email: string; password: string; roles: Array<{ id: string; name: string; slug: string }> }> = []
|
|
102
|
+
for (const user of loginableUsers) {
|
|
103
|
+
const account = accountByHash.get(user.emailHash as string)
|
|
104
|
+
if (!account || seenEmails.has(account.email)) continue
|
|
105
|
+
seenEmails.add(account.email)
|
|
106
|
+
items.push({
|
|
107
|
+
email: account.email,
|
|
108
|
+
password: account.password,
|
|
109
|
+
roles: rolesByUserId.get(user.id) ?? [],
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Keep the response ordered like the seed list so the admin table is stable.
|
|
114
|
+
const orderByEmail = new Map(EXAMPLE_PORTAL_ACCOUNTS.map((account, index) => [account.email, index]))
|
|
115
|
+
items.sort((a, b) => (orderByEmail.get(a.email) ?? 0) - (orderByEmail.get(b.email) ?? 0))
|
|
116
|
+
|
|
117
|
+
return NextResponse.json({ ok: true, items })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const demoAccountSchema = z.object({
|
|
121
|
+
email: z.string().email(),
|
|
122
|
+
password: z.string(),
|
|
123
|
+
roles: z.array(z.object({ id: z.string().uuid(), name: z.string(), slug: z.string() })),
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const errorSchema = z.object({ ok: z.literal(false), error: z.string() })
|
|
127
|
+
|
|
128
|
+
const getMethodDoc: OpenApiMethodDoc = {
|
|
129
|
+
summary: 'List seeded demo portal accounts (admin)',
|
|
130
|
+
description:
|
|
131
|
+
'Returns the example-data portal accounts that exist in the caller’s organization and can still be logged into with the credentials the seeding hook created them with. Accounts that were deactivated, left unverified, soft-deleted, or had their password changed are omitted, as is every account when example data was not seeded.',
|
|
132
|
+
tags: ['Customer Accounts Admin'],
|
|
133
|
+
responses: [{
|
|
134
|
+
status: 200,
|
|
135
|
+
description: 'Seeded demo accounts present in the current organization',
|
|
136
|
+
schema: z.object({ ok: z.literal(true), items: z.array(demoAccountSchema) }),
|
|
137
|
+
}],
|
|
138
|
+
errors: [
|
|
139
|
+
{ status: 401, description: 'Not authenticated', schema: errorSchema },
|
|
140
|
+
{ status: 403, description: 'Insufficient permissions', schema: errorSchema },
|
|
141
|
+
],
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const openApi: OpenApiRouteDoc = {
|
|
145
|
+
summary: 'Seeded demo portal accounts (admin)',
|
|
146
|
+
methods: {
|
|
147
|
+
GET: getMethodDoc,
|
|
148
|
+
},
|
|
149
|
+
}
|