Compare commits
No commits in common. "leptos" and "main" have entirely different histories.
7
.gitignore
vendored
|
@ -9,10 +9,3 @@ gen/
|
||||||
# Generated by `cargo tauri icon`
|
# Generated by `cargo tauri icon`
|
||||||
# will have various versions of the app icon
|
# will have various versions of the app icon
|
||||||
icons/
|
icons/
|
||||||
|
|
||||||
# Generated by Trunk
|
|
||||||
# will have the files to be served over http
|
|
||||||
www/
|
|
||||||
|
|
||||||
# Secret
|
|
||||||
src/server/cloud_user.txt
|
|
||||||
|
|
1857
Cargo.lock
generated
63
Cargo.toml
|
@ -1,55 +1,32 @@
|
||||||
[package]
|
[package]
|
||||||
authors = ["Bianca Fürstenau"]
|
|
||||||
description = "Buchhaltung für „Darmstadt sagt Nein zur Bezahlkarte!“"
|
|
||||||
edition = "2024"
|
|
||||||
name = "bkbh"
|
name = "bkbh"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
description = "Buchhaltung für „Darmstadt sagt Nein zur Bezahlkarte!“"
|
||||||
|
authors = ["Bianca Fürstenau"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
[features]
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
leptos = []
|
|
||||||
server = [
|
|
||||||
"dep:tauri",
|
|
||||||
"dep:tauri-plugin-opener",
|
|
||||||
"dep:tauri-plugin-fs",
|
|
||||||
"dep:tauri-plugin-sql",
|
|
||||||
"dep:sqlx",
|
|
||||||
"dep:rand",
|
|
||||||
"dep:chrono",
|
|
||||||
"dep:tokio",
|
|
||||||
"dep:curl",
|
|
||||||
"dep:openssl",
|
|
||||||
"dep:ring-compat",
|
|
||||||
]
|
|
||||||
tauri = ["server"]
|
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
# The `_lib` suffix may seem redundant but it is necessary
|
||||||
|
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||||
|
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||||
|
name = "bkbh_lib"
|
||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
name = "bkbh"
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2", features = ["config-toml"] }
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
console_error_panic_hook = { version = "0.1.7" }
|
tauri = { version = "2", features = [] }
|
||||||
leptos = { version = "^0.7", features = ["csr"] }
|
tauri-plugin-opener = "2"
|
||||||
leptos_router = { version = "^0.7" }
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = { version = "1" }
|
serde_json = "1"
|
||||||
tauri-sys = { git = "ssh://git@gitea.mathebau.de:3022/Peter/tauri-sys.git", branch = "v2", features = [
|
rusqlite = {version = "^0.33", features = ["backup", "bundled"] }
|
||||||
"core",
|
rand = {version = "^0.8"}
|
||||||
] }
|
chrono = {version = "^0.4"}
|
||||||
|
tokio = {version = "^1.43"}
|
||||||
chrono = { version = "^0.4", optional = true }
|
tauri-plugin-fs = {version = "2"}
|
||||||
curl = { version = "^0.4", optional = true }
|
curl = {version = "^0.4"}
|
||||||
openssl = { version = "^0.10", features = ["vendored"], optional = true }
|
openssl = {version = "^0.10", features = ["vendored"] }
|
||||||
rand = { version = "^0.8", optional = true }
|
ring-compat = {version = "^0.8", features = ["signature", "rand_core"] }
|
||||||
ring-compat = { version = "^0.8", features = [
|
|
||||||
"signature",
|
|
||||||
"rand_core",
|
|
||||||
], optional = true }
|
|
||||||
sqlx = { version = "^0.8", features = ["sqlite", "runtime-tokio"], optional = true }
|
|
||||||
tauri = { version = "2", features = ["config-toml"], optional = true }
|
|
||||||
tauri-plugin-fs = { version = "2", optional = true }
|
|
||||||
tauri-plugin-opener = { version = "2", optional = true }
|
|
||||||
tauri-plugin-sql = { version = "2", features = ["sqlite"], optional = true }
|
|
||||||
tokio = { version = "^1.43", optional = true }
|
|
||||||
|
|
54
README.md
|
@ -1,54 +1,8 @@
|
||||||
# Setup
|
# Setup
|
||||||
|
|
||||||
Clone the repository
|
|
||||||
and in the root directory
|
|
||||||
of the repo,
|
|
||||||
execute
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo tauri icon assets/icon.svg
|
git clone …
|
||||||
```
|
cargo tauri android init
|
||||||
|
|
||||||
and write the Nextcloud token
|
|
||||||
into `src/server/cloud_user.txt`.
|
|
||||||
|
|
||||||
## Android
|
|
||||||
|
|
||||||
To enable android development,
|
|
||||||
additionally execute
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rm gen/android/app/build.gradle.kts
|
|
||||||
NDK_HOME=<path> ANDROID_HOME=<path> cargo tauri android init
|
|
||||||
git restore gen/android/app/build.gradle.kts
|
git restore gen/android/app/build.gradle.kts
|
||||||
```
|
cargo tauri img/icon.svg
|
||||||
|
# Set up gen/android/keystore.properties
|
||||||
and set up `gen/android/keystore.properties`,
|
|
||||||
e.g. following https://v2.tauri.app/distribute/sign/android/.
|
|
||||||
|
|
||||||
# Run
|
|
||||||
|
|
||||||
## Linux Development
|
|
||||||
|
|
||||||
Tauri does not seem to support
|
|
||||||
specifying the binary
|
|
||||||
to be run in a config,
|
|
||||||
so we have to do it
|
|
||||||
on the command line.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo tauri dev -- --bin tauri
|
|
||||||
```
|
|
||||||
|
|
||||||
## Android Development
|
|
||||||
|
|
||||||
Tauri does not seem to support
|
|
||||||
specifying the Cargo features
|
|
||||||
of the library for mobile
|
|
||||||
in a config,
|
|
||||||
so we have to do it
|
|
||||||
on the command line.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
NDK_HOME=<path> ANDROID_HOME=<path> cargo tauri android dev --features tauri
|
|
||||||
```
|
```
|
||||||
|
|
26
Tauri.toml
|
@ -1,26 +0,0 @@
|
||||||
"$schema" = "https://schema.tauri.app/config/2"
|
|
||||||
identifier = "de.hessensagtnein.darmstadt.buchhaltung"
|
|
||||||
productName = "Nein!"
|
|
||||||
version = "0.1.0"
|
|
||||||
|
|
||||||
[build]
|
|
||||||
before-build-command = "cd bkbh && trunk build"
|
|
||||||
before-dev-command = "cd bkbh && trunk serve"
|
|
||||||
dev-url = "http://localhost:1420"
|
|
||||||
features = ["tauri"]
|
|
||||||
frontend-dist = "www"
|
|
||||||
|
|
||||||
[app]
|
|
||||||
windows = [{ title = "Nein zur Bezahlkarte!", width = 800, height = 600 }]
|
|
||||||
with-global-tauri = true
|
|
||||||
|
|
||||||
[bundle]
|
|
||||||
active = true
|
|
||||||
icon = [
|
|
||||||
"icons/32x32.png",
|
|
||||||
"icons/128x128.png",
|
|
||||||
"icons/128x128@2x.png",
|
|
||||||
"icons/icon.icns",
|
|
||||||
"icons/icon.ico",
|
|
||||||
]
|
|
||||||
targets = "all"
|
|
|
@ -1,9 +0,0 @@
|
||||||
[build]
|
|
||||||
dist = "www"
|
|
||||||
html_output = "index.html"
|
|
||||||
target = "trunk.html"
|
|
||||||
|
|
||||||
[serve]
|
|
||||||
open = false
|
|
||||||
port = 1420
|
|
||||||
ws_protocol = "ws"
|
|
6
build.rs
|
@ -1,7 +1,3 @@
|
||||||
fn main() {
|
fn main() {
|
||||||
if cfg!(feature = "tauri") {
|
tauri_build::build()
|
||||||
tauri_build::build();
|
|
||||||
} else {
|
|
||||||
println!("cargo::rustc-check-cfg=cfg(mobile)");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
"$schema" = "../gen/schemas/desktop-schema.json"
|
|
||||||
description = "Capability for the main window"
|
|
||||||
identifier = "default"
|
|
||||||
permissions = [
|
|
||||||
"core:default",
|
|
||||||
"opener:default",
|
|
||||||
"fs:default",
|
|
||||||
"sql:default",
|
|
||||||
"sql:allow-execute",
|
|
||||||
]
|
|
||||||
windows = ["main"]
|
|
13
capabilities/default.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Capability for the main window",
|
||||||
|
"windows": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"opener:default",
|
||||||
|
"fs:default"
|
||||||
|
]
|
||||||
|
}
|
|
@ -16,10 +16,10 @@ val tauriProperties = Properties().apply {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk = 34
|
compileSdk = 34
|
||||||
namespace = "de.hessensagtnein.darmstadt.buchhaltung"
|
namespace = "de.mathebau.bkbh"
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
||||||
applicationId = "de.hessensagtnein.darmstadt.buchhaltung"
|
applicationId = "de.mathebau.bkbh"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||||
|
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
@ -1,4 +0,0 @@
|
||||||
indentation_style = "Tabs"
|
|
||||||
max_width = 80
|
|
||||||
newline_style = "Unix"
|
|
||||||
tab_spaces = 8
|
|
|
@ -1,29 +0,0 @@
|
||||||
CREATE TABLE `swap` (
|
|
||||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
`acc` INTEGER NOT NULL,
|
|
||||||
`voucher` INTEGER NOT NULL,
|
|
||||||
`storno` INTEGER NOT NULL,
|
|
||||||
`timestamp` INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE `voucher_type` (
|
|
||||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
`store` INTEGER NOT NULL,
|
|
||||||
`value` INTEGER NOT NULL,
|
|
||||||
UNIQUE (`store`, `value`)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE `inventory` (
|
|
||||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
`acc` INTEGER NOT NULL,
|
|
||||||
`cash` INTEGER NOT NULL,
|
|
||||||
`timestamp` INTEGER NOT NULL,
|
|
||||||
UNIQUE (`acc`, `timestamp`)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE `voucher_inventory` (
|
|
||||||
`inventory` INTEGER NOT NULL,
|
|
||||||
`voucher` INTEGER NOT NULL,
|
|
||||||
`count` INTEGER NOT NULL,
|
|
||||||
PRIMARY KEY (`inventory`, `voucher`)
|
|
||||||
);
|
|
|
@ -1,2 +0,0 @@
|
||||||
[rustfmt]
|
|
||||||
overrideCommand = ["leptosfmt", "--stdin", "--rustfmt"]
|
|
|
@ -1,4 +1,3 @@
|
||||||
edition = "2024"
|
max_width = 80
|
||||||
hard_tabs = true
|
hard_tabs = true
|
||||||
max_width = 80
|
|
||||||
tab_spaces = 8
|
tab_spaces = 8
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use ring_compat::signature::ed25519::SigningKey;
|
use ring_compat::signature::ed25519::SigningKey;
|
||||||
|
use rusqlite::Connection;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use sqlx::sqlite::SqliteConnection as Connection;
|
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub db: Connection,
|
pub db: Connection,
|
||||||
|
@ -12,11 +12,23 @@ pub struct AppState {
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
todo!();
|
let db = Connection::open_in_memory()
|
||||||
let db = unimplemented!();
|
.expect("Failed to create DB.");
|
||||||
|
db.execute(
|
||||||
|
"CREATE TABLE swap (
|
||||||
|
rand INTEGER,
|
||||||
|
store INTEGER,
|
||||||
|
account INTEGER,
|
||||||
|
submitter INTEGER,
|
||||||
|
time INTEGER,
|
||||||
|
cancellation BOOL
|
||||||
|
)",
|
||||||
|
(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
let last_sync = i64::MIN;
|
let last_sync = i64::MIN;
|
||||||
let id = rng.r#gen();
|
let id = rng.gen();
|
||||||
let key = SigningKey::generate(&mut rng);
|
let key = SigningKey::generate(&mut rng);
|
||||||
AppState {
|
AppState {
|
||||||
db,
|
db,
|
|
@ -1,7 +0,0 @@
|
||||||
use bkbh::leptos::cafe::Cafe;
|
|
||||||
use leptos::prelude::*;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
console_error_panic_hook::set_once();
|
|
||||||
leptos::mount::mount_to_body(|| view! { <Cafe /> });
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
use crate::types::*;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use tauri_sys::core::invoke;
|
|
||||||
use tauri_sys::Error;
|
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
|
||||||
pub struct Swap {
|
|
||||||
store: Store,
|
|
||||||
acc: i64,
|
|
||||||
}
|
|
||||||
pub async fn swap(store: Store, acc: i64) -> Result<(), Error> {
|
|
||||||
let args = Swap { store, acc };
|
|
||||||
invoke("swap", &args).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
|
||||||
pub struct Inventory {
|
|
||||||
data: HashMap<String, String>,
|
|
||||||
}
|
|
||||||
pub async fn inventory(data: HashMap<String, String>) -> Result<(), Error> {
|
|
||||||
let args = Inventory { data };
|
|
||||||
invoke("inventory", &args).await
|
|
||||||
}
|
|
|
@ -1,11 +1,12 @@
|
||||||
use chrono::offset::Utc;
|
use chrono::offset::Utc;
|
||||||
use curl::{easy, easy::Easy2};
|
use curl::{easy, easy::Easy2};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
|
use rusqlite::{Connection, DatabaseName};
|
||||||
use ring_compat::signature::ed25519::SigningKey;
|
use ring_compat::signature::ed25519::SigningKey;
|
||||||
use tauri::{Manager, State};
|
use tauri::{Manager, State};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::server::app_state::AppState;
|
use crate::app_state::AppState;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Collector(Vec<u8>, Vec<u8>, usize);
|
struct Collector(Vec<u8>, Vec<u8>, usize);
|
||||||
|
@ -60,25 +61,23 @@ fn data_client(file: &str) -> Result<Easy2<Collector>, ()> {
|
||||||
"https://cloud.seebruecke.org/public.php/webdav/data/{}",
|
"https://cloud.seebruecke.org/public.php/webdav/data/{}",
|
||||||
file
|
file
|
||||||
);
|
);
|
||||||
client.url(&url)
|
client.url(&url).map_err(|_| ())?;
|
||||||
.map_err(|e| println!("{:?}", e))?;
|
client.username(include_str!("cloud_user.txt")).map_err(|_| ())?;
|
||||||
client.username(include_str!("cloud_user.txt"))
|
|
||||||
.map_err(|e| println!("{:?}", e))?;
|
|
||||||
client.http_auth(easy::Auth::new().auto(true))
|
client.http_auth(easy::Auth::new().auto(true))
|
||||||
.map_err(|e| println!("{:?}", e))?;
|
.map_err(|_| ())?;
|
||||||
client.ssl_cainfo_blob(include_bytes!("isrg-root-x1.pem"))
|
client.ssl_cainfo_blob(include_bytes!("isrg-root-x1.pem"))
|
||||||
.map_err(|e| println!("{:?}", e))?;
|
.map_err(|_| ())?;
|
||||||
Ok(client)
|
Ok(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn put_client(file: &str, payload: &[u8]) -> Result<Easy2<Collector>, ()> {
|
fn put_client(file: &str, payload: &[u8]) -> Result<Easy2<Collector>, ()> {
|
||||||
let mut client = data_client(&file)?;
|
let mut client = data_client(&file)?;
|
||||||
client.put(true)
|
client.put(true).map_err(|_| ())?;
|
||||||
.map_err(|e| println!("{:?}", e))?;
|
|
||||||
client.get_mut().1.extend_from_slice(payload);
|
client.get_mut().1.extend_from_slice(payload);
|
||||||
client.in_filesize(payload.len() as u64)
|
client.in_filesize(payload.len() as u64)
|
||||||
.map_err(|e| println!("{:?}", e))?;
|
.map_err(|e| println!("{:?}", e))?;
|
||||||
client.upload(true).map_err(|e| println!("{:?}", e))?;
|
client.upload(true)
|
||||||
|
.map_err(|e| println!("{:?}", e))?;
|
||||||
Ok(client)
|
Ok(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,10 +86,8 @@ pub async fn pull_data(
|
||||||
_state: State<'_, Mutex<AppState>>,
|
_state: State<'_, Mutex<AppState>>,
|
||||||
) -> Result<String, ()> {
|
) -> Result<String, ()> {
|
||||||
let mut client = data_client("")?;
|
let mut client = data_client("")?;
|
||||||
client.custom_request("PROPFIND")
|
client.custom_request("PROPFIND").map_err(|_| ())?;
|
||||||
.map_err(|e| println!("{:?}", e))?;
|
client.perform().map_err(|_| ())?;
|
||||||
client.perform()
|
|
||||||
.map_err(|e| println!("{:?}", e))?;
|
|
||||||
let content = &client.get_ref().0;
|
let content = &client.get_ref().0;
|
||||||
Ok(String::from_utf8_lossy(content).to_string())
|
Ok(String::from_utf8_lossy(content).to_string())
|
||||||
}
|
}
|
||||||
|
@ -99,7 +96,22 @@ async fn push_key(id: &u64, key: &SigningKey) -> Result<(), ()> {
|
||||||
let file = format!("{:016X}.key", id);
|
let file = format!("{:016X}.key", id);
|
||||||
let v_key = key.verifying_key();
|
let v_key = key.verifying_key();
|
||||||
let client = put_client(&file, v_key.as_ref())?;
|
let client = put_client(&file, v_key.as_ref())?;
|
||||||
let _perf = client.perform().map_err(|e| println!("{:?}", e))?;
|
let _perf = client.perform()
|
||||||
|
.map_err(|e| println!("{:?}", e))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_db(id: &u64, db: &Connection, app: tauri::AppHandle) -> Result<(), ()> {
|
||||||
|
let filename = format!("{:016X}.sqlite", id);
|
||||||
|
let path = app.path().resolve(&filename, tauri::path::BaseDirectory::Temp)
|
||||||
|
.map_err(|e| println!("{:?}", e))?;
|
||||||
|
db.backup(DatabaseName::Main, &path, None)
|
||||||
|
.map_err(|e| println!("{:?}", e))?;
|
||||||
|
let buf = std::fs::read(&path)
|
||||||
|
.map_err(|e| println!("{:?}", e))?;
|
||||||
|
let client = put_client(&filename, buf.as_ref())?;
|
||||||
|
let _perf = client.perform()
|
||||||
|
.map_err(|e| println!("{:?}", e))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,5 +122,6 @@ pub async fn push_data(
|
||||||
) -> Result<(), ()> {
|
) -> Result<(), ()> {
|
||||||
let state = state.lock().await;
|
let state = state.lock().await;
|
||||||
push_key(&state.id, &state.key).await?;
|
push_key(&state.id, &state.key).await?;
|
||||||
|
push_db(&state.id, &state.db, app)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
|
@ -1,8 +0,0 @@
|
||||||
use leptos::prelude::*;
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn Angel() -> impl IntoView {
|
|
||||||
view! {
|
|
||||||
<p>Hi</p>
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,182 +0,0 @@
|
||||||
use crate::commands::*;
|
|
||||||
use crate::types::*;
|
|
||||||
use leptos::form::FromFormData;
|
|
||||||
use leptos::prelude::*;
|
|
||||||
use leptos::task::spawn_local;
|
|
||||||
use leptos::web_sys::FormData;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn Cafe() -> impl IntoView {
|
|
||||||
let acc = signal(Account::Sumpf);
|
|
||||||
view! {
|
|
||||||
<Reception
|
|
||||||
acc=acc.0
|
|
||||||
/>
|
|
||||||
<InvForm />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
fn Reception(acc: ReadSignal<Account>) -> impl IntoView {
|
|
||||||
view! {
|
|
||||||
<div
|
|
||||||
id="cafe-voucher"
|
|
||||||
>
|
|
||||||
<h1>
|
|
||||||
{move || format!("{}", acc.get())}
|
|
||||||
</h1>
|
|
||||||
<div
|
|
||||||
class="h-container"
|
|
||||||
>
|
|
||||||
<SwapButton
|
|
||||||
store=Store::Aldi
|
|
||||||
/>
|
|
||||||
<SwapButton
|
|
||||||
store=Store::Edeka
|
|
||||||
/>
|
|
||||||
<SwapButton
|
|
||||||
store=Store::Dm
|
|
||||||
/>
|
|
||||||
<SwapButton
|
|
||||||
store=Store::Lidl
|
|
||||||
/>
|
|
||||||
<SwapButton
|
|
||||||
store=Store::Rewe
|
|
||||||
/>
|
|
||||||
<SwapButton
|
|
||||||
store=Store::Tegut
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
fn SwapButton(store: Store) -> impl IntoView {
|
|
||||||
view! {
|
|
||||||
<button
|
|
||||||
on:click=move |_| {
|
|
||||||
spawn_local(async move {
|
|
||||||
swap(store, 0).await.unwrap();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
class="column"
|
|
||||||
>
|
|
||||||
<super::store::Logo store=store />
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
fn StoreInput(store: Store) -> impl IntoView {
|
|
||||||
let txt = format!("{}", Into::<String>::into(&store));
|
|
||||||
view! {
|
|
||||||
<div
|
|
||||||
class="labelled-input"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
for=txt.clone()
|
|
||||||
>
|
|
||||||
<super::store::Logo store=store />
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
name=txt.clone()
|
|
||||||
id=txt.clone()
|
|
||||||
min=0
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
fn CashInput(value: RwSignal<String>) -> impl IntoView {
|
|
||||||
let txt = "cash";
|
|
||||||
view! {
|
|
||||||
<div
|
|
||||||
class="labelled-input"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
for=txt.clone()
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="assets/cash.svg"
|
|
||||||
class="logo"
|
|
||||||
alt="Bargeld"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
name=txt.clone()
|
|
||||||
id=txt.clone()
|
|
||||||
min=0
|
|
||||||
step=0.01
|
|
||||||
bind:value=value
|
|
||||||
on:change=move |_| {
|
|
||||||
println!("{:?}", value.get());
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
fn AccRadio(acc: Account) -> impl IntoView {
|
|
||||||
let txt = format!("{}", Into::<String>::into(&acc));
|
|
||||||
let id = format!("acc-{}", txt);
|
|
||||||
view! {
|
|
||||||
<div
|
|
||||||
class="labelled-input"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="acc"
|
|
||||||
id=id.clone()
|
|
||||||
value=txt
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
for=id.clone()
|
|
||||||
>
|
|
||||||
{format!("{}", acc)}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
fn SubmitButton() -> impl IntoView {
|
|
||||||
view! {
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="shout"
|
|
||||||
>
|
|
||||||
"Senden"
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
fn InvForm() -> impl IntoView {
|
|
||||||
let cash = RwSignal::new(String::from("0.00"));
|
|
||||||
view! {
|
|
||||||
<form
|
|
||||||
on:submit=move |ev| {
|
|
||||||
ev.prevent_default();
|
|
||||||
let data = FromFormData::from_event(ev.as_ref()).unwrap();
|
|
||||||
spawn_local(async move {inventory(data).await;});
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<AccRadio acc=Account::Sumpf />
|
|
||||||
<AccRadio acc=Account::Heinersyndikat />
|
|
||||||
<StoreInput store=Store::Aldi />
|
|
||||||
<StoreInput store=Store::Dm />
|
|
||||||
<StoreInput store=Store::Lidl />
|
|
||||||
<StoreInput store=Store::Rewe />
|
|
||||||
<StoreInput store=Store::Tegut />
|
|
||||||
<CashInput value=cash />
|
|
||||||
<SubmitButton />
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
pub mod angel;
|
|
||||||
pub mod cafe;
|
|
||||||
pub mod store;
|
|
|
@ -1,14 +0,0 @@
|
||||||
use crate::types::*;
|
|
||||||
use leptos::prelude::*;
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn Logo(store: Store) -> impl IntoView {
|
|
||||||
view! {
|
|
||||||
<img
|
|
||||||
src=format!("assets/{}.svg", Into::<String>::into(&store))
|
|
||||||
class="logo"
|
|
||||||
// FIXME: Implement fmt::Display for Store
|
|
||||||
alt=format!("{:?}", store)
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
199
src/lib.rs
|
@ -1,18 +1,184 @@
|
||||||
pub mod commands;
|
use chrono::offset::Utc;
|
||||||
#[cfg(feature = "leptos")]
|
use rusqlite::{types::ToSqlOutput, ToSql};
|
||||||
pub mod leptos;
|
use tauri::{Manager, State};
|
||||||
#[cfg(feature = "server")]
|
use tauri_plugin_fs::FsExt;
|
||||||
pub mod server;
|
use tokio::sync::Mutex;
|
||||||
pub mod types;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
mod app_state;
|
||||||
|
mod data_door;
|
||||||
|
|
||||||
|
use app_state::AppState;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
enum Store {
|
||||||
|
Aldi,
|
||||||
|
Edeka,
|
||||||
|
Dm,
|
||||||
|
Lidl,
|
||||||
|
Rewe,
|
||||||
|
Tegut,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
enum Account {
|
||||||
|
Sumpf,
|
||||||
|
Heinersyndikat,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Inventory {
|
||||||
|
acc: Account,
|
||||||
|
cash: i64,
|
||||||
|
vouchers: Vec<VoucherInventory>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct VoucherInventory {
|
||||||
|
store: Store,
|
||||||
|
count: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for Store {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||||
|
match s {
|
||||||
|
"aldi" => Ok(Store::Aldi),
|
||||||
|
"edeka" => Ok(Store::Edeka),
|
||||||
|
"dm" => Ok(Store::Dm),
|
||||||
|
"lidl" => Ok(Store::Lidl),
|
||||||
|
"rewe" => Ok(Store::Rewe),
|
||||||
|
"tegut" => Ok(Store::Tegut),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for Account {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||||
|
match s {
|
||||||
|
"sumpf" => Ok(Account::Sumpf),
|
||||||
|
"hs" => Ok(Account::Heinersyndikat),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToSql for Store {
|
||||||
|
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
|
||||||
|
match self {
|
||||||
|
Store::Aldi => 0.to_sql(),
|
||||||
|
Store::Edeka => 1.to_sql(),
|
||||||
|
Store::Dm => 2.to_sql(),
|
||||||
|
Store::Lidl => 3.to_sql(),
|
||||||
|
Store::Rewe => 4.to_sql(),
|
||||||
|
Store::Tegut => 5.to_sql(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToSql for Account {
|
||||||
|
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
|
||||||
|
match self {
|
||||||
|
Account::Sumpf => 0.to_sql(),
|
||||||
|
Account::Heinersyndikat => 1.to_sql(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_inventory(data: HashMap<String, String>) -> Result<Inventory, ()> {
|
||||||
|
let a = data.get("cafe-inventory-acc").ok_or(())?;
|
||||||
|
let acc: Account = Account::try_from(a.as_ref())?;
|
||||||
|
let mut vouchers = Vec::new();
|
||||||
|
for s in ["aldi", "dm", "lidl", "rewe", "tegut"] {
|
||||||
|
let Ok(store) = s.try_into() else {
|
||||||
|
println!("Did not find '{}' in inventory data.", s);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
match data.get(&format!("cafe-inventory-{}", s)) {
|
||||||
|
None => (),
|
||||||
|
Some(c) => {
|
||||||
|
let c = if c == "" {"0"} else {c};
|
||||||
|
let Ok(count) = c.parse() else {
|
||||||
|
println!("Invalid count '{}' for '{}' in inventory data.", c, s);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let v = VoucherInventory { store, count };
|
||||||
|
vouchers.push(v);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn inventory(
|
||||||
|
data: HashMap<String, String>,
|
||||||
|
state: State<'_, Mutex<AppState>>,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
let now = Utc::now().timestamp();
|
||||||
|
let state = state.lock().await;
|
||||||
|
let inv = parse_inventory(data)?;
|
||||||
|
for v in inv.vouchers {
|
||||||
|
state.db.execute(
|
||||||
|
"INSERT INTO voucher_inventory VALUES ()",
|
||||||
|
(
|
||||||
|
inv.acc,
|
||||||
|
v.store,
|
||||||
|
v.count,
|
||||||
|
now,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.map_err(|e| println!("{:?}", e))?;
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn swap(
|
||||||
|
store: &str,
|
||||||
|
acc: i64,
|
||||||
|
state: State<'_, Mutex<AppState>>,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
let state = state.lock().await;
|
||||||
|
let store: Store = store.try_into()?;
|
||||||
|
state.db.execute(
|
||||||
|
"INSERT INTO swap VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||||
|
(
|
||||||
|
store,
|
||||||
|
acc,
|
||||||
|
i64::from_ne_bytes(state.id.to_ne_bytes()),
|
||||||
|
Utc::now().timestamp(),
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.map_err(|e| println!("{:?}", e))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn count(state: State<'_, Mutex<AppState>>) -> Result<String, ()> {
|
||||||
|
let state = state.lock().await;
|
||||||
|
let mut stmt =
|
||||||
|
state.db.prepare("SELECT COUNT(*) FROM swap")
|
||||||
|
.map_err(|e| println!("{:?}", e))?;
|
||||||
|
let mut rows = stmt.query([]).map_err(|e| println!("{:?}", e))?;
|
||||||
|
let row = rows.next().map_err(|e| println!("{:?}", e))?;
|
||||||
|
let row = match row {
|
||||||
|
Some(r) => Ok(r),
|
||||||
|
None => {
|
||||||
|
println!("No rows");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
let cnt: u64 = row.get(0).map_err(|e| println!("{:?}", e))?;
|
||||||
|
Ok(cnt.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "tauri", feature = "server"))]
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
use server::app_state::AppState;
|
|
||||||
use tauri::{Manager, State};
|
|
||||||
use tauri_plugin_fs::FsExt;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
let state = AppState::new();
|
let state = AppState::new();
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_fs::init())
|
.plugin(tauri_plugin_fs::init())
|
||||||
|
@ -24,10 +190,11 @@ pub fn run() {
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
server::swap,
|
swap,
|
||||||
server::inventory,
|
count,
|
||||||
server::data_door::pull_data,
|
inventory,
|
||||||
server::data_door::push_data,
|
data_door::pull_data,
|
||||||
|
data_door::push_data,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|
|
@ -2,5 +2,5 @@
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
bkbh::run()
|
bkbh_lib::run()
|
||||||
}
|
}
|
|
@ -1,133 +0,0 @@
|
||||||
use chrono::offset::Utc;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use tauri::State;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
use sqlx::sqlite::SqliteConnection as Connection;
|
|
||||||
|
|
||||||
use crate::types::*;
|
|
||||||
|
|
||||||
pub mod app_state;
|
|
||||||
pub mod data_door;
|
|
||||||
|
|
||||||
use app_state::AppState;
|
|
||||||
|
|
||||||
struct Id {
|
|
||||||
id: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_inventory(data: HashMap<String, String>) -> Result<Inventory, ()> {
|
|
||||||
let a = data.get("cafe-inventory-acc").ok_or(())?;
|
|
||||||
let acc: Account = Account::try_from(a.as_ref())?;
|
|
||||||
let mut vouchers = Vec::new();
|
|
||||||
for s in ["aldi", "dm", "lidl", "rewe", "tegut"] {
|
|
||||||
let Ok(store) = s.try_into() else {
|
|
||||||
println!("Did not find '{}' in inventory data.", s);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
match data.get(&format!("cafe-inventory-{}", s)) {
|
|
||||||
None => (),
|
|
||||||
Some(c) => {
|
|
||||||
let c = if c == "" { "0" } else { c };
|
|
||||||
let Ok(count): Result<i64, _> = c.parse() else {
|
|
||||||
println!("Invalid count '{}' for '{}' in inventory data.", c, s);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let value = 50_00;
|
|
||||||
let voucher = VoucherType { store, value };
|
|
||||||
let v = (voucher, count);
|
|
||||||
vouchers.push(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn voucher_id(v: VoucherType, db: &mut Connection) -> Result<i64, ()> {
|
|
||||||
sqlx::query!(
|
|
||||||
"INSERT OR IGNORE INTO voucher_type(store, value) VALUES (?1, ?2)",
|
|
||||||
v.store,
|
|
||||||
v.value,
|
|
||||||
)
|
|
||||||
.execute(&mut *db)
|
|
||||||
.await
|
|
||||||
.map_err(|e| println!("{:?}", e))?;
|
|
||||||
let id = sqlx::query_as!(
|
|
||||||
Id,
|
|
||||||
"SELECT id FROM voucher_type WHERE store = ?1 AND value = ?2",
|
|
||||||
v.store,
|
|
||||||
v.value,
|
|
||||||
)
|
|
||||||
.fetch_one(db)
|
|
||||||
.await
|
|
||||||
.map_err(|e| println!("{:?}", e))?
|
|
||||||
.id;
|
|
||||||
Ok(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn inventory(
|
|
||||||
data: HashMap<String, String>,
|
|
||||||
state: State<'_, Mutex<AppState>>,
|
|
||||||
) -> Result<(), ()> {
|
|
||||||
println!("{:?}", data);
|
|
||||||
let now = Utc::now().timestamp();
|
|
||||||
let mut state = state.lock().await;
|
|
||||||
let inv = parse_inventory(data)?;
|
|
||||||
sqlx::query!(
|
|
||||||
"INSERT INTO inventory(acc, cash, timestamp) VALUES (?1, ?2, ?3)",
|
|
||||||
inv.acc,
|
|
||||||
inv.cash,
|
|
||||||
now,
|
|
||||||
)
|
|
||||||
.execute(&mut state.db)
|
|
||||||
.await
|
|
||||||
.map_err(|e| println!("{:?}", e))?;
|
|
||||||
let inventory = sqlx::query_as!(
|
|
||||||
Id,
|
|
||||||
"SELECT id FROM inventory WHERE acc = ?1 AND timestamp = ?2",
|
|
||||||
inv.acc,
|
|
||||||
now,
|
|
||||||
)
|
|
||||||
.fetch_one(&mut state.db)
|
|
||||||
.await
|
|
||||||
.map_err(|e| println!("{:?}", e))?
|
|
||||||
.id;
|
|
||||||
for (v, count) in inv.vouchers {
|
|
||||||
let voucher = voucher_id(v, &mut state.db).await?;
|
|
||||||
sqlx::query!(
|
|
||||||
"INSERT INTO voucher_inventory VALUES (?1, ?2, ?3)",
|
|
||||||
inventory,
|
|
||||||
voucher,
|
|
||||||
count,
|
|
||||||
)
|
|
||||||
.execute(&mut state.db)
|
|
||||||
.await
|
|
||||||
.map_err(|e| println!("{:?}", e))?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn swap(
|
|
||||||
store: Store,
|
|
||||||
acc: i64,
|
|
||||||
state: State<'_, Mutex<AppState>>,
|
|
||||||
) -> Result<(), ()> {
|
|
||||||
let timestamp = Utc::now().timestamp();
|
|
||||||
let mut state = state.lock().await;
|
|
||||||
let id = i64::from_ne_bytes(state.id.to_ne_bytes());
|
|
||||||
let value = 50_00;
|
|
||||||
let voucher = VoucherType{ store, value };
|
|
||||||
let voucher_type = voucher_id(voucher, &mut state.db).await?;
|
|
||||||
sqlx::query!(
|
|
||||||
"INSERT INTO swap(acc, voucher, storno, timestamp) VALUES (?1, ?2, ?3, ?4)",
|
|
||||||
acc,
|
|
||||||
voucher_type,
|
|
||||||
false,
|
|
||||||
timestamp,
|
|
||||||
)
|
|
||||||
.execute(&mut state.db)
|
|
||||||
.await
|
|
||||||
.map_err(|e| println!("{:?}", e))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
|
||||||
#[cfg_attr(feature = "server", derive(sqlx::Type))]
|
|
||||||
#[cfg_attr(feature = "server", repr(i64))]
|
|
||||||
pub enum Account {
|
|
||||||
Sumpf = 1,
|
|
||||||
Heinersyndikat = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&str> for Account {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
|
||||||
match s {
|
|
||||||
"sumpf" => Ok(Account::Sumpf),
|
|
||||||
"hs" => Ok(Account::Heinersyndikat),
|
|
||||||
_ => Err(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Into<String> for &Account {
|
|
||||||
fn into(self) -> String {
|
|
||||||
String::from(match *self {
|
|
||||||
Account::Sumpf => "sumpf",
|
|
||||||
Account::Heinersyndikat => "hs",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Account {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match *self {
|
|
||||||
Account::Sumpf => "Sumpf",
|
|
||||||
Account::Heinersyndikat => "Heinersyndikat",
|
|
||||||
}
|
|
||||||
.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// An amount of cash,
|
|
||||||
/// measured as an integer multiple
|
|
||||||
/// of 0.01 €.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
#[cfg_attr(feature = "server", derive(sqlx::Type))]
|
|
||||||
#[cfg_attr(feature = "server", sqlx(transparent))]
|
|
||||||
pub struct Cash(i64);
|
|
||||||
|
|
||||||
impl std::str::FromStr for Cash {
|
|
||||||
type Err = ();
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
let split: Vec<&str> = s.split(".").collect();
|
|
||||||
let i = i64::from_str(split.get(0).ok_or(())?)
|
|
||||||
.map_err(|e| println!("{:?}", e))?;
|
|
||||||
let f = match split.get(1) {
|
|
||||||
None => 0,
|
|
||||||
Some(fs) => i64::from_str(&format!("00{}", fs)[0..2])
|
|
||||||
.map_err(|e| println!("{:?}", e))?,
|
|
||||||
};
|
|
||||||
Ok(Cash(i * 100 + f))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
mod store;
|
|
||||||
mod account;
|
|
||||||
mod cash;
|
|
||||||
|
|
||||||
pub use store::Store;
|
|
||||||
pub use account::Account;
|
|
||||||
pub use cash::Cash;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct VoucherType {
|
|
||||||
pub store: Store,
|
|
||||||
pub value: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Inventory {
|
|
||||||
pub acc: Account,
|
|
||||||
pub cash: Cash,
|
|
||||||
pub vouchers: Vec<(VoucherType, i64)>,
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
|
||||||
#[cfg_attr(feature = "server", derive(sqlx::Type))]
|
|
||||||
#[cfg_attr(feature = "server", repr(i64))]
|
|
||||||
pub enum Store {
|
|
||||||
Aldi = 1,
|
|
||||||
Edeka = 2,
|
|
||||||
Dm = 3,
|
|
||||||
Lidl = 4,
|
|
||||||
Rewe = 5,
|
|
||||||
Tegut = 6,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Into<String> for &Store {
|
|
||||||
fn into(self) -> String {
|
|
||||||
String::from(match *self {
|
|
||||||
Store::Aldi => "aldi",
|
|
||||||
Store::Edeka => "edeka",
|
|
||||||
Store::Dm => "dm",
|
|
||||||
Store::Lidl => "lidl",
|
|
||||||
Store::Rewe => "rewe",
|
|
||||||
Store::Tegut => "tegut",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&str> for Store {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
|
||||||
match s {
|
|
||||||
"aldi" => Ok(Store::Aldi),
|
|
||||||
"edeka" => Ok(Store::Edeka),
|
|
||||||
"dm" => Ok(Store::Dm),
|
|
||||||
"lidl" => Ok(Store::Lidl),
|
|
||||||
"rewe" => Ok(Store::Rewe),
|
|
||||||
"tegut" => Ok(Store::Tegut),
|
|
||||||
_ => Err(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
[formatting]
|
|
||||||
indent_string = " "
|
|
||||||
reorder_keys = true
|
|
33
tauri.conf.json
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "Nein!",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"identifier": "de.mathebau.bkbh",
|
||||||
|
"build": {
|
||||||
|
"frontendDist": "www"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"withGlobalTauri": true,
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "Nein zur Bezahlkarte!",
|
||||||
|
"width": 800,
|
||||||
|
"height": 600
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "all",
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
13
trunk.html
|
@ -1,13 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link data-trunk rel="scss" href="styles.scss" />
|
|
||||||
<link data-trunk rel="copy-dir" href="assets" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Darmstadt sagt Nein zur Bezahlkartei!</title>
|
|
||||||
<link data-trunk rel="rust" href="." data-bin="leptos" data-cargo-features="leptos" data-wasm-opt="4" data-weak-refs />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 7 KiB After Width: | Height: | Size: 7 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
188
www/index.html
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="stylesheet" href="styles.css" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Darmstadt sagt Nein zur Bezahlkartei!</title>
|
||||||
|
<script type="module" src="main.js" defer></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="v-container" id="cafe" style="display: none">
|
||||||
|
<div id="cafe-voucher" style="display: none">
|
||||||
|
<h1 class="shout">
|
||||||
|
Sumpf
|
||||||
|
</h1>
|
||||||
|
<div class="h-container">
|
||||||
|
<form class="column" id="cafe-voucher-aldi">
|
||||||
|
<button type="submit">
|
||||||
|
<img
|
||||||
|
src="assets/aldi.svg"
|
||||||
|
class="logo"
|
||||||
|
alt="ALDI-Süd-Logo"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form class="column" id="cafe-voucher-dm">
|
||||||
|
<button type="submit">
|
||||||
|
<img
|
||||||
|
src="assets/dm.svg"
|
||||||
|
class="logo"
|
||||||
|
alt="dm-Logo"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form class="column" id="cafe-voucher-lidl">
|
||||||
|
<button type="submit">
|
||||||
|
<img
|
||||||
|
src="assets/lidl.svg"
|
||||||
|
class="logo"
|
||||||
|
alt="Lidl-Logo"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form class="column" id="cafe-voucher-rewe">
|
||||||
|
<button type="submit">
|
||||||
|
<img
|
||||||
|
src="assets/rewe.svg"
|
||||||
|
class="logo"
|
||||||
|
alt="Rewe-Logo"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form class="column" id="cafe-voucher-tegut">
|
||||||
|
<button type="submit">
|
||||||
|
<img
|
||||||
|
src="assets/tegut.svg"
|
||||||
|
class="logo"
|
||||||
|
alt="Tegut-Logo"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="cafe-inventory" style="display: none">
|
||||||
|
<form>
|
||||||
|
<div style="display: contents">
|
||||||
|
<div class="labelled-input">
|
||||||
|
<input type="radio" id="cafe-inventory-acc-sumpf" name="cafe-inventory-acc" value="sumpf" required>
|
||||||
|
<label for="cafe-inventory-acc-sumpf">Sumpf</label>
|
||||||
|
</div>
|
||||||
|
<div class="labelled-input">
|
||||||
|
<input type="radio" id="cafe-inventory-acc-hs" name="cafe-inventory-acc" value="hs" required>
|
||||||
|
<label for="cafe-inventory-acc-hs">Heinersyndikat</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: contents">
|
||||||
|
<div class="labelled-input" id="cafe-inventory-aldi">
|
||||||
|
<label for="cafe-inventory-aldi">
|
||||||
|
<img
|
||||||
|
src="assets/aldi.svg"
|
||||||
|
class="logo"
|
||||||
|
alt="ALDI-Süd-Logo"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<input type="number" name="cafe-inventory-aldi" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="labelled-input" id="cafe-inventory-dm">
|
||||||
|
<label for="cafe-inventory-dm">
|
||||||
|
<img
|
||||||
|
src="assets/dm.svg"
|
||||||
|
class="logo"
|
||||||
|
alt="dm-Logo"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<input type="number" name="cafe-inventory-dm" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="labelled-input" id="cafe-inventory-lidl">
|
||||||
|
<label for="cafe-inventory-lidl">
|
||||||
|
<img
|
||||||
|
src="assets/lidl.svg"
|
||||||
|
class="logo"
|
||||||
|
alt="Lidl-Logo"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<input type="number" name="cafe-inventory-lidl" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="labelled-input" id="cafe-inventory-rewe">
|
||||||
|
<label for="cafe-inventory-rewe">
|
||||||
|
<img
|
||||||
|
src="assets/rewe.svg"
|
||||||
|
class="logo"
|
||||||
|
alt="Rewe-Logo"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<input type="number" name="cafe-inventory-rewe" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="labelled-input" id="cafe-inventory-tegut">
|
||||||
|
<label for="cafe-inventory-tegut">
|
||||||
|
<img
|
||||||
|
src="assets/tegut.svg"
|
||||||
|
class="logo"
|
||||||
|
alt="Tegut-Logo"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<input type="number" name="cafe-inventory-tegut" min="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="labelled-input" id="cafe-inventory-cash">
|
||||||
|
<label for="cafe-inventory-cash">
|
||||||
|
<img
|
||||||
|
src="assets/cash.svg"
|
||||||
|
class="logo"
|
||||||
|
alt="Bargeld"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<input type="number" name="cafe-inventory-cash" min="0" step=".01">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="shout">
|
||||||
|
Senden
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="v-container nav" id="cafe-nav">
|
||||||
|
<form id="cafe-nav-inventory">
|
||||||
|
<button type="submit">
|
||||||
|
<span class="shout">
|
||||||
|
Bestand
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form id="cafe-nav-voucher">
|
||||||
|
<button type="submit">
|
||||||
|
<span class="shout">
|
||||||
|
Annahme
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="angel">
|
||||||
|
<div>
|
||||||
|
<form class="h-container">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="v-container nav" id="nav">
|
||||||
|
<form class="column" id="nav-cafe">
|
||||||
|
<button type="submit">
|
||||||
|
<img
|
||||||
|
src="assets/cafe.svg"
|
||||||
|
class="logo"
|
||||||
|
alt="Tauschcafé"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form class="column" id="nav-angel">
|
||||||
|
<button type="submit">
|
||||||
|
<img
|
||||||
|
src="assets/angel.svg"
|
||||||
|
class="logo"
|
||||||
|
alt="Botengang"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
67
www/main.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
const { invoke } = window.__TAURI__.core;
|
||||||
|
|
||||||
|
let callbacks = {
|
||||||
|
'#nav-cafe': () => activate("", "cafe", ["angel"]),
|
||||||
|
'#nav-angel': () => activate("", "angel", ["cafe"]),
|
||||||
|
'#cafe-nav-inventory': () => activate("cafe-", "inventory", ["voucher"]),
|
||||||
|
'#cafe-nav-voucher': () => activate("cafe-", "voucher", ["inventory"]),
|
||||||
|
'#cafe-inventory form': () => inventory(),
|
||||||
|
'#cafe-voucher-aldi': () => swap("aldi"),
|
||||||
|
'#cafe-voucher-dm': () => swap("dm"),
|
||||||
|
'#cafe-voucher-lidl': () => swap("lidl"),
|
||||||
|
'#cafe-voucher-rewe': () => swap("rewe"),
|
||||||
|
'#cafe-voucher-tegut': () => swap("tegut"),
|
||||||
|
}
|
||||||
|
let stores = [
|
||||||
|
"aldi",
|
||||||
|
"dm",
|
||||||
|
"lidl",
|
||||||
|
"rewe",
|
||||||
|
"tegut",
|
||||||
|
]
|
||||||
|
|
||||||
|
async function increment(el) {
|
||||||
|
var el = document.querySelector(el+" input");
|
||||||
|
let v = parseInt(el.value, 10);
|
||||||
|
v = isNaN(v) ? 0 : v;
|
||||||
|
v++;
|
||||||
|
el.value = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function inventory() {
|
||||||
|
const form = document.querySelector("#cafe-inventory form");
|
||||||
|
const fd = new FormData(form);
|
||||||
|
const obj = Object.fromEntries(fd);
|
||||||
|
await invoke("inventory", { data: obj });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function swap(s) {
|
||||||
|
await invoke("swap", { store: s, acc: 1 });
|
||||||
|
document.querySelector("h1").textContent = await invoke("count", {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function activate(ctx, el, nels) {
|
||||||
|
document.querySelector("#"+ctx+el).style.display = "";
|
||||||
|
for (const nel of nels) {
|
||||||
|
document.querySelector("#"+ctx+nel).style.display = "none";
|
||||||
|
}
|
||||||
|
document.querySelector("#"+ctx+"nav").classList.remove("v-container");
|
||||||
|
document.querySelector("#"+ctx+"nav").classList.add("h-container");
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
|
for (let key in callbacks) {
|
||||||
|
if (callbacks.hasOwnProperty(key)) {
|
||||||
|
document.querySelector(key).addEventListener("submit", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
callbacks[key]();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const store of stores) {
|
||||||
|
document.querySelector("#cafe-inventory-"+store+" label").addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
increment("#cafe-inventory-"+store);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -106,8 +106,7 @@ input[type=number] {
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=radio] {
|
input[type=radio] {
|
||||||
width: 100%;
|
height: 60%;
|
||||||
height: 7mm;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|