@accounter/client 0.0.8-alpha-20251022034712-a30a030f5a83f0dff0ee9fa8ccd0ebdaf7986647 → 0.0.8-alpha-20251022131500-0d2446e83760934b05cf2c65a8b065989f7724c8

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.
Files changed (107) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/dist/assets/Checkbox-D7nOeER7.js +6 -0
  3. package/dist/assets/Progress-_KQ8BRrQ.js +1 -0
  4. package/dist/assets/Typography-BYHNiKxL.js +1 -0
  5. package/dist/assets/accordion-aunUrlum.js +1 -0
  6. package/dist/assets/accountant-approvals-1lH0z-Rv.js +1 -0
  7. package/dist/assets/all-charges-Cngljy2T.js +1 -0
  8. package/dist/assets/arrow-up-down-De1dsSxL.js +6 -0
  9. package/dist/assets/business-7l6LGFQt.js +37 -0
  10. package/dist/assets/business-transactions-single-BmOnrzw-.js +1 -0
  11. package/dist/assets/business-trip-BH8ZEeSB.js +1 -0
  12. package/dist/assets/charges-filters-DkzCOY0r.js +1 -0
  13. package/dist/assets/charges-ledger-validation-tNA_f_7H.js +1 -0
  14. package/dist/assets/chart-BJ85ZFS0.js +74 -0
  15. package/dist/assets/data-table-pagination-_iPJtRUP.js +11 -0
  16. package/dist/assets/editable-business-trip-DCtXJ6w4.js +16 -0
  17. package/dist/assets/graphql-document-dedupe-fragments-ByT8-wlV.js +1 -0
  18. package/dist/assets/index-6W3Ndi5M.js +1 -0
  19. package/dist/assets/index-A086__I6.js +1 -0
  20. package/dist/assets/index-AoVRWBXr.js +6 -0
  21. package/dist/assets/index-BGZNb2wc.js +17 -0
  22. package/dist/assets/index-BHUgtFG6.js +1 -0
  23. package/dist/assets/index-BQw927FW.js +11 -0
  24. package/dist/assets/index-C27oA70V.js +1 -0
  25. package/dist/assets/index-CIqGQ8uI.js +1 -0
  26. package/dist/assets/index-CMXNoVEJ.js +24 -0
  27. package/dist/assets/index-CQEXsBvi.js +1 -0
  28. package/dist/assets/index-CRWwjUSx.js +1 -0
  29. package/dist/assets/index-CUPkGo8z.js +1 -0
  30. package/dist/assets/index-Ch3veRcP.js +1 -0
  31. package/dist/assets/index-Cll1w4iD.js +6 -0
  32. package/dist/assets/index-Cnf3x_0g.js +2 -0
  33. package/dist/assets/index-DCiQggcN.js +9 -0
  34. package/dist/assets/index-DFO0fSvK.js +876 -0
  35. package/dist/assets/index-DLrRdx1l.js +1 -0
  36. package/dist/assets/index-DgOX69C5.js +1 -0
  37. package/dist/assets/index-DqPz6G2w.js +2 -0
  38. package/dist/assets/index-DxgUoyCT.js +1 -0
  39. package/dist/assets/index-Dxkz1HG4.js +137 -0
  40. package/dist/assets/index-LwYKcUCw.js +1 -0
  41. package/dist/assets/index-gdTXrWXt.css +1 -0
  42. package/dist/assets/{index.es-CWwhWGxX.js → index.es-g2vV-Mpx.js} +5 -5
  43. package/dist/assets/issue-document-DlwQP2Xx.js +1 -0
  44. package/dist/assets/login-page-C2voZ_Kj.js +1 -0
  45. package/dist/assets/missing-info-charges-eJYTfSDz.js +1 -0
  46. package/dist/assets/page-not-found-t6EEvRUF.js +1 -0
  47. package/dist/assets/pencil-nACAiGf5.js +6 -0
  48. package/dist/assets/report-commentary-row-BeCkGfXh.js +1 -0
  49. package/dist/assets/save-DIy7YBvX.js +11 -0
  50. package/dist/assets/sequential-CAnleQny.js +1 -0
  51. package/dist/assets/similar-charges-by-business-modal-BTts1hyT.js +1 -0
  52. package/dist/assets/sub-CNmFodmJ.js +1 -0
  53. package/dist/assets/subMonths-BkHf0iFx.js +1 -0
  54. package/dist/index.html +2 -2
  55. package/package.json +1 -1
  56. package/src/components/business-transactions/business-extended-info.tsx +13 -15
  57. package/src/components/business-transactions/business-transactions-single.tsx +3 -3
  58. package/src/components/business-transactions/index.tsx +12 -1
  59. package/src/components/business-trips/business-trip.tsx +3 -3
  60. package/src/components/charges/cells/business-trip.tsx +6 -8
  61. package/src/components/charges/cells/counterparty.tsx +7 -5
  62. package/src/components/common/accounter-table.tsx +6 -5
  63. package/src/components/common/business-trip-report/parts/core-expense-row.tsx +11 -9
  64. package/src/components/common/business-trip-report/parts/uncategorized-transactions.tsx +11 -13
  65. package/src/components/common/buttons/index.ts +0 -2
  66. package/src/components/common/buttons/logout-button.tsx +7 -6
  67. package/src/components/common/documents-to-charge-matcher/selection-handler/index.tsx +4 -2
  68. package/src/components/common/documents-to-charge-matcher/selection-handler/wide-filtered-selection.tsx +5 -7
  69. package/src/components/common/forms/edit-document.tsx +23 -10
  70. package/src/components/common/new-documents-list.tsx +10 -8
  71. package/src/components/documents-table/cells/creditor.tsx +11 -4
  72. package/src/components/documents-table/cells/debtor.tsx +11 -4
  73. package/src/components/error-boundary.tsx +189 -0
  74. package/src/components/layout/breadcrumbs.tsx +77 -0
  75. package/src/components/layout/dashboard-layout.tsx +4 -0
  76. package/src/components/layout/document-title.tsx +31 -0
  77. package/src/components/layout/navigation-progress.tsx +52 -0
  78. package/src/components/layout/page-skeleton.tsx +49 -0
  79. package/src/components/layout/sidelinks.tsx +28 -27
  80. package/src/components/ledger-table/counterparty-cell.tsx +19 -13
  81. package/src/components/login-page.tsx +2 -1
  82. package/src/components/reports/corporate-tax-ruling-compliance-report/index.tsx +3 -3
  83. package/src/components/reports/profit-and-loss-report/index.tsx +3 -3
  84. package/src/components/reports/tax-report/index.tsx +3 -3
  85. package/src/components/screens/businesses/business.tsx +21 -9
  86. package/src/components/screens/charges/charge.tsx +22 -9
  87. package/src/components/transactions-table/cells/counterparty.tsx +9 -2
  88. package/src/components/transactions-table/cells-legacy/counterparty.tsx +9 -2
  89. package/src/index.tsx +4 -22
  90. package/src/providers/auth-guard.tsx +14 -23
  91. package/src/providers/index.tsx +7 -2
  92. package/src/providers/urql-client.ts +86 -0
  93. package/src/providers/urql.tsx +7 -12
  94. package/src/providers/user-provider.tsx +3 -2
  95. package/src/router/config.tsx +534 -0
  96. package/src/router/layouts/dashboard-layout.tsx +20 -0
  97. package/src/router/layouts/root-layout.tsx +69 -0
  98. package/src/router/loaders/auth-loader.ts +32 -0
  99. package/src/router/loaders/business-loader.ts +25 -0
  100. package/src/router/loaders/charge-loader.ts +25 -0
  101. package/src/router/loaders/index.ts +17 -0
  102. package/src/router/routes.ts +88 -0
  103. package/src/router/types.ts +62 -0
  104. package/dist/assets/index-B2UYAO1O.css +0 -1
  105. package/dist/assets/index-BexxGuN6.js +0 -1224
  106. package/src/components/common/buttons/button-with-label.tsx +0 -41
  107. package/src/components/common/buttons/button.tsx +0 -44
