Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bright-turtles-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"mpesa2csv": minor
---

feat: add daily balance tracker sheet
5 changes: 5 additions & 0 deletions .changeset/full-carrots-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"mpesa2csv": minor
---

feat: add monthly & weekly breakdown sheet
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"pdfjs-dist": "5.1.91",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwind-merge": "^3.3.1"
"tailwind-merge": "^3.3.1",
"zustand": "^5.0.8"
},
"devDependencies": {
"@changesets/cli": "^2.29.7",
Expand Down
26 changes: 26 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
{
"title": "mpesa2csv - Convert M-PESA Statements to CSV/Excel",
"width": 600,
"height": 550,
"height": 630,
"minWidth": 500,
"minHeight": 400,
"resizable": true
Expand Down Expand Up @@ -64,7 +64,7 @@
},
"windowSize": {
"width": 660,
"height": 550
"height": 630
}
}
},
Expand Down
73 changes: 70 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ function App() {
const [exportOptions, setExportOptions] = useState<ExportOptions>({
includeChargesSheet: false,
includeSummarySheet: false,
includeBreakdownSheet: false,
includeDailyBalanceSheet: false,
});
const [currentFileIndex, setCurrentFileIndex] = useState<number>(0);
const [isDownloading, setIsDownloading] = useState<boolean>(false);
Expand Down Expand Up @@ -270,6 +272,7 @@ function App() {

setError(undefined);
} catch (error: any) {
console.log(error);
if (error.includes("cancelled")) {
setError(undefined);
} else {
Expand Down Expand Up @@ -455,6 +458,72 @@ function App() {
flow, spending patterns, and insights
</p>
</div>

<div>
<label className="flex items-center space-x-2">
<Checkbox
checked={exportOptions.includeBreakdownSheet}
onCheckedChange={(value) => {
const newOptions = {
...exportOptions,
includeBreakdownSheet: Boolean(value),
};
setExportOptions(newOptions);

const combinedStatement = statements[0];
ExportService.createDownloadLink(
combinedStatement,
exportFormat,
newOptions
)
.then(setExportLink)
.catch(() => setExportLink(""));
}}
className="rounded border-gray-300 text-primary focus:ring-primary"
/>
<span className="text-sm">
Include Monthly & Weekly Breakdown Sheet
</span>
</label>
<p className="text-xs text-muted-foreground ml-6">
Creates a pivot-like table with monthly and weekly
aggregations showing inflows, outflows, net change,
and average transaction size
</p>
</div>

<div>
<label className="flex items-center space-x-2">
<Checkbox
checked={exportOptions.includeDailyBalanceSheet}
onCheckedChange={(value) => {
const newOptions = {
...exportOptions,
includeDailyBalanceSheet: Boolean(value),
};
setExportOptions(newOptions);

const combinedStatement = statements[0];
ExportService.createDownloadLink(
combinedStatement,
exportFormat,
newOptions
)
.then(setExportLink)
.catch(() => setExportLink(""));
}}
className="rounded border-gray-300 text-primary focus:ring-primary"
/>
<span className="text-sm">
Include Daily Balance Tracker Sheet
</span>
</label>
<p className="text-xs text-muted-foreground ml-6">
Creates a day-by-day balance tracker showing your
highest and lowest balances with spending pattern
insights
</p>
</div>
</div>
</div>
)}
Expand Down Expand Up @@ -515,9 +584,7 @@ function App() {
</a>
</p>
<div className="flex items-center gap-3">
{appVersion && (
<span className="">v{appVersion}</span>
)}
{appVersion && <span className="">v{appVersion}</span>}
<UpdateChecker showButton={true} />
</div>
</div>
Expand Down
89 changes: 89 additions & 0 deletions src/services/exports/chargesSheet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { MPesaStatement } from "../../types";
import * as ExcelJS from "exceljs";

export function addChargesSheet(
workbook: ExcelJS.Workbook,
statement: MPesaStatement
): void {
// Filter transactions that contain "charge" in the details (case insensitive)
const chargeTransactions = statement.transactions.filter((transaction) =>
transaction.details.toLowerCase().includes("charge")
);

if (chargeTransactions.length === 0) {
return; // No charges found, don't create the sheet
}

// Create the charges worksheet
const chargesWorksheet = workbook.addWorksheet("Charges & Fees");

// Define columns for charges sheet
chargesWorksheet.columns = [
{ header: "Receipt No", key: "receiptNo", width: 12 },
{ header: "Date", key: "date", width: 12 },
{ header: "Full Details", key: "fullDetails", width: 40 },
{ header: "Amount", key: "amount", width: 12 },
];

// Style the header row
const headerRow = chargesWorksheet.getRow(1);
headerRow.font = { bold: true };
headerRow.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFFFD700" },
};
headerRow.alignment = { horizontal: "center" };

// Process and add charge transactions
chargeTransactions.forEach((transaction) => {
const amount = transaction.withdrawn || transaction.paidIn || 0;

chargesWorksheet.addRow({
receiptNo: transaction.receiptNo,
date: transaction.completionTime,
fullDetails: transaction.details,
amount: amount,
});
});

// Add borders to all cells
const dataRange = chargesWorksheet.getRows(1, chargesWorksheet.rowCount);
if (dataRange) {
dataRange.forEach((row) => {
if (row) {
row.eachCell((cell) => {
cell.border = {
top: { style: "thin" },
left: { style: "thin" },
bottom: { style: "thin" },
right: { style: "thin" },
};
});
}
});
}

// Add summary at the bottom
const summaryStartRow = chargesWorksheet.rowCount + 2;
const totalCharges = chargeTransactions.reduce((sum, transaction) => {
return sum + (transaction.withdrawn || transaction.paidIn || 0);
}, 0);

chargesWorksheet.getCell(`A${summaryStartRow}`).value = "Total Charges:";
chargesWorksheet.getCell(`A${summaryStartRow}`).font = { bold: true };
chargesWorksheet.getCell(`D${summaryStartRow}`).value = totalCharges;
chargesWorksheet.getCell(`D${summaryStartRow}`).font = { bold: true };
chargesWorksheet.getCell(`D${summaryStartRow}`).fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFFFE4B5" },
};

chargesWorksheet.getCell(`A${summaryStartRow + 1}`).value =
"Number of Charge Transactions:";
chargesWorksheet.getCell(`A${summaryStartRow + 1}`).font = { bold: true };
chargesWorksheet.getCell(`D${summaryStartRow + 1}`).value =
chargeTransactions.length;
chargesWorksheet.getCell(`D${summaryStartRow + 1}`).font = { bold: true };
}
Loading