FinanceTracker/src/main/resources/templates/index.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>