@@ -26,6 +26,7 @@ import {
26
26
  Scale,
27
27
  Tags,
28
28
  } from 'lucide-react';
29
+ import { ROUTES } from '../../router/routes.js';
29
30
 
30
31
  export interface NavLink {
31
32
  title: string;
@@ -49,19 +50,19 @@ export const sidelinks: SideLink[] = [
49
50
  {
50
51
  title: 'All Charges',
51
52
  label: '',
52
- href: '/charges',
53
+ href: ROUTES.CHARGES.ALL,
53
54
  icon: <Receipt size={18} />,
54
55
  },
55
56
  {
56
57
  title: 'Missing Info Charges',
57
58
  label: '',
58
- href: '/charges/missing-info',
59
+ href: ROUTES.CHARGES.MISSING_INFO,
59
60
  icon: <RectangleEllipsis size={18} />,
60
61
  },
61
62
  {
62
63
  title: 'Ledger Validation',
63
64
  label: '',
64
- href: '/charges/ledger-validation',
65
+ href: ROUTES.CHARGES.LEDGER_VALIDATION,
65
66
  icon: <BookOpenCheck size={18} />,
66
67
  },
67
68
  ],
@@ -75,73 +76,73 @@ export const sidelinks: SideLink[] = [
75
76
  {
76
77
  title: 'VAT Monthly Report',
77
78
  label: '',
78
- href: '/reports/vat-monthly',
79
+ href: ROUTES.REPORTS.VAT_MONTHLY,
79
80
  icon: <Receipt size={18} />,
80
81
  },
81
82
  {
82
83
  title: 'Trial Balance Report',
83
84
  label: '',
84
- href: '/reports/trial-balance',
85
+ href: ROUTES.REPORTS.TRIAL_BALANCE,
85
86
  icon: <Scale size={18} />,
86
87
  },
87
88
  {
88
89
  title: 'Conto Report',
89
90
  label: '',
90
- href: '/reports/conto',
91
+ href: ROUTES.REPORTS.CONTO,
91
92
  icon: <Puzzle size={18} />,
92
93
  },
93
94
  {
94
95
  title: 'Profit and Loss Report',
95
96
  label: '',
96
- href: '/reports/profit-and-loss',
97
+ href: ROUTES.REPORTS.PROFIT_AND_LOSS(),
97
98
  icon: <HandCoins size={18} />,
98
99
  },
99
100
  {
100
101
  title: 'Tax Report',
101
102
  label: '',
102
- href: '/reports/tax',
103
+ href: ROUTES.REPORTS.TAX(),
103
104
  icon: <ParkingMeter size={18} />,
104
105
  },
105
106
  {
106
107
  title: 'Corporate Tax Ruling Compliance Report',
107
108
  label: '',
108
- href: '/reports/corporate-tax-ruling-compliance',
109
+ href: ROUTES.REPORTS.CORPORATE_TAX_RULING_COMPLIANCE(),
109
110
  icon: <ParkingMeter size={18} />,
110
111
  },
111
112
  {
112
113
  title: 'Depreciation Report',
113
114
  label: '',
114
- href: '/reports/depreciation',
115
+ href: ROUTES.REPORTS.DEPRECIATION,
115
116
  icon: <ChartColumnDecreasing size={18} />,
116
117
  },
117
118
  {
118
119
  title: 'Shaam 6111 Report',
119
120
  label: '',
120
- href: '/reports/shaam6111',
121
+ href: ROUTES.REPORTS.SHAAM_6111,
121
122
  icon: <HandCoins size={18} />,
122
123
  },
123
124
  {
124
125
  title: 'Accountant Approvals',
125
126
  label: '',
126
- href: '/accountant-approvals',
127
+ href: ROUTES.ACCOUNTANT_APPROVALS,
127
128
  icon: <Calculator size={18} />,
128
129
  },
129
130
  {
130
131
  title: 'Yearly Ledger Report',
131
132
  label: '',
132
- href: '/reports/yearly-ledger',
133
+ href: ROUTES.REPORTS.YEARLY_LEDGER,
133
134
  icon: <Rows4 size={18} />,
134
135
  },
135
136
  {
136
137
  title: 'Transactions Balance',
137
138
  label: '',
138
- href: '/reports/balance',
139
+ href: ROUTES.REPORTS.BALANCE,
139
140
  icon: <ChartNoAxesCombined size={18} />,
140
141
  },
141
142
  {
142
143
  title: 'Validate Reports',
143
144
  label: '',
144
- href: '/reports/validate-reports',
145
+ href: ROUTES.REPORTS.VALIDATE_REPORTS,
145
146
  icon: <CheckCheck size={18} />,
146
147
  },
147
148
  {
@@ -160,25 +161,25 @@ export const sidelinks: SideLink[] = [
160
161
  {
161
162
  title: 'Businesses',
162
163
  label: '',
163
- href: '/businesses',
164
+ href: ROUTES.BUSINESSES.ALL,
164
165
  icon: <Factory size={18} />,
165
166
  },
166
167
  {
167
168
  title: 'Business Transactions',
168
169
  label: '',
169
- href: '/businesses/transactions',
170
+ href: ROUTES.BUSINESSES.TRANSACTIONS,
170
171
  icon: <ArrowLeftRight size={18} />,
171
172
  },
172
173
  {
173
174
  title: 'Tax Categories',
174
175
  label: '',
175
- href: '/tax-categories',
176
+ href: ROUTES.TAX_CATEGORIES,
176
177
  icon: <ArrowDown01 size={18} />,
177
178
  },
178
179
  {
179
180
  title: 'Sort Codes',
180
181
  label: '',
181
- href: '/sort-codes',
182
+ href: ROUTES.SORT_CODES,
182
183
  icon: <Book size={18} />,
183
184
  },
184
185
  ],
@@ -190,7 +191,7 @@ export const sidelinks: SideLink[] = [
190
191
  icon: <Files size={18} />,
191
192
  sub: [
192
193
  {
193
- href: '/documents',
194
+ href: ROUTES.DOCUMENTS.ALL,
194
195
  title: 'All Documents',
195
196
  label: '',
196
197
  icon: <Files size={18} />,
@@ -198,19 +199,19 @@ export const sidelinks: SideLink[] = [
198
199
  {
199
200
  title: 'Issue Documents',
200
201
  label: '',
201
- href: '/documents/issue-documents',
202
+ href: ROUTES.DOCUMENTS.ISSUE_DOCUMENTS,
202
203
  icon: <FilePen size={18} />,
203
204
  },
204
205
  {
205
206
  title: 'Issue Document',
206
207
  label: '',
207
- href: '/documents/issue-document',
208
+ href: ROUTES.DOCUMENTS.ISSUE_DOCUMENT,
208
209
  icon: <FilePen size={18} />,
209
210
  },
210
211
  ],
211
212
  },
212
213
  {
213
- href: '/business-trips',
214
+ href: ROUTES.BUSINESS_TRIPS.ALL,
214
215
  title: 'Business Trips',
215
216
  label: '',
216
217
  icon: <PlaneTakeoff size={18} />,
@@ -224,25 +225,25 @@ export const sidelinks: SideLink[] = [
224
225
  {
225
226
  title: 'Main',
226
227
  label: '',
227
- href: '/charts/',
228
+ href: ROUTES.CHARTS.MAIN,
228
229
  icon: <BarChartBig size={18} />,
229
230
  },
230
231
  {
231
232
  title: 'Monthly Income/Expense',
232
233
  label: '',
233
- href: '/charts/monthly-income-expense',
234
+ href: ROUTES.CHARTS.MONTHLY_INCOME_EXPENSE,
234
235
  icon: <BarChartBig size={18} />,
235
236
  },
236
237
  ],
237
238
  },
238
239
  {
239
- href: '/salaries',
240
+ href: ROUTES.SALARIES,
240
241
  title: 'Salaries',
241
242
  label: '',
242
243
  icon: <BadgeDollarSign size={18} />,
243
244
  },
244
245
  {
245
- href: '/tags',
246
+ href: ROUTES.TAGS,
246
247
  title: 'Tags',
247
248
  label: '',
248
249
  icon: <Tags size={18} />,
@@ -1,5 +1,5 @@
1
1
  import { useCallback, type ReactElement } from 'react';
2
- import { NavLink } from '@mantine/core';
2
+ import { Link } from 'react-router-dom';
3
3
  import type { ChargeFilter } from '../../gql/graphql.js';
4
4
  import { useUrlQuery } from '../../hooks/use-url-query.js';
5
5
  import { getBusinessTransactionsHref } from '../business-transactions/index.js';
@@ -50,21 +50,27 @@ export const CounterpartyCell = ({ account, diffAccount }: Props): ReactElement
50
50
  {(account || isAccountDiff) && (
51
51
  <>
52
52
  {account && (
53
- <a href={getHref(account.id)} target="_blank" rel="noreferrer">
54
- <NavLink
55
- label={account.name}
56
- className={`[&>*>.mantine-NavLink-label]:font-semibold ${isAccountDiff ? 'line-through' : ''}`}
57
- />
58
- </a>
53
+ <Link
54
+ to={getHref(account.id)}
55
+ target="_blank"
56
+ rel="noreferrer"
57
+ onClick={event => event.stopPropagation()}
58
+ className={`inline-flex items-center font-semibold ${isAccountDiff ? 'line-through' : ''}`}
59
+ >
60
+ {account.name}
61
+ </Link>
59
62
  )}
60
63
  {isAccountDiff && diffAccount && (
61
64
  <div className="border-2 border-yellow-500 rounded-md">
62
- <a href={getHref(diffAccount.id)} target="_blank" rel="noreferrer">
63
- <NavLink
64
- label={diffAccount.name}
65
- className="[&>*>.mantine-NavLink-label]:font-semibold"
66
- />
67
- </a>
65
+ <Link
66
+ to={getHref(diffAccount.id)}
67
+ target="_blank"
68
+ rel="noreferrer"
69
+ onClick={event => event.stopPropagation()}
70
+ className="inline-flex items-center font-semibold"
71
+ >
72
+ {diffAccount.name}
73
+ </Link>
68
74
  </div>
69
75
  )}
70
76
  </>
@@ -5,6 +5,7 @@ import { toast } from 'sonner';
5
5
  import { z } from 'zod';
6
6
  import { zodResolver } from '@hookform/resolvers/zod';
7
7
  import { AuthContext } from '../providers/auth-guard.js';
8
+ import { ROUTES } from '../router/routes.js';
8
9
  import { Button } from './ui/button.js';
9
10
  import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from './ui/form.js';
10
11
  import { Input } from './ui/input.js';
@@ -32,7 +33,7 @@ export function LoginPage(): ReactElement {
32
33
  function onSubmit(values: z.infer<typeof formSchema>) {
33
34
  try {
34
35
  authService.login(values.username, values.password).then(_user => {
35
- navigate('/');
36
+ navigate(ROUTES.HOME);
36
37
  });
37
38
  toast.success('Success', {
38
39
  description: 'You have successfully logged in.',
@@ -1,6 +1,6 @@
1
1
  import { useContext, useEffect, useState, type ReactElement } from 'react';
2
2
  import { Loader2 } from 'lucide-react';
3
- import { useMatch } from 'react-router-dom';
3
+ import { useParams } from 'react-router-dom';
4
4
  import { useQuery } from 'urql';
5
5
  import { Indicator, Table, Tooltip } from '@mantine/core';
6
6
  import { CorporateTaxRulingComplianceReportDocument, Currency } from '../../../gql/graphql.js';
@@ -109,10 +109,10 @@ function multipleOptionalFormatter(amounts: number[], currency: Currency) {
109
109
  }
110
110
 
111
111
  export const CorporateTaxRulingComplianceReport = (): ReactElement => {
112
- const match = useMatch('/reports/corporate-tax-ruling-compliance/:year');
112
+ const { year: yearFromUrl } = useParams<{ year: string }>();
113
113
  const { setFiltersContext } = useContext(FiltersContext);
114
114
  const [years, setYears] = useState<number[]>(
115
- (match ? [Number(match.params.year)] : undefined) ?? [new Date().getFullYear()],
115
+ yearFromUrl ? [Number(yearFromUrl)] : [new Date().getFullYear()],
116
116
  );
117
117
 
118
118
  // fetch data
@@ -1,6 +1,6 @@
1
1
  import { useContext, useEffect, useState, type ReactElement } from 'react';
2
2
  import { Loader2 } from 'lucide-react';
3
- import { useMatch } from 'react-router-dom';
3
+ import { useParams } from 'react-router-dom';
4
4
  import { useQuery } from 'urql';
5
5
  import { Table } from '@mantine/core';
6
6
  import { ProfitAndLossReportDocument } from '../../../gql/graphql.js';
@@ -135,10 +135,10 @@ import { ProfitAndLossReportFilter } from './profit-and-loss-report-filters.js';
135
135
  `;
136
136
 
137
137
  export const ProfitAndLossReport = (): ReactElement => {
138
- const match = useMatch('/reports/profit-and-loss/:year');
138
+ const { year: yearFromUrl } = useParams<{ year: string }>();
139
139
  const { setFiltersContext } = useContext(FiltersContext);
140
140
  const [year, setYear] = useState<number>(
141
- (match ? Number(match.params.year) : undefined) ?? new Date().getFullYear(),
141
+ yearFromUrl ? Number(yearFromUrl) : new Date().getFullYear(),
142
142
  );
143
143
  const [referenceYears, setReferenceYears] = useState<number[]>([]);
144
144
 
@@ -1,6 +1,6 @@
1
1
  import { useContext, useEffect, useState, type ReactElement } from 'react';
2
2
  import { Loader2 } from 'lucide-react';
3
- import { useMatch } from 'react-router-dom';
3
+ import { useParams } from 'react-router-dom';
4
4
  import { useQuery } from 'urql';
5
5
  import { Table } from '@mantine/core';
6
6
  import { TaxReportDocument } from '../../../gql/graphql.js';
@@ -140,10 +140,10 @@ import { TaxReportFilter } from './tax-report-filters.js';
140
140
  `;
141
141
 
142
142
  export const TaxReport = (): ReactElement => {
143
- const match = useMatch('/reports/tax/:year');
143
+ const { year: yearFromUrl } = useParams<{ year: string }>();
144
144
  const { setFiltersContext } = useContext(FiltersContext);
145
145
  const [year, setYear] = useState<number>(
146
- (match ? Number(match.params.year) : undefined) ?? new Date().getFullYear(),
146
+ yearFromUrl ? Number(yearFromUrl) : new Date().getFullYear(),
147
147
  );
148
148
  const [referenceYears, setReferenceYears] = useState<number[]>([]);
149
149
 
@@ -1,8 +1,8 @@
1
1
  import { type ReactElement } from 'react';
2
- import { useMatch } from 'react-router-dom';
2
+ import { useLoaderData, useParams } from 'react-router-dom';
3
3
  import { useQuery } from 'urql';
4
4
  import Business from '@/components/business/index.js';
5
- import { BusinessScreenDocument } from '../../../gql/graphql.js';
5
+ import { BusinessScreenDocument, type BusinessScreenQuery } from '../../../gql/graphql.js';
6
6
  import { AccounterLoader } from '../../common/index.js';
7
7
 
8
8
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions -- used by codegen
@@ -20,25 +20,37 @@ export function getBusinessHref(businessId: string): string {
20
20
  }
21
21
 
22
22
  export const BusinessScreen = (): ReactElement => {
23
- const match = useMatch('/businesses/:businessId');
24
-
25
- const businessId = match?.params.businessId;
23
+ const { businessId } = useParams<{ businessId: string }>();
24
+
25
+ // Try to get loader data (will be available when navigating via router)
26
+ let loaderData: BusinessScreenQuery | undefined;
27
+ try {
28
+ // eslint-disable-next-line react-hooks/rules-of-hooks
29
+ loaderData = useLoaderData() as BusinessScreenQuery;
30
+ } catch {
31
+ // No loader data - fallback to query
32
+ }
26
33
 
34
+ // Only fetch if we don't have loader data
27
35
  const [{ data, fetching }, fetchBusiness] = useQuery({
28
36
  query: BusinessScreenDocument,
29
- pause: !businessId,
37
+ pause: !businessId || !!loaderData,
30
38
  variables: {
31
39
  businessId: businessId ?? '',
32
40
  },
33
41
  });
34
42
 
35
- if (fetching && !data?.business) {
43
+ // Use loader data if available, otherwise use query data
44
+ const businessData = loaderData || data;
45
+ const isLoading = !loaderData && fetching;
46
+
47
+ if (isLoading && !businessData?.business) {
36
48
  return <AccounterLoader />;
37
49
  }
38
50
 
39
- if (!businessId || !data?.business) {
51
+ if (!businessId || !businessData?.business) {
40
52
  return <div>Business not found</div>;
41
53
  }
42
54
 
43
- return <Business data={data.business} refetchBusiness={async () => fetchBusiness()} />;
55
+ return <Business data={businessData.business} refetchBusiness={async () => fetchBusiness()} />;
44
56
  };
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useState, type ReactElement } from 'react';
2
- import { useMatch } from 'react-router-dom';
2
+ import { useLoaderData, useParams } from 'react-router-dom';
3
3
  import { useQuery } from 'urql';
4
- import { ChargeScreenDocument } from '../../../gql/graphql.js';
4
+ import { ChargeScreenDocument, type ChargeScreenQuery } from '../../../gql/graphql.js';
5
5
  import { ChargesTable } from '../../charges/charges-table.js';
6
6
  import {
7
7
  AccounterLoader,
@@ -29,9 +29,17 @@ type Props = {
29
29
  };
30
30
 
31
31
  export const Charge = ({ chargeId }: Props): ReactElement => {
32
- const match = useMatch('/charges/:chargeId');
32
+ const { chargeId: chargeIdFromUrl } = useParams<{ chargeId: string }>();
33
+ const id = chargeId || chargeIdFromUrl;
33
34
 
34
- const id = chargeId || match?.params.chargeId;
35
+ // Try to get loader data (will be available when navigating via router)
36
+ let loaderData: ChargeScreenQuery | undefined;
37
+ try {
38
+ // eslint-disable-next-line react-hooks/rules-of-hooks
39
+ loaderData = useLoaderData() as ChargeScreenQuery;
40
+ } catch {
41
+ // No loader data - component used outside router context (e.g., as child component)
42
+ }
35
43
 
36
44
  const [editChargeId, setEditChargeId] = useState<
37
45
  { id: string; onChange: () => void } | undefined
@@ -43,19 +51,24 @@ export const Charge = ({ chargeId }: Props): ReactElement => {
43
51
  undefined,
44
52
  );
45
53
 
54
+ // Only fetch if we don't have loader data and need to fetch (prop-based usage)
46
55
  const [{ data, fetching }, fetchCharge] = useQuery({
47
56
  query: ChargeScreenDocument,
48
- pause: !id,
57
+ pause: !id || !!loaderData,
49
58
  variables: {
50
59
  chargeId: id ?? '',
51
60
  },
52
61
  });
53
62
 
54
63
  useEffect(() => {
55
- if (id) {
64
+ if (id && !loaderData) {
56
65
  fetchCharge();
57
66
  }
58
- }, [id, fetchCharge]);
67
+ }, [id, loaderData, fetchCharge]);
68
+
69
+ // Use loader data if available, otherwise use query data
70
+ const chargeData = loaderData || data;
71
+ const isLoading = !loaderData && fetching;
59
72
 
60
73
  if (!id) {
61
74
  return <div>Charge not found</div>;
@@ -63,14 +76,14 @@ export const Charge = ({ chargeId }: Props): ReactElement => {
63
76
 
64
77
  return (
65
78
  <>
66
- {fetching ? (
79
+ {isLoading ? (
67
80
  <AccounterLoader />
68
81
  ) : (
69
82
  <ChargesTable
70
83
  setEditChargeId={setEditChargeId}
71
84
  setInsertDocument={setInsertDocument}
72
85
  setMatchDocuments={setMatchDocuments}
73
- data={data?.charge ? [data.charge] : []}
86
+ data={chargeData?.charge ? [chargeData.charge] : []}
74
87
  isAllOpened
75
88
  />
76
89
  )}
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useState, type ReactElement } from 'react';
2
2
  import { CheckIcon } from 'lucide-react';
3
+ import { Link } from 'react-router-dom';
3
4
  import type { ChargeFilter } from '../../../gql/graphql.js';
4
5
  import { useGetBusinesses } from '../../../hooks/use-get-businesses.js';
5
6
  import { useUpdateTransaction } from '../../../hooks/use-update-transaction.js';
@@ -85,9 +86,15 @@ export function Counterparty({ transaction, onChange }: Props): ReactElement {
85
86
  <>
86
87
  <div className="flex flex-wrap flex-col justify-center">
87
88
  {counterparty?.id ? (
88
- <a href={getHref(counterparty.id)} target="_blank" rel="noreferrer">
89
+ <Link
90
+ to={getHref(counterparty.id)}
91
+ target="_blank"
92
+ rel="noreferrer"
93
+ onClick={event => event.stopPropagation()}
94
+ className="inline-flex items-center font-semibold"
95
+ >
89
96
  {name}
90
- </a>
97
+ </Link>
91
98
  ) : (
92
99
  <>
93
100
  <SelectWithSearch
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useState, type ReactElement } from 'react';
2
2
  import { CheckIcon } from 'lucide-react';
3
+ import { Link } from 'react-router-dom';
3
4
  import {
4
5
  TransactionsTableEntityFieldsFragmentDoc,
5
6
  type ChargeFilter,
@@ -106,9 +107,15 @@ export function Counterparty({ data, onChange, enableEdit }: Props): ReactElemen
106
107
  <td>
107
108
  <div className="flex flex-wrap gap-1 items-center justify-center">
108
109
  {counterparty?.id ? (
109
- <a href={getHref(counterparty.id)} target="_blank" rel="noreferrer">
110
+ <Link
111
+ to={getHref(counterparty.id)}
112
+ target="_blank"
113
+ rel="noreferrer"
114
+ onClick={event => event.stopPropagation()}
115
+ className="inline-flex items-center font-semibold"
116
+ >
110
117
  {name}
111
- </a>
118
+ </Link>
112
119
  ) : (
113
120
  <>
114
121
  <SelectWithSearch
package/src/index.tsx CHANGED
@@ -1,32 +1,14 @@
1
1
  import { StrictMode } from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
- import {
4
- createBrowserRouter,
5
- createRoutesFromElements,
6
- Route,
7
- RouterProvider,
8
- } from 'react-router-dom';
9
- import { App } from './app.js';
10
- import { Providers } from './providers/index.js';
3
+ import { createBrowserRouter, RouterProvider } from 'react-router-dom';
4
+ import { routes } from './router/config.js';
11
5
  import './index.css';
12
6
 
13
7
  const rootElement = document.getElementById('root');
14
-
15
8
  const root = createRoot(rootElement!);
16
9
 
17
- const router = createBrowserRouter(
18
- createRoutesFromElements(
19
- <Route
20
- path="*"
21
- element={
22
- <Providers>
23
- <App />
24
- </Providers>
25
- }
26
- errorElement={<p>error</p>} // TODO: implement
27
- />,
28
- ),
29
- );
10
+ // Create router with object-based configuration
11
+ const router = createBrowserRouter(routes);
30
12
 
31
13
  root.render(
32
14
  <StrictMode>
@@ -1,7 +1,4 @@
1
- import { createContext, useEffect, useState, type ReactNode } from 'react';
2
- import { Route, Routes, useNavigate } from 'react-router-dom';
3
- import { NetworkError } from '@/components/screens/network-error.js';
4
- import { LoginPage } from '../components/login-page.js';
1
+ import { createContext, useMemo, useState, type ReactNode } from 'react';
5
2
  import { UserService } from '../services/user-service.js';
6
3
 
7
4
  type ContextType = {
@@ -14,25 +11,19 @@ export const AuthContext = createContext<ContextType>({
14
11
  setAuthService: () => void 0,
15
12
  });
16
13
 
17
- export const AuthGuard = ({ children }: { children?: ReactNode }): ReactNode => {
18
- const [authService, setAuthService] = useState<UserService>(new UserService());
19
- const navigate = useNavigate();
14
+ /**
15
+ * AuthProvider - provides authentication context
16
+ * Auth routing logic is now handled by route loaders
17
+ */
18
+ export const AuthProvider = ({ children }: { children?: ReactNode }): ReactNode => {
19
+ // Use lazy initialization to prevent creating new UserService on every render
20
+ const [authService, setAuthService] = useState<UserService>(() => new UserService());
20
21
 
21
- const loggedIn = authService.isLoggedIn();
22
+ // Memoize the context value to prevent unnecessary re-renders
23
+ const contextValue = useMemo(() => ({ authService, setAuthService }), [authService]);
22
24
 
23
- useEffect(() => {
24
- if (!loggedIn) {
25
- navigate('/login');
26
- }
27
- }, [loggedIn, navigate]);
28
-
29
- return (
30
- <AuthContext.Provider value={{ authService, setAuthService }}>
31
- <Routes>
32
- <Route path="/login" element={<LoginPage />} />
33
- <Route path="/network-error" element={<NetworkError />} />
34
- <Route path="*" element={children} />
35
- </Routes>
36
- </AuthContext.Provider>
37
- );
25
+ return <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>;
38
26
  };
27
+
28
+ // Keep AuthGuard as alias for backwards compatibility
29
+ export const AuthGuard = AuthProvider;
@@ -5,9 +5,14 @@ import { red } from '@mui/material/colors';
5
5
  import { createTheme } from '@mui/material/styles';
6
6
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
7
7
  import { Toaster } from '../components/ui/sonner.js';
8
- import { AuthGuard } from './auth-guard.js';
8
+ import { AuthContext, AuthGuard, AuthProvider } from './auth-guard.js';
9
9
  import { UrqlProvider } from './urql.js';
10
- import { UserProvider } from './user-provider.js';
10
+ import { UserContext, UserProvider, type UserInfo } from './user-provider.js';
11
+
12
+ export { AuthGuard, AuthProvider, AuthContext };
13
+ export { FiltersContext } from './filters-context.js';
14
+ export { UrqlProvider };
15
+ export { UserProvider, UserContext, type UserInfo };
11
16
 
12
17
  const queryClient = new QueryClient();
13
18