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",
|
"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]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.98"
|
version = "1.0.98"
|
||||||
|
@ -377,12 +392,14 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bardecoder",
|
"bardecoder",
|
||||||
"base64",
|
"base64",
|
||||||
|
"chrono",
|
||||||
"dioxus",
|
"dioxus",
|
||||||
"dioxus-material-icons",
|
"dioxus-material-icons",
|
||||||
"dioxus-sdk",
|
"dioxus-sdk",
|
||||||
"gloo-timers",
|
"gloo-timers",
|
||||||
"image",
|
"image",
|
||||||
"log",
|
"log",
|
||||||
|
"qrc",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -425,6 +442,20 @@ version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
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]]
|
[[package]]
|
||||||
name = "ciborium"
|
name = "ciborium"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
@ -2417,6 +2448,30 @@ dependencies = [
|
||||||
"tracing",
|
"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]]
|
[[package]]
|
||||||
name = "icu_collections"
|
name = "icu_collections"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
|
@ -3728,6 +3783,23 @@ dependencies = [
|
||||||
"bytemuck",
|
"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]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.40"
|
version = "1.0.40"
|
||||||
|
|
|
@ -22,6 +22,8 @@ dioxus-sdk = { version = "0.6.0", features = ["storage"] }
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
reqwest = { version = "0.12.15", features = ["json"] }
|
reqwest = { version = "0.12.15", features = ["json"] }
|
||||||
dioxus-material-icons = "3.0.0"
|
dioxus-material-icons = "3.0.0"
|
||||||
|
chrono = "0.4.41"
|
||||||
|
qrc = "0.0.5"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["web"]
|
default = ["web"]
|
||||||
|
|
3
build.rs
3
build.rs
|
@ -21,7 +21,4 @@ fn main() {
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
panic!("Tailwind build failed with exit code: {}", status);
|
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 std::collections::HashMap;
|
||||||
|
|
||||||
use dioxus::signals::Readable;
|
use dioxus::signals::{Readable, Writable};
|
||||||
use dioxus_sdk::storage::use_persistent;
|
use dioxus_sdk::storage::use_persistent;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::setup::Credentials;
|
use crate::setup::Credentials;
|
||||||
|
use crate::try_recover_api;
|
||||||
|
|
||||||
use reqwest::header::{HeaderMap, HeaderValue};
|
use reqwest::header::{HeaderMap, HeaderValue};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
@ -12,11 +13,11 @@ use serde::de::DeserializeOwned;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub fn get_item(item: &str) -> Option<Item> {
|
pub fn get_item(item: &str) -> Option<Item> {
|
||||||
crate::API
|
if let Some(api) = crate::API.read().as_ref() {
|
||||||
.read()
|
api.get_item(item.to_string())
|
||||||
.as_ref()
|
} else {
|
||||||
.unwrap()
|
try_recover_api().get_item(item.to_string())
|
||||||
.get_item(item.to_string())
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn api_get_auth<T>(path: String) -> Result<T, reqwest::Error>
|
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>> =
|
let mut items: HashMap<String, Vec<Item>> =
|
||||||
api_get_auth("/items".to_string()).await.unwrap();
|
api_get_auth("/items".to_string()).await.unwrap();
|
||||||
let items = items.remove("items").unwrap();
|
let items = items.remove("items").unwrap();
|
||||||
let locations = api_get_auth("/locations".to_string()).await.unwrap();
|
let locations: HashMap<String, Location> =
|
||||||
let flow_info = api_get_auth("/flows".to_string()).await.unwrap();
|
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 {
|
Self {
|
||||||
instance,
|
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] {
|
pub async fn get_items(&self) -> &[Item] {
|
||||||
&self.items
|
&self.items
|
||||||
}
|
}
|
||||||
|
@ -358,7 +387,7 @@ impl API {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||||
pub struct FlowInfo {
|
pub struct FlowInfo {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -376,6 +405,13 @@ pub struct Item {
|
||||||
pub variants: HashMap<String, ItemVariant>,
|
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)]
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||||
pub struct ItemVariant {
|
pub struct ItemVariant {
|
||||||
pub item: String,
|
pub item: String,
|
||||||
|
|
81
src/main.rs
81
src/main.rs
|
@ -24,16 +24,18 @@ enum Route {
|
||||||
FlowPage {},
|
FlowPage {},
|
||||||
#[route("/locations")]
|
#[route("/locations")]
|
||||||
LocationsPage {},
|
LocationsPage {},
|
||||||
|
#[route("/location/:location")]
|
||||||
|
LocationPage { location: String },
|
||||||
|
|
||||||
#[route("/item/:id")]
|
#[route("/item/:id")]
|
||||||
ItemDetailPage { id: String },
|
ItemDetailPage { id: String },
|
||||||
#[route("/transaction/:id")]
|
#[route("/transaction/:id")]
|
||||||
TransactionPage { id: String },
|
TransactionPage { id: String },
|
||||||
|
|
||||||
|
#[end_layout]
|
||||||
#[route("/item/:item/:variant/consume/:id")]
|
#[route("/item/:item/:variant/consume/:id")]
|
||||||
ConsumePage { id: String, item: String, variant: String },
|
ConsumePage { id: String, item: String, variant: String },
|
||||||
|
|
||||||
|
|
||||||
#[route("/item/:item/supply?:param")]
|
#[route("/item/:item/supply?:param")]
|
||||||
SupplyPage { item: String, param: SupplyPageParam }
|
SupplyPage { item: String, param: SupplyPageParam }
|
||||||
// #[route("/blog/:id")]
|
// #[route("/blog/:id")]
|
||||||
|
@ -54,16 +56,26 @@ fn main() {
|
||||||
dioxus::launch(App);
|
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]
|
#[component]
|
||||||
fn App() -> Element {
|
fn App() -> Element {
|
||||||
let creds = use_persistent("creds", || Credentials::default());
|
let creds = use_persistent("creds", || Credentials::default());
|
||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let res = use_persistent("creds", || Credentials::default());
|
fetch_api().await;
|
||||||
if !res.read().empty() {
|
|
||||||
let api = api::API::new(res.read().instance_url.clone()).await;
|
|
||||||
*crate::API.write() = Some(api);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
|
@ -84,9 +96,21 @@ fn App() -> Element {
|
||||||
fn ItemTile(item: Item) -> Element {
|
fn ItemTile(item: Item) -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
Link {
|
Link {
|
||||||
to: Route::ItemDetailPage { id: item.uuid },
|
to: Route::ItemDetailPage { id: item.uuid },
|
||||||
{item.name.as_str()}
|
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]
|
#[component]
|
||||||
fn Navbar() -> Element {
|
fn Navbar() -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
div {
|
div {
|
||||||
id: "navbar",
|
id: "navbar",
|
||||||
|
class: "flex flex-wrap gap-2 p-2 shadow-md rounded-sm",
|
||||||
|
style: "background-color:hsl(223, 18.90%, 13.30%);",
|
||||||
Link {
|
Link {
|
||||||
to: Route::Home {},
|
to: Route::Home {},
|
||||||
div {
|
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",
|
||||||
class: "flex flex-row gap-2 p-2",
|
MaterialIcon { name: "home", size: 24 },
|
||||||
MaterialIcon { name: "home", size: 24 },
|
"Home"
|
||||||
"Home"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Link {
|
Link {
|
||||||
to: Route::ItemPage { },
|
to: Route::ItemPage { },
|
||||||
div {
|
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",
|
||||||
class: "flex flex-row gap-2 p-2",
|
MaterialIcon { name: "category", size: 24 },
|
||||||
MaterialIcon { name: "category", size: 24 },
|
"Items"
|
||||||
"Items"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Link {
|
Link {
|
||||||
to: Route::FlowPage { },
|
to: Route::FlowPage { },
|
||||||
div {
|
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",
|
||||||
class: "flex flex-row gap-2 p-2",
|
MaterialIcon { name: "assignment", size: 24 },
|
||||||
MaterialIcon { name: "assignment", size: 24 },
|
"Flows"
|
||||||
"Flows"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Link {
|
Link {
|
||||||
to: Route::LocationsPage { },
|
to: Route::LocationsPage { },
|
||||||
div {
|
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",
|
||||||
class: "flex flex-row gap-2 p-2",
|
MaterialIcon { name: "map", size: 24 },
|
||||||
MaterialIcon { name: "map", size: 24 },
|
"Locations"
|
||||||
"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 dioxus::prelude::*;
|
||||||
|
|
||||||
use crate::api;
|
use crate::{
|
||||||
|
api,
|
||||||
|
page::{BasicButton, LabeledInput, TransientHeader},
|
||||||
|
};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ConsumePage(id: String, item: String, variant: String) -> Element {
|
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);
|
let mut price = use_signal(|| 0.00);
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
|
div {
|
||||||
|
class: "w-full py-4 flex flex-col gap-6",
|
||||||
|
|
||||||
p { "Item: {item}"},
|
TransientHeader { title: format!("Consume {item} - {variant}") }
|
||||||
p { "Variant: {variant}"}
|
|
||||||
|
|
||||||
// TODO : destination
|
|
||||||
|
LabeledInput {
|
||||||
|
label: "Destination",
|
||||||
PredefinedSelector {
|
PredefinedSelector {
|
||||||
name: "Destination",
|
name: "Destination",
|
||||||
value: dest,
|
value: dest,
|
||||||
|
custom: true,
|
||||||
predefined: destinations
|
predefined: destinations
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
// Price Field
|
||||||
"Price: ",
|
LabeledInput {
|
||||||
|
label: "Price",
|
||||||
input {
|
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",
|
r#type: "number",
|
||||||
value: "{price}",
|
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 |_| {
|
onclick: move |_| {
|
||||||
let id2 = id.clone();
|
let id2 = id.clone();
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
api::API::consume_item(id2, dest(), price()).await;
|
api::API::consume_item(id2, dest(), price()).await;
|
||||||
navigator().go_back();
|
navigator().go_back();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
"Consume"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn PredefinedSelector(
|
pub fn PredefinedSelector(
|
||||||
name: String,
|
name: String,
|
||||||
value: Signal<String>,
|
value: Signal<String>,
|
||||||
|
custom: bool,
|
||||||
predefined: Resource<Vec<String>>,
|
predefined: Resource<Vec<String>>,
|
||||||
) -> Element {
|
) -> Element {
|
||||||
let mut screen_visible = use_signal(|| false);
|
let mut screen_visible = use_signal(|| false);
|
||||||
|
@ -60,32 +76,46 @@ pub fn PredefinedSelector(
|
||||||
div {
|
div {
|
||||||
class: "p-2",
|
class: "p-2",
|
||||||
button {
|
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),
|
onclick: move |_| screen_visible.set(true),
|
||||||
"{name}: {value}"
|
"{name}: {value}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if *screen_visible.read() {
|
if *screen_visible.read() {
|
||||||
div {
|
div {
|
||||||
class: "fixed inset-0 z-40 flex flex-col p-4 space-y-4 text-white",
|
class: "\
|
||||||
style: "background-color: #0f1116;",
|
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 {
|
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",
|
r#type: "text",
|
||||||
value: "{input_value}",
|
value: "{input_value}",
|
||||||
oninput: move |evt| input_value.set(evt.value().clone()),
|
oninput: move |evt| input_value.set(evt.value().clone()),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
class: "flex flex-wrap gap-2",
|
class: "flex flex-wrap gap-3",
|
||||||
match &*predefined.read() {
|
match &*predefined.read() {
|
||||||
Some(list) => rsx ! { {list.iter().map(|item| rsx! {
|
Some(list) => rsx! {
|
||||||
|
{list.iter().map(|item| rsx! {
|
||||||
button {
|
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: {
|
onclick: {
|
||||||
let item = item.clone();
|
let item = item.clone();
|
||||||
move |_| {
|
move |_| {
|
||||||
|
@ -96,13 +126,18 @@ pub fn PredefinedSelector(
|
||||||
},
|
},
|
||||||
"{item}"
|
"{item}"
|
||||||
}
|
}
|
||||||
})}},
|
})}
|
||||||
None => rsx!(p { "Loading..." }),
|
},
|
||||||
}
|
None => rsx!(p { "Loading..." }),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
class: "mt-auto flex gap-4",
|
||||||
button {
|
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 |_| {
|
onclick: move |_| {
|
||||||
value.set(input_value.read().clone());
|
value.set(input_value.read().clone());
|
||||||
screen_visible.set(false);
|
screen_visible.set(false);
|
||||||
|
@ -111,11 +146,14 @@ pub fn PredefinedSelector(
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
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),
|
onclick: move |_| screen_visible.set(false),
|
||||||
"Cancel"
|
"Cancel"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
132
src/page/home.rs
132
src/page/home.rs
|
@ -1,6 +1,7 @@
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_material_icons::MaterialIcon;
|
||||||
|
|
||||||
use crate::TransactionCard;
|
use crate::{page::HeaderTitle, TransactionCard};
|
||||||
|
|
||||||
/// Home Page
|
/// Home Page
|
||||||
#[component]
|
#[component]
|
||||||
|
@ -10,42 +11,119 @@ pub fn Home() -> Element {
|
||||||
let global = use_resource(move || async move { crate::api::API::get_global_item_stat().await });
|
let global = use_resource(move || async move { crate::api::API::get_global_item_stat().await });
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
|
div {
|
||||||
|
HeaderTitle { title: "Home" }
|
||||||
|
|
||||||
div {
|
div {
|
||||||
h1 { "Home" },
|
id: "column",
|
||||||
div {
|
class: "space-y-6",
|
||||||
id: "column",
|
|
||||||
|
|
||||||
match &*global.read_unchecked() {
|
match &*global.read_unchecked() {
|
||||||
Some(resp) => rsx! {
|
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 {
|
div {
|
||||||
id: "card",
|
class: "flex items-center gap-3",
|
||||||
p { {format!("Items: {}", resp.item_count)} }
|
MaterialIcon {
|
||||||
p { {format!("Inventory: {}", resp.total_transactions)} }
|
name: "inventory_2",
|
||||||
p { {format!("Price: {}", resp.total_price)} }
|
size: 24
|
||||||
}
|
},
|
||||||
},
|
p {
|
||||||
None => rsx! {
|
class: "text-md font-medium",
|
||||||
div { "Loading dogs..." }
|
{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 let Some(min) = &*min.read_unchecked() {
|
||||||
if !min.is_empty() {
|
if !min.is_empty() {
|
||||||
h1 { "Items under Minimum" },
|
h2 {
|
||||||
for item in min {
|
class: "text-2xl font-semibold mt-8 mb-4",
|
||||||
p { {format!("{} under minimum. Needs {} more.", item.item_variant, item.need)} }
|
"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 let Some(expired) = &*expired.read_unchecked() {
|
||||||
if !expired.is_empty() {
|
if !expired.is_empty() {
|
||||||
h1 { "Expired Items" },
|
h2 {
|
||||||
for item in expired {
|
class: "text-2xl font-semibold mt-8 mb-4",
|
||||||
TransactionCard { t: item.clone() }
|
"Expired Items"
|
||||||
|
},
|
||||||
|
div {
|
||||||
|
class: "space-y-4",
|
||||||
|
for item in expired {
|
||||||
|
TransactionCard { t: item.clone() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_material_icons::MaterialIcon;
|
||||||
|
|
||||||
use crate::{page::supply::SupplyPageParam, Route, TransactionCard};
|
use crate::{page::supply::SupplyPageParam, Route, TransactionCard};
|
||||||
|
|
||||||
|
@ -13,14 +14,14 @@ pub fn ItemDetailPage(id: String) -> Element {
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div {
|
div {
|
||||||
class: "flex flex-col h-screen",
|
class: "p-4 flex flex-col h-screen",
|
||||||
|
|
||||||
header {
|
header {
|
||||||
class: "p-4 text-white text-lg font-bold",
|
class: "text-white text-lg font-bold",
|
||||||
{item.name.as_str()}
|
{item.name.as_str()}
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
class: "p-6 flex flex-col space-y-4",
|
class: "flex flex-col space-y-4",
|
||||||
div {
|
div {
|
||||||
class: "flex items-start space-x-4",
|
class: "flex items-start space-x-4",
|
||||||
if let Some(image) = &item.image {
|
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 {
|
div {
|
||||||
class: "grid grid-cols-2 gap-4",
|
class: "grid grid-cols-2 gap-4",
|
||||||
{item.variants.iter().map(|(key, variant)| {
|
{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 dioxus::prelude::*;
|
||||||
|
|
||||||
use crate::ItemTile;
|
use crate::{
|
||||||
|
page::{item_detail::EventButton, HeaderTitle},
|
||||||
|
ItemTile,
|
||||||
|
};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ItemPage() -> Element {
|
pub fn ItemPage() -> Element {
|
||||||
|
@ -13,31 +16,32 @@ pub fn ItemPage() -> Element {
|
||||||
});
|
});
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
h1 { "Items" },
|
HeaderTitle { title: "Items" }
|
||||||
// barcode scan button
|
|
||||||
|
|
||||||
match &*items.read_unchecked() {
|
div {
|
||||||
Some(items) => {
|
class: "pb-4",
|
||||||
rsx! {
|
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 {
|
div {
|
||||||
class: "flex flex-col",
|
class: "py-3 px-4 hover:bg-gray-100 dark:hover:bg-gray-800 transition cursor-pointer",
|
||||||
for item in items {
|
ItemTile { item: item.clone() }
|
||||||
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 dioxus::prelude::*;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::{page::HeaderTitle, Route};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn LocationsPage() -> Element {
|
pub fn LocationsPage() -> Element {
|
||||||
let api = crate::API.read();
|
let api = crate::API.read();
|
||||||
|
@ -9,7 +11,7 @@ pub fn LocationsPage() -> Element {
|
||||||
let roots: Vec<String> = get_root_locations(&*locations.read());
|
let roots: Vec<String> = get_root_locations(&*locations.read());
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
h1 { "Locations" },
|
HeaderTitle { title: "Locations" },
|
||||||
|
|
||||||
for l in &roots {
|
for l in &roots {
|
||||||
LocationTreeTile { location: l.clone(), locations }
|
LocationTreeTile { location: l.clone(), locations }
|
||||||
|
@ -53,19 +55,39 @@ pub fn LocationTreeTile(
|
||||||
) -> Element {
|
) -> Element {
|
||||||
let mut open = use_signal(|| false);
|
let mut open = use_signal(|| false);
|
||||||
|
|
||||||
// TODO
|
|
||||||
let children = get_children_of_loc(location.clone(), &*locations.read());
|
let children = get_children_of_loc(location.clone(), &*locations.read());
|
||||||
|
|
||||||
rsx! {
|
let is_open = *open.read();
|
||||||
p { {location} }
|
let toggle_icon = if is_open { "▼" } else { "▶" };
|
||||||
button {
|
|
||||||
onclick: move |_| { open.set(true); },
|
|
||||||
"Expand"
|
|
||||||
}
|
|
||||||
|
|
||||||
if *open.read() {
|
rsx! {
|
||||||
for loc in &children {
|
div {
|
||||||
LocationTreeTile { location: loc.clone(), locations }
|
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 consume;
|
||||||
pub mod home;
|
pub mod home;
|
||||||
pub mod item_detail;
|
pub mod item_detail;
|
||||||
|
@ -6,10 +7,11 @@ pub mod locations;
|
||||||
pub mod supply;
|
pub mod supply;
|
||||||
pub mod transaction;
|
pub mod transaction;
|
||||||
|
|
||||||
|
pub use components::*;
|
||||||
pub use consume::ConsumePage;
|
pub use consume::ConsumePage;
|
||||||
pub use home::Home;
|
pub use home::Home;
|
||||||
pub use item_detail::ItemDetailPage;
|
pub use item_detail::ItemDetailPage;
|
||||||
pub use items::ItemPage;
|
pub use items::ItemPage;
|
||||||
pub use locations::LocationsPage;
|
pub use locations::*;
|
||||||
pub use supply::SupplyPage;
|
pub use supply::SupplyPage;
|
||||||
pub use transaction::TransactionPage;
|
pub use transaction::TransactionPage;
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_material_icons::MaterialIcon;
|
||||||
use serde::{Deserialize, Serialize};
|
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]
|
#[component]
|
||||||
pub fn SupplyPage(item: String, param: SupplyPageParam) -> Element {
|
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());
|
let mut state = use_signal(|| String::new());
|
||||||
|
|
||||||
rsx! {
|
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
|
// Variant Selection
|
||||||
PredefinedSelector {
|
LabeledInput {
|
||||||
name: "Variant",
|
label: "Variant",
|
||||||
value: variant_id,
|
PredefinedSelector {
|
||||||
predefined: variants_str
|
name: "Variant",
|
||||||
}
|
value: variant_id,
|
||||||
|
custom: false,
|
||||||
|
predefined: variants_str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Origin Field with Dropdown and Text Input
|
// Origin Selection
|
||||||
PredefinedSelector {
|
LabeledInput {
|
||||||
name: "Origin",
|
label: "Origin",
|
||||||
value: origin,
|
PredefinedSelector {
|
||||||
predefined: origins
|
name: "Origin",
|
||||||
}
|
value: origin,
|
||||||
|
custom: true,
|
||||||
|
predefined: origins
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Price Field
|
// Price Field
|
||||||
label {
|
LabeledInput {
|
||||||
"Price: ",
|
label: "Price",
|
||||||
input {
|
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",
|
r#type: "number",
|
||||||
value: "{price}",
|
value: "{price}",
|
||||||
disabled: param.force_price.is_some(),
|
disabled: param.force_price.is_some(),
|
||||||
|
@ -81,34 +96,49 @@ pub fn SupplyPage(item: String, param: SupplyPageParam) -> Element {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
// Location Dropdown
|
|
||||||
|
|
||||||
// Note
|
// Note Field
|
||||||
label {
|
LabeledInput {
|
||||||
"Note: ",
|
label: "Note",
|
||||||
input {
|
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",
|
r#type: "text",
|
||||||
value: "{note}",
|
value: "{note}",
|
||||||
oninput: move |e| note.set(e.value())
|
oninput: move |e| note.set(e.value())
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
// Submit Button
|
BasicButton {
|
||||||
|
onclick: move |_| {
|
||||||
button {
|
spawn(async move {
|
||||||
onclick: move |_| {
|
let variant = variants.iter().find(|x| x.name == variant_id()).unwrap().variant.clone();
|
||||||
spawn(async move {
|
let s = format!("{} {} {} {} {} {}", item(), variant, price(), origin(), location(), note());
|
||||||
let variant = variants.iter().find(|x| x.name == variant_id()).unwrap().variant.clone();
|
state.set(s);
|
||||||
let s = format!("{} {} {} {} {} {}", item(), variant, price(), origin(), location(), note());
|
let tid = crate::api::API::supply_item(item(), variant, price(), Some(origin()), Some(location()), Some(note())).await;
|
||||||
state.set(s);
|
navigator().replace(Route::TransactionPage { id: tid });
|
||||||
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"
|
||||||
},
|
}
|
||||||
"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::prelude::*;
|
||||||
use dioxus_material_icons::MaterialIcon;
|
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]
|
#[component]
|
||||||
pub fn TransactionPage(id: String) -> Element {
|
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
|
crate::api::API::get_transaction(id.read().to_string()).await
|
||||||
});
|
});
|
||||||
|
|
||||||
rsx! {
|
let qr = QRCode::from_string(id.read().clone()).to_png(512);
|
||||||
match transaction.value()() {
|
let qr_code_src = image_buffer_to_data_url(&qr).unwrap();
|
||||||
Some(transaction) => rsx! {
|
|
||||||
|
|
||||||
button {
|
rsx! {
|
||||||
onclick: move |_| {
|
match transaction.value()() {
|
||||||
navigator().push(Route::ConsumePage { id: transaction.uuid.clone(), item: transaction.item.clone(), variant: transaction.variant.clone() });
|
Some(transaction) => rsx! {
|
||||||
},
|
div {
|
||||||
"Consume"
|
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 {
|
// CONSUME INFO CARD or Consume Button
|
||||||
id: "column",
|
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 {
|
h3 { "Consumed" }
|
||||||
id: "row",
|
|
||||||
|
|
||||||
// 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 {
|
div {
|
||||||
id: "col-1",
|
class: "space-y-3",
|
||||||
|
|
||||||
p { {get_item(&transaction.item).unwrap().name.as_str()} }
|
IconLabel {
|
||||||
p { {get_item(&transaction.item).unwrap().variants.get(&transaction.variant).as_ref().unwrap().name.clone()} }
|
icon: "attach_money".to_string(),
|
||||||
p { {transaction.timestamp.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
|
None => rsx! {
|
||||||
|
p {
|
||||||
|
class: "text-center text-gray-500 dark:text-gray-400 p-6",
|
||||||
// consume info
|
"Loading transaction..."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
None => rsx! { p { "Loading..." }}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn IconLabel(icon: String, label: String, value: String) -> Element {
|
pub fn IconLabel(icon: String, label: String, value: String) -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "flex items-center space-x-3",
|
div {
|
||||||
MaterialIcon {
|
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",
|
||||||
name: icon,
|
|
||||||
size: 12,
|
|
||||||
},
|
|
||||||
|
|
||||||
div { class: "flex flex-col",
|
MaterialIcon {
|
||||||
span { class: "text-sm font-medium text-gray-700", "{label}" }
|
name: icon,
|
||||||
span { class: "text-lg font-semibold text-gray-900", "{value}" }
|
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>>,
|
img: &ImageBuffer<Rgba<u8>, Vec<u8>>,
|
||||||
) -> Result<String, Box<dyn std::error::Error>> {
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
// Create an in-memory buffer
|
// Create an in-memory buffer
|
||||||
|
|
42
src/setup.rs
42
src/setup.rs
|
@ -58,40 +58,66 @@ pub fn SetupPage() -> Element {
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div {
|
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 {
|
form {
|
||||||
|
class: "w-full max-w-md bg-stone-900 rounded-lg shadow-lg p-6 space-y-6",
|
||||||
onsubmit: on_submit,
|
onsubmit: on_submit,
|
||||||
|
|
||||||
div {
|
div {
|
||||||
label { "Instance URL:" }
|
class: "flex flex-col",
|
||||||
|
label {
|
||||||
|
class: "mb-2 font-semibold text-gray-700 dark:text-gray-300",
|
||||||
|
"Instance URL:"
|
||||||
|
}
|
||||||
input {
|
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}",
|
value: "{form_data.read().instance_url}",
|
||||||
oninput: move |evt| {
|
oninput: move |evt| {
|
||||||
let mut form_data = form_data.write();
|
let mut form_data = form_data.write();
|
||||||
form_data.instance_url = evt.value();
|
form_data.instance_url = evt.value();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
{form_errors.read().instance_url.as_ref().map(|error| rsx! {
|
{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 {
|
div {
|
||||||
label { "Token:" }
|
class: "flex flex-col",
|
||||||
|
label {
|
||||||
|
class: "mb-2 font-semibold text-gray-700 dark:text-gray-300",
|
||||||
|
"Token:"
|
||||||
|
}
|
||||||
input {
|
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",
|
r#type: "password",
|
||||||
value: "{form_data.read().token}",
|
value: "{form_data.read().token}",
|
||||||
oninput: move |evt| {
|
oninput: move |evt| {
|
||||||
let mut form_data = form_data.write();
|
let mut form_data = form_data.write();
|
||||||
form_data.token = evt.value();
|
form_data.token = evt.value();
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
{form_errors.read().token.as_ref().map(|error| rsx! {
|
{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() {
|
if *submitted.read() {
|
||||||
div {
|
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!" }
|
h3 { "Setup Complete!" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue