Compare commits
No commits in common. "9013e85ff0e2f318c2bfef98d204aafa315e39c1" and "345caec57f93c86d82228858d6b3fa484c683e48" have entirely different histories.
9013e85ff0
...
345caec57f
14 changed files with 14 additions and 233 deletions
|
@ -29,6 +29,7 @@ defineProps<{
|
||||||
edit?: boolean
|
edit?: boolean
|
||||||
}>();
|
}>();
|
||||||
const usersStore = useUsersStore();
|
const usersStore = useUsersStore();
|
||||||
|
await usersStore.fetch();
|
||||||
const assignedIds = defineModel<Set<number>>({ required: true });
|
const assignedIds = defineModel<Set<number>>({ required: true });
|
||||||
const assigned = computed(
|
const assigned = computed(
|
||||||
() => [...assignedIds.value].map(
|
() => [...assignedIds.value].map(
|
||||||
|
|
|
@ -5,9 +5,6 @@
|
||||||
<template>
|
<template>
|
||||||
<section class="event">
|
<section class="event">
|
||||||
<h3>{{ event.name }}</h3>
|
<h3>{{ event.name }}</h3>
|
||||||
<p v-if=event.host>
|
|
||||||
Host: {{ event.host }}
|
|
||||||
</p>
|
|
||||||
<p>{{ event.description ?? "No description provided" }}</p>
|
<p>{{ event.description ?? "No description provided" }}</p>
|
||||||
<p v-if="event.interested">
|
<p v-if="event.interested">
|
||||||
{{ event.interested }} interested
|
{{ event.interested }} interested
|
||||||
|
@ -38,7 +35,7 @@
|
||||||
<template v-if="slot.interested">
|
<template v-if="slot.interested">
|
||||||
({{ slot.interested }} interested)
|
({{ slot.interested }} interested)
|
||||||
</template>
|
</template>
|
||||||
<p v-if="slot.assigned.size">
|
<p v-if="slot.assigned">
|
||||||
Crew:
|
Crew:
|
||||||
{{ [...slot.assigned].map(id => usersStore.users.get(id)?.name).join(", ") }}
|
{{ [...slot.assigned].map(id => usersStore.users.get(id)?.name).join(", ") }}
|
||||||
</p>
|
</p>
|
||||||
|
@ -56,6 +53,7 @@ defineProps<{
|
||||||
|
|
||||||
const accountStore = useAccountStore();
|
const accountStore = useAccountStore();
|
||||||
const usersStore = useUsersStore();
|
const usersStore = useUsersStore();
|
||||||
|
await usersStore.fetch();
|
||||||
|
|
||||||
function formatTime(time: DateTime) {
|
function formatTime(time: DateTime) {
|
||||||
return time.toFormat("yyyy-LL-dd HH:mm");
|
return time.toFormat("yyyy-LL-dd HH:mm");
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
<!--
|
|
||||||
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,7 +27,9 @@ defineProps<{
|
||||||
shift: ClientScheduleShift
|
shift: ClientScheduleShift
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const accountStore = useAccountStore();
|
||||||
const usersStore = useUsersStore();
|
const usersStore = useUsersStore();
|
||||||
|
await usersStore.fetch();
|
||||||
|
|
||||||
function formatTime(time: DateTime) {
|
function formatTime(time: DateTime) {
|
||||||
return time.toFormat("yyyy-LL-dd HH:mm");
|
return time.toFormat("yyyy-LL-dd HH:mm");
|
||||||
|
|
|
@ -11,12 +11,6 @@
|
||||||
:after="event.name"
|
:after="event.name"
|
||||||
:state
|
:state
|
||||||
/>
|
/>
|
||||||
<DiffFieldString
|
|
||||||
title="Host"
|
|
||||||
:before="event.serverHost"
|
|
||||||
:after="event.host"
|
|
||||||
:state
|
|
||||||
/>
|
|
||||||
<DiffFieldString
|
<DiffFieldString
|
||||||
title="Public"
|
title="Public"
|
||||||
:before='event.serverCrew ? "No" : "Yes"'
|
:before='event.serverCrew ? "No" : "Yes"'
|
||||||
|
|
|
@ -9,9 +9,6 @@
|
||||||
<li>
|
<li>
|
||||||
<NuxtLink to="/">Home</NuxtLink>
|
<NuxtLink to="/">Home</NuxtLink>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<NuxtLink to="/events">Events</NuxtLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<NuxtLink to="/schedule">Schedule</NuxtLink>
|
<NuxtLink to="/schedule">Schedule</NuxtLink>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -215,6 +215,7 @@ interface Gap {
|
||||||
|
|
||||||
const accountStore = useAccountStore();
|
const accountStore = useAccountStore();
|
||||||
const usersStore = useUsersStore();
|
const usersStore = useUsersStore();
|
||||||
|
await usersStore.fetch();
|
||||||
const schedule = await useSchedule();
|
const schedule = await useSchedule();
|
||||||
|
|
||||||
const oneDayMs = 24 * 60 * 60 * 1000;
|
const oneDayMs = 24 * 60 * 60 * 1000;
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th>id</th>
|
<th>id</th>
|
||||||
<th>name</th>
|
<th>name</th>
|
||||||
<th>host</th>
|
|
||||||
<th>description</th>
|
<th>description</th>
|
||||||
<th>p</th>
|
<th>p</th>
|
||||||
<th>s</th>
|
<th>s</th>
|
||||||
|
@ -31,13 +30,6 @@
|
||||||
v-model="event.name"
|
v-model="event.name"
|
||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
:disabled="!canEdit(event)"
|
|
||||||
v-model="event.host"
|
|
||||||
>
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -109,7 +101,6 @@
|
||||||
>
|
>
|
||||||
<td>{{ event.id }}</td>
|
<td>{{ event.id }}</td>
|
||||||
<td>{{ event.name }}</td>
|
<td>{{ event.name }}</td>
|
||||||
<td>{{ event.host }}</td>
|
|
||||||
<td>{{ event.description }}</td>
|
<td>{{ event.description }}</td>
|
||||||
<td>{{ event.crew ? "" : "Yes"}}</td>
|
<td>{{ event.crew ? "" : "Yes"}}</td>
|
||||||
<td>{{ event.slots.size ? event.slots.size : "" }}</td>
|
<td>{{ event.slots.size ? event.slots.size : "" }}</td>
|
||||||
|
|
|
@ -209,6 +209,7 @@ interface Gap {
|
||||||
|
|
||||||
const accountStore = useAccountStore();
|
const accountStore = useAccountStore();
|
||||||
const usersStore = useUsersStore();
|
const usersStore = useUsersStore();
|
||||||
|
await usersStore.fetch();
|
||||||
const schedule = await useSchedule();
|
const schedule = await useSchedule();
|
||||||
|
|
||||||
const oneDayMs = 24 * 60 * 60 * 1000;
|
const oneDayMs = 24 * 60 * 60 * 1000;
|
||||||
|
|
|
@ -55,6 +55,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
useEventSource();
|
useEventSource();
|
||||||
const usersStore = useUsersStore();
|
const usersStore = useUsersStore();
|
||||||
|
await usersStore.fetch();
|
||||||
|
|
||||||
async function saveUser(user: ClientUser) {
|
async function saveUser(user: ClientUser) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -77,7 +77,6 @@
|
||||||
<td :colSpan="totalColumns">
|
<td :colSpan="totalColumns">
|
||||||
<div
|
<div
|
||||||
v-if="nowOffset !== undefined"
|
v-if="nowOffset !== undefined"
|
||||||
ref="nowLine"
|
|
||||||
class="now"
|
class="now"
|
||||||
:style="` --now-offset: ${nowOffset}`"
|
:style="` --now-offset: ${nowOffset}`"
|
||||||
>
|
>
|
||||||
|
@ -678,16 +677,6 @@ const nowOffset = computed(() => {
|
||||||
offset += group.width;
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -1,89 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
<template>
|
||||||
<main>
|
<main>
|
||||||
<h1>Schedule</h1>
|
<h1>Schedule & Events</h1>
|
||||||
<p>
|
<p>
|
||||||
Study carefully, we only hold these events once a year.
|
Study carefully, we only hold these events once a year.
|
||||||
</p>
|
</p>
|
||||||
|
@ -44,17 +44,12 @@
|
||||||
</optgroup>
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<Timetable ref="timetable" :schedule :eventSlotFilter :shiftSlotFilter />
|
<Timetable :schedule :eventSlotFilter :shiftSlotFilter />
|
||||||
<h2>Events</h2>
|
<h2>Events</h2>
|
||||||
<label>
|
<CardEvent
|
||||||
Hide past events
|
v-for="event in [...schedule.events.values()].filter(e => !e.deleted && [...e.slots.values()].some(eventSlotFilter))"
|
||||||
<input type="checkbox" v-model="hidePastEvents">
|
:key="event.id"
|
||||||
</label>
|
:event
|
||||||
<CardEventSlot
|
|
||||||
v-for="eventSlot in eventSlots"
|
|
||||||
:key="eventSlot.slot.id"
|
|
||||||
:event="eventSlot.event"
|
|
||||||
:slot="eventSlot.slot"
|
|
||||||
/>
|
/>
|
||||||
<template v-if="accountStore.isCrew">
|
<template v-if="accountStore.isCrew">
|
||||||
<h2>Shifts</h2>
|
<h2>Shifts</h2>
|
||||||
|
@ -96,17 +91,6 @@ 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(() => {
|
const eventSlotFilter = computed(() => {
|
||||||
if (filter.value === undefined || !accountStore.valid || schedule.value.deleted) {
|
if (filter.value === undefined || !accountStore.valid || schedule.value.deleted) {
|
||||||
return () => true;
|
return () => true;
|
||||||
|
@ -146,19 +130,4 @@ const shiftSlotFilter = computed(() => {
|
||||||
}
|
}
|
||||||
return () => false;
|
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>
|
</script>
|
||||||
|
|
|
@ -21,12 +21,6 @@ export function queryToNumber(item?: null | LocationQueryValue | LocationQueryVa
|
||||||
return Number.parseInt(item, 10);
|
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[]) {
|
export function idMap<T extends { id: Id }>(entities: T[]) {
|
||||||
return new Map(entities.map(entity => [entity.id, entity]));
|
return new Map(entities.map(entity => [entity.id, entity]));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue