diff --git a/modules/core/src/api/infrastructure/express/express-controller.ts b/modules/core/src/api/infrastructure/express/express-controller.ts index 81141bb1..6e1e2939 100644 --- a/modules/core/src/api/infrastructure/express/express-controller.ts +++ b/modules/core/src/api/infrastructure/express/express-controller.ts @@ -34,9 +34,6 @@ export abstract class ExpressController { } satisfies ApiErrorContext; const body = toProblemJson(apiError, ctx); - - console.trace(body); - return res.type("application/problem+json").status(apiError.status).json(body); } diff --git a/modules/core/src/web/hooks/index.ts b/modules/core/src/web/hooks/index.ts index be28752e..47fd0604 100644 --- a/modules/core/src/web/hooks/index.ts +++ b/modules/core/src/web/hooks/index.ts @@ -3,3 +3,4 @@ export * from "./use-pagination"; export * from "./use-query-key"; export * from "./use-toggle"; export * from "./use-unsaved-changes-notifier"; +export * from "./use-url-param-id"; diff --git a/modules/core/src/web/hooks/use-url-param-id.ts b/modules/core/src/web/hooks/use-url-param-id.ts new file mode 100644 index 00000000..401ea693 --- /dev/null +++ b/modules/core/src/web/hooks/use-url-param-id.ts @@ -0,0 +1,6 @@ +import { useParams } from "react-router-dom"; + +export const useUrlParamId = (): string | undefined => { + const { id } = useParams<{ id?: string }>(); + return id; +}; diff --git a/modules/customer-invoices/src/api/application/use-cases/report/reporter/templates/customer-invoice/logo1.jpg b/modules/customer-invoices/src/api/application/use-cases/report/reporter/templates/customer-invoice/logo1.jpg new file mode 100755 index 00000000..75df86fa Binary files /dev/null and b/modules/customer-invoices/src/api/application/use-cases/report/reporter/templates/customer-invoice/logo1.jpg differ diff --git a/modules/customer-invoices/src/api/application/use-cases/report/reporter/templates/customer-invoice/template copy.hbs b/modules/customer-invoices/src/api/application/use-cases/report/reporter/templates/customer-invoice/template copy.hbs new file mode 100644 index 00000000..a5f683cf --- /dev/null +++ b/modules/customer-invoices/src/api/application/use-cases/report/reporter/templates/customer-invoice/template copy.hbs @@ -0,0 +1,254 @@ + + + + + + Factura #{{id}} + + + + + + +
+ +
+ + + + + + + {{#if any_item_has_discount}} + + {{/if}} + + + + + {{#each items}} + + + + + {{#if ../any_item_has_discount}} + + {{/if}} + + + {{/each}} + + + + + + +
ConceptoCantidadPrecio unidadDto (%)Importe total
+
{{description}}
+ {{#if note}}
{{note}}
{{/if}} +
{{quantity}}{{unit_price}}{{discount}}{{total_price}}
* Precios en {{currency}}.
+
+ + +
+ + + + + +
+ + + +
+ + + + {{!-- 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.) + --}} + + + \ No newline at end of file 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 99052b75..49f5d87f 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 @@ -1,24 +1,87 @@ - + + - + - Presupuesto #{{id}} + Factura F26200 + + + + + +
+
+ + + + + + + + + + + + + {{#each items}} + + + + + + + + + {{/each}} + +
Cant.DescripciónPrec. UnitarioSubtotalDto (%)Importe total
{{quantity}}{{description}}{{unit_price}}{{subtotal_price}}{{discount}}{{total_price}}
+
+ +
+ +
+
+

Forma de pago: {{payment_method}}

+
+
+

Notas: {{notes}}

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Importe neto{{subtotal_price}}
% Descuento{{discount.amount}}{{discount_price}}
Base imponible{{before_tax_price}}
% IVA{{tax}}{{tax_price}}
Importe total{{total_price}}
+
+
+ +
+ + + + + \ No newline at end of file diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/queries/invoice-recipient.list.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/queries/invoice-recipient.list.mapper.ts index f707daf2..d5637b06 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/queries/invoice-recipient.list.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/queries/invoice-recipient.list.mapper.ts @@ -6,16 +6,12 @@ import { Province, Street, TINNumber, + ValidationErrorDetail, + extractOrPushError, maybeFromNullableVO, } from "@repo/rdx-ddd"; -import { - IQueryMapperWithBulk, - MapperParamsType, - SequelizeQueryMapper, - ValidationErrorDetail, - extractOrPushError, -} from "@erp/core/api"; +import { IQueryMapperWithBulk, MapperParamsType, SequelizeQueryMapper } from "@erp/core/api"; import { Result } from "@repo/rdx-utils"; import { InvoiceRecipient } from "../../../domain"; diff --git a/modules/customer-invoices/src/common/dto/request/index.ts b/modules/customer-invoices/src/common/dto/request/index.ts index 46c8ddd5..40defeed 100644 --- a/modules/customer-invoices/src/common/dto/request/index.ts +++ b/modules/customer-invoices/src/common/dto/request/index.ts @@ -3,3 +3,4 @@ export * from "./customer-invoices-list.request.dto"; export * from "./delete-customer-invoice-by-id.request.dto"; export * from "./get-customer-invoice-by-id.request.dto"; export * from "./report-customer-invoice-by-id.request.dto"; +export * from "./update-customer-invoice-by-id.request.dto"; diff --git a/modules/customer-invoices/src/common/dto/request/update-customer-invoice-by-id.request.dto.ts b/modules/customer-invoices/src/common/dto/request/update-customer-invoice-by-id.request.dto.ts new file mode 100644 index 00000000..3457831c --- /dev/null +++ b/modules/customer-invoices/src/common/dto/request/update-customer-invoice-by-id.request.dto.ts @@ -0,0 +1,9 @@ +import * as z from "zod/v4"; + +export const UpdateCustomerInvoiceByIdParamsRequestSchema = z.object({ + customer_id: z.string(), +}); + +export const UpdateCustomerByIdRequestSchema = z.object({}); + +export type UpdateCustomerByIdRequestDTO = z.infer; diff --git a/modules/customer-invoices/src/common/dto/request/update-customer-invoice.request.dto.ts b/modules/customer-invoices/src/common/dto/request/update-customer-invoice.request.dto.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/modules/customer-invoices/src/common/index.ts b/modules/customer-invoices/src/common/index.ts new file mode 100644 index 00000000..0392b1b4 --- /dev/null +++ b/modules/customer-invoices/src/common/index.ts @@ -0,0 +1 @@ +export * from "./dto"; diff --git a/modules/customers/src/api/application/presenters/queries/list-customers.presenter.ts b/modules/customers/src/api/application/presenters/queries/list-customers.presenter.ts index b151811b..8e5b89ab 100644 --- a/modules/customers/src/api/application/presenters/queries/list-customers.presenter.ts +++ b/modules/customers/src/api/application/presenters/queries/list-customers.presenter.ts @@ -3,7 +3,7 @@ import { CustomerListDTO } from "@erp/customer-invoices/api/infrastructure"; import { Criteria } from "@repo/rdx-criteria/server"; import { toEmptyString } from "@repo/rdx-ddd"; import { Collection } from "@repo/rdx-utils"; -import { CustomerListResponsetDTO } from "../../../../common/dto"; +import { ListCustomersResponseDTO } from "../../../../common/dto"; export class ListCustomersPresenter extends Presenter { protected _mapCustomer(customer: CustomerListDTO) { @@ -54,7 +54,7 @@ export class ListCustomersPresenter extends Presenter { toOutput(params: { customers: Collection; criteria: Criteria; - }): CustomerListResponsetDTO { + }): ListCustomersResponseDTO { const { customers, criteria } = params; const items = customers.map((customer) => this._mapCustomer(customer)); diff --git a/modules/customers/src/api/application/use-cases/list-customers.use-case.ts b/modules/customers/src/api/application/use-cases/list-customers.use-case.ts index 09e03377..66f81aba 100644 --- a/modules/customers/src/api/application/use-cases/list-customers.use-case.ts +++ b/modules/customers/src/api/application/use-cases/list-customers.use-case.ts @@ -3,7 +3,7 @@ import { Criteria } from "@repo/rdx-criteria/server"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { Transaction } from "sequelize"; -import { CustomerListResponsetDTO } from "../../../common/dto"; +import { ListCustomersResponseDTO } from "../../../common/dto"; import { CustomerService } from "../../domain"; import { ListCustomersPresenter } from "../presenters"; @@ -21,7 +21,7 @@ export class ListCustomersUseCase { public execute( params: ListCustomersUseCaseInput - ): Promise> { + ): Promise> { const { criteria, companyId } = params; const presenter = this.presenterRegistry.getPresenter({ resource: "customer", diff --git a/modules/customers/src/api/infrastructure/express/controllers/update-customer.controller.ts b/modules/customers/src/api/infrastructure/express/controllers/update-customer.controller.ts index f3d69400..54097941 100644 --- a/modules/customers/src/api/infrastructure/express/controllers/update-customer.controller.ts +++ b/modules/customers/src/api/infrastructure/express/controllers/update-customer.controller.ts @@ -1,5 +1,5 @@ import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; -import { UpdateCustomerRequestDTO } from "../../../../common/dto"; +import { UpdateCustomerByIdRequestDTO } from "../../../../common/dto"; import { UpdateCustomerUseCase } from "../../../application"; export class UpdateCustomerController extends ExpressController { @@ -15,7 +15,7 @@ export class UpdateCustomerController extends ExpressController { return this.forbiddenError("Tenant ID not found"); } const { customer_id } = this.req.params; - const dto = this.req.body as UpdateCustomerRequestDTO; + const dto = this.req.body as UpdateCustomerByIdRequestDTO; const result = await this.useCase.execute({ customer_id, companyId, dto }); 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 e97dcb4e..7cbb53b0 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 UpdateCustomerRequestDTO = z.infer; +export type UpdateCustomerByIdRequestDTO = z.infer; diff --git a/modules/customers/src/common/dto/response/index.ts b/modules/customers/src/common/dto/response/index.ts index 4f76f304..2f08346a 100644 --- a/modules/customers/src/common/dto/response/index.ts +++ b/modules/customers/src/common/dto/response/index.ts @@ -1,4 +1,4 @@ export * from "./create-customer.result.dto"; -export * from "./customer-list.response.dto"; export * from "./get-customer-by-id.response.dto"; +export * from "./list-customers.response.dto"; export * from "./update-customer-by-id.response.dto"; diff --git a/modules/customers/src/common/dto/response/customer-list.response.dto.ts b/modules/customers/src/common/dto/response/list-customers.response.dto.ts similarity index 81% rename from modules/customers/src/common/dto/response/customer-list.response.dto.ts rename to modules/customers/src/common/dto/response/list-customers.response.dto.ts index bc4e63b1..65d068aa 100644 --- a/modules/customers/src/common/dto/response/customer-list.response.dto.ts +++ b/modules/customers/src/common/dto/response/list-customers.response.dto.ts @@ -1,7 +1,7 @@ import { MetadataSchema, createListViewResponseSchema } from "@erp/core"; import * as z from "zod/v4"; -export const CustomerListResponseSchema = createListViewResponseSchema( +export const ListCustomersResponseSchema = createListViewResponseSchema( z.object({ id: z.uuid(), company_id: z.uuid(), @@ -34,4 +34,4 @@ export const CustomerListResponseSchema = createListViewResponseSchema( }) ); -export type CustomerListResponsetDTO = z.infer; +export type ListCustomersResponseDTO = z.infer; diff --git a/modules/customers/src/common/dto/response/update-customer-by-id.response.dto.ts b/modules/customers/src/common/dto/response/update-customer-by-id.response.dto.ts index 9d1718bc..24551e70 100644 --- a/modules/customers/src/common/dto/response/update-customer-by-id.response.dto.ts +++ b/modules/customers/src/common/dto/response/update-customer-by-id.response.dto.ts @@ -18,15 +18,19 @@ export const UpdateCustomerByIdResponseSchema = z.object({ postal_code: z.string(), country: z.string(), - email: z.string(), - phone: z.string(), + email_primary: z.string(), + email_secondary: z.string(), + phone_primary: z.string(), + phone_secondary: z.string(), + mobile_primary: z.string(), + mobile_secondary: z.string(), + fax: z.string(), website: z.string(), legal_record: z.string(), default_taxes: z.string(), - status: z.string(), language_code: z.string(), currency_code: z.string(), diff --git a/modules/customers/src/common/locales/en.json b/modules/customers/src/common/locales/en.json index 868303b3..57acba91 100644 --- a/modules/customers/src/common/locales/en.json +++ b/modules/customers/src/common/locales/en.json @@ -10,7 +10,11 @@ "name": "Name", "trade_name": "Trade name", "status": "Status", - "email": "Email" + "email": "Email", + "phone": "Phone", + "city": "City", + "tin": "TIN", + "mobile": "Mobile" } }, "create": { @@ -71,15 +75,37 @@ "placeholder": "Select country", "description": "The country of the customer" }, - "email": { - "label": "Email", - "placeholder": "Enter email", - "description": "The email address of the customer" + "email_primary": { + "label": "Primary email", + "placeholder": "Enter primary email", + "description": "The primary email address of the customer" }, - "phone": { - "label": "Phone", - "placeholder": "Enter phone number", - "description": "The phone number of the customer" + "email_secondary": { + "label": "Secondary email", + "placeholder": "Enter secondary email", + "description": "The secondary email address of the customer" + }, + + "phone_primary": { + "label": "Primary phone", + "placeholder": "Enter primary phone number", + "description": "The primary phone number of the customer" + }, + "phone_secondary": { + "label": "Secondary phone", + "placeholder": "Enter secondary phone number ", + "description": "The secondary phone number of the customer" + }, + + "mobile_primary": { + "label": "Primary mobile", + "placeholder": "Enter primary mobile number", + "description": "The primary mobile number of the customer" + }, + "mobile_secondary": { + "label": "Secondary mobile", + "placeholder": "Enter secondary mobile number", + "description": "The secondary mobile number of the customer" }, "fax": { "label": "Fax", diff --git a/modules/customers/src/common/locales/es.json b/modules/customers/src/common/locales/es.json index 9001fa7a..88cb2ad9 100644 --- a/modules/customers/src/common/locales/es.json +++ b/modules/customers/src/common/locales/es.json @@ -10,7 +10,11 @@ "name": "Nombre", "trade_name": "Nombre comercial", "status": "Estado", - "email": "Correo electrónico" + "email": "Correo electrónico", + "phone": "Teléfono", + "city": "Ciudad", + "tin": "Nº Id.", + "mobile": "Móvil" } }, "create": { @@ -71,16 +75,40 @@ "placeholder": "Seleccione el país", "description": "El país del cliente" }, - "email": { - "label": "Correo electrónico", + + "email_primary": { + "label": "Email principal", "placeholder": "Ingrese el correo electrónico", - "description": "La dirección de correo electrónico del cliente" + "description": "La dirección de correo electrónico principal del cliente" }, - "phone": { + "email_secondary": { + "label": "Email secundario", + "placeholder": "Ingrese el correo electrónico", + "description": "La dirección de correo electrónico secundario del clientºe" + }, + + "phone_primary": { "label": "Teléfono", "placeholder": "Ingrese el número de teléfono", "description": "El número de teléfono del cliente" }, + "phone_secondary": { + "label": "Teléfono secundario", + "placeholder": "Ingrese el número de teléfono secundario", + "description": "El número de teléfono secundario del cliente" + }, + + "mobile_primary": { + "label": "Teléfono", + "placeholder": "Ingrese el número de teléfono", + "description": "El número de teléfono del cliente" + }, + "mobile_secondary": { + "label": "Teléfono secundario", + "placeholder": "Ingrese el número de teléfono secundario", + "description": "El número de teléfono secundario del cliente" + }, + "fax": { "label": "Fax", "placeholder": "Ingrese el número de fax", diff --git a/modules/customers/src/web/components/client-selector.tsx b/modules/customers/src/web/components/client-selector.tsx index 97854bc4..551d9e2d 100644 --- a/modules/customers/src/web/components/client-selector.tsx +++ b/modules/customers/src/web/components/client-selector.tsx @@ -18,10 +18,10 @@ import { } from "@repo/shadcn-ui/components"; import { Building, Calendar, Mail, MapPin, Phone, Plus, User } from "lucide-react"; import { useState } from "react"; -import { CustomerListResponsetDTO } from "../../common"; +import { ListCustomersResponseDTO } from "../../common"; import { useCustomersQuery } from "../hooks"; -type Customer = CustomerListResponsetDTO["items"][number]; +type Customer = ListCustomersResponseDTO["items"][number]; const columns: TableColumn[] = [ { diff --git a/modules/customers/src/web/components/customers-list-grid.tsx b/modules/customers/src/web/components/customers-list-grid.tsx index d85d08af..d1698160 100644 --- a/modules/customers/src/web/components/customers-list-grid.tsx +++ b/modules/customers/src/web/components/customers-list-grid.tsx @@ -1,59 +1,124 @@ -import { useState } from "react"; - import { AG_GRID_LOCALE_ES } from "@ag-grid-community/locale"; -// Grid -import type { ColDef, GridOptions, ValueFormatterParams } from "ag-grid-community"; -import { AllCommunityModule, ModuleRegistry } from "ag-grid-community"; +import type { ValueFormatterParams } from "ag-grid-community"; +import { + AllCommunityModule, + ColDef, + GridOptions, + ModuleRegistry, + SizeColumnsToContentStrategy, + SizeColumnsToFitGridStrategy, + SizeColumnsToFitProvidedWidthStrategy, +} from "ag-grid-community"; +import { useMemo, useState } from "react"; -ModuleRegistry.registerModules([AllCommunityModule]); - -// Core CSS +import { Button } from "@repo/shadcn-ui/components"; import { AgGridReact } from "ag-grid-react"; +import { ChevronRightIcon } from "lucide-react"; +import { useNavigate } from "react-router-dom"; import { useCustomersQuery } from "../hooks"; import { useTranslation } from "../i18n"; import { CustomerStatusBadge } from "./customer-status-badge"; +ModuleRegistry.registerModules([AllCommunityModule]); + // Create new GridExample component export const CustomersListGrid = () => { const { t } = useTranslation(); - const { data, isLoading, isPending, isError, error } = useCustomersQuery({}); + const navigate = useNavigate(); + + const { + data: customersData, + isLoading: isLoadingCustomers, + isError: isLoadError, + error: loadError, + } = useCustomersQuery(); // Column Definitions: Defines & controls grid columns. const [colDefs] = useState([ + { field: "name", headerName: t("pages.list.grid_columns.name"), minWidth: 300 }, + { + field: "tin", + headerName: t("pages.list.grid_columns.tin"), + maxWidth: 120, + }, + { + field: "city", + headerName: t("pages.list.grid_columns.city"), + }, + { + field: "email_primary", + headerName: t("pages.list.grid_columns.email"), + }, + { + field: "phone_primary", + headerName: t("pages.list.grid_columns.phone"), + maxWidth: 120, + }, + + { + field: "mobile_primary", + headerName: t("pages.list.grid_columns.mobile"), + maxWidth: 120, + }, { field: "status", - headerName: t("pages.list.grid_columns.status"), + maxWidth: 125, cellRenderer: (params: ValueFormatterParams) => { return ; }, }, - - { field: "name", headerName: t("pages.list.grid_columns.name") }, - { field: "trade_name", headerName: t("pages.list.grid_columns.trade_name") }, - { - field: "email", - headerName: t("pages.list.grid_columns.email"), + colId: "actions", + headerName: t("pages.list.grid_columns.actions", "Actions"), + cellRenderer: (params: ValueFormatterParams) => { + const { data } = params; + return ( + + ); + }, }, ]); - const gridOptions: GridOptions = { - columnDefs: colDefs, - defaultColDef: { - editable: true, - flex: 1, - minWidth: 100, - filter: false, - sortable: false, - resizable: true, - }, - pagination: true, - paginationPageSize: 10, - paginationPageSizeSelector: [10, 20, 30, 50], - localeText: AG_GRID_LOCALE_ES, - rowSelection: { mode: "multiRow" }, - }; + const autoSizeStrategy = useMemo< + | SizeColumnsToFitGridStrategy + | SizeColumnsToFitProvidedWidthStrategy + | SizeColumnsToContentStrategy + >(() => { + return { + type: "fitGridWidth", + defaultMinWidth: 100, + columnLimits: [{ colId: "actions", minWidth: 75, maxWidth: 75 }], + }; + }, []); + + const gridOptions: GridOptions = useMemo( + () => ({ + columnDefs: colDefs, + autoSizeStrategy: autoSizeStrategy, + defaultColDef: { + editable: false, + flex: 1, + filter: false, + sortable: false, + resizable: true, + }, + pagination: true, + paginationPageSize: 10, + paginationPageSizeSelector: [10, 20, 30, 50], + localeText: AG_GRID_LOCALE_ES, + }), + [autoSizeStrategy, colDefs] + ); // Container: Defines the grid's theme & dimensions. return ( @@ -64,7 +129,11 @@ export const CustomersListGrid = () => { width: "100%", }} > - + ); }; diff --git a/modules/customers/src/web/customer-routes.tsx b/modules/customers/src/web/customer-routes.tsx index 816a99cb..0d527300 100644 --- a/modules/customers/src/web/customer-routes.tsx +++ b/modules/customers/src/web/customer-routes.tsx @@ -1,6 +1,7 @@ import { ModuleClientParams } from "@erp/core/client"; import { lazy } from "react"; import { Outlet, RouteObject } from "react-router-dom"; +import { CustomerUpdate } from "./pages/update"; // Lazy load components const CustomersLayout = lazy(() => @@ -43,6 +44,7 @@ export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => { { path: "", index: true, element: }, // index { path: "list", element: }, { path: "create", element: }, + { path: ":id/edit", element: }, // /*{ path: "create", element: }, diff --git a/modules/customers/src/web/hooks/index.ts b/modules/customers/src/web/hooks/index.ts index ad82e29e..e7126993 100644 --- a/modules/customers/src/web/hooks/index.ts +++ b/modules/customers/src/web/hooks/index.ts @@ -1 +1,5 @@ +export * from "./use-create-customer-mutation"; +export * from "./use-customer-query"; +export * from "./use-customers-context"; export * from "./use-customers-query"; +export * from "./use-update-customer-mutation"; diff --git a/modules/customers/src/web/hooks/use-create-customer-mutation.ts b/modules/customers/src/web/hooks/use-create-customer-mutation.ts index c373d8f2..6275d3b8 100644 --- a/modules/customers/src/web/hooks/use-create-customer-mutation.ts +++ b/modules/customers/src/web/hooks/use-create-customer-mutation.ts @@ -1,13 +1,14 @@ import { useDataSource, useQueryKey } from "@erp/core/hooks"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { UpdateCustomerRequestDTO } from "../../common/dto"; +import { UpdateCustomerByIdRequestDTO } from "../../common/dto"; export const useCreateCustomerMutation = () => { const queryClient = useQueryClient(); const dataSource = useDataSource(); const keys = useQueryKey(); - return useMutation>({ + return useMutation>({ + mutationKey: ["customer:create"], mutationFn: (data) => { console.log(data); return dataSource.createOne("customers", data); diff --git a/modules/customers/src/web/hooks/use-customer-query.ts b/modules/customers/src/web/hooks/use-customer-query.ts new file mode 100644 index 00000000..8b13575a --- /dev/null +++ b/modules/customers/src/web/hooks/use-customer-query.ts @@ -0,0 +1,40 @@ +import { useDataSource } from "@erp/core/hooks"; +import { GetCustomerByIdResponseDTO } from "@erp/customer-invoices/common"; +import { type QueryKey, type UseQueryOptions, useQuery } from "@tanstack/react-query"; + +export const CUSTOMER_QUERY_KEY = (id: string): QueryKey => ["customer", id] as const; + +type Options = Omit< + UseQueryOptions< + GetCustomerByIdResponseDTO, + Error, + GetCustomerByIdResponseDTO, + ReturnType + >, + "queryKey" | "queryFn" | "enabled" +> & { + enabled?: boolean; +}; + +export function useCustomerQuery(customerId?: string, options?: Options) { + const dataSource = useDataSource(); + const enabled = (options?.enabled ?? true) && !!customerId; + + return useQuery< + GetCustomerByIdResponseDTO, + Error, + GetCustomerByIdResponseDTO, + ReturnType + >({ + queryKey: CUSTOMER_QUERY_KEY(customerId ?? "unknown"), + enabled, + queryFn: async (context) => { + if (!customerId) throw new Error("customerId is required"); + + const { signal } = context; + const customer = await dataSource.getOne("customers", customerId); + return customer as GetCustomerByIdResponseDTO; + }, + ...options, + }); +} diff --git a/modules/customers/src/web/hooks/use-customers-query.tsx b/modules/customers/src/web/hooks/use-customers-query.tsx index e0281d8c..413ab111 100644 --- a/modules/customers/src/web/hooks/use-customers-query.tsx +++ b/modules/customers/src/web/hooks/use-customers-query.tsx @@ -1,21 +1,22 @@ import { useDataSource, useQueryKey } from "@erp/core/hooks"; +import { ListCustomersResponseDTO } from "@erp/customer-invoices/common"; import { useQuery } from "@tanstack/react-query"; -import { CustomerListResponsetDTO } from "../../common/dto"; // Obtener todas las facturas -export const useCustomersQuery = (params: any) => { +export const useCustomersQuery = (params?: any) => { const dataSource = useDataSource(); const keys = useQueryKey(); - return useQuery({ + return useQuery({ queryKey: keys().data().resource("customers").action("list").params(params).get(), - queryFn: (context) => { - console.log(dataSource.getBaseUrl()); + queryFn: async (context) => { const { signal } = context; - return dataSource.getList("customers", { + const customers = await dataSource.getList("customers", { signal, ...params, }); + + return customers as ListCustomersResponseDTO; }, }); }; diff --git a/modules/customers/src/web/hooks/use-customers.bak b/modules/customers/src/web/hooks/use-customers.bak deleted file mode 100644 index b4bfb751..00000000 --- a/modules/customers/src/web/hooks/use-customers.bak +++ /dev/null @@ -1,75 +0,0 @@ -import { useDataSource, useQueryKey } from "@erp/core/hooks"; -import { IListCustomersResponseDTO } from "@erp/customers/common/dto"; - -export type UseCustomersListParams = Omit & { - status?: string; - enabled?: boolean; - queryOptions?: Record; -}; - -export type UseCustomersListResponse = UseListQueryResult< - IListResponseDTO, - unknown ->; - -export type UseCustomersGetParamsType = { - enabled?: boolean; - queryOptions?: Record; -}; - -export type UseCustomersReportParamsType = { - enabled?: boolean; - queryOptions?: Record; -}; - -export const useCustomers = () => { - const actions = { - /** - * Hook para obtener la lista de facturas - * @param params - Parámetros para la consulta de la lista de facturas - * @returns - Respuesta de la consulta de la lista de facturas - */ - useList: (params: UseCustomersListParams): UseCustomersListResponse => { - const dataSource = useDataSource(); - const keys = useQueryKey(); - - const { - pagination, - status = "draft", - quickSearchTerm = undefined, - enabled = true, - queryOptions, - } = params; - - return useList({ - queryKey: keys().data().resource("customers").action("list").params(params).get(), - queryFn: () => { - return dataSource.getList({ - resource: "customers", - quickSearchTerm, - filters: - status !== "all" - ? [ - { - field: "status", - operator: "eq", - value: status, - }, - ] - : [ - { - field: "status", - operator: "ne", - value: "archived", - }, - ], - pagination, - }); - }, - enabled, - queryOptions, - }); - }, - }; - return actions; -}; diff --git a/modules/customers/src/web/hooks/use-update-customer-mutation.ts b/modules/customers/src/web/hooks/use-update-customer-mutation.ts new file mode 100644 index 00000000..a1b9089a --- /dev/null +++ b/modules/customers/src/web/hooks/use-update-customer-mutation.ts @@ -0,0 +1,38 @@ +import { useDataSource } from "@erp/core/hooks"; +import { + UpdateCustomerByIdRequestDTO, + UpdateCustomerByIdResponseDTO, +} from "@erp/customer-invoices/common"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { CUSTOMER_QUERY_KEY } from "./use-customer-query"; + +export const CUSTOMERS_LIST_KEY = ["customers"] as const; + +type MutationDeps = {}; + +export function useUpdateCustomerMutation(customerId: string, deps?: MutationDeps) { + const queryClient = useQueryClient(); + const dataSource = useDataSource(); + + return useMutation({ + mutationKey: ["customer:update", customerId], + mutationFn: async (input) => { + if (!customerId) throw new Error("customerId is required"); + const updated = await dataSource.updateOne("customers", customerId, input); + return updated as UpdateCustomerByIdResponseDTO; + }, + onSuccess: (updated) => { + // Refresca inmediatamente el detalle + queryClient.setQueryData( + CUSTOMER_QUERY_KEY(customerId), + updated + ); + + // Otra opción es invalidar el detalle para forzar refetch: + // queryClient.invalidateQueries({ queryKey: CUSTOMER_QUERY_KEY(customerId) }); + + // Invalida el listado para refrescar desde servidor + queryClient.invalidateQueries({ queryKey: CUSTOMERS_LIST_KEY }); + }, + }); +} diff --git a/modules/customers/src/web/pages/create/create.tsx b/modules/customers/src/web/pages/create/create.tsx index 85cf1436..b9a3a771 100644 --- a/modules/customers/src/web/pages/create/create.tsx +++ b/modules/customers/src/web/pages/create/create.tsx @@ -1,4 +1,4 @@ -import { AppBreadcrumb, AppContent, BackHistoryButton } from "@repo/rdx-ui/components"; +import { AppBreadcrumb, AppContent, BackHistoryButton, ButtonGroup } from "@repo/rdx-ui/components"; import { Button } from "@repo/shadcn-ui/components"; import { useNavigate } from "react-router-dom"; @@ -62,12 +62,12 @@ export const CustomerCreate = () => { {t("pages.create.description")}

-
+ -
+
diff --git a/modules/customers/src/web/pages/create/customer-edit-form.tsx b/modules/customers/src/web/pages/create/customer-edit-form.tsx index 528d39ae..5060d17b 100644 --- a/modules/customers/src/web/pages/create/customer-edit-form.tsx +++ b/modules/customers/src/web/pages/create/customer-edit-form.tsx @@ -22,7 +22,7 @@ import { import { useUnsavedChangesNotifier } from "@erp/core/hooks"; import { useTranslation } from "../../i18n"; -import { CustomerData, CustomerDataFormSchema } from "./customer.schema"; +import { CustomerData, CustomerDataUpdateUpdateSchema } from "../../schemas"; const defaultCustomerData = { id: "5e4dc5b3-96b9-4968-9490-14bd032fec5f", @@ -64,7 +64,7 @@ export const CustomerEditForm = ({ const { t } = useTranslation(); const form = useForm({ - resolver: zodResolver(CustomerDataFormSchema), + resolver: zodResolver(CustomerDataUpdateUpdateSchema), defaultValues: initialData, disabled: isPending, }); diff --git a/modules/customers/src/web/pages/create/customer.schema.ts b/modules/customers/src/web/pages/create/customer.schema.ts deleted file mode 100644 index 97bc0ed6..00000000 --- a/modules/customers/src/web/pages/create/customer.schema.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as z from "zod/v4"; - -import { UpdateCustomerByIdRequestSchema } from "../../../common"; - -export const CustomerDataFormSchema = UpdateCustomerByIdRequestSchema; -export type CustomerData = z.infer; diff --git a/modules/customers/src/web/pages/update/customer-edit-form.tsx b/modules/customers/src/web/pages/update/customer-edit-form.tsx new file mode 100644 index 00000000..04384e26 --- /dev/null +++ b/modules/customers/src/web/pages/update/customer-edit-form.tsx @@ -0,0 +1,354 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; + +import { TaxesMultiSelectField } from "@erp/core/components"; +import { SelectField, TextAreaField, TextField } from "@repo/rdx-ui/components"; +import { + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + RadioGroup, + RadioGroupItem, +} from "@repo/shadcn-ui/components"; + +import { useUnsavedChangesNotifier } from "@erp/core/hooks"; +import { GetCustomerByIdResponseDTO } from "@erp/customer-invoices/common"; +import { useTranslation } from "../../i18n"; +import { CustomerData, CustomerDataUpdateUpdateSchema } from "../../schemas"; + +interface CustomerFormProps { + formId: string; + data?: GetCustomerByIdResponseDTO; + isPending?: boolean; + /** + * Callback function to handle form submission. + * @param data - The customer data submitted by the form. + */ + onSubmit?: (data: CustomerData) => void; +} + +export const CustomerEditForm = ({ formId, data, onSubmit, isPending }: CustomerFormProps) => { + const { t } = useTranslation(); + + const form = useForm({ + resolver: zodResolver(CustomerDataUpdateUpdateSchema), + defaultValues: data, + disabled: isPending, + }); + + useUnsavedChangesNotifier({ + isDirty: form.formState.isDirty, + }); + + const handleSubmit = (data: CustomerData) => { + console.log("Datos del formulario:", data); + onSubmit?.(data); + }; + + const handleError = (errors: any) => { + console.error("Errores en el formulario:", errors); + // Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario + }; + + const handleCancel = () => { + form.reset(data); + }; + + return ( +
+ +
+ {/* Información básica */} + + + {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")} + + + + + + + )} + /> + + + + + + + + + + + + {/* Dirección */} + + + {t("form_groups.address.title")} + {t("form_groups.address.description")} + + + + + + + + + + + + + + + {/* Contacto */} + + + {t("form_groups.contact_info.title")} + {t("form_groups.contact_info.description")} + + + + + + + + + + + + + + + + + + + + + {/* Configuraciones Adicionales */} + + + {t("form_groups.additional_config.title")} + {t("form_groups.additional_config.description")} + + + + + + + + + + + +
+ +
+ + ); +}; diff --git a/modules/customers/src/web/pages/update/index.ts b/modules/customers/src/web/pages/update/index.ts new file mode 100644 index 00000000..635be644 --- /dev/null +++ b/modules/customers/src/web/pages/update/index.ts @@ -0,0 +1 @@ +export * from "./update"; diff --git a/modules/customers/src/web/pages/update/update.tsx b/modules/customers/src/web/pages/update/update.tsx new file mode 100644 index 00000000..88013bcf --- /dev/null +++ b/modules/customers/src/web/pages/update/update.tsx @@ -0,0 +1,175 @@ +import { AppBreadcrumb, AppContent, BackHistoryButton, ButtonGroup } from "@repo/rdx-ui/components"; +import { Button } from "@repo/shadcn-ui/components"; +import { useNavigate } from "react-router-dom"; + +import { useUrlParamId } from "@erp/core/hooks"; +import { useCustomerQuery, useUpdateCustomerMutation } from "../../hooks"; +import { useTranslation } from "../../i18n"; +import { CustomerEditForm } from "./customer-edit-form"; + +export const CustomerUpdate = () => { + const { t } = useTranslation(); + const customerId = useUrlParamId(); + const navigate = useNavigate(); + + // 1) Estado de carga del cliente (query) + const { + data: customerData, + isLoading: isLoadingCustomer, + isError: isLoadError, + error: loadError, + } = useCustomerQuery(customerId, { enabled: !!customerId }); + + // 2) Estado de actualización (mutación) + const { + mutateAsync: updateAsync, + isPending: isUpdating, + isError: isUpdateError, + error: updateError, + } = useUpdateCustomerMutation(customerId || ""); + + // 3) Submit con navegación condicionada por éxito + const handleSubmit = async (formData: any) => { + try { + await updateAsync(formData); // solo navegamos si no lanza + // toast?.({ title: t('pages.update.successTitle'), description: t('pages.update.successMsg') }); + navigate("/customers/list"); + } catch (e) { + // toast?.({ variant: 'destructive', title: t('pages.update.errorTitle'), description: (e as Error).message }); + // No navegamos en caso de error + } + }; + + if (isLoadingCustomer) { + return ( + <> + + +
+
+
+
+
+
+ + +
+
+
+ {/* Skeleton simple para el formulario */} +
+
+
+
+ + + ); + } + + if (isLoadError) { + return ( + <> + + +
+

+ {t("pages.update.loadErrorTitle", "No se pudo cargar el cliente")} +

+

+ {(loadError as Error)?.message ?? + t("pages.update.loadErrorMsg", "Inténtalo de nuevo más tarde.")} +

+
+
+ +
+
+ + ); + } + + if (!customerData) { + return ( + <> + + +
+

+ {t("pages.update.notFoundTitle", "Cliente no encontrado")} +

+

+ {t("pages.update.notFoundMsg", "Revisa el identificador o vuelve al listado.")} +

+
+
+ +
+
+ + ); + } + + return ( + <> + + +
+
+

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

+

+ {t("pages.update.description")} +

+
+ + + + +
+ {/* Alerta de error de actualización (si ha fallado el último intento) */} + {isUpdateError && ( +
+

+ {t("pages.update.errorTitle", "No se pudo guardar los cambios")} +

+

+ {(updateError as Error)?.message ?? + t("pages.update.errorMsg", "Revisa los datos e inténtalo de nuevo.")} +

+
+ )} + +
+ {/* Importante: proveemos un formId para que el botón del header pueda hacer submit */} + +
+
+ + ); +}; diff --git a/modules/customers/src/web/schemas/customer.schema.ts b/modules/customers/src/web/schemas/customer.schema.ts new file mode 100644 index 00000000..f23346b9 --- /dev/null +++ b/modules/customers/src/web/schemas/customer.schema.ts @@ -0,0 +1,10 @@ +import { + GetCustomerByIdResponseDTO, + UpdateCustomerByIdRequestDTO, + UpdateCustomerByIdRequestSchema, +} from "@erp/customers"; + +export type CustomerData = GetCustomerByIdResponseDTO; + +export const CustomerDataUpdateUpdateSchema = UpdateCustomerByIdRequestSchema; +export type CustomerDataFormUpdateDTO = UpdateCustomerByIdRequestDTO; diff --git a/modules/customers/src/web/schemas/index.ts b/modules/customers/src/web/schemas/index.ts new file mode 100644 index 00000000..4af663f8 --- /dev/null +++ b/modules/customers/src/web/schemas/index.ts @@ -0,0 +1 @@ +export * from "./customer.schema"; diff --git a/modules/customers/tsconfig.json b/modules/customers/tsconfig.json index ef8f21af..b4a95fde 100644 --- a/modules/customers/tsconfig.json +++ b/modules/customers/tsconfig.json @@ -28,6 +28,6 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["src", "../../packages/rdx-ddd/src/helpers/extract-or-push-error.ts"], + "include": ["src"], "exclude": ["node_modules"] }