Subida a producción como Acana

This commit is contained in:
David Arranz 2025-11-28 16:00:18 +01:00
parent 1783f630cf
commit e8d8403ae1
44 changed files with 177077 additions and 440 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@erp/factuges-server",
"version": "0.0.14",
"version": "0.0.15",
"private": true,
"scripts": {
"build": "tsup src/index.ts --config tsup.config.ts",

1
apps/web/.env.production Normal file
View File

@ -0,0 +1 @@
VITE_API_SERVER_URL=http://192.168.0.104:3002/api/v1

View File

@ -1,14 +1,14 @@
{
"name": "@erp/factuges-web",
"private": true,
"version": "0.0.14",
"version": "0.0.15",
"type": "module",
"scripts": {
"dev": "vite --host --clearScreen false",
"build": "tsc && vite build",
"build:rodax": "tsc && vite build --mode rodax",
"build:acana": "tsc && vite build --mode acana",
"preview": "vite preview",
"preview": "vite preview --host",
"clean": "rm -rf dist && rm -rf node_modules && rm -rf .turbo",
"check:deps": "pnpm exec depcheck",
"lint": "biome lint --fix",
@ -32,14 +32,13 @@
"@erp/core": "workspace:*",
"@erp/customer-invoices": "workspace:*",
"@erp/customers": "workspace:*",
"@repo/i18next": "workspace:*",
"@repo/rdx-ui": "workspace:*",
"@repo/shadcn-ui": "workspace:*",
"@tailwindcss/vite": "^4.1.11",
"@tanstack/react-query": "^5.90.6",
"axios": "^1.9.0",
"dinero.js": "^1.9.1",
"i18next": "^25.0.2",
"i18next-browser-languagedetector": "^8.1.0",
"react-error-boundary": "^6.0.0",
"react-hook-form": "^7.56.4",
"react-i18next": "^15.0.1",

View File

@ -10,16 +10,18 @@ import { Suspense } from "react";
import { I18nextProvider } from "react-i18next";
import { RouterProvider } from "react-router-dom";
import { i18n } from "@/locales";
import { clearAccessToken, getAccessToken, setAccessToken } from "./lib";
import { getAppRouter } from "./routes";
import "./app.css";
import { initI18Next } from "@repo/i18next";
export const App = () => {
DineroFactory.globalLocale = "es-ES";
const i18n = initI18Next();
const queryClient = new QueryClient({
defaultOptions: {
queries: {
@ -29,6 +31,8 @@ export const App = () => {
},
});
console.log("import.meta.env.VITE_API_SERVER_URL => ", import.meta.env.VITE_API_SERVER_URL);
const axiosInstance = createAxiosInstance({
baseURL: import.meta.env.VITE_API_SERVER_URL,
getAccessToken: () => null, //getAccessToken,

View File

@ -1,34 +1,34 @@
import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";
import { initI18Next, registerTranslations } from "@repo/i18next";
import { useTranslation as useI18NextTranslation } from "react-i18next";
export const i18n = await initI18Next();
import { useEffect } from "react";
import enResources from "./en.json";
import esResources from "./es.json";
import enUIResources from "@repo/rdx-ui/locales/en.json";
import esUIResources from "@repo/rdx-ui/locales/es.json";
const APP_MODULE_NAME = "FACTUGES_WEB";
i18n
// detect user language
// learn more: https://github.com/i18next/i18next-browser-languageDetector
.use(LanguageDetector)
// pass the i18n instance to react-i18next.
.use(initReactI18next)
// init i18next
// for all options read: https://www.i18next.com/overview/configuration-options
.init({
detection: {
order: ["navigator"],
},
debug: false, //import.meta.env.DEV,
fallbackLng: "es",
interpolation: {
escapeValue: false,
},
resources: {
en: { ...enResources, ...enUIResources },
es: { ...esResources, ...esUIResources },
},
});
const ensureAppTranslations = () => {
registerTranslations(APP_MODULE_NAME, "es", esResources);
registerTranslations(APP_MODULE_NAME, "en", enResources);
};
export { i18n };
/**
* Hook de traducción específico de la app principal.
*/
export const useAppTranslation = () => {
// Hook base, sin namespace
const base = useI18NextTranslation();
const { i18n } = base;
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => {
// idempotente: solo añade si faltan bundles
ensureAppTranslations();
}, [i18n]);
// Hook con namespace de la app
return useI18NextTranslation(APP_MODULE_NAME);
};

View File

@ -2,6 +2,8 @@
"files": [],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
"compilerOptions": {
"resolveJsonModule": true,
"esModuleInterop": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]

View File

@ -1,6 +1,6 @@
{
"name": "@erp/auth",
"version": "0.0.14",
"version": "0.0.15",
"private": true,
"type": "module",
"sideEffects": false,
@ -32,7 +32,6 @@
"@repo/rdx-ui": "workspace:*",
"@repo/shadcn-ui": "workspace:*",
"@tanstack/react-query": "^5.90.6",
"i18next": "^25.1.1",
"react-hook-form": "^7.56.2",
"react-router-dom": "^6.26.0",
"react-secure-storage": "^1.3.2"

View File

@ -8,7 +8,7 @@ export function mockUser(req: RequestWithAuth, _res: Response, next: NextFunctio
userId: UniqueID.create("9e4dc5b3-96b9-4968-9490-14bd032fec5f").data,
email: EmailAddress.create("dev@example.com").data,
companyId: UniqueID.create("019a9667-6a65-767a-a737-48234ee50a3a").data,
companySlug: "acana",
companySlug: "alonsoysal",
roles: ["admin"],
};

View File

@ -1,9 +1,10 @@
import { IModuleClient, ModuleClientParams } from "@erp/core/client";
import i18next from "i18next";
import enResources from "../common/locales/en.json";
import esResources from "../common/locales/es.json";
import type { IModuleClient, ModuleClientParams } from "@erp/core/client";
import { AuthRoutes } from "./auth-routes";
//import enResources from "../common/locales/en.json";
//import esResources from "../common/locales/es.json";
const MODULE_NAME = "auth";
const MODULE_VERSION = "1.0.0";
@ -15,8 +16,8 @@ export const AuthModuleManifiest: IModuleClient = {
layout: "auth",
routes: (params: ModuleClientParams) => {
i18next.addResourceBundle("en", MODULE_NAME, enResources, true, true);
i18next.addResourceBundle("es", MODULE_NAME, esResources, true, true);
//i18next.addResourceBundle("en", MODULE_NAME, enResources, true, true);
//i18next.addResourceBundle("es", MODULE_NAME, esResources, true, true);
return AuthRoutes(params);
},
};

View File

@ -1,6 +1,6 @@
{
"name": "@erp/core",
"version": "0.0.14",
"version": "0.0.15",
"private": true,
"type": "module",
"sideEffects": false,
@ -33,6 +33,7 @@
},
"dependencies": {
"@hookform/resolvers": "^5.0.1",
"@repo/i18next": "workspace:*",
"@repo/rdx-criteria": "workspace:*",
"@repo/rdx-ddd": "workspace:*",
"@repo/rdx-logger": "workspace:*",
@ -45,7 +46,6 @@
"express": "^4.18.2",
"handlebars": "^4.7.8",
"http-status": "^2.1.0",
"i18next": "^25.1.1",
"lucide-react": "^0.503.0",
"mime-types": "^3.0.1",
"react-hook-form": "^7.58.1",

View File

@ -1,25 +1,37 @@
import { i18n } from "i18next";
import { registerTranslations } from "@repo/i18next";
import { useEffect } from "react";
import { useTranslation as useI18NextTranslation } from "react-i18next";
import enResources from "../common/locales/en.json";
import esResources from "../common/locales/es.json";
import { MODULE_NAME } from "./manifest";
const addMissingBundles = (i18n: i18n) => {
const needsEn = !i18n.hasResourceBundle("en", MODULE_NAME);
const needsEs = !i18n.hasResourceBundle("es", MODULE_NAME);
if (needsEn) {
i18n.addResourceBundle("en", MODULE_NAME, enResources, true, true);
}
if (needsEs) {
i18n.addResourceBundle("es", MODULE_NAME, esResources, true, true);
}
/**
* Registra dinámicamente las traducciones del módulo.
*/
const ensureModuleTranslations = () => {
registerTranslations(MODULE_NAME, "en", enResources);
registerTranslations(MODULE_NAME, "es", esResources);
};
/**
* Hook de traducción del módulo, version adaptada.
*
* - Asegura los bundles del módulo.
* - Devuelve el hook react-i18next con namespace.
*/
export const useTranslation = () => {
const { i18n } = useI18NextTranslation();
addMissingBundles(i18n);
// Hook base, sin namespace
const base = useI18NextTranslation();
const { i18n } = base;
// Asegura los bundles del módulo al montar (idempotente)
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => {
ensureModuleTranslations();
}, [i18n]);
// Hook con namespace
return useI18NextTranslation(MODULE_NAME);
};

View File

@ -1,6 +1,6 @@
{
"name": "@erp/customer-invoices",
"version": "0.0.14",
"version": "0.0.15",
"private": true,
"type": "module",
"sideEffects": false,
@ -38,6 +38,7 @@
"@erp/core": "workspace:*",
"@erp/customers": "workspace:*",
"@hookform/resolvers": "^5.0.1",
"@repo/i18next": "workspace:*",
"@repo/rdx-criteria": "workspace:*",
"@repo/rdx-ddd": "workspace:*",
"@repo/rdx-logger": "workspace:*",
@ -51,7 +52,6 @@
"dinero.js": "^1.9.1",
"express": "^4.18.2",
"handlebars": "^4.7.8",
"i18next": "^25.1.1",
"libphonenumber-js": "^1.12.7",
"lucide-react": "^0.503.0",
"pg-hstore": "^2.3.4",

View File

@ -1,34 +1,37 @@
import type { KeyPrefix, Namespace, i18n } from "i18next";
import {
type UseTranslationResponse,
useTranslation as useI18NextTranslation,
} from "react-i18next";
import { registerTranslations } from "@repo/i18next";
import { useEffect } from "react";
import { useTranslation as useI18NextTranslation } from "react-i18next";
import enResources from "../common/locales/en.json";
import esResources from "../common/locales/es.json";
import { MODULE_NAME } from "./manifest";
const addMissingBundles = (i18n: i18n) => {
const needsEn = !i18n.hasResourceBundle("en", MODULE_NAME);
const needsEs = !i18n.hasResourceBundle("es", MODULE_NAME);
if (needsEn) {
i18n.addResourceBundle("en", MODULE_NAME, enResources, true, true);
}
if (needsEs) {
i18n.addResourceBundle("es", MODULE_NAME, esResources, true, true);
}
/**
* Registra dinámicamente las traducciones del módulo.
*/
const ensureModuleTranslations = () => {
registerTranslations(MODULE_NAME, "en", enResources);
registerTranslations(MODULE_NAME, "es", esResources);
};
export const useTranslation = <
Ns extends Namespace = typeof MODULE_NAME,
K extends KeyPrefix<Ns> = undefined,
>(
keyPrefix?: K
): UseTranslationResponse<Ns, K> => {
const { i18n } = useI18NextTranslation();
addMissingBundles(i18n);
return useI18NextTranslation(MODULE_NAME, { keyPrefix });
/**
* Hook de traducción del módulo, version adaptada.
*
* - Asegura los bundles del módulo.
* - Devuelve el hook react-i18next con namespace.
*/
export const useTranslation = () => {
// Hook base, sin namespace
const base = useI18NextTranslation();
const { i18n } = base;
// Asegura los bundles del módulo al montar (idempotente)
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => {
ensureModuleTranslations();
}, [i18n]);
// Hook con namespace
return useI18NextTranslation(MODULE_NAME);
};

View File

@ -1,5 +1,5 @@
import type { TaxCatalogProvider } from "@erp/core";
import type { TFunction } from "i18next";
import type { ModuleTFunction } from "@repo/rdx-ui/locales/i18n.js";
import {
type PropsWithChildren,
createContext,
@ -10,7 +10,6 @@ import {
} from "react";
import { useTranslation } from "../../../../i18n";
import type { MODULE_NAME } from "../../../../manifest";
export type ProformaContextValue = {
company_id: string;
@ -23,7 +22,7 @@ export type ProformaContextValue = {
readOnly: boolean;
taxCatalog: TaxCatalogProvider;
t: TFunction<typeof MODULE_NAME>;
t: ModuleTFunction;
changeLanguage: (lang: string) => void;
changeCurrency: (currency: string) => void;

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -0,0 +1,351 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Factura</title>
<style type="text/css">{{ asset 'tailwind.min.css' }}</style>
<style type="text/css">
header {
width: 100%;
margin-bottom: 15px;
}
/* Fila superior */
.top-header {
display: flex;
justify-content: space-between;
width: 100%;
}
/* Bloque izquierdo */
.left-block {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 75%;
}
.logo {
height: 80px;
margin-bottom: 5px;
}
.company-text {
font-size: 7pt;
line-height: 1.2;
padding-left: 10px;
text-align: right;
}
/* Bloque derecho */
.right-block {
display: flex;
flex-direction: column;
/* uno encima de otro */
align-items: flex-end;
/* o flex-start / center según quieras */
justify-content: flex-start;
width: 25%;
padding: 4px;
}
.factura-img {
height: 45px;
}
.factura-text {
font-size: 26px;
font-weight: bolder;
}
/* Fila inferior */
.bottom-header {
margin-top: 10px;
display: flex;
justify-content: space-between;
gap: 20px;
}
/* Cuadros */
.info-box {
width: 40%;
}
.info-dire {
width: 70%;
}
/* ---------------------------- */
/* ESTRUCTURA BODY */
/* ---------------------------- */
body {
font-family: Arial, sans-serif;
margin: 40px;
color: #333;
font-size: 9pt;
line-height: 1.5;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 0px;
margin-bottom: 10px;
}
/* Anchos por columna */
.col-concepto { width: 75%; text-align: left; }
.col-cantidad { width: 5%; text-align: center;}
.col-precio { width: 10%; text-align: right;}
.col-total { width: 10%; text-align: right;}
table th,
table td {
border-top: 0px solid;
border-left: 1px solid #000;
border-right: 1px solid #000;
border-bottom: 0px solid;
padding: 3px 10px;
text-align: left;
vertical-align: top;
}
table th {
border-bottom: 1px solid #000;
}
.totals {
margin-top: 20px;
width: 100%;
}
.totals td {
padding: 5px 10px;
}
.totals td.label {
text-align: right;
font-weight: bold;
}
.resume-table {
width: 100%;
border-collapse: collapse;
font-size: 9pt;
font-family: Tahoma, sans-serif;
}
/* Columna izquierda (notas / forma de pago) */
.left-col {
width: 70%;
vertical-align: top;
padding: 10px;
border: 1px solid #000;
}
/* Etiquetas */
.resume-table .label {
width: 15%;
padding: 6px 8px;
text-align: right;
border: 1px solid #000;
}
/* Valores numéricos */
.resume-table .value {
width: 15%;
padding: 6px 8px;
text-align: right;
border: 1px solid #000;
}
/* Total factura */
.total-row .label,
.total-row .value {
background-color: #eee;
font-size: 9pt;
border: 1px solid #000;
}
.total {
color: #d10000;
font-weight: bold;
text-align: right;
}
.resume-table .empty {
border: 1px solid transparent;
}
footer {
margin-top: 40px;
font-size: 8px;
}
@media print {
* {
-webkit-print-color-adjust: exact;
}
thead {
display: table-header-group;
}
tfoot {
display: table-footer-group;
}
}
</style>
</head>
<body>
<header>
<!-- FILA SUPERIOR: logo + dirección / imagen factura -->
<div class="top-header">
<div class="left-block">
<div style="display: flex; align-items: center; gap: 4px; align-content: stretch">
<img src="{{asset 'logo_alonsoysal.jpg'}}" alt="Logo Alonso y Sal" class="logo" />
{{#if verifactu.qr_code}}
<div style="flex-grow: 0; flex-shrink: 0; flex-basis:90px;">
<img src="{{verifactu.qr_code}}" alt="QR factura" style="width: 90px; height: 90px;" />
</div>
<div style="text-align: left; flex-grow: 1; flex-shrink: 1; flex-basis: auto;">
QR tributario factura verificable en sede electronica de AEAT VERI*FACTU
</div>
{{/if}}
</div>
</div>
<div class="right-block">
<div>
<div class="company-text">
<p><strong>Cocinas y Baños</strong><br/>Calle Vía Carpetana 340<br/>28047 Madrid</p>
<p>Teléfono: 914 652 842<br/>WhatsApp: 607 528 495<br/>Email: <a href="mailto:info@alonsoysal.com">info@alonsoysal.com</a><br/>Web: <a href="https://www.acanainteriorismo.com" target="_blank">www.alonsoysal.com</a></p>
</div>
</div>
</div>
</div>
<!-- FILA INFERIOR: cuadro factura + cuadro cliente -->
<div class="bottom-header">
<div class="info-box">
<p class="factura-text">FACTURA</p>
<p>Factura nº: {{series}}{{invoice_number}}</p>
<p>Fecha: {{invoice_date}}</p>
</div>
<div class="info-box info-dire">
<h2 style="font-weight:600; text-transform:uppercase; margin-bottom:0.25rem;">{{recipient.name}}</h2>
<p>{{recipient.tin}}</p>
<p>{{recipient.street}}</p>
<p>{{recipient.postal_code}} {{recipient.city}} {{recipient.province}}</p>
</div>
</div>
</header>
<main id="main">
<section id="details">
<!-- Tu tabla -->
<table class="table-header">
<thead>
<tr>
<th class="col-concepto">Concepto</th>
<th class="col-cantidad">Cantidad</th>
<th class="col-precio">Precio unidad</th>
<th class="col-total">Importe total</th>
</tr>
</thead>
<tbody>
{{#each items}}
<tr>
<td>{{description}}</td>
<td style="width: 10px !important; text-align:right;">{{#if quantity}}{{quantity}}{{else}}&nbsp;{{/if}}</td>
<td style="text-align:right;">{{#if unit_amount}}{{unit_amount}}{{else}}&nbsp;{{/if}}</td>
<td style="text-align:right;">{{#if taxable_amount}}{{taxable_amount}}{{else}}&nbsp;{{/if}}</td>
</td>
</tr>
{{/each}}
<tr class="resume-table">
<!-- Columna izquierda: notas y forma de pago -->
<td class="left-col" rowspan="10">
{{#if payment_method}}
<p><strong>Forma de pago:</strong> {{payment_method}}</p>
{{/if}}
{{#if notes}}
<p style="margin-top:0.5rem;"><strong>Notas:</strong> {{notes}}</p>
{{/if}}
</td>
<!-- Columna derecha: totales -->
{{#if discount_percentage}}
<td class="label">Importe neto</td>
<td class="value">{{subtotal_amount}}</td>
{{else}}
<td colspan="2" class="label">Base imponible</td>
<td colspan="2" class="value">{{taxable_amount}}</td>
{{/if}}
</tr>
{{#if discount_percentage}}
<tr class="resume-table">
<td class="label">Dto {{discount_percentage}}</td>
<td class="value">{{discount_amount.value}}</td>
</tr>
<tr class="resume-table">
<td colspan="2" class="label">Base imponible</td>
<td colspan="2" class="value">{{taxable_amount}}</td>
</tr>
{{/if}}
{{#each taxes}}
<tr class="resume-table">
<td class="label">{{tax_name}}</td>
<td class="value">{{taxes_amount}}</td>
</tr>
{{/each}}
<tr class="total-row">
<td class="label"><strong>Total factura</strong></td>
<td colspan="2" class="value total"><strong>{{total_amount}}</strong></td>
</tr>
</tbody>
</table>
</section>
</main>
<footer id="footer" style="margin-top:1rem; border-top:1px solid #000000;">
<aside style="margin-top: 1rem;">
<tfoot>
<p style="text-align: center;">Insc. en el Reg. Merc. de Madrid, Tomo 31.839, Libro 0, Folio 191, Sección 8, Hoja
M-572991
CIF: B86913910</p>
<p style="text-align: left; font-size: 6pt;">Información en protección de datos:<br />De conformidad con lo
dispuesto en el RGPD y LOPDGDD,
informamos que los datos personales serán tratados por
ALISO DESIGN S.L para cumplir con la obligación tributaria de emitir facturas. Podrá solicitar más
información, y ejercer sus derechos escribiendo a info@acanainteriorismo.com o mediante correo postal a la
dirección CALLE
LA FUNDICION 27 POL. IND. SANTA ANA (28522) RIVAS-VACIAMADRID, MADRID. Para el ejercicio de sus derechos, en
caso
de que sea necesario, se le solicitará documento que acredite su identidad. Si siente vulnerados sus derechos
puede presentar una reclamación ante la AEPD, en su web: www.aepd.es.</p>
</tfoot>
</aside>
</footer>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -0,0 +1,344 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="{{ asset 'tailwind.css' }}" />
<title>Factura proforma</title>
<style type="text/css">
/* ---------------------------- */
/* ESTRUCTURA CABECERA */
/* ---------------------------- */
header {
width: 100%;
margin-bottom: 15px;
}
/* Fila superior */
.top-header {
display: flex;
justify-content: space-between;
width: 100%;
}
/* Bloque izquierdo */
.left-block {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 60%;
}
.logo {
height: 70px;
margin-bottom: 5px;
}
.company-text {
font-size: 7pt;
line-height: 1;
padding-left: 10px;
}
/* Bloque derecho */
.right-block {
display: flex;
align-items: flex-start;
justify-content: flex-end;
width: 40%;
}
.factura-img {
height: 45px;
}
/* Fila inferior */
.bottom-header {
margin-top: 10px;
display: flex;
justify-content: space-between;
gap: 20px;
}
/* Cuadros */
.info-box {
border: 1px solid black;
border-radius: 12px;
padding: 8px 12px;
width: 45%;
}
.info-dire {
width: 65%;
}
/* ---------------------------- */
/* ESTRUCTURA BODY */
/* ---------------------------- */
body {
font-family: Tahoma, sans-serif;
margin: 40px;
color: #333;
font-size: 9pt;
line-height: 1.5;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 0px;
margin-bottom: 10px;
}
table th,
table td {
border-top: 0px solid;
border-left: 1px solid #000;
border-right: 1px solid #000;
border-bottom: 0px solid;
padding: 3px 10px;
text-align: left;
vertical-align: top;
}
table th {
margin-bottom: 10px;
border-top: 1px solid #000;
border-bottom: 1px solid #000;
text-align: center;
background-color: #e7e0df;
color: #ff0014;
}
.totals {
margin-top: 20px;
width: 100%;
}
.totals td {
padding: 5px 10px;
}
.totals td.label {
text-align: right;
font-weight: bold;
}
.resume-table {
width: 100%;
border-collapse: collapse;
font-size: 9pt;
font-family: Tahoma, sans-serif;
}
/* Columna izquierda (notas / forma de pago) */
.left-col {
width: 70%;
vertical-align: top;
padding: 10px;
border: 1px solid #000;
}
/* Etiquetas */
.resume-table .label {
width: 15%;
padding: 6px 8px;
text-align: right;
border: 1px solid #000;
}
/* Valores numéricos */
.resume-table .value {
width: 15%;
padding: 6px 8px;
text-align: right;
border: 1px solid #000;
}
/* Total factura */
.total-row .label,
.total-row .value {
background-color: #eee;
font-size: 9pt;
border: 1px solid #000;
}
.total {
color: #d10000;
font-weight: bold;
text-align: right;
}
.resume-table .empty {
border: 1px solid transparent;
}
footer {
margin-top: 40px;
font-size: 8px;
}
@media print {
* {
-webkit-print-color-adjust: exact;
}
thead {
display: table-header-group;
}
tfoot {
display: table-footer-group;
}
}
</style>
</head>
<body>
<header>
<!-- FILA SUPERIOR: logo + dirección / imagen factura -->
<div class="top-header">
<div class="left-block">
<img src="{{asset 'logo_acana.jpg'}}" alt="Logo Acana" class="logo" />
<div class="company-text">
<p>Aliso Design S.L. B86913910</p>
<p>C/ La Fundición, 27. Pol. Santa Ana</p>
<p>Rivas Vaciamadrid 28522 Madrid</p>
<p>Telf: 91 301 65 57 / 91 301 65 58</p>
<p>
<a href="mailto:info@acanainteriorismo.com">info@acanainteriorismo.com</a> -
<a href="https://www.acanainteriorismo.com" target="_blank">www.acanainteriorismo.com</a>
</p>
</div>
</div>
<div class="right-block">
<img src="{{asset 'factura_acana.jpg' }}" alt="Factura" class="factura-img" />
</div>
</div>
<!-- FILA INFERIOR: cuadro factura + cuadro cliente -->
<div class="bottom-header">
<div class="info-box">
<p>Factura nº: <strong>{{series}}{{invoice_number}}</strong></p>
<p>Fecha: <strong>{{invoice_date}}</strong></p>
<p>Página <span class="pageNumber"></span> de <span class="totalPages"></span></p>
</div>
<div class="info-box info-dire">
<h2 class="font-semibold uppercase mb-1">{{recipient.name}}</h2>
<p>{{recipient.tin}}</p>
<p>{{recipient.street}}</p>
<p>{{recipient.postal_code}} {{recipient.city}} {{recipient.province}}</p>
</div>
</div>
</header>
<main id="main">
<section id="details">
<!-- Tu tabla -->
<table class="table-header">
<thead>
<tr>
<th class="py-2">Concepto</th>
<th class="py-2">Ud.</th>
<th class="py-2">Imp.</th>
<th class="py-2">&nbsp;</th>
<th class="py-2">Imp.&nbsp;total</th>
</tr>
</thead>
<tbody>
{{#each items}}
<tr>
<td>{{description}}</td>
<td class="text-right">{{#if quantity}}{{quantity}}{{else}}&nbsp;{{/if}}</td>
<td class="text-right">{{#if unit_amount}}{{unit_amount}}{{else}}&nbsp;{{/if}}</td>
<td class="text-right">{{#if discount_percentage}}{{discount_percentage}}{{else}}&nbsp;{{/if}}</td>
<td class="text-right">{{#if taxable_amount}}{{taxable_amount}}{{else}}&nbsp;{{/if}}</td>
</td>
</tr>
{{/each}}
<tr class="resume-table">
<!-- Columna izquierda: notas y forma de pago -->
<td class="left-col" rowspan="10">
{{#if payment_method}}
<p><strong>Forma de pago:</strong> {{payment_method}}</p>
{{/if}}
{{#if notes}}
<p class="mt-2"><strong>Notas:</strong> {{notes}}</p>
{{/if}}
</td>
<!-- Columna derecha: totales -->
{{#if discount_percentage}}
<td colspan="2" class="label">Importe neto</td>
<td colspan="2" class="value">{{subtotal_amount}}</td>
{{else}}
<td colspan="2" class="label">Base imponible</td>
<td colspan="2" class="value">{{taxable_amount}}</td>
{{/if}}
</tr>
{{#if discount_percentage}}
<tr class="resume-table">
<td colspan="2" class="label">Dto {{discount_percentage}}</td>
<td colspan="2" class="value">{{discount_amount.value}}</td>
</tr>
<tr class="resume-table">
<td colspan="2" class="label">Base imponible</td>
<td colspan="2" class="value">{{taxable_amount}}</td>
</tr>
{{/if}}
{{#each taxes}}
<tr class="resume-table">
<td colspan="2" class="label">{{tax_name}}</td>
<td colspan="2" class="value">{{taxes_amount}}</td>
</tr>
{{/each}}
<tr class="total-row">
<td colspan="2" class="label"><strong>Total factura</strong></td>
<td colspan="2" class="value total"><strong>{{total_amount}}</strong></td>
</tr>
</tbody>
</table>
</section>
</main>
<footer id="footer" class="mt-4 border-t border-black">
<aside class="mt-4">
<tfoot>
<p class="text-center">Insc. en el Reg. Merc. de Madrid, Tomo 31.839, Libro 0, Folio 191, Sección 8, Hoja
M-572991
CIF: B86913910</p>
<p class="text-left" style="font-size: 6pt;">Información en protección de datos<br />De conformidad con lo
dispuesto en el RGPD y LOPDGDD,
informamos que los datos personales serán tratados por
ALISO DESIGN S.L para cumplir con la obligación tributaria de emitir facturas. Podrá solicitar más
información, y ejercer sus derechos escribiendo a info@acanainteriorismo.com o mediante correo postal a la
dirección CALLE
LA FUNDICION 27 POL. IND. SANTA ANA (28522) RIVAS-VACIAMADRID, MADRID. Para el ejercicio de sus derechos, en
caso
de que sea necesario, se le solicitará documento que acredite su identidad. Si siente vulnerados sus derechos
puede presentar una reclamación ante la AEPD, en su web: www.aepd.es.</p>
</tfoot>
</aside>
</footer>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"name": "@erp/customers",
"version": "0.0.14",
"version": "0.0.15",
"private": true,
"type": "module",
"sideEffects": false,
@ -32,6 +32,7 @@
"@erp/auth": "workspace:*",
"@erp/core": "workspace:*",
"@hookform/resolvers": "^5.0.1",
"@repo/i18next": "workspace:*",
"@repo/rdx-criteria": "workspace:*",
"@repo/rdx-ddd": "workspace:*",
"@repo/rdx-logger": "workspace:*",
@ -41,7 +42,6 @@
"@tanstack/react-query": "^5.90.6",
"@tanstack/react-table": "^8.21.3",
"express": "^4.18.2",
"i18next": "^25.6.0",
"lucide-react": "^0.503.0",
"react-data-table-component": "^7.7.0",
"react-hook-form": "^7.58.1",

View File

@ -1,25 +1,37 @@
import { i18n } from "i18next";
import { registerTranslations } from "@repo/i18next";
import { useEffect } from "react";
import { useTranslation as useI18NextTranslation } from "react-i18next";
import enResources from "../common/locales/en.json";
import esResources from "../common/locales/es.json";
import { MODULE_NAME } from "./manifest";
const addMissingBundles = (i18n: i18n) => {
const needsEn = !i18n.hasResourceBundle("en", MODULE_NAME);
const needsEs = !i18n.hasResourceBundle("es", MODULE_NAME);
if (needsEn) {
i18n.addResourceBundle("en", MODULE_NAME, enResources, true, true);
}
if (needsEs) {
i18n.addResourceBundle("es", MODULE_NAME, esResources, true, true);
}
/**
* Registra dinámicamente las traducciones del módulo.
*/
const ensureModuleTranslations = () => {
registerTranslations(MODULE_NAME, "en", enResources);
registerTranslations(MODULE_NAME, "es", esResources);
};
/**
* Hook de traducción del módulo, version adaptada.
*
* - Asegura los bundles del módulo.
* - Devuelve el hook react-i18next con namespace.
*/
export const useTranslation = () => {
const { i18n } = useI18NextTranslation();
addMissingBundles(i18n);
// Hook base, sin namespace
const base = useI18NextTranslation();
const { i18n } = base;
// Asegura los bundles del módulo al montar (idempotente)
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => {
ensureModuleTranslations();
}, [i18n]);
// Hook con namespace
return useI18NextTranslation(MODULE_NAME);
};

View File

@ -1,6 +1,6 @@
{
"name": "@erp/doc-numbering",
"version": "0.0.14",
"version": "0.0.15",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -18,7 +18,7 @@
"ui:add": "pnpm --filter @repo/shadcn-ui ui:add",
"create:package": "ts-node scripts/create-package.ts",
"volta:install": "curl https://get.volta.sh | bash",
"clean": "turbo run clean && rimraf ./node_modules && rimraf ./package-lock.json"
"clean": "turbo run clean && rimraf ./node_modules && rimraf ./package-lock.json && rimraf ./out"
},
"devDependencies": {
"@biomejs/biome": "2.3.1",
@ -35,4 +35,4 @@
"node": ">=24"
},
"packageManager": "pnpm@10.20.0"
}
}

View File

@ -0,0 +1,25 @@
{
"name": "@repo/i18next",
"version": "0.0.0",
"private": true,
"type": "module",
"sideEffects": false,
"scripts": {
"typecheck": "tsc -p tsconfig.json --noEmit",
"clean": "rimraf .turbo node_modules dist"
},
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"i18next": "25.6.0",
"i18next-browser-languagedetector": "^8.1.0",
"i18next-http-backend": "^3.0.2",
"react-i18next": "^12.3.1"
},
"devDependencies": {
"@repo/typescript-config": "workspace:*",
"@types/node": "^22.15.12",
"typescript": "^5.9.3"
}
}

57
packages/i18n/src/i18n.ts Normal file
View File

@ -0,0 +1,57 @@
import i18next, { type i18n } from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";
let hasInit = false;
export const initI18Next = () => {
if (hasInit) return i18next;
i18next
.use(LanguageDetector)
.use(initReactI18next)
.init({
detection: { order: ["navigator"] },
debug: false,
fallbackLng: "es",
interpolation: { escapeValue: false },
});
hasInit = true;
return i18next;
};
/**
* Registra dinámicamente traducciones de un módulo.
*
* Cada módulo tendrá su propio namespace.
*
* idempotente: si el bundle ya existe, no se vuelve a añadir.
*/
export const registerTranslations = (
moduleName: string,
locale: string,
resources: Record<string, unknown>
): void => {
if (!hasInit) {
throw new Error("i18n not initialized. Call initI18Next() first.");
}
const ns = moduleName;
const alreadyExists = i18next.hasResourceBundle(locale, ns);
if (!alreadyExists) {
i18next.addResourceBundle(locale, ns, resources, true, true);
}
};
/**
* Acceso al `t()` global por si se necesita en librerías de backend.
*/
export const t = (...args: Parameters<i18n["t"]>) => {
if (!hasInit) {
throw new Error("i18n not initialized. Call initI18Next() first.");
}
return i18next.t(...args);
};

View File

@ -0,0 +1 @@
export * from "./i18n";

View File

@ -0,0 +1,8 @@
{
"extends": "@repo/typescript-config/buildless.json",
"compilerOptions": {
"rootDir": "src"
},
"include": ["src"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}

View File

@ -1,6 +1,6 @@
{
"name": "@repo/rdx-criteria",
"version": "0.0.14",
"version": "0.0.15",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@repo/rdx-ddd",
"version": "0.0.14",
"version": "0.0.15",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@repo/rdx-logger",
"version": "0.0.14",
"version": "0.0.15",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -47,6 +47,7 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@radix-ui/react-tabs": "^1.1.12",
"@repo/i18next": "workspace:*",
"@repo/shadcn-ui": "workspace:*",
"@tanstack/react-table": "^8.21.3",
"class-variance-authority": "^0.7.1",
@ -60,4 +61,4 @@
"sonner": "^2.0.3",
"zod": "^4.1.11"
}
}
}

View File

@ -1,15 +1,17 @@
import { Button } from "@repo/shadcn-ui/components";
import { t } from "i18next";
import { ChevronLeftIcon } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "../../locales/i18n.ts";
export const BackHistoryButton = () => {
const { t } = useTranslation();
const navigate = useNavigate();
return (
<Button variant='outline' size='icon' className='h-7 w-7' onClick={() => navigate(-1)}>
<ChevronLeftIcon className='w-4 h-4' />
<span className='sr-only'>{t("common.back")}</span>
<Button className="h-7 w-7" onClick={() => navigate(-1)} size="icon" variant="outline">
<ChevronLeftIcon className="w-4 h-4" />
<span className="sr-only">{t("common.back")}</span>
</Button>
);
};

View File

@ -9,10 +9,10 @@ import {
DialogTrigger,
ScrollArea,
} from "@repo/shadcn-ui/components";
import { t } from "i18next";
import { HelpCircleIcon } from "lucide-react";
import React from "react";
import type React from "react";
import { useTranslation } from "../../locales/i18n.ts";
interface HelpButtonProps {
buttonText: string;
@ -27,24 +27,25 @@ export const HelpButton = ({
content,
className = "",
}: HelpButtonProps) => {
const { t } = useTranslation();
return (
<div className={`flex items-baseline justify-center mr-4 font-medium ${className}`}>
<Dialog>
<DialogTrigger asChild>
<Button variant='link' className='inline-flex items-center font-medium group'>
<span className='underline-offset-4 group-hover:underline'>{buttonText}</span>
<HelpCircleIcon className='w-4 h-4 ml-1 text-muted-foreground' />
<Button className="inline-flex items-center font-medium group" variant="link">
<span className="underline-offset-4 group-hover:underline">{buttonText}</span>
<HelpCircleIcon className="w-4 h-4 ml-1 text-muted-foreground" />
</Button>
</DialogTrigger>
<DialogContent className='sm:max-w-[425px]'>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
</DialogHeader>
<ScrollArea className='grid gap-4 py-2'>
<ScrollArea className="grid gap-4 py-2">
{content}
<DialogFooter>
<DialogClose asChild>
<Button type='button'>{t("common.close")}</Button>
<Button type="button">{t("common.close")}</Button>
</DialogClose>
</DialogFooter>
</ScrollArea>

View File

@ -1,137 +0,0 @@
import { Header, Table, flexRender } from "@tanstack/react-table";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
Separator,
} from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { t } from "i18next";
import { ArrowDownIcon, ArrowDownUpIcon, ArrowUpIcon, EyeOffIcon } from "lucide-react";
interface DataTableColumnHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {
table: Table<TData>;
header: Header<TData, TValue>;
}
export function DataTableColumnHeader<TData, TValue>({
table,
header,
className,
}: DataTableColumnHeaderProps<TData, TValue>) {
if (!header.column.getCanSort()) {
return (
<>
<div className={cn("data-[state=open]:bg-accent tracking-wide text-ellipsis", className)}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</div>
{false && header.column.getCanResize() && (
<Separator
orientation='vertical'
className={cn(
"absolute top-0 h-full w-[5px] bg-black/10 cursor-col-resize",
table.options.columnResizeDirection,
header.column.getIsResizing() ? "bg-primary opacity-100" : ""
)}
{...{
onDoubleClick: () => header.column.resetSize(),
onMouseDown: header.getResizeHandler(),
onTouchStart: header.getResizeHandler(),
style: {
transform:
table.options.columnResizeMode === "onEnd" && header.column.getIsResizing()
? `translateX(${
(table.options.columnResizeDirection === "rtl" ? -1 : 1) *
(table.getState().columnSizingInfo.deltaOffset ?? 0)
}px)`
: "",
},
}}
/>
)}
</>
);
}
return (
<div className={cn("flex items-center space-x-2", className)}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-label={
header.column.getIsSorted() === "desc"
? t("common.sort_desc_description")
: header.column.getIsSorted() === "asc"
? t("common.sort_asc_description")
: t("sort_none_description")
}
size='sm'
variant='ghost'
className='-ml-3 h-8 data-[state=open]:bg-accent font-bold text-muted-foreground'
>
{flexRender(header.column.columnDef.header, header.getContext())}
{header.column.getIsSorted() === "desc" ? (
<ArrowDownIcon className='w-4 h-4 ml-2' aria-hidden='true' />
) : header.column.getIsSorted() === "asc" ? (
<ArrowUpIcon className='w-4 h-4 ml-2' aria-hidden='true' />
) : (
<ArrowDownUpIcon
className='w-4 h-4 ml-2 text-muted-foreground/30'
aria-hidden='true'
/>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='start'>
{header.column.getCanSort() && (
<>
<DropdownMenuItem
onClick={() => header.column.toggleSorting(false)}
aria-label={t("common.sort_asc")}
>
<ArrowUpIcon
className='mr-2 h-3.5 w-3.5 text-muted-foreground/70'
aria-hidden='true'
/>
{t("common.sort_asc")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => header.column.toggleSorting(true)}
aria-label={t("common.sort_desc")}
>
<ArrowDownIcon
className='mr-2 h-3.5 w-3.5 text-muted-foreground/70'
aria-hidden='true'
/>
{t("common.sort_desc")}
</DropdownMenuItem>
</>
)}
{header.column.getCanSort() && header.column.getCanHide() && <DropdownMenuSeparator />}
{header.column.getCanHide() && (
<DropdownMenuItem
onClick={() => header.column.toggleVisibility(false)}
aria-label={t("Hide")}
>
<EyeOffIcon
className='mr-2 h-3.5 w-3.5 text-muted-foreground/70'
aria-hidden='true'
/>
{t("Hide")}
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}

View File

@ -1,73 +0,0 @@
import { useTranslation } from "@repo/rdx-ui/locales/i18n.ts";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@repo/shadcn-ui/components";
import { CellContext } from "@tanstack/react-table";
import { MoreVerticalIcon } from "lucide-react";
import { ReactElement } from "react";
export type DataTablaRowActionFunction<TData> = (
props: CellContext<TData, unknown>
) => DataTableRowActionDefinition<TData>[];
export type DataTableRowActionDefinition<TData> = {
label: string | "-";
icon?: ReactElement<any, any>;
shortcut?: string;
onClick?: (props: CellContext<TData, unknown>, e: React.BaseSyntheticEvent) => void;
};
export type DataTableRowActionsProps<TData, _TValue = unknown> = {
className?: string;
actions?: DataTablaRowActionFunction<TData>;
rowContext: CellContext<TData, unknown>;
};
export function DataTableRowActions<TData = any, TValue = unknown>({
actions,
rowContext,
}: DataTableRowActionsProps<TData, TValue>) {
const { t } = useTranslation();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size='icon' variant='outline' className='w-8 h-8'>
<MoreVerticalIcon className='h-3.5 w-3.5' />
<span className='sr-only'>{t("common.open_menu")}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>{t("common.actions")} </DropdownMenuLabel>
{actions &&
actions(rowContext).map((action, index) => {
// Use a more stable key: for separators, combine 'separator' and index; for items, use label and index
if (action.label === "-") {
// Use a more stable key by combining a static string and the previous/next action label if possible
const prevLabel = actions(rowContext)[index - 1]?.label ?? "start";
const nextLabel = actions(rowContext)[index + 1]?.label ?? "end";
return <DropdownMenuSeparator key={`separator-${prevLabel}-${nextLabel}`} />;
}
return (
<DropdownMenuItem
key={`action-${typeof action.label === "string" ? action.label : "item"}-${index}`}
onClick={(event) => (action.onClick ? action.onClick(rowContext, event) : null)}
>
{action.icon && <>{action.icon}</>}
{action.label}
{action.shortcut && <DropdownMenuShortcut>{action.shortcut}</DropdownMenuShortcut>}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -1,40 +0,0 @@
import { useSortable } from "@dnd-kit/sortable";
import { Button } from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { t } from "i18next";
import { GripVerticalIcon } from "lucide-react";
export interface DataTableRowDragHandleCellProps {
rowId: string;
className?: string;
}
export const DataTableRowDragHandleCell = ({
rowId,
className,
}: DataTableRowDragHandleCellProps) => {
const { attributes, listeners, isDragging } = useSortable({
id: rowId,
});
return (
<Button
onClick={(event) => {
event.preventDefault();
return;
}}
size='icon'
variant='link'
className={cn(
isDragging ? "cursor-grabbing" : "cursor-grab",
"w-4 h-4 mt-2 text-ring hover:text-muted-foreground",
className
)}
{...attributes}
{...listeners}
>
<GripVerticalIcon className='w-4 h-4' />
<span className='sr-only'>{t("common.move_row")}</span>
</Button>
);
};

View File

@ -1,3 +0,0 @@
export * from "./datatable-column-header.tsx";
export * from "./datatable-row-actions.tsx";
export * from "./datatable-row-drag-handle-cell.tsx";

View File

@ -1,5 +1,7 @@
import { cn } from "@repo/shadcn-ui/lib/utils";
import { useTranslation } from "../../locales/i18n.ts";
import { LoadingSpinIcon } from "./loading-spin-icon.tsx";
export type LoadingIndicatorProps = {
@ -26,7 +28,7 @@ export const LoadingIndicator = ({
return (
<div className={"flex flex-col items-center max-w-xs justify-center w-full h-full mx-auto"}>
<LoadingSpinIcon size={12} className={loadingSpinClassName} />
<LoadingSpinIcon className={loadingSpinClassName} size={12} />
{/*<Spinner {...spinnerProps} />*/}
{title ? (
<h2

View File

@ -1,28 +1,37 @@
import { registerTranslations } from "@repo/i18next";
import { useEffect } from "react";
import { useTranslation as useI18NextTranslation } from "react-i18next";
import { PACKAGE_NAME } from "../index.ts";
import enResources from "./en.json" with { type: "json" };
import esResources from "./es.json" with { type: "json" };
const addMissingBundles = (i18n: any) => {
const needsEn = !i18n.hasResourceBundle("en", PACKAGE_NAME);
const needsEs = !i18n.hasResourceBundle("es", PACKAGE_NAME);
if (needsEn) {
i18n.addResourceBundle("en", PACKAGE_NAME, enResources, true, true);
}
if (needsEs) {
i18n.addResourceBundle("es", PACKAGE_NAME, esResources, true, true);
}
/**
* Registra dinámicamente las traducciones del módulo.
*/
const ensureModuleTranslations = () => {
registerTranslations(PACKAGE_NAME, "en", enResources);
registerTranslations(PACKAGE_NAME, "es", esResources);
};
/**
* Hook de traducción del módulo, version adaptada.
*
* - Asegura los bundles del módulo.
* - Devuelve el hook react-i18next con namespace.
*/
export const useTranslation = () => {
const { i18n } = useI18NextTranslation();
// Hook base, sin namespace
const base = useI18NextTranslation();
const { i18n } = base;
// Asegura los bundles del módulo al montar (idempotente)
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => {
addMissingBundles(i18n);
ensureModuleTranslations();
}, [i18n]);
// Hook con namespace
return useI18NextTranslation(PACKAGE_NAME);
};

View File

@ -1,6 +1,6 @@
{
"name": "@repo/rdx-utils",
"version": "0.0.14",
"version": "0.0.15",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -207,6 +207,9 @@ importers:
'@erp/customers':
specifier: workspace:*
version: link:../../modules/customers
'@repo/i18next':
specifier: workspace:*
version: link:../../packages/i18n
'@repo/rdx-ui':
specifier: workspace:*
version: link:../../packages/rdx-ui
@ -225,12 +228,6 @@ importers:
dinero.js:
specifier: ^1.9.1
version: 1.9.1
i18next:
specifier: ^25.0.2
version: 25.6.0(typescript@5.8.3)
i18next-browser-languagedetector:
specifier: ^8.1.0
version: 8.2.0
react-error-boundary:
specifier: ^6.0.0
version: 6.0.0(react@19.2.0)
@ -310,9 +307,6 @@ importers:
'@tanstack/react-query':
specifier: ^5.90.6
version: 5.90.6(react@19.2.0)
i18next:
specifier: ^25.1.1
version: 25.6.0(typescript@5.9.3)
react-hook-form:
specifier: ^7.56.2
version: 7.66.0(react@19.2.0)
@ -350,6 +344,9 @@ importers:
'@hookform/resolvers':
specifier: ^5.0.1
version: 5.2.2(react-hook-form@7.66.0(react@19.2.0))
'@repo/i18next':
specifier: workspace:*
version: link:../../packages/i18n
'@repo/rdx-criteria':
specifier: workspace:*
version: link:../../packages/rdx-criteria
@ -386,9 +383,6 @@ importers:
http-status:
specifier: ^2.1.0
version: 2.1.0
i18next:
specifier: ^25.1.1
version: 25.6.0(typescript@5.9.3)
lucide-react:
specifier: ^0.503.0
version: 0.503.0(react@19.2.0)
@ -459,6 +453,9 @@ importers:
'@hookform/resolvers':
specifier: ^5.0.1
version: 5.2.2(react-hook-form@7.66.0(react@19.2.0))
'@repo/i18next':
specifier: workspace:*
version: link:../../packages/i18n
'@repo/rdx-criteria':
specifier: workspace:*
version: link:../../packages/rdx-criteria
@ -498,9 +495,6 @@ importers:
handlebars:
specifier: ^4.7.8
version: 4.7.8
i18next:
specifier: ^25.1.1
version: 25.6.0(typescript@5.9.3)
libphonenumber-js:
specifier: ^1.12.7
version: 1.12.25
@ -571,6 +565,9 @@ importers:
'@hookform/resolvers':
specifier: ^5.0.1
version: 5.2.2(react-hook-form@7.66.0(react@19.2.0))
'@repo/i18next':
specifier: workspace:*
version: link:../../packages/i18n
'@repo/rdx-criteria':
specifier: workspace:*
version: link:../../packages/rdx-criteria
@ -598,9 +595,6 @@ importers:
express:
specifier: ^4.18.2
version: 4.21.2
i18next:
specifier: ^25.6.0
version: 25.6.0(typescript@5.9.3)
lucide-react:
specifier: ^0.503.0
version: 0.503.0(react@19.2.0)
@ -679,6 +673,31 @@ importers:
specifier: ^5.9.3
version: 5.9.3
packages/i18n:
dependencies:
i18next:
specifier: 25.6.0
version: 25.6.0(typescript@5.9.3)
i18next-browser-languagedetector:
specifier: ^8.1.0
version: 8.2.0
i18next-http-backend:
specifier: ^3.0.2
version: 3.0.2
react-i18next:
specifier: ^12.3.1
version: 12.3.1(i18next@25.6.0(typescript@5.9.3))(react@19.2.0)
devDependencies:
'@repo/typescript-config':
specifier: workspace:*
version: link:../typescript-config
'@types/node':
specifier: ^22.15.12
version: 22.19.0
typescript:
specifier: ^5.9.3
version: 5.9.3
packages/rdx-criteria:
dependencies:
sequelize:
@ -759,6 +778,9 @@ importers:
'@radix-ui/react-tabs':
specifier: ^1.1.12
version: 1.1.13(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@repo/i18next':
specifier: workspace:*
version: link:../i18n
'@repo/shadcn-ui':
specifier: workspace:*
version: link:../shadcn-ui
@ -3340,6 +3362,9 @@ packages:
create-require@1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
cross-fetch@4.0.0:
resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@ -4036,6 +4061,9 @@ packages:
i18next-browser-languagedetector@8.2.0:
resolution: {integrity: sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==}
i18next-http-backend@3.0.2:
resolution: {integrity: sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==}
i18next@25.6.0:
resolution: {integrity: sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw==}
peerDependencies:
@ -5107,6 +5135,19 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
react-i18next@12.3.1:
resolution: {integrity: sha512-5v8E2XjZDFzK7K87eSwC7AJcAkcLt5xYZ4+yTPDAW1i7C93oOY1dnr4BaQM7un4Hm+GmghuiPvevWwlca5PwDA==}
peerDependencies:
i18next: '>= 19.0.0'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
react-i18next@15.7.4:
resolution: {integrity: sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==}
peerDependencies:
@ -8425,6 +8466,12 @@ snapshots:
create-require@1.1.1: {}
cross-fetch@4.0.0:
dependencies:
node-fetch: 2.7.0
transitivePeerDependencies:
- encoding
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@ -9188,6 +9235,12 @@ snapshots:
dependencies:
'@babel/runtime': 7.28.4
i18next-http-backend@3.0.2:
dependencies:
cross-fetch: 4.0.0
transitivePeerDependencies:
- encoding
i18next@25.6.0(typescript@5.8.3):
dependencies:
'@babel/runtime': 7.28.4
@ -10276,6 +10329,13 @@ snapshots:
dependencies:
react: 19.2.0
react-i18next@12.3.1(i18next@25.6.0(typescript@5.9.3))(react@19.2.0):
dependencies:
'@babel/runtime': 7.28.4
html-parse-stringify: 3.0.1
i18next: 25.6.0(typescript@5.9.3)
react: 19.2.0
react-i18next@15.7.4(i18next@25.6.0(typescript@5.8.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.8.3):
dependencies:
'@babel/runtime': 7.28.4

View File

@ -1,16 +0,0 @@
{
email soporte@rodax-software.com
auto_https disable_redirects
}
https://presupuestos.uecko.com:13001 {
reverse_proxy backend:3001
encode gzip # Comprime las respuestas con gzip
}
:443 {
root * /srv
file_server
try_files {path} /index.html # Esto asegura que las rutas en React funcionen correctamente
}

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_VERSION="1.2.0"
SCRIPT_VERSION="1.2.1"
# =====================================================
# FACTUGES Build Script
@ -216,19 +216,20 @@ if [[ "$MODE" == "api" || "$MODE" == "all" ]]; then
EOF
echo "📦 API manifest generado en ${OUT_API_DIR}/manifest-v${API_VERSION}-${DATE}.json"
if [[ "$LOAD" == true ]]; then
echo "📥 Cargando imagen en producción vps-2.rodax-software.com..."
[[ "$MODE" == "web" || "$MODE" == "all" ]] && scp -r -P 49152 ${OUT_WEB_DIR} rodax@vps-2.rodax-software.com:/opt/factuges/${COMPANY}/
[[ "$MODE" == "api" || "$MODE" == "all" ]] && scp -r -P 49152 ${OUT_API_DIR} rodax@vps-2.rodax-software.com:/opt/factuges/${COMPANY}/
[[ "$MODE" == "api" || "$MODE" == "all" ]] && RESULT=$(ssh -p 49152 rodax@vps-2.rodax-software.com "docker load -i /opt/factuges/${COMPANY}/api/${TAR_FILE_LATEST}")
[[ "$MODE" == "api" || "$MODE" == "all" ]] && echo $RESULT
#docker load -i "${TAR_FILE_V}"
echo "✅ Imagen cargada en producción"
fi
fi
if [[ "$LOAD" == true ]]; then
echo "📥 Cargando imagen en producción vps-2.rodax-software.com..."
[[ "$MODE" == "web" || "$MODE" == "all" ]] && scp -r -P 49152 ${OUT_WEB_DIR} rodax@vps-2.rodax-software.com:/opt/factuges/${COMPANY}/
[[ "$MODE" == "api" || "$MODE" == "all" ]] && scp -r -P 49152 ${OUT_API_DIR} rodax@vps-2.rodax-software.com:/opt/factuges/${COMPANY}/
[[ "$MODE" == "api" || "$MODE" == "all" ]] && RESULT=$(ssh -p 49152 rodax@vps-2.rodax-software.com "docker load -i ${TAR_FILE_LATEST}")
[[ "$MODE" == "api" || "$MODE" == "all" ]] && echo $RESULT
#docker load -i "${TAR_FILE_V}"
echo "✅ Imagen cargada en producción"
fi
# =====================================================
# 3⃣ Resumen
# =====================================================