ashhal commited on
Commit
53daf8a
Β·
verified Β·
1 Parent(s): 25f6d9b

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +2177 -0
app.py ADDED
@@ -0,0 +1,2177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import sqlite3
4
+ import time
5
+ import hashlib
6
+ import base64
7
+ import json
8
+ from datetime import datetime, timedelta
9
+ from io import BytesIO
10
+ import pandas as pd
11
+ import plotly.express as px
12
+ import plotly.graph_objects as go
13
+ import gradio as gr
14
+ from dateutil.relativedelta import relativedelta
15
+
16
+ # Image processing imports
17
+ try:
18
+ from PIL import Image, ImageEnhance, ImageFilter
19
+ import cv2
20
+ import numpy as np
21
+ PIL_AVAILABLE = True
22
+ except ImportError:
23
+ PIL_AVAILABLE = False
24
+ print("⚠️ PIL/OpenCV not installed. Run: pip install Pillow opencv-python")
25
+
26
+ # OCR imports
27
+ try:
28
+ import pytesseract
29
+ TESSERACT_AVAILABLE = True
30
+ except ImportError:
31
+ TESSERACT_AVAILABLE = False
32
+ print("⚠️ Pytesseract not installed. Run: pip install pytesseract")
33
+
34
+ # Google Vision API (optional)
35
+ try:
36
+ from google.cloud import vision
37
+ VISION_API_AVAILABLE = True
38
+ except ImportError:
39
+ VISION_API_AVAILABLE = False
40
+ print("⚠️ Google Vision API not available. Install with: pip install google-cloud-vision")
41
+
42
+ # Twilio Integration
43
+ try:
44
+ from twilio.rest import Client
45
+ TWILIO_AVAILABLE = True
46
+ except ImportError:
47
+ TWILIO_AVAILABLE = False
48
+ print("⚠️ Twilio not installed. Run: pip install twilio")
49
+
50
+ # Constants
51
+ EXPENSE_CATEGORIES = [
52
+ "Housing (Rent/Mortgage)",
53
+ "Utilities (Electricity/Water)",
54
+ "Groceries",
55
+ "Dining Out",
56
+ "Transportation",
57
+ "Healthcare",
58
+ "Entertainment",
59
+ "Education",
60
+ "Personal Care",
61
+ "Debt Payments",
62
+ "Savings",
63
+ "Investments",
64
+ "Charity",
65
+ "Miscellaneous"
66
+ ]
67
+
68
+ INVESTMENT_TYPES = [
69
+ "Stocks",
70
+ "Bonds",
71
+ "Mutual Funds",
72
+ "Real Estate",
73
+ "Cryptocurrency",
74
+ "Retirement Accounts",
75
+ "Other"
76
+ ]
77
+
78
+ RECURRENCE_PATTERNS = [
79
+ "Daily",
80
+ "Weekly",
81
+ "Monthly",
82
+ "Quarterly",
83
+ "Yearly"
84
+ ]
85
+
86
+ # Rate limiting setup
87
+ MAX_ATTEMPTS = 5
88
+ ATTEMPT_WINDOW = 300 # 5 minutes in seconds
89
+
90
+ # Receipt processing constants
91
+ RECEIPTS_DIR = "receipts"
92
+ if not os.path.exists(RECEIPTS_DIR):
93
+ os.makedirs(RECEIPTS_DIR)
94
+
95
+ # Security functions
96
+ def hash_password(password):
97
+ """Hash password using SHA-256 with salt"""
98
+ salt = "fingenius_secure_salt_2024"
99
+ return hashlib.sha256((password + salt).encode()).hexdigest()
100
+
101
+ def verify_password(password, hashed):
102
+ """Verify password against hash"""
103
+ return hash_password(password) == hashed
104
+
105
+ # ========== A) IMAGE PROCESSING FUNCTIONS ==========
106
+ class ImageProcessor:
107
+ """Handles image preprocessing for better OCR results"""
108
+
109
+ @staticmethod
110
+ def preprocess_receipt_image(image_path):
111
+ """
112
+ Preprocess receipt image for optimal OCR
113
+ Returns: processed image path and preprocessing info
114
+ """
115
+ try:
116
+ if not PIL_AVAILABLE:
117
+ return image_path, "No preprocessing - PIL not available"
118
+
119
+ # Load image
120
+ image = Image.open(image_path)
121
+
122
+ # Convert to RGB if needed
123
+ if image.mode != 'RGB':
124
+ image = image.convert('RGB')
125
+
126
+ # Enhance contrast
127
+ enhancer = ImageEnhance.Contrast(image)
128
+ image = enhancer.enhance(1.5)
129
+
130
+ # Enhance sharpness
131
+ enhancer = ImageEnhance.Sharpness(image)
132
+ image = enhancer.enhance(2.0)
133
+
134
+ # Convert to grayscale
135
+ image = image.convert('L')
136
+
137
+ # Apply Gaussian blur to reduce noise
138
+ image = image.filter(ImageFilter.GaussianBlur(radius=0.5))
139
+
140
+ # Convert to numpy array for OpenCV processing
141
+ if 'cv2' in globals() and cv2 is not None:
142
+ img_array = np.array(image)
143
+
144
+ # Apply threshold to get binary image
145
+ _, binary = cv2.threshold(img_array, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
146
+
147
+ # Morphological operations to clean up the image
148
+ kernel = np.ones((1,1), np.uint8)
149
+ binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
150
+
151
+ # Convert back to PIL Image
152
+ image = Image.fromarray(binary)
153
+
154
+ # Save processed image
155
+ processed_path = image_path.replace('.', '_processed.')
156
+ image.save(processed_path)
157
+
158
+ return processed_path, "Enhanced contrast, sharpness, applied thresholding"
159
+
160
+ except Exception as e:
161
+ print(f"Image preprocessing error: {e}")
162
+ return image_path, f"Preprocessing failed: {str(e)}"
163
+
164
+ @staticmethod
165
+ def extract_text_regions(image_path):
166
+ """Extract text regions from receipt image"""
167
+ try:
168
+ if not PIL_AVAILABLE:
169
+ return []
170
+
171
+ image = Image.open(image_path)
172
+ # This is a simplified version - in production, you'd use more advanced techniques
173
+ # to detect and extract specific regions (header, items, total, etc.)
174
+ return ["Full image processed"]
175
+
176
+ except Exception as e:
177
+ print(f"Text region extraction error: {e}")
178
+ return []
179
+
180
+ # ========== B) OCR SERVICE CLASS ==========
181
+ class OCRService:
182
+ """Handles OCR processing with multiple backends"""
183
+
184
+ def __init__(self):
185
+ self.tesseract_available = TESSERACT_AVAILABLE
186
+ self.vision_api_available = VISION_API_AVAILABLE and os.getenv('GOOGLE_APPLICATION_CREDENTIALS')
187
+
188
+ # Initialize Google Vision client if available
189
+ if self.vision_api_available:
190
+ try:
191
+ self.vision_client = vision.ImageAnnotatorClient()
192
+ except Exception as e:
193
+ print(f"Google Vision API initialization failed: {e}")
194
+ self.vision_api_available = False
195
+
196
+ def extract_text_from_receipt(self, image_path):
197
+ """
198
+ Extract text from receipt using available OCR service
199
+ Returns: (raw_text, confidence_score, extracted_data)
200
+ """
201
+ try:
202
+ # Try Google Vision API first if available
203
+ if self.vision_api_available:
204
+ return self._extract_with_vision_api(image_path)
205
+
206
+ # Fallback to Tesseract
207
+ elif self.tesseract_available:
208
+ return self._extract_with_tesseract(image_path)
209
+
210
+ else:
211
+ return "OCR not available", 0.0, self._create_empty_data()
212
+
213
+ except Exception as e:
214
+ print(f"OCR extraction error: {e}")
215
+ return f"OCR failed: {str(e)}", 0.0, self._create_empty_data()
216
+
217
+ def _extract_with_vision_api(self, image_path):
218
+ """Extract text using Google Vision API"""
219
+ try:
220
+ with open(image_path, 'rb') as image_file:
221
+ content = image_file.read()
222
+
223
+ image = vision.Image(content=content)
224
+ response = self.vision_client.text_detection(image=image)
225
+ texts = response.text_annotations
226
+
227
+ if texts:
228
+ raw_text = texts[0].description
229
+ confidence = min([vertex.confidence for vertex in texts if hasattr(vertex, 'confidence')] or [0.8])
230
+ extracted_data = self._parse_receipt_text(raw_text)
231
+ return raw_text, confidence, extracted_data
232
+ else:
233
+ return "No text detected", 0.0, self._create_empty_data()
234
+
235
+ except Exception as e:
236
+ print(f"Vision API error: {e}")
237
+ return f"Vision API failed: {str(e)}", 0.0, self._create_empty_data()
238
+
239
+ def _extract_with_tesseract(self, image_path):
240
+ """Extract text using Tesseract OCR"""
241
+ try:
242
+ # Preprocess image first
243
+ processed_path, _ = ImageProcessor.preprocess_receipt_image(image_path)
244
+
245
+ # Extract text with Tesseract
246
+ raw_text = pytesseract.image_to_string(
247
+ Image.open(processed_path),
248
+ config='--oem 3 --psm 6' # OCR Engine Mode 3, Page Segmentation Mode 6
249
+ )
250
+
251
+ # Get confidence data
252
+ data = pytesseract.image_to_data(Image.open(processed_path), output_type=pytesseract.Output.DICT)
253
+ confidences = [int(conf) for conf in data['conf'] if int(conf) > 0]
254
+ avg_confidence = sum(confidences) / len(confidences) if confidences else 0.0
255
+
256
+ extracted_data = self._parse_receipt_text(raw_text)
257
+ return raw_text, avg_confidence / 100.0, extracted_data
258
+
259
+ except Exception as e:
260
+ print(f"Tesseract error: {e}")
261
+ return f"Tesseract failed: {str(e)}", 0.0, self._create_empty_data()
262
+
263
+ def _parse_receipt_text(self, raw_text):
264
+ """Parse raw OCR text to extract structured data"""
265
+ extracted_data = self._create_empty_data()
266
+
267
+ lines = raw_text.split('\n')
268
+
269
+ # Extract merchant name (usually first non-empty line)
270
+ for line in lines:
271
+ if line.strip() and len(line.strip()) > 2:
272
+ extracted_data['merchant'] = line.strip()
273
+ break
274
+
275
+ # Extract date using regex patterns
276
+ date_patterns = [
277
+ r'\d{1,2}[/-]\d{1,2}[/-]\d{2,4}',
278
+ r'\d{4}[/-]\d{1,2}[/-]\d{1,2}',
279
+ r'\d{1,2}\s+\w+\s+\d{4}'
280
+ ]
281
+
282
+ for line in lines:
283
+ for pattern in date_patterns:
284
+ match = re.search(pattern, line)
285
+ if match:
286
+ extracted_data['date'] = match.group()
287
+ break
288
+ if extracted_data['date']:
289
+ break
290
+
291
+ # Extract total amount
292
+ amount_patterns = [
293
+ r'total[:\s]*\$?(\d+\.?\d*)',
294
+ r'amount[:\s]*\$?(\d+\.?\d*)',
295
+ r'sum[:\s]*\$?(\d+\.?\d*)',
296
+ r'\$(\d+\.?\d*)'
297
+ ]
298
+
299
+ for line in lines:
300
+ line_lower = line.lower()
301
+ for pattern in amount_patterns:
302
+ match = re.search(pattern, line_lower)
303
+ if match:
304
+ try:
305
+ amount = float(match.group(1))
306
+ if amount > 0:
307
+ extracted_data['total_amount'] = amount
308
+ break
309
+ except ValueError:
310
+ continue
311
+ if extracted_data['total_amount']:
312
+ break
313
+
314
+ # Extract line items (simplified approach)
315
+ line_items = []
316
+ for line in lines:
317
+ # Look for lines with item and price pattern
318
+ item_match = re.search(r'(.+?)\s+(\d+\.?\d*)', line)
319
+ if item_match and len(item_match.group(1)) > 2:
320
+ try:
321
+ item_name = item_match.group(1).strip()
322
+ item_price = float(item_match.group(2))
323
+ if item_price > 0:
324
+ line_items.append([item_name, item_price])
325
+ except ValueError:
326
+ continue
327
+
328
+ extracted_data['line_items'] = line_items[:10] # Limit to 10 items
329
+
330
+ return extracted_data
331
+
332
+ def _create_empty_data(self):
333
+ """Create empty extracted data structure"""
334
+ return {
335
+ 'merchant': '',
336
+ 'date': '',
337
+ 'total_amount': 0.0,
338
+ 'line_items': []
339
+ }
340
+
341
+ # ========== C) ENHANCED DATABASE SERVICE ==========
342
+ class DatabaseService:
343
+ def __init__(self, db_name='fin_genius.db'):
344
+ self.conn = sqlite3.connect(db_name, check_same_thread=False)
345
+ self.cursor = self.conn.cursor()
346
+ self._initialize_db()
347
+
348
+ def _initialize_db(self):
349
+ # Existing tables...
350
+ self.cursor.execute('''CREATE TABLE IF NOT EXISTS users
351
+ (phone TEXT PRIMARY KEY,
352
+ name TEXT,
353
+ password_hash TEXT,
354
+ monthly_income INTEGER DEFAULT 0,
355
+ savings_goal INTEGER DEFAULT 0,
356
+ current_balance INTEGER DEFAULT 0,
357
+ is_verified BOOLEAN DEFAULT FALSE,
358
+ family_group TEXT DEFAULT NULL,
359
+ last_balance_alert TIMESTAMP DEFAULT NULL,
360
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')
361
+
362
+ self.cursor.execute('''CREATE TABLE IF NOT EXISTS expenses
363
+ (id INTEGER PRIMARY KEY AUTOINCREMENT,
364
+ phone TEXT,
365
+ category TEXT,
366
+ allocated INTEGER DEFAULT 0,
367
+ spent INTEGER DEFAULT 0,
368
+ date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
369
+ is_recurring BOOLEAN DEFAULT FALSE,
370
+ recurrence_pattern TEXT DEFAULT NULL,
371
+ next_occurrence TIMESTAMP DEFAULT NULL,
372
+ FOREIGN KEY(phone) REFERENCES users(phone))''')
373
+
374
+ self.cursor.execute('''CREATE TABLE IF NOT EXISTS spending_log
375
+ (id INTEGER PRIMARY KEY AUTOINCREMENT,
376
+ phone TEXT,
377
+ category TEXT,
378
+ amount INTEGER,
379
+ description TEXT,
380
+ date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
381
+ balance_after INTEGER,
382
+ receipt_id TEXT DEFAULT NULL,
383
+ FOREIGN KEY(phone) REFERENCES users(phone))''')
384
+
385
+ self.cursor.execute('''CREATE TABLE IF NOT EXISTS investments
386
+ (id INTEGER PRIMARY KEY AUTOINCREMENT,
387
+ phone TEXT,
388
+ type TEXT,
389
+ name TEXT,
390
+ amount INTEGER,
391
+ date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
392
+ notes TEXT,
393
+ FOREIGN KEY(phone) REFERENCES users(phone))''')
394
+
395
+ self.cursor.execute('''CREATE TABLE IF NOT EXISTS auth_attempts
396
+ (phone TEXT PRIMARY KEY,
397
+ attempts INTEGER DEFAULT 1,
398
+ last_attempt TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')
399
+
400
+ self.cursor.execute('''CREATE TABLE IF NOT EXISTS family_groups
401
+ (group_id TEXT PRIMARY KEY,
402
+ name TEXT,
403
+ admin_phone TEXT,
404
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')
405
+
406
+ self.cursor.execute('''CREATE TABLE IF NOT EXISTS alerts
407
+ (id INTEGER PRIMARY KEY AUTOINCREMENT,
408
+ phone TEXT,
409
+ alert_type TEXT,
410
+ message TEXT,
411
+ sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
412
+ FOREIGN KEY(phone) REFERENCES users(phone))''')
413
+
414
+ # NEW: Receipts table
415
+ self.cursor.execute('''CREATE TABLE IF NOT EXISTS receipts
416
+ (receipt_id TEXT PRIMARY KEY,
417
+ user_phone TEXT,
418
+ image_path TEXT,
419
+ processed_image_path TEXT,
420
+ merchant TEXT,
421
+ amount REAL,
422
+ receipt_date TEXT,
423
+ category TEXT,
424
+ ocr_confidence REAL,
425
+ raw_text TEXT,
426
+ extracted_data TEXT,
427
+ is_validated BOOLEAN DEFAULT FALSE,
428
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
429
+ FOREIGN KEY(user_phone) REFERENCES users(phone))''')
430
+
431
+ self.conn.commit()
432
+
433
+ # Existing methods remain the same...
434
+ def get_user(self, phone):
435
+ self.cursor.execute('''SELECT name, monthly_income, savings_goal, family_group, current_balance
436
+ FROM users WHERE phone=?''', (phone,))
437
+ return self.cursor.fetchone()
438
+
439
+ def authenticate_user(self, phone, password):
440
+ self.cursor.execute('''SELECT name, password_hash FROM users WHERE phone=?''', (phone,))
441
+ result = self.cursor.fetchone()
442
+ if result and verify_password(password, result[1]):
443
+ return result[0]
444
+ return None
445
+
446
+ def create_user(self, phone, name, password):
447
+ try:
448
+ password_hash = hash_password(password)
449
+ self.cursor.execute('''INSERT INTO users (phone, name, password_hash, current_balance) VALUES (?, ?, ?, ?)''',
450
+ (phone, name, password_hash, 0))
451
+ self.conn.commit()
452
+ return True
453
+ except sqlite3.IntegrityError:
454
+ return False
455
+
456
+ def update_user_balance(self, phone, new_balance):
457
+ self.cursor.execute('''UPDATE users SET current_balance=? WHERE phone=?''',
458
+ (new_balance, phone))
459
+ self.conn.commit()
460
+
461
+ def get_current_balance(self, phone):
462
+ self.cursor.execute('''SELECT current_balance FROM users WHERE phone=?''', (phone,))
463
+ result = self.cursor.fetchone()
464
+ return result[0] if result else 0
465
+
466
+ def add_income(self, phone, amount, description="Income added"):
467
+ current_balance = self.get_current_balance(phone)
468
+ new_balance = current_balance + amount
469
+
470
+ self.cursor.execute('''INSERT INTO spending_log
471
+ (phone, category, amount, description, balance_after)
472
+ VALUES (?, ?, ?, ?, ?)''',
473
+ (phone, "Income", -amount, description, new_balance))
474
+
475
+ self.update_user_balance(phone, new_balance)
476
+ self.conn.commit()
477
+
478
+ return new_balance
479
+
480
+ def update_financials(self, phone, income, savings):
481
+ self.cursor.execute('''UPDATE users
482
+ SET monthly_income=?, savings_goal=?
483
+ WHERE phone=?''',
484
+ (income, savings, phone))
485
+ self.conn.commit()
486
+
487
+ def get_expenses(self, phone, months_back=3):
488
+ end_date = datetime.now()
489
+ start_date = end_date - relativedelta(months=months_back)
490
+
491
+ self.cursor.execute('''SELECT category, allocated, spent, date(date) as exp_date, is_recurring
492
+ FROM expenses
493
+ WHERE phone=? AND date BETWEEN ? AND ?
494
+ ORDER BY allocated DESC''',
495
+ (phone, start_date.strftime('%Y-%m-%d'), end_date.strftime('%Y-%m-%d')))
496
+ return self.cursor.fetchall()
497
+
498
+ def update_expense_allocations(self, phone, allocations):
499
+ self.cursor.execute('''DELETE FROM expenses WHERE phone=? AND allocated > 0 AND is_recurring=FALSE''', (phone,))
500
+
501
+ for category, alloc in zip(EXPENSE_CATEGORIES, allocations):
502
+ if alloc > 0:
503
+ self.cursor.execute('''INSERT INTO expenses
504
+ (phone, category, allocated)
505
+ VALUES (?, ?, ?)''',
506
+ (phone, category, alloc))
507
+
508
+ self.conn.commit()
509
+
510
+ def log_spending(self, phone, category, amount, description="", receipt_id=None):
511
+ current_balance = self.get_current_balance(phone)
512
+ new_balance = current_balance - amount
513
+
514
+ self.cursor.execute('''INSERT INTO spending_log
515
+ (phone, category, amount, description, balance_after, receipt_id)
516
+ VALUES (?, ?, ?, ?, ?, ?)''',
517
+ (phone, category, amount, description, new_balance, receipt_id))
518
+
519
+ self.update_user_balance(phone, new_balance)
520
+ self.conn.commit()
521
+
522
+ return new_balance
523
+
524
+ def record_expense(self, phone, category, amount, description="", is_recurring=False, recurrence_pattern=None, receipt_id=None):
525
+ new_balance = self.log_spending(phone, category, amount, description, receipt_id)
526
+
527
+ self.cursor.execute('''SELECT allocated, spent
528
+ FROM expenses
529
+ WHERE phone=? AND category=? AND is_recurring=FALSE''',
530
+ (phone, category))
531
+ result = self.cursor.fetchone()
532
+
533
+ if is_recurring:
534
+ next_occurrence = self._calculate_next_occurrence(datetime.now(), recurrence_pattern)
535
+ self.cursor.execute('''INSERT INTO expenses
536
+ (phone, category, spent, is_recurring, recurrence_pattern, next_occurrence)
537
+ VALUES (?, ?, ?, ?, ?, ?)''',
538
+ (phone, category, amount, True, recurrence_pattern, next_occurrence))
539
+ elif result:
540
+ alloc, spent = result
541
+ new_spent = spent + amount
542
+ self.cursor.execute('''UPDATE expenses
543
+ SET spent=?
544
+ WHERE phone=? AND category=? AND is_recurring=FALSE''',
545
+ (new_spent, phone, category))
546
+ else:
547
+ self.cursor.execute('''INSERT INTO expenses
548
+ (phone, category, spent)
549
+ VALUES (?, ?, ?)''',
550
+ (phone, category, amount))
551
+
552
+ self.conn.commit()
553
+ return True, new_balance
554
+
555
+ def _calculate_next_occurrence(self, current_date, pattern):
556
+ if pattern == "Daily":
557
+ return current_date + timedelta(days=1)
558
+ elif pattern == "Weekly":
559
+ return current_date + timedelta(weeks=1)
560
+ elif pattern == "Monthly":
561
+ return current_date + relativedelta(months=1)
562
+ elif pattern == "Quarterly":
563
+ return current_date + relativedelta(months=3)
564
+ elif pattern == "Yearly":
565
+ return current_date + relativedelta(years=1)
566
+ return current_date
567
+
568
+ def record_investment(self, phone, inv_type, name, amount, notes):
569
+ self.cursor.execute('''INSERT INTO investments
570
+ (phone, type, name, amount, notes)
571
+ VALUES (?, ?, ?, ?, ?)''',
572
+ (phone, inv_type, name, amount, notes))
573
+ self.conn.commit()
574
+ return True
575
+
576
+ def get_investments(self, phone):
577
+ self.cursor.execute('''SELECT type, name, amount, date(date) as inv_date, notes
578
+ FROM investments
579
+ WHERE phone=?
580
+ ORDER BY date DESC''', (phone,))
581
+ return self.cursor.fetchall()
582
+
583
+ def get_spending_log(self, phone, limit=50):
584
+ self.cursor.execute('''SELECT category, amount, description, date, balance_after
585
+ FROM spending_log
586
+ WHERE phone=?
587
+ ORDER BY date DESC
588
+ LIMIT ?''', (phone, limit))
589
+ return self.cursor.fetchall()
590
+
591
+ def create_family_group(self, group_name, admin_phone):
592
+ group_id = f"FG-{admin_phone[-4:]}-{int(time.time())}"
593
+ try:
594
+ self.cursor.execute('''INSERT INTO family_groups
595
+ (group_id, name, admin_phone)
596
+ VALUES (?, ?, ?)''',
597
+ (group_id, group_name, admin_phone))
598
+
599
+ self.cursor.execute('''UPDATE users
600
+ SET family_group=?
601
+ WHERE phone=?''',
602
+ (group_id, admin_phone))
603
+
604
+ self.conn.commit()
605
+ return group_id
606
+ except sqlite3.IntegrityError:
607
+ return None
608
+
609
+ def join_family_group(self, phone, group_id):
610
+ self.cursor.execute('''UPDATE users
611
+ SET family_group=?
612
+ WHERE phone=?''',
613
+ (group_id, phone))
614
+ self.conn.commit()
615
+ return True
616
+
617
+ def get_family_group(self, group_id):
618
+ self.cursor.execute('''SELECT name, admin_phone FROM family_groups WHERE group_id=?''', (group_id,))
619
+ return self.cursor.fetchone()
620
+
621
+ def get_family_members(self, group_id):
622
+ self.cursor.execute('''SELECT phone, name FROM users WHERE family_group=?''', (group_id,))
623
+ return self.cursor.fetchall()
624
+
625
+ # NEW: Receipt-related methods
626
+ def save_receipt(self, phone, receipt_data):
627
+ """Save receipt data to database"""
628
+ receipt_id = f"REC-{phone[-4:]}-{int(time.time())}"
629
+
630
+ try:
631
+ self.cursor.execute('''INSERT INTO receipts
632
+ (receipt_id, user_phone, image_path, processed_image_path,
633
+ merchant, amount, receipt_date, category, ocr_confidence,
634
+ raw_text, extracted_data, is_validated)
635
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''',
636
+ (receipt_id, phone, receipt_data.get('image_path', ''),
637
+ receipt_data.get('processed_image_path', ''),
638
+ receipt_data.get('merchant', ''),
639
+ receipt_data.get('amount', 0.0),
640
+ receipt_data.get('date', ''),
641
+ receipt_data.get('category', ''),
642
+ receipt_data.get('confidence', 0.0),
643
+ receipt_data.get('raw_text', ''),
644
+ json.dumps(receipt_data.get('extracted_data', {})),
645
+ receipt_data.get('is_validated', False)))
646
+
647
+ self.conn.commit()
648
+ return receipt_id
649
+ except sqlite3.Error as e:
650
+ print(f"Database error saving receipt: {e}")
651
+ return None
652
+
653
+ def get_receipts(self, phone, limit=20):
654
+ """Get user's receipts"""
655
+ self.cursor.execute('''SELECT receipt_id, merchant, amount, receipt_date, category,
656
+ ocr_confidence, is_validated, created_at
657
+ FROM receipts
658
+ WHERE user_phone=?
659
+ ORDER BY created_at DESC
660
+ LIMIT ?''', (phone, limit))
661
+ return self.cursor.fetchall()
662
+
663
+ def update_receipt(self, receipt_id, updates):
664
+ """Update receipt information"""
665
+ set_clause = ", ".join([f"{key}=?" for key in updates.keys()])
666
+ values = list(updates.values()) + [receipt_id]
667
+
668
+ self.cursor.execute(f'''UPDATE receipts SET {set_clause} WHERE receipt_id=?''', values)
669
+ self.conn.commit()
670
+
671
+ def auto_categorize_receipt(self, phone, merchant, amount):
672
+ """Auto-categorize based on user's spending patterns"""
673
+ # Get user's most common category for similar merchants
674
+ self.cursor.execute('''SELECT category, COUNT(*) as count
675
+ FROM spending_log
676
+ WHERE phone=? AND (description LIKE ? OR description LIKE ?)
677
+ GROUP BY category
678
+ ORDER BY count DESC
679
+ LIMIT 1''',
680
+ (phone, f'%{merchant}%', f'%{merchant.split()[0]}%'))
681
+
682
+ result = self.cursor.fetchone()
683
+ if result:
684
+ return result[0]
685
+
686
+ # Fallback categorization based on merchant keywords
687
+ merchant_lower = merchant.lower()
688
+ if any(word in merchant_lower for word in ['grocery', 'market', 'food', 'super']):
689
+ return "Groceries"
690
+ elif any(word in merchant_lower for word in ['restaurant', 'cafe', 'pizza', 'burger']):
691
+ return "Dining Out"
692
+ elif any(word in merchant_lower for word in ['gas', 'fuel', 'shell', 'bp']):
693
+ return "Transportation"
694
+ elif any(word in merchant_lower for word in ['pharmacy', 'medical', 'hospital']):
695
+ return "Healthcare"
696
+ else:
697
+ return "Miscellaneous"
698
+
699
+ # Real Twilio WhatsApp Service (unchanged)
700
+ class TwilioWhatsAppService:
701
+ def __init__(self):
702
+ self.account_sid = os.getenv('TWILIO_ACCOUNT_SID')
703
+ self.auth_token = os.getenv('TWILIO_AUTH_TOKEN')
704
+ self.whatsapp_number = 'whatsapp:+14155238886'
705
+
706
+ if self.account_sid and self.auth_token and TWILIO_AVAILABLE:
707
+ try:
708
+ self.client = Client(self.account_sid, self.auth_token)
709
+ print("βœ… Twilio WhatsApp Service initialized successfully")
710
+ self.enabled = True
711
+ except Exception as e:
712
+ print(f"❌ Failed to initialize Twilio: {e}")
713
+ self.client = None
714
+ self.enabled = False
715
+ else:
716
+ print("❌ Twilio credentials not found or Twilio not installed")
717
+ self.client = None
718
+ self.enabled = False
719
+
720
+ def send_whatsapp(self, phone, message):
721
+ if not self.enabled or not self.client:
722
+ print(f"πŸ“± [DEMO MODE] WhatsApp to {phone}: {message}")
723
+ return False
724
+
725
+ try:
726
+ to_whatsapp = f"whatsapp:{phone}"
727
+ twilio_message = self.client.messages.create(
728
+ body=message,
729
+ from_=self.whatsapp_number,
730
+ to=to_whatsapp
731
+ )
732
+
733
+ print(f"βœ… WhatsApp sent to {phone}: {twilio_message.sid}")
734
+ return True
735
+
736
+ except Exception as e:
737
+ print(f"❌ Failed to send WhatsApp to {phone}: {e}")
738
+ print(f"πŸ“± [FALLBACK] Message was: {message}")
739
+ return False
740
+
741
+ # Initialize services
742
+ db = DatabaseService()
743
+ twilio = TwilioWhatsAppService()
744
+ ocr_service = OCRService()
745
+
746
+ # Helper functions (unchanged)
747
+ def validate_phone_number(phone):
748
+ pattern = r'^\+\d{1,3}\d{6,14}$'
749
+ return re.match(pattern, phone) is not None
750
+
751
+ def validate_password(password):
752
+ if len(password) < 6:
753
+ return False, "Password must be at least 6 characters long"
754
+ if not re.search(r'[A-Za-z]', password):
755
+ return False, "Password must contain at least one letter"
756
+ if not re.search(r'\d', password):
757
+ return False, "Password must contain at least one number"
758
+ return True, "Password is valid"
759
+
760
+ def format_currency(amount):
761
+ return f"{int(amount):,} PKR" if amount else "0 PKR"
762
+
763
+ def generate_spending_chart(phone, months=3):
764
+ expenses = db.get_expenses(phone, months)
765
+ if not expenses:
766
+ return None
767
+
768
+ df = pd.DataFrame(expenses, columns=['Category', 'Allocated', 'Spent', 'Date', 'IsRecurring'])
769
+ df['Date'] = pd.to_datetime(df['Date'])
770
+ df['Month'] = df['Date'].dt.strftime('%Y-%m')
771
+
772
+ monthly_data = df.groupby(['Month', 'Category'])['Spent'].sum().unstack().fillna(0)
773
+
774
+ fig = go.Figure()
775
+ colors = px.colors.qualitative.Set3
776
+ for i, category in enumerate(monthly_data.columns):
777
+ fig.add_trace(go.Bar(
778
+ x=monthly_data.index,
779
+ y=monthly_data[category],
780
+ name=category,
781
+ marker_color=colors[i % len(colors)],
782
+ hoverinfo='y+name',
783
+ textposition='auto'
784
+ ))
785
+
786
+ fig.update_layout(
787
+ barmode='stack',
788
+ title=f'πŸ“Š Spending Trends (Last {months} Months)',
789
+ xaxis_title='Month',
790
+ yaxis_title='Amount (PKR)',
791
+ height=500,
792
+ plot_bgcolor='rgba(0,0,0,0)',
793
+ paper_bgcolor='rgba(0,0,0,0)'
794
+ )
795
+
796
+ return fig
797
+
798
+ def generate_balance_chart(phone):
799
+ spending_log = db.get_spending_log(phone, 100)
800
+ if not spending_log:
801
+ return None
802
+
803
+ df = pd.DataFrame(spending_log, columns=['Category', 'Amount', 'Description', 'Date', 'Balance'])
804
+ df['Date'] = pd.to_datetime(df['Date'])
805
+ df = df.sort_values('Date')
806
+
807
+ fig = go.Figure()
808
+ fig.add_trace(go.Scatter(
809
+ x=df['Date'],
810
+ y=df['Balance'],
811
+ mode='lines+markers',
812
+ name='Balance',
813
+ line=dict(color='#00CC96', width=3),
814
+ marker=dict(size=6),
815
+ hovertemplate='<b>Date:</b> %{x}<br><b>Balance:</b> %{y:,} PKR<extra></extra>'
816
+ ))
817
+
818
+ fig.update_layout(
819
+ title='πŸ’° Balance Trend Over Time',
820
+ xaxis_title='Date',
821
+ yaxis_title='Balance (PKR)',
822
+ height=400,
823
+ plot_bgcolor='rgba(0,0,0,0)',
824
+ paper_bgcolor='rgba(0,0,0,0)'
825
+ )
826
+
827
+ return fig
828
+
829
+ # ========== D) RECEIPT PROCESSING FUNCTIONS ==========
830
+ def process_receipt_image(image_file, phone):
831
+ """
832
+ Complete receipt processing pipeline
833
+ Returns: (success, status_message, extracted_data, image_preview)
834
+ """
835
+ try:
836
+ if not image_file:
837
+ return False, "❌ No image uploaded", {}, None
838
+
839
+ # Save uploaded image
840
+ timestamp = int(time.time())
841
+ filename = f"receipt_{phone}_{timestamp}.jpg"
842
+ image_path = os.path.join(RECEIPTS_DIR, filename)
843
+
844
+ # Handle different input types
845
+ if hasattr(image_file, 'name'):
846
+ # File upload
847
+ with open(image_path, 'wb') as f:
848
+ f.write(image_file.read())
849
+ else:
850
+ # Direct file path
851
+ image_path = image_file
852
+
853
+ # Preprocess image
854
+ processed_path, preprocessing_info = ImageProcessor.preprocess_receipt_image(image_path)
855
+
856
+ # Extract text using OCR
857
+ raw_text, confidence, extracted_data = ocr_service.extract_text_from_receipt(processed_path)
858
+
859
+ # Auto-categorize
860
+ if extracted_data.get('merchant'):
861
+ suggested_category = db.auto_categorize_receipt(
862
+ phone,
863
+ extracted_data['merchant'],
864
+ extracted_data.get('total_amount', 0)
865
+ )
866
+ extracted_data['suggested_category'] = suggested_category
867
+
868
+ # Prepare receipt data for database
869
+ receipt_data = {
870
+ 'image_path': image_path,
871
+ 'processed_image_path': processed_path,
872
+ 'merchant': extracted_data.get('merchant', ''),
873
+ 'amount': extracted_data.get('total_amount', 0.0),
874
+ 'date': extracted_data.get('date', ''),
875
+ 'category': extracted_data.get('suggested_category', 'Miscellaneous'),
876
+ 'confidence': confidence,
877
+ 'raw_text': raw_text,
878
+ 'extracted_data': extracted_data,
879
+ 'is_validated': False
880
+ }
881
+
882
+ # Save to database
883
+ receipt_id = db.save_receipt(phone, receipt_data)
884
+ extracted_data['receipt_id'] = receipt_id
885
+
886
+ status_msg = f"βœ… Receipt processed successfully! Confidence: {confidence:.1%}"
887
+ if confidence < 0.7:
888
+ status_msg += " ⚠️ Low confidence - please verify extracted data"
889
+
890
+ return True, status_msg, extracted_data, image_path
891
+
892
+ except Exception as e:
893
+ print(f"Receipt processing error: {e}")
894
+ return False, f"❌ Processing failed: {str(e)}", {}, None
895
+
896
+ def validate_and_save_receipt(phone, receipt_id, merchant, amount, date, category, line_items_data):
897
+ """
898
+ Validate edited receipt data and save as expense
899
+ """
900
+ try:
901
+ if not phone or not receipt_id:
902
+ return "❌ Session expired. Please sign in again.", "", [], []
903
+
904
+ if not merchant.strip():
905
+ return "❌ Merchant name is required", "", [], []
906
+
907
+ if amount <= 0:
908
+ return "❌ Amount must be positive", "", [], []
909
+
910
+ # Check balance
911
+ current_balance = db.get_current_balance(phone)
912
+ if current_balance < amount:
913
+ return "❌ Insufficient balance for this expense", "", [], []
914
+
915
+ # Update receipt in database
916
+ receipt_updates = {
917
+ 'merchant': merchant,
918
+ 'amount': amount,
919
+ 'receipt_date': date,
920
+ 'category': category,
921
+ 'is_validated': True
922
+ }
923
+ db.update_receipt(receipt_id, receipt_updates)
924
+
925
+ # Record as expense
926
+ description = f"Receipt: {merchant}"
927
+ if date:
928
+ description += f" ({date})"
929
+
930
+ success, new_balance = db.record_expense(
931
+ phone, category, amount, description, receipt_id=receipt_id
932
+ )
933
+
934
+ if not success:
935
+ return "❌ Failed to record expense", "", [], []
936
+
937
+ # Send WhatsApp confirmation
938
+ user_data = db.get_user(phone)
939
+ name = user_data[0] if user_data else "User"
940
+
941
+ msg = f"🧾 Receipt Expense - Hi {name}! Merchant: {merchant}, Amount: {format_currency(amount)}, Category: {category}, Remaining Balance: {format_currency(new_balance)}"
942
+ twilio.send_whatsapp(phone, msg)
943
+
944
+ # Get updated data for UI
945
+ expenses = db.get_expenses(phone)
946
+ formatted_expenses = []
947
+ if expenses:
948
+ for cat, alloc, spent, date, _ in expenses:
949
+ formatted_expenses.append([
950
+ cat, alloc, spent, alloc - spent, date.split()[0] if date else ""
951
+ ])
952
+
953
+ spending_log = db.get_spending_log(phone, 10)
954
+ formatted_spending_log = []
955
+ if spending_log:
956
+ for cat, amt, desc, date, balance_after in spending_log:
957
+ formatted_spending_log.append([
958
+ cat, amt, desc[:50] + "..." if len(desc) > 50 else desc,
959
+ date.split()[0] if date else "", balance_after
960
+ ])
961
+
962
+ status_msg = f"βœ… Receipt saved! Recorded {format_currency(amount)} for {category}"
963
+ balance_html = f"<div class='balance-amount'>πŸ’° {format_currency(new_balance)}</div>"
964
+
965
+ return status_msg, balance_html, formatted_expenses, formatted_spending_log
966
+
967
+ except Exception as e:
968
+ print(f"Receipt validation error: {e}")
969
+ return f"❌ Error saving receipt: {str(e)}", "", [], []
970
+
971
+ # ========== PAGE NAVIGATION FUNCTIONS ==========
972
+ def show_signin():
973
+ return [
974
+ gr.update(visible=False), # landing_page
975
+ gr.update(visible=True), # signin_page
976
+ gr.update(visible=False), # signup_page
977
+ gr.update(visible=False), # dashboard_page
978
+ "", # Clear signin inputs
979
+ ""
980
+ ]
981
+
982
+ def show_signup():
983
+ return [
984
+ gr.update(visible=False), # landing_page
985
+ gr.update(visible=False), # signin_page
986
+ gr.update(visible=True), # signup_page
987
+ gr.update(visible=False), # dashboard_page
988
+ "", # Clear signup inputs
989
+ "",
990
+ "",
991
+ ""
992
+ ]
993
+
994
+ def show_dashboard(phone, name):
995
+ user_data = db.get_user(phone)
996
+ current_balance = user_data[4] if user_data else 0
997
+ monthly_income = user_data[1] if user_data else 0
998
+ savings_goal = user_data[2] if user_data else 0
999
+
1000
+ # Get expense data
1001
+ expenses = db.get_expenses(phone)
1002
+ formatted_expenses = []
1003
+ if expenses:
1004
+ for cat, alloc, spent, date, _ in expenses:
1005
+ formatted_expenses.append([
1006
+ cat, alloc, spent, alloc - spent, date.split()[0] if date else ""
1007
+ ])
1008
+
1009
+ # Get investment data
1010
+ investments = db.get_investments(phone)
1011
+ formatted_investments = []
1012
+ if investments:
1013
+ for inv_type, name, amount, date, notes in investments:
1014
+ formatted_investments.append([
1015
+ inv_type, name, amount, date.split()[0] if date else "", notes or ""
1016
+ ])
1017
+
1018
+ # Get spending log
1019
+ spending_log = db.get_spending_log(phone, 10)
1020
+ formatted_spending_log = []
1021
+ if spending_log:
1022
+ for category, amount, description, date, balance_after in spending_log:
1023
+ formatted_spending_log.append([
1024
+ category, amount, description[:50] + "..." if len(description) > 50 else description,
1025
+ date.split()[0] if date else "", balance_after
1026
+ ])
1027
+
1028
+ # Get family info
1029
+ family_info = "No family group"
1030
+ family_members = []
1031
+ if user_data and user_data[3]:
1032
+ group_data = db.get_family_group(user_data[3])
1033
+ if group_data:
1034
+ family_info = f"Family Group: {group_data[0]} (Admin: {group_data[1]})"
1035
+ members = db.get_family_members(user_data[3])
1036
+ family_members = [[m[0], m[1]] for m in members]
1037
+
1038
+ # Get receipt data
1039
+ receipts = db.get_receipts(phone)
1040
+ formatted_receipts = []
1041
+ if receipts:
1042
+ for receipt_id, merchant, amount, date, category, confidence, is_validated, created_at in receipts:
1043
+ status = "βœ… Validated" if is_validated else "⏳ Pending"
1044
+ formatted_receipts.append([
1045
+ receipt_id, merchant or "Unknown", format_currency(amount),
1046
+ date or "N/A", category or "N/A", f"{confidence:.1%}",
1047
+ status, created_at.split()[0] if created_at else ""
1048
+ ])
1049
+
1050
+ # Prepare allocation inputs
1051
+ alloc_inputs = []
1052
+ if expenses:
1053
+ alloc_dict = {cat: alloc for cat, alloc, _, _, _ in expenses}
1054
+ alloc_inputs = [alloc_dict.get(cat, 0) for cat in EXPENSE_CATEGORIES]
1055
+ else:
1056
+ alloc_inputs = [0] * len(EXPENSE_CATEGORIES)
1057
+
1058
+ return [
1059
+ gr.update(visible=False), # landing_page
1060
+ gr.update(visible=False), # signin_page
1061
+ gr.update(visible=False), # signup_page
1062
+ gr.update(visible=True), # dashboard_page
1063
+ f"Welcome back, {name}! πŸ‘‹", # welcome message
1064
+ f"<div class='balance-amount'>πŸ’° {format_currency(current_balance)}</div>", # balance display
1065
+ monthly_income, # income
1066
+ savings_goal, # savings_goal
1067
+ *alloc_inputs, # allocation inputs
1068
+ formatted_expenses, # expense_table
1069
+ formatted_investments, # investments_table
1070
+ formatted_spending_log, # spending_log_table
1071
+ generate_spending_chart(phone), # spending_chart
1072
+ generate_balance_chart(phone), # balance_chart
1073
+ family_info, # family_info
1074
+ family_members, # family_members
1075
+ formatted_receipts # receipts_table
1076
+ ]
1077
+
1078
+ def return_to_landing():
1079
+ return [
1080
+ gr.update(visible=True), # landing_page
1081
+ gr.update(visible=False), # signin_page
1082
+ gr.update(visible=False), # signup_page
1083
+ gr.update(visible=False), # dashboard_page
1084
+ "", # Clear welcome
1085
+ "<div class='balance-amount'>πŸ’° 0 PKR</div>" # Clear balance
1086
+ ]
1087
+
1088
+ # ========== AUTHENTICATION FUNCTIONS ==========
1089
+ def authenticate_user(phone, password):
1090
+ if not phone or not password:
1091
+ return "❌ Please fill all fields"
1092
+
1093
+ if not validate_phone_number(phone):
1094
+ return "❌ Invalid phone format. Use +92XXXXXXXXXX"
1095
+
1096
+ user_name = db.authenticate_user(phone, password)
1097
+
1098
+ if not user_name:
1099
+ return "❌ Invalid phone number or password."
1100
+
1101
+ return f"βœ… Signed in as {user_name}"
1102
+
1103
+ def register_user(name, phone, password, confirm_password):
1104
+ if not name or not phone or not password or not confirm_password:
1105
+ return "❌ Please fill all fields"
1106
+
1107
+ if not validate_phone_number(phone):
1108
+ return "❌ Invalid phone format. Use +92XXXXXXXXXX"
1109
+
1110
+ if password != confirm_password:
1111
+ return "❌ Passwords don't match"
1112
+
1113
+ is_valid, password_msg = validate_password(password)
1114
+ if not is_valid:
1115
+ return f"❌ {password_msg}"
1116
+
1117
+ success = db.create_user(phone, name, password)
1118
+ if not success:
1119
+ return "⚠️ This number is already registered"
1120
+
1121
+ msg = f"🏦 Welcome to FinGenius Pro, {name}! Your account has been created successfully. You can now track expenses, manage budgets, and receive instant financial alerts. Start by adding your first balance! πŸ’°"
1122
+ twilio.send_whatsapp(phone, msg)
1123
+
1124
+ return "βœ… Registration complete! Check WhatsApp for confirmation and sign in to continue."
1125
+
1126
+ def add_balance(phone, amount_val, description=""):
1127
+ if not phone:
1128
+ return "❌ Session expired. Please sign in again.", ""
1129
+
1130
+ if amount_val <= 0:
1131
+ return "❌ Amount must be positive", ""
1132
+
1133
+ new_balance = db.add_income(phone, amount_val, description or "Balance added")
1134
+
1135
+ user_data = db.get_user(phone)
1136
+ if user_data:
1137
+ name = user_data[0]
1138
+ msg = f"πŸ’° Balance Added - Hi {name}! Added: {format_currency(amount_val)}. New Balance: {format_currency(new_balance)}. Description: {description or 'Balance update'}"
1139
+ twilio.send_whatsapp(phone, msg)
1140
+
1141
+ return f"βœ… Added {format_currency(amount_val)} to balance!", f"<div class='balance-amount'>πŸ’° {format_currency(new_balance)}</div>"
1142
+
1143
+ def update_financials(phone, income_val, savings_val):
1144
+ if not phone:
1145
+ return "❌ Session expired. Please sign in again."
1146
+
1147
+ if income_val < 0 or savings_val < 0:
1148
+ return "❌ Values cannot be negative"
1149
+
1150
+ db.update_financials(phone, income_val, savings_val)
1151
+
1152
+ user_data = db.get_user(phone)
1153
+ if user_data:
1154
+ name = user_data[0]
1155
+ msg = f"πŸ“Š Financial Goals Updated - Hi {name}! Monthly Income: {format_currency(income_val)}, Savings Goal: {format_currency(savings_val)}. Your budget planning is now ready! 🎯"
1156
+ twilio.send_whatsapp(phone, msg)
1157
+
1158
+ return f"βœ… Updated! Monthly Income: {format_currency(income_val)}, Savings Goal: {format_currency(savings_val)}"
1159
+
1160
+ def save_allocations(phone, *allocations):
1161
+ if not phone:
1162
+ return "❌ Session expired. Please sign in again.", []
1163
+
1164
+ if any(alloc < 0 for alloc in allocations):
1165
+ return "❌ Allocations cannot be negative", []
1166
+
1167
+ total_alloc = sum(allocations)
1168
+ user_data = db.get_user(phone)
1169
+
1170
+ if not user_data:
1171
+ return "❌ User not found", []
1172
+
1173
+ if total_alloc + user_data[2] > user_data[1]:
1174
+ return "❌ Total allocations exceed available income!", []
1175
+
1176
+ db.update_expense_allocations(phone, allocations)
1177
+
1178
+ name = user_data[0]
1179
+ msg = f"πŸ“‹ Budget Allocated - Hi {name}! Your monthly budget has been set. Total allocated: {format_currency(total_alloc)}. Start tracking your expenses now! πŸ’³"
1180
+ twilio.send_whatsapp(phone, msg)
1181
+
1182
+ expenses = db.get_expenses(phone)
1183
+ formatted_expenses = []
1184
+ if expenses:
1185
+ for cat, alloc, spent, date, _ in expenses:
1186
+ formatted_expenses.append([
1187
+ cat, alloc, spent, alloc - spent, date.split()[0] if date else ""
1188
+ ])
1189
+
1190
+ return "βœ… Budget allocations saved!", formatted_expenses
1191
+
1192
+ def record_expense(phone, category, amount, description="", is_recurring=False, recurrence_pattern=None):
1193
+ if not phone:
1194
+ return "❌ Session expired. Please sign in again.", "", [], []
1195
+
1196
+ if amount <= 0:
1197
+ return "❌ Amount must be positive", "", [], []
1198
+
1199
+ current_balance = db.get_current_balance(phone)
1200
+ if current_balance < amount:
1201
+ return "❌ Insufficient balance for this expense", "", [], []
1202
+
1203
+ success, new_balance = db.record_expense(phone, category, amount, description, is_recurring, recurrence_pattern)
1204
+
1205
+ if not success:
1206
+ return "❌ Failed to record expense", "", [], []
1207
+
1208
+ user_data = db.get_user(phone)
1209
+ name = user_data[0] if user_data else "User"
1210
+
1211
+ msg = f"πŸ’Έ Expense Recorded - Hi {name}! Category: {category}, Amount: {format_currency(amount)}, Remaining Balance: {format_currency(new_balance)}"
1212
+ if description:
1213
+ msg += f", Note: {description}"
1214
+ if is_recurring:
1215
+ msg += f" (Recurring: {recurrence_pattern})"
1216
+ twilio.send_whatsapp(phone, msg)
1217
+
1218
+ expenses = db.get_expenses(phone)
1219
+ formatted_expenses = []
1220
+ if expenses:
1221
+ for cat, alloc, spent, date, _ in expenses:
1222
+ formatted_expenses.append([
1223
+ cat, alloc, spent, alloc - spent, date.split()[0] if date else ""
1224
+ ])
1225
+
1226
+ spending_log = db.get_spending_log(phone, 10)
1227
+ formatted_spending_log = []
1228
+ if spending_log:
1229
+ for cat, amt, desc, date, balance_after in spending_log:
1230
+ formatted_spending_log.append([
1231
+ cat, amt, desc[:50] + "..." if len(desc) > 50 else desc,
1232
+ date.split()[0] if date else "", balance_after
1233
+ ])
1234
+
1235
+ status_msg = f"βœ… Recorded {format_currency(amount)} for {category}"
1236
+ balance_html = f"<div class='balance-amount'>πŸ’° {format_currency(new_balance)}</div>"
1237
+
1238
+ return status_msg, balance_html, formatted_expenses, formatted_spending_log
1239
+
1240
+ def add_investment(phone, inv_type, name, amount, notes):
1241
+ if not phone:
1242
+ return "❌ Session expired. Please sign in again.", "", []
1243
+
1244
+ if amount <= 0:
1245
+ return "❌ Amount must be positive", "", []
1246
+
1247
+ current_balance = db.get_current_balance(phone)
1248
+ if current_balance < amount:
1249
+ return "❌ Insufficient balance for investment", "", []
1250
+
1251
+ new_balance = db.log_spending(phone, "Investment", amount, f"Investment: {name}")
1252
+ db.record_investment(phone, inv_type, name, amount, notes)
1253
+
1254
+ user_data = db.get_user(phone)
1255
+ if user_data:
1256
+ user_name = user_data[0]
1257
+ msg = f"πŸ“ˆ Investment Added - Hi {user_name}! Type: {inv_type}, Name: {name}, Amount: {format_currency(amount)}, Remaining Balance: {format_currency(new_balance)}"
1258
+ if notes:
1259
+ msg += f", Notes: {notes}"
1260
+ twilio.send_whatsapp(phone, msg)
1261
+
1262
+ investments = db.get_investments(phone)
1263
+ formatted_investments = []
1264
+ if investments:
1265
+ for inv_type, name, amount, date, notes in investments:
1266
+ formatted_investments.append([
1267
+ inv_type, name, amount, date.split()[0] if date else "", notes or ""
1268
+ ])
1269
+
1270
+ balance_html = f"<div class='balance-amount'>πŸ’° {format_currency(new_balance)}</div>"
1271
+
1272
+ return f"βœ… Added investment: {name} ({format_currency(amount)})", balance_html, formatted_investments
1273
+
1274
+ def create_family_group(phone, group_name):
1275
+ if not phone or not group_name:
1276
+ return "❌ Group name required", "", []
1277
+
1278
+ group_id = db.create_family_group(group_name, phone)
1279
+ if not group_id:
1280
+ return "❌ Failed to create group", "", []
1281
+
1282
+ user_data = db.get_user(phone)
1283
+ if user_data:
1284
+ name = user_data[0]
1285
+ msg = f"πŸ‘ͺ Family Group Created - Hi {name}! You've created '{group_name}' (ID: {group_id}). Share this ID with family members to join. Manage finances together! 🏠"
1286
+ twilio.send_whatsapp(phone, msg)
1287
+
1288
+ return f"βœ… Created group: {group_name} (ID: {group_id})", f"Family Group: {group_name} (Admin: {phone})", [[phone, db.get_user(phone)[0] if db.get_user(phone) else "You"]]
1289
+
1290
+ def join_family_group(phone, group_id):
1291
+ if not phone or not group_id:
1292
+ return "❌ Group ID required", "", []
1293
+
1294
+ success = db.join_family_group(phone, group_id)
1295
+ if not success:
1296
+ return "❌ Failed to join group", "", []
1297
+
1298
+ group_data = db.get_family_group(group_id)
1299
+ if not group_data:
1300
+ return "❌ Group not found", "", []
1301
+
1302
+ user_data = db.get_user(phone)
1303
+ if user_data:
1304
+ name = user_data[0]
1305
+ msg = f"πŸ‘ͺ Joined Family Group - Hi {name}! You've joined '{group_data[0]}'. Start collaborating on family finances together! 🀝"
1306
+ twilio.send_whatsapp(phone, msg)
1307
+
1308
+ members = db.get_family_members(group_id)
1309
+ member_list = [[m[0], m[1]] for m in members]
1310
+
1311
+ return f"βœ… Joined group: {group_data[0]}", f"Family Group: {group_data[0]} (Admin: {group_data[1]})", member_list
1312
+
1313
+ # ========== E) RECEIPT PROCESSING EVENT HANDLERS ==========
1314
+ def handle_receipt_upload(image_file, phone):
1315
+ """Handle receipt image upload and processing"""
1316
+ if not phone:
1317
+ return "❌ Please sign in first", {}, "", "", "", [], None, ""
1318
+
1319
+ if not image_file:
1320
+ return "❌ Please upload an image", {}, "", "", "", [], None, ""
1321
+
1322
+ # Process the receipt
1323
+ success, status, extracted_data, image_path = process_receipt_image(image_file, phone)
1324
+
1325
+ if not success:
1326
+ return status, {}, "", "", "", [], None, ""
1327
+
1328
+ # Prepare UI updates
1329
+ merchant = extracted_data.get('merchant', '')
1330
+ amount = extracted_data.get('total_amount', 0.0)
1331
+ date = extracted_data.get('date', '')
1332
+ category = extracted_data.get('suggested_category', 'Miscellaneous')
1333
+ line_items = extracted_data.get('line_items', [])
1334
+
1335
+ # Create image preview
1336
+ try:
1337
+ image_preview = Image.open(image_path)
1338
+ # Resize for preview
1339
+ image_preview.thumbnail((400, 600))
1340
+ except:
1341
+ image_preview = None
1342
+
1343
+ return (
1344
+ status,
1345
+ {"receipt_id": extracted_data.get('receipt_id', ''), "confidence": extracted_data.get('confidence', 0.0)},
1346
+ merchant,
1347
+ amount,
1348
+ date,
1349
+ line_items,
1350
+ image_preview,
1351
+ category
1352
+ )
1353
+
1354
+ def handle_receipt_save(phone, receipt_data, merchant, amount, date, category, line_items_data):
1355
+ """Save validated receipt as expense"""
1356
+ if not phone or not receipt_data:
1357
+ return "❌ No receipt data to save", "", [], []
1358
+
1359
+ receipt_id = receipt_data.get('receipt_id')
1360
+ if not receipt_id:
1361
+ return "❌ Invalid receipt data", "", [], []
1362
+
1363
+ return validate_and_save_receipt(phone, receipt_id, merchant, amount, date, category, line_items_data)
1364
+
1365
+ # ========== F) INTERFACE ==========
1366
+ custom_css = """
1367
+ /* Fixed CSS for proper page transitions */
1368
+ .gradio-container {
1369
+ max-width: 1200px !important;
1370
+ margin: 0 auto !important;
1371
+ }
1372
+
1373
+ .landing-hero {
1374
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1375
+ min-height: 80vh;
1376
+ padding: 3rem 2rem;
1377
+ color: white;
1378
+ text-align: center;
1379
+ border-radius: 20px;
1380
+ margin: 2rem 0;
1381
+ }
1382
+
1383
+ .hero-title {
1384
+ font-size: 3.5rem;
1385
+ font-weight: 700;
1386
+ margin-bottom: 1rem;
1387
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
1388
+ }
1389
+
1390
+ .hero-subtitle {
1391
+ font-size: 1.5rem;
1392
+ margin-bottom: 2rem;
1393
+ opacity: 0.9;
1394
+ }
1395
+
1396
+ .features-grid {
1397
+ display: grid;
1398
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
1399
+ gap: 2rem;
1400
+ margin: 3rem 0;
1401
+ }
1402
+
1403
+ .feature-card {
1404
+ background: rgba(255,255,255,0.1);
1405
+ backdrop-filter: blur(10px);
1406
+ border-radius: 15px;
1407
+ padding: 2rem;
1408
+ text-align: center;
1409
+ border: 1px solid rgba(255,255,255,0.2);
1410
+ transition: transform 0.3s ease;
1411
+ }
1412
+
1413
+ .feature-card:hover {
1414
+ transform: translateY(-5px);
1415
+ }
1416
+
1417
+ .feature-icon {
1418
+ font-size: 3rem;
1419
+ margin-bottom: 1rem;
1420
+ }
1421
+
1422
+ .auth-container {
1423
+ max-width: 450px;
1424
+ margin: 2rem auto;
1425
+ background: white;
1426
+ border-radius: 20px;
1427
+ padding: 3rem;
1428
+ box-shadow: 0 20px 40px rgba(0,0,0,0.1);
1429
+ border: 1px solid #e2e8f0;
1430
+ }
1431
+
1432
+ .whatsapp-setup {
1433
+ background: linear-gradient(135deg, #25D366 0%, #128C7E 100%);
1434
+ color: white;
1435
+ padding: 2rem;
1436
+ border-radius: 15px;
1437
+ margin: 2rem 0;
1438
+ text-align: center;
1439
+ box-shadow: 0 10px 25px rgba(37, 211, 102, 0.3);
1440
+ }
1441
+
1442
+ .whatsapp-steps {
1443
+ background: rgba(255, 255, 255, 0.1);
1444
+ backdrop-filter: blur(10px);
1445
+ border-radius: 10px;
1446
+ padding: 1.5rem;
1447
+ margin: 1rem 0;
1448
+ border: 1px solid rgba(255, 255, 255, 0.2);
1449
+ }
1450
+
1451
+ .phone-highlight {
1452
+ background: rgba(255, 255, 255, 0.2);
1453
+ padding: 0.5rem 1rem;
1454
+ border-radius: 8px;
1455
+ font-family: monospace;
1456
+ font-size: 1.1rem;
1457
+ font-weight: bold;
1458
+ display: inline-block;
1459
+ margin: 0.5rem 0;
1460
+ }
1461
+
1462
+ .code-highlight {
1463
+ background: rgba(255, 255, 255, 0.15);
1464
+ padding: 0.5rem 1rem;
1465
+ border-radius: 8px;
1466
+ font-family: monospace;
1467
+ font-size: 1rem;
1468
+ font-weight: bold;
1469
+ display: inline-block;
1470
+ margin: 0.5rem 0;
1471
+ border-left: 3px solid #fff;
1472
+ }
1473
+
1474
+ .dashboard-header {
1475
+ background: linear-gradient(135deg, #2d3748 0%, #4a5568 100%);
1476
+ color: white;
1477
+ padding: 2rem;
1478
+ border-radius: 15px;
1479
+ margin-bottom: 2rem;
1480
+ text-align: center;
1481
+ font-size: 1.5rem;
1482
+ }
1483
+
1484
+ .balance-card {
1485
+ background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
1486
+ color: white;
1487
+ padding: 2rem;
1488
+ border-radius: 15px;
1489
+ text-align: center;
1490
+ margin-bottom: 2rem;
1491
+ box-shadow: 0 10px 25px rgba(72, 187, 120, 0.3);
1492
+ }
1493
+
1494
+ .balance-amount {
1495
+ font-size: 2.5rem;
1496
+ font-weight: bold;
1497
+ margin: 1rem 0;
1498
+ }
1499
+
1500
+ .receipt-upload-area {
1501
+ border: 2px dashed #cbd5e0;
1502
+ border-radius: 15px;
1503
+ padding: 2rem;
1504
+ text-align: center;
1505
+ background: #f7fafc;
1506
+ transition: all 0.3s ease;
1507
+ }
1508
+
1509
+ .receipt-upload-area:hover {
1510
+ border-color: #4299e1;
1511
+ background: #ebf8ff;
1512
+ }
1513
+
1514
+ .receipt-preview {
1515
+ max-width: 100%;
1516
+ max-height: 400px;
1517
+ border-radius: 10px;
1518
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
1519
+ }
1520
+
1521
+ .low-confidence {
1522
+ background-color: #fff3cd !important;
1523
+ border: 1px solid #ffc107 !important;
1524
+ }
1525
+
1526
+ .high-confidence {
1527
+ background-color: #d4edda !important;
1528
+ border: 1px solid #28a745 !important;
1529
+ }
1530
+
1531
+ /* Button styling */
1532
+ .primary-btn {
1533
+ background: linear-gradient(45deg, #ff6b6b, #ee5a24) !important;
1534
+ border: none !important;
1535
+ border-radius: 25px !important;
1536
+ padding: 1rem 2rem !important;
1537
+ font-size: 1.1rem !important;
1538
+ font-weight: 600 !important;
1539
+ color: white !important;
1540
+ transition: all 0.3s ease !important;
1541
+ box-shadow: 0 4px 15px rgba(238, 90, 36, 0.4) !important;
1542
+ }
1543
+
1544
+ .secondary-btn {
1545
+ background: linear-gradient(45deg, #74b9ff, #0984e3) !important;
1546
+ border: none !important;
1547
+ border-radius: 25px !important;
1548
+ padding: 1rem 2rem !important;
1549
+ font-size: 1.1rem !important;
1550
+ font-weight: 600 !important;
1551
+ color: white !important;
1552
+ transition: all 0.3s ease !important;
1553
+ box-shadow: 0 4px 15px rgba(116, 185, 255, 0.4) !important;
1554
+ }
1555
+
1556
+ /* Tab styling */
1557
+ .tab-nav {
1558
+ background: #f8fafc;
1559
+ border-radius: 10px;
1560
+ padding: 0.5rem;
1561
+ margin-bottom: 2rem;
1562
+ }
1563
+
1564
+ /* Hide elements properly */
1565
+ .hide {
1566
+ display: none !important;
1567
+ }
1568
+
1569
+ /* Ensure proper spacing */
1570
+ .gradio-row {
1571
+ margin: 1rem 0;
1572
+ }
1573
+
1574
+ .gradio-column {
1575
+ padding: 0 1rem;
1576
+ }
1577
+
1578
+ /* Custom button hover effects */
1579
+ button:hover {
1580
+ transform: translateY(-2px);
1581
+ box-shadow: 0 6px 20px rgba(0,0,0,0.15);
1582
+ }
1583
+
1584
+ /* Status message styling */
1585
+ .status-success {
1586
+ color: #38a169;
1587
+ font-weight: 600;
1588
+ }
1589
+
1590
+ .status-error {
1591
+ color: #e53e3e;
1592
+ font-weight: 600;
1593
+ }
1594
+
1595
+ /* Table styling */
1596
+ .dataframe {
1597
+ border-radius: 10px;
1598
+ overflow: hidden;
1599
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
1600
+ }
1601
+ """
1602
+
1603
+ with gr.Blocks(title="FinGenius Pro", theme=gr.themes.Soft(), css=custom_css) as demo:
1604
+ # State to track current user
1605
+ current_user = gr.State()
1606
+ receipt_data = gr.State({})
1607
+
1608
+ # ===== LANDING PAGE =====
1609
+ with gr.Column(visible=True) as landing_page:
1610
+ gr.HTML("""
1611
+ <div class="landing-hero">
1612
+ <div class="hero-title">🏦 FinGenius Pro</div>
1613
+ <div class="hero-subtitle">Your Complete Personal Finance Manager with Smart AI Alerts</div>
1614
+
1615
+ <div class="features-grid">
1616
+ <div class="feature-card">
1617
+ <div class="feature-icon">πŸ’°</div>
1618
+ <h3>Smart Balance Tracking</h3>
1619
+ <p>Real-time balance monitoring with intelligent spending alerts</p>
1620
+ </div>
1621
+ <div class="feature-card">
1622
+ <div class="feature-icon">πŸ“±</div>
1623
+ <h3>WhatsApp Integration</h3>
1624
+ <p>Get instant notifications for every expense and budget alert</p>
1625
+ </div>
1626
+ <div class="feature-card">
1627
+ <div class="feature-icon">πŸ“Š</div>
1628
+ <h3>Advanced Analytics</h3>
1629
+ <p>Beautiful charts and insights to track your spending patterns</p>
1630
+ </div>
1631
+ <div class="feature-card">
1632
+ <div class="feature-icon">🧾</div>
1633
+ <h3>Receipt Scanning</h3>
1634
+ <p>AI-powered OCR to automatically extract expense data from receipts</p>
1635
+ </div>
1636
+ <div class="feature-card">
1637
+ <div class="feature-icon">πŸ‘ͺ</div>
1638
+ <h3>Family Finance</h3>
1639
+ <p>Create family groups to manage household finances together</p>
1640
+ </div>
1641
+ <div class="feature-card">
1642
+ <div class="feature-icon">πŸ”’</div>
1643
+ <h3>Secure & Private</h3>
1644
+ <p>Password-protected accounts with encrypted data storage</p>
1645
+ </div>
1646
+ </div>
1647
+ </div>
1648
+ """)
1649
+
1650
+ with gr.Row():
1651
+ with gr.Column(scale=1):
1652
+ signin_btn = gr.Button("πŸ”‘ Sign In", variant="primary", elem_classes="primary-btn", size="lg")
1653
+ with gr.Column(scale=1):
1654
+ signup_btn = gr.Button("✨ Create Account", variant="secondary", elem_classes="secondary-btn", size="lg")
1655
+
1656
+ # ===== SIGN IN PAGE =====
1657
+ with gr.Column(visible=False) as signin_page:
1658
+ with gr.Column(elem_classes="auth-container"):
1659
+ gr.HTML("<h2 style='text-align: center; color: #2d3748; margin-bottom: 2rem;'>πŸ”‘ Welcome Back</h2>")
1660
+
1661
+ signin_phone = gr.Textbox(
1662
+ label="πŸ“± WhatsApp Number",
1663
+ placeholder="+92XXXXXXXXXX",
1664
+ info="Enter your registered WhatsApp number"
1665
+ )
1666
+ signin_password = gr.Textbox(
1667
+ label="πŸ”’ Password",
1668
+ type="password",
1669
+ placeholder="Enter your secure password"
1670
+ )
1671
+
1672
+ with gr.Row():
1673
+ submit_signin = gr.Button("Sign In", variant="primary", elem_classes="primary-btn", scale=2)
1674
+ back_to_landing_1 = gr.Button("← Back", variant="secondary", scale=1)
1675
+
1676
+ signin_status = gr.Textbox(label="Status", interactive=False)
1677
+
1678
+ # ===== SIGN UP PAGE =====
1679
+ with gr.Column(visible=False) as signup_page:
1680
+ with gr.Column(elem_classes="auth-container"):
1681
+ gr.HTML("<h2 style='text-align: center; color: #2d3748; margin-bottom: 2rem;'>✨ Create Your Account</h2>")
1682
+
1683
+ signup_name = gr.Textbox(
1684
+ label="πŸ‘€ Full Name",
1685
+ placeholder="Enter your full name"
1686
+ )
1687
+ signup_phone = gr.Textbox(
1688
+ label="πŸ“± WhatsApp Number",
1689
+ placeholder="+92XXXXXXXXXX",
1690
+ info="This will be used for notifications"
1691
+ )
1692
+ signup_password = gr.Textbox(
1693
+ label="πŸ”’ Create Password",
1694
+ type="password",
1695
+ placeholder="Minimum 6 characters with letters and numbers"
1696
+ )
1697
+ signup_confirm_password = gr.Textbox(
1698
+ label="πŸ”’ Confirm Password",
1699
+ type="password",
1700
+ placeholder="Re-enter your password"
1701
+ )
1702
+
1703
+ # WhatsApp Setup Instructions
1704
+ gr.HTML("""
1705
+ <div class='whatsapp-setup'>
1706
+ <h3>πŸ“± Enable WhatsApp Alerts</h3>
1707
+ <p style='font-size: 1.1rem; margin-bottom: 1.5rem;'>To receive instant notifications for your financial activities, follow these steps:</p>
1708
+
1709
+ <div class='whatsapp-steps'>
1710
+ <h4>Step 1: Save the Bot Number</h4>
1711
+ <p>Add this Twilio WhatsApp Sandbox number to your contacts:</p>
1712
+ <div class='phone-highlight'>+1 (415) 523-8886</div>
1713
+ </div>
1714
+
1715
+ <div class='whatsapp-steps'>
1716
+ <h4>Step 2: Send Activation Code</h4>
1717
+ <p>Send this exact message to the number above:</p>
1718
+ <div class='code-highlight'>join catch-manner</div>
1719
+ <p style='font-size: 0.9rem; opacity: 0.8; margin-top: 0.5rem;'>
1720
+ ⚠️ <strong>Important:</strong> You must send this exact code to activate the sandbox.
1721
+ </p>
1722
+ </div>
1723
+
1724
+ <div class='whatsapp-steps'>
1725
+ <h4>Step 3: Confirm Registration</h4>
1726
+ <p>After sending the code, register your FinGenius account with the <strong>same phone number</strong> you used to message the bot.</p>
1727
+ </div>
1728
+
1729
+ <div class='whatsapp-steps'>
1730
+ <h4>Step 4: Start Receiving Alerts</h4>
1731
+ <p>You'll receive instant WhatsApp notifications for:</p>
1732
+ <ul style='text-align: left; margin-left: 1rem; opacity: 0.9;'>
1733
+ <li>βœ… Account registration confirmation</li>
1734
+ <li>πŸ’° Balance updates</li>
1735
+ <li>πŸ’Έ Expense notifications</li>
1736
+ <li>🧾 Receipt processing confirmations</li>
1737
+ <li>πŸ“ˆ Investment tracking</li>
1738
+ <li>🚨 Budget alerts</li>
1739
+ </ul>
1740
+ </div>
1741
+ </div>
1742
+ """)
1743
+
1744
+ with gr.Row():
1745
+ submit_signup = gr.Button("Complete Registration", variant="primary", elem_classes="primary-btn", scale=2)
1746
+ back_to_landing_2 = gr.Button("← Back", variant="secondary", scale=1)
1747
+
1748
+ signup_status = gr.Textbox(label="Status", interactive=False)
1749
+
1750
+ # ===== DASHBOARD PAGE =====
1751
+ with gr.Column(visible=False) as dashboard_page:
1752
+ # Dashboard Header
1753
+ welcome_message = gr.HTML("", elem_classes="dashboard-header")
1754
+
1755
+ # Current Balance Display
1756
+ with gr.Column(elem_classes="balance-card"):
1757
+ balance_display = gr.HTML("<div class='balance-amount'>πŸ’° 0 PKR</div>")
1758
+
1759
+ with gr.Row():
1760
+ with gr.Column(scale=2):
1761
+ balance_amount = gr.Number(label="πŸ’° Add to Balance (PKR)", minimum=1, step=100, value=0)
1762
+ balance_description = gr.Textbox(label="Description", placeholder="Salary, gift, bonus, etc.")
1763
+ with gr.Column(scale=1):
1764
+ add_balance_btn = gr.Button("Add Balance", variant="primary", elem_classes="primary-btn")
1765
+
1766
+ balance_status = gr.Textbox(label="Balance Status", interactive=False)
1767
+
1768
+ with gr.Tabs(elem_classes="tab-nav"):
1769
+ # Dashboard Overview Tab
1770
+ with gr.Tab("πŸ“Š Dashboard Overview"):
1771
+ gr.HTML("""
1772
+ <div style="text-align: center; padding: 3rem; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border-radius: 15px; color: white; margin: 2rem 0;">
1773
+ <h2>πŸŽ‰ Welcome to FinGenius Pro!</h2>
1774
+ <p style="font-size: 1.2rem; opacity: 0.9;">Your personal finance management just got smarter. Start by adding some balance and setting up your budget allocations.</p>
1775
+ </div>
1776
+ """)
1777
+
1778
+ with gr.Row():
1779
+ gr.HTML("""
1780
+ <div style="background: #e6fffa; padding: 2rem; border-radius: 15px; border-left: 4px solid #38b2ac;">
1781
+ <h3>πŸš€ Quick Start Guide:</h3>
1782
+ <ol style="text-align: left; margin-left: 1rem;">
1783
+ <li><strong>Add Balance:</strong> Use the balance card above to add your initial funds</li>
1784
+ <li><strong>Set Income & Goals:</strong> Go to Income & Goals tab to set your monthly income and savings target</li>
1785
+ <li><strong>Plan Budget:</strong> Use Budget Planner to allocate money to different expense categories</li>
1786
+ <li><strong>Track Expenses:</strong> Log your daily expenses in the Expense Tracker</li>
1787
+ <li><strong>Scan Receipts:</strong> Use Receipt Scan to automatically extract expense data from photos</li>
1788
+ <li><strong>Monitor Investments:</strong> Keep track of your investment portfolio</li>
1789
+ </ol>
1790
+ </div>
1791
+ """)
1792
+
1793
+ # Income & Goals Tab
1794
+ with gr.Tab("πŸ“₯ Income & Goals"):
1795
+ gr.HTML("<h3>πŸ’΅ Set Your Financial Goals</h3>")
1796
+
1797
+ with gr.Row():
1798
+ income = gr.Number(label="πŸ’΅ Monthly Income (PKR)", minimum=0, step=1000, value=0)
1799
+ savings_goal = gr.Number(label="🎯 Savings Goal (PKR)", minimum=0, step=1000, value=0)
1800
+
1801
+ update_btn = gr.Button("πŸ’Ύ Update Financial Info", variant="primary", elem_classes="primary-btn")
1802
+ income_status = gr.Textbox(label="Status", interactive=False)
1803
+
1804
+ # Budget Planner Tab
1805
+ with gr.Tab("πŸ“Š Budget Planner"):
1806
+ gr.HTML("<h3>πŸ’Ό Allocate Your Monthly Budget</h3>")
1807
+
1808
+ with gr.Column():
1809
+ allocation_inputs = []
1810
+ with gr.Row():
1811
+ for i, category in enumerate(EXPENSE_CATEGORIES[:7]):
1812
+ alloc = gr.Number(label=f"🏷️ {category}", minimum=0, step=100, value=0)
1813
+ allocation_inputs.append(alloc)
1814
+ with gr.Row():
1815
+ for i, category in enumerate(EXPENSE_CATEGORIES[7:]):
1816
+ alloc = gr.Number(label=f"🏷️ {category}", minimum=0, step=100, value=0)
1817
+ allocation_inputs.append(alloc)
1818
+
1819
+ allocate_btn = gr.Button("πŸ’Ύ Save Budget Allocations", variant="primary", elem_classes="primary-btn", size="lg")
1820
+ allocation_status = gr.Textbox(label="Status", interactive=False)
1821
+
1822
+ gr.HTML("<h4>πŸ“Š Current Budget Allocations</h4>")
1823
+ expense_table = gr.Dataframe(
1824
+ headers=["Category", "Allocated", "Spent", "Remaining", "Date"],
1825
+ interactive=False,
1826
+ wrap=True
1827
+ )
1828
+
1829
+ # Receipt Scan Tab - NEW!
1830
+ with gr.Tab("πŸ“· Receipt Scan"):
1831
+ gr.HTML("""
1832
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 2rem; border-radius: 15px; margin-bottom: 2rem; text-align: center;">
1833
+ <h2>🧾 AI-Powered Receipt Scanner</h2>
1834
+ <p style="font-size: 1.1rem; opacity: 0.9;">Upload receipt photos and let AI extract expense data automatically!</p>
1835
+ </div>
1836
+ """)
1837
+
1838
+ with gr.Row():
1839
+ with gr.Column(scale=1):
1840
+ gr.HTML("<h4>πŸ“€ Upload Receipt</h4>")
1841
+
1842
+ receipt_image = gr.File(
1843
+ label="πŸ“· Receipt Image",
1844
+ file_types=["image"],
1845
+ elem_classes="receipt-upload-area"
1846
+ )
1847
+
1848
+ process_receipt_btn = gr.Button(
1849
+ "πŸ” Process Receipt",
1850
+ variant="primary",
1851
+ elem_classes="primary-btn",
1852
+ size="lg"
1853
+ )
1854
+
1855
+ receipt_status = gr.Textbox(label="Processing Status", interactive=False)
1856
+
1857
+ # Image Preview
1858
+ gr.HTML("<h4>πŸ“Έ Receipt Preview</h4>")
1859
+ receipt_preview = gr.Image(
1860
+ label="Receipt Preview",
1861
+ type="pil",
1862
+ elem_classes="receipt-preview"
1863
+ )
1864
+
1865
+ with gr.Column(scale=1):
1866
+ gr.HTML("<h4>✏️ Verify & Edit Extracted Data</h4>")
1867
+
1868
+ extracted_merchant = gr.Textbox(
1869
+ label="πŸͺ Merchant Name",
1870
+ placeholder="Store/Restaurant name",
1871
+ info="Edit if incorrectly detected"
1872
+ )
1873
+
1874
+ with gr.Row():
1875
+ extracted_amount = gr.Number(
1876
+ label="πŸ’° Total Amount (PKR)",
1877
+ minimum=0,
1878
+ step=0.01,
1879
+ value=0
1880
+ )
1881
+ extracted_date = gr.Textbox(
1882
+ label="πŸ“… Date",
1883
+ placeholder="YYYY-MM-DD or DD/MM/YYYY"
1884
+ )
1885
+
1886
+ extracted_category = gr.Dropdown(
1887
+ choices=EXPENSE_CATEGORIES,
1888
+ label="🏷️ Category",
1889
+ value="Miscellaneous",
1890
+ info="AI-suggested category (you can change it)"
1891
+ )
1892
+
1893
+ gr.HTML("<h4>πŸ“ Line Items (Optional)</h4>")
1894
+ line_items_table = gr.Dataframe(
1895
+ headers=["Item", "Price"],
1896
+ datatype=["str", "number"],
1897
+ row_count=5,
1898
+ col_count=2,
1899
+ interactive=True,
1900
+ label="Receipt Items"
1901
+ )
1902
+
1903
+ save_receipt_btn = gr.Button(
1904
+ "πŸ’Ύ Save as Expense",
1905
+ variant="primary",
1906
+ elem_classes="primary-btn",
1907
+ size="lg"
1908
+ )
1909
+
1910
+ # Receipt History
1911
+ gr.HTML("<h4>🧾 Recent Receipts</h4>")
1912
+ receipts_table = gr.Dataframe(
1913
+ headers=["Receipt ID", "Merchant", "Amount", "Date", "Category", "Confidence", "Status", "Processed"],
1914
+ interactive=False,
1915
+ wrap=True
1916
+ )
1917
+
1918
+ # Expense Tracker Tab
1919
+ with gr.Tab("πŸ’Έ Expense Tracker"):
1920
+ with gr.Row():
1921
+ with gr.Column():
1922
+ gr.HTML("<h4>βž• Log New Expense</h4>")
1923
+ expense_category = gr.Dropdown(choices=EXPENSE_CATEGORIES, label="🏷️ Category")
1924
+ expense_amount = gr.Number(label="πŸ’° Amount (PKR)", minimum=1, step=100, value=0)
1925
+ expense_description = gr.Textbox(label="πŸ“ Description", placeholder="What did you buy?")
1926
+
1927
+ with gr.Accordion("πŸ”„ Recurring Expense Settings", open=False):
1928
+ is_recurring = gr.Checkbox(label="This is a recurring expense")
1929
+ recurrence_pattern = gr.Dropdown(choices=RECURRENCE_PATTERNS, label="Frequency")
1930
+
1931
+ record_expense_btn = gr.Button("πŸ’Έ Record Expense", variant="primary", elem_classes="primary-btn", size="lg")
1932
+ expense_status = gr.Textbox(label="Status", interactive=False)
1933
+
1934
+ with gr.Column():
1935
+ gr.HTML("<h4>πŸ“ˆ Spending Analytics</h4>")
1936
+ spending_chart = gr.Plot(label="πŸ“Š Spending Analysis")
1937
+ balance_chart = gr.Plot(label="πŸ’° Balance Trend")
1938
+
1939
+ with gr.Row():
1940
+ months_history = gr.Slider(1, 12, value=3, step=1, label="πŸ“… Months History")
1941
+ update_charts_btn = gr.Button("πŸ”„ Update Analytics", variant="secondary")
1942
+
1943
+ # Spending History Tab
1944
+ with gr.Tab("πŸ“ Spending History"):
1945
+ gr.HTML("<h3>πŸ’³ Recent Transaction History</h3>")
1946
+ spending_log_table = gr.Dataframe(
1947
+ headers=["Category", "Amount", "Description", "Date", "Balance After"],
1948
+ interactive=False,
1949
+ wrap=True
1950
+ )
1951
+
1952
+ # Investment Portfolio Tab
1953
+ with gr.Tab("πŸ“ˆ Investment Portfolio"):
1954
+ with gr.Row():
1955
+ with gr.Column():
1956
+ gr.HTML("<h4>βž• Add New Investment</h4>")
1957
+ investment_type = gr.Dropdown(choices=INVESTMENT_TYPES, label="🏒 Investment Type")
1958
+ investment_name = gr.Textbox(label="πŸ“ Name/Description")
1959
+ investment_amount = gr.Number(label="πŸ’° Amount (PKR)", minimum=1, step=1000, value=0)
1960
+ investment_notes = gr.Textbox(label="πŸ“‹ Notes", lines=2, placeholder="Additional details...")
1961
+ add_investment_btn = gr.Button("πŸ“ˆ Add Investment", variant="primary", elem_classes="primary-btn")
1962
+ investment_status = gr.Textbox(label="Status", interactive=False)
1963
+
1964
+ with gr.Column():
1965
+ gr.HTML("<h4>πŸ’Ό Your Investment Portfolio</h4>")
1966
+ investments_table = gr.Dataframe(
1967
+ headers=["Type", "Name", "Amount", "Date", "Notes"],
1968
+ interactive=False,
1969
+ wrap=True
1970
+ )
1971
+
1972
+ # Family Finance Tab
1973
+ with gr.Tab("πŸ‘ͺ Family Finance"):
1974
+ gr.HTML("<h3>πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ Family Financial Management</h3>")
1975
+ family_info = gr.Textbox(label="πŸ‘₯ Current Family Group", interactive=False)
1976
+
1977
+ with gr.Row():
1978
+ with gr.Column():
1979
+ gr.HTML("<h4>βž• Create New Family Group</h4>")
1980
+ create_group_name = gr.Textbox(label="πŸ‘ͺ Group Name", placeholder="Smith Family Budget")
1981
+ create_group_btn = gr.Button("Create Family Group", variant="primary", elem_classes="primary-btn")
1982
+
1983
+ with gr.Column():
1984
+ gr.HTML("<h4>πŸ”— Join Existing Group</h4>")
1985
+ join_group_id = gr.Textbox(label="πŸ†” Group ID", placeholder="FG-XXXX-XXXXXXXX")
1986
+ join_group_btn = gr.Button("Join Family Group", variant="secondary", elem_classes="secondary-btn")
1987
+
1988
+ family_status = gr.Textbox(label="Status", interactive=False)
1989
+
1990
+ gr.HTML("<h4>πŸ‘₯ Family Members</h4>")
1991
+ family_members = gr.Dataframe(
1992
+ headers=["Phone", "Name"],
1993
+ interactive=False,
1994
+ wrap=True
1995
+ )
1996
+
1997
+ with gr.Row():
1998
+ with gr.Column(scale=3):
1999
+ pass
2000
+ with gr.Column(scale=1):
2001
+ sign_out_btn = gr.Button("πŸšͺ Sign Out", variant="stop", elem_classes="secondary-btn", size="lg")
2002
+
2003
+ # ===== EVENT HANDLERS =====
2004
+
2005
+ # Navigation
2006
+ signin_btn.click(
2007
+ show_signin,
2008
+ outputs=[landing_page, signin_page, signup_page, dashboard_page, signin_phone, signin_password]
2009
+ )
2010
+
2011
+ signup_btn.click(
2012
+ show_signup,
2013
+ outputs=[landing_page, signin_page, signup_page, dashboard_page, signup_name, signup_phone, signup_password, signup_confirm_password]
2014
+ )
2015
+
2016
+ back_to_landing_1.click(
2017
+ return_to_landing,
2018
+ outputs=[landing_page, signin_page, signup_page, dashboard_page, welcome_message, balance_display]
2019
+ )
2020
+
2021
+ back_to_landing_2.click(
2022
+ return_to_landing,
2023
+ outputs=[landing_page, signin_page, signup_page, dashboard_page, welcome_message, balance_display]
2024
+ )
2025
+
2026
+ sign_out_btn.click(
2027
+ return_to_landing,
2028
+ outputs=[landing_page, signin_page, signup_page, dashboard_page, welcome_message, balance_display]
2029
+ )
2030
+
2031
+ # Authentication
2032
+ def handle_signin(phone, password):
2033
+ status = authenticate_user(phone, password)
2034
+ if "βœ…" in status:
2035
+ user_name = status.split("as ")[1]
2036
+ current_user.value = phone
2037
+ pages = show_dashboard(phone, user_name)
2038
+ return [status] + pages
2039
+ else:
2040
+ empty_alloc = [0] * len(EXPENSE_CATEGORIES)
2041
+ return [status, gr.update(), gr.update(), gr.update(), gr.update(), "", "<div class='balance-amount'>πŸ’° 0 PKR</div>", 0, 0] + empty_alloc + [[], [], [], None, None, "No family group", [], []]
2042
+
2043
+ submit_signin.click(
2044
+ handle_signin,
2045
+ inputs=[signin_phone, signin_password],
2046
+ outputs=[signin_status, landing_page, signin_page, signup_page, dashboard_page, welcome_message, balance_display, income, savings_goal] + allocation_inputs + [expense_table, investments_table, spending_log_table, spending_chart, balance_chart, family_info, family_members, receipts_table]
2047
+ )
2048
+
2049
+ def handle_signup(name, phone, password, confirm_password):
2050
+ status = register_user(name, phone, password, confirm_password)
2051
+ return status
2052
+
2053
+ submit_signup.click(
2054
+ handle_signup,
2055
+ inputs=[signup_name, signup_phone, signup_password, signup_confirm_password],
2056
+ outputs=[signup_status]
2057
+ )
2058
+
2059
+ # Balance Management
2060
+ def handle_add_balance(amount_val, description):
2061
+ if current_user.value:
2062
+ status, balance_html = add_balance(current_user.value, amount_val, description)
2063
+ return status, balance_html
2064
+ else:
2065
+ return "❌ Please sign in first", "<div class='balance-amount'>πŸ’° 0 PKR</div>"
2066
+
2067
+ add_balance_btn.click(
2068
+ handle_add_balance,
2069
+ inputs=[balance_amount, balance_description],
2070
+ outputs=[balance_status, balance_display]
2071
+ )
2072
+
2073
+ # Financial Operations
2074
+ def handle_update_financials(income_val, savings_val):
2075
+ if current_user.value:
2076
+ return update_financials(current_user.value, income_val, savings_val)
2077
+ else:
2078
+ return "❌ Please sign in first"
2079
+
2080
+ update_btn.click(
2081
+ handle_update_financials,
2082
+ inputs=[income, savings_goal],
2083
+ outputs=[income_status]
2084
+ )
2085
+
2086
+ def handle_save_allocations(*allocations):
2087
+ if current_user.value:
2088
+ return save_allocations(current_user.value, *allocations)
2089
+ else:
2090
+ return "❌ Please sign in first", []
2091
+
2092
+ allocate_btn.click(
2093
+ handle_save_allocations,
2094
+ inputs=allocation_inputs,
2095
+ outputs=[allocation_status, expense_table]
2096
+ )
2097
+
2098
+ def handle_record_expense(category, amount, description, is_recurring, recurrence_pattern):
2099
+ if current_user.value:
2100
+ return record_expense(current_user.value, category, amount, description, is_recurring, recurrence_pattern)
2101
+ else:
2102
+ return "❌ Please sign in first", "<div class='balance-amount'>πŸ’° 0 PKR</div>", [], []
2103
+
2104
+ record_expense_btn.click(
2105
+ handle_record_expense,
2106
+ inputs=[expense_category, expense_amount, expense_description, is_recurring, recurrence_pattern],
2107
+ outputs=[expense_status, balance_display, expense_table, spending_log_table]
2108
+ )
2109
+
2110
+ def handle_add_investment(inv_type, name, amount, notes):
2111
+ if current_user.value:
2112
+ return add_investment(current_user.value, inv_type, name, amount, notes)
2113
+ else:
2114
+ return "❌ Please sign in first", "<div class='balance-amount'>πŸ’° 0 PKR</div>", []
2115
+
2116
+ add_investment_btn.click(
2117
+ handle_add_investment,
2118
+ inputs=[investment_type, investment_name, investment_amount, investment_notes],
2119
+ outputs=[investment_status, balance_display, investments_table]
2120
+ )
2121
+
2122
+ def handle_create_family_group(group_name):
2123
+ if current_user.value:
2124
+ return create_family_group(current_user.value, group_name)
2125
+ else:
2126
+ return "❌ Please sign in first", "", []
2127
+
2128
+ create_group_btn.click(
2129
+ handle_create_family_group,
2130
+ inputs=[create_group_name],
2131
+ outputs=[family_status, family_info, family_members]
2132
+ )
2133
+
2134
+ def handle_join_family_group(group_id):
2135
+ if current_user.value:
2136
+ return join_family_group(current_user.value, group_id)
2137
+ else:
2138
+ return "❌ Please sign in first", "", []
2139
+
2140
+ join_group_btn.click(
2141
+ handle_join_family_group,
2142
+ inputs=[join_group_id],
2143
+ outputs=[family_status, family_info, family_members]
2144
+ )
2145
+
2146
+ def handle_update_charts(months_history):
2147
+ if current_user.value:
2148
+ return generate_spending_chart(current_user.value, months_history), generate_balance_chart(current_user.value)
2149
+ else:
2150
+ return None, None
2151
+
2152
+ update_charts_btn.click(
2153
+ handle_update_charts,
2154
+ inputs=[months_history],
2155
+ outputs=[spending_chart, balance_chart]
2156
+ )
2157
+
2158
+ # Receipt Processing Event Handlers - NEW!
2159
+ process_receipt_btn.click(
2160
+ handle_receipt_upload,
2161
+ inputs=[receipt_image, current_user],
2162
+ outputs=[receipt_status, receipt_data, extracted_merchant, extracted_amount, extracted_date, line_items_table, receipt_preview, extracted_category]
2163
+ )
2164
+
2165
+ save_receipt_btn.click(
2166
+ handle_receipt_save,
2167
+ inputs=[current_user, receipt_data, extracted_merchant, extracted_amount, extracted_date, extracted_category, line_items_table],
2168
+ outputs=[receipt_status, balance_display, expense_table, spending_log_table]
2169
+ )
2170
+
2171
+ if __name__ == "__main__":
2172
+ demo.launch(
2173
+ server_name="0.0.0.0",
2174
+ server_port=7860,
2175
+ share=False,
2176
+ show_error=True
2177
+ )