From f6a3fb9c4d8dd86f78c9f75a23c1ac35bf35d4eb Mon Sep 17 00:00:00 2001 From: Benedikt Peetz Date: Thu, 19 Mar 2026 07:45:14 +0100 Subject: feat(treewide): Commit MVP --- .gitignore | 1 + Cargo.lock | 86 +++++++- Cargo.toml | 2 +- Trunk.toml | 7 +- flake.lock | 34 ++- flake.nix | 24 ++- index.html | 10 + nix/package.nix | 69 ++++++ public/manifest.json | 13 ++ rocie-macros/src/form/generate.rs | 136 ++++++++++-- rocie-macros/src/form/mod.rs | 11 +- rocie-macros/src/form/parse.rs | 18 ++ scripts/setup.sh | 6 + setup/Cargo.lock | 7 + setup/Cargo.toml | 6 + setup/src/main.rs | 55 +++++ src/api/mod.rs | 221 +++++++++++++++++-- src/components/async_fetch.rs | 12 +- src/components/buy.rs | 1 + src/components/catch_errors.rs | 40 ++++ src/components/checkbox_placeholder.rs | 54 +++++ src/components/container.rs | 52 ++--- src/components/inventory.rs | 4 +- src/components/login_wall.rs | 42 ++++ src/components/mod.rs | 7 + src/components/product_overview.rs | 19 +- src/components/product_parent_overview.rs | 37 ++++ src/components/recipies.rs | 20 +- src/components/textarea_placeholder.rs | 60 ++++++ src/components/unit_overview.rs | 37 +++- src/lib.rs | 60 +++++- src/pages/associate_barcode.rs | 96 +++++---- src/pages/buy.rs | 63 ++++-- src/pages/create_product.rs | 109 +++++++--- src/pages/create_product_parent.rs | 126 +++++++++++ src/pages/create_recipe.rs | 108 ++++++++++ src/pages/home.rs | 46 ++-- src/pages/inventory.rs | 68 +++--- src/pages/login.rs | 81 +++++++ src/pages/mod.rs | 83 ++++++- src/pages/not_found.rs | 7 +- src/pages/product.rs | 52 +++++ src/pages/products.rs | 68 ++++++ src/pages/provision.rs | 93 ++++++++ src/pages/recipe.rs | 344 ++++++++++++++++++++++++++++++ src/pages/recipies.rs | 74 ++++++- src/pages/units.rs | 78 +++++++ 47 files changed, 2367 insertions(+), 280 deletions(-) create mode 100644 nix/package.nix create mode 100644 public/manifest.json create mode 100755 scripts/setup.sh create mode 100644 setup/Cargo.lock create mode 100644 setup/Cargo.toml create mode 100644 setup/src/main.rs create mode 100644 src/components/catch_errors.rs create mode 100644 src/components/checkbox_placeholder.rs create mode 100644 src/components/login_wall.rs create mode 100644 src/components/product_parent_overview.rs create mode 100644 src/components/textarea_placeholder.rs create mode 100644 src/pages/create_product_parent.rs create mode 100644 src/pages/create_recipe.rs create mode 100644 src/pages/login.rs create mode 100644 src/pages/product.rs create mode 100644 src/pages/products.rs create mode 100644 src/pages/provision.rs create mode 100644 src/pages/recipe.rs create mode 100644 src/pages/units.rs diff --git a/.gitignore b/.gitignore index f608a06..8914bee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target +/setup/target /dist /public/tailwindcss-output.css diff --git a/Cargo.lock b/Cargo.lock index 645f23d..70883c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -289,6 +289,35 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fc4bff745c9b4c7fb1e97b25d13153da2bc7796260141df62378998d070207f" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -411,6 +440,15 @@ dependencies = [ "syn", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "drain_filter_polyfill" version = "0.1.3" @@ -1215,6 +1253,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -1508,6 +1552,22 @@ dependencies = [ "yansi", ] +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + [[package]] name = "quote" version = "1.0.41" @@ -1658,12 +1718,14 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.24" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", + "cookie", + "cookie_store", "futures-core", "futures-util", "http", @@ -1693,6 +1755,8 @@ dependencies = [ [[package]] name = "rocie-client" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3bb4f4858511444da651f93844df66405447bc778545b096cc7214353e34929" dependencies = [ "reqwest", "serde", @@ -1919,9 +1983,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.15.1" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" dependencies = [ "base64", "chrono", @@ -1938,9 +2002,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.15.1" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" dependencies = [ "darling", "proc-macro2", @@ -2294,9 +2358,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags", "bytes", @@ -2423,13 +2487,13 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] diff --git a/Cargo.toml b/Cargo.toml index eb9e795..9749bee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ web-sys = { version = "0.3", features = ["Document", "Window"] } [dependencies] rocie-macros = { path = "./rocie-macros/" } -rocie-client = { path = "../rocie-server/crates/rocie-client" } +rocie-client = { version = "0.1" } leptos = { version = "0.8", features = ["csr"] } reactive_stores = { version = "0.2" } leptos_meta = { version = "0.8" } diff --git a/Trunk.toml b/Trunk.toml index 68c3bf0..c16525c 100644 --- a/Trunk.toml +++ b/Trunk.toml @@ -1,10 +1,7 @@ [[hooks]] stage = "pre_build" -command = "sh" -command_arguments = [ - "-c", - "tailwindcss -i input.css -o public/tailwindcss-output.css", -] +command = "tailwindcss" +command_arguments = ["-i", "input.css", "-o", "public/tailwindcss-output.css"] [serve] addresses = ["127.0.0.1"] diff --git a/flake.lock b/flake.lock index 6f9f237..a0a10e6 100644 --- a/flake.lock +++ b/flake.lock @@ -1,12 +1,27 @@ { "nodes": { + "crane": { + "locked": { + "lastModified": 1773857772, + "narHash": "sha256-5xsK26KRHf0WytBtsBnQYC/lTWDhQuT57HJ7SzuqZcM=", + "owner": "ipetkov", + "repo": "crane", + "rev": "b556d7bbae5ff86e378451511873dfd07e4504cd", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1761164809, - "narHash": "sha256-3uM91Lx9WZomE6MMEBorJyEyBNiHWRIxza/GganDxew=", + "lastModified": 1773826178, + "narHash": "sha256-RwA0KkNaCDBMDGYef/OjG3Z5B5oRTuV6Zy1iPk3F8Ro=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "3d2db9755e7815937fb7b8f089fad9b44bc416d8", + "rev": "af90506ab0acf18cfd6449225c32c096138cba52", "type": "github" }, "original": { @@ -18,6 +33,7 @@ }, "root": { "inputs": { + "crane": "crane", "nixpkgs": "nixpkgs", "rust-overlay": "rust-overlay", "treefmt-nix": "treefmt-nix" @@ -30,11 +46,11 @@ ] }, "locked": { - "lastModified": 1761100675, - "narHash": "sha256-LX3TCDBeNpCWTDXtGyRASVcLmRPChSli34bgHnZ1DCw=", + "lastModified": 1773889863, + "narHash": "sha256-tSsmZOHBgq4qfu5MNCAEsKZL1cI4avNLw2oUTXWeb74=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "72161c6c53f6e3f8dadaf54b2204a5094c6a16ae", + "rev": "dbfd51be2692cb7022e301d14c139accb4ee63f0", "type": "github" }, "original": { @@ -50,11 +66,11 @@ ] }, "locked": { - "lastModified": 1760945191, - "narHash": "sha256-ZRVs8UqikBa4Ki3X4KCnMBtBW0ux1DaT35tgsnB1jM4=", + "lastModified": 1773297127, + "narHash": "sha256-6E/yhXP7Oy/NbXtf1ktzmU8SdVqJQ09HC/48ebEGBpk=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "f56b1934f5f8fcab8deb5d38d42fd692632b47c2", + "rev": "71b125cd05fbfd78cab3e070b73544abe24c5016", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 776e8bd..425a1a9 100644 --- a/flake.nix +++ b/flake.nix @@ -26,6 +26,7 @@ nixpkgs.follows = "nixpkgs"; }; }; + crane.url = "github:ipetkov/crane"; }; outputs = { @@ -33,6 +34,7 @@ nixpkgs, treefmt-nix, rust-overlay, + crane, }: let system = "x86_64-linux"; pkgs = import nixpkgs { @@ -48,11 +50,23 @@ pkgs.pkg-config ]; - rocie = pkgs.callPackage ./nix/package.nix {}; + rust = + pkgs.rust-bin.stable.latest.default.override { + targets = ["wasm32-unknown-unknown"]; + }; - rust = pkgs.rust-bin.stable.latest.default.override { - targets = ["wasm32-unknown-unknown"]; - }; + # NB: we don't need to overlay our custom toolchain for the *entire* + # pkgs (which would require rebuidling anything else which uses rust). + # Instead, we just want to update the scope that crane will use by appending + # our specific toolchain there. + craneLib = (crane.mkLib pkgs).overrideToolchain ( + p: + p.rust-bin.stable.latest.default.override { + targets = ["wasm32-unknown-unknown"]; + } + ); + + rocie = pkgs.callPackage ./nix/package.nix {inherit craneLib;}; treefmtEval = import ./treefmt.nix {inherit treefmt-nix pkgs;}; in { @@ -74,7 +88,7 @@ packages = [ # rust stuff rust - pkgs.mold-wrapped + pkgs.mold pkgs.cargo-edit pkgs.cargo-expand diff --git a/index.html b/index.html index 6fe87e8..7085f94 100644 --- a/index.html +++ b/index.html @@ -27,6 +27,16 @@ rel="css" href="./public/tailwindcss-output.css" /> + + + diff --git a/nix/package.nix b/nix/package.nix new file mode 100644 index 0000000..1bcc0ae --- /dev/null +++ b/nix/package.nix @@ -0,0 +1,69 @@ +# rocie - An enterprise grocery management system +# +# Copyright (C) 2024 Benedikt Peetz +# Copyright (C) 2025 Benedikt Peetz +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of Rocie. +# +# You should have received a copy of the License along with this program. +# If not, see . +{ + lib, + craneLib, + # nativeBuildInputs + trunk, + tailwindcss, + wasm-bindgen-cli_0_2_104, + binaryen, +}: +craneLib.buildPackage { + pname = "rocie-mobile"; + inherit + ((builtins.fromTOML (builtins.readFile + ../Cargo.toml)).package) + version + ; + + src = lib.cleanSourceWith { + src = lib.cleanSource ./..; + filter = name: type: + (type == "directory") + || (builtins.elem (builtins.baseNameOf name) [ + "Cargo.toml" + "Cargo.lock" + "tailwind.config.js" + "index.html" + "input.css" + "Trunk.toml" + "manifest.json" + "favicon.ico" + ]) + || (lib.strings.hasSuffix ".rs" (builtins.baseNameOf name)); + }; + strictDeps = true; + + cargoExtraArgs = "--target wasm32-unknown-unknown"; + + # Tests currently need to be run via `cargo wasi` which + # isn't packaged in nixpkgs yet... + doCheck = false; + + nativeBuildInputs = [ + trunk + tailwindcss + wasm-bindgen-cli_0_2_104 + binaryen # for wasm-opt + ]; + + buildInputs = [ + ]; + + postInstall = '' + trunk --offline --verbose build --release --locked --frozen --dist "./dist" + + rm --recursive $out/bin + + cp --recursive ./dist/. $out/ + ''; +} diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..5ea8b9d --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,13 @@ +{ + "name": "Rocie", + "icons": [ + { + "src": "favicon.ico", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": "/", + "display": "standalone", + "prefer_related_applications": false +} diff --git a/rocie-macros/src/form/generate.rs b/rocie-macros/src/form/generate.rs index 5642e6a..89acea8 100644 --- a/rocie-macros/src/form/generate.rs +++ b/rocie-macros/src/form/generate.rs @@ -1,7 +1,7 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote}; -use syn::{Ident, Type, parse_macro_input}; +use syn::{Ident, Path, Type, TypePath, parse_macro_input, punctuated::Punctuated}; use crate::form::{ParsedChild, ParsedInput}; @@ -33,7 +33,9 @@ pub fn form_impl(item: TokenStream) -> TokenStream { let output = quote!({ use crate::components::{ input_placeholder::InputPlaceholder, - select_placeholder::SelectPlaceholder + select_placeholder::SelectPlaceholder, + textarea_placeholder::TextareaPlaceholder, + checkbox_placeholder::CheckboxPlaceholder, }; use leptos::{ @@ -48,7 +50,7 @@ pub fn form_impl(item: TokenStream) -> TokenStream { Set, Show, }, - html::{Input, Select}, + html::{Input, Select, Textarea}, web_sys::SubmitEvent }; @@ -126,6 +128,8 @@ fn node_ref(child: &ParsedChild) -> TokenStream2 { let (name, ty) = match child { ParsedChild::Input { name, .. } => (name, quote! {Input}), ParsedChild::Select { name, .. } => (name, quote! {Select}), + ParsedChild::Textarea { name, .. } => (name, quote! {Textarea}), + ParsedChild::Checkbox { name, .. } => (name, quote! {Input}), ParsedChild::Show { .. } => unreachable!("Filtered out before"), }; @@ -149,7 +153,9 @@ fn signal(child: &ParsedChild) -> TokenStream2 { let (#signal_name_get, #signal_name_set) = signal(None); } } - ParsedChild::Select { .. } => quote! {}, + ParsedChild::Textarea { .. } + | ParsedChild::Checkbox { .. } + | ParsedChild::Select { .. } => quote! {}, ParsedChild::Show { children, .. } => children.iter().map(signal).collect(), } } @@ -185,7 +191,9 @@ fn auto_complete_signal(child: &ParsedChild) -> TokenStream2 { quote! {} } } - ParsedChild::Select { .. } => quote! {}, + ParsedChild::Select { .. } + | ParsedChild::Textarea { .. } + | ParsedChild::Checkbox { .. } => quote! {}, ParsedChild::Show { children, .. } => children.iter().map(auto_complete_signal).collect(), } } @@ -238,14 +246,16 @@ fn reactive_signal(child: &ParsedChild) -> TokenStream2 { quote! {} } } - ParsedChild::Select { .. } => quote! {}, + ParsedChild::Select { .. } + | ParsedChild::Textarea { .. } + | ParsedChild::Checkbox { .. } => quote! {}, ParsedChild::Show { children, .. } => children.iter().map(reactive_signal).collect(), } } fn type_verification(child: &ParsedChild) -> TokenStream2 { match child { - ParsedChild::Input { .. } => { + ParsedChild::Input { .. } | ParsedChild::Textarea { .. } | ParsedChild::Checkbox { .. } => { quote! {} } ParsedChild::Select { name, options, .. } => { @@ -278,24 +288,65 @@ fn type_verification(child: &ParsedChild) -> TokenStream2 { fn get_names(input: &ParsedChild) -> Vec<&Ident> { match input { - ParsedChild::Input { name, .. } => vec![name], - ParsedChild::Select { name, .. } => vec![name], + ParsedChild::Input { name, .. } + | ParsedChild::Textarea { name, .. } + | ParsedChild::Checkbox { name, .. } + | ParsedChild::Select { name, .. } => vec![name], ParsedChild::Show { children, .. } => children.iter().flat_map(get_names).collect(), } } fn output_struct_definition(children: &[ParsedChild]) -> TokenStream2 { - fn get_rust_types(input: &ParsedChild) -> Vec<&Type> { + macro_rules! mk_type { + ($name:ident) => {{ + let segments = { + let mut p = Punctuated::new(); + p.push(syn::PathSegment { + ident: format_ident!(stringify!($name)), + arguments: syn::PathArguments::None, + }); + p + }; + + Type::Path(TypePath { + qself: None, + path: Path { + leading_colon: None, + segments, + }, + }) + }}; + } + + let string: Type = mk_type!(String); + let boolean: Type = mk_type!(bool); + + fn get_rust_types<'a>( + input: &'a ParsedChild, + string: &'a Type, + bool: &'a Type, + ) -> Vec<&'a Type> { match input { - ParsedChild::Input { rust_type, .. } => vec![rust_type], - ParsedChild::Select { rust_type, .. } => vec![rust_type], - ParsedChild::Show { children, .. } => { - children.iter().flat_map(get_rust_types).collect() + ParsedChild::Textarea { .. } => { + vec![string] } + ParsedChild::Checkbox { .. } => { + vec![bool] + } + ParsedChild::Input { rust_type, .. } | ParsedChild::Select { rust_type, .. } => { + vec![rust_type] + } + ParsedChild::Show { children, .. } => children + .iter() + .flat_map(|i| get_rust_types(i, string, bool)) + .collect(), } } + let names = children.iter().flat_map(get_names); - let rust_types = children.iter().flat_map(get_rust_types); + let rust_types = children + .iter() + .flat_map(|i| get_rust_types(i, &string, &boolean)); quote! { struct Inputs { @@ -358,6 +409,42 @@ fn fetch_value(child: &ParsedChild) -> TokenStream2 { }; } } + ParsedChild::Textarea { name, .. } => { + let node_ref_name = node_ref_name(name); + + quote! { + let #name: String = { + let output = #node_ref_name + .get() + // event handlers can only fire after the view + // is mounted to the DOM, so the `NodeRef` will be `Some` + .expect(" + + + + } +} diff --git a/src/components/unit_overview.rs b/src/components/unit_overview.rs index 25e5675..0ea3825 100644 --- a/src/components/unit_overview.rs +++ b/src/components/unit_overview.rs @@ -1,6 +1,10 @@ use leptos::{IntoView, component, view}; +use rocie_client::models::{Unit, UnitProperty}; -use crate::components::{container::Container, icon_p::IconP}; +use crate::{ + api::{unit_properties_wrapped, units_wrapped}, + components::{async_fetch::AsyncFetch, container::Container, icon_p::IconP}, +}; #[component] pub fn UnitOverview() -> impl IntoView { @@ -8,14 +12,37 @@ pub fn UnitOverview() -> impl IntoView { }, "units"), - (view! { }, "create-unit"), - (view! { }, "create-unit-property"), + (view! { }, "units"), + ( + view! { }, + "create-unit", + ), + ( + view! { }, + "create-unit-property", + ), ] > { - "You have units" + AsyncFetch! { + @map_error_in_producer + fetcher = get_units_and_unit_properties(), + producer = |(units, unit_properties)| { + view! { +

