Proformas
This commit is contained in:
parent
9b874eebf8
commit
f19ab6022b
@ -1,12 +1,12 @@
|
||||
import { Presenter } from "@erp/core/api";
|
||||
import type { GetIssuedInvoiceByIdResponseDTO } from "@erp/customer-invoices/common";
|
||||
import { toEmptyString } from "@repo/rdx-ddd";
|
||||
import type { ArrayElement } from "@repo/rdx-utils";
|
||||
|
||||
import type { GetIssueInvoiceByIdResponseDTO } from "../../../../common/dto";
|
||||
import type { CustomerInvoiceItem, CustomerInvoiceItems } from "../../../domain";
|
||||
|
||||
type GetCustomerInvoiceItemByInvoiceIdResponseDTO = ArrayElement<
|
||||
GetIssueInvoiceByIdResponseDTO["items"]
|
||||
GetIssuedInvoiceByIdResponseDTO["items"]
|
||||
>;
|
||||
|
||||
export class CustomerInvoiceItemsFullPresenter extends Presenter {
|
||||
@ -49,7 +49,7 @@ export class CustomerInvoiceItemsFullPresenter extends Presenter {
|
||||
};
|
||||
}
|
||||
|
||||
toOutput(invoiceItems: CustomerInvoiceItems): GetIssueInvoiceByIdResponseDTO["items"] {
|
||||
toOutput(invoiceItems: CustomerInvoiceItems): GetIssuedInvoiceByIdResponseDTO["items"] {
|
||||
return invoiceItems.map(this._mapItem);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Presenter } from "@erp/core/api";
|
||||
import { toEmptyString } from "@repo/rdx-ddd";
|
||||
|
||||
import type { GetIssueInvoiceByIdResponseDTO } from "../../../../common/dto";
|
||||
import type { GetIssuedInvoiceByIdResponseDTO } from "../../../../common/dto";
|
||||
import type { CustomerInvoice } from "../../../domain";
|
||||
|
||||
import type { CustomerInvoiceItemsFullPresenter } from "./customer-invoice-items.full.presenter";
|
||||
@ -9,9 +9,9 @@ import type { RecipientInvoiceFullPresenter } from "./recipient-invoice.full.rep
|
||||
|
||||
export class CustomerInvoiceFullPresenter extends Presenter<
|
||||
CustomerInvoice,
|
||||
GetIssueInvoiceByIdResponseDTO
|
||||
GetIssuedInvoiceByIdResponseDTO
|
||||
> {
|
||||
toOutput(invoice: CustomerInvoice): GetIssueInvoiceByIdResponseDTO {
|
||||
toOutput(invoice: CustomerInvoice): GetIssuedInvoiceByIdResponseDTO {
|
||||
const itemsPresenter = this.presenterRegistry.getPresenter({
|
||||
resource: "customer-invoice-items",
|
||||
projection: "FULL",
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { Presenter } from "@erp/core/api";
|
||||
import { DomainValidationError, toEmptyString } from "@repo/rdx-ddd";
|
||||
|
||||
import type { GetIssueInvoiceByIdResponseDTO } from "../../../../common/dto";
|
||||
import type { GetIssuedInvoiceByIdResponseDTO } from "../../../../common/dto";
|
||||
import type { CustomerInvoice, InvoiceRecipient } from "../../../domain";
|
||||
|
||||
type GetRecipientInvoiceByInvoiceIdResponseDTO = GetIssueInvoiceByIdResponseDTO["recipient"];
|
||||
type GetRecipientInvoiceByInvoiceIdResponseDTO = GetIssuedInvoiceByIdResponseDTO["recipient"];
|
||||
|
||||
export class RecipientInvoiceFullPresenter extends Presenter {
|
||||
toOutput(invoice: CustomerInvoice): GetRecipientInvoiceByInvoiceIdResponseDTO {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core";
|
||||
import { type IPresenterOutputParams, Presenter } from "@erp/core/api";
|
||||
import type { GetIssueInvoiceByIdResponseDTO } from "@erp/customer-invoices/common";
|
||||
import type { GetIssuedInvoiceByIdResponseDTO } from "@erp/customer-invoices/common";
|
||||
import type { ArrayElement } from "@repo/rdx-utils";
|
||||
|
||||
type CustomerInvoiceItemsDTO = GetIssueInvoiceByIdResponseDTO["items"];
|
||||
type CustomerInvoiceItemsDTO = GetIssuedInvoiceByIdResponseDTO["items"];
|
||||
type CustomerInvoiceItemDTO = ArrayElement<CustomerInvoiceItemsDTO>;
|
||||
|
||||
export class CustomerInvoiceItemsReportPersenter extends Presenter<
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { type JsonTaxCatalogProvider, MoneyDTOHelper, SpainTaxCatalogProvider } from "@erp/core";
|
||||
import { type IPresenterOutputParams, Presenter } from "@erp/core/api";
|
||||
import type { GetIssueInvoiceByIdResponseDTO } from "@erp/customer-invoices/common";
|
||||
import type { GetIssuedInvoiceByIdResponseDTO } from "@erp/customer-invoices/common";
|
||||
import type { ArrayElement } from "@repo/rdx-utils";
|
||||
|
||||
type CustomerInvoiceTaxesDTO = GetIssueInvoiceByIdResponseDTO["taxes"];
|
||||
type CustomerInvoiceTaxesDTO = GetIssuedInvoiceByIdResponseDTO["taxes"];
|
||||
type CustomerInvoiceTaxDTO = ArrayElement<CustomerInvoiceTaxesDTO>;
|
||||
|
||||
export class CustomerInvoiceTaxesReportPresenter extends Presenter<
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import { DateHelper, MoneyDTOHelper, PercentageDTOHelper } from "@erp/core";
|
||||
import { Presenter } from "@erp/core/api";
|
||||
|
||||
import type { GetIssueInvoiceByIdResponseDTO } from "../../../../common/dto";
|
||||
import type { GetIssuedInvoiceByIdResponseDTO } from "../../../../common/dto";
|
||||
|
||||
export class CustomerInvoiceReportPresenter extends Presenter<
|
||||
GetIssueInvoiceByIdResponseDTO,
|
||||
GetIssuedInvoiceByIdResponseDTO,
|
||||
unknown
|
||||
> {
|
||||
private _formatPaymentMethodDTO(
|
||||
paymentMethod?: GetIssueInvoiceByIdResponseDTO["payment_method"]
|
||||
paymentMethod?: GetIssuedInvoiceByIdResponseDTO["payment_method"]
|
||||
) {
|
||||
if (!paymentMethod) {
|
||||
return "";
|
||||
@ -17,7 +17,7 @@ export class CustomerInvoiceReportPresenter extends Presenter<
|
||||
return paymentMethod.payment_description ?? "";
|
||||
}
|
||||
|
||||
toOutput(invoiceDTO: GetIssueInvoiceByIdResponseDTO) {
|
||||
toOutput(invoiceDTO: GetIssuedInvoiceByIdResponseDTO) {
|
||||
const itemsPresenter = this.presenterRegistry.getPresenter({
|
||||
resource: "customer-invoice-items",
|
||||
projection: "REPORT",
|
||||
|
||||
@ -3,14 +3,14 @@ import type { Criteria } from "@repo/rdx-criteria/server";
|
||||
import { toEmptyString } from "@repo/rdx-ddd";
|
||||
import type { ArrayElement, Collection } from "@repo/rdx-utils";
|
||||
|
||||
import type { ListIssueInvoicesResponseDTO } from "../../../../common/dto";
|
||||
import type { ListIssuedInvoicesResponseDTO } from "../../../../common/dto";
|
||||
import type { CustomerInvoiceListDTO } from "../../../infrastructure";
|
||||
|
||||
export class ListCustomerInvoicesPresenter extends Presenter {
|
||||
protected _mapInvoice(invoice: CustomerInvoiceListDTO) {
|
||||
const recipientDTO = invoice.recipient.toObjectString();
|
||||
|
||||
const invoiceDTO: ArrayElement<ListIssueInvoicesResponseDTO["items"]> = {
|
||||
const invoiceDTO: ArrayElement<ListIssuedInvoicesResponseDTO["items"]> = {
|
||||
id: invoice.id.toString(),
|
||||
company_id: invoice.companyId.toString(),
|
||||
is_proforma: invoice.isProforma,
|
||||
@ -50,7 +50,7 @@ export class ListCustomerInvoicesPresenter extends Presenter {
|
||||
toOutput(params: {
|
||||
customerInvoices: Collection<CustomerInvoiceListDTO>;
|
||||
criteria: Criteria;
|
||||
}): ListIssueInvoicesResponseDTO {
|
||||
}): ListIssuedInvoicesResponseDTO {
|
||||
const { customerInvoices, criteria } = params;
|
||||
|
||||
const invoices = customerInvoices.map((invoice) => this._mapInvoice(invoice));
|
||||
|
||||
@ -48,7 +48,7 @@ export class CustomerInvoiceApplicationService {
|
||||
* @param transaction - Transacción activa para la operación.
|
||||
* @returns Result<CustomerInvoiceNumber, Error> - El agregado construido o un error si falla la creación.
|
||||
*/
|
||||
async getNextIssueInvoiceNumber(
|
||||
async getNextIssuedInvoiceNumber(
|
||||
companyId: UniqueID,
|
||||
series: Maybe<CustomerInvoiceSerie>,
|
||||
transaction: Transaction
|
||||
@ -80,7 +80,7 @@ export class CustomerInvoiceApplicationService {
|
||||
* @param transaction - Transacción activa para la operación.
|
||||
* @returns Result<CustomerInvoice, Error> - La factura guardada o un error si falla la operación.
|
||||
*/
|
||||
async createIssueInvoiceInCompany(
|
||||
async createIssuedInvoiceInCompany(
|
||||
companyId: UniqueID,
|
||||
invoice: CustomerInvoice,
|
||||
transaction: Transaction
|
||||
@ -90,7 +90,7 @@ export class CustomerInvoiceApplicationService {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
return this.getIssueInvoiceByIdInCompany(companyId, invoice.id, transaction);
|
||||
return this.getIssuedInvoiceByIdInCompany(companyId, invoice.id, transaction);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -157,7 +157,7 @@ export class CustomerInvoiceApplicationService {
|
||||
|
||||
/**
|
||||
*
|
||||
* Comprueba si existe o no en persistencia una proforma con el ID proporcionado
|
||||
* Comprueba si existe o no en persistencia una factura con el ID proporcionado
|
||||
*
|
||||
* @param companyId - Identificador de la empresa a la que pertenece la factura.
|
||||
* @param invoiceId - Identificador UUID de la factura.
|
||||
@ -165,7 +165,7 @@ export class CustomerInvoiceApplicationService {
|
||||
* @returns Result<Boolean, Error> - Existe la factura o no.
|
||||
*/
|
||||
|
||||
async existsIssueInvoiceByIdInCompany(
|
||||
async existsIssuedInvoiceByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
invoiceId: UniqueID,
|
||||
transaction?: Transaction
|
||||
@ -178,7 +178,7 @@ export class CustomerInvoiceApplicationService {
|
||||
/**
|
||||
* Obtiene una colección de proformas que cumplen con los filtros definidos en un objeto Criteria.
|
||||
*
|
||||
* @param companyId - Identificador de la empresa a la que pertenece la factura.
|
||||
* @param companyId - Identificador de la empresa a la que pertenece la proforma.
|
||||
* @param criteria - Objeto con condiciones de filtro, paginación y orden.
|
||||
* @param transaction - Transacción activa para la operación.
|
||||
* @returns Result<Collection<ProformasListDTO>, Error> - Colección de proformas o error.
|
||||
@ -203,7 +203,7 @@ export class CustomerInvoiceApplicationService {
|
||||
* @param transaction - Transacción activa para la operación.
|
||||
* @returns Result<Collection<CustomerInvoiceListDTO>, Error> - Colección de facturas o error.
|
||||
*/
|
||||
async findIssueInvoiceByCriteriaInCompany(
|
||||
async findIssuedInvoiceByCriteriaInCompany(
|
||||
companyId: UniqueID,
|
||||
criteria: Criteria,
|
||||
transaction?: Transaction
|
||||
@ -222,7 +222,7 @@ export class CustomerInvoiceApplicationService {
|
||||
* @param transaction - Transacción activa para la operación.
|
||||
* @returns Result<CustomerInvoice, Error> - Factura encontrada o error.
|
||||
*/
|
||||
async getIssueInvoiceByIdInCompany(
|
||||
async getIssuedInvoiceByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
invoiceId: UniqueID,
|
||||
transaction?: Transaction
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
export * from "./issue-invoices";
|
||||
export * from "./issued-invoices";
|
||||
export * from "./proformas";
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
export * from "./get-issue-invoice.use-case";
|
||||
export * from "./list-issue-invoices.use-case";
|
||||
export * from "./report-issue-invoice.use-case";
|
||||
@ -5,19 +5,19 @@ import { Result } from "@repo/rdx-utils";
|
||||
import type { CustomerInvoiceFullPresenter } from "../../presenters/domain";
|
||||
import type { CustomerInvoiceApplicationService } from "../../services";
|
||||
|
||||
type GetIssueInvoiceUseCaseInput = {
|
||||
type GetIssuedInvoiceUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
invoice_id: string;
|
||||
};
|
||||
|
||||
export class GetIssueInvoiceUseCase {
|
||||
export class GetIssuedInvoiceUseCase {
|
||||
constructor(
|
||||
private readonly service: CustomerInvoiceApplicationService,
|
||||
private readonly transactionManager: ITransactionManager,
|
||||
private readonly presenterRegistry: IPresenterRegistry
|
||||
) {}
|
||||
|
||||
public execute(params: GetIssueInvoiceUseCaseInput) {
|
||||
public execute(params: GetIssuedInvoiceUseCaseInput) {
|
||||
const { invoice_id, companyId } = params;
|
||||
|
||||
const idOrError = UniqueID.create(invoice_id);
|
||||
@ -33,7 +33,7 @@ export class GetIssueInvoiceUseCase {
|
||||
|
||||
return this.transactionManager.complete(async (transaction) => {
|
||||
try {
|
||||
const invoiceOrError = await this.service.getIssueInvoiceByIdInCompany(
|
||||
const invoiceOrError = await this.service.getIssuedInvoiceByIdInCompany(
|
||||
companyId,
|
||||
invoiceId,
|
||||
transaction
|
||||
@ -0,0 +1,3 @@
|
||||
export * from "./get-issued-invoice.use-case";
|
||||
export * from "./list-issued-invoices.use-case";
|
||||
export * from "./report-issued-invoice.use-case";
|
||||
@ -4,16 +4,16 @@ import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import type { ListIssueInvoicesResponseDTO } from "../../../../common/dto";
|
||||
import type { ListIssuedInvoicesResponseDTO } from "../../../../common/dto";
|
||||
import type { ListCustomerInvoicesPresenter } from "../../presenters";
|
||||
import type { CustomerInvoiceApplicationService } from "../../services";
|
||||
|
||||
type ListIssueInvoicesUseCaseInput = {
|
||||
type ListIssuedInvoicesUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
criteria: Criteria;
|
||||
};
|
||||
|
||||
export class ListIssueInvoicesUseCase {
|
||||
export class ListIssuedInvoicesUseCase {
|
||||
constructor(
|
||||
private readonly service: CustomerInvoiceApplicationService,
|
||||
private readonly transactionManager: ITransactionManager,
|
||||
@ -21,8 +21,8 @@ export class ListIssueInvoicesUseCase {
|
||||
) {}
|
||||
|
||||
public execute(
|
||||
params: ListIssueInvoicesUseCaseInput
|
||||
): Promise<Result<ListIssueInvoicesResponseDTO, Error>> {
|
||||
params: ListIssuedInvoicesUseCaseInput
|
||||
): Promise<Result<ListIssuedInvoicesResponseDTO, Error>> {
|
||||
const { criteria, companyId } = params;
|
||||
const presenter = this.presenterRegistry.getPresenter({
|
||||
resource: "customer-invoice",
|
||||
@ -31,7 +31,7 @@ export class ListIssueInvoicesUseCase {
|
||||
|
||||
return this.transactionManager.complete(async (transaction: Transaction) => {
|
||||
try {
|
||||
const result = await this.service.findIssueInvoiceByCriteriaInCompany(
|
||||
const result = await this.service.findIssuedInvoiceByCriteriaInCompany(
|
||||
companyId,
|
||||
criteria,
|
||||
transaction
|
||||
@ -5,19 +5,19 @@ import { Result } from "@repo/rdx-utils";
|
||||
import type { CustomerInvoiceApplicationService } from "../../services";
|
||||
import type { CustomerInvoiceReportPDFPresenter } from "../proformas";
|
||||
|
||||
type ReportIssueInvoiceUseCaseInput = {
|
||||
type ReportIssuedInvoiceUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
invoice_id: string;
|
||||
};
|
||||
|
||||
export class ReportIssueInvoiceUseCase {
|
||||
export class ReportIssuedInvoiceUseCase {
|
||||
constructor(
|
||||
private readonly service: CustomerInvoiceApplicationService,
|
||||
private readonly transactionManager: ITransactionManager,
|
||||
private readonly presenterRegistry: IPresenterRegistry
|
||||
) {}
|
||||
|
||||
public async execute(params: ReportIssueInvoiceUseCaseInput) {
|
||||
public async execute(params: ReportIssuedInvoiceUseCaseInput) {
|
||||
const { invoice_id, companyId } = params;
|
||||
|
||||
const idOrError = UniqueID.create(invoice_id);
|
||||
@ -35,7 +35,7 @@ export class ReportIssueInvoiceUseCase {
|
||||
|
||||
return this.transactionManager.complete(async (transaction) => {
|
||||
try {
|
||||
const invoiceOrError = await this.service.getIssueInvoiceByIdInCompany(
|
||||
const invoiceOrError = await this.service.getIssuedInvoiceByIdInCompany(
|
||||
companyId,
|
||||
invoiceId,
|
||||
transaction
|
||||
@ -9,7 +9,7 @@ import {
|
||||
import type { CustomerInvoiceFullPresenter } from "../../presenters";
|
||||
import type { CustomerInvoiceApplicationService } from "../../services";
|
||||
|
||||
type IssueCustomerInvoiceUseCaseInput = {
|
||||
type IssueProformaUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
proforma_id: string;
|
||||
};
|
||||
@ -23,7 +23,7 @@ type IssueCustomerInvoiceUseCaseInput = {
|
||||
* - Marca la proforma como "issued"
|
||||
* - Persiste ambas dentro de la misma transacción
|
||||
*/
|
||||
export class IssueProformaInvoiceUseCase {
|
||||
export class IssueProformaUseCase {
|
||||
private readonly issueDomainService: IssueCustomerInvoiceDomainService;
|
||||
private readonly proformaDomainService: ProformaCustomerInvoiceDomainService;
|
||||
|
||||
@ -36,7 +36,7 @@ export class IssueProformaInvoiceUseCase {
|
||||
this.proformaDomainService = new ProformaCustomerInvoiceDomainService();
|
||||
}
|
||||
|
||||
public execute(params: IssueCustomerInvoiceUseCaseInput) {
|
||||
public execute(params: IssueProformaUseCaseInput) {
|
||||
const { proforma_id, companyId } = params;
|
||||
const idOrError = UniqueID.create(proforma_id);
|
||||
if (idOrError.isFailure) return Result.fail(idOrError.error);
|
||||
@ -59,7 +59,7 @@ export class IssueProformaInvoiceUseCase {
|
||||
const proforma = proformaResult.data;
|
||||
|
||||
/** 2. Generar nueva factura */
|
||||
const nextNumberResult = await this.service.getNextIssueInvoiceNumber(
|
||||
const nextNumberResult = await this.service.getNextIssuedInvoiceNumber(
|
||||
companyId,
|
||||
proforma.series,
|
||||
transaction
|
||||
@ -74,7 +74,7 @@ export class IssueProformaInvoiceUseCase {
|
||||
if (issuedInvoiceOrError.isFailure) return Result.fail(issuedInvoiceOrError.error);
|
||||
|
||||
/** 5. Guardar la nueva factura */
|
||||
const saveInvoiceResult = await this.service.createIssueInvoiceInCompany(
|
||||
const saveInvoiceResult = await this.service.createIssuedInvoiceInCompany(
|
||||
companyId,
|
||||
issuedInvoiceOrError.data,
|
||||
transaction
|
||||
|
||||
@ -4,7 +4,7 @@ import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import type { ListIssueInvoicesResponseDTO } from "../../../../common/dto";
|
||||
import type { ListIssuedInvoicesResponseDTO } from "../../../../common/dto";
|
||||
import type { ListCustomerInvoicesPresenter } from "../../presenters";
|
||||
import type { CustomerInvoiceApplicationService } from "../../services";
|
||||
|
||||
@ -22,7 +22,7 @@ export class ListProformasUseCase {
|
||||
|
||||
public execute(
|
||||
params: ListProformasUseCaseInput
|
||||
): Promise<Result<ListIssueInvoicesResponseDTO, Error>> {
|
||||
): Promise<Result<ListIssuedInvoicesResponseDTO, Error>> {
|
||||
const { criteria, companyId } = params;
|
||||
const presenter = this.presenterRegistry.getPresenter({
|
||||
resource: "customer-invoice",
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { Tax } from "@erp/core/api";
|
||||
import { CurrencyCode, LanguageCode } from "@repo/rdx-ddd";
|
||||
import type { Tax } from "@erp/core/api";
|
||||
import type { CurrencyCode, LanguageCode } from "@repo/rdx-ddd";
|
||||
import { Collection } from "@repo/rdx-utils";
|
||||
|
||||
import { ItemAmount } from "../../value-objects";
|
||||
import { ItemTaxes } from "../item-taxes";
|
||||
import { CustomerInvoiceItem } from "./customer-invoice-item";
|
||||
|
||||
import type { CustomerInvoiceItem } from "./customer-invoice-item";
|
||||
|
||||
export interface CustomerInvoiceItemsProps {
|
||||
items?: CustomerInvoiceItem[];
|
||||
@ -39,8 +41,9 @@ export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
|
||||
// Antes de añadir un nuevo item, debo comprobar que el item a añadir
|
||||
// tiene el mismo "currencyCode" y "languageCode" que la colección de items.
|
||||
if (
|
||||
!this._languageCode.equals(item.languageCode) ||
|
||||
!this._currencyCode.equals(item.currencyCode)
|
||||
!(
|
||||
this._languageCode.equals(item.languageCode) && this._currencyCode.equals(item.currencyCode)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import {
|
||||
City,
|
||||
Country,
|
||||
Name,
|
||||
PostalCode,
|
||||
Province,
|
||||
Street,
|
||||
TINNumber,
|
||||
type City,
|
||||
type Country,
|
||||
type Name,
|
||||
type PostalCode,
|
||||
type Province,
|
||||
type Street,
|
||||
type TINNumber,
|
||||
ValueObject,
|
||||
toEmptyString,
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
import { type Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
export interface InvoiceRecipientProps {
|
||||
name: Name;
|
||||
|
||||
@ -3,7 +3,7 @@ import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import { buildCustomerInvoiceDependencies, models, proformasRouter } from "./infrastructure";
|
||||
import { issueInvoicesRouter } from "./infrastructure/express/issue-invoices.routes";
|
||||
import { issuedInvoicesRouter } from "./infrastructure/express/issued-invoices.routes";
|
||||
|
||||
export const customerInvoicesAPIModule: IModuleServer = {
|
||||
name: "customer-invoices",
|
||||
@ -14,7 +14,7 @@ export const customerInvoicesAPIModule: IModuleServer = {
|
||||
// const contacts = getService<ContactsService>("contacts");
|
||||
const { logger } = params;
|
||||
proformasRouter(params);
|
||||
issueInvoicesRouter(params);
|
||||
issuedInvoicesRouter(params);
|
||||
logger.info("🚀 CustomerInvoices module initialized", { label: this.name });
|
||||
},
|
||||
|
||||
|
||||
@ -20,14 +20,14 @@ import {
|
||||
CustomerInvoiceReportPresenter,
|
||||
CustomerInvoiceTaxesReportPresenter,
|
||||
DeleteProformaUseCase,
|
||||
GetIssueInvoiceUseCase,
|
||||
GetIssuedInvoiceUseCase,
|
||||
GetProformaUseCase,
|
||||
IssueProformaInvoiceUseCase,
|
||||
IssueProformaUseCase,
|
||||
ListCustomerInvoicesPresenter,
|
||||
ListIssueInvoicesUseCase,
|
||||
ListIssuedInvoicesUseCase,
|
||||
ListProformasUseCase,
|
||||
RecipientInvoiceFullPresenter,
|
||||
ReportIssueInvoiceUseCase,
|
||||
ReportIssuedInvoiceUseCase,
|
||||
ReportProformaUseCase,
|
||||
UpdateProformaUseCase,
|
||||
} from "../application";
|
||||
@ -52,12 +52,12 @@ export type CustomerInvoiceDeps = {
|
||||
update_proforma: () => UpdateProformaUseCase;
|
||||
delete_proforma: () => DeleteProformaUseCase;
|
||||
report_proforma: () => ReportProformaUseCase;
|
||||
issue_proforma: () => IssueProformaInvoiceUseCase;
|
||||
issue_proforma: () => IssueProformaUseCase;
|
||||
changeStatus_proforma: () => ChangeStatusProformaUseCase;
|
||||
|
||||
list_issue_invoices: () => ListIssueInvoicesUseCase;
|
||||
get_issue_invoice: () => GetIssueInvoiceUseCase;
|
||||
report_issue_invoice: () => ReportIssueInvoiceUseCase;
|
||||
list_issued_invoices: () => ListIssuedInvoicesUseCase;
|
||||
get_issued_invoice: () => GetIssuedInvoiceUseCase;
|
||||
report_issued_invoice: () => ReportIssuedInvoiceUseCase;
|
||||
};
|
||||
};
|
||||
|
||||
@ -144,16 +144,16 @@ export function buildCustomerInvoiceDependencies(params: ModuleParams): Customer
|
||||
report_proforma: () =>
|
||||
new ReportProformaUseCase(appService, transactionManager, presenterRegistry),
|
||||
issue_proforma: () =>
|
||||
new IssueProformaInvoiceUseCase(appService, transactionManager, presenterRegistry),
|
||||
new IssueProformaUseCase(appService, transactionManager, presenterRegistry),
|
||||
changeStatus_proforma: () => new ChangeStatusProformaUseCase(appService, transactionManager),
|
||||
|
||||
// Issue Invoices
|
||||
list_issue_invoices: () =>
|
||||
new ListIssueInvoicesUseCase(appService, transactionManager, presenterRegistry),
|
||||
get_issue_invoice: () =>
|
||||
new GetIssueInvoiceUseCase(appService, transactionManager, presenterRegistry),
|
||||
report_issue_invoice: () =>
|
||||
new ReportIssueInvoiceUseCase(appService, transactionManager, presenterRegistry),
|
||||
list_issued_invoices: () =>
|
||||
new ListIssuedInvoicesUseCase(appService, transactionManager, presenterRegistry),
|
||||
get_issued_invoice: () =>
|
||||
new GetIssuedInvoiceUseCase(appService, transactionManager, presenterRegistry),
|
||||
report_issued_invoice: () =>
|
||||
new ReportIssuedInvoiceUseCase(appService, transactionManager, presenterRegistry),
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
export * from "./issue-invoices";
|
||||
export * from "./issued-invoices";
|
||||
export * from "./proformas";
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
export * from "./get-issue-invoice.controller";
|
||||
export * from "./list-issue-invoices.controller";
|
||||
export * from "./report-issue-invoice.controller";
|
||||
@ -1,10 +1,10 @@
|
||||
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||
|
||||
import type { GetIssueInvoiceUseCase } from "../../../../application";
|
||||
import type { GetIssuedInvoiceUseCase } from "../../../../application";
|
||||
import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
|
||||
|
||||
export class GetIssueInvoiceController extends ExpressController {
|
||||
public constructor(private readonly useCase: GetIssueInvoiceUseCase) {
|
||||
public constructor(private readonly useCase: GetIssuedInvoiceUseCase) {
|
||||
super();
|
||||
this.errorMapper = customerInvoicesApiErrorMapper;
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
export * from "./get-issued-invoice.controller";
|
||||
export * from "./list-issued-invoices.controller";
|
||||
export * from "./report-issued-invoice.controller";
|
||||
@ -1,11 +1,11 @@
|
||||
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||
import { Criteria } from "@repo/rdx-criteria/server";
|
||||
|
||||
import type { ListIssueInvoicesUseCase } from "../../../../application";
|
||||
import type { ListIssuedInvoicesUseCase } from "../../../../application";
|
||||
import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
|
||||
|
||||
export class ListIssueInvoicesController extends ExpressController {
|
||||
public constructor(private readonly useCase: ListIssueInvoicesUseCase) {
|
||||
export class ListIssuedInvoicesController extends ExpressController {
|
||||
public constructor(private readonly useCase: ListIssuedInvoicesUseCase) {
|
||||
super();
|
||||
this.errorMapper = customerInvoicesApiErrorMapper;
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||
|
||||
import type { ReportIssueInvoiceUseCase } from "../../../../application";
|
||||
import type { ReportIssuedInvoiceUseCase } from "../../../../application";
|
||||
import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
|
||||
|
||||
export class ReportIssueInvoiceController extends ExpressController {
|
||||
public constructor(private readonly useCase: ReportIssueInvoiceUseCase) {
|
||||
export class ReportIssuedInvoiceController extends ExpressController {
|
||||
public constructor(private readonly useCase: ReportIssuedInvoiceUseCase) {
|
||||
super();
|
||||
this.errorMapper = customerInvoicesApiErrorMapper;
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||
|
||||
import type { IssueProformaInvoiceUseCase } from "../../../../application";
|
||||
import type { IssueProformaUseCase } from "../../../../application";
|
||||
import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
|
||||
|
||||
export class IssueProformaController extends ExpressController {
|
||||
public constructor(private readonly useCase: IssueProformaInvoiceUseCase) {
|
||||
public constructor(private readonly useCase: IssueProformaUseCase) {
|
||||
super();
|
||||
this.errorMapper = customerInvoicesApiErrorMapper;
|
||||
|
||||
|
||||
@ -11,9 +11,11 @@ import {
|
||||
import {
|
||||
type CustomerInvoiceIdAlreadyExistsError,
|
||||
type EntityIsNotProformaError,
|
||||
type InvalidProformaTransitionError,
|
||||
type ProformaCannotBeConvertedToInvoiceError,
|
||||
isCustomerInvoiceIdAlreadyExistsError,
|
||||
isEntityIsNotProformaError,
|
||||
isInvalidProformaTransitionError,
|
||||
isProformaCannotBeConvertedToInvoiceError,
|
||||
isProformaCannotBeDeletedError,
|
||||
} from "../../domain";
|
||||
@ -38,6 +40,15 @@ const entityIsNotProformaError: ErrorToApiRule = {
|
||||
),
|
||||
};
|
||||
|
||||
const proformaTransitionRule: ErrorToApiRule = {
|
||||
priority: 120,
|
||||
matches: (e) => isInvalidProformaTransitionError(e),
|
||||
build: (e) =>
|
||||
new ValidationApiError(
|
||||
(e as InvalidProformaTransitionError).message || "Invalid transition for proforma."
|
||||
),
|
||||
};
|
||||
|
||||
const proformaConversionRule: ErrorToApiRule = {
|
||||
priority: 120,
|
||||
matches: (e) => isProformaCannotBeConvertedToInvoiceError(e),
|
||||
@ -62,4 +73,5 @@ export const customerInvoicesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.def
|
||||
.register(invoiceDuplicateRule)
|
||||
.register(entityIsNotProformaError)
|
||||
.register(proformaConversionRule)
|
||||
.register(proformaCannotBeDeletedRule);
|
||||
.register(proformaCannotBeDeletedRule)
|
||||
.register(proformaTransitionRule);
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
export * from "./issue-invoices.routes";
|
||||
export * from "./issued-invoices.routes";
|
||||
export * from "./proformas.routes";
|
||||
|
||||
@ -6,18 +6,18 @@ import type { Sequelize } from "sequelize";
|
||||
|
||||
import {
|
||||
GetIssueInvoiceByIdRequestSchema,
|
||||
ListIssueInvoicesRequestSchema,
|
||||
ListIssuedInvoicesRequestSchema,
|
||||
ReportIssueInvoiceByIdRequestSchema,
|
||||
} from "../../../common/dto";
|
||||
import { buildCustomerInvoiceDependencies } from "../dependencies";
|
||||
|
||||
import {
|
||||
GetIssueInvoiceController,
|
||||
ListIssueInvoicesController,
|
||||
ReportIssueInvoiceController,
|
||||
ListIssuedInvoicesController,
|
||||
ReportIssuedInvoiceController,
|
||||
} from "./controllers";
|
||||
|
||||
export const issueInvoicesRouter = (params: ModuleParams) => {
|
||||
export const issuedInvoicesRouter = (params: ModuleParams) => {
|
||||
const { app, baseRoutePath, logger } = params as {
|
||||
app: Application;
|
||||
database: Sequelize;
|
||||
@ -49,10 +49,10 @@ export const issueInvoicesRouter = (params: ModuleParams) => {
|
||||
router.get(
|
||||
"/",
|
||||
//checkTabContext,
|
||||
validateRequest(ListIssueInvoicesRequestSchema, "params"),
|
||||
validateRequest(ListIssuedInvoicesRequestSchema, "params"),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.useCases.list_issue_invoices();
|
||||
const controller = new ListIssueInvoicesController(useCase /*, deps.presenters.list */);
|
||||
const useCase = deps.useCases.list_issued_invoices();
|
||||
const controller = new ListIssuedInvoicesController(useCase /*, deps.presenters.list */);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
);
|
||||
@ -62,7 +62,7 @@ export const issueInvoicesRouter = (params: ModuleParams) => {
|
||||
//checkTabContext,
|
||||
validateRequest(GetIssueInvoiceByIdRequestSchema, "params"),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.useCases.get_issue_invoice();
|
||||
const useCase = deps.useCases.get_issued_invoice();
|
||||
const controller = new GetIssueInvoiceController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
@ -73,11 +73,11 @@ export const issueInvoicesRouter = (params: ModuleParams) => {
|
||||
//checkTabContext,
|
||||
validateRequest(ReportIssueInvoiceByIdRequestSchema, "params"),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.useCases.report_issue_invoice();
|
||||
const controller = new ReportIssueInvoiceController(useCase);
|
||||
const useCase = deps.useCases.report_issued_invoice();
|
||||
const controller = new ReportIssuedInvoiceController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
);
|
||||
|
||||
app.use(`${baseRoutePath}/issue-invoices`, router);
|
||||
app.use(`${baseRoutePath}/issued-invoices`, router);
|
||||
};
|
||||
@ -1,5 +1,9 @@
|
||||
import { type RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth/api";
|
||||
import { type ModuleParams, validateRequest } from "@erp/core/api";
|
||||
import type { ILogger } from "@repo/rdx-logger";
|
||||
import { type Application, type NextFunction, type Request, type Response, Router } from "express";
|
||||
import type { Sequelize } from "sequelize";
|
||||
|
||||
import {
|
||||
ChangeStatusProformaByIdParamsRequestSchema,
|
||||
ChangeStatusProformaByIdRequestSchema,
|
||||
@ -11,11 +15,7 @@ import {
|
||||
ReportProformaByIdRequestSchema,
|
||||
UpdateProformaByIdParamsRequestSchema,
|
||||
UpdateProformaByIdRequestSchema,
|
||||
} from "@erp/customer-invoices/common";
|
||||
import type { ILogger } from "@repo/rdx-logger";
|
||||
import { type Application, type NextFunction, type Request, type Response, Router } from "express";
|
||||
import type { Sequelize } from "sequelize";
|
||||
|
||||
} from "../../../common";
|
||||
import { buildCustomerInvoiceDependencies } from "../dependencies";
|
||||
|
||||
import {
|
||||
@ -23,7 +23,7 @@ import {
|
||||
CreateProformaController,
|
||||
DeleteProformaController,
|
||||
GetProformaController,
|
||||
IssueProformaController,
|
||||
IssueProformaController as IssuedProformaController,
|
||||
ListProformasController,
|
||||
ReportProformaController,
|
||||
UpdateProformaController,
|
||||
@ -150,7 +150,7 @@ export const proformasRouter = (params: ModuleParams) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.useCases.issue_proforma();
|
||||
const controller = new IssueProformaController(useCase);
|
||||
const controller = new IssuedProformaController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
);
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
export * from "./issue-invoices";
|
||||
export * from "./issued-invoices";
|
||||
export * from "./proformas";
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
export * from "./get-issue-invoice-by-id.request.dto";
|
||||
export * from "./list-issue-invoices.request.dto";
|
||||
export * from "./report-issue-invoice-by-id.request.dto";
|
||||
@ -1,5 +0,0 @@
|
||||
import { CriteriaSchema } from "@erp/core";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const ListIssueInvoicesRequestSchema = CriteriaSchema;
|
||||
export type ListIssueInvoicesRequestDTO = z.infer<typeof ListIssueInvoicesRequestSchema>;
|
||||
@ -0,0 +1,3 @@
|
||||
export * from "./get-issued-invoice-by-id.request.dto";
|
||||
export * from "./list-issued-invoices.request.dto";
|
||||
export * from "./report-issued-invoice-by-id.request.dto";
|
||||
@ -0,0 +1,5 @@
|
||||
import { CriteriaSchema } from "@erp/core";
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
export const ListIssuedInvoicesRequestSchema = CriteriaSchema;
|
||||
export type ListIssuedInvoicesRequestDTO = z.infer<typeof ListIssuedInvoicesRequestSchema>;
|
||||
@ -1,2 +1,2 @@
|
||||
export * from "./issue-invoices";
|
||||
export * from "./issued-invoices";
|
||||
export * from "./proformas";
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
export * from "./get-issue-invoice-by-id.response.dto";
|
||||
export * from "./list-issue-invoices.response.dto";
|
||||
@ -1,7 +1,7 @@
|
||||
import { MetadataSchema, MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const GetIssueInvoiceByIdResponseSchema = z.object({
|
||||
export const GetIssuedInvoiceByIdResponseSchema = z.object({
|
||||
id: z.uuid(),
|
||||
company_id: z.uuid(),
|
||||
|
||||
@ -79,4 +79,4 @@ export const GetIssueInvoiceByIdResponseSchema = z.object({
|
||||
metadata: MetadataSchema.optional(),
|
||||
});
|
||||
|
||||
export type GetIssueInvoiceByIdResponseDTO = z.infer<typeof GetIssueInvoiceByIdResponseSchema>;
|
||||
export type GetIssuedInvoiceByIdResponseDTO = z.infer<typeof GetIssuedInvoiceByIdResponseSchema>;
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./get-issued-invoice-by-id.response.dto";
|
||||
export * from "./list-issued-invoices.response.dto";
|
||||
@ -6,7 +6,7 @@ import {
|
||||
} from "@erp/core";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const ListIssueInvoicesResponseSchema = createPaginatedListSchema(
|
||||
export const ListIssuedInvoicesResponseSchema = createPaginatedListSchema(
|
||||
z.object({
|
||||
id: z.uuid(),
|
||||
company_id: z.uuid(),
|
||||
@ -51,4 +51,4 @@ export const ListIssueInvoicesResponseSchema = createPaginatedListSchema(
|
||||
})
|
||||
);
|
||||
|
||||
export type ListIssueInvoicesResponseDTO = z.infer<typeof ListIssueInvoicesResponseSchema>;
|
||||
export type ListIssuedInvoicesResponseDTO = z.infer<typeof ListIssuedInvoicesResponseSchema>;
|
||||
@ -1,16 +1,20 @@
|
||||
{
|
||||
"common": {
|
||||
"more_actions": "More actions",
|
||||
"append_empty_row": "Append row",
|
||||
"append_empty_row_tooltip": "Append a empty row",
|
||||
"edit_row": "Edit",
|
||||
"duplicate_row": "Duplicate",
|
||||
"duplicate_selected_rows": "Duplicate",
|
||||
"duplicate_selected_rows_tooltip": "Duplicate selected row(s)",
|
||||
"remove_selected_rows": "Remove",
|
||||
"remove_selected_rows_tooltip": "Remove selected row(s)",
|
||||
"download_pdf": "Download PDF",
|
||||
"send_email": "Send email",
|
||||
|
||||
"insert_row_above": "Insert row above",
|
||||
"insert_row_below": "Insert row below",
|
||||
"remove_row": "Remove",
|
||||
"delete_row": "Delete",
|
||||
"actions": "Actions",
|
||||
|
||||
"rows_selected": "{{count}} fila(s) seleccionadas.",
|
||||
@ -22,12 +26,15 @@
|
||||
},
|
||||
|
||||
"catalog": {
|
||||
"status": {
|
||||
"draft": "Draft",
|
||||
"issued": "Issued",
|
||||
"sent": "Sent",
|
||||
"received": "Received",
|
||||
"rejected": "Rejected"
|
||||
"proformas": {
|
||||
"status": {
|
||||
"all": "All",
|
||||
"draft": "Draft",
|
||||
"sent": "Sent",
|
||||
"approved": "Approved",
|
||||
"rejected": "Rejected",
|
||||
"issued": "Issued"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
@ -40,9 +47,11 @@
|
||||
"grid_columns": {
|
||||
"invoice_number": "Inv. number",
|
||||
"series": "Serie",
|
||||
"reference": "Reference",
|
||||
"status": "Status",
|
||||
"invoice_date": "Proforma date",
|
||||
"operation_date": "Operation date",
|
||||
"recipient": "Customer",
|
||||
"recipient_tin": "TIN",
|
||||
"recipient_name": "Customer name",
|
||||
"recipient_street": "Street",
|
||||
@ -50,7 +59,10 @@
|
||||
"recipient_province": "Province",
|
||||
"recipient_postal_code": "Postal code",
|
||||
"recipient_country": "Country",
|
||||
"total_amount": "Total price"
|
||||
"subtotal_amount": "Subtotal",
|
||||
"discount_amount": "Discount",
|
||||
"taxes_amount": "Taxes",
|
||||
"total_amount": "Total"
|
||||
}
|
||||
},
|
||||
"create": {
|
||||
|
||||
@ -1,16 +1,20 @@
|
||||
{
|
||||
"common": {
|
||||
"more_actions": "Más acciones",
|
||||
"append_empty_row": "Añadir fila",
|
||||
"append_empty_row_tooltip": "Añadir una fila vacía",
|
||||
"edit_row": "Modificar",
|
||||
"duplicate_row": "Duplicar",
|
||||
"duplicate_selected_rows": "Duplicar",
|
||||
"duplicate_selected_rows_tooltip": "Duplicar fila(s) seleccionada(s)",
|
||||
"remove_selected_rows": "Eliminar",
|
||||
"remove_selected_rows_tooltip": "Eliminar fila(s) seleccionada(s)",
|
||||
"download_pdf": "Descargar en PDF",
|
||||
"send_email": "Enviar por email",
|
||||
|
||||
"insert_row_above": "Insertar fila arriba",
|
||||
"insert_row_below": "Insertar fila abajo",
|
||||
"remove_row": "Eliminar",
|
||||
"delete_row": "Eliminar",
|
||||
"actions": "Acciones",
|
||||
|
||||
"rows_selected": "{{count}} fila(s) seleccionadas.",
|
||||
@ -21,12 +25,15 @@
|
||||
"clear": "Limpiar"
|
||||
},
|
||||
"catalog": {
|
||||
"status": {
|
||||
"draft": "Borrador",
|
||||
"issued": "Emitida",
|
||||
"sent": "Enviada",
|
||||
"received": "Recibida",
|
||||
"rejected": "Rechazada"
|
||||
"proformas": {
|
||||
"status": {
|
||||
"all": "Todas",
|
||||
"draft": "Borradores",
|
||||
"sent": "Enviadas",
|
||||
"approved": "Aprovadas",
|
||||
"rejected": "Rechazadas",
|
||||
"issued": "Emitidas"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
@ -39,9 +46,11 @@
|
||||
"grid_columns": {
|
||||
"invoice_number": "Nº proforma",
|
||||
"series": "Serie",
|
||||
"reference": "Reference",
|
||||
"status": "Estado",
|
||||
"invoice_date": "Fecha de proforma",
|
||||
"operation_date": "Fecha de operación",
|
||||
"recipient": "Cliente",
|
||||
"recipient_tin": "NIF/CIF",
|
||||
"recipient_name": "Cliente",
|
||||
"recipient_street": "Dirección",
|
||||
@ -49,6 +58,9 @@
|
||||
"recipient_province": "Provincia",
|
||||
"recipient_postal_code": "Código postal",
|
||||
"recipient_country": "País",
|
||||
"subtotal_amount": "Subtotal",
|
||||
"discount_amount": "Descuentos",
|
||||
"taxes_amount": "Impuestos",
|
||||
"total_amount": "Importe total"
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core";
|
||||
|
||||
import type {
|
||||
GetIssueInvoiceByIdResponseDTO,
|
||||
GetIssuedInvoiceByIdResponseDTO,
|
||||
UpdateCustomerInvoiceByIdRequestDTO,
|
||||
} from "../../common";
|
||||
import type { InvoiceContextValue } from "../context";
|
||||
@ -11,7 +11,7 @@ import type { InvoiceFormData } from "../schemas/invoice.form.schema";
|
||||
* Convierte el DTO completo de API a datos numéricos para el formulario.
|
||||
*/
|
||||
export const invoiceDtoToFormAdapter = {
|
||||
fromDto(dto: GetIssueInvoiceByIdResponseDTO, context: InvoiceContextValue): InvoiceFormData {
|
||||
fromDto(dto: GetIssuedInvoiceByIdResponseDTO, context: InvoiceContextValue): InvoiceFormData {
|
||||
const { taxCatalog } = context;
|
||||
return {
|
||||
invoice_number: dto.invoice_number,
|
||||
|
||||
@ -1,2 +1 @@
|
||||
@source "./components";
|
||||
@source "./pages";
|
||||
@source "**/*.{ts,tsx}";
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
export * from "./use-issue-invoice-query";
|
||||
export * from "./use-issue-invoices-query";
|
||||
@ -1,23 +0,0 @@
|
||||
import {
|
||||
GetIssueInvoiceByIdResponseSchema,
|
||||
ListIssueInvoicesResponseSchema,
|
||||
} from "@erp/customer-invoices/common";
|
||||
import type { ArrayElement } from "@repo/rdx-utils";
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
// IssueInvoices
|
||||
export const IssueInvoiceSchema = GetIssueInvoiceByIdResponseSchema.omit({
|
||||
metadata: true,
|
||||
});
|
||||
|
||||
export type IssueInvoice = z.infer<typeof IssueInvoiceSchema>;
|
||||
export type IssueInvoiceRecipient = IssueInvoice["recipient"];
|
||||
export type IssueInvoiceItem = ArrayElement<IssueInvoice["items"]>;
|
||||
|
||||
// Resultado de consulta con criteria (paginado, etc.)
|
||||
export const IssueInvoiceSummaryPageSchema = ListIssueInvoicesResponseSchema.omit({
|
||||
metadata: true,
|
||||
});
|
||||
|
||||
export type IssueInvoiceSummaryPage = z.infer<typeof IssueInvoiceSummaryPageSchema>;
|
||||
export type IssueInvoiceSummary = Omit<ArrayElement<IssueInvoiceSummaryPage["items"]>, "metadata">;
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./use-issued-invoice-query";
|
||||
export * from "./use-issued-invoices-query";
|
||||
@ -1,26 +1,26 @@
|
||||
import { useDataSource } from "@erp/core/hooks";
|
||||
import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
|
||||
|
||||
import type { IssueInvoice } from "../issue-invoice.api.schema";
|
||||
import type { IssueInvoice } from "../issued-invoice.api.schema";
|
||||
|
||||
export const ISSUE_INVOICE_QUERY_KEY = (id: string): QueryKey => ["issue-invoice", id] as const;
|
||||
export const ISSUED_INVOICE_QUERY_KEY = (id: string): QueryKey => ["issued-invoice", id] as const;
|
||||
|
||||
type InvoiceQueryOptions = {
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export const useIssueInvoiceQuery = (issueInvoiceId?: string, options?: InvoiceQueryOptions) => {
|
||||
export const useIssuedInvoiceQuery = (issuedInvoiceId?: string, options?: InvoiceQueryOptions) => {
|
||||
const dataSource = useDataSource();
|
||||
const enabled = (options?.enabled ?? true) && !!issueInvoiceId;
|
||||
const enabled = (options?.enabled ?? true) && !!issuedInvoiceId;
|
||||
|
||||
return useQuery<IssueInvoice, DefaultError>({
|
||||
queryKey: ISSUE_INVOICE_QUERY_KEY(issueInvoiceId ?? "unknown"),
|
||||
queryKey: ISSUED_INVOICE_QUERY_KEY(issuedInvoiceId ?? "unknown"),
|
||||
queryFn: async (context) => {
|
||||
const { signal } = context;
|
||||
if (!issueInvoiceId) {
|
||||
if (!issueInvoiceId) throw new Error("issueInvoiceId is required");
|
||||
if (!issuedInvoiceId) {
|
||||
if (!issuedInvoiceId) throw new Error("issueInvoiceId is required");
|
||||
}
|
||||
return await dataSource.getOne<IssueInvoice>("issue-invoices", issueInvoiceId, {
|
||||
return await dataSource.getOne<IssueInvoice>("issued-invoices", issuedInvoiceId, {
|
||||
signal,
|
||||
});
|
||||
},
|
||||
@ -2,10 +2,10 @@ import type { CriteriaDTO } from "@erp/core";
|
||||
import { useDataSource } from "@erp/core/hooks";
|
||||
import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
|
||||
|
||||
import type { IssueInvoiceSummaryPage } from "../issue-invoice.api.schema";
|
||||
import type { IssuedInvoicesummaryPage } from "../issued-invoice.api.schema";
|
||||
|
||||
export const ISSUE_INVOICES_QUERY_KEY = (criteria: CriteriaDTO): QueryKey => [
|
||||
"issue-invoice",
|
||||
export const ISSUED_INVOICES_QUERY_KEY = (criteria: CriteriaDTO): QueryKey => [
|
||||
"issued-invoice",
|
||||
{
|
||||
pageNumber: criteria.pageNumber ?? 0,
|
||||
pageSize: criteria.pageSize ?? 10,
|
||||
@ -16,21 +16,21 @@ export const ISSUE_INVOICES_QUERY_KEY = (criteria: CriteriaDTO): QueryKey => [
|
||||
},
|
||||
];
|
||||
|
||||
type IssueInvoicesQueryOptions = {
|
||||
type IssuedInvoicesQueryOptions = {
|
||||
enabled?: boolean;
|
||||
criteria?: CriteriaDTO;
|
||||
};
|
||||
|
||||
// Obtener todas las facturas
|
||||
export const useIssueInvoicesQuery = (options?: IssueInvoicesQueryOptions) => {
|
||||
export const useIssuedInvoicesQuery = (options?: IssuedInvoicesQueryOptions) => {
|
||||
const dataSource = useDataSource();
|
||||
const enabled = options?.enabled ?? true;
|
||||
const criteria = options?.criteria ?? {};
|
||||
|
||||
return useQuery<IssueInvoiceSummaryPage, DefaultError>({
|
||||
queryKey: ISSUE_INVOICES_QUERY_KEY(criteria),
|
||||
return useQuery<IssuedInvoicesummaryPage, DefaultError>({
|
||||
queryKey: ISSUED_INVOICES_QUERY_KEY(criteria),
|
||||
queryFn: async ({ signal }) => {
|
||||
return await dataSource.getList<IssueInvoiceSummaryPage>("issue-invoices", {
|
||||
return await dataSource.getList<IssuedInvoicesummaryPage>("issued-invoices", {
|
||||
signal,
|
||||
...criteria,
|
||||
});
|
||||
@ -1,16 +1,16 @@
|
||||
import { MoneyDTOHelper, PercentageDTOHelper, formatCurrency } from "@erp/core";
|
||||
|
||||
import type { IssueInvoiceSummaryPage } from "./issue-invoice.api.schema";
|
||||
import type { IssuedInvoicesummaryPage } from "./issued-invoice.api.schema";
|
||||
import type {
|
||||
IssueInvoiceSummaryData,
|
||||
IssueInvoiceSummaryPageData,
|
||||
} from "./issue-invoice-resume.form.schema";
|
||||
IssuedInvoicesummaryData,
|
||||
IssuedInvoicesummaryPageData,
|
||||
} from "./issued-invoice-resume.form.schema";
|
||||
|
||||
/**
|
||||
* Convierte el DTO completo de API a datos numéricos para el formulario.
|
||||
*/
|
||||
export const IssueInvoiceResumeDtoAdapter = {
|
||||
fromDto(pageDto: IssueInvoiceSummaryPage, context?: unknown): IssueInvoiceSummaryPageData {
|
||||
fromDto(pageDto: IssuedInvoicesummaryPage, context?: unknown): IssuedInvoicesummaryPageData {
|
||||
return {
|
||||
...pageDto,
|
||||
items: pageDto.items.map(
|
||||
@ -64,7 +64,7 @@ export const IssueInvoiceResumeDtoAdapter = {
|
||||
),
|
||||
|
||||
//taxes: dto.taxes,
|
||||
}) as unknown as IssueInvoiceSummaryData
|
||||
}) as unknown as IssuedInvoicesummaryData
|
||||
),
|
||||
};
|
||||
},
|
||||
@ -1,6 +1,6 @@
|
||||
import type { IssueInvoiceSummary, IssueInvoiceSummaryPage } from "./issue-invoice.api.schema";
|
||||
import type { IssuedInvoicesummary, IssuedInvoicesummaryPage } from "./issued-invoice.api.schema";
|
||||
|
||||
export type IssueInvoiceSummaryData = IssueInvoiceSummary & {
|
||||
export type IssuedInvoicesummaryData = IssuedInvoicesummary & {
|
||||
subtotal_amount_fmt: string;
|
||||
subtotal_amount: number;
|
||||
|
||||
@ -20,6 +20,6 @@ export type IssueInvoiceSummaryData = IssueInvoiceSummary & {
|
||||
total_amount: number;
|
||||
};
|
||||
|
||||
export type IssueInvoiceSummaryPageData = IssueInvoiceSummaryPage & {
|
||||
items: IssueInvoiceSummary[];
|
||||
export type IssuedInvoicesummaryPageData = IssuedInvoicesummaryPage & {
|
||||
items: IssuedInvoicesummary[];
|
||||
};
|
||||
@ -0,0 +1,26 @@
|
||||
import {
|
||||
GetIssuedInvoiceByIdResponseSchema,
|
||||
ListIssuedInvoicesResponseSchema,
|
||||
} from "@erp/customer-invoices/common";
|
||||
import type { ArrayElement } from "@repo/rdx-utils";
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
// IssuedInvoices
|
||||
export const IssuedInvoiceschema = GetIssuedInvoiceByIdResponseSchema.omit({
|
||||
metadata: true,
|
||||
});
|
||||
|
||||
export type IssueInvoice = z.infer<typeof IssuedInvoiceschema>;
|
||||
export type IssueInvoiceRecipient = IssueInvoice["recipient"];
|
||||
export type IssueInvoiceItem = ArrayElement<IssueInvoice["items"]>;
|
||||
|
||||
// Resultado de consulta con criteria (paginado, etc.)
|
||||
export const IssuedInvoicesummaryPageSchema = ListIssuedInvoicesResponseSchema.omit({
|
||||
metadata: true,
|
||||
});
|
||||
|
||||
export type IssuedInvoicesummaryPage = z.infer<typeof IssuedInvoicesummaryPageSchema>;
|
||||
export type IssuedInvoicesummary = Omit<
|
||||
ArrayElement<IssuedInvoicesummaryPage["items"]>,
|
||||
"metadata"
|
||||
>;
|
||||
@ -5,7 +5,6 @@ import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { usePinnedPreviewSheet } from "../../hooks";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { useProformasGridColumns } from "../../proformas/pages/list/use-proformas-grid-columns";
|
||||
import type { InvoiceSummaryFormData, InvoicesPageFormData } from "../../schemas";
|
||||
|
||||
export type InvoiceUpdateCompProps = {
|
||||
|
||||
@ -10,8 +10,6 @@ import { invoiceResumeDtoToFormAdapter } from "../../adapters/invoice-resume-dto
|
||||
import { useInvoicesQuery } from "../../hooks";
|
||||
import { useTranslation } from "../../i18n";
|
||||
|
||||
import { InvoicesListGrid } from "./invoices-list-grid";
|
||||
|
||||
export const InvoiceListPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
@ -95,16 +93,7 @@ export const InvoiceListPage = () => {
|
||||
<AppContent>
|
||||
<div className="flex flex-col w-full h-full py-3">
|
||||
<div className={"flex-1"}>
|
||||
<InvoicesListGrid
|
||||
invoicesPage={invoicesPageData}
|
||||
loading={isLoading}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
onSearchChange={handleSearchChange}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
searchValue={search}
|
||||
/>
|
||||
<>hola</>
|
||||
</div>
|
||||
</div>
|
||||
</AppContent>
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export * from "./use-proformas-grid-columns";
|
||||
export * from "./use-proformas-list";
|
||||
|
||||
@ -0,0 +1,443 @@
|
||||
import { formatDate } from "@erp/core/client";
|
||||
import { DataTableColumnHeader } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import {
|
||||
CopyIcon,
|
||||
DownloadIcon,
|
||||
EditIcon,
|
||||
MailIcon,
|
||||
MoreVerticalIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { ProformaSummaryData } from "../../../schema";
|
||||
import { ProformaStatusBadge } from "../ui";
|
||||
|
||||
type GridActionHandlers = {
|
||||
onEdit?: (proforma: ProformaSummaryData) => void;
|
||||
onDuplicate?: (proforma: ProformaSummaryData) => void;
|
||||
onDownloadPdf?: (proforma: ProformaSummaryData) => void;
|
||||
onSendEmail?: (proforma: ProformaSummaryData) => void;
|
||||
onDelete?: (proforma: ProformaSummaryData) => void;
|
||||
};
|
||||
|
||||
export function useProformasGridColumns(
|
||||
actionHandlers: GridActionHandlers = {}
|
||||
): ColumnDef<ProformaSummaryData, unknown>[] {
|
||||
const { t } = useTranslation();
|
||||
const { onEdit, onDuplicate, onDownloadPdf, onSendEmail, onDelete } = actionHandlers;
|
||||
|
||||
return React.useMemo<ColumnDef<ProformaSummaryData>[]>(
|
||||
() => [
|
||||
// Nº
|
||||
{
|
||||
accessorKey: "invoice_number",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left"
|
||||
column={column}
|
||||
title={t("pages.proformas.list.grid_columns.invoice_number")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-semibold text-left text-primary">{row.original.invoice_number}</div>
|
||||
),
|
||||
enableHiding: false,
|
||||
enableSorting: false,
|
||||
size: 160,
|
||||
minSize: 120,
|
||||
meta: {
|
||||
title: t("pages.proformas.list.grid_columns.invoice_number"),
|
||||
},
|
||||
},
|
||||
// Estado
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left"
|
||||
column={column}
|
||||
title={t("pages.proformas.list.grid_columns.status")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => <ProformaStatusBadge status={row.original.status} />,
|
||||
enableSorting: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
meta: {
|
||||
title: t("pages.proformas.list.grid_columns.status"),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "recipient",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left"
|
||||
column={column}
|
||||
title={t("pages.proformas.list.grid_columns.recipient")}
|
||||
/>
|
||||
),
|
||||
accessorFn: (row) => row.recipient.name, // para ordenar/buscar por nombre
|
||||
enableHiding: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
cell: ({ row }) => {
|
||||
const c = row.original.recipient;
|
||||
return (
|
||||
<div className="flex items-start gap-1 my-1.5">
|
||||
<div className="min-w-0 grid gap-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-medium truncate text-primary">{c.name}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{c.tin && <span className="font-base truncate">{c.tin}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
title: t("pages.proformas.list.grid_columns.recipient"),
|
||||
},
|
||||
},
|
||||
// Serie
|
||||
{
|
||||
accessorKey: "series",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left"
|
||||
column={column}
|
||||
title={t("pages.proformas.list.grid_columns.series")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => <div className="font-normal text-left">{row.original.series}</div>,
|
||||
enableSorting: false,
|
||||
size: 120,
|
||||
minSize: 100,
|
||||
meta: {
|
||||
title: t("pages.proformas.list.grid_columns.series"),
|
||||
},
|
||||
},
|
||||
// Referencia
|
||||
{
|
||||
accessorKey: "reference",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left"
|
||||
column={column}
|
||||
title={t("pages.proformas.list.grid_columns.reference")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => <div className="font-medium text-left">{row.original.reference}</div>,
|
||||
enableSorting: false,
|
||||
size: 120,
|
||||
minSize: 100,
|
||||
meta: {
|
||||
title: t("pages.proformas.list.grid_columns.reference"),
|
||||
},
|
||||
},
|
||||
|
||||
// Fecha factura
|
||||
{
|
||||
accessorKey: "invoice_date",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left tabular-nums"
|
||||
column={column}
|
||||
title={t("pages.proformas.list.grid_columns.invoice_date")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium text-left tabular-nums">
|
||||
{formatDate(row.original.invoice_date)}
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
meta: {
|
||||
title: t("pages.proformas.list.grid_columns.invoice_date"),
|
||||
},
|
||||
},
|
||||
// Fecha operación
|
||||
{
|
||||
accessorKey: "operation_date",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left tabular-nums"
|
||||
column={column}
|
||||
title={t("pages.proformas.list.grid_columns.operation_date")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium text-left tabular-nums">
|
||||
{formatDate(row.original.operation_date)}
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
meta: {
|
||||
title: t("pages.proformas.list.grid_columns.operation_date"),
|
||||
},
|
||||
},
|
||||
|
||||
// Subtotal amount
|
||||
{
|
||||
accessorKey: "subtotal_amount_fmt",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-right tabular-nums"
|
||||
column={column}
|
||||
title={t("pages.proformas.list.grid_columns.subtotal_amount")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium text-right tabular-nums">
|
||||
{row.original.subtotal_amount_fmt}
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 120,
|
||||
minSize: 100,
|
||||
meta: {
|
||||
title: t("pages.proformas.list.grid_columns.subtotal_amount"),
|
||||
},
|
||||
},
|
||||
|
||||
// Discount amount
|
||||
{
|
||||
accessorKey: "discount_amount_fmt",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-right tabular-nums"
|
||||
column={column}
|
||||
title={t("pages.proformas.list.grid_columns.discount_amount")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium text-right tabular-nums">
|
||||
{row.original.discount_amount_fmt}
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 120,
|
||||
minSize: 100,
|
||||
meta: {
|
||||
title: t("pages.proformas.list.grid_columns.discount_amount"),
|
||||
},
|
||||
},
|
||||
|
||||
// Taxes amount
|
||||
{
|
||||
accessorKey: "taxes_amount_fmt",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-right tabular-nums"
|
||||
column={column}
|
||||
title={t("pages.proformas.list.grid_columns.taxes_amount")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium text-right tabular-nums">{row.original.taxes_amount_fmt}</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 120,
|
||||
minSize: 100,
|
||||
meta: {
|
||||
title: t("pages.proformas.list.grid_columns.taxes_amount"),
|
||||
},
|
||||
},
|
||||
|
||||
// Total amount
|
||||
{
|
||||
accessorKey: "total_amount_fmt",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-right tabular-nums"
|
||||
column={column}
|
||||
title={t("pages.proformas.list.grid_columns.total_amount")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-semibold text-right tabular-nums">
|
||||
{row.original.total_amount_fmt}
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
meta: {
|
||||
title: t("pages.proformas.list.grid_columns.total_amount"),
|
||||
},
|
||||
},
|
||||
|
||||
// ─────────────────────────────
|
||||
// Acciones
|
||||
// ─────────────────────────────
|
||||
{
|
||||
id: "actions",
|
||||
header: () => <span className="sr-only">{t("common.actions")}</span>,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
size: 110,
|
||||
minSize: 96,
|
||||
cell: ({ row }) => {
|
||||
const proforma = row.original;
|
||||
const stop = (e: React.MouseEvent | React.KeyboardEvent) => e.stopPropagation();
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
{/* Editar (acción primaria) */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
aria-label={t("common.edit_row")}
|
||||
className="cursor-pointer text-muted-foreground hover:text-primary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit?.(proforma);
|
||||
}}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<EditIcon aria-hidden="true" className="size-4 " />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("common.edit_row")}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Duplicar */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
aria-label={t("common.duplicate_row")}
|
||||
className="cursor-pointer text-muted-foreground hover:text-primary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDuplicate?.(proforma);
|
||||
}}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<CopyIcon aria-hidden="true" className="size-4 " />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("common.duplicate_row")}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Descargar en PDF */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
aria-label={t("common.download_pdf")}
|
||||
className="cursor-pointer text-muted-foreground hover:text-primary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDownloadPdf?.(proforma);
|
||||
}}
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<DownloadIcon aria-hidden="true" className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("common.download_pdf")}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
aria-label={t("common.delete_row")}
|
||||
className="cursor-pointer text-destructive hover:bg-destructive/90 hover:text-white"
|
||||
onClick={() => onDelete?.(proforma)}
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<Trash2Icon aria-hidden="true" className="size-4" />
|
||||
<span className="sr-only">{t("common.delete_row")}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("common.delete_row")}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Menú demás acciones */}
|
||||
{/** biome-ignore lint/suspicious/noSelfCompare: <Desactivado por ahora> */}
|
||||
{false === false && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
aria-label={t("common.more_actions")}
|
||||
className="cursor-pointer text-muted-foreground hover:text-primary"
|
||||
onClick={stop}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" className="size-4" />
|
||||
<span className="sr-only">{t("common.more_actions")}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => onDuplicate?.(proforma)}
|
||||
>
|
||||
<CopyIcon className="mr-2 size-4" />
|
||||
{t("common.duplicate_row")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => onDownloadPdf?.(proforma)}
|
||||
>
|
||||
<DownloadIcon className="mr-2 size-4" />
|
||||
{t("common.download_pdf")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => onSendEmail?.(proforma)}
|
||||
>
|
||||
<MailIcon className="mr-2 size-4" />
|
||||
{t("common.send_email")}
|
||||
</DropdownMenuItem>{" "}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive-foreground focus:bg-destructive cursor-pointer"
|
||||
onClick={() => onDelete?.(proforma)}
|
||||
>
|
||||
<Trash2Icon className="mr-2 size-4 text-destructive focus:text-destructive-foreground" />
|
||||
{t("common.delete_row")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
title: t("common.actions"),
|
||||
},
|
||||
},
|
||||
],
|
||||
[t, onEdit, onDuplicate, onDownloadPdf, onSendEmail, onDelete]
|
||||
);
|
||||
}
|
||||
@ -11,19 +11,23 @@ export const useProformasList = () => {
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [search, setSearch] = useState("");
|
||||
const [status, setStatus] = useState("all");
|
||||
|
||||
const debouncedQ = useDebounce(search, 300);
|
||||
|
||||
const criteria = useMemo<CriteriaDTO>(
|
||||
() => ({
|
||||
const criteria = useMemo<CriteriaDTO>(() => {
|
||||
const baseFilters =
|
||||
status !== "all" ? [{ field: "status", operator: "CONTAINS", value: status }] : [];
|
||||
|
||||
return {
|
||||
q: debouncedQ || "",
|
||||
pageSize,
|
||||
pageNumber: pageIndex,
|
||||
order: "desc",
|
||||
orderBy: "invoice_date",
|
||||
}),
|
||||
[pageSize, pageIndex, debouncedQ]
|
||||
);
|
||||
filters: baseFilters,
|
||||
};
|
||||
}, [pageSize, pageIndex, debouncedQ, status]);
|
||||
|
||||
const query = useProformasQuery({ criteria });
|
||||
const data = useMemo(
|
||||
@ -33,6 +37,8 @@ export const useProformasList = () => {
|
||||
|
||||
const setSearchValue = (value: string) => setSearch(value.trim().replace(/\s+/g, " "));
|
||||
|
||||
const setStatusFilter = (newStatus: string) => setStatus(newStatus);
|
||||
|
||||
return {
|
||||
...query,
|
||||
data,
|
||||
@ -42,5 +48,6 @@ export const useProformasList = () => {
|
||||
setPageIndex,
|
||||
setPageSize,
|
||||
setSearchValue,
|
||||
setStatusFilter,
|
||||
};
|
||||
};
|
||||
|
||||
@ -51,6 +51,7 @@ export const ProformaListPage = () => {
|
||||
onPageChange={list.setPageIndex}
|
||||
onPageSizeChange={list.setPageSize}
|
||||
onSearchChange={list.setSearchValue}
|
||||
onStatusFilterChange={list.setStatusFilter}
|
||||
pageIndex={list.pageIndex}
|
||||
pageSize={list.pageSize}
|
||||
searchValue={list.search}
|
||||
|
||||
@ -1,114 +0,0 @@
|
||||
import type { CriteriaDTO } from "@erp/core";
|
||||
import { PageHeader } from "@erp/core/components";
|
||||
import { ErrorAlert } from "@erp/customers/components";
|
||||
import { AppContent, AppHeader, BackHistoryButton, useDebounce } from "@repo/rdx-ui/components";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useTranslation } from "../../../i18n";
|
||||
import { useProformasQuery } from "../../hooks";
|
||||
import { ProformaSummaryDtoAdapter } from "../../../adapters/proforma-summary-dto.adapter";
|
||||
|
||||
import { ProformasGrid } from "./proformas-grid";
|
||||
|
||||
export const ProformaListPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const debouncedQ = useDebounce(search, 300);
|
||||
|
||||
const criteria = useMemo(
|
||||
() =>
|
||||
({
|
||||
q: debouncedQ || "",
|
||||
pageSize,
|
||||
pageNumber: pageIndex,
|
||||
order: "desc",
|
||||
orderBy: "invoice_date",
|
||||
}) as CriteriaDTO,
|
||||
[pageSize, pageIndex, debouncedQ]
|
||||
);
|
||||
|
||||
const { data, isLoading, isError, error } = useProformasQuery({
|
||||
criteria,
|
||||
});
|
||||
|
||||
const proformaPageData = useMemo(() => {
|
||||
if (!data) return undefined;
|
||||
return ProformaSummaryDtoAdapter.fromDto(data);
|
||||
}, [data]);
|
||||
|
||||
const handlePageChange = (newPageIndex: number) => {
|
||||
setPageIndex(newPageIndex);
|
||||
};
|
||||
|
||||
const handlePageSizeChange = (newSize: number) => {
|
||||
setPageSize(newSize);
|
||||
setPageIndex(0);
|
||||
};
|
||||
|
||||
const handleSearchChange = (value: string) => {
|
||||
// Normalización ligera: recorta y colapsa espacios internos
|
||||
const cleaned = value.trim().replace(/\s+/g, " ");
|
||||
setSearch(cleaned);
|
||||
setPageIndex(0);
|
||||
};
|
||||
|
||||
if (isError || !proformaPageData) {
|
||||
return (
|
||||
<AppContent>
|
||||
<ErrorAlert
|
||||
message={(error as Error)?.message || "Error al cargar el listado"}
|
||||
title={t("pages.proformas.list.loadErrorTitle")}
|
||||
/>
|
||||
<BackHistoryButton />
|
||||
</AppContent>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppHeader>
|
||||
<PageHeader
|
||||
description={t("pages.proformas.list.description")}
|
||||
rightSlot={
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
aria-label={t("pages.proformas.create.title")}
|
||||
className="cursor-pointer"
|
||||
onClick={() => navigate("/proformas/create")}
|
||||
variant={"default"}
|
||||
>
|
||||
<PlusIcon aria-hidden className="mr-2 h-4 w-4" />
|
||||
{t("pages.proformas.create.title")}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
title={t("pages.proformas.list.title")}
|
||||
/>
|
||||
</AppHeader>
|
||||
<AppContent>
|
||||
<div className="flex flex-col w-full h-full py-3">
|
||||
<div className={"flex-1"}>
|
||||
<ProformasGrid
|
||||
data={proformaPageData}
|
||||
loading={isLoading}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
onSearchChange={handleSearchChange}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
searchValue={search}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AppContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,196 +0,0 @@
|
||||
import { SimpleSearchInput } from "@erp/core/components";
|
||||
import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Button,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { FileDownIcon, FilterIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { usePinnedPreviewSheet } from "../../../hooks";
|
||||
import { useTranslation } from "../../../i18n";
|
||||
import type { InvoiceSummaryFormData } from "../../../schemas";
|
||||
import type { ProformaSummaryPageData } from "../../schema/proforma-summary.web.schema";
|
||||
|
||||
import { useProformasGridColumns } from "./use-proformas-grid-columns";
|
||||
|
||||
export type ProformaGridProps = {
|
||||
data: ProformaSummaryPageData;
|
||||
loading?: boolean;
|
||||
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
onPageChange?: (pageNumber: number) => void;
|
||||
onPageSizeChange?: (pageSize: number) => void;
|
||||
|
||||
searchValue: string;
|
||||
onSearchChange: (value: string) => void;
|
||||
|
||||
onRowClick?: (
|
||||
row: ProformaSummaryPageData,
|
||||
index: number,
|
||||
event: React.MouseEvent<HTMLTableRowElement>
|
||||
) => void;
|
||||
};
|
||||
|
||||
// Create new GridExample component
|
||||
export const ProformasGrid = ({
|
||||
data,
|
||||
|
||||
loading,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
searchValue,
|
||||
onSearchChange,
|
||||
onRowClick,
|
||||
}: ProformaGridProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { items, total_items } = data;
|
||||
|
||||
// Hook con Sheet de shadcn
|
||||
const preview = usePinnedPreviewSheet<InvoiceSummaryFormData>({
|
||||
persistKey: "invoice-preview-pin",
|
||||
widthClass: "w-[500px]",
|
||||
});
|
||||
|
||||
const [statusFilter, setStatusFilter] = useState("todas");
|
||||
|
||||
const columns = useProformasGridColumns({
|
||||
onEdit: (proforma) => navigate(`/proformas/${proforma.id}/edit`),
|
||||
onDuplicate: (proforma) => null, //duplicateInvoice(inv.id),
|
||||
onDownloadPdf: (proforma) => null, //downloadInvoicePdf(inv.id),
|
||||
onSendEmail: (proforma) => null, //sendInvoiceEmail(inv.id),
|
||||
onDelete: (proforma) => null, //confirmDelete(inv.id),
|
||||
});
|
||||
|
||||
// Navegación accesible (click o teclado)
|
||||
/*const goToRow = useCallback(
|
||||
(id: string, newTab = false) => {
|
||||
const url = `/customer-invoices/${id}/edit`;
|
||||
newTab ? window.open(url, "_blank", "noopener,noreferrer") : navigate(url);
|
||||
},
|
||||
[navigate]
|
||||
);*/
|
||||
|
||||
/*const onRowClicked = useCallback(
|
||||
(e: RowClickedEvent<any>) => {
|
||||
if (!e.data) return;
|
||||
const newTab = e.event instanceof MouseEvent && (e.event.metaKey || e.event.ctrlKey);
|
||||
goToRow(e.data.id, newTab);
|
||||
},
|
||||
[goToRow]
|
||||
);
|
||||
|
||||
const onCellKeyDown = useCallback(
|
||||
(e: CellKeyDownEvent<any>) => {
|
||||
if (!e.data) return;
|
||||
|
||||
const ev = e.event;
|
||||
if (!(ev && ev instanceof KeyboardEvent)) return;
|
||||
|
||||
const key = ev.key;
|
||||
if (key === "Enter" || key === " ") {
|
||||
ev.preventDefault();
|
||||
goToRow(e.data.id);
|
||||
}
|
||||
if ((ev.ctrlKey || ev.metaKey) && key === "Enter") {
|
||||
ev.preventDefault();
|
||||
goToRow(e.data.id, true);
|
||||
}
|
||||
},
|
||||
[goToRow]
|
||||
);
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(invoice: InvoiceSummaryFormData, _i: number, e: React.MouseEvent) => {
|
||||
const url = `/customer-invoices/${invoice.id}/edit`;
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
return;
|
||||
}
|
||||
preview.open(invoice);
|
||||
},
|
||||
[preview]
|
||||
);*/
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<SkeletonDataTable
|
||||
columns={columns.length}
|
||||
footerProps={{ pageIndex, pageSize, totalItems: total_items ?? 0 }}
|
||||
rows={Math.max(6, pageSize)}
|
||||
showFooter
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Render principal
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Barra de filtros */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-6">
|
||||
<SimpleSearchInput loading={loading} onSearchChange={onSearchChange} />
|
||||
<Select onValueChange={setStatusFilter} value={statusFilter}>
|
||||
<SelectTrigger className="w-full sm:w-48 bg-white border-gray-200 shadow-sm">
|
||||
<FilterIcon className="mr-2 h-4 w-4" />
|
||||
<SelectValue placeholder="Estado" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="todas">Todas</SelectItem>
|
||||
<SelectItem value="pagada">Borradores</SelectItem>
|
||||
<SelectItem value="pendiente">Enviadas</SelectItem>
|
||||
<SelectItem value="vencida">Aprobadas</SelectItem>
|
||||
<SelectItem value="vencida">Rechazadas</SelectItem>
|
||||
<SelectItem value="vencida">Emitidas</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
className="border-blue-200 text-blue-600 hover:bg-blue-50 bg-transparent"
|
||||
variant="outline"
|
||||
>
|
||||
<FileDownIcon className="mr-2 h-4 w-4" />
|
||||
Exportar
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative flex">
|
||||
<div className={preview.isPinned ? "flex-1 mr-[500px]" : "flex-1"}>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={items}
|
||||
enablePagination
|
||||
enableRowSelection
|
||||
manualPagination
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
//onRowClick={handleRowClick}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
readOnly
|
||||
totalItems={total_items}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/*<preview.Preview>
|
||||
{({ item, isPinned, close, togglePin }) => (
|
||||
<InvoicePreviewPanel
|
||||
invoice={item}
|
||||
isPinned={isPinned}
|
||||
onClose={close}
|
||||
onTogglePin={togglePin}
|
||||
/>
|
||||
)}
|
||||
</preview.Preview>*/}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1 +1,2 @@
|
||||
export * from "./proforma-status-badge";
|
||||
export * from "./proformas-grid";
|
||||
|
||||
@ -0,0 +1,68 @@
|
||||
import { Badge } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { forwardRef } from "react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
|
||||
export type ProformaStatus = "draft" | "sent" | "approved" | "rejected" | "issued";
|
||||
|
||||
export type ProformaStatusBadgeProps = {
|
||||
status: string | ProformaStatus; // permitir cualquier valor
|
||||
dotVisible?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const statusColorConfig: Record<ProformaStatus, { badge: string; dot: string }> = {
|
||||
draft: {
|
||||
badge:
|
||||
"bg-gray-500/10 dark:bg-gray-500/20 hover:bg-gray-500/10 text-gray-600 border-gray-400/60",
|
||||
dot: "bg-gray-500",
|
||||
},
|
||||
sent: {
|
||||
badge:
|
||||
"bg-amber-500/10 dark:bg-amber-500/20 hover:bg-amber-500/10 text-amber-500 border-amber-600/60",
|
||||
dot: "bg-amber-500",
|
||||
},
|
||||
approved: {
|
||||
badge:
|
||||
"bg-emerald-500/10 dark:bg-emerald-500/20 hover:bg-emerald-500/10 text-emerald-500 border-emerald-600/60",
|
||||
dot: "bg-emerald-500",
|
||||
},
|
||||
rejected: {
|
||||
badge: "bg-red-500/10 dark:bg-red-500/20 hover:bg-red-500/10 text-red-500 border-red-600/60",
|
||||
dot: "bg-red-500",
|
||||
},
|
||||
issued: {
|
||||
badge:
|
||||
"bg-blue-600/10 dark:bg-blue-600/20 hover:bg-blue-600/10 text-blue-500 border-blue-600/60",
|
||||
dot: "bg-blue-500",
|
||||
},
|
||||
};
|
||||
|
||||
export const ProformaStatusBadge = forwardRef<HTMLDivElement, ProformaStatusBadgeProps>(
|
||||
({ status, dotVisible, className, ...props }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const normalizedStatus = status.toLowerCase() as ProformaStatus;
|
||||
const config = statusColorConfig[normalizedStatus];
|
||||
const commonClassName =
|
||||
"transition-colors duration-200 cursor-pointer shadow-none rounded-full";
|
||||
|
||||
if (!config) {
|
||||
return (
|
||||
<Badge className={cn(commonClassName, className)} ref={ref} {...props}>
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge className={cn(commonClassName, config.badge, className)} {...props}>
|
||||
{dotVisible && <div className={cn("h-1.5 w-1.5 rounded-full mr-2", config.dot)} />}
|
||||
{t(`catalog.proformas.status.${normalizedStatus}`, { defaultValue: status })}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ProformaStatusBadge.displayName = "ProformaStatusBadge";
|
||||
@ -1,18 +1,18 @@
|
||||
import { SimpleSearchInput } from "@erp/core/components";
|
||||
import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Button,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { FileDownIcon, FilterIcon } from "lucide-react";
|
||||
import { FilterIcon } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { ProformaSummaryPageData } from "../../../schema/proforma-summary.web.schema";
|
||||
import { useProformasGridColumns } from "../use-proformas-grid-columns";
|
||||
import { useProformasGridColumns } from "../hooks";
|
||||
|
||||
interface ProformasGridProps {
|
||||
data: ProformaSummaryPageData;
|
||||
@ -25,6 +25,7 @@ interface ProformasGridProps {
|
||||
onPageSizeChange: (s: number) => void;
|
||||
onRowClick?: (id: string) => void;
|
||||
onExportClick?: () => void;
|
||||
onStatusFilterChange?: (newStatus: string) => void;
|
||||
}
|
||||
|
||||
export const ProformasGrid = ({
|
||||
@ -38,10 +39,19 @@ export const ProformasGrid = ({
|
||||
onPageSizeChange,
|
||||
onRowClick,
|
||||
onExportClick,
|
||||
onStatusFilterChange,
|
||||
}: ProformasGridProps) => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const { items, total_items } = data;
|
||||
const columns = useProformasGridColumns();
|
||||
|
||||
const columns = useProformasGridColumns({
|
||||
onEdit: (proforma) => navigate(`/proformas/${proforma.id}/edit`),
|
||||
onDuplicate: (proforma) => null, //duplicateInvoice(inv.id),
|
||||
onDownloadPdf: (proforma) => null, //downloadInvoicePdf(inv.id),
|
||||
onSendEmail: (proforma) => null, //sendInvoiceEmail(inv.id),
|
||||
onDelete: (proforma) => null, //confirmDelete(inv.id),
|
||||
});
|
||||
|
||||
if (loading)
|
||||
return (
|
||||
@ -55,33 +65,37 @@ export const ProformasGrid = ({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-6">
|
||||
<SimpleSearchInput onSearchChange={onSearchChange} value={searchValue} />
|
||||
<Select defaultValue="all">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<SimpleSearchInput loading={loading} onSearchChange={onSearchChange} />
|
||||
<Select defaultValue="all" onValueChange={onStatusFilterChange}>
|
||||
<SelectTrigger className="w-full sm:w-48">
|
||||
<FilterIcon aria-hidden className="mr-2 size-4" />
|
||||
<SelectValue placeholder={t("filters.status")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">{t("filters.all")}</SelectItem>
|
||||
<SelectItem value="draft">{t("filters.draft")}</SelectItem>
|
||||
<SelectItem value="sent">{t("filters.sent")}</SelectItem>
|
||||
<SelectItem value="all">{t("catalog.proformas.status.all")}</SelectItem>
|
||||
<SelectItem value="draft">{t("catalog.proformas.status.draft")}</SelectItem>
|
||||
<SelectItem value="sent">{t("catalog.proformas.status.sent")}</SelectItem>
|
||||
<SelectItem value="approved">{t("catalog.proformas.status.approved")}</SelectItem>
|
||||
<SelectItem value="rejected">{t("catalog.proformas.status.rejected")}</SelectItem>
|
||||
<SelectItem value="issued">{t("catalog.proformas.status.issued")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button onClick={onExportClick} variant="outline">
|
||||
<FileDownIcon aria-hidden className="mr-2 size-4" />
|
||||
{t("actions.export")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
columnVisibility={{
|
||||
subtotal_amount_fmt: false,
|
||||
discount_amount_fmt: false,
|
||||
taxes_amount_fmt: false,
|
||||
}}
|
||||
data={items}
|
||||
enablePagination
|
||||
manualPagination
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
onRowClick={(_, row) => onRowClick?.(row.id)}
|
||||
onRowClick={(row, _index) => onRowClick?.(row.id)}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
totalItems={total_items}
|
||||
|
||||
@ -1,314 +0,0 @@
|
||||
import { formatDate } from "@erp/core/client";
|
||||
import { DataTableColumnHeader } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
Badge,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import {
|
||||
Building2Icon,
|
||||
CopyIcon,
|
||||
DownloadIcon,
|
||||
EditIcon,
|
||||
MailIcon,
|
||||
MoreVerticalIcon,
|
||||
Trash2Icon,
|
||||
User2Icon,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { useTranslation } from "../../../i18n";
|
||||
import type { InvoiceSummaryFormData } from "../../../schemas";
|
||||
import { CustomerInvoiceStatusBadge } from "../../../shared/ui/components";
|
||||
|
||||
type GridActionHandlers = {
|
||||
onEdit?: (invoice: InvoiceSummaryFormData) => void;
|
||||
onDuplicate?: (invoice: InvoiceSummaryFormData) => void;
|
||||
onDownloadPdf?: (invoice: InvoiceSummaryFormData) => void;
|
||||
onSendEmail?: (invoice: InvoiceSummaryFormData) => void;
|
||||
onDelete?: (invoice: InvoiceSummaryFormData) => void;
|
||||
};
|
||||
|
||||
function initials(name: string) {
|
||||
const parts = name.trim().split(/\s+/).slice(0, 2);
|
||||
return parts.map((p) => p[0]?.toUpperCase() ?? "").join("") || "?";
|
||||
}
|
||||
|
||||
const KindBadge = ({ isCompany }: { isCompany: boolean }) => (
|
||||
<Badge className="gap-1 tracking-wide text-xs text-foreground/70" variant="outline">
|
||||
{isCompany ? <Building2Icon className="size-3.5" /> : <User2Icon className="size-3.5" />}
|
||||
{isCompany ? "Company" : "Person"}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
const Soft = ({ children }: { children: React.ReactNode }) => (
|
||||
<span className="text-muted-foreground">{children}</span>
|
||||
);
|
||||
|
||||
export function useProformasGridColumns(
|
||||
actionHandlers: GridActionHandlers = {}
|
||||
): ColumnDef<InvoiceSummaryFormData>[] {
|
||||
const { t } = useTranslation();
|
||||
const { onEdit, onDuplicate, onDownloadPdf, onSendEmail, onDelete } = actionHandlers;
|
||||
|
||||
return React.useMemo<ColumnDef<InvoiceSummaryFormData>[]>(
|
||||
() => [
|
||||
// Nº
|
||||
{
|
||||
accessorKey: "invoice_number",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left"
|
||||
column={column}
|
||||
title={t("pages.proformas.list.grid_columns.invoice_number")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-semibold text-left text-primary">{row.original.invoice_number}</div>
|
||||
),
|
||||
enableHiding: false,
|
||||
enableSorting: false,
|
||||
size: 160,
|
||||
minSize: 120,
|
||||
},
|
||||
// Estado
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left"
|
||||
column={column}
|
||||
title={t("pages.proformas.list.grid_columns.status")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => <CustomerInvoiceStatusBadge status={row.original.status} />,
|
||||
enableSorting: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
},
|
||||
{
|
||||
id: "recipient",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left"
|
||||
column={column}
|
||||
title={t("pages.list.grid_columns.recipient")}
|
||||
/>
|
||||
),
|
||||
accessorFn: (row) => row.recipient.name, // para ordenar/buscar por nombre
|
||||
enableHiding: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
cell: ({ row }) => {
|
||||
const c = row.original.recipient;
|
||||
const isCompany = String(c.is_company).toLowerCase() === "true";
|
||||
return (
|
||||
<div className="flex items-start gap-1 my-1.5">
|
||||
<Avatar className="size-10 hidden">
|
||||
<AvatarFallback aria-label={c.name}>{initials(c.name)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0 grid gap-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-medium truncate text-primary">{c.name}</span>
|
||||
{c.trade_name && <Soft>({c.trade_name})</Soft>}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{c.tin && <span className="font-base truncate">{c.tin}</span>}
|
||||
<KindBadge isCompany={isCompany} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
// Serie
|
||||
{
|
||||
accessorKey: "series",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left"
|
||||
column={column}
|
||||
title={t("pages.proformas.list.grid_columns.series")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => <div className="font-normal text-left">{row.original.series}</div>,
|
||||
enableSorting: false,
|
||||
size: 120,
|
||||
minSize: 100,
|
||||
},
|
||||
// Referencia
|
||||
{
|
||||
accessorKey: "reference",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left"
|
||||
column={column}
|
||||
title={t("pages.proformas.list.grid_columns.reference")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => <div className="font-medium text-left">{row.original.reference}</div>,
|
||||
enableSorting: false,
|
||||
size: 120,
|
||||
minSize: 100,
|
||||
},
|
||||
|
||||
// Fecha factura
|
||||
{
|
||||
accessorKey: "invoice_date",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left tabular-nums"
|
||||
column={column}
|
||||
title={t("pages.proformas.list.grid_columns.invoice_date")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium text-left tabular-nums">
|
||||
{formatDate(row.original.invoice_date)}
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
},
|
||||
// Fecha operación
|
||||
{
|
||||
accessorKey: "operation_date",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left tabular-nums"
|
||||
column={column}
|
||||
title={t("pages.proformas.list.grid_columns.operation_date")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium text-left tabular-nums">
|
||||
{formatDate(row.original.operation_date)}
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
},
|
||||
|
||||
// Total
|
||||
{
|
||||
accessorKey: "total_amount_fmt",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-right tabular-nums"
|
||||
column={column}
|
||||
title={t("pages.proformas.list.grid_columns.total_amount")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-semibold text-right tabular-nums">
|
||||
{row.original.total_amount_fmt}
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
},
|
||||
|
||||
// ─────────────────────────────
|
||||
// Acciones
|
||||
// ─────────────────────────────
|
||||
{
|
||||
id: "actions",
|
||||
header: () => <span className="sr-only">{t("common.actions")}</span>,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
size: 110,
|
||||
minSize: 96,
|
||||
cell: ({ row }) => {
|
||||
const invoice = row.original;
|
||||
const stop = (e: React.MouseEvent | React.KeyboardEvent) => e.stopPropagation();
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-1 pr-1">
|
||||
{/* Editar (acción primaria) */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
aria-label={t("common.edit")}
|
||||
className="cursor-pointer text-muted-foreground hover:text-primary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit?.(invoice);
|
||||
}}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<EditIcon aria-hidden="true" className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("common.edit")}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Menú demás acciones */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
aria-label={t("common.more_actions")}
|
||||
className="cursor-pointer text-muted-foreground hover:text-primary"
|
||||
onClick={stop}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => onDuplicate?.(invoice)}
|
||||
>
|
||||
<CopyIcon className="mr-2 size-4" />
|
||||
{t("common.duplicate")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => onDownloadPdf?.(invoice)}
|
||||
>
|
||||
<DownloadIcon className="mr-2 size-4" />
|
||||
{t("common.download_pdf")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => onSendEmail?.(invoice)}
|
||||
>
|
||||
<MailIcon className="mr-2 size-4" />
|
||||
{t("common.send_email")}
|
||||
</DropdownMenuItem>{" "}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive-foreground focus:bg-destructive cursor-pointer"
|
||||
onClick={() => onDelete?.(invoice)}
|
||||
>
|
||||
<Trash2Icon className="mr-2 size-4 text-destructive focus:text-destructive-foreground" />
|
||||
{t("common.delete")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[t, onEdit, onDuplicate, onDownloadPdf, onSendEmail, onDelete]
|
||||
);
|
||||
}
|
||||
@ -13,7 +13,7 @@ export type ProformaSummaryData = ProformaSummary & {
|
||||
taxable_amount_fmt: string;
|
||||
taxable_amount: number;
|
||||
|
||||
taxes_amoun_fmt: string;
|
||||
taxes_amount_fmt: string;
|
||||
taxes_amount: number;
|
||||
|
||||
total_amount_fmt: string;
|
||||
@ -21,5 +21,5 @@ export type ProformaSummaryData = ProformaSummary & {
|
||||
};
|
||||
|
||||
export type ProformaSummaryPageData = ProformaSummaryPage & {
|
||||
items: ProformaSummary[];
|
||||
items: ProformaSummaryData[];
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import {
|
||||
CreateProformaRequestSchema,
|
||||
GetProformaByIdResponseSchema,
|
||||
ListProformasResponseSchema,
|
||||
type ListProformasResponseDTO,
|
||||
UpdateProformaByIdRequestSchema,
|
||||
} from "@erp/customer-invoices/common";
|
||||
import type { ArrayElement } from "@repo/rdx-utils";
|
||||
@ -23,9 +23,5 @@ export type CreateProformaInput = z.infer<typeof CreateProformaSchema>; // Cuerp
|
||||
export type UpdateProformaInput = z.infer<typeof UpdateProformaSchema>; // Cuerpo para actualizar
|
||||
|
||||
// Resultado de consulta con criteria (paginado, etc.)
|
||||
export const ProformaSummaryPageSchema = ListProformasResponseSchema.omit({
|
||||
metadata: true,
|
||||
});
|
||||
|
||||
export type ProformaSummaryPage = z.infer<typeof ProformaSummaryPageSchema>;
|
||||
export type ProformaSummaryPage = Omit<ListProformasResponseDTO, "metadata">;
|
||||
export type ProformaSummary = Omit<ArrayElement<ProformaSummaryPage["items"]>, "metadata">;
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
import type { ArrayElement } from "@repo/rdx-utils";
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
import { GetIssueInvoiceByIdResponseSchema, ListIssueInvoicesResponseSchema } from "../../common";
|
||||
import { GetIssuedInvoiceByIdResponseSchema, ListIssuedInvoicesResponseSchema } from "../../common";
|
||||
|
||||
export const IssueInvoiceSchema = GetIssueInvoiceByIdResponseSchema.omit({
|
||||
export const IssuedInvoiceschema = GetIssuedInvoiceByIdResponseSchema.omit({
|
||||
metadata: true,
|
||||
});
|
||||
|
||||
export type IssueInvoice = z.infer<typeof IssueInvoiceSchema>;
|
||||
export type IssueInvoice = z.infer<typeof IssuedInvoiceschema>;
|
||||
export type IssueInvoiceRecipient = IssueInvoice["recipient"];
|
||||
export type IssueInvoiceItem = ArrayElement<IssueInvoice["items"]>;
|
||||
|
||||
// Resultado de consulta con criteria (paginado, etc.)
|
||||
export const IssueInvoicesPageSchema = ListIssueInvoicesResponseSchema.omit({
|
||||
export const IssuedInvoicesPageSchema = ListIssuedInvoicesResponseSchema.omit({
|
||||
metadata: true,
|
||||
});
|
||||
|
||||
|
||||
@ -1,67 +0,0 @@
|
||||
import { Badge } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { forwardRef } from "react";
|
||||
|
||||
import { useTranslation } from "../../../i18n";
|
||||
|
||||
export type CustomerInvoiceStatus = "draft" | "sent" | "approved" | "rejected" | "issued";
|
||||
|
||||
export type CustomerInvoiceStatusBadgeProps = {
|
||||
status: string | CustomerInvoiceStatus; // permitir cualquier valor
|
||||
dotVisible?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const statusColorConfig: Record<CustomerInvoiceStatus, { badge: string; dot: string }> = {
|
||||
draft: {
|
||||
badge:
|
||||
"bg-gray-500/10 dark:bg-gray-500/20 hover:bg-gray-500/10 text-gray-600 border-gray-400/60",
|
||||
dot: "bg-gray-500",
|
||||
},
|
||||
sent: {
|
||||
badge:
|
||||
"bg-amber-500/10 dark:bg-amber-500/20 hover:bg-amber-500/10 text-amber-500 border-amber-600/60",
|
||||
dot: "bg-amber-500",
|
||||
},
|
||||
approved: {
|
||||
badge:
|
||||
"bg-emerald-500/10 dark:bg-emerald-500/20 hover:bg-emerald-500/10 text-emerald-500 border-emerald-600/60",
|
||||
dot: "bg-emerald-500",
|
||||
},
|
||||
rejected: {
|
||||
badge: "bg-red-500/10 dark:bg-red-500/20 hover:bg-red-500/10 text-red-500 border-red-600/60",
|
||||
dot: "bg-red-500",
|
||||
},
|
||||
issued: {
|
||||
badge:
|
||||
"bg-blue-600/10 dark:bg-blue-600/20 hover:bg-blue-600/10 text-blue-500 border-blue-600/60",
|
||||
dot: "bg-blue-500",
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomerInvoiceStatusBadge = forwardRef<
|
||||
HTMLDivElement,
|
||||
CustomerInvoiceStatusBadgeProps
|
||||
>(({ status, dotVisible, className, ...props }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const normalizedStatus = status.toLowerCase() as CustomerInvoiceStatus;
|
||||
const config = statusColorConfig[normalizedStatus];
|
||||
const commonClassName = "transition-colors duration-200 cursor-pointer shadow-none rounded-full";
|
||||
|
||||
if (!config) {
|
||||
return (
|
||||
<Badge className={cn(commonClassName, className)} ref={ref} {...props}>
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge className={cn(commonClassName, config.badge, className)} {...props}>
|
||||
{dotVisible && <div className={cn("h-1.5 w-1.5 rounded-full mr-2", config.dot)} />}
|
||||
{t(`catalog.status.${normalizedStatus}`, { defaultValue: status })}
|
||||
</Badge>
|
||||
);
|
||||
});
|
||||
|
||||
CustomerInvoiceStatusBadge.displayName = "CustomerInvoiceStatusBadge";
|
||||
@ -1,6 +1,7 @@
|
||||
export * from "../../../proformas/pages/list/ui/proforma-status-badge";
|
||||
|
||||
export * from "./customer-invoice-editor-skeleton";
|
||||
export * from "./customer-invoice-prices-card";
|
||||
export * from "./customer-invoice-status-badge";
|
||||
export * from "./customer-invoice-taxes-multi-select";
|
||||
export * from "./editor";
|
||||
export * from "./editor/invoice-tax-summary";
|
||||
|
||||
@ -28,6 +28,6 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src", "../core/src/common/helpers/date-helper.ts"],
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user