Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
npm run husky-lint

# si le paramètre bypass-bin est pas actif
# bypass_bin=1 git commit
if [ "$bypass_bin" != "1" ]; then
# Bloquer toute modification du dossier bin/
if git diff --cached --name-only | grep -q "^bin/"; then
Expand Down
14 changes: 3 additions & 11 deletions backend/src/entities/GroupMember.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,9 @@ export class GroupMember extends BaseEntity {
@Field()
isGroupAdmin: boolean;

@Column({ nullable: true })
@Field({ nullable: true })
firstName?: string;

@Column({ nullable: true })
@Field({ nullable: true })
lastName?: string;

@Column({ nullable: true })
@Field({ nullable: true })
email?: string;
@Column({ type: "timestamptz", default: () => "CURRENT_TIMESTAMP" })
@Field()
lastTempstampVu: Date;

@ManyToOne(
() => User,
Expand Down
105 changes: 101 additions & 4 deletions backend/src/resolvers/MessageResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Resolver,
UseMiddleware,
} from "type-graphql";
import { LessThan } from "typeorm";
import Group from "../entities/Group";
import { GroupMember } from "../entities/GroupMember";
import { Message } from "../entities/Message";
Expand Down Expand Up @@ -39,6 +40,39 @@ class GroupMessagesOutput {

@Field(() => [Message])
messages: Message[];

@Field()
lastTempstampVu: Date;
}

@InputType()
class GetLazyMessagesInput {
@Field()
groupId: number;

@Field()
oldTimestamp: string;
}

@ObjectType()
class GetLazyMessagesOutput {
@Field()
isMaximumMessages: boolean;

@Field(() => [Message])
messages: Message[];
}

@InputType()
class SetLastMessageVuInput {
@Field()
groupId: number;
}

@ObjectType()
class SetLastMessageVuOutput {
@Field()
sucess: boolean;
}

@Resolver(Group)
Expand All @@ -60,17 +94,17 @@ export default class MessageResolver {
order: { id: "DESC" },
});

const reponse: { groupId: number; messages: Message[] }[] = [];
const reponse: { groupId: number; messages: Message[]; lastTempstampVu: Date }[] = [];

// charge les 10 derniers messages de chaque groupe
// charge les 40 derniers messages de chaque groupe
for (const group of groups) {
const messages = await Message.find({
where: { group: { id: group.id } },
relations: { user: true },
order: { createdAt: "DESC" },
take: 20,
take: 40,
});
reponse.push({ groupId: group.id, messages });
reponse.push({ groupId: group.id, messages, lastTempstampVu: group.groupMember[0].lastTempstampVu });
}

return reponse;
Expand Down Expand Up @@ -112,4 +146,67 @@ export default class MessageResolver {
relations: ["user", "group"],
});
}

@UseMiddleware(RoleMiddleware())
@Query(() => GetLazyMessagesOutput)
async getLazyMessages(@Arg("data") data: GetLazyMessagesInput) {
const { groupId, oldTimestamp } = data;

let oldDate: Date;
try {
oldDate = new Date(oldTimestamp);
if (Number.isNaN(oldDate.getTime())) {
throw new Error("Invalid date");
}
} catch {
throw new Error("Invalid date format");
}

// récupère les 40 premiers messages plus anciens que oldTimestamp
const messages = await Message.find({
where: { group: { id: groupId }, createdAt: LessThan(oldDate) },
relations: { user: true },
order: { createdAt: "DESC" },
take: 40,
});

const oldestMessage = await Message.findOne({
where: { group: { id: groupId } },
order: { createdAt: "ASC" },
select: { id: true, createdAt: true },
});

const isMaximumMessages = oldestMessage ? messages.some((msg) => msg.id === oldestMessage.id) : true;

// renvoie les messages et si c'est tout les messages
return { isMaximumMessages, messages: messages };
}

// met a jour le vu du dernier message pour un groupe donné
@UseMiddleware(RoleMiddleware())
@Mutation(() => SetLastMessageVuOutput)
async setLastMessageVu(@Arg("data") data: SetLastMessageVuInput, @Ctx() ctx: ContextType) {
const { groupId } = data;

const userId = ctx.user?.id;
if (!userId) throw new Error("Utilisateur non authentifié");

// récupère le dernier message du groupe
const lastMessage = await Message.findOne({
where: { group: { id: groupId } },
order: { createdAt: "DESC" },
});

if (lastMessage) {
// met a jour le vu du dernier message pour l'utilisateur
await GroupMember.update(
{ user: { id: userId }, group: { id: groupId } },
{ lastTempstampVu: lastMessage.createdAt },
);

return { sucess: true };
}

return { sucess: false };
}
}
5 changes: 3 additions & 2 deletions backend/src/resolvers/UserResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class UpdateMyProfileInput {
@Field()
date_of_birth!: string;
@Field()
phone_number!: string;
phone_number?: string;
@Field(() => String, { nullable: true })
pictureBase64?: string;
}
Expand Down Expand Up @@ -261,13 +261,14 @@ export default class UserResolver {
password: undefined,
image_url: urlImage ? urlImage.data.url : undefined,
pictureBase64: undefined,
phone_number: data.phone_number ?? undefined,
};

// modifie l'utilisateur connecté
await User.update({ id: ctx.user.id }, newData);

//récupère le profil de l'utilisateur connecté
const user = await User.findOne({ where: { id: ctx.user.id } });
const user = await User.findOne({ where: { id: ctx.user.id }, relations: ["lists"] });

return user as User;
}
Expand Down
119 changes: 119 additions & 0 deletions bin/dockerTools.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/bin/bash
#chmod +x create.sh

