This commit is contained in:
David Arranz 2025-05-27 19:47:03 +02:00
parent e8352ae24e
commit eabd63ec09
94 changed files with 1638 additions and 1006 deletions

View File

@ -0,0 +1 @@
VITE_API_SERVER_URL=http://192.168.0.104:3002/api/v1

View File

@ -8,6 +8,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="https://fonts.upset.dev/css2?family=Poppins&display=swap" rel="stylesheet" /> <link href="https://fonts.upset.dev/css2?family=Poppins&display=swap" rel="stylesheet" />
<title>FactuGES 2025</title> <title>FactuGES 2025</title>
<!-- FAVICONS -->
</head> </head>
<body> <body>

View File

@ -12,8 +12,26 @@
"lint": "biome lint --fix", "lint": "biome lint --fix",
"format": "biome format --write" "format": "biome format --write"
}, },
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@hookform/devtools": "^4.4.0",
"@peterek/vite-plugin-favicons": "^2.1.0",
"@repo/typescript-config": "workspace:*",
"@tailwindcss/postcss": "^4.1.5",
"@tailwindcss/vite": "^4.1.6",
"@tanstack/react-query-devtools": "^5.74.11",
"@types/node": "^22.15.12",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.3",
"@vitejs/plugin-react": "^4.4.1",
"autoprefixer": "^10.4.20",
"globals": "^16.0.0",
"typescript": "~5.8.3",
"vite": "^6.3.5"
},
"dependencies": { "dependencies": {
"@erp/core": "workspace:*", "@erp/core": "workspace:*",
"@erp/auth": "workspace:*",
"@erp/invoices": "workspace:*", "@erp/invoices": "workspace:*",
"@repo/rdx-criteria": "workspace:*", "@repo/rdx-criteria": "workspace:*",
"@repo/rdx-ui": "workspace:*", "@repo/rdx-ui": "workspace:*",
@ -28,28 +46,10 @@
"react-hook-form-persist": "^3.0.0", "react-hook-form-persist": "^3.0.0",
"react-i18next": "^15.0.1", "react-i18next": "^15.0.1",
"react-router-dom": "^6.26.0", "react-router-dom": "^6.26.0",
"react-secure-storage": "^1.3.2", "sequelize": "^6.37.5",
"tailwind-merge": "^3.2.0", "tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.6", "tailwindcss": "^4.1.6",
"tw-animate-css": "^1.2.9", "tw-animate-css": "^1.2.9",
"vite-plugin-html": "^3.2.2" "vite-plugin-html": "^3.2.2"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@hookform/devtools": "^4.4.0",
"@repo/typescript-config": "workspace:*",
"@tailwindcss/postcss": "^4.1.5",
"@tailwindcss/vite": "^4.1.6",
"@tanstack/react-query-devtools": "^5.74.11",
"@types/node": "^22.15.12",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.3",
"@vitejs/plugin-react": "^4.4.1",
"autoprefixer": "^10.4.20",
"globals": "^16.0.0",
"typescript": "~5.8.3",
"vite": "^6.3.5",
"vite-plugin-robots": "^1.0.5",
"vite-plugin-static-copy": "^2.3.1"
} }
} }

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -3,14 +3,19 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 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 { UnsavedWarnProvider } from "@/lib/hooks"; import { UnsavedWarnProvider } from "@/lib/hooks";
import { i18n } from "@/locales"; import { i18n } from "@/locales";
import { DataSourceProvider } from "@erp/core/hooks"; import { clearAccessToken, getAccessToken } from "@erp/auth/client";
import { DataSourceProvider, createAxiosDataSource, createAxiosInstance } from "@erp/core/client";
import { AppRoutes } from "./app-routes"; import { AppRoutes } from "./app-routes";
import "./app.css"; import "./app.css";
/**
* Clave utilizada en el almacenamiento local para el token JWT.
*/
const TOKEN_STORAGE_KEY = "factuges.auth";
export const App = () => { export const App = () => {
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
@ -21,17 +26,26 @@ export const App = () => {
}, },
}); });
const dataSource = createAxiosDataSource(createAxiosInstance({
baseURL: import.meta.env.VITE_API_URL,
getAccessToken: () => getAccessToken(TOKEN_STORAGE_KEY),
onAuthError: () => {
clearAccessToken(TOKEN_STORAGE_KEY);
window.location.href = "/login"; // o usar navegación programática
},
}););
return ( return (
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<DataSourceProvider dataSource={createAxiosDataProvider(import.meta.env.VITE_API_URL)}> <DataSourceProvider dataSource={dataSource}>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<UnsavedWarnProvider> <UnsavedWarnProvider>
<AppRoutes /> <AppRoutes />
</UnsavedWarnProvider> </UnsavedWarnProvider>
</TooltipProvider> </TooltipProvider>
<Toaster /> <Toaster />
{import.meta.env.MODE === "development" && <ReactQueryDevtools initialIsOpen={false} />} {import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
</DataSourceProvider> </DataSourceProvider>
</QueryClientProvider> </QueryClientProvider>
</I18nextProvider> </I18nextProvider>

View File

@ -1,8 +0,0 @@
import type { ILoginResponseDTO } from "@erp/auth";
import secureLocalStorage from "react-secure-storage";
export const getApiAuthorization = () => {
const authInfo = secureLocalStorage.getItem("uecko.auth") as ILoginResponseDTO;
return authInfo?.token ? `Bearer ${authInfo.token}` : "";
};

View File

@ -1 +0,0 @@
export * from "./get-api-authorization";

View File

@ -1,48 +0,0 @@
import axiosClient from "axios";
import { setupInterceptorsTo } from "./setup-interceptors";
// extend the AxiosRequestConfig interface and add two optional options raw and silent. I
// https://dev.to/mperon/axios-error-handling-like-a-boss-333d
declare module "axios" {
export interface AxiosRequestConfig {
raw?: boolean;
silent?: boolean;
}
}
export const defaultAxiosRequestConfig = {
mode: "cors",
cache: "no-cache",
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "no-cache",
"Access-Control-Allow-Origin": "*", // Could work and fix the previous problem, but not in all APIs
//'api-key': SERVER_API_KEY,
},
//timeout: 300,
// `onUploadProgress` allows handling of progress events for uploads
// browser only
//onUploadProgress: function (progressEvent) {
// Do whatever you want with the native progress event
//},
// `onDownloadProgress` allows handling of progress events for downloads
// browser only
//onDownloadProgress: function (progressEvent) {
// Do whatever you want with the native progress event
//},
// `cancelToken` specifies a cancel token that can be used to cancel the request
/*cancelToken: new CancelToken(function (cancel) {
}),*/
};
/**
* Creates an initial 'axios' instance with custom settings.
*/
export const createAxiosInstance = () =>
setupInterceptorsTo(axiosClient.create(defaultAxiosRequestConfig));

View File

@ -1,101 +1,51 @@
import secureLocalStorage from "react-secure-storage"; import { AxiosInstance } from "axios";
import { createAxiosInstance } from "./axios-instance";
export const createAxiosAuthActions = (
apiUrl: string,
httpClient = createAxiosInstance()
): IAuthActions => ({
login: async ({ email, password }: ILogin_DTO) => {
secureLocalStorage.clear();
try {
const result = await httpClient.request<ILogin_Response_DTO>({
url: `${apiUrl}/auth/login`,
method: "POST",
data: {
email,
password,
},
});
const { data } = result;
secureLocalStorage.setItem("uecko.auth", data);
return {
success: true,
data,
};
} catch (error) {
return {
success: false,
error: {
message: "Login failed",
name: "Invalid email or password",
},
};
}
},
logout: () => {
secureLocalStorage.clear();
return Promise.resolve({
success: true,
redirectTo: "/login",
});
},
check: () => {
const authUser = secureLocalStorage.getItem("uecko.auth") as ILogin_Response_DTO;
const isAuthenticated = !!authUser?.token;
if (!isAuthenticated) secureLocalStorage.clear();
return Promise.resolve(
isAuthenticated
? {
authenticated: true,
}
: {
authenticated: false,
redirectTo: "/login",
}
);
},
getProfile: async () => {
/** /**
* id: string; * Datos requeridos para iniciar sesión.
* name: string;
* email: string;
* lang_code: string;
* roles: string[];
*/ */
export interface LoginCredentials {
try { email: string;
const result = await httpClient.request<IGetProfileResponse_DTO>({ password: string;
url: `${apiUrl}/profile`,
method: "GET",
});
const { data: profile } = result;
const authUser = secureLocalStorage.getItem("uecko.auth") as ILogin_Response_DTO;
if (authUser?.id === profile?.id) {
secureLocalStorage.setItem("uecko.profile", profile);
return Promise.resolve(profile);
} }
return Promise.resolve(null);
} catch (error) {
return Promise.resolve(null);
}
},
onError: (error: any) => { /**
secureLocalStorage.clear(); * Respuesta esperada al iniciar sesión correctamente.
return Promise.resolve({ */
error, export interface AuthResponse {
logout: true, access_token: string;
}); }
},
}); /**
* Crea funciones de autenticación usando una instancia Axios.
*
* @param api - Instancia Axios preconfigurada.
* @returns Funciones para login y logout.
*/
export const createAuthActions = (api: AxiosInstance) => {
/**
* Envía credenciales al endpoint de login y devuelve el token.
*
* @param credentials - Correo y contraseña del usuario.
* @returns Promesa con el token JWT.
*/
const login = async (credentials: LoginCredentials): Promise<AuthResponse> => {
const response = await api.post<AuthResponse>("/auth/login", credentials);
return response.data;
};
/**
* Realiza un logout desde el backend si aplica, o simplemente resuelve.
* Puede modificarse para llamar a un endpoint si se requiere.
*
* @returns Promesa resuelta.
*/
const logout = async (): Promise<void> => {
// Ejemplo: await api.post('/auth/logout');
return Promise.resolve();
};
return {
login,
logout,
};
};

