frontend: add car handling
This commit is contained in:
44
frontend/src/app.d.ts
vendored
44
frontend/src/app.d.ts
vendored
@@ -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 {}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
export async function load({ data }) {
|
||||
return {
|
||||
users: data.users,
|
||||
user: data.user
|
||||
user: data.user,
|
||||
cars: data.cars
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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/`, {
|
||||
const [usersResponse, carsResponse] = await Promise.all([
|
||||
fetch(`${BASE_API_URI}/auth/users`, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Cookie: `jwt=${jwt}`
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
// Clear the invalid JWT cookie
|
||||
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);
|
||||
|
||||
@@ -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<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);
|
||||
|
||||
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<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);
|
||||
@@ -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<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 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<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);
|
||||
console.dir(processedData);
|
||||
const apiURL = `${BASE_API_URI}/auth/users/activate`;
|
||||
|
||||
@@ -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 @@
|
||||
<span class="nav-badge">{subscriptions.length}</span>
|
||||
</button>
|
||||
</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>
|
||||
<button
|
||||
class="nav-link {activeSection === 'payments' ? 'active' : ''}"
|
||||
@@ -214,7 +208,12 @@
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn primary" on:click={() => openEditUserModal(defaultUser())}>
|
||||
<button
|
||||
class="btn primary"
|
||||
on:click={() => {
|
||||
selected = defaultUser();
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-plus"></i>
|
||||
{$t('add_new')}
|
||||
</button>
|
||||
@@ -288,7 +287,12 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<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>
|
||||
{$t('edit')}
|
||||
</button>
|
||||
@@ -350,7 +354,12 @@
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn primary" on:click={() => openEditUserModal(defaultSupporter())}>
|
||||
<button
|
||||
class="btn primary"
|
||||
on:click={() => {
|
||||
selected = defaultSupporter();
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-plus"></i>
|
||||
{$t('add_new')}
|
||||
</button>
|
||||
@@ -384,7 +393,12 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<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>
|
||||
{$t('edit')}
|
||||
</button>
|
||||
@@ -429,7 +443,12 @@
|
||||
<div class="section-header">
|
||||
<h2>{$t('subscription.subscriptions')}</h2>
|
||||
{#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>
|
||||
{$t('add_new')}
|
||||
</button>
|
||||
@@ -488,7 +507,9 @@
|
||||
<div class="button-group">
|
||||
<button
|
||||
class="btn primary"
|
||||
on:click={() => openEditSubscriptionModal(subscription)}
|
||||
on:click={() => {
|
||||
selected = subscription;
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-edit"></i>
|
||||
{$t('edit')}
|
||||
@@ -537,6 +558,112 @@
|
||||
</details>
|
||||
{/each}
|
||||
</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'}
|
||||
<h2>{$t('payments')}</h2>
|
||||
<div class="accordion">
|
||||
@@ -567,28 +694,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showUserModal && selectedUser !== null}
|
||||
{#if selected && 'email' in selected}
|
||||
// user
|
||||
<Modal on:close={close}>
|
||||
<UserEditForm
|
||||
{form}
|
||||
editor={user}
|
||||
user={selectedUser}
|
||||
user={selected}
|
||||
{subscriptions}
|
||||
{licence_categories}
|
||||
on:cancel={close}
|
||||
on:close={close}
|
||||
/>
|
||||
</Modal>
|
||||
{:else if showSubscriptionModal}
|
||||
{:else if selected && 'monthly_fee' in selected}
|
||||
//subscription
|
||||
<Modal on:close={close}>
|
||||
<SubscriptionEditForm
|
||||
{form}
|
||||
{user}
|
||||
subscription={selectedSubscription}
|
||||
subscription={selected}
|
||||
on:cancel={close}
|
||||
on:close={close}
|
||||
/>
|
||||
</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}
|
||||
|
||||
<style>
|
||||
|
||||
Reference in New Issue
Block a user