Compare commits

..

No commits in common. "leptos" and "main" have entirely different histories.
leptos ... main

47 changed files with 804 additions and 2356 deletions

7
.gitignore vendored
View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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 }

View file

@ -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
``` ```

View file

@ -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"

View file

@ -1,9 +0,0 @@
[build]
dist = "www"
html_output = "index.html"
target = "trunk.html"
[serve]
open = false
port = 1420
ws_protocol = "ws"

View file

@ -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)");
}
} }

View file

@ -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
View 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"
]
}

View file

@ -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()

View file

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Before After
Before After

View file

@ -1,4 +0,0 @@
indentation_style = "Tabs"
max_width = 80
newline_style = "Unix"
tab_spaces = 8

View file

@ -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`)
);

View file

@ -1,2 +0,0 @@
[rustfmt]
overrideCommand = ["leptosfmt", "--stdin", "--rustfmt"]

View file

@ -1,4 +1,3 @@
edition = "2024"
hard_tabs = true
max_width = 80 max_width = 80
hard_tabs = true
tab_spaces = 8 tab_spaces = 8

View file

@ -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,

View file

@ -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 /> });
}

View file

@ -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
}

View file

@ -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(())
} }

View file

@ -1,8 +0,0 @@
use leptos::prelude::*;
#[component]
pub fn Angel() -> impl IntoView {
view! {
<p>Hi</p>
}
}

View file

@ -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>
}
}

View file

@ -1,3 +0,0 @@
pub mod angel;
pub mod cafe;
pub mod store;

View file

@ -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)
/>
}
}

View file

@ -1,18 +1,184 @@
pub mod commands; use chrono::offset::Utc;
#[cfg(feature = "leptos")] use rusqlite::{types::ToSqlOutput, ToSql};
pub mod leptos;
#[cfg(feature = "server")]
pub mod server;
pub mod types;
#[cfg(all(feature = "tauri", feature = "server"))]
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
use server::app_state::AppState;
use tauri::{Manager, State}; use tauri::{Manager, State};
use tauri_plugin_fs::FsExt; use tauri_plugin_fs::FsExt;
use tokio::sync::Mutex; use tokio::sync::Mutex;
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_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
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");

View file

@ -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()
} }

View file

@ -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(())
}

View file

@ -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)
}
}

View file

@ -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))
}
}

View file

@ -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)>,
}

View file

@ -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(()),
}
}
}

View file

@ -1,3 +0,0 @@
[formatting]
indent_string = " "
reorder_keys = true

33
tauri.conf.json Normal file
View 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"
]
}
}

View file

@ -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>

View file

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Before After
Before After

188
www/index.html Normal file
View 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
View 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);
});
}
});

View file

@ -106,8 +106,7 @@ input[type=number] {
} }
input[type=radio] { input[type=radio] {
width: 100%; height: 60%;
height: 7mm;
} }
button:hover { button:hover {