View File

@ -1,416 +0,0 @@
import type { IListResponseDTO } from "@erp/core";
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 type {
ICreateOneDataProviderParams,
ICustomDataProviderParam,
IDownloadPDFDataProviderParams,
IDownloadPDFDataProviderResponse,
IFilterItemDataProviderParam,
IGetListDataProviderParams,
IGetOneDataProviderParams,
IPaginationDataProviderParam,
IRemoveOneDataProviderParams,
ISortItemDataProviderParam,
IUpdateOneDataProviderParams,
IUploadFileDataProviderParam,
} from "../hooks/use-datasource";
import { createAxiosInstance, defaultAxiosRequestConfig } from "./axios-instance";
export const createAxiosDataProvider = (
apiUrl: string,
httpClient = createAxiosInstance()
): IDataSource => ({
name: () => "AxiosDataProvider",
getApiUrl: () => apiUrl,
getApiAuthorization: getApiAuthLib,
getList: async <R>(params: IGetListDataProviderParams): Promise<IListResponseDTO<R>> => {
const { resource, quickSearchTerm, pagination, filters = [], sort = [] } = params;
const url = `${apiUrl}/${resource}`;
const urlParams = new URLSearchParams();
const { page, limit } = extractPaginationParams(pagination);
urlParams.append("page", String(page));
urlParams.append("limit", String(limit));
const generatedSort = extractSortParams(sort);
if (generatedSort.length) {
urlParams.append("$sort_by", generatedSort.join(","));
}
const queryQuickSearch = generateQuickSearch(quickSearchTerm, filters);
if (queryQuickSearch.length) {
urlParams.append("q", queryQuickSearch.join(","));
}
const queryFilters = extractFilterParams(filters);
if (queryFilters.length) {
urlParams.append("$filters", queryFilters.join(","));
}
const response = await httpClient.request<IListResponseDTO<R>>({
url: `${url}?${urlParams.toString()}`,
method: "GET",
});
return response.data;
},
getOne: async <R>(params: IGetOneDataProviderParams): Promise<R> => {
const { resource, id } = params;
const response = await httpClient.request<R>({
url: `${apiUrl}/${resource}/${id}`,
method: "GET",
});
return response.data;
},
/*saveOne: async <P, R>(params: ISaveOneDataProviderParams<P>): Promise<R> => {
const { resource, data, id } = params;
console.log(params);
const result = await httpClient.request<R>({
url: `${apiUrl}/${resource}/${id}`,
method: "PUT",
data,
});
return result.data;
},*/
createOne: async <P, R>(params: ICreateOneDataProviderParams<P>): Promise<R> => {
const { resource, data } = params;
const response = await httpClient.request<R>({
url: `${apiUrl}/${resource}`,
method: "POST",
data,
});
return response.data;
},
updateOne: async <P, R>(params: IUpdateOneDataProviderParams<P>): Promise<R> => {
const { resource, data, id } = params;
const response = await httpClient.request<R>({
url: `${apiUrl}/${resource}/${id}`,
method: "PUT",
data,
});
return response.data;
},
removeOne: async <R>(params: IRemoveOneDataProviderParams) => {
const { resource, id } = params;
await httpClient.request<R>({
url: `${apiUrl}/${resource}/${id}`,
method: "DELETE",
});
return;
},
uploadFile: async <R>(params: IUploadFileDataProviderParam): Promise<R> => {
const { path, file, key, onUploadProgress } = params;
const url = `${apiUrl}/${path}`;
const formData = new FormData();
formData.append(key || "file", file);
//console.log(file);
const response = await httpClient.post<R>(url, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
onUploadProgress,
});
return response.data;
},
downloadPDF: async (
params: IDownloadPDFDataProviderParams
): Promise<IDownloadPDFDataProviderResponse> => {
const { url, config } = params;
const response = await httpClient.get<ArrayBuffer>(url, {
responseType: "arraybuffer", // Esto es necesario para recibir los datos en formato ArrayBuffer
...config,
});
// Extraer el nombre del archivo de la cabecera Content-Disposition
const contentDisposition = response.headers["content-disposition"];
let filename = "downloaded-file.pdf"; // Valor por defecto si no se encuentra el nombre en la cabecera
if (contentDisposition) {
const match = contentDisposition.match(/filename="?(.+)"?/);
if (match && match[1]) {
filename = match[1];
}
}
// Crear un Blob con los datos descargados
const filedata = new Blob([response.data], { type: "application/pdf" });
return {
filename,
filedata,
};
},
custom: async <R>(params: ICustomDataProviderParam): Promise<R> => {
const { url, path, method, responseType, headers, signal, data, ...payload } = params;
let requestUrl: string;
if (path) {
requestUrl = `${apiUrl}/${path}`;
} else if (url) {
requestUrl = url;
} else {
throw new Error('"url" or "path" param is missing');
}
//console.log(apiUrl, path, url, requestUrl.toString());
// Preparar la respuesta personalizada
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
let customResponse: any;
// Configurar opciones comunes para la petición
const config = {
url: requestUrl.toString(),
method,
responseType,
signal,
...payload,
...defaultAxiosRequestConfig,
};
switch (method) {
case "put":
case "post":
case "patch":
customResponse = await httpClient.request<R>({
...config,
data,
});
break;
case "delete":
customResponse = await httpClient.delete<R>(requestUrl.toString(), {
responseType,
headers,
...payload,
});
break;
default:
customResponse = await httpClient.get<R>(requestUrl.toString(), {
responseType,
signal,
headers,
...payload,
});
break;
}
return customResponse.data;
},
/*getMany: async ({ resource }) => {
const { body } = await httpClient.request({
url: `${apiUrl}/${resource}`,
method: "GET",
//...defaultRequestConfig,
});
return body;
},*/
/*create: async ({ resource, values }) => {
const url = `${apiUrl}/${resource}`;
const { body } = await httpClient.post(url, values, defaultRequestConfig);
return body;
},*/
/*createMany: async ({ resource, values }) => {
const response = await Promise.all(
values.map(async (param) => {
const { body } = await httpClient.post(
`${apiUrl}/${resource}`,
param
//defaultRequestConfig,
);
return body;
})
);
return response;
},*/
/*update: async ({ resource, id, values }) => {
const url = `${apiUrl}/${resource}/${id}`;
const { body } = await httpClient.patch(url, values, defaultRequestConfig);
return body;
},*/
/*updateMany: async ({ resource, ids, values }) => {
const response = await Promise.all(
ids.map(async (id) => {
const { body } = await httpClient.patch(
`${apiUrl}/${resource}/${id}`,
values
//defaultRequestConfig,
);
return body;
})
);
return response;
},*/
// removeMany: async ({ resource, ids }) => {
// const url = `${apiUrl}/${resource}/bulk-delete`;
// const { body } = await httpClient.request({
// url,
// method: "PATCH",
// data: {
// ids,
// },
// //defaultRequestConfig,
// });
// return body;
// },
// upload: async ({ resource, file, onUploadProgress }) => {
// const url = `${apiUrl}/${resource}`;
// const options = {
// //...defaultRequestConfig,
// onUploadProgress,
// headers: {
// //...defaultRequestConfig.headers,
// "Content-Type": "multipart/form-data",
// },
// };
// const formData = new FormData();
// formData.append("file", file);
// const { body } = await httpClient.post(url, formData, options);
// return body;
// },
/*uploadMany: async ({ resource, values }) => {
const url = `${apiUrl}/${resource}`;
const options = {
//...defaultRequestConfig,
headers: {
...defaultRequestConfig.headers,
'Content-Type': 'multipart/form-data'
}
};
const response = await Promise.all(
values.map(async (value) => {
const { body } = await httpClient.post(
url,
value,
options
);
return body;
}),
);
return response;
},*/
/*custom: async ({ url, method, filters, sort, payload, query, headers }) => {
let requestUrl = `${url}?`;
if (sort) {
const generatedSort = extractSortParams(sort);
if (generatedSort) {
const { _sort, _order } = generatedSort;
const sortQuery = {
_sort: _sort.join(","),
_order: _order.join(","),
};
requestUrl = `${requestUrl}&${queryString.stringify(sortQuery)}`;
}
}
if (filters) {
const filterQuery = extractFilterParams(filters);
requestUrl = `${requestUrl}&${queryString.stringify(filterQuery)}`;
}
if (query) {
requestUrl = `${requestUrl}&${queryString.stringify(query)}`;
}
if (headers) {
httpClient.defaults.headers = {
...httpClient.defaults.headers,
...headers,
};
}
let axiosResponse;
switch (method) {
case "put":
case "post":
case "patch":
axiosResponse = await httpClient[method](url, payload);
break;
case "remove":
axiosResponse = await httpClient.delete(url);
break;
default:
axiosResponse = await httpClient.get(requestUrl);
break;
}
const { data } = axiosResponse;
return Promise.resolve({ data });
},*/
});
const extractSortParams = (sort: ISortItemDataProviderParam[] = []) =>
sort.map(({ field, order }) => `${order === "DESC" ? "-" : "+"}${field}`);
const extractFilterParams = (filters: IFilterItemDataProviderParam[] = []) =>
filters
.filter(({ field }) => field !== "q")
.map(({ field, operator, value }) => `${field}[${operator}]${value}`);
const generateQuickSearch = (
quickSearchTerm: string[] = [],
filters: IFilterItemDataProviderParam[] = []
) =>
filters.find(({ field }) => field === "q")?.value
? [filters.find(({ field }) => field === "q")?.value ?? ""]
: quickSearchTerm;
const extractPaginationParams = (pagination?: IPaginationDataProviderParam) => {
const { pageIndex = INITIAL_PAGE_INDEX, pageSize = INITIAL_PAGE_SIZE } = pagination || {};
return {
page: pageIndex,
limit: pageSize,
};
};

View File

@ -1,139 +0,0 @@
import {
AxiosError,
type AxiosInstance,
type AxiosResponse,
type InternalAxiosRequestConfig,
} from "axios";
import { getApiAuthorization } from "../api";
const onRequest = (request: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
request.headers.Authorization = getApiAuthorization();
return request;
};
const onRequestError = (error: AxiosError): Promise<AxiosError> => {
/*console.group("[request error]");
console.dir(error);
console.groupEnd();*/
return Promise.reject(error);
};
const onResponse = (response: AxiosResponse): AxiosResponse => {
/*console.group("[response]");
console.dir(response);
console.groupEnd();*/
const config = response?.config;
if (config.raw) {
return response;
}
/*if (response.status === 200) {
const data = response?.data;
if (!data) {
throw new HttpError("API Error. No data!");
}
return data;
}*/
//throw new HttpError("API Error! Invalid status code!");
return response;
};
const onResponseError = (error: AxiosError): Promise<AxiosError> => {
console.debug("[response error]");
if (error.response) {
// La respuesta fue hecha y el servidor respondió con un código de estado
// que esta fuera del rango de 2xx
console.debug("1 => El servidor respondió con un código de estado > 200");
const data = error.response.data;
const status = error.response.status;
console.debug(data);
console.debug(status);
/*
{
detail: "Quote data not valid",
instance: "/api/v1/quotes",
status: 422,
title: "Unprocessable Entity",
type: "about:blank",
context: {
params: {
},
query: {
},
body: {
date: "2024-08-13",
customer_information: "",
reference: "",
status: "draft",
id: "9c1c6073-127a-4bde-a73c-6229efb51ad0",
},
},
extra: {
errors: [
{
reference: "{reference} is not allowed to be empty",
},
{
customer_information: "{customer_information} is not allowed to be empty",
},
],
},
}
*/
switch (status) {
case 400:
console.error("Bad Request");
break;
case 401:
console.error("UnAuthorized");
//window.location.href = "/logout";
break;
case 403:
console.error("Forbidden");
break;
/*AppEvents.publish(Events.N_Error, {
message: "Forbidden",
description: "Operation ",
});*/
case 404:
console.error("Not found");
break;
case 422:
console.error("Unprocessable Content");
break;
}
return Promise.reject(data);
} else if (error.request) {
// La petición fue hecha pero no se recibió respuesta
console.debug("2 => El servidor no respondió");
console.error(error);
} else {
if (error.code === "ERR_CANCELED") {
// La petición fue hecha pero se ha cancelado
console.debug("3 => Petición cancelada");
} else {
// Algo paso al preparar la petición que lanzo un Error
console.debug("4 => Error desconocido");
console.error(error);
}
}
console.groupEnd();
return Promise.reject(error);
};
export function setupInterceptorsTo(axiosInstance: AxiosInstance): AxiosInstance {
axiosInstance.interceptors.request.use(onRequest, onRequestError);
axiosInstance.interceptors.response.use(onResponse, onResponseError);
return axiosInstance;
}

