Files
2025-07-05 05:10:42 +02:00

509 lines
20 KiB
Plaintext

package templates
import (
"fmt"
"portfolio-tracker/internal/model"
"portfolio-tracker/internal/web/templates/components"
)
templ PortfolioDetailContent(portfolio model.Portfolio, positions []model.Position, positionSummary model.PositionSummary) {
<div class="container-fluid mt-4">
<div class="page-header">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">{ portfolio.Name } ({ portfolio.BaseCurrency })</h2>
<div class="page-subtitle">
if portfolio.Description != "" {
{ portfolio.Description }
} else {
Erstellt am { portfolio.CreatedAt.Format("02.01.2006") }
}
</div>
</div>
<div class="col-auto ms-auto d-print-none">
<div class="btn-list">
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addTransactionModal">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 5l0 14"></path>
<path d="M5 12l14 0"></path>
</svg>
Transaktion hinzufügen
</button>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Portfolio Overview -->
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h3 class="card-title">Positionen</h3>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-vcenter">
<thead>
<tr>
<th>Wertpapier</th>
<th>Anzahl</th>
<th>Ø Einkaufspreis</th>
<th>Aktueller Wert</th>
<th>+/- Gesamt</th>
<th></th>
</tr>
</thead>
<tbody>
if len(positions) > 0 {
for _, position := range positions {
if position.IsOpen() {
<tr>
<td>
<div class="fw-bold">{ position.Stock }</div>
<div class="text-muted small">{ position.Currency }</div>
</td>
<td>
<span class="fw-bold">{ position.FormatShares() }</span>
</td>
<td>
<span>{ position.FormatCurrency(position.AverageCostPrice) }</span>
</td>
<td>
<div>{ position.FormatCurrency(position.CurrentPrice) }</div>
<div class="text-muted small">{ position.FormatCurrency(position.CurrentValue) }</div>
</td>
<td>
<div class={ position.GetUnrealizedPLColor() }>
<strong>{ position.FormatCurrency(position.UnrealizedPL) }</strong>
</div>
<div class={ position.GetUnrealizedPLColor() + " small" }>
{ position.FormatPercentage() }
</div>
</td>
<td>
<a href={ templ.URL("/details?stock=" + position.Stock) } class="btn btn-sm btn-outline-primary">
Details
</a>
</td>
</tr>
}
}
} else {
<tr>
<td colspan="6" class="text-center text-muted py-4">
Keine Positionen vorhanden. Fügen Sie Ihre erste Transaktion hinzu.
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
<!-- Recent Activities -->
<div class="card mt-4">
<div class="card-header">
<h3 class="card-title">Letzte Transaktionen</h3>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Datum</th>
<th>Typ</th>
<th>Wertpapier</th>
<th>Anzahl</th>
<th>Preis</th>
<th>Preis ({ portfolio.BaseCurrency })</th>
<th>Gesamt</th>
<th>Gesamt ({ portfolio.BaseCurrency })</th>
<th></th>
</tr>
</thead>
<tbody>
if len(portfolio.Activities) > 0 {
for i, activity := range portfolio.Activities {
if i < 10 {
<tr>
<td>{ activity.Date.Format("02.01.2006") }</td>
<td>
if activity.Type == "BUY" {
<span class="badge bg-green">Kauf</span>
} else if activity.Type == "SELL" {
<span class="badge bg-red">Verkauf</span>
} else if activity.Type == "DIVIDEND" {
<span class="badge bg-blue">Dividende</span>
} else {
<span class="badge bg-secondary">{ string(activity.Type) }</span>
}
</td>
<td>
<a href={ templ.URL("/details?stock=" + activity.Stock) } class="text-decoration-none">
{ activity.Stock }
</a>
</td>
<td>{ fmt.Sprintf("%.3f", activity.Amount) }</td>
<td>{ fmt.Sprintf("%.2f %s", activity.Price, activity.Currency) }</td>
<td>
if activity.Currency != portfolio.BaseCurrency {
{ getConvertedPrice(activity, portfolio.BaseCurrency) }
} else {
<span class="text-muted">-</span>
}
</td>
<td>{ fmt.Sprintf("%.2f %s", activity.Amount * activity.Price, activity.Currency) }</td>
<td>
if activity.Currency != portfolio.BaseCurrency {
{ getConvertedTotalWithFallbackInfo(activity, portfolio.BaseCurrency) }
} else {
<span class="text-muted">-</span>
}
</td>
<td>
<div class="btn-list flex-nowrap">
<button
class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#editTransactionModal"
data-activity-id={ fmt.Sprintf("%d", activity.ID) }
data-activity-type={ string(activity.Type) }
data-activity-stock={ activity.Stock }
data-activity-amount={ fmt.Sprintf("%.3f", activity.Amount) }
data-activity-price={ fmt.Sprintf("%.2f", activity.Price) }
data-activity-date={ activity.Date.Format("2006-01-02") }
data-activity-note={ activity.Note }
>
Bearbeiten
</button>
<button
class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#deleteTransactionModal"
data-activity-id={ fmt.Sprintf("%d", activity.ID) }
data-activity-stock={ activity.Stock }
data-activity-type={ string(activity.Type) }
data-activity-amount={ fmt.Sprintf("%.3f", activity.Amount) }
data-activity-date={ activity.Date.Format("02.01.2006") }
>
Löschen
</button>
</div>
</td>
</tr>
}
}
} else {
<tr>
<td colspan="9" class="text-center text-muted py-3">
Keine Transaktionen vorhanden
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Portfolio Summary -->
<div class="col-md-4">
<!-- Currency Conversion Info -->
<div class="card mb-3">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="me-3">
<svg xmlns="http://www.w3.org/2000/svg" class="icon text-blue" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<circle cx="12" cy="12" r="9"></circle>
<path d="M12 8h.01"></path>
<path d="M11 12h1v4h1"></path>
</svg>
</div>
<div class="flex-fill">
<div class="font-weight-medium">Historische Währungsumrechnung</div>
<div class="text-muted small">
Transaktionen werden mit den historischen Wechselkursen vom Transaktionsdatum in { portfolio.BaseCurrency } umgerechnet.
Bei nicht verfügbaren historischen Kursen werden aktuelle Kurse verwendet (markiert mit *).
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Portfolio-Zusammenfassung</h3>
</div>
<div class="card-body">
@PortfolioSummary(positionSummary, len(portfolio.Activities))
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h3 class="card-title">Allokation</h3>
</div>
<div class="card-body">
<div id="allocationChart"></div>
<p class="text-muted text-center">Allokations-Chart wird hier implementiert</p>
</div>
</div>
</div>
</div>
</div>
<!-- Add Transaction Modal -->
<div class="modal modal-blur fade" id="addTransactionModal" tabindex="-1" aria-labelledby="addTransactionModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addTransactionModalLabel">Transaktion hinzufügen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
</div>
<form method="post" action="/portfolio/transaction">
<input type="hidden" name="portfolio_id" value={ fmt.Sprintf("%d", portfolio.ID) }/>
<div class="modal-body">
<div class="mb-3">
<label for="transaction-type" class="form-label">Typ</label>
<select class="form-select" id="transaction-type" name="type" required>
<option value="">Wählen Sie einen Typ</option>
<option value="BUY">Kauf</option>
<option value="SELL">Verkauf</option>
<option value="DIVIDEND">Dividende</option>
</select>
</div>
<div class="mb-3">
<label for="transaction-stock" class="form-label">Wertpapier</label>
<input type="text" class="form-control" id="transaction-stock" name="stock" placeholder="z.B. AAPL" required/>
</div>
<div class="mb-3">
<label for="transaction-amount" class="form-label">Anzahl</label>
<input type="number" class="form-control" id="transaction-amount" name="amount" step="0.001" min="0" required/>
</div>
<div class="mb-3">
<label for="transaction-price" class="form-label">Preis</label>
<div class="input-group">
<input type="number" class="form-control" id="transaction-price" name="price" step="0.01" min="0" required/>
<span class="input-group-text">{ portfolio.BaseCurrency }</span>
</div>
</div>
<div class="mb-3">
<label for="transaction-date" class="form-label">Datum</label>
<input type="date" class="form-control" id="transaction-date" name="date" required/>
</div>
<div class="mb-3">
<label for="transaction-note" class="form-label">Notiz (optional)</label>
<textarea class="form-control" id="transaction-note" name="note" rows="2"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
</div>
</div>
<!-- Edit Transaction Modal -->
<div class="modal modal-blur fade" id="editTransactionModal" tabindex="-1" aria-labelledby="editTransactionModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editTransactionModalLabel">Transaktion bearbeiten</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
</div>
<form method="post" action="/portfolio/transaction/edit">
<input type="hidden" name="activity_id" id="edit-activity-id"/>
<input type="hidden" name="portfolio_id" value={ fmt.Sprintf("%d", portfolio.ID) }/>
<div class="modal-body">
<div class="mb-3">
<label for="edit-transaction-type" class="form-label">Typ</label>
<select class="form-select" id="edit-transaction-type" name="type" required>
<option value="">Wählen Sie einen Typ</option>
<option value="BUY">Kauf</option>
<option value="SELL">Verkauf</option>
<option value="DIVIDEND">Dividende</option>
</select>
</div>
<div class="mb-3">
<label for="edit-transaction-stock" class="form-label">Wertpapier</label>
<input type="text" class="form-control" id="edit-transaction-stock" name="stock" placeholder="z.B. AAPL" required/>
</div>
<div class="mb-3">
<label for="edit-transaction-amount" class="form-label">Anzahl</label>
<input type="number" class="form-control" id="edit-transaction-amount" name="amount" step="0.001" min="0" required/>
</div>
<div class="mb-3">
<label for="edit-transaction-price" class="form-label">Preis</label>
<div class="input-group">
<input type="number" class="form-control" id="edit-transaction-price" name="price" step="0.01" min="0" required/>
<span class="input-group-text">{ portfolio.BaseCurrency }</span>
</div>
</div>
<div class="mb-3">
<label for="edit-transaction-date" class="form-label">Datum</label>
<input type="date" class="form-control" id="edit-transaction-date" name="date" required/>
</div>
<div class="mb-3">
<label for="edit-transaction-note" class="form-label">Notiz (optional)</label>
<textarea class="form-control" id="edit-transaction-note" name="note" rows="2"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Transaction Modal -->
<div class="modal modal-blur fade" id="deleteTransactionModal" tabindex="-1" aria-labelledby="deleteTransactionModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteTransactionModalLabel">Transaktion löschen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
</div>
<div class="modal-body">
<p>Möchten Sie diese Transaktion wirklich löschen?</p>
<div class="text-muted" id="delete-transaction-details">
<small>Diese Aktion kann nicht rückgängig gemacht werden.</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<form method="post" action="/portfolio/transaction/delete" style="display: inline;">
<input type="hidden" name="activity_id" id="delete-activity-id"/>
<input type="hidden" name="portfolio_id" value={ fmt.Sprintf("%d", portfolio.ID) }/>
<button type="submit" class="btn btn-danger">Löschen</button>
</form>
</div>
</div>
</div>
</div>
<script>
// Set today's date as default
document.addEventListener('DOMContentLoaded', function() {
const today = new Date().toISOString().split('T')[0];
document.getElementById('transaction-date').value = today;
// Handle edit button clicks
document.addEventListener('click', function(e) {
if (e.target.closest('[data-bs-target="#editTransactionModal"]')) {
const button = e.target.closest('[data-bs-target="#editTransactionModal"]');
// Populate edit modal with transaction data
document.getElementById('edit-activity-id').value = button.dataset.activityId;
document.getElementById('edit-transaction-type').value = button.dataset.activityType;
document.getElementById('edit-transaction-stock').value = button.dataset.activityStock;
document.getElementById('edit-transaction-amount').value = button.dataset.activityAmount;
document.getElementById('edit-transaction-price').value = button.dataset.activityPrice;
document.getElementById('edit-transaction-date').value = button.dataset.activityDate;
document.getElementById('edit-transaction-note').value = button.dataset.activityNote;
}
// Handle delete button clicks
if (e.target.closest('[data-bs-target="#deleteTransactionModal"]')) {
const button = e.target.closest('[data-bs-target="#deleteTransactionModal"]');
// Populate delete modal with transaction data
document.getElementById('delete-activity-id').value = button.dataset.activityId;
// Show transaction details in delete modal
const details = document.getElementById('delete-transaction-details');
details.innerHTML = `
<small>
<strong>${button.dataset.activityType}</strong> - ${button.dataset.activityStock}<br>
${button.dataset.activityAmount} Stück am ${button.dataset.activityDate}
</small>
`;
}
});
});
</script>
}
// Separate component for portfolio summary
templ PortfolioSummary(summary model.PositionSummary, transactionCount int) {
<div class="row">
<div class="col-12">
<div class="mb-3">
<div class="text-muted">Anzahl Transaktionen</div>
<div class="h3 mb-0">{ fmt.Sprintf("%d", transactionCount) }</div>
</div>
<div class="mb-3">
<div class="text-muted">Aktueller Portfoliowert</div>
<div class="h2 mb-0">
{ formatCurrency(summary.TotalValue, summary.Currency) }
</div>
</div>
<div class="mb-3">
<div class="text-muted">Gesamtwert investiert</div>
<div class="h3 mb-0">
{ formatCurrency(summary.TotalCostBasis, summary.Currency) }
</div>
</div>
<div class="mb-3">
<div class="text-muted">Unrealisierte Gewinne/Verluste</div>
<div class={ getColorClass(summary.TotalUnrealizedPL) + " h3 mb-0" }>
{ formatCurrency(summary.TotalUnrealizedPL, summary.Currency) }
if summary.TotalCostBasis > 0 {
<small class="ms-2">({ formatPercentage(summary.TotalUnrealizedPL / summary.TotalCostBasis * 100) })</small>
}
</div>
</div>
<div class="mb-3">
<div class="text-muted">Dividenden erhalten</div>
<div class="h4 mb-0 text-success">
{ formatCurrency(summary.TotalDividends, summary.Currency) }
</div>
</div>
<div class="mb-3">
<div class="text-muted">Gesamtrendite</div>
<div class={ getColorClass(summary.TotalReturn) + " h3 mb-0" }>
{ formatCurrency(summary.TotalReturn, summary.Currency) }
if summary.TotalCostBasis > 0 {
<small class="ms-2">({ formatPercentage(summary.TotalReturnPct) })</small>
}
</div>
</div>
</div>
</div>
}
// Helper functions for formatting
func formatCurrency(value float64, currency string) string {
switch currency {
case "EUR":
return fmt.Sprintf("€%.2f", value)
case "USD":
return fmt.Sprintf("$%.2f", value)
case "GBP":
return fmt.Sprintf("£%.2f", value)
case "CHF":
return fmt.Sprintf("%.2f CHF", value)
default:
return fmt.Sprintf("%.2f %s", value, currency)
}
}
func formatPercentage(value float64) string {
if value > 0 {
return fmt.Sprintf("+%.2f%%", value)
}
return fmt.Sprintf("%.2f%%", value)
}
func getColorClass(value float64) string {
if value > 0 {
return "text-success"
} else if value < 0 {
return "text-danger"
}
return "text-muted"
}
templ PortfolioDetail(authenticated bool, username string, portfolio model.Portfolio, portfolios []model.Portfolio, positions []model.Position, positionSummary model.PositionSummary) {
@components.PageLayout(authenticated, username, portfolio.Name, PortfolioDetailContent(portfolio, positions, positionSummary), portfolios)
}