owltide/pages/edit.vue
Hornwitser 1d2edf7535 Add dialog showing diff of changes to save
Add a save dialog at the bottom of the screen that is present whenever
there are unsaved changes.  This dialog provides a diff between the
client and server state so that the user can easily confirm the changes
they are about to make are the correct changes before applying them to
the server.
2025-06-30 15:43:15 +02:00

224 lines
5.1 KiB
Vue

<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"],
});
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>