ldc-supportweb/resources/js/components/MacroPlanningComponent.vue
Jérémy Munsch 40c38fa7d6
Secure Front APP
- Remove external JS and CSS dependencies
- Use Vuejs runtime
- Add xss sanitizer
- Normalize Laravel mix + add cleaner
- Update npm deps
- Upgrade bootswatch theme
2022-05-23 17:41:14 +02:00

737 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>