@paro.io/expert-shared-components 1.14.49 → 1.14.51
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/lib/components/TaxAxis/TaxAxisApi.d.ts +16 -0
- package/lib/components/TaxAxis/TaxAxisShell.js +65 -15
- package/lib/tax-axis/components/dashboard/DashboardActions.js +2 -3
- package/lib/tax-axis/components/dashboard/DashboardSummary.js +1 -1
- package/lib/tax-axis/components/dashboard/TaxAxisDashboard.d.ts +56 -1
- package/lib/tax-axis/components/dashboard/TaxAxisDashboard.js +86 -5
- package/lib/tax-axis/components/documents/DocumentCard.d.ts +4 -2
- package/lib/tax-axis/components/documents/DocumentCard.js +40 -9
- package/lib/tax-axis/components/documents/DocumentTier.d.ts +2 -1
- package/lib/tax-axis/components/documents/DocumentTier.js +2 -2
- package/lib/tax-axis/components/documents/TaxAxisDocuments.d.ts +16 -2
- package/lib/tax-axis/components/documents/TaxAxisDocuments.js +115 -15
- package/lib/tax-axis/lib/data/documents.js +8 -8
- package/package.json +1 -1
|
@@ -21,11 +21,27 @@ export type TaxAxisUploadInput = {
|
|
|
21
21
|
documentType?: string;
|
|
22
22
|
s3Key: string;
|
|
23
23
|
};
|
|
24
|
+
export type TaxAxisDocumentRecord = {
|
|
25
|
+
documentId: string;
|
|
26
|
+
sessionId: string;
|
|
27
|
+
fileName: string;
|
|
28
|
+
fileType: string;
|
|
29
|
+
fileSize?: number;
|
|
30
|
+
documentType?: string | null;
|
|
31
|
+
status: string;
|
|
32
|
+
s3Key: string;
|
|
33
|
+
parseError?: string | null;
|
|
34
|
+
parseStageResults?: unknown[] | null;
|
|
35
|
+
createdAt?: string;
|
|
36
|
+
updatedAt?: string;
|
|
37
|
+
};
|
|
24
38
|
export type TaxAxisApi = {
|
|
25
39
|
createSession: (input: TaxAxisSessionInput) => Promise<any>;
|
|
26
40
|
getSession: (sessionId: string) => Promise<any>;
|
|
27
41
|
listSessions: (input?: Record<string, unknown>) => Promise<any[]>;
|
|
28
42
|
uploadFile: (input: TaxAxisUploadInput) => Promise<any>;
|
|
43
|
+
deleteDocument: (documentId: string) => Promise<boolean>;
|
|
44
|
+
getDocuments: (sessionId: string) => Promise<TaxAxisDocumentRecord[]>;
|
|
29
45
|
analyzeDocuments: (sessionId: string) => Promise<any>;
|
|
30
46
|
saveReviewedData: (documentId: string, reviewedData: Record<string, unknown>) => Promise<any>;
|
|
31
47
|
runLlm: (sessionId: string) => Promise<any>;
|
|
@@ -47,12 +47,16 @@ const TaxAxisClientReport_1 = require("../../tax-axis/components/clientReport/Ta
|
|
|
47
47
|
const TaxAxisPreparerWorkpaper_1 = require("../../tax-axis/components/preparerWorkpaper/TaxAxisPreparerWorkpaper");
|
|
48
48
|
const TaxAxisPresentationMode_1 = require("../../tax-axis/components/presentationMode/TaxAxisPresentationMode");
|
|
49
49
|
const UploadClient_1 = __importDefault(require("../shared/UploadClient"));
|
|
50
|
+
function sleep(ms) {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
setTimeout(resolve, ms);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
50
55
|
function toInt(value) {
|
|
51
56
|
const parsed = Number(String(value || '0').replace(/[^\d.-]/g, ''));
|
|
52
57
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
53
58
|
}
|
|
54
59
|
function buildSessionInput(profile) {
|
|
55
|
-
// Placeholder defaults are overridden by sessionDefaults from the host app.
|
|
56
60
|
const taxYear = Number(profile.year) || new Date().getFullYear();
|
|
57
61
|
return {
|
|
58
62
|
clientId: null,
|
|
@@ -79,8 +83,10 @@ const TaxAxisShell = ({ taxAxisApi, userContext = 'expert', initialSessionId, in
|
|
|
79
83
|
const [profile, setProfile] = (0, react_1.useState)(initialProfile ? Object.assign({}, initialProfile) : null);
|
|
80
84
|
const [sessionId, setSessionId] = (0, react_1.useState)(initialSessionId || null);
|
|
81
85
|
const [isProspectFlow, setIsProspectFlow] = (0, react_1.useState)(false);
|
|
86
|
+
const [llmResult, setLlmResult] = (0, react_1.useState)(null);
|
|
82
87
|
const [isBusy, setIsBusy] = (0, react_1.useState)(false);
|
|
83
88
|
const [error, setError] = (0, react_1.useState)(null);
|
|
89
|
+
const [busyMessage, setBusyMessage] = (0, react_1.useState)('Syncing Tax Axis session...');
|
|
84
90
|
const updateSessionId = (0, react_1.useCallback)((nextSessionId) => {
|
|
85
91
|
setSessionId(nextSessionId);
|
|
86
92
|
if (onSessionChange) {
|
|
@@ -101,7 +107,8 @@ const TaxAxisShell = ({ taxAxisApi, userContext = 'expert', initialSessionId, in
|
|
|
101
107
|
}
|
|
102
108
|
}
|
|
103
109
|
catch (requestError) {
|
|
104
|
-
|
|
110
|
+
const msg = requestError instanceof Error ? requestError.message : 'Unable to create Tax Axis session right now.';
|
|
111
|
+
setError(msg);
|
|
105
112
|
}
|
|
106
113
|
return null;
|
|
107
114
|
}), [sessionId, sessionDefaults === null || sessionDefaults === void 0 ? void 0 : sessionDefaults.clientId, sessionDefaults === null || sessionDefaults === void 0 ? void 0 : sessionDefaults.freelancerId, taxAxisApi, updateSessionId]);
|
|
@@ -121,6 +128,7 @@ const TaxAxisShell = ({ taxAxisApi, userContext = 'expert', initialSessionId, in
|
|
|
121
128
|
setProfile(nextProfile);
|
|
122
129
|
setIsProspectFlow(false);
|
|
123
130
|
setError(null);
|
|
131
|
+
setBusyMessage('Creating Tax Axis session...');
|
|
124
132
|
setIsBusy(true);
|
|
125
133
|
const createdSessionId = yield createSessionIfNeeded(nextProfile);
|
|
126
134
|
setIsBusy(false);
|
|
@@ -163,7 +171,7 @@ const TaxAxisShell = ({ taxAxisApi, userContext = 'expert', initialSessionId, in
|
|
|
163
171
|
documentType: doc.id.toUpperCase().replace(/[^A-Z0-9]+/g, '_'),
|
|
164
172
|
s3Key: uploadClient.state.fileName,
|
|
165
173
|
};
|
|
166
|
-
|
|
174
|
+
return taxAxisApi.uploadFile(uploadInput);
|
|
167
175
|
}), [
|
|
168
176
|
createSessionIfNeeded,
|
|
169
177
|
documentUploadUrl,
|
|
@@ -174,22 +182,40 @@ const TaxAxisShell = ({ taxAxisApi, userContext = 'expert', initialSessionId, in
|
|
|
174
182
|
taxAxisApi,
|
|
175
183
|
uploadBucketName,
|
|
176
184
|
]);
|
|
185
|
+
const handleDeleteDocument = (0, react_1.useCallback)((documentId) => __awaiter(void 0, void 0, void 0, function* () {
|
|
186
|
+
yield taxAxisApi.deleteDocument(documentId);
|
|
187
|
+
}), [taxAxisApi]);
|
|
177
188
|
const handleAnalyzeDocuments = (0, react_1.useCallback)(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
178
189
|
if (!profile)
|
|
179
190
|
return;
|
|
180
191
|
setError(null);
|
|
192
|
+
setBusyMessage('Preparing documents for analysis...');
|
|
181
193
|
setIsBusy(true);
|
|
182
194
|
const ensuredSessionId = yield createSessionIfNeeded(profile);
|
|
183
|
-
if (ensuredSessionId) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
195
|
+
if (!ensuredSessionId) {
|
|
196
|
+
setIsBusy(false);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
setBusyMessage('Verifying all documents are parsed...');
|
|
201
|
+
yield taxAxisApi.analyzeDocuments(ensuredSessionId);
|
|
202
|
+
setBusyMessage('Generating strategy results...');
|
|
203
|
+
setStep('PROCESSING');
|
|
204
|
+
const llmResponse = yield taxAxisApi.runLlm(ensuredSessionId);
|
|
205
|
+
if (llmResponse === null || llmResponse === void 0 ? void 0 : llmResponse.outputPayload) {
|
|
206
|
+
setLlmResult(llmResponse.outputPayload);
|
|
189
207
|
}
|
|
190
208
|
}
|
|
209
|
+
catch (requestError) {
|
|
210
|
+
const msg = requestError instanceof Error
|
|
211
|
+
? requestError.message
|
|
212
|
+
: 'Analysis did not complete successfully. Please review uploaded files.';
|
|
213
|
+
setError(msg);
|
|
214
|
+
setStep('DOCUMENT_UPLOAD');
|
|
215
|
+
setIsBusy(false);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
191
218
|
setIsBusy(false);
|
|
192
|
-
setStep('PROCESSING');
|
|
193
219
|
}), [createSessionIfNeeded, profile, taxAxisApi]);
|
|
194
220
|
const handleSendReport = (0, react_1.useCallback)(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
195
221
|
if (!sessionId) {
|
|
@@ -202,7 +228,10 @@ const TaxAxisShell = ({ taxAxisApi, userContext = 'expert', initialSessionId, in
|
|
|
202
228
|
yield taxAxisApi.generatePdf(sessionId);
|
|
203
229
|
}
|
|
204
230
|
catch (requestError) {
|
|
205
|
-
|
|
231
|
+
const msg = requestError instanceof Error
|
|
232
|
+
? requestError.message
|
|
233
|
+
: 'Failed to request report delivery. Please try again.';
|
|
234
|
+
setError(msg);
|
|
206
235
|
}
|
|
207
236
|
finally {
|
|
208
237
|
setIsBusy(false);
|
|
@@ -223,7 +252,20 @@ const TaxAxisShell = ({ taxAxisApi, userContext = 'expert', initialSessionId, in
|
|
|
223
252
|
react_1.default.createElement(TaxAxisProspectReport_1.TaxAxisProspectReport, { profile: profile, userContext: userContext, onUpgrade: () => setStep('DOCUMENT_UPLOAD'), onPresent: () => setStep('PRESENTATION'), onReset: handleReset })));
|
|
224
253
|
case 'DOCUMENT_UPLOAD':
|
|
225
254
|
return (react_1.default.createElement(ShellContainer, null,
|
|
226
|
-
react_1.default.createElement(TaxAxisDocuments_1.TaxAxisDocuments, { profile: profile, userContext: userContext, onUploadDocument: handleUploadDocument,
|
|
255
|
+
react_1.default.createElement(TaxAxisDocuments_1.TaxAxisDocuments, { profile: profile, userContext: userContext, onUploadDocument: handleUploadDocument, onDeleteDocument: handleDeleteDocument, fetchUploadedDocuments: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
256
|
+
const ensuredSessionId = sessionId || (profile ? yield createSessionIfNeeded(profile) : null);
|
|
257
|
+
if (!ensuredSessionId) {
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
const docs = yield taxAxisApi.getDocuments(ensuredSessionId);
|
|
261
|
+
return docs.map((doc) => ({
|
|
262
|
+
fileName: doc.fileName,
|
|
263
|
+
status: doc.status,
|
|
264
|
+
documentType: doc.documentType,
|
|
265
|
+
parseError: doc.parseError,
|
|
266
|
+
updatedAt: doc.updatedAt,
|
|
267
|
+
}));
|
|
268
|
+
}), onContinue: handleAnalyzeDocuments, onBack: () => isProspectFlow ? setStep('PROSPECT_REPORT') : setStep('SESSION_SETUP') })));
|
|
227
269
|
case 'PROCESSING':
|
|
228
270
|
return (react_1.default.createElement(ShellContainer, null,
|
|
229
271
|
react_1.default.createElement(TaxAxisProcessing_1.TaxAxisProcessing, { profile: profile, userContext: userContext, onComplete: () => setStep('DASHBOARD') })));
|
|
@@ -232,7 +274,7 @@ const TaxAxisShell = ({ taxAxisApi, userContext = 'expert', initialSessionId, in
|
|
|
232
274
|
react_1.default.createElement(TaxAxisExtractionReview_1.TaxAxisExtractionReview, { userContext: userContext, onBack: () => setStep('DASHBOARD'), onConfirmAndUnlock: () => setStep('DASHBOARD') })));
|
|
233
275
|
case 'DASHBOARD':
|
|
234
276
|
return (react_1.default.createElement(ShellContainer, null,
|
|
235
|
-
react_1.default.createElement(TaxAxisDashboard_1.TaxAxisDashboard, { profile: profile, userContext: userContext, onDownloadClient: () => setStep('CLIENT_REPORT'), onDownloadPreparer: () => setStep('PREPARER_WORKPAPER'), onPresent: () => setStep('PRESENTATION'), onSend: handleSendReport, onReviewData: () => setStep('PARSED_REVIEW'), onReset: handleReset })));
|
|
277
|
+
react_1.default.createElement(TaxAxisDashboard_1.TaxAxisDashboard, { profile: profile, userContext: userContext, llmResult: llmResult, onDownloadClient: () => setStep('CLIENT_REPORT'), onDownloadPreparer: () => setStep('PREPARER_WORKPAPER'), onPresent: () => setStep('PRESENTATION'), onSend: handleSendReport, onReviewData: () => setStep('PARSED_REVIEW'), onReset: handleReset })));
|
|
236
278
|
case 'CLIENT_REPORT':
|
|
237
279
|
return (react_1.default.createElement(ShellContainer, { fullWidth: true },
|
|
238
280
|
react_1.default.createElement(TaxAxisClientReport_1.TaxAxisClientReport, { profile: profile, userContext: userContext, onBack: () => setStep('DASHBOARD'), onNavigatePreparer: () => setStep('PREPARER_WORKPAPER') })));
|
|
@@ -255,13 +297,21 @@ const TaxAxisShell = ({ taxAxisApi, userContext = 'expert', initialSessionId, in
|
|
|
255
297
|
isProspectFlow,
|
|
256
298
|
handleAnalyzeDocuments,
|
|
257
299
|
handleUploadDocument,
|
|
300
|
+
handleDeleteDocument,
|
|
258
301
|
handleReset,
|
|
259
302
|
handleSendReport,
|
|
303
|
+
llmResult,
|
|
260
304
|
]);
|
|
261
305
|
return (react_1.default.createElement(react_1.default.Fragment, null,
|
|
262
|
-
error && (react_1.default.createElement("div", { className: 'fixed right-4 top-4 z-[200] rounded-lg border border-
|
|
306
|
+
error && (react_1.default.createElement("div", { className: 'fixed right-4 top-4 z-[200] max-w-sm rounded-lg border border-red-500/30 bg-tax-axis-surface px-4 py-3 text-xs text-red-200 shadow-lg' },
|
|
307
|
+
react_1.default.createElement("div", { className: 'flex items-start gap-2' },
|
|
308
|
+
react_1.default.createElement("span", { className: 'flex-1' }, error),
|
|
309
|
+
react_1.default.createElement("button", { onClick: () => setError(null), className: 'bg-transparent border-none text-red-300 text-sm cursor-pointer p-0 leading-none' }, "\u00D7")))),
|
|
263
310
|
isBusy && (react_1.default.createElement("div", { className: 'fixed inset-0 z-[180] bg-black/25' },
|
|
264
|
-
react_1.default.createElement("div", { className: 'absolute left-1/2 top-6 -translate-x-1/2 rounded-md bg-tax-axis-surface px-
|
|
311
|
+
react_1.default.createElement("div", { className: 'absolute left-1/2 top-6 -translate-x-1/2 rounded-md bg-tax-axis-surface px-4 py-2.5 text-xs text-tax-axis-text shadow-lg border border-tax-axis-border' },
|
|
312
|
+
react_1.default.createElement("div", { className: 'flex items-center gap-2' },
|
|
313
|
+
react_1.default.createElement("div", { className: 'w-3 h-3 rounded-full animate-spin flex-shrink-0', style: { border: '2px solid transparent', borderTopColor: '#248384' } }),
|
|
314
|
+
busyMessage)))),
|
|
265
315
|
currentView));
|
|
266
316
|
};
|
|
267
317
|
exports.TaxAxisShell = TaxAxisShell;
|
|
@@ -25,7 +25,6 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
26
|
exports.DashboardActions = DashboardActions;
|
|
27
27
|
const react_1 = __importStar(require("react"));
|
|
28
|
-
const data_1 = require("../../lib/data");
|
|
29
28
|
const compute_1 = require("../../lib/compute");
|
|
30
29
|
const TaxAxisButton_1 = require("../shared/TaxAxisButton");
|
|
31
30
|
function DashboardActions({ profile, dashEligible, computed, onDownloadPreparer, onPresent, onSend, onReset, }) {
|
|
@@ -69,7 +68,7 @@ function DashboardActions({ profile, dashEligible, computed, onDownloadPreparer,
|
|
|
69
68
|
"\u2013",
|
|
70
69
|
(0, compute_1.fmtK)(totalHi),
|
|
71
70
|
"across ",
|
|
72
|
-
|
|
71
|
+
dashEligible.length,
|
|
73
72
|
" strategies \u2014 use this to close the engagement or expand scope."),
|
|
74
73
|
react_1.default.createElement("div", { className: "flex gap-2.5 flex-wrap" },
|
|
75
74
|
react_1.default.createElement(TaxAxisButton_1.TaxAxisButton, { variant: "orange", onClick: onSend }, "Send Report to Client"),
|
|
@@ -77,7 +76,7 @@ function DashboardActions({ profile, dashEligible, computed, onDownloadPreparer,
|
|
|
77
76
|
react_1.default.createElement(TaxAxisButton_1.TaxAxisButton, { variant: "secondary" }, "Generate Engagement Letter")))),
|
|
78
77
|
react_1.default.createElement("div", { className: "flex gap-4 mt-4 pt-3.5", style: { borderTop: "1px solid rgba(36,131,132,0.15)" } }, [
|
|
79
78
|
{ v: "3x", l: "Faster than Manual" },
|
|
80
|
-
{ v:
|
|
79
|
+
{ v: String(dashEligible.length), l: "Strategies Found" },
|
|
81
80
|
{ v: "100%", l: "IRS-Cited" },
|
|
82
81
|
{ v: String(stateCount), l: `State${stateCount > 1 ? "s" : ""}` },
|
|
83
82
|
].map(({ v, l }) => (react_1.default.createElement("div", { key: l },
|
|
@@ -78,7 +78,7 @@ function DashboardSummary({ profile, dashEligible, computed, dataConfirmed, revi
|
|
|
78
78
|
profile.cpaName)),
|
|
79
79
|
react_1.default.createElement("div", { className: "flex gap-6 mt-[22px] pt-[18px]", style: { borderTop: "1px solid rgba(36,131,132,0.12)" } }, [
|
|
80
80
|
{ v: String(scoreUp), u: "/100", l: "Avg. Confidence", warn: false, ok: false },
|
|
81
|
-
{ v:
|
|
81
|
+
{ v: String(dashEligible.length), u: "", l: "Strategies Identified", warn: false, ok: false },
|
|
82
82
|
{
|
|
83
83
|
v: dataConfirmed ? "Done" : String(reviewUnreviewed),
|
|
84
84
|
u: "",
|
|
@@ -1,7 +1,62 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { ClientProfile, TaxAxisScreenProps } from "../../lib/types";
|
|
3
|
+
export interface LlmStrategy {
|
|
4
|
+
strategyType: string;
|
|
5
|
+
applicable: boolean;
|
|
6
|
+
priority: string;
|
|
7
|
+
estimatedSavings: {
|
|
8
|
+
min: number;
|
|
9
|
+
max: number;
|
|
10
|
+
};
|
|
11
|
+
summary: string;
|
|
12
|
+
implementationSteps: string[];
|
|
13
|
+
requiredForms: string[];
|
|
14
|
+
strategyId?: string;
|
|
15
|
+
status?: "COMPLETE" | "FAILED";
|
|
16
|
+
error?: string;
|
|
17
|
+
weightedScore?: number;
|
|
18
|
+
quickWin?: boolean;
|
|
19
|
+
calculationTrace?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
export interface LlmExcludedStrategy {
|
|
22
|
+
strategy_id: string;
|
|
23
|
+
status?: "EXCLUDED";
|
|
24
|
+
reason: string;
|
|
25
|
+
irs_cite?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface LlmResult {
|
|
28
|
+
strategies: LlmStrategy[];
|
|
29
|
+
summary: {
|
|
30
|
+
applicableCount: number;
|
|
31
|
+
totalStrategies: number;
|
|
32
|
+
highPriorityCount: number;
|
|
33
|
+
quickWinCount: number;
|
|
34
|
+
notEligibleCount: number;
|
|
35
|
+
estimatedSavingsMin: number;
|
|
36
|
+
estimatedSavingsMax: number;
|
|
37
|
+
failedCount?: number;
|
|
38
|
+
top3?: string[];
|
|
39
|
+
};
|
|
40
|
+
meta: {
|
|
41
|
+
provider: string;
|
|
42
|
+
tokenCount?: number;
|
|
43
|
+
totalTokens?: number;
|
|
44
|
+
inputTokens?: number;
|
|
45
|
+
outputTokens?: number;
|
|
46
|
+
latencyMs?: number;
|
|
47
|
+
sourceDocumentCount: number;
|
|
48
|
+
parsedDocumentCount: number;
|
|
49
|
+
detectedDocumentTypes: string[];
|
|
50
|
+
modelId?: string;
|
|
51
|
+
promptVersion?: string;
|
|
52
|
+
};
|
|
53
|
+
excludedStrategies?: LlmExcludedStrategy[];
|
|
54
|
+
engineOutput?: Record<string, unknown>;
|
|
55
|
+
rawOutput?: Record<string, unknown>;
|
|
56
|
+
}
|
|
3
57
|
export interface TaxAxisDashboardProps extends TaxAxisScreenProps {
|
|
4
58
|
profile: ClientProfile;
|
|
59
|
+
llmResult?: LlmResult | null;
|
|
5
60
|
onDownloadClient: () => void;
|
|
6
61
|
onDownloadPreparer: () => void;
|
|
7
62
|
onPresent: () => void;
|
|
@@ -9,4 +64,4 @@ export interface TaxAxisDashboardProps extends TaxAxisScreenProps {
|
|
|
9
64
|
onReset: () => void;
|
|
10
65
|
onReviewData?: () => void;
|
|
11
66
|
}
|
|
12
|
-
export declare function TaxAxisDashboard({ profile, onDownloadClient, onDownloadPreparer, onPresent, onSend, onReset, onReviewData, userContext: _userContext, }: TaxAxisDashboardProps): React.JSX.Element;
|
|
67
|
+
export declare function TaxAxisDashboard({ profile, llmResult, onDownloadClient, onDownloadPreparer, onPresent, onSend, onReset, onReviewData, userContext: _userContext, }: TaxAxisDashboardProps): React.JSX.Element;
|
|
@@ -34,6 +34,64 @@ const DashboardTopBar_1 = require("./DashboardTopBar");
|
|
|
34
34
|
const DashboardActions_1 = require("./DashboardActions");
|
|
35
35
|
const StrategyTile_1 = require("./StrategyTile");
|
|
36
36
|
const StrategyDetailPanel_1 = require("./StrategyDetailPanel");
|
|
37
|
+
const STRATEGY_TYPE_LABELS = {
|
|
38
|
+
ENTITY_RESTRUCTURING: "Entity Restructuring",
|
|
39
|
+
QBI_199A_OPTIMIZATION: "QBI §199A Optimization",
|
|
40
|
+
DOCUMENTATION_GAP_REMEDIATION: "Documentation Gap Remediation",
|
|
41
|
+
RETIREMENT_PLAN: "Retirement Plan Optimization",
|
|
42
|
+
DEPRECIATION_ACCELERATION: "Depreciation Acceleration",
|
|
43
|
+
COST_SEGREGATION: "Cost Segregation",
|
|
44
|
+
RD_CREDIT: "R&D Tax Credit",
|
|
45
|
+
HSA_MAXIMIZATION: "HSA Maximization",
|
|
46
|
+
SALT_PTE: "SALT / PTE Optimization",
|
|
47
|
+
WOTC: "Work Opportunity Tax Credit",
|
|
48
|
+
INCOME_DEFERRAL: "Income Deferral",
|
|
49
|
+
CHARITABLE_GIVING: "Charitable Giving Strategy",
|
|
50
|
+
FAMILY_EMPLOYMENT: "Family Employment",
|
|
51
|
+
MEALS_DEDUCTIONS: "Business Meals Deduction",
|
|
52
|
+
OPPORTUNITY_ZONE: "Opportunity Zone Investment",
|
|
53
|
+
ACCOUNTING_METHOD: "Accounting Method Change",
|
|
54
|
+
BONUS_DEPRECIATION: "Bonus Depreciation §168(k)",
|
|
55
|
+
SECTION_179: "Section 179 Expensing",
|
|
56
|
+
};
|
|
57
|
+
function humanizeStrategyType(raw) {
|
|
58
|
+
if (STRATEGY_TYPE_LABELS[raw])
|
|
59
|
+
return STRATEGY_TYPE_LABELS[raw];
|
|
60
|
+
return raw
|
|
61
|
+
.replace(/_/g, " ")
|
|
62
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
63
|
+
}
|
|
64
|
+
function mapLlmToStrategies(llm) {
|
|
65
|
+
return llm.strategies
|
|
66
|
+
.filter((s) => s.applicable)
|
|
67
|
+
.map((s, idx) => ({
|
|
68
|
+
rank: idx + 1,
|
|
69
|
+
code: s.strategyType,
|
|
70
|
+
name: humanizeStrategyType(s.strategyType),
|
|
71
|
+
cat: "income",
|
|
72
|
+
priority: (s.priority || "MEDIUM").toUpperCase(),
|
|
73
|
+
score: s.priority === "HIGH" ? 85 : s.priority === "MEDIUM" ? 70 : 55,
|
|
74
|
+
entities: [],
|
|
75
|
+
lo: s.estimatedSavings.min,
|
|
76
|
+
hi: s.estimatedSavings.max,
|
|
77
|
+
timeline: s.priority === "HIGH" ? "Act now" : "Within 90 days",
|
|
78
|
+
timelineBucket: s.priority === "HIGH" ? "now" : "90d",
|
|
79
|
+
clientBrief: s.summary,
|
|
80
|
+
action: s.implementationSteps.join(". "),
|
|
81
|
+
forms: s.requiredForms.join(", "),
|
|
82
|
+
abstract: s.summary,
|
|
83
|
+
sources: [],
|
|
84
|
+
trace: [],
|
|
85
|
+
cost: undefined,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
function buildLlmComputed(strategies) {
|
|
89
|
+
const map = new Map();
|
|
90
|
+
for (const s of strategies) {
|
|
91
|
+
map.set(s.rank, { lo: s.lo, hi: s.hi, sources: s.sources, trace: s.trace });
|
|
92
|
+
}
|
|
93
|
+
return map;
|
|
94
|
+
}
|
|
37
95
|
// ─── Formatting helper ──────────────────────────────────────────
|
|
38
96
|
const fmtK = (n) => `$${(n / 1000).toFixed(n % 1000 ? 1 : 0)}K`;
|
|
39
97
|
// ─── Timeline buckets for Client Summary ────────────────────────
|
|
@@ -42,10 +100,14 @@ const BUCKETS = [
|
|
|
42
100
|
{ key: "30d", label: "Within 30 Days", desc: "Documentation or payroll changes" },
|
|
43
101
|
{ key: "90d", label: "Within 90 Days", desc: "Structural changes — new accounts or plan setup" },
|
|
44
102
|
];
|
|
45
|
-
function TaxAxisDashboard({ profile, onDownloadClient, onDownloadPreparer, onPresent, onSend, onReset, onReviewData, userContext: _userContext = "expert", }) {
|
|
103
|
+
function TaxAxisDashboard({ profile, llmResult, onDownloadClient, onDownloadPreparer, onPresent, onSend, onReset, onReviewData, userContext: _userContext = "expert", }) {
|
|
104
|
+
var _a, _b;
|
|
105
|
+
const hasLlm = !!((_a = llmResult === null || llmResult === void 0 ? void 0 : llmResult.strategies) === null || _a === void 0 ? void 0 : _a.length);
|
|
46
106
|
// ─── Derived data ──────────────────────────────────────────────
|
|
47
|
-
const
|
|
48
|
-
const
|
|
107
|
+
const llmStrategies = (0, react_1.useMemo)(() => (hasLlm ? mapLlmToStrategies(llmResult) : []), [hasLlm, llmResult]);
|
|
108
|
+
const llmComputed = (0, react_1.useMemo)(() => (hasLlm ? buildLlmComputed(llmStrategies) : new Map()), [hasLlm, llmStrategies]);
|
|
109
|
+
const computed = (0, react_1.useMemo)(() => (hasLlm ? llmComputed : (0, compute_1.computeAllStrategies)(profile)), [hasLlm, llmComputed, profile]);
|
|
110
|
+
const dashEligible = (0, react_1.useMemo)(() => (hasLlm ? llmStrategies : (0, compute_1.filterEligibleStrategies)(profile)), [hasLlm, llmStrategies, profile]);
|
|
49
111
|
const maxSavings = Math.max(...dashEligible.map((s) => { var _a, _b; return (_b = (_a = computed.get(s.rank)) === null || _a === void 0 ? void 0 : _a.hi) !== null && _b !== void 0 ? _b : s.hi; }), 1);
|
|
50
112
|
// ─── State ─────────────────────────────────────────────────────
|
|
51
113
|
const [topTab, setTopTab] = (0, react_1.useState)("extraction");
|
|
@@ -80,6 +142,25 @@ function TaxAxisDashboard({ profile, onDownloadClient, onDownloadPreparer, onPre
|
|
|
80
142
|
// Sorted strategies for impact distribution
|
|
81
143
|
const sortedByImpact = (0, react_1.useMemo)(() => [...dashEligible].sort((a, b) => { var _a, _b, _c, _d; return ((_b = (_a = computed.get(b.rank)) === null || _a === void 0 ? void 0 : _a.hi) !== null && _b !== void 0 ? _b : b.hi) - ((_d = (_c = computed.get(a.rank)) === null || _c === void 0 ? void 0 : _c.hi) !== null && _d !== void 0 ? _d : a.hi); }), [dashEligible, computed]);
|
|
82
144
|
return (react_1.default.createElement("div", null,
|
|
145
|
+
hasLlm && (llmResult === null || llmResult === void 0 ? void 0 : llmResult.meta) && (react_1.default.createElement("div", { className: "rounded-lg mb-3 px-4 py-2.5 flex items-center justify-between text-[11px] font-tax-axis-mono", style: {
|
|
146
|
+
background: "rgba(36,131,132,0.06)",
|
|
147
|
+
border: "1px solid rgba(36,131,132,0.15)",
|
|
148
|
+
color: "#9498B8",
|
|
149
|
+
} },
|
|
150
|
+
react_1.default.createElement("span", null,
|
|
151
|
+
"Source: ",
|
|
152
|
+
react_1.default.createElement("strong", { className: "text-tax-axis-teal-light" }, llmResult.meta.provider),
|
|
153
|
+
" · ",
|
|
154
|
+
llmResult.meta.parsedDocumentCount,
|
|
155
|
+
" documents parsed",
|
|
156
|
+
" · ",
|
|
157
|
+
((_b = llmResult.meta.detectedDocumentTypes) === null || _b === void 0 ? void 0 : _b.length) || 0,
|
|
158
|
+
" document types detected"),
|
|
159
|
+
react_1.default.createElement("span", { className: "text-tax-axis-text-4" }, (() => {
|
|
160
|
+
var _a, _b;
|
|
161
|
+
const tokens = (_b = (_a = llmResult.meta.totalTokens) !== null && _a !== void 0 ? _a : llmResult.meta.tokenCount) !== null && _b !== void 0 ? _b : 0;
|
|
162
|
+
return tokens > 0 ? `${tokens} tokens` : "";
|
|
163
|
+
})()))),
|
|
83
164
|
react_1.default.createElement(DashboardSummary_1.DashboardSummary, { profile: profile, dashEligible: dashEligible, computed: computed, dataConfirmed: dataConfirmed, reviewUnreviewed: reviewStatus.unreviewed }),
|
|
84
165
|
react_1.default.createElement(DashboardTopBar_1.DashboardTopBar, { topTab: topTab, setTopTab: setTopTab, dataConfirmed: dataConfirmed, reviewUnreviewed: reviewStatus.unreviewed }),
|
|
85
166
|
topTab === "extraction" && (react_1.default.createElement("div", null,
|
|
@@ -197,7 +278,7 @@ function TaxAxisDashboard({ profile, onDownloadClient, onDownloadPreparer, onPre
|
|
|
197
278
|
const c = computed.get(s.rank);
|
|
198
279
|
return (react_1.default.createElement(StrategyTile_1.StrategyTile, { key: s.rank, s: Object.assign(Object.assign({}, s), { lo: (_a = c === null || c === void 0 ? void 0 : c.lo) !== null && _a !== void 0 ? _a : s.lo, hi: (_b = c === null || c === void 0 ? void 0 : c.hi) !== null && _b !== void 0 ? _b : s.hi }), delay: 0.2 + i * 0.06, onClick: () => setSelected(s), maxSavings: maxSavings }));
|
|
199
280
|
})),
|
|
200
|
-
react_1.default.createElement("div", { className: "rounded-[14px] overflow-hidden mb-4", style: {
|
|
281
|
+
!hasLlm && (react_1.default.createElement("div", { className: "rounded-[14px] overflow-hidden mb-4", style: {
|
|
201
282
|
background: "#0E1132",
|
|
202
283
|
border: "1px solid rgba(36,131,132,0.12)",
|
|
203
284
|
boxShadow: "0 2px 8px rgba(6,8,33,0.3)",
|
|
@@ -210,7 +291,7 @@ function TaxAxisDashboard({ profile, onDownloadClient, onDownloadPreparer, onPre
|
|
|
210
291
|
react_1.default.createElement("span", { className: "text-[13px] text-tax-axis-text" }, a.name),
|
|
211
292
|
react_1.default.createElement("div", { className: "flex items-center gap-2.5" },
|
|
212
293
|
react_1.default.createElement("span", { className: "text-[11px] font-tax-axis-mono text-tax-axis-text-2" }, a.savings),
|
|
213
|
-
react_1.default.createElement(TaxAxisBadge_1.TaxAxisBadge, { color: a.priority === "MEDIUM" ? "orange" : "neutral", size: "xs" }, a.priority)))))),
|
|
294
|
+
react_1.default.createElement(TaxAxisBadge_1.TaxAxisBadge, { color: a.priority === "MEDIUM" ? "orange" : "neutral", size: "xs" }, a.priority))))))),
|
|
214
295
|
react_1.default.createElement("div", { className: "rounded-[10px] mb-4 text-xs text-tax-axis-text leading-[1.7] font-tax-axis-body", style: {
|
|
215
296
|
padding: "14px 18px",
|
|
216
297
|
background: "#0E1132",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { DocSpec } from "../../lib/types";
|
|
3
|
-
export type DocStatus = "empty" | "validating" | "valid";
|
|
3
|
+
export type DocStatus = "empty" | "validating" | "parsing" | "failed" | "valid";
|
|
4
4
|
export interface DocState extends DocSpec {
|
|
5
5
|
status: DocStatus;
|
|
6
6
|
fileName: string | null;
|
|
7
|
+
parseError?: string | null;
|
|
7
8
|
}
|
|
8
9
|
interface DocumentCardProps {
|
|
9
10
|
doc: DocState;
|
|
@@ -13,6 +14,7 @@ interface DocumentCardProps {
|
|
|
13
14
|
helpOverride?: string;
|
|
14
15
|
onUpload: (file: File) => void;
|
|
15
16
|
onClear: () => void;
|
|
17
|
+
onRemove?: () => void;
|
|
16
18
|
}
|
|
17
|
-
export declare function DocumentCard({ doc, tierBorderColor, tierBadgeColor, tierBadgeText, helpOverride, onUpload, onClear, }: DocumentCardProps): React.JSX.Element;
|
|
19
|
+
export declare function DocumentCard({ doc, tierBorderColor, tierBadgeColor, tierBadgeText, helpOverride, onUpload, onClear, onRemove, }: DocumentCardProps): React.JSX.Element;
|
|
18
20
|
export {};
|
|
@@ -6,8 +6,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.DocumentCard = DocumentCard;
|
|
7
7
|
const react_1 = __importDefault(require("react"));
|
|
8
8
|
const TaxAxisBadge_1 = require("../shared/TaxAxisBadge");
|
|
9
|
-
// Inline style values per status — mock T hex values for SVG strokes and
|
|
10
|
-
// rgba borders that don't have direct Tailwind token equivalents.
|
|
11
9
|
const STATUS_STYLES = {
|
|
12
10
|
empty: {
|
|
13
11
|
iconBg: "#171B44",
|
|
@@ -19,17 +17,28 @@ const STATUS_STYLES = {
|
|
|
19
17
|
iconBorder: "rgba(251,154,29,0.25)",
|
|
20
18
|
cardBorder: "rgba(251,154,29,0.25)",
|
|
21
19
|
},
|
|
20
|
+
parsing: {
|
|
21
|
+
iconBg: "rgba(36,131,132,0.08)",
|
|
22
|
+
iconBorder: "rgba(36,131,132,0.25)",
|
|
23
|
+
cardBorder: "rgba(36,131,132,0.25)",
|
|
24
|
+
},
|
|
25
|
+
failed: {
|
|
26
|
+
iconBg: "rgba(197,48,48,0.08)",
|
|
27
|
+
iconBorder: "rgba(197,48,48,0.25)",
|
|
28
|
+
cardBorder: "rgba(197,48,48,0.25)",
|
|
29
|
+
},
|
|
22
30
|
valid: {
|
|
23
31
|
iconBg: "rgba(15,110,86,0.08)",
|
|
24
32
|
iconBorder: "rgba(15,110,86,0.25)",
|
|
25
33
|
cardBorder: "rgba(15,110,86,0.25)",
|
|
26
34
|
},
|
|
27
35
|
};
|
|
28
|
-
function DocumentCard({ doc, tierBorderColor, tierBadgeColor, tierBadgeText, helpOverride, onUpload, onClear, }) {
|
|
29
|
-
var _a;
|
|
36
|
+
function DocumentCard({ doc, tierBorderColor, tierBadgeColor, tierBadgeText, helpOverride, onUpload, onClear, onRemove, }) {
|
|
37
|
+
var _a, _b, _c;
|
|
30
38
|
const ss = STATUS_STYLES[doc.status];
|
|
31
39
|
const leftBorder = doc.status === "valid" ? "rgba(15,110,86,0.6)" : tierBorderColor;
|
|
32
40
|
const fileInputId = `tax-axis-upload-${doc.id}`;
|
|
41
|
+
const reuploadInputId = `tax-axis-reupload-${doc.id}`;
|
|
33
42
|
return (react_1.default.createElement("div", { className: "bg-tax-axis-surface overflow-hidden", style: {
|
|
34
43
|
border: `1px solid ${ss.cardBorder}`,
|
|
35
44
|
borderLeft: `3px solid ${leftBorder}`,
|
|
@@ -43,20 +52,42 @@ function DocumentCard({ doc, tierBorderColor, tierBadgeColor, tierBadgeText, hel
|
|
|
43
52
|
} }, doc.status === "validating" ? (react_1.default.createElement("div", { className: "w-3.5 h-3.5 rounded-full animate-spin", style: {
|
|
44
53
|
border: "2px solid transparent",
|
|
45
54
|
borderTopColor: "#FB9A1D",
|
|
46
|
-
} })) : doc.status === "
|
|
55
|
+
} })) : doc.status === "parsing" ? (react_1.default.createElement("div", { className: "w-3.5 h-3.5 rounded-full animate-spin", style: {
|
|
56
|
+
border: "2px solid transparent",
|
|
57
|
+
borderTopColor: "#248384",
|
|
58
|
+
} })) : doc.status === "failed" ? (react_1.default.createElement("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none" },
|
|
59
|
+
react_1.default.createElement("path", { d: "M4 4l6 6M10 4L4 10", stroke: "#C53030", strokeWidth: "2", strokeLinecap: "round" }))) : doc.status === "valid" ? (react_1.default.createElement("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none" },
|
|
47
60
|
react_1.default.createElement("path", { d: "M2.5 7l3.5 3.5 5.5-6", stroke: "#0F6E56", strokeWidth: "2", strokeLinecap: "round" }))) : (react_1.default.createElement("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none" },
|
|
48
61
|
react_1.default.createElement("circle", { cx: "7", cy: "7", r: "5", stroke: "#9498B8", strokeWidth: "1.5", strokeDasharray: "3 3" })))),
|
|
49
62
|
react_1.default.createElement("div", { className: "flex-1 min-w-0" },
|
|
50
63
|
react_1.default.createElement("div", { className: "flex items-center gap-1.5" },
|
|
51
64
|
react_1.default.createElement("span", { className: "text-[13px] font-medium text-white font-tax-axis-body" }, doc.name),
|
|
52
|
-
doc.status === "empty" && (react_1.default.createElement(TaxAxisBadge_1.TaxAxisBadge, { color: tierBadgeColor, size: "xs" }, tierBadgeText))
|
|
53
|
-
|
|
65
|
+
doc.status === "empty" && (react_1.default.createElement(TaxAxisBadge_1.TaxAxisBadge, { color: tierBadgeColor, size: "xs" }, tierBadgeText)),
|
|
66
|
+
doc.status === "failed" && (react_1.default.createElement(TaxAxisBadge_1.TaxAxisBadge, { color: "red", size: "xs" }, "FAILED"))),
|
|
67
|
+
react_1.default.createElement("div", { className: "text-[11px] text-tax-axis-text-3 font-tax-axis-body mt-0.5" }, doc.status === "failed" && doc.parseError
|
|
68
|
+
? doc.parseError
|
|
69
|
+
: (_a = doc.fileName) !== null && _a !== void 0 ? _a : (helpOverride !== null && helpOverride !== void 0 ? helpOverride : doc.help))),
|
|
54
70
|
react_1.default.createElement("div", { className: "flex items-center gap-2 flex-shrink-0" },
|
|
55
71
|
doc.status === "valid" && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
56
|
-
react_1.default.createElement("span", { className: "text-[10px] font-semibold font-tax-axis-mono", style: { color: "#0F6E56" } }, "
|
|
72
|
+
react_1.default.createElement("span", { className: "text-[10px] font-semibold font-tax-axis-mono", style: { color: "#0F6E56" } }, "Parsed"),
|
|
57
73
|
react_1.default.createElement("button", { onClick: onClear, className: "bg-transparent border-none p-1 text-tax-axis-text-4 text-sm cursor-pointer" }, "\u00D7"))),
|
|
74
|
+
(doc.status === "parsing" || doc.status === "validating") && (react_1.default.createElement("span", { className: "text-[10px] font-semibold font-tax-axis-mono", style: { color: doc.status === "parsing" ? "#248384" : "#FB9A1D" } }, doc.status === "parsing" ? "Parsing…" : "Uploading…")),
|
|
75
|
+
doc.status === "failed" && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
76
|
+
react_1.default.createElement("input", { id: reuploadInputId, type: "file", className: "hidden", accept: (_b = doc.accept) === null || _b === void 0 ? void 0 : _b.join(","), onChange: (event) => {
|
|
77
|
+
var _a;
|
|
78
|
+
const file = (_a = event.target.files) === null || _a === void 0 ? void 0 : _a[0];
|
|
79
|
+
if (file) {
|
|
80
|
+
onUpload(file);
|
|
81
|
+
}
|
|
82
|
+
event.currentTarget.value = "";
|
|
83
|
+
} }),
|
|
84
|
+
react_1.default.createElement("label", { htmlFor: reuploadInputId, className: "rounded-md px-2.5 py-1 text-[10px] font-semibold text-tax-axis-teal-light font-tax-axis-mono cursor-pointer", style: {
|
|
85
|
+
background: "rgba(36,131,132,0.10)",
|
|
86
|
+
border: "1px solid rgba(36,131,132,0.2)",
|
|
87
|
+
} }, "Re-upload"),
|
|
88
|
+
react_1.default.createElement("button", { onClick: onRemove || onClear, className: "bg-transparent border-none p-1 text-tax-axis-text-4 text-sm cursor-pointer", title: "Remove document" }, "\u00D7"))),
|
|
58
89
|
doc.status === "empty" && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
59
|
-
react_1.default.createElement("input", { id: fileInputId, type: "file", className: "hidden", onChange: (event) => {
|
|
90
|
+
react_1.default.createElement("input", { id: fileInputId, type: "file", className: "hidden", accept: (_c = doc.accept) === null || _c === void 0 ? void 0 : _c.join(","), onChange: (event) => {
|
|
60
91
|
var _a;
|
|
61
92
|
const file = (_a = event.target.files) === null || _a === void 0 ? void 0 : _a[0];
|
|
62
93
|
if (file) {
|
|
@@ -16,6 +16,7 @@ interface DocumentTierProps {
|
|
|
16
16
|
helpOverrides: Record<string, string>;
|
|
17
17
|
onUpload: (idx: number, file: File) => void;
|
|
18
18
|
onClear: (idx: number) => void;
|
|
19
|
+
onRemove?: (idx: number) => void;
|
|
19
20
|
}
|
|
20
|
-
export declare function DocumentTier({ tier, docs, helpOverrides, onUpload, onClear, }: DocumentTierProps): React.JSX.Element | null;
|
|
21
|
+
export declare function DocumentTier({ tier, docs, helpOverrides, onUpload, onClear, onRemove, }: DocumentTierProps): React.JSX.Element | null;
|
|
21
22
|
export {};
|
|
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.DocumentTier = DocumentTier;
|
|
7
7
|
const react_1 = __importDefault(require("react"));
|
|
8
8
|
const DocumentCard_1 = require("./DocumentCard");
|
|
9
|
-
function DocumentTier({ tier, docs, helpOverrides, onUpload, onClear, }) {
|
|
9
|
+
function DocumentTier({ tier, docs, helpOverrides, onUpload, onClear, onRemove, }) {
|
|
10
10
|
const tierDocs = tier.ids
|
|
11
11
|
.map((id) => {
|
|
12
12
|
const idx = docs.findIndex((d) => d.id === id);
|
|
@@ -19,5 +19,5 @@ function DocumentTier({ tier, docs, helpOverrides, onUpload, onClear, }) {
|
|
|
19
19
|
react_1.default.createElement("div", { className: "flex items-center gap-2 mb-2" },
|
|
20
20
|
react_1.default.createElement("span", { className: "text-[10px] font-bold uppercase tracking-widest font-tax-axis-mono", style: { color: tier.labelColor } }, tier.label),
|
|
21
21
|
react_1.default.createElement("span", { className: "text-[10px] text-tax-axis-text-4 font-tax-axis-body" }, tier.sublabel)),
|
|
22
|
-
react_1.default.createElement("div", { className: "grid gap-1.5" }, tierDocs.map(({ doc, idx }) => (react_1.default.createElement(DocumentCard_1.DocumentCard, { key: doc.id, doc: doc, tierBorderColor: tier.borderColor, tierBadgeColor: tier.badgeColor, tierBadgeText: tier.badgeText, helpOverride: helpOverrides[doc.id], onUpload: (file) => onUpload(idx, file), onClear: () => onClear(idx) }))))));
|
|
22
|
+
react_1.default.createElement("div", { className: "grid gap-1.5" }, tierDocs.map(({ doc, idx }) => (react_1.default.createElement(DocumentCard_1.DocumentCard, { key: doc.id, doc: doc, tierBorderColor: tier.borderColor, tierBadgeColor: tier.badgeColor, tierBadgeText: tier.badgeText, helpOverride: helpOverrides[doc.id], onUpload: (file) => onUpload(idx, file), onClear: () => onClear(idx), onRemove: onRemove ? () => onRemove(idx) : undefined }))))));
|
|
23
23
|
}
|
|
@@ -5,6 +5,20 @@ export interface TaxAxisDocumentsProps extends TaxAxisScreenProps {
|
|
|
5
5
|
profile: ClientProfile;
|
|
6
6
|
onContinue: () => void;
|
|
7
7
|
onBack: () => void;
|
|
8
|
-
onUploadDocument?: (doc: DocState, file: File) => Promise<void
|
|
8
|
+
onUploadDocument?: (doc: DocState, file: File) => Promise<void | {
|
|
9
|
+
documentId?: string;
|
|
10
|
+
fileName?: string;
|
|
11
|
+
status?: string;
|
|
12
|
+
documentType?: string;
|
|
13
|
+
parseError?: string | null;
|
|
14
|
+
}>;
|
|
15
|
+
onDeleteDocument?: (documentId: string) => Promise<void>;
|
|
16
|
+
fetchUploadedDocuments?: () => Promise<Array<{
|
|
17
|
+
fileName: string;
|
|
18
|
+
status: string;
|
|
19
|
+
documentType?: string | null;
|
|
20
|
+
parseError?: string | null;
|
|
21
|
+
updatedAt?: string | null;
|
|
22
|
+
}>>;
|
|
9
23
|
}
|
|
10
|
-
export declare function TaxAxisDocuments({ profile, onContinue, onBack, onUploadDocument, userContext: _userContext, }: TaxAxisDocumentsProps): React.JSX.Element;
|
|
24
|
+
export declare function TaxAxisDocuments({ profile, onContinue, onBack, onUploadDocument, onDeleteDocument, fetchUploadedDocuments, userContext: _userContext, }: TaxAxisDocumentsProps): React.JSX.Element;
|
|
@@ -37,7 +37,6 @@ const react_1 = __importStar(require("react"));
|
|
|
37
37
|
const documents_1 = require("../../lib/data/documents");
|
|
38
38
|
const TaxAxisButton_1 = require("../shared/TaxAxisButton");
|
|
39
39
|
const DocumentTier_1 = require("./DocumentTier");
|
|
40
|
-
// Stub filenames assigned when the user clicks "Upload" (mock App.jsx:1237)
|
|
41
40
|
const STUB_FILENAMES = {
|
|
42
41
|
"1120s": "2025_1120S.pdf",
|
|
43
42
|
"state-return": "2025_State_Return.pdf",
|
|
@@ -48,8 +47,6 @@ const STUB_FILENAMES = {
|
|
|
48
47
|
"fixed-assets": "Fixed_Assets.xlsx",
|
|
49
48
|
"prior-returns": "Prior_Returns.pdf",
|
|
50
49
|
};
|
|
51
|
-
// Short help text shown below each doc name when not yet uploaded
|
|
52
|
-
// (mock App.jsx:1384). Overrides the longer DOC_SPECS_BASE.help.
|
|
53
50
|
const HELP_OVERRIDES = {
|
|
54
51
|
"1120s": "Improves accuracy of every strategy",
|
|
55
52
|
"payroll": "Critical for Entity Structure and QBI calculations",
|
|
@@ -58,7 +55,6 @@ const HELP_OVERRIDES = {
|
|
|
58
55
|
"cashflow": "Helps with income deferral and timing strategies",
|
|
59
56
|
"prior-returns": "Improves confidence intervals (more years = tighter estimates)",
|
|
60
57
|
};
|
|
61
|
-
// Tier groupings from mock (App.jsx:1379-1383)
|
|
62
58
|
const TIER_DEFS = [
|
|
63
59
|
{
|
|
64
60
|
key: "required",
|
|
@@ -91,33 +87,128 @@ const TIER_DEFS = [
|
|
|
91
87
|
ids: ["state-return", "cashflow", "prior-returns"],
|
|
92
88
|
},
|
|
93
89
|
];
|
|
94
|
-
function TaxAxisDocuments({ profile, onContinue, onBack, onUploadDocument, userContext: _userContext = "expert", }) {
|
|
90
|
+
function TaxAxisDocuments({ profile, onContinue, onBack, onUploadDocument, onDeleteDocument, fetchUploadedDocuments, userContext: _userContext = "expert", }) {
|
|
91
|
+
const normalizeDocumentType = (value) => String(value || "").toUpperCase().replace(/[^A-Z0-9]+/g, "_");
|
|
95
92
|
const docSpecs = (0, react_1.useMemo)(() => (0, documents_1.getDocSpecs)(profile), [profile]);
|
|
96
|
-
const [docs, setDocs] = (0, react_1.useState)(() => docSpecs.map((s) => (Object.assign(Object.assign({}, s), { status: "empty", fileName: null }))));
|
|
93
|
+
const [docs, setDocs] = (0, react_1.useState)(() => docSpecs.map((s) => (Object.assign(Object.assign({}, s), { status: "empty", fileName: null, parseError: null }))));
|
|
94
|
+
const [uploadedDocIds, setUploadedDocIds] = (0, react_1.useState)({});
|
|
95
|
+
const [continueError, setContinueError] = (0, react_1.useState)(null);
|
|
96
|
+
const pollForParseStatus = (idx, fileName, expectedDocumentType) => __awaiter(this, void 0, void 0, function* () {
|
|
97
|
+
if (!fetchUploadedDocuments)
|
|
98
|
+
return;
|
|
99
|
+
const maxAttempts = 80;
|
|
100
|
+
const pollIntervalMs = 1500;
|
|
101
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
102
|
+
try {
|
|
103
|
+
const uploaded = yield fetchUploadedDocuments();
|
|
104
|
+
const exactTypeMatch = uploaded.filter((doc) => doc.fileName === fileName &&
|
|
105
|
+
normalizeDocumentType(String(doc.documentType || "")) ===
|
|
106
|
+
normalizeDocumentType(expectedDocumentType));
|
|
107
|
+
const filenameMatches = uploaded.filter((doc) => doc.fileName === fileName);
|
|
108
|
+
const candidates = exactTypeMatch.length > 0 ? exactTypeMatch : filenameMatches;
|
|
109
|
+
const match = candidates
|
|
110
|
+
.slice()
|
|
111
|
+
.sort((a, b) => new Date(String(b.updatedAt || "")).getTime() -
|
|
112
|
+
new Date(String(a.updatedAt || "")).getTime())[0];
|
|
113
|
+
if (match) {
|
|
114
|
+
const status = String(match.status || "").toUpperCase();
|
|
115
|
+
if (status === "FAILED") {
|
|
116
|
+
setDocs((prev) => prev.map((d, i) => i === idx
|
|
117
|
+
? Object.assign(Object.assign({}, d), { status: "failed", fileName, parseError: match.parseError || "Parsing failed. Try a different file." }) : d));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (status === "PARSED") {
|
|
121
|
+
setDocs((prev) => prev.map((d, i) => i === idx
|
|
122
|
+
? Object.assign(Object.assign({}, d), { status: "valid", fileName, parseError: null }) : d));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (status === "PARSING" || status === "UPLOADED") {
|
|
126
|
+
setDocs((prev) => prev.map((d, i) => i === idx
|
|
127
|
+
? Object.assign(Object.assign({}, d), { status: "parsing", fileName }) : d));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (pollError) {
|
|
132
|
+
// keep polling
|
|
133
|
+
}
|
|
134
|
+
yield new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
135
|
+
}
|
|
136
|
+
setDocs((prev) => prev.map((d, i) => i === idx ? Object.assign(Object.assign({}, d), { status: "failed", fileName, parseError: "Parsing timed out. Please try again." }) : d));
|
|
137
|
+
});
|
|
97
138
|
const handleUpload = (idx, file) => __awaiter(this, void 0, void 0, function* () {
|
|
98
139
|
const selectedDoc = docs[idx];
|
|
99
140
|
if (!selectedDoc)
|
|
100
141
|
return;
|
|
142
|
+
setContinueError(null);
|
|
101
143
|
setDocs((prev) => prev.map((d, i) => i === idx
|
|
102
|
-
? Object.assign(Object.assign({}, d), { status: "validating", fileName: file.name || STUB_FILENAMES[d.id] || "document.pdf" }) : d));
|
|
144
|
+
? Object.assign(Object.assign({}, d), { status: "validating", fileName: file.name || STUB_FILENAMES[d.id] || "document.pdf", parseError: null }) : d));
|
|
103
145
|
try {
|
|
104
|
-
|
|
105
|
-
yield onUploadDocument(selectedDoc, file)
|
|
146
|
+
const uploadResult = onUploadDocument
|
|
147
|
+
? yield onUploadDocument(selectedDoc, file)
|
|
148
|
+
: undefined;
|
|
149
|
+
const nextFileName = file.name || STUB_FILENAMES[selectedDoc.id] || "document.pdf";
|
|
150
|
+
const expectedDocumentType = normalizeDocumentType(selectedDoc.id);
|
|
151
|
+
const immediateStatus = String((uploadResult === null || uploadResult === void 0 ? void 0 : uploadResult.status) || "").toUpperCase();
|
|
152
|
+
if (uploadResult === null || uploadResult === void 0 ? void 0 : uploadResult.documentId) {
|
|
153
|
+
setUploadedDocIds((prev) => (Object.assign(Object.assign({}, prev), { [idx]: uploadResult.documentId })));
|
|
154
|
+
}
|
|
155
|
+
if (immediateStatus === "PARSED") {
|
|
156
|
+
setDocs((prev) => prev.map((d, i) => i === idx
|
|
157
|
+
? Object.assign(Object.assign({}, d), { status: "valid", fileName: (uploadResult === null || uploadResult === void 0 ? void 0 : uploadResult.fileName) || nextFileName, parseError: null }) : d));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (immediateStatus === "FAILED") {
|
|
161
|
+
setDocs((prev) => prev.map((d, i) => i === idx
|
|
162
|
+
? Object.assign(Object.assign({}, d), { status: "failed", fileName: (uploadResult === null || uploadResult === void 0 ? void 0 : uploadResult.fileName) || nextFileName, parseError: (uploadResult === null || uploadResult === void 0 ? void 0 : uploadResult.parseError) || "Parsing failed. Please upload a correct document." }) : d));
|
|
163
|
+
return;
|
|
106
164
|
}
|
|
107
165
|
setDocs((prev) => prev.map((d, i) => i === idx
|
|
108
|
-
? Object.assign(Object.assign({}, d), { status: "
|
|
166
|
+
? Object.assign(Object.assign({}, d), { status: "parsing", fileName: nextFileName }) : d));
|
|
167
|
+
yield pollForParseStatus(idx, nextFileName, expectedDocumentType);
|
|
109
168
|
}
|
|
110
169
|
catch (uploadError) {
|
|
170
|
+
const errorMessage = uploadError instanceof Error
|
|
171
|
+
? uploadError.message
|
|
172
|
+
: "Upload failed. Please try again.";
|
|
111
173
|
setDocs((prev) => prev.map((d, i) => i === idx
|
|
112
|
-
? Object.assign(Object.assign({}, d), { status: "
|
|
174
|
+
? Object.assign(Object.assign({}, d), { status: "failed", parseError: errorMessage }) : d));
|
|
113
175
|
}
|
|
114
176
|
});
|
|
115
177
|
const handleClear = (idx) => {
|
|
116
178
|
setDocs((prev) => prev.map((d, i) => i === idx
|
|
117
|
-
? Object.assign(Object.assign({}, d), { status: "empty", fileName: null }) : d));
|
|
179
|
+
? Object.assign(Object.assign({}, d), { status: "empty", fileName: null, parseError: null }) : d));
|
|
180
|
+
setContinueError(null);
|
|
181
|
+
};
|
|
182
|
+
const handleRemove = (idx) => __awaiter(this, void 0, void 0, function* () {
|
|
183
|
+
const docId = uploadedDocIds[idx];
|
|
184
|
+
if (docId && onDeleteDocument) {
|
|
185
|
+
try {
|
|
186
|
+
yield onDeleteDocument(docId);
|
|
187
|
+
}
|
|
188
|
+
catch (deleteError) {
|
|
189
|
+
// still clear from UI even if backend delete fails
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
setUploadedDocIds((prev) => {
|
|
193
|
+
const next = Object.assign({}, prev);
|
|
194
|
+
delete next[idx];
|
|
195
|
+
return next;
|
|
196
|
+
});
|
|
197
|
+
handleClear(idx);
|
|
198
|
+
});
|
|
199
|
+
const handleContinue = () => {
|
|
200
|
+
const failedDocs = docs.filter((d) => d.status === "failed");
|
|
201
|
+
if (failedDocs.length > 0) {
|
|
202
|
+
const names = failedDocs.map((d) => d.name).join(", ");
|
|
203
|
+
setContinueError(`Cannot continue — ${failedDocs.length === 1 ? "this document has" : "these documents have"} errors: ${names}. Please re-upload correct files or remove them.`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
setContinueError(null);
|
|
207
|
+
onContinue();
|
|
118
208
|
};
|
|
119
209
|
const validCount = docs.filter((d) => d.status === "valid").length;
|
|
120
|
-
|
|
210
|
+
const failedCount = docs.filter((d) => d.status === "failed").length;
|
|
211
|
+
const busyCount = docs.filter((d) => d.status === "validating" || d.status === "parsing").length;
|
|
121
212
|
const coveragePct = validCount === 0
|
|
122
213
|
? 32
|
|
123
214
|
: validCount === 1
|
|
@@ -166,8 +257,17 @@ function TaxAxisDocuments({ profile, onContinue, onBack, onUploadDocument, userC
|
|
|
166
257
|
coveragePct,
|
|
167
258
|
"% coverage"),
|
|
168
259
|
validCount < docs.length && (react_1.default.createElement("span", { className: "text-[10px] text-tax-axis-text-4 font-tax-axis-body" }, "Upload more documents to increase strategy coverage")))),
|
|
169
|
-
|
|
260
|
+
continueError && (react_1.default.createElement("div", { className: "py-3 px-4 mb-4 text-xs leading-relaxed font-tax-axis-body rounded-lg", style: {
|
|
261
|
+
background: "rgba(197,48,48,0.08)",
|
|
262
|
+
border: "1px solid rgba(197,48,48,0.3)",
|
|
263
|
+
color: "#FEB2B2",
|
|
264
|
+
} }, continueError)),
|
|
265
|
+
TIER_DEFS.map((tier) => (react_1.default.createElement(DocumentTier_1.DocumentTier, { key: tier.key, tier: tier, docs: docs, helpOverrides: HELP_OVERRIDES, onUpload: handleUpload, onClear: handleClear, onRemove: handleRemove }))),
|
|
170
266
|
react_1.default.createElement("div", { className: "flex gap-3 mt-6" },
|
|
171
267
|
react_1.default.createElement(TaxAxisButton_1.TaxAxisButton, { variant: "secondary", onClick: onBack }, "Back"),
|
|
172
|
-
react_1.default.createElement(TaxAxisButton_1.TaxAxisButton, { onClick:
|
|
268
|
+
react_1.default.createElement(TaxAxisButton_1.TaxAxisButton, { onClick: handleContinue, className: "flex-1", disabled: busyCount > 0 }, busyCount > 0
|
|
269
|
+
? "Processing..."
|
|
270
|
+
: failedCount > 0
|
|
271
|
+
? `Continue (${failedCount} failed)`
|
|
272
|
+
: "Continue"))));
|
|
173
273
|
}
|
|
@@ -8,14 +8,14 @@ exports.DOC_SPECS_BASE = void 0;
|
|
|
8
8
|
exports.getDocSpecs = getDocSpecs;
|
|
9
9
|
const states_1 = require("./states");
|
|
10
10
|
exports.DOC_SPECS_BASE = [
|
|
11
|
-
{ id: "1120s", name: "Federal Tax Return (1120S/1065/1040)", accept: [".pdf"], strategies: ["QBI \u00A7199A", "S-Corp Structure", "\u00A7179 Expensing", "Bonus Depreciation", "Business Interest \u00A7163(j)"], required: "optional", help: "Optional \u2014 startups or new entities may not have prior returns. Improves accuracy if available." },
|
|
12
|
-
{ id: "state-return", name: "State Return(s)", accept: [".pdf"], strategies: ["SALT / PTE Optimization", "State Nexus Analysis"], required: "conditional", help: "Required if client operates in income-tax states. Not needed for WY, NV, TX, FL, etc." },
|
|
13
|
-
{ id: "payroll", name: "Payroll Records (W-3 / 941)", accept: [".pdf", ".xlsx", ".csv"], strategies: ["S-Corp Salary", "HSA Maximization", "WOTC", "Overtime Deduction"], required: "optional", help: "Optional for V1 \u2014 intake questions substitute for most payroll data." },
|
|
14
|
-
{ id: "pnl", name: "Profit & Loss Statement (Current Year)", accept: [".pdf", ".xlsx", ".csv"], strategies: ["QBI \u00A7199A", "R&D Credit", "Business Meals", "Professional Fees", "Home Office"], required: true, help: "Required \u2014 current year P&L is the minimum input for savings calculations." },
|
|
15
|
-
{ id: "balance", name: "Balance Sheet (Current Year)", accept: [".pdf", ".xlsx"], strategies: ["Business Interest \u00A7163(j)", "\u00A7179 Expensing", "Opportunity Zone"], required: true, help: "Required \u2014 needed for asset-based strategies and financial health assessment." },
|
|
16
|
-
{ id: "cashflow", name: "Cash Flow Statement", accept: [".pdf", ".xlsx"], strategies: ["Income Deferral", "Tax Method Elections", "Estimated Tax Timing"], required: "optional", help: "Recommended \u2014 helps with income deferral and timing strategies." },
|
|
17
|
-
{ id: "fixed-assets", name: "Fixed Asset Schedule", accept: [".pdf", ".xlsx"], strategies: ["\u00A7179", "Bonus Depreciation \u00A7168(k)", "Cost Segregation"], required: "conditional", help: "Required for real estate clients (cost segregation). Optional otherwise." },
|
|
18
|
-
{ id: "prior-returns", name: "Prior Year Returns (2022\u20132024)", accept: [".pdf"], strategies: ["R&E Catch-Up \u00A7174A", "Charitable Bunching", "Confidence Intervals"], required: false, help: "More years = tighter estimates. 1 year: \u00B130% range. 2\u20133 years: \u00B115%. 4\u20135 years: \u00B15%." },
|
|
11
|
+
{ id: "1120s", name: "Federal Tax Return (1120S/1065/1040)", accept: [".pdf", ".docx", ".doc"], strategies: ["QBI \u00A7199A", "S-Corp Structure", "\u00A7179 Expensing", "Bonus Depreciation", "Business Interest \u00A7163(j)"], required: "optional", help: "Optional \u2014 startups or new entities may not have prior returns. Improves accuracy if available." },
|
|
12
|
+
{ id: "state-return", name: "State Return(s)", accept: [".pdf", ".docx", ".doc"], strategies: ["SALT / PTE Optimization", "State Nexus Analysis"], required: "conditional", help: "Required if client operates in income-tax states. Not needed for WY, NV, TX, FL, etc." },
|
|
13
|
+
{ id: "payroll", name: "Payroll Records (W-3 / 941)", accept: [".pdf", ".xlsx", ".csv", ".docx", ".doc"], strategies: ["S-Corp Salary", "HSA Maximization", "WOTC", "Overtime Deduction"], required: "optional", help: "Optional for V1 \u2014 intake questions substitute for most payroll data." },
|
|
14
|
+
{ id: "pnl", name: "Profit & Loss Statement (Current Year)", accept: [".pdf", ".xlsx", ".csv", ".docx", ".doc"], strategies: ["QBI \u00A7199A", "R&D Credit", "Business Meals", "Professional Fees", "Home Office"], required: true, help: "Required \u2014 current year P&L is the minimum input for savings calculations." },
|
|
15
|
+
{ id: "balance", name: "Balance Sheet (Current Year)", accept: [".pdf", ".xlsx", ".docx", ".doc"], strategies: ["Business Interest \u00A7163(j)", "\u00A7179 Expensing", "Opportunity Zone"], required: true, help: "Required \u2014 needed for asset-based strategies and financial health assessment." },
|
|
16
|
+
{ id: "cashflow", name: "Cash Flow Statement", accept: [".pdf", ".xlsx", ".docx", ".doc"], strategies: ["Income Deferral", "Tax Method Elections", "Estimated Tax Timing"], required: "optional", help: "Recommended \u2014 helps with income deferral and timing strategies." },
|
|
17
|
+
{ id: "fixed-assets", name: "Fixed Asset Schedule", accept: [".pdf", ".xlsx", ".docx", ".doc"], strategies: ["\u00A7179", "Bonus Depreciation \u00A7168(k)", "Cost Segregation"], required: "conditional", help: "Required for real estate clients (cost segregation). Optional otherwise." },
|
|
18
|
+
{ id: "prior-returns", name: "Prior Year Returns (2022\u20132024)", accept: [".pdf", ".docx", ".doc"], strategies: ["R&E Catch-Up \u00A7174A", "Charitable Bunching", "Confidence Intervals"], required: false, help: "More years = tighter estimates. 1 year: \u00B130% range. 2\u20133 years: \u00B115%. 4\u20135 years: \u00B15%." },
|
|
19
19
|
];
|
|
20
20
|
function getDocSpecs(profile) {
|
|
21
21
|
const states = (profile === null || profile === void 0 ? void 0 : profile.states) || [];
|