Compare commits
5 commits
345caec57f
...
9013e85ff0
Author | SHA1 | Date | |
---|---|---|---|
9013e85ff0 | |||
ae1c653af6 | |||
848a330f3a | |||
085e348aa8 | |||
4ff3dcb3fe |
14 changed files with 233 additions and 14 deletions
|
@ -29,7 +29,6 @@ defineProps<{
|
|||
edit?: boolean
|
||||
}>();
|
||||
const usersStore = useUsersStore();
|
||||
await usersStore.fetch();
|
||||
const assignedIds = defineModel<Set<number>>({ required: true });
|
||||
const assigned = computed(
|
||||
() => [...assignedIds.value].map(
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
<template>
|
||||
<section class="event">
|
||||
<h3>{{ event.name }}</h3>
|
||||
<p v-if=event.host>
|
||||
Host: {{ event.host }}
|
||||
</p>
|
||||
<p>{{ event.description ?? "No description provided" }}</p>
|
||||
<p v-if="event.interested">
|
||||
{{ event.interested }} interested
|
||||
|
@ -35,7 +38,7 @@
|
|||
<template v-if="slot.interested">
|
||||
({{ slot.interested }} interested)
|
||||
</template>
|
||||
<p v-if="slot.assigned">
|
||||
<p v-if="slot.assigned.size">
|
||||
Crew:
|
||||
{{ [...slot.assigned].map(id => usersStore.users.get(id)?.name).join(", ") }}
|
||||
</p>
|
||||
|
@ -53,7 +56,6 @@ defineProps<{
|
|||
|
||||
const accountStore = useAccountStore();
|
||||
const usersStore = useUsersStore();
|
||||
await usersStore.fetch();
|
||||
|
||||
function formatTime(time: DateTime) {
|
||||
return time.toFormat("yyyy-LL-dd HH:mm");
|
||||
|
|
68
components/CardEventSlot.vue
Normal file
68
components/CardEventSlot.vue
Normal file
|
@ -0,0 +1,68 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no>
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<section class="eventSlot">
|
||||
<hgroup>
|
||||
<h3>{{ event?.name }}</h3>
|
||||
<p>
|
||||
{{ formatTime(slot.start) }} - {{ formatTime(slot.end) }}
|
||||
</p>
|
||||
</hgroup>
|
||||
<p>{{ event?.description ?? "No description provided" }}</p>
|
||||
<p v-if="locations.length">
|
||||
At {{ locations.map(location => location?.name ?? "unknown").join(" + ") }}
|
||||
</p>
|
||||
<p v-if="event?.interested">
|
||||
{{ event?.interested }}
|
||||
<template v-if="slot.interested">
|
||||
+ {{ slot.interested }}
|
||||
</template>
|
||||
interested
|
||||
</p>
|
||||
<p v-if="slot.assigned.size">
|
||||
Crew:
|
||||
{{ [...slot.assigned].map(id => usersStore.users.get(id)?.name).join(", ") }}
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { DateTime } from '~/shared/utils/luxon';
|
||||
|
||||
const props = defineProps<{
|
||||
event?: ClientScheduleEvent,
|
||||
slot: ClientScheduleEventSlot,
|
||||
}>()
|
||||
const scheduleStore = useSchedulesStore();
|
||||
const schedule = scheduleStore.activeSchedule;
|
||||
|
||||
const usersStore = useUsersStore();
|
||||
const locations = computed(() => [...props.slot.locationIds].map(id => schedule.value.locations.get(id)));
|
||||
|
||||
function formatTime(time: DateTime) {
|
||||
return time.toFormat("yyyy-LL-dd HH:mm");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.eventSlot {
|
||||
background: color-mix(in oklab, var(--background), grey 20%);
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
.eventSlot h3 {
|
||||
margin: 0;
|
||||
}
|
||||
.eventSlot + .eventSlot {
|
||||
margin-block-start: 0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
padding-inline: 0.2em;
|
||||
}
|
||||
button.active {
|
||||
color: color-mix(in oklab, var(--foreground), green 50%);
|
||||
}
|
||||
</style>
|
|
@ -27,9 +27,7 @@ defineProps<{
|
|||
shift: ClientScheduleShift
|
||||
}>()
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const usersStore = useUsersStore();
|
||||
await usersStore.fetch();
|
||||
|
||||
function formatTime(time: DateTime) {
|
||||
return time.toFormat("yyyy-LL-dd HH:mm");
|
||||
|
|
|
@ -11,6 +11,12 @@
|
|||
:after="event.name"
|
||||
:state
|
||||
/>
|
||||
<DiffFieldString
|
||||
title="Host"
|
||||
:before="event.serverHost"
|
||||
:after="event.host"
|
||||
:state
|
||||
/>
|
||||
<DiffFieldString
|
||||
title="Public"
|
||||
:before='event.serverCrew ? "No" : "Yes"'
|
||||
|
|
|
@ -9,6 +9,9 @@
|
|||
<li>
|
||||
<NuxtLink to="/">Home</NuxtLink>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/events">Events</NuxtLink>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/schedule">Schedule</NuxtLink>
|
||||
</li>
|
||||
|
|
|
@ -215,7 +215,6 @@ interface Gap {
|
|||
|
||||
const accountStore = useAccountStore();
|
||||
const usersStore = useUsersStore();
|
||||
await usersStore.fetch();
|
||||
const schedule = await useSchedule();
|
||||
|
||||
const oneDayMs = 24 * 60 * 60 * 1000;
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
<tr>
|
||||
<th>id</th>
|
||||
<th>name</th>
|
||||
<th>host</th>
|
||||
<th>description</th>
|
||||
<th>p</th>
|
||||
<th>s</th>
|
||||
|
@ -30,6 +31,13 @@
|
|||
v-model="event.name"
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
:disabled="!canEdit(event)"
|
||||
v-model="event.host"
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
|
@ -101,6 +109,7 @@
|
|||
>
|
||||
<td>{{ event.id }}</td>
|
||||
<td>{{ event.name }}</td>
|
||||
<td>{{ event.host }}</td>
|
||||
<td>{{ event.description }}</td>
|
||||
<td>{{ event.crew ? "" : "Yes"}}</td>
|
||||
<td>{{ event.slots.size ? event.slots.size : "" }}</td>
|
||||
|
|
|
@ -209,7 +209,6 @@ interface Gap {
|
|||
|
||||
const accountStore = useAccountStore();
|
||||
const usersStore = useUsersStore();
|
||||
await usersStore.fetch();
|
||||
const schedule = await useSchedule();
|
||||
|
||||
const oneDayMs = 24 * 60 * 60 * 1000;
|
||||
|
|
|
@ -55,7 +55,6 @@
|
|||
<script lang="ts" setup>
|
||||
useEventSource();
|
||||
const usersStore = useUsersStore();
|
||||
await usersStore.fetch();
|
||||
|
||||
async function saveUser(user: ClientUser) {
|
||||
try {
|
||||
|
|
|
@ -77,6 +77,7 @@
|
|||
<td :colSpan="totalColumns">
|
||||
<div
|
||||
v-if="nowOffset !== undefined"
|
||||
ref="nowLine"
|
||||
class="now"
|
||||
:style="` --now-offset: ${nowOffset}`"
|
||||
>
|
||||
|
@ -677,6 +678,16 @@ const nowOffset = computed(() => {
|
|||
offset += group.width;
|
||||
}
|
||||
});
|
||||
|
||||
const nowLine = useTemplateRef("nowLine"); // If I name the ref now, the element disappears?!
|
||||
function scrollToNow() {
|
||||
if (nowLine.value) {
|
||||
nowLine.value.scrollIntoView({ behavior: "smooth", inline: "start", block: "nearest" });
|
||||
}
|
||||
}
|
||||
defineExpose({
|
||||
scrollToNow,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
89
pages/events.vue
Normal file
89
pages/events.vue
Normal file
|
@ -0,0 +1,89 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no>
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<main>
|
||||
<h1>Events</h1>
|
||||
<label v-if="accountStore.valid">
|
||||
Filter:
|
||||
<select
|
||||
v-model="filter"
|
||||
>
|
||||
<option
|
||||
:value="undefined"
|
||||
:selected="filter === undefined"
|
||||
><All events></option>
|
||||
<option
|
||||
value="my-schedule"
|
||||
:selected='filter === "my-schedule"'
|
||||
>My Schedule</option>
|
||||
<option
|
||||
v-if="accountStore.isCrew"
|
||||
value="assigned"
|
||||
:selected='filter === "assigned"'
|
||||
>Assigned to Me</option>
|
||||
</select>
|
||||
</label>
|
||||
<CardEvent
|
||||
v-for="event in events"
|
||||
:key="`my-${event.id}`"
|
||||
:id="String(event.name)"
|
||||
:event
|
||||
></CardEvent>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({
|
||||
title: "Schedule",
|
||||
});
|
||||
|
||||
const stringSort = useStringSort();
|
||||
const events = computed(() => {
|
||||
return [...schedule.value.events.values()].filter(e => !e.deleted && [...e.slots.values()].some(eventSlotFilter.value)).sort((a, b) => stringSort(a.name, b.name));
|
||||
});
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const usersStore = useUsersStore();
|
||||
await usersStore.fetch();
|
||||
const schedule = await useSchedule();
|
||||
|
||||
const route = useRoute();
|
||||
const filter = computed({
|
||||
get: () => queryToString(route.query.filter),
|
||||
set: (value: string | undefined) => navigateTo({
|
||||
path: route.path,
|
||||
query: {
|
||||
...route.query,
|
||||
filter: value,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const eventSlotFilter = computed(() => {
|
||||
if (filter.value === undefined || !accountStore.valid || schedule.value.deleted) {
|
||||
return () => true;
|
||||
}
|
||||
const aid = accountStore.id;
|
||||
if (filter.value === "my-schedule") {
|
||||
const slotIds = new Set(accountStore.interestedEventSlotIds);
|
||||
for (const event of schedule.value.events.values()) {
|
||||
if (!event.deleted && accountStore.interestedEventIds.has(event.id)) {
|
||||
for (const slot of event.slots.values()) {
|
||||
slotIds.add(slot.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (slot: ClientScheduleEventSlot) => slotIds.has(slot.id) || slot.assigned.has(aid!) || false;
|
||||
}
|
||||
if (filter.value === "assigned") {
|
||||
return (slot: ClientScheduleEventSlot) => slot.assigned.has(aid!) || false;
|
||||
}
|
||||
if (filter.value.startsWith("crew-")) {
|
||||
const cid = parseInt(filter.value.slice(5));
|
||||
return (slot: ClientScheduleEventSlot) => slot.assigned.has(cid) || false;
|
||||
}
|
||||
return () => false;
|
||||
});
|
||||
</script>
|
|
@ -4,7 +4,7 @@
|
|||
-->
|
||||
<template>
|
||||
<main>
|
||||
<h1>Schedule & Events</h1>
|
||||
<h1>Schedule</h1>
|
||||
<p>
|
||||
Study carefully, we only hold these events once a year.
|
||||
</p>
|
||||
|
@ -44,12 +44,17 @@
|
|||
</optgroup>
|
||||
</select>
|
||||
</label>
|
||||
<Timetable :schedule :eventSlotFilter :shiftSlotFilter />
|
||||
<Timetable ref="timetable" :schedule :eventSlotFilter :shiftSlotFilter />
|
||||
<h2>Events</h2>
|
||||
<CardEvent
|
||||
v-for="event in [...schedule.events.values()].filter(e => !e.deleted && [...e.slots.values()].some(eventSlotFilter))"
|
||||
:key="event.id"
|
||||
:event
|
||||
<label>
|
||||
Hide past events
|
||||
<input type="checkbox" v-model="hidePastEvents">
|
||||
</label>
|
||||
<CardEventSlot
|
||||
v-for="eventSlot in eventSlots"
|
||||
:key="eventSlot.slot.id"
|
||||
:event="eventSlot.event"
|
||||
:slot="eventSlot.slot"
|
||||
/>
|
||||
<template v-if="accountStore.isCrew">
|
||||
<h2>Shifts</h2>
|
||||
|
@ -91,6 +96,17 @@ const filter = computed({
|
|||
}),
|
||||
});
|
||||
|
||||
const hidePastEvents = computed({
|
||||
get: () => !queryToBoolean(route.query.showPast),
|
||||
set: (value: boolean) => navigateTo({
|
||||
path: route.path,
|
||||
query: {
|
||||
...route.query,
|
||||
showPast: value ? undefined : null,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const eventSlotFilter = computed(() => {
|
||||
if (filter.value === undefined || !accountStore.valid || schedule.value.deleted) {
|
||||
return () => true;
|
||||
|
@ -130,4 +146,19 @@ const shiftSlotFilter = computed(() => {
|
|||
}
|
||||
return () => false;
|
||||
});
|
||||
|
||||
const eventSlots = computed(() => {
|
||||
let slots = [...schedule.value.eventSlots.values()].filter(slot => !slot.deleted && eventSlotFilter.value(slot));
|
||||
if (hidePastEvents.value) {
|
||||
const nowMs = Date.now();
|
||||
slots = slots.filter(slot => slot.end.toMillis() >= nowMs);
|
||||
}
|
||||
slots.sort((a, b) => a.start.toMillis() - b.start.toMillis() || a.end.toMillis() - b.end.toMillis());
|
||||
return slots.map(slot => ({ slot, event: schedule.value.events.get(slot.eventId!) }));
|
||||
});
|
||||
|
||||
const timetable = useTemplateRef("timetable");
|
||||
onMounted(() => {
|
||||
timetable.value?.scrollToNow();
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -21,6 +21,12 @@ export function queryToNumber(item?: null | LocationQueryValue | LocationQueryVa
|
|||
return Number.parseInt(item, 10);
|
||||
}
|
||||
|
||||
export function queryToBoolean(item?: null | LocationQueryValue | LocationQueryValue[]) {
|
||||
if (item === undefined)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function idMap<T extends { id: Id }>(entities: T[]) {
|
||||
return new Map(entities.map(entity => [entity.id, entity]));
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue