From 9ef847d54b71c96dc7fdb16a963afd9d5e1d9356 Mon Sep 17 00:00:00 2001 From: david Date: Fri, 19 Sep 2025 18:55:30 +0200 Subject: [PATCH] Clientes y facturas de cliente --- apps/web/index.html | 6 + .../axios/create-axios-instance.ts | 3 +- .../data-source/axios/setup-interceptors.ts | 51 ++++- .../application/helpers/format-date-dto.ts | 2 +- .../application/helpers/format-money-dto.ts | 17 +- .../helpers/format-percentage-dto.ts | 4 + .../helpers/format-quantity-dto.ts | 4 +- ...customer-invoice-items.report.presenter.ts | 21 +- .../customer-invoice.report.presenter.ts | 25 ++- .../reporter/customer-invoice.report.pdf.ts | 5 + .../templates/customer-invoice/template.hbs | 22 ++- .../update/update-customer.use-case.ts | 24 ++- .../src/api/infrastructure/dependencies.ts | 9 +- .../express/controllers/index.ts | 2 +- .../express/customers.routes.ts | 10 +- .../update-customer-by-id.request.dto.ts | 2 +- modules/customers/src/common/locales/en.json | 17 +- modules/customers/src/common/locales/es.json | 11 +- .../src/web/components/form-debug.tsx | 2 +- .../customers/src/web/pages/create/create.tsx | 4 +- .../customer-additional-config-fields.tsx | 12 +- .../update/customer-basic-info-fields.tsx | 179 +++++++++++++++++- .../pages/update/customer-contact-fields.tsx | 72 ++++++- .../web/pages/update/customer-edit-form.tsx | 96 ++++++++-- .../customers/src/web/pages/update/update.tsx | 9 +- .../rdx-ddd/src/value-objects/utc-date.ts | 11 ++ .../rdx-ui/src/components/form/TextField.tsx | 7 +- packages/shadcn-ui/src/components/input.tsx | 2 +- packages/shadcn-ui/src/styles/globals.css | 17 +- 29 files changed, 535 insertions(+), 111 deletions(-) diff --git a/apps/web/index.html b/apps/web/index.html index 5ec17b9b..c6d6ac78 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -10,6 +10,12 @@ + + FactuGES 2025 diff --git a/modules/core/src/web/lib/data-source/axios/create-axios-instance.ts b/modules/core/src/web/lib/data-source/axios/create-axios-instance.ts index a9d8392a..e2cf47fe 100644 --- a/modules/core/src/web/lib/data-source/axios/create-axios-instance.ts +++ b/modules/core/src/web/lib/data-source/axios/create-axios-instance.ts @@ -42,7 +42,6 @@ export const createAxiosInstance = ({ }: AxiosFactoryConfig): AxiosInstance => { const instance = axios.create(defaultAxiosRequestConfig); instance.defaults.baseURL = baseURL; - setupInterceptors(instance, getAccessToken, onAuthError); - return instance; + return setupInterceptors(instance, getAccessToken, onAuthError); }; diff --git a/modules/core/src/web/lib/data-source/axios/setup-interceptors.ts b/modules/core/src/web/lib/data-source/axios/setup-interceptors.ts index 56da5a48..fe997c38 100644 --- a/modules/core/src/web/lib/data-source/axios/setup-interceptors.ts +++ b/modules/core/src/web/lib/data-source/axios/setup-interceptors.ts @@ -3,16 +3,16 @@ import { AxiosError, AxiosInstance, InternalAxiosRequestConfig } from "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 onAuthError - Función opcional que se ejecuta ante errores de autenticación (status 401). */ export const setupInterceptors = ( - instance: AxiosInstance, + axiosInstance: AxiosInstance, getAccessToken: () => string | null, onAuthError?: () => void -): void => { - instance.interceptors.request.use( +) => { + axiosInstance.interceptors.request.use( (config: InternalAxiosRequestConfig) => { const token = getAccessToken(); if (token && config.headers) { @@ -25,13 +25,48 @@ export const setupInterceptors = ( } ); - instance.interceptors.response.use( + axiosInstance.interceptors.response.use( (response) => response, (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(); - } - 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; +} diff --git a/modules/customer-invoices/src/api/application/helpers/format-date-dto.ts b/modules/customer-invoices/src/api/application/helpers/format-date-dto.ts index f4d6a720..fd7e5e53 100644 --- a/modules/customer-invoices/src/api/application/helpers/format-date-dto.ts +++ b/modules/customer-invoices/src/api/application/helpers/format-date-dto.ts @@ -3,5 +3,5 @@ import { UtcDate } from "@repo/rdx-ddd"; export function formatDateDTO(dateString: string) { const result = UtcDate.createFromISO(dateString).data; - return result.toDateString(); + return result.toEuropeanString(); } diff --git a/modules/customer-invoices/src/api/application/helpers/format-money-dto.ts b/modules/customer-invoices/src/api/application/helpers/format-money-dto.ts index 3a84ea1a..1c5787b4 100644 --- a/modules/customer-invoices/src/api/application/helpers/format-money-dto.ts +++ b/modules/customer-invoices/src/api/application/helpers/format-money-dto.ts @@ -1,9 +1,18 @@ import { MoneyDTO } from "@erp/core"; import { MoneyValue } from "@repo/rdx-ddd"; -export function formatMoneyDTO(amount: MoneyDTO, locale: string) { - if (amount.value === "") { - return ""; +export type FormatMoneyOptions = { + locale: string; + 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({ @@ -12,5 +21,5 @@ export function formatMoneyDTO(amount: MoneyDTO, locale: string) { scale: Number(amount.scale), }).data; - return money.format(locale); + return money.convertScale(newScale).format(locale); } diff --git a/modules/customer-invoices/src/api/application/helpers/format-percentage-dto.ts b/modules/customer-invoices/src/api/application/helpers/format-percentage-dto.ts index dee79061..481ad303 100644 --- a/modules/customer-invoices/src/api/application/helpers/format-percentage-dto.ts +++ b/modules/customer-invoices/src/api/application/helpers/format-percentage-dto.ts @@ -2,6 +2,10 @@ import { PercentageDTO } from "@erp/core"; import { Percentage } from "@repo/rdx-ddd"; export function formatPercentageDTO(Percentage_value: PercentageDTO, locale: string) { + if (Percentage_value.value === "0" || Percentage_value.value === "") { + return null; + } + const value = Percentage.create({ value: Number(Percentage_value.value), scale: Number(Percentage_value.scale), diff --git a/modules/customer-invoices/src/api/application/helpers/format-quantity-dto.ts b/modules/customer-invoices/src/api/application/helpers/format-quantity-dto.ts index 3a662b16..0856b75e 100644 --- a/modules/customer-invoices/src/api/application/helpers/format-quantity-dto.ts +++ b/modules/customer-invoices/src/api/application/helpers/format-quantity-dto.ts @@ -2,8 +2,8 @@ import { QuantityDTO } from "@erp/core"; import { Quantity } from "@repo/rdx-ddd"; export function formatQuantityDTO(quantity_value: QuantityDTO) { - if (quantity_value.value === "") { - return ""; + if (quantity_value.value === "0" || quantity_value.value === "") { + return null; } const value = Quantity.create({ diff --git a/modules/customer-invoices/src/api/application/presenters/queries/customer-invoice-items.report.presenter.ts b/modules/customer-invoices/src/api/application/presenters/queries/customer-invoice-items.report.presenter.ts index 97e53116..6aa964e7 100644 --- a/modules/customer-invoices/src/api/application/presenters/queries/customer-invoice-items.report.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/queries/customer-invoice-items.report.presenter.ts @@ -1,7 +1,7 @@ import { IPresenterOutputParams, Presenter } from "@erp/core/api"; import { GetCustomerInvoiceByIdResponseDTO } from "@erp/customer-invoices/common"; import { ArrayElement } from "@repo/rdx-utils"; -import { formatMoneyDTO, formatQuantityDTO } from "../../helpers"; +import { FormatMoneyOptions, formatMoneyDTO, formatQuantityDTO } from "../../helpers"; type CustomerInvoiceItemsDTO = GetCustomerInvoiceByIdResponseDTO["items"]; type CustomerInvoiceItemDTO = ArrayElement; @@ -13,18 +13,23 @@ export class CustomerInvoiceItemsReportPersenter extends Presenter< private _locale!: string; private _mapItem(invoiceItem: CustomerInvoiceItemDTO, index: number) { + const moneyOptions: FormatMoneyOptions = { + locale: this._locale, + hideZeros: true, + newScale: 2, + }; + return { ...invoiceItem, quantity: formatQuantityDTO(invoiceItem.quantity), - unit_amount: formatMoneyDTO(invoiceItem.unit_amount, this._locale), - - subtotal_amount: formatMoneyDTO(invoiceItem.subtotal_amount, this._locale), + unit_amount: formatMoneyDTO(invoiceItem.unit_amount, moneyOptions), + subtotal_amount: formatMoneyDTO(invoiceItem.subtotal_amount, moneyOptions), // discount_percetage: formatPercentageDTO(invoiceItem.discount_percentage, this._locale), - discount_amount: formatMoneyDTO(invoiceItem.discount_amount, this._locale), - taxable_amount: formatMoneyDTO(invoiceItem.taxable_amount, this._locale), - taxes_amount: formatMoneyDTO(invoiceItem.taxes_amount, this._locale), - total_amount: formatMoneyDTO(invoiceItem.total_amount, this._locale), + discount_amount: formatMoneyDTO(invoiceItem.discount_amount, moneyOptions), + taxable_amount: formatMoneyDTO(invoiceItem.taxable_amount, moneyOptions), + taxes_amount: formatMoneyDTO(invoiceItem.taxes_amount, moneyOptions), + total_amount: formatMoneyDTO(invoiceItem.total_amount, moneyOptions), }; } diff --git a/modules/customer-invoices/src/api/application/presenters/queries/customer-invoice.report.presenter.ts b/modules/customer-invoices/src/api/application/presenters/queries/customer-invoice.report.presenter.ts index ae4e1967..06185fd0 100644 --- a/modules/customer-invoices/src/api/application/presenters/queries/customer-invoice.report.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/queries/customer-invoice.report.presenter.ts @@ -1,6 +1,11 @@ import { Presenter } from "@erp/core/api"; import { GetCustomerInvoiceByIdResponseDTO } from "../../../../common/dto"; -import { formatDateDTO, formatMoneyDTO, formatPercentageDTO } from "../../helpers"; +import { + FormatMoneyOptions, + formatDateDTO, + formatMoneyDTO, + formatPercentageDTO, +} from "../../helpers"; export class CustomerInvoiceReportPresenter extends Presenter< GetCustomerInvoiceByIdResponseDTO, @@ -18,17 +23,23 @@ export class CustomerInvoiceReportPresenter extends Presenter< locale, }); + const moneyOptions: FormatMoneyOptions = { + locale, + hideZeros: true, + newScale: 2, + }; + return { ...invoiceDTO, items: itemsDTO, invoice_date: formatDateDTO(invoiceDTO.invoice_date), - subtotal_amount: formatMoneyDTO(invoiceDTO.subtotal_amount, locale), - discount_percetage: formatPercentageDTO(invoiceDTO.discount_percentage, locale), - discount_amount: formatMoneyDTO(invoiceDTO.discount_amount, locale), - taxable_amount: formatMoneyDTO(invoiceDTO.taxable_amount, locale), - taxes_amount: formatMoneyDTO(invoiceDTO.taxes_amount, locale), - total_amount: formatMoneyDTO(invoiceDTO.total_amount, locale), + subtotal_amount: formatMoneyDTO(invoiceDTO.subtotal_amount, moneyOptions), + discount_percentage: formatPercentageDTO(invoiceDTO.discount_percentage, locale), + discount_amount: formatMoneyDTO(invoiceDTO.discount_amount, moneyOptions), + taxable_amount: formatMoneyDTO(invoiceDTO.taxable_amount, moneyOptions), + taxes_amount: formatMoneyDTO(invoiceDTO.taxes_amount, moneyOptions), + total_amount: formatMoneyDTO(invoiceDTO.total_amount, moneyOptions), }; } } diff --git a/modules/customer-invoices/src/api/application/use-cases/report/reporter/customer-invoice.report.pdf.ts b/modules/customer-invoices/src/api/application/use-cases/report/reporter/customer-invoice.report.pdf.ts index be770e48..d0e49198 100644 --- a/modules/customer-invoices/src/api/application/use-cases/report/reporter/customer-invoice.report.pdf.ts +++ b/modules/customer-invoices/src/api/application/use-cases/report/reporter/customer-invoice.report.pdf.ts @@ -36,6 +36,7 @@ export class CustomerInvoiceReportPDFPresenter extends Presenter< await page.setContent(htmlData, { waitUntil: "networkidle2" }); await navigationPromise; + const reportPDF = await report.pdfPage(page, { format: "A4", margin: { @@ -48,6 +49,10 @@ export class CustomerInvoiceReportPDFPresenter extends Presenter< preferCSSPageSize: true, omitBackground: false, printBackground: true, + displayHeaderFooter: true, + headerTemplate: "
", + footerTemplate: + '
Página de
', }); await browser.close(); diff --git a/modules/customer-invoices/src/api/application/use-cases/report/reporter/templates/customer-invoice/template.hbs b/modules/customer-invoices/src/api/application/use-cases/report/reporter/templates/customer-invoice/template.hbs index c28c4e7b..1d992e01 100644 --- a/modules/customer-invoices/src/api/application/use-cases/report/reporter/templates/customer-invoice/template.hbs +++ b/modules/customer-invoices/src/api/application/use-cases/report/reporter/templates/customer-invoice/template.hbs @@ -55,14 +55,14 @@ table th, table td { - border: 0px solid #ccc; + border: 0px solid; padding: 3px 10px; text-align: left; vertical-align: top; } table th { - background-color: #f5f5f5; + margin-bottom: 10px; } .totals { @@ -81,7 +81,7 @@ footer { margin-top: 40px; - font-size: 12px; + font-size: 10px; } .highlight { @@ -119,7 +119,7 @@

