Skip to content

Commit 39b0d7e

Browse files
Merge pull request #67 from AlessGarau/develop
HOTFIX: fix possible duplicate lessons without room id because of opt…
2 parents 0e9ca28 + c9926d8 commit 39b0d7e

File tree

7 files changed

+177
-37
lines changed

7 files changed

+177
-37
lines changed

packages/client/src/api/endpoints/planning.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const planningApi = {
3535
uploadLessons: async (file: File): Promise<{
3636
message: string;
3737
importedCount: number;
38+
updatedCount: number;
3839
skippedCount: number;
3940
errors: Array<{ row: number; field?: string; message: string }>;
4041
optimization?: {

packages/client/src/pages/PlanningPage.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,22 @@ const PlanningPage = () => {
5959
...planningQueryOptions.uploadLessons(),
6060
onSuccess: (data) => {
6161
let message = `Fichier importé avec succès ! `;
62+
const parts: string[] = [];
63+
6264
if (data.importedCount > 0) {
63-
message += `${data.importedCount} cours ajoutés`;
65+
parts.push(`${data.importedCount} cours ajoutés`);
66+
}
67+
if (data.updatedCount > 0) {
68+
parts.push(`${data.updatedCount} cours mis à jour`);
6469
}
6570
if (data.skippedCount > 0) {
66-
message += data.importedCount > 0 ? ` et ${data.skippedCount} cours ignorés (déjà existants)` : `${data.skippedCount} cours ignorés (déjà existants)`;
71+
parts.push(`${data.skippedCount} cours ignorés (aucun changement car déjà existant)`);
72+
}
73+
74+
if (parts.length > 0) {
75+
message += parts.join(', ');
76+
} else {
77+
message = 'Aucun changement détecté dans le fichier importé.';
6778
}
6879

6980
toast.success(message);
@@ -177,11 +188,11 @@ const PlanningPage = () => {
177188
</div>
178189
<div className="flex gap-2">
179190
<Button
180-
label={isMdScreen ? "Télécharger" : undefined}
191+
label={isMdScreen ? "Exemple excel" : undefined}
181192
icon={DownloadIcon}
182193
onClick={() => downloadTemplateMutation.mutate()}
183194
disabled={downloadTemplateMutation.isPending}
184-
tooltip={!isMdScreen ? "Télécharger le modèle" : undefined}
195+
tooltip={!isMdScreen ? "Télécharger un exemple d'excel" : undefined}
185196
/>
186197
<Button
187198
label={isMdScreen ? "Importer une feuille" : undefined}

packages/server/src/feature/lesson/Repository.ts

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Service } from "typedi";
22
import { NodePgDatabase } from "drizzle-orm/node-postgres";
3-
import { between, eq, and, inArray } from "drizzle-orm";
3+
import { between, eq, and, inArray, lt, gt } from "drizzle-orm";
44
import { database } from "../../../database/database";
55
import { lessonTable } from "../../../database/schema/lesson";
66
import { roomTable } from "../../../database/schema/room";
@@ -114,6 +114,21 @@ export class LessonRepository implements ILessonRepository {
114114
return result[0] || null;
115115
}
116116

117+
async findOverlappingLessons(classId: string, startTime: Date, endTime: Date): Promise<Lesson[]> {
118+
const result = await this.db
119+
.select()
120+
.from(lessonTable)
121+
.where(
122+
and(
123+
eq(lessonTable.class_id, classId),
124+
lt(lessonTable.start_time, endTime),
125+
gt(lessonTable.end_time, startTime),
126+
),
127+
);
128+
129+
return result;
130+
}
131+
117132
async getLessonById(lessonId: string): Promise<Lesson | null> {
118133
const result = await this.db
119134
.select()
@@ -131,23 +146,44 @@ export class LessonRepository implements ILessonRepository {
131146
}
132147

133148
async updateLesson(lessonId: string, data: UpdateLessonData): Promise<Lesson> {
134-
const [startHour, startMinute] = data.startTime.split(":").map(Number);
135-
const [endHour, endMinute] = data.endTime.split(":").map(Number);
136-
137-
const startDate = new Date(data.date);
138-
startDate.setHours(startHour, startMinute, 0, 0);
139-
140-
const endDate = new Date(data.date);
141-
endDate.setHours(endHour, endMinute, 0, 0);
149+
const updateData: Partial<{
150+
title: string;
151+
room_id: string | undefined;
152+
start_time: Date;
153+
end_time: Date;
154+
}> = {};
155+
156+
if (data.title !== undefined) {
157+
updateData.title = data.title;
158+
}
159+
160+
if (data.roomId !== undefined && data.roomId !== null) {
161+
updateData.room_id = data.roomId;
162+
}
163+
164+
if (data.startTime !== undefined && data.endTime !== undefined) {
165+
if (data.startTime instanceof Date) {
166+
updateData.start_time = data.startTime;
167+
} else if (typeof data.startTime === "string" && data.date) {
168+
const [startHour, startMinute] = data.startTime.split(":").map(Number);
169+
const startDate = new Date(data.date);
170+
startDate.setHours(startHour, startMinute, 0, 0);
171+
updateData.start_time = startDate;
172+
}
173+
174+
if (data.endTime instanceof Date) {
175+
updateData.end_time = data.endTime;
176+
} else if (typeof data.endTime === "string" && data.date) {
177+
const [endHour, endMinute] = data.endTime.split(":").map(Number);
178+
const endDate = new Date(data.date);
179+
endDate.setHours(endHour, endMinute, 0, 0);
180+
updateData.end_time = endDate;
181+
}
182+
}
142183

143184
const result = await this.db
144185
.update(lessonTable)
145-
.set({
146-
title: data.title,
147-
room_id: data.roomId,
148-
start_time: startDate,
149-
end_time: endDate,
150-
})
186+
.set(updateData)
151187
.where(eq(lessonTable.id, lessonId))
152188
.returning();
153189

packages/server/src/feature/lesson/interface/IRepository.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ export interface CreateLessonData {
1818
}
1919

2020
export interface UpdateLessonData {
21-
title: string;
22-
roomId: string;
23-
startTime: string;
24-
endTime: string;
25-
dayOfWeek: string;
26-
date: string;
21+
title?: string;
22+
roomId?: string | null;
23+
startTime?: string | Date;
24+
endTime?: string | Date;
25+
dayOfWeek?: string;
26+
date?: string;
2727
}
2828

2929
export interface ILessonRepository {

packages/server/src/feature/planning/Controller.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,10 @@ export class PlanningController {
7676
return reply.status(200).send({
7777
message: PlanningMessage.LESSONS_IMPORTED_SUCCESSFULLY,
7878
importedCount: importResult.importedCount,
79+
updatedCount: importResult.updatedCount,
7980
skippedCount: importResult.skippedCount,
8081
errors: importResult.errors,
82+
optimization: importResult.optimization,
8183
});
8284

8385
}

packages/server/src/feature/planning/Interactor.ts

Lines changed: 100 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { PlanningError } from "./../../middleware/error/planningError";
44
import { RoomRepository } from "../room/Repository";
55
import { LessonRepository } from "../lesson/Repository";
66
import { ClassRepository } from "../class/Repository";
7+
import { UserRepository } from "../user/Repository";
78
import { RoomError } from "../../middleware/error/roomError";
89
import { LessonError } from "../../middleware/error/lessonError";
910
import { WeeklyPlanningData, ImportResult, ImportError, ImportedLesson, ImportedLessonSchema, PlanningFilterOptions } from "./validate";
@@ -19,6 +20,7 @@ export class PlanningInteractor implements IPlanningInteractor {
1920
private roomRepository: RoomRepository,
2021
private lessonRepository: LessonRepository,
2122
private classRepository: ClassRepository,
23+
private userRepository: UserRepository,
2224
private optimizationService: OptimizationService,
2325
) { }
2426

@@ -66,6 +68,7 @@ export class PlanningInteractor implements IPlanningInteractor {
6668
async importLessonsFromTemplate(fileBuffer: Buffer): Promise<ImportResult> {
6769
const errors: ImportError[] = [];
6870
let importedCount = 0;
71+
let updatedCount = 0;
6972
let skippedCount = 0;
7073
let earliestDate: Date | null = null;
7174
let latestDate: Date | null = null;
@@ -157,30 +160,114 @@ export class PlanningInteractor implements IPlanningInteractor {
157160
continue;
158161
}
159162

160-
const existingLesson = await this.lessonRepository.findLessonByDetails(
163+
let teacherId: string | null = null;
164+
if (validatedLesson.teacherName && validatedLesson.teacherName.trim()) {
165+
const nameParts = validatedLesson.teacherName.trim().split(" ");
166+
if (nameParts.length >= 2) {
167+
const firstName = nameParts[0];
168+
const lastName = nameParts.slice(1).join(" ");
169+
const teacher = await this.userRepository.findTeacherByName(firstName, lastName);
170+
if (teacher) {
171+
teacherId = teacher.id;
172+
} else {
173+
errors.push({
174+
row: rowIndex + 1,
175+
field: "teacher_name",
176+
message: `Teacher "${validatedLesson.teacherName}" not found`,
177+
});
178+
}
179+
}
180+
}
181+
182+
const overlappingLessons = await this.lessonRepository.findOverlappingLessons(
161183
classEntity.id,
162184
startDateTime,
163185
endDateTime,
164186
);
165187

166-
if (existingLesson) {
167-
skippedCount++;
168-
} else {
169-
await this.lessonRepository.createLesson({
188+
const lessonsToDelete = overlappingLessons.filter(lesson => !lesson.room_id);
189+
for (const lessonToDelete of lessonsToDelete) {
190+
await this.lessonRepository.deleteLesson(lessonToDelete.id);
191+
}
192+
193+
const lessonsWithRoom = overlappingLessons.filter(lesson => lesson.room_id);
194+
195+
let lessonCreated = false;
196+
197+
if (lessonsWithRoom.length > 0) {
198+
if (lessonsWithRoom.length > 1) {
199+
errors.push({
200+
row: rowIndex + 1,
201+
field: "time",
202+
message: "Multiple lessons with rooms overlap this time slot. Using the first one.",
203+
});
204+
}
205+
206+
const lessonToUpdate = lessonsWithRoom[0];
207+
208+
// Check if there are actual changes
209+
const hasTimeChanged = lessonToUpdate.start_time.getTime() !== startDateTime.getTime() ||
210+
lessonToUpdate.end_time.getTime() !== endDateTime.getTime();
211+
const hasTitleChanged = lessonToUpdate.title !== validatedLesson.title;
212+
213+
// Get current teacher to check if it changed
214+
const lessonWithRelations = await this.lessonRepository.getLessonWithRelations(lessonToUpdate.id);
215+
const currentTeacherId = lessonWithRelations?.users?.[0]?.id;
216+
const hasTeacherChanged = teacherId && teacherId !== currentTeacherId;
217+
218+
if (hasTimeChanged || hasTitleChanged || hasTeacherChanged) {
219+
// There are changes - update the lesson
220+
const updateData: any = {};
221+
if (hasTimeChanged) {
222+
updateData.startTime = startDateTime;
223+
updateData.endTime = endDateTime;
224+
}
225+
if (hasTitleChanged) {
226+
updateData.title = validatedLesson.title;
227+
}
228+
229+
if (Object.keys(updateData).length > 0) {
230+
await this.lessonRepository.updateLesson(lessonToUpdate.id, updateData);
231+
}
232+
233+
if (hasTeacherChanged && teacherId) {
234+
await this.lessonRepository.updateLessonTeacher(lessonToUpdate.id, teacherId);
235+
}
236+
237+
updatedCount++;
238+
239+
if (!earliestDate || startDateTime < earliestDate) {
240+
earliestDate = startDateTime;
241+
}
242+
if (!latestDate || endDateTime > latestDate) {
243+
latestDate = endDateTime;
244+
}
245+
} else {
246+
skippedCount++;
247+
}
248+
249+
lessonCreated = true;
250+
}
251+
252+
if (!lessonCreated) {
253+
const newLesson = await this.lessonRepository.createLesson({
170254
title: validatedLesson.title,
171255
startTime: startDateTime,
172256
endTime: endDateTime,
173257
classId: classEntity.id,
174258
roomId: null,
175259
});
260+
if (teacherId) {
261+
await this.lessonRepository.updateLessonTeacher(newLesson.id, teacherId);
262+
}
176263
importedCount++;
264+
}
177265

178-
if (!earliestDate || startDateTime < earliestDate) {
179-
earliestDate = startDateTime;
180-
}
181-
if (!latestDate || endDateTime > latestDate) {
182-
latestDate = endDateTime;
183-
}
266+
if (!earliestDate || startDateTime < earliestDate) {
267+
earliestDate = startDateTime;
268+
}
269+
if (!latestDate || endDateTime > latestDate) {
270+
latestDate = endDateTime;
184271
}
185272
} catch (error) {
186273
if (error instanceof Error) {
@@ -194,7 +281,7 @@ export class PlanningInteractor implements IPlanningInteractor {
194281

195282
let optimizationStatus = undefined;
196283

197-
if (importedCount > 0 && earliestDate && latestDate) {
284+
if ((importedCount > 0 || updatedCount > 0) && earliestDate && latestDate) {
198285
try {
199286
await this.optimizationService.optimizeDateRange(earliestDate, latestDate);
200287
optimizationStatus = { status: "success" as const };
@@ -209,6 +296,7 @@ export class PlanningInteractor implements IPlanningInteractor {
209296

210297
return {
211298
importedCount,
299+
updatedCount,
212300
skippedCount,
213301
errors,
214302
optimization: optimizationStatus,

packages/server/src/feature/planning/validate.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const OptimizationStatusSchema = z.object({
6767

6868
export const ImportResultSchema = z.object({
6969
importedCount: z.number(),
70+
updatedCount: z.number(),
7071
skippedCount: z.number(),
7172
errors: z.array(ImportErrorSchema),
7273
optimization: OptimizationStatusSchema.optional(),
@@ -75,6 +76,7 @@ export const ImportResultSchema = z.object({
7576
export const ImportLessonResponseSchema = z.object({
7677
message: z.string(),
7778
importedCount: z.number(),
79+
updatedCount: z.number(),
7880
skippedCount: z.number(),
7981
errors: z.array(ImportErrorSchema),
8082
optimization: OptimizationStatusSchema.optional(),

0 commit comments

Comments
 (0)