.
This commit is contained in:
parent
1c66d20c24
commit
99c161cb31
@ -11,6 +11,12 @@
|
||||
"placeholder": "Select taxes",
|
||||
"description": "Select the taxes to apply to the invoice items",
|
||||
"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": {
|
||||
|
||||
@ -10,6 +10,12 @@
|
||||
"placeholder": "Seleccionar impuestos",
|
||||
"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."
|
||||
},
|
||||
"simple_search_input": {
|
||||
"search_button": "Buscar",
|
||||
"loading": "Buscando",
|
||||
"clear_search": "Limpiar búsquedaClear search",
|
||||
"search_placeholder": "Escribe aquí para buscar..."
|
||||
}
|
||||
},
|
||||
"hooks": {
|
||||
|
||||
@ -8,7 +8,8 @@ import {
|
||||
import { Spinner } from "@repo/shadcn-ui/components/spinner";
|
||||
import { SearchIcon, XIcon } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useTranslation } from "../../i18n";
|
||||
|
||||
type SimpleSearchInputProps = {
|
||||
onSearchChange: (value: string) => void;
|
||||
@ -103,44 +104,48 @@ export const SimpleSearchInput = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='relative flex-1 max-w-xl'>
|
||||
<InputGroup className='bg-background' data-disabled={loading}>
|
||||
<div className="relative flex-1 max-w-xl">
|
||||
<InputGroup className="bg-background" data-disabled={loading}>
|
||||
<InputGroupInput
|
||||
ref={inputRef}
|
||||
placeholder={t("common.search_placeholder", "Search...")}
|
||||
value={searchValue}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
inputMode='search'
|
||||
autoComplete='off'
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
disabled={loading}
|
||||
inputMode="search"
|
||||
onChange={handleInputChange}
|
||||
onFocus={() => history.length > 0 && setOpen(true)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={t("components.simple_search_input.search_placeholder", "Search...")}
|
||||
ref={inputRef}
|
||||
spellCheck={false}
|
||||
value={searchValue}
|
||||
/>
|
||||
<InputGroupAddon>
|
||||
<SearchIcon aria-hidden />
|
||||
</InputGroupAddon>
|
||||
|
||||
<InputGroupAddon align='inline-end'>
|
||||
{loading && <Spinner aria-label={t("common.loading", "Loading")} />}
|
||||
{!searchValue && !loading && (
|
||||
<InputGroupAddon align="inline-end">
|
||||
{loading && (
|
||||
<Spinner aria-label={t("components.simple_search_input.loading", "Loading")} />
|
||||
)}
|
||||
{!(searchValue || loading) && (
|
||||
<InputGroupButton
|
||||
variant='secondary'
|
||||
className='cursor-pointer'
|
||||
className="cursor-pointer"
|
||||
onClick={() => onSearchChange(searchValue)}
|
||||
variant="secondary"
|
||||
>
|
||||
{t("common.search", "Search")}
|
||||
{t("components.simple_search_input.search_button", "Search")}
|
||||
</InputGroupButton>
|
||||
)}
|
||||
{searchValue && !loading && (
|
||||
<InputGroupButton
|
||||
variant='secondary'
|
||||
className='cursor-pointer'
|
||||
aria-label={t("common.clear", "Clear search")}
|
||||
aria-label={t("components.simple_search_input.clear_search", "Clear search")}
|
||||
className="cursor-pointer"
|
||||
onClick={handleClear}
|
||||
variant="secondary"
|
||||
>
|
||||
<XIcon className='size-4' aria-hidden />
|
||||
<span className='sr-only'>{t("common.clear", "Clear")}</span>
|
||||
<XIcon aria-hidden className="size-4" />
|
||||
<span className="sr-only">
|
||||
{t("components.simple_search_input.clear_search", "Clear")}
|
||||
</span>
|
||||
</InputGroupButton>
|
||||
)}
|
||||
</InputGroupAddon>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ResponseType } from "axios";
|
||||
import type { ResponseType } from "axios";
|
||||
|
||||
export interface ICustomParams {
|
||||
url?: string;
|
||||
@ -7,7 +7,7 @@ export interface ICustomParams {
|
||||
signal?: AbortSignal;
|
||||
responseType?: ResponseType;
|
||||
headers?: {
|
||||
[key: string]: any;
|
||||
[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.
|
||||
*
|
||||
@ -183,12 +203,16 @@ export class CustomerInvoiceApplicationService {
|
||||
* @param transaction - Transacción activa para la operación.
|
||||
* @returns Result<Collection<CustomerInvoiceListDTO>, Error> - Colección de facturas o error.
|
||||
*/
|
||||
async findInvoiceByCriteriaInCompany(
|
||||
async findIssueInvoiceByCriteriaInCompany(
|
||||
companyId: UniqueID,
|
||||
criteria: Criteria,
|
||||
transaction?: Transaction
|
||||
): 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) => {
|
||||
try {
|
||||
const result = await this.service.findInvoiceByCriteriaInCompany(
|
||||
const result = await this.service.findProformasByCriteriaInCompany(
|
||||
companyId,
|
||||
criteria,
|
||||
transaction
|
||||
|
||||
@ -41,7 +41,7 @@ export interface ICustomerInvoiceRepository {
|
||||
): 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.
|
||||
*/
|
||||
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).
|
||||
* El resultado está encapsulado en un objeto `Collection<T>`.
|
||||
*
|
||||
* @param companyId - ID de la empresa.
|
||||
* @param criteria - Criterios de búsqueda.
|
||||
* @param transaction - Transacción activa para la operación.
|
||||
* @param options - Opciones adicionales para la consulta (Sequelize FindOptions)
|
||||
* @returns Result<Collection<CustomerInvoiceListDTO>, Error>
|
||||
*
|
||||
* @see Criteria
|
||||
@ -67,7 +68,8 @@ export interface ICustomerInvoiceRepository {
|
||||
findByCriteriaInCompany(
|
||||
companyId: UniqueID,
|
||||
criteria: Criteria,
|
||||
transaction: unknown
|
||||
transaction: unknown,
|
||||
options: unknown
|
||||
): Promise<Result<Collection<CustomerInvoiceListDTO>, Error>>;
|
||||
|
||||
/**
|
||||
|
||||
@ -51,7 +51,7 @@ export const issueInvoicesRouter = (params: ModuleParams) => {
|
||||
//checkTabContext,
|
||||
validateRequest(ListIssueInvoicesRequestSchema, "params"),
|
||||
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 */);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
@ -62,7 +62,7 @@ export const issueInvoicesRouter = (params: ModuleParams) => {
|
||||
//checkTabContext,
|
||||
validateRequest(GetIssueInvoiceByIdRequestSchema, "params"),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.useCases.get();
|
||||
const useCase = deps.useCases.get_proforma();
|
||||
const controller = new GetProformaController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
@ -73,7 +73,7 @@ export const issueInvoicesRouter = (params: ModuleParams) => {
|
||||
//checkTabContext,
|
||||
validateRequest(ReportIssueInvoiceByIdRequestSchema, "params"),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.useCases.report();
|
||||
const useCase = deps.useCases.report_proforma();
|
||||
const controller = new ReportProformaController(useCase);
|
||||
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 {
|
||||
CurrencyCode,
|
||||
extractOrPushError,
|
||||
LanguageCode,
|
||||
maybeFromNullableVO,
|
||||
Percentage,
|
||||
UniqueID,
|
||||
UtcDate,
|
||||
ValidationErrorCollection,
|
||||
ValidationErrorDetail,
|
||||
type ValidationErrorDetail,
|
||||
extractOrPushError,
|
||||
maybeFromNullableVO,
|
||||
} from "@repo/rdx-ddd";
|
||||
import { type Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
import {
|
||||
CustomerInvoiceNumber,
|
||||
CustomerInvoiceSerie,
|
||||
CustomerInvoiceStatus,
|
||||
InvoiceAmount,
|
||||
InvoiceRecipient,
|
||||
type InvoiceRecipient,
|
||||
} from "../../../domain";
|
||||
import { CustomerInvoiceModel } from "../../sequelize";
|
||||
import type { CustomerInvoiceModel } from "../../sequelize";
|
||||
|
||||
import { InvoiceRecipientListMapper } from "./invoice-recipient.list.mapper";
|
||||
|
||||
export type CustomerInvoiceListDTO = {
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
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 {
|
||||
CustomerInvoice,
|
||||
@ -293,7 +293,8 @@ export class CustomerInvoiceRepository
|
||||
public async findByCriteriaInCompany(
|
||||
companyId: UniqueID,
|
||||
criteria: Criteria,
|
||||
transaction: Transaction
|
||||
transaction: Transaction,
|
||||
options: FindOptions<InferAttributes<CustomerInvoiceModel>> = {}
|
||||
): Promise<Result<Collection<CustomerInvoiceListDTO>, Error>> {
|
||||
const { CustomerModel } = this._database.models;
|
||||
|
||||
@ -315,13 +316,30 @@ export class CustomerInvoiceRepository
|
||||
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,
|
||||
company_id: companyId.toString(),
|
||||
deleted_at: null,
|
||||
...(options.where ?? {}),
|
||||
};
|
||||
|
||||
query.order = [...(query.order as OrderItem[]), ...normalizedOrder];
|
||||
|
||||
query.include = [
|
||||
...normalizedInclude,
|
||||
{
|
||||
model: CustomerModel,
|
||||
as: "current_customer",
|
||||
@ -338,7 +356,6 @@ export class CustomerInvoiceRepository
|
||||
"country",
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
model: CustomerInvoiceTaxModel,
|
||||
as: "taxes",
|
||||
|
||||
@ -31,61 +31,63 @@
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"title": "Customer invoices",
|
||||
"description": "Manage your customer invoices",
|
||||
"list": {
|
||||
"title": "Customer invoices",
|
||||
"description": "List all customer invoices",
|
||||
"grid_columns": {
|
||||
"invoice_number": "Inv. number",
|
||||
"series": "Serie",
|
||||
"status": "Status",
|
||||
"invoice_date": "Invoice date",
|
||||
"operation_date": "Operation date",
|
||||
"recipient_tin": "TIN",
|
||||
"recipient_name": "Customer name",
|
||||
"recipient_street": "Street",
|
||||
"recipient_city": "City",
|
||||
"recipient_province": "Province",
|
||||
"recipient_postal_code": "Postal code",
|
||||
"recipient_country": "Country",
|
||||
"total_amount": "Total price"
|
||||
"proformas": {
|
||||
"title": "Proformas",
|
||||
"description": "Manage your customer proformas",
|
||||
"list": {
|
||||
"title": "Customer proformas",
|
||||
"description": "List all customer proformas",
|
||||
"grid_columns": {
|
||||
"invoice_number": "Inv. number",
|
||||
"series": "Serie",
|
||||
"status": "Status",
|
||||
"invoice_date": "Proforma date",
|
||||
"operation_date": "Operation date",
|
||||
"recipient_tin": "TIN",
|
||||
"recipient_name": "Customer name",
|
||||
"recipient_street": "Street",
|
||||
"recipient_city": "City",
|
||||
"recipient_province": "Province",
|
||||
"recipient_postal_code": "Postal code",
|
||||
"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": {
|
||||
"customer": {
|
||||
"title": "Customer",
|
||||
"description": "Select the customer for this invoice."
|
||||
"description": "Select the customer for this proforma."
|
||||
},
|
||||
"items": {
|
||||
"title": "Invoice details",
|
||||
"title": "Proforma details",
|
||||
"description": ""
|
||||
},
|
||||
"basic_info": {
|
||||
"title": "Invoice information",
|
||||
"description": "Basic invoice information"
|
||||
"title": "Proforma information",
|
||||
"description": "Basic proforma information"
|
||||
},
|
||||
"totals": {
|
||||
"title": "Invoice totals",
|
||||
"description": "Breakdown of invoice amounts with discounts and taxes."
|
||||
"title": "Proforma totals",
|
||||
"description": "Breakdown of proforma amounts with discounts and taxes."
|
||||
},
|
||||
"tax_resume": {
|
||||
"title": "Resumen de impuestos",
|
||||
@ -93,7 +95,7 @@
|
||||
},
|
||||
"preferences": {
|
||||
"title": "Preferences",
|
||||
"description": "Additional invoice settings"
|
||||
"description": "Additional proforma settings"
|
||||
}
|
||||
},
|
||||
"form_fields": {
|
||||
@ -103,39 +105,39 @@
|
||||
"description": ""
|
||||
},
|
||||
"invoice_number": {
|
||||
"label": "Invoice number",
|
||||
"label": "Proforma number",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"invoice_date": {
|
||||
"label": "Invoice date",
|
||||
"label": "Proforma date",
|
||||
"placeholder": "Select a date",
|
||||
"description": "Invoice date"
|
||||
"description": "Proforma date"
|
||||
},
|
||||
"series": {
|
||||
"label": "Serie",
|
||||
"placeholder": "",
|
||||
"description": "Invoice serie"
|
||||
"description": "Proforma serie"
|
||||
},
|
||||
"operation_date": {
|
||||
"label": "Operation date",
|
||||
"placeholder": "Select a date",
|
||||
"description": "Invoice operation date"
|
||||
"description": "Proforma operation date"
|
||||
},
|
||||
"reference": {
|
||||
"label": "Reference",
|
||||
"placeholder": "Reference of the invoice",
|
||||
"description": "Reference of the invoice"
|
||||
"placeholder": "Reference of the proforma",
|
||||
"description": "Reference of the proforma"
|
||||
},
|
||||
"description": {
|
||||
"label": "Description",
|
||||
"placeholder": "Description of the invoice",
|
||||
"description": "General description of the invoice"
|
||||
"placeholder": "Description of the proforma",
|
||||
"description": "General description of the proforma"
|
||||
},
|
||||
"subtotal_amount": {
|
||||
"label": "Subtotal",
|
||||
"placeholder": "",
|
||||
"desc": "Invoice subtotal"
|
||||
"desc": "Proforma subtotal"
|
||||
},
|
||||
"discount": {
|
||||
"label": "Discount (%)",
|
||||
@ -150,12 +152,12 @@
|
||||
"total_amount": {
|
||||
"label": "Total price",
|
||||
"placeholder": "",
|
||||
"desc": "Invoice total price"
|
||||
"desc": "Proforma total price"
|
||||
},
|
||||
"notes": {
|
||||
"label": "Notes",
|
||||
"placeholder": "Additional notes about the invoice",
|
||||
"description": "Additional notes that can be included in the invoice"
|
||||
"placeholder": "Additional notes about the proforma",
|
||||
"description": "Additional notes that can be included in the proforma"
|
||||
},
|
||||
"item": {
|
||||
"quantity": {
|
||||
@ -201,7 +203,7 @@
|
||||
"total_amount": {
|
||||
"label": "Total amount",
|
||||
"placeholder": "",
|
||||
"description": "Invoice line total"
|
||||
"description": "Proforma line total"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -212,7 +214,7 @@
|
||||
"customer_invoice_taxes_multi_select": {
|
||||
"label": "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."
|
||||
},
|
||||
"hover_card_totals_summary": {
|
||||
|
||||
@ -30,78 +30,80 @@
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"title": "Facturas de clientes",
|
||||
"description": "Gestiona tus facturas de clientes",
|
||||
"list": {
|
||||
"title": "Facturas de clientes",
|
||||
"description": "Lista todas las facturas de clientes",
|
||||
"grid_columns": {
|
||||
"invoice_number": "Nº factura",
|
||||
"series": "Serie",
|
||||
"status": "Estado",
|
||||
"invoice_date": "Fecha de factura",
|
||||
"operation_date": "Fecha de operación",
|
||||
"recipient_tin": "NIF/CIF",
|
||||
"recipient_name": "Cliente",
|
||||
"recipient_street": "Dirección",
|
||||
"recipient_city": "Ciudad",
|
||||
"recipient_province": "Provincia",
|
||||
"recipient_postal_code": "Código postal",
|
||||
"recipient_country": "País",
|
||||
"total_amount": "Importe total"
|
||||
"proformas": {
|
||||
"title": "Proformas",
|
||||
"description": "Gestiona tus proformas",
|
||||
"list": {
|
||||
"title": "Proformas",
|
||||
"description": "Lista todas las proformas",
|
||||
"grid_columns": {
|
||||
"invoice_number": "Nº proforma",
|
||||
"series": "Serie",
|
||||
"status": "Estado",
|
||||
"invoice_date": "Fecha de proforma",
|
||||
"operation_date": "Fecha de operación",
|
||||
"recipient_tin": "NIF/CIF",
|
||||
"recipient_name": "Cliente",
|
||||
"recipient_street": "Dirección",
|
||||
"recipient_city": "Ciudad",
|
||||
"recipient_province": "Provincia",
|
||||
"recipient_postal_code": "Código postal",
|
||||
"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": {
|
||||
"customer": {
|
||||
"title": "Cliente",
|
||||
"description": "Selecciona el cliente para esta factura"
|
||||
"description": "Selecciona el cliente para esta proforma"
|
||||
},
|
||||
"items": {
|
||||
"title": "Detalles de la factura",
|
||||
"title": "Detalles de la proforma",
|
||||
"description": ""
|
||||
},
|
||||
"basic_info": {
|
||||
"title": "Información de la factura",
|
||||
"description": "Información básica de la factura"
|
||||
"title": "Información de la proforma",
|
||||
"description": "Información básica de la proforma"
|
||||
},
|
||||
"totals": {
|
||||
"title": "Totales de la factura",
|
||||
"description": "Desglose de los importes de la factura con descuentos e impuestos."
|
||||
"title": "Totales de la proforma",
|
||||
"description": "Desglose de los importes de la proforma con descuentos e impuestos."
|
||||
},
|
||||
"preferences": {
|
||||
"title": "Preferencias",
|
||||
"description": "Configuraciones adicionales de la factura"
|
||||
"description": "Configuraciones adicionales de la proforma"
|
||||
}
|
||||
},
|
||||
"form_fields": {
|
||||
"invoice_number": {
|
||||
"label": "Número de factura",
|
||||
"label": "Número de proforma",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"invoice_date": {
|
||||
"label": "Fecha",
|
||||
"placeholder": "Selecciona una fecha",
|
||||
"description": "Fecha de emisión de la factura"
|
||||
"description": "Fecha de emisión de la proforma"
|
||||
},
|
||||
"series": {
|
||||
"label": "Serie",
|
||||
@ -111,23 +113,23 @@
|
||||
"operation_date": {
|
||||
"label": "Fecha de operación",
|
||||
"placeholder": "Selecciona una fecha",
|
||||
"description": "Fecha de la operación de la factura"
|
||||
"description": "Fecha de la operación de la proforma"
|
||||
},
|
||||
"reference": {
|
||||
"label": "Referencia",
|
||||
"placeholder": "Referencia de la factura",
|
||||
"description": "Referencia de la factura"
|
||||
"placeholder": "Referencia de la proforma",
|
||||
"description": "Referencia de la proforma"
|
||||
},
|
||||
|
||||
"description": {
|
||||
"label": "Descripción",
|
||||
"placeholder": "Descripción de la factura",
|
||||
"description": "Descripción general de la factura"
|
||||
"placeholder": "Descripción de la proforma",
|
||||
"description": "Descripción general de la proforma"
|
||||
},
|
||||
"subtotal_amount": {
|
||||
"label": "Subtotal",
|
||||
"placeholder": "",
|
||||
"desc": "Subtotal de la factura"
|
||||
"desc": "Subtotal de la proforma"
|
||||
},
|
||||
"discount": {
|
||||
"label": "Descuento (%)",
|
||||
@ -142,12 +144,12 @@
|
||||
"total_amount": {
|
||||
"label": "Precio total",
|
||||
"placeholder": "",
|
||||
"desc": "Precio total de la factura"
|
||||
"desc": "Precio total de la proforma"
|
||||
},
|
||||
"notes": {
|
||||
"label": "Notas",
|
||||
"placeholder": "Notas adicionales sobre la factura",
|
||||
"description": "Notas adicionales que se pueden incluir en la factura"
|
||||
"placeholder": "Notas adicionales sobre la proforma",
|
||||
"description": "Notas adicionales que se pueden incluir en la proforma"
|
||||
},
|
||||
"item": {
|
||||
"quantity": {
|
||||
@ -204,7 +206,7 @@
|
||||
"customer_invoice_taxes_multi_select": {
|
||||
"label": "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."
|
||||
},
|
||||
"hover_card_totals_summary": {
|
||||
|
||||
@ -8,7 +8,7 @@ const InvoicesLayout = lazy(() =>
|
||||
);
|
||||
|
||||
const ProformaListPage = lazy(() =>
|
||||
import("./pages").then((m) => ({ default: m.InvoiceListPage }))
|
||||
import("./pages").then((m) => ({ default: m.ProformaListPage }))
|
||||
);
|
||||
|
||||
const CustomerInvoiceAdd = lazy(() =>
|
||||
@ -34,7 +34,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
||||
{ path: ":id/edit", element: <InvoiceUpdatePage /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
/*{
|
||||
path: "customer-invoices",
|
||||
element: (
|
||||
<InvoicesLayout>
|
||||
@ -45,7 +45,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
||||
//{ path: "", index: true, element: <InvoiceListPage /> }, // index
|
||||
//{ path: "list", element: <InvoiceListPage /> },
|
||||
//
|
||||
/*{ path: "create", element: <CustomerInvoicesList /> },
|
||||
{ path: "create", element: <CustomerInvoicesList /> },
|
||||
{ path: ":id", element: <CustomerInvoicesList /> },
|
||||
{ path: ":id/edit", element: <CustomerInvoicesList /> },
|
||||
{ path: ":id/delete", element: <CustomerInvoicesList /> },
|
||||
@ -54,8 +54,8 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
||||
{ path: ":id/email", element: <CustomerInvoicesList /> },
|
||||
{ path: ":id/download", 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 { UniqueID, ValidationErrorCollection } from "@repo/rdx-ddd";
|
||||
import { DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { CreateCustomerInvoiceRequestSchema } from "../../common";
|
||||
import { CustomerInvoice, InvoiceFormData } from "../schemas";
|
||||
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { CreateProformaRequestSchema } from "../../common";
|
||||
import type { Proforma } from "../proformas/proforma.api.schema";
|
||||
import type { InvoiceFormData } from "../schemas";
|
||||
|
||||
type CreateCustomerInvoicePayload = {
|
||||
data: InvoiceFormData;
|
||||
};
|
||||
|
||||
export const useCreateCustomerInvoiceMutation = () => {
|
||||
export const useCreateProforma = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const dataSource = useDataSource();
|
||||
const schema = CreateCustomerInvoiceRequestSchema;
|
||||
const schema = CreateProformaRequestSchema;
|
||||
|
||||
return useMutation<CustomerInvoice, DefaultError, CreateCustomerInvoicePayload>({
|
||||
return useMutation<Proforma, DefaultError, CreateCustomerInvoicePayload>({
|
||||
mutationKey: ["customer-invoice:create"],
|
||||
|
||||
mutationFn: async (payload) => {
|
||||
@ -37,7 +39,7 @@ export const useCreateCustomerInvoiceMutation = () => {
|
||||
}
|
||||
|
||||
const created = await dataSource.createOne("customer-invoices", newInvoiceData);
|
||||
return created as CustomerInvoice;
|
||||
return created;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["customer-invoices"] });
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useDataSource } from "@erp/core/hooks";
|
||||
import { DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
|
||||
import { CustomerInvoice } from "../schemas";
|
||||
import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
|
||||
|
||||
import type { Proforma } from "../schemas";
|
||||
|
||||
export const CUSTOMER_INVOICE_QUERY_KEY = (id: string): QueryKey =>
|
||||
["customer_invoice", id] as const;
|
||||
@ -13,14 +14,14 @@ export const useInvoiceQuery = (invoiceId?: string, options?: InvoiceQueryOption
|
||||
const dataSource = useDataSource();
|
||||
const enabled = (options?.enabled ?? true) && !!invoiceId;
|
||||
|
||||
return useQuery<CustomerInvoice, DefaultError>({
|
||||
return useQuery<Proforma, DefaultError>({
|
||||
queryKey: CUSTOMER_INVOICE_QUERY_KEY(invoiceId ?? "unknown"),
|
||||
queryFn: async (context) => {
|
||||
const { signal } = context;
|
||||
if (!invoiceId) {
|
||||
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,
|
||||
});
|
||||
},
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import { useDataSource } from "@erp/core/hooks";
|
||||
import { ValidationErrorCollection } from "@repo/rdx-ddd";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
UpdateCustomerInvoiceByIdRequestDTO,
|
||||
UpdateCustomerInvoiceByIdRequestSchema,
|
||||
} from "../../common";
|
||||
import { InvoiceFormData } from "../schemas";
|
||||
|
||||
import { type UpdateProformaByIdRequestDTO, UpdateProformaByIdRequestSchema } from "../../common";
|
||||
import type { InvoiceFormData } from "../schemas";
|
||||
|
||||
import { CUSTOMER_INVOICE_QUERY_KEY } from "./use-invoice-query";
|
||||
|
||||
export const CUSTOMER_INVOICES_LIST_KEY = ["customer-invoices"] as const;
|
||||
@ -17,10 +16,10 @@ type UpdateCustomerInvoicePayload = {
|
||||
data: Partial<InvoiceFormData>;
|
||||
};
|
||||
|
||||
export function useUpdateCustomerInvoice() {
|
||||
export function useUpdateProforma() {
|
||||
const queryClient = useQueryClient();
|
||||
const dataSource = useDataSource();
|
||||
const schema = UpdateCustomerInvoiceByIdRequestSchema;
|
||||
const schema = UpdateProformaByIdRequestSchema;
|
||||
|
||||
return useMutation<
|
||||
InvoiceFormData,
|
||||
@ -59,7 +58,7 @@ export function useUpdateCustomerInvoice() {
|
||||
const { id: invoiceId } = variables;
|
||||
|
||||
// Refresca inmediatamente el detalle
|
||||
queryClient.setQueryData<UpdateCustomerInvoiceByIdRequestDTO>(
|
||||
queryClient.setQueryData<UpdateProformaByIdRequestDTO>(
|
||||
CUSTOMER_INVOICE_QUERY_KEY(invoiceId),
|
||||
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";
|
||||
|
||||
export const MODULE_NAME = "CustomerInvoices";
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
import { AppContent } from "@repo/rdx-ui/components";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useCreateCustomerInvoiceMutation } from "../../hooks";
|
||||
|
||||
import { useCreateProforma } from "../../hooks";
|
||||
import { useTranslation } from "../../i18n";
|
||||
|
||||
import { CreateCustomerInvoiceEditForm } from "./create-customer-invoice-edit-form";
|
||||
|
||||
export const CustomerInvoiceCreate = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { mutate, isPending, isError, error } = useCreateCustomerInvoiceMutation();
|
||||
const { mutate, isPending, isError, error } = useCreateProforma();
|
||||
|
||||
const handleSubmit = (data: any) => {
|
||||
// Handle form submission logic here
|
||||
@ -51,19 +53,19 @@ export const CustomerInvoiceCreate = () => {
|
||||
return (
|
||||
<>
|
||||
<AppContent>
|
||||
<div className='flex items-center justify-between space-y-2'>
|
||||
<div className="flex items-center justify-between space-y-2">
|
||||
<div>
|
||||
<h2 className='text-2xl font-bold tracking-tight'>{t("pages.create.title")}</h2>
|
||||
<p className='text-muted-foreground'>{t("pages.create.description")}</p>
|
||||
<h2 className="text-2xl font-bold tracking-tight">{t("pages.create.title")}</h2>
|
||||
<p className="text-muted-foreground">{t("pages.create.description")}</p>
|
||||
</div>
|
||||
<div className='flex items-center justify-end mb-4'>
|
||||
<Button className='cursor-pointer' onClick={() => navigate("/customer-invoices/list")}>
|
||||
<div className="flex items-center justify-end mb-4">
|
||||
<Button className="cursor-pointer" onClick={() => navigate("/customer-invoices/list")}>
|
||||
{t("pages.create.back_to_list")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-1 flex-col gap-4 p-4'>
|
||||
<CreateCustomerInvoiceEditForm onSubmit={handleSubmit} isPending={isPending} />
|
||||
<div className="flex flex-1 flex-col gap-4 p-4">
|
||||
<CreateCustomerInvoiceEditForm isPending={isPending} onSubmit={handleSubmit} />
|
||||
</div>
|
||||
</AppContent>
|
||||
</>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
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({
|
||||
amount: z.number().nullable(),
|
||||
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 { SimpleSearchInput } from '@erp/core/components';
|
||||
import { DataTable, SkeletonDataTable } from '@repo/rdx-ui/components';
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { usePinnedPreviewSheet } from '../../hooks';
|
||||
|
||||
import { usePinnedPreviewSheet } from "../../hooks";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { InvoiceSummaryFormData, InvoicesPageFormData } from '../../schemas';
|
||||
import { useInvoicesListColumns } from './use-invoices-list-columns';
|
||||
import { useProformasGridColumns } from "../../proformas/pages/list/use-proformas-grid-columns";
|
||||
import type { InvoiceSummaryFormData, InvoicesPageFormData } from "../../schemas";
|
||||
|
||||
export type InvoiceUpdateCompProps = {
|
||||
invoicesPage: InvoicesPageFormData;
|
||||
@ -22,9 +20,12 @@ export type InvoiceUpdateCompProps = {
|
||||
searchValue: string;
|
||||
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
|
||||
export const InvoicesListGrid = ({
|
||||
@ -34,8 +35,9 @@ export const InvoicesListGrid = ({
|
||||
pageSize,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
searchValue, onSearchChange,
|
||||
onRowClick
|
||||
searchValue,
|
||||
onSearchChange,
|
||||
onRowClick,
|
||||
}: InvoiceUpdateCompProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
@ -49,7 +51,7 @@ export const InvoicesListGrid = ({
|
||||
|
||||
const [statusFilter, setStatusFilter] = useState("todas");
|
||||
|
||||
const columns = useInvoicesListColumns({
|
||||
const columns = useProformasGridColumns({
|
||||
onEdit: (invoice) => navigate(`/customer-invoices/${invoice.id}/edit`),
|
||||
onDuplicate: (invoice) => null, //duplicateInvoice(inv.id),
|
||||
onDownloadPdf: (invoice) => null, //downloadInvoicePdf(inv.id),
|
||||
@ -61,9 +63,7 @@ export const InvoicesListGrid = ({
|
||||
const goToRow = useCallback(
|
||||
(id: string, newTab = false) => {
|
||||
const url = `/customer-invoices/${id}/edit`;
|
||||
newTab
|
||||
? window.open(url, "_blank", "noopener,noreferrer")
|
||||
: navigate(url);
|
||||
newTab ? window.open(url, "_blank", "noopener,noreferrer") : navigate(url);
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
@ -71,8 +71,7 @@ export const InvoicesListGrid = ({
|
||||
const onRowClicked = useCallback(
|
||||
(e: RowClickedEvent<any>) => {
|
||||
if (!e.data) return;
|
||||
const newTab =
|
||||
e.event instanceof MouseEvent && (e.event.metaKey || e.event.ctrlKey);
|
||||
const newTab = e.event instanceof MouseEvent && (e.event.metaKey || e.event.ctrlKey);
|
||||
goToRow(e.data.id, newTab);
|
||||
},
|
||||
[goToRow]
|
||||
@ -83,7 +82,7 @@ export const InvoicesListGrid = ({
|
||||
if (!e.data) return;
|
||||
|
||||
const ev = e.event;
|
||||
if (!ev || !(ev instanceof KeyboardEvent)) return;
|
||||
if (!(ev && ev instanceof KeyboardEvent)) return;
|
||||
|
||||
const key = ev.key;
|
||||
if (key === "Enter" || key === " ") {
|
||||
@ -98,11 +97,13 @@ export const InvoicesListGrid = ({
|
||||
[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; }
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
return;
|
||||
}
|
||||
preview.open(invoice);
|
||||
},
|
||||
[preview]
|
||||
@ -113,9 +114,9 @@ export const InvoicesListGrid = ({
|
||||
<div className="flex flex-col gap-4">
|
||||
<SkeletonDataTable
|
||||
columns={columns.length}
|
||||
footerProps={{ pageIndex, pageSize, totalItems: total_items ?? 0 }}
|
||||
rows={Math.max(6, pageSize)}
|
||||
showFooter
|
||||
footerProps={{ pageIndex, pageSize, totalItems: total_items ?? 0 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -126,7 +127,7 @@ export const InvoicesListGrid = ({
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Barra de filtros */}
|
||||
<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}>
|
||||
<SelectTrigger className="w-full sm:w-48 bg-white border-gray-200 shadow-sm">
|
||||
<FilterIcon className="mr-2 h-4 w-4" />
|
||||
@ -149,16 +150,16 @@ export const InvoicesListGrid = ({
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={items}
|
||||
readOnly
|
||||
enableRowSelection
|
||||
enablePagination
|
||||
enableRowSelection
|
||||
manualPagination
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
totalItems={total_items}
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
onRowClick={handleRowClick}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
readOnly
|
||||
totalItems={total_items}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -175,4 +176,4 @@ export const InvoicesListGrid = ({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
import { PageHeader } from '@erp/core/components';
|
||||
import { ErrorAlert } from '@erp/customers/components';
|
||||
import { PageHeader } from "@erp/core/components";
|
||||
import { ErrorAlert } from "@erp/customers/components";
|
||||
import { AppContent, AppHeader, BackHistoryButton, useDebounce } from "@repo/rdx-ui/components";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useInvoicesQuery } from '../../hooks';
|
||||
|
||||
import { useInvoicesQuery } from "../../hooks";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { invoiceResumeDtoToFormAdapter } from '../../schemas/invoice-resume-dto.adapter';
|
||||
import { InvoicesListGrid } from './invoices-list-grid';
|
||||
import { invoiceResumeDtoToFormAdapter } from "../../schemas/invoice-resume-dto.adapter";
|
||||
|
||||
import { InvoicesListGrid } from "./invoices-list-grid";
|
||||
|
||||
export const InvoiceListPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -29,24 +31,18 @@ export const InvoiceListPage = () => {
|
||||
[pageSize, pageIndex, debouncedQ]
|
||||
);
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useInvoicesQuery({
|
||||
criteria
|
||||
const { data, isLoading, isError, error } = useInvoicesQuery({
|
||||
criteria,
|
||||
});
|
||||
|
||||
const invoicesPageData = useMemo(() => {
|
||||
if (!data) return undefined;
|
||||
return {
|
||||
...data,
|
||||
items: invoiceResumeDtoToFormAdapter.fromDto(data.items)
|
||||
}
|
||||
items: invoiceResumeDtoToFormAdapter.fromDto(data.items),
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
|
||||
const handlePageChange = (newPageIndex: number) => {
|
||||
setPageIndex(newPageIndex);
|
||||
};
|
||||
@ -67,8 +63,8 @@ export const InvoiceListPage = () => {
|
||||
return (
|
||||
<AppContent>
|
||||
<ErrorAlert
|
||||
title={t("pages.list.loadErrorTitle")}
|
||||
message={(error as Error)?.message || "Error al cargar el listado"}
|
||||
title={t("pages.list.loadErrorTitle")}
|
||||
/>
|
||||
<BackHistoryButton />
|
||||
</AppContent>
|
||||
@ -79,36 +75,35 @@ export const InvoiceListPage = () => {
|
||||
<>
|
||||
<AppHeader>
|
||||
<PageHeader
|
||||
title={t("pages.list.title")}
|
||||
description={t("pages.list.description")}
|
||||
rightSlot={
|
||||
<div className='flex items-center space-x-2'>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
onClick={() => navigate("/customer-invoices/create")}
|
||||
variant={'default'}
|
||||
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")}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
title={t("pages.list.title")}
|
||||
/>
|
||||
</AppHeader>
|
||||
<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"}>
|
||||
<InvoicesListGrid
|
||||
invoicesPage={invoicesPageData}
|
||||
loading={isLoading}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
searchValue={search}
|
||||
onSearchChange={handleSearchChange}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
searchValue={search}
|
||||
/>
|
||||
</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 {
|
||||
UnsavedChangesProvider,
|
||||
UpdateCommitButtonGroup,
|
||||
useHookForm
|
||||
} from "@erp/core/hooks";
|
||||
import { PageHeader } from "@erp/core/components";
|
||||
import { UnsavedChangesProvider, UpdateCommitButtonGroup, useHookForm } from "@erp/core/hooks";
|
||||
import { AppContent, AppHeader } from "@repo/rdx-ui/components";
|
||||
import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
|
||||
import { useId, useMemo } from 'react';
|
||||
import { FieldErrors, FormProvider } from "react-hook-form";
|
||||
import { useId, useMemo } from "react";
|
||||
import { type FieldErrors, FormProvider } from "react-hook-form";
|
||||
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 {
|
||||
CustomerInvoice,
|
||||
InvoiceFormData,
|
||||
InvoiceFormSchema,
|
||||
defaultCustomerInvoiceFormData,
|
||||
invoiceDtoToFormAdapter
|
||||
type InvoiceFormData,
|
||||
InvoiceFormSchema,
|
||||
type Proforma,
|
||||
defaultCustomerInvoiceFormData,
|
||||
invoiceDtoToFormAdapter,
|
||||
} from "../../schemas";
|
||||
import { InvoiceUpdateForm } from './invoice-update-form';
|
||||
|
||||
import { InvoiceUpdateForm } from "./invoice-update-form";
|
||||
|
||||
export type InvoiceUpdateCompProps = {
|
||||
invoice: CustomerInvoice,
|
||||
}
|
||||
invoice: Proforma;
|
||||
};
|
||||
|
||||
export const InvoiceUpdateComp = ({
|
||||
invoice: invoiceData,
|
||||
}: InvoiceUpdateCompProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const formId = useId();
|
||||
export const InvoiceUpdateComp = ({ invoice: invoiceData }: InvoiceUpdateCompProps) => {
|
||||
const { t } = useTranslation();
|
||||
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,
|
||||
isPending: isUpdating,
|
||||
isError: isUpdateError,
|
||||
error: updateError,
|
||||
} = useUpdateCustomerInvoice();
|
||||
const {
|
||||
mutate,
|
||||
isPending: isUpdating,
|
||||
isError: isUpdateError,
|
||||
error: updateError,
|
||||
} = useUpdateProforma();
|
||||
|
||||
const initialValues = useMemo(() => {
|
||||
return invoiceData
|
||||
? invoiceDtoToFormAdapter.fromDto(invoiceData, context)
|
||||
: defaultCustomerInvoiceFormData
|
||||
}, [invoiceData, context]);
|
||||
const initialValues = useMemo(() => {
|
||||
return invoiceData
|
||||
? invoiceDtoToFormAdapter.fromDto(invoiceData, context)
|
||||
: defaultCustomerInvoiceFormData;
|
||||
}, [invoiceData, context]);
|
||||
|
||||
const form = useHookForm<InvoiceFormData>({
|
||||
resolverSchema: InvoiceFormSchema,
|
||||
initialValues,
|
||||
disabled: !invoiceData || isUpdating,
|
||||
});
|
||||
|
||||
const form = useHookForm<InvoiceFormData>({
|
||||
resolverSchema: InvoiceFormSchema,
|
||||
initialValues,
|
||||
disabled: !invoiceData || isUpdating,
|
||||
});
|
||||
|
||||
const handleSubmit = (formData: InvoiceFormData) => {
|
||||
console.log('Guardo factura')
|
||||
const dto = invoiceDtoToFormAdapter.toDto(formData, context)
|
||||
console.log("dto => ", dto);
|
||||
mutate(
|
||||
{ id: invoice_id, data: dto as Partial<InvoiceFormData> },
|
||||
{
|
||||
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 handleBack = () => {
|
||||
navigate(-1);
|
||||
};
|
||||
|
||||
const handleError = (errors: FieldErrors<InvoiceFormData>) => {
|
||||
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}>
|
||||
<AppHeader>
|
||||
<PageHeader
|
||||
backIcon
|
||||
title={`${t("pages.edit.title")} #${invoiceData.invoice_number}`}
|
||||
description={t("pages.edit.description")}
|
||||
rightSlot={<>
|
||||
<UpdateCommitButtonGroup
|
||||
isLoading={isPending}
|
||||
|
||||
submit={{ formId, variant: 'default', disabled: isPending, label: t("pages.edit.actions.save_draft") }}
|
||||
cancel={{ formId, to: "/customer-invoices/list" }}
|
||||
/>
|
||||
</>}
|
||||
/>
|
||||
</AppHeader>
|
||||
|
||||
<AppContent>
|
||||
<FormProvider {...form}>
|
||||
<InvoiceUpdateForm
|
||||
formId={formId}
|
||||
onSubmit={handleSubmit}
|
||||
onError={handleError}
|
||||
className="bg-white rounded-xl border shadow-xl max-w-full"
|
||||
/>
|
||||
</FormProvider>
|
||||
</AppContent>
|
||||
</UnsavedChangesProvider>
|
||||
const handleSubmit = (formData: InvoiceFormData) => {
|
||||
console.log("Guardo factura");
|
||||
const dto = invoiceDtoToFormAdapter.toDto(formData, context);
|
||||
console.log("dto => ", dto);
|
||||
mutate(
|
||||
{ id: invoice_id, data: dto as Partial<InvoiceFormData> },
|
||||
{
|
||||
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 handleBack = () => {
|
||||
navigate(-1);
|
||||
};
|
||||
|
||||
const handleError = (errors: FieldErrors<InvoiceFormData>) => {
|
||||
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}>
|
||||
<AppHeader>
|
||||
<PageHeader
|
||||
backIcon
|
||||
description={t("pages.edit.description")}
|
||||
rightSlot={
|
||||
<>
|
||||
<UpdateCommitButtonGroup
|
||||
cancel={{ formId, to: "/customer-invoices/list" }}
|
||||
isLoading={isPending}
|
||||
submit={{
|
||||
formId,
|
||||
variant: "default",
|
||||
disabled: isPending,
|
||||
label: t("pages.edit.actions.save_draft"),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
title={`${t("pages.edit.title")} #${invoiceData.invoice_number}`}
|
||||
/>
|
||||
</AppHeader>
|
||||
|
||||
<AppContent>
|
||||
<FormProvider {...form}>
|
||||
<InvoiceUpdateForm
|
||||
className="bg-white rounded-xl border shadow-xl max-w-full"
|
||||
formId={formId}
|
||||
onError={handleError}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</FormProvider>
|
||||
</AppContent>
|
||||
</UnsavedChangesProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export * from "./use-proforma-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 { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
|
||||
|
||||
export const CUSTOMER_INVOICE_QUERY_KEY = (id: string): QueryKey =>
|
||||
["customer_invoice", id] as const;
|
||||
import type { ProformaSummaryPage } from "../proforma.api.schema";
|
||||
|
||||
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;
|
||||
criteria?: CriteriaDTO;
|
||||
};
|
||||
|
||||
export const useInvoiceQuery = (invoiceId?: string, options?: InvoiceQueryOptions) => {
|
||||
// Obtener todas las facturas
|
||||
export const useProformasQuery = (options?: ProformasQueryOptions) => {
|
||||
const dataSource = useDataSource();
|
||||
const enabled = (options?.enabled ?? true) && !!invoiceId;
|
||||
const enabled = options?.enabled ?? true;
|
||||
const criteria = options?.criteria ?? {};
|
||||
|
||||
return useQuery<CustomerInvoice, DefaultError>({
|
||||
queryKey: CUSTOMER_INVOICE_QUERY_KEY(invoiceId ?? "unknown"),
|
||||
queryFn: async (context) => {
|
||||
const { signal } = context;
|
||||
if (!invoiceId) {
|
||||
if (!invoiceId) throw new Error("invoiceId is required");
|
||||
}
|
||||
return await dataSource.getOne<CustomerInvoice>("customer-invoices", invoiceId, {
|
||||
return useQuery<ProformaSummaryPage, DefaultError>({
|
||||
queryKey: PROFORMAS_QUERY_KEY(criteria),
|
||||
queryFn: async ({ signal }) => {
|
||||
return await dataSource.getList<ProformaSummaryPage>("proformas", {
|
||||
signal,
|
||||
...criteria,
|
||||
});
|
||||
},
|
||||
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 { ErrorAlert } from "@erp/customers/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 { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useInvoicesQuery } from "../../../hooks";
|
||||
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 = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -21,24 +24,26 @@ export const ProformaListPage = () => {
|
||||
const debouncedQ = useDebounce(search, 300);
|
||||
|
||||
const criteria = useMemo(
|
||||
() => ({
|
||||
q: debouncedQ || "",
|
||||
pageSize,
|
||||
pageNumber: pageIndex,
|
||||
}),
|
||||
() =>
|
||||
({
|
||||
q: debouncedQ || "",
|
||||
pageSize,
|
||||
pageNumber: pageIndex,
|
||||
order: "desc",
|
||||
orderBy: "invoice_date",
|
||||
}) as CriteriaDTO,
|
||||
[pageSize, pageIndex, debouncedQ]
|
||||
);
|
||||
|
||||
const { data, isLoading, isError, error } = useInvoicesQuery({
|
||||
console.log(criteria);
|
||||
|
||||
const { data, isLoading, isError, error } = useProformasQuery({
|
||||
criteria,
|
||||
});
|
||||
|
||||
const invoicesPageData = useMemo(() => {
|
||||
const proformaPageData = useMemo(() => {
|
||||
if (!data) return undefined;
|
||||
return {
|
||||
...data,
|
||||
items: invoiceResumeDtoToFormAdapter.fromDto(data.items),
|
||||
};
|
||||
return ProformaResumeDtoAdapter.fromDto(data);
|
||||
}, [data]);
|
||||
|
||||
const handlePageChange = (newPageIndex: number) => {
|
||||
@ -57,12 +62,14 @@ export const ProformaListPage = () => {
|
||||
setPageIndex(0);
|
||||
};
|
||||
|
||||
if (isError || !invoicesPageData) {
|
||||
console.log();
|
||||
|
||||
if (isError || !proformaPageData) {
|
||||
return (
|
||||
<AppContent>
|
||||
<ErrorAlert
|
||||
message={(error as Error)?.message || "Error al cargar el listado"}
|
||||
title={t("pages.list.loadErrorTitle")}
|
||||
title={t("pages.proformas.list.loadErrorTitle")}
|
||||
/>
|
||||
<BackHistoryButton />
|
||||
</AppContent>
|
||||
@ -73,28 +80,28 @@ export const ProformaListPage = () => {
|
||||
<>
|
||||
<AppHeader>
|
||||
<PageHeader
|
||||
description={t("pages.list.description")}
|
||||
description={t("pages.proformas.list.description")}
|
||||
rightSlot={
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
aria-label={t("pages.create.title")}
|
||||
aria-label={t("pages.proformas.create.title")}
|
||||
className="cursor-pointer"
|
||||
onClick={() => navigate("/customer-invoices/create")}
|
||||
onClick={() => navigate("/proformas/create")}
|
||||
variant={"default"}
|
||||
>
|
||||
<PlusIcon aria-hidden className="mr-2 h-4 w-4" />
|
||||
{t("pages.create.title")}
|
||||
{t("pages.proformas.create.title")}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
title={t("pages.list.title")}
|
||||
title={t("pages.proformas.list.title")}
|
||||
/>
|
||||
</AppHeader>
|
||||
<AppContent>
|
||||
<div className="flex flex-col w-full h-full py-3">
|
||||
<div className={"flex-1"}>
|
||||
<ProformasGrid
|
||||
data={invoicesPageData}
|
||||
data={proformaPageData}
|
||||
loading={isLoading}
|
||||
onPageChange={handlePageChange}
|
||||
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 { z } from "zod/v4";
|
||||
|
||||
import {
|
||||
CreateCustomerInvoiceRequestSchema,
|
||||
GetIssueInvoiceByIdResponseSchema,
|
||||
ListIssueInvoicesResponseSchema,
|
||||
UpdateCustomerInvoiceByIdRequestSchema,
|
||||
} from "../../common";
|
||||
import { GetIssueInvoiceByIdResponseSchema, ListIssueInvoicesResponseSchema } from "../../common";
|
||||
|
||||
export const CustomerInvoiceSchema = GetIssueInvoiceByIdResponseSchema.omit({
|
||||
export const IssueInvoiceSchema = GetIssueInvoiceByIdResponseSchema.omit({
|
||||
metadata: true,
|
||||
});
|
||||
export const CustomerInvoiceCreateSchema = CreateCustomerInvoiceRequestSchema;
|
||||
export const CustomerInvoiceUpdateSchema = UpdateCustomerInvoiceByIdRequestSchema;
|
||||
|
||||
// Tipos (derivados de Zod o DTOs del backend)
|
||||
export type CustomerInvoice = z.infer<typeof CustomerInvoiceSchema>;
|
||||
export type CustomerInvoiceRecipient = CustomerInvoice["recipient"];
|
||||
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
|
||||
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 CustomerInvoicesPageSchema = ListIssueInvoicesResponseSchema.omit({
|
||||
export const IssueInvoicesPageSchema = ListIssueInvoicesResponseSchema.omit({
|
||||
metadata: true,
|
||||
});
|
||||
|
||||
export type PaginatedResponse = z.infer<typeof PaginationSchema>;
|
||||
export type CustomerInvoicesPage = z.infer<typeof CustomerInvoicesPageSchema>;
|
||||
//export type PaginatedResponse = z.infer<typeof PaginationSchema>;
|
||||
//export type CustomerInvoicesPage = z.infer<typeof CustomerInvoicesPageSchema>;
|
||||
|
||||
// Í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 { forwardRef } from "react";
|
||||
import { useTranslation } from "../i18n";
|
||||
import React from "react";
|
||||
|
||||
export type CustomerStatus = "active" | "inactive";
|
||||
export type CustomerStatus = "active" | "inactive" | "error";
|
||||
|
||||
export type CustomerStatusBadgeProps = {
|
||||
status: string; // permitir cualquier valor
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const statusColorConfig: Record<CustomerStatus, { badge: string; dot: string }> = {
|
||||
inactive: {
|
||||
badge:
|
||||
"bg-gray-600/10 dark:bg-gray-600/20 hover:bg-gray-600/10 text-gray-500 border-gray-600/60",
|
||||
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",
|
||||
},
|
||||
const statusColorConfig: Record<CustomerStatus, string> = {
|
||||
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",
|
||||
};
|
||||
|
||||
export const CustomerStatusBadge = forwardRef<HTMLDivElement, CustomerStatusBadgeProps>(
|
||||
({ status, className, ...props }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const normalizedStatus = status.toLowerCase() as CustomerStatus;
|
||||
const config = statusColorConfig[normalizedStatus];
|
||||
const commonClassName =
|
||||
"transition-colors duration-200 cursor-pointer shadow-none rounded-full";
|
||||
export const CustomerStatusBadge = ({ status }: { status: string }) => {
|
||||
// Map visual simple; ajustar a tu catálogo real
|
||||
const statusClass = React.useMemo(
|
||||
() =>
|
||||
status.toLowerCase() === "active" ? statusColorConfig.active : statusColorConfig.inactive,
|
||||
[status]
|
||||
);
|
||||
const contentTxt = React.useMemo(
|
||||
() =>
|
||||
status.toLowerCase() === "active" ? "El cliente está activo" : "El cliente está inactivo",
|
||||
[status]
|
||||
);
|
||||
|
||||
if (!config) {
|
||||
return (
|
||||
<Badge ref={ref} className={cn(commonClassName, className)} {...props}>
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
);
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
CustomerStatusBadge.displayName = "CustomerStatusBadge";
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export * from "./client-selector-modal";
|
||||
export * from "./customer-modal-selector";
|
||||
export * from "./customer-status-badge";
|
||||
export * from "./customers-layout";
|
||||
export * from "./editor";
|
||||
export * from "./error-alert";
|
||||
|
||||
@ -1,21 +1,30 @@
|
||||
import { DataTableColumnHeader } from '@repo/rdx-ui/components';
|
||||
import { DataTableColumnHeader } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
Badge,
|
||||
Button,
|
||||
DropdownMenu, DropdownMenuContent,
|
||||
DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger
|
||||
} from '@repo/shadcn-ui/components';
|
||||
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
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 { useTranslation } from '../../i18n';
|
||||
import { CustomerSummaryFormData } from '../../schemas';
|
||||
|
||||
import { CustomerStatusBadge } from "../../components";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import type { CustomerSummaryFormData } from "../../schemas";
|
||||
|
||||
type CustomerActionHandlers = {
|
||||
onEdit?: (customer: CustomerSummaryFormData) => void;
|
||||
@ -27,33 +36,8 @@ function shortId(id: string) {
|
||||
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 }) => (
|
||||
<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 ? "Company" : "Person"}
|
||||
</Badge>
|
||||
@ -65,18 +49,20 @@ const Soft = ({ children }: { children: React.ReactNode }) => (
|
||||
|
||||
const ContactCell = ({ customer }: { customer: CustomerSummaryFormData }) => (
|
||||
<div className="grid gap-1 text-foreground text-sm my-1.5">
|
||||
|
||||
{customer.email_primary && (
|
||||
<div className='flex items-center gap-2'>
|
||||
<MailIcon className='size-3.5' />
|
||||
<a className='group' href={`mailto:${customer.email_primary}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<MailIcon className="size-3.5" />
|
||||
<a className="group" href={`mailto:${customer.email_primary}`}>
|
||||
{customer.email_primary}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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">
|
||||
@ -84,16 +70,9 @@ const ContactCell = ({ customer }: { customer: CustomerSummaryFormData }) => (
|
||||
<span>{customer.phone_primary || customer.mobile_primary || <Soft>-</Soft>}</span>
|
||||
{customer.phone_secondary && <Soft>• {customer.phone_secondary}</Soft>}
|
||||
{customer.mobile_secondary && <Soft>• {customer.mobile_secondary}</Soft>}
|
||||
{false && customer.fax && <Soft>• fax {customer.fax}</Soft>}
|
||||
{false}
|
||||
</div>
|
||||
{false && customer.website && (
|
||||
<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>
|
||||
)}
|
||||
{false}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -111,7 +90,7 @@ const AddressCell = ({ c }: { c: CustomerSummaryFormData }) => {
|
||||
|
||||
function initials(name: string) {
|
||||
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) {
|
||||
@ -120,127 +99,140 @@ function safeHttp(url: string) {
|
||||
return `https://${url}`;
|
||||
}
|
||||
|
||||
|
||||
export function useCustomersListColumns(
|
||||
handlers: CustomerActionHandlers = {}
|
||||
): ColumnDef<CustomerSummaryFormData>[] {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
onEdit, onView, onDelete,
|
||||
} = handlers;
|
||||
const { onEdit, onView, onDelete } = handlers;
|
||||
|
||||
return React.useMemo<ColumnDef<CustomerSummaryFormData>[]>(() => [
|
||||
// Identidad + estado + metadatos (columna compuesta)
|
||||
{
|
||||
id: "customer",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t("pages.list.grid_columns.customer")} className="text-left" />
|
||||
),
|
||||
accessorFn: (row) => row.name, // para ordenar/buscar por nombre
|
||||
enableHiding: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
cell: ({ row }) => {
|
||||
const c = row.original;
|
||||
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">
|
||||
<StatusBadge 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} />
|
||||
return React.useMemo<ColumnDef<CustomerSummaryFormData>[]>(
|
||||
() => [
|
||||
// Identidad + estado + metadatos (columna compuesta)
|
||||
{
|
||||
id: "customer",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left"
|
||||
column={column}
|
||||
title={t("pages.list.grid_columns.customer")}
|
||||
/>
|
||||
),
|
||||
accessorFn: (row) => row.name, // para ordenar/buscar por nombre
|
||||
enableHiding: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
cell: ({ row }) => {
|
||||
const c = row.original;
|
||||
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">
|
||||
<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>
|
||||
);
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Contacto (emails, teléfonos, web)
|
||||
{
|
||||
id: "contact",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t("pages.list.grid_columns.contact")} className="text-left" />
|
||||
),
|
||||
accessorFn: (r) => `${r.email_primary} ${r.phone_primary} ${r.mobile_primary} ${r.website}`,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
cell: ({ row }) => <ContactCell customer={row.original} />,
|
||||
},
|
||||
// Contacto (emails, teléfonos, web)
|
||||
{
|
||||
id: "contact",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left"
|
||||
column={column}
|
||||
title={t("pages.list.grid_columns.contact")}
|
||||
/>
|
||||
),
|
||||
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)
|
||||
{
|
||||
id: "address",
|
||||
header: t("pages.list.grid_columns.address"),
|
||||
accessorFn: (r) =>
|
||||
`${r.street} ${r.street2} ${r.city} ${r.postal_code} ${r.province} ${r.country}`,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
cell: ({ row }) => <AddressCell c={row.original} />,
|
||||
},
|
||||
// Dirección (múltiples campos en bloque)
|
||||
{
|
||||
id: "address",
|
||||
header: t("pages.list.grid_columns.address"),
|
||||
accessorFn: (r) =>
|
||||
`${r.street} ${r.street2} ${r.city} ${r.postal_code} ${r.province} ${r.country}`,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
cell: ({ row }) => <AddressCell c={row.original} />,
|
||||
},
|
||||
|
||||
// Acciones
|
||||
{
|
||||
id: "actions",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t("pages.list.grid_columns.actions")} className="text-right" />
|
||||
),
|
||||
size: 64,
|
||||
minSize: 64,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => {
|
||||
const customer = row.original;
|
||||
const { website, email_primary } = customer;
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label="Edit customer"
|
||||
onClick={() => onEdit?.(customer)}
|
||||
>
|
||||
<PencilIcon className="size-4" />
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" aria-label="More actions">
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => onView?.(customer)}>Open</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onEdit?.(customer)}>Edit</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => window.open(safeHttp(website), "_blank")}>
|
||||
Visit website
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(email_primary)}>
|
||||
Copy email
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-destructive"
|
||||
onClick={() => onDelete?.(customer)}
|
||||
>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
// Acciones
|
||||
{
|
||||
id: "actions",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-right"
|
||||
column={column}
|
||||
title={t("pages.list.grid_columns.actions")}
|
||||
/>
|
||||
),
|
||||
size: 64,
|
||||
minSize: 64,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => {
|
||||
const customer = row.original;
|
||||
const { website, email_primary } = customer;
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
aria-label="Edit customer"
|
||||
onClick={() => onEdit?.(customer)}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<PencilIcon className="size-4" />
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button aria-label="More actions" size="icon" variant="ghost">
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => onView?.(customer)}>Open</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onEdit?.(customer)}>Edit</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => window.open(safeHttp(website), "_blank")}>
|
||||
Visit website
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(email_primary)}>
|
||||
Copy email
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-destructive"
|
||||
onClick={() => onDelete?.(customer)}
|
||||
>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</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.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) */
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { FindOptions } from "sequelize";
|
||||
import type { FindOptions } from "sequelize";
|
||||
|
||||
// orderItem puede ser: ['campo', 'ASC'|'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 {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@ -33,6 +5,31 @@ import {
|
||||
SidebarHeader,
|
||||
SidebarRail,
|
||||
} 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 { NavSecondary } from "./nav-secondary.tsx";
|
||||
import { NavUser } from "./nav-user.tsx";
|
||||
@ -203,7 +200,7 @@ const data2 = {
|
||||
items: [
|
||||
{
|
||||
title: "Listado de proformas",
|
||||
url: "/customer-proforma",
|
||||
url: "/proformas",
|
||||
},
|
||||
{
|
||||
title: "Enviar a Veri*Factu",
|
||||
@ -243,8 +240,8 @@ const data2 = {
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
return (
|
||||
<Sidebar collapsible='icon' {...props}>
|
||||
<SidebarHeader className='mb-3'>
|
||||
<Sidebar collapsible="icon" {...props}>
|
||||
<SidebarHeader className="mb-3">
|
||||
<TeamSwitcher teams={data2.teams} />
|
||||
<SearchForm />
|
||||
</SidebarHeader>
|
||||
@ -252,7 +249,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
<NavMain items={data2.navMain} />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavSecondary items={data.navSecondary} className='mt-auto' />
|
||||
<NavSecondary className="mt-auto" items={data.navSecondary} />
|
||||
<NavUser user={data.user} />
|
||||
</SidebarFooter>
|
||||
<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-2xl: 1px 1px 6px 0px hsl(0 0% 0% / 0.25);
|
||||
--tracking-normal: 0rem;
|
||||
--spacing: 0.20rem;
|
||||
--spacing: 0.22rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user