Compare commits
No commits in common. "a57dfe2a02ec08ac3d7a24f0c27e6344d333d145" and "11402bccc1fa8d05e165695aa832671a466c583d" have entirely different histories.
a57dfe2a02
...
11402bccc1
@ -10,12 +10,6 @@
|
|||||||
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Roboto:ital,wdth,wght@0,75..100,100..900;1,75..100,100..900&display=swap"
|
|
||||||
rel="stylesheet">
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Domine:wght@400..700&family=Roboto:ital,wdth,wght@0,75..100,100..900;1,75..100,100..900&display=swap"
|
|
||||||
rel="stylesheet">
|
|
||||||
|
|
||||||
<title>FactuGES 2025</title>
|
<title>FactuGES 2025</title>
|
||||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
|
|||||||
@ -42,6 +42,7 @@ export const createAxiosInstance = ({
|
|||||||
}: AxiosFactoryConfig): AxiosInstance => {
|
}: AxiosFactoryConfig): AxiosInstance => {
|
||||||
const instance = axios.create(defaultAxiosRequestConfig);
|
const instance = axios.create(defaultAxiosRequestConfig);
|
||||||
instance.defaults.baseURL = baseURL;
|
instance.defaults.baseURL = baseURL;
|
||||||
|
setupInterceptors(instance, getAccessToken, onAuthError);
|
||||||
|
|
||||||
return setupInterceptors(instance, getAccessToken, onAuthError);
|
return instance;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,16 +3,16 @@ import { AxiosError, AxiosInstance, InternalAxiosRequestConfig } from "axios";
|
|||||||
/**
|
/**
|
||||||
* Configura interceptores para una instancia de Axios.
|
* Configura interceptores para una instancia de Axios.
|
||||||
*
|
*
|
||||||
* @param axiosInstance - Instancia de Axios que será modificada.
|
* @param instance - Instancia de Axios que será modificada.
|
||||||
* @param getAccessToken - Función que devuelve el token JWT actual.
|
* @param getAccessToken - Función que devuelve el token JWT actual.
|
||||||
* @param onAuthError - Función opcional que se ejecuta ante errores de autenticación (status 401).
|
* @param onAuthError - Función opcional que se ejecuta ante errores de autenticación (status 401).
|
||||||
*/
|
*/
|
||||||
export const setupInterceptors = (
|
export const setupInterceptors = (
|
||||||
axiosInstance: AxiosInstance,
|
instance: AxiosInstance,
|
||||||
getAccessToken: () => string | null,
|
getAccessToken: () => string | null,
|
||||||
onAuthError?: () => void
|
onAuthError?: () => void
|
||||||
) => {
|
): void => {
|
||||||
axiosInstance.interceptors.request.use(
|
instance.interceptors.request.use(
|
||||||
(config: InternalAxiosRequestConfig) => {
|
(config: InternalAxiosRequestConfig) => {
|
||||||
const token = getAccessToken();
|
const token = getAccessToken();
|
||||||
if (token && config.headers) {
|
if (token && config.headers) {
|
||||||
@ -25,48 +25,13 @@ export const setupInterceptors = (
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
axiosInstance.interceptors.response.use(
|
instance.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
(error: AxiosError) => {
|
(error: AxiosError) => {
|
||||||
// 🔴 Transformamos SIEMPRE el error antes de propagarlo
|
if (error.response?.status === 401 && onAuthError) {
|
||||||
const normalized = normalizeAxiosError(error);
|
|
||||||
return Promise.reject(normalized);
|
|
||||||
|
|
||||||
/*if (error.response?.status === 401 && onAuthError) {
|
|
||||||
onAuthError();
|
onAuthError();
|
||||||
} */
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return axiosInstance;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Normaliza errores de Axios en un objeto estándar de Error
|
|
||||||
* con propiedades extra opcionales (status, raw).
|
|
||||||
*/
|
|
||||||
function normalizeAxiosError(error: AxiosError): Error {
|
|
||||||
let normalizedError: Error;
|
|
||||||
|
|
||||||
if (error.response?.data) {
|
|
||||||
const data: any = error.response.data;
|
|
||||||
|
|
||||||
// Intentamos localizar mensaje en campos comunes
|
|
||||||
const msg =
|
|
||||||
data.message ??
|
|
||||||
(Array.isArray(data.errors) && data.errors[0]?.msg) ??
|
|
||||||
error.message ??
|
|
||||||
"Unknown server error";
|
|
||||||
|
|
||||||
normalizedError = new Error(msg);
|
|
||||||
|
|
||||||
// Añadimos metadatos útiles
|
|
||||||
(normalizedError as any).status = error.response.status;
|
|
||||||
(normalizedError as any).raw = data;
|
|
||||||
} else {
|
|
||||||
normalizedError = new Error(error.message || "Unknown network error");
|
|
||||||
(normalizedError as any).status = error.response?.status ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalizedError;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -3,5 +3,5 @@ import { UtcDate } from "@repo/rdx-ddd";
|
|||||||
export function formatDateDTO(dateString: string) {
|
export function formatDateDTO(dateString: string) {
|
||||||
const result = UtcDate.createFromISO(dateString).data;
|
const result = UtcDate.createFromISO(dateString).data;
|
||||||
|
|
||||||
return result.toEuropeanString();
|
return result.toDateString();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,9 @@
|
|||||||
import { MoneyDTO } from "@erp/core";
|
import { MoneyDTO } from "@erp/core";
|
||||||
import { MoneyValue } from "@repo/rdx-ddd";
|
import { MoneyValue } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
export type FormatMoneyOptions = {
|
export function formatMoneyDTO(amount: MoneyDTO, locale: string) {
|
||||||
locale: string;
|
if (amount.value === "") {
|
||||||
hideZeros?: boolean;
|
return "";
|
||||||
newScale?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function formatMoneyDTO(
|
|
||||||
amount: MoneyDTO,
|
|
||||||
{ locale, hideZeros = false, newScale = 2 }: FormatMoneyOptions
|
|
||||||
) {
|
|
||||||
if (hideZeros && (amount.value === "0" || amount.value === "")) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const money = MoneyValue.create({
|
const money = MoneyValue.create({
|
||||||
@ -21,5 +12,5 @@ export function formatMoneyDTO(
|
|||||||
scale: Number(amount.scale),
|
scale: Number(amount.scale),
|
||||||
}).data;
|
}).data;
|
||||||
|
|
||||||
return money.convertScale(newScale).format(locale);
|
return money.format(locale);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,10 +2,6 @@ import { PercentageDTO } from "@erp/core";
|
|||||||
import { Percentage } from "@repo/rdx-ddd";
|
import { Percentage } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
export function formatPercentageDTO(Percentage_value: PercentageDTO, locale: string) {
|
export function formatPercentageDTO(Percentage_value: PercentageDTO, locale: string) {
|
||||||
if (Percentage_value.value === "0" || Percentage_value.value === "") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = Percentage.create({
|
const value = Percentage.create({
|
||||||
value: Number(Percentage_value.value),
|
value: Number(Percentage_value.value),
|
||||||
scale: Number(Percentage_value.scale),
|
scale: Number(Percentage_value.scale),
|
||||||
|
|||||||
@ -2,8 +2,8 @@ import { QuantityDTO } from "@erp/core";
|
|||||||
import { Quantity } from "@repo/rdx-ddd";
|
import { Quantity } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
export function formatQuantityDTO(quantity_value: QuantityDTO) {
|
export function formatQuantityDTO(quantity_value: QuantityDTO) {
|
||||||
if (quantity_value.value === "0" || quantity_value.value === "") {
|
if (quantity_value.value === "") {
|
||||||
return null;
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = Quantity.create({
|
const value = Quantity.create({
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { IPresenterOutputParams, Presenter } from "@erp/core/api";
|
import { IPresenterOutputParams, Presenter } from "@erp/core/api";
|
||||||
import { GetCustomerInvoiceByIdResponseDTO } from "@erp/customer-invoices/common";
|
import { GetCustomerInvoiceByIdResponseDTO } from "@erp/customer-invoices/common";
|
||||||
import { ArrayElement } from "@repo/rdx-utils";
|
import { ArrayElement } from "@repo/rdx-utils";
|
||||||
import { FormatMoneyOptions, formatMoneyDTO, formatQuantityDTO } from "../../helpers";
|
import { formatMoneyDTO, formatQuantityDTO } from "../../helpers";
|
||||||
|
|
||||||
type CustomerInvoiceItemsDTO = GetCustomerInvoiceByIdResponseDTO["items"];
|
type CustomerInvoiceItemsDTO = GetCustomerInvoiceByIdResponseDTO["items"];
|
||||||
type CustomerInvoiceItemDTO = ArrayElement<CustomerInvoiceItemsDTO>;
|
type CustomerInvoiceItemDTO = ArrayElement<CustomerInvoiceItemsDTO>;
|
||||||
@ -13,23 +13,18 @@ export class CustomerInvoiceItemsReportPersenter extends Presenter<
|
|||||||
private _locale!: string;
|
private _locale!: string;
|
||||||
|
|
||||||
private _mapItem(invoiceItem: CustomerInvoiceItemDTO, index: number) {
|
private _mapItem(invoiceItem: CustomerInvoiceItemDTO, index: number) {
|
||||||
const moneyOptions: FormatMoneyOptions = {
|
|
||||||
locale: this._locale,
|
|
||||||
hideZeros: true,
|
|
||||||
newScale: 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...invoiceItem,
|
...invoiceItem,
|
||||||
|
|
||||||
quantity: formatQuantityDTO(invoiceItem.quantity),
|
quantity: formatQuantityDTO(invoiceItem.quantity),
|
||||||
unit_amount: formatMoneyDTO(invoiceItem.unit_amount, moneyOptions),
|
unit_amount: formatMoneyDTO(invoiceItem.unit_amount, this._locale),
|
||||||
subtotal_amount: formatMoneyDTO(invoiceItem.subtotal_amount, moneyOptions),
|
|
||||||
|
subtotal_amount: formatMoneyDTO(invoiceItem.subtotal_amount, this._locale),
|
||||||
// discount_percetage: formatPercentageDTO(invoiceItem.discount_percentage, this._locale),
|
// discount_percetage: formatPercentageDTO(invoiceItem.discount_percentage, this._locale),
|
||||||
discount_amount: formatMoneyDTO(invoiceItem.discount_amount, moneyOptions),
|
discount_amount: formatMoneyDTO(invoiceItem.discount_amount, this._locale),
|
||||||
taxable_amount: formatMoneyDTO(invoiceItem.taxable_amount, moneyOptions),
|
taxable_amount: formatMoneyDTO(invoiceItem.taxable_amount, this._locale),
|
||||||
taxes_amount: formatMoneyDTO(invoiceItem.taxes_amount, moneyOptions),
|
taxes_amount: formatMoneyDTO(invoiceItem.taxes_amount, this._locale),
|
||||||
total_amount: formatMoneyDTO(invoiceItem.total_amount, moneyOptions),
|
total_amount: formatMoneyDTO(invoiceItem.total_amount, this._locale),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,6 @@
|
|||||||
import { Presenter } from "@erp/core/api";
|
import { Presenter } from "@erp/core/api";
|
||||||
import { GetCustomerInvoiceByIdResponseDTO } from "../../../../common/dto";
|
import { GetCustomerInvoiceByIdResponseDTO } from "../../../../common/dto";
|
||||||
import {
|
import { formatDateDTO, formatMoneyDTO, formatPercentageDTO } from "../../helpers";
|
||||||
FormatMoneyOptions,
|
|
||||||
formatDateDTO,
|
|
||||||
formatMoneyDTO,
|
|
||||||
formatPercentageDTO,
|
|
||||||
} from "../../helpers";
|
|
||||||
|
|
||||||
export class CustomerInvoiceReportPresenter extends Presenter<
|
export class CustomerInvoiceReportPresenter extends Presenter<
|
||||||
GetCustomerInvoiceByIdResponseDTO,
|
GetCustomerInvoiceByIdResponseDTO,
|
||||||
@ -23,23 +18,17 @@ export class CustomerInvoiceReportPresenter extends Presenter<
|
|||||||
locale,
|
locale,
|
||||||
});
|
});
|
||||||
|
|
||||||
const moneyOptions: FormatMoneyOptions = {
|
|
||||||
locale,
|
|
||||||
hideZeros: true,
|
|
||||||
newScale: 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...invoiceDTO,
|
...invoiceDTO,
|
||||||
items: itemsDTO,
|
items: itemsDTO,
|
||||||
|
|
||||||
invoice_date: formatDateDTO(invoiceDTO.invoice_date),
|
invoice_date: formatDateDTO(invoiceDTO.invoice_date),
|
||||||
subtotal_amount: formatMoneyDTO(invoiceDTO.subtotal_amount, moneyOptions),
|
subtotal_amount: formatMoneyDTO(invoiceDTO.subtotal_amount, locale),
|
||||||
discount_percentage: formatPercentageDTO(invoiceDTO.discount_percentage, locale),
|
discount_percetage: formatPercentageDTO(invoiceDTO.discount_percentage, locale),
|
||||||
discount_amount: formatMoneyDTO(invoiceDTO.discount_amount, moneyOptions),
|
discount_amount: formatMoneyDTO(invoiceDTO.discount_amount, locale),
|
||||||
taxable_amount: formatMoneyDTO(invoiceDTO.taxable_amount, moneyOptions),
|
taxable_amount: formatMoneyDTO(invoiceDTO.taxable_amount, locale),
|
||||||
taxes_amount: formatMoneyDTO(invoiceDTO.taxes_amount, moneyOptions),
|
taxes_amount: formatMoneyDTO(invoiceDTO.taxes_amount, locale),
|
||||||
total_amount: formatMoneyDTO(invoiceDTO.total_amount, moneyOptions),
|
total_amount: formatMoneyDTO(invoiceDTO.total_amount, locale),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,7 +36,6 @@ export class CustomerInvoiceReportPDFPresenter extends Presenter<
|
|||||||
await page.setContent(htmlData, { waitUntil: "networkidle2" });
|
await page.setContent(htmlData, { waitUntil: "networkidle2" });
|
||||||
|
|
||||||
await navigationPromise;
|
await navigationPromise;
|
||||||
|
|
||||||
const reportPDF = await report.pdfPage(page, {
|
const reportPDF = await report.pdfPage(page, {
|
||||||
format: "A4",
|
format: "A4",
|
||||||
margin: {
|
margin: {
|
||||||
@ -49,10 +48,6 @@ export class CustomerInvoiceReportPDFPresenter extends Presenter<
|
|||||||
preferCSSPageSize: true,
|
preferCSSPageSize: true,
|
||||||
omitBackground: false,
|
omitBackground: false,
|
||||||
printBackground: true,
|
printBackground: true,
|
||||||
displayHeaderFooter: true,
|
|
||||||
headerTemplate: "<div />",
|
|
||||||
footerTemplate:
|
|
||||||
'<div style="text-align: center;width: 297mm;font-size: 10px;">Página <span style="margin-right: 1cm"><span class="pageNumber"></span> de <span class="totalPages"></span></span></div>',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await browser.close();
|
await browser.close();
|
||||||
|
|||||||
@ -0,0 +1,254 @@
|
|||||||
|
<html lang="{{lang_code}}">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css"
|
||||||
|
referrerpolicy="no-referrer" />
|
||||||
|
<title>Factura #{{id}}</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--gray: #6b7280;
|
||||||
|
--light: #f3f4f6;
|
||||||
|
--border: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@page {
|
||||||
|
margin: 24mm 18mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
color: #000;
|
||||||
|
font-size: 10.5pt;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
font-size: 9pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xsmall {
|
||||||
|
font-size: 8pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
color: var(--gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: .375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
background: var(--light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th,
|
||||||
|
.table td {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.totals td {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.totals tr td:last-child {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header h1 {
|
||||||
|
letter-spacing: .03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#meta td {
|
||||||
|
padding: .35rem .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
thead {
|
||||||
|
display: table-header-group;
|
||||||
|
}
|
||||||
|
|
||||||
|
tfoot {
|
||||||
|
display: table-footer-group;
|
||||||
|
}
|
||||||
|
|
||||||
|
a[href]:after {
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<header id="header" class="mb-4">
|
||||||
|
<section class="flex items-start justify-between">
|
||||||
|
<!-- Bloque empresa -->
|
||||||
|
<aside class="pr-4">
|
||||||
|
<h1 class="text-2xl font-semibold leading-tight">{{dealer.name}}</h1>
|
||||||
|
{{#if dealer.logo}}
|
||||||
|
<img id="dealer-logo" src="{{dealer.logo}}" alt="Logo" class="mt-1 h-10 object-contain" />
|
||||||
|
{{/if}}
|
||||||
|
<address id="from" class="not-italic whitespace-pre-line small mt-2">
|
||||||
|
{{dealer.contact_information}}
|
||||||
|
</address>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Bloque meta factura -->
|
||||||
|
<aside class="box p-3 min-w-[260px]">
|
||||||
|
<table id="meta" class="w-full small">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="font-semibold">Factura nº:</td>
|
||||||
|
<td class="text-right">{{reference}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="font-semibold">Fecha:</td>
|
||||||
|
<td class="text-right">{{date}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="font-semibold">Página:</td>
|
||||||
|
<td class="text-right"><span class="pageNumber"></span> / <span class="totalPages"></span></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</aside>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Cliente -->
|
||||||
|
<section class="grid grid-cols-2 gap-4 mt-4 pb-3 border-b border-gray-200">
|
||||||
|
<aside>
|
||||||
|
<h3 class="font-semibold mb-1">Cliente</h3>
|
||||||
|
<address id="to" class="not-italic whitespace-pre-line">{{customer_information}}</address>
|
||||||
|
</aside>
|
||||||
|
<aside class="small">
|
||||||
|
{{#if customer_reference}}
|
||||||
|
<p><span class="font-semibold">Referencia cliente:</span> {{customer_reference}}</p>
|
||||||
|
{{/if}}
|
||||||
|
</aside>
|
||||||
|
</section>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="main">
|
||||||
|
<!-- Detalle líneas -->
|
||||||
|
<section id="details" class="mt-3">
|
||||||
|
<table class="table w-full border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-left">
|
||||||
|
<th class="px-2 py-2">Concepto</th>
|
||||||
|
<th class="px-2 py-2 text-right w-24">Cantidad</th>
|
||||||
|
<th class="px-2 py-2 text-right w-32">Precio unidad</th>
|
||||||
|
{{#if any_item_has_discount}}
|
||||||
|
<th class="px-2 py-2 text-right w-24">Dto (%)</th>
|
||||||
|
{{/if}}
|
||||||
|
<th class="px-2 py-2 text-right w-36">Importe total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{#each items}}
|
||||||
|
<tr class="align-top">
|
||||||
|
<td class="px-2 py-2">
|
||||||
|
<div class="font-medium">{{description}}</div>
|
||||||
|
{{#if note}}<div class="small muted whitespace-pre-line">{{note}}</div>{{/if}}
|
||||||
|
</td>
|
||||||
|
<td class="px-2 py-2 text-right">{{quantity}}</td>
|
||||||
|
<td class="px-2 py-2 text-right">{{unit_price}}</td>
|
||||||
|
{{#if ../any_item_has_discount}}
|
||||||
|
<td class="px-2 py-2 text-right">{{discount}}</td>
|
||||||
|
{{/if}}
|
||||||
|
<td class="px-2 py-2 text-right">{{total_price}}</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="px-2 py-1 xsmall muted">* Precios en {{currency}}.</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Resumen / totales -->
|
||||||
|
<section id="resume" class="grid grid-cols-2 gap-6 mt-6">
|
||||||
|
<!-- Notas y forma de pago -->
|
||||||
|
<aside>
|
||||||
|
{{#if payment_method}}
|
||||||
|
<p class="small"><span class="font-semibold">Forma de pago:</span> {{payment_method}}</p>
|
||||||
|
{{/if}}
|
||||||
|
{{#if notes}}
|
||||||
|
<div class="mt-2 small"><span class="font-semibold">Notas:</span> {{notes}}</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{!-- Bloque especial Domiciliación bancaria --}}
|
||||||
|
{{#if payment_is_direct_debit}}
|
||||||
|
<div class="mt-4 box p-3 small">
|
||||||
|
<div class="font-semibold mb-1 uppercase tracking-wide">DOMICILIACIÓN BANCARIA</div>
|
||||||
|
<div class="whitespace-pre-line">{{direct_debit_text}}</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Totals box -->
|
||||||
|
<aside class="justify-self-end w-full max-w-md">
|
||||||
|
<table class="w-full totals">
|
||||||
|
<tbody>
|
||||||
|
{{#if subtotal_price}}
|
||||||
|
<tr>
|
||||||
|
<td class="py-1">Importe neto</td>
|
||||||
|
<td class="py-1 text-right">{{subtotal_price}}</td>
|
||||||
|
</tr>
|
||||||
|
{{/if}}
|
||||||
|
{{#if discount_price}}
|
||||||
|
<tr>
|
||||||
|
<td class="py-1">% Descuento ({{discount.amount}})</td>
|
||||||
|
<td class="py-1 text-right">-{{discount_price}}</td>
|
||||||
|
</tr>
|
||||||
|
{{/if}}
|
||||||
|
<tr class="border-t border-gray-200">
|
||||||
|
<td class="py-1 font-medium">Base imponible</td>
|
||||||
|
<td class="py-1 text-right font-medium">{{before_tax_price}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="py-1">IVA {{tax}}</td>
|
||||||
|
<td class="py-1 text-right">{{tax_price}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-t-2 border-gray-300">
|
||||||
|
<td class="py-2 text-lg font-semibold">Total factura</td>
|
||||||
|
<td class="py-2 text-right text-lg font-semibold">{{total_price}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</aside>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Términos legales -->
|
||||||
|
<section id="legal_terms" class="mt-6">
|
||||||
|
<p class="xsmall muted whitespace-pre-line">{{quote.default_legal_terms}}</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer id="footer" class="mt-8 pt-3 border-t border-gray-200">
|
||||||
|
<div class="grid grid-cols-2 gap-4 small">
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">{{dealer.name}}</span>
|
||||||
|
<div class="whitespace-pre-line">{{dealer.footer_information}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
{{#if dealer.website}}<div><a href="{{dealer.website}}">{{dealer.website}}</a></div>{{/if}}
|
||||||
|
{{#if dealer.email}}<div>{{dealer.email}}</div>{{/if}}
|
||||||
|
{{#if dealer.phone}}<div>{{dealer.phone}}</div>{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
{{!-- Helpers opcionales esperados por la plantilla --}}
|
||||||
|
{{!--
|
||||||
|
any_item_has_discount: boolean precomputado en tu código
|
||||||
|
payment_is_direct_debit: boolean si forma de pago es domiciliación
|
||||||
|
direct_debit_text: texto para el bloque de domiciliación bancaria
|
||||||
|
currency: ISO o símbolo (EUR, €, etc.)
|
||||||
|
--}}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -55,14 +55,14 @@
|
|||||||
|
|
||||||
table th,
|
table th,
|
||||||
table td {
|
table td {
|
||||||
border: 0px solid;
|
border: 0px solid #ccc;
|
||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
table th {
|
table th {
|
||||||
margin-bottom: 10px;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.totals {
|
.totals {
|
||||||
@ -81,7 +81,7 @@
|
|||||||
|
|
||||||
footer {
|
footer {
|
||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight {
|
.highlight {
|
||||||
@ -119,6 +119,7 @@
|
|||||||
<div class="p-1 ">
|
<div class="p-1 ">
|
||||||
<p>Factura nº:<strong> {{invoice_number}}</strong></p>
|
<p>Factura nº:<strong> {{invoice_number}}</strong></p>
|
||||||
<p><span>Fecha:<strong> {{invoice_date}}</strong></p>
|
<p><span>Fecha:<strong> {{invoice_date}}</strong></p>
|
||||||
|
<p><span>Página:</span>1 / 1</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-1 ml-9">
|
<div class="p-1 ml-9">
|
||||||
<h2 class="font-semibold uppercase mb-1">{{recipient.name}}</h2>
|
<h2 class="font-semibold uppercase mb-1">{{recipient.name}}</h2>
|
||||||
@ -173,10 +174,9 @@
|
|||||||
{{#each items}}
|
{{#each items}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{description}}</td>
|
<td>{{description}}</td>
|
||||||
<td class="text-right">{{#if quantity}}{{quantity}}{{else}} {{/if}}</td>
|
<td class="text-right">{{quantity}}</td>
|
||||||
<td class="text-right">{{#if unit_amount}}{{unit_amount}}{{else}} {{/if}}</td>
|
<td class="text-right">{{unit_amount}}</td>
|
||||||
<td class="text-right">{{#if total_amount}}{{total_amount}}{{else}} {{/if}}</td>
|
<td class="text-right">{{total_amount}}</td>
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -197,14 +197,14 @@
|
|||||||
<div class="relative pt-10 grow">
|
<div class="relative pt-10 grow">
|
||||||
<table class="table-header min-w-full bg-transparent">
|
<table class="table-header min-w-full bg-transparent">
|
||||||
<tbody>
|
<tbody>
|
||||||
{{#if discount_percentage}}
|
{{#if percentage}}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-4 text-right">Importe neto</td>
|
<td class="px-4 text-right">Importe neto</td>
|
||||||
<td class="w-5"> </td>
|
<td class="w-5"> </td>
|
||||||
<td class="px-4 text-right">{{subtotal_amount}}</td>
|
<td class="px-4 text-right">{{subtotal_amount}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-4 text-right">Descuento {{discount_percentage}}</td>
|
<td class="px-4 text-right">Descuento 0%</td>
|
||||||
<td class="w-5"> </td>
|
<td class="w-5"> </td>
|
||||||
<td class="px-4 text-right">{{discount_amount.value}}</td>
|
<td class="px-4 text-right">{{discount_amount.value}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -237,8 +237,7 @@
|
|||||||
|
|
||||||
<footer id="footer" class="mt-4">
|
<footer id="footer" class="mt-4">
|
||||||
<aside>
|
<aside>
|
||||||
<p class="text-center">Insc. en el Reg. Merc. de Madrid, Tomo 20.073, Libro 0, Folio 141, Sección 8, Hoja M-354212
|
<p>Insc. en el Reg. Merc. de Madrid, Tomo 20.073, Libro 0, Folio 141, Sección 8, Hoja M-354212 | CIF: B83999441 -
|
||||||
| CIF: B83999441 -
|
|
||||||
Rodax Software S.L.</p>
|
Rodax Software S.L.</p>
|
||||||
</aside>
|
</aside>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export interface CustomerInvoiceProps {
|
|||||||
languageCode: LanguageCode;
|
languageCode: LanguageCode;
|
||||||
currencyCode: CurrencyCode;
|
currencyCode: CurrencyCode;
|
||||||
|
|
||||||
taxes: InvoiceTaxes;
|
taxes: Maybe<InvoiceTaxes>;
|
||||||
items: CustomerInvoiceItems;
|
items: CustomerInvoiceItems;
|
||||||
|
|
||||||
discountPercentage: Percentage;
|
discountPercentage: Percentage;
|
||||||
@ -71,7 +71,10 @@ export class CustomerInvoice
|
|||||||
currencyCode: props.currencyCode,
|
currencyCode: props.currencyCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
this._taxes = props.taxes;
|
this._taxes = props.taxes.match(
|
||||||
|
(taxes) => taxes,
|
||||||
|
() => InvoiceTaxes.create({})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static create(props: CustomerInvoiceProps, id?: UniqueID): Result<CustomerInvoice, Error> {
|
static create(props: CustomerInvoiceProps, id?: UniqueID): Result<CustomerInvoice, Error> {
|
||||||
|
|||||||
@ -115,5 +115,5 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
app.use(`${baseRoutePath}/proforma-invoices`, router);
|
app.use(`${baseRoutePath}/customer-invoices`, router);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
extractOrPushError,
|
extractOrPushError,
|
||||||
maybeFromNullableVO,
|
maybeFromNullableVO,
|
||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Maybe, Result } from "@repo/rdx-utils";
|
||||||
import {
|
import {
|
||||||
CustomerInvoice,
|
CustomerInvoice,
|
||||||
CustomerInvoiceItems,
|
CustomerInvoiceItems,
|
||||||
@ -243,7 +243,7 @@ export class CustomerInvoiceDomainMapper
|
|||||||
|
|
||||||
discountPercentage: attributes.discountPercentage!,
|
discountPercentage: attributes.discountPercentage!,
|
||||||
|
|
||||||
taxes: taxes,
|
taxes: Maybe.some(taxes),
|
||||||
items,
|
items,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,22 +1,22 @@
|
|||||||
import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
|
import { ITransactionManager } from "@erp/core/api";
|
||||||
import { UniqueID } from "@repo/rdx-ddd";
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import { UpdateCustomerByIdRequestDTO } from "../../../../common/dto";
|
import { UpdateCustomerRequestDTO } from "../../../common";
|
||||||
import { CustomerPatchProps, CustomerService } from "../../../domain";
|
import { CustomerPatchProps, CustomerService } from "../../domain";
|
||||||
import { CustomerFullPresenter } from "../../presenters";
|
import { UpdateCustomerAssembler } from "./assembler";
|
||||||
import { mapDTOToUpdateCustomerPatchProps } from "./map-dto-to-update-customer-props";
|
import { mapDTOToUpdateCustomerPatchProps } from "./map-dto-to-update-customer-props";
|
||||||
|
|
||||||
type UpdateCustomerUseCaseInput = {
|
type UpdateCustomerUseCaseInput = {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
customer_id: string;
|
customer_id: string;
|
||||||
dto: UpdateCustomerByIdRequestDTO;
|
dto: UpdateCustomerRequestDTO;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class UpdateCustomerUseCase {
|
export class UpdateCustomerUseCase {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly service: CustomerService,
|
private readonly service: CustomerService,
|
||||||
private readonly transactionManager: ITransactionManager,
|
private readonly transactionManager: ITransactionManager,
|
||||||
private readonly presenterRegistry: IPresenterRegistry
|
private readonly assembler: UpdateCustomerAssembler
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public execute(params: UpdateCustomerUseCaseInput) {
|
public execute(params: UpdateCustomerUseCaseInput) {
|
||||||
@ -28,10 +28,6 @@ export class UpdateCustomerUseCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const customerId = idOrError.data;
|
const customerId = idOrError.data;
|
||||||
const presenter = this.presenterRegistry.getPresenter({
|
|
||||||
resource: "customer",
|
|
||||||
projection: "FULL",
|
|
||||||
}) as CustomerFullPresenter;
|
|
||||||
|
|
||||||
// Mapear DTO → props de dominio
|
// Mapear DTO → props de dominio
|
||||||
const patchPropsResult = mapDTOToUpdateCustomerPatchProps(dto);
|
const patchPropsResult = mapDTOToUpdateCustomerPatchProps(dto);
|
||||||
@ -54,10 +50,10 @@ export class UpdateCustomerUseCase {
|
|||||||
return Result.fail(updatedCustomer.error);
|
return Result.fail(updatedCustomer.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const customerOrError = await this.service.saveCustomer(updatedCustomer.data, transaction);
|
const savedCustomer = await this.service.saveCustomer(updatedCustomer.data, transaction);
|
||||||
const customer = customerOrError.data;
|
|
||||||
const dto = presenter.toOutput(customer);
|
const getDTO = this.assembler.toDTO(savedCustomer.data);
|
||||||
return Result.ok(dto);
|
return Result.ok(getDTO);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
return Result.fail(error as Error);
|
return Result.fail(error as Error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import {
|
|||||||
CustomerFullPresenter,
|
CustomerFullPresenter,
|
||||||
ListCustomersPresenter,
|
ListCustomersPresenter,
|
||||||
ListCustomersUseCase,
|
ListCustomersUseCase,
|
||||||
UpdateCustomerUseCase,
|
|
||||||
} from "../application";
|
} from "../application";
|
||||||
import { GetCustomerUseCase } from "../application/use-cases/get-customer.use-case";
|
import { GetCustomerUseCase } from "../application/use-cases/get-customer.use-case";
|
||||||
import { CustomerService } from "../domain";
|
import { CustomerService } from "../domain";
|
||||||
@ -26,8 +25,8 @@ export type CustomerDeps = {
|
|||||||
list: () => ListCustomersUseCase;
|
list: () => ListCustomersUseCase;
|
||||||
get: () => GetCustomerUseCase;
|
get: () => GetCustomerUseCase;
|
||||||
create: () => CreateCustomerUseCase;
|
create: () => CreateCustomerUseCase;
|
||||||
update: () => UpdateCustomerUseCase;
|
/*update: () => UpdateCustomerUseCase;
|
||||||
//delete: () => DeleteCustomerUseCase;
|
delete: () => DeleteCustomerUseCase;*/
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -68,8 +67,8 @@ export function buildCustomerDependencies(params: ModuleParams): CustomerDeps {
|
|||||||
list: () => new ListCustomersUseCase(service, transactionManager, presenterRegistry),
|
list: () => new ListCustomersUseCase(service, transactionManager, presenterRegistry),
|
||||||
get: () => new GetCustomerUseCase(service, transactionManager, presenterRegistry),
|
get: () => new GetCustomerUseCase(service, transactionManager, presenterRegistry),
|
||||||
create: () => new CreateCustomerUseCase(service, transactionManager, presenterRegistry),
|
create: () => new CreateCustomerUseCase(service, transactionManager, presenterRegistry),
|
||||||
update: () => new UpdateCustomerUseCase(service, transactionManager, presenterRegistry),
|
/*update: () => new UpdateCustomerUseCase(_service!, transactionManager!, presenterRegistry!),
|
||||||
//delete: () => new DeleteCustomerUseCase(_service!, transactionManager!),
|
delete: () => new DeleteCustomerUseCase(_service!, transactionManager!),*/
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,4 +2,4 @@ export * from "./create-customer.controller";
|
|||||||
export * from "./delete-customer.controller";
|
export * from "./delete-customer.controller";
|
||||||
export * from "./get-customer.controller";
|
export * from "./get-customer.controller";
|
||||||
export * from "./list-customers.controller";
|
export * from "./list-customers.controller";
|
||||||
export * from "./update-customer.controller";
|
///export * from "./update-customer.controller";
|
||||||
|
|||||||
@ -6,15 +6,12 @@ import {
|
|||||||
CreateCustomerRequestSchema,
|
CreateCustomerRequestSchema,
|
||||||
CustomerListRequestSchema,
|
CustomerListRequestSchema,
|
||||||
GetCustomerByIdRequestSchema,
|
GetCustomerByIdRequestSchema,
|
||||||
UpdateCustomerByIdParamsRequestSchema,
|
|
||||||
UpdateCustomerByIdRequestSchema,
|
|
||||||
} from "../../../common/dto";
|
} from "../../../common/dto";
|
||||||
import { buildCustomerDependencies } from "../dependencies";
|
import { buildCustomerDependencies } from "../dependencies";
|
||||||
import {
|
import {
|
||||||
CreateCustomerController,
|
CreateCustomerController,
|
||||||
GetCustomerController,
|
GetCustomerController,
|
||||||
ListCustomersController,
|
ListCustomersController,
|
||||||
UpdateCustomerController,
|
|
||||||
} from "./controllers";
|
} from "./controllers";
|
||||||
|
|
||||||
export const customersRouter = (params: ModuleParams) => {
|
export const customersRouter = (params: ModuleParams) => {
|
||||||
@ -84,10 +81,9 @@ export const customersRouter = (params: ModuleParams) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
router.put(
|
/*router.put(
|
||||||
"/:customer_id",
|
"/:customer_id",
|
||||||
//checkTabContext,
|
//checkTabContext,
|
||||||
|
|
||||||
validateRequest(UpdateCustomerByIdParamsRequestSchema, "params"),
|
validateRequest(UpdateCustomerByIdParamsRequestSchema, "params"),
|
||||||
validateRequest(UpdateCustomerByIdRequestSchema, "body"),
|
validateRequest(UpdateCustomerByIdRequestSchema, "body"),
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
@ -97,7 +93,7 @@ export const customersRouter = (params: ModuleParams) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/*router.delete(
|
router.delete(
|
||||||
"/:customer_id",
|
"/:customer_id",
|
||||||
//checkTabContext,
|
//checkTabContext,
|
||||||
|
|
||||||
@ -107,7 +103,7 @@ export const customersRouter = (params: ModuleParams) => {
|
|||||||
const controller = new DeleteCustomerController(useCase);
|
const controller = new DeleteCustomerController(useCase);
|
||||||
return controller.execute(req, res, next);
|
return controller.execute(req, res, next);
|
||||||
}
|
}
|
||||||
);*/
|
); */
|
||||||
|
|
||||||
app.use(`${baseRoutePath}/customers`, router);
|
app.use(`${baseRoutePath}/customers`, router);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -31,4 +31,4 @@ export const UpdateCustomerByIdRequestSchema = z.object({
|
|||||||
currency_code: z.string().optional(),
|
currency_code: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type UpdateCustomerByIdRequestDTO = Partial<z.infer<typeof UpdateCustomerByIdRequestSchema>>;
|
export type UpdateCustomerByIdRequestDTO = z.infer<typeof UpdateCustomerByIdRequestSchema>;
|
||||||
|
|||||||
@ -21,11 +21,6 @@
|
|||||||
"title": "New customer",
|
"title": "New customer",
|
||||||
"description": "Create a new customer",
|
"description": "Create a new customer",
|
||||||
"back_to_list": "Back to the list"
|
"back_to_list": "Back to the list"
|
||||||
},
|
|
||||||
"update": {
|
|
||||||
"title": "Update customer",
|
|
||||||
"description": "Update a customer",
|
|
||||||
"back_to_list": "Back to the list"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"form_fields": {
|
"form_fields": {
|
||||||
@ -122,10 +117,10 @@
|
|||||||
"placeholder": "Enter website URL",
|
"placeholder": "Enter website URL",
|
||||||
"description": "The website of the customer"
|
"description": "The website of the customer"
|
||||||
},
|
},
|
||||||
"default_taxes": {
|
"default_tax": {
|
||||||
"label": "Default taxes",
|
"label": "Default tax",
|
||||||
"placeholder": "Select default taxes",
|
"placeholder": "Select default tax",
|
||||||
"description": "The default tax rates for the customer"
|
"description": "The default tax rate for the customer"
|
||||||
},
|
},
|
||||||
"language_code": {
|
"language_code": {
|
||||||
"label": "Language",
|
"label": "Language",
|
||||||
@ -156,8 +151,8 @@
|
|||||||
"title": "Contact information",
|
"title": "Contact information",
|
||||||
"description": "Customer contact details"
|
"description": "Customer contact details"
|
||||||
},
|
},
|
||||||
"preferences": {
|
"additional_config": {
|
||||||
"title": "Preferences",
|
"title": "Additional settings",
|
||||||
"description": "Additional customer configurations"
|
"description": "Additional customer configurations"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -21,11 +21,6 @@
|
|||||||
"title": "Nuevo cliente",
|
"title": "Nuevo cliente",
|
||||||
"description": "Crear un nuevo cliente",
|
"description": "Crear un nuevo cliente",
|
||||||
"back_to_list": "Volver a la lista"
|
"back_to_list": "Volver a la lista"
|
||||||
},
|
|
||||||
"update": {
|
|
||||||
"title": "Modificación de cliente",
|
|
||||||
"description": "Modificar los datos de un cliente",
|
|
||||||
"back_to_list": "Back to the list"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"form_fields": {
|
"form_fields": {
|
||||||
@ -124,7 +119,7 @@
|
|||||||
"placeholder": "Ingrese la URL del sitio web",
|
"placeholder": "Ingrese la URL del sitio web",
|
||||||
"description": "El sitio web del cliente"
|
"description": "El sitio web del cliente"
|
||||||
},
|
},
|
||||||
"default_taxes": {
|
"default_tax": {
|
||||||
"label": "Impuesto por defecto",
|
"label": "Impuesto por defecto",
|
||||||
"placeholder": "Seleccione el impuesto por defecto",
|
"placeholder": "Seleccione el impuesto por defecto",
|
||||||
"description": "La tasa de impuesto por defecto para el cliente"
|
"description": "La tasa de impuesto por defecto para el cliente"
|
||||||
@ -158,8 +153,8 @@
|
|||||||
"title": "Información de contacto",
|
"title": "Información de contacto",
|
||||||
"description": "Detalles de contacto del cliente"
|
"description": "Detalles de contacto del cliente"
|
||||||
},
|
},
|
||||||
"preferences": {
|
"additional_config": {
|
||||||
"title": "Preferencias",
|
"title": "Configuración adicional",
|
||||||
"description": "Configuraciones adicionales del cliente"
|
"description": "Configuraciones adicionales del cliente"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export const FormDebug = ({ form }: { form: UseFormReturn }) => {
|
|||||||
const currentValues = watch();
|
const currentValues = watch();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='p-4 border rounded bg-red-50 mb-6'>
|
<div className='mt-6 p-4 border rounded bg-gray-50'>
|
||||||
<p>
|
<p>
|
||||||
<strong>¿Formulario modificado?</strong> {isDirty ? "Sí" : "No"}
|
<strong>¿Formulario modificado?</strong> {isDirty ? "Sí" : "No"}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -53,8 +53,8 @@ export const CustomerCreate = () => {
|
|||||||
<>
|
<>
|
||||||
<AppBreadcrumb />
|
<AppBreadcrumb />
|
||||||
<AppContent>
|
<AppContent>
|
||||||
<div className='flex items-center justify-between space-y-4'>
|
<div className='flex items-center justify-between space-y-2'>
|
||||||
<div className='space-y-2'>
|
<div>
|
||||||
<h2 className='text-2xl font-bold tracking-tight text-balance scroll-m-2'>
|
<h2 className='text-2xl font-bold tracking-tight text-balance scroll-m-2'>
|
||||||
{t("pages.create.title")}
|
{t("pages.create.title")}
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@ -7,19 +7,17 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { CURRENCY_OPTIONS, LANGUAGE_OPTIONS } from "../../constants";
|
import { CURRENCY_OPTIONS, LANGUAGE_OPTIONS } from "../../constants";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
|
|
||||||
export const CustomerAdditionalConfigFields = () => {
|
export function CustomerAdditionalConfigFields({ control }: { control: any }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { control } = useForm();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className='border-0 shadow-none bg-sidebar'>
|
<Card className='shadow-none'>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t("form_groups.preferences.title")}</CardTitle>
|
<CardTitle>{t("form_groups.additional_config.title")}</CardTitle>
|
||||||
<CardDescription>{t("form_groups.preferences.description")}</CardDescription>
|
<CardDescription>{t("form_groups.additional_config.description")}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='grid grid-cols-1 gap-y-8 gap-x-6 @xl:grid-cols-2'>
|
<CardContent className='grid grid-cols-1 gap-y-8 gap-x-6 @xl:grid-cols-2'>
|
||||||
<TaxesMultiSelectField
|
<TaxesMultiSelectField
|
||||||
@ -59,4 +57,4 @@ export const CustomerAdditionalConfigFields = () => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { TaxesMultiSelectField } from "@erp/core/components";
|
|
||||||
import { TextField } from "@repo/rdx-ui/components";
|
import { TextField } from "@repo/rdx-ui/components";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@ -16,183 +15,11 @@ import {
|
|||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
|
|
||||||
export const CustomerBasicInfoFields = ({ control }: { control: any }) => {
|
export function CustomerBasicInfoFields({ control }: { control: any }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card className='shadow-none'>
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Identificación</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className='grid grid-cols-1 gap-6 md:grid-cols-4'>
|
|
||||||
<div className='sm:col-span-full'>
|
|
||||||
<FormField
|
|
||||||
control={control}
|
|
||||||
name='is_company'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className='space-y-3'>
|
|
||||||
<FormLabel>{t("form_fields.customer_type.label")}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroup
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
defaultValue={field.value ? "1" : "0"}
|
|
||||||
className='flex gap-6'
|
|
||||||
>
|
|
||||||
<FormItem className='flex items-center space-x-2'>
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroupItem value='1' />
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className='font-normal'>
|
|
||||||
{t("form_fields.customer_type.company")}
|
|
||||||
</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem className='flex items-center space-x-2'>
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroupItem value='0' />
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className='font-normal'>
|
|
||||||
{t("form_fields.customer_type.individual")}
|
|
||||||
</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
</RadioGroup>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='sm:col-span-2'>
|
|
||||||
<TextField
|
|
||||||
control={control}
|
|
||||||
name='name'
|
|
||||||
required
|
|
||||||
label={t("form_fields.name.label")}
|
|
||||||
placeholder={t("form_fields.name.placeholder")}
|
|
||||||
description={t("form_fields.name.description")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='sm:col-span-2'>
|
|
||||||
<TextField
|
|
||||||
control={control}
|
|
||||||
name='trade_name'
|
|
||||||
label={t("form_fields.trade_name.label")}
|
|
||||||
placeholder={t("form_fields.trade_name.placeholder")}
|
|
||||||
description={t("form_fields.trade_name.description")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='sm:col-span-2'>
|
|
||||||
<TaxesMultiSelectField
|
|
||||||
control={control}
|
|
||||||
name='default_taxes'
|
|
||||||
required
|
|
||||||
label={t("form_fields.default_taxes.label")}
|
|
||||||
placeholder={t("form_fields.default_taxes.placeholder")}
|
|
||||||
description={t("form_fields.default_taxes.description")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='col-auto'>
|
|
||||||
<TextField
|
|
||||||
control={control}
|
|
||||||
name='reference'
|
|
||||||
label={t("form_fields.reference.label")}
|
|
||||||
placeholder={t("form_fields.reference.placeholder")}
|
|
||||||
description={t("form_fields.reference.description")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='space-y-12'>
|
|
||||||
<div className='border-b border-gray-900/10 pb-12 dark:border-white/10'>
|
|
||||||
<h2 className='text-base/7 font-semibold text-gray-900 dark:text-white'>
|
|
||||||
{t("form_groups.basic_info.title")}
|
|
||||||
</h2>
|
|
||||||
<p className='mt-1 text-sm/6 text-gray-600 dark:text-gray-400'>
|
|
||||||
{t("form_groups.basic_info.description")}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className='mt-10 grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6'>
|
|
||||||
<div className='sm:col-span-6'>
|
|
||||||
<FormField
|
|
||||||
control={control}
|
|
||||||
name='is_company'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className='space-y-3'>
|
|
||||||
<FormLabel>{t("form_fields.customer_type.label")}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroup
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
defaultValue={field.value ? "1" : "0"}
|
|
||||||
className='flex gap-6'
|
|
||||||
>
|
|
||||||
<FormItem className='flex items-center space-x-2'>
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroupItem value='1' />
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className='font-normal'>
|
|
||||||
{t("form_fields.customer_type.company")}
|
|
||||||
</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem className='flex items-center space-x-2'>
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroupItem value='0' />
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className='font-normal'>
|
|
||||||
{t("form_fields.customer_type.individual")}
|
|
||||||
</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
</RadioGroup>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='sm:col-span-2'>
|
|
||||||
<TextField
|
|
||||||
control={control}
|
|
||||||
name='name'
|
|
||||||
required
|
|
||||||
label={t("form_fields.name.label")}
|
|
||||||
placeholder={t("form_fields.name.placeholder")}
|
|
||||||
description={t("form_fields.name.description")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='sm:col-span-2'>
|
|
||||||
<TextField
|
|
||||||
control={control}
|
|
||||||
name='trade_name'
|
|
||||||
label={t("form_fields.trade_name.label")}
|
|
||||||
placeholder={t("form_fields.trade_name.placeholder")}
|
|
||||||
description={t("form_fields.trade_name.description")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='col-span-full'>
|
|
||||||
<TextField
|
|
||||||
control={control}
|
|
||||||
name='reference'
|
|
||||||
label={t("form_fields.reference.label")}
|
|
||||||
placeholder={t("form_fields.reference.placeholder")}
|
|
||||||
description={t("form_fields.reference.description")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className='shadow-sm bg-gray-50/50'>
|
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t("form_groups.basic_info.title")}</CardTitle>
|
<CardTitle>{t("form_groups.basic_info.title")}</CardTitle>
|
||||||
<CardDescription>{t("form_groups.basic_info.description")}</CardDescription>
|
<CardDescription>{t("form_groups.basic_info.description")}</CardDescription>
|
||||||
@ -258,4 +85,4 @@ export const CustomerBasicInfoFields = ({ control }: { control: any }) => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|||||||
@ -1,28 +1,15 @@
|
|||||||
|
import { TextField } from "@repo/rdx-ui/components";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
|
|
||||||
import { TextField } from "@repo/rdx-ui/components";
|
|
||||||
import { Input } from "@repo/shadcn-ui/components";
|
|
||||||
import { ChevronDown, Phone } from "lucide-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
|
|
||||||
export function CustomerContactFields({ control }: { control: any }) {
|
export function CustomerContactFields({ control }: { control: any }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(true);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className='shadow-none'>
|
<Card className='shadow-none'>
|
||||||
@ -31,63 +18,6 @@ export function CustomerContactFields({ control }: { control: any }) {
|
|||||||
<CardDescription>{t("form_groups.contact_info.description")}</CardDescription>
|
<CardDescription>{t("form_groups.contact_info.description")}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='grid grid-cols-1 gap-y-8 gap-x-6 @xl:grid-cols-2'>
|
<CardContent className='grid grid-cols-1 gap-y-8 gap-x-6 @xl:grid-cols-2'>
|
||||||
<Collapsible open={open} onOpenChange={setOpen} className='space-y-4'>
|
|
||||||
<CollapsibleTrigger className='inline-flex items-center gap-1 text-sm text-muted-foreground hover:underline'>
|
|
||||||
Más detalles{" "}
|
|
||||||
<ChevronDown className={`h-4 w-4 transition-transform ${open ? "rotate-180" : ""}`} />
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent>
|
|
||||||
<div className='grid grid-cols-1 gap-6 md:grid-cols-2'>
|
|
||||||
<FormField
|
|
||||||
control={control}
|
|
||||||
name='phone2'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Teléfono secundario</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
icon={<Phone className='h-4 w-4 text-muted-foreground' />}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={control}
|
|
||||||
name='mobile2'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Móvil secundario</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder='+34 600 00 000'
|
|
||||||
icon={<Phone className='h-4 w-4 text-muted-foreground' />}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={control}
|
|
||||||
name='fax'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className='md:col-span-2'>
|
|
||||||
<FormLabel>Fax</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder='' {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
control={control}
|
control={control}
|
||||||
name='email_primary'
|
name='email_primary'
|
||||||
|
|||||||
@ -1,12 +1,3 @@
|
|||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@repo/shadcn-ui/components";
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { FieldErrors, useForm } from "react-hook-form";
|
import { FieldErrors, useForm } from "react-hook-form";
|
||||||
|
|
||||||
@ -22,13 +13,21 @@ import { CustomerBasicInfoFields } from "./customer-basic-info-fields";
|
|||||||
import { CustomerContactFields } from "./customer-contact-fields";
|
import { CustomerContactFields } from "./customer-contact-fields";
|
||||||
|
|
||||||
interface CustomerFormProps {
|
interface CustomerFormProps {
|
||||||
|
formId: string;
|
||||||
defaultValues: CustomerData; // ✅ ya no recibe DTO
|
defaultValues: CustomerData; // ✅ ya no recibe DTO
|
||||||
isPending?: boolean;
|
isPending?: boolean;
|
||||||
onSubmit: (data: CustomerUpdateData) => void;
|
onSubmit: (data: CustomerData) => void;
|
||||||
onError: (errors: FieldErrors<CustomerUpdateData>) => void;
|
onError: (errors: FieldErrors<CustomerUpdateData>) => void;
|
||||||
|
errorMessage?: string; // ✅ prop nueva para mostrar error global
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CustomerEditForm = ({ defaultValues, onSubmit, isPending }: CustomerFormProps) => {
|
export const CustomerEditForm = ({
|
||||||
|
formId,
|
||||||
|
defaultValues,
|
||||||
|
onSubmit,
|
||||||
|
isPending,
|
||||||
|
errorMessage,
|
||||||
|
}: CustomerFormProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const form = useForm<CustomerUpdateData>({
|
const form = useForm<CustomerUpdateData>({
|
||||||
@ -37,85 +36,14 @@ export const CustomerEditForm = ({ defaultValues, onSubmit, isPending }: Custome
|
|||||||
disabled: isPending,
|
disabled: isPending,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
|
||||||
watch,
|
|
||||||
formState: { isDirty, dirtyFields },
|
|
||||||
} = form;
|
|
||||||
|
|
||||||
useUnsavedChangesNotifier({
|
useUnsavedChangesNotifier({
|
||||||
isDirty,
|
isDirty: form.formState.isDirty,
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentValues = watch();
|
|
||||||
|
|
||||||
const handleSubmit = (data: CustomerUpdateData) => {
|
|
||||||
console.log("Datos del formulario:", data);
|
|
||||||
const changedData: Record<string, string> = {};
|
|
||||||
|
|
||||||
Object.keys(dirtyFields).forEach((field) => {
|
|
||||||
const value = String(currentValues[field as keyof CustomerUpdateData]);
|
|
||||||
changedData[field] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(changedData);
|
|
||||||
|
|
||||||
onSubmit(changedData);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleError = (errors: FieldErrors<CustomerUpdateData>) => {
|
|
||||||
console.error("Errores en el formulario:", errors);
|
|
||||||
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
form.reset(defaultValues);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<FormDebug form={form} />
|
<FormDebug form={form} />
|
||||||
<form onSubmit={form.handleSubmit(handleSubmit, handleError)}>
|
<form id={formId} onSubmit={form.handleSubmit(onSubmit, oError)}>
|
||||||
<div className='flex gap-6'>
|
|
||||||
<div className='w-full xl:w-2/3 space-y-12'>
|
|
||||||
<CustomerBasicInfoFields control={form.control} />
|
|
||||||
<CustomerContactFields control={form.control} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form {...form}>
|
|
||||||
<FormDebug form={form} />
|
|
||||||
<form id={formId} onSubmit={form.handleSubmit(handleSubmit, handleError)}>
|
|
||||||
<div className='grid grid-cols-3 gap-12'>
|
|
||||||
<div className='col-span-2'>
|
|
||||||
<Card className='border-0 shadow-none bg-background'>
|
|
||||||
<CardHeader className='px-0'>
|
|
||||||
<CardTitle>{t("form_groups.basic_info.title")}</CardTitle>
|
|
||||||
<CardDescription>{t("form_groups.basic_info.description")}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className='px-0'>
|
|
||||||
<CustomerBasicInfoFields />
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter>
|
|
||||||
<p> </p>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<CustomerAdditionalConfigFields />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form {...form}>
|
|
||||||
<FormDebug form={form} />
|
|
||||||
<form id={formId} onSubmit={form.handleSubmit(handleSubmit, handleError)}>
|
|
||||||
<div className='w-full grid grid-cols-1 space-y-8 space-x-8 xl:grid-cols-2'>
|
<div className='w-full grid grid-cols-1 space-y-8 space-x-8 xl:grid-cols-2'>
|
||||||
<CustomerBasicInfoFields control={form.control} />
|
<CustomerBasicInfoFields control={form.control} />
|
||||||
<CustomerAddressFields control={form.control} />
|
<CustomerAddressFields control={form.control} />
|
||||||
|
|||||||
@ -94,8 +94,8 @@ export const CustomerUpdate = () => {
|
|||||||
<>
|
<>
|
||||||
<AppBreadcrumb />
|
<AppBreadcrumb />
|
||||||
<AppContent>
|
<AppContent>
|
||||||
<div className='flex items-center justify-between space-y-4'>
|
<div className='flex items-center justify-between space-y-2'>
|
||||||
<div className='space-y-2'>
|
<div>
|
||||||
<h2 className='text-2xl font-bold tracking-tight text-balance scroll-m-2'>
|
<h2 className='text-2xl font-bold tracking-tight text-balance scroll-m-2'>
|
||||||
{t("pages.update.title")}
|
{t("pages.update.title")}
|
||||||
</h2>
|
</h2>
|
||||||
@ -132,10 +132,11 @@ export const CustomerUpdate = () => {
|
|||||||
<div className='flex flex-1 flex-col gap-4 p-4'>
|
<div className='flex flex-1 flex-col gap-4 p-4'>
|
||||||
{/* Importante: proveemos un formId para que el botón del header pueda hacer submit */}
|
{/* Importante: proveemos un formId para que el botón del header pueda hacer submit */}
|
||||||
<CustomerEditForm
|
<CustomerEditForm
|
||||||
defaultValues={customerData}
|
formId='customer-edit-form'
|
||||||
|
defaultValues={defaultValues}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onError={handleError}
|
|
||||||
isPending={isUpdating}
|
isPending={isUpdating}
|
||||||
|
errorMessage={isUpdateError ? getErrorMessage(updateError) : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AppContent>
|
</AppContent>
|
||||||
|
|||||||
@ -71,17 +71,6 @@ export class UtcDate extends ValueObject<UtcDateProps> {
|
|||||||
return this.date.toISOString();
|
return this.date.toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Devuelve la fecha en formato dd/mm/yyyy (formato europeo).
|
|
||||||
*/
|
|
||||||
toEuropeanString(): string {
|
|
||||||
const day = String(this.date.getUTCDate()).padStart(2, "0");
|
|
||||||
const month = String(this.date.getUTCMonth() + 1).padStart(2, "0"); // Los meses en JS empiezan en 0
|
|
||||||
const year = this.date.getUTCFullYear();
|
|
||||||
|
|
||||||
return `${day}/${month}/${year}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compara si dos instancias de UtcDate son iguales.
|
* Compara si dos instancias de UtcDate son iguales.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -53,12 +53,7 @@ export function TextField<TFormValues extends FieldValues>({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input disabled={isDisabled} placeholder={placeholder} {...field} />
|
||||||
disabled={isDisabled}
|
|
||||||
placeholder={placeholder}
|
|
||||||
{...field}
|
|
||||||
className='placeholder:font-normal placeholder:italic'
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<p className={cn("text-xs text-muted-foreground", !description && "invisible")}>
|
<p className={cn("text-xs text-muted-foreground", !description && "invisible")}>
|
||||||
|
|||||||
@ -200,8 +200,8 @@ const data2 = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Proformas de cliente",
|
title: "Facturas de cliente",
|
||||||
url: "/customer-proforma",
|
url: "/customer-invoices",
|
||||||
icon: Bot,
|
icon: Bot,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
@ -219,8 +219,8 @@ const data2 = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Facturas de cliente",
|
title: "Documentation",
|
||||||
url: "/customer-invoices",
|
url: "#",
|
||||||
icon: BookOpen,
|
icon: BookOpen,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|||||||
type={type}
|
type={type}
|
||||||
data-slot='input'
|
data-slot='input'
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-input text-foreground",
|
"bg-background text-foreground",
|
||||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
|||||||
@ -50,9 +50,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--font-sans: Roboto, sans-serif;
|
/*--font-sans: Geist, sans-serif;
|
||||||
--font-serif: Domine, serif;
|
--font-serif: Merriweather, serif;
|
||||||
--font-mono: "Roboto Mono", monospace;
|
--font-mono: "Geist Mono", monospace;*/
|
||||||
|
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
@ -92,10 +92,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--radius: 0.3rem;
|
--radius: 0.5rem;
|
||||||
--background: oklch(1.0 0.0 0);
|
--background: oklch(1.0 0.0 0);
|
||||||
--foreground: oklch(0.143 0.003 271.9282674829111);
|
--foreground: oklch(0.143 0.003 271.9282674829111);
|
||||||
--card: oklch(0.977 0.007 272.5840410480741);
|
--card: oklch(1.0 0.0 0);
|
||||||
--card-foreground: oklch(0.143 0.003 271.9282674829111);
|
--card-foreground: oklch(0.143 0.003 271.9282674829111);
|
||||||
--popover: oklch(1.0 0.0 0);
|
--popover: oklch(1.0 0.0 0);
|
||||||
--popover-foreground: oklch(0.143 0.003 271.9282674829111);
|
--popover-foreground: oklch(0.143 0.003 271.9282674829111);
|
||||||
@ -165,13 +165,16 @@
|
|||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
@apply transition-colors duration-300; /* Added transition for smooth color changes */
|
@apply transition-colors duration-300; /* Added transition for smooth color changes */
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
@apply bg-input;
|
@apply font-semibold;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
@apply font-light;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user