OHO-10(Partial Email)

This commit is contained in:
lokeshramchand-ctrl 2025-08-19 22:16:35 +05:30
parent f1397272e5
commit ead0d5182e
3 changed files with 245 additions and 39 deletions

View file

@ -255,7 +255,6 @@ function parseBankMessage(snippet) {
vendor: null, vendor: null,
type: null, type: null,
date: null, date: null,
raw: snippet,
}; };
// Amount (handles Rs/INR) // Amount (handles Rs/INR)
@ -268,14 +267,24 @@ function parseBankMessage(snippet) {
else if (/credited/i.test(snippet)) result.type = "credit"; else if (/credited/i.test(snippet)) result.type = "credit";
// Vendor (look for "to" or "at") // 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); const vendorMatch = snippet.match(/to\s+([A-Za-z0-9@.\s&-]+)/i);
if (vendorMatch) { 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 { } else {
const atMatch = snippet.match(/at\s+([A-Za-z0-9\s&.-]+)/i); 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) // Date (dd-mm-yy or dd/mm/yyyy)
const dateMatch = snippet.match(/on\s+(\d{2}[-/]\d{2}[-/]\d{2,4})/); const dateMatch = snippet.match(/on\s+(\d{2}[-/]\d{2}[-/]\d{2,4})/);
if (dateMatch) result.date = dateMatch[1]; if (dateMatch) result.date = dateMatch[1];
@ -321,28 +330,106 @@ async function fetchBankEmails(accessToken, bankEmails) {
// ---------- ROUTE ---------- // ---------- ROUTE ----------
app.post('/api/sync-gmail', async (req, res) => { app.post('/api/sync-gmail', async (req, res) => {
try { try {
const { accessToken } = req.body; const { accessToken, userId } = req.body;
if (!accessToken) { if (!accessToken) {
console.error('❌ Missing access token in request body');
return res.status(400).json({ error: "Missing access token" }); 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 bankEmails = bankRules.map(b => b.email);
const emails = await fetchBankEmails(accessToken, bankEmails);
// 🔥 parse emails into structured transactions let emails;
const parsedEmails = emails.map(email => ({ try {
from: email.from, emails = await fetchBankEmails(accessToken, bankEmails);
...parseBankMessage(email.snippet), } 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) { } catch (error) {
console.error('❌ Gmail fetch error:', error); console.error('❌ Unexpected error in /api/sync-gmail:', error);
res.status(500).json({ error: 'Failed to fetch Gmail messages' }); return res.status(500).json({ error: 'Failed to fetch and save Gmail messages' });
} }
}); });
/* ---------- SERVER ---------- */ /* ---------- SERVER ---------- */
mongoose.connect(process.env.MONGO_URI, { mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true, useNewUrlParser: true,

View file

@ -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<AutoSyncEmailsScreen> createState() => _AutoSyncEmailsScreenState();
}
class _AutoSyncEmailsScreenState extends State<AutoSyncEmailsScreen> {
bool _loading = false;
String? _status;
int _syncedCount = 0;
@override
void initState() {
super.initState();
_autoSyncEmails();
}
Future<void> _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,
),
],
),
),
);
}
}

View file

@ -115,7 +115,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart'; import 'package:google_sign_in/google_sign_in.dart';
import 'package:http/http.dart' as http; 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 'package:monarch/other_pages/enviroment.dart';
import 'dart:convert'; import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -133,7 +134,7 @@ class _LoginScreenState extends State<LoginScreen> {
'email', 'email',
'profile', 'profile',
'openid', 'openid',
'https://www.googleapis.com/auth/gmail.readonly' 'https://www.googleapis.com/auth/gmail.readonly',
], ],
serverClientId: Environment.serverClientId, serverClientId: Environment.serverClientId,
); );
@ -180,10 +181,11 @@ class _LoginScreenState extends State<LoginScreen> {
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => EmailsScreen( // builder: (_) => EmailsScreen(
accessToken: accessToken, // accessToken: accessToken,
userId: data['user']['_id'], // userId: data['user']['_id'],
), // ),
builder: (_) => AutoSyncEmailsScreen(accessToken: accessToken,userId: data['user']['_id'],),
), ),
); );
} else { } else {
@ -204,29 +206,29 @@ class _LoginScreenState extends State<LoginScreen> {
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
body: Center( body: Center(
child: _isLoading child:
? const CircularProgressIndicator() _isLoading
: ElevatedButton.icon( ? const CircularProgressIndicator()
style: ElevatedButton.styleFrom( : ElevatedButton.icon(
backgroundColor: Colors.white, style: ElevatedButton.styleFrom(
foregroundColor: Colors.black, backgroundColor: Colors.white,
padding: const EdgeInsets.symmetric( foregroundColor: Colors.black,
vertical: 12, padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 12,
horizontal: 20,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
side: const BorderSide(color: Colors.grey),
),
), ),
shape: RoundedRectangleBorder( label: const Text(
borderRadius: BorderRadius.circular(30), 'Sign in with Google',
side: const BorderSide(color: Colors.grey), style: TextStyle(fontSize: 16),
), ),
onPressed: _handleGoogleSignIn,
), ),
label: const Text(
'Sign in with Google',
style: TextStyle(fontSize: 16),
),
onPressed: _handleGoogleSignIn,
),
), ),
); );
} }
} }