mobigrid-module 1.0.7 → 1.0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 }