v1.2.5
This commit is contained in:
parent
a5dc3395e3
commit
c5aff5d2cb
354
bloques.xml
354
bloques.xml
@ -1,354 +0,0 @@
|
||||
<?xml version="1.0"
|
||||
encoding="ISO-8859-1"
|
||||
standalone="yes"?>
|
||||
<datapacket version="2.000" destination="" day="8" month="10" year="2026" hour="19" min="0" sec="28" msec="755">
|
||||
<metadata fielddefs="yes" indexdefs="no" recorddata="yes" changes="yes">
|
||||
<fielddefs count="5">
|
||||
<fielddef name="ID" fieldkind="data" datatype="integer" fieldsize="0" displaylabel="ID" editmask="" displaywidth="10" fieldindex="0" required="false" readonly="false"/>
|
||||
<fielddef name="CODIGO" fieldkind="data" datatype="string" fieldsize="50" displaylabel="CODIGO" editmask="" displaywidth="50" fieldindex="1" required="false" readonly="false"/>
|
||||
<fielddef name="VALOR" fieldkind="data" datatype="string" fieldsize="2000" displaylabel="VALOR" editmask="" displaywidth="150" fieldindex="2" required="false" readonly="false"/>
|
||||
<fielddef name="ID_EMPRESA" fieldkind="data" datatype="integer" fieldsize="0" displaylabel="ID_EMPRESA" editmask="" displaywidth="10" fieldindex="3" required="false" readonly="false"/>
|
||||
<fielddef name="VALOR_BLOB" fieldkind="data" datatype="memo" fieldsize="8" displaylabel="VALOR_BLOB" editmask="" displaywidth="10" fieldindex="4" required="false" readonly="false"/>
|
||||
</fielddefs>
|
||||
<indexdefs count="0">
|
||||
</indexdefs>
|
||||
</metadata>
|
||||
<recorddata count="48">
|
||||
<row>
|
||||
<field name="ID" value="2000000139"/>
|
||||
<field name="CODIGO" value="CAP_SEP_AMBIENTES_TITULO_FR"/>
|
||||
<field name="VALOR" value=""/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" value=""/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000140"/>
|
||||
<field name="CODIGO" value="CAP_SEP_AMBIENTES_FR"/>
|
||||
<field name="VALOR" value=""/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" value=""/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="50"/>
|
||||
<field name="CODIGO" value="CAP_SEP_AMBIENTES_ES"/>
|
||||
<field name="VALOR" value="CARACTERISTICAS SEPARACIÓN DE AMBIENTES
Medida de Hueco: 2450x3000 mm.
Modelo: LYN / Acabado: ANODIZADO / Color: NEGRO
Nº de travesaños: 2 TRAVESAÑOS HORIZONTALES
Acabado paneles: TRANSPARENTE TEMPLADO 
Tirador: DOBLE POR PUERTA
Freno: DOBLE POR PUERTA (APERTURA Y CIERRE)
Guía: VISTA (DOBLE EMBELLECEDOR)
Nº de hojas: 2 CORREDERAS + 2 FIJOS"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="51"/>
|
||||
<field name="CODIGO" value="CAP_FRE_ARM_CORREDERA_ES"/>
|
||||
<field name="VALOR" value="CARACTERISTICAS FRENTE DE ARMARIO
Medida de Hueco:2440x5180mm.
Modelo: PANORAMIC P.O. / Acabado: LACADO / Color: BLANCO
Nº de lístelos: SIN LISTELOS / Acabado paneles: PANEL LACADO
Tirador:
Cerco perimetral: 
Nº de hojas:
FRENO HIDRAULICO INCORPORADO O FRENO MAGNETICO INCORPORADO
Compuesto por:"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="52"/>
|
||||
<field name="CODIGO" value="CAP_FRE_ARM_ABATIBLE_ES"/>
|
||||
<field name="VALOR" value="CARACTERISTICAS FRENTE DE ARMARIO
Medida de hueco: 2275x1390 mm.
Modelo: ARGO / Acabado: LACADO / Color: A DETERMINAR
Grosor hoja: 22 o 19 mm
Acabado de hojas: LISAS
Tirador: UÑERO A DETERMINAR
Acabado herrajes: CROMADO O ANTRACITA
Jambas perimetrales: VISTAS, REMETIDAS O COMBI A DETERMINAR.
Nº de hojas: 2
Compuesto por:"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="53"/>
|
||||
<field name="CODIGO" value="CAP_INT_ARM_FORRADO_ES"/>
|
||||
<field name="VALOR" value="CARACTERISTICAS INTERIOR DE ARMARIO
Acabado: MELAMINA / color: A DETERMINAR
Espesor bloque: 19 mm. / Espesor complementos: 19 mm.
Barras de colgar: CROMO/ NEGRA/ BRONCE
Frentes cristal cajones: RECTO / “U” – FLOAT BRONCE C-424
Tirador cajones: FRENTE BAJO / Cajones: 4 DE 180 MM. A DETERMINAR
Baldas deslizantes: X DE 145 MM. A DETERMINAR
Compuesto por:"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="54"/>
|
||||
<field name="CODIGO" value="CAP_CLA_SEMI_SUSPENDIDO_ES"/>
|
||||
<field name="VALOR" value="Acabado: MELAMINA / color: A DETERMINAR
Medida de Hueco: 2450x3000 mm.
Espesor bloque: 25 mm. / Espesor complementos: 25 mm.
Barras de colgar: CROMO/ NEGRA/ BRONCE
Frentes cristal cajones: RECTO / “U” – FLOAT BRONCE C-424
Tirador cajones: FRENTE BAJO / Cajones: 4 DE 180 MM. A DETERMINAR
Baldas deslizantes: X DE 145 MM. A DETERMINAR
Compuesto por:"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="55"/>
|
||||
<field name="CODIGO" value="CAP_INSTALACION_ES"/>
|
||||
<field name="VALOR" value="Instalación Armario x"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="56"/>
|
||||
<field name="CODIGO" value="CAP_SUMINISTRO_ES"/>
|
||||
<field name="VALOR" value="Entrega de material en domicilio cliente"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000098"/>
|
||||
<field name="CODIGO" value="CAP_CLA_SEMI_SUSPENDIDO_EN"/>
|
||||
<field name="VALOR" value="Finish: MELAMINE / colour: TO BE DETERMINED
Size of opening: 2450x3000 mm.
Thickness block: 19 mm / Thickness complements: 19 mm.
Hanging rods: CHROME/ BLACK
Glass drawer fronts: STRAIGHT / "U" - FLOAT BRONZE C-424 
Drawer handle: xxxxx / Drawers: 4 DE 180 MM.
Shoe rack door: YES/NO / Profile finish: BLACK LACQUER / SILVER MATTE
Shoe cabinet door glass finish: FLOAT BRONZE C-424
Composed of:"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000097"/>
|
||||
<field name="CODIGO" value="CAP_CLA_SEMI_SUSPENDIDO_TITULO_EN"/>
|
||||
<field name="VALOR" value="CLASSIC DRESSER / SEMI-HUNG SUSPENDED / SUSPENDED"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000096"/>
|
||||
<field name="CODIGO" value="CAP_CLA_SEMI_SUSPENDIDO_TITULO_ES"/>
|
||||
<field name="VALOR" value="CLASICO / SEMISUSPENDIDO / SUSPENDIDO"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000084"/>
|
||||
<field name="CODIGO" value="CAP_FRE_ARM_ABATIBLE_EN"/>
|
||||
<field name="VALOR" value="CHARACTERISTICS OF THE FRONT OF THE HINGED WARDROBE
Size of opening: 2275x1390 mm.
Model: ARGO / Finish: LACQUER / Colour: TO BE DETERMINED
Door thickness: 22 or 19 mm
Door finish: SMOOTH 
Handle: KNOB TO BE DETERMINED
Hardware finish: CHROME OR ANTHRACITE
Perimetral doorposts: VISIBLE, REMETED OR COMBI TO BE DETERMINED.
Number of doors: 2 
Composed of:"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000086"/>
|
||||
<field name="CODIGO" value="CAP_FRE_ARM_ABATIBLE_TITULO_EN"/>
|
||||
<field name="VALOR" value="FRONT OF THE HINGED WARDROBE"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000085"/>
|
||||
<field name="CODIGO" value="CAP_FRE_ARM_ABATIBLE_TITULO_ES"/>
|
||||
<field name="VALOR" value="FRENTE DE ARMARIO ABATIBLE"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000089"/>
|
||||
<field name="CODIGO" value="CAP_FRE_ARM_CORREDERA_EN"/>
|
||||
<field name="VALOR" value="Size of opening: 2440x5180mm. 
Model: PANORAMIC P.O. / Finish: LACQUER / Colour: WHITE
No. of listers: NO LISTELS / Panels finish: LACQUERED PANEL 
Handle: 
Perimeter frame: 
Nº of doors: 
INCORPORATED HYDRAULIC BRAKE (not available in lower doors of 650 mm.) OR INCORPORATED MAGNETIC BRAKE. 
Composed of:"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000088"/>
|
||||
<field name="CODIGO" value="CAP_FRE_ARM_CORREDERA_TITULO_EN"/>
|
||||
<field name="VALOR" value="SLIDING WARDROBE FRONT"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000087"/>
|
||||
<field name="CODIGO" value="CAP_FRE_ARM_CORREDERA_TITULO_ES"/>
|
||||
<field name="VALOR" value="FRENTE DE ARMARIO CORREDERA"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000113"/>
|
||||
<field name="CODIGO" value="CAP_INSTALACION_EN"/>
|
||||
<field name="VALOR" value="Installation Cabinet x"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000115"/>
|
||||
<field name="CODIGO" value="CAP_INSTALACION_TITULO_EN"/>
|
||||
<field name="VALOR" value="INSTALLATION SERVICE - NET PRICE"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000114"/>
|
||||
<field name="CODIGO" value="CAP_INSTALACION_TITULO_ES"/>
|
||||
<field name="VALOR" value="SERVICIO DE INSTALACIÓN - PRECIO NETO"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000092"/>
|
||||
<field name="CODIGO" value="CAP_INT_ARM_FORRADO_EN"/>
|
||||
<field name="VALOR" value="Finish: MELAMINE / colour: TO BE DETERMINED
Thickness block: 19 mm / Thickness complements: 19 mm.
Hanging rods: CHROME/ BLACK
Glass drawer fronts: STRAIGHT / "U" - FLOAT BRONZE C-424 
Drawer handle: xxxxx / Drawers: 4 DE 180 MM.
Composed of:"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000091"/>
|
||||
<field name="CODIGO" value="CAP_INT_ARM_FORRADO_TITULO_EN"/>
|
||||
<field name="VALOR" value="INTERIOR OF THE LINED WARDROBE"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000090"/>
|
||||
<field name="CODIGO" value="CAP_INT_ARM_FORRADO_TITULO_ES"/>
|
||||
<field name="VALOR" value="INTERIOR DE ARMARIO FORRADO"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000109"/>
|
||||
<field name="CODIGO" value="CAP_INT_EST_CRISTAL_EN"/>
|
||||
<field name="VALOR" value="Frames finish: BLACK ANODIZED
Side frame panel finish: BRONZE FLOAT BRONZE GLASS C-424
Rear panel finish: BRONZE FLOAT GLASS C-424
Complements finish: MELAMIN / Colour: DENBER OAK M-242
Thickness of accessories: 25 mm.
Hanging bars: BLACK
Glass drawer fronts: STRAIGHT / "U" - FLOAT BRONZE C-424 
Drawer handle: xxxxx / Drawers: 4 DE 180 MM.
Composed of:"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000108"/>
|
||||
<field name="CODIGO" value="CAP_INT_EST_CRISTAL_ES"/>
|
||||
<field name="VALOR" value="Acabado bastidores: ANODIZADO NEGRO
Acabado panel bastidores laterales: CRISTAL TEMPLADO FLOAT BRONCE C-424
Acabado trasera: CRISTAL TEMPLADO FLOAT BRONCE C-424
Acabado complementos: MELAMINA / Color: ROBLE DENBER M-242
Espesor complementos: 25 mm.
Barras de colgar: NEGRA
Frentes cristal cajones: RECTO / “U” – FLOAT BRONCE C-424 
Tirador cajones: xxxxx / Cajones: 4 DE 180 MM.
Compuesto por:"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000107"/>
|
||||
<field name="CODIGO" value="CAP_INT_EST_CRISTAL_TITULO_EN"/>
|
||||
<field name="VALOR" value="INSIDE CHARACTERISTICS OF WARDROBE GLASS STRUCTURE"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000106"/>
|
||||
<field name="CODIGO" value="CAP_INT_EST_CRISTAL_TITULO_ES"/>
|
||||
<field name="VALOR" value="INTERIOR DE ARMARIO ESTRUCTURA DE CRISTAL"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000095"/>
|
||||
<field name="CODIGO" value="CAP_SEP_AMBIENTES_EN"/>
|
||||
<field name="VALOR" value="CHARACTERISTICS SEPARATION OF ROOMS
Size of opening: 2450x3000 mm.
Model: LYN / Finish: ANODISED / Colour: BLACK
No. of crosspieces: 2 HORIZONTAL CROSSBARS
Panel finish: TRANSPARENT TEMPLARED 
Handle: DOUBLE PER DOOR
Brake: DOUBLE PER DOOR (OPENING AND CLOSING)
Guide: VISTA (DOUBLE EMBELLECEDOR)
Number of panels: 2 SLIDING + 2 FIXED"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000094"/>
|
||||
<field name="CODIGO" value="CAP_SEP_AMBIENTES_TITULO_EN"/>
|
||||
<field name="VALOR" value="SEPARATION OF ROOMS"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000093"/>
|
||||
<field name="CODIGO" value="CAP_SEP_AMBIENTES_TITULO_ES"/>
|
||||
<field name="VALOR" value="SEPARACIÓN DE AMBIENTES"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000110"/>
|
||||
<field name="CODIGO" value="CAP_SUMINISTRO_EN"/>
|
||||
<field name="VALOR" value="Delivery of material to the customer's home"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000112"/>
|
||||
<field name="CODIGO" value="CAP_SUMINISTRO_TITULO_EN"/>
|
||||
<field name="VALOR" value="SUPPLY SERVICE - NET PRICE"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000111"/>
|
||||
<field name="CODIGO" value="CAP_SUMINISTRO_TITULO_ES"/>
|
||||
<field name="VALOR" value="SERVICIO DE SUMINISTRO - PRECIO NETO"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000122"/>
|
||||
<field name="CODIGO" value="CAP_CLA_SEMI_SUSPENDIDO_TITULO_FR"/>
|
||||
<field name="VALOR" value="CARACTÉRISTIQUES DRESSING CLASSIQUE / SEMI-SUSPENDU / SUSPENDU"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000121"/>
|
||||
<field name="CODIGO" value="CAP_CLA_SEMI_SUSPENDIDO_FR"/>
|
||||
<field name="VALOR" value="Finition : MÉLAMINE / Couleur : À DÉTERMINER
Épaisseur du bloc : 19 mm / Épaisseur des accessoires : 19 mm
Barres de penderie : CHROME / NOIR
Façades des tiroirs en verre : DROITE / “U” – FLOAT BRONZE C-424
Poignée des tiroirs : xxxxx
Tiroirs : 4 DE 180 MM
Porte de chaussures : OUI / NON
Finition du profil : LAQUÉ NOIR / ARGENT MAT
Finition du verre de la porte de chaussures : FLOAT BRONZE C-424
Composé de :"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000124"/>
|
||||
<field name="CODIGO" value="CAP_FRE_ARM_ABATIBLE_TITULO_FR"/>
|
||||
<field name="VALOR" value="CARACTÉRISTIQUES FAÇADE DE PLACARD BATTANT"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000123"/>
|
||||
<field name="CODIGO" value="CAP_FRE_ARM_ABATIBLE_FR"/>
|
||||
<field name="VALOR" value="Dimension de l’ouverture : 2275x1390 mm
Modèle : ARGO / Finition : LAQUÉE / Couleur : À DÉTERMINER
Épaisseur des vantaux : 22 ou 19 mm
Finition des vantaux : LISSES
Poignée : PRISE DE MAIN À DÉTERMINER
Finition des ferrures : CHROMÉ OU ANTHRACITE
Jambages périmétraux : APPARENTS, ENCASTRÉS OU COMBINÉS À DÉTERMINER
Nombre de vantaux : 2
Composé de :"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000126"/>
|
||||
<field name="CODIGO" value="CAP_FRE_ARM_CORREDERA_TITULO_FR"/>
|
||||
<field name="VALOR" value="CARACTÉRISTIQUES FAÇADE DE PLACARD COULISSANT"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000125"/>
|
||||
<field name="CODIGO" value="CAP_FRE_ARM_CORREDERA_FR"/>
|
||||
<field name="VALOR" value="Dimension de l’ouverture : 2440x5180mm
Modèle : PANORAMIC P.O. / Finition : LAQUÉE / Couleur : BLANC
Nombre de listels : SANS LISTELS
Finition des panneaux : PANNEAU LAQUÉ
Poignée :
Encadrement périmétral :
Nombre de vantaux :
FREIN HYDRAULIQUE INTÉGRÉ (non disponible pour les portes inférieures à 650 mm) OU FREIN MAGNÉTIQUE INTÉGRÉ
Composé de :"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000128"/>
|
||||
<field name="CODIGO" value="CAP_INSTALACION_TITULO_FR"/>
|
||||
<field name="VALOR" value="SERVICE D'INSTALLATION - PRIX NET"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000127"/>
|
||||
<field name="CODIGO" value="CAP_INSTALACION_FR"/>
|
||||
<field name="VALOR" value="Armoire d'installation x"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000130"/>
|
||||
<field name="CODIGO" value="CAP_INT_ARM_FORRADO_TITULO_FR"/>
|
||||
<field name="VALOR" value="CARACTÉRISTIQUES INTÉRIEUR DE PLACARD HABILLÉ"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000129"/>
|
||||
<field name="CODIGO" value="CAP_INT_ARM_FORRADO_FR"/>
|
||||
<field name="VALOR" value="Finition : MÉLAMINE / Couleur : À DÉTERMINER
Épaisseur du bloc : 19 mm / Épaisseur des accessoires : 19 mm
Barres de penderie : CHROME / NOIR
Façades des tiroirs en verre : DROITE / “U” – FLOAT BRONZE C-424
Poignée des tiroirs : xxxxx
Tiroirs : 4 DE 180 MM
Composé de :"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000132"/>
|
||||
<field name="CODIGO" value="CAP_INT_EST_CRISTAL_TITULO_FR"/>
|
||||
<field name="VALOR" value="CARACTÉRISTIQUES INTÉRIEUR DE PLACARD STRUCTURE EN VERRE"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000131"/>
|
||||
<field name="CODIGO" value="CAP_INT_EST_CRISTAL_FR"/>
|
||||
<field name="VALOR" value="Finition des cadres : ANODISÉ NOIR
Finition des panneaux des cadres latéraux : VERRE TREMPÉ FLOAT BRONZE C-424
Finition du dos : VERRE TREMPÉ FLOAT BRONZE C-424
Finition des accessoires : MÉLAMINE / Couleur : CHÊNE DENBER M-242
Épaisseur des accessoires : 25 mm
Barres de penderie : NOIRES
Façades des tiroirs en verre : DROITE / “U” – FLOAT BRONZE C-424
Poignée des tiroirs : xxxxx
Tiroirs : 4 DE 180 MM
Composé de :"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000135"/>
|
||||
<field name="CODIGO" value="CAP_SUMINISTRO_TITULO_FR"/>
|
||||
<field name="VALOR" value="SERVICE DE FOURNITURE - PRIX NET"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
<row>
|
||||
<field name="ID" value="2000000134"/>
|
||||
<field name="CODIGO" value="CAP_SUMINISTRO_FR"/>
|
||||
<field name="VALOR" value="Livraison du matériel au domicile du client"/>
|
||||
<field name="ID_EMPRESA" size="-1"/>
|
||||
<field name="VALOR_BLOB" size="-1"/>
|
||||
</row>
|
||||
</recorddata>
|
||||
</datapacket>
|
||||
@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="es">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
@ -7,13 +7,13 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link href="https://fonts.upset.dev/css2?family=Poppins&display=swap" rel="stylesheet" />
|
||||
<title>Presupuestador Uecko</title>
|
||||
<title><!-- app-title --></title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<noscript><!-- app-noscript --></noscript>
|
||||
<div id="uecko"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@uecko-presupuestador/client",
|
||||
"private": true,
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.5",
|
||||
"author": "Rodax Software <dev@rodax-software.com>",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@ -2,8 +2,8 @@ import { AuthProvider, ThemeProvider, UnsavedWarnProvider } from "@/lib/hooks";
|
||||
import { Toaster, TooltipProvider } from "@/ui";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { Suspense } from "react";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { I18nextProvider, useTranslation } from "react-i18next";
|
||||
import { Routes } from "./Routes";
|
||||
import { LoadingOverlay, TailwindIndicator } from "./components";
|
||||
import { createAxiosAuthActions, createAxiosDataProvider } from "./lib/axios";
|
||||
@ -12,14 +12,22 @@ import i18n from "./locales/i18n.ts";
|
||||
import { AppUpdateNotice } from "./components/AppUpdateNotice";
|
||||
|
||||
function App() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
staleTime: 10000, // Specify a staleTime to only fetch when the data is older than a certain amount of time
|
||||
const { t } = useTranslation();
|
||||
useEffect(() => {
|
||||
document.title = t("app.title");
|
||||
document.documentElement.lang = i18n.resolvedLanguage || "es";
|
||||
}, [t]);
|
||||
|
||||
const [queryClient] = useState(() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
staleTime: 10000, // Specify a staleTime to only fetch when the data is older than a certain amount of time
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<I18nextProvider i18n={i18n}>
|
||||
|
||||
@ -24,7 +24,7 @@ export const Routes = () => {
|
||||
const routesForErrors = [
|
||||
{
|
||||
path: "*",
|
||||
Element: (
|
||||
element: (
|
||||
<Suspense fallback={<LoadingOverlay />}>
|
||||
<ErrorPage />
|
||||
</Suspense>
|
||||
|
||||
@ -1,19 +1,22 @@
|
||||
import { Button } from "@/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export const ErrorPage = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className='flex min-h-[100dvh] flex-col items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8'>
|
||||
<div className='max-w-md mx-auto text-center'>
|
||||
<div className='w-12 h-12 mx-auto text-primary' />
|
||||
<h1 className='mt-4 text-3xl font-bold tracking-tight text-foreground sm:text-4xl'>
|
||||
Oops, page not found!
|
||||
{t("error_page.title")}
|
||||
</h1>
|
||||
<p className='mt-4 text-muted-foreground'>
|
||||
The page you're looking for doesn't exist or has been moved.
|
||||
{t("error_page.description")}
|
||||
</p>
|
||||
<div className='mt-6'>
|
||||
<Button className='inline-flex items-center px-4 py-2 text-sm font-medium transition-colors rounded-md shadow-sm bg-primary text-primary-foreground hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2'>
|
||||
Go to Homepage
|
||||
<Button asChild className='inline-flex items-center px-4 py-2 text-sm font-medium transition-colors rounded-md shadow-sm bg-primary text-primary-foreground hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2'>
|
||||
<Link to='/'>{t("error_page.go_home")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -15,22 +15,22 @@ import {
|
||||
} from "@/ui";
|
||||
import { joiResolver } from "@hookform/resolvers/joi";
|
||||
import { ILogin_DTO } from "@shared/contexts";
|
||||
import { t } from "i18next";
|
||||
import Joi from "joi";
|
||||
import { AlertCircleIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
import { Trans } from "react-i18next";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import SpanishJoiMessages from "../../spanish-joi-messages.json";
|
||||
import { createLoginSchema } from "./loginSchema";
|
||||
|
||||
type LoginDataForm = ILogin_DTO;
|
||||
|
||||
export const LoginPage = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { mutate: login } = useLogin({
|
||||
onSuccess: (data) => {
|
||||
const { success, error } = data;
|
||||
if (!success && error) {
|
||||
form.setError("root", error);
|
||||
form.setError("root", { type: error.name, message: error.message });
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
@ -44,19 +44,17 @@ export const LoginPage = () => {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
resolver: joiResolver(
|
||||
Joi.object({
|
||||
email: Joi.string()
|
||||
.email({ tlds: { allow: false } })
|
||||
.required(),
|
||||
password: Joi.string().min(4).alphanum().required(),
|
||||
}),
|
||||
{
|
||||
messages: SpanishJoiMessages,
|
||||
}
|
||||
),
|
||||
resolver: joiResolver(createLoginSchema(t)),
|
||||
});
|
||||
|
||||
const { getFieldState, trigger } = form;
|
||||
useEffect(() => {
|
||||
const invalidFields = (["email", "password"] as const).filter(
|
||||
(field) => getFieldState(field).invalid
|
||||
);
|
||||
if (invalidFields.length) void trigger(invalidFields);
|
||||
}, [i18n.resolvedLanguage, getFieldState, trigger]);
|
||||
|
||||
const onSubmit: SubmitHandler<LoginDataForm> = async (data) => {
|
||||
login({ email: data.email, password: data.password }, {});
|
||||
};
|
||||
@ -113,7 +111,11 @@ export const LoginPage = () => {
|
||||
<AlertTitle>
|
||||
<Trans i18nKey='common.error' />
|
||||
</AlertTitle>
|
||||
<AlertDescription>{form.formState.errors.root?.message}</AlertDescription>
|
||||
<AlertDescription>
|
||||
{form.formState.errors.root?.type === "InvalidCredentials"
|
||||
? t("login_page.invalid_credentials")
|
||||
: form.formState.errors.root?.message}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
@ -138,7 +140,7 @@ export const LoginPage = () => {
|
||||
<div className='hidden bg-muted lg:block'>
|
||||
<img
|
||||
src='/img/nara2.jpg'
|
||||
alt='Image'
|
||||
alt=''
|
||||
width='1920'
|
||||
height='1080'
|
||||
className='h-full w-full object-cover dark:brightness-[0.2] dark:grayscale'
|
||||
|
||||
@ -21,7 +21,6 @@ import {
|
||||
} from "@/ui";
|
||||
import { joiResolver } from "@hookform/resolvers/joi";
|
||||
import { ILogin_DTO } from "@shared/contexts";
|
||||
import Joi from "joi";
|
||||
|
||||
import { useToast } from "@/ui/use-toast";
|
||||
import { AlertCircleIcon, ChevronDownIcon } from "lucide-react";
|
||||
@ -29,7 +28,7 @@ import { useEffect, useState } from "react";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import SpanishJoiMessages from "../../spanish-joi-messages.json";
|
||||
import { createLoginSchema } from "./loginSchema";
|
||||
type LoginDataForm = ILogin_DTO;
|
||||
|
||||
export const LoginPageWithLanguageSelector = () => {
|
||||
@ -56,7 +55,7 @@ export const LoginPageWithLanguageSelector = () => {
|
||||
onSuccess: (data) => {
|
||||
const { success, error } = data;
|
||||
if (!success && error) {
|
||||
form.setError("root", error);
|
||||
form.setError("root", { type: error.name, message: error.message });
|
||||
}
|
||||
|
||||
if (success) {
|
||||
@ -76,19 +75,17 @@ export const LoginPageWithLanguageSelector = () => {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
resolver: joiResolver(
|
||||
Joi.object({
|
||||
email: Joi.string()
|
||||
.email({ tlds: { allow: false } })
|
||||
.required(),
|
||||
password: Joi.string().min(4).alphanum().required(),
|
||||
}),
|
||||
{
|
||||
messages: SpanishJoiMessages,
|
||||
}
|
||||
),
|
||||
resolver: joiResolver(createLoginSchema(t)),
|
||||
});
|
||||
|
||||
const { getFieldState, trigger } = form;
|
||||
useEffect(() => {
|
||||
const invalidFields = (["email", "password"] as const).filter(
|
||||
(field) => getFieldState(field).invalid
|
||||
);
|
||||
if (invalidFields.length) void trigger(invalidFields);
|
||||
}, [i18n.resolvedLanguage, getFieldState, trigger]);
|
||||
|
||||
const onSubmit: SubmitHandler<LoginDataForm> = async (data) => {
|
||||
login({ email: data.email, password: data.password }, {});
|
||||
};
|
||||
@ -172,7 +169,11 @@ export const LoginPageWithLanguageSelector = () => {
|
||||
<AlertTitle>
|
||||
<Trans i18nKey='common.error' />
|
||||
</AlertTitle>
|
||||
<AlertDescription>{form.formState.errors.root?.message}</AlertDescription>
|
||||
<AlertDescription>
|
||||
{form.formState.errors.root?.type === "InvalidCredentials"
|
||||
? t("login_page.invalid_credentials")
|
||||
: form.formState.errors.root?.message}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
@ -201,7 +202,7 @@ export const LoginPageWithLanguageSelector = () => {
|
||||
<div className='hidden bg-muted lg:block'>
|
||||
<img
|
||||
src='/img/nara2.jpg'
|
||||
alt='Image'
|
||||
alt=''
|
||||
width='1920'
|
||||
height='1080'
|
||||
className='h-full w-full object-cover dark:brightness-[0.2] dark:grayscale'
|
||||
|
||||
42
client/src/app/auth/loginSchema.test.ts
Normal file
42
client/src/app/auth/loginSchema.test.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { createInstance } from "i18next";
|
||||
import es from "../../locales/es.json";
|
||||
import en from "../../locales/en.json";
|
||||
import fr from "../../locales/fr.json";
|
||||
import ca from "../../locales/ca.json";
|
||||
import { createLoginSchema } from "./loginSchema";
|
||||
|
||||
describe("login validation", () => {
|
||||
it.each(["es", "en", "fr", "ca"])("uses %s for every login validation rule", async (lng) => {
|
||||
const i18n = createInstance();
|
||||
await i18n.init({ lng, fallbackLng: false, resources: { es, en, fr, ca } });
|
||||
const schema = createLoginSchema(i18n.t);
|
||||
const valid = { email: "dealer@example.com", password: "abcd1234" };
|
||||
const cases = [
|
||||
{ data: { ...valid, email: "" }, key: "email_required" },
|
||||
{ data: { password: valid.password }, key: "email_required" },
|
||||
{ data: { ...valid, email: "invalid" }, key: "email_invalid" },
|
||||
{ data: { ...valid, password: "" }, key: "password_required" },
|
||||
{ data: { email: valid.email }, key: "password_required" },
|
||||
{ data: { ...valid, password: "abc" }, key: "password_min" },
|
||||
{ data: { ...valid, password: "abcd!" }, key: "password_alphanum" },
|
||||
];
|
||||
|
||||
expect(schema.validate(valid).error).toBeUndefined();
|
||||
for (const { data, key } of cases) {
|
||||
const message = schema.validate(data).error?.details[0].message;
|
||||
expect(message).toBe(i18n.t(`login_page.validation.${key}`, { count: 4 }));
|
||||
expect(message).not.toContain("{{");
|
||||
}
|
||||
});
|
||||
|
||||
it("uses the new language when the resolver schema is rebuilt", async () => {
|
||||
const i18n = createInstance();
|
||||
await i18n.init({ lng: "es", resources: { es, fr } });
|
||||
const invalid = { email: "invalid", password: "abcd1234" };
|
||||
expect(createLoginSchema(i18n.t).validate(invalid).error?.message)
|
||||
.toBe(es.translation.login_page.validation.email_invalid);
|
||||
await i18n.changeLanguage("fr");
|
||||
expect(createLoginSchema(i18n.t).validate(invalid).error?.message)
|
||||
.toBe(fr.translation.login_page.validation.email_invalid);
|
||||
});
|
||||
});
|
||||
26
client/src/app/auth/loginSchema.ts
Normal file
26
client/src/app/auth/loginSchema.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { TFunction } from "i18next";
|
||||
import Joi from "joi";
|
||||
|
||||
export const createLoginSchema = (t: TFunction) =>
|
||||
Joi.object({
|
||||
email: Joi.string()
|
||||
.email({ tlds: { allow: false } })
|
||||
.required()
|
||||
.messages({
|
||||
"any.required": t("login_page.validation.email_required"),
|
||||
"string.empty": t("login_page.validation.email_required"),
|
||||
"string.base": t("login_page.validation.email_invalid"),
|
||||
"string.email": t("login_page.validation.email_invalid"),
|
||||
}),
|
||||
password: Joi.string()
|
||||
.min(4)
|
||||
.alphanum()
|
||||
.required()
|
||||
.messages({
|
||||
"any.required": t("login_page.validation.password_required"),
|
||||
"string.empty": t("login_page.validation.password_required"),
|
||||
"string.base": t("login_page.validation.password_alphanum"),
|
||||
"string.min": t("login_page.validation.password_min", { count: 4 }),
|
||||
"string.alphanum": t("login_page.validation.password_alphanum"),
|
||||
}),
|
||||
});
|
||||
@ -4,10 +4,12 @@ import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DataTablaRowActionFunction, DataTableRowActions } from "@/components";
|
||||
import { Badge } from "@/ui";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const useCatalogTableColumns = (
|
||||
actions: DataTablaRowActionFunction<IListArticles_Response_DTO>
|
||||
): ColumnDef<IListArticles_Response_DTO>[] => {
|
||||
const { t } = useTranslation();
|
||||
const customerColumns: ColumnDef<IListArticles_Response_DTO>[] = useMemo(
|
||||
() => [
|
||||
/*{
|
||||
@ -42,14 +44,14 @@ export const useCatalogTableColumns = (
|
||||
{
|
||||
id: "company",
|
||||
accessorKey: "company_name",
|
||||
header: "Compañía",
|
||||
header: t("common.company"),
|
||||
enableSorting: true,
|
||||
sortingFn: "alphanumeric",
|
||||
},
|
||||
{
|
||||
id: "state",
|
||||
accessorKey: "state",
|
||||
header: "Estado",
|
||||
header: t("quotes.list.columns.status"),
|
||||
cell: ({ renderValue }) => (
|
||||
<Badge variant={"destructive"}>
|
||||
<>{renderValue()}</>
|
||||
@ -59,19 +61,19 @@ export const useCatalogTableColumns = (
|
||||
{
|
||||
id: "phone",
|
||||
accessorKey: "phone",
|
||||
header: "Phone",
|
||||
header: t("common.phone"),
|
||||
enableSorting: true,
|
||||
sortingFn: "alphanumeric",
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Acciones",
|
||||
header: t("common.actions"),
|
||||
cell: (context) => {
|
||||
return <DataTableRowActions rowContext={context} actions={actions} />;
|
||||
},
|
||||
},
|
||||
],
|
||||
[actions]
|
||||
[actions, t]
|
||||
);
|
||||
return customerColumns;
|
||||
};
|
||||
|
||||
@ -7,12 +7,13 @@ import { DataTableToolbar } from "@/components/DataTable/DataTableToolbar";
|
||||
import { useDataTable, useDataTableContext } from "@/lib/hooks";
|
||||
import { IListArticles_Response_DTO } from "@shared/contexts";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { t } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemo } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useDealerList } from "../hooks";
|
||||
|
||||
export const DealerDataTable = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { pagination, globalFilter, isFiltered } = useDataTableContext();
|
||||
|
||||
@ -29,17 +30,19 @@ export const DealerDataTable = () => {
|
||||
{
|
||||
id: "id" as const,
|
||||
accessorKey: "id",
|
||||
header: () => t("common.identifier"),
|
||||
enableResizing: false,
|
||||
size: 10,
|
||||
},
|
||||
{
|
||||
id: "name" as const,
|
||||
accessorKey: "name",
|
||||
header: () => t("common.name"),
|
||||
enableResizing: false,
|
||||
size: 10,
|
||||
},
|
||||
],
|
||||
[]
|
||||
[t]
|
||||
);
|
||||
|
||||
const { table } = useDataTable({
|
||||
|
||||
@ -4,10 +4,12 @@ import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DataTablaRowActionFunction, DataTableRowActions } from "@/components";
|
||||
import { Badge } from "@/ui";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const useCustomerInvoiceDataTableColumns = (
|
||||
actions: DataTablaRowActionFunction<IListArticles_Response_DTO>
|
||||
): ColumnDef<IListArticles_Response_DTO>[] => {
|
||||
const { t } = useTranslation();
|
||||
const customerColumns: ColumnDef<IListArticles_Response_DTO>[] = useMemo(
|
||||
() => [
|
||||
/*{
|
||||
@ -42,14 +44,14 @@ export const useCustomerInvoiceDataTableColumns = (
|
||||
{
|
||||
id: "company",
|
||||
accessorKey: "company_name",
|
||||
header: "Compañía",
|
||||
header: t("common.company"),
|
||||
enableSorting: true,
|
||||
sortingFn: "alphanumeric",
|
||||
},
|
||||
{
|
||||
id: "state",
|
||||
accessorKey: "state",
|
||||
header: "Estado",
|
||||
header: t("quotes.list.columns.status"),
|
||||
cell: ({ renderValue }) => (
|
||||
<Badge variant={"destructive"}>
|
||||
<>{renderValue()}</>
|
||||
@ -59,19 +61,19 @@ export const useCustomerInvoiceDataTableColumns = (
|
||||
{
|
||||
id: "phone",
|
||||
accessorKey: "phone",
|
||||
header: "Phone",
|
||||
header: t("common.phone"),
|
||||
enableSorting: true,
|
||||
sortingFn: "alphanumeric",
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Acciones",
|
||||
header: t("common.actions"),
|
||||
cell: (context) => {
|
||||
return <DataTableRowActions rowContext={context} actions={actions} />;
|
||||
},
|
||||
},
|
||||
],
|
||||
[actions]
|
||||
[actions, t]
|
||||
);
|
||||
return customerColumns;
|
||||
};
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
Label,
|
||||
Progress,
|
||||
} from "@/ui";
|
||||
import { t } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEffect, useId } from "react";
|
||||
|
||||
type DownloadQuoteDialogProps = Omit<UseDownloader, "download"> & {
|
||||
@ -19,6 +19,7 @@ type DownloadQuoteDialogProps = Omit<UseDownloader, "download"> & {
|
||||
};
|
||||
|
||||
export const DownloadQuoteDialog = (props: DownloadQuoteDialogProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { percentage, cancel, error, isInProgress, onFinishDownload } = props;
|
||||
const panelId = useId();
|
||||
|
||||
@ -52,7 +53,7 @@ export const DownloadQuoteDialog = (props: DownloadQuoteDialogProps) => {
|
||||
max={100}
|
||||
getValueLabel={() => t("quotes.downloading_dialog.progress.label", {})}
|
||||
/>
|
||||
{error && <p>Error: {JSON.stringify(error)}</p>}
|
||||
{error && <p role="alert">{t("quotes.downloading_dialog.error")}</p>}
|
||||
</div>
|
||||
<DialogFooter className='sm:justify-end'>
|
||||
<DialogClose asChild>
|
||||
|
||||
@ -1,123 +1,17 @@
|
||||
import { ScrollArea } from "@/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import esBlocks from "./locales/es.json";
|
||||
import enBlocks from "./locales/en.json";
|
||||
import frBlocks from "./locales/fr.json";
|
||||
|
||||
export type BlockDataType = {
|
||||
id: number;
|
||||
id?: number;
|
||||
code: string;
|
||||
title: string;
|
||||
body: string;
|
||||
};
|
||||
|
||||
const blockData: Record<string, BlockDataType[]> = {
|
||||
es: [
|
||||
{
|
||||
id: 50,
|
||||
code: "CAP_SEP_AMBIENTES_ES",
|
||||
title: "CARACTERISTICAS SEPARACIÓN DE AMBIENTES",
|
||||
body: "Medida de Hueco: 2450x3000 mm.\r\nModelo: LYN / Acabado: ANODIZADO / Color: NEGRO\r\nNº de travesaños: 2 TRAVESAÑOS HORIZONTALES\r\nAcabado paneles: TRANSPARENTE TEMPLADO \r\nTirador: DOBLE POR PUERTA\r\nFreno: DOBLE POR PUERTA (APERTURA Y CIERRE)\r\nGuía: VISTA (DOBLE EMBELLECEDOR)\r\nNº de hojas: 2 CORREDERAS + 2 FIJOS",
|
||||
},
|
||||
{
|
||||
id: 51,
|
||||
code: "CAP_FRE_ARM_CORREDERA_ES",
|
||||
title: "CARACTERISTICAS FRENTE DE ARMARIO CORREDERA",
|
||||
body: "Medida de Hueco:2440x5180mm.\r\nModelo: PANORAMIC P.O. / Acabado: LACADO / Color: BLANCO\r\nNº de lístelos: SIN LISTELOS / Acabado paneles: PANEL LACADO\r\nTirador:\r\nCerco perimetral: \r\nNº de hojas:\r\nFRENO HIDRAULICO INCORPORADO O FRENO MAGNETICO INCORPORADO\r\nCompuesto por:",
|
||||
},
|
||||
{
|
||||
id: 52,
|
||||
code: "CAP_FRE_ARM_ABATIBLE_ES",
|
||||
title: "CARACTERISTICAS FRENTE DE ARMARIO",
|
||||
body: "Medida de hueco: 2275x1390 mm.\r\nModelo: ARGO / Acabado: LACADO / Color: A DETERMINAR\r\nGrosor hoja: 22 o 19 mm\r\nAcabado de hojas: LISAS\r\nTirador: UÑERO A DETERMINAR\r\nAcabado herrajes: CROMADO O ANTRACITA\r\nJambas perimetrales: VISTAS, REMETIDAS O COMBI A DETERMINAR.\r\nNº de hojas: 2\r\nCompuesto por:",
|
||||
},
|
||||
{
|
||||
id: 53,
|
||||
code: "CAP_INT_ARM_FORRADO_ES",
|
||||
title: "CARACTERISTICAS INTERIOR DE ARMARIO",
|
||||
body: "Acabado: MELAMINA / color: A DETERMINAR\r\nEspesor bloque: 19 mm. / Espesor complementos: 19 mm.\r\nBarras de colgar: CROMO/ NEGRA/ BRONCE\r\nFrentes cristal cajones: RECTO / “U” – FLOAT BRONCE C-424\r\nTirador cajones: FRENTE BAJO / Cajones: 4 DE 180 MM. A DETERMINAR\r\nBaldas deslizantes: X DE 145 MM. A DETERMINAR\r\nCompuesto por:",
|
||||
},
|
||||
{
|
||||
id: 54,
|
||||
code: "CAP_CLA_SEMI_SUSPENDIDO_ES",
|
||||
title: "Acabado: MELAMINA / color: A DETERMINAR",
|
||||
body: "Medida de Hueco: 2450x3000 mm.\r\nEspesor bloque: 25 mm. / Espesor complementos: 25 mm.\r\nBarras de colgar: CROMO/ NEGRA/ BRONCE\r\nFrentes cristal cajones: RECTO / “U” – FLOAT BRONCE C-424\r\nTirador cajones: FRENTE BAJO / Cajones: 4 DE 180 MM. A DETERMINAR\r\nBaldas deslizantes: X DE 145 MM. A DETERMINAR\r\nCompuesto por:",
|
||||
},
|
||||
{
|
||||
id: 2000000108,
|
||||
code: "CAP_INT_EST_CRISTAL_ES",
|
||||
title: "Acabado bastidores: ANODIZADO NEGRO",
|
||||
body: "Acabado panel bastidores laterales: CRISTAL TEMPLADO FLOAT BRONCE C-424\r\nAcabado trasera: CRISTAL TEMPLADO FLOAT BRONCE C-424\r\nAcabado complementos: MELAMINA / Color: ROBLE DENBER M-242\r\nEspesor complementos: 25 mm.\r\nBarras de colgar: NEGRA\r\nFrentes cristal cajones: RECTO / “U” – FLOAT BRONCE C-424 \r\nTirador cajones: xxxxx / Cajones: 4 DE 180 MM.\r\nCompuesto por:",
|
||||
},
|
||||
],
|
||||
en: [
|
||||
{
|
||||
id: 2000000098,
|
||||
code: "CAP_CLA_SEMI_SUSPENDIDO_EN",
|
||||
title: "Finish: MELAMINE / colour: TO BE DETERMINED",
|
||||
body: 'Size of opening: 2450x3000 mm.\r\nThickness block: 19 mm / Thickness complements: 19 mm.\r\nHanging rods: CHROME/ BLACK\r\nGlass drawer fronts: STRAIGHT / "U" - FLOAT BRONZE C-424 \r\nDrawer handle: xxxxx / Drawers: 4 DE 180 MM.\r\nShoe rack door: YES/NO / Profile finish: BLACK LACQUER / SILVER MATTE\r\nShoe cabinet door glass finish: FLOAT BRONZE C-424\r\nComposed of:',
|
||||
},
|
||||
{
|
||||
id: 2000000084,
|
||||
code: "CAP_FRE_ARM_ABATIBLE_EN",
|
||||
title: "CHARACTERISTICS OF THE FRONT OF THE HINGED WARDROBE",
|
||||
body: "Size of opening: 2275x1390 mm.\r\nModel: ARGO / Finish: LACQUER / Colour: TO BE DETERMINED\r\nDoor thickness: 22 or 19 mm\r\nDoor finish: SMOOTH \r\nHandle: KNOB TO BE DETERMINED\r\nHardware finish: CHROME OR ANTHRACITE\r\nPerimetral doorposts: VISIBLE, REMETED OR COMBI TO BE DETERMINED.\r\nNumber of doors: 2 \r\nComposed of:",
|
||||
},
|
||||
{
|
||||
id: 2000000089,
|
||||
code: "CAP_FRE_ARM_CORREDERA_EN",
|
||||
title: "Size of opening: 2440x5180mm. ",
|
||||
body: "Model: PANORAMIC P.O. / Finish: LACQUER / Colour: WHITE\r\nNo. of listers: NO LISTELS / Panels finish: LACQUERED PANEL \r\nHandle: \r\nPerimeter frame: \r\nNº of doors: \r\nINCORPORATED HYDRAULIC BRAKE (not available in lower doors of 650 mm.) OR INCORPORATED MAGNETIC BRAKE. \r\nComposed of:",
|
||||
},
|
||||
{
|
||||
id: 2000000092,
|
||||
code: "CAP_INT_ARM_FORRADO_EN",
|
||||
title: "Finish: MELAMINE / colour: TO BE DETERMINED",
|
||||
body: 'Thickness block: 19 mm / Thickness complements: 19 mm.\r\nHanging rods: CHROME/ BLACK\r\nGlass drawer fronts: STRAIGHT / "U" - FLOAT BRONZE C-424 \r\nDrawer handle: xxxxx / Drawers: 4 DE 180 MM.\r\nComposed of:',
|
||||
},
|
||||
{
|
||||
id: 2000000109,
|
||||
code: "CAP_INT_EST_CRISTAL_EN",
|
||||
title: "Frames finish: BLACK ANODIZED",
|
||||
body: 'Side frame panel finish: BRONZE FLOAT BRONZE GLASS C-424\r\nRear panel finish: BRONZE FLOAT GLASS C-424\r\nComplements finish: MELAMIN / Colour: DENBER OAK M-242\r\nThickness of accessories: 25 mm.\r\nHanging bars: BLACK\r\nGlass drawer fronts: STRAIGHT / "U" - FLOAT BRONZE C-424 \r\nDrawer handle: xxxxx / Drawers: 4 DE 180 MM.\r\nComposed of:',
|
||||
},
|
||||
{
|
||||
id: 2000000095,
|
||||
code: "CAP_SEP_AMBIENTES_EN",
|
||||
title: "CHARACTERISTICS SEPARATION OF ROOMS",
|
||||
body: "Size of opening: 2450x3000 mm.\r\nModel: LYN / Finish: ANODISED / Colour: BLACK\r\nNo. of crosspieces: 2 HORIZONTAL CROSSBARS\r\nPanel finish: TRANSPARENT TEMPLARED \r\nHandle: DOUBLE PER DOOR\r\nBrake: DOUBLE PER DOOR (OPENING AND CLOSING)\r\nGuide: VISTA (DOUBLE EMBELLECEDOR)\r\nNumber of panels: 2 SLIDING + 2 FIXED",
|
||||
},
|
||||
],
|
||||
fr: [
|
||||
{
|
||||
id: 2000000121,
|
||||
code: "CAP_CLA_SEMI_SUSPENDIDO_FR",
|
||||
title: "Finition : MÉLAMINE / Couleur : À DÉTERMINER",
|
||||
body: "Épaisseur du bloc : 19 mm / Épaisseur des accessoires : 19 mm\r\nBarres de penderie : CHROME / NOIR\r\nFaçades des tiroirs en verre : DROITE / “U” – FLOAT BRONZE C-424\r\nPoignée des tiroirs : xxxxx\r\nTiroirs : 4 DE 180 MM\r\nPorte de chaussures : OUI / NON\r\nFinition du profil : LAQUÉ NOIR / ARGENT MAT\r\nFinition du verre de la porte de chaussures : FLOAT BRONZE C-424\r\nComposé de :",
|
||||
},
|
||||
{
|
||||
id: 2000000123,
|
||||
code: "CAP_FRE_ARM_ABATIBLE_FR",
|
||||
title: "Dimension de l’ouverture : 2275x1390 mm",
|
||||
body: "Modèle : ARGO / Finition : LAQUÉE / Couleur : À DÉTERMINER\r\nÉpaisseur des vantaux : 22 ou 19 mm\r\nFinition des vantaux : LISSES\r\nPoignée : PRISE DE MAIN À DÉTERMINER\r\nFinition des ferrures : CHROMÉ OU ANTHRACITE\r\nJambages périmétraux : APPARENTS, ENCASTRÉS OU COMBINÉS À DÉTERMINER\r\nNombre de vantaux : 2\r\nComposé de :",
|
||||
},
|
||||
{
|
||||
id: 2000000125,
|
||||
code: "CAP_FRE_ARM_CORREDERA_FR",
|
||||
title: "Dimension de l’ouverture : 2440x5180mm",
|
||||
body: "Modèle : PANORAMIC P.O. / Finition : LAQUÉE / Couleur : BLANC\r\nNombre de listels : SANS LISTELS\r\nFinition des panneaux : PANNEAU LAQUÉ\r\nPoignée :\r\nEncadrement périmétral :\r\nNombre de vantaux :\r\nFREIN HYDRAULIQUE INTÉGRÉ (non disponible pour les portes inférieures à 650 mm) OU FREIN MAGNÉTIQUE INTÉGRÉ\r\nComposé de :",
|
||||
},
|
||||
{
|
||||
id: 2000000129,
|
||||
code: "CAP_INT_ARM_FORRADO_FR",
|
||||
title: "Finition : MÉLAMINE / Couleur : À DÉTERMINER",
|
||||
body: "Épaisseur du bloc : 19 mm / Épaisseur des accessoires : 19 mm\r\nBarres de penderie : CHROME / NOIR\r\nFaçades des tiroirs en verre : DROITE / “U” – FLOAT BRONZE C-424\r\nPoignée des tiroirs : xxxxx\r\nTiroirs : 4 DE 180 MM\r\nComposé de :",
|
||||
},
|
||||
{
|
||||
id: 2000000131,
|
||||
code: "CAP_INT_EST_CRISTAL_FR",
|
||||
title: "Finition des cadres : ANODISÉ NOIR",
|
||||
body: "Finition des panneaux des cadres latéraux : VERRE TREMPÉ FLOAT BRONZE C-424\r\nFinition du dos : VERRE TREMPÉ FLOAT BRONZE C-424\r\nFinition des accessoires : MÉLAMINE / Couleur : CHÊNE DENBER M-242\r\nÉpaisseur des accessoires : 25 mm\r\nBarres de penderie : NOIRES\r\nFaçades des tiroirs en verre : DROITE / “U” – FLOAT BRONZE C-424\r\nPoignée des tiroirs : xxxxx\r\nTiroirs : 4 DE 180 MM\r\nComposé de :",
|
||||
},
|
||||
],
|
||||
};
|
||||
const blockData: Record<string, BlockDataType[]> = { es: esBlocks, en: enBlocks, fr: frBlocks };
|
||||
|
||||
export const BlockList = ({
|
||||
onSelect,
|
||||
@ -135,10 +29,10 @@ export const BlockList = ({
|
||||
listState
|
||||
.filter(({ title, body }) => title || body)
|
||||
.sort((a, b) => a.title.localeCompare(b.title))
|
||||
.map((_block, _blockIdx) => {
|
||||
.map((_block) => {
|
||||
return (
|
||||
<button
|
||||
key={_block.id}
|
||||
key={_block.code}
|
||||
className='flex flex-col items-start gap-2 p-3 text-sm text-left transition-all border rounded-lg hover:bg-accent'
|
||||
onClick={() => onSelect && onSelect(_block, 1)}
|
||||
>
|
||||
|
||||
@ -0,0 +1,38 @@
|
||||
[
|
||||
{
|
||||
"id": 2000000098,
|
||||
"code": "CAP_CLA_SEMI_SUSPENDIDO_EN",
|
||||
"title": "Finish: MELAMINE / colour: TO BE DETERMINED",
|
||||
"body": "Size of opening: 2450x3000 mm.\r\nThickness block: 19 mm / Thickness complements: 19 mm.\r\nHanging rods: CHROME/ BLACK\r\nGlass drawer fronts: STRAIGHT / \"U\" - FLOAT BRONZE C-424 \r\nDrawer handle: xxxxx / Drawers: 4 DE 180 MM.\r\nShoe rack door: YES/NO / Profile finish: BLACK LACQUER / SILVER MATTE\r\nShoe cabinet door glass finish: FLOAT BRONZE C-424\r\nComposed of:"
|
||||
},
|
||||
{
|
||||
"id": 2000000084,
|
||||
"code": "CAP_FRE_ARM_ABATIBLE_EN",
|
||||
"title": "CHARACTERISTICS OF THE FRONT OF THE HINGED WARDROBE",
|
||||
"body": "Size of opening: 2275x1390 mm.\r\nModel: ARGO / Finish: LACQUER / Colour: TO BE DETERMINED\r\nDoor thickness: 22 or 19 mm\r\nDoor finish: SMOOTH \r\nHandle: KNOB TO BE DETERMINED\r\nHardware finish: CHROME OR ANTHRACITE\r\nPerimetral doorposts: VISIBLE, REMETED OR COMBI TO BE DETERMINED.\r\nNumber of doors: 2 \r\nComposed of:"
|
||||
},
|
||||
{
|
||||
"id": 2000000089,
|
||||
"code": "CAP_FRE_ARM_CORREDERA_EN",
|
||||
"title": "Size of opening: 2440x5180mm. ",
|
||||
"body": "Model: PANORAMIC P.O. / Finish: LACQUER / Colour: WHITE\r\nNo. of listers: NO LISTELS / Panels finish: LACQUERED PANEL \r\nHandle: \r\nPerimeter frame: \r\nNº of doors: \r\nINCORPORATED HYDRAULIC BRAKE (not available in lower doors of 650 mm.) OR INCORPORATED MAGNETIC BRAKE. \r\nComposed of:"
|
||||
},
|
||||
{
|
||||
"id": 2000000092,
|
||||
"code": "CAP_INT_ARM_FORRADO_EN",
|
||||
"title": "Finish: MELAMINE / colour: TO BE DETERMINED",
|
||||
"body": "Thickness block: 19 mm / Thickness complements: 19 mm.\r\nHanging rods: CHROME/ BLACK\r\nGlass drawer fronts: STRAIGHT / \"U\" - FLOAT BRONZE C-424 \r\nDrawer handle: xxxxx / Drawers: 4 DE 180 MM.\r\nComposed of:"
|
||||
},
|
||||
{
|
||||
"id": 2000000109,
|
||||
"code": "CAP_INT_EST_CRISTAL_EN",
|
||||
"title": "Frames finish: BLACK ANODIZED",
|
||||
"body": "Side frame panel finish: BRONZE FLOAT BRONZE GLASS C-424\r\nRear panel finish: BRONZE FLOAT GLASS C-424\r\nComplements finish: MELAMIN / Colour: DENBER OAK M-242\r\nThickness of accessories: 25 mm.\r\nHanging bars: BLACK\r\nGlass drawer fronts: STRAIGHT / \"U\" - FLOAT BRONZE C-424 \r\nDrawer handle: xxxxx / Drawers: 4 DE 180 MM.\r\nComposed of:"
|
||||
},
|
||||
{
|
||||
"id": 2000000095,
|
||||
"code": "CAP_SEP_AMBIENTES_EN",
|
||||
"title": "CHARACTERISTICS SEPARATION OF ROOMS",
|
||||
"body": "Size of opening: 2450x3000 mm.\r\nModel: LYN / Finish: ANODISED / Colour: BLACK\r\nNo. of crosspieces: 2 HORIZONTAL CROSSBARS\r\nPanel finish: TRANSPARENT TEMPLARED \r\nHandle: DOUBLE PER DOOR\r\nBrake: DOUBLE PER DOOR (OPENING AND CLOSING)\r\nGuide: VISTA (DOUBLE EMBELLECEDOR)\r\nNumber of panels: 2 SLIDING + 2 FIXED"
|
||||
}
|
||||
]
|
||||
@ -0,0 +1,38 @@
|
||||
[
|
||||
{
|
||||
"id": 50,
|
||||
"code": "CAP_SEP_AMBIENTES_ES",
|
||||
"title": "CARACTERISTICAS SEPARACIÓN DE AMBIENTES",
|
||||
"body": "Medida de Hueco: 2450x3000 mm.\r\nModelo: LYN / Acabado: ANODIZADO / Color: NEGRO\r\nNº de travesaños: 2 TRAVESAÑOS HORIZONTALES\r\nAcabado paneles: TRANSPARENTE TEMPLADO \r\nTirador: DOBLE POR PUERTA\r\nFreno: DOBLE POR PUERTA (APERTURA Y CIERRE)\r\nGuía: VISTA (DOBLE EMBELLECEDOR)\r\nNº de hojas: 2 CORREDERAS + 2 FIJOS"
|
||||
},
|
||||
{
|
||||
"id": 51,
|
||||
"code": "CAP_FRE_ARM_CORREDERA_ES",
|
||||
"title": "CARACTERISTICAS FRENTE DE ARMARIO CORREDERA",
|
||||
"body": "Medida de Hueco:2440x5180mm.\r\nModelo: PANORAMIC P.O. / Acabado: LACADO / Color: BLANCO\r\nNº de lístelos: SIN LISTELOS / Acabado paneles: PANEL LACADO\r\nTirador:\r\nCerco perimetral: \r\nNº de hojas:\r\nFRENO HIDRAULICO INCORPORADO O FRENO MAGNETICO INCORPORADO\r\nCompuesto por:"
|
||||
},
|
||||
{
|
||||
"id": 52,
|
||||
"code": "CAP_FRE_ARM_ABATIBLE_ES",
|
||||
"title": "CARACTERISTICAS FRENTE DE ARMARIO",
|
||||
"body": "Medida de hueco: 2275x1390 mm.\r\nModelo: ARGO / Acabado: LACADO / Color: A DETERMINAR\r\nGrosor hoja: 22 o 19 mm\r\nAcabado de hojas: LISAS\r\nTirador: UÑERO A DETERMINAR\r\nAcabado herrajes: CROMADO O ANTRACITA\r\nJambas perimetrales: VISTAS, REMETIDAS O COMBI A DETERMINAR.\r\nNº de hojas: 2\r\nCompuesto por:"
|
||||
},
|
||||
{
|
||||
"id": 53,
|
||||
"code": "CAP_INT_ARM_FORRADO_ES",
|
||||
"title": "CARACTERISTICAS INTERIOR DE ARMARIO",
|
||||
"body": "Acabado: MELAMINA / color: A DETERMINAR\r\nEspesor bloque: 19 mm. / Espesor complementos: 19 mm.\r\nBarras de colgar: CROMO/ NEGRA/ BRONCE\r\nFrentes cristal cajones: RECTO / “U” – FLOAT BRONCE C-424\r\nTirador cajones: FRENTE BAJO / Cajones: 4 DE 180 MM. A DETERMINAR\r\nBaldas deslizantes: X DE 145 MM. A DETERMINAR\r\nCompuesto por:"
|
||||
},
|
||||
{
|
||||
"id": 54,
|
||||
"code": "CAP_CLA_SEMI_SUSPENDIDO_ES",
|
||||
"title": "Acabado: MELAMINA / color: A DETERMINAR",
|
||||
"body": "Medida de Hueco: 2450x3000 mm.\r\nEspesor bloque: 25 mm. / Espesor complementos: 25 mm.\r\nBarras de colgar: CROMO/ NEGRA/ BRONCE\r\nFrentes cristal cajones: RECTO / “U” – FLOAT BRONCE C-424\r\nTirador cajones: FRENTE BAJO / Cajones: 4 DE 180 MM. A DETERMINAR\r\nBaldas deslizantes: X DE 145 MM. A DETERMINAR\r\nCompuesto por:"
|
||||
},
|
||||
{
|
||||
"id": 2000000108,
|
||||
"code": "CAP_INT_EST_CRISTAL_ES",
|
||||
"title": "Acabado bastidores: ANODIZADO NEGRO",
|
||||
"body": "Acabado panel bastidores laterales: CRISTAL TEMPLADO FLOAT BRONCE C-424\r\nAcabado trasera: CRISTAL TEMPLADO FLOAT BRONCE C-424\r\nAcabado complementos: MELAMINA / Color: ROBLE DENBER M-242\r\nEspesor complementos: 25 mm.\r\nBarras de colgar: NEGRA\r\nFrentes cristal cajones: RECTO / “U” – FLOAT BRONCE C-424 \r\nTirador cajones: xxxxx / Cajones: 4 DE 180 MM.\r\nCompuesto por:"
|
||||
}
|
||||
]
|
||||
@ -0,0 +1,44 @@
|
||||
[
|
||||
{
|
||||
"id": 2000000121,
|
||||
"code": "CAP_CLA_SEMI_SUSPENDIDO_FR",
|
||||
"title": "CARACTÉRISTIQUES DRESSING CLASSIQUE / SEMI-SUSPENDU / SUSPENDU",
|
||||
"body": "Finition : MÉLAMINE / Couleur : À DÉTERMINER\r\nÉpaisseur du bloc : 19 mm / Épaisseur des accessoires : 19 mm\r\nBarres de penderie : CHROME / NOIR\r\nFaçades des tiroirs en verre : DROITE / “U” – FLOAT BRONZE C-424\r\nPoignée des tiroirs : xxxxx\r\nTiroirs : 4 DE 180 MM\r\nPorte de chaussures : OUI / NON\r\nFinition du profil : LAQUÉ NOIR / ARGENT MAT\r\nFinition du verre de la porte de chaussures : FLOAT BRONZE C-424\r\nComposé de :"
|
||||
},
|
||||
{
|
||||
"id": 2000000123,
|
||||
"code": "CAP_FRE_ARM_ABATIBLE_FR",
|
||||
"title": "CARACTÉRISTIQUES FAÇADE DE PLACARD BATTANT",
|
||||
"body": "Dimension de l’ouverture : 2275x1390 mm\r\nModèle : ARGO / Finition : LAQUÉE / Couleur : À DÉTERMINER\r\nÉpaisseur des vantaux : 22 ou 19 mm\r\nFinition des vantaux : LISSES\r\nPoignée : PRISE DE MAIN À DÉTERMINER\r\nFinition des ferrures : CHROMÉ OU ANTHRACITE\r\nJambages périmétraux : APPARENTS, ENCASTRÉS OU COMBINÉS À DÉTERMINER\r\nNombre de vantaux : 2\r\nComposé de :"
|
||||
},
|
||||
{
|
||||
"id": 2000000125,
|
||||
"code": "CAP_FRE_ARM_CORREDERA_FR",
|
||||
"title": "CARACTÉRISTIQUES FAÇADE DE PLACARD COULISSANT",
|
||||
"body": "Dimension de l’ouverture : 2440x5180mm\r\nModèle : PANORAMIC P.O. / Finition : LAQUÉE / Couleur : BLANC\r\nNombre de listels : SANS LISTELS\r\nFinition des panneaux : PANNEAU LAQUÉ\r\nPoignée :\r\nEncadrement périmétral :\r\nNombre de vantaux :\r\nFREIN HYDRAULIQUE INTÉGRÉ (non disponible pour les portes inférieures à 650 mm) OU FREIN MAGNÉTIQUE INTÉGRÉ\r\nComposé de :"
|
||||
},
|
||||
{
|
||||
"id": 2000000129,
|
||||
"code": "CAP_INT_ARM_FORRADO_FR",
|
||||
"title": "CARACTÉRISTIQUES INTÉRIEUR DE PLACARD HABILLÉ",
|
||||
"body": "Finition : MÉLAMINE / Couleur : À DÉTERMINER\r\nÉpaisseur du bloc : 19 mm / Épaisseur des accessoires : 19 mm\r\nBarres de penderie : CHROME / NOIR\r\nFaçades des tiroirs en verre : DROITE / “U” – FLOAT BRONZE C-424\r\nPoignée des tiroirs : xxxxx\r\nTiroirs : 4 DE 180 MM\r\nComposé de :"
|
||||
},
|
||||
{
|
||||
"id": 2000000131,
|
||||
"code": "CAP_INT_EST_CRISTAL_FR",
|
||||
"title": "CARACTÉRISTIQUES INTÉRIEUR DE PLACARD STRUCTURE EN VERRE",
|
||||
"body": "Finition des cadres : ANODISÉ NOIR\r\nFinition des panneaux des cadres latéraux : VERRE TREMPÉ FLOAT BRONZE C-424\r\nFinition du dos : VERRE TREMPÉ FLOAT BRONZE C-424\r\nFinition des accessoires : MÉLAMINE / Couleur : CHÊNE DENBER M-242\r\nÉpaisseur des accessoires : 25 mm\r\nBarres de penderie : NOIRES\r\nFaçades des tiroirs en verre : DROITE / “U” – FLOAT BRONZE C-424\r\nPoignée des tiroirs : xxxxx\r\nTiroirs : 4 DE 180 MM\r\nComposé de :"
|
||||
},
|
||||
{
|
||||
"id": 2000000127,
|
||||
"code": "CAP_INSTALACION_FR",
|
||||
"title": "SERVICE D'INSTALLATION - PRIX NET",
|
||||
"body": "Armoire d'installation x"
|
||||
},
|
||||
{
|
||||
"id": 2000000134,
|
||||
"code": "CAP_SUMINISTRO_FR",
|
||||
"title": "SERVICE DE FOURNITURE - PRIX NET",
|
||||
"body": "Livraison du matériel au domicile du client"
|
||||
}
|
||||
]
|
||||
@ -32,7 +32,7 @@ const getExchangeRateRequestDate = (value: unknown): string | undefined => {
|
||||
|
||||
export const QuoteAdditionalCurrencyEditor = () => {
|
||||
const { control, formState, getValues, setValue } = useFormContext();
|
||||
const { formatNumber } = useLocalization();
|
||||
const { formatNumber, formatCurrencyName } = useLocalization();
|
||||
const { useExchangeRate } = useQuotes();
|
||||
const { mutate, isPending } = useExchangeRate();
|
||||
const enabled = Boolean(useWatch({ control, name: "additional_currency_code" }));
|
||||
@ -153,13 +153,13 @@ export const QuoteAdditionalCurrencyEditor = () => {
|
||||
<SelectContent>
|
||||
{priorityCurrencies.map((item) => (
|
||||
<SelectItem key={item.code} value={item.code} disabled={item.code === "EUR"}>
|
||||
{item.code} - {item.name}
|
||||
{item.code} - {formatCurrencyName(item.code)}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectSeparator />
|
||||
{remainingCurrencies.map((item) => (
|
||||
<SelectItem key={item.code} value={item.code}>
|
||||
{item.code} - {item.name}
|
||||
{item.code} - {formatCurrencyName(item.code)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { FormGroup } from "@/components";
|
||||
import { Button, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/ui";
|
||||
import { t } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Upload } from "lucide-react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
@ -9,6 +9,7 @@ export const QuoteDocumentsCardEditor = () => {
|
||||
defaultUploadedFiles: [],
|
||||
});*/
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { control, formState } = useFormContext();
|
||||
|
||||
return (
|
||||
@ -47,7 +48,7 @@ export const QuoteDocumentsCardEditor = () => {
|
||||
>
|
||||
<div className='grid gap-2'>
|
||||
<img
|
||||
alt='Product image'
|
||||
alt={t("common.product_image")}
|
||||
className='object-cover w-full rounded-md aspect-square'
|
||||
height='300'
|
||||
src='https://placehold.co/300'
|
||||
@ -56,7 +57,7 @@ export const QuoteDocumentsCardEditor = () => {
|
||||
<div className='grid grid-cols-3 gap-2'>
|
||||
<button>
|
||||
<img
|
||||
alt='Product image'
|
||||
alt={t("common.product_image")}
|
||||
className='object-cover w-full rounded-md aspect-square'
|
||||
height='84'
|
||||
src='https://placehold.co/84'
|
||||
@ -65,7 +66,7 @@ export const QuoteDocumentsCardEditor = () => {
|
||||
</button>
|
||||
<button>
|
||||
<img
|
||||
alt='Product image'
|
||||
alt={t("common.product_image")}
|
||||
className='object-cover w-full rounded-md aspect-square'
|
||||
height='84'
|
||||
src='https://placehold.co/84'
|
||||
|
||||
@ -4,10 +4,12 @@ import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DataTablaRowActionFunction, DataTableRowActions } from "@/components";
|
||||
import { Badge } from "@/ui";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const useQuoteDataTableColumns = (
|
||||
actions: DataTablaRowActionFunction<IListQuotes_Response_DTO>
|
||||
): ColumnDef<IListQuotes_Response_DTO>[] => {
|
||||
const { t } = useTranslation();
|
||||
const customerColumns: ColumnDef<IListQuotes_Response_DTO>[] = useMemo(
|
||||
() => [
|
||||
/*{
|
||||
@ -42,7 +44,7 @@ export const useQuoteDataTableColumns = (
|
||||
{
|
||||
id: "state",
|
||||
accessorKey: "state",
|
||||
header: "Estado",
|
||||
header: t("quotes.list.columns.status"),
|
||||
cell: ({ renderValue }) => (
|
||||
<Badge variant={"destructive"}>
|
||||
<>{renderValue()}</>
|
||||
@ -52,19 +54,19 @@ export const useQuoteDataTableColumns = (
|
||||
{
|
||||
id: "client",
|
||||
accessorKey: "client",
|
||||
header: "Cliente",
|
||||
header: t("quotes.list.columns.customer_information"),
|
||||
enableSorting: false,
|
||||
sortingFn: "alphanumeric",
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Acciones",
|
||||
header: t("common.actions"),
|
||||
cell: (context) => {
|
||||
return <DataTableRowActions rowContext={context} actions={actions} />;
|
||||
},
|
||||
},
|
||||
],
|
||||
[actions]
|
||||
[actions, t]
|
||||
);
|
||||
return customerColumns;
|
||||
};
|
||||
|
||||
@ -18,7 +18,7 @@ import {
|
||||
IGetQuote_Response_DTO,
|
||||
Language,
|
||||
} from "@shared/contexts";
|
||||
import { t } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { isEqual } from "lodash";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@ -32,6 +32,7 @@ export type QuoteDataFormItem = IGetQuote_QuoteItem_Response_DTO;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const QuoteEdit = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const quoteId = useUrlId();
|
||||
const { toast } = useToast();
|
||||
@ -90,7 +91,7 @@ export const QuoteEdit = () => {
|
||||
amount: undefined,
|
||||
scale: 0,
|
||||
},
|
||||
tax_label: "% IVA",
|
||||
tax_label: t("settings.form_fields.default_tax_label.placeholder"),
|
||||
tax_price: {
|
||||
amount: undefined,
|
||||
scale: 2,
|
||||
@ -131,7 +132,7 @@ export const QuoteEdit = () => {
|
||||
},
|
||||
],
|
||||
}),
|
||||
[quoteCurrency.code]
|
||||
[quoteCurrency.code, t]
|
||||
);
|
||||
|
||||
const { useOne, useUpdate } = useQuotes();
|
||||
|
||||
@ -27,7 +27,6 @@ import {
|
||||
Switch,
|
||||
} from "@/ui";
|
||||
|
||||
import { t } from "i18next";
|
||||
import { AlertCircleIcon } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
||||
@ -47,7 +46,7 @@ export const SettingsEditor = () => {
|
||||
const [activeSection, setActiveSection] = useState("profile");
|
||||
const { useOne, useUpdate, useUploadLogo } = useSettings();
|
||||
const { toast } = useToast();
|
||||
const { i18n } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const { data, status, error: queryError } = useOne();
|
||||
|
||||
@ -64,9 +63,9 @@ export const SettingsEditor = () => {
|
||||
amount: undefined,
|
||||
scale: 2,
|
||||
},
|
||||
default_tax_label: "% IVA",
|
||||
default_tax_label: t("settings.form_fields.default_tax_label.placeholder"),
|
||||
}),
|
||||
[]
|
||||
[t]
|
||||
);
|
||||
|
||||
const { mutate } = useUpdate();
|
||||
|
||||
@ -18,6 +18,8 @@ import {
|
||||
TableRow,
|
||||
} from "@/ui";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { DataTableColumnHeader } from "./DataTableColumnHeader";
|
||||
import { DataTablePagination, DataTablePaginationProps } from "./DataTablePagination";
|
||||
@ -65,6 +67,7 @@ export function DataTable<TData>({
|
||||
onRowClick,
|
||||
activeRowIndex,
|
||||
}: DataTableProps<TData>) {
|
||||
const { t } = useTranslation();
|
||||
const headerVisible = headerOptions?.visible;
|
||||
|
||||
return (
|
||||
@ -146,7 +149,7 @@ export function DataTable<TData>({
|
||||
className={cn("h-24 text-center", cellClassName)}
|
||||
colSpan={table.getAllColumns.length}
|
||||
>
|
||||
No hay datos para mostrar
|
||||
{t("components.data_table.no_results")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button, ButtonProps } from "@/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PlusCircleIcon } from "lucide-react";
|
||||
|
||||
export interface AddNewRowButtonProps extends ButtonProps {
|
||||
@ -8,23 +9,27 @@ export interface AddNewRowButtonProps extends ButtonProps {
|
||||
}
|
||||
|
||||
export const AddNewRowButton = ({
|
||||
label = "Añade nueva fila",
|
||||
label,
|
||||
className,
|
||||
...props
|
||||
}: AddNewRowButtonProps): JSX.Element => (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"w-full gap-1 border-dashed text-muted-foreground border-muted-foreground/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<PlusCircleIcon className={label ? "w-4 h-4 mr-2" : "w-4 h-4"} />
|
||||
{label && <>{label}</>}
|
||||
</Button>
|
||||
);
|
||||
}: AddNewRowButtonProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const buttonLabel = label ?? t("common.append_empty_row");
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"w-full gap-1 border-dashed text-muted-foreground border-muted-foreground/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<PlusCircleIcon className={buttonLabel ? "w-4 h-4 mr-2" : "w-4 h-4"} />
|
||||
{buttonLabel && <>{buttonLabel}</>}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
AddNewRowButton.displayName = "AddNewRowButton";
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
@ -31,13 +32,14 @@ export const SortableDataTableToolbar = ({
|
||||
}: {
|
||||
table: Table<unknown>;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const selectedRowsCount = table.getSelectedRowModel().rows.length;
|
||||
|
||||
if (selectedRowsCount) {
|
||||
return (
|
||||
<div className="flex items-center h-12 p-1 text-white rounded-md bg-primary ">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button>{`${selectedRowsCount} filas seleccionadas`}</Button>
|
||||
<Button>{t("common.rows_selected", { count: selectedRowsCount })}</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
@ -48,11 +50,11 @@ export const SortableDataTableToolbar = ({
|
||||
onClick={() => table.options.meta?.duplicateItems()}
|
||||
>
|
||||
<CopyPlusIcon className="w-4 h-4" />
|
||||
<span className="sm:sr-only">Duplicar</span>
|
||||
<span className="sm:sr-only">{t("common.duplicate_selected_rows")}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Duplica las fila(s) seleccionadas(s)
|
||||
{t("common.duplicate_selected_rows_tooltip")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
@ -64,10 +66,10 @@ export const SortableDataTableToolbar = ({
|
||||
onClick={() => table.options.meta?.deleteItems()}
|
||||
>
|
||||
<Trash2Icon className="w-4 h-4" />
|
||||
<span className="sm:sr-only">Eliminar</span>
|
||||
<span className="sm:sr-only">{t("common.remove_selected_rows")}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Elimina las fila(s) seleccionada(s)</TooltipContent>
|
||||
<TooltipContent>{t("common.remove_selected_rows_tooltip")}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Separator orientation="vertical" className="h-6 mx-1 bg-muted/50" />
|
||||
<Tooltip>
|
||||
@ -79,10 +81,10 @@ export const SortableDataTableToolbar = ({
|
||||
onClick={() => table.resetRowSelection()}
|
||||
>
|
||||
<ScanIcon className="w-4 h-4 md:mr-2" />
|
||||
<span className="sm:sr-only">Quitar selección</span>
|
||||
<span className="sm:sr-only">{t("common.reset_selected_rows")}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Quita la selección</TooltipContent>
|
||||
<TooltipContent>{t("common.reset_selected_rows_tooltip")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
@ -101,10 +103,10 @@ export const SortableDataTableToolbar = ({
|
||||
onClick={() => table.options.meta?.appendItem()}
|
||||
>
|
||||
<CirclePlusIcon className="w-4 h-4" />
|
||||
<span className="sr-only">Añadir</span>
|
||||
<span className="sr-only">{t("common.add")}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Añadir fila</TooltipContent>
|
||||
<TooltipContent>{t("common.append_empty_row")}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Separator
|
||||
@ -117,28 +119,28 @@ export const SortableDataTableToolbar = ({
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<ClockIcon className="w-4 h-4" />
|
||||
<span className="sr-only">Snooze</span>
|
||||
<span className="sr-only">{t("components.sortable_data_table.snooze")}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="flex w-[535px] p-0">
|
||||
<div className="flex flex-col gap-2 px-2 py-4 border-r">
|
||||
<div className="px-4 text-sm font-medium">Snooze until</div>
|
||||
<div className="px-4 text-sm font-medium">{t("components.sortable_data_table.snooze_until")}</div>
|
||||
<div className="grid min-w-[250px] gap-1">
|
||||
<Button variant="ghost" className="justify-start font-normal">
|
||||
Later today{" "}
|
||||
{t("components.sortable_data_table.later_today")}{" "}
|
||||
<span className="ml-auto text-muted-foreground"></span>
|
||||
</Button>
|
||||
<Button variant="ghost" className="justify-start font-normal">
|
||||
Tomorrow
|
||||
{t("components.sortable_data_table.tomorrow")}
|
||||
<span className="ml-auto text-muted-foreground"></span>
|
||||
</Button>
|
||||
<Button variant="ghost" className="justify-start font-normal">
|
||||
This weekend
|
||||
{t("components.sortable_data_table.this_weekend")}
|
||||
<span className="ml-auto text-muted-foreground"></span>
|
||||
</Button>
|
||||
<Button variant="ghost" className="justify-start font-normal">
|
||||
Next week
|
||||
{t("components.sortable_data_table.next_week")}
|
||||
<span className="ml-auto text-muted-foreground"></span>
|
||||
</Button>
|
||||
</div>
|
||||
@ -148,7 +150,7 @@ export const SortableDataTableToolbar = ({
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<TooltipContent>Snooze</TooltipContent>
|
||||
<TooltipContent>{t("components.sortable_data_table.snooze")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
@ -156,28 +158,28 @@ export const SortableDataTableToolbar = ({
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<ReplyIcon className="w-4 h-4" />
|
||||
<span className="sr-only">Reply</span>
|
||||
<span className="sr-only">{t("components.sortable_data_table.reply")}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Reply</TooltipContent>
|
||||
<TooltipContent>{t("components.sortable_data_table.reply")}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<ReplyAllIcon className="w-4 h-4" />
|
||||
<span className="sr-only">Reply all</span>
|
||||
<span className="sr-only">{t("components.sortable_data_table.reply_all")}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Reply all</TooltipContent>
|
||||
<TooltipContent>{t("components.sortable_data_table.reply_all")}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<ForwardIcon className="w-4 h-4" />
|
||||
<span className="sr-only">Forward</span>
|
||||
<span className="sr-only">{t("components.sortable_data_table.forward")}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Forward</TooltipContent>
|
||||
<TooltipContent>{t("components.sortable_data_table.forward")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Separator
|
||||
@ -188,7 +190,7 @@ export const SortableDataTableToolbar = ({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoreVerticalIcon className="w-4 h-4" />
|
||||
<span className="sr-only">Columnas</span>
|
||||
<span className="sr-only">{t("components.data_table.columns")}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { IGetProfileResponse_DTO, ILogin_DTO, ILogin_Response_DTO } from "@shared/contexts";
|
||||
import { t } from "i18next";
|
||||
import secureLocalStorage from "react-secure-storage";
|
||||
import { IAuthActions } from "../hooks";
|
||||
import { createAxiosInstance } from "./axiosInstance";
|
||||
@ -31,8 +32,8 @@ export const createAxiosAuthActions = (
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: "Login failed",
|
||||
name: "Invalid email or password",
|
||||
message: t("login_page.invalid_credentials"),
|
||||
name: "InvalidCredentials",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { CustomDialog } from "@/components/CustomDialog"; // Ajusta la ruta según sea necesario
|
||||
import { t } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ReactNode, SyntheticEvent, useState } from "react";
|
||||
|
||||
interface DialogConfig {
|
||||
@ -13,15 +13,18 @@ interface DialogConfig {
|
||||
|
||||
export const useCustomDialog = (config?: Partial<DialogConfig>) => {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [dialogConfig, setDialogConfig] = useState<DialogConfig | undefined>({
|
||||
title: "Título",
|
||||
description: "Descripción",
|
||||
const { t } = useTranslation();
|
||||
const [dialogOverrides, setDialogConfig] = useState<Partial<DialogConfig>>();
|
||||
const dialogConfig: DialogConfig = {
|
||||
title: t("common.confirmation"),
|
||||
description: "",
|
||||
cancelLabel: t("common.no"),
|
||||
confirmLabel: t("common.yes"),
|
||||
onCancel: () => {},
|
||||
onConfirm: () => {},
|
||||
...config,
|
||||
});
|
||||
...dialogOverrides,
|
||||
};
|
||||
|
||||
const openDialog = (event?: SyntheticEvent, config?: DialogConfig) => {
|
||||
event?.preventDefault();
|
||||
|
||||
@ -76,9 +76,20 @@ export const useCustomLocalization = (props: UseLocalizationProps) => {
|
||||
[locale]
|
||||
);
|
||||
|
||||
const formatCurrencyName = useCallback(
|
||||
(currencyCode: string) =>
|
||||
new Intl.NumberFormat(locale ?? "es", {
|
||||
style: "currency",
|
||||
currency: currencyCode,
|
||||
currencyDisplay: "name",
|
||||
}).formatToParts(1).find((part) => part.type === "currency")?.value ?? currencyCode,
|
||||
[locale]
|
||||
);
|
||||
|
||||
return {
|
||||
formatCurrency,
|
||||
formatNumber,
|
||||
formatPercentage,
|
||||
formatCurrencyName,
|
||||
};
|
||||
};
|
||||
|
||||
@ -7,12 +7,55 @@
|
||||
"reload": "Recarregar"
|
||||
},
|
||||
"common": {
|
||||
"today": "Avui"
|
||||
"today": "Avui",
|
||||
"company": "Empresa",
|
||||
"phone": "Telèfon",
|
||||
"name": "Nom",
|
||||
"identifier": "Identificador",
|
||||
"confirmation": "Confirmació",
|
||||
"product_image": "Imatge del producte"
|
||||
},
|
||||
"title": "Presupuestador para distribuidores",
|
||||
"LoginPage": {
|
||||
"title": "Pressupostador per a distribuïdors",
|
||||
"description": "Introdueixi la seva adreça de correu electrònic i contrasenya per accedir-hi"
|
||||
},
|
||||
"app": {
|
||||
"title": "Pressupostador Uecko",
|
||||
"javascript_required": "Cal activar JavaScript per utilitzar aquesta aplicació."
|
||||
},
|
||||
"error_page": {
|
||||
"title": "Pàgina no trobada",
|
||||
"description": "La pàgina que cerques no existeix o s’ha mogut.",
|
||||
"go_home": "Anar a l’inici"
|
||||
},
|
||||
"login_page": {
|
||||
"invalid_credentials": "El correu electrònic o la contrasenya són incorrectes.",
|
||||
"validation": {
|
||||
"email_required": "El correu electrònic és obligatori.",
|
||||
"email_invalid": "Introdueix un correu electrònic vàlid.",
|
||||
"password_required": "La contrasenya és obligatòria.",
|
||||
"password_min": "La contrasenya ha de tenir com a mínim {{count}} caràcters.",
|
||||
"password_alphanum": "La contrasenya només pot contenir lletres i números."
|
||||
}
|
||||
},
|
||||
"quotes": {
|
||||
"downloading_dialog": {
|
||||
"error": "No s’ha pogut descarregar el pressupost. Torna-ho a provar."
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"sortable_data_table": {
|
||||
"snooze": "Ajornar",
|
||||
"snooze_until": "Ajornar fins a",
|
||||
"later_today": "Més tard avui",
|
||||
"tomorrow": "Demà",
|
||||
"this_weekend": "Aquest cap de setmana",
|
||||
"next_week": "La setmana vinent",
|
||||
"reply": "Respondre",
|
||||
"reply_all": "Respondre a tothom",
|
||||
"forward": "Reenviar"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,7 +82,13 @@
|
||||
"button_add_term": "Add filter term",
|
||||
"button_add_term_tooltip": "Add filter term (or press Enter)",
|
||||
"help_text": "Press Enter or click the + button to add a term to the filter. Use multiple terms for a broader search."
|
||||
}
|
||||
},
|
||||
"company": "Company",
|
||||
"phone": "Phone",
|
||||
"name": "Name",
|
||||
"identifier": "Identifier",
|
||||
"confirmation": "Confirmation",
|
||||
"product_image": "Product image"
|
||||
},
|
||||
"components": {
|
||||
"loading_indicator": {
|
||||
@ -106,6 +112,17 @@
|
||||
"no_results": "No results found.",
|
||||
"clear_filters": "Clear filters",
|
||||
"selected_count": "{{count}} selected"
|
||||
},
|
||||
"sortable_data_table": {
|
||||
"snooze": "Snooze",
|
||||
"snooze_until": "Snooze until",
|
||||
"later_today": "Later today",
|
||||
"tomorrow": "Tomorrow",
|
||||
"this_weekend": "This weekend",
|
||||
"next_week": "Next week",
|
||||
"reply": "Reply",
|
||||
"reply_all": "Reply all",
|
||||
"forward": "Forward"
|
||||
}
|
||||
},
|
||||
"hooks": {
|
||||
@ -146,7 +163,15 @@
|
||||
"forgotten_password": "Forgot your password?",
|
||||
"become_dealer": "Do you want to become a Uecko partner?",
|
||||
"contact_us": "Contact us",
|
||||
"login": "Log in"
|
||||
"login": "Log in",
|
||||
"invalid_credentials": "The email address or password is incorrect.",
|
||||
"validation": {
|
||||
"email_required": "Email is required.",
|
||||
"email_invalid": "Enter a valid email address.",
|
||||
"password_required": "Password is required.",
|
||||
"password_min": "Password must contain at least {{count}} characters.",
|
||||
"password_alphanum": "Password may only contain letters and numbers."
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"welcome": "Welcome"
|
||||
@ -260,7 +285,8 @@
|
||||
"status_stopped": "stopped"
|
||||
},
|
||||
"cancel_button": "Cancel the download",
|
||||
"toast_success": "Quote downloaded"
|
||||
"toast_success": "Quote downloaded",
|
||||
"error": "The quote could not be downloaded. Please try again."
|
||||
},
|
||||
"catalog_picker_dialog": {
|
||||
"title": "Select catalog items",
|
||||
@ -359,7 +385,11 @@
|
||||
"label": "Document language",
|
||||
"desc": "Language used to generate the quote PDF",
|
||||
"placeholder": "",
|
||||
"options": { "es": "Spanish", "en": "English", "fr": "French" }
|
||||
"options": {
|
||||
"es": "Spanish",
|
||||
"en": "English",
|
||||
"fr": "French"
|
||||
}
|
||||
},
|
||||
"currency_code": {
|
||||
"label": "Currency",
|
||||
@ -378,7 +408,7 @@
|
||||
},
|
||||
"payment_method": {
|
||||
"label": "Payment method",
|
||||
"placeholder": "placeholder",
|
||||
"placeholder": "",
|
||||
"desc": "Method of payment for this quote"
|
||||
},
|
||||
"notes": {
|
||||
@ -569,6 +599,15 @@
|
||||
}
|
||||
},
|
||||
"form_fields": {}
|
||||
},
|
||||
"app": {
|
||||
"title": "Uecko quoting tool",
|
||||
"javascript_required": "You need to enable JavaScript to use this application."
|
||||
},
|
||||
"error_page": {
|
||||
"title": "Page not found",
|
||||
"description": "The page you are looking for does not exist or has been moved.",
|
||||
"go_home": "Go to homepage"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,7 +82,13 @@
|
||||
"button_add_term": "Añadir término al filtro",
|
||||
"button_add_term_tooltip": "Añadir término al filtro (o pulsa Enter)",
|
||||
"help_text": "Presiona Enter o haz clic en el botón + para añadir un término al filtro. Usa múltiples términos para una búsqueda más amplia."
|
||||
}
|
||||
},
|
||||
"company": "Compañía",
|
||||
"phone": "Teléfono",
|
||||
"name": "Nombre",
|
||||
"identifier": "Identificador",
|
||||
"confirmation": "Confirmación",
|
||||
"product_image": "Imagen del producto"
|
||||
},
|
||||
"components": {
|
||||
"loading_indicator": {
|
||||
@ -106,6 +112,17 @@
|
||||
"no_results": "No se han encontrado resultados.",
|
||||
"clear_filters": "Limpiar filtros",
|
||||
"selected_count": "{{count}} seleccionados"
|
||||
},
|
||||
"sortable_data_table": {
|
||||
"snooze": "Posponer",
|
||||
"snooze_until": "Posponer hasta",
|
||||
"later_today": "Más tarde hoy",
|
||||
"tomorrow": "Mañana",
|
||||
"this_weekend": "Este fin de semana",
|
||||
"next_week": "La próxima semana",
|
||||
"reply": "Responder",
|
||||
"reply_all": "Responder a todos",
|
||||
"forward": "Reenviar"
|
||||
}
|
||||
},
|
||||
"hooks": {
|
||||
@ -146,7 +163,15 @@
|
||||
"forgotten_password": "¿Has olvidado tu contraseña?",
|
||||
"become_dealer": "¿Quieres ser distribuidor de Uecko?",
|
||||
"contact_us": "Contacta con nosotros",
|
||||
"login": "Entrar"
|
||||
"login": "Entrar",
|
||||
"invalid_credentials": "El correo electrónico o la contraseña no son correctos.",
|
||||
"validation": {
|
||||
"email_required": "El correo electrónico es obligatorio.",
|
||||
"email_invalid": "Introduce un correo electrónico válido.",
|
||||
"password_required": "La contraseña es obligatoria.",
|
||||
"password_min": "La contraseña debe tener al menos {{count}} caracteres.",
|
||||
"password_alphanum": "La contraseña solo puede contener letras y números."
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"welcome": "Bienvenido"
|
||||
@ -260,7 +285,8 @@
|
||||
"status_stopped": "parada"
|
||||
},
|
||||
"cancel_button": "Cancelar la descarga",
|
||||
"toast_success": "Cotización descargada"
|
||||
"toast_success": "Cotización descargada",
|
||||
"error": "No se ha podido descargar el presupuesto. Vuelve a intentarlo."
|
||||
},
|
||||
"catalog_picker_dialog": {
|
||||
"title": "Seleccionar artículos del catálogo",
|
||||
@ -359,7 +385,11 @@
|
||||
"label": "Idioma del documento",
|
||||
"desc": "Idioma utilizado para generar el presupuesto en PDF",
|
||||
"placeholder": "",
|
||||
"options": { "es": "Español", "en": "Inglés", "fr": "Francés" }
|
||||
"options": {
|
||||
"es": "Español",
|
||||
"en": "Inglés",
|
||||
"fr": "Francés"
|
||||
}
|
||||
},
|
||||
"currency_code": {
|
||||
"label": "Moneda",
|
||||
@ -378,7 +408,7 @@
|
||||
},
|
||||
"payment_method": {
|
||||
"label": "Forma de pago",
|
||||
"placeholder": "placeholder",
|
||||
"placeholder": "",
|
||||
"desc": "Forma de pago que se utilizará en esta cotización"
|
||||
},
|
||||
"notes": {
|
||||
@ -394,7 +424,7 @@
|
||||
"subtotal_price": {
|
||||
"label": "Importe neto",
|
||||
"placeholder": "",
|
||||
"desc": ""
|
||||
"desc": "Subtotal del presupuesto"
|
||||
},
|
||||
"discount": {
|
||||
"label": "Descuento (%)",
|
||||
@ -414,7 +444,7 @@
|
||||
"before_tax_price": {
|
||||
"label": "Base imponible",
|
||||
"placeholder": "",
|
||||
"desc": ""
|
||||
"desc": "Base imponible"
|
||||
},
|
||||
"tax": {
|
||||
"label": "Impuesto (%)",
|
||||
@ -569,6 +599,15 @@
|
||||
}
|
||||
},
|
||||
"form_fields": {}
|
||||
},
|
||||
"app": {
|
||||
"title": "Presupuestador Uecko",
|
||||
"javascript_required": "Necesitas activar JavaScript para utilizar esta aplicación."
|
||||
},
|
||||
"error_page": {
|
||||
"title": "Página no encontrada",
|
||||
"description": "La página que buscas no existe o se ha movido.",
|
||||
"go_home": "Ir al inicio"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,21 +21,74 @@
|
||||
"close": "Fermer",
|
||||
"more": "Plus",
|
||||
"pagination": "Pagination",
|
||||
"breadcrumb": "Fil d’Ariane",
|
||||
"previous": "Précédent",
|
||||
"next": "Suivant",
|
||||
"more_pages": "Plus de pages",
|
||||
"previous_slide": "Diapositive précédente",
|
||||
"next_slide": "Diapositive suivante",
|
||||
"add": "Ajouter",
|
||||
"submit": "Envoyer",
|
||||
"download": "Télécharger",
|
||||
"help": "Aide",
|
||||
"select_all": "Tout sélectionner",
|
||||
"select_row": "Sélectionner la ligne",
|
||||
"sort_asc": "Croissant",
|
||||
"sort_asc_description": "Ordre croissant. Cliquez pour trier par ordre décroissant.",
|
||||
"sort_desc": "Décroissant",
|
||||
"sort_desc_description": "Ordre décroissant. Cliquez pour trier par ordre croissant.",
|
||||
"sort_none_description": "Aucun tri. Cliquez pour trier par ordre croissant.",
|
||||
"rows_selected": "{{count}} ligne(s) sélectionnée(s).",
|
||||
"rows_selected_of_total": "{{count}} ligne(s) sélectionnée(s) sur {{total}}.",
|
||||
"rows_per_page": "Lignes par page",
|
||||
"num_page_of_total": "Page {{count}} sur {{total}}",
|
||||
"go_to_first_page": "Aller à la première page",
|
||||
"go_to_prev_page": "Aller à la page précédente",
|
||||
"go_to_next_page": "Aller à la page suivante",
|
||||
"go_to_last_page": "Aller à la dernière page",
|
||||
"error": "Erreur",
|
||||
"actions": "Actions",
|
||||
"open_menu": "Ouvrir le menu",
|
||||
"duplicate_row": "Dupliquer",
|
||||
"duplicate_selected_rows": "Dupliquer",
|
||||
"duplicate_selected_rows_tooltip": "Dupliquer les lignes sélectionnées",
|
||||
"append_empty_row": "Ajouter une ligne",
|
||||
"append_empty_row_tooltip": "Ajouter une ligne vide",
|
||||
"append_article": "Ajouter un article",
|
||||
"append_article_tooltip": "Choisir un article du catalogue et l’ajouter",
|
||||
"append_block": "Ajouter un bloc de texte",
|
||||
"append_block_tooltip": "Choisir un bloc de texte et l’ajouter",
|
||||
"move_row": "Déplacer la ligne",
|
||||
"remove_row": "Supprimer",
|
||||
"remove_selected_rows": "Supprimer",
|
||||
"remove_selected_rows_tooltip": "Supprimer les lignes sélectionnées",
|
||||
"reset_selected_rows": "Désélectionner",
|
||||
"reset_selected_rows_tooltip": "Désélectionner les lignes sélectionnées",
|
||||
"insert_row_above": "Insérer une ligne au-dessus",
|
||||
"insert_row_below": "Insérer une ligne en dessous",
|
||||
"pick_date": "Choisir une date",
|
||||
"today": "Aujourd’hui",
|
||||
"required_field": "Ce champ est obligatoire",
|
||||
"edit": "Modifier",
|
||||
"remove": "Supprimer",
|
||||
"archive": "Archiver",
|
||||
"duplicate": "Dupliquer",
|
||||
"pick_date": "Choisir une date",
|
||||
"today": "Aujourd’hui",
|
||||
"required_field": "Ce champ est obligatoire"
|
||||
"disable_preview": "Masquer l’aperçu",
|
||||
"enable_preview": "Afficher l’aperçu",
|
||||
"filter": {
|
||||
"reset_filter": "Réinitialiser le filtre",
|
||||
"badget_remove_tooltip": "Retirer ce terme du filtre",
|
||||
"input_placeholder": "Saisissez du texte pour filtrer…",
|
||||
"button_add_term": "Ajouter un terme au filtre",
|
||||
"button_add_term_tooltip": "Ajouter un terme au filtre (ou appuyer sur Entrée)",
|
||||
"help_text": "Appuyez sur Entrée ou cliquez sur le bouton + pour ajouter un terme au filtre. Utilisez plusieurs termes pour élargir la recherche."
|
||||
},
|
||||
"company": "Entreprise",
|
||||
"phone": "Téléphone",
|
||||
"name": "Nom",
|
||||
"identifier": "Identifiant",
|
||||
"confirmation": "Confirmation",
|
||||
"product_image": "Image du produit"
|
||||
},
|
||||
"components": {
|
||||
"loading_indicator": {
|
||||
@ -59,6 +112,17 @@
|
||||
"no_results": "Aucun résultat trouvé.",
|
||||
"clear_filters": "Effacer les filtres",
|
||||
"selected_count": "{{count}} sélectionné(s)"
|
||||
},
|
||||
"sortable_data_table": {
|
||||
"snooze": "Reporter",
|
||||
"snooze_until": "Reporter jusqu’à",
|
||||
"later_today": "Plus tard aujourd’hui",
|
||||
"tomorrow": "Demain",
|
||||
"this_weekend": "Ce week-end",
|
||||
"next_week": "La semaine prochaine",
|
||||
"reply": "Répondre",
|
||||
"reply_all": "Répondre à tous",
|
||||
"forward": "Transférer"
|
||||
}
|
||||
},
|
||||
"hooks": {
|
||||
@ -99,7 +163,15 @@
|
||||
"forgotten_password": "Mot de passe oublié ?",
|
||||
"become_dealer": "Vous souhaitez devenir distributeur Uecko ?",
|
||||
"contact_us": "Contactez-nous",
|
||||
"login": "Se connecter"
|
||||
"login": "Se connecter",
|
||||
"invalid_credentials": "L’adresse e-mail ou le mot de passe est incorrect.",
|
||||
"validation": {
|
||||
"email_required": "L’adresse e-mail est obligatoire.",
|
||||
"email_invalid": "Saisissez une adresse e-mail valide.",
|
||||
"password_required": "Le mot de passe est obligatoire.",
|
||||
"password_min": "Le mot de passe doit contenir au moins {{count}} caractères.",
|
||||
"password_alphanum": "Le mot de passe ne peut contenir que des lettres et des chiffres."
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"welcome": "Bienvenue"
|
||||
@ -128,6 +200,12 @@
|
||||
"empty_state": {
|
||||
"subtitle": "Commencez par créer un devis"
|
||||
},
|
||||
"status_help": {
|
||||
"title": "Aide sur le statut des devis",
|
||||
"intro": "Le statut d’un devis indique sa position actuelle dans le processus commercial.",
|
||||
"possible": "Les statuts possibles sont :",
|
||||
"outro": "Utilisez ces statuts pour suivre efficacement vos devis et optimiser votre processus de vente. Le statut Archivé permet de conserver l’historique sans interférer avec les devis actifs."
|
||||
},
|
||||
"tabs": {
|
||||
"all": "Tous",
|
||||
"draft": "Brouillons",
|
||||
@ -207,7 +285,8 @@
|
||||
"status_stopped": "arrêté"
|
||||
},
|
||||
"cancel_button": "Annuler le téléchargement",
|
||||
"toast_success": "Devis téléchargé"
|
||||
"toast_success": "Devis téléchargé",
|
||||
"error": "Le devis n’a pas pu être téléchargé. Veuillez réessayer."
|
||||
},
|
||||
"catalog_picker_dialog": {
|
||||
"title": "Sélectionner des articles du catalogue",
|
||||
@ -225,6 +304,64 @@
|
||||
"description": "Voulez-vous envoyer ce devis à Uecko ? Cette action est irréversible.",
|
||||
"toast_status_changed": "Devis envoyé à Uecko"
|
||||
},
|
||||
"quote_status_editor": {
|
||||
"trigger_button": "Changer le statut",
|
||||
"title": "Changer le statut du devis",
|
||||
"status": {
|
||||
"draft": {
|
||||
"title": "Brouillon",
|
||||
"description": "Le devis est en cours de création."
|
||||
},
|
||||
"ready": {
|
||||
"title": "Prêt",
|
||||
"description": "Le devis est complet et prêt à être envoyé au client."
|
||||
},
|
||||
"delivered": {
|
||||
"title": "Envoyé",
|
||||
"description": "Le devis a été envoyé au client et une réponse est attendue."
|
||||
},
|
||||
"accepted": {
|
||||
"title": "Accepté",
|
||||
"description": "Le client a approuvé le devis."
|
||||
},
|
||||
"rejected": {
|
||||
"title": "Refusé",
|
||||
"description": "Le client n’a pas accepté le devis."
|
||||
},
|
||||
"archived": {
|
||||
"title": "Archivé",
|
||||
"description": "Le devis a été conservé pour référence future."
|
||||
}
|
||||
},
|
||||
"submit_button": "Changer le statut",
|
||||
"toast_status_changed": "Statut du devis modifié : {{newStatus}}"
|
||||
},
|
||||
"discount_column": {
|
||||
"label": "Afficher la colonne de remise",
|
||||
"description": "Afficher ou masquer la colonne de remise dans les lignes.",
|
||||
"confirmation": {
|
||||
"title": "Masquer les remises par ligne",
|
||||
"description": "Certaines lignes comportent une remise. Masquer la colonne ne supprimera pas ces remises : elles continueront à être prises en compte dans le montant du devis, mais ne seront plus affichées dans les lignes ni dans le PDF.",
|
||||
"confirm": "Masquer la colonne"
|
||||
}
|
||||
},
|
||||
"additional_currency": {
|
||||
"title": "Devise supplémentaire",
|
||||
"description": "Afficher, si nécessaire, le montant total du devis dans une autre devise.",
|
||||
"enabled": "Afficher dans une autre devise",
|
||||
"currency": "Devise",
|
||||
"date": "Date du taux de change",
|
||||
"rate": "Taux de change (1 EUR = …)",
|
||||
"refresh": "Actualiser le taux de change",
|
||||
"total": "Total en {{code}}"
|
||||
},
|
||||
"inline_row_actions": {
|
||||
"delete_confirmation": {
|
||||
"title": "Supprimer la ligne",
|
||||
"description": "Voulez-vous vraiment supprimer cette ligne ? Cette action est irréversible.",
|
||||
"confirm": "Supprimer la ligne"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"draft": "Brouillon",
|
||||
"ready": "Prêt",
|
||||
@ -287,13 +424,18 @@
|
||||
"subtotal_price": {
|
||||
"label": "Montant net",
|
||||
"placeholder": "",
|
||||
"desc": ""
|
||||
"desc": "Sous-total du devis"
|
||||
},
|
||||
"discount": {
|
||||
"label": "Remise (%)",
|
||||
"placeholder": "",
|
||||
"desc": "Pourcentage de remise"
|
||||
},
|
||||
"discount_value": {
|
||||
"label": "Remise ({{value}}%)",
|
||||
"placeholder": "",
|
||||
"desc": "Pourcentage de remise"
|
||||
},
|
||||
"discount_price": {
|
||||
"label": "Montant de la remise",
|
||||
"placeholder": "",
|
||||
@ -302,13 +444,18 @@
|
||||
"before_tax_price": {
|
||||
"label": "Base imposable",
|
||||
"placeholder": "",
|
||||
"desc": ""
|
||||
"desc": "Base imposable"
|
||||
},
|
||||
"tax": {
|
||||
"label": "Taxe (%)",
|
||||
"placeholder": "",
|
||||
"desc": "Pourcentage de taxe"
|
||||
},
|
||||
"tax_value": {
|
||||
"label": "{{label}} ({{value}}%)",
|
||||
"placeholder": "",
|
||||
"desc": "Pourcentage de taxe"
|
||||
},
|
||||
"tax_price": {
|
||||
"label": "Montant de la taxe",
|
||||
"placeholder": "",
|
||||
@ -452,6 +599,15 @@
|
||||
}
|
||||
},
|
||||
"form_fields": {}
|
||||
},
|
||||
"app": {
|
||||
"title": "Outil de devis Uecko",
|
||||
"javascript_required": "Vous devez activer JavaScript pour utiliser cette application."
|
||||
},
|
||||
"error_page": {
|
||||
"title": "Page introuvable",
|
||||
"description": "La page que vous recherchez n’existe pas ou a été déplacée.",
|
||||
"go_home": "Aller à l’accueil"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
58
client/src/locales/resources.test.ts
Normal file
58
client/src/locales/resources.test.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { createInstance } from "i18next";
|
||||
import es from "./es.json";
|
||||
import en from "./en.json";
|
||||
import fr from "./fr.json";
|
||||
import esBlocks from "../app/quotes/components/editors/BlocksPickerDialog/locales/es.json";
|
||||
import enBlocks from "../app/quotes/components/editors/BlocksPickerDialog/locales/en.json";
|
||||
import frBlocks from "../app/quotes/components/editors/BlocksPickerDialog/locales/fr.json";
|
||||
|
||||
const flatten = (resource: Record<string, unknown>, prefix = ""): Record<string, string> =>
|
||||
Object.fromEntries(
|
||||
Object.entries(resource).flatMap(([key, value]) => {
|
||||
const path = prefix ? `${prefix}.${key}` : key;
|
||||
return typeof value === "string"
|
||||
? [[path, value]]
|
||||
: Object.entries(flatten(value as Record<string, unknown>, path));
|
||||
})
|
||||
);
|
||||
|
||||
const variables = (value: string) => [...value.matchAll(/{{\s*(.*?)\s*}}/g)]
|
||||
.map((match) => match[1])
|
||||
.sort();
|
||||
|
||||
describe("translation resources", () => {
|
||||
const reference = flatten(es.translation);
|
||||
|
||||
it.each([en, fr])("covers every Spanish key and preserves interpolation variables", (resource) => {
|
||||
const translations = flatten(resource.translation);
|
||||
expect(Object.keys(translations).sort()).toEqual(Object.keys(reference).sort());
|
||||
for (const [key, value] of Object.entries(reference)) {
|
||||
expect({ key, variables: variables(translations[key]) }).toEqual({
|
||||
key,
|
||||
variables: variables(value),
|
||||
});
|
||||
if (value.trim()) expect({ key, empty: !translations[key].trim() }).toEqual({ key, empty: false });
|
||||
}
|
||||
});
|
||||
|
||||
it("renders the previously missing French strings without Spanish fallback", async () => {
|
||||
const i18n = createInstance();
|
||||
await i18n.init({ lng: "fr", fallbackLng: false, resources: { fr } });
|
||||
expect(i18n.t("common.append_article")).toBe("Ajouter un article");
|
||||
expect(i18n.t("common.num_page_of_total", { count: 2, total: 5 })).toBe("Page 2 sur 5");
|
||||
expect(i18n.t("quotes.quote_status_editor.status.accepted.title")).toBe("Accepté");
|
||||
expect(i18n.t("quotes.additional_currency.total", { code: "USD" })).toBe("Total en USD");
|
||||
});
|
||||
|
||||
it.each([{ blocks: enBlocks }, { blocks: frBlocks }])("provides every predefined text block in each document language", ({ blocks }) => {
|
||||
const codes = (entries: Array<{ code: string }>) => entries
|
||||
.map(({ code }) => code.replace(/_(ES|EN|FR)$/, ""))
|
||||
.sort();
|
||||
expect(codes(blocks)).toEqual(codes(esBlocks));
|
||||
expect(new Set(blocks.map(({ code }) => code)).size).toBe(blocks.length);
|
||||
for (const block of blocks) {
|
||||
expect(block.title.trim()).not.toBe("");
|
||||
expect(block.body.trim()).not.toBe("");
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -10,6 +10,15 @@ import { viteStaticCopy } from "vite-plugin-static-copy";
|
||||
const pkg = JSON.parse(readFileSync("./package.json", "utf-8"));
|
||||
const require = createRequire(import.meta.url);
|
||||
const buildId = randomUUID();
|
||||
const htmlTranslations: Array<{
|
||||
language: string;
|
||||
labels: { title: string; javascript_required: string };
|
||||
}> = ["es", "en", "fr", "ca"].map((language) => ({
|
||||
language,
|
||||
labels: JSON.parse(readFileSync(`./src/locales/${language}.json`, "utf8")).translation.app,
|
||||
}));
|
||||
const escapeHtml = (value: string) =>
|
||||
value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
|
||||
const pdfjsDistPath = path.dirname(require.resolve("pdfjs-dist/package.json"));
|
||||
const cMapsDir = normalizePath(path.join(pdfjsDistPath, "cmaps"));
|
||||
@ -27,6 +36,21 @@ export default defineConfig({
|
||||
emptyOutDir: true,
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: "localized-html",
|
||||
transformIndexHtml(html) {
|
||||
return html
|
||||
.replace("<!-- app-title -->", escapeHtml(htmlTranslations[0].labels.title))
|
||||
.replace(
|
||||
"<!-- app-noscript -->",
|
||||
htmlTranslations
|
||||
.map(({ language, labels }) =>
|
||||
`<p lang="${language}">${escapeHtml(labels.javascript_required)}</p>`
|
||||
)
|
||||
.join("\n")
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "app-build-version",
|
||||
apply: "build",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "uecko-presupuestador",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.5",
|
||||
"author": "Rodax Software <dev@rodax-software.com>",
|
||||
"license": "ISC",
|
||||
"private": true,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@uecko-presupuestador/server",
|
||||
"private": true,
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.5",
|
||||
"author": "Rodax Software <dev@rodax-software.com>",
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@uecko-presupuestador/shared",
|
||||
"private": false,
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.5",
|
||||
"main": "./index.ts",
|
||||
"author": "Rodax Software <dev@rodax-software.com>",
|
||||
"license": "ISC",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user