Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
329d10e
feat(#146): permet d'utiliser différentes URL de base en fonction de …
romanbeldent Sep 23, 2025
05d5af8
Feature/146 (#154)
laetitia-piat Sep 24, 2025
8b9ecbc
Feat/115 page admin inventaire (#153)
annieccar Sep 24, 2025
d913c6d
Feat/141 modif navbar admin (#157)
annieccar Sep 24, 2025
03a23d5
feat: Add hero section on home page
annieccar Sep 24, 2025
d9c4f5e
Revert "feat: Add hero section on home page"
annieccar Sep 24, 2025
3327a78
feat: Add hero section on home page-2 (#158)
annieccar Sep 24, 2025
8eb4b9f
Fix/layout and card aspect (#160)
annieccar Sep 25, 2025
97e589e
Feat/#45 not found page and ux (#159)
ThomGateau Sep 25, 2025
2cce7c5
fix: supprime l'entité ForgotPassword non utilisé
romanbeldent Sep 25, 2025
51c72da
chore: supprime des commentaires dans le fichier db.ts
romanbeldent Sep 25, 2025
6ccd8f7
fix: supprime l'entité PictureInput qui ne sert pas
romanbeldent Sep 25, 2025
3ac9560
feat(#135): permet d'effectuer le paiment pour une commande
laetitia-piat Sep 25, 2025
ee17e9b
Merge branch 'dev' of https://github.com/WildCodeSchool/wild-rent int…
laetitia-piat Sep 25, 2025
f125e1d
Feat/161 ensure product availability on date change (#165)
annieccar Sep 25, 2025
861f2c3
Merge branch 'dev' of https://github.com/WildCodeSchool/wild-rent int…
laetitia-piat Sep 25, 2025
aafbc29
input picture
laetitia-piat Sep 25, 2025
9e5fe26
Merge branch 'staging' into dev
laetitia-piat Sep 25, 2025
ebe3d6c
modifie query en mutation (#168)
laetitia-piat Sep 26, 2025
da7080b
Fix/add missing security on be route (#169)
annieccar Sep 26, 2025
8d76c84
chore(deps-dev): bump @playwright/test from 1.55.0 to 1.56.0 in /e2e …
dependabot[bot] Oct 19, 2025
92610e4
Update ProductResolver.ts
laetitia-piat Oct 19, 2025
0d9766b
Version playwright
laetitia-piat Oct 19, 2025
a46f06b
fix builder
laetitia-piat Oct 19, 2025
f29373f
Fix conflicts with staging (#179)
annieccar Oct 21, 2025
53a1f23
Merge branch 'staging' into dev
annieccar Oct 21, 2025
3845b22
rebase staging in dev (keep dev changes)
annieccar Oct 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions backend/src/entities/ProductOption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,4 @@ export class ProductOption extends BaseEntity {
)
orders: ProductInOrder[];

// Ce champ sert seulement à pouvoir renvoyer la valeur de available_quantity qui est calculée dans un resolver
@Field(() => Number)
availableQuantity: number;
}
8 changes: 7 additions & 1 deletion backend/src/inputs/UserInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class UpdateUserInput {
@InputType()
export class UpdateOrCreateUserInput {
@Field({ nullable: true })
id: number;
userId: number;

@Field()
first_name: string;
Expand Down Expand Up @@ -102,4 +102,10 @@ export class ForgottenPasswordRequestInput {
@Field()
@IsEmail()
email: string;
}

@InputType()
export class DeleteUserInput {
@Field()
userId!: number;
}
83 changes: 13 additions & 70 deletions backend/src/resolvers/InventoryResolver.ts
Original file line number Diff line number Diff line change
@@ -1,86 +1,27 @@
import { ProductOption } from "../entities/ProductOption";
import { ProductInOrder } from "../entities/ProductInOrder";

import { Resolver, Query, Arg } from "type-graphql";
import { Resolver, Query, Arg, Ctx } from "type-graphql";
import { OptionAvailability, OptionInventory } from "../entities/Inventory";
import { GraphQLError } from "graphql";
import { getInventoryByOptionsService } from "../services/InventoryService";

@Resolver()
export class InventoryResolver {
@Query(() => [OptionInventory])
async getInventoryByOptions(
@Arg("startDate") startDate: string,
@Arg("endDate") endDate: string,
@Arg("productId", { nullable: true }) productId?: number
@Arg("productId", { nullable: true }) productId?: number,
@Ctx() context?: any,
) {
const getAllDates = (startDate: string, endDate: string) => {
const allDates = [];
let currentDate = new Date(startDate);
const lastDate = new Date(endDate);
while (currentDate <= lastDate) {
allDates.push(new Date(currentDate));
currentDate.setDate(currentDate.getDate() + 1);
}
return allDates;
};

const allDates = getAllDates(startDate, endDate);

let productOptions;
let reservations;

if (productId) {
productOptions = await ProductOption.find({
where: { id: productId },
relations: ["product"],
});
reservations = await ProductInOrder.find({
where: { productOption: { id: productId } },
relations: ["productOption.product"],
});
} else {
productOptions = await ProductOption.find({ relations: ["product"] });
reservations = await ProductInOrder.find({
relations: ["productOption.product"],
});
}

let inventory = [];

for (const option of productOptions) {
const optionInventory: OptionInventory = {
id: option.id,
product: option.product.name,
category: option.product.category,
option: option.size,
totalQty: option.total_quantity,
reservations: [],
};
for (const date of allDates) {
const reservedItems = reservations.filter((item) => {
return (
item.productOption.id === option.id &&
new Date(item.order.rental_start_date) <= date &&
new Date(item.order.rental_end_date) >= date
);
});

if (reservedItems.length > 0) {
const reservedQty = reservedItems.reduce(
(sum, item) => sum + item.quantity,
0
);
const availableQty = option.total_quantity - reservedQty;
optionInventory.reservations.push({
date,
reservedQty,
availableQty,
});
if(!productId){
if(context.user.role !== "ADMIN"){
throw new GraphQLError("Only admins can query all inventory", {
extensions: { code: "FORBIDDEN" },
});
}
}
inventory.push(optionInventory);
}

return inventory;
return getInventoryByOptionsService(startDate, endDate, productId)
}

@Query(() => OptionAvailability)
Expand Down Expand Up @@ -115,4 +56,6 @@ export class InventoryResolver {
availableQty: minAvailableQty,
};
}


}
4 changes: 2 additions & 2 deletions backend/src/resolvers/PaymentResolver.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Arg, Field, InputType, Int, Query, Resolver } from "type-graphql";
import { Arg, Field, InputType, Int, Mutation, Resolver } from "type-graphql";

import { GraphQLJSON } from "graphql-scalars";
import paymentServices from "../services/paymentServices";
Expand All @@ -25,7 +25,7 @@ export default class PaymentResolver {
*? retourne énormément d'informations, impossible de tout couvrir
*? facilement avec un type custom à nous
*------------------------**/
@Query(() => GraphQLJSON)
@Mutation(() => GraphQLJSON)
async createCheckoutSession(
@Arg("data", () => [ProductForSessionInput]) data: ProductForSessionInput[]
) {
Expand Down
138 changes: 65 additions & 73 deletions backend/src/resolvers/ProductResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { Category } from "../entities/Category";
import { FindManyOptions, In, Raw } from "typeorm";
import { merge } from "../assets/utils";
import { Tag } from "../entities/Tag";
import { ProductInOrder } from "../entities/ProductInOrder";
//import { ProductInOrder } from "../entities/ProductInOrder";
import { getInventoryByOptionsService } from "../services/InventoryService";

@Resolver(Product)
export class ProductResolver {
Expand Down Expand Up @@ -77,9 +78,10 @@ export class ProductResolver {
.andWhere("product.price <= :maxPrice", { maxPrice: maxPrice })
.andWhere("product.price >= :minPrice", { minPrice: minPrice });


if (keyword.length > 0) {
queryBuilder.andWhere("product.name ILIKE :keyword", { keyword: `%${keyword}%` });
queryBuilder.andWhere("product.name ILIKE :keyword", {
keyword: `%${keyword}%`,
});
}

if (tags && tags.length > 0) {
Expand All @@ -97,112 +99,102 @@ export class ProductResolver {
@Arg("endDate") endDate: Date,
@Arg("categoryId", { nullable: true }) categoryId?: number,
@Arg("keyword", { nullable: true }) keyword?: string,
@Arg("minPrice", { nullable: true }) minPrice?: number,
@Arg("maxPrice", { nullable: true }) maxPrice?: number,
@Arg("tags", () => [String], { nullable: true }) tags?: string[],
@Arg("productId", {nullable: true}) productId?:number,
@Arg("minPrice", { nullable: true }) minPrice?: number,
@Arg("maxPrice", { nullable: true }) maxPrice?: number,
@Arg("tags", () => [String], { nullable: true }) tags?: string[],
@Arg("productId", { nullable: true }) productId?: number
) {



const queryBuilder = ProductOption.createQueryBuilder("po")
.leftJoinAndSelect("po.product", "product")
.leftJoinAndSelect("product.category", "category")
.leftJoinAndSelect("product.tags", "tag")
.leftJoinAndSelect("product.pictures", "pictures");

// Filtre par catégorie
const queryBuilder = ProductOption.createQueryBuilder("po")
.leftJoinAndSelect("po.product", "product")
.leftJoinAndSelect("product.category", "category")
.leftJoinAndSelect("product.tags", "tag")
.leftJoinAndSelect("product.pictures", "pictures");
// Filtre par catégorie
if (categoryId) {
queryBuilder.andWhere("category.id = :categoryId", {categoryId});
queryBuilder.andWhere("category.id = :categoryId", { categoryId });
}
// Filtre par mot-clé
if (keyword) {
queryBuilder.andWhere("product.name ILIKE :keyword", { keyword: `%${keyword}%` });
queryBuilder.andWhere("product.name ILIKE :keyword", {
keyword: `%${keyword}%`,
});
}
// Filtre par prix
if(maxPrice){
queryBuilder.andWhere("product.price <= :maxPrice", { maxPrice: maxPrice })
if (maxPrice) {
queryBuilder.andWhere("product.price <= :maxPrice", {
maxPrice: maxPrice,
});
}
if(minPrice){
queryBuilder.andWhere("product.price >= :minPrice", { minPrice: minPrice })
if (minPrice) {
queryBuilder.andWhere("product.price >= :minPrice", {
minPrice: minPrice,
});
}
// Filtre par tags
if (tags && tags.length > 0) {
queryBuilder.andWhere("tag.label IN (:...tags)", { tags });
}

// Filtre par product ID
// Filtre par product ID
if (productId) {
queryBuilder.andWhere("product.id = :productId", { productId: productId });
queryBuilder.andWhere("product.id = :productId", {
productId: productId,
});
}

// Objectif seul de récupérer la donnée "reserved_quantity" et de la sauvegarder à l'aide du addSelect
queryBuilder.addSelect(subQuery => {
return subQuery
.select("COALESCE(SUM(pio.quantity), 0)")
.from(ProductInOrder, "pio")
.leftJoin("pio.order", "o")
.where("pio.productOptionId = po.id")
.andWhere("o.rental_start_date <= :endDate")
.andWhere("o.rental_end_date >= :startDate")
}, "reserved_quantity");

// Filtre uniquement les products options qui sont disponibles pour les dates et options sélectionnées
queryBuilder.andWhere(qb => {
const reservedQty = qb.subQuery()
.select("SUM(pio.quantity)")
.from(ProductInOrder, "pio")
.leftJoin("pio.order", "o")
.where("pio.productOptionId = po.id")
.andWhere("o.rental_start_date <= :endDate")
.andWhere("o.rental_end_date >= :startDate")
.andWhere("o.status != 'CANCELLED'")
.getQuery();

return `po.total_quantity - COALESCE((${reservedQty}), 0) > 0`;
});
const productOptions = await queryBuilder.getMany();

const inventoryForDates = await getInventoryByOptionsService(
startDate.toISOString().split("T")[0],
endDate.toISOString().split("T")[0]
);

queryBuilder.setParameters({startDate, endDate});

// Récupère les ProductOptions avec la quantité disponible
const result = await queryBuilder.getRawAndEntities();
const unavailableProducts = inventoryForDates.filter((item) => {
if (!item.reservations || item.reservations.length === 0) {
return false;
}
const maxReserved = Math.max(
...item.reservations.map((r) => r.reservedQty)
);
return maxReserved >= item.totalQty;
});

const productOptionWithAvailableQty = result.entities.map(entity=>{
const rawForThisEntity = result.raw.find(r => r.po_id === entity.id)
const reserved = rawForThisEntity ? Number(rawForThisEntity.reserved_quantity) : 0
const availableProductOptions = productOptions.filter((option) => {
return !unavailableProducts.find((item) => item.id === option.id);
});

return {
...entity,
availableQuantity: entity.total_quantity - reserved
}
})
return productOptionWithAvailableQty
return availableProductOptions;
}

@Query(()=> [Product])
@Query(() => [Product])
async getAvailableProductForDates(
@Arg("startDate") startDate: Date,
@Arg("endDate") endDate: Date,
@Arg("categoryId", { nullable: true }) categoryId?: number,
@Arg("keyword", { nullable: true }) keyword?: string,
@Arg("minPrice", { nullable: true }) minPrice?: number,
@Arg("maxPrice", { nullable: true }) maxPrice?: number,
@Arg("minPrice", { nullable: true }) minPrice?: number,
@Arg("maxPrice", { nullable: true }) maxPrice?: number,
@Arg("tags", () => [String], { nullable: true }) tags?: string[]
) {
console.log("args:", startDate, endDate, categoryId, keyword, minPrice, maxPrice, tags)

const availableProductOptions = await this.getAvailableProductOptions(startDate, endDate, categoryId, keyword, minPrice, maxPrice, tags);
const availableProductOptions = await this.getAvailableProductOptions(
startDate,
endDate,
categoryId,
keyword,
minPrice,
maxPrice,
tags
);

// Extrait les Products disponibles à partir des products Options dispo
let availableProducts: Product[] = [];

availableProductOptions.forEach(option => {
availableProductOptions.forEach((option) => {
const product = option.product;
if (!availableProducts.some((item)=> item.id === product.id )) {
if (!availableProducts.some((item) => item.id === product.id)) {
availableProducts.push(product);
}
});

return availableProducts;
}

Expand Down
24 changes: 12 additions & 12 deletions backend/src/resolvers/TempUserResolver.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { IsAdmin } from "../middleware/AuthChecker";
import { TempUser } from "../entities/TempUser";
import { Arg, Ctx, Mutation, Query, Resolver } from "type-graphql";
import { Arg, Mutation, Query, Resolver, UseMiddleware } from "type-graphql";

@Resolver(TempUser)
export class TempUserResolver {

@Query(() => [TempUser])
@UseMiddleware(IsAdmin)
async getAllTempUsers( ) {
const tempUsers = await TempUser.find( {order: {
id: "ASC",
Expand All @@ -13,17 +15,15 @@ export class TempUserResolver {
return tempUsers;
}

@Mutation(() => String)
async deleteTempUser(@Arg("id") id: number, @Ctx() context: any) {
if(context.user.role !== "ADMIN" ){
throw new Error("Unauthorized")
}
const result = await TempUser.delete(id);
if (result.affected === 1) {
return "L'utilisateur a bien été supprimé";
} else {
throw new Error("L'utilisateur n'a pas été trouvé");
}
@Mutation(() => String)
@UseMiddleware(IsAdmin)
async deleteTempUser(@Arg("id") id: number) {
const result = await TempUser.delete(id);
if (result.affected === 1) {
return "L'utilisateur a bien été supprimé";
} else {
throw new Error("L'utilisateur n'a pas été trouvé");
}
}

}
Loading