next-recomponents 2.0.4 → 2.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.
@@ -1,12 +1,12 @@
1
1
  "use client";
2
2
 
3
3
  import { DataGrid, GridRowModel, GridValidRowModel } from "@mui/x-data-grid";
4
- import { Box } from "@mui/material";
4
+ import { Box, Dialog } from "@mui/material";
5
5
  import React, { useEffect, useMemo, useRef, useState } from "react";
6
6
  import useExcel from "../use-excel";
7
7
  import Button from "../button";
8
- import { Dialog } from "@mui/material";
9
- import Pre from "src/pre";
8
+
9
+ // ─── Types ────────────────────────────────────────────────────────────────────
10
10
 
11
11
  export interface TableButtonProps extends React.MouseEvent<
12
12
  HTMLButtonElement,
@@ -15,8 +15,10 @@ export interface TableButtonProps extends React.MouseEvent<
15
15
  row: Record<string, any>;
16
16
  }
17
17
 
18
+ type FooterAggregation = "sum" | "avg" | "count";
19
+
18
20
  interface FooterType {
19
- [key: string]: "sum" | "avg" | "count";
21
+ [key: string]: FooterAggregation;
20
22
  }
21
23
 
22
24
  interface TableProps {
@@ -34,37 +36,24 @@ interface TableProps {
34
36
  hideColumns?: string[];
35
37
  footer?: FooterType;
36
38
  symbols?: any;
37
-
38
39
  [key: string]: any;
39
40
  }
40
- export default function Table(props: TableProps) {
41
- if (Array.isArray(props.data)) {
42
- return <IHTable {...props} />;
43
- } else {
44
- return (
45
- <div className=" bg-white border shadow rounded p-1">
46
- <table className="rounded">
47
- <tbody>
48
- {Object.keys(props.data).map((k) => (
49
- <tr key={k} className="border-b ">
50
- <th className="font-bold p-3 text-right">{k}</th>
51
- <td
52
- className={
53
- typeof props.data[k] === "number"
54
- ? "text-right"
55
- : "text-center"
56
- }
57
- >
58
- {props.data[k]}
59
- </td>
60
- </tr>
61
- ))}
62
- </tbody>
63
- </table>
64
- </div>
65
- );
66
- }
67
- }
41
+
42
+ // ─── Height mapping ───────────────────────────────────────────────────────────
43
+
44
+ const HEIGHT_MAP: Record<number, number> = {
45
+ 1: 150,
46
+ 2: 200,
47
+ 3: 250,
48
+ 4: 310,
49
+ 5: 360,
50
+ 6: 410,
51
+ 7: 460,
52
+ 8: 510,
53
+ };
54
+
55
+ // ─── Icons ────────────────────────────────────────────────────────────────────
56
+
68
57
  function EditIcon() {
69
58
  return (
70
59
  <svg
@@ -76,66 +65,221 @@ function EditIcon() {
76
65
  width="20px"
77
66
  xmlns="http://www.w3.org/2000/svg"
78
67
  >
79
- <path d="M402.6 83.2l90.2 90.2c3.8 3.8 3.8 10 0 13.8L274.4 405.6l-92.8 10.3c-12.4 1.4-22.9-9.1-21.5-21.5l10.3-92.8L388.8 83.2c3.8-3.8 10-3.8 13.8 0zm162-22.9l-48.8-48.8c-15.2-15.2-39.9-15.2-55.2 0l-35.4 35.4c-3.8 3.8-3.8 10 0 13.8l90.2 90.2c3.8 3.8 10 3.8 13.8 0l35.4-35.4c15.2-15.3 15.2-40 0-55.2zM384 346.2V448H64V128h229.8c3.2 0 6.2-1.3 8.5-3.5l40-40c7.6-7.6 2.2-20.5-8.5-20.5H48C21.5 64 0 85.5 0 112v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V306.2c0-10.7-12.9-16-20.5-8.5l-40 40c-2.2 2.3-3.5 5.3-3.5 8.5z"></path>
68
+ <path d="M402.6 83.2l90.2 90.2c3.8 3.8 3.8 10 0 13.8L274.4 405.6l-92.8 10.3c-12.4 1.4-22.9-9.1-21.5-21.5l10.3-92.8L388.8 83.2c3.8-3.8 10-3.8 13.8 0zm162-22.9l-48.8-48.8c-15.2-15.2-39.9-15.2-55.2 0l-35.4 35.4c-3.8 3.8-3.8 10 0 13.8l90.2 90.2c3.8 3.8 10 3.8 13.8 0l35.4-35.4c15.2-15.3 15.2-40 0-55.2zM384 346.2V448H64V128h229.8c3.2 0 6.2-1.3 8.5-3.5l40-40c7.6-7.6 2.2-20.5-8.5-20.5H48C21.5 64 0 85.5 0 112v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V306.2c0-10.7-12.9-16-20.5-8.5l-40 40c-2.2 2.3-3.5 5.3-3.5 8.5z" />
80
69
  </svg>
81
70
  );
82
71
  }
83
- function IHTable({
84
- data,
85
- flex = 1,
86
- editableFields,
72
+
73
+ // ─── KeyValueTable (non-array data) ──────────────────────────────────────────
74
+
75
+ function KeyValueTable({ data }: { data: Record<string, any> }) {
76
+ return (
77
+ <div className="bg-white border shadow rounded p-1">
78
+ <table className="rounded">
79
+ <tbody>
80
+ {Object.keys(data).map((key) => (
81
+ <tr key={key} className="border-b">
82
+ <th className="font-bold p-3 text-right">{key}</th>
83
+ <td
84
+ className={
85
+ typeof data[key] === "number" ? "text-right" : "text-center"
86
+ }
87
+ >
88
+ {data[key]}
89
+ </td>
90
+ </tr>
91
+ ))}
92
+ </tbody>
93
+ </table>
94
+ </div>
95
+ );
96
+ }
97
+
98
+ // ─── CustomFooter ─────────────────────────────────────────────────────────────
99
+
100
+ const FOOTER_LABELS: Record<FooterAggregation, string> = {
101
+ sum: "Suma",
102
+ avg: "Promedio",
103
+ count: "Conteo",
104
+ };
105
+
106
+ function computeAggregation(
107
+ rows: GridValidRowModel[],
108
+ key: string,
109
+ type: FooterAggregation,
110
+ ): number {
111
+ const values = rows.map((r) => Number(r[key] ?? 0)).filter((v) => !isNaN(v));
112
+
113
+ if (type === "sum") return values.reduce((acc, v) => acc + v, 0);
114
+ if (type === "count") return values.length;
115
+ if (type === "avg")
116
+ return values.length
117
+ ? values.reduce((acc, v) => acc + v, 0) / values.length
118
+ : 0;
119
+ return 0;
120
+ }
121
+
122
+ function CustomFooter({
123
+ rows,
124
+ footer,
125
+ }: {
126
+ rows: GridValidRowModel[];
127
+ footer: FooterType;
128
+ }) {
129
+ const entries = Object.entries(footer);
130
+ if (!entries.length) return null;
131
+
132
+ return (
133
+ <div className="flex justify-end gap-6 px-4 py-2 bg-gray-100 border-t border-gray-300 text-sm font-semibold text-gray-700">
134
+ {entries.map(([key, type]) => {
135
+ const value = computeAggregation(rows, key, type);
136
+ const formatted =
137
+ type === "avg"
138
+ ? value.toLocaleString(undefined, { maximumFractionDigits: 2 })
139
+ : value.toLocaleString();
140
+ return (
141
+ <span key={key}>
142
+ {FOOTER_LABELS[type]} de{" "}
143
+ <span className="text-gray-900">{key}</span>:{" "}
144
+ <span className="text-blue-700">{formatted}</span>
145
+ </span>
146
+ );
147
+ })}
148
+ </div>
149
+ );
150
+ }
151
+
152
+ // ─── ModalDialog ──────────────────────────────────────────────────────────────
153
+
154
+ interface ModalDialogProps {
155
+ open: boolean;
156
+ onClose: () => void;
157
+ modal: React.ReactNode;
158
+ selectedRow: GridValidRowModel | undefined;
159
+ }
160
+
161
+ function ModalDialog({ open, onClose, modal, selectedRow }: ModalDialogProps) {
162
+ return (
163
+ <Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
164
+ <div className="flex justify-end">
165
+ <button
166
+ onClick={onClose}
167
+ className="text-gray-500 hover:text-gray-800 text-xl font-bold p-5"
168
+ >
169
+ &times;
170
+ </button>
171
+ </div>
172
+ <div className="mt-4 m-auto p-5">
173
+ {selectedRow &&
174
+ React.cloneElement(
175
+ modal as React.ReactElement,
176
+ { row: selectedRow } as any,
177
+ )}
178
+ </div>
179
+ </Dialog>
180
+ );
181
+ }
182
+
183
+ // ─── Toolbar ──────────────────────────────────────────────────────────────────
184
+
185
+ interface ToolbarProps {
186
+ exportName?: string;
187
+ onSave?: (data: GridValidRowModel[]) => void;
188
+ onSelect?: (data: GridValidRowModel[]) => void;
189
+ rows: GridValidRowModel[];
190
+ filteredRows: GridValidRowModel[];
191
+ }
192
+
193
+ function Toolbar({
194
+ exportName,
87
195
  onSave,
88
196
  onSelect,
89
- buttons,
90
- exportName,
91
- modal,
92
- height = 600,
93
- width = "100%",
94
- header,
95
- hideColumns = [],
96
- footer,
97
- }: TableProps) {
98
- const [open, setOpen] = useState(false);
99
- if (modal) {
100
- buttons = { ...buttons, Modal: "" };
101
- }
102
- const handleOpen = () => setOpen(true);
103
- const handleClose = () => {
104
- setOpen(false);
105
- setSelectedRows({
106
- type: "include",
107
- ids: new Set(),
108
- });
197
+ rows,
198
+ filteredRows,
199
+ }: ToolbarProps) {
200
+ const excel = useExcel();
201
+
202
+ const stripMeta = (r: GridValidRowModel) => {
203
+ const { _edited, ...rest } = r;
204
+ return rest;
109
205
  };
110
206
 
111
- const excel = useExcel();
112
- const [rows, setRows] = useState<GridValidRowModel[]>(data);
113
- const [selectedRows, setSelectedRows] = useState<any>();
114
- useEffect(() => {
115
- setRows(data);
116
- }, [data]);
207
+ if (!exportName && !onSave && !onSelect) return null;
208
+
209
+ return (
210
+ <div className="flex gap-2 bg-gray-200 border shadow rounded p-2">
211
+ {exportName && (
212
+ <Button
213
+ className="bg-green-800 text-white"
214
+ onClick={() =>
215
+ excel.export(rows.map(stripMeta), `${exportName}.xlsx`)
216
+ }
217
+ >
218
+ Exportar
219
+ </Button>
220
+ )}
221
+ {onSelect ? (
222
+ <Button
223
+ disabled={filteredRows.length === 0}
224
+ color={filteredRows.length === 0 ? "white" : "primary"}
225
+ onClick={() => onSelect(filteredRows.map(stripMeta))}
226
+ >
227
+ Guardar Selección
228
+ </Button>
229
+ ) : (
230
+ onSave && (
231
+ <Button onClick={() => onSave(rows.map(stripMeta))}>Guardar</Button>
232
+ )
233
+ )}
234
+ </div>
235
+ );
236
+ }
237
+
238
+ // ─── useColumns ───────────────────────────────────────────────────────────────
239
+
240
+ function useColumns(
241
+ rows: GridValidRowModel[],
242
+ options: {
243
+ flex: number;
244
+ editableFields?: string[];
245
+ buttons?: Record<string, any>;
246
+ hideColumns: string[];
247
+ modal?: React.ReactNode;
248
+ handleRowUpdate: (row: GridRowModel) => GridRowModel;
249
+ onModalOpen: (row: GridValidRowModel) => void;
250
+ },
251
+ ) {
252
+ const {
253
+ flex,
254
+ editableFields,
255
+ buttons,
256
+ hideColumns,
257
+ modal,
258
+ handleRowUpdate,
259
+ onModalOpen,
260
+ } = options;
117
261
 
118
- const columns = useMemo(() => {
262
+ return useMemo(() => {
119
263
  if (!rows.length) return [];
120
264
 
121
- const arr = Object.keys(rows[0])
265
+ const cols = Object.keys(rows[0])
122
266
  .filter((key) => !key.startsWith("_") && !hideColumns.includes(key))
123
- .map((key, i) => ({
267
+ .map((key) => ({
124
268
  field: key,
125
269
  headerName: key,
126
270
  flex,
127
- editable: editableFields?.includes(key),
271
+ editable: editableFields?.includes(key) ?? false,
128
272
  type: typeof rows[0][key] === "number" ? "number" : "string",
129
273
  renderCell: buttons?.[key]
130
274
  ? (params: any) =>
131
275
  React.cloneElement(buttons[key], {
132
- className: (params?.className ?? "") + " m-auto text-xs ",
276
+ className: `${params?.className ?? ""} m-auto text-xs`,
133
277
  children: params?.row?.[key],
134
278
  onClick: (e: any) => {
135
279
  e.row = params?.row;
136
280
  if (buttons[key]?.props?.onClick) {
137
281
  const newVal = buttons[key].props.onClick(e);
138
- newVal && handleRowUpdate(newVal);
282
+ if (newVal) handleRowUpdate(newVal);
139
283
  }
140
284
  },
141
285
  })
@@ -143,7 +287,7 @@ function IHTable({
143
287
  }));
144
288
 
145
289
  if (modal) {
146
- arr.unshift({
290
+ cols.unshift({
147
291
  field: "Modal",
148
292
  headerName: "Modal",
149
293
  flex,
@@ -153,9 +297,7 @@ function IHTable({
153
297
  (
154
298
  <Button
155
299
  className="text-xs"
156
- onClick={() => {
157
- handleOpen();
158
- }}
300
+ onClick={() => onModalOpen(params.row)}
159
301
  icon={<EditIcon />}
160
302
  >
161
303
  {params?.row?.["Modal"]}
@@ -164,186 +306,151 @@ function IHTable({
164
306
  });
165
307
  }
166
308
 
167
- return arr;
309
+ return cols;
168
310
  }, [rows]);
311
+ }
169
312
 
170
- const handleRowUpdate = (newRow: GridRowModel) => {
171
- if (!newRow.id) throw new Error("Fila sin id");
172
- newRow._edited = true;
173
- setRows((prev) => prev.map((row) => (row.id === newRow.id ? newRow : row)));
174
- return newRow;
313
+ // ─── IHTable (array data) ─────────────────────────────────────────────────────
314
+
315
+ function IHTable({
316
+ data,
317
+ flex = 1,
318
+ editableFields,
319
+ onSave,
320
+ onSelect,
321
+ buttons,
322
+ exportName,
323
+ modal,
324
+ height = 510,
325
+ width = "100%",
326
+ header,
327
+ hideColumns = [],
328
+ footer = {},
329
+ }: TableProps) {
330
+ if (modal && onSelect)
331
+ throw new Error("Solo se puede usar modal o onSelect por separado");
332
+
333
+ const [open, setOpen] = useState(false);
334
+ const [rows, setRows] = useState<GridValidRowModel[]>(data);
335
+ const [selectedRows, setSelectedRows] = useState<any>({
336
+ type: "include",
337
+ ids: new Set(),
338
+ });
339
+ const [modalRow, setModalRow] = useState<GridValidRowModel | undefined>();
340
+
341
+ // Inject Modal button key if modal is provided
342
+ const resolvedButtons = modal ? { ...buttons, Modal: "" } : buttons;
343
+
344
+ useEffect(() => {
345
+ setRows(data);
346
+ }, [data]);
347
+
348
+ const handleModalOpen = (row: GridValidRowModel) => {
349
+ setModalRow(row);
350
+ setOpen(true);
351
+ };
352
+ const handleClose = () => {
353
+ setOpen(false);
354
+ setModalRow(undefined);
175
355
  };
176
356
 
177
- const ref = useRef<HTMLDialogElement>(null);
178
- const cat1: any = {
179
- 1: 110,
180
- 2: 160,
181
- 3: 215,
182
- 4: 266,
183
- 5: 316,
357
+ const handleRowUpdate = (newRow: GridRowModel) => {
358
+ if (!newRow.id) throw new Error("Fila sin id");
359
+ const updated = { ...newRow, _edited: true } as any;
360
+ setRows((prev) =>
361
+ prev.map((row) => (row.id === updated.id ? updated : row)),
362
+ );
363
+ return updated;
184
364
  };
185
365
 
186
- const filtered = useMemo(() => {
187
- let filtered: any[] = [];
188
-
189
- if (selectedRows?.type == "exclude") {
190
- filtered = rows.filter(
191
- (r) => !Array.from(selectedRows.ids).includes(r.id),
192
- );
193
- } else if (selectedRows?.type == "include") {
194
- filtered = rows.filter((r) =>
195
- Array.from(selectedRows.ids).includes(r.id),
196
- );
366
+ const filteredRows = useMemo(() => {
367
+ if (selectedRows?.type === "exclude") {
368
+ return rows.filter((r) => !Array.from(selectedRows.ids).includes(r.id));
197
369
  }
198
- return filtered;
199
- }, [selectedRows]);
370
+ if (selectedRows?.type === "include") {
371
+ return rows.filter((r) => Array.from(selectedRows.ids).includes(r.id));
372
+ }
373
+ return [];
374
+ }, [selectedRows, rows]);
375
+
376
+ const columns = useColumns(rows, {
377
+ flex,
378
+ editableFields,
379
+ buttons: resolvedButtons,
380
+ hideColumns,
381
+ modal,
382
+ handleRowUpdate,
383
+ onModalOpen: handleModalOpen,
384
+ });
385
+
386
+ if (!rows.length) return null;
200
387
 
201
388
  return (
202
- rows.length > 0 && (
203
- <Box
204
- sx={{
205
- display: "flex",
206
- flexDirection: "column",
207
- height: cat1?.[height] || height,
208
- width,
209
- zIndex: 999999999,
210
- }}
211
- >
212
- {modal && (
213
- <Dialog open={open} onClose={handleClose} maxWidth="xl" fullWidth>
214
- <div className="flex justify-end">
215
- <button
216
- onClick={() => {
217
- handleClose();
218
- }}
219
- className="text-gray-500 hover:text-gray-800 text-xl font-bold p-5"
220
- >
221
- &times;
222
- </button>
223
- </div>
224
- <div className="mt-4 m-auto p-5">
225
- {selectedRows &&
226
- Array.from(selectedRows?.ids).length > 0 &&
227
- React.cloneElement(modal as any, {
228
- row: rows.find((r) =>
229
- Array.from(selectedRows?.ids).includes(r.id),
230
- ),
231
- })}
232
- </div>
233
- </Dialog>
234
- )}
235
- {header && (
236
- <div className="font-bold text-xl p-2 bg-blue-500 text-white border shadow rounded">
237
- {header}
238
- </div>
239
- )}
240
- {(exportName || onSave || onSelect) && (
241
- <div className="flex gap-2 bg-gray-200 border shadow rounded p-2">
242
- {exportName && (
243
- <Button
244
- className="bg-green-800 text-white"
245
- onClick={(e) => {
246
- excel.export(
247
- rows.map(({ _edited, ...r }) => r),
248
- `${exportName}.xlsx`,
249
- );
250
- }}
251
- >
252
- Exportar
253
- </Button>
254
- )}
255
- {onSelect ? (
256
- <Button
257
- disabled={filtered.length == 0}
258
- color={filtered.length == 0 ? "white" : "primary"}
259
- onClick={(e) => {
260
- onSelect?.(filtered.map(({ _edited, ...r }) => r));
261
- }}
262
- >
263
- Guardar Selección
264
- </Button>
265
- ) : (
266
- onSave && (
267
- <Button
268
- onClick={(e) => {
269
- onSave?.(rows.map(({ _edited, ...r }) => r));
270
- }}
271
- >
272
- Guardar
273
- </Button>
274
- )
275
- )}
276
- </div>
277
- )}
278
-
279
- <DataGrid
280
- rows={rows}
281
- columns={columns as any}
282
- checkboxSelection={Boolean(onSelect)}
283
- rowSelectionModel={selectedRows}
284
- onRowSelectionModelChange={(newSelection) => {
285
- setSelectedRows(newSelection);
286
- }}
287
- sx={{
288
- "& .MuiDataGrid-cell--editable": {
289
- backgroundColor: "#c6d8f0",
290
- fontWeight: 500,
291
- },
292
- }}
293
- editMode="row"
294
- processRowUpdate={handleRowUpdate}
295
- pageSizeOptions={[5, 10]}
389
+ <Box
390
+ sx={{
391
+ display: "flex",
392
+ flexDirection: "column",
393
+ height: HEIGHT_MAP[rows.length] ?? height,
394
+ width,
395
+ zIndex: 999999999,
396
+ }}
397
+ >
398
+ {modal && (
399
+ <ModalDialog
400
+ open={open}
401
+ onClose={handleClose}
402
+ modal={modal}
403
+ selectedRow={modalRow}
296
404
  />
297
- <CustomFooter footer={footer || {}} rows={rows} />
298
- </Box>
299
- )
300
- );
301
- }
405
+ )}
302
406
 
303
- function CustomFooter({
304
- rows,
305
- footer,
306
- }: {
307
- rows: GridValidRowModel[];
308
- footer: FooterType;
309
- }) {
310
- const entries = Object.entries(footer);
311
- if (!entries.length) return null;
407
+ {header && (
408
+ <div className="font-bold text-xl p-2 bg-blue-500 text-white border shadow rounded">
409
+ {header}
410
+ </div>
411
+ )}
312
412
 
313
- const compute = (key: string, type: "sum" | "avg" | "count") => {
314
- const values = rows
315
- .map((r) => Number(r[key] ?? 0))
316
- .filter((v) => !isNaN(v));
317
- if (type === "sum") return values.reduce((acc, v) => acc + v, 0);
318
- if (type === "avg")
319
- return values.length
320
- ? values.reduce((acc, v) => acc + v, 0) / values.length
321
- : 0;
322
- if (type === "count") return values.length;
323
- return 0;
324
- };
413
+ <Toolbar
414
+ exportName={exportName}
415
+ onSave={onSave}
416
+ onSelect={onSelect}
417
+ rows={rows}
418
+ filteredRows={filteredRows}
419
+ />
325
420
 
326
- const label: Record<string, string> = {
327
- sum: "Suma",
328
- avg: "Promedio",
329
- count: "Conteo",
330
- };
421
+ <DataGrid
422
+ rows={rows}
423
+ columns={columns as any}
424
+ checkboxSelection={Boolean(onSelect)}
425
+ rowSelectionModel={selectedRows}
426
+ onRowSelectionModelChange={!modal ? setSelectedRows : undefined}
427
+ sx={{
428
+ "& .MuiDataGrid-cell--editable": {
429
+ backgroundColor: "#c6d8f0",
430
+ fontWeight: 500,
431
+ },
432
+ ...(rows.length <= Object.keys(HEIGHT_MAP).length && {
433
+ "& .MuiDataGrid-filler": {
434
+ display: "none",
435
+ },
436
+ }),
437
+ }}
438
+ editMode="row"
439
+ processRowUpdate={handleRowUpdate}
440
+ pageSizeOptions={[5, 10]}
441
+ hideFooter={rows.length <= Object.keys(HEIGHT_MAP).length}
442
+ />
331
443
 
332
- return (
333
- <div className="flex justify-end gap-6 px-4 py-2 bg-gray-100 border-t border-gray-300 text-sm font-semibold text-gray-700">
334
- {entries.map(([key, type]) => {
335
- const value = compute(key, type);
336
- const formatted =
337
- type === "avg"
338
- ? value.toLocaleString(undefined, { maximumFractionDigits: 2 })
339
- : value.toLocaleString();
340
- return (
341
- <span key={key}>
342
- {label[type]} de <span className="text-gray-900">{key}</span>:{" "}
343
- <span className="text-blue-700">{formatted}</span>
344
- </span>
345
- );
346
- })}
347
- </div>
444
+ <CustomFooter footer={footer} rows={rows} />
445
+ </Box>
348
446
  );
349
447
  }
448
+
449
+ // ─── Public export ────────────────────────────────────────────────────────────
450
+
451
+ export default function Table(props: TableProps) {
452
+ if (Array.isArray(props.data)) {
453
+ return <IHTable {...props} />;
454
+ }
455
+ return <KeyValueTable data={props.data} />;
456
+ }