@accounter/client 0.0.8-alpha-20251022162652-6facbcde08fdda0fb354ba7105432df320367509 → 0.0.8-alpha-20251023070808-bdd9d09db3d693f2c0061a2bbd66a4bfe5224012

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 +26 -2
  2. package/dist/assets/Checkbox-Bm30-WRa.js +6 -0
  3. package/dist/assets/Progress-DxcT-wYo.js +1 -0
  4. package/dist/assets/Typography-CZLHs9y2.js +1 -0
  5. package/dist/assets/accordion-_VaDHoQb.js +1 -0
  6. package/dist/assets/accountant-approvals-C7GnMLyl.js +1 -0
  7. package/dist/assets/all-charges-DS-mdep6.js +1 -0
  8. package/dist/assets/arrow-up-down-Bt3_puEZ.js +6 -0
  9. package/dist/assets/business-Db4PzgLz.js +42 -0
  10. package/dist/assets/business-transactions-single-Canul8rR.js +1 -0
  11. package/dist/assets/business-trip-x8lljSMP.js +1 -0
  12. package/dist/assets/charges-filters-DkO9FBdC.js +1 -0
  13. package/dist/assets/charges-ledger-validation-wla_aWIc.js +1 -0
  14. package/dist/assets/chart-2uS7cQLX.js +74 -0
  15. package/dist/assets/data-table-pagination-CQQBbtVE.js +11 -0
  16. package/dist/assets/editable-business-trip-D50scoPo.js +16 -0
  17. package/dist/assets/graphql-document-dedupe-fragments-ByT8-wlV.js +1 -0
  18. package/dist/assets/index-B20Tn_iF.js +6 -0
  19. package/dist/assets/index-BJ63UrPP.js +24 -0
  20. package/dist/assets/index-BNPAKzBY.js +137 -0
  21. package/dist/assets/index-BP2Hv2J-.js +1 -0
  22. package/dist/assets/index-BTiGhl3N.js +11 -0
  23. package/dist/assets/index-BaDl3RPI.js +1 -0
  24. package/dist/assets/index-BmSAw35b.js +1 -0
  25. package/dist/assets/index-BsGG-RLV.js +876 -0
  26. package/dist/assets/index-Bskia9ol.js +1 -0
  27. package/dist/assets/index-C4JVZBO-.js +1 -0
  28. package/dist/assets/index-CDHKPA0l.js +1 -0
  29. package/dist/assets/index-CqZy7PVr.js +17 -0
  30. package/dist/assets/index-CzzfC-dD.css +1 -0
  31. package/dist/assets/index-D2w9iaKk.js +1 -0
  32. package/dist/assets/index-D5CFY1GF.js +1 -0
  33. package/dist/assets/index-DBjj2AmF.js +1 -0
  34. package/dist/assets/index-DJ33tRZ1.js +1 -0
  35. package/dist/assets/index-DKS2QKOQ.js +1 -0
  36. package/dist/assets/index-DOKYSdGb.js +1 -0
  37. package/dist/assets/index-D_PykgGt.js +2 -0
  38. package/dist/assets/index-Dk4NeG4m.js +1 -0
  39. package/dist/assets/index-Dkqd5T8J.js +2 -0
  40. package/dist/assets/index-IN8Kpfsx.js +6 -0
  41. package/dist/assets/index-OqG78rNq.js +9 -0
  42. package/dist/assets/{index.es-Bh5tff8R.js → index.es-DKjfxgqg.js} +5 -5
  43. package/dist/assets/issue-document-BFOjGS-U.js +1 -0
  44. package/dist/assets/login-page-CpRailN7.js +1 -0
  45. package/dist/assets/missing-info-charges-BnYMeatL.js +1 -0
  46. package/dist/assets/page-not-found-BLrYvmf4.js +1 -0
  47. package/dist/assets/pencil-DC6M9VTY.js +6 -0
  48. package/dist/assets/report-commentary-row-DJPRyQur.js +1 -0
  49. package/dist/assets/save-Dr3CNkcl.js +11 -0
  50. package/dist/assets/sequential-CAnleQny.js +1 -0
  51. package/dist/assets/similar-charges-by-business-modal-CCw5opHn.js +1 -0
  52. package/dist/assets/sub-DKnwnbwx.js +1 -0
  53. package/dist/assets/subMonths-B44KizJf.js +1 -0
  54. package/dist/index.html +2 -2
  55. package/package.json +2 -2
  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 +21 -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 +72 -0
  75. package/src/components/layout/dashboard-layout.tsx +4 -0
  76. package/src/components/layout/document-title.tsx +26 -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 +68 -0
  104. package/dist/assets/index-B2UYAO1O.css +0 -1
  105. package/dist/assets/index-D312yRtG.js +0 -1229
  106. package/src/components/common/buttons/button-with-label.tsx +0 -41
  107. package/src/components/common/buttons/button.tsx +0 -44
@@ -1,12 +1,15 @@
1
1
  import { useEffect, useState, type ReactElement } from 'react';
2
2
  import { useForm, type SubmitHandler } from 'react-hook-form';
3
+ import { Link } from 'react-router-dom';
3
4
  import { useQuery } from 'urql';
5
+ import { Button } from '@/components/ui/button.js';
6
+ import { Label } from '@/components/ui/label.js';
4
7
  import { Drawer, Image, Loader } from '@mantine/core';
5
8
  import { EditDocumentDocument, type UpdateDocumentFieldsInput } from '../../../gql/graphql.js';
6
9
  import { relevantDataPicker, type MakeBoolean } from '../../../helpers/form.js';
7
10
  import { useUpdateDocument } from '../../../hooks/use-update-document.js';
8
11
  import { Form } from '../../ui/form.js';
9
- import { ButtonWithLabel, ImageMagnifier, SimpleGrid } from '../index.js';
12
+ import { ImageMagnifier, SimpleGrid } from '../index.js';
10
13
  import { ModifyDocumentFields } from './modify-document-fields.js';
11
14
 
12
15
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions -- used by codegen
@@ -106,21 +109,29 @@ export const EditDocument = ({ documentId, onDone, onChange }: Props): ReactElem
106
109
  <form onSubmit={handleSubmit(onSubmit)}>
107
110
  <SimpleGrid cols={4}>
108
111
  <ModifyDocumentFields document={document} formManager={formManager} />
109
- <ButtonWithLabel
110
- target="_blank"
111
- textLabel="File"
112
- url={document?.file?.toString()}
113
- title="Open Link"
114
- />
112
+
113
+ <div className="space-y-2">
114
+ <Label htmlFor="file">File</Label>
115
+ <Link
116
+ to={document?.file?.toString() || ''}
117
+ target="_blank"
118
+ rel="noreferrer"
119
+ className="flex flex-col items-center justify-center mt-5"
120
+ >
121
+ <Button variant="outline" className="w-full mb-2">
122
+ Open File
123
+ </Button>
124
+ </Link>
125
+ </div>
115
126
  </SimpleGrid>
116
127
  <div className="flex justify-center mt-5">
117
- <button
128
+ <Button
118
129
  type="submit"
119
- className="inline-flex cursor-pointer justify-center py-2 px-4 w-2/12 border border-transparent shadow-xs text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
130
+ variant="default"
120
131
  disabled={fetching || Object.keys(dirtyFields).length === 0}
121
132
  >
122
133
  Save
123
- </button>
134
+ </Button>
124
135
  </div>
125
136
  </form>
126
137
  </Form>
@@ -1,5 +1,5 @@
1
1
  import type { ReactElement } from 'react';
2
- import { NavLink } from '@mantine/core';
2
+ import { Link } from 'react-router-dom';
3
3
  import { NewFetchedDocumentFieldsFragmentDoc } from '../../gql/graphql.js';
