diff --git a/Cargo.lock b/Cargo.lock index 852a9f2..35240fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,12 +47,38 @@ dependencies = [ "libc", ] +[[package]] +name = "angle" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf965b6b142c8c68150e815ef527d17f0fe74e622b2c5287fd8626cb7c4a5fa" +dependencies = [ + "num-traits", + "serde", + "serde_derive", +] + [[package]] name = "anyhow" version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "arbitrary" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + [[package]] name = "async-stream" version = "0.3.6" @@ -104,6 +130,36 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "attribute-derive" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05832cdddc8f2650cc2cc187cc2e952b8c133a48eb055f35211f61ee81502d77" +dependencies = [ + "attribute-derive-macro", + "derive-where", + "manyhow", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "attribute-derive-macro" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7cdbbd4bd005c5d3e2e9c885e6fa575db4f4a3572335b974d8db853b6beb61" +dependencies = [ + "collection_literals", + "interpolator", + "manyhow", + "proc-macro-utils", + "proc-macro2", + "quote", + "quote-use", + "syn 2.0.119", +] + [[package]] name = "autocfg" version = "1.3.0" @@ -176,6 +232,30 @@ dependencies = [ "objc2", ] +[[package]] +name = "borsh" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56" +dependencies = [ + "once_cell", + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "brotli" version = "9.0.0" @@ -208,9 +288,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "bytemuck" @@ -340,6 +426,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + [[package]] name = "chrono" version = "0.4.38" @@ -353,6 +445,25 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "collection_literals" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084" + +[[package]] +name = "color-rs2" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61da6f06f340226f2707c0d3a8de27e8b1cbe09050d440cbb80a0ec87ccbf79e" +dependencies = [ + "angle", + "half", + "num-traits", + "serde", + "serde_derive", +] + [[package]] name = "combine" version = "4.6.8" @@ -413,6 +524,12 @@ dependencies = [ "libc", ] +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -446,6 +563,12 @@ version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -570,6 +693,28 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive-where" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e2b94854e8576378ccda7c8de8a66ed8b4e8acbd2c50ec3418ea6c8aaf4b567" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "derive_arbitrary" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b034bd7d5f032402a2479444dcc6f74e36a03f31854d41680fb240ef682a1ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "derive_more" version = "2.1.1" @@ -739,6 +884,20 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1020,6 +1179,37 @@ dependencies = [ "version_check", ] +[[package]] +name = "get-size-derive2" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b6d1e2f75c16bfbcd0f95d84f99858a6e2f885c2287d1f5c3a96e8444a34b4" +dependencies = [ + "attribute-derive", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "get-size2" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49cf31a6d70300cf81461098f7797571362387ef4bf85d32ac47eaa59b3a5a1a" +dependencies = [ + "get-size-derive2", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -1191,6 +1381,17 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1489,6 +1690,12 @@ dependencies = [ "cfb", ] +[[package]] +name = "interpolator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" + [[package]] name = "ipnet" version = "2.12.2" @@ -1665,6 +1872,24 @@ dependencies = [ "serde", ] +[[package]] +name = "kparse" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503149c856ef5f8728156b55609019cf2714a29ebdb2a82dc1a831d2c743f28a" +dependencies = [ + "bytecount", + "memchr", + "nom", + "nom_locate", +] + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + [[package]] name = "libappindicator" version = "0.9.0" @@ -1740,9 +1965,32 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "manyhow" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "manyhow-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] [[package]] name = "markup5ever" @@ -1776,6 +2024,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1828,6 +2082,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + [[package]] name = "ndk" version = "0.9.0" @@ -1864,6 +2124,27 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom_locate" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e3c83c053b0713da60c5b8de47fe8e494fe3ece5267b2f23090a07a053ba8f3" +dependencies = [ + "bytecount", + "memchr", + "nom", +] + [[package]] name = "num-conv" version = "0.2.2" @@ -2025,6 +2306,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.13.2", "block2", + "libc", "objc2", "objc2-core-foundation", ] @@ -2116,10 +2398,13 @@ dependencies = [ "chrono", "r701", "serde", + "spreadsheet-ods", "tauri", "tauri-build", + "tauri-plugin-dialog", "tokio-stream", "toml 0.9.12+spec-1.1.0", + "zip", ] [[package]] @@ -2249,7 +2534,7 @@ checksum = "2896bade328c13f7042a297ea5ac5b0951f6cf989dea5f32c2fd98da398195cb" dependencies = [ "base64 0.23.1", "indexmap 2.14.2", - "quick-xml", + "quick-xml 0.42.0", "serde", "time", ] @@ -2310,6 +2595,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "precomputed-hash" version = "0.1.1" @@ -2369,6 +2663,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -2378,6 +2683,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "encoding_rs", + "memchr", +] + [[package]] name = "quick-xml" version = "0.42.0" @@ -2396,6 +2711,28 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quote-use" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e" +dependencies = [ + "quote", + "quote-use-macros", +] + +[[package]] +name = "quote-use-macros" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "r-efi" version = "5.3.0" @@ -2419,6 +2756,65 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -2527,6 +2923,57 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rust_decimal" +version = "1.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7653272e75dcac41dc199fbea6f5797633994fafd339943c06c9af16bf29cd3a" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.8", + "rand 0.9.5", + "serde", + "serde_json", + "wasm-bindgen", +] + +[[package]] +name = "rust_decimal_macros" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74a5a6f027e892c7a035c6fddb50435a1fbf5a734ffc0c2a9fed4d0221440519" +dependencies = [ + "quote", + "syn 2.0.119", +] + [[package]] name = "rustc-hash" version = "2.1.3" @@ -2819,6 +3266,12 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "1.0.4" @@ -2895,6 +3348,28 @@ dependencies = [ "system-deps", ] +[[package]] +name = "spreadsheet-ods" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60163f438f3d198d0f871cb0edd5577efeaa8d83ec41a96ede01fbb20d0dcb40" +dependencies = [ + "base64 0.22.1", + "chrono", + "color-rs2", + "get-size2", + "icu_locale_core", + "kparse", + "lazy_static", + "nom", + "nom_locate", + "quick-xml 0.38.4", + "rust_decimal", + "rust_decimal_macros", + "string_cache", + "zip", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -2911,6 +3386,7 @@ dependencies = [ "parking_lot", "phf_shared", "precomputed-hash", + "serde", ] [[package]] @@ -3177,6 +3653,64 @@ dependencies = [ "tauri-utils", ] +[[package]] +name = "tauri-plugin" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1140cf34a3b3b836a13103dcab17f18831d5cc3534cbd435dc01a5c6daa65aa2" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf9a5c92e39bdd84f22f6be9230e39d0c4e140345f71ffe6058dca4f71127cf" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.21", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "466eb32466fe5021b6728b06862f9ff14a9f7e1af890047654e03a96d7c0e1bb" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.21", + "toml 1.1.6+spec-1.1.0", + "url", +] + [[package]] name = "tauri-runtime" version = "2.12.1" @@ -3776,6 +4310,7 @@ dependencies = [ "cfg-if", "once_cell", "rustversion", + "serde", "wasm-bindgen-macro", "wasm-bindgen-shared", ] @@ -4121,6 +4656,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -4169,13 +4713,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.5", "windows_aarch64_msvc 0.52.5", "windows_i686_gnu 0.52.5", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.5", "windows_i686_msvc 0.52.5", "windows_x86_64_gnu 0.52.5", "windows_x86_64_gnullvm 0.52.5", "windows_x86_64_msvc 0.52.5", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows-threading" version = "0.2.1" @@ -4212,6 +4773,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -4230,6 +4797,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -4248,12 +4821,24 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -4272,6 +4857,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -4290,6 +4881,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -4308,6 +4905,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -4326,6 +4929,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.5.40" @@ -4460,6 +5069,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "zerofrom" version = "0.1.8" @@ -4514,6 +5143,21 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "zip" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap 2.14.2", + "memchr", + "time", + "zopfli", +] + [[package]] name = "zlib-rs" version = "0.6.8" @@ -4525,3 +5169,15 @@ name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/oxibadge/bun.lock b/oxibadge/bun.lock index e6ba8e4..8b59ed8 100644 --- a/oxibadge/bun.lock +++ b/oxibadge/bun.lock @@ -9,6 +9,7 @@ "@fontsource/geist": "^5.3.0", "@fontsource/geist-mono": "^5.3.0", "@tauri-apps/api": "^2", + "@tauri-apps/plugin-dialog": "^2.8.1", "svelte-i18n": "^4.0.1", }, "devDependencies": { @@ -170,6 +171,8 @@ "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.12.1", "", { "os": "win32", "cpu": "x64" }, "sha512-dIFaKslrzcbesb+bBh+E1R9km241pHyj32w6D9FPtvQeYpRUFbiCLEa/2S+S0UnIytfxepp4P+X8IerGqnXeYg=="], + "@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.8.1", "", { "dependencies": { "@tauri-apps/api": "^2.12.0" } }, "sha512-/DtE3B62JgpYuWFL5ATXM6KOXUq7pca7cJWOgSqULhZsgyjLk0PWKek1dbRDvhNBW6flNsOfWphsKnb+738Dmw=="], + "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], diff --git a/oxibadge/package.json b/oxibadge/package.json index 19747ab..1a27b26 100644 --- a/oxibadge/package.json +++ b/oxibadge/package.json @@ -18,6 +18,7 @@ "@fontsource/geist": "^5.3.0", "@fontsource/geist-mono": "^5.3.0", "@tauri-apps/api": "^2", + "@tauri-apps/plugin-dialog": "^2.8.1", "svelte-i18n": "^4.0.1" }, "devDependencies": { diff --git a/oxibadge/src-tauri/Cargo.toml b/oxibadge/src-tauri/Cargo.toml index 71c98d5..df4dfc8 100644 --- a/oxibadge/src-tauri/Cargo.toml +++ b/oxibadge/src-tauri/Cargo.toml @@ -22,3 +22,6 @@ toml = "0.9" r701 = { path = "../../r701", features = ["serde"] } chrono = { version = "0.4.38", default-features = false, features = ["clock"] } tokio-stream = { version = "0.1.19", default-features = false } +tauri-plugin-dialog = "2.8.1" +spreadsheet-ods = "1.0.4" +zip = { version = "6", default-features = false, features = ["deflate"] } diff --git a/oxibadge/src-tauri/capabilities/default.json b/oxibadge/src-tauri/capabilities/default.json index 194c1d3..dd51a8f 100644 --- a/oxibadge/src-tauri/capabilities/default.json +++ b/oxibadge/src-tauri/capabilities/default.json @@ -5,6 +5,7 @@ "windows": ["main"], "permissions": [ "core:default", - "core:window:allow-destroy" + "core:window:allow-destroy", + "dialog:default" ] } diff --git a/oxibadge/src-tauri/src/export.rs b/oxibadge/src-tauri/src/export.rs new file mode 100644 index 0000000..9ba0f62 --- /dev/null +++ b/oxibadge/src-tauri/src/export.rs @@ -0,0 +1,383 @@ +//! Builds the `.ods` timesheet file(s) the review screen's export button +//! produces, matching a specific layout/style the user already relies on +//! in their own spreadsheets (see the project history for the reference +//! file this was reverse-engineered from): a date column, four punch +//! columns (entry/exit for each shift) and a formula-driven "hours worked" +//! column with a monthly total. Column headers are plain text sent by the +//! frontend (translated via its own i18n), not hardcoded here. +//! +//! One employee gets one sheet in one `.ods` file. Exporting more than one +//! employee at once bundles one `.ods` per employee into a `.zip`. + +use chrono::{Duration, NaiveDate}; +use serde::Deserialize; +use spreadsheet_ods::color::Rgb; +use spreadsheet_ods::defaultstyles::DefaultFormat; +use spreadsheet_ods::style::units::{Border, FontSize, TextAlign}; +use spreadsheet_ods::{ + write_ods_buf, CellStyle, CellStyleRef, Length, Sheet, ValueFormatRef, ValueFormatTimeDuration, + WorkBook, +}; +use std::collections::HashSet; +use std::io::Write; +use std::path::Path; + +/// One employee's corrected timesheet for a month, as sent by the review +/// screen's export button. `days[d][c]` is the list of times punched in +/// slot `c` (entry/exit of the 1st/2nd shift) on day `d + 1`; usually 0 or +/// 1 entries, more if the day had stacked punches. A stacked slot's times +/// all end up in the same cell (one per line), matching how the review +/// screen itself stacks them — the export is a 1:1 copy of the app's data, +/// nothing is dropped. +#[derive(Debug, Deserialize)] +pub struct EmployeeTimesheet { + pub id: u32, + pub name: String, + pub days: Vec>>, +} + +/// The column header text, already translated by the frontend. +#[derive(Debug, Deserialize)] +pub struct ColumnHeaders { + pub date: String, + pub in1: String, + pub out1: String, + pub in2: String, + pub out2: String, + pub total: String, +} + +#[derive(Debug, Deserialize)] +pub struct ExportPayload { + #[allow(dead_code)] + pub scope: String, + pub year: i32, + pub month: u32, + pub employees: Vec, + pub headers: ColumnHeaders, +} + +// Column layout, 0-based. A and G are narrow blank spacer columns, matching +// the reference file. +const COL_DATE: u32 = 1; +const COL_IN1: u32 = 2; +const COL_OUT1: u32 = 3; +const COL_IN2: u32 = 4; +const COL_OUT2: u32 = 5; +const COL_TOTAL: u32 = 7; + +const ROW_HEADER: u32 = 1; +const ROW_FIRST_DAY: u32 = 2; + +struct Styles { + header_date: CellStyleRef, + header_time: CellStyleRef, + header_total: CellStyleRef, + date_a: CellStyleRef, + date_b: CellStyleRef, + time_a: CellStyleRef, + time_b: CellStyleRef, + total_a: CellStyleRef, + total_b: CellStyleRef, +} + +/// A centered, bordered, 10pt Liberation Sans cell style, optionally tinted. +fn styled(name: &str, format: &ValueFormatRef, bg: Option>) -> CellStyle { + let mut s = CellStyle::new(name, format); + s.set_font_name("Liberation Sans"); + s.set_font_size(FontSize::from(Length::Pt(10.0))); + s.set_color(Rgb::new(0, 0, 0)); + s.set_text_align(TextAlign::Center); + s.set_border(Length::Pt(0.74), Border::Solid, Rgb::new(0, 0, 0)); + if let Some(rgb) = bg { + s.set_background_color(rgb); + } + s +} + +fn build_styles(wb: &mut WorkBook) -> Styles { + let mut duration_fmt = ValueFormatTimeDuration::new_named("ox-duration"); + duration_fmt.set_truncate_on_overflow(false); + duration_fmt.part_hours().long_style().build(); + duration_fmt.part_text(":").build(); + duration_fmt.part_minutes().long_style().build(); + let duration_fmt = wb.add_timeduration_format(duration_fmt); + + let plain = DefaultFormat::default(); + + Styles { + header_date: wb.add_cellstyle(styled( + "ox-header-date", + &plain, + Some(Rgb::new(0x3f, 0xaf, 0x46)), + )), + header_time: wb.add_cellstyle(styled( + "ox-header-time", + &plain, + Some(Rgb::new(0x59, 0x83, 0xb0)), + )), + header_total: wb.add_cellstyle(styled( + "ox-header-total", + &plain, + Some(Rgb::new(0xed, 0x4c, 0x05)), + )), + date_a: wb.add_cellstyle(styled("ox-date-a", &plain, None)), + date_b: wb.add_cellstyle(styled( + "ox-date-b", + &plain, + Some(Rgb::new(0xdd, 0xe8, 0xcb)), + )), + time_a: wb.add_cellstyle(styled("ox-time-a", &plain, None)), + time_b: wb.add_cellstyle(styled( + "ox-time-b", + &plain, + Some(Rgb::new(0xde, 0xe6, 0xef)), + )), + total_a: wb.add_cellstyle(styled("ox-total-a", &duration_fmt, None)), + total_b: wb.add_cellstyle(styled( + "ox-total-b", + &duration_fmt, + Some(Rgb::new(0xff, 0xdb, 0xb6)), + )), + } +} + +fn parse_hhmm(s: &str) -> Option { + let (h, m) = s.split_once(':')?; + let h: i64 = h.parse().ok()?; + let m: i64 = m.parse().ok()?; + if !(0..24).contains(&h) || !(0..60).contains(&m) { + return None; + } + Some(h * 60 + m) +} + +/// First punch of a slot, parsed to minutes since midnight, or 0 (midnight) +/// if missing/unparseable — same as how the embedded ODF formula treats a +/// blank cell in a subtraction. +fn first_minutes_or_zero(slot: &[String]) -> i64 { + slot.first().and_then(|s| parse_hhmm(s)).unwrap_or(0) +} + +/// Worked minutes for a day: (out1 - in1) + (out2 - in2). Matches the +/// "hours worked" column's formula term for term (blank cells count as 0), +/// so e.g. a day with only an in1 and an out2 punched (no lunch break) still +/// nets out to the full span via cancellation, exactly like the live +/// formula would compute once the file is opened. +fn worked_minutes(day: &[Vec]) -> i64 { + let at = |i: usize| { + day.get(i) + .map(Vec::as_slice) + .map(first_minutes_or_zero) + .unwrap_or(0) + }; + (at(1) - at(0)) + (at(3) - at(2)) +} + +// The review screen already requires every employee to have a name before +// it lets the user continue (see NewEmployees.svelte), so `name` here is +// never genuinely empty. The `id` fallback below only guards the much +// narrower case of a name that sanitizes down to nothing (e.g. "***"): +// not a missing name, just one unusable verbatim as a sheet/file name. + +fn sanitize_sheet_name(name: &str, id: u32) -> String { + let cleaned: String = name + .chars() + .map(|c| if "[]*?:/\\'".contains(c) { ' ' } else { c }) + .collect(); + let trimmed: String = cleaned.trim().chars().take(31).collect(); + if trimmed.is_empty() { + id.to_string() + } else { + trimmed + } +} + +fn sanitize_filename(name: &str, id: u32) -> String { + let cleaned: String = name + .chars() + .map(|c| { + if "/\\:*?\"<>|".contains(c) || c.is_control() { + '_' + } else { + c + } + }) + .collect(); + let trimmed = cleaned.trim().trim_matches('.').to_string(); + if trimmed.is_empty() { + id.to_string() + } else { + trimmed + } +} + +/// Builds the `.ods` bytes for one employee's month. +fn build_ods_bytes( + year: i32, + month: u32, + employee: &EmployeeTimesheet, + headers: &ColumnHeaders, +) -> Result, String> { + let mut wb = WorkBook::new_empty(); + let styles = build_styles(&mut wb); + + let mut sheet = Sheet::new(sanitize_sheet_name(&employee.name, employee.id)); + sheet.set_col_width(0, Length::In(0.1799)); + for col in [COL_DATE, COL_IN1, COL_OUT1, COL_IN2, COL_OUT2, COL_TOTAL] { + sheet.set_col_width(col, Length::In(0.8335)); + } + sheet.set_col_width(6, Length::In(0.1665)); + + sheet.set_styled( + ROW_HEADER, + COL_DATE, + headers.date.as_str(), + &styles.header_date, + ); + sheet.set_styled( + ROW_HEADER, + COL_IN1, + headers.in1.as_str(), + &styles.header_time, + ); + sheet.set_styled( + ROW_HEADER, + COL_OUT1, + headers.out1.as_str(), + &styles.header_time, + ); + sheet.set_styled( + ROW_HEADER, + COL_IN2, + headers.in2.as_str(), + &styles.header_time, + ); + sheet.set_styled( + ROW_HEADER, + COL_OUT2, + headers.out2.as_str(), + &styles.header_time, + ); + sheet.set_styled( + ROW_HEADER, + COL_TOTAL, + headers.total.as_str(), + &styles.header_total, + ); + + let mut row = ROW_FIRST_DAY; + let mut total_minutes = 0i64; + for (di, day) in employee.days.iter().enumerate() { + let empty = day.iter().all(|slot| slot.iter().all(|t| t.is_empty())); + if empty { + continue; + } + + let zebra_b = (row - ROW_FIRST_DAY) % 2 == 1; + let (date_style, time_style, total_style) = if zebra_b { + (&styles.date_b, &styles.time_b, &styles.total_b) + } else { + (&styles.date_a, &styles.time_a, &styles.total_a) + }; + + let date = NaiveDate::from_ymd_opt(year, month, di as u32 + 1) + .map(|d| d.format("%d/%m/%Y").to_string()) + .unwrap_or_default(); + sheet.set_styled(row, COL_DATE, date, date_style); + + for (col, slot_idx) in [(COL_IN1, 0), (COL_OUT1, 1), (COL_IN2, 2), (COL_OUT2, 3)] { + sheet.set_cellstyle(row, col, time_style); + if let Some(slot) = day.get(slot_idx) { + // A stacked slot (more than one punch) goes into the cell one time per + // line, so no punch the app shows is ever silently dropped from the export. + let joined = slot + .iter() + .filter(|t| !t.is_empty()) + .cloned() + .collect::>() + .join("\n"); + if !joined.is_empty() { + sheet.set_value(row, col, joined); + } + } + } + + let minutes = worked_minutes(day); + total_minutes += minutes; + let odf_row = row + 1; // ODF cell references are 1-based. + // spreadsheet-ods doesn't serialize negative durations correctly (emits per-component + // negative numbers, which Calc can't parse), so the cached value is clamped to 0 for an + // all-zero-or-negative day (unresolved/invalid source data); the live formula, once the + // file is recalculated, still shows the real (possibly negative or #VALUE!) result. + sheet.set_styled_value( + row, + COL_TOTAL, + Duration::minutes(minutes.max(0)), + total_style, + ); + sheet.set_formula( + row, + COL_TOTAL, + format!("of:=[.F{odf_row}]-[.E{odf_row}]+[.D{odf_row}]-[.C{odf_row}]"), + ); + + row += 1; + } + + let last_data_row = row - 1; + if last_data_row >= ROW_FIRST_DAY { + let total_row = row + 1; // one blank spacer row, like the reference file. + let first_odf_row = ROW_FIRST_DAY + 1; + let last_odf_row = last_data_row + 1; + sheet.set_styled_value( + total_row, + COL_TOTAL, + Duration::minutes(total_minutes.max(0)), + &styles.total_b, + ); + sheet.set_formula( + total_row, + COL_TOTAL, + format!("of:=SUM([.H{first_odf_row}:.H{last_odf_row}])"), + ); + } + + wb.push_sheet(sheet); + write_ods_buf(&mut wb, Vec::new()).map_err(|err| err.to_string()) +} + +/// Writes the export to `path`: a single `.ods` for one employee, or a +/// `.zip` of one `.ods` per employee otherwise. +pub fn export_to_path(payload: ExportPayload, path: &Path) -> Result<(), String> { + if payload.employees.len() <= 1 { + let employee = payload + .employees + .first() + .ok_or_else(|| "no employees to export".to_string())?; + let bytes = build_ods_bytes(payload.year, payload.month, employee, &payload.headers)?; + std::fs::write(path, bytes).map_err(|err| err.to_string()) + } else { + let file = std::fs::File::create(path).map_err(|err| err.to_string())?; + let mut zip = zip::ZipWriter::new(file); + let options = zip::write::SimpleFileOptions::default() + .compression_method(zip::CompressionMethod::Deflated); + + let mut used = HashSet::new(); + for employee in &payload.employees { + let bytes = build_ods_bytes(payload.year, payload.month, employee, &payload.headers)?; + let base = sanitize_filename(&employee.name, employee.id); + let mut name = format!("{base}.ods"); + let mut n = 2; + while !used.insert(name.clone()) { + name = format!("{base} ({n}).ods"); + n += 1; + } + zip.start_file(&name, options) + .map_err(|err| err.to_string())?; + zip.write_all(&bytes).map_err(|err| err.to_string())?; + } + zip.finish().map_err(|err| err.to_string())?; + Ok(()) + } +} diff --git a/oxibadge/src-tauri/src/lib.rs b/oxibadge/src-tauri/src/lib.rs index be3df7b..5f50b40 100644 --- a/oxibadge/src-tauri/src/lib.rs +++ b/oxibadge/src-tauri/src/lib.rs @@ -1,11 +1,13 @@ mod employees; +mod export; mod preferences; use chrono::{Local, TimeZone}; +use export::ExportPayload; use preferences::Settings; use r701::{Record, R701}; -use serde::Deserialize; use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; use tauri::AppHandle; use tokio_stream::StreamExt; @@ -130,42 +132,18 @@ async fn load_employees( Ok(employees) } -/// One employee's corrected timesheet for a month, as sent by the review -/// screen's export button. `days[d][c]` is the list of times (usually 0 -/// or 1, more if stacked) punched in slot `c` (entry/exit of the 1st/2nd -/// shift) on day `d + 1`. -/// -/// For now `export_timesheet` only `dbg!`-prints this, so none of the -/// fields are read outside of the derived `Debug` impl (which the -/// dead-code lint deliberately ignores). -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -struct EmployeeTimesheet { - id: u32, - name: String, - days: Vec>>, -} - -#[derive(Debug, Deserialize)] -struct ExportPayload { - scope: String, - year: i32, - month: u32, - employees: Vec, -} - -/// Placeholder export: just prints each employee's corrected timesheet. +/// Writes the review screen's corrected timesheet to `path`, chosen by the +/// frontend via the native "save" dialog: a single `.ods` for one employee, +/// or a `.zip` of one `.ods` per employee for an "all employees" export. #[tauri::command] -fn export_timesheet(payload: ExportPayload) { - dbg!(&payload.scope, payload.year, payload.month); - for employee in &payload.employees { - dbg!(employee); - } +fn export_timesheet(payload: ExportPayload, path: String) -> Result<(), String> { + export::export_to_path(payload, &PathBuf::from(path)) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) .invoke_handler(tauri::generate_handler![ load_settings, save_settings, diff --git a/oxibadge/src/lib/components/NewEmployees.svelte b/oxibadge/src/lib/components/NewEmployees.svelte index ab8946a..dc0b952 100644 --- a/oxibadge/src/lib/components/NewEmployees.svelte +++ b/oxibadge/src/lib/components/NewEmployees.svelte @@ -1,7 +1,7 @@ {#if step === 'review'} @@ -152,7 +176,7 @@ employees={reviewEmployees} shift={shifts} onback={() => requestLeaveReview(() => (step = 'confirm-month'))} - onexport={(payload) => invoke('export_timesheet', { payload })} + onexport={onExport} onrename={onEmployeeRenamed} ondirtychange={(dirty) => (reviewDirty = dirty)} />