.
This commit is contained in:
parent
bba38e67f2
commit
bf9ed99a90
@ -1,7 +1,9 @@
|
|||||||
import { IModuleClient } from "@erp/core/client";
|
import type { IModuleClient } from "@erp/core/client";
|
||||||
import { AppLayout } from "@repo/rdx-ui/components";
|
import { AppLayout } from "@repo/rdx-ui/components";
|
||||||
import { createBrowserRouter, createRoutesFromElements, Navigate, Route } from "react-router-dom";
|
import { Navigate, Route, createBrowserRouter, createRoutesFromElements } from "react-router-dom";
|
||||||
|
|
||||||
import { ModuleRoutes } from "@/components/module-routes";
|
import { ModuleRoutes } from "@/components/module-routes";
|
||||||
|
|
||||||
import { ErrorPage, LoginForm } from "../pages";
|
import { ErrorPage, LoginForm } from "../pages";
|
||||||
import { modules } from "../register-modules"; // Aquí ca
|
import { modules } from "../register-modules"; // Aquí ca
|
||||||
|
|
||||||
@ -33,24 +35,24 @@ export const getAppRouter = () => {
|
|||||||
|
|
||||||
return createBrowserRouter(
|
return createBrowserRouter(
|
||||||
createRoutesFromElements(
|
createRoutesFromElements(
|
||||||
<Route path='/'>
|
<Route path="/">
|
||||||
{/* Auth Layout */}
|
{/* Auth Layout */}
|
||||||
<Route path='/auth'>
|
<Route path="/auth">
|
||||||
<Route index element={<Navigate to='login' />} />
|
<Route element={<Navigate to="login" />} index />
|
||||||
<Route path='login' element={<LoginForm />} />
|
<Route element={<LoginForm />} path="login" />
|
||||||
<Route path='*' element={<ModuleRoutes modules={grouped.auth} params={params} />} />
|
<Route element={<ModuleRoutes modules={grouped.auth} params={params} />} path="*" />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* App Layout */}
|
{/* App Layout */}
|
||||||
<Route element={<AppLayout />}>
|
<Route element={<AppLayout />}>
|
||||||
{/* Dynamic Module Routes */}
|
{/* Dynamic Module Routes */}
|
||||||
<Route path='*' element={<ModuleRoutes modules={grouped.app} params={params} />} />
|
<Route element={<ModuleRoutes modules={grouped.app} params={params} />} path="*" />
|
||||||
|
|
||||||
{/* Main Layout */}
|
{/* Main Layout */}
|
||||||
<Route path='/dashboard' element={<ErrorPage />} />
|
<Route element={<ErrorPage />} path="/dashboard" />
|
||||||
<Route path='/settings' element={<ErrorPage />} />
|
<Route element={<ErrorPage />} path="/settings" />
|
||||||
<Route path='/catalog' element={<ErrorPage />} />
|
<Route element={<ErrorPage />} path="/catalog" />
|
||||||
<Route path='/quotes' element={<ErrorPage />} />
|
<Route element={<ErrorPage />} path="/quotes" />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
export * from "./documents";
|
export * from "./documents";
|
||||||
|
export * from "./mappers";
|
||||||
export * from "./presenters";
|
export * from "./presenters";
|
||||||
export * from "./renderers";
|
export * from "./renderers";
|
||||||
export * from "./snapshot-builders";
|
export * from "./snapshot-builders";
|
||||||
|
|||||||
1
modules/core/src/api/application/mappers/index.ts
Normal file
1
modules/core/src/api/application/mappers/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./mapper.interface";
|
||||||
@ -56,23 +56,21 @@ export type DomainMapperWithBulk<TPersistence, TDomain> = IDomainMapper<TPersist
|
|||||||
Partial<IBulkDomainMapper<TPersistence, TDomain>>;
|
Partial<IBulkDomainMapper<TPersistence, TDomain>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* 👓 Mapper de Read Model (Persistencia → Read Model/Proyección de Lectura)
|
||||||
* 👓 Mapper de Read Model (Persistencia ↔ DTO/Proyección de Lectura)
|
* - Responsabilidad: transformar registros de persistencia en read models para lectura (listados, resúmenes, informes).
|
||||||
* - Responsabilidad: transformar registros de persistencia en DTOs para lectura (listados, resúmenes, informes).
|
|
||||||
* - No intenta reconstruir agregados ni validar value objects de dominio.
|
* - No intenta reconstruir agregados ni validar value objects de dominio.
|
||||||
**/
|
**/
|
||||||
export interface IQueryMapperWithBulk<TPersistence, TDTO> {
|
export interface IQueryMapperWithBulk<TPersistence, TReadModel> {
|
||||||
/**
|
/**
|
||||||
* Convierte un registro crudo en un DTO de lectura.
|
* Convierte un registro crudo en un read model de lectura.
|
||||||
*/
|
*/
|
||||||
mapToDTO(raw: TPersistence, params?: MapperParamsType): Result<TDTO, Error>;
|
mapToReadModel(raw: TPersistence, params?: MapperParamsType): Result<TReadModel, Error>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convierte múltiples registros crudos en una Collection de DTOs de lectura.
|
* Convierte múltiples registros crudos en una Collection de read models de lectura.
|
||||||
*/
|
*/
|
||||||
mapToDTOCollection(
|
mapToReadModelCollection(
|
||||||
raws: TPersistence[],
|
raws: TPersistence[],
|
||||||
totalCount: number,
|
totalCount: number,
|
||||||
params?: MapperParamsType
|
params?: MapperParamsType
|
||||||
): Result<Collection<TDTO>, Error>;
|
): Result<Collection<TReadModel>, Error>;
|
||||||
}
|
}
|
||||||
@ -1,3 +1,2 @@
|
|||||||
export * from "./errors";
|
export * from "./errors";
|
||||||
export * from "./repositories";
|
|
||||||
export * from "./value-objects";
|
export * from "./value-objects";
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from "./repository.interface";
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { Collection, Result, ResultCollection } from "@repo/rdx-utils";
|
import { Collection, Result, ResultCollection } from "@repo/rdx-utils";
|
||||||
import type { Model } from "sequelize";
|
import type { Model } from "sequelize";
|
||||||
|
|
||||||
import type { MapperParamsType } from "../../../../domain";
|
import type { MapperParamsType } from "../../../../application";
|
||||||
|
|
||||||
import type { ISequelizeDomainMapper } from "./sequelize-mapper.interface";
|
import type { ISequelizeDomainMapper } from "./sequelize-mapper.interface";
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { DomainMapperWithBulk, IQueryMapperWithBulk } from "../../../../domain";
|
import type { DomainMapperWithBulk, IQueryMapperWithBulk } from "@erp/core/api/application";
|
||||||
|
|
||||||
export interface ISequelizeDomainMapper<TModel, TModelAttributes, TEntity>
|
export interface ISequelizeDomainMapper<TModel, TModelAttributes, TEntity>
|
||||||
extends DomainMapperWithBulk<TModel | TModelAttributes, TEntity> {}
|
extends DomainMapperWithBulk<TModel | TModelAttributes, TEntity> {}
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
import { Collection, Result } from "@repo/rdx-utils";
|
import { Collection, Result } from "@repo/rdx-utils";
|
||||||
import type { Model } from "sequelize";
|
import type { Model } from "sequelize";
|
||||||
|
|
||||||
import type { MapperParamsType } from "../../../../domain";
|
import type { MapperParamsType } from "../../../../application";
|
||||||
|
|
||||||
import type { ISequelizeQueryMapper } from "./sequelize-mapper.interface";
|
import type { ISequelizeQueryMapper } from "./sequelize-mapper.interface";
|
||||||
|
|
||||||
export abstract class SequelizeQueryMapper<TModel extends Model, TEntity>
|
export abstract class SequelizeQueryMapper<TModel extends Model, TEntity>
|
||||||
implements ISequelizeQueryMapper<TModel, TEntity>
|
implements ISequelizeQueryMapper<TModel, TEntity>
|
||||||
{
|
{
|
||||||
public abstract mapToDTO(raw: TModel, params?: MapperParamsType): Result<TEntity, Error>;
|
public abstract mapToReadModel(raw: TModel, params?: MapperParamsType): Result<TEntity, Error>;
|
||||||
|
|
||||||
public mapToDTOCollection(
|
public mapToReadModelCollection(
|
||||||
raws: TModel[],
|
raws: TModel[],
|
||||||
totalCount: number,
|
totalCount: number,
|
||||||
params?: MapperParamsType
|
params?: MapperParamsType
|
||||||
@ -23,7 +23,7 @@ export abstract class SequelizeQueryMapper<TModel extends Model, TEntity>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const items = _source.map((value, index) => {
|
const items = _source.map((value, index) => {
|
||||||
const result = this.mapToDTO(value as TModel, { index, ...params });
|
const result = this.mapToReadModel(value as TModel, { index, ...params });
|
||||||
if (result.isFailure) {
|
if (result.isFailure) {
|
||||||
throw result.error;
|
throw result.error;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from "./issued-invoice-list.dto";
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
export * from "./application-models";
|
|
||||||
export * from "./di";
|
export * from "./di";
|
||||||
export * from "./dtos";
|
|
||||||
export * from "./mappers";
|
export * from "./mappers";
|
||||||
|
export * from "./models";
|
||||||
export * from "./repositories";
|
export * from "./repositories";
|
||||||
export * from "./services";
|
export * from "./services";
|
||||||
export * from "./snapshot-builders";
|
export * from "./snapshot-builders";
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import type { MapperParamsType } from "@erp/core/api";
|
import type { MapperParamsType } from "@erp/core/api";
|
||||||
import type { Result } from "@repo/rdx-utils";
|
import type { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { IssuedInvoiceListDTO } from "../dtos";
|
import type { IssuedInvoiceSummary } from "../models";
|
||||||
|
|
||||||
export interface IIssuedInvoiceListMapper {
|
export interface IIssuedInvoiceSummaryMapper {
|
||||||
mapToDTO(raw: unknown, params?: MapperParamsType): Result<IssuedInvoiceListDTO, Error>;
|
mapToDTO(raw: unknown, params?: MapperParamsType): Result<IssuedInvoiceSummary, Error>;
|
||||||
}
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./issued-invoice-summary";
|
||||||
@ -10,7 +10,7 @@ import type {
|
|||||||
VerifactuRecord,
|
VerifactuRecord,
|
||||||
} from "../../../domain";
|
} from "../../../domain";
|
||||||
|
|
||||||
export type IssuedInvoiceListDTO = {
|
export type IssuedInvoiceSummary = {
|
||||||
id: UniqueID;
|
id: UniqueID;
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
|
|
||||||
@ -3,7 +3,7 @@ import type { UniqueID } from "@repo/rdx-ddd";
|
|||||||
import type { Collection, Result } from "@repo/rdx-utils";
|
import type { Collection, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { IssuedInvoice } from "../../../domain";
|
import type { IssuedInvoice } from "../../../domain";
|
||||||
import type { IssuedInvoiceListDTO } from "../dtos";
|
import type { IssuedInvoiceSummary } from "../models";
|
||||||
|
|
||||||
export interface IIssuedInvoiceRepository {
|
export interface IIssuedInvoiceRepository {
|
||||||
create(invoice: IssuedInvoice, transaction?: unknown): Promise<Result<void, Error>>;
|
create(invoice: IssuedInvoice, transaction?: unknown): Promise<Result<void, Error>>;
|
||||||
@ -24,5 +24,5 @@ export interface IIssuedInvoiceRepository {
|
|||||||
companyId: UniqueID,
|
companyId: UniqueID,
|
||||||
criteria: Criteria,
|
criteria: Criteria,
|
||||||
transaction: unknown
|
transaction: unknown
|
||||||
): Promise<Result<Collection<IssuedInvoiceListDTO>, Error>>;
|
): Promise<Result<Collection<IssuedInvoiceSummary>, Error>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import type { Collection, Result } from "@repo/rdx-utils";
|
|||||||
import type { Transaction } from "sequelize";
|
import type { Transaction } from "sequelize";
|
||||||
|
|
||||||
import type { IssuedInvoice } from "../../../domain";
|
import type { IssuedInvoice } from "../../../domain";
|
||||||
import type { IssuedInvoiceListDTO } from "../dtos";
|
import type { IssuedInvoiceSummary } from "../models";
|
||||||
import type { IIssuedInvoiceRepository } from "../repositories";
|
import type { IIssuedInvoiceRepository } from "../repositories";
|
||||||
|
|
||||||
export interface IIssuedInvoiceFinder {
|
export interface IIssuedInvoiceFinder {
|
||||||
@ -24,7 +24,7 @@ export interface IIssuedInvoiceFinder {
|
|||||||
companyId: UniqueID,
|
companyId: UniqueID,
|
||||||
criteria: Criteria,
|
criteria: Criteria,
|
||||||
transaction?: Transaction
|
transaction?: Transaction
|
||||||
): Promise<Result<Collection<IssuedInvoiceListDTO>, Error>>;
|
): Promise<Result<Collection<IssuedInvoiceSummary>, Error>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IssuedInvoiceFinder implements IIssuedInvoiceFinder {
|
export class IssuedInvoiceFinder implements IIssuedInvoiceFinder {
|
||||||
@ -50,7 +50,7 @@ export class IssuedInvoiceFinder implements IIssuedInvoiceFinder {
|
|||||||
companyId: UniqueID,
|
companyId: UniqueID,
|
||||||
criteria: Criteria,
|
criteria: Criteria,
|
||||||
transaction?: Transaction
|
transaction?: Transaction
|
||||||
): Promise<Result<Collection<IssuedInvoiceListDTO>, Error>> {
|
): Promise<Result<Collection<IssuedInvoiceSummary>, Error>> {
|
||||||
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction);
|
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import type { ISnapshotBuilder } from "@erp/core/api";
|
import type { ISnapshotBuilder } from "@erp/core/api";
|
||||||
import { maybeToEmptyString } from "@repo/rdx-ddd";
|
import { maybeToEmptyString } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
import type { IssuedInvoiceListDTO } from "../../dtos";
|
import type { IssuedInvoiceSummary } from "../../models";
|
||||||
|
|
||||||
import type { IIssuedInvoiceListItemSnapshot } from "./issued-invoice-list-item-snapshot.interface";
|
import type { IIssuedInvoiceListItemSnapshot } from "./issued-invoice-list-item-snapshot.interface";
|
||||||
|
|
||||||
export interface IIssuedInvoiceListItemSnapshotBuilder
|
export interface IIssuedInvoiceListItemSnapshotBuilder
|
||||||
extends ISnapshotBuilder<IssuedInvoiceListDTO, IIssuedInvoiceListItemSnapshot> {}
|
extends ISnapshotBuilder<IssuedInvoiceSummary, IIssuedInvoiceListItemSnapshot> {}
|
||||||
|
|
||||||
export class IssuedInvoiceListItemSnapshotBuilder implements IIssuedInvoiceListItemSnapshotBuilder {
|
export class IssuedInvoiceListItemSnapshotBuilder implements IIssuedInvoiceListItemSnapshotBuilder {
|
||||||
toOutput(invoice: IssuedInvoiceListDTO): IIssuedInvoiceListItemSnapshot {
|
toOutput(invoice: IssuedInvoiceSummary): IIssuedInvoiceListItemSnapshot {
|
||||||
const recipient = invoice.recipient.toObjectString();
|
const recipient = invoice.recipient.toObjectString();
|
||||||
|
|
||||||
const verifactu = invoice.verifactu.match(
|
const verifactu = invoice.verifactu.match(
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from "./proforma-list.dto";
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
export * from "./application-models";
|
|
||||||
export * from "./di";
|
export * from "./di";
|
||||||
export * from "./dtos";
|
|
||||||
export * from "./mappers";
|
export * from "./mappers";
|
||||||
|
export * from "./models";
|
||||||
export * from "./repositories";
|
export * from "./repositories";
|
||||||
export * from "./services";
|
export * from "./services";
|
||||||
export * from "./snapshot-builders";
|
export * from "./snapshot-builders";
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { MapperParamsType } from "@erp/core/api";
|
import type { MapperParamsType } from "@erp/core/api";
|
||||||
import type { Result } from "@repo/rdx-utils";
|
import type { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { ProformaListDTO } from "../dtos";
|
import type { ProformaListDTO } from "../models";
|
||||||
|
|
||||||
export interface IProformaListMapper {
|
export interface IProformaListMapper {
|
||||||
mapToDTO(raw: unknown, params?: MapperParamsType): Result<ProformaListDTO, Error>;
|
mapToDTO(raw: unknown, params?: MapperParamsType): Result<ProformaListDTO, Error>;
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./proforma-resume";
|
||||||
@ -3,7 +3,7 @@ import type { UniqueID } from "@repo/rdx-ddd";
|
|||||||
import type { Collection, Result } from "@repo/rdx-utils";
|
import type { Collection, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { InvoiceStatus, Proforma } from "../../../domain";
|
import type { InvoiceStatus, Proforma } from "../../../domain";
|
||||||
import type { ProformaListDTO } from "../dtos";
|
import type { ProformaListDTO } from "../models";
|
||||||
|
|
||||||
export interface IProformaRepository {
|
export interface IProformaRepository {
|
||||||
create(proforma: Proforma, transaction?: unknown): Promise<Result<void, Error>>;
|
create(proforma: Proforma, transaction?: unknown): Promise<Result<void, Error>>;
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import type { Collection, Result } from "@repo/rdx-utils";
|
|||||||
import type { Transaction } from "sequelize";
|
import type { Transaction } from "sequelize";
|
||||||
|
|
||||||
import type { Proforma } from "../../../domain";
|
import type { Proforma } from "../../../domain";
|
||||||
import type { ProformaListDTO } from "../dtos";
|
import type { ProformaListDTO } from "../models";
|
||||||
import type { IProformaRepository } from "../repositories";
|
import type { IProformaRepository } from "../repositories";
|
||||||
|
|
||||||
export interface IProformaFinder {
|
export interface IProformaFinder {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { ISnapshotBuilder } from "@erp/core/api";
|
import type { ISnapshotBuilder } from "@erp/core/api";
|
||||||
import { maybeToEmptyString } from "@repo/rdx-ddd";
|
import { maybeToEmptyString } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
import type { ProformaListDTO } from "../../dtos";
|
import type { ProformaListDTO } from "../../models";
|
||||||
|
|
||||||
import type { IProformaListItemSnapshot } from "./proforma-list-item-snapshot.interface";
|
import type { IProformaListItemSnapshot } from "./proforma-list-item-snapshot.interface";
|
||||||
|
|
||||||
|
|||||||
@ -192,6 +192,7 @@ export class CustomerInvoice
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Method to get the complete list of line items
|
// Method to get the complete list of line items
|
||||||
|
|
||||||
public get items(): CustomerInvoiceItems {
|
public get items(): CustomerInvoiceItems {
|
||||||
return this._items;
|
return this._items;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { IssuedInvoiceListDTO } from "../../../../../../application";
|
import type { IssuedInvoiceSummary } from "../../../../../../application";
|
||||||
import { InvoiceRecipient } from "../../../../../../domain";
|
import { InvoiceRecipient } from "../../../../../../domain";
|
||||||
import type { CustomerInvoiceModel } from "../../../../../common";
|
import type { CustomerInvoiceModel } from "../../../../../common";
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ export class SequelizeIssuedInvoiceRecipientListMapper extends SequelizeQueryMap
|
|||||||
|
|
||||||
const { errors, attributes } = params as {
|
const { errors, attributes } = params as {
|
||||||
errors: ValidationErrorDetail[];
|
errors: ValidationErrorDetail[];
|
||||||
attributes: Partial<IssuedInvoiceListDTO>;
|
attributes: Partial<IssuedInvoiceSummary>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { isProforma } = attributes;
|
const { isProforma } = attributes;
|
||||||
|
|||||||
@ -11,7 +11,10 @@ import {
|
|||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
import { Maybe, Result } from "@repo/rdx-utils";
|
import { Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { IIssuedInvoiceListMapper, IssuedInvoiceListDTO } from "../../../../../../application";
|
import type {
|
||||||
|
IIssuedInvoiceSummaryMapper,
|
||||||
|
IssuedInvoiceSummary,
|
||||||
|
} from "../../../../../../application";
|
||||||
import {
|
import {
|
||||||
InvoiceAmount,
|
InvoiceAmount,
|
||||||
InvoiceNumber,
|
InvoiceNumber,
|
||||||
@ -25,8 +28,8 @@ import { SequelizeIssuedInvoiceRecipientListMapper } from "./sequelize-issued-in
|
|||||||
import { SequelizeVerifactuRecordListMapper } from "./sequelize-verifactu-record.list.mapper";
|
import { SequelizeVerifactuRecordListMapper } from "./sequelize-verifactu-record.list.mapper";
|
||||||
|
|
||||||
export class SequelizeIssuedInvoiceListMapper
|
export class SequelizeIssuedInvoiceListMapper
|
||||||
extends SequelizeQueryMapper<CustomerInvoiceModel, IssuedInvoiceListDTO>
|
extends SequelizeQueryMapper<CustomerInvoiceModel, IssuedInvoiceSummary>
|
||||||
implements IIssuedInvoiceListMapper
|
implements IIssuedInvoiceSummaryMapper
|
||||||
{
|
{
|
||||||
private _recipientMapper: SequelizeIssuedInvoiceRecipientListMapper;
|
private _recipientMapper: SequelizeIssuedInvoiceRecipientListMapper;
|
||||||
private _verifactuMapper: SequelizeVerifactuRecordListMapper;
|
private _verifactuMapper: SequelizeVerifactuRecordListMapper;
|
||||||
@ -37,14 +40,14 @@ export class SequelizeIssuedInvoiceListMapper
|
|||||||
this._verifactuMapper = new SequelizeVerifactuRecordListMapper();
|
this._verifactuMapper = new SequelizeVerifactuRecordListMapper();
|
||||||
}
|
}
|
||||||
|
|
||||||
public mapToDTO(
|
public mapToReadModel(
|
||||||
raw: CustomerInvoiceModel,
|
raw: CustomerInvoiceModel,
|
||||||
params?: MapperParamsType
|
params?: MapperParamsType
|
||||||
): Result<IssuedInvoiceListDTO, Error> {
|
): Result<IssuedInvoiceSummary, Error> {
|
||||||
const errors: ValidationErrorDetail[] = [];
|
const errors: ValidationErrorDetail[] = [];
|
||||||
|
|
||||||
// 1) Valores escalares (atributos generales)
|
// 1) Valores escalares (atributos generales)
|
||||||
const attributes = this.mapAttributesToDTO(raw, { errors, ...params });
|
const attributes = this._mapAttributesToReadModel(raw, { errors, ...params });
|
||||||
|
|
||||||
// 2) Recipient (snapshot en la factura o include)
|
// 2) Recipient (snapshot en la factura o include)
|
||||||
const recipientResult = this._recipientMapper.mapToDTO(raw, {
|
const recipientResult = this._recipientMapper.mapToDTO(raw, {
|
||||||
@ -111,7 +114,7 @@ export class SequelizeIssuedInvoiceListMapper
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapAttributesToDTO(raw: CustomerInvoiceModel, params?: MapperParamsType) {
|
private _mapAttributesToReadModel(raw: CustomerInvoiceModel, params?: MapperParamsType) {
|
||||||
const { errors } = params as {
|
const { errors } = params as {
|
||||||
errors: ValidationErrorDetail[];
|
errors: ValidationErrorDetail[];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import type { UniqueID } from "@repo/rdx-ddd";
|
|||||||
import { type Collection, Result } from "@repo/rdx-utils";
|
import { type Collection, Result } from "@repo/rdx-utils";
|
||||||
import type { FindOptions, InferAttributes, OrderItem, Sequelize, Transaction } from "sequelize";
|
import type { FindOptions, InferAttributes, OrderItem, Sequelize, Transaction } from "sequelize";
|
||||||
|
|
||||||
import type { IIssuedInvoiceRepository, IssuedInvoiceListDTO } from "../../../../../application";
|
import type { IIssuedInvoiceRepository, IssuedInvoiceSummary } from "../../../../../application";
|
||||||
import type { IssuedInvoice } from "../../../../../domain";
|
import type { IssuedInvoice } from "../../../../../domain";
|
||||||
import {
|
import {
|
||||||
CustomerInvoiceItemModel,
|
CustomerInvoiceItemModel,
|
||||||
@ -212,7 +212,7 @@ export class IssuedInvoiceRepository
|
|||||||
criteria: Criteria,
|
criteria: Criteria,
|
||||||
transaction: Transaction,
|
transaction: Transaction,
|
||||||
options: FindOptions<InferAttributes<CustomerInvoiceModel>> = {}
|
options: FindOptions<InferAttributes<CustomerInvoiceModel>> = {}
|
||||||
): Promise<Result<Collection<IssuedInvoiceListDTO>, Error>> {
|
): Promise<Result<Collection<IssuedInvoiceSummary>, Error>> {
|
||||||
const { CustomerModel } = this.database.models;
|
const { CustomerModel } = this.database.models;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -301,7 +301,7 @@ export class IssuedInvoiceRepository
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return this.listMapper.mapToDTOCollection(rows, count);
|
return this.listMapper.mapToReadModelCollection(rows, count);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
return Result.fail(translateSequelizeError(err));
|
return Result.fail(translateSequelizeError(err));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,23 +2,23 @@ import type { ProformasInternalDeps } from "./proformas.di";
|
|||||||
|
|
||||||
export type ProformasServicesDeps = {
|
export type ProformasServicesDeps = {
|
||||||
services: {
|
services: {
|
||||||
listIssuedInvoices: (filters: unknown, context: unknown) => null;
|
listProformas: (filters: unknown, context: unknown) => null;
|
||||||
getIssuedInvoiceById: (id: unknown, context: unknown) => null;
|
getProformaById: (id: unknown, context: unknown) => null;
|
||||||
generateIssuedInvoiceReport: (id: unknown, options: unknown, context: unknown) => null;
|
generateProformaReport: (id: unknown, options: unknown, context: unknown) => null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function buildProformaServices(deps: ProformasInternalDeps): ProformasServicesDeps {
|
export function buildProformaServices(deps: ProformasInternalDeps): ProformasServicesDeps {
|
||||||
return {
|
return {
|
||||||
services: {
|
services: {
|
||||||
listIssuedInvoices: (filters, context) => null,
|
listProformas: (filters, context) => null,
|
||||||
//internal.useCases.listIssuedInvoices().execute(filters, context),
|
//internal.useCases.listProformas().execute(filters, context),
|
||||||
|
|
||||||
getIssuedInvoiceById: (id, context) => null,
|
getProformaById: (id, context) => null,
|
||||||
//internal.useCases.getIssuedInvoiceById().execute(id, context),
|
//internal.useCases.getProformaById().execute(id, context),
|
||||||
|
|
||||||
generateIssuedInvoiceReport: (id, options, context) => null,
|
generateProformaReport: (id, options, context) => null,
|
||||||
//internal.useCases.reportIssuedInvoice().execute(id, options, context),
|
//internal.useCases.reportProforma().execute(id, options, context),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -368,10 +368,12 @@ export class ProformaRepository
|
|||||||
? [options.include]
|
? [options.include]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
console.log(query.where);
|
||||||
|
|
||||||
query.where = {
|
query.where = {
|
||||||
...query.where,
|
...query.where,
|
||||||
...(options.where ?? {}),
|
...(options.where ?? {}),
|
||||||
is_proforma: false,
|
is_proforma: true,
|
||||||
company_id: companyId.toString(),
|
company_id: companyId.toString(),
|
||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
};
|
};
|
||||||
@ -422,7 +424,7 @@ export class ProformaRepository
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return this.listMapper.mapToDTOCollection(rows, count);
|
return this.listMapper.mapToReadModelCollection(rows, count);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
return Result.fail(translateSequelizeError(err));
|
return Result.fail(translateSequelizeError(err));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,29 +2,32 @@ import type { ModuleClientParams } from "@erp/core/client";
|
|||||||
import { lazy } from "react";
|
import { lazy } from "react";
|
||||||
import { Outlet, type RouteObject } from "react-router-dom";
|
import { Outlet, type RouteObject } from "react-router-dom";
|
||||||
|
|
||||||
|
import { ProformaCreatePage } from "./proformas/create";
|
||||||
|
|
||||||
const ProformaLayout = lazy(() =>
|
const ProformaLayout = lazy(() =>
|
||||||
import("./proformas/ui").then((m) => ({ default: m.ProformaLayout }))
|
import("./proformas/ui").then((m) => ({ default: m.ProformaLayout }))
|
||||||
);
|
);
|
||||||
|
|
||||||
const IssuedInvoicesLayout = lazy(() =>
|
|
||||||
import("./issued-invoices/ui").then((m) => ({ default: m.IssuedInvoicesLayout }))
|
|
||||||
);
|
|
||||||
|
|
||||||
const ProformasListPage = lazy(() =>
|
const ProformasListPage = lazy(() =>
|
||||||
import("./proformas/list").then((m) => ({ default: m.ProformaListPage }))
|
import("./proformas/list").then((m) => ({ default: m.ProformaListPage }))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ProformasCreatePage = lazy(() =>
|
||||||
|
import("./proformas/create").then((m) => ({ default: m.ProformaCreatePage }))
|
||||||
|
);
|
||||||
|
|
||||||
|
/*const InvoiceUpdatePage = lazy(() =>
|
||||||
|
import("./pages").then((m) => ({ default: m.InvoiceUpdatePage }))
|
||||||
|
);*/
|
||||||
|
|
||||||
|
const IssuedInvoicesLayout = lazy(() =>
|
||||||
|
import("./issued-invoices/ui").then((m) => ({ default: m.IssuedInvoicesLayout }))
|
||||||
|
);
|
||||||
|
|
||||||
const IssuedInvoiceListPage = lazy(() =>
|
const IssuedInvoiceListPage = lazy(() =>
|
||||||
import("./issued-invoices/list").then((m) => ({ default: m.IssuedInvoiceListPage }))
|
import("./issued-invoices/list").then((m) => ({ default: m.IssuedInvoiceListPage }))
|
||||||
);
|
);
|
||||||
|
|
||||||
/*const CustomerInvoiceAdd = lazy(() =>
|
|
||||||
import("./pages").then((m) => ({ default: m.CustomerInvoiceCreate }))
|
|
||||||
);
|
|
||||||
const InvoiceUpdatePage = lazy(() =>
|
|
||||||
import("./pages").then((m) => ({ default: m.InvoiceUpdatePage }))
|
|
||||||
);*/
|
|
||||||
|
|
||||||
export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[] => {
|
export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[] => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -37,7 +40,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
|||||||
children: [
|
children: [
|
||||||
{ path: "", index: true, element: <ProformasListPage /> }, // index
|
{ path: "", index: true, element: <ProformasListPage /> }, // index
|
||||||
{ path: "list", element: <ProformasListPage /> },
|
{ path: "list", element: <ProformasListPage /> },
|
||||||
//{ path: "create", element: <CustomerInvoiceAdd /> },
|
{ path: "create", element: <ProformaCreatePage /> },
|
||||||
//{ path: ":id/edit", element: <InvoiceUpdatePage /> },
|
//{ path: ":id/edit", element: <InvoiceUpdatePage /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { MoneyDTOHelper, PercentageDTOHelper, formatCurrency } from "@erp/core";
|
import { MoneyDTOHelper, formatCurrency } from "@erp/core";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
IssuedInvoiceSummaryData,
|
IssuedInvoiceSummaryData,
|
||||||
@ -26,14 +26,14 @@ export const IssuedInvoiceSummaryDtoAdapter = {
|
|||||||
summaryDto.language_code
|
summaryDto.language_code
|
||||||
),
|
),
|
||||||
|
|
||||||
discount_percentage: PercentageDTOHelper.toNumber(summaryDto.discount_percentage),
|
/*discount_percentage: PercentageDTOHelper.toNumber(summaryDto.discount_percentage),
|
||||||
discount_percentage_fmt: PercentageDTOHelper.toNumericString(
|
discount_percentage_fmt: PercentageDTOHelper.toNumericString(
|
||||||
summaryDto.discount_percentage
|
summaryDto.discount_percentage
|
||||||
),
|
),*/
|
||||||
|
|
||||||
discount_amount: MoneyDTOHelper.toNumber(summaryDto.discount_amount),
|
discount_amount: MoneyDTOHelper.toNumber(summaryDto.total_discount_amount),
|
||||||
discount_amount_fmt: formatCurrency(
|
discount_amount_fmt: formatCurrency(
|
||||||
MoneyDTOHelper.toNumber(summaryDto.discount_amount),
|
MoneyDTOHelper.toNumber(summaryDto.total_discount_amount),
|
||||||
Number(summaryDto.total_amount.scale || 2),
|
Number(summaryDto.total_amount.scale || 2),
|
||||||
summaryDto.currency_code,
|
summaryDto.currency_code,
|
||||||
summaryDto.language_code
|
summaryDto.language_code
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./ui";
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./pages";
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./proforma-create-page";
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
import { PageHeader } from "@erp/core/components";
|
||||||
|
import { AppContent, AppHeader } from "@repo/rdx-ui/components";
|
||||||
|
import { Button } from "@repo/shadcn-ui/components";
|
||||||
|
import { PlusIcon } from "lucide-react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import { useTranslation } from "../../../../i18n";
|
||||||
|
|
||||||
|
export const ProformaCreatePage = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AppHeader>
|
||||||
|
<PageHeader
|
||||||
|
description={t("pages.proformas.list.description")}
|
||||||
|
rightSlot={
|
||||||
|
<Button
|
||||||
|
aria-label={t("pages.proformas.create.title")}
|
||||||
|
onClick={() => navigate("/proformas/create")}
|
||||||
|
>
|
||||||
|
<PlusIcon aria-hidden className="mr-2 size-4" />
|
||||||
|
{t("pages.proformas.create.title")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
title={t("pages.proformas.list.title")}
|
||||||
|
/>
|
||||||
|
</AppHeader>
|
||||||
|
|
||||||
|
<AppContent>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<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">hola</div>
|
||||||
|
</AppContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -63,7 +63,6 @@ export const ProformaListPage = () => {
|
|||||||
rightSlot={
|
rightSlot={
|
||||||
<Button
|
<Button
|
||||||
aria-label={t("pages.proformas.create.title")}
|
aria-label={t("pages.proformas.create.title")}
|
||||||
className="hidden"
|
|
||||||
onClick={() => navigate("/proformas/create")}
|
onClick={() => navigate("/proformas/create")}
|
||||||
>
|
>
|
||||||
<PlusIcon aria-hidden className="mr-2 size-4" />
|
<PlusIcon aria-hidden className="mr-2 size-4" />
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { Criteria } from "@repo/rdx-criteria/server";
|
|||||||
import { UniqueID } from "@repo/rdx-ddd";
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Collection, Result } from "@repo/rdx-utils";
|
import { Collection, Result } from "@repo/rdx-utils";
|
||||||
import { Transaction } from "sequelize";
|
import { Transaction } from "sequelize";
|
||||||
import { Customer, CustomerPatchProps, CustomerProps, ICustomerRepository } from "../domain";
|
import { Customer, CustomerPatchProps, ICustomerProps, ICustomerRepository } from "../domain";
|
||||||
import { CustomerListDTO } from "../infrastructure";
|
import { CustomerListDTO } from "../infrastructure";
|
||||||
|
|
||||||
export class CustomerApplicationService {
|
export class CustomerApplicationService {
|
||||||
@ -19,7 +19,7 @@ export class CustomerApplicationService {
|
|||||||
*/
|
*/
|
||||||
buildCustomerInCompany(
|
buildCustomerInCompany(
|
||||||
companyId: UniqueID,
|
companyId: UniqueID,
|
||||||
props: Omit<CustomerProps, "companyId">,
|
props: Omit<ICustomerProps, "companyId">,
|
||||||
customerId?: UniqueID
|
customerId?: UniqueID
|
||||||
): Result<Customer, Error> {
|
): Result<Customer, Error> {
|
||||||
return Customer.create({ ...props, companyId }, customerId);
|
return Customer.create({ ...props, companyId }, customerId);
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
import type { IProformaRepository } from "../repositories";
|
||||||
|
import { type IProformaFinder, ProformaFinder } from "../services";
|
||||||
|
|
||||||
|
export function buildProformaFinder(repository: IProformaRepository): IProformaFinder {
|
||||||
|
return new ProformaFinder(repository);
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
// application/issued-invoices/di/snapshot-builders.di.ts
|
||||||
|
|
||||||
|
import {
|
||||||
|
CustomerFullSnapshotBuilder,
|
||||||
|
CustomerItemReportSnapshotBuilder,
|
||||||
|
CustomerItemsFullSnapshotBuilder,
|
||||||
|
CustomerListItemSnapshotBuilder,
|
||||||
|
CustomerRecipientFullSnapshotBuilder,
|
||||||
|
CustomerReportSnapshotBuilder,
|
||||||
|
CustomerTaxReportSnapshotBuilder,
|
||||||
|
CustomerTaxesFullSnapshotBuilder,
|
||||||
|
} from "../snapshot-builders";
|
||||||
|
|
||||||
|
export function buildCustomerSnapshotBuilders() {
|
||||||
|
const itemsBuilder = new CustomerItemsFullSnapshotBuilder();
|
||||||
|
|
||||||
|
const taxesBuilder = new CustomerTaxesFullSnapshotBuilder();
|
||||||
|
|
||||||
|
const recipientBuilder = new CustomerRecipientFullSnapshotBuilder();
|
||||||
|
|
||||||
|
const fullSnapshotBuilder = new CustomerFullSnapshotBuilder(
|
||||||
|
itemsBuilder,
|
||||||
|
recipientBuilder,
|
||||||
|
taxesBuilder
|
||||||
|
);
|
||||||
|
|
||||||
|
const listSnapshotBuilder = new CustomerListItemSnapshotBuilder();
|
||||||
|
|
||||||
|
const itemsReportBuilder = new CustomerItemReportSnapshotBuilder();
|
||||||
|
const taxesReportBuilder = new CustomerTaxReportSnapshotBuilder();
|
||||||
|
const reportSnapshotBuilder = new CustomerReportSnapshotBuilder(
|
||||||
|
itemsReportBuilder,
|
||||||
|
taxesReportBuilder
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
full: fullSnapshotBuilder,
|
||||||
|
list: listSnapshotBuilder,
|
||||||
|
report: reportSnapshotBuilder,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
import type { ITransactionManager } from "@erp/core/api";
|
||||||
|
|
||||||
|
import type { ICreateCustomerInputMapper } from "../mappers";
|
||||||
|
import type {
|
||||||
|
ICustomerCreator,
|
||||||
|
ICustomerFinder,
|
||||||
|
CustomerDocumentGeneratorService,
|
||||||
|
} from "../services";
|
||||||
|
import type {
|
||||||
|
ICustomerListItemSnapshotBuilder,
|
||||||
|
ICustomerReportSnapshotBuilder,
|
||||||
|
} from "../snapshot-builders";
|
||||||
|
import type { ICustomerFullSnapshotBuilder } from "../snapshot-builders/full";
|
||||||
|
import { GetCustomerByIdUseCase, ListCustomersUseCase, ReportCustomerUseCase } from "../use-cases";
|
||||||
|
import { CreateCustomerUseCase } from "../use-cases/create-customer";
|
||||||
|
|
||||||
|
export function buildGetCustomerByIdUseCase(deps: {
|
||||||
|
finder: ICustomerFinder;
|
||||||
|
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
||||||
|
transactionManager: ITransactionManager;
|
||||||
|
}) {
|
||||||
|
return new GetCustomerByIdUseCase(deps.finder, deps.fullSnapshotBuilder, deps.transactionManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildListCustomersUseCase(deps: {
|
||||||
|
finder: ICustomerFinder;
|
||||||
|
itemSnapshotBuilder: ICustomerListItemSnapshotBuilder;
|
||||||
|
transactionManager: ITransactionManager;
|
||||||
|
}) {
|
||||||
|
return new ListCustomersUseCase(deps.finder, deps.itemSnapshotBuilder, deps.transactionManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildReportCustomerUseCase(deps: {
|
||||||
|
finder: ICustomerFinder;
|
||||||
|
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
||||||
|
reportSnapshotBuilder: ICustomerReportSnapshotBuilder;
|
||||||
|
documentService: CustomerDocumentGeneratorService;
|
||||||
|
transactionManager: ITransactionManager;
|
||||||
|
}) {
|
||||||
|
return new ReportCustomerUseCase(
|
||||||
|
deps.finder,
|
||||||
|
deps.fullSnapshotBuilder,
|
||||||
|
deps.reportSnapshotBuilder,
|
||||||
|
deps.documentService,
|
||||||
|
deps.transactionManager
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCreateCustomerUseCase(deps: {
|
||||||
|
creator: ICustomerCreator;
|
||||||
|
dtoMapper: ICreateCustomerInputMapper;
|
||||||
|
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
||||||
|
transactionManager: ITransactionManager;
|
||||||
|
}) {
|
||||||
|
return new CreateCustomerUseCase({
|
||||||
|
dtoMapper: deps.dtoMapper,
|
||||||
|
creator: deps.creator,
|
||||||
|
fullSnapshotBuilder: deps.fullSnapshotBuilder,
|
||||||
|
transactionManager: deps.transactionManager,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*export function buildUpdateCustomerUseCase(deps: {
|
||||||
|
finder: ICustomerFinder;
|
||||||
|
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
||||||
|
}) {
|
||||||
|
return new UpdateCustomerUseCase(deps.finder, deps.fullSnapshotBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDeleteCustomerUseCase(deps: { finder: ICustomerFinder }) {
|
||||||
|
return new DeleteCustomerUseCase(deps.finder);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildIssueCustomerUseCase(deps: { finder: ICustomerFinder }) {
|
||||||
|
return new IssueCustomerUseCase(deps.finder);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildChangeStatusCustomerUseCase(deps: {
|
||||||
|
finder: ICustomerFinder;
|
||||||
|
transactionManager: ITransactionManager;
|
||||||
|
}) {
|
||||||
|
return new ChangeStatusCustomerUseCase(deps.finder, deps.transactionManager);
|
||||||
|
}*/
|
||||||
5
modules/customers/src/api/application/di/index.ts
Normal file
5
modules/customers/src/api/application/di/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from "./customer-creator.di";
|
||||||
|
export * from "./customer-finder.di";
|
||||||
|
export * from "./customer-input-mappers.di";
|
||||||
|
export * from "./customer-snapshot-builders.di";
|
||||||
|
export * from "./customer-use-cases.di";
|
||||||
@ -1,3 +1,7 @@
|
|||||||
//export * from "./customer-application.service";
|
export * from "./di";
|
||||||
//export * from "./presenters";
|
export * from "./mappers";
|
||||||
|
export * from "./models";
|
||||||
|
export * from "./repositories";
|
||||||
|
export * from "./services";
|
||||||
|
export * from "./snapshot-builders";
|
||||||
export * from "./use-cases";
|
export * from "./use-cases";
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
import type { DomainMapperWithBulk } from "@erp/core/api";
|
||||||
|
|
||||||
|
import type { Customer } from "../../domain";
|
||||||
|
import type { CustomerCreationAttributes, CustomerModel } from "../../infrastructure";
|
||||||
|
|
||||||
|
export interface ICustomerDomainMapper
|
||||||
|
extends DomainMapperWithBulk<CustomerModel | CustomerCreationAttributes, Customer> {}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import type { IQueryMapperWithBulk } from "@erp/core/api";
|
||||||
|
|
||||||
|
import type { CustomerModel } from "../../infrastructure";
|
||||||
|
import type { CustomerSummary } from "../models";
|
||||||
|
|
||||||
|
export interface ICustomerSummaryMapper
|
||||||
|
extends IQueryMapperWithBulk<CustomerModel, CustomerSummary> {}
|
||||||
2
modules/customers/src/api/application/mappers/index.ts
Normal file
2
modules/customers/src/api/application/mappers/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./customer-domain-mapper.interface";
|
||||||
|
export * from "./customer-summary-mapper.interface";
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
import type {
|
||||||
|
CurrencyCode,
|
||||||
|
EmailAddress,
|
||||||
|
LanguageCode,
|
||||||
|
Name,
|
||||||
|
PhoneNumber,
|
||||||
|
PostalAddress,
|
||||||
|
TINNumber,
|
||||||
|
URLAddress,
|
||||||
|
UniqueID,
|
||||||
|
} from "@repo/rdx-ddd";
|
||||||
|
import type { Maybe } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import type { CustomerStatus } from "../../domain";
|
||||||
|
|
||||||
|
export type CustomerSummary = {
|
||||||
|
id: UniqueID;
|
||||||
|
companyId: UniqueID;
|
||||||
|
status: CustomerStatus;
|
||||||
|
reference: Maybe<Name>;
|
||||||
|
|
||||||
|
isCompany: boolean;
|
||||||
|
name: Name;
|
||||||
|
tradeName: Maybe<Name>;
|
||||||
|
tin: Maybe<TINNumber>;
|
||||||
|
|
||||||
|
address: PostalAddress;
|
||||||
|
|
||||||
|
emailPrimary: Maybe<EmailAddress>;
|
||||||
|
emailSecondary: Maybe<EmailAddress>;
|
||||||
|
phonePrimary: Maybe<PhoneNumber>;
|
||||||
|
phoneSecondary: Maybe<PhoneNumber>;
|
||||||
|
mobilePrimary: Maybe<PhoneNumber>;
|
||||||
|
mobileSecondary: Maybe<PhoneNumber>;
|
||||||
|
|
||||||
|
fax: Maybe<PhoneNumber>;
|
||||||
|
website: Maybe<URLAddress>;
|
||||||
|
|
||||||
|
languageCode: LanguageCode;
|
||||||
|
currencyCode: CurrencyCode;
|
||||||
|
};
|
||||||
1
modules/customers/src/api/application/models/index.ts
Normal file
1
modules/customers/src/api/application/models/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./customer-summary";
|
||||||
@ -1,8 +1,9 @@
|
|||||||
import { Criteria } from "@repo/rdx-criteria/server";
|
import type { Criteria } from "@repo/rdx-criteria/server";
|
||||||
import { UniqueID } from "@repo/rdx-ddd";
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Collection, Result } from "@repo/rdx-utils";
|
import type { Collection, Result } from "@repo/rdx-utils";
|
||||||
import { CustomerListDTO } from "../../infrastructure/mappers";
|
|
||||||
import { Customer } from "../aggregates";
|
import type { Customer } from "../../domain/aggregates";
|
||||||
|
import type { CustomerSummary } from "../models";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interfaz del repositorio para el agregado `Customer`.
|
* Interfaz del repositorio para el agregado `Customer`.
|
||||||
@ -56,7 +57,7 @@ export interface ICustomerRepository {
|
|||||||
companyId: UniqueID,
|
companyId: UniqueID,
|
||||||
criteria: Criteria,
|
criteria: Criteria,
|
||||||
transaction: unknown
|
transaction: unknown
|
||||||
): Promise<Result<Collection<CustomerListDTO>, Error>>;
|
): Promise<Result<Collection<CustomerSummary>, Error>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Elimina un Customer por su ID, dentro de una empresa.
|
* Elimina un Customer por su ID, dentro de una empresa.
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
import type { Criteria } from "@repo/rdx-criteria/server";
|
||||||
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
|
import type { Collection, Result } from "@repo/rdx-utils";
|
||||||
|
import type { Transaction } from "sequelize";
|
||||||
|
|
||||||
|
import type { Customer } from "../../domain";
|
||||||
|
import type { CustomerListDTO } from "../dtos";
|
||||||
|
import type { ICustomerRepository } from "../repositories";
|
||||||
|
|
||||||
|
export interface ICustomerFinder {
|
||||||
|
findCustomerById(
|
||||||
|
companyId: UniqueID,
|
||||||
|
invoiceId: UniqueID,
|
||||||
|
transaction?: Transaction
|
||||||
|
): Promise<Result<Customer, Error>>;
|
||||||
|
|
||||||
|
customerExists(
|
||||||
|
companyId: UniqueID,
|
||||||
|
invoiceId: UniqueID,
|
||||||
|
transaction?: Transaction
|
||||||
|
): Promise<Result<boolean, Error>>;
|
||||||
|
|
||||||
|
findCustomersByCriteria(
|
||||||
|
companyId: UniqueID,
|
||||||
|
criteria: Criteria,
|
||||||
|
transaction?: Transaction
|
||||||
|
): Promise<Result<Collection<CustomerListDTO>, Error>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CustomerFinder implements ICustomerFinder {
|
||||||
|
constructor(private readonly repository: ICustomerRepository) {}
|
||||||
|
|
||||||
|
async findCustomerById(
|
||||||
|
companyId: UniqueID,
|
||||||
|
customerId: UniqueID,
|
||||||
|
transaction?: Transaction
|
||||||
|
): Promise<Result<Customer, Error>> {
|
||||||
|
return this.repository.getByIdInCompany(companyId, customerId, transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
async customerExists(
|
||||||
|
companyId: UniqueID,
|
||||||
|
customerId: UniqueID,
|
||||||
|
transaction?: Transaction
|
||||||
|
): Promise<Result<boolean, Error>> {
|
||||||
|
return this.repository.existsByIdInCompany(companyId, customerId, transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findCustomersByCriteria(
|
||||||
|
companyId: UniqueID,
|
||||||
|
criteria: Criteria,
|
||||||
|
transaction?: Transaction
|
||||||
|
): Promise<Result<Collection<CustomerListDTO>, Error>> {
|
||||||
|
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
modules/customers/src/api/application/services/index.ts
Normal file
1
modules/customers/src/api/application/services/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./customer-finder";
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
//export * from "./full";
|
||||||
|
export * from "./list";
|
||||||
|
//export * from "./report";
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import type { ISnapshotBuilder } from "@erp/core/api";
|
||||||
|
|
||||||
|
import type { CustomerListDTO } from "../../dtos";
|
||||||
|
|
||||||
|
import type { ICustomerListItemSnapshot } from "./customer-list-item-snapshot.interface";
|
||||||
|
|
||||||
|
export interface ICustomerListItemSnapshotBuilder
|
||||||
|
extends ISnapshotBuilder<CustomerListDTO, ICustomerListItemSnapshot> {}
|
||||||
|
|
||||||
|
export class CustomerListItemSnapshotBuilder implements ICustomerListItemSnapshotBuilder {
|
||||||
|
toOutput(customer: CustomerListDTO): ICustomerListItemSnapshot {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export type ICustomerListItemSnapshot = {};
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./customer-list-item-snapshot.interface";
|
||||||
|
export * from "./customer-list-item-snapshot-builder";
|
||||||
@ -24,7 +24,7 @@ import {
|
|||||||
import { Collection, Result, isNullishOrEmpty } from "@repo/rdx-utils";
|
import { Collection, Result, isNullishOrEmpty } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { CreateCustomerRequestDTO } from "../../../../common";
|
import type { CreateCustomerRequestDTO } from "../../../../common";
|
||||||
import { type CustomerProps, CustomerStatus } from "../../../domain";
|
import { type ICustomerProps, CustomerStatus } from "../../../domain";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convierte el DTO a las props validadas (CustomerProps).
|
* Convierte el DTO a las props validadas (CustomerProps).
|
||||||
@ -197,7 +197,7 @@ export function mapDTOToCreateCustomerProps(dto: CreateCustomerRequestDTO) {
|
|||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const customerProps: Omit<CustomerProps, "companyId"> = {
|
const customerProps: Omit<ICustomerProps, "companyId"> = {
|
||||||
status: status!,
|
status: status!,
|
||||||
reference: reference!,
|
reference: reference!,
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
|
import type { ITransactionManager } from "@erp/core/api";
|
||||||
import { Criteria } from "@repo/rdx-criteria/server";
|
import type { Criteria } from "@repo/rdx-criteria/server";
|
||||||
import { UniqueID } from "@repo/rdx-ddd";
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import { Transaction } from "sequelize";
|
import type { Transaction } from "sequelize";
|
||||||
import { ListCustomersResponseDTO } from "../../../common/dto";
|
|
||||||
import { CustomerApplicationService } from "../../application";
|
import type { ListCustomersResponseDTO } from "../../../common/dto";
|
||||||
import { ListCustomersPresenter } from "../presenters";
|
import type { ICustomerFinder } from "../services";
|
||||||
|
import type { ICustomerListItemSnapshotBuilder } from "../snapshot-builders/list";
|
||||||
|
|
||||||
type ListCustomersUseCaseInput = {
|
type ListCustomersUseCaseInput = {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
@ -14,39 +15,42 @@ type ListCustomersUseCaseInput = {
|
|||||||
|
|
||||||
export class ListCustomersUseCase {
|
export class ListCustomersUseCase {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly service: CustomerApplicationService,
|
private readonly finder: ICustomerFinder,
|
||||||
private readonly transactionManager: ITransactionManager,
|
private readonly listItemSnapshotBuilder: ICustomerListItemSnapshotBuilder,
|
||||||
private readonly presenterRegistry: IPresenterRegistry
|
private readonly transactionManager: ITransactionManager
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public execute(
|
public execute(
|
||||||
params: ListCustomersUseCaseInput
|
params: ListCustomersUseCaseInput
|
||||||
): Promise<Result<ListCustomersResponseDTO, Error>> {
|
): Promise<Result<ListCustomersResponseDTO, Error>> {
|
||||||
const { criteria, companyId } = params;
|
const { criteria, companyId } = params;
|
||||||
const presenter = this.presenterRegistry.getPresenter({
|
|
||||||
resource: "customer",
|
|
||||||
projection: "LIST",
|
|
||||||
}) as ListCustomersPresenter;
|
|
||||||
|
|
||||||
return this.transactionManager.complete(async (transaction: Transaction) => {
|
return this.transactionManager.complete(async (transaction: Transaction) => {
|
||||||
try {
|
try {
|
||||||
const result = await this.service.findCustomerByCriteriaInCompany(
|
const result = await this.finder.findCustomersByCriteria(companyId, criteria, transaction);
|
||||||
companyId,
|
|
||||||
criteria,
|
|
||||||
transaction
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.isFailure) {
|
if (result.isFailure) {
|
||||||
return Result.fail(result.error);
|
return Result.fail(result.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const customers = result.data;
|
const customers = result.data;
|
||||||
const dto = presenter.toOutput({
|
const totalCustomers = customers.total();
|
||||||
customers,
|
|
||||||
criteria,
|
|
||||||
});
|
|
||||||
|
|
||||||
return Result.ok(dto);
|
const items = customers.map((item) => this.listItemSnapshotBuilder.toOutput(item));
|
||||||
|
|
||||||
|
const snapshot = {
|
||||||
|
page: criteria.pageNumber,
|
||||||
|
per_page: criteria.pageSize,
|
||||||
|
total_pages: Math.ceil(totalCustomers / criteria.pageSize),
|
||||||
|
total_items: totalCustomers,
|
||||||
|
items: items,
|
||||||
|
metadata: {
|
||||||
|
entity: "customers",
|
||||||
|
criteria: criteria.toJSON(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return Result.ok(snapshot);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
return Result.fail(error as Error);
|
return Result.fail(error as Error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import { type Collection, type Maybe, Result } from "@repo/rdx-utils";
|
|||||||
|
|
||||||
import type { CustomerStatus } from "../value-objects";
|
import type { CustomerStatus } from "../value-objects";
|
||||||
|
|
||||||
export interface CustomerProps {
|
export interface ICustomerProps {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
status: CustomerStatus;
|
status: CustomerStatus;
|
||||||
reference: Maybe<Name>;
|
reference: Maybe<Name>;
|
||||||
@ -49,7 +49,7 @@ export interface CustomerProps {
|
|||||||
currencyCode: CurrencyCode;
|
currencyCode: CurrencyCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CustomerPatchProps = Partial<Omit<CustomerProps, "companyId" | "address">> & {
|
export type CustomerPatchProps = Partial<Omit<ICustomerProps, "companyId" | "address">> & {
|
||||||
address?: PostalAddressPatchProps;
|
address?: PostalAddressPatchProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -90,8 +90,12 @@ export interface ICustomer {
|
|||||||
readonly currencyCode: CurrencyCode;
|
readonly currencyCode: CurrencyCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Customer extends AggregateRoot<CustomerProps> implements ICustomer {
|
type CreateCustomerProps = ICustomerProps;
|
||||||
static create(props: CustomerProps, id?: UniqueID): Result<Customer, Error> {
|
type InternalCustomerProps = ICustomerProps;
|
||||||
|
|
||||||
|
export class Customer extends AggregateRoot<InternalCustomerProps> implements ICustomer {
|
||||||
|
|
||||||
|
static create(props: CreateCustomerProps, id?: UniqueID): Result<Customer, Error> {
|
||||||
const contact = new Customer(props, id);
|
const contact = new Customer(props, id);
|
||||||
|
|
||||||
// Reglas de negocio / validaciones
|
// Reglas de negocio / validaciones
|
||||||
@ -105,12 +109,18 @@ export class Customer extends AggregateRoot<CustomerProps> implements ICustomer
|
|||||||
return Result.ok(contact);
|
return Result.ok(contact);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rehidratación desde persistencia
|
||||||
|
static rehydrate(props: InternalCustomerProps, id: UniqueID): Customer {
|
||||||
|
return new Customer(props, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public update(partialCustomer: CustomerPatchProps): Result<Customer, Error> {
|
public update(partialCustomer: CustomerPatchProps): Result<Customer, Error> {
|
||||||
const { address: partialAddress, ...rest } = partialCustomer;
|
const { address: partialAddress, ...rest } = partialCustomer;
|
||||||
const updatedProps = {
|
const updatedProps = {
|
||||||
...this.props,
|
...this.props,
|
||||||
...rest,
|
...rest,
|
||||||
} as CustomerProps;
|
} as ICustomerProps;
|
||||||
|
|
||||||
if (partialAddress) {
|
if (partialAddress) {
|
||||||
const updatedAddressOrError = this.address.update(partialAddress);
|
const updatedAddressOrError = this.address.update(partialAddress);
|
||||||
@ -124,6 +134,8 @@ export class Customer extends AggregateRoot<CustomerProps> implements ICustomer
|
|||||||
return Customer.create(updatedProps, this.id);
|
return Customer.create(updatedProps, this.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
|
||||||
public get isIndividual(): boolean {
|
public get isIndividual(): boolean {
|
||||||
return !this.props.isCompany;
|
return !this.props.isCompany;
|
||||||
}
|
}
|
||||||
@ -1 +1 @@
|
|||||||
export * from "./customer";
|
export * from "./customer.aggregate";
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
export * from "./aggregates";
|
export * from "./aggregates";
|
||||||
export * from "./errors";
|
export * from "./errors";
|
||||||
export * from "./repositories";
|
|
||||||
export * from "./value-objects";
|
export * from "./value-objects";
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export * from "./customer-address-type";
|
export * from "./customer-address-type.vo";
|
||||||
export * from "./customer-number";
|
export * from "./customer-number.vo";
|
||||||
export * from "./customer-serie";
|
export * from "./customer-serie.vo";
|
||||||
export * from "./customer-status";
|
export * from "./customer-status.vo";
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { IModuleServer } from "@erp/core/api";
|
import type { IModuleServer } from "@erp/core/api";
|
||||||
|
|
||||||
import { models } from "./infrastructure";
|
import { customersRouter, models } from "./infrastructure";
|
||||||
|
|
||||||
export * from "./infrastructure/sequelize";
|
export * from "./infrastructure/sequelize";
|
||||||
|
|
||||||
@ -19,25 +19,29 @@ export const customersAPIModule: IModuleServer = {
|
|||||||
async setup(params) {
|
async setup(params) {
|
||||||
const { env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger } = params;
|
const { env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger } = params;
|
||||||
|
|
||||||
|
|
||||||
|
// 1) Dominio interno
|
||||||
|
//const customerInternalDeps = buildCustomerDependencies(params);
|
||||||
|
|
||||||
|
// 2) Servicios públicos (Application Services)
|
||||||
|
//const customerServices = buildCustomerServices(customerInternalDeps);
|
||||||
|
|
||||||
logger.info("🚀 Customers module dependencies registered", {
|
logger.info("🚀 Customers module dependencies registered", {
|
||||||
label: this.name,
|
label: this.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Modelos Sequelize del módulo
|
// Modelos Sequelize del módulo
|
||||||
models,
|
models,
|
||||||
|
|
||||||
// Servicios expuestos a otros módulos
|
// Servicios expuestos a otros módulos
|
||||||
services: {
|
services: {
|
||||||
customers: {
|
//customers: customerServices,
|
||||||
/*...*/
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Implementación privada del módulo
|
// Implementación privada del módulo
|
||||||
internal: {
|
internal: {
|
||||||
customers: {
|
//customers: customerInternalDeps,
|
||||||
/*...*/
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -56,7 +60,7 @@ export const customersAPIModule: IModuleServer = {
|
|||||||
const customersInternalDeps = getInternal("customers", "customers");
|
const customersInternalDeps = getInternal("customers", "customers");
|
||||||
|
|
||||||
// Registro de rutas HTTP
|
// Registro de rutas HTTP
|
||||||
//customersRouter(params, customersInternalDeps);
|
customersRouter(params, customersInternalDeps);
|
||||||
|
|
||||||
logger.info("🚀 Customers module started", {
|
logger.info("🚀 Customers module started", {
|
||||||
label: this.name,
|
label: this.name,
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
import { CustomerApplicationService } from "../application/customer-application.service";
|
import { CustomerApplicationService } from "../application/customer-application.service";
|
||||||
import { CustomerFullPresenter, ListCustomersPresenter } from "../application/presenters";
|
import { CustomerFullPresenter, ListCustomersPresenter } from "../application/presenters";
|
||||||
|
|
||||||
import { CustomerDomainMapper, CustomerListMapper } from "./mappers";
|
import { CustomerDomainMapper, CustomerSummaryMapper } from "./mappers";
|
||||||
import { CustomerRepository } from "./sequelize";
|
import { CustomerRepository } from "./sequelize";
|
||||||
|
|
||||||
export type CustomerDeps = {
|
export type CustomerDeps = {
|
||||||
@ -40,7 +40,7 @@ export function buildCustomerDependencies(params: ModuleParams): CustomerDeps {
|
|||||||
const mapperRegistry = new InMemoryMapperRegistry();
|
const mapperRegistry = new InMemoryMapperRegistry();
|
||||||
mapperRegistry
|
mapperRegistry
|
||||||
.registerDomainMapper({ resource: "customer" }, new CustomerDomainMapper())
|
.registerDomainMapper({ resource: "customer" }, new CustomerDomainMapper())
|
||||||
.registerQueryMapper({ resource: "customer", query: "LIST" }, new CustomerListMapper());
|
.registerQueryMapper({ resource: "customer", query: "LIST" }, new CustomerSummaryMapper());
|
||||||
|
|
||||||
// Repository & Services
|
// Repository & Services
|
||||||
const repo = new CustomerRepository({ mapperRegistry, database });
|
const repo = new CustomerRepository({ mapperRegistry, database });
|
||||||
|
|||||||
@ -0,0 +1,36 @@
|
|||||||
|
import {
|
||||||
|
CreateCustomerInputMapper,
|
||||||
|
type ICatalogs,
|
||||||
|
type ICustomerDomainMapper,
|
||||||
|
type ICustomerListMapper,
|
||||||
|
} from "../../../application";
|
||||||
|
import { SequelizeCustomerDomainMapper, SequelizeCustomerListMapper } from "../persistence";
|
||||||
|
|
||||||
|
export interface ICustomerPersistenceMappers {
|
||||||
|
domainMapper: ICustomerDomainMapper;
|
||||||
|
listMapper: ICustomerListMapper;
|
||||||
|
|
||||||
|
createMapper: CreateCustomerInputMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buildCustomerPersistenceMappers = (
|
||||||
|
catalogs: ICatalogs
|
||||||
|
): ICustomerPersistenceMappers => {
|
||||||
|
const { taxCatalog } = catalogs;
|
||||||
|
|
||||||
|
// Mappers para el repositorio
|
||||||
|
const domainMapper = new SequelizeCustomerDomainMapper({
|
||||||
|
taxCatalog,
|
||||||
|
});
|
||||||
|
const listMapper = new SequelizeCustomerListMapper();
|
||||||
|
|
||||||
|
// Mappers el DTO a las props validadas (CustomerProps) y luego construir agregado
|
||||||
|
const createMapper = new CreateCustomerInputMapper({ taxCatalog });
|
||||||
|
|
||||||
|
return {
|
||||||
|
domainMapper,
|
||||||
|
listMapper,
|
||||||
|
|
||||||
|
createMapper,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import type { Sequelize } from "sequelize";
|
||||||
|
|
||||||
|
import { CustomerRepository } from "../persistence";
|
||||||
|
|
||||||
|
import type { ICustomerPersistenceMappers } from "./customer-persistence-mappers.di";
|
||||||
|
|
||||||
|
export const buildCustomerRepository = (params: {
|
||||||
|
database: Sequelize;
|
||||||
|
mappers: ICustomerPersistenceMappers;
|
||||||
|
}) => {
|
||||||
|
const { database, mappers } = params;
|
||||||
|
|
||||||
|
return new CustomerRepository(mappers.domainMapper, mappers.listMapper, database);
|
||||||
|
};
|
||||||
73
modules/customers/src/api/infrastructure/di/customers.di.ts
Normal file
73
modules/customers/src/api/infrastructure/di/customers.di.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { type ModuleParams, buildCatalogs, buildTransactionManager } from "@erp/core/api";
|
||||||
|
|
||||||
|
export type CustomersInternalDeps = {
|
||||||
|
useCases: {
|
||||||
|
listCustomers: () => ListCustomersUseCase;
|
||||||
|
getCustomerById: () => GetCustomerByIdUseCase;
|
||||||
|
reportCustomer: () => ReportCustomerUseCase;
|
||||||
|
createCustomer: () => CreateCustomerUseCase;
|
||||||
|
|
||||||
|
/*
|
||||||
|
updateCustomer: () => UpdateCustomerUseCase;
|
||||||
|
deleteCustomer: () => DeleteCustomerUseCase;
|
||||||
|
issueCustomer: () => IssueCustomerUseCase;
|
||||||
|
changeStatusCustomer: () => ChangeStatusCustomerUseCase;*/
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export function buildCustomersDependencies(params: ModuleParams): CustomersInternalDeps {
|
||||||
|
const { database } = params;
|
||||||
|
|
||||||
|
// Infrastructure
|
||||||
|
const transactionManager = buildTransactionManager(database);
|
||||||
|
const catalogs = buildCatalogs();
|
||||||
|
const persistenceMappers = buildCustomerPersistenceMappers(catalogs);
|
||||||
|
|
||||||
|
const repository = buildCustomerRepository({ database, mappers: persistenceMappers });
|
||||||
|
const numberService = buildCustomerNumberGenerator();
|
||||||
|
|
||||||
|
// Application helpers
|
||||||
|
const inputMappers = buildCustomerInputMappers(catalogs);
|
||||||
|
const finder = buildCustomerFinder(repository);
|
||||||
|
const creator = buildCustomerCreator({ numberService, repository });
|
||||||
|
|
||||||
|
const snapshotBuilders = buildCustomersnapshotBuilders();
|
||||||
|
const documentGeneratorPipeline = buildCustomerDocumentService(params);
|
||||||
|
|
||||||
|
// Internal use cases (factories)
|
||||||
|
return {
|
||||||
|
useCases: {
|
||||||
|
listCustomers: () =>
|
||||||
|
buildListCustomersUseCase({
|
||||||
|
finder,
|
||||||
|
itemSnapshotBuilder: snapshotBuilders.list,
|
||||||
|
transactionManager,
|
||||||
|
}),
|
||||||
|
|
||||||
|
getCustomerById: () =>
|
||||||
|
buildGetCustomerByIdUseCase({
|
||||||
|
finder,
|
||||||
|
fullSnapshotBuilder: snapshotBuilders.full,
|
||||||
|
transactionManager,
|
||||||
|
}),
|
||||||
|
|
||||||
|
reportCustomer: () =>
|
||||||
|
buildReportCustomerUseCase({
|
||||||
|
finder,
|
||||||
|
fullSnapshotBuilder: snapshotBuilders.full,
|
||||||
|
reportSnapshotBuilder: snapshotBuilders.report,
|
||||||
|
documentService: documentGeneratorPipeline,
|
||||||
|
transactionManager,
|
||||||
|
}),
|
||||||
|
|
||||||
|
createCustomer: () =>
|
||||||
|
buildCreateCustomerUseCase({
|
||||||
|
creator,
|
||||||
|
dtoMapper: inputMappers.createInputMapper,
|
||||||
|
fullSnapshotBuilder: snapshotBuilders.full,
|
||||||
|
transactionManager,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
1
modules/customers/src/api/infrastructure/di/index.ts
Normal file
1
modules/customers/src/api/infrastructure/di/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./customers.di";
|
||||||
@ -25,7 +25,7 @@ import {
|
|||||||
UpdateCustomerController,
|
UpdateCustomerController,
|
||||||
} from "./controllers";
|
} from "./controllers";
|
||||||
|
|
||||||
export const customersRouter = (params: ModuleParams) => {
|
export const customersRouter = (params: ModuleParams, deps: CustomerInternalDeps) => {
|
||||||
const { app, baseRoutePath, logger } = params as {
|
const { app, baseRoutePath, logger } = params as {
|
||||||
app: Application;
|
app: Application;
|
||||||
database: Sequelize;
|
database: Sequelize;
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
import {
|
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
|
||||||
type ISequelizeDomainMapper,
|
import type { ICustomerDomainMapper } from "@erp/customers/api/application";
|
||||||
type MapperParamsType,
|
|
||||||
SequelizeDomainMapper,
|
|
||||||
} from "@erp/core/api";
|
|
||||||
import {
|
import {
|
||||||
City,
|
City,
|
||||||
Country,
|
Country,
|
||||||
@ -16,7 +13,7 @@ import {
|
|||||||
Province,
|
Province,
|
||||||
Street,
|
Street,
|
||||||
TINNumber,
|
TINNumber,
|
||||||
TaxCode,
|
type TaxCode,
|
||||||
TextValue,
|
TextValue,
|
||||||
URLAddress,
|
URLAddress,
|
||||||
UniqueID,
|
UniqueID,
|
||||||
@ -26,14 +23,11 @@ import {
|
|||||||
maybeFromNullableResult,
|
maybeFromNullableResult,
|
||||||
maybeToNullable,
|
maybeToNullable,
|
||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
import { Collection, Result, isNullishOrEmpty } from "@repo/rdx-utils";
|
import { Collection, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import { Customer, type CustomerProps, CustomerStatus } from "../../../domain";
|
import { Customer, CustomerStatus, type ICustomerProps } from "../../../domain";
|
||||||
import type { CustomerCreationAttributes, CustomerModel } from "../../sequelize";
|
import type { CustomerCreationAttributes, CustomerModel } from "../../sequelize";
|
||||||
|
|
||||||
export interface ICustomerDomainMapper
|
|
||||||
extends ISequelizeDomainMapper<CustomerModel, CustomerCreationAttributes, Customer> {}
|
|
||||||
|
|
||||||
export class CustomerDomainMapper
|
export class CustomerDomainMapper
|
||||||
extends SequelizeDomainMapper<CustomerModel, CustomerCreationAttributes, Customer>
|
extends SequelizeDomainMapper<CustomerModel, CustomerCreationAttributes, Customer>
|
||||||
implements ICustomerDomainMapper
|
implements ICustomerDomainMapper
|
||||||
@ -175,14 +169,14 @@ export class CustomerDomainMapper
|
|||||||
|
|
||||||
// source.default_taxes is stored as a comma-separated string
|
// source.default_taxes is stored as a comma-separated string
|
||||||
const defaultTaxes = new Collection<TaxCode>();
|
const defaultTaxes = new Collection<TaxCode>();
|
||||||
if (!isNullishOrEmpty(source.default_taxes)) {
|
/*if (!isNullishOrEmpty(source.default_taxes)) {
|
||||||
source.default_taxes!.split(",").map((taxCode, index) => {
|
source.default_taxes!.split(",").map((taxCode, index) => {
|
||||||
const tax = extractOrPushError(TaxCode.create(taxCode), `default_taxes.${index}`, errors);
|
const tax = extractOrPushError(TaxCode.create(taxCode), `default_taxes.${index}`, errors);
|
||||||
if (tax) {
|
if (tax) {
|
||||||
defaultTaxes.add(tax!);
|
defaultTaxes.add(tax!);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}*/
|
||||||
|
|
||||||
// Now, create the PostalAddress VO
|
// Now, create the PostalAddress VO
|
||||||
const postalAddressProps = {
|
const postalAddressProps = {
|
||||||
@ -205,7 +199,7 @@ export class CustomerDomainMapper
|
|||||||
return Result.fail(new ValidationErrorCollection("Customer props mapping failed", errors));
|
return Result.fail(new ValidationErrorCollection("Customer props mapping failed", errors));
|
||||||
}
|
}
|
||||||
|
|
||||||
const customerProps: CustomerProps = {
|
const customerProps: ICustomerProps = {
|
||||||
companyId: companyId!,
|
companyId: companyId!,
|
||||||
status: status!,
|
status: status!,
|
||||||
reference: reference!,
|
reference: reference!,
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
import {
|
import { type MapperParamsType, SequelizeQueryMapper } from "@erp/core/api";
|
||||||
type ISequelizeQueryMapper,
|
|
||||||
type MapperParamsType,
|
|
||||||
SequelizeQueryMapper,
|
|
||||||
} from "@erp/core/api";
|
|
||||||
import {
|
import {
|
||||||
City,
|
City,
|
||||||
Country,
|
Country,
|
||||||
@ -24,46 +20,20 @@ import {
|
|||||||
extractOrPushError,
|
extractOrPushError,
|
||||||
maybeFromNullableResult,
|
maybeFromNullableResult,
|
||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
import { type Maybe, Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import type { CustomerSummary, ICustomerSummaryMapper } from "../../../application";
|
||||||
import { CustomerStatus } from "../../../domain";
|
import { CustomerStatus } from "../../../domain";
|
||||||
import type { CustomerModel } from "../../sequelize";
|
import type { CustomerModel } from "../../sequelize";
|
||||||
|
|
||||||
export type CustomerListDTO = {
|
export class CustomerSummaryMapper
|
||||||
id: UniqueID;
|
extends SequelizeQueryMapper<CustomerModel, CustomerSummary>
|
||||||
companyId: UniqueID;
|
implements ICustomerSummaryMapper
|
||||||
status: CustomerStatus;
|
|
||||||
reference: Maybe<Name>;
|
|
||||||
|
|
||||||
isCompany: boolean;
|
|
||||||
name: Name;
|
|
||||||
tradeName: Maybe<Name>;
|
|
||||||
tin: Maybe<TINNumber>;
|
|
||||||
|
|
||||||
address: PostalAddress;
|
|
||||||
|
|
||||||
emailPrimary: Maybe<EmailAddress>;
|
|
||||||
emailSecondary: Maybe<EmailAddress>;
|
|
||||||
phonePrimary: Maybe<PhoneNumber>;
|
|
||||||
phoneSecondary: Maybe<PhoneNumber>;
|
|
||||||
mobilePrimary: Maybe<PhoneNumber>;
|
|
||||||
mobileSecondary: Maybe<PhoneNumber>;
|
|
||||||
|
|
||||||
fax: Maybe<PhoneNumber>;
|
|
||||||
website: Maybe<URLAddress>;
|
|
||||||
|
|
||||||
languageCode: LanguageCode;
|
|
||||||
currencyCode: CurrencyCode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ICustomerListMapper
|
|
||||||
extends ISequelizeQueryMapper<CustomerModel, CustomerListDTO> {}
|
|
||||||
|
|
||||||
export class CustomerListMapper
|
|
||||||
extends SequelizeQueryMapper<CustomerModel, CustomerListDTO>
|
|
||||||
implements ICustomerListMapper
|
|
||||||
{
|
{
|
||||||
public mapToDTO(raw: CustomerModel, params?: MapperParamsType): Result<CustomerListDTO, Error> {
|
public mapToReadModel(
|
||||||
|
raw: CustomerModel,
|
||||||
|
params?: MapperParamsType
|
||||||
|
): Result<CustomerSummary, Error> {
|
||||||
const errors: ValidationErrorDetail[] = [];
|
const errors: ValidationErrorDetail[] = [];
|
||||||
|
|
||||||
// 1) Valores escalares (atributos generales)
|
// 1) Valores escalares (atributos generales)
|
||||||
@ -217,7 +187,7 @@ export class CustomerListMapper
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.ok<CustomerListDTO>({
|
return Result.ok<CustomerSummary>({
|
||||||
id: customerId!,
|
id: customerId!,
|
||||||
companyId: companyId!,
|
companyId: companyId!,
|
||||||
status: status!,
|
status: status!,
|
||||||
@ -1 +1 @@
|
|||||||
export * from "./customer.list.mapper";
|
export * from "./customer-summary.mapper";
|
||||||
|
|||||||
@ -7,16 +7,25 @@ import {
|
|||||||
import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
|
import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
|
||||||
import type { UniqueID } from "@repo/rdx-ddd";
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { type Collection, Result } from "@repo/rdx-utils";
|
import { type Collection, Result } from "@repo/rdx-utils";
|
||||||
import type { Transaction } from "sequelize";
|
import type { Sequelize, Transaction } from "sequelize";
|
||||||
|
|
||||||
import type { Customer, ICustomerRepository } from "../../../domain";
|
import type { CustomerSummary, ICustomerRepository } from "../../../application";
|
||||||
import type { CustomerListDTO, ICustomerDomainMapper, ICustomerListMapper } from "../../mappers";
|
import type { Customer } from "../../../domain";
|
||||||
|
import type { ICustomerDomainMapper, ICustomerListMapper } from "../../mappers";
|
||||||
import { CustomerModel } from "../models/customer.model";
|
import { CustomerModel } from "../models/customer.model";
|
||||||
|
|
||||||
export class CustomerRepository
|
export class CustomerRepository
|
||||||
extends SequelizeRepository<Customer>
|
extends SequelizeRepository<Customer>
|
||||||
implements ICustomerRepository
|
implements ICustomerRepository
|
||||||
{
|
{
|
||||||
|
constructor(
|
||||||
|
private readonly domainMapper: ICustomerDomainMapper,
|
||||||
|
private readonly listMapper: ICustomerListMapper,
|
||||||
|
database: Sequelize
|
||||||
|
) {
|
||||||
|
super({ database });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Crea un nuevo cliente
|
* Crea un nuevo cliente
|
||||||
@ -27,18 +36,15 @@ export class CustomerRepository
|
|||||||
*/
|
*/
|
||||||
async create(customer: Customer, transaction?: Transaction): Promise<Result<void, Error>> {
|
async create(customer: Customer, transaction?: Transaction): Promise<Result<void, Error>> {
|
||||||
try {
|
try {
|
||||||
const mapper: ICustomerDomainMapper = this._registry.getDomainMapper({
|
const dtoResult = this.domainMapper.mapToPersistence(customer);
|
||||||
resource: "customer",
|
|
||||||
});
|
|
||||||
const dto = mapper.mapToPersistence(customer);
|
|
||||||
|
|
||||||
if (dto.isFailure) {
|
if (dtoResult.isFailure) {
|
||||||
return Result.fail(dto.error);
|
return Result.fail(dtoResult.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data } = dto;
|
const dto = dtoResult.data;
|
||||||
|
|
||||||
await CustomerModel.create(data, {
|
await CustomerModel.create(dto, {
|
||||||
include: [{ all: true }],
|
include: [{ all: true }],
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
@ -58,12 +64,9 @@ export class CustomerRepository
|
|||||||
*/
|
*/
|
||||||
async update(customer: Customer, transaction?: Transaction): Promise<Result<void, Error>> {
|
async update(customer: Customer, transaction?: Transaction): Promise<Result<void, Error>> {
|
||||||
try {
|
try {
|
||||||
const mapper: ICustomerDomainMapper = this._registry.getDomainMapper({
|
const dtoResult = this.domainMapper.mapToPersistence(customer);
|
||||||
resource: "customer",
|
|
||||||
});
|
|
||||||
const dto = mapper.mapToPersistence(customer);
|
|
||||||
|
|
||||||
const { id, ...updatePayload } = dto.data;
|
const { id, ...updatePayload } = dtoResult.data;
|
||||||
const [affected] = await CustomerModel.update(updatePayload, {
|
const [affected] = await CustomerModel.update(updatePayload, {
|
||||||
where: { id /*, version */ },
|
where: { id /*, version */ },
|
||||||
//fields: Object.keys(updatePayload),
|
//fields: Object.keys(updatePayload),
|
||||||
@ -146,7 +149,7 @@ export class CustomerRepository
|
|||||||
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
|
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
|
||||||
* @param criteria - Criterios de búsqueda.
|
* @param criteria - Criterios de búsqueda.
|
||||||
* @param transaction - Transacción activa para la operación.
|
* @param transaction - Transacción activa para la operación.
|
||||||
* @returns Result<Collection<Customer>, Error>
|
* @returns Result<Collection<CustomerListDTO>, Error>
|
||||||
*
|
*
|
||||||
* @see Criteria
|
* @see Criteria
|
||||||
*/
|
*/
|
||||||
@ -154,15 +157,10 @@ export class CustomerRepository
|
|||||||
companyId: UniqueID,
|
companyId: UniqueID,
|
||||||
criteria: Criteria,
|
criteria: Criteria,
|
||||||
transaction?: Transaction
|
transaction?: Transaction
|
||||||
): Promise<Result<Collection<CustomerListDTO>, Error>> {
|
): Promise<Result<Collection<CustomerSummary>, Error>> {
|
||||||
try {
|
try {
|
||||||
const mapper: ICustomerListMapper = this._registry.getQueryMapper({
|
const criteriaConverter = new CriteriaToSequelizeConverter();
|
||||||
resource: "customer",
|
const query = criteriaConverter.convert(criteria, {
|
||||||
query: "LIST",
|
|
||||||
});
|
|
||||||
|
|
||||||
const converter = new CriteriaToSequelizeConverter();
|
|
||||||
const query = converter.convert(criteria, {
|
|
||||||
searchableFields: [
|
searchableFields: [
|
||||||
"name",
|
"name",
|
||||||
"trade_name",
|
"trade_name",
|
||||||
@ -195,19 +193,7 @@ export class CustomerRepository
|
|||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
/*const [rows, count] = await Promise.all([
|
return this.listMapper.mapToDTOCollection(rows, count);
|
||||||
CustomerModel.findAll({
|
|
||||||
...CustomerModel,
|
|
||||||
transaction,
|
|
||||||
}),
|
|
||||||
CustomerModel.count({
|
|
||||||
where: query.where,
|
|
||||||
distinct: true, // evita duplicados por LEFT JOIN
|
|
||||||
transaction,
|
|
||||||
}),
|
|
||||||
]);*/
|
|
||||||
|
|
||||||
return mapper.mapToDTOCollection(rows, count);
|
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
return Result.fail(translateSequelizeError(err));
|
return Result.fail(translateSequelizeError(err));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
import { LookupDialog } from "@repo/rdx-ui/components";
|
|
||||||
import DataTable, { TableColumn } from "react-data-table-component";
|
|
||||||
import { useDebounce } from "use-debounce";
|
|
||||||
|
|
||||||
import { buildTextFilters } from "@erp/core/client";
|
import { buildTextFilters } from "@erp/core/client";
|
||||||
|
import { LookupDialog } from "@repo/rdx-ui/components";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@ -18,7 +15,10 @@ import {
|
|||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { Building, Calendar, Mail, MapPin, Phone, Plus, User } from "lucide-react";
|
import { Building, Calendar, Mail, MapPin, Phone, Plus, User } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ListCustomersResponseDTO } from "../../common";
|
import DataTable, { type TableColumn } from "react-data-table-component";
|
||||||
|
import { useDebounce } from "use-debounce";
|
||||||
|
|
||||||
|
import type { ListCustomersResponseDTO } from "../../common";
|
||||||
import { useCustomerListQuery } from "../hooks";
|
import { useCustomerListQuery } from "../hooks";
|
||||||
|
|
||||||
type Customer = ListCustomersResponseDTO["items"][number];
|
type Customer = ListCustomersResponseDTO["items"][number];
|
||||||
@ -143,96 +143,96 @@ export const ClientSelectorModal = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='space-y-4 max-w-2xl'>
|
<div className="space-y-4 max-w-2xl">
|
||||||
<div className='space-y-1'>
|
<div className="space-y-1">
|
||||||
<Label>Cliente</Label>
|
<Label>Cliente</Label>
|
||||||
<Button variant='outline' className='w-full justify-start' onClick={handleSelectClient}>
|
<Button className="w-full justify-start" onClick={handleSelectClient} variant="outline">
|
||||||
<User className='h-4 w-4 mr-2' />
|
<User className="h-4 w-4 mr-2" />
|
||||||
{selectedCustomer ? selectedCustomer.name : "Seleccionar cliente"}
|
{selectedCustomer ? selectedCustomer.name : "Seleccionar cliente"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedCustomer && (
|
{selectedCustomer && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className='p-4 space-y-2'>
|
<CardContent className="p-4 space-y-2">
|
||||||
<div className='flex items-center justify-between'>
|
<div className="flex items-center justify-between">
|
||||||
<div className='flex items-center gap-2'>
|
<div className="flex items-center gap-2">
|
||||||
<User className='h-6 w-6 text-primary' />
|
<User className="h-6 w-6 text-primary" />
|
||||||
<h3 className='font-semibold'>{selectedCustomer.name}</h3>
|
<h3 className="font-semibold">{selectedCustomer.name}</h3>
|
||||||
<Badge
|
<Badge
|
||||||
|
className="text-xs"
|
||||||
variant={selectedCustomer.status === "Activo" ? "default" : "secondary"}
|
variant={selectedCustomer.status === "Activo" ? "default" : "secondary"}
|
||||||
className='text-xs'
|
|
||||||
>
|
>
|
||||||
{selectedCustomer.status}
|
{selectedCustomer.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<span className='text-sm text-muted-foreground'>{selectedCustomer.company}</span>
|
<span className="text-sm text-muted-foreground">{selectedCustomer.company}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className='text-sm text-muted-foreground'>{selectedCustomer.email}</p>
|
<p className="text-sm text-muted-foreground">{selectedCustomer.email}</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<LookupDialog
|
<LookupDialog
|
||||||
open={open}
|
description="Busca un cliente por nombre, email o empresa"
|
||||||
onOpenChange={setOpen}
|
|
||||||
items={data?.items ?? []}
|
|
||||||
totalItems={data?.total_items ?? 0}
|
|
||||||
search={search}
|
|
||||||
onSearchChange={setSearch}
|
|
||||||
isLoading={isLoading}
|
|
||||||
isError={isError}
|
isError={isError}
|
||||||
refetch={refetch}
|
isLoading={isLoading}
|
||||||
title='Seleccionar cliente'
|
items={data?.items ?? []}
|
||||||
description='Busca un cliente por nombre, email o empresa'
|
|
||||||
onSelect={(customer) => {
|
|
||||||
setSelectedCustomer(customer);
|
|
||||||
setOpen(false);
|
|
||||||
}}
|
|
||||||
onCreate={(e) => {
|
onCreate={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
console.log("Crear nuevo cliente");
|
console.log("Crear nuevo cliente");
|
||||||
}}
|
}}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
onPageChange={setPageNumber}
|
||||||
|
onSearchChange={setSearch}
|
||||||
|
onSelect={(customer) => {
|
||||||
|
setSelectedCustomer(customer);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
open={open}
|
||||||
page={pageNumber}
|
page={pageNumber}
|
||||||
perPage={pageSize}
|
perPage={pageSize}
|
||||||
onPageChange={setPageNumber}
|
refetch={refetch}
|
||||||
renderItem={() => null} // No se usa con DataTable
|
|
||||||
renderContainer={(items) => (
|
renderContainer={(items) => (
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={items}
|
data={items}
|
||||||
|
highlightOnHover
|
||||||
|
noDataComponent="No se encontraron resultados"
|
||||||
|
onChangePage={(p) => setPageNumber(p)}
|
||||||
onRowClicked={(item) => {
|
onRowClicked={(item) => {
|
||||||
setSelectedCustomer(item);
|
setSelectedCustomer(item);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
pagination
|
pagination
|
||||||
paginationServer
|
|
||||||
paginationPerPage={pageSize}
|
paginationPerPage={pageSize}
|
||||||
|
paginationServer
|
||||||
paginationTotalRows={data?.total_items ?? 0}
|
paginationTotalRows={data?.total_items ?? 0}
|
||||||
onChangePage={(p) => setPageNumber(p)}
|
|
||||||
highlightOnHover
|
|
||||||
pointerOnHover
|
pointerOnHover
|
||||||
noDataComponent='No se encontraron resultados'
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
renderItem={() => null}
|
||||||
|
search={search}
|
||||||
|
title="Seleccionar cliente" // No se usa con DataTable
|
||||||
|
totalItems={data?.total_items ?? 0}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
|
<Dialog onOpenChange={setIsCreateOpen} open={isCreateOpen}>
|
||||||
<DialogContent className='max-w-md'>
|
<DialogContent className="max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className='flex items-center gap-2'>
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<Plus className='size-5' />
|
<Plus className="size-5" />
|
||||||
Nuevo Cliente
|
Nuevo Cliente
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<p className='text-muted-foreground text-sm mb-4'>Formulario de creación pendiente…</p>
|
<p className="text-muted-foreground text-sm mb-4">Formulario de creación pendiente…</p>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant='outline' onClick={() => setIsCreateOpen(false)}>
|
<Button onClick={() => setIsCreateOpen(false)} variant="outline">
|
||||||
Cancelar
|
Cancelar
|
||||||
</Button>
|
</Button>
|
||||||
<Button>
|
<Button>
|
||||||
<Plus className='h-4 w-4 mr-2' />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
Crear
|
Crear
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@ -246,33 +246,33 @@ export const ClientSelectorModal = () => {
|
|||||||
|
|
||||||
const CustomerCard = ({ customer }: { customer: Customer }) => (
|
const CustomerCard = ({ customer }: { customer: Customer }) => (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className='p-4 space-y-2'>
|
<CardContent className="p-4 space-y-2">
|
||||||
<div className='flex items-center gap-2'>
|
<div className="flex items-center gap-2">
|
||||||
<User className='size-5' />
|
<User className="size-5" />
|
||||||
<span className='font-semibold'>{customer.name}</span>
|
<span className="font-semibold">{customer.name}</span>
|
||||||
<Badge variant={customer.status === "Activo" ? "default" : "secondary"}>
|
<Badge variant={customer.status === "Activo" ? "default" : "secondary"}>
|
||||||
{customer.status}
|
{customer.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className='text-sm text-muted-foreground flex flex-col gap-1'>
|
<div className="text-sm text-muted-foreground flex flex-col gap-1">
|
||||||
<div className='flex items-center gap-1'>
|
<div className="flex items-center gap-1">
|
||||||
<Mail className='h-4 w-4' />
|
<Mail className="h-4 w-4" />
|
||||||
{customer.email}
|
{customer.email}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-1'>
|
<div className="flex items-center gap-1">
|
||||||
<Building className='h-4 w-4' />
|
<Building className="h-4 w-4" />
|
||||||
{customer.company}
|
{customer.company}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-1'>
|
<div className="flex items-center gap-1">
|
||||||
<Phone className='h-4 w-4' />
|
<Phone className="h-4 w-4" />
|
||||||
{customer.phone}
|
{customer.phone}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-1'>
|
<div className="flex items-center gap-1">
|
||||||
<MapPin className='h-4 w-4' />
|
<MapPin className="h-4 w-4" />
|
||||||
{customer.address}
|
{customer.address}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-1'>
|
<div className="flex items-center gap-1">
|
||||||
<Calendar className='h-4 w-4' />
|
<Calendar className="h-4 w-4" />
|
||||||
{new Date(customer.createdAt).toLocaleDateString("es-ES")}
|
{new Date(customer.createdAt).toLocaleDateString("es-ES")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,19 +1,9 @@
|
|||||||
import {
|
import { Button, Item, ItemContent, ItemFooter, ItemTitle } from "@repo/shadcn-ui/components";
|
||||||
Button, Item,
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
ItemContent,
|
import { EyeIcon, MapPinIcon, RefreshCwIcon, UserPlusIcon } from "lucide-react";
|
||||||
ItemFooter,
|
import React, { useMemo } from "react";
|
||||||
ItemTitle
|
|
||||||
} from "@repo/shadcn-ui/components";
|
|
||||||
import { cn } from '@repo/shadcn-ui/lib/utils';
|
|
||||||
import {
|
|
||||||
EyeIcon,
|
|
||||||
MapPinIcon,
|
|
||||||
RefreshCwIcon,
|
|
||||||
UserPlusIcon
|
|
||||||
} from "lucide-react";
|
|
||||||
import React, { useMemo } from 'react';
|
|
||||||
import { CustomerSummary } from "../../schemas";
|
|
||||||
|
|
||||||
|
import type { CustomerSummary } from "../../schemas";
|
||||||
|
|
||||||
interface CustomerCardProps {
|
interface CustomerCardProps {
|
||||||
customer: CustomerSummary;
|
customer: CustomerSummary;
|
||||||
@ -57,42 +47,38 @@ export const CustomerCard = ({
|
|||||||
const address = useMemo(() => buildAddress(customer), [customer]);
|
const address = useMemo(() => buildAddress(customer), [customer]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Item variant="outline" className={className}>
|
<Item className={className} variant="outline">
|
||||||
<ItemContent>
|
<ItemContent>
|
||||||
<ItemTitle className="flex items-start gap-2 w-full justify-between">
|
<ItemTitle className="flex items-start gap-2 w-full justify-between">
|
||||||
<span className="grow text-balance">{customer.name}</span>
|
<span className="grow text-balance">{customer.name}</span>
|
||||||
{/* Eye solo si onViewCustomer existe */}
|
{/* Eye solo si onViewCustomer existe */}
|
||||||
{onViewCustomer && (
|
{onViewCustomer && (
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
aria-label="Ver ficha completa del cliente"
|
||||||
variant='ghost'
|
className="cursor-pointer"
|
||||||
size='sm'
|
|
||||||
className='cursor-pointer'
|
|
||||||
onClick={onViewCustomer}
|
onClick={onViewCustomer}
|
||||||
aria-label='Ver ficha completa del cliente'
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
>
|
>
|
||||||
<EyeIcon className='size-4 text-muted-foreground' />
|
<EyeIcon className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ItemTitle>
|
</ItemTitle>
|
||||||
<div
|
<div
|
||||||
data-slot="item-description"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance",
|
"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance",
|
||||||
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
||||||
"text-sm text-muted-foreground"
|
"text-sm text-muted-foreground"
|
||||||
)}>
|
|
||||||
{/* TIN en su propia línea si existe */}
|
|
||||||
{customer.tin && (
|
|
||||||
<div className="font-mono tabular-nums">{customer.tin}</div>
|
|
||||||
)}
|
)}
|
||||||
|
data-slot="item-description"
|
||||||
|
>
|
||||||
|
{/* TIN en su propia línea si existe */}
|
||||||
|
{customer.tin && <div className="font-mono tabular-nums">{customer.tin}</div>}
|
||||||
|
|
||||||
{/* Dirección */}
|
{/* Dirección */}
|
||||||
{address.has ? (
|
{address.has ? (
|
||||||
<address
|
<address aria-label={address.full} className="not-italic mt-1 text-pretty">
|
||||||
className="not-italic mt-1 text-pretty"
|
|
||||||
aria-label={address.full}
|
|
||||||
>
|
|
||||||
{/* Desktop/tablet: compacto en una línea (o dos por wrap natural) */}
|
{/* Desktop/tablet: compacto en una línea (o dos por wrap natural) */}
|
||||||
<div className="hidden sm:flex items-center gap-1 flex-wrap">
|
<div className="hidden sm:flex items-center gap-1 flex-wrap">
|
||||||
<MapPinIcon aria-hidden className="size-3.5 translate-y-[1px]" />
|
<MapPinIcon aria-hidden className="size-3.5 translate-y-[1px]" />
|
||||||
@ -101,7 +87,9 @@ export const CustomerCard = ({
|
|||||||
<React.Fragment key={i}>
|
<React.Fragment key={i}>
|
||||||
<span>{part}</span>
|
<span>{part}</span>
|
||||||
{i < address.stack.length - 1 && (
|
{i < address.stack.length - 1 && (
|
||||||
<span aria-hidden className="mx-1">·</span>
|
<span aria-hidden className="mx-1">
|
||||||
|
·
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
@ -129,26 +117,26 @@ export const CustomerCard = ({
|
|||||||
<ItemFooter className="flex-wrap gap-2">
|
<ItemFooter className="flex-wrap gap-2">
|
||||||
{onChangeCustomer && (
|
{onChangeCustomer && (
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
className="flex-1 min-w-36 gap-2 cursor-pointer"
|
||||||
variant='outline'
|
|
||||||
size='sm'
|
|
||||||
onClick={onChangeCustomer}
|
onClick={onChangeCustomer}
|
||||||
className='flex-1 min-w-36 gap-2 cursor-pointer'
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
>
|
>
|
||||||
<RefreshCwIcon className='size-4' />
|
<RefreshCwIcon className="size-4" />
|
||||||
<span className='text-sm text-muted-foreground'>Cambiar de cliente</span>
|
<span className="text-sm text-muted-foreground">Cambiar de cliente</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{onAddNewCustomer && (
|
{onAddNewCustomer && (
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
className="flex-1 min-w-36 gap-2 cursor-pointer"
|
||||||
variant='outline'
|
|
||||||
size='sm'
|
|
||||||
onClick={onAddNewCustomer}
|
onClick={onAddNewCustomer}
|
||||||
className='flex-1 min-w-36 gap-2 cursor-pointer'
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
>
|
>
|
||||||
<UserPlusIcon className='size-4' />
|
<UserPlusIcon className="size-4" />
|
||||||
<span className='text-sm text-muted-foreground'>Nuevo cliente</span>
|
<span className="text-sm text-muted-foreground">Nuevo cliente</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ItemFooter>
|
</ItemFooter>
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { UnsavedChangesProvider, useUnsavedChangesContext } from "@erp/core/hooks";
|
import { UnsavedChangesProvider, useUnsavedChangesContext } from "@erp/core/hooks";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@ -10,12 +9,12 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import { useCallback, useId } from 'react';
|
import { useCallback, useId } from "react";
|
||||||
import { useTranslation } from "../../i18n";
|
|
||||||
import { useCustomerCreateController } from '../../pages/create/use-customer-create-controller';
|
|
||||||
import { CustomerFormData } from "../../schemas";
|
|
||||||
import { CustomerEditForm } from '../editor';
|
|
||||||
|
|
||||||
|
import { useTranslation } from "../../i18n";
|
||||||
|
import { useCustomerCreateController } from "../../pages/create/use-customer-create-controller";
|
||||||
|
import type { CustomerFormData } from "../../schemas";
|
||||||
|
import { CustomerEditForm } from "../editor";
|
||||||
|
|
||||||
type CustomerCreateModalProps = {
|
type CustomerCreateModalProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -25,62 +24,61 @@ type CustomerCreateModalProps = {
|
|||||||
onSubmit: () => void; // ← mantenemos tu firma (no se usa directamente aquí)
|
onSubmit: () => void; // ← mantenemos tu firma (no se usa directamente aquí)
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CustomerCreateModal({
|
export function CustomerCreateModal({ open, onOpenChange }: CustomerCreateModalProps) {
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
}: CustomerCreateModalProps) {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const formId = useId();
|
const formId = useId();
|
||||||
|
|
||||||
const { requestConfirm } = useUnsavedChangesContext();
|
const { requestConfirm } = useUnsavedChangesContext();
|
||||||
|
|
||||||
const {
|
const { form, isCreating, isCreateError, createError, handleSubmit, handleError, FormProvider } =
|
||||||
form, isCreating, isCreateError, createError,
|
useCustomerCreateController();
|
||||||
handleSubmit, handleError, FormProvider
|
|
||||||
} = useCustomerCreateController();
|
|
||||||
|
|
||||||
const { isDirty } = form.formState;
|
const { isDirty } = form.formState;
|
||||||
|
|
||||||
const guardClose = useCallback(async (nextOpen: boolean) => {
|
const guardClose = useCallback(
|
||||||
if (nextOpen) return onOpenChange(true);
|
async (nextOpen: boolean) => {
|
||||||
|
if (nextOpen) return onOpenChange(true);
|
||||||
|
|
||||||
if (isCreating) return;
|
if (isCreating) return;
|
||||||
|
|
||||||
if (!isDirty) {
|
if (!isDirty) {
|
||||||
return onOpenChange(false);
|
return onOpenChange(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await requestConfirm()) {
|
if (await requestConfirm()) {
|
||||||
return onOpenChange(false);
|
return onOpenChange(false);
|
||||||
}
|
}
|
||||||
}, [requestConfirm, isCreating, onOpenChange, isDirty]);
|
},
|
||||||
|
[requestConfirm, isCreating, onOpenChange, isDirty]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFormSubmit = (data: CustomerFormData) =>
|
||||||
const handleFormSubmit = (data: CustomerFormData) => handleSubmit(data /*, () => onOpenChange(false)*/);
|
handleSubmit(data /*, () => onOpenChange(false)*/);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UnsavedChangesProvider isDirty={isDirty}>
|
<UnsavedChangesProvider isDirty={isDirty}>
|
||||||
|
<Dialog onOpenChange={guardClose} open={open}>
|
||||||
<Dialog open={open} onOpenChange={guardClose}>
|
|
||||||
<DialogContent className="bg-card border-border p-0 max-w-[calc(100vw-2rem)] sm:[calc(max-w-3xl-2rem)] h-[calc(100dvh-2rem)]">
|
<DialogContent className="bg-card border-border p-0 max-w-[calc(100vw-2rem)] sm:[calc(max-w-3xl-2rem)] h-[calc(100dvh-2rem)]">
|
||||||
<DialogHeader className="px-6 pt-6 pb-4 border-b">
|
<DialogHeader className="px-6 pt-6 pb-4 border-b">
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<Plus className="size-5" /> {t("pages.create.title")}
|
<Plus className="size-5" /> {t("pages.create.title")}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className='text-left'>{t("pages.create.description")}</DialogDescription>
|
<DialogDescription className="text-left">
|
||||||
|
{t("pages.create.description")}
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="overflow-y-auto h:[calc(100%-8rem)]">
|
<div className="overflow-y-auto h:[calc(100%-8rem)]">
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
<CustomerEditForm
|
<CustomerEditForm
|
||||||
formId={formId}
|
|
||||||
onSubmit={handleFormSubmit}
|
|
||||||
onError={handleError}
|
|
||||||
className="max-w-none"
|
className="max-w-none"
|
||||||
|
formId={formId}
|
||||||
|
onError={handleError}
|
||||||
|
onSubmit={handleFormSubmit}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isCreateError && (
|
{isCreateError && (
|
||||||
<p role="alert" className="mt-3 text-sm text-destructive">
|
<p className="mt-3 text-sm text-destructive" role="alert">
|
||||||
{(createError as Error)?.message}
|
{(createError as Error)?.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@ -88,16 +86,26 @@ export function CustomerCreateModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="px-6 py-4 border-t bg-card">
|
<DialogFooter className="px-6 py-4 border-t bg-card">
|
||||||
<Button type="button" form={formId} variant="outline" className='cursor-pointer' onClick={() => guardClose(false)} disabled={isCreating}>
|
<Button
|
||||||
{t('common.cancel', "Cancelar")}
|
className="cursor-pointer"
|
||||||
|
disabled={isCreating}
|
||||||
|
form={formId}
|
||||||
|
onClick={() => guardClose(false)}
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{t("common.cancel", "Cancelar")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" form={formId} disabled={isCreating} className='cursor-pointer'>
|
<Button className="cursor-pointer" disabled={isCreating} form={formId} type="submit">
|
||||||
{isCreating ? <span aria-live="polite">{t('common.saving', "Guardando")}</span> : <span>{t('common.save', "Guardar")}</span>}
|
{isCreating ? (
|
||||||
|
<span aria-live="polite">{t("common.saving", "Guardando")}</span>
|
||||||
|
) : (
|
||||||
|
<span>{t("common.save", "Guardar")}</span>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
</UnsavedChangesProvider>
|
</UnsavedChangesProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,8 +1,9 @@
|
|||||||
import { Field, FieldLabel } from "@repo/shadcn-ui/components";
|
import { Field, FieldLabel } from "@repo/shadcn-ui/components";
|
||||||
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
|
import { type Control, Controller, type FieldPath, type FieldValues } from "react-hook-form";
|
||||||
|
|
||||||
|
import type { CustomerSummary } from "../../schemas";
|
||||||
|
|
||||||
import { cn } from '@repo/shadcn-ui/lib/utils';
|
|
||||||
import { Control, Controller, FieldPath, FieldValues } from "react-hook-form";
|
|
||||||
import { CustomerSummary } from '../../schemas';
|
|
||||||
import { CustomerModalSelector } from "./customer-modal-selector";
|
import { CustomerModalSelector } from "./customer-modal-selector";
|
||||||
|
|
||||||
type CustomerModalSelectorFieldProps<TFormValues extends FieldValues> = {
|
type CustomerModalSelectorFieldProps<TFormValues extends FieldValues> = {
|
||||||
@ -12,7 +13,7 @@ type CustomerModalSelectorFieldProps<TFormValues extends FieldValues> = {
|
|||||||
label?: string;
|
label?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
orientation?: "vertical" | "horizontal" | "responsive",
|
orientation?: "vertical" | "horizontal" | "responsive";
|
||||||
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
@ -28,8 +29,7 @@ export function CustomerModalSelectorField<TFormValues extends FieldValues>({
|
|||||||
label,
|
label,
|
||||||
description,
|
description,
|
||||||
|
|
||||||
orientation = 'vertical',
|
orientation = "vertical",
|
||||||
|
|
||||||
|
|
||||||
disabled = false,
|
disabled = false,
|
||||||
required = false,
|
required = false,
|
||||||
@ -49,21 +49,21 @@ export function CustomerModalSelectorField<TFormValues extends FieldValues>({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Field
|
<Field
|
||||||
|
className={cn("gap-1", className)}
|
||||||
data-invalid={fieldState.invalid}
|
data-invalid={fieldState.invalid}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn("gap-1", className)}
|
|
||||||
>
|
>
|
||||||
{label && (
|
{label && (
|
||||||
<FieldLabel className='text-xs text-muted-foreground text-nowrap' htmlFor={name}>
|
<FieldLabel className="text-xs text-muted-foreground text-nowrap" htmlFor={name}>
|
||||||
{label}
|
{label}
|
||||||
</FieldLabel>
|
</FieldLabel>
|
||||||
)}
|
)}
|
||||||
<CustomerModalSelector
|
<CustomerModalSelector
|
||||||
value={value}
|
|
||||||
onValueChange={onChange}
|
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
readOnly={isReadOnly}
|
|
||||||
initialCustomer={initiaCustomer as CustomerSummary}
|
initialCustomer={initiaCustomer as CustomerSummary}
|
||||||
|
onValueChange={onChange}
|
||||||
|
readOnly={isReadOnly}
|
||||||
|
value={value}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,11 +1,17 @@
|
|||||||
import { useEffect, useId, useMemo, useState } from "react";
|
import { useEffect, useId, useMemo, useState } from "react";
|
||||||
|
|
||||||
import { useCustomerListQuery } from "../../hooks";
|
import { useCustomerListQuery } from "../../hooks";
|
||||||
import { CustomerFormData, CustomerSummary, defaultCustomerFormData } from "../../schemas";
|
import {
|
||||||
|
type CustomerFormData,
|
||||||
|
type CustomerSummary,
|
||||||
|
defaultCustomerFormData,
|
||||||
|
} from "../../schemas";
|
||||||
|
|
||||||
import { CustomerCard } from "./customer-card";
|
import { CustomerCard } from "./customer-card";
|
||||||
import { CustomerCreateModal } from './customer-create-modal';
|
import { CustomerCreateModal } from "./customer-create-modal";
|
||||||
import { CustomerEmptyCard } from "./customer-empty-card";
|
import { CustomerEmptyCard } from "./customer-empty-card";
|
||||||
import { CustomerSearchDialog } from "./customer-search-dialog";
|
import { CustomerSearchDialog } from "./customer-search-dialog";
|
||||||
import { CustomerViewDialog } from './customer-view-dialog';
|
import { CustomerViewDialog } from "./customer-view-dialog";
|
||||||
|
|
||||||
// Debounce pequeño y tipado
|
// Debounce pequeño y tipado
|
||||||
function useDebouncedValue<T>(value: T, delay = 300) {
|
function useDebouncedValue<T>(value: T, delay = 300) {
|
||||||
@ -17,7 +23,6 @@ function useDebouncedValue<T>(value: T, delay = 300) {
|
|||||||
return debounced;
|
return debounced;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type CustomerModalSelectorProps = {
|
type CustomerModalSelectorProps = {
|
||||||
value?: string;
|
value?: string;
|
||||||
onValueChange?: (id: string) => void;
|
onValueChange?: (id: string) => void;
|
||||||
@ -25,7 +30,7 @@ type CustomerModalSelectorProps = {
|
|||||||
readOnly?: boolean; // Ver ficha, pero no cambiar/crear
|
readOnly?: boolean; // Ver ficha, pero no cambiar/crear
|
||||||
initialCustomer?: CustomerSummary;
|
initialCustomer?: CustomerSummary;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const CustomerModalSelector = ({
|
export const CustomerModalSelector = ({
|
||||||
value,
|
value,
|
||||||
@ -35,7 +40,6 @@ export const CustomerModalSelector = ({
|
|||||||
initialCustomer,
|
initialCustomer,
|
||||||
className,
|
className,
|
||||||
}: CustomerModalSelectorProps) => {
|
}: CustomerModalSelectorProps) => {
|
||||||
|
|
||||||
const dialogId = useId();
|
const dialogId = useId();
|
||||||
|
|
||||||
const [showSearch, setShowSearch] = useState(false);
|
const [showSearch, setShowSearch] = useState(false);
|
||||||
@ -68,32 +72,28 @@ export const CustomerModalSelector = ({
|
|||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
error,
|
error,
|
||||||
} = useCustomerListQuery(
|
} = useCustomerListQuery({
|
||||||
{
|
enabled: showSearch, // <- evita llamadas innecesarias
|
||||||
enabled: showSearch, // <- evita llamadas innecesarias
|
criteria,
|
||||||
criteria
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Combinar locales optimistas + remotos
|
// Combinar locales optimistas + remotos
|
||||||
const customers: CustomerSummary[] = useMemo(() => {
|
const customers: CustomerSummary[] = useMemo(() => {
|
||||||
const remoteCustomers = remoteCustomersPage ? remoteCustomersPage.items : []
|
const remoteCustomers = remoteCustomersPage ? remoteCustomersPage.items : [];
|
||||||
const byId = new Map<string, CustomerSummary>();
|
const byId = new Map<string, CustomerSummary>();
|
||||||
[...localCreated, ...remoteCustomers].forEach((c) => byId.set(c.id, c as CustomerSummary));
|
[...localCreated, ...remoteCustomers].forEach((c) => byId.set(c.id, c as CustomerSummary));
|
||||||
return Array.from(byId.values());
|
return Array.from(byId.values());
|
||||||
}, [localCreated, remoteCustomersPage]);
|
}, [localCreated, remoteCustomersPage]);
|
||||||
|
|
||||||
|
|
||||||
// Sync con value e initialCustomer
|
// Sync con value e initialCustomer
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const found = customers.find((c) => c.id === value) ?? initialCustomer ?? null;
|
const found = customers.find((c) => c.id === value) ?? initialCustomer ?? null;
|
||||||
setSelected(found ?? null);
|
setSelected(found ?? null);
|
||||||
}, [value, customers, initialCustomer]);
|
}, [value, customers, initialCustomer]);
|
||||||
|
|
||||||
|
|
||||||
// Crear cliente (optimista) mapeando desde CustomerDraft -> CustomerSummary
|
// Crear cliente (optimista) mapeando desde CustomerDraft -> CustomerSummary
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
if (!newClient.name || !newClient.email_primary) return;
|
if (!(newClient.name && newClient.email_primary)) return;
|
||||||
|
|
||||||
const newCustomer: CustomerSummary = defaultCustomerFormData as CustomerSummary;
|
const newCustomer: CustomerSummary = defaultCustomerFormData as CustomerSummary;
|
||||||
|
|
||||||
@ -104,8 +104,8 @@ export const CustomerModalSelector = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Handlers de tarjeta según modo
|
// Handlers de tarjeta según modo
|
||||||
const canChange = !disabled && !readOnly;
|
const canChange = !(disabled || readOnly);
|
||||||
const canCreate = !disabled && !readOnly;
|
const canCreate = !(disabled || readOnly);
|
||||||
const canView = !!selected && !disabled;
|
const canView = !!selected && !disabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -115,62 +115,64 @@ export const CustomerModalSelector = ({
|
|||||||
<CustomerCard
|
<CustomerCard
|
||||||
className={className}
|
className={className}
|
||||||
customer={selected}
|
customer={selected}
|
||||||
onViewCustomer={canView ? () => setShowView(true) : undefined}
|
|
||||||
onChangeCustomer={canChange ? () => setShowSearch(true) : undefined}
|
|
||||||
onAddNewCustomer={canCreate ? () => setShowNewForm(true) : undefined}
|
onAddNewCustomer={canCreate ? () => setShowNewForm(true) : undefined}
|
||||||
|
onChangeCustomer={canChange ? () => setShowSearch(true) : undefined}
|
||||||
|
onViewCustomer={canView ? () => setShowView(true) : undefined}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<CustomerEmptyCard
|
<CustomerEmptyCard
|
||||||
className={className}
|
|
||||||
onClick={!disabled && !readOnly ? () => setShowSearch(true) : undefined}
|
|
||||||
onKeyDown={
|
|
||||||
!disabled && !readOnly
|
|
||||||
? (e) => {
|
|
||||||
if (e.key === "Enter" || e.key === " ") setShowSearch(true);
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
aria-haspopup="dialog"
|
|
||||||
aria-controls={dialogId}
|
aria-controls={dialogId}
|
||||||
aria-disabled={disabled || readOnly}
|
aria-disabled={disabled || readOnly}
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
className={className}
|
||||||
|
onClick={disabled || readOnly ? undefined : () => setShowSearch(true)}
|
||||||
|
onKeyDown={
|
||||||
|
disabled || readOnly
|
||||||
|
? undefined
|
||||||
|
: (e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") setShowSearch(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CustomerSearchDialog
|
<CustomerSearchDialog
|
||||||
open={showSearch}
|
|
||||||
onOpenChange={setShowSearch}
|
|
||||||
searchQuery={searchQuery}
|
|
||||||
onSearchQueryChange={setSearchQuery}
|
|
||||||
customers={customers}
|
customers={customers}
|
||||||
selectedClient={selected}
|
errorMessage={
|
||||||
|
isError ? ((error as Error)?.message ?? "Error al cargar clientes") : undefined
|
||||||
|
}
|
||||||
|
isError={isError}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onCreateClient={(name) => {
|
||||||
|
setNewClient((prev) => ({ ...prev, name: name ?? "" }));
|
||||||
|
setShowNewForm(true);
|
||||||
|
}}
|
||||||
|
onOpenChange={setShowSearch}
|
||||||
|
onSearchQueryChange={setSearchQuery}
|
||||||
onSelectClient={(c) => {
|
onSelectClient={(c) => {
|
||||||
setSelected(c);
|
setSelected(c);
|
||||||
onValueChange?.(c.id);
|
onValueChange?.(c.id);
|
||||||
setShowSearch(false);
|
setShowSearch(false);
|
||||||
}}
|
}}
|
||||||
onCreateClient={(name) => {
|
open={showSearch}
|
||||||
setNewClient((prev) => ({ ...prev, name: name ?? "" }));
|
searchQuery={searchQuery}
|
||||||
setShowNewForm(true);
|
selectedClient={selected}
|
||||||
}}
|
|
||||||
isLoading={isLoading}
|
|
||||||
isError={isError}
|
|
||||||
errorMessage={isError ? ((error as Error)?.message ?? "Error al cargar clientes") : undefined}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CustomerViewDialog
|
<CustomerViewDialog
|
||||||
customerId={selected?.id ?? null}
|
customerId={selected?.id ?? null}
|
||||||
open={showView}
|
|
||||||
onOpenChange={setShowView}
|
onOpenChange={setShowView}
|
||||||
|
open={showView}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Diálogo de alta rápida */}
|
{/* Diálogo de alta rápida */}
|
||||||
<CustomerCreateModal
|
<CustomerCreateModal
|
||||||
open={showNewForm}
|
|
||||||
onOpenChange={setShowNewForm}
|
|
||||||
client={newClient}
|
client={newClient}
|
||||||
onChange={setNewClient}
|
onChange={setNewClient}
|
||||||
|
onOpenChange={setShowNewForm}
|
||||||
onSubmit={handleCreate}
|
onSubmit={handleCreate}
|
||||||
|
open={showNewForm}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -24,7 +24,8 @@ import {
|
|||||||
UserIcon,
|
UserIcon,
|
||||||
UserPlusIcon,
|
UserPlusIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { CustomerSummary } from "../../schemas";
|
|
||||||
|
import type { CustomerSummary } from "../../schemas";
|
||||||
|
|
||||||
interface CustomerSearchDialogProps {
|
interface CustomerSearchDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -53,45 +54,45 @@ export const CustomerSearchDialog = ({
|
|||||||
isError,
|
isError,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
}: CustomerSearchDialogProps) => {
|
}: CustomerSearchDialogProps) => {
|
||||||
const isEmpty = !isLoading && !isError && customers && customers.length === 0;
|
const isEmpty = !(isLoading || isError) && customers && customers.length === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog onOpenChange={onOpenChange} open={open}>
|
||||||
<DialogContent className='sm:max-w-2xl bg-card border-border p-0'>
|
<DialogContent className="sm:max-w-2xl bg-card border-border p-0">
|
||||||
<DialogHeader className='px-6 pt-6 pb-4'>
|
<DialogHeader className="px-6 pt-6 pb-4">
|
||||||
<DialogTitle className='flex items-center justify-between'>
|
<DialogTitle className="flex items-center justify-between">
|
||||||
<span className='flex items-center gap-2'>
|
<span className="flex items-center gap-2">
|
||||||
<User className='size-5' />
|
<User className="size-5" />
|
||||||
Seleccionar Cliente
|
Seleccionar Cliente
|
||||||
</span>
|
</span>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>Busca un cliente existente o crea uno nuevo.</DialogDescription>
|
<DialogDescription>Busca un cliente existente o crea uno nuevo.</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className='px-6 pb-3'>
|
<div className="px-6 pb-3">
|
||||||
<Command className='border rounded-lg' shouldFilter={false}>
|
<Command className="border rounded-lg" shouldFilter={false}>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
autoFocus
|
autoFocus
|
||||||
|
onValueChange={onSearchQueryChange}
|
||||||
placeholder="Buscar cliente..."
|
placeholder="Buscar cliente..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onValueChange={onSearchQueryChange}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CommandList className='max-h-[600px]'>
|
<CommandList className="max-h-[600px]">
|
||||||
<CommandEmpty>
|
<CommandEmpty>
|
||||||
<div className='flex flex-col items-center gap-2 py-6 text-sm'>
|
<div className="flex flex-col items-center gap-2 py-6 text-sm">
|
||||||
<User className='size-8 text-muted-foreground/50' />
|
<User className="size-8 text-muted-foreground/50" />
|
||||||
{isLoading && <p>Cargando…</p>}
|
{isLoading && <p>Cargando…</p>}
|
||||||
{isError && <p className='text-destructive'>{errorMessage}</p>}
|
{isError && <p className="text-destructive">{errorMessage}</p>}
|
||||||
{!isLoading && !isError && (
|
{!(isLoading || isError) && (
|
||||||
<>
|
<>
|
||||||
<p>No se encontraron clientes</p>
|
<p>No se encontraron clientes</p>
|
||||||
{searchQuery && (
|
{searchQuery && (
|
||||||
<Button
|
<Button
|
||||||
|
className="cursor-pointer"
|
||||||
onClick={() => onCreateClient(searchQuery)}
|
onClick={() => onCreateClient(searchQuery)}
|
||||||
className='cursor-pointer'
|
|
||||||
>
|
>
|
||||||
<UserPlusIcon className='mr-2 size-4' />
|
<UserPlusIcon className="mr-2 size-4" />
|
||||||
Crear cliente "{searchQuery}"
|
Crear cliente "{searchQuery}"
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@ -104,38 +105,38 @@ export const CustomerSearchDialog = ({
|
|||||||
{customers.map((customer) => {
|
{customers.map((customer) => {
|
||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
|
className="flex items-center gap-x-4 py-5 cursor-pointer"
|
||||||
key={customer.id}
|
key={customer.id}
|
||||||
value={customer.id}
|
|
||||||
onSelect={() => onSelectClient(customer)}
|
onSelect={() => onSelectClient(customer)}
|
||||||
className='flex items-center gap-x-4 py-5 cursor-pointer'
|
value={customer.id}
|
||||||
>
|
>
|
||||||
<div className='flex size-12 items-center justify-center rounded-full bg-primary/10'>
|
<div className="flex size-12 items-center justify-center rounded-full bg-primary/10">
|
||||||
<UserIcon className='size-8 stroke-1 text-primary' />
|
<UserIcon className="size-8 stroke-1 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div className='flex-1 space-y-1 min-w-0'>
|
<div className="flex-1 space-y-1 min-w-0">
|
||||||
<div className='flex items-center gap-2'>
|
<div className="flex items-center gap-2">
|
||||||
<span className='text-sm font-semibold'>{customer.name}</span>
|
<span className="text-sm font-semibold">{customer.name}</span>
|
||||||
{customer.trade_name && (
|
{customer.trade_name && (
|
||||||
<Badge variant='secondary' className='text-sm'>
|
<Badge className="text-sm" variant="secondary">
|
||||||
{customer.trade_name}
|
{customer.trade_name}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-6 text-sm font-medium text-muted-foreground'>
|
<div className="flex items-center gap-6 text-sm font-medium text-muted-foreground">
|
||||||
{customer.tin && (
|
{customer.tin && (
|
||||||
<div className='flex items-center gap-2 text-sm text-muted-foreground'>
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<CreditCardIcon className='h-4 w-4 shrink-0' />
|
<CreditCardIcon className="h-4 w-4 shrink-0" />
|
||||||
<span className='font-medium'>{customer.tin}</span>
|
<span className="font-medium">{customer.tin}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{customer.email_primary && (
|
{customer.email_primary && (
|
||||||
<span className='flex items-center gap-1'>
|
<span className="flex items-center gap-1">
|
||||||
<MailIcon className='size-4' /> {customer.email_primary}
|
<MailIcon className="size-4" /> {customer.email_primary}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{customer.mobile_primary && (
|
{customer.mobile_primary && (
|
||||||
<span className='flex items-center gap-1'>
|
<span className="flex items-center gap-1">
|
||||||
<SmartphoneIcon className='size-4' /> {customer.mobile_primary}
|
<SmartphoneIcon className="size-4" /> {customer.mobile_primary}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -153,12 +154,12 @@ export const CustomerSearchDialog = ({
|
|||||||
</CommandList>
|
</CommandList>
|
||||||
</Command>
|
</Command>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className='sm:justify-center px-6 pb-6'>
|
<DialogFooter className="sm:justify-center px-6 pb-6">
|
||||||
<Button
|
<Button
|
||||||
|
className="cursor-pointer text-center"
|
||||||
onClick={() => onCreateClient(searchQuery)}
|
onClick={() => onCreateClient(searchQuery)}
|
||||||
className='cursor-pointer text-center'
|
|
||||||
>
|
>
|
||||||
<UserPlusIcon className='mr-2 size-4' />
|
<UserPlusIcon className="mr-2 size-4" />
|
||||||
Añadir nuevo cliente
|
Añadir nuevo cliente
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@ -1,209 +1,224 @@
|
|||||||
import {
|
import {
|
||||||
Badge, Button, Card, CardContent, CardHeader, CardTitle,
|
Badge,
|
||||||
Dialog,
|
Button,
|
||||||
DialogContent,
|
Card,
|
||||||
DialogDescription,
|
CardContent,
|
||||||
DialogHeader,
|
CardHeader,
|
||||||
DialogTitle,
|
CardTitle,
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { Banknote, FileText, Languages, Mail, MapPin, Phone, Smartphone, X } from "lucide-react";
|
import { Banknote, FileText, Languages, Mail, MapPin, Phone, Smartphone, X } from "lucide-react";
|
||||||
|
|
||||||
// CustomerViewDialog.tsx
|
// CustomerViewDialog.tsx
|
||||||
import { useCustomerQuery } from "../../hooks";
|
import { useCustomerQuery } from "../../hooks";
|
||||||
|
|
||||||
type CustomerViewDialogProps = {
|
type CustomerViewDialogProps = {
|
||||||
customerId: string | null;
|
customerId: string | null;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const CustomerViewDialog = ({
|
export const CustomerViewDialog = ({ customerId, open, onOpenChange }: CustomerViewDialogProps) => {
|
||||||
customerId,
|
const {
|
||||||
open,
|
data: customer,
|
||||||
onOpenChange,
|
isLoading,
|
||||||
}: CustomerViewDialogProps) => {
|
isError,
|
||||||
const {
|
error,
|
||||||
data: customer,
|
} = useCustomerQuery(customerId ?? "", { enabled: open && !!customerId });
|
||||||
isLoading,
|
|
||||||
isError,
|
return (
|
||||||
error,
|
<Dialog onOpenChange={onOpenChange} open={open}>
|
||||||
} = useCustomerQuery(customerId ?? "", { enabled: open && !!customerId });
|
<DialogContent className="sm:max-w-3xl bg-card border-border p-0">
|
||||||
|
<DialogHeader className="px-6 pt-6">
|
||||||
return (
|
<DialogTitle className="flex items-center justify-between" id="customer-view-title">
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<span className="text-balance">
|
||||||
<DialogContent className="sm:max-w-3xl bg-card border-border p-0">
|
{customer?.name ?? "Cliente"}
|
||||||
<DialogHeader className="px-6 pt-6">
|
{customer?.trade_name && (
|
||||||
<DialogTitle id="customer-view-title" className="flex items-center justify-between">
|
<span className="ml-2 text-muted-foreground">({customer.trade_name})</span>
|
||||||
<span className="text-balance">
|
)}
|
||||||
{customer?.name ?? "Cliente"}
|
</span>
|
||||||
{customer?.trade_name && (
|
<Button
|
||||||
<span className="ml-2 text-muted-foreground">({customer.trade_name})</span>
|
aria-label="Cerrar"
|
||||||
)}
|
className="cursor-pointer"
|
||||||
</span>
|
onClick={() => onOpenChange(false)}
|
||||||
<Button
|
size="icon"
|
||||||
type="button"
|
type="button"
|
||||||
size="icon"
|
variant="ghost"
|
||||||
variant="ghost"
|
>
|
||||||
className="cursor-pointer"
|
<X className="size-4" />
|
||||||
onClick={() => onOpenChange(false)}
|
</Button>
|
||||||
aria-label="Cerrar"
|
</DialogTitle>
|
||||||
>
|
<DialogDescription className="px-0">
|
||||||
<X className="size-4" />
|
{customer?.tin ? (
|
||||||
</Button>
|
<Badge className="ml-0 font-mono" variant="secondary">
|
||||||
</DialogTitle>
|
{customer.tin}
|
||||||
<DialogDescription className="px-0">
|
</Badge>
|
||||||
{customer?.tin ? (
|
) : (
|
||||||
<Badge variant="secondary" className="ml-0 font-mono">{customer.tin}</Badge>
|
<span className="text-muted-foreground">Ficha del cliente</span>
|
||||||
) : (
|
)}
|
||||||
<span className="text-muted-foreground">Ficha del cliente</span>
|
</DialogDescription>
|
||||||
)}
|
</DialogHeader>
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
<div className="px-6 pb-6">
|
||||||
|
{isLoading && (
|
||||||
<div className="px-6 pb-6">
|
<p aria-live="polite" role="status">
|
||||||
{isLoading && <p role="status" aria-live="polite">Cargando…</p>}
|
Cargando…
|
||||||
{isError && (
|
</p>
|
||||||
<p role="alert" className="text-destructive">
|
)}
|
||||||
{(error as Error)?.message ?? "No se pudo cargar el cliente"}
|
{isError && (
|
||||||
</p>
|
<p className="text-destructive" role="alert">
|
||||||
)}
|
{(error as Error)?.message ?? "No se pudo cargar el cliente"}
|
||||||
|
</p>
|
||||||
{!isLoading && !isError && customer && (
|
)}
|
||||||
<div className="grid gap-6 md:grid-cols-2 max-h-[70vh] overflow-y-auto pr-1">
|
|
||||||
<Card>
|
{!(isLoading || isError) && customer && (
|
||||||
<CardHeader>
|
<div className="grid gap-6 md:grid-cols-2 max-h-[70vh] overflow-y-auto pr-1">
|
||||||
<CardTitle className="flex items-center gap-2 text-lg">
|
<Card>
|
||||||
<FileText className="size-5 text-primary" />
|
<CardHeader>
|
||||||
Información Básica
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
</CardTitle>
|
<FileText className="size-5 text-primary" />
|
||||||
</CardHeader>
|
Información Básica
|
||||||
<CardContent className="space-y-3">
|
</CardTitle>
|
||||||
<div>
|
</CardHeader>
|
||||||
<dt className="text-sm text-muted-foreground">Nombre</dt>
|
<CardContent className="space-y-3">
|
||||||
<dd className="mt-1">{customer.name}</dd>
|
<div>
|
||||||
</div>
|
<dt className="text-sm text-muted-foreground">Nombre</dt>
|
||||||
{customer.reference && (
|
<dd className="mt-1">{customer.name}</dd>
|
||||||
<div>
|
</div>
|
||||||
<dt className="text-sm text-muted-foreground">Referencia</dt>
|
{customer.reference && (
|
||||||
<dd className="mt-1 font-mono">{customer.reference}</dd>
|
<div>
|
||||||
</div>
|
<dt className="text-sm text-muted-foreground">Referencia</dt>
|
||||||
)}
|
<dd className="mt-1 font-mono">{customer.reference}</dd>
|
||||||
{customer.legal_record && (
|
</div>
|
||||||
<div>
|
)}
|
||||||
<dt className="text-sm text-muted-foreground">Registro Legal</dt>
|
{customer.legal_record && (
|
||||||
<dd className="mt-1">{customer.legal_record}</dd>
|
<div>
|
||||||
</div>
|
<dt className="text-sm text-muted-foreground">Registro Legal</dt>
|
||||||
)}
|
<dd className="mt-1">{customer.legal_record}</dd>
|
||||||
{!!customer.default_taxes?.length && (
|
</div>
|
||||||
<div>
|
)}
|
||||||
<dt className="text-sm text-muted-foreground">Impuestos por defecto</dt>
|
{!!customer.default_taxes?.length && (
|
||||||
<dd className="mt-1 flex flex-wrap gap-1">
|
<div>
|
||||||
{customer.default_taxes.map((tax: string) => (
|
<dt className="text-sm text-muted-foreground">Impuestos por defecto</dt>
|
||||||
<Badge key={tax} variant="secondary">{tax}</Badge>
|
<dd className="mt-1 flex flex-wrap gap-1">
|
||||||
))}
|
{customer.default_taxes.map((tax: string) => (
|
||||||
</dd>
|
<Badge key={tax} variant="secondary">
|
||||||
</div>
|
{tax}
|
||||||
)}
|
</Badge>
|
||||||
</CardContent>
|
))}
|
||||||
</Card>
|
</dd>
|
||||||
|
</div>
|
||||||
<Card>
|
)}
|
||||||
<CardHeader>
|
</CardContent>
|
||||||
<CardTitle className="flex items-center gap-2 text-lg">
|
</Card>
|
||||||
<MapPin className="size-5 text-primary" />
|
|
||||||
Dirección
|
<Card>
|
||||||
</CardTitle>
|
<CardHeader>
|
||||||
</CardHeader>
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
<CardContent className="space-y-3">
|
<MapPin className="size-5 text-primary" />
|
||||||
<div>
|
Dirección
|
||||||
<dt className="text-sm text-muted-foreground">Calle</dt>
|
</CardTitle>
|
||||||
<dd className="mt-1">
|
</CardHeader>
|
||||||
{customer.street}
|
<CardContent className="space-y-3">
|
||||||
{customer.street2 && (<><br />{customer.street2}</>)}
|
<div>
|
||||||
</dd>
|
<dt className="text-sm text-muted-foreground">Calle</dt>
|
||||||
</div>
|
<dd className="mt-1">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
{customer.street}
|
||||||
<div>
|
{customer.street2 && (
|
||||||
<dt className="text-sm text-muted-foreground">Ciudad</dt>
|
<>
|
||||||
<dd className="mt-1">{customer.city}</dd>
|
<br />
|
||||||
</div>
|
{customer.street2}
|
||||||
<div>
|
</>
|
||||||
<dt className="text-sm text-muted-foreground">Código Postal</dt>
|
)}
|
||||||
<dd className="mt-1">{customer.postal_code}</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div>
|
||||||
<div>
|
<dt className="text-sm text-muted-foreground">Ciudad</dt>
|
||||||
<dt className="text-sm text-muted-foreground">Provincia</dt>
|
<dd className="mt-1">{customer.city}</dd>
|
||||||
<dd className="mt-1">{customer.province}</dd>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div>
|
<dt className="text-sm text-muted-foreground">Código Postal</dt>
|
||||||
<dt className="text-sm text-muted-foreground">País</dt>
|
<dd className="mt-1">{customer.postal_code}</dd>
|
||||||
<dd className="mt-1">{customer.country}</dd>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="grid grid-cols-2 gap-4">
|
||||||
</CardContent>
|
<div>
|
||||||
</Card>
|
<dt className="text-sm text-muted-foreground">Provincia</dt>
|
||||||
|
<dd className="mt-1">{customer.province}</dd>
|
||||||
<Card className="md:col-span-2">
|
</div>
|
||||||
<CardHeader>
|
<div>
|
||||||
<CardTitle className="flex items-center gap-2 text-lg">
|
<dt className="text-sm text-muted-foreground">País</dt>
|
||||||
<Mail className="size-5 text-primary" />
|
<dd className="mt-1">{customer.country}</dd>
|
||||||
Contacto y Preferencias
|
</div>
|
||||||
</CardTitle>
|
</div>
|
||||||
</CardHeader>
|
</CardContent>
|
||||||
<CardContent className="grid gap-6 md:grid-cols-2">
|
</Card>
|
||||||
<div className="space-y-3">
|
|
||||||
{customer.email_primary && (
|
<Card className="md:col-span-2">
|
||||||
<div className="flex items-start gap-3">
|
<CardHeader>
|
||||||
<Mail className="mt-0.5 size-4 text-muted-foreground" />
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
<div className="flex-1">
|
<Mail className="size-5 text-primary" />
|
||||||
<dt className="text-sm text-muted-foreground">Email</dt>
|
Contacto y Preferencias
|
||||||
<dd className="mt-1">{customer.email_primary}</dd>
|
</CardTitle>
|
||||||
</div>
|
</CardHeader>
|
||||||
</div>
|
<CardContent className="grid gap-6 md:grid-cols-2">
|
||||||
)}
|
<div className="space-y-3">
|
||||||
{customer.mobile_primary && (
|
{customer.email_primary && (
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<Smartphone className="mt-0.5 size-4 text-muted-foreground" />
|
<Mail className="mt-0.5 size-4 text-muted-foreground" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<dt className="text-sm text-muted-foreground">Móvil</dt>
|
<dt className="text-sm text-muted-foreground">Email</dt>
|
||||||
<dd className="mt-1">{customer.mobile_primary}</dd>
|
<dd className="mt-1">{customer.email_primary}</dd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{customer.phone_primary && (
|
{customer.mobile_primary && (
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<Phone className="mt-0.5 size-4 text-muted-foreground" />
|
<Smartphone className="mt-0.5 size-4 text-muted-foreground" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<dt className="text-sm text-muted-foreground">Teléfono</dt>
|
<dt className="text-sm text-muted-foreground">Móvil</dt>
|
||||||
<dd className="mt-1">{customer.phone_primary}</dd>
|
<dd className="mt-1">{customer.mobile_primary}</dd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
{customer.phone_primary && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
<div className="space-y-3">
|
<Phone className="mt-0.5 size-4 text-muted-foreground" />
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex-1">
|
||||||
<Languages className="mt-0.5 size-4 text-muted-foreground" />
|
<dt className="text-sm text-muted-foreground">Teléfono</dt>
|
||||||
<div className="flex-1">
|
<dd className="mt-1">{customer.phone_primary}</dd>
|
||||||
<dt className="text-sm text-muted-foreground">Idioma</dt>
|
</div>
|
||||||
<dd className="mt-1">{customer.language_code}</dd>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<Banknote className="mt-0.5 size-4 text-muted-foreground" />
|
<div className="space-y-3">
|
||||||
<div className="flex-1">
|
<div className="flex items-start gap-3">
|
||||||
<dt className="text-sm text-muted-foreground">Moneda</dt>
|
<Languages className="mt-0.5 size-4 text-muted-foreground" />
|
||||||
<dd className="mt-1">{customer.currency_code}</dd>
|
<div className="flex-1">
|
||||||
</div>
|
<dt className="text-sm text-muted-foreground">Idioma</dt>
|
||||||
</div>
|
<dd className="mt-1">{customer.language_code}</dd>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
<div className="flex items-start gap-3">
|
||||||
</div>
|
<Banknote className="mt-0.5 size-4 text-muted-foreground" />
|
||||||
)}
|
<div className="flex-1">
|
||||||
</div>
|
<dt className="text-sm text-muted-foreground">Moneda</dt>
|
||||||
</DialogContent>
|
<dd className="mt-1">{customer.currency_code}</dd>
|
||||||
</Dialog>
|
</div>
|
||||||
);
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
import type { PropsWithChildren } from "react";
|
|
||||||
|
|
||||||
import { CustomersProvider } from "../context";
|
|
||||||
|
|
||||||
export const CustomersLayout = ({ children }: PropsWithChildren) => {
|
|
||||||
return <CustomersProvider>{children}</CustomersProvider>;
|
|
||||||
};
|
|
||||||
@ -1,18 +1,24 @@
|
|||||||
import { Field, FieldDescription, FieldGroup, FieldLegend, FieldSet } from '@repo/shadcn-ui/components';
|
|
||||||
|
|
||||||
import { SelectField } from "@repo/rdx-ui/components";
|
import { SelectField } from "@repo/rdx-ui/components";
|
||||||
|
import {
|
||||||
|
Field,
|
||||||
|
FieldDescription,
|
||||||
|
FieldGroup,
|
||||||
|
FieldLegend,
|
||||||
|
FieldSet,
|
||||||
|
} from "@repo/shadcn-ui/components";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { CURRENCY_OPTIONS, LANGUAGE_OPTIONS } from "../../constants";
|
import { CURRENCY_OPTIONS, LANGUAGE_OPTIONS } from "../../constants";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import { CustomerFormData } from "../../schemas";
|
import type { CustomerFormData } from "../../schemas";
|
||||||
|
|
||||||
interface CustomerAdditionalConfigFieldsProps {
|
interface CustomerAdditionalConfigFieldsProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const CustomerAdditionalConfigFields = ({
|
export const CustomerAdditionalConfigFields = ({
|
||||||
className, ...props
|
className,
|
||||||
|
...props
|
||||||
}: CustomerAdditionalConfigFieldsProps) => {
|
}: CustomerAdditionalConfigFieldsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { control } = useFormContext<CustomerFormData>();
|
const { control } = useFormContext<CustomerFormData>();
|
||||||
@ -21,28 +27,28 @@ export const CustomerAdditionalConfigFields = ({
|
|||||||
<FieldSet className={className} {...props}>
|
<FieldSet className={className} {...props}>
|
||||||
<FieldLegend>{t("form_groups.preferences.title")}</FieldLegend>
|
<FieldLegend>{t("form_groups.preferences.title")}</FieldLegend>
|
||||||
<FieldDescription>{t("form_groups.preferences.description")}</FieldDescription>
|
<FieldDescription>{t("form_groups.preferences.description")}</FieldDescription>
|
||||||
<FieldGroup className='grid grid-cols-1 gap-x-6 lg:grid-cols-4'>
|
<FieldGroup className="grid grid-cols-1 gap-x-6 lg:grid-cols-4">
|
||||||
<Field className='lg:col-span-2'>
|
<Field className="lg:col-span-2">
|
||||||
<SelectField
|
<SelectField
|
||||||
control={control}
|
control={control}
|
||||||
name='language_code'
|
|
||||||
required
|
|
||||||
label={t("form_fields.language_code.label")}
|
|
||||||
placeholder={t("form_fields.language_code.placeholder")}
|
|
||||||
description={t("form_fields.language_code.description")}
|
description={t("form_fields.language_code.description")}
|
||||||
items={[...LANGUAGE_OPTIONS]}
|
items={[...LANGUAGE_OPTIONS]}
|
||||||
|
label={t("form_fields.language_code.label")}
|
||||||
|
name="language_code"
|
||||||
|
placeholder={t("form_fields.language_code.placeholder")}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field className='lg:col-span-2'>
|
<Field className="lg:col-span-2">
|
||||||
<SelectField
|
<SelectField
|
||||||
className='lg:col-span-2'
|
className="lg:col-span-2"
|
||||||
control={control}
|
control={control}
|
||||||
name='currency_code'
|
|
||||||
required
|
|
||||||
label={t("form_fields.currency_code.label")}
|
|
||||||
placeholder={t("form_fields.currency_code.placeholder")}
|
|
||||||
description={t("form_fields.currency_code.description")}
|
description={t("form_fields.currency_code.description")}
|
||||||
items={[...CURRENCY_OPTIONS]}
|
items={[...CURRENCY_OPTIONS]}
|
||||||
|
label={t("form_fields.currency_code.label")}
|
||||||
|
name="currency_code"
|
||||||
|
placeholder={t("form_fields.currency_code.placeholder")}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
|
import { SelectField, TextField } from "@repo/rdx-ui/components";
|
||||||
import {
|
import {
|
||||||
SelectField,
|
Field,
|
||||||
TextField
|
FieldDescription,
|
||||||
} from "@repo/rdx-ui/components";
|
FieldGroup,
|
||||||
import { Field, FieldDescription, FieldGroup, FieldLegend, FieldSet } from '@repo/shadcn-ui/components';
|
FieldLegend,
|
||||||
|
FieldSet,
|
||||||
|
} from "@repo/shadcn-ui/components";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { COUNTRY_OPTIONS } from "../../constants";
|
import { COUNTRY_OPTIONS } from "../../constants";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import { CustomerFormData } from "../../schemas";
|
import type { CustomerFormData } from "../../schemas";
|
||||||
|
|
||||||
interface CustomerAddressFieldsProps {
|
interface CustomerAddressFieldsProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -20,58 +24,58 @@ export const CustomerAddressFields = ({ className, ...props }: CustomerAddressFi
|
|||||||
<FieldSet className={className} {...props}>
|
<FieldSet className={className} {...props}>
|
||||||
<FieldLegend>{t("form_groups.address.title")}</FieldLegend>
|
<FieldLegend>{t("form_groups.address.title")}</FieldLegend>
|
||||||
<FieldDescription>{t("form_groups.address.description")}</FieldDescription>
|
<FieldDescription>{t("form_groups.address.description")}</FieldDescription>
|
||||||
<FieldGroup className='grid grid-cols-1 gap-x-6 lg:grid-cols-4'>
|
<FieldGroup className="grid grid-cols-1 gap-x-6 lg:grid-cols-4">
|
||||||
<TextField
|
<TextField
|
||||||
className='lg:col-span-2'
|
className="lg:col-span-2"
|
||||||
control={control}
|
control={control}
|
||||||
name='street'
|
|
||||||
label={t("form_fields.street.label")}
|
|
||||||
placeholder={t("form_fields.street.placeholder")}
|
|
||||||
description={t("form_fields.street.description")}
|
description={t("form_fields.street.description")}
|
||||||
|
label={t("form_fields.street.label")}
|
||||||
|
name="street"
|
||||||
|
placeholder={t("form_fields.street.placeholder")}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
className='lg:col-span-2'
|
className="lg:col-span-2"
|
||||||
control={control}
|
control={control}
|
||||||
name='street2'
|
|
||||||
label={t("form_fields.street2.label")}
|
|
||||||
placeholder={t("form_fields.street2.placeholder")}
|
|
||||||
description={t("form_fields.street2.description")}
|
description={t("form_fields.street2.description")}
|
||||||
|
label={t("form_fields.street2.label")}
|
||||||
|
name="street2"
|
||||||
|
placeholder={t("form_fields.street2.placeholder")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
className='lg:col-span-2'
|
className="lg:col-span-2"
|
||||||
control={control}
|
control={control}
|
||||||
name='city'
|
|
||||||
label={t("form_fields.city.label")}
|
|
||||||
placeholder={t("form_fields.city.placeholder")}
|
|
||||||
description={t("form_fields.city.description")}
|
description={t("form_fields.city.description")}
|
||||||
|
label={t("form_fields.city.label")}
|
||||||
|
name="city"
|
||||||
|
placeholder={t("form_fields.city.placeholder")}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
control={control}
|
control={control}
|
||||||
name='postal_code'
|
|
||||||
label={t("form_fields.postal_code.label")}
|
|
||||||
placeholder={t("form_fields.postal_code.placeholder")}
|
|
||||||
description={t("form_fields.postal_code.description")}
|
description={t("form_fields.postal_code.description")}
|
||||||
|
label={t("form_fields.postal_code.label")}
|
||||||
|
name="postal_code"
|
||||||
|
placeholder={t("form_fields.postal_code.placeholder")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Field className='lg:col-span-2 lg:col-start-1'>
|
<Field className="lg:col-span-2 lg:col-start-1">
|
||||||
<TextField
|
<TextField
|
||||||
control={control}
|
control={control}
|
||||||
name='province'
|
|
||||||
label={t("form_fields.province.label")}
|
|
||||||
placeholder={t("form_fields.province.placeholder")}
|
|
||||||
description={t("form_fields.province.description")}
|
description={t("form_fields.province.description")}
|
||||||
|
label={t("form_fields.province.label")}
|
||||||
|
name="province"
|
||||||
|
placeholder={t("form_fields.province.placeholder")}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field className='lg:col-span-2'>
|
<Field className="lg:col-span-2">
|
||||||
<SelectField
|
<SelectField
|
||||||
control={control}
|
control={control}
|
||||||
name='country'
|
|
||||||
required
|
|
||||||
label={t("form_fields.country.label")}
|
|
||||||
placeholder={t("form_fields.country.placeholder")}
|
|
||||||
description={t("form_fields.country.description")}
|
description={t("form_fields.country.description")}
|
||||||
items={[...COUNTRY_OPTIONS]}
|
items={[...COUNTRY_OPTIONS]}
|
||||||
|
label={t("form_fields.country.label")}
|
||||||
|
name="country"
|
||||||
|
placeholder={t("form_fields.country.placeholder")}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
// components/CustomerSkeleton.tsx
|
// components/CustomerSkeleton.tsx
|
||||||
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
|
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||||
import { Button } from "@repo/shadcn-ui/components";
|
import { Button } from "@repo/shadcn-ui/components";
|
||||||
|
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
|
|
||||||
export const CustomerEditorSkeleton = () => {
|
export const CustomerEditorSkeleton = () => {
|
||||||
@ -8,24 +9,24 @@ export const CustomerEditorSkeleton = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppContent>
|
<AppContent>
|
||||||
<div className='flex items-center justify-between'>
|
<div className="flex items-center justify-between">
|
||||||
<div className='space-y-2' aria-hidden='true'>
|
<div aria-hidden="true" className="space-y-2">
|
||||||
<div className='h-7 w-64 rounded-md bg-muted animate-pulse' />
|
<div className="h-7 w-64 rounded-md bg-muted animate-pulse" />
|
||||||
<div className='h-5 w-96 rounded-md bg-muted animate-pulse' />
|
<div className="h-5 w-96 rounded-md bg-muted animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2'>
|
<div className="flex items-center gap-2">
|
||||||
<BackHistoryButton />
|
<BackHistoryButton />
|
||||||
<Button disabled aria-busy>
|
<Button aria-busy disabled>
|
||||||
{t("pages.update.submit")}
|
{t("pages.update.submit")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-6 grid gap-4' aria-hidden='true'>
|
<div aria-hidden="true" className="mt-6 grid gap-4">
|
||||||
<div className='h-10 w-full rounded-md bg-muted animate-pulse' />
|
<div className="h-10 w-full rounded-md bg-muted animate-pulse" />
|
||||||
<div className='h-10 w-full rounded-md bg-muted animate-pulse' />
|
<div className="h-10 w-full rounded-md bg-muted animate-pulse" />
|
||||||
<div className='h-28 w-full rounded-md bg-muted animate-pulse' />
|
<div className="h-28 w-full rounded-md bg-muted animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
<span className='sr-only'>{t("pages.update.loading", "Cargando cliente...")}</span>
|
<span className="sr-only">{t("pages.update.loading", "Cargando cliente...")}</span>
|
||||||
</AppContent>
|
</AppContent>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
export * from "./client-selector-modal";
|
export * from "./client-selector-modal";
|
||||||
export * from "./customer-modal-selector";
|
export * from "./customer-modal-selector";
|
||||||
export * from "./customer-status-badge";
|
|
||||||
export * from "./customers-layout";
|
|
||||||
export * from "./editor";
|
export * from "./editor";
|
||||||
export * from "./error-alert";
|
export * from "./error-alert";
|
||||||
export * from "./not-found-card";
|
export * from "./not-found-card";
|
||||||
|
|||||||
@ -3,31 +3,29 @@ import { lazy } from "react";
|
|||||||
import { Outlet, type RouteObject } from "react-router-dom";
|
import { Outlet, type RouteObject } from "react-router-dom";
|
||||||
|
|
||||||
// Lazy load components
|
// Lazy load components
|
||||||
const CustomersLayout = lazy(() =>
|
const CustomerLayout = lazy(() => import("./ui").then((m) => ({ default: m.CustomerLayout })));
|
||||||
import("./components").then((m) => ({ default: m.CustomersLayout }))
|
const CustomersList = lazy(() => import("./list").then((m) => ({ default: m.CustomerListPage })));
|
||||||
);
|
|
||||||
|
|
||||||
const CustomersList = lazy(() => import("./pages").then((m) => ({ default: m.CustomersListPage })));
|
//const CustomerView = lazy(() => import("./pages").then((m) => ({ default: m.CustomerViewPage })));
|
||||||
const CustomerView = lazy(() => import("./pages").then((m) => ({ default: m.CustomerViewPage })));
|
//const CustomerAdd = lazy(() => import("./pages").then((m) => ({ default: m.CustomerCreatePage })));
|
||||||
const CustomerAdd = lazy(() => import("./pages").then((m) => ({ default: m.CustomerCreatePage })));
|
/*const CustomerUpdate = lazy(() =>
|
||||||
const CustomerUpdate = lazy(() =>
|
|
||||||
import("./pages").then((m) => ({ default: m.CustomerUpdatePage }))
|
import("./pages").then((m) => ({ default: m.CustomerUpdatePage }))
|
||||||
);
|
);*/
|
||||||
|
|
||||||
export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
|
export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
path: "customers",
|
path: "customers",
|
||||||
element: (
|
element: (
|
||||||
<CustomersLayout>
|
<CustomerLayout>
|
||||||
<Outlet context={params} />
|
<Outlet context={params} />
|
||||||
</CustomersLayout>
|
</CustomerLayout>
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
{ path: "", index: true, element: <CustomersList /> }, // index
|
{ path: "", index: true, element: <CustomersList /> }, // index
|
||||||
{ path: "list", element: <CustomersList /> },
|
{ path: "list", element: <CustomersList /> },
|
||||||
//{ path: "create", element: <CustomerAdd /> },
|
//{ path: "create", element: <CustomerAdd /> },
|
||||||
{ path: ":id", element: <CustomerView /> },
|
//{ path: ":id", element: <CustomerView /> },
|
||||||
//{ path: ":id/edit", element: <CustomerUpdate /> },
|
//{ path: ":id/edit", element: <CustomerUpdate /> },
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
export * from "./use-create-customer-mutation";
|
export * from "./use-create-customer-mutation";
|
||||||
export * from "./use-customer-list-query";
|
|
||||||
export * from "./use-customer-query";
|
export * from "./use-customer-query";
|
||||||
export * from "./use-customers-context";
|
export * from "./use-customers-context";
|
||||||
export * from "./use-update-customer-mutation";
|
export * from "./use-update-customer-mutation";
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
import type { CustomerSummaryPage, CustomerSummaryPageData } from "../../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convierte el DTO completo de API a datos numéricos para el formulario.
|
||||||
|
*/
|
||||||
|
export const CustomerSummaryDtoAdapter = {
|
||||||
|
fromDto(pageDto: CustomerSummaryPage, context?: unknown): CustomerSummaryPageData {
|
||||||
|
return {
|
||||||
|
...pageDto,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
1
modules/customers/src/web/list/adapters/index.ts
Normal file
1
modules/customers/src/web/list/adapters/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./customer-summary-dto.adapter";
|
||||||
18
modules/customers/src/web/list/api/get-customer-list.api.ts
Normal file
18
modules/customers/src/web/list/api/get-customer-list.api.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { CriteriaDTO } from "@erp/core";
|
||||||
|
import type { IDataSource } from "@erp/core/client";
|
||||||
|
|
||||||
|
import type { CustomerSummaryPage } from "../../types";
|
||||||
|
|
||||||
|
export async function getCustomerListApi(
|
||||||
|
dataSource: IDataSource,
|
||||||
|
signal: AbortSignal,
|
||||||
|
criteria: CriteriaDTO
|
||||||
|
) {
|
||||||
|
const response = dataSource.getList<CustomerSummaryPage>("customers", {
|
||||||
|
signal,
|
||||||
|
...criteria,
|
||||||
|
});
|
||||||
|
|
||||||
|
//return mapIssuedInvoiceList(raw);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
1
modules/customers/src/web/list/api/index.ts
Normal file
1
modules/customers/src/web/list/api/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./get-customer-list.api";
|
||||||
1
modules/customers/src/web/list/controllers/index.ts
Normal file
1
modules/customers/src/web/list/controllers/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./use-customer-list-page.controller";
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { useCustomerListController } from "./use-customer-list.controller";
|
||||||
|
|
||||||
|
export function useCustomerListPageController() {
|
||||||
|
const listCtrl = useCustomerListController();
|
||||||
|
|
||||||
|
return {
|
||||||
|
listCtrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user