205 lines
9.6 KiB
HTML
205 lines
9.6 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Investments{% endblock %}
|
|
{% block page_title %}Investment Portfolio{% endblock %}
|
|
|
|
{% block topbar_actions %}
|
|
<form method="POST" action="{{ url_for('investments.refresh_prices') }}" class="d-inline">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="btn btn-sm btn-outline-secondary me-1" style="font-size:12px;" title="Refresh prices for all tickers">
|
|
<i class="bi bi-arrow-clockwise me-1"></i>Refresh Prices
|
|
</button>
|
|
</form>
|
|
<a href="{{ url_for('investments.new') }}" class="btn btn-sm btn-primary" style="font-size:12px;"><i class="bi bi-plus-lg me-1"></i>Add Holding</a>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
{% if portfolio.count == 0 %}
|
|
<div class="pcard text-center py-5">
|
|
<i class="bi bi-graph-up-arrow text-muted" style="font-size:3rem;"></i>
|
|
<h5 class="mt-3 mb-1">No investments yet</h5>
|
|
<p class="text-muted small mb-3">Track stocks, ETFs, crypto, real estate, and more.</p>
|
|
<a href="{{ url_for('investments.new') }}" class="btn btn-primary btn-sm">Add First Holding</a>
|
|
</div>
|
|
{% else %}
|
|
|
|
<!-- Summary cards -->
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-6 col-xl-3">
|
|
<div class="stat-card">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<div class="stat-label">Total Value</div>
|
|
<div class="stat-value text-invest">{{ portfolio.total_value | currency }}</div>
|
|
</div>
|
|
<div class="stat-icon" style="background:#dbeafe;color:#1e40af;"><i class="bi bi-graph-up"></i></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-6 col-xl-3">
|
|
<div class="stat-card">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<div class="stat-label">Total Cost</div>
|
|
<div class="stat-value">{{ portfolio.total_cost | currency }}</div>
|
|
</div>
|
|
<div class="stat-icon" style="background:#f1f5f9;color:#64748b;"><i class="bi bi-cash-stack"></i></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-6 col-xl-3">
|
|
<div class="stat-card">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<div class="stat-label">Unrealized P&L</div>
|
|
<div class="stat-value {% if portfolio.total_gain >= 0 %}text-income{% else %}text-expense{% endif %}">
|
|
{{ portfolio.total_gain | currency }}
|
|
</div>
|
|
</div>
|
|
<div class="stat-icon" style="background:{% if portfolio.total_gain >= 0 %}#d1fae5{% else %}#fee2e2{% endif %};color:{% if portfolio.total_gain >= 0 %}#065f46{% else %}#991b1b{% endif %};">
|
|
<i class="bi bi-{% if portfolio.total_gain >= 0 %}trending-up{% else %}trending-down{% endif %}"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-6 col-xl-3">
|
|
<div class="stat-card">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<div class="stat-label">Return</div>
|
|
<div class="stat-value {% if portfolio.total_gain_pct >= 0 %}text-income{% else %}text-expense{% endif %}">
|
|
{% if portfolio.total_gain_pct >= 0 %}+{% endif %}{{ portfolio.total_gain_pct }}%
|
|
</div>
|
|
</div>
|
|
<div class="stat-icon" style="background:#ede9fe;color:#5b21b6;"><i class="bi bi-percent"></i></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Chart + Allocation -->
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-12 col-lg-5">
|
|
<div class="pcard h-100">
|
|
<div class="pcard-title mb-3">Allocation</div>
|
|
<div style="position:relative;height:220px;display:flex;align-items:center;justify-content:center;">
|
|
<canvas id="allocChart"></canvas>
|
|
</div>
|
|
<div class="mt-3">
|
|
{% for item in portfolio.allocation %}
|
|
<div class="d-flex justify-content-between align-items-center py-1" style="border-bottom:1px solid var(--border);">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<div style="width:10px;height:10px;border-radius:2px;background:{{ item.color }};flex-shrink:0;"></div>
|
|
<span style="font-size:13px;">{{ item.type | replace('_',' ') | title }}</span>
|
|
</div>
|
|
<div class="d-flex gap-3">
|
|
<span class="mono text-muted" style="font-size:12px;">{{ item.value | currency }}</span>
|
|
<span class="mono" style="font-size:12px;width:40px;text-align:right;">{{ item.pct }}%</span>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Holdings table -->
|
|
<div class="col-12 col-lg-7">
|
|
<div class="pcard p-0 h-100">
|
|
<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">Holdings</span>
|
|
</div>
|
|
<table class="pfm-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="padding-left:20px;">Asset</th>
|
|
<th class="text-end">Shares</th>
|
|
<th class="text-end">Price</th>
|
|
<th class="text-end">Value</th>
|
|
<th class="text-end" style="padding-right:20px;">P&L</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for inv in portfolio.investments %}
|
|
<tr style="cursor:pointer;" onclick="location.href='{{ url_for('investments.detail', id=inv.id) }}'">
|
|
<td style="padding-left:20px;">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<div style="width:28px;height:28px;border-radius:7px;background:{{ asset_colors.get(inv.asset_type,'#94a3b8') }}22;color:{{ asset_colors.get(inv.asset_type,'#94a3b8') }};display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;">
|
|
{{ (inv.ticker or inv.asset_name[:2]).upper()[:2] }}
|
|
</div>
|
|
<div>
|
|
<div style="font-size:13px;font-weight:500;">{{ inv.asset_name }}</div>
|
|
<div style="font-size:11px;color:var(--muted);">
|
|
{% if inv.ticker %}<span class="mono">{{ inv.ticker }}</span> · {% endif %}
|
|
{{ asset_labels.get(inv.asset_type, inv.asset_type) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="text-end mono" style="font-size:12px;color:var(--muted);">
|
|
{{ "%.4f"|format(inv.shares|float) | replace('.0000','') }}
|
|
</td>
|
|
<td class="text-end">
|
|
{% if inv.current_price %}
|
|
<span class="mono" style="font-size:12px;">{{ inv.current_price | currency }}</span>
|
|
{% else %}
|
|
<span class="text-muted" style="font-size:12px;">—</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="text-end mono" style="font-size:13px;font-weight:600;">{{ inv.current_value | currency }}</td>
|
|
<td class="text-end" style="padding-right:20px;">
|
|
<div class="mono {% if inv.unrealized_gain >= 0 %}text-income{% else %}text-expense{% endif %}" style="font-size:12px;font-weight:600;">
|
|
{% if inv.unrealized_gain >= 0 %}+{% endif %}{{ inv.unrealized_gain | currency }}
|
|
</div>
|
|
<div style="font-size:10px;color:var(--muted);">
|
|
{% if inv.unrealized_gain_pct >= 0 %}+{% endif %}{{ inv.unrealized_gain_pct }}%
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
{% if portfolio.count > 0 %}
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
|
|
<script>
|
|
(function(){
|
|
const ctx = document.getElementById('allocChart').getContext('2d');
|
|
new Chart(ctx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: {{ chart_labels | tojson }},
|
|
datasets: [{
|
|
data: {{ chart_values | tojson }},
|
|
backgroundColor: {{ chart_colors | tojson }},
|
|
borderWidth: 2,
|
|
borderColor: '#ffffff',
|
|
hoverOffset: 6,
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
cutout: '68%',
|
|
plugins: {
|
|
legend: { display: false },
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(ctx) {
|
|
const sym = '{{ current_user.currency_symbol }}';
|
|
return ' ' + sym + ctx.parsed.toLocaleString(undefined, {minimumFractionDigits:2, maximumFractionDigits:2});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
{% endif %}
|
|
{% endblock %}
|