@open-mercato/core 0.6.6-develop.6330.1.a261878aa8 → 0.6.6-develop.6332.1.6e73f0f55b
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/dist/modules/attachments/components/AttachmentPartitionSettings.js +3 -0
- package/dist/modules/attachments/components/AttachmentPartitionSettings.js.map +2 -2
- package/dist/modules/auth/api/logout.js +2 -3
- package/dist/modules/auth/api/logout.js.map +2 -2
- package/dist/modules/auth/api/roles/acl/route.js +2 -2
- package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
- package/dist/modules/auth/api/session/refresh.js +5 -6
- package/dist/modules/auth/api/session/refresh.js.map +2 -2
- package/dist/modules/auth/api/users/acl/route.js +2 -2
- package/dist/modules/auth/api/users/acl/route.js.map +2 -2
- package/dist/modules/auth/backend/roles/[id]/edit/page.js +12 -0
- package/dist/modules/auth/backend/roles/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/backend/users/[id]/edit/page.js +12 -0
- package/dist/modules/auth/backend/users/[id]/edit/page.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/requestRedirect.js +31 -3
- package/dist/modules/auth/lib/requestRedirect.js.map +2 -2
- package/dist/modules/business_rules/api/rules/route.js +3 -3
- package/dist/modules/business_rules/api/rules/route.js.map +2 -2
- package/dist/modules/business_rules/api/sets/route.js +3 -3
- package/dist/modules/business_rules/api/sets/route.js.map +2 -2
- package/dist/modules/business_rules/backend/rules/[id]/page.js +12 -1
- package/dist/modules/business_rules/backend/rules/[id]/page.js.map +2 -2
- package/dist/modules/business_rules/backend/sets/[id]/page.js +12 -1
- package/dist/modules/business_rules/backend/sets/[id]/page.js.map +2 -2
- package/dist/modules/catalog/api/product-media/route.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js +12 -0
- package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +15 -0
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +12 -1
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
- package/dist/modules/catalog/components/PriceKindSettings.js +13 -3
- package/dist/modules/catalog/components/PriceKindSettings.js.map +2 -2
- package/dist/modules/catalog/lib/bulkDelete.js.map +2 -2
- package/dist/modules/currencies/backend/currencies/[id]/page.js +15 -3
- package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
- package/dist/modules/currencies/backend/currencies/page.js +2 -1
- package/dist/modules/currencies/backend/currencies/page.js.map +2 -2
- package/dist/modules/currencies/backend/exchange-rates/[id]/page.js +12 -1
- package/dist/modules/currencies/backend/exchange-rates/[id]/page.js.map +2 -2
- package/dist/modules/currencies/backend/exchange-rates/page.js +3 -2
- package/dist/modules/currencies/backend/exchange-rates/page.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/roles/[id].js +3 -3
- package/dist/modules/customer_accounts/api/admin/roles/[id].js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users/[id].js +3 -3
- package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
- package/dist/modules/customers/api/addresses/route.js +4 -2
- package/dist/modules/customers/api/addresses/route.js.map +2 -2
- package/dist/modules/customers/api/tags/route.js +8 -3
- package/dist/modules/customers/api/tags/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +13 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +24 -9
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js +53 -23
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.js +13 -3
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +19 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +13 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/commands/interactions.js +5 -5
- package/dist/modules/customers/commands/interactions.js.map +2 -2
- package/dist/modules/customers/commands/pipeline-stages.js +24 -2
- package/dist/modules/customers/commands/pipeline-stages.js.map +2 -2
- package/dist/modules/customers/commands/pipelines.js +24 -2
- package/dist/modules/customers/commands/pipelines.js.map +2 -2
- package/dist/modules/customers/components/detail/AddressesSection.js +46 -29
- package/dist/modules/customers/components/detail/AddressesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +5 -1
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/notesAdapter.js +25 -18
- package/dist/modules/customers/components/detail/notesAdapter.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/[id]/route.js +38 -19
- package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/route.js +1 -1
- package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.js +32 -15
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-schedule-service.js +19 -5
- package/dist/modules/data_sync/lib/sync-schedule-service.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js +3 -3
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/route.js +3 -3
- package/dist/modules/dictionaries/api/[dictionaryId]/route.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionariesManager.js +12 -1
- package/dist/modules/dictionaries/components/DictionariesManager.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js.map +2 -2
- package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js +12 -0
- package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js.map +2 -2
- package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.js +12 -0
- package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.js.map +2 -2
- package/dist/modules/entities/api/entities.js +2 -2
- package/dist/modules/entities/api/entities.js.map +2 -2
- package/dist/modules/entities/api/records.js +7 -5
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/feature_toggles/api/overrides/route.js +2 -2
- package/dist/modules/feature_toggles/api/overrides/route.js.map +2 -2
- package/dist/modules/feature_toggles/backend/feature-toggles/global/[id]/edit/page.js.map +2 -2
- package/dist/modules/feature_toggles/data/entities.js +1 -1
- package/dist/modules/feature_toggles/data/entities.js.map +2 -2
- package/dist/modules/feature_toggles/data/validators.js +2 -1
- package/dist/modules/feature_toggles/data/validators.js.map +2 -2
- package/dist/modules/feature_toggles/lib/queries.js +2 -1
- package/dist/modules/feature_toggles/lib/queries.js.map +2 -2
- package/dist/modules/inbox_ops/api/settings/route.js +2 -2
- package/dist/modules/inbox_ops/api/settings/route.js.map +2 -2
- package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +12 -1
- package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js +12 -1
- package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
- package/dist/modules/resources/components/detail/notesAdapter.js +25 -18
- package/dist/modules/resources/components/detail/notesAdapter.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +14 -1
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +11 -10
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/commands/payments.js +6 -1
- package/dist/modules/sales/commands/payments.js.map +2 -2
- package/dist/modules/sales/commands/returns.js +3 -3
- package/dist/modules/sales/commands/returns.js.map +2 -2
- package/dist/modules/sales/commands/shared.js +3 -3
- package/dist/modules/sales/commands/shared.js.map +2 -2
- package/dist/modules/sales/commands/shipments.js +6 -1
- package/dist/modules/sales/commands/shipments.js.map +2 -2
- package/dist/modules/sales/components/documents/PaymentDialog.js +6 -3
- package/dist/modules/sales/components/documents/PaymentDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/PaymentsSection.js +7 -3
- package/dist/modules/sales/components/documents/PaymentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentDialog.js +6 -3
- package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentsSection.js +7 -3
- package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
- package/dist/modules/sales/di.js +39 -3
- package/dist/modules/sales/di.js.map +2 -2
- package/dist/modules/sales/setup.js +2 -0
- package/dist/modules/sales/setup.js.map +2 -2
- package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js +12 -1
- package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js +12 -1
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js +12 -1
- package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +12 -1
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/commands/job-histories.js +3 -3
- package/dist/modules/staff/commands/job-histories.js.map +2 -2
- package/dist/modules/staff/components/detail/addressesAdapter.js +33 -24
- package/dist/modules/staff/components/detail/addressesAdapter.js.map +2 -2
- package/dist/modules/staff/components/detail/notesAdapter.js +25 -18
- package/dist/modules/staff/components/detail/notesAdapter.js.map +2 -2
- package/dist/modules/translations/api/[entityType]/[entityId]/route.js +37 -0
- package/dist/modules/translations/api/[entityType]/[entityId]/route.js.map +2 -2
- package/dist/modules/translations/components/TranslationManager.js +7 -1
- package/dist/modules/translations/components/TranslationManager.js.map +2 -2
- package/dist/modules/workflows/api/definitions/[id]/route.js +3 -3
- package/dist/modules/workflows/api/definitions/[id]/route.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/[id]/page.js +12 -1
- package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js +11 -1
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/attachments/components/AttachmentPartitionSettings.tsx +3 -0
- package/src/modules/auth/api/logout.ts +2 -3
- package/src/modules/auth/api/roles/acl/route.ts +2 -2
- package/src/modules/auth/api/session/refresh.ts +5 -6
- package/src/modules/auth/api/users/acl/route.ts +2 -2
- package/src/modules/auth/backend/roles/[id]/edit/page.tsx +17 -0
- package/src/modules/auth/backend/users/[id]/edit/page.tsx +17 -0
- package/src/modules/auth/data/entities.ts +2 -2
- package/src/modules/auth/lib/requestRedirect.ts +44 -2
- package/src/modules/business_rules/api/rules/route.ts +3 -3
- package/src/modules/business_rules/api/sets/route.ts +3 -3
- package/src/modules/business_rules/backend/rules/[id]/page.tsx +17 -1
- package/src/modules/business_rules/backend/sets/[id]/page.tsx +18 -1
- package/src/modules/catalog/api/product-media/route.ts +4 -0
- package/src/modules/catalog/backend/catalog/categories/[id]/edit/page.tsx +17 -0
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +21 -0
- package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +16 -1
- package/src/modules/catalog/components/PriceKindSettings.tsx +15 -7
- package/src/modules/catalog/lib/bulkDelete.ts +6 -0
- package/src/modules/currencies/backend/currencies/[id]/page.tsx +20 -3
- package/src/modules/currencies/backend/currencies/page.tsx +2 -1
- package/src/modules/currencies/backend/exchange-rates/[id]/page.tsx +17 -1
- package/src/modules/currencies/backend/exchange-rates/page.tsx +3 -2
- package/src/modules/customer_accounts/api/admin/roles/[id].ts +3 -3
- package/src/modules/customer_accounts/api/admin/users/[id].ts +3 -3
- package/src/modules/customers/api/addresses/route.ts +2 -0
- package/src/modules/customers/api/tags/route.ts +6 -1
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +19 -2
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +29 -8
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.ts +47 -20
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.ts +13 -3
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +21 -1
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +18 -2
- package/src/modules/customers/commands/interactions.ts +5 -5
- package/src/modules/customers/commands/pipeline-stages.ts +26 -2
- package/src/modules/customers/commands/pipelines.ts +26 -2
- package/src/modules/customers/components/detail/AddressesSection.tsx +50 -28
- package/src/modules/customers/components/detail/DealForm.tsx +12 -0
- package/src/modules/customers/components/detail/notesAdapter.ts +29 -18
- package/src/modules/customers/i18n/de.json +2 -0
- package/src/modules/customers/i18n/en.json +2 -0
- package/src/modules/customers/i18n/es.json +2 -0
- package/src/modules/customers/i18n/pl.json +2 -0
- package/src/modules/data_sync/api/schedules/[id]/route.ts +38 -20
- package/src/modules/data_sync/api/schedules/route.ts +1 -1
- package/src/modules/data_sync/backend/data-sync/page.tsx +32 -15
- package/src/modules/data_sync/lib/sync-schedule-service.ts +30 -5
- package/src/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.ts +3 -3
- package/src/modules/dictionaries/api/[dictionaryId]/route.ts +3 -3
- package/src/modules/dictionaries/components/DictionariesManager.tsx +21 -1
- package/src/modules/dictionaries/components/DictionaryEntriesEditor.tsx +2 -0
- package/src/modules/directory/backend/directory/organizations/[id]/edit/page.tsx +17 -0
- package/src/modules/directory/backend/directory/tenants/[id]/edit/page.tsx +17 -0
- package/src/modules/entities/api/entities.ts +2 -2
- package/src/modules/entities/api/records.ts +7 -5
- package/src/modules/feature_toggles/api/overrides/route.ts +2 -2
- package/src/modules/feature_toggles/backend/feature-toggles/global/[id]/edit/page.tsx +12 -0
- package/src/modules/feature_toggles/data/entities.ts +1 -1
- package/src/modules/feature_toggles/data/validators.ts +1 -0
- package/src/modules/feature_toggles/lib/queries.ts +1 -0
- package/src/modules/inbox_ops/api/settings/route.ts +2 -2
- package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +17 -1
- package/src/modules/resources/backend/resources/resources/[id]/page.tsx +17 -1
- package/src/modules/resources/components/detail/notesAdapter.ts +29 -18
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +17 -1
- package/src/modules/sales/commands/documents.ts +11 -10
- package/src/modules/sales/commands/payments.ts +11 -0
- package/src/modules/sales/commands/returns.ts +3 -3
- package/src/modules/sales/commands/shared.ts +10 -4
- package/src/modules/sales/commands/shipments.ts +14 -0
- package/src/modules/sales/components/documents/PaymentDialog.tsx +7 -3
- package/src/modules/sales/components/documents/PaymentsSection.tsx +8 -3
- package/src/modules/sales/components/documents/ShipmentDialog.tsx +7 -3
- package/src/modules/sales/components/documents/ShipmentsSection.tsx +8 -3
- package/src/modules/sales/di.ts +68 -9
- package/src/modules/sales/setup.ts +2 -0
- package/src/modules/staff/backend/staff/leave-requests/[id]/page.tsx +17 -1
- package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +17 -1
- package/src/modules/staff/backend/staff/team-roles/[id]/edit/page.tsx +17 -1
- package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +17 -1
- package/src/modules/staff/commands/job-histories.ts +3 -3
- package/src/modules/staff/components/detail/addressesAdapter.ts +40 -23
- package/src/modules/staff/components/detail/notesAdapter.ts +28 -18
- package/src/modules/translations/api/[entityType]/[entityId]/route.ts +74 -0
- package/src/modules/translations/components/TranslationManager.tsx +14 -1
- package/src/modules/workflows/api/definitions/[id]/route.ts +3 -3
- package/src/modules/workflows/backend/definitions/[id]/page.tsx +18 -1
- package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +19 -1
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
import { useRouter, useParams } from 'next/navigation'
|
|
4
|
+
import { useRouter, useParams, usePathname } from 'next/navigation'
|
|
5
5
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
6
6
|
import { CrudForm, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
|
|
7
7
|
import { updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
8
8
|
import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
9
9
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
10
10
|
import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
11
|
-
import { buildOptimisticLockHeader
|
|
11
|
+
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
12
|
+
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
13
|
+
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
12
14
|
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
13
15
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
14
16
|
import { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'
|
|
@@ -35,6 +37,7 @@ type CurrencyData = {
|
|
|
35
37
|
export default function EditCurrencyPage({ params }: { params?: { id?: string } }) {
|
|
36
38
|
const t = useT()
|
|
37
39
|
const router = useRouter()
|
|
40
|
+
const pathname = usePathname()
|
|
38
41
|
const { confirm: confirmDialog, ConfirmDialogElement } = useConfirmDialog()
|
|
39
42
|
const mutationContextId = 'currencies-edit:delete'
|
|
40
43
|
const { runMutation, retryLastMutation } = useGuardedMutation<{
|
|
@@ -185,11 +188,25 @@ export default function EditCurrencyPage({ params }: { params?: { id?: string }
|
|
|
185
188
|
flash(t('currencies.flash.deleted'), 'success')
|
|
186
189
|
router.push('/backend/currencies')
|
|
187
190
|
} catch (error) {
|
|
188
|
-
if (
|
|
191
|
+
if (surfaceRecordConflict(error, t)) return
|
|
189
192
|
flash(t('currencies.flash.deleteError'), 'error')
|
|
190
193
|
}
|
|
191
194
|
}, [currency, t, router, confirmDialog, mutationContextId, retryLastMutation, runMutation])
|
|
192
195
|
|
|
196
|
+
// Publish page-load record context to the AppShell-owned `backend:record:current`
|
|
197
|
+
// mount so the enterprise record_locks widget resolves `currencies.currency` + id
|
|
198
|
+
// explicitly. The resourceKind mirrors the CrudForm `versionHistory` so the held
|
|
199
|
+
// lock matches the save-time conflict surface for the same currency.
|
|
200
|
+
useSetCurrentRecordInjectionContext(
|
|
201
|
+
buildRecordInjectionContext({
|
|
202
|
+
resourceKind: 'currencies.currency',
|
|
203
|
+
resourceId: currency?.id ?? null,
|
|
204
|
+
updatedAt: currency?.updatedAt ?? currency?.updated_at ?? null,
|
|
205
|
+
data: currency as Record<string, unknown> | null,
|
|
206
|
+
path: pathname,
|
|
207
|
+
}),
|
|
208
|
+
)
|
|
209
|
+
|
|
193
210
|
if (loading) {
|
|
194
211
|
return (
|
|
195
212
|
<Page>
|
|
@@ -14,6 +14,7 @@ import { Plus, Star } from 'lucide-react'
|
|
|
14
14
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
15
15
|
import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
16
16
|
import { buildOptimisticLockHeader, extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
17
|
+
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
17
18
|
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
18
19
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
19
20
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
@@ -188,7 +189,7 @@ export default function CurrenciesPage() {
|
|
|
188
189
|
flash(t('currencies.flash.deleted'), 'success')
|
|
189
190
|
setReloadToken((token) => token + 1)
|
|
190
191
|
} catch (error) {
|
|
191
|
-
if (
|
|
192
|
+
if (surfaceRecordConflict(error, t, { onRefresh: () => setReloadToken((token) => token + 1) })) return
|
|
192
193
|
flash(t('currencies.flash.deleteError'), 'error')
|
|
193
194
|
}
|
|
194
195
|
},
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
import { useRouter } from 'next/navigation'
|
|
4
|
+
import { useRouter, usePathname } from 'next/navigation'
|
|
5
5
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
6
6
|
import { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
|
|
7
7
|
import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
|
|
8
8
|
import { updateCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
9
9
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
10
10
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
11
|
+
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
11
12
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
12
13
|
import {
|
|
13
14
|
loadCurrencyOptions,
|
|
@@ -47,6 +48,7 @@ type ExchangeRateData = {
|
|
|
47
48
|
export default function EditExchangeRatePage({ params }: { params?: { id?: string } }) {
|
|
48
49
|
const t = useT()
|
|
49
50
|
const router = useRouter()
|
|
51
|
+
const pathname = usePathname()
|
|
50
52
|
|
|
51
53
|
const [exchangeRate, setExchangeRate] = React.useState<ExchangeRateData | null>(null)
|
|
52
54
|
const [loading, setLoading] = React.useState(true)
|
|
@@ -84,6 +86,20 @@ export default function EditExchangeRatePage({ params }: { params?: { id?: strin
|
|
|
84
86
|
[t, loadOptions]
|
|
85
87
|
)
|
|
86
88
|
|
|
89
|
+
// Publish page-load record context to the AppShell-owned `backend:record:current`
|
|
90
|
+
// mount so the enterprise record_locks widget resolves `currencies.exchange_rate`
|
|
91
|
+
// + id explicitly. The resourceKind mirrors the CrudForm `versionHistory` so the
|
|
92
|
+
// held lock matches the save-time conflict surface for the same exchange rate.
|
|
93
|
+
useSetCurrentRecordInjectionContext(
|
|
94
|
+
buildRecordInjectionContext({
|
|
95
|
+
resourceKind: 'currencies.exchange_rate',
|
|
96
|
+
resourceId: exchangeRate?.id ?? null,
|
|
97
|
+
updatedAt: exchangeRate?.updatedAt ?? exchangeRate?.updated_at ?? null,
|
|
98
|
+
data: exchangeRate as Record<string, unknown> | null,
|
|
99
|
+
path: pathname,
|
|
100
|
+
}),
|
|
101
|
+
)
|
|
102
|
+
|
|
87
103
|
if (loading) {
|
|
88
104
|
return (
|
|
89
105
|
<Page>
|
|
@@ -13,7 +13,8 @@ import { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'
|
|
|
13
13
|
import { Plus } from 'lucide-react'
|
|
14
14
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
15
15
|
import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
16
|
-
import { buildOptimisticLockHeader
|
|
16
|
+
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
17
|
+
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
17
18
|
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
18
19
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
19
20
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
@@ -152,7 +153,7 @@ export default function ExchangeRatesPage() {
|
|
|
152
153
|
flash(t('exchangeRates.flash.deleted'), 'success')
|
|
153
154
|
setReloadToken((token) => token + 1)
|
|
154
155
|
} catch (error) {
|
|
155
|
-
if (
|
|
156
|
+
if (surfaceRecordConflict(error, t, { onRefresh: () => setReloadToken((token) => token + 1) })) return
|
|
156
157
|
flash(t('exchangeRates.flash.deleteError'), 'error')
|
|
157
158
|
}
|
|
158
159
|
},
|
|
@@ -7,7 +7,7 @@ import { RbacService } from '@open-mercato/core/modules/auth/services/rbacServic
|
|
|
7
7
|
import { CustomerRole, CustomerRoleAcl, CustomerUserRole } from '@open-mercato/core/modules/customer_accounts/data/entities'
|
|
8
8
|
import { updateRoleSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'
|
|
9
9
|
import { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'
|
|
10
|
-
import {
|
|
10
|
+
import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
11
11
|
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
12
12
|
|
|
13
13
|
export const metadata = {}
|
|
@@ -107,7 +107,7 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
|
|
|
107
107
|
// Optimistic lock: refuse a stale overwrite so two admins editing the same role
|
|
108
108
|
// in parallel cannot silently clobber each other (#2055). Strictly additive.
|
|
109
109
|
try {
|
|
110
|
-
|
|
110
|
+
await enforceCommandOptimisticLockWithGuards(container, {
|
|
111
111
|
resourceKind: 'customer_accounts.role',
|
|
112
112
|
resourceId: role.id,
|
|
113
113
|
current: role.updatedAt ?? null,
|
|
@@ -169,7 +169,7 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
|
|
|
169
169
|
|
|
170
170
|
// Optimistic lock: refuse a stale delete. Strictly additive.
|
|
171
171
|
try {
|
|
172
|
-
|
|
172
|
+
await enforceCommandOptimisticLockWithGuards(container, {
|
|
173
173
|
resourceKind: 'customer_accounts.role',
|
|
174
174
|
resourceId: role.id,
|
|
175
175
|
current: role.updatedAt ?? null,
|
|
@@ -12,7 +12,7 @@ import { adminUpdateUserSchema } from '@open-mercato/core/modules/customer_accou
|
|
|
12
12
|
import { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'
|
|
13
13
|
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
14
14
|
import { isOwnedCompanyEntity } from '@open-mercato/core/modules/customer_accounts/lib/customerEntityOwnership'
|
|
15
|
-
import {
|
|
15
|
+
import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
16
16
|
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
17
17
|
|
|
18
18
|
export const metadata = {}
|
|
@@ -143,7 +143,7 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
|
|
|
143
143
|
// customer user in parallel cannot silently clobber each other (#2055). The
|
|
144
144
|
// check is strictly additive — a no-op when the client sends no expected-version header.
|
|
145
145
|
try {
|
|
146
|
-
|
|
146
|
+
await enforceCommandOptimisticLockWithGuards(container, {
|
|
147
147
|
resourceKind: 'customer_accounts.user',
|
|
148
148
|
resourceId: user.id,
|
|
149
149
|
current: user.updatedAt ?? null,
|
|
@@ -267,7 +267,7 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
|
|
|
267
267
|
// Optimistic lock: refuse a stale delete (e.g. deleting a record another admin
|
|
268
268
|
// already modified). Strictly additive — a no-op without the expected-version header.
|
|
269
269
|
try {
|
|
270
|
-
|
|
270
|
+
await enforceCommandOptimisticLockWithGuards(container, {
|
|
271
271
|
resourceKind: 'customer_accounts.user',
|
|
272
272
|
resourceId: user.id,
|
|
273
273
|
current: user.updatedAt ?? null,
|
|
@@ -68,6 +68,7 @@ const crud = makeCrudRoute({
|
|
|
68
68
|
'is_primary',
|
|
69
69
|
'organization_id',
|
|
70
70
|
'tenant_id',
|
|
71
|
+
'updated_at',
|
|
71
72
|
],
|
|
72
73
|
sortFieldMap: {
|
|
73
74
|
createdAt: 'created_at',
|
|
@@ -142,6 +143,7 @@ const addressListItemSchema = z.object({
|
|
|
142
143
|
is_primary: z.boolean().nullable().optional(),
|
|
143
144
|
organization_id: z.string().uuid().nullable().optional(),
|
|
144
145
|
tenant_id: z.string().uuid().nullable().optional(),
|
|
146
|
+
updated_at: z.string().nullable().optional(),
|
|
145
147
|
})
|
|
146
148
|
|
|
147
149
|
const addressCreateResponseSchema = z.object({
|
|
@@ -42,12 +42,16 @@ const crud = makeCrudRoute({
|
|
|
42
42
|
idField: 'id',
|
|
43
43
|
orgField: 'organizationId',
|
|
44
44
|
tenantField: 'tenantId',
|
|
45
|
+
// CustomerTag has no `deleted_at` column. Without this, the auto-registered
|
|
46
|
+
// generic optimistic-lock reader would filter on `deletedAt: null` against a
|
|
47
|
+
// non-existent column, throw, and fail open — silently disabling the lock.
|
|
48
|
+
softDeleteField: null,
|
|
45
49
|
},
|
|
46
50
|
indexer: { entityType: E.customers.customer_tag },
|
|
47
51
|
list: {
|
|
48
52
|
schema: listSchema,
|
|
49
53
|
entityId: E.customers.customer_tag,
|
|
50
|
-
fields: ['id', 'slug', 'label', 'color', 'description', 'organization_id', 'tenant_id'],
|
|
54
|
+
fields: ['id', 'slug', 'label', 'color', 'description', 'organization_id', 'tenant_id', 'updated_at'],
|
|
51
55
|
buildFilters: async (query: any) => {
|
|
52
56
|
const filters: Record<string, any> = {}
|
|
53
57
|
if (query.search) {
|
|
@@ -107,6 +111,7 @@ const tagListItemSchema = z.object({
|
|
|
107
111
|
description: z.string().nullable().optional(),
|
|
108
112
|
organization_id: z.string().uuid().nullable().optional(),
|
|
109
113
|
tenant_id: z.string().uuid().nullable().optional(),
|
|
114
|
+
updated_at: z.string().nullable().optional(),
|
|
110
115
|
})
|
|
111
116
|
|
|
112
117
|
const tagCreateResponseSchema = z.object({
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import Link from 'next/link'
|
|
5
|
-
import { useRouter, useSearchParams } from 'next/navigation'
|
|
5
|
+
import { useRouter, useSearchParams, usePathname } from 'next/navigation'
|
|
6
6
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
7
7
|
import { Building2, Hash, Users, BarChart3, StickyNote } from 'lucide-react'
|
|
8
8
|
import { updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
@@ -16,6 +16,7 @@ import { AttachmentsSection, ErrorMessage, LoadingMessage, RecordNotFoundState,
|
|
|
16
16
|
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
17
17
|
import { InjectionSpot, useInjectionWidgets } from '@open-mercato/ui/backend/injection/InjectionSpot'
|
|
18
18
|
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
19
|
+
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
19
20
|
import { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
20
21
|
import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
|
|
21
22
|
import { CollapsibleZoneLayout, type ZoneSectionDescriptor } from '@open-mercato/ui/backend/crud/CollapsibleZoneLayout'
|
|
@@ -49,6 +50,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
49
50
|
const t = useT()
|
|
50
51
|
const router = useRouter()
|
|
51
52
|
const searchParams = useSearchParams()
|
|
53
|
+
const pathname = usePathname()
|
|
52
54
|
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
53
55
|
|
|
54
56
|
const detailTranslator = React.useMemo(() => createTranslatorWithFallback(t), [t])
|
|
@@ -193,6 +195,21 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
193
195
|
logContext: 'customers.companies-v2',
|
|
194
196
|
})
|
|
195
197
|
|
|
198
|
+
// Publish page-load record context to the AppShell-owned `backend:record:current`
|
|
199
|
+
// mount so the enterprise record_locks widget resolves `customers.company` + id
|
|
200
|
+
// explicitly (the hardcoded path allowlist misses the `companies-v2` route).
|
|
201
|
+
useSetCurrentRecordInjectionContext(
|
|
202
|
+
buildRecordInjectionContext({
|
|
203
|
+
resourceKind: 'customers.company',
|
|
204
|
+
resourceId: currentCompanyId,
|
|
205
|
+
updatedAt: (data?.company as { updatedAt?: string | null; updated_at?: string | null } | undefined)?.updatedAt
|
|
206
|
+
?? (data?.company as { updated_at?: string | null } | undefined)?.updated_at
|
|
207
|
+
?? null,
|
|
208
|
+
data: data as Record<string, unknown> | null,
|
|
209
|
+
path: pathname,
|
|
210
|
+
}),
|
|
211
|
+
)
|
|
212
|
+
|
|
196
213
|
const handleEditActivity = React.useCallback((activity: { id: string; interactionType?: string; title?: string | null; body?: string | null; scheduledAt?: string | null; occurredAt?: string | null; [key: string]: unknown }) => {
|
|
197
214
|
const raw = activity as Record<string, unknown>
|
|
198
215
|
const durationValue = typeof raw.duration === 'number'
|
|
@@ -470,7 +487,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
470
487
|
<CrudForm<CompanyEditFormValues>
|
|
471
488
|
embedded
|
|
472
489
|
trackDirtyWhenEmbedded
|
|
473
|
-
injectionSpotId="customers.company"
|
|
490
|
+
injectionSpotId="crud-form:customers.company"
|
|
474
491
|
entityIds={[E.customers.customer_entity, E.customers.customer_company_profile]}
|
|
475
492
|
schema={formSchema}
|
|
476
493
|
fields={formFields}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
2
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
3
|
+
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
3
4
|
import { updateCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
4
|
-
import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
5
|
+
import { readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
6
|
+
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
5
7
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
8
|
import type {
|
|
7
9
|
CompanyAssociationApiRecord,
|
|
@@ -95,6 +97,8 @@ type UseDealAssociationsOptions = {
|
|
|
95
97
|
data: DealDetailPayload | null
|
|
96
98
|
setData: React.Dispatch<React.SetStateAction<DealDetailPayload | null>>
|
|
97
99
|
runMutationWithContext: GuardedMutationRunner
|
|
100
|
+
/** Re-fetch the deal detail; wired into the conflict bar's refresh action on a 409. */
|
|
101
|
+
onRefresh?: (() => void) | null
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
type UseDealAssociationsResult = {
|
|
@@ -113,6 +117,7 @@ export function useDealAssociations({
|
|
|
113
117
|
data,
|
|
114
118
|
setData,
|
|
115
119
|
runMutationWithContext,
|
|
120
|
+
onRefresh,
|
|
116
121
|
}: UseDealAssociationsOptions): UseDealAssociationsResult {
|
|
117
122
|
const t = useT()
|
|
118
123
|
const [peopleEditorIds, setPeopleEditorIds] = React.useState<string[]>([])
|
|
@@ -263,7 +268,10 @@ export function useDealAssociations({
|
|
|
263
268
|
setPeopleSaving(true)
|
|
264
269
|
try {
|
|
265
270
|
await runMutationWithContext(
|
|
266
|
-
() =>
|
|
271
|
+
() => withScopedApiRequestHeaders(
|
|
272
|
+
buildOptimisticLockHeader(data?.deal.updatedAt),
|
|
273
|
+
() => updateCrud('customers/deals', { id: currentDealId, personIds: nextIds }),
|
|
274
|
+
),
|
|
267
275
|
{ id: currentDealId, personIds: nextIds, operation: 'updateDealPeople' },
|
|
268
276
|
)
|
|
269
277
|
const nextPeople = await loadPeopleAssociations(nextIds.slice(0, 3))
|
|
@@ -277,7 +285,7 @@ export function useDealAssociations({
|
|
|
277
285
|
}
|
|
278
286
|
: prev,
|
|
279
287
|
)
|
|
280
|
-
} catch {
|
|
288
|
+
} catch (error) {
|
|
281
289
|
setPeopleEditorIds(previousIds)
|
|
282
290
|
setData((prev) =>
|
|
283
291
|
prev
|
|
@@ -289,12 +297,16 @@ export function useDealAssociations({
|
|
|
289
297
|
}
|
|
290
298
|
: prev,
|
|
291
299
|
)
|
|
292
|
-
|
|
300
|
+
// runMutationWithContext already surfaces the conflict bar on a 409; only
|
|
301
|
+
// fall back to the generic flash when this is not a record conflict.
|
|
302
|
+
if (!surfaceRecordConflict(error, t, { onRefresh: onRefresh ?? null })) {
|
|
303
|
+
flash(t('customers.deals.detail.peopleUpdateError', 'Failed to update linked people.'), 'error')
|
|
304
|
+
}
|
|
293
305
|
} finally {
|
|
294
306
|
setPeopleSaving(false)
|
|
295
307
|
}
|
|
296
308
|
},
|
|
297
|
-
[currentDealId, data?.people, loadPeopleAssociations, peopleEditorIds, runMutationWithContext, setData, t],
|
|
309
|
+
[currentDealId, data?.deal.updatedAt, data?.people, loadPeopleAssociations, onRefresh, peopleEditorIds, runMutationWithContext, setData, t],
|
|
298
310
|
)
|
|
299
311
|
|
|
300
312
|
const handleCompaniesAssociationsChange = React.useCallback(
|
|
@@ -307,7 +319,10 @@ export function useDealAssociations({
|
|
|
307
319
|
setCompaniesSaving(true)
|
|
308
320
|
try {
|
|
309
321
|
await runMutationWithContext(
|
|
310
|
-
() =>
|
|
322
|
+
() => withScopedApiRequestHeaders(
|
|
323
|
+
buildOptimisticLockHeader(data?.deal.updatedAt),
|
|
324
|
+
() => updateCrud('customers/deals', { id: currentDealId, companyIds: nextIds }),
|
|
325
|
+
),
|
|
311
326
|
{ id: currentDealId, companyIds: nextIds, operation: 'updateDealCompanies' },
|
|
312
327
|
)
|
|
313
328
|
const nextCompanies = await loadCompanyAssociations(nextIds.slice(0, 3))
|
|
@@ -321,7 +336,7 @@ export function useDealAssociations({
|
|
|
321
336
|
}
|
|
322
337
|
: prev,
|
|
323
338
|
)
|
|
324
|
-
} catch {
|
|
339
|
+
} catch (error) {
|
|
325
340
|
setCompaniesEditorIds(previousIds)
|
|
326
341
|
setData((prev) =>
|
|
327
342
|
prev
|
|
@@ -333,7 +348,11 @@ export function useDealAssociations({
|
|
|
333
348
|
}
|
|
334
349
|
: prev,
|
|
335
350
|
)
|
|
336
|
-
|
|
351
|
+
// runMutationWithContext already surfaces the conflict bar on a 409; only
|
|
352
|
+
// fall back to the generic flash when this is not a record conflict.
|
|
353
|
+
if (!surfaceRecordConflict(error, t, { onRefresh: onRefresh ?? null })) {
|
|
354
|
+
flash(t('customers.deals.detail.companiesUpdateError', 'Failed to update linked companies.'), 'error')
|
|
355
|
+
}
|
|
337
356
|
} finally {
|
|
338
357
|
setCompaniesSaving(false)
|
|
339
358
|
}
|
|
@@ -342,7 +361,9 @@ export function useDealAssociations({
|
|
|
342
361
|
companiesEditorIds,
|
|
343
362
|
currentDealId,
|
|
344
363
|
data?.companies,
|
|
364
|
+
data?.deal.updatedAt,
|
|
345
365
|
loadCompanyAssociations,
|
|
366
|
+
onRefresh,
|
|
346
367
|
runMutationWithContext,
|
|
347
368
|
setData,
|
|
348
369
|
t,
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
2
|
import { updateCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
3
|
-
import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
3
|
+
import { readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
4
|
+
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
5
|
+
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
6
|
+
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
7
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
4
8
|
import type { DealStatsPayload, GuardedMutationRunner } from './types'
|
|
5
9
|
|
|
6
10
|
type UseDealClosureOptions = {
|
|
7
11
|
currentDealId: string | null
|
|
12
|
+
dealUpdatedAt: string | null
|
|
8
13
|
runMutationWithContext: GuardedMutationRunner
|
|
9
14
|
confirmDiscardIfDirty: () => Promise<boolean>
|
|
10
15
|
onClosed: () => Promise<void>
|
|
@@ -26,10 +31,12 @@ type UseDealClosureResult = {
|
|
|
26
31
|
|
|
27
32
|
export function useDealClosure({
|
|
28
33
|
currentDealId,
|
|
34
|
+
dealUpdatedAt,
|
|
29
35
|
runMutationWithContext,
|
|
30
36
|
confirmDiscardIfDirty,
|
|
31
37
|
onClosed,
|
|
32
38
|
}: UseDealClosureOptions): UseDealClosureResult {
|
|
39
|
+
const t = useT()
|
|
33
40
|
const [lostDialogOpen, setLostDialogOpen] = React.useState(false)
|
|
34
41
|
const [wonPopupOpen, setWonPopupOpen] = React.useState(false)
|
|
35
42
|
const [lostPopupOpen, setLostPopupOpen] = React.useState(false)
|
|
@@ -51,45 +58,65 @@ export function useDealClosure({
|
|
|
51
58
|
const handleWon = React.useCallback(async () => {
|
|
52
59
|
if (!currentDealId) return
|
|
53
60
|
if (!(await confirmDiscardIfDirty())) return
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
try {
|
|
62
|
+
await runMutationWithContext(
|
|
63
|
+
() => withScopedApiRequestHeaders(
|
|
64
|
+
buildOptimisticLockHeader(dealUpdatedAt),
|
|
65
|
+
() => updateCrud('customers/deals', { id: currentDealId, closureOutcome: 'won', status: 'win' }),
|
|
66
|
+
),
|
|
67
|
+
{ id: currentDealId, closureOutcome: 'won', status: 'win', operation: 'closeWon' },
|
|
68
|
+
)
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (!surfaceRecordConflict(err, t, { onRefresh: () => { void onClosed() } })) {
|
|
71
|
+
flash(t('customers.deals.detail.closeWonError', 'Failed to mark deal as won.'), 'error')
|
|
72
|
+
}
|
|
73
|
+
return
|
|
74
|
+
}
|
|
58
75
|
const stats = await fetchDealStats()
|
|
59
76
|
setWonStats(stats)
|
|
60
77
|
setWonPopupOpen(true)
|
|
61
78
|
await onClosed()
|
|
62
|
-
}, [confirmDiscardIfDirty, currentDealId, fetchDealStats, onClosed, runMutationWithContext])
|
|
79
|
+
}, [confirmDiscardIfDirty, currentDealId, dealUpdatedAt, fetchDealStats, onClosed, runMutationWithContext, t])
|
|
63
80
|
|
|
64
81
|
const handleLostConfirm = React.useCallback(
|
|
65
82
|
async (input: { lossReasonId: string; lossNotes?: string }) => {
|
|
66
83
|
if (!currentDealId) return
|
|
67
84
|
if (!(await confirmDiscardIfDirty())) return
|
|
68
|
-
|
|
69
|
-
(
|
|
70
|
-
|
|
85
|
+
try {
|
|
86
|
+
await runMutationWithContext(
|
|
87
|
+
() => withScopedApiRequestHeaders(
|
|
88
|
+
buildOptimisticLockHeader(dealUpdatedAt),
|
|
89
|
+
() =>
|
|
90
|
+
updateCrud('customers/deals', {
|
|
91
|
+
id: currentDealId,
|
|
92
|
+
closureOutcome: 'lost',
|
|
93
|
+
status: 'loose',
|
|
94
|
+
lossReasonId: input.lossReasonId,
|
|
95
|
+
lossNotes: input.lossNotes ?? null,
|
|
96
|
+
}),
|
|
97
|
+
),
|
|
98
|
+
{
|
|
71
99
|
id: currentDealId,
|
|
72
100
|
closureOutcome: 'lost',
|
|
73
101
|
status: 'loose',
|
|
74
102
|
lossReasonId: input.lossReasonId,
|
|
75
103
|
lossNotes: input.lossNotes ?? null,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
)
|
|
104
|
+
operation: 'closeLost',
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (!surfaceRecordConflict(err, t, { onRefresh: () => { void onClosed() } })) {
|
|
109
|
+
flash(t('customers.deals.detail.closeLostError', 'Failed to mark deal as lost.'), 'error')
|
|
110
|
+
}
|
|
111
|
+
return
|
|
112
|
+
}
|
|
86
113
|
setLostDialogOpen(false)
|
|
87
114
|
const stats = await fetchDealStats()
|
|
88
115
|
setLostStats(stats)
|
|
89
116
|
setLostPopupOpen(true)
|
|
90
117
|
await onClosed()
|
|
91
118
|
},
|
|
92
|
-
[confirmDiscardIfDirty, currentDealId, fetchDealStats, onClosed, runMutationWithContext],
|
|
119
|
+
[confirmDiscardIfDirty, currentDealId, dealUpdatedAt, fetchDealStats, onClosed, runMutationWithContext, t],
|
|
93
120
|
)
|
|
94
121
|
|
|
95
122
|
const openLostDialog = React.useCallback(() => setLostDialogOpen(true), [])
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
2
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
3
3
|
import { updateCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
4
|
+
import { withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
5
|
+
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
6
|
+
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
4
7
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
5
8
|
import type { DealDetailPayload, GuardedMutationRunner } from './types'
|
|
6
9
|
|
|
@@ -35,13 +38,20 @@ export function useDealPipeline({
|
|
|
35
38
|
setIsStageSaving(true)
|
|
36
39
|
try {
|
|
37
40
|
await runMutationWithContext(
|
|
38
|
-
() =>
|
|
41
|
+
() => withScopedApiRequestHeaders(
|
|
42
|
+
buildOptimisticLockHeader(data.deal.updatedAt),
|
|
43
|
+
() => updateCrud('customers/deals', { id: currentDealId, pipelineStageId: nextStageId }),
|
|
44
|
+
),
|
|
39
45
|
{ id: currentDealId, pipelineStageId: nextStageId, operation: 'updateDealStage' },
|
|
40
46
|
)
|
|
41
47
|
flash(t('customers.deals.detail.stageUpdateSuccess', 'Deal stage updated.'), 'success')
|
|
42
48
|
await onStageChanged()
|
|
43
|
-
} catch {
|
|
44
|
-
|
|
49
|
+
} catch (err) {
|
|
50
|
+
// A concurrent edit / pessimistic lock 409 routes to the unified conflict
|
|
51
|
+
// bar or merge dialog (S3); surface any other error as a flash.
|
|
52
|
+
if (!surfaceRecordConflict(err, t, { onRefresh: () => { void onStageChanged() } })) {
|
|
53
|
+
flash(t('customers.deals.detail.stageUpdateError', 'Failed to update deal stage.'), 'error')
|
|
54
|
+
}
|
|
45
55
|
} finally {
|
|
46
56
|
setIsStageSaving(false)
|
|
47
57
|
}
|
|
@@ -4,11 +4,12 @@ import * as React from 'react'
|
|
|
4
4
|
import Link from 'next/link'
|
|
5
5
|
import { Building2, UserSearch, Users } from 'lucide-react'
|
|
6
6
|
import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
|
|
7
|
-
import { useRouter, useSearchParams } from 'next/navigation'
|
|
7
|
+
import { useRouter, useSearchParams, usePathname } from 'next/navigation'
|
|
8
8
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
9
9
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
10
10
|
import { AttachmentsSection, ErrorMessage, LoadingMessage, NotesSection, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
|
|
11
11
|
import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
|
|
12
|
+
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
12
13
|
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
13
14
|
import { CollapsibleZoneLayout } from '@open-mercato/ui/backend/crud/CollapsibleZoneLayout'
|
|
14
15
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -52,6 +53,7 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
|
|
|
52
53
|
const t = useT()
|
|
53
54
|
const router = useRouter()
|
|
54
55
|
const searchParams = useSearchParams()
|
|
56
|
+
const pathname = usePathname()
|
|
55
57
|
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
56
58
|
const detailTranslator = React.useMemo(() => createTranslatorWithFallback(t), [t])
|
|
57
59
|
|
|
@@ -80,6 +82,20 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
|
|
|
80
82
|
data,
|
|
81
83
|
})
|
|
82
84
|
|
|
85
|
+
// Publish page-load record context with explicit `updatedAt`/`data` to the
|
|
86
|
+
// AppShell-owned `backend:record:current` mount. The deal path is already in the
|
|
87
|
+
// widget's allowlist (presence works) but supplied no version/record payload —
|
|
88
|
+
// this gives the merge dialog its action-log base and field-diff source.
|
|
89
|
+
useSetCurrentRecordInjectionContext(
|
|
90
|
+
buildRecordInjectionContext({
|
|
91
|
+
resourceKind: 'customers.deal',
|
|
92
|
+
resourceId: data?.deal.id ?? null,
|
|
93
|
+
updatedAt: data?.deal.updatedAt ?? null,
|
|
94
|
+
data: data as Record<string, unknown> | null,
|
|
95
|
+
path: pathname,
|
|
96
|
+
}),
|
|
97
|
+
)
|
|
98
|
+
|
|
83
99
|
const notesAdapter = React.useMemo(
|
|
84
100
|
() => createCustomerNotesAdapter(detailTranslator, { runMutation: runMutationWithContext }),
|
|
85
101
|
[detailTranslator, runMutationWithContext],
|
|
@@ -171,6 +187,7 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
|
|
|
171
187
|
data,
|
|
172
188
|
setData,
|
|
173
189
|
runMutationWithContext,
|
|
190
|
+
onRefresh: () => { void loadData() },
|
|
174
191
|
})
|
|
175
192
|
|
|
176
193
|
const { isStageSaving, handleStageChange } = useDealPipeline({
|
|
@@ -195,6 +212,7 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
|
|
|
195
212
|
handleLostConfirm,
|
|
196
213
|
} = useDealClosure({
|
|
197
214
|
currentDealId,
|
|
215
|
+
dealUpdatedAt: data?.deal.updatedAt ?? null,
|
|
198
216
|
runMutationWithContext,
|
|
199
217
|
confirmDiscardIfDirty,
|
|
200
218
|
onClosed: loadData,
|
|
@@ -365,6 +383,8 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
|
|
|
365
383
|
showAssociationsGroup={false}
|
|
366
384
|
showVersionHistory={false}
|
|
367
385
|
showCancelAction={false}
|
|
386
|
+
injectionSpotId="crud-form:customers.deal"
|
|
387
|
+
optimisticLockUpdatedAt={data.deal.updatedAt}
|
|
368
388
|
onDirtyChange={setIsDirty}
|
|
369
389
|
initialPipelineOptions={formPipelineOptions}
|
|
370
390
|
initialPipelineStageOptions={data.pipelineStages}
|