@open-mercato/core 0.6.7-develop.6725.1.07a748cfc2 → 0.6.7-develop.6744.1.0483b18565
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/generated/entities/resources_resource/index.js +2 -0
- package/dist/generated/entities/resources_resource/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/salesFixtures.js +31 -2
- package/dist/helpers/integration/salesFixtures.js.map +2 -2
- package/dist/helpers/integration/salesUi.js +5 -0
- package/dist/helpers/integration/salesUi.js.map +2 -2
- package/dist/modules/communication_channels/data/enrichers.js +4 -4
- package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
- package/dist/modules/customers/analytics.js +1 -0
- package/dist/modules/customers/analytics.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js +17 -3
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +7 -1
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +2 -1
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/DealsSection.js +1 -1
- package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
- package/dist/modules/customers/data/validators.js +3 -1
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js +52 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +42 -26
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js +34 -12
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js.map +2 -2
- package/dist/modules/dictionaries/api/openapi.js +15 -2
- package/dist/modules/dictionaries/api/openapi.js.map +2 -2
- package/dist/modules/dictionaries/components/hooks/useDictionaryEntries.js +30 -29
- package/dist/modules/dictionaries/components/hooks/useDictionaryEntries.js.map +2 -2
- package/dist/modules/dictionaries/data/validators.js +7 -0
- package/dist/modules/dictionaries/data/validators.js.map +2 -2
- package/dist/modules/dictionaries/lib/clientEntries.js +4 -3
- package/dist/modules/dictionaries/lib/clientEntries.js.map +2 -2
- package/dist/modules/dictionaries/lib/fetchAllEntries.js +43 -0
- package/dist/modules/dictionaries/lib/fetchAllEntries.js.map +7 -0
- package/dist/modules/entities/api/definitions.js +51 -3
- package/dist/modules/entities/api/definitions.js.map +2 -2
- package/dist/modules/entities/api/entities.js +21 -2
- package/dist/modules/entities/api/entities.js.map +2 -2
- package/dist/modules/entities/api/records.js +3 -24
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/api/sidebar-entities.js +34 -12
- package/dist/modules/entities/api/sidebar-entities.js.map +2 -2
- package/dist/modules/entities/lib/entityAcl.js +46 -0
- package/dist/modules/entities/lib/entityAcl.js.map +2 -2
- package/dist/modules/messages/api/route.js +2 -5
- package/dist/modules/messages/api/route.js.map +2 -2
- package/dist/modules/messages/lib/participantScope.js +15 -0
- package/dist/modules/messages/lib/participantScope.js.map +7 -0
- package/dist/modules/planner/components/unavailabilityReasons.js +3 -3
- package/dist/modules/planner/components/unavailabilityReasons.js.map +2 -2
- package/dist/modules/progress/lib/progressService.js +2 -0
- package/dist/modules/progress/lib/progressService.js.map +2 -2
- package/dist/modules/progress/lib/progressServiceImpl.js +322 -104
- package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
- package/dist/modules/resources/api/resources.js +2 -0
- package/dist/modules/resources/api/resources.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js +3 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
- package/dist/modules/resources/commands/resources.js +9 -0
- package/dist/modules/resources/commands/resources.js.map +2 -2
- package/dist/modules/resources/components/detail/dictionaries.js +3 -3
- package/dist/modules/resources/components/detail/dictionaries.js.map +2 -2
- package/dist/modules/resources/data/entities.js +3 -0
- package/dist/modules/resources/data/entities.js.map +2 -2
- package/dist/modules/resources/data/validators.js +6 -2
- package/dist/modules/resources/data/validators.js.map +2 -2
- package/dist/modules/resources/lib/seeds.js +16 -9
- package/dist/modules/resources/lib/seeds.js.map +2 -2
- package/dist/modules/resources/migrations/Migration20260608231000.js +13 -0
- package/dist/modules/resources/migrations/Migration20260608231000.js.map +7 -0
- package/dist/modules/sales/backend/sales/documents/create/page.js +11 -14
- package/dist/modules/sales/backend/sales/documents/create/page.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +8 -0
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +18 -9
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +12 -5
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentForm.js +48 -2
- package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js +167 -0
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js.map +7 -0
- package/dist/modules/sales/data/validators.js +5 -1
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/staff/components/detail/dictionaries.js +3 -3
- package/dist/modules/staff/components/detail/dictionaries.js.map +2 -2
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +11 -4
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +2 -2
- package/generated/entities/resources_resource/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/helpers/integration/salesFixtures.ts +34 -2
- package/src/helpers/integration/salesUi.ts +8 -0
- package/src/modules/communication_channels/data/enrichers.ts +6 -26
- package/src/modules/customers/analytics.ts +1 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.tsx +35 -3
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +10 -1
- package/src/modules/customers/components/detail/DealForm.tsx +5 -1
- package/src/modules/customers/components/detail/DealsSection.tsx +1 -1
- package/src/modules/customers/data/validators.ts +5 -1
- package/src/modules/dashboards/i18n/de.json +3 -0
- package/src/modules/dashboards/i18n/en.json +3 -0
- package/src/modules/dashboards/i18n/es.json +3 -0
- package/src/modules/dashboards/i18n/pl.json +3 -0
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/config.ts +86 -0
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +33 -18
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.ts +2 -2
- package/src/modules/dictionaries/api/[dictionaryId]/entries/route.ts +39 -11
- package/src/modules/dictionaries/api/openapi.ts +19 -0
- package/src/modules/dictionaries/components/hooks/useDictionaryEntries.ts +35 -36
- package/src/modules/dictionaries/data/validators.ts +14 -0
- package/src/modules/dictionaries/lib/clientEntries.ts +4 -3
- package/src/modules/dictionaries/lib/fetchAllEntries.ts +72 -0
- package/src/modules/entities/api/definitions.ts +51 -3
- package/src/modules/entities/api/entities.ts +30 -2
- package/src/modules/entities/api/records.ts +3 -27
- package/src/modules/entities/api/sidebar-entities.ts +43 -14
- package/src/modules/entities/lib/entityAcl.ts +58 -0
- package/src/modules/messages/api/route.ts +4 -5
- package/src/modules/messages/lib/participantScope.ts +64 -0
- package/src/modules/planner/components/unavailabilityReasons.ts +3 -3
- package/src/modules/progress/AGENTS.md +11 -1
- package/src/modules/progress/lib/progressService.ts +1 -0
- package/src/modules/progress/lib/progressServiceImpl.ts +385 -117
- package/src/modules/resources/api/resources.ts +2 -0
- package/src/modules/resources/backend/resources/resources/[id]/page.tsx +9 -3
- package/src/modules/resources/commands/resources.ts +10 -0
- package/src/modules/resources/components/detail/dictionaries.ts +3 -3
- package/src/modules/resources/data/entities.ts +3 -0
- package/src/modules/resources/data/validators.ts +5 -0
- package/src/modules/resources/lib/seeds.ts +16 -9
- package/src/modules/resources/migrations/.snapshot-open-mercato.json +17 -1
- package/src/modules/resources/migrations/Migration20260608231000.ts +13 -0
- package/src/modules/sales/backend/sales/documents/create/page.tsx +2 -4
- package/src/modules/sales/commands/documents.ts +8 -0
- package/src/modules/sales/components/documents/ItemsSection.tsx +19 -10
- package/src/modules/sales/components/documents/LineItemDialog.tsx +13 -5
- package/src/modules/sales/components/documents/SalesDocumentForm.tsx +48 -6
- package/src/modules/sales/components/documents/SalesOrderDraftLines.tsx +198 -0
- package/src/modules/sales/data/validators.ts +7 -1
- package/src/modules/sales/i18n/de.json +2 -0
- package/src/modules/sales/i18n/en.json +2 -0
- package/src/modules/sales/i18n/es.json +2 -0
- package/src/modules/sales/i18n/pl.json +2 -0
- package/src/modules/staff/components/detail/dictionaries.ts +3 -3
- package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +12 -5
|
@@ -1,16 +1,66 @@
|
|
|
1
1
|
import { isValidDateRangePreset } from "@open-mercato/ui/backend/date-range";
|
|
2
|
+
const CLOSED_DEAL_STATUSES = ["win", "loose", "won", "lost", "closed"];
|
|
3
|
+
const OPEN_DEAL_CLOSURE_OUTCOME_FILTER = {
|
|
4
|
+
field: "closureOutcome",
|
|
5
|
+
operator: "is_null"
|
|
6
|
+
};
|
|
7
|
+
const PIPELINE_STATUS_SCOPES = ["open", "all"];
|
|
2
8
|
const DEFAULT_SETTINGS = {
|
|
3
|
-
dateRange: "this_month"
|
|
9
|
+
dateRange: "this_month",
|
|
10
|
+
statusScope: "open"
|
|
4
11
|
};
|
|
12
|
+
function isValidStatusScope(value) {
|
|
13
|
+
return typeof value === "string" && PIPELINE_STATUS_SCOPES.includes(value);
|
|
14
|
+
}
|
|
5
15
|
function hydrateSettings(raw) {
|
|
6
16
|
if (!raw || typeof raw !== "object") return { ...DEFAULT_SETTINGS };
|
|
7
17
|
const obj = raw;
|
|
8
18
|
return {
|
|
9
|
-
dateRange: isValidDateRangePreset(obj.dateRange) ? obj.dateRange : DEFAULT_SETTINGS.dateRange
|
|
19
|
+
dateRange: isValidDateRangePreset(obj.dateRange) ? obj.dateRange : DEFAULT_SETTINGS.dateRange,
|
|
20
|
+
statusScope: isValidStatusScope(obj.statusScope) ? obj.statusScope : DEFAULT_SETTINGS.statusScope
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function dehydrateSettings(settings) {
|
|
24
|
+
return {
|
|
25
|
+
dateRange: settings.dateRange,
|
|
26
|
+
statusScope: settings.statusScope
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function buildPipelineDataRequest(settings) {
|
|
30
|
+
const request = {
|
|
31
|
+
entityType: "customers:deals",
|
|
32
|
+
metric: {
|
|
33
|
+
field: "valueAmount",
|
|
34
|
+
aggregate: "sum"
|
|
35
|
+
},
|
|
36
|
+
groupBy: {
|
|
37
|
+
field: "pipelineStage",
|
|
38
|
+
resolveLabels: true
|
|
39
|
+
},
|
|
40
|
+
dateRange: {
|
|
41
|
+
field: "createdAt",
|
|
42
|
+
preset: settings.dateRange
|
|
43
|
+
}
|
|
10
44
|
};
|
|
45
|
+
if (settings.statusScope === "open") {
|
|
46
|
+
request.filters = [
|
|
47
|
+
...CLOSED_DEAL_STATUSES.map((status) => ({
|
|
48
|
+
field: "status",
|
|
49
|
+
operator: "neq",
|
|
50
|
+
value: status
|
|
51
|
+
})),
|
|
52
|
+
{ ...OPEN_DEAL_CLOSURE_OUTCOME_FILTER }
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
return request;
|
|
11
56
|
}
|
|
12
57
|
export {
|
|
58
|
+
CLOSED_DEAL_STATUSES,
|
|
13
59
|
DEFAULT_SETTINGS,
|
|
60
|
+
OPEN_DEAL_CLOSURE_OUTCOME_FILTER,
|
|
61
|
+
PIPELINE_STATUS_SCOPES,
|
|
62
|
+
buildPipelineDataRequest,
|
|
63
|
+
dehydrateSettings,
|
|
14
64
|
hydrateSettings
|
|
15
65
|
};
|
|
16
66
|
//# sourceMappingURL=config.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/dashboards/widgets/dashboard/pipeline-summary/config.ts"],
|
|
4
|
-
"sourcesContent": ["import { type DateRangePreset, isValidDateRangePreset } from '@open-mercato/ui/backend/date-range'\n\nexport type PipelineSummarySettings = {\n dateRange: DateRangePreset\n}\n\nexport const DEFAULT_SETTINGS: PipelineSummarySettings = {\n dateRange: 'this_month',\n}\n\nexport function hydrateSettings(raw: unknown): PipelineSummarySettings {\n if (!raw || typeof raw !== 'object') return { ...DEFAULT_SETTINGS }\n const obj = raw as Record<string, unknown>\n return {\n dateRange: isValidDateRangePreset(obj.dateRange) ? obj.dateRange : DEFAULT_SETTINGS.dateRange,\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAA+B,8BAA8B;
|
|
4
|
+
"sourcesContent": ["import { type DateRangePreset, isValidDateRangePreset } from '@open-mercato/ui/backend/date-range'\nimport type { WidgetDataRequest } from '../../../services/widgetDataService'\n\n// Deal statuses that mean the deal is closed and must stop counting as current pipeline.\n//\n// This is a denylist, not an allowlist: `customer_deals.status` is a lenient `text` column\n// fed by the per-tenant `deal_status` dictionary, so a status unknown to code counts as OPEN\n// and keeps contributing to the chart. That keeps tenant-specific stages visible and mirrors\n// `customers/lib/interactionStatus.ts`, which treats an unknown interaction status as open.\n//\n// Every vocabulary that reaches the column has to be listed here:\n// - `win` / `loose` are written by the deal closure UI and the kanban board.\n// - `won` / `lost` are written verbatim by the `customers.update_deal_stage` AI tool, whose\n// free-form `toStage` is passed straight through to `status`.\n// - `closed` is a seeded `deal_status` dictionary value, persisted by the dashboards\n// analytics seed and treated as terminal by the demo-data generator.\nexport const CLOSED_DEAL_STATUSES = ['win', 'loose', 'won', 'lost', 'closed'] as const\n\n// A deal is also closed when `closure_outcome` records the outcome, which the deals-summary\n// KPI already treats as terminal: `api/deals/summary/route.ts` counts a deal as won or lost\n// when EITHER `status` OR `closure_outcome` says so. The two columns can disagree \u2014 the CRUD\n// API accepts `closureOutcome` on its own, and the AI tool writes `status` without ever setting\n// it \u2014 so the chart has to test both signals to agree with those KPI cards (#4668).\n//\n// The `closure_outcome` side is expressed as `IS NULL` rather than as a `neq` denylist for two\n// reasons, and swapping it for `neq` would be a silent regression:\n// - `closure_outcome` is nullable, and every OPEN deal holds NULL there. `neq` renders as\n// `column != ?`, and in SQL `NULL != 'won'` is NULL, not true \u2014 a `neq` filter would drop\n// every open deal and empty the chart. `status` has no such problem: it is `text not null\n// default 'open'`, which is why the status side stays a denylist.\n// - Unlike `status`, this column is not fed by a per-tenant dictionary. Every write path\n// validates it against the same closed `z.enum(['won', 'lost'])` (`data/validators.ts`,\n// `api/deals/[id]/route.ts`, `api/deals/[id]/stats/route.ts`), so a non-null value always\n// means closed and there is no tenant-specific vocabulary to keep visible.\nexport const OPEN_DEAL_CLOSURE_OUTCOME_FILTER = {\n field: 'closureOutcome',\n operator: 'is_null',\n} as const\n\nexport const PIPELINE_STATUS_SCOPES = ['open', 'all'] as const\n\nexport type PipelineStatusScope = typeof PIPELINE_STATUS_SCOPES[number]\n\nexport type PipelineSummarySettings = {\n dateRange: DateRangePreset\n statusScope: PipelineStatusScope\n}\n\nexport const DEFAULT_SETTINGS: PipelineSummarySettings = {\n dateRange: 'this_month',\n statusScope: 'open',\n}\n\nfunction isValidStatusScope(value: unknown): value is PipelineStatusScope {\n return typeof value === 'string' && (PIPELINE_STATUS_SCOPES as readonly string[]).includes(value)\n}\n\nexport function hydrateSettings(raw: unknown): PipelineSummarySettings {\n if (!raw || typeof raw !== 'object') return { ...DEFAULT_SETTINGS }\n const obj = raw as Record<string, unknown>\n return {\n dateRange: isValidDateRangePreset(obj.dateRange) ? obj.dateRange : DEFAULT_SETTINGS.dateRange,\n statusScope: isValidStatusScope(obj.statusScope) ? obj.statusScope : DEFAULT_SETTINGS.statusScope,\n }\n}\n\nexport function dehydrateSettings(settings: PipelineSummarySettings): Record<string, unknown> {\n return {\n dateRange: settings.dateRange,\n statusScope: settings.statusScope,\n }\n}\n\nexport function buildPipelineDataRequest(settings: PipelineSummarySettings): WidgetDataRequest {\n const request: WidgetDataRequest = {\n entityType: 'customers:deals',\n metric: {\n field: 'valueAmount',\n aggregate: 'sum',\n },\n groupBy: {\n field: 'pipelineStage',\n resolveLabels: true,\n },\n dateRange: {\n field: 'createdAt',\n preset: settings.dateRange,\n },\n }\n\n if (settings.statusScope === 'open') {\n request.filters = [\n ...CLOSED_DEAL_STATUSES.map((status) => ({\n field: 'status',\n operator: 'neq' as const,\n value: status,\n })),\n { ...OPEN_DEAL_CLOSURE_OUTCOME_FILTER },\n ]\n }\n\n return request\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAA+B,8BAA8B;AAgBtD,MAAM,uBAAuB,CAAC,OAAO,SAAS,OAAO,QAAQ,QAAQ;AAkBrE,MAAM,mCAAmC;AAAA,EAC9C,OAAO;AAAA,EACP,UAAU;AACZ;AAEO,MAAM,yBAAyB,CAAC,QAAQ,KAAK;AAS7C,MAAM,mBAA4C;AAAA,EACvD,WAAW;AAAA,EACX,aAAa;AACf;AAEA,SAAS,mBAAmB,OAA8C;AACxE,SAAO,OAAO,UAAU,YAAa,uBAA6C,SAAS,KAAK;AAClG;AAEO,SAAS,gBAAgB,KAAuC;AACrE,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO,EAAE,GAAG,iBAAiB;AAClE,QAAM,MAAM;AACZ,SAAO;AAAA,IACL,WAAW,uBAAuB,IAAI,SAAS,IAAI,IAAI,YAAY,iBAAiB;AAAA,IACpF,aAAa,mBAAmB,IAAI,WAAW,IAAI,IAAI,cAAc,iBAAiB;AAAA,EACxF;AACF;AAEO,SAAS,kBAAkB,UAA4D;AAC5F,SAAO;AAAA,IACL,WAAW,SAAS;AAAA,IACpB,aAAa,SAAS;AAAA,EACxB;AACF;AAEO,SAAS,yBAAyB,UAAsD;AAC7F,QAAM,UAA6B;AAAA,IACjC,YAAY;AAAA,IACZ,QAAQ;AAAA,MACN,OAAO;AAAA,MACP,WAAW;AAAA,IACb;AAAA,IACA,SAAS;AAAA,MACP,OAAO;AAAA,MACP,eAAe;AAAA,IACjB;AAAA,IACA,WAAW;AAAA,MACT,OAAO;AAAA,MACP,QAAQ,SAAS;AAAA,IACnB;AAAA,EACF;AAEA,MAAI,SAAS,gBAAgB,QAAQ;AACnC,YAAQ,UAAU;AAAA,MAChB,GAAG,qBAAqB,IAAI,CAAC,YAAY;AAAA,QACvC,OAAO;AAAA,QACP,UAAU;AAAA,QACV,OAAO;AAAA,MACT,EAAE;AAAA,MACF,EAAE,GAAG,iCAAiC;AAAA,IACxC;AAAA,EACF;AAEA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -8,27 +8,17 @@ import {
|
|
|
8
8
|
DateRangeSelect,
|
|
9
9
|
InlineDateRangeSelect
|
|
10
10
|
} from "@open-mercato/ui/backend/date-range";
|
|
11
|
-
import {
|
|
11
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@open-mercato/ui/primitives/select";
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_SETTINGS,
|
|
14
|
+
buildPipelineDataRequest,
|
|
15
|
+
hydrateSettings
|
|
16
|
+
} from "./config.js";
|
|
12
17
|
import { formatCurrencyCompact } from "../../../lib/formatters.js";
|
|
13
18
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
14
19
|
const logger = createLogger("dashboards").child({ component: "pipeline-summary" });
|
|
15
20
|
async function fetchPipelineData(settings, fetchWidgetData) {
|
|
16
|
-
|
|
17
|
-
entityType: "customers:deals",
|
|
18
|
-
metric: {
|
|
19
|
-
field: "valueAmount",
|
|
20
|
-
aggregate: "sum"
|
|
21
|
-
},
|
|
22
|
-
groupBy: {
|
|
23
|
-
field: "pipelineStage",
|
|
24
|
-
resolveLabels: true
|
|
25
|
-
},
|
|
26
|
-
dateRange: {
|
|
27
|
-
field: "createdAt",
|
|
28
|
-
preset: settings.dateRange
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
return fetchWidgetData(body);
|
|
21
|
+
return fetchWidgetData(buildPipelineDataRequest(settings));
|
|
32
22
|
}
|
|
33
23
|
function formatStageLabel(stage, t) {
|
|
34
24
|
if (stage == null || stage === "") return t("dashboards.analytics.labels.unknown", "Unknown");
|
|
@@ -75,15 +65,41 @@ const PipelineSummaryWidget = ({
|
|
|
75
65
|
});
|
|
76
66
|
}, [refresh, refreshToken]);
|
|
77
67
|
if (mode === "settings") {
|
|
78
|
-
return /* @__PURE__ */
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
68
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4 text-sm", children: [
|
|
69
|
+
/* @__PURE__ */ jsx(
|
|
70
|
+
DateRangeSelect,
|
|
71
|
+
{
|
|
72
|
+
id: "pipeline-summary-date-range",
|
|
73
|
+
label: t("dashboards.analytics.settings.dateRange", "Date Range"),
|
|
74
|
+
value: hydrated.dateRange,
|
|
75
|
+
onChange: (dateRange) => onSettingsChange({ ...hydrated, dateRange })
|
|
76
|
+
}
|
|
77
|
+
),
|
|
78
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
79
|
+
/* @__PURE__ */ jsx(
|
|
80
|
+
"label",
|
|
81
|
+
{
|
|
82
|
+
htmlFor: "pipeline-summary-status-scope",
|
|
83
|
+
className: "text-xs font-semibold uppercase text-muted-foreground",
|
|
84
|
+
children: t("dashboards.analytics.settings.dealStatusScope", "Deals included")
|
|
85
|
+
}
|
|
86
|
+
),
|
|
87
|
+
/* @__PURE__ */ jsxs(
|
|
88
|
+
Select,
|
|
89
|
+
{
|
|
90
|
+
value: hydrated.statusScope,
|
|
91
|
+
onValueChange: (value) => onSettingsChange({ ...hydrated, statusScope: value }),
|
|
92
|
+
children: [
|
|
93
|
+
/* @__PURE__ */ jsx(SelectTrigger, { id: "pipeline-summary-status-scope", size: "sm", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
94
|
+
/* @__PURE__ */ jsxs(SelectContent, { children: [
|
|
95
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "open", children: t("dashboards.analytics.settings.dealStatusScopeOpen", "Open deals only") }),
|
|
96
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "all", children: t("dashboards.analytics.settings.dealStatusScopeAll", "All deals, including won and lost") })
|
|
97
|
+
] })
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
] })
|
|
102
|
+
] });
|
|
87
103
|
}
|
|
88
104
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
|
|
89
105
|
/* @__PURE__ */ jsx("div", { className: "flex justify-end mb-2", children: /* @__PURE__ */ jsx(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { BarChart, type BarChartDataItem } from '@open-mercato/ui/backend/charts'\nimport {\n DateRangeSelect,\n InlineDateRangeSelect,\n type DateRangePreset,\n} from '@open-mercato/ui/backend/date-range'\nimport {
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { BarChart, type BarChartDataItem } from '@open-mercato/ui/backend/charts'\nimport {\n DateRangeSelect,\n InlineDateRangeSelect,\n type DateRangePreset,\n} from '@open-mercato/ui/backend/date-range'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@open-mercato/ui/primitives/select'\nimport {\n DEFAULT_SETTINGS,\n buildPipelineDataRequest,\n hydrateSettings,\n type PipelineStatusScope,\n type PipelineSummarySettings,\n} from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\nimport { formatCurrencyCompact } from '../../../lib/formatters'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dashboards').child({ component: 'pipeline-summary' })\n\nasync function fetchPipelineData(settings: PipelineSummarySettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {\n return fetchWidgetData<WidgetDataResponse>(buildPipelineDataRequest(settings))\n}\n\nfunction formatStageLabel(stage: unknown, t: (key: string, fallback: string) => string): string {\n if (stage == null || stage === '') return t('dashboards.analytics.labels.unknown', 'Unknown')\n const stageStr = String(stage)\n if (stageStr === '0' || stageStr === 'null' || stageStr === 'undefined') {\n return t('dashboards.analytics.labels.unknown', 'Unknown')\n }\n return stageStr\n .replace(/_/g, ' ')\n .replace(/\\b\\w/g, (l) => l.toUpperCase())\n}\n\nconst PipelineSummaryWidget: React.FC<DashboardWidgetComponentProps<PipelineSummarySettings>> = ({\n mode,\n settings = DEFAULT_SETTINGS,\n onSettingsChange,\n refreshToken,\n onRefreshStateChange,\n}) => {\n const t = useT()\n const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])\n const [data, setData] = React.useState<BarChartDataItem[]>([])\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n\n const fetchWidgetData = useWidgetData()\n const refresh = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n const result = await fetchPipelineData(hydrated, fetchWidgetData)\n const chartData = result.data\n .filter((item) => item.groupKey != null && item.groupKey !== '' && String(item.groupKey) !== '0')\n .map((item) => ({\n stage: formatStageLabel(item.groupLabel ?? item.groupKey, t),\n Value: item.value ?? 0,\n }))\n setData(chartData)\n } catch (err) {\n logger.error('Failed to load pipeline data', { err })\n setError(t('dashboards.analytics.widgets.pipelineSummary.error', 'Failed to load data'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n }\n }, [hydrated, fetchWidgetData, onRefreshStateChange, t])\n\n React.useEffect(() => {\n refresh().catch(() => {})\n }, [refresh, refreshToken])\n\n if (mode === 'settings') {\n return (\n <div className=\"space-y-4 text-sm\">\n <DateRangeSelect\n id=\"pipeline-summary-date-range\"\n label={t('dashboards.analytics.settings.dateRange', 'Date Range')}\n value={hydrated.dateRange}\n onChange={(dateRange: DateRangePreset) => onSettingsChange({ ...hydrated, dateRange })}\n />\n <div className=\"space-y-1.5\">\n <label\n htmlFor=\"pipeline-summary-status-scope\"\n className=\"text-xs font-semibold uppercase text-muted-foreground\"\n >\n {t('dashboards.analytics.settings.dealStatusScope', 'Deals included')}\n </label>\n <Select\n value={hydrated.statusScope}\n onValueChange={(value) => onSettingsChange({ ...hydrated, statusScope: value as PipelineStatusScope })}\n >\n <SelectTrigger id=\"pipeline-summary-status-scope\" size=\"sm\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"open\">\n {t('dashboards.analytics.settings.dealStatusScopeOpen', 'Open deals only')}\n </SelectItem>\n <SelectItem value=\"all\">\n {t('dashboards.analytics.settings.dealStatusScopeAll', 'All deals, including won and lost')}\n </SelectItem>\n </SelectContent>\n </Select>\n </div>\n </div>\n )\n }\n\n return (\n <div className=\"flex flex-col h-full\">\n <div className=\"flex justify-end mb-2\">\n <InlineDateRangeSelect\n value={hydrated.dateRange}\n onChange={(dateRange) => onSettingsChange({ ...hydrated, dateRange })}\n />\n </div>\n <div className=\"flex-1 min-h-0\">\n <BarChart\n data={data}\n index=\"stage\"\n categories={['Value']}\n categoryLabels={{ Value: t('dashboards.analytics.labels.value', 'Value') }}\n loading={loading}\n error={error}\n valueFormatter={formatCurrencyCompact}\n colors={['violet']}\n showLegend={false}\n emptyMessage={t('dashboards.analytics.widgets.pipelineSummary.empty', 'No deal data for this period')}\n />\n </div>\n </div>\n )\n}\n\nexport default PipelineSummaryWidget\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { lazyDashboardWidget } from "@open-mercato/shared/modules/dashboard/widgets";
|
|
2
|
-
import { DEFAULT_SETTINGS, hydrateSettings } from "./config.js";
|
|
2
|
+
import { DEFAULT_SETTINGS, dehydrateSettings, hydrateSettings } from "./config.js";
|
|
3
3
|
const PipelineSummaryWidget = lazyDashboardWidget(() => import("./widget.client.js"));
|
|
4
4
|
const widget = {
|
|
5
5
|
metadata: {
|
|
@@ -17,7 +17,7 @@ const widget = {
|
|
|
17
17
|
},
|
|
18
18
|
Widget: PipelineSummaryWidget,
|
|
19
19
|
hydrateSettings,
|
|
20
|
-
dehydrateSettings
|
|
20
|
+
dehydrateSettings
|
|
21
21
|
};
|
|
22
22
|
var widget_default = widget;
|
|
23
23
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.ts"],
|
|
4
|
-
"sourcesContent": ["import { lazyDashboardWidget, type DashboardWidgetModule } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { DEFAULT_SETTINGS, hydrateSettings, type PipelineSummarySettings } from './config'\nconst PipelineSummaryWidget = lazyDashboardWidget(() => import('./widget.client'))\n\nconst widget: DashboardWidgetModule<PipelineSummarySettings> = {\n metadata: {\n id: 'dashboards.analytics.pipelineSummary',\n title: 'Pipeline Summary',\n description: 'Deal value by pipeline stage',\n features: ['analytics.view', 'customers.deals.view'],\n defaultSize: 'md',\n defaultEnabled: false,\n defaultSettings: DEFAULT_SETTINGS,\n tags: ['analytics', 'customers', 'deals', 'chart'],\n category: 'analytics',\n icon: 'git-branch',\n supportsRefresh: true,\n },\n Widget: PipelineSummaryWidget,\n hydrateSettings,\n dehydrateSettings
|
|
5
|
-
"mappings": "AAAA,SAAS,2BAAuD;AAChE,SAAS,kBAAkB,uBAAqD;
|
|
4
|
+
"sourcesContent": ["import { lazyDashboardWidget, type DashboardWidgetModule } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { DEFAULT_SETTINGS, dehydrateSettings, hydrateSettings, type PipelineSummarySettings } from './config'\nconst PipelineSummaryWidget = lazyDashboardWidget(() => import('./widget.client'))\n\nconst widget: DashboardWidgetModule<PipelineSummarySettings> = {\n metadata: {\n id: 'dashboards.analytics.pipelineSummary',\n title: 'Pipeline Summary',\n description: 'Deal value by pipeline stage',\n features: ['analytics.view', 'customers.deals.view'],\n defaultSize: 'md',\n defaultEnabled: false,\n defaultSettings: DEFAULT_SETTINGS,\n tags: ['analytics', 'customers', 'deals', 'chart'],\n category: 'analytics',\n icon: 'git-branch',\n supportsRefresh: true,\n },\n Widget: PipelineSummaryWidget,\n hydrateSettings,\n dehydrateSettings,\n}\n\nexport default widget\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,2BAAuD;AAChE,SAAS,kBAAkB,mBAAmB,uBAAqD;AACnG,MAAM,wBAAwB,oBAAoB,MAAM,OAAO,iBAAiB,CAAC;AAEjF,MAAM,SAAyD;AAAA,EAC7D,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,aAAa;AAAA,IACb,UAAU,CAAC,kBAAkB,sBAAsB;AAAA,IACnD,aAAa;AAAA,IACb,gBAAgB;AAAA,IAChB,iBAAiB;AAAA,IACjB,MAAM,CAAC,aAAa,aAAa,SAAS,OAAO;AAAA,IACjD,UAAU;AAAA,IACV,MAAM;AAAA,IACN,iBAAiB;AAAA,EACnB;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AACF;AAEA,IAAO,iBAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -3,7 +3,11 @@ import { z } from "zod";
|
|
|
3
3
|
import { runWithCacheTenant } from "@open-mercato/cache";
|
|
4
4
|
import { Dictionary, DictionaryEntry } from "@open-mercato/core/modules/dictionaries/data/entities";
|
|
5
5
|
import { resolveDictionariesRouteContext, resolveDictionaryActorId } from "@open-mercato/core/modules/dictionaries/api/context";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
createDictionaryEntrySchema,
|
|
8
|
+
listDictionaryEntriesQuerySchema,
|
|
9
|
+
DICTIONARY_ENTRIES_MAX_LIMIT
|
|
10
|
+
} from "@open-mercato/core/modules/dictionaries/data/validators";
|
|
7
11
|
import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
8
12
|
import {
|
|
9
13
|
runCrudMutationGuardAfterSuccess,
|
|
@@ -12,6 +16,7 @@ import {
|
|
|
12
16
|
import { serializeOperationMetadata } from "@open-mercato/shared/lib/commands/operationMetadata";
|
|
13
17
|
import {
|
|
14
18
|
createDictionaryEntrySchema as createEntryDocSchema,
|
|
19
|
+
dictionaryEntriesQuerySchema,
|
|
15
20
|
dictionaryEntryListResponseSchema,
|
|
16
21
|
dictionaryEntryResponseSchema,
|
|
17
22
|
dictionaryIdParamsSchema,
|
|
@@ -36,7 +41,7 @@ const DICTIONARY_ENTRY_RESOURCE = "dictionaries.entry";
|
|
|
36
41
|
const DICTIONARY_DEFINITION_RESOURCE = "dictionaries.dictionary";
|
|
37
42
|
const DICTIONARY_ENTRIES_TTL_MS = 5 * 6e4;
|
|
38
43
|
function buildEntriesCacheKey(params) {
|
|
39
|
-
return `dictionaries:entries:${params.dictionaryId}:org=${params.organizationId ?? "null"}:sort=${params.sortMode}`;
|
|
44
|
+
return `dictionaries:entries:${params.dictionaryId}:org=${params.organizationId ?? "null"}:sort=${params.sortMode}:limit=${params.limit}:offset=${params.offset}`;
|
|
40
45
|
}
|
|
41
46
|
async function loadDictionary(context, id, options = {}) {
|
|
42
47
|
const { allowInherited = false } = options;
|
|
@@ -72,27 +77,35 @@ async function GET(req, ctx) {
|
|
|
72
77
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
73
78
|
}
|
|
74
79
|
const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId });
|
|
80
|
+
const requestUrl = new URL(req.url);
|
|
81
|
+
const { limit, offset } = listDictionaryEntriesQuerySchema.parse({
|
|
82
|
+
limit: requestUrl.searchParams.get("limit") ?? void 0,
|
|
83
|
+
offset: requestUrl.searchParams.get("offset") ?? void 0
|
|
84
|
+
});
|
|
75
85
|
const dictionary = await loadDictionary(context, dictionaryId, { allowInherited: true });
|
|
76
86
|
const dictionaryTenantId = dictionary.tenantId;
|
|
77
87
|
const dictionaryOrgId = dictionary.organizationId ?? null;
|
|
78
88
|
const sortMode = resolveDictionaryEntrySortMode(dictionary.entrySortMode);
|
|
79
|
-
const
|
|
80
|
-
const
|
|
89
|
+
const isDefaultPage = limit === DICTIONARY_ENTRIES_MAX_LIMIT && offset === 0;
|
|
90
|
+
const cache = isCrudCacheEnabled() && isDefaultPage ? resolveCrudCache(context.container) : null;
|
|
91
|
+
const cacheKey = cache ? buildEntriesCacheKey({ dictionaryId, organizationId: dictionaryOrgId, sortMode, limit, offset }) : null;
|
|
81
92
|
if (cache && cacheKey) {
|
|
82
93
|
const cached = await runWithCacheTenant(dictionaryTenantId, () => cache.get(cacheKey));
|
|
83
94
|
if (cached) {
|
|
84
95
|
return NextResponse.json(cached);
|
|
85
96
|
}
|
|
86
97
|
}
|
|
98
|
+
const entriesFilter = {
|
|
99
|
+
dictionary,
|
|
100
|
+
organizationId: dictionary.organizationId,
|
|
101
|
+
tenantId: dictionary.tenantId
|
|
102
|
+
};
|
|
103
|
+
const total = await context.em.count(DictionaryEntry, entriesFilter);
|
|
87
104
|
const entries = await findWithDecryption(
|
|
88
105
|
context.em,
|
|
89
106
|
DictionaryEntry,
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
organizationId: dictionary.organizationId,
|
|
93
|
-
tenantId: dictionary.tenantId
|
|
94
|
-
},
|
|
95
|
-
{},
|
|
107
|
+
entriesFilter,
|
|
108
|
+
{ orderBy: { position: "asc", id: "asc" }, limit, offset },
|
|
96
109
|
{ tenantId: dictionary.tenantId, organizationId: dictionary.organizationId }
|
|
97
110
|
);
|
|
98
111
|
const sortedEntries = sortDictionaryEntries(entries, sortMode);
|
|
@@ -107,7 +120,15 @@ async function GET(req, ctx) {
|
|
|
107
120
|
isDefault: entry.isDefault ?? false,
|
|
108
121
|
createdAt: entry.createdAt,
|
|
109
122
|
updatedAt: entry.updatedAt
|
|
110
|
-
}))
|
|
123
|
+
})),
|
|
124
|
+
total,
|
|
125
|
+
limit,
|
|
126
|
+
offset,
|
|
127
|
+
hasMore: offset + sortedEntries.length < total,
|
|
128
|
+
// Page membership comes from the database ordering, while the page itself
|
|
129
|
+
// is ordered in memory. A client assembling several pages has to re-apply
|
|
130
|
+
// the same comparator, so it needs the mode that produced this ordering.
|
|
131
|
+
sortMode
|
|
111
132
|
};
|
|
112
133
|
if (cache && cacheKey) {
|
|
113
134
|
try {
|
|
@@ -231,8 +252,9 @@ async function POST(req, ctx) {
|
|
|
231
252
|
}
|
|
232
253
|
const dictionaryEntriesGetDoc = {
|
|
233
254
|
summary: "List dictionary entries",
|
|
234
|
-
description:
|
|
255
|
+
description: `Returns entries for the specified dictionary ordered by its configured entry sort mode. The number of entries returned per request is capped at ${DICTIONARY_ENTRIES_MAX_LIMIT}; use \`limit\` and \`offset\` with the \`total\`/\`hasMore\` response fields to page through larger dictionaries.`,
|
|
235
256
|
tags: [dictionariesTag],
|
|
257
|
+
query: dictionaryEntriesQuerySchema,
|
|
236
258
|
responses: [
|
|
237
259
|
{ status: 200, description: "Dictionary entries.", schema: dictionaryEntryListResponseSchema }
|
|
238
260
|
],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/dictionaries/api/%5BdictionaryId%5D/entries/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport { resolveDictionariesRouteContext, resolveDictionaryActorId } from '@open-mercato/core/modules/dictionaries/api/context'\nimport { createDictionaryEntrySchema } from '@open-mercato/core/modules/dictionaries/data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n createDictionaryEntrySchema as createEntryDocSchema,\n dictionaryEntryListResponseSchema,\n dictionaryEntryResponseSchema,\n dictionaryIdParamsSchema,\n dictionariesErrorSchema,\n dictionariesTag,\n} from '../../openapi'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n buildCollectionTags,\n buildRecordTag,\n isCrudCacheEnabled,\n resolveCrudCache,\n} from '@open-mercato/shared/lib/crud/cache'\nimport {\n resolveDictionaryEntrySortMode,\n sortDictionaryEntries,\n} from '@open-mercato/core/modules/dictionaries/lib/entrySort'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dictionaries').child({ component: 'entries-api' })\n\nconst paramsSchema = z.object({ dictionaryId: z.string().uuid() })\n\nconst DICTIONARY_ENTRY_RESOURCE = 'dictionaries.entry'\nconst DICTIONARY_DEFINITION_RESOURCE = 'dictionaries.dictionary'\nconst DICTIONARY_ENTRIES_TTL_MS = 5 * 60_000\n\nfunction buildEntriesCacheKey(params: {\n dictionaryId: string\n organizationId: string | null\n sortMode: string\n}): string {\n return `dictionaries:entries:${params.dictionaryId}:org=${params.organizationId ?? 'null'}:sort=${params.sortMode}`\n}\n\nasync function loadDictionary(\n context: Awaited<ReturnType<typeof resolveDictionariesRouteContext>>,\n id: string,\n options: { allowInherited?: boolean } = {},\n) {\n const { allowInherited = false } = options\n if (!allowInherited && !context.organizationId) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.organization_required', 'Organization context is required') })\n }\n const baseFilter = {\n id,\n tenantId: context.tenantId,\n deletedAt: null,\n }\n const filter = allowInherited\n ? {\n ...baseFilter,\n ...(context.readableOrganizationIds.length\n ? { organizationId: { $in: context.readableOrganizationIds } }\n : {}),\n }\n : {\n ...baseFilter,\n organizationId: context.organizationId,\n }\n const dictionary = await context.em.findOne(Dictionary, filter)\n if (!dictionary) {\n throw new CrudHttpError(404, { error: context.translate('dictionaries.errors.not_found', 'Dictionary not found') })\n }\n return dictionary\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['dictionaries.view'] },\n POST: { requireAuth: true, requireFeatures: ['dictionaries.manage'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { dictionaryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n if (!context.auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })\n const dictionary = await loadDictionary(context, dictionaryId, { allowInherited: true })\n const dictionaryTenantId = dictionary.tenantId\n const dictionaryOrgId = dictionary.organizationId ?? null\n const sortMode = resolveDictionaryEntrySortMode(dictionary.entrySortMode)\n\n // Dictionary CF selects hit this on every CrudForm open, so cache the\n // decrypted+sorted options payload. The entry writes flow through the\n // command bus (resourceKind dictionaries.entry), which already flushes the\n // matching collection tag post-commit \u2014 no new invalidation wiring needed.\n const cache = isCrudCacheEnabled() ? resolveCrudCache(context.container) : null\n const cacheKey = cache\n ? buildEntriesCacheKey({ dictionaryId, organizationId: dictionaryOrgId, sortMode })\n : null\n\n if (cache && cacheKey) {\n const cached = await runWithCacheTenant(dictionaryTenantId, () => cache.get(cacheKey))\n if (cached) {\n return NextResponse.json(cached)\n }\n }\n\n const entries = await findWithDecryption(\n context.em,\n DictionaryEntry,\n {\n dictionary,\n organizationId: dictionary.organizationId,\n tenantId: dictionary.tenantId,\n },\n {},\n { tenantId: dictionary.tenantId, organizationId: dictionary.organizationId },\n )\n const sortedEntries = sortDictionaryEntries(entries, sortMode)\n\n const payload = {\n items: sortedEntries.map((entry) => ({\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n position: entry.position ?? 0,\n isDefault: entry.isDefault ?? false,\n createdAt: entry.createdAt,\n updatedAt: entry.updatedAt,\n })),\n }\n\n if (cache && cacheKey) {\n try {\n await runWithCacheTenant(dictionaryTenantId, () =>\n cache.set(cacheKey, payload, {\n ttl: DICTIONARY_ENTRIES_TTL_MS,\n tags: [\n ...buildCollectionTags(DICTIONARY_ENTRY_RESOURCE, dictionaryTenantId, [dictionaryOrgId]),\n buildRecordTag(DICTIONARY_DEFINITION_RESOURCE, dictionaryTenantId, dictionaryId),\n ],\n }),\n )\n } catch {}\n }\n\n return NextResponse.json(payload)\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid input', details: err.issues }, { status: 400 })\n }\n logger.error('Failed to load dictionary entries', { err })\n return NextResponse.json({ error: 'Failed to load dictionary entries' }, { status: 500 })\n }\n}\n\nexport async function POST(req: Request, ctx: { params?: { dictionaryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n if (!context.auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })\n const payload = createDictionaryEntrySchema.parse(await req.json().catch(() => ({})))\n const guardUserId = resolveDictionaryActorId(context.auth)\n const guardResult = await validateCrudMutationGuard(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: DICTIONARY_ENTRY_RESOURCE,\n resourceId: dictionaryId,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: payload,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n // These nested routes do not use makeCrudRoute, so we invoke the command bus directly.\n const commandBus = (context.container.resolve('commandBus') as CommandBus)\n const { result, logEntry } = await commandBus.execute('dictionaries.entries.create', {\n input: { ...payload, dictionaryId },\n ctx: context.ctx,\n })\n const createResult = (result ?? {}) as { entryId?: string | null }\n const createdEntryId = typeof createResult.entryId === 'string' ? createResult.entryId : null\n if (!createdEntryId) {\n throw new CrudHttpError(500, { error: context.translate('dictionaries.errors.entry_create_failed', 'Failed to create dictionary entry') })\n }\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: DICTIONARY_ENTRY_RESOURCE,\n resourceId: createdEntryId,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n const entry = await findOneWithDecryption(\n context.em.fork(),\n DictionaryEntry,\n createdEntryId,\n { populate: ['dictionary'] },\n { tenantId: context.auth.tenantId ?? null, organizationId: context.auth.orgId ?? null },\n )\n if (!entry) {\n throw new CrudHttpError(500, { error: context.translate('dictionaries.errors.entry_create_failed', 'Failed to create dictionary entry') })\n }\n const response = NextResponse.json({\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n position: entry.position ?? 0,\n isDefault: entry.isDefault ?? false,\n createdAt: entry.createdAt,\n updatedAt: entry.updatedAt,\n }, { status: 201 })\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'dictionaries.entry',\n resourceId: createdEntryId,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid input', details: err.issues }, { status: 400 })\n }\n logger.error('Failed to create dictionary entry', { err })\n return NextResponse.json({ error: 'Failed to create dictionary entry' }, { status: 500 })\n }\n}\n\nconst dictionaryEntriesGetDoc: OpenApiMethodDoc = {\n summary: 'List dictionary entries',\n description: 'Returns entries for the specified dictionary ordered by its configured entry sort mode.',\n tags: [dictionariesTag],\n responses: [\n { status: 200, description: 'Dictionary entries.', schema: dictionaryEntryListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid parameters', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to load dictionary entries', schema: dictionariesErrorSchema },\n ],\n}\n\nconst dictionaryEntriesPostDoc: OpenApiMethodDoc = {\n summary: 'Create dictionary entry',\n description: 'Creates a new entry in the specified dictionary.',\n tags: [dictionariesTag],\n requestBody: {\n contentType: 'application/json',\n schema: createEntryDocSchema,\n description: 'Entry value, label, and optional presentation metadata.',\n },\n responses: [\n { status: 201, description: 'Dictionary entry created.', schema: dictionaryEntryResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to create dictionary entry', schema: dictionariesErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dictionariesTag,\n summary: 'Dictionary entries collection',\n pathParams: dictionaryIdParamsSchema,\n methods: {\n GET: dictionaryEntriesGetDoc,\n POST: dictionaryEntriesPostDoc,\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,YAAY,uBAAuB;AAC5C,SAAS,iCAAiC,gCAAgC;AAC1E,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport { resolveDictionariesRouteContext, resolveDictionaryActorId } from '@open-mercato/core/modules/dictionaries/api/context'\nimport {\n createDictionaryEntrySchema,\n listDictionaryEntriesQuerySchema,\n DICTIONARY_ENTRIES_MAX_LIMIT,\n} from '@open-mercato/core/modules/dictionaries/data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n createDictionaryEntrySchema as createEntryDocSchema,\n dictionaryEntriesQuerySchema,\n dictionaryEntryListResponseSchema,\n dictionaryEntryResponseSchema,\n dictionaryIdParamsSchema,\n dictionariesErrorSchema,\n dictionariesTag,\n} from '../../openapi'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n buildCollectionTags,\n buildRecordTag,\n isCrudCacheEnabled,\n resolveCrudCache,\n} from '@open-mercato/shared/lib/crud/cache'\nimport {\n resolveDictionaryEntrySortMode,\n sortDictionaryEntries,\n} from '@open-mercato/core/modules/dictionaries/lib/entrySort'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dictionaries').child({ component: 'entries-api' })\n\nconst paramsSchema = z.object({ dictionaryId: z.string().uuid() })\n\nconst DICTIONARY_ENTRY_RESOURCE = 'dictionaries.entry'\nconst DICTIONARY_DEFINITION_RESOURCE = 'dictionaries.dictionary'\nconst DICTIONARY_ENTRIES_TTL_MS = 5 * 60_000\n\nfunction buildEntriesCacheKey(params: {\n dictionaryId: string\n organizationId: string | null\n sortMode: string\n limit: number\n offset: number\n}): string {\n return `dictionaries:entries:${params.dictionaryId}:org=${params.organizationId ?? 'null'}:sort=${params.sortMode}:limit=${params.limit}:offset=${params.offset}`\n}\n\nasync function loadDictionary(\n context: Awaited<ReturnType<typeof resolveDictionariesRouteContext>>,\n id: string,\n options: { allowInherited?: boolean } = {},\n) {\n const { allowInherited = false } = options\n if (!allowInherited && !context.organizationId) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.organization_required', 'Organization context is required') })\n }\n const baseFilter = {\n id,\n tenantId: context.tenantId,\n deletedAt: null,\n }\n const filter = allowInherited\n ? {\n ...baseFilter,\n ...(context.readableOrganizationIds.length\n ? { organizationId: { $in: context.readableOrganizationIds } }\n : {}),\n }\n : {\n ...baseFilter,\n organizationId: context.organizationId,\n }\n const dictionary = await context.em.findOne(Dictionary, filter)\n if (!dictionary) {\n throw new CrudHttpError(404, { error: context.translate('dictionaries.errors.not_found', 'Dictionary not found') })\n }\n return dictionary\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['dictionaries.view'] },\n POST: { requireAuth: true, requireFeatures: ['dictionaries.manage'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { dictionaryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n if (!context.auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })\n const requestUrl = new URL(req.url)\n const { limit, offset } = listDictionaryEntriesQuerySchema.parse({\n limit: requestUrl.searchParams.get('limit') ?? undefined,\n offset: requestUrl.searchParams.get('offset') ?? undefined,\n })\n const dictionary = await loadDictionary(context, dictionaryId, { allowInherited: true })\n const dictionaryTenantId = dictionary.tenantId\n const dictionaryOrgId = dictionary.organizationId ?? null\n const sortMode = resolveDictionaryEntrySortMode(dictionary.entrySortMode)\n\n // Dictionary CF selects hit this on every CrudForm open, so cache the\n // decrypted+sorted options payload. The entry writes flow through the\n // command bus (resourceKind dictionaries.entry), which already flushes the\n // matching collection tag post-commit \u2014 no new invalidation wiring needed.\n // Only the default page is cached: keying the cache on caller-supplied\n // pagination would let any reader mint unbounded distinct cache entries.\n const isDefaultPage = limit === DICTIONARY_ENTRIES_MAX_LIMIT && offset === 0\n const cache = isCrudCacheEnabled() && isDefaultPage ? resolveCrudCache(context.container) : null\n const cacheKey = cache\n ? buildEntriesCacheKey({ dictionaryId, organizationId: dictionaryOrgId, sortMode, limit, offset })\n : null\n\n if (cache && cacheKey) {\n const cached = await runWithCacheTenant(dictionaryTenantId, () => cache.get(cacheKey))\n if (cached) {\n return NextResponse.json(cached)\n }\n }\n\n const entriesFilter = {\n dictionary,\n organizationId: dictionary.organizationId,\n tenantId: dictionary.tenantId,\n }\n const total = await context.em.count(DictionaryEntry, entriesFilter)\n // Bound the decrypt+sort work per request: page membership is decided by a\n // stable database ordering, and the dictionary's sort mode orders the page.\n const entries = await findWithDecryption(\n context.em,\n DictionaryEntry,\n entriesFilter,\n { orderBy: { position: 'asc', id: 'asc' }, limit, offset },\n { tenantId: dictionary.tenantId, organizationId: dictionary.organizationId },\n )\n const sortedEntries = sortDictionaryEntries(entries, sortMode)\n\n const payload = {\n items: sortedEntries.map((entry) => ({\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n position: entry.position ?? 0,\n isDefault: entry.isDefault ?? false,\n createdAt: entry.createdAt,\n updatedAt: entry.updatedAt,\n })),\n total,\n limit,\n offset,\n hasMore: offset + sortedEntries.length < total,\n // Page membership comes from the database ordering, while the page itself\n // is ordered in memory. A client assembling several pages has to re-apply\n // the same comparator, so it needs the mode that produced this ordering.\n sortMode,\n }\n\n if (cache && cacheKey) {\n try {\n await runWithCacheTenant(dictionaryTenantId, () =>\n cache.set(cacheKey, payload, {\n ttl: DICTIONARY_ENTRIES_TTL_MS,\n tags: [\n ...buildCollectionTags(DICTIONARY_ENTRY_RESOURCE, dictionaryTenantId, [dictionaryOrgId]),\n buildRecordTag(DICTIONARY_DEFINITION_RESOURCE, dictionaryTenantId, dictionaryId),\n ],\n }),\n )\n } catch {}\n }\n\n return NextResponse.json(payload)\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid input', details: err.issues }, { status: 400 })\n }\n logger.error('Failed to load dictionary entries', { err })\n return NextResponse.json({ error: 'Failed to load dictionary entries' }, { status: 500 })\n }\n}\n\nexport async function POST(req: Request, ctx: { params?: { dictionaryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n if (!context.auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })\n const payload = createDictionaryEntrySchema.parse(await req.json().catch(() => ({})))\n const guardUserId = resolveDictionaryActorId(context.auth)\n const guardResult = await validateCrudMutationGuard(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: DICTIONARY_ENTRY_RESOURCE,\n resourceId: dictionaryId,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: payload,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n // These nested routes do not use makeCrudRoute, so we invoke the command bus directly.\n const commandBus = (context.container.resolve('commandBus') as CommandBus)\n const { result, logEntry } = await commandBus.execute('dictionaries.entries.create', {\n input: { ...payload, dictionaryId },\n ctx: context.ctx,\n })\n const createResult = (result ?? {}) as { entryId?: string | null }\n const createdEntryId = typeof createResult.entryId === 'string' ? createResult.entryId : null\n if (!createdEntryId) {\n throw new CrudHttpError(500, { error: context.translate('dictionaries.errors.entry_create_failed', 'Failed to create dictionary entry') })\n }\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: DICTIONARY_ENTRY_RESOURCE,\n resourceId: createdEntryId,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n const entry = await findOneWithDecryption(\n context.em.fork(),\n DictionaryEntry,\n createdEntryId,\n { populate: ['dictionary'] },\n { tenantId: context.auth.tenantId ?? null, organizationId: context.auth.orgId ?? null },\n )\n if (!entry) {\n throw new CrudHttpError(500, { error: context.translate('dictionaries.errors.entry_create_failed', 'Failed to create dictionary entry') })\n }\n const response = NextResponse.json({\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n position: entry.position ?? 0,\n isDefault: entry.isDefault ?? false,\n createdAt: entry.createdAt,\n updatedAt: entry.updatedAt,\n }, { status: 201 })\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'dictionaries.entry',\n resourceId: createdEntryId,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid input', details: err.issues }, { status: 400 })\n }\n logger.error('Failed to create dictionary entry', { err })\n return NextResponse.json({ error: 'Failed to create dictionary entry' }, { status: 500 })\n }\n}\n\nconst dictionaryEntriesGetDoc: OpenApiMethodDoc = {\n summary: 'List dictionary entries',\n description: `Returns entries for the specified dictionary ordered by its configured entry sort mode. The number of entries returned per request is capped at ${DICTIONARY_ENTRIES_MAX_LIMIT}; use \\`limit\\` and \\`offset\\` with the \\`total\\`/\\`hasMore\\` response fields to page through larger dictionaries.`,\n tags: [dictionariesTag],\n query: dictionaryEntriesQuerySchema,\n responses: [\n { status: 200, description: 'Dictionary entries.', schema: dictionaryEntryListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid parameters', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to load dictionary entries', schema: dictionariesErrorSchema },\n ],\n}\n\nconst dictionaryEntriesPostDoc: OpenApiMethodDoc = {\n summary: 'Create dictionary entry',\n description: 'Creates a new entry in the specified dictionary.',\n tags: [dictionariesTag],\n requestBody: {\n contentType: 'application/json',\n schema: createEntryDocSchema,\n description: 'Entry value, label, and optional presentation metadata.',\n },\n responses: [\n { status: 201, description: 'Dictionary entry created.', schema: dictionaryEntryResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to create dictionary entry', schema: dictionariesErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dictionariesTag,\n summary: 'Dictionary entries collection',\n pathParams: dictionaryIdParamsSchema,\n methods: {\n GET: dictionaryEntriesGetDoc,\n POST: dictionaryEntriesPostDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
createDictionaryEntrySchema,
|
|
5
5
|
updateDictionaryEntrySchema,
|
|
6
6
|
reorderDictionaryEntriesSchema,
|
|
7
|
-
setDefaultDictionaryEntrySchema
|
|
7
|
+
setDefaultDictionaryEntrySchema,
|
|
8
|
+
DICTIONARY_ENTRIES_MAX_LIMIT
|
|
8
9
|
} from "@open-mercato/core/modules/dictionaries/data/validators";
|
|
9
10
|
import { dictionaryEntrySortModeSchema } from "@open-mercato/core/modules/dictionaries/lib/entrySort";
|
|
10
11
|
const dictionariesTag = "Dictionaries";
|
|
@@ -47,7 +48,18 @@ const dictionaryEntrySchema = z.object({
|
|
|
47
48
|
updatedAt: z.string().nullable()
|
|
48
49
|
});
|
|
49
50
|
const dictionaryEntryListResponseSchema = z.object({
|
|
50
|
-
items: z.array(dictionaryEntrySchema)
|
|
51
|
+
items: z.array(dictionaryEntrySchema),
|
|
52
|
+
total: z.number().int(),
|
|
53
|
+
limit: z.number().int(),
|
|
54
|
+
offset: z.number().int(),
|
|
55
|
+
hasMore: z.boolean(),
|
|
56
|
+
sortMode: dictionaryEntrySortModeSchema.describe(
|
|
57
|
+
"Sort mode that ordered this page. Clients assembling multiple pages must re-apply it across the combined set."
|
|
58
|
+
)
|
|
59
|
+
});
|
|
60
|
+
const dictionaryEntriesQuerySchema = z.object({
|
|
61
|
+
limit: z.coerce.number().int().positive().max(DICTIONARY_ENTRIES_MAX_LIMIT).optional().describe(`Maximum entries to return. Defaults to and is capped at ${DICTIONARY_ENTRIES_MAX_LIMIT}.`),
|
|
62
|
+
offset: z.coerce.number().int().min(0).optional().describe("Number of entries to skip. Defaults to 0.")
|
|
51
63
|
});
|
|
52
64
|
const dictionaryEntryResponseSchema = dictionaryEntrySchema;
|
|
53
65
|
const dictionaryIdParamsSchema = z.object({
|
|
@@ -73,6 +85,7 @@ export {
|
|
|
73
85
|
dictionariesOkSchema,
|
|
74
86
|
dictionariesTag,
|
|
75
87
|
dictionaryDetailSchema,
|
|
88
|
+
dictionaryEntriesQuerySchema,
|
|
76
89
|
dictionaryEntryListResponseSchema,
|
|
77
90
|
dictionaryEntryParamsSchema,
|
|
78
91
|
dictionaryEntryResponseSchema,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/dictionaries/api/openapi.ts"],
|
|
4
|
-
"sourcesContent": ["import { z } from 'zod'\nimport {\n upsertDictionarySchema,\n createDictionaryEntrySchema,\n updateDictionaryEntrySchema,\n reorderDictionaryEntriesSchema,\n setDefaultDictionaryEntrySchema,\n} from '@open-mercato/core/modules/dictionaries/data/validators'\nimport { dictionaryEntrySortModeSchema } from '@open-mercato/core/modules/dictionaries/lib/entrySort'\n\nexport const dictionariesTag = 'Dictionaries'\n\nexport const dictionariesErrorSchema = z.object({\n error: z.string(),\n}).passthrough()\n\nexport const dictionariesOkSchema = z.object({\n ok: z.literal(true),\n})\n\nexport const dictionaryListQuerySchema = z.object({\n includeInactive: z.enum(['true', 'false']).optional(),\n})\n\nexport const dictionarySchema = z.object({\n id: z.string().uuid(),\n key: z.string(),\n name: z.string(),\n description: z.string().nullable(),\n isSystem: z.boolean(),\n isActive: z.boolean(),\n managerVisibility: z.string().nullable().optional(),\n entrySortMode: dictionaryEntrySortModeSchema.optional(),\n organizationId: z.string().uuid().nullable(),\n isInherited: z.boolean().optional(),\n createdAt: z.string(),\n updatedAt: z.string().nullable(),\n})\n\nexport const dictionaryListResponseSchema = z.object({\n items: z.array(dictionarySchema),\n})\n\nexport const dictionaryDetailSchema = dictionarySchema\n\nexport const dictionaryEntrySchema = z.object({\n id: z.string().uuid(),\n value: z.string(),\n label: z.string(),\n color: z.string().nullable(),\n icon: z.string().nullable(),\n position: z.number(),\n isDefault: z.boolean(),\n createdAt: z.string(),\n updatedAt: z.string().nullable(),\n})\n\nexport const dictionaryEntryListResponseSchema = z.object({\n items: z.array(dictionaryEntrySchema),\n})\n\nexport const dictionaryEntryResponseSchema = dictionaryEntrySchema\n\nexport const dictionaryIdParamsSchema = z.object({\n dictionaryId: z.string().uuid(),\n})\n\nexport const dictionaryEntryParamsSchema = z.object({\n dictionaryId: z.string().uuid(),\n entryId: z.string().uuid(),\n})\n\nexport const dictionaryUpdateSchema = upsertDictionarySchema.partial()\n\nexport const reorderEntriesRequestSchema = z.object({\n entries: z.array(z.object({\n id: z.string().uuid(),\n position: z.number().int().min(0),\n })).min(1),\n})\n\nexport const setDefaultEntryRequestSchema = z.object({\n entryId: z.string().uuid(),\n})\n\nexport {\n upsertDictionarySchema,\n createDictionaryEntrySchema,\n updateDictionaryEntrySchema,\n reorderDictionaryEntriesSchema,\n setDefaultDictionaryEntrySchema,\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,SAAS;AAClB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,qCAAqC;AAEvC,MAAM,kBAAkB;AAExB,MAAM,0BAA0B,EAAE,OAAO;AAAA,EAC9C,OAAO,EAAE,OAAO;AAClB,CAAC,EAAE,YAAY;AAER,MAAM,uBAAuB,EAAE,OAAO;AAAA,EAC3C,IAAI,EAAE,QAAQ,IAAI;AACpB,CAAC;AAEM,MAAM,4BAA4B,EAAE,OAAO;AAAA,EAChD,iBAAiB,EAAE,KAAK,CAAC,QAAQ,OAAO,CAAC,EAAE,SAAS;AACtD,CAAC;AAEM,MAAM,mBAAmB,EAAE,OAAO;AAAA,EACvC,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,KAAK,EAAE,OAAO;AAAA,EACd,MAAM,EAAE,OAAO;AAAA,EACf,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,UAAU,EAAE,QAAQ;AAAA,EACpB,UAAU,EAAE,QAAQ;AAAA,EACpB,mBAAmB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EAClD,eAAe,8BAA8B,SAAS;AAAA,EACtD,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC3C,aAAa,EAAE,QAAQ,EAAE,SAAS;AAAA,EAClC,WAAW,EAAE,OAAO;AAAA,EACpB,WAAW,EAAE,OAAO,EAAE,SAAS;AACjC,CAAC;AAEM,MAAM,+BAA+B,EAAE,OAAO;AAAA,EACnD,OAAO,EAAE,MAAM,gBAAgB;AACjC,CAAC;AAEM,MAAM,yBAAyB;AAE/B,MAAM,wBAAwB,EAAE,OAAO;AAAA,EAC5C,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,OAAO,EAAE,OAAO;AAAA,EAChB,OAAO,EAAE,OAAO;AAAA,EAChB,OAAO,EAAE,OAAO,EAAE,SAAS;AAAA,EAC3B,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,EAC1B,UAAU,EAAE,OAAO;AAAA,EACnB,WAAW,EAAE,QAAQ;AAAA,EACrB,WAAW,EAAE,OAAO;AAAA,EACpB,WAAW,EAAE,OAAO,EAAE,SAAS;AACjC,CAAC;AAEM,MAAM,oCAAoC,EAAE,OAAO;AAAA,EACxD,OAAO,EAAE,MAAM,qBAAqB;
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport {\n upsertDictionarySchema,\n createDictionaryEntrySchema,\n updateDictionaryEntrySchema,\n reorderDictionaryEntriesSchema,\n setDefaultDictionaryEntrySchema,\n DICTIONARY_ENTRIES_MAX_LIMIT,\n} from '@open-mercato/core/modules/dictionaries/data/validators'\nimport { dictionaryEntrySortModeSchema } from '@open-mercato/core/modules/dictionaries/lib/entrySort'\n\nexport const dictionariesTag = 'Dictionaries'\n\nexport const dictionariesErrorSchema = z.object({\n error: z.string(),\n}).passthrough()\n\nexport const dictionariesOkSchema = z.object({\n ok: z.literal(true),\n})\n\nexport const dictionaryListQuerySchema = z.object({\n includeInactive: z.enum(['true', 'false']).optional(),\n})\n\nexport const dictionarySchema = z.object({\n id: z.string().uuid(),\n key: z.string(),\n name: z.string(),\n description: z.string().nullable(),\n isSystem: z.boolean(),\n isActive: z.boolean(),\n managerVisibility: z.string().nullable().optional(),\n entrySortMode: dictionaryEntrySortModeSchema.optional(),\n organizationId: z.string().uuid().nullable(),\n isInherited: z.boolean().optional(),\n createdAt: z.string(),\n updatedAt: z.string().nullable(),\n})\n\nexport const dictionaryListResponseSchema = z.object({\n items: z.array(dictionarySchema),\n})\n\nexport const dictionaryDetailSchema = dictionarySchema\n\nexport const dictionaryEntrySchema = z.object({\n id: z.string().uuid(),\n value: z.string(),\n label: z.string(),\n color: z.string().nullable(),\n icon: z.string().nullable(),\n position: z.number(),\n isDefault: z.boolean(),\n createdAt: z.string(),\n updatedAt: z.string().nullable(),\n})\n\nexport const dictionaryEntryListResponseSchema = z.object({\n items: z.array(dictionaryEntrySchema),\n total: z.number().int(),\n limit: z.number().int(),\n offset: z.number().int(),\n hasMore: z.boolean(),\n sortMode: dictionaryEntrySortModeSchema.describe(\n 'Sort mode that ordered this page. Clients assembling multiple pages must re-apply it across the combined set.',\n ),\n})\n\nexport const dictionaryEntriesQuerySchema = z.object({\n limit: z.coerce\n .number()\n .int()\n .positive()\n .max(DICTIONARY_ENTRIES_MAX_LIMIT)\n .optional()\n .describe(`Maximum entries to return. Defaults to and is capped at ${DICTIONARY_ENTRIES_MAX_LIMIT}.`),\n offset: z.coerce.number().int().min(0).optional().describe('Number of entries to skip. Defaults to 0.'),\n})\n\nexport const dictionaryEntryResponseSchema = dictionaryEntrySchema\n\nexport const dictionaryIdParamsSchema = z.object({\n dictionaryId: z.string().uuid(),\n})\n\nexport const dictionaryEntryParamsSchema = z.object({\n dictionaryId: z.string().uuid(),\n entryId: z.string().uuid(),\n})\n\nexport const dictionaryUpdateSchema = upsertDictionarySchema.partial()\n\nexport const reorderEntriesRequestSchema = z.object({\n entries: z.array(z.object({\n id: z.string().uuid(),\n position: z.number().int().min(0),\n })).min(1),\n})\n\nexport const setDefaultEntryRequestSchema = z.object({\n entryId: z.string().uuid(),\n})\n\nexport {\n upsertDictionarySchema,\n createDictionaryEntrySchema,\n updateDictionaryEntrySchema,\n reorderDictionaryEntriesSchema,\n setDefaultDictionaryEntrySchema,\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,47 +1,48 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
3
2
|
import { useQuery } from "@tanstack/react-query";
|
|
4
3
|
import {
|
|
5
4
|
createDictionaryMap,
|
|
6
5
|
normalizeDictionaryEntries
|
|
7
6
|
} from "../dictionaryAppearance.js";
|
|
7
|
+
import { fetchAllDictionaryEntries } from "../../lib/fetchAllEntries.js";
|
|
8
8
|
const DICTIONARY_ENTRIES_STALE_TIME = 5 * 60 * 1e3;
|
|
9
9
|
const BASE_DICTIONARY_ENTRIES_KEY = ["dictionaries", "entries"];
|
|
10
10
|
const dictionaryEntriesQueryKey = (dictionaryId, scopeVersion = 0) => [...BASE_DICTIONARY_ENTRIES_KEY, dictionaryId, `scope:${scopeVersion}`];
|
|
11
|
+
function parseDictionaryEntryItems(items) {
|
|
12
|
+
const parsed = [];
|
|
13
|
+
for (const item of items) {
|
|
14
|
+
if (!item || typeof item !== "object") continue;
|
|
15
|
+
const data = item;
|
|
16
|
+
const id = typeof data.id === "string" ? data.id : "";
|
|
17
|
+
const value = typeof data.value === "string" ? data.value.trim() : "";
|
|
18
|
+
if (!id || !value) continue;
|
|
19
|
+
const label = typeof data.label === "string" && data.label.trim().length ? data.label.trim() : value;
|
|
20
|
+
const color = typeof data.color === "string" && /^#([0-9a-fA-F]{6})$/.test(data.color) ? `#${data.color.slice(1).toLowerCase()}` : null;
|
|
21
|
+
const icon = typeof data.icon === "string" && data.icon.trim().length ? data.icon.trim() : null;
|
|
22
|
+
const createdAt = typeof data.createdAt === "string" ? data.createdAt : null;
|
|
23
|
+
const updatedAt = typeof data.updatedAt === "string" ? data.updatedAt : null;
|
|
24
|
+
parsed.push({
|
|
25
|
+
id,
|
|
26
|
+
value,
|
|
27
|
+
label,
|
|
28
|
+
color,
|
|
29
|
+
icon,
|
|
30
|
+
createdAt,
|
|
31
|
+
updatedAt
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
11
36
|
const dictionaryEntriesQueryOptions = (dictionaryId, scopeVersion = 0) => ({
|
|
12
37
|
queryKey: dictionaryEntriesQueryKey(dictionaryId, scopeVersion),
|
|
13
38
|
staleTime: DICTIONARY_ENTRIES_STALE_TIME,
|
|
14
39
|
gcTime: DICTIONARY_ENTRIES_STALE_TIME,
|
|
15
40
|
queryFn: async () => {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const message = typeof payload.error === "string" ? payload.error : "Failed to load dictionary entries.";
|
|
20
|
-
throw new Error(message);
|
|
21
|
-
}
|
|
22
|
-
const items = Array.isArray(payload.items) ? payload.items : [];
|
|
23
|
-
const parsed = [];
|
|
24
|
-
for (const item of items) {
|
|
25
|
-
if (!item || typeof item !== "object") continue;
|
|
26
|
-
const data = item;
|
|
27
|
-
const id = typeof data.id === "string" ? data.id : "";
|
|
28
|
-
const value = typeof data.value === "string" ? data.value.trim() : "";
|
|
29
|
-
if (!id || !value) continue;
|
|
30
|
-
const label = typeof data.label === "string" && data.label.trim().length ? data.label.trim() : value;
|
|
31
|
-
const color = typeof data.color === "string" && /^#([0-9a-fA-F]{6})$/.test(data.color) ? `#${data.color.slice(1).toLowerCase()}` : null;
|
|
32
|
-
const icon = typeof data.icon === "string" && data.icon.trim().length ? data.icon.trim() : null;
|
|
33
|
-
const createdAt = typeof data.createdAt === "string" ? data.createdAt : null;
|
|
34
|
-
const updatedAt = typeof data.updatedAt === "string" ? data.updatedAt : null;
|
|
35
|
-
parsed.push({
|
|
36
|
-
id,
|
|
37
|
-
value,
|
|
38
|
-
label,
|
|
39
|
-
color,
|
|
40
|
-
icon,
|
|
41
|
-
createdAt,
|
|
42
|
-
updatedAt
|
|
43
|
-
});
|
|
41
|
+
const pages = await fetchAllDictionaryEntries(dictionaryId);
|
|
42
|
+
if (!pages.ok) {
|
|
43
|
+
throw new Error(pages.errorMessage ?? "Failed to load dictionary entries.");
|
|
44
44
|
}
|
|
45
|
+
const parsed = parseDictionaryEntryItems(pages.items);
|
|
45
46
|
const normalized = normalizeDictionaryEntries(
|
|
46
47
|
parsed.map(({ value, label, color, icon }) => ({ value, label, color, icon })),
|
|
47
48
|
{ sort: false }
|