Add basic Excel export
This commit is contained in:
parent
485fe6ae53
commit
b16a716c6c
10
moneymgr_backend/Cargo.lock
generated
10
moneymgr_backend/Cargo.lock
generated
@ -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"
|
||||
|
@ -35,3 +35,4 @@ httpdate = "1.0.3"
|
||||
chrono = "0.4.41"
|
||||
tempfile = "3.19.1"
|
||||
zip = "2.6.1"
|
||||
rust_xlsxwriter = "0.86.1"
|
@ -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))
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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(
|
||||
|
@ -40,4 +40,11 @@ export class BackupApi {
|
||||
formData: fd,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Excel export
|
||||
*/
|
||||
static ExcelExportURL(): string {
|
||||
return APIClient.backendURL() + "/backup/xslx/export";
|
||||
}
|
||||
}
|
||||
|
@ -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 */}
|
||||
<ImportExportModal
|
||||
icon={<img src={excelIcon} width={"25em"} />}
|
||||
label="Excel"
|
||||
description={"Export data in Excel format"}
|
||||
exportURL={BackupApi.ExcelExportURL()}
|
||||
/>
|
||||
</Grid>
|
||||
</MoneyMgrWebRouteContainer>
|
||||
);
|
||||
|
48
moneymgr_web/src/routes/excel.svg
Normal file
48
moneymgr_web/src/routes/excel.svg
Normal file
@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||
<!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/">
|
||||
<!ENTITY ns_ai "http://ns.adobe.com/AdobeIllustrator/10.0/">
|
||||
<!ENTITY ns_graphs "http://ns.adobe.com/Graphs/1.0/">
|
||||
<!ENTITY ns_vars "http://ns.adobe.com/Variables/1.0/">
|
||||
<!ENTITY ns_imrep "http://ns.adobe.com/ImageReplacement/1.0/">
|
||||
<!ENTITY ns_sfw "http://ns.adobe.com/SaveForWeb/1.0/">
|
||||
<!ENTITY ns_custom "http://ns.adobe.com/GenericCustomNamespace/1.0/">
|
||||
<!ENTITY ns_adobe_xpath "http://ns.adobe.com/XPath/1.0/">
|
||||
]>
|
||||
<svg version="1.1" id="Livello_1" xmlns:x="&ns_extend;" xmlns:i="&ns_ai;" xmlns:graph="&ns_graphs;"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 2289.75 2130"
|
||||
enable-background="new 0 0 2289.75 2130" xml:space="preserve">
|
||||
<metadata>
|
||||
<sfw xmlns="&ns_sfw;">
|
||||
<slices></slices>
|
||||
<sliceSourceBounds bottomLeftOrigin="true" height="2130" width="2289.75" x="-1147.5" y="-1041"></sliceSourceBounds>
|
||||
</sfw>
|
||||
</metadata>
|
||||
<path fill="#185C37" d="M1437.75,1011.75L532.5,852v1180.393c0,53.907,43.7,97.607,97.607,97.607l0,0h1562.036
|
||||
c53.907,0,97.607-43.7,97.607-97.607l0,0V1597.5L1437.75,1011.75z"/>
|
||||
<path fill="#21A366" d="M1437.75,0H630.107C576.2,0,532.5,43.7,532.5,97.607c0,0,0,0,0,0V532.5l905.25,532.5L1917,1224.75
|
||||
L2289.75,1065V532.5L1437.75,0z"/>
|
||||
<path fill="#107C41" d="M532.5,532.5h905.25V1065H532.5V532.5z"/>
|
||||
<path opacity="0.1" enable-background="new " d="M1180.393,426H532.5v1331.25h647.893c53.834-0.175,97.432-43.773,97.607-97.607
|
||||
V523.607C1277.825,469.773,1234.227,426.175,1180.393,426z"/>
|
||||
<path opacity="0.2" enable-background="new " d="M1127.143,479.25H532.5V1810.5h594.643
|
||||
c53.834-0.175,97.432-43.773,97.607-97.607V576.857C1224.575,523.023,1180.977,479.425,1127.143,479.25z"/>
|
||||
<path opacity="0.2" enable-background="new " d="M1127.143,479.25H532.5V1704h594.643c53.834-0.175,97.432-43.773,97.607-97.607
|
||||
V576.857C1224.575,523.023,1180.977,479.425,1127.143,479.25z"/>
|
||||
<path opacity="0.2" enable-background="new " d="M1073.893,479.25H532.5V1704h541.393c53.834-0.175,97.432-43.773,97.607-97.607
|
||||
V576.857C1171.325,523.023,1127.727,479.425,1073.893,479.25z"/>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="203.5132" y1="1729.0183" x2="967.9868" y2="404.9817" gradientTransform="matrix(1 0 0 -1 0 2132)">
|
||||
<stop offset="0" style="stop-color:#18884F"/>
|
||||
<stop offset="0.5" style="stop-color:#117E43"/>
|
||||
<stop offset="1" style="stop-color:#0B6631"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#SVGID_1_)" d="M97.607,479.25h976.285c53.907,0,97.607,43.7,97.607,97.607v976.285
|
||||
c0,53.907-43.7,97.607-97.607,97.607H97.607C43.7,1650.75,0,1607.05,0,1553.143V576.857C0,522.95,43.7,479.25,97.607,479.25z"/>
|
||||
<path fill="#FFFFFF" d="M302.3,1382.264l205.332-318.169L319.5,747.683h151.336l102.666,202.35
|
||||
c9.479,19.223,15.975,33.494,19.49,42.919h1.331c6.745-15.336,13.845-30.228,21.3-44.677L725.371,747.79h138.929l-192.925,314.548
|
||||
L869.2,1382.263H721.378L602.79,1160.158c-5.586-9.45-10.326-19.376-14.164-29.66h-1.757c-3.474,10.075-8.083,19.722-13.739,28.755
|
||||
l-122.102,223.011H302.3z"/>
|
||||
<path fill="#33C481" d="M2192.143,0H1437.75v532.5h852V97.607C2289.75,43.7,2246.05,0,2192.143,0L2192.143,0z"/>
|
||||
<path fill="#107C41" d="M1437.75,1065h852v532.5h-852V1065z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.4 KiB |
Loading…
x
Reference in New Issue
Block a user