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,234 @@
@{
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 Claude 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()
}