diff --git a/.changeset/charges-fees-sheet.md b/.changeset/charges-fees-sheet.md new file mode 100644 index 0000000..61a803f --- /dev/null +++ b/.changeset/charges-fees-sheet.md @@ -0,0 +1,11 @@ +--- +"mpesa2csv": minor +--- + +Add optional Charges/Fees sheet to Excel exports + +- Add new export option to include a separate "Charges & Fees" sheet when exporting to Excel +- Filter and categorize all transactions containing "charge" in the details +- Display charges with date, amount, and balance information +- Include summary totals for total charges and number of charge transactions + diff --git a/README.md b/README.md index 580206c..7ff82da 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # mpesa2csv -A desktop application built with Tauri, React, and TypeScript that converts M-PESA statement PDFs to CSV format. +A desktop application built with Tauri, React, and TypeScript that converts M-PESA statement PDFs to CSV/XLSX files. ## Features -- Convert M-PESA statement PDF files to CSV format +- Convert M-PESA statement PDF files to CSV/XLSX Files - Handle password-protected PDF files - Process the data locally - your statements never leave your computer - Modern and user-friendly interface diff --git a/package.json b/package.json index f843a92..4cf8863 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "sync-versions": "node scripts/sync-versions.js" }, "dependencies": { + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.3", "@tailwindcss/vite": "^4.1.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 47f5302..487a73a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@radix-ui/react-checkbox': + specifier: ^1.3.3 + version: 1.3.3(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@radix-ui/react-select': specifier: ^2.2.6 version: 2.2.6(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -548,6 +551,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.7': resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} peerDependencies: @@ -658,6 +674,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-primitive@2.1.3': resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} peerDependencies: @@ -2532,6 +2561,22 @@ snapshots: '@types/react': 19.1.13 '@types/react-dom': 19.1.9(@types/react@19.1.13) + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.13)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.13)(react@19.1.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.13)(react@19.1.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.13)(react@19.1.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.13)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.13 + '@types/react-dom': 19.1.9(@types/react@19.1.13) + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.13)(react@19.1.1) @@ -2627,6 +2672,16 @@ snapshots: '@types/react': 19.1.13 '@types/react-dom': 19.1.9(@types/react@19.1.13) + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.13)(react@19.1.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.13)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.13 + '@types/react-dom': 19.1.9(@types/react@19.1.13) + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/react-slot': 1.2.3(@types/react@19.1.13)(react@19.1.1) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 52638dd..cd886d6 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2122,7 +2122,7 @@ dependencies = [ [[package]] name = "mpesa2csv" -version = "0.0.2" +version = "0.0.3" dependencies = [ "serde", "serde_json", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f44e892..89185b4 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "mpesa2csv" version = "0.0.3" -description = "Convert M-PESA Statements to CSV Files" +description = "Convert M-PESA Statements to CSV/Excel Files" authors = ["David Amunga"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ecb6537..395447f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -12,9 +12,9 @@ "app": { "windows": [ { - "title": "mpesa2csv - Convert M-PESA Statements to CSV", + "title": "mpesa2csv - Convert M-PESA Statements to CSV/Excel", "width": 600, - "height": 450, + "height": 500, "minWidth": 500, "minHeight": 400, "resizable": true @@ -36,8 +36,8 @@ ], "publisher": "David Amunga", "category": "Utility", - "shortDescription": "Convert M-PESA statements to CSV", - "longDescription": "A desktop application that converts M-PESA statement PDFs to CSV format. Process your statements locally with complete privacy.", + "shortDescription": "Convert M-PESA statements to CSV/Excel", + "longDescription": "A desktop application that converts M-PESA statement PDFs to CSV/Excel files. Process your statements locally with complete privacy.", "copyright": "Copyright © 2025 David Amunga. All rights reserved.", "licenseFile": "../LICENSE", "externalBin": [], @@ -64,7 +64,7 @@ }, "windowSize": { "width": 660, - "height": 400 + "height": 500 } } }, diff --git a/src/App.tsx b/src/App.tsx index 94522e2..c37f112 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,10 @@ import { useState, useEffect } from "react"; -import { MPesaStatement, FileStatus, ExportFormat } from "./types"; +import { + MPesaStatement, + FileStatus, + ExportFormat, + ExportOptions, +} from "./types"; import { PdfService } from "./services/pdfService"; import { ExportService } from "./services/exportService"; import FileUploader from "./components/file-uploader"; @@ -15,6 +20,7 @@ import { SelectValue, } from "./components/ui/select"; import { Button } from "./components/ui/button"; +import { Checkbox } from "./components/ui/checkbox"; function App() { const [files, setFiles] = useState([]); @@ -26,6 +32,9 @@ function App() { const [exportFormat, setExportFormat] = useState( ExportFormat.XLSX ); + const [exportOptions, setExportOptions] = useState({ + includeChargesSheet: false, + }); const [currentFileIndex, setCurrentFileIndex] = useState(0); const [isDownloading, setIsDownloading] = useState(false); @@ -119,7 +128,11 @@ function App() { ); // Generate download link asynchronously - ExportService.createDownloadLink(combinedStatement, exportFormat) + ExportService.createDownloadLink( + combinedStatement, + exportFormat, + exportOptions + ) .then(setExportLink) .catch(() => setExportLink("")); setExportFileName(fileName); @@ -183,7 +196,11 @@ function App() { formatDateForFilename() ); - ExportService.createDownloadLink(combinedStatement, exportFormat) + ExportService.createDownloadLink( + combinedStatement, + exportFormat, + exportOptions + ) .then(setExportLink) .catch(() => setExportLink("")); setExportFileName(fileName); @@ -221,7 +238,8 @@ function App() { const arrayBuffer = await ExportService.getFileBuffer( combinedStatement, - exportFormat + exportFormat, + exportOptions ); const content = new Uint8Array(arrayBuffer); @@ -324,7 +342,11 @@ function App() { formatDateForFilename() ); - ExportService.createDownloadLink(combinedStatement, value) + ExportService.createDownloadLink( + combinedStatement, + value, + exportOptions + ) .then(setExportLink) .catch(() => setExportLink("")); setExportFileName(fileName); @@ -345,6 +367,46 @@ function App() { + {exportFormat === ExportFormat.XLSX && ( +
+ +
+ +

+ Creates a separate sheet with all transaction charges + and fees +

+
+
+ )} +
-