Files
PowderCoatingLogix/src/PowderCoating.Web/Views/Reports/CashFlowForecast.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

235 lines
11 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"] = "Cash Flow Forecast";
ViewData["PageIcon"] = "bi-cash-stack";
}
<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-projected 30/60/90-day cash position based on open invoices, outstanding bills, and your job pipeline.</p>
</div>
<button id="btnRunForecast" class="btn btn-success">
<i class="bi bi-magic me-1"></i>Generate Forecast
</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-cash-stack"></i>
</div>
<h5 class="text-muted">No forecast generated yet</h5>
<p class="text-muted small mb-3">Click <strong>Generate Forecast</strong> to analyse your open invoices, outstanding bills, and active job pipeline.</p>
<button class="btn btn-success btn-sm" onclick="runForecast()">
<i class="bi bi-magic me-1"></i>Generate Forecast
</button>
</div>
<!-- Loading state -->
<div id="loadingState" class="text-center py-5 d-none">
<div class="spinner-border text-success mb-3" role="status"></div>
<p class="text-muted">Analysing your AR, AP, and job pipeline…<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="forecastResults" class="d-none">
<!-- Outlook banner -->
<div id="outlookBanner" class="alert alert-permanent d-flex align-items-center gap-3 mb-4">
<div id="outlookIcon" style="font-size:1.8rem;"></div>
<div>
<div id="outlookTitle" class="fw-bold fs-5"></div>
<div id="outlookSub" class="small"></div>
</div>
</div>
<!-- Period cards -->
<div class="row g-3 mb-4">
<div class="col-md-4">
<div class="card h-100">
<div class="card-header fw-semibold d-flex align-items-center gap-2">
<i class="bi bi-calendar-event text-primary"></i> Next 30 Days
</div>
<div class="card-body">
<div class="row text-center g-2 mb-3">
<div class="col-4">
<div class="small text-muted">Inflows</div>
<div id="in30" class="fw-bold text-success fs-6"></div>
</div>
<div class="col-4">
<div class="small text-muted">Outflows</div>
<div id="out30" class="fw-bold text-danger fs-6"></div>
</div>
<div class="col-4">
<div class="small text-muted">Net</div>
<div id="net30" class="fw-bold fs-6"></div>
</div>
</div>
<ul id="items30" class="list-unstyled mb-0" style="font-size:0.8rem;"></ul>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100">
<div class="card-header fw-semibold d-flex align-items-center gap-2">
<i class="bi bi-calendar2-event text-primary"></i> Next 60 Days
</div>
<div class="card-body">
<div class="row text-center g-2 mb-3">
<div class="col-4">
<div class="small text-muted">Inflows</div>
<div id="in60" class="fw-bold text-success fs-6"></div>
</div>
<div class="col-4">
<div class="small text-muted">Outflows</div>
<div id="out60" class="fw-bold text-danger fs-6"></div>
</div>
<div class="col-4">
<div class="small text-muted">Net</div>
<div id="net60" class="fw-bold fs-6"></div>
</div>
</div>
<ul id="items60" class="list-unstyled mb-0" style="font-size:0.8rem;"></ul>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100">
<div class="card-header fw-semibold d-flex align-items-center gap-2">
<i class="bi bi-calendar3-event text-primary"></i> Next 90 Days
</div>
<div class="card-body">
<div class="row text-center g-2 mb-3">
<div class="col-4">
<div class="small text-muted">Inflows</div>
<div id="in90" class="fw-bold text-success fs-6"></div>
</div>
<div class="col-4">
<div class="small text-muted">Outflows</div>
<div id="out90" class="fw-bold text-danger fs-6"></div>
</div>
<div class="col-4">
<div class="small text-muted">Net</div>
<div id="net90" class="fw-bold fs-6"></div>
</div>
</div>
<ul id="items90" class="list-unstyled mb-0" style="font-size:0.8rem;"></ul>
</div>
</div>
</div>
</div>
<!-- Insights -->
<div class="card mb-4">
<div class="card-header fw-semibold d-flex align-items-center gap-2">
<i class="bi bi-lightbulb text-warning"></i> AI Insights
</div>
<div class="card-body">
<ul id="insightsList" class="mb-0" style="line-height:1.8;"></ul>
</div>
</div>
<div class="text-muted small text-end">
<i class="bi bi-robot me-1"></i>Generated by AI &middot; <span id="forecastTimestamp"></span>
&middot; <a href="#" onclick="runForecast(); return false;">Refresh</a>
</div>
</div>
@section Scripts {
<script>
document.getElementById('btnRunForecast').addEventListener('click', runForecast);
function fmt(n) {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(n);
}
function setNetColor(el, val) {
el.textContent = fmt(val);
el.className = 'fw-bold fs-6 ' + (val >= 0 ? 'text-success' : 'text-danger');
}
function fillPeriod(suffix, period) {
document.getElementById('in' + suffix).textContent = fmt(period.expectedInflows);
document.getElementById('out' + suffix).textContent = fmt(period.expectedOutflows);
setNetColor(document.getElementById('net' + suffix), period.netCashFlow);
const ul = document.getElementById('items' + suffix);
ul.innerHTML = '';
(period.keyItems || []).forEach(item => {
const li = document.createElement('li');
li.className = 'text-muted mb-1';
li.innerHTML = '<i class="bi bi-dot"></i>' + item;
ul.appendChild(li);
});
}
async function runForecast() {
document.getElementById('idleState').classList.add('d-none');
document.getElementById('loadingState').classList.remove('d-none');
document.getElementById('errorState').classList.add('d-none');
document.getElementById('forecastResults').classList.add('d-none');
try {
const resp = await fetch('/Reports/GenerateCashFlowForecast', {
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;
}
// Outlook banner
const outlookMap = {
strong: { cls: 'alert-success', icon: '💪', title: 'Strong Cash Position', sub: 'Your projected cash flow looks healthy across all three periods.' },
moderate: { cls: 'alert-info', icon: '👍', title: 'Moderate Cash Position', sub: 'Cash flow looks manageable — a few items to watch.' },
tight: { cls: 'alert-warning', icon: '⚠️', title: 'Tight Cash Position', sub: 'Cash flow may be constrained — consider following up on open invoices.' },
concerning: { cls: 'alert-danger', icon: '🚨', title: 'Concerning Cash Position', sub: 'Projected cash flow is under pressure — immediate action may be needed.' }
};
const outlook = outlookMap[data.outlook] || outlookMap.moderate;
const banner = document.getElementById('outlookBanner');
banner.className = 'alert alert-permanent d-flex align-items-center gap-3 mb-4 ' + outlook.cls;
document.getElementById('outlookIcon').textContent = outlook.icon;
document.getElementById('outlookTitle').textContent = outlook.title;
document.getElementById('outlookSub').textContent = outlook.sub;
fillPeriod('30', data.next30Days);
fillPeriod('60', data.next60Days);
fillPeriod('90', data.next90Days);
const ul = document.getElementById('insightsList');
ul.innerHTML = '';
(data.insights || []).forEach(ins => {
const li = document.createElement('li');
li.className = 'mb-2';
li.innerHTML = '<i class="bi bi-check-circle-fill text-success me-2"></i>' + ins;
ul.appendChild(li);
});
document.getElementById('forecastTimestamp').textContent = new Date().toLocaleString();
document.getElementById('forecastResults').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()
}