05/31 Phase 6
This commit is contained in:
@@ -200,7 +200,7 @@
|
||||
<a href="{{ url_for('investments.index') }}" class="sb-link {% if request.blueprint == 'investments' %}active{% endif %}">
|
||||
<i class="bi bi-graph-up-arrow"></i><span class="lt">Investments</span>
|
||||
</a>
|
||||
<a href="#" class="sb-link">
|
||||
<a href="{{ url_for('reports.index') }}" class="sb-link {% if request.blueprint == 'reports' %}active{% endif %}">
|
||||
<i class="bi bi-file-earmark-bar-graph"></i><span class="lt">Reports</span>
|
||||
</a>
|
||||
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Reports{% endblock %}
|
||||
{% block page_title %}Reports{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
.report-tab { font-size:13px; border-radius:6px; padding:6px 14px; border:1px solid var(--border); background:var(--card-bg); color:var(--muted); text-decoration:none; transition:all .15s; }
|
||||
.report-tab:hover { background:#f1f5f9; color:var(--text); }
|
||||
.report-tab.active { background:#0f172a; color:#f1f5f9; border-color:#0f172a; }
|
||||
{% endblock %}
|
||||
|
||||
{% block topbar_actions %}
|
||||
<!-- Export buttons -->
|
||||
<div class="d-flex gap-1">
|
||||
<a href="{{ url_for('reports.export_csv',
|
||||
year=selected_year, month=selected_month or '') }}"
|
||||
class="btn btn-sm btn-outline-secondary" style="font-size:12px;" title="Export CSV">
|
||||
<i class="bi bi-filetype-csv me-1"></i>CSV
|
||||
</a>
|
||||
<a href="{{ url_for('reports.export_excel',
|
||||
year=selected_year, month=selected_month or '') }}"
|
||||
class="btn btn-sm btn-outline-secondary" style="font-size:12px;" title="Export Excel">
|
||||
<i class="bi bi-file-earmark-excel me-1"></i>Excel
|
||||
</a>
|
||||
<a href="{{ url_for('reports.export_pdf',
|
||||
type=report_type, year=selected_year,
|
||||
month=selected_month or '', quarter=selected_quarter or '') }}"
|
||||
class="btn btn-sm btn-outline-secondary" style="font-size:12px;" title="Export PDF">
|
||||
<i class="bi bi-file-earmark-pdf me-1"></i>PDF
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Report type tabs + period selector -->
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3 mb-4">
|
||||
<div class="d-flex gap-1 flex-wrap">
|
||||
<a href="{{ url_for('reports.monthly', year=selected_year, month=selected_month or current_month) }}"
|
||||
class="report-tab {% if report_type=='monthly' %}active{% endif %}">Monthly</a>
|
||||
<a href="{{ url_for('reports.quarterly', year=selected_year, quarter=selected_quarter or 1) }}"
|
||||
class="report-tab {% if report_type=='quarterly' %}active{% endif %}">Quarterly</a>
|
||||
<a href="{{ url_for('reports.yearly', year=selected_year) }}"
|
||||
class="report-tab {% if report_type=='yearly' %}active{% endif %}">Yearly</a>
|
||||
<a href="{{ url_for('reports.tax', year=selected_year) }}"
|
||||
class="report-tab">Tax Year</a>
|
||||
</div>
|
||||
|
||||
<!-- Period picker -->
|
||||
<form method="GET" action="{{ url_for('reports.' + report_type) }}" class="d-flex gap-2 align-items-center">
|
||||
<select name="year" class="form-select form-select-sm" style="width:auto;">
|
||||
{% for y in years %}
|
||||
<option value="{{ y }}" {% if y == selected_year %}selected{% endif %}>{{ y }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if report_type == 'monthly' %}
|
||||
<select name="month" class="form-select form-select-sm" style="width:auto;">
|
||||
{% for m in range(1, 13) %}
|
||||
<option value="{{ m }}" {% if m == selected_month %}selected{% endif %}>
|
||||
{{ ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][m-1] }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% elif report_type == 'quarterly' %}
|
||||
<select name="quarter" class="form-select form-select-sm" style="width:auto;">
|
||||
{% for q in range(1, 5) %}
|
||||
<option value="{{ q }}" {% if q == selected_quarter %}selected{% endif %}>Q{{ q }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-sm btn-primary" style="font-size:12px;">Go</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Summary cards -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-xl-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Income</div>
|
||||
<div class="stat-value text-income">{{ report.income | currency }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-xl-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Expenses</div>
|
||||
<div class="stat-value text-expense">{{ report.expense | currency }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-xl-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Net</div>
|
||||
<div class="stat-value {% if report.net >= 0 %}text-income{% else %}text-expense{% endif %}">{{ report.net | currency }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-xl-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Savings Rate</div>
|
||||
<div class="stat-value {% if report.savings_rate >= 0 %}text-invest{% else %}text-expense{% endif %}">{{ report.savings_rate }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts row -->
|
||||
<div class="row g-3 mb-4">
|
||||
<!-- Period chart: bar for monthly, monthly bars for quarterly/yearly -->
|
||||
<div class="col-12 col-lg-7">
|
||||
<div class="pcard h-100">
|
||||
<div class="pcard-title mb-3">
|
||||
{% if report_type == 'monthly' %}Income vs Expenses — {{ report.period }}
|
||||
{% elif report_type == 'quarterly' %}Monthly Breakdown — {{ report.period }}
|
||||
{% else %}Monthly Breakdown — {{ report.period }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="position:relative;height:240px;"><canvas id="periodChart"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expense donut -->
|
||||
<div class="col-12 col-lg-5">
|
||||
<div class="pcard h-100">
|
||||
<div class="pcard-title mb-3">Expense Breakdown</div>
|
||||
{% if report.expense_categories %}
|
||||
<div style="position:relative;height:180px;"><canvas id="expDonut"></canvas></div>
|
||||
<div class="mt-3">
|
||||
{% for cat in report.expense_categories[:5] %}
|
||||
<div class="d-flex justify-content-between py-1" style="font-size:12px;border-bottom:1px solid var(--border);">
|
||||
<span><span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:{{ cat.color }};margin-right:6px;"></span>{{ cat.name }}</span>
|
||||
<span class="mono">{{ cat.total | currency }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted small">No expenses this period.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Net worth history -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12">
|
||||
<div class="pcard">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span class="pcard-title mb-0">Net Worth History</span>
|
||||
<form method="POST" action="{{ url_for('reports.manual_snapshot') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-secondary" style="font-size:11px;" title="Save today's snapshot">
|
||||
<i class="bi bi-camera me-1"></i>Snapshot Now
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% if nw_history.count > 1 %}
|
||||
<div style="position:relative;height:200px;"><canvas id="nwChart"></canvas></div>
|
||||
{% else %}
|
||||
<p class="text-muted small">Not enough snapshots yet. Snapshots save automatically on the 1st of each month, or click "Snapshot Now".</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category spending trends -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12">
|
||||
<div class="pcard">
|
||||
<div class="pcard-title mb-3">Spending Trends — Top Categories (Last 6 Months)</div>
|
||||
{% if cat_trend.datasets %}
|
||||
<div style="position:relative;height:220px;"><canvas id="trendChart"></canvas></div>
|
||||
{% else %}
|
||||
<p class="text-muted small">Not enough data for trends yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Yearly avg (only for yearly report) -->
|
||||
{% if report_type == 'yearly' %}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="pcard pcard-sm text-center">
|
||||
<div class="pcard-title">Avg Monthly Income</div>
|
||||
<div class="mono text-income fw-bold" style="font-size:16px;">{{ report.avg_monthly_income | currency }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="pcard pcard-sm text-center">
|
||||
<div class="pcard-title">Avg Monthly Expense</div>
|
||||
<div class="mono text-expense fw-bold" style="font-size:16px;">{{ report.avg_monthly_expense | currency }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
|
||||
<script>
|
||||
const sym = '{{ current_user.currency_symbol }}';
|
||||
const fmtCur = v => sym + Math.abs(v).toLocaleString(undefined, {minimumFractionDigits:0, maximumFractionDigits:0});
|
||||
|
||||
// Period chart
|
||||
(function(){
|
||||
{% if report_type == 'monthly' %}
|
||||
const ctx = document.getElementById('periodChart').getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['{{ report.period }}'],
|
||||
datasets: [
|
||||
{ label:'Income', data:[{{ report.income }}], backgroundColor:'#10b98133', borderColor:'#10b981', borderWidth:2, borderRadius:6 },
|
||||
{ label:'Expenses', data:[{{ report.expense }}], backgroundColor:'#ef444433', borderColor:'#ef4444', borderWidth:2, borderRadius:6 },
|
||||
]
|
||||
},
|
||||
options: { responsive:true, maintainAspectRatio:false, plugins:{ legend:{ position:'bottom', labels:{ font:{ size:11 } } } }, scales:{ y:{ ticks:{ callback: v => fmtCur(v) }, grid:{ color:'#f1f5f9' } }, x:{ grid:{ display:false } } } }
|
||||
});
|
||||
{% else %}
|
||||
const months = {{ report.months | map(attribute='label') | list | tojson }};
|
||||
const incomes = {{ report.months | map(attribute='income') | list | tojson }};
|
||||
const expenses = {{ report.months | map(attribute='expense') | list | tojson }};
|
||||
const ctx = document.getElementById('periodChart').getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: months,
|
||||
datasets: [
|
||||
{ label:'Income', data:incomes, backgroundColor:'#10b98133', borderColor:'#10b981', borderWidth:2, borderRadius:4 },
|
||||
{ label:'Expenses', data:expenses, backgroundColor:'#ef444433', borderColor:'#ef4444', borderWidth:2, borderRadius:4 },
|
||||
]
|
||||
},
|
||||
options: { responsive:true, maintainAspectRatio:false, plugins:{ legend:{ position:'bottom', labels:{ font:{ size:11 } } } }, scales:{ y:{ ticks:{ callback: v => fmtCur(v) }, grid:{ color:'#f1f5f9' } }, x:{ grid:{ display:false }, ticks:{ font:{ size:11 } } } } }
|
||||
});
|
||||
{% endif %}
|
||||
})();
|
||||
|
||||
// Expense donut
|
||||
{% if report.expense_categories %}
|
||||
(function(){
|
||||
const ctx = document.getElementById('expDonut').getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: {{ report.expense_categories | map(attribute='name') | list | tojson }},
|
||||
datasets: [{ data: {{ report.expense_categories | map(attribute='total') | list | tojson }}, backgroundColor: {{ report.expense_categories | map(attribute='color') | list | tojson }}, borderWidth:2, borderColor:'#fff', hoverOffset:4 }]
|
||||
},
|
||||
options: { responsive:true, maintainAspectRatio:false, cutout:'65%', plugins:{ legend:{ display:false }, tooltip:{ callbacks:{ label: ctx => ' ' + fmtCur(ctx.parsed) } } } }
|
||||
});
|
||||
})();
|
||||
{% endif %}
|
||||
|
||||
// Net worth history
|
||||
{% if nw_history.count > 1 %}
|
||||
(function(){
|
||||
const ctx = document.getElementById('nwChart').getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: {{ nw_history.labels | tojson }},
|
||||
datasets: [
|
||||
{ label:'Net Worth', data: {{ nw_history.values | tojson }}, borderColor:'#3b82f6', backgroundColor:'#3b82f611', borderWidth:2, pointRadius:3, tension:.3, fill:true },
|
||||
{ label:'Assets', data: {{ nw_history.assets | tojson }}, borderColor:'#10b981', borderWidth:1.5, pointRadius:2, tension:.3, fill:false, borderDash:[4,3] },
|
||||
]
|
||||
},
|
||||
options: { responsive:true, maintainAspectRatio:false, plugins:{ legend:{ position:'bottom', labels:{ font:{ size:11 } } } }, scales:{ y:{ ticks:{ callback: v => fmtCur(v) }, grid:{ color:'#f1f5f9' } }, x:{ grid:{ display:false }, ticks:{ font:{ size:10 } } } } }
|
||||
});
|
||||
})();
|
||||
{% endif %}
|
||||
|
||||
// Category trends
|
||||
{% if cat_trend.datasets %}
|
||||
(function(){
|
||||
const ctx = document.getElementById('trendChart').getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: { labels: {{ cat_trend.labels | tojson }}, datasets: {{ cat_trend.datasets | tojson }} },
|
||||
options: {
|
||||
responsive:true, maintainAspectRatio:false,
|
||||
plugins:{ legend:{ position:'bottom', labels:{ font:{ size:11 }, boxWidth:10 } } },
|
||||
scales:{ y:{ ticks:{ callback: v => fmtCur(v) }, grid:{ color:'#f1f5f9' } }, x:{ grid:{ display:false }, ticks:{ font:{ size:11 } } } }
|
||||
}
|
||||
});
|
||||
})();
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,147 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Tax Summary {{ report.year }}{% endblock %}
|
||||
{% block page_title %}Tax Year Summary{% endblock %}
|
||||
|
||||
{% block topbar_actions %}
|
||||
<a href="{{ url_for('reports.export_csv', year=selected_year) }}" class="btn btn-sm btn-outline-secondary" style="font-size:12px;">
|
||||
<i class="bi bi-filetype-csv me-1"></i>Export CSV
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Year selector -->
|
||||
<div class="d-flex align-items-center gap-3 mb-4">
|
||||
<form method="GET" action="{{ url_for('reports.tax') }}" class="d-flex gap-2">
|
||||
<select name="year" class="form-select form-select-sm" style="width:auto;">
|
||||
{% for y in years %}
|
||||
<option value="{{ y }}" {% if y == selected_year %}selected{% endif %}>{{ y }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="btn btn-sm btn-primary" style="font-size:12px;">Go</button>
|
||||
</form>
|
||||
<span class="text-muted" style="font-size:12px;">{{ report.date_from.strftime('%b %d, %Y') }} – {{ report.date_to.strftime('%b %d, %Y') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Total Income</div>
|
||||
<div class="stat-value text-income">{{ report.total_income | currency }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Total Expenses</div>
|
||||
<div class="stat-value text-expense">{{ report.total_expense | currency }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Net for Year</div>
|
||||
<div class="stat-value {% if report.net >= 0 %}text-income{% else %}text-expense{% endif %}">{{ report.net | currency }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Income Sources</div>
|
||||
<div class="stat-value text-invest">{{ report.income_categories | length }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- Income by category -->
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="pcard p-0">
|
||||
<div class="px-4 py-3" style="border-bottom:1px solid var(--border);">
|
||||
<span class="pcard-title mb-0">Income by Source</span>
|
||||
</div>
|
||||
<table class="pfm-table">
|
||||
<thead><tr><th style="padding-left:20px;">Category</th><th class="text-end" style="padding-right:20px;">Total</th></tr></thead>
|
||||
<tbody>
|
||||
{% if report.income_categories %}
|
||||
{% for cat in report.income_categories %}
|
||||
<tr>
|
||||
<td style="padding-left:20px;">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div style="width:8px;height:8px;border-radius:2px;background:{{ cat.color }};"></div>
|
||||
<span style="font-size:13px;">{{ cat.name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end mono text-income" style="font-size:13px;font-weight:500;padding-right:20px;">{{ cat.total | currency }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr style="font-weight:700;background:#f8fafc;">
|
||||
<td style="padding-left:20px;font-size:13px;">Total</td>
|
||||
<td class="text-end mono text-income" style="font-size:13px;padding-right:20px;">{{ report.total_income | currency }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="2" class="text-muted text-center py-3" style="font-size:13px;">No income recorded</td></tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expense by category -->
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="pcard p-0">
|
||||
<div class="px-4 py-3" style="border-bottom:1px solid var(--border);">
|
||||
<span class="pcard-title mb-0">Expenses by Category</span>
|
||||
</div>
|
||||
<table class="pfm-table">
|
||||
<thead><tr><th style="padding-left:20px;">Category</th><th class="text-end" style="padding-right:20px;">Total</th></tr></thead>
|
||||
<tbody>
|
||||
{% if report.expense_categories %}
|
||||
{% for cat in report.expense_categories %}
|
||||
<tr>
|
||||
<td style="padding-left:20px;">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div style="width:8px;height:8px;border-radius:2px;background:{{ cat.color }};"></div>
|
||||
<span style="font-size:13px;">{{ cat.name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end mono text-expense" style="font-size:13px;font-weight:500;padding-right:20px;">{{ cat.total | currency }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr style="font-weight:700;background:#f8fafc;">
|
||||
<td style="padding-left:20px;font-size:13px;">Total</td>
|
||||
<td class="text-end mono text-expense" style="font-size:13px;padding-right:20px;">{{ report.total_expense | currency }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="2" class="text-muted text-center py-3" style="font-size:13px;">No expenses recorded</td></tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- All income transactions for tax year -->
|
||||
{% if report.income_transactions %}
|
||||
<div class="pcard p-0 mt-3">
|
||||
<div class="d-flex justify-content-between align-items-center px-4 py-3" style="border-bottom:1px solid var(--border);">
|
||||
<span class="pcard-title mb-0">All Income — {{ report.year }}</span>
|
||||
<span class="text-muted" style="font-size:12px;">{{ report.income_transactions | length }} transactions</span>
|
||||
</div>
|
||||
<table class="pfm-table">
|
||||
<thead><tr><th style="padding-left:20px;">Date</th><th>Description</th><th>Category</th><th class="text-end" style="padding-right:20px;">Amount</th></tr></thead>
|
||||
<tbody>
|
||||
{% for txn in report.income_transactions %}
|
||||
<tr>
|
||||
<td style="padding-left:20px;font-size:12px;color:var(--muted);white-space:nowrap;">{{ txn.date.strftime('%b %d, %Y') }}</td>
|
||||
<td style="font-size:13px;">{{ txn.description }}</td>
|
||||
<td style="font-size:12px;color:var(--muted);">{{ txn.category.name if txn.category else '—' }}</td>
|
||||
<td class="text-end mono text-income" style="font-size:13px;font-weight:500;padding-right:20px;">+{{ txn.amount | currency }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-3">
|
||||
<a href="{{ url_for('reports.index') }}" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-left me-1"></i>Back to Reports</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user