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.
@@ -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 }