owltide/components/ShiftsTable.vue

283 lines
6.8 KiB
Vue
Raw Normal View History

2025-03-15 17:06:23 +01:00
<template>
<div v-if="schedule.deleted">
Error: Unexpected deleted schedule.
</div>
<div v-else>
2025-03-15 17:06:23 +01:00
<table>
<thead>
<tr>
<th>id</th>
<th>name</th>
<th>role</th>
<th>s</th>
<th>description</th>
<th v-if="edit"></th>
</tr>
</thead>
<tbody>
<template v-if="edit">
<tr
v-for="shift in shifts?.filter(s => !s.deleted)"
2025-03-15 17:06:23 +01:00
:key="shift.id"
:class="{ removed: removed.has(shift.id) }"
>
<td>{{ shift.id }}</td>
<td>
<input
type="text"
:value="shift.name"
@input="editShift(shift, { name: ($event as any).target.value })"
>
</td>
<td>
<select
:value="shift.roleId"
@change="editShift(shift, { roleId: ($event as any).target.value })"
2025-03-15 17:06:23 +01:00
>
<option
v-for="role in schedule.roles?.filter(r => !r.deleted)"
2025-03-15 17:06:23 +01:00
:key="role.id"
:value="role.id"
:selected="shift.roleId === role.id"
2025-03-15 17:06:23 +01:00
>{{ role.name }}</option>
</select>
</td>
<td>{{ shift.slots.length ? shift.slots.length : "" }}</td>
<td>
<input
type="text"
:value="shift.description"
@input="editShift(shift, { description: ($event as any).target.value })"
>
</td>
<td>
<button
type="button"
:disabled="removed.has(shift.id)"
@click="delShift(shift.id)"
>Delete</button>
<button
v-if="changes.some(c => c.id === shift.id)"
2025-03-15 17:06:23 +01:00
type="button"
@click="revertShift(shift.id)"
>Revert</button>
</td>
</tr>
<tr>
<td>{{ newShiftId }}</td>
2025-03-15 17:06:23 +01:00
<td>
<input
type="text"
v-model="newShiftName"
>
</td>
<td>
<select v-model="newShiftRole">
<option
v-for="role in schedule.roles?.filter(r => !r.deleted)"
2025-03-15 17:06:23 +01:00
:key="role.id"
:value="role.id"
:selected="role.id === newShiftRole"
>{{ role.name }}</option>
</select>
</td>
<td></td>
<td>
<input
type="text"
v-model="newShiftDescription"
>
</td>
<td>
<button
v-if="shiftExists(newShiftName)"
disabled
>Shift already exists</button>
<button
v-else
type="button"
@click="newShift"
>Add Shift</button>
</td>
</tr>
</template>
<template v-else>
<tr
v-for="shift in shifts?.filter(s => !s.deleted)"
2025-03-15 17:06:23 +01:00
:key="shift.id"
>
<td>{{ shift.id }}</td>
<td>{{ shift.name }}</td>
<td>{{ shift.roleId }}</td>
2025-03-15 17:06:23 +01:00
<td>{{ shift.slots.length ? shift.slots.length : "" }}</td>
<td>{{ shift.description }}</td>
</tr>
</template>
</tbody>
</table>
<p v-if="changes.length">
Changes are not saved yet.
<button
type="button"
@click="saveShifts"
>Save Changes</button>
</p>
<details>
<summary>Debug</summary>
<ol>
<li v-for="change in changes">
<pre><code>{{ JSON.stringify(change, undefined, 2) }}</code></pre>
</li>
</ol>
</details>
</div>
</template>
<script lang="ts" setup>
import type { ApiSchedule, ApiScheduleShift } from '~/shared/types/api';
2025-03-15 17:06:23 +01:00
import { toId } from '~/shared/utils/functions';
import { applyUpdatesToArray } from '~/shared/utils/update';
2025-03-15 17:06:23 +01:00
const props = defineProps<{
edit?: boolean,
roleId?: number,
2025-03-15 17:06:23 +01:00
}>();
const schedule = await useSchedule();
const changes = ref<ApiScheduleShift[]>([]);
const removed = computed(() => new Set(changes.value.filter(c => c.deleted).map(c => c.id)));
2025-03-15 17:06:23 +01:00
function replaceChange(
change: ApiScheduleShift,
changes: ApiScheduleShift[],
2025-03-15 17:06:23 +01:00
) {
const index = changes.findIndex(item => (
item.deleted === change.deleted && item.id === change.id
2025-03-15 17:06:23 +01:00
));
const copy = [...changes];
if (index !== -1)
copy.splice(index, 1, change);
else
copy.push(change);
return copy;
}
function revertChange(id: number, changes: ApiScheduleShift[]) {
return changes.filter(change => change.id !== id);
2025-03-15 17:06:23 +01:00
}
const newShiftName = ref("");
const newShiftId = computed(() => {
if (schedule.value.deleted) {
throw new Error("Unexpected deleted schedule");
}
return Math.max(
1,
...schedule.value.shifts?.map(r => r.id) ?? [],
...changes.value.map(c => c.id)
) + 1;
});
const newShiftRole = ref(props.roleId);
watch(() => props.roleId, () => {
newShiftRole.value = props.roleId;
2025-03-15 17:06:23 +01:00
});
const newShiftDescription = ref("");
function editShift(
shift: Extract<ApiScheduleShift, { deleted?: false }>,
edits: { name?: string, description?: string, roleId?: number }
2025-03-15 17:06:23 +01:00
) {
const copy = { ...shift };
if (edits.name !== undefined) {
copy.name = edits.name;
}
if (edits.description !== undefined) {
copy.description = edits.description || undefined;
}
if (edits.roleId !== undefined) {
copy.roleId = edits.roleId;
2025-03-15 17:06:23 +01:00
}
changes.value = replaceChange(copy, changes.value);
2025-03-15 17:06:23 +01:00
}
function delShift(id: number) {
const change = { id, updatedAt: "", deleted: true as const };
2025-03-15 17:06:23 +01:00
changes.value = replaceChange(change, changes.value);
}
function revertShift(id: number) {
2025-03-15 17:06:23 +01:00
changes.value = revertChange(id, changes.value);
}
function shiftExists(name: string) {
if (schedule.value.deleted) {
throw new Error("Unexpected deleted schedule");
}
name = toId(name);
2025-03-15 17:06:23 +01:00
return (
schedule.value.shifts?.some(s => !s.deleted && toId(s.name) === name)
|| changes.value.some(c => !c.deleted && c.name === name)
2025-03-15 17:06:23 +01:00
);
}
function newShift() {
if (shiftExists(newShiftName.value)) {
alert(`Shift ${newShiftName.value} already exists`);
return;
}
if (schedule.value.deleted) {
throw new Error("Unexpected deleted schedule");
}
2025-03-15 17:06:23 +01:00
if (!newShiftRole.value) {
alert(`Invalid role`);
return;
}
const change = {
id: newShiftId.value,
updatedAt: "",
name: newShiftName.value,
roleId: newShiftRole.value,
description: newShiftDescription.value || undefined,
slots: [],
2025-03-15 17:06:23 +01:00
};
changes.value = replaceChange(change, changes.value);
newShiftName.value = "";
newShiftDescription.value = "";
}
async function saveShifts() {
try {
await $fetch("/api/schedule", {
method: "PATCH",
body: {
id: 111,
updatedAt: "",
shifts: changes.value
} satisfies ApiSchedule,
2025-03-15 17:06:23 +01:00
});
changes.value = [];
} catch (err: any) {
console.error(err);
alert(err?.data?.message ?? err.message);
}
}
const shifts = computed(() => {
if (schedule.value.deleted) {
throw new Error("Unexpected deleted schedule");
}
const data = [...schedule.value.shifts ?? []].filter(shift => !shift.deleted && (!props.roleId || shift.roleId === props.roleId));
applyUpdatesToArray(changes.value.filter(change => !change.deleted), data);
2025-03-15 17:06:23 +01:00
return data;
});
</script>
<style scoped>
table {
border-spacing: 0;
}
table th {
text-align: left;
border-bottom: 1px solid var(--foreground);
}
table :is(th, td) + :is(th, td) {
padding-inline-start: 0.4em;
}
.removed :is(td, input) {
text-decoration: line-through;
}
</style>