From 1ab03ea10b1cfabd75c05932dbc371408de82b13 Mon Sep 17 00:00:00 2001 From: Sergio Ramirez Date: Thu, 11 Sep 2025 12:33:36 -0400 Subject: [PATCH] agregado Scroll infinito para las encuestas --- .../features/inventory/inventory.service.ts | 47 +++--- .../src/features/surveys/surveys.service.ts | 12 +- apps/web/app/dashboard/productos/page.tsx | 19 --- apps/web/constants/status.ts | 8 ++ .../inventory/update-product-form.tsx | 2 +- .../products/product-list-scroll.tsx | 46 +++--- .../components/products/product-list.tsx | 30 ---- .../components/products/productCard.tsx | 44 +++--- .../surveys/components/survey-card.tsx | 55 +++++++ .../surveys/components/survey-list.tsx | 136 ++++++++++-------- .../surveys/hooks/use-query-surveys.ts | 20 ++- apps/web/hooks/use-safe-query.ts | 19 +-- 12 files changed, 236 insertions(+), 202 deletions(-) delete mode 100644 apps/web/feactures/inventory/components/products/product-list.tsx create mode 100644 apps/web/feactures/surveys/components/survey-card.tsx diff --git a/apps/api/src/features/inventory/inventory.service.ts b/apps/api/src/features/inventory/inventory.service.ts index 75ed217..3dcf427 100644 --- a/apps/api/src/features/inventory/inventory.service.ts +++ b/apps/api/src/features/inventory/inventory.service.ts @@ -3,7 +3,7 @@ import { Inject, Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import * as schema from 'src/database/index'; import { products, viewProductsStore } from 'src/database/index'; -import { eq, like, or, SQL, sql, and, not } from 'drizzle-orm'; +import { eq, like, or, SQL, sql, and, not, ne } from 'drizzle-orm'; import { CreateProductDto } from './dto/create-product.dto'; import { UpdateProductDto } from './dto/update-product.dto'; import { Product, Store, Inventory } from './entities/inventory.entity'; @@ -28,12 +28,21 @@ export class InventoryService { let searchCondition: SQL | undefined; if (search) { - searchCondition = and(or( - like(products.title, `%${search}%`), - like(products.description, `%${search}%`), - ), eq(products.userId, id)); + searchCondition = and( + or( + like(products.title, `%${search}%`), + like(products.description, `%${search}%`), + ), + and( + eq(products.userId, id), + ne(products.status, 'ELIMINADO') + ) + ) } else { - searchCondition = eq(products.userId, id); + searchCondition = and( + eq(products.userId, id), + ne(products.status, 'ELIMINADO') + ) } // Build sort condition @@ -98,7 +107,7 @@ export class InventoryService { like(viewProductsStore.title, `%${search}%`), like(viewProductsStore.description, `%${search}%`) ), - or(eq(viewProductsStore.status, 'PUBLICADO'), eq(viewProductsStore.status, 'AGOTADO')) + and(ne(viewProductsStore.status, 'BORRADOR'), ne(viewProductsStore.status, 'ELIMINADO')) ) } else if (search) { or( @@ -106,7 +115,7 @@ export class InventoryService { like(viewProductsStore.description, `%${search}%`) ) } else if (isStore) { - searchCondition = or(eq(viewProductsStore.status, 'PUBLICADO'), eq(viewProductsStore.status, 'AGOTADO')) + searchCondition = and(ne(viewProductsStore.status, 'BORRADOR'), ne(viewProductsStore.status, 'ELIMINADO')) } // Build sort condition @@ -177,7 +186,7 @@ export class InventoryService { phone: viewProductsStore.phone }) .from(viewProductsStore) - .where(eq(viewProductsStore.id, id)); + .where(and(eq(viewProductsStore.id, id), ne(viewProductsStore.status, 'ELIMINADO'))); if (find.length === 0) { throw new HttpException('Product does not exist', HttpStatus.NOT_FOUND); @@ -290,23 +299,21 @@ export class InventoryService { } async remove(productId: number, userId: number): Promise<{ message: string }> { - const picturesPath = join(__dirname, '..', '..', '..', '..', 'web', 'public', 'uploads', 'inventory', userId.toString() , productId.toString()); + // const picturesPath = join(__dirname, '..', '..', '..', '..', 'web', 'public', 'uploads', 'inventory', userId.toString() , productId.toString()); - try { - // Borra el directorio y todos sus contenidos de forma recursiva y forzada. - await rm(picturesPath, { recursive: true, force: true }); - } catch (error) { - // Es buena práctica manejar el error, aunque `force: true` lo hace menos probable. - // Podrías registrar el error, pero no detener la ejecución. - console.error(`No se pudo eliminar el directorio ${picturesPath}:`, error); - } + // try { + // // Borra el directorio y todos sus contenidos de forma recursiva y forzada. + // await rm(picturesPath, { recursive: true, force: true }); + // } catch (error) { + // console.error(`No se pudo eliminar el directorio ${picturesPath}:`, error); + // } // Check if exists await this.findOne(productId); // Delete user (this will cascade delete related records due to foreign key constraints) - await this.drizzle.delete(products).where(eq(products.id, productId)); - // await this.drizzle.update(products).set({ status: 'ELIMINADO' }).where(eq(products.id, productId)); + // await this.drizzle.delete(products).where(eq(products.id, productId)); + await this.drizzle.update(products).set({ status: 'ELIMINADO' }).where(eq(products.id, productId)); return { message: 'Product deleted successfully' }; } diff --git a/apps/api/src/features/surveys/surveys.service.ts b/apps/api/src/features/surveys/surveys.service.ts index 7a7e6a8..8e401b0 100644 --- a/apps/api/src/features/surveys/surveys.service.ts +++ b/apps/api/src/features/surveys/surveys.service.ts @@ -5,7 +5,7 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import * as schema from '@/database/index'; import { CreateSurveyDto } from './dto/create-survey.dto'; import { UpdateSurveyDto } from './dto/update-survey.dto'; -import { and, count, eq, ilike, isNull, or, sql } from 'drizzle-orm'; +import { and, count, eq, ilike, isNull, like, or, sql } from 'drizzle-orm'; import { SurveyDetailDto, SurveyStatisticsResponseDto } from './dto/statistics-response.dto'; import { PaginationDto } from '@/common/dto/pagination.dto'; import { AnswersSurveyDto } from './dto/response-survey.dto'; @@ -92,7 +92,15 @@ export class SurveysService { // } if (findForUserDto.rol[0].rol !== 'superadmin' && findForUserDto.rol[0].rol !== 'admin') { - searchCondition = or(eq(surveys.targetAudience, findForUserDto.rol[0].rol), eq(surveys.targetAudience, 'all')) + searchCondition = and( + or( + eq(surveys.targetAudience, findForUserDto.rol[0].rol), + eq(surveys.targetAudience, 'all') + ), + like(surveys.title, `%${search}%`) + ) + } else { + searchCondition = like(surveys.title, `%${search}%`) } // console.log(searchCondition); diff --git a/apps/web/app/dashboard/productos/page.tsx b/apps/web/app/dashboard/productos/page.tsx index 26053c3..2282728 100644 --- a/apps/web/app/dashboard/productos/page.tsx +++ b/apps/web/app/dashboard/productos/page.tsx @@ -1,28 +1,11 @@ 'use client'; -// import PageContainer from '@/components/layout/page-container'; -// import { ProductList } from '@/feactures/inventory/components/products/product-list'; import { ProductList } from '@/feactures/inventory/components/products/product-list-scroll'; import { Button } from '@repo/shadcn/components/ui/button'; // import { Metadata } from 'next'; -import { useAllProductQuery } from '@/feactures/inventory/hooks/use-query-products'; -import { allProducts } from '@/feactures/inventory/schemas/inventory'; - - -// export const metadata: Metadata = { -// title: 'Productos - Fondemi', -// description: 'Listado de productos disponibles', -// }; export default function SurveysPage() { - // const { data } = useAllProductQuery(); - // let products: allProducts[] = [] - - // if (data?.data) { - // products = data.data - // } return ( - //

Productos disponibles

@@ -32,7 +15,5 @@ export default function SurveysPage() {
- //
- ); } \ No newline at end of file diff --git a/apps/web/constants/status.ts b/apps/web/constants/status.ts index 69a67d4..d9526d7 100644 --- a/apps/web/constants/status.ts +++ b/apps/web/constants/status.ts @@ -1,5 +1,13 @@ export const STATUS = { + PUBLICADO:"Publicado", + AGOTADO:"Agotado", + BORRADOR:"Borrador" +} + +export const PRIVATESTATUS = { PUBLICADO:"Publicado", AGOTADO:"Agotado", BORRADOR:"Borrador", + ELIMINADO:"Eliminado", + BLOQUEADO:"Bloqueado" } \ No newline at end of file diff --git a/apps/web/feactures/inventory/components/inventory/update-product-form.tsx b/apps/web/feactures/inventory/components/inventory/update-product-form.tsx index 7d6b4e8..5344f12 100644 --- a/apps/web/feactures/inventory/components/inventory/update-product-form.tsx +++ b/apps/web/feactures/inventory/components/inventory/update-product-form.tsx @@ -24,7 +24,7 @@ import { Textarea } from '@repo/shadcn/components/ui/textarea'; import {STATUS} from '@/constants/status' import { useState, useEffect } from 'react'; import {sizeFormate} from "@/feactures/inventory/utils/sizeFormate" -import { z } from 'zod'; // Asegúrate de importar Zod +// import { z } from 'zod'; // Asegúrate de importar Zod // --- MODIFICACIÓN CLAVE --- // Extiende tu esquema para incluir el campo de imagen como FileList para el frontend diff --git a/apps/web/feactures/inventory/components/products/product-list-scroll.tsx b/apps/web/feactures/inventory/components/products/product-list-scroll.tsx index 45af75b..e66a89e 100644 --- a/apps/web/feactures/inventory/components/products/product-list-scroll.tsx +++ b/apps/web/feactures/inventory/components/products/product-list-scroll.tsx @@ -2,7 +2,7 @@ import { useRouter } from 'next/navigation'; import { useAllProductInfiniteQuery } from '@/feactures/inventory/hooks/use-query-products'; import { ProductCard } from '@/feactures/inventory/components/products/productCard'; -import { allProducts } from '@/feactures/inventory/schemas/inventory'; +// import { allProducts } from '@/feactures/inventory/schemas/inventory'; import { useRef, useEffect, useState } from 'react'; import { Input } from '@repo/shadcn/components/ui/input'; import { Button } from '@repo/shadcn/components/ui/button'; @@ -20,21 +20,6 @@ export function ProductList() { isLoading } = useAllProductInfiniteQuery(search); - // Funcion al hacer click en un producto - const goTo = (id: number) => { - router.push(`/dashboard/productos/${id}`); - }; - - // funcion para el buscador - const formSubmit = (e: React.FormEvent) => { - e.preventDefault() - const formdata = new FormData(e.currentTarget) - setSearch(formdata.get('search') as string) - console.log('submit') - } - - const products = data?.pages.flatMap(page => page.data) || []; - useEffect(() => { if (!lastProductRef.current || !hasNextPage || isFetchingNextPage) return; @@ -58,26 +43,39 @@ export function ProductList() { }; }, [hasNextPage, isFetchingNextPage, fetchNextPage]); + // Funcion al hacer click en un producto + const goTo = (id: number) => { + router.push(`/dashboard/productos/${id}`); + }; + + // funcion para el buscador + const formSubmit = (e: React.FormEvent) => { + e.preventDefault() + const formdata = new FormData(e.currentTarget) + setSearch(formdata.get('search') as string) + console.log('submit') + } + + const products = data?.pages.flatMap(page => page.data) || []; + return (
+
+ + +
{isLoading ? (

Cargando productos...

) : products.length === 0 ? (
-

- No hay productos disponibles en este momento. -

+

No hay productos disponibles en este momento.

) : ( <> -
- - -
{products.map((item, index) => { - const isLastElement = index === products.length - 1; + const isLastElement = index === products.length - 1; return (
goTo(Number(item.id))} /> diff --git a/apps/web/feactures/inventory/components/products/product-list.tsx b/apps/web/feactures/inventory/components/products/product-list.tsx deleted file mode 100644 index 6aebc0e..0000000 --- a/apps/web/feactures/inventory/components/products/product-list.tsx +++ /dev/null @@ -1,30 +0,0 @@ -'use client'; -import { useRouter } from 'next/navigation'; -import { useAllProductQuery } from '@/feactures/inventory/hooks/use-query-products'; -import { allProducts } from '../../schemas/inventory'; -import { ProductCard } from '@/feactures/inventory/components/products/productCard' - -export function ProductList() { - const router = useRouter(); - const { data: produts } = useAllProductQuery(); - - const handle = (id: number) => { - router.push(`/dashboard/productos/${id}`); - }; - - return ( -
- {produts?.meta.totalPages === 0 ? ( -
-

- No hay productos disponibles en este momento. -

-
- ) : ( - produts?.data.map((data: allProducts) => ( - handle(Number(data.id))} key={data.id} /> - )) - )} -
- ); -} \ No newline at end of file diff --git a/apps/web/feactures/inventory/components/products/productCard.tsx b/apps/web/feactures/inventory/components/products/productCard.tsx index 0cf8709..6ed084a 100644 --- a/apps/web/feactures/inventory/components/products/productCard.tsx +++ b/apps/web/feactures/inventory/components/products/productCard.tsx @@ -10,34 +10,30 @@ import { import { allProducts } from '../../schemas/inventory'; interface cardProps { - product: allProducts; - onClick?: () => void; + product: allProducts; + onClick?: () => void; } export function ProductCard({ product, onClick }: cardProps) { return ( - - {/* */} - - {product.title.charAt(0).toUpperCase() + product.title.slice(1)} - - {/* */} - - - - - {product.status === 'AGOTADO' ? ( -

AGOTADO

- ): ('')} -

$ {product.price}

-
-
- + + + {product.title.charAt(0).toUpperCase() + product.title.slice(1)} + + + + + + {product.status === 'AGOTADO' ? ( +

AGOTADO

+ ) : ('')} +

$ {product.price}

+
+
) } \ No newline at end of file diff --git a/apps/web/feactures/surveys/components/survey-card.tsx b/apps/web/feactures/surveys/components/survey-card.tsx new file mode 100644 index 0000000..b0399c4 --- /dev/null +++ b/apps/web/feactures/surveys/components/survey-card.tsx @@ -0,0 +1,55 @@ +import { Button } from '@repo/shadcn/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@repo/shadcn/card'; +import { Badge } from '@repo/shadcn/badge'; +import { BadgeCheck } from 'lucide-react'; +import { SurveyAnswerForUser } from '../schemas/survey'; + + +interface cardProps { + data: SurveyAnswerForUser; + onClick: (id: number) => void; +} + +export function SurveyCard ({ data, onClick }: cardProps) { + return ( + + + {data.surveys.title} + {data.surveys.description} + + +
+
+ Fecha de creación: + {new Date(data.surveys.created_at).toLocaleDateString()} +
+ {data.surveys.closingDate && ( +
+ Fecha de cierre: + {new Date(data.surveys.closingDate).toLocaleDateString()} +
+ )} +
+
+ + {data.answers_surveys === null ? ( + + ) : ( + + + Realizada + + )} + +
+ ) +} \ No newline at end of file diff --git a/apps/web/feactures/surveys/components/survey-list.tsx b/apps/web/feactures/surveys/components/survey-list.tsx index 3cc2a50..8915296 100644 --- a/apps/web/feactures/surveys/components/survey-list.tsx +++ b/apps/web/feactures/surveys/components/survey-list.tsx @@ -4,83 +4,99 @@ // - Permite editar encuestas existentes // - Permite eliminar encuestas con confirmación // - Muestra el estado (publicada/borrador), fechas y conteo de respuestas - - 'use client'; import { Button } from '@repo/shadcn/button'; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from '@repo/shadcn/card'; import { useRouter } from 'next/navigation'; -import { useSurveysForUserQuery } from '@/feactures/surveys/hooks/use-query-surveys'; -import { SurveyAnswerForUser } from '../schemas/survey'; -import { Badge } from '@repo/shadcn/badge'; -import { BadgeCheck } from 'lucide-react'; +import { useAllSurveysInfiniteQuery } from '@/feactures/surveys/hooks/use-query-surveys'; +import { SurveyCard } from '@/feactures/surveys/components/survey-card'; + +import { SurveyAnswerForUser } from '../schemas/survey'; + +import { useEffect, useRef, useState } from 'react'; +import { Input } from '@repo/shadcn/components/ui/input'; export function SurveyList() { - const router = useRouter(); - const {data: surveys} = useSurveysForUserQuery() + const lastProductRef = useRef(null); + const [search, setSearch] = useState("") + + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading + } = useAllSurveysInfiniteQuery(search) + + useEffect(() => { + if (!lastProductRef.current || !hasNextPage || isFetchingNextPage) return; + + const observer = new IntersectionObserver( + (entries) => { + if (entries[0]?.isIntersecting) { + fetchNextPage(); + } + }, + { + root: null, + rootMargin: '200px', + threshold: 1.0, + } + ); + + observer.observe(lastProductRef.current); + + return () => { + observer.disconnect(); + }; + }, [hasNextPage, isFetchingNextPage, fetchNextPage]); + + const surveys = data?.pages.flatMap(page => page.data) || []; + + // funcion para el buscador + const formSubmit = (e: React.FormEvent) => { + e.preventDefault() + const formdata = new FormData(e.currentTarget) + setSearch(formdata.get('search') as string) + // console.log('submit') + } const handleRespond = (surveyId: number) => { router.push(`/dashboard/encuestas/${surveyId}/responder`); }; - // console.log(surveys?.data) return (
- {surveys?.meta.totalPages === 0 ? ( -
+
+ + +
+ {isLoading ? ( +
+

Cargando productos...

+
+ ) : surveys.length === 0 ? ( +

No hay encuestas disponibles en este momento.

-
+ ) : ( - surveys?.data.map((data: SurveyAnswerForUser) => ( - - - - {data.surveys.title} - {data.surveys.description} - - -
-
- Fecha de creación: - {/* {data.surveys.created_at.toLocaleDateString()} */} - {new Date(data.surveys.created_at).toLocaleDateString()} -
- {data.surveys.closingDate && ( -
- Fecha de cierre: - {/* {data.surveys.closingDate.toLocaleDateString()} */} - {new Date(data.surveys.closingDate).toLocaleDateString()} -
- )} + <> + {surveys.map((data: SurveyAnswerForUser, index) => { + const isLastElement = index === surveys.length - 1; + return ( +
+
- - - {data.answers_surveys === null ? ( - - ) : ( - - - Realizada - - )} - - - )) + ) + })} + {isFetchingNextPage && ( +
+

Cargando más productos...

+
+ )} + )}
- ); + ) } \ No newline at end of file diff --git a/apps/web/feactures/surveys/hooks/use-query-surveys.ts b/apps/web/feactures/surveys/hooks/use-query-surveys.ts index 2609b84..eb83ff8 100644 --- a/apps/web/feactures/surveys/hooks/use-query-surveys.ts +++ b/apps/web/feactures/surveys/hooks/use-query-surveys.ts @@ -1,5 +1,5 @@ 'use client' -import { useSafeQuery } from "@/hooks/use-safe-query"; +import { useSafeInfiniteQuery, useSafeQuery } from "@/hooks/use-safe-query"; import { getSurveyByIdAction, getSurveysAction, getSurveysForUserAction } from "../actions/surveys-actions"; @@ -8,13 +8,25 @@ export function useSurveysQuery(params = {}) { return useSafeQuery(['surveys',params], () => getSurveysAction(params)) } +export function useAllSurveysInfiniteQuery(search: string = '') { + return useSafeInfiniteQuery( + ['surveys', search], + // pageParam + 1 para evitar duplicación de datos + ({ pageParam = 0 }) => getSurveysForUserAction({ page: pageParam + 1, limit: 10, search }), + (lastPage, allPages) => { + // Esta lógica determina el 'pageParam' para la siguiente página + const nextPage = allPages.length; + // Puedes añadir una condición para saber si hay más páginas + if (lastPage.data.length < 10) return undefined; + return nextPage; + } + ) +} + export function useSurveysForUserQuery(params = {}) { return useSafeQuery(['surveys',params], () => getSurveysForUserAction(params)) } - - - export function useSurveysByIdQuery(id: number) { return useSafeQuery(['surveys',id], () => getSurveyByIdAction(id)) } \ No newline at end of file diff --git a/apps/web/hooks/use-safe-query.ts b/apps/web/hooks/use-safe-query.ts index 713e89e..e15703b 100644 --- a/apps/web/hooks/use-safe-query.ts +++ b/apps/web/hooks/use-safe-query.ts @@ -17,7 +17,6 @@ export function useSafeInfiniteQuery( queryKey: [string, K?], queryFn: ({ pageParam }: { pageParam: number }) => Promise, getNextPageParam: (lastPage: T, allPages: T[]) => number | undefined, - // options?: Omit, 'queryKey' | 'queryFn'> ) { return useInfiniteQuery({ queryKey, @@ -25,20 +24,4 @@ export function useSafeInfiniteQuery( getNextPageParam, initialPageParam: 0, }) -} - -// export function useAllProductInfiniteQuery(){ -// return useInfiniteQuery({ -// queryKey:['product'], -// queryFn: ({ pageParam = 0 }) => getAllProducts({ page: pageParam + 1, limit: 10 }), -// getNextPageParam: (lastPage, allPages) => { -// // Esta lógica determina el 'pageParam' para la siguiente página -// const nextPage = allPages.length; -// // Puedes añadir una condición para saber si hay más páginas -// if (lastPage.data.length < 10) return undefined; -// return nextPage; -// }, -// initialPageParam: 0, -// }) - -// } \ No newline at end of file +} \ No newline at end of file