diff --git a/src/App.tsx b/src/App.tsx index ef84373..b044d2d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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, @@ -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([]); + const [fileHashes, setFileHashes] = useState>(new Map()); const [status, setStatus] = useState(FileStatus.IDLE); const [error, setError] = useState(undefined); const [statements, setStatements] = useState([]); @@ -40,6 +45,11 @@ function App() { const [downloadSuccess, setDownloadSuccess] = useState(false); const [savedFilePath, setSavedFilePath] = useState(""); const [currentPlatform, setCurrentPlatform] = useState(""); + const [cachedPassword, setCachedPassword] = useState(null); + const [showHistory, setShowHistory] = useState(false); + + const { getPassword, savePassword } = usePasswordCacheStore(); + const { addFile: addRecentFile } = useRecentFilesStore(); const getDefaultFileName = () => { return ( @@ -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(); + 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); @@ -121,7 +155,8 @@ function App() { const processFiles = async ( filesToProcess: File[], startIndex: number = 0, - existingStatements: MPesaStatement[] = [] + existingStatements: MPesaStatement[] = [], + hashMap?: Map ) => { const processedStatements: MPesaStatement[] = [...existingStatements]; @@ -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) => @@ -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) => @@ -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 { @@ -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; @@ -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); @@ -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) { @@ -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([]); @@ -304,8 +381,115 @@ function App() { } }; + const saveToRecentFiles = async ( + processedFiles: File[], + statement: MPesaStatement, + hashMap: Map + ) => { + 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([]); @@ -313,6 +497,8 @@ function App() { setIsDownloading(false); setDownloadSuccess(false); setSavedFilePath(""); + setCachedPassword(null); + setShowHistory(false); if (exportLink) { URL.revokeObjectURL(exportLink); @@ -426,6 +612,26 @@ function App() { return (
+ + {/* History Modal Overlay */} + {showHistory && ( +
{ + if (e.target === e.currentTarget) { + setShowHistory(false); + } + }} + > +
+ setShowHistory(false)} + onReprocessFile={handleReprocessFile} + /> +
+
+ )} +
@@ -454,6 +660,7 @@ function App() { currentFileName={files[currentFileIndex]?.name} currentFileIndex={currentFileIndex} totalFiles={files.length} + defaultPassword={cachedPassword} />
) : status === FileStatus.PROCESSING ? ( @@ -553,17 +760,28 @@ function App() {