05/31 Phase 4
This commit is contained in:
@@ -197,7 +197,7 @@
|
||||
</a>
|
||||
|
||||
<div class="sb-section">Growth</div>
|
||||
<a href="#" class="sb-link">
|
||||
<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">
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ inv.asset_name }}{% endblock %}
|
||||
{% block page_title %}{{ inv.asset_name }}{% endblock %}
|
||||
|
||||
{% block topbar_actions %}
|
||||
<a href="{{ url_for('investments.add_transaction', id=inv.id) }}" class="btn btn-sm btn-primary" style="font-size:12px;"><i class="bi bi-plus-lg me-1"></i>Add Transaction</a>
|
||||
<a href="{{ url_for('investments.edit', id=inv.id) }}" class="btn btn-sm btn-outline-secondary ms-1" style="font-size:12px;"><i class="bi bi-pencil me-1"></i>Edit</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Holding summary -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12 col-md-5">
|
||||
<div class="pcard h-100" style="border-left:4px solid {{ asset_colors.get(inv.asset_type,'#94a3b8') }};">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<div>
|
||||
<div style="font-size:18px;font-weight:700;">{{ inv.asset_name }}</div>
|
||||
<div style="font-size:12px;color:var(--muted);">
|
||||
{% if inv.ticker %}<span class="mono">{{ inv.ticker }}</span> · {% endif %}
|
||||
<span style="background:{{ asset_colors.get(inv.asset_type,'#94a3b8') }}22;color:{{ asset_colors.get(inv.asset_type,'#94a3b8') }};padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;">
|
||||
{{ asset_labels.get(inv.asset_type, inv.asset_type) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="{{ url_for('investments.delete', id=inv.id) }}" onsubmit="return confirm('Remove this holding?')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" style="font-size:11px;">Remove</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mt-1">
|
||||
<div class="col-6">
|
||||
<div style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;">Shares</div>
|
||||
<div class="mono" style="font-size:16px;font-weight:600;">{{ "%.6f"|format(inv.shares|float)|replace('000000','').rstrip('0').rstrip('.') }}</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;">Avg Cost</div>
|
||||
<div class="mono" style="font-size:16px;font-weight:600;">{{ inv.avg_cost_basis | currency }}</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;">Current Price</div>
|
||||
<div class="mono" style="font-size:16px;font-weight:600;">
|
||||
{% if inv.current_price %}{{ inv.current_price | currency }}
|
||||
{% else %}<span class="text-muted">—</span>{% endif %}
|
||||
</div>
|
||||
{% if inv.last_price_update %}
|
||||
<div style="font-size:10px;color:var(--muted);">{{ inv.last_price_update.strftime('%b %d, %H:%M') }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;">Market Value</div>
|
||||
<div class="mono text-invest" style="font-size:16px;font-weight:600;">{{ inv.current_value | currency }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr style="border-color:var(--border);">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<div style="font-size:11px;color:var(--muted);">Total Cost Basis</div>
|
||||
<div class="mono" style="font-size:14px;">{{ inv.total_cost | currency }}</div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<div style="font-size:11px;color:var(--muted);">Unrealized P&L</div>
|
||||
<div class="mono {% if inv.unrealized_gain >= 0 %}text-income{% else %}text-expense{% endif %}" style="font-size:14px;font-weight:600;">
|
||||
{% if inv.unrealized_gain >= 0 %}+{% endif %}{{ inv.unrealized_gain | currency }}
|
||||
<span style="font-size:11px;">({% if inv.unrealized_gain_pct >= 0 %}+{% endif %}{{ inv.unrealized_gain_pct }}%)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if inv.notes %}
|
||||
<div style="font-size:12px;color:var(--muted);margin-top:12px;padding-top:12px;border-top:1px solid var(--border);">{{ inv.notes }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-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">Transaction History</span>
|
||||
<a href="{{ url_for('investments.add_transaction', id=inv.id) }}" class="btn btn-sm btn-outline-primary" style="font-size:11px;padding:2px 8px;"><i class="bi bi-plus-lg"></i></a>
|
||||
</div>
|
||||
{% if txns %}
|
||||
<table class="pfm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="padding-left:20px;">Date</th>
|
||||
<th>Type</th>
|
||||
<th class="text-end">Shares</th>
|
||||
<th class="text-end">Price</th>
|
||||
<th class="text-end">Total</th>
|
||||
<th class="text-end" style="padding-right:20px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for txn in txns %}
|
||||
<tr>
|
||||
<td style="padding-left:20px;font-size:12px;color:var(--muted);white-space:nowrap;">{{ txn.date.strftime('%b %d, %Y') }}</td>
|
||||
<td>
|
||||
<span class="badge" style="font-size:11px;{% if txn.transaction_type == 'buy' %}background:#d1fae5;color:#065f46;{% elif txn.transaction_type == 'sell' %}background:#fee2e2;color:#991b1b;{% elif txn.transaction_type == 'dividend' %}background:#dbeafe;color:#1e40af;{% else %}background:#f3e8ff;color:#6b21a8;{% endif %}">
|
||||
{{ txn.transaction_type | title }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end mono" style="font-size:12px;">{{ txn.shares }}</td>
|
||||
<td class="text-end mono" style="font-size:12px;">{{ txn.price_per_share | currency }}</td>
|
||||
<td class="text-end mono {% if txn.transaction_type == 'buy' %}text-expense{% else %}text-income{% endif %}" style="font-size:12px;font-weight:500;">
|
||||
{{ txn.total_amount | currency }}
|
||||
</td>
|
||||
<td class="text-end" style="padding-right:20px;">
|
||||
<form method="POST" action="{{ url_for('investments.delete_transaction', id=txn.id) }}" onsubmit="return confirm('Delete this transaction?')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" style="font-size:11px;padding:2px 6px;">×</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<p class="text-muted small mb-2">No transactions yet.</p>
|
||||
<a href="{{ url_for('investments.add_transaction', id=inv.id) }}" class="btn btn-sm btn-primary">Record First Buy</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="{{ url_for('investments.index') }}" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-left me-1"></i>Back to Portfolio</a>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,69 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
{% block page_title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-8 col-lg-6">
|
||||
<div class="pcard">
|
||||
<form method="POST" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.asset_name.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
{{ form.asset_name(class="form-control" + (" is-invalid" if form.asset_name.errors else ""),
|
||||
placeholder="e.g. Apple Inc., Bitcoin, Vanguard S&P 500") }}
|
||||
{% for e in form.asset_name.errors %}<div class="invalid-feedback">{{ e }}</div>{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-6">
|
||||
{{ form.asset_type.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
{{ form.asset_type(class="form-select") }}
|
||||
</div>
|
||||
<div class="col-6">
|
||||
{{ form.ticker.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
<div class="input-group">
|
||||
{{ form.ticker(class="form-control", placeholder="AAPL", id="tickerInput", style="text-transform:uppercase;") }}
|
||||
<button type="button" class="btn btn-outline-secondary" id="checkTicker" style="font-size:12px;">Check</button>
|
||||
</div>
|
||||
<div id="tickerResult" style="font-size:12px;margin-top:4px;"></div>
|
||||
<small class="text-muted" style="font-size:11px;">Yahoo Finance format. e.g. AAPL, BTC-USD, ETH-USD</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form.notes.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
{{ form.notes(class="form-control", rows=2, placeholder="Optional notes") }}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
<a href="{{ url_for('investments.index') }}" class="btn btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.getElementById('checkTicker').addEventListener('click', function() {
|
||||
const ticker = document.getElementById('tickerInput').value.trim().toUpperCase();
|
||||
const result = document.getElementById('tickerResult');
|
||||
if (!ticker) { result.textContent = ''; return; }
|
||||
result.innerHTML = '<span class="text-muted">Fetching…</span>';
|
||||
fetch('/investments/api/price/' + encodeURIComponent(ticker))
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.price) {
|
||||
result.innerHTML = '<span class="text-success"><i class="bi bi-check-circle me-1"></i>' + ticker + ' → {{ current_user.currency_symbol }}' + data.price.toFixed(4) + '</span>';
|
||||
} else {
|
||||
result.innerHTML = '<span class="text-danger"><i class="bi bi-x-circle me-1"></i>Ticker not found or no data</span>';
|
||||
}
|
||||
})
|
||||
.catch(() => { result.innerHTML = '<span class="text-warning">Could not fetch price</span>'; });
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,204 @@
|
||||
{% 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 %}
|
||||
@@ -0,0 +1,123 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
{% block page_title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-8 col-lg-6">
|
||||
|
||||
<!-- Holding mini-card -->
|
||||
<div class="pcard pcard-sm mb-3 d-flex align-items-center gap-3">
|
||||
<div style="width:36px;height:36px;border-radius:9px;background:#dbeafe;color:#1e40af;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:13px;">
|
||||
{{ (inv.ticker or inv.asset_name[:2]).upper()[:2] }}
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:14px;font-weight:600;">{{ inv.asset_name }}</div>
|
||||
<div style="font-size:12px;color:var(--muted);">
|
||||
{% if inv.ticker %}<span class="mono">{{ inv.ticker }}</span> · {% endif %}
|
||||
{{ inv.shares|float|round(4) }} shares held
|
||||
{% if inv.current_price %} · Current: {{ inv.current_price | currency }}{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pcard">
|
||||
<form method="POST" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.transaction_type.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
{{ form.transaction_type(class="form-select", id="txnType") }}
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-6">
|
||||
{{ form.shares.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
{{ form.shares(class="form-control" + (" is-invalid" if form.shares.errors else ""),
|
||||
placeholder="0.00", id="sharesInput") }}
|
||||
{% for e in form.shares.errors %}<div class="invalid-feedback">{{ e }}</div>{% endfor %}
|
||||
</div>
|
||||
<div class="col-6">
|
||||
{{ form.price_per_share.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
<div class="input-group">
|
||||
<span class="input-group-text" style="font-size:12px;">{{ current_user.currency_symbol }}</span>
|
||||
{{ form.price_per_share(class="form-control", placeholder="0.0000", id="priceInput") }}
|
||||
{% if inv.ticker %}
|
||||
<button type="button" class="btn btn-outline-secondary" id="fillPrice" style="font-size:11px;" title="Fill current price">↓</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-6">
|
||||
{{ form.fees.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
<div class="input-group">
|
||||
<span class="input-group-text" style="font-size:12px;">{{ current_user.currency_symbol }}</span>
|
||||
{{ form.fees(class="form-control", placeholder="0.00") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
{{ form.date.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
{{ form.date(class="form-control") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total preview -->
|
||||
<div class="mb-3 p-3" style="background:#f8fafc;border-radius:8px;font-size:13px;">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="text-muted">Estimated Total</span>
|
||||
<span class="mono fw-bold" id="totalPreview">{{ current_user.currency_symbol }}0.00</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form.notes.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
{{ form.notes(class="form-control", placeholder="Optional notes") }}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
<a href="{{ url_for('investments.detail', id=inv.id) }}" class="btn btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
const sym = '{{ current_user.currency_symbol }}';
|
||||
const sharesInput = document.getElementById('sharesInput');
|
||||
const priceInput = document.getElementById('priceInput');
|
||||
const totalPreview = document.getElementById('totalPreview');
|
||||
|
||||
function updateTotal() {
|
||||
const s = parseFloat(sharesInput.value) || 0;
|
||||
const p = parseFloat(priceInput.value) || 0;
|
||||
totalPreview.textContent = sym + (s * p).toLocaleString(undefined, {minimumFractionDigits:2, maximumFractionDigits:2});
|
||||
}
|
||||
sharesInput.addEventListener('input', updateTotal);
|
||||
priceInput.addEventListener('input', updateTotal);
|
||||
|
||||
{% if inv.ticker %}
|
||||
const fillBtn = document.getElementById('fillPrice');
|
||||
if (fillBtn) {
|
||||
fillBtn.addEventListener('click', function() {
|
||||
fillBtn.textContent = '…';
|
||||
fetch('/investments/api/price/{{ inv.ticker }}')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.price) {
|
||||
priceInput.value = data.price.toFixed(4);
|
||||
updateTotal();
|
||||
}
|
||||
fillBtn.textContent = '↓';
|
||||
})
|
||||
.catch(() => { fillBtn.textContent = '↓'; });
|
||||
});
|
||||
}
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user