@open-mercato/core 0.6.6-develop.6526.1.6df9b3dd84 → 0.6.6-develop.6530.1.6d8442bafd

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 (24) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customer_accounts/widgets/injection-table.js +0 -2
  3. package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
  4. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +7 -1
  5. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  6. package/dist/modules/customers/components/detail/CompanyDetailTabs.js +11 -7
  7. package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
  8. package/dist/modules/customers/components/detail/useDealsAccess.js +16 -0
  9. package/dist/modules/customers/components/detail/useDealsAccess.js.map +7 -0
  10. package/dist/modules/sales/components/documents/AddressesSection.js +6 -2
  11. package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
  12. package/dist/modules/sales/components/documents/SalesDocumentForm.js +44 -0
  13. package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
  14. package/dist/modules/sales/components/documents/ShipmentDialog.js +59 -38
  15. package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
  16. package/package.json +7 -7
  17. package/src/modules/customer_accounts/AGENTS.md +8 -6
  18. package/src/modules/customer_accounts/widgets/injection-table.ts +0 -2
  19. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +11 -1
  20. package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +13 -7
  21. package/src/modules/customers/components/detail/useDealsAccess.ts +19 -0
  22. package/src/modules/sales/components/documents/AddressesSection.tsx +6 -2
  23. package/src/modules/sales/components/documents/SalesDocumentForm.tsx +46 -0
  24. package/src/modules/sales/components/documents/ShipmentDialog.tsx +65 -40
@@ -14,6 +14,7 @@ import {
14
14
  Plus,
15
15
  } from 'lucide-react'
16
16
  import type { SectionAction } from '@open-mercato/ui/backend/detail'
17
+ import { useDealsAccess } from './useDealsAccess'
17
18
 
18
19
  export type CompanyTabId =
19
20
  | 'people'
