Files
PowderCoatingLogix/src/PowderCoating.Web/Views/Reports/AnomalyDetection.cshtml
T
spouliot 4128c15bbb Remove 'Claude' brand references from all user-facing views
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>
2026-04-25 22:51:36 -04:00

178 lines
8.7 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@{
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 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()
}