ldc-supportweb/resources/ts/components/ScheduleComponent.vue
2022-08-30 13:21:07 +00:00

1203 lines
33 KiB
Vue

<template>
<div class="plannings">
<div class="row justify-content-center">
<div class="col-1 text-center actions_col">
<a
class="sticky-top btn btn-primary mt-5"
@click.prevent="goToPlanning(false)"
>←</a>
</div>
<div class="col-10">
<table
class="table table-bordered align-middle text-center schedules_table"
>
<thead>
<tr>
<th rowspan="2" />
<th
:colspan="showWeekends ? 7 : 5"
v-for="item in weeksCells"
class="Mon Fri"
:key="item.num"
>
S {{ item.num }} - {{ item.month }} {{ item.year }}
</th>
</tr>
<tr>
<th
v-for="day in days"
:class="`day${day.eday}`"
:key="day.num"
>
{{ day.day }} {{ day.num }}
</th>
</tr>
</thead>
<tbody>
<template v-for="(user, index) in schedules">
<tr
v-for="(slot, key) in user"
:key="`${String(index)} ${String(key)}`"
>
<td
:rowspan="daySlots"
class="resource_ident"
v-if="key == 1"
>
{{ String(users[index].name) }}
</td>
<td
v-for="day in days"
:class="'dynCell day ' + String(user[key][day.key as any].type.value) + ' ' + day.eday"
:key="day.key"
>
<div
:class="
'cellsizer ' +
(user[key][day.key as any].type.value
? String(user[key][day.key as any].type.value)
: 'empty')
"
>
<b v-if="user[key][day.key as any]['model_lie']['value']">
<a
target="_blank"
class="text-light bg-dark"
:href="String(getCellObjectInfo(user[key][day.key as any])?.url)"
>
{{
String(user[key][day.key as any]["id_lie"]["label"])
}}
</a> </b><br>
{{ String(user[key][day.key as any]["text"]) }}
<button
class="btn btn-edit"
type="button"
data-bs-toggle="modal"
data-bs-target="#editModal"
@click.prevent="
setModalData(
index,
key,
day.key,
user[key][day.key as any]['id']
)
"
>
✎
</button>
<button
:class="'btn btn-paste ' + (!clipBoard.set ? 'hide' : '')"
type="button"
@click.prevent="
pasteCell(
index,
key,
day.key,
user[key][day.key as any]['id']
)
"
>
📋
</button>
<button
class="btn btn-copy"
type="button"
@click.prevent="
copyCell(
index,
key,
day.key,
user[key][day.key as any]['id']
)
"
>
<span
style="
font-size: 0.875em;
margin-right: 0.125em;
position: relative;
top: -0.25em;
left: -0.125em;
"
>
📄
<span
style="position: absolute; top: 0.25em; left: 0.25em"
>
📄
</span>
</span>
</button>
<button
:class="
'btn btn-clear ' + (!user[key][day.key as any]['id'] ? 'hide' : '')
"
@click.prevent="
eraseCell(
index,
key,
day.key,
user[key][day.key as any]['id']
)
"
>
⌫
</button>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="col-1 text-center actions_col">
<a
class="sticky-top btn btn-primary mt-5"
@click="goToPlanning(true)"
>→</a>
</div>
</div>
<!-- Modal -->
<div
class="modal fade"
id="editModal"
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"
>
{{ modale.title }}
</h5>
<button
ref="close"
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
/>
</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"
>
Categorie
</label>
<select
id="categorie"
ref="type"
class="form-select"
aria-label="Sélectionnez une catégorie"
>
<option value="">
Sélectionnez une catégorie
</option>
<option
v-for="(activity, index) of activities"
:key="index"
:value="activity.value"
:selected="modale.activity.value == activity.value"
:class="activity.value"
>
{{ String(activity.label) }}
</option>
</select>
</div>
<div class="form-group mb-3">
<label
for="morph"
class="col-form-label text-md-right ms-5"
>
Type d'élément lié
</label>
<select
id="morph"
ref="morph"
class="form-select"
@change="selectChange"
aria-label="Sélectionnez un élément à lier"
>
<option value="">
Sélectionnez un type d'objet à lier
</option>
<option
v-for="(model, index) of models"
:key="index"
:value="model.value"
:selected="modale.model_lie.value == model.value"
>
{{ String(model.label) }}
</option>
</select>
</div>
<div
class="form-group mb-3"
v-if="modale.dDisplay"
>
<label
for="select_id_lie"
class="col-form-label text-md-right ms-5"
>
Élément lié
</label>
<VueSelect
id="select_id_lie"
ref="select"
:loading="loading"
v-model="value"
:clearable="false"
label="label"
:options="data"
@search="search"
:disabled="dReadonly || dDisabled"
:filterable="true"
:searchable="true"
@input="setSelected"
>
<li
slot="list-footer"
class="pagination"
>
<button
@click.prevent="
$event.preventDefault();
page -= 1;
"
:disabled="!hasPrevPage"
class="btn btn-sm btn-primary mt-2 mb-2 ms-2 me-2"
>
Précédent
</button>
<button
@click.prevent="
$event.preventDefault();
page += 1;
"
:disabled="!hasNextPage"
class="btn btn-sm btn-primary mt-2 mb-2"
>
Suivant
</button>
</li>
</VueSelect>
</div>
<div class="form-group mb-3">
<label
for="text"
class="col-form-label text-md-right ms-5"
>
Description courte
</label>
<textarea
ref="comment"
class="form-control"
name="text"
id="text"
rows="2"
placeholder=""
@keydown="limitLines($event)"
v-model="modale.text"
/>
</div>
</div>
</div>
<div class="row justify-content-center">
<div class="col-8 text-center">
<button
class="btn btn-primary me-3"
@click.prevent="saveCell()"
>
Valider
</button>
<button
class="btn btn-danger"
@click.prevent="($refs.close as HTMLButtonElement).click()"
>
Fermer
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import axios from "axios";
import VueSelect from "vue-select";
import { defineComponent } from "vue";
import * as Toastify from "toastify-js";
import route from "../modules/route";
import trans from "../modules/trans";
import * as momentJs from "moment";
type Lie = {
label: string | null;
value: number | string | null;
};
type Day = {
day: string;
num: string;
eday: string;
key: string;
};
type DaySlot = {
id: number;
type: Lie;
text: string;
id_lie: Lie;
model_lie: Lie;
};
type SchedDay = {
id: number | null;
type: Lie;
text: string;
id_lie: Lie;
model_lie: Lie;
};
type Modale = {
id: number;
userid: number;
slot: number;
day: string;
title: string;
dDisplay: boolean;
text: string;
activity: Lie;
model_lie: Lie;
id_lie: Lie;
};
type ServerData = {
id: number;
user_id: number;
date: string;
activity: Lie;
slot: number;
model_lie: Lie;
id_lie: Lie;
description: string;
};
type ClipBoardType = {
set: boolean;
type: Lie;
text: string;
id_lie: Lie | null;
model_lie: Lie | null;
user_id: number | null;
slot: number | null;
activity: Lie | null;
id: number | null;
description: string;
date: string;
};
export default defineComponent({
mixins: [trans, route, { methods: { momentJs } }],
components: {
VueSelect,
},
mounted() {
window.Toastify = Toastify;
const json = String(this.$attrs.json ?? "{}"),
data = JSON.parse(json);
this.daySlots = !Number.isNaN(Number.parseInt(data.daySlots))
? Number.parseInt(data.daySlots)
: 4;
this.showWeekends = data.daySlots == "true" ? true : false;
this.currentDate = data.browseDate ?? new Date();
this.weeksCells = data.weeks;
this.numweeks = this.weeksCells.length;
this.days = this.prepareDays();
this.users = data.users;
this.schedules = this.prepareSched(data.schedules);
this.activities = data.activities;
this.models = data.models;
},
data(): {
showWeekends: boolean;
daySlots: number;
numweeks: number;
currentDate: null;
weeksCells: {
num: number;
month: number;
year: number;
}[];
days: Day[];
users: LaravelModelList;
schedules: Record<number, Record<number, DaySlot[]>>;
activities: LaravelModelList;
models: LaravelModelList;
error: string;
modale: Modale;
//v-select de ses morts
dDisabled: boolean;
dReadonly: boolean;
loading: boolean;
value: Lie;
searchText: string;
data: [];
page: number;
perPage: number;
hasNextPage: boolean;
hasPrevPage: boolean;
emptyCell: SchedDay;
clipBoard: ClipBoardType;
} {
return {
showWeekends: false,
daySlots: 4,
numweeks: 0,
currentDate: null,
weeksCells: [],
days: [],
users: [],
schedules: [],
activities: [],
models: [],
error: "",
modale: {
id: 0,
userid: 0,
slot: 0,
day: "01/01/1900",
title: "",
dDisplay: false,
text: "",
activity: {
label: "",
value: 0,
},
model_lie: {
label: "",
value: 0,
},
id_lie: {
label: "",
value: 0,
},
},
//v-select de ses morts
dDisabled: false,
dReadonly: false,
loading: false,
value: {
label: "",
value: 0,
},
searchText: "",
data: [],
page: 1,
perPage: 15,
hasNextPage: true,
hasPrevPage: false,
emptyCell: {
type: {
label: "",
value: 0,
},
text: "",
id_lie: {
label: "",
value: 0,
},
model_lie: {
label: "",
value: 0,
},
id: null,
},
clipBoard: {
set: false,
type: {
label: "",
value: 0,
},
text: "",
id_lie: null,
model_lie: null,
id: null,
user_id: null,
slot: null,
description: "",
date: "",
activity: {
label: "",
value: 0,
},
},
};
},
watch: {
page() {
if (this.page < 1) {
this.page = 1;
}
if (this.page > 1) {
this.hasPrevPage = true;
} else {
this.hasPrevPage = false;
}
this.search();
},
data() {
if (this.data.length == this.perPage) {
this.hasNextPage = true;
} else {
this.hasNextPage = false;
}
},
},
methods: {
sendToast(message: string) {
Toastify({
text: message,
duration: 3000,
gravity: "top",
position: "right",
offset: {
x: "2rem",
y: "3rem",
},
style: {
background: "linear-gradient(to right, #00317d, #0058ca)",
},
}).showToast();
},
goToPlanning(forward: boolean): void {
const route = this.route("schedule.index");
if (!route) {
throw new Error("undefined route schedule.index");
}
// console.log(forward);
let date = this.momentJs(this.currentDate)
.startOf("week")
.subtract(7, "day");
if (forward) {
date = this.momentJs(this.currentDate).add(7, "day");
}
const url = new URL(route);
(window as Window).location =
url.origin + url.pathname + "?p=" + date.format("DD/MM/YYYY");
},
prepareDays(): Day[] {
let mydays = [];
for (let i = 0; i < this.numweeks * 7; i++) {
const date = this.momentJs(this.currentDate).add(i, "day"),
obj = {
day: date.format("ddd"),
num: date.format("DD"),
eday: date.locale("en").format("ddd"),
key: date.format("DD/MM/YYYY"),
};
mydays.push(obj);
if (!this.showWeekends) {
if (date.day() == 5) {
i += 2;
}
}
}
return mydays;
},
prepareSched(
dbscheds: Record<number, Record<number, DaySlot[]>>
): Record<number, Record<number, DaySlot[]>> {
const templateObj: SchedDay = {
type: {
label: "",
value: 0,
},
text: "",
id_lie: {
label: "",
value: 0,
},
model_lie: {
label: "",
value: 0,
},
id: null,
};
//we need to make missing users
Object.keys(this.users).forEach((id) => {
if (dbscheds[Number.parseInt(id)] === undefined) {
dbscheds[Number.parseInt(id)] = {};
}
});
Object.keys(dbscheds).forEach((userid) => {
//what if we missing a slot for the whole period ?
//we need to make missing slots too
for (let i = 1; i < this.daySlots + 1; i++) {
if (dbscheds[Number.parseInt(userid)][i] === undefined) {
dbscheds[Number.parseInt(userid)][i] = [];
}
}
Object.keys(dbscheds[Number.parseInt(userid)]).forEach((slotkey) => {
this.days.forEach((day) => {
const schedSlot = dbscheds[Number.parseInt(userid)][
Number.parseInt(slotkey)
] as SchedDay[];
if (schedSlot[day.key as any] === undefined) {
//console.log('need ' + day.key + ' for slot ' + slotkey + ' for user ' + userid);
schedSlot[day.key as any] = templateObj;
}
});
});
});
return dbscheds;
},
getCellObjectInfo(cellData: DaySlot) {
for (let model of this.models) {
if (
cellData["model_lie"].value === model.value &&
model.value == "tickets"
) {
return {
url: this.route("ticket.show", {
TICKET: String(cellData["id_lie"].value),
}),
text: cellData["model_lie"].label,
};
}
if (
cellData["model_lie"].value === model.value &&
model.value == "sites"
) {
return {
url: this.route("sites.show", {
SITE: String(cellData["id_lie"].value),
}),
text: cellData["model_lie"].label,
};
}
if (
cellData["model_lie"].value === model.value &&
model.value == "projects"
) {
return {
url: this.route("projects.show", {
PROJECT: String(cellData["id_lie"].value),
}),
text: cellData["model_lie"].label,
};
}
}
return null;
},
resetModalFields() {
this.value = {
label: "",
value: 0,
};
(this.$refs.type as HTMLSelectElement).value = "";
(this.$refs.morph as HTMLSelectElement).value = "";
},
setModalData(userid: number, slotkey: number, day: string, id: number) {
let text = this.schedules[userid][slotkey][day as any]["text"],
activity = this.schedules[userid][slotkey][day as any]["type"],
model = this.schedules[userid][slotkey][day as any]["model_lie"],
enumobject = {
label: "",
value: "",
},
currentModale: Modale = {
id: this.schedules[userid][slotkey][day as any]["id"],
userid: userid,
slot: slotkey,
day: day,
title: String(id),
dDisplay: false,
text: text,
activity: activity,
model_lie: model ?? enumobject,
id_lie: this.schedules[userid][slotkey][day as any]["id_lie"],
};
this.modale = currentModale;
this.$nextTick(() => {
if (this.modale.id_lie.value === null) {
throw new Error(" modale.id_lie.value is null");
}
let route = null;
switch (this.modale.model_lie.value) {
case "tickets":
this.modale.dDisplay = true;
route = this.route("ticket.show", {
TICKET: this.modale.id_lie.value,
});
break;
case "projects":
this.modale.dDisplay = true;
route = this.route("projects.show", {
PROJECT: this.modale.id_lie.value,
});
break;
case "sites":
this.modale.dDisplay = true;
route = this.route("sites.show", {
SITE: this.modale.id_lie.value,
});
break;
default:
this.modale.dDisplay = false;
}
this.selectChange();
this.value = this.schedules[userid][slotkey][day as any]["id_lie"];
});
},
pasteCell(userid: number, slotkey: number, day: string, id: number) {
if (this.clipBoard.activity === null) {
throw new Error("clipBoard activity is null");
}
if (this.clipBoard.model_lie === null) {
throw new Error("clipBoard model_lie is null");
}
if (this.clipBoard.id_lie === null) {
throw new Error("clipBoard id_lie is null");
}
//this.eraseCell(userid,slotkey,day,id)
const data: ServerData = {
id: id,
user_id: userid,
date: day,
activity: this.clipBoard.activity,
slot: slotkey,
model_lie: this.clipBoard.model_lie,
id_lie: this.clipBoard.id_lie,
description: this.clipBoard.description,
};
this.saveCellOnServer(data);
//do not clear clipboard
//this.clearClipBoard()
this.sendToast("Cellule collée");
},
clearClipBoard() {
this.clipBoard = {
set: false,
type: {
label: "",
value: 0,
},
text: "",
activity: {
label: "",
value: 0,
},
id_lie: {
label: "",
value: 0,
},
model_lie: {
label: "",
value: 0,
},
id: null,
user_id: null,
slot: null,
description: "",
date: "",
};
},
copyCell(userid: number, slotkey: number, day: string, id: number) {
this.clipBoard = {
set: true,
id: id,
user_id: userid,
date: day,
activity: this.schedules[userid][slotkey][day as any].type,
slot: slotkey,
model_lie: this.schedules[userid][slotkey][day as any].model_lie,
id_lie: this.schedules[userid][slotkey][day as any].id_lie,
description: this.schedules[userid][slotkey][day as any].text,
text: "",
type: {
label: "",
value: 0,
},
};
this.sendToast("Cellule copiée");
},
setSelected(value: Lie) {
this.modale.id_lie = value;
},
search(search: string | false | null = null) {
this.$nextTick(() => {
this.error = "";
let route;
switch ((this.$refs.morph as HTMLSelectElement).value) {
case "tickets":
route = this.route("ticket.index");
break;
case "projects":
route = this.route("projects.index");
break;
case "sites":
route = this.route("sites.index");
break;
}
if (route === null || route === undefined) {
throw new Error(
" unhandled select " + (this.$refs.morph as HTMLSelectElement).value
);
}
if (search !== false && search !== null) {
this.searchText = search;
}
route += "?page=" + this.page;
route += "&s=" + this.searchText;
this.loading = true;
axios
.get(route)
.then((response) => {
this.data = response.data;
})
.catch((error: any) => {
if (error.response.status === 429) {
location.reload();
}
this.error = error;
})
.then(() => {
this.loading = false;
});
});
},
saveCell() {
this.error = "";
const data: ServerData = {
id: this.modale.id,
user_id: this.modale.userid,
date: this.modale.day,
activity: {
label: String((this.$refs.type as HTMLSelectElement).value),
value: (this.$refs.type as HTMLSelectElement).value,
},
slot: this.modale.slot,
model_lie: {
label: String((this.$refs.morph as HTMLSelectElement).value),
value: (this.$refs.morph as HTMLSelectElement).value,
},
id_lie: this.value,
description: (this.$refs.comment as HTMLTextAreaElement).value,
};
this.saveCellOnServer(data);
},
saveCellOnServer(data: ServerData) {
this.error = "";
const self = this,
routeUri = self.route("schedule.setCellContent");
if (self.error.length) {
return;
}
if (!routeUri) {
throw new Error("schedule.setCellContent is undefined");
}
axios
.post(routeUri, {
id: data.id,
user_id: data.user_id,
date: data.date,
activity: data.activity.value,
slot: data.slot,
model_lie: data.model_lie.value,
id_lie: data.id_lie.value,
description: data.description,
})
.then((response) => {
if (response.data.status) {
const sched = Object.assign({}, response.data.sched);
// sched.date = Date.parse(sched.date)
// sched.date = this.moment(sched.date).format('DD/MM/YYYY');
let cellData = {
type: sched.activity,
text: sched.description,
id_lie: sched.id_lie,
model_lie: sched.model_lie,
id: sched.id,
};
this.schedules[sched.user_id][sched.slot][sched.date] = cellData;
(this.$refs.close as HTMLButtonElement).click();
} else {
this.error = "Réponse inconnue";
}
})
.catch((error: any) => {
console.log(error);
if (error.response.status === 429) {
location.reload();
}
if (error.response.status === 422) {
if (error.response.data.errors) {
this.error = "";
for (let id in error.response.data.errors) {
let field = error.response.data.errors[id];
this.error += field.join("\n") + "\n";
}
return;
} else if (error.response.data.message) {
this.error = error.response.data.message;
return;
}
}
this.error = error;
});
},
eraseCell(userid: number, slotkey: number, day: string, id: number) {
const route = this.route("schedule.eraseCellContent"),
data = {
id: id,
};
if (!route) {
throw new Error("undefined route schedule.eraseCellContent");
}
axios
.post(route, data)
.then((response) => {
if (response.data.status) {
this.schedules[userid][slotkey][day as any] = this
.emptyCell as DaySlot;
} else {
console.error("Unknown response");
}
})
.catch((error: any) => {
if (error.response.status === 429) {
location.reload();
}
console.error(error);
});
},
selectChange() {
this.value = {
label: null,
value: null,
};
switch ((this.$refs.morph as HTMLSelectElement).value) {
case "tickets":
case "projects":
case "sites":
this.modale.dDisplay = true;
this.search(false);
break;
default:
this.modale.dDisplay = false;
}
},
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;
}
},
},
});
</script>
<style lang="scss">
@import "vue-select/src/scss/vue-select.scss";
.actions_col {
width: 45px;
padding: 0;
}
.plannings {
.sticky-top {
top: 20rem !important;
}
}
table {
.Mon {
border-left: 4px solid;
}
// .Fri {
// border-right: 4px solid;
// }
}
table.schedules_table {
table-layout: fixed;
.resource_ident {
font-weight: bold;
width: 150px;
}
.cellsizer {
height: 50px;
text-overflow: ellipsis;
overflow: hidden;
b {
width: 90%;
}
}
}
.dynCell {
position: relative;
&.day {
//position: relative;
&:hover .btn {
opacity: 1;
}
&:hover .cellsizer {
height: 140px;
padding-top: 2rem;
position: absolute;
width: 200%;
left: -50%;
top: -55%;
z-index: 20;
border: 1px solid;
}
$buttonBgColor: #4d4d4d;
// 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 {
position: absolute;
top: 0.1rem;
left: 0.1rem;
padding: 0;
width: 1.8rem;
height: 1.8rem;
margin: 0;
background-color: $buttonBgColor;
opacity: 0;
&.hide {
opacity: 0 !important;
}
&:hover,
&:focus {
background-color: darken($buttonBgColor, 10%);
}
&.btn-clear {
color: darken($color: #fff, $amount: 5);
}
&:nth-last-child(2) {
top: 0.1rem;
right: inherit;
left: 2.1rem;
}
&:nth-last-child(3) {
right: inherit;
top: 0.1rem;
left: 4.1rem;
}
&:nth-last-child(1) {
top: 0.1rem;
left: calc(100% - 1.9rem);
}
}
}
}
@import "toastify-js/src/toastify.css";
@import "vue-select/src/scss/vue-select.scss";
.planning {
overflow: auto;
td {
height: 50px;
}
#text {
height: 5.5rem;
padding: 1rem 2rem;
resize: none;
width: 37rem;
margin: 0 auto;
}
}
</style>