update
This commit is contained in:
parent
ca24591d9d
commit
995a8b3476
16 changed files with 789 additions and 246 deletions
72
Cargo.lock
generated
72
Cargo.lock
generated
|
@ -26,6 +26,21 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.98"
|
||||
|
@ -377,12 +392,14 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"bardecoder",
|
||||
"base64",
|
||||
"chrono",
|
||||
"dioxus",
|
||||
"dioxus-material-icons",
|
||||
"dioxus-sdk",
|
||||
"gloo-timers",
|
||||
"image",
|
||||
"log",
|
||||
"qrc",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -425,6 +442,20 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium"
|
||||
version = "0.2.2"
|
||||
|
@ -2417,6 +2448,30 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.0.0"
|
||||
|
@ -3728,6 +3783,23 @@ dependencies = [
|
|||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qrc"
|
||||
version = "0.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f64ee6dc98fa28fec2e13ebcbe910530ef3b82a733853d48a7c98ffc0c534e"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"image",
|
||||
"qrcode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qrcode"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "166f136dfdb199f98186f3649cf7a0536534a61417a1a30221b492b4fb60ce3f"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
|
|
|
@ -22,6 +22,8 @@ dioxus-sdk = { version = "0.6.0", features = ["storage"] }
|
|||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
reqwest = { version = "0.12.15", features = ["json"] }
|
||||
dioxus-material-icons = "3.0.0"
|
||||
chrono = "0.4.41"
|
||||
qrc = "0.0.5"
|
||||
|
||||
[features]
|
||||
default = ["web"]
|
||||
|
|
3
build.rs
3
build.rs
|
@ -21,7 +21,4 @@ fn main() {
|
|||
if !status.success() {
|
||||
panic!("Tailwind build failed with exit code: {}", status);
|
||||
}
|
||||
|
||||
println!("cargo:rerun-if-changed={input}");
|
||||
println!("cargo:rerun-if-changed=tailwind.config.js");
|
||||
}
|
||||
|
|
54
src/api.rs
54
src/api.rs
|
@ -1,10 +1,11 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use dioxus::signals::Readable;
|
||||
use dioxus::signals::{Readable, Writable};
|
||||
use dioxus_sdk::storage::use_persistent;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::setup::Credentials;
|
||||
use crate::try_recover_api;
|
||||
|
||||
use reqwest::header::{HeaderMap, HeaderValue};
|
||||
use reqwest::Client;
|
||||
|
@ -12,11 +13,11 @@ use serde::de::DeserializeOwned;
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn get_item(item: &str) -> Option<Item> {
|
||||
crate::API
|
||||
.read()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_item(item.to_string())
|
||||
if let Some(api) = crate::API.read().as_ref() {
|
||||
api.get_item(item.to_string())
|
||||
} else {
|
||||
try_recover_api().get_item(item.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn api_get_auth<T>(path: String) -> Result<T, reqwest::Error>
|
||||
|
@ -88,8 +89,18 @@ impl API {
|
|||
let mut items: HashMap<String, Vec<Item>> =
|
||||
api_get_auth("/items".to_string()).await.unwrap();
|
||||
let items = items.remove("items").unwrap();
|
||||
let locations = api_get_auth("/locations".to_string()).await.unwrap();
|
||||
let flow_info = api_get_auth("/flows".to_string()).await.unwrap();
|
||||
let locations: HashMap<String, Location> =
|
||||
api_get_auth("/locations".to_string()).await.unwrap();
|
||||
let flow_info: HashMap<String, FlowInfo> =
|
||||
api_get_auth("/flows".to_string()).await.unwrap();
|
||||
|
||||
let mut p_items = use_persistent("api_items", || Vec::<Item>::new());
|
||||
p_items.set(items.clone());
|
||||
let mut p_locations =
|
||||
use_persistent("api_locations", || HashMap::<String, Location>::new());
|
||||
p_locations.set(locations.clone());
|
||||
let mut p_flowinfo = use_persistent("api_flow_info", || HashMap::<String, FlowInfo>::new());
|
||||
p_flowinfo.set(flow_info.clone());
|
||||
|
||||
Self {
|
||||
instance,
|
||||
|
@ -99,6 +110,24 @@ impl API {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn try_recover() -> Self {
|
||||
Self {
|
||||
instance: use_persistent("creds", || Credentials::default())
|
||||
.read()
|
||||
.instance_url
|
||||
.clone(),
|
||||
items: use_persistent("api_items", || Vec::<Item>::new())
|
||||
.read()
|
||||
.clone(),
|
||||
locations: use_persistent("api_locations", || HashMap::<String, Location>::new())
|
||||
.read()
|
||||
.clone(),
|
||||
flow_info: use_persistent("api_flow_info", || HashMap::<String, FlowInfo>::new())
|
||||
.read()
|
||||
.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_items(&self) -> &[Item] {
|
||||
&self.items
|
||||
}
|
||||
|
@ -358,7 +387,7 @@ impl API {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct FlowInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
|
@ -376,6 +405,13 @@ pub struct Item {
|
|||
pub variants: HashMap<String, ItemVariant>,
|
||||
}
|
||||
|
||||
impl Item {
|
||||
pub fn get_variant(&self, variant: &str) -> Option<ItemVariant> {
|
||||
let var = self.variants.iter().find(|x| *x.0 == variant)?;
|
||||
Some(var.1.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct ItemVariant {
|
||||
pub item: String,
|
||||
|
|
81
src/main.rs
81
src/main.rs
|
@ -24,16 +24,18 @@ enum Route {
|
|||
FlowPage {},
|
||||
#[route("/locations")]
|
||||
LocationsPage {},
|
||||
#[route("/location/:location")]
|
||||
LocationPage { location: String },
|
||||
|
||||
#[route("/item/:id")]
|
||||
ItemDetailPage { id: String },
|
||||
#[route("/transaction/:id")]
|
||||
TransactionPage { id: String },
|
||||
|
||||
#[end_layout]
|
||||
#[route("/item/:item/:variant/consume/:id")]
|
||||
ConsumePage { id: String, item: String, variant: String },
|
||||
|
||||
|
||||
#[route("/item/:item/supply?:param")]
|
||||
SupplyPage { item: String, param: SupplyPageParam }
|
||||
// #[route("/blog/:id")]
|
||||
|
@ -54,16 +56,26 @@ fn main() {
|
|||
dioxus::launch(App);
|
||||
}
|
||||
|
||||
pub fn try_recover_api() -> crate::api::API {
|
||||
api::API::try_recover()
|
||||
}
|
||||
|
||||
pub async fn fetch_api() {
|
||||
let res = use_persistent("creds", || Credentials::default())
|
||||
.read()
|
||||
.clone();
|
||||
if !res.empty() {
|
||||
let api = api::API::new(res.instance_url.clone()).await;
|
||||
*crate::API.write() = Some(api);
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App() -> Element {
|
||||
let creds = use_persistent("creds", || Credentials::default());
|
||||
|
||||
spawn(async move {
|
||||
let res = use_persistent("creds", || Credentials::default());
|
||||
if !res.read().empty() {
|
||||
let api = api::API::new(res.read().instance_url.clone()).await;
|
||||
*crate::API.write() = Some(api);
|
||||
}
|
||||
fetch_api().await;
|
||||
});
|
||||
|
||||
rsx! {
|
||||
|
@ -84,9 +96,21 @@ fn App() -> Element {
|
|||
fn ItemTile(item: Item) -> Element {
|
||||
rsx! {
|
||||
Link {
|
||||
to: Route::ItemDetailPage { id: item.uuid },
|
||||
{item.name.as_str()}
|
||||
to: Route::ItemDetailPage { id: item.uuid },
|
||||
class: "flex gap-2",
|
||||
if let Some(img) = item.image {
|
||||
img { src: crate::API.read().as_ref().unwrap().get_url_instance(img.to_string()), width: "48" }
|
||||
} else {
|
||||
MaterialIcon {
|
||||
name: "view_in_ar",
|
||||
size: 48
|
||||
}
|
||||
}
|
||||
p {
|
||||
class: "text-lg my-auto",
|
||||
{item.name.as_str()}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -137,47 +161,42 @@ fn TransactionCard(t: Transaction) -> Element {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared navbar component.
|
||||
#[component]
|
||||
fn Navbar() -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
id: "navbar",
|
||||
class: "flex flex-wrap gap-2 p-2 shadow-md rounded-sm",
|
||||
style: "background-color:hsl(223, 18.90%, 13.30%);",
|
||||
Link {
|
||||
to: Route::Home {},
|
||||
div {
|
||||
class: "flex flex-row gap-2 p-2",
|
||||
MaterialIcon { name: "home", size: 24 },
|
||||
"Home"
|
||||
}
|
||||
class: "flex items-center gap-2 px-3 py-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer text-gray-900 dark:text-gray-100",
|
||||
MaterialIcon { name: "home", size: 24 },
|
||||
"Home"
|
||||
}
|
||||
Link {
|
||||
to: Route::ItemPage { },
|
||||
div {
|
||||
class: "flex flex-row gap-2 p-2",
|
||||
MaterialIcon { name: "category", size: 24 },
|
||||
"Items"
|
||||
}
|
||||
class: "flex items-center gap-2 px-3 py-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer text-gray-900 dark:text-gray-100",
|
||||
MaterialIcon { name: "category", size: 24 },
|
||||
"Items"
|
||||
}
|
||||
Link {
|
||||
to: Route::FlowPage { },
|
||||
div {
|
||||
class: "flex flex-row gap-2 p-2",
|
||||
MaterialIcon { name: "assignment", size: 24 },
|
||||
"Flows"
|
||||
}
|
||||
class: "flex items-center gap-2 px-3 py-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer text-gray-900 dark:text-gray-100",
|
||||
MaterialIcon { name: "assignment", size: 24 },
|
||||
"Flows"
|
||||
}
|
||||
Link {
|
||||
to: Route::LocationsPage { },
|
||||
div {
|
||||
class: "flex flex-row gap-2 p-2",
|
||||
MaterialIcon { name: "map", size: 24 },
|
||||
"Locations"
|
||||
}
|
||||
class: "flex items-center gap-2 px-3 py-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer text-gray-900 dark:text-gray-100",
|
||||
MaterialIcon { name: "map", size: 24 },
|
||||
"Locations"
|
||||
}
|
||||
}
|
||||
|
||||
Outlet::<Route> {}
|
||||
div {
|
||||
class: "py-4",
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
90
src/page/components.rs
Normal file
90
src/page/components.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
use chrono::{DateTime, Local, NaiveDateTime, Utc};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::page::supply::CloseXButton;
|
||||
|
||||
#[component]
|
||||
pub fn HeaderTitle(title: String) -> Element {
|
||||
rsx! {
|
||||
h1 {
|
||||
class: "text-3xl font-bold mb-6",
|
||||
{title}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TransientHeader(title: String) -> Element {
|
||||
rsx! {
|
||||
h1 {
|
||||
class: "flex text-2xl font-semibold border-b border-neutral-200 dark:border-neutral-700 pb-4",
|
||||
CloseXButton { }
|
||||
{title}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn BasicButton(title: String, onclick: EventHandler<MouseEvent>) -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
class: "mt-4",
|
||||
button {
|
||||
class: "\
|
||||
inline-flex items-center gap-2 px-4 py-2 rounded-md \
|
||||
bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 \
|
||||
text-white text-sm font-medium shadow-sm hover:shadow-md transition-all duration-200 cursor-pointer",
|
||||
onclick: onclick,
|
||||
{title}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn LabeledInput(label: String, children: Element) -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
class: "flex flex-col gap-1",
|
||||
label { class: "text-sm font-medium", {label} }
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_human_date(unix_ts: i64) -> String {
|
||||
let naive = NaiveDateTime::from_timestamp(unix_ts, 0);
|
||||
let datetime_utc: DateTime<Utc> = DateTime::from_utc(naive, Utc);
|
||||
let datetime_local: DateTime<Local> = DateTime::from(datetime_utc);
|
||||
|
||||
let now = Local::now();
|
||||
let duration = now.signed_duration_since(datetime_local);
|
||||
|
||||
// Format the date part
|
||||
let date_str = datetime_local.format("%Y-%m-%d %H:%M").to_string();
|
||||
|
||||
// Format the delta part
|
||||
let ago_str = if duration.num_seconds() < 60 {
|
||||
"just now".to_string()
|
||||
} else if duration.num_minutes() < 60 {
|
||||
format!(
|
||||
"{} minute{} ago",
|
||||
duration.num_minutes(),
|
||||
if duration.num_minutes() != 1 { "s" } else { "" }
|
||||
)
|
||||
} else if duration.num_hours() < 24 {
|
||||
format!(
|
||||
"{} hour{} ago",
|
||||
duration.num_hours(),
|
||||
if duration.num_hours() != 1 { "s" } else { "" }
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{} day{} ago",
|
||||
duration.num_days(),
|
||||
if duration.num_days() != 1 { "s" } else { "" }
|
||||
)
|
||||
};
|
||||
|
||||
format!("{} ({})", date_str, ago_str)
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
use crate::api;
|
||||
use crate::{
|
||||
api,
|
||||
page::{BasicButton, LabeledInput, TransientHeader},
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn ConsumePage(id: String, item: String, variant: String) -> Element {
|
||||
|
@ -14,43 +17,56 @@ pub fn ConsumePage(id: String, item: String, variant: String) -> Element {
|
|||
let mut price = use_signal(|| 0.00);
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "w-full py-4 flex flex-col gap-6",
|
||||
|
||||
p { "Item: {item}"},
|
||||
p { "Variant: {variant}"}
|
||||
TransientHeader { title: format!("Consume {item} - {variant}") }
|
||||
|
||||
// TODO : destination
|
||||
|
||||
LabeledInput {
|
||||
label: "Destination",
|
||||
PredefinedSelector {
|
||||
name: "Destination",
|
||||
value: dest,
|
||||
custom: true,
|
||||
predefined: destinations
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
"Price: ",
|
||||
// Price Field
|
||||
LabeledInput {
|
||||
label: "Price",
|
||||
input {
|
||||
class: "rounded-md border border-neutral-300 dark:border-neutral-700 bg-white dark:bg-neutral-800 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400",
|
||||
r#type: "number",
|
||||
value: "{price}",
|
||||
oninput: move |e| price.set(e.value().parse().unwrap())
|
||||
oninput: move |e| {
|
||||
price.set(e.value().parse().unwrap())
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
BasicButton {
|
||||
title: "Consume",
|
||||
|
||||
button {
|
||||
onclick: move |_| {
|
||||
let id2 = id.clone();
|
||||
spawn(async move {
|
||||
api::API::consume_item(id2, dest(), price()).await;
|
||||
navigator().go_back();
|
||||
});
|
||||
},
|
||||
"Consume"
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn PredefinedSelector(
|
||||
name: String,
|
||||
value: Signal<String>,
|
||||
custom: bool,
|
||||
predefined: Resource<Vec<String>>,
|
||||
) -> Element {
|
||||
let mut screen_visible = use_signal(|| false);
|
||||
|
@ -60,32 +76,46 @@ pub fn PredefinedSelector(
|
|||
div {
|
||||
class: "p-2",
|
||||
button {
|
||||
class: "cursor-pointer underline text-blue-600",
|
||||
class: "\
|
||||
cursor-pointer rounded-lg border border-neutral-300 dark:border-neutral-700 \
|
||||
px-3 py-1 text-md font-medium text-neutral-700 dark:text-neutral-300 \
|
||||
hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors duration-200",
|
||||
onclick: move |_| screen_visible.set(true),
|
||||
"{name}: {value}"
|
||||
}
|
||||
}
|
||||
|
||||
if *screen_visible.read() {
|
||||
div {
|
||||
class: "fixed inset-0 z-40 flex flex-col p-4 space-y-4 text-white",
|
||||
style: "background-color: #0f1116;",
|
||||
if *screen_visible.read() {
|
||||
div {
|
||||
class: "\
|
||||
fixed inset-0 z-40 flex flex-col bg-black bg-opacity-80 p-6 space-y-6 text-white \
|
||||
backdrop-blur-sm",
|
||||
|
||||
h2 { class: "text-xl font-bold", "Select a value for {name}" }
|
||||
h2 {
|
||||
class: "text-2xl font-semibold",
|
||||
"Select a value for {name}"
|
||||
}
|
||||
|
||||
if custom {
|
||||
input {
|
||||
class: "border p-2 text-lg",
|
||||
class: "\
|
||||
rounded-md border border-neutral-600 bg-neutral-900 px-4 py-2 text-lg \
|
||||
text-white focus:outline-none focus:ring-2 focus:ring-blue-500 transition",
|
||||
r#type: "text",
|
||||
value: "{input_value}",
|
||||
oninput: move |evt| input_value.set(evt.value().clone()),
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
class: "flex flex-wrap gap-2",
|
||||
match &*predefined.read() {
|
||||
Some(list) => rsx ! { {list.iter().map(|item| rsx! {
|
||||
div {
|
||||
class: "flex flex-wrap gap-3",
|
||||
match &*predefined.read() {
|
||||
Some(list) => rsx! {
|
||||
{list.iter().map(|item| rsx! {
|
||||
button {
|
||||
class: "bg-gray-200 px-3 py-1 rounded hover:bg-gray-300",
|
||||
class: "\
|
||||
rounded-md bg-neutral-700 px-4 py-2 text-sm hover:bg-neutral-600 \
|
||||
transition-colors cursor-pointer select-none",
|
||||
onclick: {
|
||||
let item = item.clone();
|
||||
move |_| {
|
||||
|
@ -96,13 +126,18 @@ pub fn PredefinedSelector(
|
|||
},
|
||||
"{item}"
|
||||
}
|
||||
})}},
|
||||
None => rsx!(p { "Loading..." }),
|
||||
}
|
||||
})}
|
||||
},
|
||||
None => rsx!(p { "Loading..." }),
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
class: "mt-auto flex gap-4",
|
||||
button {
|
||||
class: "mt-auto bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600",
|
||||
class: "\
|
||||
flex-1 bg-blue-600 hover:bg-blue-700 rounded-md px-5 py-2 text-white font-semibold \
|
||||
transition-colors cursor-pointer",
|
||||
onclick: move |_| {
|
||||
value.set(input_value.read().clone());
|
||||
screen_visible.set(false);
|
||||
|
@ -111,11 +146,14 @@ pub fn PredefinedSelector(
|
|||
}
|
||||
|
||||
button {
|
||||
class: "text-gray-500 underline",
|
||||
class: "\
|
||||
flex-1 text-center underline text-neutral-400 hover:text-neutral-200 \
|
||||
transition-colors cursor-pointer",
|
||||
onclick: move |_| screen_visible.set(false),
|
||||
"Cancel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
132
src/page/home.rs
132
src/page/home.rs
|
@ -1,6 +1,7 @@
|
|||
use dioxus::prelude::*;
|
||||
use dioxus_material_icons::MaterialIcon;
|
||||
|
||||
use crate::TransactionCard;
|
||||
use crate::{page::HeaderTitle, TransactionCard};
|
||||
|
||||
/// Home Page
|
||||
#[component]
|
||||
|
@ -10,42 +11,119 @@ pub fn Home() -> Element {
|
|||
let global = use_resource(move || async move { crate::api::API::get_global_item_stat().await });
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
HeaderTitle { title: "Home" }
|
||||
|
||||
div {
|
||||
h1 { "Home" },
|
||||
div {
|
||||
id: "column",
|
||||
id: "column",
|
||||
class: "space-y-6",
|
||||
|
||||
match &*global.read_unchecked() {
|
||||
Some(resp) => rsx! {
|
||||
match &*global.read_unchecked() {
|
||||
Some(resp) => rsx! {
|
||||
div {
|
||||
id: "card",
|
||||
class: "\
|
||||
w-full flex items-center justify-between bg-white dark:bg-neutral-800 \
|
||||
shadow-md rounded-xl p-5",
|
||||
|
||||
// Items
|
||||
div {
|
||||
id: "card",
|
||||
p { {format!("Items: {}", resp.item_count)} }
|
||||
p { {format!("Inventory: {}", resp.total_transactions)} }
|
||||
p { {format!("Price: {}", resp.total_price)} }
|
||||
}
|
||||
},
|
||||
None => rsx! {
|
||||
div { "Loading dogs..." }
|
||||
},
|
||||
class: "flex items-center gap-3",
|
||||
MaterialIcon {
|
||||
name: "inventory_2",
|
||||
size: 24
|
||||
},
|
||||
p {
|
||||
class: "text-md font-medium",
|
||||
{format!("Items: {}", resp.item_count)}
|
||||
}
|
||||
},
|
||||
|
||||
// Inventory
|
||||
div {
|
||||
class: "flex items-center gap-3",
|
||||
MaterialIcon {
|
||||
name: "storage",
|
||||
size: 24
|
||||
},
|
||||
p {
|
||||
class: "text-md font-medium",
|
||||
{format!("Inventory: {}", resp.total_transactions)}
|
||||
}
|
||||
},
|
||||
|
||||
// Price
|
||||
div {
|
||||
class: "flex items-center gap-3",
|
||||
MaterialIcon {
|
||||
name: "attach_money",
|
||||
size: 24
|
||||
},
|
||||
p {
|
||||
class: "text-md font-medium",
|
||||
{format!("Price: ${:.2}", resp.total_price)}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
None => rsx! {
|
||||
div {
|
||||
class: "text-center text-gray-500 dark:text-gray-400",
|
||||
"Loading dogs..."
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
if let Some(min) = &*min.read_unchecked() {
|
||||
if !min.is_empty() {
|
||||
h1 { "Items under Minimum" },
|
||||
for item in min {
|
||||
p { {format!("{} under minimum. Needs {} more.", item.item_variant, item.need)} }
|
||||
}
|
||||
if let Some(min) = &*min.read_unchecked() {
|
||||
if !min.is_empty() {
|
||||
h2 {
|
||||
class: "text-2xl font-semibold mt-8 mb-4",
|
||||
"Items under Minimum"
|
||||
},
|
||||
div {
|
||||
class: "space-y-3",
|
||||
for item in min {
|
||||
div {
|
||||
class: "\
|
||||
w-full flex items-center justify-between bg-white dark:bg-gray-800 \
|
||||
shadow rounded-lg px-4 py-3",
|
||||
|
||||
div {
|
||||
class: "flex items-center gap-2",
|
||||
MaterialIcon {
|
||||
name: "warning",
|
||||
size: 20
|
||||
},
|
||||
p {
|
||||
class: "font-medium",
|
||||
{format!("{} under minimum", item.item_variant)}
|
||||
}
|
||||
},
|
||||
|
||||
p {
|
||||
class: "text-sm text-gray-600 dark:text-gray-400",
|
||||
{format!("Needs {} more.", item.need)}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(expired) = &*expired.read_unchecked() {
|
||||
if !expired.is_empty() {
|
||||
h1 { "Expired Items" },
|
||||
for item in expired {
|
||||
TransactionCard { t: item.clone() }
|
||||
if let Some(expired) = &*expired.read_unchecked() {
|
||||
if !expired.is_empty() {
|
||||
h2 {
|
||||
class: "text-2xl font-semibold mt-8 mb-4",
|
||||
"Expired Items"
|
||||
},
|
||||
div {
|
||||
class: "space-y-4",
|
||||
for item in expired {
|
||||
TransactionCard { t: item.clone() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use dioxus::prelude::*;
|
||||
use dioxus_material_icons::MaterialIcon;
|
||||
|
||||
use crate::{page::supply::SupplyPageParam, Route, TransactionCard};
|
||||
|
||||
|
@ -13,14 +14,14 @@ pub fn ItemDetailPage(id: String) -> Element {
|
|||
|
||||
rsx! {
|
||||
div {
|
||||
class: "flex flex-col h-screen",
|
||||
class: "p-4 flex flex-col h-screen",
|
||||
|
||||
header {
|
||||
class: "p-4 text-white text-lg font-bold",
|
||||
class: "text-white text-lg font-bold",
|
||||
{item.name.as_str()}
|
||||
}
|
||||
div {
|
||||
class: "p-6 flex flex-col space-y-4",
|
||||
class: "flex flex-col space-y-4",
|
||||
div {
|
||||
class: "flex items-start space-x-4",
|
||||
if let Some(image) = &item.image {
|
||||
|
@ -38,6 +39,23 @@ pub fn ItemDetailPage(id: String) -> Element {
|
|||
}
|
||||
}
|
||||
|
||||
EventButton {
|
||||
icon: "pallet",
|
||||
title: "Supply",
|
||||
onclick: move |_| {
|
||||
navigator().push(
|
||||
Route::SupplyPage {
|
||||
item: item.uuid.clone(),
|
||||
param: SupplyPageParam {
|
||||
only_variants: None,
|
||||
force_price: None,
|
||||
force_origin: None,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
class: "grid grid-cols-2 gap-4",
|
||||
{item.variants.iter().map(|(key, variant)| {
|
||||
|
@ -72,22 +90,30 @@ pub fn ItemDetailPage(id: String) -> Element {
|
|||
}
|
||||
}
|
||||
|
||||
button {
|
||||
class: "fixed bottom-4 right-4 p-4 bg-green-500 text-white rounded-full shadow-lg",
|
||||
onclick: move |_| {
|
||||
navigator().push(
|
||||
Route::SupplyPage {
|
||||
item: item.uuid.clone(),
|
||||
param: SupplyPageParam {
|
||||
only_variants: None,
|
||||
force_price: None,
|
||||
force_origin: None
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
"+"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn EventButton(icon: String, title: String, onclick: EventHandler<MouseEvent>) -> Element {
|
||||
rsx! {
|
||||
button {
|
||||
class: "\
|
||||
flex items-center gap-2 px-3 py-1.5 rounded-md \
|
||||
text-sm font-medium \
|
||||
border border-neutral-300 dark:border-neutral-700 \
|
||||
text-neutral-800 dark:text-neutral-200 \
|
||||
bg-white dark:bg-neutral-900 \
|
||||
hover:bg-neutral-100 dark:hover:bg-neutral-800 \
|
||||
hover:shadow-sm \
|
||||
transition-all duration-200 cursor-pointer w-fit",
|
||||
onclick: onclick,
|
||||
MaterialIcon {
|
||||
name: icon,
|
||||
size: 20
|
||||
},
|
||||
{title}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
use crate::ItemTile;
|
||||
use crate::{
|
||||
page::{item_detail::EventButton, HeaderTitle},
|
||||
ItemTile,
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn ItemPage() -> Element {
|
||||
|
@ -13,31 +16,32 @@ pub fn ItemPage() -> Element {
|
|||
});
|
||||
|
||||
rsx! {
|
||||
h1 { "Items" },
|
||||
// barcode scan button
|
||||
HeaderTitle { title: "Items" }
|
||||
|
||||
match &*items.read_unchecked() {
|
||||
Some(items) => {
|
||||
rsx! {
|
||||
div {
|
||||
class: "pb-4",
|
||||
EventButton { icon: "qr_code_2", title: "Scan Transaction QR", onclick: move |_| {} }
|
||||
}
|
||||
|
||||
// barcode scan button
|
||||
|
||||
match &*items.read_unchecked() {
|
||||
Some(items) => rsx! {
|
||||
div {
|
||||
class: "flex flex-col divide-y divide-gray-200 dark:divide-gray-700",
|
||||
for item in items {
|
||||
div {
|
||||
class: "flex flex-col",
|
||||
for item in items {
|
||||
ItemTile { item: item.clone() }
|
||||
}
|
||||
class: "py-3 px-4 hover:bg-gray-100 dark:hover:bg-gray-800 transition cursor-pointer",
|
||||
ItemTile { item: item.clone() }
|
||||
}
|
||||
}
|
||||
},
|
||||
None => { rsx! { p { "Loading" }}}
|
||||
}
|
||||
},
|
||||
None => rsx! {
|
||||
p { "Loading" }
|
||||
}
|
||||
|
||||
|
||||
button {
|
||||
onclick: move |_| {
|
||||
// TODO : scan transaction qr
|
||||
},
|
||||
"Scan QR Transaction"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use dioxus::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{page::HeaderTitle, Route};
|
||||
|
||||
#[component]
|
||||
pub fn LocationsPage() -> Element {
|
||||
let api = crate::API.read();
|
||||
|
@ -9,7 +11,7 @@ pub fn LocationsPage() -> Element {
|
|||
let roots: Vec<String> = get_root_locations(&*locations.read());
|
||||
|
||||
rsx! {
|
||||
h1 { "Locations" },
|
||||
HeaderTitle { title: "Locations" },
|
||||
|
||||
for l in &roots {
|
||||
LocationTreeTile { location: l.clone(), locations }
|
||||
|
@ -53,19 +55,39 @@ pub fn LocationTreeTile(
|
|||
) -> Element {
|
||||
let mut open = use_signal(|| false);
|
||||
|
||||
// TODO
|
||||
let children = get_children_of_loc(location.clone(), &*locations.read());
|
||||
|
||||
rsx! {
|
||||
p { {location} }
|
||||
button {
|
||||
onclick: move |_| { open.set(true); },
|
||||
"Expand"
|
||||
}
|
||||
let is_open = *open.read();
|
||||
let toggle_icon = if is_open { "▼" } else { "▶" };
|
||||
|
||||
if *open.read() {
|
||||
for loc in &children {
|
||||
LocationTreeTile { location: loc.clone(), locations }
|
||||
rsx! {
|
||||
div {
|
||||
class: "w-full",
|
||||
div {
|
||||
class: "flex items-center justify-start p-2 gap-2 rounded cursor-pointer transition",
|
||||
onclick: move |_| {
|
||||
open.set(!is_open);
|
||||
},
|
||||
if children.len() != 0 {
|
||||
span {
|
||||
class: "text-sm",
|
||||
"{toggle_icon}"
|
||||
}
|
||||
}
|
||||
Link {
|
||||
class: "text-sm font-medium",
|
||||
to: Route::LocationPage { location: location.clone() },
|
||||
"{location}"
|
||||
}
|
||||
}
|
||||
|
||||
if is_open {
|
||||
div {
|
||||
class: "ml-4 mt-1 space-y-1",
|
||||
for loc in &children {
|
||||
LocationTreeTile { location: loc.clone(), locations }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
pub mod components;
|
||||
pub mod consume;
|
||||
pub mod home;
|
||||
pub mod item_detail;
|
||||
|
@ -6,10 +7,11 @@ pub mod locations;
|
|||
pub mod supply;
|
||||
pub mod transaction;
|
||||
|
||||
pub use components::*;
|
||||
pub use consume::ConsumePage;
|
||||
pub use home::Home;
|
||||
pub use item_detail::ItemDetailPage;
|
||||
pub use items::ItemPage;
|
||||
pub use locations::LocationsPage;
|
||||
pub use locations::*;
|
||||
pub use supply::SupplyPage;
|
||||
pub use transaction::TransactionPage;
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_material_icons::MaterialIcon;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{api, page::consume::PredefinedSelector, Route};
|
||||
use crate::{
|
||||
api::{self, get_item},
|
||||
page::{consume::PredefinedSelector, BasicButton, LabeledInput, TransientHeader},
|
||||
Route,
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn SupplyPage(item: String, param: SupplyPageParam) -> Element {
|
||||
|
@ -48,28 +53,38 @@ pub fn SupplyPage(item: String, param: SupplyPageParam) -> Element {
|
|||
let mut state = use_signal(|| String::new());
|
||||
|
||||
rsx! {
|
||||
h1 { "Add new Item" },
|
||||
div {
|
||||
class: "w-full py-4 flex flex-col gap-6",
|
||||
|
||||
p { {state} },
|
||||
TransientHeader { title: format!("Add new {}", get_item(&*item.read()).unwrap().name) }
|
||||
|
||||
// Variant Selection
|
||||
PredefinedSelector {
|
||||
name: "Variant",
|
||||
value: variant_id,
|
||||
predefined: variants_str
|
||||
}
|
||||
// Variant Selection
|
||||
LabeledInput {
|
||||
label: "Variant",
|
||||
PredefinedSelector {
|
||||
name: "Variant",
|
||||
value: variant_id,
|
||||
custom: false,
|
||||
predefined: variants_str
|
||||
}
|
||||
}
|
||||
|
||||
// Origin Field with Dropdown and Text Input
|
||||
PredefinedSelector {
|
||||
name: "Origin",
|
||||
value: origin,
|
||||
predefined: origins
|
||||
}
|
||||
// Origin Selection
|
||||
LabeledInput {
|
||||
label: "Origin",
|
||||
PredefinedSelector {
|
||||
name: "Origin",
|
||||
value: origin,
|
||||
custom: true,
|
||||
predefined: origins
|
||||
}
|
||||
}
|
||||
|
||||
// Price Field
|
||||
label {
|
||||
"Price: ",
|
||||
// Price Field
|
||||
LabeledInput {
|
||||
label: "Price",
|
||||
input {
|
||||
class: "rounded-md border border-neutral-300 dark:border-neutral-700 bg-white dark:bg-neutral-800 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400",
|
||||
r#type: "number",
|
||||
value: "{price}",
|
||||
disabled: param.force_price.is_some(),
|
||||
|
@ -81,34 +96,49 @@ pub fn SupplyPage(item: String, param: SupplyPageParam) -> Element {
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// Location Dropdown
|
||||
}
|
||||
|
||||
// Note
|
||||
label {
|
||||
"Note: ",
|
||||
// Note Field
|
||||
LabeledInput {
|
||||
label: "Note",
|
||||
input {
|
||||
class: "rounded-md border border-neutral-300 dark:border-neutral-700 bg-white dark:bg-neutral-800 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400",
|
||||
r#type: "text",
|
||||
value: "{note}",
|
||||
oninput: move |e| note.set(e.value())
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Submit Button
|
||||
|
||||
button {
|
||||
onclick: move |_| {
|
||||
spawn(async move {
|
||||
let variant = variants.iter().find(|x| x.name == variant_id()).unwrap().variant.clone();
|
||||
let s = format!("{} {} {} {} {} {}", item(), variant, price(), origin(), location(), note());
|
||||
state.set(s);
|
||||
let tid = crate::api::API::supply_item(item(), variant, price(), Some(origin()), Some(location()), Some(note())).await;
|
||||
navigator().replace(Route::TransactionPage { id: tid });
|
||||
});
|
||||
},
|
||||
"Create"
|
||||
}
|
||||
BasicButton {
|
||||
onclick: move |_| {
|
||||
spawn(async move {
|
||||
let variant = variants.iter().find(|x| x.name == variant_id()).unwrap().variant.clone();
|
||||
let s = format!("{} {} {} {} {} {}", item(), variant, price(), origin(), location(), note());
|
||||
state.set(s);
|
||||
let tid = crate::api::API::supply_item(item(), variant, price(), Some(origin()), Some(location()), Some(note())).await;
|
||||
navigator().replace(Route::TransactionPage { id: tid });
|
||||
});
|
||||
},
|
||||
title: "Create"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CloseXButton() -> Element {
|
||||
rsx! {
|
||||
button {
|
||||
class: "\
|
||||
relative mx-4 flex items-center justify-center w-8 h-8 \
|
||||
rounded-full border border-neutral-300 dark:border-neutral-700 \
|
||||
text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-700 \
|
||||
transition-colors duration-200 cursor-pointer",
|
||||
onclick: move |_| {
|
||||
navigator().go_back();
|
||||
},
|
||||
MaterialIcon { name: "arrow_back", size: 24 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
use dioxus::prelude::*;
|
||||
use dioxus_material_icons::MaterialIcon;
|
||||
use qrc::QRCode;
|
||||
|
||||
use crate::{api::get_item, Route};
|
||||
use crate::{
|
||||
api::get_item,
|
||||
fetch_api,
|
||||
page::{item_detail::EventButton, to_human_date},
|
||||
qrscan::image_buffer_to_data_url,
|
||||
Route,
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn TransactionPage(id: String) -> Element {
|
||||
|
@ -10,70 +17,164 @@ pub fn TransactionPage(id: String) -> Element {
|
|||
crate::api::API::get_transaction(id.read().to_string()).await
|
||||
});
|
||||
|
||||
rsx! {
|
||||
match transaction.value()() {
|
||||
Some(transaction) => rsx! {
|
||||
let qr = QRCode::from_string(id.read().clone()).to_png(512);
|
||||
let qr_code_src = image_buffer_to_data_url(&qr).unwrap();
|
||||
|
||||
button {
|
||||
onclick: move |_| {
|
||||
navigator().push(Route::ConsumePage { id: transaction.uuid.clone(), item: transaction.item.clone(), variant: transaction.variant.clone() });
|
||||
},
|
||||
"Consume"
|
||||
rsx! {
|
||||
match transaction.value()() {
|
||||
Some(transaction) => rsx! {
|
||||
div {
|
||||
class: "max-w-4xl mx-auto p-4 space-y-6",
|
||||
|
||||
// HEADER CARD
|
||||
div {
|
||||
class: "flex flex-row items-center space-x-6 p-4 rounded-lg bg-white dark:bg-gray-800 shadow-md",
|
||||
|
||||
img {
|
||||
class: "w-24 h-24 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 shadow-md",
|
||||
src: qr_code_src,
|
||||
alt: "Transaction QR code"
|
||||
}
|
||||
|
||||
div {
|
||||
class: "flex flex-col space-y-1",
|
||||
|
||||
p { class: "text-xl font-semibold text-gray-900 dark:text-gray-100", "{get_item(&transaction.item).unwrap().name}" }
|
||||
p { class: "text-md text-gray-600 dark:text-gray-300", "{get_item(&transaction.item).unwrap().get_variant(&transaction.variant).unwrap().name}" }
|
||||
p { class: "text-sm text-gray-500 dark:text-gray-400", {to_human_date(transaction.timestamp)} }
|
||||
if transaction.expired && transaction.consumed.is_none() {
|
||||
span {
|
||||
class: "inline-block bg-red-100 text-red-700 text-xs font-semibold px-2 py-1 rounded-full uppercase tracking-wide",
|
||||
"Expired"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
div {
|
||||
id: "column",
|
||||
// CONSUME INFO CARD or Consume Button
|
||||
if let Some(consumed) = transaction.consumed.as_ref() {
|
||||
div {
|
||||
class: "p-4 rounded-lg bg-blue-50 dark:bg-blue-900 shadow-md flex flex-col space-y-3",
|
||||
|
||||
div {
|
||||
id: "row",
|
||||
h3 { "Consumed" }
|
||||
|
||||
// TODO : transaction qr code
|
||||
IconLabel {
|
||||
icon: "location_on".to_string(),
|
||||
label: "Destination".to_string(),
|
||||
value: consumed.destination.clone()
|
||||
}
|
||||
IconLabel {
|
||||
icon: "attach_money".to_string(),
|
||||
label: "Price".to_string(),
|
||||
value: format!("{:.2}", consumed.price)
|
||||
}
|
||||
IconLabel {
|
||||
icon: "schedule".to_string(),
|
||||
label: "Consumed At".to_string(),
|
||||
value: to_human_date(consumed.timestamp)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
EventButton {
|
||||
icon: "check_circle".to_string(),
|
||||
title: "Consume".to_string(),
|
||||
onclick: move |_| {
|
||||
navigator().push(Route::ConsumePage {
|
||||
id: transaction.uuid.clone(),
|
||||
item: transaction.item.clone(),
|
||||
variant: transaction.variant.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// OTHER INFO LIST
|
||||
div {
|
||||
id: "col-1",
|
||||
class: "space-y-3",
|
||||
|
||||
p { {get_item(&transaction.item).unwrap().name.as_str()} }
|
||||
p { {get_item(&transaction.item).unwrap().variants.get(&transaction.variant).as_ref().unwrap().name.clone()} }
|
||||
p { {transaction.timestamp.to_string()} }
|
||||
IconLabel {
|
||||
icon: "attach_money".to_string(),
|
||||
label: "Price".to_string(),
|
||||
value: format!("{:.2}", transaction.price)
|
||||
}
|
||||
|
||||
if let Some(origin) = transaction.origin.as_ref() {
|
||||
IconLabel {
|
||||
icon: "home".to_string(),
|
||||
label: "Origin".to_string(),
|
||||
value: origin.clone()
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(location) = transaction.location.as_ref() {
|
||||
IconLabel {
|
||||
icon: "place".to_string(),
|
||||
label: "Location".to_string(),
|
||||
value: location.name.clone()
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(note) = transaction.note.as_ref() {
|
||||
if !note.is_empty() {
|
||||
IconLabel {
|
||||
icon: "note".to_string(),
|
||||
label: "Note".to_string(),
|
||||
value: note.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(quanta) = transaction.quanta {
|
||||
IconLabel {
|
||||
icon: "inventory_2".to_string(),
|
||||
label: "Quantity".to_string(),
|
||||
value: quanta.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(properties) = transaction.properties.as_ref() {
|
||||
IconLabel {
|
||||
icon: "info".to_string(),
|
||||
label: "Properties".to_string(),
|
||||
value: serde_json::to_string_pretty(properties).unwrap_or_else(|_| "{}".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// TODO : expired notify
|
||||
|
||||
// icon texts
|
||||
|
||||
// price
|
||||
p { {transaction.price.to_string()} },
|
||||
// origin
|
||||
if let Some(origin) = &transaction.origin {
|
||||
IconLabel { icon: "home".to_string(), label: "Origin".to_owned(), value: origin.clone() }
|
||||
}
|
||||
// location
|
||||
// note
|
||||
|
||||
|
||||
// consume info
|
||||
},
|
||||
None => rsx! {
|
||||
p {
|
||||
class: "text-center text-gray-500 dark:text-gray-400 p-6",
|
||||
"Loading transaction..."
|
||||
}
|
||||
}
|
||||
},
|
||||
None => rsx! { p { "Loading..." }}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn IconLabel(icon: String, label: String, value: String) -> Element {
|
||||
rsx! {
|
||||
div { class: "flex items-center space-x-3",
|
||||
MaterialIcon {
|
||||
name: icon,
|
||||
size: 12,
|
||||
},
|
||||
div {
|
||||
class: "flex items-center space-x-4 p-2 rounded-md bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors duration-200 shadow-sm",
|
||||
|
||||
div { class: "flex flex-col",
|
||||
span { class: "text-sm font-medium text-gray-700", "{label}" }
|
||||
span { class: "text-lg font-semibold text-gray-900", "{value}" }
|
||||
MaterialIcon {
|
||||
name: icon,
|
||||
size: 24,
|
||||
},
|
||||
|
||||
div {
|
||||
class: "flex flex-col",
|
||||
|
||||
span {
|
||||
class: "text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400",
|
||||
"{label}"
|
||||
},
|
||||
|
||||
span {
|
||||
class: "text-base font-bold text-gray-900 dark:text-gray-100",
|
||||
"{value}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -134,7 +134,7 @@ fn stop_camera_stream(media_stream: &web_sys::MediaStream) {
|
|||
}
|
||||
}
|
||||
|
||||
fn image_buffer_to_data_url(
|
||||
pub fn image_buffer_to_data_url(
|
||||
img: &ImageBuffer<Rgba<u8>, Vec<u8>>,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
// Create an in-memory buffer
|
||||
|
|
42
src/setup.rs
42
src/setup.rs
|
@ -58,40 +58,66 @@ pub fn SetupPage() -> Element {
|
|||
|
||||
rsx! {
|
||||
div {
|
||||
h1 { "Setup Page" }
|
||||
class: "min-h-screen flex flex-col justify-center items-center px-4",
|
||||
|
||||
h1 {
|
||||
class: "text-3xl font-bold mb-8 text-neutral-100",
|
||||
"Setup Page"
|
||||
}
|
||||
|
||||
form {
|
||||
class: "w-full max-w-md bg-stone-900 rounded-lg shadow-lg p-6 space-y-6",
|
||||
onsubmit: on_submit,
|
||||
|
||||
div {
|
||||
label { "Instance URL:" }
|
||||
class: "flex flex-col",
|
||||
label {
|
||||
class: "mb-2 font-semibold text-gray-700 dark:text-gray-300",
|
||||
"Instance URL:"
|
||||
}
|
||||
input {
|
||||
class: "border border-gray-300 dark:border-gray-700 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-100",
|
||||
value: "{form_data.read().instance_url}",
|
||||
oninput: move |evt| {
|
||||
let mut form_data = form_data.write();
|
||||
form_data.instance_url = evt.value();
|
||||
}
|
||||
},
|
||||
}
|
||||
{form_errors.read().instance_url.as_ref().map(|error| rsx! {
|
||||
p { class: "error", "{error}" }
|
||||
p { class: "text-red-600 mt-1 text-sm", "{error}" }
|
||||
})}
|
||||
}
|
||||
|
||||
div {
|
||||
label { "Token:" }
|
||||
class: "flex flex-col",
|
||||
label {
|
||||
class: "mb-2 font-semibold text-gray-700 dark:text-gray-300",
|
||||
"Token:"
|
||||
}
|
||||
input {
|
||||
class: "border border-gray-300 dark:border-gray-700 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-100",
|
||||
r#type: "password",
|
||||
value: "{form_data.read().token}",
|
||||
oninput: move |evt| {
|
||||
let mut form_data = form_data.write();
|
||||
form_data.token = evt.value();
|
||||
},
|
||||
}
|
||||
}
|
||||
{form_errors.read().token.as_ref().map(|error| rsx! {
|
||||
p { class: "error", "{error}" }
|
||||
p { class: "text-red-600 mt-1 text-sm", "{error}" }
|
||||
})}
|
||||
}
|
||||
button { type: "submit", "Complete Setup" }
|
||||
|
||||
button {
|
||||
type: "submit",
|
||||
class: "cursor-pointer w-full bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 text-white font-semibold py-2 rounded-md transition",
|
||||
"Complete Setup"
|
||||
}
|
||||
}
|
||||
|
||||
if *submitted.read() {
|
||||
div {
|
||||
class: "mt-6 bg-green-100 dark:bg-green-800 text-green-800 dark:text-green-200 rounded-md p-4 max-w-md w-full text-center font-semibold",
|
||||
h3 { "Setup Complete!" }
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue