profile name surname and save profile data
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -3,8 +3,19 @@
|
||||
|
||||
{% block title %}{{ t(key="profile-title", lang=lang | default(value='sk')) }}{% endblock title %}
|
||||
|
||||
{% macro field(label, value) %}
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ label }}</label>
|
||||
{% if value %}
|
||||
<p class="text-sm text-on-surface/80 dark:text-on-surface-dark/80">{{ value }}</p>
|
||||
{% else %}
|
||||
<p class="text-sm italic text-on-surface/50 dark:text-on-surface-dark/50">{{ t(key="profile-not-set", lang=lang | default(value='sk')) }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro field %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<div class="mx-auto max-w-2xl" x-data="{ editing: {% if error %}true{% else %}false{% endif %} }">
|
||||
<h1 class="text-3xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="profile-title", lang=lang | default(value='sk')) }}</h1>
|
||||
<p class="mt-2 text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="profile-intro", lang=lang | default(value='sk')) }}</p>
|
||||
|
||||
@@ -17,7 +28,60 @@
|
||||
{{ ui::alert_danger(message=t(key="profile-company-required", lang=lang | default(value='sk')), extra="mt-4") }}
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/account/profile" hx-boost="false" class="mt-6 space-y-6">
|
||||
<!-- read-only view (default) -->
|
||||
<div x-show="!editing" class="mt-6 space-y-6">
|
||||
<fieldset class="space-y-2 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-type", lang=lang | default(value='sk')) }}</legend>
|
||||
<div class="flex items-center gap-2">
|
||||
{% 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 %}
|
||||
<span class="text-xs text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="account-type-locked", lang=lang | default(value='sk')) }}</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{% if account_type == "company" %}
|
||||
<fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-company-details", lang=lang | default(value='sk')) }}</legend>
|
||||
{{ self::field(label=t(key="company-name", lang=lang | default(value='sk')), value=company_name) }}
|
||||
<div class="grid gap-4 sm:grid-cols-3">
|
||||
{{ 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) }}
|
||||
</div>
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
|
||||
<fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-contact", lang=lang | default(value='sk')) }}</legend>
|
||||
{{ 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 %}
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-shipping", lang=lang | default(value='sk')) }}</legend>
|
||||
{{ self::field(label=t(key="checkout-address", lang=lang | default(value='sk')), value=address) }}
|
||||
<div class="grid gap-4 sm:grid-cols-3">
|
||||
{{ 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) }}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{{ 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"') }}
|
||||
</div>
|
||||
|
||||
<!-- edit form -->
|
||||
<form x-show="editing" x-cloak method="post" action="/account/profile" hx-boost="false" class="mt-6 space-y-6">
|
||||
<!-- account type is fixed at registration and shown read-only -->
|
||||
<fieldset class="space-y-2 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-type", lang=lang | default(value='sk')) }}</legend>
|
||||
@@ -59,9 +123,15 @@
|
||||
<!-- contact (name/email are managed by the login) -->
|
||||
<fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-contact", lang=lang | default(value='sk')) }}</legend>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-name", lang=lang | default(value='sk')) }}</label>
|
||||
<p class="text-sm text-on-surface/80 dark:text-on-surface-dark/80">{{ name }}</p>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="space-y-1.5">
|
||||
<label for="first_name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="profile-first-name", lang=lang | default(value='sk')) }}</label>
|
||||
{{ ui::input(name="first_name", id="first_name", value=first_name | default(value=''), autocomplete="given-name") }}
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<label for="last_name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="profile-last-name", lang=lang | default(value='sk')) }}</label>
|
||||
{{ ui::input(name="last_name", id="last_name", value=last_name | default(value=''), autocomplete="family-name") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-email", lang=lang | default(value='sk')) }}</label>
|
||||
@@ -149,7 +219,10 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{{ ui::button(label=t(key="profile-save", lang=lang | default(value='sk')), type="submit", size="px-6 py-2.5 text-sm") }}
|
||||
<div class="flex items-center gap-3">
|
||||
{{ 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"') }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -229,8 +229,9 @@
|
||||
{{ ui::textarea(name="note", id="note", rows="3") }}
|
||||
</div>
|
||||
|
||||
{% if logged_in_customer %}
|
||||
<!-- logged-in customers can persist this address to their profile for next time -->
|
||||
{% if logged_in_customer and not profile_filled %}
|
||||
<!-- offered only when the profile has no saved address yet; if it was filled
|
||||
in advance we leave it untouched -->
|
||||
{{ ui::checkbox(name="save_profile", id="save_profile", label=t(key="checkout-save-profile", lang=lang | default(value='sk')), checked=true) }}
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ use crate::{
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ProfileForm {
|
||||
first_name: Option<String>,
|
||||
last_name: Option<String>,
|
||||
company_name: Option<String>,
|
||||
company_id: Option<String>,
|
||||
tax_id: Option<String>,
|
||||
@@ -39,6 +41,24 @@ fn trimmed(value: Option<&str>) -> Option<String> {
|
||||
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<String> {
|
||||
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<Response> {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -113,6 +113,12 @@ async fn checkout_page(
|
||||
let p = |get: fn(&customer_profiles::Model) -> Option<String>| {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user