-
-
-
- TOTAL: 960,56 €
-
-
+
+
+
+
+
+
+
+ TOTAL: {{total_amount.value}} €
+
+
-
-
-
-
-
- | Concepto |
- Cantidad |
- Precio unidad |
- Importe total |
-
-
+
+
+
+ {{#each items}}
+
+ | {{description}} |
+ {{quantity.value}} |
+ {{unit_amount.value}} € |
+ {{total_amount.value}} € |
+
+ {{/each}}
+
+
+
-
-
- | Base imponible: |
- 761,14 € |
-
-
- | IVA (21%): |
- 159,84 € |
-
-
- | Total factura: |
- 960,56 € |
-
-
+
-
+
+
+
+
+
diff --git a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts
index ddadea7a..006fc90f 100644
--- a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts
+++ b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts
@@ -1,7 +1,7 @@
-import { DomainValidationError } from "@erp/core/api";
import {
AggregateRoot,
CurrencyCode,
+ DomainValidationError,
LanguageCode,
Percentage,
TextValue,
@@ -168,8 +168,27 @@ export class CustomerInvoice
return this.recipient.isSome();
}
+ private _getDiscountAmount(subtotalAmount: InvoiceAmount): InvoiceAmount {
+ return subtotalAmount.percentage(this.discountPercentage);
+ }
+
+ private _getTaxableAmount(
+ subtotalAmount: InvoiceAmount,
+ discountAmount: InvoiceAmount
+ ): InvoiceAmount {
+ return subtotalAmount.subtract(discountAmount);
+ }
+
+ private _getTaxesAmount(taxableAmount: InvoiceAmount): InvoiceAmount {
+ return this._taxes.getTaxesAmount(taxableAmount);
+ }
+
+ private _getTotalAmount(taxableAmount: InvoiceAmount, taxesAmount: InvoiceAmount): InvoiceAmount {
+ return taxableAmount.add(taxesAmount);
+ }
+
public getSubtotalAmount(): InvoiceAmount {
- const itemsSubtotal = this.items.getTotalAmount().convertScale(2);
+ const itemsSubtotal = this.items.getSubtotalAmount().convertScale(2);
return InvoiceAmount.create({
value: itemsSubtotal.value,
@@ -178,11 +197,11 @@ export class CustomerInvoice
}
public getDiscountAmount(): InvoiceAmount {
- return this.getSubtotalAmount().percentage(this.discountPercentage) as InvoiceAmount;
+ return this._getDiscountAmount(this.getSubtotalAmount());
}
public getTaxableAmount(): InvoiceAmount {
- return this.getSubtotalAmount().subtract(this.getDiscountAmount()) as InvoiceAmount;
+ return this._getTaxableAmount(this.getSubtotalAmount(), this.getDiscountAmount());
}
public getTaxesAmount(): InvoiceAmount {
@@ -191,20 +210,24 @@ export class CustomerInvoice
public getTotalAmount(): InvoiceAmount {
const taxableAmount = this.getTaxableAmount();
- return taxableAmount.add(this._getTaxesAmount(taxableAmount)) as InvoiceAmount;
+ const taxesAmount = this._getTaxesAmount(taxableAmount);
+
+ return this._getTotalAmount(taxableAmount, taxesAmount);
}
public getAllAmounts() {
+ const subtotalAmount = this.getSubtotalAmount();
+ const discountAmount = this._getDiscountAmount(subtotalAmount);
+ const taxableAmount = this._getTaxableAmount(subtotalAmount, discountAmount);
+ const taxesAmount = this._getTaxesAmount(taxableAmount);
+ const totalAmount = this._getTotalAmount(taxableAmount, taxesAmount);
+
return {
- subtotalAmount: this.getSubtotalAmount(),
- discountAmount: this.getDiscountAmount(),
- taxableAmount: this.getTaxableAmount(),
- taxesAmount: this.getTaxesAmount(),
- totalAmount: this.getTotalAmount(),
+ subtotalAmount,
+ discountAmount,
+ taxableAmount,
+ taxesAmount,
+ totalAmount,
};
}
-
- private _getTaxesAmount(_taxableAmount: InvoiceAmount): InvoiceAmount {
- return this._taxes.getTaxesAmount(_taxableAmount);
- }
}
diff --git a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts b/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts
index 544d1e81..b8977891 100644
--- a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts
+++ b/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts
@@ -95,6 +95,26 @@ export class CustomerInvoiceItem
return this.getProps();
}
+ private _getDiscountAmount(subtotalAmount: ItemAmount): ItemAmount {
+ const discount = this.discountPercentage.match(
+ (percentage) => percentage,
+ () => ItemDiscount.zero()
+ );
+ return subtotalAmount.percentage(discount);
+ }
+
+ private _getTaxableAmount(subtotalAmount: ItemAmount, discountAmount: ItemAmount): ItemAmount {
+ return subtotalAmount.subtract(discountAmount);
+ }
+
+ private _getTaxesAmount(taxableAmount: ItemAmount): ItemAmount {
+ return this.props.taxes.getTaxesAmount(taxableAmount);
+ }
+
+ private _getTotalAmount(taxableAmount: ItemAmount, taxesAmount: ItemAmount): ItemAmount {
+ return taxableAmount.add(taxesAmount);
+ }
+
public getSubtotalAmount(): ItemAmount {
const curCode = this.currencyCode.code;
const quantity = this.quantity.match(
@@ -110,15 +130,11 @@ export class CustomerInvoiceItem
}
public getDiscountAmount(): ItemAmount {
- const discount = this.discountPercentage.match(
- (percentage) => percentage,
- () => ItemDiscount.zero()
- );
- return this.getSubtotalAmount().percentage(discount);
+ return this._getDiscountAmount(this.getSubtotalAmount());
}
public getTaxableAmount(): ItemAmount {
- return this.getSubtotalAmount().subtract(this.getDiscountAmount());
+ return this._getTaxableAmount(this.getSubtotalAmount(), this.getDiscountAmount());
}
public getTaxesAmount(): ItemAmount {
@@ -127,20 +143,24 @@ export class CustomerInvoiceItem
public getTotalAmount(): ItemAmount {
const taxableAmount = this.getTaxableAmount();
- return taxableAmount.add(this._getTaxesAmount(taxableAmount));
+ const taxesAmount = this._getTaxesAmount(taxableAmount);
+
+ return this._getTotalAmount(taxableAmount, taxesAmount);
}
public getAllAmounts() {
+ const subtotalAmount = this.getSubtotalAmount();
+ const discountAmount = this._getDiscountAmount(subtotalAmount);
+ const taxableAmount = this._getTaxableAmount(subtotalAmount, discountAmount);
+ const taxesAmount = this._getTaxesAmount(taxableAmount);
+ const totalAmount = this._getTotalAmount(taxableAmount, taxesAmount);
+
return {
- subtotalAmount: this.getSubtotalAmount(),
- discountAmount: this.getDiscountAmount(),
- taxableAmount: this.getTaxableAmount(),
- taxesAmount: this.getTaxesAmount(),
- totalAmount: this.getTotalAmount(),
+ subtotalAmount,
+ discountAmount,
+ taxableAmount,
+ taxesAmount,
+ totalAmount,
};
}
-
- private _getTaxesAmount(_taxableAmount: ItemAmount): ItemAmount {
- return this.props.taxes.getTaxesAmount(_taxableAmount);
- }
}
diff --git a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-items.ts b/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-items.ts
index d79bd499..8710a2b8 100644
--- a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-items.ts
+++ b/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-items.ts
@@ -36,9 +36,37 @@ export class CustomerInvoiceItems extends Collection
{
return super.add(item);
}
+ public getSubtotalAmount(): ItemAmount {
+ return this.getAll().reduce(
+ (total, tax) => total.add(tax.getSubtotalAmount()),
+ ItemAmount.zero(this._currencyCode.code)
+ );
+ }
+
+ public getDiscountAmount(): ItemAmount {
+ return this.getAll().reduce(
+ (total, item) => total.add(item.getDiscountAmount()),
+ ItemAmount.zero(this._currencyCode.code)
+ );
+ }
+
+ public getTaxableAmount(): ItemAmount {
+ return this.getAll().reduce(
+ (total, item) => total.add(item.getTaxableAmount()),
+ ItemAmount.zero(this._currencyCode.code)
+ );
+ }
+
+ public getTaxesAmount(): ItemAmount {
+ return this.getAll().reduce(
+ (total, item) => total.add(item.getTaxesAmount()),
+ ItemAmount.zero(this._currencyCode.code)
+ );
+ }
+
public getTotalAmount(): ItemAmount {
return this.getAll().reduce(
- (total, tax) => total.add(tax.getTotalAmount()),
+ (total, item) => total.add(item.getTotalAmount()),
ItemAmount.zero(this._currencyCode.code)
);
}
diff --git a/modules/customer-invoices/src/api/domain/value-objects/invoice-amount.ts b/modules/customer-invoices/src/api/domain/value-objects/invoice-amount.ts
index a00f9d14..e4cbcae1 100644
--- a/modules/customer-invoices/src/api/domain/value-objects/invoice-amount.ts
+++ b/modules/customer-invoices/src/api/domain/value-objects/invoice-amount.ts
@@ -35,36 +35,60 @@ export class InvoiceAmount extends MoneyValue {
convertScale(newScale: number) {
const mv = super.convertScale(newScale);
const p = mv.toPrimitive();
- return new InvoiceAmount({ value: p.value, currency_code: p.currency_code });
+ return new InvoiceAmount({
+ value: p.value,
+ currency_code: p.currency_code,
+ scale: InvoiceAmount.DEFAULT_SCALE,
+ });
}
add(addend: MoneyValue) {
const mv = super.add(addend);
const p = mv.toPrimitive();
- return new InvoiceAmount({ value: p.value, currency_code: p.currency_code });
+ return new InvoiceAmount({
+ value: p.value,
+ currency_code: p.currency_code,
+ scale: InvoiceAmount.DEFAULT_SCALE,
+ });
}
subtract(subtrahend: MoneyValue) {
const mv = super.subtract(subtrahend);
const p = mv.toPrimitive();
- return new InvoiceAmount({ value: p.value, currency_code: p.currency_code });
+ return new InvoiceAmount({
+ value: p.value,
+ currency_code: p.currency_code,
+ scale: InvoiceAmount.DEFAULT_SCALE,
+ });
}
multiply(multiplier: number | Quantity) {
const mv = super.multiply(multiplier);
const p = mv.toPrimitive();
- return new InvoiceAmount({ value: p.value, currency_code: p.currency_code });
+ return new InvoiceAmount({
+ value: p.value,
+ currency_code: p.currency_code,
+ scale: InvoiceAmount.DEFAULT_SCALE,
+ });
}
divide(divisor: number | Quantity) {
const mv = super.divide(divisor);
const p = mv.toPrimitive();
- return new InvoiceAmount({ value: p.value, currency_code: p.currency_code });
+ return new InvoiceAmount({
+ value: p.value,
+ currency_code: p.currency_code,
+ scale: InvoiceAmount.DEFAULT_SCALE,
+ });
}
percentage(percentage: number | Percentage) {
const mv = super.percentage(percentage);
const p = mv.toPrimitive();
- return new InvoiceAmount({ value: p.value, currency_code: p.currency_code });
+ return new InvoiceAmount({
+ value: p.value,
+ currency_code: p.currency_code,
+ scale: InvoiceAmount.DEFAULT_SCALE,
+ });
}
}
diff --git a/modules/customer-invoices/src/api/domain/value-objects/item-amount.ts b/modules/customer-invoices/src/api/domain/value-objects/item-amount.ts
index 6f63dfd0..04d22c79 100644
--- a/modules/customer-invoices/src/api/domain/value-objects/item-amount.ts
+++ b/modules/customer-invoices/src/api/domain/value-objects/item-amount.ts
@@ -35,36 +35,63 @@ export class ItemAmount extends MoneyValue {
convertScale(newScale: number) {
const mv = super.convertScale(newScale);
const p = mv.toPrimitive();
- return new ItemAmount({ value: p.value, currency_code: p.currency_code });
+ return new ItemAmount({
+ value: p.value,
+ currency_code: p.currency_code,
+ scale: ItemAmount.DEFAULT_SCALE,
+ });
}
add(addend: MoneyValue) {
const mv = super.add(addend);
const p = mv.toPrimitive();
- return new ItemAmount({ value: p.value, currency_code: p.currency_code });
+ return new ItemAmount({
+ value: p.value,
+ currency_code: p.currency_code,
+ scale: ItemAmount.DEFAULT_SCALE,
+ });
}
subtract(subtrahend: MoneyValue) {
const mv = super.subtract(subtrahend);
const p = mv.toPrimitive();
- return new ItemAmount({ value: p.value, currency_code: p.currency_code });
+ return new ItemAmount({
+ value: p.value,
+ currency_code: p.currency_code,
+ scale: ItemAmount.DEFAULT_SCALE,
+ });
}
multiply(multiplier: number | Quantity) {
const mv = super.multiply(multiplier);
const p = mv.toPrimitive();
- return new ItemAmount({ value: p.value, currency_code: p.currency_code });
+
+ const result = new ItemAmount({
+ value: p.value,
+ currency_code: p.currency_code,
+ scale: ItemAmount.DEFAULT_SCALE,
+ });
+
+ return result;
}
divide(divisor: number | Quantity) {
const mv = super.divide(divisor);
const p = mv.toPrimitive();
- return new ItemAmount({ value: p.value, currency_code: p.currency_code });
+ return new ItemAmount({
+ value: p.value,
+ currency_code: p.currency_code,
+ scale: ItemAmount.DEFAULT_SCALE,
+ });
}
percentage(percentage: number | Percentage) {
const mv = super.percentage(percentage);
const p = mv.toPrimitive();
- return new ItemAmount({ value: p.value, currency_code: p.currency_code });
+ return new ItemAmount({
+ value: p.value,
+ currency_code: p.currency_code,
+ scale: ItemAmount.DEFAULT_SCALE,
+ });
}
}
diff --git a/modules/customer-invoices/src/api/infrastructure/dependencies.ts b/modules/customer-invoices/src/api/infrastructure/dependencies.ts
index 84b68c67..62a4716e 100644
--- a/modules/customer-invoices/src/api/infrastructure/dependencies.ts
+++ b/modules/customer-invoices/src/api/infrastructure/dependencies.ts
@@ -17,6 +17,7 @@ import {
GetCustomerInvoiceUseCase,
ListCustomerInvoicesPresenter,
ListCustomerInvoicesUseCase,
+ RecipientInvoiceFullPresenter,
ReportCustomerInvoiceUseCase,
} from "../application";
@@ -77,6 +78,13 @@ export function buildCustomerInvoiceDependencies(params: ModuleParams): Customer
},
presenter: new CustomerInvoiceItemsFullPresenter(presenterRegistry),
},
+ {
+ key: {
+ resource: "recipient-invoice",
+ projection: "FULL",
+ },
+ presenter: new RecipientInvoiceFullPresenter(presenterRegistry),
+ },
{
key: {
resource: "customer-invoice",
diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts
index e1573ef0..038bf453 100644
--- a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts
+++ b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts
@@ -130,6 +130,7 @@ export class CustomerInvoiceRepository
const row = await CustomerInvoiceModel.findOne({
where: { id: id.toString(), company_id: companyId.toString() },
+ order: [[{ model: CustomerInvoiceItemModel, as: "items" }, "position", "ASC"]],
include: [
{
model: CustomerModel,
diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-item.model.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-item.model.ts
index ce08af9d..ea6f87a9 100644
--- a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-item.model.ts
+++ b/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice-item.model.ts
@@ -217,7 +217,9 @@ export default (database: Sequelize) => {
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
- defaultScope: {},
+ defaultScope: {
+ order: [["position", "ASC"]],
+ },
scopes: {},
}
diff --git a/modules/customer-invoices/src/common/dto/response/get-customer-invoice-by-id.response.dto.ts b/modules/customer-invoices/src/common/dto/response/get-customer-invoice-by-id.response.dto.ts
index 75fd924f..52df31e1 100644
--- a/modules/customer-invoices/src/common/dto/response/get-customer-invoice-by-id.response.dto.ts
+++ b/modules/customer-invoices/src/common/dto/response/get-customer-invoice-by-id.response.dto.ts
@@ -17,6 +17,19 @@ export const GetCustomerInvoiceByIdResponseSchema = z.object({
language_code: z.string(),
currency_code: z.string(),
+ customer_id: z.string(),
+ recipient: z.object({
+ id: z.string(),
+ name: z.string(),
+ tin: z.string(),
+ street: z.string(),
+ street2: z.string(),
+ city: z.string(),
+ province: z.string(),
+ postal_code: z.string(),
+ country: z.string(),
+ }),
+
taxes: z.string(),
subtotal_amount: MoneySchema,
diff --git a/modules/customers/src/web/components/customer-editor-skeleton.tsx b/modules/customers/src/web/components/customer-editor-skeleton.tsx
new file mode 100644
index 00000000..bfc3eb87
--- /dev/null
+++ b/modules/customers/src/web/components/customer-editor-skeleton.tsx
@@ -0,0 +1,33 @@
+// components/CustomerSkeleton.tsx
+import { AppBreadcrumb, AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
+import { Button } from "@repo/shadcn-ui/components";
+import { useTranslation } from "../i18n";
+
+export const CustomerEditorSkeleton = () => {
+ const { t } = useTranslation();
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ {t("pages.update.loading", "Cargando cliente...")}
+
+ >
+ );
+};
diff --git a/modules/customers/src/web/components/error-alert.tsx b/modules/customers/src/web/components/error-alert.tsx
new file mode 100644
index 00000000..01098060
--- /dev/null
+++ b/modules/customers/src/web/components/error-alert.tsx
@@ -0,0 +1,16 @@
+// components/ErrorAlert.tsx
+interface ErrorAlertProps {
+ title: string;
+ message: string;
+}
+
+export const ErrorAlert = ({ title, message }: ErrorAlertProps) => (
+
+);
diff --git a/modules/customers/src/web/components/index.ts b/modules/customers/src/web/components/index.ts
index 5c3ca390..b3a10d5b 100644
--- a/modules/customers/src/web/components/index.ts
+++ b/modules/customers/src/web/components/index.ts
@@ -1,3 +1,6 @@
export * from "./client-selector";
+export * from "./customer-editor-skeleton";
export * from "./customers-layout";
export * from "./customers-list-grid";
+export * from "./error-alert";
+export * from "./not-found-card";
diff --git a/modules/customers/src/web/components/not-found-card.tsx b/modules/customers/src/web/components/not-found-card.tsx
new file mode 100644
index 00000000..3d33f654
--- /dev/null
+++ b/modules/customers/src/web/components/not-found-card.tsx
@@ -0,0 +1,19 @@
+// components/NotFoundCard.tsx
+import { BackHistoryButton } from "@repo/rdx-ui/components";
+
+interface NotFoundCardProps {
+ title: string;
+ message: string;
+}
+
+export const NotFoundCard = ({ title, message }: NotFoundCardProps) => (
+ <>
+
+
+
+
+ >
+);
diff --git a/modules/customers/src/web/constants/customer.constants.ts b/modules/customers/src/web/constants/customer.constants.ts
new file mode 100644
index 00000000..a65625bb
--- /dev/null
+++ b/modules/customers/src/web/constants/customer.constants.ts
@@ -0,0 +1,28 @@
+export const COUNTRY_OPTIONS = [
+ { value: "ES", label: "España" },
+ { value: "FR", label: "Francia" },
+ { value: "DE", label: "Alemania" },
+ { value: "IT", label: "Italia" },
+ { value: "PT", label: "Portugal" },
+ { value: "US", label: "Estados Unidos" },
+ { value: "MX", label: "México" },
+ { value: "AR", label: "Argentina" },
+] as const;
+
+export const LANGUAGE_OPTIONS = [
+ { value: "es", label: "Español" },
+ { value: "en", label: "Inglés" },
+ { value: "fr", label: "Francés" },
+ { value: "de", label: "Alemán" },
+ { value: "it", label: "Italiano" },
+ { value: "pt", label: "Portugués" },
+] as const;
+
+export const CURRENCY_OPTIONS = [
+ { value: "EUR", label: "Euro" },
+ { value: "USD", label: "Dólar estadounidense" },
+ { value: "GBP", label: "Libra esterlina" },
+ { value: "ARS", label: "Peso argentino" },
+ { value: "MXN", label: "Peso mexicano" },
+ { value: "JPY", label: "Yen japonés" },
+] as const;
diff --git a/modules/customers/src/web/constants/index.ts b/modules/customers/src/web/constants/index.ts
new file mode 100644
index 00000000..1186e595
--- /dev/null
+++ b/modules/customers/src/web/constants/index.ts
@@ -0,0 +1 @@
+export * from "./customer.constants";
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 6275d3b8..b0e0e889 100644
--- a/modules/customers/src/web/hooks/use-create-customer-mutation.ts
+++ b/modules/customers/src/web/hooks/use-create-customer-mutation.ts
@@ -1,20 +1,21 @@
-import { useDataSource, useQueryKey } from "@erp/core/hooks";
+import { useDataSource } from "@erp/core/hooks";
import { useMutation, useQueryClient } from "@tanstack/react-query";
-import { UpdateCustomerByIdRequestDTO } from "../../common/dto";
+import { CustomerCreateData, CustomerData } from "../schemas";
+import { CUSTOMERS_LIST_KEY } from "./use-update-customer-mutation";
-export const useCreateCustomerMutation = () => {
+export function 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);
+ mutationFn: async (data: CustomerCreateData) => {
+ const created = await dataSource.createOne("customers", data);
+ return created as CustomerData;
},
onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["customers"] });
+ // Invalida el listado de clientes para incluir el nuevo
+ queryClient.invalidateQueries({ queryKey: CUSTOMERS_LIST_KEY });
},
});
-};
+}
diff --git a/modules/customers/src/web/hooks/use-update-customer-mutation.ts b/modules/customers/src/web/hooks/use-update-customer-mutation.ts
index a1b9089a..bb25774c 100644
--- a/modules/customers/src/web/hooks/use-update-customer-mutation.ts
+++ b/modules/customers/src/web/hooks/use-update-customer-mutation.ts
@@ -1,32 +1,36 @@
import { useDataSource } from "@erp/core/hooks";
-import {
- UpdateCustomerByIdRequestDTO,
- UpdateCustomerByIdResponseDTO,
-} from "@erp/customer-invoices/common";
import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { CustomerData, CustomerUpdateData } from "../schemas";
import { CUSTOMER_QUERY_KEY } from "./use-customer-query";
export const CUSTOMERS_LIST_KEY = ["customers"] as const;
-type MutationDeps = {};
+type UpdateCustomerPayload = {
+ id: string;
+ data: CustomerUpdateData;
+};
-export function useUpdateCustomerMutation(customerId: string, deps?: MutationDeps) {
+export function useUpdateCustomerMutation() {
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;
+ return useMutation({
+ mutationKey: ["customer:update"], //, customerId],
+
+ mutationFn: async (payload) => {
+ const { id: customerId, data } = payload;
+ if (!customerId) {
+ throw new Error("customerId is required");
+ }
+
+ const updated = await dataSource.updateOne("customers", customerId, data);
+ return updated as CustomerData;
},
- onSuccess: (updated) => {
+ onSuccess: (updated, variables) => {
+ const { id: customerId } = variables;
+
// Refresca inmediatamente el detalle
- queryClient.setQueryData(
- CUSTOMER_QUERY_KEY(customerId),
- updated
- );
+ queryClient.setQueryData(CUSTOMER_QUERY_KEY(customerId), updated);
// Otra opción es invalidar el detalle para forzar refetch:
// queryClient.invalidateQueries({ queryKey: CUSTOMER_QUERY_KEY(customerId) });
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 5060d17b..e39cc24a 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, CustomerDataUpdateUpdateSchema } from "../../schemas";
+import { CustomerData, CustomerUpdateSchema } 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(CustomerDataUpdateUpdateSchema),
+ resolver: zodResolver(CustomerUpdateSchema),
defaultValues: initialData,
disabled: isPending,
});
diff --git a/modules/customers/src/web/pages/update/customer-edit-form.tsx b/modules/customers/src/web/pages/update/customer-edit-form.tsx
index 172d2004..d9d23d84 100644
--- a/modules/customers/src/web/pages/update/customer-edit-form.tsx
+++ b/modules/customers/src/web/pages/update/customer-edit-form.tsx
@@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
-import { useForm } from "react-hook-form";
+import { FieldErrors, useForm } from "react-hook-form";
import { TaxesMultiSelectField } from "@erp/core/components";
import { SelectField, TextAreaField, TextField } from "@repo/rdx-ui/components";
@@ -21,26 +21,26 @@ import {
} from "@repo/shadcn-ui/components";
import { useUnsavedChangesNotifier } from "@erp/core/hooks";
-import { GetCustomerByIdResponseDTO } from "@erp/customer-invoices/common";
+import { COUNTRY_OPTIONS, LANGUAGE_OPTIONS } from "../../constants/customer.constants";
import { useTranslation } from "../../i18n";
-import { CustomerData, CustomerDataUpdateUpdateSchema } from "../../schemas";
+import { CustomerData, CustomerUpdateData, CustomerUpdateSchema } from "../../schemas";
interface CustomerFormProps {
formId: string;
- data?: GetCustomerByIdResponseDTO;
+ data?: CustomerData;
isPending?: boolean;
/**
* Callback function to handle form submission.
* @param data - The customer data submitted by the form.
*/
- onSubmit?: (data: CustomerData) => void;
+ onSubmit?: (data: CustomerUpdateData) => void;
}
export const CustomerEditForm = ({ formId, data, onSubmit, isPending }: CustomerFormProps) => {
const { t } = useTranslation();
- const form = useForm({
- resolver: zodResolver(CustomerDataUpdateUpdateSchema),
+ const form = useForm({
+ resolver: zodResolver(CustomerUpdateSchema),
defaultValues: data,
disabled: isPending,
});
@@ -49,12 +49,12 @@ export const CustomerEditForm = ({ formId, data, onSubmit, isPending }: Customer
isDirty: form.formState.isDirty,
});
- const handleSubmit = (data: CustomerData) => {
+ const handleSubmit = (data: CustomerUpdateData) => {
console.log("Datos del formulario:", data);
onSubmit?.(data);
};
- const handleError = (errors: any) => {
+ const handleError = (errors: FieldErrors) => {
console.error("Errores en el formulario:", errors);
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
};
@@ -63,8 +63,21 @@ export const CustomerEditForm = ({ formId, data, onSubmit, isPending }: Customer
form.reset(data);
};
+ const {
+ formState: { isDirty, dirtyFields },
+ } = form;
+
return (
);
diff --git a/modules/customers/src/web/pages/update/update.tsx b/modules/customers/src/web/pages/update/update.tsx
index 88013bcf..82490e63 100644
--- a/modules/customers/src/web/pages/update/update.tsx
+++ b/modules/customers/src/web/pages/update/update.tsx
@@ -3,8 +3,11 @@ import { Button } from "@repo/shadcn-ui/components";
import { useNavigate } from "react-router-dom";
import { useUrlParamId } from "@erp/core/hooks";
+import { showErrorToast, showSuccessToast } from "@repo/shadcn-ui/lib/utils";
+import { CustomerEditorSkeleton, ErrorAlert, NotFoundCard } from "../../components";
import { useCustomerQuery, useUpdateCustomerMutation } from "../../hooks";
import { useTranslation } from "../../i18n";
+import { CustomerUpdateData } from "../../schemas";
import { CustomerEditForm } from "./customer-edit-form";
export const CustomerUpdate = () => {
@@ -22,50 +25,29 @@ export const CustomerUpdate = () => {
// 2) Estado de actualización (mutación)
const {
- mutateAsync: updateAsync,
+ mutateAsync,
isPending: isUpdating,
isError: isUpdateError,
error: updateError,
- } = useUpdateCustomerMutation(customerId || "");
+ } = useUpdateCustomerMutation();
// 3) Submit con navegación condicionada por éxito
- const handleSubmit = async (formData: any) => {
+ const handleSubmit = async (formData: CustomerUpdateData) => {
try {
- await updateAsync(formData); // solo navegamos si no lanza
- // toast?.({ title: t('pages.update.successTitle'), description: t('pages.update.successMsg') });
- navigate("/customers/list");
+ const result = await mutateAsync({ id: customerId!, data: formData });
+
+ if (result) {
+ showSuccessToast(t("pages.update.successTitle"), 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
+ showErrorToast(t("pages.update.errorTitle"), (e as Error).message);
+ } finally {
}
};
if (isLoadingCustomer) {
- return (
- <>
-
-
-
-
-
-
-
-
-
-
- {/* Skeleton simple para el formulario */}
-
-
-
-
-
- >
- );
+ return ;
}
if (isLoadError) {
@@ -73,19 +55,14 @@ export const CustomerUpdate = () => {
<>
-
-
- {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.")}
-
-
+
+
@@ -94,26 +71,18 @@ export const CustomerUpdate = () => {
);
}
- if (!customerData) {
+ if (!customerData)
return (
<>
-
-
- {t("pages.update.notFoundTitle", "Cliente no encontrado")}
-
-
- {t("pages.update.notFoundMsg", "Revisa el identificador o vuelve al listado.")}
-
-
-
-
-
+
>
);
- }
return (
<>
@@ -145,19 +114,13 @@ export const CustomerUpdate = () => {
{/* 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.")}
-
-
+
)}
diff --git a/modules/customers/src/web/schemas/customer.schema.ts b/modules/customers/src/web/schemas/customer.schema.ts
index f23346b9..579a6504 100644
--- a/modules/customers/src/web/schemas/customer.schema.ts
+++ b/modules/customers/src/web/schemas/customer.schema.ts
@@ -1,4 +1,6 @@
import {
+ CreateCustomerRequestDTO,
+ CreateCustomerRequestSchema,
GetCustomerByIdResponseDTO,
UpdateCustomerByIdRequestDTO,
UpdateCustomerByIdRequestSchema,
@@ -6,5 +8,8 @@ import {
export type CustomerData = GetCustomerByIdResponseDTO;
-export const CustomerDataUpdateUpdateSchema = UpdateCustomerByIdRequestSchema;
-export type CustomerDataFormUpdateDTO = UpdateCustomerByIdRequestDTO;
+export const CustomerCreateSchema = CreateCustomerRequestSchema;
+export type CustomerCreateData = CreateCustomerRequestDTO;
+
+export const CustomerUpdateSchema = UpdateCustomerByIdRequestSchema;
+export type CustomerUpdateData = UpdateCustomerByIdRequestDTO;
diff --git a/packages/rdx-ddd/src/value-objects/money-value.ts b/packages/rdx-ddd/src/value-objects/money-value.ts
index 9f114f97..5d291742 100644
--- a/packages/rdx-ddd/src/value-objects/money-value.ts
+++ b/packages/rdx-ddd/src/value-objects/money-value.ts
@@ -201,6 +201,10 @@ export class MoneyValue extends ValueObject implements IMoneyVa
return this.dinero.hasSameAmount(comparator.dinero);
}
+ hasSameScale(comparator: MoneyValue): boolean {
+ return this.dinero.getPrecision() === comparator.dinero.getPrecision();
+ }
+
/**
* Devuelve una cadena con el importe formateado.
* Ejemplo: 123456 -> €1,234.56
diff --git a/packages/shadcn-ui/src/components/sonner.tsx b/packages/shadcn-ui/src/components/sonner.tsx
index 957524ed..2cb4a4d3 100644
--- a/packages/shadcn-ui/src/components/sonner.tsx
+++ b/packages/shadcn-ui/src/components/sonner.tsx
@@ -1,15 +1,15 @@
-"use client"
+"use client";
-import { useTheme } from "next-themes"
-import { Toaster as Sonner, ToasterProps } from "sonner"
+import { useTheme } from "next-themes";
+import { Toaster as Sonner, ToasterProps, toast } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
- const { theme = "system" } = useTheme()
+ const { theme = "system" } = useTheme();
return (
{
}
{...props}
/>
- )
-}
+ );
+};
-export { Toaster }
+export { Toaster, toast };
diff --git a/packages/shadcn-ui/src/lib/utils.ts b/packages/shadcn-ui/src/lib/utils.ts
index 365058ce..c5f1413e 100644
--- a/packages/shadcn-ui/src/lib/utils.ts
+++ b/packages/shadcn-ui/src/lib/utils.ts
@@ -1,6 +1,25 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
+import { toast } from "../components/sonner.tsx";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+
+/**
+ * Muestra un toast de éxito
+ */
+export function showSuccessToast(title: string, description?: string) {
+ toast.success(title, {
+ description,
+ });
+}
+
+/**
+ * Muestra un toast de error
+ */
+export function showErrorToast(title: string, description?: string) {
+ toast.error(title, {
+ description,
+ });
+}