OHO-10(Partial Email)
This commit is contained in:
parent
f1397272e5
commit
ead0d5182e
3 changed files with 245 additions and 39 deletions
115
Backend/index.js
115
Backend/index.js
|
|
@ -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,
|
||||||
|
|
|
||||||
117
Frontend/lib/emai_screen.dart
Normal file
117
Frontend/lib/emai_screen.dart
Normal 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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,7 +206,8 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
body: Center(
|
body: Center(
|
||||||
child: _isLoading
|
child:
|
||||||
|
_isLoading
|
||||||
? const CircularProgressIndicator()
|
? const CircularProgressIndicator()
|
||||||
: ElevatedButton.icon(
|
: ElevatedButton.icon(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
|
|
@ -229,4 +232,3 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue