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 }: {arg: { event: 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.event.startStr }} -
{{ 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>