This commit is contained in:
David Arranz 2025-05-19 13:59:13 +02:00
parent 87eb51a44b
commit bead394ebd
48 changed files with 598 additions and 80 deletions

View File

@ -4,9 +4,10 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { I18nextProvider } from "react-i18next"; import { I18nextProvider } from "react-i18next";
import { createAxiosDataProvider } from "@/lib/axios/create-axios-data-provider"; import { createAxiosDataProvider } from "@/lib/axios/create-axios-data-provider";
import { DataSourceProvider, UnsavedWarnProvider } from "@/lib/hooks"; import { UnsavedWarnProvider } from "@/lib/hooks";
import { i18n } from "@/locales"; import { i18n } from "@/locales";
import { DataSourceProvider } from "@erp/core/hooks";
import { AppRoutes } from "./app-routes"; import { AppRoutes } from "./app-routes";
import "./app.css"; import "./app.css";

View File

@ -1,5 +1,6 @@
import type { ListResponseDTO } from "@erp/core"; import type { IListResponseDTO } from "@erp/core";
import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@repo/rdx-criteria"; import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@repo/rdx-criteria";
import type { IDataSource } from "../../../../../modules/core/src/web/hooks/use-datasource/datasource.interface";
import { getApiAuthorization as getApiAuthLib } from "../api"; import { getApiAuthorization as getApiAuthLib } from "../api";
import type { import type {
ICreateOneDataProviderParams, ICreateOneDataProviderParams,
@ -15,7 +16,6 @@ import type {
IUpdateOneDataProviderParams, IUpdateOneDataProviderParams,
IUploadFileDataProviderParam, IUploadFileDataProviderParam,
} from "../hooks/use-datasource"; } from "../hooks/use-datasource";
import type { IDataSource } from "../hooks/use-datasource/datasource";
import { createAxiosInstance, defaultAxiosRequestConfig } from "./axios-instance"; import { createAxiosInstance, defaultAxiosRequestConfig } from "./axios-instance";
export const createAxiosDataProvider = ( export const createAxiosDataProvider = (
@ -28,7 +28,7 @@ export const createAxiosDataProvider = (
getApiAuthorization: getApiAuthLib, getApiAuthorization: getApiAuthLib,
getList: async <R>(params: IGetListDataProviderParams): Promise<ListResponseDTO<R>> => { getList: async <R>(params: IGetListDataProviderParams): Promise<IListResponseDTO<R>> => {
const { resource, quickSearchTerm, pagination, filters = [], sort = [] } = params; const { resource, quickSearchTerm, pagination, filters = [], sort = [] } = params;
const url = `${apiUrl}/${resource}`; const url = `${apiUrl}/${resource}`;
@ -53,7 +53,7 @@ export const createAxiosDataProvider = (
urlParams.append("$filters", queryFilters.join(",")); urlParams.append("$filters", queryFilters.join(","));
} }
const response = await httpClient.request<ListResponseDTO<R>>({ const response = await httpClient.request<IListResponseDTO<R>>({
url: `${url}?${urlParams.toString()}`, url: `${url}?${urlParams.toString()}`,
method: "GET", method: "GET",
}); });

View File

@ -1,3 +1,2 @@
export * from "./use-datasource";
export * from "./use-theme"; export * from "./use-theme";
export * from "./use-unsaved-changes-notifier"; export * from "./use-unsaved-changes-notifier";

View File

@ -1,11 +0,0 @@
import { type PropsWithChildren, createContext } from "react";
import { type IDataSource } from "./datasource";
export const DataSourceContext = createContext<IDataSource | undefined>(undefined);
export const DataSourceProvider = ({
dataSource,
children,
}: PropsWithChildren<{
dataSource: IDataSource;
}>) => <DataSourceContext.Provider value={dataSource}>{children}</DataSourceContext.Provider>;

View File

@ -1,2 +0,0 @@
export * from "./datasource-context";
export * from "./datasource";

View File

@ -27,6 +27,10 @@
"noUncheckedSideEffectImports": true, "noUncheckedSideEffectImports": true,
"allowUnreachableCode": true "allowUnreachableCode": true
}, },
"include": ["src"], "include": [
"src",
"../../modules/core/src/web/hooks/use-datasource/use-datasource.tsx",
"../../modules/core/src/web/hooks/use-datasource/datasource.interface.ts"
],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }

View File

@ -5,11 +5,13 @@
"types": "src/index.ts", "types": "src/index.ts",
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
"./dto": "./src/dto/index.ts",
"./hooks": "./src/web/hooks/index.ts", "./hooks": "./src/web/hooks/index.ts",
"./components": "./src/web/components/index.tsx", "./components": "./src/web/components/index.tsx",
"./components/*": "./src/web/components/*.tsx" "./components/*": "./src/web/components/*.tsx"
}, },
"peerDependencies": { "peerDependencies": {
"joi": "^17.13.3",
"react": "^18 || ^19", "react": "^18 || ^19",
"react-dom": "^18 || ^19", "react-dom": "^18 || ^19",
"sequelize": "^6.37.5" "sequelize": "^6.37.5"
@ -17,13 +19,18 @@
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.4", "@biomejs/biome": "1.9.4",
"@testing-library/react-hooks": "^8.0.1", "@testing-library/react-hooks": "^8.0.1",
"@types/axios": "^0.14.4",
"@types/jest": "29.5.14", "@types/jest": "29.5.14",
"@types/react": "^19.1.2", "@types/react": "^19.1.2",
"@types/react-dom": "^19.1.3", "@types/react-dom": "^19.1.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },
"dependencies": { "dependencies": {
"@repo/rdx-utils": "workspace:*",
"@repo/rdx-criteria": "workspace:*", "@repo/rdx-criteria": "workspace:*",
"@tanstack/react-query": "^5.75.4",
"axios": "^1.9.0",
"joi": "^17.13.3",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-router-dom": "^6.26.0", "react-router-dom": "^6.26.0",

View File

@ -1,19 +1,15 @@
export interface ListResponseDTO<T> { export interface IListResponseDTO<T> {
page: number; page: number;
perpage: number; perpage: number;
totalpages: number; total_pages: number;
totalitems: number; total_items: number;
items: T[]; items: T[];
} }
export const IsResponseAListDTO = <T>(response: any): response is ListResponseDTO<T> => { export const isResponseAListDTO = <T>(data: any): data is IListResponseDTO<T> => {
return ( return data && typeof data.total_items === "number";
typeof response === "object" &&
response !== null &&
Object.prototype.hasOwnProperty.call(response, "totalitems")
);
}; };
export const existsMoreReponsePages = <T>(response: any): response is ListResponseDTO<T> => { export const existsMoreReponsePages = <T>(response: any): response is IListResponseDTO<T> => {
return IsResponseAListDTO(response) && response.page + 1 < response.totalpages; return isResponseAListDTO(response) && response.page + 1 < response.total_pages;
}; };

View File

@ -1,5 +1,5 @@
import { Result, RuleValidator } from "@repo/rdx-utils";
import Joi from "joi"; import Joi from "joi";
import { Result, RuleValidator } from "../../domain";
export interface IMoneyDTO { export interface IMoneyDTO {
amount: number | null; amount: number | null;

View File

@ -1,5 +1,5 @@
import { Result, RuleValidator } from "@repo/rdx-utils";
import Joi from "joi"; import Joi from "joi";
import { Result, RuleValidator } from "../../domain";
export interface IQuantityDTO { export interface IQuantityDTO {
amount: number | null; amount: number | null;

View File

@ -1,2 +1,4 @@
export * from "./use-datasource";
export * from "./use-pagination"; export * from "./use-pagination";
export * from "./use-query-key";
export * from "./use-toggle"; export * from "./use-toggle";

View File

@ -1,4 +1,4 @@
import type { ListResponseDTO } from "@erp/core"; import type { IListResponseDTO } from "@erp/core";
import { type AxiosHeaderValue, type ResponseType } from "axios"; import { type AxiosHeaderValue, type ResponseType } from "axios";
export interface IPaginationDataProviderParam { export interface IPaginationDataProviderParam {
@ -87,7 +87,7 @@ export interface ICustomDataProviderParam {
export interface IDataSource { export interface IDataSource {
name: () => string; name: () => string;
getList: <R>(params: IGetListDataProviderParams) => Promise<ListResponseDTO<R>>; getList: <R>(params: IGetListDataProviderParams) => Promise<IListResponseDTO<R>>;
getOne: <R>(params: IGetOneDataProviderParams) => Promise<R>; getOne: <R>(params: IGetOneDataProviderParams) => Promise<R>;
//saveOne: <P, R>(params: ISaveOneDataProviderParams<P>) => Promise<R>; //saveOne: <P, R>(params: ISaveOneDataProviderParams<P>) => Promise<R>;
createOne: <P, R>(params: ICreateOneDataProviderParams<P>) => Promise<R>; createOne: <P, R>(params: ICreateOneDataProviderParams<P>) => Promise<R>;

View File

@ -0,0 +1,3 @@
export * from "./datasource.interface";
export * from "./use-datasource";
export * from "./use-list";

View File

@ -0,0 +1,19 @@
import { type PropsWithChildren, createContext, useContext } from "react";
import { IDataSource } from "./datasource.interface";
export const DataSourceContext = createContext<IDataSource | undefined>(undefined);
export const DataSourceProvider = ({
dataSource,
children,
}: PropsWithChildren<{
dataSource: IDataSource;
}>) => <DataSourceContext.Provider value={dataSource}>{children}</DataSourceContext.Provider>;
export const useDataSource = () => {
const context = useContext(DataSourceContext);
if (context === undefined)
throw new Error("useDataSource must be used within a DataSourceProvider");
return context;
};

View File

@ -0,0 +1,87 @@
import {
QueryFunctionContext,
QueryKey,
UseQueryOptions,
UseQueryResult,
keepPreviousData,
useQuery,
} from "@tanstack/react-query";
import { isResponseAListDTO } from "@erp/core/dto";
import {
UseLoadingOvertimeOptionsProps,
UseLoadingOvertimeReturnType,
useLoadingOvertime,
} from "./use-loading-overtime";
const DEFAULT_REFETCH_INTERVAL = 2 * 60 * 1000; // 2 minutes
const DEFAULT_STALE_TIME = 60 * 1000; // 1 minute
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export type UseListQueryOptions<TUseListQueryData, TUseListQueryError> = {
queryKey: QueryKey;
queryFn: (context: QueryFunctionContext) => Promise<TUseListQueryData>;
enabled?: boolean;
refetchInterval?: number | false;
select?: (data: TUseListQueryData) => TUseListQueryData;
queryOptions?: Partial<UseQueryOptions<TUseListQueryData, TUseListQueryError>>;
} & UseLoadingOvertimeOptionsProps;
export type UseListQueryResult<TUseListQueryData, TUseListQueryError> = UseQueryResult<
TUseListQueryData,
TUseListQueryError
> & {
isEmpty: boolean;
} & UseLoadingOvertimeReturnType;
/**
* Hook para manejar consultas de listas con React Query,
* incluye detección de listas vacías y control de sobretiempo de carga.
*/
export const useList = <TUseListQueryData, TUseListQueryError>({
queryKey,
queryFn,
enabled,
refetchInterval,
select,
queryOptions = {},
overtimeOptions,
}: UseListQueryOptions<TUseListQueryData, TUseListQueryError>): UseListQueryResult<
TUseListQueryData,
TUseListQueryError
> => {
if (!queryFn) {
console.error("queryFn es requerido en useList");
throw new Error("queryFn es requerido en useList");
}
const queryResponse = useQuery<TUseListQueryData, TUseListQueryError>({
queryKey,
queryFn,
placeholderData: keepPreviousData,
staleTime: DEFAULT_STALE_TIME,
refetchInterval: refetchInterval ?? DEFAULT_REFETCH_INTERVAL,
refetchOnWindowFocus: true,
enabled: enabled && !!queryFn,
select,
...queryOptions,
});
const { elapsedTime } = useLoadingOvertime({
isPending: queryResponse.isFetching,
interval: overtimeOptions?.interval,
onInterval: overtimeOptions?.onInterval,
});
const isEmpty =
queryResponse.isSuccess &&
isResponseAListDTO(queryResponse.data) &&
queryResponse.data.total_items === 0;
const result = {
...queryResponse,
overtime: { elapsedTime },
isEmpty,
};
return result;
};

View File

@ -0,0 +1,101 @@
import { useEffect, useState } from "react";
export type UseLoadingOvertimeRefineContext = Omit<
UseLoadingOvertimeCoreProps,
"isPending" | "interval"
> &
Required<Pick<UseLoadingOvertimeCoreProps, "interval">>;
export type UseLoadingOvertimeOptionsProps = {
overtimeOptions?: UseLoadingOvertimeCoreOptions;
};
export type UseLoadingOvertimeReturnType = {
overtime: {
elapsedTime?: number;
};
};
type UseLoadingOvertimeCoreOptions = Omit<UseLoadingOvertimeCoreProps, "isPending">;
type UseLoadingOvertimeCoreReturnType = {
elapsedTime?: number;
};
export type UseLoadingOvertimeCoreProps = {
/**
* The pengind state. If true, the elapsed time will be calculated.
*/
isPending: boolean;
/**
* The interval in milliseconds. If the pending time exceeds this time, the `onInterval` callback will be called.
* If not specified, the `interval` value from the `overtime` option of the `RefineProvider` will be used.
*
* @default: 1000 (1 second)
*/
interval?: number;
/**
* The callback function that will be called when the pending time exceeds the specified time.
* If not specified, the `onInterval` value from the `overtime` option of the `RefineProvider` will be used.
*
* @param elapsedInterval The elapsed time in milliseconds.
*/
onInterval?: (elapsedInterval: number) => void;
};
/**
* if you need to do something when the loading time exceeds the specified time, refine provides the `useLoadingOvertime` hook.
* It returns the elapsed time in milliseconds.
*
* @example
* const { elapsedTime } = useLoadingOvertime({
* isLoading,
* interval: 1000,
* onInterval(elapsedInterval) {
* console.log("loading overtime", elapsedInterval);
* },
* });
*/
export const useLoadingOvertime = ({
isPending,
interval = 1000,
onInterval,
}: UseLoadingOvertimeCoreProps): UseLoadingOvertimeCoreReturnType => {
const [elapsedTime, setElapsedTime] = useState<number | undefined>(undefined);
useEffect(() => {
let intervalFn: ReturnType<typeof setInterval>;
if (isPending) {
intervalFn = setInterval(() => {
// increase elapsed time
setElapsedTime((prevElapsedTime) => {
if (prevElapsedTime === undefined) {
return interval;
}
return prevElapsedTime + interval;
});
}, interval);
}
return () => {
clearInterval(intervalFn);
// reset elapsed time
setElapsedTime(undefined);
};
}, [isPending, interval]);
useEffect(() => {
// call onInterval callback
if (onInterval && elapsedTime) {
onInterval(elapsedTime);
}
}, [onInterval, elapsedTime]);
return {
elapsedTime,
};
};

View File

@ -0,0 +1,5 @@
import { keys } from "./key-builder";
export const useQueryKey = () => {
return keys;
};

View File

@ -0,0 +1,181 @@
type BaseKey = string | number;
type ParametrizedDataActions = "list" | "infinite";
type IdRequiredDataActions = "one" | "report" | "upload";
type IdsRequiredDataActions = "many";
type DataMutationActions =
| "custom"
| "customMutation"
| "create"
| "createMany"
| "update"
| "updateMany"
| "delete"
| "deleteMany";
type AuthActionType =
| "login"
| "logout"
| "profile"
| "register"
| "forgotPassword"
| "check"
| "onError"
| "permissions"
| "updatePassword";
type AuditActionType = "list" | "log" | "rename";
type IdType = BaseKey;
type IdsType = IdType[];
type ParamsType = any;
type KeySegment = string | IdType | IdsType | ParamsType;
export function arrayFindIndex<T>(array: T[], slice: T[]): number {
return array.findIndex(
(_, index) =>
index <= array.length - slice.length &&
slice.every((sliceItem, sliceIndex) => array[index + sliceIndex] === sliceItem)
);
}
export function arrayReplace<T>(array: T[], partToBeReplaced: T[], newPart: T[]): T[] {
const newArray: T[] = [...array];
const startIndex = arrayFindIndex(array, partToBeReplaced);
if (startIndex !== -1) {
newArray.splice(startIndex, partToBeReplaced.length, ...newPart);
}
return newArray;
}
export function stripUndefined(segments: KeySegment[]) {
return segments.filter((segment) => segment !== undefined);
}
class BaseKeyBuilder {
segments: KeySegment[] = [];
constructor(segments: KeySegment[] = []) {
this.segments = segments;
}
key() {
return this.segments;
}
get() {
return this.segments;
}
}
class ParamsKeyBuilder extends BaseKeyBuilder {
params(paramsValue?: ParamsType) {
return new BaseKeyBuilder([...this.segments, paramsValue]);
}
}
class DataIdRequiringKeyBuilder extends BaseKeyBuilder {
id(idValue?: IdType) {
return new ParamsKeyBuilder([...this.segments, idValue ? String(idValue) : undefined]);
}
}
class DataIdsRequiringKeyBuilder extends BaseKeyBuilder {
ids(...idsValue: IdsType) {
return new ParamsKeyBuilder([
...this.segments,
...(idsValue.length ? [idsValue.map((el) => String(el))] : []),
]);
}
}
class DataResourceKeyBuilder extends BaseKeyBuilder {
action(actionType: ParametrizedDataActions): ParamsKeyBuilder;
action(actionType: IdRequiredDataActions): DataIdRequiringKeyBuilder;
action(actionType: IdsRequiredDataActions): DataIdsRequiringKeyBuilder;
action(
actionType: ParametrizedDataActions | IdRequiredDataActions | IdsRequiredDataActions
): ParamsKeyBuilder | DataIdRequiringKeyBuilder | DataIdsRequiringKeyBuilder {
if (["one", "report"].includes(actionType)) {
return new DataIdRequiringKeyBuilder([...this.segments, actionType]);
}
if (actionType === "many") {
return new DataIdsRequiringKeyBuilder([...this.segments, actionType]);
}
if (["list", "infinite"].includes(actionType)) {
return new ParamsKeyBuilder([...this.segments, actionType]);
}
throw new Error("Invalid action type");
}
}
class DataKeyBuilder extends BaseKeyBuilder {
resource(resourceName?: string) {
return new DataResourceKeyBuilder([...this.segments, resourceName]);
}
mutation(mutationName: DataMutationActions) {
return new ParamsKeyBuilder([
...(mutationName === "custom" ? this.segments : [this.segments[0]]),
mutationName,
]);
}
}
class AuthKeyBuilder extends BaseKeyBuilder {
action(actionType: AuthActionType) {
return new ParamsKeyBuilder([...this.segments, actionType]);
}
}
class AccessResourceKeyBuilder extends BaseKeyBuilder {
action(resourceName: string) {
return new ParamsKeyBuilder([...this.segments, resourceName]);
}
}
class AccessKeyBuilder extends BaseKeyBuilder {
resource(resourceName?: string) {
return new AccessResourceKeyBuilder([...this.segments, resourceName]);
}
}
class AuditActionKeyBuilder extends BaseKeyBuilder {
action(actionType: Extract<AuditActionType, "list">) {
return new ParamsKeyBuilder([...this.segments, actionType]);
}
}
class AuditKeyBuilder extends BaseKeyBuilder {
resource(resourceName?: string) {
return new AuditActionKeyBuilder([...this.segments, resourceName]);
}
action(actionType: Extract<AuditActionType, "rename" | "log">) {
return new ParamsKeyBuilder([...this.segments, actionType]);
}
}
export class KeyBuilder extends BaseKeyBuilder {
data(name?: string) {
return new DataKeyBuilder(["data", name || "default"]);
}
auth() {
return new AuthKeyBuilder(["auth"]);
}
access() {
return new AccessKeyBuilder(["access"]);
}
audit() {
return new AuditKeyBuilder(["audit"]);
}
}
export const keys = () => new KeyBuilder([]);

View File

@ -8,14 +8,17 @@
"./api": "./src/api/index.ts", "./api": "./src/api/index.ts",
"./dto": "./src/common/dto/index.ts", "./dto": "./src/common/dto/index.ts",
"./manifest": "./src/web/manifest.ts", "./manifest": "./src/web/manifest.ts",
"./hooks/*": ["./src/web/hooks/*.tsx", "./src/hooks/*.ts"], "./hooks/*": [
"./src/web/hooks/*.tsx",
"./src/hooks/*.ts"
],
"./components": "./src/web/components/index.tsx", "./components": "./src/web/components/index.tsx",
"./components/*": "./src/web/components/*.tsx", "./components/*": "./src/web/components/*.tsx",
"./locales": "./src/common/locales/index.tsx" "./locales": "./src/common/locales/index.tsx"
}, },
"peerDependencies": { "peerDependencies": {
"ag-grid-react": "^33.3.0",
"ag-grid-community": "^33.3.0", "ag-grid-community": "^33.3.0",
"ag-grid-react": "^33.3.0",
"i18next": "^25.1.1", "i18next": "^25.1.1",
"react": "^18 || ^19", "react": "^18 || ^19",
"react-dom": "^18 || ^19", "react-dom": "^18 || ^19",
@ -38,6 +41,8 @@
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-i18next": "^15.5.1", "react-i18next": "^15.5.1",
"react-router-dom": "^6.26.0" "react-router-dom": "^6.26.0",
"slugify": "^1.6.6",
"zod": "^3.24.4"
} }
} }

View File

@ -11,7 +11,7 @@ import {
import { ITransactionManager } from "@/core/common/infrastructure/database"; import { ITransactionManager } from "@/core/common/infrastructure/database";
import { logger } from "@/core/logger"; import { logger } from "@/core/logger";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { ICreateInvoiceRequestDTO } from "../presentation/dto"; import { ICreateInvoiceRequestDTO } from "../../common/dto";
export class CreateInvoiceUseCase { export class CreateInvoiceUseCase {
constructor( constructor(

View File

@ -1,8 +1,8 @@
import { UniqueID } from "@/core/common/domain"; import { UniqueID } from "@/core/common/domain";
import { ITransactionManager } from "@/core/common/infrastructure/database"; import { ITransactionManager } from "@/core/common/infrastructure/database";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { IUpdateInvoiceRequestDTO } from "../../common/dto";
import { IInvoiceService, Invoice } from "../domain"; import { IInvoiceService, Invoice } from "../domain";
import { IUpdateInvoiceRequestDTO } from "../presentation/dto";
export class CreateInvoiceUseCase { export class CreateInvoiceUseCase {
constructor( constructor(

View File

@ -1,6 +1,6 @@
import { CreateInvoiceUseCase } from "@/contexts/invoices/application/create-invoice.use-case"; import { CreateInvoiceUseCase } from "@/contexts/invoices/application/create-invoice.use-case";
import { ExpressController, UniqueID } from "@/core"; import { ExpressController, UniqueID } from "@/core";
import { ICreateInvoiceRequestDTO } from "../../dto"; import { ICreateInvoiceRequestDTO } from "../../../../common/dto";
import { ICreateInvoicePresenter } from "./presenter"; import { ICreateInvoicePresenter } from "./presenter";
export class CreateInvoiceController extends ExpressController { export class CreateInvoiceController extends ExpressController {

View File

@ -1,5 +1,5 @@
import { Invoice } from "@/contexts/invoices/domain"; import { Invoice } from "@/contexts/invoices/domain";
import { ICreateInvoiceResponseDTO } from "../../../dto"; import { ICreateInvoiceResponseDTO } from "../../../../../common/dto";
export interface ICreateInvoicePresenter { export interface ICreateInvoicePresenter {
toDTO: (invoice: Invoice) => ICreateInvoiceResponseDTO; toDTO: (invoice: Invoice) => ICreateInvoiceResponseDTO;

View File

@ -1,5 +1,5 @@
import { IGetInvoiceResponseDTO } from "../../../../../common/dto";
import { Invoice, InvoiceItem } from "../../../../domain"; import { Invoice, InvoiceItem } from "../../../../domain";
import { IGetInvoiceResponseDTO } from "../../../dto";
export interface IGetInvoicePresenter { export interface IGetInvoicePresenter {
toDTO: (invoice: Invoice) => IGetInvoiceResponseDTO; toDTO: (invoice: Invoice) => IGetInvoiceResponseDTO;

View File

@ -1,6 +1,6 @@
import { Invoice } from "@/contexts/invoices/domain"; import { Invoice } from "@/contexts/invoices/domain";
import { Collection } from "@/core"; import { Collection } from "@/core";
import { IListInvoicesResponseDTO } from "../../../dto"; import { IListInvoicesResponseDTO } from "../../../../../common/dto";
export interface IListInvoicesPresenter { export interface IListInvoicesPresenter {
toDTO: (invoices: Collection<Invoice>) => IListInvoicesResponseDTO[]; toDTO: (invoices: Collection<Invoice>) => IListInvoicesResponseDTO[];

View File

@ -1,2 +1 @@
export * from "./controllers"; export * from "./controllers";
export * from "./dto";

View File

@ -1,4 +1,4 @@
import { IMoneyDTO, IQuantityDTO } from "@/core/common/presentation"; import { IMoneyDTO, IQuantityDTO } from "@erp/core";
export interface IListInvoicesResponseDTO { export interface IListInvoicesResponseDTO {
id: string; id: string;

View File

@ -1,15 +1,10 @@
import { PropsWithChildren } from "react"; import { PropsWithChildren } from "react";
import { SectionCards } from "@repo/rdx-ui/components/layout/section-cards";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { InvoicesProvider } from "../hooks";
export const InvoicesLayout = ({ children }: PropsWithChildren) => { export const InvoicesLayout = ({ children }: PropsWithChildren) => {
const { t } = useTranslation("invoices"); const { t } = useTranslation("invoices");
return ( return <InvoicesProvider>{children}</InvoicesProvider>;
<>
<SectionCards />
{children}
</>
);
}; };

View File

@ -15,7 +15,7 @@ export const InvoicesProvider = ({ children }: PropsWithChildren) => {
setPagination, setPagination,
}} }}
> >
{children} <div className='invoices-layout'>{children}</div>
</InvoicesContext.Provider> </InvoicesContext.Provider>
); );
}; };

View File

@ -0,0 +1,82 @@
import { IListResponseDTO } from "@erp/core";
import {
IGetListDataProviderParams,
UseListQueryResult,
useDataSource,
useList,
useQueryKey,
} from "@erp/core/hooks";
import { IListInvoicesResponseDTO } from "@erp/invoices/common/dto";
export type UseInvoicesListParams = Omit<IGetListDataProviderParams, "filters" | "resource"> & {
status?: string;
enabled?: boolean;
queryOptions?: Record<string, unknown>;
};
export type UseInvoicesListResponse = UseListQueryResult<
IListResponseDTO<IListInvoicesResponseDTO>,
unknown
>;
export type UseInvoicesGetParamsType = {
enabled?: boolean;
queryOptions?: Record<string, unknown>;
};
export type UseInvoicesReportParamsType = {
enabled?: boolean;
queryOptions?: Record<string, unknown>;
};
export const useInvoices = () => {
const actions = {
/**
* Hook para obtener la lista de facturas
* @param params - Parámetros para la consulta de la lista de facturas
* @returns - Respuesta de la consulta de la lista de facturas
*/
useList: (params: UseInvoicesListParams): UseInvoicesListResponse => {
const dataSource = useDataSource();
const keys = useQueryKey();
const {
pagination,
status = "draft",
quickSearchTerm = undefined,
enabled = true,
queryOptions,
} = params;
return useList({
queryKey: keys().data().resource("invoices").action("list").params(params).get(),
queryFn: () => {
return dataSource.getList({
resource: "invoices",
quickSearchTerm,
filters:
status !== "all"
? [
{
field: "status",
operator: "eq",
value: status,
},
]
: [
{
field: "status",
operator: "ne",
value: "archived",
},
],
pagination,
});
},
enabled,
queryOptions,
});
},
};
return actions;
};

View File

@ -1 +1 @@
export * from "./invoices-list"; export * from "./list";

View File

@ -1,5 +1,5 @@
import { Result, UndefinedOr } from "@repo/rdx-utils"; import { Result, ResultCollection, RuleValidator, UndefinedOr } from "@repo/rdx-utils";
import { ResultCollection, RuleValidator, StringValueObject } from "../helpers"; import { StringValueObject } from "../helpers";
import { Filter, IFilter } from "./Filter"; import { Filter, IFilter } from "./Filter";
export interface IFilterCriteria { export interface IFilterCriteria {

View File

@ -1,7 +1,6 @@
import { ValueObject } from "@repo/rdx-ddd"; import { ValueObject } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result, RuleValidator } from "@repo/rdx-utils";
import Joi from "joi"; import Joi from "joi";
import { RuleValidator } from "../helpers";
export interface IOrderProps { export interface IOrderProps {
type: string; type: string;

View File

@ -1,5 +1,5 @@
import { Result, UndefinedOr } from "@repo/rdx-utils"; import { Result, ResultCollection, RuleValidator, UndefinedOr } from "@repo/rdx-utils";
import { ResultCollection, RuleValidator, StringValueObject } from "../helpers"; import { StringValueObject } from "../helpers";
import { IOrderProps, Order } from "./Order"; import { IOrderProps, Order } from "./Order";
import { IOrderCollection, OrderCollection } from "./OrderCollection"; import { IOrderCollection, OrderCollection } from "./OrderCollection";

View File

@ -1,6 +1,5 @@
import { DomainError } from "../../../errors"; import { ValueObject } from "@repo/rdx-ddd";
import { Result } from "../../Result"; import { Result } from "@repo/rdx-utils";
import { ValueObject } from "../../ValueObject";
import { Order } from "./Order"; import { Order } from "./Order";
import { OrderCollection } from "./OrderCollection"; import { OrderCollection } from "./OrderCollection";
@ -17,19 +16,16 @@ export interface IOrderRoot {
toObject(): Record<string, any>; toObject(): Record<string, any>;
} }
export class OrderRoot export class OrderRoot extends ValueObject<IOrderRootProps> implements IOrderRoot {
extends ValueObject<IOrderRootProps>
implements IOrderRoot
{
public static createASC(value: Order[]): Result<IOrderRoot> { public static createASC(value: Order[]): Result<IOrderRoot> {
return this.create({ return OrderRoot.create({
type: "asc", type: "asc",
value, value,
}); });
} }
public static createDESC(value: Order[]): Result<IOrderRoot> { public static createDESC(value: Order[]): Result<IOrderRoot> {
return this.create({ return OrderRoot.create({
type: "desc", type: "desc",
value, value,
}); });
@ -37,7 +33,7 @@ export class OrderRoot
public static create(orderRootProps: any): Result<IOrderRoot> { public static create(orderRootProps: any): Result<IOrderRoot> {
// Validación de props // Validación de props
const valid = this.validate(orderRootProps); const valid = OrderRoot.validate(orderRootProps);
if (valid.isFailure) { if (valid.isFailure) {
return Result.fail(valid.error); return Result.fail(valid.error);
} }
@ -51,7 +47,7 @@ export class OrderRoot
} }
protected static validate(OrderRootRoot: IOrderRootProps): Result<any> { protected static validate(OrderRootRoot: IOrderRootProps): Result<any> {
throw DomainError.create("NOT IMPLEMENT", "OrderRoot.validate()"); throw new Error("NOT IMPLEMENT OrderRoot.validate()");
/*return Validator.isOneOf( /*return Validator.isOneOf(
{ {
@ -75,6 +71,10 @@ export class OrderRoot
this._items = props.items; this._items = props.items;
} }
getValue() {
return this.props;
}
get type(): string { get type(): string {
return this._type; return this._type;
} }

View File

@ -1,8 +1,7 @@
import Joi from "joi"; import Joi from "joi";
import { ValueObject } from "@repo/rdx-ddd"; import { ValueObject } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result, RuleValidator } from "@repo/rdx-utils";
import { RuleValidator } from "../helpers";
import { import {
INITIAL_PAGE_INDEX, INITIAL_PAGE_INDEX,
INITIAL_PAGE_SIZE, INITIAL_PAGE_SIZE,

View File

@ -1,7 +1,6 @@
import { ValueObject } from "@repo/rdx-ddd"; import { ValueObject } from "@repo/rdx-ddd";
import { Result, UndefinedOr } from "@repo/rdx-utils"; import { Result, RuleValidator, UndefinedOr } from "@repo/rdx-utils";
import Joi from "joi"; import Joi from "joi";
import { RuleValidator } from "../helpers";
export interface IQuickSearchCriteria { export interface IQuickSearchCriteria {
searchTerms: string[]; searchTerms: string[];

View File

@ -1,3 +1 @@
export * from "./RuleValidator";
export * from "./ResultCollection";
export * from "./StringValueObject"; export * from "./StringValueObject";

View File

@ -13,5 +13,11 @@
"@repo/typescript-config": "workspace:*", "@repo/typescript-config": "workspace:*",
"@types/node": "^22.15.12", "@types/node": "^22.15.12",
"typescript": "^5.8.3" "typescript": "^5.8.3"
},
"dependencies": {
"joi": "^17.13.3"
},
"peerDependencies": {
"joi": "^17.13.3"
} }
} }

View File

@ -1,4 +1,6 @@
export * from "./collection"; export * from "./collection";
export * from "./maybe"; export * from "./maybe";
export * from "./result"; export * from "./result";
export * from "./result-collection";
export * from "./rule-validator";
export * from "./utils"; export * from "./utils";

View File

@ -1,4 +1,4 @@
import { Result } from "@repo/rdx-utils"; import { Result } from "./result";
export interface IResultCollection<T, E extends Error = Error> { export interface IResultCollection<T, E extends Error = Error> {
add(result: Result<T, E>): void; add(result: Result<T, E>): void;

View File

@ -1,5 +1,5 @@
import { Result } from "@repo/rdx-utils";
import Joi, { ValidationError } from "joi"; import Joi, { ValidationError } from "joi";
import { Result } from "./result";
export type TRuleValidatorResult<T> = Result<T, ValidationError>; export type TRuleValidatorResult<T> = Result<T, ValidationError>;

View File

@ -110,6 +110,7 @@ function SidebarProvider({
// This makes it easier to style the sidebar with Tailwind classes. // This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed"; const state = open ? "expanded" : "collapsed";
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
const contextValue = React.useMemo<SidebarContextProps>( const contextValue = React.useMemo<SidebarContextProps>(
() => ({ () => ({
state, state,

View File

@ -338,6 +338,18 @@ importers:
'@repo/rdx-criteria': '@repo/rdx-criteria':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/rdx-criteria version: link:../../packages/rdx-criteria
'@repo/rdx-utils':
specifier: workspace:*
version: link:../../packages/rdx-utils
'@tanstack/react-query':
specifier: ^5.75.4
version: 5.75.4(react@19.1.0)
axios:
specifier: ^1.9.0
version: 1.9.0
joi:
specifier: ^17.13.3
version: 17.13.3
react: react:
specifier: ^19.1.0 specifier: ^19.1.0
version: 19.1.0 version: 19.1.0
@ -357,6 +369,9 @@ importers:
'@testing-library/react-hooks': '@testing-library/react-hooks':
specifier: ^8.0.1 specifier: ^8.0.1
version: 8.0.1(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 8.0.1(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@types/axios':
specifier: ^0.14.4
version: 0.14.4
'@types/jest': '@types/jest':
specifier: 29.5.14 specifier: 29.5.14
version: 29.5.14 version: 29.5.14
@ -405,6 +420,12 @@ importers:
react-router-dom: react-router-dom:
specifier: ^6.26.0 specifier: ^6.26.0
version: 6.30.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 6.30.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
slugify:
specifier: ^1.6.6
version: 1.6.6
zod:
specifier: ^3.24.4
version: 3.24.4
devDependencies: devDependencies:
'@biomejs/biome': '@biomejs/biome':
specifier: 1.9.4 specifier: 1.9.4
@ -570,6 +591,10 @@ importers:
version: 5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.12)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.29.2)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1)) version: 5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.12)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.29.2)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1))
packages/rdx-utils: packages/rdx-utils:
dependencies:
joi:
specifier: ^17.13.3
version: 17.13.3
devDependencies: devDependencies:
'@repo/typescript-config': '@repo/typescript-config':
specifier: workspace:* specifier: workspace:*
@ -2611,6 +2636,10 @@ packages:
resolution: {integrity: sha512-NxPRAT/mywJ6agqLuVsOag1btEUbPYacVqCndQjvkm5EN0DfjvBIYCsXA/i2Q+Z0hqX84UeIIfIXAQiXpAXZmA==} resolution: {integrity: sha512-NxPRAT/mywJ6agqLuVsOag1btEUbPYacVqCndQjvkm5EN0DfjvBIYCsXA/i2Q+Z0hqX84UeIIfIXAQiXpAXZmA==}
hasBin: true hasBin: true
'@types/axios@0.14.4':
resolution: {integrity: sha512-9JgOaunvQdsQ/qW2OPmE5+hCeUB52lQSolecrFrthct55QekhmXEwT203s20RL+UHtCQc15y3VXpby9E7Kkh/g==}
deprecated: This is a stub types definition. axios provides its own type definitions, so you do not need this installed.
'@types/babel__core@7.20.5': '@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
@ -5468,6 +5497,10 @@ packages:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'} engines: {node: '>=8'}
slugify@1.6.6:
resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==}
engines: {node: '>=8.0.0'}
smart-buffer@4.2.0: smart-buffer@4.2.0:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
@ -8028,6 +8061,12 @@ snapshots:
semver: 7.6.2 semver: 7.6.2
update-check: 1.5.4 update-check: 1.5.4
'@types/axios@0.14.4':
dependencies:
axios: 1.9.0
transitivePeerDependencies:
- debug
'@types/babel__core@7.20.5': '@types/babel__core@7.20.5':
dependencies: dependencies:
'@babel/parser': 7.27.1 '@babel/parser': 7.27.1
@ -11136,6 +11175,8 @@ snapshots:
slash@3.0.0: {} slash@3.0.0: {}
slugify@1.6.6: {}
smart-buffer@4.2.0: {} smart-buffer@4.2.0: {}
snake-case@2.1.0: snake-case@2.1.0: