Add editing of shift time slots
This commit is contained in:
parent
accc1690ff
commit
905ec8a38b
2 changed files with 683 additions and 0 deletions
653
components/ShiftScheduleTable.vue
Normal file
653
components/ShiftScheduleTable.vue
Normal file
|
@ -0,0 +1,653 @@
|
|||
<template>
|
||||
<div>
|
||||
<Timetable :schedule="schedulePreview" />
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>start</th>
|
||||
<th>end</th>
|
||||
<th>duration</th>
|
||||
<th>shift</th>
|
||||
<th>s</th>
|
||||
<th>role</th>
|
||||
<th v-if="edit"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-if="edit">
|
||||
<tr
|
||||
v-for="ss in shiftSlots"
|
||||
:key='ss.slot?.id ?? ss.start.toMillis()'
|
||||
:class='{
|
||||
removed: ss.type === "slot" && removed.has(ss.id),
|
||||
gap: ss.type === "gap",
|
||||
}'
|
||||
>
|
||||
<template v-if="ss.type === 'gap'">
|
||||
<td colspan="2">
|
||||
{{ gapFormat(ss) }}
|
||||
gap
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="time"
|
||||
v-model="newShiftDuration"
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
v-model="newShiftName"
|
||||
>
|
||||
</td>
|
||||
<td></td>
|
||||
<td>
|
||||
<select
|
||||
v-model="newShiftRole"
|
||||
>
|
||||
<option
|
||||
v-for="role in schedule.roles"
|
||||
:key="role.id"
|
||||
:value="role.id"
|
||||
:selected="role.id === newShiftRole"
|
||||
>{{ role.name }}</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
Add at
|
||||
<button
|
||||
type="button"
|
||||
@click="newShiftSlot({ start: ss.start })"
|
||||
>Start</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="newShiftSlot({ end: ss.end })"
|
||||
>End</button>
|
||||
</td>
|
||||
</template>
|
||||
<template v-else-if='edit'>
|
||||
<td>
|
||||
<input
|
||||
type="datetime-local"
|
||||
:value="ss.start.toFormat('yyyy-LL-dd\'T\'HH:mm')"
|
||||
@blur="editShiftSlot(ss, { start: ($event as any).target.value })"
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="time"
|
||||
:value="ss.end.toFormat('HH:mm')"
|
||||
@input="editShiftSlot(ss, { end: ($event as any).target.value })"
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="time"
|
||||
:value='dropDay(ss.end.diff(ss.start)).toFormat("hh:mm")'
|
||||
@input="editShiftSlot(ss, { duration: ($event as any).target.value })"
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
:value="ss.name"
|
||||
@input="editShiftSlot(ss, { name: ($event as any).target.value })"
|
||||
>
|
||||
</td>
|
||||
<td>{{ status(ss) }}</td>
|
||||
<td>
|
||||
<select
|
||||
:value="ss.role"
|
||||
@change="editShiftSlot(ss, { role: ($event as any).target.value })"
|
||||
>
|
||||
<option
|
||||
v-for="role in schedule.roles"
|
||||
:key="role.id"
|
||||
:value="role.id"
|
||||
:selected="role.id === ss.role"
|
||||
>{{ role.name }}</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
:disabled="removed.has(ss.id)"
|
||||
type="button"
|
||||
@click="delShiftSlot(ss)"
|
||||
>Remove</button>
|
||||
<button
|
||||
v-if="changes.some(c => c.data.id === ss.id)"
|
||||
type="button"
|
||||
@click="revertShiftSlot(ss.id)"
|
||||
>Revert</button>
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<input
|
||||
type="datetime-local"
|
||||
v-model="newShiftStart"
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="time"
|
||||
v-model="newShiftEnd"
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="time"
|
||||
v-model="newShiftDuration"
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
v-model="newShiftName"
|
||||
>
|
||||
</td>
|
||||
<td colspan="2">
|
||||
<button
|
||||
type="button"
|
||||
@click="newShiftSlot()"
|
||||
>Add Shift</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template v-else>
|
||||
<tr
|
||||
v-for="ss in shiftSlots"
|
||||
:key='ss.slot?.id ?? ss.start.toMillis()'
|
||||
:class='{
|
||||
gap: ss.type === "gap",
|
||||
}'
|
||||
>
|
||||
<template v-if="ss.type === 'gap'">
|
||||
<td colspan="2">
|
||||
{{ gapFormat(ss) }}
|
||||
gap
|
||||
</td>
|
||||
</template>
|
||||
<template v-else>
|
||||
<td>{{ ss.start.toFormat("yyyy-LL-dd HH:mm") }}</td>
|
||||
<td>{{ ss.end.toFormat("HH:mm") }}</td>
|
||||
<td>{{ ss.end.diff(ss.start).toFormat('hh:mm') }}</td>
|
||||
<td>{{ ss.name }}</td>
|
||||
<td>{{ status(ss) }}</td>
|
||||
<td>{{ ss.role }}</td>
|
||||
</template>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
<p v-if="changes.length">
|
||||
Changes are not save yet.
|
||||
<button
|
||||
type="button"
|
||||
@click="saveShiftSlots"
|
||||
>Save Changes</button>
|
||||
</p>
|
||||
<details>
|
||||
<summary>Debug</summary>
|
||||
<b>ShiftSlot changes</b>
|
||||
<ol>
|
||||
<li v-for="change in changes">
|
||||
<pre><code>{{ JSON.stringify((({ shift, slot, ...data }) => ({ op: change.op, data }))(change.data as any), undefined, " ") }}</code></pre>
|
||||
</li>
|
||||
</ol>
|
||||
<b>Shift changes</b>
|
||||
<ol>
|
||||
<li v-for="change in shiftChanges">
|
||||
<pre><code>{{ JSON.stringify((({ shift, slot, ...data }) => ({ op: change.op, data }))(change.data as any), undefined, " ") }}</code></pre>
|
||||
</li>
|
||||
</ol>
|
||||
</details>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import type { ChangeRecord, Schedule, Shift, Role, ShiftSlot as ShiftTimeSlot} from '~/shared/types/schedule';
|
||||
import { applyChangeArray } from '~/shared/utils/changes';
|
||||
import { enumerate, pairs, toId } from '~/shared/utils/functions';
|
||||
|
||||
const props = defineProps<{
|
||||
edit?: boolean,
|
||||
role?: string,
|
||||
}>();
|
||||
|
||||
interface ShiftSlot {
|
||||
type: "slot",
|
||||
id: string,
|
||||
shift?: Shift,
|
||||
slot?: ShiftTimeSlot,
|
||||
origRole: string,
|
||||
name: string,
|
||||
role: string,
|
||||
start: DateTime,
|
||||
end: DateTime,
|
||||
}
|
||||
|
||||
interface Gap {
|
||||
type: "gap",
|
||||
id?: undefined,
|
||||
shift?: undefined,
|
||||
slot?: undefined,
|
||||
name?: undefined,
|
||||
role?: string,
|
||||
start: DateTime,
|
||||
end: DateTime,
|
||||
}
|
||||
|
||||
function status(shiftSlot: ShiftSlot) {
|
||||
if (
|
||||
!shiftSlot.shift
|
||||
|| shiftSlot.shift.name !== shiftSlot.name
|
||||
) {
|
||||
const shift = schedule.value.rota?.find(shift => shift.name === shiftSlot.name);
|
||||
return shift ? "L" : "N";
|
||||
}
|
||||
return shiftSlot.shift.slots.length === 1 ? "" : shiftSlot.shift.slots.length;
|
||||
}
|
||||
|
||||
// Filter out set records where a del record exists for the same id.
|
||||
function filterSetOps<T extends { op: "set" | "del", data: { id: string }}>(changes: T[]) {
|
||||
const deleteIds = new Set(changes.filter(c => c.op === "del").map(c => c.data.id));
|
||||
return changes.filter(c => c.op !== "set" || !deleteIds.has(c.data.id));
|
||||
}
|
||||
|
||||
function findShift(
|
||||
shiftSlot: ShiftSlot,
|
||||
changes: ChangeRecord<Shift>[],
|
||||
schedule: Schedule,
|
||||
) {
|
||||
let setShift = changes.find(
|
||||
c => (
|
||||
c.op === "set"
|
||||
&& c.data.name === shiftSlot.name
|
||||
&& c.data.role === shiftSlot.role
|
||||
)
|
||||
)?.data as Shift | undefined;
|
||||
if (
|
||||
!setShift
|
||||
&& shiftSlot.shift
|
||||
&& shiftSlot.shift.name === shiftSlot.name
|
||||
) {
|
||||
setShift = shiftSlot.shift;
|
||||
}
|
||||
if (!setShift) {
|
||||
setShift = schedule.rota?.find(e => e.name === shiftSlot.name);
|
||||
}
|
||||
let delShift;
|
||||
if (shiftSlot.shift) {
|
||||
delShift = changes.find(
|
||||
c => c.op === "set" && c.data.name === shiftSlot.shift!.name
|
||||
)?.data as Shift | undefined;
|
||||
if (!delShift) {
|
||||
delShift = schedule.rota?.find(e => e.name === shiftSlot.shift!.name);
|
||||
}
|
||||
}
|
||||
return { setShift, delShift };
|
||||
}
|
||||
|
||||
function mergeSlot(shift: Shift, shiftSlot: ShiftSlot): Shift {
|
||||
const oldSlot = shift.slots.find(s => s.id === shiftSlot.id);
|
||||
const nextId = Math.max(-1, ...shift.slots.map(s => {
|
||||
const id = /-(\d+)$/.exec(s.id)?.[1];
|
||||
return id ? parseInt(id) : 0;
|
||||
})) + 1;
|
||||
const start = shiftSlot.start.toUTC().toISO({ suppressSeconds: true })!;
|
||||
const end = shiftSlot.end.toUTC().toISO({ suppressSeconds: true })!;
|
||||
|
||||
if (shift.role !== shiftSlot.role) {
|
||||
console.warn(`Attempt to add slot id=${shiftSlot.id} role=${shiftSlot.role} to shift id=${shift.id} role=${shift.role}`);
|
||||
}
|
||||
|
||||
// Edit slot in-place if possible
|
||||
if (oldSlot && oldSlot.id === shiftSlot.id) {
|
||||
return {
|
||||
...shift,
|
||||
slots: shift.slots.map(s => s.id !== oldSlot.id ? s : { ...s, start, end, }),
|
||||
};
|
||||
}
|
||||
|
||||
// Else remove old slot if it exist and insert a new one
|
||||
return {
|
||||
...shift,
|
||||
slots: [...(oldSlot ? shift.slots.filter(s => s.id !== oldSlot.id) : shift.slots), {
|
||||
id: oldSlot ? oldSlot.id : `${shift.id}-${nextId}`,
|
||||
start,
|
||||
end,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
const shiftChanges = computed(() => {
|
||||
let eventChanges: ChangeRecord<Shift>[] = [];
|
||||
for (const change of filterSetOps(changes.value)) {
|
||||
if (change.op === "set") {
|
||||
let { setShift, delShift } = findShift(change.data, eventChanges, schedule.value);
|
||||
if (delShift && delShift !== setShift) {
|
||||
eventChanges = removeSlot(eventChanges, delShift, change.data);
|
||||
}
|
||||
if (!setShift) {
|
||||
setShift = {
|
||||
id: toId(change.data.name),
|
||||
name: change.data.name,
|
||||
role: change.data.role,
|
||||
slots: [],
|
||||
};
|
||||
}
|
||||
setShift = {
|
||||
...setShift,
|
||||
role: change.data.role,
|
||||
}
|
||||
|
||||
eventChanges = replaceChange({
|
||||
op: "set",
|
||||
data: mergeSlot(setShift, change.data),
|
||||
}, eventChanges);
|
||||
|
||||
} else if (change.op === "del") {
|
||||
let { delShift } = findShift(change.data, eventChanges, schedule.value);
|
||||
if (delShift) {
|
||||
eventChanges = removeSlot(eventChanges, delShift, change.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
return eventChanges;
|
||||
});
|
||||
|
||||
const schedulePreview = computed(() => {
|
||||
const rota = [...schedule.value.rota ?? []]
|
||||
applyChangeArray(shiftChanges.value, rota);
|
||||
return {
|
||||
...schedule.value,
|
||||
rota,
|
||||
};
|
||||
});
|
||||
|
||||
function removeSlot(eventChanges: ChangeRecord<Shift>[], shift: Shift, shiftSlot: ShiftSlot) {
|
||||
let oldSlot = shift.slots.find(s => s.id === shiftSlot.id);
|
||||
if (oldSlot) {
|
||||
eventChanges = replaceChange({
|
||||
op: "set",
|
||||
data: { ...shift, slots: shift.slots.filter(s => s.id !== oldSlot.id) },
|
||||
}, eventChanges);
|
||||
}
|
||||
return eventChanges;
|
||||
}
|
||||
|
||||
const { data: session } = await useAccountSession();
|
||||
const schedule = await useSchedule();
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
const timezone = computed(
|
||||
() => session.value?.account?.timezone ?? runtimeConfig.public.defaultTimezone
|
||||
);
|
||||
|
||||
type ShiftSlotChange = { op: "set" | "del", data: ShiftSlot } ;
|
||||
|
||||
const changes = ref<ShiftSlotChange[]>([]);
|
||||
const removed = computed(() => new Set(changes.value.filter(c => c.op === "del").map(c => c.data.id)));
|
||||
|
||||
function replaceChange<T extends { op: "set" | "del", data: { id: string }}>(
|
||||
change: T,
|
||||
changes: T[],
|
||||
) {
|
||||
const index = changes.findIndex(item => (
|
||||
item.op === change.op && item.data.id === change.data.id
|
||||
));
|
||||
const copy = [...changes];
|
||||
if (index !== -1)
|
||||
copy.splice(index, 1, change);
|
||||
else
|
||||
copy.push(change);
|
||||
return copy;
|
||||
}
|
||||
function revertChange<T extends { op: "set" | "del", data: { id: string }}>(id: string, changes: T[]) {
|
||||
return changes.filter(change => change.data.id !== id);
|
||||
}
|
||||
|
||||
const oneDayMs = 24 * 60 * 60 * 1000;
|
||||
function dropDay(diff: Duration) {
|
||||
if (diff.toMillis() >= oneDayMs) {
|
||||
return diff.minus({ days: 1 });
|
||||
}
|
||||
return diff;
|
||||
}
|
||||
|
||||
const newShiftStart = ref("");
|
||||
const newShiftDuration = ref("01:00");
|
||||
const newShiftEnd = computed({
|
||||
get: () => (
|
||||
DateTime.fromISO(newShiftStart.value, { zone: timezone.value })
|
||||
.plus(Duration.fromISOTime(newShiftDuration.value))
|
||||
.toFormat("HH:mm")
|
||||
),
|
||||
set: (value: string) => {
|
||||
const start = DateTime.fromISO(newShiftStart.value, { zone: timezone.value });
|
||||
const end = endFromTime(start, value);
|
||||
newShiftDuration.value = dropDay(end.diff(start)).toFormat("hh:mm");
|
||||
},
|
||||
});
|
||||
const newShiftRole = ref(props.role);
|
||||
watch(() => props.role, () => {
|
||||
newShiftRole.value = props.role;
|
||||
});
|
||||
|
||||
function endFromTime(start: DateTime, time: string) {
|
||||
let end = start.startOf("day").plus(Duration.fromISOTime(time));
|
||||
if (end.toMillis() <= start.toMillis()) {
|
||||
end = end.plus({ days: 1 });
|
||||
}
|
||||
return end;
|
||||
}
|
||||
function durationFromTime(time: string) {
|
||||
let duration = Duration.fromISOTime(time);
|
||||
if (duration.toMillis() === 0) {
|
||||
duration = Duration.fromMillis(oneDayMs);
|
||||
}
|
||||
return duration;
|
||||
}
|
||||
const newShiftName = ref("");
|
||||
function editShiftSlot(
|
||||
shiftSlot: ShiftSlot,
|
||||
edits: {
|
||||
start?: string,
|
||||
end?: string,
|
||||
duration?: string,
|
||||
name?: string,
|
||||
role?: string,
|
||||
}
|
||||
) {
|
||||
if (edits.start) {
|
||||
const start = DateTime.fromISO(edits.start, { zone: timezone.value });
|
||||
shiftSlot = {
|
||||
...shiftSlot,
|
||||
start,
|
||||
end: start.plus(shiftSlot.end.diff(shiftSlot.start)),
|
||||
};
|
||||
}
|
||||
if (edits.end !== undefined) {
|
||||
|
||||
shiftSlot = {
|
||||
...shiftSlot,
|
||||
end: endFromTime(shiftSlot.start, edits.end),
|
||||
};
|
||||
}
|
||||
if (edits.duration !== undefined) {
|
||||
shiftSlot = {
|
||||
...shiftSlot,
|
||||
end: shiftSlot.start.plus(durationFromTime(edits.duration)),
|
||||
};
|
||||
}
|
||||
if (edits.name !== undefined) {
|
||||
shiftSlot = {
|
||||
...shiftSlot,
|
||||
name: edits.name,
|
||||
};
|
||||
}
|
||||
if (edits.role !== undefined) {
|
||||
let changesCopy = changes.value;
|
||||
for (const slot of shiftSlots.value) {
|
||||
if (slot.type === "slot" && slot.shift?.name === shiftSlot.name) {
|
||||
changesCopy = replaceChange({
|
||||
op: "set",
|
||||
data: { ...slot, role: edits.role }
|
||||
}, changesCopy);
|
||||
}
|
||||
}
|
||||
changesCopy = replaceChange({
|
||||
op: "set",
|
||||
data: { ...shiftSlot, role: edits.role }
|
||||
}, changesCopy);
|
||||
changes.value = changesCopy;
|
||||
return;
|
||||
}
|
||||
const change = { op: "set" as const, data: shiftSlot };
|
||||
changes.value = replaceChange(change, changes.value);
|
||||
}
|
||||
function delShiftSlot(shiftSlot: ShiftSlot) {
|
||||
const change = { op: "del" as const, data: shiftSlot };
|
||||
changes.value = replaceChange(change, changes.value);
|
||||
}
|
||||
function revertShiftSlot(id: string) {
|
||||
changes.value = revertChange(id, changes.value);
|
||||
}
|
||||
function newShiftSlot(options: { start?: DateTime, end?: DateTime } = {}) {
|
||||
const name = newShiftName.value;
|
||||
const role = newShiftRole.value;
|
||||
if (!role) {
|
||||
alert("Invalid role");
|
||||
return;
|
||||
}
|
||||
let start;
|
||||
let end;
|
||||
const duration = durationFromTime(newShiftDuration.value);
|
||||
if (!duration.isValid) {
|
||||
alert("Invalid duration");
|
||||
return;
|
||||
}
|
||||
if (options.start) {
|
||||
start = options.start;
|
||||
end = options.start.plus(duration);
|
||||
} else if (options.end) {
|
||||
end = options.end;
|
||||
start = options.end.minus(duration);
|
||||
} else {
|
||||
start = DateTime.fromISO(newShiftStart.value, { zone: timezone.value });
|
||||
end = endFromTime(start, newShiftEnd.value);
|
||||
}
|
||||
if (!start.isValid || !end.isValid) {
|
||||
alert("Invalid start and/or end time");
|
||||
return;
|
||||
}
|
||||
const change: ChangeRecord<ShiftSlot> = {
|
||||
op: "set" as const,
|
||||
data: {
|
||||
type: "slot",
|
||||
id: `$new-${Date.now()}`,
|
||||
name,
|
||||
origRole: role,
|
||||
role,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
};
|
||||
newShiftName.value = "";
|
||||
changes.value = replaceChange(change, changes.value);
|
||||
}
|
||||
async function saveShiftSlots() {
|
||||
try {
|
||||
await $fetch("/api/schedule", {
|
||||
method: "PATCH",
|
||||
body: { rota: shiftChanges.value },
|
||||
});
|
||||
changes.value = [];
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
alert(err?.data?.message ?? err.message);
|
||||
}
|
||||
}
|
||||
|
||||
const oneHourMs = 60 * 60 * 1000;
|
||||
function gapFormat(gap: Gap) {
|
||||
let diff = gap.end.diff(gap.start);
|
||||
if (diff.toMillis() % oneHourMs !== 0)
|
||||
diff = diff.shiftTo("hours", "minutes");
|
||||
else
|
||||
diff = diff.shiftTo("hours");
|
||||
return diff.toHuman({ listStyle: "short", unitDisplay: "short" });
|
||||
}
|
||||
|
||||
const shiftSlots = computed(() => {
|
||||
const data: (ShiftSlot | Gap)[] = [];
|
||||
for (const shift of schedule.value.rota ?? []) {
|
||||
if (props.role !== undefined && shift.role !== props.role)
|
||||
continue;
|
||||
for (const slot of shift.slots) {
|
||||
data.push({
|
||||
type: "slot",
|
||||
id: slot.id,
|
||||
shift,
|
||||
slot,
|
||||
name: shift.name,
|
||||
role: shift.role,
|
||||
origRole: shift.role,
|
||||
start: DateTime.fromISO(slot.start, { zone: timezone.value }),
|
||||
end: DateTime.fromISO(slot.end, { zone: timezone.value }),
|
||||
});
|
||||
}
|
||||
}
|
||||
applyChangeArray(changes.value.filter(change => change.op === "set"), data as ShiftSlot[]);
|
||||
data.sort((a, b) => a.start.toMillis() - b.start.toMillis() || a.end.toMillis() - b.end.toMillis());
|
||||
|
||||
// Insert gaps
|
||||
let maxEnd = 0;
|
||||
const gaps: [number, Gap][] = []
|
||||
for (const [index, [first, second]] of enumerate(pairs(data))) {
|
||||
maxEnd = Math.max(maxEnd, first.end.toMillis());
|
||||
if (maxEnd < second.start.toMillis()) {
|
||||
gaps.push([index, {
|
||||
type: "gap",
|
||||
role: props.role,
|
||||
start: DateTime.fromMillis(maxEnd),
|
||||
end: second.start,
|
||||
}]);
|
||||
}
|
||||
}
|
||||
gaps.reverse();
|
||||
for (const [index, gap] of gaps) {
|
||||
data.splice(index + 1, 0, gap);
|
||||
}
|
||||
return data;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
label {
|
||||
display: inline;
|
||||
padding-inline-end: 0.75em;
|
||||
}
|
||||
table {
|
||||
margin-block-start: 1rem;
|
||||
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;
|
||||
}
|
||||
.gap {
|
||||
height: 1.8em;
|
||||
}
|
||||
.removed {
|
||||
background-color: color-mix(in oklab, var(--background), rgb(255, 0, 0) 40%);
|
||||
}
|
||||
.removed :is(td, input) {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
</style>
|
|
@ -26,6 +26,25 @@
|
|||
<EventsTable :edit="true" />
|
||||
<h2>Roles</h2>
|
||||
<RolesTable :edit="true" />
|
||||
<h2>Shift Schedule</h2>
|
||||
<label>
|
||||
Role Filter:
|
||||
<select
|
||||
v-model="roleFilter"
|
||||
>
|
||||
<option
|
||||
:value="undefined"
|
||||
:selected="roleFilter === undefined"
|
||||
><All roles></option>
|
||||
<option
|
||||
v-for="role in schedule.roles"
|
||||
:key="role.id"
|
||||
:value="role.id"
|
||||
:selected="roleFilter === role.id"
|
||||
>{{ role.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<ShiftScheduleTable :edit="true" :role="roleFilter" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
|
@ -59,6 +78,17 @@ const locationFilter = computed({
|
|||
}),
|
||||
});
|
||||
|
||||
const roleFilter = computed({
|
||||
get: () => queryToString(route.query.role),
|
||||
set: (value: string | undefined) => navigateTo({
|
||||
path: route.path,
|
||||
query: {
|
||||
...route.query,
|
||||
role: value,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const { data: session } = await useAccountSession();
|
||||
const isAdmin = computed(() => session.value?.account.type === "admin")
|
||||
</script>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue