diff --git a/chat3.json b/chat3.json index c20153b..b247d15 100644 --- a/chat3.json +++ b/chat3.json @@ -3,149 +3,16 @@ { "name": "Aurélie Antoine", "skills": ["INFIRMIER", "PRELEVEMENT"], - "unavailableDates": ["2024-12-25", "2024-12-26", "2024-12-31"], - "undesiredDates": ["2024-12-24", "2024-12-30"], + "unavailableDates": ["2024-12-25", "2024-12-26"], + "undesiredDates": ["2024-12-24", "2024-12-31"], "desiredDates": ["2024-12-20", "2024-12-21"] }, { "name": "Cathy Coucou", "skills": ["MEDECIN", "SUPERVISION"], - "unavailableDates": ["2024-12-30", "2024-12-31"], - "undesiredDates": ["2024-12-29", "2024-12-28"], + "unavailableDates": ["2024-12-30"], + "undesiredDates": ["2024-12-29"], "desiredDates": ["2024-12-22", "2024-12-23"] - }, - { - "name": "Sophie Bernard-Dupont", - "skills": ["INFIRMIER", "ACCUEIL"], - "unavailableDates": ["2024-12-27", "2024-12-28"], - "undesiredDates": ["2024-12-25", "2024-12-26"], - "desiredDates": ["2024-12-24", "2024-12-29"] - }, - { - "name": "Jean Leroy", - "skills": ["TRANSPORT", "PRELEVEMENT"], - "unavailableDates": ["2024-12-24", "2024-12-25"], - "undesiredDates": ["2024-12-23", "2024-12-22"], - "desiredDates": ["2024-12-27", "2024-12-30"] - }, - { - "name": "Anne Moreau", - "skills": ["MEDECIN", "INFIRMIER"], - "unavailableDates": ["2024-12-26", "2024-12-27"], - "undesiredDates": ["2024-12-31", "2024-12-20"], - "desiredDates": ["2024-12-21", "2024-12-29"] - }, - { - "name": "Luc Petit", - "skills": ["ACCUEIL", "TRANSPORT"], - "unavailableDates": ["2024-12-20", "2024-12-21"], - "undesiredDates": ["2024-12-22", "2024-12-23"], - "desiredDates": ["2024-12-25", "2024-12-26"] - }, - { - "name": "Marie Dubois", - "skills": ["INFIRMIER", "ACCUEIL"], - "unavailableDates": ["2024-12-27", "2024-12-28"], - "undesiredDates": ["2024-12-29", "2024-12-30"], - "desiredDates": ["2024-12-22", "2024-12-23"] - }, - { - "name": "Pierre Martin", - "skills": ["MEDECIN", "SUPERVISION"], - "unavailableDates": ["2024-12-20", "2024-12-21"], - "undesiredDates": ["2024-12-22", "2024-12-23"], - "desiredDates": ["2024-12-29", "2024-12-30"] - }, - { - "name": "Claire Dupuis", - "skills": ["INFIRMIER", "PRELEVEMENT"], - "unavailableDates": ["2024-12-25", "2024-12-26"], - "undesiredDates": ["2024-12-27", "2024-12-28"], - "desiredDates": ["2024-12-20", "2024-12-21"] - }, - { - "name": "Thomas Lambert", - "skills": ["TRANSPORT", "ACCUEIL"], - "unavailableDates": ["2024-12-30", "2024-12-31"], - "undesiredDates": ["2024-12-24", "2024-12-25"], - "desiredDates": ["2024-12-22", "2024-12-23"] - }, - { - "name": "Julie Lefèvre", - "skills": ["INFIRMIER", "MEDECIN"], - "unavailableDates": ["2024-12-22", "2024-12-23"], - "undesiredDates": ["2024-12-24", "2024-12-25"], - "desiredDates": ["2024-12-27", "2024-12-28"] - }, - { - "name": "Nicolas Bernard", - "skills": ["PRELEVEMENT", "SUPERVISION"], - "unavailableDates": ["2024-12-29", "2024-12-30"], - "undesiredDates": ["2024-12-20", "2024-12-21"], - "desiredDates": ["2024-12-25", "2024-12-26"] - }, - { - "name": "Isabelle Moreau", - "skills": ["ACCUEIL", "INFIRMIER"], - "unavailableDates": ["2024-12-24", "2024-12-25"], - "undesiredDates": ["2024-12-26", "2024-12-27"], - "desiredDates": ["2024-12-22", "2024-12-23"] - }, - { - "name": "François Dubois", - "skills": ["MEDECIN", "TRANSPORT"], - "unavailableDates": ["2024-12-27", "2024-12-28"], - "undesiredDates": ["2024-12-29", "2024-12-30"], - "desiredDates": ["2024-12-20", "2024-12-21"] - }, - { - "name": "Élodie Martin", - "skills": ["INFIRMIER", "ACCUEIL"], - "unavailableDates": ["2024-12-31"], - "undesiredDates": ["2024-12-25", "2024-12-26"], - "desiredDates": ["2024-12-27", "2024-12-28"] - }, - { - "name": "Guillaume Lefèvre", - "skills": ["PRELEVEMENT", "TRANSPORT"], - "unavailableDates": ["2024-12-20", "2024-12-21"], - "undesiredDates": ["2024-12-22", "2024-12-23"], - "desiredDates": ["2024-12-29", "2024-12-30"] - }, - { - "name": "Caroline Lambert", - "skills": ["MEDECIN", "SUPERVISION"], - "unavailableDates": ["2024-12-25", "2024-12-26"], - "undesiredDates": ["2024-12-27", "2024-12-28"], - "desiredDates": ["2024-12-20", "2024-12-21"] - }, - { - "name": "Olivier Bernard", - "skills": ["INFIRMIER", "PRELEVEMENT"], - "unavailableDates": ["2024-12-22", "2024-12-23"], - "undesiredDates": ["2024-12-24", "2024-12-25"], - "desiredDates": ["2024-12-29", "2024-12-30"] - }, - { - "name": "Sandrine Moreau", - "skills": ["ACCUEIL", "TRANSPORT"], - "unavailableDates": ["2024-12-27", "2024-12-28"], - "undesiredDates": ["2024-12-29", "2024-12-30"], - "desiredDates": ["2024-12-20", "2024-12-21"] - }, - { - "name": "David Lefèvre", - "skills": ["MEDECIN", "INFIRMIER"], - "unavailableDates": ["2024-12-30", "2024-12-31"], - "undesiredDates": ["2024-12-24", "2024-12-25"], - "desiredDates": ["2024-12-22", "2024-12-23"] - }, - { - "name": "Céline Martin", - "skills": ["SUPERVISION", "ACCUEIL"], - "unavailableDates": ["2024-12-24", "2024-12-25"], - "undesiredDates": ["2024-12-26", "2024-12-27"], - "desiredDates": ["2024-12-29", "2024-12-30"] } ], "collectes": [ @@ -154,114 +21,9 @@ "start": "2024-12-20T08:00:00", "end": "2024-12-20T16:00:00", "location": "Centre de collecte - Toulouse", - "requiredSkills": { - "INFIRMIER": 3, - "MEDECIN": 1, - "ACCUEIL": 1, - "TRANSPORT": 1 - } - }, - { - "id": "collecte_blagnac_20241220", - "start": "2024-12-20T08:00:00", - "end": "2024-12-20T16:00:00", - "location": "Centre de collecte - Blagnac", "requiredSkills": { "INFIRMIER": 2, - "MEDECIN": 1, - "ACCUEIL": 1 - } - }, - { - "id": "collecte_purpan_20241221", - "start": "2024-12-21T08:00:00", - "end": "2024-12-21T16:00:00", - "location": "Hôpital Purpan - Toulouse", - "requiredSkills": { - "INFIRMIER": 3, - "MEDECIN": 1, - "ACCUEIL": 1, - "TRANSPORT": 1 - } - }, - { - "id": "collecte_rangueil_20241221", - "start": "2024-12-21T08:00:00", - "end": "2024-12-21T16:00:00", - "location": "Hôpital Rangueil - Toulouse", - "requiredSkills": { - "INFIRMIER": 2, - "MEDECIN": 1, - "PRELEVEMENT": 1 - } - }, - { - "id": "collecte_toulouse_20241222", - "start": "2024-12-22T08:00:00", - "end": "2024-12-22T16:00:00", - "location": "Centre de collecte - Toulouse", - "requiredSkills": { - "INFIRMIER": 3, - "MEDECIN": 1, - "ACCUEIL": 1, - "TRANSPORT": 1 - } - }, - { - "id": "collecte_blagnac_20241222", - "start": "2024-12-22T08:00:00", - "end": "2024-12-22T16:00:00", - "location": "Centre de collecte - Blagnac", - "requiredSkills": { - "INFIRMIER": 2, - "MEDECIN": 1, - "ACCUEIL": 1 - } - }, - { - "id": "collecte_purpan_20241223", - "start": "2024-12-23T08:00:00", - "end": "2024-12-23T16:00:00", - "location": "Hôpital Purpan - Toulouse", - "requiredSkills": { - "INFIRMIER": 3, - "MEDECIN": 1, - "ACCUEIL": 1, - "TRANSPORT": 1 - } - }, - { - "id": "collecte_rangueil_20241223", - "start": "2024-12-23T08:00:00", - "end": "2024-12-23T16:00:00", - "location": "Hôpital Rangueil - Toulouse", - "requiredSkills": { - "INFIRMIER": 2, - "MEDECIN": 1, - "PRELEVEMENT": 1 - } - }, - { - "id": "collecte_toulouse_20241224", - "start": "2024-12-24T08:00:00", - "end": "2024-12-24T16:00:00", - "location": "Centre de collecte - Toulouse", - "requiredSkills": { - "INFIRMIER": 3, - "MEDECIN": 1, - "ACCUEIL": 1, - "TRANSPORT": 1 - } - }, - { - "id": "collecte_blagnac_20241224", - "start": "2024-12-24T08:00:00", - "end": "2024-12-24T16:00:00", - "location": "Centre de collecte - Blagnac", - "requiredSkills": { - "INFIRMIER": 2, - "MEDECIN": 1, - "ACCUEIL": 1 + "MEDECIN": 1 } } ] diff --git a/newUI.html b/newUI.html index 8089d8a..fc1b2ea 100644 --- a/newUI.html +++ b/newUI.html @@ -168,7 +168,7 @@ const timelineOptions = { timeAxis: {scale: "hour", step: 6}, orientation: {axis: "top"}, - stack: false, + stack: true, xss: {disabled: true} }; @@ -372,131 +372,155 @@ } } } + function getColorForSkill(skill) { + const skillColors = { + "INFIRMIER": "#FF69B4", // Rose + "MEDECIN": "#4169E1", // Bleu + "PRELEVEMENT": "#FF8C00", // Orange + "ACCUEIL": "#228B22", // Vert + "TRANSPORT": "#8B008B", // Violet + "SUPERVISION": "#FFD700" // Or + }; + return skillColors[skill] || COLORS.DEFAULT; + } function renderSchedule(schedule) { - // Clear existing data + // Efface les anciennes données byEmployeeGroupDataSet.clear(); byEmployeeItemDataSet.clear(); byLocationGroupDataSet.clear(); byLocationItemDataSet.clear(); - - // Update score + + // Met à jour le score $('#score').text('Score: ' + (schedule.score || '?')); $('#analyzeButton').prop('disabled', !schedule.score); - - // Count unassigned shifts + + // Compte les shifts non assignés let unassignedCount = 0; schedule.shifts.forEach(shift => { if (!shift.employee) unassignedCount++; }); - $('#unassignedShifts').text( - unassignedCount === 0 - ? 'No unassigned shifts' - : `${unassignedCount} unassigned shifts` + unassignedCount === 0 ? 'No unassigned shifts' : `${unassignedCount} unassigned shifts` ); - // Render employees - schedule.employees.forEach((employee, index) => { - // Add employee group - const skillsBadges = employee.skills.map(skill => + // Rend les employés + schedule.employees.forEach(employee => { + const skillsBadges = employee.skills.map(skill => `${skill}` ).join(''); - byEmployeeGroupDataSet.add({ id: employee.name, content: `
${employee.name}
${skillsBadges}
` }); - // Add availability indicators + // Ajoute les disponibilités ['unavailableDates', 'undesiredDates', 'desiredDates'].forEach((dateType, typeIndex) => { const color = [COLORS.UNAVAILABLE, COLORS.UNDESIRED, COLORS.DESIRED][typeIndex]; - const label = ['Unavailable', 'Undesired', 'Desired'][typeIndex]; - employee[dateType]?.forEach((date, dateIndex) => { const startDate = new Date(date + 'T00:00:00'); const endDate = new Date(date + 'T23:59:59'); - byEmployeeItemDataSet.add({ id: `${employee.name}-${dateType}-${dateIndex}`, group: employee.name, - content: label, + content: ['Unavailable', 'Undesired', 'Desired'][typeIndex], start: startDate, end: endDate, type: 'background', - style: `opacity: 0.5; background-color: ${color}` + style: `opacity: 0.3; background-color: ${color}` }); }); - }); - }); + } + ); - // Render locations and shifts - const locations = [...new Set(schedule.shifts.map(shift => shift.location))]; - locations.forEach(location => { - byLocationGroupDataSet.add({ - id: location, - content: location - }); - }); - - // Render shifts - schedule.shifts.forEach((shift, index) => { - const startTime = new Date(shift.start); - const endTime = new Date(shift.end); - - if (shift.employee) { - // Assigned shift - const hasRequiredSkill = shift.employee.skills.includes(shift.requiredSkill); - const skillColor = hasRequiredSkill ? COLORS.SKILL_MATCH : COLORS.SKILL_MISMATCH; - const shiftColor = getShiftColor(shift, shift.employee); - - // Add to employee timeline - byEmployeeItemDataSet.add({ - id: `shift-${index}-emp`, - group: shift.employee.name, - content: `
${shift.location}
- ${shift.requiredSkill}
`, - start: startTime, - end: endTime, - style: `background-color: ${shiftColor}` - }); - - // Add to location timeline - byLocationItemDataSet.add({ - id: `shift-${index}-loc`, - group: shift.location, - content: `
${shift.employee.name}
- ${shift.requiredSkill}
`, - start: startTime, - end: endTime, - style: `background-color: ${shiftColor}` - }); - } else { - // Unassigned shift - byLocationItemDataSet.add({ - id: `shift-${index}-unassigned`, - group: shift.location, - content: `
Unassigned
- ${shift.requiredSkill}
`, - start: startTime, - end: endTime, - style: `background-color: ${COLORS.UNASSIGNED}` + // Crée les groupes par (lieu + compétence) + const locationSkillGroups = {}; + schedule.shifts.forEach(shift => { + const groupId = `${shift.location}_${shift.requiredSkill}`; + if (!locationSkillGroups[groupId]) { + locationSkillGroups[groupId] = true; + byLocationGroupDataSet.add({ + id: groupId, + content: `
+ ${shift.location}
+ + ${shift.requiredSkill} + +
` }); } }); - // Set timeline window + // Ajoute les shifts aux groupes + schedule.shifts.forEach((shift, index) => { + const startTime = new Date(shift.start); + const endTime = new Date(shift.end); + const groupId = `${shift.location}_${shift.requiredSkill}`; + const skillColor = getColorForSkill(shift.requiredSkill); + + // Ajoute au timeline "By Employee" + if (shift.employee) { + const hasRequiredSkill = shift.employee.skills.includes(shift.requiredSkill); + const shiftColor = hasRequiredSkill ? COLORS.SKILL_MATCH : COLORS.SKILL_MISMATCH; + + byEmployeeItemDataSet.add({ + id: `shift-${index}-emp`, + group: shift.employee.name, + content: `
+ ${shift.location}
+ ${shift.requiredSkill}
+ ${startTime.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})} - + ${endTime.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})} +
`, + start: startTime, + end: endTime, + style: `background-color: ${shiftColor}; border: 1px solid #555; color: #fff;` + }); + } + + // Ajoute au timeline "By Location" + if (shift.employee) { + const hasRequiredSkill = shift.employee.skills.includes(shift.requiredSkill); + const shiftColor = hasRequiredSkill ? skillColor : COLORS.SKILL_MISMATCH; + + byLocationItemDataSet.add({ + id: `shift-${index}-loc`, + group: groupId, + content: `
+ ${shift.employee.name}
+ ${startTime.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})} - + ${endTime.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})} +
`, + start: startTime, + end: endTime, + style: `background-color: ${shiftColor}; border: 1px solid #555; color: #fff;` + }); + } else { + // Shift non assigné + byLocationItemDataSet.add({ + id: `shift-${index}-unassigned`, + group: groupId, + content: `
Non assigné
`, + start: startTime, + end: endTime, + style: `background-color: ${COLORS.UNASSIGNED}; border: 1px solid #555; color: #000;` + }); + } + }); + + // Ajuste la fenêtre de la timeline if (schedule.shifts.length > 0) { const dates = schedule.shifts.map(shift => new Date(shift.start)); const minDate = new Date(Math.min(...dates)); const maxDate = new Date(Math.max(...dates)); maxDate.setDate(maxDate.getDate() + 1); - byEmployeeTimeline.setWindow(minDate, maxDate); byLocationTimeline.setWindow(minDate, maxDate); } } + + function getShiftColor(shift, employee) { const shiftDate = new Date(shift.start).toISOString().split('T')[0]; diff --git a/src/main/java/org/acme/employeescheduling/solver/EmployeeSchedulingConstraintProvider.java b/src/main/java/org/acme/employeescheduling/solver/EmployeeSchedulingConstraintProvider.java index e1a70be..28314c9 100644 --- a/src/main/java/org/acme/employeescheduling/solver/EmployeeSchedulingConstraintProvider.java +++ b/src/main/java/org/acme/employeescheduling/solver/EmployeeSchedulingConstraintProvider.java @@ -4,11 +4,13 @@ import static ai.timefold.solver.core.api.score.stream.Joiners.equal; import static ai.timefold.solver.core.api.score.stream.Joiners.lessThanOrEqual; import static ai.timefold.solver.core.api.score.stream.Joiners.overlapping; +import java.math.BigDecimal; import java.time.Duration; import java.time.LocalDateTime; import java.util.function.Function; import ai.timefold.solver.core.api.score.buildin.hardsoftbigdecimal.HardSoftBigDecimalScore; +import ai.timefold.solver.core.api.score.buildin.hardsoftlong.HardSoftLongScore; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintCollectors; import ai.timefold.solver.core.api.score.stream.ConstraintFactory; @@ -38,9 +40,10 @@ public class EmployeeSchedulingConstraintProvider implements ConstraintProvider // Hard constraints requiredSkill(constraintFactory), noOverlappingShifts(constraintFactory), - atLeast10HoursBetweenTwoShifts(constraintFactory), + atLeast11HoursBetweenTwoShifts(constraintFactory), oneShiftPerDay(constraintFactory), unavailableEmployee(constraintFactory), + dailyWorkDuration(constraintFactory), // Soft constraints undesiredDayForEmployee(constraintFactory), desiredDayForEmployee(constraintFactory), @@ -48,6 +51,7 @@ public class EmployeeSchedulingConstraintProvider implements ConstraintProvider }; } + // Vérifie que l'employé affecté à un shift possède la compétence requise. private Constraint requiredSkill(ConstraintFactory constraintFactory) { return constraintFactory.forEach(Shift.class) .filter(shift -> shift.getEmployee() != null && @@ -56,6 +60,7 @@ public class EmployeeSchedulingConstraintProvider implements ConstraintProvider .asConstraint("Missing required skill"); } + // Empêche qu'un employé ait deux shifts qui se chevauchent. private Constraint noOverlappingShifts(ConstraintFactory constraintFactory) { return constraintFactory.forEachUniquePair(Shift.class, equal(Shift::getEmployee), @@ -64,19 +69,20 @@ public class EmployeeSchedulingConstraintProvider implements ConstraintProvider .asConstraint("Overlapping shift"); } - Constraint atLeast10HoursBetweenTwoShifts(ConstraintFactory constraintFactory) { + // Garantit un repos minimal de 11 heures entre deux shifts consécutifs pour un employé. +Constraint atLeast11HoursBetweenTwoShifts(ConstraintFactory constraintFactory) { return constraintFactory.forEach(Shift.class) .join(Shift.class, equal(Shift::getEmployee), lessThanOrEqual(Shift::getEnd, Shift::getStart)) - .filter((firstShift, - secondShift) -> Duration.between(firstShift.getEnd(), secondShift.getStart()).toHours() < 10) - .penalize(HardSoftBigDecimalScore.ONE_HARD, + .filter((firstShift, secondShift) -> Duration.between(firstShift.getEnd(), secondShift.getStart()).toHours() < 11) + .penalizeBigDecimal(HardSoftBigDecimalScore.ONE_HARD, (firstShift, secondShift) -> { - int breakLength = (int) Duration.between(firstShift.getEnd(), secondShift.getStart()).toMinutes(); - return (10 * 60) - breakLength; + long breakLengthInMinutes = Duration.between(firstShift.getEnd(), secondShift.getStart()).toMinutes(); + return new BigDecimal((11 * 60) - breakLengthInMinutes); }) - .asConstraint("At least 10 hours between 2 shifts"); + .asConstraint("At least 11 hours between 2 shifts"); } + // Un employé ne peut avoir qu'un seul shift par jour Constraint oneShiftPerDay(ConstraintFactory constraintFactory) { return constraintFactory.forEachUniquePair(Shift.class, equal(Shift::getEmployee), equal(shift -> shift.getStart().toLocalDate())) @@ -84,6 +90,7 @@ public class EmployeeSchedulingConstraintProvider implements ConstraintProvider .asConstraint("Max one shift per day"); } + // Un employé ne peut pas être affecté à un shift pendant ses dates d'indisponibilité. Constraint unavailableEmployee(ConstraintFactory constraintFactory) { return constraintFactory.forEach(Shift.class) .join(Employee.class, equal(Shift::getEmployee, Function.identity())) @@ -93,6 +100,30 @@ public class EmployeeSchedulingConstraintProvider implements ConstraintProvider .asConstraint("Unavailable employee"); } + // Spécifie qu'un employé doit travailler entre 4 et 10 heures par jour. + private Constraint dailyWorkDuration(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(Shift.class) + .groupBy(Shift::getEmployee, + shift -> shift.getStart().toLocalDate(), + ConstraintCollectors.sumDuration(shift -> Duration.between(shift.getStart(), shift.getEnd()))) + .penalizeBigDecimal(HardSoftBigDecimalScore.ONE_HARD, + (employee, date, totalDuration) -> { + // Convertir la durée totale en BigDecimal pour le calcul + BigDecimal totalMinutes = new BigDecimal(totalDuration.toMinutes()); + BigDecimal minMinutes = new BigDecimal(4 * 60); + BigDecimal maxMinutes = new BigDecimal(10 * 60); + if (totalMinutes.compareTo(minMinutes) < 0) { + return minMinutes.subtract(totalMinutes); + } + if (totalMinutes.compareTo(maxMinutes) > 0) { + return totalMinutes.subtract(maxMinutes); + } + return BigDecimal.ZERO; + }) + .asConstraint("Daily work duration between 4 and 10 hours"); + } + + // Pénalise les shifts qui tombent sur des jours non souhaités par l'employé. Constraint undesiredDayForEmployee(ConstraintFactory constraintFactory) { return constraintFactory.forEach(Shift.class) .join(Employee.class, equal(Shift::getEmployee, Function.identity())) @@ -102,6 +133,7 @@ public class EmployeeSchedulingConstraintProvider implements ConstraintProvider .asConstraint("Undesired day for employee"); } + // Récompense les shifts qui tombent sur des jours souhaités par l'employé. Constraint desiredDayForEmployee(ConstraintFactory constraintFactory) { return constraintFactory.forEach(Shift.class) .join(Employee.class, equal(Shift::getEmployee, Function.identity())) @@ -111,6 +143,7 @@ public class EmployeeSchedulingConstraintProvider implements ConstraintProvider .asConstraint("Desired day for employee"); } + // Équilibre la répartition des shifts entre les employés pour éviter que certains en aient trop et d'autres trop peu. Constraint balanceEmployeeShiftAssignments(ConstraintFactory constraintFactory) { return constraintFactory.forEach(Shift.class) .groupBy(Shift::getEmployee, ConstraintCollectors.count()) diff --git a/src/main/resources/META-INF/resources/upload-simple.html b/src/main/resources/META-INF/resources/upload-simple.html new file mode 100644 index 0000000..8f5bcc1 --- /dev/null +++ b/src/main/resources/META-INF/resources/upload-simple.html @@ -0,0 +1,509 @@ + + + + + + Employee scheduling - Timefold Solver (Optimized) + + + + + +
+
+ Loading... +
+
+ +
+
+
+
+ +

