From 12e00a782dfb0a7abd2e46a43f6d6a9d5e70d878 Mon Sep 17 00:00:00 2001 From: Priec Date: Fri, 19 Jun 2026 11:37:51 +0200 Subject: [PATCH] profile name surname and save profile data --- assets/i18n/en/main.ftl | 5 ++ assets/i18n/sk/main.ftl | 5 ++ assets/views/account/profile.html | 85 ++++++++++++++++++++++++++++--- assets/views/shop/checkout.html | 5 +- src/controllers/account.rs | 36 +++++++++++++ src/controllers/checkout.rs | 7 +++ 6 files changed, 135 insertions(+), 8 deletions(-) diff --git a/assets/i18n/en/main.ftl b/assets/i18n/en/main.ftl index 9f92713..28a4f61 100644 --- a/assets/i18n/en/main.ftl +++ b/assets/i18n/en/main.ftl @@ -274,6 +274,11 @@ profile-intro = We'll use these details to prefill checkout. profile-saved = Profile saved. profile-save = Save profile profile-company-required = For a company account, please fill in company name, IČO and DIČ. +profile-first-name = First name +profile-last-name = Surname +profile-edit = Edit profile +profile-cancel = Cancel +profile-not-set = Not set account-type-locked = Account type can't be changed after registration. checkout-create-account = Create an account from this order checkout-create-account-hint = We'll email you a link to set your password. This order will be linked to your account. diff --git a/assets/i18n/sk/main.ftl b/assets/i18n/sk/main.ftl index 6453d0c..d6ab07d 100644 --- a/assets/i18n/sk/main.ftl +++ b/assets/i18n/sk/main.ftl @@ -274,6 +274,11 @@ profile-intro = Tieto údaje použijeme na predvyplnenie pokladne. profile-saved = Profil bol uložený. profile-save = Uložiť profil profile-company-required = Pri firemnom účte vyplňte názov firmy, IČO a DIČ. +profile-first-name = Meno +profile-last-name = Priezvisko +profile-edit = Upraviť profil +profile-cancel = Zrušiť +profile-not-set = Neuvedené account-type-locked = Typ účtu sa po registrácii nedá zmeniť. checkout-create-account = Vytvoriť účet z tejto objednávky checkout-create-account-hint = Pošleme vám e-mail na nastavenie hesla. Objednávka sa priradí k vášmu účtu. diff --git a/assets/views/account/profile.html b/assets/views/account/profile.html index d8fa519..61c58fd 100644 --- a/assets/views/account/profile.html +++ b/assets/views/account/profile.html @@ -3,8 +3,19 @@ {% block title %}{{ t(key="profile-title", lang=lang | default(value='sk')) }}{% endblock title %} +{% macro field(label, value) %} +
+ + {% if value %} +

{{ value }}

+ {% else %} +

{{ t(key="profile-not-set", lang=lang | default(value='sk')) }}

+ {% endif %} +
+{% endmacro field %} + {% block content %} -
+

{{ t(key="profile-title", lang=lang | default(value='sk')) }}

{{ t(key="profile-intro", lang=lang | default(value='sk')) }}

@@ -17,7 +28,60 @@ {{ ui::alert_danger(message=t(key="profile-company-required", lang=lang | default(value='sk')), extra="mt-4") }} {% endif %} -
+ +
+
+ {{ t(key="account-type", lang=lang | default(value='sk')) }} +
+ {% if account_type == "company" %} + {{ ui::badge(label=t(key="account-company", lang=lang | default(value='sk')), variant="primary") }} + {% else %} + {{ ui::badge(label=t(key="account-personal", lang=lang | default(value='sk')), variant="neutral") }} + {% endif %} + {{ t(key="account-type-locked", lang=lang | default(value='sk')) }} +
+
+ + {% if account_type == "company" %} +
+ {{ t(key="account-company-details", lang=lang | default(value='sk')) }} + {{ self::field(label=t(key="company-name", lang=lang | default(value='sk')), value=company_name) }} +
+ {{ self::field(label=t(key="company-ico", lang=lang | default(value='sk')), value=company_id) }} + {{ self::field(label=t(key="company-dic", lang=lang | default(value='sk')), value=tax_id) }} + {{ self::field(label=t(key="company-icdph", lang=lang | default(value='sk')), value=vat_id) }} +
+
+ {% endif %} + +
+ {{ t(key="checkout-contact", lang=lang | default(value='sk')) }} + {{ self::field(label=t(key="checkout-name", lang=lang | default(value='sk')), value=name) }} + {{ self::field(label=t(key="checkout-email", lang=lang | default(value='sk')), value=email) }} + {% if phone %} + {% set phone_full = phone_prefix | default(value='') %} + {% set phone_full = phone_full ~ ' ' ~ phone %} + {{ self::field(label=t(key="checkout-phone", lang=lang | default(value='sk')), value=phone_full) }} + {% else %} + {{ self::field(label=t(key="checkout-phone", lang=lang | default(value='sk')), value='') }} + {% endif %} +
+ +
+ {{ t(key="checkout-shipping", lang=lang | default(value='sk')) }} + {{ self::field(label=t(key="checkout-address", lang=lang | default(value='sk')), value=address) }} +
+ {{ self::field(label=t(key="checkout-city", lang=lang | default(value='sk')), value=city) }} + {{ self::field(label=t(key="checkout-zip", lang=lang | default(value='sk')), value=zip) }} + {{ self::field(label=t(key="checkout-country", lang=lang | default(value='sk')), value=country) }} +
+
+ + {{ ui::button(label=t(key="profile-edit", lang=lang | default(value='sk')), type="button", size="px-6 py-2.5 text-sm", attrs='@click="editing = true"') }} +
+ + +
{{ t(key="account-type", lang=lang | default(value='sk')) }} @@ -59,9 +123,15 @@
{{ t(key="checkout-contact", lang=lang | default(value='sk')) }} -
- -

{{ name }}

+
+
+ + {{ ui::input(name="first_name", id="first_name", value=first_name | default(value=''), autocomplete="given-name") }} +
+
+ + {{ ui::input(name="last_name", id="last_name", value=last_name | default(value=''), autocomplete="family-name") }} +
@@ -149,7 +219,10 @@
- {{ ui::button(label=t(key="profile-save", lang=lang | default(value='sk')), type="submit", size="px-6 py-2.5 text-sm") }} +
+ {{ ui::button(label=t(key="profile-save", lang=lang | default(value='sk')), type="submit", size="px-6 py-2.5 text-sm") }} + {{ ui::button(label=t(key="profile-cancel", lang=lang | default(value='sk')), type="button", variant="outline-secondary", size="px-6 py-2.5 text-sm", attrs='@click="editing = false"') }} +
{% endblock content %} diff --git a/assets/views/shop/checkout.html b/assets/views/shop/checkout.html index 4909b71..79fa985 100644 --- a/assets/views/shop/checkout.html +++ b/assets/views/shop/checkout.html @@ -229,8 +229,9 @@ {{ ui::textarea(name="note", id="note", rows="3") }}
- {% if logged_in_customer %} - + {% if logged_in_customer and not profile_filled %} + {{ ui::checkbox(name="save_profile", id="save_profile", label=t(key="checkout-save-profile", lang=lang | default(value='sk')), checked=true) }} {% endif %} diff --git a/src/controllers/account.rs b/src/controllers/account.rs index 5c651b0..73181c8 100644 --- a/src/controllers/account.rs +++ b/src/controllers/account.rs @@ -23,6 +23,8 @@ use crate::{ #[derive(Debug, Deserialize)] struct ProfileForm { + first_name: Option, + last_name: Option, company_name: Option, company_id: Option, tax_id: Option, @@ -39,6 +41,24 @@ fn trimmed(value: Option<&str>) -> Option { value.map(str::trim).filter(|v| !v.is_empty()).map(String::from) } +/// Split a stored full name into (first name, surname). The surname is +/// everything after the first whitespace, so multi-word surnames round-trip. +fn split_name(name: &str) -> (String, String) { + match name.trim().split_once(char::is_whitespace) { + Some((first, rest)) => (first.to_string(), rest.trim().to_string()), + None => (name.trim().to_string(), String::new()), + } +} + +/// Recombine the two name fields into the single stored `name`. Returns `None` +/// when the result is too short to be a valid name (the user can't blank it out). +fn full_name_from_form(form: &ProfileForm) -> Option { + let first = form.first_name.as_deref().unwrap_or("").trim(); + let last = form.last_name.as_deref().unwrap_or("").trim(); + let full = format!("{first} {last}").trim().to_string(); + (full.chars().count() >= 2).then_some(full) +} + /// Build the persisted fields from the submitted form. Company identifiers are /// only kept for company accounts (a personal account can never carry them). fn fields_from_form(form: &ProfileForm, is_company: bool) -> ProfileFields { @@ -92,6 +112,7 @@ fn profile_view( saved: bool, error: bool, ) -> Result { + let (first_name, last_name) = split_name(&user.name); format::view( v, "account/profile.html", @@ -101,6 +122,8 @@ fn profile_view( "saved": saved, "error": error, "name": user.name, + "first_name": first_name, + "last_name": last_name, "email": user.email, "account_type": user.account_type, "company_name": fields.company_name, @@ -147,12 +170,25 @@ async fn save_profile( if guard::is_admin(&ctx, &user) { return format::redirect("/admin/dashboard"); } + // Apply the edited name to a working copy so it's reflected on both the + // success and re-rendered-error views. A blank/too-short name is ignored — + // the field can't be cleared. + let mut user = user; + let new_name = full_name_from_form(&form).filter(|n| *n != user.name); + if let Some(name) = new_name.clone() { + user.name = name; + } let fields = fields_from_form(&form, user.is_company()); // A company account's profile is rejected (and re-shown with the entered // values) until it carries its required identifiers. if user.is_company() && company_fields_missing(&fields) { return profile_view(&v, &jar, &user, &fields, false, true); } + if let Some(name) = new_name { + let mut active = user.clone().into_active_model(); + active.name = ActiveValue::set(name); + active.update(&ctx.db).await?; + } customer_profiles::Model::upsert(&ctx.db, user.id, fields.clone()).await?; profile_view(&v, &jar, &user, &fields, true, false) } diff --git a/src/controllers/checkout.rs b/src/controllers/checkout.rs index bc9a472..964b456 100644 --- a/src/controllers/checkout.rs +++ b/src/controllers/checkout.rs @@ -113,6 +113,12 @@ async fn checkout_page( let p = |get: fn(&customer_profiles::Model) -> Option| { profile.as_ref().and_then(get) }; + // Whether the customer already has a shipping address on file. When they do, + // the "save this address to my profile" opt-in is pointless (the profile was + // filled in advance), so it's hidden and the existing profile is left alone. + let profile_filled = profile + .as_ref() + .is_some_and(|pr| pr.address.is_some() && pr.city.is_some() && pr.zip.is_some()); format::view( &v, @@ -126,6 +132,7 @@ async fn checkout_page( "packeta_api_key": settings::get(&ctx, "packeta_api_key").unwrap_or(""), "logged_in_admin": is_admin, "logged_in_customer": is_customer, + "profile_filled": profile_filled, // A logged-in customer's account type is fixed; only guests pick it // and may opt to create an account from the order. "account_fixed": is_customer,