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>
235 lines
11 KiB
Plaintext
235 lines
11 KiB
Plaintext
@{
|
||
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 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="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 · <span id="forecastTimestamp"></span>
|
||
· <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()
|
||
}
|