Factura nº: {{invoice_number}}

Fecha: {{invoice_date}}

-

Página:1 / 1

+

Página: /

{{recipient.name}}

@@ -174,9 +174,10 @@ {{#each items}} {{description}} - {{quantity}} - {{unit_amount}} - {{total_amount}} + {{#if quantity}}{{quantity}}{{else}} {{/if}} + {{#if unit_amount}}{{unit_amount}}{{else}} {{/if}} + {{#if total_amount}}{{total_amount}}{{else}} {{/if}} + {{/each}} @@ -197,14 +198,14 @@
- {{#if percentage}} + {{#if discount_percentage}} - + @@ -237,7 +238,8 @@
diff --git a/modules/customers/src/api/application/use-cases/update/update-customer.use-case.ts b/modules/customers/src/api/application/use-cases/update/update-customer.use-case.ts index 028cdadc..04ec170e 100644 --- a/modules/customers/src/api/application/use-cases/update/update-customer.use-case.ts +++ b/modules/customers/src/api/application/use-cases/update/update-customer.use-case.ts @@ -1,22 +1,22 @@ -import { ITransactionManager } from "@erp/core/api"; +import { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import { UpdateCustomerRequestDTO } from "../../../common"; -import { CustomerPatchProps, CustomerService } from "../../domain"; -import { UpdateCustomerAssembler } from "./assembler"; +import { UpdateCustomerByIdRequestDTO } from "../../../../common/dto"; +import { CustomerPatchProps, CustomerService } from "../../../domain"; +import { CustomerFullPresenter } from "../../presenters"; import { mapDTOToUpdateCustomerPatchProps } from "./map-dto-to-update-customer-props"; type UpdateCustomerUseCaseInput = { companyId: UniqueID; customer_id: string; - dto: UpdateCustomerRequestDTO; + dto: UpdateCustomerByIdRequestDTO; }; export class UpdateCustomerUseCase { constructor( private readonly service: CustomerService, private readonly transactionManager: ITransactionManager, - private readonly assembler: UpdateCustomerAssembler + private readonly presenterRegistry: IPresenterRegistry ) {} public execute(params: UpdateCustomerUseCaseInput) { @@ -28,6 +28,10 @@ export class UpdateCustomerUseCase { } const customerId = idOrError.data; + const presenter = this.presenterRegistry.getPresenter({ + resource: "customer", + projection: "FULL", + }) as CustomerFullPresenter; // Mapear DTO → props de dominio const patchPropsResult = mapDTOToUpdateCustomerPatchProps(dto); @@ -50,10 +54,10 @@ export class UpdateCustomerUseCase { return Result.fail(updatedCustomer.error); } - const savedCustomer = await this.service.saveCustomer(updatedCustomer.data, transaction); - - const getDTO = this.assembler.toDTO(savedCustomer.data); - return Result.ok(getDTO); + const customerOrError = await this.service.saveCustomer(updatedCustomer.data, transaction); + const customer = customerOrError.data; + const dto = presenter.toOutput(customer); + return Result.ok(dto); } catch (error: unknown) { return Result.fail(error as Error); } diff --git a/modules/customers/src/api/infrastructure/dependencies.ts b/modules/customers/src/api/infrastructure/dependencies.ts index cf48aee9..28e8c805 100644 --- a/modules/customers/src/api/infrastructure/dependencies.ts +++ b/modules/customers/src/api/infrastructure/dependencies.ts @@ -9,6 +9,7 @@ import { CustomerFullPresenter, ListCustomersPresenter, ListCustomersUseCase, + UpdateCustomerUseCase, } from "../application"; import { GetCustomerUseCase } from "../application/use-cases/get-customer.use-case"; import { CustomerService } from "../domain"; @@ -25,8 +26,8 @@ export type CustomerDeps = { list: () => ListCustomersUseCase; get: () => GetCustomerUseCase; create: () => CreateCustomerUseCase; - /*update: () => UpdateCustomerUseCase; - delete: () => DeleteCustomerUseCase;*/ + update: () => UpdateCustomerUseCase; + //delete: () => DeleteCustomerUseCase; }; }; @@ -67,8 +68,8 @@ export function buildCustomerDependencies(params: ModuleParams): CustomerDeps { list: () => new ListCustomersUseCase(service, transactionManager, presenterRegistry), get: () => new GetCustomerUseCase(service, transactionManager, presenterRegistry), create: () => new CreateCustomerUseCase(service, transactionManager, presenterRegistry), - /*update: () => new UpdateCustomerUseCase(_service!, transactionManager!, presenterRegistry!), - delete: () => new DeleteCustomerUseCase(_service!, transactionManager!),*/ + update: () => new UpdateCustomerUseCase(service, transactionManager, presenterRegistry), + //delete: () => new DeleteCustomerUseCase(_service!, transactionManager!), }, }; } diff --git a/modules/customers/src/api/infrastructure/express/controllers/index.ts b/modules/customers/src/api/infrastructure/express/controllers/index.ts index 60ce3f1f..981dca00 100644 --- a/modules/customers/src/api/infrastructure/express/controllers/index.ts +++ b/modules/customers/src/api/infrastructure/express/controllers/index.ts @@ -2,4 +2,4 @@ export * from "./create-customer.controller"; export * from "./delete-customer.controller"; export * from "./get-customer.controller"; export * from "./list-customers.controller"; -///export * from "./update-customer.controller"; +export * from "./update-customer.controller"; diff --git a/modules/customers/src/api/infrastructure/express/customers.routes.ts b/modules/customers/src/api/infrastructure/express/customers.routes.ts index a9849394..7678441d 100644 --- a/modules/customers/src/api/infrastructure/express/customers.routes.ts +++ b/modules/customers/src/api/infrastructure/express/customers.routes.ts @@ -6,12 +6,15 @@ import { CreateCustomerRequestSchema, CustomerListRequestSchema, GetCustomerByIdRequestSchema, + UpdateCustomerByIdParamsRequestSchema, + UpdateCustomerByIdRequestSchema, } from "../../../common/dto"; import { buildCustomerDependencies } from "../dependencies"; import { CreateCustomerController, GetCustomerController, ListCustomersController, + UpdateCustomerController, } from "./controllers"; export const customersRouter = (params: ModuleParams) => { @@ -81,9 +84,10 @@ export const customersRouter = (params: ModuleParams) => { } ); - /*router.put( + router.put( "/:customer_id", //checkTabContext, + validateRequest(UpdateCustomerByIdParamsRequestSchema, "params"), validateRequest(UpdateCustomerByIdRequestSchema, "body"), (req: Request, res: Response, next: NextFunction) => { @@ -93,7 +97,7 @@ export const customersRouter = (params: ModuleParams) => { } ); - router.delete( + /*router.delete( "/:customer_id", //checkTabContext, @@ -103,7 +107,7 @@ export const customersRouter = (params: ModuleParams) => { const controller = new DeleteCustomerController(useCase); return controller.execute(req, res, next); } - ); */ + );*/ app.use(`${baseRoutePath}/customers`, router); }; diff --git a/modules/customers/src/common/dto/request/update-customer-by-id.request.dto.ts b/modules/customers/src/common/dto/request/update-customer-by-id.request.dto.ts index 7cbb53b0..5bd4a7df 100644 --- a/modules/customers/src/common/dto/request/update-customer-by-id.request.dto.ts +++ b/modules/customers/src/common/dto/request/update-customer-by-id.request.dto.ts @@ -31,4 +31,4 @@ export const UpdateCustomerByIdRequestSchema = z.object({ currency_code: z.string().optional(), }); -export type UpdateCustomerByIdRequestDTO = z.infer; +export type UpdateCustomerByIdRequestDTO = Partial>; diff --git a/modules/customers/src/common/locales/en.json b/modules/customers/src/common/locales/en.json index 3ce0a224..9b87f3ee 100644 --- a/modules/customers/src/common/locales/en.json +++ b/modules/customers/src/common/locales/en.json @@ -21,6 +21,11 @@ "title": "New customer", "description": "Create a new customer", "back_to_list": "Back to the list" + }, + "update": { + "title": "Update customer", + "description": "Update a customer", + "back_to_list": "Back to the list" } }, "form_fields": { @@ -117,10 +122,10 @@ "placeholder": "Enter website URL", "description": "The website of the customer" }, - "default_tax": { - "label": "Default tax", - "placeholder": "Select default tax", - "description": "The default tax rate for the customer" + "default_taxes": { + "label": "Default taxes", + "placeholder": "Select default taxes", + "description": "The default tax rates for the customer" }, "language_code": { "label": "Language", @@ -151,8 +156,8 @@ "title": "Contact information", "description": "Customer contact details" }, - "additional_config": { - "title": "Additional settings", + "preferences": { + "title": "Preferences", "description": "Additional customer configurations" } }, diff --git a/modules/customers/src/common/locales/es.json b/modules/customers/src/common/locales/es.json index 70a8a0e4..fe629cc9 100644 --- a/modules/customers/src/common/locales/es.json +++ b/modules/customers/src/common/locales/es.json @@ -21,6 +21,11 @@ "title": "Nuevo cliente", "description": "Crear un nuevo cliente", "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": { @@ -119,7 +124,7 @@ "placeholder": "Ingrese la URL del sitio web", "description": "El sitio web del cliente" }, - "default_tax": { + "default_taxes": { "label": "Impuesto por defecto", "placeholder": "Seleccione el impuesto por defecto", "description": "La tasa de impuesto por defecto para el cliente" @@ -153,8 +158,8 @@ "title": "Información de contacto", "description": "Detalles de contacto del cliente" }, - "additional_config": { - "title": "Configuración adicional", + "preferences": { + "title": "Preferencias", "description": "Configuraciones adicionales del cliente" } }, diff --git a/modules/customers/src/web/components/form-debug.tsx b/modules/customers/src/web/components/form-debug.tsx index c5be808d..e2f62033 100644 --- a/modules/customers/src/web/components/form-debug.tsx +++ b/modules/customers/src/web/components/form-debug.tsx @@ -9,7 +9,7 @@ export const FormDebug = ({ form }: { form: UseFormReturn }) => { const currentValues = watch(); return ( -
+

¿Formulario modificado? {isDirty ? "Sí" : "No"}

diff --git a/modules/customers/src/web/pages/create/create.tsx b/modules/customers/src/web/pages/create/create.tsx index b9a3a771..6b4a61bf 100644 --- a/modules/customers/src/web/pages/create/create.tsx +++ b/modules/customers/src/web/pages/create/create.tsx @@ -53,8 +53,8 @@ export const CustomerCreate = () => { <> -
-
+
+

{t("pages.create.title")}

diff --git a/modules/customers/src/web/pages/update/customer-additional-config-fields.tsx b/modules/customers/src/web/pages/update/customer-additional-config-fields.tsx index f5bbee99..e1ff0fcb 100644 --- a/modules/customers/src/web/pages/update/customer-additional-config-fields.tsx +++ b/modules/customers/src/web/pages/update/customer-additional-config-fields.tsx @@ -7,17 +7,19 @@ import { CardHeader, CardTitle, } from "@repo/shadcn-ui/components"; +import { useForm } from "react-hook-form"; import { CURRENCY_OPTIONS, LANGUAGE_OPTIONS } from "../../constants"; import { useTranslation } from "../../i18n"; -export function CustomerAdditionalConfigFields({ control }: { control: any }) { +export const CustomerAdditionalConfigFields = () => { const { t } = useTranslation(); + const { control } = useForm(); return ( - + - {t("form_groups.additional_config.title")} - {t("form_groups.additional_config.description")} + {t("form_groups.preferences.title")} + {t("form_groups.preferences.description")} ); -} +}; diff --git a/modules/customers/src/web/pages/update/customer-basic-info-fields.tsx b/modules/customers/src/web/pages/update/customer-basic-info-fields.tsx index 3f3797b2..3eedb5f4 100644 --- a/modules/customers/src/web/pages/update/customer-basic-info-fields.tsx +++ b/modules/customers/src/web/pages/update/customer-basic-info-fields.tsx @@ -1,3 +1,4 @@ +import { TaxesMultiSelectField } from "@erp/core/components"; import { TextField } from "@repo/rdx-ui/components"; import { Card, @@ -15,11 +16,183 @@ import { } from "@repo/shadcn-ui/components"; import { useTranslation } from "../../i18n"; -export function CustomerBasicInfoFields({ control }: { control: any }) { +export const CustomerBasicInfoFields = ({ control }: { control: any }) => { const { t } = useTranslation(); return ( - + + + Identificación + + +
+
+ ( + + {t("form_fields.customer_type.label")} + + + + + + + + {t("form_fields.customer_type.company")} + + + + + + + + {t("form_fields.customer_type.individual")} + + + + + + + )} + /> +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+
+
+
+ ); + + return ( +
+
+

+ {t("form_groups.basic_info.title")} +

+

+ {t("form_groups.basic_info.description")} +

+ +
+
+ ( + + {t("form_fields.customer_type.label")} + + + + + + + + {t("form_fields.customer_type.company")} + + + + + + + + {t("form_fields.customer_type.individual")} + + + + + + + )} + /> +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+
+ ); + + return ( + {t("form_groups.basic_info.title")} {t("form_groups.basic_info.description")} @@ -85,4 +258,4 @@ export function CustomerBasicInfoFields({ control }: { control: any }) { ); -} +}; diff --git a/modules/customers/src/web/pages/update/customer-contact-fields.tsx b/modules/customers/src/web/pages/update/customer-contact-fields.tsx index 2834def1..621454d9 100644 --- a/modules/customers/src/web/pages/update/customer-contact-fields.tsx +++ b/modules/customers/src/web/pages/update/customer-contact-fields.tsx @@ -1,15 +1,28 @@ -import { TextField } from "@repo/rdx-ui/components"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, + Collapsible, + CollapsibleContent, + CollapsibleTrigger, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, } 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"; export function CustomerContactFields({ control }: { control: any }) { const { t } = useTranslation(); + const [open, setOpen] = useState(true); return ( @@ -18,6 +31,63 @@ export function CustomerContactFields({ control }: { control: any }) { {t("form_groups.contact_info.description")} + + + Más detalles{" "} + + + +
+ ( + + Teléfono secundario + + } + {...field} + /> + + + + )} + /> + ( + + Móvil secundario + + } + {...field} + /> + + + + )} + /> + ( + + Fax + + + + + + )} + /> +
+
+
+ void; + onSubmit: (data: CustomerUpdateData) => void; onError: (errors: FieldErrors) => void; - errorMessage?: string; // ✅ prop nueva para mostrar error global } -export const CustomerEditForm = ({ - formId, - defaultValues, - onSubmit, - isPending, - errorMessage, -}: CustomerFormProps) => { +export const CustomerEditForm = ({ defaultValues, onSubmit, isPending }: CustomerFormProps) => { const { t } = useTranslation(); const form = useForm({ @@ -36,14 +37,85 @@ export const CustomerEditForm = ({ disabled: isPending, }); + const { + watch, + formState: { isDirty, dirtyFields }, + } = form; + useUnsavedChangesNotifier({ - isDirty: form.formState.isDirty, + isDirty, }); + const currentValues = watch(); + + const handleSubmit = (data: CustomerUpdateData) => { + console.log("Datos del formulario:", data); + const changedData: Record = {}; + + 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) => { + 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 ( +
+ + +
+
+ + + {t("form_groups.basic_info.title")} + {t("form_groups.basic_info.description")} + + + + + +

 

+
+
+
+
+ +
+
+ + + ); + + return ( +
+ +
diff --git a/modules/customers/src/web/pages/update/update.tsx b/modules/customers/src/web/pages/update/update.tsx index da23a87b..21fa770d 100644 --- a/modules/customers/src/web/pages/update/update.tsx +++ b/modules/customers/src/web/pages/update/update.tsx @@ -94,8 +94,8 @@ export const CustomerUpdate = () => { <> -
-
+
+

{t("pages.update.title")}

@@ -132,11 +132,10 @@ export const CustomerUpdate = () => {
{/* Importante: proveemos un formId para que el botón del header pueda hacer submit */}
diff --git a/packages/rdx-ddd/src/value-objects/utc-date.ts b/packages/rdx-ddd/src/value-objects/utc-date.ts index 8eebddb2..1656919f 100644 --- a/packages/rdx-ddd/src/value-objects/utc-date.ts +++ b/packages/rdx-ddd/src/value-objects/utc-date.ts @@ -71,6 +71,17 @@ export class UtcDate extends ValueObject { 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. */ diff --git a/packages/rdx-ui/src/components/form/TextField.tsx b/packages/rdx-ui/src/components/form/TextField.tsx index f0ddef74..11ceef88 100644 --- a/packages/rdx-ui/src/components/form/TextField.tsx +++ b/packages/rdx-ui/src/components/form/TextField.tsx @@ -53,7 +53,12 @@ export function TextField({
)} - +

diff --git a/packages/shadcn-ui/src/components/input.tsx b/packages/shadcn-ui/src/components/input.tsx index 91a9d83b..64053b9f 100644 --- a/packages/shadcn-ui/src/components/input.tsx +++ b/packages/shadcn-ui/src/components/input.tsx @@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) { type={type} data-slot='input' 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", "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", diff --git a/packages/shadcn-ui/src/styles/globals.css b/packages/shadcn-ui/src/styles/globals.css index 37c13160..60d38ddb 100644 --- a/packages/shadcn-ui/src/styles/globals.css +++ b/packages/shadcn-ui/src/styles/globals.css @@ -50,9 +50,9 @@ } @theme inline { - /*--font-sans: Geist, sans-serif; - --font-serif: Merriweather, serif; - --font-mono: "Geist Mono", monospace;*/ + --font-sans: Roboto, sans-serif; + --font-serif: Domine, serif; + --font-mono: "Roboto Mono", monospace; --color-background: var(--background); --color-foreground: var(--foreground); @@ -92,10 +92,10 @@ } :root { - --radius: 0.5rem; + --radius: 0.3rem; --background: oklch(1.0 0.0 0); --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); --popover: oklch(1.0 0.0 0); --popover-foreground: oklch(0.143 0.003 271.9282674829111); @@ -165,16 +165,13 @@ @apply border-border outline-ring/50; @apply transition-colors duration-300; /* Added transition for smooth color changes */ } + body { @apply bg-background text-foreground; } input { - @apply font-semibold; - } - - label { - @apply font-light; + @apply bg-input; } }

Importe neto   {{subtotal_amount}}
Descuento 0%Descuento {{discount_percentage}}   {{discount_amount.value}}