@@ -87,6 +88,7 @@ export function CompanyDetailTabs({
87
88
  children,
88
89
  }: CompanyDetailTabsProps) {
89
90
  const t = useT()
91
+ const { canViewDeals } = useDealsAccess()
90
92
 
91
93
  const builtInTabs: TabDef[] = React.useMemo(
92
94
  () => [
@@ -96,12 +98,16 @@ export function CompanyDetailTabs({
96
98
  icon: <Users className="size-4" />,
97
99
  badge: <CountBadge count={peopleCount} />,
98
100
  },
99
- {
100
- id: 'deals',
101
- label: t('customers.companies.detail.tabs.deals', 'Deals'),
102
- icon: <Handshake className="size-4" />,
103
- badge: <CountBadge count={dealsCount} />,
104
- },
101
+ ...(canViewDeals
102
+ ? [
103
+ {
104
+ id: 'deals' as CompanyTabId,
105
+ label: t('customers.companies.detail.tabs.deals', 'Deals'),
106
+ icon: <Handshake className="size-4" />,
107
+ badge: <CountBadge count={dealsCount} />,
108
+ },
109
+ ]
110
+ : []),
105
111
  {
106
112
  id: 'activity-log',
107
113
  label: t('customers.companies.detail.tabs.activityLog', 'Activity log'),
@@ -121,7 +127,7 @@ export function CompanyDetailTabs({
121
127
  badge: <CountBadge count={filesCount} />,
122
128
  },
123
129
  ],
124
- [t, peopleCount, dealsCount, activitiesCount, filesCount],
130
+ [t, canViewDeals, peopleCount, dealsCount, activitiesCount, filesCount],
125
131
  )
126
132
 
127
133
  const allTabs: TabDef[] = React.useMemo(
@@ -0,0 +1,19 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { useBackendChrome } from '@open-mercato/ui/backend/BackendChromeProvider'
5
+ import { hasFeature } from '@open-mercato/shared/security/features'
6
+
7
+ export type DealsAccess = {
8
+ canViewDeals: boolean
9
+ isReady: boolean
10
+ }
11
+
12
+ export function useDealsAccess(): DealsAccess {
13
+ const { payload, isReady } = useBackendChrome()
14
+ const canViewDeals = React.useMemo(
15
+ () => hasFeature(payload?.grantedFeatures ?? [], 'customers.deals.view'),
16
+ [payload?.grantedFeatures],
17
+ )
18
+ return { canViewDeals, isReady }
19
+ }
@@ -890,8 +890,12 @@ export function SalesDocumentAddressesSection({
890
890
  if (res?.result?.id) billingId = res.result.id
891
891
  }
892
892
 
893
- payload.shippingAddressSnapshot = shippingSnapshot ?? null
894
- payload.billingAddressSnapshot = billingSnapshot ?? null
893
+ if (shippingSnapshot || !shippingId) {
894
+ payload.shippingAddressSnapshot = shippingSnapshot ?? null
895
+ }
896
+ if (billingSnapshot || !billingId) {
897
+ payload.billingAddressSnapshot = billingSnapshot ?? null
898
+ }
895
899
  payload.shippingAddressId = shippingSnapshot ? null : shippingId
896
900
  payload.billingAddressId = billingSnapshot ? null : billingId
897
901
 
@@ -168,6 +168,7 @@ function CustomerQuickCreate({ t, onCreated }: CustomerQuickCreateProps) {
168
168
  async (values: PersonFormValues) => {
169
169
  setSaving(true)
170
170
  try {
171
+ const addresses = Array.isArray(values.addresses) ? values.addresses : []
171
172
  const payload = buildPersonPayload(values, organizationId)
172
173
  const { result } = await createCrud<{ id?: string; entityId?: string }>('customers/people', payload, {
173
174
  errorMessage: t('sales.documents.form.customer.quick.error', 'Failed to create customer.'),
@@ -177,6 +178,51 @@ function CustomerQuickCreate({ t, onCreated }: CustomerQuickCreateProps) {
177
178
  (result && typeof result.id === 'string' && result.id) ||
178
179
  null
179
180
  if (!id) throw new Error('Missing customer id')
181
+ if (addresses.length) {
182
+ const normalize = (value?: string | null) => {
183
+ if (typeof value !== 'string') return undefined
184
+ const trimmed = value.trim()
185
+ return trimmed.length ? trimmed : undefined
186
+ }
187
+ for (const entry of addresses) {
188
+ const normalizedLine1 = normalize(entry.addressLine1)
189
+ if (!normalizedLine1) continue
190
+ const body: Record<string, unknown> = {
191
+ entityId: id,
192
+ ...(organizationId ? { organizationId } : {}),
193
+ addressLine1: normalizedLine1,
194
+ isPrimary: entry.isPrimary ?? false,
195
+ }
196
+ const name = normalize(entry.name)
197
+ if (name !== undefined) body.name = name
198
+ const purpose = normalize(entry.purpose)
199
+ if (purpose !== undefined) body.purpose = purpose
200
+ const line2 = normalize(entry.addressLine2)
201
+ if (line2 !== undefined) body.addressLine2 = line2
202
+ const buildingNumber = normalize(entry.buildingNumber)
203
+ if (buildingNumber !== undefined) body.buildingNumber = buildingNumber
204
+ const flatNumber = normalize(entry.flatNumber)
205
+ if (flatNumber !== undefined) body.flatNumber = flatNumber
206
+ const city = normalize(entry.city)
207
+ if (city !== undefined) body.city = city
208
+ const region = normalize(entry.region)
209
+ if (region !== undefined) body.region = region
210
+ const postalCode = normalize(entry.postalCode)
211
+ if (postalCode !== undefined) body.postalCode = postalCode
212
+ const country = normalize(entry.country)
213
+ if (country !== undefined) body.country = country.toUpperCase()
214
+ if (typeof entry.latitude === 'number') body.latitude = entry.latitude
215
+ if (typeof entry.longitude === 'number') body.longitude = entry.longitude
216
+ try {
217
+ await createCrud('customers/addresses', body)
218
+ } catch (addressErr) {
219
+ const message =
220
+ (addressErr instanceof Error && addressErr.message ? addressErr.message : null) ||
221
+ t('customers.people.detail.addresses.error')
222
+ flash(message, 'error')
223
+ }
224
+ }
225
+ }
180
226
  const displayName =
181
227
  typeof values.displayName === 'string' && values.displayName.trim().length
182
228
  ? values.displayName.trim()
@@ -296,6 +296,13 @@ export function ShipmentDialog({
296
296
  const [addressOptions, setAddressOptions] = React.useState<ShipmentAddressOption[]>([])
297
297
  const [addressLoading, setAddressLoading] = React.useState(false)
298
298
  const [addressError, setAddressError] = React.useState<string | null>(null)
299
+ const addressLoadPromiseRef = React.useRef<{
300
+ orderId: string
301
+ promise: Promise<ShipmentAddressOption[]>
302
+ } | null>(null)
303
+ const currentAddressOrderIdRef = React.useRef(orderId)
304
+ currentAddressOrderIdRef.current = orderId
305
+ const initializedDialogKeyRef = React.useRef<string | null>(null)
299
306
  const [documentStatuses, setDocumentStatuses] = React.useState<StatusOption[]>([])
300
307
  const [lineStatuses, setLineStatuses] = React.useState<StatusOption[]>([])
301
308
  const [shipmentStatuses, setShipmentStatuses] = React.useState<StatusOption[]>([])
@@ -479,37 +486,52 @@ export function ShipmentDialog({
479
486
  [],
480
487
  )
481
488
 
482
- const loadAddressOptions = React.useCallback(async () => {
489
+ const loadAddressOptions = React.useCallback((): Promise<ShipmentAddressOption[]> => {
490
+ const activeRequest = addressLoadPromiseRef.current
491
+ if (activeRequest?.orderId === orderId) return activeRequest.promise
492
+
483
493
  setAddressLoading(true)
484
- try {
485
- const params = new URLSearchParams({
486
- page: '1',
487
- pageSize: '100',
488
- documentId: orderId,
489
- documentKind: 'order',
490
- })
491
- const response = await apiCall<{ items?: Array<Record<string, unknown>> }>(
492
- `/api/sales/document-addresses?${params.toString()}`,
493
- undefined,
494
- { fallback: { items: [] } },
495
- )
496
- const items = Array.isArray(response.result?.items) ? response.result.items : []
497
- const mapped = items
498
- .map((item) => mapDocumentAddressOption(item))
499
- .filter((entry): entry is ShipmentAddressOption => Boolean(entry))
500
- mergeAddressOptions(mapped)
501
- setAddressError(null)
502
- return mapped
503
- } catch (err) {
504
- logger.error('sales.shipments.addresses.load', { err })
505
- setAddressError(
506
- t('sales.documents.shipments.addressLoadError', 'Failed to load addresses.'),
507
- )
508
- return []
509
- } finally {
510
- setAddressLoading(false)
511
- }
512
- }, [mapDocumentAddressOption, mergeAddressOptions, orderId, t])
494
+ const request = (async () => {
495
+ try {
496
+ const params = new URLSearchParams({
497
+ page: '1',
498
+ pageSize: '100',
499
+ documentId: orderId,
500
+ documentKind: 'order',
501
+ })
502
+ const response = await apiCall<{ items?: Array<Record<string, unknown>> }>(
503
+ `/api/sales/document-addresses?${params.toString()}`,
504
+ undefined,
505
+ { fallback: { items: [] } },
506
+ )
507
+ const items = Array.isArray(response.result?.items) ? response.result.items : []
508
+ const mapped = items
509
+ .map((item) => mapDocumentAddressOption(item))
510
+ .filter((entry): entry is ShipmentAddressOption => Boolean(entry))
511
+ const loadedOptions = dedupeAddressOptions([...baseAddressOptions, ...mapped])
512
+ if (currentAddressOrderIdRef.current !== orderId) return baseAddressOptions
513
+ mergeAddressOptions(loadedOptions)
514
+ setAddressError(null)
515
+ return loadedOptions
516
+ } catch (err) {
517
+ logger.error('sales.shipments.addresses.load', { err })
518
+ if (currentAddressOrderIdRef.current === orderId) {
519
+ setAddressError(
520
+ t('sales.documents.shipments.addressLoadError', 'Failed to load addresses.'),
521
+ )
522
+ }
523
+ return baseAddressOptions
524
+ }
525
+ })()
526
+ addressLoadPromiseRef.current = { orderId, promise: request }
527
+ void request.then(() => {
528
+ if (addressLoadPromiseRef.current?.promise === request) {
529
+ addressLoadPromiseRef.current = null
530
+ setAddressLoading(false)
531
+ }
532
+ })
533
+ return request
534
+ }, [baseAddressOptions, mapDocumentAddressOption, mergeAddressOptions, orderId, t])
513
535
 
514
536
  const buildPriceSubtitle = React.useCallback(
515
537
  (option: ShippingMethodOption): string | undefined => {
@@ -851,16 +873,20 @@ export function ShipmentDialog({
851
873
  )
852
874
 
853
875
  React.useEffect(() => {
854
- if (!open) return
876
+ if (!open) {
877
+ initializedDialogKeyRef.current = null
878
+ return
879
+ }
880
+ const dialogKey = `${orderId}:${mode}:${shipment?.id ?? 'new'}`
881
+ if (initializedDialogKeyRef.current === dialogKey) return
882
+ initializedDialogKeyRef.current = dialogKey
855
883
  setFormResetKey((prev) => prev + 1)
856
884
  if (!shippingMethods.length) {
857
885
  void loadShippingMethods()
858
886
  }
859
887
  setAddressOptions(baseAddressOptions)
860
888
  setAddressError(null)
861
- if (!addressOptions.length) {
862
- void loadAddressOptions()
863
- }
889
+ void loadAddressOptions()
864
890
  if (!shipmentStatuses.length) {
865
891
  void loadShipmentStatuses()
866
892
  }
@@ -871,7 +897,6 @@ export function ShipmentDialog({
871
897
  void loadLineStatuses()
872
898
  }
873
899
  }, [
874
- addressOptions.length,
875
900
  baseAddressOptions,
876
901
  documentStatuses.length,
877
902
  loadAddressOptions,
@@ -881,6 +906,8 @@ export function ShipmentDialog({
881
906
  loadShippingMethods,
882
907
  mode,
883
908
  open,
909
+ orderId,
910
+ shipment?.id,
884
911
  lineStatuses.length,
885
912
  shipmentStatuses.length,
886
913
  shippingMethods.length,
@@ -939,11 +966,9 @@ export function ShipmentDialog({
939
966
 
940
967
  const fetchAddressItems = React.useCallback(
941
968
  async (query?: string): Promise<LookupSelectItem[]> => {
942
- if (!addressOptions.length && !addressLoading) {
943
- await loadAddressOptions()
944
- }
969
+ const options = addressOptions.length ? addressOptions : await loadAddressOptions()
945
970
  const needle = query?.trim().toLowerCase() ?? ''
946
- return addressOptions
971
+ return options
947
972
  .filter((option) => {
948
973
  if (!needle) return true
949
974
  return (
@@ -962,7 +987,7 @@ export function ShipmentDialog({
962
987
  ),
963
988
  }))
964
989
  },
965
- [addressLoading, addressOptions, loadAddressOptions],
990
+ [addressOptions, loadAddressOptions],
966
991
  )
967
992
 
968
993
  const validateItems = React.useCallback(