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 { 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;
@@ -14,57 +14,57 @@ interface Subscription {
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 {

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: user_not_found_or_wrong_password: 'Existiert nicht oder falsches Passwort',
"Existiert nicht oder falsches Passwort", email_already_registered: 'Ein Mitglied wurde schon mit dieser Emailadresse erstellt.',
email_already_registered: alphanumunicode: 'beinhaltet nicht erlaubte Zeichen',
"Ein Mitglied wurde schon mit dieser Emailadresse erstellt.", safe_content: 'I see what you did there! Do not cross this line!',
alphanumunicode: "beinhaltet nicht erlaubte Zeichen", iban: 'Ungültig. Format: DE07123412341234123412',
safe_content: "I see what you did there! Do not cross this line!", bic: 'Ungültig. Format: BELADEBEXXX',
iban: "Ungültig. Format: DE07123412341234123412", email: 'Format ungültig',
bic: "Ungültig. Format: BELADEBEXXX", number: 'Ist keine Nummer',
email: "Format ungültig", euDriversLicence: 'Ist kein europäischer Führerschein',
number: "Ist keine Nummer", lte: 'Ist zu groß/neu',
euDriversLicence: "Ist kein europäischer Führerschein", gt: 'Ist zu klein/alt',
lte: "Ist zu groß/neu", required: 'Feld wird benötigt',
gt: "Ist zu klein/alt", image: 'Dies ist kein Bild',
required: "Feld wird benötigt", alphanum: 'beinhaltet ungültige Zeichen',
image: "Dies ist kein Bild", alphaunicode: 'darf nur aus Buchstaben bestehen'
alphanum: "beinhaltet ungültige Zeichen", }
alphaunicode: "darf nur aus Buchstaben bestehen",
},
}, },
licenceCategory: { licenceCategory: {
AM: "Mopeds und leichte vierrädrige Kraftfahrzeuge (50ccm, max 45km/h)", AM: 'Mopeds und leichte vierrädrige Kraftfahrzeuge (50ccm, max 45km/h)',
A1: "Leichte Motorräder (125ccm)", A1: 'Leichte Motorräder (125ccm)',
A2: "Motorräder mit mittlerer Leistung (max 35kW)", A2: 'Motorräder mit mittlerer Leistung (max 35kW)',
A: "Motorräder", A: 'Motorräder',
B: "Kraftfahrzeuge ≤ 3500 kg, ≤ 8 Sitzplätze", B: 'Kraftfahrzeuge ≤ 3500 kg, ≤ 8 Sitzplätze',
C1: "Mittelschwere Fahrzeuge -7500 kg", C1: 'Mittelschwere Fahrzeuge -7500 kg',
C: "Schwere Nutzfahrzeuge > 3500 kg", C: 'Schwere Nutzfahrzeuge > 3500 kg',
D1: "Kleinbusse 9-16 Sitzplätze", D1: 'Kleinbusse 9-16 Sitzplätze',
D: "Busse > 8 Sitzplätze", D: 'Busse > 8 Sitzplätze',
BE: "Fahrzeugklasse B mit Anhänger", BE: 'Fahrzeugklasse B mit Anhänger',
C1E: "Fahrzeugklasse C1 mit Anhänger", C1E: 'Fahrzeugklasse C1 mit Anhänger',
CE: "Fahrzeugklasse C mit Anhänger", CE: 'Fahrzeugklasse C mit Anhänger',
D1E: "Fahrzeugklasse D1 mit Anhänger", D1E: 'Fahrzeugklasse D1 mit Anhänger',
DE: "Fahrzeugklasse D mit Anhänger", DE: 'Fahrzeugklasse D mit Anhänger',
L: "Land-, Forstwirtschaftsfahrzeuge, Stapler max 40km/h", L: 'Land-, Forstwirtschaftsfahrzeuge, Stapler max 40km/h',
T: "Land-, Forstwirtschaftsfahrzeuge, Stapler max 60km/h", T: 'Land-, Forstwirtschaftsfahrzeuge, Stapler max 60km/h'
}, },
users: "Mitglieder", users: 'Mitglieder',
user: { user: {
login: "Nutzer Anmeldung", login: 'Nutzer Anmeldung',
edit: "Nutzer bearbeiten", edit: 'Nutzer bearbeiten',
user: "Nutzer", user: 'Nutzer',
management: "Mitgliederverwaltung", management: 'Mitgliederverwaltung',
id: "Mitgliedsnr", id: 'Mitgliedsnr',
name: "Name", name: 'Name',
email: "Email", email: 'Email',
status: "Status", status: 'Status',
role: "Nutzerrolle", role: 'Nutzerrolle'
}, },
cancel: "Abbrechen", cancel: 'Abbrechen',
confirm: "Bestätigen", confirm: 'Bestätigen',
actions: "Aktionen", actions: 'Aktionen',
edit: "Bearbeiten", edit: 'Bearbeiten',
delete: "Löschen", delete: 'Löschen',
mandate_date_signed: "Mandatserteilungsdatum", mandate_date_signed: 'Mandatserteilungsdatum',
licence_categories: "Führerscheinklassen", licence_categories: 'Führerscheinklassen',
subscription_model: "Mitgliedschatfsmodell", subscription_model: 'Mitgliedschatfsmodell',
licence: "Führerschein", licence: 'Führerschein',
licence_number: "Führerscheinnummer", licence_number: 'Führerscheinnummer',
issued_date: "Ausgabedatum", issued_date: 'Ausgabedatum',
expiration_date: "Ablaufdatum", expiration_date: 'Ablaufdatum',
country: "Land", country: 'Land',
monthly_fee: "Monatliche Gebühr", monthly_fee: 'Monatliche Gebühr',
hourly_rate: "Stundensatz", hourly_rate: 'Stundensatz',
details: "Details", details: 'Details',
conditions: "Bedingungen", conditions: 'Bedingungen',
unknown: "Unbekannt", unknown: 'Unbekannt',
notes: "Notizen", notes: 'Notizen',
address: "Straße & Hausnummer", address: 'Straße & Hausnummer',
city: "Wohnort", city: 'Wohnort',
zip_code: "PLZ", zip_code: 'PLZ',
forgot_password: "Passwort vergessen?", forgot_password: 'Passwort vergessen?',
password: "Passwort", password: 'Passwort',
password_repeat: "Passwort wiederholen", password_repeat: 'Passwort wiederholen',
email: "Email", email: 'Email',
company: "Firma", company: 'Firma',
login: "Anmeldung", login: 'Anmeldung',
profile: "Profil", profile: 'Profil',
membership: "Mitgliedschaft", membership: 'Mitgliedschaft',
bankaccount: "Kontodaten", bankaccount: 'Kontodaten',
first_name: "Vorname", first_name: 'Vorname',
last_name: "Nachname", last_name: 'Nachname',
name: "Name", name: 'Name',
phone: "Telefonnummer", phone: 'Telefonnummer',
date_of_birth: "Geburtstag", dateofbirth: 'Geburtstag',
status: "Status", status: 'Status',
start: "Beginn", start: 'Beginn',
end: "Ende", end: 'Ende',
parent_member_id: "Hauptmitgliedsnr.", parent_member_id: 'Hauptmitgliedsnr.',
bank_account_holder: "Kontoinhaber", bank_account_holder: 'Kontoinhaber',
bank_name: "Bank", bank_name: 'Bank',
iban: "IBAN", iban: 'IBAN',
bic: "BIC", bic: 'BIC',
mandate_reference: "SEPA Mandat", mandate_reference: 'SEPA Mandat',
subscriptions: "Tarifmodelle", subscriptions: 'Tarifmodelle',
payments: "Zahlungen", payments: 'Zahlungen',
add_new: "Neu", add_new: 'Neu',
included_hours_per_year: "Inkludierte Stunden pro Jahr", included_hours_per_year: 'Inkludierte Stunden pro Jahr',
included_hours_per_month: "Inkludierte Stunden pro Monat", 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,6 +1,6 @@
// @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),
@@ -8,7 +8,7 @@ export const [send, receive] = crossfade({
// 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,
@@ -16,9 +16,9 @@ export const [send, receive] = crossfade({
css: (t) => ` css: (t) => `
transform: ${transform} scale(${t}); transform: ${transform} scale(${t});
opacity: ${t} opacity: ${t}
`, `
}; };
}, }
}); });
/** /**
@@ -37,9 +37,7 @@ 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());
}; };
@@ -50,7 +48,7 @@ export const isValidPasswordStrong = (password) => {
*/ */
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());
@@ -70,15 +68,15 @@ export function isEmpty(obj) {
} }
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,8 +84,8 @@ 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) {
@@ -104,9 +102,7 @@ export function userDatesFromRFC3339(user) {
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,8 +111,8 @@ 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) {
@@ -133,9 +129,7 @@ export function userDatesToRFC3339(user) {
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
);
} }
} }
@@ -146,13 +140,13 @@ export function userDatesToRFC3339(user) {
*/ */
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 {
@@ -160,15 +154,15 @@ export function formatError(obj) {
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;
@@ -184,20 +178,20 @@ export function refreshCookie(newToken, event) {
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
@@ -33,87 +29,24 @@ export const actions = {
updateUser: async ({ request, fetch, cookies, locals }) => { updateUser: async ({ request, fetch, cookies, locals }) => {
let formData = await request.formData(); let formData = await request.formData();
/** @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']>} */
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)
);
console.dir(processedData.user.membership);
const isCreating = !processedData.user.id || processedData.user.id === 0;
console.log('Is updating: ', isCreating);
console.dir(formData); console.dir(formData);
console.dir(cleanUpdateData);
const apiURL = `${BASE_API_URI}/backend/users/update/`; const apiURL = `${BASE_API_URI}/backend/users/update/`;
/** @type {RequestInit} */ /** @type {RequestInit} */
const requestUpdateOptions = { const requestUpdateOptions = {
method: "PATCH", method: 'PATCH',
credentials: "include", credentials: 'include',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get("jwt")}`, Cookie: `jwt=${cookies.get('jwt')}`
}, },
body: JSON.stringify(cleanUpdateData), body: JSON.stringify(processedData)
}; };
const res = await fetch(apiURL, requestUpdateOptions); const res = await fetch(apiURL, requestUpdateOptions);
@@ -128,6 +61,7 @@ export const actions = {
userDatesFromRFC3339(locals.user); userDatesFromRFC3339(locals.user);
throw redirect(303, `/auth/about/${response.id}`); throw redirect(303, `/auth/about/${response.id}`);
}, },
/** /**
* *
* @param request - The request object * @param request - The request object
@@ -141,11 +75,11 @@ export const actions = {
/** @type {RequestInit} */ /** @type {RequestInit} */
const requestInitOptions = { const requestInitOptions = {
method: "POST", method: 'POST',
headers: { 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); const res = await fetch(`${BASE_API_URI}/file/upload/`, requestInitOptions);
@@ -160,7 +94,7 @@ export const actions = {
return { return {
success: true, success: true,
profile_picture: response[""], profile_picture: response['']
}; };
}, },
@@ -177,11 +111,11 @@ export const actions = {
/** @type {RequestInit} */ /** @type {RequestInit} */
const requestInitOptions = { const requestInitOptions = {
method: "DELETE", method: 'DELETE',
headers: { 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); const res = await fetch(`${BASE_API_URI}/file/delete/`, requestInitOptions);
@@ -194,7 +128,7 @@ export const actions = {
return { return {
success: true, success: true,
profile_picture: "", 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,134 +2,10 @@
// - 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 }) {
@@ -152,30 +28,23 @@ export const actions = {
updateUser: async ({ request, fetch, cookies, locals }) => { updateUser: async ({ request, fetch, cookies, locals }) => {
let formData = await request.formData(); let formData = await request.formData();
// Convert form data to nested object
const rawData = formDataToObject(formData); const rawData = formDataToObject(formData);
const processedData = processFormData(rawData); 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); console.dir(processedData.user.membership);
const isCreating = !processedData.user.id || processedData.user.id === 0; 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`; const apiURL = `${BASE_API_URI}/backend/users/update`;
/** @type {RequestInit} */ /** @type {RequestInit} */
const requestOptions = { const requestOptions = {
method: isCreating ? "POST" : "PATCH", method: isCreating ? 'POST' : 'PATCH',
credentials: "include", credentials: 'include',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get("jwt")}`, Cookie: `jwt=${cookies.get('jwt')}`
}, },
body: JSON.stringify(cleanUpdateData), body: JSON.stringify(processedData)
}; };
const res = await fetch(apiURL, requestOptions); const res = await fetch(apiURL, requestOptions);
@@ -187,9 +56,9 @@ export const actions = {
} }
const response = await res.json(); const response = await res.json();
console.log("Server success response:", response); console.log('Server success response:', response);
locals.user = response; locals.user = response;
userDatesFromRFC3339(locals.user); userDatesFromRFC3339(locals.user);
throw redirect(303, `/auth/about/${response.id}`); throw redirect(303, `/auth/about/${response.id}`);
}, }
}; };

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"`
@@ -75,7 +75,7 @@ func (u *User) Safe() map[string]interface{} {
"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)