.
This commit is contained in:
parent
3a881c554f
commit
f47a97bfa0
@ -21,6 +21,7 @@
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/dinero.js": "^1.9.4",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/glob": "^8.1.0",
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
//import { initPackages } from "@/core/package-loader";
|
||||
import cors from "cors";
|
||||
import dotenv from "dotenv";
|
||||
import express, { Application } from "express";
|
||||
import helmet from "helmet";
|
||||
@ -21,6 +22,27 @@ export function createApp(): Application {
|
||||
|
||||
app.use(responseTime()); // set up the response-time middleware
|
||||
|
||||
// enable CORS - Cross Origin Resource Sharing
|
||||
app.use(
|
||||
cors({
|
||||
origin: process.env.FRONTEND_URL || "http://localhost:5173",
|
||||
methods: "GET,POST,PUT,DELETE,OPTIONS",
|
||||
credentials: true,
|
||||
|
||||
exposedHeaders: [
|
||||
"Access-Control-Allow-Headers",
|
||||
"Access-Control-Allow-Origin",
|
||||
"Content-Disposition",
|
||||
"Content-Type",
|
||||
"Content-Length",
|
||||
"X-Total-Count",
|
||||
"Pagination-Count",
|
||||
"Pagination-Page",
|
||||
"Pagination-Limit",
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// secure apps by setting various HTTP headers
|
||||
app.use(helmet());
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ export const App = () => {
|
||||
});
|
||||
|
||||
const axiosInstance = createAxiosInstance({
|
||||
baseURL: import.meta.env.VITE_API_URL,
|
||||
baseURL: import.meta.env.VITE_API_SERVER_URL,
|
||||
getAccessToken,
|
||||
onAuthError: () => {
|
||||
clearAccessToken();
|
||||
@ -32,8 +32,6 @@ export const App = () => {
|
||||
},
|
||||
});
|
||||
|
||||
console.log(axiosInstance.defaults.env);
|
||||
|
||||
const dataSource = createAxiosDataSource(axiosInstance);
|
||||
|
||||
return (
|
||||
|
||||
83
docs/prompt-auth.md
Normal file
83
docs/prompt-auth.md
Normal file
@ -0,0 +1,83 @@
|
||||
📌 CONTEXTO DEL PROYECTO:
|
||||
Estoy desarrollando una API en **Node.js** con **TypeScript**, **Express.js**, **Sequelize**, y **Arquitectura Hexagonal (Ports & Adapters)** bajo los principios de **DDD (Domain-Driven Design)** y **SOLID**.
|
||||
La API maneja **autenticación con PassportJS**, soporte para **JWT y LocalStrategy**, gestión segura de contraseñas con **bcrypt**, y transformación de respuestas con **DTOs (Data Transfer Objects)**.
|
||||
|
||||
📌 PRINCIPIOS Y PATRONES A APLICAR:
|
||||
✅ **DDD (Domain-Driven Design):**
|
||||
- Los **agregados** encapsulan su estado y exponen solo lo necesario.
|
||||
- Cada **agregado** tiene su propio **mapper** para conversión entre persistencia y dominio.
|
||||
- **Repositorios** manejan **agregados** y deben desacoplarse de Sequelize.
|
||||
|
||||
✅ **SOLID (Principios de Diseño):**
|
||||
- **SRP:** Cada clase tiene una única responsabilidad.
|
||||
- **OCP:** El código debe ser fácil de extender sin modificarlo.
|
||||
- **LSP:** Los objetos derivados deben poder sustituir a sus clases base.
|
||||
- **ISP:** Usar interfaces específicas en lugar de dependencias directas.
|
||||
- **DIP:** Usar **interfaces** (`IAuthProvider`, `ITransactionManager`) en lugar de acoplarse a implementaciones concretas.
|
||||
|
||||
📌 GESTIÓN DE DEPENDENCIAS:
|
||||
✅ **Factory Pattern (`createAuthService`)**
|
||||
- `AuthService` debe crearse con un **Factory** (`createAuthService()`), permitiendo cambiar su implementación sin afectar la API.
|
||||
- `AuthService` depende de `IAuthenticatedUserRepository`, `ITransactionManager` y `IAuthProvider`.
|
||||
|
||||
📌 AUTENTICACIÓN CON `PassportJS`:
|
||||
✅ **Soporte para JWT y LocalStrategy**
|
||||
- `PassportAuthProvider` maneja **JWTStrategy** para autenticación con tokens y **LocalStrategy** para validación con email y contraseña.
|
||||
- Se debe usar `bcrypt` para cifrar contraseñas antes de almacenarlas.
|
||||
- `validateUser()` en `PassportAuthProvider` verifica credenciales con `LocalStrategy`.
|
||||
|
||||
📌 GESTIÓN SEGURA DE CONTRASEÑAS:
|
||||
✅ **Uso de `bcrypt` para cifrado de contraseñas**
|
||||
- `PasswordHash.create()` valida la fuerza de la contraseña antes de cifrarla.
|
||||
- `bcrypt.compare()` se usa para validar la contraseña al hacer login.
|
||||
- Las contraseñas **nunca deben almacenarse en texto plano en la base de datos**.
|
||||
|
||||
📌 GESTIÓN DE ERRORES:
|
||||
✅ **Uso de `ApiError` para respuestas de error estructuradas**
|
||||
- `ExpressController` debe devolver errores con `ApiError` (`status`, `title`, `detail`, `timestamp`).
|
||||
- `RegisterController` y `LoginController` deben usar `ApiError` en caso de errores (`409 Conflict`, `401 Unauthorized`, `500 Internal Server Error`).
|
||||
- Se deben capturar errores de **Sequelize** en `BaseRepository` (`UniqueConstraintError`, `ConnectionError`, `TimeoutError`).
|
||||
- **Errores de unicidad (`SequelizeUniqueConstraintError`) deben ser personalizados según el contexto usando `errorMapper` en los repositorios.**
|
||||
|
||||
📌 FLUJO DE LA API `/register`:
|
||||
1️⃣ **Request llega a `RegisterController`**.
|
||||
2️⃣ **Valida `email`, `username`, `password` como Value Objects**.
|
||||
3️⃣ **Llama a `AuthService.registerUser()`**.
|
||||
4️⃣ **`AuthService` usa `validateUser()` para evitar usuarios duplicados**.
|
||||
5️⃣ **Si el usuario es nuevo, se cifra la contraseña con `bcrypt.hash()`**.
|
||||
6️⃣ **Se almacena en la BD con `AuthenticatedUserRepository.create()`**.
|
||||
7️⃣ **Se generan `accessToken` y `refreshToken` con `PassportAuthProvider`**.
|
||||
8️⃣ **Se responde con los tokens y el `userId`**.
|
||||
|
||||
📌 FLUJO DE LA API `/login`:
|
||||
1️⃣ **Request llega a `LoginController`**.
|
||||
2️⃣ **Llama a `AuthService.loginUser()` con `email` y `password`**.
|
||||
3️⃣ **`AuthService` busca el usuario en `AuthenticatedUserRepository.findUserByEmail()`**.
|
||||
4️⃣ **Si el usuario existe, compara contraseñas con `bcrypt.compare()`**.
|
||||
5️⃣ **Si la contraseña es correcta, genera `accessToken` y `refreshToken`**.
|
||||
6️⃣ **Devuelve los tokens en la respuesta**.
|
||||
|
||||
📌 TRANSFORMACIÓN DE RESPUESTAS:
|
||||
✅ **Los Controladores son responsables de la transformación de datos antes de enviarlos.**
|
||||
✅ **Se usan `Presenters` (`AuthResponsePresenter`) para mapear datos del dominio a DTOs (`AuthResponseDTO`).**
|
||||
✅ **Se inyectan `Presenters` en los controladores para desacoplar la lógica de transformación.**
|
||||
|
||||
📌 SEGURIDAD Y MEJORES PRÁCTICAS:
|
||||
✅ **Usar `TransactionManager.complete()` para manejar transacciones.**
|
||||
✅ **Evitar acoplamientos directos entre servicios y repositorios.**
|
||||
✅ **Usar `bcrypt` con `SALT_ROUNDS=12` configurables en `.env`.**
|
||||
✅ **Si ocurre un error en la BD, `rollback()` debe ejecutarse automáticamente.**
|
||||
✅ **Devolver códigos HTTP adecuados (`401 Unauthorized`, `409 Conflict`, `500 Internal Server Error`).**
|
||||
|
||||
📌 SOLICITUDES QUE PUEDO HACERTE:
|
||||
- **Generar código** cumpliendo con estas reglas.
|
||||
- **Revisar código y detectar violaciones de SOLID o DDD**.
|
||||
- **Optimizar `AuthService` para mejorar la seguridad y escalabilidad**.
|
||||
- **Implementar `/refresh` para renovar `accessTokens`**.
|
||||
- **Escribir pruebas unitarias y de integración para `registerUser()` y `loginUser()`**.
|
||||
- **Optimizar `AuthResponsePresenter` para soportar más formatos (`XML`, `CSV`).**
|
||||
|
||||
⚠️ **IMPORTANTE:**
|
||||
- **NO generes código que acople dependencias directamente**.
|
||||
- **NO uses Sequelize directamente en los servicios**.
|
||||
- **SIEMPRE usa interfaces (`IAuthProvider`, `ITransactionManager`) en lugar de instancias concretas**.
|
||||
93
docs/prompt-customer-invoices.md
Normal file
93
docs/prompt-customer-invoices.md
Normal file
@ -0,0 +1,93 @@
|
||||
📌 CONTEXTO DEL PROYECTO:
|
||||
Estoy desarrollando una API en **Node.js** con **TypeScript**, **Express.js**, **Sequelize**, y **Arquitectura Hexagonal (Ports & Adapters)** bajo los principios de **DDD (Domain-Driven Design)** y **SOLID**.
|
||||
Este módulo es para **facturas de cliente (Customer Invoices)** y debe cumplir con los estándares definidos previamente para el contexto de autenticación, adaptados a los datos, reglas y operaciones de las facturas.
|
||||
|
||||
📌 PRINCIPIOS Y PATRONES A APLICAR:
|
||||
✅ **DDD (Domain-Driven Design)**
|
||||
- Las **facturas** serán agregados raíz (`CustomerInvoice`) con propiedades como `id`, `customerId`, `invoiceNumber`, `date`, `lines`, `totals`, etc.
|
||||
- Las **líneas de factura** (`InvoiceLine`) se modelan como entidades o value objects dentro del agregado.
|
||||
- Se usará un `Mapper` para convertir entre dominio y persistencia.
|
||||
- Repositorios (`ICustomerInvoiceRepository`) solo manejan agregados.
|
||||
- Operaciones como `createInvoice`, `updateInvoice`, `getInvoiceById` serán gestionadas en `CustomerInvoiceService`.
|
||||
|
||||
✅ **SOLID**
|
||||
- Usar SRP: cada clase con una responsabilidad clara.
|
||||
- Usar interfaces como `ICustomerInvoiceRepository`, `ITransactionManager`, `IInvoicePresenter`.
|
||||
- Evitar acoplamientos entre capa de aplicación y capa de infraestructura.
|
||||
- Controladores deben transformar los datos en DTOs antes de enviarlos.
|
||||
|
||||
📌 ESTRUCTURA DE DATOS:
|
||||
|
||||
La entidad `CustomerInvoice` tendrá:
|
||||
- `id`: UUID (ValueObject `UniqueID`)
|
||||
- `status`: string (ValueObject) (`Draft`, `Emitted`, `Sent`, `Rejected`)
|
||||
- `invoiceNumber`: string (ValueObject )
|
||||
- `invoiceSeries`: string (ValueObject )
|
||||
- `customerId`: UUID (ValueObject)
|
||||
- `issueDate`: UtcDate (ValueObject)
|
||||
- `operationDate`: UtcDate (ValueObject)
|
||||
- `lines`: array de `InvoiceLine`, cada una con:
|
||||
- `productId`: (Optional) UUID (ValueObject `UniqueID`)
|
||||
- `description`: string (ValueObject)
|
||||
- `quantity`: Amount (number) and scale (number) (ValueObject Quantity)
|
||||
- `unitPrice`: Amount (number), scale (number) and currency (string) (ValueObject MoneyValue)
|
||||
- `totalLineAmount`: Amount (number), scale (number) and currency (string) (ValueObject MoneyValue)
|
||||
- `subtotalPrice`: Amount (number), scale (number) and currency (string) (ValueObject MoneyValue)
|
||||
- `discount`: Amount (number) and scale (number) (ValueObject Percentage)
|
||||
- `tax`: Amount (number) and scale (number) (ValueObject Percentage)
|
||||
- `totalAmount`: Amount (number), scale (number) and currency (string) (ValueObject MoneyValue)
|
||||
|
||||
|
||||
📌 OPERACIONES PRINCIPALES:
|
||||
- `createInvoice(data: CreateCustomerInvoiceDTO)`
|
||||
- `getInvoiceById(id: string)`
|
||||
- `updateInvoice(id: string, changes: UpdateCustomerInvoiceDTO)`
|
||||
- `listInvoices(filter?)`
|
||||
- `deleteInvoice(id: string)`
|
||||
- `markAsPaid(id: string)`
|
||||
|
||||
📌 VALIDACIONES:
|
||||
✅ Usar `Zod` para validaciones en Value Objects (`InvoiceNumber`, `InvoiceDate`, `InvoiceLine`).
|
||||
✅ No se deben lanzar excepciones para errores de validación. Usar `Result<T, E>` como en autenticación.
|
||||
|
||||
📌 PRESENTACIÓN:
|
||||
✅ Los controladores convierten el agregado `CustomerInvoice` a `CustomerInvoiceDTO`.
|
||||
✅ Se usan `Presenters` (`CustomerInvoicePresenter`) para construir los DTOs.
|
||||
✅ DTOs solo exponen campos necesarios: nada sensible, nada interno.
|
||||
✅ Se puede extender con `CsvInvoicePresenter`, `PdfInvoicePresenter`, etc.
|
||||
|
||||
📌 AUTORIZACIÓN Y ACCESO:
|
||||
✅ Se protegerán rutas con middleware JWT (`authenticateJWT`).
|
||||
✅ Se validará que el usuario autenticado pueda acceder/modificar la factura (p. ej. pertenece a su empresa).
|
||||
|
||||
📌 PERSISTENCIA:
|
||||
✅ `SequelizeCustomerInvoiceRepository` no accede directamente al modelo de Sequelize, lo hace a través de `Mapper`.
|
||||
✅ La clase `CustomerInvoiceMapper` mapea de/agregado <-> persistencia.
|
||||
✅ Soporta transacciones Sequelize (`TransactionManager.complete(...)`).
|
||||
✅ Los nombres de los campos y las tablas en la base de datos seguirán la notación "snake_case".
|
||||
✅ Los nombres de los campos en los modelos Sequelize seguirán la notación "snake_case".
|
||||
✅ Los modelos Sequelize de los agregados tienen campos 'timestamp' con los nombres "created_at", "updated_at" y "deleted_at".
|
||||
|
||||
|
||||
📌 ERRORES:
|
||||
✅ Usar `ApiError` para errores en la API.
|
||||
✅ Los repositorios deben capturar errores de Sequelize (`UniqueConstraintError`, etc.) y convertirlos a errores de dominio con mensajes claros y específicos (mediante `errorMapper`).
|
||||
|
||||
📌 TESTING:
|
||||
✅ Los servicios (`CustomerInvoiceService`) serán testeados con mocks de repositorio.
|
||||
✅ Las rutas serán testeadas con `supertest`.
|
||||
✅ Las validaciones de ValueObjects tendrán pruebas unitarias.
|
||||
|
||||
📌 NOMENCLATURA:
|
||||
- Agregado: `CustomerInvoice`
|
||||
- Línea: `CustomerInvoiceLine`
|
||||
- DTOs: `CustomerInvoiceDTO`, `InvoiceLineDTO`
|
||||
- Presentadores: `CustomerInvoicePresenter`
|
||||
- Repositorio: `ICustomerInvoiceRepository`, `SequelizeCustomerInvoiceRepository`
|
||||
- ValueObjects: `CustomerInvoiceNumber`, `CustomerInvoiceDate`, `CustomerInvoiceTotal`, `CustomerInvoiceItemQuantity`, etc.
|
||||
|
||||
⚠️ IMPORTANTE:
|
||||
- NO incluir `passwords`, `tokens`, o datos sensibles en respuestas.
|
||||
- NO usar Sequelize directamente en servicios ni controladores.
|
||||
- NO devolver el agregado como respuesta bruta. Usar DTOs.
|
||||
- TODO debe estar tipado, incluso los métodos de los servicios (`Promise<Result<T, Error>>`).
|
||||
@ -2,53 +2,78 @@ import { AxiosInstance } from "axios";
|
||||
import { ICustomParams, IDataSource } from "../datasource.interface";
|
||||
import { defaultAxiosRequestConfig } from "./create-axios-instance";
|
||||
|
||||
/**
|
||||
* Crea un DataSource basado en Axios.
|
||||
* @param client Instancia de Axios que se utilizará para las peticiones HTTP.
|
||||
* @returns Un objeto que implementa la interfaz IDataSource.
|
||||
* @throws Error si la instancia de Axios no es válida o no tiene la configuración necesaria.
|
||||
* @example
|
||||
* const axiosInstance = createAxiosInstance({
|
||||
* baseURL: "https://api.example.com",
|
||||
* getAccessToken: () => localStorage.getItem("accessToken"),
|
||||
* onAuthError: () => {
|
||||
* console.error("Error de autenticación");
|
||||
* // Manejar el error de autenticación, por ejemplo, redirigir al login
|
||||
* },
|
||||
* });
|
||||
* const dataSource = createAxiosDataSource(axiosInstance);
|
||||
*
|
||||
*/
|
||||
|
||||
export const createAxiosDataSource = (client: AxiosInstance): IDataSource => {
|
||||
function getBaseUrlOrFail(): string {
|
||||
const baseUrl = client.getUri();
|
||||
// Validaciones de la instancia de Axios
|
||||
if (!client) {
|
||||
throw new Error("[Axios] Se esperaba una instancia de Axios.");
|
||||
}
|
||||
|
||||
if (!baseUrl) {
|
||||
throw new Error("[Axios] baseURL no está definido en esta instancia.");
|
||||
}
|
||||
if (!(client as AxiosInstance).getUri) {
|
||||
throw new Error("[Axios] La instancia proporcionada no es una instancia de Axios válida.");
|
||||
}
|
||||
|
||||
return baseUrl;
|
||||
if (typeof (client as AxiosInstance).getUri !== "function") {
|
||||
throw new Error("[Axios] La instancia proporcionada no tiene el método getUri.");
|
||||
}
|
||||
|
||||
if (typeof (client as AxiosInstance).getUri() !== "string") {
|
||||
throw new Error("[Axios] baseURL no está definido en esta instancia.");
|
||||
}
|
||||
|
||||
return {
|
||||
getBaseUrl: getBaseUrlOrFail,
|
||||
getBaseUrl: () => (client as AxiosInstance).getUri(),
|
||||
|
||||
getList: async <T>(resource: string, params?: Record<string, any>) => {
|
||||
const res = await client.get<T[]>(resource, params);
|
||||
const res = await (client as AxiosInstance).get<T[]>(resource, params);
|
||||
return res.data;
|
||||
},
|
||||
|
||||
getOne: async <T>(resource: string, id: string | number) => {
|
||||
const res = await client.get<T>(`${resource}/${id}`);
|
||||
const res = await (client as AxiosInstance).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 } });
|
||||
const res = await (client as AxiosInstance).get<T[]>(`${resource}`, { params: { ids } });
|
||||
return res.data;
|
||||
},
|
||||
|
||||
createOne: async <T>(resource: string, data: Partial<T>) => {
|
||||
const res = await client.post<T>(resource, data);
|
||||
const res = await (client as AxiosInstance).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);
|
||||
const res = await (client as AxiosInstance).put<T>(`${resource}/${id}`, data);
|
||||
return res.data;
|
||||
},
|
||||
|
||||
deleteOne: async <T>(resource: string, id: string | number) => {
|
||||
await client.delete(`${resource}/${id}`);
|
||||
await (client as AxiosInstance).delete(`${resource}/${id}`);
|
||||
},
|
||||
|
||||
custom: async <T>(customParams: ICustomParams) => {
|
||||
const { url, path, method, responseType, headers, signal, data, ...payload } = customParams;
|
||||
|
||||
const requestUrl = path ? `${getBaseUrlOrFail()}/${path}` : url;
|
||||
const requestUrl = path ? `${(client as AxiosInstance).getUri()}/${path}` : url;
|
||||
if (!requestUrl) throw new Error('"url" or "path" param is missing');
|
||||
|
||||
const config = {
|
||||
@ -67,14 +92,14 @@ export const createAxiosDataSource = (client: AxiosInstance): IDataSource => {
|
||||
case "put":
|
||||
case "post":
|
||||
case "patch":
|
||||
customResponse = await client.request<T>({
|
||||
customResponse = await (client as AxiosInstance).request<T>({
|
||||
...config,
|
||||
data,
|
||||
});
|
||||
break;
|
||||
|
||||
case "delete":
|
||||
customResponse = await client.delete<T>(requestUrl, {
|
||||
customResponse = await (client as AxiosInstance).delete<T>(requestUrl, {
|
||||
responseType,
|
||||
headers,
|
||||
...payload,
|
||||
@ -82,7 +107,7 @@ export const createAxiosDataSource = (client: AxiosInstance): IDataSource => {
|
||||
break;
|
||||
|
||||
default:
|
||||
customResponse = await client.get<T>(requestUrl, {
|
||||
customResponse = await (client as AxiosInstance).get<T>(requestUrl, {
|
||||
responseType,
|
||||
signal,
|
||||
headers,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
import axios, { AxiosInstance, CreateAxiosDefaults } from "axios";
|
||||
import { setupInterceptors } from "./setup-interceptors";
|
||||
|
||||
/**
|
||||
@ -20,7 +20,7 @@ export interface AxiosFactoryConfig {
|
||||
onAuthError?: () => void;
|
||||
}
|
||||
|
||||
export const defaultAxiosRequestConfig = {
|
||||
export const defaultAxiosRequestConfig: CreateAxiosDefaults = {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
@ -40,10 +40,11 @@ export const createAxiosInstance = ({
|
||||
getAccessToken,
|
||||
onAuthError,
|
||||
}: AxiosFactoryConfig): AxiosInstance => {
|
||||
const instance = axios.create({
|
||||
baseURL,
|
||||
...defaultAxiosRequestConfig,
|
||||
});
|
||||
console.log({ baseURL, getAccessToken, onAuthError });
|
||||
|
||||
const instance = axios.create(defaultAxiosRequestConfig);
|
||||
|
||||
instance.defaults.baseURL = baseURL;
|
||||
|
||||
setupInterceptors(instance, getAccessToken, onAuthError);
|
||||
|
||||
|
||||
@ -87,6 +87,8 @@ export const CustomerInvoicesGrid = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
console.log(isError, error);
|
||||
|
||||
// Container: Defines the grid's theme & dimensions.
|
||||
return (
|
||||
<div
|
||||
|
||||
@ -9,7 +9,7 @@ export const useCustomerInvoices = (params: any) => {
|
||||
return useQuery<any[]>({
|
||||
queryKey: keys().data().resource("invoices").action("list").params(params).get(),
|
||||
queryFn: (context) => {
|
||||
console.log(context);
|
||||
console.log(dataSource.getBaseUrl());
|
||||
const { signal } = context;
|
||||
return dataSource.getList("customer-invoices", {
|
||||
signal,
|
||||
|
||||
@ -12,7 +12,7 @@ export const CustomerInvoicesList = () => {
|
||||
const navigate = useNavigate();
|
||||
const [status, setStatus] = useState("all");
|
||||
|
||||
const CustomerInvoiceStatuses = [
|
||||
/*const CustomerInvoiceStatuses = [
|
||||
{ value: "all", label: t("customerInvoices.list.tabs.all") },
|
||||
{ value: "draft", label: t("customerInvoices.list.tabs.draft") },
|
||||
{ value: "ready", label: t("customerInvoices.list.tabs.ready") },
|
||||
@ -20,7 +20,7 @@ export const CustomerInvoicesList = () => {
|
||||
{ value: "accepted", label: t("customerInvoices.list.tabs.accepted") },
|
||||
{ value: "rejected", label: t("customerInvoices.list.tabs.rejected") },
|
||||
{ value: "archived", label: t("customerInvoices.list.tabs.archived") },
|
||||
];
|
||||
];*/
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -34,7 +34,7 @@ export const CustomerInvoicesList = () => {
|
||||
<p className='text-muted-foreground'>{t("customerInvoices.list.description")}</p>
|
||||
</div>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<Button onClick={() => navigate("/CustomerInvoices/add")}>
|
||||
<Button onClick={() => navigate("/customer-invoices/add")}>
|
||||
<PlusIcon className='w-4 h-4 mr-2' />
|
||||
{t("customerInvoices.create.title")}
|
||||
</Button>
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export * from "./components";
|
||||
export * from "./components/index.tsx";
|
||||
|
||||
@ -132,6 +132,9 @@ importers:
|
||||
'@types/bcrypt':
|
||||
specifier: ^5.0.2
|
||||
version: 5.0.2
|
||||
'@types/cors':
|
||||
specifier: ^2.8.19
|
||||
version: 2.8.19
|
||||
'@types/dinero.js':
|
||||
specifier: ^1.9.4
|
||||
version: 1.9.4
|
||||
@ -2621,6 +2624,9 @@ packages:
|
||||
'@types/connect@3.4.38':
|
||||
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||
|
||||
'@types/cors@2.8.19':
|
||||
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
|
||||
|
||||
'@types/d3-array@3.2.1':
|
||||
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
|
||||
|
||||
@ -7961,6 +7967,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 22.15.24
|
||||
|
||||
'@types/cors@2.8.19':
|
||||
dependencies:
|
||||
'@types/node': 22.15.24
|
||||
|
||||
'@types/d3-array@3.2.1': {}
|
||||
|
||||
'@types/d3-color@3.1.3': {}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user