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)
+
+
+
+
+
+
+
+
+
+
+
Employee scheduling solver (Optimized - No Visualization)
+
Upload your JSON file and generate the optimal schedule for your employees.
+
+
+
+
+
+
+
+
+
+
Drag & drop your JSON file here or click to select
+
+
+
+
+
+
+ loaded successfully
+
+
+
+
+
Or use demo data:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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.
+
+
+
+
+
+
+
+
+
+
Drag & drop your JSON file here or click to select
+
+
+
+
+
+
+ loaded successfully
+
+
+
+
+
Or use demo data:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Score: ?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Employee scheduling solver (Optimized)
+
Upload your JSON file and generate the optimal schedule for your employees.
+
+
+
+
+
+
+
+
+
+
Drag & drop your JSON file here or click to select
+
+
+
+
+
+
+ loaded successfully
+
+
+
+
+
+
Or use demo data:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Score: ?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+