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.
224 lines
5.1 KiB
Vue
224 lines
5.1 KiB
Vue
<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"],
|
|
});
|
|
|
|
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>
|