@acvl/mobile-components 0.0.0-stage → 0.0.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.
- package/README.md +147 -2
- package/assets/fonts/Inter_400Regular.ttf +0 -0
- package/assets/fonts/Inter_500Medium.ttf +0 -0
- package/assets/fonts/Inter_600SemiBold.ttf +0 -0
- package/assets/fonts/Inter_700Bold.ttf +0 -0
- package/assets/fonts/Inter_800ExtraBold.ttf +0 -0
- package/assets/fonts/Inter_900Black.ttf +0 -0
- package/assets/fonts/OFL.txt +93 -0
- package/lib/module/camara/CampoDeFotos.js +124 -0
- package/lib/module/camara/CampoDeFotos.js.map +1 -0
- package/lib/module/camara/elegir.js +104 -0
- package/lib/module/camara/elegir.js.map +1 -0
- package/lib/module/camara/index.js +10 -0
- package/lib/module/camara/index.js.map +1 -0
- package/lib/module/camara/reducir.js +93 -0
- package/lib/module/camara/reducir.js.map +1 -0
- package/lib/module/componentes/AccionesDelEncabezado.js +212 -0
- package/lib/module/componentes/AccionesDelEncabezado.js.map +1 -0
- package/lib/module/componentes/BotonDeAvatar.js +55 -0
- package/lib/module/componentes/BotonDeAvatar.js.map +1 -0
- package/lib/module/componentes/BotonGrande.js +105 -0
- package/lib/module/componentes/BotonGrande.js.map +1 -0
- package/lib/module/componentes/CabeceraEmpujada.js +101 -0
- package/lib/module/componentes/CabeceraEmpujada.js.map +1 -0
- package/lib/module/componentes/Chip.js +77 -0
- package/lib/module/componentes/Chip.js.map +1 -0
- package/lib/module/componentes/ContenidoDePantalla.js +56 -0
- package/lib/module/componentes/ContenidoDePantalla.js.map +1 -0
- package/lib/module/componentes/EstadoVacio.js +65 -0
- package/lib/module/componentes/EstadoVacio.js.map +1 -0
- package/lib/module/componentes/Foto.js +106 -0
- package/lib/module/componentes/Foto.js.map +1 -0
- package/lib/module/componentes/IconoDeRenglon.js +44 -0
- package/lib/module/componentes/IconoDeRenglon.js.map +1 -0
- package/lib/module/componentes/Logo.js +44 -0
- package/lib/module/componentes/Logo.js.map +1 -0
- package/lib/module/componentes/Nota.js +51 -0
- package/lib/module/componentes/Nota.js.map +1 -0
- package/lib/module/componentes/OpcionesExcluyentes.js +69 -0
- package/lib/module/componentes/OpcionesExcluyentes.js.map +1 -0
- package/lib/module/componentes/PantallaDeError.js +84 -0
- package/lib/module/componentes/PantallaDeError.js.map +1 -0
- package/lib/module/componentes/Pestanas.js +87 -0
- package/lib/module/componentes/Pestanas.js.map +1 -0
- package/lib/module/componentes/Rotulo.js +28 -0
- package/lib/module/componentes/Rotulo.js.map +1 -0
- package/lib/module/componentes/RotuloDeSeccion.js +87 -0
- package/lib/module/componentes/RotuloDeSeccion.js.map +1 -0
- package/lib/module/componentes/Tarjeta.js +63 -0
- package/lib/module/componentes/Tarjeta.js.map +1 -0
- package/lib/module/componentes/TarjetaDeDatos.js +79 -0
- package/lib/module/componentes/TarjetaDeDatos.js.map +1 -0
- package/lib/module/componentes/TiraDeFotos.js +184 -0
- package/lib/module/componentes/TiraDeFotos.js.map +1 -0
- package/lib/module/componentes/VisorDeFoto.js +171 -0
- package/lib/module/componentes/VisorDeFoto.js.map +1 -0
- package/lib/module/componentes/logos/bola-blanca.png +0 -0
- package/lib/module/componentes/logos/bola.png +0 -0
- package/lib/module/componentes/logos/logo-blanco.png +0 -0
- package/lib/module/componentes/logos/logo.png +0 -0
- package/lib/module/dialogos/HostDeDialogos.js +170 -0
- package/lib/module/dialogos/HostDeDialogos.js.map +1 -0
- package/lib/module/dialogos/dialogos.js +112 -0
- package/lib/module/dialogos/dialogos.js.map +1 -0
- package/lib/module/formularios/CampoDeSeleccion.js +72 -0
- package/lib/module/formularios/CampoDeSeleccion.js.map +1 -0
- package/lib/module/formularios/SelectorDeOpcion.js +406 -0
- package/lib/module/formularios/SelectorDeOpcion.js.map +1 -0
- package/lib/module/formularios/TextoDeAccesoControlado.js +73 -0
- package/lib/module/formularios/TextoDeAccesoControlado.js.map +1 -0
- package/lib/module/formularios/campos.js +501 -0
- package/lib/module/formularios/campos.js.map +1 -0
- package/lib/module/formularios/controlados.js +199 -0
- package/lib/module/formularios/controlados.js.map +1 -0
- package/lib/module/formularios/errores.js +91 -0
- package/lib/module/formularios/errores.js.map +1 -0
- package/lib/module/formularios/guardado.js +78 -0
- package/lib/module/formularios/guardado.js.map +1 -0
- package/lib/module/formularios/useBusqueda.js +22 -0
- package/lib/module/formularios/useBusqueda.js.map +1 -0
- package/lib/module/fotos/elegir.js +105 -0
- package/lib/module/fotos/elegir.js.map +1 -0
- package/lib/module/fotos/index.js +9 -0
- package/lib/module/fotos/index.js.map +1 -0
- package/lib/module/hoja/HojaDeLista.js +211 -0
- package/lib/module/hoja/HojaDeLista.js.map +1 -0
- package/lib/module/hoja/HojaInferior.js +112 -0
- package/lib/module/hoja/HojaInferior.js.map +1 -0
- package/lib/module/hoja/index.js +8 -0
- package/lib/module/hoja/index.js.map +1 -0
- package/lib/module/hoja/useMasDeLaFicha.js +48 -0
- package/lib/module/hoja/useMasDeLaFicha.js.map +1 -0
- package/lib/module/index.js +67 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/listas/HojaDeFiltros.js +270 -0
- package/lib/module/listas/HojaDeFiltros.js.map +1 -0
- package/lib/module/listas/ListaPaginada.js +103 -0
- package/lib/module/listas/ListaPaginada.js.map +1 -0
- package/lib/module/listas/TarjetaDeslizable.js +118 -0
- package/lib/module/listas/TarjetaDeslizable.js.map +1 -0
- package/lib/module/listas/paginado.js +11 -0
- package/lib/module/listas/paginado.js.map +1 -0
- package/lib/module/listas/piezas.js +666 -0
- package/lib/module/listas/piezas.js.map +1 -0
- package/lib/module/navegacion/BarraDePestanas.js +162 -0
- package/lib/module/navegacion/BarraDePestanas.js.map +1 -0
- package/lib/module/navegacion/BotonDeBarra.js +58 -0
- package/lib/module/navegacion/BotonDeBarra.js.map +1 -0
- package/lib/module/navegacion/encabezado.js +103 -0
- package/lib/module/navegacion/encabezado.js.map +1 -0
- package/lib/module/pantallas/PantallaDeAcceso.js +195 -0
- package/lib/module/pantallas/PantallaDeAcceso.js.map +1 -0
- package/lib/module/pantallas/PantallaDeEdicion.js +121 -0
- package/lib/module/pantallas/PantallaDeEdicion.js.map +1 -0
- package/lib/module/pantallas/PantallaDeFicha.js +94 -0
- package/lib/module/pantallas/PantallaDeFicha.js.map +1 -0
- package/lib/module/pantallas/PantallaDeHistorial.js +328 -0
- package/lib/module/pantallas/PantallaDeHistorial.js.map +1 -0
- package/lib/module/pantallas/PantallaDePerfil.js +137 -0
- package/lib/module/pantallas/PantallaDePerfil.js.map +1 -0
- package/lib/module/tema/ProveedorDeTema.js +122 -0
- package/lib/module/tema/ProveedorDeTema.js.map +1 -0
- package/lib/module/tema/estilos.js +54 -0
- package/lib/module/tema/estilos.js.map +1 -0
- package/lib/module/tema/fuentes.js +28 -0
- package/lib/module/tema/fuentes.js.map +1 -0
- package/lib/module/tema/index.js +8 -0
- package/lib/module/tema/index.js.map +1 -0
- package/lib/module/tema/paleta.js +107 -0
- package/lib/module/tema/paleta.js.map +1 -0
- package/lib/module/tema/tipografia.js +32 -0
- package/lib/module/tema/tipografia.js.map +1 -0
- package/lib/module/texto.js +48 -0
- package/lib/module/texto.js.map +1 -0
- package/lib/typescript/src/camara/CampoDeFotos.d.ts +30 -0
- package/lib/typescript/src/camara/CampoDeFotos.d.ts.map +1 -0
- package/lib/typescript/src/camara/elegir.d.ts +47 -0
- package/lib/typescript/src/camara/elegir.d.ts.map +1 -0
- package/lib/typescript/src/camara/index.d.ts +4 -0
- package/lib/typescript/src/camara/index.d.ts.map +1 -0
- package/lib/typescript/src/camara/reducir.d.ts +32 -0
- package/lib/typescript/src/camara/reducir.d.ts.map +1 -0
- package/lib/typescript/src/componentes/AccionesDelEncabezado.d.ts +34 -0
- package/lib/typescript/src/componentes/AccionesDelEncabezado.d.ts.map +1 -0
- package/lib/typescript/src/componentes/BotonDeAvatar.d.ts +17 -0
- package/lib/typescript/src/componentes/BotonDeAvatar.d.ts.map +1 -0
- package/lib/typescript/src/componentes/BotonGrande.d.ts +26 -0
- package/lib/typescript/src/componentes/BotonGrande.d.ts.map +1 -0
- package/lib/typescript/src/componentes/CabeceraEmpujada.d.ts +18 -0
- package/lib/typescript/src/componentes/CabeceraEmpujada.d.ts.map +1 -0
- package/lib/typescript/src/componentes/Chip.d.ts +14 -0
- package/lib/typescript/src/componentes/Chip.d.ts.map +1 -0
- package/lib/typescript/src/componentes/ContenidoDePantalla.d.ts +17 -0
- package/lib/typescript/src/componentes/ContenidoDePantalla.d.ts.map +1 -0
- package/lib/typescript/src/componentes/EstadoVacio.d.ts +17 -0
- package/lib/typescript/src/componentes/EstadoVacio.d.ts.map +1 -0
- package/lib/typescript/src/componentes/Foto.d.ts +41 -0
- package/lib/typescript/src/componentes/Foto.d.ts.map +1 -0
- package/lib/typescript/src/componentes/IconoDeRenglon.d.ts +25 -0
- package/lib/typescript/src/componentes/IconoDeRenglon.d.ts.map +1 -0
- package/lib/typescript/src/componentes/Logo.d.ts +13 -0
- package/lib/typescript/src/componentes/Logo.d.ts.map +1 -0
- package/lib/typescript/src/componentes/Nota.d.ts +7 -0
- package/lib/typescript/src/componentes/Nota.d.ts.map +1 -0
- package/lib/typescript/src/componentes/OpcionesExcluyentes.d.ts +14 -0
- package/lib/typescript/src/componentes/OpcionesExcluyentes.d.ts.map +1 -0
- package/lib/typescript/src/componentes/PantallaDeError.d.ts +23 -0
- package/lib/typescript/src/componentes/PantallaDeError.d.ts.map +1 -0
- package/lib/typescript/src/componentes/Pestanas.d.ts +28 -0
- package/lib/typescript/src/componentes/Pestanas.d.ts.map +1 -0
- package/lib/typescript/src/componentes/Rotulo.d.ts +6 -0
- package/lib/typescript/src/componentes/Rotulo.d.ts.map +1 -0
- package/lib/typescript/src/componentes/RotuloDeSeccion.d.ts +20 -0
- package/lib/typescript/src/componentes/RotuloDeSeccion.d.ts.map +1 -0
- package/lib/typescript/src/componentes/Tarjeta.d.ts +25 -0
- package/lib/typescript/src/componentes/Tarjeta.d.ts.map +1 -0
- package/lib/typescript/src/componentes/TarjetaDeDatos.d.ts +13 -0
- package/lib/typescript/src/componentes/TarjetaDeDatos.d.ts.map +1 -0
- package/lib/typescript/src/componentes/TiraDeFotos.d.ts +36 -0
- package/lib/typescript/src/componentes/TiraDeFotos.d.ts.map +1 -0
- package/lib/typescript/src/componentes/VisorDeFoto.d.ts +23 -0
- package/lib/typescript/src/componentes/VisorDeFoto.d.ts.map +1 -0
- package/lib/typescript/src/dialogos/HostDeDialogos.d.ts +14 -0
- package/lib/typescript/src/dialogos/HostDeDialogos.d.ts.map +1 -0
- package/lib/typescript/src/dialogos/dialogos.d.ts +65 -0
- package/lib/typescript/src/dialogos/dialogos.d.ts.map +1 -0
- package/lib/typescript/src/formularios/CampoDeSeleccion.d.ts +24 -0
- package/lib/typescript/src/formularios/CampoDeSeleccion.d.ts.map +1 -0
- package/lib/typescript/src/formularios/SelectorDeOpcion.d.ts +59 -0
- package/lib/typescript/src/formularios/SelectorDeOpcion.d.ts.map +1 -0
- package/lib/typescript/src/formularios/TextoDeAccesoControlado.d.ts +18 -0
- package/lib/typescript/src/formularios/TextoDeAccesoControlado.d.ts.map +1 -0
- package/lib/typescript/src/formularios/campos.d.ts +113 -0
- package/lib/typescript/src/formularios/campos.d.ts.map +1 -0
- package/lib/typescript/src/formularios/controlados.d.ts +29 -0
- package/lib/typescript/src/formularios/controlados.d.ts.map +1 -0
- package/lib/typescript/src/formularios/errores.d.ts +36 -0
- package/lib/typescript/src/formularios/errores.d.ts.map +1 -0
- package/lib/typescript/src/formularios/guardado.d.ts +36 -0
- package/lib/typescript/src/formularios/guardado.d.ts.map +1 -0
- package/lib/typescript/src/formularios/useBusqueda.d.ts +7 -0
- package/lib/typescript/src/formularios/useBusqueda.d.ts.map +1 -0
- package/lib/typescript/src/fotos/elegir.d.ts +28 -0
- package/lib/typescript/src/fotos/elegir.d.ts.map +1 -0
- package/lib/typescript/src/fotos/index.d.ts +3 -0
- package/lib/typescript/src/fotos/index.d.ts.map +1 -0
- package/lib/typescript/src/hoja/HojaDeLista.d.ts +50 -0
- package/lib/typescript/src/hoja/HojaDeLista.d.ts.map +1 -0
- package/lib/typescript/src/hoja/HojaInferior.d.ts +21 -0
- package/lib/typescript/src/hoja/HojaInferior.d.ts.map +1 -0
- package/lib/typescript/src/hoja/index.d.ts +4 -0
- package/lib/typescript/src/hoja/index.d.ts.map +1 -0
- package/lib/typescript/src/hoja/useMasDeLaFicha.d.ts +27 -0
- package/lib/typescript/src/hoja/useMasDeLaFicha.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +46 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/listas/HojaDeFiltros.d.ts +76 -0
- package/lib/typescript/src/listas/HojaDeFiltros.d.ts.map +1 -0
- package/lib/typescript/src/listas/ListaPaginada.d.ts +39 -0
- package/lib/typescript/src/listas/ListaPaginada.d.ts.map +1 -0
- package/lib/typescript/src/listas/TarjetaDeslizable.d.ts +33 -0
- package/lib/typescript/src/listas/TarjetaDeslizable.d.ts.map +1 -0
- package/lib/typescript/src/listas/paginado.d.ts +16 -0
- package/lib/typescript/src/listas/paginado.d.ts.map +1 -0
- package/lib/typescript/src/listas/piezas.d.ts +157 -0
- package/lib/typescript/src/listas/piezas.d.ts.map +1 -0
- package/lib/typescript/src/navegacion/BarraDePestanas.d.ts +57 -0
- package/lib/typescript/src/navegacion/BarraDePestanas.d.ts.map +1 -0
- package/lib/typescript/src/navegacion/BotonDeBarra.d.ts +15 -0
- package/lib/typescript/src/navegacion/BotonDeBarra.d.ts.map +1 -0
- package/lib/typescript/src/navegacion/encabezado.d.ts +44 -0
- package/lib/typescript/src/navegacion/encabezado.d.ts.map +1 -0
- package/lib/typescript/src/pantallas/PantallaDeAcceso.d.ts +32 -0
- package/lib/typescript/src/pantallas/PantallaDeAcceso.d.ts.map +1 -0
- package/lib/typescript/src/pantallas/PantallaDeEdicion.d.ts +38 -0
- package/lib/typescript/src/pantallas/PantallaDeEdicion.d.ts.map +1 -0
- package/lib/typescript/src/pantallas/PantallaDeFicha.d.ts +35 -0
- package/lib/typescript/src/pantallas/PantallaDeFicha.d.ts.map +1 -0
- package/lib/typescript/src/pantallas/PantallaDeHistorial.d.ts +65 -0
- package/lib/typescript/src/pantallas/PantallaDeHistorial.d.ts.map +1 -0
- package/lib/typescript/src/pantallas/PantallaDePerfil.d.ts +19 -0
- package/lib/typescript/src/pantallas/PantallaDePerfil.d.ts.map +1 -0
- package/lib/typescript/src/tema/ProveedorDeTema.d.ts +32 -0
- package/lib/typescript/src/tema/ProveedorDeTema.d.ts.map +1 -0
- package/lib/typescript/src/tema/estilos.d.ts +31 -0
- package/lib/typescript/src/tema/estilos.d.ts.map +1 -0
- package/lib/typescript/src/tema/fuentes.d.ts +20 -0
- package/lib/typescript/src/tema/fuentes.d.ts.map +1 -0
- package/lib/typescript/src/tema/index.d.ts +6 -0
- package/lib/typescript/src/tema/index.d.ts.map +1 -0
- package/lib/typescript/src/tema/paleta.d.ts +86 -0
- package/lib/typescript/src/tema/paleta.d.ts.map +1 -0
- package/lib/typescript/src/tema/tipografia.d.ts +29 -0
- package/lib/typescript/src/tema/tipografia.d.ts.map +1 -0
- package/lib/typescript/src/texto.d.ts +16 -0
- package/lib/typescript/src/texto.d.ts.map +1 -0
- package/package.json +140 -5
- package/src/camara/CampoDeFotos.tsx +132 -0
- package/src/camara/elegir.ts +113 -0
- package/src/camara/index.ts +15 -0
- package/src/camara/reducir.ts +87 -0
- package/src/componentes/AccionesDelEncabezado.tsx +247 -0
- package/src/componentes/BotonDeAvatar.tsx +59 -0
- package/src/componentes/BotonGrande.tsx +100 -0
- package/src/componentes/CabeceraEmpujada.tsx +69 -0
- package/src/componentes/Chip.tsx +60 -0
- package/src/componentes/ContenidoDePantalla.tsx +53 -0
- package/src/componentes/EstadoVacio.tsx +45 -0
- package/src/componentes/Foto.tsx +92 -0
- package/src/componentes/IconoDeRenglon.tsx +42 -0
- package/src/componentes/Logo.tsx +34 -0
- package/src/componentes/Nota.tsx +37 -0
- package/src/componentes/OpcionesExcluyentes.tsx +62 -0
- package/src/componentes/PantallaDeError.tsx +76 -0
- package/src/componentes/Pestanas.tsx +86 -0
- package/src/componentes/Rotulo.tsx +21 -0
- package/src/componentes/RotuloDeSeccion.tsx +71 -0
- package/src/componentes/Tarjeta.tsx +58 -0
- package/src/componentes/TarjetaDeDatos.tsx +62 -0
- package/src/componentes/TiraDeFotos.tsx +189 -0
- package/src/componentes/VisorDeFoto.tsx +181 -0
- package/src/componentes/logos/bola-blanca.png +0 -0
- package/src/componentes/logos/bola.png +0 -0
- package/src/componentes/logos/logo-blanco.png +0 -0
- package/src/componentes/logos/logo.png +0 -0
- package/src/dialogos/HostDeDialogos.tsx +172 -0
- package/src/dialogos/dialogos.ts +148 -0
- package/src/formularios/CampoDeSeleccion.tsx +60 -0
- package/src/formularios/SelectorDeOpcion.tsx +407 -0
- package/src/formularios/TextoDeAccesoControlado.tsx +72 -0
- package/src/formularios/campos.tsx +497 -0
- package/src/formularios/controlados.tsx +200 -0
- package/src/formularios/errores.ts +105 -0
- package/src/formularios/guardado.ts +76 -0
- package/src/formularios/useBusqueda.ts +19 -0
- package/src/fotos/elegir.ts +116 -0
- package/src/fotos/index.ts +6 -0
- package/src/hoja/HojaDeLista.tsx +214 -0
- package/src/hoja/HojaInferior.tsx +103 -0
- package/src/hoja/index.ts +5 -0
- package/src/hoja/useMasDeLaFicha.tsx +63 -0
- package/src/index.ts +151 -0
- package/src/listas/HojaDeFiltros.tsx +286 -0
- package/src/listas/ListaPaginada.tsx +117 -0
- package/src/listas/TarjetaDeslizable.tsx +126 -0
- package/src/listas/paginado.ts +15 -0
- package/src/listas/piezas.tsx +670 -0
- package/src/navegacion/BarraDePestanas.tsx +198 -0
- package/src/navegacion/BotonDeBarra.tsx +61 -0
- package/src/navegacion/encabezado.tsx +88 -0
- package/src/pantallas/PantallaDeAcceso.tsx +184 -0
- package/src/pantallas/PantallaDeEdicion.tsx +116 -0
- package/src/pantallas/PantallaDeFicha.tsx +115 -0
- package/src/pantallas/PantallaDeHistorial.tsx +293 -0
- package/src/pantallas/PantallaDePerfil.tsx +126 -0
- package/src/tema/ProveedorDeTema.tsx +150 -0
- package/src/tema/estilos.ts +51 -0
- package/src/tema/fuentes.ts +20 -0
- package/src/tema/index.ts +10 -0
- package/src/tema/paleta.ts +147 -0
- package/src/tema/tipografia.ts +29 -0
- package/src/texto.tsx +44 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ActivityIndicator, RefreshControl, ScrollView, View } from "react-native";
|
|
3
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
4
|
+
import type { AccionDelEncabezado } from "../componentes/AccionesDelEncabezado";
|
|
5
|
+
import BotonGrande from "../componentes/BotonGrande";
|
|
6
|
+
import CabeceraEmpujada from "../componentes/CabeceraEmpujada";
|
|
7
|
+
import EstadoVacio from "../componentes/EstadoVacio";
|
|
8
|
+
import { crearEstilos, usePaleta } from "../tema";
|
|
9
|
+
|
|
10
|
+
/** Lo que la ficha lee de un `use…Query` de RTK Query. */
|
|
11
|
+
interface ConsultaDeFicha {
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
isError: boolean;
|
|
14
|
+
isFetching: boolean;
|
|
15
|
+
refetch: () => unknown;
|
|
16
|
+
error?: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const esNoEncontrado = (error: unknown) =>
|
|
20
|
+
typeof error === "object" && error !== null && "status" in error && error.status === 404;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Una ficha de solo lectura (un viaje, un tracto, un cliente): la cabecera con atrás, y lo suyo en
|
|
24
|
+
* un scroll que se refresca jalando hacia abajo. Mientras carga, o si falla, lo dice en vez de
|
|
25
|
+
* pintar vacía.
|
|
26
|
+
*
|
|
27
|
+
* `children` sólo se pinta con datos: quien la usa no tiene que preguntar si ya llegaron.
|
|
28
|
+
*/
|
|
29
|
+
export default function PantallaDeFicha({
|
|
30
|
+
eyebrow,
|
|
31
|
+
titulo,
|
|
32
|
+
consulta,
|
|
33
|
+
hayDatos,
|
|
34
|
+
acciones,
|
|
35
|
+
estado,
|
|
36
|
+
encima,
|
|
37
|
+
children,
|
|
38
|
+
}: {
|
|
39
|
+
eyebrow: string;
|
|
40
|
+
titulo: string;
|
|
41
|
+
/** El estado de la ficha bajo su título (un `Chip`); sólo con datos. */
|
|
42
|
+
estado?: React.ReactNode;
|
|
43
|
+
/** Las acciones de la ficha, en su encabezado; sólo se ofrecen con datos. */
|
|
44
|
+
acciones?: AccionDelEncabezado[];
|
|
45
|
+
/**
|
|
46
|
+
* Lo que va encima de la ficha y no dentro de su scroll: sus hojas inferiores
|
|
47
|
+
* (`HojaInferior`) y diálogos. Sólo con datos.
|
|
48
|
+
*/
|
|
49
|
+
encima?: React.ReactNode;
|
|
50
|
+
consulta: ConsultaDeFicha;
|
|
51
|
+
hayDatos: boolean;
|
|
52
|
+
children: React.ReactNode;
|
|
53
|
+
}) {
|
|
54
|
+
const c = usePaleta();
|
|
55
|
+
const styles = useStyles();
|
|
56
|
+
const insets = useSafeAreaInsets();
|
|
57
|
+
|
|
58
|
+
let cuerpo: React.ReactNode;
|
|
59
|
+
if (consulta.isLoading) {
|
|
60
|
+
cuerpo = <ActivityIndicator color={c.primary} size="large" style={styles.cargando} />;
|
|
61
|
+
} else if (!hayDatos) {
|
|
62
|
+
cuerpo = esNoEncontrado(consulta.error) ? (
|
|
63
|
+
<EstadoVacio
|
|
64
|
+
icono="file-hidden"
|
|
65
|
+
titulo="No existe"
|
|
66
|
+
texto="Puede que lo hayan borrado, o que no tengas permiso de verlo."
|
|
67
|
+
/>
|
|
68
|
+
) : (
|
|
69
|
+
<EstadoVacio
|
|
70
|
+
icono="cloud-off-outline"
|
|
71
|
+
titulo="No se pudo cargar"
|
|
72
|
+
texto="Revisa tu señal e intenta de nuevo."
|
|
73
|
+
>
|
|
74
|
+
<BotonGrande icono="refresh" onPress={() => consulta.refetch()}>
|
|
75
|
+
Reintentar
|
|
76
|
+
</BotonGrande>
|
|
77
|
+
</EstadoVacio>
|
|
78
|
+
);
|
|
79
|
+
} else {
|
|
80
|
+
cuerpo = (
|
|
81
|
+
<ScrollView
|
|
82
|
+
contentContainerStyle={[styles.contenido, { paddingBottom: insets.bottom + 24 }]}
|
|
83
|
+
refreshControl={
|
|
84
|
+
<RefreshControl
|
|
85
|
+
refreshing={consulta.isFetching && !consulta.isLoading}
|
|
86
|
+
onRefresh={() => consulta.refetch()}
|
|
87
|
+
colors={[c.primary]}
|
|
88
|
+
progressBackgroundColor={c.superficie}
|
|
89
|
+
/>
|
|
90
|
+
}
|
|
91
|
+
>
|
|
92
|
+
{children}
|
|
93
|
+
</ScrollView>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<View style={styles.root}>
|
|
99
|
+
<CabeceraEmpujada
|
|
100
|
+
eyebrow={eyebrow}
|
|
101
|
+
titulo={titulo}
|
|
102
|
+
acciones={hayDatos ? acciones : undefined}
|
|
103
|
+
estado={hayDatos ? estado : undefined}
|
|
104
|
+
/>
|
|
105
|
+
{cuerpo}
|
|
106
|
+
{hayDatos ? encima : null}
|
|
107
|
+
</View>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const useStyles = crearEstilos((c) => ({
|
|
112
|
+
root: { flex: 1, backgroundColor: c.fondo },
|
|
113
|
+
cargando: { marginTop: 40 },
|
|
114
|
+
contenido: { padding: 16, gap: 20 },
|
|
115
|
+
}));
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { ActivityIndicator, Pressable, RefreshControl, ScrollView, View } from "react-native";
|
|
4
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
5
|
+
import BotonGrande from "../componentes/BotonGrande";
|
|
6
|
+
import CabeceraEmpujada from "../componentes/CabeceraEmpujada";
|
|
7
|
+
import EstadoVacio from "../componentes/EstadoVacio";
|
|
8
|
+
import Tarjeta from "../componentes/Tarjeta";
|
|
9
|
+
import { crearEstilos, renglon, tipografia, usePaleta, type Paleta } from "../tema";
|
|
10
|
+
import { Text } from "../texto";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Un movimiento de la bitácora de un registro, como lo responde cualquier endpoint `historial/`
|
|
14
|
+
* de los backends de ACV (`BaseHistorySerializer`): el registro **entero** como quedó, con quién,
|
|
15
|
+
* cuándo y qué campos cambiaron.
|
|
16
|
+
*/
|
|
17
|
+
export interface EventoDeHistorial {
|
|
18
|
+
/** Ya redactado por el backend: «Actualización - Concepto de Orden de envío». */
|
|
19
|
+
history_type: string;
|
|
20
|
+
/** El nombre completo de quien lo hizo; nulo si lo movió el sistema. */
|
|
21
|
+
history_user: string | null;
|
|
22
|
+
/** ISO en UTC. */
|
|
23
|
+
history_date: string;
|
|
24
|
+
/** Los que cambiaron en este paso; nulo en la creación, que no tiene contra qué compararse. */
|
|
25
|
+
campos_con_cambios: string[] | null;
|
|
26
|
+
[campo: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Lo que devuelve cualquier endpoint `historial/`: la línea de tiempo, del más nuevo al más viejo,
|
|
31
|
+
* y aparte cómo se llama cada campo. Las etiquetas son además el filtro de qué se pinta: un campo
|
|
32
|
+
* sin etiqueta es de los internos de la bitácora.
|
|
33
|
+
*/
|
|
34
|
+
export interface Historial {
|
|
35
|
+
history: EventoDeHistorial[];
|
|
36
|
+
labels: Record<string, string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type NombreDeIcono = React.ComponentProps<typeof MaterialCommunityIcons>["name"];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Cómo se pinta cada tipo de movimiento. El backend manda la acción ya redactada, así que el icono
|
|
43
|
+
* y el color salen del prefijo.
|
|
44
|
+
*/
|
|
45
|
+
const TIPOS: { prefijo: string; icono: NombreDeIcono; colores: (c: Paleta) => Colores }[] = [
|
|
46
|
+
{ prefijo: "Creación", icono: "plus-circle-outline", colores: (c) => c.exito },
|
|
47
|
+
{ prefijo: "Actualización", icono: "pencil-outline", colores: (c) => c.info },
|
|
48
|
+
{ prefijo: "Eliminación", icono: "minus-circle-outline", colores: (c) => c.error },
|
|
49
|
+
];
|
|
50
|
+
type Colores = { fondo: string; tinta: string };
|
|
51
|
+
|
|
52
|
+
const pintaDe = (c: Paleta, tipo: string) => {
|
|
53
|
+
const t = TIPOS.find((x) => tipo.startsWith(x.prefijo));
|
|
54
|
+
return t
|
|
55
|
+
? { icono: t.icono, ...t.colores(c) }
|
|
56
|
+
: { icono: "history" as NombreDeIcono, fondo: c.superficieTenue, tinta: c.secondary };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Parte «Acción - Modelo» en sus dos mitades; sin guion, todo es el encabezado. */
|
|
60
|
+
const partirTipo = (tipo: string) => {
|
|
61
|
+
const corte = tipo.indexOf(" - ");
|
|
62
|
+
if (corte === -1) return { accion: "", modelo: tipo };
|
|
63
|
+
return { accion: tipo.slice(0, corte), modelo: tipo.slice(corte + 3) };
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** El valor de un campo tal como viene de la bitácora, listo para pintarse. */
|
|
67
|
+
const formatear = (valor: unknown): string => {
|
|
68
|
+
if (valor === null || valor === undefined || valor === "") return "—";
|
|
69
|
+
if (typeof valor === "boolean") return valor ? "Sí" : "No";
|
|
70
|
+
if (Array.isArray(valor)) return valor.length ? valor.join(", ") : "—";
|
|
71
|
+
if (typeof valor === "object") return JSON.stringify(valor);
|
|
72
|
+
return String(valor);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** La fecha y hora del movimiento, en la hora del teléfono. */
|
|
76
|
+
const fechaHora = (iso: string) => {
|
|
77
|
+
const fecha = new Date(iso);
|
|
78
|
+
return Number.isNaN(fecha.getTime())
|
|
79
|
+
? "—"
|
|
80
|
+
: fecha.toLocaleString("es-MX", {
|
|
81
|
+
day: "2-digit",
|
|
82
|
+
month: "short",
|
|
83
|
+
year: "numeric",
|
|
84
|
+
hour: "2-digit",
|
|
85
|
+
minute: "2-digit",
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Un movimiento.
|
|
91
|
+
*
|
|
92
|
+
* De entrada enseña sólo lo que cambió en ese paso: el evento trae el registro entero como quedó, y
|
|
93
|
+
* en un teléfono treinta campos por tarjeta esconden el que importa. El resto está a un toque, que
|
|
94
|
+
* es donde se va a buscar el dato que no se movió.
|
|
95
|
+
*/
|
|
96
|
+
export function TarjetaDeEvento({
|
|
97
|
+
evento,
|
|
98
|
+
labels,
|
|
99
|
+
}: {
|
|
100
|
+
evento: EventoDeHistorial;
|
|
101
|
+
labels: Record<string, string>;
|
|
102
|
+
}) {
|
|
103
|
+
const c = usePaleta();
|
|
104
|
+
const styles = useStyles();
|
|
105
|
+
const [abierto, setAbierto] = React.useState(false);
|
|
106
|
+
|
|
107
|
+
const { accion, modelo } = partirTipo(evento.history_type);
|
|
108
|
+
const pinta = pintaDe(c, evento.history_type);
|
|
109
|
+
|
|
110
|
+
// Las etiquetas mandan qué se pinta y en qué orden.
|
|
111
|
+
const campos = Object.keys(labels).filter((campo) => campo in evento);
|
|
112
|
+
const cambiados = evento.campos_con_cambios;
|
|
113
|
+
const visibles = abierto ? campos : campos.filter((campo) => cambiados?.includes(campo));
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<Tarjeta style={styles.evento}>
|
|
117
|
+
<View style={styles.cabecera}>
|
|
118
|
+
<View style={[styles.icono, { backgroundColor: pinta.fondo }]}>
|
|
119
|
+
<MaterialCommunityIcons name={pinta.icono} size={20} color={pinta.tinta} />
|
|
120
|
+
</View>
|
|
121
|
+
<View style={styles.flex}>
|
|
122
|
+
<Text style={styles.titulo}>{modelo}</Text>
|
|
123
|
+
<Text style={styles.linea}>
|
|
124
|
+
{[accion, evento.history_user || "Sistema"].filter(Boolean).join(" · ")}
|
|
125
|
+
</Text>
|
|
126
|
+
<Text style={styles.linea}>{fechaHora(evento.history_date)}</Text>
|
|
127
|
+
</View>
|
|
128
|
+
</View>
|
|
129
|
+
|
|
130
|
+
{/* Un arreglo vacío es «se guardó sin mover nada»; nulo es la creación, que no lista
|
|
131
|
+
cambios. */}
|
|
132
|
+
{!abierto && cambiados?.length === 0 ? (
|
|
133
|
+
<Text style={styles.sinCambios}>Sin cambios en los campos.</Text>
|
|
134
|
+
) : null}
|
|
135
|
+
|
|
136
|
+
{visibles.map((campo) => {
|
|
137
|
+
const cambiado = Boolean(cambiados?.includes(campo));
|
|
138
|
+
return (
|
|
139
|
+
<View key={campo} style={[styles.dato, cambiado && styles.datoCambiado]}>
|
|
140
|
+
<Text style={[styles.etiqueta, cambiado && styles.etiquetaCambiada]}>
|
|
141
|
+
{labels[campo]}
|
|
142
|
+
</Text>
|
|
143
|
+
<Text style={styles.valor}>{formatear(evento[campo])}</Text>
|
|
144
|
+
</View>
|
|
145
|
+
);
|
|
146
|
+
})}
|
|
147
|
+
|
|
148
|
+
{campos.length > 0 ? (
|
|
149
|
+
<Pressable
|
|
150
|
+
onPress={() => setAbierto((previo) => !previo)}
|
|
151
|
+
accessibilityRole="button"
|
|
152
|
+
hitSlop={8}
|
|
153
|
+
style={({ pressed }) => [styles.alternar, pressed && styles.presionada]}
|
|
154
|
+
>
|
|
155
|
+
<Text style={styles.alternarTexto}>
|
|
156
|
+
{abierto
|
|
157
|
+
? "Ocultar campos"
|
|
158
|
+
: cambiados?.length
|
|
159
|
+
? "Ver todos los campos"
|
|
160
|
+
: "Ver los campos"}
|
|
161
|
+
</Text>
|
|
162
|
+
<MaterialCommunityIcons
|
|
163
|
+
name={abierto ? "chevron-up" : "chevron-down"}
|
|
164
|
+
size={22}
|
|
165
|
+
color={c.primary}
|
|
166
|
+
/>
|
|
167
|
+
</Pressable>
|
|
168
|
+
) : null}
|
|
169
|
+
</Tarjeta>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Lo que la pantalla lee de un `use…Query` de RTK Query. */
|
|
174
|
+
interface ConsultaDeHistorial {
|
|
175
|
+
data?: Historial;
|
|
176
|
+
isLoading: boolean;
|
|
177
|
+
isFetching: boolean;
|
|
178
|
+
isError: boolean;
|
|
179
|
+
refetch: () => unknown;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* La bitácora de un registro: quién lo movió, cuándo y qué le cambió. Es el panel de historial del
|
|
184
|
+
* portal, que allá se acopla al costado de la ficha; en el teléfono no hay costado, así que es una
|
|
185
|
+
* pantalla empujada a la que se llega desde las acciones de la ficha.
|
|
186
|
+
*
|
|
187
|
+
* Quien la usa pone la consulta (de dónde se pide, con su permiso). Conviene pedirla con
|
|
188
|
+
* `refetchOnMountOrArgChange`: se abre justo después de mover algo, y servir la caché enseñaría un
|
|
189
|
+
* historial sin el movimiento que se viene a comprobar.
|
|
190
|
+
*
|
|
191
|
+
* En varios registros el backend mezcla en una sola línea de tiempo el registro y lo que cuelga de
|
|
192
|
+
* él (una orden con sus conceptos, un embarque con su transporte); por eso cada tarjeta dice de qué
|
|
193
|
+
* registro habla.
|
|
194
|
+
*/
|
|
195
|
+
export default function PantallaDeHistorial({
|
|
196
|
+
eyebrow,
|
|
197
|
+
consulta,
|
|
198
|
+
}: {
|
|
199
|
+
/** De qué registro es: «Recibo R-0012». */
|
|
200
|
+
eyebrow: string;
|
|
201
|
+
consulta: ConsultaDeHistorial;
|
|
202
|
+
}) {
|
|
203
|
+
const c = usePaleta();
|
|
204
|
+
const styles = useStyles();
|
|
205
|
+
const insets = useSafeAreaInsets();
|
|
206
|
+
const { data, isLoading, isFetching, isError, refetch } = consulta;
|
|
207
|
+
const eventos = data?.history ?? [];
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<View style={styles.root}>
|
|
211
|
+
<CabeceraEmpujada eyebrow={eyebrow} titulo="Historial" />
|
|
212
|
+
{/* El error sólo se queda con la pantalla cuando no hay nada que enseñar: un refresco
|
|
213
|
+
que falla no debe borrar el historial que ya se estaba leyendo. */}
|
|
214
|
+
{isError && !data ? (
|
|
215
|
+
<EstadoVacio
|
|
216
|
+
icono="cloud-off-outline"
|
|
217
|
+
titulo="No se pudo cargar el historial"
|
|
218
|
+
texto="Revisa tu señal e intenta de nuevo."
|
|
219
|
+
>
|
|
220
|
+
<BotonGrande icono="refresh" modo="contorno" onPress={() => refetch()}>
|
|
221
|
+
Reintentar
|
|
222
|
+
</BotonGrande>
|
|
223
|
+
</EstadoVacio>
|
|
224
|
+
) : isLoading ? (
|
|
225
|
+
<ActivityIndicator color={c.primary} style={styles.cargando} />
|
|
226
|
+
) : (
|
|
227
|
+
<ScrollView
|
|
228
|
+
contentContainerStyle={[
|
|
229
|
+
styles.contenido,
|
|
230
|
+
{ paddingBottom: insets.bottom + 24 },
|
|
231
|
+
]}
|
|
232
|
+
refreshControl={
|
|
233
|
+
<RefreshControl
|
|
234
|
+
refreshing={isFetching}
|
|
235
|
+
onRefresh={() => refetch()}
|
|
236
|
+
tintColor={c.primary}
|
|
237
|
+
colors={[c.primary]}
|
|
238
|
+
progressBackgroundColor={c.superficie}
|
|
239
|
+
/>
|
|
240
|
+
}
|
|
241
|
+
>
|
|
242
|
+
{eventos.length === 0 ? (
|
|
243
|
+
<EstadoVacio icono="history" titulo="Sin movimientos registrados" />
|
|
244
|
+
) : (
|
|
245
|
+
eventos.map((evento, indice) => (
|
|
246
|
+
// El backend no da una llave por evento (mezcla modelos con ids que se
|
|
247
|
+
// repiten); el índice sirve porque la lista se reemplaza entera.
|
|
248
|
+
<TarjetaDeEvento
|
|
249
|
+
key={indice}
|
|
250
|
+
evento={evento}
|
|
251
|
+
labels={data?.labels ?? {}}
|
|
252
|
+
/>
|
|
253
|
+
))
|
|
254
|
+
)}
|
|
255
|
+
</ScrollView>
|
|
256
|
+
)}
|
|
257
|
+
</View>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const useStyles = crearEstilos((c) => ({
|
|
262
|
+
root: { flex: 1, backgroundColor: c.fondo },
|
|
263
|
+
cargando: { marginTop: 40 },
|
|
264
|
+
contenido: { padding: 16, gap: 12 },
|
|
265
|
+
flex: { flex: 1 },
|
|
266
|
+
evento: { gap: 10 },
|
|
267
|
+
cabecera: { flexDirection: "row", alignItems: "flex-start", gap: 12 },
|
|
268
|
+
icono: {
|
|
269
|
+
width: 36,
|
|
270
|
+
height: 36,
|
|
271
|
+
borderRadius: 18,
|
|
272
|
+
alignItems: "center",
|
|
273
|
+
justifyContent: "center",
|
|
274
|
+
},
|
|
275
|
+
titulo: { ...renglon(tipografia.cuerpo), fontWeight: "800" },
|
|
276
|
+
linea: { ...renglon(tipografia.secundario), color: c.secondary },
|
|
277
|
+
sinCambios: { ...renglon(tipografia.secundario), color: c.secondary, fontStyle: "italic" },
|
|
278
|
+
// Un campo por renglón: con la letra grande, dos columnas parten los valores a media palabra.
|
|
279
|
+
dato: { borderRadius: 10, paddingVertical: 6, paddingHorizontal: 10, gap: 2 },
|
|
280
|
+
datoCambiado: { backgroundColor: c.primaryWash },
|
|
281
|
+
etiqueta: {
|
|
282
|
+
...renglon(tipografia.etiqueta),
|
|
283
|
+
fontWeight: "800",
|
|
284
|
+
color: c.secondary,
|
|
285
|
+
textTransform: "uppercase",
|
|
286
|
+
letterSpacing: 0.6,
|
|
287
|
+
},
|
|
288
|
+
etiquetaCambiada: { color: c.primary },
|
|
289
|
+
valor: { ...renglon(tipografia.secundario), fontWeight: "600" },
|
|
290
|
+
alternar: { flexDirection: "row", alignItems: "center", gap: 2, alignSelf: "flex-start" },
|
|
291
|
+
alternarTexto: { ...renglon(tipografia.secundario), fontWeight: "700", color: c.primary },
|
|
292
|
+
presionada: { opacity: 0.6 },
|
|
293
|
+
}));
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ScrollView, View } from "react-native";
|
|
3
|
+
import { Avatar, Button, Card } from "react-native-paper";
|
|
4
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
5
|
+
import CabeceraEmpujada from "../componentes/CabeceraEmpujada";
|
|
6
|
+
import ContenidoDePantalla from "../componentes/ContenidoDePantalla";
|
|
7
|
+
import Pestanas, { type Pestana } from "../componentes/Pestanas";
|
|
8
|
+
import {
|
|
9
|
+
crearEstilos,
|
|
10
|
+
renglon,
|
|
11
|
+
tipografia,
|
|
12
|
+
usePreferenciaDeTema,
|
|
13
|
+
type PreferenciaDeTema,
|
|
14
|
+
} from "../tema";
|
|
15
|
+
import { Text } from "../texto";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* La apariencia: **igual que el teléfono** por omisión, o fija. Tres opciones y no un interruptor de
|
|
19
|
+
* «modo oscuro», como el del portal: con un interruptor, quien lo toca una vez ya no vuelve a seguir
|
|
20
|
+
* al teléfono.
|
|
21
|
+
*/
|
|
22
|
+
const APARIENCIAS: Pestana<PreferenciaDeTema>[] = [
|
|
23
|
+
{ key: "sistema", label: "Teléfono", icono: "cellphone" },
|
|
24
|
+
{ key: "claro", label: "Claro", icono: "white-balance-sunny" },
|
|
25
|
+
{ key: "oscuro", label: "Oscuro", icono: "weather-night" },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* El perfil de una app, igual en todas: quién entró, cómo se ve la app y cerrar sesión. Va
|
|
30
|
+
* empujado encima de las pestañas (lo abre el avatar del header). Necesita `ProveedorDeTema`.
|
|
31
|
+
*
|
|
32
|
+
* Lo propio de cada app va por props: quién es, qué pasa al cerrar sesión (avisar si hay envíos
|
|
33
|
+
* pendientes, confirmar) y, en `children`, lo que vaya al final (la versión instalada).
|
|
34
|
+
*/
|
|
35
|
+
export default function PantallaDePerfil({
|
|
36
|
+
rotulo,
|
|
37
|
+
nombre,
|
|
38
|
+
subtitulo,
|
|
39
|
+
onCerrarSesion,
|
|
40
|
+
saliendo = false,
|
|
41
|
+
children,
|
|
42
|
+
}: {
|
|
43
|
+
/** Qué tipo de sesión es («Operador», «Guardia de caseta»): el rótulo de la cabecera. */
|
|
44
|
+
rotulo: string;
|
|
45
|
+
nombre?: string | null;
|
|
46
|
+
/** El usuario, el número de empleado. */
|
|
47
|
+
subtitulo?: string | null;
|
|
48
|
+
onCerrarSesion: () => void;
|
|
49
|
+
saliendo?: boolean;
|
|
50
|
+
children?: React.ReactNode;
|
|
51
|
+
}) {
|
|
52
|
+
const styles = useStyles();
|
|
53
|
+
const [tema, setTema] = usePreferenciaDeTema();
|
|
54
|
+
const insets = useSafeAreaInsets();
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<View style={styles.root}>
|
|
58
|
+
<CabeceraEmpujada eyebrow={rotulo} titulo="Perfil" />
|
|
59
|
+
|
|
60
|
+
<ContenidoDePantalla paddingBottom={insets.bottom + 20}>
|
|
61
|
+
<ScrollView
|
|
62
|
+
contentContainerStyle={styles.contenido}
|
|
63
|
+
keyboardShouldPersistTaps="handled"
|
|
64
|
+
>
|
|
65
|
+
<Card mode={"outlined"} style={styles.card}>
|
|
66
|
+
{/* Paper recorta título y subtítulo a un renglón por omisión: un nombre
|
|
67
|
+
largo con la letra al 180% se perdía. */}
|
|
68
|
+
<Card.Title
|
|
69
|
+
title={nombre || "N/A"}
|
|
70
|
+
titleVariant={"titleLarge"}
|
|
71
|
+
titleStyle={{ fontWeight: "bold" }}
|
|
72
|
+
titleNumberOfLines={0}
|
|
73
|
+
subtitleNumberOfLines={0}
|
|
74
|
+
subtitleStyle={renglon(tipografia.secundario)}
|
|
75
|
+
subtitle={subtitulo || "N/A"}
|
|
76
|
+
subtitleVariant={"bodySmall"}
|
|
77
|
+
left={(props) => (
|
|
78
|
+
<Avatar.Text
|
|
79
|
+
{...props}
|
|
80
|
+
label={(nombre || "?").charAt(0).toUpperCase()}
|
|
81
|
+
color="#ffffff"
|
|
82
|
+
/>
|
|
83
|
+
)}
|
|
84
|
+
/>
|
|
85
|
+
</Card>
|
|
86
|
+
|
|
87
|
+
<View style={styles.apariencia}>
|
|
88
|
+
<Text style={styles.rotulo}>Apariencia</Text>
|
|
89
|
+
<Pestanas pestanas={APARIENCIAS} activa={tema} onSelect={setTema} />
|
|
90
|
+
</View>
|
|
91
|
+
|
|
92
|
+
{/* El tope de ancho vive en este View y no en el Button: el Surface de
|
|
93
|
+
Paper reparte el estilo en dos capas anidadas en iOS y `maxWidth` cae
|
|
94
|
+
en la de adentro, que queda pegada a la izquierda. */}
|
|
95
|
+
<View style={styles.acciones}>
|
|
96
|
+
<Button
|
|
97
|
+
onPress={onCerrarSesion}
|
|
98
|
+
mode={"contained"}
|
|
99
|
+
icon={"logout"}
|
|
100
|
+
loading={saliendo}
|
|
101
|
+
disabled={saliendo}
|
|
102
|
+
>
|
|
103
|
+
Cerrar sesión
|
|
104
|
+
</Button>
|
|
105
|
+
</View>
|
|
106
|
+
{children}
|
|
107
|
+
</ScrollView>
|
|
108
|
+
</ContenidoDePantalla>
|
|
109
|
+
</View>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const useStyles = crearEstilos((c) => ({
|
|
114
|
+
root: { flex: 1, backgroundColor: c.fondo },
|
|
115
|
+
contenido: { gap: 20, alignItems: "center" },
|
|
116
|
+
card: { width: "100%", backgroundColor: c.superficie, borderColor: c.secondaryLighter },
|
|
117
|
+
acciones: { width: "100%", maxWidth: 300, gap: 20 },
|
|
118
|
+
apariencia: { width: "100%", gap: 8 },
|
|
119
|
+
rotulo: {
|
|
120
|
+
...renglon(tipografia.etiqueta),
|
|
121
|
+
fontWeight: "800",
|
|
122
|
+
color: c.secondary,
|
|
123
|
+
textTransform: "uppercase",
|
|
124
|
+
letterSpacing: 0.8,
|
|
125
|
+
},
|
|
126
|
+
}));
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { StatusBar } from "expo-status-bar";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Appearance } from "react-native";
|
|
4
|
+
import { configureFonts, MD3DarkTheme, MD3LightTheme, PaperProvider } from "react-native-paper";
|
|
5
|
+
import { useEsOscuro, useFondoDeLaRaiz } from "./estilos";
|
|
6
|
+
import { fontFamily } from "./fuentes";
|
|
7
|
+
import { PALETA_CLARA, PALETA_OSCURA, type Paleta } from "./paleta";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Cómo quiere ver la app quien la usa: **igual que el teléfono** (por omisión), o siempre clara, o
|
|
11
|
+
* siempre oscura. Es del teléfono y no de la cuenta: sobrevive al cerrar sesión.
|
|
12
|
+
*/
|
|
13
|
+
export type PreferenciaDeTema = "sistema" | "claro" | "oscuro";
|
|
14
|
+
|
|
15
|
+
const PREFERENCIAS: readonly PreferenciaDeTema[] = ["sistema", "claro", "oscuro"];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Dónde guarda cada app la preferencia (SecureStore, AsyncStorage…). `leer` puede devolver
|
|
19
|
+
* cualquier cosa: lo que no sea una preferencia válida cuenta como `"sistema"`.
|
|
20
|
+
*/
|
|
21
|
+
export interface AlmacenDelTema {
|
|
22
|
+
leer: () => Promise<string | null | undefined>;
|
|
23
|
+
guardar: (preferencia: PreferenciaDeTema) => Promise<unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Pone la preferencia **en toda la app nativa** (`Appearance.setColorScheme`), no sólo en lo que
|
|
28
|
+
* pinta React: así `useColorScheme` ya contesta la elegida, y lo nativo —un mapa de Google, el
|
|
29
|
+
* selector de fecha, la barra de estado— la sigue sin que nadie se la pase. «Igual que el teléfono»
|
|
30
|
+
* es quitar la imposición.
|
|
31
|
+
*/
|
|
32
|
+
const aplicar = (preferencia: PreferenciaDeTema) =>
|
|
33
|
+
Appearance.setColorScheme(
|
|
34
|
+
preferencia === "claro" ? "light" : preferencia === "oscuro" ? "dark" : "unspecified",
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// La escala tipográfica de Paper trae `System` en iOS y `sans-serif` en Android; en Android se
|
|
38
|
+
// cambia la familia por Inter y se conservan tamaños y pesos de cada variante. Los `Text` de
|
|
39
|
+
// React Native no leen el tema: esos van por `texto.tsx`.
|
|
40
|
+
const fonts = fontFamily ? configureFonts({ config: { fontFamily } }) : MD3LightTheme.fonts;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* El tema de Paper de cada modo, con los colores de la paleta: los componentes de Paper (Card,
|
|
44
|
+
* Button, Avatar, Menu) no leen la paleta, leen esto.
|
|
45
|
+
*/
|
|
46
|
+
const temaDePaper = (base: typeof MD3LightTheme, c: Paleta) => ({
|
|
47
|
+
...base,
|
|
48
|
+
fonts,
|
|
49
|
+
colors: {
|
|
50
|
+
...base.colors,
|
|
51
|
+
primary: c.primary,
|
|
52
|
+
onPrimary: c.sobrePrimario,
|
|
53
|
+
primaryLight: c.primaryLight,
|
|
54
|
+
primaryLighter: c.primaryLighter,
|
|
55
|
+
primaryDark: c.primaryDark,
|
|
56
|
+
|
|
57
|
+
secondary: c.secondary,
|
|
58
|
+
secondaryLight: c.secondaryLight,
|
|
59
|
+
secondaryLighter: c.secondaryLighter,
|
|
60
|
+
secondaryDark: c.secondaryDark,
|
|
61
|
+
|
|
62
|
+
background: c.fondo,
|
|
63
|
+
surface: c.superficie,
|
|
64
|
+
onSurface: c.texto,
|
|
65
|
+
outline: c.secondaryLighter,
|
|
66
|
+
surfacePrimary: c.superficie,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const TEMA_CLARO = temaDePaper(MD3LightTheme, PALETA_CLARA);
|
|
71
|
+
const TEMA_OSCURO = temaDePaper(MD3DarkTheme, PALETA_OSCURA);
|
|
72
|
+
|
|
73
|
+
type ContextoDelTema = [PreferenciaDeTema, (preferencia: PreferenciaDeTema) => void];
|
|
74
|
+
|
|
75
|
+
const Contexto = React.createContext<ContextoDelTema | null>(null);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* El tema de la app: lee la preferencia guardada, se la impone a lo nativo, y da el tema de Paper,
|
|
79
|
+
* la barra de estado y el fondo de la raíz de ahora. Va en el layout raíz, por encima de todo lo
|
|
80
|
+
* que pinta.
|
|
81
|
+
*
|
|
82
|
+
* **No pinta nada hasta leer la preferencia**: así no se asoma el claro un momento en quien eligió
|
|
83
|
+
* el oscuro. Leerla es una consulta local, no se nota.
|
|
84
|
+
*/
|
|
85
|
+
export function ProveedorDeTema({
|
|
86
|
+
almacen,
|
|
87
|
+
children,
|
|
88
|
+
}: {
|
|
89
|
+
/** Sin almacén, la preferencia dura lo que dure la app abierta. */
|
|
90
|
+
almacen?: AlmacenDelTema;
|
|
91
|
+
children: React.ReactNode;
|
|
92
|
+
}) {
|
|
93
|
+
const [preferencia, setPreferencia] = React.useState<PreferenciaDeTema | null>(
|
|
94
|
+
almacen ? null : "sistema",
|
|
95
|
+
);
|
|
96
|
+
const oscuro = useEsOscuro();
|
|
97
|
+
useFondoDeLaRaiz((oscuro ? PALETA_OSCURA : PALETA_CLARA).fondo);
|
|
98
|
+
|
|
99
|
+
React.useEffect(() => {
|
|
100
|
+
if (!almacen) return;
|
|
101
|
+
let vigente = true;
|
|
102
|
+
almacen
|
|
103
|
+
.leer()
|
|
104
|
+
.catch(() => null)
|
|
105
|
+
.then((guardada) => {
|
|
106
|
+
if (!vigente) return;
|
|
107
|
+
const valida = PREFERENCIAS.find((p) => p === guardada) ?? "sistema";
|
|
108
|
+
aplicar(valida);
|
|
109
|
+
setPreferencia(valida);
|
|
110
|
+
});
|
|
111
|
+
return () => {
|
|
112
|
+
vigente = false;
|
|
113
|
+
};
|
|
114
|
+
// Se lee una sola vez, al arrancar: un almacén nuevo en cada render no la vuelve a leer.
|
|
115
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
const valor = React.useMemo<ContextoDelTema | null>(
|
|
119
|
+
() =>
|
|
120
|
+
preferencia === null
|
|
121
|
+
? null
|
|
122
|
+
: [
|
|
123
|
+
preferencia,
|
|
124
|
+
(nueva) => {
|
|
125
|
+
aplicar(nueva);
|
|
126
|
+
setPreferencia(nueva);
|
|
127
|
+
almacen?.guardar(nueva).catch(() => {});
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
[preferencia, almacen],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (!valor) return null;
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<Contexto.Provider value={valor}>
|
|
137
|
+
<PaperProvider theme={oscuro ? TEMA_OSCURO : TEMA_CLARO}>
|
|
138
|
+
<StatusBar style={oscuro ? "light" : "dark"} />
|
|
139
|
+
{children}
|
|
140
|
+
</PaperProvider>
|
|
141
|
+
</Contexto.Provider>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** La preferencia del tema y cómo cambiarla (el perfil). Sólo dentro de `ProveedorDeTema`. */
|
|
146
|
+
export function usePreferenciaDeTema(): ContextoDelTema {
|
|
147
|
+
const valor = React.useContext(Contexto);
|
|
148
|
+
if (!valor) throw new Error("usePreferenciaDeTema va dentro de <ProveedorDeTema>.");
|
|
149
|
+
return valor;
|
|
150
|
+
}
|