chefcodeocr / static /index.html
Mariem-Daha's picture
Upload 22 files
8c33e8e verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invoice OCR System</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 50%, #2563eb 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
header {
text-align: center;
color: white;
margin-bottom: 30px;
}
.logo {
width: 120px;
height: 120px;
margin: 0 auto 20px;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
h1 {
font-size: 2.5em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
.subtitle {
font-size: 1.1em;
opacity: 0.9;
}
.card {
background: white;
border-radius: 15px;
padding: 30px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.upload-zone {
border: 3px dashed #3b82f6;
border-radius: 10px;
padding: 60px 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
background: #eff6ff;
}
.upload-zone:hover {
border-color: #1e40af;
background: #dbeafe;
transform: translateY(-2px);
}
.upload-zone.drag-over {
border-color: #1e3a8a;
background: #bfdbfe;
}
.upload-icon {
font-size: 48px;
margin-bottom: 15px;
}
.upload-text {
font-size: 1.2em;
color: #1e40af;
font-weight: 600;
margin-bottom: 8px;
}
.upload-subtext {
color: #666;
font-size: 0.9em;
}
#file-input {
display: none;
}
.btn {
background: linear-gradient(135deg, #1e3a8a 0%, #2563eb 100%);
color: white;
border: none;
padding: 12px 30px;
border-radius: 8px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-block;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(37, 99, 235, 0.4);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.loading {
display: none;
text-align: center;
padding: 40px;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #2563eb;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.results {
display: none;
}
.section-title {
font-size: 1.3em;
color: #333;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #2563eb;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
margin-bottom: 25px;
}
.info-item {
background: #eff6ff;
padding: 15px;
border-radius: 8px;
border-left: 4px solid #2563eb;
}
.info-label {
font-size: 0.85em;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 5px;
}
.info-value {
font-size: 1.1em;
color: #333;
font-weight: 600;
}
.line-items-table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
.line-items-table th {
background: #1e40af;
color: white;
padding: 12px;
text-align: left;
font-weight: 600;
}
.line-items-table td {
padding: 12px;
border-bottom: 1px solid #e0e0e0;
}
.line-items-table tr:hover {
background: #eff6ff;
}
.cost-breakdown {
background: linear-gradient(135deg, #1e3a8a 0%, #2563eb 100%);
color: white;
padding: 20px;
border-radius: 10px;
margin-top: 20px;
}
.cost-item {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid rgba(255,255,255,0.2);
}
.cost-item:last-child {
border-bottom: none;
font-size: 1.2em;
font-weight: 700;
padding-top: 12px;
}
.invoice-list {
margin-top: 30px;
}
.invoice-card {
background: #eff6ff;
padding: 20px;
border-radius: 10px;
margin-bottom: 15px;
border-left: 5px solid #2563eb;
cursor: pointer;
transition: all 0.3s ease;
}
.invoice-card:hover {
background: #dbeafe;
transform: translateX(5px);
}
.invoice-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.invoice-number {
font-size: 1.2em;
font-weight: 700;
color: #1e40af;
}
.invoice-amount {
font-size: 1.3em;
font-weight: 700;
color: #1e3a8a;
}
.invoice-meta {
display: flex;
gap: 20px;
font-size: 0.9em;
color: #666;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: linear-gradient(135deg, #1e3a8a 0%, #2563eb 100%);
color: white;
padding: 25px;
border-radius: 12px;
text-align: center;
}
.stat-value {
font-size: 2.5em;
font-weight: 700;
margin-bottom: 5px;
}
.stat-label {
font-size: 0.9em;
opacity: 0.9;
}
.empty-state {
text-align: center;
padding: 40px;
color: #999;
}
.delete-btn {
background: #dc3545;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 0.9em;
transition: all 0.3s ease;
}
.delete-btn:hover {
background: #c82333;
}
.view-btn {
background: #2563eb;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 0.9em;
transition: all 0.3s ease;
margin-left: 10px;
}
.view-btn:hover {
background: #1e40af;
}
.invoice-card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.invoice-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0,0,0,0.15);
}
.alert {
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
}
.alert-success {
background: #d4edda;
color: #155724;
border-left: 5px solid #28a745;
}
.alert-error {
background: #f8d7da;
color: #721c24;
border-left: 5px solid #dc3545;
}
</style>
</head>
<body>
<div class="container">
<header>
<img src="/static/logo.svg" alt="Invoice OCR Logo" class="logo">
<h1>📄 Invoice OCR System</h1>
<p class="subtitle">Powered by Google Document AI + Gemini 2.0 Flash</p>
</header>
<div id="alert-success" class="alert alert-success"></div>
<div id="alert-error" class="alert alert-error"></div>
<!-- Upload Section -->
<div class="card">
<div class="upload-zone" id="upload-zone">
<div class="upload-icon">📤</div>
<div class="upload-text">Drop invoice here or click to upload</div>
<div class="upload-subtext">Supports JPG, PNG, PDF</div>
<input type="file" id="file-input" accept="image/*,.pdf">
</div>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Processing invoice with AI...</p>
<p style="color: #666; font-size: 0.9em; margin-top: 10px;">This may take 2-4 seconds</p>
</div>
</div>
<!-- Results Section -->
<div id="results" class="results">
<div class="card">
<h2 class="section-title">✅ Invoice Processed Successfully</h2>
<!-- Supplier Information -->
<h3 class="section-title" style="margin-top: 25px;">📦 Supplier Information</h3>
<div class="info-grid" id="supplier-info"></div>
<!-- Customer Information -->
<h3 class="section-title" style="margin-top: 25px;">👤 Customer Information</h3>
<div class="info-grid" id="customer-info"></div>
<!-- Invoice Details -->
<h3 class="section-title" style="margin-top: 25px;">📋 Invoice Details</h3>
<div class="info-grid" id="invoice-details"></div>
<!-- Line Items -->
<h3 class="section-title" style="margin-top: 25px;">📊 Line Items</h3>
<div style="overflow-x: auto;">
<table class="line-items-table" id="line-items-table">
<thead>
<tr>
<th>Item Code</th>
<th>Description</th>
<th>Type</th>
<th>Quantity</th>
<th>Unit</th>
<th>Unit Price</th>
<th>Total</th>
</tr>
</thead>
<tbody id="line-items-body"></tbody>
</table>
</div>
<!-- Financial Summary -->
<h3 class="section-title" style="margin-top: 25px;">💰 Financial Summary</h3>
<div class="info-grid" id="financial-summary"></div>
<!-- Payment Information -->
<h3 class="section-title" style="margin-top: 25px;">🏦 Payment Information</h3>
<div class="info-grid" id="payment-info"></div>
<!-- Cost Breakdown -->
<div class="cost-breakdown">
<h3 style="margin-bottom: 15px;">💵 Processing Cost</h3>
<div class="cost-item">
<span>Document AI OCR:</span>
<span id="cost-docai">$0.00000</span>
</div>
<div class="cost-item">
<span>Gemini Input (<span id="tokens-input">0</span> tokens):</span>
<span id="cost-gemini-input">$0.00000</span>
</div>
<div class="cost-item">
<span>Gemini Output (<span id="tokens-output">0</span> tokens):</span>
<span id="cost-gemini-output">$0.00000</span>
</div>
<div class="cost-item">
<span>Total Processing Cost:</span>
<span id="cost-total">$0.00000</span>
</div>
</div>
<div style="text-align: center; margin-top: 20px;">
<button class="btn" onclick="resetUpload()">Process Another Invoice</button>
</div>
</div>
</div>
<!-- Statistics -->
<div class="card">
<h2 class="section-title">📈 Statistics</h2>
<div class="stats-grid" id="stats-grid">
<div class="stat-card">
<div class="stat-value" id="stat-total">0</div>
<div class="stat-label">Total Invoices</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-cost">$0.00</div>
<div class="stat-label">Total Cost</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-avg">$0.00</div>
<div class="stat-label">Average Cost</div>
</div>
</div>
</div>
<!-- Invoice History -->
<div class="card">
<h2 class="section-title">📚 Invoice History</h2>
<div id="invoice-list" class="invoice-list"></div>
</div>
</div>
<script>
const uploadZone = document.getElementById('upload-zone');
const fileInput = document.getElementById('file-input');
const loading = document.getElementById('loading');
const results = document.getElementById('results');
// Drag and drop handlers
uploadZone.addEventListener('click', () => fileInput.click());
uploadZone.addEventListener('dragover', (e) => {
e.preventDefault();
uploadZone.classList.add('drag-over');
});
uploadZone.addEventListener('dragleave', () => {
uploadZone.classList.remove('drag-over');
});
uploadZone.addEventListener('drop', (e) => {
e.preventDefault();
uploadZone.classList.remove('drag-over');
const file = e.dataTransfer.files[0];
if (file) handleFileUpload(file);
});
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) handleFileUpload(file);
});
async function handleFileUpload(file) {
const formData = new FormData();
formData.append('file', file);
uploadZone.style.display = 'none';
loading.style.display = 'block';
results.style.display = 'none';
hideAlerts();
try {
const response = await fetch('/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
if (response.ok) {
displayResults(data);
showAlert('Invoice processed successfully!', 'success');
loadInvoices();
loadStats();
} else {
throw new Error(data.detail || 'Processing failed');
}
} catch (error) {
showAlert(`Error: ${error.message}`, 'error');
resetUpload();
}
}
function displayResults(data) {
loading.style.display = 'none';
results.style.display = 'block';
// Supplier Information
const supplierInfo = document.getElementById('supplier-info');
supplierInfo.innerHTML = '';
if (data.invoice.supplier_data) {
const supplier = JSON.parse(data.invoice.supplier_data);
addInfoItem(supplierInfo, 'Company Name', supplier.name);
addInfoItem(supplierInfo, 'Address', supplier.address);
addInfoItem(supplierInfo, 'Phone', supplier.phone);
addInfoItem(supplierInfo, 'Email', supplier.email);
addInfoItem(supplierInfo, 'Tax ID', supplier.tax_id);
}
// Customer Information
const customerInfo = document.getElementById('customer-info');
customerInfo.innerHTML = '';
if (data.invoice.customer_data) {
const customer = JSON.parse(data.invoice.customer_data);
addInfoItem(customerInfo, 'Company Name', customer.name);
addInfoItem(customerInfo, 'Address', customer.address);
addInfoItem(customerInfo, 'Phone', customer.phone);
addInfoItem(customerInfo, 'Email', customer.email);
}
// Invoice Details
const invoiceDetails = document.getElementById('invoice-details');
invoiceDetails.innerHTML = '';
if (data.invoice.invoice_details) {
const details = JSON.parse(data.invoice.invoice_details);
addInfoItem(invoiceDetails, 'Invoice Number', details.invoice_number);
addInfoItem(invoiceDetails, 'Invoice Date', details.invoice_date);
addInfoItem(invoiceDetails, 'Due Date', details.due_date);
addInfoItem(invoiceDetails, 'PO Number', details.po_number);
addInfoItem(invoiceDetails, 'Payment Terms', details.payment_terms);
}
// Line Items
const lineItemsBody = document.getElementById('line-items-body');
lineItemsBody.innerHTML = '';
const currency = data.invoice.currency || 'EUR';
if (data.invoice.line_items) {
const items = JSON.parse(data.invoice.line_items);
items.forEach(item => {
const row = document.createElement('tr');
// Format type with color coding
let typeHtml = '-';
if (item.type) {
const typeColors = {
'produce': '#10b981',
'protein': '#ef4444',
'beverage': '#3b82f6',
'dairy': '#f59e0b',
'grain': '#8b5cf6',
'condiment': '#f97316',
'cleaning': '#06b6d4',
'packaging': '#6b7280',
'miscellaneous': '#9ca3af'
};
const color = typeColors[item.type] || '#6b7280';
typeHtml = `<span style="background: ${color}; color: white; padding: 4px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 600;">${item.type.toUpperCase()}</span>`;
}
row.innerHTML = `
<td>${item.item_code || '-'}</td>
<td>${item.description || '-'}</td>
<td>${typeHtml}</td>
<td>${item.quantity || '-'}</td>
<td>${item.unit || '-'}</td>
<td>${item.unit_price ? currency + ' ' + item.unit_price.toFixed(2) : '-'}</td>
<td><strong>${item.total_price ? currency + ' ' + item.total_price.toFixed(2) : '-'}</strong></td>
`;
lineItemsBody.appendChild(row);
});
}
// Financial Summary
const financialSummary = document.getElementById('financial-summary');
financialSummary.innerHTML = '';
if (data.invoice.financial_summary) {
const summary = JSON.parse(data.invoice.financial_summary);
const currencySymbol = summary.currency || currency || 'EUR';
addInfoItem(financialSummary, 'Subtotal', summary.subtotal ? currencySymbol + ' ' + summary.subtotal.toFixed(2) : '-');
addInfoItem(financialSummary, 'Tax Amount', summary.tax_amount ? currencySymbol + ' ' + summary.tax_amount.toFixed(2) : '-');
addInfoItem(financialSummary, 'Total Amount', summary.total_amount ? currencySymbol + ' ' + summary.total_amount.toFixed(2) : '-');
addInfoItem(financialSummary, 'Currency', currencySymbol);
} // Payment Information
const paymentInfo = document.getElementById('payment-info');
paymentInfo.innerHTML = '';
if (data.invoice.payment_info) {
const payment = JSON.parse(data.invoice.payment_info);
addInfoItem(paymentInfo, 'Bank Name', payment.bank_name);
addInfoItem(paymentInfo, 'Account Number', payment.account_number);
addInfoItem(paymentInfo, 'IBAN', payment.iban);
addInfoItem(paymentInfo, 'SWIFT/BIC', payment.swift_code);
}
// Cost Breakdown
document.getElementById('cost-docai').textContent = '$' + data.costs.document_ai_cost.toFixed(6);
document.getElementById('tokens-input').textContent = data.costs.gemini_input_tokens;
document.getElementById('cost-gemini-input').textContent = '$' + data.costs.gemini_input_cost.toFixed(6);
document.getElementById('tokens-output').textContent = data.costs.gemini_output_tokens;
document.getElementById('cost-gemini-output').textContent = '$' + data.costs.gemini_output_cost.toFixed(6);
document.getElementById('cost-total').textContent = '$' + data.costs.total_cost.toFixed(6);
}
function addInfoItem(container, label, value) {
// Show "N/A" for missing values instead of hiding the field
const displayValue = value || 'N/A';
const item = document.createElement('div');
item.className = 'info-item';
item.innerHTML = `
<div class="info-label">${label}</div>
<div class="info-value">${displayValue}</div>
`;
container.appendChild(item);
}
async function loadInvoices() {
try {
const response = await fetch('/invoices');
const invoices = await response.json();
const invoiceList = document.getElementById('invoice-list');
invoiceList.innerHTML = '';
if (invoices.length === 0) {
invoiceList.innerHTML = '<div class="empty-state">No invoices processed yet</div>';
return;
}
invoices.forEach(invoice => {
const details = JSON.parse(invoice.invoice_details);
const summary = JSON.parse(invoice.financial_summary);
const supplier = JSON.parse(invoice.supplier_data);
const currency = invoice.currency || summary.currency || 'EUR';
const card = document.createElement('div');
card.className = 'invoice-card';
card.style.cursor = 'pointer';
card.innerHTML = `
<div class="invoice-header">
<div class="invoice-number">${supplier.name || 'Unknown Supplier'}</div>
<div class="invoice-amount">${currency} ${summary.total_amount ? summary.total_amount.toFixed(2) : '0.00'}</div>
</div>
<div class="invoice-meta">
<span>� ${details.invoice_number || 'N/A'}</span>
<span>📅 ${details.invoice_date || 'N/A'}</span>
</div>
<div style="margin-top: 10px;">
<button class="delete-btn" id="delete-${invoice.id}">Delete</button>
<button class="view-btn" id="view-${invoice.id}">View Details</button>
</div>
`;
invoiceList.appendChild(card);
// Add event listeners after appending to DOM
document.getElementById(`delete-${invoice.id}`).addEventListener('click', (e) => {
e.stopPropagation();
deleteInvoice(invoice.id);
});
document.getElementById(`view-${invoice.id}`).addEventListener('click', (e) => {
e.stopPropagation();
viewInvoice(invoice.id);
});
// Make card clickable to view details
card.onclick = () => viewInvoice(invoice.id);
});
} catch (error) {
console.error('Error loading invoices:', error);
}
}
async function loadStats() {
try {
const response = await fetch('/stats');
const stats = await response.json();
document.getElementById('stat-total').textContent = stats.total_invoices;
document.getElementById('stat-cost').textContent = '$' + stats.total_processing_cost.toFixed(4);
document.getElementById('stat-avg').textContent = '$' + stats.average_cost_per_invoice.toFixed(6);
} catch (error) {
console.error('Error loading stats:', error);
}
}
async function viewInvoice(id) {
try {
const response = await fetch(`/invoices/${id}`);
const data = await response.json();
displayResults(data);
// Scroll to results
document.getElementById('results').scrollIntoView({ behavior: 'smooth' });
showAlert('Invoice loaded successfully', 'success');
} catch (error) {
showAlert(`Error: ${error.message}`, 'error');
}
}
async function deleteInvoice(id) {
if (!confirm('Are you sure you want to delete this invoice?')) return;
try {
const response = await fetch(`/invoices/${id}`, { method: 'DELETE' });
if (response.ok) {
showAlert('Invoice deleted successfully', 'success');
loadInvoices();
loadStats();
} else {
throw new Error('Delete failed');
}
} catch (error) {
showAlert(`Error: ${error.message}`, 'error');
}
}
function resetUpload() {
uploadZone.style.display = 'block';
loading.style.display = 'none';
results.style.display = 'none';
fileInput.value = '';
hideAlerts();
}
function showAlert(message, type) {
hideAlerts();
const alert = document.getElementById(`alert-${type}`);
alert.textContent = message;
alert.style.display = 'block';
setTimeout(() => hideAlerts(), 5000);
}
function hideAlerts() {
document.getElementById('alert-success').style.display = 'none';
document.getElementById('alert-error').style.display = 'none';
}
// Load initial data
loadInvoices();
loadStats();
</script>
</body>
</html>