View File

@ -20,7 +20,7 @@ i18n
detection: { detection: {
order: ["navigator"], order: ["navigator"],
}, },
debug: import.meta.env.MODE === "development", debug: import.meta.env.DEV,
fallbackLng: "es", fallbackLng: "es",
interpolation: { interpolation: {
escapeValue: false, escapeValue: false,

View File

@ -27,10 +27,6 @@
"noUncheckedSideEffectImports": true, "noUncheckedSideEffectImports": true,
"allowUnreachableCode": true "allowUnreachableCode": true
}, },
"include": [ "include": ["src"],
"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

@ -1,11 +1,12 @@
import { defineConfig } from "vite"; import favicons from "@peterek/vite-plugin-favicons";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import path from "path"; import path from "path";
import { defineConfig } from "vite";
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react(), tailwindcss()], plugins: [react(), tailwindcss(), favicons("public/logo.svg")],
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),

View File

@ -4,13 +4,9 @@
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/common/index.ts",
"./api": "./src/api/index.ts", "./api": "./src/api/index.ts",
"./dto": "./src/common/dto/index.ts", "./client": "./src/web/index.ts"
"./hooks/*": ["./src/web/hooks/*.tsx", "./src/hooks/*.ts"],
"./components": "./src/web/components/index.tsx",
"./components/*": "./src/web/components/*.tsx",
"./locales": "./src/common/locales/index.tsx"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^18 || ^19", "react": "^18 || ^19",
@ -28,6 +24,7 @@
"@erp/core": "workspace:*", "@erp/core": "workspace:*",
"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",
"react-secure-storage": "^1.3.2"
} }
} }

View File

@ -1,3 +0,0 @@
export * from "./api";
export * from "./common";
export * from "./web";

View File

@ -0,0 +1,15 @@
import { JSX } from "react";
import { Navigate } from "react-router-dom";
/**
* Protege una ruta para usuarios autenticados.
*/
export const AuthGuard = ({ children }: { children: JSX.Element }) => {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Navigate to='/login' replace />;
}
return children;
};

View File

@ -1,7 +1,7 @@
import { ErrorOverlay, LoadingOverlay } from "@repo/rdx-ui/components"; import { ErrorOverlay, LoadingOverlay } from "@repo/rdx-ui/components";
import React, { useId } from "react"; import React, { useId } from "react";
import { Navigate, useLocation } from "react-router-dom"; import { Navigate, useLocation } from "react-router-dom";
import { useGetProfile, useIsLoggedIn } from "../hooks"; import { useGetProfile, useIsLoggedIn } from "../hooks.old";
type ProctectRouteProps = { type ProctectRouteProps = {
children?: React.ReactNode; children?: React.ReactNode;

View File

@ -0,0 +1,6 @@
export * from "./auth-actions";
export * from "./auth-context";
export * from "./use-auth";
export * from "./use-get-profile";
export * from "./use-is-logged-in";
export * from "./use-login";

View File

@ -1,6 +0,0 @@
export * from "./auth-actions";
export * from "./auth-context";
export * from "./use-auth";
export * from "./use-get-profile";
export * from "./use-is-logged-in";
export * from "./use-login";

View File

@ -0,0 +1,49 @@
import { createContext, useContext, useEffect, useState } from "react";
interface AuthContextType {
token: string | null;
isAuthenticated: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
/**
* Proveedor de autenticación para toda la app.
*/
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const [token, setToken] = useState<string | null>(getAccessToken());
useEffect(() => {
setToken(getAccessToken());
}, []);
const login = async (email: string, password: string) => {
const { access_token } = await authService.login({ email, password });
localStorage.setItem("access_token", access_token);
setToken(access_token);
};
const logout = () => {
clearAccessToken();
setToken(null);
};
return (
<AuthContext.Provider value={{ token, isAuthenticated: !!token, login, logout }}>
{children}
</AuthContext.Provider>
);
};
/**
* Hook para acceder al contexto de autenticación.
*/
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth debe usarse dentro de <AuthProvider>");
}
return context;
};

View File

@ -0,0 +1,9 @@
import { useAuth } from "./useAuth";
/**
* Devuelve un booleano reactivo si el usuario está autenticado.
*/
export const useIsAuthenticated = () => {
const { isAuthenticated } = useAuth();
return isAuthenticated;
};

View File

@ -0,0 +1,2 @@
export * from "./components";
export * from "./lib";

View File

@ -0,0 +1,23 @@
import secureLocalStorage from "react-secure-storage";
/**
* Servicio para manejar la obtención del token JWT desde el almacenamiento local.
* Este archivo puede evolucionar a un AuthService más completo en el futuro.
*/
/**
* Obtiene el token JWT almacenado localmente.
*
* @returns El token como string, o null si no está disponible.
*/
export const getAccessToken = (tokenStorageKey: string): string | null => {
const authInfo = secureLocalStorage.getItem(tokenStorageKey) as { token?: string } | null;
return typeof authInfo?.token === "string" ? authInfo.token : null;
};
/**
* Limpia el token JWT del almacenamiento local.
*/
export const clearAccessToken = (tokenStorageKey: string): void => {
secureLocalStorage.removeItem(tokenStorageKey);
};

View File

@ -0,0 +1 @@
export * from "./access-token-setup";

View File

@ -0,0 +1,17 @@
export const authService = createAuthActions(axiosClient);
/**
* Autentica al usuario con email y password, y guarda el token en localStorage.
*/
export const login = async (email: string, password: string): Promise<void> => {
const { access_token } = await authService.login({ email, password });
setAccessToken(access_token);
};
/**
* Limpia el token local y ejecuta el logout remoto (si aplica).
*/
export const logout = async (): Promise<void> => {
await authService.logout(); // opcional: puede no existir en backend
clearAccessToken();
};

