diff --git a/apps/www/src/lib/components/portal/EventTable.svelte b/apps/www/src/lib/components/portal/EventTable.svelte
new file mode 100644
index 00000000..aa5ff3bb
--- /dev/null
+++ b/apps/www/src/lib/components/portal/EventTable.svelte
@@ -0,0 +1,222 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {#if filteredEvents.length === 0}
+
+ Ingen {activeTab === 'upcoming' ? 'kommende' : 'tidligere'} arrangementer å vise.
+
+ {:else}
+
+
+ {#if isMobile}
+
+
+
+
+ {:else}
+
+
+
+
+
+
+ {/if}
+
+
+ | Arrangement |
+ {#if isMobile}
+ Status |
+ {:else}
+ Dato |
+ Antall vakter |
+ Status |
+ |
+ {/if}
+
+
+
+ {#each filteredEvents as event (event.id)}
+ {@const status = getEventStatus(event)}
+
+ |
+
+
+ {event.name}
+ |
+ {#if isMobile}
+
+ {status}
+ |
+ {:else}
+
+ {formatDate(event.date)}
+ |
+
+ {countShifts(event)}
+ |
+
+ {status}
+ |
+ {#if !isMobile}
+
+ {#if $user?.role === 'board'}
+
+
+
+ {/if}
+ |
+ {/if}
+ {/if}
+
+ {/each}
+
+
+
+ {/if}
+
diff --git a/apps/www/src/lib/date.ts b/apps/www/src/lib/date.ts
index 3c3eda95..11bf2215 100644
--- a/apps/www/src/lib/date.ts
+++ b/apps/www/src/lib/date.ts
@@ -18,3 +18,7 @@ export const time = (date: Dateish) => {
export const normalDate = (date: Dateish) => {
return format(new Date(date), 'dd.MM.yyyy HH:mm');
};
+
+export const ISOStandard = (date: Dateish) => {
+ return format(new Date(date), "yyyy-MM-dd'T'HH:mm");
+};
diff --git a/apps/www/src/lib/services/event.service.ts b/apps/www/src/lib/services/event.service.ts
index 426ffb03..1bd7eb64 100644
--- a/apps/www/src/lib/services/event.service.ts
+++ b/apps/www/src/lib/services/event.service.ts
@@ -67,7 +67,85 @@ export class EventService {
return event;
}
+ async updateEvent(id: string, eventData: { name: string; date: Date }) {
+ const event = await this.#db
+ .insert(events)
+ .values({
+ id,
+ ...eventData
+ })
+ .onConflictDoUpdate({
+ target: events.id,
+ set: eventData
+ })
+ .returning()
+ .then((rows) => rows[0]);
+
+ return event;
+ }
+
+ async updateShift(id: string, shiftData: { eventId: string; startAt: Date; endAt: Date }) {
+ const shift = await this.#db
+ .insert(shifts)
+ .values({
+ id,
+ ...shiftData
+ })
+ .onConflictDoUpdate({
+ target: shifts.id,
+ set: shiftData
+ })
+ .returning()
+ .then((rows) => rows[0]);
+ return shift;
+ }
+
+ async findUpcomingEvents() {
+ const event = await this.#db.query.events.findMany({
+ orderBy: (row, { asc }) => [asc(row.date)],
+ with: {
+ shifts: {
+ with: {
+ members: {
+ with: {
+ user: true
+ }
+ }
+ }
+ }
+ },
+ where: (events, { gte }) => gte(events.date, new Date())
+ });
+
+ return event;
+ }
+
+ async findPastEvents() {
+ const event = await this.#db.query.events.findMany({
+ orderBy: (row, { desc }) => [desc(row.date)],
+ with: {
+ shifts: {
+ with: {
+ members: {
+ with: {
+ user: true
+ }
+ }
+ }
+ }
+ },
+ where: (events, { lt }) => lt(events.date, new Date())
+ });
+
+ return event;
+ }
+
async delete(id: string) {
await this.#db.delete(events).where(eq(events.id, id));
}
+
+ async deleteShift(id: string) {
+ await this.#db.delete(userShifts).where(eq(userShifts.shiftId, id));
+ await this.#db.delete(shifts).where(eq(shifts.id, id));
+ }
}
diff --git a/apps/www/src/routes/portal/admin/+page.svelte b/apps/www/src/routes/portal/admin/+page.svelte
index 5c1ab9ea..93d3984b 100644
--- a/apps/www/src/routes/portal/admin/+page.svelte
+++ b/apps/www/src/routes/portal/admin/+page.svelte
@@ -12,15 +12,19 @@
let isModalOpen = $state(false);
let boardMembers = $derived.by(() =>
- data.users.filter((user: User) => {
- return user.role === 'board' && user.name.toLowerCase().includes(search.toLowerCase());
- })
+ data.users
+ .filter((user: User) => {
+ return user.role === 'board' && user.name.toLowerCase().includes(search.toLowerCase());
+ })
+ .sort((a, b) => a.name.localeCompare(b.name))
);
let normalMembers = $derived.by(() =>
- data.users.filter((user: User) => {
- return user.role === 'normal' && user.name.toLowerCase().includes(search.toLowerCase());
- })
+ data.users
+ .filter((user: User) => {
+ return user.role === 'normal' && user.name.toLowerCase().includes(search.toLowerCase());
+ })
+ .sort((a, b) => a.name.localeCompare(b.name))
);
function closeModal() {
diff --git a/apps/www/src/routes/portal/arrangementer/+page.server.ts b/apps/www/src/routes/portal/arrangementer/+page.server.ts
index 4e3773aa..23692af8 100644
--- a/apps/www/src/routes/portal/arrangementer/+page.server.ts
+++ b/apps/www/src/routes/portal/arrangementer/+page.server.ts
@@ -1,21 +1,10 @@
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => {
- const events = await locals.db.query.events.findMany({
- orderBy: (row, { asc }) => [asc(row.date)],
- with: {
- shifts: true
- },
- where: (events, { gte }) => gte(events.date, new Date())
- });
-
- const outdatedEvents = await locals.db.query.events.findMany({
- orderBy: (row, { desc }) => [desc(row.date)],
- with: {
- shifts: true
- },
- where: (events, { lt }) => lt(events.date, new Date())
- });
+ const [events, outdatedEvents] = await Promise.all([
+ locals.eventService.findUpcomingEvents(),
+ locals.eventService.findPastEvents()
+ ]);
return {
events,
diff --git a/apps/www/src/routes/portal/arrangementer/+page.svelte b/apps/www/src/routes/portal/arrangementer/+page.svelte
index 8536884e..8e6ba441 100644
--- a/apps/www/src/routes/portal/arrangementer/+page.svelte
+++ b/apps/www/src/routes/portal/arrangementer/+page.svelte
@@ -1,59 +1,13 @@
-
+
+
-
-{#if showOutdatedEvents}
-
-{/if}
diff --git a/apps/www/src/routes/portal/arrangementer/[id]/+page.svelte b/apps/www/src/routes/portal/arrangementer/[id]/+page.svelte
index 6ed2cba1..fd0036ef 100644
--- a/apps/www/src/routes/portal/arrangementer/[id]/+page.svelte
+++ b/apps/www/src/routes/portal/arrangementer/[id]/+page.svelte
@@ -1,55 +1,155 @@
{data.event.name}
-
{data.event.name}
+
- Vakter
+
+
+
+
{data.event.name}
+
+
+
+
+
+
+
+
-
- {#each data.event.shifts as shift}
- {@const isInShift = shift.members.some((member) => member.userId === $user?.id)}
- -
-
{capitalize(formatDate(shift.startAt))}
- {time(subHours(shift.startAt, 2))} - {time(subHours(shift.endAt, 2))}
- Ansvarlige: {shift.members.map((member) => member.user.name).join(', ')}
+
+ {#if activeTab === 'details'}
+
+
+
+
+
+
+
Dato
+
{formatDate(data.event.date)}
+
+
- {#if !isInShift}
-
- {:else}
-
- {/if}
-
- {/each}
-
-
+
+
+
+
+
+
Antall vakter
+
+ {data.event.shifts.length}
+ {data.event.shifts.length === 1 ? 'vakt' : 'vakter'}
+
+
+
-
- {#if data.user?.role === 'board'}
- Farlig
-
- {/if}
+
+
+
+
+
+
Ansvarlige
+
+ {#each data.event.shifts as shift, i}
+ -
+ Vakt {i + 1}:
+ {shift.members.map((member) => member.user.name).join(', ') ||
+ 'Ingen ansvarlige'}
+
+ {/each}
+
+
+
+
+ {:else if activeTab === 'shifts'}
+
+ {#each data.event.shifts as shift, i}
+ {@const isInShift = shift.members.some((member) => member.userId === $user?.id)}
+
+
+
Vakt {i + 1}
+
+
+
+
+
Dato
+
{capitalize(formatDate(shift.startAt))}
+
+
+
Tid
+
+ {time(subHours(shift.startAt, 2))} - {time(subHours(shift.endAt, 2))}
+
+
+
+
+
Ansvarlige
+
+ {shift.members.map((member) => member.user.name).join(', ') ||
+ 'Ingen ansvarlige'}
+
+
+ {#if !isPastEvent}
+ {#if !isInShift}
+
+ {:else}
+
+ {/if}
+ {/if}
+
+
+ {/each}
+
+ {/if}
+
+
diff --git a/apps/www/src/routes/portal/arrangementer/[id]/edit/+page.server.ts b/apps/www/src/routes/portal/arrangementer/[id]/edit/+page.server.ts
new file mode 100644
index 00000000..37178573
--- /dev/null
+++ b/apps/www/src/routes/portal/arrangementer/[id]/edit/+page.server.ts
@@ -0,0 +1,133 @@
+import { error, fail, redirect } from '@sveltejs/kit';
+import type { Actions, PageServerLoad } from './$types';
+
+export const load: PageServerLoad = async ({ locals, params }) => {
+ const event = await locals.eventService.findFullEventById(params.id);
+ if (!event) {
+ throw error(404, 'Event not found');
+ }
+
+ const users = await locals.userService.findAll().then((users) =>
+ users.map((user) => ({
+ label: user.name,
+ value: user.id
+ }))
+ );
+
+ return {
+ event,
+ users
+ };
+};
+
+export const actions: Actions = {
+ delete: async ({ params, locals }) => {
+ if (locals.user?.role !== 'board') {
+ return fail(401, { message: 'Unauthorized' });
+ }
+
+ await locals.eventService.delete(params.id);
+ throw redirect(303, '/portal/arrangementer');
+ },
+
+ save: async ({ request, params, locals }) => {
+ if (locals.user?.role !== 'board') {
+ return fail(401, { message: 'Unauthorized' });
+ }
+
+ const formData = await request.formData();
+ const eventId = params.id;
+
+ await locals.eventService.updateEvent(params.id, {
+ name: String(formData.get('name') || ''),
+ date: new Date(String(formData.get('date') || ''))
+ });
+
+ const deletedShiftIds = formData.getAll('deletedShiftIds').map((id) => String(id));
+ for (const shiftId of deletedShiftIds) {
+ await locals.eventService.deleteShift(shiftId);
+ }
+
+ const removedUserShifts = formData.getAll('removedUserShifts').map((kv) => String(kv));
+ for (const userShift of removedUserShifts) {
+ const [shiftId, userId] = userShift.split('|');
+ if (shiftId && userId) {
+ await locals.eventService.deleteUserShift({ shiftId, userId });
+ }
+ }
+
+ const existingEvent = await locals.eventService.findFullEventById(eventId);
+ if (!existingEvent) {
+ return fail(404, { message: 'Event not found' });
+ }
+
+ const shiftsCount = parseInt(String(formData.get('shiftsCount') || '0'), 10);
+ const processedShifts = [];
+
+ for (let i = 0; i < shiftsCount; i++) {
+ const shiftId = formData.get(`shift[${i}].id`)?.toString();
+ const startAt = new Date(String(formData.get(`shift[${i}].startAt`)));
+ const endAt = new Date(String(formData.get(`shift[${i}].endAt`)));
+
+ let shift;
+
+ if (shiftId) {
+ shift = await locals.eventService.updateShift(shiftId, {
+ eventId,
+ startAt,
+ endAt
+ });
+ } else {
+ const shifts = await locals.eventService.createShifts([
+ {
+ eventId,
+ startAt,
+ endAt
+ }
+ ]);
+ shift = shifts?.[0];
+ }
+
+ if (!shift) {
+ return fail(500, { message: 'Failed to create/update shift' });
+ }
+
+ processedShifts.push({
+ shiftId: shift.id,
+ index: i
+ });
+ }
+
+ for (const { shiftId, index } of processedShifts) {
+ const userCount = parseInt(String(formData.get(`shift[${index}].userCount`) || '0'), 10);
+
+ const existingShift = existingEvent.shifts.find((s) => s.id === shiftId);
+ const existingUserIds = existingShift?.members.map((m) => m.user.id) || [];
+
+ const newUserIds: string[] = [];
+ for (let j = 0; j < userCount; j++) {
+ const userId = formData.get(`shift[${index}].user[${j}].id`)?.toString();
+ if (userId?.trim()) {
+ newUserIds.push(userId);
+ }
+ }
+
+ const usersToAdd = newUserIds.filter((userId) => !existingUserIds.includes(userId));
+ const usersToRemove = existingUserIds.filter((userId) => !newUserIds.includes(userId));
+
+ if (usersToAdd.length > 0) {
+ const userShiftsToCreate = usersToAdd.map((userId) => ({
+ shiftId,
+ userId
+ }));
+ await locals.eventService.createUserShifts(userShiftsToCreate);
+ }
+
+ for (const userId of usersToRemove) {
+ await locals.eventService.deleteUserShift({ shiftId, userId });
+ }
+ }
+
+ return { success: true, message: 'Vakten har blitt oppdatert' };
+ }
+};
diff --git a/apps/www/src/routes/portal/arrangementer/[id]/edit/+page.svelte b/apps/www/src/routes/portal/arrangementer/[id]/edit/+page.svelte
new file mode 100644
index 00000000..8cc8a388
--- /dev/null
+++ b/apps/www/src/routes/portal/arrangementer/[id]/edit/+page.svelte
@@ -0,0 +1,247 @@
+
+
+
+ Rediger arrangement: {eventState.name}
+
+
+
+
+
Arrangement detaljer
+
+
+
+
+ {#if form?.message}
+
+ {/if}
+
+ {#if deletedShiftIds.length > 0 || removedUserShifts.length > 0}
+
+
Ulagrede endringer:
+
+ {#if deletedShiftIds.length > 0}
+ -
+ • {deletedShiftIds.length} vakt{deletedShiftIds.length > 1 ? 'er' : ''} vil bli slettet
+
+ {/if}
+ {#if removedUserShifts.length > 0}
+ -
+ • {removedUserShifts.length} bruker{removedUserShifts.length > 1 ? 'e' : ''} vil bli fjernet
+
+ {/if}
+
+
+ {/if}
+
+
+
+
diff --git a/apps/www/src/routes/portal/arrangementer/ny/+page.svelte b/apps/www/src/routes/portal/arrangementer/ny/+page.svelte
index 51cbb19d..fe128b46 100644
--- a/apps/www/src/routes/portal/arrangementer/ny/+page.svelte
+++ b/apps/www/src/routes/portal/arrangementer/ny/+page.svelte
@@ -1,7 +1,6 @@