@accounter/client 0.0.8-alpha-20251022130946-0923a77d2ee3f22a60a7f5b1e0623bd3bee88868 → 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.
- package/CHANGELOG.md +25 -1
- package/dist/assets/Checkbox-D7nOeER7.js +6 -0
- package/dist/assets/Progress-_KQ8BRrQ.js +1 -0
- package/dist/assets/Typography-BYHNiKxL.js +1 -0
- package/dist/assets/accordion-aunUrlum.js +1 -0
- package/dist/assets/accountant-approvals-1lH0z-Rv.js +1 -0
- package/dist/assets/all-charges-Cngljy2T.js +1 -0
- package/dist/assets/arrow-up-down-De1dsSxL.js +6 -0
- package/dist/assets/business-7l6LGFQt.js +37 -0
- package/dist/assets/business-transactions-single-BmOnrzw-.js +1 -0
- package/dist/assets/business-trip-BH8ZEeSB.js +1 -0
- package/dist/assets/charges-filters-DkzCOY0r.js +1 -0
- package/dist/assets/charges-ledger-validation-tNA_f_7H.js +1 -0
- package/dist/assets/chart-BJ85ZFS0.js +74 -0
- package/dist/assets/data-table-pagination-_iPJtRUP.js +11 -0
- package/dist/assets/editable-business-trip-DCtXJ6w4.js +16 -0
- package/dist/assets/graphql-document-dedupe-fragments-ByT8-wlV.js +1 -0
- package/dist/assets/index-6W3Ndi5M.js +1 -0
- package/dist/assets/index-A086__I6.js +1 -0
- package/dist/assets/index-AoVRWBXr.js +6 -0
- package/dist/assets/index-BGZNb2wc.js +17 -0
- package/dist/assets/index-BHUgtFG6.js +1 -0
- package/dist/assets/index-BQw927FW.js +11 -0
- package/dist/assets/index-C27oA70V.js +1 -0
- package/dist/assets/index-CIqGQ8uI.js +1 -0
- package/dist/assets/index-CMXNoVEJ.js +24 -0
- package/dist/assets/index-CQEXsBvi.js +1 -0
- package/dist/assets/index-CRWwjUSx.js +1 -0
- package/dist/assets/index-CUPkGo8z.js +1 -0
- package/dist/assets/index-Ch3veRcP.js +1 -0
- package/dist/assets/index-Cll1w4iD.js +6 -0
- package/dist/assets/index-Cnf3x_0g.js +2 -0
- package/dist/assets/index-DCiQggcN.js +9 -0
- package/dist/assets/index-DFO0fSvK.js +876 -0
- package/dist/assets/index-DLrRdx1l.js +1 -0
- package/dist/assets/index-DgOX69C5.js +1 -0
- package/dist/assets/index-DqPz6G2w.js +2 -0
- package/dist/assets/index-DxgUoyCT.js +1 -0
- package/dist/assets/index-Dxkz1HG4.js +137 -0
- package/dist/assets/index-LwYKcUCw.js +1 -0
- package/dist/assets/index-gdTXrWXt.css +1 -0
- package/dist/assets/{index.es-CWwhWGxX.js → index.es-g2vV-Mpx.js} +5 -5
- package/dist/assets/issue-document-DlwQP2Xx.js +1 -0
- package/dist/assets/login-page-C2voZ_Kj.js +1 -0
- package/dist/assets/missing-info-charges-eJYTfSDz.js +1 -0
- package/dist/assets/page-not-found-t6EEvRUF.js +1 -0
- package/dist/assets/pencil-nACAiGf5.js +6 -0
- package/dist/assets/report-commentary-row-BeCkGfXh.js +1 -0
- package/dist/assets/save-DIy7YBvX.js +11 -0
- package/dist/assets/sequential-CAnleQny.js +1 -0
- package/dist/assets/similar-charges-by-business-modal-BTts1hyT.js +1 -0
- package/dist/assets/sub-CNmFodmJ.js +1 -0
- package/dist/assets/subMonths-BkHf0iFx.js +1 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/components/business-transactions/business-extended-info.tsx +13 -15
- package/src/components/business-transactions/business-transactions-single.tsx +3 -3
- package/src/components/business-transactions/index.tsx +12 -1
- package/src/components/business-trips/business-trip.tsx +3 -3
- package/src/components/charges/cells/business-trip.tsx +6 -8
- package/src/components/charges/cells/counterparty.tsx +7 -5
- package/src/components/common/accounter-table.tsx +6 -5
- package/src/components/common/business-trip-report/parts/core-expense-row.tsx +11 -9
- package/src/components/common/business-trip-report/parts/uncategorized-transactions.tsx +11 -13
- package/src/components/common/buttons/index.ts +0 -2
- package/src/components/common/buttons/logout-button.tsx +7 -6
- package/src/components/common/documents-to-charge-matcher/selection-handler/index.tsx +4 -2
- package/src/components/common/documents-to-charge-matcher/selection-handler/wide-filtered-selection.tsx +5 -7
- package/src/components/common/forms/edit-document.tsx +23 -10
- package/src/components/common/new-documents-list.tsx +10 -8
- package/src/components/documents-table/cells/creditor.tsx +11 -4
- package/src/components/documents-table/cells/debtor.tsx +11 -4
- package/src/components/error-boundary.tsx +189 -0
- package/src/components/layout/breadcrumbs.tsx +77 -0
- package/src/components/layout/dashboard-layout.tsx +4 -0
- package/src/components/layout/document-title.tsx +31 -0
- package/src/components/layout/navigation-progress.tsx +52 -0
- package/src/components/layout/page-skeleton.tsx +49 -0
- package/src/components/layout/sidelinks.tsx +28 -27
- package/src/components/ledger-table/counterparty-cell.tsx +19 -13
- package/src/components/login-page.tsx +2 -1
- package/src/components/reports/corporate-tax-ruling-compliance-report/index.tsx +3 -3
- package/src/components/reports/profit-and-loss-report/index.tsx +3 -3
- package/src/components/reports/tax-report/index.tsx +3 -3
- package/src/components/screens/businesses/business.tsx +21 -9
- package/src/components/screens/charges/charge.tsx +22 -9
- package/src/components/transactions-table/cells/counterparty.tsx +9 -2
- package/src/components/transactions-table/cells-legacy/counterparty.tsx +9 -2
- package/src/gql/graphql.ts +1554 -4842
- package/src/index.tsx +4 -22
- package/src/providers/auth-guard.tsx +14 -23
- package/src/providers/index.tsx +7 -2
- package/src/providers/urql-client.ts +86 -0
- package/src/providers/urql.tsx +7 -12
- package/src/providers/user-provider.tsx +3 -2
- package/src/router/config.tsx +534 -0
- package/src/router/layouts/dashboard-layout.tsx +20 -0
- package/src/router/layouts/root-layout.tsx +69 -0
- package/src/router/loaders/auth-loader.ts +32 -0
- package/src/router/loaders/business-loader.ts +25 -0
- package/src/router/loaders/charge-loader.ts +25 -0
- package/src/router/loaders/index.ts +17 -0
- package/src/router/routes.ts +88 -0
- package/src/router/types.ts +62 -0
- package/dist/assets/index-B2UYAO1O.css +0 -1
- package/dist/assets/index-BexxGuN6.js +0 -1224
- package/src/components/common/buttons/button-with-label.tsx +0 -41
- 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 {
|
|
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,31 @@ 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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
<
|
|
128
|
+
<Button
|
|
118
129
|
type="submit"
|
|
119
|
-
|
|
130
|
+
variant="default"
|
|
131
|
+
// className="inline-flex cursor-pointer justify-center py-2 px-4 w-2/12 mr-5 border border-transparent shadow-xs text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
132
|
+
onClick={onDone}
|
|
120
133
|
disabled={fetching || Object.keys(dirtyFields).length === 0}
|
|
121
134
|
>
|
|
122
135
|
Save
|
|
123
|
-
</
|
|
136
|
+
</Button>
|
|
124
137
|
</div>
|
|
125
138
|
</form>
|
|
126
139
|
</Form>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactElement } from 'react';
|
|
2
|
-
import {
|
|
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
|
-
<
|
|
35
|
+
<Link
|
|
36
36
|
key={doc.id}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
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
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
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,77 @@
|
|
|
1
|
+
import { Fragment, type ReactElement } from 'react';
|
|
2
|
+
import { ChevronRight, Home } from 'lucide-react';
|
|
3
|
+
import { Link, useMatches } from 'react-router-dom';
|
|
4
|
+
|
|
5
|
+
interface RouteHandle {
|
|
6
|
+
breadcrumb?: string | ((data?: unknown) => string);
|
|
7
|
+
title?: string | ((data?: unknown) => string);
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface Crumb {
|
|
12
|
+
title: string;
|
|
13
|
+
path: string;
|
|
14
|
+
isLast: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Breadcrumbs component - displays navigation path
|
|
19
|
+
* Reads breadcrumb data from route handles
|
|
20
|
+
*/
|
|
21
|
+
export function Breadcrumbs(): ReactElement {
|
|
22
|
+
const matches = useMatches();
|
|
23
|
+
|
|
24
|
+
// Build breadcrumb trail from matched routes
|
|
25
|
+
const crumbs: Crumb[] = matches
|
|
26
|
+
.filter(match => {
|
|
27
|
+
const handle = match.handle as RouteHandle | undefined;
|
|
28
|
+
return handle?.breadcrumb;
|
|
29
|
+
})
|
|
30
|
+
.map((match, index, array) => {
|
|
31
|
+
const handle = match.handle as RouteHandle;
|
|
32
|
+
const title =
|
|
33
|
+
typeof handle.breadcrumb === 'function'
|
|
34
|
+
? handle.breadcrumb(match.data)
|
|
35
|
+
: handle.breadcrumb || 'Page';
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
title,
|
|
39
|
+
path: match.pathname,
|
|
40
|
+
isLast: index === array.length - 1,
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Don't render if no breadcrumbs
|
|
45
|
+
if (crumbs.length === 0) {
|
|
46
|
+
return <div className="h-8" />; // Spacer to maintain layout
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<nav aria-label="Breadcrumb" className="flex items-center space-x-1 text-sm text-gray-600">
|
|
51
|
+
{/* Home icon as first item */}
|
|
52
|
+
<Link
|
|
53
|
+
to="/"
|
|
54
|
+
className="flex items-center hover:text-gray-900 transition-colors"
|
|
55
|
+
aria-label="Home"
|
|
56
|
+
>
|
|
57
|
+
<Home className="h-4 w-4" />
|
|
58
|
+
</Link>
|
|
59
|
+
|
|
60
|
+
{/* Breadcrumb items */}
|
|
61
|
+
{crumbs.map((crumb, index) => (
|
|
62
|
+
<Fragment key={`${crumb.path}-${index}`}>
|
|
63
|
+
<ChevronRight className="h-4 w-4 text-gray-400" />
|
|
64
|
+
{crumb.isLast ? (
|
|
65
|
+
<span className="font-medium text-gray-900" aria-current="page">
|
|
66
|
+
{crumb.title}
|
|
67
|
+
</span>
|
|
68
|
+
) : (
|
|
69
|
+
<Link to={crumb.path} className="hover:text-gray-900 transition-colors">
|
|
70
|
+
{crumb.title}
|
|
71
|
+
</Link>
|
|
72
|
+
)}
|
|
73
|
+
</Fragment>
|
|
74
|
+
))}
|
|
75
|
+
</nav>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -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,31 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useMatches } from 'react-router-dom';
|
|
3
|
+
|
|
4
|
+
interface RouteHandle {
|
|
5
|
+
title?: string | ((data?: unknown) => string);
|
|
6
|
+
breadcrumb?: string | ((data?: unknown) => string);
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Component that updates document title based on route handle
|
|
12
|
+
* Place this in the root layout to automatically update titles
|
|
13
|
+
*/
|
|
14
|
+
export function DocumentTitle() {
|
|
15
|
+
const matches = useMatches();
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const lastMatch = matches[matches.length - 1];
|
|
19
|
+
const handle = lastMatch?.handle as RouteHandle | undefined;
|
|
20
|
+
|
|
21
|
+
if (handle?.title) {
|
|
22
|
+
const title =
|
|
23
|
+
typeof handle.title === 'function' ? handle.title(lastMatch.data) : handle.title;
|
|
24
|
+
document.title = `${title} - Accounter`;
|
|
25
|
+
} else {
|
|
26
|
+
document.title = 'Accounter';
|
|
27
|
+
}
|
|
28
|
+
}, [matches]);
|
|
29
|
+
|
|
30
|
+
return <div style={{ display: 'none' }} />;
|
|
31
|
+
}
|
|
@@ -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
|
+
}
|