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

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
@@ -14,187 +10,125 @@ import { toRFC3339 } from "$lib/utils/helpers";
/** @type {import('./$types').PageServerLoad} */
export async function load({ locals, params }) {
// redirect user if not logged in
if (!locals.user) {
throw redirect(302, `/auth/login?next=/auth/about/${params.id}`);
}
// redirect user if not logged in
if (!locals.user) {
throw redirect(302, `/auth/login?next=/auth/about/${params.id}`);
}
}
/** @type {import('./$types').Actions} */
export const actions = {
/**
*
* @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
*/
updateUser: async ({ request, fetch, cookies, locals }) => {
let formData = await request.formData();
/**
*
* @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
*/
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;
}
});
const rawData = formDataToObject(formData);
const processedData = processFormData(rawData);
/** @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,
},
};
console.dir(processedData.user.membership);
const isCreating = !processedData.user.id || processedData.user.id === 0;
console.log('Is updating: ', isCreating);
console.dir(formData);
const apiURL = `${BASE_API_URI}/backend/users/update/`;
// 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} */
const updateData = { user: userData };
// Remove undefined or null properties
const cleanUpdateData = JSON.parse(
JSON.stringify(updateData),
(key, value) => (value !== null && value !== "" ? value : undefined)
);
if (!res.ok) {
const response = await res.json();
const errors = formatError(response.errors);
return fail(400, { errors: errors });
}
console.dir(formData);
console.dir(cleanUpdateData);
const apiURL = `${BASE_API_URI}/backend/users/update/`;
const response = await res.json();
locals.user = response;
userDatesFromRFC3339(locals.user);
throw redirect(303, `/auth/about/${response.id}`);
},
/** @type {RequestInit} */
const requestUpdateOptions = {
method: "PATCH",
credentials: "include",
headers: {
"Content-Type": "application/json",
Cookie: `jwt=${cookies.get("jwt")}`,
},
body: JSON.stringify(cleanUpdateData),
};
const res = await fetch(apiURL, requestUpdateOptions);
/**
*
* @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();
if (!res.ok) {
const response = await res.json();
const errors = formatError(response.errors);
return fail(400, { errors: errors });
}
/** @type {RequestInit} */
const requestInitOptions = {
method: 'POST',
headers: {
Cookie: `jwt=${cookies.get('jwt')}`
},
body: formData
};
const response = await res.json();
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();
const res = await fetch(`${BASE_API_URI}/file/upload/`, requestInitOptions);
/** @type {RequestInit} */
const requestInitOptions = {
method: "POST",
headers: {
Cookie: `jwt=${cookies.get("jwt")}`,
},
body: formData,
};
if (!res.ok) {
const response = await res.json();
const errors = formatError(response.errors);
return fail(400, { errors: errors });
}
const res = await fetch(`${BASE_API_URI}/file/upload/`, requestInitOptions);
const response = await res.json();
if (!res.ok) {
const response = await res.json();
const errors = formatError(response.errors);
return fail(400, { errors: errors });
}
return {
success: true,
profile_picture: response['']
};
},
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 {
success: true,
profile_picture: response[""],
};
},
/** @type {RequestInit} */
const requestInitOptions = {
method: 'DELETE',
headers: {
Cookie: `jwt=${cookies.get('jwt')}`
},
body: formData
};
/**
*
* @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();
const res = await fetch(`${BASE_API_URI}/file/delete/`, requestInitOptions);
/** @type {RequestInit} */
const requestInitOptions = {
method: "DELETE",
headers: {
Cookie: `jwt=${cookies.get("jwt")}`,
},
body: formData,
};
if (!res.ok) {
const response = await res.json();
const errors = formatError(response.errors);
return fail(400, { errors: errors });
}
const res = await fetch(`${BASE_API_URI}/file/delete/`, requestInitOptions);
if (!res.ok) {
const response = await res.json();
const errors = formatError(response.errors);
return fail(400, { errors: errors });
}
return {
success: true,
profile_picture: "",
};
},
return {
success: true,
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,194 +2,63 @@
// - 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 }) {
// redirect user if not logged in
if (!locals.user) {
throw redirect(302, `/auth/login?next=/auth/users`);
}
// redirect user if not logged in
if (!locals.user) {
throw redirect(302, `/auth/login?next=/auth/users`);
}
}
/** @type {import('./$types').Actions} */
export const actions = {
/**
*
* @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
*/
updateUser: async ({ request, fetch, cookies, locals }) => {
let formData = await request.formData();
/**
*
* @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
*/
updateUser: async ({ request, fetch, cookies, locals }) => {
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
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);
const apiURL = `${BASE_API_URI}/backend/users/update`;
/** @type {RequestInit} */
const requestOptions = {
method: isCreating ? 'POST' : 'PATCH',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get('jwt')}`
},
body: JSON.stringify(processedData)
};
/** @type {RequestInit} */
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);
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 errors = formatError(response.errors);
return fail(400, { errors: errors });
}
const response = await res.json();
console.log("Server success response:", response);
locals.user = response;
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')}
>
<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>