Clientes y facturas de cliente
This commit is contained in:
parent
11402bccc1
commit
9ef847d54b
@ -10,6 +10,12 @@
|
|||||||
|
|
||||||
<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,7 +42,6 @@ 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 instance;
|
return setupInterceptors(instance, getAccessToken, onAuthError);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 instance - Instancia de Axios que será modificada.
|
* @param axiosInstance - 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 = (
|
||||||
instance: AxiosInstance,
|
axiosInstance: AxiosInstance,
|
||||||
getAccessToken: () => string | null,
|
getAccessToken: () => string | null,
|
||||||
onAuthError?: () => void
|
onAuthError?: () => void
|
||||||
): void => {
|
) => {
|
||||||
instance.interceptors.request.use(
|
axiosInstance.interceptors.request.use(
|
||||||
(config: InternalAxiosRequestConfig) => {
|
(config: InternalAxiosRequestConfig) => {
|
||||||
const token = getAccessToken();
|
const token = getAccessToken();
|
||||||
if (token && config.headers) {
|
if (token && config.headers) {
|
||||||
@ -25,13 +25,48 @@ export const setupInterceptors = (
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
instance.interceptors.response.use(
|
axiosInstance.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
(error: AxiosError) => {
|
(error: AxiosError) => {
|
||||||
if (error.response?.status === 401 && onAuthError) {
|
// 🔴 Transformamos SIEMPRE el error antes de propagarlo
|
||||||
|
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.toDateString();
|
return result.toEuropeanString();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,18 @@
|
|||||||
import { MoneyDTO } from "@erp/core";
|
import { MoneyDTO } from "@erp/core";
|
||||||
import { MoneyValue } from "@repo/rdx-ddd";
|
import { MoneyValue } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
export function formatMoneyDTO(amount: MoneyDTO, locale: string) {
|
export type FormatMoneyOptions = {
|
||||||
if (amount.value === "") {
|
locale: string;
|
||||||
return "";
|
hideZeros?: boolean;
|
||||||
|
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({
|
||||||
@ -12,5 +21,5 @@ export function formatMoneyDTO(amount: MoneyDTO, locale: string) {
|
|||||||
scale: Number(amount.scale),
|
scale: Number(amount.scale),
|
||||||
}).data;
|
}).data;
|
||||||
|
|
||||||
return money.format(locale);
|
return money.convertScale(newScale).format(locale);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,10 @@ 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 === "") {
|
if (quantity_value.value === "0" || quantity_value.value === "") {
|
||||||
return "";
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 { formatMoneyDTO, formatQuantityDTO } from "../../helpers";
|
import { FormatMoneyOptions, formatMoneyDTO, formatQuantityDTO } from "../../helpers";
|
||||||
|
|
||||||
type CustomerInvoiceItemsDTO = GetCustomerInvoiceByIdResponseDTO["items"];
|
type CustomerInvoiceItemsDTO = GetCustomerInvoiceByIdResponseDTO["items"];
|
||||||
type CustomerInvoiceItemDTO = ArrayElement<CustomerInvoiceItemsDTO>;
|
type CustomerInvoiceItemDTO = ArrayElement<CustomerInvoiceItemsDTO>;
|
||||||
@ -13,18 +13,23 @@ 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, this._locale),
|
unit_amount: formatMoneyDTO(invoiceItem.unit_amount, moneyOptions),
|
||||||
|
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, this._locale),
|
discount_amount: formatMoneyDTO(invoiceItem.discount_amount, moneyOptions),
|
||||||
taxable_amount: formatMoneyDTO(invoiceItem.taxable_amount, this._locale),
|
taxable_amount: formatMoneyDTO(invoiceItem.taxable_amount, moneyOptions),
|
||||||
taxes_amount: formatMoneyDTO(invoiceItem.taxes_amount, this._locale),
|
taxes_amount: formatMoneyDTO(invoiceItem.taxes_amount, moneyOptions),
|
||||||
total_amount: formatMoneyDTO(invoiceItem.total_amount, this._locale),
|
total_amount: formatMoneyDTO(invoiceItem.total_amount, moneyOptions),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
import { Presenter } from "@erp/core/api";
|
import { Presenter } from "@erp/core/api";
|
||||||
import { GetCustomerInvoiceByIdResponseDTO } from "../../../../common/dto";
|
import { GetCustomerInvoiceByIdResponseDTO } from "../../../../common/dto";
|
||||||
import { formatDateDTO, formatMoneyDTO, formatPercentageDTO } from "../../helpers";
|
import {
|
||||||
|
FormatMoneyOptions,
|
||||||
|
formatDateDTO,
|
||||||
|
formatMoneyDTO,
|
||||||
|
formatPercentageDTO,
|
||||||
|
} from "../../helpers";
|
||||||
|
|
||||||
export class CustomerInvoiceReportPresenter extends Presenter<
|
export class CustomerInvoiceReportPresenter extends Presenter<
|
||||||
GetCustomerInvoiceByIdResponseDTO,
|
GetCustomerInvoiceByIdResponseDTO,
|
||||||
@ -18,17 +23,23 @@ 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, locale),
|
subtotal_amount: formatMoneyDTO(invoiceDTO.subtotal_amount, moneyOptions),
|
||||||
discount_percetage: formatPercentageDTO(invoiceDTO.discount_percentage, locale),
|
discount_percentage: formatPercentageDTO(invoiceDTO.discount_percentage, locale),
|
||||||
discount_amount: formatMoneyDTO(invoiceDTO.discount_amount, locale),
|
discount_amount: formatMoneyDTO(invoiceDTO.discount_amount, moneyOptions),
|
||||||
taxable_amount: formatMoneyDTO(invoiceDTO.taxable_amount, locale),
|
taxable_amount: formatMoneyDTO(invoiceDTO.taxable_amount, moneyOptions),
|
||||||
taxes_amount: formatMoneyDTO(invoiceDTO.taxes_amount, locale),
|
taxes_amount: formatMoneyDTO(invoiceDTO.taxes_amount, moneyOptions),
|
||||||
total_amount: formatMoneyDTO(invoiceDTO.total_amount, locale),
|
total_amount: formatMoneyDTO(invoiceDTO.total_amount, moneyOptions),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,6 +36,7 @@ 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: {
|
||||||
@ -48,6 +49,10 @@ 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();
|
||||||
|
|||||||
@ -55,14 +55,14 @@
|
|||||||
|
|
||||||
table th,
|
table th,
|
||||||
table td {
|
table td {
|
||||||
border: 0px solid #ccc;
|
border: 0px solid;
|
||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
table th {
|
table th {
|
||||||
background-color: #f5f5f5;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.totals {
|
.totals {
|
||||||
@ -81,7 +81,7 @@
|
|||||||
|
|
||||||
footer {
|
footer {
|
||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
font-size: 12px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight {
|
.highlight {
|
||||||
@ -119,7 +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>
|
<p><span>Página:</span><span class="pageNumber"></span> / <span class="totalPages"></span></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>
|
||||||
@ -174,9 +174,10 @@
|
|||||||
{{#each items}}
|
{{#each items}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{description}}</td>
|
<td>{{description}}</td>
|
||||||
<td class="text-right">{{quantity}}</td>
|
<td class="text-right">{{#if quantity}}{{quantity}}{{else}} {{/if}}</td>
|
||||||
<td class="text-right">{{unit_amount}}</td>
|
<td class="text-right">{{#if unit_amount}}{{unit_amount}}{{else}} {{/if}}</td>
|
||||||
<td class="text-right">{{total_amount}}</td>
|
<td class="text-right">{{#if total_amount}}{{total_amount}}{{else}} {{/if}}</td>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -197,14 +198,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 percentage}}
|
{{#if discount_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 0%</td>
|
<td class="px-4 text-right">Descuento {{discount_percentage}}</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,7 +238,8 @@
|
|||||||
|
|
||||||
<footer id="footer" class="mt-4">
|
<footer id="footer" class="mt-4">
|
||||||
<aside>
|
<aside>
|
||||||
<p>Insc. en el Reg. Merc. de Madrid, Tomo 20.073, Libro 0, Folio 141, Sección 8, Hoja M-354212 | CIF: B83999441 -
|
<p class="text-center">Insc. en el Reg. Merc. de Madrid, Tomo 20.073, Libro 0, Folio 141, Sección 8, Hoja M-354212
|
||||||
|
| CIF: B83999441 -
|
||||||
Rodax Software S.L.</p>
|
Rodax Software S.L.</p>
|
||||||
</aside>
|
</aside>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@ -1,22 +1,22 @@
|
|||||||
import { ITransactionManager } from "@erp/core/api";
|
import { IPresenterRegistry, 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 { UpdateCustomerRequestDTO } from "../../../common";
|
import { UpdateCustomerByIdRequestDTO } from "../../../../common/dto";
|
||||||
import { CustomerPatchProps, CustomerService } from "../../domain";
|
import { CustomerPatchProps, CustomerService } from "../../../domain";
|
||||||
import { UpdateCustomerAssembler } from "./assembler";
|
import { CustomerFullPresenter } from "../../presenters";
|
||||||
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: UpdateCustomerRequestDTO;
|
dto: UpdateCustomerByIdRequestDTO;
|
||||||
};
|
};
|
||||||
|
|
||||||
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 assembler: UpdateCustomerAssembler
|
private readonly presenterRegistry: IPresenterRegistry
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public execute(params: UpdateCustomerUseCaseInput) {
|
public execute(params: UpdateCustomerUseCaseInput) {
|
||||||
@ -28,6 +28,10 @@ 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);
|
||||||
@ -50,10 +54,10 @@ export class UpdateCustomerUseCase {
|
|||||||
return Result.fail(updatedCustomer.error);
|
return Result.fail(updatedCustomer.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedCustomer = await this.service.saveCustomer(updatedCustomer.data, transaction);
|
const customerOrError = await this.service.saveCustomer(updatedCustomer.data, transaction);
|
||||||
|
const customer = customerOrError.data;
|
||||||
const getDTO = this.assembler.toDTO(savedCustomer.data);
|
const dto = presenter.toOutput(customer);
|
||||||
return Result.ok(getDTO);
|
return Result.ok(dto);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
return Result.fail(error as Error);
|
return Result.fail(error as Error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ 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";
|
||||||
@ -25,8 +26,8 @@ export type CustomerDeps = {
|
|||||||
list: () => ListCustomersUseCase;
|
list: () => ListCustomersUseCase;
|
||||||
get: () => GetCustomerUseCase;
|
get: () => GetCustomerUseCase;
|
||||||
create: () => CreateCustomerUseCase;
|
create: () => CreateCustomerUseCase;
|
||||||
/*update: () => UpdateCustomerUseCase;
|
update: () => UpdateCustomerUseCase;
|
||||||
delete: () => DeleteCustomerUseCase;*/
|
//delete: () => DeleteCustomerUseCase;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -67,8 +68,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,12 +6,15 @@ 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) => {
|
||||||
@ -81,9 +84,10 @@ 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) => {
|
||||||
@ -93,7 +97,7 @@ export const customersRouter = (params: ModuleParams) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
router.delete(
|
/*router.delete(
|
||||||
"/:customer_id",
|
"/:customer_id",
|
||||||
//checkTabContext,
|
//checkTabContext,
|
||||||
|
|
||||||
@ -103,7 +107,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 = z.infer<typeof UpdateCustomerByIdRequestSchema>;
|
export type UpdateCustomerByIdRequestDTO = Partial<z.infer<typeof UpdateCustomerByIdRequestSchema>>;
|
||||||
|
|||||||
@ -21,6 +21,11 @@
|
|||||||
"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": {
|
||||||
@ -117,10 +122,10 @@
|
|||||||
"placeholder": "Enter website URL",
|
"placeholder": "Enter website URL",
|
||||||
"description": "The website of the customer"
|
"description": "The website of the customer"
|
||||||
},
|
},
|
||||||
"default_tax": {
|
"default_taxes": {
|
||||||
"label": "Default tax",
|
"label": "Default taxes",
|
||||||
"placeholder": "Select default tax",
|
"placeholder": "Select default taxes",
|
||||||
"description": "The default tax rate for the customer"
|
"description": "The default tax rates for the customer"
|
||||||
},
|
},
|
||||||
"language_code": {
|
"language_code": {
|
||||||
"label": "Language",
|
"label": "Language",
|
||||||
@ -151,8 +156,8 @@
|
|||||||
"title": "Contact information",
|
"title": "Contact information",
|
||||||
"description": "Customer contact details"
|
"description": "Customer contact details"
|
||||||
},
|
},
|
||||||
"additional_config": {
|
"preferences": {
|
||||||
"title": "Additional settings",
|
"title": "Preferences",
|
||||||
"description": "Additional customer configurations"
|
"description": "Additional customer configurations"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -21,6 +21,11 @@
|
|||||||
"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": {
|
||||||
@ -119,7 +124,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_tax": {
|
"default_taxes": {
|
||||||
"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"
|
||||||
@ -153,8 +158,8 @@
|
|||||||
"title": "Información de contacto",
|
"title": "Información de contacto",
|
||||||
"description": "Detalles de contacto del cliente"
|
"description": "Detalles de contacto del cliente"
|
||||||
},
|
},
|
||||||
"additional_config": {
|
"preferences": {
|
||||||
"title": "Configuración adicional",
|
"title": "Preferencias",
|
||||||
"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='mt-6 p-4 border rounded bg-gray-50'>
|
<div className='p-4 border rounded bg-red-50 mb-6'>
|
||||||
<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-2'>
|
<div className='flex items-center justify-between space-y-4'>
|
||||||
<div>
|
<div className='space-y-2'>
|
||||||
<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,17 +7,19 @@ 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 function CustomerAdditionalConfigFields({ control }: { control: any }) {
|
export const CustomerAdditionalConfigFields = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { control } = useForm();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className='shadow-none'>
|
<Card className='border-0 shadow-none bg-sidebar'>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t("form_groups.additional_config.title")}</CardTitle>
|
<CardTitle>{t("form_groups.preferences.title")}</CardTitle>
|
||||||
<CardDescription>{t("form_groups.additional_config.description")}</CardDescription>
|
<CardDescription>{t("form_groups.preferences.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
|
||||||
@ -57,4 +59,4 @@ export function CustomerAdditionalConfigFields({ control }: { control: any }) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { TaxesMultiSelectField } from "@erp/core/components";
|
||||||
import { TextField } from "@repo/rdx-ui/components";
|
import { TextField } from "@repo/rdx-ui/components";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@ -15,11 +16,183 @@ import {
|
|||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
|
|
||||||
export function CustomerBasicInfoFields({ control }: { control: any }) {
|
export const CustomerBasicInfoFields = ({ control }: { control: any }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className='shadow-none'>
|
<Card>
|
||||||
|
<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>
|
||||||
@ -85,4 +258,4 @@ export function CustomerBasicInfoFields({ control }: { control: any }) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@ -1,15 +1,28 @@
|
|||||||
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'>
|
||||||
@ -18,6 +31,63 @@ 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,3 +1,12 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
@ -13,21 +22,13 @@ 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: CustomerData) => void;
|
onSubmit: (data: CustomerUpdateData) => void;
|
||||||
onError: (errors: FieldErrors<CustomerUpdateData>) => void;
|
onError: (errors: FieldErrors<CustomerUpdateData>) => void;
|
||||||
errorMessage?: string; // ✅ prop nueva para mostrar error global
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CustomerEditForm = ({
|
export const CustomerEditForm = ({ defaultValues, onSubmit, isPending }: CustomerFormProps) => {
|
||||||
formId,
|
|
||||||
defaultValues,
|
|
||||||
onSubmit,
|
|
||||||
isPending,
|
|
||||||
errorMessage,
|
|
||||||
}: CustomerFormProps) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const form = useForm<CustomerUpdateData>({
|
const form = useForm<CustomerUpdateData>({
|
||||||
@ -36,14 +37,85 @@ export const CustomerEditForm = ({
|
|||||||
disabled: isPending,
|
disabled: isPending,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
watch,
|
||||||
|
formState: { isDirty, dirtyFields },
|
||||||
|
} = form;
|
||||||
|
|
||||||
useUnsavedChangesNotifier({
|
useUnsavedChangesNotifier({
|
||||||
isDirty: form.formState.isDirty,
|
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 id={formId} onSubmit={form.handleSubmit(onSubmit, oError)}>
|
<form onSubmit={form.handleSubmit(handleSubmit, handleError)}>
|
||||||
|
<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-2'>
|
<div className='flex items-center justify-between space-y-4'>
|
||||||
<div>
|
<div className='space-y-2'>
|
||||||
<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,11 +132,10 @@ 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
|
||||||
formId='customer-edit-form'
|
defaultValues={customerData}
|
||||||
defaultValues={defaultValues}
|
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
|
onError={handleError}
|
||||||
isPending={isUpdating}
|
isPending={isUpdating}
|
||||||
errorMessage={isUpdateError ? getErrorMessage(updateError) : undefined}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AppContent>
|
</AppContent>
|
||||||
|
|||||||
@ -71,6 +71,17 @@ 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,7 +53,12 @@ export function TextField<TFormValues extends FieldValues>({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input disabled={isDisabled} placeholder={placeholder} {...field} />
|
<Input
|
||||||
|
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")}>
|
||||||
|
|||||||
@ -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-background text-foreground",
|
"bg-input 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: Geist, sans-serif;
|
--font-sans: Roboto, sans-serif;
|
||||||
--font-serif: Merriweather, serif;
|
--font-serif: Domine, serif;
|
||||||
--font-mono: "Geist Mono", monospace;*/
|
--font-mono: "Roboto 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.5rem;
|
--radius: 0.3rem;
|
||||||
--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(1.0 0.0 0);
|
--card: oklch(0.977 0.007 272.5840410480741);
|
||||||
--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,16 +165,13 @@
|
|||||||
@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 font-semibold;
|
@apply bg-input;
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
@apply font-light;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user