Employee scheduling solver (Optimized - No Visualization)

+

Upload your JSON file and generate the optimal schedule for your employees.

+ + +
+
+
1. Load your data
+
+
+
+
+
+ +

Drag & drop your JSON file here or click to select

+ + +
+ +
+
+
Or use demo data:
+ +
+
+
+
+ + +
+
+
2. Solve the problem
+
+
+ + + + +
+
+ + +
+
+
3. Results Summary
+
+
+
+
+
+
+
Score
+

?

+
+
+
+
+
+
+
Total Shifts
+

0

+
+
+
+
+
+
+
Assigned Shifts
+

0

+
+
+
+
+
+
+
Unassigned Shifts
+

0

+
+
+
+
+
+
+
+
+
Additional Info
+
+

Employees: 0

+

Locations: 0

+

Date Range: -

+
+
+
+
+
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/upload-vieux.html b/src/main/resources/META-INF/resources/upload-vieux.html new file mode 100644 index 0000000..82ce302 --- /dev/null +++ b/src/main/resources/META-INF/resources/upload-vieux.html @@ -0,0 +1,705 @@ + + + + + + Employee scheduling - Timefold Solver on Quarkus + + + + + + +
+
+
+
+ +

Employee scheduling solver

+

Upload your JSON file and generate the optimal schedule for your employees.

