Initial commit

This commit is contained in:
2026-04-23 21:38:24 -04:00
commit 63e12a9636
1762 changed files with 1672620 additions and 0 deletions
@@ -0,0 +1,177 @@
@{
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 510 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 Claude AI &middot; <span id="analysisTimestamp"></span>
&middot; <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()
}