Compare commits
3 Commits
a9bc1e47db
...
94ae364aad
| Author | SHA1 | Date | |
|---|---|---|---|
| 94ae364aad | |||
| 9fab9f83ef | |||
| 9be454ca81 |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
target/
|
||||||
|
|
||||||
932
claude2.json
Normal file
932
claude2.json
Normal file
@ -0,0 +1,932 @@
|
|||||||
|
{
|
||||||
|
"employees": [
|
||||||
|
{
|
||||||
|
"name": "Dr. Marie Dupont",
|
||||||
|
"skills": ["MEDECIN", "SUPERVISION"],
|
||||||
|
"unavailableDates": ["2024-12-25", "2024-12-26"],
|
||||||
|
"undesiredDates": ["2024-12-24", "2024-12-31"],
|
||||||
|
"desiredDates": ["2024-12-20", "2024-12-23"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Dr. Pierre Moreau",
|
||||||
|
"skills": ["MEDECIN"],
|
||||||
|
"unavailableDates": ["2024-12-22"],
|
||||||
|
"undesiredDates": ["2024-12-29"],
|
||||||
|
"desiredDates": ["2024-12-21", "2024-12-27"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Dr. Claire Rousseau",
|
||||||
|
"skills": ["MEDECIN", "SUPERVISION"],
|
||||||
|
"unavailableDates": [],
|
||||||
|
"undesiredDates": ["2024-12-25"],
|
||||||
|
"desiredDates": ["2024-12-24", "2024-12-28"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Inf. Sophie Bernard",
|
||||||
|
"skills": ["INFIRMIER", "PRELEVEMENT"],
|
||||||
|
"unavailableDates": ["2024-12-25"],
|
||||||
|
"undesiredDates": ["2024-12-31"],
|
||||||
|
"desiredDates": ["2024-12-20", "2024-12-21"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Inf. Jean Leroy",
|
||||||
|
"skills": ["INFIRMIER", "ACCUEIL"],
|
||||||
|
"unavailableDates": ["2024-12-24", "2024-12-25"],
|
||||||
|
"undesiredDates": [],
|
||||||
|
"desiredDates": ["2024-12-27", "2024-12-30"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Inf. Anne Moreau",
|
||||||
|
"skills": ["INFIRMIER", "PRELEVEMENT"],
|
||||||
|
"unavailableDates": ["2024-12-26"],
|
||||||
|
"undesiredDates": ["2024-12-23"],
|
||||||
|
"desiredDates": ["2024-12-21", "2024-12-29"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Inf. Luc Petit",
|
||||||
|
"skills": ["INFIRMIER"],
|
||||||
|
"unavailableDates": [],
|
||||||
|
"undesiredDates": ["2024-12-20"],
|
||||||
|
"desiredDates": ["2024-12-22", "2024-12-28"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Inf. Julie Martin",
|
||||||
|
"skills": ["INFIRMIER", "ACCUEIL"],
|
||||||
|
"unavailableDates": ["2024-12-30"],
|
||||||
|
"undesiredDates": ["2024-12-25"],
|
||||||
|
"desiredDates": ["2024-12-23", "2024-12-27"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Inf. Thomas Dubois",
|
||||||
|
"skills": ["INFIRMIER", "PRELEVEMENT"],
|
||||||
|
"unavailableDates": [],
|
||||||
|
"undesiredDates": ["2024-12-22"],
|
||||||
|
"desiredDates": ["2024-12-24", "2024-12-26"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Chauffeur Michel Blanc",
|
||||||
|
"skills": ["CHAUFFEUR", "TRANSPORT"],
|
||||||
|
"unavailableDates": ["2024-12-25"],
|
||||||
|
"undesiredDates": ["2024-12-24"],
|
||||||
|
"desiredDates": ["2024-12-20", "2024-12-27"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Chauffeur Paul Girard",
|
||||||
|
"skills": ["CHAUFFEUR", "LOGISTIQUE"],
|
||||||
|
"unavailableDates": ["2024-12-26"],
|
||||||
|
"undesiredDates": [],
|
||||||
|
"desiredDates": ["2024-12-22", "2024-12-29"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Chauffeur Marc Vincent",
|
||||||
|
"skills": ["CHAUFFEUR"],
|
||||||
|
"unavailableDates": ["2024-12-31"],
|
||||||
|
"undesiredDates": ["2024-12-23"],
|
||||||
|
"desiredDates": ["2024-12-21", "2024-12-28"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Acc. Sylvie Roux",
|
||||||
|
"skills": ["ACCUEIL", "SECRETARIAT"],
|
||||||
|
"unavailableDates": ["2024-12-25", "2024-12-26"],
|
||||||
|
"undesiredDates": ["2024-12-24"],
|
||||||
|
"desiredDates": ["2024-12-20", "2024-12-30"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Acc. Nathalie Fabre",
|
||||||
|
"skills": ["ACCUEIL"],
|
||||||
|
"unavailableDates": ["2024-12-22"],
|
||||||
|
"undesiredDates": ["2024-12-29"],
|
||||||
|
"desiredDates": ["2024-12-21", "2024-12-27"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Acc. Patricia Leclerc",
|
||||||
|
"skills": ["ACCUEIL", "INFORMATIQUE"],
|
||||||
|
"unavailableDates": [],
|
||||||
|
"undesiredDates": ["2024-12-25", "2024-12-31"],
|
||||||
|
"desiredDates": ["2024-12-23", "2024-12-28"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"collectes": [
|
||||||
|
{
|
||||||
|
"id": "collecte_toulouse_centre_20241220",
|
||||||
|
"start": "2024-12-20T08:00:00",
|
||||||
|
"end": "2024-12-20T17:00:00",
|
||||||
|
"location": "Centre de collecte - Toulouse Centre",
|
||||||
|
"requiredSkills": {
|
||||||
|
"INFIRMIER": 2,
|
||||||
|
"MEDECIN": 1,
|
||||||
|
"CHAUFFEUR": 1,
|
||||||
|
"ACCUEIL": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "collecte_blagnac_20241221",
|
||||||
|
"start": "2024-12-21T09:00:00",
|
||||||
|
"end": "2024-12-21T16:00:00",
|
||||||
|
"location": "Centre de collecte - Blagnac",
|
||||||
|
"requiredSkills": {
|
||||||
|
"INFIRMIER": 2,
|
||||||
|
"MEDECIN": 1,
|
||||||
|
"CHAUFFEUR": 1,
|
||||||
|
"ACCUEIL": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "collecte_colomiers_20241222",
|
||||||
|
"start": "2024-12-22T08:30:00",
|
||||||
|
"end": "2024-12-22T17:30:00",
|
||||||
|
"location": "Centre de collecte - Colomiers",
|
||||||
|
"requiredSkills": {
|
||||||
|
"INFIRMIER": 3,
|
||||||
|
"MEDECIN": 1,
|
||||||
|
"CHAUFFEUR": 1,
|
||||||
|
"ACCUEIL": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "collecte_mobile_muret_20241223",
|
||||||
|
"start": "2024-12-23T07:30:00",
|
||||||
|
"end": "2024-12-23T18:30:00",
|
||||||
|
"location": "Collecte mobile - Muret",
|
||||||
|
"requiredSkills": {
|
||||||
|
"INFIRMIER": 2,
|
||||||
|
"MEDECIN": 1,
|
||||||
|
"CHAUFFEUR": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "collecte_tournefeuille_20241224",
|
||||||
|
"start": "2024-12-24T08:00:00",
|
||||||
|
"end": "2024-12-24T14:00:00",
|
||||||
|
"location": "Centre de collecte - Tournefeuille",
|
||||||
|
"requiredSkills": {
|
||||||
|
"INFIRMIER": 1,
|
||||||
|
"MEDECIN": 1,
|
||||||
|
"CHAUFFEUR": 1,
|
||||||
|
"ACCUEIL": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "collecte_urgence_purpan_20241227",
|
||||||
|
"start": "2024-12-27T06:00:00",
|
||||||
|
"end": "2024-12-27T20:00:00",
|
||||||
|
"location": "Hôpital Purpan - Urgence",
|
||||||
|
"requiredSkills": {
|
||||||
|
"INFIRMIER": 4,
|
||||||
|
"MEDECIN": 2,
|
||||||
|
"CHAUFFEUR": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "collecte_leguevin_20241228",
|
||||||
|
"start": "2024-12-28T09:00:00",
|
||||||
|
"end": "2024-12-28T16:00:00",
|
||||||
|
"location": "Centre de collecte - Léguevin",
|
||||||
|
"requiredSkills": {
|
||||||
|
"INFIRMIER": 2,
|
||||||
|
"MEDECIN": 1,
|
||||||
|
"CHAUFFEUR": 1,
|
||||||
|
"ACCUEIL": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "collecte_weekend_rangueil_20241229",
|
||||||
|
"start": "2024-12-29T10:00:00",
|
||||||
|
"end": "2024-12-29T18:00:00",
|
||||||
|
"location": "Hôpital Rangueil - Weekend",
|
||||||
|
"requiredSkills": {
|
||||||
|
"INFIRMIER": 3,
|
||||||
|
"MEDECIN": 1,
|
||||||
|
"CHAUFFEUR": 1,
|
||||||
|
"ACCUEIL": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "collecte_mobile_saint_gaudens_20241230",
|
||||||
|
"start": "2024-12-30T08:00:00",
|
||||||
|
"end": "2024-12-30T19:00:00",
|
||||||
|
"location": "Collecte mobile - Saint-Gaudens",
|
||||||
|
"requiredSkills": {
|
||||||
|
"INFIRMIER": 3,
|
||||||
|
"MEDECIN": 1,
|
||||||
|
"CHAUFFEUR": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "collecte_reveillon_toulouse_20241231",
|
||||||
|
"start": "2024-12-31T14:00:00",
|
||||||
|
"end": "2024-12-31T22:00:00",
|
||||||
|
"location": "Centre de collecte - Toulouse Réveillon",
|
||||||
|
"requiredSkills": {
|
||||||
|
"INFIRMIER": 2,
|
||||||
|
"MEDECIN": 1,
|
||||||
|
"CHAUFFEUR": 1,
|
||||||
|
"ACCUEIL": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shifts": [
|
||||||
|
{
|
||||||
|
"id": "shift_toulouse_centre_20241220_chauffeur",
|
||||||
|
"start": "2024-12-20T07:00:00",
|
||||||
|
"end": "2024-12-20T18:00:00",
|
||||||
|
"location": "Centre de collecte - Toulouse Centre",
|
||||||
|
"requiredSkill": "CHAUFFEUR",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_toulouse_centre_20241220",
|
||||||
|
"start": "2024-12-20T08:00:00",
|
||||||
|
"end": "2024-12-20T17:00:00",
|
||||||
|
"location": "Centre de collecte - Toulouse Centre"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_toulouse_centre_20241220_accueil",
|
||||||
|
"start": "2024-12-20T08:30:00",
|
||||||
|
"end": "2024-12-20T16:30:00",
|
||||||
|
"location": "Centre de collecte - Toulouse Centre",
|
||||||
|
"requiredSkill": "ACCUEIL",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_toulouse_centre_20241220",
|
||||||
|
"start": "2024-12-20T08:00:00",
|
||||||
|
"end": "2024-12-20T17:00:00",
|
||||||
|
"location": "Centre de collecte - Toulouse Centre"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_toulouse_centre_20241220_medecin",
|
||||||
|
"start": "2024-12-20T08:45:00",
|
||||||
|
"end": "2024-12-20T16:45:00",
|
||||||
|
"location": "Centre de collecte - Toulouse Centre",
|
||||||
|
"requiredSkill": "MEDECIN",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_toulouse_centre_20241220",
|
||||||
|
"start": "2024-12-20T08:00:00",
|
||||||
|
"end": "2024-12-20T17:00:00",
|
||||||
|
"location": "Centre de collecte - Toulouse Centre"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_toulouse_centre_20241220_infirmier_1",
|
||||||
|
"start": "2024-12-20T09:00:00",
|
||||||
|
"end": "2024-12-20T17:00:00",
|
||||||
|
"location": "Centre de collecte - Toulouse Centre",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_toulouse_centre_20241220",
|
||||||
|
"start": "2024-12-20T08:00:00",
|
||||||
|
"end": "2024-12-20T17:00:00",
|
||||||
|
"location": "Centre de collecte - Toulouse Centre"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_toulouse_centre_20241220_infirmier_2",
|
||||||
|
"start": "2024-12-20T09:00:00",
|
||||||
|
"end": "2024-12-20T17:00:00",
|
||||||
|
"location": "Centre de collecte - Toulouse Centre",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_toulouse_centre_20241220",
|
||||||
|
"start": "2024-12-20T08:00:00",
|
||||||
|
"end": "2024-12-20T17:00:00",
|
||||||
|
"location": "Centre de collecte - Toulouse Centre"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_blagnac_20241221_chauffeur",
|
||||||
|
"start": "2024-12-21T08:00:00",
|
||||||
|
"end": "2024-12-21T17:00:00",
|
||||||
|
"location": "Centre de collecte - Blagnac",
|
||||||
|
"requiredSkill": "CHAUFFEUR",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_blagnac_20241221",
|
||||||
|
"start": "2024-12-21T09:00:00",
|
||||||
|
"end": "2024-12-21T16:00:00",
|
||||||
|
"location": "Centre de collecte - Blagnac"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_blagnac_20241221_accueil",
|
||||||
|
"start": "2024-12-21T09:30:00",
|
||||||
|
"end": "2024-12-21T15:30:00",
|
||||||
|
"location": "Centre de collecte - Blagnac",
|
||||||
|
"requiredSkill": "ACCUEIL",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_blagnac_20241221",
|
||||||
|
"start": "2024-12-21T09:00:00",
|
||||||
|
"end": "2024-12-21T16:00:00",
|
||||||
|
"location": "Centre de collecte - Blagnac"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_blagnac_20241221_medecin",
|
||||||
|
"start": "2024-12-21T09:15:00",
|
||||||
|
"end": "2024-12-21T15:45:00",
|
||||||
|
"location": "Centre de collecte - Blagnac",
|
||||||
|
"requiredSkill": "MEDECIN",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_blagnac_20241221",
|
||||||
|
"start": "2024-12-21T09:00:00",
|
||||||
|
"end": "2024-12-21T16:00:00",
|
||||||
|
"location": "Centre de collecte - Blagnac"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_blagnac_20241221_infirmier_1",
|
||||||
|
"start": "2024-12-21T09:00:00",
|
||||||
|
"end": "2024-12-21T16:00:00",
|
||||||
|
"location": "Centre de collecte - Blagnac",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_blagnac_20241221",
|
||||||
|
"start": "2024-12-21T09:00:00",
|
||||||
|
"end": "2024-12-21T16:00:00",
|
||||||
|
"location": "Centre de collecte - Blagnac"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_blagnac_20241221_infirmier_2",
|
||||||
|
"start": "2024-12-21T09:00:00",
|
||||||
|
"end": "2024-12-21T16:00:00",
|
||||||
|
"location": "Centre de collecte - Blagnac",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_blagnac_20241221",
|
||||||
|
"start": "2024-12-21T09:00:00",
|
||||||
|
"end": "2024-12-21T16:00:00",
|
||||||
|
"location": "Centre de collecte - Blagnac"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_colomiers_20241222_chauffeur",
|
||||||
|
"start": "2024-12-22T07:30:00",
|
||||||
|
"end": "2024-12-22T18:30:00",
|
||||||
|
"location": "Centre de collecte - Colomiers",
|
||||||
|
"requiredSkill": "CHAUFFEUR",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_colomiers_20241222",
|
||||||
|
"start": "2024-12-22T08:30:00",
|
||||||
|
"end": "2024-12-22T17:30:00",
|
||||||
|
"location": "Centre de collecte - Colomiers"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_colomiers_20241222_accueil",
|
||||||
|
"start": "2024-12-22T08:00:00",
|
||||||
|
"end": "2024-12-22T18:00:00",
|
||||||
|
"location": "Centre de collecte - Colomiers",
|
||||||
|
"requiredSkill": "ACCUEIL",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_colomiers_20241222",
|
||||||
|
"start": "2024-12-22T08:30:00",
|
||||||
|
"end": "2024-12-22T17:30:00",
|
||||||
|
"location": "Centre de collecte - Colomiers"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_colomiers_20241222_medecin",
|
||||||
|
"start": "2024-12-22T08:45:00",
|
||||||
|
"end": "2024-12-22T17:15:00",
|
||||||
|
"location": "Centre de collecte - Colomiers",
|
||||||
|
"requiredSkill": "MEDECIN",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_colomiers_20241222",
|
||||||
|
"start": "2024-12-22T08:30:00",
|
||||||
|
"end": "2024-12-22T17:30:00",
|
||||||
|
"location": "Centre de collecte - Colomiers"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_colomiers_20241222_infirmier_1",
|
||||||
|
"start": "2024-12-22T09:00:00",
|
||||||
|
"end": "2024-12-22T17:30:00",
|
||||||
|
"location": "Centre de collecte - Colomiers",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_colomiers_20241222",
|
||||||
|
"start": "2024-12-22T08:30:00",
|
||||||
|
"end": "2024-12-22T17:30:00",
|
||||||
|
"location": "Centre de collecte - Colomiers"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_colomiers_20241222_infirmier_2",
|
||||||
|
"start": "2024-12-22T09:00:00",
|
||||||
|
"end": "2024-12-22T17:30:00",
|
||||||
|
"location": "Centre de collecte - Colomiers",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_colomiers_20241222",
|
||||||
|
"start": "2024-12-22T08:30:00",
|
||||||
|
"end": "2024-12-22T17:30:00",
|
||||||
|
"location": "Centre de collecte - Colomiers"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_colomiers_20241222_infirmier_3",
|
||||||
|
"start": "2024-12-22T09:00:00",
|
||||||
|
"end": "2024-12-22T17:30:00",
|
||||||
|
"location": "Centre de collecte - Colomiers",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_colomiers_20241222",
|
||||||
|
"start": "2024-12-22T08:30:00",
|
||||||
|
"end": "2024-12-22T17:30:00",
|
||||||
|
"location": "Centre de collecte - Colomiers"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_mobile_muret_20241223_chauffeur_1",
|
||||||
|
"start": "2024-12-23T07:00:00",
|
||||||
|
"end": "2024-12-23T19:00:00",
|
||||||
|
"location": "Collecte mobile - Muret",
|
||||||
|
"requiredSkill": "CHAUFFEUR",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_mobile_muret_20241223",
|
||||||
|
"start": "2024-12-23T07:30:00",
|
||||||
|
"end": "2024-12-23T18:30:00",
|
||||||
|
"location": "Collecte mobile - Muret"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_mobile_muret_20241223_chauffeur_2",
|
||||||
|
"start": "2024-12-23T07:00:00",
|
||||||
|
"end": "2024-12-23T19:00:00",
|
||||||
|
"location": "Collecte mobile - Muret",
|
||||||
|
"requiredSkill": "CHAUFFEUR",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_mobile_muret_20241223",
|
||||||
|
"start": "2024-12-23T07:30:00",
|
||||||
|
"end": "2024-12-23T18:30:00",
|
||||||
|
"location": "Collecte mobile - Muret"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_mobile_muret_20241223_medecin",
|
||||||
|
"start": "2024-12-23T08:00:00",
|
||||||
|
"end": "2024-12-23T18:00:00",
|
||||||
|
"location": "Collecte mobile - Muret",
|
||||||
|
"requiredSkill": "MEDECIN",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_mobile_muret_20241223",
|
||||||
|
"start": "2024-12-23T07:30:00",
|
||||||
|
"end": "2024-12-23T18:30:00",
|
||||||
|
"location": "Collecte mobile - Muret"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_mobile_muret_20241223_infirmier_1",
|
||||||
|
"start": "2024-12-23T08:30:00",
|
||||||
|
"end": "2024-12-23T18:30:00",
|
||||||
|
"location": "Collecte mobile - Muret",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_mobile_muret_20241223",
|
||||||
|
"start": "2024-12-23T07:30:00",
|
||||||
|
"end": "2024-12-23T18:30:00",
|
||||||
|
"location": "Collecte mobile - Muret"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_mobile_muret_20241223_infirmier_2",
|
||||||
|
"start": "2024-12-23T08:30:00",
|
||||||
|
"end": "2024-12-23T18:30:00",
|
||||||
|
"location": "Collecte mobile - Muret",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_mobile_muret_20241223",
|
||||||
|
"start": "2024-12-23T07:30:00",
|
||||||
|
"end": "2024-12-23T18:30:00",
|
||||||
|
"location": "Collecte mobile - Muret"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_tournefeuille_20241224_chauffeur",
|
||||||
|
"start": "2024-12-24T07:30:00",
|
||||||
|
"end": "2024-12-24T15:00:00",
|
||||||
|
"location": "Centre de collecte - Tournefeuille",
|
||||||
|
"requiredSkill": "CHAUFFEUR",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_tournefeuille_20241224",
|
||||||
|
"start": "2024-12-24T08:00:00",
|
||||||
|
"end": "2024-12-24T14:00:00",
|
||||||
|
"location": "Centre de collecte - Tournefeuille"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_tournefeuille_20241224_accueil",
|
||||||
|
"start": "2024-12-24T08:00:00",
|
||||||
|
"end": "2024-12-24T14:00:00",
|
||||||
|
"location": "Centre de collecte - Tournefeuille",
|
||||||
|
"requiredSkill": "ACCUEIL",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_tournefeuille_20241224",
|
||||||
|
"start": "2024-12-24T08:00:00",
|
||||||
|
"end": "2024-12-24T14:00:00",
|
||||||
|
"location": "Centre de collecte - Tournefeuille"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_tournefeuille_20241224_medecin",
|
||||||
|
"start": "2024-12-24T08:15:00",
|
||||||
|
"end": "2024-12-24T13:45:00",
|
||||||
|
"location": "Centre de collecte - Tournefeuille",
|
||||||
|
"requiredSkill": "MEDECIN",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_tournefeuille_20241224",
|
||||||
|
"start": "2024-12-24T08:00:00",
|
||||||
|
"end": "2024-12-24T14:00:00",
|
||||||
|
"location": "Centre de collecte - Tournefeuille"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_tournefeuille_20241224_infirmier",
|
||||||
|
"start": "2024-12-24T08:30:00",
|
||||||
|
"end": "2024-12-24T14:00:00",
|
||||||
|
"location": "Centre de collecte - Tournefeuille",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_tournefeuille_20241224",
|
||||||
|
"start": "2024-12-24T08:00:00",
|
||||||
|
"end": "2024-12-24T14:00:00",
|
||||||
|
"location": "Centre de collecte - Tournefeuille"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_urgence_purpan_20241227_chauffeur",
|
||||||
|
"start": "2024-12-27T05:30:00",
|
||||||
|
"end": "2024-12-27T20:30:00",
|
||||||
|
"location": "Hôpital Purpan - Urgence",
|
||||||
|
"requiredSkill": "CHAUFFEUR",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_urgence_purpan_20241227",
|
||||||
|
"start": "2024-12-27T06:00:00",
|
||||||
|
"end": "2024-12-27T20:00:00",
|
||||||
|
"location": "Hôpital Purpan - Urgence"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_urgence_purpan_20241227_infirmier_3",
|
||||||
|
"start": "2024-12-27T09:00:00",
|
||||||
|
"end": "2024-12-27T16:00:00",
|
||||||
|
"location": "Hôpital Purpan - Urgence",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_urgence_purpan_20241227",
|
||||||
|
"start": "2024-12-27T06:00:00",
|
||||||
|
"end": "2024-12-27T20:00:00",
|
||||||
|
"location": "Hôpital Purpan - Urgence"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_urgence_purpan_20241227_infirmier_4",
|
||||||
|
"start": "2024-12-27T15:00:00",
|
||||||
|
"end": "2024-12-27T20:00:00",
|
||||||
|
"location": "Hôpital Purpan - Urgence",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_urgence_purpan_20241227",
|
||||||
|
"start": "2024-12-27T06:00:00",
|
||||||
|
"end": "2024-12-27T20:00:00",
|
||||||
|
"location": "Hôpital Purpan - Urgence"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_leguevin_20241228_chauffeur",
|
||||||
|
"start": "2024-12-28T08:00:00",
|
||||||
|
"end": "2024-12-28T17:00:00",
|
||||||
|
"location": "Centre de collecte - Léguevin",
|
||||||
|
"requiredSkill": "CHAUFFEUR",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_leguevin_20241228",
|
||||||
|
"start": "2024-12-28T09:00:00",
|
||||||
|
"end": "2024-12-28T16:00:00",
|
||||||
|
"location": "Centre de collecte - Léguevin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_leguevin_20241228_accueil",
|
||||||
|
"start": "2024-12-28T09:30:00",
|
||||||
|
"end": "2024-12-28T15:30:00",
|
||||||
|
"location": "Centre de collecte - Léguevin",
|
||||||
|
"requiredSkill": "ACCUEIL",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_leguevin_20241228",
|
||||||
|
"start": "2024-12-28T09:00:00",
|
||||||
|
"end": "2024-12-28T16:00:00",
|
||||||
|
"location": "Centre de collecte - Léguevin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_leguevin_20241228_medecin",
|
||||||
|
"start": "2024-12-28T09:15:00",
|
||||||
|
"end": "2024-12-28T15:45:00",
|
||||||
|
"location": "Centre de collecte - Léguevin",
|
||||||
|
"requiredSkill": "MEDECIN",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_leguevin_20241228",
|
||||||
|
"start": "2024-12-28T09:00:00",
|
||||||
|
"end": "2024-12-28T16:00:00",
|
||||||
|
"location": "Centre de collecte - Léguevin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_leguevin_20241228_infirmier_1",
|
||||||
|
"start": "2024-12-28T09:00:00",
|
||||||
|
"end": "2024-12-28T16:00:00",
|
||||||
|
"location": "Centre de collecte - Léguevin",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_leguevin_20241228",
|
||||||
|
"start": "2024-12-28T09:00:00",
|
||||||
|
"end": "2024-12-28T16:00:00",
|
||||||
|
"location": "Centre de collecte - Léguevin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_leguevin_20241228_infirmier_2",
|
||||||
|
"start": "2024-12-28T09:00:00",
|
||||||
|
"end": "2024-12-28T16:00:00",
|
||||||
|
"location": "Centre de collecte - Léguevin",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_leguevin_20241228",
|
||||||
|
"start": "2024-12-28T09:00:00",
|
||||||
|
"end": "2024-12-28T16:00:00",
|
||||||
|
"location": "Centre de collecte - Léguevin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_weekend_rangueil_20241229_chauffeur",
|
||||||
|
"start": "2024-12-29T09:00:00",
|
||||||
|
"end": "2024-12-29T19:00:00",
|
||||||
|
"location": "Hôpital Rangueil - Weekend",
|
||||||
|
"requiredSkill": "CHAUFFEUR",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_weekend_rangueil_20241229",
|
||||||
|
"start": "2024-12-29T10:00:00",
|
||||||
|
"end": "2024-12-29T18:00:00",
|
||||||
|
"location": "Hôpital Rangueil - Weekend"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_weekend_rangueil_20241229_accueil",
|
||||||
|
"start": "2024-12-29T10:30:00",
|
||||||
|
"end": "2024-12-29T17:30:00",
|
||||||
|
"location": "Hôpital Rangueil - Weekend",
|
||||||
|
"requiredSkill": "ACCUEIL",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_weekend_rangueil_20241229",
|
||||||
|
"start": "2024-12-29T10:00:00",
|
||||||
|
"end": "2024-12-29T18:00:00",
|
||||||
|
"location": "Hôpital Rangueil - Weekend"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_weekend_rangueil_20241229_medecin",
|
||||||
|
"start": "2024-12-29T10:15:00",
|
||||||
|
"end": "2024-12-29T17:45:00",
|
||||||
|
"location": "Hôpital Rangueil - Weekend",
|
||||||
|
"requiredSkill": "MEDECIN",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_weekend_rangueil_20241229",
|
||||||
|
"start": "2024-12-29T10:00:00",
|
||||||
|
"end": "2024-12-29T18:00:00",
|
||||||
|
"location": "Hôpital Rangueil - Weekend"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_weekend_rangueil_20241229_infirmier_1",
|
||||||
|
"start": "2024-12-29T10:00:00",
|
||||||
|
"end": "2024-12-29T18:00:00",
|
||||||
|
"location": "Hôpital Rangueil - Weekend",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_weekend_rangueil_20241229",
|
||||||
|
"start": "2024-12-29T10:00:00",
|
||||||
|
"end": "2024-12-29T18:00:00",
|
||||||
|
"location": "Hôpital Rangueil - Weekend"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_weekend_rangueil_20241229_infirmier_2",
|
||||||
|
"start": "2024-12-29T10:00:00",
|
||||||
|
"end": "2024-12-29T18:00:00",
|
||||||
|
"location": "Hôpital Rangueil - Weekend",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_weekend_rangueil_20241229",
|
||||||
|
"start": "2024-12-29T10:00:00",
|
||||||
|
"end": "2024-12-29T18:00:00",
|
||||||
|
"location": "Hôpital Rangueil - Weekend"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_weekend_rangueil_20241229_infirmier_3",
|
||||||
|
"start": "2024-12-29T10:00:00",
|
||||||
|
"end": "2024-12-29T18:00:00",
|
||||||
|
"location": "Hôpital Rangueil - Weekend",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_weekend_rangueil_20241229",
|
||||||
|
"start": "2024-12-29T10:00:00",
|
||||||
|
"end": "2024-12-29T18:00:00",
|
||||||
|
"location": "Hôpital Rangueil - Weekend"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_mobile_saint_gaudens_20241230_chauffeur_1",
|
||||||
|
"start": "2024-12-30T07:00:00",
|
||||||
|
"end": "2024-12-30T20:00:00",
|
||||||
|
"location": "Collecte mobile - Saint-Gaudens",
|
||||||
|
"requiredSkill": "CHAUFFEUR",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_mobile_saint_gaudens_20241230",
|
||||||
|
"start": "2024-12-30T08:00:00",
|
||||||
|
"end": "2024-12-30T19:00:00",
|
||||||
|
"location": "Collecte mobile - Saint-Gaudens"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_mobile_saint_gaudens_20241230_chauffeur_2",
|
||||||
|
"start": "2024-12-30T07:00:00",
|
||||||
|
"end": "2024-12-30T20:00:00",
|
||||||
|
"location": "Collecte mobile - Saint-Gaudens",
|
||||||
|
"requiredSkill": "CHAUFFEUR",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_mobile_saint_gaudens_20241230",
|
||||||
|
"start": "2024-12-30T08:00:00",
|
||||||
|
"end": "2024-12-30T19:00:00",
|
||||||
|
"location": "Collecte mobile - Saint-Gaudens"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_mobile_saint_gaudens_20241230_medecin",
|
||||||
|
"start": "2024-12-30T08:30:00",
|
||||||
|
"end": "2024-12-30T18:30:00",
|
||||||
|
"location": "Collecte mobile - Saint-Gaudens",
|
||||||
|
"requiredSkill": "MEDECIN",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_mobile_saint_gaudens_20241230",
|
||||||
|
"start": "2024-12-30T08:00:00",
|
||||||
|
"end": "2024-12-30T19:00:00",
|
||||||
|
"location": "Collecte mobile - Saint-Gaudens"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_mobile_saint_gaudens_20241230_infirmier_1",
|
||||||
|
"start": "2024-12-30T08:00:00",
|
||||||
|
"end": "2024-12-30T19:00:00",
|
||||||
|
"location": "Collecte mobile - Saint-Gaudens",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_mobile_saint_gaudens_20241230",
|
||||||
|
"start": "2024-12-30T08:00:00",
|
||||||
|
"end": "2024-12-30T19:00:00",
|
||||||
|
"location": "Collecte mobile - Saint-Gaudens"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_mobile_saint_gaudens_20241230_infirmier_2",
|
||||||
|
"start": "2024-12-30T08:00:00",
|
||||||
|
"end": "2024-12-30T19:00:00",
|
||||||
|
"location": "Collecte mobile - Saint-Gaudens",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_mobile_saint_gaudens_20241230",
|
||||||
|
"start": "2024-12-30T08:00:00",
|
||||||
|
"end": "2024-12-30T19:00:00",
|
||||||
|
"location": "Collecte mobile - Saint-Gaudens"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_mobile_saint_gaudens_20241230_infirmier_3",
|
||||||
|
"start": "2024-12-30T08:00:00",
|
||||||
|
"end": "2024-12-30T19:00:00",
|
||||||
|
"location": "Collecte mobile - Saint-Gaudens",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_mobile_saint_gaudens_20241230",
|
||||||
|
"start": "2024-12-30T08:00:00",
|
||||||
|
"end": "2024-12-30T19:00:00",
|
||||||
|
"location": "Collecte mobile - Saint-Gaudens"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_reveillon_toulouse_20241231_chauffeur",
|
||||||
|
"start": "2024-12-31T13:30:00",
|
||||||
|
"end": "2024-12-31T22:30:00",
|
||||||
|
"location": "Centre de collecte - Toulouse Réveillon",
|
||||||
|
"requiredSkill": "CHAUFFEUR",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_reveillon_toulouse_20241231",
|
||||||
|
"start": "2024-12-31T14:00:00",
|
||||||
|
"end": "2024-12-31T22:00:00",
|
||||||
|
"location": "Centre de collecte - Toulouse Réveillon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_reveillon_toulouse_20241231_accueil",
|
||||||
|
"start": "2024-12-31T14:30:00",
|
||||||
|
"end": "2024-12-31T21:30:00",
|
||||||
|
"location": "Centre de collecte - Toulouse Réveillon",
|
||||||
|
"requiredSkill": "ACCUEIL",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_reveillon_toulouse_20241231",
|
||||||
|
"start": "2024-12-31T14:00:00",
|
||||||
|
"end": "2024-12-31T22:00:00",
|
||||||
|
"location": "Centre de collecte - Toulouse Réveillon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_reveillon_toulouse_20241231_medecin",
|
||||||
|
"start": "2024-12-31T14:15:00",
|
||||||
|
"end": "2024-12-31T21:45:00",
|
||||||
|
"location": "Centre de collecte - Toulouse Réveillon",
|
||||||
|
"requiredSkill": "MEDECIN",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_reveillon_toulouse_20241231",
|
||||||
|
"start": "2024-12-31T14:00:00",
|
||||||
|
"end": "2024-12-31T22:00:00",
|
||||||
|
"location": "Centre de collecte - Toulouse Réveillon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_reveillon_toulouse_20241231_infirmier_1",
|
||||||
|
"start": "2024-12-31T14:00:00",
|
||||||
|
"end": "2024-12-31T22:00:00",
|
||||||
|
"location": "Centre de collecte - Toulouse Réveillon",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_reveillon_toulouse_20241231",
|
||||||
|
"start": "2024-12-31T14:00:00",
|
||||||
|
"end": "2024-12-31T22:00:00",
|
||||||
|
"location": "Centre de collecte - Toulouse Réveillon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_reveillon_toulouse_20241231_infirmier_2",
|
||||||
|
"start": "2024-12-31T14:00:00",
|
||||||
|
"end": "2024-12-31T22:00:00",
|
||||||
|
"location": "Centre de collecte - Toulouse Réveillon",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_reveillon_toulouse_20241231",
|
||||||
|
"start": "2024-12-31T14:00:00",
|
||||||
|
"end": "2024-12-31T22:00:00",
|
||||||
|
"location": "Centre de collecte - Toulouse Réveillon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_urgence_purpan_20241227_medecin_1",
|
||||||
|
"start": "2024-12-27T06:00:00",
|
||||||
|
"end": "2024-12-27T14:00:00",
|
||||||
|
"location": "Hôpital Purpan - Urgence",
|
||||||
|
"requiredSkill": "MEDECIN",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_urgence_purpan_20241227",
|
||||||
|
"start": "2024-12-27T06:00:00",
|
||||||
|
"end": "2024-12-27T20:00:00",
|
||||||
|
"location": "Hôpital Purpan - Urgence"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_urgence_purpan_20241227_medecin_2",
|
||||||
|
"start": "2024-12-27T12:00:00",
|
||||||
|
"end": "2024-12-27T20:00:00",
|
||||||
|
"location": "Hôpital Purpan - Urgence",
|
||||||
|
"requiredSkill": "MEDECIN",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_urgence_purpan_20241227",
|
||||||
|
"start": "2024-12-27T06:00:00",
|
||||||
|
"end": "2024-12-27T20:00:00",
|
||||||
|
"location": "Hôpital Purpan - Urgence"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_urgence_purpan_20241227_infirmier_1",
|
||||||
|
"start": "2024-12-27T06:30:00",
|
||||||
|
"end": "2024-12-27T13:30:00",
|
||||||
|
"location": "Hôpital Purpan - Urgence",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_urgence_purpan_20241227",
|
||||||
|
"start": "2024-12-27T06:00:00",
|
||||||
|
"end": "2024-12-27T20:00:00",
|
||||||
|
"location": "Hôpital Purpan - Urgence"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_urgence_purpan_20241227_infirmier_2",
|
||||||
|
"start": "2024-12-27T13:00:00",
|
||||||
|
"end": "2024-12-27T20:00:00",
|
||||||
|
"location": "Hôpital Purpan - Urgence",
|
||||||
|
"requiredSkill": "INFIRMIER",
|
||||||
|
"collecte": {
|
||||||
|
"id": "collecte_urgence_purpan_20241227",
|
||||||
|
"start": "2024-12-27T06:00:00",
|
||||||
|
"end": "2024-12-27T20:00:00",
|
||||||
|
"location": "Hôpital Purpan - Urgence"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -162,14 +162,7 @@
|
|||||||
let autoRefreshIntervalId = null;
|
let autoRefreshIntervalId = null;
|
||||||
|
|
||||||
// Timefold server URL - modify this to match your server
|
// 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)
|
|
||||||
// const TIMEFOLD_SERVER = 'http://localhost:3000/api';
|
|
||||||
|
|
||||||
// Option 3: Si vous servez depuis le serveur Timefold
|
|
||||||
const TIMEFOLD_SERVER = window.location.origin;
|
|
||||||
|
|
||||||
// Timeline configuration
|
// Timeline configuration
|
||||||
const timelineOptions = {
|
const timelineOptions = {
|
||||||
@ -108,7 +108,7 @@
|
|||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link active" id="byLocationTab" data-bs-toggle="tab"
|
<button class="nav-link active" id="byLocationTab" data-bs-toggle="tab"
|
||||||
data-bs-target="#byLocationPanel" type="button" role="tab">
|
data-bs-target="#byLocationPanel" type="button" role="tab">
|
||||||
By location
|
By collecte
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
@ -163,14 +163,14 @@
|
|||||||
|
|
||||||
// Timefold server URL - modify this to match your server
|
// Timefold server URL - modify this to match your server
|
||||||
// Option 1: Direct (nécessite CORS activé sur le serveur)
|
// 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';
|
// const TIMEFOLD_SERVER = 'http://localhost:3000/api';
|
||||||
|
|
||||||
// Option 3: Si vous servez depuis le serveur Timefold
|
|
||||||
const TIMEFOLD_SERVER = window.location.origin;
|
|
||||||
|
|
||||||
// Timeline configuration
|
// Timeline configuration
|
||||||
const timelineOptions = {
|
const timelineOptions = {
|
||||||
timeAxis: {scale: "hour", step: 6},
|
timeAxis: {scale: "hour", step: 6},
|
||||||
@ -270,7 +270,16 @@
|
|||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function(e) {
|
reader.onload = function(e) {
|
||||||
try {
|
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;
|
loadedSchedule = jsonData;
|
||||||
|
|
||||||
// Show file info
|
// Show file info
|
||||||
@ -285,10 +294,26 @@
|
|||||||
|
|
||||||
showNotification('File loaded successfully!', 'success');
|
showNotification('File loaded successfully!', 'success');
|
||||||
} catch (error) {
|
} 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() {
|
function loadDemoData() {
|
||||||
@ -325,15 +350,49 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$.post(TIMEFOLD_SERVER + '/schedules', JSON.stringify(loadedSchedule))
|
console.log('Sending data to solve:', loadedSchedule);
|
||||||
.done(function(data) {
|
|
||||||
scheduleId = data;
|
$.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);
|
refreshSolvingButtons(true);
|
||||||
showNotification('Solving started...', 'info');
|
showNotification('Solving started...', 'info');
|
||||||
})
|
} else {
|
||||||
.fail(function(xhr) {
|
showNotification('Server returned empty response', 'danger');
|
||||||
showNotification('Failed to start solving: ' + xhr.responseText, '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() {
|
function stopSolving() {
|
||||||
@ -437,14 +496,32 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Render locations and shifts
|
// Render collectes/shifts as groups in location view
|
||||||
const locations = [...new Set(schedule.shifts.map(shift => shift.location))];
|
if (schedule.collectes && schedule.shifts) {
|
||||||
locations.forEach(location => {
|
// Create individual groups for each shift within collectes
|
||||||
byLocationGroupDataSet.add({
|
schedule.shifts.forEach((shift, index) => {
|
||||||
id: location,
|
const collecte = schedule.collectes.find(c => c.id === shift.collecte?.id);
|
||||||
content: location
|
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
|
// Render shifts
|
||||||
schedule.shifts.forEach((shift, index) => {
|
schedule.shifts.forEach((shift, index) => {
|
||||||
@ -461,17 +538,17 @@
|
|||||||
byEmployeeItemDataSet.add({
|
byEmployeeItemDataSet.add({
|
||||||
id: `shift-${index}-emp`,
|
id: `shift-${index}-emp`,
|
||||||
group: shift.employee.name,
|
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>`,
|
<span class="badge" style="background-color:${skillColor}">${shift.requiredSkill}</span></div>`,
|
||||||
start: startTime,
|
start: startTime,
|
||||||
end: endTime,
|
end: endTime,
|
||||||
style: `background-color: ${shiftColor}`
|
style: `background-color: ${shiftColor}`
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add to location timeline
|
// Add to shift-specific timeline (each shift has its own group)
|
||||||
byLocationItemDataSet.add({
|
byLocationItemDataSet.add({
|
||||||
id: `shift-${index}-loc`,
|
id: `shift-${index}-loc`,
|
||||||
group: shift.location,
|
group: `shift-group-${index}`,
|
||||||
content: `<div><strong>${shift.employee.name}</strong><br/>
|
content: `<div><strong>${shift.employee.name}</strong><br/>
|
||||||
<span class="badge" style="background-color:${skillColor}">${shift.requiredSkill}</span></div>`,
|
<span class="badge" style="background-color:${skillColor}">${shift.requiredSkill}</span></div>`,
|
||||||
start: startTime,
|
start: startTime,
|
||||||
@ -482,8 +559,8 @@
|
|||||||
// Unassigned shift
|
// Unassigned shift
|
||||||
byLocationItemDataSet.add({
|
byLocationItemDataSet.add({
|
||||||
id: `shift-${index}-unassigned`,
|
id: `shift-${index}-unassigned`,
|
||||||
group: shift.location,
|
group: `shift-group-${index}`,
|
||||||
content: `<div><strong>Unassigned</strong><br/>
|
content: `<div><strong>Non assigné</strong><br/>
|
||||||
<span class="badge bg-secondary">${shift.requiredSkill}</span></div>`,
|
<span class="badge bg-secondary">${shift.requiredSkill}</span></div>`,
|
||||||
start: startTime,
|
start: startTime,
|
||||||
end: endTime,
|
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
|
// Set timeline window
|
||||||
if (schedule.shifts.length > 0) {
|
if (schedule.shifts.length > 0) {
|
||||||
const dates = schedule.shifts.map(shift => new Date(shift.start));
|
const dates = schedule.shifts.map(shift => new Date(shift.start));
|
||||||
@ -44,3 +44,8 @@ quarkus.swagger-ui.always-include=true
|
|||||||
|
|
||||||
quarkus.log.category."ai.timefold.solver".level=DEBUG
|
quarkus.log.category."ai.timefold.solver".level=DEBUG
|
||||||
quarkus.log.category."org.acme.employeescheduling".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=*
|
||||||
File diff suppressed because one or more lines are too long
734
target/classes/META-INF/resources/upload.html
Normal file
734
target/classes/META-INF/resources/upload.html
Normal file
@ -0,0 +1,734 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
|
||||||
|
<meta content="width=device-width, initial-scale=1" name="viewport">
|
||||||
|
<title>Employee scheduling - Timefold Solver on Quarkus</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vis-timeline@7.7.2/styles/vis-timeline-graph2d.min.css" integrity="sha256-svzNasPg1yR5gvEaRei2jg+n4Pc3sVyMUWeS6xRAh6U=" crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
.vis-time-axis .vis-grid.vis-saturday,
|
||||||
|
.vis-time-axis .vis-grid.vis-sunday {
|
||||||
|
background: #D3D7CFFF;
|
||||||
|
}
|
||||||
|
.file-upload-area {
|
||||||
|
border: 2px dashed #ccc;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
.file-upload-area:hover {
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
.file-upload-area.dragover {
|
||||||
|
border-color: #007bff;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="sticky-top d-flex justify-content-center align-items-center" aria-live="polite" aria-atomic="true">
|
||||||
|
<div id="notificationPanel" style="position: absolute; top: .5rem;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="mt-4">Employee scheduling solver</h1>
|
||||||
|
<p>Upload your JSON file and generate the optimal schedule for your employees.</p>
|
||||||
|
|
||||||
|
<!-- File Upload Section -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>1. Load your data</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="file-upload-area" id="fileUploadArea">
|
||||||
|
<i class="fas fa-upload fa-3x text-muted mb-3"></i>
|
||||||
|
<p class="mb-2">Drag & drop your JSON file here or click to select</p>
|
||||||
|
<input type="file" id="fileInput" accept=".json" style="display: none;">
|
||||||
|
<button class="btn btn-outline-primary" onclick="document.getElementById('fileInput').click()">
|
||||||
|
Choose File
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="fileInfo" class="mt-2" style="display: none;">
|
||||||
|
<small class="text-success">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
<span id="fileName"></span> loaded successfully
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Or use demo data:</h6>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-secondary dropdown-toggle" type="button" id="testDataButton" data-bs-toggle="dropdown">
|
||||||
|
Select demo data
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu" id="testDataMenu">
|
||||||
|
<!-- Filled by JavaScript -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Control Section -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>2. Solve the problem</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<button id="solveButton" type="button" class="btn btn-success me-2" disabled>
|
||||||
|
<span class="fas fa-play"></span> Solve
|
||||||
|
</button>
|
||||||
|
<button id="stopSolvingButton" type="button" class="btn btn-danger me-2" style="display: none;">
|
||||||
|
<span class="fas fa-stop"></span> Stop solving
|
||||||
|
</button>
|
||||||
|
<button id="downloadButton" type="button" class="btn btn-info me-2" disabled>
|
||||||
|
<span class="fas fa-download"></span> Download Solution
|
||||||
|
</button>
|
||||||
|
<span id="unassignedShifts" class="ms-2 align-middle fw-bold"></span>
|
||||||
|
<span id="score" class="score ms-2 align-middle fw-bold">Score: ?</span>
|
||||||
|
<button id="analyzeButton" type="button" class="ms-2 btn btn-secondary" disabled>
|
||||||
|
<span class="fas fa-question"></span> Analyze
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Visualization Section -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">3. View results</h5>
|
||||||
|
<ul class="nav nav-pills" role="tablist">
|
||||||
|
<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 collecte
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="byEmployeeTab" data-bs-toggle="tab"
|
||||||
|
data-bs-target="#byEmployeePanel" type="button" role="tab">
|
||||||
|
By employee
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="tab-pane fade show active" id="byLocationPanel" role="tabpanel">
|
||||||
|
<div id="byLocationVisualization" style="height: 400px;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade" id="byEmployeePanel" role="tabpanel">
|
||||||
|
<div id="byEmployeeVisualization" style="height: 400px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Score Analysis Modal -->
|
||||||
|
<div class="modal fade" id="scoreAnalysisModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Score analysis <span id="scoreAnalysisScoreLabel"></span></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="scoreAnalysisModalContent">
|
||||||
|
<!-- Filled by JavaScript -->
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@js-joda/core@5.4.2/dist/js-joda.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vis-timeline@7.7.2/standalone/umd/vis-timeline-graph2d.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Global variables
|
||||||
|
let loadedSchedule = null;
|
||||||
|
let scheduleId = null;
|
||||||
|
let autoRefreshIntervalId = null;
|
||||||
|
|
||||||
|
// 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';
|
||||||
|
|
||||||
|
// 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';
|
||||||
|
|
||||||
|
// Timeline configuration
|
||||||
|
const timelineOptions = {
|
||||||
|
timeAxis: {scale: "hour", step: 6},
|
||||||
|
orientation: {axis: "top"},
|
||||||
|
stack: false,
|
||||||
|
xss: {disabled: true}
|
||||||
|
};
|
||||||
|
|
||||||
|
let byEmployeeGroupDataSet = new vis.DataSet();
|
||||||
|
let byEmployeeItemDataSet = new vis.DataSet();
|
||||||
|
let byLocationGroupDataSet = new vis.DataSet();
|
||||||
|
let byLocationItemDataSet = new vis.DataSet();
|
||||||
|
|
||||||
|
let byEmployeeTimeline = new vis.Timeline(
|
||||||
|
document.getElementById('byEmployeeVisualization'),
|
||||||
|
byEmployeeItemDataSet,
|
||||||
|
byEmployeeGroupDataSet,
|
||||||
|
timelineOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
let byLocationTimeline = new vis.Timeline(
|
||||||
|
document.getElementById('byLocationVisualization'),
|
||||||
|
byLocationItemDataSet,
|
||||||
|
byLocationGroupDataSet,
|
||||||
|
timelineOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
// Colors for visualization
|
||||||
|
const COLORS = {
|
||||||
|
UNAVAILABLE: '#ef2929',
|
||||||
|
UNDESIRED: '#f57900',
|
||||||
|
DESIRED: '#73d216',
|
||||||
|
DEFAULT: '#729fcf',
|
||||||
|
UNASSIGNED: '#EF292999',
|
||||||
|
SKILL_MATCH: '#8ae234',
|
||||||
|
SKILL_MISMATCH: '#ef2929'
|
||||||
|
};
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
setupEventListeners();
|
||||||
|
setupAjax();
|
||||||
|
loadDemoData();
|
||||||
|
});
|
||||||
|
|
||||||
|
function setupEventListeners() {
|
||||||
|
// File upload
|
||||||
|
const fileInput = document.getElementById('fileInput');
|
||||||
|
const fileUploadArea = document.getElementById('fileUploadArea');
|
||||||
|
|
||||||
|
fileInput.addEventListener('change', handleFileSelect);
|
||||||
|
fileUploadArea.addEventListener('click', () => fileInput.click());
|
||||||
|
fileUploadArea.addEventListener('dragover', handleDragOver);
|
||||||
|
fileUploadArea.addEventListener('drop', handleDrop);
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
$('#solveButton').click(solve);
|
||||||
|
$('#stopSolvingButton').click(stopSolving);
|
||||||
|
$('#analyzeButton').click(analyze);
|
||||||
|
$('#downloadButton').click(downloadSolution);
|
||||||
|
|
||||||
|
// Tabs
|
||||||
|
$('#byEmployeeTab').on('shown.bs.tab', () => byEmployeeTimeline.redraw());
|
||||||
|
$('#byLocationTab').on('shown.bs.tab', () => byLocationTimeline.redraw());
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupAjax() {
|
||||||
|
$.ajaxSetup({
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json,text/plain'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileSelect(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
readJsonFile(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragOver(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.currentTarget.classList.add('dragover');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrop(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.currentTarget.classList.remove('dragover');
|
||||||
|
const file = event.dataTransfer.files[0];
|
||||||
|
if (file && file.type === 'application/json') {
|
||||||
|
readJsonFile(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJsonFile(file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
try {
|
||||||
|
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
|
||||||
|
$('#fileName').text(file.name);
|
||||||
|
$('#fileInfo').show();
|
||||||
|
|
||||||
|
// Enable solve button
|
||||||
|
$('#solveButton').prop('disabled', false);
|
||||||
|
|
||||||
|
// Render the loaded data
|
||||||
|
renderSchedule(jsonData);
|
||||||
|
|
||||||
|
showNotification('File loaded successfully!', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
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, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDemoData() {
|
||||||
|
// Load demo data options from server
|
||||||
|
$.get(TIMEFOLD_SERVER + '/demo-data')
|
||||||
|
.done(function(data) {
|
||||||
|
data.forEach(item => {
|
||||||
|
$('#testDataMenu').append(
|
||||||
|
`<a class="dropdown-item" href="#" onclick="loadDemoDataSet('${item}')">${item}</a>`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.fail(function() {
|
||||||
|
console.log('Demo data not available');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDemoDataSet(dataSet) {
|
||||||
|
$.get(TIMEFOLD_SERVER + '/demo-data/' + dataSet)
|
||||||
|
.done(function(data) {
|
||||||
|
loadedSchedule = data;
|
||||||
|
renderSchedule(data);
|
||||||
|
$('#solveButton').prop('disabled', false);
|
||||||
|
showNotification(`Demo data ${dataSet} loaded!`, 'success');
|
||||||
|
})
|
||||||
|
.fail(function() {
|
||||||
|
showNotification('Failed to load demo data', 'danger');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function solve() {
|
||||||
|
if (!loadedSchedule) {
|
||||||
|
showNotification('Please load data first', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
} 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() {
|
||||||
|
if (scheduleId) {
|
||||||
|
$.ajax({
|
||||||
|
url: TIMEFOLD_SERVER + '/schedules/' + scheduleId,
|
||||||
|
type: 'DELETE'
|
||||||
|
}).done(function() {
|
||||||
|
refreshSolvingButtons(false);
|
||||||
|
refreshSchedule();
|
||||||
|
showNotification('Solving stopped', 'info');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshSchedule() {
|
||||||
|
if (scheduleId) {
|
||||||
|
$.get(TIMEFOLD_SERVER + '/schedules/' + scheduleId)
|
||||||
|
.done(function(schedule) {
|
||||||
|
loadedSchedule = schedule;
|
||||||
|
renderSchedule(schedule);
|
||||||
|
$('#downloadButton').prop('disabled', false);
|
||||||
|
})
|
||||||
|
.fail(function() {
|
||||||
|
refreshSolvingButtons(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshSolvingButtons(solving) {
|
||||||
|
if (solving) {
|
||||||
|
$('#solveButton').hide();
|
||||||
|
$('#stopSolvingButton').show();
|
||||||
|
if (autoRefreshIntervalId == null) {
|
||||||
|
autoRefreshIntervalId = setInterval(refreshSchedule, 2000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$('#solveButton').show();
|
||||||
|
$('#stopSolvingButton').hide();
|
||||||
|
if (autoRefreshIntervalId != null) {
|
||||||
|
clearInterval(autoRefreshIntervalId);
|
||||||
|
autoRefreshIntervalId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSchedule(schedule) {
|
||||||
|
// Clear existing data
|
||||||
|
byEmployeeGroupDataSet.clear();
|
||||||
|
byEmployeeItemDataSet.clear();
|
||||||
|
byLocationGroupDataSet.clear();
|
||||||
|
byLocationItemDataSet.clear();
|
||||||
|
|
||||||
|
// Update 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++;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#unassignedShifts').text(
|
||||||
|
unassignedCount === 0
|
||||||
|
? 'No unassigned shifts'
|
||||||
|
: `${unassignedCount} unassigned shifts`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render employees
|
||||||
|
schedule.employees.forEach((employee, index) => {
|
||||||
|
// Add employee group
|
||||||
|
const skillsBadges = employee.skills.map(skill =>
|
||||||
|
`<span class="badge bg-secondary me-1">${skill}</span>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
byEmployeeGroupDataSet.add({
|
||||||
|
id: employee.name,
|
||||||
|
content: `<div><strong>${employee.name}</strong><br/>${skillsBadges}</div>`
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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) => {
|
||||||
|
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,
|
||||||
|
start: startDate,
|
||||||
|
end: endDate,
|
||||||
|
type: 'background',
|
||||||
|
style: `opacity: 0.5; 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: `<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) => {
|
||||||
|
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: `<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 shift-specific timeline (each shift has its own group)
|
||||||
|
byLocationItemDataSet.add({
|
||||||
|
id: `shift-${index}-loc`,
|
||||||
|
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,
|
||||||
|
end: endTime,
|
||||||
|
style: `background-color: ${shiftColor}`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Unassigned shift
|
||||||
|
byLocationItemDataSet.add({
|
||||||
|
id: `shift-${index}-unassigned`,
|
||||||
|
group: `shift-group-${index}`,
|
||||||
|
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}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
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];
|
||||||
|
|
||||||
|
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) {
|
||||||
|
showNotification('No score to analyze', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: TIMEFOLD_SERVER + '/schedules/analyze',
|
||||||
|
type: 'PUT',
|
||||||
|
data: JSON.stringify(loadedSchedule)
|
||||||
|
}).done(function(analysis) {
|
||||||
|
displayScoreAnalysis(analysis);
|
||||||
|
}).fail(function() {
|
||||||
|
showNotification('Analysis failed', 'danger');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayScoreAnalysis(analysis) {
|
||||||
|
$('#scoreAnalysisScoreLabel').text(`(${loadedSchedule.score})`);
|
||||||
|
|
||||||
|
const content = $('#scoreAnalysisModalContent');
|
||||||
|
content.empty();
|
||||||
|
|
||||||
|
const table = $(`
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Constraint</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Matches</th>
|
||||||
|
<th>Score</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
`);
|
||||||
|
|
||||||
|
analysis.constraints.forEach(constraint => {
|
||||||
|
const icon = constraint.score.includes('hard') && constraint.score.includes('-')
|
||||||
|
? '<i class="fas fa-exclamation-triangle text-danger"></i>'
|
||||||
|
: '<i class="fas fa-check-circle text-success"></i>';
|
||||||
|
|
||||||
|
const row = $(`
|
||||||
|
<tr>
|
||||||
|
<td>${icon}</td>
|
||||||
|
<td>${constraint.name}</td>
|
||||||
|
<td>${constraint.score.includes('hard') ? 'Hard' : 'Soft'}</td>
|
||||||
|
<td>${constraint.matches?.length || 0}</td>
|
||||||
|
<td>${constraint.score}</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
|
||||||
|
table.find('tbody').append(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
content.append(table);
|
||||||
|
new bootstrap.Modal('#scoreAnalysisModal').show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadSolution() {
|
||||||
|
if (loadedSchedule) {
|
||||||
|
const dataStr = JSON.stringify(loadedSchedule, null, 2);
|
||||||
|
const dataBlob = new Blob([dataStr], {type: 'application/json'});
|
||||||
|
const url = URL.createObjectURL(dataBlob);
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = 'timefold_solution.json';
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
showNotification('Solution downloaded', 'success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNotification(message, type) {
|
||||||
|
const alertDiv = $(`
|
||||||
|
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
|
||||||
|
${message}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
$('#notificationPanel').append(alertDiv);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
alertDiv.alert('close');
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -44,3 +44,8 @@ quarkus.swagger-ui.always-include=true
|
|||||||
|
|
||||||
quarkus.log.category."ai.timefold.solver".level=DEBUG
|
quarkus.log.category."ai.timefold.solver".level=DEBUG
|
||||||
quarkus.log.category."org.acme.employeescheduling".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=*
|
||||||
BIN
target/employee-scheduling-dev.jar
Normal file
BIN
target/employee-scheduling-dev.jar
Normal file
Binary file not shown.
BIN
target/quarkus/bootstrap/dev-app-model.dat
Normal file
BIN
target/quarkus/bootstrap/dev-app-model.dat
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user