@axzydev/axzy_ui_system 1.7.0 → 1.7.2

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.
Files changed (54) hide show
  1. package/dist/index.cjs +1938 -491
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.cts +739 -3
  5. package/dist/index.d.ts +739 -3
  6. package/dist/index.js +1830 -417
  7. package/dist/index.js.map +1 -1
  8. package/dist/layered.css +1 -1
  9. package/package.json +14 -7
  10. package/src/App.tsx +60 -2
  11. package/src/components/atoms/image/image.props.ts +2 -0
  12. package/src/components/atoms/image/image.tsx +3 -2
  13. package/src/components/atoms/segmented-control/segmented-control.tsx +3 -0
  14. package/src/components/molecules/card/card.tsx +12 -0
  15. package/src/components/molecules/stat-card/stat-card.tsx +10 -0
  16. package/src/components/organisms/dialog/dialog.props.ts +2 -0
  17. package/src/components/organisms/dialog/dialog.tsx +4 -3
  18. package/src/dev.css +6 -0
  19. package/src/index.ts +90 -1
  20. package/src/json-renderer/catalog/axzyCatalog.ts +362 -0
  21. package/src/json-renderer/catalog/catalog.props.ts +128 -0
  22. package/src/json-renderer/catalog/resolve.ts +225 -0
  23. package/src/json-renderer/index.ts +70 -0
  24. package/src/json-renderer/kernel/NodeBoundary.tsx +53 -0
  25. package/src/json-renderer/kernel/SchemaRenderer.tsx +197 -0
  26. package/src/json-renderer/kernel/kernel.test.tsx +341 -0
  27. package/src/json-renderer/kernel/partial.ts +92 -0
  28. package/src/json-renderer/kernel/renderNode.tsx +113 -0
  29. package/src/json-renderer/kernel/schemaRenderer.props.ts +42 -0
  30. package/src/json-renderer/kernel/types.ts +159 -0
  31. package/src/json-renderer/shared/errors.ts +92 -0
  32. package/src/json-renderer/shared/limits.ts +87 -0
  33. package/src/json-renderer/shared/paths.ts +66 -0
  34. package/src/json-renderer/shared/shared.test.ts +108 -0
  35. package/src/json-renderer/stream/tolerantParse.test.ts +152 -0
  36. package/src/json-renderer/stream/tolerantParse.ts +449 -0
  37. package/src/json-renderer/test/setup.ts +38 -0
  38. package/src/json-renderer/validation/migrate.ts +83 -0
  39. package/src/json-renderer/validation/validate.ts +161 -0
  40. package/src/json-renderer/validation/validation.test.ts +165 -0
  41. package/src/json-renderer/vocabulary/buildContract.ts +158 -0
  42. package/src/sandbox/axzyConfig.test.ts +40 -0
  43. package/src/sandbox/axzyConfig.ts +77 -0
  44. package/src/sandbox/i18n.ts +218 -0
  45. package/src/sandbox/navigation.ts +1 -0
  46. package/src/sandbox/useHashRoute.ts +39 -9
  47. package/src/showcases/DemoJsonDashboard.tsx +489 -0
  48. package/src/showcases/DemoJsonDocs.tsx +339 -0
  49. package/src/showcases/DemoJsonPortfolio.tsx +257 -0
  50. package/src/showcases/JsonScreenShowcase.tsx +341 -0
  51. package/src/showcases/demoApp/demoData.ts +181 -0
  52. package/src/showcases/demoApp/nodeTree.test.ts +39 -0
  53. package/src/showcases/demoApp/nodeTree.ts +35 -0
  54. package/src/showcases/demoApp/schemas.test.ts +54 -0
