mobigrid-module 1.0.7 → 1.0.8
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/components/CustomTable/CustomTable.tsx +182 -0
- package/components/CustomTable/Pagination.tsx +136 -0
- package/components/Icon.tsx +27 -0
- package/components/Layout/PageHeader.tsx +270 -0
- package/components/ui/alert.tsx +59 -0
- package/components/ui/button.tsx +57 -0
- package/components/ui/calendar.tsx +70 -0
- package/components/ui/date-picker-with-range.tsx +167 -0
- package/components/ui/dialog.tsx +120 -0
- package/components/ui/input.tsx +22 -0
- package/components/ui/pagination.tsx +117 -0
- package/components/ui/popover.tsx +31 -0
- package/components/ui/select.tsx +157 -0
- package/components/ui/sonner.tsx +30 -0
- package/components/ui/table.tsx +120 -0
- package/index.d.ts +55 -0
- package/index.tsx +254 -0
- package/lib/utils.ts +6 -0
- package/package.json +6 -3
@@ -0,0 +1,182 @@
|
|
1
|
+
import React from "react";
|
2
|
+
import {
|
3
|
+
flexRender,
|
4
|
+
getCoreRowModel,
|
5
|
+
useReactTable,
|
6
|
+
} from "@tanstack/react-table";
|
7
|
+
import {
|
8
|
+
Table,
|
9
|
+
TableBody,
|
10
|
+
TableCell,
|
11
|
+
TableHead,
|
12
|
+
TableHeader,
|
13
|
+
TableRow,
|
14
|
+
} from "../ui/table";
|
15
|
+
import { format } from "date-fns";
|
16
|
+
import Icon from "../Icon";
|
17
|
+
|
18
|
+
interface TableProps {
|
19
|
+
data: any[];
|
20
|
+
columns: any[];
|
21
|
+
isLoading: boolean;
|
22
|
+
}
|
23
|
+
|
24
|
+
export function CustomTable({ data, columns, isLoading }: TableProps) {
|
25
|
+
const table = useReactTable({
|
26
|
+
data: isLoading ? [] : data,
|
27
|
+
columns: columns.map((col) => ({
|
28
|
+
accessorKey: col.key,
|
29
|
+
header: col.title,
|
30
|
+
cell: (info) => {
|
31
|
+
if (col.key === "ACTIONS_BUTTONS") {
|
32
|
+
return (
|
33
|
+
<div className="flex gap-2 justify-end">
|
34
|
+
{col.actions?.map((action: any, index: number) => (
|
35
|
+
<button
|
36
|
+
key={index}
|
37
|
+
className={`inline-flex items-center px-2 py-1 text-sm rounded-full transition-colors duration-200 ${
|
38
|
+
index === 0
|
39
|
+
? "text-blue-700 bg-blue-50 border border-blue-200 hover:bg-blue-100"
|
40
|
+
: index === 1
|
41
|
+
? "text-green-700 bg-green-50 border border-green-200 hover:bg-green-100"
|
42
|
+
: index === 2
|
43
|
+
? "text-purple-700 bg-purple-50 border border-purple-200 hover:bg-purple-100"
|
44
|
+
: index === 3
|
45
|
+
? "text-orange-700 bg-orange-50 border border-orange-200 hover:bg-orange-100"
|
46
|
+
: "text-gray-700 bg-gray-50 border border-gray-200 hover:bg-gray-100"
|
47
|
+
}`}
|
48
|
+
onClick={() => {
|
49
|
+
console.log(action.action, info.row.original);
|
50
|
+
}}
|
51
|
+
>
|
52
|
+
{action.icon && (
|
53
|
+
<svg
|
54
|
+
xmlns="http://www.w3.org/2000/svg"
|
55
|
+
width="16"
|
56
|
+
height="16"
|
57
|
+
viewBox="0 0 24 24"
|
58
|
+
fill="none"
|
59
|
+
stroke="currentColor"
|
60
|
+
strokeWidth="2"
|
61
|
+
strokeLinecap="round"
|
62
|
+
strokeLinejoin="round"
|
63
|
+
className="mr-2"
|
64
|
+
>
|
65
|
+
<Icon name={action.icon.replace(/^icon-/, '')} className="mr-2" />
|
66
|
+
</svg>
|
67
|
+
)}
|
68
|
+
{action.label}
|
69
|
+
</button>
|
70
|
+
))}
|
71
|
+
</div>
|
72
|
+
);
|
73
|
+
}
|
74
|
+
if (col.type === "status") {
|
75
|
+
const statusColors: { [key: string]: string } = {
|
76
|
+
PENDING: "bg-yellow-100 text-yellow-800",
|
77
|
+
PAID: "bg-green-100 text-green-800",
|
78
|
+
CANCELLED: "bg-red-100 text-red-800",
|
79
|
+
CANCEL_STARTED: "bg-orange-100 text-orange-800",
|
80
|
+
ECHEC: "bg-gray-100 text-gray-800",
|
81
|
+
};
|
82
|
+
const status = info.getValue() as string;
|
83
|
+
return (
|
84
|
+
<div className="flex items-center justify-center">
|
85
|
+
<span
|
86
|
+
className={`px-2 py-1 rounded-full text-xs font-medium text-center ${
|
87
|
+
statusColors[status] || "bg-gray-100 text-gray-800"
|
88
|
+
}`}
|
89
|
+
>
|
90
|
+
{status || "-"}
|
91
|
+
</span>
|
92
|
+
</div>
|
93
|
+
);
|
94
|
+
}
|
95
|
+
if (col.type === "money") {
|
96
|
+
return `${info.getValue()} ${col.currency}`;
|
97
|
+
}
|
98
|
+
if (col.type === "date") {
|
99
|
+
return format(new Date(info.getValue() as string), "dd-MM-yyyy");
|
100
|
+
}
|
101
|
+
if (col.scroll) {
|
102
|
+
return (
|
103
|
+
<div className="max-h-24 overflow-y-auto">
|
104
|
+
{info.getValue()}
|
105
|
+
</div>
|
106
|
+
);
|
107
|
+
}
|
108
|
+
return info.getValue();
|
109
|
+
},
|
110
|
+
})),
|
111
|
+
getCoreRowModel: getCoreRowModel(),
|
112
|
+
enableMultiRowSelection: true,
|
113
|
+
enableRowSelection: true,
|
114
|
+
state: {
|
115
|
+
rowSelection: {},
|
116
|
+
},
|
117
|
+
});
|
118
|
+
|
119
|
+
return (
|
120
|
+
<Table className="border border-gray-200 rounded-md">
|
121
|
+
<TableHeader className="bg-gray-50">
|
122
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
123
|
+
<TableRow key={headerGroup.id}>
|
124
|
+
{headerGroup.headers.map((header) => (
|
125
|
+
<TableHead key={header.id} className="font-medium text-gray-800 py-2">
|
126
|
+
{flexRender(
|
127
|
+
header.column.columnDef.header,
|
128
|
+
header.getContext()
|
129
|
+
)}
|
130
|
+
</TableHead>
|
131
|
+
))}
|
132
|
+
</TableRow>
|
133
|
+
))}
|
134
|
+
</TableHeader>
|
135
|
+
<TableBody>
|
136
|
+
{isLoading ? (
|
137
|
+
<TableRow>
|
138
|
+
<TableCell colSpan={columns.length} className="text-center py-4">
|
139
|
+
<svg
|
140
|
+
className="animate-spin h-5 w-5 mx-auto mb-2"
|
141
|
+
xmlns="http://www.w3.org/2000/svg"
|
142
|
+
fill="none"
|
143
|
+
viewBox="0 0 24 24"
|
144
|
+
>
|
145
|
+
<circle
|
146
|
+
className="opacity-25"
|
147
|
+
cx="12"
|
148
|
+
cy="12"
|
149
|
+
r="10"
|
150
|
+
stroke="currentColor"
|
151
|
+
strokeWidth="4"
|
152
|
+
></circle>
|
153
|
+
<path
|
154
|
+
className="opacity-75"
|
155
|
+
fill="currentColor"
|
156
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
157
|
+
></path>
|
158
|
+
</svg>
|
159
|
+
Chargement des données...
|
160
|
+
</TableCell>
|
161
|
+
</TableRow>
|
162
|
+
) : !data || data.length === 0 ? (
|
163
|
+
<TableRow>
|
164
|
+
<TableCell colSpan={columns.length} className="text-center py-4">
|
165
|
+
Aucune donnée disponible
|
166
|
+
</TableCell>
|
167
|
+
</TableRow>
|
168
|
+
) : (
|
169
|
+
table.getRowModel().rows.map((row) => (
|
170
|
+
<TableRow key={row.id} className="bg-white hover:bg-gray-100">
|
171
|
+
{row.getVisibleCells().map((cell) => (
|
172
|
+
<TableCell key={cell.id}>
|
173
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
174
|
+
</TableCell>
|
175
|
+
))}
|
176
|
+
</TableRow>
|
177
|
+
))
|
178
|
+
)}
|
179
|
+
</TableBody>
|
180
|
+
</Table>
|
181
|
+
);
|
182
|
+
}
|
@@ -0,0 +1,136 @@
|
|
1
|
+
import React from "react";
|
2
|
+
import {
|
3
|
+
Pagination,
|
4
|
+
PaginationContent,
|
5
|
+
PaginationEllipsis,
|
6
|
+
PaginationItem,
|
7
|
+
PaginationLink,
|
8
|
+
PaginationNext,
|
9
|
+
PaginationPrevious,
|
10
|
+
} from "../ui/pagination";
|
11
|
+
|
12
|
+
interface PaginationProps {
|
13
|
+
currentPage: number;
|
14
|
+
totalPages: number;
|
15
|
+
onPageChange: (page: number) => void;
|
16
|
+
}
|
17
|
+
|
18
|
+
export default function ({
|
19
|
+
currentPage,
|
20
|
+
totalPages,
|
21
|
+
onPageChange,
|
22
|
+
}: PaginationProps) {
|
23
|
+
const renderPaginationItems = () => {
|
24
|
+
const items = [];
|
25
|
+
|
26
|
+
// Previous button
|
27
|
+
items.push(
|
28
|
+
<PaginationItem key="prev">
|
29
|
+
<PaginationPrevious
|
30
|
+
href="#"
|
31
|
+
onClick={(e) => {
|
32
|
+
e.preventDefault();
|
33
|
+
if (currentPage > 1) onPageChange(currentPage - 1);
|
34
|
+
}}
|
35
|
+
/>
|
36
|
+
</PaginationItem>
|
37
|
+
);
|
38
|
+
|
39
|
+
// First page
|
40
|
+
items.push(
|
41
|
+
<PaginationItem key={1}>
|
42
|
+
<PaginationLink
|
43
|
+
href="#"
|
44
|
+
isActive={currentPage === 1}
|
45
|
+
onClick={(e) => {
|
46
|
+
e.preventDefault();
|
47
|
+
onPageChange(1);
|
48
|
+
}}
|
49
|
+
>
|
50
|
+
1
|
51
|
+
</PaginationLink>
|
52
|
+
</PaginationItem>
|
53
|
+
);
|
54
|
+
|
55
|
+
// Calculate visible page range
|
56
|
+
let startPage = Math.max(2, currentPage - 1);
|
57
|
+
let endPage = Math.min(totalPages - 1, currentPage + 1);
|
58
|
+
|
59
|
+
// Add ellipsis after first page if needed
|
60
|
+
if (startPage > 2) {
|
61
|
+
items.push(
|
62
|
+
<PaginationItem key="ellipsis1">
|
63
|
+
<PaginationEllipsis />
|
64
|
+
</PaginationItem>
|
65
|
+
);
|
66
|
+
}
|
67
|
+
|
68
|
+
// Add middle pages
|
69
|
+
for (let i = startPage; i <= endPage; i++) {
|
70
|
+
items.push(
|
71
|
+
<PaginationItem key={i}>
|
72
|
+
<PaginationLink
|
73
|
+
href="#"
|
74
|
+
isActive={currentPage === i}
|
75
|
+
onClick={(e) => {
|
76
|
+
e.preventDefault();
|
77
|
+
onPageChange(i);
|
78
|
+
}}
|
79
|
+
>
|
80
|
+
{i}
|
81
|
+
</PaginationLink>
|
82
|
+
</PaginationItem>
|
83
|
+
);
|
84
|
+
}
|
85
|
+
|
86
|
+
// Add ellipsis before last page if needed
|
87
|
+
if (endPage < totalPages - 1) {
|
88
|
+
items.push(
|
89
|
+
<PaginationItem key="ellipsis2">
|
90
|
+
<PaginationEllipsis />
|
91
|
+
</PaginationItem>
|
92
|
+
);
|
93
|
+
}
|
94
|
+
|
95
|
+
// Last page (if not already included)
|
96
|
+
if (totalPages > 1) {
|
97
|
+
items.push(
|
98
|
+
<PaginationItem key={totalPages}>
|
99
|
+
<PaginationLink
|
100
|
+
href="#"
|
101
|
+
isActive={currentPage === totalPages}
|
102
|
+
onClick={(e) => {
|
103
|
+
e.preventDefault();
|
104
|
+
onPageChange(totalPages);
|
105
|
+
}}
|
106
|
+
>
|
107
|
+
{totalPages}
|
108
|
+
</PaginationLink>
|
109
|
+
</PaginationItem>
|
110
|
+
);
|
111
|
+
}
|
112
|
+
|
113
|
+
// Next button
|
114
|
+
items.push(
|
115
|
+
<PaginationItem key="next">
|
116
|
+
<PaginationNext
|
117
|
+
href="#"
|
118
|
+
onClick={(e) => {
|
119
|
+
e.preventDefault();
|
120
|
+
if (currentPage < totalPages) onPageChange(currentPage + 1);
|
121
|
+
}}
|
122
|
+
/>
|
123
|
+
</PaginationItem>
|
124
|
+
);
|
125
|
+
|
126
|
+
return items;
|
127
|
+
};
|
128
|
+
|
129
|
+
return (
|
130
|
+
<Pagination>
|
131
|
+
<PaginationContent>
|
132
|
+
{renderPaginationItems()}
|
133
|
+
</PaginationContent>
|
134
|
+
</Pagination>
|
135
|
+
);
|
136
|
+
}
|
@@ -0,0 +1,27 @@
|
|
1
|
+
import React from "react";
|
2
|
+
import * as FeatherIcons from 'react-feather';
|
3
|
+
import { IconProps as FeatherIconProps } from 'react-feather';
|
4
|
+
|
5
|
+
interface IconProps extends Omit<FeatherIconProps, 'ref'> {
|
6
|
+
name: string;
|
7
|
+
}
|
8
|
+
|
9
|
+
const Icon = ({ name, ...props }: IconProps): JSX.Element | null => {
|
10
|
+
const formatIconName = (iconName: string): string => {
|
11
|
+
return iconName
|
12
|
+
.split(/[-_]/)
|
13
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
14
|
+
.join('');
|
15
|
+
};
|
16
|
+
|
17
|
+
const IconComponent = FeatherIcons[formatIconName(name) as keyof typeof FeatherIcons];
|
18
|
+
|
19
|
+
if (!IconComponent) {
|
20
|
+
console.warn(`Icon "${name}" not found in react-feather library`);
|
21
|
+
return null;
|
22
|
+
}
|
23
|
+
|
24
|
+
return <IconComponent {...props} />;
|
25
|
+
};
|
26
|
+
|
27
|
+
export default Icon;
|
@@ -0,0 +1,270 @@
|
|
1
|
+
import * as React from "react";
|
2
|
+
import { DatePickerWithRange } from "../ui/date-picker-with-range";
|
3
|
+
import { Button } from "../ui/button";
|
4
|
+
import { DateRange } from "react-day-picker";
|
5
|
+
import { Input } from "../ui/input";
|
6
|
+
import {
|
7
|
+
Select,
|
8
|
+
SelectItem,
|
9
|
+
SelectContent,
|
10
|
+
SelectValue,
|
11
|
+
SelectTrigger,
|
12
|
+
} from "../ui/select";
|
13
|
+
import { useState, useMemo } from "react";
|
14
|
+
|
15
|
+
interface PageHeaderProps {
|
16
|
+
title: string;
|
17
|
+
filters: any;
|
18
|
+
configFilters: any;
|
19
|
+
setFilters: (filters: any) => void;
|
20
|
+
onSearch: () => void;
|
21
|
+
count: number;
|
22
|
+
isLoading: boolean;
|
23
|
+
setCurrentPage: (page: number) => void;
|
24
|
+
}
|
25
|
+
|
26
|
+
export function PageHeader({
|
27
|
+
title,
|
28
|
+
filters,
|
29
|
+
setFilters,
|
30
|
+
configFilters,
|
31
|
+
onSearch,
|
32
|
+
count,
|
33
|
+
isLoading,
|
34
|
+
setCurrentPage,
|
35
|
+
}: PageHeaderProps) {
|
36
|
+
const [initialFilters, setInitialFilters] = React.useState(filters);
|
37
|
+
|
38
|
+
const [exportDisabled, setExportDisabled] = useState(false);
|
39
|
+
const filtersChanged = React.useMemo(() => {
|
40
|
+
return JSON.stringify(initialFilters) !== JSON.stringify(filters);
|
41
|
+
}, [initialFilters, filters]);
|
42
|
+
|
43
|
+
const handleSearch = async () => {
|
44
|
+
setInitialFilters(filters);
|
45
|
+
if (filtersChanged) await setCurrentPage(1);
|
46
|
+
onSearch();
|
47
|
+
};
|
48
|
+
|
49
|
+
const handleDateChange = (date: DateRange | undefined) => {
|
50
|
+
setFilters({ ...filters, fromDate: date?.from, toDate: date?.to });
|
51
|
+
};
|
52
|
+
|
53
|
+
const handleExport = async (ctx: string) => {
|
54
|
+
try {
|
55
|
+
const response = await fetch(
|
56
|
+
(() => {
|
57
|
+
let url = `/cashplus/newfront/templates/extract-action.cfm?ctx=${ctx}`;
|
58
|
+
for (const [key, value] of Object.entries(filters)) {
|
59
|
+
if (value && typeof value !== "object") {
|
60
|
+
if (key === "fromDate" || key === "toDate") {
|
61
|
+
const date = new Date(value as string);
|
62
|
+
url =
|
63
|
+
url +
|
64
|
+
`&${key}=${date.getFullYear()}-${String(
|
65
|
+
date.getMonth() + 1
|
66
|
+
).padStart(2, "0")}-${String(date.getDate()).padStart(
|
67
|
+
2,
|
68
|
+
"0"
|
69
|
+
)}`;
|
70
|
+
continue;
|
71
|
+
}
|
72
|
+
|
73
|
+
url = url + `&${key}=${value}`;
|
74
|
+
}
|
75
|
+
}
|
76
|
+
return url;
|
77
|
+
})()
|
78
|
+
);
|
79
|
+
|
80
|
+
if (!response.ok) {
|
81
|
+
throw new Error("Export failed");
|
82
|
+
}
|
83
|
+
|
84
|
+
const blob = await response.blob();
|
85
|
+
const url = window.URL.createObjectURL(blob);
|
86
|
+
const a = document.createElement("a");
|
87
|
+
a.href = url;
|
88
|
+
const fileName = `${title.replace(/\s+/g, "_")}_${filters.fromDate}_${
|
89
|
+
filters.toDate
|
90
|
+
}_.csv`;
|
91
|
+
a.download = fileName;
|
92
|
+
document.body.appendChild(a);
|
93
|
+
a.click();
|
94
|
+
window.URL.revokeObjectURL(url);
|
95
|
+
document.body.removeChild(a);
|
96
|
+
} catch (error) {
|
97
|
+
console.error("Export error:", error);
|
98
|
+
}
|
99
|
+
};
|
100
|
+
|
101
|
+
return (
|
102
|
+
<div className="flex flex-col gap-2">
|
103
|
+
<h1 className="text-2xl font-bold text-[rgb(0,137,149)]">
|
104
|
+
<svg
|
105
|
+
xmlns="http://www.w3.org/2000/svg"
|
106
|
+
width="24"
|
107
|
+
height="24"
|
108
|
+
viewBox="0 0 24 24"
|
109
|
+
fill="none"
|
110
|
+
stroke="rgb(0, 137, 149)"
|
111
|
+
strokeWidth="2"
|
112
|
+
strokeLinecap="round"
|
113
|
+
strokeLinejoin="round"
|
114
|
+
className="inline-block mr-2"
|
115
|
+
>
|
116
|
+
<line x1="8" y1="6" x2="21" y2="6" />
|
117
|
+
<line x1="8" y1="12" x2="21" y2="12" />
|
118
|
+
<line x1="8" y1="18" x2="21" y2="18" />
|
119
|
+
<line x1="3" y1="6" x2="3.01" y2="6" />
|
120
|
+
<line x1="3" y1="12" x2="3.01" y2="12" />
|
121
|
+
<line x1="3" y1="18" x2="3.01" y2="18" />
|
122
|
+
</svg>
|
123
|
+
|
124
|
+
{title}
|
125
|
+
<span className="ml-2 px-2 py-1 text-sm bg-gray-100 text-[rgb(0,137,149)] rounded-full">
|
126
|
+
{count}
|
127
|
+
</span>
|
128
|
+
</h1>
|
129
|
+
<div className="border-b border-gray-100 mb-4"></div>
|
130
|
+
<div className="flex gap-4 flex-wrap justify-between">
|
131
|
+
<DatePickerWithRange
|
132
|
+
dateFrom={filters.fromDate}
|
133
|
+
dateTo={filters.toDate}
|
134
|
+
handleDateChange={handleDateChange}
|
135
|
+
/>
|
136
|
+
{configFilters &&
|
137
|
+
configFilters.map((filter: any, index: number) => (
|
138
|
+
<div key={`${filter.name}-${index}`}>
|
139
|
+
{filter.type === "Text" && (
|
140
|
+
<Input
|
141
|
+
type="text"
|
142
|
+
placeholder={filter.placeholder}
|
143
|
+
value={filters[filter.name]}
|
144
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
145
|
+
setFilters({ ...filters, [filter.name]: e.target.value })
|
146
|
+
}
|
147
|
+
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
|
148
|
+
if (e.key === "Enter") {
|
149
|
+
handleSearch();
|
150
|
+
}
|
151
|
+
}}
|
152
|
+
/>
|
153
|
+
)}
|
154
|
+
{filter.type === "Select" && (
|
155
|
+
<Select
|
156
|
+
disabled={filter.disabled || filter.options.length === 0}
|
157
|
+
value={filters[filter.name]}
|
158
|
+
onValueChange={(value: string) => {
|
159
|
+
setFilters({
|
160
|
+
...filters,
|
161
|
+
[filter.name]: value === "_empty" ? undefined : value,
|
162
|
+
});
|
163
|
+
}}
|
164
|
+
>
|
165
|
+
<SelectTrigger>
|
166
|
+
<SelectValue placeholder={filter.placeholder} />
|
167
|
+
</SelectTrigger>
|
168
|
+
<SelectContent>
|
169
|
+
{useMemo(
|
170
|
+
() =>
|
171
|
+
filter.options.map(
|
172
|
+
(
|
173
|
+
option: { VALUE: string; NAME: string },
|
174
|
+
optionIndex: number
|
175
|
+
) => (
|
176
|
+
<SelectItem
|
177
|
+
key={`${option.VALUE}-${optionIndex}`}
|
178
|
+
value={option.VALUE || "_empty"}
|
179
|
+
>
|
180
|
+
{option.NAME}
|
181
|
+
</SelectItem>
|
182
|
+
)
|
183
|
+
),
|
184
|
+
[filter.options]
|
185
|
+
)}
|
186
|
+
</SelectContent>
|
187
|
+
</Select>
|
188
|
+
)}
|
189
|
+
</div>
|
190
|
+
))}
|
191
|
+
<div className="flex gap-2 ml-auto">
|
192
|
+
{configFilters.some((filter: any) => filter.type === "Export") && (
|
193
|
+
<Button
|
194
|
+
disabled={exportDisabled}
|
195
|
+
onClick={async () => {
|
196
|
+
setExportDisabled(true);
|
197
|
+
const exportFilter = configFilters.find(
|
198
|
+
(filter: any) => filter.type === "Export"
|
199
|
+
);
|
200
|
+
await handleExport(exportFilter?.ctx);
|
201
|
+
const timer = setInterval(() => {
|
202
|
+
setExportDisabled(false);
|
203
|
+
clearInterval(timer);
|
204
|
+
}, 30000);
|
205
|
+
}}
|
206
|
+
size="sm"
|
207
|
+
>
|
208
|
+
<svg
|
209
|
+
xmlns="http://www.w3.org/2000/svg"
|
210
|
+
width="16"
|
211
|
+
height="16"
|
212
|
+
viewBox="0 0 24 24"
|
213
|
+
fill="none"
|
214
|
+
stroke="currentColor"
|
215
|
+
strokeWidth="2"
|
216
|
+
strokeLinecap="round"
|
217
|
+
strokeLinejoin="round"
|
218
|
+
>
|
219
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
220
|
+
<polyline points="7 10 12 15 17 10" />
|
221
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
222
|
+
</svg>
|
223
|
+
CSV
|
224
|
+
</Button>
|
225
|
+
)}
|
226
|
+
<Button size="sm" onClick={handleSearch} disabled={isLoading}>
|
227
|
+
{filtersChanged ? (
|
228
|
+
<>
|
229
|
+
<svg
|
230
|
+
xmlns="http://www.w3.org/2000/svg"
|
231
|
+
width="16"
|
232
|
+
height="16"
|
233
|
+
viewBox="0 0 24 24"
|
234
|
+
fill="none"
|
235
|
+
stroke="currentColor"
|
236
|
+
strokeWidth="2"
|
237
|
+
strokeLinecap="round"
|
238
|
+
strokeLinejoin="round"
|
239
|
+
>
|
240
|
+
<circle cx="11" cy="11" r="8" />
|
241
|
+
<path d="m21 21-4.3-4.3" />
|
242
|
+
</svg>
|
243
|
+
Recherche
|
244
|
+
</>
|
245
|
+
) : (
|
246
|
+
<>
|
247
|
+
<svg
|
248
|
+
width="16"
|
249
|
+
height="16"
|
250
|
+
viewBox="0 0 24 24"
|
251
|
+
fill="none"
|
252
|
+
stroke={"currentColor"}
|
253
|
+
strokeWidth="2"
|
254
|
+
strokeLinecap="round"
|
255
|
+
strokeLinejoin="round"
|
256
|
+
className="reload-icon"
|
257
|
+
aria-hidden="true"
|
258
|
+
>
|
259
|
+
<path d="M21 12a9 9 0 11-3-6.74" />
|
260
|
+
<path d="M21 3v6h-6" />
|
261
|
+
</svg>
|
262
|
+
Actualiser
|
263
|
+
</>
|
264
|
+
)}
|
265
|
+
</Button>
|
266
|
+
</div>
|
267
|
+
</div>
|
268
|
+
</div>
|
269
|
+
);
|
270
|
+
}
|
@@ -0,0 +1,59 @@
|
|
1
|
+
import * as React from "react"
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
3
|
+
|
4
|
+
import { cn } from "../../lib/utils"
|
5
|
+
|
6
|
+
const alertVariants = cva(
|
7
|
+
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
8
|
+
{
|
9
|
+
variants: {
|
10
|
+
variant: {
|
11
|
+
default: "bg-background text-foreground",
|
12
|
+
destructive:
|
13
|
+
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
14
|
+
},
|
15
|
+
},
|
16
|
+
defaultVariants: {
|
17
|
+
variant: "default",
|
18
|
+
},
|
19
|
+
}
|
20
|
+
)
|
21
|
+
|
22
|
+
const Alert = React.forwardRef<
|
23
|
+
HTMLDivElement,
|
24
|
+
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
25
|
+
>(({ className, variant, ...props }, ref) => (
|
26
|
+
<div
|
27
|
+
ref={ref}
|
28
|
+
role="alert"
|
29
|
+
className={cn(alertVariants({ variant }), className)}
|
30
|
+
{...props}
|
31
|
+
/>
|
32
|
+
))
|
33
|
+
Alert.displayName = "Alert"
|
34
|
+
|
35
|
+
const AlertTitle = React.forwardRef<
|
36
|
+
HTMLParagraphElement,
|
37
|
+
React.HTMLAttributes<HTMLHeadingElement>
|
38
|
+
>(({ className, ...props }, ref) => (
|
39
|
+
<h5
|
40
|
+
ref={ref}
|
41
|
+
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
42
|
+
{...props}
|
43
|
+
/>
|
44
|
+
))
|
45
|
+
AlertTitle.displayName = "AlertTitle"
|
46
|
+
|
47
|
+
const AlertDescription = React.forwardRef<
|
48
|
+
HTMLParagraphElement,
|
49
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
50
|
+
>(({ className, ...props }, ref) => (
|
51
|
+
<div
|
52
|
+
ref={ref}
|
53
|
+
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
54
|
+
{...props}
|
55
|
+
/>
|
56
|
+
))
|
57
|
+
AlertDescription.displayName = "AlertDescription"
|
58
|
+
|
59
|
+
export { Alert, AlertTitle, AlertDescription }
|