frontend: add car handling

This commit is contained in:
Alex
2025-03-15 00:12:00 +01:00
parent 380fee09c1
commit c9d5a88dbf
5 changed files with 330 additions and 60 deletions

44
frontend/src/app.d.ts vendored
View File

@@ -68,12 +68,52 @@ interface User {
notes: string | ''; 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 { declare global {
namespace App { namespace App {
// interface Error {} // interface Error {}
interface Locals { interface Locals {
user: User; user: User;
users: User[]; users: User[];
cars: Cars[];
subscriptions: Subscription[]; subscriptions: Subscription[];
licence_categories: LicenceCategory[]; licence_categories: LicenceCategory[];
} }
@@ -84,6 +124,10 @@ declare global {
licence: Licence; licence: Licence;
licenceCategory: LicenceCategory; licenceCategory: LicenceCategory;
bankAccount: BankAccount; bankAccount: BankAccount;
car: Car;
insurance: Insurance;
location: Location;
damage: Damage;
} }
// interface PageData {} // interface PageData {}
// interface Platform {} // interface Platform {}

View File

@@ -2,6 +2,7 @@
export async function load({ data }) { export async function load({ data }) {
return { return {
users: data.users, users: data.users,
user: data.user user: data.user,
cars: data.cars
}; };
} }

View File

@@ -1,43 +1,63 @@
import { BASE_API_URI } from '$lib/utils/constants'; import { BASE_API_URI } from '$lib/utils/constants';
import { redirect } from '@sveltejs/kit'; 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'; import { base } from '$app/paths';
/** @type {import('./$types').LayoutServerLoad} */ /** @type {import('./$types').LayoutServerLoad} */
export async function load({ cookies, fetch, locals }) { export async function load({ cookies, fetch, locals }) {
const jwt = cookies.get('jwt'); const jwt = cookies.get('jwt');
try { try {
const response = await fetch(`${BASE_API_URI}/auth/users/`, { const [usersResponse, carsResponse] = await Promise.all([
credentials: 'include', fetch(`${BASE_API_URI}/auth/users`, {
headers: { credentials: 'include',
Cookie: `jwt=${jwt}` headers: { Cookie: `jwt=${jwt}` }
} }),
}); fetch(`${BASE_API_URI}/auth/cars`, {
if (!response.ok) { credentials: 'include',
// Clear the invalid JWT cookie headers: { Cookie: `jwt=${jwt}` }
})
]);
if (!usersResponse.ok || !carsResponse.ok) {
cookies.delete('jwt', { path: '/' }); cookies.delete('jwt', { path: '/' });
throw redirect(302, `${base}/auth/login?next=${base}/auth/admin/users/`); 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']}*/ /** @type {App.Locals['users']}*/
const users = data.users; const users = usersData.users;
/** @type {App.Types['car'][]} */
const cars = carsData.cars;
users.forEach((user) => { users.forEach((user) => {
userDatesFromRFC3339(user); userDatesFromRFC3339(user);
}); });
cars.forEach((car) => {
carDatesFromRFC3339(car);
});
locals.users = users; locals.users = users;
locals.cars = cars;
// Check if the server sent a new token // 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); refreshCookie(newToken, cookies);
return { return {
subscriptions: locals.subscriptions, subscriptions: locals.subscriptions,
licence_categories: locals.licence_categories, licence_categories: locals.licence_categories,
users: locals.users, users: locals.users,
user: locals.user user: locals.user,
cars: locals.cars
}; };
} catch (error) { } catch (error) {
console.error('Error fetching data:', error); console.error('Error fetching data:', error);

View File

@@ -7,6 +7,7 @@ import { formatError, hasPrivilige, userDatesFromRFC3339 } from '$lib/utils/help
import { fail, redirect } from '@sveltejs/kit'; import { fail, redirect } from '@sveltejs/kit';
import { import {
formDataToObject, formDataToObject,
processCarFormData,
processSubscriptionFormData, processSubscriptionFormData,
processUserFormData processUserFormData
} from '$lib/utils/processing'; } from '$lib/utils/processing';
@@ -36,7 +37,12 @@ export const actions = {
updateUser: async ({ request, fetch, cookies, locals }) => { updateUser: async ({ request, fetch, cookies, locals }) => {
let formData = await request.formData(); let formData = await request.formData();
const rawData = formDataToObject(formData); const rawFormData = formDataToObject(formData);
/** @type {{object: Partial<App.Locals['user']>, confirm_password: string}} */
const rawData = {
object: /** @type {Partial<App.Locals['user']>} */ (rawFormData.object),
confirm_password: rawFormData.confirm_password
};
const processedData = processUserFormData(rawData); const processedData = processUserFormData(rawData);
console.dir(processedData.user.membership); console.dir(processedData.user.membership);
@@ -96,7 +102,55 @@ export const actions = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get('jwt')}` 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<App.Types['car']>, confirm_password: string}} */
const rawData = {
object: /** @type {Partial<App.Types['car']>} */ (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); const res = await fetch(apiURL, requestOptions);
@@ -123,7 +177,12 @@ export const actions = {
userDelete: async ({ request, fetch, cookies }) => { userDelete: async ({ request, fetch, cookies }) => {
let formData = await request.formData(); let formData = await request.formData();
const rawData = formDataToObject(formData); const rawFormData = formDataToObject(formData);
/** @type {{object: Partial<App.Locals['user']>, confirm_password: string}} */
const rawData = {
object: /** @type {Partial<App.Locals['user']>} */ (rawFormData.object),
confirm_password: rawFormData.confirm_password
};
const processedData = processUserFormData(rawData); const processedData = processUserFormData(rawData);
const apiURL = `${BASE_API_URI}/auth/users`; const apiURL = `${BASE_API_URI}/auth/users`;
@@ -176,7 +235,7 @@ export const actions = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get('jwt')}` Cookie: `jwt=${cookies.get('jwt')}`
}, },
body: JSON.stringify(processedData) body: JSON.stringify(processedData.subscription)
}; };
const res = await fetch(apiURL, requestOptions); const res = await fetch(apiURL, requestOptions);
@@ -191,10 +250,23 @@ export const actions = {
console.log('Server success response:', response); console.log('Server success response:', response);
throw redirect(303, `${base}/auth/admin/users`); 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 }) => { grantBackendAccess: async ({ request, fetch, cookies }) => {
let formData = await request.formData(); let formData = await request.formData();
const rawData = formDataToObject(formData); const rawFormData = formDataToObject(formData);
/** @type {{object: Partial<App.Types['subscription']>, confirm_password: string}} */
const rawData = {
object: /** @type {Partial<App.Types['subscription']>} */ (rawFormData.object),
confirm_password: rawFormData.confirm_password
};
const processedData = processUserFormData(rawData); const processedData = processUserFormData(rawData);
console.dir(processedData); console.dir(processedData);
const apiURL = `${BASE_API_URI}/auth/users/activate`; const apiURL = `${BASE_API_URI}/auth/users/activate`;

View File

@@ -8,7 +8,13 @@
import { applyAction, enhance } from '$app/forms'; import { applyAction, enhance } from '$app/forms';
import { hasPrivilige, receive, send } from '$lib/utils/helpers'; import { hasPrivilige, receive, send } from '$lib/utils/helpers';
import { PERMISSIONS } from '$lib/utils/constants'; 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} */ /** @type {import('./$types').ActionData} */
export let form; export let form;
@@ -16,18 +22,17 @@
$: ({ $: ({
user = [], user = [],
users = [], users = [],
cars = [],
licence_categories = [], licence_categories = [],
subscriptions = [], subscriptions = [],
payments = [] payments = []
} = $page.data); } = $page.data);
let activeSection = 'members'; let activeSection = 'members';
/** @type{App.Locals['user'] | null} */
let selectedUser = null; /** @type{App.Types['car'] | App.Types['subscription'] | App.Locals['user'] | null} */
/** @type{App.Types['subscription'] | null} */ let selected = null;
let selectedSubscription = null;
let showSubscriptionModal = false;
let showUserModal = false;
let searchTerm = ''; let searchTerm = '';
$: members = users.filter((/** @type{App.Locals['user']} */ user) => { $: 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 = () => { const close = () => {
showUserModal = false; selected = null;
showSubscriptionModal = false;
selectedUser = null;
selectedSubscription = null;
if (form) { if (form) {
form.errors = []; form.errors = [];
} }
@@ -167,6 +151,16 @@
<span class="nav-badge">{subscriptions.length}</span> <span class="nav-badge">{subscriptions.length}</span>
</button> </button>
</li> </li>
<li>
<button
class="nav-link {activeSection === 'cars' ? 'active' : ''}"
on:click={() => setActiveSection('cars')}
>
<i class="fas fa-car"></i>
{$t('cars')}
<span class="nav-badge">{cars.length}</span>
</button>
</li>
<li> <li>
<button <button
class="nav-link {activeSection === 'payments' ? 'active' : ''}" class="nav-link {activeSection === 'payments' ? 'active' : ''}"
@@ -214,7 +208,12 @@
</button> </button>
</div> </div>
<div> <div>
<button class="btn primary" on:click={() => openEditUserModal(defaultUser())}> <button
class="btn primary"
on:click={() => {
selected = defaultUser();
}}
>
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
{$t('add_new')} {$t('add_new')}
</button> </button>
@@ -288,7 +287,12 @@
</tbody> </tbody>
</table> </table>
<div class="button-group"> <div class="button-group">
<button class="btn primary" on:click={() => openEditUserModal(user)}> <button
class="btn primary"
on:click={() => {
selected = user;
}}
>
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
{$t('edit')} {$t('edit')}
</button> </button>
@@ -350,7 +354,12 @@
</button> </button>
</div> </div>
<div> <div>
<button class="btn primary" on:click={() => openEditUserModal(defaultSupporter())}> <button
class="btn primary"
on:click={() => {
selected = defaultSupporter();
}}
>
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
{$t('add_new')} {$t('add_new')}
</button> </button>
@@ -384,7 +393,12 @@
</tbody> </tbody>
</table> </table>
<div class="button-group"> <div class="button-group">
<button class="btn primary" on:click={() => openEditUserModal(user)}> <button
class="btn primary"
on:click={() => {
selected = user;
}}
>
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
{$t('edit')} {$t('edit')}
</button> </button>
@@ -429,7 +443,12 @@
<div class="section-header"> <div class="section-header">
<h2>{$t('subscription.subscriptions')}</h2> <h2>{$t('subscription.subscriptions')}</h2>
{#if hasPrivilige(user, PERMISSIONS.Super)} {#if hasPrivilige(user, PERMISSIONS.Super)}
<button class="btn primary" on:click={() => openEditSubscriptionModal(null)}> <button
class="btn primary"
on:click={() => {
selected = defaultSubscription();
}}
>
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
{$t('add_new')} {$t('add_new')}
</button> </button>
@@ -488,7 +507,9 @@
<div class="button-group"> <div class="button-group">
<button <button
class="btn primary" class="btn primary"
on:click={() => openEditSubscriptionModal(subscription)} on:click={() => {
selected = subscription;
}}
> >
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
{$t('edit')} {$t('edit')}
@@ -537,6 +558,112 @@
</details> </details>
{/each} {/each}
</div> </div>
{:else if activeSection === 'cars'}
<div class="section-header">
<h2>{$t('cars')}</h2>
{#if hasPrivilige(user, PERMISSIONS.Super)}
<button
class="btn primary"
on:click={() => {
selected = defaultCar();
}}
>
<i class="fas fa-plus"></i>
{$t('add_new')}
</button>
{/if}
</div>
<div class="accordion">
{#each cars as car}
<details class="accordion-item">
<summary class="accordion-header">
{car.model + ' (' + car.licence_plate + ')'}
</summary>
<div class="accordion-content">
<table class="table">
<tbody>
<tr>
<th>{$t('car.model')}</th>
<td>{car.brand + ' ' + car.model + ' (' + car.color + ')'}</td>
</tr>
<tr>
<th>{$t('price')}</th>
<td
>{car.price + '€'}{car.rate
? ' + ' + car.rate + '€/' + $t('month')
: ''}</td
>
</tr>
<tr>
<th>{$t('car.damages')}</th>
<td>{car.damages?.length || 0}</td>
</tr>
<tr>
<th>{$t('insurance')}</th>
<td
>{car.insurance
? car.insurance.company + '(' + car.insurance.reference + ')'
: '-'}</td
>
</tr>
<tr>
<th>{$t('car.end_date')}</th>
<td>{car.end_date || '-'}</td>
</tr>
</tbody>
</table>
{#if hasPrivilige(user, PERMISSIONS.Update)}
<div class="button-group">
<button
class="btn primary"
on:click={() => {
selected = car;
}}
>
<i class="fas fa-edit"></i>
{$t('edit')}
</button>
<form
method="POST"
action="?/carDelete"
use:enhance={() => {
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
}
}}
>
<input type="hidden" name="subscription[id]" value={car.id} />
<button class="btn danger" type="submit">
<i class="fas fa-trash"></i>
{$t('delete')}
</button>
</form>
</div>
{/if}
</div>
</details>
{/each}
</div>
{:else if activeSection === 'payments'} {:else if activeSection === 'payments'}
<h2>{$t('payments')}</h2> <h2>{$t('payments')}</h2>
<div class="accordion"> <div class="accordion">
@@ -567,28 +694,34 @@
</div> </div>
</div> </div>
{#if showUserModal && selectedUser !== null} {#if selected && 'email' in selected}
// user
<Modal on:close={close}> <Modal on:close={close}>
<UserEditForm <UserEditForm
{form} {form}
editor={user} editor={user}
user={selectedUser} user={selected}
{subscriptions} {subscriptions}
{licence_categories} {licence_categories}
on:cancel={close} on:cancel={close}
on:close={close} on:close={close}
/> />
</Modal> </Modal>
{:else if showSubscriptionModal} {:else if selected && 'monthly_fee' in selected}
//subscription
<Modal on:close={close}> <Modal on:close={close}>
<SubscriptionEditForm <SubscriptionEditForm
{form} {form}
{user} {user}
subscription={selectedSubscription} subscription={selected}
on:cancel={close} on:cancel={close}
on:close={close} on:close={close}
/> />
</Modal> </Modal>
{:else if selected && 'brand' in selected}
<Modal on:close={close}>
<CarEditForm {form} editor={user} car={selected} on:cancel={close} on:close={close} />
</Modal>
{/if} {/if}
<style> <style>