{move || format!( + "You have {} units and {} unit properties.", + units.len(), + unit_properties.len() + )}

+ } + }, + } }
} } + +async fn get_units_and_unit_properties() +-> Result<(Vec, Vec), leptos::error::Error> { + Ok((units_wrapped().await?, unit_properties_wrapped().await?)) +} diff --git a/src/lib.rs b/src/lib.rs index 36210e7..a884201 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,7 +31,10 @@ use reactive_stores::Store; use rocie_client::apis::configuration::Configuration; use crate::pages::{ - associate_barcode::AssociateBarcode, buy::Buy, create_product::CreateProduct, home::Home, inventory::Inventory, not_found::NotFound, recipies::Recipies + associate_barcode::AssociateBarcode, buy::Buy, create_product::CreateProduct, + create_product_parent::CreateProductParent, create_recipe::CreateRecipe, home::Home, + inventory::Inventory, login::Login, not_found::NotFound, product::Product, products::Products, + provision::Provision, recipe::Recipe, recipies::Recipies, units::Units, }; #[derive(Debug, Clone, Store)] @@ -40,6 +43,7 @@ pub struct ConfigState { } #[component] +#[expect(clippy::too_many_lines)] pub fn App() -> impl IntoView { // Provides context that manages stylesheets, titles, meta tags, etc. provide_meta_context(); @@ -48,7 +52,7 @@ pub fn App() -> impl IntoView { let mut config = Configuration::new(); config.user_agent = Some("rocie-mobile".to_owned()); - "http://127.0.0.1:8080".clone_into(&mut config.base_path); + "/api/".clone_into(&mut config.base_path); config }; @@ -71,6 +75,18 @@ pub fn App() -> impl IntoView { view! { } } /> + } + } + /> + } + } + /> // Inventory impl IntoView { view! { } } /> + } + } + /> + } + } + /> // Products + } + } + /> impl IntoView { view! { } } /> + } + } + /> + + // Product Parents + } + } + /> + + // Units + } + } + /> } diff --git a/src/pages/associate_barcode.rs b/src/pages/associate_barcode.rs index 20714ff..0e1308d 100644 --- a/src/pages/associate_barcode.rs +++ b/src/pages/associate_barcode.rs @@ -1,9 +1,10 @@ use leptos::{ IntoView, component, - prelude::{Get, Show, WriteSignal, signal}, + prelude::{ElementExt, Get, Show, WriteSignal, signal}, task::spawn_local, view, }; +use leptos_router::{NavigateOptions, hooks::use_navigate}; use rocie_client::models::{Barcode, BarcodeId, Product, Unit, UnitAmount, UnitId}; use rocie_macros::Form; use uuid::Uuid; @@ -14,55 +15,62 @@ use crate::{ product_by_name_404_wrapped, product_by_name_external_wrapped, product_suggestion_by_name_wrapped, unit_by_id_wrapped, unit_property_by_id_wrapped, }, - components::{async_fetch::AsyncResource, banner::Banner, site_header::SiteHeader}, + components::{ + async_fetch::AsyncResource, banner::Banner, catch_errors::CatchErrors, + login_wall::LoginWall, site_header::SiteHeader, + }, }; #[component] pub fn AssociateBarcode() -> impl IntoView { - let product_name_signal; - let (errors, errors_set) = signal(None); let (show_units, show_units_set) = signal(false); view! { - - - - - - - { - Form! { - on_submit = |barcode_id, product_name, amount, unit_id| { - let config = get_config!(); - - spawn_local(async move { - let output = async { - let product = product_by_name_external_wrapped(&config, &product_name).await?; - - associate_barcode_external_wrapped(&config, product.id, Barcode { - amount:UnitAmount { - unit: UnitId { value: unit_id }, - value: u32::from(amount), - }, - id: BarcodeId { value: barcode_id }, - }).await?; - - Ok::<_, leptos::error::Error>(()) - }; - - match output.await { - Ok(()) => (), - Err(err) => { - errors_set.set( - Some( - format!("Could not associate barcode: {err}") - ) - ); - }, - } - }); + + + + + + + + + { + let product_name_signal; + Form! { + on_submit = |barcode_id, product_name, amount, unit_id| { + let config = get_config!(); + let navigate = use_navigate(); + + spawn_local(async move { + let output = async { + let product = product_by_name_external_wrapped(&config, product_name.trim()).await?; + + associate_barcode_external_wrapped(&config, product.id, Barcode { + amount:UnitAmount { + unit: UnitId { value: unit_id }, + value: u32::from(amount), + }, + id: BarcodeId { value: barcode_id }, + }).await?; + + Ok::<_, leptos::error::Error>(()) + }; + + match output.await { + Ok(()) => { + navigate("/associate-barcode-product", NavigateOptions::default()); + }, + Err(err) => { + errors_set.set( + Some( + format!("Could not associate barcode: {err}") + ) + ); + }, + } + }); }; impl IntoView { html_type="number", label="Amount" /> - } - } + } + } + + } } diff --git a/src/pages/buy.rs b/src/pages/buy.rs index f3335f6..e4cd599 100644 --- a/src/pages/buy.rs +++ b/src/pages/buy.rs @@ -4,12 +4,16 @@ use leptos::{ task::spawn_local, view, }; +use leptos_router::{NavigateOptions, hooks::use_navigate}; use log::info; use rocie_client::models::BarcodeId; use crate::{ api::{buy_barcode_external_wrapped, get_config}, - components::{banner::Banner, form::Form, site_header::SiteHeader}, + components::{ + banner::Banner, catch_errors::CatchErrors, form::Form, login_wall::LoginWall, + site_header::SiteHeader, + }, }; #[component] @@ -17,29 +21,44 @@ pub fn Buy() -> impl IntoView { let (on_submit_errored, on_submit_errored_set) = signal(None); view! { - + + + - - - + + + - { - Form! { - on_submit = |barcode_number, times| { - let config = get_config!(); + { + Form! { + on_submit = |barcode_number, times| { + let config = get_config!(); + let navigate = use_navigate(); - spawn_local(async move { - if let Err(err) = buy_barcode_external_wrapped(&config, BarcodeId { value: barcode_number }, u32::from(times)).await { - let error = format!("Error in form on-submit for barcode `{barcode_number}`: {err}"); + spawn_local(async move { + match buy_barcode_external_wrapped( + &config, + BarcodeId { value: barcode_number }, + u32::from(times) + ).await { + Ok(()) => { + navigate("/buy", NavigateOptions::default()); + on_submit_errored_set.set(None); + }, + Err(err) => { + let error = + format!( + "Error in form \ + on-submit for barcode \ + `{barcode_number}`: {err}" + ); + on_submit_errored_set.set(Some(error)); + }, + } - on_submit_errored_set.set(Some(error)); - } else { - on_submit_errored_set.set(None); - } - - info!("Bought barcode {barcode_number} {times} times"); - }); + info!("Bought barcode {barcode_number} {times} times"); + }); }; impl IntoView { html_type="number", label="Times" /> - } - } + } + } + + } } diff --git a/src/pages/create_product.rs b/src/pages/create_product.rs index fcd3b0b..fdf8f28 100644 --- a/src/pages/create_product.rs +++ b/src/pages/create_product.rs @@ -1,4 +1,4 @@ -use std::{convert::Infallible, str::FromStr}; +use std::{convert::Infallible, iter, str::FromStr}; use leptos::{ IntoView, component, @@ -6,16 +6,23 @@ use leptos::{ task::spawn_local, view, }; -use rocie_client::models::{ProductStub, UnitPropertyId}; +use leptos_router::{NavigateOptions, hooks::use_navigate}; +use rocie_client::models::{ProductParentId, ProductStub, UnitPropertyId}; use rocie_macros::Form; use uuid::Uuid; use crate::{ - api::{get_config, register_product_external_wrapped, unit_properties_wrapped}, - components::{async_fetch::AsyncResource, banner::Banner, site_header::SiteHeader}, + api::{ + get_config, product_parents_wrapped, register_product_external_wrapped, + unit_properties_wrapped, + }, + components::{ + async_fetch::AsyncResource, banner::Banner, catch_errors::CatchErrors, + login_wall::LoginWall, site_header::SiteHeader, + }, }; -struct OptionalString(Option); +pub(crate) struct OptionalString(pub(crate) Option); impl FromStr for OptionalString { type Err = Infallible; @@ -29,35 +36,54 @@ impl FromStr for OptionalString { } } +struct OptionalParentId(Option); + +impl FromStr for OptionalParentId { + type Err = uuid::Error; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + Ok(Self(None)) + } else { + Ok(Self(Some(ProductParentId { value: s.parse()? }))) + } + } +} + #[component] pub fn CreateProduct() -> impl IntoView { let (error_message, error_message_set) = signal(None); view! { - - - - - - - { - Form! { - on_submit = |product_name, product_description, unit_property_id| { - let config = get_config!(); - - spawn_local(async move { - match register_product_external_wrapped(&config, ProductStub { - description: Some(product_description.0), - name: product_name, - parent: None, // TODO: Add this <2025-10-25> - unit_property: UnitPropertyId { value: unit_property_id }, + + + + + + + + + { + Form! { + on_submit = |product_name, product_description, unit_property_id, parent| { + let config = get_config!(); + let navigate = use_navigate(); + + spawn_local(async move { + match register_product_external_wrapped(&config, ProductStub { + description: product_description.0.map(|d| d.trim().to_owned()), + name: product_name.trim().to_owned(), + parent: parent.0, + unit_property: UnitPropertyId { value: unit_property_id }, + } + ).await { + Ok(_id) => { + navigate("/create-product", NavigateOptions::default()); + } + Err(err) => error_message_set.set(Some(format!("Failed to create product: {err}"))), } - ).await { - Ok(_id) => {} - Err(err) => error_message_set.set(Some(format!("Failed to create product: {err}"))), - } - }); - }; + }); + }; impl IntoView { label="Product Description" /> + impl IntoView { } }, /> - } - } + } + } + + } } diff --git a/src/pages/create_product_parent.rs b/src/pages/create_product_parent.rs new file mode 100644 index 0000000..152347a --- /dev/null +++ b/src/pages/create_product_parent.rs @@ -0,0 +1,126 @@ +use std::{convert::Infallible, iter, str::FromStr}; + +use leptos::{ + IntoView, component, + prelude::{Get, Show, signal}, + task::spawn_local, + view, +}; +use leptos_router::{NavigateOptions, hooks::use_navigate}; +use rocie_client::models::{ProductParentId, ProductParentStub}; +use rocie_macros::Form; + +use crate::{ + api::{get_config, product_parents_wrapped, register_product_parent_external_wrapped}, + components::{ + async_fetch::AsyncResource, banner::Banner, catch_errors::CatchErrors, + login_wall::LoginWall, site_header::SiteHeader, + }, +}; + +struct OptionalString(Option); + +impl FromStr for OptionalString { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + Ok(Self(None)) + } else { + Ok(Self(Some(s.to_owned()))) + } + } +} + +struct OptionalParentId(Option); + +impl FromStr for OptionalParentId { + type Err = uuid::Error; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + Ok(Self(None)) + } else { + Ok(Self(Some(ProductParentId { value: s.parse()? }))) + } + } +} + +#[component] +pub fn CreateProductParent() -> impl IntoView { + let (error_message, error_message_set) = signal(None); + + view! { + + + + + + + + + { + Form! { + on_submit = |name, description, parent| { + let config = get_config!(); + let navigate = use_navigate(); + + spawn_local(async move { + match register_product_parent_external_wrapped(&config, ProductParentStub { + description: description.0.map(|d| d.trim().to_owned()), + name: name.trim().to_owned(), + parent: parent.0, + } + ).await { + Ok(_id) => { + navigate("/create-product-parent", NavigateOptions::default()); + } + Err(err) => error_message_set.set(Some(format!("Failed to create product: {err}"))), + } + }); + }; + + + + + + + +