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 $user?.role === 'board'} + + + + {/if} + + +
+ + {#if filteredEvents.length === 0} +
+ Ingen {activeTab === 'upcoming' ? 'kommende' : 'tidligere'} arrangementer å vise. +
+ {:else} +
+ + {#if isMobile} + + + + + {:else} + + + + + + + {/if} + + + + {#if isMobile} + + {:else} + + + + + {/if} + + + + {#each filteredEvents as event (event.id)} + {@const status = getEventStatus(event)} + + + {#if isMobile} + + {:else} + + + + {#if !isMobile} + + {/if} + {/if} + + {/each} + +
ArrangementStatusDatoAntall vakterStatus
+ + {event.name} + + {status} + + {formatDate(event.date)} + + {countShifts(event)} + + {status} + + {#if $user?.role === 'board'} + + + + {/if} +
+
+ {/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 @@ Arrangementer -Arrangementer -{#if $user?.role == 'board'} -

- Nytt arrangement -

-{/if} - - - -
- +
+
- -{#if showOutdatedEvents} -
-
    - {#each data.outdatedEvents as event} -
  • - -
  • - {:else} -

    Ingen tidligere arrangementer

    - {/each} -
-
-{/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} +
+ + + Tilbake + +
- 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} +
+

{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} + +
+ + {#each deletedShiftIds as id} + + {/each} + {#each removedUserShifts as keyValue} + + {/each} + +
+
+ + +
+
+ +
+
+

Vakter

+ +
+ + {#each eventState.shifts as shift, i} +
+
+

Vakt {i + 1}

+ +
+ + {#if i < originalShifts.length} + + {/if} + +
+ + +
+ +
+
+

Ansvarlige

+ +
+ + + + {#if shift.users.length === 0} +

Ingen ansvarlige

+ {:else} +
+ {#each shift.users as user, j} +
+ u.id && u.id !== user.id) + .map((u) => u.id)} + onchange={(option) => { + if (option?.value) { + user.id = option.value; + user.name = option.label || ''; + } + }} + placeholder="Velg en bruker" + /> + + +
+ {/each} +
+ {/if} +
+
+ {/each} +
+ +
+
+ + + + +
+
+
+
+
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 @@