mirror of
https://github.com/wahyd4/links.git
synced 2026-08-09 05:06:16 +10:00
Update UI
This commit is contained in:
+10
-1
@@ -121,8 +121,15 @@ def get_weekly_overview() -> dict:
|
||||
week_change_pct = round((week_gain / last_week_total) * 100, 2)
|
||||
|
||||
# Per-portfolio breakdown
|
||||
_palette = [
|
||||
{'badge': 'bg-indigo-100 text-indigo-800', 'row': 'bg-indigo-50', 'border': 'border-indigo-200'},
|
||||
{'badge': 'bg-emerald-100 text-emerald-800', 'row': 'bg-emerald-50', 'border': 'border-emerald-200'},
|
||||
{'badge': 'bg-amber-100 text-amber-800', 'row': 'bg-amber-50', 'border': 'border-amber-200'},
|
||||
{'badge': 'bg-rose-100 text-rose-800', 'row': 'bg-rose-50', 'border': 'border-rose-200'},
|
||||
{'badge': 'bg-sky-100 text-sky-800', 'row': 'bg-sky-50', 'border': 'border-sky-200'},
|
||||
]
|
||||
portfolio_rows = []
|
||||
for portfolio in Portfolio.objects.all():
|
||||
for idx, portfolio in enumerate(Portfolio.objects.all()):
|
||||
def _snap(date):
|
||||
if not date:
|
||||
return None
|
||||
@@ -145,6 +152,8 @@ def get_weekly_overview() -> dict:
|
||||
'last_week_value': last_val,
|
||||
'change': change,
|
||||
'change_pct': change_pct,
|
||||
'position_count': portfolio.stocks.filter(quantity__gt=0).count(),
|
||||
'colors': _palette[idx % len(_palette)],
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -19,10 +19,20 @@ def dashboard(request):
|
||||
fy_start = now.year if now.month >= 7 else now.year - 1
|
||||
fy_label = f"FY {str(fy_start)[2:]}-{str(fy_start + 1)[2:]}"
|
||||
|
||||
all_holdings = get_all_holdings()
|
||||
# Merge snapshot data (value, change, change_pct) into each holdings group
|
||||
rows_by_id = {row['portfolio'].id: row for row in overview.get('portfolio_rows', [])}
|
||||
for group in all_holdings:
|
||||
row = rows_by_id.get(group['portfolio'].id, {})
|
||||
group['this_week_value'] = row.get('this_week_value')
|
||||
group['change'] = row.get('change')
|
||||
group['change_pct'] = row.get('change_pct')
|
||||
group['position_count'] = row.get('position_count', len(group['holdings']))
|
||||
|
||||
return render(request, 'invest/dashboard.html', {
|
||||
'overview': overview,
|
||||
'fy_label': fy_label,
|
||||
'all_holdings': get_all_holdings(),
|
||||
'all_holdings': all_holdings,
|
||||
'chart_data_json': get_performance_chart_data() or 'null',
|
||||
})
|
||||
|
||||
|
||||
@@ -84,132 +84,72 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- ── Portfolio value table ─────────────────────────────────── -->
|
||||
{% if overview.portfolio_rows %}
|
||||
<div class="bg-white rounded-lg shadow-sm overflow-hidden mb-4">
|
||||
<div class="px-6 py-4 border-b border-stone-100">
|
||||
<p class="text-xs font-semibold tracking-widest text-stone-500 uppercase">Portfolio Breakdown</p>
|
||||
</div>
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-xs font-semibold tracking-widest text-stone-400 uppercase border-b border-stone-100">
|
||||
<th class="px-6 py-3 text-left">Portfolio</th>
|
||||
<th class="px-6 py-3 text-right">This Week</th>
|
||||
<th class="px-6 py-3 text-right">Last Week</th>
|
||||
<th class="px-6 py-3 text-right">Change</th>
|
||||
<th class="px-6 py-3 text-right">Change %</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-stone-50">
|
||||
{% for row in overview.portfolio_rows %}
|
||||
<tr class="hover:bg-stone-50">
|
||||
<td class="px-6 py-4 font-semibold text-stone-900">{{ row.portfolio.name }}</td>
|
||||
<td class="px-6 py-4 text-right text-stone-700">
|
||||
{% if row.this_week_value is not None %}${{ row.this_week_value|floatformat:0 }}{% else %}<span class="text-stone-300">—</span>{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right text-stone-400">
|
||||
{% if row.last_week_value is not None %}${{ row.last_week_value|floatformat:0 }}{% else %}<span class="text-stone-300">—</span>{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right font-medium
|
||||
{% if row.change is not None %}{% if row.change >= 0 %}text-green-700{% else %}text-red-600{% endif %}{% else %}text-stone-300{% endif %}">
|
||||
{% if row.change is not None %}{% if row.change >= 0 %}+{% endif %}${{ row.change|floatformat:0 }}{% else %}—{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right font-medium
|
||||
{% if row.change_pct is not None %}{% if row.change_pct >= 0 %}text-green-700{% else %}text-red-600{% endif %}{% else %}text-stone-300{% endif %}">
|
||||
{% if row.change_pct is not None %}{% if row.change_pct >= 0 %}+{% endif %}{{ row.change_pct|floatformat:2 }}%{% else %}—{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% if overview.this_week_total is not None %}
|
||||
<tfoot>
|
||||
<tr class="border-t-2 border-stone-200 bg-stone-50">
|
||||
<td class="px-6 py-3 font-semibold text-stone-700 text-xs uppercase tracking-wide">Total</td>
|
||||
<td class="px-6 py-3 text-right font-bold text-stone-900">${{ overview.this_week_total|floatformat:0 }}</td>
|
||||
<td class="px-6 py-3 text-right text-stone-400">
|
||||
{% if overview.last_week_total is not None %}${{ overview.last_week_total|floatformat:0 }}{% else %}—{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-3 text-right font-bold
|
||||
{% if overview.week_gain is not None %}{% if overview.week_gain >= 0 %}text-green-700{% else %}text-red-600{% endif %}{% endif %}">
|
||||
{% if overview.week_gain is not None %}{% if overview.week_gain >= 0 %}+{% endif %}${{ overview.week_gain|floatformat:0 }}{% else %}—{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-3 text-right font-bold
|
||||
{% if overview.week_change_pct is not None %}{% if overview.week_change_pct >= 0 %}text-green-700{% else %}text-red-600{% endif %}{% endif %}">
|
||||
{% if overview.week_change_pct is not None %}{% if overview.week_change_pct >= 0 %}+{% endif %}{{ overview.week_change_pct|floatformat:2 }}%{% else %}—{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-white rounded-lg shadow-sm p-12 text-center">
|
||||
<p class="text-stone-400 text-lg">No portfolios yet.</p>
|
||||
<p class="text-stone-300 text-sm mt-1">Snapshots are captured every Saturday at 08:00.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- ── Hint ──────────────────────────────────────────────────── -->
|
||||
<p class="text-xs text-stone-400 text-center mt-2">
|
||||
<p class="text-xs text-stone-400 text-center mt-2 mb-6">
|
||||
Snapshots captured every Saturday 08:00 · Values in portfolio's quote currency
|
||||
</p>
|
||||
|
||||
<!-- ── Unified live holdings ─────────────────────────────────── -->
|
||||
<!-- ── Live holdings (per-portfolio cards) ───────────────────── -->
|
||||
{% if all_holdings %}
|
||||
<div class="mt-6 mb-2">
|
||||
<p class="text-xs font-semibold tracking-widest text-stone-500 uppercase">Live Holdings</p>
|
||||
</div>
|
||||
|
||||
<!-- Legend badges -->
|
||||
<div class="flex flex-wrap gap-2 mb-3">
|
||||
<div class="space-y-4">
|
||||
{% for group in all_holdings %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ group.colors.badge }}">
|
||||
{{ group.portfolio.name }}
|
||||
· ${{ group.total_value|floatformat:0 }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-xs font-semibold tracking-widest text-stone-400 uppercase border-b border-stone-100">
|
||||
<th class="px-6 py-3 text-left">Portfolio</th>
|
||||
<th class="px-6 py-3 text-left">Stock</th>
|
||||
<th class="px-6 py-3 text-right">Qty</th>
|
||||
<th class="px-6 py-3 text-right">Price</th>
|
||||
<th class="px-6 py-3 text-right">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for group in all_holdings %}
|
||||
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
<!-- Card header -->
|
||||
<div class="px-6 py-4 flex items-center justify-between border-b border-stone-100">
|
||||
<div>
|
||||
<p class="font-semibold text-stone-900 text-sm">
|
||||
{{ group.portfolio.name }}
|
||||
</p>
|
||||
<p class="text-xs text-stone-400 mt-0.5">
|
||||
{{ group.position_count }} position{{ group.position_count|pluralize }}
|
||||
{% if overview.this_week_date %}· Last updated {{ overview.this_week_date|date:"j M Y" }}{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="font-bold text-stone-900 text-sm">
|
||||
{% if group.this_week_value is not None %}${{ group.this_week_value|floatformat:0 }}{% else %}<span class="text-stone-300">—</span>{% endif %}
|
||||
</p>
|
||||
{% if group.change is not None %}
|
||||
<p class="text-xs font-medium mt-0.5 {% if group.change >= 0 %}text-green-700{% else %}text-red-600{% endif %}">
|
||||
{% if group.change >= 0 %}+{% endif %}${{ group.change|floatformat:0 }}
|
||||
({% if group.change_pct >= 0 %}+{% endif %}{{ group.change_pct|floatformat:1 }}%)
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="text-xs text-stone-300 mt-0.5">No prior snapshot</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Holdings table -->
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-xs font-semibold tracking-widest text-stone-400 uppercase border-b border-stone-100">
|
||||
<th class="px-6 py-3 text-left">Ticker</th>
|
||||
<th class="px-6 py-3 text-right">Qty</th>
|
||||
<th class="px-6 py-3 text-right">Price</th>
|
||||
<th class="px-6 py-3 text-right">Mkt Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for stock in group.holdings %}
|
||||
<tr class="border-b border-stone-50 hover:bg-stone-50 {{ group.colors.row }}">
|
||||
{% if forloop.first %}
|
||||
<td class="px-6 py-3 font-semibold" rowspan="{{ group.holdings|length }}">
|
||||
<td class="px-6 py-3 font-medium text-stone-900">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {{ group.colors.badge }}">
|
||||
{{ group.portfolio.name }}
|
||||
{{ stock.stock_code }}
|
||||
</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="px-6 py-3 font-medium text-stone-900">{{ stock.stock_code }}</td>
|
||||
<td class="px-6 py-3 text-right text-stone-500">{{ stock.quantity|floatformat:2 }}</td>
|
||||
<td class="px-6 py-3 text-right text-stone-500">{{ stock.quantity|floatformat:0 }}</td>
|
||||
<td class="px-6 py-3 text-right text-stone-500">
|
||||
{% if stock.current_price %}${{ stock.current_price|floatformat:2 }}{% else %}<span class="text-stone-300">—</span>{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-3 text-right font-semibold text-stone-800">${{ stock.current_value|floatformat:0 }}</td>
|
||||
<td class="px-6 py-3 text-right font-semibold text-stone-800">
|
||||
{% if stock.current_value %}${{ stock.current_value|floatformat:0 }}{% else %}<span class="text-stone-300">—</span>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<!-- Sub-total row -->
|
||||
<tr class="border-b-2 {{ group.colors.border }}">
|
||||
<td class="px-6 py-2 text-xs text-right text-stone-400 uppercase tracking-wide font-semibold" colspan="4">
|
||||
{{ group.portfolio.name }} total
|
||||
</td>
|
||||
<td class="px-6 py-2 text-right font-bold text-stone-700">${{ group.total_value|floatformat:0 }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user