Compare commits

...

11 Commits

Author SHA1 Message Date
Alex
451e42a1fc add: locales for server errors 2024-10-09 18:01:43 +02:00
Alex
4de5a54cac fix: date formatting 2024-10-08 07:39:21 +02:00
Alex
1367ba4fa1 grouped and sorted licence categories display 2024-10-08 07:39:03 +02:00
Alex
9023979b43 add mandate_date_signed formatting 2024-10-08 07:37:45 +02:00
Alex
8071bba435 add: mandate_date_signed locale 2024-10-08 07:36:32 +02:00
Alex
a947075e80 mod: licence category locale 2024-10-08 07:36:16 +02:00
Alex
34a6ecd039 add default RFC3339 datestring 2024-10-08 07:35:42 +02:00
Alex
4316eaad79 removed commented Avatar stuff 2024-10-08 07:35:24 +02:00
Alex
a0e1b32b19 add: mandate_date_signed display 2024-10-08 07:33:28 +02:00
Alex
68b78cc443 typo 2024-10-08 07:32:59 +02:00
Alex
bdcc98a2ac add: readonly option for inputfields 2024-10-08 07:32:29 +02:00
7 changed files with 237 additions and 166 deletions

View File

@@ -47,7 +47,7 @@ export async function handle({ event, resolve }) {
event.locals.subscriptions = data.subscriptions; event.locals.subscriptions = data.subscriptions;
event.locals.user = data.user; event.locals.user = data.user;
event.locals.licence_categories = data.licence_categories; event.locals.licence_categories = data.licence_categories;
console.dir(event.locals.licence_categories); console.dir(event.locals.user);
if (event.locals.user.date_of_birth) { if (event.locals.user.date_of_birth) {
event.locals.user.date_of_birth = event.locals.user.date_of_birth =
event.locals.user.date_of_birth.split("T")[0]; event.locals.user.date_of_birth.split("T")[0];
@@ -62,6 +62,14 @@ export async function handle({ event, resolve }) {
event.locals.user.membership.end_date.split("T")[0]; event.locals.user.membership.end_date.split("T")[0];
} }
} }
if (event.locals.user.drivers_licence?.issued_date) {
event.locals.user.drivers_licence.issued_date =
event.locals.user.drivers_licence.issued_date.split("T")[0];
}
if (event.locals.user.drivers_licence?.expiration_date) {
event.locals.user.drivers_licence.expiration_date =
event.locals.user.drivers_licence.expiration_date.split("T")[0];
}
if ( if (
event.locals.user.bank_account && event.locals.user.bank_account &&
event.locals.user.bank_account.mandate_date_signed event.locals.user.bank_account.mandate_date_signed

View File

@@ -35,6 +35,9 @@
/** @type {boolean} */ /** @type {boolean} */
export let checked = false; export let checked = false;
/** @type {boolean} */
export let readonly = false;
/** /**
* @param {Event} event - The input event * @param {Event} event - The input event
*/ */
@@ -119,12 +122,13 @@
<div class="input-box {type === 'checkbox' ? 'checkbox-container' : ''}"> <div class="input-box {type === 'checkbox' ? 'checkbox-container' : ''}">
{#if type === "checkbox"} {#if type === "checkbox"}
<label class="checkbox-label"> <label class="form-control {readonly ? 'form-control--disabled' : ''}">
<input <input
type="checkbox" type="checkbox"
{name} {name}
{value} {value}
{checked} {checked}
{readonly}
on:change={() => (checked = !checked)} on:change={() => (checked = !checked)}
/> />
<span class="checkbox-text"> {label} </span> <span class="checkbox-text"> {label} </span>
@@ -154,8 +158,9 @@
{placeholder} {placeholder}
{required} {required}
{value} {value}
{readonly}
{rows} {rows}
class="input textarea" class="input textarea {readonly ? 'readonly' : ''}"
style="height:{rows * 1.5}em;" style="height:{rows * 1.5}em;"
/> />
{:else if type != "checkbox"} {:else if type != "checkbox"}
@@ -163,37 +168,96 @@
{name} {name}
{type} {type}
{placeholder} {placeholder}
{readonly}
{value} {value}
{required} {required}
on:input={handleInput} on:input={handleInput}
on:blur={handleInput} on:blur={handleInput}
class="input" class="input {readonly ? 'readonly' : ''}"
/> />
{/if} {/if}
</div> </div>
</div> </div>
<style> <style>
:root {
--form-control-color: #6bff55;
--form-control-disabled: #959495;
}
.form-control {
font-size: 1rem;
font-weight: bold;
line-height: 1.1;
display: grid;
grid-template-columns: 1.5em auto;
gap: 0.75em;
align-items: center;
opacity: 0.8;
}
.form-control--disabled {
color: var(--form-control-disabled);
cursor: not-allowed;
}
input[type="checkbox"] {
-webkit-appearance: none;
appearance: none;
background-color: var(--form-background);
margin: 0;
font: inherit;
color: currentColor;
width: 1.75em;
height: 1.75em;
border: 0.15em solid currentColor;
border-radius: 0.5em;
transform: translateY(-0.075em);
display: grid;
place-content: center;
transition: transform 0.2s ease-in-out;
}
input[type="checkbox"]::before {
content: "";
width: 1em;
height: 1em;
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
transform: scale(0);
transform-origin: bottom left;
transition: 120ms transform ease-in-out;
box-shadow: inset 1em 1em var(--form-control-color);
background-color: CanvasText;
}
input[type="checkbox"]:checked::before {
transform: scale(1);
}
input[type="checkbox"]:hover {
outline: max(2px, 0.15em) solid currentColor;
outline-offset: max(2px, 0.15em);
transform: scale(1.3);
}
input[type="checkbox"]:disabled {
--form-control-color: var(--form-control-disabled);
color: var(--form-control-disabled);
cursor: not-allowed;
}
.readonly {
background-color: #ececec;
cursor: not-allowed;
opacity: 0.7;
color: #4f4f4f;
}
.checkbox-container { .checkbox-container {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
background-color: transparent; background-color: transparent;
} }
.checkbox-label {
display: inline-flex;
align-items: center;
cursor: pointer;
user-select: none;
justify-content: center;
}
.checkbox-label input[type="checkbox"] {
margin-right: 5px;
flex-shrink: 0;
width: 40px;
}
.checkbox-text { .checkbox-text {
font-size: 16px; font-size: 16px;
} }

View File

@@ -46,10 +46,44 @@ export default {
email: "Ungültige Emailadresse", email: "Ungültige Emailadresse",
drivers_licence: "Nummer zu kurz(11 Zeichen)", drivers_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_update: "Sie sind nicht befugt dieses Update durchzuführen",
internal_server_error:
"Verdammt, fehler auf unserer Seite, probieren Sie es nochmal, danach rufen Sie nach Hilfe",
},
validation: {
no_user_id_provided: "Nutzer ID fehlt im Header",
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: { 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",
@@ -66,6 +100,7 @@ export default {
}, },
cancel: "Abbrechen", cancel: "Abbrechen",
confirm: "Bestätigen", confirm: "Bestätigen",
mandate_date_signed: "Mandatserteilungsdatum",
licence_categories: "Führerscheinklassen", licence_categories: "Führerscheinklassen",
subscription_model: "Mitgliedschatfsmodell", subscription_model: "Mitgliedschatfsmodell",
licence: "Führerschein", licence: "Führerschein",
@@ -88,7 +123,7 @@ export default {
password_repeat: "Passwort wiederholen", password_repeat: "Passwort wiederholen",
email: "Email", email: "Email",
company: "Firma", company: "Firma",
login: "Anmelden", login: "Anmeldung",
user: "Nutzer", user: "Nutzer",
user_login: "Nutzer Anmeldung", user_login: "Nutzer Anmeldung",
user_edit: "Nutzer bearbeiten", user_edit: "Nutzer bearbeiten",

View File

@@ -112,7 +112,7 @@ export function formatError(obj) {
} }
export function toRFC3339(dateString) { export function toRFC3339(dateString) {
if (!dateString) return ""; 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();
} }

View File

@@ -24,6 +24,18 @@ export const actions = {
updateUser: async ({ request, fetch, cookies, locals }) => { updateUser: async ({ request, fetch, cookies, locals }) => {
let formData = await request.formData(); let formData = await request.formData();
const licenceCategories = formData
.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;
}
})
.filter(Boolean);
/** @type {Partial<App.Locals['user']>} */ /** @type {Partial<App.Locals['user']>} */
const updateData = { const updateData = {
id: Number(formData.get("id")), id: Number(formData.get("id")),
@@ -51,7 +63,9 @@ export const actions = {
}, },
bank_account: { bank_account: {
id: Number(formData.get("bank_account_id")), id: Number(formData.get("bank_account_id")),
mandate_date_signed: String(formData.get("mandate_date_signed")), mandate_date_signed: toRFC3339(
String(formData.get("mandate_date_signed"))
),
bank: String(formData.get("bank")), bank: String(formData.get("bank")),
account_holder_name: String(formData.get("account_holder_name")), account_holder_name: String(formData.get("account_holder_name")),
iban: String(formData.get("iban")), iban: String(formData.get("iban")),
@@ -65,19 +79,15 @@ export const actions = {
issued_date: toRFC3339(formData.get("issued_date")), issued_date: toRFC3339(formData.get("issued_date")),
expiration_date: toRFC3339(formData.get("expiration_date")), expiration_date: toRFC3339(formData.get("expiration_date")),
country: String(formData.get("country")), country: String(formData.get("country")),
licence_categories: formData licence_categories: licenceCategories,
.getAll("licence_categories[]")
.map((category) => ({
id: -1, // Use -1 as a placeholder for new categories
category: String(category),
})),
}, },
}; };
// Remove undefined or null properties // Remove undefined or null properties
const cleanUpdateData = Object.fromEntries( const cleanUpdateData = JSON.parse(
Object.entries(updateData).filter(([_, v]) => v != null) JSON.stringify(updateData),
(key, value) => (value !== null && value !== "" ? value : undefined)
); );
console.dir(formData);
console.dir(cleanUpdateData); console.dir(cleanUpdateData);
const apiURL = `${BASE_API_URI}/backend/users/update/`; const apiURL = `${BASE_API_URI}/backend/users/update/`;
const res = await fetch(apiURL, { const res = await fetch(apiURL, {
@@ -111,7 +121,18 @@ export const actions = {
locals.user.membership.end_date = locals.user.membership.end_date =
locals.user.membership.end_date.split("T")[0]; locals.user.membership.end_date.split("T")[0];
} }
if (locals.user.bank_account?.mandate_date_signed) {
locals.user.bank_account.mandate_date_signed =
locals.user.bank_account.mandate_date_signed.split("T")[0];
}
if (locals.user.drivers_licence?.issued_date) {
locals.user.drivers_licence.issued_date =
locals.user.drivers_licence.issued_date.split("T")[0];
}
if (locals.user.drivers_licence?.expiration_date) {
locals.user.drivers_licence.expiration_date =
locals.user.drivers_licence.expiration_date.split("T")[0];
}
throw redirect(303, `/auth/about/${response.id}`); throw redirect(303, `/auth/about/${response.id}`);
}, },
/** /**

View File

@@ -20,9 +20,36 @@
/** @type {App.Locals['user']}*/ /** @type {App.Locals['user']}*/
$: user = $page.data.user; $: user = $page.data.user;
/**
* creates groups of categories depending on the first letter
* @param {App.Locals['licence_categories']} categories - the categories to sort and group
* @returns {Object.<string, App.Locals['licence_categories']>} Grouped categories
*/
function groupCategories(categories) {
return Object.entries(categories)
.sort((a, b) => a[1].category.localeCompare(b[1].category))
.reduce(
(
/** @type {Object.<string, App.Locals['licence_categories']>} */ acc,
[_, category]
) => {
const firstLetter = category.category[0];
if (!acc[firstLetter]) {
acc[firstLetter] = [];
}
acc[firstLetter].push(category);
return acc;
},
{}
);
}
/** @type {App.Locals['licence_categories']} */ /** @type {App.Locals['licence_categories']} */
$: licence_categories = $page.data.licence_categories; $: licence_categories = $page.data.licence_categories;
/** @type {Object.<string, App.Locals['licence_categories']>} */
$: groupedCategories = groupCategories(licence_categories);
// /** @typedef {{name: string, src: string}} Avatar */ // /** @typedef {{name: string, src: string}} Avatar */
// const avatarFiles = import.meta.glob("$lib/img/Avatar-*.jpeg", { // const avatarFiles = import.meta.glob("$lib/img/Avatar-*.jpeg", {
// eager: true, // eager: true,
@@ -82,27 +109,6 @@
onMount(() => { onMount(() => {
console.dir(user); console.dir(user);
console.dir(licence_categories);
// avatars = Object.entries(avatarFiles).map(([path, module]) => {
// if (typeof path !== "string") {
// throw new Error("Unexpected non-string path");
// }
// if (
// typeof module !== "object" ||
// module === null ||
// !("default" in module)
// ) {
// throw new Error("Unexpected module format");
// }
// const src = module.default;
// if (typeof src !== "string") {
// throw new Error("Unexpected default export type");
// }
// return {
// name: path.split("/").pop()?.split(".")[0] ?? "Unknown",
// src: src,
// };
// });
}); });
/** /**
@@ -126,28 +132,9 @@
}; };
/** @type {import('./$types').SubmitFunction} */ /** @type {import('./$types').SubmitFunction} */
// const handleUpload = async () => {
// isUploading = true;
// return async ({ result }) => {
// isUploading = false;
// /** @type {any} */
// const res = result;
// if (result.type === "success" || result.type === "redirect") {
// user.profile_picture = res.data.profile_picture;
// }
// await applyAction(result);
// };
// };
</script> </script>
<div class="hero-container"> <div class="hero-container">
<!-- <div class="hero-logo">
<img
src={user.profile_picture ? user.profile_picture : Avatar}
alt={`${user.first_name} ${user.last_name}`}
width="200"
/>
</div> -->
<div class="user-info"> <div class="user-info">
{#if user.status} {#if user.status}
<h3 class="hero-subtitle subtitle info-row"> <h3 class="hero-subtitle subtitle info-row">
@@ -210,71 +197,6 @@
{#if showModal} {#if showModal}
<Modal on:close={close}> <Modal on:close={close}>
<!-- <div class="avatar-container">
<form
class="avatar-form"
action="?/uploadImage"
method="post"
enctype="multipart/form-data"
use:enhance={handleUpload}
>
<div class="current-avatar">
<ImageInput
avatar={user.profile_picture}
fieldName="profile_picture"
title="Nutzerbild auswählen"
/>
</div>
<div class="avatar-buttons">
{#if !user.profile_picture}
{#if isUploading}
<SmallLoader width={30} message={"Uploading..."} />
{:else}
<button class="button-dark" type="submit">Bild hochladen</button>
{/if}
{:else}
<input
type="hidden"
hidden
name="profile_picture_url"
bind:value={user.profile_picture}
required
/>
{#if isUploading}
<SmallLoader width={30} message={"Lösche..."} />
{:else}
<button
class="button-dark"
formaction="?/deleteImage"
type="submit"
>
Bild löschen
</button>
{/if}
{/if}
</div>
</form>
<div class="avatar-buttons">
<button class="button-dark" on:click={toggleAvatars}>
{showAvatars ? "Abbrechen" : "Profilbild auswählen"}
</button>
</div>
{#if showAvatars}
<div class="avatar-selection">
{#each avatars as avatar}
<button
class="avatar-option"
on:click={() => {
user.profile_picture = avatar.src;
showAvatars = false;
}}
>
<img src={avatar.src} alt={avatar.name} width="80" />
</button>
{/each}
</div>
{/if}
</div> -->
<form <form
class="content" class="content"
action="?/updateUser" action="?/updateUser"
@@ -451,7 +373,7 @@
toUpperCase={true} toUpperCase={true}
/> />
<InputField <InputField
name="isued_date" name="issued_date"
type="date" type="date"
label={$t("issued_date")} label={$t("issued_date")}
bind:value={user.drivers_licence.issued_date} bind:value={user.drivers_licence.issued_date}
@@ -473,15 +395,20 @@
<div class="licence-categories"> <div class="licence-categories">
<h3>{$t("licence_categories")}</h3> <h3>{$t("licence_categories")}</h3>
<div class="checkbox-grid"> <div class="checkbox-grid">
{#each licence_categories as category} {#each Object.entries(groupedCategories) as [group, categories], groupIndex}
{#if groupIndex > 0}
<div class="category-break" />
{/if}
{#each categories as category}
<div class="checkbox-item"> <div class="checkbox-item">
<div class="checkbox-label-container"> <div class="checkbox-label-container">
<InputField <InputField
type="checkbox" type="checkbox"
name="licence_categories[]" name="licence_categories[]"
value={category.category} value={JSON.stringify(category)}
label={category.category} label={category.category}
checked={user.drivers_licence.licence_categories != null && checked={user.drivers_licence.licence_categories !=
null &&
user.drivers_licence.licence_categories.some( user.drivers_licence.licence_categories.some(
(cat) => cat.category === category.category (cat) => cat.category === category.category
)} )}
@@ -492,6 +419,7 @@
</span> </span>
</div> </div>
{/each} {/each}
{/each}
</div> </div>
</div> </div>
</div> </div>
@@ -607,6 +535,13 @@
bind:value={user.bank_account.mandate_reference} bind:value={user.bank_account.mandate_reference}
placeholder={$t("placeholder.mandate_reference")} placeholder={$t("placeholder.mandate_reference")}
/> />
<InputField
name="mandate_date_signed"
label={$t("mandate_date_signed")}
type="date"
bind:value={user.bank_account.mandate_date_signed}
readonly={true}
/>
</div> </div>
<div class="button-container"> <div class="button-container">
{#if isUpdating} {#if isUpdating}
@@ -623,6 +558,16 @@
{/if} {/if}
<style> <style>
.category-break {
grid-column: 1 / -1;
height: 1px;
background-color: #ccc;
margin-top: 10px;
margin-left: 20%;
width: 60%;
opacity: 0.4;
}
.licence-categories { .licence-categories {
margin-bottom: 20px; margin-bottom: 20px;
} }
@@ -630,7 +575,7 @@
.checkbox-grid { .checkbox-grid {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 5px; gap: 0px;
} }
.checkbox-item { .checkbox-item {
@@ -641,21 +586,21 @@
.checkbox-label-container { .checkbox-label-container {
flex: 0 0 auto; flex: 0 0 auto;
margin-right: 10px; width: 4em;
margin-right: 5px;
} }
.checkbox-description { .checkbox-description {
flex: 1; flex: 1;
font-size: 14px; font-size: 15px;
color: #9b9b9b; color: #9b9b9b;
text-align: right;
margin-left: 10px; margin-left: 10px;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.checkbox-grid { .checkbox-grid {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 20px; gap: 0px;
} }
} }

View File

@@ -2,12 +2,10 @@ package models
import ( import (
"time" "time"
"gorm.io/gorm"
) )
type DriversLicence struct { type DriversLicence struct {
gorm.Model ID uint `json:"id" gorm:"primaryKey"`
Status int8 `json:"status" validate:"omitempty,number"` Status int8 `json:"status" validate:"omitempty,number"`
LicenceNumber string `json:"number" validate:"omitempty,euDriversLicence,safe_content"` LicenceNumber string `json:"number" validate:"omitempty,euDriversLicence,safe_content"`
IssuedDate time.Time `json:"issued_date" validate:"omitempty,lte"` IssuedDate time.Time `json:"issued_date" validate:"omitempty,lte"`
@@ -17,6 +15,6 @@ type DriversLicence struct {
} }
type LicenceCategory struct { type LicenceCategory struct {
gorm.Model ID uint `json:"id" gorm:"primaryKey"`
Category string `json:"category" validate:"safe_content"` Category string `json:"category" validate:"safe_content"`
} }