ldc-supportweb/resources/js/components/MacroPlanningComponent.vue

738 lines
32 KiB
Vue
Raw Normal View History

<template>
<!-- TODO: order by name, status -->
<div ref="macroschedule" class="macroschedule">
<div class="row mb-3">
<div class="col-1">
<button class="btn btn-primary" @click.prevent="goPreviousWeek()">←</button>
</div>
<div class="col">
<h1 class="text-secondary">Plan de charge</h1>
<div class="input-group">
<label for="scheduleDateSelect" class="input-group-text">Voir date&nbsp;:&nbsp;</label>
<input id="scheduleDateSelect" class="form-control" type="date" :value="startMoment.format('YYYY-MM-DD')" @input="showMacroScheduleDate($event.target.value)">
</div>
</div>
<div class="col">
<fieldset>
<legend>Légende</legend>
<template v-if="isAdmin">
<span v-for="activityEnum of activitiesEnum" :key="activityEnum.value" :class="'p-1 m-2 border badge badge-activity-' + activityEnum.value">{{ activityEnum.label }}</span>
<span class="p-1 m-2 border badge badge-marker">Marqueur</span>
<span class="p-1 m-2 border badge badge-today">Aujourd'hui</span>
</template>
<template v-else>
<span class="p-1 m-2 border badge badge-activity-light">Libre</span>
<span class="p-1 m-2 border badge badge-activity-dark">Occupé</span>
</template>
</fieldset>
</div>
<div class="col-1">
<button class="btn btn-primary" @click.prevent="goNextWeek()">→</button>
</div>
</div>
<div class="row justify-content-center">
<div class="col-12">
<table class="table table-bordered">
<thead>
<tr>
<th scope="col" rowspan="3" class="align-middle">{{ isAdmin ? 'Projets' : '' }}</th>
<th scope="col" rowspan="3" class="align-middle">{{ isAdmin ? 'Statut' : '' }}</th>
<th v-if="daysThisMonth.length" :colspan="daysThisMonth.length" class="text-center">
{{ daysThisMonth[0].format('MMMM') | ucfirst }}
</th>
<th v-if="daysNextMonth.length" :colspan="daysNextMonth.length" class="text-center">
{{ daysNextMonth[0].format('MMMM') | ucfirst }}
</th>
</tr>
<tr>
<th v-for="weekNumber in macroscheduleConfig.numberOfWeeksToShow" :key="weekNumber" :colspan="7 - 2" class="text-center">
SEMAINE&nbsp;{{ currentWeek + weekNumber - 1}}
</th>
</tr>
<tr>
<template v-for="(currentDay, index) of daysToShow">
<th scope="col" v-if="dayShouldbeenSeen(currentDay)" :key="index" :class="'dayCell p-0 text-center ' + (currentDay.isSame(todayMoment, 'day') ? 'today' : '')">
{{ currentDay.format('dd') }}&nbsp;{{ String(currentDay.date()).padStart(2, '0') }}
</th>
</template>
</tr>
</thead>
<tbody>
<template v-if="isAdmin">
<tr v-for="project of projects" :key="project.id" class="projectRow">
<th scope="col">
<a :href="route('projects.show', {PROJECT: project.id })" :class="'rounded-1 p-1 project text-nowrap text-truncate overflow-hidden bg-enum-'+project.status">{{ project.name | str_limit(45) }}</a>
</th>
<td>
<span class="fw-bold text-nowrap text-truncate overflow-hidden">{{ projectStatusEnum[project.status].label }}</span>
</td>
<template v-for="(currentDay, index) of daysToShow">
<td :class="'p-0 border border-dark activities ' + (currentDay.isSame(todayMoment, 'day') ? 'today' : '')" v-if="dayShouldbeenSeen(currentDay)" :key="index">
<div
v-for="(activity, index) in [getActivity(project, getMorningMoment(currentDay)), getActivity(project, getAfterNoonMoment(currentDay))]"
:key="index"
:class="getActivityClass(activity)+ ' activity ' + (hoverActivityId === activity.id ? 'hover' : '')"
@click="showModale(project, activity, currentDay, index === 0)"
@mouseover="setActivityHover(activity)"
@mouseleave="setActivityHover(activity, false)">
<div :title="getActivityTooltip(activity)" data-bs-toggle="tooltip">
&nbsp;
</div>
<template v-if="projectHasMarkersForTheDay(currentDay, project)">
<div
class="marker"
data-bs-toggle="tooltip"
data-bs-html="true"
:title="getMarkersTooltip(currentDay, project)"></div>
</template>
</div>
</td>
</template>
</tr>
</template>
<tr class="projectRow">
<th scope="col" class="border-end-0 d-flex justify-content-between"><span class="fw-bold h5">Disponibilité</span></th>
<td class="border-start-0"></td>
<template v-for="(currentDay, index) of daysToShow">
<td :class="'p-0 border border-dark activities ' + (currentDay.isSame(todayMoment, 'day') ? 'today' : '')" v-if="dayShouldbeenSeen(currentDay)" :key="index">
<template v-if="isAdmin">
<div class="activity text-center" :style="{ 'background-color' : 'rgba(0,0,0,'+ getLoadPercent(getMorningMoment(currentDay)) }">
{{ getLoad(getMorningMoment(currentDay)) > 1 ? '&#128293;' : '&nbsp;' }}
</div>
<div class="activity text-center" :style="{ 'background-color' : 'rgba(0,0,0,'+ getLoadPercent(getAfterNoonMoment(currentDay)) }">
{{ getLoad(getAfterNoonMoment(currentDay)) > 1 ? '&#128293;' : '&nbsp;' }}
</div>
</template>
<template v-else>
<div :class="'activity text-center ' +(getLoad(getMorningMoment(currentDay)) > macroscheduleConfig.loadDevThreshold ? 'bg-dark' : 'bg-light')">&nbsp;</div>
<div :class="'activity text-center ' +(getLoad(getAfterNoonMoment(currentDay)) > macroscheduleConfig.loadDevThreshold ? 'bg-dark' : 'bg-light')">&nbsp;</div>
</template>
</td>
</template>
</tr>
</tbody>
</table>
</div>
</div>
<!--
- une Date début, et un choix matin/après-midi
- une Date Fin, et un choix matin/après-midi
- une activité (cf. liste annexe ci-dessous)
- une charge (en nombre d’hommes occupés)
- un commentaire
-->
<div ref="editModal" class="modal fade" tabindex="-1" aria-labelledby="modalLabel" aria-hidden="true" >
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalLabel" ref="modalLabel">Activité pour {{ modaleProps.name }}</h5>
</div>
<div class="modal-body">
<div class="container">
<div class="row justify-content-center">
<div v-if="error" class="alert alert-danger" role="alert">
{{ error }}
</div>
<div class="col-8">
<div class="form-group mb-3">
<label for="categorie" class="col-form-label text-md-right ms-5">Activité</label>
<select id="categorie" ref="type" class="form-select" aria-label="Sélectionnez une catégorie" :disabled="modalLoading" v-model="modaleProps.activity" required>
<option :value="undefined" disabled>Sélectionnez une activité</option>
<option v-for="activityEnum in activitiesEnum" :key="activityEnum.value" :value="activityEnum.value" :class="activityEnum.value">{{ activityEnum.label }}</option>
</select>
</div>
<div class="form-group mb-3">
<label for="start" class="col-form-label text-md-right ms-5">Date début</label>
<div class="input-group mb-3">
<input class="form-control" id="start" type="date" name="start" :value="moment(modaleProps.start).format('YYYY-MM-DD')" @input="updateDateField($event, 'start')" :disabled="modalLoading">
<select class="form-select" :disabled="modalLoading" v-model="modaleProps.startMorning" @input="updateHourField($event, 'start')">
<option value="1" :selected="moment(modaleProps.start).format('HH') === '00'">Matin</option>
<option value="0" :selected="moment(modaleProps.start).format('HH') === '12'">Midi</option>
</select>
</div>
</div>
<div class="form-group mb-3">
<label for="end" class="col-form-label text-md-right ms-5">Date fin</label>
<div class="input-group mb-3">
<input class="form-control" id="end" type="date" name="end" :value="moment(modaleProps.end).format('YYYY-MM-DD')" @input="updateDateField($event, 'end')" :disabled="modalLoading">
<select class="form-select" :disabled="modalLoading" v-model="modaleProps.endMorning" @input="updateHourField($event, 'end')">
<option value="1" :selected="moment(modaleProps.end).format('HH') === '11'">Midi</option>
<option value="0" :selected="moment(modaleProps.end).format('HH') === '23'">Soir</option>
</select>
</div>
</div>
<div class="form-group mb-3">
<label for="comment" class="col-form-label text-md-right ms-5">Commentaire</label>
<textarea ref="comment" class="form-control" name="comment" id="comment" rows="2" placeholder="" @keydown="limitLines($event)" v-model="modaleProps.comment" :disabled="modalLoading"></textarea>
</div>
<div class="form-group mb-3">
<label for="load" class="col-form-label text-md-right ms-5">Charge <i>(nombre de personnes consommées sur la période)</i></label>
<input type="number" class="form-control" name="load" id="load" v-model="modaleProps.load" :disabled="modalLoading" min="1"/>
</div>
</div>
</div>
<div class="row justify-content-center">
<div class="col-8 text-center">
<button class="btn btn-primary me-3" type="button" :disabled="modalLoading" @click="saveActivity()">
<span v-if="modalLoading" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
{{ modalLoading ? 'Chargement...' : 'Valider' }}
</button>
<button class="btn btn-danger" @click="modale.hide();$nextTick(refreshTooltips)" :disabled="modalLoading">Fermer</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Toastify from 'toastify-js'
import axios from 'axios';
export default {
mounted() {
let self = this
window.Toastify = Toastify
let data = JSON.parse(this.$parent.$options.json)
this.projects = data.projects
this.loadCollection = data.loadCollection
this.macroscheduleConfig = data.macroscheduleConfig
this.activitiesEnum = data.activitiesEnum
this.projectStatusEnum = data.projectStatusEnum
this.today = data.today
this.startDay = data.startDay
this.isAdmin = data.isAdmin
this.$nextTick(this.refreshTooltips)
this.modale = new this.bootstrap.Modal(this.$refs.editModal)
this.$refs.editModal.addEventListener('hide.bs.modal', function (e) {
if(self.modalLoading) {
e.preventDefault()
return false
}
})
},
data() {
return {
isAdmin: false,
today: '',
startDay: '',
textLimit: 254,
hoverActivityId: null,
error: '',
modaleProps: {
activity: null,
start: '',
startMorning: 1,
end: '',
endMorning: 1,
load: 0,
comment: '',
name: '',
project: null
},
currentActivity: null,
projects: [],
activitiesEnum: [],
projectStatusEnum: [],
macroscheduleConfig: [],
loadCollection: {},
modale: null,
modalLoading: false,
tooltipTriggerList: []
};
},
computed: {
todayMoment() {
return this.moment(this.today)
},
startMoment() {
return this.moment(this.startDay)
},
selectedMoment() {
return this.moment(this.startMoment.week(), 'w').hours(0).minutes(0).seconds(0).milliseconds(0).add(1, 'day').startOf('week')
},
currentMonthName() {
return this.selectedMoment.format('MMMM')
},
currentWeek() {
return this.selectedMoment.week()
},
daysToShow() {
let days = [], startMoment = this.selectedMoment.clone()
for (let i = this.macroscheduleConfig.numberOfWeeksToShow; i > 0; i--) {
for(let dayNumber = 0; dayNumber < 7; dayNumber++) {
days.push(startMoment)
startMoment = startMoment.clone().add(1, 'day')
}
}
return days
},
daysThisMonth() {
let days = this.daysToShow, currentMonth = days[0]?.month(),
out = []
for (let day of days) {
if(currentMonth === day.month() && this.dayShouldbeenSeen(day)) {
out.push(day)
}
}
return out
},
daysNextMonth() {
let days = this.daysToShow, currentMonth = days[0]?.month(),
out = []
for (let day of days) {
if(currentMonth !== day.month() && this.dayShouldbeenSeen(day)) {
out.push(day)
}
}
return out
}
},
watch: {
modaleProps: {
handler(object) {
// * Validate Date fields
this.error = ''
this.validateDateField(this.moment(this.modaleProps.start), 'start')
this.validateDateField(this.moment(this.modaleProps.end), 'end')
},
deep: true
},
currentActivity: {
handler(activityObject) {
if (activityObject.project_id) {
let project = this.getProject(activityObject.project_id)
// * Insert (or update) activity
this.$set(project.activities, activityObject.id, Object.assign({}, activityObject))
this.$nextTick(this.refreshTooltips)
this.$forceUpdate()
}
},
deep: true
}
},
methods: {
validateDateField(moment, fieldName) {
if(!this.dayShouldbeenSeen(moment)) {
this.error = 'La date de ' + (fieldName === 'start' ? 'début' : 'fin') + ' ne peut être un jour de weekend.'
return false
}
return true
},
updateDateField(e, fieldName) {
let date = this.moment(this.modaleProps[fieldName]),
time = Number.parseInt(this.modaleProps[fieldName + 'Morning']) === 1 ?
this.getMorningMoment(date, fieldName === 'start') :
this.getAfterNoonMoment(date, fieldName === 'start'),
input = this.moment(e.target.value)
if(this.validateDateField(input, fieldName)) {
this.error = ''
this.$set(this.modaleProps, fieldName, input.format('YYYY-MM-DD') + 'T' + time.format('HH:mm:ss'))
}
},
updateHourField(e, fieldName) {
let date = this.moment(this.modaleProps[fieldName])
if(Number.parseInt(e.target.value) === 1) {
this.$set(this.modaleProps, fieldName, this.getMorningMoment(date, fieldName === 'start').format('YYYY-MM-DD\THH:mm:ss'))
} else {
this.$set(this.modaleProps, fieldName, this.getAfterNoonMoment(date, fieldName === 'start').format('YYYY-MM-DD\THH:mm:ss'))
}
},
saveActivity() {
let self = this,
activity = Object.assign({}, this.currentActivity)
if(this.error.length) {
return;
}
self.modalLoading = true
activity.activity = self.modaleProps.activity
activity.start = self.modaleProps.start
activity.end = self.modaleProps.end
activity.load = self.modaleProps.load
activity.comment = self.modaleProps.comment
activity.project_id = self.modaleProps.project.id
axios.post(self.route('macroplanning.activity'), activity)
.then(response => {
self.error = ''
self.sendToast('L\'activité à été mis à jour')
self.$nextTick(() => {
self.modalLoading = false
self.modale.hide()
activity.id = response.data.id
self.$set(self, 'currentActivity', activity)
})
if(response.data.hasOverlap) {
window.location.reload()
}
}).catch(error => {
if(error.response.status === 429) {
location.reload()
}
if(error.response.status === 422) {
if(error.response.data.errors) {
self.error = ''
for(let id in error.response.data.errors) {
let field = error.response.data.errors[id]
self.error += field.join('\n') + '\n'
}
return
} else if(error.response.data.message) {
self.error = error.response.data.message
return
}
}
self.error = error
}).then(() => {
self.modalLoading = false
self.$nextTick(self.refreshTooltips)
})
},
showModale(project, activity, selectedMoment, isMorning = true) {
selectedMoment = selectedMoment.clone()
let self = this,
startMoment = (activity.start ? this.moment(activity.start) :
(isMorning ? this.getMorningMoment(selectedMoment, true) : this.getAfterNoonMoment(selectedMoment, true))),
endMoment = (activity.end ? this.moment(activity.end) :
(isMorning ? this.getMorningMoment(selectedMoment, false) : this.getAfterNoonMoment(selectedMoment, false)))
this.modaleProps = {
activity: activity.activity,
start: activity.start ?? startMoment.format('YYYY-MM-DD\THH:mm:ss'),
startMorning: startMoment.format('HH') === '00' ? 1 : 0,
end: activity.end ?? endMoment.format('YYYY-MM-DD\THH:mm:ss'),
endMorning: endMoment.format('HH') === '11' ? 1 : 0,
load: activity.load,
comment: activity.comment,
name: project.name,
project: project
}
this.currentActivity = activity
this.modale.show()
setTimeout(() => {
self.$nextTick(self.refreshTooltips)
}, 2000)
},
dayShouldbeenSeen(moment) {
return moment.day() !== 6 && moment.day() !== 0
},
projectHasMarkersForTheDay(moment, project) {
for(let marker of project.markers) {
if (moment.isSame(marker.marker, 'day')) {
return true
}
}
return false
},
setActivityHover(activity, activate = true) {
if(!activate) {
this.hoverActivityId = null
} else {
this.hoverActivityId = activity.activity !== undefined ? activity.id : ''
}
},
// * GETTERS
getLoadPercent(moment) {
let load = this.getLoad(moment)
return load > 1 ? 1 : load
},
getLoad(moment) {
return this.loadCollection[moment.format('YYYY-MM-DD 00:00:00')] ?? null
},
getActivityTooltip(activity) {
let activityEnumLabel = activity.activity !== undefined ? (this.activitiesEnum[activity.activity] ? this.activitiesEnum[activity.activity].label : '') : ''
return activity.activity !== undefined ? `${activityEnumLabel}, Charge ${activity.load} personnes, ${activity.comment}` : ''
},
getActivityClass(activity) {
return activity ? `bg-enum-${activity.activity}` : 'bg-white'
},
getProject(project_id) {
return this.projects[project_id]
},
getActivity(project, moment) {
for(let activity of Object.values(project.activities)) {
if(moment.isBetween(this.moment(activity.start, this.moment.DATETIME_LOCAL_SECONDS), this.moment(activity.end, this.moment.DATETIME_LOCAL_SECONDS), 'hour', '[]')) {
return activity
}
}
return {}
},
getMorningMoment(moment, start = true) {
moment = moment.clone()
return start ? moment.hours(0).minutes(0).seconds(0) : moment.hours(11).minutes(59).seconds(0)
},
getAfterNoonMoment(moment, start = true) {
moment = moment.clone()
return start ? moment.hours(12).minutes(0).seconds(0) : moment.hours(23).minutes(59).seconds(0)
},
getMarkersTooltip(moment, project) {
let tooltip = ''
for(let marker of project.markers) {
if (moment.isSame(marker.marker, 'day')) {
tooltip += `${this.moment(marker.marker).format('DD/MM/YYYY')} ${marker.label}<br>`
}
}
return tooltip
},
// * NAVIGATION
goPreviousWeek() {
let moment = this.startMoment ?? this.todayMoment
moment = this.moment(moment.week(), 'w')
this.showMacroScheduleDate('', moment.subtract(1, 'week'))
},
goNextWeek() {
let moment = this.startMoment ?? this.todayMoment
moment = this.moment(moment.week(), 'w')
this.showMacroScheduleDate('', moment.add(1, 'week'))
},
showMacroScheduleDate(dateString, moment = null) {
let url = new URL(window.location)
moment = moment ?? this.moment(dateString)
location = url.origin+ url.pathname + '?date='+moment.format('DD/MM/YYYY')
},
sendToast(message) {
Toastify({
text: message,
duration: 3000,
gravity: "top",
position: "right",
offset: {
x: '2rem',
y: '3rem'
},
style: {
background: "linear-gradient(to right, #00317d, #0058ca)",
}
}).showToast();
},
refreshTooltips() {
let exisitingToolips = document.querySelectorAll('.tooltip[role="tooltip"]')
for(let tooltip of exisitingToolips) {
tooltip.remove()
}
this.tooltipTriggerList = [].slice.call(this.$refs.macroschedule.querySelectorAll('[data-bs-toggle="tooltip"]'))
this.tooltipTriggerList.map((tooltipTriggerEl) => {
return new this.bootstrap.Tooltip(tooltipTriggerEl, { delay: 500 })
})
},
limitLines(e) {
let keynum, lines = this.$refs.comment.value.split('\n').length;
// IE
if(window.event) {
keynum = e.keyCode;
// Netscape/Firefox/Opera
} else if(e.which) {
keynum = e.which;
}
if(keynum != 8 && this.$refs.comment.value.length >= this.textLimit) {
e.preventDefault()
return false;
}
if(keynum == 13 && lines == this.$refs.comment.rows) {
e.preventDefault()
return false;
}
}
}
};
</script>
<style lang="scss" scoped>
@import './../../sass/variables';
@import './../../sass/complement';
@import "toastify-js/src/toastify.css";
.macroschedule {
@each $index, $color in $enum-activities {
.badge-activity-#{$index} {
background-color: $color;
color: a11y-color(#353535 ,$color);
}
}
.badge-marker {
background-color: rgba(255, 2, 204, 0.6);
color: a11y-color(#353535 ,rgba(255, 2, 204, 0.6));
}
.badge-today {
background-color: rgba(0, 255, 0, 0.6);
color: a11y-color(#353535 ,rgba(0, 255, 0, 0.6));
}
.badge-activity-light {
background-color: $light;
color: a11y-color(#353535 ,$light);
}
.badge-activity-dark {
background-color: $dark;
color: a11y-color(#FFF ,$dark);
}
.project {
@each $index, $color in $enum-project-status {
&.bg-enum-#{$index} {
background-color: $color;
color: a11y-color(#f6f6f6 ,$color);
}
}
}
th.today,td.today > div {
position: relative;
&:before {
content: ' ';
position: absolute;
top: 50%;
left: 0.2rem;
transform: translateY(-50%);
height: 110%;
width: 0.3rem;
background-color: rgba(0, 255, 0, 0.6);
}
}
tr:last-child > td.today {
position: relative;
&:after {
content: '↑ Aujourd\'hui';
position: absolute;
bottom: -2.5rem;
left: 0;
background-color: rgb(0, 255, 0);
padding: 0.2rem;
border: 1px solid rgba(0, 0, 0, 0.4);
border-radius: 0.2rem;
width: 7rem;
}
}
.dayCell {
width: 3rem;
}
.projectRow {
&:not(:first-child) {
border-top: 2px solid black;
}
}
.activities .activity {
&:not(:first-child) {
border-top: 1px solid black;
}
}
$buttonBgColor: #4d4d4d;
.activity {
position: relative;
.bg-white {
height: 100%;
width: 100%;
}
@each $index, $color in $enum-activities {
&.bg-enum-#{$index} {
background-color: $color;
color: a11y-color(#353535 ,$color);
height: 100%;
width: 100%;
}
}
// Reset button Style to nice one
button {
display: inline;
border: none;
padding: 0.6rem 3rem;
margin: 0;
text-decoration: none;
background: $buttonBgColor;
font-size: 1rem;
cursor: pointer;
text-align: center;
transition: background 250ms ease-in-out, transform 150ms ease;
-webkit-appearance: none;
-moz-appearance: none;
}
button:disabled,
button[disabled] {
color: #aaa;
}
button:hover,
button:focus {
background: darken($buttonBgColor, 10%);
}
button:focus {
outline: 1px solid #fff;
outline-offset: -4px;
}
button:active {
transform: scale(0.99);
}
.btn {
display: none;
position: absolute;
top: 0.1rem;
left: 0.1rem;
padding: 0;
width: 1.8rem;
height: 1.8rem;
margin: 0;
background-color: $buttonBgColor;
&:hover,
&:focus {
background-color: darken($buttonBgColor, 10%);
}
&.btn-clear {
color: darken($color: #fff, $amount: 5);
}
}
.infos {
display: none;
}
&:hover {
cursor: pointer;
}
&.hover > div {
position: relative;
z-index: 2;
background-color: rgba(0, 0, 0, 0.2);
}
& > div.marker {
z-index: 200;
position: absolute;
top: 50%;
right: 0.2rem;
transform: translateY(-50%);
height: 110%;
width: 0.7rem;
background-color: rgba(255, 2, 204, 0.6);
}
}
}
</style>