4128c15bbb
Replace with generic 'AI', 'AI agent', or 'AI system' throughout. Keeps the underlying vendor implementation details off the UI. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
178 lines
8.7 KiB
Plaintext
178 lines
8.7 KiB
Plaintext
@{
|
||
ViewData["Title"] = "Anomaly Detection";
|
||
ViewData["PageIcon"] = "bi-shield-exclamation";
|
||
}
|
||
|
||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||
<div class="d-flex align-items-center gap-2">
|
||
<a asp-controller="Reports" asp-action="Landing" class="btn btn-sm btn-outline-secondary">
|
||
<i class="bi bi-arrow-left"></i>
|
||
</a>
|
||
<p class="text-muted mb-0 small">AI scans your recent bills and expense trends for duplicates, unusual amounts, and accounts running over budget.</p>
|
||
</div>
|
||
<button id="btnRunAnalysis" class="btn btn-warning text-dark">
|
||
<i class="bi bi-magic me-1"></i>Run Analysis
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Idle state -->
|
||
<div id="idleState" class="text-center py-5">
|
||
<div class="mb-3" style="font-size:3rem;color:var(--bs-border-color);">
|
||
<i class="bi bi-shield-exclamation"></i>
|
||
</div>
|
||
<h5 class="text-muted">No analysis run yet</h5>
|
||
<p class="text-muted small mb-3">Click <strong>Run Analysis</strong> to scan your bills and expense accounts for anomalies.</p>
|
||
<button class="btn btn-warning btn-sm text-dark" onclick="runAnalysis()">
|
||
<i class="bi bi-magic me-1"></i>Run Analysis
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Loading state -->
|
||
<div id="loadingState" class="text-center py-5 d-none">
|
||
<div class="spinner-border text-warning mb-3" role="status"></div>
|
||
<p class="text-muted">Scanning bills and expense accounts for anomalies…<br><small>This usually takes 5–10 seconds.</small></p>
|
||
</div>
|
||
|
||
<!-- Error state -->
|
||
<div id="errorState" class="d-none">
|
||
<div class="alert alert-danger alert-permanent d-flex align-items-center gap-2">
|
||
<i class="bi bi-exclamation-triangle-fill"></i>
|
||
<span id="errorMessage">An error occurred.</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Results -->
|
||
<div id="analysisResults" class="d-none">
|
||
|
||
<!-- Summary badges -->
|
||
<div class="d-flex align-items-center gap-3 mb-4 flex-wrap">
|
||
<div id="badgeCritical" class="d-flex align-items-center gap-2 px-3 py-2 rounded" style="background:#fef2f2;">
|
||
<i class="bi bi-exclamation-octagon-fill text-danger fs-5"></i>
|
||
<div>
|
||
<span id="cntCritical" class="fw-bold fs-5 text-danger">0</span>
|
||
<span class="text-danger small ms-1">Critical</span>
|
||
</div>
|
||
</div>
|
||
<div class="d-flex align-items-center gap-2 px-3 py-2 rounded" style="background:#fffbeb;">
|
||
<i class="bi bi-exclamation-triangle-fill text-warning fs-5"></i>
|
||
<div>
|
||
<span id="cntWarning" class="fw-bold fs-5 text-warning">0</span>
|
||
<span class="text-warning small ms-1">Warnings</span>
|
||
</div>
|
||
</div>
|
||
<div class="d-flex align-items-center gap-2 px-3 py-2 rounded" style="background:#eff6ff;">
|
||
<i class="bi bi-info-circle-fill text-info fs-5"></i>
|
||
<div>
|
||
<span id="cntInfo" class="fw-bold fs-5 text-info">0</span>
|
||
<span class="text-info small ms-1">Info</span>
|
||
</div>
|
||
</div>
|
||
<div id="allClearBadge" class="d-none d-flex align-items-center gap-2 px-3 py-2 rounded" style="background:#f0fdf4;">
|
||
<i class="bi bi-shield-check text-success fs-5"></i>
|
||
<span class="text-success fw-semibold">All Clear — no anomalies detected</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Flags list -->
|
||
<div id="flagsList"></div>
|
||
|
||
<div class="text-muted small text-end mt-3">
|
||
<i class="bi bi-robot me-1"></i>Generated by AI · <span id="analysisTimestamp"></span>
|
||
· <a href="#" onclick="runAnalysis(); return false;">Re-run</a>
|
||
</div>
|
||
</div>
|
||
|
||
@section Scripts {
|
||
<script>
|
||
document.getElementById('btnRunAnalysis').addEventListener('click', runAnalysis);
|
||
|
||
const typeLabels = {
|
||
duplicate: { label: 'Duplicate Bill', icon: 'bi-files', color: 'danger' },
|
||
amount_spike: { label: 'Amount Spike', icon: 'bi-graph-up-arrow', color: 'warning' },
|
||
unusual_vendor: { label: 'Unusual Vendor', icon: 'bi-person-exclamation', color: 'warning' },
|
||
account_overrun: { label: 'Account Over Budget', icon: 'bi-bar-chart-fill', color: 'info' }
|
||
};
|
||
|
||
const severityConfig = {
|
||
critical: { cls: 'border-danger', headerCls: 'bg-danger text-white', badgeCls: 'bg-danger' },
|
||
warning: { cls: 'border-warning', headerCls: 'bg-warning text-dark', badgeCls: 'bg-warning text-dark' },
|
||
info: { cls: 'border-info', headerCls: 'bg-info text-white', badgeCls: 'bg-info' }
|
||
};
|
||
|
||
async function runAnalysis() {
|
||
document.getElementById('idleState').classList.add('d-none');
|
||
document.getElementById('loadingState').classList.remove('d-none');
|
||
document.getElementById('errorState').classList.add('d-none');
|
||
document.getElementById('analysisResults').classList.add('d-none');
|
||
|
||
try {
|
||
const resp = await fetch('/Reports/RunAnomalyDetection', {
|
||
method: 'POST',
|
||
headers: { 'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? '' }
|
||
});
|
||
const data = await resp.json();
|
||
|
||
document.getElementById('loadingState').classList.add('d-none');
|
||
|
||
if (!data.success) {
|
||
document.getElementById('errorMessage').textContent = data.errorMessage || 'An error occurred.';
|
||
document.getElementById('errorState').classList.remove('d-none');
|
||
return;
|
||
}
|
||
|
||
document.getElementById('cntCritical').textContent = data.criticalCount;
|
||
document.getElementById('cntWarning').textContent = data.warningCount;
|
||
document.getElementById('cntInfo').textContent = data.infoCount;
|
||
|
||
const totalFlags = (data.flags || []).length;
|
||
document.getElementById('allClearBadge').classList.toggle('d-none', totalFlags > 0);
|
||
document.getElementById('badgeCritical').parentElement.querySelectorAll(':scope > div:not(#allClearBadge)')
|
||
.forEach(el => el.classList.toggle('d-none', totalFlags === 0));
|
||
|
||
const container = document.getElementById('flagsList');
|
||
container.innerHTML = '';
|
||
|
||
// Sort: critical first, then warning, then info
|
||
const sorted = [...(data.flags || [])].sort((a, b) => {
|
||
const order = { critical: 0, warning: 1, info: 2 };
|
||
return (order[a.severity] ?? 3) - (order[b.severity] ?? 3);
|
||
});
|
||
|
||
sorted.forEach(flag => {
|
||
const typeInfo = typeLabels[flag.type] || { label: flag.type, icon: 'bi-exclamation-circle', color: 'secondary' };
|
||
const sev = severityConfig[flag.severity] || severityConfig.warning;
|
||
|
||
const card = document.createElement('div');
|
||
card.className = `card mb-3 border ${sev.cls}`;
|
||
card.innerHTML = `
|
||
<div class="card-header d-flex align-items-center gap-2 ${sev.headerCls} py-2">
|
||
<i class="bi ${typeInfo.icon}"></i>
|
||
<span class="fw-semibold">${flag.title}</span>
|
||
<span class="badge ${sev.badgeCls} ms-auto text-uppercase" style="font-size:0.65rem;">${flag.severity}</span>
|
||
<span class="badge bg-light text-dark" style="font-size:0.65rem;">${typeInfo.label}</span>
|
||
${flag.billNumber ? `<span class="badge bg-secondary ms-1" style="font-size:0.65rem;">${flag.billNumber}</span>` : ''}
|
||
</div>
|
||
<div class="card-body py-2">
|
||
<p class="mb-1">${flag.description}</p>
|
||
${flag.recommendedAction ? `
|
||
<div class="d-flex align-items-start gap-2 mt-2 p-2 rounded" style="background:var(--bs-tertiary-bg);">
|
||
<i class="bi bi-arrow-right-circle-fill text-primary mt-1 flex-shrink-0"></i>
|
||
<span class="small"><strong>Recommended:</strong> ${flag.recommendedAction}</span>
|
||
</div>` : ''}
|
||
</div>`;
|
||
container.appendChild(card);
|
||
});
|
||
|
||
document.getElementById('analysisTimestamp').textContent = new Date().toLocaleString();
|
||
document.getElementById('analysisResults').classList.remove('d-none');
|
||
|
||
} catch (e) {
|
||
document.getElementById('loadingState').classList.add('d-none');
|
||
document.getElementById('errorMessage').textContent = 'Network error — please try again.';
|
||
document.getElementById('errorState').classList.remove('d-none');
|
||
}
|
||
}
|
||
</script>
|
||
@Html.AntiForgeryToken()
|
||
}
|