MichaelEdou Claude Opus 4.6 commited on
Commit
efc415a
·
1 Parent(s): a47d0d3

feat: add Email Senders page, replace Reports with Coming Soon, remove AI references

Browse files

- Reports page replaced with Coming Soon placeholder
- New Email Senders page: view senders, expand to see transactions, send
transaction history via Gmail API
- Backend /api/senders routes: list senders, get transactions, send email
- Sidebar updated with Email Senders navigation link
- All AI/IA mentions removed from UI labels and i18n strings
- AI Chat assistant removed from MainLayout and TransactionReviewPage
- Chat route replaced with senders route on backend
- ScanModal and Journal page AI labels renamed to neutral terms

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

packages/server/src/index.ts CHANGED
@@ -19,7 +19,7 @@ import emailRoutes from './routes/emails.js';
19
  import transactionRoutes from './routes/transactions.js';
20
  import settingsRoutes from './routes/settings.js';
21
  import receiptRoutes from './routes/receipts.js';
22
- import chatRoutes from './routes/chat.js';
23
  import screenshotRoutes from './routes/screenshots.js';
24
  import { errorHandler } from './middleware/errorHandler.js';
25
 
@@ -67,7 +67,7 @@ app.use('/api/scan', emailRoutes);
67
  app.use('/api/transactions', transactionRoutes);
68
  app.use('/api/settings', settingsRoutes);
69
  app.use('/api/receipts', receiptRoutes);
70
- app.use('/api/chat', chatRoutes);
71
  app.use('/api/screenshots', screenshotRoutes);
72
 
73
  // Serve frontend static files in production
 
19
  import transactionRoutes from './routes/transactions.js';
20
  import settingsRoutes from './routes/settings.js';
21
  import receiptRoutes from './routes/receipts.js';
22
+ import senderRoutes from './routes/senders.js';
23
  import screenshotRoutes from './routes/screenshots.js';
24
  import { errorHandler } from './middleware/errorHandler.js';
25
 
 
67
  app.use('/api/transactions', transactionRoutes);
68
  app.use('/api/settings', settingsRoutes);
69
  app.use('/api/receipts', receiptRoutes);
70
+ app.use('/api/senders', senderRoutes);
71
  app.use('/api/screenshots', screenshotRoutes);
72
 
73
  // Serve frontend static files in production
packages/server/src/routes/senders.ts ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+ import { google } from 'googleapis';
3
+ import { db } from '../db/index.js';
4
+ import { transactions, users } from '../db/schema.js';
5
+ import { eq, desc, sql, and } from 'drizzle-orm';
6
+ import { requireAuth, type AuthRequest } from '../middleware/auth.js';
7
+ import { config } from '../config/env.js';
8
+
9
+ const router = Router();
10
+
11
+ // GET /api/senders — List unique senders with stats (user-scoped)
12
+ router.get('/', requireAuth, async (req: AuthRequest, res) => {
13
+ const search = (req.query.search as string) || '';
14
+
15
+ const conditions = [eq(transactions.userId, req.userId!)];
16
+ if (search) {
17
+ const s = '%' + search + '%';
18
+ conditions.push(sql`${transactions.sender} LIKE ${s}`);
19
+ }
20
+
21
+ const where = and(...conditions);
22
+
23
+ const senders = await db.select({
24
+ sender: transactions.sender,
25
+ totalTransactions: sql<number>`count(*)`,
26
+ totalAmount: sql<number>`COALESCE(sum(${transactions.amount}), 0)`,
27
+ lastTransaction: sql<string>`max(${transactions.date})`,
28
+ })
29
+ .from(transactions)
30
+ .where(where)
31
+ .groupBy(transactions.sender)
32
+ .orderBy(desc(sql`count(*)`));
33
+
34
+ res.json({ senders });
35
+ });
36
+
37
+ // GET /api/senders/:name/transactions — Get all transactions for a sender
38
+ router.get('/:name/transactions', requireAuth, async (req: AuthRequest, res) => {
39
+ const senderName = decodeURIComponent(req.params.name as string);
40
+
41
+ const rows = await db.select()
42
+ .from(transactions)
43
+ .where(and(
44
+ eq(transactions.userId, req.userId!),
45
+ eq(transactions.sender, senderName)
46
+ ))
47
+ .orderBy(desc(sql`datetime(${transactions.date})`));
48
+
49
+ res.json({ transactions: rows });
50
+ });
51
+
52
+ // POST /api/senders/:name/send — Send transaction history email to a sender
53
+ router.post('/:name/send', requireAuth, async (req: AuthRequest, res) => {
54
+ const senderName = decodeURIComponent(req.params.name as string);
55
+ const { recipientEmail } = req.body;
56
+
57
+ if (!recipientEmail) {
58
+ res.status(400).json({ error: true, message: 'recipientEmail is required' });
59
+ return;
60
+ }
61
+
62
+ // Fetch all transactions for this sender
63
+ const rows = await db.select()
64
+ .from(transactions)
65
+ .where(and(
66
+ eq(transactions.userId, req.userId!),
67
+ eq(transactions.sender, senderName)
68
+ ))
69
+ .orderBy(desc(sql`datetime(${transactions.date})`));
70
+
71
+ if (rows.length === 0) {
72
+ res.status(404).json({ error: true, message: 'No transactions found for this sender' });
73
+ return;
74
+ }
75
+
76
+ // Get user's OAuth credentials
77
+ const user = await db.select().from(users).where(eq(users.id, req.userId!)).get();
78
+ if (!user?.accessToken) {
79
+ res.status(401).json({ error: true, message: 'Google OAuth required to send emails' });
80
+ return;
81
+ }
82
+
83
+ const oauth2Client = new google.auth.OAuth2(
84
+ config.VITE_GOOGLE_CLIENT_ID,
85
+ config.GOOGLE_CLIENT_SECRET
86
+ );
87
+ oauth2Client.setCredentials({
88
+ access_token: user.accessToken,
89
+ refresh_token: user.refreshToken,
90
+ expiry_date: user.tokenExpires ? new Date(user.tokenExpires).getTime() : undefined,
91
+ });
92
+
93
+ // Build email body
94
+ const totalAmount = rows.reduce((sum, tx) => sum + (tx.amount || 0), 0);
95
+ const formattedTotal = totalAmount.toLocaleString('fr-CA', { style: 'currency', currency: 'CAD' });
96
+
97
+ const txRows = rows.map(tx => {
98
+ const date = new Date(tx.date).toLocaleDateString('fr-CA');
99
+ const amount = (tx.amount || 0).toLocaleString('fr-CA', { style: 'currency', currency: 'CAD' });
100
+ return `<tr>
101
+ <td style="padding:8px 12px;border-bottom:1px solid #e2e8f0;font-size:13px;">${date}</td>
102
+ <td style="padding:8px 12px;border-bottom:1px solid #e2e8f0;font-size:13px;text-align:right;font-weight:600;">${amount}</td>
103
+ <td style="padding:8px 12px;border-bottom:1px solid #e2e8f0;font-size:13px;font-family:monospace;color:#64748b;">${tx.reference || '—'}</td>
104
+ <td style="padding:8px 12px;border-bottom:1px solid #e2e8f0;font-size:13px;">${tx.branch || '—'}</td>
105
+ <td style="padding:8px 12px;border-bottom:1px solid #e2e8f0;font-size:13px;">${tx.status || '—'}</td>
106
+ </tr>`;
107
+ }).join('');
108
+
109
+ const html = `<!DOCTYPE html>
110
+ <html>
111
+ <head><meta charset="UTF-8"></head>
112
+ <body style="margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f8fafc;">
113
+ <div style="max-width:700px;margin:0 auto;padding:32px 16px;">
114
+ <div style="background:white;border-radius:12px;border:1px solid #e2e8f0;overflow:hidden;">
115
+ <div style="background:linear-gradient(135deg,#1e40af,#3b82f6);padding:24px 32px;color:white;">
116
+ <h1 style="margin:0;font-size:20px;font-weight:700;">ICC Interac Manager</h1>
117
+ <p style="margin:4px 0 0;font-size:14px;opacity:0.9;">Transaction History Report</p>
118
+ </div>
119
+ <div style="padding:24px 32px;">
120
+ <p style="margin:0 0 16px;font-size:15px;color:#334155;">
121
+ Hello <strong>${senderName}</strong>,
122
+ </p>
123
+ <p style="margin:0 0 24px;font-size:14px;color:#64748b;">
124
+ Here is a summary of your Interac e-Transfer transactions on record:
125
+ </p>
126
+ <div style="background:#f1f5f9;border-radius:8px;padding:16px;margin-bottom:24px;display:flex;gap:24px;">
127
+ <div>
128
+ <div style="font-size:11px;text-transform:uppercase;color:#94a3b8;font-weight:600;">Total Transactions</div>
129
+ <div style="font-size:24px;font-weight:800;color:#1e293b;">${rows.length}</div>
130
+ </div>
131
+ <div>
132
+ <div style="font-size:11px;text-transform:uppercase;color:#94a3b8;font-weight:600;">Total Amount</div>
133
+ <div style="font-size:24px;font-weight:800;color:#1e293b;">${formattedTotal}</div>
134
+ </div>
135
+ </div>
136
+ <table style="width:100%;border-collapse:collapse;border:1px solid #e2e8f0;border-radius:8px;overflow:hidden;">
137
+ <thead>
138
+ <tr style="background:#f8fafc;">
139
+ <th style="padding:10px 12px;text-align:left;font-size:11px;text-transform:uppercase;color:#64748b;font-weight:700;border-bottom:2px solid #e2e8f0;">Date</th>
140
+ <th style="padding:10px 12px;text-align:right;font-size:11px;text-transform:uppercase;color:#64748b;font-weight:700;border-bottom:2px solid #e2e8f0;">Amount</th>
141
+ <th style="padding:10px 12px;text-align:left;font-size:11px;text-transform:uppercase;color:#64748b;font-weight:700;border-bottom:2px solid #e2e8f0;">Reference</th>
142
+ <th style="padding:10px 12px;text-align:left;font-size:11px;text-transform:uppercase;color:#64748b;font-weight:700;border-bottom:2px solid #e2e8f0;">Branch</th>
143
+ <th style="padding:10px 12px;text-align:left;font-size:11px;text-transform:uppercase;color:#64748b;font-weight:700;border-bottom:2px solid #e2e8f0;">Status</th>
144
+ </tr>
145
+ </thead>
146
+ <tbody>${txRows}</tbody>
147
+ </table>
148
+ <p style="margin:24px 0 0;font-size:12px;color:#94a3b8;text-align:center;">
149
+ This email was generated automatically by ICC Interac Manager.
150
+ </p>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </body>
155
+ </html>`;
156
+
157
+ // Send via Gmail API
158
+ const subject = `Your Interac Transaction History — ICC (${rows.length} transactions)`;
159
+ const fromEmail = user.email;
160
+
161
+ const rawMessage = [
162
+ `From: ${fromEmail}`,
163
+ `To: ${recipientEmail}`,
164
+ `Subject: =?UTF-8?B?${Buffer.from(subject).toString('base64')}?=`,
165
+ 'MIME-Version: 1.0',
166
+ 'Content-Type: text/html; charset=UTF-8',
167
+ '',
168
+ html,
169
+ ].join('\r\n');
170
+
171
+ const encodedMessage = Buffer.from(rawMessage)
172
+ .toString('base64')
173
+ .replace(/\+/g, '-')
174
+ .replace(/\//g, '_')
175
+ .replace(/=+$/, '');
176
+
177
+ try {
178
+ const gmail = google.gmail({ version: 'v1', auth: oauth2Client });
179
+ await gmail.users.messages.send({
180
+ userId: 'me',
181
+ requestBody: { raw: encodedMessage },
182
+ });
183
+
184
+ res.json({ success: true, message: `Email sent to ${recipientEmail}` });
185
+ } catch (err: any) {
186
+ res.status(500).json({ error: true, message: err.message || 'Failed to send email' });
187
+ }
188
+ });
189
+
190
+ export default router;
packages/web/src/App.tsx CHANGED
@@ -7,6 +7,7 @@ import ReportsPage from '@/pages/ReportsPage';
7
  import BranchesPage from '@/pages/BranchesPage';
8
  import SettingsPage from '@/pages/SettingsPage';
9
  import JournalPage from '@/pages/JournalPage';
 
10
  import TransactionsPage from '@/pages/TransactionsPage';
11
  import TransactionReviewPage from '@/pages/TransactionReviewPage';
12
  import ReceiptPreviewPage from '@/pages/ReceiptPreviewPage';
@@ -38,6 +39,7 @@ function App() {
38
  <Route path="/branches" element={<BranchesPage />} />
39
  <Route path="/transactions/:id/receipt" element={<ReceiptPreviewPage />} />
40
  <Route path="/journal" element={<JournalPage />} />
 
41
  <Route path="/settings" element={<SettingsPage />} />
42
  </Route>
43
 
 
7
  import BranchesPage from '@/pages/BranchesPage';
8
  import SettingsPage from '@/pages/SettingsPage';
9
  import JournalPage from '@/pages/JournalPage';
10
+ import EmailSendersPage from '@/pages/EmailSendersPage';
11
  import TransactionsPage from '@/pages/TransactionsPage';
12
  import TransactionReviewPage from '@/pages/TransactionReviewPage';
13
  import ReceiptPreviewPage from '@/pages/ReceiptPreviewPage';
 
39
  <Route path="/branches" element={<BranchesPage />} />
40
  <Route path="/transactions/:id/receipt" element={<ReceiptPreviewPage />} />
41
  <Route path="/journal" element={<JournalPage />} />
42
+ <Route path="/email-senders" element={<EmailSendersPage />} />
43
  <Route path="/settings" element={<SettingsPage />} />
44
  </Route>
45
 
packages/web/src/components/layout/MainLayout.tsx CHANGED
@@ -1,7 +1,6 @@
1
  import { useEffect } from 'react';
2
  import { Outlet, useNavigate } from 'react-router';
3
  import Sidebar from './Sidebar';
4
- import ChatAssistant from '@/components/chat/ChatAssistant';
5
  import { useAuthStore } from '@/stores/authStore';
6
 
7
  export default function MainLayout() {
@@ -40,7 +39,6 @@ export default function MainLayout() {
40
  <main className="flex flex-1 flex-col overflow-hidden bg-background">
41
  <Outlet />
42
  </main>
43
- <ChatAssistant />
44
  </div>
45
  );
46
  }
 
1
  import { useEffect } from 'react';
2
  import { Outlet, useNavigate } from 'react-router';
3
  import Sidebar from './Sidebar';
 
4
  import { useAuthStore } from '@/stores/authStore';
5
 
6
  export default function MainLayout() {
 
39
  <main className="flex flex-1 flex-col overflow-hidden bg-background">
40
  <Outlet />
41
  </main>
 
42
  </div>
43
  );
44
  }
packages/web/src/components/layout/Sidebar.tsx CHANGED
@@ -9,6 +9,7 @@ import {
9
  Settings,
10
  LogOut,
11
  Wallet,
 
12
  } from 'lucide-react';
13
  import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
14
  import { cn } from '@/lib/utils';
@@ -26,6 +27,7 @@ const navItems: NavItem[] = [
26
  { icon: BarChart3, labelKey: 'nav.reports', href: '/reports' },
27
  { icon: Store, labelKey: 'nav.branches', href: '/branches' },
28
  { icon: ScrollText, labelKey: 'nav.journal', href: '/journal' },
 
29
  { icon: Settings, labelKey: 'nav.settings', href: '/settings' },
30
  ];
31
 
 
9
  Settings,
10
  LogOut,
11
  Wallet,
12
+ Mail,
13
  } from 'lucide-react';
