Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions programmerbar-web/src/lib/components/portal/Training.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,27 @@

interface Props {
userId?: string | number | null;
userIds?: string[];
isOpen: boolean;
userName?: string;
onclose?: () => void;
onsave?: (data: { completionStatus: { isComplete: boolean } }) => void;
}

let { userId = null, isOpen = false, userName = 'bruker', onclose, onsave }: Props = $props();
let {
userId = null,
userIds = [],
isOpen = false,
userName = 'bruker',
onclose,
onsave
}: Props = $props();

let trainingItems = $state<TrainingItem[]>([...DEFAULT_TRAINING_ITEMS]);
let isSaving = $state(false);
let saveError = $state('');

let isTrainingMode = $derived(userId !== null && userId !== undefined);
let isTrainingMode = $derived(userIds.length > 0 || (userId !== null && userId !== undefined));
let completedCount = $derived(trainingItems.filter((item) => item.completed).length);
let totalCount = $derived(trainingItems.length);
let isComplete = $derived(completedCount === totalCount);
Expand All @@ -35,6 +44,7 @@
$effect(() => {
if (isOpen) {
trainingItems = [...DEFAULT_TRAINING_ITEMS];
saveError = '';
}
});

Expand All @@ -46,7 +56,8 @@
}

function handleSave() {
if (!isComplete) return;
if (!isComplete || isSaving) return;
saveError = '';
isSaving = true;
const form = document.getElementById('trainingForm') as HTMLFormElement;
if (form) {
Expand All @@ -55,7 +66,7 @@
}

function handleClose() {
onclose?.();
if (!isSaving) onclose?.();
}

const groupedItems = $derived(
Expand Down Expand Up @@ -84,6 +95,9 @@
</ModalHeader>

<ModalBody>
{#if saveError}<p role="alert" class="mb-4 text-red-600 dark:text-red-400">
{saveError}
</p>{/if}
<div class="space-y-6">
{#each Object.entries(groupedItems) as [category, items] (category)}
<div class="space-y-3">
Expand Down Expand Up @@ -188,12 +202,16 @@
}
} else if (result.type === 'failure') {
const data = result.data as { error?: string } | undefined;
console.error('Failed to complete training:', data?.error);
saveError = data?.error || 'Kunne ikke lagre opplæringen. Prøv igjen.';
} else {
saveError = 'Kunne ikke lagre opplæringen. Prøv igjen.';
}
};
}}
>
<input type="hidden" name="userId" value={userId?.toString() || ''} />
{#each userIds.length ? userIds : [userId?.toString() || ''] as id (id)}
<input type="hidden" name="userId" value={id} />
{/each}
<input type="hidden" name="trainingData" value={JSON.stringify(trainingItems)} />
</form>
{/if}
9 changes: 9 additions & 0 deletions programmerbar-web/src/lib/server/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ export class UserService {
.then((rows) => rows[0]);
}

async completeTrainingForUsers(userIds: string[]) {
if (userIds.length === 0) return [];
return await this.#db
.update(users)
.set({ isTrained: true })
.where(and(inArray(users.id, userIds), not(users.isDeleted)))
.returning({ id: users.id });
}

async updateTrainingStatus(userId: string, isTrained: boolean) {
const updatedUser = await this.#db
.update(users)
Expand Down
22 changes: 22 additions & 0 deletions programmerbar-web/src/lib/utils/training.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest';
import { DEFAULT_TRAINING_ITEMS, isTrainingComplete } from './training';

const completed = DEFAULT_TRAINING_ITEMS.map((item) => ({ ...item, completed: true }));
describe('isTrainingComplete', () => {
it('accepts the complete checklist', () => {
expect(isTrainingComplete(completed)).toBe(true);
});
it('rejects missing, duplicated, unknown and incomplete items', () => {
for (const value of [
null,
{},
[],
completed.slice(1),
[...completed.slice(1), completed[1]],
[...completed.slice(1), { id: -1, completed: true }],
DEFAULT_TRAINING_ITEMS
]) {
expect(isTrainingComplete(value)).toBe(false);
}
});
});
8 changes: 8 additions & 0 deletions programmerbar-web/src/lib/utils/training.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,11 @@ export const DEFAULT_TRAINING_ITEMS: TrainingItem[] = [
category: TRAINING_CATEGORIES.LAWS_SAFETY
}
];

export function isTrainingComplete(value: unknown): boolean {
if (!Array.isArray(value) || value.length !== DEFAULT_TRAINING_ITEMS.length) return false;
return DEFAULT_TRAINING_ITEMS.every(
(required) =>
value.filter((item) => item?.id === required.id && item.completed === true).length === 1
);
}
29 changes: 29 additions & 0 deletions programmerbar-web/src/routes/(portal)/portal/admin/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isTrainingComplete } from '$lib/utils/training';
import { redirect, fail, type Actions } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';

Expand All @@ -15,6 +16,34 @@ export const load: PageServerLoad = async ({ locals }) => {
};

export const actions: Actions = {
completeTraining: async ({ request, locals }) => {
if (!locals.user || locals.user.role !== 'board') {
return fail(401, { error: 'Du har ikke tilgang til å registrere opplæring.' });
}
const formData = await request.formData();
const ids = formData.getAll('userId');
if (!ids.length || ids.some((id) => typeof id !== 'string' || !id.trim())) {
return fail(400, { error: 'Velg minst én bruker.' });
}
const userIds = [...new Set(ids as string[])];
let trainingData: unknown;
try {
trainingData = JSON.parse(String(formData.get('trainingData')));
} catch {
return fail(400, { error: 'Ugyldig opplæringsdata.' });
}
if (!isTrainingComplete(trainingData)) {
return fail(400, { error: 'Alle opplæringspunktene må være fullført.' });
}
const users = await locals.userService.findAll();
if (userIds.some((id) => !users.some((user) => user.id === id))) {
return fail(400, {
error: 'En valgt bruker finnes ikke lenger. Oppdater siden og prøv igjen.'
});
}
await locals.userService.completeTrainingForUsers(userIds);
return { success: true, trainingCompleted: true };
},
updateRole: async ({ request, locals }) => {
const formData = await request.formData();
const userId = formData.get('userId') as string;
Expand Down
Loading
Loading