@zauru-sdk/components 2.0.118 → 2.0.120

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.
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
2
2
  import { SelectField } from "../Form/SelectField/index.js";
3
3
  import { TextField } from "../Form/TextField/index.js";
4
4
  import { CheckBox } from "../Form/Checkbox/index.js";
5
- import { createModal } from "../Modal/index.js";
5
+ import { createItemModal, createModal } from "../Modal/index.js";
6
6
  import { Button } from "../Buttons/index.js";
7
7
  import {
8
8
  GenericDynamicTableColumn,
@@ -17,9 +17,10 @@ import { useFormContext } from "react-hook-form";
17
17
  import { ComponentError } from "../Alerts/index.js";
18
18
 
19
19
  export type FooterColumnConfig = {
20
- content: React.ReactNode;
20
+ content?: React.ReactNode;
21
21
  className?: string;
22
22
  name?: string;
23
+ cell?: (rows: RowDataType[]) => React.ReactNode;
23
24
  };
24
25
 
25
26
  type Props = {
@@ -31,26 +32,40 @@ type Props = {
31
32
  footerRow?: FooterColumnConfig[];
32
33
  thCSSProperties?: React.CSSProperties;
33
34
  thElementsClassName?: string;
35
+ /** Controla si se pueden o no editar los campos (oculta botones y/o deshabilita campos). */
34
36
  editable?: boolean;
37
+ /** Opciones de búsqueda. */
35
38
  searcheables?: SelectFieldOption[];
39
+ /** Activa o desactiva el “skeleton” de carga. */
36
40
  loading?: boolean;
41
+ /** Controla la paginación. */
37
42
  paginated?: boolean;
38
43
  defaultItemsPerPage?: number;
39
44
  itemsPerPageOptions?: number[];
45
+ /** Quita el color de fondo por defecto a las filas pares. */
40
46
  withoutBg?: boolean;
47
+ /** Orientación de los encabezados (`horizontal` o `vertical`). */
41
48
  orientation?: "horizontal" | "vertical";
49
+ /** Máximo número de filas permitidas al hacer clic en “agregar fila”. */
42
50
  maxRows?: number;
51
+ /** Muestra un diálogo de confirmación antes de eliminar una fila. */
43
52
  confirmDelete?: boolean;
53
+ /** Función personalizada para manejar el botón de “agregar fila”. */
44
54
  addRowButtonHandler?: (
45
55
  tableData: RowDataType[],
46
56
  setTableData: (data: RowDataType[]) => void
47
57
  ) => void;
58
+ /**
59
+ * Controla si todo el componente se renderiza en modo de solo lectura,
60
+ * es decir, sin permitir ningún tipo de interacción de edición o eliminación.
61
+ */
62
+ readOnly?: boolean;
48
63
  };
49
64
 
50
65
  const GenericDynamicTableErrorComponent = ({ name }: { name: string }) => {
51
66
  const {
52
67
  formState: { errors },
53
- } = useFormContext() || { formState: {} }; // Obtener el contexto solo si existe
68
+ } = useFormContext() || { formState: {} };
54
69
  const error = errors ? errors[name ?? "-1"] : undefined;
55
70
 
56
71
  return error ? (
@@ -76,6 +91,23 @@ const GenericDynamicTableErrorComponent = ({ name }: { name: string }) => {
76
91
  defaultValue={
77
92
  invoiceDetailsDefaultValue ?? [{ id: crypto.randomUUID() }]
78
93
  }
94
+ addRowButtonHandler={async (tableData, setTableData) => {
95
+ const selectedItem = await createItemModal(ecommerceItems, {
96
+ itemSize: {
97
+ width: "150px",
98
+ height: "150px",
99
+ },
100
+ });
101
+ if (selectedItem) {
102
+ setTableData([
103
+ ...tableData,
104
+ {
105
+ id: crypto.randomUUID(),
106
+ code: selectedItem.code,
107
+ },
108
+ ]);
109
+ }
110
+ }}
79
111
  columns={[
80
112
  {
81
113
  label: "Producto",
@@ -106,11 +138,23 @@ const GenericDynamicTableErrorComponent = ({ name }: { name: string }) => {
106
138
  }
107
139
  ]}
108
140
  footerRow={[
109
- { content: "Total", className: "text-left font-bold" },
110
- { content: calculateTotal(), className: "text-center" },
111
- { content: "", className: "text-center" }
141
+ {
142
+ name: "code",
143
+ content: "Total",
144
+ className: "text-left font-bold",
145
+ },
146
+ {
147
+ name: "total",
148
+ className: "text-left font-bold",
149
+ cell: (rows: any) => {
150
+ return `${rows.reduce((acc: number, row: any) => {
151
+ return acc + row.total;
152
+ }, 0)}`;
153
+ },
154
+ },
112
155
  ]}
113
156
  maxRows={2}
157
+ readOnly={false}
114
158
  />
115
159
  */
116
160
  export const GenericDynamicTable = (props: Props) => {
@@ -123,6 +167,7 @@ export const GenericDynamicTable = (props: Props) => {
123
167
  thCSSProperties,
124
168
  thElementsClassName = "",
125
169
  editable = true,
170
+ readOnly = false, // Nuevo prop
126
171
  searcheables = [],
127
172
  loading = false,
128
173
  paginated = true,
@@ -136,6 +181,12 @@ export const GenericDynamicTable = (props: Props) => {
136
181
  addRowButtonHandler,
137
182
  } = props;
138
183
 
184
+ /**
185
+ * Definimos una variable interna para saber si los campos son
186
+ * efectivamente editables: solo si `editable` es true y `readOnly` es false.
187
+ */
188
+ const isEditable = editable && !readOnly;
189
+
139
190
  try {
140
191
  const [tableData, setTableData] = useState<RowDataType[]>(defaultValue);
141
192
  const [deletedData, setDeletedData] = useState<RowDataType[]>([]);
@@ -149,6 +200,7 @@ export const GenericDynamicTable = (props: Props) => {
149
200
  if (defaultValue.length) {
150
201
  setTableData(defaultValue);
151
202
  }
203
+ // eslint-disable-next-line react-hooks/exhaustive-deps
152
204
  }, []);
153
205
 
154
206
  useEffect(() => {
@@ -157,6 +209,7 @@ export const GenericDynamicTable = (props: Props) => {
157
209
 
158
210
  useEffect(() => {
159
211
  changeFilteredData();
212
+ // eslint-disable-next-line react-hooks/exhaustive-deps
160
213
  }, [tableData, search]);
161
214
 
162
215
  const totalPages = () => {
@@ -170,11 +223,11 @@ export const GenericDynamicTable = (props: Props) => {
170
223
  const defs: { [key: string]: any } = {};
171
224
  columns.forEach((x) => {
172
225
  defs[`${x.name}`] =
173
- x.type == "label" || x.type == "textField"
226
+ x.type === "label" || x.type === "textField"
174
227
  ? ""
175
- : x.type == "selectField"
228
+ : x.type === "selectField"
176
229
  ? 0
177
- : x.type == "checkbox"
230
+ : x.type === "checkbox"
178
231
  ? false
179
232
  : 0;
180
233
  });
@@ -187,9 +240,13 @@ export const GenericDynamicTable = (props: Props) => {
187
240
  const removeRow = (rowId: string) => {
188
241
  const newDeletedData = [...deletedData];
189
242
  const deletedItem = tableData?.find((x) => x.id === rowId);
243
+
244
+ // Si la fila tenía un "id" no numérico (ej. generamos un UUID al vuelo),
245
+ // igual se procede a eliminar, aunque no se guarde en "deletedData".
190
246
  if (deletedItem && !isNaN(deletedItem.id)) {
191
247
  newDeletedData.push(deletedItem);
192
248
  }
249
+
193
250
  setDeletedData(newDeletedData);
194
251
  setTableData((prevData) => prevData?.filter((x) => x.id !== rowId));
195
252
  };
@@ -208,12 +265,16 @@ export const GenericDynamicTable = (props: Props) => {
208
265
  };
209
266
 
210
267
  const renderHeader = () => {
268
+ const rendereableColumns = columns.filter(
269
+ (column) => column.type !== "hidden"
270
+ );
211
271
  if (orientation === "horizontal") {
212
272
  return (
213
273
  <tr style={{ ...thCSSProperties }}>
214
- {columns.map((column, index) => {
274
+ {rendereableColumns.map((column, index) => {
215
275
  const ancho =
216
- column.width ?? (editable ? 94 : 100) / (columns.length ?? 1);
276
+ column.width ??
277
+ (isEditable ? 94 : 100) / (rendereableColumns.length ?? 1);
217
278
  return (
218
279
  <th
219
280
  key={index}
@@ -226,7 +287,7 @@ export const GenericDynamicTable = (props: Props) => {
226
287
  </th>
227
288
  );
228
289
  })}
229
- {editable && <th style={{ width: "4%" }}></th>}
290
+ {isEditable && <th style={{ width: "4%" }}></th>}
230
291
  </tr>
231
292
  );
232
293
  } else {
@@ -235,6 +296,10 @@ export const GenericDynamicTable = (props: Props) => {
235
296
  };
236
297
 
237
298
  const renderRow = (rowData: RowDataType, index: number) => {
299
+ const rendereableColumns = columns.filter(
300
+ (column) => column.type !== "hidden"
301
+ );
302
+
238
303
  if (orientation === "horizontal") {
239
304
  return (
240
305
  <tr
@@ -243,12 +308,13 @@ export const GenericDynamicTable = (props: Props) => {
243
308
  index % 2 === 0 ? `${withoutBg ? "" : "bg-gray-200"}` : ""
244
309
  }
245
310
  >
246
- {columns.map((column) => renderCell(rowData, column))}
247
- {editable && renderDeleteButton(rowData)}
311
+ {rendereableColumns.map((column) => renderCell(rowData, column))}
312
+ {isEditable && renderDeleteButton(rowData)}
248
313
  </tr>
249
314
  );
250
315
  } else {
251
- return columns.map((column) => (
316
+ // Orientación vertical
317
+ return rendereableColumns.map((column) => (
252
318
  <tr
253
319
  key={`${rowData.id}-${column.name}`}
254
320
  className={
@@ -263,8 +329,8 @@ export const GenericDynamicTable = (props: Props) => {
263
329
  {column.label}
264
330
  </th>
265
331
  {renderCell(rowData, column)}
266
- {editable &&
267
- column === columns[columns.length - 1] &&
332
+ {isEditable &&
333
+ column === rendereableColumns[rendereableColumns.length - 1] &&
268
334
  renderDeleteButton(rowData)}
269
335
  </tr>
270
336
  ));
@@ -285,6 +351,11 @@ export const GenericDynamicTable = (props: Props) => {
285
351
  </td>
286
352
  );
287
353
  }
354
+
355
+ if (column.type === "hidden") {
356
+ return <></>;
357
+ }
358
+
288
359
  const tempVal = rowData[column.name as any];
289
360
 
290
361
  const defaultVal =
@@ -292,6 +363,23 @@ export const GenericDynamicTable = (props: Props) => {
292
363
  ? column.options?.find((x) => x.value === tempVal)
293
364
  : tempVal;
294
365
 
366
+ // Solo lectura: en este caso mostramos el valor como label
367
+ if (readOnly) {
368
+ return (
369
+ <td
370
+ key={`${rowData.id}-${column.name}`}
371
+ className={`align-middle p-1 ${column.cellClassName || ""}`}
372
+ >
373
+ <div>
374
+ {column.cell
375
+ ? column.cell(rowData)
376
+ : defaultVal?.label ?? tempVal}
377
+ </div>
378
+ </td>
379
+ );
380
+ }
381
+
382
+ // Modo normal
295
383
  if (column.type === "label") {
296
384
  return (
297
385
  <td
@@ -303,6 +391,7 @@ export const GenericDynamicTable = (props: Props) => {
303
391
  );
304
392
  }
305
393
 
394
+ // Determinamos el componente que usaremos según "type"
306
395
  const FieldComponent =
307
396
  column.type === "textField"
308
397
  ? TextField
@@ -327,7 +416,8 @@ export const GenericDynamicTable = (props: Props) => {
327
416
  name={`${rowData.id}-${column.name}`}
328
417
  type={column.textFieldType}
329
418
  integer={!!column.integer}
330
- disabled={column.disabled}
419
+ /** Se deshabilita si la columna lo exige o si la tabla está en modo no editable */
420
+ disabled={column.disabled || !isEditable}
331
421
  isClearable
332
422
  onChange={(value: any) => {
333
423
  const sendValue = value?.value ?? value;
@@ -381,6 +471,7 @@ export const GenericDynamicTable = (props: Props) => {
381
471
  currentPage * itemsPerPage
382
472
  );
383
473
 
474
+ // Si estamos cargando, mostramos celdas skeleton
384
475
  if (loading) {
385
476
  mapeable = [
386
477
  { id: 1 },
@@ -461,14 +552,14 @@ export const GenericDynamicTable = (props: Props) => {
461
552
  .map((x) => x.label)
462
553
  .join(", ")}`}
463
554
  onChange={handleChangeSearch}
464
- disabled={loading}
555
+ disabled={loading || readOnly}
465
556
  />
466
557
  </div>
467
558
  )}
468
559
  <table className="w-full">
469
560
  {orientation === "horizontal" && <thead>{renderHeader()}</thead>}
470
561
  <tbody>{renderRows()}</tbody>
471
- {editable && (
562
+ {isEditable && (
472
563
  <tfoot>
473
564
  <tr>
474
565
  <td
@@ -503,23 +594,33 @@ export const GenericDynamicTable = (props: Props) => {
503
594
  {footerRow && (
504
595
  <tfoot className="border-t-2 border-black">
505
596
  <tr>
506
- {columns.map((column, index) => {
507
- const footerCell = footerRow.find(
508
- (fc) => fc.name === column.name
509
- );
510
- return (
511
- <td
512
- key={index}
513
- colSpan={orientation === "vertical" ? 2 : 1}
514
- className={`align-middle ${
515
- footerCell?.className || ""
516
- }`}
517
- >
518
- {footerCell ? footerCell.content : <></>}
519
- </td>
520
- );
521
- })}
522
- {editable && <td></td>}
597
+ {columns
598
+ .filter((column) => column.type !== "hidden")
599
+ .map((column, index) => {
600
+ const footerCell = footerRow.find(
601
+ (fc) => fc.name === column.name
602
+ );
603
+ return (
604
+ <td
605
+ key={index}
606
+ colSpan={orientation === "vertical" ? 2 : 1}
607
+ className={`align-middle ${
608
+ footerCell?.className || ""
609
+ }`}
610
+ >
611
+ {footerCell ? (
612
+ footerCell.cell ? (
613
+ footerCell.cell(tableData)
614
+ ) : (
615
+ footerCell.content
616
+ )
617
+ ) : (
618
+ <></>
619
+ )}
620
+ </td>
621
+ );
622
+ })}
623
+ {isEditable && <td></td>}
523
624
  </tr>
524
625
  </tfoot>
525
626
  )}
@@ -529,7 +630,7 @@ export const GenericDynamicTable = (props: Props) => {
529
630
  <div className="flex items-center">
530
631
  <Button
531
632
  type="button"
532
- disabled={currentPage === 1}
633
+ disabled={currentPage === 1 || readOnly}
533
634
  onClickSave={() =>
534
635
  setCurrentPage((old) => Math.max(old - 1, 1))
535
636
  }
@@ -539,7 +640,7 @@ export const GenericDynamicTable = (props: Props) => {
539
640
  <span className="mx-2">{`Página ${currentPage} de ${totalPages()}`}</span>
540
641
  <Button
541
642
  type="button"
542
- disabled={currentPage === totalPages()}
643
+ disabled={currentPage === totalPages() || readOnly}
543
644
  onClickSave={() =>
544
645
  setCurrentPage((old) => Math.min(old + 1, totalPages()))
545
646
  }
@@ -19,9 +19,36 @@ import { getDepSelectOptions, getMunSelectOptions } from "@zauru-sdk/common";
19
19
  import { StaticAlert } from "../../Alerts/index.js";
20
20
  import { SubContainer } from "../../Containers/index.js";
21
21
  import { LineSeparator } from "../../LineSeparator/index.js";
22
+ import { z } from "zod";
23
+ export const getDynamicBaculoFormSchema = (
24
+ form?: FormGraphQL,
25
+ extraFieldValidations: { [key: string]: any } = {}
26
+ ) => {
27
+ if (!form) {
28
+ return z.any();
29
+ }
30
+
31
+ let fieldValidations = { ...extraFieldValidations };
32
+ form.settings_form_fields.forEach((field) => {
33
+ if (field.required) {
34
+ if (field.field_type === "yes_no") {
35
+ //se ignora la validación
36
+ } else {
37
+ // Si el campo es requerido, se debe tener al menos un carácter
38
+ fieldValidations = {
39
+ ...fieldValidations,
40
+ [`${field.form_id}_${field.id}`]: z.coerce
41
+ .string()
42
+ .min(1, `Este campo es requerido.`),
43
+ };
44
+ }
45
+ }
46
+ });
47
+
48
+ return z.object(fieldValidations).passthrough(); // Iniciar con un esquema que deja pasar todo.
49
+ };
22
50
 
23
51
  type Props = {
24
- formName?: string;
25
52
  form?: FormGraphQL;
26
53
  options?: { showTitle: boolean; showDescription: boolean };
27
54
  defaultValues?: FormSubmissionValueGraphQL[];
@@ -34,7 +61,6 @@ export function DynamicBaculoForm(props: Props) {
34
61
  const {
35
62
  form,
36
63
  options = { showDescription: false, showTitle: false },
37
- formName = "",
38
64
  namesStr = "",
39
65
  defaultValues = [],
40
66
  showingRules = [],
@@ -45,7 +71,7 @@ export function DynamicBaculoForm(props: Props) {
45
71
  return (
46
72
  <StaticAlert
47
73
  title="No se encontró el formulario dinámico"
48
- description={`Ocurrió un error encontrando el formulario para ${formName}, contacte al administrador con este mensaje de error.`}
74
+ description={`Ocurrió un error encontrando el formulario, contacte al administrador con este mensaje de error.`}
49
75
  type="info"
50
76
  />
51
77
  );
@@ -111,7 +137,7 @@ export function DynamicBaculoForm(props: Props) {
111
137
  title={`${field.required ? "*" : ""}${field.name}`}
112
138
  name={`${namesStr}${field.form_id}_${field.id}`}
113
139
  hint={field.hint}
114
- disabled={readOnly}
140
+ readOnly={readOnly}
115
141
  defaultValue={defaultValue?.value}
116
142
  download={true}
117
143
  />
@@ -125,7 +151,7 @@ export function DynamicBaculoForm(props: Props) {
125
151
  hint={field.hint}
126
152
  showAvailableTypes
127
153
  fileTypes={["png", "jpg", "jpeg"]}
128
- disabled={readOnly}
154
+ readOnly={readOnly}
129
155
  defaultValue={defaultValue?.value}
130
156
  />
131
157
  );
@@ -138,7 +164,7 @@ export function DynamicBaculoForm(props: Props) {
138
164
  hint={field.hint}
139
165
  showAvailableTypes
140
166
  fileTypes={["pdf"]}
141
- disabled={readOnly}
167
+ readOnly={readOnly}
142
168
  defaultValue={defaultValue?.value}
143
169
  download={true}
144
170
  />