14
  import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
15
  import { cn } from '@/lib/utils';
 
27
  { icon: BarChart3, labelKey: 'nav.reports', href: '/reports' },
28
  { icon: Store, labelKey: 'nav.branches', href: '/branches' },
29
  { icon: ScrollText, labelKey: 'nav.journal', href: '/journal' },
30
+ { icon: Mail, labelKey: 'nav.emailSenders', href: '/email-senders' },
31
  { icon: Settings, labelKey: 'nav.settings', href: '/settings' },
32
  ];
33
 
packages/web/src/components/review/ValidationForm.tsx CHANGED
@@ -2,7 +2,7 @@ import { useState } from 'react';
2
  import { useTranslation } from 'react-i18next';
3
  import { useNavigate } from 'react-router';
4
  import { useQuery } from '@tanstack/react-query';
5
- import { FileEdit, Bot, CheckCircle, Trash2, Target } from 'lucide-react';
6
  import { Input } from '@/components/ui/input';
7
  import { Button } from '@/components/ui/button';
8
  import {
@@ -119,8 +119,8 @@ export default function ValidationForm({ transaction }: ValidationFormProps) {
119
  </div>
120
  </div>
121
  <div className="flex items-center gap-1 rounded-full border border-green-100 bg-green-50 px-2.5 py-1 text-xs font-medium text-green-700">
122
- <Bot className="h-3.5 w-3.5" />
123
- <span>AI Parsed</span>
124
  </div>
125
  </div>
126
 
 
2
  import { useTranslation } from 'react-i18next';
3
  import { useNavigate } from 'react-router';
4
  import { useQuery } from '@tanstack/react-query';
5
+ import { FileEdit, CheckCircle, Trash2, Target } from 'lucide-react';
6
  import { Input } from '@/components/ui/input';
7
  import { Button } from '@/components/ui/button';
8
  import {
 
119
  </div>
120
  </div>
121
  <div className="flex items-center gap-1 rounded-full border border-green-100 bg-green-50 px-2.5 py-1 text-xs font-medium text-green-700">
122
+ <CheckCircle className="h-3.5 w-3.5" />
123
+ <span>{t('review.extracted', 'Extracted')}</span>
124
  </div>
125
  </div>
126
 
packages/web/src/components/scan/ScanModal.tsx CHANGED
@@ -178,7 +178,7 @@ export default function ScanModal({ onClose, onMinimize, preset = 'today', force
178
  </h3>
179
  <p className="text-sm text-slate-500 flex items-center gap-1.5">
180
  {isActive && <span className="h-1.5 w-1.5 rounded-full bg-green-500 animate-pulse" />}
181
- {t('scanModal.aiEngine')}
182
  </p>
183
  </div>
184
  </div>
@@ -255,10 +255,10 @@ export default function ScanModal({ onClose, onMinimize, preset = 'today', force
255
  </div>
256
  </div>
257
 
258
- {/* AI extraction log */}
259
  <div className="mb-8 rounded-xl border border-slate-200 bg-slate-50 overflow-hidden">
260
  <div className="border-b border-slate-200 bg-slate-100/50 px-4 py-2 text-xs font-semibold text-slate-500 uppercase tracking-wider flex justify-between items-center">
261
- <span>{t('scanModal.aiLog')}</span>
262
  {isActive && <span className="h-1.5 w-1.5 rounded-full bg-green-500 animate-pulse" />}
263
  </div>
264
  <div ref={logContainerRef} className="h-40 overflow-y-auto p-4 space-y-2 font-mono text-sm">
 
178
  </h3>
179
  <p className="text-sm text-slate-500 flex items-center gap-1.5">
180
  {isActive && <span className="h-1.5 w-1.5 rounded-full bg-green-500 animate-pulse" />}
181
+ {t('scanModal.engine', 'Scan Engine active')}
182
  </p>
183
  </div>
184
  </div>
 
255
  </div>
256
  </div>
257
 
258
+ {/* Extraction log */}
259
  <div className="mb-8 rounded-xl border border-slate-200 bg-slate-50 overflow-hidden">
260
  <div className="border-b border-slate-200 bg-slate-100/50 px-4 py-2 text-xs font-semibold text-slate-500 uppercase tracking-wider flex justify-between items-center">
261
+ <span>{t('scanModal.extractionLog', 'Extraction Log')}</span>
262
  {isActive && <span className="h-1.5 w-1.5 rounded-full bg-green-500 animate-pulse" />}
263
  </div>
264
  <div ref={logContainerRef} className="h-40 overflow-y-auto p-4 space-y-2 font-mono text-sm">
packages/web/src/i18n/locales/en.json CHANGED
@@ -21,6 +21,7 @@
21
  "reports": "Reports",
22
  "branches": "Branches",
23
  "journal": "Journal",
 
24
  "settings": "Settings",
25
  "logout": "Sign out"
26
  },
@@ -120,9 +121,12 @@
120
  "excelDescription": "Download a detailed report containing all transaction metadata, sorted by date.",
121
  "generateReport": "Generate report",
122
  "pdfTitle": "Batch Download (PDF)",
123
- "pdfDescription": "ZIP archive containing individual AI-generated receipts for the selected period.",
124
  "startDownload": "Start download"
125
- }
 
 
 
126
  },
127
  "branches": {
128
  "title": "Branch Management",
@@ -189,8 +193,8 @@
189
  "referenceNumber": "Reference number",
190
  "autoEmailNotice": "This email was sent automatically. Please do not reply.",
191
  "formTitle": "Extraction: Validation Form",
192
- "formSubtitle": "Review and correct AI-extracted data",
193
- "aiConfidence": "AI Confidence: {{score}}",
194
  "transactionDetails": "Transaction Details",
195
  "amount": "Amount (CAD)",
196
  "transactionDate": "Transaction Date",
@@ -201,7 +205,7 @@
201
  "verificationRequired": "Verification Required",
202
  "createNewBranch": "Create New Branch: \"{{name}}\"",
203
  "partialMatch": "Partial match",
204
- "aiSuggestion": "AI suggests \"Montréal\" based on history, but this is a new contact.",
205
  "internalNotes": "Internal Notes",
206
  "addComment": "Add a comment...",
207
  "skip": "Skip",
@@ -210,7 +214,7 @@
210
  "shortcutValidate": "to validate",
211
  "shortcutSkip": "to skip",
212
  "matchAccuracy": "Amount Accuracy",
213
- "aiExtracted": "AI Amount",
214
  "emailOriginal": "Email Amount",
215
  "accuracyLabel": "Accuracy",
216
  "noRawEmail": "Source email not available"
@@ -265,7 +269,7 @@
265
  "title": "Scanning in progress",
266
  "titleComplete": "Scan complete",
267
  "titleError": "Scan error",
268
- "aiEngine": "AI Engine active • v2.4.1",
269
  "sessionId": "Session ID",
270
  "starting": "Starting scan...",
271
  "emailsLabel": "emails",
@@ -278,7 +282,7 @@
278
  "emailsAnalyzed": "Emails analyzed",
279
  "duplicatesSkipped": "Duplicates skipped",
280
  "errors": "Errors",
281
- "aiLog": "AI Extraction Log",
282
  "logFound": "Found",
283
  "logAmount": "Amount",
284
  "logDuplicate": "Duplicate",
@@ -296,7 +300,7 @@
296
  },
297
  "journal": {
298
  "title": "Operations Journal",
299
- "subtitle": "Complete history and audit of scan operations, AI processing and transfer routing.",
300
  "refresh": "Refresh",
301
  "scansToday": "Scans Today",
302
  "successRate": "Success Rate",
@@ -337,13 +341,40 @@
337
  "finished": "Finished",
338
  "processed": "Processed",
339
  "skipped": "skipped (duplicates)",
340
- "aiInfo": "AI Data",
341
  "provider": "Provider",
342
  "model": "Model",
343
  "errorDetails": "Error Details",
344
  "scanId": "Scan ID"
345
  }
346
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  "settings": {
348
  "title": "Profile & System",
349
  "roleAdmin": "Branch Administrator",
@@ -372,17 +403,6 @@
372
  "resetData": "Reset data",
373
  "confirmReset": "Confirm reset"
374
  },
375
- "chat": {
376
- "title": "ICC Assistant",
377
- "subtitle": "Interac transfer management",
378
- "placeholder": "Ask me something...",
379
- "clearHistory": "Clear history",
380
- "welcome": "Hello! I'm the ICC assistant. I can scan your emails, show transactions, or check statistics.",
381
- "quickScanToday": "Scan today",
382
- "quickScan7days": "Scan 7 days",
383
- "quickTransactions": "Transactions",
384
- "quickStats": "Statistics"
385
- },
386
  "common": {
387
  "loading": "Loading...",
388
  "error": "Error",
 
21
  "reports": "Reports",
22
  "branches": "Branches",
23
  "journal": "Journal",
24
+ "emailSenders": "Email Senders",
25
  "settings": "Settings",
26
  "logout": "Sign out"
27
  },
 
121
  "excelDescription": "Download a detailed report containing all transaction metadata, sorted by date.",
122
  "generateReport": "Generate report",
123
  "pdfTitle": "Batch Download (PDF)",
124
+ "pdfDescription": "ZIP archive containing individual receipts for the selected period.",
125
  "startDownload": "Start download"
126
+ },
127
+ "comingSoon": "Coming Soon",
128
+ "comingSoonDesc": "This feature is currently under development. Stay tuned for advanced reporting and export capabilities.",
129
+ "comingSoonStatus": "Under development"
130
  },
131
  "branches": {
132
  "title": "Branch Management",
 
193
  "referenceNumber": "Reference number",
194
  "autoEmailNotice": "This email was sent automatically. Please do not reply.",
195
  "formTitle": "Extraction: Validation Form",
196
+ "formSubtitle": "Review and correct extracted data",
197
+ "confidence": "Confidence: {{score}}",
198
  "transactionDetails": "Transaction Details",
199
  "amount": "Amount (CAD)",
200
  "transactionDate": "Transaction Date",
 
205
  "verificationRequired": "Verification Required",
206
  "createNewBranch": "Create New Branch: \"{{name}}\"",
207
  "partialMatch": "Partial match",
208
+ "suggestion": "Suggests \"Montréal\" based on history, but this is a new contact.",
209
  "internalNotes": "Internal Notes",
210
  "addComment": "Add a comment...",
211
  "skip": "Skip",
 
214
  "shortcutValidate": "to validate",
215
  "shortcutSkip": "to skip",
216
  "matchAccuracy": "Amount Accuracy",
217
+ "aiExtracted": "Extracted Amount",
218
  "emailOriginal": "Email Amount",
219
  "accuracyLabel": "Accuracy",
220
  "noRawEmail": "Source email not available"
 
269
  "title": "Scanning in progress",
270
  "titleComplete": "Scan complete",
271
  "titleError": "Scan error",
272
+ "engine": "Scan Engine active • v2.4.1",
273
  "sessionId": "Session ID",
274
  "starting": "Starting scan...",
275
  "emailsLabel": "emails",
 
282
  "emailsAnalyzed": "Emails analyzed",
283
  "duplicatesSkipped": "Duplicates skipped",
284
  "errors": "Errors",
285
+ "extractionLog": "Extraction Log",
286
  "logFound": "Found",
287
  "logAmount": "Amount",
288
  "logDuplicate": "Duplicate",
 
300
  },
301
  "journal": {
302
  "title": "Operations Journal",
303
+ "subtitle": "Complete history and audit of scan operations, email processing and transfer routing.",
304
  "refresh": "Refresh",
305
  "scansToday": "Scans Today",
306
  "successRate": "Success Rate",
 
341
  "finished": "Finished",
342
  "processed": "Processed",
343
  "skipped": "skipped (duplicates)",
344
+ "engineInfo": "Engine Info",
345
  "provider": "Provider",
346
  "model": "Model",
347
  "errorDetails": "Error Details",
348
  "scanId": "Scan ID"
349
  }
350
  },
351
+ "emailSenders": {
352
+ "title": "Email Senders",
353
+ "subtitle": "Send transaction history to senders via email.",
354
+ "searchPlaceholder": "Search a sender...",
355
+ "noResults": "No senders found",
356
+ "showing": "Showing",
357
+ "of": "of",
358
+ "senders": "senders",
359
+ "col": {
360
+ "sender": "Sender",
361
+ "totalTransactions": "Transactions",
362
+ "totalAmount": "Total Amount",
363
+ "lastTransaction": "Last Transaction",
364
+ "actions": "Actions"
365
+ },
366
+ "sendHistory": "Send History",
367
+ "sendAll": "Send to All",
368
+ "sending": "Sending...",
369
+ "sent": "Sent",
370
+ "sendSuccess": "Email sent successfully to {{name}}",
371
+ "sendError": "Failed to send email to {{name}}",
372
+ "sendAllConfirm": "Send transaction history to all {{count}} senders?",
373
+ "emailSubject": "Your Interac Transaction History — ICC",
374
+ "emailPreview": "Preview Email",
375
+ "lastSent": "Last sent",
376
+ "never": "Never"
377
+ },
378
  "settings": {
379
  "title": "Profile & System",
380
  "roleAdmin": "Branch Administrator",
 
403
  "resetData": "Reset data",
404
  "confirmReset": "Confirm reset"
405
  },
 
 
 
 
 
 
 
 
 
 
 
406
  "common": {
407
  "loading": "Loading...",
408
  "error": "Error",
packages/web/src/i18n/locales/fr.json CHANGED
@@ -21,6 +21,7 @@
21
  "reports": "Rapports",
22
  "branches": "Succursales",
23
  "journal": "Journal",
 
24
  "settings": "Paramètres",
25
  "logout": "Déconnexion"
26
  },
@@ -120,9 +121,12 @@
120
  "excelDescription": "Téléchargez un rapport détaillé contenant toutes les métadonnées des transactions, triées par date.",
121
  "generateReport": "Générer le rapport",
122
  "pdfTitle": "Téléchargement groupé (PDF)",
123
- "pdfDescription": "Archive ZIP contenant les reçus individuels générés par l'IA pour la période sélectionnée.",
124
  "startDownload": "Démarrer le téléchargement"
125
- }
 
 
 
