From 44ff63dd96fe6f6c4a0068c340cec6778ff77820 Mon Sep 17 00:00:00 2001 From: Sojabio Date: Thu, 7 Nov 2024 12:00:11 +0100 Subject: [PATCH] search products by dates --- backend/src/entities/article.ts | 4 +- backend/src/resolvers/ArticleResolver.ts | 34 +-- backend/src/resolvers/ProductResolver.ts | 78 ++++++- backend/src/resolvers/ReservationResolver.ts | 10 + frontend/src/App.tsx | 5 +- .../src/components/EditArticleDropdown.tsx | 48 ----- frontend/src/components/ListProductsTable.tsx | 14 -- frontend/src/components/Navbar.tsx | 15 +- frontend/src/components/RangePicker.tsx | 16 +- frontend/src/components/ReservationButton.tsx | 13 +- .../src/components/ReservationsInfoModal.tsx | 77 +++++++ frontend/src/generated/graphql-types.ts | 201 ++++++++++++------ frontend/src/graphql/queries.ts | 51 +++-- frontend/src/interface/types.tsx | 6 +- frontend/src/pages/NewArticle.tsx | 42 ++-- frontend/src/pages/NewProduct.tsx | 4 +- frontend/src/pages/search/Search.tsx | 27 ++- frontend/src/pages/search/SearchError.tsx | 9 - .../src/pages/search/[searchKeywords].tsx | 15 +- 19 files changed, 416 insertions(+), 253 deletions(-) delete mode 100644 frontend/src/components/EditArticleDropdown.tsx create mode 100644 frontend/src/components/ReservationsInfoModal.tsx delete mode 100644 frontend/src/pages/search/SearchError.tsx diff --git a/backend/src/entities/article.ts b/backend/src/entities/article.ts index 35d944c..4c8c130 100644 --- a/backend/src/entities/article.ts +++ b/backend/src/entities/article.ts @@ -25,7 +25,7 @@ export class Article extends BaseEntity { @ManyToOne(() => Product, (product) => product.articles) product: Product; - @Field(() => [Reservation]) // GraphQL + @Field(() => [Reservation], { nullable: true }) // GraphQL @ManyToMany(() => Reservation, reservation => reservation.articles) // TypeORM - reservations: Reservation[]; + reservations?: Reservation[]; } diff --git a/backend/src/resolvers/ArticleResolver.ts b/backend/src/resolvers/ArticleResolver.ts index 2b7a8ca..2ae5554 100644 --- a/backend/src/resolvers/ArticleResolver.ts +++ b/backend/src/resolvers/ArticleResolver.ts @@ -26,46 +26,14 @@ class EditArticleInput { availability: boolean; } -// data from range picket -@InputType() -class DateRangeInput { - @Field(() => Date) - startDate: Date - - @Field(() => Date) - endDate: Date -} - - @Resolver(Article) class ArticleResolver { @Query(() => [Article]) async getAllArticles() { - const article = await Article.find({ relations: { product: true, reservations: true } }); + const article = await Article.find({ relations: { product: true, reservations: true } }) return article } - @Query(() => [Article]) - async getAvailableArticles(@Arg("dateRange") dateRange: DateRangeInput) { - const { startDate, endDate } = dateRange - const articles = await Article.find({ relations: { reservations: true } }) - - // Filter articles based on their reservation dates - const availableArticles = articles.filter(article => { - return article.reservations.every(reservation => { - const reservationStart = new Date(reservation.startDate) - const reservationEnd = new Date(reservation.endDate) - - return ( - reservationEnd < startDate || // Reservation ends before the start date I searched - reservationStart > endDate // Reservation starts after the end date I searched - }); - }); - - return availableArticles; - } - - @Authorized(Role.Admin) @Mutation(() => Article) async createNewArticle(@Arg("data") newArticleData: NewArticleInput) { diff --git a/backend/src/resolvers/ProductResolver.ts b/backend/src/resolvers/ProductResolver.ts index 7c57d78..fb5fb58 100644 --- a/backend/src/resolvers/ProductResolver.ts +++ b/backend/src/resolvers/ProductResolver.ts @@ -12,6 +12,7 @@ import { } from "type-graphql"; import { Role } from "../entities/user"; + @InputType() class NewProductInput implements Partial { @Field() @@ -27,14 +28,31 @@ class NewProductInput implements Partial { price: number; } +// data from range picker +@InputType() +class ProductDateRangeInput { + @Field(() => Date,{ nullable: true }) + startDate: Date + + @Field(() => Date ,{ nullable: true }) + endDate: Date +} + @Resolver(Product) class ProductResolver { @Query(() => [Product]) async getAllProducts() { - const products = await Product.find({ relations: ["articles"] }); + const products = await Product.find({ + relations: { + articles: { + reservations: true, + }, + }, + }); return products; } + @Authorized(Role.Admin) @Mutation(() => Product) async createNewProduct(@Arg("data") newProductData: NewProductInput) { @@ -55,13 +73,63 @@ class ProductResolver { } @Query(() => [Product]) - async searchProducts(@Arg("keyword") keyword: string) { - const products = await Product.find({ + async searchAndFilterProducts( + @Arg("keyword", { nullable: true }) keyword?: string, + @Arg("dateRangeInput", { nullable: true }) dateRangeInput?: ProductDateRangeInput + ) { + let products: Product[] + + + if (keyword) { + products = await Product.find({ where: [{ name: Like(`%${keyword}%`) }], - }); - return products; + relations: { + articles: { + reservations: true, + }, + }, + }) + } else { + products = await Product.find({ + relations: { + articles: { + reservations: true, + }, + }, + }) } + if (!dateRangeInput) { + return products + } + const availableProducts = products.filter(product => { + // si un produit n'a pas d'article associé il n'est pas dispo pas dispo + if (!product.articles || product.articles.length === 0) { + return false + } + + // si un produit a au moins un article dispo, on l'affiche + return product.articles.some(article => { + // si un article n'a pas de réservation associée, il est dispo + if (!article.reservations || article.reservations.length === 0) { + return true + } + + // je regarde les réservations pour chaque article. + // la fonction renvoie false si, pour au moins une réservation : + return article.reservations.every(reservation => { + return ( + reservation.endDate < dateRangeInput.startDate || // la date de début que j'ai choisie tombe avant la fin de la réservation + reservation.startDate > dateRangeInput.endDate // la date de fin que j'ai choisie tombe après le début de la réservation + ) + }) + }) + }) + + return availableProducts + } + + @Authorized(Role.Admin) @Mutation(() => Product) async editProduct( diff --git a/backend/src/resolvers/ReservationResolver.ts b/backend/src/resolvers/ReservationResolver.ts index 81e9a46..f620da5 100644 --- a/backend/src/resolvers/ReservationResolver.ts +++ b/backend/src/resolvers/ReservationResolver.ts @@ -54,6 +54,16 @@ class ReservationResolver { return reservation; } + @Query(() => [Reservation]) + async getReservationsByArticleId(@Arg("articleId") articleId: string) { + const reservations = await Reservation.find({ + where: { articles: { id: Number(articleId) } }, + relations: ["user", "articles", "articles.product"], + }); + return reservations + } + + @Query(() => ReservationWithTotal) async getCurrentReservationByUserId(@Ctx() context: Context) { if (context.id !== undefined) { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 06f15ea..0f8a78e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,7 +7,6 @@ import ProductDescription from "./pages/ProductDescription"; import Admin from "./pages/Admin"; import Login from "./pages/Login"; import SearchPage from "./pages/search/[searchKeywords]"; -import SearchError from "./pages/search/SearchError"; import Profile from "./pages/Profile"; import { Cart } from "./pages/Cart"; @@ -21,8 +20,8 @@ const App = () => { } /> } /> } /> - } /> - } /> + } /> + } /> } /> } /> diff --git a/frontend/src/components/EditArticleDropdown.tsx b/frontend/src/components/EditArticleDropdown.tsx deleted file mode 100644 index 120e4b9..0000000 --- a/frontend/src/components/EditArticleDropdown.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { message, Select } from "antd"; -import { - GetAllArticlesDocument, - GetAllProductsDocument, - useEditArticleMutation, -} from "../generated/graphql-types"; - -function EditArticleDropdown({ - availability, - id, -}: { - availability: boolean; - id: number; -}) { - const [editArticle] = useEditArticleMutation({ - onCompleted(data) { - console.log("mutation completed data", data); - message.success("La disponibilité de l'article a bien été modifiée"); - }, - onError(error) { - console.log("error after executing mutation", error); - }, - refetchQueries: [GetAllProductsDocument, GetAllArticlesDocument], - }); - - const onChange = async (value: boolean) => { - await editArticle({ - variables: { - article: id.toString(), - data: { availability: value }, - }, - }); - }; - - return ( - - Disponible - Indisponible - - - { scroll={{ x: true }} /> + {isModalOpen && + + } ); }; diff --git a/frontend/src/pages/NewProduct.tsx b/frontend/src/pages/NewProduct.tsx index d7c2a94..8a1d0fa 100644 --- a/frontend/src/pages/NewProduct.tsx +++ b/frontend/src/pages/NewProduct.tsx @@ -1,6 +1,6 @@ import { GetAllProductsDocument, - SearchProductsDocument, + SearchAndFilterProductsDocument, useCreateNewProductMutation, } from "../generated/graphql-types"; import { Button, Form, Input, Card, Typography } from "antd"; @@ -17,7 +17,7 @@ const NewProduct = () => { onError(error) { console.log("error after executing mutation", error); }, - refetchQueries: [GetAllProductsDocument, SearchProductsDocument], + refetchQueries: [GetAllProductsDocument, SearchAndFilterProductsDocument], }); const [form] = Form.useForm(); diff --git a/frontend/src/pages/search/Search.tsx b/frontend/src/pages/search/Search.tsx index 687f534..3cc6d31 100644 --- a/frontend/src/pages/search/Search.tsx +++ b/frontend/src/pages/search/Search.tsx @@ -1,23 +1,36 @@ +import { Link } from "react-router-dom"; import ProductCard from "../../components/ProductCard"; -import { useSearchProductsQuery } from "../../generated/graphql-types"; +import { useSearchAndFilterProductsQuery } from "../../generated/graphql-types"; +import dayjs from "dayjs" + function Search({ keyword }: { keyword: string }) { - const { data, loading, error } = useSearchProductsQuery({ - variables: { keyword: keyword as string }, + const startDate = localStorage.getItem("startDate") ? dayjs(localStorage.getItem("startDate")) : null + const endDate = localStorage.getItem("endDate") ? dayjs(localStorage.getItem("endDate")) : null + const dateRangeInput = startDate && endDate ? { startDate, endDate } : null + + + const { data, loading, error } = useSearchAndFilterProductsQuery({ + variables: { + keyword: keyword || null, + dateRangeInput: dateRangeInput + }, }); + console.log("data in search", data) + if (loading) return

Loading...

; if (error) return

Error: {error.message}

; return (
- {data?.searchProducts.length === 0 ? ( + {data?.searchAndFilterProducts.length === 0 ? (

Aucun produit trouvé

) : ( - data?.searchProducts.map((product) => ( -
+ data?.searchAndFilterProducts.map((product) => ( + -
+ )) )}
diff --git a/frontend/src/pages/search/SearchError.tsx b/frontend/src/pages/search/SearchError.tsx deleted file mode 100644 index 6142a37..0000000 --- a/frontend/src/pages/search/SearchError.tsx +++ /dev/null @@ -1,9 +0,0 @@ -function SearchError() { - return ( -
-

Rentrez un mot clé pour effectuer une recherche

-
- ); -} - -export default SearchError; diff --git a/frontend/src/pages/search/[searchKeywords].tsx b/frontend/src/pages/search/[searchKeywords].tsx index 6754ef1..89ffba0 100644 --- a/frontend/src/pages/search/[searchKeywords].tsx +++ b/frontend/src/pages/search/[searchKeywords].tsx @@ -1,15 +1,18 @@ -import { useParams } from "react-router-dom"; -import SearchError from "./SearchError"; +import { useNavigate, useParams } from "react-router-dom"; +import { useEffect } from "react"; import Search from "./Search"; const SearchPage = () => { const { keyword } = useParams<{ keyword?: string }>(); + const navigate = useNavigate(); - if (!keyword) { - return ; - } + useEffect(() => { + if (!keyword && !localStorage.getItem("startDate") && !localStorage.getItem("endDate")) { + navigate("/"); + } + }, [keyword, navigate]); - return ; + return ; }; export default SearchPage;