owltide/pages/edit.vue
Hornwitser 9592cd3160 Name the application Owltide
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.
2025-07-01 18:41:24 +02:00

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"
>&lt;All Crew&gt;</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"
>&lt;All locations&gt;</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"
>&lt;All roles&gt;</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>