Spaces:
Runtime error
Runtime error
Create app.py
Browse files
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 |
+
)
|