4
4
  import { getFragmentData, type FragmentType } from '../../gql/index.js';
5
5
  import { getChargeHref } from '../screens/charges/charge.js';
@@ -32,14 +32,16 @@ export const NewDocumentsList = ({ data }: Props): ReactElement => {
32
32
  {documents
33
33
  .filter(({ charge }) => charge)
34
34
  .map(doc => (
35
- <NavLink
35
+ <Link
36
36
  key={doc.id}
37
- label={`${doc.charge!.counterparty?.name ?? 'Unknown'} - ${doc.charge!.userDescription}`}
38
- onClick={(event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
39
- event.stopPropagation();
40
- window.open(getChargeHref(doc.charge!.id), '_blank', 'noreferrer');
41
- }}
42
- />
37
+ to={getChargeHref(doc.charge!.id)}
38
+ target="_blank"
39
+ rel="noreferrer"
40
+ onClick={event => event.stopPropagation()}
41
+ className="inline-flex items-center font-semibold"
42
+ >
43
+ {`${doc.charge!.counterparty?.name ?? 'Unknown'} - ${doc.charge!.userDescription}`}
44
+ </Link>
43
45
  ))}
44
46
  </div>
45
47
  );
@@ -1,9 +1,10 @@
1
1
  import { useCallback, useMemo, useState, type ReactElement } from 'react';
2
+ import { Link } from 'react-router-dom';
2
3
  import { DocumentType } from '@/gql/graphql.js';
3
4
  import { useGetBusinesses } from '@/hooks/use-get-businesses.js';
4
5
  import { useUpdateDocument } from '@/hooks/use-update-document.js';
5
6
  import { useUrlQuery } from '@/hooks/use-url-query.js';
6
- import { Indicator, NavLink } from '@mantine/core';
7
+ import { Indicator } from '@mantine/core';
7
8
  import { getBusinessHref } from '../../charges/helpers.js';
8
9
  import { ConfirmMiniButton, InsertBusiness, SelectWithSearch } from '../../common/index.js';
9
10
  import type { DocumentsTableRowType } from '../columns.js';
