From ead0d5182efe21899bdaccc74ead281805128bf9 Mon Sep 17 00:00:00 2001 From: lokeshramchand-ctrl Date: Tue, 19 Aug 2025 22:16:35 +0530 Subject: [PATCH] OHO-10(Partial Email) --- Backend/index.js | 115 +++++++++++++++++++++++++++++---- Frontend/lib/emai_screen.dart | 117 ++++++++++++++++++++++++++++++++++ Frontend/lib/login.dart | 52 +++++++-------- 3 files changed, 245 insertions(+), 39 deletions(-) create mode 100644 Frontend/lib/emai_screen.dart diff --git a/Backend/index.js b/Backend/index.js index 57cdb85..2d95399 100644 --- a/Backend/index.js +++ b/Backend/index.js @@ -255,7 +255,6 @@ function parseBankMessage(snippet) { vendor: null, type: null, date: null, - raw: snippet, }; // Amount (handles Rs/INR) @@ -268,14 +267,24 @@ function parseBankMessage(snippet) { else if (/credited/i.test(snippet)) result.type = "credit"; // Vendor (look for "to" or "at") + // ✅ Extract vendor name cleanly (ignore VPAs and emails) const vendorMatch = snippet.match(/to\s+([A-Za-z0-9@.\s&-]+)/i); if (vendorMatch) { - result.vendor = vendorMatch[1].trim(); + let vendorRaw = vendorMatch[1].trim(); + // Remove email-like or VPA patterns (e.g., xxxx@xxxx) + vendorRaw = vendorRaw.replace(/[a-z0-9._%+-]+@[a-z0-9.-]+/gi, '').trim(); + // Collapse multiple spaces + vendorRaw = vendorRaw.replace(/\s{2,}/g, ' '); + result.vendor = vendorRaw; } else { const atMatch = snippet.match(/at\s+([A-Za-z0-9\s&.-]+)/i); - if (atMatch) result.vendor = atMatch[1].trim(); + if (atMatch) { + let vendorRaw = atMatch[1].trim(); + vendorRaw = vendorRaw.replace(/[a-z0-9._%+-]+@[a-z0-9.-]+/gi, '').trim(); + vendorRaw = vendorRaw.replace(/\s{2,}/g, ' '); + result.vendor = vendorRaw; + } } - // Date (dd-mm-yy or dd/mm/yyyy) const dateMatch = snippet.match(/on\s+(\d{2}[-/]\d{2}[-/]\d{2,4})/); if (dateMatch) result.date = dateMatch[1]; @@ -321,28 +330,106 @@ async function fetchBankEmails(accessToken, bankEmails) { // ---------- ROUTE ---------- app.post('/api/sync-gmail', async (req, res) => { try { - const { accessToken } = req.body; + const { accessToken, userId } = req.body; if (!accessToken) { + console.error('❌ Missing access token in request body'); return res.status(400).json({ error: "Missing access token" }); } + if (!userId) { + console.error('❌ Missing userId in request body'); + return res.status(400).json({ error: "Missing userId" }); + } const bankEmails = bankRules.map(b => b.email); - const emails = await fetchBankEmails(accessToken, bankEmails); - // 🔥 parse emails into structured transactions - const parsedEmails = emails.map(email => ({ - from: email.from, - ...parseBankMessage(email.snippet), - })); + let emails; + try { + emails = await fetchBankEmails(accessToken, bankEmails); + } catch (fetchError) { + console.error('❌ Error fetching bank emails:', fetchError); + return res.status(500).json({ error: 'Failed to fetch bank emails' }); + } + + if (!emails || emails.length === 0) { + console.warn('⚠️ No bank emails found for provided email accounts'); + return res.json({ success: true, count: 0, transactions: [] }); + } + + const savedTxns = []; + + for (const email of emails) { + let parsed; + try { + parsed = parseBankMessage(email.snippet); + } catch (parseError) { + console.error('❌ Error parsing email snippet:', parseError, 'Snippet:', email.snippet); + continue; // Skip this email and move on + } + + if (!parsed.amount) { + console.warn('⚠️ Parsed email missing amount, skipping:', parsed); + continue; + } + + let category = 'Other'; + try { + const predictRes = await axios.post('http://192.168.1.10:5000/api/predict', { + description: parsed.vendor || "Unknown" + }, { timeout: 5000 }); // Optional timeout + + if (predictRes.data && typeof predictRes.data.category === 'string' && predictRes.data.category.trim() !== '') { + category = predictRes.data.category; + } else { + console.warn('⚠️ Prediction API returned invalid category for vendor:', parsed.vendor); + } + } catch (predictError) { + console.error('❌ Prediction API error:', predictError.message); + } + + const parseDateString = (dateStr) => { + if (!dateStr) return null; + const [day, month, yearSuffix] = dateStr.split('-'); + const year = 2000 + parseInt(yearSuffix, 10); + return new Date(year, parseInt(month, 10) - 1, parseInt(day, 10)); +}; + +const dateValue = parseDateString(parsed.date) || new Date(); + +const newTxn = new Transaction({ + userId, + description: parsed.vendor || "Unknown Vendor", + amount: parsed.amount, + type: parsed.type || "unknown", + date: dateValue, + vendor: parsed.vendor, + category, + source: "email", + bank: email.from, +}); + + + try { + await newTxn.save(); + savedTxns.push(newTxn); + } catch (saveError) { + console.error('❌ Error saving transaction to database:', saveError, 'Transaction:', newTxn); + } + } + + return res.json({ + success: true, + count: savedTxns.length, + transactions: savedTxns, + }); - res.json({ success: true, emails: parsedEmails }); } catch (error) { - console.error('❌ Gmail fetch error:', error); - res.status(500).json({ error: 'Failed to fetch Gmail messages' }); + console.error('❌ Unexpected error in /api/sync-gmail:', error); + return res.status(500).json({ error: 'Failed to fetch and save Gmail messages' }); } }); + /* ---------- SERVER ---------- */ mongoose.connect(process.env.MONGO_URI, { useNewUrlParser: true, diff --git a/Frontend/lib/emai_screen.dart b/Frontend/lib/emai_screen.dart new file mode 100644 index 0000000..c4ae589 --- /dev/null +++ b/Frontend/lib/emai_screen.dart @@ -0,0 +1,117 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; + +class AutoSyncEmailsScreen extends StatefulWidget { + final String accessToken; + final String userId; // userId now required by backend + + const AutoSyncEmailsScreen({ + super.key, + required this.accessToken, + required this.userId, + }); + + @override + State createState() => _AutoSyncEmailsScreenState(); +} + +class _AutoSyncEmailsScreenState extends State { + bool _loading = false; + String? _status; + int _syncedCount = 0; + + @override + void initState() { + super.initState(); + _autoSyncEmails(); + } + + Future _autoSyncEmails() async { + setState(() { + _loading = true; + _status = "Syncing your bank emails..."; + _syncedCount = 0; + }); + + try { + final response = await http.post( + Uri.parse("http://192.168.1.10:3000/api/sync-gmail"), + headers: {"Content-Type": "application/json"}, + body: jsonEncode({ + "accessToken": widget.accessToken, + "userId": widget.userId, + }), + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + + if (data['success'] == true && data['count'] != null) { + setState(() { + _loading = false; + _syncedCount = data['count']; + _status = "✅ Synced $_syncedCount transactions successfully!"; + }); + + // Optional: navigate back or to another screen after a delay + Future.delayed(const Duration(seconds: 2), () { + Navigator.pop(context, true); + }); + } else { + setState(() { + _loading = false; + _status = "❌ Sync failed: Unexpected response format"; + }); + } + } else { + setState(() { + _loading = false; + _status = "❌ Backend error: ${response.body}"; + }); + } + } catch (error) { + setState(() { + _loading = false; + _status = "⚠️ Network or unexpected error: $error"; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("Auto Sync Emails")), + body: Center( + child: + _loading + ? Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 20), + Text(_status ?? "Processing..."), + ], + ) + : Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + _status ?? "Done", + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16), + ), + const SizedBox(height: 20), + ElevatedButton.icon( + icon: const Icon(Icons.refresh), + label: const Text("Retry"), + onPressed: _autoSyncEmails, + ), + ], + ), + ), + ); + } +} diff --git a/Frontend/lib/login.dart b/Frontend/lib/login.dart index 4bb814b..527d7a7 100644 --- a/Frontend/lib/login.dart +++ b/Frontend/lib/login.dart @@ -115,7 +115,8 @@ import 'package:flutter/material.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:http/http.dart' as http; -import 'package:monarch/email.dart'; +import 'package:monarch/emai_screen.dart'; +//import 'package:monarch/email.dart'; import 'package:monarch/other_pages/enviroment.dart'; import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; @@ -133,7 +134,7 @@ class _LoginScreenState extends State { 'email', 'profile', 'openid', - 'https://www.googleapis.com/auth/gmail.readonly' + 'https://www.googleapis.com/auth/gmail.readonly', ], serverClientId: Environment.serverClientId, ); @@ -180,10 +181,11 @@ class _LoginScreenState extends State { Navigator.pushReplacement( context, MaterialPageRoute( - builder: (_) => EmailsScreen( - accessToken: accessToken, - userId: data['user']['_id'], - ), + // builder: (_) => EmailsScreen( + // accessToken: accessToken, + // userId: data['user']['_id'], + // ), + builder: (_) => AutoSyncEmailsScreen(accessToken: accessToken,userId: data['user']['_id'],), ), ); } else { @@ -204,29 +206,29 @@ class _LoginScreenState extends State { return Scaffold( backgroundColor: Colors.white, body: Center( - child: _isLoading - ? const CircularProgressIndicator() - : ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 20, + child: + _isLoading + ? const CircularProgressIndicator() + : ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 20, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + side: const BorderSide(color: Colors.grey), + ), ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), - side: const BorderSide(color: Colors.grey), + label: const Text( + 'Sign in with Google', + style: TextStyle(fontSize: 16), ), + onPressed: _handleGoogleSignIn, ), - label: const Text( - 'Sign in with Google', - style: TextStyle(fontSize: 16), - ), - onPressed: _handleGoogleSignIn, - ), ), ); } } -