avant changement UI
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
package org.acme.employeescheduling.domain;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public class Collecte {
|
||||
private String id;
|
||||
private LocalDateTime start;
|
||||
private LocalDateTime end;
|
||||
private String location;
|
||||
private Map<String, Integer> requiredSkills;
|
||||
private List<Shift> shifts;
|
||||
|
||||
public Collecte() {
|
||||
this.requiredSkills = new HashMap<>();
|
||||
this.shifts = new ArrayList<>();
|
||||
}
|
||||
|
||||
public Collecte(String id, LocalDateTime start, LocalDateTime end, String location, Map<String, Integer> requiredSkills) {
|
||||
this.id = id;
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.location = location;
|
||||
this.requiredSkills = requiredSkills != null ? requiredSkills : new HashMap<>();
|
||||
this.shifts = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void generateShifts() {
|
||||
if (this.requiredSkills == null) {
|
||||
this.requiredSkills = new HashMap<>();
|
||||
}
|
||||
|
||||
this.shifts = this.requiredSkills.entrySet().stream()
|
||||
.flatMap(entry -> {
|
||||
String skill = entry.getKey();
|
||||
int quantity = entry.getValue();
|
||||
return IntStream.range(0, quantity)
|
||||
.mapToObj(i -> new Shift(
|
||||
this.id + "_" + skill + "_" + i,
|
||||
this.start,
|
||||
this.end,
|
||||
this.location,
|
||||
skill,
|
||||
null, // employee
|
||||
this // collecte
|
||||
));
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// Getters et setters
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
public LocalDateTime getStart() { return start; }
|
||||
public void setStart(LocalDateTime start) { this.start = start; }
|
||||
public LocalDateTime getEnd() { return end; }
|
||||
public void setEnd(LocalDateTime end) { this.end = end; }
|
||||
public String getLocation() { return location; }
|
||||
public void setLocation(String location) { this.location = location; }
|
||||
public Map<String, Integer> getRequiredSkills() { return requiredSkills; }
|
||||
public void setRequiredSkills(Map<String, Integer> requiredSkills) { this.requiredSkills = requiredSkills; }
|
||||
public List<Shift> getShifts() { return shifts; }
|
||||
public void setShifts(List<Shift> shifts) { this.shifts = shifts; }
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package org.acme.employeescheduling.domain;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.stream.Collectors;
|
||||
import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty;
|
||||
import ai.timefold.solver.core.api.domain.solution.PlanningScore;
|
||||
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
|
||||
@@ -12,61 +13,50 @@ import ai.timefold.solver.core.api.solver.SolverStatus;
|
||||
|
||||
@PlanningSolution
|
||||
public class EmployeeSchedule {
|
||||
@ProblemFactCollectionProperty
|
||||
@ValueRangeProvider(id = "employeeRange")
|
||||
private List<Employee> employees;
|
||||
|
||||
@ProblemFactCollectionProperty
|
||||
@ValueRangeProvider
|
||||
private List<Employee> employees;
|
||||
private List<Collecte> collectes;
|
||||
|
||||
@PlanningEntityCollectionProperty
|
||||
private List<Shift> shifts;
|
||||
|
||||
@PlanningScore
|
||||
private HardSoftBigDecimalScore score;
|
||||
|
||||
private SolverStatus solverStatus;
|
||||
|
||||
// No-arg constructor required for Timefold
|
||||
public EmployeeSchedule() {}
|
||||
|
||||
public EmployeeSchedule(List<Employee> employees, List<Shift> shifts) {
|
||||
this.employees = employees;
|
||||
this.shifts = shifts;
|
||||
// Constructeur vide requis par Timefold
|
||||
public EmployeeSchedule() {
|
||||
this.employees = new ArrayList<>();
|
||||
this.collectes = new ArrayList<>();
|
||||
this.shifts = new ArrayList<>();
|
||||
}
|
||||
|
||||
public EmployeeSchedule(HardSoftBigDecimalScore score, SolverStatus solverStatus) {
|
||||
this.score = score;
|
||||
this.solverStatus = solverStatus;
|
||||
// Constructeur principal
|
||||
public EmployeeSchedule(List<Employee> employees, List<Collecte> collectes) {
|
||||
this.employees = employees != null ? employees : new ArrayList<>();
|
||||
this.collectes = collectes != null ? collectes : new ArrayList<>();
|
||||
|
||||
// Générer les shifts à partir des collectes
|
||||
this.shifts = collectes != null ?
|
||||
collectes.stream()
|
||||
.peek(Collecte::generateShifts)
|
||||
.flatMap(collecte -> collecte.getShifts().stream())
|
||||
.collect(Collectors.toList()) :
|
||||
new ArrayList<>();
|
||||
}
|
||||
|
||||
public List<Employee> getEmployees() {
|
||||
return employees;
|
||||
}
|
||||
|
||||
public void setEmployees(List<Employee> employees) {
|
||||
this.employees = employees;
|
||||
}
|
||||
|
||||
public List<Shift> getShifts() {
|
||||
return shifts;
|
||||
}
|
||||
|
||||
public void setShifts(List<Shift> shifts) {
|
||||
this.shifts = shifts;
|
||||
}
|
||||
|
||||
public HardSoftBigDecimalScore getScore() {
|
||||
return score;
|
||||
}
|
||||
|
||||
public void setScore(HardSoftBigDecimalScore score) {
|
||||
this.score = score;
|
||||
}
|
||||
|
||||
public SolverStatus getSolverStatus() {
|
||||
return solverStatus;
|
||||
}
|
||||
|
||||
public void setSolverStatus(SolverStatus solverStatus) {
|
||||
this.solverStatus = solverStatus;
|
||||
}
|
||||
// Getters et setters
|
||||
public List<Employee> getEmployees() { return employees; }
|
||||
public void setEmployees(List<Employee> employees) { this.employees = employees; }
|
||||
public List<Collecte> getCollectes() { return collectes; }
|
||||
public void setCollectes(List<Collecte> collectes) { this.collectes = collectes; }
|
||||
public List<Shift> getShifts() { return shifts; }
|
||||
public void setShifts(List<Shift> shifts) { this.shifts = shifts; }
|
||||
public HardSoftBigDecimalScore getScore() { return score; }
|
||||
public void setScore(HardSoftBigDecimalScore score) { this.score = score; }
|
||||
public SolverStatus getSolverStatus() { return solverStatus; }
|
||||
public void setSolverStatus(SolverStatus solverStatus) { this.solverStatus = solverStatus; }
|
||||
}
|
||||
|
||||
@@ -20,28 +20,30 @@ public class Shift {
|
||||
|
||||
private String location;
|
||||
private String requiredSkill;
|
||||
private Collecte collecte;
|
||||
|
||||
@PlanningVariable
|
||||
@PlanningVariable(valueRangeProviderRefs = "employeeRange")
|
||||
private Employee employee;
|
||||
|
||||
public Shift() {
|
||||
}
|
||||
|
||||
public Shift(LocalDateTime start, LocalDateTime end, String location, String requiredSkill) {
|
||||
this(start, end, location, requiredSkill, null);
|
||||
this(null, start, end, location, requiredSkill, null, null);;
|
||||
}
|
||||
|
||||
public Shift(LocalDateTime start, LocalDateTime end, String location, String requiredSkill, Employee employee) {
|
||||
this(null, start, end, location, requiredSkill, employee);
|
||||
this(null, start, end, location, requiredSkill, employee, null);
|
||||
}
|
||||
|
||||
public Shift(String id, LocalDateTime start, LocalDateTime end, String location, String requiredSkill, Employee employee) {
|
||||
public Shift(String id, LocalDateTime start, LocalDateTime end, String location, String requiredSkill, Employee employee, Collecte collecte) {
|
||||
this.id = id;
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.location = location;
|
||||
this.requiredSkill = requiredSkill;
|
||||
this.employee = employee;
|
||||
this.collecte = collecte;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
@@ -88,6 +90,9 @@ public class Shift {
|
||||
return employee;
|
||||
}
|
||||
|
||||
public Collecte getCollecte() { return collecte; }
|
||||
public void setCollecte(Collecte collecte) { this.collecte = collecte; }
|
||||
|
||||
public void setEmployee(Employee employee) {
|
||||
this.employee = employee;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DELETE;
|
||||
@@ -17,14 +16,12 @@ import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis;
|
||||
import ai.timefold.solver.core.api.score.buildin.hardsoftbigdecimal.HardSoftBigDecimalScore;
|
||||
import ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy;
|
||||
import ai.timefold.solver.core.api.solver.SolutionManager;
|
||||
import ai.timefold.solver.core.api.solver.SolverManager;
|
||||
import ai.timefold.solver.core.api.solver.SolverStatus;
|
||||
|
||||
import org.acme.employeescheduling.domain.EmployeeSchedule;
|
||||
import org.acme.employeescheduling.rest.exception.EmployeeScheduleSolverException;
|
||||
import org.acme.employeescheduling.rest.exception.ErrorInfo;
|
||||
@@ -42,13 +39,9 @@ import org.slf4j.LoggerFactory;
|
||||
@Tag(name = "Employee Schedules", description = "Employee Schedules service for assigning employees to shifts.")
|
||||
@Path("schedules")
|
||||
public class EmployeeScheduleResource {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(EmployeeScheduleResource.class);
|
||||
|
||||
SolverManager<EmployeeSchedule, String> solverManager;
|
||||
SolutionManager<EmployeeSchedule, HardSoftBigDecimalScore> solutionManager;
|
||||
|
||||
// TODO: Without any "time to live", the map may eventually grow out of memory.
|
||||
private final ConcurrentMap<String, Job> jobIdToJob = new ConcurrentHashMap<>();
|
||||
|
||||
@Inject
|
||||
@@ -77,20 +70,36 @@ public class EmployeeScheduleResource {
|
||||
@POST
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
public String solve(EmployeeSchedule problem) {
|
||||
String jobId = UUID.randomUUID().toString();
|
||||
jobIdToJob.put(jobId, Job.ofSchedule(problem));
|
||||
solverManager.solveBuilder()
|
||||
.withProblemId(jobId)
|
||||
.withProblemFinder(jobId_ -> jobIdToJob.get(jobId).schedule)
|
||||
.withBestSolutionConsumer(solution -> jobIdToJob.put(jobId, Job.ofSchedule(solution)))
|
||||
.withExceptionHandler((jobId_, exception) -> {
|
||||
jobIdToJob.put(jobId, Job.ofException(exception));
|
||||
LOGGER.error("Failed solving jobId ({}).", jobId, exception);
|
||||
})
|
||||
.run();
|
||||
return jobId;
|
||||
}
|
||||
public String solve(EmployeeSchedule problem) {
|
||||
String jobId = UUID.randomUUID().toString();
|
||||
LOGGER.info("Submitting problem with jobId: {}", jobId);
|
||||
LOGGER.info("Problem details - Employees: {}, Collectes: {}",
|
||||
problem.getEmployees().size(),
|
||||
problem.getCollectes().size());
|
||||
|
||||
jobIdToJob.put(jobId, Job.ofSchedule(problem));
|
||||
|
||||
LOGGER.info("Starting solver for jobId: {}", jobId);
|
||||
solverManager.solveBuilder()
|
||||
.withProblemId(jobId)
|
||||
.withProblemFinder(jobId_ -> {
|
||||
Job job = jobIdToJob.get(jobId);
|
||||
LOGGER.debug("Problem finder called for jobId: {}, problem: {}", jobId, job != null ? job.schedule : null);
|
||||
return job != null ? job.schedule : null;
|
||||
})
|
||||
.withBestSolutionConsumer(solution -> {
|
||||
LOGGER.info("New best solution for jobId: {}", jobId);
|
||||
jobIdToJob.put(jobId, Job.ofSchedule(solution));
|
||||
})
|
||||
.withExceptionHandler((jobId_, exception) -> {
|
||||
LOGGER.error("Exception during solving jobId {}: {}", jobId, exception.getMessage(), exception);
|
||||
jobIdToJob.put(jobId, Job.ofException(exception));
|
||||
})
|
||||
.run();
|
||||
|
||||
LOGGER.info("Solver started for jobId: {}", jobId);
|
||||
return jobId;
|
||||
}
|
||||
|
||||
@Operation(summary = "Submit a schedule to analyze its score.")
|
||||
@APIResponses(value = {
|
||||
@@ -131,17 +140,6 @@ public class EmployeeScheduleResource {
|
||||
return schedule;
|
||||
}
|
||||
|
||||
private EmployeeSchedule getEmployeeScheduleAndCheckForExceptions(String jobId) {
|
||||
Job job = jobIdToJob.get(jobId);
|
||||
if (job == null) {
|
||||
throw new EmployeeScheduleSolverException(jobId, Response.Status.NOT_FOUND, "No schedule found.");
|
||||
}
|
||||
if (job.exception != null) {
|
||||
throw new EmployeeScheduleSolverException(jobId, job.exception);
|
||||
}
|
||||
return job.schedule;
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Terminate solving for a given job ID. Returns the best solution of the schedule so far, as it might still be running or not even started.")
|
||||
@APIResponses(value = {
|
||||
@@ -160,7 +158,6 @@ public class EmployeeScheduleResource {
|
||||
@Path("{jobId}")
|
||||
public EmployeeSchedule terminateSolving(
|
||||
@Parameter(description = "The job ID returned by the POST method.") @PathParam("jobId") String jobId) {
|
||||
// TODO: Replace with .terminateEarlyAndWait(... [, timeout]); see https://github.com/TimefoldAI/timefold-solver/issues/77
|
||||
solverManager.terminateEarly(jobId);
|
||||
return getEmployeeSchedule(jobId);
|
||||
}
|
||||
@@ -170,7 +167,7 @@ public class EmployeeScheduleResource {
|
||||
@APIResponses(value = {
|
||||
@APIResponse(responseCode = "200", description = "The schedule status and the best score so far.",
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(implementation = EmployeeSchedule.class))),
|
||||
schema = @Schema(implementation = ScheduleStatus.class))),
|
||||
@APIResponse(responseCode = "404", description = "No schedule found.",
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(implementation = ErrorInfo.class))),
|
||||
@@ -181,21 +178,50 @@ public class EmployeeScheduleResource {
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("{jobId}/status")
|
||||
public EmployeeSchedule getStatus(
|
||||
public Response getStatus(
|
||||
@Parameter(description = "The job ID returned by the POST method.") @PathParam("jobId") String jobId) {
|
||||
EmployeeSchedule schedule = getEmployeeScheduleAndCheckForExceptions(jobId);
|
||||
SolverStatus solverStatus = solverManager.getSolverStatus(jobId);
|
||||
return new EmployeeSchedule(schedule.getScore(), solverStatus);
|
||||
|
||||
return Response.ok(new ScheduleStatus(schedule.getScore(), solverStatus)).build();
|
||||
}
|
||||
|
||||
private EmployeeSchedule getEmployeeScheduleAndCheckForExceptions(String jobId) {
|
||||
Job job = jobIdToJob.get(jobId);
|
||||
if (job == null) {
|
||||
throw new EmployeeScheduleSolverException(jobId, Response.Status.NOT_FOUND, "No schedule found.");
|
||||
}
|
||||
if (job.exception != null) {
|
||||
throw new EmployeeScheduleSolverException(jobId, job.exception);
|
||||
}
|
||||
return job.schedule;
|
||||
}
|
||||
|
||||
private record Job(EmployeeSchedule schedule, Throwable exception) {
|
||||
|
||||
static Job ofSchedule(EmployeeSchedule schedule) {
|
||||
return new Job(schedule, null);
|
||||
}
|
||||
|
||||
static Job ofException(Throwable error) {
|
||||
return new Job(null, error);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ScheduleStatus {
|
||||
private HardSoftBigDecimalScore score;
|
||||
private SolverStatus solverStatus;
|
||||
|
||||
public ScheduleStatus(HardSoftBigDecimalScore score, SolverStatus solverStatus) {
|
||||
this.score = score;
|
||||
this.solverStatus = solverStatus;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public HardSoftBigDecimalScore getScore() {
|
||||
return score;
|
||||
}
|
||||
|
||||
public SolverStatus getSolverStatus() {
|
||||
return solverStatus;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.acme.employeescheduling.solver;
|
||||
|
||||
import ai.timefold.solver.core.api.score.buildin.hardsoftbigdecimal.HardSoftBigDecimalScore;
|
||||
import ai.timefold.solver.core.api.score.stream.Constraint;
|
||||
import ai.timefold.solver.core.api.score.stream.ConstraintFactory;
|
||||
import ai.timefold.solver.core.api.score.stream.ConstraintProvider;
|
||||
import ai.timefold.solver.core.api.score.stream.ConstraintCollectors;
|
||||
// import org.acme.employeescheduling.domain.Collecte;
|
||||
import org.acme.employeescheduling.domain.Shift;
|
||||
|
||||
public class CollecteConstraintProvider implements ConstraintProvider {
|
||||
|
||||
@Override
|
||||
public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
|
||||
return new Constraint[] {
|
||||
requiredSkills(constraintFactory)
|
||||
};
|
||||
}
|
||||
|
||||
private Constraint requiredSkills(ConstraintFactory constraintFactory) {
|
||||
return constraintFactory.forEach(Shift.class)
|
||||
.groupBy(Shift::getCollecte, Shift::getRequiredSkill, ConstraintCollectors.count())
|
||||
.penalize(HardSoftBigDecimalScore.ONE_HARD,
|
||||
(collecte, skill, count) -> {
|
||||
int requiredQuantity = collecte.getRequiredSkills().getOrDefault(skill, 0);
|
||||
return Math.max(0, requiredQuantity - count);
|
||||
})
|
||||
.asConstraint("Insufficient employees with required skill");
|
||||
}
|
||||
}
|
||||
@@ -48,19 +48,20 @@ public class EmployeeSchedulingConstraintProvider implements ConstraintProvider
|
||||
};
|
||||
}
|
||||
|
||||
Constraint requiredSkill(ConstraintFactory constraintFactory) {
|
||||
return constraintFactory.forEach(Shift.class)
|
||||
.filter(shift -> !shift.getEmployee().getSkills().contains(shift.getRequiredSkill()))
|
||||
.penalize(HardSoftBigDecimalScore.ONE_HARD)
|
||||
.asConstraint("Missing required skill");
|
||||
private Constraint requiredSkill(ConstraintFactory constraintFactory) {
|
||||
return constraintFactory.forEach(Shift.class)
|
||||
.filter(shift -> shift.getEmployee() != null &&
|
||||
!shift.getEmployee().getSkills().contains(shift.getRequiredSkill()))
|
||||
.penalize(HardSoftBigDecimalScore.ONE_HARD)
|
||||
.asConstraint("Missing required skill");
|
||||
}
|
||||
|
||||
Constraint noOverlappingShifts(ConstraintFactory constraintFactory) {
|
||||
return constraintFactory.forEachUniquePair(Shift.class, equal(Shift::getEmployee),
|
||||
overlapping(Shift::getStart, Shift::getEnd))
|
||||
.penalize(HardSoftBigDecimalScore.ONE_HARD,
|
||||
EmployeeSchedulingConstraintProvider::getMinuteOverlap)
|
||||
.asConstraint("Overlapping shift");
|
||||
private Constraint noOverlappingShifts(ConstraintFactory constraintFactory) {
|
||||
return constraintFactory.forEachUniquePair(Shift.class,
|
||||
equal(Shift::getEmployee),
|
||||
overlapping(shift -> shift.getStart(), shift -> shift.getEnd()))
|
||||
.penalize(HardSoftBigDecimalScore.ONE_HARD)
|
||||
.asConstraint("Overlapping shift");
|
||||
}
|
||||
|
||||
Constraint atLeast10HoursBetweenTwoShifts(ConstraintFactory constraintFactory) {
|
||||
|
||||
@@ -41,3 +41,6 @@ quarkus.swagger-ui.always-include=true
|
||||
########################
|
||||
|
||||
%test.quarkus.timefold.solver.termination.spent-limit=10s
|
||||
|
||||
quarkus.log.category."ai.timefold.solver".level=DEBUG
|
||||
quarkus.log.category."org.acme.employeescheduling".level=DEBUG
|
||||
|
||||
Reference in New Issue
Block a user