126
  },
127
  "branches": {
128
  "title": "Gestion des Succursales",
@@ -189,8 +193,8 @@
189
  "referenceNumber": "Numéro de référence",
190
  "autoEmailNotice": "Ce courriel a été envoyé automatiquement. Veuillez ne pas répondre.",
191
  "formTitle": "Extraction: Formulaire de Validation",
192
- "formSubtitle": "Vérifiez et corrigez les données extraites par l'IA",
193
- "aiConfidence": "Confiance IA: {{score}}",
194
  "transactionDetails": "Détails de la Transaction",
195
  "amount": "Montant (CAD)",
196
  "transactionDate": "Date de Transaction",
@@ -201,7 +205,7 @@
201
  "verificationRequired": "Vérification Requise",
202
  "createNewBranch": "Créer Nouvelle Succursale: \"{{name}}\"",
203
  "partialMatch": "Match partiel",
204
- "aiSuggestion": "L'IA suggère \"Montréal\" basé sur l'historique, mais c'est un nouveau contact.",
205
  "internalNotes": "Notes Internes",
206
  "addComment": "Ajouter un commentaire...",
207
  "skip": "Ignorer",
@@ -210,7 +214,7 @@
210
  "shortcutValidate": "pour valider",
211
  "shortcutSkip": "pour ignorer",
212
  "matchAccuracy": "Précision du montant",
213
- "aiExtracted": "Montant IA",
214
  "emailOriginal": "Montant courriel",
215
  "accuracyLabel": "Précision",
216
  "noRawEmail": "Courriel source non disponible"
@@ -265,7 +269,7 @@
265
  "title": "Numérisation en cours",
266
  "titleComplete": "Numérisation terminée",
267
  "titleError": "Erreur de numérisation",
268
- "aiEngine": "Moteur IA actif • v2.4.1",
269
  "sessionId": "ID Session",
270
  "starting": "Démarrage du scan...",
271
  "emailsLabel": "courriels",
@@ -278,7 +282,7 @@
278
  "emailsAnalyzed": "E-mails analysés",
279
  "duplicatesSkipped": "Doublons ignorés",
280
  "errors": "Erreurs",
281
- "aiLog": "Journal d'extraction IA",
282
  "logFound": "Trouvé",
283
  "logAmount": "Montant",
284
  "logDuplicate": "Doublon",
@@ -296,7 +300,7 @@
296
  },
