create-bluecopa-react-app 1.0.40 → 1.0.42
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/README.md +16 -14
- package/package.json +1 -1
- package/templates/latest/.claude/settings.local.json +56 -0
- package/templates/latest/.env.example +8 -0
- package/templates/latest/Agent.md +598 -775
- package/templates/latest/CLAUDE.md +759 -0
- package/templates/latest/README.md +17 -8
- package/templates/latest/app/app.css +292 -85
- package/templates/latest/app/app.tsx +48 -39
- package/templates/latest/app/components/bluecopa-logo.tsx +20 -0
- package/templates/latest/app/components/charts/bar-chart.tsx +132 -0
- package/templates/latest/app/components/charts/base-chart.tsx +149 -0
- package/templates/latest/app/components/charts/chart-provider.tsx +71 -0
- package/templates/latest/app/components/charts/chart-theme.ts +262 -0
- package/templates/latest/app/components/charts/chart-utils.ts +142 -0
- package/templates/latest/app/components/charts/donut-chart.tsx +110 -0
- package/templates/latest/app/components/charts/index.ts +5 -0
- package/templates/latest/app/components/layouts/app-layout.tsx +22 -0
- package/templates/latest/app/components/layouts/app-sidebar.tsx +88 -0
- package/templates/latest/app/components/layouts/nav-main.tsx +50 -0
- package/templates/latest/app/components/layouts/nav-user.tsx +38 -0
- package/templates/latest/app/components/layouts/site-header.tsx +93 -0
- package/templates/latest/app/components/loading-screen.tsx +41 -0
- package/templates/latest/app/components/ui/ag-grid-table.tsx +79 -0
- package/templates/latest/app/components/ui/button.tsx +23 -23
- package/templates/latest/app/components/ui/card.tsx +20 -20
- package/templates/latest/app/components/ui/dropdown-menu.tsx +54 -49
- package/templates/latest/app/components/ui/input.tsx +8 -8
- package/templates/latest/app/components/ui/label.tsx +8 -8
- package/templates/latest/app/components/ui/separator.tsx +7 -7
- package/templates/latest/app/components/ui/sheet.tsx +43 -32
- package/templates/latest/app/components/ui/sidebar.tsx +240 -235
- package/templates/latest/app/components/ui/skeleton.tsx +4 -4
- package/templates/latest/app/components/ui/sonner.tsx +6 -9
- package/templates/latest/app/components/ui/tabs.tsx +15 -15
- package/templates/latest/app/components/ui/tooltip.tsx +18 -12
- package/templates/latest/app/constants/index.ts +31 -0
- package/templates/latest/app/contexts/app-context.tsx +201 -0
- package/templates/latest/app/hooks/use-mobile.ts +13 -12
- package/templates/latest/app/main.tsx +1 -1
- package/templates/latest/app/pages/dashboard.tsx +246 -0
- package/templates/latest/app/pages/payments.tsx +182 -0
- package/templates/latest/app/pages/settings.tsx +128 -0
- package/templates/latest/app/routes/index.tsx +19 -0
- package/templates/latest/app/single-spa.tsx +68 -86
- package/templates/latest/app/types/index.ts +37 -0
- package/templates/latest/app/utils/ag-grid-datasource.ts +63 -0
- package/templates/latest/app/utils/ag-grid-license.ts +12 -0
- package/templates/latest/app/utils/ag-grid-theme.ts +9 -0
- package/templates/latest/app/utils/component-style.ts +7 -0
- package/templates/latest/app/utils/style-drivers.ts +24 -0
- package/templates/latest/app/utils/utils.ts +10 -0
- package/templates/latest/components.json +3 -3
- package/templates/latest/index.html +30 -2
- package/templates/latest/package-lock.json +30 -416
- package/templates/latest/package.json +8 -18
- package/templates/latest/preview/index.html +125 -285
- package/templates/latest/public/favicon.svg +1 -0
- package/templates/latest/vite.config.ts +2 -8
- package/templates/latest/app/components/app-sidebar.tsx +0 -182
- package/templates/latest/app/components/chart-area-interactive.tsx +0 -290
- package/templates/latest/app/components/data-table.tsx +0 -807
- package/templates/latest/app/components/nav-documents.tsx +0 -92
- package/templates/latest/app/components/nav-main.tsx +0 -40
- package/templates/latest/app/components/nav-secondary.tsx +0 -42
- package/templates/latest/app/components/nav-user.tsx +0 -111
- package/templates/latest/app/components/section-cards.tsx +0 -102
- package/templates/latest/app/components/site-header.tsx +0 -28
- package/templates/latest/app/components/ui/avatar.tsx +0 -53
- package/templates/latest/app/components/ui/badge.tsx +0 -46
- package/templates/latest/app/components/ui/breadcrumb.tsx +0 -109
- package/templates/latest/app/components/ui/chart.tsx +0 -352
- package/templates/latest/app/components/ui/checkbox.tsx +0 -30
- package/templates/latest/app/components/ui/drawer.tsx +0 -139
- package/templates/latest/app/components/ui/select.tsx +0 -183
- package/templates/latest/app/components/ui/table.tsx +0 -117
- package/templates/latest/app/components/ui/toggle-group.tsx +0 -73
- package/templates/latest/app/components/ui/toggle.tsx +0 -47
- package/templates/latest/app/data/data.json +0 -614
- package/templates/latest/app/data/mock-payments.json +0 -122
- package/templates/latest/app/data/mock-transactions.json +0 -128
- package/templates/latest/app/hooks/use-bluecopa-user.ts +0 -37
- package/templates/latest/app/lib/utils.ts +0 -6
- package/templates/latest/app/routes/apitest.tsx +0 -2118
- package/templates/latest/app/routes/comments.tsx +0 -588
- package/templates/latest/app/routes/dashboard.tsx +0 -36
- package/templates/latest/app/routes/payments.tsx +0 -342
- package/templates/latest/app/routes/statements.tsx +0 -493
- package/templates/latest/app/routes/websocket.tsx +0 -450
- package/templates/latest/app/routes.tsx +0 -22
- package/templates/latest/dist/assets/__federation_expose_App-OFfdinOR.js +0 -97
- package/templates/latest/dist/assets/__federation_fn_import-CzfA7kmP.js +0 -438
- package/templates/latest/dist/assets/__federation_shared_react-Bp6HVBS4.js +0 -16
- package/templates/latest/dist/assets/__federation_shared_react-dom-BCcRGiYp.js +0 -17
- package/templates/latest/dist/assets/client-CkHcT_xc.js +0 -76035
- package/templates/latest/dist/assets/index-B3cD3sP_.js +0 -60
- package/templates/latest/dist/assets/index-BzNimew1.js +0 -69
- package/templates/latest/dist/assets/index-DMFtQdNS.js +0 -412
- package/templates/latest/dist/assets/remoteEntry.css +0 -3996
- package/templates/latest/dist/assets/remoteEntry.js +0 -88
- package/templates/latest/dist/favicon.ico +0 -0
- package/templates/latest/dist/index.html +0 -19
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IconArrowDownRight,
|
|
3
|
+
IconArrowUpRight,
|
|
4
|
+
IconCreditCard,
|
|
5
|
+
IconReceipt,
|
|
6
|
+
IconTransfer,
|
|
7
|
+
IconUsers,
|
|
8
|
+
} from "@tabler/icons-react";
|
|
9
|
+
|
|
10
|
+
import { toast } from "sonner";
|
|
11
|
+
import {
|
|
12
|
+
Card,
|
|
13
|
+
CardContent,
|
|
14
|
+
CardDescription,
|
|
15
|
+
CardHeader,
|
|
16
|
+
CardTitle,
|
|
17
|
+
} from "~/components/ui/card";
|
|
18
|
+
import { Button } from "~/components/ui/button";
|
|
19
|
+
import { BarChart } from "~/components/charts/bar-chart";
|
|
20
|
+
import { DonutChart } from "~/components/charts/donut-chart";
|
|
21
|
+
|
|
22
|
+
/* ─── KPI Data ─── */
|
|
23
|
+
|
|
24
|
+
const kpis = [
|
|
25
|
+
{
|
|
26
|
+
title: "Total Revenue",
|
|
27
|
+
value: "$45,231.89",
|
|
28
|
+
change: "+20.1%",
|
|
29
|
+
trend: "up" as const,
|
|
30
|
+
icon: IconCreditCard,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
title: "Transactions",
|
|
34
|
+
value: "2,350",
|
|
35
|
+
change: "+18.2%",
|
|
36
|
+
trend: "up" as const,
|
|
37
|
+
icon: IconTransfer,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
title: "Outstanding Invoices",
|
|
41
|
+
value: "$12,234.00",
|
|
42
|
+
change: "-4.5%",
|
|
43
|
+
trend: "down" as const,
|
|
44
|
+
icon: IconReceipt,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
title: "Active Customers",
|
|
48
|
+
value: "573",
|
|
49
|
+
change: "+12.3%",
|
|
50
|
+
trend: "up" as const,
|
|
51
|
+
icon: IconUsers,
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/* ─── Chart Data ─── */
|
|
56
|
+
|
|
57
|
+
const revenueData = {
|
|
58
|
+
categories: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"],
|
|
59
|
+
series: [
|
|
60
|
+
{
|
|
61
|
+
name: "Revenue",
|
|
62
|
+
data: [4200, 3800, 5100, 4600, 5800, 6200, 5900, 7100],
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const expensesData = {
|
|
68
|
+
items: [
|
|
69
|
+
{ name: "Payroll", value: 42 },
|
|
70
|
+
{ name: "Operations", value: 28 },
|
|
71
|
+
{ name: "Marketing", value: 16 },
|
|
72
|
+
{ name: "Software", value: 9 },
|
|
73
|
+
{ name: "Other", value: 5 },
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/* ─── Recent Transactions ─── */
|
|
78
|
+
|
|
79
|
+
const recentTransactions = [
|
|
80
|
+
{ id: "TXN-001", customer: "Acme Corp", amount: "$1,250.00", status: "Completed", date: "Feb 15, 2026" },
|
|
81
|
+
{ id: "TXN-002", customer: "Globex Inc", amount: "$3,420.00", status: "Pending", date: "Feb 14, 2026" },
|
|
82
|
+
{ id: "TXN-003", customer: "Initech LLC", amount: "$890.50", status: "Completed", date: "Feb 14, 2026" },
|
|
83
|
+
{ id: "TXN-004", customer: "Umbrella Co", amount: "$2,100.00", status: "Failed", date: "Feb 13, 2026" },
|
|
84
|
+
{ id: "TXN-005", customer: "Stark Industries", amount: "$5,670.00", status: "Completed", date: "Feb 13, 2026" },
|
|
85
|
+
{ id: "TXN-006", customer: "Wayne Enterprises", amount: "$4,320.00", status: "Pending", date: "Feb 12, 2026" },
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
/* ─── Components ─── */
|
|
89
|
+
|
|
90
|
+
function KpiCard({ title, value, change, trend, icon: Icon }: (typeof kpis)[number]) {
|
|
91
|
+
return (
|
|
92
|
+
<Card>
|
|
93
|
+
<CardContent className="copa:pt-6">
|
|
94
|
+
<div className="copa:flex copa:items-center copa:justify-between">
|
|
95
|
+
<div className="copa:flex copa:flex-col copa:gap-1">
|
|
96
|
+
<span className="copa:text-xs copa:font-medium copa:text-muted-foreground copa:uppercase copa:tracking-wide">
|
|
97
|
+
{title}
|
|
98
|
+
</span>
|
|
99
|
+
<span className="copa:text-2xl copa:font-bold">{value}</span>
|
|
100
|
+
<div className="copa:flex copa:items-center copa:gap-1 copa:text-xs">
|
|
101
|
+
{trend === "up" ? (
|
|
102
|
+
<IconArrowUpRight className="copa:size-3.5 copa:text-green-600" />
|
|
103
|
+
) : (
|
|
104
|
+
<IconArrowDownRight className="copa:size-3.5 copa:text-red-500" />
|
|
105
|
+
)}
|
|
106
|
+
<span className={trend === "up" ? "copa:text-green-600" : "copa:text-red-500"}>
|
|
107
|
+
{change}
|
|
108
|
+
</span>
|
|
109
|
+
<span className="copa:text-muted-foreground">vs last month</span>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
<div className="copa:flex copa:size-10 copa:items-center copa:justify-center copa:rounded-lg copa:bg-primary/10">
|
|
113
|
+
<Icon className="copa:size-5 copa:text-primary" />
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</CardContent>
|
|
117
|
+
</Card>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function StatusBadge({ status }: { status: string }) {
|
|
122
|
+
const styles: Record<string, string> = {
|
|
123
|
+
Completed: "copa:bg-green-100 copa:text-green-700",
|
|
124
|
+
Pending: "copa:bg-amber-100 copa:text-amber-700",
|
|
125
|
+
Failed: "copa:bg-red-100 copa:text-red-700",
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<span className={`copa:inline-flex copa:items-center copa:rounded-full copa:px-2 copa:py-0.5 copa:text-xs copa:font-medium ${styles[status] ?? "copa:bg-muted copa:text-muted-foreground"}`}>
|
|
130
|
+
{status}
|
|
131
|
+
</span>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function TransactionsTable() {
|
|
136
|
+
return (
|
|
137
|
+
<Card>
|
|
138
|
+
<CardHeader>
|
|
139
|
+
<CardTitle>Recent Transactions</CardTitle>
|
|
140
|
+
<CardDescription>Latest payment activity across your workspace.</CardDescription>
|
|
141
|
+
</CardHeader>
|
|
142
|
+
<CardContent className="copa:p-0">
|
|
143
|
+
<div className="copa:overflow-auto">
|
|
144
|
+
<table className="copa:w-full copa:text-sm">
|
|
145
|
+
<thead>
|
|
146
|
+
<tr className="copa:border-b copa:border-border">
|
|
147
|
+
<th className="copa:px-6 copa:py-3 copa:text-left copa:text-xs copa:font-medium copa:text-muted-foreground copa:uppercase copa:tracking-wide">
|
|
148
|
+
ID
|
|
149
|
+
</th>
|
|
150
|
+
<th className="copa:px-6 copa:py-3 copa:text-left copa:text-xs copa:font-medium copa:text-muted-foreground copa:uppercase copa:tracking-wide">
|
|
151
|
+
Customer
|
|
152
|
+
</th>
|
|
153
|
+
<th className="copa:px-6 copa:py-3 copa:text-right copa:text-xs copa:font-medium copa:text-muted-foreground copa:uppercase copa:tracking-wide">
|
|
154
|
+
Amount
|
|
155
|
+
</th>
|
|
156
|
+
<th className="copa:px-6 copa:py-3 copa:text-left copa:text-xs copa:font-medium copa:text-muted-foreground copa:uppercase copa:tracking-wide">
|
|
157
|
+
Status
|
|
158
|
+
</th>
|
|
159
|
+
<th className="copa:px-6 copa:py-3 copa:text-left copa:text-xs copa:font-medium copa:text-muted-foreground copa:uppercase copa:tracking-wide">
|
|
160
|
+
Date
|
|
161
|
+
</th>
|
|
162
|
+
</tr>
|
|
163
|
+
</thead>
|
|
164
|
+
<tbody>
|
|
165
|
+
{recentTransactions.map((txn) => (
|
|
166
|
+
<tr
|
|
167
|
+
key={txn.id}
|
|
168
|
+
className="copa:border-b copa:border-border/50 last:copa:border-0 copa:hover:bg-muted/50 copa:transition-colors"
|
|
169
|
+
>
|
|
170
|
+
<td className="copa:px-6 copa:py-3 copa:font-medium">{txn.id}</td>
|
|
171
|
+
<td className="copa:px-6 copa:py-3">{txn.customer}</td>
|
|
172
|
+
<td className="copa:px-6 copa:py-3 copa:text-right copa:font-medium">{txn.amount}</td>
|
|
173
|
+
<td className="copa:px-6 copa:py-3">
|
|
174
|
+
<StatusBadge status={txn.status} />
|
|
175
|
+
</td>
|
|
176
|
+
<td className="copa:px-6 copa:py-3 copa:text-muted-foreground">{txn.date}</td>
|
|
177
|
+
</tr>
|
|
178
|
+
))}
|
|
179
|
+
</tbody>
|
|
180
|
+
</table>
|
|
181
|
+
</div>
|
|
182
|
+
</CardContent>
|
|
183
|
+
</Card>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* ─── Page ─── */
|
|
188
|
+
|
|
189
|
+
export default function Dashboard() {
|
|
190
|
+
return (
|
|
191
|
+
<>
|
|
192
|
+
<div className="copa:flex copa:items-center copa:justify-between">
|
|
193
|
+
<h1 className="copa:text-2xl copa:font-semibold">Dashboard</h1>
|
|
194
|
+
<Button
|
|
195
|
+
onClick={() =>
|
|
196
|
+
toast.success("Event has been created", {
|
|
197
|
+
description: "Sunday, February 17, 2026 at 5:30 PM",
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
>
|
|
201
|
+
Show Toast
|
|
202
|
+
</Button>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<div className="copa:grid copa:gap-4 copa:sm:grid-cols-2 copa:lg:grid-cols-4">
|
|
206
|
+
{kpis.map((kpi) => (
|
|
207
|
+
<KpiCard key={kpi.title} {...kpi} />
|
|
208
|
+
))}
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div className="copa:grid copa:gap-4 copa:lg:grid-cols-2">
|
|
212
|
+
<Card>
|
|
213
|
+
<CardHeader>
|
|
214
|
+
<CardTitle>Monthly Revenue</CardTitle>
|
|
215
|
+
<CardDescription>Revenue trend over the last 8 months.</CardDescription>
|
|
216
|
+
</CardHeader>
|
|
217
|
+
<CardContent>
|
|
218
|
+
<BarChart
|
|
219
|
+
data={revenueData}
|
|
220
|
+
height={280}
|
|
221
|
+
formatValue={(v) => `$${(v / 1000).toFixed(1)}K`}
|
|
222
|
+
/>
|
|
223
|
+
</CardContent>
|
|
224
|
+
</Card>
|
|
225
|
+
|
|
226
|
+
<Card>
|
|
227
|
+
<CardHeader>
|
|
228
|
+
<CardTitle>Expenses Breakdown</CardTitle>
|
|
229
|
+
<CardDescription>Spending distribution by category.</CardDescription>
|
|
230
|
+
</CardHeader>
|
|
231
|
+
<CardContent>
|
|
232
|
+
<DonutChart
|
|
233
|
+
data={expensesData}
|
|
234
|
+
height={280}
|
|
235
|
+
centerText="100%"
|
|
236
|
+
centerSubText="Total"
|
|
237
|
+
showLabels={false}
|
|
238
|
+
/>
|
|
239
|
+
</CardContent>
|
|
240
|
+
</Card>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<TransactionsTable />
|
|
244
|
+
</>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { useMemo, useCallback, useState } from "react";
|
|
2
|
+
import { useDataset } from "@bluecopa/react";
|
|
3
|
+
import { IconAlertTriangle, IconLoader2, IconSearch } from "@tabler/icons-react";
|
|
4
|
+
import type { ColDef, GridReadyEvent } from "ag-grid-community";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
Card,
|
|
8
|
+
CardContent,
|
|
9
|
+
CardDescription,
|
|
10
|
+
CardHeader,
|
|
11
|
+
CardTitle,
|
|
12
|
+
} from "~/components/ui/card";
|
|
13
|
+
import { AgGridTable } from "~/components/ui/ag-grid-table";
|
|
14
|
+
import { createServerSideDatasource } from "~/utils/ag-grid-datasource";
|
|
15
|
+
import { Input } from "~/components/ui/input";
|
|
16
|
+
|
|
17
|
+
const DATASET_ID = "0a3be3311769d7e9edb830adf72e41d9";
|
|
18
|
+
const USE_SERVER_SIDE = false;
|
|
19
|
+
|
|
20
|
+
type PaymentRow = Record<string, unknown>;
|
|
21
|
+
|
|
22
|
+
function parseResponse(data: unknown): {
|
|
23
|
+
rows: PaymentRow[];
|
|
24
|
+
columns: string[];
|
|
25
|
+
} {
|
|
26
|
+
const response = data as Record<string, unknown> | unknown[] | undefined;
|
|
27
|
+
let rows: PaymentRow[] = [];
|
|
28
|
+
let columns: string[] = [];
|
|
29
|
+
|
|
30
|
+
if (Array.isArray(response)) {
|
|
31
|
+
rows = response as PaymentRow[];
|
|
32
|
+
} else if (response && typeof response === "object") {
|
|
33
|
+
if ("data" in response && Array.isArray(response.data)) {
|
|
34
|
+
rows = response.data as PaymentRow[];
|
|
35
|
+
} else if ("records" in response && Array.isArray(response.records)) {
|
|
36
|
+
rows = response.records as PaymentRow[];
|
|
37
|
+
} else if ("rows" in response && Array.isArray(response.rows)) {
|
|
38
|
+
rows = response.rows as PaymentRow[];
|
|
39
|
+
}
|
|
40
|
+
if ("columns" in response && Array.isArray(response.columns)) {
|
|
41
|
+
columns = response.columns as string[];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (columns.length === 0 && rows.length > 0) {
|
|
46
|
+
columns = Object.keys(rows[0]).filter(
|
|
47
|
+
(key) => !key.startsWith("_") && key !== "id",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { rows, columns };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default function Payments() {
|
|
55
|
+
const [quickFilter, setQuickFilter] = useState("");
|
|
56
|
+
|
|
57
|
+
const { data, isLoading, error } = useDataset(
|
|
58
|
+
USE_SERVER_SIDE ? undefined! : DATASET_ID,
|
|
59
|
+
{ limit: 100 },
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const { rows, columns } = useMemo(() => parseResponse(data), [data]);
|
|
63
|
+
|
|
64
|
+
const onGridReady = useCallback((event: GridReadyEvent) => {
|
|
65
|
+
if (!USE_SERVER_SIDE) return;
|
|
66
|
+
const datasource = createServerSideDatasource({ tableId: DATASET_ID });
|
|
67
|
+
event.api.setGridOption("serverSideDatasource", datasource);
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const columnDefs: ColDef[] = useMemo(
|
|
71
|
+
() =>
|
|
72
|
+
columns.map((col) => ({
|
|
73
|
+
field: col,
|
|
74
|
+
headerName: col
|
|
75
|
+
.replace(/([A-Z])/g, " $1")
|
|
76
|
+
.replace(/_/g, " ")
|
|
77
|
+
.trim(),
|
|
78
|
+
})),
|
|
79
|
+
[columns],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (isLoading) {
|
|
83
|
+
return (
|
|
84
|
+
<>
|
|
85
|
+
<h1 className="copa:text-2xl copa:font-semibold">Payments</h1>
|
|
86
|
+
<Card className="copa:max-w-md">
|
|
87
|
+
<CardContent className="copa:flex copa:items-center copa:gap-3 copa:py-12 copa:justify-center">
|
|
88
|
+
<IconLoader2 className="copa:size-5 copa:animate-spin copa:text-primary" />
|
|
89
|
+
<span className="copa:text-sm copa:text-muted-foreground">
|
|
90
|
+
Loading payments data…
|
|
91
|
+
</span>
|
|
92
|
+
</CardContent>
|
|
93
|
+
</Card>
|
|
94
|
+
</>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (error) {
|
|
99
|
+
return (
|
|
100
|
+
<>
|
|
101
|
+
<h1 className="copa:text-2xl copa:font-semibold">Payments</h1>
|
|
102
|
+
<Card className="copa:max-w-lg">
|
|
103
|
+
<CardContent className="copa:flex copa:items-start copa:gap-3 copa:py-8">
|
|
104
|
+
<IconAlertTriangle className="copa:size-5 copa:text-destructive copa:shrink-0 copa:mt-0.5" />
|
|
105
|
+
<div className="copa:flex copa:flex-col copa:gap-1">
|
|
106
|
+
<span className="copa:text-sm copa:font-medium">
|
|
107
|
+
Failed to load payments
|
|
108
|
+
</span>
|
|
109
|
+
<span className="copa:text-xs copa:text-muted-foreground">
|
|
110
|
+
{error.message}
|
|
111
|
+
</span>
|
|
112
|
+
<span className="copa:text-xs copa:text-muted-foreground copa:mt-1">
|
|
113
|
+
Dataset ID: {DATASET_ID}
|
|
114
|
+
</span>
|
|
115
|
+
</div>
|
|
116
|
+
</CardContent>
|
|
117
|
+
</Card>
|
|
118
|
+
</>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<>
|
|
124
|
+
<div className="copa:flex copa:items-center copa:justify-between">
|
|
125
|
+
<h1 className="copa:text-2xl copa:font-semibold">Payments</h1>
|
|
126
|
+
<span className="copa:text-xs copa:text-muted-foreground copa:bg-muted copa:rounded-md copa:px-2 copa:py-1">
|
|
127
|
+
{USE_SERVER_SIDE ? "Server-side" : `${rows.length} records`}
|
|
128
|
+
</span>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<Card>
|
|
132
|
+
<CardHeader>
|
|
133
|
+
<CardTitle>Payment Records</CardTitle>
|
|
134
|
+
<CardDescription>
|
|
135
|
+
Live data from dataset {DATASET_ID.slice(0, 8)}…
|
|
136
|
+
</CardDescription>
|
|
137
|
+
</CardHeader>
|
|
138
|
+
<CardContent className="copa:px-4 copa:pb-4 copa:pt-0">
|
|
139
|
+
<AgGridTable
|
|
140
|
+
columnDefs={columnDefs}
|
|
141
|
+
{...(!USE_SERVER_SIDE ? { rowData: rows } : {})}
|
|
142
|
+
height={600}
|
|
143
|
+
quickFilterText={quickFilter}
|
|
144
|
+
header={
|
|
145
|
+
<div className="copa:px-4 copa:pt-2">
|
|
146
|
+
<div className="copa:relative copa:max-w-xs">
|
|
147
|
+
<IconSearch className="copa:absolute copa:left-2.5 copa:top-2.5 copa:size-4 copa:text-muted-foreground" />
|
|
148
|
+
<Input
|
|
149
|
+
placeholder="Search payments…"
|
|
150
|
+
value={quickFilter}
|
|
151
|
+
onChange={(e) => setQuickFilter(e.target.value)}
|
|
152
|
+
className="copa:pl-9 copa:h-9"
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
}
|
|
157
|
+
gridOptions={{
|
|
158
|
+
...(USE_SERVER_SIDE
|
|
159
|
+
? { rowModelType: "serverSide" as const }
|
|
160
|
+
: {}),
|
|
161
|
+
pagination: true,
|
|
162
|
+
paginationPageSize: 50,
|
|
163
|
+
paginationPageSizeSelector: [20, 50, 100],
|
|
164
|
+
...(USE_SERVER_SIDE
|
|
165
|
+
? { cacheBlockSize: 50, maxBlocksInCache: 5 }
|
|
166
|
+
: {}),
|
|
167
|
+
rowSelection: { mode: "multiRow" },
|
|
168
|
+
onGridReady,
|
|
169
|
+
defaultColDef: {
|
|
170
|
+
sortable: true,
|
|
171
|
+
filter: true,
|
|
172
|
+
resizable: true,
|
|
173
|
+
minWidth: 100,
|
|
174
|
+
flex: 1,
|
|
175
|
+
},
|
|
176
|
+
}}
|
|
177
|
+
/>
|
|
178
|
+
</CardContent>
|
|
179
|
+
</Card>
|
|
180
|
+
</>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Card,
|
|
3
|
+
CardContent,
|
|
4
|
+
CardDescription,
|
|
5
|
+
CardHeader,
|
|
6
|
+
CardTitle,
|
|
7
|
+
} from "~/components/ui/card";
|
|
8
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
|
|
9
|
+
import { useAppContext } from "~/contexts/app-context";
|
|
10
|
+
|
|
11
|
+
function SettingRow({
|
|
12
|
+
label,
|
|
13
|
+
value,
|
|
14
|
+
}: {
|
|
15
|
+
label: string;
|
|
16
|
+
value: React.ReactNode;
|
|
17
|
+
}) {
|
|
18
|
+
return (
|
|
19
|
+
<div className="copa:flex copa:items-center copa:justify-between copa:py-3 copa:border-b copa:border-border/50 last:copa:border-0">
|
|
20
|
+
<span className="copa:text-sm copa:text-muted-foreground">{label}</span>
|
|
21
|
+
<span className="copa:text-sm copa:font-medium">{value}</span>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function CurrencyTab() {
|
|
27
|
+
const { currentWorkspaceSettings } = useAppContext();
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="copa:grid copa:gap-4 copa:max-w-2xl">
|
|
31
|
+
<Card>
|
|
32
|
+
<CardHeader>
|
|
33
|
+
<CardTitle>Currency</CardTitle>
|
|
34
|
+
<CardDescription>
|
|
35
|
+
Default currency used across reports and transactions.
|
|
36
|
+
</CardDescription>
|
|
37
|
+
</CardHeader>
|
|
38
|
+
<CardContent>
|
|
39
|
+
<SettingRow label="Base currency" value={currentWorkspaceSettings.currency} />
|
|
40
|
+
<SettingRow label="Display format" value="1,234.56" />
|
|
41
|
+
<SettingRow label="Currency position" value="Before amount" />
|
|
42
|
+
<SettingRow label="Decimal places" value="2" />
|
|
43
|
+
</CardContent>
|
|
44
|
+
</Card>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function NumberSettingsTab() {
|
|
50
|
+
const { currentWorkspaceSettings } = useAppContext();
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="copa:grid copa:gap-4 copa:max-w-2xl">
|
|
54
|
+
<Card>
|
|
55
|
+
<CardHeader>
|
|
56
|
+
<CardTitle>Number Format</CardTitle>
|
|
57
|
+
<CardDescription>
|
|
58
|
+
How numbers, dates, and percentages are displayed.
|
|
59
|
+
</CardDescription>
|
|
60
|
+
</CardHeader>
|
|
61
|
+
<CardContent>
|
|
62
|
+
<SettingRow label="Thousands separator" value="Comma (1,000)" />
|
|
63
|
+
<SettingRow label="Decimal separator" value="Period (0.00)" />
|
|
64
|
+
<SettingRow label="Date format" value={currentWorkspaceSettings.dateFormat} />
|
|
65
|
+
<SettingRow label="Timezone" value={currentWorkspaceSettings.timezone} />
|
|
66
|
+
<SettingRow label="Percentage format" value="45.2%" />
|
|
67
|
+
<SettingRow label="Negative numbers" value="(1,234.56)" />
|
|
68
|
+
</CardContent>
|
|
69
|
+
</Card>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function ChartThemeTab() {
|
|
75
|
+
return (
|
|
76
|
+
<div className="copa:grid copa:gap-4 copa:max-w-2xl">
|
|
77
|
+
<Card>
|
|
78
|
+
<CardHeader>
|
|
79
|
+
<CardTitle>Chart Theme</CardTitle>
|
|
80
|
+
<CardDescription>
|
|
81
|
+
Visual appearance for charts and data visualizations.
|
|
82
|
+
</CardDescription>
|
|
83
|
+
</CardHeader>
|
|
84
|
+
<CardContent>
|
|
85
|
+
<SettingRow label="Color palette" value="Bluecopa Default" />
|
|
86
|
+
<SettingRow label="Chart font" value="Satoshi" />
|
|
87
|
+
<SettingRow label="Grid lines" value="Subtle" />
|
|
88
|
+
<SettingRow label="Tooltip style" value="Floating card" />
|
|
89
|
+
<SettingRow label="Animation" value="Enabled" />
|
|
90
|
+
<SettingRow label="Legend position" value="Bottom" />
|
|
91
|
+
</CardContent>
|
|
92
|
+
</Card>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default function Settings() {
|
|
98
|
+
const { user, isAdmin } = useAppContext();
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<>
|
|
102
|
+
<div className="copa:flex copa:items-center copa:justify-between">
|
|
103
|
+
<h1 className="copa:text-2xl copa:font-semibold">Settings</h1>
|
|
104
|
+
<span className="copa:text-xs copa:text-muted-foreground copa:bg-muted copa:rounded-md copa:px-2 copa:py-1">
|
|
105
|
+
{isAdmin ? "Admin" : "Member"} · {user?.email}
|
|
106
|
+
</span>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<Tabs defaultValue="currency">
|
|
110
|
+
<TabsList>
|
|
111
|
+
<TabsTrigger value="currency">Currency</TabsTrigger>
|
|
112
|
+
<TabsTrigger value="numbers">Number Settings</TabsTrigger>
|
|
113
|
+
<TabsTrigger value="charts">Chart Theme</TabsTrigger>
|
|
114
|
+
</TabsList>
|
|
115
|
+
|
|
116
|
+
<TabsContent value="currency">
|
|
117
|
+
<CurrencyTab />
|
|
118
|
+
</TabsContent>
|
|
119
|
+
<TabsContent value="numbers">
|
|
120
|
+
<NumberSettingsTab />
|
|
121
|
+
</TabsContent>
|
|
122
|
+
<TabsContent value="charts">
|
|
123
|
+
<ChartThemeTab />
|
|
124
|
+
</TabsContent>
|
|
125
|
+
</Tabs>
|
|
126
|
+
</>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Routes, Route, Navigate } from "react-router-dom";
|
|
2
|
+
import { AppLayout } from "~/components/layouts/app-layout";
|
|
3
|
+
import Dashboard from "~/pages/dashboard";
|
|
4
|
+
import Payments from "~/pages/payments";
|
|
5
|
+
import Settings from "~/pages/settings";
|
|
6
|
+
import { ROUTES } from "~/constants";
|
|
7
|
+
|
|
8
|
+
export default function RouteConfig() {
|
|
9
|
+
return (
|
|
10
|
+
<Routes>
|
|
11
|
+
<Route element={<AppLayout />}>
|
|
12
|
+
<Route path={ROUTES.DASHBOARD} element={<Dashboard />} />
|
|
13
|
+
<Route path={ROUTES.PAYMENTS} element={<Payments />} />
|
|
14
|
+
<Route path={ROUTES.SETTINGS} element={<Settings />} />
|
|
15
|
+
</Route>
|
|
16
|
+
<Route path="*" element={<Navigate to={ROUTES.DASHBOARD} replace />} />
|
|
17
|
+
</Routes>
|
|
18
|
+
);
|
|
19
|
+
}
|