+ + +
+
+
1. Load your data
+
+
+
+
+
+ +

Drag & drop your JSON file here or click to select

+ + +
+ +
+
+
Or use demo data:
+ +
+
+
+
+ + +
+
+
2. Solve the problem
+
+
+ + + + + Score: ? + +
+
+ + +
+
+
3. View results
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + diff --git a/src/main/resources/META-INF/resources/upload.html b/src/main/resources/META-INF/resources/upload.html index 9b1d47d..04a5b9e 100644 --- a/src/main/resources/META-INF/resources/upload.html +++ b/src/main/resources/META-INF/resources/upload.html @@ -215,6 +215,18 @@ loadDemoData(); }); + function getColorForSkill(skill) { + const skillColors = { + "INFIRMIER": "#FF69B4", // Rose + "MEDECIN": "#4169E1", // Bleu + "PRELEVEMENT": "#FF8C00", // Orange + "ACCUEIL": "#228B22", // Vert + "TRANSPORT": "#8B008B", // Violet + "SUPERVISION": "#FFD700" // Or + }; + return skillColors[skill] || COLORS.DEFAULT; + } + function setupEventListeners() { // File upload const fileInput = document.getElementById('fileInput'); @@ -415,6 +427,12 @@ loadedSchedule = schedule; renderSchedule(schedule); $('#downloadButton').prop('disabled', false); + + // Vérifier le statut et arrêter l'auto-refresh si terminé + if (schedule.solverStatus === 'NOT_SOLVING') { + console.log('Solving terminé, arrêt de l\'auto-refresh'); + refreshSolvingButtons(false); + } }) .fail(function() { refreshSolvingButtons(false); @@ -427,7 +445,7 @@ $('#solveButton').hide(); $('#stopSolvingButton').show(); if (autoRefreshIntervalId == null) { - autoRefreshIntervalId = setInterval(refreshSchedule, 2000); + autoRefreshIntervalId = setInterval(refreshSchedule, 5000); } } else { $('#solveButton').show(); @@ -440,204 +458,196 @@ } function renderSchedule(schedule) { - // Clear existing data + console.time('renderSchedule'); // Pour mesurer le temps + + // Sauvegarde les positions de scroll actuelles + const employeeScroll = document.querySelector('#byEmployeeVisualization .vis-center')?.scrollTop || 0; + const locationScroll = document.querySelector('#byLocationVisualization .vis-center')?.scrollTop || 0; + + // Désactive les mises à jour automatiques pendant la construction byEmployeeGroupDataSet.clear(); byEmployeeItemDataSet.clear(); byLocationGroupDataSet.clear(); byLocationItemDataSet.clear(); - - // Update score + + // Met à jour le score $('#score').text('Score: ' + (schedule.score || '?')); $('#analyzeButton').prop('disabled', !schedule.score); - - // Count unassigned shifts - let unassignedCount = 0; - schedule.shifts.forEach(shift => { - if (!shift.employee) unassignedCount++; - }); - + + // Compte les shifts non assignés (optimisé avec filter) + const unassignedCount = schedule.shifts.filter(shift => !shift.employee).length; $('#unassignedShifts').text( - unassignedCount === 0 - ? 'No unassigned shifts' - : `${unassignedCount} unassigned shifts` + unassignedCount === 0 ? 'No unassigned shifts' : `${unassignedCount} unassigned shifts` ); - // Render employees - schedule.employees.forEach((employee, index) => { - // Add employee group - const skillsBadges = employee.skills.map(skill => - `${skill}` - ).join(''); - - byEmployeeGroupDataSet.add({ + // Prépare tous les groupes et items en mémoire + const employeeGroups = []; + const employeeItems = []; + const locationGroupsData = []; + const locationItems = []; + + // === PARTIE 1: Employés === + schedule.employees.forEach(employee => { + employeeGroups.push({ id: employee.name, - content: `
${employee.name}
${skillsBadges}
` + content: `
${employee.name}
` }); - // Add availability indicators - ['unavailableDates', 'undesiredDates', 'desiredDates'].forEach((dateType, typeIndex) => { - const color = [COLORS.UNAVAILABLE, COLORS.UNDESIRED, COLORS.DESIRED][typeIndex]; - const label = ['Unavailable', 'Undesired', 'Desired'][typeIndex]; - - employee[dateType]?.forEach((date, dateIndex) => { + // Ajoute toutes les disponibilités d'un coup + const dateTypes = [ + { key: 'unavailableDates', label: 'Unavailable', color: COLORS.UNAVAILABLE }, + { key: 'undesiredDates', label: 'Undesired', color: COLORS.UNDESIRED }, + { key: 'desiredDates', label: 'Desired', color: COLORS.DESIRED } + ]; + + dateTypes.forEach(({ key, label, color }) => { + employee[key]?.forEach((date, dateIndex) => { const startDate = new Date(date + 'T00:00:00'); const endDate = new Date(date + 'T23:59:59'); - - byEmployeeItemDataSet.add({ - id: `${employee.name}-${dateType}-${dateIndex}`, + employeeItems.push({ + id: `${employee.name}-${key}-${dateIndex}`, group: employee.name, content: label, start: startDate, end: endDate, type: 'background', - style: `opacity: 0.5; background-color: ${color}` + style: `opacity: 0.3; background-color: ${color}` }); }); }); }); - // Render collectes/shifts as groups in location view - if (schedule.collectes && schedule.shifts) { - // Create individual groups for each shift within collectes - schedule.shifts.forEach((shift, index) => { - const collecte = schedule.collectes.find(c => c.id === shift.collecte?.id); - const collecteInfo = collecte ? - `Collecte: ${collecte.id} - Requis: ${Object.entries(collecte.requiredSkills || {}).map(([skill, count]) => `${count} ${skill}`).join(', ')}` : - 'Collecte inconnue'; - - byLocationGroupDataSet.add({ - id: `shift-group-${index}`, - content: `
${shift.location}
- ${collecteInfo}
- Shift ${shift.requiredSkill}
` - }); - }); - } else if (schedule.shifts) { - // Fallback for old format without collectes - const locations = [...new Set(schedule.shifts.map(shift => shift.location))]; - locations.forEach(location => { - byLocationGroupDataSet.add({ - id: location, - content: location - }); - }); - } + // === PARTIE 2: Locations === + // Pré-calcule toutes les données nécessaires + const locations = [...new Set(schedule.shifts.map(shift => shift.location))]; + const locationSkillsMap = new Map(); + + // Construit la map location -> skills en une seule passe + schedule.shifts.forEach(shift => { + if (!locationSkillsMap.has(shift.location)) { + locationSkillsMap.set(shift.location, new Set()); + } + locationSkillsMap.get(shift.location).add(shift.requiredSkill); + }); + + // Crée tous les groupes + locations.forEach(location => { + const locationGroupId = `loc-${location}`; + const nestedGroups = []; + const skills = Array.from(locationSkillsMap.get(location)); + + skills.forEach(skill => { + const subGroupId = `${location}_${skill}`; + nestedGroups.push(subGroupId); + + locationGroupsData.push({ + id: subGroupId, + content: ` + ${skill} + `, + nestedGroups: [] + }); + }); + + locationGroupsData.push({ + id: locationGroupId, + content: `
${location}
`, + nestedGroups: nestedGroups + }); + }); + + // === PARTIE 3: Shifts === + // Crée une map des compétences des employés pour un accès rapide + const employeeSkillsMap = new Map(); + schedule.employees.forEach(emp => { + employeeSkillsMap.set(emp.name, new Set(emp.skills)); + }); - // Render shifts schedule.shifts.forEach((shift, index) => { const startTime = new Date(shift.start); const endTime = new Date(shift.end); + const groupId = `${shift.location}_${shift.requiredSkill}`; + const skillColor = getColorForSkill(shift.requiredSkill); + const timeStr = `${startTime.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})} - ${endTime.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}`; + if (shift.employee) { - // Assigned shift - const hasRequiredSkill = shift.employee.skills.includes(shift.requiredSkill); - const skillColor = hasRequiredSkill ? COLORS.SKILL_MATCH : COLORS.SKILL_MISMATCH; - const shiftColor = getShiftColor(shift, shift.employee); + const hasRequiredSkill = employeeSkillsMap.get(shift.employee.name)?.has(shift.requiredSkill); + const shiftColor = hasRequiredSkill ? COLORS.SKILL_MATCH : COLORS.SKILL_MISMATCH; - // Add to employee timeline - byEmployeeItemDataSet.add({ + // Timeline "By Employee" + employeeItems.push({ id: `shift-${index}-emp`, group: shift.employee.name, - content: `
${shift.collecte?.id || shift.location}
- ${shift.requiredSkill}
`, + content: `
+ ${shift.location}
+ ${shift.requiredSkill}
+ ${timeStr} +
`, start: startTime, end: endTime, - style: `background-color: ${shiftColor}` + style: `background-color: ${shiftColor}; border: 1px solid #555; color: #fff;` }); - // Add to shift-specific timeline (each shift has its own group) - byLocationItemDataSet.add({ + // Timeline "By Location" + const locationShiftColor = hasRequiredSkill ? skillColor : COLORS.SKILL_MISMATCH; + locationItems.push({ id: `shift-${index}-loc`, - group: `shift-group-${index}`, - content: `
${shift.employee.name}
- ${shift.requiredSkill}
`, + group: groupId, + content: `
+ ${shift.employee.name}
+ ${timeStr} +
`, start: startTime, end: endTime, - style: `background-color: ${shiftColor}` + style: `background-color: ${locationShiftColor}; border: 1px solid #555; color: #fff;` }); } else { - // Unassigned shift - byLocationItemDataSet.add({ + // Shift non assigné + locationItems.push({ id: `shift-${index}-unassigned`, - group: `shift-group-${index}`, - content: `
Non assigné
- ${shift.requiredSkill}
`, + group: groupId, + content: `
Non assigné
`, start: startTime, end: endTime, - style: `background-color: ${COLORS.UNASSIGNED}` + style: `background-color: ${COLORS.UNASSIGNED}; border: 1px solid #555; color: #000;` }); } }); - // Fallback for old format without collectes - if (!schedule.collectes && schedule.shifts) { - // Override groups for old format - use locations instead - byLocationGroupDataSet.clear(); - const locations = [...new Set(schedule.shifts.map(shift => shift.location))]; - locations.forEach(location => { - byLocationGroupDataSet.add({ - id: location, - content: location - }); - }); - - // Re-render shifts with location groups for old format - schedule.shifts.forEach((shift, index) => { - const startTime = new Date(shift.start); - const endTime = new Date(shift.end); - - if (shift.employee) { - const hasRequiredSkill = shift.employee.skills.includes(shift.requiredSkill); - const skillColor = hasRequiredSkill ? COLORS.SKILL_MATCH : COLORS.SKILL_MISMATCH; - const shiftColor = getShiftColor(shift, shift.employee); - - byLocationItemDataSet.add({ - id: `shift-${index}-loc-old`, - group: shift.location, - content: `
${shift.employee.name}
- ${shift.requiredSkill}
`, - start: startTime, - end: endTime, - style: `background-color: ${shiftColor}` - }); - } else { - byLocationItemDataSet.add({ - id: `shift-${index}-unassigned-old`, - group: shift.location, - content: `
Non assigné
- ${shift.requiredSkill}
`, - start: startTime, - end: endTime, - style: `background-color: ${COLORS.UNASSIGNED}` - }); - } - }); - } + // === PARTIE 4: Ajoute tout en une seule opération === + console.log(`Adding ${employeeGroups.length} employee groups`); + console.log(`Adding ${employeeItems.length} employee items`); + console.log(`Adding ${locationGroupsData.length} location groups`); + console.log(`Adding ${locationItems.length} location items`); - // Set timeline window + // Utilise add() avec un tableau pour tout ajouter d'un coup + byEmployeeGroupDataSet.add(employeeGroups); + byEmployeeItemDataSet.add(employeeItems); + byLocationGroupDataSet.add(locationGroupsData); + byLocationItemDataSet.add(locationItems); + + // Ajuste la fenêtre de la timeline if (schedule.shifts.length > 0) { const dates = schedule.shifts.map(shift => new Date(shift.start)); const minDate = new Date(Math.min(...dates)); const maxDate = new Date(Math.max(...dates)); maxDate.setDate(maxDate.getDate() + 1); - byEmployeeTimeline.setWindow(minDate, maxDate); byLocationTimeline.setWindow(minDate, maxDate); } + + // Restaure les positions de scroll après un court délai + setTimeout(() => { + const employeeScrollContainer = document.querySelector('#byEmployeeVisualization .vis-center'); + const locationScrollContainer = document.querySelector('#byLocationVisualization .vis-center'); + if (employeeScrollContainer) employeeScrollContainer.scrollTop = employeeScroll; + if (locationScrollContainer) locationScrollContainer.scrollTop = locationScroll; + }, 50); + + console.timeEnd('renderSchedule'); } - function getShiftColor(shift, employee) { - const shiftDate = new Date(shift.start).toISOString().split('T')[0]; - - if (employee.unavailableDates?.includes(shiftDate)) { - return COLORS.UNAVAILABLE; - } else if (employee.undesiredDates?.includes(shiftDate)) { - return COLORS.UNDESIRED; - } else if (employee.desiredDates?.includes(shiftDate)) { - return COLORS.DESIRED; - } - return COLORS.DEFAULT; - } function analyze() { if (!loadedSchedule?.score) { diff --git a/src/main/resources/META-INF/resources/upload.html.foiré b/src/main/resources/META-INF/resources/upload.html.foiré new file mode 100644 index 0000000..364ca2d --- /dev/null +++ b/src/main/resources/META-INF/resources/upload.html.foiré @@ -0,0 +1,528 @@ + + + + + + Employee scheduling - Timefold Solver (Optimized) + + + + + + + + + + + +
+
+ Chargement... +
+
+ +
+ +
+
+
+ +

Employee scheduling solver (Optimized)

+

Upload your JSON file and generate the optimal schedule for your employees.

+ + +
+
1. Load your data
+
+
+
+
+ +

Drag & drop your JSON file here or click to select

+ + +
+ +
+ +
+
Or use demo data:
+ +
+
+
+
+ + +
+
2. Solve the problem
+
+ + + + + Score: ? + +
+
+ + +
+
+
3. View results
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + +