@open-mercato/core 0.6.7-develop.6766.1.40c9011995 → 0.6.7-develop.6770.1.b52298cf20
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/frontend/login.js +3 -0
- package/dist/modules/auth/frontend/login.js.map +2 -2
- package/dist/modules/customers/api/deals/summary/route.js +9 -115
- package/dist/modules/customers/api/deals/summary/route.js.map +2 -2
- package/dist/modules/customers/lib/dealsSummaryQueries.js +144 -0
- package/dist/modules/customers/lib/dealsSummaryQueries.js.map +7 -0
- package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js +7 -16
- package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/frontend/login.tsx +3 -0
- package/src/modules/customers/api/deals/summary/route.ts +10 -158
- package/src/modules/customers/lib/dealsSummaryQueries.ts +211 -0
- package/src/modules/directory/backend/directory/organizations/[id]/edit/page.tsx +10 -14
- package/src/modules/directory/i18n/de.json +1 -0
- package/src/modules/directory/i18n/en.json +1 -0
- package/src/modules/directory/i18n/es.json +1 -0
- package/src/modules/directory/i18n/pl.json +1 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/customers/lib/dealsSummaryQueries.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager as CoreEntityManager } from '@mikro-orm/core'\nimport type { EntityManager as PgEntityManager } from '@mikro-orm/postgresql'\nimport { fetchStuckDealIds } from './stuckDeals'\n\ntype DateWindow = {\n start: Date\n end: Date\n}\n\nexport type OpenPipelineRow = {\n stage: string | null\n currency: string | null\n total: string | number | null\n count: string | number\n owner_user_id: string | null\n}\n\nexport type WindowSumRow = {\n currency: string | null\n current_total: string | number | null\n current_count: string | number\n previous_total: string | number | null\n previous_count: string | number\n}\n\nexport type WinLossRow = {\n current_won: string | number\n current_lost: string | number\n previous_won: string | number\n previous_lost: string | number\n}\n\nexport type WinRateMonthRow = {\n period: string\n won: string | number\n lost: string | number\n}\n\nexport type DealsSummaryQueryRows = {\n openRows: OpenPipelineRow[]\n inflowRows: WindowSumRow[]\n wonRows: WindowSumRow[]\n winLossRows: WinLossRow[]\n seriesRows: WinRateMonthRow[]\n overdueRows: Array<{ id: string }>\n openStuckIds: string[]\n}\n\nexport type DealsSummaryQueryInput = {\n em: CoreEntityManager\n tenantId: string\n organizationIds: string[]\n currentQuarter: DateWindow\n previousQuarter: DateWindow\n seriesStart: Date\n}\n\ntype SqlPredicate = {\n clause: string\n values: string[]\n}\n\nexport function buildOpenDealPredicate(statuses: readonly string[]): SqlPredicate {\n if (statuses.length === 0) {\n throw new Error('[internal] Open-deal predicate requires at least one status')\n }\n\n const placeholders = statuses.map(() => '?').join(',')\n return {\n clause: `status IN (${placeholders}) AND closure_outcome IS NULL`,\n values: [...statuses],\n }\n}\n\nexport async function loadDealsSummaryQueryRows(input: DealsSummaryQueryInput): Promise<DealsSummaryQueryRows> {\n if (input.organizationIds.length === 0) {\n throw new Error('[internal] Deals summary query requires an organization scope')\n }\n\n const connection = input.em.getConnection()\n const organizationPlaceholders = input.organizationIds.map(() => '?').join(',')\n const scopeWhere = `tenant_id = ? AND organization_id IN (${organizationPlaceholders}) AND deleted_at IS NULL`\n const scopeValues: Array<string | number | null> = [input.tenantId, ...input.organizationIds]\n const openDealsPredicate = buildOpenDealPredicate(['open', 'in_progress'])\n\n const openRows = await connection.execute<OpenPipelineRow[]>(\n `SELECT\n pipeline_stage AS stage,\n UPPER(COALESCE(value_currency, '')) AS currency,\n COALESCE(SUM(value_amount), 0) AS total,\n COUNT(*) AS count,\n owner_user_id\n FROM customer_deals\n WHERE ${scopeWhere} AND ${openDealsPredicate.clause}\n GROUP BY pipeline_stage, UPPER(COALESCE(value_currency, '')), owner_user_id`,\n [...scopeValues, ...openDealsPredicate.values],\n )\n\n const inflowRows = await connection.execute<WindowSumRow[]>(\n `SELECT\n UPPER(COALESCE(value_currency, '')) AS currency,\n COALESCE(SUM(value_amount) FILTER (WHERE created_at >= ? AND created_at < ?), 0) AS current_total,\n COUNT(*) FILTER (WHERE created_at >= ? AND created_at < ?) AS current_count,\n COALESCE(SUM(value_amount) FILTER (WHERE created_at >= ? AND created_at < ?), 0) AS previous_total,\n COUNT(*) FILTER (WHERE created_at >= ? AND created_at < ?) AS previous_count\n FROM customer_deals\n WHERE ${scopeWhere} AND ${openDealsPredicate.clause}\n GROUP BY UPPER(COALESCE(value_currency, ''))`,\n [\n input.currentQuarter.start.toISOString(),\n input.currentQuarter.end.toISOString(),\n input.currentQuarter.start.toISOString(),\n input.currentQuarter.end.toISOString(),\n input.previousQuarter.start.toISOString(),\n input.previousQuarter.end.toISOString(),\n input.previousQuarter.start.toISOString(),\n input.previousQuarter.end.toISOString(),\n ...scopeValues,\n ...openDealsPredicate.values,\n ],\n )\n\n const wonRows = await connection.execute<WindowSumRow[]>(\n `SELECT\n UPPER(COALESCE(value_currency, '')) AS currency,\n COALESCE(SUM(value_amount) FILTER (WHERE updated_at >= ? AND updated_at < ?), 0) AS current_total,\n COUNT(*) FILTER (WHERE updated_at >= ? AND updated_at < ?) AS current_count,\n COALESCE(SUM(value_amount) FILTER (WHERE updated_at >= ? AND updated_at < ?), 0) AS previous_total,\n COUNT(*) FILTER (WHERE updated_at >= ? AND updated_at < ?) AS previous_count\n FROM customer_deals\n WHERE ${scopeWhere} AND (status = 'win' OR closure_outcome = 'won')\n GROUP BY UPPER(COALESCE(value_currency, ''))`,\n [\n input.currentQuarter.start.toISOString(),\n input.currentQuarter.end.toISOString(),\n input.currentQuarter.start.toISOString(),\n input.currentQuarter.end.toISOString(),\n input.previousQuarter.start.toISOString(),\n input.previousQuarter.end.toISOString(),\n input.previousQuarter.start.toISOString(),\n input.previousQuarter.end.toISOString(),\n ...scopeValues,\n ],\n )\n\n const winLossRows = await connection.execute<WinLossRow[]>(\n `SELECT\n COUNT(*) FILTER (WHERE (status = 'win' OR closure_outcome = 'won') AND updated_at >= ? AND updated_at < ?) AS current_won,\n COUNT(*) FILTER (WHERE (status = 'loose' OR closure_outcome = 'lost') AND updated_at >= ? AND updated_at < ?) AS current_lost,\n COUNT(*) FILTER (WHERE (status = 'win' OR closure_outcome = 'won') AND updated_at >= ? AND updated_at < ?) AS previous_won,\n COUNT(*) FILTER (WHERE (status = 'loose' OR closure_outcome = 'lost') AND updated_at >= ? AND updated_at < ?) AS previous_lost\n FROM customer_deals\n WHERE ${scopeWhere}`,\n [\n input.currentQuarter.start.toISOString(),\n input.currentQuarter.end.toISOString(),\n input.currentQuarter.start.toISOString(),\n input.currentQuarter.end.toISOString(),\n input.previousQuarter.start.toISOString(),\n input.previousQuarter.end.toISOString(),\n input.previousQuarter.start.toISOString(),\n input.previousQuarter.end.toISOString(),\n ...scopeValues,\n ],\n )\n\n const seriesRows = await connection.execute<WinRateMonthRow[]>(\n `SELECT\n to_char(date_trunc('month', updated_at AT TIME ZONE 'UTC'), 'YYYY-MM') AS period,\n COUNT(*) FILTER (WHERE status = 'win' OR closure_outcome = 'won') AS won,\n COUNT(*) FILTER (WHERE status = 'loose' OR closure_outcome = 'lost') AS lost\n FROM customer_deals\n WHERE ${scopeWhere} AND updated_at >= ?\n GROUP BY 1`,\n [...scopeValues, input.seriesStart.toISOString()],\n )\n\n const overdueDealsPredicate = buildOpenDealPredicate(['open'])\n const overdueRows = await connection.execute<Array<{ id: string }>>(\n `SELECT id FROM customer_deals\n WHERE ${scopeWhere} AND ${overdueDealsPredicate.clause} AND expected_close_at IS NOT NULL AND expected_close_at < CURRENT_DATE`,\n [...scopeValues, ...overdueDealsPredicate.values],\n )\n\n const stuckIdLists = await Promise.all(\n input.organizationIds.map((organizationId) =>\n fetchStuckDealIds(input.em as unknown as PgEntityManager, organizationId, input.tenantId),\n ),\n )\n const stuckIdValues = Array.from(new Set(stuckIdLists.flat()))\n let openStuckIds: string[] = []\n if (stuckIdValues.length > 0) {\n const stuckPlaceholders = stuckIdValues.map(() => '?').join(',')\n const openStuckRows: Array<{ id: string }> = await connection.execute<Array<{ id: string }>>(\n `SELECT id FROM customer_deals\n WHERE ${scopeWhere} AND ${openDealsPredicate.clause} AND id IN (${stuckPlaceholders})`,\n [...scopeValues, ...openDealsPredicate.values, ...stuckIdValues],\n )\n openStuckIds = openStuckRows.map((row) => row.id)\n }\n\n return {\n openRows,\n inflowRows,\n wonRows,\n winLossRows,\n seriesRows,\n overdueRows,\n openStuckIds,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -6,7 +6,6 @@ import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
|
6
6
|
import { extractCustomFieldEntries } from "@open-mercato/shared/lib/crud/custom-fields-client";
|
|
7
7
|
import { E } from "../../../../../../../generated/entities.ids.generated.js";
|
|
8
8
|
import { OrganizationSelect } from "@open-mercato/core/modules/directory/components/OrganizationSelect";
|
|
9
|
-
import { TenantSelect } from "@open-mercato/core/modules/directory/components/TenantSelect";
|
|
10
9
|
import {
|
|
11
10
|
buildOrganizationTreeOptions
|
|
12
11
|
} from "@open-mercato/core/modules/directory/lib/tree";
|
|
@@ -27,6 +26,7 @@ function EditOrganizationPage({ params }) {
|
|
|
27
26
|
const [initialValues, setInitialValues] = React.useState(null);
|
|
28
27
|
const [pathLabel, setPathLabel] = React.useState("");
|
|
29
28
|
const [tenantId, setTenantId] = React.useState(null);
|
|
29
|
+
const [tenantName, setTenantName] = React.useState(null);
|
|
30
30
|
const [loading, setLoading] = React.useState(true);
|
|
31
31
|
const [error, setError] = React.useState(null);
|
|
32
32
|
const [isNotFound, setIsNotFound] = React.useState(false);
|
|
@@ -101,6 +101,7 @@ function EditOrganizationPage({ params }) {
|
|
|
101
101
|
}
|
|
102
102
|
const resolvedTenantId = record.tenantId || null;
|
|
103
103
|
setTenantId(resolvedTenantId);
|
|
104
|
+
setTenantName(record.tenantName ?? null);
|
|
104
105
|
const baseTree = await loadParentTree(resolvedTenantId, record.descendantIds ?? []);
|
|
105
106
|
const fullTree = buildOrganizationTreeOptions(baseTree);
|
|
106
107
|
const nodeMap = new Map(fullTree.map((opt) => [opt.value, opt]));
|
|
@@ -160,20 +161,10 @@ function EditOrganizationPage({ params }) {
|
|
|
160
161
|
id: "tenantId",
|
|
161
162
|
label: t("directory.organizations.form.field.tenant", "Tenant"),
|
|
162
163
|
type: "custom",
|
|
163
|
-
component: (
|
|
164
|
-
|
|
165
|
-
{
|
|
166
|
-
|
|
167
|
-
value: typeof value === "string" ? value : tenantId,
|
|
168
|
-
onChange: (next) => {
|
|
169
|
-
const normalized = next ?? null;
|
|
170
|
-
setTenantId(normalized);
|
|
171
|
-
setValue(normalized);
|
|
172
|
-
},
|
|
173
|
-
includeEmptyOption: false,
|
|
174
|
-
className: "w-full h-9 rounded border px-2 text-sm"
|
|
175
|
-
}
|
|
176
|
-
)
|
|
164
|
+
component: () => /* @__PURE__ */ jsxs("div", { children: [
|
|
165
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm", children: tenantName ?? "\u2014" }),
|
|
166
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: t("directory.organizations.form.field.tenantReadonlyDescription", "Tenant cannot be changed for an existing organization.") })
|
|
167
|
+
] })
|
|
177
168
|
}
|
|
178
169
|
] : [],
|
|
179
170
|
{ id: "name", label: t("directory.organizations.form.field.name", "Name"), type: "text", required: true },
|
|
@@ -215,7 +206,7 @@ function EditOrganizationPage({ params }) {
|
|
|
215
206
|
}
|
|
216
207
|
},
|
|
217
208
|
{ id: "isActive", label: t("directory.organizations.form.field.isActive", "Active"), type: "checkbox" }
|
|
218
|
-
], [actorIsSuperAdmin, childSummary, parentTree, t, tenantId]);
|
|
209
|
+
], [actorIsSuperAdmin, childSummary, parentTree, t, tenantId, tenantName]);
|
|
219
210
|
const detailFields = React.useMemo(() => actorIsSuperAdmin ? ["tenantId", "name", "slug", "parentId", "childrenInfo", "isActive"] : ["name", "slug", "parentId", "childrenInfo", "isActive"], [actorIsSuperAdmin]);
|
|
220
211
|
const groups = React.useMemo(() => [
|
|
221
212
|
{ id: "details", title: t("directory.organizations.form.group.details", "Details"), column: 1, fields: detailFields },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../src/modules/directory/backend/directory/organizations/%5Bid%5D/edit/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { usePathname } from 'next/navigation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { extractCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields-client'\nimport { E } from '#generated/entities.ids.generated'\nimport { OrganizationSelect } from '@open-mercato/core/modules/directory/components/OrganizationSelect'\nimport { TenantSelect } from '@open-mercato/core/modules/directory/components/TenantSelect'\nimport {\n buildOrganizationTreeOptions,\n type OrganizationTreeNode,\n type OrganizationTreeOption,\n} from '@open-mercato/core/modules/directory/lib/tree'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { deleteCrud, updateCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'\nimport { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'\n\ntype TreeResponse = {\n items: OrganizationTreeNode[]\n}\n\ntype OrganizationResponse = {\n items: Array<{\n id: string\n name: string\n slug?: string | null\n tenantId: string\n tenantName?: string | null\n parentId: string | null\n childIds: string[]\n ancestorIds: string[]\n descendantIds: string[]\n isActive: boolean\n pathLabel: string\n updatedAt?: string | null\n updated_at?: string | null\n } & Record<string, unknown>>\n}\n\nconst TREE_STEP = 16\nconst TREE_PADDING = 12\n\nexport default function EditOrganizationPage({ params }: { params?: { id?: string } }) {\n const orgId = params?.id\n const t = useT()\n const pathname = usePathname()\n const [initialValues, setInitialValues] = React.useState<Record<string, unknown> | null>(null)\n const [pathLabel, setPathLabel] = React.useState<string>('')\n const [tenantId, setTenantId] = React.useState<string | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [isNotFound, setIsNotFound] = React.useState(false)\n const [parentTree, setParentTree] = React.useState<OrganizationTreeNode[]>([])\n const [childSummary, setChildSummary] = React.useState<OrganizationTreeOption[]>([])\n const [originalChildIds, setOriginalChildIds] = React.useState<string[]>([])\n const [actorIsSuperAdmin, setActorIsSuperAdmin] = React.useState(false)\n const skipTenantEffectRef = React.useRef(true)\n\n React.useEffect(() => {\n let cancelled = false\n async function loadActor() {\n try {\n const { ok, result } = await apiCall<{ isSuperAdmin?: boolean }>('/api/auth/roles?page=1&pageSize=1')\n if (!cancelled && ok) setActorIsSuperAdmin(Boolean(result?.isSuperAdmin))\n } catch {\n if (!cancelled) setActorIsSuperAdmin(false)\n }\n }\n loadActor()\n return () => { cancelled = true }\n }, [])\n\n const markSelectable = React.useCallback((nodes: OrganizationTreeNode[], excluded: Set<string>): OrganizationTreeNode[] => (\n nodes.map((node) => ({\n ...node,\n selectable: !excluded.has(node.id),\n children: Array.isArray(node.children) ? markSelectable(node.children, excluded) : [],\n }))\n ), [])\n\n const loadParentTree = React.useCallback(async (\n targetTenantId: string | null,\n excludedIds: Iterable<string>,\n ): Promise<OrganizationTreeNode[]> => {\n const treeParams = new URLSearchParams({ view: 'tree', includeInactive: 'true' })\n if (targetTenantId) treeParams.set('tenantId', targetTenantId)\n if (orgId) treeParams.set('ids', orgId)\n try {\n const { ok, result } = await apiCall<TreeResponse>(`/api/directory/organizations?${treeParams.toString()}`)\n if (!ok) {\n setParentTree([])\n return []\n }\n const baseTree = Array.isArray(result?.items) ? result.items ?? [] : []\n const excludedSet = new Set<string>(excludedIds)\n if (orgId) excludedSet.add(orgId)\n setParentTree(markSelectable(baseTree, excludedSet))\n return baseTree\n } catch {\n setParentTree([])\n return []\n }\n }, [markSelectable, orgId])\n\n React.useEffect(() => {\n if (!orgId) return\n const currentOrgId = orgId\n let cancelled = false\n async function load() {\n setLoading(true)\n setError(null)\n setIsNotFound(false)\n try {\n const { ok, status, result } = await apiCall<OrganizationResponse>(\n `/api/directory/organizations?view=manage&ids=${currentOrgId}&status=all&includeInactive=true&page=1&pageSize=1`,\n )\n if (!ok) {\n if (status === 404) {\n if (!cancelled) setIsNotFound(true)\n return\n }\n throw new Error(t('directory.organizations.form.errors.load', 'Failed to load organization'))\n }\n const record = Array.isArray(result?.items) ? result.items?.[0] : undefined\n if (!record) {\n if (!cancelled) setIsNotFound(true)\n return\n }\n const resolvedTenantId = record.tenantId || null\n setTenantId(resolvedTenantId)\n const baseTree = await loadParentTree(resolvedTenantId, record.descendantIds ?? [])\n const fullTree = buildOrganizationTreeOptions(baseTree)\n const nodeMap = new Map(fullTree.map((opt) => [opt.value, opt]))\n const childrenDetails = record.childIds\n .map((id) => nodeMap.get(id))\n .filter((node): node is OrganizationTreeOption => !!node)\n setChildSummary(childrenDetails)\n setOriginalChildIds(Array.isArray(record.childIds) ? record.childIds : [])\n\n const customValues = extractCustomFieldEntries(record as Record<string, unknown>)\n setInitialValues({\n id: record.id,\n name: record.name,\n slug: record.slug ?? '',\n parentId: record.parentId || '',\n isActive: record.isActive,\n tenantId: resolvedTenantId,\n updatedAt: typeof record.updatedAt === 'string'\n ? record.updatedAt\n : typeof record.updated_at === 'string'\n ? record.updated_at\n : null,\n ...customValues,\n })\n setPathLabel(record.pathLabel)\n } catch (err: unknown) {\n if (!cancelled) {\n const fallback = t('directory.organizations.form.errors.load', 'Failed to load organization')\n const message = err instanceof Error && err.message ? err.message : fallback\n setError(message)\n }\n } finally {\n if (!cancelled) {\n skipTenantEffectRef.current = false\n setLoading(false)\n }\n }\n }\n load()\n return () => { cancelled = true }\n }, [loadParentTree, orgId, t])\n\n React.useEffect(() => {\n if (!actorIsSuperAdmin) return\n if (skipTenantEffectRef.current) {\n skipTenantEffectRef.current = false\n return\n }\n if (!orgId) return\n if (!tenantId) {\n setParentTree([])\n setChildSummary([])\n setOriginalChildIds([])\n return\n }\n void loadParentTree(tenantId, [])\n setChildSummary([])\n setOriginalChildIds([])\n }, [actorIsSuperAdmin, loadParentTree, orgId, tenantId])\n\n const fields = React.useMemo<CrudField[]>(() => [\n ...(actorIsSuperAdmin ? [\n {\n id: 'tenantId',\n label: t('directory.organizations.form.field.tenant', 'Tenant'),\n type: 'custom',\n component: ({ value, setValue }) => (\n <TenantSelect\n id=\"tenantId\"\n value={typeof value === 'string' ? value : tenantId}\n onChange={(next) => {\n const normalized = next ?? null\n setTenantId(normalized)\n setValue(normalized)\n }}\n includeEmptyOption={false}\n className=\"w-full h-9 rounded border px-2 text-sm\"\n />\n ),\n } as CrudField,\n ] : []),\n { id: 'name', label: t('directory.organizations.form.field.name', 'Name'), type: 'text', required: true },\n {\n id: 'slug',\n label: t('directory.organizations.form.field.slug', 'Slug'),\n type: 'text',\n description: t('directory.organizations.form.field.slug.description', 'URL-safe identifier used for the customer portal (lowercase letters, numbers, hyphens, underscores).'),\n },\n {\n id: 'parentId',\n label: t('directory.organizations.form.field.parent', 'Parent'),\n type: 'custom',\n component: ({ id, value, setValue }) => (\n <OrganizationSelect\n id={id}\n value={value ? String(value) : null}\n onChange={(next) => setValue(next ?? '')}\n nodes={parentTree}\n includeEmptyOption\n emptyOptionLabel={t('directory.organizations.form.rootOption', '\u2014 Root level \u2014')}\n className=\"w-full h-9 rounded border px-2 text-sm\"\n />\n ),\n },\n {\n id: 'childrenInfo',\n label: t('directory.organizations.form.field.children', 'Children'),\n type: 'custom',\n component: () => {\n if (!childSummary.length) {\n return <p className=\"text-xs text-muted-foreground\">{t('directory.organizations.form.children.empty', 'No direct children assigned.')}</p>\n }\n return (\n <ul className=\"space-y-1 text-sm\">\n {childSummary.map((child) => (\n <li key={child.value} className=\"leading-none\">\n <span style={{ paddingLeft: child.depth > 0 ? TREE_PADDING + (child.depth - 1) * TREE_STEP : 0 }}>\n {child.depth > 0 ? <span className=\"text-muted-foreground\">\u21B3 </span> : null}\n {child.name}\n </span>\n </li>\n ))}\n </ul>\n )\n },\n },\n { id: 'isActive', label: t('directory.organizations.form.field.isActive', 'Active'), type: 'checkbox' },\n ], [actorIsSuperAdmin, childSummary, parentTree, t, tenantId])\n\n const detailFields = React.useMemo(() => (\n actorIsSuperAdmin\n ? ['tenantId', 'name', 'slug', 'parentId', 'childrenInfo', 'isActive']\n : ['name', 'slug', 'parentId', 'childrenInfo', 'isActive']\n ), [actorIsSuperAdmin])\n\n const groups: CrudFormGroup[] = React.useMemo(() => ([\n { id: 'details', title: t('directory.organizations.form.group.details', 'Details'), column: 1, fields: detailFields },\n { id: 'custom', title: t('directory.organizations.form.group.customFields', 'Custom Data'), column: 2, kind: 'customFields' },\n ]), [detailFields, t])\n\n // Publish page-load record context to the AppShell-owned `backend:record:current`\n // mount so the enterprise record_locks widget resolves `directory.organization` + id\n // explicitly. The resourceKind mirrors the CrudForm `versionHistory` so the held\n // lock matches the save-time conflict surface for the same organization.\n useSetCurrentRecordInjectionContext(\n buildRecordInjectionContext({\n resourceKind: 'directory.organization',\n resourceId: orgId || null,\n updatedAt: typeof initialValues?.updatedAt === 'string' ? initialValues.updatedAt : null,\n data: initialValues,\n path: pathname,\n }),\n )\n\n if (!orgId) {\n return (\n <Page>\n <PageBody>\n <div className=\"rounded border border-destructive/40 bg-destructive/10 px-4 py-3 text-sm text-destructive\">\n {t('directory.organizations.form.errors.missingId', 'Organization identifier is missing.')}\n </div>\n </PageBody>\n </Page>\n )\n }\n\n if (isNotFound) {\n return (\n <Page>\n <PageBody>\n <RecordNotFoundState\n label={t('directory.organizations.form.errors.notFound', 'Organization not found')}\n backHref=\"/backend/directory/organizations\"\n backLabel={t('directory.organizations.form.actions.backToList', 'Back to organizations')}\n />\n </PageBody>\n </Page>\n )\n }\n\n if (error && !loading && !initialValues) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage label={error} />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <CrudForm\n title={t('directory.organizations.form.title.edit', 'Edit Organization')}\n backHref=\"/backend/directory/organizations\"\n versionHistory={{ resourceKind: 'directory.organization', resourceId: orgId ? String(orgId) : '' }}\n fields={fields}\n groups={groups}\n entityId={E.directory.organization}\n initialValues={initialValues ?? { id: orgId, tenantId: tenantId ?? null, name: '', slug: '', parentId: '', isActive: true, childIds: [] }}\n optimisticLockUpdatedAt={typeof initialValues?.updatedAt === 'string' ? initialValues.updatedAt : null}\n isLoading={loading}\n loadingMessage={t('directory.organizations.form.loading', 'Loading organization...')}\n submitLabel={t('directory.organizations.form.action.save', 'Save')}\n cancelHref=\"/backend/directory/organizations\"\n successRedirect={`/backend/directory/organizations?flash=${encodeURIComponent(t('directory.organizations.flash.updated', 'Organization updated'))}&type=success`}\n extraActions={pathLabel ? (\n <span className=\"text-xs text-muted-foreground\">\n {t('directory.organizations.form.pathLabel', { path: pathLabel })}\n </span>\n ) : null}\n onSubmit={async (values) => {\n await submitUpdateOrganization({\n values: values as Record<string, unknown>,\n orgId: orgId ?? '',\n tenantId,\n originalChildIds,\n messages: {\n orgIdRequired: t('directory.organizations.errors.orgIdRequired', 'Organization identifier is required'),\n },\n })\n }}\n onDelete={async () => {\n await deleteCrud('directory/organizations', orgId ?? '', {\n errorMessage: t('directory.organizations.errors.deleteFailed', 'Failed to delete organization'),\n })\n }}\n deleteRedirect={`/backend/directory/organizations?flash=${encodeURIComponent(t('directory.organizations.flash.deleted', 'Organization deleted'))}&type=success`}\n />\n </PageBody>\n </Page>\n )\n}\n\ntype UpdateOrganizationPayload = {\n id: string\n name: string\n slug?: string | null\n isActive: boolean\n parentId: string | null\n childIds: string[]\n tenantId?: string | null\n customFields?: Record<string, unknown>\n}\n\ntype UpdateOrganizationRequest = (payload: UpdateOrganizationPayload) => Promise<void>\n\nconst defaultUpdateOrganizationRequest: UpdateOrganizationRequest = async (payload) => {\n await updateCrud('directory/organizations', payload)\n}\n\nexport async function submitUpdateOrganization(options: {\n values: Record<string, unknown>\n orgId: string\n tenantId: string | null\n originalChildIds: string[]\n updateOrganization?: UpdateOrganizationRequest\n messages?: {\n orgIdRequired?: string\n }\n}): Promise<void> {\n const {\n values,\n orgId,\n tenantId,\n originalChildIds,\n updateOrganization = defaultUpdateOrganizationRequest,\n messages,\n } = options\n\n const payloadId = typeof values.id === 'string' && values.id.length ? values.id : orgId\n if (!payloadId) {\n const message = messages?.orgIdRequired ?? 'Organization identifier is required'\n throw createCrudFormError(message, { id: message })\n }\n\n const customFields = collectCustomFieldValues(values)\n\n const submittedTenantId =\n typeof values.tenantId === 'string' && values.tenantId.trim().length\n ? values.tenantId.trim()\n : tenantId\n\n const payload: UpdateOrganizationPayload = {\n id: payloadId,\n name: typeof values.name === 'string' ? values.name : '',\n isActive: values.isActive !== false,\n parentId:\n typeof values.parentId === 'string' && values.parentId.length\n ? values.parentId\n : null,\n childIds: originalChildIds,\n }\n\n if (typeof values.slug === 'string') {\n const trimmedSlug = values.slug.trim()\n payload.slug = trimmedSlug.length ? trimmedSlug : null\n }\n\n if (submittedTenantId !== undefined && submittedTenantId !== null) {\n payload.tenantId = submittedTenantId\n }\n if (Object.keys(customFields).length > 0) {\n payload.customFields = customFields\n }\n\n await updateOrganization(payload)\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { usePathname } from 'next/navigation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { extractCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields-client'\nimport { E } from '#generated/entities.ids.generated'\nimport { OrganizationSelect } from '@open-mercato/core/modules/directory/components/OrganizationSelect'\nimport {\n buildOrganizationTreeOptions,\n type OrganizationTreeNode,\n type OrganizationTreeOption,\n} from '@open-mercato/core/modules/directory/lib/tree'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { deleteCrud, updateCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'\nimport { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'\n\ntype TreeResponse = {\n items: OrganizationTreeNode[]\n}\n\ntype OrganizationResponse = {\n items: Array<{\n id: string\n name: string\n slug?: string | null\n tenantId: string\n tenantName?: string | null\n parentId: string | null\n childIds: string[]\n ancestorIds: string[]\n descendantIds: string[]\n isActive: boolean\n pathLabel: string\n updatedAt?: string | null\n updated_at?: string | null\n } & Record<string, unknown>>\n}\n\nconst TREE_STEP = 16\nconst TREE_PADDING = 12\n\nexport default function EditOrganizationPage({ params }: { params?: { id?: string } }) {\n const orgId = params?.id\n const t = useT()\n const pathname = usePathname()\n const [initialValues, setInitialValues] = React.useState<Record<string, unknown> | null>(null)\n const [pathLabel, setPathLabel] = React.useState<string>('')\n const [tenantId, setTenantId] = React.useState<string | null>(null)\n const [tenantName, setTenantName] = React.useState<string | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [isNotFound, setIsNotFound] = React.useState(false)\n const [parentTree, setParentTree] = React.useState<OrganizationTreeNode[]>([])\n const [childSummary, setChildSummary] = React.useState<OrganizationTreeOption[]>([])\n const [originalChildIds, setOriginalChildIds] = React.useState<string[]>([])\n const [actorIsSuperAdmin, setActorIsSuperAdmin] = React.useState(false)\n const skipTenantEffectRef = React.useRef(true)\n\n React.useEffect(() => {\n let cancelled = false\n async function loadActor() {\n try {\n const { ok, result } = await apiCall<{ isSuperAdmin?: boolean }>('/api/auth/roles?page=1&pageSize=1')\n if (!cancelled && ok) setActorIsSuperAdmin(Boolean(result?.isSuperAdmin))\n } catch {\n if (!cancelled) setActorIsSuperAdmin(false)\n }\n }\n loadActor()\n return () => { cancelled = true }\n }, [])\n\n const markSelectable = React.useCallback((nodes: OrganizationTreeNode[], excluded: Set<string>): OrganizationTreeNode[] => (\n nodes.map((node) => ({\n ...node,\n selectable: !excluded.has(node.id),\n children: Array.isArray(node.children) ? markSelectable(node.children, excluded) : [],\n }))\n ), [])\n\n const loadParentTree = React.useCallback(async (\n targetTenantId: string | null,\n excludedIds: Iterable<string>,\n ): Promise<OrganizationTreeNode[]> => {\n const treeParams = new URLSearchParams({ view: 'tree', includeInactive: 'true' })\n if (targetTenantId) treeParams.set('tenantId', targetTenantId)\n if (orgId) treeParams.set('ids', orgId)\n try {\n const { ok, result } = await apiCall<TreeResponse>(`/api/directory/organizations?${treeParams.toString()}`)\n if (!ok) {\n setParentTree([])\n return []\n }\n const baseTree = Array.isArray(result?.items) ? result.items ?? [] : []\n const excludedSet = new Set<string>(excludedIds)\n if (orgId) excludedSet.add(orgId)\n setParentTree(markSelectable(baseTree, excludedSet))\n return baseTree\n } catch {\n setParentTree([])\n return []\n }\n }, [markSelectable, orgId])\n\n React.useEffect(() => {\n if (!orgId) return\n const currentOrgId = orgId\n let cancelled = false\n async function load() {\n setLoading(true)\n setError(null)\n setIsNotFound(false)\n try {\n const { ok, status, result } = await apiCall<OrganizationResponse>(\n `/api/directory/organizations?view=manage&ids=${currentOrgId}&status=all&includeInactive=true&page=1&pageSize=1`,\n )\n if (!ok) {\n if (status === 404) {\n if (!cancelled) setIsNotFound(true)\n return\n }\n throw new Error(t('directory.organizations.form.errors.load', 'Failed to load organization'))\n }\n const record = Array.isArray(result?.items) ? result.items?.[0] : undefined\n if (!record) {\n if (!cancelled) setIsNotFound(true)\n return\n }\n const resolvedTenantId = record.tenantId || null\n setTenantId(resolvedTenantId)\n setTenantName(record.tenantName ?? null)\n const baseTree = await loadParentTree(resolvedTenantId, record.descendantIds ?? [])\n const fullTree = buildOrganizationTreeOptions(baseTree)\n const nodeMap = new Map(fullTree.map((opt) => [opt.value, opt]))\n const childrenDetails = record.childIds\n .map((id) => nodeMap.get(id))\n .filter((node): node is OrganizationTreeOption => !!node)\n setChildSummary(childrenDetails)\n setOriginalChildIds(Array.isArray(record.childIds) ? record.childIds : [])\n\n const customValues = extractCustomFieldEntries(record as Record<string, unknown>)\n setInitialValues({\n id: record.id,\n name: record.name,\n slug: record.slug ?? '',\n parentId: record.parentId || '',\n isActive: record.isActive,\n tenantId: resolvedTenantId,\n updatedAt: typeof record.updatedAt === 'string'\n ? record.updatedAt\n : typeof record.updated_at === 'string'\n ? record.updated_at\n : null,\n ...customValues,\n })\n setPathLabel(record.pathLabel)\n } catch (err: unknown) {\n if (!cancelled) {\n const fallback = t('directory.organizations.form.errors.load', 'Failed to load organization')\n const message = err instanceof Error && err.message ? err.message : fallback\n setError(message)\n }\n } finally {\n if (!cancelled) {\n skipTenantEffectRef.current = false\n setLoading(false)\n }\n }\n }\n load()\n return () => { cancelled = true }\n }, [loadParentTree, orgId, t])\n\n React.useEffect(() => {\n if (!actorIsSuperAdmin) return\n if (skipTenantEffectRef.current) {\n skipTenantEffectRef.current = false\n return\n }\n if (!orgId) return\n if (!tenantId) {\n setParentTree([])\n setChildSummary([])\n setOriginalChildIds([])\n return\n }\n void loadParentTree(tenantId, [])\n setChildSummary([])\n setOriginalChildIds([])\n }, [actorIsSuperAdmin, loadParentTree, orgId, tenantId])\n\n const fields = React.useMemo<CrudField[]>(() => [\n ...(actorIsSuperAdmin ? [\n {\n id: 'tenantId',\n label: t('directory.organizations.form.field.tenant', 'Tenant'),\n type: 'custom',\n component: () => (\n <div>\n <p className=\"text-sm\">{tenantName ?? '\u2014'}</p>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {t('directory.organizations.form.field.tenantReadonlyDescription', 'Tenant cannot be changed for an existing organization.')}\n </p>\n </div>\n ),\n } as CrudField,\n ] : []),\n { id: 'name', label: t('directory.organizations.form.field.name', 'Name'), type: 'text', required: true },\n {\n id: 'slug',\n label: t('directory.organizations.form.field.slug', 'Slug'),\n type: 'text',\n description: t('directory.organizations.form.field.slug.description', 'URL-safe identifier used for the customer portal (lowercase letters, numbers, hyphens, underscores).'),\n },\n {\n id: 'parentId',\n label: t('directory.organizations.form.field.parent', 'Parent'),\n type: 'custom',\n component: ({ id, value, setValue }) => (\n <OrganizationSelect\n id={id}\n value={value ? String(value) : null}\n onChange={(next) => setValue(next ?? '')}\n nodes={parentTree}\n includeEmptyOption\n emptyOptionLabel={t('directory.organizations.form.rootOption', '\u2014 Root level \u2014')}\n className=\"w-full h-9 rounded border px-2 text-sm\"\n />\n ),\n },\n {\n id: 'childrenInfo',\n label: t('directory.organizations.form.field.children', 'Children'),\n type: 'custom',\n component: () => {\n if (!childSummary.length) {\n return <p className=\"text-xs text-muted-foreground\">{t('directory.organizations.form.children.empty', 'No direct children assigned.')}</p>\n }\n return (\n <ul className=\"space-y-1 text-sm\">\n {childSummary.map((child) => (\n <li key={child.value} className=\"leading-none\">\n <span style={{ paddingLeft: child.depth > 0 ? TREE_PADDING + (child.depth - 1) * TREE_STEP : 0 }}>\n {child.depth > 0 ? <span className=\"text-muted-foreground\">\u21B3 </span> : null}\n {child.name}\n </span>\n </li>\n ))}\n </ul>\n )\n },\n },\n { id: 'isActive', label: t('directory.organizations.form.field.isActive', 'Active'), type: 'checkbox' },\n ], [actorIsSuperAdmin, childSummary, parentTree, t, tenantId, tenantName])\n\n const detailFields = React.useMemo(() => (\n actorIsSuperAdmin\n ? ['tenantId', 'name', 'slug', 'parentId', 'childrenInfo', 'isActive']\n : ['name', 'slug', 'parentId', 'childrenInfo', 'isActive']\n ), [actorIsSuperAdmin])\n\n const groups: CrudFormGroup[] = React.useMemo(() => ([\n { id: 'details', title: t('directory.organizations.form.group.details', 'Details'), column: 1, fields: detailFields },\n { id: 'custom', title: t('directory.organizations.form.group.customFields', 'Custom Data'), column: 2, kind: 'customFields' },\n ]), [detailFields, t])\n\n // Publish page-load record context to the AppShell-owned `backend:record:current`\n // mount so the enterprise record_locks widget resolves `directory.organization` + id\n // explicitly. The resourceKind mirrors the CrudForm `versionHistory` so the held\n // lock matches the save-time conflict surface for the same organization.\n useSetCurrentRecordInjectionContext(\n buildRecordInjectionContext({\n resourceKind: 'directory.organization',\n resourceId: orgId || null,\n updatedAt: typeof initialValues?.updatedAt === 'string' ? initialValues.updatedAt : null,\n data: initialValues,\n path: pathname,\n }),\n )\n\n if (!orgId) {\n return (\n <Page>\n <PageBody>\n <div className=\"rounded border border-destructive/40 bg-destructive/10 px-4 py-3 text-sm text-destructive\">\n {t('directory.organizations.form.errors.missingId', 'Organization identifier is missing.')}\n </div>\n </PageBody>\n </Page>\n )\n }\n\n if (isNotFound) {\n return (\n <Page>\n <PageBody>\n <RecordNotFoundState\n label={t('directory.organizations.form.errors.notFound', 'Organization not found')}\n backHref=\"/backend/directory/organizations\"\n backLabel={t('directory.organizations.form.actions.backToList', 'Back to organizations')}\n />\n </PageBody>\n </Page>\n )\n }\n\n if (error && !loading && !initialValues) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage label={error} />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <CrudForm\n title={t('directory.organizations.form.title.edit', 'Edit Organization')}\n backHref=\"/backend/directory/organizations\"\n versionHistory={{ resourceKind: 'directory.organization', resourceId: orgId ? String(orgId) : '' }}\n fields={fields}\n groups={groups}\n entityId={E.directory.organization}\n initialValues={initialValues ?? { id: orgId, tenantId: tenantId ?? null, name: '', slug: '', parentId: '', isActive: true, childIds: [] }}\n optimisticLockUpdatedAt={typeof initialValues?.updatedAt === 'string' ? initialValues.updatedAt : null}\n isLoading={loading}\n loadingMessage={t('directory.organizations.form.loading', 'Loading organization...')}\n submitLabel={t('directory.organizations.form.action.save', 'Save')}\n cancelHref=\"/backend/directory/organizations\"\n successRedirect={`/backend/directory/organizations?flash=${encodeURIComponent(t('directory.organizations.flash.updated', 'Organization updated'))}&type=success`}\n extraActions={pathLabel ? (\n <span className=\"text-xs text-muted-foreground\">\n {t('directory.organizations.form.pathLabel', { path: pathLabel })}\n </span>\n ) : null}\n onSubmit={async (values) => {\n await submitUpdateOrganization({\n values: values as Record<string, unknown>,\n orgId: orgId ?? '',\n tenantId,\n originalChildIds,\n messages: {\n orgIdRequired: t('directory.organizations.errors.orgIdRequired', 'Organization identifier is required'),\n },\n })\n }}\n onDelete={async () => {\n await deleteCrud('directory/organizations', orgId ?? '', {\n errorMessage: t('directory.organizations.errors.deleteFailed', 'Failed to delete organization'),\n })\n }}\n deleteRedirect={`/backend/directory/organizations?flash=${encodeURIComponent(t('directory.organizations.flash.deleted', 'Organization deleted'))}&type=success`}\n />\n </PageBody>\n </Page>\n )\n}\n\ntype UpdateOrganizationPayload = {\n id: string\n name: string\n slug?: string | null\n isActive: boolean\n parentId: string | null\n childIds: string[]\n tenantId?: string | null\n customFields?: Record<string, unknown>\n}\n\ntype UpdateOrganizationRequest = (payload: UpdateOrganizationPayload) => Promise<void>\n\nconst defaultUpdateOrganizationRequest: UpdateOrganizationRequest = async (payload) => {\n await updateCrud('directory/organizations', payload)\n}\n\nexport async function submitUpdateOrganization(options: {\n values: Record<string, unknown>\n orgId: string\n tenantId: string | null\n originalChildIds: string[]\n updateOrganization?: UpdateOrganizationRequest\n messages?: {\n orgIdRequired?: string\n }\n}): Promise<void> {\n const {\n values,\n orgId,\n tenantId,\n originalChildIds,\n updateOrganization = defaultUpdateOrganizationRequest,\n messages,\n } = options\n\n const payloadId = typeof values.id === 'string' && values.id.length ? values.id : orgId\n if (!payloadId) {\n const message = messages?.orgIdRequired ?? 'Organization identifier is required'\n throw createCrudFormError(message, { id: message })\n }\n\n const customFields = collectCustomFieldValues(values)\n\n const submittedTenantId =\n typeof values.tenantId === 'string' && values.tenantId.trim().length\n ? values.tenantId.trim()\n : tenantId\n\n const payload: UpdateOrganizationPayload = {\n id: payloadId,\n name: typeof values.name === 'string' ? values.name : '',\n isActive: values.isActive !== false,\n parentId:\n typeof values.parentId === 'string' && values.parentId.length\n ? values.parentId\n : null,\n childIds: originalChildIds,\n }\n\n if (typeof values.slug === 'string') {\n const trimmedSlug = values.slug.trim()\n payload.slug = trimmedSlug.length ? trimmedSlug : null\n }\n\n if (submittedTenantId !== undefined && submittedTenantId !== null) {\n payload.tenantId = submittedTenantId\n }\n if (Object.keys(customFields).length > 0) {\n payload.customFields = customFields\n }\n\n await updateOrganization(payload)\n}\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.7-develop.
|
|
3
|
+
"version": "0.6.7-develop.6770.1.b52298cf20",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -254,16 +254,16 @@
|
|
|
254
254
|
"zod": "^4.4.3"
|
|
255
255
|
},
|
|
256
256
|
"peerDependencies": {
|
|
257
|
-
"@open-mercato/ai-assistant": "0.6.7-develop.
|
|
258
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
259
|
-
"@open-mercato/ui": "0.6.7-develop.
|
|
257
|
+
"@open-mercato/ai-assistant": "0.6.7-develop.6770.1.b52298cf20",
|
|
258
|
+
"@open-mercato/shared": "0.6.7-develop.6770.1.b52298cf20",
|
|
259
|
+
"@open-mercato/ui": "0.6.7-develop.6770.1.b52298cf20",
|
|
260
260
|
"react": "^19.0.0",
|
|
261
261
|
"react-dom": "^19.0.0"
|
|
262
262
|
},
|
|
263
263
|
"devDependencies": {
|
|
264
|
-
"@open-mercato/ai-assistant": "0.6.7-develop.
|
|
265
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
266
|
-
"@open-mercato/ui": "0.6.7-develop.
|
|
264
|
+
"@open-mercato/ai-assistant": "0.6.7-develop.6770.1.b52298cf20",
|
|
265
|
+
"@open-mercato/shared": "0.6.7-develop.6770.1.b52298cf20",
|
|
266
|
+
"@open-mercato/ui": "0.6.7-develop.6770.1.b52298cf20",
|
|
267
267
|
"@testing-library/dom": "^10.4.1",
|
|
268
268
|
"@testing-library/jest-dom": "^6.9.1",
|
|
269
269
|
"@testing-library/react": "^16.3.1",
|
|
@@ -15,6 +15,7 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
|
15
15
|
import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
16
16
|
import { clearAllOperations } from '@open-mercato/ui/backend/operations/store'
|
|
17
17
|
import { notifyAuthIdentityChange } from '@open-mercato/ui/backend/AuthSessionGuard'
|
|
18
|
+
import { clearAllPerspectiveState } from '@open-mercato/ui/backend/DataTable'
|
|
18
19
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
19
20
|
import { X } from 'lucide-react'
|
|
20
21
|
import { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'
|
|
@@ -264,6 +265,7 @@ export default function LoginPage() {
|
|
|
264
265
|
const res = await fetch('/api/auth/login', { method: 'POST', body: form })
|
|
265
266
|
if (res.redirected) {
|
|
266
267
|
clearAllOperations()
|
|
268
|
+
clearAllPerspectiveState()
|
|
267
269
|
notifyAuthIdentityChange()
|
|
268
270
|
// NextResponse.redirect from API
|
|
269
271
|
router.replace(res.url)
|
|
@@ -318,6 +320,7 @@ export default function LoginPage() {
|
|
|
318
320
|
const data = await res.json().catch(() => null) as LoginResponseEventDetail
|
|
319
321
|
emitLoginResponseEvent(data)
|
|
320
322
|
clearAllOperations()
|
|
323
|
+
clearAllPerspectiveState()
|
|
321
324
|
notifyAuthIdentityChange()
|
|
322
325
|
if (data && typeof data.redirect === 'string' && data.redirect.length > 0) {
|
|
323
326
|
router.replace(data.redirect)
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import type { EntityManager as CoreEntityManager } from '@mikro-orm/core'
|
|
4
|
-
import type { EntityManager as PgEntityManager } from '@mikro-orm/postgresql'
|
|
5
4
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
6
5
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
7
6
|
import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
|
|
8
7
|
import type { ExchangeRateService, RateResult } from '@open-mercato/core/modules/currencies/services/exchangeRateService'
|
|
9
8
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
10
|
-
import { fetchStuckDealIds } from '../../../lib/stuckDeals'
|
|
11
9
|
import { resolveDealsOrganizationIds } from '../../../lib/dealsOrganizationScope'
|
|
12
10
|
import { resolveOptionalBaseCurrencyCode } from '../../../lib/optionalBaseCurrency'
|
|
11
|
+
import { loadDealsSummaryQueryRows } from '../../../lib/dealsSummaryQueries'
|
|
13
12
|
import {
|
|
14
13
|
computeDelta,
|
|
15
14
|
convertSumsToBase,
|
|
@@ -27,7 +26,6 @@ export const metadata = {
|
|
|
27
26
|
GET: { requireAuth: true, requireFeatures: ['customers.deals.view'] },
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
const OPEN_STATUSES = ['open', 'in_progress'] as const
|
|
31
29
|
const TRAILING_MONTHS = 6
|
|
32
30
|
const TOP_OWNERS = 5
|
|
33
31
|
|
|
@@ -108,40 +106,6 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
108
106
|
},
|
|
109
107
|
}
|
|
110
108
|
|
|
111
|
-
type OpenPipelineRow = {
|
|
112
|
-
stage: string | null
|
|
113
|
-
currency: string | null
|
|
114
|
-
total: string | number | null
|
|
115
|
-
count: string | number
|
|
116
|
-
owner_user_id: string | null
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
type WindowSumRow = {
|
|
120
|
-
currency: string | null
|
|
121
|
-
current_total: string | number | null
|
|
122
|
-
current_count: string | number
|
|
123
|
-
previous_total: string | number | null
|
|
124
|
-
previous_count: string | number
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
type WinLossRow = {
|
|
128
|
-
current_won: string | number
|
|
129
|
-
current_lost: string | number
|
|
130
|
-
previous_won: string | number
|
|
131
|
-
previous_lost: string | number
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
type WinRateMonthRow = {
|
|
135
|
-
period: string
|
|
136
|
-
won: string | number
|
|
137
|
-
lost: string | number
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
type OwnerCountRow = {
|
|
141
|
-
owner_user_id: string | null
|
|
142
|
-
count: string | number
|
|
143
|
-
}
|
|
144
|
-
|
|
145
109
|
function toNumber(value: string | number | null | undefined): number {
|
|
146
110
|
const parsed = Number(value ?? 0)
|
|
147
111
|
return Number.isFinite(parsed) ? parsed : 0
|
|
@@ -185,133 +149,21 @@ export async function GET(req: Request) {
|
|
|
185
149
|
const trailingMonths = getTrailingMonths(today, TRAILING_MONTHS)
|
|
186
150
|
const seriesStart = trailingMonths[0]?.start ?? currentQuarter.start
|
|
187
151
|
|
|
188
|
-
const connection = em.getConnection()
|
|
189
|
-
|
|
190
152
|
const baseCurrencyCode = await resolveOptionalBaseCurrencyCode(
|
|
191
153
|
container,
|
|
192
154
|
effectiveTenantId,
|
|
193
155
|
orgFilterIds[0],
|
|
194
156
|
)
|
|
195
157
|
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
pipeline_stage AS stage,
|
|
206
|
-
UPPER(COALESCE(value_currency, '')) AS currency,
|
|
207
|
-
COALESCE(SUM(value_amount), 0) AS total,
|
|
208
|
-
COUNT(*) AS count,
|
|
209
|
-
owner_user_id
|
|
210
|
-
FROM customer_deals
|
|
211
|
-
WHERE ${scopeWhere} AND status IN (${openPlaceholders})
|
|
212
|
-
GROUP BY pipeline_stage, UPPER(COALESCE(value_currency, '')), owner_user_id`,
|
|
213
|
-
[...scopeValues, ...OPEN_STATUSES],
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
// 2) Open-deal value created in the current vs previous quarter (pipeline inflow delta).
|
|
217
|
-
const inflowRows: WindowSumRow[] = await connection.execute<WindowSumRow[]>(
|
|
218
|
-
`SELECT
|
|
219
|
-
UPPER(COALESCE(value_currency, '')) AS currency,
|
|
220
|
-
COALESCE(SUM(value_amount) FILTER (WHERE created_at >= ? AND created_at < ?), 0) AS current_total,
|
|
221
|
-
COUNT(*) FILTER (WHERE created_at >= ? AND created_at < ?) AS current_count,
|
|
222
|
-
COALESCE(SUM(value_amount) FILTER (WHERE created_at >= ? AND created_at < ?), 0) AS previous_total,
|
|
223
|
-
COUNT(*) FILTER (WHERE created_at >= ? AND created_at < ?) AS previous_count
|
|
224
|
-
FROM customer_deals
|
|
225
|
-
WHERE ${scopeWhere} AND status IN (${openPlaceholders})
|
|
226
|
-
GROUP BY UPPER(COALESCE(value_currency, ''))`,
|
|
227
|
-
[
|
|
228
|
-
currentQuarter.start.toISOString(), currentQuarter.end.toISOString(),
|
|
229
|
-
currentQuarter.start.toISOString(), currentQuarter.end.toISOString(),
|
|
230
|
-
previousQuarter.start.toISOString(), previousQuarter.end.toISOString(),
|
|
231
|
-
previousQuarter.start.toISOString(), previousQuarter.end.toISOString(),
|
|
232
|
-
...scopeValues, ...OPEN_STATUSES,
|
|
233
|
-
],
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
// 3) Won value per currency for the current vs previous quarter (updated_at in window).
|
|
237
|
-
const wonRows: WindowSumRow[] = await connection.execute<WindowSumRow[]>(
|
|
238
|
-
`SELECT
|
|
239
|
-
UPPER(COALESCE(value_currency, '')) AS currency,
|
|
240
|
-
COALESCE(SUM(value_amount) FILTER (WHERE updated_at >= ? AND updated_at < ?), 0) AS current_total,
|
|
241
|
-
COUNT(*) FILTER (WHERE updated_at >= ? AND updated_at < ?) AS current_count,
|
|
242
|
-
COALESCE(SUM(value_amount) FILTER (WHERE updated_at >= ? AND updated_at < ?), 0) AS previous_total,
|
|
243
|
-
COUNT(*) FILTER (WHERE updated_at >= ? AND updated_at < ?) AS previous_count
|
|
244
|
-
FROM customer_deals
|
|
245
|
-
WHERE ${scopeWhere} AND (status = 'win' OR closure_outcome = 'won')
|
|
246
|
-
GROUP BY UPPER(COALESCE(value_currency, ''))`,
|
|
247
|
-
[
|
|
248
|
-
currentQuarter.start.toISOString(), currentQuarter.end.toISOString(),
|
|
249
|
-
currentQuarter.start.toISOString(), currentQuarter.end.toISOString(),
|
|
250
|
-
previousQuarter.start.toISOString(), previousQuarter.end.toISOString(),
|
|
251
|
-
previousQuarter.start.toISOString(), previousQuarter.end.toISOString(),
|
|
252
|
-
...scopeValues,
|
|
253
|
-
],
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
// 4) Win/lost counts for the current vs previous quarter (win rate + delta-pp).
|
|
257
|
-
const winLossRows: WinLossRow[] = await connection.execute<WinLossRow[]>(
|
|
258
|
-
`SELECT
|
|
259
|
-
COUNT(*) FILTER (WHERE (status = 'win' OR closure_outcome = 'won') AND updated_at >= ? AND updated_at < ?) AS current_won,
|
|
260
|
-
COUNT(*) FILTER (WHERE (status = 'loose' OR closure_outcome = 'lost') AND updated_at >= ? AND updated_at < ?) AS current_lost,
|
|
261
|
-
COUNT(*) FILTER (WHERE (status = 'win' OR closure_outcome = 'won') AND updated_at >= ? AND updated_at < ?) AS previous_won,
|
|
262
|
-
COUNT(*) FILTER (WHERE (status = 'loose' OR closure_outcome = 'lost') AND updated_at >= ? AND updated_at < ?) AS previous_lost
|
|
263
|
-
FROM customer_deals
|
|
264
|
-
WHERE ${scopeWhere}`,
|
|
265
|
-
[
|
|
266
|
-
currentQuarter.start.toISOString(), currentQuarter.end.toISOString(),
|
|
267
|
-
currentQuarter.start.toISOString(), currentQuarter.end.toISOString(),
|
|
268
|
-
previousQuarter.start.toISOString(), previousQuarter.end.toISOString(),
|
|
269
|
-
previousQuarter.start.toISOString(), previousQuarter.end.toISOString(),
|
|
270
|
-
...scopeValues,
|
|
271
|
-
],
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
// 5) Win-rate series over the trailing months (won/lost grouped by updated_at month).
|
|
275
|
-
const seriesRows: WinRateMonthRow[] = await connection.execute<WinRateMonthRow[]>(
|
|
276
|
-
`SELECT
|
|
277
|
-
to_char(date_trunc('month', updated_at AT TIME ZONE 'UTC'), 'YYYY-MM') AS period,
|
|
278
|
-
COUNT(*) FILTER (WHERE status = 'win' OR closure_outcome = 'won') AS won,
|
|
279
|
-
COUNT(*) FILTER (WHERE status = 'loose' OR closure_outcome = 'lost') AS lost
|
|
280
|
-
FROM customer_deals
|
|
281
|
-
WHERE ${scopeWhere} AND updated_at >= ?
|
|
282
|
-
GROUP BY 1`,
|
|
283
|
-
[...scopeValues, seriesStart.toISOString()],
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
// Overdue open deals (id set) + stuck deals (id set) → union count for "need attention".
|
|
287
|
-
const overdueRows: Array<{ id: string }> = await connection.execute<Array<{ id: string }>>(
|
|
288
|
-
`SELECT id FROM customer_deals
|
|
289
|
-
WHERE ${scopeWhere} AND status = 'open' AND expected_close_at IS NOT NULL AND expected_close_at < CURRENT_DATE`,
|
|
290
|
-
[...scopeValues],
|
|
291
|
-
)
|
|
292
|
-
// `fetchStuckDealIds` is single-org; run it for every org in scope so multi-org callers don't
|
|
293
|
-
// undercount stuck deals (the aggregates above already span every org in `orgFilterIds`).
|
|
294
|
-
const stuckIdLists = await Promise.all(
|
|
295
|
-
orgFilterIds.map((orgId) =>
|
|
296
|
-
fetchStuckDealIds(em as unknown as PgEntityManager, orgId, effectiveTenantId)),
|
|
297
|
-
)
|
|
298
|
-
const stuckIdSet = new Set<string>()
|
|
299
|
-
for (const list of stuckIdLists) for (const id of list) stuckIdSet.add(id)
|
|
300
|
-
|
|
301
|
-
// The stuck-deal query does not filter status, so a stuck id can be a won/lost/closed deal.
|
|
302
|
-
// "Need attention" is an active-deal metric — intersect with the open (OPEN_STATUSES) set so
|
|
303
|
-
// terminal deals never inflate the count.
|
|
304
|
-
let openStuckIds: string[] = []
|
|
305
|
-
if (stuckIdSet.size > 0) {
|
|
306
|
-
const stuckIdValues = Array.from(stuckIdSet)
|
|
307
|
-
const stuckPlaceholders = stuckIdValues.map(() => '?').join(',')
|
|
308
|
-
const openStuckRows: Array<{ id: string }> = await connection.execute<Array<{ id: string }>>(
|
|
309
|
-
`SELECT id FROM customer_deals
|
|
310
|
-
WHERE ${scopeWhere} AND status IN (${openPlaceholders}) AND id IN (${stuckPlaceholders})`,
|
|
311
|
-
[...scopeValues, ...OPEN_STATUSES, ...stuckIdValues],
|
|
312
|
-
)
|
|
313
|
-
openStuckIds = openStuckRows.map((row) => row.id)
|
|
314
|
-
}
|
|
158
|
+
const { openRows, inflowRows, wonRows, winLossRows, seriesRows, overdueRows, openStuckIds } =
|
|
159
|
+
await loadDealsSummaryQueryRows({
|
|
160
|
+
em,
|
|
161
|
+
tenantId: effectiveTenantId,
|
|
162
|
+
organizationIds: orgFilterIds,
|
|
163
|
+
currentQuarter,
|
|
164
|
+
previousQuarter,
|
|
165
|
+
seriesStart,
|
|
166
|
+
})
|
|
315
167
|
|
|
316
168
|
const attentionIds = new Set<string>()
|
|
317
169
|
for (const row of overdueRows) attentionIds.add(row.id)
|