From 8a56a9887b92a5e732542d4c29d4ab19fdcc2a8f Mon Sep 17 00:00:00 2001 From: "Joseph T. French" Date: Wed, 27 May 2026 11:57:21 -0500 Subject: [PATCH 1/2] Update @robosystems/client to version 0.3.34 and refactor ChartOfAccountsContent for improved mapping logic --- package-lock.json | 8 +- package.json | 2 +- .../ledger/chart-of-accounts/content.tsx | 197 ++++++------------ 3 files changed, 71 insertions(+), 136 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7e08d00..b99dd2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@aws-sdk/client-sns": "^3.1041.0", - "@robosystems/client": "0.3.33", + "@robosystems/client": "0.3.34", "flowbite": "^3.1", "flowbite-react": "^0.12.5", "intuit-oauth": "^4.1.0", @@ -3288,9 +3288,9 @@ } }, "node_modules/@robosystems/client": { - "version": "0.3.33", - "resolved": "https://registry.npmjs.org/@robosystems/client/-/client-0.3.33.tgz", - "integrity": "sha512-JIu6PbH7s5UXN0laE0adtsB+Fxv4Mf4Ls85haC8LQNYQGMLfehaAtFQ+jhiF3mY9wGyrICR3Osq8NhiGlVOpLA==", + "version": "0.3.34", + "resolved": "https://registry.npmjs.org/@robosystems/client/-/client-0.3.34.tgz", + "integrity": "sha512-vkzZBUEf6EGorNYGpuY3zBIYUOtwKkTUc/nPr+9hPm0cgwuBE9YMhC+c34HzKGs2z13oq2HsSFNb/FSi5ZX4hQ==", "license": "MIT", "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", diff --git a/package.json b/package.json index 634fc0a..f3d0590 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ }, "dependencies": { "@aws-sdk/client-sns": "^3.1041.0", - "@robosystems/client": "0.3.33", + "@robosystems/client": "0.3.34", "flowbite": "^3.1", "flowbite-react": "^0.12.5", "intuit-oauth": "^4.1.0", diff --git a/src/app/(app)/ledger/chart-of-accounts/content.tsx b/src/app/(app)/ledger/chart-of-accounts/content.tsx index 86e1439..fd05751 100644 --- a/src/app/(app)/ledger/chart-of-accounts/content.tsx +++ b/src/app/(app)/ledger/chart-of-accounts/content.tsx @@ -349,12 +349,11 @@ const ChartOfAccountsContent: FC = function () { // review or edit the FAC / rs-GAAP relationships. const [showMappings, setShowMappings] = useState(false) - // Inline editing state - const [facElements, setFacElements] = useState([]) + // Inline editing state. Mapping is CoA → rs-gaap only; the FAC level is + // inferred (reverse fac-to-rs-gaap equivalence), never edited here. const [rsGaapElements, setRsGaapElements] = useState([]) const [editingState, setEditingState] = useState<{ accountId: string - mode: 'fac' | 'rsGaap' } | null>(null) const [isSaving, setIsSaving] = useState(false) @@ -428,46 +427,32 @@ const ChartOfAccountsContent: FC = function () { } try { - const [detail, coverage, facResult, rsGaapResult] = await Promise.all([ + const [detail, coverage, rsGaapResult] = await Promise.all([ clients.ledger.getMapping(currentGraph.graphId, selectedMappingId), clients.ledger .getMappingCoverage(currentGraph.graphId, selectedMappingId) .catch(() => null), - // Load FAC elements for the dropdown (once) - facElements.length === 0 - ? clients.ledger - .listElements(currentGraph.graphId, { - source: 'fac', - isAbstract: false, - limit: 200, - }) - .catch(() => ({ elements: [] })) - : null, - // Load rs-gaap elements in two pages (API max is 1000; ~1863 total) + // Candidate concepts per EFS classification, limited to those that + // render under the active Reporting Style (mappingCandidates wraps + // suggest_mapping_candidates, minus subtotals). One call per trait, + // combined into the flat list the dropdown groups client-side. + // Mapping outside this set would land a fact on an unreachable branch. rsGaapElements.length === 0 - ? Promise.all([ - clients.ledger - .listElements(currentGraph.graphId, { - source: 'rs-gaap', - isAbstract: false, - limit: 1000, - offset: 0, - }) - .catch(() => ({ elements: [] })), - clients.ledger - .listElements(currentGraph.graphId, { - source: 'rs-gaap', - isAbstract: false, - limit: 1000, - offset: 1000, - }) - .catch(() => ({ elements: [] })), - ]).then(([p1, p2]) => ({ - elements: [ - ...((p1 as { elements?: unknown[] })?.elements ?? []), - ...((p2 as { elements?: unknown[] })?.elements ?? []), - ], - })) + ? Promise.all( + ( + [ + 'asset', + 'liability', + 'equity', + 'revenue', + 'expense', + ] as const + ).map((cls) => + clients.ledger + .getMappingCandidates(currentGraph.graphId, cls) + .catch(() => []) + ) + ).then((lists) => ({ elements: lists.flat() })) : null, ]) @@ -480,10 +465,13 @@ const ChartOfAccountsContent: FC = function () { id: e.id as string, name: e.name as string, qname: (e.qname as string) ?? '', - classification: (e.classification as string) ?? '', + // EFS class lives on `trait` (asset/liability/equity/revenue/expense); + // the legacy `classification` field was removed from the Element type. + // The dropdown groups by this value, so reading the wrong field left + // every concept ungrouped → "No matching concepts". + classification: (e.trait as string) ?? '', })) } - if (facResult) setFacElements(toGaapElements(facResult)) if (rsGaapResult) setRsGaapElements(toGaapElements(rsGaapResult)) } catch (err) { console.error('Error loading mapping detail:', err) @@ -493,7 +481,7 @@ const ChartOfAccountsContent: FC = function () { } loadMappingData() - // eslint-disable-next-line react-hooks/exhaustive-deps -- facElements/rsGaapElements intentionally excluded to avoid re-fetching + // eslint-disable-next-line react-hooks/exhaustive-deps -- rsGaapElements intentionally excluded to avoid re-fetching }, [currentGraph, selectedMappingId]) // Build GAAP lookup from mapping associations, keyed by from_element_id. @@ -542,18 +530,13 @@ const ChartOfAccountsContent: FC = function () { // Handle GAAP element selection (fac or rsGaap slot) const handleSelectGaap = useCallback( - async ( - accountId: string, - gaapElement: GaapElement, - mode: 'fac' | 'rsGaap' - ) => { + async (accountId: string, gaapElement: GaapElement) => { if (!currentGraph || !selectedMappingId) return setIsSaving(true) try { const accountMappings = gaapByElementId.get(accountId) - const existing = - mode === 'fac' ? accountMappings?.fac : accountMappings?.rsGaap + const existing = accountMappings?.rsGaap if (existing) { await clients.ledger.deleteMappingAssociation(currentGraph.graphId, { mapping_id: selectedMappingId, @@ -580,14 +563,13 @@ const ChartOfAccountsContent: FC = function () { [currentGraph, selectedMappingId, gaapByElementId, refreshMappingData] ) - // Handle clear mapping (fac or rsGaap slot) + // Handle clear mapping (rs-gaap) const handleClearMapping = useCallback( - async (accountId: string, mode: 'fac' | 'rsGaap') => { + async (accountId: string) => { if (!currentGraph || !selectedMappingId) return const accountMappings = gaapByElementId.get(accountId) - const existing = - mode === 'fac' ? accountMappings?.fac : accountMappings?.rsGaap + const existing = accountMappings?.rsGaap if (!existing) return setIsSaving(true) @@ -927,93 +909,46 @@ const ChartOfAccountsContent: FC = function () { ) : isEditing ? ( - handleSelectGaap( - account.id, - el, - editingState!.mode - ) - } - onClear={() => - handleClearMapping( - account.id, - editingState!.mode - ) + handleSelectGaap(account.id, el) } + onClear={() => handleClearMapping(account.id)} onClose={() => setEditingState(null)} /> ) : ( -
- {/* FAC row */} - - {/* rs-gaap row */} - -
+ )} + + + )} )} From 4964e7a85ac7604f44c62709e39b77c1c6eeac65 Mon Sep 17 00:00:00 2001 From: "Joseph T. French" Date: Wed, 27 May 2026 12:23:42 -0500 Subject: [PATCH 2/2] fix classifications --- .../(app)/ledger/chart-of-accounts/content.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/app/(app)/ledger/chart-of-accounts/content.tsx b/src/app/(app)/ledger/chart-of-accounts/content.tsx index fd05751..b1897fe 100644 --- a/src/app/(app)/ledger/chart-of-accounts/content.tsx +++ b/src/app/(app)/ledger/chart-of-accounts/content.tsx @@ -102,7 +102,6 @@ interface GaapMapping { } interface AccountMappings { - fac: GaapMapping | null rsGaap: GaapMapping | null } @@ -437,6 +436,9 @@ const ChartOfAccountsContent: FC = function () { // suggest_mapping_candidates, minus subtotals). One call per trait, // combined into the flat list the dropdown groups client-side. // Mapping outside this set would land a fact on an unreachable branch. + // The trait set must match the backend MappingOperator's (it groups + // CoA elements by their EFS trait); gain/loss are valid CoA traits + // with their own rs-gaap concepts, so they're fetched too. rsGaapElements.length === 0 ? Promise.all( ( @@ -446,6 +448,8 @@ const ChartOfAccountsContent: FC = function () { 'equity', 'revenue', 'expense', + 'gain', + 'loss', ] as const ).map((cls) => clients.ledger @@ -485,7 +489,8 @@ const ChartOfAccountsContent: FC = function () { }, [currentGraph, selectedMappingId]) // Build GAAP lookup from mapping associations, keyed by from_element_id. - // Each account can have both a FAC mapping (fac:*) and an rs-gaap mapping (rs-gaap:*). + // Mapping is CoA → rs-gaap only. Legacy fac:* associations are skipped so + // they don't show in the rs-gaap slot; the FAC level is inferred, not edited. const gaapByElementId = useMemo(() => { const map = new Map() if (!mappingDetail?.associations) return map @@ -493,7 +498,8 @@ const ChartOfAccountsContent: FC = function () { for (const assoc of mappingDetail.associations) { const fromId = assoc.fromElementId if (!fromId) continue - const existing = map.get(fromId) ?? { fac: null, rsGaap: null } + if (assoc.toElementQname?.startsWith('fac:')) continue + const existing = map.get(fromId) ?? { rsGaap: null } const mapping: GaapMapping = { gaapName: assoc.toElementName ?? '', gaapQname: assoc.toElementQname ?? '', @@ -502,11 +508,7 @@ const ChartOfAccountsContent: FC = function () { fromElementId: fromId, toElementId: assoc.toElementId, } - if (assoc.toElementQname?.startsWith('fac:')) { - map.set(fromId, { ...existing, fac: mapping }) - } else { - map.set(fromId, { ...existing, rsGaap: mapping }) - } + map.set(fromId, { ...existing, rsGaap: mapping }) } return map }, [mappingDetail])