first commit

This commit is contained in:
Matthias Hinrichs
2025-07-05 03:10:41 +02:00
commit 9b7bdcbc53
39 changed files with 5109 additions and 0 deletions
@@ -0,0 +1,424 @@
package templates
import (
"fmt"
"portfolio-tracker/internal/model"
"portfolio-tracker/internal/web/templates/components"
)
templ PortfolioDetailContent(portfolio model.Portfolio) {
<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(portfolio.Activities) > 0 {
<tr>
<td colspan="6" class="text-center text-muted py-4">
Position calculation will be implemented here
</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 {
{ getConvertedTotal(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">Währungsumrechnung</div>
<div class="text-muted small">
Transaktionen in anderen Währungen werden automatisch in { portfolio.BaseCurrency } umgerechnet.
Wechselkurse werden stündlich aktualisiert.
</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(portfolio.Activities, portfolio.BaseCurrency)
</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(activities []model.Activity, currency string) {
<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", len(activities)) }</div>
</div>
<div class="mb-3">
<div class="text-muted">Gesamtwert investiert</div>
<div class="h2 mb-0">
{ formatTotalInvestedWithConversion(activities, currency) }
</div>
</div>
<div class="mb-3">
<div class="text-muted">Letzter Kauf</div>
<div class="h4 mb-0 text-muted">
{ getLastBuyDate(activities) }
</div>
</div>
</div>
</div>
}
templ PortfolioDetail(authenticated bool, username string, portfolio model.Portfolio, portfolios []model.Portfolio) {
@components.PageLayout(authenticated, username, portfolio.Name, PortfolioDetailContent(portfolio), portfolios)
}