297
  "journal": {
298
  "title": "Journal des Opérations",
299
- "subtitle": "Historique complet et audit des opérations de scan, de traitement IA et de routage des virements.",
300
  "refresh": "Rafraîchir",
301
  "scansToday": "Scans aujourd'hui",
302
  "successRate": "Taux de succès",
@@ -337,13 +341,40 @@
337
  "finished": "Fin",
338
  "processed": "Traités",
339
  "skipped": "ignorés (doublons)",
340
- "aiInfo": "Données IA",
341
  "provider": "Fournisseur",
342
  "model": "Modèle",
343
  "errorDetails": "Détails de l'erreur",
344
  "scanId": "ID Scan"
345
  }
346
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  "settings": {
348
  "title": "Profil et Système",
349
  "roleAdmin": "Administrateur de succursale",
@@ -372,17 +403,6 @@
372
  "resetData": "Réinitialiser les données",
373
  "confirmReset": "Confirmer la réinitialisation"
374
  },
375
- "chat": {
376
- "title": "Assistant ICC",
377
- "subtitle": "Gestion des virements Interac",
378
- "placeholder": "Demandez-moi quelque chose...",
379
- "clearHistory": "Effacer l'historique",
380
- "welcome": "Bonjour! Je suis l'assistant ICC. Je peux scanner vos courriels, afficher les transactions, ou consulter les statistiques.",
381
- "quickScanToday": "Scanner aujourd'hui",
382
- "quickScan7days": "Scanner 7 jours",
383
- "quickTransactions": "Transactions",
384
- "quickStats": "Statistiques"
385
- },
386
  "common": {
387
  "loading": "Chargement...",
388
  "error": "Erreur",
 
21
  "reports": "Rapports",
22
  "branches": "Succursales",
23
  "journal": "Journal",
24
+ "emailSenders": "Envoi aux expéditeurs",
25
  "settings": "Paramètres",
26
  "logout": "Déconnexion"
27
  },
 
121
  "excelDescription": "Téléchargez un rapport détaillé contenant toutes les métadonnées des transactions, triées par date.",
122
  "generateReport": "Générer le rapport",
123
  "pdfTitle": "Téléchargement groupé (PDF)",
124
+ "pdfDescription": "Archive ZIP contenant les reçus individuels pour la période sélectionnée.",
125
  "startDownload": "Démarrer le téléchargement"
126
+ },
127
+ "comingSoon": "Bientôt disponible",
128
+ "comingSoonDesc": "Cette fonctionnalité est en cours de développement. Restez à l'écoute pour des rapports avancés et des capacités d'exportation.",
129
+ "comingSoonStatus": "En développement"
130
  },
131
  "branches": {
132
  "title": "Gestion des Succursales",
 
193
  "referenceNumber": "Numéro de référence",
194
  "autoEmailNotice": "Ce courriel a été envoyé automatiquement. Veuillez ne pas répondre.",
195
  "formTitle": "Extraction: Formulaire de Validation",
196
+ "formSubtitle": "Vérifiez et corrigez les données extraites",
197
+ "confidence": "Confiance: {{score}}",
198
  "transactionDetails": "Détails de la Transaction",
199
  "amount": "Montant (CAD)",
200
  "transactionDate": "Date de Transaction",
 
205
  "verificationRequired": "Vérification Requise",
206
  "createNewBranch": "Créer Nouvelle Succursale: \"{{name}}\"",
207
  "partialMatch": "Match partiel",
208
+ "suggestion": "Suggère \"Montréal\" basé sur l'historique, mais c'est un nouveau contact.",
209
  "internalNotes": "Notes Internes",
210
  "addComment": "Ajouter un commentaire...",
211
  "skip": "Ignorer",
 
214
  "shortcutValidate": "pour valider",
215
  "shortcutSkip": "pour ignorer",
216
  "matchAccuracy": "Précision du montant",
217
+ "aiExtracted": "Montant extrait",
218
  "emailOriginal": "Montant courriel",
219
  "accuracyLabel": "Précision",
220
  "noRawEmail": "Courriel source non disponible"
 
269
  "title": "Numérisation en cours",
270
  "titleComplete": "Numérisation terminée",
271
  "titleError": "Erreur de numérisation",
272
+ "engine": "Moteur de scan actif • v2.4.1",
273
  "sessionId": "ID Session",
274
  "starting": "Démarrage du scan...",
275
  "emailsLabel": "courriels",
 
282
  "emailsAnalyzed": "E-mails analysés",
283
  "duplicatesSkipped": "Doublons ignorés",
284
  "errors": "Erreurs",
285
+ "extractionLog": "Journal d'extraction",
286
  "logFound": "Trouvé",
287
  "logAmount": "Montant",
288
  "logDuplicate": "Doublon",
 
300
  },
