rm dioxus-sdk storage + localstorage
This commit is contained in:
parent
b6394c41c5
commit
f16be1536c
19 changed files with 49 additions and 2165 deletions
146
Cargo.lock
generated
146
Cargo.lock
generated
|
@ -206,15 +206,6 @@ dependencies = [
|
||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "atomic-polyfill"
|
|
||||||
version = "1.0.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4"
|
|
||||||
dependencies = [
|
|
||||||
"critical-section",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
|
@ -395,7 +386,6 @@ dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"dioxus",
|
"dioxus",
|
||||||
"dioxus-material-icons",
|
"dioxus-material-icons",
|
||||||
"dioxus-sdk",
|
|
||||||
"gloo-timers",
|
"gloo-timers",
|
||||||
"image",
|
"image",
|
||||||
"log",
|
"log",
|
||||||
|
@ -483,12 +473,6 @@ dependencies = [
|
||||||
"half",
|
"half",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cobs"
|
|
||||||
version = "0.2.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cocoa"
|
name = "cocoa"
|
||||||
version = "0.25.0"
|
version = "0.25.0"
|
||||||
|
@ -732,12 +716,6 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "critical-section"
|
|
||||||
version = "1.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-channel"
|
name = "crossbeam-channel"
|
||||||
version = "0.5.15"
|
version = "0.5.15"
|
||||||
|
@ -1289,31 +1267,6 @@ dependencies = [
|
||||||
"syn 2.0.101",
|
"syn 2.0.101",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dioxus-sdk"
|
|
||||||
version = "0.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3b7b74aede7070ec1c0ef582dbec8bce93a5c40421155459fcdbcaa0e6ef0bf0"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"dioxus",
|
|
||||||
"dioxus-signals",
|
|
||||||
"directories",
|
|
||||||
"futures-util",
|
|
||||||
"js-sys",
|
|
||||||
"once_cell",
|
|
||||||
"postcard",
|
|
||||||
"rustc-hash",
|
|
||||||
"serde",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
"uuid",
|
|
||||||
"warnings",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"web-sys",
|
|
||||||
"yazi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dioxus-signals"
|
name = "dioxus-signals"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
|
@ -1327,7 +1280,6 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"serde",
|
|
||||||
"tracing",
|
"tracing",
|
||||||
"warnings",
|
"warnings",
|
||||||
]
|
]
|
||||||
|
@ -1376,33 +1328,13 @@ dependencies = [
|
||||||
"syn 2.0.101",
|
"syn 2.0.101",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "directories"
|
|
||||||
version = "4.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210"
|
|
||||||
dependencies = [
|
|
||||||
"dirs-sys 0.3.7",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs"
|
name = "dirs"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs-sys 0.5.0",
|
"dirs-sys",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dirs-sys"
|
|
||||||
version = "0.3.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"redox_users 0.4.6",
|
|
||||||
"winapi",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1413,7 +1345,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"option-ext",
|
"option-ext",
|
||||||
"redox_users 0.5.0",
|
"redox_users",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1500,18 +1432,6 @@ version = "1.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "embedded-io"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "embedded-io"
|
|
||||||
version = "0.6.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.35"
|
version = "0.8.35"
|
||||||
|
@ -2247,15 +2167,6 @@ dependencies = [
|
||||||
"crunchy",
|
"crunchy",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hash32"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
|
|
||||||
dependencies = [
|
|
||||||
"byteorder",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
|
@ -2274,20 +2185,6 @@ version = "0.15.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
|
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "heapless"
|
|
||||||
version = "0.7.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"
|
|
||||||
dependencies = [
|
|
||||||
"atomic-polyfill",
|
|
||||||
"hash32",
|
|
||||||
"rustc_version 0.4.1",
|
|
||||||
"serde",
|
|
||||||
"spin",
|
|
||||||
"stable_deref_trait",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
@ -3666,19 +3563,6 @@ version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2"
|
checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "postcard"
|
|
||||||
version = "1.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8"
|
|
||||||
dependencies = [
|
|
||||||
"cobs",
|
|
||||||
"embedded-io 0.4.0",
|
|
||||||
"embedded-io 0.6.1",
|
|
||||||
"heapless",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
|
@ -3937,17 +3821,6 @@ dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "redox_users"
|
|
||||||
version = "0.4.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
|
||||||
dependencies = [
|
|
||||||
"getrandom 0.2.16",
|
|
||||||
"libredox",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_users"
|
name = "redox_users"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
@ -4583,15 +4456,6 @@ dependencies = [
|
||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spin"
|
|
||||||
version = "0.9.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
|
||||||
dependencies = [
|
|
||||||
"lock_api",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
@ -5984,12 +5848,6 @@ version = "0.8.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
|
checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "yazi"
|
|
||||||
version = "0.1.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
|
|
@ -8,7 +8,7 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus = { version = "0.6.0", features = ["router"] }
|
dioxus = { version = "0.6.0", features = ["router"] }
|
||||||
web-sys = { version = "0.3", features = ["HtmlVideoElement", "MediaDevices", "MediaStream", "MediaStreamConstraints", "Navigator", "HtmlCanvasElement", "CanvasRenderingContext2d", "MediaStreamTrack", "ImageData"] }
|
web-sys = { version = "0.3", features = ["HtmlVideoElement", "MediaDevices", "MediaStream", "MediaStreamConstraints", "Navigator", "HtmlCanvasElement", "CanvasRenderingContext2d", "MediaStreamTrack", "ImageData", "Storage", "Window"] }
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
log = "0.4.27"
|
log = "0.4.27"
|
||||||
wasm-bindgen-futures = "0.4.50"
|
wasm-bindgen-futures = "0.4.50"
|
||||||
|
@ -18,7 +18,6 @@ gloo-timers = { version = "0.3.0", features = ["futures"] }
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
bardecoder = "0.5.0"
|
bardecoder = "0.5.0"
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
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"
|
||||||
|
|
37
src/api.rs
37
src/api.rs
|
@ -1,10 +1,10 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use dioxus::signals::{Readable, Writable};
|
use dioxus::signals::{Readable, Writable};
|
||||||
use dioxus_sdk::storage::{get_from_storage, use_persistent, SessionStorage};
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::setup::Credentials;
|
use crate::setup::Credentials;
|
||||||
|
use crate::store::{load_from_local_storage, save_to_local_storage};
|
||||||
use crate::try_recover_api;
|
use crate::try_recover_api;
|
||||||
|
|
||||||
use reqwest::header::{HeaderMap, HeaderValue};
|
use reqwest::header::{HeaderMap, HeaderValue};
|
||||||
|
@ -24,8 +24,7 @@ pub async fn api_get_auth<T>(path: String) -> Result<T, reqwest::Error>
|
||||||
where
|
where
|
||||||
T: DeserializeOwned,
|
T: DeserializeOwned,
|
||||||
{
|
{
|
||||||
let creds =
|
let creds = load_from_local_storage::<Credentials>("creds").unwrap_or_default();
|
||||||
get_from_storage::<SessionStorage, _>("creds".to_owned(), || Credentials::default());
|
|
||||||
let token = creds.token.as_str();
|
let token = creds.token.as_str();
|
||||||
let instance = creds.instance_url.as_str();
|
let instance = creds.instance_url.as_str();
|
||||||
|
|
||||||
|
@ -52,8 +51,7 @@ where
|
||||||
T: DeserializeOwned,
|
T: DeserializeOwned,
|
||||||
X: Serialize,
|
X: Serialize,
|
||||||
{
|
{
|
||||||
let creds =
|
let creds = load_from_local_storage::<Credentials>("creds").unwrap_or_default();
|
||||||
get_from_storage::<SessionStorage, _>("creds".to_owned(), || Credentials::default());
|
|
||||||
let token = creds.token.as_str();
|
let token = creds.token.as_str();
|
||||||
let instance = creds.instance_url.as_str();
|
let instance = creds.instance_url.as_str();
|
||||||
|
|
||||||
|
@ -94,13 +92,9 @@ impl API {
|
||||||
let flow_info: HashMap<String, FlowInfo> =
|
let flow_info: HashMap<String, FlowInfo> =
|
||||||
api_get_auth("/flows".to_string()).await.unwrap();
|
api_get_auth("/flows".to_string()).await.unwrap();
|
||||||
|
|
||||||
let mut p_items = use_persistent("api_items", || Vec::<Item>::new());
|
save_to_local_storage("api_items", items.clone());
|
||||||
p_items.set(items.clone());
|
save_to_local_storage("api_locations", locations.clone());
|
||||||
let mut p_locations =
|
save_to_local_storage("api_flow_info", flow_info.clone());
|
||||||
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,
|
||||||
|
@ -112,19 +106,12 @@ impl API {
|
||||||
|
|
||||||
pub fn try_recover() -> Self {
|
pub fn try_recover() -> Self {
|
||||||
Self {
|
Self {
|
||||||
instance: use_persistent("creds", || Credentials::default())
|
instance: load_from_local_storage::<Credentials>("creds")
|
||||||
.read()
|
.unwrap_or_default()
|
||||||
.instance_url
|
.instance_url,
|
||||||
.clone(),
|
items: load_from_local_storage("api_items").unwrap_or_default(),
|
||||||
items: use_persistent("api_items", || Vec::<Item>::new())
|
locations: load_from_local_storage("api_locations").unwrap_or_default(),
|
||||||
.read()
|
flow_info: load_from_local_storage("api_flow_info").unwrap_or_default(),
|
||||||
.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(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
101
src/main.dart
101
src/main.dart
|
@ -1,101 +0,0 @@
|
||||||
import 'package:cdb_ui/api.dart';
|
|
||||||
import 'package:cdb_ui/pages/flow/flows_page.dart';
|
|
||||||
import 'package:cdb_ui/pages/items.dart';
|
|
||||||
import 'package:cdb_ui/pages/locations.dart';
|
|
||||||
import 'package:cdb_ui/pages/setup.dart';
|
|
||||||
import 'package:cdb_ui/pages/home.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
Future<void> main() async {
|
|
||||||
runApp(const MyApp());
|
|
||||||
}
|
|
||||||
|
|
||||||
class MyApp extends StatefulWidget {
|
|
||||||
const MyApp({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<MyApp> createState() => _MyAppState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MyAppState extends State<MyApp> {
|
|
||||||
bool init = false;
|
|
||||||
|
|
||||||
refresh() {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
() async {
|
|
||||||
await API().init(refresh);
|
|
||||||
if (API().isInit()) {
|
|
||||||
await API().prefetch();
|
|
||||||
setState(() {
|
|
||||||
init = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return MaterialApp(
|
|
||||||
title: 'CDB',
|
|
||||||
theme: ThemeData(
|
|
||||||
colorScheme: ColorScheme.fromSeed(
|
|
||||||
seedColor: Colors.deepPurple, brightness: Brightness.dark),
|
|
||||||
useMaterial3: true,
|
|
||||||
),
|
|
||||||
home: API().isInit()
|
|
||||||
? (API().isPrefetched()
|
|
||||||
? const MyHomePage()
|
|
||||||
: const Scaffold(
|
|
||||||
body: Center(child: CircularProgressIndicator()),
|
|
||||||
))
|
|
||||||
: const SetupPage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MyHomePage extends StatefulWidget {
|
|
||||||
const MyHomePage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<MyHomePage> createState() => _MyHomePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MyHomePageState extends State<MyHomePage> {
|
|
||||||
int pageIndex = 0;
|
|
||||||
|
|
||||||
List<Widget> pages = [
|
|
||||||
const HomePage(),
|
|
||||||
const ItemsPage(),
|
|
||||||
const FlowsPage(),
|
|
||||||
const LocationsPage()
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
bottomNavigationBar: BottomNavigationBar(
|
|
||||||
fixedColor: Colors.white,
|
|
||||||
unselectedItemColor: Colors.white70,
|
|
||||||
items: const [
|
|
||||||
BottomNavigationBarItem(icon: Icon(Icons.home), label: "Home"),
|
|
||||||
BottomNavigationBarItem(
|
|
||||||
icon: Icon(Icons.data_object), label: "Items"),
|
|
||||||
BottomNavigationBarItem(icon: Icon(Icons.receipt), label: "Flows"),
|
|
||||||
BottomNavigationBarItem(
|
|
||||||
icon: Icon(Icons.location_city), label: "Locations"),
|
|
||||||
],
|
|
||||||
currentIndex: pageIndex,
|
|
||||||
onTap: (value) {
|
|
||||||
setState(() {
|
|
||||||
pageIndex = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
body: pages[pageIndex],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
10
src/main.rs
10
src/main.rs
|
@ -1,7 +1,6 @@
|
||||||
use api::{Item, Transaction};
|
use api::{Item, Transaction};
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus_material_icons::{MaterialIcon, MaterialIconStylesheet};
|
use dioxus_material_icons::{MaterialIcon, MaterialIconStylesheet};
|
||||||
use dioxus_sdk::storage::{get_from_storage, use_persistent, SessionStorage};
|
|
||||||
use qrscan::QRCodeScanPage;
|
use qrscan::QRCodeScanPage;
|
||||||
use setup::{Credentials, SetupPage};
|
use setup::{Credentials, SetupPage};
|
||||||
|
|
||||||
|
@ -9,8 +8,10 @@ pub mod api;
|
||||||
pub mod page;
|
pub mod page;
|
||||||
pub mod qrscan;
|
pub mod qrscan;
|
||||||
pub mod setup;
|
pub mod setup;
|
||||||
|
pub mod store;
|
||||||
|
|
||||||
use page::{supply::SupplyPageParam, *};
|
use page::{supply::SupplyPageParam, *};
|
||||||
|
use store::load_from_local_storage;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Routable, PartialEq)]
|
#[derive(Debug, Clone, Routable, PartialEq)]
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
|
@ -52,7 +53,6 @@ const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css");
|
||||||
pub static API: GlobalSignal<Option<api::API>> = Signal::global(|| None);
|
pub static API: GlobalSignal<Option<api::API>> = Signal::global(|| None);
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
dioxus_sdk::set_dir!();
|
|
||||||
dioxus::launch(App);
|
dioxus::launch(App);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ pub fn try_recover_api() -> crate::api::API {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_api() {
|
pub async fn fetch_api() {
|
||||||
let res = get_from_storage::<SessionStorage, _>("creds".to_owned(), || Credentials::default());
|
let res: Credentials = load_from_local_storage("creds").unwrap_or_default();
|
||||||
if !res.empty() {
|
if !res.empty() {
|
||||||
let api = api::API::new(res.instance_url.clone()).await;
|
let api = api::API::new(res.instance_url.clone()).await;
|
||||||
*crate::API.write() = Some(api);
|
*crate::API.write() = Some(api);
|
||||||
|
@ -70,7 +70,7 @@ pub async fn fetch_api() {
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn App() -> Element {
|
fn App() -> Element {
|
||||||
let creds = use_persistent("creds", || Credentials::default());
|
let creds: Credentials = load_from_local_storage("creds").unwrap_or_default();
|
||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
fetch_api().await;
|
fetch_api().await;
|
||||||
|
@ -82,7 +82,7 @@ fn App() -> Element {
|
||||||
document::Link { rel: "stylesheet", href: TAILWIND_CSS }
|
document::Link { rel: "stylesheet", href: TAILWIND_CSS }
|
||||||
MaterialIconStylesheet { }
|
MaterialIconStylesheet { }
|
||||||
|
|
||||||
if creds.read().empty() {
|
if creds.empty() {
|
||||||
SetupPage { }
|
SetupPage { }
|
||||||
} else {
|
} else {
|
||||||
Router::<Route> {}
|
Router::<Route> {}
|
||||||
|
|
|
@ -1,115 +0,0 @@
|
||||||
import 'package:cdb_ui/api.dart';
|
|
||||||
import 'package:cdb_ui/pages/supply.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class ConsumePage extends StatefulWidget {
|
|
||||||
final Transaction transaction;
|
|
||||||
final Function refresh;
|
|
||||||
|
|
||||||
const ConsumePage(this.transaction, this.refresh, {super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ConsumePage> createState() => _ConsumePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ConsumePageState extends State<ConsumePage> {
|
|
||||||
final _formKey = GlobalKey<FormState>();
|
|
||||||
String _selectedDestination = "";
|
|
||||||
String _price = "";
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _consume() {
|
|
||||||
if (_formKey.currentState!.validate()) {
|
|
||||||
_formKey.currentState!.save();
|
|
||||||
|
|
||||||
API()
|
|
||||||
.consumeItem(widget.transaction.uuid, _selectedDestination,
|
|
||||||
double.parse(_price))
|
|
||||||
.then((_) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('Item consumed successfully!')),
|
|
||||||
);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
widget.refresh();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text('Consume Item'),
|
|
||||||
),
|
|
||||||
body: FutureBuilder(
|
|
||||||
future: API().getUniqueField(
|
|
||||||
widget.transaction.item, widget.transaction.variant, "destination"),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (!snapshot.hasData) {
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
|
|
||||||
var destinations = snapshot.data!;
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Form(
|
|
||||||
key: _formKey,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Destination Field with Dropdown and Text Input
|
|
||||||
AutocompletedTextField(
|
|
||||||
options: destinations,
|
|
||||||
getValue: () => _selectedDestination,
|
|
||||||
onChanged: (value) {
|
|
||||||
_selectedDestination = value;
|
|
||||||
},
|
|
||||||
onSelection: (selection) {
|
|
||||||
setState(() {
|
|
||||||
_selectedDestination = selection;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
label: "Destination"),
|
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Price Field
|
|
||||||
TextFormField(
|
|
||||||
decoration: const InputDecoration(labelText: 'Price'),
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
initialValue: _price,
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'Please enter a price';
|
|
||||||
}
|
|
||||||
if (double.tryParse(value) == null) {
|
|
||||||
return 'Please enter a valid number';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
onSaved: (value) {
|
|
||||||
_price = value!;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
// Submit Button
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: _consume,
|
|
||||||
child: const Text('Consume Item'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class ExpandableListItem {
|
|
||||||
ExpandableListItem({
|
|
||||||
required this.body,
|
|
||||||
required this.header,
|
|
||||||
this.isExpanded = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
Widget body;
|
|
||||||
Widget header;
|
|
||||||
bool isExpanded;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ExpandableList extends StatefulWidget {
|
|
||||||
final List<ExpandableListItem> entries;
|
|
||||||
|
|
||||||
const ExpandableList(this.entries, {super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ExpandableList> createState() => _ExpandableListState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ExpandableListState extends State<ExpandableList> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SingleChildScrollView(
|
|
||||||
child: Container(
|
|
||||||
child: _buildPanel(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPanel() {
|
|
||||||
return ExpansionPanelList(
|
|
||||||
expansionCallback: (int index, bool isExpanded) {
|
|
||||||
setState(() {
|
|
||||||
widget.entries[index].isExpanded = isExpanded;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
children: widget.entries.map<ExpansionPanel>((ExpandableListItem item) {
|
|
||||||
return ExpansionPanel(
|
|
||||||
headerBuilder: (BuildContext context, bool isExpanded) {
|
|
||||||
return ListTile(
|
|
||||||
title: item.header,
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
widget.entries.firstWhere((x) => x == item).isExpanded =
|
|
||||||
!isExpanded;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
body: item.body,
|
|
||||||
isExpanded: item.isExpanded,
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,144 +0,0 @@
|
||||||
import 'package:cdb_ui/api.dart' as API;
|
|
||||||
import 'package:cdb_ui/pages/flow/create_flow_page.dart';
|
|
||||||
import 'package:cdb_ui/pages/flow/end_flow_page.dart';
|
|
||||||
import 'package:cdb_ui/pages/flow/flow_note.dart';
|
|
||||||
import 'package:cdb_ui/pages/transaction.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:qr_bar_code/qr/src/qr_code.dart';
|
|
||||||
import 'package:qr_bar_code/qr/src/types.dart';
|
|
||||||
|
|
||||||
class ActiveFlowPage extends StatefulWidget {
|
|
||||||
final API.Flow flow;
|
|
||||||
final API.FlowInfo info;
|
|
||||||
|
|
||||||
const ActiveFlowPage(this.flow, this.info, {super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ActiveFlowPage> createState() => _ActiveFlowPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ActiveFlowPageState extends State<ActiveFlowPage> {
|
|
||||||
_refresh() {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(widget.info.name),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
if (widget.info.produces?.isNotEmpty ?? false) {
|
|
||||||
Navigator.of(context).push(MaterialPageRoute(
|
|
||||||
builder: (context) =>
|
|
||||||
EndFlowWithProduce(widget.flow, widget.info)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// simple dialog
|
|
||||||
var confirm = await showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: const Text('Are you sure?'),
|
|
||||||
content: const Text('This will end the flow.'),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(false),
|
|
||||||
child: const Text('Cancel'),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
|
||||||
child: const Text('End'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (confirm ?? false) {
|
|
||||||
await API
|
|
||||||
.API()
|
|
||||||
.endFlow(widget.flow.id)
|
|
||||||
.then((x) => Navigator.of(context).pop());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.stop)),
|
|
||||||
if (widget.info.next != null)
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
var newInfo = API.API().getFlowInfo(widget.info.next!);
|
|
||||||
Navigator.of(context).push(MaterialPageRoute(
|
|
||||||
builder: (context) {
|
|
||||||
return CreateFlowPage(
|
|
||||||
newInfo,
|
|
||||||
() {},
|
|
||||||
previousFlow: widget.flow,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.arrow_forward)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
QRCode(
|
|
||||||
data: widget.flow.id,
|
|
||||||
size: 128,
|
|
||||||
eyeStyle: const QREyeStyle(color: Colors.white),
|
|
||||||
dataModuleStyle: const QRDataModuleStyle(color: Colors.white),
|
|
||||||
semanticsLabel: "Transaction UUID",
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 16.0,
|
|
||||||
),
|
|
||||||
Text("Started since: ${tsFormat(widget.flow.started)}"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (widget.flow.input != null)
|
|
||||||
...widget.flow.input!.map((x) => Text("Input: $x")).toList(),
|
|
||||||
if (widget.flow.done != null) ...[
|
|
||||||
Text("Ended: ${tsFormat(widget.flow.done!.ended)}"),
|
|
||||||
if (widget.flow.done!.next != null)
|
|
||||||
Text("Next: ${widget.flow.done!.next!}"),
|
|
||||||
...widget.flow.done!.produced!
|
|
||||||
.map((x) => Text("Produced: $x"))
|
|
||||||
.toList(),
|
|
||||||
],
|
|
||||||
const Divider(),
|
|
||||||
FutureBuilder(
|
|
||||||
future: API.API().getNotesOfFlow(widget.flow.id),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (!snapshot.hasData) {
|
|
||||||
return const CircularProgressIndicator();
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = snapshot.data!;
|
|
||||||
|
|
||||||
return Expanded(
|
|
||||||
child: ListView(
|
|
||||||
children: data
|
|
||||||
.map(
|
|
||||||
(x) => FlowNoteCard(x),
|
|
||||||
)
|
|
||||||
.toList()));
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).push(MaterialPageRoute(
|
|
||||||
builder: (context) => AddNotePage(widget.flow, _refresh)));
|
|
||||||
},
|
|
||||||
child: const Icon(Icons.note_add),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,135 +0,0 @@
|
||||||
import 'package:cdb_ui/api.dart' as API;
|
|
||||||
import 'package:cdb_ui/pages/flow/active_flow_page.dart';
|
|
||||||
import 'package:cdb_ui/pages/transaction.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class CreateFlowPage extends StatefulWidget {
|
|
||||||
final API.FlowInfo info;
|
|
||||||
final Function refresh;
|
|
||||||
final API.Flow? previousFlow;
|
|
||||||
|
|
||||||
const CreateFlowPage(this.info, this.refresh, {this.previousFlow, super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<CreateFlowPage> createState() => _CreateFlowPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CreateFlowPageState extends State<CreateFlowPage> {
|
|
||||||
List<API.Transaction> depends = [];
|
|
||||||
|
|
||||||
void _create(BuildContext context) {
|
|
||||||
if (widget.previousFlow != null) {
|
|
||||||
API
|
|
||||||
.API()
|
|
||||||
.continueFlow(widget.previousFlow!.id,
|
|
||||||
input: depends.map((x) => x.uuid).toList())
|
|
||||||
.then((x) {
|
|
||||||
API.API().getFlow(x).then((flow) {
|
|
||||||
var info = API.API().getFlowInfo(flow.kind);
|
|
||||||
Navigator.of(context).pushReplacement(MaterialPageRoute(
|
|
||||||
builder: (context) => ActiveFlowPage(flow, info),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
API
|
|
||||||
.API()
|
|
||||||
.startFlow(widget.info.id, input: depends.map((x) => x.uuid).toList())
|
|
||||||
.then((flowID) {
|
|
||||||
widget.refresh();
|
|
||||||
API.API().getFlow(flowID).then((flow) {
|
|
||||||
Navigator.of(context).pushReplacement(MaterialPageRoute(
|
|
||||||
builder: (context) => ActiveFlowPage(flow, widget.info)));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void selectDependItems(BuildContext context, String itemVariant) {
|
|
||||||
var (item, variant) = API.itemVariant(itemVariant);
|
|
||||||
|
|
||||||
API.API().getInventoryOfVariant(item, variant).then((transactions) {
|
|
||||||
Navigator.of(context).push(MaterialPageRoute(
|
|
||||||
builder: (context) {
|
|
||||||
return TransactionSelectPage(transactions, onSelect: (t) {
|
|
||||||
if (!depends.contains(t)) {
|
|
||||||
setState(() {
|
|
||||||
depends.add(t);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, exclude: depends);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> buildInputSelection(BuildContext context) {
|
|
||||||
if (widget.info.depends.isEmpty) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
Column(
|
|
||||||
children: widget.info.depends.map((x) {
|
|
||||||
var (item, variant) = API.itemVariant(x);
|
|
||||||
return ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
selectDependItems(context, x);
|
|
||||||
},
|
|
||||||
child:
|
|
||||||
Text("Add ${API.API().getItem(item).variants[variant]!.name}"));
|
|
||||||
}).toList()),
|
|
||||||
const Divider(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget flowContinuation() {
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Card(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Text(API.API().getFlowInfo(widget.previousFlow!.kind).name),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Card(child: Icon(Icons.arrow_right)),
|
|
||||||
Card(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Text(widget.info.name),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(widget.previousFlow != null
|
|
||||||
? "Continue to ${widget.previousFlow!.kind}"
|
|
||||||
: "Create new ${widget.info.name} Flow")),
|
|
||||||
body: Column(
|
|
||||||
children: [
|
|
||||||
if (widget.previousFlow != null) ...[
|
|
||||||
flowContinuation(),
|
|
||||||
const Divider()
|
|
||||||
],
|
|
||||||
...buildInputSelection(context),
|
|
||||||
Card(
|
|
||||||
child: Column(
|
|
||||||
children: depends
|
|
||||||
.map((x) => TransactionCard(x, () {},
|
|
||||||
onTap: (x) {}, onLongPress: (x) {}))
|
|
||||||
.toList())),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () => _create(context),
|
|
||||||
child: const Text("Create Flow"))
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,96 +0,0 @@
|
||||||
import 'package:cdb_ui/api.dart' as API;
|
|
||||||
import 'package:cdb_ui/api.dart';
|
|
||||||
import 'package:cdb_ui/pages/supply.dart';
|
|
||||||
import 'package:cdb_ui/pages/transaction.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class EndFlowWithProduce extends StatefulWidget {
|
|
||||||
final API.Flow flow;
|
|
||||||
final API.FlowInfo info;
|
|
||||||
|
|
||||||
const EndFlowWithProduce(this.flow, this.info, {super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<EndFlowWithProduce> createState() => _EndFlowWithProduceState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _EndFlowWithProduceState extends State<EndFlowWithProduce> {
|
|
||||||
List<SupplyForm> produces = [];
|
|
||||||
late Map<String, Location> locations;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
locations = API.API().getLocations();
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh() {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
void addProduced(SupplyForm t) {
|
|
||||||
setState(() {
|
|
||||||
produces.add(t);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> addProduceButtons() {
|
|
||||||
List<Widget> ret = [];
|
|
||||||
|
|
||||||
for (var i in widget.info.produces!) {
|
|
||||||
var (itemID, variant) = API.itemVariant(i);
|
|
||||||
var item = API.API().getItem(itemID);
|
|
||||||
ret.add(ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).push(MaterialPageRoute(
|
|
||||||
builder: (context) {
|
|
||||||
return SupplyPage(
|
|
||||||
item,
|
|
||||||
refresh,
|
|
||||||
onlyVariants: [variant],
|
|
||||||
forcePrice: "0.00",
|
|
||||||
forceOrigin: "flow::${widget.flow.kind}::${widget.flow.id}",
|
|
||||||
onCreate: addProduced,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
},
|
|
||||||
child: Text("Produced ${item.variants[variant]!.name}")));
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
_endFlow() {
|
|
||||||
API.API().endFlow(widget.flow.id, produced: produces).then((x) {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text("End ${widget.info.name} Flow"),
|
|
||||||
),
|
|
||||||
body: Column(
|
|
||||||
children: [
|
|
||||||
...addProduceButtons(),
|
|
||||||
const Divider(),
|
|
||||||
...produces.map((x) {
|
|
||||||
return TransactionCard(
|
|
||||||
x.transaction(locations),
|
|
||||||
() {},
|
|
||||||
onLongPress: (x) {},
|
|
||||||
onTap: (x) {},
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
const SizedBox(
|
|
||||||
height: 10,
|
|
||||||
),
|
|
||||||
ElevatedButton(onPressed: _endFlow, child: const Text("End Flow"))
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,85 +0,0 @@
|
||||||
import 'package:cdb_ui/api.dart' as API;
|
|
||||||
import 'package:cdb_ui/pages/flow/active_flow_page.dart';
|
|
||||||
import 'package:cdb_ui/pages/flow/create_flow_page.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class FlowInfoPage extends StatefulWidget {
|
|
||||||
final API.FlowInfo info;
|
|
||||||
|
|
||||||
const FlowInfoPage(this.info, {super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<FlowInfoPage> createState() => _FlowInfoPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FlowInfoPageState extends State<FlowInfoPage> {
|
|
||||||
void refresh() {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(title: Text(widget.info.name)),
|
|
||||||
body: Column(
|
|
||||||
children: [
|
|
||||||
// todo : ui improve
|
|
||||||
if (widget.info.next != null)
|
|
||||||
Text("Next: ${API.API().getFlowInfo(widget.info.next!).name}"),
|
|
||||||
if (widget.info.depends.isNotEmpty)
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
|
||||||
const Text("Flow can use: "),
|
|
||||||
...widget.info.depends.map((x) {
|
|
||||||
var (item, variant) = API.itemVariant(x);
|
|
||||||
return Text(API.API().getItem(item).variants[variant]!.name);
|
|
||||||
}).toList(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (widget.info.produces?.isNotEmpty ?? false)
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
|
||||||
const Text("Flow can produce: "),
|
|
||||||
...widget.info.produces!.map((x) {
|
|
||||||
var (item, variant) = API.itemVariant(x);
|
|
||||||
return Text(API.API().getItem(item).variants[variant]!.name);
|
|
||||||
}).toList(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
FutureBuilder(
|
|
||||||
future: API.API().getActiveFlowsOf(widget.info.id),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (!snapshot.hasData) {
|
|
||||||
return const CircularProgressIndicator();
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = snapshot.data!;
|
|
||||||
|
|
||||||
return Expanded(
|
|
||||||
child: ListView(
|
|
||||||
children: data
|
|
||||||
.map((x) => ListTile(
|
|
||||||
title: Text(x.id),
|
|
||||||
onTap: () =>
|
|
||||||
Navigator.of(context).push(MaterialPageRoute(
|
|
||||||
builder: (context) =>
|
|
||||||
ActiveFlowPage(x, widget.info),
|
|
||||||
))))
|
|
||||||
.toList()),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).push(MaterialPageRoute(
|
|
||||||
builder: (context) => CreateFlowPage(widget.info, refresh)));
|
|
||||||
},
|
|
||||||
child: const Icon(Icons.add)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
import 'package:cdb_ui/pages/transaction.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:cdb_ui/api.dart' as API;
|
|
||||||
|
|
||||||
class AddNotePage extends StatelessWidget {
|
|
||||||
late final TextEditingController _noteController = TextEditingController();
|
|
||||||
final API.Flow flow;
|
|
||||||
final Function refresh;
|
|
||||||
|
|
||||||
AddNotePage(this.flow, this.refresh, {super.key});
|
|
||||||
|
|
||||||
void _submit(BuildContext context) {
|
|
||||||
API.API().addNoteToFlow(flow.id, _noteController.text).then((x) {
|
|
||||||
refresh();
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text("Add Note"),
|
|
||||||
),
|
|
||||||
body: Column(
|
|
||||||
children: [
|
|
||||||
TextFormField(
|
|
||||||
decoration: const InputDecoration(labelText: 'Note'),
|
|
||||||
controller: _noteController,
|
|
||||||
maxLines: 10),
|
|
||||||
const SizedBox(
|
|
||||||
height: 14,
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () => _submit(context), child: const Text("Add Note"))
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FlowNoteCard extends StatelessWidget {
|
|
||||||
final API.FlowNote note;
|
|
||||||
|
|
||||||
const FlowNoteCard(this.note, {super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Card(
|
|
||||||
child: ListTile(
|
|
||||||
dense: true,
|
|
||||||
title: Text(tsFormat(note.timestamp),
|
|
||||||
style: const TextStyle(fontSize: 12)),
|
|
||||||
subtitle: Text(note.content, overflow: TextOverflow.ellipsis),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,175 +0,0 @@
|
||||||
import 'package:cdb_ui/api.dart' as API;
|
|
||||||
import 'package:cdb_ui/pages/expandable_list.dart';
|
|
||||||
import 'package:cdb_ui/pages/flow/active_flow_page.dart';
|
|
||||||
import 'package:cdb_ui/pages/flow/flow_info_page.dart';
|
|
||||||
import 'package:cdb_ui/pages/supply.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class FlowsPage extends StatefulWidget {
|
|
||||||
const FlowsPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<FlowsPage> createState() => _FlowsPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FlowsPageState extends State<FlowsPage> {
|
|
||||||
int tabSelection = 0;
|
|
||||||
Map<String, API.FlowInfo>? flowInfos;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
flowInfos = API.API().getFlows();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget flowTile(BuildContext context, API.FlowInfo x) {
|
|
||||||
return ListTile(
|
|
||||||
title: Text(x.name),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).push(MaterialPageRoute(
|
|
||||||
builder: (context) => FlowInfoPage(x),
|
|
||||||
));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget listFlowInfoByActive(
|
|
||||||
BuildContext context, Map<String, API.FlowInfo> infos) {
|
|
||||||
return FutureBuilder(future: () async {
|
|
||||||
var included = [];
|
|
||||||
|
|
||||||
for (var key in infos.keys) {
|
|
||||||
var active = await API.API().getActiveFlowsOf(key);
|
|
||||||
if (active.isNotEmpty) {
|
|
||||||
included.add(infos[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return included;
|
|
||||||
}(), builder: (context, snapshot) {
|
|
||||||
if (!snapshot.hasData) {
|
|
||||||
return const CircularProgressIndicator();
|
|
||||||
}
|
|
||||||
|
|
||||||
var included = snapshot.data!;
|
|
||||||
|
|
||||||
return ListView(
|
|
||||||
children: included.map((x) => flowTile(context, x)).toList());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget listAllFlowInfos(
|
|
||||||
BuildContext context, Map<String, API.FlowInfo> infos) {
|
|
||||||
return ListView(
|
|
||||||
children: infos.values.map((x) {
|
|
||||||
return flowTile(context, x);
|
|
||||||
}).toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget listFlowInfoByProduced(
|
|
||||||
BuildContext context, Map<String, API.FlowInfo> infos) {
|
|
||||||
Map<String, List<API.FlowInfo>> producedMapping = {};
|
|
||||||
|
|
||||||
for (var f in infos.values) {
|
|
||||||
for (var produces in f.produces ?? []) {
|
|
||||||
var item = API.itemVariant(produces).$1;
|
|
||||||
producedMapping.putIfAbsent(item, () {
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
producedMapping[item]!.add(f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<ExpandableListItem> items = [];
|
|
||||||
|
|
||||||
for (var key in producedMapping.keys) {
|
|
||||||
var flows = Column(
|
|
||||||
children: producedMapping[key]!.map((x) {
|
|
||||||
return flowTile(context, x);
|
|
||||||
}).toList());
|
|
||||||
items.add(ExpandableListItem(
|
|
||||||
body: flows, header: Text(API.API().getItem(key).name)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return ExpandableList(items);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget listFlowInfoByDependant(
|
|
||||||
BuildContext context, Map<String, API.FlowInfo> infos) {
|
|
||||||
Map<String, List<API.FlowInfo>> dependsMapping = {};
|
|
||||||
|
|
||||||
for (var f in infos.values) {
|
|
||||||
for (var produces in f.depends) {
|
|
||||||
var item = API.itemVariant(produces).$1;
|
|
||||||
// todo : add only if item is in inventory
|
|
||||||
dependsMapping.putIfAbsent(item, () {
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
dependsMapping[item]!.add(f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<ExpandableListItem> items = [];
|
|
||||||
|
|
||||||
for (var key in dependsMapping.keys) {
|
|
||||||
var flows = Column(
|
|
||||||
children: dependsMapping[key]!.map((x) {
|
|
||||||
return flowTile(context, x);
|
|
||||||
}).toList());
|
|
||||||
items.add(ExpandableListItem(
|
|
||||||
body: flows, header: Text(API.API().getItem(key).name)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return ExpandableList(items);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (flowInfos == null) {
|
|
||||||
return const CircularProgressIndicator();
|
|
||||||
}
|
|
||||||
|
|
||||||
return DefaultTabController(
|
|
||||||
length: 4,
|
|
||||||
child: Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text("Flows"),
|
|
||||||
bottom: TabBar(
|
|
||||||
tabs: const [
|
|
||||||
Tab(text: "All"),
|
|
||||||
Tab(text: "Produces"),
|
|
||||||
Tab(text: "Depends"),
|
|
||||||
Tab(text: "Active")
|
|
||||||
],
|
|
||||||
onTap: (value) {
|
|
||||||
setState(() {
|
|
||||||
tabSelection = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
body: switch (tabSelection) {
|
|
||||||
0 => listAllFlowInfos(context, flowInfos!),
|
|
||||||
1 => listFlowInfoByProduced(context, flowInfos!),
|
|
||||||
2 => listFlowInfoByDependant(context, flowInfos!),
|
|
||||||
3 => listFlowInfoByActive(context, flowInfos!),
|
|
||||||
_ => const Text("..."),
|
|
||||||
},
|
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
onPressed: () async {
|
|
||||||
// scan flow code
|
|
||||||
var code = await scanQRCode(context, title: "Scan Flow Code");
|
|
||||||
|
|
||||||
API.API().getFlow(code!).then((flow) {
|
|
||||||
var info = API.API().getFlowInfo(flow.kind);
|
|
||||||
Navigator.of(context).push(MaterialPageRoute(
|
|
||||||
builder: (context) {
|
|
||||||
return ActiveFlowPage(flow, info);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: const Icon(Icons.qr_code),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,118 +0,0 @@
|
||||||
import 'package:cdb_ui/api.dart';
|
|
||||||
import 'package:cdb_ui/pages/home.dart';
|
|
||||||
import 'package:cdb_ui/pages/transaction.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'supply.dart';
|
|
||||||
|
|
||||||
// todo : show est. time remaining until inventory gets empty (based on demand)
|
|
||||||
|
|
||||||
class ItemView extends StatefulWidget {
|
|
||||||
final Item item;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ItemView> createState() => _ItemViewState();
|
|
||||||
|
|
||||||
const ItemView({super.key, required this.item});
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ItemViewState extends State<ItemView> {
|
|
||||||
void refresh() {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(title: Text(widget.item.name)),
|
|
||||||
body: Column(children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 28),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: widget.item.image != null
|
|
||||||
? Image.network(
|
|
||||||
"${API().instance}/${widget.item.image}",
|
|
||||||
height: 100,
|
|
||||||
width: 100,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 16.0,
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
widget.item.name,
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
if (widget.item.category != null)
|
|
||||||
Text(widget.item.category!),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 18),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: widget.item.variants.entries.map((entry) {
|
|
||||||
return Column(children: [
|
|
||||||
Text(
|
|
||||||
entry.value.name,
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
FutureBuilder(
|
|
||||||
future: API().getStat(widget.item.id, entry.key),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (!snapshot.hasData) {
|
|
||||||
return const CircularProgressIndicator();
|
|
||||||
}
|
|
||||||
|
|
||||||
var stat = snapshot.data!;
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Text("Amount: ${stat.amount}"),
|
|
||||||
Text(
|
|
||||||
"Total Cost: ${stat.totalPrice.toStringAsFixed(2)}")
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
}).toList()),
|
|
||||||
const SizedBox(
|
|
||||||
height: 12,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
FutureBuilder(
|
|
||||||
future: API().getInventory(widget.item.id),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (!snapshot.hasData) {
|
|
||||||
return const CircularProgressIndicator();
|
|
||||||
}
|
|
||||||
var data = snapshot.data!;
|
|
||||||
|
|
||||||
return Expanded(
|
|
||||||
child: ListView(
|
|
||||||
children:
|
|
||||||
data.map((x) => TransactionCard(x, refresh)).toList()));
|
|
||||||
},
|
|
||||||
)
|
|
||||||
]),
|
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).push(MaterialPageRoute(
|
|
||||||
builder: (context) => SupplyPage(widget.item, refresh)));
|
|
||||||
},
|
|
||||||
child: const Icon(Icons.add)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,165 +0,0 @@
|
||||||
import 'package:cdb_ui/api.dart';
|
|
||||||
import 'package:cdb_ui/pages/supply.dart';
|
|
||||||
import 'package:cdb_ui/pages/transaction.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_simple_treeview/flutter_simple_treeview.dart';
|
|
||||||
|
|
||||||
class LocationsPage extends StatefulWidget {
|
|
||||||
const LocationsPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<LocationsPage> createState() => _LocationsPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LocationsPageState extends State<LocationsPage> {
|
|
||||||
Map<String, Location>? locations;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
locations = API().getLocations();
|
|
||||||
}
|
|
||||||
|
|
||||||
TreeNode buildTree(BuildContext context, String locID) {
|
|
||||||
return TreeNode(
|
|
||||||
key: ValueKey(locID),
|
|
||||||
content: Expanded(
|
|
||||||
child: ListTile(
|
|
||||||
title: Text(locations![locID]!.name),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).push(MaterialPageRoute(
|
|
||||||
builder: (context) => LocationView(locations![locID]!),
|
|
||||||
));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
children: locations!.keys.where((key2) {
|
|
||||||
if (locations![key2]!.parent != null) {
|
|
||||||
return locations![key2]!.parent! == locations![locID]!.id;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}).map((key2) {
|
|
||||||
return buildTree(context, key2);
|
|
||||||
}).toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (locations == null) {
|
|
||||||
return const Scaffold(
|
|
||||||
body: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text("Locations"),
|
|
||||||
),
|
|
||||||
body: TreeView(
|
|
||||||
indent: 15,
|
|
||||||
nodes: locations!.keys
|
|
||||||
.where((key) => locations![key]!.parent == null)
|
|
||||||
.map((key) {
|
|
||||||
return buildTree(context, key);
|
|
||||||
}).toList()),
|
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
onPressed: () async {
|
|
||||||
// scan location code
|
|
||||||
var code = await scanQRCode(context, title: "Scan Location Code");
|
|
||||||
|
|
||||||
if (!locations!.containsKey(code)) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('The location $code does not exist.')),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Navigator.of(context).push(MaterialPageRoute(
|
|
||||||
builder: (context) => LocationView(locations![code]!),
|
|
||||||
));
|
|
||||||
},
|
|
||||||
child: const Icon(Icons.qr_code),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LocationView extends StatefulWidget {
|
|
||||||
final Location location;
|
|
||||||
|
|
||||||
const LocationView(this.location, {super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<LocationView> createState() => _LocationViewState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LocationViewState extends State<LocationView> {
|
|
||||||
bool recursive = true;
|
|
||||||
|
|
||||||
void refresh() {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(widget.location.name),
|
|
||||||
),
|
|
||||||
body: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Card(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
if (widget.location.parent != null)
|
|
||||||
Text("Inside: ${widget.location.parent!}"),
|
|
||||||
if (widget.location.conditions?.temperature != null)
|
|
||||||
Text(
|
|
||||||
"Temperature: ${widget.location.conditions!.temperature} C°")
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Checkbox(
|
|
||||||
value: !recursive,
|
|
||||||
onChanged: (bool? newValue) {
|
|
||||||
setState(() {
|
|
||||||
recursive = !(newValue ?? false);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const Expanded(
|
|
||||||
child: Text(
|
|
||||||
'Show only exact matches with location',
|
|
||||||
style: TextStyle(fontSize: 16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: FutureBuilder(
|
|
||||||
future: API().getTransactionsOfLocation(widget.location.id,
|
|
||||||
recursive: recursive),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (!snapshot.hasData) {
|
|
||||||
return const CircularProgressIndicator();
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = snapshot.data!;
|
|
||||||
|
|
||||||
return ListView(
|
|
||||||
children: data
|
|
||||||
.map((x) => TransactionCard(x, refresh))
|
|
||||||
.toList());
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,354 +0,0 @@
|
||||||
import 'package:cdb_ui/api.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:simple_barcode_scanner/enum.dart';
|
|
||||||
import 'package:simple_barcode_scanner/simple_barcode_scanner.dart';
|
|
||||||
|
|
||||||
Future<String?> scanQRCode(BuildContext context,
|
|
||||||
{String title = "Scan QR Code"}) async {
|
|
||||||
var res = await Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => SimpleBarcodeScannerPage(
|
|
||||||
scanType: ScanType.qr,
|
|
||||||
appBarTitle: title,
|
|
||||||
),
|
|
||||||
));
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
class SupplyPage extends StatefulWidget {
|
|
||||||
final Item item;
|
|
||||||
final Function refresh;
|
|
||||||
final List<String>? onlyVariants;
|
|
||||||
final String? forcePrice;
|
|
||||||
final String? forceOrigin;
|
|
||||||
|
|
||||||
// callback function for receiving a transaction without creating on the API
|
|
||||||
final Function(SupplyForm)? onCreate;
|
|
||||||
|
|
||||||
const SupplyPage(this.item, this.refresh,
|
|
||||||
{this.onlyVariants,
|
|
||||||
this.onCreate,
|
|
||||||
this.forceOrigin,
|
|
||||||
this.forcePrice,
|
|
||||||
super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<SupplyPage> createState() => _SupplyPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SupplyPageState extends State<SupplyPage> {
|
|
||||||
late List<String> availableVariants;
|
|
||||||
late String variant;
|
|
||||||
final _formKey = GlobalKey<FormState>();
|
|
||||||
String _selectedOrigin = "";
|
|
||||||
String _selectedLocation = "";
|
|
||||||
late TextEditingController _priceController;
|
|
||||||
late TextEditingController _noteController;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
availableVariants =
|
|
||||||
widget.onlyVariants ?? widget.item.variants.keys.toList();
|
|
||||||
|
|
||||||
variant = availableVariants.first;
|
|
||||||
|
|
||||||
_selectedOrigin = widget.forceOrigin ?? "";
|
|
||||||
_priceController = TextEditingController(text: widget.forcePrice ?? "");
|
|
||||||
_noteController = TextEditingController(text: "");
|
|
||||||
}
|
|
||||||
|
|
||||||
void _supply() {
|
|
||||||
if (_formKey.currentState!.validate()) {
|
|
||||||
_formKey.currentState!.save();
|
|
||||||
|
|
||||||
if (widget.onCreate != null) {
|
|
||||||
var t = SupplyForm(
|
|
||||||
itemID: widget.item.id,
|
|
||||||
variant: variant,
|
|
||||||
price: double.parse(_priceController.text),
|
|
||||||
origin: _selectedOrigin,
|
|
||||||
location: _selectedLocation,
|
|
||||||
note: _noteController.text);
|
|
||||||
|
|
||||||
widget.onCreate!(t);
|
|
||||||
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
widget.refresh();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
API()
|
|
||||||
.supplyItem(
|
|
||||||
widget.item.id,
|
|
||||||
variant,
|
|
||||||
double.parse(_priceController.text),
|
|
||||||
_selectedOrigin,
|
|
||||||
_selectedLocation,
|
|
||||||
_noteController.text)
|
|
||||||
.then((_) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('Item added successfully!')),
|
|
||||||
);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
widget.refresh();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text('Add New Item'),
|
|
||||||
),
|
|
||||||
body: FutureBuilder(
|
|
||||||
future: _fetchData(),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (!snapshot.hasData) {
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = snapshot.data as Map<String, dynamic>;
|
|
||||||
var locations = data['locations']! as Map<String, Location>;
|
|
||||||
var origins = data['origins']! as List<String>;
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Form(
|
|
||||||
key: _formKey,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Variant Selection
|
|
||||||
DropdownButtonFormField<String>(
|
|
||||||
hint: const Text('Select Variant'),
|
|
||||||
value: variant,
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
variant = value!;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
items: availableVariants.map((entryKey) {
|
|
||||||
var entry = widget.item.variants[entryKey]!;
|
|
||||||
return DropdownMenuItem<String>(
|
|
||||||
value: entryKey,
|
|
||||||
child: Text(entry.name),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
onSaved: (value) {
|
|
||||||
variant = value!;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Origin Field with Dropdown and Text Input
|
|
||||||
if (widget.forceOrigin == null) ...[
|
|
||||||
AutocompletedTextField(
|
|
||||||
options: origins,
|
|
||||||
getValue: () => _selectedOrigin,
|
|
||||||
onChanged: (value) {
|
|
||||||
_selectedOrigin = value;
|
|
||||||
},
|
|
||||||
onSelection: (String selection) async {
|
|
||||||
var price = _priceController.text.isEmpty
|
|
||||||
? await API()
|
|
||||||
.getLatestPrice(widget.item.id, variant,
|
|
||||||
origin: selection)
|
|
||||||
.then((x) => x.toStringAsFixed(2))
|
|
||||||
: _priceController.text;
|
|
||||||
setState(() {
|
|
||||||
_priceController.text = price;
|
|
||||||
_selectedOrigin = selection;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
label: "Origin"),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
|
||||||
|
|
||||||
// Price Field
|
|
||||||
if (widget.forcePrice == null) ...[
|
|
||||||
TextFormField(
|
|
||||||
decoration: const InputDecoration(labelText: 'Price'),
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
controller: _priceController,
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'Please enter a price';
|
|
||||||
}
|
|
||||||
if (double.tryParse(value) == null) {
|
|
||||||
return 'Please enter a valid number';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
|
||||||
|
|
||||||
// Location Dropdown
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: DropdownButtonFormField<String>(
|
|
||||||
hint: const Text('Select Location'),
|
|
||||||
value: _selectedLocation,
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_selectedLocation = value!;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
items: locations.keys
|
|
||||||
.map<DropdownMenuItem<String>>((id) {
|
|
||||||
return DropdownMenuItem<String>(
|
|
||||||
value: id,
|
|
||||||
child:
|
|
||||||
Text(locations[id]!.fullNamePath(locations)),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
onSaved: (value) {
|
|
||||||
_selectedLocation = value!;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 12,
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
var code = await scanQRCode(context);
|
|
||||||
setState(() {
|
|
||||||
if (API().getLocations().keys.contains(code)) {
|
|
||||||
_selectedLocation = code!;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.qr_code),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
// Note
|
|
||||||
TextFormField(
|
|
||||||
decoration: const InputDecoration(labelText: 'Note'),
|
|
||||||
controller: _noteController,
|
|
||||||
maxLines: 5),
|
|
||||||
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
// Submit Button
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: _supply,
|
|
||||||
child: const Text('Add Item'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Map<String, dynamic>> _fetchData() async {
|
|
||||||
var locations = API().getLocations();
|
|
||||||
var origins = await API().getUniqueField(widget.item.id, variant, "origin");
|
|
||||||
origins.insert(0, "");
|
|
||||||
locations[""] = Location.zero();
|
|
||||||
return {'locations': locations, 'origins': origins};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
|
||||||
class AutocompletedTextField extends StatelessWidget {
|
|
||||||
late List<String> options;
|
|
||||||
late Function(String) onChanged;
|
|
||||||
late String Function() getValue;
|
|
||||||
late Function(String) onSelection;
|
|
||||||
late String label;
|
|
||||||
|
|
||||||
AutocompletedTextField(
|
|
||||||
{super.key,
|
|
||||||
required this.options,
|
|
||||||
required this.getValue,
|
|
||||||
required this.onChanged,
|
|
||||||
required this.onSelection,
|
|
||||||
required this.label});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Autocomplete<String>(
|
|
||||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
|
||||||
if (textEditingValue.text.isEmpty) {
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
return options.where((String option) {
|
|
||||||
return option
|
|
||||||
.toLowerCase()
|
|
||||||
.contains(textEditingValue.text.toLowerCase());
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onSelected: onSelection,
|
|
||||||
fieldViewBuilder:
|
|
||||||
(context, textEditingController, focusNode, onFieldSubmitted) {
|
|
||||||
textEditingController.text = getValue();
|
|
||||||
return TextFormField(
|
|
||||||
onChanged: onChanged,
|
|
||||||
controller: textEditingController,
|
|
||||||
focusNode: focusNode,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: label,
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SupplyForm {
|
|
||||||
final String itemID;
|
|
||||||
final String variant;
|
|
||||||
final double price;
|
|
||||||
final String? origin;
|
|
||||||
final String? location;
|
|
||||||
final String note;
|
|
||||||
|
|
||||||
factory SupplyForm.fromJson(Map<String, dynamic> json) {
|
|
||||||
return SupplyForm(
|
|
||||||
itemID: json['item'],
|
|
||||||
variant: json['variant'],
|
|
||||||
price: json['price'],
|
|
||||||
origin: json['origin'],
|
|
||||||
location: json['location'],
|
|
||||||
note: json['note'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> json() {
|
|
||||||
return {
|
|
||||||
"item": itemID,
|
|
||||||
"variant": variant,
|
|
||||||
"price": price,
|
|
||||||
"origin": origin,
|
|
||||||
"location": location,
|
|
||||||
"note": note
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
SupplyForm({
|
|
||||||
required this.itemID,
|
|
||||||
required this.variant,
|
|
||||||
required this.price,
|
|
||||||
required this.origin,
|
|
||||||
required this.location,
|
|
||||||
required this.note,
|
|
||||||
});
|
|
||||||
|
|
||||||
Transaction transaction(Map<String, Location> locations) {
|
|
||||||
return Transaction.inMemory(itemID, variant, price, origin,
|
|
||||||
location != null ? locations[location!] : null, note);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,378 +0,0 @@
|
||||||
import 'package:cdb_ui/api.dart';
|
|
||||||
import 'package:cdb_ui/pages/consume.dart';
|
|
||||||
import 'package:cdb_ui/pages/supply.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:qr_bar_code/qr/qr.dart';
|
|
||||||
|
|
||||||
class TransactionPage extends StatefulWidget {
|
|
||||||
final Transaction transaction;
|
|
||||||
final Function? refresh;
|
|
||||||
|
|
||||||
const TransactionPage(this.transaction, {this.refresh, super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<TransactionPage> createState() => _TransactionPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TransactionPageState extends State<TransactionPage> {
|
|
||||||
late Transaction transaction;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
transaction = widget.transaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> reload() async {
|
|
||||||
if (widget.refresh != null) {
|
|
||||||
widget.refresh!();
|
|
||||||
}
|
|
||||||
|
|
||||||
var updateTransaction = await API().getTransaction(transaction.uuid);
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
transaction = updateTransaction;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(API().getItem(transaction.item).name),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
var locations = API().getLocations();
|
|
||||||
List<String> locationList = locations.keys.toList();
|
|
||||||
String? selectedLocationID;
|
|
||||||
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: const Text('Move Transaction'),
|
|
||||||
content: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
DropdownButton<String>(
|
|
||||||
value: selectedLocationID,
|
|
||||||
onChanged: (value) {
|
|
||||||
selectedLocationID = value!;
|
|
||||||
API()
|
|
||||||
.moveTransaction(widget.transaction.uuid,
|
|
||||||
selectedLocationID!)
|
|
||||||
.then((x) {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
});
|
|
||||||
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
items: locationList
|
|
||||||
.map<DropdownMenuItem<String>>((locationID) {
|
|
||||||
return DropdownMenuItem<String>(
|
|
||||||
value: locationID,
|
|
||||||
child: Text(locations[locationID]!.name),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
var locations = API().getLocations();
|
|
||||||
var code = await scanQRCode(context,
|
|
||||||
title: "Scan Location Code");
|
|
||||||
if (!locations.containsKey(code)) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
|
||||||
'The location $code does not exist.')),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
API()
|
|
||||||
.moveTransaction(widget.transaction.uuid,
|
|
||||||
selectedLocationID!)
|
|
||||||
.then(
|
|
||||||
(x) {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.qr_code))
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.move_up))
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
QRCode(
|
|
||||||
data: transaction.uuid,
|
|
||||||
size: 128,
|
|
||||||
eyeStyle: const QREyeStyle(color: Colors.white),
|
|
||||||
dataModuleStyle: const QRDataModuleStyle(color: Colors.white),
|
|
||||||
semanticsLabel: "Transaction UUID",
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 16.0,
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
|
||||||
// todo : human names
|
|
||||||
Text(
|
|
||||||
API().getItem(transaction.item).name,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold, fontSize: 28),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
API()
|
|
||||||
.getItem(transaction.item)
|
|
||||||
.variants[transaction.variant]!
|
|
||||||
.name,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 20,
|
|
||||||
color: Colors.grey),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
Text("Added: ${tsFormat(transaction.timestamp)}"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
const SizedBox(
|
|
||||||
height: 12.0,
|
|
||||||
),
|
|
||||||
|
|
||||||
if (transaction.expired) const Text("Transaction is Expired!"),
|
|
||||||
|
|
||||||
IconText(Icons.money, transaction.price.toStringAsFixed(2),
|
|
||||||
color: Colors.green),
|
|
||||||
|
|
||||||
if (transaction.origin != null)
|
|
||||||
IconText(Icons.store, transaction.origin!, color: Colors.blue),
|
|
||||||
|
|
||||||
if (transaction.location != null)
|
|
||||||
IconText(Icons.location_city, transaction.location!.name),
|
|
||||||
|
|
||||||
if (transaction.note != null) Text(transaction.note!),
|
|
||||||
|
|
||||||
if (transaction.consumed != null) ...[
|
|
||||||
const Divider(),
|
|
||||||
Text("Consumed on: ${tsFormat(transaction.consumed!.timestamp)}"),
|
|
||||||
IconText(Icons.store, transaction.consumed!.destination,
|
|
||||||
color: Colors.blue),
|
|
||||||
IconText(
|
|
||||||
Icons.money, transaction.consumed!.price.toStringAsFixed(2),
|
|
||||||
color: Colors.green),
|
|
||||||
]
|
|
||||||
|
|
||||||
// todo : chart with price history
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
floatingActionButton: transaction.consumed == null
|
|
||||||
? FloatingActionButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).push(MaterialPageRoute(
|
|
||||||
builder: (context) {
|
|
||||||
return ConsumePage(transaction, reload);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
},
|
|
||||||
child: const Icon(Icons.receipt_long))
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TransactionCard extends StatelessWidget {
|
|
||||||
final Transaction t;
|
|
||||||
final Function refresh;
|
|
||||||
final Function(Transaction)? onTap;
|
|
||||||
final Function(Transaction)? onLongPress;
|
|
||||||
|
|
||||||
const TransactionCard(this.t, this.refresh,
|
|
||||||
{this.onTap, this.onLongPress, super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return InkWell(
|
|
||||||
onTap: () {
|
|
||||||
if (onTap != null) {
|
|
||||||
onTap!(t);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Navigator.of(context).push(MaterialPageRoute(
|
|
||||||
builder: (context) {
|
|
||||||
return ConsumePage(t, refresh);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
},
|
|
||||||
onLongPress: () {
|
|
||||||
if (onLongPress != null) {
|
|
||||||
onLongPress!(t);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Navigator.of(context).push(MaterialPageRoute(
|
|
||||||
builder: (context) {
|
|
||||||
return TransactionPage(t);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
},
|
|
||||||
child: Card(
|
|
||||||
color: t.expired ? Colors.red[100] : Colors.black,
|
|
||||||
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
elevation: 4,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
API().getItem(t.item).name,
|
|
||||||
style: const TextStyle(fontSize: 16),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 8,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
API().getItem(t.item).variants[t.variant]!.name,
|
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey[400]),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (t.timestamp != 0)
|
|
||||||
Text(
|
|
||||||
tsFormat(t.timestamp),
|
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if ((t.note ?? "").isNotEmpty) ...[
|
|
||||||
const SizedBox(
|
|
||||||
height: 4,
|
|
||||||
),
|
|
||||||
Text(t.note!)
|
|
||||||
],
|
|
||||||
const SizedBox(
|
|
||||||
height: 10,
|
|
||||||
),
|
|
||||||
IconText(Icons.money, "${t.price.toStringAsFixed(2)} €",
|
|
||||||
color: Colors.green),
|
|
||||||
if (t.origin != null) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
IconText(Icons.store, t.origin!, color: Colors.blue)
|
|
||||||
],
|
|
||||||
if (t.location != null) ...[
|
|
||||||
const SizedBox(
|
|
||||||
height: 8,
|
|
||||||
),
|
|
||||||
IconText(Icons.location_city, t.location!.name)
|
|
||||||
]
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String tsFormat(int ts) {
|
|
||||||
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(ts * 1000);
|
|
||||||
return DateFormat('yyyy-MM-dd HH:mm:ss').format(dateTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
class IconText extends StatelessWidget {
|
|
||||||
final IconData icon;
|
|
||||||
final String text;
|
|
||||||
final Color? color;
|
|
||||||
|
|
||||||
const IconText(this.icon, this.text, {super.key, this.color});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Icon(icon, size: 18, color: color),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(
|
|
||||||
text,
|
|
||||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TransactionSelectPage extends StatelessWidget {
|
|
||||||
final Function(Transaction) onSelect;
|
|
||||||
final List<Transaction> selections;
|
|
||||||
final List<Transaction>? exclude;
|
|
||||||
|
|
||||||
const TransactionSelectPage(this.selections,
|
|
||||||
{super.key, required this.onSelect, this.exclude});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var selectionList = [];
|
|
||||||
|
|
||||||
for (var s in selections) {
|
|
||||||
if (exclude?.any((x) => x.uuid == s.uuid) ?? false) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectionList.add(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text("Select a Transaction"),
|
|
||||||
),
|
|
||||||
body: ListView(
|
|
||||||
children: selectionList.isEmpty
|
|
||||||
? [
|
|
||||||
const ListTile(
|
|
||||||
title: Center(child: Text("No Transactions available")),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
: selectionList
|
|
||||||
.map((x) => TransactionCard(
|
|
||||||
x,
|
|
||||||
() {},
|
|
||||||
onLongPress: (x) {},
|
|
||||||
onTap: (t) {
|
|
||||||
onSelect(t);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
))
|
|
||||||
.toList()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
10
src/setup.rs
10
src/setup.rs
|
@ -1,8 +1,9 @@
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus_sdk::storage::{new_storage, use_persistent, SessionStorage};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::store::save_to_local_storage;
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize, Clone, PartialEq)]
|
#[derive(Default, Serialize, Deserialize, Clone, PartialEq)]
|
||||||
pub struct Credentials {
|
pub struct Credentials {
|
||||||
pub instance_url: String,
|
pub instance_url: String,
|
||||||
|
@ -45,15 +46,14 @@ pub fn SetupPage() -> Element {
|
||||||
evt.prevent_default();
|
evt.prevent_default();
|
||||||
|
|
||||||
if validate_form() {
|
if validate_form() {
|
||||||
let mut creds =
|
let creds = Credentials::default();
|
||||||
new_storage::<SessionStorage, _>("creds".to_string(), || Credentials::default());
|
|
||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let api = crate::api::API::new(creds.read().instance_url.clone()).await;
|
let api = crate::api::API::new(creds.instance_url.clone()).await;
|
||||||
*crate::API.write() = Some(api);
|
*crate::API.write() = Some(api);
|
||||||
});
|
});
|
||||||
|
|
||||||
creds.set(form_data.read().clone());
|
save_to_local_storage("creds", form_data.read().clone());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
24
src/store.rs
Normal file
24
src/store.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use web_sys::window;
|
||||||
|
|
||||||
|
pub fn save_to_local_storage_str(key: &str, value: &str) {
|
||||||
|
let window = window().expect("no global `window` exists");
|
||||||
|
let storage = window.local_storage().unwrap().unwrap();
|
||||||
|
storage.set_item(key, value).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_from_local_storage_str(key: &str) -> Option<String> {
|
||||||
|
let window = window().expect("no global `window` exists");
|
||||||
|
let storage = window.local_storage().unwrap().unwrap();
|
||||||
|
storage.get_item(key).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_to_local_storage<T: Serialize>(key: &str, value: T) {
|
||||||
|
let val = serde_json::to_string(&value).unwrap();
|
||||||
|
save_to_local_storage_str(key, &val);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_from_local_storage<T: for<'a> Deserialize<'a>>(key: &str) -> Option<T> {
|
||||||
|
let val = load_from_local_storage_str(key)?;
|
||||||
|
serde_json::from_str(&val).ok()
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue