styling, DateOfBirth corrected

This commit is contained in:
Alex
2025-01-31 19:27:15 +01:00
parent c2d5188765
commit 67ef3a2fca
14 changed files with 716 additions and 779 deletions

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

@@ -1,87 +1,87 @@
// See https://kit.svelte.dev/docs/types#app // See https://kit.svelte.dev/docs/types#app
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 | -1;
hourly_rate?: number | -1; hourly_rate?: number | -1;
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 {
id: number | -1; id: number | -1;
status: number | -1; status: number | -1;
start_date: string | ""; start_date: string | '';
end_date: string | ""; end_date: string | '';
parent_member_id: number | -1; parent_member_id: number | -1;
subscription_model: Subscription; subscription_model: Subscription;
} }
interface BankAccount { interface BankAccount {
id: number | -1; id: number | -1;
mandate_date_signed: string | ""; mandate_date_signed: string | '';
bank: string | ""; bank: string | '';
account_holder_name: string | ""; account_holder_name: string | '';
iban: string | ""; iban: string | '';
bic: string | ""; bic: string | '';
mandate_reference: string | ""; mandate_reference: string | '';
} }
interface Licence { interface Licence {
id: number | -1; id: number | -1;
status: number | -1; status: number | -1;
licence_number: string | ""; licence_number: string | '';
issued_date: string | ""; issued_date: string | '';
expiration_date: string | ""; expiration_date: string | '';
country: string | ""; country: string | '';
licence_categories: LicenceCategory[]; licence_categories: LicenceCategory[];
} }
interface LicenceCategory { interface LicenceCategory {
id: number | -1; id: number | -1;
category: string | ""; category: string | '';
} }
interface User { interface User {
email: string | ""; email: string | '';
first_name: string | ""; first_name: string | '';
last_name: string | ""; last_name: string | '';
phone: string | ""; phone: string | '';
notes: string | ""; notes: string | '';
address: string | ""; address: string | '';
zip_code: string | ""; zip_code: string | '';
city: string | ""; city: string | '';
status: number | -1; status: number | -1;
id: number | -1; id: number | -1;
role_id: number | -1; role_id: number | -1;
date_of_birth: string | ""; dateofbirth: string | '';
company: string | ""; company: string | '';
profile_picture: string | ""; profile_picture: string | '';
payment_status: number | -1; payment_status: number | -1;
membership: Membership; membership: Membership;
bank_account: BankAccount; bank_account: BankAccount;
licence: Licence; licence: Licence;
notes: string | ""; notes: string | '';
} }
declare global { declare global {
namespace App { namespace App {
// interface Error {} // interface Error {}
interface Locals { interface Locals {
user: User; user: User;
users: User[]; users: User[];
subscriptions: Subscription[]; subscriptions: Subscription[];
licence_categories: LicenceCategory[]; licence_categories: LicenceCategory[];
} }
interface Types { interface Types {
licenceCategory: LicenceCategory; licenceCategory: LicenceCategory;
} }
// interface PageData {} // interface PageData {}
// interface Platform {} // interface Platform {}
} }
} }
export {}; export {};

View File

@@ -283,7 +283,6 @@
.input { .input {
width: 100%; width: 100%;
margin: 0.5rem 0;
} }
input, input,
textarea, textarea,
@@ -316,7 +315,6 @@
} }
/* Add consistent spacing between input boxes */ /* Add consistent spacing between input boxes */
.input-box { .input-box {
margin: 1rem 0;
padding: 0.5rem; padding: 0.5rem;
background-color: var(--surface0); background-color: var(--surface0);
border-radius: 6px; border-radius: 6px;

View File

@@ -28,28 +28,6 @@
<div class="modal-background"> <div class="modal-background">
<div transition:modal|global={{ duration: 1000 }} class="modal" role="dialog" aria-modal="true"> <div transition:modal|global={{ duration: 1000 }} class="modal" role="dialog" aria-modal="true">
<!-- svelte-ignore a11y-missing-attribute -->
<a
title={$t('cancel')}
class="modal-close"
on:click={closeModal}
role="button"
tabindex="0"
on:keydown={(e) => e.key == 'Enter' && closeModal()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 384 512"
aria-hidden="true"
>
<path
d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"
/>
</svg>
<span class="sr-only">{$t('cancel')}</span>
</a>
<div class="container"> <div class="container">
<slot /> <slot />
</div> </div>
@@ -64,7 +42,7 @@
top: 0; top: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: rgba(30, 30, 46, 0.65); /* var(--base) with 0.75 opacity */ background: var(--modal-backdrop); /* var(--base) with 0.75 opacity */
backdrop-filter: blur(4px); /* Optional: adds a slight blur effect */ backdrop-filter: blur(4px); /* Optional: adds a slight blur effect */
z-index: 9999; z-index: 9999;
display: flex; display: flex;
@@ -149,7 +127,6 @@
.modal .container { .modal .container {
flex-direction: column; flex-direction: column;
left: 0; left: 0;
width: 100%;
} }
} }
</style> </style>

View File

@@ -23,7 +23,7 @@
zip_code: '', zip_code: '',
city: '', city: '',
company: '', company: '',
date_of_birth: '', dateofbirth: '',
notes: '', notes: '',
profile_picture: '', profile_picture: '',
payment_status: 0, payment_status: 0,
@@ -297,11 +297,11 @@
placeholder={$t('placeholder.phone')} placeholder={$t('placeholder.phone')}
/> />
<InputField <InputField
name="user[date_of_birth]" name="user[dateofbirth]"
type="date" type="date"
label={$t('date_of_birth')} label={$t('dateofbirth')}
bind:value={localUser.date_of_birth} bind:value={localUser.dateofbirth}
placeholder={$t('placeholder.date_of_birth')} placeholder={$t('placeholder.dateofbirth')}
/> />
<InputField <InputField
name="user[address]" name="user[address]"
@@ -529,7 +529,7 @@
.category-break { .category-break {
grid-column: 1 / -1; grid-column: 1 / -1;
height: 1px; height: 1px;
background-color: var(--surface0); background-color: var(--overlay0);
margin-top: 10px; margin-top: 10px;
margin-left: 20%; margin-left: 20%;
width: 60%; width: 60%;

View File

@@ -25,6 +25,7 @@
--base: #1e1e2e; --base: #1e1e2e;
--mantle: #181825; --mantle: #181825;
--crust: #11111b; --crust: #11111b;
--modal-backdrop: rgba(49, 50, 68, 0.45); /* For Mocha theme */
} }
@font-face { @font-face {
@@ -385,8 +386,6 @@ li strong {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin: 10px 0;
padding: 10px;
width: 100%; width: 100%;
height: auto; height: auto;
box-sizing: border-box; box-sizing: border-box;

View File

@@ -1,179 +1,176 @@
export default { export default {
userStatus: { userStatus: {
1: "Nicht verifiziert", 1: 'Nicht verifiziert',
2: "Verifiziert", 2: 'Verifiziert',
3: "Aktiv", 3: 'Aktiv',
4: "Passiv", 4: 'Passiv',
5: "Deaktiviert", 5: 'Deaktiviert'
}, },
userRole: { userRole: {
0: "Mitglied", 0: 'Mitglied',
1: "Betrachter", 1: 'Betrachter',
4: "Bearbeiter", 4: 'Bearbeiter',
8: "Administrator", 8: 'Administrator'
}, },
placeholder: { placeholder: {
password: "Passwort eingeben...", password: 'Passwort eingeben...',
email: "Emailadresse eingeben...", email: 'Emailadresse eingeben...',
company: "Firmennamen eingeben...", company: 'Firmennamen eingeben...',
first_name: "Vornamen eingeben...", first_name: 'Vornamen eingeben...',
last_name: "Nachnamen eingeben...", last_name: 'Nachnamen eingeben...',
phone: "Telefonnummer eingeben...", phone: 'Telefonnummer eingeben...',
address: "Straße und Hausnummer eingeben...", address: 'Straße und Hausnummer eingeben...',
zip_code: "Postleitzahl eingeben...", zip_code: 'Postleitzahl eingeben...',
city: "Wohnort eingeben...", city: 'Wohnort eingeben...',
bank_name: "Namen der Bank eingeben...", bank_name: 'Namen der Bank eingeben...',
parent_member_id: "Mitgliedsnr des Hauptmitglieds eingeben...", parent_member_id: 'Mitgliedsnr des Hauptmitglieds eingeben...',
bank_account_holder: "Namen eingeben...", bank_account_holder: 'Namen eingeben...',
iban: "IBAN eingeben..", iban: 'IBAN eingeben..',
bic: "BIC eingeben(Bei nicht deutschen Konten)...", bic: 'BIC eingeben(Bei nicht deutschen Konten)...',
mandate_reference: "SEPA Mandatsreferenz eingeben..", mandate_reference: 'SEPA Mandatsreferenz eingeben..',
notes: "Deine Notizen zu {name}...", notes: 'Deine Notizen zu {name}...',
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'
}, },
validation: { validation: {
required: "Eingabe benötigt", required: 'Eingabe benötigt',
password: "Password zu kurz, mindestens 8 Zeichen", password: 'Password zu kurz, mindestens 8 Zeichen',
password_match: "Passwörter stimmen nicht überein!", password_match: 'Passwörter stimmen nicht überein!',
phone: "Ungültiges Format(+491738762387 oder 0173850698)", phone: 'Ungültiges Format(+491738762387 oder 0173850698)',
zip_code: "Ungültige Postleitzahl(Nur deutsche Wohnorte sind zulässig)", zip_code: 'Ungültige Postleitzahl(Nur deutsche Wohnorte sind zulässig)',
bic: "Ungültige BIC", bic: 'Ungültige BIC',
iban: "Ungültige IBAN", iban: 'Ungültige IBAN',
date: "Bitte geben Sie ein Datum ein", date: 'Bitte geben Sie ein Datum ein',
email: "Ungültige Emailadresse", email: 'Ungültige Emailadresse',
licence: "Nummer zu kurz(11 Zeichen)", licence: 'Nummer zu kurz(11 Zeichen)'
}, },
server: { server: {
error: { error: {
invalid_json: "JSON Daten sind ungültig", invalid_json: 'JSON Daten sind ungültig',
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: 'Sie sind nicht befugt diese Handlung 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 jemanden vom Verein an.'
"Verdammt, Fehler auf unserer Seite, probieren Sie es nochmal, danach rufen Sie jemanden vom Verein an.", },
}, validation: {
validation: { invalid_user_id: 'Nutzer ID ungültig',
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", user_not_found_or_wrong_password: 'Existiert nicht oder falsches Passwort',
user_not_found_or_wrong_password: email_already_registered: 'Ein Mitglied wurde schon mit dieser Emailadresse erstellt.',
"Existiert nicht oder falsches Passwort", alphanumunicode: 'beinhaltet nicht erlaubte Zeichen',
email_already_registered: safe_content: 'I see what you did there! Do not cross this line!',
"Ein Mitglied wurde schon mit dieser Emailadresse erstellt.", iban: 'Ungültig. Format: DE07123412341234123412',
alphanumunicode: "beinhaltet nicht erlaubte Zeichen", bic: 'Ungültig. Format: BELADEBEXXX',
safe_content: "I see what you did there! Do not cross this line!", email: 'Format ungültig',
iban: "Ungültig. Format: DE07123412341234123412", number: 'Ist keine Nummer',
bic: "Ungültig. Format: BELADEBEXXX", euDriversLicence: 'Ist kein europäischer Führerschein',
email: "Format ungültig", lte: 'Ist zu groß/neu',
number: "Ist keine Nummer", gt: 'Ist zu klein/alt',
euDriversLicence: "Ist kein europäischer Führerschein", required: 'Feld wird benötigt',
lte: "Ist zu groß/neu", image: 'Dies ist kein Bild',
gt: "Ist zu klein/alt", alphanum: 'beinhaltet ungültige Zeichen',
required: "Feld wird benötigt", alphaunicode: 'darf nur aus Buchstaben bestehen'
image: "Dies ist kein Bild", }
alphanum: "beinhaltet ungültige Zeichen", },
alphaunicode: "darf nur aus Buchstaben bestehen", licenceCategory: {
}, AM: 'Mopeds und leichte vierrädrige Kraftfahrzeuge (50ccm, max 45km/h)',
}, A1: 'Leichte Motorräder (125ccm)',
licenceCategory: { A2: 'Motorräder mit mittlerer Leistung (max 35kW)',
AM: "Mopeds und leichte vierrädrige Kraftfahrzeuge (50ccm, max 45km/h)", A: 'Motorräder',
A1: "Leichte Motorräder (125ccm)", B: 'Kraftfahrzeuge ≤ 3500 kg, ≤ 8 Sitzplätze',
A2: "Motorräder mit mittlerer Leistung (max 35kW)", C1: 'Mittelschwere Fahrzeuge -7500 kg',
A: "Motorräder", C: 'Schwere Nutzfahrzeuge > 3500 kg',
B: "Kraftfahrzeuge ≤ 3500 kg, ≤ 8 Sitzplätze", D1: 'Kleinbusse 9-16 Sitzplätze',
C1: "Mittelschwere Fahrzeuge -7500 kg", D: 'Busse > 8 Sitzplätze',
C: "Schwere Nutzfahrzeuge > 3500 kg", BE: 'Fahrzeugklasse B mit Anhänger',
D1: "Kleinbusse 9-16 Sitzplätze", C1E: 'Fahrzeugklasse C1 mit Anhänger',
D: "Busse > 8 Sitzplätze", CE: 'Fahrzeugklasse C mit Anhänger',
BE: "Fahrzeugklasse B mit Anhänger", D1E: 'Fahrzeugklasse D1 mit Anhänger',
C1E: "Fahrzeugklasse C1 mit Anhänger", DE: 'Fahrzeugklasse D mit Anhänger',
CE: "Fahrzeugklasse C mit Anhänger", L: 'Land-, Forstwirtschaftsfahrzeuge, Stapler max 40km/h',
D1E: "Fahrzeugklasse D1 mit Anhänger", T: 'Land-, Forstwirtschaftsfahrzeuge, Stapler max 60km/h'
DE: "Fahrzeugklasse D mit Anhänger", },
L: "Land-, Forstwirtschaftsfahrzeuge, Stapler max 40km/h", users: 'Mitglieder',
T: "Land-, Forstwirtschaftsfahrzeuge, Stapler max 60km/h", user: {
}, login: 'Nutzer Anmeldung',
users: "Mitglieder", edit: 'Nutzer bearbeiten',
user: { user: 'Nutzer',
login: "Nutzer Anmeldung", management: 'Mitgliederverwaltung',
edit: "Nutzer bearbeiten", id: 'Mitgliedsnr',
user: "Nutzer", name: 'Name',
management: "Mitgliederverwaltung", email: 'Email',
id: "Mitgliedsnr", status: 'Status',
name: "Name", role: 'Nutzerrolle'
email: "Email", },
status: "Status", cancel: 'Abbrechen',
role: "Nutzerrolle", confirm: 'Bestätigen',
}, actions: 'Aktionen',
cancel: "Abbrechen", edit: 'Bearbeiten',
confirm: "Bestätigen", delete: 'Löschen',
actions: "Aktionen", mandate_date_signed: 'Mandatserteilungsdatum',
edit: "Bearbeiten", licence_categories: 'Führerscheinklassen',
delete: "Löschen", subscription_model: 'Mitgliedschatfsmodell',
mandate_date_signed: "Mandatserteilungsdatum", licence: 'Führerschein',
licence_categories: "Führerscheinklassen", licence_number: 'Führerscheinnummer',
subscription_model: "Mitgliedschatfsmodell", issued_date: 'Ausgabedatum',
licence: "Führerschein", expiration_date: 'Ablaufdatum',
licence_number: "Führerscheinnummer", country: 'Land',
issued_date: "Ausgabedatum", monthly_fee: 'Monatliche Gebühr',
expiration_date: "Ablaufdatum", hourly_rate: 'Stundensatz',
country: "Land", details: 'Details',
monthly_fee: "Monatliche Gebühr", conditions: 'Bedingungen',
hourly_rate: "Stundensatz", unknown: 'Unbekannt',
details: "Details", notes: 'Notizen',
conditions: "Bedingungen", address: 'Straße & Hausnummer',
unknown: "Unbekannt", city: 'Wohnort',
notes: "Notizen", zip_code: 'PLZ',
address: "Straße & Hausnummer", forgot_password: 'Passwort vergessen?',
city: "Wohnort", password: 'Passwort',
zip_code: "PLZ", password_repeat: 'Passwort wiederholen',
forgot_password: "Passwort vergessen?", email: 'Email',
password: "Passwort", company: 'Firma',
password_repeat: "Passwort wiederholen", login: 'Anmeldung',
email: "Email", profile: 'Profil',
company: "Firma", membership: 'Mitgliedschaft',
login: "Anmeldung", bankaccount: 'Kontodaten',
profile: "Profil", first_name: 'Vorname',
membership: "Mitgliedschaft", last_name: 'Nachname',
bankaccount: "Kontodaten", name: 'Name',
first_name: "Vorname", phone: 'Telefonnummer',
last_name: "Nachname", dateofbirth: 'Geburtstag',
name: "Name", status: 'Status',
phone: "Telefonnummer", start: 'Beginn',
date_of_birth: "Geburtstag", end: 'Ende',
status: "Status", parent_member_id: 'Hauptmitgliedsnr.',
start: "Beginn", bank_account_holder: 'Kontoinhaber',
end: "Ende", bank_name: 'Bank',
parent_member_id: "Hauptmitgliedsnr.", iban: 'IBAN',
bank_account_holder: "Kontoinhaber", bic: 'BIC',
bank_name: "Bank", mandate_reference: 'SEPA Mandat',
iban: "IBAN", subscriptions: 'Tarifmodelle',
bic: "BIC", payments: 'Zahlungen',
mandate_reference: "SEPA Mandat", add_new: 'Neu',
subscriptions: "Tarifmodelle", included_hours_per_year: 'Inkludierte Stunden pro Jahr',
payments: "Zahlungen", included_hours_per_month: 'Inkludierte Stunden pro Monat',
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: {
id: "Zahlungs-Nr", id: 'Zahlungs-Nr',
amount: "Betrag", amount: 'Betrag',
date: "Datum", date: 'Datum',
status: "Status", status: 'Status'
}, },
// For subscription statuses // For subscription statuses
subscriptionStatus: { subscriptionStatus: {
pending: "Ausstehend", pending: 'Ausstehend',
completed: "Abgeschlossen", completed: 'Abgeschlossen',
failed: "Fehlgeschlagen", failed: 'Fehlgeschlagen',
cancelled: "Storniert", cancelled: 'Storniert'
}, }
}; };

View File

@@ -1,24 +1,24 @@
// @ts-nocheck // @ts-nocheck
import { quintOut } from "svelte/easing"; import { quintOut } from 'svelte/easing';
import { crossfade } from "svelte/transition"; import { crossfade } from 'svelte/transition';
export const [send, receive] = crossfade({ export const [send, receive] = crossfade({
duration: (d) => Math.sqrt(d * 200), duration: (d) => Math.sqrt(d * 200),
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
fallback(node, params) { fallback(node, params) {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const transform = style.transform === "none" ? "" : style.transform; const transform = style.transform === 'none' ? '' : style.transform;
return { return {
duration: 600, duration: 600,
easing: quintOut, easing: quintOut,
css: (t) => ` css: (t) => `
transform: ${transform} scale(${t}); transform: ${transform} scale(${t});
opacity: ${t} opacity: ${t}
`, `
}; };
}, }
}); });
/** /**
@@ -27,9 +27,9 @@ export const [send, receive] = crossfade({
* @param {string} email - The email to validate * @param {string} email - The email to validate
*/ */
export const isValidEmail = (email) => { export const isValidEmail = (email) => {
const EMAIL_REGEX = const EMAIL_REGEX =
/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/; /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
return EMAIL_REGEX.test(email.trim()); return EMAIL_REGEX.test(email.trim());
}; };
/** /**
* Validates a strong password field * Validates a strong password field
@@ -37,11 +37,9 @@ export const isValidEmail = (email) => {
* @param {string} password - The password to validate * @param {string} password - The password to validate
*/ */
export const isValidPasswordStrong = (password) => { export const isValidPasswordStrong = (password) => {
const strongRegex = new RegExp( const strongRegex = new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})');
"^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})"
);
return strongRegex.test(password.trim()); return strongRegex.test(password.trim());
}; };
/** /**
* Validates a medium password field * Validates a medium password field
@@ -49,11 +47,11 @@ export const isValidPasswordStrong = (password) => {
* @param {string} password - The password to validate * @param {string} password - The password to validate
*/ */
export const isValidPasswordMedium = (password) => { export const isValidPasswordMedium = (password) => {
const mediumRegex = new RegExp( const mediumRegex = new RegExp(
"^(((?=.*[a-z])(?=.*[A-Z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9])))(?=.{6,})" '^(((?=.*[a-z])(?=.*[A-Z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9])))(?=.{6,})'
); );
return mediumRegex.test(password.trim()); return mediumRegex.test(password.trim());
}; };
/** /**
@@ -63,22 +61,22 @@ export const isValidPasswordMedium = (password) => {
*/ */
export function isEmpty(obj) { export function isEmpty(obj) {
for (const _i in obj) { for (const _i in obj) {
return false; return false;
} }
return true; return true;
} }
export function toRFC3339(dateString) { export function toRFC3339(dateString) {
if (!dateString) dateString = "0001-01-01T00:00:00.000Z"; if (!dateString) dateString = '0001-01-01T00:00:00.000Z';
const date = new Date(dateString); const date = new Date(dateString);
return date.toISOString(); return date.toISOString();
} }
export function fromRFC3339(dateString) { export function fromRFC3339(dateString) {
if (!dateString) dateString = "0001-01-01T00:00:00.000Z"; if (!dateString) dateString = '0001-01-01T00:00:00.000Z';
const date = new Date(dateString); const date = new Date(dateString);
return date.toISOString().split("T")[0]; return date.toISOString().split('T')[0];
} }
/** /**
@@ -86,28 +84,26 @@ export function fromRFC3339(dateString) {
* @param {App.Locals.User} user - The user object to format * @param {App.Locals.User} user - The user object to format
*/ */
export function userDatesFromRFC3339(user) { export function userDatesFromRFC3339(user) {
if (user.date_of_birth) { if (user.dateofbirth) {
user.date_of_birth = fromRFC3339(user.date_of_birth); user.dateofbirth = fromRFC3339(user.dateofbirth);
} }
if (user.membership) { if (user.membership) {
if (user.membership.start_date) { if (user.membership.start_date) {
user.membership.start_date = fromRFC3339(user.membership.start_date); user.membership.start_date = fromRFC3339(user.membership.start_date);
} }
if (user.membership.end_date) { if (user.membership.end_date) {
user.membership.end_date = fromRFC3339(user.membership.end_date); user.membership.end_date = fromRFC3339(user.membership.end_date);
} }
} }
if (user.licence?.issued_date) { if (user.licence?.issued_date) {
user.licence.issued_date = fromRFC3339(user.licence.issued_date); user.licence.issued_date = fromRFC3339(user.licence.issued_date);
} }
if (user.licence?.expiration_date) { if (user.licence?.expiration_date) {
user.licence.expiration_date = fromRFC3339(user.licence.expiration_date); user.licence.expiration_date = fromRFC3339(user.licence.expiration_date);
} }
if (user.bank_account && user.bank_account.mandate_date_signed) { if (user.bank_account && user.bank_account.mandate_date_signed) {
user.bank_account.mandate_date_signed = fromRFC3339( user.bank_account.mandate_date_signed = fromRFC3339(user.bank_account.mandate_date_signed);
user.bank_account.mandate_date_signed }
);
}
} }
/** /**
@@ -115,28 +111,26 @@ export function userDatesFromRFC3339(user) {
* @param {App.Locals.User} user - The user object to format * @param {App.Locals.User} user - The user object to format
*/ */
export function userDatesToRFC3339(user) { export function userDatesToRFC3339(user) {
if (user.date_of_birth) { if (user.dateofbirth) {
user.date_of_birth = toRFC3339(user.date_of_birth); user.dateofbirth = toRFC3339(user.dateofbirth);
} }
if (user.membership) { if (user.membership) {
if (user.membership.start_date) { if (user.membership.start_date) {
user.membership.start_date = toRFC3339(user.membership.start_date); user.membership.start_date = toRFC3339(user.membership.start_date);
} }
if (user.membership.end_date) { if (user.membership.end_date) {
user.membership.end_date = toRFC3339(user.membership.end_date); user.membership.end_date = toRFC3339(user.membership.end_date);
} }
} }
if (user.licence?.issued_date) { if (user.licence?.issued_date) {
user.licence.issued_date = toRFC3339(user.licence.issued_date); user.licence.issued_date = toRFC3339(user.licence.issued_date);
} }
if (user.licence?.expiration_date) { if (user.licence?.expiration_date) {
user.licence.expiration_date = toRFC3339(user.licence.expiration_date); user.licence.expiration_date = toRFC3339(user.licence.expiration_date);
} }
if (user.bank_account && user.bank_account.mandate_date_signed) { if (user.bank_account && user.bank_account.mandate_date_signed) {
user.bank_account.mandate_date_signed = toRFC3339( user.bank_account.mandate_date_signed = toRFC3339(user.bank_account.mandate_date_signed);
user.bank_account.mandate_date_signed }
);
}
} }
/** /**
@@ -145,33 +139,33 @@ export function userDatesToRFC3339(user) {
* @returns {array} The formatted error object * @returns {array} The formatted error object
*/ */
export function formatError(obj) { export function formatError(obj) {
const errors = []; const errors = [];
if (typeof obj === "object" && obj !== null) { if (typeof obj === 'object' && obj !== null) {
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
obj.forEach((error) => { obj.forEach((error) => {
errors.push({ errors.push({
field: error.field, field: error.field,
key: error.key, key: error.key,
id: Math.random() * 1000, id: Math.random() * 1000
}); });
}); });
} else { } else {
Object.keys(obj).forEach((field) => { Object.keys(obj).forEach((field) => {
errors.push({ errors.push({
field: field, field: field,
key: obj[field].key, key: obj[field].key,
id: Math.random() * 1000, id: Math.random() * 1000
}); });
}); });
} }
} else { } else {
errors.push({ errors.push({
field: "general", field: 'general',
key: obj, key: obj,
id: 0, id: 0
}); });
} }
return errors; return errors;
} }
/** /**
@@ -180,26 +174,26 @@ export function formatError(obj) {
* @param {import('RequestEvent<Partial<Record<string, string>>, string | null>')} event - The event object * @param {import('RequestEvent<Partial<Record<string, string>>, string | null>')} event - The event object
*/ */
export function refreshCookie(newToken, event) { export function refreshCookie(newToken, event) {
if (newToken) { if (newToken) {
const match = newToken.match(/jwt=([^;]+)/); const match = newToken.match(/jwt=([^;]+)/);
if (match) { if (match) {
if (event) { if (event) {
event.cookies.set("jwt", match[1], { event.cookies.set('jwt', match[1], {
path: "/", path: '/',
httpOnly: true, httpOnly: true,
secure: process.env.NODE_ENV === "production", // Secure in production secure: process.env.NODE_ENV === 'production', // Secure in production
sameSite: "lax", sameSite: 'lax',
maxAge: 5 * 24 * 60 * 60, // 5 days in seconds maxAge: 5 * 24 * 60 * 60 // 5 days in seconds
}); });
} else { } else {
cookies.set("jwt", match[1], { cookies.set('jwt', match[1], {
path: "/", path: '/',
httpOnly: true, httpOnly: true,
secure: process.env.NODE_ENV === "production", // Secure in production secure: process.env.NODE_ENV === 'production', // Secure in production
sameSite: "lax", sameSite: 'lax',
maxAge: 5 * 24 * 60 * 60, // 5 days in seconds maxAge: 5 * 24 * 60 * 60 // 5 days in seconds
}); });
} }
} }
} }
} }

