The name is inspired by the watchful owl perching from the tree tops with complete overview of all that's going on combined with -tide in the sense it's used for in words like summertide and eastertide.
231 lines
5.3 KiB
Vue
231 lines
5.3 KiB
Vue
<!--
|
|
SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no>
|
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
-->
|
|
<template>
|
|
<main>
|
|
<h1>Edit</h1>
|
|
<label>
|
|
Crew Filter:
|
|
<select
|
|
v-model="crewFilter"
|
|
>
|
|
<option
|
|
:value="undefined"
|
|
:selected="crewFilter === undefined"
|
|
><All Crew></option>
|
|
<option
|
|
v-for="user in [...usersStore.users.values()].filter(a => a.type === 'crew' || a.type === 'admin')"
|
|
:key="user.id"
|
|
:value="String(user.id)"
|
|
:selected="crewFilter === String(user.id)"
|
|
>{{ user.name }}</option>
|
|
</select>
|
|
</label>
|
|
<Timetable :schedule :eventSlotFilter :shiftSlotFilter />
|
|
<Tabs
|
|
:tabs
|
|
default="locations"
|
|
>
|
|
<template #locations>
|
|
<TableScheduleLocations :edit="accountStore.canEditPublic" />
|
|
</template>
|
|
<template #events>
|
|
<TableScheduleEvents :edit="true" />
|
|
</template>
|
|
<template #eventSlots>
|
|
<label>
|
|
Location Filter:
|
|
<select
|
|
v-model="locationFilter"
|
|
>
|
|
<option
|
|
:value="undefined"
|
|
:selected="locationFilter === undefined"
|
|
><All locations></option>
|
|
<option
|
|
v-for="location in schedule.locations.values()"
|
|
:key="location.id"
|
|
:value="location.id"
|
|
:disabled="location.deleted"
|
|
:selected="locationFilter === location.id"
|
|
>{{ location.name }}</option>
|
|
</select>
|
|
</label>
|
|
<TableScheduleEventSlots :edit="true" :locationId="locationFilter" :eventSlotFilter />
|
|
</template>
|
|
<template #roles>
|
|
<TableScheduleRoles :edit="true" />
|
|
</template>
|
|
<template #shifts>
|
|
<TableScheduleShifts :edit="true" :roleId="roleFilter" />
|
|
</template>
|
|
<template #shiftSlots>
|
|
<label>
|
|
Role Filter:
|
|
<select
|
|
v-model="roleFilter"
|
|
>
|
|
<option
|
|
:value="undefined"
|
|
:selected="roleFilter === undefined"
|
|
><All roles></option>
|
|
<option
|
|
v-for="role in schedule.roles.values()"
|
|
:key="role.id"
|
|
:value="role.id"
|
|
:disabled="role.deleted"
|
|
:selected="roleFilter === role.id"
|
|
>{{ role.name }}</option>
|
|
</select>
|
|
</label>
|
|
<TableScheduleShiftSlots :edit="true" :roleId="roleFilter" :shiftSlotFilter />
|
|
</template>
|
|
</Tabs>
|
|
<section
|
|
class="saveDialog"
|
|
:class="{ visible: schedule.isModified() }"
|
|
>
|
|
<DiffSchedule
|
|
v-if="reviewOpen"
|
|
:schedule
|
|
/>
|
|
<hr v-if="reviewOpen">
|
|
<section class="saveLine">
|
|
<span>Changes are not saved yet.</span>
|
|
<button
|
|
type="button"
|
|
v-if="reviewOpen"
|
|
@click="saveChanges"
|
|
>Save changes</button>
|
|
<button
|
|
type="button"
|
|
class="review"
|
|
@click="reviewOpen = !reviewOpen"
|
|
>
|
|
{{ reviewOpen ? "Close" : "Review" }}
|
|
</button>
|
|
</section>
|
|
</section>
|
|
</main>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
definePageMeta({
|
|
middleware: ["authenticated"],
|
|
allowedAccountTypes: ["crew", "admin"],
|
|
});
|
|
useHead({
|
|
title: "Edit",
|
|
});
|
|
|
|
const tabs = [
|
|
{ id: "locations", title: "Locations" },
|
|
{ id: "events", title: "Events" },
|
|
{ id: "eventSlots", title: "Event Slots" },
|
|
{ id: "roles", title: "Roles" },
|
|
{ id: "shifts", title: "Shifts" },
|
|
{ id: "shiftSlots", title: "Shifts Slots" },
|
|
];
|
|
|
|
const schedule = await useSchedule();
|
|
const usersStore = useUsersStore();
|
|
await usersStore.fetch();
|
|
const accountStore = useAccountStore();
|
|
|
|
const route = useRoute();
|
|
const crewFilter = computed({
|
|
get: () => queryToString(route.query.crew),
|
|
set: (value: string | undefined) => navigateTo({
|
|
path: route.path,
|
|
query: {
|
|
...route.query,
|
|
crew: value,
|
|
},
|
|
}),
|
|
});
|
|
const eventSlotFilter = computed(() => {
|
|
if (crewFilter.value === undefined || !accountStore.valid) {
|
|
return () => true;
|
|
}
|
|
const cid = parseInt(crewFilter.value);
|
|
return (slot: ClientScheduleEventSlot) => slot.assigned.has(cid);
|
|
});
|
|
const shiftSlotFilter = computed(() => {
|
|
if (crewFilter.value === undefined || !accountStore.valid) {
|
|
return () => true;
|
|
}
|
|
const cid = parseInt(crewFilter.value);
|
|
return (slot: ClientScheduleShiftSlot) => slot.assigned.has(cid);
|
|
});
|
|
|
|
const locationFilter = computed({
|
|
get: () => queryToNumber(route.query.location),
|
|
set: (value: number | undefined) => navigateTo({
|
|
path: route.path,
|
|
query: {
|
|
...route.query,
|
|
location: value !== undefined ? String(value) : undefined,
|
|
},
|
|
}),
|
|
});
|
|
|
|
const roleFilter = computed({
|
|
get: () => queryToNumber(route.query.role),
|
|
set: (value: string | undefined) => navigateTo({
|
|
path: route.path,
|
|
query: {
|
|
...route.query,
|
|
role: value,
|
|
},
|
|
}),
|
|
});
|
|
|
|
const reviewOpen = ref(false);
|
|
|
|
async function saveChanges() {
|
|
try {
|
|
await $fetch("/api/schedule", {
|
|
method: "PATCH",
|
|
body: schedule.value.toApi(true),
|
|
});
|
|
} catch (err: any) {
|
|
console.error(err);
|
|
alert(err?.data?.message ?? err.message);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.saveDialog {
|
|
visibility: hidden;
|
|
max-height: calc(100dvh - 2rem);
|
|
overflow-y: auto;
|
|
max-width: 30rem;
|
|
padding: 0.2rem 0.4rem;
|
|
border: 1px solid CanvasText;
|
|
background-color: color-mix(in srgb, Canvas, CanvasText 10%);
|
|
margin-block-start: 1rem;
|
|
margin-block-end: 0.5rem;
|
|
margin-inline: auto;
|
|
}
|
|
.saveDialog.visible {
|
|
visibility: visible;
|
|
position: sticky;
|
|
bottom: 0.5rem;
|
|
}
|
|
.saveDialog hr {
|
|
margin-block-start: 0.4rem;
|
|
margin-block-end: 0.2rem;
|
|
}
|
|
.saveLine {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
.saveLine span {
|
|
margin-inline-end: auto;
|
|
}
|
|
button.review {
|
|
width: 4.5rem;
|
|
}
|
|
</style>
|