Compare commits
10 commits
52973ffa9a
...
732566a29c
Author | SHA1 | Date | |
---|---|---|---|
732566a29c | |||
5898a46a1b | |||
adeef4f629 | |||
37edf122a1 | |||
96681bfd37 | |||
6d9d937c70 | |||
a8c62e6688 | |||
f29b1f7afd | |||
9a46ea5af0 | |||
d006be251c |
23 changed files with 363 additions and 80 deletions
|
@ -2,7 +2,7 @@
|
||||||
# SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no>
|
# SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no>
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
# Based on Next.js's docker image example
|
# Based on Next.js's docker image example
|
||||||
FROM node:22 AS base
|
FROM node:22-slim AS base
|
||||||
|
|
||||||
# Install dependencies only when needed
|
# Install dependencies only when needed
|
||||||
FROM base AS deps
|
FROM base AS deps
|
||||||
|
|
|
@ -83,3 +83,7 @@ label>* {
|
||||||
label + label {
|
label + label {
|
||||||
margin-block-start: 0.5rem;
|
margin-block-start: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preWrap {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
|
@ -8,7 +8,15 @@
|
||||||
<p v-if=event.host>
|
<p v-if=event.host>
|
||||||
Host: {{ event.host }}
|
Host: {{ event.host }}
|
||||||
</p>
|
</p>
|
||||||
<p>{{ event.description ?? "No description provided" }}</p>
|
<div v-if="event.notice" class="notice preWrap">
|
||||||
|
<div class="noticeIcon">
|
||||||
|
⚠️
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
{{ event.notice }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p class="preWrap">{{ event.description ?? "No description provided" }}</p>
|
||||||
<p v-if="event.interested">
|
<p v-if="event.interested">
|
||||||
{{ event.interested }} interested
|
{{ event.interested }} interested
|
||||||
</p>
|
</p>
|
||||||
|
@ -79,6 +87,22 @@ async function toggle(type: "event" | "slot", id: number, slotIds?: number[]) {
|
||||||
margin-block-start: 0.5rem;
|
margin-block-start: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notice {
|
||||||
|
display: flex;
|
||||||
|
width: fit-content;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin-block: 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
border: 1px solid color-mix(in oklab, CanvasText, orange 50%);
|
||||||
|
background-color: color-mix(in oklab, Canvas, orange 40%);
|
||||||
|
}
|
||||||
|
.noticeIcon {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-self: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding-inline: 0.2em;
|
padding-inline: 0.2em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,15 @@
|
||||||
<p v-if=event?.host>
|
<p v-if=event?.host>
|
||||||
Host: {{ event.host }}
|
Host: {{ event.host }}
|
||||||
</p>
|
</p>
|
||||||
<p>{{ event?.description ?? "No description provided" }}</p>
|
<div v-if="event?.notice" class="notice preWrap">
|
||||||
|
<div class="noticeIcon">
|
||||||
|
⚠️
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
{{ event.notice }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p class="preWrap">{{ event?.description ?? "No description provided" }}</p>
|
||||||
<p v-if="locations.length">
|
<p v-if="locations.length">
|
||||||
At {{ locations.map(location => location?.name ?? "unknown").join(" + ") }}
|
At {{ locations.map(location => location?.name ?? "unknown").join(" + ") }}
|
||||||
</p>
|
</p>
|
||||||
|
@ -62,6 +70,22 @@ function formatTime(time: DateTime) {
|
||||||
margin-block-start: 0.5rem;
|
margin-block-start: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notice {
|
||||||
|
display: flex;
|
||||||
|
width: fit-content;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin-block: 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
border: 1px solid color-mix(in oklab, CanvasText, orange 50%);
|
||||||
|
background-color: color-mix(in oklab, Canvas, orange 40%);
|
||||||
|
}
|
||||||
|
.noticeIcon {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-self: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding-inline: 0.2em;
|
padding-inline: 0.2em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<template>
|
<template>
|
||||||
<section class="shift">
|
<section class="shift">
|
||||||
<h3>{{ shift.name }}</h3>
|
<h3>{{ shift.name }}</h3>
|
||||||
<p>{{ shift.description ?? "No description provided" }}</p>
|
<p class="preWrap">{{ shift.description ?? "No description provided" }}</p>
|
||||||
|
|
||||||
<h4>Timeslots</h4>
|
<h4>Timeslots</h4>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
|
@ -15,9 +15,14 @@
|
||||||
:key="index"
|
:key="index"
|
||||||
:class="type"
|
:class="type"
|
||||||
>
|
>
|
||||||
|
<div class="symbol">
|
||||||
|
{{ { "removed": "- ", "added": "+ "}[type] }}
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
{{ text }}
|
{{ text }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
@ -33,20 +38,23 @@ defineProps<{
|
||||||
grid-template-columns: 5rem 1fr;
|
grid-template-columns: 5rem 1fr;
|
||||||
column-gap: 1rem;
|
column-gap: 1rem;
|
||||||
}
|
}
|
||||||
.removed {
|
:is(.removed, .added) {
|
||||||
|
display: flex;
|
||||||
grid-column: 2 / 2;
|
grid-column: 2 / 2;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
:is(.removed, .added) .symbol {
|
||||||
|
display: block;
|
||||||
|
font-family: monospace;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.removed {
|
||||||
color: color-mix(in srgb, CanvasText, red 40%);
|
color: color-mix(in srgb, CanvasText, red 40%);
|
||||||
}
|
}
|
||||||
.removed::before {
|
.removed .content{
|
||||||
content: "- ";
|
text-decoration-line: line-through;
|
||||||
font-family: monospace;
|
|
||||||
}
|
}
|
||||||
.added {
|
.added {
|
||||||
grid-column: 2 / 2;
|
|
||||||
color: color-mix(in srgb, CanvasText, green 40%);
|
color: color-mix(in srgb, CanvasText, green 40%);
|
||||||
}
|
}
|
||||||
.added::before {
|
|
||||||
content: "+ ";
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -318,6 +318,7 @@ function newEventSlot(options: { start?: DateTime, end?: DateTime } = {}) {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
new Set(newEventLocationIds.value),
|
new Set(newEventLocationIds.value),
|
||||||
|
false,
|
||||||
new Set(),
|
new Set(),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
<th>id</th>
|
<th>id</th>
|
||||||
<th>name</th>
|
<th>name</th>
|
||||||
<th>host</th>
|
<th>host</th>
|
||||||
|
<th>notice</th>
|
||||||
<th>description</th>
|
<th>description</th>
|
||||||
<th>p</th>
|
<th>p</th>
|
||||||
<th>s</th>
|
<th>s</th>
|
||||||
|
@ -39,11 +40,18 @@
|
||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<textarea
|
||||||
type="text"
|
rows="1"
|
||||||
|
:disabled="!canEdit(event)"
|
||||||
|
v-model="event.notice"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<textarea
|
||||||
|
rows="1"
|
||||||
:disabled="!canEdit(event)"
|
:disabled="!canEdit(event)"
|
||||||
v-model="event.description"
|
v-model="event.description"
|
||||||
>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
|
@ -78,9 +86,21 @@
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
v-model="newEventDescription"
|
v-model="newEventHost"
|
||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<textarea
|
||||||
|
rows="1"
|
||||||
|
v-model="newEventNotice"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<textarea
|
||||||
|
rows="1"
|
||||||
|
v-model="newEventDescription"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
@ -110,7 +130,8 @@
|
||||||
<td>{{ event.id }}</td>
|
<td>{{ event.id }}</td>
|
||||||
<td>{{ event.name }}</td>
|
<td>{{ event.name }}</td>
|
||||||
<td>{{ event.host }}</td>
|
<td>{{ event.host }}</td>
|
||||||
<td>{{ event.description }}</td>
|
<td class="preWrap">{{ event.notice }}</td>
|
||||||
|
<td class="preWrap">{{ 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>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -136,6 +157,8 @@ function canEdit(event: ClientScheduleEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const newEventName = ref("");
|
const newEventName = ref("");
|
||||||
|
const newEventHost = ref("");
|
||||||
|
const newEventNotice = ref("");
|
||||||
const newEventDescription = ref("");
|
const newEventDescription = ref("");
|
||||||
const newEventPublic = ref(false);
|
const newEventPublic = ref(false);
|
||||||
function eventExists(name: string) {
|
function eventExists(name: string) {
|
||||||
|
@ -156,8 +179,9 @@ function newEvent() {
|
||||||
schedule.value.nextClientId--,
|
schedule.value.nextClientId--,
|
||||||
newEventName.value,
|
newEventName.value,
|
||||||
!newEventPublic.value,
|
!newEventPublic.value,
|
||||||
"",
|
newEventHost.value,
|
||||||
false,
|
false,
|
||||||
|
newEventNotice.value,
|
||||||
newEventDescription.value,
|
newEventDescription.value,
|
||||||
0,
|
0,
|
||||||
new Set(),
|
new Set(),
|
||||||
|
@ -165,6 +189,8 @@ function newEvent() {
|
||||||
);
|
);
|
||||||
schedule.value.events.add(event);
|
schedule.value.events.add(event);
|
||||||
newEventName.value = "";
|
newEventName.value = "";
|
||||||
|
newEventHost.value = "";
|
||||||
|
newEventNotice.value = "";
|
||||||
newEventDescription.value = "";
|
newEventDescription.value = "";
|
||||||
newEventPublic.value = false;
|
newEventPublic.value = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,10 +28,10 @@
|
||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<textarea
|
||||||
type="text"
|
rows="1"
|
||||||
v-model="location.description"
|
v-model="location.description"
|
||||||
>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<td>{{ location.id }}</td>
|
<td>{{ location.id }}</td>
|
||||||
<td>{{ location.name }}</td>
|
<td>{{ location.name }}</td>
|
||||||
<td>{{ location.description }}</td>
|
<td class="preWrap">{{ location.description }}</td>
|
||||||
</template>
|
</template>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if='edit'>
|
<tr v-if='edit'>
|
||||||
|
@ -63,10 +63,10 @@
|
||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<textarea
|
||||||
type="text"
|
rows="1"
|
||||||
v-model="newLocationDescription"
|
v-model="newLocationDescription"
|
||||||
>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td colspan="1">
|
<td colspan="1">
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -28,10 +28,10 @@
|
||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<textarea
|
||||||
type="text"
|
rows="1"
|
||||||
v-model="role.description"
|
v-model="role.description"
|
||||||
>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
|
@ -55,10 +55,10 @@
|
||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<textarea
|
||||||
type="text"
|
rows="1"
|
||||||
v-model="newRoleDescription"
|
v-model="newRoleDescription"
|
||||||
>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
|
@ -80,7 +80,7 @@
|
||||||
>
|
>
|
||||||
<td>{{ role.id }}</td>
|
<td>{{ role.id }}</td>
|
||||||
<td>{{ role.name }}</td>
|
<td>{{ role.name }}</td>
|
||||||
<td>{{ role.description }}</td>
|
<td class="preWrap">{{ role.description }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -37,10 +37,10 @@
|
||||||
</td>
|
</td>
|
||||||
<td>{{ shift.slots.size ? shift.slots.size : "" }}</td>
|
<td>{{ shift.slots.size ? shift.slots.size : "" }}</td>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<textarea
|
||||||
type="text"
|
rows="1"
|
||||||
v-model="shift.description"
|
v-model="shift.description"
|
||||||
>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
|
@ -71,10 +71,10 @@
|
||||||
</td>
|
</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<textarea
|
||||||
type="text"
|
rows="1"
|
||||||
v-model="newShiftDescription"
|
v-model="newShiftDescription"
|
||||||
>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
|
@ -98,7 +98,7 @@
|
||||||
<td>{{ shift.name }}</td>
|
<td>{{ shift.name }}</td>
|
||||||
<td>{{ shift.roleId }}</td>
|
<td>{{ shift.roleId }}</td>
|
||||||
<td>{{ shift.slots.size ? shift.slots.size : "" }}</td>
|
<td>{{ shift.slots.size ? shift.slots.size : "" }}</td>
|
||||||
<td>{{ shift.description }}</td>
|
<td class="preWrap">{{ shift.description }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -17,7 +17,12 @@
|
||||||
<tr v-for="user in usersStore.users.values()">
|
<tr v-for="user in usersStore.users.values()">
|
||||||
<td>{{ user.id }}</td>
|
<td>{{ user.id }}</td>
|
||||||
<td>
|
<td>
|
||||||
|
<NuxtLink :to="`/admin/users/${user.id}`">
|
||||||
|
<template v-if="user.name">
|
||||||
{{ user.name }}
|
{{ user.name }}
|
||||||
|
</template>
|
||||||
|
<i v-else>(empty)</i>
|
||||||
|
</NuxtLink>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<select
|
<select
|
||||||
|
@ -39,7 +44,7 @@
|
||||||
<button
|
<button
|
||||||
v-if="user.isModified()"
|
v-if="user.isModified()"
|
||||||
type="button"
|
type="button"
|
||||||
@click="saveUser(user);"
|
@click="usersStore.saveUser(user);"
|
||||||
>Save</button>
|
>Save</button>
|
||||||
<button
|
<button
|
||||||
v-if="user.isModified()"
|
v-if="user.isModified()"
|
||||||
|
@ -55,18 +60,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
useEventSource();
|
useEventSource();
|
||||||
const usersStore = useUsersStore();
|
const usersStore = useUsersStore();
|
||||||
|
|
||||||
async function saveUser(user: ClientUser) {
|
|
||||||
try {
|
|
||||||
await $fetch("/api/admin/user", {
|
|
||||||
method: "PATCH",
|
|
||||||
body: user.toApi(),
|
|
||||||
});
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error(err);
|
|
||||||
alert(err?.data?.message ?? err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -105,6 +105,7 @@
|
||||||
:class='{"event": cell.slot, "crew": cell.event?.crew }'
|
:class='{"event": cell.slot, "crew": cell.event?.crew }'
|
||||||
:title="cell.event?.name"
|
:title="cell.event?.name"
|
||||||
>
|
>
|
||||||
|
{{ cell.event?.notice ? "⚠️" : undefined }}
|
||||||
{{ cell.event?.name }}
|
{{ cell.event?.name }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
94
pages/admin/users/[id].vue
Normal file
94
pages/admin/users/[id].vue
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
<template>
|
||||||
|
<main v-if="userDetails.deleted">
|
||||||
|
<h1>Deleted user {{ id }}</h1>
|
||||||
|
</main>
|
||||||
|
<main v-else>
|
||||||
|
<h1>User {{ user.name }}</h1>
|
||||||
|
<dl>
|
||||||
|
<dt>
|
||||||
|
<label for="user-type">
|
||||||
|
Type
|
||||||
|
</label>
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
<select
|
||||||
|
v-if='user.type !== "anonymous"'
|
||||||
|
v-model="user.type"
|
||||||
|
>
|
||||||
|
<option value="regular">Regular</option>
|
||||||
|
<option value="crew">Crew</option>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
</select>
|
||||||
|
<template v-else>
|
||||||
|
{{ user.type }}
|
||||||
|
</template>
|
||||||
|
</dd>
|
||||||
|
<dt>Interested Events:</dt>
|
||||||
|
<dd>{{ userDetails.interestedEventIds }}</dd>
|
||||||
|
<dt>Interested Slots:</dt>
|
||||||
|
<dd>{{ userDetails.interestedEventSlotIds }}</dd>
|
||||||
|
<dt>Timezone:</dt>
|
||||||
|
<dd>{{ userDetails.timezone }}</dd>
|
||||||
|
<dt>Locale:</dt>
|
||||||
|
<dd>{{ userDetails.locale }}</dd>
|
||||||
|
</dl>
|
||||||
|
<button
|
||||||
|
:disabled="!user.isModified()"
|
||||||
|
type="button"
|
||||||
|
@click="usersStore.saveUser(user);"
|
||||||
|
>Save</button>
|
||||||
|
<button
|
||||||
|
:disabled="!user.isModified()"
|
||||||
|
type="button"
|
||||||
|
@click="user.discard()"
|
||||||
|
>Discard</button>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { ApiTombstone, ApiUserDetails } from '~/shared/types/api';
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: "Admin",
|
||||||
|
});
|
||||||
|
|
||||||
|
useEventSource();
|
||||||
|
const route = useRoute();
|
||||||
|
const usersStore = useUsersStore();
|
||||||
|
await usersStore.fetch();
|
||||||
|
|
||||||
|
const id = computed(() => {
|
||||||
|
const id = queryToNumber(route.params.id);
|
||||||
|
if (id === undefined) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: "Bad Request",
|
||||||
|
message: "User id required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
});
|
||||||
|
const user = computed(() => {
|
||||||
|
const user = usersStore.users.get(id.value);
|
||||||
|
if (user === undefined) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
statusMessage: "Not Found",
|
||||||
|
message: "User not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { pending, data, error } = await useFetch(() => `/api/users/${id.value}/details`);
|
||||||
|
const userDetails = data as Ref<ApiUserDetails | ApiTombstone>;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
dl {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
column-gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
34
server/api/users/[id]/details.get.ts
Normal file
34
server/api/users/[id]/details.get.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { z } from "zod/v4-mini";
|
||||||
|
import { readUsers } from "~/server/database";
|
||||||
|
import { serverUserToApiDetails } from "~/server/utils/user";
|
||||||
|
|
||||||
|
const integerStringSchema = z.pipe(
|
||||||
|
z.string().check(z.regex(/^\d+/)),
|
||||||
|
z.transform(Number)
|
||||||
|
);
|
||||||
|
|
||||||
|
const detailsSchema = z.object({
|
||||||
|
id: integerStringSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
await requireServerSessionWithAdmin(event);
|
||||||
|
const users = await readUsers();
|
||||||
|
const { success, error, data: params } = detailsSchema.safeParse(getRouterParams(event));
|
||||||
|
if (!success) {
|
||||||
|
throw createError({
|
||||||
|
status: 400,
|
||||||
|
statusText: "Bad Request",
|
||||||
|
message: z.prettifyError(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const user = users.find(user => user.id === params.id);
|
||||||
|
if (!user) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
statusMessage: "Not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverUserToApiDetails(user);
|
||||||
|
})
|
|
@ -9,7 +9,7 @@ const locations = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: "Stage",
|
name: "Stage",
|
||||||
description: "Inside the main building.",
|
description: "Inside the main building.\n\nMind the gap.",
|
||||||
updatedAt: "d-1 18:21",
|
updatedAt: "d-1 18:21",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -43,7 +43,8 @@ let eventId = 1;
|
||||||
const events = [
|
const events = [
|
||||||
{
|
{
|
||||||
name: "Arcade",
|
name: "Arcade",
|
||||||
description: "Play retro games!",
|
notice: "No food or drinks allowed!\n\nClosed drinking containers are okay.",
|
||||||
|
description: "Play retro games!\n\nWe have anything from C64 to PS5.",
|
||||||
slots: [
|
slots: [
|
||||||
"d1 12:00 4h clubhouse",
|
"d1 12:00 4h clubhouse",
|
||||||
"d2 12:00 4h clubhouse",
|
"d2 12:00 4h clubhouse",
|
||||||
|
@ -158,13 +159,14 @@ const events = [
|
||||||
const idMedic = 1;
|
const idMedic = 1;
|
||||||
const idSecurity = 2;
|
const idSecurity = 2;
|
||||||
const roles = [
|
const roles = [
|
||||||
{ id: idMedic, name: "Medic", updatedAt: "d-2 12:34" },
|
{ id: idMedic, name: "Medic", description: "Helping those in need.\n\nAsk lead if in doubt.", updatedAt: "d-2 12:34" },
|
||||||
{ id: idSecurity, name: "Security", updatedAt: "d-2 12:39" },
|
{ id: idSecurity, name: "Security", description: "Keeping the con safe", updatedAt: "d-2 12:39" },
|
||||||
]
|
]
|
||||||
|
|
||||||
const shifts = [
|
const shifts = [
|
||||||
{
|
{
|
||||||
name: "Medic Early",
|
name: "Medic Early",
|
||||||
|
description: "Not much happens early.\n\nPrefer strolling outside to be visible.",
|
||||||
roleId: idMedic,
|
roleId: idMedic,
|
||||||
slots: [
|
slots: [
|
||||||
"d1 12:00 4h",
|
"d1 12:00 4h",
|
||||||
|
@ -338,10 +340,11 @@ export function generateDemoSchedule(): ApiSchedule {
|
||||||
id: 111,
|
id: 111,
|
||||||
updatedAt: toIso(toDate(origin, "d-2", "10:01")),
|
updatedAt: toIso(toDate(origin, "d-2", "10:01")),
|
||||||
events: events.map(
|
events: events.map(
|
||||||
({ id, name, crew, description, slots }) => ({
|
({ id, name, crew, notice, description, slots }) => ({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
crew,
|
crew,
|
||||||
|
notice,
|
||||||
description,
|
description,
|
||||||
interested: eventCounts.get(id),
|
interested: eventCounts.get(id),
|
||||||
slots: slots.map(shorthand => toSlot(origin, shorthand, slotCounts, eventSlotIdToAssigned)),
|
slots: slots.map(shorthand => toSlot(origin, shorthand, slotCounts, eventSlotIdToAssigned)),
|
||||||
|
@ -357,16 +360,18 @@ export function generateDemoSchedule(): ApiSchedule {
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
roles: roles.map(
|
roles: roles.map(
|
||||||
({ id, name, updatedAt }) => ({
|
({ id, name, description, updatedAt }) => ({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
|
description,
|
||||||
updatedAt: toIso(toDate(origin, ...(updatedAt.split(" ")) as [string, string])),
|
updatedAt: toIso(toDate(origin, ...(updatedAt.split(" ")) as [string, string])),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
shifts: shifts.map(
|
shifts: shifts.map(
|
||||||
({ id, name, roleId, slots }) => ({
|
({ id, name, description, roleId, slots }) => ({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
|
description,
|
||||||
roleId,
|
roleId,
|
||||||
slots: slots.map(shorthand => toShift(origin, shorthand, shiftSlotIdToAssigned)),
|
slots: slots.map(shorthand => toShift(origin, shorthand, shiftSlotIdToAssigned)),
|
||||||
updatedAt: toIso(toDate(origin, "d-1", "13:23")),
|
updatedAt: toIso(toDate(origin, "d-1", "13:23")),
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
} from "~/server/database";
|
} from "~/server/database";
|
||||||
import { broadcastEvent } from "../streams";
|
import { broadcastEvent } from "../streams";
|
||||||
import type { ApiAuthenticationProvider, ApiSession } from "~/shared/types/api";
|
import type { ApiAuthenticationProvider, ApiSession } from "~/shared/types/api";
|
||||||
|
import { serverUserToApiAccount } from "./user";
|
||||||
|
|
||||||
async function removeSessionSubscription(sessionId: number) {
|
async function removeSessionSubscription(sessionId: number) {
|
||||||
const subscriptions = await readSubscriptions();
|
const subscriptions = await readSubscriptions();
|
||||||
|
@ -182,7 +183,7 @@ export async function serverSessionToApi(event: H3Event, session: ServerSession)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: session.id,
|
id: session.id,
|
||||||
account,
|
account: serverUserToApiAccount(account),
|
||||||
authenticationProvider: session.authenticationProvider,
|
authenticationProvider: session.authenticationProvider,
|
||||||
authenticationName: session.authenticationName,
|
authenticationName: session.authenticationName,
|
||||||
push,
|
push,
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
import type { ServerUser } from "~/server/database"
|
import type { ServerUser } from "~/server/database"
|
||||||
import type { ApiTombstone, ApiUser } from "~/shared/types/api";
|
import type { ApiAccount, ApiTombstone, ApiUser, ApiUserDetails } from "~/shared/types/api";
|
||||||
|
|
||||||
export function serverUserToApi(user: ServerUser): ApiUser | ApiTombstone {
|
export function serverUserToApi(user: ServerUser): ApiUser | ApiTombstone {
|
||||||
if (user.deleted) {
|
if (user.deleted) {
|
||||||
|
@ -20,3 +20,37 @@ export function serverUserToApi(user: ServerUser): ApiUser | ApiTombstone {
|
||||||
name: user.name,
|
name: user.name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function serverUserToApiDetails(user: ServerUser): ApiUserDetails | ApiTombstone {
|
||||||
|
if (user.deleted) {
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
updatedAt: user.updatedAt,
|
||||||
|
deleted: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
updatedAt: user.updatedAt,
|
||||||
|
interestedEventIds: user.interestedEventIds,
|
||||||
|
interestedEventSlotIds: user.interestedEventSlotIds,
|
||||||
|
timezone: user.timezone,
|
||||||
|
locale: user.locale,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serverUserToApiAccount(user?: ServerUser): ApiAccount | undefined {
|
||||||
|
if (!user || user.deleted) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
updatedAt: user.updatedAt,
|
||||||
|
type: user.type,
|
||||||
|
name: user.name,
|
||||||
|
interestedEventIds: user.interestedEventIds,
|
||||||
|
interestedEventSlotIds: user.interestedEventSlotIds,
|
||||||
|
timezone: user.timezone,
|
||||||
|
locale: user.locale,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -35,17 +35,7 @@ export const apiUserTypeSchema = z.union([
|
||||||
])
|
])
|
||||||
export type ApiUserType = z.infer<typeof apiUserTypeSchema>;
|
export type ApiUserType = z.infer<typeof apiUserTypeSchema>;
|
||||||
|
|
||||||
export interface ApiAccount {
|
export type ApiAccount = ApiUser & ApiUserDetails
|
||||||
id: Id,
|
|
||||||
updatedAt: string,
|
|
||||||
type: ApiUserType,
|
|
||||||
/** Name of the account. Not present on anonymous accounts */
|
|
||||||
name?: string,
|
|
||||||
interestedEventIds?: number[],
|
|
||||||
interestedEventSlotIds?: number[],
|
|
||||||
timezone?: string,
|
|
||||||
locale?: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const apiAccountPatchSchema = z.object({
|
export const apiAccountPatchSchema = z.object({
|
||||||
name: z.optional(z.string()),
|
name: z.optional(z.string()),
|
||||||
|
@ -91,6 +81,7 @@ export const apiScheduleEventSlotSchema = z.object({
|
||||||
start: z.string(),
|
start: z.string(),
|
||||||
end: z.string(),
|
end: z.string(),
|
||||||
locationIds: z.array(idSchema),
|
locationIds: z.array(idSchema),
|
||||||
|
cancelled: z.optional(z.boolean()),
|
||||||
assigned: z.optional(z.array(z.number())),
|
assigned: z.optional(z.array(z.number())),
|
||||||
interested: z.optional(z.number()),
|
interested: z.optional(z.number()),
|
||||||
});
|
});
|
||||||
|
@ -101,6 +92,7 @@ export const apiScheduleEventSchema = defineApiEntity({
|
||||||
crew: z.optional(z.boolean()),
|
crew: z.optional(z.boolean()),
|
||||||
host: z.optional(z.string()),
|
host: z.optional(z.string()),
|
||||||
cancelled: z.optional(z.boolean()),
|
cancelled: z.optional(z.boolean()),
|
||||||
|
notice: z.optional(z.string()),
|
||||||
description: z.optional(z.string()),
|
description: z.optional(z.string()),
|
||||||
interested: z.optional(z.number()),
|
interested: z.optional(z.number()),
|
||||||
slots: z.array(apiScheduleEventSlotSchema),
|
slots: z.array(apiScheduleEventSlotSchema),
|
||||||
|
@ -151,6 +143,16 @@ export const apiUserPatchSchema = z.object({
|
||||||
});
|
});
|
||||||
export type ApiUserPatch = z.infer<typeof apiUserPatchSchema>;
|
export type ApiUserPatch = z.infer<typeof apiUserPatchSchema>;
|
||||||
|
|
||||||
|
export interface ApiUserDetails {
|
||||||
|
id: Id,
|
||||||
|
updatedAt: string,
|
||||||
|
deleted?: false,
|
||||||
|
interestedEventIds?: number[],
|
||||||
|
interestedEventSlotIds?: number[],
|
||||||
|
timezone?: string,
|
||||||
|
locale?: string,
|
||||||
|
}
|
||||||
|
|
||||||
export interface ApiAccountUpdate {
|
export interface ApiAccountUpdate {
|
||||||
type: "account-update",
|
type: "account-update",
|
||||||
data: ApiAccount,
|
data: ApiAccount,
|
||||||
|
|
|
@ -62,6 +62,17 @@ export const useUsersStore = defineStore("users", () => {
|
||||||
state.fetched.value = false;
|
state.fetched.value = false;
|
||||||
await actions.fetch();
|
await actions.fetch();
|
||||||
},
|
},
|
||||||
|
async saveUser(user: ClientUser) {
|
||||||
|
try {
|
||||||
|
await $fetch("/api/admin/user", {
|
||||||
|
method: "PATCH",
|
||||||
|
body: user.toApi(),
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err);
|
||||||
|
alert(err?.data?.message ?? err.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
appEventSource?.addEventListener("update", (event) => {
|
appEventSource?.addEventListener("update", (event) => {
|
||||||
|
|
|
@ -21,15 +21,15 @@ function fixtureClientSchedule(multiSlot = false) {
|
||||||
|
|
||||||
const events = [
|
const events = [
|
||||||
new ClientScheduleEvent(
|
new ClientScheduleEvent(
|
||||||
1, now, false, "Up", false, "", false, "What's Up?", 0, new Set(multiSlot ? [1, 2] : [1]),
|
1, now, false, "Up", false, "", false, "", "What's Up?", 0, new Set(multiSlot ? [1, 2] : [1]),
|
||||||
),
|
),
|
||||||
new ClientScheduleEvent(
|
new ClientScheduleEvent(
|
||||||
2, now, false, "Down", false, "", false, "", 0, new Set(multiSlot ? [] : [2]),
|
2, now, false, "Down", false, "", false, "", "", 0, new Set(multiSlot ? [] : [2]),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
const eventSlots = idMap([
|
const eventSlots = idMap([
|
||||||
new ClientScheduleEventSlot(1, false, false, 1, now, now.plus({ hours: 1 }), new Set([left.id]), new Set(), 0),
|
new ClientScheduleEventSlot(1, false, false, 1, now, now.plus({ hours: 1 }), new Set([left.id]), false, new Set(), 0),
|
||||||
new ClientScheduleEventSlot(2, false, false, multiSlot ? 1 : 2, now, now.plus({ hours: 2 }), new Set([right.id]), new Set(), 0),
|
new ClientScheduleEventSlot(2, false, false, multiSlot ? 1 : 2, now, now.plus({ hours: 2 }), new Set([right.id]), false, new Set(), 0),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const red = new ClientScheduleRole(1, now, false, "Red", "Is a color.");
|
const red = new ClientScheduleRole(1, now, false, "Red", "Is a color.");
|
||||||
|
@ -174,7 +174,7 @@ describe("class ClientSchedule", () => {
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"event",
|
"event",
|
||||||
(schedule) => ClientScheduleEvent.create(schedule, 3, "New location", false, "", false, "", 0, new Set(), { zone, locale })
|
(schedule) => ClientScheduleEvent.create(schedule, 3, "New location", false, "", false, "", "", 0, new Set(), { zone, locale })
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"role",
|
"role",
|
||||||
|
@ -277,6 +277,7 @@ describe("class ClientSchedule", () => {
|
||||||
now,
|
now,
|
||||||
now.plus({ hours: 3 }),
|
now.plus({ hours: 3 }),
|
||||||
new Set(),
|
new Set(),
|
||||||
|
false,
|
||||||
new Set(),
|
new Set(),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
|
@ -136,6 +136,7 @@ export class ClientScheduleEvent extends ClientEntity<ApiScheduleEvent> {
|
||||||
serverCrew: boolean;
|
serverCrew: boolean;
|
||||||
serverHost: string;
|
serverHost: string;
|
||||||
serverCancelled: boolean;
|
serverCancelled: boolean;
|
||||||
|
serverNotice: string;
|
||||||
serverDescription: string;
|
serverDescription: string;
|
||||||
serverInterested: number;
|
serverInterested: number;
|
||||||
serverSlotIds: Set<Id>;
|
serverSlotIds: Set<Id>;
|
||||||
|
@ -148,6 +149,7 @@ export class ClientScheduleEvent extends ClientEntity<ApiScheduleEvent> {
|
||||||
public crew: boolean,
|
public crew: boolean,
|
||||||
public host: string,
|
public host: string,
|
||||||
public cancelled: boolean,
|
public cancelled: boolean,
|
||||||
|
public notice: string,
|
||||||
public description: string,
|
public description: string,
|
||||||
public interested: number,
|
public interested: number,
|
||||||
public slotIds: Set<Id>,
|
public slotIds: Set<Id>,
|
||||||
|
@ -157,6 +159,7 @@ export class ClientScheduleEvent extends ClientEntity<ApiScheduleEvent> {
|
||||||
this.serverCrew = crew;
|
this.serverCrew = crew;
|
||||||
this.serverHost = host;
|
this.serverHost = host;
|
||||||
this.serverCancelled = cancelled;
|
this.serverCancelled = cancelled;
|
||||||
|
this.serverNotice = notice;
|
||||||
this.serverDescription = description;
|
this.serverDescription = description;
|
||||||
this.serverInterested = interested;
|
this.serverInterested = interested;
|
||||||
this.serverSlotIds = new Set(slotIds);
|
this.serverSlotIds = new Set(slotIds);
|
||||||
|
@ -173,6 +176,7 @@ export class ClientScheduleEvent extends ClientEntity<ApiScheduleEvent> {
|
||||||
|| this.crew !== this.serverCrew
|
|| this.crew !== this.serverCrew
|
||||||
|| this.host !== this.serverHost
|
|| this.host !== this.serverHost
|
||||||
|| this.cancelled !== this.serverCancelled
|
|| this.cancelled !== this.serverCancelled
|
||||||
|
|| this.notice !== this.serverNotice
|
||||||
|| this.description !== this.serverDescription
|
|| this.description !== this.serverDescription
|
||||||
|| this.interested !== this.serverInterested
|
|| this.interested !== this.serverInterested
|
||||||
|| !setEquals(this.slotIds, this.serverSlotIds)
|
|| !setEquals(this.slotIds, this.serverSlotIds)
|
||||||
|
@ -190,6 +194,7 @@ export class ClientScheduleEvent extends ClientEntity<ApiScheduleEvent> {
|
||||||
this.crew = this.serverCrew;
|
this.crew = this.serverCrew;
|
||||||
this.host = this.serverHost;
|
this.host = this.serverHost;
|
||||||
this.cancelled = this.serverCancelled;
|
this.cancelled = this.serverCancelled;
|
||||||
|
this.notice = this.serverNotice;
|
||||||
this.description = this.serverDescription;
|
this.description = this.serverDescription;
|
||||||
this.interested = this.serverInterested;
|
this.interested = this.serverInterested;
|
||||||
for (const id of this.serverSlotIds) {
|
for (const id of this.serverSlotIds) {
|
||||||
|
@ -208,6 +213,7 @@ export class ClientScheduleEvent extends ClientEntity<ApiScheduleEvent> {
|
||||||
crew: boolean,
|
crew: boolean,
|
||||||
host: string,
|
host: string,
|
||||||
cancelled: boolean,
|
cancelled: boolean,
|
||||||
|
notice: string,
|
||||||
description: string,
|
description: string,
|
||||||
interested: number,
|
interested: number,
|
||||||
slotIds: Set<Id>,
|
slotIds: Set<Id>,
|
||||||
|
@ -221,6 +227,7 @@ export class ClientScheduleEvent extends ClientEntity<ApiScheduleEvent> {
|
||||||
crew,
|
crew,
|
||||||
host,
|
host,
|
||||||
cancelled,
|
cancelled,
|
||||||
|
notice,
|
||||||
description,
|
description,
|
||||||
interested,
|
interested,
|
||||||
slotIds,
|
slotIds,
|
||||||
|
@ -241,6 +248,7 @@ export class ClientScheduleEvent extends ClientEntity<ApiScheduleEvent> {
|
||||||
api.crew ?? false,
|
api.crew ?? false,
|
||||||
api.host ?? "",
|
api.host ?? "",
|
||||||
api.cancelled ?? false,
|
api.cancelled ?? false,
|
||||||
|
api.notice ?? "",
|
||||||
api.description ?? "",
|
api.description ?? "",
|
||||||
api.interested ?? 0,
|
api.interested ?? 0,
|
||||||
new Set(api.slots.map(slot => slot.id)),
|
new Set(api.slots.map(slot => slot.id)),
|
||||||
|
@ -258,6 +266,7 @@ export class ClientScheduleEvent extends ClientEntity<ApiScheduleEvent> {
|
||||||
this.serverCrew = api.crew ?? false;
|
this.serverCrew = api.crew ?? false;
|
||||||
this.serverHost = api.host ?? "";
|
this.serverHost = api.host ?? "";
|
||||||
this.serverCancelled = api.cancelled ?? false;
|
this.serverCancelled = api.cancelled ?? false;
|
||||||
|
this.serverNotice = api.notice ?? "";
|
||||||
this.serverDescription = api.description ?? "";
|
this.serverDescription = api.description ?? "";
|
||||||
this.serverInterested = api.interested ?? 0;
|
this.serverInterested = api.interested ?? 0;
|
||||||
this.serverSlotIds = new Set(api.slots.map(slot => slot.id));
|
this.serverSlotIds = new Set(api.slots.map(slot => slot.id));
|
||||||
|
@ -281,6 +290,7 @@ export class ClientScheduleEvent extends ClientEntity<ApiScheduleEvent> {
|
||||||
crew: this.crew || undefined,
|
crew: this.crew || undefined,
|
||||||
host: this.host || undefined,
|
host: this.host || undefined,
|
||||||
cancelled: this.cancelled || undefined,
|
cancelled: this.cancelled || undefined,
|
||||||
|
notice: this.notice || undefined,
|
||||||
description: this.description || undefined,
|
description: this.description || undefined,
|
||||||
interested: this.interested || undefined,
|
interested: this.interested || undefined,
|
||||||
slots: [...this.slots.values()].filter(slot => !slot.deleted).map(slot => slot.toApi()),
|
slots: [...this.slots.values()].filter(slot => !slot.deleted).map(slot => slot.toApi()),
|
||||||
|
@ -295,6 +305,7 @@ export class ClientScheduleEventSlot {
|
||||||
serverStart: DateTime;
|
serverStart: DateTime;
|
||||||
serverEnd: DateTime;
|
serverEnd: DateTime;
|
||||||
serverLocationIds: Set<Id>;
|
serverLocationIds: Set<Id>;
|
||||||
|
serverCancelled: boolean;
|
||||||
serverAssigned: Set<Id>;
|
serverAssigned: Set<Id>;
|
||||||
serverInterested: number;
|
serverInterested: number;
|
||||||
|
|
||||||
|
@ -306,6 +317,7 @@ export class ClientScheduleEventSlot {
|
||||||
public start: DateTime,
|
public start: DateTime,
|
||||||
public end: DateTime,
|
public end: DateTime,
|
||||||
public locationIds: Set<Id>,
|
public locationIds: Set<Id>,
|
||||||
|
public cancelled: boolean,
|
||||||
public assigned: Set<Id>,
|
public assigned: Set<Id>,
|
||||||
public interested: number,
|
public interested: number,
|
||||||
) {
|
) {
|
||||||
|
@ -314,6 +326,7 @@ export class ClientScheduleEventSlot {
|
||||||
this.serverStart = start;
|
this.serverStart = start;
|
||||||
this.serverEnd = end;
|
this.serverEnd = end;
|
||||||
this.serverLocationIds = new Set(locationIds);
|
this.serverLocationIds = new Set(locationIds);
|
||||||
|
this.serverCancelled = cancelled;
|
||||||
this.serverAssigned = new Set(assigned);
|
this.serverAssigned = new Set(assigned);
|
||||||
this.serverInterested = interested;
|
this.serverInterested = interested;
|
||||||
}
|
}
|
||||||
|
@ -327,6 +340,7 @@ export class ClientScheduleEventSlot {
|
||||||
|| this.start.toMillis() !== this.serverStart.toMillis()
|
|| this.start.toMillis() !== this.serverStart.toMillis()
|
||||||
|| this.end.toMillis() !== this.serverEnd.toMillis()
|
|| this.end.toMillis() !== this.serverEnd.toMillis()
|
||||||
|| !setEquals(this.locationIds, this.serverLocationIds)
|
|| !setEquals(this.locationIds, this.serverLocationIds)
|
||||||
|
|| this.cancelled !== this.serverCancelled
|
||||||
|| !setEquals(this.assigned, this.serverAssigned)
|
|| !setEquals(this.assigned, this.serverAssigned)
|
||||||
|| this.interested !== this.serverInterested
|
|| this.interested !== this.serverInterested
|
||||||
);
|
);
|
||||||
|
@ -349,6 +363,7 @@ export class ClientScheduleEventSlot {
|
||||||
this.start = this.serverStart;
|
this.start = this.serverStart;
|
||||||
this.end = this.serverEnd;
|
this.end = this.serverEnd;
|
||||||
this.locationIds = new Set(this.serverLocationIds);
|
this.locationIds = new Set(this.serverLocationIds);
|
||||||
|
this.cancelled = this.serverCancelled;
|
||||||
this.assigned = new Set(this.serverAssigned);
|
this.assigned = new Set(this.serverAssigned);
|
||||||
this.interested = this.serverInterested;
|
this.interested = this.serverInterested;
|
||||||
}
|
}
|
||||||
|
@ -360,6 +375,7 @@ export class ClientScheduleEventSlot {
|
||||||
start: DateTime,
|
start: DateTime,
|
||||||
end: DateTime,
|
end: DateTime,
|
||||||
locationIds: Set<Id>,
|
locationIds: Set<Id>,
|
||||||
|
cancelled: boolean,
|
||||||
assigned: Set<Id>,
|
assigned: Set<Id>,
|
||||||
interested: number,
|
interested: number,
|
||||||
) {
|
) {
|
||||||
|
@ -371,6 +387,7 @@ export class ClientScheduleEventSlot {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
locationIds,
|
locationIds,
|
||||||
|
cancelled,
|
||||||
assigned,
|
assigned,
|
||||||
interested,
|
interested,
|
||||||
);
|
);
|
||||||
|
@ -391,6 +408,7 @@ export class ClientScheduleEventSlot {
|
||||||
DateTime.fromISO(api.start, opts),
|
DateTime.fromISO(api.start, opts),
|
||||||
DateTime.fromISO(api.end, opts),
|
DateTime.fromISO(api.end, opts),
|
||||||
new Set(api.locationIds),
|
new Set(api.locationIds),
|
||||||
|
api.cancelled ?? false,
|
||||||
new Set(api.assigned),
|
new Set(api.assigned),
|
||||||
api.interested ?? 0,
|
api.interested ?? 0,
|
||||||
);
|
);
|
||||||
|
@ -408,6 +426,7 @@ export class ClientScheduleEventSlot {
|
||||||
this.serverStart = DateTime.fromISO(api.start, opts);
|
this.serverStart = DateTime.fromISO(api.start, opts);
|
||||||
this.serverEnd = DateTime.fromISO(api.end, opts);
|
this.serverEnd = DateTime.fromISO(api.end, opts);
|
||||||
this.serverLocationIds = new Set(api.locationIds);
|
this.serverLocationIds = new Set(api.locationIds);
|
||||||
|
this.serverCancelled = api.cancelled ?? false;
|
||||||
this.serverAssigned = new Set(api.assigned);
|
this.serverAssigned = new Set(api.assigned);
|
||||||
this.serverInterested = api.interested ?? 0;
|
this.serverInterested = api.interested ?? 0;
|
||||||
if (!wasModified || !this.isModified()) {
|
if (!wasModified || !this.isModified()) {
|
||||||
|
@ -424,6 +443,7 @@ export class ClientScheduleEventSlot {
|
||||||
start: toIso(this.start),
|
start: toIso(this.start),
|
||||||
end: toIso(this.end),
|
end: toIso(this.end),
|
||||||
locationIds: [...this.locationIds],
|
locationIds: [...this.locationIds],
|
||||||
|
cancelled: this.cancelled || undefined,
|
||||||
assigned: this.assigned.size ? [...this.assigned] : undefined,
|
assigned: this.assigned.size ? [...this.assigned] : undefined,
|
||||||
interested: this.interested || undefined,
|
interested: this.interested || undefined,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue