.
This commit is contained in:
parent
1c66d20c24
commit
99c161cb31
@ -11,6 +11,12 @@
|
|||||||
"placeholder": "Select taxes",
|
"placeholder": "Select taxes",
|
||||||
"description": "Select the taxes to apply to the invoice items",
|
"description": "Select the taxes to apply to the invoice items",
|
||||||
"invalid_tax_selection": "Invalid tax selection. Please select a valid tax."
|
"invalid_tax_selection": "Invalid tax selection. Please select a valid tax."
|
||||||
|
},
|
||||||
|
"simple_search_input": {
|
||||||
|
"search_button": "Search",
|
||||||
|
"loading": "Loading",
|
||||||
|
"clear_search": "Clear search",
|
||||||
|
"search_placeholder": "Search..."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hooks": {
|
"hooks": {
|
||||||
|
|||||||
@ -10,6 +10,12 @@
|
|||||||
"placeholder": "Seleccionar impuestos",
|
"placeholder": "Seleccionar impuestos",
|
||||||
"description": "Seleccionar los impuestos a aplicar a los artículos de la factura",
|
"description": "Seleccionar los impuestos a aplicar a los artículos de la factura",
|
||||||
"invalid_tax_selection": "Selección de impuestos no válida. Por favor, seleccione un impuesto válido."
|
"invalid_tax_selection": "Selección de impuestos no válida. Por favor, seleccione un impuesto válido."
|
||||||
|
},
|
||||||
|
"simple_search_input": {
|
||||||
|
"search_button": "Buscar",
|
||||||
|
"loading": "Buscando",
|
||||||
|
"clear_search": "Limpiar búsquedaClear search",
|
||||||
|
"search_placeholder": "Escribe aquí para buscar..."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hooks": {
|
"hooks": {
|
||||||
|
|||||||
@ -8,7 +8,8 @@ import {
|
|||||||
import { Spinner } from "@repo/shadcn-ui/components/spinner";
|
import { Spinner } from "@repo/shadcn-ui/components/spinner";
|
||||||
import { SearchIcon, XIcon } from "lucide-react";
|
import { SearchIcon, XIcon } from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
import { useTranslation } from "../../i18n";
|
||||||
|
|
||||||
type SimpleSearchInputProps = {
|
type SimpleSearchInputProps = {
|
||||||
onSearchChange: (value: string) => void;
|
onSearchChange: (value: string) => void;
|
||||||
@ -103,44 +104,48 @@ export const SimpleSearchInput = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative flex-1 max-w-xl'>
|
<div className="relative flex-1 max-w-xl">
|
||||||
<InputGroup className='bg-background' data-disabled={loading}>
|
<InputGroup className="bg-background" data-disabled={loading}>
|
||||||
<InputGroupInput
|
<InputGroupInput
|
||||||
ref={inputRef}
|
autoComplete="off"
|
||||||
placeholder={t("common.search_placeholder", "Search...")}
|
|
||||||
value={searchValue}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
inputMode='search'
|
|
||||||
autoComplete='off'
|
|
||||||
spellCheck={false}
|
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
inputMode="search"
|
||||||
|
onChange={handleInputChange}
|
||||||
onFocus={() => history.length > 0 && setOpen(true)}
|
onFocus={() => history.length > 0 && setOpen(true)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={t("components.simple_search_input.search_placeholder", "Search...")}
|
||||||
|
ref={inputRef}
|
||||||
|
spellCheck={false}
|
||||||
|
value={searchValue}
|
||||||
/>
|
/>
|
||||||
<InputGroupAddon>
|
<InputGroupAddon>
|
||||||
<SearchIcon aria-hidden />
|
<SearchIcon aria-hidden />
|
||||||
</InputGroupAddon>
|
</InputGroupAddon>
|
||||||
|
|
||||||
<InputGroupAddon align='inline-end'>
|
<InputGroupAddon align="inline-end">
|
||||||
{loading && <Spinner aria-label={t("common.loading", "Loading")} />}
|
{loading && (
|
||||||
{!searchValue && !loading && (
|
<Spinner aria-label={t("components.simple_search_input.loading", "Loading")} />
|
||||||
|
)}
|
||||||
|
{!(searchValue || loading) && (
|
||||||
<InputGroupButton
|
<InputGroupButton
|
||||||
variant='secondary'
|
className="cursor-pointer"
|
||||||
className='cursor-pointer'
|
|
||||||
onClick={() => onSearchChange(searchValue)}
|
onClick={() => onSearchChange(searchValue)}
|
||||||
|
variant="secondary"
|
||||||
>
|
>
|
||||||
{t("common.search", "Search")}
|
{t("components.simple_search_input.search_button", "Search")}
|
||||||
</InputGroupButton>
|
</InputGroupButton>
|
||||||
)}
|
)}
|
||||||
{searchValue && !loading && (
|
{searchValue && !loading && (
|
||||||
<InputGroupButton
|
<InputGroupButton
|
||||||
variant='secondary'
|
aria-label={t("components.simple_search_input.clear_search", "Clear search")}
|
||||||
className='cursor-pointer'
|
className="cursor-pointer"
|
||||||
aria-label={t("common.clear", "Clear search")}
|
|
||||||
onClick={handleClear}
|
onClick={handleClear}
|
||||||
|
variant="secondary"
|
||||||
>
|
>
|
||||||
<XIcon className='size-4' aria-hidden />
|
<XIcon aria-hidden className="size-4" />
|
||||||
<span className='sr-only'>{t("common.clear", "Clear")}</span>
|
<span className="sr-only">
|
||||||
|
{t("components.simple_search_input.clear_search", "Clear")}
|
||||||
|
</span>
|
||||||
</InputGroupButton>
|
</InputGroupButton>
|
||||||
)}
|
)}
|
||||||
</InputGroupAddon>
|
</InputGroupAddon>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ResponseType } from "axios";
|
import type { ResponseType } from "axios";
|
||||||
|
|
||||||
export interface ICustomParams {
|
export interface ICustomParams {
|
||||||
url?: string;
|
url?: string;
|
||||||
@ -7,7 +7,7 @@ export interface ICustomParams {
|
|||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
responseType?: ResponseType;
|
responseType?: ResponseType;
|
||||||
headers?: {
|
headers?: {
|
||||||
[key: string]: any;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -175,6 +175,26 @@ 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 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.
|
||||||
|
*/
|
||||||
|
async findProformasByCriteriaInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
|
criteria: Criteria,
|
||||||
|
transaction?: Transaction
|
||||||
|
): Promise<Result<Collection<CustomerInvoiceListDTO>, Error>> {
|
||||||
|
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction, {
|
||||||
|
where: {
|
||||||
|
is_proforma: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtiene una colección de facturas que cumplen con los filtros definidos en un objeto Criteria.
|
* Obtiene una colección de facturas que cumplen con los filtros definidos en un objeto Criteria.
|
||||||
*
|
*
|
||||||
@ -183,12 +203,16 @@ export class CustomerInvoiceApplicationService {
|
|||||||
* @param transaction - Transacción activa para la operación.
|
* @param transaction - Transacción activa para la operación.
|
||||||
* @returns Result<Collection<CustomerInvoiceListDTO>, Error> - Colección de facturas o error.
|
* @returns Result<Collection<CustomerInvoiceListDTO>, Error> - Colección de facturas o error.
|
||||||
*/
|
*/
|
||||||
async findInvoiceByCriteriaInCompany(
|
async findIssueInvoiceByCriteriaInCompany(
|
||||||
companyId: UniqueID,
|
companyId: UniqueID,
|
||||||
criteria: Criteria,
|
criteria: Criteria,
|
||||||
transaction?: Transaction
|
transaction?: Transaction
|
||||||
): Promise<Result<Collection<CustomerInvoiceListDTO>, Error>> {
|
): Promise<Result<Collection<CustomerInvoiceListDTO>, Error>> {
|
||||||
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction);
|
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction, {
|
||||||
|
where: {
|
||||||
|
is_proforma: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export class ListProformasUseCase {
|
|||||||
|
|
||||||
return this.transactionManager.complete(async (transaction: Transaction) => {
|
return this.transactionManager.complete(async (transaction: Transaction) => {
|
||||||
try {
|
try {
|
||||||
const result = await this.service.findInvoiceByCriteriaInCompany(
|
const result = await this.service.findProformasByCriteriaInCompany(
|
||||||
companyId,
|
companyId,
|
||||||
criteria,
|
criteria,
|
||||||
transaction
|
transaction
|
||||||
|
|||||||
@ -41,7 +41,7 @@ export interface ICustomerInvoiceRepository {
|
|||||||
): Promise<Result<boolean, Error>>;
|
): Promise<Result<boolean, Error>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recupera una factura por su ID y companyId.
|
* Recupera una factura/proforma por su ID y companyId.
|
||||||
* Devuelve un `NotFoundError` si no se encuentra.
|
* Devuelve un `NotFoundError` si no se encuentra.
|
||||||
*/
|
*/
|
||||||
getByIdInCompany(
|
getByIdInCompany(
|
||||||
@ -53,13 +53,14 @@ export interface ICustomerInvoiceRepository {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Consulta facturas dentro de una empresa usando un
|
* Consulta facturas/proformas dentro de una empresa usando un
|
||||||
* objeto Criteria (filtros, orden, paginación).
|
* objeto Criteria (filtros, orden, paginación).
|
||||||
* El resultado está encapsulado en un objeto `Collection<T>`.
|
* El resultado está encapsulado en un objeto `Collection<T>`.
|
||||||
*
|
*
|
||||||
* @param companyId - ID de la empresa.
|
* @param companyId - ID de la empresa.
|
||||||
* @param criteria - Criterios de búsqueda.
|
* @param criteria - Criterios de búsqueda.
|
||||||
* @param transaction - Transacción activa para la operación.
|
* @param transaction - Transacción activa para la operación.
|
||||||
|
* @param options - Opciones adicionales para la consulta (Sequelize FindOptions)
|
||||||
* @returns Result<Collection<CustomerInvoiceListDTO>, Error>
|
* @returns Result<Collection<CustomerInvoiceListDTO>, Error>
|
||||||
*
|
*
|
||||||
* @see Criteria
|
* @see Criteria
|
||||||
@ -67,7 +68,8 @@ export interface ICustomerInvoiceRepository {
|
|||||||
findByCriteriaInCompany(
|
findByCriteriaInCompany(
|
||||||
companyId: UniqueID,
|
companyId: UniqueID,
|
||||||
criteria: Criteria,
|
criteria: Criteria,
|
||||||
transaction: unknown
|
transaction: unknown,
|
||||||
|
options: unknown
|
||||||
): Promise<Result<Collection<CustomerInvoiceListDTO>, Error>>;
|
): Promise<Result<Collection<CustomerInvoiceListDTO>, Error>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -51,7 +51,7 @@ export const issueInvoicesRouter = (params: ModuleParams) => {
|
|||||||
//checkTabContext,
|
//checkTabContext,
|
||||||
validateRequest(ListIssueInvoicesRequestSchema, "params"),
|
validateRequest(ListIssueInvoicesRequestSchema, "params"),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const useCase = deps.useCases.list();
|
const useCase = deps.useCases.list_proformas();
|
||||||
const controller = new ListProformasController(useCase /*, deps.presenters.list */);
|
const controller = new ListProformasController(useCase /*, deps.presenters.list */);
|
||||||
return controller.execute(req, res, next);
|
return controller.execute(req, res, next);
|
||||||
}
|
}
|
||||||
@ -62,7 +62,7 @@ export const issueInvoicesRouter = (params: ModuleParams) => {
|
|||||||
//checkTabContext,
|
//checkTabContext,
|
||||||
validateRequest(GetIssueInvoiceByIdRequestSchema, "params"),
|
validateRequest(GetIssueInvoiceByIdRequestSchema, "params"),
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const useCase = deps.useCases.get();
|
const useCase = deps.useCases.get_proforma();
|
||||||
const controller = new GetProformaController(useCase);
|
const controller = new GetProformaController(useCase);
|
||||||
return controller.execute(req, res, next);
|
return controller.execute(req, res, next);
|
||||||
}
|
}
|
||||||
@ -73,7 +73,7 @@ export const issueInvoicesRouter = (params: ModuleParams) => {
|
|||||||
//checkTabContext,
|
//checkTabContext,
|
||||||
validateRequest(ReportIssueInvoiceByIdRequestSchema, "params"),
|
validateRequest(ReportIssueInvoiceByIdRequestSchema, "params"),
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const useCase = deps.useCases.report();
|
const useCase = deps.useCases.report_proforma();
|
||||||
const controller = new ReportProformaController(useCase);
|
const controller = new ReportProformaController(useCase);
|
||||||
return controller.execute(req, res, next);
|
return controller.execute(req, res, next);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,25 +1,30 @@
|
|||||||
import { ISequelizeQueryMapper, MapperParamsType, SequelizeQueryMapper } from "@erp/core/api";
|
import {
|
||||||
|
type ISequelizeQueryMapper,
|
||||||
|
type MapperParamsType,
|
||||||
|
SequelizeQueryMapper,
|
||||||
|
} from "@erp/core/api";
|
||||||
import {
|
import {
|
||||||
CurrencyCode,
|
CurrencyCode,
|
||||||
extractOrPushError,
|
|
||||||
LanguageCode,
|
LanguageCode,
|
||||||
maybeFromNullableVO,
|
|
||||||
Percentage,
|
Percentage,
|
||||||
UniqueID,
|
UniqueID,
|
||||||
UtcDate,
|
UtcDate,
|
||||||
ValidationErrorCollection,
|
ValidationErrorCollection,
|
||||||
ValidationErrorDetail,
|
type ValidationErrorDetail,
|
||||||
|
extractOrPushError,
|
||||||
|
maybeFromNullableVO,
|
||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
|
import { type Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import { Maybe, Result } from "@repo/rdx-utils";
|
|
||||||
import {
|
import {
|
||||||
CustomerInvoiceNumber,
|
CustomerInvoiceNumber,
|
||||||
CustomerInvoiceSerie,
|
CustomerInvoiceSerie,
|
||||||
CustomerInvoiceStatus,
|
CustomerInvoiceStatus,
|
||||||
InvoiceAmount,
|
InvoiceAmount,
|
||||||
InvoiceRecipient,
|
type InvoiceRecipient,
|
||||||
} from "../../../domain";
|
} from "../../../domain";
|
||||||
import { CustomerInvoiceModel } from "../../sequelize";
|
import type { CustomerInvoiceModel } from "../../sequelize";
|
||||||
|
|
||||||
import { InvoiceRecipientListMapper } from "./invoice-recipient.list.mapper";
|
import { InvoiceRecipientListMapper } from "./invoice-recipient.list.mapper";
|
||||||
|
|
||||||
export type CustomerInvoiceListDTO = {
|
export type CustomerInvoiceListDTO = {
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {
|
|||||||
import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
|
import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
|
||||||
import type { UniqueID } from "@repo/rdx-ddd";
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { type Collection, Result } from "@repo/rdx-utils";
|
import { type Collection, Result } from "@repo/rdx-utils";
|
||||||
import type { FindOptions, InferAttributes, Transaction } from "sequelize";
|
import type { FindOptions, InferAttributes, OrderItem, Transaction } from "sequelize";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
CustomerInvoice,
|
CustomerInvoice,
|
||||||
@ -293,7 +293,8 @@ export class CustomerInvoiceRepository
|
|||||||
public async findByCriteriaInCompany(
|
public async findByCriteriaInCompany(
|
||||||
companyId: UniqueID,
|
companyId: UniqueID,
|
||||||
criteria: Criteria,
|
criteria: Criteria,
|
||||||
transaction: Transaction
|
transaction: Transaction,
|
||||||
|
options: FindOptions<InferAttributes<CustomerInvoiceModel>> = {}
|
||||||
): Promise<Result<Collection<CustomerInvoiceListDTO>, Error>> {
|
): Promise<Result<Collection<CustomerInvoiceListDTO>, Error>> {
|
||||||
const { CustomerModel } = this._database.models;
|
const { CustomerModel } = this._database.models;
|
||||||
|
|
||||||
@ -315,13 +316,30 @@ export class CustomerInvoiceRepository
|
|||||||
strictMode: true, // fuerza error si ORDER BY no permitido
|
strictMode: true, // fuerza error si ORDER BY no permitido
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Normalización defensiva de order/include
|
||||||
|
const normalizedOrder = Array.isArray(options.order)
|
||||||
|
? options.order
|
||||||
|
: options.order
|
||||||
|
? [options.order]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const normalizedInclude = Array.isArray(options.include)
|
||||||
|
? options.include
|
||||||
|
: options.include
|
||||||
|
? [options.include]
|
||||||
|
: [];
|
||||||
|
|
||||||
query.where = {
|
query.where = {
|
||||||
...query.where,
|
...query.where,
|
||||||
company_id: companyId.toString(),
|
company_id: companyId.toString(),
|
||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
|
...(options.where ?? {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
query.order = [...(query.order as OrderItem[]), ...normalizedOrder];
|
||||||
|
|
||||||
query.include = [
|
query.include = [
|
||||||
|
...normalizedInclude,
|
||||||
{
|
{
|
||||||
model: CustomerModel,
|
model: CustomerModel,
|
||||||
as: "current_customer",
|
as: "current_customer",
|
||||||
@ -338,7 +356,6 @@ export class CustomerInvoiceRepository
|
|||||||
"country",
|
"country",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
model: CustomerInvoiceTaxModel,
|
model: CustomerInvoiceTaxModel,
|
||||||
as: "taxes",
|
as: "taxes",
|
||||||
|
|||||||
@ -31,61 +31,63 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
"title": "Customer invoices",
|
"proformas": {
|
||||||
"description": "Manage your customer invoices",
|
"title": "Proformas",
|
||||||
"list": {
|
"description": "Manage your customer proformas",
|
||||||
"title": "Customer invoices",
|
"list": {
|
||||||
"description": "List all customer invoices",
|
"title": "Customer proformas",
|
||||||
"grid_columns": {
|
"description": "List all customer proformas",
|
||||||
"invoice_number": "Inv. number",
|
"grid_columns": {
|
||||||
"series": "Serie",
|
"invoice_number": "Inv. number",
|
||||||
"status": "Status",
|
"series": "Serie",
|
||||||
"invoice_date": "Invoice date",
|
"status": "Status",
|
||||||
"operation_date": "Operation date",
|
"invoice_date": "Proforma date",
|
||||||
"recipient_tin": "TIN",
|
"operation_date": "Operation date",
|
||||||
"recipient_name": "Customer name",
|
"recipient_tin": "TIN",
|
||||||
"recipient_street": "Street",
|
"recipient_name": "Customer name",
|
||||||
"recipient_city": "City",
|
"recipient_street": "Street",
|
||||||
"recipient_province": "Province",
|
"recipient_city": "City",
|
||||||
"recipient_postal_code": "Postal code",
|
"recipient_province": "Province",
|
||||||
"recipient_country": "Country",
|
"recipient_postal_code": "Postal code",
|
||||||
"total_amount": "Total price"
|
"recipient_country": "Country",
|
||||||
|
"total_amount": "Total price"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"create": {
|
||||||
|
"title": "New customer proforma",
|
||||||
|
"description": "Create a new customer proforma",
|
||||||
|
"back_to_list": "Back to the list"
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"title": "Edit customer proforma",
|
||||||
|
"description": "Edit the selected customer proforma"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"title": "Delete customer proforma",
|
||||||
|
"description": "Delete the selected customer proforma"
|
||||||
|
},
|
||||||
|
"view": {
|
||||||
|
"title": "View customer proforma",
|
||||||
|
"description": "View the details of the selected customer proforma"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"create": {
|
|
||||||
"title": "New customer invoice",
|
|
||||||
"description": "Create a new customer invoice",
|
|
||||||
"back_to_list": "Back to the list"
|
|
||||||
},
|
|
||||||
"edit": {
|
|
||||||
"title": "Edit customer invoice",
|
|
||||||
"description": "Edit the selected customer invoice"
|
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"title": "Delete customer invoice",
|
|
||||||
"description": "Delete the selected customer invoice"
|
|
||||||
},
|
|
||||||
"view": {
|
|
||||||
"title": "View customer invoice",
|
|
||||||
"description": "View the details of the selected customer invoice"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"form_groups": {
|
"form_groups": {
|
||||||
"customer": {
|
"customer": {
|
||||||
"title": "Customer",
|
"title": "Customer",
|
||||||
"description": "Select the customer for this invoice."
|
"description": "Select the customer for this proforma."
|
||||||
},
|
},
|
||||||
"items": {
|
"items": {
|
||||||
"title": "Invoice details",
|
"title": "Proforma details",
|
||||||
"description": ""
|
"description": ""
|
||||||
},
|
},
|
||||||
"basic_info": {
|
"basic_info": {
|
||||||
"title": "Invoice information",
|
"title": "Proforma information",
|
||||||
"description": "Basic invoice information"
|
"description": "Basic proforma information"
|
||||||
},
|
},
|
||||||
"totals": {
|
"totals": {
|
||||||
"title": "Invoice totals",
|
"title": "Proforma totals",
|
||||||
"description": "Breakdown of invoice amounts with discounts and taxes."
|
"description": "Breakdown of proforma amounts with discounts and taxes."
|
||||||
},
|
},
|
||||||
"tax_resume": {
|
"tax_resume": {
|
||||||
"title": "Resumen de impuestos",
|
"title": "Resumen de impuestos",
|
||||||
@ -93,7 +95,7 @@
|
|||||||
},
|
},
|
||||||
"preferences": {
|
"preferences": {
|
||||||
"title": "Preferences",
|
"title": "Preferences",
|
||||||
"description": "Additional invoice settings"
|
"description": "Additional proforma settings"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"form_fields": {
|
"form_fields": {
|
||||||
@ -103,39 +105,39 @@
|
|||||||
"description": ""
|
"description": ""
|
||||||
},
|
},
|
||||||
"invoice_number": {
|
"invoice_number": {
|
||||||
"label": "Invoice number",
|
"label": "Proforma number",
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"description": ""
|
"description": ""
|
||||||
},
|
},
|
||||||
"invoice_date": {
|
"invoice_date": {
|
||||||
"label": "Invoice date",
|
"label": "Proforma date",
|
||||||
"placeholder": "Select a date",
|
"placeholder": "Select a date",
|
||||||
"description": "Invoice date"
|
"description": "Proforma date"
|
||||||
},
|
},
|
||||||
"series": {
|
"series": {
|
||||||
"label": "Serie",
|
"label": "Serie",
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"description": "Invoice serie"
|
"description": "Proforma serie"
|
||||||
},
|
},
|
||||||
"operation_date": {
|
"operation_date": {
|
||||||
"label": "Operation date",
|
"label": "Operation date",
|
||||||
"placeholder": "Select a date",
|
"placeholder": "Select a date",
|
||||||
"description": "Invoice operation date"
|
"description": "Proforma operation date"
|
||||||
},
|
},
|
||||||
"reference": {
|
"reference": {
|
||||||
"label": "Reference",
|
"label": "Reference",
|
||||||
"placeholder": "Reference of the invoice",
|
"placeholder": "Reference of the proforma",
|
||||||
"description": "Reference of the invoice"
|
"description": "Reference of the proforma"
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"label": "Description",
|
"label": "Description",
|
||||||
"placeholder": "Description of the invoice",
|
"placeholder": "Description of the proforma",
|
||||||
"description": "General description of the invoice"
|
"description": "General description of the proforma"
|
||||||
},
|
},
|
||||||
"subtotal_amount": {
|
"subtotal_amount": {
|
||||||
"label": "Subtotal",
|
"label": "Subtotal",
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"desc": "Invoice subtotal"
|
"desc": "Proforma subtotal"
|
||||||
},
|
},
|
||||||
"discount": {
|
"discount": {
|
||||||
"label": "Discount (%)",
|
"label": "Discount (%)",
|
||||||
@ -150,12 +152,12 @@
|
|||||||
"total_amount": {
|
"total_amount": {
|
||||||
"label": "Total price",
|
"label": "Total price",
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"desc": "Invoice total price"
|
"desc": "Proforma total price"
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"label": "Notes",
|
"label": "Notes",
|
||||||
"placeholder": "Additional notes about the invoice",
|
"placeholder": "Additional notes about the proforma",
|
||||||
"description": "Additional notes that can be included in the invoice"
|
"description": "Additional notes that can be included in the proforma"
|
||||||
},
|
},
|
||||||
"item": {
|
"item": {
|
||||||
"quantity": {
|
"quantity": {
|
||||||
@ -201,7 +203,7 @@
|
|||||||
"total_amount": {
|
"total_amount": {
|
||||||
"label": "Total amount",
|
"label": "Total amount",
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"description": "Invoice line total"
|
"description": "Proforma line total"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -212,7 +214,7 @@
|
|||||||
"customer_invoice_taxes_multi_select": {
|
"customer_invoice_taxes_multi_select": {
|
||||||
"label": "Taxes",
|
"label": "Taxes",
|
||||||
"placeholder": "Select taxes",
|
"placeholder": "Select taxes",
|
||||||
"description": "Select the taxes to apply to the invoice items",
|
"description": "Select the taxes to apply to the proforma items",
|
||||||
"invalid_tax_selection": "Invalid tax selection. Please select a valid tax."
|
"invalid_tax_selection": "Invalid tax selection. Please select a valid tax."
|
||||||
},
|
},
|
||||||
"hover_card_totals_summary": {
|
"hover_card_totals_summary": {
|
||||||
|
|||||||
@ -30,78 +30,80 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
"title": "Facturas de clientes",
|
"proformas": {
|
||||||
"description": "Gestiona tus facturas de clientes",
|
"title": "Proformas",
|
||||||
"list": {
|
"description": "Gestiona tus proformas",
|
||||||
"title": "Facturas de clientes",
|
"list": {
|
||||||
"description": "Lista todas las facturas de clientes",
|
"title": "Proformas",
|
||||||
"grid_columns": {
|
"description": "Lista todas las proformas",
|
||||||
"invoice_number": "Nº factura",
|
"grid_columns": {
|
||||||
"series": "Serie",
|
"invoice_number": "Nº proforma",
|
||||||
"status": "Estado",
|
"series": "Serie",
|
||||||
"invoice_date": "Fecha de factura",
|
"status": "Estado",
|
||||||
"operation_date": "Fecha de operación",
|
"invoice_date": "Fecha de proforma",
|
||||||
"recipient_tin": "NIF/CIF",
|
"operation_date": "Fecha de operación",
|
||||||
"recipient_name": "Cliente",
|
"recipient_tin": "NIF/CIF",
|
||||||
"recipient_street": "Dirección",
|
"recipient_name": "Cliente",
|
||||||
"recipient_city": "Ciudad",
|
"recipient_street": "Dirección",
|
||||||
"recipient_province": "Provincia",
|
"recipient_city": "Ciudad",
|
||||||
"recipient_postal_code": "Código postal",
|
"recipient_province": "Provincia",
|
||||||
"recipient_country": "País",
|
"recipient_postal_code": "Código postal",
|
||||||
"total_amount": "Importe total"
|
"recipient_country": "País",
|
||||||
|
"total_amount": "Importe total"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"create": {
|
||||||
|
"title": "Nueva proforma",
|
||||||
|
"description": "Crear una nueva proforma",
|
||||||
|
"back_to_list": "Volver al listado"
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"title": "Editar proforma",
|
||||||
|
"description": "Editar la proforma seleccionada"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"title": "Eliminar proforma",
|
||||||
|
"description": "Eliminar la proforma seleccionada"
|
||||||
|
},
|
||||||
|
"view": {
|
||||||
|
"title": "Ver proforma",
|
||||||
|
"description": "Ver los detalles de la proforma seleccionada"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"create": {
|
|
||||||
"title": "Nueva factura de cliente",
|
|
||||||
"description": "Crear una nueva factura de cliente",
|
|
||||||
"back_to_list": "Volver al listado"
|
|
||||||
},
|
|
||||||
"edit": {
|
|
||||||
"title": "Editar factura de cliente",
|
|
||||||
"description": "Editar la factura de cliente seleccionada"
|
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"title": "Eliminar factura de cliente",
|
|
||||||
"description": "Eliminar la factura de cliente seleccionada"
|
|
||||||
},
|
|
||||||
"view": {
|
|
||||||
"title": "Ver factura de cliente",
|
|
||||||
"description": "Ver los detalles de la factura de cliente seleccionada"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"form_groups": {
|
"form_groups": {
|
||||||
"customer": {
|
"customer": {
|
||||||
"title": "Cliente",
|
"title": "Cliente",
|
||||||
"description": "Selecciona el cliente para esta factura"
|
"description": "Selecciona el cliente para esta proforma"
|
||||||
},
|
},
|
||||||
"items": {
|
"items": {
|
||||||
"title": "Detalles de la factura",
|
"title": "Detalles de la proforma",
|
||||||
"description": ""
|
"description": ""
|
||||||
},
|
},
|
||||||
"basic_info": {
|
"basic_info": {
|
||||||
"title": "Información de la factura",
|
"title": "Información de la proforma",
|
||||||
"description": "Información básica de la factura"
|
"description": "Información básica de la proforma"
|
||||||
},
|
},
|
||||||
"totals": {
|
"totals": {
|
||||||
"title": "Totales de la factura",
|
"title": "Totales de la proforma",
|
||||||
"description": "Desglose de los importes de la factura con descuentos e impuestos."
|
"description": "Desglose de los importes de la proforma con descuentos e impuestos."
|
||||||
},
|
},
|
||||||
"preferences": {
|
"preferences": {
|
||||||
"title": "Preferencias",
|
"title": "Preferencias",
|
||||||
"description": "Configuraciones adicionales de la factura"
|
"description": "Configuraciones adicionales de la proforma"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"form_fields": {
|
"form_fields": {
|
||||||
"invoice_number": {
|
"invoice_number": {
|
||||||
"label": "Número de factura",
|
"label": "Número de proforma",
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"description": ""
|
"description": ""
|
||||||
},
|
},
|
||||||
"invoice_date": {
|
"invoice_date": {
|
||||||
"label": "Fecha",
|
"label": "Fecha",
|
||||||
"placeholder": "Selecciona una fecha",
|
"placeholder": "Selecciona una fecha",
|
||||||
"description": "Fecha de emisión de la factura"
|
"description": "Fecha de emisión de la proforma"
|
||||||
},
|
},
|
||||||
"series": {
|
"series": {
|
||||||
"label": "Serie",
|
"label": "Serie",
|
||||||
@ -111,23 +113,23 @@
|
|||||||
"operation_date": {
|
"operation_date": {
|
||||||
"label": "Fecha de operación",
|
"label": "Fecha de operación",
|
||||||
"placeholder": "Selecciona una fecha",
|
"placeholder": "Selecciona una fecha",
|
||||||
"description": "Fecha de la operación de la factura"
|
"description": "Fecha de la operación de la proforma"
|
||||||
},
|
},
|
||||||
"reference": {
|
"reference": {
|
||||||
"label": "Referencia",
|
"label": "Referencia",
|
||||||
"placeholder": "Referencia de la factura",
|
"placeholder": "Referencia de la proforma",
|
||||||
"description": "Referencia de la factura"
|
"description": "Referencia de la proforma"
|
||||||
},
|
},
|
||||||
|
|
||||||
"description": {
|
"description": {
|
||||||
"label": "Descripción",
|
"label": "Descripción",
|
||||||
"placeholder": "Descripción de la factura",
|
"placeholder": "Descripción de la proforma",
|
||||||
"description": "Descripción general de la factura"
|
"description": "Descripción general de la proforma"
|
||||||
},
|
},
|
||||||
"subtotal_amount": {
|
"subtotal_amount": {
|
||||||
"label": "Subtotal",
|
"label": "Subtotal",
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"desc": "Subtotal de la factura"
|
"desc": "Subtotal de la proforma"
|
||||||
},
|
},
|
||||||
"discount": {
|
"discount": {
|
||||||
"label": "Descuento (%)",
|
"label": "Descuento (%)",
|
||||||
@ -142,12 +144,12 @@
|
|||||||
"total_amount": {
|
"total_amount": {
|
||||||
"label": "Precio total",
|
"label": "Precio total",
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"desc": "Precio total de la factura"
|
"desc": "Precio total de la proforma"
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"label": "Notas",
|
"label": "Notas",
|
||||||
"placeholder": "Notas adicionales sobre la factura",
|
"placeholder": "Notas adicionales sobre la proforma",
|
||||||
"description": "Notas adicionales que se pueden incluir en la factura"
|
"description": "Notas adicionales que se pueden incluir en la proforma"
|
||||||
},
|
},
|
||||||
"item": {
|
"item": {
|
||||||
"quantity": {
|
"quantity": {
|
||||||
@ -204,7 +206,7 @@
|
|||||||
"customer_invoice_taxes_multi_select": {
|
"customer_invoice_taxes_multi_select": {
|
||||||
"label": "Impuestos",
|
"label": "Impuestos",
|
||||||
"placeholder": "Selecciona impuestos",
|
"placeholder": "Selecciona impuestos",
|
||||||
"description": "Selecciona los impuestos a aplicar a los artículos de la factura",
|
"description": "Selecciona los impuestos a aplicar a los artículos de la proforma",
|
||||||
"invalid_tax_selection": "Selección de impuestos no válida. Por favor, selecciona un impuesto válido."
|
"invalid_tax_selection": "Selección de impuestos no válida. Por favor, selecciona un impuesto válido."
|
||||||
},
|
},
|
||||||
"hover_card_totals_summary": {
|
"hover_card_totals_summary": {
|
||||||
|
|||||||
@ -8,7 +8,7 @@ const InvoicesLayout = lazy(() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const ProformaListPage = lazy(() =>
|
const ProformaListPage = lazy(() =>
|
||||||
import("./pages").then((m) => ({ default: m.InvoiceListPage }))
|
import("./pages").then((m) => ({ default: m.ProformaListPage }))
|
||||||
);
|
);
|
||||||
|
|
||||||
const CustomerInvoiceAdd = lazy(() =>
|
const CustomerInvoiceAdd = lazy(() =>
|
||||||
@ -34,7 +34,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
|||||||
{ path: ":id/edit", element: <InvoiceUpdatePage /> },
|
{ path: ":id/edit", element: <InvoiceUpdatePage /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
/*{
|
||||||
path: "customer-invoices",
|
path: "customer-invoices",
|
||||||
element: (
|
element: (
|
||||||
<InvoicesLayout>
|
<InvoicesLayout>
|
||||||
@ -45,7 +45,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
|||||||
//{ path: "", index: true, element: <InvoiceListPage /> }, // index
|
//{ path: "", index: true, element: <InvoiceListPage /> }, // index
|
||||||
//{ path: "list", element: <InvoiceListPage /> },
|
//{ path: "list", element: <InvoiceListPage /> },
|
||||||
//
|
//
|
||||||
/*{ path: "create", element: <CustomerInvoicesList /> },
|
{ path: "create", element: <CustomerInvoicesList /> },
|
||||||
{ path: ":id", element: <CustomerInvoicesList /> },
|
{ path: ":id", element: <CustomerInvoicesList /> },
|
||||||
{ path: ":id/edit", element: <CustomerInvoicesList /> },
|
{ path: ":id/edit", element: <CustomerInvoicesList /> },
|
||||||
{ path: ":id/delete", element: <CustomerInvoicesList /> },
|
{ path: ":id/delete", element: <CustomerInvoicesList /> },
|
||||||
@ -54,8 +54,8 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
|||||||
{ path: ":id/email", element: <CustomerInvoicesList /> },
|
{ path: ":id/email", element: <CustomerInvoicesList /> },
|
||||||
{ path: ":id/download", element: <CustomerInvoicesList /> },
|
{ path: ":id/download", element: <CustomerInvoicesList /> },
|
||||||
{ path: ":id/duplicate", element: <CustomerInvoicesList /> },
|
{ path: ":id/duplicate", element: <CustomerInvoicesList /> },
|
||||||
{ path: ":id/preview", element: <CustomerInvoicesList /> },*/
|
{ path: ":id/preview", element: <CustomerInvoicesList /> },
|
||||||
],
|
],
|
||||||
},
|
},*/
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,19 +1,21 @@
|
|||||||
import { useDataSource } from "@erp/core/hooks";
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
import { UniqueID, ValidationErrorCollection } from "@repo/rdx-ddd";
|
import { UniqueID, ValidationErrorCollection } from "@repo/rdx-ddd";
|
||||||
import { DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { CreateCustomerInvoiceRequestSchema } from "../../common";
|
|
||||||
import { CustomerInvoice, InvoiceFormData } from "../schemas";
|
import { CreateProformaRequestSchema } from "../../common";
|
||||||
|
import type { Proforma } from "../proformas/proforma.api.schema";
|
||||||
|
import type { InvoiceFormData } from "../schemas";
|
||||||
|
|
||||||
type CreateCustomerInvoicePayload = {
|
type CreateCustomerInvoicePayload = {
|
||||||
data: InvoiceFormData;
|
data: InvoiceFormData;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCreateCustomerInvoiceMutation = () => {
|
export const useCreateProforma = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const dataSource = useDataSource();
|
const dataSource = useDataSource();
|
||||||
const schema = CreateCustomerInvoiceRequestSchema;
|
const schema = CreateProformaRequestSchema;
|
||||||
|
|
||||||
return useMutation<CustomerInvoice, DefaultError, CreateCustomerInvoicePayload>({
|
return useMutation<Proforma, DefaultError, CreateCustomerInvoicePayload>({
|
||||||
mutationKey: ["customer-invoice:create"],
|
mutationKey: ["customer-invoice:create"],
|
||||||
|
|
||||||
mutationFn: async (payload) => {
|
mutationFn: async (payload) => {
|
||||||
@ -37,7 +39,7 @@ export const useCreateCustomerInvoiceMutation = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const created = await dataSource.createOne("customer-invoices", newInvoiceData);
|
const created = await dataSource.createOne("customer-invoices", newInvoiceData);
|
||||||
return created as CustomerInvoice;
|
return created;
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["customer-invoices"] });
|
queryClient.invalidateQueries({ queryKey: ["customer-invoices"] });
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useDataSource } from "@erp/core/hooks";
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
import { DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
|
import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
|
||||||
import { CustomerInvoice } from "../schemas";
|
|
||||||
|
import type { Proforma } from "../schemas";
|
||||||
|
|
||||||
export const CUSTOMER_INVOICE_QUERY_KEY = (id: string): QueryKey =>
|
export const CUSTOMER_INVOICE_QUERY_KEY = (id: string): QueryKey =>
|
||||||
["customer_invoice", id] as const;
|
["customer_invoice", id] as const;
|
||||||
@ -13,14 +14,14 @@ export const useInvoiceQuery = (invoiceId?: string, options?: InvoiceQueryOption
|
|||||||
const dataSource = useDataSource();
|
const dataSource = useDataSource();
|
||||||
const enabled = (options?.enabled ?? true) && !!invoiceId;
|
const enabled = (options?.enabled ?? true) && !!invoiceId;
|
||||||
|
|
||||||
return useQuery<CustomerInvoice, DefaultError>({
|
return useQuery<Proforma, DefaultError>({
|
||||||
queryKey: CUSTOMER_INVOICE_QUERY_KEY(invoiceId ?? "unknown"),
|
queryKey: CUSTOMER_INVOICE_QUERY_KEY(invoiceId ?? "unknown"),
|
||||||
queryFn: async (context) => {
|
queryFn: async (context) => {
|
||||||
const { signal } = context;
|
const { signal } = context;
|
||||||
if (!invoiceId) {
|
if (!invoiceId) {
|
||||||
if (!invoiceId) throw new Error("invoiceId is required");
|
if (!invoiceId) throw new Error("invoiceId is required");
|
||||||
}
|
}
|
||||||
return await dataSource.getOne<CustomerInvoice>("customer-invoices", invoiceId, {
|
return await dataSource.getOne<Proforma>("customer-invoices", invoiceId, {
|
||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
import { useDataSource } from "@erp/core/hooks";
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
import { ValidationErrorCollection } from "@repo/rdx-ddd";
|
import { ValidationErrorCollection } from "@repo/rdx-ddd";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
|
||||||
UpdateCustomerInvoiceByIdRequestDTO,
|
import { type UpdateProformaByIdRequestDTO, UpdateProformaByIdRequestSchema } from "../../common";
|
||||||
UpdateCustomerInvoiceByIdRequestSchema,
|
import type { InvoiceFormData } from "../schemas";
|
||||||
} from "../../common";
|
|
||||||
import { InvoiceFormData } from "../schemas";
|
|
||||||
import { CUSTOMER_INVOICE_QUERY_KEY } from "./use-invoice-query";
|
import { CUSTOMER_INVOICE_QUERY_KEY } from "./use-invoice-query";
|
||||||
|
|
||||||
export const CUSTOMER_INVOICES_LIST_KEY = ["customer-invoices"] as const;
|
export const CUSTOMER_INVOICES_LIST_KEY = ["customer-invoices"] as const;
|
||||||
@ -17,10 +16,10 @@ type UpdateCustomerInvoicePayload = {
|
|||||||
data: Partial<InvoiceFormData>;
|
data: Partial<InvoiceFormData>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useUpdateCustomerInvoice() {
|
export function useUpdateProforma() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const dataSource = useDataSource();
|
const dataSource = useDataSource();
|
||||||
const schema = UpdateCustomerInvoiceByIdRequestSchema;
|
const schema = UpdateProformaByIdRequestSchema;
|
||||||
|
|
||||||
return useMutation<
|
return useMutation<
|
||||||
InvoiceFormData,
|
InvoiceFormData,
|
||||||
@ -59,7 +58,7 @@ export function useUpdateCustomerInvoice() {
|
|||||||
const { id: invoiceId } = variables;
|
const { id: invoiceId } = variables;
|
||||||
|
|
||||||
// Refresca inmediatamente el detalle
|
// Refresca inmediatamente el detalle
|
||||||
queryClient.setQueryData<UpdateCustomerInvoiceByIdRequestDTO>(
|
queryClient.setQueryData<UpdateProformaByIdRequestDTO>(
|
||||||
CUSTOMER_INVOICE_QUERY_KEY(invoiceId),
|
CUSTOMER_INVOICE_QUERY_KEY(invoiceId),
|
||||||
updated
|
updated
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./use-issue-invoice-query";
|
||||||
|
export * from "./use-issue-invoices-query";
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
|
import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import type { IssueInvoice } from "../issue-invoice.api.schema";
|
||||||
|
|
||||||
|
export const ISSUE_INVOICE_QUERY_KEY = (id: string): QueryKey => ["issue-invoice", id] as const;
|
||||||
|
|
||||||
|
type InvoiceQueryOptions = {
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useIssueInvoiceQuery = (issueInvoiceId?: string, options?: InvoiceQueryOptions) => {
|
||||||
|
const dataSource = useDataSource();
|
||||||
|
const enabled = (options?.enabled ?? true) && !!issueInvoiceId;
|
||||||
|
|
||||||
|
return useQuery<IssueInvoice, DefaultError>({
|
||||||
|
queryKey: ISSUE_INVOICE_QUERY_KEY(issueInvoiceId ?? "unknown"),
|
||||||
|
queryFn: async (context) => {
|
||||||
|
const { signal } = context;
|
||||||
|
if (!issueInvoiceId) {
|
||||||
|
if (!issueInvoiceId) throw new Error("issueInvoiceId is required");
|
||||||
|
}
|
||||||
|
return await dataSource.getOne<IssueInvoice>("issue-invoices", issueInvoiceId, {
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
enabled,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
export function useQuery<
|
||||||
|
TQueryFnData = unknown,
|
||||||
|
TError = unknown,
|
||||||
|
TData = TQueryFnData,
|
||||||
|
TQueryKey extends QueryKey = QueryKey
|
||||||
|
>
|
||||||
|
|
||||||
|
TQueryFnData: the type returned from the queryFn.
|
||||||
|
TError: the type of Errors to expect from the queryFn.
|
||||||
|
TData: the type our data property will eventually have.
|
||||||
|
Only relevant if you use the select option,
|
||||||
|
because then the data property can be different
|
||||||
|
from what the queryFn returns.
|
||||||
|
Otherwise, it will default to whatever the queryFn returns.
|
||||||
|
TQueryKey: the type of our queryKey, only relevant
|
||||||
|
if you use the queryKey that is passed to your queryFn.
|
||||||
|
|
||||||
|
*/
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
export const ISSUE_INVOICES_QUERY_KEY = (criteria: CriteriaDTO): QueryKey => [
|
||||||
|
"issue-invoice",
|
||||||
|
{
|
||||||
|
pageNumber: criteria.pageNumber ?? 0,
|
||||||
|
pageSize: criteria.pageSize ?? 10,
|
||||||
|
q: criteria.q ?? "",
|
||||||
|
filters: criteria.filters ?? [],
|
||||||
|
orderBy: criteria.orderBy ?? "",
|
||||||
|
order: criteria.order ?? "",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
type IssueInvoicesQueryOptions = {
|
||||||
|
enabled?: boolean;
|
||||||
|
criteria?: CriteriaDTO;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Obtener todas las facturas
|
||||||
|
export const useIssueInvoicesQuery = (options?: IssueInvoicesQueryOptions) => {
|
||||||
|
const dataSource = useDataSource();
|
||||||
|
const enabled = options?.enabled ?? true;
|
||||||
|
const criteria = options?.criteria ?? {};
|
||||||
|
|
||||||
|
return useQuery<IssueInvoiceSummaryPage, DefaultError>({
|
||||||
|
queryKey: ISSUE_INVOICES_QUERY_KEY(criteria),
|
||||||
|
queryFn: async ({ signal }) => {
|
||||||
|
return await dataSource.getList<IssueInvoiceSummaryPage>("issue-invoices", {
|
||||||
|
signal,
|
||||||
|
...criteria,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
enabled,
|
||||||
|
placeholderData: (previousData, previousQuery) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`)
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./hooks";
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
import { MoneyDTOHelper, PercentageDTOHelper, formatCurrency } from "@erp/core";
|
||||||
|
|
||||||
|
import type { IssueInvoiceSummaryPage } from "./issue-invoice.api.schema";
|
||||||
|
import type {
|
||||||
|
IssueInvoiceSummaryData,
|
||||||
|
IssueInvoiceSummaryPageData,
|
||||||
|
} from "./issue-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 {
|
||||||
|
return {
|
||||||
|
...pageDto,
|
||||||
|
items: pageDto.items.map(
|
||||||
|
(summaryDto) =>
|
||||||
|
({
|
||||||
|
...summaryDto,
|
||||||
|
|
||||||
|
subtotal_amount: MoneyDTOHelper.toNumber(summaryDto.subtotal_amount),
|
||||||
|
subtotal_amount_fmt: formatCurrency(
|
||||||
|
MoneyDTOHelper.toNumber(summaryDto.subtotal_amount),
|
||||||
|
Number(summaryDto.total_amount.scale || 2),
|
||||||
|
summaryDto.currency_code,
|
||||||
|
summaryDto.language_code
|
||||||
|
),
|
||||||
|
|
||||||
|
discount_percentage: PercentageDTOHelper.toNumber(summaryDto.discount_percentage),
|
||||||
|
discount_percentage_fmt: PercentageDTOHelper.toNumericString(
|
||||||
|
summaryDto.discount_percentage
|
||||||
|
),
|
||||||
|
|
||||||
|
discount_amount: MoneyDTOHelper.toNumber(summaryDto.discount_amount),
|
||||||
|
discount_amount_fmt: formatCurrency(
|
||||||
|
MoneyDTOHelper.toNumber(summaryDto.discount_amount),
|
||||||
|
Number(summaryDto.total_amount.scale || 2),
|
||||||
|
summaryDto.currency_code,
|
||||||
|
summaryDto.language_code
|
||||||
|
),
|
||||||
|
|
||||||
|
taxable_amount: MoneyDTOHelper.toNumber(summaryDto.taxable_amount),
|
||||||
|
taxable_amount_fmt: formatCurrency(
|
||||||
|
MoneyDTOHelper.toNumber(summaryDto.taxable_amount),
|
||||||
|
Number(summaryDto.total_amount.scale || 2),
|
||||||
|
summaryDto.currency_code,
|
||||||
|
summaryDto.language_code
|
||||||
|
),
|
||||||
|
|
||||||
|
taxes_amount: MoneyDTOHelper.toNumber(summaryDto.taxes_amount),
|
||||||
|
taxes_amount_fmt: formatCurrency(
|
||||||
|
MoneyDTOHelper.toNumber(summaryDto.taxes_amount),
|
||||||
|
Number(summaryDto.total_amount.scale || 2),
|
||||||
|
summaryDto.currency_code,
|
||||||
|
summaryDto.language_code
|
||||||
|
),
|
||||||
|
|
||||||
|
total_amount: MoneyDTOHelper.toNumber(summaryDto.total_amount),
|
||||||
|
total_amount_fmt: formatCurrency(
|
||||||
|
MoneyDTOHelper.toNumber(summaryDto.total_amount),
|
||||||
|
Number(summaryDto.total_amount.scale || 2),
|
||||||
|
summaryDto.currency_code,
|
||||||
|
summaryDto.language_code
|
||||||
|
),
|
||||||
|
|
||||||
|
//taxes: dto.taxes,
|
||||||
|
}) as unknown as IssueInvoiceSummaryData
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import type { IssueInvoiceSummary, IssueInvoiceSummaryPage } from "./issue-invoice.api.schema";
|
||||||
|
|
||||||
|
export type IssueInvoiceSummaryData = IssueInvoiceSummary & {
|
||||||
|
subtotal_amount_fmt: string;
|
||||||
|
subtotal_amount: number;
|
||||||
|
|
||||||
|
discount_percentage_fmt: string;
|
||||||
|
discount_percentage: number;
|
||||||
|
|
||||||
|
discount_amount_fmt: string;
|
||||||
|
discount_amount: number;
|
||||||
|
|
||||||
|
taxable_amount_fmt: string;
|
||||||
|
taxable_amount: number;
|
||||||
|
|
||||||
|
taxes_amoun_fmt: string;
|
||||||
|
taxes_amount: number;
|
||||||
|
|
||||||
|
total_amount_fmt: string;
|
||||||
|
total_amount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IssueInvoiceSummaryPageData = IssueInvoiceSummaryPage & {
|
||||||
|
items: IssueInvoiceSummary[];
|
||||||
|
};
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
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">;
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { IModuleClient, ModuleClientParams } from "@erp/core/client";
|
import type { IModuleClient, ModuleClientParams } from "@erp/core/client";
|
||||||
|
|
||||||
import { CustomerInvoiceRoutes } from "./customer-invoice-routes";
|
import { CustomerInvoiceRoutes } from "./customer-invoice-routes";
|
||||||
|
|
||||||
export const MODULE_NAME = "CustomerInvoices";
|
export const MODULE_NAME = "CustomerInvoices";
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
import { AppContent } from "@repo/rdx-ui/components";
|
import { AppContent } from "@repo/rdx-ui/components";
|
||||||
import { Button } from "@repo/shadcn-ui/components";
|
import { Button } from "@repo/shadcn-ui/components";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useCreateCustomerInvoiceMutation } from "../../hooks";
|
|
||||||
|
import { useCreateProforma } from "../../hooks";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
|
|
||||||
import { CreateCustomerInvoiceEditForm } from "./create-customer-invoice-edit-form";
|
import { CreateCustomerInvoiceEditForm } from "./create-customer-invoice-edit-form";
|
||||||
|
|
||||||
export const CustomerInvoiceCreate = () => {
|
export const CustomerInvoiceCreate = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { mutate, isPending, isError, error } = useCreateCustomerInvoiceMutation();
|
const { mutate, isPending, isError, error } = useCreateProforma();
|
||||||
|
|
||||||
const handleSubmit = (data: any) => {
|
const handleSubmit = (data: any) => {
|
||||||
// Handle form submission logic here
|
// Handle form submission logic here
|
||||||
@ -51,19 +53,19 @@ export const CustomerInvoiceCreate = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppContent>
|
<AppContent>
|
||||||
<div className='flex items-center justify-between space-y-2'>
|
<div className="flex items-center justify-between space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<h2 className='text-2xl font-bold tracking-tight'>{t("pages.create.title")}</h2>
|
<h2 className="text-2xl font-bold tracking-tight">{t("pages.create.title")}</h2>
|
||||||
<p className='text-muted-foreground'>{t("pages.create.description")}</p>
|
<p className="text-muted-foreground">{t("pages.create.description")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center justify-end mb-4'>
|
<div className="flex items-center justify-end mb-4">
|
||||||
<Button className='cursor-pointer' onClick={() => navigate("/customer-invoices/list")}>
|
<Button className="cursor-pointer" onClick={() => navigate("/customer-invoices/list")}>
|
||||||
{t("pages.create.back_to_list")}
|
{t("pages.create.back_to_list")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-1 flex-col gap-4 p-4'>
|
<div className="flex flex-1 flex-col gap-4 p-4">
|
||||||
<CreateCustomerInvoiceEditForm onSubmit={handleSubmit} isPending={isPending} />
|
<CreateCustomerInvoiceEditForm isPending={isPending} onSubmit={handleSubmit} />
|
||||||
</div>
|
</div>
|
||||||
</AppContent>
|
</AppContent>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
import { CreateCustomerInvoiceRequestSchema } from "../../../common/dto";
|
|
||||||
|
|
||||||
export const CustomerInvoiceItemDataFormSchema = CreateCustomerInvoiceRequestSchema.extend({
|
import { CreateProformaRequestSchema } from "../../../common/dto";
|
||||||
|
|
||||||
|
export const CustomerInvoiceItemDataFormSchema = CreateProformaRequestSchema.extend({
|
||||||
subtotal_price: z.object({
|
subtotal_price: z.object({
|
||||||
amount: z.number().nullable(),
|
amount: z.number().nullable(),
|
||||||
scale: z.number(),
|
scale: z.number(),
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
import type { CellKeyDownEvent, RowClickedEvent } from "ag-grid-community";
|
import { SimpleSearchInput } from "@erp/core/components";
|
||||||
|
import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
|
|
||||||
import { SimpleSearchInput } from '@erp/core/components';
|
|
||||||
import { DataTable, SkeletonDataTable } from '@repo/rdx-ui/components';
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { usePinnedPreviewSheet } from '../../hooks';
|
|
||||||
|
import { usePinnedPreviewSheet } from "../../hooks";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import { InvoiceSummaryFormData, InvoicesPageFormData } from '../../schemas';
|
import { useProformasGridColumns } from "../../proformas/pages/list/use-proformas-grid-columns";
|
||||||
import { useInvoicesListColumns } from './use-invoices-list-columns';
|
import type { InvoiceSummaryFormData, InvoicesPageFormData } from "../../schemas";
|
||||||
|
|
||||||
export type InvoiceUpdateCompProps = {
|
export type InvoiceUpdateCompProps = {
|
||||||
invoicesPage: InvoicesPageFormData;
|
invoicesPage: InvoicesPageFormData;
|
||||||
@ -22,9 +20,12 @@ export type InvoiceUpdateCompProps = {
|
|||||||
searchValue: string;
|
searchValue: string;
|
||||||
onSearchChange: (value: string) => void;
|
onSearchChange: (value: string) => void;
|
||||||
|
|
||||||
onRowClick?: (row: InvoiceSummaryFormData, index: number, event: React.MouseEvent<HTMLTableRowElement>) => void;
|
onRowClick?: (
|
||||||
}
|
row: InvoiceSummaryFormData,
|
||||||
|
index: number,
|
||||||
|
event: React.MouseEvent<HTMLTableRowElement>
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
// Create new GridExample component
|
// Create new GridExample component
|
||||||
export const InvoicesListGrid = ({
|
export const InvoicesListGrid = ({
|
||||||
@ -34,8 +35,9 @@ export const InvoicesListGrid = ({
|
|||||||
pageSize,
|
pageSize,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
onPageSizeChange,
|
onPageSizeChange,
|
||||||
searchValue, onSearchChange,
|
searchValue,
|
||||||
onRowClick
|
onSearchChange,
|
||||||
|
onRowClick,
|
||||||
}: InvoiceUpdateCompProps) => {
|
}: InvoiceUpdateCompProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -49,7 +51,7 @@ export const InvoicesListGrid = ({
|
|||||||
|
|
||||||
const [statusFilter, setStatusFilter] = useState("todas");
|
const [statusFilter, setStatusFilter] = useState("todas");
|
||||||
|
|
||||||
const columns = useInvoicesListColumns({
|
const columns = useProformasGridColumns({
|
||||||
onEdit: (invoice) => navigate(`/customer-invoices/${invoice.id}/edit`),
|
onEdit: (invoice) => navigate(`/customer-invoices/${invoice.id}/edit`),
|
||||||
onDuplicate: (invoice) => null, //duplicateInvoice(inv.id),
|
onDuplicate: (invoice) => null, //duplicateInvoice(inv.id),
|
||||||
onDownloadPdf: (invoice) => null, //downloadInvoicePdf(inv.id),
|
onDownloadPdf: (invoice) => null, //downloadInvoicePdf(inv.id),
|
||||||
@ -61,9 +63,7 @@ export const InvoicesListGrid = ({
|
|||||||
const goToRow = useCallback(
|
const goToRow = useCallback(
|
||||||
(id: string, newTab = false) => {
|
(id: string, newTab = false) => {
|
||||||
const url = `/customer-invoices/${id}/edit`;
|
const url = `/customer-invoices/${id}/edit`;
|
||||||
newTab
|
newTab ? window.open(url, "_blank", "noopener,noreferrer") : navigate(url);
|
||||||
? window.open(url, "_blank", "noopener,noreferrer")
|
|
||||||
: navigate(url);
|
|
||||||
},
|
},
|
||||||
[navigate]
|
[navigate]
|
||||||
);
|
);
|
||||||
@ -71,8 +71,7 @@ export const InvoicesListGrid = ({
|
|||||||
const onRowClicked = useCallback(
|
const onRowClicked = useCallback(
|
||||||
(e: RowClickedEvent<any>) => {
|
(e: RowClickedEvent<any>) => {
|
||||||
if (!e.data) return;
|
if (!e.data) return;
|
||||||
const newTab =
|
const newTab = e.event instanceof MouseEvent && (e.event.metaKey || e.event.ctrlKey);
|
||||||
e.event instanceof MouseEvent && (e.event.metaKey || e.event.ctrlKey);
|
|
||||||
goToRow(e.data.id, newTab);
|
goToRow(e.data.id, newTab);
|
||||||
},
|
},
|
||||||
[goToRow]
|
[goToRow]
|
||||||
@ -83,7 +82,7 @@ export const InvoicesListGrid = ({
|
|||||||
if (!e.data) return;
|
if (!e.data) return;
|
||||||
|
|
||||||
const ev = e.event;
|
const ev = e.event;
|
||||||
if (!ev || !(ev instanceof KeyboardEvent)) return;
|
if (!(ev && ev instanceof KeyboardEvent)) return;
|
||||||
|
|
||||||
const key = ev.key;
|
const key = ev.key;
|
||||||
if (key === "Enter" || key === " ") {
|
if (key === "Enter" || key === " ") {
|
||||||
@ -98,11 +97,13 @@ export const InvoicesListGrid = ({
|
|||||||
[goToRow]
|
[goToRow]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const handleRowClick = useCallback(
|
const handleRowClick = useCallback(
|
||||||
(invoice: InvoiceSummaryFormData, _i: number, e: React.MouseEvent) => {
|
(invoice: InvoiceSummaryFormData, _i: number, e: React.MouseEvent) => {
|
||||||
const url = `/customer-invoices/${invoice.id}/edit`;
|
const url = `/customer-invoices/${invoice.id}/edit`;
|
||||||
if (e.metaKey || e.ctrlKey) { window.open(url, "_blank", "noopener,noreferrer"); return; }
|
if (e.metaKey || e.ctrlKey) {
|
||||||
|
window.open(url, "_blank", "noopener,noreferrer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
preview.open(invoice);
|
preview.open(invoice);
|
||||||
},
|
},
|
||||||
[preview]
|
[preview]
|
||||||
@ -113,9 +114,9 @@ export const InvoicesListGrid = ({
|
|||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<SkeletonDataTable
|
<SkeletonDataTable
|
||||||
columns={columns.length}
|
columns={columns.length}
|
||||||
|
footerProps={{ pageIndex, pageSize, totalItems: total_items ?? 0 }}
|
||||||
rows={Math.max(6, pageSize)}
|
rows={Math.max(6, pageSize)}
|
||||||
showFooter
|
showFooter
|
||||||
footerProps={{ pageIndex, pageSize, totalItems: total_items ?? 0 }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -126,7 +127,7 @@ export const InvoicesListGrid = ({
|
|||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{/* Barra de filtros */}
|
{/* Barra de filtros */}
|
||||||
<div className="flex flex-col sm:flex-row gap-4 mb-6">
|
<div className="flex flex-col sm:flex-row gap-4 mb-6">
|
||||||
<SimpleSearchInput onSearchChange={onSearchChange} loading={loading} />
|
<SimpleSearchInput loading={loading} onSearchChange={onSearchChange} />
|
||||||
{/*<Select value={statusFilter} onValueChange={setStatusFilter}>
|
{/*<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||||
<SelectTrigger className="w-full sm:w-48 bg-white border-gray-200 shadow-sm">
|
<SelectTrigger className="w-full sm:w-48 bg-white border-gray-200 shadow-sm">
|
||||||
<FilterIcon className="mr-2 h-4 w-4" />
|
<FilterIcon className="mr-2 h-4 w-4" />
|
||||||
@ -149,16 +150,16 @@ export const InvoicesListGrid = ({
|
|||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={items}
|
data={items}
|
||||||
readOnly
|
|
||||||
enableRowSelection
|
|
||||||
enablePagination
|
enablePagination
|
||||||
|
enableRowSelection
|
||||||
manualPagination
|
manualPagination
|
||||||
pageIndex={pageIndex}
|
|
||||||
pageSize={pageSize}
|
|
||||||
totalItems={total_items}
|
|
||||||
onPageChange={onPageChange}
|
onPageChange={onPageChange}
|
||||||
onPageSizeChange={onPageSizeChange}
|
onPageSizeChange={onPageSizeChange}
|
||||||
onRowClick={handleRowClick}
|
onRowClick={handleRowClick}
|
||||||
|
pageIndex={pageIndex}
|
||||||
|
pageSize={pageSize}
|
||||||
|
readOnly
|
||||||
|
totalItems={total_items}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
import { PageHeader } from '@erp/core/components';
|
import { PageHeader } from "@erp/core/components";
|
||||||
import { ErrorAlert } from '@erp/customers/components';
|
import { ErrorAlert } from "@erp/customers/components";
|
||||||
import { AppContent, AppHeader, BackHistoryButton, useDebounce } from "@repo/rdx-ui/components";
|
import { AppContent, AppHeader, BackHistoryButton, useDebounce } from "@repo/rdx-ui/components";
|
||||||
import { Button } from "@repo/shadcn-ui/components";
|
import { Button } from "@repo/shadcn-ui/components";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useInvoicesQuery } from '../../hooks';
|
|
||||||
|
import { useInvoicesQuery } from "../../hooks";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import { invoiceResumeDtoToFormAdapter } from '../../schemas/invoice-resume-dto.adapter';
|
import { invoiceResumeDtoToFormAdapter } from "../../schemas/invoice-resume-dto.adapter";
|
||||||
import { InvoicesListGrid } from './invoices-list-grid';
|
|
||||||
|
import { InvoicesListGrid } from "./invoices-list-grid";
|
||||||
|
|
||||||
export const InvoiceListPage = () => {
|
export const InvoiceListPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -29,24 +31,18 @@ export const InvoiceListPage = () => {
|
|||||||
[pageSize, pageIndex, debouncedQ]
|
[pageSize, pageIndex, debouncedQ]
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const { data, isLoading, isError, error } = useInvoicesQuery({
|
||||||
data,
|
criteria,
|
||||||
isLoading,
|
|
||||||
isError,
|
|
||||||
error,
|
|
||||||
} = useInvoicesQuery({
|
|
||||||
criteria
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const invoicesPageData = useMemo(() => {
|
const invoicesPageData = useMemo(() => {
|
||||||
if (!data) return undefined;
|
if (!data) return undefined;
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
items: invoiceResumeDtoToFormAdapter.fromDto(data.items)
|
items: invoiceResumeDtoToFormAdapter.fromDto(data.items),
|
||||||
}
|
};
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
|
||||||
const handlePageChange = (newPageIndex: number) => {
|
const handlePageChange = (newPageIndex: number) => {
|
||||||
setPageIndex(newPageIndex);
|
setPageIndex(newPageIndex);
|
||||||
};
|
};
|
||||||
@ -67,8 +63,8 @@ export const InvoiceListPage = () => {
|
|||||||
return (
|
return (
|
||||||
<AppContent>
|
<AppContent>
|
||||||
<ErrorAlert
|
<ErrorAlert
|
||||||
title={t("pages.list.loadErrorTitle")}
|
|
||||||
message={(error as Error)?.message || "Error al cargar el listado"}
|
message={(error as Error)?.message || "Error al cargar el listado"}
|
||||||
|
title={t("pages.list.loadErrorTitle")}
|
||||||
/>
|
/>
|
||||||
<BackHistoryButton />
|
<BackHistoryButton />
|
||||||
</AppContent>
|
</AppContent>
|
||||||
@ -79,36 +75,35 @@ export const InvoiceListPage = () => {
|
|||||||
<>
|
<>
|
||||||
<AppHeader>
|
<AppHeader>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={t("pages.list.title")}
|
|
||||||
description={t("pages.list.description")}
|
description={t("pages.list.description")}
|
||||||
rightSlot={
|
rightSlot={
|
||||||
<div className='flex items-center space-x-2'>
|
<div className="flex items-center space-x-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => navigate("/customer-invoices/create")}
|
|
||||||
variant={'default'}
|
|
||||||
aria-label={t("pages.create.title")}
|
aria-label={t("pages.create.title")}
|
||||||
className='cursor-pointer'
|
className="cursor-pointer"
|
||||||
|
onClick={() => navigate("/customer-invoices/create")}
|
||||||
|
variant={"default"}
|
||||||
>
|
>
|
||||||
<PlusIcon className="mr-2 h-4 w-4" aria-hidden />
|
<PlusIcon aria-hidden className="mr-2 h-4 w-4" />
|
||||||
{t("pages.create.title")}
|
{t("pages.create.title")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
title={t("pages.list.title")}
|
||||||
/>
|
/>
|
||||||
</AppHeader>
|
</AppHeader>
|
||||||
<AppContent>
|
<AppContent>
|
||||||
|
<div className="flex flex-col w-full h-full py-3">
|
||||||
<div className='flex flex-col w-full h-full py-3'>
|
|
||||||
<div className={"flex-1"}>
|
<div className={"flex-1"}>
|
||||||
<InvoicesListGrid
|
<InvoicesListGrid
|
||||||
invoicesPage={invoicesPageData}
|
invoicesPage={invoicesPageData}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
pageIndex={pageIndex}
|
|
||||||
pageSize={pageSize}
|
|
||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
onPageSizeChange={handlePageSizeChange}
|
onPageSizeChange={handlePageSizeChange}
|
||||||
searchValue={search}
|
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
|
pageIndex={pageIndex}
|
||||||
|
pageSize={pageSize}
|
||||||
|
searchValue={search}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,233 +0,0 @@
|
|||||||
import { formatDate } from '@erp/core/client';
|
|
||||||
import { DataTableColumnHeader } from '@repo/rdx-ui/components';
|
|
||||||
import {
|
|
||||||
Button, 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 { CustomerInvoiceStatusBadge } from '../../components';
|
|
||||||
import { useTranslation } from '../../i18n';
|
|
||||||
import { InvoiceSummaryFormData } from '../../schemas';
|
|
||||||
|
|
||||||
type InvoiceActionHandlers = {
|
|
||||||
onEdit?: (invoice: InvoiceSummaryFormData) => void;
|
|
||||||
onDuplicate?: (invoice: InvoiceSummaryFormData) => void;
|
|
||||||
onDownloadPdf?: (invoice: InvoiceSummaryFormData) => void;
|
|
||||||
onSendEmail?: (invoice: InvoiceSummaryFormData) => void;
|
|
||||||
onDelete?: (invoice: InvoiceSummaryFormData) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useInvoicesListColumns(
|
|
||||||
handlers: InvoiceActionHandlers = {}
|
|
||||||
): ColumnDef<InvoiceSummaryFormData>[] {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const {
|
|
||||||
onEdit, onDuplicate, onDownloadPdf, onSendEmail, onDelete,
|
|
||||||
} = handlers;
|
|
||||||
|
|
||||||
return React.useMemo<ColumnDef<InvoiceSummaryFormData>[]>(() => [
|
|
||||||
// Nº
|
|
||||||
{
|
|
||||||
accessorKey: "invoice_number",
|
|
||||||
header: ({ column }) => (
|
|
||||||
<DataTableColumnHeader column={column} title={t("pages.list.grid_columns.invoice_number")} className="text-left" />
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="font-semibold text-left text-primary">
|
|
||||||
{row.getValue("invoice_number")}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
enableHiding: false,
|
|
||||||
enableSorting: false,
|
|
||||||
size: 160,
|
|
||||||
minSize: 120,
|
|
||||||
},
|
|
||||||
// Estado
|
|
||||||
{
|
|
||||||
accessorKey: "status",
|
|
||||||
header: ({ column }) => (
|
|
||||||
<DataTableColumnHeader column={column} title={t("pages.list.grid_columns.status")} className="text-left" />
|
|
||||||
),
|
|
||||||
cell: ({ row }) => <CustomerInvoiceStatusBadge status={row.getValue("status")} />,
|
|
||||||
enableSorting: false,
|
|
||||||
size: 140,
|
|
||||||
minSize: 120,
|
|
||||||
},
|
|
||||||
// Serie
|
|
||||||
{
|
|
||||||
accessorKey: "series",
|
|
||||||
header: ({ column }) => (
|
|
||||||
<DataTableColumnHeader column={column} title={t("pages.list.grid_columns.series")} className="text-left" />
|
|
||||||
),
|
|
||||||
cell: ({ row }) => <div className="font-medium text-left">{row.getValue("series")}</div>,
|
|
||||||
enableSorting: false,
|
|
||||||
size: 120,
|
|
||||||
minSize: 100,
|
|
||||||
},
|
|
||||||
// Fecha factura
|
|
||||||
{
|
|
||||||
accessorKey: "invoice_date",
|
|
||||||
header: ({ column }) => (
|
|
||||||
<DataTableColumnHeader column={column} title={t("pages.list.grid_columns.invoice_date")} className="text-left tabular-nums" />
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="font-medium text-left tabular-nums">
|
|
||||||
{formatDate(row.getValue("invoice_date"))}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
enableSorting: false,
|
|
||||||
size: 140,
|
|
||||||
minSize: 120,
|
|
||||||
},
|
|
||||||
// Fecha operación
|
|
||||||
{
|
|
||||||
accessorKey: "operation_date",
|
|
||||||
header: ({ column }) => (
|
|
||||||
<DataTableColumnHeader column={column} title={t("pages.list.grid_columns.operation_date")} className="text-left tabular-nums" />
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="font-medium text-left tabular-nums">
|
|
||||||
{formatDate(row.getValue("operation_date"))}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
enableSorting: false,
|
|
||||||
size: 140,
|
|
||||||
minSize: 120,
|
|
||||||
},
|
|
||||||
// TIN
|
|
||||||
{
|
|
||||||
id: "recipient_tin",
|
|
||||||
accessorKey: "recipient.tin",
|
|
||||||
header: ({ column }) => (
|
|
||||||
<DataTableColumnHeader column={column} title={t("pages.list.grid_columns.recipient_tin")} className="text-left tabular-nums" />
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="font-medium text-left tabular-nums">{row.getValue("recipient_tin")}</div>
|
|
||||||
),
|
|
||||||
enableSorting: false,
|
|
||||||
size: 160,
|
|
||||||
minSize: 140,
|
|
||||||
},
|
|
||||||
// Cliente
|
|
||||||
{
|
|
||||||
accessorKey: "recipient.name",
|
|
||||||
id: "recipient_name",
|
|
||||||
header: ({ column }) => (
|
|
||||||
<DataTableColumnHeader column={column} title={t("pages.list.grid_columns.recipient_name")} className="text-left" />
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="font-semibold text-left truncate" title={row.getValue("recipient_name")}>
|
|
||||||
{row.getValue("recipient_name")}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
enableSorting: false,
|
|
||||||
size: 260,
|
|
||||||
minSize: 200,
|
|
||||||
},
|
|
||||||
// Total
|
|
||||||
{
|
|
||||||
accessorKey: "total_amount_fmt",
|
|
||||||
header: ({ column }) => (
|
|
||||||
<DataTableColumnHeader column={column} title={t("pages.list.grid_columns.total_amount")} className="text-right tabular-nums" />
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="font-semibold text-right tabular-nums">{row.getValue("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" onClick={stop} onKeyDown={stop}>
|
|
||||||
{/* Editar (acción primaria) */}
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className='cursor-pointer text-muted-foreground hover:text-primary'
|
|
||||||
aria-label={t("common.edit")}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onEdit?.(invoice);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EditIcon className="size-4" aria-hidden="true" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>{t("common.edit")}</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
{/* Menú demás acciones */}
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className='cursor-pointer text-muted-foreground hover:text-primary'
|
|
||||||
aria-label={t("common.more_actions")}
|
|
||||||
onClick={stop}
|
|
||||||
>
|
|
||||||
<MoreVerticalIcon className="size-4" aria-hidden="true" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" className="w-48">
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => onDuplicate?.(invoice)}
|
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
|
||||||
<CopyIcon className="mr-2 size-4" />
|
|
||||||
{t("common.duplicate")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => onDownloadPdf?.(invoice)}
|
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
|
||||||
<DownloadIcon className="mr-2 size-4" />
|
|
||||||
{t("common.download_pdf")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => onSendEmail?.(invoice)}
|
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
|
||||||
<MailIcon className="mr-2 size-4" />
|
|
||||||
{t("common.send_email")}
|
|
||||||
</DropdownMenuItem> <DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => onDelete?.(invoice)}
|
|
||||||
className="text-destructive focus:text-destructive-foreground focus:bg-destructive cursor-pointer"
|
|
||||||
>
|
|
||||||
<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]);
|
|
||||||
}
|
|
||||||
@ -1,116 +1,117 @@
|
|||||||
import { PageHeader } from '@erp/core/components';
|
import { PageHeader } from "@erp/core/components";
|
||||||
import {
|
import { UnsavedChangesProvider, UpdateCommitButtonGroup, useHookForm } from "@erp/core/hooks";
|
||||||
UnsavedChangesProvider,
|
|
||||||
UpdateCommitButtonGroup,
|
|
||||||
useHookForm
|
|
||||||
} from "@erp/core/hooks";
|
|
||||||
import { AppContent, AppHeader } from "@repo/rdx-ui/components";
|
import { AppContent, AppHeader } from "@repo/rdx-ui/components";
|
||||||
import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
|
import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
|
||||||
import { useId, useMemo } from 'react';
|
import { useId, useMemo } from "react";
|
||||||
import { FieldErrors, FormProvider } from "react-hook-form";
|
import { type FieldErrors, FormProvider } from "react-hook-form";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useInvoiceContext } from '../../context';
|
|
||||||
import { useUpdateCustomerInvoice } from "../../hooks";
|
import { useInvoiceContext } from "../../context";
|
||||||
|
import { useUpdateProforma } from "../../hooks";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import {
|
import {
|
||||||
CustomerInvoice,
|
type InvoiceFormData,
|
||||||
InvoiceFormData,
|
InvoiceFormSchema,
|
||||||
InvoiceFormSchema,
|
type Proforma,
|
||||||
defaultCustomerInvoiceFormData,
|
defaultCustomerInvoiceFormData,
|
||||||
invoiceDtoToFormAdapter
|
invoiceDtoToFormAdapter,
|
||||||
} from "../../schemas";
|
} from "../../schemas";
|
||||||
import { InvoiceUpdateForm } from './invoice-update-form';
|
|
||||||
|
|
||||||
|
import { InvoiceUpdateForm } from "./invoice-update-form";
|
||||||
|
|
||||||
export type InvoiceUpdateCompProps = {
|
export type InvoiceUpdateCompProps = {
|
||||||
invoice: CustomerInvoice,
|
invoice: Proforma;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const InvoiceUpdateComp = ({
|
export const InvoiceUpdateComp = ({ invoice: invoiceData }: InvoiceUpdateCompProps) => {
|
||||||
invoice: invoiceData,
|
const { t } = useTranslation();
|
||||||
}: InvoiceUpdateCompProps) => {
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation();
|
const formId = useId();
|
||||||
const navigate = useNavigate();
|
|
||||||
const formId = useId();
|
const context = useInvoiceContext();
|
||||||
|
const { invoice_id } = context;
|
||||||
const context = useInvoiceContext();
|
|
||||||
const { invoice_id } = context;
|
const isPending = !invoiceData;
|
||||||
|
|
||||||
const isPending = !invoiceData;
|
const {
|
||||||
|
mutate,
|
||||||
const {
|
isPending: isUpdating,
|
||||||
mutate,
|
isError: isUpdateError,
|
||||||
isPending: isUpdating,
|
error: updateError,
|
||||||
isError: isUpdateError,
|
} = useUpdateProforma();
|
||||||
error: updateError,
|
|
||||||
} = useUpdateCustomerInvoice();
|
const initialValues = useMemo(() => {
|
||||||
|
return invoiceData
|
||||||
const initialValues = useMemo(() => {
|
? invoiceDtoToFormAdapter.fromDto(invoiceData, context)
|
||||||
return invoiceData
|
: defaultCustomerInvoiceFormData;
|
||||||
? invoiceDtoToFormAdapter.fromDto(invoiceData, context)
|
}, [invoiceData, context]);
|
||||||
: defaultCustomerInvoiceFormData
|
|
||||||
}, [invoiceData, context]);
|
const form = useHookForm<InvoiceFormData>({
|
||||||
|
resolverSchema: InvoiceFormSchema,
|
||||||
|
initialValues,
|
||||||
const form = useHookForm<InvoiceFormData>({
|
disabled: !invoiceData || isUpdating,
|
||||||
resolverSchema: InvoiceFormSchema,
|
});
|
||||||
initialValues,
|
|
||||||
disabled: !invoiceData || isUpdating,
|
const handleSubmit = (formData: InvoiceFormData) => {
|
||||||
});
|
console.log("Guardo factura");
|
||||||
|
const dto = invoiceDtoToFormAdapter.toDto(formData, context);
|
||||||
const handleSubmit = (formData: InvoiceFormData) => {
|
console.log("dto => ", dto);
|
||||||
console.log('Guardo factura')
|
mutate(
|
||||||
const dto = invoiceDtoToFormAdapter.toDto(formData, context)
|
{ id: invoice_id, data: dto as Partial<InvoiceFormData> },
|
||||||
console.log("dto => ", dto);
|
{
|
||||||
mutate(
|
onSuccess: () =>
|
||||||
{ id: invoice_id, data: dto as Partial<InvoiceFormData> },
|
showSuccessToast(t("pages.update.success.title"), t("pages.update.success.message")),
|
||||||
{
|
onError: (e) => showErrorToast(t("pages.update.errorTitle"), e.message),
|
||||||
onSuccess: () => showSuccessToast(t("pages.update.success.title"), t("pages.update.success.message")),
|
}
|
||||||
onError: (e) => showErrorToast(t("pages.update.errorTitle"), e.message),
|
);
|
||||||
}
|
};
|
||||||
);
|
|
||||||
};
|
const handleReset = () =>
|
||||||
|
form.reset((invoiceData as unknown as InvoiceFormData) ?? defaultCustomerInvoiceFormData);
|
||||||
const handleReset = () =>
|
|
||||||
form.reset((invoiceData as unknown as InvoiceFormData) ?? defaultCustomerInvoiceFormData);
|
const handleBack = () => {
|
||||||
|
navigate(-1);
|
||||||
const handleBack = () => {
|
};
|
||||||
navigate(-1);
|
|
||||||
};
|
const handleError = (errors: FieldErrors<InvoiceFormData>) => {
|
||||||
|
console.error("Errores en el formulario:", errors);
|
||||||
const handleError = (errors: FieldErrors<InvoiceFormData>) => {
|
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
|
||||||
console.error("Errores en el formulario:", errors);
|
};
|
||||||
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
|
|
||||||
};
|
return (
|
||||||
|
<UnsavedChangesProvider isDirty={form.formState.isDirty}>
|
||||||
return (
|
<AppHeader>
|
||||||
<UnsavedChangesProvider isDirty={form.formState.isDirty}>
|
<PageHeader
|
||||||
<AppHeader>
|
backIcon
|
||||||
<PageHeader
|
description={t("pages.edit.description")}
|
||||||
backIcon
|
rightSlot={
|
||||||
title={`${t("pages.edit.title")} #${invoiceData.invoice_number}`}
|
<>
|
||||||
description={t("pages.edit.description")}
|
<UpdateCommitButtonGroup
|
||||||
rightSlot={<>
|
cancel={{ formId, to: "/customer-invoices/list" }}
|
||||||
<UpdateCommitButtonGroup
|
isLoading={isPending}
|
||||||
isLoading={isPending}
|
submit={{
|
||||||
|
formId,
|
||||||
submit={{ formId, variant: 'default', disabled: isPending, label: t("pages.edit.actions.save_draft") }}
|
variant: "default",
|
||||||
cancel={{ formId, to: "/customer-invoices/list" }}
|
disabled: isPending,
|
||||||
/>
|
label: t("pages.edit.actions.save_draft"),
|
||||||
</>}
|
}}
|
||||||
/>
|
/>
|
||||||
</AppHeader>
|
</>
|
||||||
|
}
|
||||||
<AppContent>
|
title={`${t("pages.edit.title")} #${invoiceData.invoice_number}`}
|
||||||
<FormProvider {...form}>
|
/>
|
||||||
<InvoiceUpdateForm
|
</AppHeader>
|
||||||
formId={formId}
|
|
||||||
onSubmit={handleSubmit}
|
<AppContent>
|
||||||
onError={handleError}
|
<FormProvider {...form}>
|
||||||
className="bg-white rounded-xl border shadow-xl max-w-full"
|
<InvoiceUpdateForm
|
||||||
/>
|
className="bg-white rounded-xl border shadow-xl max-w-full"
|
||||||
</FormProvider>
|
formId={formId}
|
||||||
</AppContent>
|
onError={handleError}
|
||||||
</UnsavedChangesProvider>
|
onSubmit={handleSubmit}
|
||||||
);
|
/>
|
||||||
|
</FormProvider>
|
||||||
|
</AppContent>
|
||||||
|
</UnsavedChangesProvider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
@ -1 +1,2 @@
|
|||||||
|
export * from "./use-proforma-query";
|
||||||
export * from "./use-proformas-query";
|
export * from "./use-proformas-query";
|
||||||
|
|||||||
@ -0,0 +1,49 @@
|
|||||||
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
|
import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import type { Proforma } from "../proforma.api.schema";
|
||||||
|
|
||||||
|
export const PROFORMA_QUERY_KEY = (id: string): QueryKey => ["proforma", id] as const;
|
||||||
|
|
||||||
|
type InvoiceQueryOptions = {
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useProformaQuery = (proformaId?: string, options?: InvoiceQueryOptions) => {
|
||||||
|
const dataSource = useDataSource();
|
||||||
|
const enabled = (options?.enabled ?? true) && !!proformaId;
|
||||||
|
|
||||||
|
return useQuery<Proforma, DefaultError>({
|
||||||
|
queryKey: PROFORMA_QUERY_KEY(proformaId ?? "unknown"),
|
||||||
|
queryFn: async (context) => {
|
||||||
|
const { signal } = context;
|
||||||
|
if (!proformaId) {
|
||||||
|
if (!proformaId) throw new Error("proformaId is required");
|
||||||
|
}
|
||||||
|
return await dataSource.getOne<Proforma>("proformas", proformaId, {
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
enabled,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
export function useQuery<
|
||||||
|
TQueryFnData = unknown,
|
||||||
|
TError = unknown,
|
||||||
|
TData = TQueryFnData,
|
||||||
|
TQueryKey extends QueryKey = QueryKey
|
||||||
|
>
|
||||||
|
|
||||||
|
TQueryFnData: the type returned from the queryFn.
|
||||||
|
TError: the type of Errors to expect from the queryFn.
|
||||||
|
TData: the type our data property will eventually have.
|
||||||
|
Only relevant if you use the select option,
|
||||||
|
because then the data property can be different
|
||||||
|
from what the queryFn returns.
|
||||||
|
Otherwise, it will default to whatever the queryFn returns.
|
||||||
|
TQueryKey: the type of our queryKey, only relevant
|
||||||
|
if you use the queryKey that is passed to your queryFn.
|
||||||
|
|
||||||
|
*/
|
||||||
@ -1,48 +1,41 @@
|
|||||||
|
import type { CriteriaDTO } from "@erp/core";
|
||||||
import { useDataSource } from "@erp/core/hooks";
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
|
import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
export const CUSTOMER_INVOICE_QUERY_KEY = (id: string): QueryKey =>
|
import type { ProformaSummaryPage } from "../proforma.api.schema";
|
||||||
["customer_invoice", id] as const;
|
|
||||||
|
|
||||||
type InvoiceQueryOptions = {
|
export const PROFORMAS_QUERY_KEY = (criteria: CriteriaDTO): QueryKey => [
|
||||||
|
"proforma",
|
||||||
|
{
|
||||||
|
pageNumber: criteria.pageNumber ?? 0,
|
||||||
|
pageSize: criteria.pageSize ?? 10,
|
||||||
|
q: criteria.q ?? "",
|
||||||
|
filters: criteria.filters ?? [],
|
||||||
|
orderBy: criteria.orderBy ?? "",
|
||||||
|
order: criteria.order ?? "",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
type ProformasQueryOptions = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
criteria?: CriteriaDTO;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useInvoiceQuery = (invoiceId?: string, options?: InvoiceQueryOptions) => {
|
// Obtener todas las facturas
|
||||||
|
export const useProformasQuery = (options?: ProformasQueryOptions) => {
|
||||||
const dataSource = useDataSource();
|
const dataSource = useDataSource();
|
||||||
const enabled = (options?.enabled ?? true) && !!invoiceId;
|
const enabled = options?.enabled ?? true;
|
||||||
|
const criteria = options?.criteria ?? {};
|
||||||
|
|
||||||
return useQuery<CustomerInvoice, DefaultError>({
|
return useQuery<ProformaSummaryPage, DefaultError>({
|
||||||
queryKey: CUSTOMER_INVOICE_QUERY_KEY(invoiceId ?? "unknown"),
|
queryKey: PROFORMAS_QUERY_KEY(criteria),
|
||||||
queryFn: async (context) => {
|
queryFn: async ({ signal }) => {
|
||||||
const { signal } = context;
|
return await dataSource.getList<ProformaSummaryPage>("proformas", {
|
||||||
if (!invoiceId) {
|
|
||||||
if (!invoiceId) throw new Error("invoiceId is required");
|
|
||||||
}
|
|
||||||
return await dataSource.getOne<CustomerInvoice>("customer-invoices", invoiceId, {
|
|
||||||
signal,
|
signal,
|
||||||
|
...criteria,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
enabled,
|
enabled,
|
||||||
|
placeholderData: (previousData, previousQuery) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
export function useQuery<
|
|
||||||
TQueryFnData = unknown,
|
|
||||||
TError = unknown,
|
|
||||||
TData = TQueryFnData,
|
|
||||||
TQueryKey extends QueryKey = QueryKey
|
|
||||||
>
|
|
||||||
|
|
||||||
TQueryFnData: the type returned from the queryFn.
|
|
||||||
TError: the type of Errors to expect from the queryFn.
|
|
||||||
TData: the type our data property will eventually have.
|
|
||||||
Only relevant if you use the select option,
|
|
||||||
because then the data property can be different
|
|
||||||
from what the queryFn returns.
|
|
||||||
Otherwise, it will default to whatever the queryFn returns.
|
|
||||||
TQueryKey: the type of our queryKey, only relevant
|
|
||||||
if you use the queryKey that is passed to your queryFn.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import type { CriteriaDTO } from "@erp/core";
|
||||||
import { PageHeader } from "@erp/core/components";
|
import { PageHeader } from "@erp/core/components";
|
||||||
import { ErrorAlert } from "@erp/customers/components";
|
import { ErrorAlert } from "@erp/customers/components";
|
||||||
import { AppContent, AppHeader, BackHistoryButton, useDebounce } from "@repo/rdx-ui/components";
|
import { AppContent, AppHeader, BackHistoryButton, useDebounce } from "@repo/rdx-ui/components";
|
||||||
@ -6,9 +7,11 @@ import { PlusIcon } from "lucide-react";
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { useInvoicesQuery } from "../../../hooks";
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import { invoiceResumeDtoToFormAdapter } from "../../../schemas";
|
import { useProformasQuery } from "../../hooks";
|
||||||
|
import { ProformaResumeDtoAdapter } from "../../proforma-resume-dto.adapter";
|
||||||
|
|
||||||
|
import { ProformasGrid } from "./proformas-grid";
|
||||||
|
|
||||||
export const ProformaListPage = () => {
|
export const ProformaListPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -21,24 +24,26 @@ export const ProformaListPage = () => {
|
|||||||
const debouncedQ = useDebounce(search, 300);
|
const debouncedQ = useDebounce(search, 300);
|
||||||
|
|
||||||
const criteria = useMemo(
|
const criteria = useMemo(
|
||||||
() => ({
|
() =>
|
||||||
q: debouncedQ || "",
|
({
|
||||||
pageSize,
|
q: debouncedQ || "",
|
||||||
pageNumber: pageIndex,
|
pageSize,
|
||||||
}),
|
pageNumber: pageIndex,
|
||||||
|
order: "desc",
|
||||||
|
orderBy: "invoice_date",
|
||||||
|
}) as CriteriaDTO,
|
||||||
[pageSize, pageIndex, debouncedQ]
|
[pageSize, pageIndex, debouncedQ]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data, isLoading, isError, error } = useInvoicesQuery({
|
console.log(criteria);
|
||||||
|
|
||||||
|
const { data, isLoading, isError, error } = useProformasQuery({
|
||||||
criteria,
|
criteria,
|
||||||
});
|
});
|
||||||
|
|
||||||
const invoicesPageData = useMemo(() => {
|
const proformaPageData = useMemo(() => {
|
||||||
if (!data) return undefined;
|
if (!data) return undefined;
|
||||||
return {
|
return ProformaResumeDtoAdapter.fromDto(data);
|
||||||
...data,
|
|
||||||
items: invoiceResumeDtoToFormAdapter.fromDto(data.items),
|
|
||||||
};
|
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const handlePageChange = (newPageIndex: number) => {
|
const handlePageChange = (newPageIndex: number) => {
|
||||||
@ -57,12 +62,14 @@ export const ProformaListPage = () => {
|
|||||||
setPageIndex(0);
|
setPageIndex(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isError || !invoicesPageData) {
|
console.log();
|
||||||
|
|
||||||
|
if (isError || !proformaPageData) {
|
||||||
return (
|
return (
|
||||||
<AppContent>
|
<AppContent>
|
||||||
<ErrorAlert
|
<ErrorAlert
|
||||||
message={(error as Error)?.message || "Error al cargar el listado"}
|
message={(error as Error)?.message || "Error al cargar el listado"}
|
||||||
title={t("pages.list.loadErrorTitle")}
|
title={t("pages.proformas.list.loadErrorTitle")}
|
||||||
/>
|
/>
|
||||||
<BackHistoryButton />
|
<BackHistoryButton />
|
||||||
</AppContent>
|
</AppContent>
|
||||||
@ -73,28 +80,28 @@ export const ProformaListPage = () => {
|
|||||||
<>
|
<>
|
||||||
<AppHeader>
|
<AppHeader>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
description={t("pages.list.description")}
|
description={t("pages.proformas.list.description")}
|
||||||
rightSlot={
|
rightSlot={
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Button
|
<Button
|
||||||
aria-label={t("pages.create.title")}
|
aria-label={t("pages.proformas.create.title")}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => navigate("/customer-invoices/create")}
|
onClick={() => navigate("/proformas/create")}
|
||||||
variant={"default"}
|
variant={"default"}
|
||||||
>
|
>
|
||||||
<PlusIcon aria-hidden className="mr-2 h-4 w-4" />
|
<PlusIcon aria-hidden className="mr-2 h-4 w-4" />
|
||||||
{t("pages.create.title")}
|
{t("pages.proformas.create.title")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
title={t("pages.list.title")}
|
title={t("pages.proformas.list.title")}
|
||||||
/>
|
/>
|
||||||
</AppHeader>
|
</AppHeader>
|
||||||
<AppContent>
|
<AppContent>
|
||||||
<div className="flex flex-col w-full h-full py-3">
|
<div className="flex flex-col w-full h-full py-3">
|
||||||
<div className={"flex-1"}>
|
<div className={"flex-1"}>
|
||||||
<ProformasGrid
|
<ProformasGrid
|
||||||
data={invoicesPageData}
|
data={proformaPageData}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
onPageSizeChange={handlePageSizeChange}
|
onPageSizeChange={handlePageSizeChange}
|
||||||
|
|||||||
@ -0,0 +1,196 @@
|
|||||||
|
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 "../../proforma-resume.form.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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,314 @@
|
|||||||
|
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 { CustomerInvoiceStatusBadge } from "../../../components";
|
||||||
|
import { useTranslation } from "../../../i18n";
|
||||||
|
import type { InvoiceSummaryFormData } from "../../../schemas";
|
||||||
|
|
||||||
|
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]
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
import { MoneyDTOHelper, PercentageDTOHelper, formatCurrency } from "@erp/core";
|
||||||
|
|
||||||
|
import type { ProformaSummaryPage } from "./proforma.api.schema";
|
||||||
|
import type { ProformaSummaryData, ProformaSummaryPageData } from "./proforma-resume.form.schema";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convierte el DTO completo de API a datos numéricos para el formulario.
|
||||||
|
*/
|
||||||
|
export const ProformaResumeDtoAdapter = {
|
||||||
|
fromDto(pageDto: ProformaSummaryPage, context?: unknown): ProformaSummaryPageData {
|
||||||
|
return {
|
||||||
|
...pageDto,
|
||||||
|
items: pageDto.items.map(
|
||||||
|
(summaryDto) =>
|
||||||
|
({
|
||||||
|
...summaryDto,
|
||||||
|
|
||||||
|
subtotal_amount: MoneyDTOHelper.toNumber(summaryDto.subtotal_amount),
|
||||||
|
subtotal_amount_fmt: formatCurrency(
|
||||||
|
MoneyDTOHelper.toNumber(summaryDto.subtotal_amount),
|
||||||
|
Number(summaryDto.total_amount.scale || 2),
|
||||||
|
summaryDto.currency_code,
|
||||||
|
summaryDto.language_code
|
||||||
|
),
|
||||||
|
|
||||||
|
discount_percentage: PercentageDTOHelper.toNumber(summaryDto.discount_percentage),
|
||||||
|
discount_percentage_fmt: PercentageDTOHelper.toNumericString(
|
||||||
|
summaryDto.discount_percentage
|
||||||
|
),
|
||||||
|
|
||||||
|
discount_amount: MoneyDTOHelper.toNumber(summaryDto.discount_amount),
|
||||||
|
discount_amount_fmt: formatCurrency(
|
||||||
|
MoneyDTOHelper.toNumber(summaryDto.discount_amount),
|
||||||
|
Number(summaryDto.total_amount.scale || 2),
|
||||||
|
summaryDto.currency_code,
|
||||||
|
summaryDto.language_code
|
||||||
|
),
|
||||||
|
|
||||||
|
taxable_amount: MoneyDTOHelper.toNumber(summaryDto.taxable_amount),
|
||||||
|
taxable_amount_fmt: formatCurrency(
|
||||||
|
MoneyDTOHelper.toNumber(summaryDto.taxable_amount),
|
||||||
|
Number(summaryDto.total_amount.scale || 2),
|
||||||
|
summaryDto.currency_code,
|
||||||
|
summaryDto.language_code
|
||||||
|
),
|
||||||
|
|
||||||
|
taxes_amount: MoneyDTOHelper.toNumber(summaryDto.taxes_amount),
|
||||||
|
taxes_amount_fmt: formatCurrency(
|
||||||
|
MoneyDTOHelper.toNumber(summaryDto.taxes_amount),
|
||||||
|
Number(summaryDto.total_amount.scale || 2),
|
||||||
|
summaryDto.currency_code,
|
||||||
|
summaryDto.language_code
|
||||||
|
),
|
||||||
|
|
||||||
|
total_amount: MoneyDTOHelper.toNumber(summaryDto.total_amount),
|
||||||
|
total_amount_fmt: formatCurrency(
|
||||||
|
MoneyDTOHelper.toNumber(summaryDto.total_amount),
|
||||||
|
Number(summaryDto.total_amount.scale || 2),
|
||||||
|
summaryDto.currency_code,
|
||||||
|
summaryDto.language_code
|
||||||
|
),
|
||||||
|
|
||||||
|
//taxes: dto.taxes,
|
||||||
|
}) as unknown as ProformaSummaryData
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import type { ProformaSummary, ProformaSummaryPage } from "./proforma.api.schema";
|
||||||
|
|
||||||
|
export type ProformaSummaryData = ProformaSummary & {
|
||||||
|
subtotal_amount_fmt: string;
|
||||||
|
subtotal_amount: number;
|
||||||
|
|
||||||
|
discount_percentage_fmt: string;
|
||||||
|
discount_percentage: number;
|
||||||
|
|
||||||
|
discount_amount_fmt: string;
|
||||||
|
discount_amount: number;
|
||||||
|
|
||||||
|
taxable_amount_fmt: string;
|
||||||
|
taxable_amount: number;
|
||||||
|
|
||||||
|
taxes_amoun_fmt: string;
|
||||||
|
taxes_amount: number;
|
||||||
|
|
||||||
|
total_amount_fmt: string;
|
||||||
|
total_amount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProformaSummaryPageData = ProformaSummaryPage & {
|
||||||
|
items: ProformaSummary[];
|
||||||
|
};
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import {
|
||||||
|
CreateProformaRequestSchema,
|
||||||
|
GetProformaByIdResponseSchema,
|
||||||
|
ListProformasResponseSchema,
|
||||||
|
UpdateProformaByIdRequestSchema,
|
||||||
|
} from "@erp/customer-invoices/common";
|
||||||
|
import type { ArrayElement } from "@repo/rdx-utils";
|
||||||
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
|
// Proformas
|
||||||
|
export const ProformaSchema = GetProformaByIdResponseSchema.omit({
|
||||||
|
metadata: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Proforma = z.infer<typeof ProformaSchema>;
|
||||||
|
export type ProformaRecipient = Proforma["recipient"];
|
||||||
|
export type ProformaItem = ArrayElement<Proforma["items"]>;
|
||||||
|
|
||||||
|
export const CreateProformaSchema = CreateProformaRequestSchema;
|
||||||
|
export const UpdateProformaSchema = UpdateProformaByIdRequestSchema;
|
||||||
|
|
||||||
|
export type CreateProformaInput = z.infer<typeof CreateProformaSchema>; // Cuerpo para crear
|
||||||
|
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 ProformaSummary = Omit<ArrayElement<ProformaSummaryPage["items"]>, "metadata">;
|
||||||
@ -1,35 +1,23 @@
|
|||||||
import type { PaginationSchema } from "@erp/core";
|
|
||||||
import type { ArrayElement } from "@repo/rdx-utils";
|
import type { ArrayElement } from "@repo/rdx-utils";
|
||||||
import type { z } from "zod/v4";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
import {
|
import { GetIssueInvoiceByIdResponseSchema, ListIssueInvoicesResponseSchema } from "../../common";
|
||||||
CreateCustomerInvoiceRequestSchema,
|
|
||||||
GetIssueInvoiceByIdResponseSchema,
|
|
||||||
ListIssueInvoicesResponseSchema,
|
|
||||||
UpdateCustomerInvoiceByIdRequestSchema,
|
|
||||||
} from "../../common";
|
|
||||||
|
|
||||||
export const CustomerInvoiceSchema = GetIssueInvoiceByIdResponseSchema.omit({
|
export const IssueInvoiceSchema = GetIssueInvoiceByIdResponseSchema.omit({
|
||||||
metadata: true,
|
metadata: true,
|
||||||
});
|
});
|
||||||
export const CustomerInvoiceCreateSchema = CreateCustomerInvoiceRequestSchema;
|
|
||||||
export const CustomerInvoiceUpdateSchema = UpdateCustomerInvoiceByIdRequestSchema;
|
|
||||||
|
|
||||||
// Tipos (derivados de Zod o DTOs del backend)
|
export type IssueInvoice = z.infer<typeof IssueInvoiceSchema>;
|
||||||
export type CustomerInvoice = z.infer<typeof CustomerInvoiceSchema>;
|
export type IssueInvoiceRecipient = IssueInvoice["recipient"];
|
||||||
export type CustomerInvoiceRecipient = CustomerInvoice["recipient"];
|
export type IssueInvoiceItem = ArrayElement<IssueInvoice["items"]>;
|
||||||
export type CustomerInvoiceItem = ArrayElement<CustomerInvoice["items"]>;
|
|
||||||
|
|
||||||
export type CustomerInvoiceCreateInput = z.infer<typeof CustomerInvoiceCreateSchema>; // Cuerpo para crear
|
|
||||||
export type CustomerInvoiceUpdateInput = z.infer<typeof CustomerInvoiceUpdateSchema>; // Cuerpo para actualizar
|
|
||||||
|
|
||||||
// Resultado de consulta con criteria (paginado, etc.)
|
// Resultado de consulta con criteria (paginado, etc.)
|
||||||
export const CustomerInvoicesPageSchema = ListIssueInvoicesResponseSchema.omit({
|
export const IssueInvoicesPageSchema = ListIssueInvoicesResponseSchema.omit({
|
||||||
metadata: true,
|
metadata: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type PaginatedResponse = z.infer<typeof PaginationSchema>;
|
//export type PaginatedResponse = z.infer<typeof PaginationSchema>;
|
||||||
export type CustomerInvoicesPage = z.infer<typeof CustomerInvoicesPageSchema>;
|
//export type CustomerInvoicesPage = z.infer<typeof CustomerInvoicesPageSchema>;
|
||||||
|
|
||||||
// Ítem simplificado dentro del listado (no toda la entidad)
|
// Ítem simplificado dentro del listado (no toda la entidad)
|
||||||
export type CustomerInvoiceSummary = Omit<ArrayElement<CustomerInvoicesPage["items"]>, "metadata">;
|
//export type CustomerInvoiceSummary = Omit<ArrayElement<CustomerInvoicesPage["items"]>, "metadata">;
|
||||||
|
|||||||
@ -1,51 +1,43 @@
|
|||||||
import { Badge } from "@repo/shadcn-ui/components";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@repo/shadcn-ui/components";
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import { forwardRef } from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "../i18n";
|
|
||||||
|
|
||||||
export type CustomerStatus = "active" | "inactive";
|
export type CustomerStatus = "active" | "inactive" | "error";
|
||||||
|
|
||||||
export type CustomerStatusBadgeProps = {
|
export type CustomerStatusBadgeProps = {
|
||||||
status: string; // permitir cualquier valor
|
status: string; // permitir cualquier valor
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusColorConfig: Record<CustomerStatus, { badge: string; dot: string }> = {
|
const statusColorConfig: Record<CustomerStatus, string> = {
|
||||||
inactive: {
|
inactive: "text-gray-400 bg-gray-100 dark:text-gray-500 dark:bg-gray-100/10",
|
||||||
badge:
|
active: "text-green-500 bg-green-500/10 dark:text-green-400 dark:bg-green-400/10",
|
||||||
"bg-gray-600/10 dark:bg-gray-600/20 hover:bg-gray-600/10 text-gray-500 border-gray-600/60",
|
error: "text-rose-500 bg-rose-500/10 dark:text-rose-400 dark:bg-rose-400/10",
|
||||||
dot: "bg-gray-500",
|
|
||||||
},
|
|
||||||
active: {
|
|
||||||
badge:
|
|
||||||
"bg-emerald-600/10 dark:bg-emerald-600/20 hover:bg-emerald-600/10 text-emerald-500 border-emerald-600/60",
|
|
||||||
dot: "bg-emerald-500",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CustomerStatusBadge = forwardRef<HTMLDivElement, CustomerStatusBadgeProps>(
|
export const CustomerStatusBadge = ({ status }: { status: string }) => {
|
||||||
({ status, className, ...props }, ref) => {
|
// Map visual simple; ajustar a tu catálogo real
|
||||||
const { t } = useTranslation();
|
const statusClass = React.useMemo(
|
||||||
const normalizedStatus = status.toLowerCase() as CustomerStatus;
|
() =>
|
||||||
const config = statusColorConfig[normalizedStatus];
|
status.toLowerCase() === "active" ? statusColorConfig.active : statusColorConfig.inactive,
|
||||||
const commonClassName =
|
[status]
|
||||||
"transition-colors duration-200 cursor-pointer shadow-none rounded-full";
|
);
|
||||||
|
const contentTxt = React.useMemo(
|
||||||
|
() =>
|
||||||
|
status.toLowerCase() === "active" ? "El cliente está activo" : "El cliente está inactivo",
|
||||||
|
[status]
|
||||||
|
);
|
||||||
|
|
||||||
if (!config) {
|
return (
|
||||||
return (
|
<Tooltip>
|
||||||
<Badge ref={ref} className={cn(commonClassName, className)} {...props}>
|
<TooltipTrigger asChild>
|
||||||
{status}
|
<div className={cn("flex-none rounded-full p-1", statusClass)}>
|
||||||
</Badge>
|
<div className="size-2 rounded-full bg-current" />
|
||||||
);
|
</div>
|
||||||
}
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{contentTxt}</TooltipContent>
|
||||||
return (
|
</Tooltip>
|
||||||
<Badge className={cn(commonClassName, config.badge, className)} {...props}>
|
);
|
||||||
<div className={cn("h-1.5 w-1.5 rounded-full mr-2", config.dot)} />
|
};
|
||||||
{t(`catalog.status.${status}`)}
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
CustomerStatusBadge.displayName = "CustomerStatusBadge";
|
CustomerStatusBadge.displayName = "CustomerStatusBadge";
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
export * from "./client-selector-modal";
|
export * from "./client-selector-modal";
|
||||||
export * from "./customer-modal-selector";
|
export * from "./customer-modal-selector";
|
||||||
|
export * from "./customer-status-badge";
|
||||||
export * from "./customers-layout";
|
export * from "./customers-layout";
|
||||||
export * from "./editor";
|
export * from "./editor";
|
||||||
export * from "./error-alert";
|
export * from "./error-alert";
|
||||||
|
|||||||
@ -1,21 +1,30 @@
|
|||||||
import { DataTableColumnHeader } from '@repo/rdx-ui/components';
|
import { DataTableColumnHeader } from "@repo/rdx-ui/components";
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
AvatarFallback,
|
AvatarFallback,
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
DropdownMenu, DropdownMenuContent,
|
DropdownMenu,
|
||||||
DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger,
|
DropdownMenuContent,
|
||||||
Tooltip,
|
DropdownMenuItem,
|
||||||
TooltipContent,
|
DropdownMenuLabel,
|
||||||
TooltipTrigger
|
DropdownMenuSeparator,
|
||||||
} from '@repo/shadcn-ui/components';
|
DropdownMenuTrigger,
|
||||||
import { cn } from '@repo/shadcn-ui/lib/utils';
|
} from "@repo/shadcn-ui/components";
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import { Building2Icon, GlobeIcon, MailIcon, MoreHorizontalIcon, PencilIcon, PhoneIcon, User2Icon } from 'lucide-react';
|
import {
|
||||||
|
Building2Icon,
|
||||||
|
MailIcon,
|
||||||
|
MoreHorizontalIcon,
|
||||||
|
PencilIcon,
|
||||||
|
PhoneIcon,
|
||||||
|
User2Icon,
|
||||||
|
} from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useTranslation } from '../../i18n';
|
|
||||||
import { CustomerSummaryFormData } from '../../schemas';
|
import { CustomerStatusBadge } from "../../components";
|
||||||
|
import { useTranslation } from "../../i18n";
|
||||||
|
import type { CustomerSummaryFormData } from "../../schemas";
|
||||||
|
|
||||||
type CustomerActionHandlers = {
|
type CustomerActionHandlers = {
|
||||||
onEdit?: (customer: CustomerSummaryFormData) => void;
|
onEdit?: (customer: CustomerSummaryFormData) => void;
|
||||||
@ -27,33 +36,8 @@ function shortId(id: string) {
|
|||||||
return id ? `${id.slice(0, 4)}_${id.slice(-4)}` : "-";
|
return id ? `${id.slice(0, 4)}_${id.slice(-4)}` : "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
const statuses = {
|
|
||||||
inactive: 'text-gray-400 bg-gray-100 dark:text-gray-500 dark:bg-gray-100/10',
|
|
||||||
active: 'text-green-500 bg-green-500/10 dark:text-green-400 dark:bg-green-400/10',
|
|
||||||
error: 'text-rose-500 bg-rose-500/10 dark:text-rose-400 dark:bg-rose-400/10',
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Helpers UI ----
|
|
||||||
const StatusBadge = ({ status }: { status: string }) => {
|
|
||||||
// Map visual simple; ajustar a tu catálogo real
|
|
||||||
const statusClass = React.useMemo(() => status.toLowerCase() === 'active' ? statuses.active : statuses.inactive, [status]);
|
|
||||||
const contentTxt = React.useMemo(() => status.toLowerCase() === 'active' ? 'El cliente está activo' : 'El cliente está inactivo', [status]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className={cn('flex-none rounded-full p-1', statusClass)}>
|
|
||||||
<div className="size-2 rounded-full bg-current" />
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>{contentTxt}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
const KindBadge = ({ isCompany }: { isCompany: boolean }) => (
|
const KindBadge = ({ isCompany }: { isCompany: boolean }) => (
|
||||||
<Badge variant="outline" className="gap-1 tracking-wide text-xs text-foreground/70">
|
<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 ? <Building2Icon className="size-3.5" /> : <User2Icon className="size-3.5" />}
|
||||||
{isCompany ? "Company" : "Person"}
|
{isCompany ? "Company" : "Person"}
|
||||||
</Badge>
|
</Badge>
|
||||||
@ -65,18 +49,20 @@ const Soft = ({ children }: { children: React.ReactNode }) => (
|
|||||||
|
|
||||||
const ContactCell = ({ customer }: { customer: CustomerSummaryFormData }) => (
|
const ContactCell = ({ customer }: { customer: CustomerSummaryFormData }) => (
|
||||||
<div className="grid gap-1 text-foreground text-sm my-1.5">
|
<div className="grid gap-1 text-foreground text-sm my-1.5">
|
||||||
|
|
||||||
{customer.email_primary && (
|
{customer.email_primary && (
|
||||||
<div className='flex items-center gap-2'>
|
<div className="flex items-center gap-2">
|
||||||
<MailIcon className='size-3.5' />
|
<MailIcon className="size-3.5" />
|
||||||
<a className='group' href={`mailto:${customer.email_primary}`}>
|
<a className="group" href={`mailto:${customer.email_primary}`}>
|
||||||
{customer.email_primary}
|
{customer.email_primary}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{customer.email_secondary && (
|
{customer.email_secondary && (
|
||||||
<div className="flex items-center gap-2"><MailIcon className="size-3.5" />{customer.email_secondary}</div>
|
<div className="flex items-center gap-2">
|
||||||
|
<MailIcon className="size-3.5" />
|
||||||
|
{customer.email_secondary}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
@ -84,16 +70,9 @@ const ContactCell = ({ customer }: { customer: CustomerSummaryFormData }) => (
|
|||||||
<span>{customer.phone_primary || customer.mobile_primary || <Soft>-</Soft>}</span>
|
<span>{customer.phone_primary || customer.mobile_primary || <Soft>-</Soft>}</span>
|
||||||
{customer.phone_secondary && <Soft>• {customer.phone_secondary}</Soft>}
|
{customer.phone_secondary && <Soft>• {customer.phone_secondary}</Soft>}
|
||||||
{customer.mobile_secondary && <Soft>• {customer.mobile_secondary}</Soft>}
|
{customer.mobile_secondary && <Soft>• {customer.mobile_secondary}</Soft>}
|
||||||
{false && customer.fax && <Soft>• fax {customer.fax}</Soft>}
|
{false}
|
||||||
</div>
|
</div>
|
||||||
{false && customer.website && (
|
{false}
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
|
||||||
<GlobeIcon className="size-3.5" />
|
|
||||||
<a className="underline underline-offset-2" href={safeHttp(customer.website)} target="_blank" rel="noreferrer">
|
|
||||||
{customer.website}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -111,7 +90,7 @@ const AddressCell = ({ c }: { c: CustomerSummaryFormData }) => {
|
|||||||
|
|
||||||
function initials(name: string) {
|
function initials(name: string) {
|
||||||
const parts = name.trim().split(/\s+/).slice(0, 2);
|
const parts = name.trim().split(/\s+/).slice(0, 2);
|
||||||
return parts.map(p => p[0]?.toUpperCase() ?? "").join("") || "?";
|
return parts.map((p) => p[0]?.toUpperCase() ?? "").join("") || "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeHttp(url: string) {
|
function safeHttp(url: string) {
|
||||||
@ -120,127 +99,140 @@ function safeHttp(url: string) {
|
|||||||
return `https://${url}`;
|
return `https://${url}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function useCustomersListColumns(
|
export function useCustomersListColumns(
|
||||||
handlers: CustomerActionHandlers = {}
|
handlers: CustomerActionHandlers = {}
|
||||||
): ColumnDef<CustomerSummaryFormData>[] {
|
): ColumnDef<CustomerSummaryFormData>[] {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
const { onEdit, onView, onDelete } = handlers;
|
||||||
onEdit, onView, onDelete,
|
|
||||||
} = handlers;
|
|
||||||
|
|
||||||
return React.useMemo<ColumnDef<CustomerSummaryFormData>[]>(() => [
|
return React.useMemo<ColumnDef<CustomerSummaryFormData>[]>(
|
||||||
// Identidad + estado + metadatos (columna compuesta)
|
() => [
|
||||||
{
|
// Identidad + estado + metadatos (columna compuesta)
|
||||||
id: "customer",
|
{
|
||||||
header: ({ column }) => (
|
id: "customer",
|
||||||
<DataTableColumnHeader column={column} title={t("pages.list.grid_columns.customer")} className="text-left" />
|
header: ({ column }) => (
|
||||||
),
|
<DataTableColumnHeader
|
||||||
accessorFn: (row) => row.name, // para ordenar/buscar por nombre
|
className="text-left"
|
||||||
enableHiding: false,
|
column={column}
|
||||||
size: 140,
|
title={t("pages.list.grid_columns.customer")}
|
||||||
minSize: 120,
|
/>
|
||||||
cell: ({ row }) => {
|
),
|
||||||
const c = row.original;
|
accessorFn: (row) => row.name, // para ordenar/buscar por nombre
|
||||||
const isCompany = String(c.is_company).toLowerCase() === "true";
|
enableHiding: false,
|
||||||
return (
|
size: 140,
|
||||||
<div className="flex items-start gap-1 my-1.5">
|
minSize: 120,
|
||||||
<Avatar className="size-10 hidden">
|
cell: ({ row }) => {
|
||||||
<AvatarFallback aria-label={c.name}>{initials(c.name)}</AvatarFallback>
|
const c = row.original;
|
||||||
</Avatar>
|
const isCompany = String(c.is_company).toLowerCase() === "true";
|
||||||
<div className="min-w-0 grid gap-1">
|
return (
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex items-start gap-1 my-1.5">
|
||||||
<StatusBadge status={c.status} /> <span className="font-medium truncate text-primary">{c.name}</span>
|
<Avatar className="size-10 hidden">
|
||||||
{c.trade_name && <Soft>({c.trade_name})</Soft>}
|
<AvatarFallback aria-label={c.name}>{initials(c.name)}</AvatarFallback>
|
||||||
</div>
|
</Avatar>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="min-w-0 grid gap-1">
|
||||||
{c.tin && <span className="font-base truncate">{c.tin}</span>}
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<KindBadge isCompany={isCompany} />
|
<CustomerStatusBadge status={c.status} />{" "}
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
|
|
||||||
// Contacto (emails, teléfonos, web)
|
// Contacto (emails, teléfonos, web)
|
||||||
{
|
{
|
||||||
id: "contact",
|
id: "contact",
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
<DataTableColumnHeader column={column} title={t("pages.list.grid_columns.contact")} className="text-left" />
|
<DataTableColumnHeader
|
||||||
),
|
className="text-left"
|
||||||
accessorFn: (r) => `${r.email_primary} ${r.phone_primary} ${r.mobile_primary} ${r.website}`,
|
column={column}
|
||||||
size: 140,
|
title={t("pages.list.grid_columns.contact")}
|
||||||
minSize: 120,
|
/>
|
||||||
cell: ({ row }) => <ContactCell customer={row.original} />,
|
),
|
||||||
},
|
accessorFn: (r) => `${r.email_primary} ${r.phone_primary} ${r.mobile_primary} ${r.website}`,
|
||||||
|
size: 140,
|
||||||
|
minSize: 120,
|
||||||
|
cell: ({ row }) => <ContactCell customer={row.original} />,
|
||||||
|
},
|
||||||
|
|
||||||
// Dirección (múltiples campos en bloque)
|
// Dirección (múltiples campos en bloque)
|
||||||
{
|
{
|
||||||
id: "address",
|
id: "address",
|
||||||
header: t("pages.list.grid_columns.address"),
|
header: t("pages.list.grid_columns.address"),
|
||||||
accessorFn: (r) =>
|
accessorFn: (r) =>
|
||||||
`${r.street} ${r.street2} ${r.city} ${r.postal_code} ${r.province} ${r.country}`,
|
`${r.street} ${r.street2} ${r.city} ${r.postal_code} ${r.province} ${r.country}`,
|
||||||
size: 140,
|
size: 140,
|
||||||
minSize: 120,
|
minSize: 120,
|
||||||
cell: ({ row }) => <AddressCell c={row.original} />,
|
cell: ({ row }) => <AddressCell c={row.original} />,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Acciones
|
// Acciones
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
<DataTableColumnHeader column={column} title={t("pages.list.grid_columns.actions")} className="text-right" />
|
<DataTableColumnHeader
|
||||||
),
|
className="text-right"
|
||||||
size: 64,
|
column={column}
|
||||||
minSize: 64,
|
title={t("pages.list.grid_columns.actions")}
|
||||||
enableSorting: false,
|
/>
|
||||||
enableHiding: false,
|
),
|
||||||
cell: ({ row }) => {
|
size: 64,
|
||||||
const customer = row.original;
|
minSize: 64,
|
||||||
const { website, email_primary } = customer;
|
enableSorting: false,
|
||||||
return (
|
enableHiding: false,
|
||||||
<div className="flex justify-end">
|
cell: ({ row }) => {
|
||||||
<div className="flex items-center gap-1">
|
const customer = row.original;
|
||||||
<Button
|
const { website, email_primary } = customer;
|
||||||
variant="ghost"
|
return (
|
||||||
size="icon"
|
<div className="flex justify-end">
|
||||||
aria-label="Edit customer"
|
<div className="flex items-center gap-1">
|
||||||
onClick={() => onEdit?.(customer)}
|
<Button
|
||||||
>
|
aria-label="Edit customer"
|
||||||
<PencilIcon className="size-4" />
|
onClick={() => onEdit?.(customer)}
|
||||||
</Button>
|
size="icon"
|
||||||
<DropdownMenu>
|
variant="ghost"
|
||||||
<DropdownMenuTrigger asChild>
|
>
|
||||||
<Button variant="ghost" size="icon" aria-label="More actions">
|
<PencilIcon className="size-4" />
|
||||||
<MoreHorizontalIcon className="size-4" />
|
</Button>
|
||||||
</Button>
|
<DropdownMenu>
|
||||||
</DropdownMenuTrigger>
|
<DropdownMenuTrigger asChild>
|
||||||
<DropdownMenuContent align="end">
|
<Button aria-label="More actions" size="icon" variant="ghost">
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
<MoreHorizontalIcon className="size-4" />
|
||||||
<DropdownMenuSeparator />
|
</Button>
|
||||||
<DropdownMenuItem onClick={() => onView?.(customer)}>Open</DropdownMenuItem>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuItem onClick={() => onEdit?.(customer)}>Edit</DropdownMenuItem>
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
<DropdownMenuItem onClick={() => window.open(safeHttp(website), "_blank")}>
|
<DropdownMenuSeparator />
|
||||||
Visit website
|
<DropdownMenuItem onClick={() => onView?.(customer)}>Open</DropdownMenuItem>
|
||||||
</DropdownMenuItem>
|
<DropdownMenuItem onClick={() => onEdit?.(customer)}>Edit</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(email_primary)}>
|
<DropdownMenuSeparator />
|
||||||
Copy email
|
<DropdownMenuItem onClick={() => window.open(safeHttp(website), "_blank")}>
|
||||||
</DropdownMenuItem>
|
Visit website
|
||||||
<DropdownMenuSeparator />
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(email_primary)}>
|
||||||
className="text-destructive"
|
Copy email
|
||||||
onClick={() => onDelete?.(customer)}
|
</DropdownMenuItem>
|
||||||
>
|
<DropdownMenuSeparator />
|
||||||
Delete
|
<DropdownMenuItem
|
||||||
</DropdownMenuItem>
|
className="text-destructive"
|
||||||
</DropdownMenuContent>
|
onClick={() => onDelete?.(customer)}
|
||||||
</DropdownMenu>
|
>
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
], [t, onEdit, onView, onDelete]);
|
[t, onEdit, onView, onDelete]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,23 @@ export class CriteriaToSequelizeConverter implements ICriteriaToOrmConverter {
|
|||||||
this.applyOrder(options, criteria, mappings, params);
|
this.applyOrder(options, criteria, mappings, params);
|
||||||
this.applyPagination(options, criteria);
|
this.applyPagination(options, criteria);
|
||||||
|
|
||||||
return options;
|
const normalizedOrder = Array.isArray(options.order)
|
||||||
|
? options.order
|
||||||
|
: options.order
|
||||||
|
? [options.order]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const normalizedInclude = Array.isArray(options.include)
|
||||||
|
? options.include
|
||||||
|
: options.include
|
||||||
|
? [options.include]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
order: normalizedOrder,
|
||||||
|
include: normalizedInclude,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Filtros simples (sin anidaciones complejas) */
|
/** Filtros simples (sin anidaciones complejas) */
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FindOptions } from "sequelize";
|
import type { FindOptions } from "sequelize";
|
||||||
|
|
||||||
// orderItem puede ser: ['campo', 'ASC'|'DESC']
|
// orderItem puede ser: ['campo', 'ASC'|'DESC']
|
||||||
// o [Sequelize.literal('score'), 'DESC']
|
// o [Sequelize.literal('score'), 'DESC']
|
||||||
|
|||||||
@ -1,31 +1,3 @@
|
|||||||
import {
|
|
||||||
AudioWaveform,
|
|
||||||
Command,
|
|
||||||
FileCheckIcon,
|
|
||||||
Frame,
|
|
||||||
GalleryVerticalEnd,
|
|
||||||
HomeIcon,
|
|
||||||
MapIcon,
|
|
||||||
PieChart,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
BarChartIcon,
|
|
||||||
CameraIcon,
|
|
||||||
ClipboardListIcon,
|
|
||||||
DatabaseIcon,
|
|
||||||
FileCodeIcon,
|
|
||||||
FileIcon,
|
|
||||||
FileTextIcon,
|
|
||||||
FolderIcon,
|
|
||||||
HelpCircleIcon,
|
|
||||||
LayoutDashboardIcon,
|
|
||||||
ListIcon,
|
|
||||||
SettingsIcon,
|
|
||||||
UsersIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@ -33,6 +5,31 @@ import {
|
|||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
SidebarRail,
|
SidebarRail,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
|
import {
|
||||||
|
AudioWaveform,
|
||||||
|
BarChartIcon,
|
||||||
|
CameraIcon,
|
||||||
|
ClipboardListIcon,
|
||||||
|
Command,
|
||||||
|
DatabaseIcon,
|
||||||
|
FileCheckIcon,
|
||||||
|
FileCodeIcon,
|
||||||
|
FileIcon,
|
||||||
|
FileTextIcon,
|
||||||
|
FolderIcon,
|
||||||
|
Frame,
|
||||||
|
GalleryVerticalEnd,
|
||||||
|
HelpCircleIcon,
|
||||||
|
HomeIcon,
|
||||||
|
LayoutDashboardIcon,
|
||||||
|
ListIcon,
|
||||||
|
MapIcon,
|
||||||
|
PieChart,
|
||||||
|
SettingsIcon,
|
||||||
|
UsersIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import type * as React from "react";
|
||||||
|
|
||||||
import { NavMain } from "./nav-main.tsx";
|
import { NavMain } from "./nav-main.tsx";
|
||||||
import { NavSecondary } from "./nav-secondary.tsx";
|
import { NavSecondary } from "./nav-secondary.tsx";
|
||||||
import { NavUser } from "./nav-user.tsx";
|
import { NavUser } from "./nav-user.tsx";
|
||||||
@ -203,7 +200,7 @@ const data2 = {
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "Listado de proformas",
|
title: "Listado de proformas",
|
||||||
url: "/customer-proforma",
|
url: "/proformas",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Enviar a Veri*Factu",
|
title: "Enviar a Veri*Factu",
|
||||||
@ -243,8 +240,8 @@ const data2 = {
|
|||||||
|
|
||||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible='icon' {...props}>
|
<Sidebar collapsible="icon" {...props}>
|
||||||
<SidebarHeader className='mb-3'>
|
<SidebarHeader className="mb-3">
|
||||||
<TeamSwitcher teams={data2.teams} />
|
<TeamSwitcher teams={data2.teams} />
|
||||||
<SearchForm />
|
<SearchForm />
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
@ -252,7 +249,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||||||
<NavMain items={data2.navMain} />
|
<NavMain items={data2.navMain} />
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<NavSecondary items={data.navSecondary} className='mt-auto' />
|
<NavSecondary className="mt-auto" items={data.navSecondary} />
|
||||||
<NavUser user={data.user} />
|
<NavUser user={data.user} />
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
<SidebarRail />
|
<SidebarRail />
|
||||||
|
|||||||
@ -65,7 +65,7 @@
|
|||||||
--shadow-xl: 1px 1px 6px 0px hsl(0 0% 0% / 0.1), 1px 8px 10px -1px hsl(0 0% 0% / 0.1);
|
--shadow-xl: 1px 1px 6px 0px hsl(0 0% 0% / 0.1), 1px 8px 10px -1px hsl(0 0% 0% / 0.1);
|
||||||
--shadow-2xl: 1px 1px 6px 0px hsl(0 0% 0% / 0.25);
|
--shadow-2xl: 1px 1px 6px 0px hsl(0 0% 0% / 0.25);
|
||||||
--tracking-normal: 0rem;
|
--tracking-normal: 0rem;
|
||||||
--spacing: 0.20rem;
|
--spacing: 0.22rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user