diff --git a/app b/app new file mode 100755 index 0000000..dde8542 Binary files /dev/null and b/app differ diff --git a/data/portfolio.db b/data/portfolio.db index 9733d37..2d807a4 100644 Binary files a/data/portfolio.db and b/data/portfolio.db differ diff --git a/internal/handler/api.go b/internal/handler/api.go index 310472d..6cc9d1b 100644 --- a/internal/handler/api.go +++ b/internal/handler/api.go @@ -83,7 +83,7 @@ func getStockCurrency(stock string) (string, error) { } // Parse the JSON response - var resp model.YahooChartResponse + var resp util.YahooChartResponse err = json.Unmarshal([]byte(data), &resp) if err != nil { return "", err diff --git a/internal/handler/portfolio.go b/internal/handler/portfolio.go index ff79e42..b1cf4db 100644 --- a/internal/handler/portfolio.go +++ b/internal/handler/portfolio.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "portfolio-tracker/internal/model" + "portfolio-tracker/internal/service" "portfolio-tracker/internal/web/templates" "strconv" "strings" @@ -53,10 +54,30 @@ func PortfolioDetailHandler(w http.ResponseWriter, r *http.Request) { return } + // Calculate positions + fmt.Printf("DEBUG: Starting position calculation for portfolio %d with %d activities\n", portfolio.ID, len(portfolio.Activities)) + positionCalculator := service.NewPositionCalculator() + positions, positionSummary, err := positionCalculator.CalculatePositions(portfolio) + if err != nil { + fmt.Printf("Error calculating positions: %v\n", err) + // Continue with empty positions rather than failing + positions = []model.Position{} + positionSummary = model.PositionSummary{ + Currency: portfolio.BaseCurrency, + LastUpdated: time.Now(), + } + } + + fmt.Printf("DEBUG: Position calculation completed. Found %d positions\n", len(positions)) + for i, pos := range positions { + fmt.Printf("DEBUG: Position %d: %s - Shares: %.4f, Open: %v\n", i+1, pos.Stock, pos.Shares, pos.IsOpen()) + } + fmt.Printf("DEBUG: Position summary - Total Value: %.2f, Total Cost: %.2f\n", positionSummary.TotalValue, positionSummary.TotalCostBasis) + // Get all user portfolios for navigation portfolios := getUserPortfolios(username) - component := templates.PortfolioDetail(auth, username, portfolio, portfolios) + component := templates.PortfolioDetail(auth, username, portfolio, portfolios, positions, positionSummary) templ.Handler(component).ServeHTTP(w, r) } @@ -196,6 +217,14 @@ func PortfolioTransactionHandler(w http.ResponseWriter, r *http.Request) { fmt.Printf("Stock %s currency: %s\n", stock, stockCurrency) + // Handle GBp (pence) to GBP (pounds) conversion + // If the stock is quoted in pence, convert price to pounds + if stockCurrency == "GBp" { + price = price / 100.0 + stockCurrency = "GBP" + fmt.Printf("Converted GBp price %.2f to GBP price %.2f\n", price*100, price) + } + // Create activity activity := model.Activity{ PortfolioID: uint(portfolioID), @@ -317,6 +346,14 @@ func EditTransactionHandler(w http.ResponseWriter, r *http.Request) { stockCurrency = portfolio.BaseCurrency } + // Handle GBp (pence) to GBP (pounds) conversion + // If the stock is quoted in pence, convert price to pounds + if stockCurrency == "GBp" { + price = price / 100.0 + stockCurrency = "GBP" + fmt.Printf("Converted GBp price %.2f to GBP price %.2f\n", price*100, price) + } + // Update activity fields activity.Type = model.ActivityType(transactionType) activity.Stock = stock diff --git a/internal/model/position.go b/internal/model/position.go new file mode 100644 index 0000000..425a7e3 --- /dev/null +++ b/internal/model/position.go @@ -0,0 +1,117 @@ +package model + +import ( + "fmt" + "time" +) + +// Position represents a calculated position for a specific stock in a portfolio +type Position struct { + Stock string `json:"stock"` // Stock symbol/ISIN + Shares float64 `json:"shares"` // Total shares held (can be negative if oversold) + AverageCostPrice float64 `json:"average_cost_price"` // Average cost per share in portfolio base currency + TotalCostBasis float64 `json:"total_cost_basis"` // Total amount invested in portfolio base currency + CurrentPrice float64 `json:"current_price"` // Current market price per share in portfolio base currency + CurrentValue float64 `json:"current_value"` // Current total value (shares * current_price) + UnrealizedPL float64 `json:"unrealized_pl"` // Unrealized profit/loss (current_value - total_cost_basis) + UnrealizedPLPct float64 `json:"unrealized_pl_pct"` // Unrealized profit/loss percentage + Currency string `json:"currency"` // Stock's native currency + BaseCurrency string `json:"base_currency"` // Portfolio's base currency + LastUpdated time.Time `json:"last_updated"` // When position was last calculated + + // Dividend information + TotalDividends float64 `json:"total_dividends"` // Total dividends received in base currency + + // Transaction summary + TotalBought float64 `json:"total_bought"` // Total shares bought + TotalSold float64 `json:"total_sold"` // Total shares sold + RealizedPL float64 `json:"realized_pl"` // Realized profit/loss from sales +} + +// PositionSummary represents aggregated portfolio position data +type PositionSummary struct { + TotalValue float64 `json:"total_value"` // Total portfolio value + TotalCostBasis float64 `json:"total_cost_basis"` // Total amount invested + TotalUnrealizedPL float64 `json:"total_unrealized_pl"` // Total unrealized profit/loss + TotalRealizedPL float64 `json:"total_realized_pl"` // Total realized profit/loss + TotalDividends float64 `json:"total_dividends"` // Total dividends received + TotalReturn float64 `json:"total_return"` // Total return (realized + unrealized + dividends) + TotalReturnPct float64 `json:"total_return_pct"` // Total return percentage + Currency string `json:"currency"` // Portfolio base currency + LastUpdated time.Time `json:"last_updated"` // When summary was calculated +} + +// IsOpen returns true if the position has shares (positive or negative) +func (p *Position) IsOpen() bool { + return p.Shares != 0 +} + +// IsLong returns true if the position has positive shares +func (p *Position) IsLong() bool { + return p.Shares > 0 +} + +// IsShort returns true if the position has negative shares +func (p *Position) IsShort() bool { + return p.Shares < 0 +} + +// GetDisplayValue returns the absolute value for display purposes +func (p *Position) GetDisplayValue() float64 { + if p.CurrentValue < 0 { + return -p.CurrentValue + } + return p.CurrentValue +} + +// GetUnrealizedPLColor returns CSS class for profit/loss color +func (p *Position) GetUnrealizedPLColor() string { + if p.UnrealizedPL > 0 { + return "text-green" + } else if p.UnrealizedPL < 0 { + return "text-red" + } + return "text-muted" +} + +// GetUnrealizedPLIcon returns icon for profit/loss +func (p *Position) GetUnrealizedPLIcon() string { + if p.UnrealizedPL > 0 { + return "trending-up" + } else if p.UnrealizedPL < 0 { + return "trending-down" + } + return "minus" +} + +// FormatCurrency formats a value with currency symbol +func (p *Position) FormatCurrency(value float64) string { + switch p.BaseCurrency { + case "EUR": + return fmt.Sprintf("€%.2f", value) + case "USD": + return fmt.Sprintf("$%.2f", value) + case "GBP": + return fmt.Sprintf("£%.2f", value) + case "CHF": + return fmt.Sprintf("%.2f CHF", value) + default: + return fmt.Sprintf("%.2f %s", value, p.BaseCurrency) + } +} + +// FormatShares formats share count with appropriate decimal places +func (p *Position) FormatShares() string { + if p.Shares == float64(int(p.Shares)) { + return fmt.Sprintf("%.0f", p.Shares) + } + return fmt.Sprintf("%.4f", p.Shares) +} + +// FormatPercentage formats percentage with sign and color +func (p *Position) FormatPercentage() string { + if p.UnrealizedPLPct > 0 { + return fmt.Sprintf("+%.2f%%", p.UnrealizedPLPct) + } + return fmt.Sprintf("%.2f%%", p.UnrealizedPLPct) +} diff --git a/internal/service/position_calculator.go b/internal/service/position_calculator.go new file mode 100644 index 0000000..eaeee92 --- /dev/null +++ b/internal/service/position_calculator.go @@ -0,0 +1,298 @@ +package service + +import ( + "encoding/json" + "fmt" + "sort" + "time" + + "portfolio-tracker/internal/model" + "portfolio-tracker/internal/util" +) + +// PositionCalculator calculates positions from activities +type PositionCalculator struct{} + +// NewPositionCalculator creates a new position calculator +func NewPositionCalculator() *PositionCalculator { + return &PositionCalculator{} +} + +// CalculatePositions calculates all positions for a portfolio +func (pc *PositionCalculator) CalculatePositions(portfolio model.Portfolio) ([]model.Position, model.PositionSummary, error) { + // Group activities by stock + stockActivities := pc.groupActivitiesByStock(portfolio.Activities) + + var positions []model.Position + var totalValue, totalCostBasis, totalUnrealizedPL, totalRealizedPL, totalDividends float64 + + for stock, activities := range stockActivities { + position, err := pc.calculateStockPosition(stock, activities, portfolio.BaseCurrency) + if err != nil { + fmt.Printf("Error calculating position for %s: %v\n", stock, err) + continue + } + + // Only include positions that have shares or have had activity + if position.IsOpen() || position.TotalBought > 0 || position.TotalSold > 0 { + positions = append(positions, position) + + // Add to portfolio totals + totalValue += position.CurrentValue + totalCostBasis += position.TotalCostBasis + totalUnrealizedPL += position.UnrealizedPL + totalRealizedPL += position.RealizedPL + totalDividends += position.TotalDividends + } + } + + // Calculate total return + totalReturn := totalUnrealizedPL + totalRealizedPL + totalDividends + totalReturnPct := 0.0 + if totalCostBasis > 0 { + totalReturnPct = (totalReturn / totalCostBasis) * 100 + } + + summary := model.PositionSummary{ + TotalValue: totalValue, + TotalCostBasis: totalCostBasis, + TotalUnrealizedPL: totalUnrealizedPL, + TotalRealizedPL: totalRealizedPL, + TotalDividends: totalDividends, + TotalReturn: totalReturn, + TotalReturnPct: totalReturnPct, + Currency: portfolio.BaseCurrency, + LastUpdated: time.Now(), + } + + // Sort positions by current value (descending) + sort.Slice(positions, func(i, j int) bool { + return positions[i].CurrentValue > positions[j].CurrentValue + }) + + return positions, summary, nil +} + +// groupActivitiesByStock groups activities by stock symbol +func (pc *PositionCalculator) groupActivitiesByStock(activities []model.Activity) map[string][]model.Activity { + stockActivities := make(map[string][]model.Activity) + + for _, activity := range activities { + stockActivities[activity.Stock] = append(stockActivities[activity.Stock], activity) + } + + // Sort activities by date within each stock + for stock := range stockActivities { + sort.Slice(stockActivities[stock], func(i, j int) bool { + return stockActivities[stock][i].Date.Before(stockActivities[stock][j].Date) + }) + } + + return stockActivities +} + +// calculateStockPosition calculates position for a single stock +func (pc *PositionCalculator) calculateStockPosition(stock string, activities []model.Activity, baseCurrency string) (model.Position, error) { + position := model.Position{ + Stock: stock, + BaseCurrency: baseCurrency, + LastUpdated: time.Now(), + } + + var totalShares float64 + var totalCostBasis float64 + var totalDividends float64 + var totalBought float64 + var totalSold float64 + var realizedPL float64 + + // Process each activity + for _, activity := range activities { + // Handle GBp (pence) to GBP (pounds) conversion for historical transactions + activityPrice := activity.Price + activityCurrency := activity.Currency + if activity.Currency == "GBp" { + activityPrice = activity.Price / 100.0 + activityCurrency = "GBP" + } + + convertedAmount, err := pc.convertToBaseCurrency(activity.Amount, activityCurrency, baseCurrency, activity.Date) + if err != nil { + fmt.Printf("Warning: Could not convert currency for %s: %v\n", stock, err) + convertedAmount = activity.Amount // Use original amount as fallback + } + + convertedPrice, err := pc.convertToBaseCurrency(activityPrice, activityCurrency, baseCurrency, activity.Date) + if err != nil { + fmt.Printf("Warning: Could not convert price for %s: %v\n", stock, err) + convertedPrice = activity.Price // Use original price as fallback + } + + convertedFee, err := pc.convertToBaseCurrency(activity.Fee, activityCurrency, baseCurrency, activity.Date) + if err != nil { + convertedFee = activity.Fee // Use original fee as fallback + } + + convertedTaxes, err := pc.convertToBaseCurrency(activity.Taxes, activityCurrency, baseCurrency, activity.Date) + if err != nil { + convertedTaxes = activity.Taxes // Use original taxes as fallback + } + + switch activity.Type { + case model.Buy: + totalShares += activity.Amount + totalBought += activity.Amount + totalCostBasis += (convertedPrice * activity.Amount) + convertedFee + convertedTaxes + position.Currency = activityCurrency + + case model.Sell: + if totalShares > 0 { + // Calculate realized P&L using average cost method + avgCostPerShare := totalCostBasis / totalShares + soldValue := (convertedPrice * activity.Amount) - convertedFee - convertedTaxes + soldCostBasis := avgCostPerShare * activity.Amount + realizedPL += soldValue - soldCostBasis + + // Update totals + totalShares -= activity.Amount + totalSold += activity.Amount + totalCostBasis -= soldCostBasis + } + + case model.Dividend: + totalDividends += convertedAmount + + case model.Fee: + // Fees reduce the total return + totalDividends -= convertedAmount + + case model.Tax: + // Taxes reduce the total return + totalDividends -= convertedAmount + } + } + + // Calculate average cost price + averageCostPrice := 0.0 + if totalShares > 0 { + averageCostPrice = totalCostBasis / totalShares + } + + // Get current market price + currentPrice, err := pc.getCurrentPrice(stock, baseCurrency) + if err != nil { + fmt.Printf("Warning: Could not get current price for %s: %v\n", stock, err) + currentPrice = averageCostPrice // Use average cost as fallback + } + + // Calculate current value and unrealized P&L + currentValue := totalShares * currentPrice + unrealizedPL := currentValue - totalCostBasis + unrealizedPLPct := 0.0 + if totalCostBasis > 0 { + unrealizedPLPct = (unrealizedPL / totalCostBasis) * 100 + } + + position.Shares = totalShares + position.AverageCostPrice = averageCostPrice + position.TotalCostBasis = totalCostBasis + position.CurrentPrice = currentPrice + position.CurrentValue = currentValue + position.UnrealizedPL = unrealizedPL + position.UnrealizedPLPct = unrealizedPLPct + position.TotalDividends = totalDividends + position.TotalBought = totalBought + position.TotalSold = totalSold + position.RealizedPL = realizedPL + + return position, nil +} + +// convertToBaseCurrency converts amount to base currency +func (pc *PositionCalculator) convertToBaseCurrency(amount float64, fromCurrency, toCurrency string, date time.Time) (float64, error) { + if fromCurrency == toCurrency { + return amount, nil + } + + return util.ConvertCurrencyHistorical(amount, fromCurrency, toCurrency, date) +} + +// getCurrentPrice gets current market price for a stock +func (pc *PositionCalculator) getCurrentPrice(stock, baseCurrency string) (float64, error) { + // Try to get current price from Yahoo Finance + data, err := util.FetchYahooFinanceData(stock) + if err != nil { + return 0, fmt.Errorf("failed to fetch stock data: %v", err) + } + + // Parse the JSON response + var resp util.YahooChartResponse + err = json.Unmarshal([]byte(data), &resp) + if err != nil { + return 0, fmt.Errorf("failed to parse stock data: %v", err) + } + + if len(resp.Chart.Result) == 0 { + return 0, fmt.Errorf("no data found for stock %s", stock) + } + + result := resp.Chart.Result[0] + + // Get the latest price + var latestPrice float64 + if len(result.Indicators.Quote) > 0 && len(result.Indicators.Quote[0].Close) > 0 { + prices := result.Indicators.Quote[0].Close + // Find the last non-null price + for i := len(prices) - 1; i >= 0; i-- { + if prices[i] != 0 { + latestPrice = prices[i] + break + } + } + } + + if latestPrice == 0 { + return 0, fmt.Errorf("no valid price found for stock %s", stock) + } + + // Convert to base currency if needed + stockCurrency := "USD" // Default to USD if not specified + if result.Meta.Currency != "" { + stockCurrency = result.Meta.Currency + } + + // Handle GBp (pence) to GBP (pounds) conversion + if stockCurrency == "GBp" { + latestPrice = latestPrice / 100.0 + stockCurrency = "GBP" + } + + if stockCurrency != baseCurrency { + convertedPrice, err := util.ConvertCurrency(latestPrice, stockCurrency, baseCurrency) + if err != nil { + fmt.Printf("Warning: Could not convert price from %s to %s: %v\n", stockCurrency, baseCurrency, err) + return latestPrice, nil // Return original price if conversion fails + } + return convertedPrice, nil + } + + return latestPrice, nil +} + +// CalculatePositionForStock calculates position for a specific stock +func (pc *PositionCalculator) CalculatePositionForStock(stock string, activities []model.Activity, baseCurrency string) (model.Position, error) { + // Filter activities for this stock + var stockActivities []model.Activity + for _, activity := range activities { + if activity.Stock == stock { + stockActivities = append(stockActivities, activity) + } + } + + // Sort by date + sort.Slice(stockActivities, func(i, j int) bool { + return stockActivities[i].Date.Before(stockActivities[j].Date) + }) + + return pc.calculateStockPosition(stock, stockActivities, baseCurrency) +} diff --git a/internal/util/yahoo-utils.go b/internal/util/yahoo-utils.go index ac370df..52494b5 100644 --- a/internal/util/yahoo-utils.go +++ b/internal/util/yahoo-utils.go @@ -9,127 +9,172 @@ import ( ) type YahooChartResponse struct { - Chart struct { - Result []struct { - Timestamp []int64 `json:"timestamp"` - Indicators struct { - Quote []struct { - Close []float64 `json:"close"` - } `json:"quote"` - } `json:"indicators"` - } `json:"result"` - } `json:"chart"` + Chart struct { + Result []struct { + Meta struct { + Symbol string `json:"symbol"` + RegularMarketPrice float64 `json:"regularMarketPrice"` + Exchange string `json:"exchangeName"` + Currency string `json:"currency"` + Instrument string `json:"instrumentType"` + FirstTrade int64 `json:"firstTradeDate"` + GMTOffset int64 `json:"gmtoffset"` + Timezone string `json:"exchangeTimezoneName"` + ShortName string `json:"shortName"` + LongName string `json:"longName"` + Range string `json:"range"` + DataGranularity string `json:"dataGranularity"` + PriceHint int `json:"priceHint"` + PreviousClose float64 `json:"previousClose"` + Scale int `json:"scale"` + HasPrePostMarket bool `json:"hasPrePostMarketData"` + TradingPeriods [][]struct { + Timezone string `json:"timezone"` + Start int64 `json:"start"` + End int64 `json:"end"` + GMTOffset int64 `json:"gmtoffset"` + } `json:"tradingPeriods"` + ValidRanges []string `json:"validRanges"` + } `json:"meta"` + Timestamp []int64 `json:"timestamp"` + Indicators struct { + Quote []struct { + Open []float64 `json:"open"` + High []float64 `json:"high"` + Low []float64 `json:"low"` + Close []float64 `json:"close"` + Volume []int64 `json:"volume"` + } `json:"quote"` + Adjclose []struct { + Adjclose []float64 `json:"adjclose"` + } `json:"adjclose,omitempty"` + } `json:"indicators"` + Events struct { + Dividends map[string]struct { + Amount float64 `json:"amount"` + Date int64 `json:"date"` + } `json:"dividends,omitempty"` + Splits map[string]struct { + Date int64 `json:"date"` + Numerator float64 `json:"numerator"` + Denominator float64 `json:"denominator"` + SplitRatio string `json:"splitRatio"` + } `json:"splits,omitempty"` + } `json:"events,omitempty"` + } `json:"result"` + Error interface{} `json:"error"` + } `json:"chart"` } func StockSearch(query string) (string, error) { - url := fmt.Sprintf("https://query2.finance.yahoo.com/v1/finance/search?q=%s", query) + url := fmt.Sprintf("https://query2.finance.yahoo.com/v1/finance/search?q=%s", query) - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return "", fmt.Errorf("Fehler beim Erstellen der Anfrage: %v", err) - } + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return "", fmt.Errorf("Fehler beim Erstellen der Anfrage: %v", err) + } - req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15") + req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15") - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return "", fmt.Errorf("Fehler beim Abrufen der URL: %v", err) - } - defer resp.Body.Close() + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("Fehler beim Abrufen der URL: %v", err) + } + defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("Fehler: HTTP Status %s", resp.Status) - } + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("Fehler: HTTP Status %s", resp.Status) + } - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("Fehler beim Lesen der Antwort: %v", err) - } + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("Fehler beim Lesen der Antwort: %v", err) + } - return string(body), nil + return string(body), nil } func FetchYahooFinanceData(stock string) (string, error) { - url := fmt.Sprintf( - "https://query2.finance.yahoo.com/v8/finance/chart/%s?range=1y&interval=1d&events=history", - stock, - ) + url := fmt.Sprintf( + "https://query2.finance.yahoo.com/v8/finance/chart/%s?range=1y&interval=1d&events=history", + stock, + ) - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return "", fmt.Errorf("Fehler beim Erstellen der Anfrage: %v", err) - } + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return "", fmt.Errorf("Fehler beim Erstellen der Anfrage: %v", err) + } - req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15") + req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15") - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return "", fmt.Errorf("Fehler beim Abrufen der URL: %v", err) - } - defer resp.Body.Close() + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("Fehler beim Abrufen der URL: %v", err) + } + defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("Fehler: HTTP Status %s", resp.Status) - } + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("Fehler: HTTP Status %s", resp.Status) + } - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("Fehler beim Lesen der Antwort: %v", err) - } + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("Fehler beim Lesen der Antwort: %v", err) + } - return string(body), nil + return string(body), nil } func FetchYahooFinanceDataMax(stock string) (string, error) { - url := fmt.Sprintf( - "https://query2.finance.yahoo.com/v8/finance/chart/%s?range=max&interval=1mo&events=div", - stock, - ) + url := fmt.Sprintf( + "https://query2.finance.yahoo.com/v8/finance/chart/%s?range=max&interval=1mo&events=div", + stock, + ) - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return "", fmt.Errorf("Fehler beim Erstellen der Anfrage: %v", err) - } + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return "", fmt.Errorf("Fehler beim Erstellen der Anfrage: %v", err) + } - req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15") + req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15") - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return "", fmt.Errorf("Fehler beim Abrufen der URL: %v", err) - } - defer resp.Body.Close() + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("Fehler beim Abrufen der URL: %v", err) + } + defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("Fehler: HTTP Status %s", resp.Status) - } + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("Fehler: HTTP Status %s", resp.Status) + } - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("Fehler beim Lesen der Antwort: %v", err) - } + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("Fehler beim Lesen der Antwort: %v", err) + } - return string(body), nil + return string(body), nil } // Neu: Daten für das Chart extrahieren func ExtractChartData(jsonStr string) ([]string, []float64, error) { - var data YahooChartResponse - err := json.Unmarshal([]byte(jsonStr), &data) - if err != nil { - return nil, nil, fmt.Errorf("Fehler beim Parsen des JSON: %v", err) - } - if len(data.Chart.Result) == 0 { - return nil, nil, fmt.Errorf("Keine Daten gefunden") - } - timestamps := data.Chart.Result[0].Timestamp - closes := data.Chart.Result[0].Indicators.Quote[0].Close + var data YahooChartResponse + err := json.Unmarshal([]byte(jsonStr), &data) + if err != nil { + return nil, nil, fmt.Errorf("Fehler beim Parsen des JSON: %v", err) + } + if len(data.Chart.Result) == 0 { + return nil, nil, fmt.Errorf("Keine Daten gefunden") + } + timestamps := data.Chart.Result[0].Timestamp + closes := data.Chart.Result[0].Indicators.Quote[0].Close - labels := make([]string, len(timestamps)) - for i, ts := range timestamps { - labels[i] = time.Unix(ts, 0).Format("2006-01-02") - } - return labels, closes, nil -} \ No newline at end of file + labels := make([]string, len(timestamps)) + for i, ts := range timestamps { + labels[i] = time.Unix(ts, 0).Format("2006-01-02") + } + return labels, closes, nil +} diff --git a/internal/web/templates/portfolio-detail.templ b/internal/web/templates/portfolio-detail.templ index 7c06e3c..39f45da 100644 --- a/internal/web/templates/portfolio-detail.templ +++ b/internal/web/templates/portfolio-detail.templ @@ -6,7 +6,7 @@ import ( "portfolio-tracker/internal/web/templates/components" ) -templ PortfolioDetailContent(portfolio model.Portfolio) { +templ PortfolioDetailContent(portfolio model.Portfolio, positions []model.Position, positionSummary model.PositionSummary) {
@@ -396,32 +424,85 @@ templ PortfolioDetailContent(portfolio model.Portfolio) { } // Separate component for portfolio summary -templ PortfolioSummary(activities []model.Activity, currency string) { +templ PortfolioSummary(summary model.PositionSummary, transactionCount int) {
Anzahl Transaktionen
-
{ fmt.Sprintf("%d", len(activities)) }
+
{ fmt.Sprintf("%d", transactionCount) }
+
+
+
Aktueller Portfoliowert
+
+ { formatCurrency(summary.TotalValue, summary.Currency) } +
Gesamtwert investiert
-
- { formatTotalInvestedWithConversion(activities, currency) } -
-
- * = Aktueller Kurs verwendet (historischer Kurs nicht verfügbar) +
+ { formatCurrency(summary.TotalCostBasis, summary.Currency) }
-
Letzter Kauf
-
- { getLastBuyDate(activities) } +
Unrealisierte Gewinne/Verluste
+
+ { formatCurrency(summary.TotalUnrealizedPL, summary.Currency) } + if summary.TotalCostBasis > 0 { + ({ formatPercentage(summary.TotalUnrealizedPL / summary.TotalCostBasis * 100) }) + } +
+
+
+
Dividenden erhalten
+
+ { formatCurrency(summary.TotalDividends, summary.Currency) } +
+
+
+
Gesamtrendite
+
+ { formatCurrency(summary.TotalReturn, summary.Currency) } + if summary.TotalCostBasis > 0 { + ({ formatPercentage(summary.TotalReturnPct) }) + }
} -templ PortfolioDetail(authenticated bool, username string, portfolio model.Portfolio, portfolios []model.Portfolio) { - @components.PageLayout(authenticated, username, portfolio.Name, PortfolioDetailContent(portfolio), portfolios) +// Helper functions for formatting +func formatCurrency(value float64, currency string) string { + switch currency { + case "EUR": + return fmt.Sprintf("€%.2f", value) + case "USD": + return fmt.Sprintf("$%.2f", value) + case "GBP": + return fmt.Sprintf("£%.2f", value) + case "CHF": + return fmt.Sprintf("%.2f CHF", value) + default: + return fmt.Sprintf("%.2f %s", value, currency) + } +} + +func formatPercentage(value float64) string { + if value > 0 { + return fmt.Sprintf("+%.2f%%", value) + } + return fmt.Sprintf("%.2f%%", value) +} + +func getColorClass(value float64) string { + if value > 0 { + return "text-success" + } else if value < 0 { + return "text-danger" + } + return "text-muted" +} + +templ PortfolioDetail(authenticated bool, username string, portfolio model.Portfolio, portfolios []model.Portfolio, positions []model.Position, positionSummary model.PositionSummary) { + @components.PageLayout(authenticated, username, portfolio.Name, PortfolioDetailContent(portfolio, positions, positionSummary), portfolios) } diff --git a/internal/web/templates/portfolio-detail_templ.go b/internal/web/templates/portfolio-detail_templ.go index 0226b73..19a2dc2 100644 --- a/internal/web/templates/portfolio-detail_templ.go +++ b/internal/web/templates/portfolio-detail_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.906 +// templ: version: v0.2.778 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -14,7 +14,7 @@ import ( "portfolio-tracker/internal/web/templates/components" ) -func PortfolioDetailContent(portfolio model.Portfolio) templ.Component { +func PortfolioDetailContent(portfolio model.Portfolio, positions []model.Position, positionSummary model.PositionSummary) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -35,33 +35,33 @@ func PortfolioDetailContent(portfolio model.Portfolio) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") 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: `portfolio-detail.templ`, Line: 14, Col: 44} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/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 = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " (") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" (") 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: `portfolio-detail.templ`, Line: 14, Col: 72} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/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 = templruntime.WriteString(templ_7745c5c3_Buffer, 3, ")

") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(")

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -69,495 +69,652 @@ 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: `portfolio-detail.templ`, Line: 17, Col: 30} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/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 = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "Erstellt am ") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("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: `portfolio-detail.templ`, Line: 19, Col: 61} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/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 = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

Positionen

") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Positionen

WertpapierAnzahlØ EinkaufspreisAktueller Wert+/- Gesamt
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if len(portfolio.Activities) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err + if len(positions) > 0 { + for _, position := range positions { + if position.IsOpen() { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
WertpapierAnzahlØ EinkaufspreisAktueller Wert+/- Gesamt
Position calculation will be implemented here
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(position.Stock) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 63, Col: 51} + } + _, 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("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(position.Currency) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 64, Col: 63} + } + _, 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("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(position.FormatShares()) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 67, Col: 61} + } + _, 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("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(position.FormatCurrency(position.AverageCostPrice)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 70, Col: 72} + } + _, 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("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(position.FormatCurrency(position.CurrentPrice)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 73, Col: 67} + } + _, 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("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(position.FormatCurrency(position.CurrentValue)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 74, Col: 92} + } + _, 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("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 = []any{position.GetUnrealizedPLColor()} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(position.FormatCurrency(position.UnrealizedPL)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 78, Col: 71} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var15 = []any{position.GetUnrealizedPLColor() + " small"} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(position.FormatPercentage()) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 81, Col: 44} + } + _, 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("
Details
Keine Positionen vorhanden. Fügen Sie Ihre erste Transaktion hinzu.
Keine Positionen vorhanden. Fügen Sie Ihre erste Transaktion hinzu.

Letzte Transaktionen

DatumTypWertpapierAnzahlPreisPreis (") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Letzte Transaktionen

") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(")") 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 = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "
DatumTypWertpapierAnzahlPreisPreis (") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var6 string - templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(portfolio.BaseCurrency) + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(portfolio.BaseCurrency) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 91, Col: 45} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 119, Col: 45} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, ")GesamtGesamt (") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(")GesamtGesamt (") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var7 string - templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(portfolio.BaseCurrency) + var templ_7745c5c3_Var20 string + templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(portfolio.BaseCurrency) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 93, Col: 46} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 121, Col: 46} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, ")
") - 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: `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 = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if activity.Type == "BUY" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "Kauf") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else if activity.Type == "SELL" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "Verkauf") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else if activity.Type == "DIVIDEND" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "Dividende") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "") - 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: `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 = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") - 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: `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 = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "") - 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: `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 = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") - 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: `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 = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if activity.Currency != portfolio.BaseCurrency { - 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: `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 = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "-") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "") - 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: `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 = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "") - 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(getConvertedTotalWithFallbackInfo(activity, portfolio.BaseCurrency)) - if templ_7745c5c3_Err != nil { - 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 = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "-") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
") 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)) + templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(activity.Date.Format("02.01.2006")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 146, Col: 73} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 130, Col: 53} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\" data-activity-date=\"") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") 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: `portfolio-detail.templ`, Line: 147, Col: 71} + if activity.Type == "BUY" { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Kauf") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if activity.Type == "SELL" { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Verkauf") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if activity.Type == "DIVIDEND" { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Dividende") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var22 string + templ_7745c5c3_Var22, 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: 139, 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("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Bearbeiten ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var25 string - templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(activity.Stock) + templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.3f", activity.Amount)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 157, Col: 52} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 147, Col: 55} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\" data-activity-type=\"") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") 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)) + templ_7745c5c3_Var26, 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: `portfolio-detail.templ`, Line: 158, Col: 58} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 148, Col: 76} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\" data-activity-amount=\"") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") 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: `portfolio-detail.templ`, Line: 159, Col: 75} + if activity.Currency != portfolio.BaseCurrency { + var templ_7745c5c3_Var27 string + templ_7745c5c3_Var27, 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: 151, Col: 68} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("-") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\" data-activity-date=\"") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") 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")) + templ_7745c5c3_Var28, 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: `portfolio-detail.templ`, Line: 160, Col: 71} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 156, Col: 94} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\">Löschen
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if activity.Currency != portfolio.BaseCurrency { + var templ_7745c5c3_Var29 string + templ_7745c5c3_Var29, 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: 159, Col: 84} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("-") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Keine Transaktionen vorhanden
Keine Transaktionen vorhanden
Historische Währungsumrechnung
Transaktionen werden mit den historischen Wechselkursen vom Transaktionsdatum in ") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Historische Währungsumrechnung
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) + var templ_7745c5c3_Var42 string + templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(portfolio.BaseCurrency) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 199, Col: 114} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 227, Col: 114} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " umgerechnet. Bei nicht verfügbaren historischen Kursen werden aktuelle Kurse verwendet (markiert mit *).