@@ -0,0 +1,489 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { FaBook, FaBoxOpen, FaChartBar, FaSave, FaShoppingCart, FaTrash, FaUsers } from "react-icons/fa";
3
+ import { FiHome } from "react-icons/fi";
4
+ import { AxzyLogo } from "../sandbox/AxzyLogo";
5
+ import {
6
+ ITAlert,
7
+ ITButton,
8
+ ITDialog,
9
+ ITFlex,
10
+ ITGrid,
11
+ ITInput,
12
+ ITLayout,
13
+ ITLoader,
14
+ ITPage,
15
+ ITSchemaRenderer,
16
+ ITStack,
17
+ ITText,
18
+ ITToast,
19
+ ITTripleFilter,
20
+ ITDropfile,
21
+ ITDatePicker,
22
+ ITTimePicker,
23
+ ITSelect,
24
+ ITSearchSelect,
25
+ ITInputNumber,
26
+ parsePartialSchema,
27
+ useITTranslation,
28
+ validateSchema,
29
+ type JsonEngineIssue,
30
+ type NodeSchema,
31
+ type SchemaValue,
32
+ type ScreenSchema,
33
+ } from "../index";
34
+ import { DemoJsonDocs } from "./DemoJsonDocs";
35
+ import {
36
+ actualizarUsuario,
37
+ crearUsuario,
38
+ createDemoFetcher,
39
+ setEstadoFiltro,
40
+ eliminarUsuario,
41
+ isDemoSource,
42
+ obtenerUsuario,
43
+ type DemoUsuario,
44
+ type DemoUsuarioInput,
45
+ } from "./demoApp/demoData";
46
+
47
+ /** Páginas del dashboard, en orden de navegación. */
48
+ export const DEMO_APP_PAGES = ["inicio", "usuarios", "pedidos", "productos", "reportes", "docs"] as const;
49
+
50
+ /** Nombre de una página del dashboard. */
51
+ export type DemoAppPage = (typeof DEMO_APP_PAGES)[number];
52
+
53
+ /** Clave de i18n del rótulo de cada página. */
54
+ const PAGE_LABEL: Record<DemoAppPage, string> = {
55
+ inicio: "sandbox.app.nav.inicio",
56
+ usuarios: "sandbox.app.nav.usuarios",
57
+ pedidos: "sandbox.app.nav.pedidos",
58
+ productos: "sandbox.app.nav.productos",
59
+ reportes: "sandbox.app.nav.reportes",
60
+ docs: "sandbox.app.nav.docs",
61
+ };
62
+
63
+ /** Registro de iconos: el JSON nombra, el host decide qué componente es. */
64
+ const ICONOS: Record<string, React.ReactNode> = {
65
+ home: <FiHome size={14} />,
66
+ users: <FaUsers size={13} />,
67
+ cart: <FaShoppingCart size={13} />,
68
+ box: <FaBoxOpen size={13} />,
69
+ chart: <FaChartBar size={13} />,
70
+ book: <FaBook size={12} />,
71
+ };
72
+
73
+ /** Módulo del sidebar tal como lo declara `layout.json`. */
74
+ interface LayoutModule {
75
+ id: string;
76
+ label: string | { i18n: string };
77
+ icon?: string;
78
+ page: string;
79
+ badge?: number | string;
80
+ subitems?: { id: string; label: string | { i18n: string }; page: string; query?: Record<string, string> }[];
81
+ }
82
+
83
+ /** Shell declarado en JSON. */
84
+ interface LayoutFile {
85
+ layout: { brand?: { logo?: string; text?: string }; topBarNav?: string[]; modules: LayoutModule[] };
86
+ }
87
+
88
+ /** Estado del formulario de usuario. */
89
+ const ROLES = ["Admin", "Operador", "Auditor", "Soporte"];
90
+ const ESTADOS = ["Activo", "Inactivo"];
91
+ const EMPRESAS = ["CYC ERP", "Puerto Nuevo", "CheckApp", "Grupo Delta", "Nova Salud", "Ferretería Sur"];
92
+ const FORM_VACIO: DemoUsuarioInput = { nombre: "", correo: "", rol: "Operador", estado: "Activo", edad: 30, empresa: "CheckApp", turno: "09:00", alta: new Date().toISOString().slice(0, 10) };
93
+
94
+ /** `true` cuando el valor es una referencia de fuente (`{ $source }`). */
95
+ function isSourceRef(value: unknown): value is { $source: string } {
96
+ return Boolean(value) && typeof value === "object" && typeof (value as { $source?: unknown }).$source === "string";
97
+ }
98
+
99
+ /** `true` cuando el valor es una clave de traducción (`{ i18n }`). */
100
+ function isI18nRef(value: unknown): value is { i18n: string; params?: Record<string, SchemaValue> } {
101
+ return Boolean(value) && typeof value === "object" && typeof (value as { i18n?: unknown }).i18n === "string";
102
+ }
103
+
104
+ /** Props del dashboard. */
105
+ export interface DemoJsonDashboardProps {
106
+ /** Página activa. */
107
+ page: DemoAppPage;
108
+ /** Navega a otra página del dashboard. */
109
+ onNavigate: (page: DemoAppPage) => void;
110
+ /** Vuelve al portafolio de demostración. */
111
+ onExit: () => void;
112
+ /** Filtro de estado leído de la URL (`?estado=`). */
113
+ filterEstado: string;
114
+ /** Escribe el filtro en la URL. */
115
+ onFilterChange: (estado: string) => void;
116
+ }
117
+
118
+ /**
119
+ * Sistema de demostración: `ITLayout` + `ITPage` como shell, páginas en JSON y
120
+ * un CRUD real sobre `ITDataTable`.
121
+ *
122
+ * El host hace seis cosas:
123
+ * 1. Carga el JSON de la página (`/demo/app/<página>.json`).
124
+ * 2. Lo valida y muestra el veredicto.
125
+ * 3. Resuelve textos (`{ i18n }`).
126
+ * 4. Resuelve fuentes de datos (`{ $source }` → `fetchData`).
127
+ * 5. Resuelve acciones por nombre (`ir-pagina`, `nuevo-registro`, `editar-usuario`).
128
+ * 6. Guarda: el formulario es del host, porque escribir es autoridad del host.
129
+ *
130
+ * @example
131
+ * // Ruta: #demo/app/usuarios
132
+ * <DemoJsonDashboard page="usuarios" onNavigate={setPage} onExit={goToDemo} />
133
+ */
134
+ export const DemoJsonDashboard: React.FC<DemoJsonDashboardProps> = ({ page, onNavigate, onExit, filterEstado, onFilterChange }) => {
135
+ const { t } = useITTranslation();
136
+ const [esquema, setEsquema] = useState<ScreenSchema | null>(null);
137
+ const [error, setError] = useState<string | null>(null);
138
+ const [avisos, setAvisos] = useState<JsonEngineIssue[]>([]);
139
+
140
+ // CRUD: el esquema pide; el host escribe.
141
+ const [formulario, setFormulario] = useState<DemoUsuarioInput | null>(null);
142
+ // El sidebar se colapsa solo en pantallas estrechas y se puede alternar.
143
+ const [colapsado, setColapsado] = useState(false);
144
+ const [filtroEstado, setFiltroEstado] = useState<string>(filterEstado);
145
+ // La URL manda: si cambia el `?estado=`, la tabla se ajusta.
146
+ useEffect(() => {
147
+ setFiltroEstado(filterEstado);
148
+ }, [filterEstado]);
149
+ const [shell, setShell] = useState<LayoutFile["layout"] | null>(null);
150
+ const [aviso, setAviso] = useState<{ mensaje: string; tipo: "success" | "danger" } | null>(null);
151
+ const [editandoId, setEditandoId] = useState<number | null>(null);
152
+ const [version, setVersion] = useState(0);
153
+
154
+ const pagina: DemoAppPage = DEMO_APP_PAGES.includes(page) ? page : "inicio";
155
+
156
+ /** Auto-colapso: en cuanto el ancho baja de 1100px, el rail pasa a iconos. */
157
+ useEffect(() => {
158
+ if (typeof window === "undefined") return;
159
+ const media = window.matchMedia("(max-width: 1100px)");
160
+ const aplicar = () => setColapsado(media.matches);
161
+ aplicar();
162
+ media.addEventListener("change", aplicar);
163
+ return () => media.removeEventListener("change", aplicar);
164
+ }, []);
165
+
166
+ /** Muestra un aviso y lo retira solo. */
167
+ const toast = useCallback((mensaje: string, tipo: "success" | "danger" = "success") => {
168
+ setAviso({ mensaje, tipo });
169
+ window.setTimeout(() => setAviso(null), 3000);
170
+ }, []);
171
+ const url = `/demo/app/${pagina}.json`;
172
+
173
+ /** 0. El shell también es JSON: se carga una vez. */
174
+ useEffect(() => {
175
+ fetch("/demo/app/layout.json")
176
+ .then((r) => r.json() as Promise<LayoutFile>)
177
+ .then((archivo) => setShell(archivo.layout))
178
+ .catch(() => setShell(null));
179
+ }, []);
180
+
181
+ /** 1. Carga del JSON de la página. */
182
+ useEffect(() => {
183
+ let cancelado = false;
184
+ setEsquema(null);
185
+ setError(null);
186
+ setAvisos([]);
187
+
188
+ fetch(url)
189
+ .then((respuesta) => {
190
+ if (!respuesta.ok) throw new Error(`HTTP ${respuesta.status}`);
191
+ return respuesta.text();
192
+ })
193
+ .then((texto) => {
194
+ if (!cancelado) setEsquema(parsePartialSchema(texto).tree);
195
+ })
196
+ .catch((fallo: unknown) => {
197
+ if (!cancelado) setError(fallo instanceof Error ? fallo.message : String(fallo));
198
+ });
199
+
200
+ return () => {
201
+ cancelado = true;
202
+ };
203
+ }, [url]);
204
+
205
+ const informe = useMemo(() => (esquema ? validateSchema(esquema) : null), [esquema]);
206
+
207
+ /** Abre el formulario de usuario (alta o edición). */
208
+ const abrirFormulario = useCallback((usuario?: DemoUsuario) => {
209
+ if (usuario) {
210
+ setEditandoId(usuario.id);
211
+ setFormulario({ nombre: usuario.nombre, correo: usuario.correo, rol: usuario.rol, estado: usuario.estado, edad: usuario.edad, empresa: usuario.empresa, turno: usuario.turno, alta: usuario.alta });
212
+ } else {
213
+ setEditandoId(null);
214
+ setFormulario({ ...FORM_VACIO });
215
+ }
216
+ }, []);
217
+
218
+ /** `ITInput` entrega el evento: se normaliza a string antes de guardarlo. */
219
+ const setCampo = useCallback(
220
+ (campo: keyof DemoUsuarioInput) => (evento: unknown) => {
221
+ const valor =
222
+ typeof evento === "string"
223
+ ? evento
224
+ : ((evento as { target?: { value?: string } } | null)?.target?.value ?? "");
225
+ setFormulario((previo) => (previo ? { ...previo, [campo]: valor } : previo));
226
+ },
227
+ []
228
+ );
229
+
230
+ const cerrarFormulario = useCallback(() => {
231
+ setFormulario(null);
232
+ setEditandoId(null);
233
+ }, []);
234
+
235
+ /** Guarda el formulario y remonta el esquema para refrescar la tabla. */
236
+ const guardar = useCallback(() => {
237
+ if (!formulario || !formulario.nombre.trim() || !formulario.correo.trim()) return;
238
+ if (editandoId === null) crearUsuario(formulario);
239
+ else actualizarUsuario(editandoId, formulario);
240
+ cerrarFormulario();
241
+ setVersion((n) => n + 1);
242
+ toast(t("sandbox.app.toast.saved"));
243
+ }, [formulario, editandoId, cerrarFormulario, t, toast]);
244
+
245
+ const borrar = useCallback(() => {
246
+ if (editandoId === null) return;
247
+ eliminarUsuario(editandoId);
248
+ cerrarFormulario();
249
+ setVersion((n) => n + 1);
250
+ toast(t("sandbox.app.toast.deleted"), "danger");
251
+ }, [editandoId, cerrarFormulario, t, toast]);
252
+
253
+ /** El filtro triple se aplica en el fetcher y se remonta la tabla. */
254
+ useEffect(() => {
255
+ setEstadoFiltro(pagina === "docs" ? null : filtroEstado);
256
+ }, [filtroEstado, pagina]);
257
+
258
+ /** Fetchers estables: uno por fuente, no uno por render. */
259
+ const fetchers = useMemo(
260
+ () => ({
261
+ usuarios: createDemoFetcher("usuarios"),
262
+ pedidos: createDemoFetcher("pedidos"),
263
+ productos: createDemoFetcher("productos"),
264
+ }),
265
+ []
266
+ );
267
+
268
+ /** 3 y 4. Textos y fuentes de datos. */
269
+ const resolveValue = useCallback(
270
+ (value: unknown): unknown => {
271
+ if (isSourceRef(value)) {
272
+ return isDemoSource(value.$source) ? fetchers[value.$source] : () => Promise.resolve({ data: [], total: 0 });
273
+ }
274
+ if (isI18nRef(value)) {
275
+ const params = value.params
276
+ ? Object.fromEntries(Object.entries(value.params).map(([clave, dato]) => [clave, String(dato)]))
277
+ : undefined;
278
+ return t(value.i18n, params);
279
+ }
280
+ return value;
281
+ },
282
+ [t, fetchers]
283
+ );
284
+
285
+ /** 5. Acciones por nombre. La navegación y la escritura son del host. */
286
+ const resolveAction = useCallback(
287
+ (node: NodeSchema, _prop: string, action: unknown) => {
288
+ const declarada = action as { action?: string; args?: Record<string, SchemaValue> } | undefined;
289
+ const nombre = declarada?.action;
290
+ if (!nombre) return undefined;
291
+
292
+ return (...args: unknown[]) => {
293
+ switch (nombre) {
294
+ case "ir-pagina": {
295
+ const destino = String(declarada?.args?.pagina ?? "");
296
+ if (DEMO_APP_PAGES.includes(destino as DemoAppPage)) onNavigate(destino as DemoAppPage);
297
+ return;
298
+ }
299
+ case "nuevo-registro":
300
+ abrirFormulario();
301
+ return;
302
+ case "editar-usuario": {
303
+ // La tabla pasa la fila como primer argumento.
304
+ const fila = args[0] as DemoUsuario | undefined;
305
+ const usuario = fila && typeof fila.id === "number" ? obtenerUsuario(fila.id) : undefined;
306
+ if (usuario) abrirFormulario(usuario);
307
+ return;
308
+ }
309
+ default:
310
+ return;
311
+ }
312
+ };
313
+ },
314
+ [onNavigate, abrirFormulario, pagina, onFilterChange]
315
+ );
316
+
317
+ const cuerpo = error ? (
318
+ <ITAlert variant="error" title={t("sandbox.app.loadError")}>
319
+ {`${url}: ${error}`}
320
+ </ITAlert>
321
+ ) : !esquema ? (
322
+ <ITFlex align="center" justify="center" gap={3} className="min-h-[320px]">
323
+ <ITLoader variant="spinner" size="md" color="primary" />
324
+ <ITText as="span" muted>
325
+ {t("sandbox.app.loading")}
326
+ </ITText>
327
+ </ITFlex>
328
+ ) : (
329
+ <ITStack direction="column" spacing={5}>
330
+ <ITFlex align="center" justify="between" wrap="wrap" gap={2}>
331
+ <ITText as="span" muted className="font-mono !text-[10px] uppercase !tracking-[0.12em]">
332
+ {`${url} · ${informe?.verdict ?? "?"} · ${informe?.warnings.length ?? 0} avisos`}
333
+ </ITText>
334
+ </ITFlex>
335
+ {[...(informe?.errors ?? []), ...(informe?.warnings ?? []), ...avisos].slice(0, 4).map((aviso, indice) => (
336
+ <ITText key={`${aviso.code}-${indice}`} as="span" className="font-mono !text-[10px] text-amber-700 dark:text-amber-400">
337
+ {`${aviso.code} ${aviso.path}: ${aviso.message}`}
338
+ </ITText>
339
+ ))}
340
+ <ITSchemaRenderer
341
+ key={version}
342
+ schema={esquema}
343
+ resolveValue={resolveValue}
344
+ resolveAction={resolveAction}
345
+ onIssue={(aviso) =>
346
+ setAvisos((previos) =>
347
+ previos.some((item) => item.code === aviso.code && item.path === aviso.path) ? previos : [...previos, aviso]
348
+ )
349
+ }
350
+ />
351
+ </ITStack>
352
+ );
353
+
354
+ return (
355
+ <>
356
+ <ITLayout
357
+ topBar={{
358
+ logo: <AxzyLogo variante="icono" tamano={20} />,
359
+ logoText: shell?.brand?.text ?? "AXZY · demo",
360
+ navItems: DEMO_APP_PAGES.map((item) => ({
361
+ id: item,
362
+ label: t(PAGE_LABEL[item]),
363
+ isActive: item === pagina,
364
+ action: () => onNavigate(item),
365
+ })),
366
+ children: (
367
+ <ITButton label={t("sandbox.app.exit")} size="sm" variant="outlined" color="gray" onClick={onExit} />
368
+ ),
369
+ }}
370
+ sidebar={{
371
+ isCollapsed: colapsado,
372
+ onToggleCollapse: () => setColapsado((previo) => !previo),
373
+ brand: {
374
+ logo: <AxzyLogo variante="icono" tamano={20} />,
375
+ text: shell?.brand?.text ?? "AXZY · demo",
376
+ },
377
+ navigationItems: (shell?.modules ?? []).map((modulo) => ({
378
+ id: modulo.id,
379
+ label: isI18nRef(modulo.label) ? t(modulo.label.i18n) : String(modulo.label),
380
+ icon: ICONOS[modulo.icon ?? "box"],
381
+ isActive: modulo.page === pagina,
382
+ badge: modulo.badge,
383
+ action: () => onNavigate(modulo.page as DemoAppPage),
384
+ // Los sub-items llevan `query`: filtran de verdad, no son decoración.
385
+ subitems: modulo.subitems?.map((sub) => ({
386
+ id: sub.id,
387
+ label: isI18nRef(sub.label) ? t(sub.label.i18n) : String(sub.label),
388
+ action: () => {
389
+ if (sub.page !== pagina) onNavigate(sub.page as DemoAppPage);
390
+ onFilterChange(sub.query?.estado ?? "Todos");
391
+ setVersion((n) => n + 1);
392
+ },
393
+ })),
394
+ })),
395
+ }}
396
+ >
397
+ <ITPage
398
+ title={esquema && isI18nRef(esquema.title) ? t(esquema.title.i18n) : t(PAGE_LABEL[pagina])}
399
+ description={esquema && isI18nRef(esquema.description) ? t(esquema.description.i18n) : undefined}
400
+ breadcrumbs={[
401
+ { label: t("sandbox.app.title"), href: "#demo/app/inicio" },
402
+ { label: t(PAGE_LABEL[pagina]) },
403
+ ]}
404
+ >
405
+ <ITStack direction="column" spacing={4}>
406
+ {pagina !== "docs" && (
407
+ <ITTripleFilter
408
+ value={filtroEstado}
409
+ onChange={(valor) => {
410
+ onFilterChange(valor);
411
+ setVersion((n) => n + 1);
412
+ }}
413
+ options={[
414
+ { label: t("sandbox.app.sub.todos"), value: "Todos" },
415
+ { label: t("sandbox.app.sub.activos"), value: "Activo" },
416
+ { label: t("sandbox.app.sub.inactivos"), value: "Inactivo" },
417
+ ]}
418
+ />
419
+ )}
420
+ {pagina === "docs" ? <DemoJsonDocs /> : cuerpo}
421
+ </ITStack>
422
+ </ITPage>
423
+ </ITLayout>
424
+
425
+ <ITDialog
426
+ isOpen={formulario !== null}
427
+ onClose={cerrarFormulario}
428
+ title={editandoId === null ? t("sandbox.app.form.create") : t("sandbox.app.form.edit")}
429
+ >
430
+ {formulario && (
431
+ <div className="w-full max-w-[620px]">
432
+ <ITGrid container columns={12} spacing={3}>
433
+ {/* Avatar: compacto, en su propia celda. */}
434
+ <ITGrid item xs={12} md={4}>
435
+ <ITDropfile
436
+ onFileSelect={() => undefined}
437
+ acceptedFileTypes={["image/png", "image/jpeg"] as never}
438
+ />
439
+ </ITGrid>
440
+ <ITGrid item xs={12} md={8}>
441
+ <ITStack direction="column" spacing={3}>
442
+ <ITInput name="nombre" label={t("sandbox.app.form.name")} value={formulario.nombre} onChange={setCampo("nombre")} />
443
+ <ITInput name="correo" label={t("sandbox.app.form.email")} value={formulario.correo} onChange={setCampo("correo")} />
444
+ <ITFlex gap={3}>
445
+ <ITInputNumber name="edad" label={t("sandbox.app.form.age")} value={formulario.edad} decimals={0} onChange={(valor) => setFormulario({ ...formulario, edad: valor ?? 0 })} />
446
+ <ITSelect name="rol" label={t("sandbox.app.form.role")} value={formulario.rol} options={ROLES.map((rol) => ({ value: rol, label: rol }))} onChange={(evento) => setFormulario({ ...formulario, rol: evento.target.value })} />
447
+ </ITFlex>
448
+ </ITStack>
449
+ </ITGrid>
450
+ <ITGrid item xs={12} md={6}>
451
+ <ITSelect name="estado" label={t("sandbox.app.form.status")} value={formulario.estado} options={ESTADOS.map((estado) => ({ value: estado, label: estado }))} onChange={(evento) => setFormulario({ ...formulario, estado: evento.target.value })} />
452
+ </ITGrid>
453
+ <ITGrid item xs={12} md={6}>
454
+ <ITSearchSelect label={t("sandbox.app.form.company")} value={formulario.empresa} options={EMPRESAS.map((empresa) => ({ value: empresa, label: empresa }))} valueField="value" labelField="label" onChange={(valor) => setFormulario({ ...formulario, empresa: String(valor) })} />
455
+ </ITGrid>
456
+ <ITGrid item xs={12} md={6}>
457
+ <ITDatePicker name="alta" label={t("sandbox.app.form.since")} value={new Date(formulario.alta)} onChange={(fecha) => { const valor = Array.isArray(fecha) ? fecha[0] : fecha; setFormulario({ ...formulario, alta: valor ? valor.toISOString().slice(0, 10) : formulario.alta }); }} />
458
+ </ITGrid>
459
+ <ITGrid item xs={12} md={6}>
460
+ <ITTimePicker name="turno" label={t("sandbox.app.form.shift")} value={formulario.turno} onChange={(evento) => setFormulario({ ...formulario, turno: String(evento?.target?.value ?? formulario.turno) })} />
461
+ </ITGrid>
462
+ </ITGrid>
463
+ <ITFlex justify="between" wrap="wrap" gap={2} className="mt-5 border-t border-slate-200 pt-4 dark:border-slate-800">
464
+ {editandoId !== null ? (
465
+ <ITButton label={t("sandbox.app.form.delete")} icon={<FaTrash size={10} />} size="sm" variant="outlined" color="danger" onClick={borrar} />
466
+ ) : (<span />)}
467
+ <ITFlex gap={2}>
468
+ <ITButton label={t("sandbox.app.form.cancel")} size="sm" variant="text" color="gray" onClick={cerrarFormulario} />
469
+ <ITButton label={t("sandbox.app.form.save")} icon={<FaSave size={10} />} size="sm" disabled={!formulario.nombre.trim() || !formulario.correo.trim()} onClick={guardar} />
470
+ </ITFlex>
471
+ </ITFlex>
472
+ </div>
473
+ )}
474
+ </ITDialog>
475
+
476
+ {aviso && (
477
+ <ITToast
478
+ message={aviso.mensaje}
479
+ type={aviso.tipo}
480
+ position="bottom-right"
481
+ duration={3000}
482
+ onClose={() => setAviso(null)}
483
+ />
484
+ )}
485
+ </>
486
+ );
487
+ };
488
+
489
+ export default DemoJsonDashboard;