# arrete le script avec des sécurités
set -euo pipefail

# affiche le menu de sélection d'une liste d'options passées en argument
# $1 titre, $2 options
input_select() {
local selected=0
local titre="$1"
local -n OPTIONS="$2"
local key

draw_menu() {
clear
echo "$titre"
echo

for i in "${!OPTIONS[@]}"; do
if [[ $i -eq $selected ]]; then
echo -e " 🟢 \e[32m${OPTIONS[$i]}\e[0m"
else
echo -e " ⚫ \e[31m${OPTIONS[$i]}\e[0m"
fi
done
}

set +e
while true; do
draw_menu

# Lit une touche sans affichage
IFS= read -rsn1 key

# Flèches = séquence ESC + [A/[B
if [[ $key == $'\x1b' ]]; then
IFS= read -rsn2 key
case "$key" in
"[A") ((selected--)) ;; # haut
"[B") ((selected++)) ;; # bas
esac
elif [[ $key == "" ]]; then
# Entrée
break
fi

# si on dépasse les bornes, on revient au début/fin
(( selected < 0 )) && selected=$((${#OPTIONS[@]} - 1))
(( selected >= ${#OPTIONS[@]} )) && selected=0
done

set -e
menu_selected=$selected
# option selectionnée efface le menu
clear
return 0
}

mapfile -t CONTAINERS < <(docker ps -a --format "{{.Names}}")
CONTAINERS+=("Tous les conteneurs")

input_select "Quelle contenaire choisir" CONTAINERS
selectedNbPages=$menu_selected
contenaire=("${CONTAINERS[$selectedNbPages]}")

# action demander
optionAction=(
"logs -f"
"start"
"stop"
"restart"
"exec -it bash"
)
input_select "Quelle est action que vous voulez effectuer sur $contenaire" optionAction
indexSelectedAction=$menu_selected
action=("${optionAction[$indexSelectedAction]}")

# exécuter la commande docker
if [ "$contenaire" == "Tous les conteneurs" ]; then
chemainCompose="./compose.dev.yaml"
# log du docker compose
if [ "$action" == "logs -f" ]; then
docker-compose -f "$chemainCompose" logs -f
elif [ "$action" == "start" ]; then
docker compose --env-file .env.dev -f "$chemainCompose" up -d --build
elif [ "$action" == "stop" ]; then
docker compose --env-file .env.dev -f "$chemainCompose" down
elif [ "$action" == "restart" ]; then
docker compose --env-file .env.dev -f "$chemainCompose" down && docker compose --env-file .env.dev -f "$chemainCompose" up -d --build
elif [ "$action" == "exec -it bash" ]; then
echo "⚠️ Impossible d'exécuter 'exec -it bash' sur tous les conteneurs en même temps."
else
echo "⚠️ Action non reconnue."
fi
else
# log du docker compose
if [ "$action" == "logs -f" ]; then
docker logs -f "$contenaire"

elif [ "$action" == "start" ]; then
docker start "$contenaire"
echo "✅ Le contenaire "$contenaire" a démarré."

elif [ "$action" == "stop" ]; then
docker stop "$contenaire"
echo "✅ Le contenaire "$contenaire" a été arrêté."

elif [ "$action" == "restart" ]; then
docker restart "$contenaire"
echo "✅ Le contenaire "$contenaire" a été redémarré."

elif [ "$action" == "exec -it bash" ]; then
docker exec -it "$contenaire" sh

else
echo "⚠️ Action non reconnue."
fi
fi
2 changes: 1 addition & 1 deletion frontend/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="fr">

<head>
<meta charset="UTF-8" />
Expand Down
24 changes: 16 additions & 8 deletions frontend/src/components/groups/Groups.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { GetAllMyGroupsQuery } from "../../generated/graphql-types";
import type { GetAllMyGroupsQuery } from "../../graphql/generated/graphql-types";
import { useToggle } from "../../hooks/useToggle";
import type { Message } from "../../types/Message";
import { formatDate } from "../../utils/dateCalculator";
import CreateGroupForm from "../forms/CreateGroupForm";
import Button from "../utils/Button";
Expand All @@ -13,15 +14,20 @@ type GroupsProps = {
loading: boolean;
error?: string;
onClick?: () => void;
messages: Record<number, Message[]>;
getNbNewMessages: (groupId: number, messages: Message[]) => number;
updateLastVu: (groupId: number, date: Date | string, serveurSyconization?: boolean) => void;
};

export default function Groups({ groups, setActiveGroup, loading, error }: GroupsProps) {
// const [isOpen, setIsOpen] = useState(false);

// function toggleModal() {
// setIsOpen(!isOpen);
// }

export default function Groups({
groups,
setActiveGroup,
loading,
error,
messages,
getNbNewMessages,
updateLastVu,
}: GroupsProps) {
const createGroupModal = useToggle(false);
const closeCreateGroupModal = () => {
createGroupModal.close();
Expand All @@ -47,7 +53,9 @@ export default function Groups({ groups, setActiveGroup, loading, error }: Group
title={group.name}
onClick={() => {
setActiveGroup?.(group);
updateLastVu(Number(group.id), messages[Number(group.id)][0].createdAt);
}}
nbNewMessages={getNbNewMessages(Number(group.id), messages[Number(group.id)] || [])}
>
<p className="text-gray-600 text-sm sm:text-base truncate overflow-hidden text-ellipsis whitespace-nowrap">
<span> Date limite: {formatDate(new Date(group.deadline))} </span> <br />
Expand Down
Loading