1151 lines
36 KiB
Vue
1151 lines
36 KiB
Vue
<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" v-on: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 :
|
||
</label>
|
||
<input
|
||
id="scheduleDateSelect"
|
||
class="form-control"
|
||
type="date"
|
||
:value="startMoment.format('YYYY-MM-DD')"
|
||
v-on:input="showMacroScheduleDate($event.target ? ($event.target as HTMLInputElement).value : '')"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="col">
|
||
<fieldset>
|
||
<legend>Légende</legend>
|
||
<template v-if="isAdmin">
|
||
<span
|
||
v-for="(activityEnum, index) of activitiesEnum"
|
||
:key="index"
|
||
:class="`p-1 m-2 border badge badge-activity-${String(activityEnum.value)}`"
|
||
>
|
||
{{ String(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" v-on: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"
|
||
>
|
||
{{ ucfirst(daysThisMonth[0].format("MMMM")) }}
|
||
</th>
|
||
<th
|
||
v-if="daysNextMonth.length"
|
||
:colspan="daysNextMonth.length"
|
||
class="text-center"
|
||
>
|
||
{{ ucfirst(daysNextMonth[0].format("MMMM")) }}
|
||
</th>
|
||
</tr>
|
||
<tr>
|
||
<th
|
||
v-if="macroscheduleConfig !== null"
|
||
v-for="weekNumber in macroscheduleConfig.numberOfWeeksToShow"
|
||
:key="weekNumber"
|
||
:colspan="7 - 2"
|
||
class="text-center"
|
||
>
|
||
SEMAINE {{ 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") }} {{
|
||
String(currentDay.date()).padStart(2, "0")
|
||
}}
|
||
</th>
|
||
</template>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<template v-if="isAdmin">
|
||
<tr v-for="(project, index) of projects" :key="index" class="projectRow">
|
||
<th scope="col">
|
||
<a
|
||
:href="getProjectRoute(project)"
|
||
:class="
|
||
'rounded-1 p-1 project text-nowrap text-truncate overflow-hidden bg-enum-' +
|
||
project.status
|
||
"
|
||
>
|
||
{{ str_limit(String(project.name), 45) }}
|
||
</a>
|
||
</th>
|
||
<td>
|
||
<span class="fw-bold text-nowrap text-truncate overflow-hidden">
|
||
{{ String(projectStatusEnum[project.status as number].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(getMorningMoment(currentDay), project),
|
||
getActivity(getAfterNoonMoment(currentDay), project),
|
||
]"
|
||
:key="index"
|
||
:class="
|
||
getActivityClass(activity) +
|
||
' activity ' +
|
||
(hoverActivityId === activity.id ? 'hover' : '')
|
||
"
|
||
v-on:click="showModale(project, activity, currentDay, index === 0)"
|
||
v-on:mouseover="setActivityHover(activity)"
|
||
v-on:mouseleave="setActivityHover(activity, false)"
|
||
>
|
||
<div :title="getActivityTooltip(activity)" data-bs-toggle="tooltip">
|
||
|
||
</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 ? "🔥" : " "
|
||
}}
|
||
</div>
|
||
<div
|
||
class="activity text-center"
|
||
:style="{
|
||
'background-color':
|
||
'rgba(0,0,0,' + getLoadPercent(getAfterNoonMoment(currentDay)),
|
||
}"
|
||
>
|
||
{{
|
||
getLoad(getAfterNoonMoment(currentDay)) > 1
|
||
? "🔥"
|
||
: " "
|
||
}}
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<div
|
||
v-if="macroscheduleConfig !== null"
|
||
:class="
|
||
'activity text-center ' +
|
||
(getLoad(getMorningMoment(currentDay)) >
|
||
macroscheduleConfig.loadDevThreshold
|
||
? 'bg-dark'
|
||
: 'bg-light')
|
||
"
|
||
>
|
||
|
||
</div>
|
||
<div
|
||
v-if="macroscheduleConfig !== null"
|
||
:class="
|
||
'activity text-center ' +
|
||
(getLoad(getAfterNoonMoment(currentDay)) >
|
||
macroscheduleConfig.loadDevThreshold
|
||
? 'bg-dark'
|
||
: 'bg-light')
|
||
"
|
||
>
|
||
|
||
</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, index) in activitiesEnum"
|
||
:key="index"
|
||
: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="momentJs(modaleProps.start).format('YYYY-MM-DD')"
|
||
v-on:input="updateDateField($event, 'start')"
|
||
:disabled="modalLoading"
|
||
/>
|
||
<select
|
||
class="form-select"
|
||
:disabled="modalLoading"
|
||
v-model="modaleProps.startMorning"
|
||
v-on:input="updateHourField($event, 'start')"
|
||
>
|
||
<option
|
||
value="1"
|
||
:selected="momentJs(modaleProps.start).format('HH') === '00'"
|
||
>
|
||
Matin
|
||
</option>
|
||
<option
|
||
value="0"
|
||
:selected="momentJs(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="momentJs(modaleProps.end).format('YYYY-MM-DD')"
|
||
v-on:input="updateDateField($event, 'end')"
|
||
:disabled="modalLoading"
|
||
/>
|
||
<select
|
||
class="form-select"
|
||
:disabled="modalLoading"
|
||
v-model="modaleProps.endMorning"
|
||
v-on:input="updateHourField($event, 'end')"
|
||
>
|
||
<option
|
||
value="1"
|
||
:selected="momentJs(modaleProps.end).format('HH') === '11'"
|
||
>
|
||
Midi
|
||
</option>
|
||
<option
|
||
value="0"
|
||
:selected="momentJs(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=""
|
||
v-on: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"
|
||
v-on: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"
|
||
v-on:click.prevent="
|
||
modale ? modale.hide() : null;
|
||
$nextTick(refreshTooltips);
|
||
"
|
||
:disabled="modalLoading"
|
||
>
|
||
Fermer
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts">
|
||
import { defineComponent } from "vue";
|
||
import * as Toastify from "toastify-js";
|
||
import axios from "axios";
|
||
import trans from "../modules/trans";
|
||
import route from "../modules/route";
|
||
import { Moment } from "moment";
|
||
import * as momentJs from "moment";
|
||
import type { Modal } from "bootstrap";
|
||
|
||
export default defineComponent({
|
||
mixins: [trans, route, { methods: { momentJs } }],
|
||
mounted() {
|
||
let self = this;
|
||
window.Toastify = Toastify;
|
||
|
||
const json = String(this.$attrs.json ?? "{}"),
|
||
data = JSON.parse(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 window.bootstrap.Modal(this.$refs.editModal as HTMLElement);
|
||
(this.$refs.editModal as HTMLElement).addEventListener(
|
||
"hide.bs.modal",
|
||
function (e: Event) {
|
||
if (self.modalLoading) {
|
||
e.preventDefault();
|
||
return false;
|
||
}
|
||
}
|
||
);
|
||
this.initTooltips();
|
||
},
|
||
data(): {
|
||
isAdmin: boolean;
|
||
today: string;
|
||
startDay: string;
|
||
textLimit: number;
|
||
hoverActivityId: string | null;
|
||
error: string | undefined;
|
||
modaleProps: {
|
||
[key: string]: LaravelModel | number | string | null | 1 | 0;
|
||
activity: number | null;
|
||
start: string;
|
||
startMorning: 1 | 0;
|
||
end: string;
|
||
endMorning: 1 | 0;
|
||
load: number;
|
||
comment: string;
|
||
name: string;
|
||
project: LaravelModel | null;
|
||
};
|
||
currentActivity: LaravelModel | null;
|
||
projects: LaravelModelList;
|
||
activitiesEnum: LaravelModelList;
|
||
projectStatusEnum: LaravelModelList;
|
||
macroscheduleConfig: {
|
||
loadDevThreshold: number;
|
||
numberOfWeeksToShow: number;
|
||
} | null;
|
||
loadCollection: Record<string, number> | null;
|
||
modale: Modal | null;
|
||
modalLoading: boolean;
|
||
} {
|
||
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: null,
|
||
loadCollection: {},
|
||
modale: null,
|
||
modalLoading: false,
|
||
};
|
||
},
|
||
computed: {
|
||
todayMoment(): Moment {
|
||
return this.momentJs(this.today);
|
||
},
|
||
startMoment(): Moment {
|
||
return this.momentJs(this.startDay);
|
||
},
|
||
selectedMoment(): Moment {
|
||
return this.momentJs(this.startMoment.week(), "w")
|
||
.hours(0)
|
||
.minutes(0)
|
||
.seconds(0)
|
||
.milliseconds(0)
|
||
.add(1, "day")
|
||
.startOf("week");
|
||
},
|
||
currentMonthName(): string {
|
||
return this.selectedMoment.format("MMMM");
|
||
},
|
||
currentWeek(): number {
|
||
return this.selectedMoment.week();
|
||
},
|
||
daysToShow(): Moment[] {
|
||
let days = [],
|
||
startMoment = this.selectedMoment.clone();
|
||
if (!this.macroscheduleConfig) {
|
||
return [];
|
||
}
|
||
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(): Moment[] {
|
||
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(): Moment[] {
|
||
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() {
|
||
// * Validate Date fields
|
||
this.error = "";
|
||
this.validateDateField(this.momentJs(this.modaleProps.start), "start");
|
||
this.validateDateField(this.momentJs(this.modaleProps.end), "end");
|
||
},
|
||
deep: true,
|
||
},
|
||
currentActivity: {
|
||
handler(activityObject: LaravelModel | null) {
|
||
if (activityObject && activityObject.project_id) {
|
||
const project = this.getProject(activityObject.project_id as number);
|
||
// * Insert (or update) activity
|
||
(project.activities as LaravelModelList)[
|
||
activityObject.id as number
|
||
] = Object.assign({}, activityObject);
|
||
this.$nextTick(this.refreshTooltips);
|
||
this.$forceUpdate();
|
||
}
|
||
},
|
||
deep: true,
|
||
},
|
||
},
|
||
methods: {
|
||
validateDateField(moment: Moment, fieldName: string): boolean {
|
||
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: Event, fieldName: string): void {
|
||
const date = this.momentJs(this.modaleProps[fieldName]),
|
||
target = e.target,
|
||
time =
|
||
Number.parseInt(String(this.modaleProps[`${fieldName}Morning`])) === 1
|
||
? this.getMorningMoment(date, fieldName === "start")
|
||
: this.getAfterNoonMoment(date, fieldName === "start"),
|
||
input = target ? this.momentJs((target as HTMLInputElement).value) : null;
|
||
|
||
if (!input) {
|
||
throw new Error("updateDateField target input is undefined");
|
||
}
|
||
|
||
if (this.validateDateField(input, fieldName)) {
|
||
this.error = "";
|
||
this.modaleProps[fieldName] =
|
||
input.format("YYYY-MM-DD") + "T" + time.format("HH:mm:ss");
|
||
}
|
||
},
|
||
updateHourField(e: Event, fieldName: string): void {
|
||
const date = this.momentJs(this.modaleProps[fieldName]),
|
||
target = e.target;
|
||
if (target && Number.parseInt((target as HTMLInputElement).value) === 1) {
|
||
this.modaleProps[fieldName] = this.getMorningMoment(
|
||
date,
|
||
fieldName === "start"
|
||
).format("YYYY-MM-DD\THH:mm:ss");
|
||
} else {
|
||
this.modaleProps[fieldName] = this.getAfterNoonMoment(
|
||
date,
|
||
fieldName === "start"
|
||
).format("YYYY-MM-DD\THH:mm:ss");
|
||
}
|
||
},
|
||
saveActivity(): void {
|
||
const self = this,
|
||
activity = Object.assign({}, this.currentActivity) as LaravelModel,
|
||
routeUri = self.route("macroplanning.activity");
|
||
|
||
if (self.error?.length) {
|
||
return;
|
||
}
|
||
|
||
if (!routeUri) {
|
||
throw new Error("macroplanning.activity is undefined");
|
||
}
|
||
|
||
self.modalLoading = true;
|
||
|
||
if (self.modaleProps.activity === null) {
|
||
throw new Error("modaleProps.activity is null");
|
||
}
|
||
if (self.modaleProps.project === null) {
|
||
throw new Error("modaleProps.project is null");
|
||
}
|
||
|
||
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(routeUri, activity)
|
||
.then((response) => {
|
||
self.error = "";
|
||
self.sendToast("L'activité à été mis à jour");
|
||
self.$nextTick(() => {
|
||
if (!self.modale) {
|
||
throw new Error("modale is undefined");
|
||
}
|
||
self.modalLoading = false;
|
||
self.modale.hide();
|
||
activity.id = response.data.id;
|
||
self.currentActivity = activity;
|
||
});
|
||
if (response.data.hasOverlap) {
|
||
window.location.reload();
|
||
}
|
||
})
|
||
.catch((error: any) => {
|
||
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: LaravelModel,
|
||
activity: LaravelModel,
|
||
selectedMoment: Moment,
|
||
isMorning: boolean = true
|
||
) {
|
||
selectedMoment = selectedMoment.clone();
|
||
|
||
if (!this.modale) {
|
||
throw new Error("modale is undefined");
|
||
}
|
||
|
||
const self = this,
|
||
startMoment = activity.start
|
||
? this.momentJs(String(activity.start))
|
||
: isMorning
|
||
? this.getMorningMoment(selectedMoment, true)
|
||
: this.getAfterNoonMoment(selectedMoment, true),
|
||
endMoment = activity.end
|
||
? this.momentJs(String(activity.end))
|
||
: isMorning
|
||
? this.getMorningMoment(selectedMoment, false)
|
||
: this.getAfterNoonMoment(selectedMoment, false);
|
||
|
||
this.modaleProps = {
|
||
activity: activity.activity as number | null,
|
||
start: String(activity.start ?? startMoment.format("YYYY-MM-DD\THH:mm:ss")),
|
||
startMorning: startMoment.format("HH") === "00" ? 1 : 0,
|
||
end: String(activity.end ?? endMoment.format("YYYY-MM-DD\THH:mm:ss")),
|
||
endMorning: endMoment.format("HH") === "11" ? 1 : 0,
|
||
load: Number.parseInt(String(activity.load)),
|
||
comment: String(activity.comment ?? ""),
|
||
name: String(project.name ?? ""),
|
||
project: project,
|
||
};
|
||
this.currentActivity = activity;
|
||
this.modale.show();
|
||
|
||
setTimeout(() => {
|
||
self.$nextTick(self.refreshTooltips);
|
||
}, 2000);
|
||
},
|
||
dayShouldbeenSeen(moment: Moment) {
|
||
return moment.day() !== 6 && moment.day() !== 0;
|
||
},
|
||
projectHasMarkersForTheDay(moment: Moment, project: LaravelModel) {
|
||
for (let marker of project.markers as LaravelModelList) {
|
||
if (moment.isSame(String(marker.marker), "day")) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
},
|
||
|
||
setActivityHover(activity: LaravelModel, activate = true): void {
|
||
if (!activate) {
|
||
this.hoverActivityId = null;
|
||
} else {
|
||
this.hoverActivityId = activity.id !== undefined ? String(activity.id) : "";
|
||
}
|
||
},
|
||
|
||
// * GETTERS
|
||
getLoadPercent(moment: Moment): number {
|
||
const load = this.getLoad(moment);
|
||
if (load === null) {
|
||
return 0;
|
||
}
|
||
return load > 1 ? 1 : load;
|
||
},
|
||
getLoad(moment: Moment): number {
|
||
if (!this.loadCollection) {
|
||
throw new Error("loadCollection is not defined");
|
||
}
|
||
return this.loadCollection[moment.format("YYYY-MM-DD 00:00:00")] ?? 0;
|
||
},
|
||
getActivityTooltip(activity: LaravelModel): string {
|
||
let activityEnumLabel =
|
||
activity.activity !== undefined
|
||
? this.activitiesEnum[activity.activity as number]
|
||
? this.activitiesEnum[activity.activity as number].label
|
||
: ""
|
||
: "";
|
||
return activity.activity !== undefined
|
||
? `${activityEnumLabel}, Charge ${activity.load} personnes, ${activity.comment}`
|
||
: "";
|
||
},
|
||
getActivityClass(activity: LaravelModel): string {
|
||
return activity ? `bg-enum-${activity.activity}` : "bg-white";
|
||
},
|
||
getProject(project_id: number): LaravelModel {
|
||
return this.projects[project_id];
|
||
},
|
||
getActivity(moment: Moment, project: LaravelModel): LaravelModel {
|
||
for (let activity of Object.values(project.activities) as LaravelModelList) {
|
||
if (
|
||
moment.isBetween(
|
||
this.momentJs(
|
||
String(activity.start),
|
||
momentJs.HTML5_FMT.DATETIME_LOCAL_SECONDS
|
||
),
|
||
this.momentJs(
|
||
String(activity.end),
|
||
momentJs.HTML5_FMT.DATETIME_LOCAL_SECONDS
|
||
),
|
||
"hour",
|
||
"[]"
|
||
)
|
||
) {
|
||
return activity;
|
||
}
|
||
}
|
||
return {};
|
||
},
|
||
getMorningMoment(moment: Moment, start: boolean = true): Moment {
|
||
moment = moment.clone();
|
||
return start
|
||
? moment.hours(0).minutes(0).seconds(0)
|
||
: moment.hours(11).minutes(59).seconds(0);
|
||
},
|
||
getAfterNoonMoment(moment: Moment, start: boolean = true): Moment {
|
||
moment = moment.clone();
|
||
return start
|
||
? moment.hours(12).minutes(0).seconds(0)
|
||
: moment.hours(23).minutes(59).seconds(0);
|
||
},
|
||
getMarkersTooltip(moment: Moment, project: LaravelModel): string {
|
||
let tooltip = "";
|
||
for (let marker of project.markers as LaravelModelList) {
|
||
if (moment.isSame(String(marker.marker), "day")) {
|
||
tooltip += `${this.momentJs(String(marker.marker)).format("DD/MM/YYYY")} ${
|
||
marker.label
|
||
}<br>`;
|
||
}
|
||
}
|
||
return tooltip;
|
||
},
|
||
getProjectRoute(project: LaravelModel): string {
|
||
const route = this.route("projects.show", { PROJECT: project.id as number });
|
||
if (!route) {
|
||
throw new Error("undefined route projects.show");
|
||
}
|
||
return route;
|
||
},
|
||
|
||
// * FILTERS
|
||
str_limit(value: string, size: number = 25) {
|
||
if (!value) return "";
|
||
value = value.toString();
|
||
if (value.length <= size) {
|
||
return value;
|
||
}
|
||
return value.substr(0, size) + "...";
|
||
},
|
||
ucfirst(value: string) {
|
||
return value.charAt(0).toUpperCase() + value.slice(1);
|
||
},
|
||
|
||
// * NAVIGATION
|
||
goPreviousWeek(): void {
|
||
let moment = this.startMoment ?? this.todayMoment;
|
||
moment = this.momentJs(moment.week(), "w");
|
||
this.showMacroScheduleDate("", moment.subtract(1, "week"));
|
||
},
|
||
goNextWeek(): void {
|
||
let moment = this.startMoment ?? this.todayMoment;
|
||
moment = this.momentJs(moment.week(), "w");
|
||
this.showMacroScheduleDate("", moment.add(1, "week"));
|
||
},
|
||
showMacroScheduleDate(dateString: string, moment: Moment | null = null): void {
|
||
const url = new URL(String(window.location));
|
||
moment = moment ?? this.momentJs(dateString);
|
||
(window as Window).location =
|
||
url.origin + url.pathname + "?date=" + moment.format("DD/MM/YYYY");
|
||
},
|
||
sendToast(message: string): void {
|
||
Toastify({
|
||
text: message,
|
||
duration: 3000,
|
||
gravity: "top",
|
||
position: "right",
|
||
offset: {
|
||
x: "2rem",
|
||
y: "3rem",
|
||
},
|
||
style: {
|
||
background: "linear-gradient(to right, #00317d, #0058ca)",
|
||
},
|
||
}).showToast();
|
||
},
|
||
refreshTooltips(): void {
|
||
let exisitingToolips = document.querySelectorAll('.tooltip[role="tooltip"]');
|
||
for (let tooltip of exisitingToolips) {
|
||
tooltip.remove();
|
||
}
|
||
this.initTooltips();
|
||
},
|
||
limitLines(e: KeyboardEvent) {
|
||
const comment = this.$refs.comment as HTMLTextAreaElement;
|
||
let keynum,
|
||
lines = comment.value.split("\n").length;
|
||
if (e.key !== undefined) {
|
||
keynum = e.key;
|
||
} else if (e.keyCode !== undefined) {
|
||
// IE
|
||
if (window.event) {
|
||
keynum = e.keyCode;
|
||
// Netscape/Firefox/Opera
|
||
} else if (e.which) {
|
||
keynum = e.which;
|
||
}
|
||
}
|
||
|
||
if (keynum === undefined) {
|
||
throw new Error("undefined pressed key");
|
||
}
|
||
keynum = Number.parseInt(String(keynum));
|
||
|
||
if (keynum !== 8 && comment.value.length >= this.textLimit) {
|
||
e.preventDefault();
|
||
return false;
|
||
}
|
||
if (keynum === 13 && lines == comment.rows) {
|
||
e.preventDefault();
|
||
return false;
|
||
}
|
||
},
|
||
initTooltips(): void {
|
||
// * INIT Bootstrap tooltips
|
||
let tooltipTriggerList = [].slice.call(
|
||
document.querySelectorAll('.color-picker-component [data-bs-toggle="tooltip"]')
|
||
);
|
||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||
return new window.bootstrap.Tooltip(tooltipTriggerEl, {
|
||
delay: { show: 1000, hide: 300 },
|
||
});
|
||
});
|
||
},
|
||
},
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
@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>
|