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,
|
||||
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,
|
||||
|
|
|
|||
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: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<LoginScreen> {
|
|||
'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<LoginScreen> {
|
|||
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<LoginScreen> {
|
|||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue