app is now using historical exchange rates for transactions

This commit is contained in:
Matthias Hinrichs
2025-07-05 03:44:53 +02:00
parent a96bbe4d2a
commit aef9342cc5
11 changed files with 926 additions and 237 deletions
+47 -5
View File
@@ -6,6 +6,7 @@ import (
"net/http"
"portfolio-tracker/internal/model"
"portfolio-tracker/internal/util"
"time"
)
func JsonEndpoint(w http.ResponseWriter, r *http.Request) {
@@ -110,7 +111,26 @@ func ClearCurrencyCacheHandler(w http.ResponseWriter, r *http.Request) {
response := map[string]string{
"status": "success",
"message": "Currency cache cleared successfully",
"message": "Currency cache (including historical rates) cleared successfully",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// CleanExpiredCacheHandler removes expired cache entries
func CleanExpiredCacheHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Nur POST erlaubt", http.StatusMethodNotAllowed)
return
}
removedCount := util.CleanExpiredCache()
response := map[string]interface{}{
"status": "success",
"message": "Expired cache entries cleaned",
"removed_count": removedCount,
}
w.Header().Set("Content-Type", "application/json")
@@ -124,12 +144,34 @@ func CurrencyCacheInfoHandler(w http.ResponseWriter, r *http.Request) {
return
}
cacheInfo := util.GetCacheInfo()
detailedCacheInfo := util.GetDetailedCacheInfo()
// Separate historical and current rates
historicalRates := make(map[string]interface{})
currentRates := make(map[string]interface{})
for key, cached := range detailedCacheInfo {
rateInfo := map[string]interface{}{
"rate": cached.Rate,
"timestamp": cached.Timestamp,
"date": cached.Date,
"age_hours": time.Since(cached.Timestamp).Hours(),
}
if cached.Date == cached.Timestamp.Format("2006-01-02") {
currentRates[key] = rateInfo
} else {
historicalRates[key] = rateInfo
}
}
response := map[string]interface{}{
"status": "success",
"cache_size": len(cacheInfo),
"entries": cacheInfo,
"status": "success",
"total_entries": len(detailedCacheInfo),
"historical_rates": historicalRates,
"current_rates": currentRates,
"historical_count": len(historicalRates),
"current_count": len(currentRates),
}
w.Header().Set("Content-Type", "application/json")
+230 -16
View File
@@ -17,32 +17,56 @@ type ExchangeRateResponse struct {
Rates map[string]float64 `json:"rates"`
}
// HistoricalExchangeRateResponse represents historical rate response
type HistoricalExchangeRateResponse struct {
Success bool `json:"success"`
Historical bool `json:"historical"`
Base string `json:"base"`
Date string `json:"date"`
Rates map[string]float64 `json:"rates"`
TimeLastUpdate int64 `json:"time_last_update_unix"`
}
// CachedRate stores exchange rate with timestamp
type CachedRate struct {
Rate float64
Timestamp time.Time
Date string // The date this rate is for (YYYY-MM-DD format)
}
// Currency conversion cache
var (
exchangeRateCache = make(map[string]CachedRate)
cacheMutex sync.RWMutex
cacheValidDuration = 1 * time.Hour // Cache rates for 1 hour
cacheValidDuration = 24 * time.Hour // Cache historical rates for 24 hours
currentRateCache = 1 * time.Hour // Cache current rates for 1 hour
)
// GetExchangeRate fetches the exchange rate from fromCurrency to toCurrency
// GetExchangeRate fetches the current exchange rate from fromCurrency to toCurrency
func GetExchangeRate(fromCurrency, toCurrency string) (float64, error) {
return GetHistoricalExchangeRate(fromCurrency, toCurrency, time.Now())
}
// GetHistoricalExchangeRate fetches the exchange rate for a specific date
func GetHistoricalExchangeRate(fromCurrency, toCurrency string, date time.Time) (float64, error) {
// If currencies are the same, return 1.0
if fromCurrency == toCurrency {
return 1.0, nil
}
cacheKey := fmt.Sprintf("%s_%s", fromCurrency, toCurrency)
dateStr := date.Format("2006-01-02")
cacheKey := fmt.Sprintf("%s_%s_%s", fromCurrency, toCurrency, dateStr)
// Check cache first
cacheMutex.RLock()
if cached, exists := exchangeRateCache[cacheKey]; exists {
if time.Since(cached.Timestamp) < cacheValidDuration {
// Determine cache validity based on whether it's historical or current
validDuration := cacheValidDuration
if isToday(date) {
validDuration = currentRateCache
}
if time.Since(cached.Timestamp) < validDuration {
cacheMutex.RUnlock()
return cached.Rate, nil
}
@@ -50,7 +74,7 @@ func GetExchangeRate(fromCurrency, toCurrency string) (float64, error) {
cacheMutex.RUnlock()
// Fetch from API if not in cache or cache is expired
rate, err := fetchExchangeRateFromAPI(fromCurrency, toCurrency)
rate, err := fetchHistoricalExchangeRateFromAPI(fromCurrency, toCurrency, date)
if err != nil {
return 0, err
}
@@ -60,16 +84,27 @@ func GetExchangeRate(fromCurrency, toCurrency string) (float64, error) {
exchangeRateCache[cacheKey] = CachedRate{
Rate: rate,
Timestamp: time.Now(),
Date: dateStr,
}
cacheMutex.Unlock()
return rate, nil
}
// fetchExchangeRateFromAPI fetches exchange rate from a free API
func fetchExchangeRateFromAPI(fromCurrency, toCurrency string) (float64, error) {
// Using exchangerate-api.com (completely free, no API key required)
url := fmt.Sprintf("https://api.exchangerate-api.com/v4/latest/%s", fromCurrency)
// fetchHistoricalExchangeRateFromAPI fetches exchange rate for a specific date
func fetchHistoricalExchangeRateFromAPI(fromCurrency, toCurrency string, date time.Time) (float64, error) {
var url string
dateStr := date.Format("2006-01-02")
today := time.Now().Format("2006-01-02")
// Use different endpoints for historical vs current rates
if dateStr == today {
// Current rates
url = fmt.Sprintf("https://api.exchangerate-api.com/v4/latest/%s", fromCurrency)
} else {
// Historical rates - try exchangerate-api.com historical endpoint
url = fmt.Sprintf("https://api.exchangerate-api.com/v4/historical/%s/%s", fromCurrency, dateStr)
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
@@ -78,7 +113,7 @@ func fetchExchangeRateFromAPI(fromCurrency, toCurrency string) (float64, error)
req.Header.Set("User-Agent", "Portfolio-Tracker/1.0")
client := &http.Client{Timeout: 10 * time.Second}
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
return 0, fmt.Errorf("error fetching exchange rate: %v", err)
@@ -86,6 +121,10 @@ func fetchExchangeRateFromAPI(fromCurrency, toCurrency string) (float64, error)
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
// If historical API fails, try fallback strategy
if dateStr != today {
return fetchHistoricalRateFallback(fromCurrency, toCurrency, date)
}
return 0, fmt.Errorf("API returned status %d", resp.StatusCode)
}
@@ -94,7 +133,7 @@ func fetchExchangeRateFromAPI(fromCurrency, toCurrency string) (float64, error)
return 0, fmt.Errorf("error reading response: %v", err)
}
// Parse exchangerate-api.com response format
// Try parsing as standard response first
var apiResp struct {
Base string `json:"base"`
Date string `json:"date"`
@@ -113,13 +152,111 @@ func fetchExchangeRateFromAPI(fromCurrency, toCurrency string) (float64, error)
return rate, nil
}
// ConvertCurrency converts an amount from one currency to another
// fetchHistoricalRateFallback tries alternative methods for historical rates
func fetchHistoricalRateFallback(fromCurrency, toCurrency string, date time.Time) (float64, error) {
// Try frankfurter.app as fallback for historical rates (free and reliable)
dateStr := date.Format("2006-01-02")
url := fmt.Sprintf("https://api.frankfurter.app/%s?from=%s&to=%s", dateStr, fromCurrency, toCurrency)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return 0, fmt.Errorf("error creating fallback request: %v", err)
}
req.Header.Set("User-Agent", "Portfolio-Tracker/1.0")
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
// If historical rate fails, use current rate as last resort
return getCurrentRateAsFallback(fromCurrency, toCurrency)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return getCurrentRateAsFallback(fromCurrency, toCurrency)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return getCurrentRateAsFallback(fromCurrency, toCurrency)
}
var fallbackResp struct {
Amount float64 `json:"amount"`
Base string `json:"base"`
Date string `json:"date"`
Rates map[string]float64 `json:"rates"`
}
if err := json.Unmarshal(body, &fallbackResp); err != nil {
return getCurrentRateAsFallback(fromCurrency, toCurrency)
}
if rate, exists := fallbackResp.Rates[toCurrency]; exists {
return rate, nil
}
return getCurrentRateAsFallback(fromCurrency, toCurrency)
}
// getCurrentRateAsFallback gets current rate when historical rate is unavailable
func getCurrentRateAsFallback(fromCurrency, toCurrency string) (float64, error) {
url := fmt.Sprintf("https://api.exchangerate-api.com/v4/latest/%s", fromCurrency)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return 0, fmt.Errorf("error creating fallback request: %v", err)
}
req.Header.Set("User-Agent", "Portfolio-Tracker/1.0")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return 0, fmt.Errorf("error fetching fallback rate: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return 0, fmt.Errorf("fallback API returned status %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, fmt.Errorf("error reading fallback response: %v", err)
}
var apiResp struct {
Base string `json:"base"`
Date string `json:"date"`
Rates map[string]float64 `json:"rates"`
}
if err := json.Unmarshal(body, &apiResp); err != nil {
return 0, fmt.Errorf("error parsing fallback JSON: %v", err)
}
rate, exists := apiResp.Rates[toCurrency]
if !exists {
return 0, fmt.Errorf("currency %s not found in fallback response", toCurrency)
}
return rate, nil
}
// ConvertCurrency converts an amount from one currency to another using current rates
func ConvertCurrency(amount float64, fromCurrency, toCurrency string) (float64, error) {
return ConvertCurrencyHistorical(amount, fromCurrency, toCurrency, time.Now())
}
// ConvertCurrencyHistorical converts an amount using historical exchange rate for a specific date
func ConvertCurrencyHistorical(amount float64, fromCurrency, toCurrency string, date time.Time) (float64, error) {
if fromCurrency == toCurrency {
return amount, nil
}
rate, err := GetExchangeRate(fromCurrency, toCurrency)
rate, err := GetHistoricalExchangeRate(fromCurrency, toCurrency, date)
if err != nil {
return 0, err
}
@@ -178,11 +315,16 @@ func FormatCurrencyWithSymbol(amount float64, currency string) string {
// ConvertAndFormat converts currency and formats it nicely
func ConvertAndFormat(amount float64, fromCurrency, toCurrency string) string {
return ConvertAndFormatHistorical(amount, fromCurrency, toCurrency, time.Now())
}
// ConvertAndFormatHistorical converts currency using historical rate and formats it
func ConvertAndFormatHistorical(amount float64, fromCurrency, toCurrency string, date time.Time) string {
if fromCurrency == toCurrency {
return FormatCurrencyWithSymbol(amount, toCurrency)
}
convertedAmount, err := ConvertCurrency(amount, fromCurrency, toCurrency)
convertedAmount, err := ConvertCurrencyHistorical(amount, fromCurrency, toCurrency, date)
if err != nil {
// If conversion fails, return original amount with note
return fmt.Sprintf("%.2f %s (conv. error)", amount, fromCurrency)
@@ -191,13 +333,18 @@ func ConvertAndFormat(amount float64, fromCurrency, toCurrency string) string {
return FormatCurrencyWithSymbol(convertedAmount, toCurrency)
}
// BatchConvertCurrency converts multiple amounts in one API call for efficiency
// BatchConvertCurrency converts multiple amounts in one API call for efficiency using current rates
func BatchConvertCurrency(amounts []float64, fromCurrency, toCurrency string) ([]float64, error) {
return BatchConvertCurrencyHistorical(amounts, fromCurrency, toCurrency, time.Now())
}
// BatchConvertCurrencyHistorical converts multiple amounts using historical rate for a specific date
func BatchConvertCurrencyHistorical(amounts []float64, fromCurrency, toCurrency string, date time.Time) ([]float64, error) {
if fromCurrency == toCurrency {
return amounts, nil
}
rate, err := GetExchangeRate(fromCurrency, toCurrency)
rate, err := GetHistoricalExchangeRate(fromCurrency, toCurrency, date)
if err != nil {
return nil, err
}
@@ -228,3 +375,70 @@ func GetCacheInfo() map[string]time.Time {
}
return info
}
// GetDetailedCacheInfo returns detailed information about cached exchange rates
func GetDetailedCacheInfo() map[string]CachedRate {
cacheMutex.RLock()
defer cacheMutex.RUnlock()
info := make(map[string]CachedRate)
for key, cached := range exchangeRateCache {
info[key] = cached
}
return info
}
// CleanExpiredCache removes expired cache entries
func CleanExpiredCache() int {
cacheMutex.Lock()
defer cacheMutex.Unlock()
removed := 0
now := time.Now()
for key, cached := range exchangeRateCache {
// Determine if this is a current or historical rate
validDuration := cacheValidDuration
if isToday(parseDate(cached.Date)) {
validDuration = currentRateCache
}
if now.Sub(cached.Timestamp) > validDuration {
delete(exchangeRateCache, key)
removed++
}
}
return removed
}
// Helper functions
// isToday checks if a date is today
func isToday(date time.Time) bool {
now := time.Now()
return date.Year() == now.Year() && date.Month() == now.Month() && date.Day() == now.Day()
}
// parseDate parses a date string in YYYY-MM-DD format
func parseDate(dateStr string) time.Time {
date, err := time.Parse("2006-01-02", dateStr)
if err != nil {
return time.Time{}
}
return date
}
// GetExchangeRateWithFallback gets exchange rate with multiple fallback strategies
func GetExchangeRateWithFallback(fromCurrency, toCurrency string, date time.Time) (float64, bool, error) {
rate, err := GetHistoricalExchangeRate(fromCurrency, toCurrency, date)
if err != nil {
// Try with current date as fallback
currentRate, currentErr := GetExchangeRate(fromCurrency, toCurrency)
if currentErr != nil {
return 0, false, fmt.Errorf("both historical (%v) and current (%v) rate fetching failed", err, currentErr)
}
return currentRate, false, nil // false indicates fallback was used
}
return rate, true, nil // true indicates historical rate was used
}
+223 -14
View File
@@ -153,13 +153,15 @@ func TestGetCacheInfo(t *testing.T) {
// Add test data to cache
testTime := time.Now()
cacheMutex.Lock()
exchangeRateCache["EUR_USD"] = CachedRate{
exchangeRateCache["EUR_USD_2024-01-01"] = CachedRate{
Rate: 1.2,
Timestamp: testTime,
Date: "2024-01-01",
}
exchangeRateCache["GBP_EUR"] = CachedRate{
exchangeRateCache["GBP_EUR_2024-01-01"] = CachedRate{
Rate: 1.15,
Timestamp: testTime,
Date: "2024-01-01",
}
cacheMutex.Unlock()
@@ -169,14 +171,14 @@ func TestGetCacheInfo(t *testing.T) {
t.Errorf("Expected 2 cache entries, got %d", len(info))
}
if timestamp, exists := info["EUR_USD"]; !exists {
t.Error("Expected EUR_USD entry in cache info")
if timestamp, exists := info["EUR_USD_2024-01-01"]; !exists {
t.Error("Expected EUR_USD_2024-01-01 entry in cache info")
} else if !timestamp.Equal(testTime) {
t.Error("Expected timestamp to match test time")
}
if timestamp, exists := info["GBP_EUR"]; !exists {
t.Error("Expected GBP_EUR entry in cache info")
if timestamp, exists := info["GBP_EUR_2024-01-01"]; !exists {
t.Error("Expected GBP_EUR_2024-01-01 entry in cache info")
} else if !timestamp.Equal(testTime) {
t.Error("Expected timestamp to match test time")
}
@@ -186,20 +188,227 @@ func TestGetCacheInfo(t *testing.T) {
func TestCacheExpiration(t *testing.T) {
ClearCache()
// Add expired entry to cache
expiredTime := time.Now().Add(-2 * time.Hour) // 2 hours ago
// Add expired entry to cache with a specific rate
expiredTime := time.Now().Add(-25 * time.Hour) // 25 hours ago (older than 24h cache)
testDate := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
cacheKey := "USD_EUR_2024-01-01"
expiredRate := 999.999 // Unrealistic rate to detect if cache is used
cacheMutex.Lock()
exchangeRateCache["TEST_EXPIRED"] = CachedRate{
Rate: 1.0,
exchangeRateCache[cacheKey] = CachedRate{
Rate: expiredRate,
Timestamp: expiredTime,
Date: "2024-01-01",
}
cacheMutex.Unlock()
// This should not use the expired cache (but will fail API call)
// We're mainly testing that it doesn't return the cached expired value
_, err := GetExchangeRate("TEST", "EXPIRED")
// Verify the expired entry exists
cacheMutex.RLock()
_, exists := exchangeRateCache[cacheKey]
cacheMutex.RUnlock()
if !exists {
t.Error("Expected expired cache entry to exist before test")
return
}
// This should not use the expired cache, it should try to fetch from API
// Even if API fails, it should not return the expired cached rate
rate, err := GetHistoricalExchangeRate("USD", "EUR", testDate)
// Either:
// 1. We get a reasonable rate from API (not the cached unrealistic rate)
// 2. We get an error (which is fine, means cache wasn't used)
if err == nil {
t.Error("Expected error due to invalid currency pair, but got none")
// If we got a rate, it should not be the expired cached rate
if rate == expiredRate {
t.Errorf("Function returned expired cached rate %f, should have fetched from API", expiredRate)
}
// A reasonable EUR/USD rate should be between 0.5 and 2.0
if rate < 0.5 || rate > 2.0 {
t.Errorf("Got unreasonable exchange rate %f, might be using expired cache", rate)
}
}
// If err != nil, that's fine - it means the API call was attempted and failed
// The important thing is that the expired cache was not used
}
// Test historical exchange rate functionality
func TestGetHistoricalExchangeRate_SameCurrency(t *testing.T) {
testDate := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
rate, err := GetHistoricalExchangeRate("USD", "USD", testDate)
if err != nil {
t.Errorf("Expected no error for same currency, got %v", err)
}
if rate != 1.0 {
t.Errorf("Expected rate 1.0 for same currency, got %f", rate)
}
}
func TestConvertCurrencyHistorical_SameCurrency(t *testing.T) {
amount := 100.0
testDate := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
converted, err := ConvertCurrencyHistorical(amount, "EUR", "EUR", testDate)
if err != nil {
t.Errorf("Expected no error for same currency conversion, got %v", err)
}
if converted != amount {
t.Errorf("Expected %f for same currency conversion, got %f", amount, converted)
}
}
func TestBatchConvertCurrencyHistorical_SameCurrency(t *testing.T) {
amounts := []float64{100.0, 200.0, 300.0}
currency := "EUR"
testDate := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
converted, err := BatchConvertCurrencyHistorical(amounts, currency, currency, testDate)
if err != nil {
t.Errorf("Expected no error for same currency batch conversion, got %v", err)
}
if len(converted) != len(amounts) {
t.Errorf("Expected %d converted amounts, got %d", len(amounts), len(converted))
}
for i, amount := range amounts {
if converted[i] != amount {
t.Errorf("Expected converted[%d] = %f, got %f", i, amount, converted[i])
}
}
}
func TestConvertAndFormatHistorical_SameCurrency(t *testing.T) {
amount := 100.0
currency := "USD"
testDate := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
result := ConvertAndFormatHistorical(amount, currency, currency, testDate)
expected := FormatCurrencyWithSymbol(amount, currency)
if result != expected {
t.Errorf("ConvertAndFormatHistorical(%f, %s, %s, %v) = %s, expected %s",
amount, currency, currency, testDate, result, expected)
}
}
func TestGetExchangeRateWithFallback_SameCurrency(t *testing.T) {
testDate := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
rate, isHistorical, err := GetExchangeRateWithFallback("USD", "USD", testDate)
if err != nil {
t.Errorf("Expected no error for same currency, got %v", err)
}
if rate != 1.0 {
t.Errorf("Expected rate 1.0 for same currency, got %f", rate)
}
if !isHistorical {
t.Error("Expected isHistorical to be true for same currency")
}
}
func TestCleanExpiredCache(t *testing.T) {
// Clear cache first
ClearCache()
// Add some test data to cache
now := time.Now()
expiredTime := now.Add(-25 * time.Hour) // Older than 24 hours
recentTime := now.Add(-30 * time.Minute) // Recent
cacheMutex.Lock()
exchangeRateCache["OLD_RATE_2024-01-01"] = CachedRate{
Rate: 1.0,
Timestamp: expiredTime,
Date: "2024-01-01",
}
exchangeRateCache["NEW_RATE_"+now.Format("2006-01-02")] = CachedRate{
Rate: 1.1,
Timestamp: recentTime,
Date: now.Format("2006-01-02"),
}
cacheMutex.Unlock()
// Clean expired cache
removed := CleanExpiredCache()
// Should have removed the old entry
if removed != 1 {
t.Errorf("Expected 1 removed entry, got %d", removed)
}
// Verify the recent entry is still there
info := GetCacheInfo()
if len(info) != 1 {
t.Errorf("Expected 1 remaining cache entry, got %d", len(info))
}
}
func TestGetDetailedCacheInfo(t *testing.T) {
// Clear cache first
ClearCache()
// Add test data to cache
testTime := time.Now()
cacheMutex.Lock()
exchangeRateCache["EUR_USD_2024-01-01"] = CachedRate{
Rate: 1.2,
Timestamp: testTime,
Date: "2024-01-01",
}
cacheMutex.Unlock()
info := GetDetailedCacheInfo()
if len(info) != 1 {
t.Errorf("Expected 1 cache entry, got %d", len(info))
}
if cached, exists := info["EUR_USD_2024-01-01"]; !exists {
t.Error("Expected EUR_USD_2024-01-01 entry in detailed cache info")
} else {
if cached.Rate != 1.2 {
t.Errorf("Expected rate 1.2, got %f", cached.Rate)
}
if cached.Date != "2024-01-01" {
t.Errorf("Expected date 2024-01-01, got %s", cached.Date)
}
if !cached.Timestamp.Equal(testTime) {
t.Error("Expected timestamp to match test time")
}
}
}
func TestIsToday(t *testing.T) {
now := time.Now()
yesterday := now.AddDate(0, 0, -1)
tomorrow := now.AddDate(0, 0, 1)
if !isToday(now) {
t.Error("Expected isToday(now) to be true")
}
if isToday(yesterday) {
t.Error("Expected isToday(yesterday) to be false")
}
if isToday(tomorrow) {
t.Error("Expected isToday(tomorrow) to be false")
}
}
func TestParseDate(t *testing.T) {
testDate := "2024-01-01"
expected := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
result := parseDate(testDate)
if !result.Equal(expected) {
t.Errorf("Expected parsed date %v, got %v", expected, result)
}
// Test invalid date
invalidResult := parseDate("invalid-date")
if !invalidResult.IsZero() {
t.Error("Expected zero time for invalid date")
}
}
+180 -16
View File
@@ -18,7 +18,7 @@ func calculateTotalInvested(activities []model.Activity) float64 {
return total
}
// calculateTotalInvestedInBaseCurrency calculates total invested converted to portfolio base currency
// calculateTotalInvestedInBaseCurrency calculates total invested converted to portfolio base currency using historical rates
func calculateTotalInvestedInBaseCurrency(activities []model.Activity, baseCurrency string) float64 {
var total float64 = 0
for _, activity := range activities {
@@ -28,10 +28,17 @@ func calculateTotalInvestedInBaseCurrency(activities []model.Activity, baseCurre
if activity.Currency == baseCurrency {
total += activityTotal
} else {
convertedTotal, err := util.ConvertCurrency(activityTotal, activity.Currency, baseCurrency)
// Use historical exchange rate for the transaction date
convertedTotal, err := util.ConvertCurrencyHistorical(activityTotal, activity.Currency, baseCurrency, activity.Date)
if err != nil {
// If conversion fails, add original amount (fallback)
total += activityTotal
// If historical conversion fails, try current rate as fallback
fallbackTotal, fallbackErr := util.ConvertCurrency(activityTotal, activity.Currency, baseCurrency)
if fallbackErr != nil {
// If all conversions fail, add original amount (last resort)
total += activityTotal
} else {
total += fallbackTotal
}
} else {
total += convertedTotal
}
@@ -55,13 +62,13 @@ func getLastBuyDate(activities []model.Activity) string {
return lastBuy.Format("02.01.2006")
}
// convertActivityPrice converts activity price to portfolio base currency if different
// convertActivityPrice converts activity price to portfolio base currency using historical rate
func convertActivityPrice(activity model.Activity, portfolioBaseCurrency string) (float64, string, error) {
if activity.Currency == portfolioBaseCurrency {
return activity.Price, "", nil // No conversion needed
}
convertedPrice, err := util.ConvertCurrency(activity.Price, activity.Currency, portfolioBaseCurrency)
convertedPrice, err := util.ConvertCurrencyHistorical(activity.Price, activity.Currency, portfolioBaseCurrency, activity.Date)
if err != nil {
return 0, "", err
}
@@ -69,13 +76,13 @@ func convertActivityPrice(activity model.Activity, portfolioBaseCurrency string)
return convertedPrice, portfolioBaseCurrency, nil
}
// formatActivityPrice formats activity price with conversion if needed
// formatActivityPrice formats activity price with conversion if needed using historical rates
func formatActivityPrice(activity model.Activity, portfolioBaseCurrency string) string {
if activity.Currency == portfolioBaseCurrency {
return fmt.Sprintf("%.2f %s", activity.Price, activity.Currency)
}
convertedPrice, err := util.ConvertCurrency(activity.Price, activity.Currency, portfolioBaseCurrency)
convertedPrice, err := util.ConvertCurrencyHistorical(activity.Price, activity.Currency, portfolioBaseCurrency, activity.Date)
if err != nil {
return fmt.Sprintf("%.2f %s (conv. error)", activity.Price, activity.Currency)
}
@@ -83,7 +90,7 @@ func formatActivityPrice(activity model.Activity, portfolioBaseCurrency string)
return fmt.Sprintf("%.2f %s (~%.2f %s)", activity.Price, activity.Currency, convertedPrice, portfolioBaseCurrency)
}
// formatActivityTotal formats activity total with conversion if needed
// formatActivityTotal formats activity total with conversion if needed using historical rates
func formatActivityTotal(activity model.Activity, portfolioBaseCurrency string) string {
total := activity.Amount * activity.Price
@@ -91,7 +98,7 @@ func formatActivityTotal(activity model.Activity, portfolioBaseCurrency string)
return fmt.Sprintf("%.2f %s", total, activity.Currency)
}
convertedTotal, err := util.ConvertCurrency(total, activity.Currency, portfolioBaseCurrency)
convertedTotal, err := util.ConvertCurrencyHistorical(total, activity.Currency, portfolioBaseCurrency, activity.Date)
if err != nil {
return fmt.Sprintf("%.2f %s (conv. error)", total, activity.Currency)
}
@@ -99,28 +106,38 @@ func formatActivityTotal(activity model.Activity, portfolioBaseCurrency string)
return fmt.Sprintf("%.2f %s (~%.2f %s)", total, activity.Currency, convertedTotal, portfolioBaseCurrency)
}
// getConvertedPrice returns the converted price or original if conversion fails
// getConvertedPrice returns the converted price using historical exchange rate or original if conversion fails
func getConvertedPrice(activity model.Activity, portfolioBaseCurrency string) string {
if activity.Currency == portfolioBaseCurrency {
return ""
}
convertedPrice, err := util.ConvertCurrency(activity.Price, activity.Currency, portfolioBaseCurrency)
// Use historical exchange rate for the transaction date
convertedPrice, isHistorical, err := util.GetExchangeRateWithFallback(activity.Currency, portfolioBaseCurrency, activity.Date)
if err != nil {
return "Conv. Error"
}
return fmt.Sprintf("%.2f %s", convertedPrice, portfolioBaseCurrency)
finalPrice := activity.Price * convertedPrice
// Add indicator if fallback (current) rate was used instead of historical
if !isHistorical {
return fmt.Sprintf("%.2f %s*", finalPrice, portfolioBaseCurrency)
}
return fmt.Sprintf("%.2f %s", finalPrice, portfolioBaseCurrency)
}
// getConvertedTotal returns the converted total or original if conversion fails
// getConvertedTotal returns the converted total using historical exchange rate or original if conversion fails
func getConvertedTotal(activity model.Activity, portfolioBaseCurrency string) string {
if activity.Currency == portfolioBaseCurrency {
return ""
}
total := activity.Amount * activity.Price
convertedTotal, err := util.ConvertCurrency(total, activity.Currency, portfolioBaseCurrency)
// Use historical exchange rate for the transaction date
convertedTotal, err := util.ConvertCurrencyHistorical(total, activity.Currency, portfolioBaseCurrency, activity.Date)
if err != nil {
return "Conv. Error"
}
@@ -128,15 +145,47 @@ func getConvertedTotal(activity model.Activity, portfolioBaseCurrency string) st
return fmt.Sprintf("%.2f %s", convertedTotal, portfolioBaseCurrency)
}
// formatTotalInvestedWithConversion formats total invested with currency info
// getConvertedTotalWithFallbackInfo returns the converted total with information about rate source
func getConvertedTotalWithFallbackInfo(activity model.Activity, portfolioBaseCurrency string) string {
if activity.Currency == portfolioBaseCurrency {
return ""
}
total := activity.Amount * activity.Price
// Use historical exchange rate with fallback information
rate, isHistorical, err := util.GetExchangeRateWithFallback(activity.Currency, portfolioBaseCurrency, activity.Date)
if err != nil {
return "Conv. Error"
}
convertedTotal := total * rate
// Add indicator if fallback (current) rate was used instead of historical
if !isHistorical {
return fmt.Sprintf("%.2f %s*", convertedTotal, portfolioBaseCurrency)
}
return fmt.Sprintf("%.2f %s", convertedTotal, portfolioBaseCurrency)
}
// formatTotalInvestedWithConversion formats total invested with currency info and conversion indicators
func formatTotalInvestedWithConversion(activities []model.Activity, baseCurrency string) string {
convertedTotal := calculateTotalInvestedInBaseCurrency(activities, baseCurrency)
// Check if any activities have different currencies
hasDifferentCurrencies := false
hasHistoricalConversions := false
for _, activity := range activities {
if activity.Type == model.Buy && activity.Currency != baseCurrency {
hasDifferentCurrencies = true
// Check if we can get historical rate for this transaction
_, isHistorical, err := util.GetExchangeRateWithFallback(activity.Currency, baseCurrency, activity.Date)
if err == nil && isHistorical {
hasHistoricalConversions = true
}
break
}
}
@@ -145,5 +194,120 @@ func formatTotalInvestedWithConversion(activities []model.Activity, baseCurrency
return fmt.Sprintf("%.2f %s", convertedTotal, baseCurrency)
}
if hasHistoricalConversions {
return fmt.Sprintf("%.2f %s (historical rates)", convertedTotal, baseCurrency)
}
return fmt.Sprintf("%.2f %s (converted)", convertedTotal, baseCurrency)
}
// calculateTotalInvestedByDate calculates total invested up to a specific date
func calculateTotalInvestedByDate(activities []model.Activity, baseCurrency string, endDate time.Time) float64 {
var total float64 = 0
for _, activity := range activities {
if activity.Type == model.Buy && activity.Date.Before(endDate.AddDate(0, 0, 1)) { // Include transactions on endDate
activityTotal := activity.Amount * activity.Price
if activity.Currency == baseCurrency {
total += activityTotal
} else {
// Use historical exchange rate for the transaction date
convertedTotal, err := util.ConvertCurrencyHistorical(activityTotal, activity.Currency, baseCurrency, activity.Date)
if err != nil {
// If historical conversion fails, try current rate as fallback
fallbackTotal, fallbackErr := util.ConvertCurrency(activityTotal, activity.Currency, baseCurrency)
if fallbackErr != nil {
// If all conversions fail, add original amount (last resort)
total += activityTotal
} else {
total += fallbackTotal
}
} else {
total += convertedTotal
}
}
}
}
return total
}
// getConversionInfo returns information about currency conversions used in the portfolio
func getConversionInfo(activities []model.Activity, baseCurrency string) map[string]interface{} {
info := map[string]interface{}{
"hasDifferentCurrencies": false,
"historicalConversions": 0,
"fallbackConversions": 0,
"errorConversions": 0,
"currencies": make(map[string]int),
}
currencies := make(map[string]int)
historicalCount := 0
fallbackCount := 0
errorCount := 0
for _, activity := range activities {
if activity.Type == model.Buy {
currencies[activity.Currency]++
if activity.Currency != baseCurrency {
info["hasDifferentCurrencies"] = true
// Check conversion status
_, isHistorical, err := util.GetExchangeRateWithFallback(activity.Currency, baseCurrency, activity.Date)
if err != nil {
errorCount++
} else if isHistorical {
historicalCount++
} else {
fallbackCount++
}
}
}
}
info["historicalConversions"] = historicalCount
info["fallbackConversions"] = fallbackCount
info["errorConversions"] = errorCount
info["currencies"] = currencies
return info
}
// formatCurrencyWithConversionNote formats currency amount with appropriate conversion notes
func formatCurrencyWithConversionNote(amount float64, fromCurrency, toCurrency string, transactionDate time.Time) string {
if fromCurrency == toCurrency {
return util.FormatCurrencyWithSymbol(amount, toCurrency)
}
rate, isHistorical, err := util.GetExchangeRateWithFallback(fromCurrency, toCurrency, transactionDate)
if err != nil {
return fmt.Sprintf("%.2f %s (conv. error)", amount, fromCurrency)
}
convertedAmount := amount * rate
if isHistorical {
return fmt.Sprintf("%.2f %s (historical rate)", convertedAmount, toCurrency)
}
return fmt.Sprintf("%.2f %s (current rate)", convertedAmount, toCurrency)
}
// getActivityConversionStatus returns the conversion status for a specific activity
func getActivityConversionStatus(activity model.Activity, portfolioBaseCurrency string) string {
if activity.Currency == portfolioBaseCurrency {
return "same_currency"
}
_, isHistorical, err := util.GetExchangeRateWithFallback(activity.Currency, portfolioBaseCurrency, activity.Date)
if err != nil {
return "error"
}
if isHistorical {
return "historical"
}
return "fallback"
}
@@ -128,7 +128,7 @@ templ PortfolioDetailContent(portfolio model.Portfolio) {
<td>{ fmt.Sprintf("%.2f %s", activity.Amount * activity.Price, activity.Currency) }</td>
<td>
if activity.Currency != portfolio.BaseCurrency {
{ getConvertedTotal(activity, portfolio.BaseCurrency) }
{ getConvertedTotalWithFallbackInfo(activity, portfolio.BaseCurrency) }
} else {
<span class="text-muted">-</span>
}
@@ -194,10 +194,10 @@ templ PortfolioDetailContent(portfolio model.Portfolio) {
</svg>
</div>
<div class="flex-fill">
<div class="font-weight-medium">Währungsumrechnung</div>
<div class="font-weight-medium">Historische Währungsumrechnung</div>
<div class="text-muted small">
Transaktionen in anderen Währungen werden automatisch in { portfolio.BaseCurrency } umgerechnet.
Wechselkurse werden stündlich aktualisiert.
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>
@@ -408,6 +408,9 @@ templ PortfolioSummary(activities []model.Activity, currency string) {
<div class="h2 mb-0">
{ formatTotalInvestedWithConversion(activities, currency) }
</div>
<div class="text-muted small">
* = Aktueller Kurs verwendet (historischer Kurs nicht verfügbar)
</div>
</div>
<div class="mb-3">
<div class="text-muted">Letzter Kauf</div>
@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.778
// templ: version: v0.3.906
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
@@ -35,33 +35,33 @@ func PortfolioDetailContent(portfolio model.Portfolio) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<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\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<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\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(portfolio.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 14, Col: 44}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 14, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" (")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " (")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(portfolio.BaseCurrency)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 14, Col: 72}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 14, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(")</h2><div class=\"page-subtitle\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, ")</h2><div class=\"page-subtitle\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -69,175 +69,179 @@ func PortfolioDetailContent(portfolio model.Portfolio) templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(portfolio.Description)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 17, Col: 30}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 17, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Erstellt am ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "Erstellt am ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(portfolio.CreatedAt.Format("02.01.2006"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 19, Col: 61}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 19, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</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>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</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 templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(portfolio.Activities) > 0 {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<tr><td colspan=\"6\" class=\"text-center text-muted py-4\">Position calculation will be implemented here</td></tr>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<tr><td colspan=\"6\" class=\"text-center text-muted py-4\">Position calculation will be implemented here</td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<tr><td colspan=\"6\" class=\"text-center text-muted py-4\">Keine Positionen vorhanden. Fügen Sie Ihre erste Transaktion hinzu.</td></tr>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<tr><td colspan=\"6\" class=\"text-center text-muted py-4\">Keine Positionen vorhanden. Fügen Sie Ihre erste Transaktion hinzu.</td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</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 (")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</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 (")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(portfolio.BaseCurrency)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 91, Col: 45}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 91, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(")</th><th>Gesamt</th><th>Gesamt (")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, ")</th><th>Gesamt</th><th>Gesamt (")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(portfolio.BaseCurrency)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 93, Col: 46}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 93, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(")</th><th></th></tr></thead> <tbody>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, ")</th><th></th></tr></thead> <tbody>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(portfolio.Activities) > 0 {
for i, activity := range portfolio.Activities {
if i < 10 {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<tr><td>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<tr><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(activity.Date.Format("02.01.2006"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 102, Col: 53}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 102, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td><td>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if activity.Type == "BUY" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<span class=\"badge bg-green\">Kauf</span>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<span class=\"badge bg-green\">Kauf</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if activity.Type == "SELL" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<span class=\"badge bg-red\">Verkauf</span>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<span class=\"badge bg-red\">Verkauf</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if activity.Type == "DIVIDEND" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<span class=\"badge bg-blue\">Dividende</span>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<span class=\"badge bg-blue\">Dividende</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<span class=\"badge bg-secondary\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<span class=\"badge bg-secondary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(string(activity.Type))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 111, Col: 71}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 111, Col: 71}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td><td><a href=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</td><td><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 templ.SafeURL = templ.URL("/details?stock=" + activity.Stock)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var10)))
var templ_7745c5c3_Var10 templ.SafeURL
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL("/details?stock=" + activity.Stock))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 115, Col: 69}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" class=\"text-decoration-none\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" class=\"text-decoration-none\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(activity.Stock)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 116, Col: 31}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 116, Col: 31}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></td><td>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</a></td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.3f", activity.Amount))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 119, Col: 55}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 119, Col: 55}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td><td>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2f %s", activity.Price, activity.Currency))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 120, Col: 76}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 120, Col: 76}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td><td>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -245,233 +249,233 @@ func PortfolioDetailContent(portfolio model.Portfolio) templ.Component {
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(getConvertedPrice(activity, portfolio.BaseCurrency))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 123, Col: 68}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 123, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<span class=\"text-muted\">-</span>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<span class=\"text-muted\">-</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td><td>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2f %s", activity.Amount*activity.Price, activity.Currency))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 128, Col: 94}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 128, Col: 94}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td><td>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if activity.Currency != portfolio.BaseCurrency {
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(getConvertedTotal(activity, portfolio.BaseCurrency))
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(getConvertedTotalWithFallbackInfo(activity, portfolio.BaseCurrency))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 131, Col: 68}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 131, Col: 84}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<span class=\"text-muted\">-</span>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<span class=\"text-muted\">-</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</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=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</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=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", activity.ID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 142, Col: 65}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 142, Col: 65}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" data-activity-type=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" data-activity-type=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(string(activity.Type))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 143, Col: 58}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 143, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" data-activity-stock=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" data-activity-stock=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(activity.Stock)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 144, Col: 52}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 144, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" data-activity-amount=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" data-activity-amount=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.3f", activity.Amount))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 145, Col: 75}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 145, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" data-activity-price=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\" data-activity-price=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2f", activity.Price))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 146, Col: 73}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 146, Col: 73}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" data-activity-date=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\" data-activity-date=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(activity.Date.Format("2006-01-02"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 147, Col: 71}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 147, Col: 71}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" data-activity-note=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\" data-activity-note=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(activity.Note)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 148, Col: 50}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 148, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">Bearbeiten</button> <button class=\"btn btn-sm btn-outline-danger\" data-bs-toggle=\"modal\" data-bs-target=\"#deleteTransactionModal\" data-activity-id=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\">Bearbeiten</button> <button class=\"btn btn-sm btn-outline-danger\" data-bs-toggle=\"modal\" data-bs-target=\"#deleteTransactionModal\" data-activity-id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", activity.ID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 156, Col: 65}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 156, Col: 65}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" data-activity-stock=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" data-activity-stock=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(activity.Stock)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 157, Col: 52}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 157, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" data-activity-type=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\" data-activity-type=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(string(activity.Type))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 158, Col: 58}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 158, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" data-activity-amount=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\" data-activity-amount=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.3f", activity.Amount))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 159, Col: 75}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 159, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" data-activity-date=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\" data-activity-date=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(activity.Date.Format("02.01.2006"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 160, Col: 71}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 160, Col: 71}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">Löschen</button></div></td></tr>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\">Löschen</button></div></td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
}
} else {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<tr><td colspan=\"9\" class=\"text-center text-muted py-3\">Keine Transaktionen vorhanden</td></tr>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<tr><td colspan=\"9\" class=\"text-center text-muted py-3\">Keine Transaktionen vorhanden</td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</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 ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</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 ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(portfolio.BaseCurrency)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 199, Col: 91}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 199, Col: 114}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" 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\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " 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\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -479,76 +483,76 @@ func PortfolioDetailContent(portfolio model.Portfolio) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</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=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</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=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", portfolio.ID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 235, Col: 85}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 235, Col: 85}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><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\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\"><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\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(portfolio.BaseCurrency)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 258, Col: 63}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 258, Col: 63}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</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=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</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=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", portfolio.ID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 288, Col: 85}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 288, Col: 85}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><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\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\"><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\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var33 string
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(portfolio.BaseCurrency)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 311, Col: 63}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 311, Col: 63}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</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=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</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=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var34 string
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", portfolio.ID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 349, Col: 86}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 349, Col: 86}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <button type=\"submit\" class=\"btn btn-danger\">Löschen</button></form></div></div></div></div><script>\n\t\t// Set today's date as default\n\t\tdocument.addEventListener('DOMContentLoaded', function() {\n\t\t\tconst today = new Date().toISOString().split('T')[0];\n\t\t\tdocument.getElementById('transaction-date').value = today;\n\n\t\t\t// Handle edit button clicks\n\t\t\tdocument.addEventListener('click', function(e) {\n\t\t\t\tif (e.target.closest('[data-bs-target=\"#editTransactionModal\"]')) {\n\t\t\t\t\tconst button = e.target.closest('[data-bs-target=\"#editTransactionModal\"]');\n\n\t\t\t\t\t// Populate edit modal with transaction data\n\t\t\t\t\tdocument.getElementById('edit-activity-id').value = button.dataset.activityId;\n\t\t\t\t\tdocument.getElementById('edit-transaction-type').value = button.dataset.activityType;\n\t\t\t\t\tdocument.getElementById('edit-transaction-stock').value = button.dataset.activityStock;\n\t\t\t\t\tdocument.getElementById('edit-transaction-amount').value = button.dataset.activityAmount;\n\t\t\t\t\tdocument.getElementById('edit-transaction-price').value = button.dataset.activityPrice;\n\t\t\t\t\tdocument.getElementById('edit-transaction-date').value = button.dataset.activityDate;\n\t\t\t\t\tdocument.getElementById('edit-transaction-note').value = button.dataset.activityNote;\n\t\t\t\t}\n\n\t\t\t\t// Handle delete button clicks\n\t\t\t\tif (e.target.closest('[data-bs-target=\"#deleteTransactionModal\"]')) {\n\t\t\t\t\tconst button = e.target.closest('[data-bs-target=\"#deleteTransactionModal\"]');\n\n\t\t\t\t\t// Populate delete modal with transaction data\n\t\t\t\t\tdocument.getElementById('delete-activity-id').value = button.dataset.activityId;\n\n\t\t\t\t\t// Show transaction details in delete modal\n\t\t\t\t\tconst details = document.getElementById('delete-transaction-details');\n\t\t\t\t\tdetails.innerHTML = `\n\t\t\t\t\t\t<small>\n\t\t\t\t\t\t\t<strong>${button.dataset.activityType}</strong> - ${button.dataset.activityStock}<br>\n\t\t\t\t\t\t\t${button.dataset.activityAmount} Stück am ${button.dataset.activityDate}\n\t\t\t\t\t\t</small>\n\t\t\t\t\t`;\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t</script>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\"> <button type=\"submit\" class=\"btn btn-danger\">Löschen</button></form></div></div></div></div><script>\n\t\t// Set today's date as default\n\t\tdocument.addEventListener('DOMContentLoaded', function() {\n\t\t\tconst today = new Date().toISOString().split('T')[0];\n\t\t\tdocument.getElementById('transaction-date').value = today;\n\n\t\t\t// Handle edit button clicks\n\t\t\tdocument.addEventListener('click', function(e) {\n\t\t\t\tif (e.target.closest('[data-bs-target=\"#editTransactionModal\"]')) {\n\t\t\t\t\tconst button = e.target.closest('[data-bs-target=\"#editTransactionModal\"]');\n\n\t\t\t\t\t// Populate edit modal with transaction data\n\t\t\t\t\tdocument.getElementById('edit-activity-id').value = button.dataset.activityId;\n\t\t\t\t\tdocument.getElementById('edit-transaction-type').value = button.dataset.activityType;\n\t\t\t\t\tdocument.getElementById('edit-transaction-stock').value = button.dataset.activityStock;\n\t\t\t\t\tdocument.getElementById('edit-transaction-amount').value = button.dataset.activityAmount;\n\t\t\t\t\tdocument.getElementById('edit-transaction-price').value = button.dataset.activityPrice;\n\t\t\t\t\tdocument.getElementById('edit-transaction-date').value = button.dataset.activityDate;\n\t\t\t\t\tdocument.getElementById('edit-transaction-note').value = button.dataset.activityNote;\n\t\t\t\t}\n\n\t\t\t\t// Handle delete button clicks\n\t\t\t\tif (e.target.closest('[data-bs-target=\"#deleteTransactionModal\"]')) {\n\t\t\t\t\tconst button = e.target.closest('[data-bs-target=\"#deleteTransactionModal\"]');\n\n\t\t\t\t\t// Populate delete modal with transaction data\n\t\t\t\t\tdocument.getElementById('delete-activity-id').value = button.dataset.activityId;\n\n\t\t\t\t\t// Show transaction details in delete modal\n\t\t\t\t\tconst details = document.getElementById('delete-transaction-details');\n\t\t\t\t\tdetails.innerHTML = `\n\t\t\t\t\t\t<small>\n\t\t\t\t\t\t\t<strong>${button.dataset.activityType}</strong> - ${button.dataset.activityStock}<br>\n\t\t\t\t\t\t\t${button.dataset.activityAmount} Stück am ${button.dataset.activityDate}\n\t\t\t\t\t\t</small>\n\t\t\t\t\t`;\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t</script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
return nil
})
}
@@ -574,50 +578,50 @@ func PortfolioSummary(activities []model.Activity, currency string) templ.Compon
templ_7745c5c3_Var35 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"row\"><div class=\"col-12\"><div class=\"mb-3\"><div class=\"text-muted\">Anzahl Transaktionen</div><div class=\"h3 mb-0\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<div class=\"row\"><div class=\"col-12\"><div class=\"mb-3\"><div class=\"text-muted\">Anzahl Transaktionen</div><div class=\"h3 mb-0\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var36 string
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(activities)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 404, Col: 61}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 404, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div><div class=\"mb-3\"><div class=\"text-muted\">Gesamtwert investiert</div><div class=\"h2 mb-0\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</div></div><div class=\"mb-3\"><div class=\"text-muted\">Gesamtwert investiert</div><div class=\"h2 mb-0\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var37 string
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(formatTotalInvestedWithConversion(activities, currency))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 409, Col: 62}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 409, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div><div class=\"mb-3\"><div class=\"text-muted\">Letzter Kauf</div><div class=\"h4 mb-0 text-muted\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</div><div class=\"text-muted small\">* = Aktueller Kurs verwendet (historischer Kurs nicht verfügbar)</div></div><div class=\"mb-3\"><div class=\"text-muted\">Letzter Kauf</div><div class=\"h4 mb-0 text-muted\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var38 string
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(getLastBuyDate(activities))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 415, Col: 33}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 418, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div></div></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</div></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
return nil
})
}
@@ -646,7 +650,7 @@ func PortfolioDetail(authenticated bool, username string, portfolio model.Portfo
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
return nil
})
}