Web UI Ok

This commit is contained in:
2025-09-21 11:34:53 +02:00
parent 9be454ca81
commit 9fab9f83ef
8 changed files with 1243 additions and 57 deletions

File diff suppressed because one or more lines are too long

View File

@@ -108,7 +108,7 @@
<li class="nav-item" role="presentation">
<button class="nav-link active" id="byLocationTab" data-bs-toggle="tab"
data-bs-target="#byLocationPanel" type="button" role="tab">
By location
By collecte
</button>
</li>
<li class="nav-item" role="presentation">
@@ -163,14 +163,14 @@
// Timefold server URL - modify this to match your server
// Option 1: Direct (nécessite CORS activé sur le serveur)
// const TIMEFOLD_SERVER = 'http://10.0.100.13:8080';
const TIMEFOLD_SERVER = 'http://10.0.100.13:8080';
// Option 2: Via proxy local (démarrez le serveur proxy sur port 3000)
// Option 2: Si vous servez depuis le serveur Timefold même
// const TIMEFOLD_SERVER = window.location.origin;
// Option 3: Via proxy local (si vous utilisez un proxy)
// const TIMEFOLD_SERVER = 'http://localhost:3000/api';
// Option 3: Si vous servez depuis le serveur Timefold
const TIMEFOLD_SERVER = window.location.origin;
// Timeline configuration
const timelineOptions = {
timeAxis: {scale: "hour", step: 6},
@@ -270,7 +270,16 @@
const reader = new FileReader();
reader.onload = function(e) {
try {
const jsonData = JSON.parse(e.target.result);
const rawText = e.target.result;
// Debug: montrer les premiers caractères
console.log('Premiers caractères du fichier:', rawText.substring(0, 50));
console.log('Code du premier caractère:', rawText.charCodeAt(0));
// Nettoyer le texte (supprimer BOM et espaces invisibles)
const cleanText = rawText.trim().replace(/^\uFEFF/, '');
const jsonData = JSON.parse(cleanText);
loadedSchedule = jsonData;
// Show file info
@@ -285,10 +294,26 @@
showNotification('File loaded successfully!', 'success');
} catch (error) {
showNotification('Error parsing JSON file: ' + error.message, 'danger');
console.error('Erreur détaillée:', error);
console.error('Contenu du fichier:', e.target.result.substring(0, 200));
showNotification(`Error parsing JSON file: ${error.message}`, 'danger');
// Afficher une aide pour résoudre le problème
const helpMessage = `
<div class="alert alert-warning mt-3">
<h6>Conseils pour résoudre le problème:</h6>
<ul class="mb-0">
<li>Vérifiez que le fichier commence bien par { et se termine par }</li>
<li>Sauvegardez le fichier en UTF-8 sans BOM</li>
<li>Vérifiez qu'il n'y a pas de virgules en trop</li>
<li>Testez votre JSON sur <a href="https://jsonlint.com" target="_blank">JSONLint</a></li>
</ul>
</div>
`;
$('#fileInfo').html(helpMessage).show();
}
};
reader.readAsText(file);
reader.readAsText(file, 'UTF-8');
}
function loadDemoData() {
@@ -325,15 +350,49 @@
return;
}
$.post(TIMEFOLD_SERVER + '/schedules', JSON.stringify(loadedSchedule))
.done(function(data) {
scheduleId = data;
console.log('Sending data to solve:', loadedSchedule);
$.post({
url: TIMEFOLD_SERVER + '/schedules',
data: JSON.stringify(loadedSchedule),
contentType: 'application/json',
dataType: 'text' // Attendre du texte (l'ID du job)
})
.done(function(data, textStatus, xhr) {
console.log('Solve response:', data);
console.log('Response status:', xhr.status);
console.log('Response headers:', xhr.getAllResponseHeaders());
if (data && data.trim()) {
scheduleId = data.trim();
refreshSolvingButtons(true);
showNotification('Solving started...', 'info');
})
.fail(function(xhr) {
showNotification('Failed to start solving: ' + xhr.responseText, 'danger');
} else {
showNotification('Server returned empty response', 'danger');
}
})
.fail(function(xhr, textStatus, errorThrown) {
console.error('Solve failed:', {
status: xhr.status,
statusText: xhr.statusText,
responseText: xhr.responseText,
textStatus: textStatus,
errorThrown: errorThrown
});
let errorMsg = 'Failed to start solving: ';
if (xhr.status === 0) {
errorMsg += 'Cannot connect to server. Check CORS configuration.';
} else if (xhr.status === 404) {
errorMsg += 'Endpoint not found. Check server URL.';
} else if (xhr.responseText) {
errorMsg += xhr.responseText;
} else {
errorMsg += `${xhr.status} ${xhr.statusText}`;
}
showNotification(errorMsg, 'danger');
});
}
function stopSolving() {
@@ -437,14 +496,32 @@
});
});
// Render locations and shifts
const locations = [...new Set(schedule.shifts.map(shift => shift.location))];
locations.forEach(location => {
byLocationGroupDataSet.add({
id: location,
content: location
// 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: `<div><strong>${shift.location}</strong><br/>
<small>${collecteInfo}</small><br/>
<small class="text-primary">Shift ${shift.requiredSkill}</small></div>`
});
});
});
} 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
});
});
}
// Render shifts
schedule.shifts.forEach((shift, index) => {
@@ -461,17 +538,17 @@
byEmployeeItemDataSet.add({
id: `shift-${index}-emp`,
group: shift.employee.name,
content: `<div><strong>${shift.location}</strong><br/>
content: `<div><strong>${shift.collecte?.id || shift.location}</strong><br/>
<span class="badge" style="background-color:${skillColor}">${shift.requiredSkill}</span></div>`,
start: startTime,
end: endTime,
style: `background-color: ${shiftColor}`
});
// Add to location timeline
// Add to shift-specific timeline (each shift has its own group)
byLocationItemDataSet.add({
id: `shift-${index}-loc`,
group: shift.location,
group: `shift-group-${index}`,
content: `<div><strong>${shift.employee.name}</strong><br/>
<span class="badge" style="background-color:${skillColor}">${shift.requiredSkill}</span></div>`,
start: startTime,
@@ -482,8 +559,8 @@
// Unassigned shift
byLocationItemDataSet.add({
id: `shift-${index}-unassigned`,
group: shift.location,
content: `<div><strong>Unassigned</strong><br/>
group: `shift-group-${index}`,
content: `<div><strong>Non assigné</strong><br/>
<span class="badge bg-secondary">${shift.requiredSkill}</span></div>`,
start: startTime,
end: endTime,
@@ -492,6 +569,51 @@
}
});
// 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: `<div><strong>${shift.employee.name}</strong><br/>
<span class="badge" style="background-color:${skillColor}">${shift.requiredSkill}</span></div>`,
start: startTime,
end: endTime,
style: `background-color: ${shiftColor}`
});
} else {
byLocationItemDataSet.add({
id: `shift-${index}-unassigned-old`,
group: shift.location,
content: `<div><strong>Non assigné</strong><br/>
<span class="badge bg-secondary">${shift.requiredSkill}</span></div>`,
start: startTime,
end: endTime,
style: `background-color: ${COLORS.UNASSIGNED}`
});
}
});
}
// Set timeline window
if (schedule.shifts.length > 0) {
const dates = schedule.shifts.map(shift => new Date(shift.start));
@@ -609,4 +731,4 @@
}
</script>
</body>
</html>
</html>

View File

@@ -44,3 +44,8 @@ quarkus.swagger-ui.always-include=true
quarkus.log.category."ai.timefold.solver".level=DEBUG
quarkus.log.category."org.acme.employeescheduling".level=DEBUG
# quarkus.http.cors=true
# quarkus.http.cors.origins=*
# quarkus.http.cors.headers=*
# quarkus.http.cors.methods=*

Binary file not shown.