diff --git a/client/src/graphQL/mutations/events.ts b/client/src/graphQL/mutations/events.ts
new file mode 100644
index 0000000..b4896d5
--- /dev/null
+++ b/client/src/graphQL/mutations/events.ts
@@ -0,0 +1,59 @@
+import { gql } from "@apollo/client";
+
+export const DELETE_EVENT = gql`
+ mutation DeleteEvent($eventId: Float!) {
+ deleteEvent(eventId: $eventId)
+ }
+`;
+
+export const UPDATE_EVENT = gql`
+ mutation UpdateEvent(
+ $eventId: Float!,
+ $trainerId: Float,
+ $serviceId: Float,
+ $date: DateTime,
+ $title: String,
+ $description: String,
+ $location: LocationInput,
+ $group_max_size: Float,
+ $price: Float
+ ) {
+ updateEvent(
+ eventId: $eventId,
+ trainerId: $trainerId,
+ serviceId: $serviceId,
+ date: $date,
+ title: $title,
+ description: $description,
+ location: $location,
+ group_max_size: $group_max_size,
+ price: $price
+ ) {
+ id
+ date
+ title
+ description
+ location {
+ address
+ city
+ postal_code
+ latitude
+ longitude
+ }
+ group_max_size
+ price
+ trainer {
+ id
+ user {
+ id
+ firstname
+ lastname
+ }
+ }
+ service {
+ id
+ name
+ }
+ }
+ }
+`;
diff --git a/client/src/graphQL/queries/event.ts b/client/src/graphQL/queries/event.ts
new file mode 100644
index 0000000..3fff180
--- /dev/null
+++ b/client/src/graphQL/queries/event.ts
@@ -0,0 +1,33 @@
+import { gql } from "@apollo/client";
+
+export const GET_ALL_EVENTS = gql`
+ query GetAllEvents {
+ getAllEvents {
+ id
+ date
+ title
+ description
+ group_max_size
+ location {
+ latitude
+ longitude
+ }
+ }
+ }
+`;
+
+export const GET_EVENT_BY_ID = gql`
+ query GetEventById($eventId: Float!) {
+ getEventById(eventId: $eventId) {
+ id
+ date
+ title
+ description
+ location {
+ latitude
+ longitude
+ }
+ group_max_size
+ }
+ }
+`;
diff --git a/client/src/main.tsx b/client/src/main.tsx
index 17d135e..f206ed1 100644
--- a/client/src/main.tsx
+++ b/client/src/main.tsx
@@ -31,6 +31,7 @@ import Homepage from "@/pages/Homepage/Homepage.tsx";
import Login from "@/pages/Login/Login.tsx";
import NewPassword from "./pages/Login/NewPassword.tsx";
import Planning from "./pages/Planning/Planning.tsx";
+import EventDetail from "./pages/Events/EventDetail.tsx";
import Profile from "@/pages/Profile/Profile.tsx";
import Registration from "./pages/Registration/Registration.tsx";
import ResetLink from "./pages/Login/ResetLink.tsx";
@@ -163,7 +164,7 @@ const router = createBrowserRouter([
},
{
path: ":id",
- element: ,
+ element: ,
},
],
},
diff --git a/client/src/pages/Events/EventDetail.tsx b/client/src/pages/Events/EventDetail.tsx
new file mode 100644
index 0000000..7cb2223
--- /dev/null
+++ b/client/src/pages/Events/EventDetail.tsx
@@ -0,0 +1,38 @@
+import { useParams } from "react-router-dom";
+import { useQuery } from "@apollo/client";
+import { GET_EVENT_BY_ID } from "@/graphQL/queries/event";
+
+function EventDetail() {
+ const { id } = useParams();
+ const { data, loading, error } = useQuery(GET_EVENT_BY_ID, {
+ variables: { eventId: Number(id) },
+ });
+
+ if (loading) return
Chargement de l'événement...
;
+ if (error) return Erreur : {error.message}
;
+
+ const event = data?.getEventById;
+
+ return (
+
+
{event.title}
+
+
Description : {event.description}
+
Date : {new Date(event.date).toLocaleString()}
+
Taille max du groupe : {event.group_max_size}
+
+
Latitude : {event.location.latitude}
+
Longitude : {event.location.longitude}
+
+
+
+
+ );
+}
+
+export default EventDetail;
diff --git a/client/src/pages/Planning/Planning.scss b/client/src/pages/Planning/Planning.scss
index 98877fa..13070b7 100644
--- a/client/src/pages/Planning/Planning.scss
+++ b/client/src/pages/Planning/Planning.scss
@@ -1,10 +1,72 @@
@import '@style';
-/* Container */
-
.calendar-container {
- margin: 20px;
- margin-top: 30px;
+ padding: 20px;
+
+ /* Under -> Allow to modify element per view */
+
+ /* dayGridMonth */
+ .event-content-month {
+ padding: 2px;
+
+ .event-title {
+ font-size: $s;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-weight: $semiBold;
+ color: $primary-dark;
+
+ }
+
+ .fc-v-event {
+ background-color: $green-300;
+ width: 100%;
+ word-break: normal;
+ }
+ }
+
+ /* timeGridWeek */
+ .event-content-week {
+ padding: 4px;
+
+ .event-title {
+ font-size: 1em;
+ font-weight: bold;
+ margin-bottom: 4px;
+ }
+
+ .event-description {
+ font-size: 0.85em;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+
+ /* listWeek */
+ .event-content-list {
+ padding: 8px;
+
+ .event-title {
+ font-size: 1.1em;
+ font-weight: bold;
+ margin-bottom: 6px;
+ }
+
+ .event-description {
+ font-size: 0.9em;
+ margin-bottom: 6px;
+ }
+
+ .event-details {
+ font-size: 0.85em;
+ color: #666;
+
+ .event-location {
+ margin-top: 4px;
+ }
+ }
+ }
}
.fc-theme-standard .fc-scrollgrid {
@@ -34,7 +96,7 @@
&:first-child {
border-top-left-radius: $atoms-radius !important;
}
-
+
&:last-child {
border-top-right-radius: $atoms-radius !important;
}
@@ -44,7 +106,7 @@
&:first-child {
border-bottom-left-radius: $atoms-radius !important;
}
-
+
&:last-child {
border-bottom-right-radius: $atoms-radius !important;
}
@@ -374,7 +436,8 @@
padding: 0.5rem !important;
}
- .fc td, .fc th {
+ .fc td,
+ .fc th {
border-style: none;
}
}
\ No newline at end of file
diff --git a/client/src/pages/Planning/Planning.tsx b/client/src/pages/Planning/Planning.tsx
index 80e4b22..cb7add4 100644
--- a/client/src/pages/Planning/Planning.tsx
+++ b/client/src/pages/Planning/Planning.tsx
@@ -2,9 +2,13 @@ import "@/pages/Planning/Planning.scss";
import PlanningHeader from "@/components/_molecules/PlanningHeader/PlanningHeader.tsx";
+import { useNavigate } from "react-router-dom";
import { useUser } from "@/hooks/useUser";
import { useIsMobile } from "@/hooks/checkIsMobile";
import { useState } from "react";
+import { useQuery } from "@apollo/client";
+import { GET_ALL_EVENTS } from "@/graphQL/queries/event";
+
import FullCalendar from "@fullcalendar/react";
import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid";
@@ -12,10 +16,74 @@ import listPlugin from "@fullcalendar/list";
import frLocale from "@fullcalendar/core/locales/fr";
import interactionPlugin from "@fullcalendar/interaction";
-function PlanningTrainer() {
- const [currentView] = useState("dayGridMonth");
+interface Event {
+ id: number;
+ date: string;
+ title: string;
+ description: string;
+ group_max_size: number;
+ location: Location;
+}
+
+interface Location {
+ latitude: number;
+ longitude: number;
+}
+
+interface GetAllEventsData {
+ getAllEvents: Event[];
+}
+
+function Planning() {
+ /* Business logic */
+ const navigate = useNavigate();
const { user } = useUser();
+ const { data } = useQuery(GET_ALL_EVENTS);
+
+ const events =
+ data?.getAllEvents.map((event: Event) => ({
+ id: event.id.toString(),
+ title: event.title,
+ start: new Date(event.date),
+ description: event.description,
+ extendedProps: {
+ group_max_size: event.group_max_size,
+ location: event.location,
+ },
+ })) || [];
+
+ /* FullCalendar views */
+
+ const [currentView, setCurrentView] = useState("dayGridMonth");
+
+ // Allow to generate differents titles in function of view for PlanningHeader
+ const getPlanningHeaderProps = () => {
+ switch (currentView) {
+ case "dayGridMonth":
+ return {
+ title: "Planning Mensuel",
+ };
+
+ case "timeGridWeek":
+ return {
+ title: "Planning Hebdomadaire",
+ };
+
+ case "listWeek":
+ return {
+ title: "Liste des Événements",
+ };
+
+ default:
+ return {
+ title: "Planning",
+ };
+ }
+ };
+
+ // Initialize the props to change for PlanningHeader
+ const { title } = getPlanningHeaderProps();
const isMobile = useIsMobile();
@@ -34,7 +102,7 @@ function PlanningTrainer() {
<>
{user?.role === "trainer" && (
@@ -51,6 +119,7 @@ function PlanningTrainer() {
headerToolbar={isMobile ? mobileToolbar : desktopToolbar}
locale={frLocale}
height="auto"
+ events={events}
views={{
dayGridMonth: { buttonText: "Mois" },
timeGridWeek: { buttonText: "Semaine" },
@@ -59,10 +128,70 @@ function PlanningTrainer() {
buttonText={{
today: "Aujourd'hui",
}}
+ datesSet={(arg) => {
+ setCurrentView(arg.view.type);
+ }}
+ eventContent={(arg) => {
+ // View Month
+ if (currentView === "dayGridMonth") {
+ return (
+
+ );
+ }
+ // View Week
+ if (currentView === "timeGridWeek") {
+ return (
+
+
{arg.event.title}
+
+ {arg.event.extendedProps.description}
+
+
+ );
+ }
+ // View list of Events
+ if (currentView === "listWeek") {
+ return (
+
+
{arg.event.title}
+
+ {arg.event.extendedProps.description}
+
+
+
+ Taille max. du groupe :{" "}
+ {arg.event.extendedProps.group_max_size}
+
+
+ Coordonnées : {arg.event.extendedProps.location.latitude},
+ {arg.event.extendedProps.location.longitude}
+
+
+
+ );
+ }
+ }}
+ // Navigate to event details where we can update or delete the event
+ eventClick={(clickInfo) => {
+ // Get event id to use it in the path
+ const eventId = clickInfo.event.id;
+ const userRole = user?.role;
+ // Navigate to the event in function of the role
+ if (userRole === "trainer") {
+ navigate(`/trainer/planning/my-events/${eventId}`);
+ } else if (userRole === "owner") {
+ navigate(`/owner/planning/${eventId}`);
+ } else {
+ // If unauthorized user try to force the URL
+ console.error("Vous n'êtes pas autorisé à voir cet événement");
+ }
+ }}
/>
>
);
}
-export default PlanningTrainer;
+export default Planning;
diff --git a/server/src/dataSource/dataSource.ts b/server/src/dataSource/dataSource.ts
index f40b222..01e5ae7 100644
--- a/server/src/dataSource/dataSource.ts
+++ b/server/src/dataSource/dataSource.ts
@@ -11,6 +11,6 @@ export const dataSource = new DataSource({
// TypeORM configuration
entities: ["src/entities/*.ts"],
- synchronize: true,
+ synchronize: false,
logging: true, // FIXME: delete this line in production
});
diff --git a/server/src/entities/Coordinates.ts b/server/src/entities/Coordinates.ts
index bcf1723..e0a43f9 100644
--- a/server/src/entities/Coordinates.ts
+++ b/server/src/entities/Coordinates.ts
@@ -3,8 +3,13 @@ import { Field, Float, ObjectType } from "type-graphql";
@ObjectType()
export class Coordinates {
@Field(() => Float)
- latitude: number | undefined;
+ latitude?: number;
@Field(() => Float)
- longitude: number | undefined;
+ longitude?: number;
+
+ constructor(latitude: number, longitude: number) {
+ this.latitude = latitude;
+ this.longitude = longitude;
+ }
}
diff --git a/server/src/entities/Event.ts b/server/src/entities/Event.ts
index 0426855..e0d273e 100644
--- a/server/src/entities/Event.ts
+++ b/server/src/entities/Event.ts
@@ -39,6 +39,13 @@ export class Event {
@Field()
group_max_size: number;
+ @Column("decimal", { precision: 6, scale: 2 })
+ @Field()
+ price: number;
+
+ // Ajouter une @Column duration pour la durée, voir si on l'ajoute en minutes, en heure...
+ // L'ajouter ici
+
@ManyToOne(
() => Trainer,
(trainer) => trainer.event,
@@ -73,6 +80,7 @@ export class Event {
description: string,
location: Coordinates,
group_max_size = 0,
+ price = 0,
) {
this.trainer = trainer;
this.service = service;
@@ -81,5 +89,6 @@ export class Event {
this.description = description;
this.location = location;
this.group_max_size = group_max_size;
+ this.price = price;
}
}
diff --git a/server/src/entities/Service.ts b/server/src/entities/Service.ts
index 4df3b5e..adc0248 100644
--- a/server/src/entities/Service.ts
+++ b/server/src/entities/Service.ts
@@ -15,7 +15,7 @@ import { Event } from "./Event";
export class Service {
@PrimaryGeneratedColumn()
@Field((_) => ID)
- service_id?: number;
+ id?: number;
@Column({
type: "varchar",
diff --git a/server/src/index.ts b/server/src/index.ts
index 107572c..b9e7b0d 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -14,13 +14,14 @@ import { initTestData } from "./dataSource/initTestData";
import { UserResolvers } from "./resolvers/UserResolvers";
import { DogResolver } from "./resolvers/DogResolver";
+import { EventResolver } from "./resolvers/EventResolver";
dotenv.config();
const port = 3200;
export async function startServerApollo() {
const schema = await buildSchema({
- resolvers: [UserResolvers, DogResolver],
+ resolvers: [UserResolvers, DogResolver, EventResolver],
});
const server = new ApolloServer({ schema });
diff --git a/server/src/resolvers/EventResolver.ts b/server/src/resolvers/EventResolver.ts
new file mode 100644
index 0000000..b55a41e
--- /dev/null
+++ b/server/src/resolvers/EventResolver.ts
@@ -0,0 +1,121 @@
+import { Arg, Query, Resolver, Mutation } from "type-graphql";
+import { dataSource } from "../dataSource/dataSource";
+import { Event } from "../entities/Event";
+import { Service } from "../entities/Service";
+import { Trainer } from "../entities/Trainer";
+
+import "dotenv/config";
+import type { Coordinates } from "../entities/Coordinates";
+
+import { LocationInput } from "../types/inputTypes";
+
+const eventRepository = dataSource.getRepository(Event);
+const trainerRepository = dataSource.getRepository(Trainer);
+const serviceRepository = dataSource.getRepository(Service);
+
+@Resolver()
+export class EventResolver {
+ // Get all events
+ @Query(() => [Event])
+ async getAllEvents(): Promise {
+ const events: Event[] = await eventRepository.find();
+ return events;
+ }
+
+ // Get one event by id
+ @Query(() => Event)
+ async getEventById(@Arg("eventId") eventId: number): Promise {
+ const eventsId: Event = await eventRepository.findOneOrFail({
+ where: { id: eventId },
+ });
+ return eventsId;
+ }
+
+ // Create event
+ @Mutation(() => Event)
+ async createEvent(
+ @Arg("trainerId") trainerId: number,
+ @Arg("serviceId") serviceId: number,
+ @Arg("date") date: Date,
+ @Arg("title") title: string,
+ @Arg("description") description: string,
+ @Arg("location") location: LocationInput,
+ @Arg("group_max_size") group_max_size: number,
+ @Arg("price") price: number,
+ ): Promise {
+ const trainer = await trainerRepository.findOneBy({ id: trainerId });
+ if (!trainer) {
+ throw new Error(`Trainer with ID ${trainerId} not found`);
+ }
+ const service = await serviceRepository.findOneBy({ id: serviceId });
+ if (!service) {
+ throw new Error(`Service with ID ${serviceId} not found`);
+ }
+
+ const event = new Event(
+ trainer,
+ service,
+ date,
+ title,
+ description,
+ location,
+ group_max_size,
+ price,
+ );
+
+ return await eventRepository.save(event);
+ }
+
+ // Update event
+ @Mutation(() => Event)
+ async updateEvent(
+ @Arg("eventId") eventId: number,
+ @Arg("trainerId") trainerId: number,
+ @Arg("serviceId") serviceId: number,
+ @Arg("date") date: Date,
+ @Arg("title") title: string,
+ @Arg("description") description: string,
+ @Arg("location", () => LocationInput) location: LocationInput,
+ @Arg("group_max_size") group_max_size: number,
+ @Arg("price") price: number,
+ ): Promise {
+ const event = await eventRepository.findOne({
+ where: {
+ id: eventId,
+ trainer: { id: trainerId },
+ service: { id: serviceId },
+ },
+ relations: ["trainer", "service"],
+ });
+
+ if (!event) {
+ throw new Error(`Event with ID ${eventId} not found`);
+ }
+
+ if (date) event.date = date;
+ if (title) event.title = title;
+ if (description) event.description = description;
+ if (location) event.location = location;
+ if (group_max_size) event.group_max_size = group_max_size;
+ if (price) event.price = price;
+
+ return await eventRepository.save(event);
+ }
+
+ // Delete event
+ @Mutation(() => Boolean)
+ async deleteEvent(@Arg("eventId") eventId: number): Promise {
+ const event = await eventRepository.findOne({
+ where: {
+ id: eventId,
+ },
+ });
+
+ if (!event) {
+ throw new Error(`Event with ID ${eventId} not found`);
+ }
+
+ const result = await eventRepository.delete(eventId);
+ return result.affected ? result.affected > 0 : false;
+ }
+}
diff --git a/server/src/types/inputTypes.ts b/server/src/types/inputTypes.ts
index 5fb6987..1849b00 100644
--- a/server/src/types/inputTypes.ts
+++ b/server/src/types/inputTypes.ts
@@ -38,3 +38,12 @@ export class UpdateUserInput {
@Field({ nullable: true })
company_name?: string;
}
+
+@InputType()
+export class LocationInput {
+ @Field()
+ latitude?: number;
+
+ @Field()
+ longitude?: number;
+}
diff --git a/server/src/types/responseType.ts b/server/src/types/responseType.ts
index 112310d..b887d83 100644
--- a/server/src/types/responseType.ts
+++ b/server/src/types/responseType.ts
@@ -1,6 +1,7 @@
import { ObjectType, Field } from "type-graphql";
import { Trainer } from "../entities/Trainer";
import { Owner } from "../entities/Owner";
+import { Event } from "../entities/Event";
@ObjectType()
export class MessageAndUserResponse {
@@ -10,3 +11,12 @@ export class MessageAndUserResponse {
@Field(() => Trainer || Owner)
user?: Trainer | Owner;
}
+
+@ObjectType()
+export class EventResponse {
+ @Field()
+ message!: string;
+
+ @Field(() => Event)
+ event?: Event;
+}