@accounter/client 0.0.7-alpha-20250824105500-3626afe3dcdebcf10a7c77e76f0f4f5e4055867d → 0.0.7-alpha-20250824105539-11d89409a8942fb030a7abff740450f8a906266d

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.
@@ -0,0 +1,266 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import { AlertCircle, ChevronDown, ChevronRight, Eye } from 'lucide-react';
3
+ import { useQuery } from 'urql';
4
+ import {
5
+ AccountantApprovalStatusDocument,
6
+ AccountantStatus,
7
+ ChargeSortByField,
8
+ } from '../../../../../gql/graphql.js';
9
+ import type { TimelessDateString } from '../../../../../helpers/dates.js';
10
+ import { Badge } from '../../../../ui/badge.jsx';
11
+ import { Button } from '../../../../ui/button.jsx';
12
+ import { CardContent } from '../../../../ui/card.jsx';
13
+ import { Collapsible, CollapsibleContent } from '../../../../ui/collapsible.js';
14
+ import { getAllChargesHref } from '../../../charges/all-charges.jsx';
15
+ import {
16
+ BaseStepCard,
17
+ type BaseStepProps,
18
+ type StepAction,
19
+ type StepStatus,
20
+ } from '../step-base.jsx';
21
+
22
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions -- used by codegen
23
+ /* GraphQL */ `
24
+ query AccountantApprovalStatus($fromDate: TimelessDate!, $toDate: TimelessDate!) {
25
+ accountantApprovalStatus(from: $fromDate, to: $toDate) {
26
+ totalCharges
27
+ approvedCount
28
+ pendingCount
29
+ unapprovedCount
30
+ }
31
+ }
32
+ `;
33
+
34
+ interface ChargeValidationData {
35
+ approvedPercentage: number;
36
+ pendingPercentage: number;
37
+ unapprovedPercentage: number;
38
+ totalCharges: number;
39
+ approvedCount: number;
40
+ pendingCount: number;
41
+ unapprovedCount: number;
42
+ }
43
+
44
+ interface Step01Props extends BaseStepProps {
45
+ year: number;
46
+ adminBusinessId?: string;
47
+ }
48
+
49
+ export function Step01ValidateCharges(props: Step01Props) {
50
+ const [status, setStatus] = useState<StepStatus>('loading');
51
+ const [chargeData, setChargeData] = useState<ChargeValidationData>({
52
+ approvedPercentage: 0,
53
+ pendingPercentage: 0,
54
+ unapprovedPercentage: 100,
55
+ totalCharges: 1,
56
+ approvedCount: 0,
57
+ pendingCount: 0,
58
+ unapprovedCount: 1,
59
+ });
60
+ const [isDetailsExpanded, setIsDetailsExpanded] = useState(false);
61
+ const [hasReportedCompletion, setHasReportedCompletion] = useState(false);
62
+
63
+ // Report status changes to parent
64
+ useEffect(() => {
65
+ if (props.onStatusChange) {
66
+ props.onStatusChange(props.id, status);
67
+ }
68
+
69
+ // Track if we've reported completion to avoid double counting
70
+ if (status === 'completed' && !hasReportedCompletion) {
71
+ setHasReportedCompletion(true);
72
+ } else if (status !== 'completed' && hasReportedCompletion) {
73
+ setHasReportedCompletion(false);
74
+ }
75
+ }, [status, props.onStatusChange, props.id, hasReportedCompletion]);
76
+
77
+ const [{ data, fetching }, fetchStatus] = useQuery({
78
+ query: AccountantApprovalStatusDocument,
79
+ variables: {
80
+ fromDate: `${props.year}-01-01` as TimelessDateString,
81
+ toDate: `${props.year}-12-31` as TimelessDateString,
82
+ },
83
+ });
84
+
85
+ useEffect(() => {
86
+ if (!props.adminBusinessId) {
87
+ setStatus('blocked');
88
+ } else if (fetching) {
89
+ setStatus('loading');
90
+ }
91
+ }, [props.adminBusinessId, fetching]);
92
+
93
+ useEffect(() => {
94
+ if (data?.accountantApprovalStatus) {
95
+ const { totalCharges, approvedCount, pendingCount, unapprovedCount } =
96
+ data.accountantApprovalStatus;
97
+ const accountantApprovalStatus: ChargeValidationData = {
98
+ approvedPercentage: (approvedCount / totalCharges) * 100 || 0,
99
+ pendingPercentage: (pendingCount / totalCharges) * 100 || 0,
100
+ unapprovedPercentage: (unapprovedCount / totalCharges) * 100 || 0,
101
+ totalCharges,
102
+ approvedCount,
103
+ pendingCount,
104
+ unapprovedCount,
105
+ };
106
+
107
+ setChargeData(accountantApprovalStatus);
108
+
109
+ // Determine status based on data
110
+ if (
111
+ accountantApprovalStatus.pendingPercentage === 0 &&
112
+ accountantApprovalStatus.unapprovedPercentage === 0
113
+ ) {
114
+ setStatus('completed');
115
+ } else if (
116
+ accountantApprovalStatus.pendingPercentage + accountantApprovalStatus.unapprovedPercentage <
117
+ 30
118
+ ) {
119
+ setStatus('in-progress');
120
+ } else {
121
+ setStatus('pending');
122
+ }
123
+ }
124
+ }, [data]);
125
+
126
+ const href = useMemo(() => {
127
+ return getAllChargesHref({
128
+ byOwners: props.adminBusinessId ? [props.adminBusinessId] : undefined,
129
+ fromAnyDate: `${props.year}-01-01` as TimelessDateString,
130
+ toAnyDate: `${props.year}-12-31` as TimelessDateString,
131
+ accountantStatus: [AccountantStatus.Pending, AccountantStatus.Unapproved],
132
+ sortBy: {
133
+ field: ChargeSortByField.Date,
134
+ asc: false,
135
+ },
136
+ });
137
+ }, [props.adminBusinessId, props.year]);
138
+
139
+ const actions: StepAction[] = [{ label: 'Review Charges', href }];
140
+
141
+ const refreshData = async () => {
142
+ await fetchStatus();
143
+ };
144
+
145
+ return (
146
+ <BaseStepCard {...props} status={status} icon={<Eye className="h-4 w-4" />} actions={actions}>
147
+ <Collapsible open={isDetailsExpanded}>
148
+ <CardContent className="pt-0 border-t">
149
+ <Button
150
+ variant="ghost"
151
+ size="sm"
152
+ onClick={() => setIsDetailsExpanded(!isDetailsExpanded)}
153
+ className="w-full justify-between p-2 h-auto"
154
+ >
155
+ <div className="flex items-center gap-2">
156
+ <span className="text-sm font-medium">Charge Validation Details</span>
157
+ <Badge variant="outline" className="text-xs">
158
+ {chargeData.pendingCount + chargeData.unapprovedCount} need attention
159
+ </Badge>
160
+ </div>
161
+ {isDetailsExpanded ? (
162
+ <ChevronDown className="h-4 w-4" />
163
+ ) : (
164
+ <ChevronRight className="h-4 w-4" />
165
+ )}
166
+ </Button>
167
+ </CardContent>
168
+ <CollapsibleContent>
169
+ <CardContent className="pt-0">
170
+ <div className="space-y-4">
171
+ <div className="space-y-3">
172
+ <div className="flex justify-between items-center text-sm">
173
+ <span className="font-medium">Charge Review Progress</span>
174
+ <div className="flex items-center gap-2">
175
+ <span className="text-muted-foreground">
176
+ {chargeData.totalCharges.toLocaleString()} total charges
177
+ </span>
178
+ <Button
179
+ variant="ghost"
180
+ size="sm"
181
+ onClick={refreshData}
182
+ disabled={status === 'loading'}
183
+ >
184
+ Refresh
185
+ </Button>
186
+ </div>
187
+ </div>
188
+
189
+ {/* Combined Progress Bar */}
190
+ <div className="relative h-6 bg-gray-200 rounded-full overflow-hidden">
191
+ <div
192
+ className="absolute left-0 top-0 h-full bg-green-500 transition-all duration-300"
193
+ style={{ width: `${chargeData.approvedPercentage}%` }}
194
+ />
195
+ <div
196
+ className="absolute top-0 h-full bg-orange-500 transition-all duration-300"
197
+ style={{
198
+ left: `${chargeData.approvedPercentage}%`,
199
+ width: `${chargeData.pendingPercentage}%`,
200
+ }}
201
+ />
202
+ <div
203
+ className="absolute top-0 h-full bg-red-500 transition-all duration-300"
204
+ style={{
205
+ left: `${chargeData.approvedPercentage + chargeData.pendingPercentage}%`,
206
+ width: `${chargeData.unapprovedPercentage}%`,
207
+ }}
208
+ />
209
+ </div>
210
+
211
+ {/* Legend */}
212
+ <div className="grid grid-cols-3 gap-4 text-sm">
213
+ <div className="flex items-center gap-2">
214
+ <div className="w-3 h-3 bg-green-500 rounded-full" />
215
+ <div>
216
+ <div className="font-medium text-green-700">
217
+ Approved ({chargeData.approvedPercentage}%)
218
+ </div>
219
+ <div className="text-xs text-muted-foreground">
220
+ {chargeData.approvedCount.toLocaleString()} charges
221
+ </div>
222
+ </div>
223
+ </div>
224
+
225
+ <div className="flex items-center gap-2">
226
+ <div className="w-3 h-3 bg-orange-500 rounded-full" />
227
+ <div>
228
+ <div className="font-medium text-orange-700">
229
+ Pending ({chargeData.pendingPercentage}%)
230
+ </div>
231
+ <div className="text-xs text-muted-foreground">
232
+ {chargeData.pendingCount.toLocaleString()} charges
233
+ </div>
234
+ </div>
235
+ </div>
236
+
237
+ <div className="flex items-center gap-2">
238
+ <div className="w-3 h-3 bg-red-500 rounded-full" />
239
+ <div>
240
+ <div className="font-medium text-red-700">
241
+ Unapproved ({chargeData.unapprovedPercentage}%)
242
+ </div>
243
+ <div className="text-xs text-muted-foreground">
244
+ {chargeData.unapprovedCount.toLocaleString()} charges
245
+ </div>
246
+ </div>
247
+ </div>
248
+ </div>
249
+ </div>
250
+
251
+ {(chargeData.pendingPercentage > 0 || chargeData.unapprovedPercentage > 0) && (
252
+ <div className="flex items-center gap-2 p-3 bg-orange-50 rounded-lg border border-orange-200">
253
+ <AlertCircle className="h-4 w-4 text-orange-600" />
254
+ <span className="text-sm text-orange-800">
255
+ {chargeData.pendingCount + chargeData.unapprovedCount} charges need review
256
+ before proceeding
257
+ </span>
258
+ </div>
259
+ )}
260
+ </div>
261
+ </CardContent>
262
+ </CollapsibleContent>
263
+ </Collapsible>
264
+ </BaseStepCard>
265
+ );
266
+ }
@@ -0,0 +1,132 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import { AlertTriangle, Settings } from 'lucide-react';
3
+ import { useQuery } from 'urql';
4
+ import { ChargeSortByField, LedgerValidationStatusDocument } from '../../../../../gql/graphql.js';
5
+ import type { TimelessDateString } from '../../../../../helpers/index.js';
6
+ import { getLedgerValidationHref } from '../../../../charges-ledger-validation.js';
7
+ import { Badge } from '../../../../ui/badge.jsx';
8
+ import { CardContent } from '../../../../ui/card.jsx';
9
+ import {
10
+ BaseStepCard,
11
+ type BaseStepProps,
12
+ type StepAction,
13
+ type StepStatus,
14
+ } from '../step-base.jsx';
15
+
16
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions -- used by codegen
17
+ /* GraphQL */ `
18
+ query LedgerValidationStatus($limit: Int, $filters: ChargeFilter) {
19
+ chargesWithLedgerChanges(limit: $limit, filters: $filters) {
20
+ charge {
21
+ id
22
+ }
23
+ }
24
+ }
25
+ `;
26
+
27
+ interface Step02Props extends BaseStepProps {
28
+ year: number;
29
+ adminBusinessId?: string;
30
+ }
31
+
32
+ export function Step02LedgerChanges(props: Step02Props) {
33
+ const [status, setStatus] = useState<StepStatus>('blocked');
34
+ const [pendingChanges, setPendingChanges] = useState<number>(Infinity);
35
+
36
+ const [{ data, fetching }, fetchStatus] = useQuery({
37
+ query: LedgerValidationStatusDocument,
38
+ variables: {
39
+ filters: {
40
+ byOwners: props.adminBusinessId ? [props.adminBusinessId] : [],
41
+ fromAnyDate: `${props.year}-01-01` as TimelessDateString,
42
+ toAnyDate: `${props.year}-12-31` as TimelessDateString,
43
+ },
44
+ },
45
+ pause: true,
46
+ });
47
+
48
+ useEffect(() => {
49
+ if (!data && !fetching && props.adminBusinessId) {
50
+ fetchStatus();
51
+ }
52
+ });
53
+
54
+ useEffect(() => {
55
+ if (fetching) setStatus('loading');
56
+ }, [fetching]);
57
+
58
+ // Report status changes to parent
59
+ useEffect(() => {
60
+ if (props.onStatusChange) {
61
+ props.onStatusChange(props.id, status);
62
+ }
63
+ }, [status, props.onStatusChange, props.id]);
64
+
65
+ useEffect(() => {
66
+ if (data?.chargesWithLedgerChanges) {
67
+ const pendingChanges = data.chargesWithLedgerChanges.filter(
68
+ charge => !!charge.charge?.id,
69
+ ).length;
70
+ setPendingChanges(pendingChanges);
71
+
72
+ if (pendingChanges === 0) {
73
+ setStatus('completed');
74
+ } else {
75
+ setStatus('in-progress');
76
+ }
77
+ }
78
+ }, [data]);
79
+
80
+ const href = useMemo(() => {
81
+ return getLedgerValidationHref({
82
+ byOwners: props.adminBusinessId ? [props.adminBusinessId] : undefined,
83
+ fromAnyDate: `${props.year}-01-01` as TimelessDateString,
84
+ toAnyDate: `${props.year}-12-31` as TimelessDateString,
85
+ sortBy: {
86
+ field: ChargeSortByField.Date,
87
+ asc: false,
88
+ },
89
+ });
90
+ }, [props.adminBusinessId, props.year]);
91
+
92
+ const actions: StepAction[] = [{ label: 'View Ledger Status', href }];
93
+
94
+ return (
95
+ <BaseStepCard
96
+ {...props}
97
+ status={status}
98
+ icon={<Settings className="h-4 w-4" />}
99
+ actions={actions}
100
+ >
101
+ {pendingChanges > 0 && (
102
+ <CardContent className="pt-0 border-t">
103
+ {fetching ? (
104
+ <div className="flex items-center gap-2 p-3 bg-gray-50 rounded-lg border border-gray-200 animate-pulse">
105
+ <Settings className="h-4 w-4 text-gray-400" />
106
+ <div className="flex-1">
107
+ <span className="text-sm text-gray-600 font-medium">
108
+ Checking for pending ledger changes...
109
+ </span>
110
+ </div>
111
+ </div>
112
+ ) : (
113
+ <div className="flex items-center gap-2 p-3 bg-red-50 rounded-lg border border-red-200">
114
+ <AlertTriangle className="h-4 w-4 text-red-600" />
115
+ <div className="flex-1">
116
+ <span className="text-sm text-red-800 font-medium">
117
+ {pendingChanges} pending ledger changes detected
118
+ </span>
119
+ {/* <div className="text-xs text-red-600 mt-1">
120
+ Last updated: {new Date(ledgerStatus.lastUpdate).toLocaleString()}
121
+ </div> */}
122
+ </div>
123
+ <Badge variant="destructive" className="text-xs">
124
+ Action Required
125
+ </Badge>
126
+ </div>
127
+ )}
128
+ </CardContent>
129
+ )}
130
+ </BaseStepCard>
131
+ );
132
+ }
@@ -0,0 +1,171 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { Calculator } from 'lucide-react';
5
+ import type { TimelessDateString } from '../../../../../helpers/dates.js';
6
+ import { BalanceChargeModal } from '../../../../common/modals/balance-charge-modal.jsx';
7
+ import { getContoReportHref } from '../../../../reports/conto/index.jsx';
8
+ import { getTrialBalanceReportHref } from '../../../../reports/trial-balance-report/index.jsx';
9
+ import { Collapsible, CollapsibleContent } from '../../../../ui/collapsible.js';
10
+ import { BaseStepCard, type BaseStepProps, type StepStatus } from '../step-base.jsx';
11
+
12
+ interface UserType {
13
+ type: 'new' | 'migrating' | 'continuing';
14
+ balanceStatus?: 'verified' | 'pending' | 'missing';
15
+ }
16
+
17
+ interface Step03Props extends BaseStepProps {
18
+ year: number;
19
+ adminBusinessId?: string;
20
+ }
21
+
22
+ // Sub-step components
23
+ function SubStep3A({
24
+ level,
25
+ year,
26
+ adminBusinessId,
27
+ disabled,
28
+ }: {
29
+ level: number;
30
+ year: number;
31
+ adminBusinessId: string;
32
+ disabled: boolean;
33
+ }) {
34
+ const contoHref = getContoReportHref({
35
+ fromDate: `${year - 1}-01-01` as TimelessDateString,
36
+ toDate: `${year - 1}-12-31` as TimelessDateString,
37
+ ownerIds: [adminBusinessId],
38
+ });
39
+ const [balanceChargeModalOpen, setBalanceChargeModalOpen] = useState(false);
40
+
41
+ return (
42
+ <>
43
+ <BaseStepCard
44
+ id="3a"
45
+ title="Migrating Users"
46
+ description="Create balance charge and dynamic report"
47
+ status="pending"
48
+ level={level}
49
+ actions={[
50
+ { label: 'Create Balance Charge', onClick: () => setBalanceChargeModalOpen(true) },
51
+ { label: 'Generate Dynamic Report', href: contoHref },
52
+ // { label: 'Upload Conto 331 Report', href: '/upload/conto331' },
53
+ ]}
54
+ disabled={disabled}
55
+ />
56
+ <BalanceChargeModal
57
+ open={balanceChargeModalOpen}
58
+ onOpenChange={setBalanceChargeModalOpen}
59
+ onClose={() => setBalanceChargeModalOpen(false)}
60
+ />
61
+ </>
62
+ );
63
+ }
64
+
65
+ function SubStep3B({
66
+ level,
67
+ year,
68
+ adminBusinessId,
69
+ disabled,
70
+ }: {
71
+ level: number;
72
+ year: number;
73
+ adminBusinessId: string;
74
+ disabled: boolean;
75
+ }) {
76
+ const href = getTrialBalanceReportHref({
77
+ toDate: `${year - 1}-12-31` as TimelessDateString,
78
+ ownerIds: [adminBusinessId],
79
+ });
80
+ return (
81
+ <BaseStepCard
82
+ id="3b"
83
+ title="Continuing Users"
84
+ description="Compare with previous year final trial balance"
85
+ status="pending"
86
+ level={level}
87
+ actions={[{ label: 'View Previous Year Ending Balance', href }]}
88
+ disabled={disabled}
89
+ />
90
+ );
91
+ }
92
+
93
+ export default function Step03OpeningBalance(props: Step03Props) {
94
+ const [status, setStatus] = useState<StepStatus>('loading');
95
+ const [userType, setUserType] = useState<UserType | null>(null);
96
+ const [isExpanded, setIsExpanded] = useState(false);
97
+
98
+ useEffect(() => {
99
+ if (!props.adminBusinessId) {
100
+ setStatus('blocked');
101
+ }
102
+ }, [props.adminBusinessId]);
103
+
104
+ // Report status changes to parent
105
+ useEffect(() => {
106
+ if (props.onStatusChange) {
107
+ props.onStatusChange(props.id, status);
108
+ }
109
+ }, [status, props.onStatusChange, props.id]);
110
+
111
+ useEffect(() => {
112
+ const fetchUserType = async () => {
113
+ try {
114
+ // Simulate API call to determine user type
115
+ await new Promise(resolve => setTimeout(resolve, 600));
116
+
117
+ const data: UserType = {
118
+ type: 'continuing',
119
+ balanceStatus: 'pending',
120
+ };
121
+
122
+ setUserType(data);
123
+
124
+ if (data.type === 'new') {
125
+ setStatus('completed');
126
+ } else if (data.balanceStatus === 'verified') {
127
+ setStatus('completed');
128
+ } else {
129
+ setStatus('pending');
130
+ }
131
+ } catch (error) {
132
+ console.error('Error fetching user type:', error);
133
+ setStatus('blocked');
134
+ }
135
+ };
136
+
137
+ fetchUserType();
138
+ }, []);
139
+
140
+ return (
141
+ <>
142
+ <BaseStepCard
143
+ {...props}
144
+ status={status}
145
+ icon={<Calculator className="h-4 w-4" />}
146
+ hasSubsteps
147
+ isExpanded={isExpanded}
148
+ onToggleExpanded={() => setIsExpanded(!isExpanded)}
149
+ />
150
+
151
+ {props.adminBusinessId && (
152
+ <Collapsible open={isExpanded}>
153
+ <CollapsibleContent className="space-y-2">
154
+ <SubStep3A
155
+ level={1}
156
+ year={props.year}
157
+ adminBusinessId={props.adminBusinessId}
158
+ disabled={userType?.type !== 'migrating'}
159
+ />
160
+ <SubStep3B
161
+ level={1}
162
+ year={props.year}
163
+ adminBusinessId={props.adminBusinessId}
164
+ disabled={userType?.type !== 'continuing'}
165
+ />
166
+ </CollapsibleContent>
167
+ </Collapsible>
168
+ )}
169
+ </>
170
+ );
171
+ }