my profile orders and sidebar
This commit is contained in:
@@ -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