@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
|
@@ -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 { User, Hash, Users, Building2 } from 'lucide-react'
|
|
7
7
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
8
8
|
import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
|
|
@@ -24,6 +24,7 @@ import { AttachmentsSection, ErrorMessage, LoadingMessage, RecordNotFoundState,
|
|
|
24
24
|
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
25
25
|
import { InjectionSpot, useInjectionWidgets } from '@open-mercato/ui/backend/injection/InjectionSpot'
|
|
26
26
|
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
27
|
+
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
27
28
|
import { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
28
29
|
|
|
29
30
|
import { ActivitiesSection } from '../../../../components/detail/ActivitiesSection'
|
|
@@ -57,6 +58,7 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
57
58
|
const t = useT()
|
|
58
59
|
const router = useRouter()
|
|
59
60
|
const searchParams = useSearchParams()
|
|
61
|
+
const pathname = usePathname()
|
|
60
62
|
const { organizationId } = useOrganizationScopeDetail()
|
|
61
63
|
const isMobile = useIsMobile()
|
|
62
64
|
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
@@ -219,6 +221,20 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
219
221
|
[injectionContext, runMutation],
|
|
220
222
|
)
|
|
221
223
|
|
|
224
|
+
// Publish page-load record context to the AppShell-owned `backend:record:current`
|
|
225
|
+
// mount so the enterprise record_locks widget resolves `customers.person` + id
|
|
226
|
+
// explicitly (the hardcoded path allowlist misses the `people-v2` route).
|
|
227
|
+
// Presence/acquire/heartbeat run on load; the hook clears on unmount/record switch.
|
|
228
|
+
useSetCurrentRecordInjectionContext(
|
|
229
|
+
buildRecordInjectionContext({
|
|
230
|
+
resourceKind: 'customers.person',
|
|
231
|
+
resourceId: currentPersonId,
|
|
232
|
+
updatedAt: data?.person?.updatedAt ?? data?.person?.updated_at ?? null,
|
|
233
|
+
data: data as Record<string, unknown> | null,
|
|
234
|
+
path: pathname,
|
|
235
|
+
}),
|
|
236
|
+
)
|
|
237
|
+
|
|
222
238
|
const handleAddActivity = React.useCallback((kind: ActivityKind) => {
|
|
223
239
|
setScheduleEditData({
|
|
224
240
|
id: '',
|
|
@@ -518,7 +534,7 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
518
534
|
<CrudForm<PersonEditFormValues>
|
|
519
535
|
embedded
|
|
520
536
|
trackDirtyWhenEmbedded
|
|
521
|
-
injectionSpotId="customers.person"
|
|
537
|
+
injectionSpotId="crud-form:customers.person"
|
|
522
538
|
entityIds={[E.customers.customer_entity, E.customers.customer_person_profile]}
|
|
523
539
|
schema={formSchema}
|
|
524
540
|
fields={fields}
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
buildCustomFieldResetMap,
|
|
40
40
|
} from '@open-mercato/shared/lib/commands/customFieldSnapshots'
|
|
41
41
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
42
|
-
import { enforceRecordGoneIsConflict,
|
|
42
|
+
import { enforceRecordGoneIsConflict, enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
43
43
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
44
44
|
import type { CrudIndexerConfig, CrudEventsConfig } from '@open-mercato/shared/lib/crud/types'
|
|
45
45
|
import { recomputeNextInteraction } from '../lib/interactionProjection'
|
|
@@ -644,7 +644,7 @@ const updateInteractionCommand: CommandHandler<InteractionUpdateInput, { interac
|
|
|
644
644
|
// when the client opted into optimistic locking, a stale edit fails with the
|
|
645
645
|
// unified 409 instead of silently overwriting (#2055). Strictly additive —
|
|
646
646
|
// no-op when no expected-version header is present.
|
|
647
|
-
|
|
647
|
+
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
648
648
|
resourceKind: 'customers.interaction',
|
|
649
649
|
resourceId: interaction.id,
|
|
650
650
|
current: interaction.updatedAt,
|
|
@@ -912,7 +912,7 @@ const completeInteractionCommand: CommandHandler<InteractionCompleteInput, { int
|
|
|
912
912
|
ensureTenantScope(ctx, interaction.tenantId)
|
|
913
913
|
ensureOrganizationScope(ctx, interaction.organizationId)
|
|
914
914
|
|
|
915
|
-
|
|
915
|
+
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
916
916
|
resourceKind: 'customers.interaction',
|
|
917
917
|
resourceId: interaction.id,
|
|
918
918
|
current: interaction.updatedAt,
|
|
@@ -1052,7 +1052,7 @@ const cancelInteractionCommand: CommandHandler<InteractionCancelInput, { interac
|
|
|
1052
1052
|
ensureTenantScope(ctx, interaction.tenantId)
|
|
1053
1053
|
ensureOrganizationScope(ctx, interaction.organizationId)
|
|
1054
1054
|
|
|
1055
|
-
|
|
1055
|
+
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
1056
1056
|
resourceKind: 'customers.interaction',
|
|
1057
1057
|
resourceId: interaction.id,
|
|
1058
1058
|
current: interaction.updatedAt,
|
|
@@ -1190,7 +1190,7 @@ const deleteInteractionCommand: CommandHandler<{ body?: Record<string, unknown>;
|
|
|
1190
1190
|
ensureTenantScope(ctx, interaction.tenantId)
|
|
1191
1191
|
ensureOrganizationScope(ctx, interaction.organizationId)
|
|
1192
1192
|
|
|
1193
|
-
|
|
1193
|
+
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
1194
1194
|
resourceKind: 'customers.interaction',
|
|
1195
1195
|
resourceId: interaction.id,
|
|
1196
1196
|
current: interaction.updatedAt,
|
|
@@ -16,6 +16,10 @@ import {
|
|
|
16
16
|
import { ensureOrganizationScope, ensureTenantScope, ensureDictionaryEntry } from './shared'
|
|
17
17
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
18
18
|
import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
|
|
19
|
+
import {
|
|
20
|
+
enforceCommandOptimisticLockWithGuards,
|
|
21
|
+
enforceRecordGoneIsConflict,
|
|
22
|
+
} from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
19
23
|
|
|
20
24
|
const createPipelineStageCommand: CommandHandler<PipelineStageCreateInput, { stageId: string }> = {
|
|
21
25
|
id: 'customers.pipeline-stages.create',
|
|
@@ -106,11 +110,21 @@ const updatePipelineStageCommand: CommandHandler<PipelineStageUpdateInput, void>
|
|
|
106
110
|
...(callerTenantId ? { tenantId: callerTenantId } : {}),
|
|
107
111
|
...(callerOrganizationId ? { organizationId: callerOrganizationId } : {}),
|
|
108
112
|
})
|
|
109
|
-
if (!stage)
|
|
113
|
+
if (!stage) {
|
|
114
|
+
enforceRecordGoneIsConflict({ resourceKind: 'customers.pipelineStage', resourceId: parsed.id, request: ctx.request ?? null })
|
|
115
|
+
throw new CrudHttpError(404, { error: 'Pipeline stage not found' })
|
|
116
|
+
}
|
|
110
117
|
|
|
111
118
|
ensureTenantScope(ctx, stage.tenantId)
|
|
112
119
|
ensureOrganizationScope(ctx, stage.organizationId)
|
|
113
120
|
|
|
121
|
+
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
122
|
+
resourceKind: 'customers.pipelineStage',
|
|
123
|
+
resourceId: stage.id,
|
|
124
|
+
current: stage.updatedAt,
|
|
125
|
+
request: ctx.request ?? null,
|
|
126
|
+
})
|
|
127
|
+
|
|
114
128
|
await withAtomicFlush(em, [
|
|
115
129
|
() => {
|
|
116
130
|
if (parsed.label !== undefined) stage.label = parsed.label
|
|
@@ -148,11 +162,21 @@ const deletePipelineStageCommand: CommandHandler<PipelineStageDeleteInput, void>
|
|
|
148
162
|
...(callerTenantId ? { tenantId: callerTenantId } : {}),
|
|
149
163
|
...(callerOrganizationId ? { organizationId: callerOrganizationId } : {}),
|
|
150
164
|
})
|
|
151
|
-
if (!stage)
|
|
165
|
+
if (!stage) {
|
|
166
|
+
enforceRecordGoneIsConflict({ resourceKind: 'customers.pipelineStage', resourceId: parsed.id, request: ctx.request ?? null })
|
|
167
|
+
throw new CrudHttpError(404, { error: 'Pipeline stage not found' })
|
|
168
|
+
}
|
|
152
169
|
|
|
153
170
|
ensureTenantScope(ctx, stage.tenantId)
|
|
154
171
|
ensureOrganizationScope(ctx, stage.organizationId)
|
|
155
172
|
|
|
173
|
+
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
174
|
+
resourceKind: 'customers.pipelineStage',
|
|
175
|
+
resourceId: stage.id,
|
|
176
|
+
current: stage.updatedAt,
|
|
177
|
+
request: ctx.request ?? null,
|
|
178
|
+
})
|
|
179
|
+
|
|
156
180
|
const activeDealsCount = await em.count(CustomerDeal, {
|
|
157
181
|
pipelineStageId: parsed.id,
|
|
158
182
|
deletedAt: null,
|
|
@@ -12,6 +12,10 @@ import {
|
|
|
12
12
|
} from '../data/validators'
|
|
13
13
|
import { ensureOrganizationScope, ensureTenantScope } from './shared'
|
|
14
14
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
15
|
+
import {
|
|
16
|
+
enforceCommandOptimisticLockWithGuards,
|
|
17
|
+
enforceRecordGoneIsConflict,
|
|
18
|
+
} from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
15
19
|
import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
|
|
16
20
|
|
|
17
21
|
const createPipelineCommand: CommandHandler<PipelineCreateInput, { pipelineId: string }> = {
|
|
@@ -56,11 +60,21 @@ const updatePipelineCommand: CommandHandler<PipelineUpdateInput, void> = {
|
|
|
56
60
|
|
|
57
61
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
58
62
|
const pipeline = await em.findOne(CustomerPipeline, { id: parsed.id })
|
|
59
|
-
if (!pipeline)
|
|
63
|
+
if (!pipeline) {
|
|
64
|
+
enforceRecordGoneIsConflict({ resourceKind: 'customers.pipeline', resourceId: parsed.id, request: ctx.request ?? null })
|
|
65
|
+
throw new CrudHttpError(404, { error: 'Pipeline not found' })
|
|
66
|
+
}
|
|
60
67
|
|
|
61
68
|
ensureTenantScope(ctx, pipeline.tenantId)
|
|
62
69
|
ensureOrganizationScope(ctx, pipeline.organizationId)
|
|
63
70
|
|
|
71
|
+
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
72
|
+
resourceKind: 'customers.pipeline',
|
|
73
|
+
resourceId: pipeline.id,
|
|
74
|
+
current: pipeline.updatedAt,
|
|
75
|
+
request: ctx.request ?? null,
|
|
76
|
+
})
|
|
77
|
+
|
|
64
78
|
await withAtomicFlush(em, [
|
|
65
79
|
async () => {
|
|
66
80
|
if (parsed.isDefault && !pipeline.isDefault) {
|
|
@@ -86,11 +100,21 @@ const deletePipelineCommand: CommandHandler<PipelineDeleteInput, void> = {
|
|
|
86
100
|
|
|
87
101
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
88
102
|
const pipeline = await em.findOne(CustomerPipeline, { id: parsed.id })
|
|
89
|
-
if (!pipeline)
|
|
103
|
+
if (!pipeline) {
|
|
104
|
+
enforceRecordGoneIsConflict({ resourceKind: 'customers.pipeline', resourceId: parsed.id, request: ctx.request ?? null })
|
|
105
|
+
throw new CrudHttpError(404, { error: 'Pipeline not found' })
|
|
106
|
+
}
|
|
90
107
|
|
|
91
108
|
ensureTenantScope(ctx, pipeline.tenantId)
|
|
92
109
|
ensureOrganizationScope(ctx, pipeline.organizationId)
|
|
93
110
|
|
|
111
|
+
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
112
|
+
resourceKind: 'customers.pipeline',
|
|
113
|
+
resourceId: pipeline.id,
|
|
114
|
+
current: pipeline.updatedAt,
|
|
115
|
+
request: ctx.request ?? null,
|
|
116
|
+
})
|
|
117
|
+
|
|
94
118
|
const activeDealsCount = await em.count(CustomerDeal, {
|
|
95
119
|
pipelineId: parsed.id,
|
|
96
120
|
deletedAt: null,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
import { apiCall, apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
4
|
+
import { apiCall, apiCallOrThrow, readApiResultOrThrow, 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'
|
|
5
7
|
import { AddressesSection as SharedAddressesSection } from '@open-mercato/ui/backend/detail'
|
|
6
8
|
import type { AddressDataAdapter, AddressTypesAdapter, AddressFormatStrategy, SectionAction } from '@open-mercato/ui/backend/detail'
|
|
7
9
|
import { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
@@ -99,6 +101,12 @@ export function AddressesSection({
|
|
|
99
101
|
: typeof record.isPrimary === 'boolean'
|
|
100
102
|
? record.isPrimary
|
|
101
103
|
: false,
|
|
104
|
+
updatedAt:
|
|
105
|
+
typeof record.updated_at === 'string'
|
|
106
|
+
? record.updated_at
|
|
107
|
+
: typeof record.updatedAt === 'string'
|
|
108
|
+
? record.updatedAt
|
|
109
|
+
: null,
|
|
102
110
|
}
|
|
103
111
|
})
|
|
104
112
|
.filter((value): value is NonNullable<typeof value> => value !== null)
|
|
@@ -107,7 +115,7 @@ export function AddressesSection({
|
|
|
107
115
|
const response = await apiCallOrThrow<Record<string, unknown>>(
|
|
108
116
|
'/api/customers/addresses',
|
|
109
117
|
{
|
|
110
|
-
// optimistic-lock-exempt: address
|
|
118
|
+
// optimistic-lock-exempt: address create-only (no prior version to compare)
|
|
111
119
|
method: 'POST',
|
|
112
120
|
headers: { 'content-type': 'application/json' },
|
|
113
121
|
body: JSON.stringify({
|
|
@@ -120,33 +128,47 @@ export function AddressesSection({
|
|
|
120
128
|
)
|
|
121
129
|
return response.result ?? {}
|
|
122
130
|
},
|
|
123
|
-
update: async ({ id, payload }) => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
131
|
+
update: async ({ id, payload, updatedAt }) => {
|
|
132
|
+
try {
|
|
133
|
+
await withScopedApiRequestHeaders(
|
|
134
|
+
buildOptimisticLockHeader(updatedAt ?? null),
|
|
135
|
+
() => apiCallOrThrow(
|
|
136
|
+
'/api/customers/addresses',
|
|
137
|
+
{
|
|
138
|
+
method: 'PUT',
|
|
139
|
+
headers: { 'content-type': 'application/json' },
|
|
140
|
+
body: JSON.stringify({
|
|
141
|
+
id,
|
|
142
|
+
...payload,
|
|
143
|
+
country: payload.country ? payload.country.toUpperCase() : undefined,
|
|
144
|
+
}),
|
|
145
|
+
},
|
|
146
|
+
{ errorMessage: t('customers.people.detail.addresses.error') },
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
} catch (err) {
|
|
150
|
+
surfaceRecordConflict(err, t)
|
|
151
|
+
throw err
|
|
152
|
+
}
|
|
138
153
|
},
|
|
139
|
-
delete: async ({ id }) => {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
154
|
+
delete: async ({ id, updatedAt }) => {
|
|
155
|
+
try {
|
|
156
|
+
await withScopedApiRequestHeaders(
|
|
157
|
+
buildOptimisticLockHeader(updatedAt ?? null),
|
|
158
|
+
() => apiCallOrThrow(
|
|
159
|
+
'/api/customers/addresses',
|
|
160
|
+
{
|
|
161
|
+
method: 'DELETE',
|
|
162
|
+
headers: { 'content-type': 'application/json' },
|
|
163
|
+
body: JSON.stringify({ id }),
|
|
164
|
+
},
|
|
165
|
+
{ errorMessage: t('customers.people.detail.addresses.error') },
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
} catch (err) {
|
|
169
|
+
surfaceRecordConflict(err, t)
|
|
170
|
+
throw err
|
|
171
|
+
}
|
|
150
172
|
},
|
|
151
173
|
}), [t])
|
|
152
174
|
|
|
@@ -69,6 +69,14 @@ export type DealFormProps = {
|
|
|
69
69
|
showCancelAction?: boolean
|
|
70
70
|
initialPipelineOptions?: PipelineOption[]
|
|
71
71
|
initialPipelineStageOptions?: PipelineStageOption[]
|
|
72
|
+
/**
|
|
73
|
+
* Injection spot id for the form-scoped record_locks widget (e.g.
|
|
74
|
+
* `customers.deal`). Mirrors how people-v2/companies-v2 mount their save-time
|
|
75
|
+
* `crud-form:*` widget so a deal save conflict surfaces the merge dialog.
|
|
76
|
+
*/
|
|
77
|
+
injectionSpotId?: string
|
|
78
|
+
/** Optimistic-lock version (`deal.updatedAt`) for the embedded CrudForm. */
|
|
79
|
+
optimisticLockUpdatedAt?: string | null
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
type EntityOption = {
|
|
@@ -732,6 +740,8 @@ export function DealForm({
|
|
|
732
740
|
showCancelAction = true,
|
|
733
741
|
initialPipelineOptions,
|
|
734
742
|
initialPipelineStageOptions,
|
|
743
|
+
injectionSpotId,
|
|
744
|
+
optimisticLockUpdatedAt,
|
|
735
745
|
}: DealFormProps) {
|
|
736
746
|
const t = useT()
|
|
737
747
|
const [pending, setPending] = React.useState(false)
|
|
@@ -1168,6 +1178,8 @@ export function DealForm({
|
|
|
1168
1178
|
backHref={backHref}
|
|
1169
1179
|
hideFooterActions={hideFooterActions}
|
|
1170
1180
|
onDirtyChange={onDirtyChange}
|
|
1181
|
+
injectionSpotId={injectionSpotId}
|
|
1182
|
+
optimisticLockUpdatedAt={optimisticLockUpdatedAt}
|
|
1171
1183
|
collapsibleGroups={collapsibleGroups}
|
|
1172
1184
|
sortableGroups={sortableGroups}
|
|
1173
1185
|
versionHistory={showVersionHistory && mode === 'edit' && initialValues?.id
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
3
|
+
import { apiCallOrThrow, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
4
|
+
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
4
5
|
import { mapCommentSummary, type NotesDataAdapter } from '@open-mercato/ui/backend/detail/NotesSection'
|
|
5
6
|
|
|
6
7
|
type Translator = (key: string, fallback?: string, params?: Record<string, string | number>) => string
|
|
@@ -92,33 +93,43 @@ export function createCustomerNotesAdapter(
|
|
|
92
93
|
)
|
|
93
94
|
return response.result ?? {}
|
|
94
95
|
},
|
|
95
|
-
update: async ({ id, patch }) => {
|
|
96
|
+
update: async ({ id, patch, updatedAt }) => {
|
|
96
97
|
const payload: Record<string, unknown> = { id }
|
|
97
98
|
if (patch.body !== undefined) payload.body = patch.body
|
|
98
99
|
if (patch.appearanceIcon !== undefined) payload.appearanceIcon = patch.appearanceIcon
|
|
99
100
|
if (patch.appearanceColor !== undefined) payload.appearanceColor = patch.appearanceColor
|
|
101
|
+
// Send the optimistic-lock header (note's loaded updatedAt) so a stale edit —
|
|
102
|
+
// or an edit after the note was deleted in another tab — surfaces the unified
|
|
103
|
+
// conflict bar (409) instead of silently overwriting. The guarded runner
|
|
104
|
+
// (useGuardedMutation) routes the 409 through surfaceRecordConflict.
|
|
100
105
|
await runWrite(
|
|
101
|
-
() =>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
() => withScopedApiRequestHeaders(
|
|
107
|
+
buildOptimisticLockHeader(updatedAt ?? null),
|
|
108
|
+
() => apiCallOrThrow(
|
|
109
|
+
'/api/customers/comments',
|
|
110
|
+
{
|
|
111
|
+
method: 'PUT',
|
|
112
|
+
headers: { 'content-type': 'application/json' },
|
|
113
|
+
body: JSON.stringify(payload),
|
|
114
|
+
},
|
|
115
|
+
{ errorMessage: translator('customers.people.detail.notes.updateError') },
|
|
116
|
+
),
|
|
109
117
|
),
|
|
110
118
|
{ operation: 'updateNote', id },
|
|
111
119
|
)
|
|
112
120
|
},
|
|
113
|
-
delete: async ({ id }) => {
|
|
121
|
+
delete: async ({ id, updatedAt }) => {
|
|
114
122
|
await runWrite(
|
|
115
|
-
() =>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
() => withScopedApiRequestHeaders(
|
|
124
|
+
buildOptimisticLockHeader(updatedAt ?? null),
|
|
125
|
+
() => apiCallOrThrow(
|
|
126
|
+
`/api/customers/comments?id=${encodeURIComponent(id)}`,
|
|
127
|
+
{
|
|
128
|
+
method: 'DELETE',
|
|
129
|
+
headers: { 'content-type': 'application/json' },
|
|
130
|
+
},
|
|
131
|
+
{ errorMessage: translator('customers.people.detail.notes.deleteError', 'Failed to delete note') },
|
|
132
|
+
),
|
|
122
133
|
),
|
|
123
134
|
{ operation: 'deleteNote', id },
|
|
124
135
|
)
|
|
@@ -923,6 +923,8 @@
|
|
|
923
923
|
"customers.deals.detail.badge.deal": "Deal",
|
|
924
924
|
"customers.deals.detail.badge.lost": "Lost",
|
|
925
925
|
"customers.deals.detail.badge.won": "Won",
|
|
926
|
+
"customers.deals.detail.closeLostError": "Deal konnte nicht als verloren markiert werden.",
|
|
927
|
+
"customers.deals.detail.closeWonError": "Deal konnte nicht als gewonnen markiert werden.",
|
|
926
928
|
"customers.deals.detail.closure.lost": "Lost",
|
|
927
929
|
"customers.deals.detail.closure.prompt": "Close deal — choose outcome",
|
|
928
930
|
"customers.deals.detail.closure.won": "Won",
|
|
@@ -923,6 +923,8 @@
|
|
|
923
923
|
"customers.deals.detail.badge.deal": "Deal",
|
|
924
924
|
"customers.deals.detail.badge.lost": "Lost",
|
|
925
925
|
"customers.deals.detail.badge.won": "Won",
|
|
926
|
+
"customers.deals.detail.closeLostError": "Failed to mark deal as lost.",
|
|
927
|
+
"customers.deals.detail.closeWonError": "Failed to mark deal as won.",
|
|
926
928
|
"customers.deals.detail.closure.lost": "Lost",
|
|
927
929
|
"customers.deals.detail.closure.prompt": "Close deal — choose outcome",
|
|
928
930
|
"customers.deals.detail.closure.won": "Won",
|
|
@@ -923,6 +923,8 @@
|
|
|
923
923
|
"customers.deals.detail.badge.deal": "Deal",
|
|
924
924
|
"customers.deals.detail.badge.lost": "Lost",
|
|
925
925
|
"customers.deals.detail.badge.won": "Won",
|
|
926
|
+
"customers.deals.detail.closeLostError": "No se pudo marcar el deal como perdido.",
|
|
927
|
+
"customers.deals.detail.closeWonError": "No se pudo marcar el deal como ganado.",
|
|
926
928
|
"customers.deals.detail.closure.lost": "Lost",
|
|
927
929
|
"customers.deals.detail.closure.prompt": "Close deal — choose outcome",
|
|
928
930
|
"customers.deals.detail.closure.won": "Won",
|
|
@@ -923,6 +923,8 @@
|
|
|
923
923
|
"customers.deals.detail.badge.deal": "Deal",
|
|
924
924
|
"customers.deals.detail.badge.lost": "Lost",
|
|
925
925
|
"customers.deals.detail.badge.won": "Won",
|
|
926
|
+
"customers.deals.detail.closeLostError": "Nie udało się oznaczyć deala jako przegranego.",
|
|
927
|
+
"customers.deals.detail.closeWonError": "Nie udało się oznaczyć deala jako wygranego.",
|
|
926
928
|
"customers.deals.detail.closure.lost": "Lost",
|
|
927
929
|
"customers.deals.detail.closure.prompt": "Close deal — choose outcome",
|
|
928
930
|
"customers.deals.detail.closure.won": "Won",
|
|
@@ -3,6 +3,8 @@ import { z } from 'zod'
|
|
|
3
3
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
4
4
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
5
|
import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
|
|
6
|
+
import { readOptimisticLockExpected } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
7
|
+
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
6
8
|
import { updateSyncScheduleSchema } from '../../../data/validators'
|
|
7
9
|
import type { SyncScheduleService } from '../../../lib/sync-schedule-service'
|
|
8
10
|
import { serializeSchedule } from '../serialize'
|
|
@@ -111,7 +113,8 @@ export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
111
113
|
timezone: parsed.data.timezone ?? current.timezone,
|
|
112
114
|
fullSync: parsed.data.fullSync ?? current.fullSync,
|
|
113
115
|
isEnabled: parsed.data.isEnabled ?? current.isEnabled,
|
|
114
|
-
|
|
116
|
+
expectedUpdatedAt: readOptimisticLockExpected(req),
|
|
117
|
+
}, scope, container)
|
|
115
118
|
|
|
116
119
|
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
117
120
|
await runCrudMutationGuardAfterSuccess(container, {
|
|
@@ -126,9 +129,11 @@ export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
126
129
|
metadata: guardResult.metadata ?? null,
|
|
127
130
|
})
|
|
128
131
|
}
|
|
129
|
-
|
|
130
132
|
return NextResponse.json(serializeSchedule(schedule))
|
|
131
133
|
} catch (error) {
|
|
134
|
+
if (isCrudHttpError(error)) {
|
|
135
|
+
return NextResponse.json(error.body, { status: error.status })
|
|
136
|
+
}
|
|
132
137
|
const message = error instanceof Error ? error.message : 'Failed to update sync schedule'
|
|
133
138
|
return NextResponse.json({ error: message }, { status: 422 })
|
|
134
139
|
}
|
|
@@ -168,25 +173,38 @@ export async function DELETE(req: Request, ctx: { params?: Promise<{ id?: string
|
|
|
168
173
|
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
169
174
|
}
|
|
170
175
|
|
|
171
|
-
|
|
176
|
+
try {
|
|
177
|
+
const deleted = await scheduleService.deleteSchedule(
|
|
178
|
+
parsedParams.data.id,
|
|
179
|
+
scope,
|
|
180
|
+
container,
|
|
181
|
+
readOptimisticLockExpected(req),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if (!deleted) {
|
|
185
|
+
return NextResponse.json({ error: 'Schedule not found' }, { status: 404 })
|
|
186
|
+
}
|
|
172
187
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
188
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
189
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
190
|
+
tenantId: auth.tenantId,
|
|
191
|
+
organizationId: scope.organizationId,
|
|
192
|
+
userId: auth.sub,
|
|
193
|
+
resourceKind: 'data_sync.schedule',
|
|
194
|
+
resourceId: parsedParams.data.id,
|
|
195
|
+
operation: 'delete',
|
|
196
|
+
requestMethod: req.method,
|
|
197
|
+
requestHeaders: req.headers,
|
|
198
|
+
metadata: guardResult.metadata ?? null,
|
|
199
|
+
})
|
|
200
|
+
}
|
|
176
201
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
operation: 'delete',
|
|
185
|
-
requestMethod: req.method,
|
|
186
|
-
requestHeaders: req.headers,
|
|
187
|
-
metadata: guardResult.metadata ?? null,
|
|
188
|
-
})
|
|
202
|
+
return NextResponse.json({ deleted: true })
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (isCrudHttpError(error)) {
|
|
205
|
+
return NextResponse.json(error.body, { status: error.status })
|
|
206
|
+
}
|
|
207
|
+
const message = error instanceof Error ? error.message : 'Failed to delete sync schedule'
|
|
208
|
+
return NextResponse.json({ error: message }, { status: 422 })
|
|
189
209
|
}
|
|
190
|
-
|
|
191
|
-
return NextResponse.json({ deleted: true })
|
|
192
210
|
}
|
|
@@ -90,7 +90,7 @@ export async function POST(req: Request) {
|
|
|
90
90
|
const schedule = await scheduleService.saveSchedule({
|
|
91
91
|
...parsed.data,
|
|
92
92
|
expectedUpdatedAt: readOptimisticLockExpected(req),
|
|
93
|
-
}, scope)
|
|
93
|
+
}, scope, container)
|
|
94
94
|
|
|
95
95
|
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
96
96
|
await runCrudMutationGuardAfterSuccess(container, {
|
|
@@ -26,6 +26,7 @@ import { Switch } from '@open-mercato/ui/primitives/switch'
|
|
|
26
26
|
import { RowActions } from '@open-mercato/ui/backend/RowActions'
|
|
27
27
|
import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
28
28
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
29
|
+
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
29
30
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
30
31
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
31
32
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -402,21 +403,27 @@ export default function SyncRunsDashboardPage() {
|
|
|
402
403
|
setIsSavingSchedule(true)
|
|
403
404
|
try {
|
|
404
405
|
const call = await runMutation({
|
|
405
|
-
//
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
406
|
+
// Keyed upsert (POST). When the editor holds an existing schedule's
|
|
407
|
+
// `updatedAt`, the lock header version-checks the resolved row on the
|
|
408
|
+
// server; a brand-new schedule has a null `updatedAt`, so the header is
|
|
409
|
+
// empty and the create path stays unaffected.
|
|
410
|
+
operation: () => withScopedApiRequestHeaders(
|
|
411
|
+
buildOptimisticLockHeader(scheduleEditor.updatedAt),
|
|
412
|
+
() => apiCall<SyncScheduleRecord>('/api/data_sync/schedules', {
|
|
413
|
+
method: 'POST',
|
|
414
|
+
headers: { 'Content-Type': 'application/json' },
|
|
415
|
+
body: JSON.stringify({
|
|
416
|
+
integrationId: selectedIntegration.integrationId,
|
|
417
|
+
entityType: selectedEntityType,
|
|
418
|
+
direction: selectedDirection,
|
|
419
|
+
scheduleType: scheduleEditor.scheduleType,
|
|
420
|
+
scheduleValue: scheduleEditor.scheduleValue.trim(),
|
|
421
|
+
timezone: scheduleEditor.timezone.trim() || DEFAULT_TIMEZONE,
|
|
422
|
+
fullSync: scheduleEditor.fullSync,
|
|
423
|
+
isEnabled: scheduleEditor.isEnabled,
|
|
424
|
+
}),
|
|
425
|
+
}, { fallback: null }),
|
|
426
|
+
),
|
|
420
427
|
mutationPayload: {
|
|
421
428
|
integrationId: selectedIntegration.integrationId,
|
|
422
429
|
entityType: selectedEntityType,
|
|
@@ -435,6 +442,16 @@ export default function SyncRunsDashboardPage() {
|
|
|
435
442
|
})
|
|
436
443
|
|
|
437
444
|
if (!call.ok || !call.result) {
|
|
445
|
+
const conflictError = Object.assign(
|
|
446
|
+
new Error((call.result as { error?: string } | null)?.error ?? t('data_sync.dashboard.schedule.error', 'Failed to save recurring schedule')),
|
|
447
|
+
{
|
|
448
|
+
status: call.status,
|
|
449
|
+
...(call.result && typeof call.result === 'object' ? call.result : {}),
|
|
450
|
+
},
|
|
451
|
+
)
|
|
452
|
+
if (surfaceRecordConflict(conflictError, t)) {
|
|
453
|
+
return
|
|
454
|
+
}
|
|
438
455
|
flash((call.result as { error?: string } | null)?.error ?? t('data_sync.dashboard.schedule.error', 'Failed to save recurring schedule'), 'error')
|
|
439
456
|
return
|
|
440
457
|
}
|