@djangocfg/layouts 1.0.4 → 1.0.5
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/package.json +5 -5
- package/src/layouts/PaymentsLayout/PaymentsLayout.tsx +47 -65
- package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +118 -163
- package/src/layouts/PaymentsLayout/components/PaymentDetailsDialog.tsx +31 -45
- package/src/layouts/PaymentsLayout/components/index.ts +1 -4
- package/src/layouts/PaymentsLayout/events.ts +23 -84
- package/src/layouts/PaymentsLayout/index.ts +7 -11
- package/src/layouts/PaymentsLayout/types.ts +3 -16
- package/src/layouts/PaymentsLayout/views/overview/components/BalanceCard.tsx +45 -16
- package/src/layouts/PaymentsLayout/views/overview/components/RecentPayments.tsx +16 -12
- package/src/layouts/PaymentsLayout/views/overview/components/index.ts +0 -2
- package/src/layouts/PaymentsLayout/views/overview/index.tsx +3 -6
- package/src/layouts/PaymentsLayout/views/payments/components/PaymentsList.tsx +50 -30
- package/src/layouts/PaymentsLayout/views/payments/components/index.ts +0 -1
- package/src/layouts/PaymentsLayout/views/payments/index.tsx +1 -2
- package/src/layouts/PaymentsLayout/views/transactions/components/TransactionsList.tsx +273 -0
- package/src/layouts/PaymentsLayout/views/transactions/components/index.ts +1 -0
- package/src/layouts/PaymentsLayout/views/transactions/index.tsx +5 -17
- package/src/layouts/PaymentsLayout/README.md +0 -133
- package/src/layouts/PaymentsLayout/components/CreateApiKeyDialog.tsx +0 -172
- package/src/layouts/PaymentsLayout/components/DeleteApiKeyDialog.tsx +0 -100
- package/src/layouts/PaymentsLayout/context/RootPaymentsContext.tsx +0 -129
- package/src/layouts/PaymentsLayout/views/apikeys/components/ApiKeyMetrics.tsx +0 -109
- package/src/layouts/PaymentsLayout/views/apikeys/components/ApiKeysList.tsx +0 -194
- package/src/layouts/PaymentsLayout/views/apikeys/components/index.ts +0 -3
- package/src/layouts/PaymentsLayout/views/apikeys/index.tsx +0 -19
- package/src/layouts/PaymentsLayout/views/overview/components/MetricsCards.tsx +0 -103
- package/src/layouts/PaymentsLayout/views/tariffs/index.tsx +0 -29
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Payments List Component
|
|
2
|
+
* Payments List Component (v2.0 - Simplified)
|
|
3
3
|
* Display paginated list of payments with filters
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -29,8 +29,7 @@ import {
|
|
|
29
29
|
} from '@djangocfg/ui';
|
|
30
30
|
import { Plus, Search, Filter, ChevronLeft, ChevronRight, RefreshCw, ExternalLink } from 'lucide-react';
|
|
31
31
|
import { usePaymentsContext } from '@djangocfg/api/cfg/contexts';
|
|
32
|
-
import { openCreatePaymentDialog } from '../../../events';
|
|
33
|
-
import { openPaymentDetails } from '../../../components/PaymentDetailsDialog';
|
|
32
|
+
import { openCreatePaymentDialog, openPaymentDetailsDialog } from '../../../events';
|
|
34
33
|
|
|
35
34
|
export const PaymentsList: React.FC = () => {
|
|
36
35
|
const {
|
|
@@ -38,22 +37,23 @@ export const PaymentsList: React.FC = () => {
|
|
|
38
37
|
isLoadingPayments,
|
|
39
38
|
refreshPayments,
|
|
40
39
|
} = usePaymentsContext();
|
|
41
|
-
|
|
40
|
+
|
|
42
41
|
const paymentsList = payments?.results || [];
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
42
|
+
const currentPage = payments?.page || 1;
|
|
43
|
+
const pageSize = payments?.page_size || 20;
|
|
44
|
+
const totalCount = payments?.count || 0;
|
|
46
45
|
|
|
47
46
|
const [searchTerm, setSearchTerm] = useState('');
|
|
48
47
|
const [statusFilter, setStatusFilter] = useState<string>('all');
|
|
49
48
|
|
|
50
|
-
const formatCurrency = (amount?: number | null) => {
|
|
49
|
+
const formatCurrency = (amount?: number | string | null) => {
|
|
51
50
|
if (amount === null || amount === undefined) return '$0.00';
|
|
51
|
+
const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
|
|
52
52
|
return new Intl.NumberFormat('en-US', {
|
|
53
53
|
style: 'currency',
|
|
54
54
|
currency: 'USD',
|
|
55
55
|
minimumFractionDigits: 2,
|
|
56
|
-
}).format(
|
|
56
|
+
}).format(numAmount);
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
const getRelativeTime = (date: string | null | undefined): string => {
|
|
@@ -77,9 +77,11 @@ export const PaymentsList: React.FC = () => {
|
|
|
77
77
|
case 'success':
|
|
78
78
|
return 'default';
|
|
79
79
|
case 'pending':
|
|
80
|
+
case 'confirming':
|
|
80
81
|
return 'secondary';
|
|
81
82
|
case 'failed':
|
|
82
83
|
case 'error':
|
|
84
|
+
case 'expired':
|
|
83
85
|
return 'destructive';
|
|
84
86
|
default:
|
|
85
87
|
return 'outline';
|
|
@@ -88,13 +90,13 @@ export const PaymentsList: React.FC = () => {
|
|
|
88
90
|
|
|
89
91
|
const handleSearch = async (value: string) => {
|
|
90
92
|
setSearchTerm(value);
|
|
91
|
-
// TODO: Implement search/filter in PaymentsContext
|
|
93
|
+
// TODO: Implement search/filter in PaymentsContext when API supports it
|
|
92
94
|
await refreshPayments();
|
|
93
95
|
};
|
|
94
96
|
|
|
95
97
|
const handleStatusFilter = async (status: string) => {
|
|
96
98
|
setStatusFilter(status);
|
|
97
|
-
// TODO: Implement
|
|
99
|
+
// TODO: Implement status filter in PaymentsContext when API supports it
|
|
98
100
|
await refreshPayments();
|
|
99
101
|
};
|
|
100
102
|
|
|
@@ -103,9 +105,24 @@ export const PaymentsList: React.FC = () => {
|
|
|
103
105
|
await refreshPayments();
|
|
104
106
|
};
|
|
105
107
|
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
108
|
+
// Filter payments client-side for now (until API supports filtering)
|
|
109
|
+
const filteredPayments = paymentsList.filter((payment) => {
|
|
110
|
+
const matchesSearch = searchTerm
|
|
111
|
+
? payment.id?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
112
|
+
payment.status?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
113
|
+
payment.currency_code?.toLowerCase().includes(searchTerm.toLowerCase())
|
|
114
|
+
: true;
|
|
115
|
+
|
|
116
|
+
const matchesStatus = statusFilter !== 'all'
|
|
117
|
+
? payment.status?.toLowerCase() === statusFilter.toLowerCase()
|
|
118
|
+
: true;
|
|
119
|
+
|
|
120
|
+
return matchesSearch && matchesStatus;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const totalPages = Math.ceil((totalCount || 0) / (pageSize || 20));
|
|
124
|
+
const showingFrom = ((currentPage || 1) - 1) * (pageSize || 20) + 1;
|
|
125
|
+
const showingTo = Math.min((currentPage || 1) * (pageSize || 20), totalCount || 0);
|
|
109
126
|
|
|
110
127
|
return (
|
|
111
128
|
<Card>
|
|
@@ -131,7 +148,7 @@ export const PaymentsList: React.FC = () => {
|
|
|
131
148
|
<div className="relative flex-1">
|
|
132
149
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
133
150
|
<Input
|
|
134
|
-
placeholder="Search
|
|
151
|
+
placeholder="Search by ID, status, or currency..."
|
|
135
152
|
value={searchTerm}
|
|
136
153
|
onChange={(e) => handleSearch(e.target.value)}
|
|
137
154
|
className="pl-10"
|
|
@@ -147,7 +164,9 @@ export const PaymentsList: React.FC = () => {
|
|
|
147
164
|
<SelectItem value="all">All Statuses</SelectItem>
|
|
148
165
|
<SelectItem value="completed">Completed</SelectItem>
|
|
149
166
|
<SelectItem value="pending">Pending</SelectItem>
|
|
167
|
+
<SelectItem value="confirming">Confirming</SelectItem>
|
|
150
168
|
<SelectItem value="failed">Failed</SelectItem>
|
|
169
|
+
<SelectItem value="expired">Expired</SelectItem>
|
|
151
170
|
</SelectContent>
|
|
152
171
|
</Select>
|
|
153
172
|
</div>
|
|
@@ -165,7 +184,7 @@ export const PaymentsList: React.FC = () => {
|
|
|
165
184
|
</div>
|
|
166
185
|
))}
|
|
167
186
|
</div>
|
|
168
|
-
) :
|
|
187
|
+
) : filteredPayments.length === 0 ? (
|
|
169
188
|
<div className="text-center py-12">
|
|
170
189
|
<div className="w-16 h-16 mx-auto mb-4 bg-muted rounded-full flex items-center justify-center">
|
|
171
190
|
<Search className="w-8 h-8 text-muted-foreground" />
|
|
@@ -192,16 +211,16 @@ export const PaymentsList: React.FC = () => {
|
|
|
192
211
|
<TableHead>Currency</TableHead>
|
|
193
212
|
<TableHead>Status</TableHead>
|
|
194
213
|
<TableHead>Provider</TableHead>
|
|
195
|
-
<TableHead>ID</TableHead>
|
|
214
|
+
<TableHead>Payment ID</TableHead>
|
|
196
215
|
<TableHead className="text-right">Actions</TableHead>
|
|
197
216
|
</TableRow>
|
|
198
217
|
</TableHeader>
|
|
199
218
|
<TableBody>
|
|
200
|
-
{
|
|
219
|
+
{filteredPayments.map((payment) => (
|
|
201
220
|
<TableRow
|
|
202
221
|
key={payment.id}
|
|
203
222
|
className="cursor-pointer hover:bg-accent"
|
|
204
|
-
onClick={() =>
|
|
223
|
+
onClick={() => openPaymentDetailsDialog(String(payment.id))}
|
|
205
224
|
>
|
|
206
225
|
<TableCell>
|
|
207
226
|
<div>
|
|
@@ -219,14 +238,16 @@ export const PaymentsList: React.FC = () => {
|
|
|
219
238
|
{formatCurrency(payment.amount_usd)}
|
|
220
239
|
</TableCell>
|
|
221
240
|
<TableCell>
|
|
222
|
-
<Badge variant="outline">
|
|
241
|
+
<Badge variant="outline">{payment.currency_code || 'USD'}</Badge>
|
|
223
242
|
</TableCell>
|
|
224
243
|
<TableCell>
|
|
225
244
|
<Badge variant={getStatusVariant(payment.status)}>{payment.status}</Badge>
|
|
226
245
|
</TableCell>
|
|
227
|
-
<TableCell
|
|
246
|
+
<TableCell className="text-sm text-muted-foreground">
|
|
247
|
+
NowPayments
|
|
248
|
+
</TableCell>
|
|
228
249
|
<TableCell className="font-mono text-sm text-muted-foreground">
|
|
229
|
-
{payment.id.slice(0, 8)}
|
|
250
|
+
{payment.id ? `${payment.id.toString().slice(0, 8)}...` : 'N/A'}
|
|
230
251
|
</TableCell>
|
|
231
252
|
<TableCell className="text-right">
|
|
232
253
|
<Button
|
|
@@ -234,7 +255,7 @@ export const PaymentsList: React.FC = () => {
|
|
|
234
255
|
size="sm"
|
|
235
256
|
onClick={(e) => {
|
|
236
257
|
e.stopPropagation();
|
|
237
|
-
|
|
258
|
+
openPaymentDetailsDialog(String(payment.id));
|
|
238
259
|
}}
|
|
239
260
|
>
|
|
240
261
|
<ExternalLink className="h-4 w-4" />
|
|
@@ -250,28 +271,28 @@ export const PaymentsList: React.FC = () => {
|
|
|
250
271
|
{totalPages > 1 && (
|
|
251
272
|
<div className="flex items-center justify-between">
|
|
252
273
|
<div className="text-sm text-muted-foreground">
|
|
253
|
-
Showing {showingFrom} to {showingTo} of {
|
|
274
|
+
Showing {showingFrom} to {showingTo} of {totalCount} payments
|
|
254
275
|
</div>
|
|
255
276
|
|
|
256
277
|
<div className="flex items-center gap-2">
|
|
257
278
|
<Button
|
|
258
279
|
variant="outline"
|
|
259
280
|
size="sm"
|
|
260
|
-
onClick={() => handlePageChange((
|
|
261
|
-
disabled={!
|
|
281
|
+
onClick={() => handlePageChange((currentPage || 1) - 1)}
|
|
282
|
+
disabled={!currentPage || currentPage <= 1}
|
|
262
283
|
>
|
|
263
284
|
<ChevronLeft className="h-4 w-4" />
|
|
264
285
|
</Button>
|
|
265
286
|
|
|
266
287
|
<span className="text-sm">
|
|
267
|
-
Page {
|
|
288
|
+
Page {currentPage || 1} of {totalPages}
|
|
268
289
|
</span>
|
|
269
290
|
|
|
270
291
|
<Button
|
|
271
292
|
variant="outline"
|
|
272
293
|
size="sm"
|
|
273
|
-
onClick={() => handlePageChange((
|
|
274
|
-
disabled={!
|
|
294
|
+
onClick={() => handlePageChange((currentPage || 1) + 1)}
|
|
295
|
+
disabled={!currentPage || currentPage >= totalPages}
|
|
275
296
|
>
|
|
276
297
|
<ChevronRight className="h-4 w-4" />
|
|
277
298
|
</Button>
|
|
@@ -284,4 +305,3 @@ export const PaymentsList: React.FC = () => {
|
|
|
284
305
|
</Card>
|
|
285
306
|
);
|
|
286
307
|
};
|
|
287
|
-
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transactions List Component (v2.0 - Simplified)
|
|
3
|
+
* Display transaction history with balance changes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React, { useState } from 'react';
|
|
9
|
+
import {
|
|
10
|
+
Card,
|
|
11
|
+
CardContent,
|
|
12
|
+
CardHeader,
|
|
13
|
+
CardTitle,
|
|
14
|
+
Table,
|
|
15
|
+
TableBody,
|
|
16
|
+
TableCell,
|
|
17
|
+
TableHead,
|
|
18
|
+
TableHeader,
|
|
19
|
+
TableRow,
|
|
20
|
+
Button,
|
|
21
|
+
Badge,
|
|
22
|
+
Input,
|
|
23
|
+
Select,
|
|
24
|
+
SelectContent,
|
|
25
|
+
SelectItem,
|
|
26
|
+
SelectTrigger,
|
|
27
|
+
SelectValue,
|
|
28
|
+
Skeleton,
|
|
29
|
+
} from '@djangocfg/ui';
|
|
30
|
+
import { History, Search, Filter, RefreshCw, ArrowUpRight, ArrowDownLeft } from 'lucide-react';
|
|
31
|
+
import { useOverviewContext } from '@djangocfg/api/cfg/contexts';
|
|
32
|
+
|
|
33
|
+
export const TransactionsList: React.FC = () => {
|
|
34
|
+
const {
|
|
35
|
+
transactions,
|
|
36
|
+
isLoadingTransactions,
|
|
37
|
+
refreshTransactions,
|
|
38
|
+
} = useOverviewContext();
|
|
39
|
+
|
|
40
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
41
|
+
const [typeFilter, setTypeFilter] = useState<string>('all');
|
|
42
|
+
|
|
43
|
+
// Extract transactions array from response (handle different possible structures)
|
|
44
|
+
const transactionsList = transactions?.results || transactions?.transactions || [];
|
|
45
|
+
|
|
46
|
+
const formatCurrency = (amount?: number | null) => {
|
|
47
|
+
if (amount === null || amount === undefined) return '$0.00';
|
|
48
|
+
return new Intl.NumberFormat('en-US', {
|
|
49
|
+
style: 'currency',
|
|
50
|
+
currency: 'USD',
|
|
51
|
+
minimumFractionDigits: 2,
|
|
52
|
+
}).format(amount);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const formatDate = (date: string | null | undefined): string => {
|
|
56
|
+
if (!date) return 'N/A';
|
|
57
|
+
try {
|
|
58
|
+
return new Date(date).toLocaleString('en-US', {
|
|
59
|
+
year: 'numeric',
|
|
60
|
+
month: 'short',
|
|
61
|
+
day: 'numeric',
|
|
62
|
+
hour: '2-digit',
|
|
63
|
+
minute: '2-digit',
|
|
64
|
+
});
|
|
65
|
+
} catch {
|
|
66
|
+
return 'Invalid date';
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const getRelativeTime = (date: string | null | undefined): string => {
|
|
71
|
+
if (!date) return 'N/A';
|
|
72
|
+
|
|
73
|
+
const now = new Date();
|
|
74
|
+
const target = new Date(date);
|
|
75
|
+
const diffInSeconds = Math.floor((now.getTime() - target.getTime()) / 1000);
|
|
76
|
+
|
|
77
|
+
if (diffInSeconds < 60) return 'Just now';
|
|
78
|
+
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;
|
|
79
|
+
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
|
|
80
|
+
return `${Math.floor(diffInSeconds / 86400)}d ago`;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const getTypeVariant = (
|
|
84
|
+
type: string | null | undefined
|
|
85
|
+
): 'default' | 'destructive' | 'outline' | 'secondary' => {
|
|
86
|
+
switch (type?.toLowerCase()) {
|
|
87
|
+
case 'deposit':
|
|
88
|
+
case 'credit':
|
|
89
|
+
return 'default';
|
|
90
|
+
case 'withdrawal':
|
|
91
|
+
case 'debit':
|
|
92
|
+
return 'destructive';
|
|
93
|
+
default:
|
|
94
|
+
return 'outline';
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const getTypeIcon = (type: string | null | undefined) => {
|
|
99
|
+
const isDeposit = type?.toLowerCase() === 'deposit' || type?.toLowerCase() === 'credit';
|
|
100
|
+
return isDeposit ? (
|
|
101
|
+
<ArrowDownLeft className="h-4 w-4 text-green-600" />
|
|
102
|
+
) : (
|
|
103
|
+
<ArrowUpRight className="h-4 w-4 text-red-600" />
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const handleSearch = async (value: string) => {
|
|
108
|
+
setSearchTerm(value);
|
|
109
|
+
await refreshTransactions();
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const handleTypeFilter = async (type: string) => {
|
|
113
|
+
setTypeFilter(type);
|
|
114
|
+
await refreshTransactions();
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Filter transactions client-side
|
|
118
|
+
const filteredTransactions = transactionsList.filter((transaction: any) => {
|
|
119
|
+
const matchesSearch = searchTerm
|
|
120
|
+
? transaction.id?.toString().toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
121
|
+
transaction.description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
122
|
+
transaction.type?.toLowerCase().includes(searchTerm.toLowerCase())
|
|
123
|
+
: true;
|
|
124
|
+
|
|
125
|
+
const matchesType = typeFilter !== 'all'
|
|
126
|
+
? transaction.type?.toLowerCase() === typeFilter.toLowerCase()
|
|
127
|
+
: true;
|
|
128
|
+
|
|
129
|
+
return matchesSearch && matchesType;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (isLoadingTransactions) {
|
|
133
|
+
return (
|
|
134
|
+
<Card>
|
|
135
|
+
<CardHeader>
|
|
136
|
+
<CardTitle className="flex items-center gap-2">
|
|
137
|
+
<History className="h-5 w-5" />
|
|
138
|
+
Transaction History
|
|
139
|
+
</CardTitle>
|
|
140
|
+
</CardHeader>
|
|
141
|
+
<CardContent className="space-y-3">
|
|
142
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
143
|
+
<div key={i} className="flex items-center justify-between p-4 border rounded-sm">
|
|
144
|
+
<div className="space-y-2">
|
|
145
|
+
<Skeleton className="h-4 w-32" />
|
|
146
|
+
<Skeleton className="h-3 w-24" />
|
|
147
|
+
</div>
|
|
148
|
+
<Skeleton className="h-6 w-16" />
|
|
149
|
+
</div>
|
|
150
|
+
))}
|
|
151
|
+
</CardContent>
|
|
152
|
+
</Card>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<Card>
|
|
158
|
+
<CardHeader>
|
|
159
|
+
<CardTitle className="flex items-center justify-between">
|
|
160
|
+
<div className="flex items-center gap-2">
|
|
161
|
+
<History className="h-5 w-5" />
|
|
162
|
+
Transaction History
|
|
163
|
+
</div>
|
|
164
|
+
<Button variant="outline" size="sm" onClick={refreshTransactions} disabled={isLoadingTransactions}>
|
|
165
|
+
<RefreshCw className={`h-4 w-4 mr-2 ${isLoadingTransactions ? 'animate-spin' : ''}`} />
|
|
166
|
+
Refresh
|
|
167
|
+
</Button>
|
|
168
|
+
</CardTitle>
|
|
169
|
+
</CardHeader>
|
|
170
|
+
|
|
171
|
+
<CardContent className="space-y-4">
|
|
172
|
+
{/* Filters */}
|
|
173
|
+
<div className="flex flex-col sm:flex-row gap-4">
|
|
174
|
+
<div className="relative flex-1">
|
|
175
|
+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
176
|
+
<Input
|
|
177
|
+
placeholder="Search by ID, description, or type..."
|
|
178
|
+
value={searchTerm}
|
|
179
|
+
onChange={(e) => handleSearch(e.target.value)}
|
|
180
|
+
className="pl-10"
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<Select value={typeFilter} onValueChange={handleTypeFilter}>
|
|
185
|
+
<SelectTrigger className="w-full sm:w-48">
|
|
186
|
+
<Filter className="h-4 w-4 mr-2" />
|
|
187
|
+
<SelectValue placeholder="Filter by type" />
|
|
188
|
+
</SelectTrigger>
|
|
189
|
+
<SelectContent>
|
|
190
|
+
<SelectItem value="all">All Types</SelectItem>
|
|
191
|
+
<SelectItem value="deposit">Deposits</SelectItem>
|
|
192
|
+
<SelectItem value="withdrawal">Withdrawals</SelectItem>
|
|
193
|
+
</SelectContent>
|
|
194
|
+
</Select>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
{/* Transactions Table */}
|
|
198
|
+
{filteredTransactions.length === 0 ? (
|
|
199
|
+
<div className="text-center py-12">
|
|
200
|
+
<div className="w-16 h-16 mx-auto mb-4 bg-muted rounded-full flex items-center justify-center">
|
|
201
|
+
<History className="w-8 h-8 text-muted-foreground" />
|
|
202
|
+
</div>
|
|
203
|
+
<h3 className="text-lg font-semibold mb-2">No Transactions Found</h3>
|
|
204
|
+
<p className="text-muted-foreground">
|
|
205
|
+
{searchTerm || typeFilter !== 'all'
|
|
206
|
+
? 'No transactions match your current filters'
|
|
207
|
+
: "You don't have any transactions yet"}
|
|
208
|
+
</p>
|
|
209
|
+
</div>
|
|
210
|
+
) : (
|
|
211
|
+
<div className="rounded-md border">
|
|
212
|
+
<Table>
|
|
213
|
+
<TableHeader>
|
|
214
|
+
<TableRow>
|
|
215
|
+
<TableHead>Date & Time</TableHead>
|
|
216
|
+
<TableHead>Type</TableHead>
|
|
217
|
+
<TableHead>Amount</TableHead>
|
|
218
|
+
<TableHead>Balance After</TableHead>
|
|
219
|
+
<TableHead>Description</TableHead>
|
|
220
|
+
<TableHead>Reference</TableHead>
|
|
221
|
+
</TableRow>
|
|
222
|
+
</TableHeader>
|
|
223
|
+
<TableBody>
|
|
224
|
+
{filteredTransactions.map((transaction: any, index: number) => {
|
|
225
|
+
const isDeposit = transaction.type?.toLowerCase() === 'deposit' || transaction.type?.toLowerCase() === 'credit';
|
|
226
|
+
return (
|
|
227
|
+
<TableRow key={transaction.id || index}>
|
|
228
|
+
<TableCell>
|
|
229
|
+
<div>
|
|
230
|
+
<div className="font-medium">
|
|
231
|
+
{formatDate(transaction.created_at || transaction.timestamp)}
|
|
232
|
+
</div>
|
|
233
|
+
<div className="text-sm text-muted-foreground">
|
|
234
|
+
{getRelativeTime(transaction.created_at || transaction.timestamp)}
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</TableCell>
|
|
238
|
+
<TableCell>
|
|
239
|
+
<div className="flex items-center gap-2">
|
|
240
|
+
{getTypeIcon(transaction.type)}
|
|
241
|
+
<Badge variant={getTypeVariant(transaction.type)}>
|
|
242
|
+
{transaction.type || 'Unknown'}
|
|
243
|
+
</Badge>
|
|
244
|
+
</div>
|
|
245
|
+
</TableCell>
|
|
246
|
+
<TableCell className="font-mono font-semibold">
|
|
247
|
+
<span className={isDeposit ? 'text-green-600' : 'text-red-600'}>
|
|
248
|
+
{isDeposit ? '+' : '-'}
|
|
249
|
+
{formatCurrency(Math.abs(transaction.amount || transaction.amount_usd || 0))}
|
|
250
|
+
</span>
|
|
251
|
+
</TableCell>
|
|
252
|
+
<TableCell className="font-mono">
|
|
253
|
+
{formatCurrency(transaction.balance_after || 0)}
|
|
254
|
+
</TableCell>
|
|
255
|
+
<TableCell className="text-sm">
|
|
256
|
+
{transaction.description || transaction.note || 'No description'}
|
|
257
|
+
</TableCell>
|
|
258
|
+
<TableCell className="font-mono text-sm text-muted-foreground">
|
|
259
|
+
{transaction.reference || transaction.payment_id
|
|
260
|
+
? `${(transaction.reference || transaction.payment_id).toString().slice(0, 8)}...`
|
|
261
|
+
: 'N/A'}
|
|
262
|
+
</TableCell>
|
|
263
|
+
</TableRow>
|
|
264
|
+
);
|
|
265
|
+
})}
|
|
266
|
+
</TableBody>
|
|
267
|
+
</Table>
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
</CardContent>
|
|
271
|
+
</Card>
|
|
272
|
+
);
|
|
273
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TransactionsList } from './TransactionsList';
|
|
@@ -1,29 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Transactions View
|
|
2
|
+
* Transactions View (v2.0 - Simplified)
|
|
3
3
|
* View transaction history and balance changes
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
'use client';
|
|
7
7
|
|
|
8
8
|
import React from 'react';
|
|
9
|
-
import {
|
|
10
|
-
import { History } from 'lucide-react';
|
|
9
|
+
import { TransactionsList } from './components';
|
|
11
10
|
|
|
12
11
|
export const TransactionsView: React.FC = () => {
|
|
13
12
|
return (
|
|
14
|
-
<
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
<div className="w-16 h-16 mx-auto mb-4 bg-muted rounded-full flex items-center justify-center">
|
|
18
|
-
<History className="w-8 h-8 text-muted-foreground" />
|
|
19
|
-
</div>
|
|
20
|
-
<h3 className="text-lg font-semibold mb-2">Transactions Coming Soon</h3>
|
|
21
|
-
<p className="text-muted-foreground">
|
|
22
|
-
Transaction history and balance changes will be available here
|
|
23
|
-
</p>
|
|
24
|
-
</div>
|
|
25
|
-
</CardContent>
|
|
26
|
-
</Card>
|
|
13
|
+
<div className="space-y-6">
|
|
14
|
+
<TransactionsList />
|
|
15
|
+
</div>
|
|
27
16
|
);
|
|
28
17
|
};
|
|
29
|
-
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
# Payments Layout
|
|
2
|
-
|
|
3
|
-
Modern payments layout with tabbed interface for managing payments, balances, API keys, and subscriptions.
|
|
4
|
-
|
|
5
|
-
## Structure
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
PaymentsLayout/
|
|
9
|
-
├── PaymentsLayout.tsx # Main layout with tabs and providers
|
|
10
|
-
├── events.ts # Event-based dialog communication
|
|
11
|
-
├── types.ts # TypeScript types
|
|
12
|
-
├── index.ts # Public exports
|
|
13
|
-
└── views/ # Tab views
|
|
14
|
-
├── overview/ # Dashboard with metrics and balance
|
|
15
|
-
│ ├── components/
|
|
16
|
-
│ │ ├── MetricsCards.tsx
|
|
17
|
-
│ │ ├── BalanceCard.tsx
|
|
18
|
-
│ │ ├── RecentPayments.tsx
|
|
19
|
-
│ │ └── index.ts
|
|
20
|
-
│ └── index.tsx
|
|
21
|
-
├── payments/ # Payment transactions list
|
|
22
|
-
│ ├── components/
|
|
23
|
-
│ │ ├── PaymentsList.tsx
|
|
24
|
-
│ │ └── index.ts
|
|
25
|
-
│ └── index.tsx
|
|
26
|
-
├── transactions/ # Transaction history (placeholder)
|
|
27
|
-
│ └── index.tsx
|
|
28
|
-
├── apikeys/ # API keys management
|
|
29
|
-
│ ├── components/
|
|
30
|
-
│ │ ├── ApiKeysList.tsx
|
|
31
|
-
│ │ ├── ApiKeyMetrics.tsx
|
|
32
|
-
│ │ └── index.ts
|
|
33
|
-
│ └── index.tsx
|
|
34
|
-
└── tariffs/ # Tariff plans (placeholder)
|
|
35
|
-
└── index.tsx
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## Usage
|
|
39
|
-
|
|
40
|
-
```tsx
|
|
41
|
-
import { PaymentsLayout } from '@djangocfg/layouts';
|
|
42
|
-
|
|
43
|
-
function PaymentsPage() {
|
|
44
|
-
return <PaymentsLayout />;
|
|
45
|
-
}
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## Features
|
|
49
|
-
|
|
50
|
-
### Tabs
|
|
51
|
-
- **Overview** - Dashboard with key metrics, balance card, and recent payments
|
|
52
|
-
- **Payments** - Full payment history with search and filters
|
|
53
|
-
- **Transactions** - Balance transaction history (coming soon)
|
|
54
|
-
- **API Keys** - Create, manage, and monitor API keys
|
|
55
|
-
- **Tariffs** - Subscription plans and pricing (coming soon)
|
|
56
|
-
|
|
57
|
-
### Contexts
|
|
58
|
-
The layout automatically provides all necessary contexts:
|
|
59
|
-
- `OverviewProvider` - Dashboard metrics and overview data
|
|
60
|
-
- `PaymentsProvider` - Payment CRUD operations
|
|
61
|
-
- `BalancesProvider` - User balance management
|
|
62
|
-
- `ApiKeysProvider` - API key management
|
|
63
|
-
|
|
64
|
-
### Event System
|
|
65
|
-
Uses event-based communication for dialogs:
|
|
66
|
-
- `openCreateApiKeyDialog()` - Open create API key dialog
|
|
67
|
-
- `openEditApiKeyDialog(id)` - Open edit API key dialog
|
|
68
|
-
- `openDeleteApiKeyDialog(id)` - Open delete confirmation dialog
|
|
69
|
-
- `openCreatePaymentDialog()` - Open create payment dialog
|
|
70
|
-
- `openPaymentDetailsDialog(id)` - Open payment details dialog
|
|
71
|
-
- `openCancelPaymentDialog(id)` - Open cancel payment dialog
|
|
72
|
-
- `closePaymentsDialog()` - Close any active dialog
|
|
73
|
-
|
|
74
|
-
## Components
|
|
75
|
-
|
|
76
|
-
### Overview View
|
|
77
|
-
- **MetricsCards** - Display key metrics (balance, payments, API keys)
|
|
78
|
-
- **BalanceCard** - Show current balance with quick actions
|
|
79
|
-
- **RecentPayments** - List of recent payment transactions
|
|
80
|
-
|
|
81
|
-
### Payments View
|
|
82
|
-
- **PaymentsList** - Paginated payment list with filters and search
|
|
83
|
-
|
|
84
|
-
### API Keys View
|
|
85
|
-
- **ApiKeyMetrics** - API key usage statistics
|
|
86
|
-
- **ApiKeysList** - List of API keys with management actions
|
|
87
|
-
|
|
88
|
-
## Responsive Design
|
|
89
|
-
- Mobile-first responsive layout
|
|
90
|
-
- Adaptive tabs (icons only on mobile, text on desktop)
|
|
91
|
-
- Responsive grid layouts for cards and metrics
|
|
92
|
-
|
|
93
|
-
## Architecture
|
|
94
|
-
|
|
95
|
-
### Decomposed Contexts
|
|
96
|
-
Uses specialized contexts from `@djangocfg/api/cfg/contexts`:
|
|
97
|
-
- `PaymentsContext` - Payment operations
|
|
98
|
-
- `BalancesContext` - Balance queries
|
|
99
|
-
- `ApiKeysContext` - API key CRUD
|
|
100
|
-
- `OverviewContext` - Dashboard data
|
|
101
|
-
|
|
102
|
-
### View-based Structure
|
|
103
|
-
Each tab is a separate view with:
|
|
104
|
-
- Dedicated components folder
|
|
105
|
-
- Dedicated hooks folder (for future custom hooks)
|
|
106
|
-
- Clean separation of concerns
|
|
107
|
-
|
|
108
|
-
### Event-driven Dialogs
|
|
109
|
-
Dialogs are controlled via events, not props:
|
|
110
|
-
- Loose coupling between components
|
|
111
|
-
- Easy to trigger from anywhere
|
|
112
|
-
- Centralized dialog management
|
|
113
|
-
|
|
114
|
-
## Migration from Old Layout
|
|
115
|
-
|
|
116
|
-
The old `PaymentsLayout_old` has been replaced with this new structure. Key improvements:
|
|
117
|
-
|
|
118
|
-
1. **Better Organization** - Views are logically separated
|
|
119
|
-
2. **Decomposed Contexts** - Each context has single responsibility
|
|
120
|
-
3. **Event System** - Better dialog management
|
|
121
|
-
4. **Scalability** - Easy to add new tabs and features
|
|
122
|
-
5. **Type Safety** - Full TypeScript support
|
|
123
|
-
|
|
124
|
-
## Future Enhancements
|
|
125
|
-
|
|
126
|
-
- [ ] Complete Transactions view with filtering
|
|
127
|
-
- [ ] Complete Tariffs view with plan comparison
|
|
128
|
-
- [ ] Add payment creation dialogs
|
|
129
|
-
- [ ] Add API key creation/edit dialogs
|
|
130
|
-
- [ ] Add real-time payment status updates
|
|
131
|
-
- [ ] Add charts and analytics
|
|
132
|
-
- [ ] Add export functionality
|
|
133
|
-
|