diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts index d446664..326f5ce 100644 --- a/frontend/src/app.d.ts +++ b/frontend/src/app.d.ts @@ -68,12 +68,52 @@ interface User { notes: string | ''; } +interface Car { + id: number | -1; + name: string | ''; + status: number | 0; + brand: string | ''; + model: string | ''; + price: number | 0; + rate: number | 0; + start_date: string | ''; + end_date: string | ''; + color: string | ''; + licence_plate: string | ''; + location: Location; + damages: Damage[] | []; + insurances: Insurance[] | []; + notes: string | ''; +} + +interface Location { + latitude: number | 0; + longitude: number | 0; +} + +interface Damage { + id: number | -1; + opponent: User; + insurance: Insurance | null; + notes: string | ''; +} + +interface Insurance { + id: number | -1; + company: string | ''; + reference: string | ''; + start_date: string | ''; + end_date: string | ''; + notes: string | ''; +} + declare global { namespace App { // interface Error {} interface Locals { user: User; users: User[]; + cars: Cars[]; subscriptions: Subscription[]; licence_categories: LicenceCategory[]; } @@ -84,6 +124,10 @@ declare global { licence: Licence; licenceCategory: LicenceCategory; bankAccount: BankAccount; + car: Car; + insurance: Insurance; + location: Location; + damage: Damage; } // interface PageData {} // interface Platform {} diff --git a/frontend/src/routes/auth/admin/users/+layout.js b/frontend/src/routes/auth/admin/users/+layout.js index 767c933..524962e 100644 --- a/frontend/src/routes/auth/admin/users/+layout.js +++ b/frontend/src/routes/auth/admin/users/+layout.js @@ -2,6 +2,7 @@ export async function load({ data }) { return { users: data.users, - user: data.user + user: data.user, + cars: data.cars }; } diff --git a/frontend/src/routes/auth/admin/users/+layout.server.js b/frontend/src/routes/auth/admin/users/+layout.server.js index e8244ba..087f42d 100644 --- a/frontend/src/routes/auth/admin/users/+layout.server.js +++ b/frontend/src/routes/auth/admin/users/+layout.server.js @@ -1,43 +1,63 @@ import { BASE_API_URI } from '$lib/utils/constants'; import { redirect } from '@sveltejs/kit'; -import { userDatesFromRFC3339, refreshCookie } from '$lib/utils/helpers'; +import { userDatesFromRFC3339, refreshCookie, carDatesFromRFC3339 } from '$lib/utils/helpers'; import { base } from '$app/paths'; /** @type {import('./$types').LayoutServerLoad} */ export async function load({ cookies, fetch, locals }) { const jwt = cookies.get('jwt'); try { - const response = await fetch(`${BASE_API_URI}/auth/users/`, { - credentials: 'include', - headers: { - Cookie: `jwt=${jwt}` - } - }); - if (!response.ok) { - // Clear the invalid JWT cookie + const [usersResponse, carsResponse] = await Promise.all([ + fetch(`${BASE_API_URI}/auth/users`, { + credentials: 'include', + headers: { Cookie: `jwt=${jwt}` } + }), + fetch(`${BASE_API_URI}/auth/cars`, { + credentials: 'include', + headers: { Cookie: `jwt=${jwt}` } + }) + ]); + if (!usersResponse.ok || !carsResponse.ok) { cookies.delete('jwt', { path: '/' }); throw redirect(302, `${base}/auth/login?next=${base}/auth/admin/users/`); } + const [usersData, carsData] = await Promise.all([usersResponse.json(), carsResponse.json()]); + // const response = await fetch(`${BASE_API_URI}/auth/users/`, { + // credentials: 'include', + // headers: { + // Cookie: `jwt=${jwt}` + // } + // }); + // if (!response.ok) { + // // Clear the invalid JWT cookie + // cookies.delete('jwt', { path: '/' }); + // throw redirect(302, `${base}/auth/login?next=${base}/auth/admin/users/`); + // } - const data = await response.json(); + // const data = await response.json(); /** @type {App.Locals['users']}*/ - const users = data.users; + const users = usersData.users; + /** @type {App.Types['car'][]} */ + const cars = carsData.cars; users.forEach((user) => { userDatesFromRFC3339(user); }); + cars.forEach((car) => { + carDatesFromRFC3339(car); + }); locals.users = users; - + locals.cars = cars; // Check if the server sent a new token - const newToken = response.headers.get('Set-Cookie'); + const newToken = usersResponse.headers.get('Set-Cookie'); refreshCookie(newToken, cookies); - return { subscriptions: locals.subscriptions, licence_categories: locals.licence_categories, users: locals.users, - user: locals.user + user: locals.user, + cars: locals.cars }; } catch (error) { console.error('Error fetching data:', error); diff --git a/frontend/src/routes/auth/admin/users/+page.server.js b/frontend/src/routes/auth/admin/users/+page.server.js index 5fd1ab6..ab02e9d 100644 --- a/frontend/src/routes/auth/admin/users/+page.server.js +++ b/frontend/src/routes/auth/admin/users/+page.server.js @@ -7,6 +7,7 @@ import { formatError, hasPrivilige, userDatesFromRFC3339 } from '$lib/utils/help import { fail, redirect } from '@sveltejs/kit'; import { formDataToObject, + processCarFormData, processSubscriptionFormData, processUserFormData } from '$lib/utils/processing'; @@ -36,7 +37,12 @@ export const actions = { updateUser: async ({ request, fetch, cookies, locals }) => { let formData = await request.formData(); - const rawData = formDataToObject(formData); + const rawFormData = formDataToObject(formData); + /** @type {{object: Partial, confirm_password: string}} */ + const rawData = { + object: /** @type {Partial} */ (rawFormData.object), + confirm_password: rawFormData.confirm_password + }; const processedData = processUserFormData(rawData); console.dir(processedData.user.membership); @@ -96,7 +102,55 @@ export const actions = { 'Content-Type': 'application/json', Cookie: `jwt=${cookies.get('jwt')}` }, - body: JSON.stringify(processedData) + body: JSON.stringify(processedData.subscription) + }; + + const res = await fetch(apiURL, requestOptions); + + if (!res.ok) { + const response = await res.json(); + const errors = formatError(response.errors); + return fail(400, { errors: errors }); + } + + const response = await res.json(); + console.log('Server success response:', response); + throw redirect(303, `${base}/auth/admin/users`); + }, + + /** + * + * @param request - The request object + * @param fetch - Fetch object from sveltekit + * @param cookies - SvelteKit's cookie object + * @returns Error data or redirects user to the home page or the previous page + */ + updateCar: async ({ request, fetch, cookies }) => { + let formData = await request.formData(); + + const rawFormData = formDataToObject(formData); + /** @type {{object: Partial, confirm_password: string}} */ + const rawData = { + object: /** @type {Partial} */ (rawFormData.object), + confirm_password: rawFormData.confirm_password + }; + const processedData = processCarFormData(rawData); + + const isCreating = !processedData.car.id || processedData.car.id === 0; + console.log('Is creating: ', isCreating); + console.log('sending: ', JSON.stringify(processedData.car)); + + const apiURL = `${BASE_API_URI}/auth/cars`; + + /** @type {RequestInit} */ + const requestOptions = { + method: isCreating ? 'POST' : 'PUT', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + Cookie: `jwt=${cookies.get('jwt')}` + }, + body: JSON.stringify(processedData.car) }; const res = await fetch(apiURL, requestOptions); @@ -123,7 +177,12 @@ export const actions = { userDelete: async ({ request, fetch, cookies }) => { let formData = await request.formData(); - const rawData = formDataToObject(formData); + const rawFormData = formDataToObject(formData); + /** @type {{object: Partial, confirm_password: string}} */ + const rawData = { + object: /** @type {Partial} */ (rawFormData.object), + confirm_password: rawFormData.confirm_password + }; const processedData = processUserFormData(rawData); const apiURL = `${BASE_API_URI}/auth/users`; @@ -176,7 +235,7 @@ export const actions = { 'Content-Type': 'application/json', Cookie: `jwt=${cookies.get('jwt')}` }, - body: JSON.stringify(processedData) + body: JSON.stringify(processedData.subscription) }; const res = await fetch(apiURL, requestOptions); @@ -191,10 +250,23 @@ export const actions = { console.log('Server success response:', response); throw redirect(303, `${base}/auth/admin/users`); }, + + /** + * + * @param request - The request object + * @param fetch - Fetch object from sveltekit + * @param cookies - SvelteKit's cookie object + * @returns + */ grantBackendAccess: async ({ request, fetch, cookies }) => { let formData = await request.formData(); - const rawData = formDataToObject(formData); + const rawFormData = formDataToObject(formData); + /** @type {{object: Partial, confirm_password: string}} */ + const rawData = { + object: /** @type {Partial} */ (rawFormData.object), + confirm_password: rawFormData.confirm_password + }; const processedData = processUserFormData(rawData); console.dir(processedData); const apiURL = `${BASE_API_URI}/auth/users/activate`; diff --git a/frontend/src/routes/auth/admin/users/+page.svelte b/frontend/src/routes/auth/admin/users/+page.svelte index 08cbdbc..a4800e9 100644 --- a/frontend/src/routes/auth/admin/users/+page.svelte +++ b/frontend/src/routes/auth/admin/users/+page.svelte @@ -8,7 +8,13 @@ import { applyAction, enhance } from '$app/forms'; import { hasPrivilige, receive, send } from '$lib/utils/helpers'; import { PERMISSIONS } from '$lib/utils/constants'; - import { defaultSupporter, defaultUser } from '$lib/utils/defaults'; + import { + defaultCar, + defaultSubscription, + defaultSupporter, + defaultUser + } from '$lib/utils/defaults'; + import CarEditForm from '$lib/components/CarEditForm.svelte'; /** @type {import('./$types').ActionData} */ export let form; @@ -16,18 +22,17 @@ $: ({ user = [], users = [], + cars = [], licence_categories = [], subscriptions = [], payments = [] } = $page.data); let activeSection = 'members'; - /** @type{App.Locals['user'] | null} */ - let selectedUser = null; - /** @type{App.Types['subscription'] | null} */ - let selectedSubscription = null; - let showSubscriptionModal = false; - let showUserModal = false; + + /** @type{App.Types['car'] | App.Types['subscription'] | App.Locals['user'] | null} */ + let selected = null; + let searchTerm = ''; $: members = users.filter((/** @type{App.Locals['user']} */ user) => { @@ -95,29 +100,8 @@ }); }; - /** - * Opens the edit modal for the selected user. - * @param {App.Locals['user']} user The user to edit. - */ - const openEditUserModal = (user) => { - selectedUser = user; - showUserModal = true; - }; - - /** - * Opens the edit modal for the selected subscription. - * @param {App.Types['subscription'] | null} subscription The user to edit. - */ - const openEditSubscriptionModal = (subscription) => { - selectedSubscription = subscription; - showSubscriptionModal = true; - }; - const close = () => { - showUserModal = false; - showSubscriptionModal = false; - selectedUser = null; - selectedSubscription = null; + selected = null; if (form) { form.errors = []; } @@ -167,6 +151,16 @@ {subscriptions.length} +
  • + +
  • @@ -288,7 +287,12 @@
    - @@ -350,7 +354,12 @@
    - @@ -384,7 +393,12 @@
    - @@ -429,7 +443,12 @@

    {$t('subscription.subscriptions')}

    {#if hasPrivilige(user, PERMISSIONS.Super)} - @@ -488,7 +507,9 @@
    + {:else if activeSection === 'cars'} +
    +

    {$t('cars')}

    + {#if hasPrivilige(user, PERMISSIONS.Super)} + + {/if} +
    +
    + {#each cars as car} +
    + + {car.model + ' (' + car.licence_plate + ')'} + +
    + + + + + + + + + + + + + + + + + + + + + + + +
    {$t('car.model')}{car.brand + ' ' + car.model + ' (' + car.color + ')'}
    {$t('price')}{car.price + '€'}{car.rate + ? ' + ' + car.rate + '€/' + $t('month') + : ''}
    {$t('car.damages')}{car.damages?.length || 0}
    {$t('insurance')}{car.insurance + ? car.insurance.company + '(' + car.insurance.reference + ')' + : '-'}
    {$t('car.end_date')}{car.end_date || '-'}
    + {#if hasPrivilige(user, PERMISSIONS.Update)} +
    + +
    { + return async ({ result }) => { + if (result.type === 'success' || result.type === 'redirect') { + await applyAction(result); + } else { + document + .querySelector('.accordion-content') + ?.scrollTo({ top: 0, behavior: 'smooth' }); + await applyAction(result); + } + }; + }} + on:submit|preventDefault={(/** @type {SubmitEvent} */ e) => { + if ( + !confirm( + $t('dialog.car_deletion', { + values: { + name: car.brand + ' ' + car.model + ' (' + car.licence_plate + ')' + } + }) + ) + ) { + e.preventDefault(); // Cancel form submission if user declines + } + }} + > + + +
    +
    + {/if} +
    +
    + {/each} +
    {:else if activeSection === 'payments'}

    {$t('payments')}

    @@ -567,28 +694,34 @@
    -{#if showUserModal && selectedUser !== null} +{#if selected && 'email' in selected} + // user -{:else if showSubscriptionModal} +{:else if selected && 'monthly_fee' in selected} + //subscription +{:else if selected && 'brand' in selected} + + + {/if}