Loading module/Oscar/src/Oscar/Service/ProjectGrantApiService.php +3 −1 Changes for module/Oscar/src/Oscar/Service/ProjectGrantApiService.php: 3 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -916,6 +916,7 @@ class ProjectGrantApiService implements $dates = $activityDateRepository->getMilestonesByIdsActivity([$activity->getId()]); $now = date('Y-m-d'); $nowDt = new \DateTime($now); /** @var ActivityDate $data */ foreach ($dates as $data) { Loading @@ -929,7 +930,8 @@ class ProjectGrantApiService implements 'type_id' => $data->getType()->getId(), 'finished' => $data->getFinished(), 'finishedBy' => $data->getFinishedBy(), 'validable' => $data->getType()->isFinishable() 'validable' => $data->getType()->isFinishable(), 'late' => $data->isLate($nowDt) //'hasProgression' => $data->getProgressInfo() ]; $out['entities'][] = $data; Loading ui/src/components/DateSlider.vue 0 → 100644 +239 −0 Changes for ui/src/components/DateSlider.vue: 239 added lines, 0 removed lines. Original line number Diff line number Diff line <template> <div class="timeline-container"> TOTO <button @click="scroll('gauche')" class="nav-btn left">◀</button> <div ref="scrollContainer" class="timeline-scroll"> <div class="timeline-line"></div> <div v-for="(item, index) in itemsTries" :key="index" :ref="el => { if (item.estLePlusProche) elementPlusProche = el }" class="timeline-item" :class="{ 'is-closest': item.estLePlusProche }" > <div class="timeline-date"> {{ formaterDate(item.date) }} </div> <div class="timeline-dot"> <i class="icon-bank" v-if="item.type === 'payment'"></i> <i class="icon-calendar" v-else></i> </div> <div class="timeline-content"> <h4>{{ item.label }}</h4> {{ item.status }} / {{ item.late }} <!-- <span class="badge" :class="item.status">{{ item.status }}</span> --> </div> </div> </div> <button @click="scroll('droite')" class="nav-btn right">▶</button> </div> </template> <script setup> import { computed, ref, onMounted, nextTick } from 'vue' // Vos données initiales passées en Props const props = defineProps({ donnees: { type: Array, required: true } }) const scrollContainer = ref(null) const elementPlusProche = ref(null) // 1. Trier les dates et identifier la plus proche d'aujourd'hui const itemsTries = computed(() => { const aujourdhui = new Date() // On trie d'abord le tableau par date const listeTriee = [...props.donnees].sort((a, b) => new Date(a.date) - new Date(b.date)) // On cherche l'index de l'élément le plus proche d'aujourd'hui let indexPlusProche = 0 let differenceMinimale = Infinity listeTriee.forEach((item, index) => { const diff = Math.abs(new Date(item.date) - aujourdhui) if (diff < differenceMinimale) { differenceMinimale = diff indexPlusProche = index } }) // On retourne le tableau en ajoutant un drapeau "estLePlusProche" return listeTriee.map((item, index) => ({ ...item, estLePlusProche: index === indexPlusProche })) }) // 2. Centrer automatiquement sur l'élément le plus proche au chargement onMounted(async () => { await nextTick() // On attend que le DOM soit rendu if (elementPlusProche.value) { elementPlusProche.value.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' // Centre l'élément horizontalement }) } }) // 3. Gestion du défilement des boutons const scroll = (direction) => { if (scrollContainer.value) { const valeurDefilement = 300 scrollContainer.value.scrollBy({ left: direction === 'gauche' ? -valeurDefilement : valeurDefilement, behavior: 'smooth' }) } } // Outil pour afficher joliment la date const formaterDate = (dateString) => { return new Date(dateString).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' }) } </script> <style scoped> /* Conteneur principal */ .timeline-container { display: flex; align-items: center; position: relative; width: 100%; padding: 40px 0; background: #f9f9f9; border-radius: 8px; } /* Zone de défilement masquant la scrollbar */ .timeline-scroll { display: flex; overflow-x: auto; scroll-behavior: smooth; width: 100%; padding: 20px 50px; position: relative; scrollbar-width: none; /* Firefox */ } .timeline-scroll::-webkit-scrollbar { display: none; /* Chrome, Safari, Opera */ } /* La ligne horizontale */ .timeline-line { position: absolute; top: 50%; left: 0; right: 0; height: 4px; background: #ddd; transform: translateY(-50%); z-index: 1; } /* Un bloc d'événement */ .timeline-item { position: relative; display: flex; flex-direction: column; align-items: center; min-width: 150px; margin: 0 1em; z-index: 2; transition: transform 0.3s ease; } /* Style de mise en valeur de la date la plus proche */ .timeline-item.is-closest { transform: scale(1.1); } .timeline-item.is-closest .timeline-date { background: #3b82f6; color: white; font-weight: bold; } /* Éléments internes */ .timeline-date { background: #fff; padding: .25em 1em; border-radius: 20px; font-size: 0.85rem; box-shadow: 0 0px 8px rgba(0,0,0,0.25); margin-bottom: .25em; } .timeline-dot { width: 32px; height: 32px; border-radius: 50%; background: #94a3b8; border: 3px solid #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.2); margin-bottom: 0px; color: white; } .timeline-dot i { position: absolute; transform: translate(1px, 0px); } /* Couleurs selon le type */ .timeline-dot.milestone { background: #ec4899; } .timeline-dot.payment { background: #10b981; } .timeline-content { background: white; padding: 10px 15px; border-radius: 6px; text-align: center; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1); } .timeline-content h4 { margin: 0 0 5px 0; font-size: 0.95rem; } /* Boutons de navigation */ .nav-btn { position: absolute; top: 50%; transform: translateY(-50%); background: white; border: 1px solid #ddd; border-radius: 50%; width: 40px; height: 40px; cursor: pointer; z-index: 10; box-shadow: 0 2px 5px rgba(0,0,0,0.15); display: flex; align-items: center; justify-content: center; } .nav-btn:hover { background: #f0f0f0; } .left { left: 10px; } .right { right: 10px; } /* Badges de statut */ .badge { font-size: 0.75rem; padding: 2px 6px; border-radius: 4px; background: #e2e8f0; } </style> No newline at end of file ui/src/views/Activity.vue +12 −5 Changes for ui/src/views/Activity.vue: 12 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -289,12 +289,15 @@ <div class="tab-content" id="nav-tabContent"> <div class="tab-pane fade show active" id="nav-home" role="tabpanel" aria-labelledby="nav-home-tab" tabindex="0"> Générale {{ milestones }} <DateSlider :donnees="timeline" /> <div class="date-slider"> <pre> <!--<pre> {{ payments }} {{ milestones }} ----- {{ timeline }} </pre> </pre>--> </div> </div> <div class="tab-pane fade" id="nav-profile" role="tabpanel" aria-labelledby="nav-profile-tab" tabindex="0"> Loading Loading @@ -872,6 +875,7 @@ import WorkpackagesActivity from "./WorkpackagesActivity.vue"; import 'vue-json-pretty/lib/styles.css'; import PersonDisplay from "../components/PersonDisplay.vue"; import vScrollStatus from "../directives/vScrollStatus.js"; import DateSlider from "../components/DateSlider.vue"; axios.defaults.headers.common['Accept'] = 'application/json'; axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; Loading @@ -887,6 +891,7 @@ export default { ActivityLogs, ActivityDocument, ActivitySpentSynthesis, DateSlider, EntityWithRole, Loader, Payments, Loading Loading @@ -983,7 +988,8 @@ export default { timeline.push({ label: item.type.label, date: item.dateStart, status: 'test', late: item.late, status: item.late ? 'error' : 'autre', type: 'milestone' }) }) Loading @@ -991,15 +997,16 @@ export default { if( this.payments ){ this.payments.forEach(item => { timeline.push({ label: "Versement " + item.statusLabel, label: "Versement de " + item.amount + "€", date: item.status == 2 ? item.datePayment : item.datePredicted, late: item.late, status: 'test', type: 'payment' }) }) } return timeline; return timeline.sort((a,b) => new Date(a.date) - new Date(b.date)); }, personsWP() { if (!this.persons) { Loading Loading
module/Oscar/src/Oscar/Service/ProjectGrantApiService.php +3 −1 Changes for module/Oscar/src/Oscar/Service/ProjectGrantApiService.php: 3 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -916,6 +916,7 @@ class ProjectGrantApiService implements $dates = $activityDateRepository->getMilestonesByIdsActivity([$activity->getId()]); $now = date('Y-m-d'); $nowDt = new \DateTime($now); /** @var ActivityDate $data */ foreach ($dates as $data) { Loading @@ -929,7 +930,8 @@ class ProjectGrantApiService implements 'type_id' => $data->getType()->getId(), 'finished' => $data->getFinished(), 'finishedBy' => $data->getFinishedBy(), 'validable' => $data->getType()->isFinishable() 'validable' => $data->getType()->isFinishable(), 'late' => $data->isLate($nowDt) //'hasProgression' => $data->getProgressInfo() ]; $out['entities'][] = $data; Loading
ui/src/components/DateSlider.vue 0 → 100644 +239 −0 Changes for ui/src/components/DateSlider.vue: 239 added lines, 0 removed lines. Original line number Diff line number Diff line <template> <div class="timeline-container"> TOTO <button @click="scroll('gauche')" class="nav-btn left">◀</button> <div ref="scrollContainer" class="timeline-scroll"> <div class="timeline-line"></div> <div v-for="(item, index) in itemsTries" :key="index" :ref="el => { if (item.estLePlusProche) elementPlusProche = el }" class="timeline-item" :class="{ 'is-closest': item.estLePlusProche }" > <div class="timeline-date"> {{ formaterDate(item.date) }} </div> <div class="timeline-dot"> <i class="icon-bank" v-if="item.type === 'payment'"></i> <i class="icon-calendar" v-else></i> </div> <div class="timeline-content"> <h4>{{ item.label }}</h4> {{ item.status }} / {{ item.late }} <!-- <span class="badge" :class="item.status">{{ item.status }}</span> --> </div> </div> </div> <button @click="scroll('droite')" class="nav-btn right">▶</button> </div> </template> <script setup> import { computed, ref, onMounted, nextTick } from 'vue' // Vos données initiales passées en Props const props = defineProps({ donnees: { type: Array, required: true } }) const scrollContainer = ref(null) const elementPlusProche = ref(null) // 1. Trier les dates et identifier la plus proche d'aujourd'hui const itemsTries = computed(() => { const aujourdhui = new Date() // On trie d'abord le tableau par date const listeTriee = [...props.donnees].sort((a, b) => new Date(a.date) - new Date(b.date)) // On cherche l'index de l'élément le plus proche d'aujourd'hui let indexPlusProche = 0 let differenceMinimale = Infinity listeTriee.forEach((item, index) => { const diff = Math.abs(new Date(item.date) - aujourdhui) if (diff < differenceMinimale) { differenceMinimale = diff indexPlusProche = index } }) // On retourne le tableau en ajoutant un drapeau "estLePlusProche" return listeTriee.map((item, index) => ({ ...item, estLePlusProche: index === indexPlusProche })) }) // 2. Centrer automatiquement sur l'élément le plus proche au chargement onMounted(async () => { await nextTick() // On attend que le DOM soit rendu if (elementPlusProche.value) { elementPlusProche.value.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' // Centre l'élément horizontalement }) } }) // 3. Gestion du défilement des boutons const scroll = (direction) => { if (scrollContainer.value) { const valeurDefilement = 300 scrollContainer.value.scrollBy({ left: direction === 'gauche' ? -valeurDefilement : valeurDefilement, behavior: 'smooth' }) } } // Outil pour afficher joliment la date const formaterDate = (dateString) => { return new Date(dateString).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' }) } </script> <style scoped> /* Conteneur principal */ .timeline-container { display: flex; align-items: center; position: relative; width: 100%; padding: 40px 0; background: #f9f9f9; border-radius: 8px; } /* Zone de défilement masquant la scrollbar */ .timeline-scroll { display: flex; overflow-x: auto; scroll-behavior: smooth; width: 100%; padding: 20px 50px; position: relative; scrollbar-width: none; /* Firefox */ } .timeline-scroll::-webkit-scrollbar { display: none; /* Chrome, Safari, Opera */ } /* La ligne horizontale */ .timeline-line { position: absolute; top: 50%; left: 0; right: 0; height: 4px; background: #ddd; transform: translateY(-50%); z-index: 1; } /* Un bloc d'événement */ .timeline-item { position: relative; display: flex; flex-direction: column; align-items: center; min-width: 150px; margin: 0 1em; z-index: 2; transition: transform 0.3s ease; } /* Style de mise en valeur de la date la plus proche */ .timeline-item.is-closest { transform: scale(1.1); } .timeline-item.is-closest .timeline-date { background: #3b82f6; color: white; font-weight: bold; } /* Éléments internes */ .timeline-date { background: #fff; padding: .25em 1em; border-radius: 20px; font-size: 0.85rem; box-shadow: 0 0px 8px rgba(0,0,0,0.25); margin-bottom: .25em; } .timeline-dot { width: 32px; height: 32px; border-radius: 50%; background: #94a3b8; border: 3px solid #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.2); margin-bottom: 0px; color: white; } .timeline-dot i { position: absolute; transform: translate(1px, 0px); } /* Couleurs selon le type */ .timeline-dot.milestone { background: #ec4899; } .timeline-dot.payment { background: #10b981; } .timeline-content { background: white; padding: 10px 15px; border-radius: 6px; text-align: center; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1); } .timeline-content h4 { margin: 0 0 5px 0; font-size: 0.95rem; } /* Boutons de navigation */ .nav-btn { position: absolute; top: 50%; transform: translateY(-50%); background: white; border: 1px solid #ddd; border-radius: 50%; width: 40px; height: 40px; cursor: pointer; z-index: 10; box-shadow: 0 2px 5px rgba(0,0,0,0.15); display: flex; align-items: center; justify-content: center; } .nav-btn:hover { background: #f0f0f0; } .left { left: 10px; } .right { right: 10px; } /* Badges de statut */ .badge { font-size: 0.75rem; padding: 2px 6px; border-radius: 4px; background: #e2e8f0; } </style> No newline at end of file
ui/src/views/Activity.vue +12 −5 Changes for ui/src/views/Activity.vue: 12 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -289,12 +289,15 @@ <div class="tab-content" id="nav-tabContent"> <div class="tab-pane fade show active" id="nav-home" role="tabpanel" aria-labelledby="nav-home-tab" tabindex="0"> Générale {{ milestones }} <DateSlider :donnees="timeline" /> <div class="date-slider"> <pre> <!--<pre> {{ payments }} {{ milestones }} ----- {{ timeline }} </pre> </pre>--> </div> </div> <div class="tab-pane fade" id="nav-profile" role="tabpanel" aria-labelledby="nav-profile-tab" tabindex="0"> Loading Loading @@ -872,6 +875,7 @@ import WorkpackagesActivity from "./WorkpackagesActivity.vue"; import 'vue-json-pretty/lib/styles.css'; import PersonDisplay from "../components/PersonDisplay.vue"; import vScrollStatus from "../directives/vScrollStatus.js"; import DateSlider from "../components/DateSlider.vue"; axios.defaults.headers.common['Accept'] = 'application/json'; axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; Loading @@ -887,6 +891,7 @@ export default { ActivityLogs, ActivityDocument, ActivitySpentSynthesis, DateSlider, EntityWithRole, Loader, Payments, Loading Loading @@ -983,7 +988,8 @@ export default { timeline.push({ label: item.type.label, date: item.dateStart, status: 'test', late: item.late, status: item.late ? 'error' : 'autre', type: 'milestone' }) }) Loading @@ -991,15 +997,16 @@ export default { if( this.payments ){ this.payments.forEach(item => { timeline.push({ label: "Versement " + item.statusLabel, label: "Versement de " + item.amount + "€", date: item.status == 2 ? item.datePayment : item.datePredicted, late: item.late, status: 'test', type: 'payment' }) }) } return timeline; return timeline.sort((a,b) => new Date(a.date) - new Date(b.date)); }, personsWP() { if (!this.persons) { Loading