@open-mercato/core 0.6.6-develop.6401.1.8c07df34a7 → 0.6.6-develop.6403.1.b57089b6fe
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/customers/backend/customers/deals/[id]/hooks/useDealClosure.js +9 -12
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js.map +2 -2
- package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js +49 -10
- package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js.map +2 -2
- package/dist/modules/planner/api/availability-weekly.js +11 -1
- package/dist/modules/planner/api/availability-weekly.js.map +2 -2
- package/dist/modules/planner/commands/availability-weekly.js +34 -3
- package/dist/modules/planner/commands/availability-weekly.js.map +2 -2
- package/dist/modules/planner/components/AvailabilityRulesEditor.js +14 -6
- package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
- package/dist/modules/sales/cli.js +38 -1
- package/dist/modules/sales/cli.js.map +2 -2
- package/dist/modules/sales/lib/dictionaries.js +70 -0
- package/dist/modules/sales/lib/dictionaries.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.ts +9 -13
- package/src/modules/customers/components/detail/ConfirmDealLostDialog.tsx +58 -8
- package/src/modules/customers/i18n/de.json +4 -0
- package/src/modules/customers/i18n/en.json +4 -0
- package/src/modules/customers/i18n/es.json +4 -0
- package/src/modules/customers/i18n/pl.json +4 -0
- package/src/modules/planner/api/availability-weekly.ts +10 -0
- package/src/modules/planner/commands/availability-weekly.ts +46 -8
- package/src/modules/planner/components/AvailabilityRulesEditor.tsx +15 -6
- package/src/modules/sales/cli.ts +39 -1
- package/src/modules/sales/lib/dictionaries.ts +90 -0
|
@@ -8,14 +8,24 @@ import {
|
|
|
8
8
|
type PlannerAvailabilityWeeklyReplaceInput,
|
|
9
9
|
} from '../data/validators'
|
|
10
10
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
11
|
-
import
|
|
11
|
+
import { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'
|
|
12
12
|
import {
|
|
13
13
|
enforceCommandOptimisticLock,
|
|
14
14
|
enforceRecordGoneIsConflict,
|
|
15
15
|
} from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
16
|
+
import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
|
|
17
|
+
import type { CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'
|
|
18
|
+
import type { PlannerAvailabilityKind, PlannerAvailabilitySubjectType } from '../data/entities'
|
|
16
19
|
import { ensureOrganizationScope, ensureTenantScope, extractUndoPayload } from './shared'
|
|
20
|
+
import { plannerAvailabilityRuleSetCrudEvents } from '../lib/crud'
|
|
21
|
+
import { E } from '#generated/entities.ids.generated'
|
|
17
22
|
|
|
18
23
|
const AVAILABILITY_RULE_RESOURCE_KIND = 'planner.availability.rule'
|
|
24
|
+
const AVAILABILITY_RULE_SET_CACHE_RESOURCE_KIND = 'planner.availability-rule-set'
|
|
25
|
+
|
|
26
|
+
const availabilityRuleSetCrudIndexer: CrudIndexerConfig<PlannerAvailabilityRuleSet> = {
|
|
27
|
+
entityType: E.planner.planner_availability_rule_set,
|
|
28
|
+
}
|
|
19
29
|
|
|
20
30
|
// Canonical resource kind for the parent rule set, matching the tag the CRUD
|
|
21
31
|
// factory derives for `planner.availability-rule-sets.*` commands. Weekly
|
|
@@ -95,6 +105,14 @@ function toAvailabilityRuleSnapshot(record: PlannerAvailabilityRule): Availabili
|
|
|
95
105
|
}
|
|
96
106
|
}
|
|
97
107
|
|
|
108
|
+
function nextRuleSetUpdatedAt(current: Date | null | undefined, fallback: Date): Date {
|
|
109
|
+
const currentMs = current instanceof Date ? current.getTime() : Number.NaN
|
|
110
|
+
if (Number.isFinite(currentMs) && fallback.getTime() <= currentMs) {
|
|
111
|
+
return new Date(currentMs + 1)
|
|
112
|
+
}
|
|
113
|
+
return fallback
|
|
114
|
+
}
|
|
115
|
+
|
|
98
116
|
async function loadWeeklySnapshots(
|
|
99
117
|
em: EntityManager,
|
|
100
118
|
params: {
|
|
@@ -173,12 +191,12 @@ const replaceWeeklyAvailabilityCommand: CommandHandler<PlannerAvailabilityWeekly
|
|
|
173
191
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
174
192
|
const now = new Date()
|
|
175
193
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
194
|
+
// The weekly rules of a rule set are a sub-resource of that rule set: the
|
|
195
|
+
// parent is the optimistic-lock consistency boundary. Guard the parent's
|
|
196
|
+
// version (so a stale weekly save loses to a concurrent rule-set
|
|
197
|
+
// change/delete) and bump its `updated_at` after the replace (so a
|
|
198
|
+
// concurrent rule-set delete/update with a stale token conflicts). See #2927.
|
|
199
|
+
const touchedRuleSet = await em.transactional(async (trx): Promise<PlannerAvailabilityRuleSet | null> => {
|
|
182
200
|
let ruleSet: PlannerAvailabilityRuleSet | null = null
|
|
183
201
|
if (parsed.subjectType === 'ruleset') {
|
|
184
202
|
ruleSet = await trx.findOne(PlannerAvailabilityRuleSet, {
|
|
@@ -250,13 +268,30 @@ const replaceWeeklyAvailabilityCommand: CommandHandler<PlannerAvailabilityWeekly
|
|
|
250
268
|
})
|
|
251
269
|
|
|
252
270
|
if (ruleSet) {
|
|
253
|
-
ruleSet.updatedAt = now
|
|
271
|
+
ruleSet.updatedAt = nextRuleSetUpdatedAt(ruleSet.updatedAt, now)
|
|
254
272
|
trx.persist(ruleSet)
|
|
255
273
|
}
|
|
256
274
|
|
|
257
275
|
await trx.flush()
|
|
276
|
+
return ruleSet
|
|
258
277
|
})
|
|
259
278
|
|
|
279
|
+
if (touchedRuleSet) {
|
|
280
|
+
const dataEngine = ctx.container.resolve('dataEngine') as DataEngine
|
|
281
|
+
await emitCrudSideEffects({
|
|
282
|
+
dataEngine,
|
|
283
|
+
action: 'updated',
|
|
284
|
+
entity: touchedRuleSet,
|
|
285
|
+
identifiers: {
|
|
286
|
+
id: touchedRuleSet.id,
|
|
287
|
+
organizationId: touchedRuleSet.organizationId,
|
|
288
|
+
tenantId: touchedRuleSet.tenantId,
|
|
289
|
+
},
|
|
290
|
+
events: plannerAvailabilityRuleSetCrudEvents,
|
|
291
|
+
indexer: availabilityRuleSetCrudIndexer,
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
|
|
260
295
|
return { ok: true }
|
|
261
296
|
},
|
|
262
297
|
buildLog: async ({ input, snapshots, ctx }) => {
|
|
@@ -284,6 +319,9 @@ const replaceWeeklyAvailabilityCommand: CommandHandler<PlannerAvailabilityWeekly
|
|
|
284
319
|
after,
|
|
285
320
|
} satisfies WeeklyUndoPayload,
|
|
286
321
|
},
|
|
322
|
+
context: parsed.subjectType === 'ruleset'
|
|
323
|
+
? { cacheAliases: [AVAILABILITY_RULE_SET_CACHE_RESOURCE_KIND] }
|
|
324
|
+
: null,
|
|
287
325
|
}
|
|
288
326
|
},
|
|
289
327
|
undo: async ({ logEntry, ctx }) => {
|
|
@@ -19,6 +19,7 @@ import { createCrud, deleteCrud, updateCrud } from '@open-mercato/ui/backend/uti
|
|
|
19
19
|
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
20
20
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
21
21
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
22
|
+
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
22
23
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
23
24
|
import { ComboboxInput, TimePicker } from '@open-mercato/ui/backend/inputs'
|
|
24
25
|
import { DictionaryEntrySelect, type DictionarySelectLabels } from '@open-mercato/core/modules/dictionaries/components/DictionaryEntrySelect'
|
|
@@ -30,7 +31,6 @@ import {
|
|
|
30
31
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
31
32
|
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
32
33
|
import { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
33
|
-
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
34
34
|
import { parseAvailabilityRuleWindow } from '@open-mercato/core/modules/planner/lib/availabilitySchedule'
|
|
35
35
|
import { deleteAvailabilityRuleSet } from '@open-mercato/core/modules/planner/lib/deleteAvailabilityRuleSet'
|
|
36
36
|
import { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'
|
|
@@ -778,6 +778,12 @@ export function AvailabilityRulesEditor({
|
|
|
778
778
|
void refreshBookedEvents()
|
|
779
779
|
}, [refreshBookedEvents])
|
|
780
780
|
|
|
781
|
+
const refreshAfterRuleSetConflict = React.useCallback(() => {
|
|
782
|
+
void refreshRuleSets()
|
|
783
|
+
void refreshRuleSetRules()
|
|
784
|
+
void refreshAvailability()
|
|
785
|
+
}, [refreshAvailability, refreshRuleSetRules, refreshRuleSets])
|
|
786
|
+
|
|
781
787
|
const weeklyDraft = React.useMemo(() => buildWeeklyDraft(activeRules), [activeRules])
|
|
782
788
|
const [weeklyWindows, setWeeklyWindows] = React.useState<WeeklyWindows>(() => cloneWeeklyWindows(weeklyDraft))
|
|
783
789
|
const weeklyWindowsRef = React.useRef<WeeklyWindows>(cloneWeeklyWindows(weeklyDraft))
|
|
@@ -893,14 +899,15 @@ export function AvailabilityRulesEditor({
|
|
|
893
899
|
if (!shouldSkipRefresh) {
|
|
894
900
|
await refreshAvailability()
|
|
895
901
|
await refreshRuleSetRules()
|
|
902
|
+
if (usingRuleSet) {
|
|
903
|
+
await refreshRuleSets()
|
|
904
|
+
}
|
|
896
905
|
}
|
|
897
906
|
if (parentRuleSet) {
|
|
898
907
|
await refreshRuleSets()
|
|
899
908
|
}
|
|
900
909
|
} catch (error) {
|
|
901
|
-
if (surfaceRecordConflict(error, t))
|
|
902
|
-
return
|
|
903
|
-
}
|
|
910
|
+
if (surfaceRecordConflict(error, t, { onRefresh: refreshAfterRuleSetConflict })) return
|
|
904
911
|
const message = error instanceof Error ? error.message : listLabels.saveWeeklyError
|
|
905
912
|
flash(message, 'error')
|
|
906
913
|
} finally {
|
|
@@ -912,8 +919,9 @@ export function AvailabilityRulesEditor({
|
|
|
912
919
|
refreshAvailability,
|
|
913
920
|
refreshRuleSetRules,
|
|
914
921
|
refreshRuleSets,
|
|
915
|
-
|
|
922
|
+
refreshAfterRuleSetConflict,
|
|
916
923
|
effectiveRulesetId,
|
|
924
|
+
ruleSets,
|
|
917
925
|
subjectId,
|
|
918
926
|
subjectType,
|
|
919
927
|
t,
|
|
@@ -1180,7 +1188,7 @@ export function AvailabilityRulesEditor({
|
|
|
1180
1188
|
refreshRuleSets,
|
|
1181
1189
|
onSuccess: () => flash(listLabels.ruleSetDeleteSuccess, 'success'),
|
|
1182
1190
|
onError: (error) => {
|
|
1183
|
-
if (surfaceRecordConflict(error, t)) return
|
|
1191
|
+
if (surfaceRecordConflict(error, t, { onRefresh: refreshAfterRuleSetConflict })) return
|
|
1184
1192
|
console.error('planner.availability-rule-sets.delete', error)
|
|
1185
1193
|
const normalized = normalizeCrudServerError(error)
|
|
1186
1194
|
flash(normalized.message ?? listLabels.ruleSetDeleteError, 'error')
|
|
@@ -1195,6 +1203,7 @@ export function AvailabilityRulesEditor({
|
|
|
1195
1203
|
listLabels.ruleSetDeleteSuccess,
|
|
1196
1204
|
onRulesetChange,
|
|
1197
1205
|
refreshAvailability,
|
|
1206
|
+
refreshAfterRuleSetConflict,
|
|
1198
1207
|
refreshRuleSets,
|
|
1199
1208
|
ruleSets,
|
|
1200
1209
|
rulesetId,
|
package/src/modules/sales/cli.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ModuleCli } from '@open-mercato/shared/modules/registry'
|
|
2
2
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
3
3
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
4
|
-
import { seedSalesAdjustmentKinds, seedSalesStatusDictionaries } from './lib/dictionaries'
|
|
4
|
+
import { backfillDealLossReasonDictionary, seedSalesAdjustmentKinds, seedSalesStatusDictionaries } from './lib/dictionaries'
|
|
5
5
|
import { seedSalesTaxRates } from './lib/seeds'
|
|
6
6
|
import { ensureExamplePaymentMethods, ensureExampleShippingMethods } from './seed/examples-data'
|
|
7
7
|
import { seedSalesExamples } from './seed/examples'
|
|
@@ -103,6 +103,43 @@ const seedAdjustmentKindsCommand: ModuleCli = {
|
|
|
103
103
|
},
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
const backfillDealLossReasonsCommand: ModuleCli = {
|
|
107
|
+
command: 'backfill-deal-loss-reasons',
|
|
108
|
+
async run(rest) {
|
|
109
|
+
const args = parseArgs(rest)
|
|
110
|
+
const tenantId = String(args.tenantId ?? args.tenant ?? '')
|
|
111
|
+
const organizationId = String(args.organizationId ?? args.org ?? args.orgId ?? '')
|
|
112
|
+
if (!tenantId || !organizationId) {
|
|
113
|
+
console.error('Usage: mercato sales backfill-deal-loss-reasons --tenant <tenantId> --org <organizationId>')
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
const container = await createRequestContainer()
|
|
117
|
+
try {
|
|
118
|
+
const em = container.resolve<EntityManager>('em')
|
|
119
|
+
const result = await em.transactional(async (tem) => {
|
|
120
|
+
const backfillResult = await backfillDealLossReasonDictionary(tem, { tenantId, organizationId })
|
|
121
|
+
await tem.flush()
|
|
122
|
+
return backfillResult
|
|
123
|
+
})
|
|
124
|
+
console.log(
|
|
125
|
+
[
|
|
126
|
+
`Deal loss reasons dictionary ready for organization ${organizationId}.`,
|
|
127
|
+
`dictionaryId=${result.dictionaryId}`,
|
|
128
|
+
`createdDictionary=${String(result.createdDictionary)}`,
|
|
129
|
+
`copiedLegacyEntries=${String(result.copiedLegacyEntries)}`,
|
|
130
|
+
`seededDefaultEntries=${String(result.seededDefaultEntries)}`,
|
|
131
|
+
`existingEntries=${String(result.existingEntries)}`,
|
|
132
|
+
].join(' '),
|
|
133
|
+
)
|
|
134
|
+
} finally {
|
|
135
|
+
const disposable = container as unknown as { dispose?: () => Promise<void> }
|
|
136
|
+
if (typeof disposable.dispose === 'function') {
|
|
137
|
+
await disposable.dispose()
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
|
|
106
143
|
const seedShippingMethodsCommand: ModuleCli = {
|
|
107
144
|
command: 'seed-shipping-methods',
|
|
108
145
|
async run(rest) {
|
|
@@ -189,6 +226,7 @@ export default [
|
|
|
189
226
|
seedTaxRatesCommand,
|
|
190
227
|
seedStatusesCommand,
|
|
191
228
|
seedAdjustmentKindsCommand,
|
|
229
|
+
backfillDealLossReasonsCommand,
|
|
192
230
|
seedShippingMethodsCommand,
|
|
193
231
|
seedPaymentMethodsCommand,
|
|
194
232
|
seedExamplesCommand,
|
|
@@ -133,6 +133,14 @@ type SalesDictionarySeed = {
|
|
|
133
133
|
|
|
134
134
|
type SeedScope = { tenantId: string; organizationId: string }
|
|
135
135
|
|
|
136
|
+
export type BackfillDealLossReasonDictionaryResult = {
|
|
137
|
+
dictionaryId: string
|
|
138
|
+
createdDictionary: boolean
|
|
139
|
+
copiedLegacyEntries: number
|
|
140
|
+
seededDefaultEntries: number
|
|
141
|
+
existingEntries: number
|
|
142
|
+
}
|
|
143
|
+
|
|
136
144
|
const ORDER_STATUS_DEFAULTS: SalesDictionarySeed[] = [
|
|
137
145
|
{ value: 'draft', label: 'Draft', color: '#94a3b8', icon: 'lucide:file-pen-line' },
|
|
138
146
|
{ value: 'pending_approval', label: 'Pending Approval', color: '#f59e0b', icon: 'lucide:hourglass' },
|
|
@@ -262,6 +270,88 @@ const DEAL_LOSS_REASON_DEFAULTS: SalesDictionarySeed[] = [
|
|
|
262
270
|
{ value: 'other', label: 'Other', color: '#71717a', icon: 'lucide:minus-circle' },
|
|
263
271
|
]
|
|
264
272
|
|
|
273
|
+
export async function backfillDealLossReasonDictionary(
|
|
274
|
+
em: EntityManager,
|
|
275
|
+
scope: SeedScope
|
|
276
|
+
): Promise<BackfillDealLossReasonDictionaryResult> {
|
|
277
|
+
const definition = getSalesDictionaryDefinition('deal-loss-reasons')
|
|
278
|
+
const existingDictionary = await em.findOne(Dictionary, {
|
|
279
|
+
tenantId: scope.tenantId,
|
|
280
|
+
organizationId: scope.organizationId,
|
|
281
|
+
key: definition.key,
|
|
282
|
+
deletedAt: null,
|
|
283
|
+
})
|
|
284
|
+
const dictionary = existingDictionary ?? await ensureSalesDictionary({
|
|
285
|
+
em,
|
|
286
|
+
tenantId: scope.tenantId,
|
|
287
|
+
organizationId: scope.organizationId,
|
|
288
|
+
kind: 'deal-loss-reasons',
|
|
289
|
+
})
|
|
290
|
+
const targetEntries = await em.find(DictionaryEntry, {
|
|
291
|
+
dictionary,
|
|
292
|
+
tenantId: scope.tenantId,
|
|
293
|
+
organizationId: scope.organizationId,
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
if (targetEntries.length > 0) {
|
|
297
|
+
return {
|
|
298
|
+
dictionaryId: dictionary.id,
|
|
299
|
+
createdDictionary: !existingDictionary,
|
|
300
|
+
copiedLegacyEntries: 0,
|
|
301
|
+
seededDefaultEntries: 0,
|
|
302
|
+
existingEntries: targetEntries.length,
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const legacyDictionary = await em.findOne(Dictionary, {
|
|
307
|
+
tenantId: scope.tenantId,
|
|
308
|
+
organizationId: scope.organizationId,
|
|
309
|
+
key: 'customer_lost_reason',
|
|
310
|
+
deletedAt: null,
|
|
311
|
+
})
|
|
312
|
+
const legacyEntries = legacyDictionary
|
|
313
|
+
? await em.find(
|
|
314
|
+
DictionaryEntry,
|
|
315
|
+
{
|
|
316
|
+
dictionary: legacyDictionary,
|
|
317
|
+
tenantId: scope.tenantId,
|
|
318
|
+
organizationId: scope.organizationId,
|
|
319
|
+
},
|
|
320
|
+
{ orderBy: { position: 'asc', label: 'asc' } },
|
|
321
|
+
)
|
|
322
|
+
: []
|
|
323
|
+
|
|
324
|
+
let copiedLegacyEntries = 0
|
|
325
|
+
let seededDefaultEntries = 0
|
|
326
|
+
const processedValues = new Set<string>()
|
|
327
|
+
const seeds = legacyEntries.length
|
|
328
|
+
? legacyEntries.map((entry) => ({
|
|
329
|
+
value: entry.value,
|
|
330
|
+
label: entry.label,
|
|
331
|
+
color: entry.color,
|
|
332
|
+
icon: entry.icon,
|
|
333
|
+
}))
|
|
334
|
+
: DEAL_LOSS_REASON_DEFAULTS
|
|
335
|
+
|
|
336
|
+
for (const seed of seeds) {
|
|
337
|
+
const normalizedValue = normalizeDictionaryValue(seed.value ?? '')
|
|
338
|
+
if (!normalizedValue || processedValues.has(normalizedValue)) continue
|
|
339
|
+
const entry = await ensureSalesDictionaryEntry(em, scope, 'deal-loss-reasons', seed)
|
|
340
|
+
if (!entry) continue
|
|
341
|
+
processedValues.add(normalizedValue)
|
|
342
|
+
if (legacyEntries.length) copiedLegacyEntries += 1
|
|
343
|
+
else seededDefaultEntries += 1
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
dictionaryId: dictionary.id,
|
|
348
|
+
createdDictionary: !existingDictionary,
|
|
349
|
+
copiedLegacyEntries,
|
|
350
|
+
seededDefaultEntries,
|
|
351
|
+
existingEntries: 0,
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
265
355
|
export async function seedSalesStatusDictionaries(
|
|
266
356
|
em: EntityManager,
|
|
267
357
|
scope: SeedScope
|