Spaces:
Running
Running
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 +2 -2
- packages/server/src/routes/senders.ts +190 -0
- packages/web/src/App.tsx +2 -0
- packages/web/src/components/layout/MainLayout.tsx +0 -2
- packages/web/src/components/layout/Sidebar.tsx +2 -0
- packages/web/src/components/review/ValidationForm.tsx +3 -3
- packages/web/src/components/scan/ScanModal.tsx +3 -3
- packages/web/src/i18n/locales/en.json +41 -21
- packages/web/src/i18n/locales/fr.json +41 -21
- packages/web/src/pages/EmailSendersPage.tsx +328 -0
- packages/web/src/pages/JournalPage.tsx +2 -2
- packages/web/src/pages/ReportsPage.tsx +16 -32
- packages/web/src/pages/TransactionReviewPage.tsx +1 -2
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
|
| 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/
|
| 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,
|
| 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 |
-
<
|
| 123 |
-
<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.
|
| 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 |
-
{/*
|
| 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.
|
| 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
|
| 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
|
| 193 |
-
"
|
| 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 |
-
"
|
| 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": "
|
| 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 |
-
"
|
| 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 |
-
"
|
| 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,
|
| 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 |
-
"
|
| 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
|
| 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
|
| 193 |
-
"
|
| 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 |
-
"
|
| 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
|
| 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 |
-
"
|
| 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 |
-
"
|
| 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
|
| 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 |
-
"
|
| 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 |
-
{/*
|
| 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.
|
| 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 {
|
| 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 |
-
{/*
|
| 36 |
-
<
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
<
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
/>
|
| 46 |
-
<
|
| 47 |
-
|
| 48 |
-
|
| 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 |
-
|
| 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 |
}
|