Clientes y facturas de cliente
This commit is contained in:
parent
4d3430cc91
commit
096abdccb2
@ -62,7 +62,7 @@ export const App = () => {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
</UnsavedWarnProvider>
|
</UnsavedWarnProvider>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<Toaster />
|
<Toaster position='top-right' />
|
||||||
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
|
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</DataSourceProvider>
|
</DataSourceProvider>
|
||||||
|
|||||||
@ -17,8 +17,8 @@ export interface IDataSource {
|
|||||||
getList<T, R>(resource: string, params?: Record<string, unknown>): Promise<R>;
|
getList<T, R>(resource: string, params?: Record<string, unknown>): Promise<R>;
|
||||||
getOne<T>(resource: string, id: string | number): Promise<T>;
|
getOne<T>(resource: string, id: string | number): Promise<T>;
|
||||||
getMany<T, R>(resource: string, ids: Array<string | number>): Promise<R>;
|
getMany<T, R>(resource: string, ids: Array<string | number>): Promise<R>;
|
||||||
createOne<T>(resource: string, data: Partial<T>): Promise<T>;
|
createOne<T, R>(resource: string, data: Partial<T>): Promise<R>;
|
||||||
updateOne<T>(resource: string, id: string | number, data: Partial<T>): Promise<T>;
|
updateOne<T, R>(resource: string, id: string | number, data: Partial<T>): Promise<R>;
|
||||||
deleteOne<T>(resource: string, id: string | number): Promise<void>;
|
deleteOne<T>(resource: string, id: string | number): Promise<void>;
|
||||||
|
|
||||||
custom: <R>(customParams: ICustomParams) => Promise<R>;
|
custom: <R>(customParams: ICustomParams) => Promise<R>;
|
||||||
|
|||||||
@ -30,8 +30,6 @@ export class CustomerInvoiceItemsFullPresenter extends Presenter {
|
|||||||
() => ({ value: "", scale: "", currency_code: "" })
|
() => ({ value: "", scale: "", currency_code: "" })
|
||||||
),
|
),
|
||||||
|
|
||||||
taxes: invoiceItem.taxes.getCodesToString(),
|
|
||||||
|
|
||||||
subtotal_amount: allAmounts.subtotalAmount.toObjectString(),
|
subtotal_amount: allAmounts.subtotalAmount.toObjectString(),
|
||||||
|
|
||||||
discount_percentage: invoiceItem.discountPercentage.match(
|
discount_percentage: invoiceItem.discountPercentage.match(
|
||||||
@ -40,8 +38,11 @@ export class CustomerInvoiceItemsFullPresenter extends Presenter {
|
|||||||
),
|
),
|
||||||
|
|
||||||
discount_amount: allAmounts.discountAmount.toObjectString(),
|
discount_amount: allAmounts.discountAmount.toObjectString(),
|
||||||
|
|
||||||
taxable_amount: allAmounts.taxableAmount.toObjectString(),
|
taxable_amount: allAmounts.taxableAmount.toObjectString(),
|
||||||
|
taxes: invoiceItem.taxes.getCodesToString(),
|
||||||
taxes_amount: allAmounts.taxesAmount.toObjectString(),
|
taxes_amount: allAmounts.taxesAmount.toObjectString(),
|
||||||
|
|
||||||
total_amount: allAmounts.totalAmount.toObjectString(),
|
total_amount: allAmounts.totalAmount.toObjectString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { toEmptyString } from "@repo/rdx-ddd";
|
|||||||
import { GetCustomerInvoiceByIdResponseDTO } from "../../../../common/dto";
|
import { GetCustomerInvoiceByIdResponseDTO } from "../../../../common/dto";
|
||||||
import { CustomerInvoice } from "../../../domain";
|
import { CustomerInvoice } from "../../../domain";
|
||||||
import { CustomerInvoiceItemsFullPresenter } from "./customer-invoice-items.full.presenter";
|
import { CustomerInvoiceItemsFullPresenter } from "./customer-invoice-items.full.presenter";
|
||||||
|
import { RecipientInvoiceFullPresenter } from "./recipient-invoice.full.representer";
|
||||||
|
|
||||||
export class CustomerInvoiceFullPresenter extends Presenter<
|
export class CustomerInvoiceFullPresenter extends Presenter<
|
||||||
CustomerInvoice,
|
CustomerInvoice,
|
||||||
@ -14,6 +15,12 @@ export class CustomerInvoiceFullPresenter extends Presenter<
|
|||||||
projection: "FULL",
|
projection: "FULL",
|
||||||
}) as CustomerInvoiceItemsFullPresenter;
|
}) as CustomerInvoiceItemsFullPresenter;
|
||||||
|
|
||||||
|
const recipientPresenter = this.presenterRegistry.getPresenter({
|
||||||
|
resource: "recipient-invoice",
|
||||||
|
projection: "FULL",
|
||||||
|
}) as RecipientInvoiceFullPresenter;
|
||||||
|
|
||||||
|
const recipient = recipientPresenter.toOutput(invoice);
|
||||||
const items = itemsPresenter.toOutput(invoice.items);
|
const items = itemsPresenter.toOutput(invoice.items);
|
||||||
const allAmounts = invoice.getAllAmounts();
|
const allAmounts = invoice.getAllAmounts();
|
||||||
|
|
||||||
@ -33,6 +40,9 @@ export class CustomerInvoiceFullPresenter extends Presenter<
|
|||||||
language_code: invoice.languageCode.toString(),
|
language_code: invoice.languageCode.toString(),
|
||||||
currency_code: invoice.currencyCode.toString(),
|
currency_code: invoice.currencyCode.toString(),
|
||||||
|
|
||||||
|
customer_id: invoice.customerId.toString(),
|
||||||
|
recipient,
|
||||||
|
|
||||||
taxes: invoice.taxes.getCodesToString(),
|
taxes: invoice.taxes.getCodesToString(),
|
||||||
|
|
||||||
subtotal_amount: allAmounts.subtotalAmount.toObjectString(),
|
subtotal_amount: allAmounts.subtotalAmount.toObjectString(),
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./customer-invoice-items.full.presenter";
|
export * from "./customer-invoice-items.full.presenter";
|
||||||
export * from "./customer-invoice.full.presenter";
|
export * from "./customer-invoice.full.presenter";
|
||||||
|
export * from "./recipient-invoice.full.representer";
|
||||||
|
|||||||
@ -0,0 +1,45 @@
|
|||||||
|
import { Presenter } from "@erp/core/api";
|
||||||
|
import { DomainValidationError, toEmptyString } from "@repo/rdx-ddd";
|
||||||
|
import { GetCustomerInvoiceByIdResponseDTO } from "../../../../common/dto";
|
||||||
|
import { CustomerInvoice, InvoiceRecipient } from "../../../domain";
|
||||||
|
|
||||||
|
type GetRecipientInvoiceByInvoiceIdResponseDTO = GetCustomerInvoiceByIdResponseDTO["recipient"];
|
||||||
|
|
||||||
|
export class RecipientInvoiceFullPresenter extends Presenter {
|
||||||
|
toOutput(invoice: CustomerInvoice): GetRecipientInvoiceByInvoiceIdResponseDTO {
|
||||||
|
if (!invoice.recipient) {
|
||||||
|
throw DomainValidationError.requiredValue("recipient", {
|
||||||
|
cause: invoice,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return invoice.recipient.match(
|
||||||
|
(recipient: InvoiceRecipient) => {
|
||||||
|
return {
|
||||||
|
id: invoice.customerId.toString(),
|
||||||
|
name: recipient.name.toString(),
|
||||||
|
tin: recipient.tin.toString(),
|
||||||
|
street: toEmptyString(recipient.street, (value) => value.toString()),
|
||||||
|
street2: toEmptyString(recipient.street2, (value) => value.toString()),
|
||||||
|
city: toEmptyString(recipient.city, (value) => value.toString()),
|
||||||
|
province: toEmptyString(recipient.province, (value) => value.toString()),
|
||||||
|
postal_code: toEmptyString(recipient.postalCode, (value) => value.toString()),
|
||||||
|
country: toEmptyString(recipient.country, (value) => value.toString()),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
return {
|
||||||
|
id: "",
|
||||||
|
name: "",
|
||||||
|
tin: "",
|
||||||
|
street: "",
|
||||||
|
street2: "",
|
||||||
|
city: "",
|
||||||
|
province: "",
|
||||||
|
postal_code: "",
|
||||||
|
country: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -45,12 +45,13 @@
|
|||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin-top: 25px;
|
margin-top: 0px;
|
||||||
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
table th,
|
table th,
|
||||||
table td {
|
table td {
|
||||||
border: 1px solid #ccc;
|
border: 0px solid #ccc;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
@ -98,22 +99,21 @@
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
|
|
||||||
<aside class="flex items-start mb-4 w-full">
|
<aside class="flex items-start mb-4 w-full">
|
||||||
<!-- Bloque IZQUIERDO: imagen arriba + texto abajo, alineado a la izquierda -->
|
<!-- Bloque IZQUIERDO: imagen arriba + texto abajo, alineado a la izquierda -->
|
||||||
<div class="w-[70%] flex flex-col items-start text-left">
|
<div class="w-[70%] flex flex-col items-start text-left">
|
||||||
<img src="https://rodax-software.com/images/logo1.jpg" alt="Logo Rodax" class="block h-14 w-auto mb-1" />
|
<img src="https://rodax-software.com/images/logo1.jpg" alt="Logo Rodax" class="block h-14 w-auto mb-1" />
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="p-1 ">
|
<div class="p-1 ">
|
||||||
<p><span>Factura nº:</span>xxxxxxxx</p>
|
<p>Factura nº:<strong> {{invoice_number}}</strong></p>
|
||||||
<p><span>Fecha:</span>12/12/2024</p>
|
<p><span>Fecha:<strong> {{operation_date}}</strong></p>
|
||||||
|
<p><span>Página:</span>1 / 1</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-1 ml-9">
|
<div class="p-1 ml-9">
|
||||||
<h2 class="font-semibold uppercase mb-1">{{customer.name}}</h2>
|
<h2 class="font-semibold uppercase mb-1">{{recipient.name}}</h2>
|
||||||
<p>AAAA</p>
|
<p>{{recipient.tin}}</p>
|
||||||
<p>BBBBBBsdfsfsdf sfsdf sf sdfs fsdfsd fsdf sdfsd fds </p>
|
<p>{{recipient.street}}</p>
|
||||||
<p>CCCCC</p>
|
<p>{{recipient.postal_code}} {{recipient.city}} {{recipient.province}}</p>
|
||||||
<p>DDDDD</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -130,166 +130,90 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="relative bg-blue-400">
|
<main id="main">
|
||||||
<!-- Badge TOTAL superpuesto -->
|
<section id="details">
|
||||||
<div class="absolute -top-7 right-0">
|
|
||||||
<div class="relative bg-[#f08119] text-white text-sm font-semibold px-3 py-1 shadow">
|
<div class="relative pt-0 border-b border-black">
|
||||||
TOTAL: 960,56 €
|
<!-- Badge TOTAL superpuesto -->
|
||||||
<!-- Triángulo izquierdo -->
|
<div class="absolute -top-7 right-0">
|
||||||
<span aria-hidden="true" class="absolute -left-3 top-0 bottom-0 my-auto h-0 w-0
|
<div class="relative bg-[#f08119] text-white text-sm font-semibold px-3 py-1 shadow">
|
||||||
border-y-[14px] border-y-transparent
|
TOTAL: {{total_amount.value}} €
|
||||||
border-r-[14px] border-r-amber-500"></span>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tu tabla -->
|
<!-- Tu tabla -->
|
||||||
<table class="w-full border-t border-black">
|
<table class="table-header">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="text-left">
|
<tr class="text-left">
|
||||||
<th class="py-2">Concepto</th>
|
<th class="py-2">Concepto</th>
|
||||||
<th class="py-2">Cantidad</th>
|
<th class="py-2">Cantidad</th>
|
||||||
<th class="py-2">Precio unidad</th>
|
<th class="py-2">Precio unidad</th>
|
||||||
<th class="py-2">Importe total</th>
|
<th class="py-2">Importe total</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
{{#each items}}
|
||||||
<td>Mantenimiento de sistemas informáticos - Agosto (1 Equipo Servidor, 30 Ordenadores, 2 Impresoras, Disco
|
<tr>
|
||||||
copias de seguridad)</td>
|
<td>{{description}}</td>
|
||||||
<td>1</td>
|
<td class="text-right">{{quantity.value}}</td>
|
||||||
<td>0,14 €</td>
|
<td class="text-right">{{unit_amount.value}} €</td>
|
||||||
<td>0,14 €</td>
|
<td class="text-right">{{total_amount.value}} €</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
{{/each}}
|
||||||
<td>Mantenimiento del programa FactuGES</td>
|
</tbody>
|
||||||
<td>1</td>
|
</table>
|
||||||
<td>40,00 €</td>
|
</section>
|
||||||
<td>40,00 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Control VPN para portátil Rubén</td>
|
|
||||||
<td>1</td>
|
|
||||||
<td>6,00 €</td>
|
|
||||||
<td>6,00 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Control VPN para portátil Míriam</td>
|
|
||||||
<td>1</td>
|
|
||||||
<td>6,00 €</td>
|
|
||||||
<td>6,00 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Control VPN para portátil Fernando</td>
|
|
||||||
<td>1</td>
|
|
||||||
<td>6,00 €</td>
|
|
||||||
<td>6,00 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Control VPN para portátil Elena</td>
|
|
||||||
<td>1</td>
|
|
||||||
<td>6,00 €</td>
|
|
||||||
<td>6,00 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Control VPN para portátil Miguel</td>
|
|
||||||
<td>1</td>
|
|
||||||
<td>6,00 €</td>
|
|
||||||
<td>6,00 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Control VPN para portátil Adrian</td>
|
|
||||||
<td>1</td>
|
|
||||||
<td>6,00 €</td>
|
|
||||||
<td>6,00 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Control VPN para portátil David Lablanca</td>
|
|
||||||
<td>1</td>
|
|
||||||
<td>6,00 €</td>
|
|
||||||
<td>6,00 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Control VPN para portátil Noemí</td>
|
|
||||||
<td>1</td>
|
|
||||||
<td>6,00 €</td>
|
|
||||||
<td>6,00 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Control VPN para portátil John</td>
|
|
||||||
<td>1</td>
|
|
||||||
<td>6,00 €</td>
|
|
||||||
<td>6,00 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Control VPN para portátil Eva</td>
|
|
||||||
<td>1</td>
|
|
||||||
<td>6,00 €</td>
|
|
||||||
<td>6,00 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Control VPN para portátil Alberto</td>
|
|
||||||
<td>1</td>
|
|
||||||
<td>6,00 €</td>
|
|
||||||
<td>6,00 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Mantenimiento mensual copia de seguridad remota y VPN (Uecko Madrid)</td>
|
|
||||||
<td>1</td>
|
|
||||||
<td>40,00 €</td>
|
|
||||||
<td>40,00 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>50% dto fidelización servicios contratados</td>
|
|
||||||
<td>-1</td>
|
|
||||||
<td>20,00 €</td>
|
|
||||||
<td>-20,00 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Mantenimiento de presupuestador web para distribuidores (Agosto)</td>
|
|
||||||
<td>1</td>
|
|
||||||
<td>375,00 €</td>
|
|
||||||
<td>375,00 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Informe de compras de artículos (Presupuesto 22/04/25)</td>
|
|
||||||
<td>1</td>
|
|
||||||
<td>260,00 €</td>
|
|
||||||
<td>260,00 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Informe presupuestos cliente (Gunni Tentrino) – modificación funcionalidad visible. Sin cargo.</td>
|
|
||||||
<td>1</td>
|
|
||||||
<td>0,00 €</td>
|
|
||||||
<td>0,00 €</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<table class="totals">
|
<section id="resume" class="flex items-center justify-between pb-4 mb-4">
|
||||||
<tr>
|
|
||||||
<td class="label">Base imponible:</td>
|
|
||||||
<td>761,14 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="label">IVA (21%):</td>
|
|
||||||
<td>159,84 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="label"><strong>Total factura:</strong></td>
|
|
||||||
<td><strong>960,56 €</strong></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<footer>
|
<div class="grow">
|
||||||
|
<div class="pt-4">
|
||||||
|
<p class="text-sm"><strong>Forma de pago:</strong> {{payment_method}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="pt-4">
|
||||||
|
<p class="text-sm"><strong>Notas:</strong> {{notes}} </p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grow">
|
||||||
|
<table class="table-header min-w-full bg-transparent">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="px-4 py-2 text-right">Importe neto</td>
|
||||||
|
<td class="px-4 py-2 text-right">761,14 €</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="px-4 py-2 text-right">Descuento 0%</td>
|
||||||
|
<td class="px-4 py-2 text-right">0</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="px-4 py-2 text-right">Base imponible</td>
|
||||||
|
<td class="px-4 py-2 text-right">765,14€</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="px-4 py-2 text-right">IVA 21%</td>
|
||||||
|
<td class="px-4 py-2 text-right">159,84 €</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="bg-[#f08119] text-white font-semibold">
|
||||||
|
<td class="px-4 py-2 text-right bg-amber-700">Total factura</td>
|
||||||
|
<td class="px-4 py-2 text-right">960,56 €</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer id="footer" class="mt-4">
|
||||||
|
<aside>
|
||||||
<p>Insc. en el Reg. Merc. de Madrid, Tomo 20.073, Libro 0, Folio 141, Sección 8, Hoja M-354212 | CIF: B83999441 -
|
<p>Insc. en el Reg. Merc. de Madrid, Tomo 20.073, Libro 0, Folio 141, Sección 8, Hoja M-354212 | CIF: B83999441 -
|
||||||
Rodax Software S.L.</p>
|
Rodax Software S.L.</p>
|
||||||
<p><strong>Forma de pago:</strong> Domiciliación bancaria</p>
|
</aside>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { DomainValidationError } from "@erp/core/api";
|
|
||||||
import {
|
import {
|
||||||
AggregateRoot,
|
AggregateRoot,
|
||||||
CurrencyCode,
|
CurrencyCode,
|
||||||
|
DomainValidationError,
|
||||||
LanguageCode,
|
LanguageCode,
|
||||||
Percentage,
|
Percentage,
|
||||||
TextValue,
|
TextValue,
|
||||||
@ -168,8 +168,27 @@ export class CustomerInvoice
|
|||||||
return this.recipient.isSome();
|
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 {
|
public getSubtotalAmount(): InvoiceAmount {
|
||||||
const itemsSubtotal = this.items.getTotalAmount().convertScale(2);
|
const itemsSubtotal = this.items.getSubtotalAmount().convertScale(2);
|
||||||
|
|
||||||
return InvoiceAmount.create({
|
return InvoiceAmount.create({
|
||||||
value: itemsSubtotal.value,
|
value: itemsSubtotal.value,
|
||||||
@ -178,11 +197,11 @@ export class CustomerInvoice
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getDiscountAmount(): InvoiceAmount {
|
public getDiscountAmount(): InvoiceAmount {
|
||||||
return this.getSubtotalAmount().percentage(this.discountPercentage) as InvoiceAmount;
|
return this._getDiscountAmount(this.getSubtotalAmount());
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTaxableAmount(): InvoiceAmount {
|
public getTaxableAmount(): InvoiceAmount {
|
||||||
return this.getSubtotalAmount().subtract(this.getDiscountAmount()) as InvoiceAmount;
|
return this._getTaxableAmount(this.getSubtotalAmount(), this.getDiscountAmount());
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTaxesAmount(): InvoiceAmount {
|
public getTaxesAmount(): InvoiceAmount {
|
||||||
@ -191,20 +210,24 @@ export class CustomerInvoice
|
|||||||
|
|
||||||
public getTotalAmount(): InvoiceAmount {
|
public getTotalAmount(): InvoiceAmount {
|
||||||
const taxableAmount = this.getTaxableAmount();
|
const taxableAmount = this.getTaxableAmount();
|
||||||
return taxableAmount.add(this._getTaxesAmount(taxableAmount)) as InvoiceAmount;
|
const taxesAmount = this._getTaxesAmount(taxableAmount);
|
||||||
|
|
||||||
|
return this._getTotalAmount(taxableAmount, taxesAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAllAmounts() {
|
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 {
|
return {
|
||||||
subtotalAmount: this.getSubtotalAmount(),
|
subtotalAmount,
|
||||||
discountAmount: this.getDiscountAmount(),
|
discountAmount,
|
||||||
taxableAmount: this.getTaxableAmount(),
|
taxableAmount,
|
||||||
taxesAmount: this.getTaxesAmount(),
|
taxesAmount,
|
||||||
totalAmount: this.getTotalAmount(),
|
totalAmount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getTaxesAmount(_taxableAmount: InvoiceAmount): InvoiceAmount {
|
|
||||||
return this._taxes.getTaxesAmount(_taxableAmount);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,6 +95,26 @@ export class CustomerInvoiceItem
|
|||||||
return this.getProps();
|
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 {
|
public getSubtotalAmount(): ItemAmount {
|
||||||
const curCode = this.currencyCode.code;
|
const curCode = this.currencyCode.code;
|
||||||
const quantity = this.quantity.match(
|
const quantity = this.quantity.match(
|
||||||
@ -110,15 +130,11 @@ export class CustomerInvoiceItem
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getDiscountAmount(): ItemAmount {
|
public getDiscountAmount(): ItemAmount {
|
||||||
const discount = this.discountPercentage.match(
|
return this._getDiscountAmount(this.getSubtotalAmount());
|
||||||
(percentage) => percentage,
|
|
||||||
() => ItemDiscount.zero()
|
|
||||||
);
|
|
||||||
return this.getSubtotalAmount().percentage(discount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTaxableAmount(): ItemAmount {
|
public getTaxableAmount(): ItemAmount {
|
||||||
return this.getSubtotalAmount().subtract(this.getDiscountAmount());
|
return this._getTaxableAmount(this.getSubtotalAmount(), this.getDiscountAmount());
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTaxesAmount(): ItemAmount {
|
public getTaxesAmount(): ItemAmount {
|
||||||
@ -127,20 +143,24 @@ export class CustomerInvoiceItem
|
|||||||
|
|
||||||
public getTotalAmount(): ItemAmount {
|
public getTotalAmount(): ItemAmount {
|
||||||
const taxableAmount = this.getTaxableAmount();
|
const taxableAmount = this.getTaxableAmount();
|
||||||
return taxableAmount.add(this._getTaxesAmount(taxableAmount));
|
const taxesAmount = this._getTaxesAmount(taxableAmount);
|
||||||
|
|
||||||
|
return this._getTotalAmount(taxableAmount, taxesAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAllAmounts() {
|
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 {
|
return {
|
||||||
subtotalAmount: this.getSubtotalAmount(),
|
subtotalAmount,
|
||||||
discountAmount: this.getDiscountAmount(),
|
discountAmount,
|
||||||
taxableAmount: this.getTaxableAmount(),
|
taxableAmount,
|
||||||
taxesAmount: this.getTaxesAmount(),
|
taxesAmount,
|
||||||
totalAmount: this.getTotalAmount(),
|
totalAmount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getTaxesAmount(_taxableAmount: ItemAmount): ItemAmount {
|
|
||||||
return this.props.taxes.getTaxesAmount(_taxableAmount);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,9 +36,37 @@ export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
|
|||||||
return super.add(item);
|
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 {
|
public getTotalAmount(): ItemAmount {
|
||||||
return this.getAll().reduce(
|
return this.getAll().reduce(
|
||||||
(total, tax) => total.add(tax.getTotalAmount()),
|
(total, item) => total.add(item.getTotalAmount()),
|
||||||
ItemAmount.zero(this._currencyCode.code)
|
ItemAmount.zero(this._currencyCode.code)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,36 +35,60 @@ export class InvoiceAmount extends MoneyValue {
|
|||||||
convertScale(newScale: number) {
|
convertScale(newScale: number) {
|
||||||
const mv = super.convertScale(newScale);
|
const mv = super.convertScale(newScale);
|
||||||
const p = mv.toPrimitive();
|
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) {
|
add(addend: MoneyValue) {
|
||||||
const mv = super.add(addend);
|
const mv = super.add(addend);
|
||||||
const p = mv.toPrimitive();
|
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) {
|
subtract(subtrahend: MoneyValue) {
|
||||||
const mv = super.subtract(subtrahend);
|
const mv = super.subtract(subtrahend);
|
||||||
const p = mv.toPrimitive();
|
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) {
|
multiply(multiplier: number | Quantity) {
|
||||||
const mv = super.multiply(multiplier);
|
const mv = super.multiply(multiplier);
|
||||||
const p = mv.toPrimitive();
|
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) {
|
divide(divisor: number | Quantity) {
|
||||||
const mv = super.divide(divisor);
|
const mv = super.divide(divisor);
|
||||||
const p = mv.toPrimitive();
|
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) {
|
percentage(percentage: number | Percentage) {
|
||||||
const mv = super.percentage(percentage);
|
const mv = super.percentage(percentage);
|
||||||
const p = mv.toPrimitive();
|
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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,36 +35,63 @@ export class ItemAmount extends MoneyValue {
|
|||||||
convertScale(newScale: number) {
|
convertScale(newScale: number) {
|
||||||
const mv = super.convertScale(newScale);
|
const mv = super.convertScale(newScale);
|
||||||
const p = mv.toPrimitive();
|
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) {
|
add(addend: MoneyValue) {
|
||||||
const mv = super.add(addend);
|
const mv = super.add(addend);
|
||||||
const p = mv.toPrimitive();
|
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) {
|
subtract(subtrahend: MoneyValue) {
|
||||||
const mv = super.subtract(subtrahend);
|
const mv = super.subtract(subtrahend);
|
||||||
const p = mv.toPrimitive();
|
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) {
|
multiply(multiplier: number | Quantity) {
|
||||||
const mv = super.multiply(multiplier);
|
const mv = super.multiply(multiplier);
|
||||||
const p = mv.toPrimitive();
|
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) {
|
divide(divisor: number | Quantity) {
|
||||||
const mv = super.divide(divisor);
|
const mv = super.divide(divisor);
|
||||||
const p = mv.toPrimitive();
|
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) {
|
percentage(percentage: number | Percentage) {
|
||||||
const mv = super.percentage(percentage);
|
const mv = super.percentage(percentage);
|
||||||
const p = mv.toPrimitive();
|
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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import {
|
|||||||
GetCustomerInvoiceUseCase,
|
GetCustomerInvoiceUseCase,
|
||||||
ListCustomerInvoicesPresenter,
|
ListCustomerInvoicesPresenter,
|
||||||
ListCustomerInvoicesUseCase,
|
ListCustomerInvoicesUseCase,
|
||||||
|
RecipientInvoiceFullPresenter,
|
||||||
ReportCustomerInvoiceUseCase,
|
ReportCustomerInvoiceUseCase,
|
||||||
} from "../application";
|
} from "../application";
|
||||||
|
|
||||||
@ -77,6 +78,13 @@ export function buildCustomerInvoiceDependencies(params: ModuleParams): Customer
|
|||||||
},
|
},
|
||||||
presenter: new CustomerInvoiceItemsFullPresenter(presenterRegistry),
|
presenter: new CustomerInvoiceItemsFullPresenter(presenterRegistry),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: {
|
||||||
|
resource: "recipient-invoice",
|
||||||
|
projection: "FULL",
|
||||||
|
},
|
||||||
|
presenter: new RecipientInvoiceFullPresenter(presenterRegistry),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: {
|
key: {
|
||||||
resource: "customer-invoice",
|
resource: "customer-invoice",
|
||||||
|
|||||||
@ -130,6 +130,7 @@ export class CustomerInvoiceRepository
|
|||||||
|
|
||||||
const row = await CustomerInvoiceModel.findOne({
|
const row = await CustomerInvoiceModel.findOne({
|
||||||
where: { id: id.toString(), company_id: companyId.toString() },
|
where: { id: id.toString(), company_id: companyId.toString() },
|
||||||
|
order: [[{ model: CustomerInvoiceItemModel, as: "items" }, "position", "ASC"]],
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: CustomerModel,
|
model: CustomerModel,
|
||||||
|
|||||||
@ -217,7 +217,9 @@ export default (database: Sequelize) => {
|
|||||||
|
|
||||||
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
||||||
|
|
||||||
defaultScope: {},
|
defaultScope: {
|
||||||
|
order: [["position", "ASC"]],
|
||||||
|
},
|
||||||
|
|
||||||
scopes: {},
|
scopes: {},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,19 @@ export const GetCustomerInvoiceByIdResponseSchema = z.object({
|
|||||||
language_code: z.string(),
|
language_code: z.string(),
|
||||||
currency_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(),
|
taxes: z.string(),
|
||||||
|
|
||||||
subtotal_amount: MoneySchema,
|
subtotal_amount: MoneySchema,
|
||||||
|
|||||||
@ -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 (
|
||||||
|
<>
|
||||||
|
<AppBreadcrumb />
|
||||||
|
<AppContent>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<div className='space-y-2' aria-hidden='true'>
|
||||||
|
<div className='h-7 w-64 rounded-md bg-muted animate-pulse' />
|
||||||
|
<div className='h-5 w-96 rounded-md bg-muted animate-pulse' />
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<BackHistoryButton />
|
||||||
|
<Button disabled aria-busy>
|
||||||
|
{t("pages.update.submit")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='mt-6 grid gap-4' aria-hidden='true'>
|
||||||
|
<div className='h-10 w-full rounded-md bg-muted animate-pulse' />
|
||||||
|
<div className='h-10 w-full rounded-md bg-muted animate-pulse' />
|
||||||
|
<div className='h-28 w-full rounded-md bg-muted animate-pulse' />
|
||||||
|
</div>
|
||||||
|
<span className='sr-only'>{t("pages.update.loading", "Cargando cliente...")}</span>
|
||||||
|
</AppContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
16
modules/customers/src/web/components/error-alert.tsx
Normal file
16
modules/customers/src/web/components/error-alert.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// components/ErrorAlert.tsx
|
||||||
|
interface ErrorAlertProps {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ErrorAlert = ({ title, message }: ErrorAlertProps) => (
|
||||||
|
<div
|
||||||
|
className='mb-4 rounded-lg border border-destructive/50 bg-destructive/10 p-4'
|
||||||
|
role='alert'
|
||||||
|
aria-live='assertive'
|
||||||
|
>
|
||||||
|
<p className='font-semibold text-destructive-foreground'>{title}</p>
|
||||||
|
<p className='text-sm text-destructive-foreground/90'>{message}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@ -1,3 +1,6 @@
|
|||||||
export * from "./client-selector";
|
export * from "./client-selector";
|
||||||
|
export * from "./customer-editor-skeleton";
|
||||||
export * from "./customers-layout";
|
export * from "./customers-layout";
|
||||||
export * from "./customers-list-grid";
|
export * from "./customers-list-grid";
|
||||||
|
export * from "./error-alert";
|
||||||
|
export * from "./not-found-card";
|
||||||
|
|||||||
19
modules/customers/src/web/components/not-found-card.tsx
Normal file
19
modules/customers/src/web/components/not-found-card.tsx
Normal file
@ -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) => (
|
||||||
|
<>
|
||||||
|
<div className='rounded-lg border bg-card p-6'>
|
||||||
|
<h3 className='text-lg font-semibold'>{title}</h3>
|
||||||
|
<p className='text-sm text-muted-foreground'>{message}</p>
|
||||||
|
</div>
|
||||||
|
<div className='mt-4 flex items-center justify-end'>
|
||||||
|
<BackHistoryButton />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
28
modules/customers/src/web/constants/customer.constants.ts
Normal file
28
modules/customers/src/web/constants/customer.constants.ts
Normal file
@ -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;
|
||||||
1
modules/customers/src/web/constants/index.ts
Normal file
1
modules/customers/src/web/constants/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./customer.constants";
|
||||||
@ -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 { 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 queryClient = useQueryClient();
|
||||||
const dataSource = useDataSource();
|
const dataSource = useDataSource();
|
||||||
const keys = useQueryKey();
|
|
||||||
|
|
||||||
return useMutation<UpdateCustomerByIdRequestDTO, Error, Partial<UpdateCustomerByIdRequestDTO>>({
|
return useMutation<CustomerData, Error, CustomerCreateData>({
|
||||||
mutationKey: ["customer:create"],
|
mutationKey: ["customer:create"],
|
||||||
mutationFn: (data) => {
|
mutationFn: async (data: CustomerCreateData) => {
|
||||||
console.log(data);
|
const created = await dataSource.createOne("customers", data);
|
||||||
return dataSource.createOne("customers", data);
|
return created as CustomerData;
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["customers"] });
|
// Invalida el listado de clientes para incluir el nuevo
|
||||||
|
queryClient.invalidateQueries({ queryKey: CUSTOMERS_LIST_KEY });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|||||||
@ -1,32 +1,36 @@
|
|||||||
import { useDataSource } from "@erp/core/hooks";
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
import {
|
|
||||||
UpdateCustomerByIdRequestDTO,
|
|
||||||
UpdateCustomerByIdResponseDTO,
|
|
||||||
} from "@erp/customer-invoices/common";
|
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { CustomerData, CustomerUpdateData } from "../schemas";
|
||||||
import { CUSTOMER_QUERY_KEY } from "./use-customer-query";
|
import { CUSTOMER_QUERY_KEY } from "./use-customer-query";
|
||||||
|
|
||||||
export const CUSTOMERS_LIST_KEY = ["customers"] as const;
|
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 queryClient = useQueryClient();
|
||||||
const dataSource = useDataSource();
|
const dataSource = useDataSource();
|
||||||
|
|
||||||
return useMutation<UpdateCustomerByIdResponseDTO, Error, UpdateCustomerByIdRequestDTO>({
|
return useMutation<CustomerData, Error, UpdateCustomerPayload>({
|
||||||
mutationKey: ["customer:update", customerId],
|
mutationKey: ["customer:update"], //, customerId],
|
||||||
mutationFn: async (input) => {
|
|
||||||
if (!customerId) throw new Error("customerId is required");
|
mutationFn: async (payload) => {
|
||||||
const updated = await dataSource.updateOne("customers", customerId, input);
|
const { id: customerId, data } = payload;
|
||||||
return updated as UpdateCustomerByIdResponseDTO;
|
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
|
// Refresca inmediatamente el detalle
|
||||||
queryClient.setQueryData<UpdateCustomerByIdResponseDTO>(
|
queryClient.setQueryData<CustomerData>(CUSTOMER_QUERY_KEY(customerId), updated);
|
||||||
CUSTOMER_QUERY_KEY(customerId),
|
|
||||||
updated
|
|
||||||
);
|
|
||||||
|
|
||||||
// Otra opción es invalidar el detalle para forzar refetch:
|
// Otra opción es invalidar el detalle para forzar refetch:
|
||||||
// queryClient.invalidateQueries({ queryKey: CUSTOMER_QUERY_KEY(customerId) });
|
// queryClient.invalidateQueries({ queryKey: CUSTOMER_QUERY_KEY(customerId) });
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import {
|
|||||||
|
|
||||||
import { useUnsavedChangesNotifier } from "@erp/core/hooks";
|
import { useUnsavedChangesNotifier } from "@erp/core/hooks";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import { CustomerData, CustomerDataUpdateUpdateSchema } from "../../schemas";
|
import { CustomerData, CustomerUpdateSchema } from "../../schemas";
|
||||||
|
|
||||||
const defaultCustomerData = {
|
const defaultCustomerData = {
|
||||||
id: "5e4dc5b3-96b9-4968-9490-14bd032fec5f",
|
id: "5e4dc5b3-96b9-4968-9490-14bd032fec5f",
|
||||||
@ -64,7 +64,7 @@ export const CustomerEditForm = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const form = useForm<CustomerData>({
|
const form = useForm<CustomerData>({
|
||||||
resolver: zodResolver(CustomerDataUpdateUpdateSchema),
|
resolver: zodResolver(CustomerUpdateSchema),
|
||||||
defaultValues: initialData,
|
defaultValues: initialData,
|
||||||
disabled: isPending,
|
disabled: isPending,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
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 { TaxesMultiSelectField } from "@erp/core/components";
|
||||||
import { SelectField, TextAreaField, TextField } from "@repo/rdx-ui/components";
|
import { SelectField, TextAreaField, TextField } from "@repo/rdx-ui/components";
|
||||||
@ -21,26 +21,26 @@ import {
|
|||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
|
|
||||||
import { useUnsavedChangesNotifier } from "@erp/core/hooks";
|
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 { useTranslation } from "../../i18n";
|
||||||
import { CustomerData, CustomerDataUpdateUpdateSchema } from "../../schemas";
|
import { CustomerData, CustomerUpdateData, CustomerUpdateSchema } from "../../schemas";
|
||||||
|
|
||||||
interface CustomerFormProps {
|
interface CustomerFormProps {
|
||||||
formId: string;
|
formId: string;
|
||||||
data?: GetCustomerByIdResponseDTO;
|
data?: CustomerData;
|
||||||
isPending?: boolean;
|
isPending?: boolean;
|
||||||
/**
|
/**
|
||||||
* Callback function to handle form submission.
|
* Callback function to handle form submission.
|
||||||
* @param data - The customer data submitted by the form.
|
* @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) => {
|
export const CustomerEditForm = ({ formId, data, onSubmit, isPending }: CustomerFormProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const form = useForm<CustomerData>({
|
const form = useForm<CustomerUpdateData>({
|
||||||
resolver: zodResolver(CustomerDataUpdateUpdateSchema),
|
resolver: zodResolver(CustomerUpdateSchema),
|
||||||
defaultValues: data,
|
defaultValues: data,
|
||||||
disabled: isPending,
|
disabled: isPending,
|
||||||
});
|
});
|
||||||
@ -49,12 +49,12 @@ export const CustomerEditForm = ({ formId, data, onSubmit, isPending }: Customer
|
|||||||
isDirty: form.formState.isDirty,
|
isDirty: form.formState.isDirty,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = (data: CustomerData) => {
|
const handleSubmit = (data: CustomerUpdateData) => {
|
||||||
console.log("Datos del formulario:", data);
|
console.log("Datos del formulario:", data);
|
||||||
onSubmit?.(data);
|
onSubmit?.(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleError = (errors: any) => {
|
const handleError = (errors: FieldErrors<CustomerUpdateData>) => {
|
||||||
console.error("Errores en el formulario:", errors);
|
console.error("Errores en el formulario:", errors);
|
||||||
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
|
// 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);
|
form.reset(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
formState: { isDirty, dirtyFields },
|
||||||
|
} = form;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
|
<div className='mt-6 p-4 border rounded bg-gray-50'>
|
||||||
|
<p>
|
||||||
|
<strong>¿Formulario modificado?</strong> {isDirty ? "Sí" : "No"}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Campos modificados:</strong>{" "}
|
||||||
|
{Object.keys(dirtyFields).length > 0 ? Object.keys(dirtyFields).join(", ") : "Ninguno"}
|
||||||
|
</p>
|
||||||
|
</div>{" "}
|
||||||
<form id={formId} onSubmit={form.handleSubmit(handleSubmit, handleError)}>
|
<form id={formId} onSubmit={form.handleSubmit(handleSubmit, handleError)}>
|
||||||
<div className='w-full grid grid-cols-1 space-y-8 space-x-8 xl:grid-cols-2'>
|
<div className='w-full grid grid-cols-1 space-y-8 space-x-8 xl:grid-cols-2'>
|
||||||
{/* Información básica */}
|
{/* Información básica */}
|
||||||
@ -82,13 +95,13 @@ export const CustomerEditForm = ({ formId, data, onSubmit, isPending }: Customer
|
|||||||
<FormLabel>{t("form_fields.customer_type.label")}</FormLabel>
|
<FormLabel>{t("form_fields.customer_type.label")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
onValueChange={field.onChange}
|
onValueChange={(value) => field.onChange(value === "1")}
|
||||||
defaultValue={field.value ? "1" : "0"}
|
defaultValue={field.value ? "1" : "0"}
|
||||||
className='flex gap-6'
|
className='flex gap-6'
|
||||||
>
|
>
|
||||||
<FormItem className='flex items-center space-x-2'>
|
<FormItem className='flex items-center space-x-2'>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<RadioGroupItem value={"1"} />
|
<RadioGroupItem value='1' />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className='font-normal'>
|
<FormLabel className='font-normal'>
|
||||||
{t("form_fields.customer_type.company")}
|
{t("form_fields.customer_type.company")}
|
||||||
@ -97,7 +110,7 @@ export const CustomerEditForm = ({ formId, data, onSubmit, isPending }: Customer
|
|||||||
|
|
||||||
<FormItem className='flex items-center space-x-2'>
|
<FormItem className='flex items-center space-x-2'>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<RadioGroupItem value={"0"} />
|
<RadioGroupItem value='0' />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className='font-normal'>
|
<FormLabel className='font-normal'>
|
||||||
{t("form_fields.customer_type.individual")}
|
{t("form_fields.customer_type.individual")}
|
||||||
@ -188,16 +201,7 @@ export const CustomerEditForm = ({ formId, data, onSubmit, isPending }: Customer
|
|||||||
label={t("form_fields.country.label")}
|
label={t("form_fields.country.label")}
|
||||||
placeholder={t("form_fields.country.placeholder")}
|
placeholder={t("form_fields.country.placeholder")}
|
||||||
description={t("form_fields.country.description")}
|
description={t("form_fields.country.description")}
|
||||||
items={[
|
items={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" },
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@ -294,36 +298,12 @@ export const CustomerEditForm = ({ formId, data, onSubmit, isPending }: Customer
|
|||||||
|
|
||||||
<SelectField
|
<SelectField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='lang_code'
|
name='language_code'
|
||||||
required
|
required
|
||||||
label={t("form_fields.lang_code.label")}
|
label={t("form_fields.lang_code.label")}
|
||||||
placeholder={t("form_fields.lang_code.placeholder")}
|
placeholder={t("form_fields.lang_code.placeholder")}
|
||||||
description={t("form_fields.lang_code.description")}
|
description={t("form_fields.lang_code.description")}
|
||||||
items={[
|
items={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" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectField
|
|
||||||
control={form.control}
|
|
||||||
name='currency_code'
|
|
||||||
required
|
|
||||||
label={t("form_fields.currency_code.label")}
|
|
||||||
placeholder={t("form_fields.currency_code.placeholder")}
|
|
||||||
description={t("form_fields.currency_code.description")}
|
|
||||||
items={[
|
|
||||||
{ 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" },
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextAreaField
|
<TextAreaField
|
||||||
@ -338,7 +318,7 @@ export const CustomerEditForm = ({ formId, data, onSubmit, isPending }: Customer
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<Button type='submit'>Submit</Button>
|
<Button type='submit'>{t("pages.update.submit")}</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,8 +3,11 @@ import { Button } from "@repo/shadcn-ui/components";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { useUrlParamId } from "@erp/core/hooks";
|
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 { useCustomerQuery, useUpdateCustomerMutation } from "../../hooks";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
|
import { CustomerUpdateData } from "../../schemas";
|
||||||
import { CustomerEditForm } from "./customer-edit-form";
|
import { CustomerEditForm } from "./customer-edit-form";
|
||||||
|
|
||||||
export const CustomerUpdate = () => {
|
export const CustomerUpdate = () => {
|
||||||
@ -22,50 +25,29 @@ export const CustomerUpdate = () => {
|
|||||||
|
|
||||||
// 2) Estado de actualización (mutación)
|
// 2) Estado de actualización (mutación)
|
||||||
const {
|
const {
|
||||||
mutateAsync: updateAsync,
|
mutateAsync,
|
||||||
isPending: isUpdating,
|
isPending: isUpdating,
|
||||||
isError: isUpdateError,
|
isError: isUpdateError,
|
||||||
error: updateError,
|
error: updateError,
|
||||||
} = useUpdateCustomerMutation(customerId || "");
|
} = useUpdateCustomerMutation();
|
||||||
|
|
||||||
// 3) Submit con navegación condicionada por éxito
|
// 3) Submit con navegación condicionada por éxito
|
||||||
const handleSubmit = async (formData: any) => {
|
const handleSubmit = async (formData: CustomerUpdateData) => {
|
||||||
try {
|
try {
|
||||||
await updateAsync(formData); // solo navegamos si no lanza
|
const result = await mutateAsync({ id: customerId!, data: formData });
|
||||||
// toast?.({ title: t('pages.update.successTitle'), description: t('pages.update.successMsg') });
|
|
||||||
navigate("/customers/list");
|
if (result) {
|
||||||
|
showSuccessToast(t("pages.update.successTitle"), t("pages.update.successMsg"));
|
||||||
|
navigate("/customers/list");
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// toast?.({ variant: 'destructive', title: t('pages.update.errorTitle'), description: (e as Error).message });
|
showErrorToast(t("pages.update.errorTitle"), (e as Error).message);
|
||||||
// No navegamos en caso de error
|
} finally {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoadingCustomer) {
|
if (isLoadingCustomer) {
|
||||||
return (
|
return <CustomerEditorSkeleton />;
|
||||||
<>
|
|
||||||
<AppBreadcrumb />
|
|
||||||
<AppContent>
|
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<div className='h-7 w-64 rounded-md bg-muted animate-pulse' />
|
|
||||||
<div className='h-5 w-96 rounded-md bg-muted animate-pulse' />
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center gap-2'>
|
|
||||||
<BackHistoryButton />
|
|
||||||
<Button disabled aria-busy>
|
|
||||||
{t("pages.update.submit")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='mt-6 grid gap-4'>
|
|
||||||
{/* Skeleton simple para el formulario */}
|
|
||||||
<div className='h-10 w-full rounded-md bg-muted animate-pulse' />
|
|
||||||
<div className='h-10 w-full rounded-md bg-muted animate-pulse' />
|
|
||||||
<div className='h-28 w-full rounded-md bg-muted animate-pulse' />
|
|
||||||
</div>
|
|
||||||
</AppContent>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoadError) {
|
if (isLoadError) {
|
||||||
@ -73,19 +55,14 @@ export const CustomerUpdate = () => {
|
|||||||
<>
|
<>
|
||||||
<AppBreadcrumb />
|
<AppBreadcrumb />
|
||||||
<AppContent>
|
<AppContent>
|
||||||
<div
|
<ErrorAlert
|
||||||
className='mb-4 rounded-lg border border-destructive/50 bg-destructive/10 p-4'
|
title={t("pages.update.loadErrorTitle", "No se pudo cargar el cliente")}
|
||||||
role='alert'
|
message={
|
||||||
aria-live='assertive'
|
(loadError as Error)?.message ??
|
||||||
>
|
t("pages.update.loadErrorMsg", "Inténtalo de nuevo más tarde.")
|
||||||
<p className='font-semibold text-destructive-foreground'>
|
}
|
||||||
{t("pages.update.loadErrorTitle", "No se pudo cargar el cliente")}
|
/>
|
||||||
</p>
|
|
||||||
<p className='text-sm text-destructive-foreground/90'>
|
|
||||||
{(loadError as Error)?.message ??
|
|
||||||
t("pages.update.loadErrorMsg", "Inténtalo de nuevo más tarde.")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center justify-end'>
|
<div className='flex items-center justify-end'>
|
||||||
<BackHistoryButton />
|
<BackHistoryButton />
|
||||||
</div>
|
</div>
|
||||||
@ -94,26 +71,18 @@ export const CustomerUpdate = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!customerData) {
|
if (!customerData)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppBreadcrumb />
|
<AppBreadcrumb />
|
||||||
<AppContent>
|
<AppContent>
|
||||||
<div className='rounded-lg border bg-card p-6'>
|
<NotFoundCard
|
||||||
<h3 className='text-lg font-semibold'>
|
title={t("pages.update.notFoundTitle", "Cliente no encontrado")}
|
||||||
{t("pages.update.notFoundTitle", "Cliente no encontrado")}
|
message={t("pages.update.notFoundMsg", "Revisa el identificador o vuelve al listado.")}
|
||||||
</h3>
|
/>
|
||||||
<p className='text-sm text-muted-foreground'>
|
|
||||||
{t("pages.update.notFoundMsg", "Revisa el identificador o vuelve al listado.")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className='mt-4 flex items-center justify-end'>
|
|
||||||
<BackHistoryButton />
|
|
||||||
</div>
|
|
||||||
</AppContent>
|
</AppContent>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -145,19 +114,13 @@ export const CustomerUpdate = () => {
|
|||||||
</div>
|
</div>
|
||||||
{/* Alerta de error de actualización (si ha fallado el último intento) */}
|
{/* Alerta de error de actualización (si ha fallado el último intento) */}
|
||||||
{isUpdateError && (
|
{isUpdateError && (
|
||||||
<div
|
<ErrorAlert
|
||||||
className='mb-2 rounded-lg border border-destructive/50 bg-destructive/10 p-3'
|
title={t("pages.update.errorTitle", "No se pudo guardar los cambios")}
|
||||||
role='alert'
|
message={
|
||||||
aria-live='assertive'
|
(updateError as Error)?.message ??
|
||||||
>
|
t("pages.update.errorMsg", "Revisa los datos e inténtalo de nuevo.")
|
||||||
<p className='text-sm font-medium text-destructive-foreground'>
|
}
|
||||||
{t("pages.update.errorTitle", "No se pudo guardar los cambios")}
|
/>
|
||||||
</p>
|
|
||||||
<p className='text-xs text-destructive-foreground/90'>
|
|
||||||
{(updateError as Error)?.message ??
|
|
||||||
t("pages.update.errorMsg", "Revisa los datos e inténtalo de nuevo.")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className='flex flex-1 flex-col gap-4 p-4'>
|
<div className='flex flex-1 flex-col gap-4 p-4'>
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
|
CreateCustomerRequestDTO,
|
||||||
|
CreateCustomerRequestSchema,
|
||||||
GetCustomerByIdResponseDTO,
|
GetCustomerByIdResponseDTO,
|
||||||
UpdateCustomerByIdRequestDTO,
|
UpdateCustomerByIdRequestDTO,
|
||||||
UpdateCustomerByIdRequestSchema,
|
UpdateCustomerByIdRequestSchema,
|
||||||
@ -6,5 +8,8 @@ import {
|
|||||||
|
|
||||||
export type CustomerData = GetCustomerByIdResponseDTO;
|
export type CustomerData = GetCustomerByIdResponseDTO;
|
||||||
|
|
||||||
export const CustomerDataUpdateUpdateSchema = UpdateCustomerByIdRequestSchema;
|
export const CustomerCreateSchema = CreateCustomerRequestSchema;
|
||||||
export type CustomerDataFormUpdateDTO = UpdateCustomerByIdRequestDTO;
|
export type CustomerCreateData = CreateCustomerRequestDTO;
|
||||||
|
|
||||||
|
export const CustomerUpdateSchema = UpdateCustomerByIdRequestSchema;
|
||||||
|
export type CustomerUpdateData = UpdateCustomerByIdRequestDTO;
|
||||||
|
|||||||
@ -201,6 +201,10 @@ export class MoneyValue extends ValueObject<MoneyValueProps> implements IMoneyVa
|
|||||||
return this.dinero.hasSameAmount(comparator.dinero);
|
return this.dinero.hasSameAmount(comparator.dinero);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasSameScale(comparator: MoneyValue): boolean {
|
||||||
|
return this.dinero.getPrecision() === comparator.dinero.getPrecision();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Devuelve una cadena con el importe formateado.
|
* Devuelve una cadena con el importe formateado.
|
||||||
* Ejemplo: 123456 -> €1,234.56
|
* Ejemplo: 123456 -> €1,234.56
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes";
|
||||||
import { Toaster as Sonner, ToasterProps } from "sonner"
|
import { Toaster as Sonner, ToasterProps, toast } from "sonner";
|
||||||
|
|
||||||
const Toaster = ({ ...props }: ToasterProps) => {
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
const { theme = "system" } = useTheme()
|
const { theme = "system" } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sonner
|
<Sonner
|
||||||
theme={theme as ToasterProps["theme"]}
|
theme={theme as ToasterProps["theme"]}
|
||||||
className="toaster group"
|
className='toaster group'
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--normal-bg": "var(--popover)",
|
"--normal-bg": "var(--popover)",
|
||||||
@ -19,7 +19,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|||||||
}
|
}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export { Toaster }
|
export { Toaster, toast };
|
||||||
|
|||||||
@ -1,6 +1,25 @@
|
|||||||
import { type ClassValue, clsx } from "clsx";
|
import { type ClassValue, clsx } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { toast } from "../components/sonner.tsx";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user