frontend:locale & subscription handling

This commit is contained in:
Alex
2025-02-11 13:27:14 +01:00
parent 447f149423
commit 2492f410b1
8 changed files with 307 additions and 150 deletions

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

@@ -3,12 +3,12 @@
interface Subscription { interface Subscription {
id: number | -1; id: number | -1;
name: string | ''; name: string | '';
details?: string | ''; details: string | '';
conditions?: string | ''; conditions: string | '';
monthly_fee?: number | -1; monthly_fee: number | 0;
hourly_rate?: number | -1; hourly_rate: number | 0;
included_hours_per_year?: number | 0; included_hours_per_year: number | 0;
included_hours_per_month?: number | 0; included_hours_per_month: number | 0;
} }
interface Membership { interface Membership {
@@ -79,6 +79,7 @@ declare global {
} }
interface Types { interface Types {
licenceCategory: LicenceCategory; licenceCategory: LicenceCategory;
subscription: Subscription;
} }
// interface PageData {} // interface PageData {}
// interface Platform {} // interface Platform {}

View File

@@ -0,0 +1,180 @@
<script>
import InputField from '$lib/components/InputField.svelte';
import SmallLoader from '$lib/components/SmallLoader.svelte';
import { createEventDispatcher } from 'svelte';
import { applyAction, enhance } from '$app/forms';
import { receive, send } from '$lib/utils/helpers';
import { t } from 'svelte-i18n';
const dispatch = createEventDispatcher();
/** @type {import('../../routes/auth/about/[id]/$types').ActionData} */
export let form;
/** @type {App.Locals['user'] } */
export let user;
/** @type {App.Types['subscription'] | null} */
export let subscription;
/** @type {App.Types['subscription']} */
const blankSubscription = {
id: 0,
name: '',
details: '',
conditions: '',
monthly_fee: 0,
hourly_rate: 0,
included_hours_per_year: 0,
included_hours_per_month: 0
};
$: {
if (subscription !== undefined) {
subscription = subscription === null ? { ...blankSubscription } : { ...subscription };
}
}
$: isLoading = subscription === undefined || user === undefined;
let isUpdating = false;
/** @type {import('../../routes/auth/about/[id]/$types').SubmitFunction} */
const handleUpdate = async () => {
isUpdating = true;
return async ({ result }) => {
isUpdating = false;
if (result.type === 'success' || result.type === 'redirect') {
dispatch('close');
} else {
document.querySelector('.modal .container')?.scrollTo({ top: 0, behavior: 'smooth' });
}
await applyAction(result);
};
};
</script>
{#if isLoading}
<SmallLoader width={30} message={$t('loading.subscription_data')} />
{:else if user && subscription}
<form class="content" action="?/updateSubscription" method="POST" use:enhance={handleUpdate}>
<input name="usbscription[id]" type="hidden" bind:value={subscription.id} />
<h1 class="step-title" style="text-align: center;">{$t('subscritption.edit')}</h1>
{#if form?.errors}
{#each form?.errors as error (error.id)}
<h4
class="step-subtitle warning"
in:receive|global={{ key: error.id }}
out:send|global={{ key: error.id }}
>
{$t(error.field) + ': ' + $t(error.key)}
</h4>
{/each}
{/if}
<div class="tab-content" style="display: block">
<InputField
name="subscription[name]"
label={$t('subscription.name')}
bind:value={subscription.name}
placeholder={$t('placeholder.subscription_name')}
required={true}
readonly={user.role_id < 8}
/>
<InputField
name="subscription[details]"
label={$t('details')}
type="textarea"
bind:value={subscription.details}
placeholder={$t('placeholder.subscription_details')}
required={true}
/>
<InputField
name="subscription[conditions]"
type="textarea"
label={$t('subscription.conditions')}
bind:value={subscription.conditions}
placeholder={$t('placeholder.subscription_conditions')}
readonly={user.role_id < 8}
/>
<InputField
name="subscription[monthly_fee]"
type="number"
label={$t('subscription.monthly_fee')}
bind:value={subscription.monthly_fee}
placeholder={$t('placeholder.subscription_monthly_fee')}
required={true}
readonly={user.role_id < 8}
/>
<InputField
name="subscription[hourly_rate]"
type="number"
label={$t('subscription.hourly_rate')}
bind:value={subscription.hourly_rate}
required={true}
readonly={user.role_id < 8}
/>
<InputField
name="subscription[included_hours_per_year]"
type="number"
label={$t('subscription.included_hours_per_year')}
bind:value={subscription.included_hours_per_year}
readonly={user.role_id < 8}
/>
<InputField
name="included_hours_per_month"
type="number"
label={$t('subscription.included_hours_per_month')}
bind:value={subscription.included_hours_per_month}
readonly={user.role_id < 8}
/>
</div>
<div class="button-container">
{#if isUpdating}
<SmallLoader width={30} message={'Aktualisiere...'} />
{:else}
<button type="button" class="button-dark" on:click={() => dispatch('cancel')}>
{$t('cancel')}</button
>
<button type="submit" class="button-dark">{$t('confirm')}</button>
{/if}
</div>
</form>
{/if}
<style>
.tab-content {
padding: 1rem;
border-radius: 0 0 3px 3px;
background-color: var(--surface0);
border: 1px solid var(--surface1);
margin-top: 1rem;
}
.button-container {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
margin-top: 1rem;
width: 100%;
}
.button-container button {
flex: 1 1 0;
min-width: 120px;
max-width: calc(50% - 5px);
background-color: var(--surface1);
color: var(--text);
border: 1px solid var(--overlay0);
transition: all 0.2s ease-in-out;
}
.button-container button:hover {
background-color: var(--surface2);
border-color: var(--lavender);
}
@media (max-width: 480px) {
.button-container button {
flex-basis: 100%;
max-width: none;
}
}
</style>

View File

@@ -38,7 +38,13 @@
parent_member_id: 0, parent_member_id: 0,
subscription_model: { subscription_model: {
id: 0, id: 0,
name: '' name: '',
details: '',
conditions: '',
monthly_fee: 0,
hourly_rate: 0,
included_hours_per_month: 0,
included_hours_per_year: 0
} }
}, },
licence: { licence: {
@@ -278,7 +284,7 @@
<InputField <InputField
name="user[email]" name="user[email]"
type="email" type="email"
label={$t('email')} label={$t('user.email')}
bind:value={localUser.email} bind:value={localUser.email}
placeholder={$t('placeholder.email')} placeholder={$t('placeholder.email')}
required={true} required={true}
@@ -403,29 +409,29 @@
<InputField <InputField
name="user[membership][subscription_model][name]" name="user[membership][subscription_model][name]"
type="select" type="select"
label={$t('subscription_model')} label={$t('subscription.subscription')}
bind:value={localUser.membership.subscription_model.name} bind:value={localUser.membership.subscription_model.name}
options={subscriptionModelOptions} options={subscriptionModelOptions}
/> />
<div class="subscription-info"> <div class="subscription-info">
<div class="subscription-column"> <div class="subscription-column">
<p> <p>
<strong>{$t('monthly_fee')}:</strong> <strong>{$t('subscription.monthly_fee')}:</strong>
{selectedSubscriptionModel?.monthly_fee || '-'} {selectedSubscriptionModel?.monthly_fee || '-'}
</p> </p>
<p> <p>
<strong>{$t('hourly_rate')}:</strong> <strong>{$t('subscription.hourly_rate')}:</strong>
{selectedSubscriptionModel?.hourly_rate || '-'} {selectedSubscriptionModel?.hourly_rate || '-'}
</p> </p>
{#if selectedSubscriptionModel?.included_hours_per_year} {#if selectedSubscriptionModel?.included_hours_per_year}
<p> <p>
<strong>{$t('included_hours_per_year')}:</strong> <strong>{$t('subscription.included_hours_per_year')}:</strong>
{selectedSubscriptionModel?.included_hours_per_year} {selectedSubscriptionModel?.included_hours_per_year}
</p> </p>
{/if} {/if}
{#if selectedSubscriptionModel?.included_hours_per_month} {#if selectedSubscriptionModel?.included_hours_per_month}
<p> <p>
<strong>{$t('included_hours_per_month')}:</strong> <strong>{$t('subscription.included_hours_per_month')}:</strong>
{selectedSubscriptionModel?.included_hours_per_month} {selectedSubscriptionModel?.included_hours_per_month}
</p> </p>
{/if} {/if}
@@ -437,7 +443,7 @@
</p> </p>
{#if selectedSubscriptionModel?.conditions} {#if selectedSubscriptionModel?.conditions}
<p> <p>
<strong>{$t('conditions')}:</strong> <strong>{$t('subscription.conditions')}:</strong>
{selectedSubscriptionModel?.conditions} {selectedSubscriptionModel?.conditions}
</p> </p>
{/if} {/if}

View File

@@ -32,7 +32,10 @@ export default {
licence_number: 'Auf dem Führerschein unter Feld 5', licence_number: 'Auf dem Führerschein unter Feld 5',
issued_date: 'Ausgabedatum unter Feld 4a', issued_date: 'Ausgabedatum unter Feld 4a',
expiration_date: 'Ablaufdatum unter Feld 4b', expiration_date: 'Ablaufdatum unter Feld 4b',
issuing_country: 'Ausstellendes Land' issuing_country: 'Ausstellendes Land',
subscription_name: 'Name des Tarifmodells',
subscription_details: 'Beschreibe das Tarifmodell...',
subscription_conditions: 'Beschreibe die Bedingungen zur Nutzung...'
}, },
validation: { validation: {
required: 'Eingabe benötigt', required: 'Eingabe benötigt',
@@ -107,6 +110,21 @@ export default {
status: 'Status', status: 'Status',
role: 'Nutzerrolle' role: 'Nutzerrolle'
}, },
subscription: {
name: 'Modellname',
edit: 'Modell bearbeiten',
subscription: 'Tarifmodell',
subscriptions: 'Tarifmodelle',
conditions: 'Bedingungen',
monthly_fee: 'Monatliche Gebühr',
hourly_rate: 'Stundensatz',
included_hours_per_year: 'Inkludierte Stunden pro Jahr',
included_hours_per_month: 'Inkludierte Stunden pro Monat'
},
loading: {
user_data: 'Lade Nutzerdaten',
subscription_data: 'Lade Modelldaten'
},
cancel: 'Abbrechen', cancel: 'Abbrechen',
confirm: 'Bestätigen', confirm: 'Bestätigen',
actions: 'Aktionen', actions: 'Aktionen',
@@ -120,10 +138,7 @@ export default {
issued_date: 'Ausgabedatum', issued_date: 'Ausgabedatum',
expiration_date: 'Ablaufdatum', expiration_date: 'Ablaufdatum',
country: 'Land', country: 'Land',
monthly_fee: 'Monatliche Gebühr',
hourly_rate: 'Stundensatz',
details: 'Details', details: 'Details',
conditions: 'Bedingungen',
unknown: 'Unbekannt', unknown: 'Unbekannt',
notes: 'Notizen', notes: 'Notizen',
address: 'Straße & Hausnummer', address: 'Straße & Hausnummer',
@@ -132,7 +147,6 @@ export default {
forgot_password: 'Passwort vergessen?', forgot_password: 'Passwort vergessen?',
password: 'Passwort', password: 'Passwort',
password_repeat: 'Passwort wiederholen', password_repeat: 'Passwort wiederholen',
email: 'Email',
company: 'Firma', company: 'Firma',
login: 'Anmeldung', login: 'Anmeldung',
profile: 'Profil', profile: 'Profil',
@@ -140,7 +154,6 @@ export default {
bankaccount: 'Kontodaten', bankaccount: 'Kontodaten',
first_name: 'Vorname', first_name: 'Vorname',
last_name: 'Nachname', last_name: 'Nachname',
name: 'Name',
phone: 'Telefonnummer', phone: 'Telefonnummer',
dateofbirth: 'Geburtstag', dateofbirth: 'Geburtstag',
status: 'Status', status: 'Status',
@@ -152,11 +165,8 @@ export default {
iban: 'IBAN', iban: 'IBAN',
bic: 'BIC', bic: 'BIC',
mandate_reference: 'SEPA Mandat', mandate_reference: 'SEPA Mandat',
subscriptions: 'Tarifmodelle',
payments: 'Zahlungen', payments: 'Zahlungen',
add_new: 'Neu', add_new: 'Neu',
included_hours_per_year: 'Inkludierte Stunden pro Jahr',
included_hours_per_month: 'Inkludierte Stunden pro Monat',
// For payments section // For payments section
payment: { payment: {

View File

@@ -91,7 +91,15 @@ export function processFormData(rawData) {
parent_member_id: Number(rawData.user.membership?.parent_member_id) || 0, parent_member_id: Number(rawData.user.membership?.parent_member_id) || 0,
subscription_model: { subscription_model: {
id: Number(rawData.user.membership?.subscription_model?.id) || 0, id: Number(rawData.user.membership?.subscription_model?.id) || 0,
name: String(rawData.user.membership?.subscription_model?.name) || '' name: String(rawData.user.membership?.subscription_model?.name) || '',
details: String(rawData.user.membership?.subscription_model?.details) || '',
conditions: String(rawData.user.membership?.subscription_model?.conditions) || '',
hourly_rate: Number(rawData.user.membership?.subscription_model?.hourly_rate) || 0,
monthly_fee: Number(rawData.user.membership?.subscription_model?.monthly_fee) || 0,
included_hours_per_month:
Number(rawData.user.membership?.subscription_model?.included_hours_per_month) || 0,
included_hours_per_year:
Number(rawData.user.membership?.subscription_model?.included_hours_per_year) || 0
} }
}, },

View File

@@ -1,8 +1,7 @@
/** @type {import('./$types').LayoutLoad} */ /** @type {import('./$types').LayoutLoad} */
export async function load({ fetch, url, data }) { export async function load({ data }) {
const { users } = data;
return { return {
users: data.users, users: data.users,
user: data.user, user: data.user
}; };
} }

View File

@@ -1,32 +1,50 @@
<script> <script>
import Modal from '$lib/components/Modal.svelte'; import Modal from '$lib/components/Modal.svelte';
import UserEditForm from '$lib/components/UserEditForm.svelte'; import UserEditForm from '$lib/components/UserEditForm.svelte';
import SubscriptionEditForm from '$lib/components/SubscriptionEditForm.svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { page } from '$app/stores'; import { page } from '$app/stores';
/** @type {import('./$types').ActionData} */ /** @type {import('./$types').ActionData} */
export let form; export let form;
$: ({ users = [], licence_categories = [], subscriptions = [], payments = [] } = $page.data); $: ({
user = [],
users = [],
licence_categories = [],
subscriptions = [],
payments = []
} = $page.data);
let activeSection = 'users'; let activeSection = 'users';
/** @type{App.Locals['user'] | null} */ /** @type{App.Locals['user'] | null} */
let selectedUser = null; let selectedUser = null;
/** @type{App.Types['subscription'] | null} */
let selectedSubscription = null;
let showModal = false; let showModal = false;
/** /**
* Opens the edit modal for the selected user. * Opens the edit modal for the selected user.
* @param {App.Locals['user'] | null} user The user to edit. * @param {App.Locals['user'] | null} user The user to edit.
*/ */
const openEditModal = (user) => { const openEditUserModal = (user) => {
selectedUser = user; selectedUser = user;
console.dir(selectedUser); showModal = true;
};
/**
* Opens the edit modal for the selected subscription.
* @param {App.Types['subscription'] | null} subscription The user to edit.
*/
const openEditSubscriptionModal = (subscription) => {
selectedSubscription = subscription;
showModal = true; showModal = true;
}; };
const close = () => { const close = () => {
showModal = false; showModal = false;
selectedUser = null; selectedUser = null;
selectedSubscription = null;
if (form) { if (form) {
form.errors = undefined; form.errors = undefined;
} }
@@ -62,7 +80,7 @@
on:click={() => setActiveSection('subscriptions')} on:click={() => setActiveSection('subscriptions')}
> >
<i class="fas fa-clipboard-list"></i> <i class="fas fa-clipboard-list"></i>
{$t('subscriptions')} {$t('subscription.subscriptions')}
<span class="nav-badge">{subscriptions.length}</span> <span class="nav-badge">{subscriptions.length}</span>
</button> </button>
</li> </li>
@@ -83,7 +101,7 @@
{#if activeSection === 'users'} {#if activeSection === 'users'}
<div class="section-header"> <div class="section-header">
<h2>{$t('users')}</h2> <h2>{$t('users')}</h2>
<button class="btn primary" on:click={() => openEditModal(null)}> <button class="btn primary" on:click={() => openEditUserModal(null)}>
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
{$t('add_new')} {$t('add_new')}
</button> </button>
@@ -103,11 +121,11 @@
<td>{user.id}</td> <td>{user.id}</td>
</tr> </tr>
<tr> <tr>
<th>{$t('name')}</th> <th>{$t('user.name')}</th>
<td>{user.first_name} {user.last_name}</td> <td>{user.first_name} {user.last_name}</td>
</tr> </tr>
<tr> <tr>
<th>{$t('email')}</th> <th>{$t('user.email')}</th>
<td>{user.email}</td> <td>{user.email}</td>
</tr> </tr>
<tr> <tr>
@@ -117,7 +135,7 @@
</tbody> </tbody>
</table> </table>
<div class="button-group"> <div class="button-group">
<button class="btn primary" on:click={() => openEditModal(user)}> <button class="btn primary" on:click={() => openEditUserModal(user)}>
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
{$t('edit')} {$t('edit')}
</button> </button>
@@ -132,11 +150,13 @@
</div> </div>
{:else if activeSection === 'subscriptions'} {:else if activeSection === 'subscriptions'}
<div class="section-header"> <div class="section-header">
<h2>{$t('subscriptions')}</h2> <h2>{$t('subscription.subscriptions')}</h2>
<button class="btn primary" on:click={() => openEditModal(null)}> {#if user.role_id == 8}
<button class="btn primary" on:click={() => openEditUserModal(null)}>
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
{$t('add_new')} {$t('add_new')}
</button> </button>
{/if}
</div> </div>
<div class="accordion"> <div class="accordion">
{#each subscriptions as subscription} {#each subscriptions as subscription}
@@ -148,7 +168,7 @@
<table class="table"> <table class="table">
<tbody> <tbody>
<tr> <tr>
<th>{$t('monthly_fee')}</th> <th>{$t('subscription.monthly_fee')}</th>
<td <td
>{subscription.monthly_fee !== -1 >{subscription.monthly_fee !== -1
? subscription.monthly_fee + '€' ? subscription.monthly_fee + '€'
@@ -156,7 +176,7 @@
> >
</tr> </tr>
<tr> <tr>
<th>{$t('hourly_rate')}</th> <th>{$t('subscription.hourly_rate')}</th>
<td <td
>{subscription.hourly_rate !== -1 >{subscription.hourly_rate !== -1
? subscription.hourly_rate + '€' ? subscription.hourly_rate + '€'
@@ -164,11 +184,11 @@
> >
</tr> </tr>
<tr> <tr>
<th>{$t('included_hours_per_year')}</th> <th>{$t('subscription.included_hours_per_year')}</th>
<td>{subscription.included_hours_per_year || 0}</td> <td>{subscription.included_hours_per_year || 0}</td>
</tr> </tr>
<tr> <tr>
<th>{$t('included_hours_per_month')}</th> <th>{$t('subscription.included_hours_per_month')}</th>
<td>{subscription.included_hours_per_month || 0}</td> <td>{subscription.included_hours_per_month || 0}</td>
</tr> </tr>
<tr> <tr>
@@ -176,11 +196,28 @@
<td>{subscription.details || '-'}</td> <td>{subscription.details || '-'}</td>
</tr> </tr>
<tr> <tr>
<th>{$t('conditions')}</th> <th>{$t('subscription.conditions')}</th>
<td>{subscription.conditions || '-'}</td> <td>{subscription.conditions || '-'}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
{#if user.role_id == 8}
<div class="button-group">
<button
class="btn primary"
on:click={() => openEditSubscriptionModal(subscription)}
>
<i class="fas fa-edit"></i>
{$t('edit')}
</button>
{#if !users.some(/** @param{App.Locals['user']} user */ (user) => user.membership?.subscription_model?.id === subscription.id)}
<button class="btn danger">
<i class="fas fa-trash"></i>
{$t('delete')}
</button>
{/if}
</div>
{/if}
</div> </div>
</details> </details>
{/each} {/each}
@@ -217,6 +254,7 @@
{#if showModal} {#if showModal}
<Modal on:close={close}> <Modal on:close={close}>
{#if selectedUser}
<UserEditForm <UserEditForm
{form} {form}
user={selectedUser} user={selectedUser}
@@ -225,6 +263,15 @@
on:cancel={close} on:cancel={close}
on:close={close} on:close={close}
/> />
{:else if selectedSubscription}
<SubscriptionEditForm
{form}
{user}
subscription={selectedSubscription}
on:cancel={close}
on:close={close}
/>
{/if}
</Modal> </Modal>
{/if} {/if}

View File

@@ -1,94 +0,0 @@
<!-- - Create a table or list view of all users.
- Implement a search or filter functionality.
- Add a modal component for editing user details (reuse the modal from about/[id]). -->
<script>
import { onMount } from "svelte";
import Modal from "$lib/components/Modal.svelte";
import UserEditForm from "$lib/components/UserEditForm.svelte";
import { t } from "svelte-i18n";
import { page } from "$app/stores";
import "static/css/bootstrap.min.css";
import "static/js/bootstrapv5/bootstrap.bundle.min.js";
/** @type {import('./$types').ActionData} */
export let form;
$: ({ user, users, licence_categories, subscriptions } = $page.data);
/** @type(App.Locals['user'] | null) */
let selectedUser = null;
let showModal = false;
/**
* Opens the edit modal for the selected user.
* @param {App.Locals['user']} user The user to edit.
*/
const openEditModal = (user) => {
selectedUser = user;
showModal = true;
};
/**
* Opens the delete modal for the selected user.
* @param {App.Locals['user']} user The user to edit.
*/
const openDelete = (user) => {};
const close = () => {
showModal = false;
selectedUser = null;
if (form) {
form.errors = undefined;
}
};
</script>
<div class="admin-users-page">
<h1>{$t("user.management")}</h1>
<div class="search-filter" />
<table class="user-table">
<thead>
<tr>
<th>{$t("user.id")}</th>
<th>{$t("name")}</th>
<th>{$t("email")}</th>
<th>{$t("status")}</th>
<th>{$t("actions")}</th>
</tr>
</thead>
<tbody>
{#each users as user}
<tr>
<td>{user.id}</td>
<td>{user.first_name} {user.last_name}</td>
<td>{user.email}</td>
<td>{$t("userStatus." + user.status)}</td>
<td>
<button on:click={() => openEditModal(user)}>{$t("edit")}</button>
<button on:click={() => openDelete(user)}>{$t("delete")}</button>
</td>
</tr>{/each}
</tbody>
</table>
<div class="pagination" />
{#if showModal}
<Modal on:close={close}>
<UserEditForm
{form}
user={selectedUser}
{subscriptions}
{licence_categories}
on:cancel={close}
/>
</Modal>
{/if}
</div>
<style>
</style>