Portfolio-Zusammenfassung

") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" umgerechnet. Bei nicht verfügbaren historischen Kursen werden aktuelle Kurse verwendet (markiert mit *).

Portfolio-Zusammenfassung

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = PortfolioSummary(portfolio.Activities, portfolio.BaseCurrency).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = PortfolioSummary(positionSummary, len(portfolio.Activities)).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "

Allokation

Allokations-Chart wird hier implementiert

Transaktion hinzufügen

Allokation

Allokations-Chart wird hier implementiert

Transaktion hinzufügen
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var31 string - templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(portfolio.BaseCurrency) + var templ_7745c5c3_Var44 string + templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(portfolio.BaseCurrency) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 258, Col: 63} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 286, Col: 63} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "
Transaktion bearbeiten
Transaktion bearbeiten
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var33 string - templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(portfolio.BaseCurrency) + var templ_7745c5c3_Var46 string + templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(portfolio.BaseCurrency) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 311, Col: 63} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 339, Col: 63} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "
Transaktion löschen

Möchten Sie diese Transaktion wirklich löschen?

Diese Aktion kann nicht rückgängig gemacht werden.
Transaktion löschen

Möchten Sie diese Transaktion wirklich löschen?

Diese Aktion kann nicht rückgängig gemacht werden.
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - return nil + return templ_7745c5c3_Err }) } // Separate component for portfolio summary -func PortfolioSummary(activities []model.Activity, currency string) templ.Component { +func PortfolioSummary(summary model.PositionSummary, transactionCount int) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -573,59 +730,220 @@ func PortfolioSummary(activities []model.Activity, currency string) templ.Compon }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var35 := templ.GetChildren(ctx) - if templ_7745c5c3_Var35 == nil { - templ_7745c5c3_Var35 = templ.NopComponent + templ_7745c5c3_Var48 := templ.GetChildren(ctx) + if templ_7745c5c3_Var48 == nil { + templ_7745c5c3_Var48 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "
Anzahl Transaktionen
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Anzahl Transaktionen
") 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))) + var templ_7745c5c3_Var49 string + templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", transactionCount)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 404, Col: 61} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 432, Col: 62} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "
Gesamtwert investiert
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Aktueller Portfoliowert
") 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)) + var templ_7745c5c3_Var50 string + templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(formatCurrency(summary.TotalValue, summary.Currency)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 409, Col: 62} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 437, Col: 59} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "
* = Aktueller Kurs verwendet (historischer Kurs nicht verfügbar)
Letzter Kauf
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Gesamtwert investiert
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var38 string - templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(getLastBuyDate(activities)) + var templ_7745c5c3_Var51 string + templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(formatCurrency(summary.TotalCostBasis, summary.Currency)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `portfolio-detail.templ`, Line: 418, Col: 33} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 443, Col: 63} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Unrealisierte Gewinne/Verluste
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - return nil + var templ_7745c5c3_Var52 = []any{getColorClass(summary.TotalUnrealizedPL) + " h3 mb-0"} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var52...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var54 string + templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(formatCurrency(summary.TotalUnrealizedPL, summary.Currency)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 449, Col: 66} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if summary.TotalCostBasis > 0 { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("(") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var55 string + templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(formatPercentage(summary.TotalUnrealizedPL / summary.TotalCostBasis * 100)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 451, Col: 103} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(")") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Dividenden erhalten
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var56 string + templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(formatCurrency(summary.TotalDividends, summary.Currency)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 458, Col: 63} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Gesamtrendite
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var57 = []any{getColorClass(summary.TotalReturn) + " h3 mb-0"} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var57...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var59 string + templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinStringErrs(formatCurrency(summary.TotalReturn, summary.Currency)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 464, Col: 60} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if summary.TotalCostBasis > 0 { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("(") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var60 string + templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.JoinStringErrs(formatPercentage(summary.TotalReturnPct)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 466, Col: 69} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var60)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(")") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err }) } -func PortfolioDetail(authenticated bool, username string, portfolio model.Portfolio, portfolios []model.Portfolio) templ.Component { +// Helper functions for formatting +func formatCurrency(value float64, currency string) string { + switch currency { + case "EUR": + return fmt.Sprintf("€%.2f", value) + case "USD": + return fmt.Sprintf("$%.2f", value) + case "GBP": + return fmt.Sprintf("£%.2f", value) + case "CHF": + return fmt.Sprintf("%.2f CHF", value) + default: + return fmt.Sprintf("%.2f %s", value, currency) + } +} + +func formatPercentage(value float64) string { + if value > 0 { + return fmt.Sprintf("+%.2f%%", value) + } + return fmt.Sprintf("%.2f%%", value) +} + +func getColorClass(value float64) string { + if value > 0 { + return "text-success" + } else if value < 0 { + return "text-danger" + } + return "text-muted" +} + +func PortfolioDetail(authenticated bool, username string, portfolio model.Portfolio, portfolios []model.Portfolio, positions []model.Position, positionSummary model.PositionSummary) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -641,16 +959,16 @@ func PortfolioDetail(authenticated bool, username string, portfolio model.Portfo }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var39 := templ.GetChildren(ctx) - if templ_7745c5c3_Var39 == nil { - templ_7745c5c3_Var39 = templ.NopComponent + templ_7745c5c3_Var61 := templ.GetChildren(ctx) + if templ_7745c5c3_Var61 == nil { + templ_7745c5c3_Var61 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = components.PageLayout(authenticated, username, portfolio.Name, PortfolioDetailContent(portfolio), portfolios).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = components.PageLayout(authenticated, username, portfolio.Name, PortfolioDetailContent(portfolio, positions, positionSummary), portfolios).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - return nil + return templ_7745c5c3_Err }) }