@open-mercato/core 0.6.6-develop.6399.1.93ba8ca030 → 0.6.6-develop.6402.1.080aab8ec9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/auth/lib/backendChrome.js +3 -2
- package/dist/modules/auth/lib/backendChrome.js.map +2 -2
- package/dist/modules/currencies/backend/config/currency-fetching/page.meta.js +4 -13
- package/dist/modules/currencies/backend/config/currency-fetching/page.meta.js.map +2 -2
- package/dist/modules/currencies/backend/currencies/page.meta.js +1 -1
- package/dist/modules/currencies/backend/currencies/page.meta.js.map +2 -2
- package/dist/modules/currencies/backend/exchange-rates/page.meta.js +1 -0
- package/dist/modules/currencies/backend/exchange-rates/page.meta.js.map +2 -2
- 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/entities/api/definitions.batch.js +29 -2
- package/dist/modules/entities/api/definitions.batch.js.map +2 -2
- package/dist/modules/entities/api/definitions.js +9 -2
- package/dist/modules/entities/api/definitions.js.map +2 -2
- package/dist/modules/entities/api/definitions.manage.js +14 -3
- package/dist/modules/entities/api/definitions.manage.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/page.js +43 -17
- package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
- package/dist/modules/entities/lib/definitions-version.js +55 -0
- package/dist/modules/entities/lib/definitions-version.js.map +7 -0
- 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/auth/lib/backendChrome.tsx +3 -2
- package/src/modules/currencies/backend/config/currency-fetching/page.meta.ts +4 -15
- package/src/modules/currencies/backend/currencies/page.meta.ts +1 -1
- package/src/modules/currencies/backend/exchange-rates/page.meta.ts +1 -0
- 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/entities/api/definitions.batch.ts +38 -1
- package/src/modules/entities/api/definitions.manage.ts +14 -1
- package/src/modules/entities/api/definitions.ts +10 -1
- package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +53 -17
- package/src/modules/entities/lib/definitions-version.ts +102 -0
- package/src/modules/sales/cli.ts +39 -1
- package/src/modules/sales/lib/dictionaries.ts +90 -0
|
@@ -201,6 +201,75 @@ const DEAL_LOSS_REASON_DEFAULTS = [
|
|
|
201
201
|
{ value: "timing", label: "Bad timing", color: "#6366f1", icon: "lucide:clock" },
|
|
202
202
|
{ value: "other", label: "Other", color: "#71717a", icon: "lucide:minus-circle" }
|
|
203
203
|
];
|
|
204
|
+
async function backfillDealLossReasonDictionary(em, scope) {
|
|
205
|
+
const definition = getSalesDictionaryDefinition("deal-loss-reasons");
|
|
206
|
+
const existingDictionary = await em.findOne(Dictionary, {
|
|
207
|
+
tenantId: scope.tenantId,
|
|
208
|
+
organizationId: scope.organizationId,
|
|
209
|
+
key: definition.key,
|
|
210
|
+
deletedAt: null
|
|
211
|
+
});
|
|
212
|
+
const dictionary = existingDictionary ?? await ensureSalesDictionary({
|
|
213
|
+
em,
|
|
214
|
+
tenantId: scope.tenantId,
|
|
215
|
+
organizationId: scope.organizationId,
|
|
216
|
+
kind: "deal-loss-reasons"
|
|
217
|
+
});
|
|
218
|
+
const targetEntries = await em.find(DictionaryEntry, {
|
|
219
|
+
dictionary,
|
|
220
|
+
tenantId: scope.tenantId,
|
|
221
|
+
organizationId: scope.organizationId
|
|
222
|
+
});
|
|
223
|
+
if (targetEntries.length > 0) {
|
|
224
|
+
return {
|
|
225
|
+
dictionaryId: dictionary.id,
|
|
226
|
+
createdDictionary: !existingDictionary,
|
|
227
|
+
copiedLegacyEntries: 0,
|
|
228
|
+
seededDefaultEntries: 0,
|
|
229
|
+
existingEntries: targetEntries.length
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
const legacyDictionary = await em.findOne(Dictionary, {
|
|
233
|
+
tenantId: scope.tenantId,
|
|
234
|
+
organizationId: scope.organizationId,
|
|
235
|
+
key: "customer_lost_reason",
|
|
236
|
+
deletedAt: null
|
|
237
|
+
});
|
|
238
|
+
const legacyEntries = legacyDictionary ? await em.find(
|
|
239
|
+
DictionaryEntry,
|
|
240
|
+
{
|
|
241
|
+
dictionary: legacyDictionary,
|
|
242
|
+
tenantId: scope.tenantId,
|
|
243
|
+
organizationId: scope.organizationId
|
|
244
|
+
},
|
|
245
|
+
{ orderBy: { position: "asc", label: "asc" } }
|
|
246
|
+
) : [];
|
|
247
|
+
let copiedLegacyEntries = 0;
|
|
248
|
+
let seededDefaultEntries = 0;
|
|
249
|
+
const processedValues = /* @__PURE__ */ new Set();
|
|
250
|
+
const seeds = legacyEntries.length ? legacyEntries.map((entry) => ({
|
|
251
|
+
value: entry.value,
|
|
252
|
+
label: entry.label,
|
|
253
|
+
color: entry.color,
|
|
254
|
+
icon: entry.icon
|
|
255
|
+
})) : DEAL_LOSS_REASON_DEFAULTS;
|
|
256
|
+
for (const seed of seeds) {
|
|
257
|
+
const normalizedValue = normalizeDictionaryValue(seed.value ?? "");
|
|
258
|
+
if (!normalizedValue || processedValues.has(normalizedValue)) continue;
|
|
259
|
+
const entry = await ensureSalesDictionaryEntry(em, scope, "deal-loss-reasons", seed);
|
|
260
|
+
if (!entry) continue;
|
|
261
|
+
processedValues.add(normalizedValue);
|
|
262
|
+
if (legacyEntries.length) copiedLegacyEntries += 1;
|
|
263
|
+
else seededDefaultEntries += 1;
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
dictionaryId: dictionary.id,
|
|
267
|
+
createdDictionary: !existingDictionary,
|
|
268
|
+
copiedLegacyEntries,
|
|
269
|
+
seededDefaultEntries,
|
|
270
|
+
existingEntries: 0
|
|
271
|
+
};
|
|
272
|
+
}
|
|
204
273
|
async function seedSalesStatusDictionaries(em, scope) {
|
|
205
274
|
await seedSalesDictionary(em, scope, "order-status", ORDER_STATUS_DEFAULTS);
|
|
206
275
|
await seedSalesDictionary(em, scope, "order-line-status", ORDER_LINE_STATUS_DEFAULTS);
|
|
@@ -218,6 +287,7 @@ async function seedSalesDictionaries(em, scope) {
|
|
|
218
287
|
const DEFAULT_ADJUSTMENT_KIND_VALUES = ADJUSTMENT_KIND_DEFAULTS.map((entry) => entry.value);
|
|
219
288
|
export {
|
|
220
289
|
DEFAULT_ADJUSTMENT_KIND_VALUES,
|
|
290
|
+
backfillDealLossReasonDictionary,
|
|
221
291
|
ensureSalesDictionary,
|
|
222
292
|
getSalesDictionaryDefinition,
|
|
223
293
|
normalizeDictionaryValue,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/sales/lib/dictionaries.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport {\n normalizeDictionaryValue,\n sanitizeDictionaryColor,\n sanitizeDictionaryIcon,\n} from '@open-mercato/core/modules/dictionaries/lib/utils'\n\nexport type SalesDictionaryKind =\n | 'order-status'\n | 'order-line-status'\n | 'shipment-status'\n | 'payment-status'\n | 'deal-loss-reasons'\n | 'adjustment-kind'\n\ntype SalesDictionaryDefinition = {\n key: string\n name: string\n singular: string\n description: string\n resourceKind: string\n commandPrefix: string\n}\n\nconst DEFINITIONS: Record<SalesDictionaryKind, SalesDictionaryDefinition> = {\n 'order-status': {\n key: 'sales.order_status',\n name: 'Sales order statuses',\n singular: 'Sales order status',\n description: 'Configurable set of statuses used by sales orders.',\n resourceKind: 'sales.order-status',\n commandPrefix: 'sales.order-statuses',\n },\n 'order-line-status': {\n key: 'sales.order_line_status',\n name: 'Sales order line statuses',\n singular: 'Sales order line status',\n description: 'Configurable set of statuses used by sales order lines.',\n resourceKind: 'sales.order-line-status',\n commandPrefix: 'sales.order-line-statuses',\n },\n 'shipment-status': {\n key: 'sales.shipment_status',\n name: 'Shipment statuses',\n singular: 'Shipment status',\n description: 'Configurable set of statuses used by shipments.',\n resourceKind: 'sales.shipment-status',\n commandPrefix: 'sales.shipment-statuses',\n },\n 'payment-status': {\n key: 'sales.payment_status',\n name: 'Payment statuses',\n singular: 'Payment status',\n description: 'Configurable set of statuses used by payments.',\n resourceKind: 'sales.payment-status',\n commandPrefix: 'sales.payment-statuses',\n },\n 'deal-loss-reasons': {\n key: 'sales.deal_loss_reason',\n name: 'Deal loss reasons',\n singular: 'Deal loss reason',\n description: 'Reusable reasons used when closing deals as lost.',\n resourceKind: 'sales.deal-loss-reason',\n commandPrefix: 'sales.deal-loss-reasons',\n },\n 'adjustment-kind': {\n key: 'sales.adjustment_kind',\n name: 'Sales adjustment kinds',\n singular: 'Sales adjustment kind',\n description: 'Reusable adjustment kinds applied to sales documents.',\n resourceKind: 'sales.adjustment-kind',\n commandPrefix: 'sales.adjustment-kinds',\n },\n}\n\nexport function getSalesDictionaryDefinition(kind: SalesDictionaryKind): SalesDictionaryDefinition {\n return DEFINITIONS[kind]\n}\n\nexport async function ensureSalesDictionary(params: {\n em: EntityManager\n tenantId: string\n organizationId: string\n kind: SalesDictionaryKind\n}): Promise<Dictionary> {\n const { em, tenantId, organizationId, kind } = params\n const def = getSalesDictionaryDefinition(kind)\n let dictionary = await em.findOne(Dictionary, {\n tenantId,\n organizationId,\n key: def.key,\n deletedAt: null,\n })\n if (!dictionary) {\n dictionary = em.create(Dictionary, {\n tenantId,\n organizationId,\n key: def.key,\n name: def.name,\n description: def.description,\n isSystem: true,\n isActive: true,\n managerVisibility: 'hidden',\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(dictionary)\n await em.flush()\n }\n return dictionary\n}\n\nexport async function resolveDictionaryEntryValue(\n em: EntityManager,\n entryId: string | null | undefined,\n scope: { tenantId: string }\n): Promise<string | null> {\n if (!entryId) return null\n const entry = await em.findOne(DictionaryEntry, { id: entryId, tenantId: scope.tenantId })\n if (!entry) return null\n return entry.value?.trim() || null\n}\n\nexport { normalizeDictionaryValue, sanitizeDictionaryColor, sanitizeDictionaryIcon }\n\ntype SalesDictionarySeed = {\n value: string\n label: string\n color?: string | null\n icon?: string | null\n}\n\ntype SeedScope = { tenantId: string; organizationId: string }\n\nconst ORDER_STATUS_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'draft', label: 'Draft', color: '#94a3b8', icon: 'lucide:file-pen-line' },\n { value: 'pending_approval', label: 'Pending Approval', color: '#f59e0b', icon: 'lucide:hourglass' },\n { value: 'approved', label: 'Approved', color: '#16a34a', icon: 'lucide:check-circle' },\n { value: 'rejected', label: 'Rejected', color: '#ef4444', icon: 'lucide:x-circle' },\n { value: 'sent', label: 'Sent', color: '#0ea5e9', icon: 'lucide:send' },\n { value: 'confirmed', label: 'Confirmed', color: '#2563eb', icon: 'lucide:badge-check' },\n { value: 'in_fulfillment', label: 'In fulfillment', color: '#f59e0b', icon: 'lucide:loader-2' },\n { value: 'fulfilled', label: 'Fulfilled', color: '#16a34a', icon: 'lucide:check-circle-2' },\n { value: 'on_hold', label: 'On hold', color: '#a855f7', icon: 'lucide:pause-circle' },\n { value: 'canceled', label: 'Canceled', color: '#ef4444', icon: 'lucide:x-circle' },\n]\n\nconst ORDER_LINE_STATUS_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'pending', label: 'Pending', color: '#94a3b8', icon: 'lucide:clock' },\n { value: 'allocated', label: 'Allocated', color: '#6366f1', icon: 'lucide:inbox' },\n { value: 'picking', label: 'Picking', color: '#f59e0b', icon: 'lucide:hand' },\n { value: 'packed', label: 'Packed', color: '#0ea5e9', icon: 'lucide:package' },\n { value: 'shipped', label: 'Shipped', color: '#2563eb', icon: 'lucide:truck' },\n { value: 'delivered', label: 'Delivered', color: '#16a34a', icon: 'lucide:check-circle-2' },\n { value: 'backordered', label: 'Backordered', color: '#d946ef', icon: 'lucide:alert-octagon' },\n { value: 'returned', label: 'Returned', color: '#0d9488', icon: 'lucide:undo-2' },\n { value: 'canceled', label: 'Canceled', color: '#ef4444', icon: 'lucide:x-circle' },\n]\n\nconst SHIPMENT_STATUS_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'pending', label: 'Pending', color: '#94a3b8', icon: 'lucide:clock-3' },\n { value: 'packed', label: 'Packed', color: '#22c55e', icon: 'lucide:package-check' },\n { value: 'shipped', label: 'Shipped', color: '#2563eb', icon: 'lucide:truck' },\n { value: 'in_transit', label: 'In transit', color: '#0ea5e9', icon: 'lucide:loader' },\n { value: 'delivered', label: 'Delivered', color: '#16a34a', icon: 'lucide:check-circle-2' },\n { value: 'canceled', label: 'Canceled', color: '#ef4444', icon: 'lucide:x-circle' },\n { value: 'returned', label: 'Returned', color: '#0d9488', icon: 'lucide:undo-2' },\n]\n\nconst PAYMENT_STATUS_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'pending', label: 'Pending', color: '#94a3b8', icon: 'lucide:clock-3' },\n { value: 'authorized', label: 'Authorized', color: '#6366f1', icon: 'lucide:badge-check' },\n { value: 'captured', label: 'Captured', color: '#0ea5e9', icon: 'lucide:banknote' },\n { value: 'received', label: 'Received', color: '#16a34a', icon: 'lucide:check' },\n { value: 'refunded', label: 'Refunded', color: '#f59e0b', icon: 'lucide:rotate-ccw' },\n { value: 'failed', label: 'Failed', color: '#ef4444', icon: 'lucide:triangle-alert' },\n { value: 'canceled', label: 'Canceled', color: '#ef4444', icon: 'lucide:x-circle' },\n]\n\nconst ADJUSTMENT_KIND_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'discount', label: 'Discount' },\n { value: 'tax', label: 'Tax' },\n { value: 'shipping', label: 'Shipping' },\n { value: 'surcharge', label: 'Surcharge' },\n { value: 'return', label: 'Return' },\n { value: 'custom', label: 'Custom' },\n]\n\nasync function ensureSalesDictionaryEntry(\n em: EntityManager,\n scope: SeedScope,\n kind: SalesDictionaryKind,\n seed: SalesDictionarySeed\n): Promise<DictionaryEntry | null> {\n const value = seed.value?.trim()\n if (!value) return null\n const dictionary = await ensureSalesDictionary({\n em,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n kind,\n })\n const normalizedValue = normalizeDictionaryValue(value)\n const color = seed.color === undefined ? undefined : sanitizeDictionaryColor(seed.color)\n const icon = seed.icon === undefined ? undefined : sanitizeDictionaryIcon(seed.icon)\n const existing = await em.findOne(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n normalizedValue,\n })\n if (existing) {\n let changed = false\n if (color !== undefined && existing.color !== color) {\n existing.color = color ?? null\n changed = true\n }\n if (icon !== undefined && existing.icon !== icon) {\n existing.icon = icon ?? null\n changed = true\n }\n if (changed) {\n existing.updatedAt = new Date()\n em.persist(existing)\n }\n return existing\n }\n const now = new Date()\n const entry = em.create(DictionaryEntry, {\n dictionary,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n value,\n label: seed.label?.trim() || value,\n normalizedValue,\n color: color ?? null,\n icon: icon ?? null,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(entry)\n return entry\n}\n\nasync function seedSalesDictionary(\n em: EntityManager,\n scope: SeedScope,\n kind: SalesDictionaryKind,\n defaults: SalesDictionarySeed[]\n): Promise<void> {\n for (const seed of defaults) {\n await ensureSalesDictionaryEntry(em, scope, kind, seed)\n }\n}\n\nconst DEAL_LOSS_REASON_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'lost_to_competitor', label: 'Lost to competitor', color: '#ef4444', icon: 'lucide:users' },\n { value: 'no_budget', label: 'No budget', color: '#f59e0b', icon: 'lucide:wallet' },\n { value: 'no_decision', label: 'No decision made', color: '#94a3b8', icon: 'lucide:help-circle' },\n { value: 'timing', label: 'Bad timing', color: '#6366f1', icon: 'lucide:clock' },\n { value: 'other', label: 'Other', color: '#71717a', icon: 'lucide:minus-circle' },\n]\n\nexport async function seedSalesStatusDictionaries(\n em: EntityManager,\n scope: SeedScope\n): Promise<void> {\n await seedSalesDictionary(em, scope, 'order-status', ORDER_STATUS_DEFAULTS)\n await seedSalesDictionary(em, scope, 'order-line-status', ORDER_LINE_STATUS_DEFAULTS)\n await seedSalesDictionary(em, scope, 'shipment-status', SHIPMENT_STATUS_DEFAULTS)\n await seedSalesDictionary(em, scope, 'payment-status', PAYMENT_STATUS_DEFAULTS)\n await seedSalesDictionary(em, scope, 'deal-loss-reasons', DEAL_LOSS_REASON_DEFAULTS)\n}\n\nexport async function seedSalesAdjustmentKinds(\n em: EntityManager,\n scope: SeedScope\n): Promise<void> {\n await seedSalesDictionary(em, scope, 'adjustment-kind', ADJUSTMENT_KIND_DEFAULTS)\n}\n\nexport async function seedSalesDictionaries(\n em: EntityManager,\n scope: SeedScope\n): Promise<void> {\n await seedSalesStatusDictionaries(em, scope)\n await seedSalesAdjustmentKinds(em, scope)\n}\n\nexport const DEFAULT_ADJUSTMENT_KIND_VALUES = ADJUSTMENT_KIND_DEFAULTS.map((entry) => entry.value)\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,YAAY,uBAAuB;AAC5C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAmBP,MAAM,cAAsE;AAAA,EAC1E,gBAAgB;AAAA,IACd,KAAK;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,IACV,aAAa;AAAA,IACb,cAAc;AAAA,IACd,eAAe;AAAA,EACjB;AAAA,EACA,qBAAqB;AAAA,IACnB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,IACV,aAAa;AAAA,IACb,cAAc;AAAA,IACd,eAAe;AAAA,EACjB;AAAA,EACA,mBAAmB;AAAA,IACjB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,IACV,aAAa;AAAA,IACb,cAAc;AAAA,IACd,eAAe;AAAA,EACjB;AAAA,EACA,kBAAkB;AAAA,IAChB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,IACV,aAAa;AAAA,IACb,cAAc;AAAA,IACd,eAAe;AAAA,EACjB;AAAA,EACA,qBAAqB;AAAA,IACnB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,IACV,aAAa;AAAA,IACb,cAAc;AAAA,IACd,eAAe;AAAA,EACjB;AAAA,EACA,mBAAmB;AAAA,IACjB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,IACV,aAAa;AAAA,IACb,cAAc;AAAA,IACd,eAAe;AAAA,EACjB;AACF;AAEO,SAAS,6BAA6B,MAAsD;AACjG,SAAO,YAAY,IAAI;AACzB;AAEA,eAAsB,sBAAsB,QAKpB;AACtB,QAAM,EAAE,IAAI,UAAU,gBAAgB,KAAK,IAAI;AAC/C,QAAM,MAAM,6BAA6B,IAAI;AAC7C,MAAI,aAAa,MAAM,GAAG,QAAQ,YAAY;AAAA,IAC5C;AAAA,IACA;AAAA,IACA,KAAK,IAAI;AAAA,IACT,WAAW;AAAA,EACb,CAAC;AACD,MAAI,CAAC,YAAY;AACf,iBAAa,GAAG,OAAO,YAAY;AAAA,MACjC;AAAA,MACA;AAAA,MACA,KAAK,IAAI;AAAA,MACT,MAAM,IAAI;AAAA,MACV,aAAa,IAAI;AAAA,MACjB,UAAU;AAAA,MACV,UAAU;AAAA,MACV,mBAAmB;AAAA,MACnB,WAAW,oBAAI,KAAK;AAAA,MACpB,WAAW,oBAAI,KAAK;AAAA,IACtB,CAAC;AACD,OAAG,QAAQ,UAAU;AACrB,UAAM,GAAG,MAAM;AAAA,EACjB;AACA,SAAO;AACT;AAEA,eAAsB,4BACpB,IACA,SACA,OACwB;AACxB,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,QAAQ,MAAM,GAAG,QAAQ,iBAAiB,EAAE,IAAI,SAAS,UAAU,MAAM,SAAS,CAAC;AACzF,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,MAAM,OAAO,KAAK,KAAK;AAChC;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport {\n normalizeDictionaryValue,\n sanitizeDictionaryColor,\n sanitizeDictionaryIcon,\n} from '@open-mercato/core/modules/dictionaries/lib/utils'\n\nexport type SalesDictionaryKind =\n | 'order-status'\n | 'order-line-status'\n | 'shipment-status'\n | 'payment-status'\n | 'deal-loss-reasons'\n | 'adjustment-kind'\n\ntype SalesDictionaryDefinition = {\n key: string\n name: string\n singular: string\n description: string\n resourceKind: string\n commandPrefix: string\n}\n\nconst DEFINITIONS: Record<SalesDictionaryKind, SalesDictionaryDefinition> = {\n 'order-status': {\n key: 'sales.order_status',\n name: 'Sales order statuses',\n singular: 'Sales order status',\n description: 'Configurable set of statuses used by sales orders.',\n resourceKind: 'sales.order-status',\n commandPrefix: 'sales.order-statuses',\n },\n 'order-line-status': {\n key: 'sales.order_line_status',\n name: 'Sales order line statuses',\n singular: 'Sales order line status',\n description: 'Configurable set of statuses used by sales order lines.',\n resourceKind: 'sales.order-line-status',\n commandPrefix: 'sales.order-line-statuses',\n },\n 'shipment-status': {\n key: 'sales.shipment_status',\n name: 'Shipment statuses',\n singular: 'Shipment status',\n description: 'Configurable set of statuses used by shipments.',\n resourceKind: 'sales.shipment-status',\n commandPrefix: 'sales.shipment-statuses',\n },\n 'payment-status': {\n key: 'sales.payment_status',\n name: 'Payment statuses',\n singular: 'Payment status',\n description: 'Configurable set of statuses used by payments.',\n resourceKind: 'sales.payment-status',\n commandPrefix: 'sales.payment-statuses',\n },\n 'deal-loss-reasons': {\n key: 'sales.deal_loss_reason',\n name: 'Deal loss reasons',\n singular: 'Deal loss reason',\n description: 'Reusable reasons used when closing deals as lost.',\n resourceKind: 'sales.deal-loss-reason',\n commandPrefix: 'sales.deal-loss-reasons',\n },\n 'adjustment-kind': {\n key: 'sales.adjustment_kind',\n name: 'Sales adjustment kinds',\n singular: 'Sales adjustment kind',\n description: 'Reusable adjustment kinds applied to sales documents.',\n resourceKind: 'sales.adjustment-kind',\n commandPrefix: 'sales.adjustment-kinds',\n },\n}\n\nexport function getSalesDictionaryDefinition(kind: SalesDictionaryKind): SalesDictionaryDefinition {\n return DEFINITIONS[kind]\n}\n\nexport async function ensureSalesDictionary(params: {\n em: EntityManager\n tenantId: string\n organizationId: string\n kind: SalesDictionaryKind\n}): Promise<Dictionary> {\n const { em, tenantId, organizationId, kind } = params\n const def = getSalesDictionaryDefinition(kind)\n let dictionary = await em.findOne(Dictionary, {\n tenantId,\n organizationId,\n key: def.key,\n deletedAt: null,\n })\n if (!dictionary) {\n dictionary = em.create(Dictionary, {\n tenantId,\n organizationId,\n key: def.key,\n name: def.name,\n description: def.description,\n isSystem: true,\n isActive: true,\n managerVisibility: 'hidden',\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(dictionary)\n await em.flush()\n }\n return dictionary\n}\n\nexport async function resolveDictionaryEntryValue(\n em: EntityManager,\n entryId: string | null | undefined,\n scope: { tenantId: string }\n): Promise<string | null> {\n if (!entryId) return null\n const entry = await em.findOne(DictionaryEntry, { id: entryId, tenantId: scope.tenantId })\n if (!entry) return null\n return entry.value?.trim() || null\n}\n\nexport { normalizeDictionaryValue, sanitizeDictionaryColor, sanitizeDictionaryIcon }\n\ntype SalesDictionarySeed = {\n value: string\n label: string\n color?: string | null\n icon?: string | null\n}\n\ntype SeedScope = { tenantId: string; organizationId: string }\n\nexport type BackfillDealLossReasonDictionaryResult = {\n dictionaryId: string\n createdDictionary: boolean\n copiedLegacyEntries: number\n seededDefaultEntries: number\n existingEntries: number\n}\n\nconst ORDER_STATUS_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'draft', label: 'Draft', color: '#94a3b8', icon: 'lucide:file-pen-line' },\n { value: 'pending_approval', label: 'Pending Approval', color: '#f59e0b', icon: 'lucide:hourglass' },\n { value: 'approved', label: 'Approved', color: '#16a34a', icon: 'lucide:check-circle' },\n { value: 'rejected', label: 'Rejected', color: '#ef4444', icon: 'lucide:x-circle' },\n { value: 'sent', label: 'Sent', color: '#0ea5e9', icon: 'lucide:send' },\n { value: 'confirmed', label: 'Confirmed', color: '#2563eb', icon: 'lucide:badge-check' },\n { value: 'in_fulfillment', label: 'In fulfillment', color: '#f59e0b', icon: 'lucide:loader-2' },\n { value: 'fulfilled', label: 'Fulfilled', color: '#16a34a', icon: 'lucide:check-circle-2' },\n { value: 'on_hold', label: 'On hold', color: '#a855f7', icon: 'lucide:pause-circle' },\n { value: 'canceled', label: 'Canceled', color: '#ef4444', icon: 'lucide:x-circle' },\n]\n\nconst ORDER_LINE_STATUS_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'pending', label: 'Pending', color: '#94a3b8', icon: 'lucide:clock' },\n { value: 'allocated', label: 'Allocated', color: '#6366f1', icon: 'lucide:inbox' },\n { value: 'picking', label: 'Picking', color: '#f59e0b', icon: 'lucide:hand' },\n { value: 'packed', label: 'Packed', color: '#0ea5e9', icon: 'lucide:package' },\n { value: 'shipped', label: 'Shipped', color: '#2563eb', icon: 'lucide:truck' },\n { value: 'delivered', label: 'Delivered', color: '#16a34a', icon: 'lucide:check-circle-2' },\n { value: 'backordered', label: 'Backordered', color: '#d946ef', icon: 'lucide:alert-octagon' },\n { value: 'returned', label: 'Returned', color: '#0d9488', icon: 'lucide:undo-2' },\n { value: 'canceled', label: 'Canceled', color: '#ef4444', icon: 'lucide:x-circle' },\n]\n\nconst SHIPMENT_STATUS_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'pending', label: 'Pending', color: '#94a3b8', icon: 'lucide:clock-3' },\n { value: 'packed', label: 'Packed', color: '#22c55e', icon: 'lucide:package-check' },\n { value: 'shipped', label: 'Shipped', color: '#2563eb', icon: 'lucide:truck' },\n { value: 'in_transit', label: 'In transit', color: '#0ea5e9', icon: 'lucide:loader' },\n { value: 'delivered', label: 'Delivered', color: '#16a34a', icon: 'lucide:check-circle-2' },\n { value: 'canceled', label: 'Canceled', color: '#ef4444', icon: 'lucide:x-circle' },\n { value: 'returned', label: 'Returned', color: '#0d9488', icon: 'lucide:undo-2' },\n]\n\nconst PAYMENT_STATUS_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'pending', label: 'Pending', color: '#94a3b8', icon: 'lucide:clock-3' },\n { value: 'authorized', label: 'Authorized', color: '#6366f1', icon: 'lucide:badge-check' },\n { value: 'captured', label: 'Captured', color: '#0ea5e9', icon: 'lucide:banknote' },\n { value: 'received', label: 'Received', color: '#16a34a', icon: 'lucide:check' },\n { value: 'refunded', label: 'Refunded', color: '#f59e0b', icon: 'lucide:rotate-ccw' },\n { value: 'failed', label: 'Failed', color: '#ef4444', icon: 'lucide:triangle-alert' },\n { value: 'canceled', label: 'Canceled', color: '#ef4444', icon: 'lucide:x-circle' },\n]\n\nconst ADJUSTMENT_KIND_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'discount', label: 'Discount' },\n { value: 'tax', label: 'Tax' },\n { value: 'shipping', label: 'Shipping' },\n { value: 'surcharge', label: 'Surcharge' },\n { value: 'return', label: 'Return' },\n { value: 'custom', label: 'Custom' },\n]\n\nasync function ensureSalesDictionaryEntry(\n em: EntityManager,\n scope: SeedScope,\n kind: SalesDictionaryKind,\n seed: SalesDictionarySeed\n): Promise<DictionaryEntry | null> {\n const value = seed.value?.trim()\n if (!value) return null\n const dictionary = await ensureSalesDictionary({\n em,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n kind,\n })\n const normalizedValue = normalizeDictionaryValue(value)\n const color = seed.color === undefined ? undefined : sanitizeDictionaryColor(seed.color)\n const icon = seed.icon === undefined ? undefined : sanitizeDictionaryIcon(seed.icon)\n const existing = await em.findOne(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n normalizedValue,\n })\n if (existing) {\n let changed = false\n if (color !== undefined && existing.color !== color) {\n existing.color = color ?? null\n changed = true\n }\n if (icon !== undefined && existing.icon !== icon) {\n existing.icon = icon ?? null\n changed = true\n }\n if (changed) {\n existing.updatedAt = new Date()\n em.persist(existing)\n }\n return existing\n }\n const now = new Date()\n const entry = em.create(DictionaryEntry, {\n dictionary,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n value,\n label: seed.label?.trim() || value,\n normalizedValue,\n color: color ?? null,\n icon: icon ?? null,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(entry)\n return entry\n}\n\nasync function seedSalesDictionary(\n em: EntityManager,\n scope: SeedScope,\n kind: SalesDictionaryKind,\n defaults: SalesDictionarySeed[]\n): Promise<void> {\n for (const seed of defaults) {\n await ensureSalesDictionaryEntry(em, scope, kind, seed)\n }\n}\n\nconst DEAL_LOSS_REASON_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'lost_to_competitor', label: 'Lost to competitor', color: '#ef4444', icon: 'lucide:users' },\n { value: 'no_budget', label: 'No budget', color: '#f59e0b', icon: 'lucide:wallet' },\n { value: 'no_decision', label: 'No decision made', color: '#94a3b8', icon: 'lucide:help-circle' },\n { value: 'timing', label: 'Bad timing', color: '#6366f1', icon: 'lucide:clock' },\n { value: 'other', label: 'Other', color: '#71717a', icon: 'lucide:minus-circle' },\n]\n\nexport async function backfillDealLossReasonDictionary(\n em: EntityManager,\n scope: SeedScope\n): Promise<BackfillDealLossReasonDictionaryResult> {\n const definition = getSalesDictionaryDefinition('deal-loss-reasons')\n const existingDictionary = await em.findOne(Dictionary, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n key: definition.key,\n deletedAt: null,\n })\n const dictionary = existingDictionary ?? await ensureSalesDictionary({\n em,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n kind: 'deal-loss-reasons',\n })\n const targetEntries = await em.find(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n\n if (targetEntries.length > 0) {\n return {\n dictionaryId: dictionary.id,\n createdDictionary: !existingDictionary,\n copiedLegacyEntries: 0,\n seededDefaultEntries: 0,\n existingEntries: targetEntries.length,\n }\n }\n\n const legacyDictionary = await em.findOne(Dictionary, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n key: 'customer_lost_reason',\n deletedAt: null,\n })\n const legacyEntries = legacyDictionary\n ? await em.find(\n DictionaryEntry,\n {\n dictionary: legacyDictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n },\n { orderBy: { position: 'asc', label: 'asc' } },\n )\n : []\n\n let copiedLegacyEntries = 0\n let seededDefaultEntries = 0\n const processedValues = new Set<string>()\n const seeds = legacyEntries.length\n ? legacyEntries.map((entry) => ({\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n }))\n : DEAL_LOSS_REASON_DEFAULTS\n\n for (const seed of seeds) {\n const normalizedValue = normalizeDictionaryValue(seed.value ?? '')\n if (!normalizedValue || processedValues.has(normalizedValue)) continue\n const entry = await ensureSalesDictionaryEntry(em, scope, 'deal-loss-reasons', seed)\n if (!entry) continue\n processedValues.add(normalizedValue)\n if (legacyEntries.length) copiedLegacyEntries += 1\n else seededDefaultEntries += 1\n }\n\n return {\n dictionaryId: dictionary.id,\n createdDictionary: !existingDictionary,\n copiedLegacyEntries,\n seededDefaultEntries,\n existingEntries: 0,\n }\n}\n\nexport async function seedSalesStatusDictionaries(\n em: EntityManager,\n scope: SeedScope\n): Promise<void> {\n await seedSalesDictionary(em, scope, 'order-status', ORDER_STATUS_DEFAULTS)\n await seedSalesDictionary(em, scope, 'order-line-status', ORDER_LINE_STATUS_DEFAULTS)\n await seedSalesDictionary(em, scope, 'shipment-status', SHIPMENT_STATUS_DEFAULTS)\n await seedSalesDictionary(em, scope, 'payment-status', PAYMENT_STATUS_DEFAULTS)\n await seedSalesDictionary(em, scope, 'deal-loss-reasons', DEAL_LOSS_REASON_DEFAULTS)\n}\n\nexport async function seedSalesAdjustmentKinds(\n em: EntityManager,\n scope: SeedScope\n): Promise<void> {\n await seedSalesDictionary(em, scope, 'adjustment-kind', ADJUSTMENT_KIND_DEFAULTS)\n}\n\nexport async function seedSalesDictionaries(\n em: EntityManager,\n scope: SeedScope\n): Promise<void> {\n await seedSalesStatusDictionaries(em, scope)\n await seedSalesAdjustmentKinds(em, scope)\n}\n\nexport const DEFAULT_ADJUSTMENT_KIND_VALUES = ADJUSTMENT_KIND_DEFAULTS.map((entry) => entry.value)\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6402.1.080aab8ec9",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -248,16 +248,16 @@
|
|
|
248
248
|
"zod": "^4.4.3"
|
|
249
249
|
},
|
|
250
250
|
"peerDependencies": {
|
|
251
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
252
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
253
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
251
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6402.1.080aab8ec9",
|
|
252
|
+
"@open-mercato/shared": "0.6.6-develop.6402.1.080aab8ec9",
|
|
253
|
+
"@open-mercato/ui": "0.6.6-develop.6402.1.080aab8ec9",
|
|
254
254
|
"react": "^19.0.0",
|
|
255
255
|
"react-dom": "^19.0.0"
|
|
256
256
|
},
|
|
257
257
|
"devDependencies": {
|
|
258
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
259
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
260
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
258
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6402.1.080aab8ec9",
|
|
259
|
+
"@open-mercato/shared": "0.6.6-develop.6402.1.080aab8ec9",
|
|
260
|
+
"@open-mercato/ui": "0.6.6-develop.6402.1.080aab8ec9",
|
|
261
261
|
"@testing-library/dom": "^10.4.1",
|
|
262
262
|
"@testing-library/jest-dom": "^6.9.1",
|
|
263
263
|
"@testing-library/react": "^16.3.1",
|
|
@@ -95,8 +95,9 @@ const settingsSectionOrder: Record<string, number> = {
|
|
|
95
95
|
'customer-portal': 3,
|
|
96
96
|
'data-designer': 4,
|
|
97
97
|
'module-configs': 5,
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
currencies: 6,
|
|
99
|
+
directory: 7,
|
|
100
|
+
'feature-toggles': 8,
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
type NavGroupWithWeight = Omit<BackendChromeNavGroup, 'id' | 'defaultName' | 'items'> & {
|
|
@@ -1,22 +1,11 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
|
|
3
|
-
const exchangeRateIcon = React.createElement(
|
|
4
|
-
'svg',
|
|
5
|
-
{ width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
|
|
6
|
-
React.createElement('path', { d: 'M7 16l-4-4 4-4' }),
|
|
7
|
-
React.createElement('path', { d: 'M3 12h11' }),
|
|
8
|
-
React.createElement('path', { d: 'M17 8l4 4-4 4' }),
|
|
9
|
-
React.createElement('path', { d: 'M21 12H10' }),
|
|
10
|
-
)
|
|
11
|
-
|
|
12
1
|
export const metadata = {
|
|
13
|
-
icon:
|
|
2
|
+
icon: 'download',
|
|
14
3
|
requireAuth: true,
|
|
15
4
|
requireFeatures: ['currencies.fetch.view'],
|
|
16
|
-
pageGroup: '
|
|
17
|
-
pageGroupKey: '
|
|
5
|
+
pageGroup: 'Currencies',
|
|
6
|
+
pageGroupKey: 'currencies.nav.group',
|
|
18
7
|
pageTitle: 'Currency Rate Fetching',
|
|
19
8
|
pageTitleKey: 'currencies.fetch.title',
|
|
20
|
-
pageOrder:
|
|
9
|
+
pageOrder: 30,
|
|
21
10
|
pageContext: 'settings' as const,
|
|
22
11
|
}
|
|
@@ -5,8 +5,8 @@ export const metadata = {
|
|
|
5
5
|
pageTitleKey: 'currencies.page.title',
|
|
6
6
|
pageGroup: 'Currencies',
|
|
7
7
|
pageGroupKey: 'currencies.nav.group',
|
|
8
|
-
pagePriority: 60,
|
|
9
8
|
pageOrder: 10,
|
|
9
|
+
pageContext: 'settings' as const,
|
|
10
10
|
icon: 'coins',
|
|
11
11
|
breadcrumb: [{ label: 'Currencies', labelKey: 'currencies.page.title' }],
|
|
12
12
|
}
|
|
@@ -6,6 +6,7 @@ export const metadata = {
|
|
|
6
6
|
pageGroup: 'Currencies',
|
|
7
7
|
pageGroupKey: 'currencies.nav.group',
|
|
8
8
|
pageOrder: 20,
|
|
9
|
+
pageContext: 'settings' as const,
|
|
9
10
|
icon: 'refresh-cw',
|
|
10
11
|
breadcrumb: [
|
|
11
12
|
{ label: 'Currencies', labelKey: 'currencies.page.title', href: '/backend/currencies' },
|
|
@@ -82,25 +82,21 @@ export function useDealClosure({
|
|
|
82
82
|
async (input: { lossReasonId: string; lossNotes?: string }) => {
|
|
83
83
|
if (!currentDealId) return
|
|
84
84
|
if (!(await confirmDiscardIfDirty())) return
|
|
85
|
+
const lossPayload = {
|
|
86
|
+
id: currentDealId,
|
|
87
|
+
closureOutcome: 'lost' as const,
|
|
88
|
+
status: 'loose',
|
|
89
|
+
lossReasonId: input.lossReasonId,
|
|
90
|
+
...(input.lossNotes ? { lossNotes: input.lossNotes } : {}),
|
|
91
|
+
}
|
|
85
92
|
try {
|
|
86
93
|
await runMutationWithContext(
|
|
87
94
|
() => withScopedApiRequestHeaders(
|
|
88
95
|
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
|
-
}),
|
|
96
|
+
() => updateCrud('customers/deals', lossPayload),
|
|
97
97
|
),
|
|
98
98
|
{
|
|
99
|
-
|
|
100
|
-
closureOutcome: 'lost',
|
|
101
|
-
status: 'loose',
|
|
102
|
-
lossReasonId: input.lossReasonId,
|
|
103
|
-
lossNotes: input.lossNotes ?? null,
|
|
99
|
+
...lossPayload,
|
|
104
100
|
operation: 'closeLost',
|
|
105
101
|
},
|
|
106
102
|
)
|
|
@@ -40,18 +40,28 @@ export function ConfirmDealLostDialog({
|
|
|
40
40
|
const [lossReasons, setLossReasons] = React.useState<LossReasonOption[]>([])
|
|
41
41
|
const [reasonListOpen, setReasonListOpen] = React.useState(false)
|
|
42
42
|
const [error, setError] = React.useState('')
|
|
43
|
+
const [dictionaryLoadFailed, setDictionaryLoadFailed] = React.useState(false)
|
|
44
|
+
const [isLoadingReasons, setIsLoadingReasons] = React.useState(false)
|
|
43
45
|
const [isConfirming, setIsConfirming] = React.useState(false)
|
|
44
46
|
|
|
45
47
|
React.useEffect(() => {
|
|
46
48
|
if (!open) return
|
|
47
49
|
let cancelled = false
|
|
50
|
+
setIsLoadingReasons(true)
|
|
51
|
+
setDictionaryLoadFailed(false)
|
|
48
52
|
loadDictionaryEntriesByKey('sales.deal_loss_reason')
|
|
49
53
|
.then((items) => {
|
|
50
54
|
if (!cancelled) setLossReasons(items)
|
|
51
55
|
})
|
|
52
56
|
.catch((loadError) => {
|
|
53
57
|
console.error('customers.deals.detail.lossReasons failed', loadError)
|
|
54
|
-
if (!cancelled)
|
|
58
|
+
if (!cancelled) {
|
|
59
|
+
setLossReasons([])
|
|
60
|
+
setDictionaryLoadFailed(true)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
.finally(() => {
|
|
64
|
+
if (!cancelled) setIsLoadingReasons(false)
|
|
55
65
|
})
|
|
56
66
|
return () => {
|
|
57
67
|
cancelled = true
|
|
@@ -64,14 +74,35 @@ export function ConfirmDealLostDialog({
|
|
|
64
74
|
setLossNotes('')
|
|
65
75
|
setReasonListOpen(false)
|
|
66
76
|
setError('')
|
|
77
|
+
setDictionaryLoadFailed(false)
|
|
78
|
+
setIsConfirming(false)
|
|
67
79
|
}, [open])
|
|
68
80
|
|
|
69
81
|
const selectedLossReason = React.useMemo(
|
|
70
82
|
() => lossReasons.find((reason) => reason.id === lossReasonId) ?? null,
|
|
71
83
|
[lossReasonId, lossReasons],
|
|
72
84
|
)
|
|
85
|
+
const hasLossReasons = lossReasons.length > 0
|
|
86
|
+
const reasonUnavailable = !isLoadingReasons && !hasLossReasons
|
|
87
|
+
const unavailableReasonText = dictionaryLoadFailed
|
|
88
|
+
? t('customers.deals.detail.lost.reasonLoadError', 'Loss reasons could not be loaded.')
|
|
89
|
+
: t('customers.deals.detail.lost.reasonUnavailable', 'No loss reasons are configured.')
|
|
90
|
+
const reasonHelpText = React.useMemo(() => {
|
|
91
|
+
if (selectedLossReason?.description) return selectedLossReason.description
|
|
92
|
+
if (isLoadingReasons) {
|
|
93
|
+
return t('customers.deals.detail.lost.reasonLoading', 'Loading loss reasons...')
|
|
94
|
+
}
|
|
95
|
+
if (reasonUnavailable) {
|
|
96
|
+
return unavailableReasonText
|
|
97
|
+
}
|
|
98
|
+
return t('customers.deals.detail.lost.reasonHelp', 'Choose the closest reason from the dictionary.')
|
|
99
|
+
}, [isLoadingReasons, reasonUnavailable, selectedLossReason?.description, t, unavailableReasonText])
|
|
73
100
|
|
|
74
101
|
const handleConfirm = React.useCallback(async () => {
|
|
102
|
+
if (isLoadingReasons || reasonUnavailable) {
|
|
103
|
+
setError(unavailableReasonText)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
75
106
|
if (!lossReasonId) {
|
|
76
107
|
setError(t('customers.deals.detail.lost.reasonRequired', 'Please select a loss reason'))
|
|
77
108
|
return
|
|
@@ -85,11 +116,13 @@ export function ConfirmDealLostDialog({
|
|
|
85
116
|
} finally {
|
|
86
117
|
setIsConfirming(false)
|
|
87
118
|
}
|
|
88
|
-
}, [lossNotes, lossReasonId, onConfirm, t])
|
|
119
|
+
}, [isLoadingReasons, lossNotes, lossReasonId, onConfirm, reasonUnavailable, t, unavailableReasonText])
|
|
120
|
+
|
|
121
|
+
const confirmDisabled = isConfirming || isLoadingReasons || reasonUnavailable || !lossReasonId
|
|
89
122
|
|
|
90
123
|
const handleKeyDown = useDialogKeyHandler({
|
|
91
124
|
onConfirm: () => void handleConfirm(),
|
|
92
|
-
disabled:
|
|
125
|
+
disabled: confirmDisabled,
|
|
93
126
|
})
|
|
94
127
|
|
|
95
128
|
return (
|
|
@@ -138,10 +171,13 @@ export function ConfirmDealLostDialog({
|
|
|
138
171
|
>
|
|
139
172
|
<div className="min-w-0">
|
|
140
173
|
<div className="truncate text-base font-semibold text-foreground">
|
|
141
|
-
{selectedLossReason?.label
|
|
174
|
+
{selectedLossReason?.label
|
|
175
|
+
?? (isLoadingReasons
|
|
176
|
+
? t('customers.deals.detail.lost.reasonLoadingShort', 'Loading...')
|
|
177
|
+
: t('customers.deals.detail.lost.reasonPlaceholder', 'Select loss reason'))}
|
|
142
178
|
</div>
|
|
143
179
|
<div className="truncate text-sm text-muted-foreground">
|
|
144
|
-
{
|
|
180
|
+
{reasonHelpText}
|
|
145
181
|
</div>
|
|
146
182
|
</div>
|
|
147
183
|
<ChevronDown className="ml-3 size-4 shrink-0 text-muted-foreground" />
|
|
@@ -149,7 +185,7 @@ export function ConfirmDealLostDialog({
|
|
|
149
185
|
|
|
150
186
|
{reasonListOpen ? (
|
|
151
187
|
<div className="overflow-hidden rounded-md border border-border/80 bg-background">
|
|
152
|
-
{lossReasons.map((reason, index) => {
|
|
188
|
+
{hasLossReasons ? lossReasons.map((reason, index) => {
|
|
153
189
|
const isSelected = reason.id === lossReasonId
|
|
154
190
|
return (
|
|
155
191
|
<Button
|
|
@@ -178,10 +214,19 @@ export function ConfirmDealLostDialog({
|
|
|
178
214
|
) : null}
|
|
179
215
|
</Button>
|
|
180
216
|
)
|
|
181
|
-
})
|
|
217
|
+
}) : (
|
|
218
|
+
<div className="px-4 py-3 text-sm text-muted-foreground">
|
|
219
|
+
{unavailableReasonText}
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
182
222
|
</div>
|
|
183
223
|
) : null}
|
|
184
224
|
</div>
|
|
225
|
+
{reasonUnavailable ? (
|
|
226
|
+
<p className="text-xs text-destructive">
|
|
227
|
+
{unavailableReasonText}
|
|
228
|
+
</p>
|
|
229
|
+
) : null}
|
|
185
230
|
{error ? <p className="text-xs text-destructive">{error}</p> : null}
|
|
186
231
|
</div>
|
|
187
232
|
|
|
@@ -203,7 +248,12 @@ export function ConfirmDealLostDialog({
|
|
|
203
248
|
<Button type="button" variant="outline" onClick={onClose}>
|
|
204
249
|
{t('customers.deals.detail.lost.cancel', 'Cancel')}
|
|
205
250
|
</Button>
|
|
206
|
-
<Button
|
|
251
|
+
<Button
|
|
252
|
+
type="button"
|
|
253
|
+
variant="destructive"
|
|
254
|
+
disabled={confirmDisabled}
|
|
255
|
+
onClick={() => { void handleConfirm() }}
|
|
256
|
+
>
|
|
207
257
|
{t('customers.deals.detail.lost.confirm', 'Mark as Lost')}
|
|
208
258
|
</Button>
|
|
209
259
|
</DialogFooter>
|
|
@@ -992,8 +992,12 @@
|
|
|
992
992
|
"customers.deals.detail.lost.reasonFallbackDescription": "No description available.",
|
|
993
993
|
"customers.deals.detail.lost.reasonHelp": "Choose the closest reason from the dictionary.",
|
|
994
994
|
"customers.deals.detail.lost.reasonLabel": "Loss reason",
|
|
995
|
+
"customers.deals.detail.lost.reasonLoadError": "Loss reasons could not be loaded.",
|
|
996
|
+
"customers.deals.detail.lost.reasonLoading": "Loading loss reasons...",
|
|
997
|
+
"customers.deals.detail.lost.reasonLoadingShort": "Loading...",
|
|
995
998
|
"customers.deals.detail.lost.reasonPlaceholder": "Select loss reason",
|
|
996
999
|
"customers.deals.detail.lost.reasonRequired": "Please select a loss reason",
|
|
1000
|
+
"customers.deals.detail.lost.reasonUnavailable": "No loss reasons are configured.",
|
|
997
1001
|
"customers.deals.detail.lost.salesCycle": "Sales cycle",
|
|
998
1002
|
"customers.deals.detail.lost.secondaryAction": "Set reminder for Q3",
|
|
999
1003
|
"customers.deals.detail.lost.title": "Mark deal as Lost?",
|
|
@@ -992,8 +992,12 @@
|
|
|
992
992
|
"customers.deals.detail.lost.reasonFallbackDescription": "No description available.",
|
|
993
993
|
"customers.deals.detail.lost.reasonHelp": "Choose the closest reason from the dictionary.",
|
|
994
994
|
"customers.deals.detail.lost.reasonLabel": "Loss reason",
|
|
995
|
+
"customers.deals.detail.lost.reasonLoadError": "Loss reasons could not be loaded.",
|
|
996
|
+
"customers.deals.detail.lost.reasonLoading": "Loading loss reasons...",
|
|
997
|
+
"customers.deals.detail.lost.reasonLoadingShort": "Loading...",
|
|
995
998
|
"customers.deals.detail.lost.reasonPlaceholder": "Select loss reason",
|
|
996
999
|
"customers.deals.detail.lost.reasonRequired": "Please select a loss reason",
|
|
1000
|
+
"customers.deals.detail.lost.reasonUnavailable": "No loss reasons are configured.",
|
|
997
1001
|
"customers.deals.detail.lost.salesCycle": "Sales cycle",
|
|
998
1002
|
"customers.deals.detail.lost.secondaryAction": "Set reminder for Q3",
|
|
999
1003
|
"customers.deals.detail.lost.title": "Mark deal as Lost?",
|
|
@@ -992,8 +992,12 @@
|
|
|
992
992
|
"customers.deals.detail.lost.reasonFallbackDescription": "No description available.",
|
|
993
993
|
"customers.deals.detail.lost.reasonHelp": "Choose the closest reason from the dictionary.",
|
|
994
994
|
"customers.deals.detail.lost.reasonLabel": "Loss reason",
|
|
995
|
+
"customers.deals.detail.lost.reasonLoadError": "Loss reasons could not be loaded.",
|
|
996
|
+
"customers.deals.detail.lost.reasonLoading": "Loading loss reasons...",
|
|
997
|
+
"customers.deals.detail.lost.reasonLoadingShort": "Loading...",
|
|
995
998
|
"customers.deals.detail.lost.reasonPlaceholder": "Select loss reason",
|
|
996
999
|
"customers.deals.detail.lost.reasonRequired": "Please select a loss reason",
|
|
1000
|
+
"customers.deals.detail.lost.reasonUnavailable": "No loss reasons are configured.",
|
|
997
1001
|
"customers.deals.detail.lost.salesCycle": "Sales cycle",
|
|
998
1002
|
"customers.deals.detail.lost.secondaryAction": "Set reminder for Q3",
|
|
999
1003
|
"customers.deals.detail.lost.title": "Mark deal as Lost?",
|
|
@@ -992,8 +992,12 @@
|
|
|
992
992
|
"customers.deals.detail.lost.reasonFallbackDescription": "No description available.",
|
|
993
993
|
"customers.deals.detail.lost.reasonHelp": "Choose the closest reason from the dictionary.",
|
|
994
994
|
"customers.deals.detail.lost.reasonLabel": "Loss reason",
|
|
995
|
+
"customers.deals.detail.lost.reasonLoadError": "Loss reasons could not be loaded.",
|
|
996
|
+
"customers.deals.detail.lost.reasonLoading": "Loading loss reasons...",
|
|
997
|
+
"customers.deals.detail.lost.reasonLoadingShort": "Loading...",
|
|
995
998
|
"customers.deals.detail.lost.reasonPlaceholder": "Select loss reason",
|
|
996
999
|
"customers.deals.detail.lost.reasonRequired": "Please select a loss reason",
|
|
1000
|
+
"customers.deals.detail.lost.reasonUnavailable": "No loss reasons are configured.",
|
|
997
1001
|
"customers.deals.detail.lost.salesCycle": "Sales cycle",
|
|
998
1002
|
"customers.deals.detail.lost.secondaryAction": "Set reminder for Q3",
|
|
999
1003
|
"customers.deals.detail.lost.title": "Mark deal as Lost?",
|
|
@@ -5,9 +5,12 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
|
5
5
|
import { CustomFieldDef, CustomFieldEntityConfig } from '@open-mercato/core/modules/entities/data/entities'
|
|
6
6
|
import { customFieldEntityConfigSchema, upsertCustomFieldDefSchema } from '@open-mercato/core/modules/entities/data/validators'
|
|
7
7
|
import { z } from 'zod'
|
|
8
|
+
import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
9
|
+
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
8
10
|
import { invalidateDefinitionsCache } from './definitions.cache'
|
|
9
11
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
10
12
|
import { mergeEntityFieldsetConfig, normalizeEntityFieldsetConfig } from '../lib/fieldsets'
|
|
13
|
+
import { resolveEntityDefinitionsVersion } from '../lib/definitions-version'
|
|
11
14
|
import {
|
|
12
15
|
beginEntitiesMutationGuard,
|
|
13
16
|
FIELD_DEFINITION_RESOURCE_KIND,
|
|
@@ -91,6 +94,31 @@ export async function POST(req: Request) {
|
|
|
91
94
|
})
|
|
92
95
|
if (guard.blockedResponse) return guard.blockedResponse
|
|
93
96
|
|
|
97
|
+
// Optimistic locking (issue #3152): the batch upserts a whole definition set,
|
|
98
|
+
// so guard the entity's aggregate schema version (the newest updated_at across
|
|
99
|
+
// its field definitions and fieldset config). A stale save — another tab changed
|
|
100
|
+
// the schema after this form loaded — fails with the same structured 409 the CRUD
|
|
101
|
+
// path returns. Strictly additive: callers that do not send the expected version
|
|
102
|
+
// header pass through unchanged.
|
|
103
|
+
const currentVersion = await resolveEntityDefinitionsVersion(em, {
|
|
104
|
+
entityId,
|
|
105
|
+
tenantId: scope.tenantId,
|
|
106
|
+
organizationId: scope.organizationId,
|
|
107
|
+
})
|
|
108
|
+
try {
|
|
109
|
+
enforceCommandOptimisticLock({
|
|
110
|
+
resourceKind: FIELD_DEFINITION_RESOURCE_KIND,
|
|
111
|
+
resourceId: entityId,
|
|
112
|
+
current: currentVersion,
|
|
113
|
+
request: req,
|
|
114
|
+
})
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (isCrudHttpError(err) && err.status === 409) {
|
|
117
|
+
return NextResponse.json(err.body, { status: err.status })
|
|
118
|
+
}
|
|
119
|
+
throw err
|
|
120
|
+
}
|
|
121
|
+
|
|
94
122
|
await em.begin()
|
|
95
123
|
try {
|
|
96
124
|
// Prefetch every existing definition for this entity in a single query, then index
|
|
@@ -206,11 +234,20 @@ export async function POST(req: Request) {
|
|
|
206
234
|
entityIds: [entityId],
|
|
207
235
|
})
|
|
208
236
|
|
|
209
|
-
|
|
237
|
+
// Return the post-save version so the form can round-trip the token for a
|
|
238
|
+
// subsequent save (reorder, delete, second save) without a false conflict.
|
|
239
|
+
const nextVersion = await resolveEntityDefinitionsVersion(em, {
|
|
240
|
+
entityId,
|
|
241
|
+
tenantId: scope.tenantId,
|
|
242
|
+
organizationId: scope.organizationId,
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
return NextResponse.json({ ok: true, version: nextVersion })
|
|
210
246
|
}
|
|
211
247
|
|
|
212
248
|
const batchResponseSchema = z.object({
|
|
213
249
|
ok: z.literal(true),
|
|
250
|
+
version: z.string().nullable().optional(),
|
|
214
251
|
})
|
|
215
252
|
|
|
216
253
|
export const openApi: OpenApiRouteDoc = {
|
|
@@ -5,6 +5,8 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
|
5
5
|
import { CustomFieldDef } from '@open-mercato/core/modules/entities/data/entities'
|
|
6
6
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
7
7
|
import { loadEntityFieldsetConfigs } from '../lib/fieldsets'
|
|
8
|
+
import { resolveDefinitionMutationScope } from '../lib/definition-scope'
|
|
9
|
+
import { resolveEntityDefinitionsVersion } from '../lib/definitions-version'
|
|
8
10
|
|
|
9
11
|
export const metadata = {
|
|
10
12
|
GET: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },
|
|
@@ -17,8 +19,12 @@ export async function GET(req: Request) {
|
|
|
17
19
|
const auth = await getAuthFromRequest(req)
|
|
18
20
|
if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
19
21
|
|
|
20
|
-
const
|
|
22
|
+
const container = await createRequestContainer()
|
|
23
|
+
const { resolve } = container
|
|
21
24
|
const em = resolve('em') as any
|
|
25
|
+
// Resolve the same definition scope the mutating endpoints use so the version
|
|
26
|
+
// token round-trips consistently (issue #3152).
|
|
27
|
+
const versionScope = await resolveDefinitionMutationScope({ auth, container, request: req })
|
|
22
28
|
// Load all scoped records (active/inactive/deleted) so that per-scope tombstones
|
|
23
29
|
// can shadow global definitions. We'll filter out deleted winners later.
|
|
24
30
|
const [defs, tombstones, configMap] = await Promise.all([
|
|
@@ -84,11 +90,17 @@ export async function GET(req: Request) {
|
|
|
84
90
|
}))
|
|
85
91
|
const deletedKeys = Array.from(tombstonedKeys)
|
|
86
92
|
const cfg = configMap.get(entityId) ?? { fieldsets: [], singleFieldsetPerRecord: true }
|
|
93
|
+
const version = await resolveEntityDefinitionsVersion(em, {
|
|
94
|
+
entityId,
|
|
95
|
+
tenantId: versionScope.tenantId,
|
|
96
|
+
organizationId: versionScope.organizationId,
|
|
97
|
+
})
|
|
87
98
|
return NextResponse.json({
|
|
88
99
|
items,
|
|
89
100
|
deletedKeys,
|
|
90
101
|
fieldsets: cfg.fieldsets,
|
|
91
102
|
settings: { singleFieldsetPerRecord: cfg.singleFieldsetPerRecord },
|
|
103
|
+
version,
|
|
92
104
|
})
|
|
93
105
|
}
|
|
94
106
|
|
|
@@ -125,6 +137,7 @@ const definitionsManageResponseSchema = z.object({
|
|
|
125
137
|
deletedKeys: z.array(z.string()),
|
|
126
138
|
fieldsets: z.array(manageFieldsetSchema).optional(),
|
|
127
139
|
settings: z.object({ singleFieldsetPerRecord: z.boolean().optional() }).optional(),
|
|
140
|
+
version: z.string().nullable().optional(),
|
|
128
141
|
})
|
|
129
142
|
|
|
130
143
|
export const openApi: OpenApiRouteDoc = {
|