Compare commits
No commits in common. "732566a29c08d30101a343f79071bdaef4bfd901" and "52973ffa9aaebe07abcddd43c7d8e00d00ef1302" have entirely different histories.
732566a29c
...
52973ffa9a
23 changed files with 80 additions and 363 deletions
|
@ -2,7 +2,7 @@
|
|||
# SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no>
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Based on Next.js's docker image example
|
||||
FROM node:22-slim AS base
|
||||
FROM node:22 AS base
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM base AS deps
|
||||
|
|
|
@ -83,7 +83,3 @@ label>* {
|
|||
label + label {
|
||||
margin-block-start: 0.5rem;
|
||||
}
|
||||
|
||||
.preWrap {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
|
|
@ -8,15 +8,7 @@
|
|||
<p v-if=event.host>
|
||||
Host: {{ event.host }}
|
||||
</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>{{ event.description ?? "No description provided" }}</p>
|
||||
<p v-if="event.interested">
|
||||
{{ event.interested }} interested
|
||||
</p>
|
||||
|
@ -87,22 +79,6 @@ async function toggle(type: "event" | "slot", id: number, slotIds?: number[]) {
|
|||
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 {
|
||||
padding-inline: 0.2em;
|
||||
}
|
||||
|
|
|
@ -13,15 +13,7 @@
|
|||
<p v-if=event?.host>
|
||||
Host: {{ event.host }}
|
||||
</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>{{ event?.description ?? "No description provided" }}</p>
|
||||
<p v-if="locations.length">
|
||||
At {{ locations.map(location => location?.name ?? "unknown").join(" + ") }}
|
||||
</p>
|
||||
|
@ -70,22 +62,6 @@ function formatTime(time: DateTime) {
|
|||
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 {
|
||||
padding-inline: 0.2em;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<template>
|
||||
<section class="shift">
|
||||
<h3>{{ shift.name }}</h3>
|
||||
<p class="preWrap">{{ shift.description ?? "No description provided" }}</p>
|
||||
<p>{{ shift.description ?? "No description provided" }}</p>
|
||||
|
||||
<h4>Timeslots</h4>
|
||||
<ul>
|
||||
|
|
|
@ -15,14 +15,9 @@
|
|||
:key="index"
|
||||
:class="type"
|
||||
>
|
||||
<div class="symbol">
|
||||
{{ { "removed": "- ", "added": "+ "}[type] }}
|
||||
</div>
|
||||
<div class="content">
|
||||
{{ text }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -38,23 +33,20 @@ defineProps<{
|
|||
grid-template-columns: 5rem 1fr;
|
||||
column-gap: 1rem;
|
||||
}
|
||||
:is(.removed, .added) {
|
||||
display: flex;
|
||||
grid-column: 2 / 2;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
:is(.removed, .added) .symbol {
|
||||
display: block;
|
||||
font-family: monospace;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.removed {
|
||||
grid-column: 2 / 2;
|
||||
color: color-mix(in srgb, CanvasText, red 40%);
|
||||
}
|
||||
.removed .content{
|
||||
text-decoration-line: line-through;
|
||||
.removed::before {
|
||||
content: "- ";
|
||||
font-family: monospace;
|
||||
}
|
||||
.added {
|
||||
grid-column: 2 / 2;
|
||||
color: color-mix(in srgb, CanvasText, green 40%);
|
||||
}
|
||||
.added::before {
|
||||
content: "+ ";
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -318,7 +318,6 @@ function newEventSlot(options: { start?: DateTime, end?: DateTime } = {}) {
|
|||
start,
|
||||
end,
|
||||
new Set(newEventLocationIds.value),
|
||||
false,
|
||||
new Set(),
|
||||
0,
|
||||
);
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
<th>id</th>
|
||||
<th>name</th>
|
||||
<th>host</th>
|
||||
<th>notice</th>
|
||||
<th>description</th>
|
||||
<th>p</th>
|
||||
<th>s</th>
|
||||
|
@ -40,18 +39,11 @@
|
|||
>
|
||||
</td>
|
||||
<td>
|
||||
<textarea
|
||||
rows="1"
|
||||
:disabled="!canEdit(event)"
|
||||
v-model="event.notice"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<textarea
|
||||
rows="1"
|
||||
<input
|
||||
type="text"
|
||||
:disabled="!canEdit(event)"
|
||||
v-model="event.description"
|
||||
/>
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
|
@ -86,20 +78,8 @@
|
|||
<td>
|
||||
<input
|
||||
type="text"
|
||||
v-model="newEventHost"
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<textarea
|
||||
rows="1"
|
||||
v-model="newEventNotice"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<textarea
|
||||
rows="1"
|
||||
v-model="newEventDescription"
|
||||
/>
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
|
@ -130,8 +110,7 @@
|
|||
<td>{{ event.id }}</td>
|
||||
<td>{{ event.name }}</td>
|
||||
<td>{{ event.host }}</td>
|
||||
<td class="preWrap">{{ event.notice }}</td>
|
||||
<td class="preWrap">{{ event.description }}</td>
|
||||
<td>{{ event.description }}</td>
|
||||
<td>{{ event.crew ? "" : "Yes"}}</td>
|
||||
<td>{{ event.slots.size ? event.slots.size : "" }}</td>
|
||||
</tr>
|
||||
|
@ -157,8 +136,6 @@ function canEdit(event: ClientScheduleEvent) {
|
|||
}
|
||||
|
||||
const newEventName = ref("");
|
||||
const newEventHost = ref("");
|
||||
const newEventNotice = ref("");
|
||||
const newEventDescription = ref("");
|
||||
const newEventPublic = ref(false);
|
||||
function eventExists(name: string) {
|
||||
|
@ -179,9 +156,8 @@ function newEvent() {
|
|||
schedule.value.nextClientId--,
|
||||
newEventName.value,
|
||||
!newEventPublic.value,
|
||||
newEventHost.value,
|
||||
"",
|
||||
false,
|
||||
newEventNotice.value,
|
||||
newEventDescription.value,
|
||||
0,
|
||||
new Set(),
|
||||
|
@ -189,8 +165,6 @@ function newEvent() {
|
|||
);
|
||||
schedule.value.events.add(event);
|
||||
newEventName.value = "";
|
||||
newEventHost.value = "";
|
||||
newEventNotice.value = "";
|
||||
newEventDescription.value = "";
|
||||
newEventPublic.value = false;
|
||||
}
|
||||
|
|
|
@ -28,10 +28,10 @@
|
|||
>
|
||||
</td>
|
||||
<td>
|
||||
<textarea
|
||||
rows="1"
|
||||
<input
|
||||
type="text"
|
||||
v-model="location.description"
|
||||
/>
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
|
@ -49,7 +49,7 @@
|
|||
<template v-else>
|
||||
<td>{{ location.id }}</td>
|
||||
<td>{{ location.name }}</td>
|
||||
<td class="preWrap">{{ location.description }}</td>
|
||||
<td>{{ location.description }}</td>
|
||||
</template>
|
||||
</tr>
|
||||
<tr v-if='edit'>
|
||||
|
@ -63,10 +63,10 @@
|
|||
>
|
||||
</td>
|
||||
<td>
|
||||
<textarea
|
||||
rows="1"
|
||||
<input
|
||||
type="text"
|
||||
v-model="newLocationDescription"
|
||||
/>
|
||||
>
|
||||
</td>
|
||||
<td colspan="1">
|
||||
<button
|
||||
|
|
|
@ -28,10 +28,10 @@
|
|||
>
|
||||
</td>
|
||||
<td>
|
||||
<textarea
|
||||
rows="1"
|
||||
<input
|
||||
type="text"
|
||||
v-model="role.description"
|
||||
/>
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
|
@ -55,10 +55,10 @@
|
|||
>
|
||||
</td>
|
||||
<td>
|
||||
<textarea
|
||||
rows="1"
|
||||
<input
|
||||
type="text"
|
||||
v-model="newRoleDescription"
|
||||
/>
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
|
@ -80,7 +80,7 @@
|
|||
>
|
||||
<td>{{ role.id }}</td>
|
||||
<td>{{ role.name }}</td>
|
||||
<td class="preWrap">{{ role.description }}</td>
|
||||
<td>{{ role.description }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
|
|
|
@ -37,10 +37,10 @@
|
|||
</td>
|
||||
<td>{{ shift.slots.size ? shift.slots.size : "" }}</td>
|
||||
<td>
|
||||
<textarea
|
||||
rows="1"
|
||||
<input
|
||||
type="text"
|
||||
v-model="shift.description"
|
||||
/>
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
|
@ -71,10 +71,10 @@
|
|||
</td>
|
||||
<td></td>
|
||||
<td>
|
||||
<textarea
|
||||
rows="1"
|
||||
<input
|
||||
type="text"
|
||||
v-model="newShiftDescription"
|
||||
/>
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
|
@ -98,7 +98,7 @@
|
|||
<td>{{ shift.name }}</td>
|
||||
<td>{{ shift.roleId }}</td>
|
||||
<td>{{ shift.slots.size ? shift.slots.size : "" }}</td>
|
||||
<td class="preWrap">{{ shift.description }}</td>
|
||||
<td>{{ shift.description }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
|
|
|
@ -17,12 +17,7 @@
|
|||
<tr v-for="user in usersStore.users.values()">
|
||||
<td>{{ user.id }}</td>
|
||||
<td>
|
||||
<NuxtLink :to="`/admin/users/${user.id}`">
|
||||
<template v-if="user.name">
|
||||
{{ user.name }}
|
||||
</template>
|
||||
<i v-else>(empty)</i>
|
||||
</NuxtLink>
|
||||
</td>
|
||||
<td>
|
||||
<select
|
||||
|
@ -44,7 +39,7 @@
|
|||
<button
|
||||
v-if="user.isModified()"
|
||||
type="button"
|
||||
@click="usersStore.saveUser(user);"
|
||||
@click="saveUser(user);"
|
||||
>Save</button>
|
||||
<button
|
||||
v-if="user.isModified()"
|
||||
|
@ -60,6 +55,18 @@
|
|||
<script lang="ts" setup>
|
||||
useEventSource();
|
||||
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>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -105,7 +105,6 @@
|
|||
:class='{"event": cell.slot, "crew": cell.event?.crew }'
|
||||
:title="cell.event?.name"
|
||||
>
|
||||
{{ cell.event?.notice ? "⚠️" : undefined }}
|
||||
{{ cell.event?.name }}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,94 +0,0 @@
|
|||
<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>
|
|
@ -1,34 +0,0 @@
|
|||
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,
|
||||
name: "Stage",
|
||||
description: "Inside the main building.\n\nMind the gap.",
|
||||
description: "Inside the main building.",
|
||||
updatedAt: "d-1 18:21",
|
||||
},
|
||||
{
|
||||
|
@ -43,8 +43,7 @@ let eventId = 1;
|
|||
const events = [
|
||||
{
|
||||
name: "Arcade",
|
||||
notice: "No food or drinks allowed!\n\nClosed drinking containers are okay.",
|
||||
description: "Play retro games!\n\nWe have anything from C64 to PS5.",
|
||||
description: "Play retro games!",
|
||||
slots: [
|
||||
"d1 12:00 4h clubhouse",
|
||||
"d2 12:00 4h clubhouse",
|
||||
|
@ -159,14 +158,13 @@ const events = [
|
|||
const idMedic = 1;
|
||||
const idSecurity = 2;
|
||||
const roles = [
|
||||
{ id: idMedic, name: "Medic", description: "Helping those in need.\n\nAsk lead if in doubt.", updatedAt: "d-2 12:34" },
|
||||
{ id: idSecurity, name: "Security", description: "Keeping the con safe", updatedAt: "d-2 12:39" },
|
||||
{ id: idMedic, name: "Medic", updatedAt: "d-2 12:34" },
|
||||
{ id: idSecurity, name: "Security", updatedAt: "d-2 12:39" },
|
||||
]
|
||||
|
||||
const shifts = [
|
||||
{
|
||||
name: "Medic Early",
|
||||
description: "Not much happens early.\n\nPrefer strolling outside to be visible.",
|
||||
roleId: idMedic,
|
||||
slots: [
|
||||
"d1 12:00 4h",
|
||||
|
@ -340,11 +338,10 @@ export function generateDemoSchedule(): ApiSchedule {
|
|||
id: 111,
|
||||
updatedAt: toIso(toDate(origin, "d-2", "10:01")),
|
||||
events: events.map(
|
||||
({ id, name, crew, notice, description, slots }) => ({
|
||||
({ id, name, crew, description, slots }) => ({
|
||||
id,
|
||||
name,
|
||||
crew,
|
||||
notice,
|
||||
description,
|
||||
interested: eventCounts.get(id),
|
||||
slots: slots.map(shorthand => toSlot(origin, shorthand, slotCounts, eventSlotIdToAssigned)),
|
||||
|
@ -360,18 +357,16 @@ export function generateDemoSchedule(): ApiSchedule {
|
|||
})
|
||||
),
|
||||
roles: roles.map(
|
||||
({ id, name, description, updatedAt }) => ({
|
||||
({ id, name, updatedAt }) => ({
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
updatedAt: toIso(toDate(origin, ...(updatedAt.split(" ")) as [string, string])),
|
||||
})
|
||||
),
|
||||
shifts: shifts.map(
|
||||
({ id, name, description, roleId, slots }) => ({
|
||||
({ id, name, roleId, slots }) => ({
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
roleId,
|
||||
slots: slots.map(shorthand => toShift(origin, shorthand, shiftSlotIdToAssigned)),
|
||||
updatedAt: toIso(toDate(origin, "d-1", "13:23")),
|
||||
|
|
|
@ -15,7 +15,6 @@ import {
|
|||
} from "~/server/database";
|
||||
import { broadcastEvent } from "../streams";
|
||||
import type { ApiAuthenticationProvider, ApiSession } from "~/shared/types/api";
|
||||
import { serverUserToApiAccount } from "./user";
|
||||
|
||||
async function removeSessionSubscription(sessionId: number) {
|
||||
const subscriptions = await readSubscriptions();
|
||||
|
@ -183,7 +182,7 @@ export async function serverSessionToApi(event: H3Event, session: ServerSession)
|
|||
|
||||
return {
|
||||
id: session.id,
|
||||
account: serverUserToApiAccount(account),
|
||||
account,
|
||||
authenticationProvider: session.authenticationProvider,
|
||||
authenticationName: session.authenticationName,
|
||||
push,
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import type { ServerUser } from "~/server/database"
|
||||
import type { ApiAccount, ApiTombstone, ApiUser, ApiUserDetails } from "~/shared/types/api";
|
||||
import type { ApiTombstone, ApiUser } from "~/shared/types/api";
|
||||
|
||||
export function serverUserToApi(user: ServerUser): ApiUser | ApiTombstone {
|
||||
if (user.deleted) {
|
||||
|
@ -20,37 +20,3 @@ export function serverUserToApi(user: ServerUser): ApiUser | ApiTombstone {
|
|||
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,7 +35,17 @@ export const apiUserTypeSchema = z.union([
|
|||
])
|
||||
export type ApiUserType = z.infer<typeof apiUserTypeSchema>;
|
||||
|
||||
export type ApiAccount = ApiUser & ApiUserDetails
|
||||
export interface ApiAccount {
|
||||
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({
|
||||
name: z.optional(z.string()),
|
||||
|
@ -81,7 +91,6 @@ export const apiScheduleEventSlotSchema = z.object({
|
|||
start: z.string(),
|
||||
end: z.string(),
|
||||
locationIds: z.array(idSchema),
|
||||
cancelled: z.optional(z.boolean()),
|
||||
assigned: z.optional(z.array(z.number())),
|
||||
interested: z.optional(z.number()),
|
||||
});
|
||||
|
@ -92,7 +101,6 @@ export const apiScheduleEventSchema = defineApiEntity({
|
|||
crew: z.optional(z.boolean()),
|
||||
host: z.optional(z.string()),
|
||||
cancelled: z.optional(z.boolean()),
|
||||
notice: z.optional(z.string()),
|
||||
description: z.optional(z.string()),
|
||||
interested: z.optional(z.number()),
|
||||
slots: z.array(apiScheduleEventSlotSchema),
|
||||
|
@ -143,16 +151,6 @@ export const apiUserPatchSchema = z.object({
|
|||
});
|
||||
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 {
|
||||
type: "account-update",
|
||||
data: ApiAccount,
|
||||
|
|
|
@ -62,17 +62,6 @@ export const useUsersStore = defineStore("users", () => {
|
|||
state.fetched.value = false;
|
||||
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) => {
|
||||
|
|
|
@ -21,15 +21,15 @@ function fixtureClientSchedule(multiSlot = false) {
|
|||
|
||||
const events = [
|
||||
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(
|
||||
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([
|
||||
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]), false, new Set(), 0),
|
||||
new ClientScheduleEventSlot(1, false, false, 1, now, now.plus({ hours: 1 }), new Set([left.id]), new Set(), 0),
|
||||
new ClientScheduleEventSlot(2, false, false, multiSlot ? 1 : 2, now, now.plus({ hours: 2 }), new Set([right.id]), new Set(), 0),
|
||||
]);
|
||||
|
||||
const red = new ClientScheduleRole(1, now, false, "Red", "Is a color.");
|
||||
|
@ -174,7 +174,7 @@ describe("class ClientSchedule", () => {
|
|||
],
|
||||
[
|
||||
"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",
|
||||
|
@ -277,7 +277,6 @@ describe("class ClientSchedule", () => {
|
|||
now,
|
||||
now.plus({ hours: 3 }),
|
||||
new Set(),
|
||||
false,
|
||||
new Set(),
|
||||
0,
|
||||
);
|
||||
|
|
|
@ -136,7 +136,6 @@ export class ClientScheduleEvent extends ClientEntity<ApiScheduleEvent> {
|
|||
serverCrew: boolean;
|
||||
serverHost: string;
|
||||
serverCancelled: boolean;
|
||||
serverNotice: string;
|
||||
serverDescription: string;
|
||||
serverInterested: number;
|
||||
serverSlotIds: Set<Id>;
|
||||
|
@ -149,7 +148,6 @@ export class ClientScheduleEvent extends ClientEntity<ApiScheduleEvent> {
|
|||
public crew: boolean,
|
||||
public host: string,
|
||||
public cancelled: boolean,
|
||||
public notice: string,
|
||||
public description: string,
|
||||
public interested: number,
|
||||
public slotIds: Set<Id>,
|
||||
|
@ -159,7 +157,6 @@ export class ClientScheduleEvent extends ClientEntity<ApiScheduleEvent> {
|
|||
this.serverCrew = crew;
|
||||
this.serverHost = host;
|
||||
this.serverCancelled = cancelled;
|
||||
this.serverNotice = notice;
|
||||
this.serverDescription = description;
|
||||
this.serverInterested = interested;
|
||||
this.serverSlotIds = new Set(slotIds);
|
||||
|
@ -176,7 +173,6 @@ export class ClientScheduleEvent extends ClientEntity<ApiScheduleEvent> {
|
|||
|| this.crew !== this.serverCrew
|
||||
|| this.host !== this.serverHost
|
||||
|| this.cancelled !== this.serverCancelled
|
||||
|| this.notice !== this.serverNotice
|
||||
|| this.description !== this.serverDescription
|
||||
|| this.interested !== this.serverInterested
|
||||
|| !setEquals(this.slotIds, this.serverSlotIds)
|
||||
|
@ -194,7 +190,6 @@ export class ClientScheduleEvent extends ClientEntity<ApiScheduleEvent> {
|
|||
this.crew = this.serverCrew;
|
||||
this.host = this.serverHost;
|
||||
this.cancelled = this.serverCancelled;
|
||||
this.notice = this.serverNotice;
|
||||
this.description = this.serverDescription;
|
||||
this.interested = this.serverInterested;
|
||||
for (const id of this.serverSlotIds) {
|
||||
|
@ -213,7 +208,6 @@ export class ClientScheduleEvent extends ClientEntity<ApiScheduleEvent> {
|
|||
crew: boolean,
|
||||
host: string,
|
||||
cancelled: boolean,
|
||||
notice: string,
|
||||
description: string,
|
||||
interested: number,
|
||||
slotIds: Set<Id>,
|
||||
|
@ -227,7 +221,6 @@ export class ClientScheduleEvent extends ClientEntity<ApiScheduleEvent> {
|
|||
crew,
|
||||
host,
|
||||
cancelled,
|
||||
notice,
|
||||
description,
|
||||
interested,
|
||||
slotIds,
|
||||
|
@ -248,7 +241,6 @@ export class ClientScheduleEvent extends ClientEntity<ApiScheduleEvent> {
|
|||
api.crew ?? false,
|
||||
api.host ?? "",
|
||||
api.cancelled ?? false,
|
||||
api.notice ?? "",
|
||||
api.description ?? "",
|
||||
api.interested ?? 0,
|
||||
new Set(api.slots.map(slot => slot.id)),
|
||||
|
@ -266,7 +258,6 @@ export class ClientScheduleEvent extends ClientEntity<ApiScheduleEvent> {
|
|||
this.serverCrew = api.crew ?? false;
|
||||
this.serverHost = api.host ?? "";
|
||||
this.serverCancelled = api.cancelled ?? false;
|
||||
this.serverNotice = api.notice ?? "";
|
||||
this.serverDescription = api.description ?? "";
|
||||
this.serverInterested = api.interested ?? 0;
|
||||
this.serverSlotIds = new Set(api.slots.map(slot => slot.id));
|
||||
|
@ -290,7 +281,6 @@ export class ClientScheduleEvent extends ClientEntity<ApiScheduleEvent> {
|
|||
crew: this.crew || undefined,
|
||||
host: this.host || undefined,
|
||||
cancelled: this.cancelled || undefined,
|
||||
notice: this.notice || undefined,
|
||||
description: this.description || undefined,
|
||||
interested: this.interested || undefined,
|
||||
slots: [...this.slots.values()].filter(slot => !slot.deleted).map(slot => slot.toApi()),
|
||||
|
@ -305,7 +295,6 @@ export class ClientScheduleEventSlot {
|
|||
serverStart: DateTime;
|
||||
serverEnd: DateTime;
|
||||
serverLocationIds: Set<Id>;
|
||||
serverCancelled: boolean;
|
||||
serverAssigned: Set<Id>;
|
||||
serverInterested: number;
|
||||
|
||||
|
@ -317,7 +306,6 @@ export class ClientScheduleEventSlot {
|
|||
public start: DateTime,
|
||||
public end: DateTime,
|
||||
public locationIds: Set<Id>,
|
||||
public cancelled: boolean,
|
||||
public assigned: Set<Id>,
|
||||
public interested: number,
|
||||
) {
|
||||
|
@ -326,7 +314,6 @@ export class ClientScheduleEventSlot {
|
|||
this.serverStart = start;
|
||||
this.serverEnd = end;
|
||||
this.serverLocationIds = new Set(locationIds);
|
||||
this.serverCancelled = cancelled;
|
||||
this.serverAssigned = new Set(assigned);
|
||||
this.serverInterested = interested;
|
||||
}
|
||||
|
@ -340,7 +327,6 @@ export class ClientScheduleEventSlot {
|
|||
|| this.start.toMillis() !== this.serverStart.toMillis()
|
||||
|| this.end.toMillis() !== this.serverEnd.toMillis()
|
||||
|| !setEquals(this.locationIds, this.serverLocationIds)
|
||||
|| this.cancelled !== this.serverCancelled
|
||||
|| !setEquals(this.assigned, this.serverAssigned)
|
||||
|| this.interested !== this.serverInterested
|
||||
);
|
||||
|
@ -363,7 +349,6 @@ export class ClientScheduleEventSlot {
|
|||
this.start = this.serverStart;
|
||||
this.end = this.serverEnd;
|
||||
this.locationIds = new Set(this.serverLocationIds);
|
||||
this.cancelled = this.serverCancelled;
|
||||
this.assigned = new Set(this.serverAssigned);
|
||||
this.interested = this.serverInterested;
|
||||
}
|
||||
|
@ -375,7 +360,6 @@ export class ClientScheduleEventSlot {
|
|||
start: DateTime,
|
||||
end: DateTime,
|
||||
locationIds: Set<Id>,
|
||||
cancelled: boolean,
|
||||
assigned: Set<Id>,
|
||||
interested: number,
|
||||
) {
|
||||
|
@ -387,7 +371,6 @@ export class ClientScheduleEventSlot {
|
|||
start,
|
||||
end,
|
||||
locationIds,
|
||||
cancelled,
|
||||
assigned,
|
||||
interested,
|
||||
);
|
||||
|
@ -408,7 +391,6 @@ export class ClientScheduleEventSlot {
|
|||
DateTime.fromISO(api.start, opts),
|
||||
DateTime.fromISO(api.end, opts),
|
||||
new Set(api.locationIds),
|
||||
api.cancelled ?? false,
|
||||
new Set(api.assigned),
|
||||
api.interested ?? 0,
|
||||
);
|
||||
|
@ -426,7 +408,6 @@ export class ClientScheduleEventSlot {
|
|||
this.serverStart = DateTime.fromISO(api.start, opts);
|
||||
this.serverEnd = DateTime.fromISO(api.end, opts);
|
||||
this.serverLocationIds = new Set(api.locationIds);
|
||||
this.serverCancelled = api.cancelled ?? false;
|
||||
this.serverAssigned = new Set(api.assigned);
|
||||
this.serverInterested = api.interested ?? 0;
|
||||
if (!wasModified || !this.isModified()) {
|
||||
|
@ -443,7 +424,6 @@ export class ClientScheduleEventSlot {
|
|||
start: toIso(this.start),
|
||||
end: toIso(this.end),
|
||||
locationIds: [...this.locationIds],
|
||||
cancelled: this.cancelled || undefined,
|
||||
assigned: this.assigned.size ? [...this.assigned] : undefined,
|
||||
interested: this.interested || undefined,
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue