phone number + country
This commit is contained in:
@@ -222,10 +222,17 @@ checkout-contact = Contact details
|
|||||||
checkout-shipping = Shipping address
|
checkout-shipping = Shipping address
|
||||||
checkout-email = Email
|
checkout-email = Email
|
||||||
checkout-name = Full name
|
checkout-name = Full name
|
||||||
|
checkout-phone = Phone
|
||||||
checkout-address = Address
|
checkout-address = Address
|
||||||
checkout-city = City
|
checkout-city = City
|
||||||
checkout-zip = Postal code
|
checkout-zip = Postal code
|
||||||
checkout-country = Country
|
checkout-country = Country
|
||||||
|
country-sk = Slovakia
|
||||||
|
country-cz = Czechia
|
||||||
|
country-at = Austria
|
||||||
|
country-de = Germany
|
||||||
|
country-pl = Poland
|
||||||
|
country-hu = Hungary
|
||||||
checkout-note = Order note
|
checkout-note = Order note
|
||||||
checkout-place-order = Place order
|
checkout-place-order = Place order
|
||||||
checkout-summary = Order summary
|
checkout-summary = Order summary
|
||||||
|
|||||||
@@ -222,10 +222,17 @@ checkout-contact = Kontaktné údaje
|
|||||||
checkout-shipping = Dodacia adresa
|
checkout-shipping = Dodacia adresa
|
||||||
checkout-email = E-mail
|
checkout-email = E-mail
|
||||||
checkout-name = Meno a priezvisko
|
checkout-name = Meno a priezvisko
|
||||||
|
checkout-phone = Telefón
|
||||||
checkout-address = Adresa
|
checkout-address = Adresa
|
||||||
checkout-city = Mesto
|
checkout-city = Mesto
|
||||||
checkout-zip = PSČ
|
checkout-zip = PSČ
|
||||||
checkout-country = Krajina
|
checkout-country = Krajina
|
||||||
|
country-sk = Slovensko
|
||||||
|
country-cz = Česko
|
||||||
|
country-at = Rakúsko
|
||||||
|
country-de = Nemecko
|
||||||
|
country-pl = Poľsko
|
||||||
|
country-hu = Maďarsko
|
||||||
checkout-note = Poznámka k objednávke
|
checkout-note = Poznámka k objednávke
|
||||||
checkout-place-order = Odoslať objednávku
|
checkout-place-order = Odoslať objednávku
|
||||||
checkout-summary = Súhrn objednávky
|
checkout-summary = Súhrn objednávky
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
<p class="text-xs uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="order-customer", lang=lang | default(value='sk')) }}</p>
|
<p class="text-xs uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="order-customer", lang=lang | default(value='sk')) }}</p>
|
||||||
<p class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ order.customer_name }}</p>
|
<p class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ order.customer_name }}</p>
|
||||||
<p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ order.email }}</p>
|
<p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ order.email }}</p>
|
||||||
|
{% if order.phone %}<p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ order.phone }}</p>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="checkout-shipping", lang=lang | default(value='sk')) }}</p>
|
<p class="text-xs uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="checkout-shipping", lang=lang | default(value='sk')) }}</p>
|
||||||
|
|||||||
@@ -43,6 +43,25 @@
|
|||||||
<input id="customer_name" name="customer_name" type="text" required
|
<input id="customer_name" name="customer_name" type="text" required
|
||||||
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label for="phone" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-phone", lang=lang | default(value='sk')) }}</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<select id="phone_prefix" name="phone_prefix" aria-label="{{ t(key='checkout-phone', lang=lang | default(value='sk')) }}"
|
||||||
|
class="w-32 shrink-0 rounded-radius border border-outline bg-surface px-2 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
||||||
|
<option value="+421">🇸🇰 +421</option>
|
||||||
|
<option value="+420">🇨🇿 +420</option>
|
||||||
|
<option value="+43">🇦🇹 +43</option>
|
||||||
|
<option value="+49">🇩🇪 +49</option>
|
||||||
|
<option value="+48">🇵🇱 +48</option>
|
||||||
|
<option value="+36">🇭🇺 +36</option>
|
||||||
|
<option value="+44">🇬🇧 +44</option>
|
||||||
|
<option value="+39">🇮🇹 +39</option>
|
||||||
|
<option value="+33">🇫🇷 +33</option>
|
||||||
|
</select>
|
||||||
|
<input id="phone" name="phone" type="tel" required autocomplete="tel" inputmode="tel" placeholder="900 000 000"
|
||||||
|
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<!-- shipping address -->
|
<!-- shipping address -->
|
||||||
@@ -66,8 +85,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="country" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-country", lang=lang | default(value='sk')) }}</label>
|
<label for="country" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-country", lang=lang | default(value='sk')) }}</label>
|
||||||
<input id="country" name="country" type="text" required value="Slovensko"
|
<select id="country" name="country" required
|
||||||
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
||||||
|
<option value="Slovensko" selected>🇸🇰 {{ t(key="country-sk", lang=lang | default(value='sk')) }}</option>
|
||||||
|
<option value="Česko">🇨🇿 {{ t(key="country-cz", lang=lang | default(value='sk')) }}</option>
|
||||||
|
<option value="Rakúsko">🇦🇹 {{ t(key="country-at", lang=lang | default(value='sk')) }}</option>
|
||||||
|
<option value="Nemecko">🇩🇪 {{ t(key="country-de", lang=lang | default(value='sk')) }}</option>
|
||||||
|
<option value="Poľsko">🇵🇱 {{ t(key="country-pl", lang=lang | default(value='sk')) }}</option>
|
||||||
|
<option value="Maďarsko">🇭🇺 {{ t(key="country-hu", lang=lang | default(value='sk')) }}</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ mod m20260616_150812_add_shipping_fields_to_orders;
|
|||||||
mod m20260616_160000_add_parent_to_categories;
|
mod m20260616_160000_add_parent_to_categories;
|
||||||
mod m20260617_000001_add_carrier_to_shipping_methods;
|
mod m20260617_000001_add_carrier_to_shipping_methods;
|
||||||
mod m20260617_000002_add_shipment_to_orders;
|
mod m20260617_000002_add_shipment_to_orders;
|
||||||
|
mod m20260617_000003_add_phone_to_orders;
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
@@ -62,6 +63,7 @@ impl MigratorTrait for Migrator {
|
|||||||
Box::new(m20260616_160000_add_parent_to_categories::Migration),
|
Box::new(m20260616_160000_add_parent_to_categories::Migration),
|
||||||
Box::new(m20260617_000001_add_carrier_to_shipping_methods::Migration),
|
Box::new(m20260617_000001_add_carrier_to_shipping_methods::Migration),
|
||||||
Box::new(m20260617_000002_add_shipment_to_orders::Migration),
|
Box::new(m20260617_000002_add_shipment_to_orders::Migration),
|
||||||
|
Box::new(m20260617_000003_add_phone_to_orders::Migration),
|
||||||
// inject-above (do not remove this comment)
|
// inject-above (do not remove this comment)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
17
migration/src/m20260617_000003_add_phone_to_orders.rs
Normal file
17
migration/src/m20260617_000003_add_phone_to_orders.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
use loco_rs::schema::*;
|
||||||
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
// Customer contact phone, also passed to carriers for pickup SMS.
|
||||||
|
add_column(m, "orders", "phone", ColType::StringNull).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
remove_column(m, "orders", "phone").await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -178,6 +178,7 @@ async fn ship(
|
|||||||
order_number: &order.order_number,
|
order_number: &order.order_number,
|
||||||
recipient_name: recipient,
|
recipient_name: recipient,
|
||||||
email: &order.email,
|
email: &order.email,
|
||||||
|
phone: order.phone.as_deref(),
|
||||||
address: order.address.as_deref(),
|
address: order.address.as_deref(),
|
||||||
city: order.city.as_deref(),
|
city: order.city.as_deref(),
|
||||||
zip: order.zip.as_deref(),
|
zip: order.zip.as_deref(),
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ const PAYMENT_METHODS: [&str; 2] = ["cod", "bank_transfer"];
|
|||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct CheckoutForm {
|
struct CheckoutForm {
|
||||||
email: String,
|
email: String,
|
||||||
|
phone_prefix: String,
|
||||||
|
phone: String,
|
||||||
customer_name: String,
|
customer_name: String,
|
||||||
address: String,
|
address: String,
|
||||||
city: String,
|
city: String,
|
||||||
@@ -111,6 +113,25 @@ async fn place_order(
|
|||||||
}
|
}
|
||||||
let email =
|
let email =
|
||||||
trimmed(&form.email).ok_or_else(|| Error::BadRequest("email is required".to_string()))?;
|
trimmed(&form.email).ok_or_else(|| Error::BadRequest("email is required".to_string()))?;
|
||||||
|
// Combine the dialling-code prefix with the local number into one E.164-ish
|
||||||
|
// value (e.g. "+421 900123456").
|
||||||
|
let number =
|
||||||
|
trimmed(&form.phone).ok_or_else(|| Error::BadRequest("phone is required".to_string()))?;
|
||||||
|
let phone = match trimmed(&form.phone_prefix) {
|
||||||
|
Some(prefix) => format!("{prefix} {number}"),
|
||||||
|
None => number,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Contact and shipping-address fields are mandatory (also enforced in the
|
||||||
|
// browser via `required`).
|
||||||
|
let require = |value: &str, field: &str| -> Result<String> {
|
||||||
|
trimmed(value).ok_or_else(|| Error::BadRequest(format!("{field} is required")))
|
||||||
|
};
|
||||||
|
let customer_name = require(&form.customer_name, "name")?;
|
||||||
|
let address = require(&form.address, "address")?;
|
||||||
|
let city = require(&form.city, "city")?;
|
||||||
|
let zip = require(&form.zip, "zip")?;
|
||||||
|
let country = require(&form.country, "country")?;
|
||||||
|
|
||||||
if !PAYMENT_METHODS.contains(&form.payment_method.as_str()) {
|
if !PAYMENT_METHODS.contains(&form.payment_method.as_str()) {
|
||||||
return Err(Error::BadRequest("invalid payment method".to_string()));
|
return Err(Error::BadRequest("invalid payment method".to_string()));
|
||||||
@@ -141,11 +162,12 @@ async fn place_order(
|
|||||||
&valid,
|
&valid,
|
||||||
orders::Checkout {
|
orders::Checkout {
|
||||||
email,
|
email,
|
||||||
customer_name: trimmed(&form.customer_name),
|
phone,
|
||||||
address: trimmed(&form.address),
|
customer_name: Some(customer_name),
|
||||||
city: trimmed(&form.city),
|
address: Some(address),
|
||||||
zip: trimmed(&form.zip),
|
city: Some(city),
|
||||||
country: trimmed(&form.country),
|
zip: Some(zip),
|
||||||
|
country: Some(country),
|
||||||
note: form.note.as_deref().and_then(trimmed),
|
note: form.note.as_deref().and_then(trimmed),
|
||||||
payment_method: form.payment_method,
|
payment_method: form.payment_method,
|
||||||
method,
|
method,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ pub struct ShipmentRequest<'a> {
|
|||||||
pub order_number: &'a str,
|
pub order_number: &'a str,
|
||||||
pub recipient_name: &'a str,
|
pub recipient_name: &'a str,
|
||||||
pub email: &'a str,
|
pub email: &'a str,
|
||||||
|
pub phone: Option<&'a str>,
|
||||||
pub address: Option<&'a str>,
|
pub address: Option<&'a str>,
|
||||||
pub city: Option<&'a str>,
|
pub city: Option<&'a str>,
|
||||||
pub zip: Option<&'a str>,
|
pub zip: Option<&'a str>,
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ pub async fn create_shipment(ctx: &AppContext, req: ShipmentRequest<'_>) -> Resu
|
|||||||
<name>{}</name>\
|
<name>{}</name>\
|
||||||
<surname>-</surname>\
|
<surname>-</surname>\
|
||||||
<email>{}</email>\
|
<email>{}</email>\
|
||||||
|
<phone>{}</phone>\
|
||||||
<addressId>{}</addressId>\
|
<addressId>{}</addressId>\
|
||||||
<value>{:.2}</value>\
|
<value>{:.2}</value>\
|
||||||
<cod>{:.2}</cod>\
|
<cod>{:.2}</cod>\
|
||||||
@@ -72,6 +73,7 @@ pub async fn create_shipment(ctx: &AppContext, req: ShipmentRequest<'_>) -> Resu
|
|||||||
xml_escape(req.order_number),
|
xml_escape(req.order_number),
|
||||||
xml_escape(req.recipient_name),
|
xml_escape(req.recipient_name),
|
||||||
xml_escape(req.email),
|
xml_escape(req.email),
|
||||||
|
xml_escape(req.phone.unwrap_or("")),
|
||||||
xml_escape(address_id),
|
xml_escape(address_id),
|
||||||
value,
|
value,
|
||||||
cod,
|
cod,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ pub struct Model {
|
|||||||
#[sea_orm(unique)]
|
#[sea_orm(unique)]
|
||||||
pub order_number: String,
|
pub order_number: String,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
|
pub phone: Option<String>,
|
||||||
pub customer_name: Option<String>,
|
pub customer_name: Option<String>,
|
||||||
pub status: String,
|
pub status: String,
|
||||||
pub total_cents: i64,
|
pub total_cents: i64,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ pub type Orders = Entity;
|
|||||||
/// database inside [`place`] so the customer cannot influence what they pay.
|
/// database inside [`place`] so the customer cannot influence what they pay.
|
||||||
pub struct Checkout {
|
pub struct Checkout {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
|
pub phone: String,
|
||||||
pub customer_name: Option<String>,
|
pub customer_name: Option<String>,
|
||||||
pub address: Option<String>,
|
pub address: Option<String>,
|
||||||
pub city: Option<String>,
|
pub city: Option<String>,
|
||||||
@@ -64,6 +65,7 @@ pub async fn place(ctx: &AppContext, items: &[(i32, i32)], details: Checkout) ->
|
|||||||
let order = ActiveModel {
|
let order = ActiveModel {
|
||||||
order_number: Set(generate_order_number()),
|
order_number: Set(generate_order_number()),
|
||||||
email: Set(details.email),
|
email: Set(details.email),
|
||||||
|
phone: Set(Some(details.phone)),
|
||||||
customer_name: Set(details.customer_name),
|
customer_name: Set(details.customer_name),
|
||||||
status: Set("pending".to_string()),
|
status: Set("pending".to_string()),
|
||||||
total_cents: Set(subtotal + details.method.price_cents),
|
total_cents: Set(subtotal + details.method.price_cents),
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ pub fn detail(order: &orders::Model, bank_iban: &str, bank_account_name: &str) -
|
|||||||
"id": order.id,
|
"id": order.id,
|
||||||
"order_number": order.order_number,
|
"order_number": order.order_number,
|
||||||
"email": order.email,
|
"email": order.email,
|
||||||
|
"phone": order.phone,
|
||||||
"customer_name": order.customer_name,
|
"customer_name": order.customer_name,
|
||||||
"status": order.status,
|
"status": order.status,
|
||||||
"subtotal": format_price(order.total_cents - order.shipping_cents),
|
"subtotal": format_price(order.total_cents - order.shipping_cents),
|
||||||
|
|||||||
Reference in New Issue
Block a user