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