.
This commit is contained in:
parent
e8352ae24e
commit
eabd63ec09
1
apps/web/.env.development
Normal file
1
apps/web/.env.development
Normal file
@ -0,0 +1 @@
|
||||
VITE_API_SERVER_URL=http://192.168.0.104:3002/api/v1
|
||||
@ -8,6 +8,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link href="https://fonts.upset.dev/css2?family=Poppins&display=swap" rel="stylesheet" />
|
||||
<title>FactuGES 2025</title>
|
||||
<!-- FAVICONS -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@ -12,8 +12,26 @@
|
||||
"lint": "biome lint --fix",
|
||||
"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": {
|
||||
"@erp/core": "workspace:*",
|
||||
"@erp/auth": "workspace:*",
|
||||
"@erp/invoices": "workspace:*",
|
||||
"@repo/rdx-criteria": "workspace:*",
|
||||
"@repo/rdx-ui": "workspace:*",
|
||||
@ -28,28 +46,10 @@
|
||||
"react-hook-form-persist": "^3.0.0",
|
||||
"react-i18next": "^15.0.1",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"react-secure-storage": "^1.3.2",
|
||||
"sequelize": "^6.37.5",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss": "^4.1.6",
|
||||
"tw-animate-css": "^1.2.9",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@ -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 |
@ -3,14 +3,19 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
|
||||
import { createAxiosDataProvider } from "@/lib/axios/create-axios-data-provider";
|
||||
import { UnsavedWarnProvider } from "@/lib/hooks";
|
||||
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 "./app.css";
|
||||
|
||||
/**
|
||||
* Clave utilizada en el almacenamiento local para el token JWT.
|
||||
*/
|
||||
const TOKEN_STORAGE_KEY = "factuges.auth";
|
||||
|
||||
export const App = () => {
|
||||
const queryClient = new QueryClient({
|
||||
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 (
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<DataSourceProvider dataSource={createAxiosDataProvider(import.meta.env.VITE_API_URL)}>
|
||||
<DataSourceProvider dataSource={dataSource}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<UnsavedWarnProvider>
|
||||
<AppRoutes />
|
||||
</UnsavedWarnProvider>
|
||||
</TooltipProvider>
|
||||
<Toaster />
|
||||
{import.meta.env.MODE === "development" && <ReactQueryDevtools initialIsOpen={false} />}
|
||||
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
|
||||
</DataSourceProvider>
|
||||
</QueryClientProvider>
|
||||
</I18nextProvider>
|
||||
|
||||
@ -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}` : "";
|
||||
};
|
||||
@ -1 +0,0 @@
|
||||
export * from "./get-api-authorization";
|
||||
@ -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));
|
||||
@ -1,101 +1,51 @@
|
||||
import secureLocalStorage from "react-secure-storage";
|
||||
import { createAxiosInstance } from "./axios-instance";
|
||||
import { AxiosInstance } from "axios";
|
||||
|
||||
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;
|
||||
* name: string;
|
||||
* email: string;
|
||||
* lang_code: string;
|
||||
* roles: string[];
|
||||
* Datos requeridos para iniciar sesión.
|
||||
*/
|
||||
|
||||
try {
|
||||
const result = await httpClient.request<IGetProfileResponse_DTO>({
|
||||
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);
|
||||
export interface LoginCredentials {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
} catch (error) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
},
|
||||
|
||||
onError: (error: any) => {
|
||||
secureLocalStorage.clear();
|
||||
return Promise.resolve({
|
||||
error,
|
||||
logout: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
/**
|
||||
* Respuesta esperada al iniciar sesión correctamente.
|
||||
*/
|
||||
export interface AuthResponse {
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
@ -20,7 +20,7 @@ i18n
|
||||
detection: {
|
||||
order: ["navigator"],
|
||||
},
|
||||
debug: import.meta.env.MODE === "development",
|
||||
debug: import.meta.env.DEV,
|
||||
fallbackLng: "es",
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
|
||||
@ -27,10 +27,6 @@
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"allowUnreachableCode": true
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"../../modules/core/src/web/hooks/use-datasource/use-datasource.tsx",
|
||||
"../../modules/core/src/web/hooks/use-datasource/datasource.interface.ts"
|
||||
],
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import favicons from "@peterek/vite-plugin-favicons";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import path from "path";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
plugins: [react(), tailwindcss(), favicons("public/logo.svg")],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
|
||||
@ -4,13 +4,9 @@
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
".": "./src/common/index.ts",
|
||||
"./api": "./src/api/index.ts",
|
||||
"./dto": "./src/common/dto/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"
|
||||
"./client": "./src/web/index.ts"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19",
|
||||
@ -28,6 +24,7 @@
|
||||
"@erp/core": "workspace:*",
|
||||
"react": "^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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
export * from "./api";
|
||||
export * from "./common";
|
||||
export * from "./web";
|
||||
15
modules/auth/src/web/components/auth-guard.tsx
Normal file
15
modules/auth/src/web/components/auth-guard.tsx
Normal 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;
|
||||
};
|
||||
@ -1,7 +1,7 @@
|
||||
import { ErrorOverlay, LoadingOverlay } from "@repo/rdx-ui/components";
|
||||
import React, { useId } from "react";
|
||||
import { Navigate, useLocation } from "react-router-dom";
|
||||
import { useGetProfile, useIsLoggedIn } from "../hooks";
|
||||
import { useGetProfile, useIsLoggedIn } from "../hooks.old";
|
||||
|
||||
type ProctectRouteProps = {
|
||||
children?: React.ReactNode;
|
||||
|
||||
6
modules/auth/src/web/hooks.old/index.ts
Normal file
6
modules/auth/src/web/hooks.old/index.ts
Normal 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";
|
||||
@ -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";
|
||||
49
modules/auth/src/web/hooks/useAuth.tsx
Normal file
49
modules/auth/src/web/hooks/useAuth.tsx
Normal 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;
|
||||
};
|
||||
0
modules/auth/src/web/hooks/useCurrentUser.ts
Normal file
0
modules/auth/src/web/hooks/useCurrentUser.ts
Normal file
9
modules/auth/src/web/hooks/useIsAuthenticated.ts
Normal file
9
modules/auth/src/web/hooks/useIsAuthenticated.ts
Normal 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;
|
||||
};
|
||||
2
modules/auth/src/web/index.ts
Normal file
2
modules/auth/src/web/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./components";
|
||||
export * from "./lib";
|
||||
23
modules/auth/src/web/lib/access-token-setup.ts
Normal file
23
modules/auth/src/web/lib/access-token-setup.ts
Normal 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);
|
||||
};
|
||||
1
modules/auth/src/web/lib/index.ts
Normal file
1
modules/auth/src/web/lib/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./access-token-setup";
|
||||
17
modules/auth/src/web/services/auth-service.ts
Normal file
17
modules/auth/src/web/services/auth-service.ts
Normal 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();
|
||||
};
|
||||
0
modules/auth/src/web/services/index.ts
Normal file
0
modules/auth/src/web/services/index.ts
Normal file
0
modules/auth/src/web/services/me-service.ts
Normal file
0
modules/auth/src/web/services/me-service.ts
Normal file
0
modules/auth/src/web/services/types.ts
Normal file
0
modules/auth/src/web/services/types.ts
Normal file
0
modules/auth/src/web/services/user-service.ts
Normal file
0
modules/auth/src/web/services/user-service.ts
Normal file
@ -4,18 +4,9 @@
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./dto": "./src/dto/index.ts",
|
||||
"./hooks": "./src/web/hooks/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"
|
||||
".": "./src/common/index.ts",
|
||||
"./api": "./src/api/index.ts",
|
||||
"./client": "./src/web/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
@ -33,12 +24,9 @@
|
||||
"@repo/rdx-criteria": "workspace:*",
|
||||
"@tanstack/react-query": "^5.75.4",
|
||||
"axios": "^1.9.0",
|
||||
"express": "^4.18.2",
|
||||
"http-status": "^2.1.0",
|
||||
"joi": "^17.13.3",
|
||||
"libphonenumber-js": "^1.11.20",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"sequelize": "^6.37.5"
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export * from "./dto";
|
||||
export * from "../common/dto";
|
||||
export * from "./infrastructure";
|
||||
export * from "./logger";
|
||||
export * from "./modules";
|
||||
@ -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 httpStatus from "http-status";
|
||||
import { ApiError } from "./api-error";
|
||||
@ -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 { Result } from "@repo/rdx-utils";
|
||||
import { FindOptions, ModelDefined, Sequelize, Transaction } from "sequelize";
|
||||
35
modules/core/src/web/api/axios/create-axios-data-source.ts
Normal file
35
modules/core/src/web/api/axios/create-axios-data-source.ts
Normal 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}`);
|
||||
},
|
||||
};
|
||||
};
|
||||
47
modules/core/src/web/api/axios/create-axios-instance.ts
Normal file
47
modules/core/src/web/api/axios/create-axios-instance.ts
Normal 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;
|
||||
};
|
||||
2
modules/core/src/web/api/axios/index.ts
Normal file
2
modules/core/src/web/api/axios/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./create-axios-data-source";
|
||||
export * from "./create-axios-instance";
|
||||
37
modules/core/src/web/api/axios/setup-interceptors.ts
Normal file
37
modules/core/src/web/api/axios/setup-interceptors.ts
Normal 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);
|
||||
}
|
||||
);
|
||||
};
|
||||
20
modules/core/src/web/api/datasource-context.tsx
Normal file
20
modules/core/src/web/api/datasource-context.tsx
Normal 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;
|
||||
};
|
||||
8
modules/core/src/web/api/datasource.interface.ts
Normal file
8
modules/core/src/web/api/datasource.interface.ts
Normal 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>;
|
||||
}
|
||||
3
modules/core/src/web/api/index.ts
Normal file
3
modules/core/src/web/api/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./axios";
|
||||
export * from "./datasource-context";
|
||||
export * from "./datasource.interface";
|
||||
@ -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;
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
export * from "./datasource.interface";
|
||||
export * from "./use-datasource";
|
||||
export * from "./use-list";
|
||||
@ -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;
|
||||
};
|
||||
@ -7,7 +7,7 @@ import {
|
||||
useQuery,
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
import { isResponseAListDTO } from "@erp/core/dto";
|
||||
import { isResponseAListDTO } from "@erp/core/common/dto";
|
||||
import {
|
||||
UseLoadingOvertimeOptionsProps,
|
||||
UseLoadingOvertimeReturnType,
|
||||
|
||||
@ -1 +1,3 @@
|
||||
export * from "./api";
|
||||
export * from "./components";
|
||||
export * from "./hooks";
|
||||
|
||||
@ -13,25 +13,12 @@
|
||||
"./components/*": "./src/web/components/*.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": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.3",
|
||||
"@types/react-i18next": "^8.1.0",
|
||||
"sequelize": "^6.37.5",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -50,6 +37,7 @@
|
||||
"react-dom": "^19.1.0",
|
||||
"react-i18next": "^15.5.1",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"sequelize": "^6.37.5",
|
||||
"slugify": "^1.6.6",
|
||||
"zod": "^3.24.4"
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 { Transaction } from "sequelize";
|
||||
import { IInvoiceService, Invoice } from "../domain";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Criteria } from "@repo/rdx-criteria";
|
||||
import { Criteria } from "@repo/rdx-criteria/server";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Collection, Result } from "@repo/rdx-utils";
|
||||
import { Invoice } from "../aggregates";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Criteria } from "@repo/rdx-criteria";
|
||||
import { Criteria } from "@repo/rdx-criteria/server";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Collection, Result } from "@repo/rdx-utils";
|
||||
import { IInvoiceProps, Invoice } from "../aggregates";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Criteria } from "@repo/rdx-criteria";
|
||||
import { Criteria } from "@repo/rdx-criteria/server";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Collection, Result } from "@repo/rdx-utils";
|
||||
import { Transaction } from "sequelize";
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 { Collection, Result } from "@repo/rdx-utils";
|
||||
import { Sequelize, Transaction } from "sequelize";
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 { IListInvoicesResponseDTO } from "../../../../common/dto";
|
||||
import { Invoice } from "../../../domain";
|
||||
|
||||
@ -50,26 +50,32 @@ interface IRow {
|
||||
export const InvoicesGrid = () => {
|
||||
const { useList } = useInvoices();
|
||||
|
||||
const { data, isPending, isError, error } = useList({});
|
||||
const { data, isLoading, isPending, isError, error } = useList({});
|
||||
|
||||
// Column Definitions: Defines & controls grid columns.
|
||||
const [colDefs] = useState<ColDef[]>([
|
||||
{ field: "invoice_number" },
|
||||
{ field: "invoice_series" },
|
||||
{
|
||||
field: "mission",
|
||||
filter: true,
|
||||
minWidth: 200,
|
||||
field: "status",
|
||||
},
|
||||
{ field: "company", filter: false },
|
||||
{ field: "location" },
|
||||
{ field: "date" },
|
||||
|
||||
{ field: "issue_date" },
|
||||
{ field: "operation_date" },
|
||||
{
|
||||
field: "price",
|
||||
field: "subtotal",
|
||||
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
|
||||
@ -92,7 +98,7 @@ export const InvoicesGrid = () => {
|
||||
>
|
||||
<AgGridReact
|
||||
rowData={data}
|
||||
loading={loading}
|
||||
loading={isLoading || isPending}
|
||||
columnDefs={colDefs}
|
||||
defaultColDef={defaultColDef}
|
||||
pagination={true}
|
||||
|
||||
@ -7,10 +7,8 @@
|
||||
"clean": "rm -rf node_modules"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"sequelize": "^6.37.5"
|
||||
".": "./src/defaults.ts",
|
||||
"./server": "./src/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/typescript-config": "workspace:*"
|
||||
|
||||
@ -14,11 +14,6 @@
|
||||
"./locales/*": "./src/locales/*",
|
||||
"./hooks/*": ["./src/hooks/*.tsx", "./src/hooks/*.ts"]
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19",
|
||||
"react-dom": "^18 || ^19",
|
||||
"react-router": "^6.26.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
|
||||
@ -16,8 +16,5 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"joi": "^17.13.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"joi": "^17.13.3"
|
||||
}
|
||||
}
|
||||
|
||||
1271
pnpm-lock.yaml
1271
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -2,15 +2,12 @@ packages:
|
||||
- packages/*
|
||||
- modules/*
|
||||
- apps/*
|
||||
- "!apps/web"
|
||||
|
||||
|
||||
ignoredBuiltDependencies:
|
||||
- esbuild
|
||||
onlyBuiltDependencies:
|
||||
- '@biomejs/biome'
|
||||
- '@parcel/watcher'
|
||||
- '@tailwindcss/oxide'
|
||||
- bcrypt
|
||||
- core-js-pure
|
||||
|
||||
ignoredBuiltDependencies:
|
||||
- esbuild
|
||||
- sharp
|
||||
|
||||
Loading…
Reference in New Issue
Block a user