View File

View File

View File

@ -4,18 +4,9 @@
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/common/index.ts",
"./dto": "./src/dto/index.ts", "./api": "./src/api/index.ts",
"./hooks": "./src/web/hooks/index.ts", "./client": "./src/web/index.ts"
"./components": "./src/web/components/index.tsx",
"./components/*": "./src/web/components/*.tsx"
},
"peerDependencies": {
"express": "^4.18.2",
"joi": "^17.13.3",
"react": "^18 || ^19",
"react-dom": "^18 || ^19",
"sequelize": "^6.37.5"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.4", "@biomejs/biome": "1.9.4",
@ -33,12 +24,9 @@
"@repo/rdx-criteria": "workspace:*", "@repo/rdx-criteria": "workspace:*",
"@tanstack/react-query": "^5.75.4", "@tanstack/react-query": "^5.75.4",
"axios": "^1.9.0", "axios": "^1.9.0",
"express": "^4.18.2",
"http-status": "^2.1.0", "http-status": "^2.1.0",
"joi": "^17.13.3", "joi": "^17.13.3",
"libphonenumber-js": "^1.11.20", "libphonenumber-js": "^1.11.20",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^6.26.0", "react-router-dom": "^6.26.0",
"sequelize": "^6.37.5" "sequelize": "^6.37.5"
} }

View File

@ -1,4 +1,4 @@
export * from "./dto"; export * from "../common/dto";
export * from "./infrastructure"; export * from "./infrastructure";
export * from "./logger"; export * from "./logger";
export * from "./modules"; export * from "./modules";

View File

@ -1,4 +1,4 @@
import { Criteria, CriteriaFromUrlConverter } from "@repo/rdx-criteria"; import { Criteria, CriteriaFromUrlConverter } from "@repo/rdx-criteria/server";
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import httpStatus from "http-status"; import httpStatus from "http-status";
import { ApiError } from "./api-error"; import { ApiError } from "./api-error";

View File

@ -1,4 +1,4 @@
import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria"; import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
import { IAggregateRootRepository, UniqueID } from "@repo/rdx-ddd"; import { IAggregateRootRepository, UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { FindOptions, ModelDefined, Sequelize, Transaction } from "sequelize"; import { FindOptions, ModelDefined, Sequelize, Transaction } from "sequelize";

View File

@ -0,0 +1,35 @@
import { AxiosInstance } from "axios";
import { IDataSource } from "../datasource.interface";
export const createAxiosDataSource = (client: AxiosInstance): IDataSource => {
return {
getList: async <T>(resource: string, params?: Record<string, any>) => {
const res = await client.get<T[]>(resource, { params });
return res.data;
},
getOne: async <T>(resource: string, id: string | number) => {
const res = await client.get<T>(`${resource}/${id}`);
return res.data;
},
getMany: async <T>(resource: string, ids: Array<string | number>) => {
const res = await client.get<T[]>(`${resource}`, { params: { ids } });
return res.data;
},
createOne: async <T>(resource: string, data: Partial<T>) => {
const res = await client.post<T>(resource, data);
return res.data;
},
updateOne: async <T>(resource: string, id: string | number, data: Partial<T>) => {
const res = await client.put<T>(`${resource}/${id}`, data);
return res.data;
},
deleteOne: async <T>(resource: string, id: string | number) => {
await client.delete(`${resource}/${id}`);
},
};
};

View File

@ -0,0 +1,47 @@
import axios, { AxiosInstance } from "axios";
import { setupInterceptors } from "./setup-interceptors";
/**
* Configuración necesaria para crear una instancia de Axios personalizada.
*/
export interface AxiosFactoryConfig {
/** URL base del backend API. */
baseURL: string;
/**
* Función que retorna el token JWT actual.
* Debe devolver `null` si no hay token disponible.
*/
getAccessToken: () => string | null;
/**
* Función opcional que se ejecuta cuando ocurre un error de autenticación (por ejemplo, 401).
*/
onAuthError?: () => void;
}
/**
* Crea una instancia de Axios preconfigurada con interceptores.
*
* @param config - Configuración necesaria para inicializar Axios correctamente.
* @returns Instancia de Axios lista para usarse.
*/
export const createAxiosInstance = ({
baseURL,
getAccessToken,
onAuthError,
}: AxiosFactoryConfig): AxiosInstance => {
const instance = axios.create({
baseURL,
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "no-cache",
"Access-Control-Allow-Origin": "*", // Could work and fix the previous problem, but not in all APIs
},
});
setupInterceptors(instance, getAccessToken, onAuthError);
return instance;
};

View File

@ -0,0 +1,2 @@
export * from "./create-axios-data-source";
export * from "./create-axios-instance";

View File

@ -0,0 +1,37 @@
import { AxiosError, AxiosInstance, InternalAxiosRequestConfig } from "axios";
/**
* Configura interceptores para una instancia de Axios.
*
* @param instance - Instancia de Axios que será modificada.
* @param getAccessToken - Función que devuelve el token JWT actual.
* @param onAuthError - Función opcional que se ejecuta ante errores de autenticación (status 401).
*/
export const setupInterceptors = (
instance: AxiosInstance,
getAccessToken: () => string | null,
onAuthError?: () => void
): void => {
instance.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = getAccessToken();
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error: unknown) => {
return Promise.reject(error);
}
);
instance.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
if (error.response?.status === 401 && onAuthError) {
onAuthError();
}
return Promise.reject(error);
}
);
};