@@ -104,9 +105,15 @@ export const Creditor = ({ document, onChange }: Props): ReactElement => {
104
105
  <Indicator inline size={12} disabled={!isError} color="red" zIndex="auto">
105
106
  {shouldHaveCreditor &&
106
107
  (id ? (
107
- <a href={getHref(id)} target="_blank" rel="noreferrer">
108
- <NavLink label={name} className="[&>*>.mantine-NavLink-label]:font-semibold" />
109
- </a>
108
+ <Link
109
+ to={getHref(id)}
110
+ target="_blank"
111
+ rel="noreferrer"
112
+ onClick={event => event.stopPropagation()}
113
+ className="inline-flex items-center font-semibold"
114
+ >
115
+ {name}
116
+ </Link>
110
117
  ) : (
111
118
  <SelectWithSearch
112
119
  options={selectableBusinesses}
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useMemo, useState, type ReactElement } from 'react';
2
+ import { Link } from 'react-router-dom';
2
3
  import { useGetBusinesses } from '@/hooks/use-get-businesses.js';
3
- import { Indicator, NavLink } from '@mantine/core';
4
+ import { Indicator } from '@mantine/core';
4
5
  import { DocumentType } from '../../../gql/graphql.js';
5
6
  import { useUpdateDocument } from '../../../hooks/use-update-document.js';
6
7
  import { useUrlQuery } from '../../../hooks/use-url-query.js';
@@ -100,9 +101,15 @@ export const Debtor = ({ document, onChange }: Props): ReactElement => {
100
101
  <Indicator inline size={12} disabled={!isError} color="red" zIndex="auto">
101
102
  {shouldHaveDebtor &&
102
103
  (id ? (
103
- <a href={getHref(id)} target="_blank" rel="noreferrer">
104
- <NavLink label={name} className="[&>*>.mantine-NavLink-label]:font-semibold" />
105
- </a>
104
+ <Link
105
+ to={getHref(id)}
106
+ target="_blank"
107
+ rel="noreferrer"
108
+ onClick={event => event.stopPropagation()}
109
+ className="inline-flex items-center font-semibold"
110
+ >
111
+ {name}
112
+ </Link>
106
113
  ) : (
107
114
  <SelectWithSearch
108
115
  options={selectableBusinesses}
@@ -0,0 +1,189 @@
1
+ import { type ReactElement } from 'react';
2
+ import { AlertCircle, Home, RefreshCw } from 'lucide-react';
3
+ import { isRouteErrorResponse, Link, useRouteError } from 'react-router-dom';
4
+ import { ROUTES } from '../router/routes.js';
5
+ import { Button } from './ui/button.js';
6
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card.js';
7
+
8
+ /**
9
+ * Global error boundary for router errors
10
+ * Handles different error types and provides appropriate UI
11
+ */
12
+ export function ErrorBoundary(): ReactElement {
13
+ const error = useRouteError();
14
+
15
+ // Handle route errors (404, 401, 500, etc.)
16
+ if (isRouteErrorResponse(error)) {
17
+ switch (error.status) {
18
+ case 404:
19
+ return (
20
+ <ErrorLayout
21
+ icon={<AlertCircle className="h-16 w-16 text-yellow-500" />}
22
+ title="Page Not Found"
23
+ description="The page you're looking for doesn't exist."
24
+ statusCode={404}
25
+ >
26
+ <div className="flex gap-4">
27
+ <Link to={ROUTES.HOME}>
28
+ <Button variant="default">
29
+ <Home className="mr-2 h-4 w-4" />
30
+ Go Home
31
+ </Button>
32
+ </Link>
33
+ <Button variant="outline" onClick={() => window.history.back()}>
34
+ Go Back
35
+ </Button>
36
+ </div>
37
+ </ErrorLayout>
38
+ );
39
+
40
+ case 401:
41
+ return (
42
+ <ErrorLayout
43
+ icon={<AlertCircle className="h-16 w-16 text-red-500" />}
44
+ title="Unauthorized"
45
+ description="You need to be logged in to access this page."
46
+ statusCode={401}
47
+ >
48
+ <Link to={ROUTES.LOGIN}>
49
+ <Button variant="default">Go to Login</Button>
50
+ </Link>
51
+ </ErrorLayout>
52
+ );
53
+
54
+ case 403:
55
+ return (
56
+ <ErrorLayout
57
+ icon={<AlertCircle className="h-16 w-16 text-red-500" />}
58
+ title="Forbidden"
59
+ description="You don't have permission to access this resource."
60
+ statusCode={403}
61
+ >
62
+ <Link to={ROUTES.HOME}>
63
+ <Button variant="default">
64
+ <Home className="mr-2 h-4 w-4" />
65
+ Go Home
66
+ </Button>
67
+ </Link>
68
+ </ErrorLayout>
69
+ );
70
+
71
+ case 500:
72
+ return (
73
+ <ErrorLayout
74
+ icon={<AlertCircle className="h-16 w-16 text-red-500" />}
75
+ title="Server Error"
76
+ description="Something went wrong on our end. We're working to fix it."
77
+ statusCode={500}
78
+ >
79
+ <div className="flex gap-4">
80
+ <Button variant="default" onClick={() => window.location.reload()}>
81
+ <RefreshCw className="mr-2 h-4 w-4" />
82
+ Reload Page
83
+ </Button>
84
+ <Link to={ROUTES.HOME}>
85
+ <Button variant="outline">
86
+ <Home className="mr-2 h-4 w-4" />
87
+ Go Home
88
+ </Button>
89
+ </Link>
90
+ </div>
91
+ </ErrorLayout>
92
+ );
93
+
94
+ default:
95
+ return (
96
+ <ErrorLayout
97
+ icon={<AlertCircle className="h-16 w-16 text-orange-500" />}
98
+ title={`Error ${error.status}`}
99
+ description={error.statusText || 'An unexpected error occurred'}
100
+ statusCode={error.status}
101
+ >
102
+ <Link to={ROUTES.HOME}>
103
+ <Button variant="default">
104
+ <Home className="mr-2 h-4 w-4" />
105
+ Go Home
106
+ </Button>
107
+ </Link>
108
+ </ErrorLayout>
109
+ );
110
+ }
111
+ }
112
+
113
+ // Handle generic JavaScript errors
114
+ const genericError = error as Error;
115
+ const isDevelopment = import.meta.env.DEV;
116
+
117
+ return (
118
+ <ErrorLayout
119
+ icon={<AlertCircle className="h-16 w-16 text-red-500" />}
120
+ title="Unexpected Error"
121
+ description="An unexpected error occurred. Please try again."
122
+ >
123
+ {isDevelopment && genericError.message && (
124
+ <Card className="max-w-2xl mt-4 bg-red-50 border-red-200">
125
+ <CardHeader>
126
+ <CardTitle className="text-sm text-red-700">Error Details (Dev Only)</CardTitle>
127
+ </CardHeader>
128
+ <CardContent>
129
+ <pre className="text-xs text-red-600 overflow-auto">
130
+ {genericError.message}
131
+ {genericError.stack && `\n\n${genericError.stack}`}
132
+ </pre>
133
+ </CardContent>
134
+ </Card>
135
+ )}
136
+ <div className="flex gap-4 mt-4">
137
+ <Button variant="default" onClick={() => window.location.reload()}>
138
+ <RefreshCw className="mr-2 h-4 w-4" />
139
+ Reload Page
140
+ </Button>
141
+ <Link to={ROUTES.HOME}>
142
+ <Button variant="outline">
143
+ <Home className="mr-2 h-4 w-4" />
144
+ Go Home
145
+ </Button>
146
+ </Link>
147
+ </div>
148
+ </ErrorLayout>
149
+ );
150
+ }
151
+
152
+ /**
153
+ * Reusable error layout component
154
+ */
155
+ interface ErrorLayoutProps {
156
+ icon: ReactElement;
157
+ title: string;
158
+ description: string;
159
+ statusCode?: number;
160
+ children?: React.ReactNode;
161
+ }
162
+
163
+ function ErrorLayout({
164
+ icon,
165
+ title,
166
+ description,
167
+ statusCode,
168
+ children,
169
+ }: ErrorLayoutProps): ReactElement {
170
+ return (
171
+ <div className="min-h-screen flex items-center justify-center bg-gray-50 p-4">
172
+ <Card className="max-w-md w-full">
173
+ <CardHeader className="text-center space-y-4">
174
+ <div className="flex justify-center">{icon}</div>
175
+ <div>
176
+ <CardTitle className="text-2xl font-bold">
177
+ {title}
178
+ {statusCode && (
179
+ <span className="ml-2 text-sm text-muted-foreground">({statusCode})</span>
180
+ )}
181
+ </CardTitle>
182
+ <CardDescription className="mt-2">{description}</CardDescription>
183
+ </div>
184
+ </CardHeader>
185
+ <CardContent className="flex flex-col items-center space-y-4">{children}</CardContent>
186
+ </Card>
187
+ </div>
188
+ );
189
+ }
@@ -0,0 +1,72 @@
1
+ import { Fragment, type ReactElement } from 'react';
2
+ import { ChevronRight, Home } from 'lucide-react';
3
+ import { Link, useMatches } from 'react-router-dom';
4
+ import type { RouteHandle } from '@/router/types.js';
5
+
6
+ interface Crumb {
7
+ title: string;
8
+ path: string;
9
+ isLast: boolean;
10
+ }
11
+
12
+ /**
13
+ * Breadcrumbs component - displays navigation path
14
+ * Reads breadcrumb data from route handles
15
+ */
16
+ export function Breadcrumbs(): ReactElement {
17
+ const matches = useMatches();
18
+
19
+ // Build breadcrumb trail from matched routes
20
+ const crumbs: Crumb[] = matches
21
+ .filter(match => {
22
+ const handle = match.handle as RouteHandle | undefined;
23
+ return handle?.breadcrumb;
24
+ })
25
+ .map((match, index, array) => {
26
+ const handle = match.handle as RouteHandle;
27
+ const title =
28
+ typeof handle.breadcrumb === 'function'
29
+ ? handle.breadcrumb(match.data)
30
+ : handle.breadcrumb || 'Page';
31
+
32
+ return {
33
+ title,
34
+ path: match.pathname,
35
+ isLast: index === array.length - 1,
36
+ };
37
+ });
38
+
39
+ // Don't render if no breadcrumbs
40
+ if (crumbs.length === 0) {
41
+ return <div className="h-8" />; // Spacer to maintain layout
42
+ }
43
+
44
+ return (
45
+ <nav aria-label="Breadcrumb" className="flex items-center space-x-1 text-sm text-gray-600">
46
+ {/* Home icon as first item */}
47
+ <Link
48
+ to="/"
49
+ className="flex items-center hover:text-gray-900 transition-colors"
50
+ aria-label="Home"
51
+ >
52
+ <Home className="h-4 w-4" />
53
+ </Link>
54
+
55
+ {/* Breadcrumb items */}
56
+ {crumbs.map((crumb, index) => (
57
+ <Fragment key={`${crumb.path}-${index}`}>
58
+ <ChevronRight className="h-4 w-4 text-gray-400" />
59
+ {crumb.isLast ? (
60
+ <span className="font-medium text-gray-900" aria-current="page">
61
+ {crumb.title}
62
+ </span>
63
+ ) : (
64
+ <Link to={crumb.path} className="hover:text-gray-900 transition-colors">
65
+ {crumb.title}
66
+ </Link>
67
+ )}
68
+ </Fragment>
69
+ ))}
70
+ </nav>
71
+ );
72
+ }
@@ -1,7 +1,9 @@
1
1
  import type { ReactElement } from 'react';
2
2
  import { useSidebar } from '../../hooks/use-sidebar.js';
3
+ import { Breadcrumbs } from './breadcrumbs.js';
3
4
  import { Footer } from './footer.js';
4
5
  import { Header } from './header.js';
6
+ import { NavigationProgress } from './navigation-progress.js';
5
7
  import { Sidebar } from './sidebar.js';
6
8
 
7
9
  type DashboardLayoutProps = {
@@ -14,12 +16,14 @@ export function DashboardLayout({ children, filtersContext }: DashboardLayoutPro
14
16
 
15
17
  return (
16
18
  <main>
19
+ <NavigationProgress />
17
20
  <Header />
18
21
  <div className="flex h-screen overflow-hidden bg-gray-100">
19
22
  <Sidebar />
20
23
  <div
21
24
  className={`overflow-scroll flex flex-col justify-start gap-10 z-0 my-20 pl-10 flex-1 ${isMinimized ? 'pr-10' : 'pr-5'} transition-all`}
22
25
  >
26
+ <Breadcrumbs />
23
27
  {children}
24
28
  </div>
25
29
  </div>
@@ -0,0 +1,26 @@
1
+ import { useEffect } from 'react';
2
+ import { useMatches } from 'react-router-dom';
3
+ import type { RouteHandle } from '@/router/types.js';
4
+
5
+ /**
6
+ * Component that updates document title based on route handle
7
+ * Place this in the root layout to automatically update titles
8
+ */
9
+ export function DocumentTitle() {
10
+ const matches = useMatches();
11
+
12
+ useEffect(() => {
13
+ const lastMatch = matches[matches.length - 1];
14
+ const handle = lastMatch?.handle as RouteHandle | undefined;
15
+
16
+ if (handle?.title) {
17
+ const title =
18
+ typeof handle.title === 'function' ? handle.title(lastMatch.data) : handle.title;
19
+ document.title = `${title} - Accounter`;
20
+ } else {
21
+ document.title = 'Accounter';
22
+ }
23
+ }, [matches]);
24
+
25
+ return <div style={{ display: 'none' }} />;
26
+ }
@@ -0,0 +1,52 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useNavigation } from 'react-router-dom';
3
+ import { Progress } from '../ui/progress.js';
4
+
5
+ /**
6
+ * Global navigation progress indicator
7
+ * Shows a loading bar at the top of the page during route transitions
8
+ */
9
+ export function NavigationProgress() {
10
+ const navigation = useNavigation();
11
+ const [progress, setProgress] = useState(0);
12
+ const isLoading = navigation.state === 'loading';
13
+
14
+ useEffect(() => {
15
+ if (isLoading) {
16
+ // Start at 20% when loading begins
17
+ setProgress(20);
18
+
19
+ // Simulate progress
20
+ const interval = setInterval(() => {
21
+ setProgress(prev => {
22
+ // Asymptotically approach 90% but never quite reach it
23
+ const next = prev + (90 - prev) * 0.1;
24
+ return Math.min(next, 90);
25
+ });
26
+ }, 300);
27
+
28
+ return () => clearInterval(interval);
29
+ }
30
+
31
+ // Complete the progress when loading finishes
32
+ setProgress(100);
33
+
34
+ // Reset after animation
35
+ const timeout = setTimeout(() => {
36
+ setProgress(0);
37
+ }, 500);
38
+
39
+ return () => clearTimeout(timeout);
40
+ }, [isLoading]);
41
+
42
+ // Don't render anything if not loading and progress is 0
43
+ if (!isLoading && progress === 0) {
44
+ return null;
45
+ }
46
+
47
+ return (
48
+ <div className="fixed top-0 left-0 right-0 z-50 h-1">
49
+ <Progress value={progress} className="h-1 rounded-none transition-all duration-300" />
50
+ </div>
51
+ );
52
+ }
@@ -0,0 +1,49 @@
1
+ import type { ReactElement } from 'react';
2
+ import { Skeleton } from '../ui/skeleton.js';
3
+
4
+ /**
5
+ * Generic page loading skeleton
6
+ * Displayed during route transitions
7
+ */
8
+ export function PageSkeleton(): ReactElement {
9
+ return (
10
+ <div className="flex flex-col gap-4 p-4">
11
+ <Skeleton className="h-12 w-64" /> {/* Page title */}
12
+ <div className="flex gap-4">
13
+ <Skeleton className="h-10 w-32" /> {/* Button/filter */}
14
+ <Skeleton className="h-10 w-32" />
15
+ </div>
16
+ <Skeleton className="h-96 w-full" /> {/* Main content area */}
17
+ </div>
18
+ );
19
+ }
20
+
21
+ /**
22
+ * Table loading skeleton
23
+ */
24
+ export function TableSkeleton(): ReactElement {
25
+ return (
26
+ <div className="flex flex-col gap-2">
27
+ <Skeleton className="h-12 w-full" /> {/* Header */}
28
+ {Array.from({ length: 8 }).map((_, i) => (
29
+ <Skeleton key={i} className="h-16 w-full" />
30
+ ))}
31
+ </div>
32
+ );
33
+ }
34
+
35
+ /**
36
+ * Report loading skeleton
37
+ */
38
+ export function ReportSkeleton(): ReactElement {
39
+ return (
40
+ <div className="flex flex-col gap-6">
41
+ <Skeleton className="h-16 w-96" /> {/* Report title */}
42
+ <div className="flex gap-4">
43
+ <Skeleton className="h-10 w-48" /> {/* Date filter */}
44
+ <Skeleton className="h-10 w-32" /> {/* Export button */}
45
+ </div>
46
+ <Skeleton className="h-[600px] w-full" /> {/* Report content */}
47
+ </div>
48
+ );
49
+ }