From b16a716c6cfed813e5d82ffddaf4398b384bca59 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 5 May 2025 22:29:45 +0200 Subject: [PATCH] Add basic Excel export --- moneymgr_backend/Cargo.lock | 10 ++++ moneymgr_backend/Cargo.toml | 3 +- .../src/controllers/backup_controller.rs | 46 ++++++++++++++++++ moneymgr_backend/src/controllers/mod.rs | 2 + moneymgr_backend/src/main.rs | 4 ++ moneymgr_web/src/api/BackupApi.ts | 7 +++ moneymgr_web/src/routes/BackupRoute.tsx | 9 ++++ moneymgr_web/src/routes/excel.svg | 48 +++++++++++++++++++ 8 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 moneymgr_web/src/routes/excel.svg diff --git a/moneymgr_backend/Cargo.lock b/moneymgr_backend/Cargo.lock index 9b5157e..64cfdff 100644 --- a/moneymgr_backend/Cargo.lock +++ b/moneymgr_backend/Cargo.lock @@ -2282,6 +2282,7 @@ dependencies = [ "rand 0.9.0", "rust-embed", "rust-s3", + "rust_xlsxwriter", "serde", "serde_json", "sha2", @@ -2990,6 +2991,15 @@ dependencies = [ "url", ] +[[package]] +name = "rust_xlsxwriter" +version = "0.86.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ab327483e6b6fc521b7e303691c14bf3624a68e0cb4c2b9f9c5d692e07f637" +dependencies = [ + "zip", +] + [[package]] name = "rustc-demangle" version = "0.1.24" diff --git a/moneymgr_backend/Cargo.toml b/moneymgr_backend/Cargo.toml index 70d3fcf..b76f386 100644 --- a/moneymgr_backend/Cargo.toml +++ b/moneymgr_backend/Cargo.toml @@ -34,4 +34,5 @@ sha2 = "0.10.8" httpdate = "1.0.3" chrono = "0.4.41" tempfile = "3.19.1" -zip = "2.6.1" \ No newline at end of file +zip = "2.6.1" +rust_xlsxwriter = "0.86.1" \ No newline at end of file diff --git a/moneymgr_backend/src/controllers/backup_controller.rs b/moneymgr_backend/src/controllers/backup_controller.rs index 0901b71..8b0649f 100644 --- a/moneymgr_backend/src/controllers/backup_controller.rs +++ b/moneymgr_backend/src/controllers/backup_controller.rs @@ -16,6 +16,7 @@ use crate::services::{accounts_service, files_service, movements_service}; use crate::utils::time_utils::{format_date, time}; use actix_files::NamedFile; use actix_web::{HttpRequest, HttpResponse}; +use rust_xlsxwriter::{Color, Format, Workbook}; use serde::Serialize; use serde::de::DeserializeOwned; use std::collections::HashMap; @@ -257,3 +258,48 @@ pub async fn zip_import(auth: AuthExtractor, file: FileExtractor) -> HttpResult Ok(HttpResponse::Accepted().finish()) } + +/// Export all movement to XSLX +pub async fn xslx_export(auth: AuthExtractor) -> HttpResult { + let mut workbook = Workbook::new(); + + for account in accounts_service::get_list_user(auth.user_id()).await? { + let worksheet = workbook.add_worksheet(); + worksheet.set_name(account.name.to_string())?; + + // Configure columns + let header_format = Format::new() + .set_bold() + .set_background_color(Color::Black) + .set_foreground_color(Color::White); + + worksheet.set_column_width(0, 15)?; + worksheet.set_column_width(1, 70)?; + worksheet.set_column_width(2, 15)?; + + // Write headers + worksheet.write_with_format(0, 0, "Date", &header_format)?; + worksheet.write_with_format(0, 1, "Label", &header_format)?; + worksheet.write_with_format(0, 2, "Amount", &header_format)?; + + // Write movements + let mut movements = movements_service::get_list_account(account.id()).await?; + movements.sort_by(|a, b| a.time.to_string().cmp(&b.time.to_string())); + for (idx, movement) in movements.iter().enumerate() { + worksheet.write(idx as u32 + 1, 0, format_date(movement.time)?)?; + worksheet.write(idx as u32 + 1, 1, &movement.label)?; + worksheet.write(idx as u32 + 1, 2, movement.amount)?; + } + } + + // Save final Excel Document + let raw_excel = workbook.save_to_buffer()?; + + Ok(HttpResponse::Ok() + .content_type("application/vnd.ms-excel") + .insert_header(( + "Content-Disposition", + format!("attachment; filename={}", export_filename("xslx")), + )) + .body(raw_excel)) +} diff --git a/moneymgr_backend/src/controllers/mod.rs b/moneymgr_backend/src/controllers/mod.rs index 61a8672..28e1ca3 100644 --- a/moneymgr_backend/src/controllers/mod.rs +++ b/moneymgr_backend/src/controllers/mod.rs @@ -38,6 +38,8 @@ pub enum HttpFailure { StdIoError(#[from] std::io::Error), #[error("an error occurred while performing backup/recovery operation: {0}")] BackupControllerError(#[from] BackupControllerError), + #[error("an error while encoding Excel document: {0}")] + XslxError(#[from] rust_xlsxwriter::XlsxError), } impl ResponseError for HttpFailure { diff --git a/moneymgr_backend/src/main.rs b/moneymgr_backend/src/main.rs index a238189..bed0b2f 100644 --- a/moneymgr_backend/src/main.rs +++ b/moneymgr_backend/src/main.rs @@ -178,6 +178,10 @@ async fn main() -> std::io::Result<()> { "/api/backup/zip/import", web::post().to(backup_controller::zip_import), ) + .route( + "/api/backup/xslx/export", + web::get().to(backup_controller::xslx_export), + ) // Static assets .route("/", web::get().to(static_controller::root_index)) .route( diff --git a/moneymgr_web/src/api/BackupApi.ts b/moneymgr_web/src/api/BackupApi.ts index ea0074e..1cd06a5 100644 --- a/moneymgr_web/src/api/BackupApi.ts +++ b/moneymgr_web/src/api/BackupApi.ts @@ -40,4 +40,11 @@ export class BackupApi { formData: fd, }); } + + /** + * Excel export + */ + static ExcelExportURL(): string { + return APIClient.backendURL() + "/backup/xslx/export"; + } } diff --git a/moneymgr_web/src/routes/BackupRoute.tsx b/moneymgr_web/src/routes/BackupRoute.tsx index 5c5aa77..5bb2adc 100644 --- a/moneymgr_web/src/routes/BackupRoute.tsx +++ b/moneymgr_web/src/routes/BackupRoute.tsx @@ -20,6 +20,7 @@ import { useLoadingMessage } from "../hooks/context_providers/LoadingMessageProv import { useSnackbar } from "../hooks/context_providers/SnackbarProvider"; import { MoneyMgrWebRouteContainer } from "../widgets/MoneyMgrWebRouteContainer"; import { RouterLink } from "../widgets/RouterLink"; +import excelIcon from "./excel.svg"; export function BackupRoute(): React.ReactElement { return ( @@ -63,6 +64,14 @@ export function BackupRoute(): React.ReactElement { exportURL={BackupApi.FinancesManagerExportURL} onImport={BackupApi.FinancesManagerImport} /> + + {/* Excel */} + } + label="Excel" + description={"Export data in Excel format"} + exportURL={BackupApi.ExcelExportURL()} + /> ); diff --git a/moneymgr_web/src/routes/excel.svg b/moneymgr_web/src/routes/excel.svg new file mode 100644 index 0000000..4ddc8e0 --- /dev/null +++ b/moneymgr_web/src/routes/excel.svg @@ -0,0 +1,48 @@ + + + + + + + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + +