ldc-supportweb/resources/ts/bo/components/ScheduleComponent.vue
2023-08-14 09:23:39 +00:00

1067 lines
30 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)"
>
<FontAwesomeIcon icon="text-white fa-solid fa-arrow-left-long" />
</a>
</div>
<div class="col-11">
<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 bg-transparent ' + String(user[key][day.key as any].type.name) + ' ' + day.eday"
:key="day.key"
>
<div
:class="
'cellsizer ' +
(user[key][day.key as any].type.name
? String(user[key][day.key as any].type.name)
: 'empty')
"
>
<a
target="_blank"
class="text-decoration-none"
:href="String(getCellObjectInfo(user[key][day.key as any])?.url)"
>
<button
v-if="user[key][day.key as any]['model_lie']['value']"
class="btn-text position-relative top-0 start-0 btn bg-dark w-fit mw-100 h-auto overflow-hidden text-capitalize text-white opacity-100 p-0 px-1"
>
{{
String(user[key][day.key as any]["id_lie"]["label"])
}}
</button>
</a>
<br>
{{ String(user[key][day.key as any]["text"]) }}
<button
class="btn btn-edit text-white"
type="button"
data-bs-toggle="modal"
data-bs-target="#editModal"
@click.prevent="
setModalData(
index,
key,
day.key,
user[key][day.key as any]['id']
)
"
>
<FontAwesomeIcon icon="fa-solid fa-pen" />
</button>
<button
:class="'btn btn-paste text-white ' + (!clipBoard.set ? 'hide' : '')"
type="button"
@click.prevent="
pasteCell(
index,
key,
day.key,
user[key][day.key as any]['id']
)
"
>
<FontAwesomeIcon icon="fa-solid fa-clipboard" />
</button>
<button
class="btn btn-copy text-white"
type="button"
@click.prevent="
copyCell(
index,
key,
day.key,
user[key][day.key as any]['id']
)
"
>
<FontAwesomeIcon icon="fa-solid fa-copy" />
</button>
<button
:class="
'btn btn-clear text-white ' + (!user[key][day.key as any]['id'] ? 'hide' : '')
"
@click.prevent="
eraseCell(
index,
key,
day.key,
user[key][day.key as any]['id']
)
"
>
<FontAwesomeIcon icon="fa-solid fa-eraser" />
</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)"
>
<FontAwesomeIcon icon="text-white fa-solid fa-arrow-right-long" />
</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"
>
{{ (String(modale.id) === "Nouveau") ? __('Création d\'une tâche') : __('Édition d\'une tâche') }}
</h5>
<button
ref="close"
type="button"
class="btn btn-sm btn-primary bg-primary"
data-bs-dismiss="modal"
aria-label="Close"
>
<FontAwesomeIcon icon="text-white fa-solid fa-xmark" />
</button>
</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.name == activity.name"
: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>
<SearchBelongsToDropdownComponent
:label="__('crud.minicrud.select', {entity: (($refs.morph as HTMLSelectElement).value).slice(0, (($refs.morph as HTMLSelectElement).value).length-1 )})"
:targetmodel="
'App\\Models\\' + formatToModelName(($refs.morph as HTMLSelectElement).value)
"
:routename="
($refs.morph as HTMLSelectElement).value + '.json-paginate'
"
name="scheduleSiteId"
:modelrelated="data"
:modelselected="value"
:readonly="false"
:placeholder="__('crud.minicrud.select', {entity: (($refs.morph as HTMLSelectElement).value).slice(0, (($refs.morph as HTMLSelectElement).value).length-1 )})"
:helper="__('crud.minicrud.select', {entity: (($refs.morph as HTMLSelectElement).value).slice(0, (($refs.morph as HTMLSelectElement).value).length-1 )})"
:required="true"
/>
</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 { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import axios from "axios";
import momentJs from "moment";
import { defineComponent } from "vue";
import route from "../../modules/route";
import toast from "../../modules/toast";
import trans from "../../modules/trans";
import SearchBelongsToDropdownComponent from "./SearchBelongsToDropdownComponent.vue";
type Lie = {
label: string | null;
name: 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, toast, { methods: { momentJs } }],
components: {
FontAwesomeIcon,
SearchBelongsToDropdownComponent,
},
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: {
label: string;
name: string;
table: string;
value: string;
}[];
error: string;
modale: Modale;
dDisabled: boolean;
dReadonly: boolean;
loading: boolean;
value: Lie;
searchText: string;
data: [];
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: "",
name: "",
value: 0,
},
model_lie: {
label: "",
name: "",
value: 0,
},
id_lie: {
label: "",
name: "",
value: null,
},
},
dDisabled: false,
dReadonly: false,
loading: false,
value: {
label: null,
name: null,
value: null,
},
searchText: "",
data: [],
emptyCell: {
type: {
label: "",
name: "",
value: 0,
},
text: "",
id_lie: {
label: "",
name: "",
value: null,
},
model_lie: {
label: "",
name: "",
value: 0,
},
id: null,
},
clipBoard: {
set: false,
type: {
label: "",
name: "",
value: 0,
},
text: "",
id_lie: null,
model_lie: null,
id: null,
user_id: null,
slot: null,
description: "",
date: "",
activity: {
label: "",
name: "",
value: 0,
},
},
};
},
mounted() {
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 = Object.values(data.models);
},
methods: {
goToPlanning(forward: boolean): void {
const route = this.route("schedule.index");
if (!route) {
throw new Error("undefined route schedule.index");
}
const thisWeekMonday = this.momentJs(this.currentDate).startOf("week");
const url = new URL(route);
(window as Window).location =
`${url.origin}${url.pathname}?p=` +
(forward
? thisWeekMonday.add(8, "day")
: thisWeekMonday.subtract(6, "day")
).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: "",
name: "",
value: 0,
},
text: "",
id_lie: {
label: "",
name: "",
value: null,
},
model_lie: {
label: "",
name: "",
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) {
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"],
};
}
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"],
};
}
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"],
};
}
}
return null;
},
resetModalFields() {
this.value = {
label: null,
name: null,
value: null,
};
(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"],
currentModale: Modale = {
id: this.schedules[userid][slotkey][day as any]["id"] ?? this.__("Nouveau"),
userid: userid,
slot: slotkey,
day: day,
title: String(id),
dDisplay: false,
text: text,
activity: activity,
model_lie: model ?? "",
id_lie: this.schedules[userid][slotkey][day as any]["id_lie"],
};
this.modale = currentModale;
this.$nextTick(() => {
this.modale.dDisplay = true;
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");
}
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);
this.notify(
"Cellule collée",
3000,
{
background: "linear-gradient(to right, #00317d, #0058ca)",
}
);
},
clearClipBoard() {
this.clipBoard = {
set: false,
type: {
label: "",
name: "",
value: 0,
},
text: "",
activity: {
label: "",
name: "",
value: 0,
},
id_lie: {
label: "",
name: "",
value: 0,
},
model_lie: {
label: "",
name: "",
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: "",
name: "",
value: 0,
},
};
this.notify(
"Cellule copiée",
3000,
{
background: "linear-gradient(to right, #00317d, #0058ca)",
}
);
},
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),
name: 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),
name: String((this.$refs.type as HTMLSelectElement).value),
value: (this.$refs.morph as HTMLSelectElement).value,
},
id_lie: {
label: null,
name: null,
value: (document.getElementsByName("scheduleSiteId")[0] as HTMLInputElement).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 ?? data.activity,
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);
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,
name: null,
value: null,
};
this.modale.dDisplay = false;
this.$nextTick(() => {
if ((this.$refs.morph as HTMLSelectElement).value) {
this.modale.dDisplay = true;
}
});
},
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;
}
},
formatToModelName(s: string) {
return s.charAt(0).toUpperCase() + s.slice(1, s.length-1);
}
},
});
</script>
<style lang="scss">
@import "vue-select/dist/vue-select.css";
.actions_col {
width: 45px;
padding: 0;
}
.plannings {
.sticky-top {
top: 20rem !important;
}
}
table {
.Mon {
border-left: 4px solid;
}
}
table.schedules_table {
table-layout: fixed;
.resource_ident {
font-weight: bold;
width: 150px;
}
.cellsizer {
height: 50px;
text-overflow: ellipsis;
overflow: hidden;
.btn-text {
margin: 0 0.1rem;
text-overflow: ellipsis;
}
}
}
.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: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%);
}
&: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/dist/vue-select.css";
.planning {
overflow: auto;
td {
height: 50px;
}
#text {
height: 5.5rem;
padding: 1rem 2rem;
resize: none;
width: 37rem;
margin: 0 auto;
}
}
</style>