View File

@ -0,0 +1,20 @@
import { createContext, useContext } from "react";
import { IDataSource } from "./datasource.interface";
const DataSourceContext = createContext<IDataSource | null>(null);
export const DataSourceProvider = ({
dataSource,
children,
}: {
dataSource: IDataSource;
children: React.ReactNode;
}) => <DataSourceContext.Provider value={dataSource}>{children}</DataSourceContext.Provider>;
export const useDataSource = (): IDataSource => {
const context = useContext(DataSourceContext);
if (!context) {
throw new Error("useDataSource debe usarse dentro de <DataSourceProvider>");
}
return context;
};

View File

@ -0,0 +1,8 @@
export interface IDataSource {
getList<T>(resource: string, params?: Record<string, any>): Promise<T[]>;
getOne<T>(resource: string, id: string | number): Promise<T>;
getMany<T>(resource: string, ids: Array<string | number>): Promise<T[]>;
createOne<T>(resource: string, data: Partial<T>): Promise<T>;
updateOne<T>(resource: string, id: string | number, data: Partial<T>): Promise<T>;
deleteOne<T>(resource: string, id: string | number): Promise<void>;
}

View File

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

View File

@ -1,114 +0,0 @@
import type { IListResponseDTO } from "@erp/core";
import { type AxiosHeaderValue, type ResponseType } from "axios";
export interface IPaginationDataProviderParam {
pageIndex: number;
pageSize: number;
}
export interface ISortItemDataProviderParam {
order: string;
field: string;
}
export interface IFilterItemDataProviderParam {
field: string;
operator?: string;
value?: string;
}
export interface IGetListDataProviderParams {
resource: string;
quickSearchTerm?: string[];
pagination?: IPaginationDataProviderParam;
sort?: ISortItemDataProviderParam[];
filters?: IFilterItemDataProviderParam[];
}
export interface IGetOneDataProviderParams {
resource: string;
id: string;
}
export interface ISaveOneDataProviderParams<T> {
resource: string;
data: T;
id: string;
}
export interface ICreateOneDataProviderParams<T> {
resource: string;
data: T;
}
export interface IUpdateOneDataProviderParams<T> {
resource: string;
data: T;
id: string;
}
export interface IRemoveOneDataProviderParams {
resource: string;
id: string;
}
export interface IDownloadPDFDataProviderParams {
url: string;
config?: {
[key: string]: unknown;
};
}
export interface IDownloadPDFDataProviderResponse {
filename: string;
filedata: Blob;
}
export interface IUploadFileDataProviderParam {
path: string;
//resource: string;
//id: string;
file: File;
key: string;
onUploadProgress?: any;
}
export interface ICustomDataProviderParam {
url?: string;
path?: string;
method: "get" | "delete" | "head" | "options" | "post" | "put" | "patch";
signal?: AbortSignal;
responseType?: ResponseType;
headers?: {
[key: string]: AxiosHeaderValue;
};
[key: string]: unknown;
}
export interface IDataSource {
name: () => string;
getList: <R>(params: IGetListDataProviderParams) => Promise<IListResponseDTO<R>>;
getOne: <R>(params: IGetOneDataProviderParams) => Promise<R>;
//saveOne: <P, R>(params: ISaveOneDataProviderParams<P>) => Promise<R>;
createOne: <P, R>(params: ICreateOneDataProviderParams<P>) => Promise<R>;
updateOne: <P, R>(params: IUpdateOneDataProviderParams<P>) => Promise<R>;
removeOne: (params: IRemoveOneDataProviderParams) => Promise<void>;
downloadPDF: (
params: IDownloadPDFDataProviderParams
) => Promise<IDownloadPDFDataProviderResponse>;
uploadFile: <R>(params: IUploadFileDataProviderParam) => Promise<R>;
custom: <R>(params: ICustomDataProviderParam) => Promise<R>;
getApiUrl: () => string;
getApiAuthorization: () => string;
//create: () => any;
//createMany: () => any;
//removeMany: () => any;
//getMany: () => any;
//update: () => any;
//updateMany: () => any;
//upload: () => any;
//custom: () => any;
//getApiUrl: () => string;
}

View File

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

View File

@ -1,19 +0,0 @@
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

@ -7,7 +7,7 @@ import {
useQuery, useQuery,
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import { isResponseAListDTO } from "@erp/core/dto"; import { isResponseAListDTO } from "@erp/core/common/dto";
import { import {
UseLoadingOvertimeOptionsProps, UseLoadingOvertimeOptionsProps,
UseLoadingOvertimeReturnType, UseLoadingOvertimeReturnType,

View File

@ -1 +1,3 @@
export * from "./api";
export * from "./components";
export * from "./hooks"; export * from "./hooks";

View File

@ -13,25 +13,12 @@
"./components/*": "./src/web/components/*.tsx", "./components/*": "./src/web/components/*.tsx",
"./locales": "./src/common/locales/index.tsx" "./locales": "./src/common/locales/index.tsx"
}, },
"peerDependencies": {
"ag-grid-community": "^33.3.0",
"ag-grid-react": "^33.3.0",
"express": "^4.18.2",
"i18next": "^25.1.1",
"lucide-react": "^0.503.0",
"react": "^18 || ^19",
"react-dom": "^18 || ^19",
"react-i18next": "^15.5.1",
"react-router-dom": "^6.26.0",
"sequelize": "^6.37.5"
},
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.4", "@biomejs/biome": "1.9.4",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/react": "^19.1.2", "@types/react": "^19.1.2",
"@types/react-dom": "^19.1.3", "@types/react-dom": "^19.1.3",
"@types/react-i18next": "^8.1.0", "@types/react-i18next": "^8.1.0",
"sequelize": "^6.37.5",
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },
"dependencies": { "dependencies": {
@ -50,6 +37,7 @@
"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",
"sequelize": "^6.37.5",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"zod": "^3.24.4" "zod": "^3.24.4"
} }

