mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
495 lines
16 KiB
Vue
495 lines
16 KiB
Vue
<template>
|
|
<div class="row">
|
|
<div class="col-sm">
|
|
<label class="form-label">{{ $t("created_availabilities") }}</label>
|
|
<vue-multiselect
|
|
v-model="pickedLocation"
|
|
:options="locations"
|
|
:label="'name'"
|
|
:track-by="'id'"
|
|
:selectLabel="'Presser \'Entrée\' pour choisir'"
|
|
:selectedLabel="'Choisir'"
|
|
:deselectLabel="'Presser \'Entrée\' pour enlever'"
|
|
:placeholder="'Choisir'"
|
|
></vue-multiselect>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="display-options row justify-content-between"
|
|
style="margin-top: 1rem"
|
|
>
|
|
<div class="col-sm-9 col-xs-12">
|
|
<div class="input-group mb-3">
|
|
<label class="input-group-text" for="slotDuration"
|
|
>Durée des créneaux</label
|
|
>
|
|
<select
|
|
v-model="slotDuration"
|
|
id="slotDuration"
|
|
class="form-select"
|
|
>
|
|
<option value="00:05:00">5 minutes</option>
|
|
<option value="00:10:00">10 minutes</option>
|
|
<option value="00:15:00">15 minutes</option>
|
|
<option value="00:30:00">30 minutes</option>
|
|
</select>
|
|
<label class="input-group-text" for="slotMinTime">De</label>
|
|
<select
|
|
v-model="slotMinTime"
|
|
id="slotMinTime"
|
|
class="form-select"
|
|
>
|
|
<option value="00:00:00">0h</option>
|
|
<option value="01:00:00">1h</option>
|
|
<option value="02:00:00">2h</option>
|
|
<option value="03:00:00">3h</option>
|
|
<option value="04:00:00">4h</option>
|
|
<option value="05:00:00">5h</option>
|
|
<option value="06:00:00">6h</option>
|
|
<option value="07:00:00">7h</option>
|
|
<option value="08:00:00">8h</option>
|
|
<option value="09:00:00">9h</option>
|
|
<option value="10:00:00">10h</option>
|
|
<option value="11:00:00">11h</option>
|
|
<option value="12:00:00">12h</option>
|
|
</select>
|
|
<label class="input-group-text" for="slotMaxTime">À</label>
|
|
<select
|
|
v-model="slotMaxTime"
|
|
id="slotMaxTime"
|
|
class="form-select"
|
|
>
|
|
<option value="12:00:00">12h</option>
|
|
<option value="13:00:00">13h</option>
|
|
<option value="14:00:00">14h</option>
|
|
<option value="15:00:00">15h</option>
|
|
<option value="16:00:00">16h</option>
|
|
<option value="17:00:00">17h</option>
|
|
<option value="18:00:00">18h</option>
|
|
<option value="19:00:00">19h</option>
|
|
<option value="20:00:00">20h</option>
|
|
<option value="21:00:00">21h</option>
|
|
<option value="22:00:00">22h</option>
|
|
<option value="23:00:00">23h</option>
|
|
<option value="23:59:59">24h</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="col-xs-12 col-sm-3">
|
|
<div class="float-end">
|
|
<div class="form-check input-group">
|
|
<span class="input-group-text">
|
|
<input
|
|
id="showHideWE"
|
|
class="mt-0"
|
|
type="checkbox"
|
|
v-model="showWeekends"
|
|
/>
|
|
</span>
|
|
<label
|
|
for="showHideWE"
|
|
class="form-check-label input-group-text"
|
|
>Week-ends</label
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<FullCalendar :options="calendarOptions" ref="calendarRef">
|
|
<template v-slot:eventContent="arg: EventApi">
|
|
<span :class="eventClasses(arg.event)">
|
|
<b v-if="arg.event.extendedProps.is === 'remote'">{{
|
|
arg.event.title
|
|
}}</b>
|
|
<b v-else-if="arg.event.extendedProps.is === 'range'"
|
|
>{{ arg.timeText }} -
|
|
{{ arg.event.extendedProps.locationName }}</b
|
|
>
|
|
<b v-else-if="arg.event.extendedProps.is === 'local'">{{
|
|
arg.event.title
|
|
}}</b>
|
|
<b v-else>no 'is'</b>
|
|
<a
|
|
v-if="arg.event.extendedProps.is === 'range'"
|
|
class="fa fa-fw fa-times delete"
|
|
@click.prevent="onClickDelete(arg.event)"
|
|
>
|
|
</a>
|
|
</span>
|
|
</template>
|
|
</FullCalendar>
|
|
|
|
<div id="copy-widget">
|
|
<div class="container mt-2 mb-2">
|
|
<div class="row justify-content-between align-items-center mb-4">
|
|
<div class="col-xs-12 col-sm-3 col-md-2">
|
|
<h6 class="chill-red">{{ $t("copy_range_from_to") }}</h6>
|
|
</div>
|
|
<div class="col-xs-12 col-sm-9 col-md-2">
|
|
<select
|
|
v-model="dayOrWeek"
|
|
id="dayOrWeek"
|
|
class="form-select"
|
|
>
|
|
<option value="day">{{ $t("from_day_to_day") }}</option>
|
|
<option value="week">
|
|
{{ $t("from_week_to_week") }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<template v-if="dayOrWeek === 'day'">
|
|
<div class="col-xs-12 col-sm-3 col-md-3">
|
|
<input
|
|
class="form-control"
|
|
type="date"
|
|
v-model="copyFrom"
|
|
/>
|
|
</div>
|
|
<div class="col-xs-12 col-sm-1 col-md-1 copy-chevron">
|
|
<i class="fa fa-angle-double-right"></i>
|
|
</div>
|
|
<div class="col-xs-12 col-sm-3 col-md-3">
|
|
<input
|
|
class="form-control"
|
|
type="date"
|
|
v-model="copyTo"
|
|
/>
|
|
</div>
|
|
<div class="col-xs-12 col-sm-5 col-md-1">
|
|
<button
|
|
class="btn btn-action float-end"
|
|
@click="copyDay"
|
|
>
|
|
{{ $t("copy_range") }}
|
|
</button>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<div class="col-xs-12 col-sm-3 col-md-3">
|
|
<select
|
|
v-model="copyFromWeek"
|
|
id="copyFromWeek"
|
|
class="form-select"
|
|
>
|
|
<option
|
|
v-for="w in lastWeeks"
|
|
:value="w.value"
|
|
:key="w.value"
|
|
>
|
|
{{ w.text }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-xs-12 col-sm-1 col-md-1 copy-chevron">
|
|
<i class="fa fa-angle-double-right"></i>
|
|
</div>
|
|
<div class="col-xs-12 col-sm-3 col-md-3">
|
|
<select
|
|
v-model="copyToWeek"
|
|
id="copyToWeek"
|
|
class="form-select"
|
|
>
|
|
<option
|
|
v-for="w in nextWeeks"
|
|
:value="w.value"
|
|
:key="w.value"
|
|
>
|
|
{{ w.text }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-xs-12 col-sm-5 col-md-1">
|
|
<button
|
|
class="btn btn-action float-end"
|
|
@click="copyWeek"
|
|
>
|
|
{{ $t("copy_range") }}
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- not directly seen, but include in a modal -->
|
|
<edit-location ref="editLocation"></edit-location>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type {
|
|
CalendarOptions,
|
|
DatesSetArg,
|
|
EventInput,
|
|
} from "@fullcalendar/core";
|
|
import { reactive, computed, ref, onMounted } from "vue";
|
|
import { useStore } from "vuex";
|
|
import { key } from "./store";
|
|
import FullCalendar from "@fullcalendar/vue3";
|
|
import frLocale from "@fullcalendar/core/locales/fr";
|
|
import interactionPlugin, {
|
|
DropArg,
|
|
EventResizeDoneArg,
|
|
} from "@fullcalendar/interaction";
|
|
import timeGridPlugin from "@fullcalendar/timegrid";
|
|
import {
|
|
EventApi,
|
|
DateSelectArg,
|
|
EventDropArg,
|
|
EventClickArg,
|
|
} from "@fullcalendar/core";
|
|
import {
|
|
dateToISO,
|
|
ISOToDate,
|
|
} from "../../../../../ChillMainBundle/Resources/public/chill/js/date";
|
|
import VueMultiselect from "vue-multiselect";
|
|
import { Location } from "../../../../../ChillMainBundle/Resources/public/types";
|
|
import EditLocation from "./Components/EditLocation.vue";
|
|
import { useI18n } from "vue-i18n";
|
|
|
|
const store = useStore(key);
|
|
|
|
const { t } = useI18n();
|
|
|
|
const showWeekends = ref(false);
|
|
const slotDuration = ref("00:15:00");
|
|
const slotMinTime = ref("09:00:00");
|
|
const slotMaxTime = ref("18:00:00");
|
|
const copyFrom = ref<string | null>(null);
|
|
const copyTo = ref<string | null>(null);
|
|
const editLocation = ref<InstanceType<typeof EditLocation> | null>(null);
|
|
const dayOrWeek = ref("day");
|
|
const copyFromWeek = ref<string | null>(null);
|
|
const copyToWeek = ref<string | null>(null);
|
|
|
|
interface Weeks {
|
|
value: string | null;
|
|
text: string;
|
|
}
|
|
|
|
const getMonday = (week: number): Date => {
|
|
const lastMonday = new Date();
|
|
lastMonday.setDate(
|
|
lastMonday.getDate() - ((lastMonday.getDay() + 6) % 7) + week * 7,
|
|
);
|
|
return lastMonday;
|
|
};
|
|
|
|
const dateOptions: Intl.DateTimeFormatOptions = {
|
|
weekday: "long",
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
};
|
|
|
|
const lastWeeks = computed((): Weeks[] =>
|
|
Array.from(Array(30).keys()).map((w) => {
|
|
const lastMonday = getMonday(15 - w);
|
|
return {
|
|
value: dateToISO(lastMonday),
|
|
text: `Semaine du ${lastMonday.toLocaleDateString("fr-FR", dateOptions)}`,
|
|
};
|
|
}),
|
|
);
|
|
|
|
const nextWeeks = computed((): Weeks[] =>
|
|
Array.from(Array(52).keys()).map((w) => {
|
|
const nextMonday = getMonday(w + 1);
|
|
return {
|
|
value: dateToISO(nextMonday),
|
|
text: `Semaine du ${nextMonday.toLocaleDateString("fr-FR", dateOptions)}`,
|
|
};
|
|
}),
|
|
);
|
|
|
|
const baseOptions = ref<CalendarOptions>({
|
|
locale: frLocale,
|
|
plugins: [interactionPlugin, timeGridPlugin],
|
|
initialView: "timeGridWeek",
|
|
initialDate: new Date(),
|
|
scrollTimeReset: false,
|
|
selectable: true,
|
|
// when the dates are changes in the fullcalendar view OR when new events are added
|
|
datesSet: onDatesSet,
|
|
// when a date is selected
|
|
select: onDateSelect,
|
|
// when a event is resized
|
|
eventResize: onEventDropOrResize,
|
|
// when an event is moved
|
|
eventDrop: onEventDropOrResize,
|
|
// when an event si clicked
|
|
eventClick: onEventClick,
|
|
selectMirror: false,
|
|
editable: true,
|
|
headerToolbar: {
|
|
left: "prev,next today",
|
|
center: "title",
|
|
right: "timeGridWeek,timeGridDay",
|
|
},
|
|
});
|
|
|
|
const ranges = computed<EventInput[]>(() => {
|
|
return store.state.calendarRanges.ranges;
|
|
});
|
|
|
|
const locations = computed<Location[]>(() => {
|
|
return store.state.locations.locations;
|
|
});
|
|
|
|
const pickedLocation = computed<Location | null>({
|
|
get(): Location | null {
|
|
return (
|
|
store.state.locations.locationPicked ||
|
|
store.state.locations.currentLocation
|
|
);
|
|
},
|
|
set(newLocation: Location | null): void {
|
|
store.commit("locations/setLocationPicked", newLocation, {
|
|
root: true,
|
|
});
|
|
},
|
|
});
|
|
|
|
/**
|
|
* return the show classes for the event
|
|
* @param arg
|
|
*/
|
|
const eventClasses = function (arg: EventApi): object {
|
|
return { calendarRangeItems: true };
|
|
};
|
|
|
|
/*
|
|
// currently, all events are stored into calendarRanges, due to reactivity bug
|
|
const remotes = computed<EventInput[]>(() => {
|
|
return store.state.calendarRemotes.remotes;
|
|
});
|
|
|
|
const sources = computed<EventSourceInput[]>(() => {
|
|
const sources = [];
|
|
|
|
const rangeSource: EventSourceInput = {
|
|
id: 'ranges',
|
|
events: ranges.value,
|
|
};
|
|
|
|
sources.push(rangeSource);
|
|
|
|
return sources;
|
|
});
|
|
*/
|
|
|
|
const calendarOptions = computed((): CalendarOptions => {
|
|
return {
|
|
...baseOptions.value,
|
|
weekends: showWeekends.value,
|
|
slotDuration: slotDuration.value,
|
|
events: ranges.value,
|
|
slotMinTime: slotMinTime.value,
|
|
slotMaxTime: slotMaxTime.value,
|
|
};
|
|
});
|
|
|
|
/**
|
|
* launched when the calendar range date change
|
|
*/
|
|
function onDatesSet(event: DatesSetArg): void {
|
|
store.dispatch("fullCalendar/setCurrentDatesView", {
|
|
start: event.start,
|
|
end: event.end,
|
|
});
|
|
}
|
|
|
|
function onDateSelect(event: DateSelectArg): void {
|
|
if (null === pickedLocation.value) {
|
|
window.alert(
|
|
"Indiquez une localisation avant de créer une période de disponibilité.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
store.dispatch("calendarRanges/createRange", {
|
|
start: event.start,
|
|
end: event.end,
|
|
location: pickedLocation.value,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* When a calendar range is deleted
|
|
*/
|
|
function onClickDelete(event: EventApi): void {
|
|
if (event.extendedProps.is !== "range") {
|
|
return;
|
|
}
|
|
|
|
store.dispatch(
|
|
"calendarRanges/deleteRange",
|
|
event.extendedProps.calendarRangeId,
|
|
);
|
|
}
|
|
|
|
function onEventDropOrResize(payload: EventDropArg | EventResizeDoneArg) {
|
|
if (payload.event.extendedProps.is !== "range") {
|
|
return;
|
|
}
|
|
const changedEvent = payload.event;
|
|
|
|
store.dispatch("calendarRanges/patchRangeTime", {
|
|
calendarRangeId: payload.event.extendedProps.calendarRangeId,
|
|
start: payload.event.start,
|
|
end: payload.event.end,
|
|
});
|
|
}
|
|
|
|
function onEventClick(payload: EventClickArg): void {
|
|
// @ts-ignore TS does not recognize the target. But it does exists.
|
|
if (payload.jsEvent.target.classList.contains("delete")) {
|
|
return;
|
|
}
|
|
if (payload.event.extendedProps.is !== "range") {
|
|
return;
|
|
}
|
|
|
|
editLocation.value?.startEdit(payload.event);
|
|
}
|
|
|
|
function copyDay() {
|
|
if (null === copyFrom.value || null === copyTo.value) {
|
|
return;
|
|
}
|
|
store.dispatch("calendarRanges/copyFromDayToAnotherDay", {
|
|
from: ISOToDate(copyFrom.value),
|
|
to: ISOToDate(copyTo.value),
|
|
});
|
|
}
|
|
|
|
function copyWeek() {
|
|
if (null === copyFromWeek.value || null === copyToWeek.value) {
|
|
return;
|
|
}
|
|
store.dispatch("calendarRanges/copyFromWeekToAnotherWeek", {
|
|
fromMonday: ISOToDate(copyFromWeek.value),
|
|
toMonday: ISOToDate(copyToWeek.value),
|
|
});
|
|
}
|
|
|
|
onMounted(() => {
|
|
copyFromWeek.value = dateToISO(getMonday(0));
|
|
copyToWeek.value = dateToISO(getMonday(1));
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
#copy-widget {
|
|
position: sticky;
|
|
bottom: 0px;
|
|
background-color: white;
|
|
z-index: 9999999999;
|
|
padding: 0.25rem 0 0.25rem;
|
|
}
|
|
div.copy-chevron {
|
|
text-align: center;
|
|
font-size: x-large;
|
|
width: 2rem;
|
|
}
|
|
</style>
|