301
  "journal": {
302
  "title": "Journal des Opérations",
303
+ "subtitle": "Historique complet et audit des opérations de scan, de traitement et de routage des virements.",
304
  "refresh": "Rafraîchir",
305
  "scansToday": "Scans aujourd'hui",
306
  "successRate": "Taux de succès",
 
341
  "finished": "Fin",
342
  "processed": "Traités",
343
  "skipped": "ignorés (doublons)",
344
+ "engineInfo": "Infos moteur",
345
  "provider": "Fournisseur",
346
  "model": "Modèle",
347
  "errorDetails": "Détails de l'erreur",
348
  "scanId": "ID Scan"
349
  }
350
  },
351
+ "emailSenders": {
352
+ "title": "Envoi aux expéditeurs",
353
+ "subtitle": "Envoyez l'historique des transactions aux expéditeurs par courriel.",
354
+ "searchPlaceholder": "Rechercher un expéditeur...",
355
+ "noResults": "Aucun expéditeur trouvé",
356
+ "showing": "Affichage de",
357
+ "of": "sur",
358
+ "senders": "expéditeurs",
359
+ "col": {
360
+ "sender": "Expéditeur",
361
+ "totalTransactions": "Transactions",
362
+ "totalAmount": "Montant total",
363
+ "lastTransaction": "Dernière transaction",
364
+ "actions": "Actions"
365
+ },
366
+ "sendHistory": "Envoyer l'historique",
367
+ "sendAll": "Envoyer à tous",
368
+ "sending": "Envoi en cours...",
369
+ "sent": "Envoyé",
370
+ "sendSuccess": "Courriel envoyé avec succès à {{name}}",
371
+ "sendError": "Échec de l'envoi du courriel à {{name}}",
372
+ "sendAllConfirm": "Envoyer l'historique des transactions aux {{count}} expéditeurs ?",
373
+ "emailSubject": "Votre historique de transactions Interac — ICC",
374
+ "emailPreview": "Aperçu du courriel",
375
+ "lastSent": "Dernier envoi",
376
+ "never": "Jamais"
377
+ },
378
  "settings": {
379
  "title": "Profil et Système",
380
  "roleAdmin": "Administrateur de succursale",
 
403
  "resetData": "Réinitialiser les données",
404
  "confirmReset": "Confirmer la réinitialisation"
405
  },
 
 
 
 
 
 
 
 
 
 
 