View File

@ -1,5 +1,5 @@
import { ITransactionManager } from "@erp/core"; import { ITransactionManager } from "@erp/core";
import { Criteria } from "@repo/rdx-criteria"; import { Criteria } from "@repo/rdx-criteria/server";
import { Collection, Result } from "@repo/rdx-utils"; import { Collection, Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { IInvoiceService, Invoice } from "../domain"; import { IInvoiceService, Invoice } from "../domain";

View File

@ -1,4 +1,4 @@
import { Criteria } from "@repo/rdx-criteria"; 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 { Invoice } from "../aggregates"; import { Invoice } from "../aggregates";

View File

@ -1,4 +1,4 @@
import { Criteria } from "@repo/rdx-criteria"; 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 { IInvoiceProps, Invoice } from "../aggregates"; import { IInvoiceProps, Invoice } from "../aggregates";

View File

@ -1,4 +1,4 @@
import { Criteria } from "@repo/rdx-criteria"; 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";

View File

@ -1,5 +1,5 @@
import { SequelizeRepository } from "@erp/core"; import { SequelizeRepository } from "@erp/core";
import { Criteria } from "@repo/rdx-criteria"; 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 { Sequelize, Transaction } from "sequelize"; import { Sequelize, Transaction } from "sequelize";

View File

@ -1,5 +1,5 @@
import { IListResponseDTO } from "@erp/core"; import { IListResponseDTO } from "@erp/core";
import { Criteria } from "@repo/rdx-criteria"; import { Criteria } from "@repo/rdx-criteria/server";
import { Collection } from "@repo/rdx-utils"; import { Collection } from "@repo/rdx-utils";
import { IListInvoicesResponseDTO } from "../../../../common/dto"; import { IListInvoicesResponseDTO } from "../../../../common/dto";
import { Invoice } from "../../../domain"; import { Invoice } from "../../../domain";

View File

@ -50,26 +50,32 @@ interface IRow {
export const InvoicesGrid = () => { export const InvoicesGrid = () => {
const { useList } = useInvoices(); const { useList } = useInvoices();
const { data, isPending, isError, error } = useList({}); const { data, isLoading, isPending, isError, error } = useList({});
// Column Definitions: Defines & controls grid columns. // Column Definitions: Defines & controls grid columns.
const [colDefs] = useState<ColDef[]>([ const [colDefs] = useState<ColDef[]>([
{ field: "invoice_number" },
{ field: "invoice_series" },
{ {
field: "mission", field: "status",
filter: true,
minWidth: 200,
}, },
{ field: "company", filter: false },
{ field: "location" }, { field: "issue_date" },
{ field: "date" }, { field: "operation_date" },
{ {
field: "price", field: "subtotal",
valueFormatter: (params: ValueFormatterParams) => { valueFormatter: (params: ValueFormatterParams) => {
return `£${params.value.toLocaleString()}`; return "0 €";
//return `£${params.value.toLocaleString()}`;
},
},
{
field: "total",
valueFormatter: (params: ValueFormatterParams) => {
return "0 €";
//return `£${params.value.toLocaleString()}`;
}, },
}, },
{ field: "successful" },
{ field: "rocket" },
]); ]);
// Apply settings across all columns // Apply settings across all columns
@ -92,7 +98,7 @@ export const InvoicesGrid = () => {
> >
<AgGridReact <AgGridReact
rowData={data} rowData={data}
loading={loading} loading={isLoading || isPending}
columnDefs={colDefs} columnDefs={colDefs}
defaultColDef={defaultColDef} defaultColDef={defaultColDef}
pagination={true} pagination={true}

View File

@ -7,10 +7,8 @@
"clean": "rm -rf node_modules" "clean": "rm -rf node_modules"
}, },
"exports": { "exports": {
".": "./src/index.ts" ".": "./src/defaults.ts",
}, "./server": "./src/index.ts"
"peerDependencies": {
"sequelize": "^6.37.5"
}, },
"devDependencies": { "devDependencies": {
"@repo/typescript-config": "workspace:*" "@repo/typescript-config": "workspace:*"

View File

@ -14,11 +14,6 @@
"./locales/*": "./src/locales/*", "./locales/*": "./src/locales/*",
"./hooks/*": ["./src/hooks/*.tsx", "./src/hooks/*.ts"] "./hooks/*": ["./src/hooks/*.tsx", "./src/hooks/*.ts"]
}, },
"peerDependencies": {
"react": "^18 || ^19",
"react-dom": "^18 || ^19",
"react-router": "^6.26.0"
},
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.4", "@biomejs/biome": "1.9.4",
"@repo/typescript-config": "workspace:*", "@repo/typescript-config": "workspace:*",

View File

@ -16,8 +16,5 @@
}, },
"dependencies": { "dependencies": {
"joi": "^17.13.3" "joi": "^17.13.3"
},
"peerDependencies": {
"joi": "^17.13.3"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -2,15 +2,12 @@ packages:
- packages/* - packages/*
- modules/* - modules/*
- apps/* - apps/*
- "!apps/web" ignoredBuiltDependencies:
- esbuild
onlyBuiltDependencies: onlyBuiltDependencies:
- '@biomejs/biome' - '@biomejs/biome'
- '@parcel/watcher' - '@parcel/watcher'
- '@tailwindcss/oxide' - '@tailwindcss/oxide'
- bcrypt - bcrypt
- core-js-pure - core-js-pure
- sharp
ignoredBuiltDependencies:
- esbuild