Compare commits
1 Commits
e51eda9a8c
...
v0.1.10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43c6c04dcf |
@@ -279,6 +279,16 @@ profile-last-name = Surname
|
||||
profile-edit = Edit profile
|
||||
profile-cancel = Cancel
|
||||
profile-not-set = Not set
|
||||
nav-account = My account
|
||||
account-orders = My orders
|
||||
account-change-password = Change password
|
||||
orders-active = Active orders
|
||||
orders-past = Past orders
|
||||
orders-empty = You don't have any orders yet.
|
||||
password-change-title = Change password
|
||||
password-current = Current password
|
||||
password-current-wrong = Your current password is incorrect.
|
||||
password-changed = Your password has been changed.
|
||||
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.
|
||||
@@ -308,6 +318,7 @@ admin-no-orders = No orders yet.
|
||||
order-status-pending = Pending
|
||||
order-status-paid = Paid
|
||||
order-status-shipped = Shipped
|
||||
order-status-delivered = Delivered
|
||||
order-status-cancelled = Cancelled
|
||||
order-update-status = Update status
|
||||
|
||||
|
||||
@@ -279,6 +279,16 @@ profile-last-name = Priezvisko
|
||||
profile-edit = Upraviť profil
|
||||
profile-cancel = Zrušiť
|
||||
profile-not-set = Neuvedené
|
||||
nav-account = Môj účet
|
||||
account-orders = Moje objednávky
|
||||
account-change-password = Zmeniť heslo
|
||||
orders-active = Aktívne objednávky
|
||||
orders-past = Staršie objednávky
|
||||
orders-empty = Zatiaľ nemáte žiadne objednávky.
|
||||
password-change-title = Zmeniť heslo
|
||||
password-current = Súčasné heslo
|
||||
password-current-wrong = Vaše súčasné heslo je nesprávne.
|
||||
password-changed = Vaše heslo bolo zmenené.
|
||||
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.
|
||||
@@ -308,6 +318,7 @@ admin-no-orders = Zatiaľ žiadne objednávky.
|
||||
order-status-pending = Čaká na spracovanie
|
||||
order-status-paid = Zaplatené
|
||||
order-status-shipped = Odoslané
|
||||
order-status-delivered = Doručené
|
||||
order-status-cancelled = Zrušené
|
||||
order-update-status = Zmeniť stav
|
||||
|
||||
|
||||
76
assets/views/account/order_detail.html
Normal file
76
assets/views/account/order_detail.html
Normal file
@@ -0,0 +1,76 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "macros/ui.html" as ui %}
|
||||
|
||||
{% block title %}{{ order.order_number }}{% endblock title %}
|
||||
|
||||
{% macro status_badge(status) %}
|
||||
{% if status == "delivered" %}{{ ui::badge(label=t(key="order-status-" ~ status, lang=lang | default(value='sk')), variant="success") }}
|
||||
{% elif status == "shipped" %}{{ ui::badge(label=t(key="order-status-" ~ status, lang=lang | default(value='sk')), variant="primary") }}
|
||||
{% elif status == "paid" %}{{ ui::badge(label=t(key="order-status-" ~ status, lang=lang | default(value='sk')), variant="info") }}
|
||||
{% elif status == "cancelled" %}{{ ui::badge(label=t(key="order-status-" ~ status, lang=lang | default(value='sk')), variant="danger") }}
|
||||
{% else %}{{ ui::badge(label=t(key="order-status-" ~ status, lang=lang | default(value='sk')), variant="warning") }}
|
||||
{% endif %}
|
||||
{% endmacro status_badge %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mx-auto max-w-2xl space-y-6">
|
||||
<a href="/account/orders" class="inline-flex items-center gap-1 text-sm text-primary underline-offset-2 hover:underline dark:text-primary-dark">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" /></svg>
|
||||
{{ t(key="account-orders", lang=lang | default(value='sk')) }}
|
||||
</a>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<h1 class="font-mono text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ order.order_number }}</h1>
|
||||
{{ self::status_badge(status=order.status) }}
|
||||
</div>
|
||||
<p class="text-sm text-on-surface/60 dark:text-on-surface-dark/60">{{ order.created_at | truncate(length=10, end="") }}</p>
|
||||
|
||||
<div class="rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||
<ul class="space-y-2 pb-3 text-sm">
|
||||
{% for item in items %}
|
||||
<li class="flex justify-between gap-2">
|
||||
<span class="text-on-surface/80 dark:text-on-surface-dark/80">{{ item.product_name }} × {{ item.quantity }}</span>
|
||||
<span class="tabular-nums">{{ item.line_total }} {{ order.currency }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="space-y-1 border-t border-outline py-3 text-sm dark:border-outline-dark">
|
||||
<div class="flex justify-between"><span class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="checkout-subtotal", lang=lang | default(value='sk')) }}</span><span class="tabular-nums">{{ order.subtotal }} {{ order.currency }}</span></div>
|
||||
<div class="flex justify-between"><span class="text-on-surface/70 dark:text-on-surface-dark/70">{{ order.carrier_name }}</span><span class="tabular-nums">{{ order.shipping }} {{ order.currency }}</span></div>
|
||||
{% if order.pickup_point_name %}<div class="text-xs text-on-surface/60 dark:text-on-surface-dark/60">{{ order.pickup_point_name }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="flex justify-between border-t border-outline pt-3 font-bold dark:border-outline-dark">
|
||||
<span>{{ t(key="order-total", lang=lang | default(value='sk')) }}</span>
|
||||
<span class="tabular-nums text-primary dark:text-primary-dark">{{ order.total }} {{ order.currency }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if order.tracking_number %}
|
||||
<div class="rounded-radius border border-outline bg-surface p-4 text-sm dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<span class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="order-tracking", lang=lang | default(value='sk')) }}</span>
|
||||
<span class="font-mono font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ order.tracking_number }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="rounded-radius border border-outline bg-surface p-6 text-sm dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||
<h2 class="mb-2 font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-shipping", lang=lang | default(value='sk')) }}</h2>
|
||||
<p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ order.customer_name }}</p>
|
||||
{% if order.address %}<p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ order.address }}</p>{% endif %}
|
||||
<p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ order.zip }} {{ order.city }}{% if order.country %}, {{ order.country }}{% endif %}</p>
|
||||
</div>
|
||||
|
||||
{% if order.payment_method == "bank_transfer" and order.status == "pending" %}
|
||||
<div class="space-y-2 rounded-radius border border-primary/40 bg-primary/5 p-6 text-sm dark:border-primary-dark/40">
|
||||
<p class="font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="payment-bank-instructions", lang=lang | default(value='sk')) }}</p>
|
||||
<div class="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1">
|
||||
<span class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="bank-account-name", lang=lang | default(value='sk')) }}</span><span class="font-medium">{{ order.bank_account_name }}</span>
|
||||
<span class="text-on-surface/70 dark:text-on-surface-dark/70">IBAN</span><span class="font-mono font-medium">{{ order.bank_iban }}</span>
|
||||
<span class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="bank-variable-symbol", lang=lang | default(value='sk')) }}</span><span class="font-mono font-medium">{{ order.variable_symbol }}</span>
|
||||
<span class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="bank-amount", lang=lang | default(value='sk')) }}</span><span class="font-medium tabular-nums">{{ order.total }} {{ order.currency }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
52
assets/views/account/orders.html
Normal file
52
assets/views/account/orders.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "macros/ui.html" as ui %}
|
||||
|
||||
{% block title %}{{ t(key="account-orders", lang=lang | default(value='sk')) }}{% endblock title %}
|
||||
|
||||
{# status → badge variant #}
|
||||
{% macro status_badge(status) %}
|
||||
{% if status == "delivered" %}{{ ui::badge(label=t(key="order-status-" ~ status, lang=lang | default(value='sk')), variant="success") }}
|
||||
{% elif status == "shipped" %}{{ ui::badge(label=t(key="order-status-" ~ status, lang=lang | default(value='sk')), variant="primary") }}
|
||||
{% elif status == "paid" %}{{ ui::badge(label=t(key="order-status-" ~ status, lang=lang | default(value='sk')), variant="info") }}
|
||||
{% elif status == "cancelled" %}{{ ui::badge(label=t(key="order-status-" ~ status, lang=lang | default(value='sk')), variant="danger") }}
|
||||
{% else %}{{ ui::badge(label=t(key="order-status-" ~ status, lang=lang | default(value='sk')), variant="warning") }}
|
||||
{% endif %}
|
||||
{% endmacro status_badge %}
|
||||
|
||||
{% macro order_row(order) %}
|
||||
<a href="/account/orders/{{ order.order_number }}"
|
||||
class="flex flex-wrap items-center justify-between gap-3 rounded-radius border border-outline bg-surface p-4 transition hover:border-primary hover:bg-primary/5 dark:border-outline-dark dark:bg-surface-dark-alt dark:hover:border-primary-dark">
|
||||
<div class="min-w-0">
|
||||
<p class="font-mono text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ order.order_number }}</p>
|
||||
<p class="text-xs text-on-surface/60 dark:text-on-surface-dark/60">{{ order.created_at | truncate(length=10, end="") }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
{{ self::status_badge(status=order.status) }}
|
||||
<span class="tabular-nums text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ order.total }} {{ order.currency }}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endmacro order_row %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mx-auto max-w-3xl space-y-8">
|
||||
<h1 class="text-3xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-orders", lang=lang | default(value='sk')) }}</h1>
|
||||
|
||||
{% if active_orders | length == 0 and past_orders | length == 0 %}
|
||||
<p class="rounded-radius border border-outline bg-surface p-6 text-sm text-on-surface/70 dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark/70">{{ t(key="orders-empty", lang=lang | default(value='sk')) }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if active_orders | length > 0 %}
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-lg font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="orders-active", lang=lang | default(value='sk')) }}</h2>
|
||||
{% for order in active_orders %}{{ self::order_row(order=order) }}{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if past_orders | length > 0 %}
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-lg font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="orders-past", lang=lang | default(value='sk')) }}</h2>
|
||||
{% for order in past_orders %}{{ self::order_row(order=order) }}{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
43
assets/views/account/password.html
Normal file
43
assets/views/account/password.html
Normal file
@@ -0,0 +1,43 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "macros/ui.html" as ui %}
|
||||
|
||||
{% block title %}{{ t(key="password-change-title", lang=lang | default(value='sk')) }}{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mx-auto max-w-md">
|
||||
<h1 class="text-3xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="password-change-title", lang=lang | default(value='sk')) }}</h1>
|
||||
|
||||
{% if changed %}
|
||||
<div class="mt-4 rounded-radius border border-success bg-success/10 px-4 py-3 text-sm text-success" role="status">
|
||||
{{ t(key="password-changed", lang=lang | default(value='sk')) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if error == "current" %}
|
||||
{{ ui::alert_danger(message=t(key="password-current-wrong", lang=lang | default(value='sk')), extra="mt-4") }}
|
||||
{% elif error == "mismatch" %}
|
||||
{{ ui::alert_danger(message=t(key="set-password-mismatch", lang=lang | default(value='sk')), extra="mt-4") }}
|
||||
{% elif error == "weak" %}
|
||||
{{ ui::alert_danger(message=t(key="set-password-weak", lang=lang | default(value='sk')), extra="mt-4") }}
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/account/password" hx-boost="false" class="mt-6 flex flex-col gap-4"
|
||||
x-data="{ password: '', confirm: '' }">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="current_password" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="password-current", lang=lang | default(value='sk')) }}</label>
|
||||
{{ ui::input(name="current_password", id="current_password", type="password", required=true, autocomplete="current-password") }}
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="password" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="set-password-new", lang=lang | default(value='sk')) }}</label>
|
||||
{{ ui::input(name="password", id="password", type="password", required=true, autocomplete="new-password", attrs='x-model="password"') }}
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="password_confirm" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="set-password-confirm", lang=lang | default(value='sk')) }}</label>
|
||||
{{ ui::input(name="password_confirm", id="password_confirm", type="password", required=true, autocomplete="new-password", attrs='x-model="confirm"') }}
|
||||
<span x-cloak x-show="confirm.length > 0 && password !== confirm" class="text-xs text-danger dark:text-danger">
|
||||
{{ t(key="set-password-mismatch", lang=lang | default(value='sk')) }}
|
||||
</span>
|
||||
</div>
|
||||
{{ ui::button(label=t(key="password-change-title", lang=lang | default(value='sk')), type="submit", extra="mt-1 w-full", attrs=':disabled="password !== confirm"') }}
|
||||
</form>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -158,6 +158,22 @@
|
||||
class="fixed inset-0 z-30 bg-black/50 lg:hidden"></div>
|
||||
|
||||
<div class="mx-auto flex w-full max-w-7xl gap-8 px-4 py-8">
|
||||
{% if account_nav %}
|
||||
<!-- account-area sidebar: replaces the storefront categories while the
|
||||
customer is inside /account/*. -->
|
||||
<aside x-cloak x-show="cats || lg" aria-label="{{ t(key='nav-account', lang=lang | default(value='sk')) }}"
|
||||
class="fixed inset-y-0 left-0 z-40 w-64 overflow-y-auto border-r border-outline bg-surface-alt p-4 lg:static lg:z-auto lg:w-64 lg:shrink-0 lg:self-start lg:overflow-visible lg:rounded-radius lg:border lg:p-3 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||
<h2 class="px-3 pb-2 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="nav-account", lang=lang | default(value='sk')) }}</h2>
|
||||
<ul class="space-y-1">
|
||||
<li><a href="/account/orders" data-nav="/account/orders" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-primary focus:outline-hidden focus-visible:underline aria-[current=page]:font-semibold aria-[current=page]:bg-primary/10 aria-[current=page]:text-primary dark:text-on-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="account-orders", lang=lang | default(value='sk')) }}</a></li>
|
||||
<li><a href="/account/profile" data-nav="/account/profile" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-primary focus:outline-hidden focus-visible:underline aria-[current=page]:font-semibold aria-[current=page]:bg-primary/10 aria-[current=page]:text-primary dark:text-on-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="profile-title", lang=lang | default(value='sk')) }}</a></li>
|
||||
<li><a href="/account/password" data-nav="/account/password" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-primary focus:outline-hidden focus-visible:underline aria-[current=page]:font-semibold aria-[current=page]:bg-primary/10 aria-[current=page]:text-primary dark:text-on-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="account-change-password", lang=lang | default(value='sk')) }}</a></li>
|
||||
</ul>
|
||||
<form method="post" action="/logout" hx-boost="false" class="mt-4 border-t border-outline pt-3 dark:border-outline-dark">
|
||||
<button type="submit" class="block w-full rounded-radius px-3 py-2 text-left text-sm font-medium text-danger underline-offset-2 transition hover:bg-primary/5 focus:outline-hidden focus-visible:underline">{{ t(key="logout", lang=lang | default(value='sk')) }}</button>
|
||||
</form>
|
||||
</aside>
|
||||
{% else %}
|
||||
<!-- persistent category sidebar (off-canvas drawer on mobile).
|
||||
hx-preserve keeps this node across boosted page swaps, so it is
|
||||
fetched once (hx-trigger=load) and never reloaded on navigation. -->
|
||||
@@ -166,6 +182,7 @@
|
||||
hx-get="/partials/categories" hx-trigger="load"
|
||||
class="fixed inset-y-0 left-0 z-40 w-64 overflow-y-auto border-r border-outline bg-surface-alt p-4 lg:static lg:z-auto lg:w-64 lg:shrink-0 lg:self-start lg:overflow-visible lg:rounded-radius lg:border lg:p-3 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||
</aside>
|
||||
{% endif %}
|
||||
|
||||
<main class="min-w-0 flex-1">
|
||||
{% block content %}{% endblock content %}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
use axum_extra::extract::cookie::CookieJar;
|
||||
use loco_rs::prelude::*;
|
||||
use sea_orm::QueryOrder;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
@@ -16,11 +17,16 @@ use crate::{
|
||||
controllers::i18n::current_lang,
|
||||
models::{
|
||||
customer_profiles::{self, ProfileFields},
|
||||
users,
|
||||
order_items, orders, users,
|
||||
},
|
||||
shared::guard,
|
||||
shared::{guard, settings},
|
||||
views::checkout as order_view,
|
||||
};
|
||||
|
||||
/// Active (still-being-fulfilled) order statuses. Anything else
|
||||
/// (`delivered`, `cancelled`) is considered closed/past.
|
||||
const ACTIVE_STATUSES: [&str; 3] = ["pending", "paid", "shipped"];
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ProfileForm {
|
||||
first_name: Option<String>,
|
||||
@@ -119,6 +125,7 @@ fn profile_view(
|
||||
json!({
|
||||
"logged_in_admin": false,
|
||||
"logged_in_customer": true,
|
||||
"account_nav": true,
|
||||
"saved": saved,
|
||||
"error": error,
|
||||
"name": user.name,
|
||||
@@ -193,8 +200,165 @@ async fn save_profile(
|
||||
profile_view(&v, &jar, &user, &fields, true, false)
|
||||
}
|
||||
|
||||
/// Lists the signed-in customer's orders, split into still-active and past.
|
||||
#[debug_handler]
|
||||
async fn orders_page(
|
||||
jar: CookieJar,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
let Some(user) = guard::current_user(&ctx, &jar).await else {
|
||||
return format::redirect("/login");
|
||||
};
|
||||
if guard::is_admin(&ctx, &user) {
|
||||
return format::redirect("/admin/dashboard");
|
||||
}
|
||||
let rows = orders::Entity::find()
|
||||
.filter(orders::Column::UserId.eq(user.id))
|
||||
.order_by_desc(orders::Column::CreatedAt)
|
||||
.all(&ctx.db)
|
||||
.await?;
|
||||
let (active, past): (Vec<_>, Vec<_>) = rows
|
||||
.iter()
|
||||
.partition(|o| ACTIVE_STATUSES.contains(&o.status.as_str()));
|
||||
let shape = |list: Vec<&orders::Model>| -> Vec<_> {
|
||||
list.into_iter().map(order_view::summary).collect()
|
||||
};
|
||||
|
||||
format::view(
|
||||
&v,
|
||||
"account/orders.html",
|
||||
json!({
|
||||
"logged_in_admin": false,
|
||||
"logged_in_customer": true,
|
||||
"account_nav": true,
|
||||
"active_orders": shape(active),
|
||||
"past_orders": shape(past),
|
||||
"lang": current_lang(&jar),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Shows a single order belonging to the signed-in customer. Orders owned by
|
||||
/// someone else (or guest orders) are not found here.
|
||||
#[debug_handler]
|
||||
async fn order_detail_page(
|
||||
jar: CookieJar,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
Path(order_number): Path<String>,
|
||||
) -> Result<Response> {
|
||||
let Some(user) = guard::current_user(&ctx, &jar).await else {
|
||||
return format::redirect("/login");
|
||||
};
|
||||
if guard::is_admin(&ctx, &user) {
|
||||
return format::redirect("/admin/dashboard");
|
||||
}
|
||||
let order = orders::Entity::find()
|
||||
.filter(orders::Column::OrderNumber.eq(order_number))
|
||||
.one(&ctx.db)
|
||||
.await?
|
||||
.filter(|o| o.user_id == Some(user.id))
|
||||
.ok_or_else(|| Error::NotFound)?;
|
||||
let items = order_items::Entity::find()
|
||||
.filter(order_items::Column::OrderId.eq(order.id))
|
||||
.all(&ctx.db)
|
||||
.await?;
|
||||
|
||||
format::view(
|
||||
&v,
|
||||
"account/order_detail.html",
|
||||
json!({
|
||||
"logged_in_admin": false,
|
||||
"logged_in_customer": true,
|
||||
"account_nav": true,
|
||||
"order": order_view::detail(
|
||||
&order,
|
||||
settings::get(&ctx, "bank_iban").unwrap_or(""),
|
||||
settings::get(&ctx, "bank_account_name").unwrap_or(""),
|
||||
),
|
||||
"items": order_view::items(&items),
|
||||
"lang": current_lang(&jar),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ChangePasswordForm {
|
||||
current_password: String,
|
||||
password: String,
|
||||
password_confirm: String,
|
||||
}
|
||||
|
||||
fn password_view(
|
||||
v: &TeraView,
|
||||
jar: &CookieJar,
|
||||
changed: bool,
|
||||
error: Option<&str>,
|
||||
) -> Result<Response> {
|
||||
format::view(
|
||||
v,
|
||||
"account/password.html",
|
||||
json!({
|
||||
"logged_in_admin": false,
|
||||
"logged_in_customer": true,
|
||||
"account_nav": true,
|
||||
"changed": changed,
|
||||
"error": error,
|
||||
"lang": current_lang(jar),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn change_password_page(
|
||||
jar: CookieJar,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
let Some(user) = guard::current_user(&ctx, &jar).await else {
|
||||
return format::redirect("/login");
|
||||
};
|
||||
if guard::is_admin(&ctx, &user) {
|
||||
return format::redirect("/admin/dashboard");
|
||||
}
|
||||
password_view(&v, &jar, false, None)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn change_password(
|
||||
jar: CookieJar,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
Form(form): Form<ChangePasswordForm>,
|
||||
) -> Result<Response> {
|
||||
let Some(user) = guard::current_user(&ctx, &jar).await else {
|
||||
return format::redirect("/login");
|
||||
};
|
||||
if guard::is_admin(&ctx, &user) {
|
||||
return format::redirect("/admin/dashboard");
|
||||
}
|
||||
if !user.verify_password(&form.current_password) {
|
||||
return password_view(&v, &jar, false, Some("current"));
|
||||
}
|
||||
if form.password != form.password_confirm {
|
||||
return password_view(&v, &jar, false, Some("mismatch"));
|
||||
}
|
||||
if form.password.len() < 8 {
|
||||
return password_view(&v, &jar, false, Some("weak"));
|
||||
}
|
||||
user.into_active_model()
|
||||
.reset_password(&ctx.db, &form.password)
|
||||
.await?;
|
||||
password_view(&v, &jar, true, None)
|
||||
}
|
||||
|
||||
pub fn routes() -> Routes {
|
||||
Routes::new()
|
||||
.add("/account/profile", get(profile_page))
|
||||
.add("/account/profile", post(save_profile))
|
||||
.add("/account/orders", get(orders_page))
|
||||
.add("/account/orders/{order_number}", get(order_detail_page))
|
||||
.add("/account/password", get(change_password_page))
|
||||
.add("/account/password", post(change_password))
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ use crate::{
|
||||
shared::{guard, settings},
|
||||
};
|
||||
|
||||
pub(crate) const ORDER_STATUSES: [&str; 4] = ["pending", "paid", "shipped", "cancelled"];
|
||||
pub(crate) const ORDER_STATUSES: [&str; 5] =
|
||||
["pending", "paid", "shipped", "delivered", "cancelled"];
|
||||
|
||||
/// Fallback parcel weight when products carry no weight of their own.
|
||||
const DEFAULT_PARCEL_WEIGHT_GRAMS: i32 = 1000;
|
||||
|
||||
Reference in New Issue
Block a user