Compare commits
2 Commits
66ce257198
...
183e4da7f4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
183e4da7f4 | ||
|
|
11c55a17ea |
3
frontend/src/app.d.ts
vendored
3
frontend/src/app.d.ts
vendored
@@ -76,6 +76,9 @@ declare global {
|
|||||||
subscriptions: Subscription[];
|
subscriptions: Subscription[];
|
||||||
licence_categories: LicenceCategory[];
|
licence_categories: LicenceCategory[];
|
||||||
}
|
}
|
||||||
|
interface Types {
|
||||||
|
licenceCategory: LicenceCategory;
|
||||||
|
}
|
||||||
// interface PageData {}
|
// interface PageData {}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Poiret+One&family=Quicksand:wght@400;500;600;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Poiret+One&family=Quicksand:wght@400;500;600;700&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||||
|
/>
|
||||||
|
<!-- <link
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
|
||||||
|
rel="stylesheet"
|
||||||
|
/> -->
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import { refreshCookie, userDatesFromRFC3339 } from "$lib/utils/helpers";
|
|||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Handle} */
|
/** @type {import('@sveltejs/kit').Handle} */
|
||||||
export async function handle({ event, resolve }) {
|
export async function handle({ event, resolve }) {
|
||||||
|
console.log("Hook started", event.url.pathname);
|
||||||
if (event.locals.user) {
|
if (event.locals.user) {
|
||||||
// if there is already a user in session load page as normal
|
// if there is already a user in session load page as normal
|
||||||
|
console.log("user is logged in");
|
||||||
return await resolve(event);
|
return await resolve(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,8 +54,6 @@ export async function handle({ event, resolve }) {
|
|||||||
event.locals.user = data.user;
|
event.locals.user = data.user;
|
||||||
event.locals.subscriptions = subscriptionsData.subscriptions;
|
event.locals.subscriptions = subscriptionsData.subscriptions;
|
||||||
event.locals.licence_categories = licence_categoriesData.licence_categories;
|
event.locals.licence_categories = licence_categoriesData.licence_categories;
|
||||||
// console.log("hooks.server: Printing locals:");
|
|
||||||
// console.dir(event.locals);
|
|
||||||
|
|
||||||
// load page as normal
|
// load page as normal
|
||||||
return await resolve(event);
|
return await resolve(event);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
<div class="modal-background">
|
<div class="modal-background">
|
||||||
<div
|
<div
|
||||||
transition:modal={{ duration: 1000 }}
|
transition:modal|global={{ duration: 1000 }}
|
||||||
class="modal"
|
class="modal"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#key key}
|
{#key key}
|
||||||
<div in:slide={{ duration, delay: duration }} out:slide={{ duration }}>
|
<div in:slide|global={{ duration, delay: duration }} out:slide|global={{ duration }}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
{/key}
|
{/key}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import InputField from "$lib/components/InputField.svelte";
|
import InputField from "$lib/components/InputField.svelte";
|
||||||
import SmallLoader from "$lib/components/SmallLoader.svelte";
|
import SmallLoader from "$lib/components/SmallLoader.svelte";
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
import { applyAction, enhance } from "$app/forms";
|
import { applyAction, enhance } from "$app/forms";
|
||||||
import { receive, send } from "$lib/utils/helpers";
|
import { receive, send } from "$lib/utils/helpers";
|
||||||
import { t } from "svelte-i18n";
|
import { t } from "svelte-i18n";
|
||||||
@@ -11,10 +12,8 @@
|
|||||||
/** @type {App.Locals['subscriptions']}*/
|
/** @type {App.Locals['subscriptions']}*/
|
||||||
export let subscriptions;
|
export let subscriptions;
|
||||||
|
|
||||||
/** @type {App.Locals['user'] | null} */
|
/** @type {App.Locals['user']} */
|
||||||
export let user;
|
const blankUser = {
|
||||||
if (user == null) {
|
|
||||||
user = {
|
|
||||||
id: 0,
|
id: 0,
|
||||||
email: "",
|
email: "",
|
||||||
first_name: "",
|
first_name: "",
|
||||||
@@ -60,8 +59,38 @@
|
|||||||
mandate_reference: "",
|
mandate_reference: "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @type {App.Locals['user'] | null} */
|
||||||
|
export let user;
|
||||||
|
|
||||||
|
/** @type {App.Locals['user'] } */
|
||||||
|
let localUser;
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (user !== undefined && !localUser) {
|
||||||
|
localUser =
|
||||||
|
user === null
|
||||||
|
? { ...blankUser }
|
||||||
|
: {
|
||||||
|
...user,
|
||||||
|
licence: user.licence || blankUser.licence,
|
||||||
|
membership: user.membership || blankUser.membership,
|
||||||
|
bank_account: user.bank_account || blankUser.bank_account,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: isNewUser = user === null;
|
||||||
|
$: isLoading = user === undefined;
|
||||||
|
|
||||||
|
$: {
|
||||||
|
console.log("incomingUser:", user);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add debug logging for user
|
||||||
|
$: {
|
||||||
|
console.log("processed user:", user);
|
||||||
|
}
|
||||||
/** @type {App.Locals['licence_categories']} */
|
/** @type {App.Locals['licence_categories']} */
|
||||||
export let licence_categories;
|
export let licence_categories;
|
||||||
|
|
||||||
@@ -91,6 +120,7 @@
|
|||||||
{ value: 5, label: $t("userStatus.5"), color: "#FF4646" }, // Red for "Deaktiviert"
|
{ value: 5, label: $t("userStatus.5"), color: "#FF4646" }, // Red for "Deaktiviert"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
const TABS = ["profile", "licence", "membership", "bankaccount"];
|
const TABS = ["profile", "licence", "membership", "bankaccount"];
|
||||||
let activeTab = TABS[0];
|
let activeTab = TABS[0];
|
||||||
|
|
||||||
@@ -106,7 +136,7 @@
|
|||||||
}));
|
}));
|
||||||
$: selectedSubscriptionModel =
|
$: selectedSubscriptionModel =
|
||||||
subscriptions.find(
|
subscriptions.find(
|
||||||
(sub) => sub?.id === user.membership?.subscription_model.id
|
(sub) => sub?.id === localUser.membership?.subscription_model.id
|
||||||
) || null;
|
) || null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -142,31 +172,38 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @type {import('../../routes/auth/about/[id]/$types').SubmitFunction} */
|
/** @type {import('../../routes/auth/about/[id]/$types').SubmitFunction} */
|
||||||
const handleUpdate = async ({ form, formData, action, cancel }) => {
|
const handleUpdate = async ({ formData, action, cancel }) => {
|
||||||
isUpdating = true;
|
isUpdating = true;
|
||||||
return async ({ result }) => {
|
return async ({ result }) => {
|
||||||
isUpdating = false;
|
isUpdating = false;
|
||||||
if (result.type === "success" || result.type === "redirect") {
|
if (result.type === "success" || result.type === "redirect") {
|
||||||
close();
|
close();
|
||||||
|
} else {
|
||||||
|
document
|
||||||
|
.querySelector(".modal .container")
|
||||||
|
?.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
}
|
}
|
||||||
await applyAction(result);
|
await applyAction(result);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form
|
{#if isLoading}
|
||||||
|
<SmallLoader width={30} message={$t("loading.user_data")} />
|
||||||
|
{:else if localUser}
|
||||||
|
<form
|
||||||
class="content"
|
class="content"
|
||||||
action="?/updateUser"
|
action="?/updateUser"
|
||||||
method="POST"
|
method="POST"
|
||||||
use:enhance={handleUpdate}
|
use:enhance={handleUpdate}
|
||||||
>
|
>
|
||||||
<input name="id" type="number" hidden bind:value={user.id} />
|
<input name="user[id]" type="hidden" bind:value={localUser.id} />
|
||||||
<h1 class="step-title" style="text-align: center;">{$t("user.edit")}</h1>
|
<h1 class="step-title" style="text-align: center;">{$t("user.edit")}</h1>
|
||||||
{#if form?.success}
|
{#if form?.success}
|
||||||
<h4
|
<h4
|
||||||
class="step-subtitle warning"
|
class="step-subtitle warning"
|
||||||
in:receive={{ key: Math.floor(Math.random() * 100) }}
|
in:receive|global={{ key: Math.floor(Math.random() * 100) }}
|
||||||
out:send={{ key: Math.floor(Math.random() * 100) }}
|
out:send|global={{ key: Math.floor(Math.random() * 100) }}
|
||||||
>
|
>
|
||||||
Um einen fehlerhaften upload Ihres Bildes zu vermeiden, clicke bitte auf
|
Um einen fehlerhaften upload Ihres Bildes zu vermeiden, clicke bitte auf
|
||||||
den "Update" Button unten.
|
den "Update" Button unten.
|
||||||
@@ -176,8 +213,8 @@
|
|||||||
{#each form?.errors as error (error.id)}
|
{#each form?.errors as error (error.id)}
|
||||||
<h4
|
<h4
|
||||||
class="step-subtitle warning"
|
class="step-subtitle warning"
|
||||||
in:receive={{ key: error.id }}
|
in:receive|global={{ key: error.id }}
|
||||||
out:send={{ key: error.id }}
|
out:send|global={{ key: error.id }}
|
||||||
>
|
>
|
||||||
{$t(error.field) + ": " + $t(error.key)}
|
{$t(error.field) + ": " + $t(error.key)}
|
||||||
</h4>
|
</h4>
|
||||||
@@ -187,8 +224,8 @@
|
|||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
hidden
|
hidden
|
||||||
name="profile_picture"
|
name="user[profile_picture]"
|
||||||
bind:value={user.profile_picture}
|
bind:value={localUser.profile_picture}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="button-container">
|
<div class="button-container">
|
||||||
@@ -208,23 +245,23 @@
|
|||||||
style="display: {activeTab === 'profile' ? 'block' : 'none'}"
|
style="display: {activeTab === 'profile' ? 'block' : 'none'}"
|
||||||
>
|
>
|
||||||
<InputField
|
<InputField
|
||||||
name="status"
|
name="user[status]"
|
||||||
type="select"
|
type="select"
|
||||||
label={$t("status")}
|
label={$t("status")}
|
||||||
bind:value={user.status}
|
bind:value={localUser.status}
|
||||||
options={userStatusOptions}
|
options={userStatusOptions}
|
||||||
/>
|
/>
|
||||||
{#if user.role_id === 8}
|
{#if localUser.role_id === 8}
|
||||||
<InputField
|
<InputField
|
||||||
name="role_id"
|
name="user[role_id]"
|
||||||
type="select"
|
type="select"
|
||||||
label={$t("user.role")}
|
label={$t("user.role")}
|
||||||
bind:value={user.role_id}
|
bind:value={localUser.role_id}
|
||||||
options={userRoleOptions}
|
options={userRoleOptions}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<InputField
|
<InputField
|
||||||
name="password"
|
name="user[password]"
|
||||||
type="password"
|
type="password"
|
||||||
label={$t("password")}
|
label={$t("password")}
|
||||||
placeholder={$t("placeholder.password")}
|
placeholder={$t("placeholder.password")}
|
||||||
@@ -240,72 +277,72 @@
|
|||||||
otherPasswordValue={password}
|
otherPasswordValue={password}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="first_name"
|
name="user[first_name]"
|
||||||
label={$t("first_name")}
|
label={$t("first_name")}
|
||||||
bind:value={user.first_name}
|
bind:value={localUser.first_name}
|
||||||
placeholder={$t("placeholder.first_name")}
|
placeholder={$t("placeholder.first_name")}
|
||||||
required={true}
|
required={true}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="last_name"
|
name="user[last_name]"
|
||||||
label={$t("last_name")}
|
label={$t("last_name")}
|
||||||
bind:value={user.last_name}
|
bind:value={localUser.last_name}
|
||||||
placeholder={$t("placeholder.last_name")}
|
placeholder={$t("placeholder.last_name")}
|
||||||
required={true}
|
required={true}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="company"
|
name="user[company]"
|
||||||
label={$t("company")}
|
label={$t("company")}
|
||||||
bind:value={user.company}
|
bind:value={localUser.company}
|
||||||
placeholder={$t("placeholder.company")}
|
placeholder={$t("placeholder.company")}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="email"
|
name="user[email]"
|
||||||
type="email"
|
type="email"
|
||||||
label={$t("email")}
|
label={$t("email")}
|
||||||
bind:value={user.email}
|
bind:value={localUser.email}
|
||||||
placeholder={$t("placeholder.email")}
|
placeholder={$t("placeholder.email")}
|
||||||
required={true}
|
required={true}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="phone"
|
name="user[phone]"
|
||||||
type="tel"
|
type="tel"
|
||||||
label={$t("phone")}
|
label={$t("phone")}
|
||||||
bind:value={user.phone}
|
bind:value={localUser.phone}
|
||||||
placeholder={$t("placeholder.phone")}
|
placeholder={$t("placeholder.phone")}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="birth_date"
|
name="user[date_of_birth]"
|
||||||
type="date"
|
type="date"
|
||||||
label={$t("birth_date")}
|
label={$t("date_of_birth")}
|
||||||
bind:value={user.date_of_birth}
|
bind:value={localUser.date_of_birth}
|
||||||
placeholder={$t("placeholder.birth_date")}
|
placeholder={$t("placeholder.date_of_birth")}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="address"
|
name="user[address]"
|
||||||
label={$t("address")}
|
label={$t("address")}
|
||||||
bind:value={user.address}
|
bind:value={localUser.address}
|
||||||
placeholder={$t("placeholder.address")}
|
placeholder={$t("placeholder.address")}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="zip_code"
|
name="user[zip_code]"
|
||||||
label={$t("zip_code")}
|
label={$t("zip_code")}
|
||||||
bind:value={user.zip_code}
|
bind:value={localUser.zip_code}
|
||||||
placeholder={$t("placeholder.zip_code")}
|
placeholder={$t("placeholder.zip_code")}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="city"
|
name="user[city]"
|
||||||
label={$t("city")}
|
label={$t("city")}
|
||||||
bind:value={user.city}
|
bind:value={localUser.city}
|
||||||
placeholder={$t("placeholder.city")}
|
placeholder={$t("placeholder.city")}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="notes"
|
name="user[notes]"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
label={$t("notes")}
|
label={$t("notes")}
|
||||||
bind:value={user.notes}
|
bind:value={localUser.notes}
|
||||||
placeholder={$t("placeholder.notes", {
|
placeholder={$t("placeholder.notes", {
|
||||||
values: { name: user.first_name || "" },
|
values: { name: localUser.first_name || "" },
|
||||||
})}
|
})}
|
||||||
rows={10}
|
rows={10}
|
||||||
/>
|
/>
|
||||||
@@ -315,38 +352,38 @@
|
|||||||
style="display: {activeTab === 'licence' ? 'block' : 'none'}"
|
style="display: {activeTab === 'licence' ? 'block' : 'none'}"
|
||||||
>
|
>
|
||||||
<InputField
|
<InputField
|
||||||
name="licence_status"
|
name="user[licence][status]"
|
||||||
type="select"
|
type="select"
|
||||||
label={$t("status")}
|
label={$t("status")}
|
||||||
bind:value={user.licence.status}
|
bind:value={localUser.licence.status}
|
||||||
options={licenceStatusOptions}
|
options={licenceStatusOptions}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="licence_number"
|
name="user[licence][number]"
|
||||||
type="text"
|
type="text"
|
||||||
label={$t("licence_number")}
|
label={$t("licence_number")}
|
||||||
bind:value={user.licence.licence_number}
|
bind:value={localUser.licence.licence_number}
|
||||||
placeholder={$t("placeholder.licence_number")}
|
placeholder={$t("placeholder.licence_number")}
|
||||||
toUpperCase={true}
|
toUpperCase={true}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="issued_date"
|
name="user[licence][issued_date]"
|
||||||
type="date"
|
type="date"
|
||||||
label={$t("issued_date")}
|
label={$t("issued_date")}
|
||||||
bind:value={user.licence.issued_date}
|
bind:value={localUser.licence.issued_date}
|
||||||
placeholder={$t("placeholder.issued_date")}
|
placeholder={$t("placeholder.issued_date")}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="expiration_date"
|
name="user[licence][expiration_date]"
|
||||||
type="date"
|
type="date"
|
||||||
label={$t("expiration_date")}
|
label={$t("expiration_date")}
|
||||||
bind:value={user.licence.expiration_date}
|
bind:value={localUser.licence.expiration_date}
|
||||||
placeholder={$t("placeholder.expiration_date")}
|
placeholder={$t("placeholder.expiration_date")}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="country"
|
name="user[licence][country]"
|
||||||
label={$t("country")}
|
label={$t("country")}
|
||||||
bind:value={user.licence.country}
|
bind:value={localUser.licence.country}
|
||||||
placeholder={$t("placeholder.issuing_country")}
|
placeholder={$t("placeholder.issuing_country")}
|
||||||
/>
|
/>
|
||||||
<div class="licence-categories">
|
<div class="licence-categories">
|
||||||
@@ -361,11 +398,11 @@
|
|||||||
<div class="checkbox-label-container">
|
<div class="checkbox-label-container">
|
||||||
<InputField
|
<InputField
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="licence_categories[]"
|
name="user[licence][categories[]]"
|
||||||
value={JSON.stringify(category)}
|
value={JSON.stringify(category)}
|
||||||
label={category.category}
|
label={category.category}
|
||||||
checked={user.licence.licence_categories != null &&
|
checked={localUser.licence.licence_categories != null &&
|
||||||
user.licence.licence_categories.some(
|
localUser.licence.licence_categories.some(
|
||||||
(cat) => cat.category === category.category
|
(cat) => cat.category === category.category
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -384,17 +421,17 @@
|
|||||||
style="display: {activeTab === 'membership' ? 'block' : 'none'}"
|
style="display: {activeTab === 'membership' ? 'block' : 'none'}"
|
||||||
>
|
>
|
||||||
<InputField
|
<InputField
|
||||||
name="membership_status"
|
name="user[membership][status]"
|
||||||
type="select"
|
type="select"
|
||||||
label={$t("status")}
|
label={$t("status")}
|
||||||
bind:value={user.membership.status}
|
bind:value={localUser.membership.status}
|
||||||
options={membershipStatusOptions}
|
options={membershipStatusOptions}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="subscription_model_name"
|
name="user[membership][subscription_model][name]"
|
||||||
type="select"
|
type="select"
|
||||||
label={$t("subscription_model")}
|
label={$t("subscription_model")}
|
||||||
bind:value={user.membership.subscription_model.name}
|
bind:value={localUser.membership.subscription_model.name}
|
||||||
options={subscriptionModelOptions}
|
options={subscriptionModelOptions}
|
||||||
/>
|
/>
|
||||||
<div class="subscription-info">
|
<div class="subscription-info">
|
||||||
@@ -434,24 +471,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<InputField
|
<InputField
|
||||||
name="membership_start_date"
|
name="user[membership][start_date]"
|
||||||
type="date"
|
type="date"
|
||||||
label={$t("start")}
|
label={$t("start")}
|
||||||
bind:value={user.membership.start_date}
|
bind:value={localUser.membership.start_date}
|
||||||
placeholder={$t("placeholder.start_date")}
|
placeholder={$t("placeholder.start_date")}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="membership_end_date"
|
name="user[membership][end_date]"
|
||||||
type="date"
|
type="date"
|
||||||
label={$t("end")}
|
label={$t("end")}
|
||||||
bind:value={user.membership.end_date}
|
bind:value={localUser.membership.end_date}
|
||||||
placeholder={$t("placeholder.end_date")}
|
placeholder={$t("placeholder.end_date")}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="parent_member_id"
|
name="user[membership][parent_member_id]"
|
||||||
type="number"
|
type="number"
|
||||||
label={$t("parent_member_id")}
|
label={$t("parent_member_id")}
|
||||||
bind:value={user.membership.parent_member_id}
|
bind:value={localUser.membership.parent_member_id}
|
||||||
placeholder={$t("placeholder.parent_member_id")}
|
placeholder={$t("placeholder.parent_member_id")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -460,42 +497,42 @@
|
|||||||
style="display: {activeTab === 'bankaccount' ? 'block' : 'none'}"
|
style="display: {activeTab === 'bankaccount' ? 'block' : 'none'}"
|
||||||
>
|
>
|
||||||
<InputField
|
<InputField
|
||||||
name="account_holder_name"
|
name="user[bank_account][account_holder_name]"
|
||||||
label={$t("bank_account_holder")}
|
label={$t("bank_account_holder")}
|
||||||
bind:value={user.bank_account.account_holder_name}
|
bind:value={localUser.bank_account.account_holder_name}
|
||||||
placeholder={$t("placeholder.bank_account_holder")}
|
placeholder={$t("placeholder.bank_account_holder")}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="bank"
|
name="user[bank_account][bank_name]"
|
||||||
label={$t("bank_name")}
|
label={$t("bank_name")}
|
||||||
bind:value={user.bank_account.bank}
|
bind:value={localUser.bank_account.bank}
|
||||||
placeholder={$t("placeholder.bank_name")}
|
placeholder={$t("placeholder.bank_name")}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="iban"
|
name="user[bank_account][iban]"
|
||||||
label={$t("iban")}
|
label={$t("iban")}
|
||||||
bind:value={user.bank_account.iban}
|
bind:value={localUser.bank_account.iban}
|
||||||
placeholder={$t("placeholder.iban")}
|
placeholder={$t("placeholder.iban")}
|
||||||
toUpperCase={true}
|
toUpperCase={true}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="bic"
|
name="user[bank_account][bic]"
|
||||||
label={$t("bic")}
|
label={$t("bic")}
|
||||||
bind:value={user.bank_account.bic}
|
bind:value={localUser.bank_account.bic}
|
||||||
placeholder={$t("placeholder.bic")}
|
placeholder={$t("placeholder.bic")}
|
||||||
toUpperCase={true}
|
toUpperCase={true}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="mandate_reference"
|
name="user[bank_account][mandate_reference]"
|
||||||
label={$t("mandate_reference")}
|
label={$t("mandate_reference")}
|
||||||
bind:value={user.bank_account.mandate_reference}
|
bind:value={localUser.bank_account.mandate_reference}
|
||||||
placeholder={$t("placeholder.mandate_reference")}
|
placeholder={$t("placeholder.mandate_reference")}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="mandate_date_signed"
|
name="user[bank_account][mandate_date_signed]"
|
||||||
label={$t("mandate_date_signed")}
|
label={$t("mandate_date_signed")}
|
||||||
type="date"
|
type="date"
|
||||||
bind:value={user.bank_account.mandate_date_signed}
|
bind:value={localUser.bank_account.mandate_date_signed}
|
||||||
readonly={true}
|
readonly={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -503,13 +540,18 @@
|
|||||||
{#if isUpdating}
|
{#if isUpdating}
|
||||||
<SmallLoader width={30} message={"Aktualisiere..."} />
|
<SmallLoader width={30} message={"Aktualisiere..."} />
|
||||||
{:else}
|
{:else}
|
||||||
<button type="button" class="button-dark" on:click={close}>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button-dark"
|
||||||
|
on:click={() => dispatch("cancel")}
|
||||||
|
>
|
||||||
{$t("cancel")}</button
|
{$t("cancel")}</button
|
||||||
>
|
>
|
||||||
<button type="submit" class="button-dark">{$t("confirm")}</button>
|
<button type="submit" class="button-dark">{$t("confirm")}</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.category-break {
|
.category-break {
|
||||||
|
|||||||
151
frontend/src/lib/css/bootstrap-custom.scss
vendored
Normal file
151
frontend/src/lib/css/bootstrap-custom.scss
vendored
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
@import "bootstrap/scss/functions";
|
||||||
|
@import "bootstrap/scss/variables";
|
||||||
|
@import "bootstrap/scss/mixins";
|
||||||
|
|
||||||
|
// Core variables
|
||||||
|
$theme-colors: (
|
||||||
|
"primary": #d43aff,
|
||||||
|
"secondary": #595b5c,
|
||||||
|
"success": #00b7ef,
|
||||||
|
"warning": rgb(225 29 72),
|
||||||
|
"danger": #eb5424,
|
||||||
|
"light": #9b9b9b,
|
||||||
|
"dark": #2f2f2f,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Typography
|
||||||
|
$font-family-base: "Quicksand", sans-serif;
|
||||||
|
$font-family-monospace: "Roboto Mono", monospace;
|
||||||
|
$font-size-base: 1rem;
|
||||||
|
$line-height-base: 1.8;
|
||||||
|
$headings-font-weight: normal;
|
||||||
|
$headings-color: #fff;
|
||||||
|
|
||||||
|
// Body
|
||||||
|
$body-bg: black;
|
||||||
|
$body-color: #9b9b9b;
|
||||||
|
|
||||||
|
// Links
|
||||||
|
$link-color: #00b7ef;
|
||||||
|
$link-decoration: none;
|
||||||
|
$link-hover-decoration: underline;
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
$btn-padding-y: 1.125rem; // 18px
|
||||||
|
$btn-padding-x: 1.75rem; // 28px
|
||||||
|
$btn-font-weight: 500;
|
||||||
|
$btn-letter-spacing: 1px;
|
||||||
|
$btn-border-width: 1px;
|
||||||
|
$btn-transition: all 0.3s ease-in-out;
|
||||||
|
|
||||||
|
// Forms
|
||||||
|
$input-bg: #494848;
|
||||||
|
$input-color: white;
|
||||||
|
$input-border-color: #494848;
|
||||||
|
$input-border-radius: 6px;
|
||||||
|
$input-padding-y: 0.625rem; // 10px
|
||||||
|
$input-padding-x: 0.625rem; // 10px
|
||||||
|
$input-font-family: $font-family-monospace;
|
||||||
|
$input-font-size: 1rem;
|
||||||
|
$input-focus-border-color: lighten($input-border-color, 10%);
|
||||||
|
$input-focus-box-shadow: none;
|
||||||
|
|
||||||
|
// Cards
|
||||||
|
$card-bg: #2f2f2f;
|
||||||
|
$card-border-radius: 3px;
|
||||||
|
$card-border-width: 0;
|
||||||
|
$card-spacer-y: 1.25rem;
|
||||||
|
$card-spacer-x: 1.25rem;
|
||||||
|
|
||||||
|
// Modals
|
||||||
|
$modal-content-bg: #2f2f2f;
|
||||||
|
$modal-header-border-color: #595b5c;
|
||||||
|
$modal-footer-border-color: #595b5c;
|
||||||
|
|
||||||
|
// Navbar
|
||||||
|
$navbar-dark-color: #9b9b9b;
|
||||||
|
$navbar-dark-hover-color: #fff;
|
||||||
|
$navbar-padding-y: 1rem;
|
||||||
|
$navbar-nav-link-padding-x: 1rem;
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
$border-radius: 3px;
|
||||||
|
$border-radius-lg: 6px;
|
||||||
|
$box-shadow: none;
|
||||||
|
|
||||||
|
// Custom utility classes
|
||||||
|
.text-uppercase {
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom button styles
|
||||||
|
.btn {
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
&-dark {
|
||||||
|
@include button-variant(transparent, #595b5c);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #fff;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-primary {
|
||||||
|
&:hover {
|
||||||
|
background-color: darken(#d43aff, 5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom input styles
|
||||||
|
.form-control {
|
||||||
|
&:focus {
|
||||||
|
background-color: lighten($input-bg, 5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
padding: 0 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 5em auto 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 45px 0;
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 2rem 0;
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin: 0 0 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li strong {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre,
|
||||||
|
code {
|
||||||
|
display: inline;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override Bootstrap's link hover behavior
|
||||||
|
a:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom-color: #00b7ef;
|
||||||
|
}
|
||||||
|
// Import Bootstrap after variable overrides
|
||||||
|
@import "bootstrap/scss/bootstrap";
|
||||||
40
frontend/src/lib/css/styles.min.css
vendored
40
frontend/src/lib/css/styles.min.css
vendored
@@ -387,6 +387,46 @@ li strong {
|
|||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-family: "Roboto Mono", monospace;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
padding: 18px 28px;
|
||||||
|
border: 1px solid;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
transition: border-color 0.3s ease-in-out, background-color 0.3s ease-in-out;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.primary {
|
||||||
|
background-color: #4361ee;
|
||||||
|
border-color: #4361ee;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.primary:hover {
|
||||||
|
background-color: #3651d4;
|
||||||
|
border-color: #3651d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.danger {
|
||||||
|
background-color: #dc2626;
|
||||||
|
border-color: #dc2626;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.danger:hover {
|
||||||
|
background-color: #c51f1f;
|
||||||
|
border-color: #c51f1f;
|
||||||
|
}
|
||||||
.warning {
|
.warning {
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|||||||
@@ -52,12 +52,12 @@ export default {
|
|||||||
no_auth_token: "Nicht authorisiert, fehlender oder ungültiger Auth-Token",
|
no_auth_token: "Nicht authorisiert, fehlender oder ungültiger Auth-Token",
|
||||||
jwt_parsing_error:
|
jwt_parsing_error:
|
||||||
"Nicht authorisiert, Auth-Token konnte nicht gelesen werden",
|
"Nicht authorisiert, Auth-Token konnte nicht gelesen werden",
|
||||||
unauthorized_update: "Sie sind nicht befugt dieses Update durchzuführen",
|
unauthorized: "Sie sind nicht befugt diese Handlung durchzuführen",
|
||||||
internal_server_error:
|
internal_server_error:
|
||||||
"Verdammt, fehler auf unserer Seite, probieren Sie es nochmal, danach rufen Sie nach Hilfe",
|
"Verdammt, Fehler auf unserer Seite, probieren Sie es nochmal, danach rufen Sie jemanden vom Verein an.",
|
||||||
},
|
},
|
||||||
validation: {
|
validation: {
|
||||||
no_user_id_provided: "Nutzer ID fehlt im Header",
|
invalid_user_id: "Nutzer ID ungültig",
|
||||||
invalid_subscription_model: "Model nicht gefunden",
|
invalid_subscription_model: "Model nicht gefunden",
|
||||||
user_not_found: "{field} konnte nicht gefunden werden",
|
user_not_found: "{field} konnte nicht gefunden werden",
|
||||||
invalid_user_data: "Nutzerdaten ungültig",
|
invalid_user_data: "Nutzerdaten ungültig",
|
||||||
@@ -98,6 +98,7 @@ export default {
|
|||||||
L: "Land-, Forstwirtschaftsfahrzeuge, Stapler max 40km/h",
|
L: "Land-, Forstwirtschaftsfahrzeuge, Stapler max 40km/h",
|
||||||
T: "Land-, Forstwirtschaftsfahrzeuge, Stapler max 60km/h",
|
T: "Land-, Forstwirtschaftsfahrzeuge, Stapler max 60km/h",
|
||||||
},
|
},
|
||||||
|
users: "Mitglieder",
|
||||||
user: {
|
user: {
|
||||||
login: "Nutzer Anmeldung",
|
login: "Nutzer Anmeldung",
|
||||||
edit: "Nutzer bearbeiten",
|
edit: "Nutzer bearbeiten",
|
||||||
@@ -144,7 +145,7 @@ export default {
|
|||||||
last_name: "Nachname",
|
last_name: "Nachname",
|
||||||
name: "Name",
|
name: "Name",
|
||||||
phone: "Telefonnummer",
|
phone: "Telefonnummer",
|
||||||
birth_date: "Geburtstag",
|
date_of_birth: "Geburtstag",
|
||||||
status: "Status",
|
status: "Status",
|
||||||
start: "Beginn",
|
start: "Beginn",
|
||||||
end: "Ende",
|
end: "Ende",
|
||||||
@@ -154,4 +155,25 @@ export default {
|
|||||||
iban: "IBAN",
|
iban: "IBAN",
|
||||||
bic: "BIC",
|
bic: "BIC",
|
||||||
mandate_reference: "SEPA Mandat",
|
mandate_reference: "SEPA Mandat",
|
||||||
|
subscriptions: "Tarifmodelle",
|
||||||
|
payments: "Zahlungen",
|
||||||
|
add_new: "Neu",
|
||||||
|
included_hours_per_year: "Inkludierte Stunden pro Jahr",
|
||||||
|
included_hours_per_month: "Inkludierte Stunden pro Monat",
|
||||||
|
|
||||||
|
// For payments section
|
||||||
|
payment: {
|
||||||
|
id: "Zahlungs-Nr",
|
||||||
|
amount: "Betrag",
|
||||||
|
date: "Datum",
|
||||||
|
status: "Status",
|
||||||
|
},
|
||||||
|
|
||||||
|
// For subscription statuses
|
||||||
|
subscriptionStatus: {
|
||||||
|
pending: "Ausstehend",
|
||||||
|
completed: "Abgeschlossen",
|
||||||
|
failed: "Fehlgeschlagen",
|
||||||
|
cancelled: "Storniert",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import "$lib/utils/i18n.js";
|
import "$lib/utils/i18n.js";
|
||||||
|
|
||||||
|
// import "$lib/css/bootstrap-custom.scss";
|
||||||
/** @type {import('./$types').PageData} */
|
/** @type {import('./$types').PageData} */
|
||||||
export let data;
|
export let data;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
import { BASE_API_URI } from "$lib/utils/constants";
|
import { BASE_API_URI } from "$lib/utils/constants";
|
||||||
import { formatError, userDatesFromRFC3339 } from "$lib/utils/helpers";
|
import {
|
||||||
|
formatError,
|
||||||
|
userDatesFromRFC3339,
|
||||||
|
userDatesToRFC3339,
|
||||||
|
} from "$lib/utils/helpers";
|
||||||
import { fail, redirect } from "@sveltejs/kit";
|
import { fail, redirect } from "@sveltejs/kit";
|
||||||
import { toRFC3339 } from "$lib/utils/helpers";
|
import { toRFC3339 } from "$lib/utils/helpers";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} UpdateData
|
||||||
|
* @property {Partial<App.Locals['user']>} user
|
||||||
|
*/
|
||||||
|
|
||||||
/** @type {import('./$types').PageServerLoad} */
|
/** @type {import('./$types').PageServerLoad} */
|
||||||
export async function load({ locals, params }) {
|
export async function load({ locals, params }) {
|
||||||
// redirect user if not logged in
|
// redirect user if not logged in
|
||||||
@@ -24,6 +33,7 @@ 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();
|
||||||
|
|
||||||
|
/** @type {App.Types['licenceCategory'][]} */
|
||||||
const licenceCategories = formData
|
const licenceCategories = formData
|
||||||
.getAll("licence_categories[]")
|
.getAll("licence_categories[]")
|
||||||
.filter((value) => typeof value === "string")
|
.filter((value) => typeof value === "string")
|
||||||
@@ -34,11 +44,10 @@ export const actions = {
|
|||||||
console.error("Failed to parse licence category:", value);
|
console.error("Failed to parse licence category:", value);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
/** @type {Partial<App.Locals['user']>} */
|
/** @type {Partial<App.Locals['user']>} */
|
||||||
const updateData = {
|
const userData = {
|
||||||
id: Number(formData.get("id")),
|
id: Number(formData.get("id")),
|
||||||
first_name: String(formData.get("first_name")),
|
first_name: String(formData.get("first_name")),
|
||||||
last_name: String(formData.get("last_name")),
|
last_name: String(formData.get("last_name")),
|
||||||
@@ -48,7 +57,7 @@ export const actions = {
|
|||||||
address: String(formData.get("address")),
|
address: String(formData.get("address")),
|
||||||
zip_code: String(formData.get("zip_code")),
|
zip_code: String(formData.get("zip_code")),
|
||||||
city: String(formData.get("city")),
|
city: String(formData.get("city")),
|
||||||
date_of_birth: toRFC3339(formData.get("birth_date")),
|
date_of_birth: toRFC3339(formData.get("date_of_birth")),
|
||||||
company: String(formData.get("company")),
|
company: String(formData.get("company")),
|
||||||
profile_picture: String(formData.get("profile_picture")),
|
profile_picture: String(formData.get("profile_picture")),
|
||||||
membership: {
|
membership: {
|
||||||
@@ -64,9 +73,7 @@ export const actions = {
|
|||||||
},
|
},
|
||||||
bank_account: {
|
bank_account: {
|
||||||
id: Number(formData.get("bank_account_id")),
|
id: Number(formData.get("bank_account_id")),
|
||||||
mandate_date_signed: toRFC3339(
|
mandate_date_signed: toRFC3339(formData.get("mandate_date_signed")),
|
||||||
String(formData.get("mandate_date_signed"))
|
|
||||||
),
|
|
||||||
bank: String(formData.get("bank")),
|
bank: String(formData.get("bank")),
|
||||||
account_holder_name: String(formData.get("account_holder_name")),
|
account_holder_name: String(formData.get("account_holder_name")),
|
||||||
iban: String(formData.get("iban")),
|
iban: String(formData.get("iban")),
|
||||||
@@ -83,11 +90,17 @@ export const actions = {
|
|||||||
licence_categories: licenceCategories,
|
licence_categories: licenceCategories,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// userDatesToRFC3339(userData);
|
||||||
|
|
||||||
|
/** @type {UpdateData} */
|
||||||
|
const updateData = { user: userData };
|
||||||
// Remove undefined or null properties
|
// Remove undefined or null properties
|
||||||
const cleanUpdateData = JSON.parse(
|
const cleanUpdateData = JSON.parse(
|
||||||
JSON.stringify(updateData),
|
JSON.stringify(updateData),
|
||||||
(key, value) => (value !== null && value !== "" ? value : undefined)
|
(key, value) => (value !== null && value !== "" ? value : undefined)
|
||||||
);
|
);
|
||||||
|
|
||||||
console.dir(formData);
|
console.dir(formData);
|
||||||
console.dir(cleanUpdateData);
|
console.dir(cleanUpdateData);
|
||||||
const apiURL = `${BASE_API_URI}/backend/users/update/`;
|
const apiURL = `${BASE_API_URI}/backend/users/update/`;
|
||||||
|
|||||||
@@ -4,13 +4,6 @@ import { userDatesFromRFC3339, refreshCookie } from "$lib/utils/helpers";
|
|||||||
|
|
||||||
/** @type {import('./$types').LayoutServerLoad} */
|
/** @type {import('./$types').LayoutServerLoad} */
|
||||||
export async function load({ cookies, fetch, locals }) {
|
export async function load({ cookies, fetch, locals }) {
|
||||||
// if (locals.users) {
|
|
||||||
// return {
|
|
||||||
// users: locals.users,
|
|
||||||
// user: locals.user,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
const jwt = cookies.get("jwt");
|
const jwt = cookies.get("jwt");
|
||||||
try {
|
try {
|
||||||
// Fetch user data, subscriptions, and licence categories in parallel
|
// Fetch user data, subscriptions, and licence categories in parallel
|
||||||
@@ -27,7 +20,6 @@ export async function load({ cookies, fetch, locals }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// 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 = response.headers.get("Set-Cookie");
|
||||||
refreshCookie(newToken, null);
|
refreshCookie(newToken, null);
|
||||||
|
|||||||
@@ -7,6 +7,130 @@ import { formatError, userDatesFromRFC3339 } from "$lib/utils/helpers";
|
|||||||
import { fail, redirect } from "@sveltejs/kit";
|
import { fail, redirect } from "@sveltejs/kit";
|
||||||
import { toRFC3339 } from "$lib/utils/helpers";
|
import { toRFC3339 } from "$lib/utils/helpers";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts FormData to a nested object structure
|
||||||
|
* @param {FormData} formData - The FormData object to convert
|
||||||
|
* @returns {{ user: Partial<App.Locals['user']> }} Nested object representation of the form data
|
||||||
|
*/
|
||||||
|
function formDataToObject(formData) {
|
||||||
|
/** @type { Partial<App.Locals['user']> } */
|
||||||
|
const object = {};
|
||||||
|
|
||||||
|
console.log("Form data entries:");
|
||||||
|
for (const [key, value] of formData.entries()) {
|
||||||
|
console.log("Key:", key, "Value:", value);
|
||||||
|
}
|
||||||
|
for (const [key, value] of formData.entries()) {
|
||||||
|
/** @type {string[]} */
|
||||||
|
const keys = key.match(/\[([^\]]+)\]/g)?.map((k) => k.slice(1, -1)) || [
|
||||||
|
key,
|
||||||
|
];
|
||||||
|
console.log("Processed keys:", keys);
|
||||||
|
/** @type {Record<string, any>} */
|
||||||
|
let current = object;
|
||||||
|
|
||||||
|
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]] || {};
|
||||||
|
/**
|
||||||
|
* Move to the next level of the object
|
||||||
|
* @type {Record<string, any>}
|
||||||
|
*/
|
||||||
|
current = current[keys[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastKey = keys[keys.length - 1];
|
||||||
|
if (lastKey.endsWith("[]")) {
|
||||||
|
/**
|
||||||
|
* Handle array fields (licence categories)
|
||||||
|
*/
|
||||||
|
const arrayKey = lastKey.slice(0, -2);
|
||||||
|
current[arrayKey] = current[arrayKey] || [];
|
||||||
|
current[arrayKey].push(value);
|
||||||
|
} else {
|
||||||
|
current[lastKey] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { user: object };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the raw form data into the expected user data structure
|
||||||
|
* @param {{ user: Partial<App.Locals['user']> } } rawData - The raw form data object
|
||||||
|
* @returns {{ user: Partial<App.Locals['user']> }} Processed user data
|
||||||
|
*/
|
||||||
|
function processFormData(rawData) {
|
||||||
|
/** @type {{ user: Partial<App.Locals['user']> }} */
|
||||||
|
const processedData = {
|
||||||
|
user: {
|
||||||
|
id: Number(rawData.user.id) || 0,
|
||||||
|
status: Number(rawData.user.status),
|
||||||
|
role_id: Number(rawData.user.role_id),
|
||||||
|
first_name: String(rawData.user.first_name),
|
||||||
|
last_name: String(rawData.user.last_name),
|
||||||
|
email: String(rawData.user.email),
|
||||||
|
phone: String(rawData.user.phone || ""),
|
||||||
|
company: String(rawData.user.company || ""),
|
||||||
|
date_of_birth: toRFC3339(rawData.user.date_of_birth),
|
||||||
|
address: String(rawData.user.address || ""),
|
||||||
|
zip_code: String(rawData.user.zip_code || ""),
|
||||||
|
city: String(rawData.user.city || ""),
|
||||||
|
notes: String(rawData.user.notes || ""),
|
||||||
|
profile_picture: String(rawData.user.profile_picture || ""),
|
||||||
|
|
||||||
|
membership: {
|
||||||
|
id: Number(rawData.user.membership?.id) || 0,
|
||||||
|
status: Number(rawData.user.membership?.status),
|
||||||
|
start_date: toRFC3339(rawData.user.membership?.start_date),
|
||||||
|
end_date: toRFC3339(rawData.user.membership?.end_date),
|
||||||
|
parent_member_id:
|
||||||
|
Number(rawData.user.membership?.parent_member_id) || 0,
|
||||||
|
subscription_model: {
|
||||||
|
id: Number(rawData.user.membership?.subscription_model?.id) || 0,
|
||||||
|
name: String(rawData.user.membership?.subscription_model?.name) || "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
licence: {
|
||||||
|
id: Number(rawData.user.licence?.id) || 0,
|
||||||
|
status: Number(rawData.user.licence?.status),
|
||||||
|
licence_number: String(rawData.user.licence?.licence_number || ""),
|
||||||
|
issued_date: toRFC3339(rawData.user.licence?.issued_date),
|
||||||
|
expiration_date: toRFC3339(rawData.user.licence?.expiration_date),
|
||||||
|
country: String(rawData.user.licence?.country || ""),
|
||||||
|
licence_categories: rawData.user.licence?.licence_categories || [],
|
||||||
|
},
|
||||||
|
|
||||||
|
bank_account: {
|
||||||
|
id: Number(rawData.user.bank_account?.id) || 0,
|
||||||
|
account_holder_name: String(
|
||||||
|
rawData.user.bank_account?.account_holder_name || ""
|
||||||
|
),
|
||||||
|
bank: String(rawData.user.bank_account?.bank || ""),
|
||||||
|
iban: String(rawData.user.bank_account?.iban || ""),
|
||||||
|
bic: String(rawData.user.bank_account?.bic || ""),
|
||||||
|
mandate_reference: String(
|
||||||
|
rawData.user.bank_account?.mandate_reference || ""
|
||||||
|
),
|
||||||
|
mandate_date_signed: toRFC3339(
|
||||||
|
rawData.user.bank_account?.mandate_date_signed
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return processedData;
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {import('./$types').PageServerLoad} */
|
/** @type {import('./$types').PageServerLoad} */
|
||||||
export async function load({ locals, params }) {
|
export async function load({ locals, params }) {
|
||||||
// redirect user if not logged in
|
// redirect user if not logged in
|
||||||
@@ -28,77 +152,24 @@ 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 licenceCategories = formData
|
// Convert form data to nested object
|
||||||
.getAll("licence_categories[]")
|
const rawData = formDataToObject(formData);
|
||||||
.filter((value) => typeof value === "string")
|
|
||||||
.map((value) => {
|
const processedData = processFormData(rawData);
|
||||||
try {
|
|
||||||
return JSON.parse(value);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to parse licence category:", value);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
/** @type {Partial<App.Locals['user']>} */
|
|
||||||
const updateData = {
|
|
||||||
id: Number(formData.get("id")),
|
|
||||||
first_name: String(formData.get("first_name")),
|
|
||||||
last_name: String(formData.get("last_name")),
|
|
||||||
email: String(formData.get("email")),
|
|
||||||
phone: String(formData.get("phone")),
|
|
||||||
notes: String(formData.get("notes")),
|
|
||||||
address: String(formData.get("address")),
|
|
||||||
zip_code: String(formData.get("zip_code")),
|
|
||||||
city: String(formData.get("city")),
|
|
||||||
date_of_birth: toRFC3339(formData.get("birth_date")),
|
|
||||||
company: String(formData.get("company")),
|
|
||||||
profile_picture: String(formData.get("profile_picture")),
|
|
||||||
membership: {
|
|
||||||
id: Number(formData.get("membership_id")),
|
|
||||||
start_date: toRFC3339(formData.get("membership_start_date")),
|
|
||||||
end_date: toRFC3339(formData.get("membership_end_date")),
|
|
||||||
status: Number(formData.get("membership_status")),
|
|
||||||
parent_member_id: Number(formData.get("parent_member_id")),
|
|
||||||
subscription_model: {
|
|
||||||
id: Number(formData.get("subscription_model_id")),
|
|
||||||
name: String(formData.get("subscription_model_name")),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
bank_account: {
|
|
||||||
id: Number(formData.get("bank_account_id")),
|
|
||||||
mandate_date_signed: toRFC3339(
|
|
||||||
String(formData.get("mandate_date_signed"))
|
|
||||||
),
|
|
||||||
bank: String(formData.get("bank")),
|
|
||||||
account_holder_name: String(formData.get("account_holder_name")),
|
|
||||||
iban: String(formData.get("iban")),
|
|
||||||
bic: String(formData.get("bic")),
|
|
||||||
mandate_reference: String(formData.get("mandate_reference")),
|
|
||||||
},
|
|
||||||
licence: {
|
|
||||||
id: Number(formData.get("drivers_licence_id")),
|
|
||||||
status: Number(formData.get("licence_status")),
|
|
||||||
licence_number: String(formData.get("licence_number")),
|
|
||||||
issued_date: toRFC3339(formData.get("issued_date")),
|
|
||||||
expiration_date: toRFC3339(formData.get("expiration_date")),
|
|
||||||
country: String(formData.get("country")),
|
|
||||||
licence_categories: licenceCategories,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// Remove undefined or null properties
|
// Remove undefined or null properties
|
||||||
const cleanUpdateData = JSON.parse(
|
const cleanUpdateData = JSON.parse(
|
||||||
JSON.stringify(updateData),
|
JSON.stringify(processedData),
|
||||||
(key, value) => (value !== null && value !== "" ? value : undefined)
|
(key, value) => (value !== null && value !== "" ? value : undefined)
|
||||||
);
|
);
|
||||||
console.dir(formData);
|
console.dir(processedData.user.membership);
|
||||||
console.dir(cleanUpdateData);
|
const isCreating = !processedData.user.id || processedData.user.id === 0;
|
||||||
const apiURL = `${BASE_API_URI}/backend/users/update/`;
|
console.log("Is creating: ", isCreating);
|
||||||
|
const apiURL = `${BASE_API_URI}/backend/users/update`;
|
||||||
|
|
||||||
/** @type {RequestInit} */
|
/** @type {RequestInit} */
|
||||||
const requestUpdateOptions = {
|
const requestOptions = {
|
||||||
method: "PATCH",
|
method: isCreating ? "POST" : "PATCH",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -106,7 +177,8 @@ export const actions = {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(cleanUpdateData),
|
body: JSON.stringify(cleanUpdateData),
|
||||||
};
|
};
|
||||||
const res = await fetch(apiURL, requestUpdateOptions);
|
|
||||||
|
const res = await fetch(apiURL, requestOptions);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const response = await res.json();
|
const response = await res.json();
|
||||||
@@ -115,6 +187,7 @@ export const actions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await res.json();
|
const response = await res.json();
|
||||||
|
console.log("Server success response:", response);
|
||||||
locals.user = response;
|
locals.user = response;
|
||||||
userDatesFromRFC3339(locals.user);
|
userDatesFromRFC3339(locals.user);
|
||||||
throw redirect(303, `/auth/about/${response.id}`);
|
throw redirect(303, `/auth/about/${response.id}`);
|
||||||
|
|||||||
@@ -1,39 +1,36 @@
|
|||||||
<!-- - 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>
|
<script>
|
||||||
import { onMount } from "svelte";
|
|
||||||
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 { Styles } from "@sveltestrap/sveltestrap";
|
||||||
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;
|
||||||
|
|
||||||
$: ({ user, users, licence_categories, subscriptions } = $page.data);
|
$: ({
|
||||||
|
user,
|
||||||
|
users = [],
|
||||||
|
licence_categories = [],
|
||||||
|
subscriptions = [],
|
||||||
|
payments = [],
|
||||||
|
} = $page.data);
|
||||||
|
|
||||||
/** @type(App.Locals['user'] | null) */
|
let activeSection = "users";
|
||||||
|
/** @type{App.Locals['user'] | null} */
|
||||||
let selectedUser = null;
|
let selectedUser = 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']} user The user to edit.
|
* @param {App.Locals['user'] | null} user The user to edit.
|
||||||
*/
|
*/
|
||||||
const openEditModal = (user) => {
|
const openEditModal = (user) => {
|
||||||
selectedUser = user;
|
selectedUser = user;
|
||||||
|
console.dir(selectedUser);
|
||||||
showModal = true;
|
showModal = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens the delete modal for the selected user.
|
|
||||||
* @param {App.Locals['user']} user The user to edit.
|
|
||||||
*/
|
|
||||||
const openDelete = (user) => {};
|
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
showModal = false;
|
showModal = false;
|
||||||
selectedUser = null;
|
selectedUser = null;
|
||||||
@@ -41,41 +38,194 @@
|
|||||||
form.errors = undefined;
|
form.errors = undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sets the active admin section for display
|
||||||
|
* @param {string} section The new active section.
|
||||||
|
*/
|
||||||
|
const setActiveSection = (section) => {
|
||||||
|
activeSection = section;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="admin-users-page">
|
<div class="container">
|
||||||
<h1>{$t("user.management")}</h1>
|
<div class="layout">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<nav class="sidebar">
|
||||||
|
<ul class="nav-list">
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
class="nav-link {activeSection === 'users' ? 'active' : ''}"
|
||||||
|
on:click={() => setActiveSection("users")}
|
||||||
|
>
|
||||||
|
<i class="fas fa-users" />
|
||||||
|
<span class="nav-badge">{users.length}</span>
|
||||||
|
{$t("users")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
class="nav-link {activeSection === 'subscriptions' ? 'active' : ''}"
|
||||||
|
on:click={() => setActiveSection("subscriptions")}
|
||||||
|
>
|
||||||
|
<i class="fas fa-clipboard-list" />
|
||||||
|
<span class="nav-badge">{subscriptions.length}</span>
|
||||||
|
{$t("subscriptions")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
class="nav-link {activeSection === 'payments' ? 'active' : ''}"
|
||||||
|
on:click={() => setActiveSection("payments")}
|
||||||
|
>
|
||||||
|
<i class="fas fa-credit-card" />
|
||||||
|
{$t("payments")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<div class="search-filter" />
|
<!-- Main Content -->
|
||||||
|
<main class="main-content">
|
||||||
<table class="user-table">
|
{#if activeSection === "users"}
|
||||||
<thead>
|
<div class="section-header">
|
||||||
|
<h2>{$t("users")}</h2>
|
||||||
|
<button class="btn primary" on:click={() => openEditModal(null)}>
|
||||||
|
<i class="fas fa-plus" />
|
||||||
|
{$t("add_new")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="accordion">
|
||||||
|
{#each users as user}
|
||||||
|
<details class="accordion-item">
|
||||||
|
<summary class="accordion-header">
|
||||||
|
{user.first_name}
|
||||||
|
{user.last_name} - {user.email}
|
||||||
|
</summary>
|
||||||
|
<div class="accordion-content">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{$t("user.id")}</th>
|
<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.id}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{$t("name")}</th>
|
||||||
<td>{user.first_name} {user.last_name}</td>
|
<td>{user.first_name} {user.last_name}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{$t("email")}</th>
|
||||||
<td>{user.email}</td>
|
<td>{user.email}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{$t("status")}</th>
|
||||||
<td>{$t("userStatus." + user.status)}</td>
|
<td>{$t("userStatus." + user.status)}</td>
|
||||||
<td>
|
</tr>
|
||||||
<button on:click={() => openEditModal(user)}>{$t("edit")}</button>
|
|
||||||
<button on:click={() => openDelete(user)}>{$t("delete")}</button>
|
|
||||||
</td>
|
|
||||||
</tr>{/each}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<div class="button-group">
|
||||||
|
<button
|
||||||
|
class="btn primary"
|
||||||
|
on:click={() => openEditModal(user)}
|
||||||
|
>
|
||||||
|
<i class="fas fa-edit" />
|
||||||
|
{$t("edit")}
|
||||||
|
</button>
|
||||||
|
<button class="btn danger">
|
||||||
|
<i class="fas fa-trash" />
|
||||||
|
{$t("delete")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else if activeSection === "subscriptions"}
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>{$t("subscriptions")}</h2>
|
||||||
|
<button class="btn primary" on:click={() => openEditModal(null)}>
|
||||||
|
<i class="fas fa-plus" />
|
||||||
|
{$t("add_new")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="accordion">
|
||||||
|
{#each subscriptions as subscription}
|
||||||
|
<details class="accordion-item">
|
||||||
|
<summary class="accordion-header">
|
||||||
|
{subscription.name}
|
||||||
|
</summary>
|
||||||
|
<div class="accordion-content">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>{$t("monthly_fee")}</th>
|
||||||
|
<td
|
||||||
|
>{subscription.monthly_fee !== -1
|
||||||
|
? subscription.monthly_fee + "€"
|
||||||
|
: "-"}</td
|
||||||
|
>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{$t("hourly_rate")}</th>
|
||||||
|
<td
|
||||||
|
>{subscription.hourly_rate !== -1
|
||||||
|
? subscription.hourly_rate + "€"
|
||||||
|
: "-"}</td
|
||||||
|
>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{$t("included_hours_per_year")}</th>
|
||||||
|
<td>{subscription.included_hours_per_year || 0}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{$t("included_hours_per_month")}</th>
|
||||||
|
<td>{subscription.included_hours_per_month || 0}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{$t("details")}</th>
|
||||||
|
<td>{subscription.details || "-"}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{$t("conditions")}</th>
|
||||||
|
<td>{subscription.conditions || "-"}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else if activeSection === "payments"}
|
||||||
|
<h2>{$t("payments")}</h2>
|
||||||
|
<div class="accordion">
|
||||||
|
{#each payments as payment}
|
||||||
|
<details class="accordion-item">
|
||||||
|
<summary class="accordion-header">
|
||||||
|
Payment #{payment.id} - {payment.amount}€
|
||||||
|
</summary>
|
||||||
|
<div class="accordion-content">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<td>{new Date(payment.date).toLocaleDateString()}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Status</th>
|
||||||
|
<td>{payment.status}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="pagination" />
|
{#if showModal}
|
||||||
|
|
||||||
{#if showModal}
|
|
||||||
<Modal on:close={close}>
|
<Modal on:close={close}>
|
||||||
<UserEditForm
|
<UserEditForm
|
||||||
{form}
|
{form}
|
||||||
@@ -85,8 +235,132 @@
|
|||||||
on:cancel={close}
|
on:cancel={close}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 1rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 250px 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
height: 100%;
|
||||||
|
width: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 250px;
|
||||||
|
min-height: 600px;
|
||||||
|
background: #2f2f2f;
|
||||||
|
border-right: 1px solid #494848;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #9b9b9b;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
transition: color 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
background: #fdfff5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active {
|
||||||
|
background: #494848;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
padding: 2rem;
|
||||||
|
min-width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-item {
|
||||||
|
border: none;
|
||||||
|
background: #2f2f2f;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-header {
|
||||||
|
padding: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: "Roboto Mono", monospace;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-content {
|
||||||
|
padding: 1rem;
|
||||||
|
background: #494848;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-family: "Roboto Mono", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th,
|
||||||
|
.table td {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-bottom: 1px solid #2f2f2f;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
color: #9b9b9b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
.layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: static;
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-top: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
94
frontend/src/routes/auth/admin/users/old+page.svelte
Normal file
94
frontend/src/routes/auth/admin/users/old+page.svelte
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<!-- - 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>
|
||||||
@@ -5,7 +5,9 @@ import { fail, redirect } from "@sveltejs/kit";
|
|||||||
/** @type {import('./$types').PageServerLoad} */
|
/** @type {import('./$types').PageServerLoad} */
|
||||||
export async function load({ locals }) {
|
export async function load({ locals }) {
|
||||||
// redirect user if logged in
|
// redirect user if logged in
|
||||||
|
console.log("loading login page");
|
||||||
if (locals.user) {
|
if (locals.user) {
|
||||||
|
console.log("user is logged in");
|
||||||
throw redirect(302, "/");
|
throw redirect(302, "/");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,11 +22,11 @@ export const actions = {
|
|||||||
* @returns Error data or redirects user to the home page or the previous page
|
* @returns Error data or redirects user to the home page or the previous page
|
||||||
*/
|
*/
|
||||||
login: async ({ request, fetch, cookies }) => {
|
login: async ({ request, fetch, cookies }) => {
|
||||||
|
console.log("login action called");
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
const email = String(data.get("email"));
|
const email = String(data.get("email"));
|
||||||
const password = String(data.get("password"));
|
const password = String(data.get("password"));
|
||||||
const next = String(data.get("next"));
|
const next = String(data.get("next"));
|
||||||
|
|
||||||
/** @type {RequestInit} */
|
/** @type {RequestInit} */
|
||||||
const requestInitOptions = {
|
const requestInitOptions = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -37,9 +39,7 @@ export const actions = {
|
|||||||
password: password,
|
password: password,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
const res = await fetch(`${BASE_API_URI}/users/login`, requestInitOptions);
|
||||||
const res = await fetch(`${BASE_API_URI}/users/login/`, requestInitOptions);
|
|
||||||
|
|
||||||
console.log("Login response status:", res.status);
|
console.log("Login response status:", res.status);
|
||||||
console.log("Login response headers:", Object.fromEntries(res.headers));
|
console.log("Login response headers:", Object.fromEntries(res.headers));
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,8 @@
|
|||||||
{#each form?.errors as error (error.id)}
|
{#each form?.errors as error (error.id)}
|
||||||
<h4
|
<h4
|
||||||
class="step-subtitle warning"
|
class="step-subtitle warning"
|
||||||
in:receive={{ key: error.id }}
|
in:receive|global={{ key: error.id }}
|
||||||
out:send={{ key: error.id }}
|
out:send|global={{ key: error.id }}
|
||||||
>
|
>
|
||||||
{$t(error.key)}
|
{$t(error.key)}
|
||||||
</h4>
|
</h4>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { sveltePreprocess } from "svelte-preprocess";
|
||||||
// import adapter from '@sveltejs/adapter-auto';
|
// import adapter from '@sveltejs/adapter-auto';
|
||||||
import adapter from '@sveltejs/adapter-vercel';
|
import adapter from "@sveltejs/adapter-vercel";
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
@@ -8,9 +9,9 @@ const config = {
|
|||||||
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||||
adapter: adapter({
|
adapter: adapter({
|
||||||
runtime: 'edge'
|
runtime: "edge",
|
||||||
})
|
}),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -51,6 +51,13 @@ func (uc *UserController) GetAllUsers(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a slice to hold the safe user representations
|
||||||
|
safeUsers := make([]map[string]interface{}, len(*users))
|
||||||
|
|
||||||
|
// Convert each user to its safe representation
|
||||||
|
for i, user := range *users {
|
||||||
|
safeUsers[i] = user.Safe()
|
||||||
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"users": users,
|
"users": users,
|
||||||
})
|
})
|
||||||
@@ -65,10 +72,12 @@ func (uc *UserController) UpdateHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var user models.User
|
var user models.User
|
||||||
if err := c.ShouldBindJSON(&user); err != nil {
|
var updateData RegistrationData
|
||||||
|
if err := c.ShouldBindJSON(&updateData); err != nil {
|
||||||
utils.HandleValidationError(c, err)
|
utils.HandleValidationError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
user = updateData.User
|
||||||
|
|
||||||
if !utils.HasPrivilige(requestUser, constants.Priviliges.Update) && user.ID != requestUser.ID {
|
if !utils.HasPrivilige(requestUser, constants.Priviliges.Update) && user.ID != requestUser.ID {
|
||||||
utils.RespondWithError(c, errors.ErrNotAuthorized, "Not allowed to update user", http.StatusForbidden, "user", "server.error.unauthorized")
|
utils.RespondWithError(c, errors.ErrNotAuthorized, "Not allowed to update user", http.StatusForbidden, "user", "server.error.unauthorized")
|
||||||
|
|||||||
@@ -9,13 +9,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
gorm.Model
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
DeletedAt *time.Time `gorm:"index"`
|
||||||
DateOfBirth time.Time `gorm:"not null" json:"date_of_birth" binding:"required,safe_content"`
|
DateOfBirth time.Time `gorm:"not null" json:"date_of_birth" binding:"required,safe_content"`
|
||||||
Company string `json:"company" binding:"omitempty,omitnil,safe_content"`
|
Company string `json:"company" binding:"omitempty,omitnil,safe_content"`
|
||||||
Phone string `json:"phone" binding:"omitempty,omitnil,safe_content"`
|
Phone string `json:"phone" binding:"omitempty,omitnil,safe_content"`
|
||||||
Notes string `json:"notes" binding:"safe_content"`
|
Notes string `json:"notes" binding:"safe_content"`
|
||||||
FirstName string `gorm:"not null" json:"first_name" binding:"required,safe_content"`
|
FirstName string `gorm:"not null" json:"first_name" binding:"required,safe_content"`
|
||||||
Password string `json:"password" binding:"required_unless=RoleID 0,safe_content"`
|
Password string `json:"password" binding:"safe_content"`
|
||||||
Email string `gorm:"unique;not null" json:"email" binding:"required,email,safe_content"`
|
Email string `gorm:"unique;not null" json:"email" binding:"required,email,safe_content"`
|
||||||
LastName string `gorm:"not null" json:"last_name" binding:"required,safe_content"`
|
LastName string `gorm:"not null" json:"last_name" binding:"required,safe_content"`
|
||||||
ProfilePicture string `json:"profile_picture" binding:"omitempty,omitnil,image,safe_content"`
|
ProfilePicture string `json:"profile_picture" binding:"omitempty,omitnil,image,safe_content"`
|
||||||
@@ -31,7 +34,6 @@ type User struct {
|
|||||||
MembershipID uint
|
MembershipID uint
|
||||||
Licence *Licence `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"licence"`
|
Licence *Licence `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"licence"`
|
||||||
LicenceID uint
|
LicenceID uint
|
||||||
ID uint `json:"id"`
|
|
||||||
PaymentStatus int8 `json:"payment_status"`
|
PaymentStatus int8 `json:"payment_status"`
|
||||||
Status int8 `json:"status"`
|
Status int8 `json:"status"`
|
||||||
RoleID int8 `json:"role_id"`
|
RoleID int8 `json:"role_id"`
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ type UserRepositoryInterface interface {
|
|||||||
|
|
||||||
type UserRepository struct{}
|
type UserRepository struct{}
|
||||||
|
|
||||||
|
func PasswordExists(userID *uint) (bool, error) {
|
||||||
|
var user models.User
|
||||||
|
result := database.DB.Select("password").First(&user, userID)
|
||||||
|
if result.Error != nil {
|
||||||
|
return false, result.Error
|
||||||
|
}
|
||||||
|
return user.Password != "", nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ur *UserRepository) CreateUser(user *models.User) (uint, error) {
|
func (ur *UserRepository) CreateUser(user *models.User) (uint, error) {
|
||||||
result := database.DB.Create(user)
|
result := database.DB.Create(user)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
@@ -57,30 +66,6 @@ func (ur *UserRepository) UpdateUser(user *models.User) (*models.User, error) {
|
|||||||
return errors.ErrNoRowsAffected
|
return errors.ErrNoRowsAffected
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the update or creation of Licence and its Categories
|
|
||||||
// if user.Licence != nil {
|
|
||||||
// if existingUser.Licence == nil {
|
|
||||||
// // Create new Licence if it doesn't exist
|
|
||||||
// logger.Error.Printf("Licence creation: %+v", user.Licence)
|
|
||||||
// if err := tx.Create(user.Licence).Error; err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// // Update user with new licence ID
|
|
||||||
// // if err := tx.Model(&existingUser).Update("licence_id", user.Licence.ID).Error; err != nil {
|
|
||||||
// // return err
|
|
||||||
// // }
|
|
||||||
// } else {
|
|
||||||
// // Update existing licence
|
|
||||||
// if err := tx.Model(&existingUser.Licence).Updates(user.Licence).Error; err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// // Replace the Categories with the new list
|
|
||||||
// if err := tx.Model(&existingUser.Licence).Association("Categories").Replace(user.Licence.Categories); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Update the Membership if provided
|
// Update the Membership if provided
|
||||||
if user.Membership.ID != 0 {
|
if user.Membership.ID != 0 {
|
||||||
if err := tx.Model(&existingUser.Membership).Updates(user.Membership).Error; err != nil {
|
if err := tx.Model(&existingUser.Membership).Updates(user.Membership).Error; err != nil {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ func RegisterRoutes(router *gin.Engine, userController *controllers.UserControll
|
|||||||
userRouter.GET("/current", userController.CurrentUserHandler)
|
userRouter.GET("/current", userController.CurrentUserHandler)
|
||||||
userRouter.POST("/logout", userController.LogoutHandler)
|
userRouter.POST("/logout", userController.LogoutHandler)
|
||||||
userRouter.PATCH("/update", userController.UpdateHandler)
|
userRouter.PATCH("/update", userController.UpdateHandler)
|
||||||
|
userRouter.POST("/update", userController.RegisterUser)
|
||||||
userRouter.GET("/all", userController.GetAllUsers)
|
userRouter.GET("/all", userController.GetAllUsers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,20 @@ func validateUser(sl validator.StructLevel) {
|
|||||||
user := sl.Current().Interface().(models.User)
|
user := sl.Current().Interface().(models.User)
|
||||||
|
|
||||||
isSuper := user.RoleID >= constants.Roles.Admin
|
isSuper := user.RoleID >= constants.Roles.Admin
|
||||||
|
|
||||||
|
if user.RoleID > constants.Roles.Member && user.Password == "" {
|
||||||
|
passwordExists, err := repositories.PasswordExists(&user.ID)
|
||||||
|
if err != nil || !passwordExists {
|
||||||
|
logger.Error.Printf("Error checking password exists for user %v: %v", user.Email, err)
|
||||||
|
sl.ReportError(user.Password, "Password", "password", "required", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
// Validate User > 18 years old
|
// Validate User > 18 years old
|
||||||
if !isSuper && user.DateOfBirth.After(time.Now().AddDate(-18, 0, 0)) {
|
if !isSuper && user.DateOfBirth.After(time.Now().AddDate(-18, 0, 0)) {
|
||||||
sl.ReportError(user.DateOfBirth, "DateOfBirth", "date_of_birth", "age", "")
|
sl.ReportError(user.DateOfBirth, "DateOfBirth", "date_of_birth", "age", "")
|
||||||
}
|
}
|
||||||
// validate subscriptionModel
|
// validate subscriptionModel
|
||||||
|
logger.Error.Printf("User: %#v", user)
|
||||||
if user.Membership.SubscriptionModel.Name == "" {
|
if user.Membership.SubscriptionModel.Name == "" {
|
||||||
sl.ReportError(user.Membership.SubscriptionModel.Name, "SubscriptionModel.Name", "name", "required", "")
|
sl.ReportError(user.Membership.SubscriptionModel.Name, "SubscriptionModel.Name", "name", "required", "")
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user