This commit is contained in:
Alex
2025-04-10 15:40:22 +02:00
parent 87f08dd3be
commit 18f5dadb06
48 changed files with 1650 additions and 981 deletions

View File

@@ -5,8 +5,10 @@
import { applyAction, enhance } from '$app/forms';
import { hasPrivilige, receive, send } from '$lib/utils/helpers';
import { t } from 'svelte-i18n';
import { defaultCar } from '$lib/utils/defaults';
import { defaultDamage, defaultInsurance, defaultOpponent } from '$lib/utils/defaults';
import { PERMISSIONS } from '$lib/utils/constants';
import Modal from './Modal.svelte';
import UserEditForm from './UserEditForm.svelte';
const dispatch = createEventDispatcher();
@@ -16,19 +18,54 @@
/** @type {App.Locals['user'] } */
export let editor;
/** @type {App.Types['car'] | null} */
/** @type {App.Locals['users'] } */
export let users;
/** @type {App.Types['car']} */
export let car;
console.log('Opening car modal with:', car);
$: car = car || { ...defaultCar() };
$: console.log(
'damage.opponent changed:',
car?.damages.map((d) => d.opponent)
);
$: console.log(
'damage.insurance changed:',
car?.damages.map((d) => d.insurance)
);
// TODO: Remove when working
// $: if (car.damages.length > 0 && !car.damages.every((d) => d.insurance && d.opponent)) {
// car.damages = car.damages.map((damage) => ({
// ...damage,
// insurance: damage.insurance ?? defaultInsurance(),
// opponent: damage.opponent ?? defaultOpponent()
// }));
// }
let initialized = false; // Prevents infinite loops
// Ensure damages have default values once `car` is loaded
$: if (car && !initialized) {
car = {
...car,
damages:
car.damages?.map((damage) => ({
...damage,
insurance: damage.insurance ?? defaultInsurance(),
opponent: damage.opponent ?? defaultOpponent()
})) || []
};
initialized = true; // Prevents re-running
}
$: isLoading = car === undefined || editor === undefined;
let isUpdating = false;
let readonlyUser = !hasPrivilige(editor, PERMISSIONS.Update);
/** @type {number | null} */
let editingUserIndex = null;
const TABS = ['car.car', 'insurance', 'car.damages'];
let activeTab = TABS[0];
/** @type {import('../../routes/auth/about/[id]/$types').SubmitFunction} */
/** @type {import('@sveltejs/kit').SubmitFunction} */
const handleUpdate = async () => {
isUpdating = true;
return async ({ result }) => {
@@ -47,7 +84,7 @@
<SmallLoader width={30} message={$t('loading.car_data')} />
{:else if editor && car}
<form class="content" action="?/updateCar" method="POST" use:enhance={handleUpdate}>
<input name="susbscription[id]" type="hidden" bind:value={car.id} />
<input name="car[id]" type="hidden" bind:value={car.id} />
<h1 class="step-title" style="text-align: center;">
{car.id ? $t('car.edit') : $t('car.create')}
</h1>
@@ -155,48 +192,357 @@
/>
</div>
<div class="tab-content" style="display: {activeTab === 'insurance' ? 'block' : 'none'}">
{#each car.insurances as insurance}
<InputField
name="car[insurance][company]"
label={$t('company')}
bind:value={insurance.company}
placeholder={$t('placeholder.company')}
required={true}
readonly={readonlyUser}
/>
<InputField
name="car[insurance][reference]"
label={$t('insurance.reference')}
bind:value={insurance.reference}
placeholder={$t('placeholder.insurance_reference')}
required={true}
readonly={readonlyUser}
/>
<InputField
name="car[insurance][start_date]"
type="date"
label={$t('start')}
bind:value={insurance.start_date}
readonly={readonlyUser}
/>
<InputField
name="car[insurance][end_date]"
type="date"
label={$t('end')}
bind:value={insurance.end_date}
readonly={readonlyUser}
/>
<InputField
name="car[insurance][notes]"
type="textarea"
label={$t('notes')}
bind:value={insurance.notes}
placeholder={$t('placeholder.notes', {
values: { name: insurance.company || '' }
})}
rows={10}
/>
{/each}
<div class="accordion">
{#each car.insurances as insurance, index}
<input hidden value={insurance?.id} name="car[insurances][{index}][id]" />
<details class="accordion-item" open={index === car.insurances.length - 1}>
<summary class="accordion-header">
{insurance.company ? insurance.company : ''}
{insurance.reference ? ' (' + insurance.reference + ')' : ''}
</summary>
<div class="accordion-content">
<InputField
name="car[insurances][{index}][company]"
label={$t('company')}
bind:value={insurance.company}
placeholder={$t('placeholder.company')}
required={true}
readonly={readonlyUser}
/>
<InputField
name="car[insurances][{index}][reference]"
label={$t('insurance_reference')}
bind:value={insurance.reference}
placeholder={$t('placeholder.insurance_reference')}
required={true}
readonly={readonlyUser}
/>
<InputField
name="car[insurances][{index}][start_date]"
type="date"
label={$t('start')}
bind:value={insurance.start_date}
readonly={readonlyUser}
/>
<InputField
name="car[insurances][{index}][end_date]"
type="date"
label={$t('end')}
bind:value={insurance.end_date}
readonly={readonlyUser}
/>
<InputField
name="car[insurances][{index}][notes]"
type="textarea"
label={$t('notes')}
bind:value={insurance.notes}
placeholder={$t('placeholder.notes', {
values: { name: insurance.company || '' }
})}
rows={10}
/>
{#if hasPrivilige(editor, PERMISSIONS.Delete)}
<button
type="button"
class="btn btn-delete danger"
on:click={() => {
if (
confirm(
$t('dialog.insurance_deletion', {
values: {
name: insurance.company + ' (' + insurance.reference + ')'
}
})
)
) {
car.insurances = car.insurances.filter((_, i) => i !== index);
}
}}
>
<i class="fas fa-trash"></i>
{$t('delete')}
</button>
{/if}
</div>
</details>
{/each}
</div>
<div class="button-group">
{#if hasPrivilige(editor, PERMISSIONS.Create)}
<button
type="button"
class="btn primary"
on:click={() => {
car.insurances = [...car.insurances, defaultInsurance()];
}}
>
<i class="fas fa-plus"></i>
{$t('add_new')}
</button>
{/if}
</div>
</div>
<div class="tab-content" style="display: {activeTab === 'car.damages' ? 'block' : 'none'}">
<div class="accordion">
{#each car.damages as damage, index (damage.id)}
<input type="hidden" name="car[damages][{index}][id]" value={damage.id} />
<details class="accordion-item" open={index === car.damages.length - 1}>
<summary class="accordion-header">
<span class="nav-badge">
{damage.name} -
{damage.opponent.first_name}
{damage.opponent.last_name}
</span>
</summary>
<div class="accordion-content">
<InputField
name="car[damages][{index}][date]"
type="date"
label={$t('date')}
bind:value={damage.date}
readonly={readonlyUser}
/>
<InputField
name="car[damages][{index}][name]"
label={$t('car.damages')}
bind:value={damage.name}
required={true}
readonly={readonlyUser}
/>
<InputField
name="car[damages][{index}][driver_id]"
type="select"
label={$t('user.member')}
options={users
?.filter((u) => u.role_id > 0)
.map((u) => ({
value: u.id,
label: `${u.first_name} ${u.last_name}`,
color: '--subtext1'
})) || []}
bind:value={damage.driver_id}
readonly={readonlyUser}
/>
<h4>{$t('user.opponent')}</h4>
<input
hidden
name={`car[damages][${index}][opponent][id]`}
value={car.damages[index].opponent.id}
/>
<input
hidden
name={`car[damages][${index}][opponent][email]`}
value={car.damages[index].opponent.email}
/>
<input
hidden
name={`car[damages][${index}][opponent][first_name]`}
value={car.damages[index].opponent.first_name}
/>
<input
hidden
name={`car[damages][${index}][opponent][last_name]`}
value={damage.opponent.last_name}
/>
<input
hidden
name={`car[damages][${index}][opponent][phone]`}
value={damage.opponent.phone}
/>
<input
hidden
name={`car[damages][${index}][opponent][address]`}
value={damage.opponent.address}
/>
<input
hidden
name={`car[damages][${index}][opponent][city]`}
value={damage.opponent.city}
/>
<input
hidden
name={`car[damages][${index}][opponent][zip_code]`}
value={damage.opponent.zip_code}
/>
<input
hidden
name={`car[damages][${index}][opponent][notes]`}
value={damage.opponent.notes}
/>
<input
hidden
name={`car[damages][${index}][opponent][role_id]`}
value={damage.opponent.role_id}
/>
<input
hidden
name={`car[damages][${index}][opponent][status]`}
value={damage.opponent.status}
/>
<input
hidden
name={`car[damages][${index}][opponent][dateofbirth]`}
value={damage.opponent.dateofbirth}
/>
<input
hidden
name={`car[damages][${index}][opponent][company]`}
value={damage.opponent.company}
/>
<input
hidden
name={`car[damages][${index}][opponent][bank_account][id]`}
value={damage.opponent.bank_account.id}
/>
<input
hidden
name={`car[damages][${index}][opponent][bank_account][mandate_date_signed]`}
value={damage.opponent.bank_account.mandate_date_signed}
/>
<input
hidden
name={`car[damages][${index}][opponent][bank_account][bank]`}
value={damage.opponent.bank_account.bank}
/>
<input
hidden
name={`car[damages][${index}][opponent][bank_account][account_holder_name]`}
value={damage.opponent.bank_account.account_holder_name}
/>
<input
hidden
name={`car[damages][${index}][opponent][bank_account][iban]`}
value={damage.opponent.bank_account.iban}
/>
<input
hidden
name={`car[damages][${index}][opponent][bank_account][bic]`}
value={damage.opponent.bank_account.bic}
/>
<input
hidden
name={`car[damages][${index}][opponent][bank_account][mandate_reference]`}
value={damage.opponent.bank_account.mandate_reference}
/>
<details class="accordion-item">
<summary class="accordion-header">
<span class="nav-badge">
{#if damage.opponent?.first_name}
{damage.opponent.first_name} {damage.opponent.last_name}
{:else}
{$t('not_set')}
{/if}
</span>
</summary>
<div class="accordion-content">
<table class="table">
<tbody>
<tr>
<th>{$t('email')}</th>
<td>{damage.opponent?.email || '-'}</td>
</tr>
<tr>
<th>{$t('phone')}</th>
<td>{damage.opponent?.phone || '-'}</td>
</tr>
<tr>
<th>{$t('address')}</th>
<td>{damage.opponent?.address || '-'}</td>
</tr>
<tr>
<th>{$t('city')}</th>
<td>{damage.opponent?.city || '-'}</td>
</tr>
<tr>
<th>{$t('zip_code')}</th>
<td>{damage.opponent?.zip_code || '-'}</td>
</tr>
</tbody>
</table>
<div class="button-group">
<button
type="button"
class="btn primary"
on:click={() => {
if (!damage.opponent) {
damage.opponent = defaultOpponent();
}
editingUserIndex = index;
}}
>
<i class="fas fa-edit"></i>
{damage.opponent?.id ? $t('edit') : $t('edit')}
</button>
</div>
</div>
</details>
<input
hidden
name={`car[damages][${index}][insurance][id]`}
value={damage.insurance.id}
/>
<input hidden name={`car[damages][${index}][insurance][start_date]`} value="" />
<input hidden name={`car[damages][${index}][insurance][end_date]`} value="" />
<InputField
name="car[damages][{index}][insurance][company]"
label={$t('insurance')}
bind:value={damage.insurance.company}
placeholder={$t('placeholder.company')}
readonly={readonlyUser}
/>
<InputField
name="car[damages][{index}][insurance][reference]"
label={$t('insurance_reference')}
bind:value={damage.insurance.reference}
placeholder={$t('placeholder.insurance_reference')}
readonly={readonlyUser}
/>
<InputField
name="car[damages][{index}][notes]"
type="textarea"
label={$t('notes')}
bind:value={damage.notes}
placeholder={$t('placeholder.notes')}
rows={10}
/>
{#if hasPrivilige(editor, PERMISSIONS.Delete)}
<button
type="button"
class="btn btn-delete danger"
on:click={() => {
if (
confirm(
$t('dialog.damage_deletion', {
values: {
name: damage.name
}
})
)
) {
car.damages = car.damages.filter((_, i) => i !== index);
}
}}
>
<i class="fas fa-trash"></i>
{$t('delete')}
</button>
{/if}
</div>
</details>
{/each}
</div>
{#if hasPrivilige(editor, PERMISSIONS.Create)}
<button
type="button"
class="btn primary"
on:click={() => {
car.damages = [...car.damages, defaultDamage()];
}}
>
<i class="fas fa-plus"></i>
{$t('add_new')}
</button>
{/if}
</div>
<div class="button-container">
{#if isUpdating}
@@ -211,7 +557,81 @@
</form>
{/if}
{#if editingUserIndex !== null}
<Modal on:close={close}>
<UserEditForm
{form}
submit_form={false}
subscriptions={null}
licence_categories={null}
{editor}
bind:user={car.damages[editingUserIndex].opponent}
on:cancel={() => (editingUserIndex = null)}
on:close={() => {
car.damages = car.damages;
editingUserIndex = null;
}}
/>
</Modal>
{/if}
<style>
.accordion-item {
border: none;
background: var(--surface0);
margin-bottom: 0.5rem;
border-radius: 8px;
overflow: hidden;
}
.accordion-header {
display: flex;
padding: 1rem;
cursor: pointer;
font-family: 'Roboto Mono', monospace;
color: var(--text);
background: var(--surface1);
transition: background-color 0.2s ease-in-out;
}
.accordion-header:hover {
background: var(--surface2);
}
.accordion-content {
padding: 1rem;
background: var(--surface0);
border-top: 1px solid var(--surface1);
}
.accordion-content .table {
width: 100%;
border-collapse: collapse;
font-family: 'Roboto Mono', monospace;
}
.accordion-content .table th,
.accordion-content .table td {
padding: 0.75rem;
border-bottom: 1px solid #2f2f2f;
text-align: left;
}
.accordion-content .table th {
color: var(--subtext1);
}
.accordion-content .table td {
color: var(--text);
}
.button-container button.active {
background-color: var(--mauve);
border-color: var(--mauve);
color: var(--base);
}
.btn-delete {
margin-left: auto;
}
.tab-content {
padding: 1rem;
border-radius: 0 0 3px 3px;
@@ -219,6 +639,16 @@
border: 1px solid var(--surface1);
margin-top: 1rem;
}
.tab-content h4 {
text-align: center;
padding: 0.75rem;
margin: 1rem 0;
color: var(--lavender);
font-family: 'Roboto Mono', monospace;
font-weight: 500;
letter-spacing: 0.5px;
}
.button-container {
display: flex;
justify-content: space-between;

View File

@@ -50,9 +50,9 @@
let inputValue = target.value;
if (toUpperCase) {
inputValue = inputValue.toUpperCase();
target.value = inputValue; // Update the input field value
}
value = inputValue;
target.value = inputValue; // Update the input field value
value = inputValue.trim();
}
}

View File

@@ -44,7 +44,7 @@
<form class="content" action="?/updateSubscription" method="POST" use:enhance={handleUpdate}>
<input name="susbscription[id]" type="hidden" bind:value={subscription.id} />
<h1 class="step-title" style="text-align: center;">
{subscription.id ? $t('subscription.edit') : $t('subscription.create')}
{subscription.id ? $t('subscriptions.edit') : $t('subscriptions.create')}
</h1>
{#if form?.errors}
{#each form?.errors as error (error.id)}
@@ -60,7 +60,7 @@
<div class="tab-content" style="display: block">
<InputField
name="subscription[name]"
label={$t('subscription.name')}
label={$t('subscriptions.name')}
bind:value={subscription.name}
placeholder={$t('placeholder.subscription_name')}
required={true}
@@ -77,7 +77,7 @@
<InputField
name="subscription[conditions]"
type="textarea"
label={$t('subscription.conditions')}
label={$t('subscriptions.conditions')}
bind:value={subscription.conditions}
placeholder={$t('placeholder.subscription_conditions')}
readonly={subscription.id > 0}
@@ -85,7 +85,7 @@
<InputField
name="subscription[monthly_fee]"
type="number"
label={$t('subscription.monthly_fee')}
label={$t('subscriptions.monthly_fee')}
bind:value={subscription.monthly_fee}
placeholder={$t('placeholder.subscription_monthly_fee')}
required={true}
@@ -94,7 +94,7 @@
<InputField
name="subscription[hourly_rate]"
type="number"
label={$t('subscription.hourly_rate')}
label={$t('subscriptions.hourly_rate')}
bind:value={subscription.hourly_rate}
required={true}
readonly={subscription.id > 0}
@@ -102,14 +102,14 @@
<InputField
name="subscription[included_hours_per_year]"
type="number"
label={$t('subscription.included_hours_per_year')}
label={$t('subscriptions.included_hours_per_year')}
bind:value={subscription.included_hours_per_year}
readonly={subscription.id > 0}
/>
<InputField
name="included_hours_per_month"
type="number"
label={$t('subscription.included_hours_per_month')}
label={$t('subscriptions.included_hours_per_month')}
bind:value={subscription.included_hours_per_month}
readonly={subscription.id > 0}
/>

View File

@@ -6,7 +6,7 @@
import { hasPrivilige, receive, send } from '$lib/utils/helpers';
import { t } from 'svelte-i18n';
import { PERMISSIONS } from '$lib/utils/constants';
import { defaultLicence } from '$lib/utils/defaults';
// import { defaultBankAccount, defaultLicence, defaultMembership } from '$lib/utils/defaults';
/** @type {import('../../routes/auth/about/[id]/$types').ActionData} */
export let form;
@@ -20,10 +20,15 @@
export let submit_form = true;
// Ensure licence is initialized before passing to child
$: if (user && !user.licence) {
user.licence = defaultLicence();
}
// $: if (user && !user.licence) {
// user.licence = defaultLicence();
// }
// $: if (user && !user.membership) {
// user.membership = defaultMembership();
// }
// $: if (user && !user.bank_account) {
// user.bank_account = defaultBankAccount();
// }
/** @type {App.Locals['user']} */
export let editor;
@@ -31,7 +36,9 @@
// $: isNewUser = user === null;
$: isLoading = user === undefined;
$: if (user != null) {
console.log(user);
}
/** @type {App.Locals['licence_categories'] | null} */
export let licence_categories;
@@ -43,6 +50,7 @@
{ value: 5, label: $t('userStatus.5'), color: '--red' } // Red for "Deaktiviert"
];
const userRoleOptions = [
{ value: -1, label: $t('userRole.-1'), color: '--red' }, // Red for "Opponent"
{ value: 0, label: $t('userRole.0'), color: '--subtext1' }, // Grey for "Nicht verifiziert"
{ value: 1, label: $t('userRole.1'), color: '--light-green' }, // Light green for "Verifiziert"
{ value: 2, label: $t('userRole.2'), color: '--green' }, // Light green for "Verifiziert"
@@ -62,14 +70,10 @@
];
const dispatch = createEventDispatcher();
const TABS = [
'profile',
'bankaccount',
...(hasPrivilige(user, PERMISSIONS.Member) ? 'membership' : []),
...(user.licence ? 'licence' : [])
];
/** @type { (keyof user)[] } */
const TABS = ['membership', 'licence', 'bank_account'];
let activeTab = TABS[0];
let activeTab = 'profile';
let isUpdating = false,
password = '',
@@ -77,13 +81,13 @@
/** @type {Object.<string, App.Locals['licence_categories']>} */
$: groupedCategories = licence_categories ? groupCategories(licence_categories) : {};
$: subscriptionModelOptions = subscriptions
$: subscriptionOptions = subscriptions
? subscriptions.map((sub) => ({
value: sub?.name ?? '',
label: sub?.name ?? ''
}))
: [];
$: selectedSubscriptionModel = subscriptions
$: selectedSubscription = subscriptions
? subscriptions.find((sub) => sub?.name === user.membership?.subscription.name) || null
: null;
/**
@@ -172,26 +176,38 @@
{/if}
<div class="button-container">
<button
type="button"
class="button-dark"
class:active={activeTab === 'profile'}
on:click={() => (activeTab = 'profile')}
>
{$t('profile')}
</button>
{#each TABS as tab}
<button
type="button"
class="button-dark"
class:active={activeTab === tab}
on:click={() => (activeTab = tab)}
>
{$t(tab)}
</button>
{#if user[tab] != null}
<button
type="button"
class="button-dark"
class:active={activeTab === tab}
on:click={() => (activeTab = tab)}
>
{$t('user.' + tab)}
</button>
{/if}
{/each}
</div>
<div class="tab-content" style="display: {activeTab === 'profile' ? 'block' : 'none'}">
<InputField
name="user[status]"
type="select"
label={$t('status')}
bind:value={user.status}
options={userStatusOptions}
readonly={readonlyUser}
/>
{#if hasPrivilige(user, PERMISSIONS.Member)}
<InputField
name="user[status]"
type="select"
label={$t('status')}
bind:value={user.status}
options={userStatusOptions}
readonly={readonlyUser}
/>
{/if}
{#if hasPrivilige(editor, PERMISSIONS.Super)}
<InputField
name="user[role_id]"
@@ -367,133 +383,137 @@
</div>
</div>
{/if}
<div
class="tab-content"
style="display: {activeTab === 'membership' && subscriptions ? 'block' : 'none'}"
>
<InputField
name="user[membership][status]"
type="select"
label={$t('status')}
bind:value={user.membership.status}
options={membershipStatusOptions}
readonly={readonlyUser}
/>
<InputField
name="user[membership][subscription][name]"
type="select"
label={$t('subscriptions.subscription')}
bind:value={user.membership.subscription.name}
options={subscriptionModelOptions}
readonly={readonlyUser || !hasPrivilige(user, PERMISSIONS.Member)}
/>
<div class="subscription-info">
{#if hasPrivilige(user, PERMISSIONS.Member)}
{#if user.membership}
<div
class="tab-content"
style="display: {activeTab === 'membership' && subscriptions ? 'block' : 'none'}"
>
<InputField
name="user[membership][status]"
type="select"
label={$t('status')}
bind:value={user.membership.status}
options={membershipStatusOptions}
readonly={readonlyUser}
/>
<InputField
name="user[membership][subscription][name]"
type="select"
label={$t('subscriptions.subscription')}
bind:value={user.membership.subscription.name}
options={subscriptionOptions}
readonly={readonlyUser || !hasPrivilige(user, PERMISSIONS.Member)}
/>
<div class="subscription-info">
{#if hasPrivilige(user, PERMISSIONS.Member)}
<div class="subscription-column">
<p>
<strong>{$t('subscriptions.monthly_fee')}:</strong>
{selectedSubscription?.monthly_fee || '-'}
</p>
<p>
<strong>{$t('subscriptions.hourly_rate')}:</strong>
{selectedSubscription?.hourly_rate || '-'}
</p>
{#if selectedSubscription?.included_hours_per_year}
<p>
<strong>{$t('subscriptions.included_hours_per_year')}:</strong>
{selectedSubscription?.included_hours_per_year}
</p>
{/if}
{#if selectedSubscription?.included_hours_per_month}
<p>
<strong>{$t('subscriptions.included_hours_per_month')}:</strong>
{selectedSubscription?.included_hours_per_month}
</p>
{/if}
</div>
{/if}
<div class="subscription-column">
<p>
<strong>{$t('subscriptions.monthly_fee')}:</strong>
{selectedSubscriptionModel?.monthly_fee || '-'}
<strong>{$t('details')}:</strong>
{selectedSubscription?.details || '-'}
</p>
<p>
<strong>{$t('subscriptions.hourly_rate')}:</strong>
{selectedSubscriptionModel?.hourly_rate || '-'}
</p>
{#if selectedSubscriptionModel?.included_hours_per_year}
{#if selectedSubscription?.conditions}
<p>
<strong>{$t('subscriptions.included_hours_per_year')}:</strong>
{selectedSubscriptionModel?.included_hours_per_year}
</p>
{/if}
{#if selectedSubscriptionModel?.included_hours_per_month}
<p>
<strong>{$t('subscriptions.included_hours_per_month')}:</strong>
{selectedSubscriptionModel?.included_hours_per_month}
<strong>{$t('subscriptions.conditions')}:</strong>
{selectedSubscription?.conditions}
</p>
{/if}
</div>
{/if}
<div class="subscription-column">
<p>
<strong>{$t('details')}:</strong>
{selectedSubscriptionModel?.details || '-'}
</p>
{#if selectedSubscriptionModel?.conditions}
<p>
<strong>{$t('subscriptions.conditions')}:</strong>
{selectedSubscriptionModel?.conditions}
</p>
{/if}
</div>
</div>
<InputField
name="user[membership][start_date]"
type="date"
label={$t('start')}
bind:value={user.membership.start_date}
placeholder={$t('placeholder.start_date')}
readonly={readonlyUser}
/>
<InputField
name="user[membership][end_date]"
type="date"
label={$t('end')}
bind:value={user.membership.end_date}
placeholder={$t('placeholder.end_date')}
readonly={readonlyUser}
/>
{#if hasPrivilige(user, PERMISSIONS.Member)}
<InputField
name="user[membership][parent_member_id]"
type="number"
label={$t('parent_member_id')}
bind:value={user.membership.parent_member_id}
placeholder={$t('placeholder.parent_member_id')}
name="user[membership][start_date]"
type="date"
label={$t('start')}
bind:value={user.membership.start_date}
placeholder={$t('placeholder.start_date')}
readonly={readonlyUser}
/>
{/if}
</div>
<div class="tab-content" style="display: {activeTab === 'bankaccount' ? 'block' : 'none'}">
<InputField
name="user[bank_account][account_holder_name]"
label={$t('bank_account_holder')}
bind:value={user.bank_account.account_holder_name}
placeholder={$t('placeholder.bank_account_holder')}
/>
<InputField
name="user[bank_account][bank_name]"
label={$t('bank_name')}
bind:value={user.bank_account.bank}
placeholder={$t('placeholder.bank_name')}
/>
<InputField
name="user[bank_account][iban]"
label={$t('iban')}
bind:value={user.bank_account.iban}
placeholder={$t('placeholder.iban')}
toUpperCase={true}
/>
<InputField
name="user[bank_account][bic]"
label={$t('bic')}
bind:value={user.bank_account.bic}
placeholder={$t('placeholder.bic')}
toUpperCase={true}
/>
<InputField
name="user[bank_account][mandate_reference]"
label={$t('mandate_reference')}
bind:value={user.bank_account.mandate_reference}
placeholder={$t('placeholder.mandate_reference')}
readonly={readonlyUser}
/>
<InputField
name="user[bank_account][mandate_date_signed]"
label={$t('mandate_date_signed')}
type="date"
bind:value={user.bank_account.mandate_date_signed}
readonly={true}
/>
</div>
<InputField
name="user[membership][end_date]"
type="date"
label={$t('end')}
bind:value={user.membership.end_date}
placeholder={$t('placeholder.end_date')}
readonly={readonlyUser}
/>
{#if hasPrivilige(user, PERMISSIONS.Member)}
<InputField
name="user[membership][parent_member_id]"
type="number"
label={$t('parent_member_id')}
bind:value={user.membership.parent_member_id}
placeholder={$t('placeholder.parent_member_id')}
readonly={readonlyUser}
/>
{/if}
</div>
{/if}
{#if user.bank_account}
<div class="tab-content" style="display: {activeTab === 'bank_account' ? 'block' : 'none'}">
<InputField
name="user[bank_account][account_holder_name]"
label={$t('bank_account_holder')}
bind:value={user.bank_account.account_holder_name}
placeholder={$t('placeholder.bank_account_holder')}
/>
<InputField
name="user[bank_account][bank_name]"
label={$t('bank_name')}
bind:value={user.bank_account.bank}
placeholder={$t('placeholder.bank_name')}
/>
<InputField
name="user[bank_account][iban]"
label={$t('iban')}
bind:value={user.bank_account.iban}
placeholder={$t('placeholder.iban')}
toUpperCase={true}
/>
<InputField
name="user[bank_account][bic]"
label={$t('bic')}
bind:value={user.bank_account.bic}
placeholder={$t('placeholder.bic')}
toUpperCase={true}
/>
<InputField
name="user[bank_account][mandate_reference]"
label={$t('mandate_reference')}
bind:value={user.bank_account.mandate_reference}
placeholder={$t('placeholder.mandate_reference')}
readonly={readonlyUser}
/>
<InputField
name="user[bank_account][mandate_date_signed]"
label={$t('mandate_date_signed')}
type="date"
bind:value={user.bank_account.mandate_date_signed}
readonly={true}
/>
</div>
{/if}
<div class="button-container">
{#if isUpdating}
<SmallLoader width={30} message={$t('loading.updating')} />

View File

@@ -7,7 +7,7 @@ export default {
5: 'Passiv'
},
userRole: {
'-5': 'Unfallgegner',
'-1': 'Unfallgegner',
0: 'Sponsor',
1: 'Mitglied',
2: 'Betrachter',
@@ -131,6 +131,7 @@ export default {
edit: 'Nutzer bearbeiten',
create: 'Nutzer erstellen',
user: 'Nutzer',
member: 'Mitglied',
management: 'Mitgliederverwaltung',
id: 'Mitgliedsnr',
first_name: 'Vorname',
@@ -138,9 +139,12 @@ export default {
phone: 'Telefonnummer',
dateofbirth: 'Geburtstag',
email: 'Email',
membership: 'Mitgliedschaft',
bank_account: 'Kontodaten',
status: 'Status',
role: 'Nutzerrolle',
supporter: 'Sponsor'
supporter: 'Sponsor',
opponent: 'Unfallgegner'
},
subscriptions: {
name: 'Modellname',
@@ -186,8 +190,11 @@ export default {
actions: 'Aktionen',
edit: 'Bearbeiten',
delete: 'Löschen',
not_set: 'Nicht gesetzt',
noone: 'Niemand',
search: 'Suche:',
name: 'Name',
date: 'Datum',
price: 'Preis',
color: 'Farbe',
grant_backend_access: 'Backend Zugriff gewähren',
@@ -221,8 +228,6 @@ export default {
login: 'Anmeldung',
profile: 'Profil',
cars: 'Fahrzeuge',
membership: 'Mitgliedschaft',
bankaccount: 'Kontodaten',
status: 'Status',
start: 'Beginn',
end: 'Ende',

View File

@@ -182,7 +182,7 @@ export default {
login: 'Login',
profile: 'Profile',
membership: 'Membership',
bankaccount: 'Bank Account',
bank_account: 'Bank Account',
status: 'Status',
start: 'Start',
end: 'End',

View File

@@ -10,6 +10,3 @@ export const PERMISSIONS = {
Delete: 4,
Super: 8
};
export const SUPPORTER_SUBSCRIPTION_NAME = 'Keins';
export const OPPONENT_SUBSCRIPTION_NAME = 'Keins';

View File

@@ -1,7 +1,5 @@
// src/lib/utils/defaults.js
import { OPPONENT_SUBSCRIPTION_NAME, SUPPORTER_SUBSCRIPTION_NAME } from './constants';
/**
* @returns {App.Types['subscription']}
*/
@@ -79,8 +77,6 @@ export function defaultUser() {
company: '',
dateofbirth: '',
notes: '',
profile_picture: '',
payment_status: 0,
status: 1,
role_id: 1,
membership: defaultMembership(),
@@ -97,7 +93,7 @@ export function defaultSupporter() {
supporter.status = 5;
supporter.role_id = 0;
supporter.licence = null;
supporter.membership.subscription.name = SUPPORTER_SUBSCRIPTION_NAME;
supporter.membership = null;
return supporter;
}
@@ -109,7 +105,7 @@ export function defaultOpponent() {
opponent.status = 5;
opponent.role_id = -1;
opponent.licence = null;
opponent.membership.subscription.name = OPPONENT_SUBSCRIPTION_NAME;
opponent.membership = null;
return opponent;
}
/**
@@ -128,8 +124,11 @@ export function defaultLocation() {
export function defaultDamage() {
return {
id: 0,
opponent: defaultUser(),
insurance: null,
name: '',
opponent: defaultOpponent(),
driver_id: -1,
insurance: defaultInsurance(),
date: '',
notes: ''
};
}
@@ -155,7 +154,7 @@ export function defaultCar() {
return {
id: 0,
name: '',
status: '',
status: 0,
brand: '',
model: '',
price: 0,

View File

@@ -72,7 +72,7 @@ export function isEmpty(obj) {
* @returns string
*/
export function toRFC3339(dateString) {
if (!dateString) dateString = '0001-01-01T00:00:00.000Z';
if (!dateString || dateString == '') dateString = '0001-01-01T00:00:00.000Z';
const date = new Date(dateString);
return date.toISOString();
}

View File

@@ -1,3 +1,4 @@
import { defaultBankAccount, defaultMembership } from './defaults';
import { toRFC3339 } from './helpers';
/**
@@ -24,20 +25,18 @@ export function formDataToObject(formData) {
// console.log('Current object state:', JSON.stringify(current));
for (let i = 0; i < keys.length - 1; i++) {
/**
* Create nested object if it doesn't exist
* @type {Record<string, any>}
* @description Ensures proper nesting structure for user data fields
* @example
* // For input name="user[membership][status]"
* // Creates: { user: { membership: { status: value } } }
*/
current[keys[i]] = current[keys[i]] || {};
const currentKey = keys[i];
const nextKey = keys[i + 1];
const isNextKeyArrayIndex = !isNaN(Number(nextKey));
if (!current[currentKey]) {
// If next key is a number, initialize an array, otherwise an object
current[currentKey] = isNextKeyArrayIndex ? [] : {};
}
/**
* Move to the next level of the object
* @type {Record<string, any>}
*/
current = current[keys[i]];
current = current[currentKey];
}
const lastKey = keys[keys.length - 1];
@@ -50,7 +49,20 @@ export function formDataToObject(formData) {
current[lastKey].push(value);
}
} else {
current[lastKey] = value;
if (Array.isArray(current)) {
// If current is an array, lastKey should be the index
const index = parseInt(lastKey);
current[index] = current[index] || {};
if (keys.length > 2) {
// For nested properties within array elements
const propertyKey = keys[keys.length - 1];
current[index][propertyKey] = value;
} else {
current[index] = value;
}
} else {
current[lastKey] = value;
}
}
}
@@ -58,9 +70,9 @@ export function formDataToObject(formData) {
}
/**
* Processes the raw form data into the expected user data structure
* @param {{ object: Partial<App.Locals['user']>, confirm_password: string} } rawData - The raw form data object
* @returns {{ user: Partial<App.Locals['user']> }} Processed user data
* Processes the raw form data into the expected membership data structure
* @param { App.Types['membership'] } membership - The raw form data object
* @returns {App.Types['membership']} Processed membership data
*/
export function processMembershipFormData(membership) {
return {
@@ -73,39 +85,71 @@ export function processMembershipFormData(membership) {
};
}
licence: {
id: Number(rawData.object.licence?.id) || 0,
status: Number(rawData.object.licence?.status),
number: String(rawData.object.licence?.number || ''),
issued_date: toRFC3339(String(rawData.object.licence?.issued_date || '')),
expiration_date: toRFC3339(String(rawData.object.licence?.expiration_date || '')),
country: String(rawData.object.licence?.country || ''),
categories: rawData.object.licence?.categories || []
},
/**
* Processes the raw form data into the expected licence data structure
* @param { App.Types['licence'] } licence - The raw form data object
* @returns {App.Types['licence']} Processed licence data
*/
export function processLicenceFormData(licence) {
return {
id: Number(licence?.id) || 0,
status: Number(licence?.status),
number: String(licence?.number || ''),
issued_date: toRFC3339(String(licence?.issued_date || '')),
expiration_date: toRFC3339(String(licence?.expiration_date || '')),
country: String(licence?.country || ''),
categories: licence?.categories || []
};
}
bank_account: {
id: Number(rawData.object.bank_account?.id) || 0,
account_holder_name: String(rawData.object.bank_account?.account_holder_name || ''),
bank: String(rawData.object.bank_account?.bank || ''),
iban: String(rawData.object.bank_account?.iban || ''),
bic: String(rawData.object.bank_account?.bic || ''),
mandate_reference: String(rawData.object.bank_account?.mandate_reference || ''),
mandate_date_signed: toRFC3339(
String(rawData.object.bank_account?.mandate_date_signed || '')
)
}
}
/**
* Processes the raw form data into the expected bank_account data structure
* @param { App.Types['bankAccount'] } bank_account - The raw form data object
* @returns {App.Types['bankAccount']} Processed bank_account data
*/
export function processBankAccountFormData(bank_account) {
{
return {
id: Number(bank_account?.id) || 0,
account_holder_name: String(bank_account?.account_holder_name || ''),
bank: String(bank_account?.bank || ''),
iban: String(bank_account?.iban || ''),
bic: String(bank_account?.bic || ''),
mandate_reference: String(bank_account?.mandate_reference || ''),
mandate_date_signed: toRFC3339(String(bank_account?.mandate_date_signed || ''))
};
}
}
/**
* Processes the raw form data into the expected user data structure
* @param { Partial<App.Locals['user']> } user - The raw form data object
* @returns {App.Locals['user']} Processed user data
*/
export function processUserFormData(user) {
/** @type {App.Locals['user']} */
let processedData = {
id: Number(user.id) || 0,
status: Number(user.status),
role_id: Number(user.role_id),
first_name: String(user.first_name),
last_name: String(user.last_name),
password: String(user.password) || '',
email: String(user.email),
phone: String(user.phone || ''),
company: String(user.company || ''),
dateofbirth: toRFC3339(String(user.dateofbirth || '')),
address: String(user.address || ''),
zip_code: String(user.zip_code || ''),
city: String(user.city || ''),
notes: String(user.notes || ''),
membership: processMembershipFormData(user.membership ? user.membership : defaultMembership()),
licence: user.licence ? processLicenceFormData(user.licence) : null,
bank_account: processBankAccountFormData(
user.bank_account ? user.bank_account : defaultBankAccount()
)
};
// console.log('Categories: --------');
// console.dir(rawData.object.licence);
if (
rawData.object.password &&
rawData.confirm_password &&
rawData.object.password === rawData.confirm_password &&
rawData.object.password.trim() !== ''
) {
processedData.user.password = rawData.object.password;
}
const clean = JSON.parse(JSON.stringify(processedData), (key, value) =>
value !== null && value !== '' ? value : undefined
);
@@ -136,39 +180,85 @@ export function processSubscriptionFormData(subscription) {
console.dir(clean);
return clean;
}
/**
* Processes the raw form data into the expected insurance data structure
* @param {App.Types['insurance']} insurance - The raw form data object
* @returns {App.Types['insurance']} Processed user data
*/
export function processInsuranceFormData(insurance) {
return {
id: Number(insurance.id) || 0,
company: String(insurance.company) || '',
reference: String(insurance.reference) || '',
start_date: toRFC3339(String(insurance.start_date) || '') || '',
end_date: toRFC3339(String(insurance.end_date) || '') || '',
notes: String(insurance.notes) || ''
};
}
/**
* Processes the raw form data into the expected car data structure
* @param {{ object: Partial<App.Types['car']>, confirm_password: string }} rawData - The raw form data object
* @returns {{ car: Partial<App.Types['car']> }} Processed user data
* @param {Partial<App.Types['car']>} car - The raw form data object
* @returns {App.Types['car']} Processed user data
*/
export function processCarFormData(rawData) {
/** @type {{ car: Partial<App.Types['car']> }} */
export function processCarFormData(car) {
console.dir(car);
/** @type {App.Types['car']} */
let processedData = {
car: {
id: Number(rawData.object.id) || 0,
name: String(rawData.object.name) || '',
status: Number(rawData.object.status) || 0,
brand: String(rawData.object.brand) || '',
model: String(rawData.object.model) || '',
price: Number(rawData.object.price) || 0,
rate: Number(rawData.object.rate) || 0,
licence_plate: String(rawData.object.licence_plate) || '',
start_date: toRFC3339(String(rawData.object.start_date)) || '',
end_date: toRFC3339(String(rawData.object.end_date)) || '',
color: String(rawData.object.color) || '',
notes: String(rawData.object.notes) || '',
location: {
latitude: Number(rawData.object.location?.latitude) || 0,
longitude: Number(rawData.object.location?.longitude) || 0
},
damages: rawData.object.damages || [],
insurances: rawData.object.insurances || []
}
id: Number(car.id) || 0,
name: String(car.name) || '',
status: Number(car.status) || 0,
brand: String(car.brand) || '',
model: String(car.model) || '',
price: Number(car.price) || 0,
rate: Number(car.rate) || 0,
licence_plate: String(car.licence_plate),
start_date: 'start_date' in car ? toRFC3339(String(car.start_date) || '') : '',
end_date: 'end_date' in car ? toRFC3339(String(car.end_date) || '') : '',
color: String(car.color) || '',
notes: String(car.notes) || '',
location:
'location' in car
? {
latitude: Number(car.location?.latitude) || 0,
longitude: Number(car.location?.longitude) || 0
}
: {
latitude: 0,
longitude: 0
},
damages: /** @type {App.Types['damage'][]} */ ([]),
insurances: /** @type {App.Types['insurance'][]} */ ([])
};
car.insurances?.forEach((insurance) => {
processedData.insurances.push(processInsuranceFormData(insurance));
});
car.damages?.forEach((damage) => {
console.dir(damage);
processedData.damages.push(processDamageFormData(damage));
});
const clean = JSON.parse(JSON.stringify(processedData), (key, value) =>
value !== null && value !== '' ? value : undefined
);
console.dir(clean);
return clean;
}
/**
* Processes the raw form data into the expected damage data structure
* @param { App.Types['damage'] } damage - The raw form data object
* @returns {App.Types['damage']} Processed damage data
*/
export function processDamageFormData(damage) {
return {
id: Number(damage.id) || 0,
name: String(damage.name) || '',
opponent: processUserFormData(damage.opponent),
driver_id: Number(damage.driver_id) || 0,
insurance: processInsuranceFormData(damage.insurance),
date: toRFC3339(String(damage.date) || ''),
notes: String(damage.notes) || ''
};
}