367 lines
15 KiB
HTML
367 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" data-bs-theme="light">
|
|
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="description" content="Personal finance dashboard showing income, expenses, and transactions">
|
|
<title>FinanceTracker | Dashboard</title>
|
|
|
|
<!-- Bootstrap -->
|
|
<link rel="stylesheet" th:href="@{/assets/bootstrap/css/bootstrap.min.css}">
|
|
|
|
<!-- Material Icons -->
|
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
|
|
|
<style>
|
|
:root {
|
|
--positive-bg: rgba(40, 167, 69, 0.12);
|
|
--negative-bg: rgba(220, 53, 69, 0.12);
|
|
}
|
|
|
|
body {
|
|
background-color: #f8f9fc;
|
|
}
|
|
|
|
.txn-positive td {
|
|
background-color: var(--positive-bg);
|
|
}
|
|
|
|
.txn-negative td {
|
|
background-color: var(--negative-bg);
|
|
}
|
|
|
|
.numeric {
|
|
text-align: right;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.chart-area {
|
|
position: relative;
|
|
min-height: 260px;
|
|
}
|
|
|
|
.card {
|
|
border: none;
|
|
border-radius: 0.75rem;
|
|
}
|
|
|
|
.card-header {
|
|
background: transparent;
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.material-icons {
|
|
vertical-align: middle;
|
|
font-size: 20px;
|
|
}
|
|
|
|
.insight-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.75rem;
|
|
border-radius: 0.5rem;
|
|
background-color: #f8f9fc;
|
|
}
|
|
|
|
.insight-item+.insight-item {
|
|
margin-top: 0.75rem;
|
|
}
|
|
|
|
.insight-icon {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background-color: #e9ecef;
|
|
color: #495057;
|
|
}
|
|
|
|
.scroll-to-top {
|
|
position: fixed;
|
|
right: 1rem;
|
|
bottom: 1rem;
|
|
width: 40px;
|
|
height: 40px;
|
|
background-color: #0d6efd;
|
|
color: #fff;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-decoration: none;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body id="page-top">
|
|
<div id="wrapper">
|
|
|
|
<!-- Sidebar -->
|
|
<div th:replace="~{fragments/sidebar-nav :: sidebar-nav}"></div>
|
|
|
|
<div id="content-wrapper" class="d-flex flex-column">
|
|
<div id="content">
|
|
|
|
<!-- Topbar -->
|
|
<div th:replace="~{fragments/topbar :: topbar}"></div>
|
|
|
|
<div class="container-fluid px-4">
|
|
|
|
<!-- Header -->
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h3 class="mb-0">Dashboard</h3>
|
|
<a th:hx-get="@{/summary}" hx-trigger="click" hx-swap="none"
|
|
class="btn btn-sm btn-primary">
|
|
<span class="material-icons me-1">description</span>
|
|
Generate Report
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Overview Cards -->
|
|
<div class="row g-3 mb-4">
|
|
<div th:replace="~{/fragments/home/overviewCard :: overviewCard(${avgMonthlyEarnings})}"></div>
|
|
<div th:replace="~{/fragments/home/overviewCard :: overviewCard(${avgMonthlyExpenditure})}">
|
|
</div>
|
|
<div th:replace="~{/fragments/home/overviewCard :: overviewCard(${totalAnnualEarnings})}"></div>
|
|
<div th:replace="~{/fragments/home/overviewCard :: overviewCard(${totalAnnualFees})}"></div>
|
|
</div>
|
|
|
|
<div class="row g-4">
|
|
|
|
<!-- Charts -->
|
|
<div class="col-lg-9">
|
|
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header d-flex align-items-center gap-2">
|
|
<span class="material-icons text-primary">show_chart</span>
|
|
<h6 class="fw-bold text-primary mb-0">Quarterly Breakdown</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="chart-area">
|
|
<canvas id="categoryBarChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4">
|
|
|
|
<div class="col-md-6">
|
|
<div class="card shadow h-100">
|
|
<div class="card-header d-flex align-items-center gap-2">
|
|
<span class="material-icons text-success">trending_up</span>
|
|
<h6 class="fw-bold text-primary mb-0">Income Breakdown</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="chart-area">
|
|
<canvas id="incomeDoughnut"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<div class="card shadow h-100">
|
|
<div class="card-header d-flex align-items-center gap-2">
|
|
<span class="material-icons text-danger">trending_down</span>
|
|
<h6 class="fw-bold text-primary mb-0">Expenditure Breakdown</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="chart-area">
|
|
<canvas id="expenditureDoughnut"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Transactions -->
|
|
<div class="card shadow mt-4">
|
|
<div class="card-header d-flex align-items-center gap-2">
|
|
<span class="material-icons text-primary">receipt_long</span>
|
|
<h6 class="fw-bold text-primary mb-0">Transaction Overview</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Date</th>
|
|
<th>Category</th>
|
|
<th class="numeric">Money In</th>
|
|
<th class="numeric">Money Out</th>
|
|
<th class="numeric">Balance</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr th:each="transaction : ${transactions}"
|
|
th:class="${(transaction.moneyIn + transaction.moneyOut) < 0 ? 'txn-negative' : 'txn-positive'}">
|
|
<td th:text="${transaction.date}"></td>
|
|
<td th:text="${transaction.category}"></td>
|
|
<td class="numeric"
|
|
th:text="${#numbers.formatCurrency(transaction.moneyIn)}"></td>
|
|
<td class="numeric"
|
|
th:text="${#numbers.formatCurrency(transaction.moneyOut)}"></td>
|
|
<td class="numeric"
|
|
th:text="${#numbers.formatCurrency(transaction.balance)}"></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Insights Panel -->
|
|
<div class="col-lg-3">
|
|
<div class="card shadow h-100">
|
|
<div class="card-header d-flex align-items-center gap-2">
|
|
<span class="material-icons text-primary">insights</span>
|
|
<h6 class="fw-bold text-primary mb-0">Insights</h6>
|
|
</div>
|
|
<div class="card-body small">
|
|
|
|
<div class="insight-item">
|
|
<div class="insight-icon">
|
|
<span class="material-icons">warning</span>
|
|
</div>
|
|
<div>
|
|
<div class="fw-semibold">High Spending</div>
|
|
<div class="text-muted">Expenditure exceeded income this month</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="insight-item">
|
|
<div class="insight-icon">
|
|
<span class="material-icons">savings</span>
|
|
</div>
|
|
<div>
|
|
<div class="fw-semibold">Savings Rate</div>
|
|
<div class="text-muted">You saved 18% of income</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="insight-item">
|
|
<div class="insight-icon">
|
|
<span class="material-icons">calendar_month</span>
|
|
</div>
|
|
<div>
|
|
<div class="fw-semibold">Upcoming Fees</div>
|
|
<div class="text-muted">2 annual fees due next quarter</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div th:replace="~{fragments/footer :: footer}"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<a class="scroll-to-top rounded" href="#page-top">
|
|
<span class="material-icons">keyboard_arrow_up</span>
|
|
</a>
|
|
|
|
<!-- Scripts -->
|
|
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
|
|
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
|
|
<script src="assets/js/chart.min.js"></script>
|
|
<script src="assets/js/bs-init.js"></script>
|
|
<script src="assets/js/theme.js"></script>
|
|
<script th:inline="javascript">
|
|
/*<![CDATA[*/
|
|
const renderChart = ({ canvasId, type, data, labels, colours }) => {
|
|
const ctx = document.getElementById(canvasId);
|
|
if (!ctx) return;
|
|
|
|
new Chart(ctx, {
|
|
type,
|
|
data: {
|
|
labels,
|
|
datasets: [{
|
|
data,
|
|
backgroundColor: colours,
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { position: 'bottom' }
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
renderChart({
|
|
canvasId: 'incomeDoughnut',
|
|
type: /*[[${incomeChartType}]]*/ 'doughnut',
|
|
data: /*[[${incomeDoughnutdata}]]*/[],
|
|
labels: /*[[${incomeDoughnutLabels}]]*/[],
|
|
colours: /*[[${incomeDoughnutColours}]]*/[]
|
|
});
|
|
|
|
renderChart({
|
|
canvasId: 'expenditureDoughnut',
|
|
type: /*[[${expenseChartType}]]*/ 'doughnut',
|
|
data: /*[[${expenseDoughnutdata}]]*/[],
|
|
labels: /*[[${expenseDoughnutLabels}]]*/[],
|
|
colours: /*[[${expenseDoughnutColours}]]*/[]
|
|
});
|
|
|
|
new Chart(document.getElementById('categoryBarChart'), {
|
|
type: 'line',
|
|
data: {
|
|
labels: /*[[${labels}]]*/[],
|
|
datasets: [
|
|
{
|
|
label: 'Money In',
|
|
data: /*[[${moneyIn}]]*/[],
|
|
borderColor: '#2E7D32',
|
|
backgroundColor: 'rgba(46,125,50,0.15)',
|
|
borderWidth: 2,
|
|
fill: true,
|
|
pointRadius: 4,
|
|
tension: 0.3
|
|
},
|
|
{
|
|
label: 'Money Out',
|
|
data: /*[[${moneyOut}]]*/[],
|
|
borderColor: '#C62828',
|
|
backgroundColor: 'rgba(198,40,40,0.15)',
|
|
borderWidth: 2,
|
|
fill: true,
|
|
pointRadius: 4,
|
|
tension: 0.3
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
callback: v => `$${v.toLocaleString()}`
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
/*]]>*/
|
|
|
|
</script>
|
|
|
|
</body>
|
|
|
|
</html> |