This commit is contained in:
David Arranz 2026-10-14 12:00:19 +02:00
parent a5dc3395e3
commit c5aff5d2cb
38 changed files with 759 additions and 622 deletions

View File

@ -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&#xD;&#xA;Medida de Hueco: 2450x3000 mm.&#xD;&#xA;Modelo: LYN / Acabado: ANODIZADO / Color: NEGRO&#xD;&#xA;Nº de travesaños: 2 TRAVESAÑOS HORIZONTALES&#xD;&#xA;Acabado paneles: TRANSPARENTE TEMPLADO &#xD;&#xA;Tirador: DOBLE POR PUERTA&#xD;&#xA;Freno: DOBLE POR PUERTA (APERTURA Y CIERRE)&#xD;&#xA;Guía: VISTA (DOBLE EMBELLECEDOR)&#xD;&#xA;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&#xD;&#xA;Medida de Hueco:2440x5180mm.&#xD;&#xA;Modelo: PANORAMIC P.O. / Acabado: LACADO / Color: BLANCO&#xD;&#xA;Nº de lístelos: SIN LISTELOS / Acabado paneles: PANEL LACADO&#xD;&#xA;Tirador:&#xD;&#xA;Cerco perimetral: &#xD;&#xA;Nº de hojas:&#xD;&#xA;FRENO HIDRAULICO INCORPORADO O FRENO MAGNETICO INCORPORADO&#xD;&#xA;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&#xD;&#xA;Medida de hueco: 2275x1390 mm.&#xD;&#xA;Modelo: ARGO / Acabado: LACADO / Color: A DETERMINAR&#xD;&#xA;Grosor hoja: 22 o 19 mm&#xD;&#xA;Acabado de hojas: LISAS&#xD;&#xA;Tirador: UÑERO A DETERMINAR&#xD;&#xA;Acabado herrajes: CROMADO O ANTRACITA&#xD;&#xA;Jambas perimetrales: VISTAS, REMETIDAS O COMBI A DETERMINAR.&#xD;&#xA;Nº de hojas: 2&#xD;&#xA;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&#xD;&#xA;Acabado: MELAMINA / color: A DETERMINAR&#xD;&#xA;Espesor bloque: 19 mm. / Espesor complementos: 19 mm.&#xD;&#xA;Barras de colgar: CROMO/ NEGRA/ BRONCE&#xD;&#xA;Frentes cristal cajones: RECTO / “U” – FLOAT BRONCE C-424&#xD;&#xA;Tirador cajones: FRENTE BAJO / Cajones: 4 DE 180 MM. A DETERMINAR&#xD;&#xA;Baldas deslizantes: X DE 145 MM. A DETERMINAR&#xD;&#xA;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&#xD;&#xA;Medida de Hueco: 2450x3000 mm.&#xD;&#xA;Espesor bloque: 25 mm. / Espesor complementos: 25 mm.&#xD;&#xA;Barras de colgar: CROMO/ NEGRA/ BRONCE&#xD;&#xA;Frentes cristal cajones: RECTO / “U” – FLOAT BRONCE C-424&#xD;&#xA;Tirador cajones: FRENTE BAJO / Cajones: 4 DE 180 MM. A DETERMINAR&#xD;&#xA;Baldas deslizantes: X DE 145 MM. A DETERMINAR&#xD;&#xA;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&#xD;&#xA;Size of opening: 2450x3000 mm.&#xD;&#xA;Thickness block: 19 mm / Thickness complements: 19 mm.&#xD;&#xA;Hanging rods: CHROME/ BLACK&#xD;&#xA;Glass drawer fronts: STRAIGHT / &quot;U&quot; - FLOAT BRONZE C-424 &#xD;&#xA;Drawer handle: xxxxx / Drawers: 4 DE 180 MM.&#xD;&#xA;Shoe rack door: YES/NO / Profile finish: BLACK LACQUER / SILVER MATTE&#xD;&#xA;Shoe cabinet door glass finish: FLOAT BRONZE C-424&#xD;&#xA;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&#xD;&#xA;Size of opening: 2275x1390 mm.&#xD;&#xA;Model: ARGO / Finish: LACQUER / Colour: TO BE DETERMINED&#xD;&#xA;Door thickness: 22 or 19 mm&#xD;&#xA;Door finish: SMOOTH &#xD;&#xA;Handle: KNOB TO BE DETERMINED&#xD;&#xA;Hardware finish: CHROME OR ANTHRACITE&#xD;&#xA;Perimetral doorposts: VISIBLE, REMETED OR COMBI TO BE DETERMINED.&#xD;&#xA;Number of doors: 2 &#xD;&#xA;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. &#xD;&#xA;Model: PANORAMIC P.O. / Finish: LACQUER / Colour: WHITE&#xD;&#xA;No. of listers: NO LISTELS / Panels finish: LACQUERED PANEL &#xD;&#xA;Handle: &#xD;&#xA;Perimeter frame: &#xD;&#xA;Nº of doors: &#xD;&#xA;INCORPORATED HYDRAULIC BRAKE (not available in lower doors of 650 mm.) OR INCORPORATED MAGNETIC BRAKE. &#xD;&#xA;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&#xD;&#xA;Thickness block: 19 mm / Thickness complements: 19 mm.&#xD;&#xA;Hanging rods: CHROME/ BLACK&#xD;&#xA;Glass drawer fronts: STRAIGHT / &quot;U&quot; - FLOAT BRONZE C-424 &#xD;&#xA;Drawer handle: xxxxx / Drawers: 4 DE 180 MM.&#xD;&#xA;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&#xD;&#xA;Side frame panel finish: BRONZE FLOAT BRONZE GLASS C-424&#xD;&#xA;Rear panel finish: BRONZE FLOAT GLASS C-424&#xD;&#xA;Complements finish: MELAMIN / Colour: DENBER OAK M-242&#xD;&#xA;Thickness of accessories: 25 mm.&#xD;&#xA;Hanging bars: BLACK&#xD;&#xA;Glass drawer fronts: STRAIGHT / &quot;U&quot; - FLOAT BRONZE C-424 &#xD;&#xA;Drawer handle: xxxxx / Drawers: 4 DE 180 MM.&#xD;&#xA;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&#xD;&#xA;Acabado panel bastidores laterales: CRISTAL TEMPLADO FLOAT BRONCE C-424&#xD;&#xA;Acabado trasera: CRISTAL TEMPLADO FLOAT BRONCE C-424&#xD;&#xA;Acabado complementos: MELAMINA / Color: ROBLE DENBER M-242&#xD;&#xA;Espesor complementos: 25 mm.&#xD;&#xA;Barras de colgar: NEGRA&#xD;&#xA;Frentes cristal cajones: RECTO / “U” – FLOAT BRONCE C-424 &#xD;&#xA;Tirador cajones: xxxxx / Cajones: 4 DE 180 MM.&#xD;&#xA;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&#xD;&#xA;Size of opening: 2450x3000 mm.&#xD;&#xA;Model: LYN / Finish: ANODISED / Colour: BLACK&#xD;&#xA;No. of crosspieces: 2 HORIZONTAL CROSSBARS&#xD;&#xA;Panel finish: TRANSPARENT TEMPLARED &#xD;&#xA;Handle: DOUBLE PER DOOR&#xD;&#xA;Brake: DOUBLE PER DOOR (OPENING AND CLOSING)&#xD;&#xA;Guide: VISTA (DOUBLE EMBELLECEDOR)&#xD;&#xA;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&#xD;&#xA;Épaisseur du bloc : 19 mm / Épaisseur des accessoires : 19 mm&#xD;&#xA;Barres de penderie : CHROME / NOIR&#xD;&#xA;Façades des tiroirs en verre : DROITE / “U” – FLOAT BRONZE C-424&#xD;&#xA;Poignée des tiroirs : xxxxx&#xD;&#xA;Tiroirs : 4 DE 180 MM&#xD;&#xA;Porte de chaussures : OUI / NON&#xD;&#xA;Finition du profil : LAQUÉ NOIR / ARGENT MAT&#xD;&#xA;Finition du verre de la porte de chaussures : FLOAT BRONZE C-424&#xD;&#xA;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&#xD;&#xA;Modèle : ARGO / Finition : LAQUÉE / Couleur : À DÉTERMINER&#xD;&#xA;Épaisseur des vantaux : 22 ou 19 mm&#xD;&#xA;Finition des vantaux : LISSES&#xD;&#xA;Poignée : PRISE DE MAIN À DÉTERMINER&#xD;&#xA;Finition des ferrures : CHROMÉ OU ANTHRACITE&#xD;&#xA;Jambages périmétraux : APPARENTS, ENCASTRÉS OU COMBINÉS À DÉTERMINER&#xD;&#xA;Nombre de vantaux : 2&#xD;&#xA;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&#xD;&#xA;Modèle : PANORAMIC P.O. / Finition : LAQUÉE / Couleur : BLANC&#xD;&#xA;Nombre de listels : SANS LISTELS&#xD;&#xA;Finition des panneaux : PANNEAU LAQUÉ&#xD;&#xA;Poignée :&#xD;&#xA;Encadrement périmétral :&#xD;&#xA;Nombre de vantaux :&#xD;&#xA;FREIN HYDRAULIQUE INTÉGRÉ (non disponible pour les portes inférieures à 650 mm) OU FREIN MAGNÉTIQUE INTÉGRÉ&#xD;&#xA;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&#xD;&#xA;Épaisseur du bloc : 19 mm / Épaisseur des accessoires : 19 mm&#xD;&#xA;Barres de penderie : CHROME / NOIR&#xD;&#xA;Façades des tiroirs en verre : DROITE / “U” – FLOAT BRONZE C-424&#xD;&#xA;Poignée des tiroirs : xxxxx&#xD;&#xA;Tiroirs : 4 DE 180 MM&#xD;&#xA;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&#xD;&#xA;Finition des panneaux des cadres latéraux : VERRE TREMPÉ FLOAT BRONZE C-424&#xD;&#xA;Finition du dos : VERRE TREMPÉ FLOAT BRONZE C-424&#xD;&#xA;Finition des accessoires : MÉLAMINE / Couleur : CHÊNE DENBER M-242&#xD;&#xA;Épaisseur des accessoires : 25 mm&#xD;&#xA;Barres de penderie : NOIRES&#xD;&#xA;Façades des tiroirs en verre : DROITE / “U” – FLOAT BRONZE C-424&#xD;&#xA;Poignée des tiroirs : xxxxx&#xD;&#xA;Tiroirs : 4 DE 180 MM&#xD;&#xA;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>

View File

@ -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>

View File

@ -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": {

View File

@ -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}>

View File

@ -24,7 +24,7 @@ export const Routes = () => {
const routesForErrors = [
{
path: "*",
Element: (
element: (
<Suspense fallback={<LoadingOverlay />}>
<ErrorPage />
</Suspense>

View File

@ -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>

View File

@ -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'

View File

@ -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'

View 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);
});
});

View 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"),
}),
});

View File

@ -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;
};

View File

@ -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({

View File

@ -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;
};

View File

@ -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>

View File

@ -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)}
>

View File

@ -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"
}
]

View File

@ -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:"
}
]

View File

@ -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"
}
]

View File

@ -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>

View File

@ -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'

View File

@ -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;
};

View File

@ -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();

View File

@ -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();

View File

@ -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>
)}

View File

@ -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";

View File

@ -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">

View File

@ -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",
},
};
}

View File

@ -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();

View File

@ -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,
};
};

View File

@ -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"
}
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View 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("");
}
});
});

View File

@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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",

View File

@ -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,

View File

@ -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": {

View File

@ -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",