Skip to content
Open
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
244 changes: 231 additions & 13 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState, useEffect } from "react";
import { invoke } from "@tauri-apps/api/core";
import { platform } from "@tauri-apps/plugin-os";
import { Download, RotateCcw, ExternalLink } from "lucide-react";
import { Download, RotateCcw, ExternalLink, History } from "lucide-react";

import {
MPesaStatement,
Expand All @@ -17,9 +17,14 @@ import ExportOptions from "./components/export-options";
import { UpdateChecker } from "./components/update-checker";
import { Button } from "./components/ui/button";
import { formatDateForFilename } from "./utils/helpers";
import { calculateQuickFileHash } from "./utils/fileHash";
import { usePasswordCacheStore } from "./stores/passwordCacheStore";
import { useRecentFilesStore } from "./stores/recentFilesStore";
import RecentFilesHistory from "./components/recent-files-history";

function App() {
const [files, setFiles] = useState<File[]>([]);
const [fileHashes, setFileHashes] = useState<Map<number, string>>(new Map());
const [status, setStatus] = useState<FileStatus>(FileStatus.IDLE);
const [error, setError] = useState<string | undefined>(undefined);
const [statements, setStatements] = useState<MPesaStatement[]>([]);
Expand All @@ -40,6 +45,11 @@ function App() {
const [downloadSuccess, setDownloadSuccess] = useState<boolean>(false);
const [savedFilePath, setSavedFilePath] = useState<string>("");
const [currentPlatform, setCurrentPlatform] = useState<string>("");
const [cachedPassword, setCachedPassword] = useState<string | null>(null);
const [showHistory, setShowHistory] = useState<boolean>(false);

const { getPassword, savePassword } = usePasswordCacheStore();
const { addFile: addRecentFile } = useRecentFilesStore();

const getDefaultFileName = () => {
return (
Expand Down Expand Up @@ -98,14 +108,38 @@ function App() {
}, []);

const handleFilesSelected = async (selectedFiles: File[]) => {
console.log('[App] ===== FILE UPLOAD STARTED =====');
console.log('[App] Selected files:', selectedFiles.map(f => f.name));

setFiles(selectedFiles);
setStatus(FileStatus.LOADING);
setError(undefined);
setStatements([]);
setCurrentFileIndex(0);

// Calculate hashes for all files
console.log('[App] Starting file hash calculation for', selectedFiles.length, 'files');
const hashMap = new Map<number, string>();
try {
await Promise.all(
selectedFiles.map(async (file, index) => {
try {
const hash = await calculateQuickFileHash(file);
hashMap.set(index, hash);
console.log('[App] Hash calculated for file', index, ':', file.name, '->', hash.substring(0, 16) + '...');
} catch (err) {
console.error(`[App] Failed to hash file ${file.name}:`, err);
}
})
);
setFileHashes(hashMap);
console.log('[App] All hashes calculated. Total:', hashMap.size);
} catch (err) {
console.error('[App] Error calculating file hashes:', err);
}

try {
const result = await processFiles(selectedFiles);
const result = await processFiles(selectedFiles, 0, [], hashMap);
if (result?.error) {
setStatus(FileStatus.ERROR);
setError(result.error);
Expand All @@ -121,7 +155,8 @@ function App() {
const processFiles = async (
filesToProcess: File[],
startIndex: number = 0,
existingStatements: MPesaStatement[] = []
existingStatements: MPesaStatement[] = [],
hashMap?: Map<number, string>
) => {
const processedStatements: MPesaStatement[] = [...existingStatements];

Expand All @@ -130,6 +165,12 @@ function App() {
const file = filesToProcess[i];

try {
// Get cached password if available
const fileHash = (hashMap || fileHashes).get(i);
console.log('[App] Processing file', i, '- Hash:', fileHash ? fileHash.substring(0, 16) + '...' : 'NO HASH');
const foundCachedPassword = fileHash ? getPassword(fileHash) : null;
console.log('[App] Cached password for file', i, ':', foundCachedPassword ? '***FOUND***' : 'NOT FOUND');

const result = (await Promise.race([
PdfService.loadPdf(file),
new Promise((_, reject) =>
Expand All @@ -141,10 +182,17 @@ function App() {
])) as { isProtected: boolean; pdf?: any };

if (result.isProtected) {
// Set cached password so the PasswordPrompt can auto-fill
if (foundCachedPassword) {
console.log('[App] ✅ Setting cached password for auto-fill');
setCachedPassword(foundCachedPassword);
}
setStatus(FileStatus.PROTECTED);
return { needsPassword: true, fileIndex: i, processedStatements };
} else if (result.pdf) {
setStatus(FileStatus.PROCESSING);
console.log('[App] ℹ️ No password needed for:', file.name);

const statement = (await Promise.race([
PdfService.parseMpesaStatement(result.pdf),
new Promise((_, reject) =>
Expand Down Expand Up @@ -191,6 +239,10 @@ function App() {
.catch(() => setExportLink(""));
setExportFileName(fileName);
setStatus(FileStatus.SUCCESS);

// Save to recent files history
console.log('[App] Normal flow complete - saving to history');
saveToRecentFiles(filesToProcess, combinedStatement, hashMap || fileHashes);
}

return {
Expand Down Expand Up @@ -224,11 +276,27 @@ function App() {

setStatus(FileStatus.PROCESSING);
setError(undefined);
setCachedPassword(null); // Clear cached password after use

try {
const currentFile = files[currentFileIndex];
const pdf = await PdfService.unlockPdf(currentFile, password);

// Save password to cache if file was successfully unlocked
const fileHash = fileHashes.get(currentFileIndex);
console.log('[App] Attempting to save password for file', currentFileIndex);
console.log('[App] File hash:', fileHash ? fileHash.substring(0, 16) + '...' : 'NO HASH FOUND');
console.log('[App] Current fileHashes map size:', fileHashes.size);
console.log('[App] All hashes in map:', Array.from(fileHashes.entries()).map(([k, v]) => `${k}: ${v.substring(0, 16)}...`));

if (fileHash) {
console.log('[App] ✅ Saving password to cache for:', currentFile.name);
savePassword(fileHash, password, currentFile.name);
console.log('[App] ✅ Password saved successfully');
} else {
console.error('[App] ❌ Cannot save password - no hash found for file index:', currentFileIndex);
}

const statement = await PdfService.parseMpesaStatement(pdf);
statement.fileName = currentFile.name;

Expand Down Expand Up @@ -259,6 +327,10 @@ function App() {
.catch(() => setExportLink(""));
setExportFileName(fileName);
setStatus(FileStatus.SUCCESS);

// Save to recent files history after password submission
console.log('[App] Password flow complete - saving to history');
saveToRecentFiles(files, combinedStatement, fileHashes);
}
} catch (err: any) {
setStatus(FileStatus.PROTECTED);
Expand All @@ -270,6 +342,7 @@ function App() {
if (files.length === 0) return;

setError(undefined);
setCachedPassword(null); // Clear cached password when skipping
const nextIndex = currentFileIndex + 1;

if (nextIndex < files.length) {
Expand All @@ -296,6 +369,10 @@ function App() {
.catch(() => setExportLink(""));
setExportFileName(fileName);
setStatus(FileStatus.SUCCESS);

// Save to recent files history after skip
console.log('[App] Skip flow complete - saving to history');
saveToRecentFiles(files, combinedStatement, fileHashes);
} else {
setStatus(FileStatus.IDLE);
setFiles([]);
Expand All @@ -304,15 +381,124 @@ function App() {
}
};

const saveToRecentFiles = async (
processedFiles: File[],
statement: MPesaStatement,
hashMap: Map<number, string>
) => {
console.log('[RecentFiles] saveToRecentFiles called');
console.log('[RecentFiles] Processing', processedFiles.length, 'files');
console.log('[RecentFiles] Statement has', statement.transactions.length, 'transactions');
console.log('[RecentFiles] HashMap size:', hashMap.size);

for (const [index, file] of processedFiles.entries()) {
const fileHash = hashMap.get(index);
console.log(`[RecentFiles] File ${index}:`, file.name, 'Hash:', fileHash ? fileHash.substring(0, 16) + '...' : 'NO HASH');

if (!fileHash) {
console.error('[RecentFiles] ❌ Skipping file - no hash found for index:', index);
continue;
}

// Calculate date range from transactions
const transactions = statement.transactions;
let dateRange = undefined;
if (transactions.length > 0) {
const dates = transactions.map(t => new Date(t.completionTime));
const minDate = new Date(Math.min(...dates.map(d => d.getTime())));
const maxDate = new Date(Math.max(...dates.map(d => d.getTime())));
dateRange = {
from: minDate.toLocaleDateString(),
to: maxDate.toLocaleDateString(),
};
console.log('[RecentFiles] Date range:', dateRange);
}

// Calculate totals
const totalPaidIn = transactions.reduce((sum, t) => sum + (t.paidIn || 0), 0);
const totalWithdrawn = transactions.reduce((sum, t) => sum + (t.withdrawn || 0), 0);
const finalBalance = transactions.length > 0
? transactions[transactions.length - 1].balance
: 0;

console.log('[RecentFiles] Totals - Paid In:', totalPaidIn, 'Withdrawn:', totalWithdrawn, 'Balance:', finalBalance);

// Convert file to base64 for storage
let fileData: string | undefined;
try {
const arrayBuffer = await file.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
const binaryString = Array.from(uint8Array, byte => String.fromCharCode(byte)).join('');
fileData = btoa(binaryString);
console.log('[RecentFiles] File data encoded, size:', fileData.length, 'chars');
} catch (error) {
console.error('[RecentFiles] Failed to encode file:', error);
}

const recentFileEntry = {
id: `${fileHash}-${Date.now()}`,
fileName: file.name,
fileSize: file.size,
fileHash,
processedDate: Date.now(),
transactionCount: transactions.length,
isPasswordProtected: !!getPassword(fileHash),
dateRange,
totalPaidIn,
totalWithdrawn,
finalBalance,
fileData, // Store base64 encoded file
};

console.log('[RecentFiles] Adding entry:', { ...recentFileEntry, fileData: fileData ? 'STORED' : 'NOT_STORED' });
addRecentFile(recentFileEntry);
console.log('[RecentFiles] ✅ Entry added successfully');
}

console.log('[RecentFiles] saveToRecentFiles completed');
};

const handleReprocessFile = async (fileEntry: any) => {
console.log('[App] Reprocessing file from history:', fileEntry.fileName);

if (!fileEntry.fileData) {
setError('File data not available for reprocessing');
return;
}

try {
// Decode base64 back to File object
const binaryString = atob(fileEntry.fileData);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
const blob = new Blob([bytes], { type: 'application/pdf' });
const file = new File([blob], fileEntry.fileName, { type: 'application/pdf' });

console.log('[App] File reconstructed:', file.name, file.size);

// Close history and process the file
setShowHistory(false);
handleFilesSelected([file]);
} catch (error) {
console.error('[App] Failed to reprocess file:', error);
setError('Failed to reprocess file from history');
}
};

const handleReset = () => {
setFiles([]);
setFileHashes(new Map());
setStatus(FileStatus.IDLE);
setError(undefined);
setStatements([]);
setCurrentFileIndex(0);
setIsDownloading(false);
setDownloadSuccess(false);
setSavedFilePath("");
setCachedPassword(null);
setShowHistory(false);

if (exportLink) {
URL.revokeObjectURL(exportLink);
Expand Down Expand Up @@ -426,6 +612,26 @@ function App() {
return (
<div className="min-h-screen max-h-screen flex flex-col overflow-hidden">
<UpdateChecker autoCheck={true} />

{/* History Modal Overlay */}
{showHistory && (
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowHistory(false);
}
}}
>
<div className="rounded-lg shadow-2xl max-w-4xl w-full max-h-[80vh] overflow-hidden border border-zinc-800 dark:border-zinc-800">
<RecentFilesHistory
onClose={() => setShowHistory(false)}
onReprocessFile={handleReprocessFile}
/>
</div>
</div>
)}

<div className="flex-1 mx-auto px-4 py-4 flex flex-col max-w-4xl w-full">
<main className="flex-1 flex items-center justify-center">
<div className="w-full max-w-2xl transition-all duration-300 ease-in-out">
Expand Down Expand Up @@ -454,6 +660,7 @@ function App() {
currentFileName={files[currentFileIndex]?.name}
currentFileIndex={currentFileIndex}
totalFiles={files.length}
defaultPassword={cachedPassword}
/>
</div>
) : status === FileStatus.PROCESSING ? (
Expand Down Expand Up @@ -553,17 +760,28 @@ function App() {

<footer className="flex-shrink-0 text-center text-xs border-t py-3 mt-0">
<div className="flex flex-col sm:flex-row items-center justify-between gap-2">
<p>
Built by{" "}
<a
href="https://twitter.com/davidamunga_"
className="text-green-500 hover:text-green-500/80 font-medium transition-colors"
target="_blank"
rel="noopener noreferrer"
<div className="flex items-center gap-3">
<p>
Built by{" "}
<a
href="https://twitter.com/davidamunga_"
className="text-green-500 hover:text-green-500/80 font-medium transition-colors"
target="_blank"
rel="noopener noreferrer"
>
@davidamunga
</a>
</p>
<Button
variant="ghost"
size="sm"
onClick={() => setShowHistory(true)}
className="flex items-center gap-1 text-xs"
>
@davidamunga
</a>
</p>
<History className="w-3 h-3" />
History
</Button>
</div>
<div className="flex items-center gap-3">
{appVersion && <span className="">v{appVersion}</span>}
<UpdateChecker showButton={true} />
Expand Down
Loading