View File

@@ -0,0 +1,121 @@
import { toRFC3339 } from './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
*/
export 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
*/
export 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 || ''),
dateofbirth: toRFC3339(rawData.user.dateofbirth),
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)
}
}
};
// Remove undefined or null properties
const cleanUpdateData = JSON.parse(JSON.stringify(processedData), (key, value) =>
value !== null && value !== '' ? value : undefined
);
console.dir(cleanUpdateData);
return cleanUpdateData;
}

View File

@@ -1,11 +1,7 @@
import { BASE_API_URI } from "$lib/utils/constants"; import { BASE_API_URI } from '$lib/utils/constants';
import { import { formatError, userDatesFromRFC3339 } from '$lib/utils/helpers';
formatError, import { fail, redirect } from '@sveltejs/kit';
userDatesFromRFC3339, import { formDataToObject, processFormData } from '$lib/utils/processing';
userDatesToRFC3339,
} from "$lib/utils/helpers";
import { fail, redirect } from "@sveltejs/kit";
import { toRFC3339 } from "$lib/utils/helpers";
/** /**
* @typedef {Object} UpdateData * @typedef {Object} UpdateData
@@ -14,187 +10,125 @@ import { toRFC3339 } from "$lib/utils/helpers";
/** @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
if (!locals.user) { if (!locals.user) {
throw redirect(302, `/auth/login?next=/auth/about/${params.id}`); throw redirect(302, `/auth/login?next=/auth/about/${params.id}`);
} }
} }
/** @type {import('./$types').Actions} */ /** @type {import('./$types').Actions} */
export const actions = { export const actions = {
/** /**
* *
* @param request - The request object * @param request - The request object
* @param fetch - Fetch object from sveltekit * @param fetch - Fetch object from sveltekit
* @param cookies - SvelteKit's cookie object * @param cookies - SvelteKit's cookie object
* @param locals - The local object, housing current user * @param locals - The local object, housing current user
* @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
*/ */
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 rawData = formDataToObject(formData);
const licenceCategories = formData const processedData = processFormData(rawData);
.getAll("licence_categories[]")
.filter((value) => typeof value === "string")
.map((value) => {
try {
return JSON.parse(value);
} catch (e) {
console.error("Failed to parse licence category:", value);
return null;
}
});
/** @type {Partial<App.Locals['user']>} */ console.dir(processedData.user.membership);
const userData = { const isCreating = !processedData.user.id || processedData.user.id === 0;
id: Number(formData.get("id")), console.log('Is updating: ', isCreating);
first_name: String(formData.get("first_name")), console.dir(formData);
last_name: String(formData.get("last_name")), const apiURL = `${BASE_API_URI}/backend/users/update/`;
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("date_of_birth")),
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(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,
},
};
// userDatesToRFC3339(userData); /** @type {RequestInit} */
const requestUpdateOptions = {
method: 'PATCH',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get('jwt')}`
},
body: JSON.stringify(processedData)
};
const res = await fetch(apiURL, requestUpdateOptions);
/** @type {UpdateData} */ if (!res.ok) {
const updateData = { user: userData }; const response = await res.json();
// Remove undefined or null properties const errors = formatError(response.errors);
const cleanUpdateData = JSON.parse( return fail(400, { errors: errors });
JSON.stringify(updateData), }
(key, value) => (value !== null && value !== "" ? value : undefined)
);
console.dir(formData); const response = await res.json();
console.dir(cleanUpdateData); locals.user = response;
const apiURL = `${BASE_API_URI}/backend/users/update/`; userDatesFromRFC3339(locals.user);
throw redirect(303, `/auth/about/${response.id}`);
},
/** @type {RequestInit} */ /**
const requestUpdateOptions = { *
method: "PATCH", * @param request - The request object
credentials: "include", * @param fetch - Fetch object from sveltekit
headers: { * @param cookies - SvelteKit's cookie object
"Content-Type": "application/json", * @param locals - The local object, housing current user
Cookie: `jwt=${cookies.get("jwt")}`, * @returns Error data or redirects user to the home page or the previous page
}, */
body: JSON.stringify(cleanUpdateData), uploadImage: async ({ request, fetch, cookies }) => {
}; const formData = await request.formData();
const res = await fetch(apiURL, requestUpdateOptions);
if (!res.ok) { /** @type {RequestInit} */
const response = await res.json(); const requestInitOptions = {
const errors = formatError(response.errors); method: 'POST',
return fail(400, { errors: errors }); headers: {
} Cookie: `jwt=${cookies.get('jwt')}`
},
body: formData
};
const response = await res.json(); const res = await fetch(`${BASE_API_URI}/file/upload/`, requestInitOptions);
locals.user = response;
userDatesFromRFC3339(locals.user);
throw redirect(303, `/auth/about/${response.id}`);
},
/**
*
* @param request - The request object
* @param fetch - Fetch object from sveltekit
* @param cookies - SvelteKit's cookie object
* @param locals - The local object, housing current user
* @returns Error data or redirects user to the home page or the previous page
*/
uploadImage: async ({ request, fetch, cookies }) => {
const formData = await request.formData();
/** @type {RequestInit} */ if (!res.ok) {
const requestInitOptions = { const response = await res.json();
method: "POST", const errors = formatError(response.errors);
headers: { return fail(400, { errors: errors });
Cookie: `jwt=${cookies.get("jwt")}`, }
},
body: formData,
};
const res = await fetch(`${BASE_API_URI}/file/upload/`, requestInitOptions); const response = await res.json();
if (!res.ok) { return {
const response = await res.json(); success: true,
const errors = formatError(response.errors); profile_picture: response['']
return fail(400, { errors: errors }); };
} },
const response = await res.json(); /**
*
* @param request - The request object
* @param fetch - Fetch object from sveltekit
* @param cookies - SvelteKit's cookie object
* @param locals - The local object, housing current user
* @returns Error data or redirects user to the home page or the previous page
*/
deleteImage: async ({ request, fetch, cookies }) => {
const formData = await request.formData();
return { /** @type {RequestInit} */
success: true, const requestInitOptions = {
profile_picture: response[""], method: 'DELETE',
}; headers: {
}, Cookie: `jwt=${cookies.get('jwt')}`
},
body: formData
};
/** const res = await fetch(`${BASE_API_URI}/file/delete/`, requestInitOptions);
*
* @param request - The request object
* @param fetch - Fetch object from sveltekit
* @param cookies - SvelteKit's cookie object
* @param locals - The local object, housing current user
* @returns Error data or redirects user to the home page or the previous page
*/
deleteImage: async ({ request, fetch, cookies }) => {
const formData = await request.formData();
/** @type {RequestInit} */ if (!res.ok) {
const requestInitOptions = { const response = await res.json();
method: "DELETE", const errors = formatError(response.errors);
headers: { return fail(400, { errors: errors });
Cookie: `jwt=${cookies.get("jwt")}`, }
},
body: formData,
};
const res = await fetch(`${BASE_API_URI}/file/delete/`, requestInitOptions); return {
success: true,
if (!res.ok) { profile_picture: ''
const response = await res.json(); };
const errors = formatError(response.errors); }
return fail(400, { errors: errors });
}
return {
success: true,
profile_picture: "",
};
},
}; };

View File

@@ -65,10 +65,10 @@
<span class="value">{user.phone}</span> <span class="value">{user.phone}</span>
</h3> </h3>
{/if} {/if}
{#if user.date_of_birth} {#if user.dateofbirth}
<h3 class="hero-subtitle subtitle info-row"> <h3 class="hero-subtitle subtitle info-row">
<span class="label">Geburtstag:</span> <span class="label">Geburtstag:</span>
<span class="value">{user.date_of_birth}</span> <span class="value">{user.dateofbirth}</span>
</h3> </h3>
{/if} {/if}
{#if user.notes} {#if user.notes}

View File

@@ -2,194 +2,63 @@
// - Implement a load function to fetch a list of all users. // - Implement a load function to fetch a list of all users.
// - Create actions for updating user information (similar to the about/[id] route). // - Create actions for updating user information (similar to the about/[id] route).
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 } from '$lib/utils/helpers';
import { fail, redirect } from "@sveltejs/kit"; import { fail, redirect } from '@sveltejs/kit';
import { toRFC3339 } from "$lib/utils/helpers"; import { formDataToObject, processFormData } from '$lib/utils/processing';
/**
* 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
if (!locals.user) { if (!locals.user) {
throw redirect(302, `/auth/login?next=/auth/users`); throw redirect(302, `/auth/login?next=/auth/users`);
} }
} }
/** @type {import('./$types').Actions} */ /** @type {import('./$types').Actions} */
export const actions = { export const actions = {
/** /**
* *
* @param request - The request object * @param request - The request object
* @param fetch - Fetch object from sveltekit * @param fetch - Fetch object from sveltekit
* @param cookies - SvelteKit's cookie object * @param cookies - SvelteKit's cookie object
* @param locals - The local object, housing current user * @param locals - The local object, housing current user
* @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
*/ */
updateUser: async ({ request, fetch, cookies, locals }) => { updateUser: async ({ request, fetch, cookies, locals }) => {
let formData = await request.formData(); let formData = await request.formData();
// Convert form data to nested object const rawData = formDataToObject(formData);
const rawData = formDataToObject(formData); const processedData = processFormData(rawData);
const processedData = processFormData(rawData); console.dir(processedData.user.membership);
const isCreating = !processedData.user.id || processedData.user.id === 0;
console.log('Is creating: ', isCreating);
const apiURL = `${BASE_API_URI}/backend/users/update`;
// Remove undefined or null properties /** @type {RequestInit} */
const cleanUpdateData = JSON.parse( const requestOptions = {
JSON.stringify(processedData), method: isCreating ? 'POST' : 'PATCH',
(key, value) => (value !== null && value !== "" ? value : undefined) credentials: 'include',
); headers: {
console.dir(processedData.user.membership); 'Content-Type': 'application/json',
const isCreating = !processedData.user.id || processedData.user.id === 0; Cookie: `jwt=${cookies.get('jwt')}`
console.log("Is creating: ", isCreating); },
const apiURL = `${BASE_API_URI}/backend/users/update`; body: JSON.stringify(processedData)
};
/** @type {RequestInit} */ const res = await fetch(apiURL, requestOptions);
const requestOptions = {
method: isCreating ? "POST" : "PATCH",
credentials: "include",
headers: {
"Content-Type": "application/json",
Cookie: `jwt=${cookies.get("jwt")}`,
},
body: JSON.stringify(cleanUpdateData),
};
const res = await fetch(apiURL, requestOptions); if (!res.ok) {
const response = await res.json();
const errors = formatError(response.errors);
return fail(400, { errors: errors });
}
if (!res.ok) { const response = await res.json();
const response = await res.json(); console.log('Server success response:', response);
const errors = formatError(response.errors); locals.user = response;
return fail(400, { errors: errors }); userDatesFromRFC3339(locals.user);
} throw redirect(303, `/auth/about/${response.id}`);
}
const response = await res.json();
console.log("Server success response:", response);
locals.user = response;
userDatesFromRFC3339(locals.user);
throw redirect(303, `/auth/about/${response.id}`);
},
}; };

View File

@@ -58,8 +58,8 @@
on:click={() => setActiveSection('users')} on:click={() => setActiveSection('users')}
> >
<i class="fas fa-users"></i> <i class="fas fa-users"></i>
<span class="nav-badge">{users.length}</span>
{$t('users')} {$t('users')}
<span class="nav-badge">{users.length}</span>
</button> </button>
</li> </li>
<li> <li>
@@ -68,8 +68,8 @@
on:click={() => setActiveSection('subscriptions')} on:click={() => setActiveSection('subscriptions')}
> >
<i class="fas fa-clipboard-list"></i> <i class="fas fa-clipboard-list"></i>
<span class="nav-badge">{subscriptions.length}</span>
{$t('subscriptions')} {$t('subscriptions')}
<span class="nav-badge">{subscriptions.length}</span>
</button> </button>
</li> </li>
<li> <li>
@@ -238,7 +238,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 0 1rem; padding: 0 1rem;
color: white; color: var(--text);
} }
.layout { .layout {
@@ -252,8 +252,8 @@
.sidebar { .sidebar {
width: 250px; width: 250px;
min-height: 600px; min-height: 600px;
background: #2f2f2f; background: var(--surface0);
border-right: 1px solid #494848; border-right: 1px solid var(--surface1);
} }
.nav-list { .nav-list {
@@ -272,7 +272,7 @@
background: none; background: none;
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
color: #9b9b9b; color: var(--subtext0);
text-transform: uppercase; text-transform: uppercase;
font-weight: 500; font-weight: 500;
letter-spacing: 1px; letter-spacing: 1px;
@@ -280,12 +280,14 @@
} }
.nav-link:hover { .nav-link:hover {
background: #fdfff5; background: var(--surface1);
color: var(--text);
} }
.nav-link.active { .nav-link.active {
background: #494848; background: var(--surface2);
color: white; color: var(--lavender);
border-left: 3px solid var(--mauve);
} }
.main-content { .main-content {
@@ -295,20 +297,29 @@
.accordion-item { .accordion-item {
border: none; border: none;
background: #2f2f2f; background: var(--surface0);
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
border-radius: 8px;
overflow: hidden;
} }
.accordion-header { .accordion-header {
padding: 1rem; padding: 1rem;
cursor: pointer; cursor: pointer;
font-family: 'Roboto Mono', monospace; font-family: 'Roboto Mono', monospace;
color: white; color: var(--text);
background: var(--surface1);
transition: background-color 0.2s ease-in-out;
}
.accordion-header:hover {
background: var(--surface2);
} }
.accordion-content { .accordion-content {
padding: 1rem; padding: 1rem;
background: #494848; background: var(--surface0);
border-top: 1px solid var(--surface1);
} }
.table { .table {
@@ -325,11 +336,11 @@
} }
.table th { .table th {
color: #9b9b9b; color: var(--subtext1);
} }
.table td { .table td {
color: white; color: var(--text);
} }
@media (max-width: 680px) { @media (max-width: 680px) {
@@ -358,5 +369,42 @@
.section-header h2 { .section-header h2 {
margin: 0; margin: 0;
color: var(--lavender);
}
/* Additional styles for better visual hierarchy */
details[open] .accordion-header {
background: var(--surface2);
color: var(--lavender);
}
.button-group {
margin-top: 1rem;
display: flex;
gap: 0.5rem;
}
/* Style for the nav badge */
.nav-badge {
background: var(--surface2);
color: var(--text);
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
margin-left: auto;
}
/* Improved focus states */
.nav-link:focus,
.accordion-header:focus {
outline: 2px solid var(--mauve);
outline-offset: -2px;
border-radius: 8px;
}
/* Add subtle transitions */
.accordion-item,
.accordion-header,
.nav-link {
transition: all 0.2s ease-in-out;
} }
</style> </style>

View File

@@ -13,7 +13,7 @@ type User struct {
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"` 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:"dateofbirth" 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"`
@@ -63,19 +63,19 @@ func (u *User) PasswordMatches(plaintextPassword string) (bool, error) {
func (u *User) Safe() map[string]interface{} { func (u *User) Safe() map[string]interface{} {
result := map[string]interface{}{ result := map[string]interface{}{
"email": u.Email, "email": u.Email,
"first_name": u.FirstName, "first_name": u.FirstName,
"last_name": u.LastName, "last_name": u.LastName,
"phone": u.Phone, "phone": u.Phone,
"notes": u.Notes, "notes": u.Notes,
"address": u.Address, "address": u.Address,
"zip_code": u.ZipCode, "zip_code": u.ZipCode,
"city": u.City, "city": u.City,
"status": u.Status, "status": u.Status,
"id": u.ID, "id": u.ID,
"role_id": u.RoleID, "role_id": u.RoleID,
"company": u.Company, "company": u.Company,
"date_of_birth": u.DateOfBirth, "dateofbirth": u.DateOfBirth,
"membership": map[string]interface{}{ "membership": map[string]interface{}{
"id": u.Membership.ID, "id": u.Membership.ID,
"start_date": u.Membership.StartDate, "start_date": u.Membership.StartDate,

View File

@@ -24,7 +24,7 @@ func validateUser(sl validator.StructLevel) {
} }
// 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", "dateofbirth", "age", "")
} }
// validate subscriptionModel // validate subscriptionModel
logger.Error.Printf("User: %#v", user) logger.Error.Printf("User: %#v", user)