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

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

@@ -2,9 +2,9 @@
interface Subscription {
id: number | -1;
name: string | "";
details?: string | "";
conditions?: string | "";
name: string | '';
details?: string | '';
conditions?: string | '';
monthly_fee?: number | -1;
hourly_rate?: number | -1;
included_hours_per_year?: number | 0;
@@ -14,57 +14,57 @@ interface Subscription {
interface Membership {
id: number | -1;
status: number | -1;
start_date: string | "";
end_date: string | "";
start_date: string | '';
end_date: string | '';
parent_member_id: number | -1;
subscription_model: Subscription;
}
interface BankAccount {
id: number | -1;
mandate_date_signed: string | "";
bank: string | "";
account_holder_name: string | "";
iban: string | "";
bic: string | "";
mandate_reference: string | "";
mandate_date_signed: string | '';
bank: string | '';
account_holder_name: string | '';
iban: string | '';
bic: string | '';
mandate_reference: string | '';
}
interface Licence {
id: number | -1;
status: number | -1;
licence_number: string | "";
issued_date: string | "";
expiration_date: string | "";
country: string | "";
licence_number: string | '';
issued_date: string | '';
expiration_date: string | '';
country: string | '';
licence_categories: LicenceCategory[];
}
interface LicenceCategory {
id: number | -1;
category: string | "";
category: string | '';
}
interface User {
email: string | "";
first_name: string | "";
last_name: string | "";
phone: string | "";
notes: string | "";
address: string | "";
zip_code: string | "";
city: string | "";
email: string | '';
first_name: string | '';
last_name: string | '';
phone: string | '';
notes: string | '';
address: string | '';
zip_code: string | '';
city: string | '';
status: number | -1;
id: number | -1;
role_id: number | -1;
date_of_birth: string | "";
company: string | "";
profile_picture: string | "";
dateofbirth: string | '';
company: string | '';
profile_picture: string | '';
payment_status: number | -1;
membership: Membership;
bank_account: BankAccount;
licence: Licence;
notes: string | "";
notes: string | '';
}
declare global {

View File

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

View File

@@ -28,28 +28,6 @@
<div class="modal-background">
<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">
<slot />
</div>
@@ -64,7 +42,7 @@
top: 0;
right: 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 */
z-index: 9999;
display: flex;
@@ -149,7 +127,6 @@
.modal .container {
flex-direction: column;
left: 0;
width: 100%;
}
}
</style>

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
// @ts-nocheck
import { quintOut } from "svelte/easing";
import { crossfade } from "svelte/transition";
import { quintOut } from 'svelte/easing';
import { crossfade } from 'svelte/transition';
export const [send, receive] = crossfade({
duration: (d) => Math.sqrt(d * 200),
@@ -8,7 +8,7 @@ export const [send, receive] = crossfade({
// eslint-disable-next-line no-unused-vars
fallback(node, params) {
const style = getComputedStyle(node);
const transform = style.transform === "none" ? "" : style.transform;
const transform = style.transform === 'none' ? '' : style.transform;
return {
duration: 600,
@@ -16,9 +16,9 @@ export const [send, receive] = crossfade({
css: (t) => `
transform: ${transform} scale(${t});
opacity: ${t}
`,
`
};
},
}
});
/**
@@ -37,9 +37,7 @@ export const isValidEmail = (email) => {
* @param {string} password - The password to validate
*/
export const isValidPasswordStrong = (password) => {
const strongRegex = new RegExp(
"^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})"
);
const strongRegex = new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})');
return strongRegex.test(password.trim());
};
@@ -50,7 +48,7 @@ export const isValidPasswordStrong = (password) => {
*/
export const isValidPasswordMedium = (password) => {
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());
@@ -70,15 +68,15 @@ export function isEmpty(obj) {
}
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);
return date.toISOString();
}
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);
return date.toISOString().split("T")[0];
return date.toISOString().split('T')[0];
}
/**
@@ -86,8 +84,8 @@ export function fromRFC3339(dateString) {
* @param {App.Locals.User} user - The user object to format
*/
export function userDatesFromRFC3339(user) {
if (user.date_of_birth) {
user.date_of_birth = fromRFC3339(user.date_of_birth);
if (user.dateofbirth) {
user.dateofbirth = fromRFC3339(user.dateofbirth);
}
if (user.membership) {
if (user.membership.start_date) {
@@ -104,9 +102,7 @@ export function userDatesFromRFC3339(user) {
user.licence.expiration_date = fromRFC3339(user.licence.expiration_date);
}
if (user.bank_account && user.bank_account.mandate_date_signed) {
user.bank_account.mandate_date_signed = fromRFC3339(
user.bank_account.mandate_date_signed
);
user.bank_account.mandate_date_signed = fromRFC3339(user.bank_account.mandate_date_signed);
}
}
@@ -115,8 +111,8 @@ export function userDatesFromRFC3339(user) {
* @param {App.Locals.User} user - The user object to format
*/
export function userDatesToRFC3339(user) {
if (user.date_of_birth) {
user.date_of_birth = toRFC3339(user.date_of_birth);
if (user.dateofbirth) {
user.dateofbirth = toRFC3339(user.dateofbirth);
}
if (user.membership) {
if (user.membership.start_date) {
@@ -133,9 +129,7 @@ export function userDatesToRFC3339(user) {
user.licence.expiration_date = toRFC3339(user.licence.expiration_date);
}
if (user.bank_account && user.bank_account.mandate_date_signed) {
user.bank_account.mandate_date_signed = toRFC3339(
user.bank_account.mandate_date_signed
);
user.bank_account.mandate_date_signed = toRFC3339(user.bank_account.mandate_date_signed);
}
}
@@ -146,13 +140,13 @@ export function userDatesToRFC3339(user) {
*/
export function formatError(obj) {
const errors = [];
if (typeof obj === "object" && obj !== null) {
if (typeof obj === 'object' && obj !== null) {
if (Array.isArray(obj)) {
obj.forEach((error) => {
errors.push({
field: error.field,
key: error.key,
id: Math.random() * 1000,
id: Math.random() * 1000
});
});
} else {
@@ -160,15 +154,15 @@ export function formatError(obj) {
errors.push({
field: field,
key: obj[field].key,
id: Math.random() * 1000,
id: Math.random() * 1000
});
});
}
} else {
errors.push({
field: "general",
field: 'general',
key: obj,
id: 0,
id: 0
});
}
return errors;
@@ -184,20 +178,20 @@ export function refreshCookie(newToken, event) {
const match = newToken.match(/jwt=([^;]+)/);
if (match) {
if (event) {
event.cookies.set("jwt", match[1], {
path: "/",
event.cookies.set('jwt', match[1], {
path: '/',
httpOnly: true,
secure: process.env.NODE_ENV === "production", // Secure in production
sameSite: "lax",
maxAge: 5 * 24 * 60 * 60, // 5 days in seconds
secure: process.env.NODE_ENV === 'production', // Secure in production
sameSite: 'lax',
maxAge: 5 * 24 * 60 * 60 // 5 days in seconds
});
} else {
cookies.set("jwt", match[1], {
path: "/",
cookies.set('jwt', match[1], {
path: '/',
httpOnly: true,
secure: process.env.NODE_ENV === "production", // Secure in production
sameSite: "lax",
maxAge: 5 * 24 * 60 * 60, // 5 days in seconds
secure: process.env.NODE_ENV === 'production', // Secure in production
sameSite: 'lax',
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 {
formatError,
userDatesFromRFC3339,
userDatesToRFC3339,
} from "$lib/utils/helpers";
import { fail, redirect } from "@sveltejs/kit";
import { toRFC3339 } from "$lib/utils/helpers";
import { BASE_API_URI } from '$lib/utils/constants';
import { formatError, userDatesFromRFC3339 } from '$lib/utils/helpers';
import { fail, redirect } from '@sveltejs/kit';
import { formDataToObject, processFormData } from '$lib/utils/processing';
/**
* @typedef {Object} UpdateData
@@ -33,87 +29,24 @@ export const actions = {
updateUser: async ({ request, fetch, cookies, locals }) => {
let formData = await request.formData();
/** @type {App.Types['licenceCategory'][]} */
const licenceCategories = formData
.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']>} */
const userData = {
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("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 {UpdateData} */
const updateData = { user: userData };
// Remove undefined or null properties
const cleanUpdateData = JSON.parse(
JSON.stringify(updateData),
(key, value) => (value !== null && value !== "" ? value : undefined)
);
const rawData = formDataToObject(formData);
const processedData = processFormData(rawData);
console.dir(processedData.user.membership);
const isCreating = !processedData.user.id || processedData.user.id === 0;
console.log('Is updating: ', isCreating);
console.dir(formData);
console.dir(cleanUpdateData);
const apiURL = `${BASE_API_URI}/backend/users/update/`;
/** @type {RequestInit} */
const requestUpdateOptions = {
method: "PATCH",
credentials: "include",
method: 'PATCH',
credentials: 'include',
headers: {
"Content-Type": "application/json",
Cookie: `jwt=${cookies.get("jwt")}`,
'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get('jwt')}`
},
body: JSON.stringify(cleanUpdateData),
body: JSON.stringify(processedData)
};
const res = await fetch(apiURL, requestUpdateOptions);
@@ -128,6 +61,7 @@ export const actions = {
userDatesFromRFC3339(locals.user);
throw redirect(303, `/auth/about/${response.id}`);
},
/**
*
* @param request - The request object
@@ -141,11 +75,11 @@ export const actions = {
/** @type {RequestInit} */
const requestInitOptions = {
method: "POST",
method: 'POST',
headers: {
Cookie: `jwt=${cookies.get("jwt")}`,
Cookie: `jwt=${cookies.get('jwt')}`
},
body: formData,
body: formData
};
const res = await fetch(`${BASE_API_URI}/file/upload/`, requestInitOptions);
@@ -160,7 +94,7 @@ export const actions = {
return {
success: true,
profile_picture: response[""],
profile_picture: response['']
};
},
@@ -177,11 +111,11 @@ export const actions = {
/** @type {RequestInit} */
const requestInitOptions = {
method: "DELETE",
method: 'DELETE',
headers: {
Cookie: `jwt=${cookies.get("jwt")}`,
Cookie: `jwt=${cookies.get('jwt')}`
},
body: formData,
body: formData
};
const res = await fetch(`${BASE_API_URI}/file/delete/`, requestInitOptions);
@@ -194,7 +128,7 @@ export const actions = {
return {
success: true,
profile_picture: "",
profile_picture: ''
};
},
}
};

View File

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

View File

@@ -2,134 +2,10 @@
// - Implement a load function to fetch a list of all users.
// - Create actions for updating user information (similar to the about/[id] route).
import { BASE_API_URI } from "$lib/utils/constants";
import { formatError, userDatesFromRFC3339 } from "$lib/utils/helpers";
import { fail, redirect } from "@sveltejs/kit";
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;
}
import { BASE_API_URI } from '$lib/utils/constants';
import { formatError, userDatesFromRFC3339 } from '$lib/utils/helpers';
import { fail, redirect } from '@sveltejs/kit';
import { formDataToObject, processFormData } from '$lib/utils/processing';
/** @type {import('./$types').PageServerLoad} */
export async function load({ locals, params }) {
@@ -152,30 +28,23 @@ export const actions = {
updateUser: async ({ request, fetch, cookies, locals }) => {
let formData = await request.formData();
// Convert form data to nested object
const rawData = formDataToObject(formData);
const processedData = processFormData(rawData);
// Remove undefined or null properties
const cleanUpdateData = JSON.parse(
JSON.stringify(processedData),
(key, value) => (value !== null && value !== "" ? value : undefined)
);
console.dir(processedData.user.membership);
const isCreating = !processedData.user.id || processedData.user.id === 0;
console.log("Is creating: ", isCreating);
console.log('Is creating: ', isCreating);
const apiURL = `${BASE_API_URI}/backend/users/update`;
/** @type {RequestInit} */
const requestOptions = {
method: isCreating ? "POST" : "PATCH",
credentials: "include",
method: isCreating ? 'POST' : 'PATCH',
credentials: 'include',
headers: {
"Content-Type": "application/json",
Cookie: `jwt=${cookies.get("jwt")}`,
'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get('jwt')}`
},
body: JSON.stringify(cleanUpdateData),
body: JSON.stringify(processedData)
};
const res = await fetch(apiURL, requestOptions);
@@ -187,9 +56,9 @@ export const actions = {
}
const response = await res.json();
console.log("Server success response:", response);
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')}
>
<i class="fas fa-users"></i>
<span class="nav-badge">{users.length}</span>
{$t('users')}
<span class="nav-badge">{users.length}</span>
</button>
</li>
<li>
@@ -68,8 +68,8 @@
on:click={() => setActiveSection('subscriptions')}
>
<i class="fas fa-clipboard-list"></i>
<span class="nav-badge">{subscriptions.length}</span>
{$t('subscriptions')}
<span class="nav-badge">{subscriptions.length}</span>
</button>
</li>
<li>
@@ -238,7 +238,7 @@
width: 100%;
height: 100%;
padding: 0 1rem;
color: white;
color: var(--text);
}
.layout {
@@ -252,8 +252,8 @@
.sidebar {
width: 250px;
min-height: 600px;
background: #2f2f2f;
border-right: 1px solid #494848;
background: var(--surface0);
border-right: 1px solid var(--surface1);
}
.nav-list {
@@ -272,7 +272,7 @@
background: none;
text-align: left;
cursor: pointer;
color: #9b9b9b;
color: var(--subtext0);
text-transform: uppercase;
font-weight: 500;
letter-spacing: 1px;
@@ -280,12 +280,14 @@
}
.nav-link:hover {
background: #fdfff5;
background: var(--surface1);
color: var(--text);
}
.nav-link.active {
background: #494848;
color: white;
background: var(--surface2);
color: var(--lavender);
border-left: 3px solid var(--mauve);
}
.main-content {
@@ -295,20 +297,29 @@
.accordion-item {
border: none;
background: #2f2f2f;
background: var(--surface0);
margin-bottom: 0.5rem;
border-radius: 8px;
overflow: hidden;
}
.accordion-header {
padding: 1rem;
cursor: pointer;
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 {
padding: 1rem;
background: #494848;
background: var(--surface0);
border-top: 1px solid var(--surface1);
}
.table {
@@ -325,11 +336,11 @@
}
.table th {
color: #9b9b9b;
color: var(--subtext1);
}
.table td {
color: white;
color: var(--text);
}
@media (max-width: 680px) {
@@ -358,5 +369,42 @@
.section-header h2 {
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>

View File

@@ -13,7 +13,7 @@ type User struct {
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:"dateofbirth" binding:"required,safe_content"`
Company string `json:"company" binding:"omitempty,omitnil,safe_content"`
Phone string `json:"phone" binding:"omitempty,omitnil,safe_content"`
Notes string `json:"notes" binding:"safe_content"`
@@ -75,7 +75,7 @@ func (u *User) Safe() map[string]interface{} {
"id": u.ID,
"role_id": u.RoleID,
"company": u.Company,
"date_of_birth": u.DateOfBirth,
"dateofbirth": u.DateOfBirth,
"membership": map[string]interface{}{
"id": u.Membership.ID,
"start_date": u.Membership.StartDate,

View File

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