406
  "common": {
407
  "loading": "Chargement...",
408
  "error": "Erreur",
packages/web/src/pages/EmailSendersPage.tsx ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { useQuery, useMutation } from '@tanstack/react-query';
4
+ import { toast } from 'sonner';
5
+ import Header from '@/components/layout/Header';
6
+ import { api } from '@/services/api';
7
+ import {
8
+ Search,
9
+ Mail,
10
+ Send,
11
+ Loader2,
12
+ CheckCircle,
13
+ ChevronRight,
14
+ ChevronDown,
15
+ DollarSign,
16
+ Hash,
17
+ Calendar,
18
+ } from 'lucide-react';
19
+ import { Button } from '@/components/ui/button';
20
+ import { cn } from '@/lib/utils';
21
+
22
+ interface SenderInfo {
23
+ sender: string;
24
+ totalTransactions: number;
25
+ totalAmount: number;
26
+ lastTransaction: string;
27
+ }
28
+
29
+ interface SenderTransaction {
30
+ id: string;
31
+ date: string;
32
+ amount: number;
33
+ reference: string | null;
34
+ branch: string | null;
35
+ status: string;
36
+ recipientEmail: string | null;
37
+ }
38
+
39
+ export default function EmailSendersPage() {
40
+ const { t } = useTranslation();
41
+ const [search, setSearch] = useState('');
42
+
43
+ const { data, isLoading } = useQuery({
44
+ queryKey: ['senders', search],
45
+ queryFn: () => {
46
+ const params = new URLSearchParams();
47
+ if (search) params.set('search', search);
48
+ return api.get<{ senders: SenderInfo[] }>(`/senders?${params}`);
49
+ },
50
+ });
51
+
52
+ const senders = data?.senders ?? [];
53
+ const filtered = senders;
54
+
55
+ return (
56
+ <div className="flex flex-1 flex-col h-full overflow-hidden">
57
+ <Header title={t('emailSenders.title')} />
58
+ <div className="flex-1 overflow-y-auto bg-background p-6 md:p-8 lg:p-10">
59
+ <div className="max-w-7xl mx-auto w-full flex flex-col gap-6">
60
+ {/* Page Header */}
61
+ <div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-4 pb-2 border-b border-slate-200">
62
+ <div>
63
+ <h1 className="text-3xl font-extrabold text-foreground tracking-tight">
64
+ {t('emailSenders.title')}
65
+ </h1>
66
+ <p className="text-muted-foreground mt-2 text-base max-w-2xl">
67
+ {t('emailSenders.subtitle')}
68
+ </p>
69
+ </div>
70
+ </div>
71
+
72
+ {/* Search */}
73
+ <div className="relative">
74
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
75
+ <input
76
+ type="text"
77
+ value={search}
78
+ onChange={(e) => setSearch(e.target.value)}
79
+ placeholder={t('emailSenders.searchPlaceholder')}
80
+ className="w-full pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary shadow-sm"
81
+ />
82
+ </div>
83
+
84
+ {/* Stats */}
85
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
86
+ <div className="bg-card rounded-xl border border-border p-5 shadow-sm">
87
+ <div className="flex items-center gap-3">
88
+ <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-50 text-blue-600">
89
+ <Mail className="h-5 w-5" />
90
+ </div>
91
+ <div>
92
+ <p className="text-xs font-medium text-muted-foreground uppercase">{t('emailSenders.senders')}</p>
93
+ <p className="text-2xl font-bold text-foreground">{senders.length}</p>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ <div className="bg-card rounded-xl border border-border p-5 shadow-sm">
98
+ <div className="flex items-center gap-3">
99
+ <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-emerald-50 text-emerald-600">
100
+ <Hash className="h-5 w-5" />
101
+ </div>
102
+ <div>
103
+ <p className="text-xs font-medium text-muted-foreground uppercase">Total Transactions</p>
104
+ <p className="text-2xl font-bold text-foreground">
105
+ {senders.reduce((s, x) => s + x.totalTransactions, 0).toLocaleString('fr-CA')}
106
+ </p>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ <div className="bg-card rounded-xl border border-border p-5 shadow-sm">
111
+ <div className="flex items-center gap-3">
112
+ <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-amber-50 text-amber-600">
113
+ <DollarSign className="h-5 w-5" />
114
+ </div>
115
+ <div>
116
+ <p className="text-xs font-medium text-muted-foreground uppercase">Total Amount</p>
117
+ <p className="text-2xl font-bold text-foreground">
118
+ {senders.reduce((s, x) => s + x.totalAmount, 0).toLocaleString('fr-CA', { style: 'currency', currency: 'CAD' })}
119
+ </p>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </div>
124
+
125
+ {/* Table */}
126
+ <div className="bg-card rounded-xl border border-border shadow-sm overflow-hidden">
127
+ {isLoading ? (
128
+ <div className="flex items-center justify-center py-20">
129
+ <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
130
+ </div>
131
+ ) : filtered.length === 0 ? (
132
+ <div className="flex flex-col items-center justify-center py-20 text-muted-foreground">
133
+ <Mail className="h-10 w-10 mb-3 opacity-50" />
134
+ <p className="font-medium">{t('emailSenders.noResults')}</p>
135
+ </div>
136
+ ) : (
137
+ <table className="w-full text-sm">
138
+ <thead>
139
+ <tr className="border-b border-border bg-slate-50/50">
140
+ <th className="text-left py-3 px-6 text-xs font-bold uppercase text-muted-foreground">{t('emailSenders.col.sender')}</th>
141
+ <th className="text-center py-3 px-4 text-xs font-bold uppercase text-muted-foreground">{t('emailSenders.col.totalTransactions')}</th>
142
+ <th className="text-right py-3 px-4 text-xs font-bold uppercase text-muted-foreground">{t('emailSenders.col.totalAmount')}</th>
143
+ <th className="text-center py-3 px-4 text-xs font-bold uppercase text-muted-foreground">{t('emailSenders.col.lastTransaction')}</th>
144
+ <th className="text-right py-3 px-6 text-xs font-bold uppercase text-muted-foreground">{t('emailSenders.col.actions')}</th>
145
+ </tr>
146
+ </thead>
147
+ <tbody>
148
+ {filtered.map((sender) => (
149
+ <SenderRow key={sender.sender} sender={sender} />
150
+ ))}
151
+ </tbody>
152
+ </table>
153
+ )}
154
+ </div>
155
+
156
+ {/* Footer */}
157
+ <div className="text-xs text-muted-foreground text-center">
158
+ {t('emailSenders.showing')} {filtered.length} {t('emailSenders.of')} {senders.length} {t('emailSenders.senders')}
159
+ </div>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ );
164
+ }
165
+
166
+ function SenderRow({ sender }: { sender: SenderInfo }) {
167
+ const { t } = useTranslation();
168
+ const [isExpanded, setIsExpanded] = useState(false);
169
+ const [emailInput, setEmailInput] = useState('');
170
+
171
+ const { data: txData, isLoading: txLoading } = useQuery({
172
+ queryKey: ['sender-transactions', sender.sender],
173
+ queryFn: () => api.get<{ transactions: SenderTransaction[] }>(`/senders/${encodeURIComponent(sender.sender)}/transactions`),
174
+ enabled: isExpanded,
175
+ });
176
+
177
+ const sendMutation = useMutation({
178
+ mutationFn: (recipientEmail: string) =>
179
+ api.post<{ success: boolean; message: string }>(`/senders/${encodeURIComponent(sender.sender)}/send`, { recipientEmail }),
180
+ onSuccess: () => {
181
+ toast.success(t('emailSenders.sendSuccess', { name: sender.sender }));
182
+ },
183
+ onError: (err: any) => {
184
+ toast.error(t('emailSenders.sendError', { name: sender.sender }) + ': ' + (err.response?.data?.message || err.message));
185
+ },
186
+ });
187
+
188
+ const formattedAmount = sender.totalAmount.toLocaleString('fr-CA', { style: 'currency', currency: 'CAD' });
189
+ const lastDate = sender.lastTransaction
190
+ ? new Date(sender.lastTransaction).toLocaleDateString('fr-CA')
191
+ : '—';
192
+
193
+ return (
194
+ <>
195
+ <tr
196
+ className="border-b border-border hover:bg-slate-50/50 cursor-pointer transition-colors group"
197
+ onClick={() => setIsExpanded(!isExpanded)}
198
+ >
199
+ <td className="py-3 px-6">
200
+ <div className="flex items-center gap-3">
201
+ <div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-bold">
202
+ {sender.sender.charAt(0).toUpperCase()}
203
+ </div>
204
+ <span className="font-semibold text-foreground">{sender.sender}</span>
205
+ </div>
206
+ </td>
207
+ <td className="py-3 px-4 text-center">
208
+ <span className="inline-flex items-center rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-semibold text-slate-700">
209
+ {sender.totalTransactions}
210
+ </span>
211
+ </td>
212
+ <td className="py-3 px-4 text-right font-mono font-semibold text-foreground">
213
+ {formattedAmount}
214
+ </td>
215
+ <td className="py-3 px-4 text-center text-muted-foreground text-xs">
216
+ <div className="flex items-center justify-center gap-1">
217
+ <Calendar className="h-3.5 w-3.5" />
218
+ {lastDate}
219
+ </div>
220
+ </td>
221
+ <td className="py-3 px-6 text-right">
222
+ {isExpanded ? (
223
+ <ChevronDown className="h-4 w-4 text-slate-400 inline" />
224
+ ) : (
225
+ <ChevronRight className="h-4 w-4 text-slate-300 group-hover:text-slate-500 transition-colors inline" />
226
+ )}
227
+ </td>
228
+ </tr>
229
+
230
+ {isExpanded && (
231
+ <tr className="bg-slate-50 border-b border-slate-200">
232
+ <td colSpan={5} className="p-0">
233
+ <div className="px-6 py-5 space-y-4">
234
+ {/* Send email form */}
235
+ <div className="flex items-center gap-3">
236
+ <div className="relative flex-1 max-w-md">
237
+ <Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
238
+ <input
239
+ type="email"
240
+ value={emailInput}
241
+ onChange={(e) => setEmailInput(e.target.value)}
242
+ onClick={(e) => e.stopPropagation()}
243
+ placeholder="email@example.com"
244
+ className="w-full pl-10 pr-4 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary"
245
+ />
246
+ </div>
247
+ <Button
248
+ size="sm"
249
+ onClick={(e) => {
250
+ e.stopPropagation();
251
+ if (emailInput.includes('@')) {
252
+ sendMutation.mutate(emailInput);
253
+ }
254
+ }}
255
+ disabled={!emailInput.includes('@') || sendMutation.isPending}
256
+ className="gap-2"
257
+ >
258
+ {sendMutation.isPending ? (
259
+ <Loader2 className="h-4 w-4 animate-spin" />
260
+ ) : sendMutation.isSuccess ? (
261
+ <CheckCircle className="h-4 w-4" />
262
+ ) : (
263
+ <Send className="h-4 w-4" />
264
+ )}
265
+ {sendMutation.isPending
266
+ ? t('emailSenders.sending')
267
+ : sendMutation.isSuccess
268
+ ? t('emailSenders.sent')
269
+ : t('emailSenders.sendHistory')}
270
+ </Button>
271
+ </div>
272
+
273
+ {/* Transaction list */}
274
+ {txLoading ? (
275
+ <div className="flex items-center justify-center py-6">
276
+ <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
277
+ </div>
278
+ ) : txData && txData.transactions.length > 0 ? (
279
+ <div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
280
+ <table className="w-full text-xs">
281
+ <thead>
282
+ <tr className="bg-slate-50 border-b border-slate-200">
283
+ <th className="text-left py-2 px-3 font-semibold text-slate-500 uppercase">Date</th>
284
+ <th className="text-right py-2 px-3 font-semibold text-slate-500 uppercase">{t('table.amount')}</th>
285
+ <th className="text-left py-2 px-3 font-semibold text-slate-500 uppercase">{t('table.reference')}</th>
286
+ <th className="text-left py-2 px-3 font-semibold text-slate-500 uppercase">{t('table.branch')}</th>
287
+ <th className="text-left py-2 px-3 font-semibold text-slate-500 uppercase">{t('table.status')}</th>
288
+ </tr>
289
+ </thead>
290
+ <tbody>
291
+ {txData.transactions.map((tx) => (
292
+ <tr key={tx.id} className="border-b border-slate-100 last:border-0">
293
+ <td className="py-2 px-3 text-slate-700">
294
+ {new Date(tx.date).toLocaleDateString('fr-CA')}
295
+ </td>
296
+ <td className="py-2 px-3 text-right font-mono font-semibold text-slate-900">
297
+ {(tx.amount || 0).toLocaleString('fr-CA', { style: 'currency', currency: 'CAD' })}
298
+ </td>
299
+ <td className="py-2 px-3 font-mono text-slate-500">
300
+ {tx.reference || '—'}
301
+ </td>
302
+ <td className="py-2 px-3 text-slate-600">
303
+ {tx.branch || '—'}
304
+ </td>
305
+ <td className="py-2 px-3">
306
+ <span className={cn(
307
+ 'inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-bold uppercase',
308
+ tx.status === 'deposited' ? 'bg-emerald-50 text-emerald-700' :
309
+ tx.status === 'pending' ? 'bg-amber-50 text-amber-700' :
310
+ tx.status === 'expired' ? 'bg-red-50 text-red-700' :
311
+ 'bg-slate-100 text-slate-600'
312
+ )}>
313
+ {t(`status.${tx.status}`, tx.status)}
314
+ </span>
315
+ </td>
316
+ </tr>
317
+ ))}
318
+ </tbody>
319
+ </table>
320
+ </div>
321
+ ) : null}
322
+ </div>
323
+ </td>
324
+ </tr>
325
+ )}
326
+ </>
327
+ );
328
+ }
packages/web/src/pages/JournalPage.tsx CHANGED
@@ -471,11 +471,11 @@ function ScanRow({
471
  </div>
472
  </div>
473
 
474
- {/* AI Info */}
475
  <div className="lg:col-span-5 flex flex-col gap-2">
476
  <h4 className="text-[11px] font-bold uppercase text-slate-400 tracking-wider flex items-center gap-2">
477
  <Zap className="h-3.5 w-3.5" />
478
- {t('journal.detail.aiInfo')}
479
  </h4>
480
  <div className="bg-white border border-slate-200 p-4 rounded-md shadow-sm h-full flex flex-col gap-4">
481
  <div>
 
471
  </div>
472
  </div>
473
 
474
+ {/* Engine Info */}
475
  <div className="lg:col-span-5 flex flex-col gap-2">
476
  <h4 className="text-[11px] font-bold uppercase text-slate-400 tracking-wider flex items-center gap-2">
477
  <Zap className="h-3.5 w-3.5" />
478
+ {t('journal.detail.engineInfo', 'Engine Info')}
479
  </h4>
480
  <div className="bg-white border border-slate-200 p-4 rounded-md shadow-sm h-full flex flex-col gap-4">
481
  <div>
packages/web/src/pages/ReportsPage.tsx CHANGED
@@ -1,15 +1,8 @@
1
  import { useTranslation } from 'react-i18next';
2
- import { HelpCircle } from 'lucide-react';
3
- import { Button } from '@/components/ui/button';
4
- import { useTransactionStats } from '@/hooks/useTransactions';
5
- import ReportFilters from '@/components/reports/ReportFilters';
6
- import ReportStats from '@/components/reports/ReportStats';
7
- import BranchChart from '@/components/reports/BranchChart';
8
- import ExportSection from '@/components/reports/ExportSection';
9
 
10
  export default function ReportsPage() {
11
  const { t } = useTranslation();
12
- const { data: stats, isLoading } = useTransactionStats();
13
 
14
  return (
15
  <div className="flex-1 flex flex-col h-full overflow-y-auto bg-background p-6 md:p-8 lg:p-10 relative">
@@ -24,33 +17,24 @@ export default function ReportsPage() {
24
  {t('reports.subtitle')}
25
  </p>
26
  </div>
27
- <div className="flex items-center gap-3">
28
- <Button variant="outline" size="sm" className="gap-2 shadow-sm">
29
- <HelpCircle className="h-4 w-4" />
30
- {t('reports.helpCenter')}
31
- </Button>
32
- </div>
33
  </div>
34
 
35
- {/* Filters */}
36
- <ReportFilters branches={stats?.byBranch?.map((b) => b.branch).filter(Boolean) || []} />
37
-
38
- {/* Stats + Chart */}
39
- <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
40
- <ReportStats
41
- totalAmount={stats?.totalAmount ?? 0}
42
- totalCount={stats?.totalCount ?? 0}
43
- byStatus={stats?.byStatus}
44
- isLoading={isLoading}
45
- />
46
- <BranchChart
47
- branches={stats?.byBranch ?? []}
48
- isLoading={isLoading}
49
- />
50
  </div>
51
-
52
- {/* Export Section */}
53
- <ExportSection />
54
  </div>
55
  </div>
56
  );
 
1
  import { useTranslation } from 'react-i18next';
2
+ import { BarChart3, Clock } from 'lucide-react';
 
 
 
 
 
 
3
 
4
  export default function ReportsPage() {
5
  const { t } = useTranslation();
 
6
 
7
  return (
8
  <div className="flex-1 flex flex-col h-full overflow-y-auto bg-background p-6 md:p-8 lg:p-10 relative">
 
17
  {t('reports.subtitle')}
18
  </p>
19
  </div>
 
 
 
 
 
 
20
  </div>
21
 
22
+ {/* Coming Soon */}
23
+ <div className="flex flex-col items-center justify-center py-24">
24
+ <div className="flex h-20 w-20 items-center justify-center rounded-2xl bg-primary/10 mb-6">
25
+ <BarChart3 className="h-10 w-10 text-primary" />
26
+ </div>
27
+ <h2 className="text-2xl font-bold text-foreground mb-2">
28
+ {t('reports.comingSoon', 'Coming Soon')}
29
+ </h2>
30
+ <p className="text-muted-foreground text-center max-w-md">
31
+ {t('reports.comingSoonDesc', 'This feature is currently under development. Stay tuned for advanced reporting and export capabilities.')}
32
+ </p>
33
+ <div className="flex items-center gap-2 mt-6 text-sm text-muted-foreground bg-slate-50 px-4 py-2 rounded-lg border border-slate-100">
34
+ <Clock className="h-4 w-4" />
35
+ <span>{t('reports.comingSoonStatus', 'Under development')}</span>
36
+ </div>
37
  </div>
 
 
 
38
  </div>
39
  </div>
40
  );
packages/web/src/pages/TransactionReviewPage.tsx CHANGED
@@ -3,7 +3,7 @@ import { Link, useParams } from 'react-router';
3
  import { Home, ChevronRight, Landmark, Loader2 } from 'lucide-react';
4
  import EmailSourcePane from '@/components/review/EmailSourcePane';
5
  import ValidationForm from '@/components/review/ValidationForm';
6
- import ChatAssistant from '@/components/chat/ChatAssistant';
7
  import { useTransaction } from '@/hooks/useTransactions';
8
 
9
  export default function TransactionReviewPage() {
@@ -147,7 +147,6 @@ export default function TransactionReviewPage() {
147
  </span>
148
  </div>
149
  </main>
150
- <ChatAssistant />
151
  </div>
152
  );
153
  }
 
3
  import { Home, ChevronRight, Landmark, Loader2 } from 'lucide-react';
4
  import EmailSourcePane from '@/components/review/EmailSourcePane';
5
  import ValidationForm from '@/components/review/ValidationForm';
6
+
7
  import { useTransaction } from '@/hooks/useTransactions';
8
 
9
  export default function TransactionReviewPage() {
 
147
  </span>
148
  </div>
149
  </main>
 
150
  </div>
151
  );
152
  }