first commit
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
package auth
|
||||
|
||||
templ RegisterForm() {
|
||||
<form method="post" action="/user/register" style="max-width:400px;margin:auto;">
|
||||
<h2>Registrieren</h2>
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Benutzername</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">E-Mail</label>
|
||||
<input type="email" class="form-control" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Passwort</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Registrieren</button>
|
||||
</form>
|
||||
}
|
||||
|
||||
templ LoginForm() {
|
||||
<form method="post" action="/user/login" style="max-width:400px;margin:auto;">
|
||||
<h2>Anmelden</h2>
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Benutzername</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Passwort</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Anmelden</button>
|
||||
</form>
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.2.778
|
||||
package auth
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func RegisterForm() 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<form method=\"post\" action=\"/user/register\" style=\"max-width:400px;margin:auto;\"><h2>Registrieren</h2><div class=\"mb-3\"><label for=\"username\" class=\"form-label\">Benutzername</label> <input type=\"text\" class=\"form-control\" id=\"username\" name=\"username\" required></div><div class=\"mb-3\"><label for=\"email\" class=\"form-label\">E-Mail</label> <input type=\"email\" class=\"form-control\" id=\"email\" name=\"email\" required></div><div class=\"mb-3\"><label for=\"password\" class=\"form-label\">Passwort</label> <input type=\"password\" class=\"form-control\" id=\"password\" name=\"password\" required></div><button type=\"submit\" class=\"btn btn-primary w-100\">Registrieren</button></form>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func LoginForm() 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var2 == nil {
|
||||
templ_7745c5c3_Var2 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<form method=\"post\" action=\"/user/login\" style=\"max-width:400px;margin:auto;\"><h2>Anmelden</h2><div class=\"mb-3\"><label for=\"username\" class=\"form-label\">Benutzername</label> <input type=\"text\" class=\"form-control\" id=\"username\" name=\"username\" required></div><div class=\"mb-3\"><label for=\"password\" class=\"form-label\">Passwort</label> <input type=\"password\" class=\"form-control\" id=\"password\" name=\"password\" required></div><button type=\"submit\" class=\"btn btn-primary w-100\">Anmelden</button></form>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,78 @@
|
||||
package components
|
||||
|
||||
import "portfolio-tracker/internal/model"
|
||||
|
||||
templ PageLayout(authenticated bool, username string, title string, content templ.Component, portfolios []model.Portfolio) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>{ title }</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/core@1.3.2/dist/css/tabler.min.css"/>
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
body {
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.container-xl, .container-fluid {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
.page-wrapper {
|
||||
margin-left: 220px;
|
||||
width: auto;
|
||||
}
|
||||
.card {
|
||||
width: 100%;
|
||||
}
|
||||
.dropdown-search {
|
||||
position: relative;
|
||||
}
|
||||
.dropdown-menu-search {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 0 0 .25rem .25rem;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.dropdown-menu-search.show {
|
||||
display: block;
|
||||
}
|
||||
.dropdown-item-search {
|
||||
display: block;
|
||||
padding: .5rem 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.dropdown-item-search:hover {
|
||||
background: #f1f3f4;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
|
||||
@Search()
|
||||
<div class="page">
|
||||
@Navigation(authenticated, portfolios)
|
||||
<div class="page-wrapper">
|
||||
@content
|
||||
</div>
|
||||
</div>
|
||||
@SearchJS()
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tabler/core@1.3.2/dist/js/tabler.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.2.778
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import "portfolio-tracker/internal/model"
|
||||
|
||||
func PageLayout(authenticated bool, username string, title string, content templ.Component, portfolios []model.Portfolio) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/components/layout.templ`, Line: 11, Col: 17}
|
||||
}
|
||||
_, 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("</title><link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@tabler/core@1.3.2/dist/css/tabler.min.css\"><style>\n\t\t\thtml, body {\n\t\t\t\theight: 100%;\n\t\t\t\twidth: 100%;\n\t\t\t}\n\t\t\tbody {\n\t\t\t\tmin-height: 100vh;\n\t\t\t\twidth: 100vw;\n\t\t\t\toverflow-x: hidden;\n\t\t\t}\n\t\t\t.container-xl, .container-fluid {\n\t\t\t\twidth: 100% !important;\n\t\t\t\tmax-width: 100% !important;\n\t\t\t\tpadding-left: 24px;\n\t\t\t\tpadding-right: 24px;\n\t\t\t}\n\t\t\t.page-wrapper {\n\t\t\t\tmargin-left: 220px;\n\t\t\t\twidth: auto;\n\t\t\t}\n\t\t\t.card {\n\t\t\t\twidth: 100%;\n\t\t\t}\n\t\t\t.dropdown-search {\n\t\t\t\tposition: relative;\n\t\t\t}\n\t\t\t.dropdown-menu-search {\n\t\t\t\tdisplay: none;\n\t\t\t\tposition: absolute;\n\t\t\t\ttop: 100%;\n\t\t\t\tleft: 0;\n\t\t\t\twidth: 100%;\n\t\t\t\tz-index: 1000;\n\t\t\t\tbackground: #fff;\n\t\t\t\tborder: 1px solid #ddd;\n\t\t\t\tborder-radius: 0 0 .25rem .25rem;\n\t\t\t\tmax-height: 300px;\n\t\t\t\toverflow-y: auto;\n\t\t\t}\n\t\t\t.dropdown-menu-search.show {\n\t\t\t\tdisplay: block;\n\t\t\t}\n\t\t\t.dropdown-item-search {\n\t\t\t\tdisplay: block;\n\t\t\t\tpadding: .5rem 1rem;\n\t\t\t\tcursor: pointer;\n\t\t\t}\n\t\t\t.dropdown-item-search:hover {\n\t\t\t\tbackground: #f1f3f4;\n\t\t\t}\n\t\t</style></head><body><script src=\"https://cdn.jsdelivr.net/npm/apexcharts\"></script>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Search().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"page\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Navigation(authenticated, portfolios).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"page-wrapper\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = content.Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = SearchJS().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<script src=\"https://cdn.jsdelivr.net/npm/@tabler/core@1.3.2/dist/js/tabler.min.js\"></script></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,192 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"portfolio-tracker/internal/model"
|
||||
)
|
||||
|
||||
templ Navigation(authenticated bool, portfolios []model.Portfolio) {
|
||||
<aside class="navbar navbar-vertical navbar-expand-lg" data-bs-theme="dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">
|
||||
<span class="navbar-brand-text">Portfolio Tracker</span>
|
||||
</a>
|
||||
<div class="collapse navbar-collapse show">
|
||||
<ul class="navbar-nav pt-lg-3">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/">
|
||||
<span class="nav-link-icon">
|
||||
<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>
|
||||
<rect x="3" y="12" width="6" height="8" rx="1"></rect>
|
||||
<rect x="9" y="8" width="6" height="12" rx="1"></rect>
|
||||
<rect x="15" y="4" width="6" height="16" rx="1"></rect>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nav-link-title">Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="portfolioDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span class="nav-link-icon">
|
||||
<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="M9 5H7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2V7a2 2 0 0 0 -2 -2h-2"></path>
|
||||
<path d="M9 3m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z"></path>
|
||||
<path d="M9 12l2 2l4 -4"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nav-link-title">Portfolio</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="portfolioDropdown">
|
||||
if len(portfolios) > 0 {
|
||||
for _, portfolio := range portfolios {
|
||||
<li><a class="dropdown-item" href={ templ.URL("/portfolio/" + templ.EscapeString(fmt.Sprintf("%d", portfolio.ID))) }>{ portfolio.Name }</a></li>
|
||||
}
|
||||
<li><hr class="dropdown-divider"/></li>
|
||||
} else {
|
||||
<li><span class="dropdown-item-text text-muted">Keine Portfolios vorhanden</span></li>
|
||||
<li><hr class="dropdown-divider"/></li>
|
||||
}
|
||||
<li>
|
||||
<a class="dropdown-item" href="/portfolio/new" data-bs-toggle="modal" data-bs-target="#createPortfolioModal">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-sm me-2" 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>
|
||||
Neues Portfolio erstellen
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">
|
||||
<span class="nav-link-icon">
|
||||
<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="M3 21l18 0"></path>
|
||||
<path d="M3 10l18 0"></path>
|
||||
<path d="M5 6l7 -3l7 3"></path>
|
||||
<path d="M4 10l0 11"></path>
|
||||
<path d="M20 10l0 11"></path>
|
||||
<path d="M8 14l0 3"></path>
|
||||
<path d="M12 14l0 3"></path>
|
||||
<path d="M16 14l0 3"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nav-link-title">Wertpapiere</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Weitere Menüpunkte hier -->
|
||||
</ul>
|
||||
<div id="login_buttons" class="mt-4" style="padding-left:10px; padding-bottom:10px;">
|
||||
if authenticated == true {
|
||||
<form method="post" action="/user/logout">
|
||||
<button type="submit" class="btn btn-danger w-100">Logout</button>
|
||||
</form>
|
||||
} else {
|
||||
<button type="button" class="btn btn-outline-primary w-100 mb-2" data-bs-toggle="modal" data-bs-target="#loginModal">Login</button>
|
||||
<button type="button" class="btn btn-primary w-100" data-bs-toggle="modal" data-bs-target="#registerModal">Registrieren</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Create Portfolio Modal -->
|
||||
<div class="modal modal-blur fade" id="createPortfolioModal" tabindex="-1" aria-labelledby="createPortfolioModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="createPortfolioModalLabel">Neues Portfolio erstellen</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
|
||||
</div>
|
||||
<form method="post" action="/portfolio/create">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="portfolio-name" class="form-label">Portfolio Name</label>
|
||||
<input type="text" class="form-control" id="portfolio-name" name="name" placeholder="z.B. Mein Hauptportfolio" required/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="portfolio-currency" class="form-label">Basiswährung</label>
|
||||
<select class="form-select" id="portfolio-currency" name="base_currency" required>
|
||||
<option value="">Wählen Sie eine Währung</option>
|
||||
<option value="EUR">EUR - Euro</option>
|
||||
<option value="USD">USD - US Dollar</option>
|
||||
<option value="GBP">GBP - Britisches Pfund</option>
|
||||
<option value="CHF">CHF - Schweizer Franken</option>
|
||||
<option value="JPY">JPY - Japanischer Yen</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="portfolio-description" class="form-label">Beschreibung (optional)</label>
|
||||
<textarea class="form-control" id="portfolio-description" name="description" rows="3" placeholder="Beschreibung des Portfolios..."></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">Portfolio erstellen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Login Modal -->
|
||||
<div class="modal modal-blur fade" id="loginModal" tabindex="-1" aria-labelledby="loginModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="loginModalLabel">Anmelden</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
|
||||
</div>
|
||||
<form method="post" action="/user/login">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="login-username" class="form-label">Benutzername</label>
|
||||
<input type="text" class="form-control" id="login-username" name="username" required/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="login-password" class="form-label">Passwort</label>
|
||||
<input type="password" class="form-control" id="login-password" name="password" required/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer d-flex justify-content-between align-items-center">
|
||||
<a href="#" class="text-secondary small" data-bs-dismiss="modal" style="text-decoration:none;">Abbrechen</a>
|
||||
<button type="submit" class="btn btn-primary">Anmelden</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Register Modal -->
|
||||
<div class="modal modal-blur fade" id="registerModal" tabindex="-1" aria-labelledby="registerModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="registerModalLabel">Registrieren</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
|
||||
</div>
|
||||
<form method="post" action="/user/register">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="register-username" class="form-label">Benutzername</label>
|
||||
<input type="text" class="form-control" id="register-username" name="username" required/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="register-email" class="form-label">E-Mail</label>
|
||||
<input type="email" class="form-control" id="register-email" name="email" required/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="register-password" class="form-label">Passwort</label>
|
||||
<input type="password" class="form-control" id="register-password" name="password" required/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer d-flex justify-content-between align-items-center">
|
||||
<a href="#" class="text-secondary small" data-bs-dismiss="modal" style="text-decoration:none;">Abbrechen</a>
|
||||
<button type="submit" class="btn btn-primary">Registrieren</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.2.778
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"portfolio-tracker/internal/model"
|
||||
)
|
||||
|
||||
func Navigation(authenticated bool, portfolios []model.Portfolio) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<aside class=\"navbar navbar-vertical navbar-expand-lg\" data-bs-theme=\"dark\"><div class=\"container-fluid\"><a class=\"navbar-brand\" href=\"#\"><span class=\"navbar-brand-text\">Portfolio Tracker</span></a><div class=\"collapse navbar-collapse show\"><ul class=\"navbar-nav pt-lg-3\"><li class=\"nav-item\"><a class=\"nav-link active\" href=\"/\"><span class=\"nav-link-icon\"><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> <rect x=\"3\" y=\"12\" width=\"6\" height=\"8\" rx=\"1\"></rect> <rect x=\"9\" y=\"8\" width=\"6\" height=\"12\" rx=\"1\"></rect> <rect x=\"15\" y=\"4\" width=\"6\" height=\"16\" rx=\"1\"></rect></svg></span> <span class=\"nav-link-title\">Dashboard</span></a></li><li class=\"nav-item dropdown\"><a class=\"nav-link dropdown-toggle\" href=\"#\" id=\"portfolioDropdown\" role=\"button\" data-bs-toggle=\"dropdown\" aria-expanded=\"false\"><span class=\"nav-link-icon\"><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=\"M9 5H7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2V7a2 2 0 0 0 -2 -2h-2\"></path> <path d=\"M9 3m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z\"></path> <path d=\"M9 12l2 2l4 -4\"></path></svg></span> <span class=\"nav-link-title\">Portfolio</span></a><ul class=\"dropdown-menu\" aria-labelledby=\"portfolioDropdown\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(portfolios) > 0 {
|
||||
for _, portfolio := range portfolios {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li><a class=\"dropdown-item\" href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 templ.SafeURL = templ.URL("/portfolio/" + templ.EscapeString(fmt.Sprintf("%d", portfolio.ID)))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var2)))
|
||||
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_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(portfolio.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/components/navigation.templ`, Line: 44, Col: 142}
|
||||
}
|
||||
_, 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("</a></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <li><hr class=\"dropdown-divider\"></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li><span class=\"dropdown-item-text text-muted\">Keine Portfolios vorhanden</span></li><li><hr class=\"dropdown-divider\"></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li><a class=\"dropdown-item\" href=\"/portfolio/new\" data-bs-toggle=\"modal\" data-bs-target=\"#createPortfolioModal\"><svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon icon-sm me-2\" 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> Neues Portfolio erstellen</a></li></ul></li><li class=\"nav-item\"><a class=\"nav-link\" href=\"#\"><span class=\"nav-link-icon\"><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=\"M3 21l18 0\"></path> <path d=\"M3 10l18 0\"></path> <path d=\"M5 6l7 -3l7 3\"></path> <path d=\"M4 10l0 11\"></path> <path d=\"M20 10l0 11\"></path> <path d=\"M8 14l0 3\"></path> <path d=\"M12 14l0 3\"></path> <path d=\"M16 14l0 3\"></path></svg></span> <span class=\"nav-link-title\">Wertpapiere</span></a></li><!-- Weitere Menüpunkte hier --></ul><div id=\"login_buttons\" class=\"mt-4\" style=\"padding-left:10px; padding-bottom:10px;\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if authenticated == true {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<form method=\"post\" action=\"/user/logout\"><button type=\"submit\" class=\"btn btn-danger w-100\">Logout</button></form>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<button type=\"button\" class=\"btn btn-outline-primary w-100 mb-2\" data-bs-toggle=\"modal\" data-bs-target=\"#loginModal\">Login</button> <button type=\"button\" class=\"btn btn-primary w-100\" data-bs-toggle=\"modal\" data-bs-target=\"#registerModal\">Registrieren</button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div></div></aside><!-- Create Portfolio Modal --><div class=\"modal modal-blur fade\" id=\"createPortfolioModal\" tabindex=\"-1\" aria-labelledby=\"createPortfolioModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"createPortfolioModalLabel\">Neues Portfolio erstellen</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Schließen\"></button></div><form method=\"post\" action=\"/portfolio/create\"><div class=\"modal-body\"><div class=\"mb-3\"><label for=\"portfolio-name\" class=\"form-label\">Portfolio Name</label> <input type=\"text\" class=\"form-control\" id=\"portfolio-name\" name=\"name\" placeholder=\"z.B. Mein Hauptportfolio\" required></div><div class=\"mb-3\"><label for=\"portfolio-currency\" class=\"form-label\">Basiswährung</label> <select class=\"form-select\" id=\"portfolio-currency\" name=\"base_currency\" required><option value=\"\">Wählen Sie eine Währung</option> <option value=\"EUR\">EUR - Euro</option> <option value=\"USD\">USD - US Dollar</option> <option value=\"GBP\">GBP - Britisches Pfund</option> <option value=\"CHF\">CHF - Schweizer Franken</option> <option value=\"JPY\">JPY - Japanischer Yen</option></select></div><div class=\"mb-3\"><label for=\"portfolio-description\" class=\"form-label\">Beschreibung (optional)</label> <textarea class=\"form-control\" id=\"portfolio-description\" name=\"description\" rows=\"3\" placeholder=\"Beschreibung des Portfolios...\"></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\">Portfolio erstellen</button></div></form></div></div></div><!-- Login Modal --><div class=\"modal modal-blur fade\" id=\"loginModal\" tabindex=\"-1\" aria-labelledby=\"loginModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"loginModalLabel\">Anmelden</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Schließen\"></button></div><form method=\"post\" action=\"/user/login\"><div class=\"modal-body\"><div class=\"mb-3\"><label for=\"login-username\" class=\"form-label\">Benutzername</label> <input type=\"text\" class=\"form-control\" id=\"login-username\" name=\"username\" required></div><div class=\"mb-3\"><label for=\"login-password\" class=\"form-label\">Passwort</label> <input type=\"password\" class=\"form-control\" id=\"login-password\" name=\"password\" required></div></div><div class=\"modal-footer d-flex justify-content-between align-items-center\"><a href=\"#\" class=\"text-secondary small\" data-bs-dismiss=\"modal\" style=\"text-decoration:none;\">Abbrechen</a> <button type=\"submit\" class=\"btn btn-primary\">Anmelden</button></div></form></div></div></div><!-- Register Modal --><div class=\"modal modal-blur fade\" id=\"registerModal\" tabindex=\"-1\" aria-labelledby=\"registerModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"registerModalLabel\">Registrieren</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Schließen\"></button></div><form method=\"post\" action=\"/user/register\"><div class=\"modal-body\"><div class=\"mb-3\"><label for=\"register-username\" class=\"form-label\">Benutzername</label> <input type=\"text\" class=\"form-control\" id=\"register-username\" name=\"username\" required></div><div class=\"mb-3\"><label for=\"register-email\" class=\"form-label\">E-Mail</label> <input type=\"email\" class=\"form-control\" id=\"register-email\" name=\"email\" required></div><div class=\"mb-3\"><label for=\"register-password\" class=\"form-label\">Passwort</label> <input type=\"password\" class=\"form-control\" id=\"register-password\" name=\"password\" required></div></div><div class=\"modal-footer d-flex justify-content-between align-items-center\"><a href=\"#\" class=\"text-secondary small\" data-bs-dismiss=\"modal\" style=\"text-decoration:none;\">Abbrechen</a> <button type=\"submit\" class=\"btn btn-primary\">Registrieren</button></div></form></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,19 @@
|
||||
package components
|
||||
|
||||
templ Search() {
|
||||
<div class="navbar navbar-expand-lg navbar-light bg-white" style="margin-left:220px;">
|
||||
<div class="container-fluid">
|
||||
<form class="d-flex dropdown-search" id="stock-search-form" autocomplete="off" style="width:100%;">
|
||||
<span class="input-group-text" style="background:transparent; border:none; padding-right:0;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icon-tabler-search" viewBox="0 0 24 24">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<input class="form-control me-2" type="search" placeholder="WKN, ISIN, Name ..." aria-label="Search" id="stock-search-input" autocomplete="off" style="margin-left:-1px;"/>
|
||||
<button class="btn btn-primary" type="submit">Suchen</button>
|
||||
<div class="dropdown-menu-search" id="search-dropdown"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.2.778
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func Search() 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"navbar navbar-expand-lg navbar-light bg-white\" style=\"margin-left:220px;\"><div class=\"container-fluid\"><form class=\"d-flex dropdown-search\" id=\"stock-search-form\" autocomplete=\"off\" style=\"width:100%;\"><span class=\"input-group-text\" style=\"background:transparent; border:none; padding-right:0;\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"icon icon-tabler icon-tabler-search\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"8\"></circle> <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"></line></svg></span> <input class=\"form-control me-2\" type=\"search\" placeholder=\"WKN, ISIN, Name ...\" aria-label=\"Search\" id=\"stock-search-input\" autocomplete=\"off\" style=\"margin-left:-1px;\"> <button class=\"btn btn-primary\" type=\"submit\">Suchen</button><div class=\"dropdown-menu-search\" id=\"search-dropdown\"></div></form></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,89 @@
|
||||
package components
|
||||
|
||||
templ SearchJS() {
|
||||
<script>
|
||||
const input = document.getElementById('stock-search-input');
|
||||
const dropdown = document.getElementById('search-dropdown');
|
||||
let debounceTimeout;
|
||||
let selectedIndex = -1;
|
||||
|
||||
input.addEventListener('input', function () {
|
||||
selectedIndex = -1;
|
||||
const query = input.value.trim();
|
||||
clearTimeout(debounceTimeout);
|
||||
if (query.length < 2) {
|
||||
dropdown.classList.remove('show');
|
||||
dropdown.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
debounceTimeout = setTimeout(() => {
|
||||
fetch('/api/stocksearch?q=' + encodeURIComponent(query))
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.quotes && data.quotes.length > 0) {
|
||||
dropdown.innerHTML = data.quotes.map(item =>
|
||||
`<a class="dropdown-item-search" href="/details?stock=${encodeURIComponent(item.symbol)}">${item.longname || item.shortname || item.symbol} (${item.symbol})</a>`
|
||||
).join('');
|
||||
dropdown.classList.add('show');
|
||||
} else {
|
||||
dropdown.innerHTML = '<div class="dropdown-item-search">Keine Ergebnisse</div>';
|
||||
dropdown.classList.add('show');
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
|
||||
input.addEventListener('keydown', function (e) {
|
||||
const items = Array.from(dropdown.querySelectorAll('.dropdown-item-search'));
|
||||
if (!dropdown.classList.contains('show') || items.length === 0) return;
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
selectedIndex = (selectedIndex + 1) % items.length;
|
||||
updateDropdownSelection(items);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
selectedIndex = (selectedIndex - 1 + items.length) % items.length;
|
||||
updateDropdownSelection(items);
|
||||
} else if (e.key === 'Enter') {
|
||||
if (selectedIndex >= 0 && selectedIndex < items.length) {
|
||||
e.preventDefault();
|
||||
window.location.href = items[selectedIndex].getAttribute('href');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function updateDropdownSelection(items) {
|
||||
items.forEach((item, idx) => {
|
||||
if (idx === selectedIndex) {
|
||||
item.classList.add('active');
|
||||
item.scrollIntoView({ block: 'nearest' });
|
||||
} else {
|
||||
item.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
if (!input.contains(e.target) && !dropdown.contains(e.target)) {
|
||||
dropdown.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('stock-search-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const items = Array.from(dropdown.querySelectorAll('.dropdown-item-search'));
|
||||
// Wenn ein Eintrag ausgewählt ist, nimm diesen
|
||||
if (selectedIndex >= 0 && selectedIndex < items.length) {
|
||||
window.location.href = items[selectedIndex].getAttribute('href');
|
||||
return;
|
||||
}
|
||||
// Sonst nimm das erste Ergebnis, falls vorhanden
|
||||
if (items.length > 0) {
|
||||
window.location.href = items[0].getAttribute('href');
|
||||
return;
|
||||
}
|
||||
// Sonst nichts tun
|
||||
});
|
||||
</script>
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.2.778
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func SearchJS() 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<script>\n const input = document.getElementById('stock-search-input');\n const dropdown = document.getElementById('search-dropdown');\n let debounceTimeout;\n let selectedIndex = -1;\n\n input.addEventListener('input', function () {\n selectedIndex = -1;\n const query = input.value.trim();\n clearTimeout(debounceTimeout);\n if (query.length < 2) {\n dropdown.classList.remove('show');\n dropdown.innerHTML = '';\n return;\n }\n debounceTimeout = setTimeout(() => {\n fetch('/api/stocksearch?q=' + encodeURIComponent(query))\n .then(res => res.json())\n .then(data => {\n if (data.quotes && data.quotes.length > 0) {\n dropdown.innerHTML = data.quotes.map(item =>\n `<a class=\"dropdown-item-search\" href=\"/details?stock=${encodeURIComponent(item.symbol)}\">${item.longname || item.shortname || item.symbol} (${item.symbol})</a>`\n ).join('');\n dropdown.classList.add('show');\n } else {\n dropdown.innerHTML = '<div class=\"dropdown-item-search\">Keine Ergebnisse</div>';\n dropdown.classList.add('show');\n }\n });\n }, 300);\n });\n\n input.addEventListener('keydown', function (e) {\n const items = Array.from(dropdown.querySelectorAll('.dropdown-item-search'));\n if (!dropdown.classList.contains('show') || items.length === 0) return;\n\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n selectedIndex = (selectedIndex + 1) % items.length;\n updateDropdownSelection(items);\n } else if (e.key === 'ArrowUp') {\n e.preventDefault();\n selectedIndex = (selectedIndex - 1 + items.length) % items.length;\n updateDropdownSelection(items);\n } else if (e.key === 'Enter') {\n if (selectedIndex >= 0 && selectedIndex < items.length) {\n e.preventDefault();\n window.location.href = items[selectedIndex].getAttribute('href');\n }\n }\n });\n\n function updateDropdownSelection(items) {\n items.forEach((item, idx) => {\n if (idx === selectedIndex) {\n item.classList.add('active');\n item.scrollIntoView({ block: 'nearest' });\n } else {\n item.classList.remove('active');\n }\n });\n }\n\n document.addEventListener('click', function (e) {\n if (!input.contains(e.target) && !dropdown.contains(e.target)) {\n dropdown.classList.remove('show');\n }\n });\n\n document.getElementById('stock-search-form').addEventListener('submit', function(e) {\n e.preventDefault();\n const items = Array.from(dropdown.querySelectorAll('.dropdown-item-search'));\n // Wenn ein Eintrag ausgewählt ist, nimm diesen\n if (selectedIndex >= 0 && selectedIndex < items.length) {\n window.location.href = items[selectedIndex].getAttribute('href');\n return;\n }\n // Sonst nimm das erste Ergebnis, falls vorhanden\n if (items.length > 0) {\n window.location.href = items[0].getAttribute('href');\n return;\n }\n // Sonst nichts tun\n });\n </script>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,149 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"portfolio-tracker/internal/model"
|
||||
"portfolio-tracker/internal/util"
|
||||
"time"
|
||||
)
|
||||
|
||||
// calculateTotalInvested calculates the total amount invested (buy transactions)
|
||||
func calculateTotalInvested(activities []model.Activity) float64 {
|
||||
var total float64 = 0
|
||||
for _, activity := range activities {
|
||||
if activity.Type == model.Buy {
|
||||
total += activity.Amount * activity.Price
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// calculateTotalInvestedInBaseCurrency calculates total invested converted to portfolio base currency
|
||||
func calculateTotalInvestedInBaseCurrency(activities []model.Activity, baseCurrency string) float64 {
|
||||
var total float64 = 0
|
||||
for _, activity := range activities {
|
||||
if activity.Type == model.Buy {
|
||||
activityTotal := activity.Amount * activity.Price
|
||||
|
||||
if activity.Currency == baseCurrency {
|
||||
total += activityTotal
|
||||
} else {
|
||||
convertedTotal, err := util.ConvertCurrency(activityTotal, activity.Currency, baseCurrency)
|
||||
if err != nil {
|
||||
// If conversion fails, add original amount (fallback)
|
||||
total += activityTotal
|
||||
} else {
|
||||
total += convertedTotal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// getLastBuyDate returns the date of the last buy transaction
|
||||
func getLastBuyDate(activities []model.Activity) string {
|
||||
var lastBuy time.Time
|
||||
for _, activity := range activities {
|
||||
if activity.Type == model.Buy && activity.Date.After(lastBuy) {
|
||||
lastBuy = activity.Date
|
||||
}
|
||||
}
|
||||
if lastBuy.IsZero() {
|
||||
return "Keine Käufe"
|
||||
}
|
||||
return lastBuy.Format("02.01.2006")
|
||||
}
|
||||
|
||||
// convertActivityPrice converts activity price to portfolio base currency if different
|
||||
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)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
return convertedPrice, portfolioBaseCurrency, nil
|
||||
}
|
||||
|
||||
// formatActivityPrice formats activity price with conversion if needed
|
||||
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)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%.2f %s (conv. error)", activity.Price, activity.Currency)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%.2f %s (~%.2f %s)", activity.Price, activity.Currency, convertedPrice, portfolioBaseCurrency)
|
||||
}
|
||||
|
||||
// formatActivityTotal formats activity total with conversion if needed
|
||||
func formatActivityTotal(activity model.Activity, portfolioBaseCurrency string) string {
|
||||
total := activity.Amount * activity.Price
|
||||
|
||||
if activity.Currency == portfolioBaseCurrency {
|
||||
return fmt.Sprintf("%.2f %s", total, activity.Currency)
|
||||
}
|
||||
|
||||
convertedTotal, err := util.ConvertCurrency(total, activity.Currency, portfolioBaseCurrency)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%.2f %s (conv. error)", total, activity.Currency)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%.2f %s (~%.2f %s)", total, activity.Currency, convertedTotal, portfolioBaseCurrency)
|
||||
}
|
||||
|
||||
// getConvertedPrice returns the converted price 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)
|
||||
if err != nil {
|
||||
return "Conv. Error"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%.2f %s", convertedPrice, portfolioBaseCurrency)
|
||||
}
|
||||
|
||||
// getConvertedTotal returns the converted total 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)
|
||||
if err != nil {
|
||||
return "Conv. Error"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%.2f %s", convertedTotal, portfolioBaseCurrency)
|
||||
}
|
||||
|
||||
// formatTotalInvestedWithConversion formats total invested with currency info
|
||||
func formatTotalInvestedWithConversion(activities []model.Activity, baseCurrency string) string {
|
||||
convertedTotal := calculateTotalInvestedInBaseCurrency(activities, baseCurrency)
|
||||
|
||||
// Check if any activities have different currencies
|
||||
hasDifferentCurrencies := false
|
||||
for _, activity := range activities {
|
||||
if activity.Type == model.Buy && activity.Currency != baseCurrency {
|
||||
hasDifferentCurrencies = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasDifferentCurrencies {
|
||||
return fmt.Sprintf("%.2f %s", convertedTotal, baseCurrency)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%.2f %s (converted)", convertedTotal, baseCurrency)
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"portfolio-tracker/internal/model"
|
||||
"portfolio-tracker/internal/web/templates/components"
|
||||
)
|
||||
|
||||
templ PortfolioDetailContent(portfolio model.Portfolio) {
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="page-header">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="page-title">{ portfolio.Name } ({ portfolio.BaseCurrency })</h2>
|
||||
<div class="page-subtitle">
|
||||
if portfolio.Description != "" {
|
||||
{ portfolio.Description }
|
||||
} else {
|
||||
Erstellt am { portfolio.CreatedAt.Format("02.01.2006") }
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto ms-auto d-print-none">
|
||||
<div class="btn-list">
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addTransactionModal">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M12 5l0 14"></path>
|
||||
<path d="M5 12l14 0"></path>
|
||||
</svg>
|
||||
Transaktion hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<!-- Portfolio Overview -->
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Positionen</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Wertpapier</th>
|
||||
<th>Anzahl</th>
|
||||
<th>Ø Einkaufspreis</th>
|
||||
<th>Aktueller Wert</th>
|
||||
<th>+/- Gesamt</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
if len(portfolio.Activities) > 0 {
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted py-4">
|
||||
Position calculation will be implemented here
|
||||
</td>
|
||||
</tr>
|
||||
} else {
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted py-4">
|
||||
Keine Positionen vorhanden. Fügen Sie Ihre erste Transaktion hinzu.
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Recent Activities -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Letzte Transaktionen</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Typ</th>
|
||||
<th>Wertpapier</th>
|
||||
<th>Anzahl</th>
|
||||
<th>Preis</th>
|
||||
<th>Preis ({ portfolio.BaseCurrency })</th>
|
||||
<th>Gesamt</th>
|
||||
<th>Gesamt ({ portfolio.BaseCurrency })</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
if len(portfolio.Activities) > 0 {
|
||||
for i, activity := range portfolio.Activities {
|
||||
if i < 10 {
|
||||
<tr>
|
||||
<td>{ activity.Date.Format("02.01.2006") }</td>
|
||||
<td>
|
||||
if activity.Type == "BUY" {
|
||||
<span class="badge bg-green">Kauf</span>
|
||||
} else if activity.Type == "SELL" {
|
||||
<span class="badge bg-red">Verkauf</span>
|
||||
} else if activity.Type == "DIVIDEND" {
|
||||
<span class="badge bg-blue">Dividende</span>
|
||||
} else {
|
||||
<span class="badge bg-secondary">{ string(activity.Type) }</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<a href={ templ.URL("/details?stock=" + activity.Stock) } class="text-decoration-none">
|
||||
{ activity.Stock }
|
||||
</a>
|
||||
</td>
|
||||
<td>{ fmt.Sprintf("%.3f", activity.Amount) }</td>
|
||||
<td>{ fmt.Sprintf("%.2f %s", activity.Price, activity.Currency) }</td>
|
||||
<td>
|
||||
if activity.Currency != portfolio.BaseCurrency {
|
||||
{ getConvertedPrice(activity, portfolio.BaseCurrency) }
|
||||
} else {
|
||||
<span class="text-muted">-</span>
|
||||
}
|
||||
</td>
|
||||
<td>{ fmt.Sprintf("%.2f %s", activity.Amount * activity.Price, activity.Currency) }</td>
|
||||
<td>
|
||||
if activity.Currency != portfolio.BaseCurrency {
|
||||
{ getConvertedTotal(activity, portfolio.BaseCurrency) }
|
||||
} else {
|
||||
<span class="text-muted">-</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-list flex-nowrap">
|
||||
<button
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#editTransactionModal"
|
||||
data-activity-id={ fmt.Sprintf("%d", activity.ID) }
|
||||
data-activity-type={ string(activity.Type) }
|
||||
data-activity-stock={ activity.Stock }
|
||||
data-activity-amount={ fmt.Sprintf("%.3f", activity.Amount) }
|
||||
data-activity-price={ fmt.Sprintf("%.2f", activity.Price) }
|
||||
data-activity-date={ activity.Date.Format("2006-01-02") }
|
||||
data-activity-note={ activity.Note }
|
||||
>
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#deleteTransactionModal"
|
||||
data-activity-id={ fmt.Sprintf("%d", activity.ID) }
|
||||
data-activity-stock={ activity.Stock }
|
||||
data-activity-type={ string(activity.Type) }
|
||||
data-activity-amount={ fmt.Sprintf("%.3f", activity.Amount) }
|
||||
data-activity-date={ activity.Date.Format("02.01.2006") }
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
} else {
|
||||
<tr>
|
||||
<td colspan="9" class="text-center text-muted py-3">
|
||||
Keine Transaktionen vorhanden
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Portfolio Summary -->
|
||||
<div class="col-md-4">
|
||||
<!-- Currency Conversion Info -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="me-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon text-blue" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<circle cx="12" cy="12" r="9"></circle>
|
||||
<path d="M12 8h.01"></path>
|
||||
<path d="M11 12h1v4h1"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-fill">
|
||||
<div class="font-weight-medium">Währungsumrechnung</div>
|
||||
<div class="text-muted small">
|
||||
Transaktionen in anderen Währungen werden automatisch in { portfolio.BaseCurrency } umgerechnet.
|
||||
Wechselkurse werden stündlich aktualisiert.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Portfolio-Zusammenfassung</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@PortfolioSummary(portfolio.Activities, portfolio.BaseCurrency)
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Allokation</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="allocationChart"></div>
|
||||
<p class="text-muted text-center">Allokations-Chart wird hier implementiert</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add Transaction Modal -->
|
||||
<div class="modal modal-blur fade" id="addTransactionModal" tabindex="-1" aria-labelledby="addTransactionModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="addTransactionModalLabel">Transaktion hinzufügen</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
|
||||
</div>
|
||||
<form method="post" action="/portfolio/transaction">
|
||||
<input type="hidden" name="portfolio_id" value={ fmt.Sprintf("%d", portfolio.ID) }/>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="transaction-type" class="form-label">Typ</label>
|
||||
<select class="form-select" id="transaction-type" name="type" required>
|
||||
<option value="">Wählen Sie einen Typ</option>
|
||||
<option value="BUY">Kauf</option>
|
||||
<option value="SELL">Verkauf</option>
|
||||
<option value="DIVIDEND">Dividende</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="transaction-stock" class="form-label">Wertpapier</label>
|
||||
<input type="text" class="form-control" id="transaction-stock" name="stock" placeholder="z.B. AAPL" required/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="transaction-amount" class="form-label">Anzahl</label>
|
||||
<input type="number" class="form-control" id="transaction-amount" name="amount" step="0.001" min="0" required/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="transaction-price" class="form-label">Preis</label>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="transaction-price" name="price" step="0.01" min="0" required/>
|
||||
<span class="input-group-text">{ portfolio.BaseCurrency }</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="transaction-date" class="form-label">Datum</label>
|
||||
<input type="date" class="form-control" id="transaction-date" name="date" required/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="transaction-note" class="form-label">Notiz (optional)</label>
|
||||
<textarea class="form-control" id="transaction-note" name="note" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="submit" class="btn btn-primary">Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Edit Transaction Modal -->
|
||||
<div class="modal modal-blur fade" id="editTransactionModal" tabindex="-1" aria-labelledby="editTransactionModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editTransactionModalLabel">Transaktion bearbeiten</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
|
||||
</div>
|
||||
<form method="post" action="/portfolio/transaction/edit">
|
||||
<input type="hidden" name="activity_id" id="edit-activity-id"/>
|
||||
<input type="hidden" name="portfolio_id" value={ fmt.Sprintf("%d", portfolio.ID) }/>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="edit-transaction-type" class="form-label">Typ</label>
|
||||
<select class="form-select" id="edit-transaction-type" name="type" required>
|
||||
<option value="">Wählen Sie einen Typ</option>
|
||||
<option value="BUY">Kauf</option>
|
||||
<option value="SELL">Verkauf</option>
|
||||
<option value="DIVIDEND">Dividende</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="edit-transaction-stock" class="form-label">Wertpapier</label>
|
||||
<input type="text" class="form-control" id="edit-transaction-stock" name="stock" placeholder="z.B. AAPL" required/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="edit-transaction-amount" class="form-label">Anzahl</label>
|
||||
<input type="number" class="form-control" id="edit-transaction-amount" name="amount" step="0.001" min="0" required/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="edit-transaction-price" class="form-label">Preis</label>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="edit-transaction-price" name="price" step="0.01" min="0" required/>
|
||||
<span class="input-group-text">{ portfolio.BaseCurrency }</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="edit-transaction-date" class="form-label">Datum</label>
|
||||
<input type="date" class="form-control" id="edit-transaction-date" name="date" required/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="edit-transaction-note" class="form-label">Notiz (optional)</label>
|
||||
<textarea class="form-control" id="edit-transaction-note" name="note" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="submit" class="btn btn-primary">Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Delete Transaction Modal -->
|
||||
<div class="modal modal-blur fade" id="deleteTransactionModal" tabindex="-1" aria-labelledby="deleteTransactionModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteTransactionModalLabel">Transaktion löschen</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Möchten Sie diese Transaktion wirklich löschen?</p>
|
||||
<div class="text-muted" id="delete-transaction-details">
|
||||
<small>Diese Aktion kann nicht rückgängig gemacht werden.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<form method="post" action="/portfolio/transaction/delete" style="display: inline;">
|
||||
<input type="hidden" name="activity_id" id="delete-activity-id"/>
|
||||
<input type="hidden" name="portfolio_id" value={ fmt.Sprintf("%d", portfolio.ID) }/>
|
||||
<button type="submit" class="btn btn-danger">Löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Set today's date as default
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('transaction-date').value = today;
|
||||
|
||||
// Handle edit button clicks
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('[data-bs-target="#editTransactionModal"]')) {
|
||||
const button = e.target.closest('[data-bs-target="#editTransactionModal"]');
|
||||
|
||||
// Populate edit modal with transaction data
|
||||
document.getElementById('edit-activity-id').value = button.dataset.activityId;
|
||||
document.getElementById('edit-transaction-type').value = button.dataset.activityType;
|
||||
document.getElementById('edit-transaction-stock').value = button.dataset.activityStock;
|
||||
document.getElementById('edit-transaction-amount').value = button.dataset.activityAmount;
|
||||
document.getElementById('edit-transaction-price').value = button.dataset.activityPrice;
|
||||
document.getElementById('edit-transaction-date').value = button.dataset.activityDate;
|
||||
document.getElementById('edit-transaction-note').value = button.dataset.activityNote;
|
||||
}
|
||||
|
||||
// Handle delete button clicks
|
||||
if (e.target.closest('[data-bs-target="#deleteTransactionModal"]')) {
|
||||
const button = e.target.closest('[data-bs-target="#deleteTransactionModal"]');
|
||||
|
||||
// Populate delete modal with transaction data
|
||||
document.getElementById('delete-activity-id').value = button.dataset.activityId;
|
||||
|
||||
// Show transaction details in delete modal
|
||||
const details = document.getElementById('delete-transaction-details');
|
||||
details.innerHTML = `
|
||||
<small>
|
||||
<strong>${button.dataset.activityType}</strong> - ${button.dataset.activityStock}<br>
|
||||
${button.dataset.activityAmount} Stück am ${button.dataset.activityDate}
|
||||
</small>
|
||||
`;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
||||
// Separate component for portfolio summary
|
||||
templ PortfolioSummary(activities []model.Activity, currency string) {
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="mb-3">
|
||||
<div class="text-muted">Anzahl Transaktionen</div>
|
||||
<div class="h3 mb-0">{ fmt.Sprintf("%d", len(activities)) }</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-muted">Gesamtwert investiert</div>
|
||||
<div class="h2 mb-0">
|
||||
{ formatTotalInvestedWithConversion(activities, currency) }
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-muted">Letzter Kauf</div>
|
||||
<div class="h4 mb-0 text-muted">
|
||||
{ getLastBuyDate(activities) }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ PortfolioDetail(authenticated bool, username string, portfolio model.Portfolio, portfolios []model.Portfolio) {
|
||||
@components.PageLayout(authenticated, username, portfolio.Name, PortfolioDetailContent(portfolio), portfolios)
|
||||
}
|
||||
@@ -0,0 +1,653 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.2.778
|
||||
package templates
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"portfolio-tracker/internal/model"
|
||||
"portfolio-tracker/internal/web/templates/components"
|
||||
)
|
||||
|
||||
func PortfolioDetailContent(portfolio model.Portfolio) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
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\">")
|
||||
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}
|
||||
}
|
||||
_, 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(" (")
|
||||
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}
|
||||
}
|
||||
_, 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\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if portfolio.Description != "" {
|
||||
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}
|
||||
}
|
||||
_, 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 ")
|
||||
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}
|
||||
}
|
||||
_, 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>")
|
||||
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>")
|
||||
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>")
|
||||
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 (")
|
||||
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}
|
||||
}
|
||||
_, 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 (")
|
||||
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}
|
||||
}
|
||||
_, 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>")
|
||||
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>")
|
||||
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}
|
||||
}
|
||||
_, 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>")
|
||||
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>")
|
||||
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>")
|
||||
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>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<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}
|
||||
}
|
||||
_, 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>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</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)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" 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}
|
||||
}
|
||||
_, 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>")
|
||||
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}
|
||||
}
|
||||
_, 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>")
|
||||
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}
|
||||
}
|
||||
_, 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>")
|
||||
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: `internal/web/templates/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>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</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}
|
||||
}
|
||||
_, 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>")
|
||||
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))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/portfolio-detail.templ`, Line: 131, Col: 68}
|
||||
}
|
||||
_, 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>")
|
||||
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=\"")
|
||||
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}
|
||||
}
|
||||
_, 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=\"")
|
||||
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}
|
||||
}
|
||||
_, 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=\"")
|
||||
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}
|
||||
}
|
||||
_, 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=\"")
|
||||
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}
|
||||
}
|
||||
_, 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=\"")
|
||||
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}
|
||||
}
|
||||
_, 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=\"")
|
||||
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}
|
||||
}
|
||||
_, 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=\"")
|
||||
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}
|
||||
}
|
||||
_, 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=\"")
|
||||
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}
|
||||
}
|
||||
_, 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=\"")
|
||||
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}
|
||||
}
|
||||
_, 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=\"")
|
||||
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}
|
||||
}
|
||||
_, 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=\"")
|
||||
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}
|
||||
}
|
||||
_, 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=\"")
|
||||
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}
|
||||
}
|
||||
_, 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>")
|
||||
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>")
|
||||
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 ")
|
||||
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}
|
||||
}
|
||||
_, 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\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = PortfolioSummary(portfolio.Activities, portfolio.BaseCurrency).Render(ctx, templ_7745c5c3_Buffer)
|
||||
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=\"")
|
||||
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}
|
||||
}
|
||||
_, 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\">")
|
||||
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}
|
||||
}
|
||||
_, 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=\"")
|
||||
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}
|
||||
}
|
||||
_, 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\">")
|
||||
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}
|
||||
}
|
||||
_, 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=\"")
|
||||
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}
|
||||
}
|
||||
_, 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>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
// Separate component for portfolio summary
|
||||
func PortfolioSummary(activities []model.Activity, currency string) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var35 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var35 == nil {
|
||||
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\">")
|
||||
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}
|
||||
}
|
||||
_, 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\">")
|
||||
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}
|
||||
}
|
||||
_, 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\">")
|
||||
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}
|
||||
}
|
||||
_, 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>")
|
||||
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 {
|
||||
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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var39 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var39 == nil {
|
||||
templ_7745c5c3_Var39 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = components.PageLayout(authenticated, username, portfolio.Name, PortfolioDetailContent(portfolio), portfolios).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,154 @@
|
||||
package templates
|
||||
|
||||
import "portfolio-tracker/internal/web/templates/components"
|
||||
import "portfolio-tracker/internal/model"
|
||||
|
||||
templ PortfolioContent(username string) {
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="page-header">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="page-title">Portfolio</h2>
|
||||
<div class="page-subtitle">Verwalten Sie Ihre Investitionen</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 d="M12 5l0 14" />
|
||||
<path d="M5 12l14 0" />
|
||||
</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">Portfolio-Übersicht</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Wertpapier</th>
|
||||
<th>Anzahl</th>
|
||||
<th>Kurs</th>
|
||||
<th>Wert</th>
|
||||
<th>+/-</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted">
|
||||
Keine Positionen vorhanden. Fügen Sie Ihre erste Transaktion hinzu.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Portfolio Summary -->
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Zusammenfassung</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="mb-3">
|
||||
<div class="text-muted">Gesamtwert</div>
|
||||
<div class="h2 mb-0">€0,00</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-muted">Gewinn/Verlust</div>
|
||||
<div class="h3 mb-0 text-success">€0,00</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-muted">Rendite</div>
|
||||
<div class="h3 mb-0 text-success">0,00%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Letzte Transaktionen</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-center text-muted py-3">
|
||||
Keine Transaktionen vorhanden
|
||||
</div>
|
||||
</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">
|
||||
<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.01" min="0" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="transaction-price" class="form-label">Preis</label>
|
||||
<input type="number" class="form-control" id="transaction-price" name="price" step="0.01" min="0" required>
|
||||
</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>
|
||||
}
|
||||
|
||||
templ Portfolio(authenticated bool, username string, portfolios []model.Portfolio) {
|
||||
@components.PageLayout(authenticated, username, "Portfolio", PortfolioContent(username), portfolios)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.2.778
|
||||
package templates
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import "portfolio-tracker/internal/web/templates/components"
|
||||
import "portfolio-tracker/internal/model"
|
||||
|
||||
func PortfolioContent(username string) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
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\">Portfolio</h2><div class=\"page-subtitle\">Verwalten Sie Ihre Investitionen</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\">Portfolio-Übersicht</h3></div><div class=\"card-body\"><div class=\"table-responsive\"><table class=\"table table-vcenter\"><thead><tr><th>Wertpapier</th><th>Anzahl</th><th>Kurs</th><th>Wert</th><th>+/-</th><th></th></tr></thead> <tbody><tr><td colspan=\"6\" class=\"text-center text-muted\">Keine Positionen vorhanden. Fügen Sie Ihre erste Transaktion hinzu.</td></tr></tbody></table></div></div></div></div><!-- Portfolio Summary --><div class=\"col-md-4\"><div class=\"card\"><div class=\"card-header\"><h3 class=\"card-title\">Zusammenfassung</h3></div><div class=\"card-body\"><div class=\"row\"><div class=\"col-12\"><div class=\"mb-3\"><div class=\"text-muted\">Gesamtwert</div><div class=\"h2 mb-0\">€0,00</div></div><div class=\"mb-3\"><div class=\"text-muted\">Gewinn/Verlust</div><div class=\"h3 mb-0 text-success\">€0,00</div></div><div class=\"mb-3\"><div class=\"text-muted\">Rendite</div><div class=\"h3 mb-0 text-success\">0,00%</div></div></div></div></div></div><div class=\"card mt-3\"><div class=\"card-header\"><h3 class=\"card-title\">Letzte Transaktionen</h3></div><div class=\"card-body\"><div class=\"text-center text-muted py-3\">Keine Transaktionen vorhanden</div></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\"><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.01\" min=\"0\" required></div><div class=\"mb-3\"><label for=\"transaction-price\" class=\"form-label\">Preis</label> <input type=\"number\" class=\"form-control\" id=\"transaction-price\" name=\"price\" step=\"0.01\" min=\"0\" required></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>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func Portfolio(authenticated bool, username string, portfolios []model.Portfolio) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var2 == nil {
|
||||
templ_7745c5c3_Var2 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = components.PageLayout(authenticated, username, "Portfolio", PortfolioContent(username), portfolios).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,25 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"portfolio-tracker/internal/model"
|
||||
"portfolio-tracker/internal/web/templates/components"
|
||||
)
|
||||
|
||||
templ DashboardContent(username string) {
|
||||
<div class="container-xl mt-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Willkommen, { username }!</h3>
|
||||
<p class="card-text">Hier ist dein Dashboard.</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="chart"></div>
|
||||
<p class="text-muted">Hier können Sie Ihre Portfolio-Übersicht einsehen.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ Result(authenticated bool, username string, portfolios []model.Portfolio) {
|
||||
@components.PageLayout(authenticated, username, "Dashboard", DashboardContent(username), portfolios)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.2.778
|
||||
package templates
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"portfolio-tracker/internal/model"
|
||||
"portfolio-tracker/internal/web/templates/components"
|
||||
)
|
||||
|
||||
func DashboardContent(username string) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container-xl mt-4\"><div class=\"card\"><div class=\"card-header\"><h3 class=\"card-title\">Willkommen, ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(username)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/start.templ`, Line: 12, Col: 49}
|
||||
}
|
||||
_, 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("!</h3><p class=\"card-text\">Hier ist dein Dashboard.</p></div><div class=\"card-body\"><div id=\"chart\"></div><p class=\"text-muted\">Hier können Sie Ihre Portfolio-Übersicht einsehen.</p></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func Result(authenticated bool, username string, portfolios []model.Portfolio) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var3 == nil {
|
||||
templ_7745c5c3_Var3 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = components.PageLayout(authenticated, username, "Dashboard", DashboardContent(username), portfolios).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,300 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"portfolio-tracker/internal/model"
|
||||
"portfolio-tracker/internal/web/templates/components"
|
||||
)
|
||||
|
||||
templ StockDetailsContent(stock string, data model.YahooChartResponse, portfolios []model.Portfolio) {
|
||||
<div class="page-header d-print-none" aria-label="Page header">
|
||||
<div class="container-fluid">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">{ data.Chart.Result[0].Meta.Symbol }</div>
|
||||
<h2 class="page-title">{ data.Chart.Result[0].Meta.LongName }</h2>
|
||||
</div>
|
||||
<div class="col-auto ms-auto d-print-none">
|
||||
<div class="btn-list">
|
||||
<span class="d-none d-sm-inline">
|
||||
<a href="/portfolio" class="btn btn-outline-primary">Zum Portfolio</a>
|
||||
</span>
|
||||
<button type="button" class="btn btn-primary btn-5 d-none d-sm-inline-block" data-bs-toggle="modal" data-bs-target="#addToPortfolioModal">
|
||||
<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>
|
||||
Zu Portfolio hinzufügen
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary btn-6 d-sm-none btn-icon" data-bs-toggle="modal" data-bs-target="#addToPortfolioModal" aria-label="Zu Portfolio hinzufügen">
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row">
|
||||
<!-- Linke Spalte: Charts -->
|
||||
<div class="col-md-8 col-12">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h1 class="card-title">Kursentwicklung</h1>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h1 class="card-title">Dividenden</h1>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="chart2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Rechte Spalte: Details -->
|
||||
<div class="col-md-4 col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Details</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-unstyled">
|
||||
<li><strong>Name:</strong> { data.Chart.Result[0].Meta.LongName }</li>
|
||||
<li><strong>Symbol:</strong> { data.Chart.Result[0].Meta.Symbol }</li>
|
||||
<li><strong>Währung:</strong> { data.Chart.Result[0].Meta.Currency }</li>
|
||||
<li><strong>Börse:</strong> { data.Chart.Result[0].Meta.Exchange }</li>
|
||||
<li><strong>Zeitzone:</strong> { data.Chart.Result[0].Meta.Timezone }</li>
|
||||
<li><strong>Aktueller Preis:</strong> { data.Chart.Result[0].Meta.RegularMarketPrice }</li>
|
||||
<li><strong>Instrumenttyp:</strong> { data.Chart.Result[0].Meta.Instrument }</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add to Portfolio Modal -->
|
||||
<div class="modal modal-blur fade" id="addToPortfolioModal" tabindex="-1" aria-labelledby="addToPortfolioModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="addToPortfolioModalLabel">{ data.Chart.Result[0].Meta.Symbol } zu Portfolio 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">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="portfolio-select" class="form-label">Portfolio</label>
|
||||
<select class="form-select" id="portfolio-select" name="portfolio_id" required>
|
||||
<option value="">Wählen Sie ein Portfolio</option>
|
||||
for _, portfolio := range portfolios {
|
||||
<option value={ fmt.Sprintf("%d", portfolio.ID) }>
|
||||
{ portfolio.Name } ({ portfolio.BaseCurrency })
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="transaction-type" class="form-label">Transaktionstyp</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" value={ data.Chart.Result[0].Meta.Symbol } readonly/>
|
||||
<div class="form-text">{ data.Chart.Result[0].Meta.LongName }</div>
|
||||
</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 pro Aktie</label>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="transaction-price" name="price" step="0.01" min="0" value={ data.Chart.Result[0].Meta.RegularMarketPrice } required/>
|
||||
<span class="input-group-text">{ data.Chart.Result[0].Meta.Currency }</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" placeholder="z.B. Grund für Kauf/Verkauf"></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">Zu Portfolio hinzufügen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Inline Scripts for Charts -->
|
||||
<script>
|
||||
// Stock symbol for API calls
|
||||
const stockSymbol = {{ data.Chart.Result[0].Meta.Symbol }};
|
||||
|
||||
// Set today's date as default
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('transaction-date').value = today;
|
||||
|
||||
// First Chart: Stock Price
|
||||
fetch('/api/yahoo?stock=' + encodeURIComponent(stockSymbol))
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Stock data received:', data);
|
||||
if (data.chart && data.chart.result && data.chart.result.length > 0) {
|
||||
const result = data.chart.result[0];
|
||||
const timestamps = result.timestamp.map(ts =>
|
||||
new Date(ts * 1000).toISOString().slice(0, 10)
|
||||
);
|
||||
const closes = result.indicators.quote[0].close.map(v =>
|
||||
v !== null ? Math.round(v * 10000) / 10000 : null
|
||||
);
|
||||
|
||||
const options = {
|
||||
chart: {
|
||||
type: 'area',
|
||||
height: 350,
|
||||
toolbar: {
|
||||
show: true
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
series: [{
|
||||
name: 'Kurs',
|
||||
data: closes
|
||||
}],
|
||||
xaxis: {
|
||||
categories: timestamps,
|
||||
type: 'datetime'
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
formatter: function(value) {
|
||||
return value ? value.toFixed(2) : '';
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
x: {
|
||||
format: 'dd MMM yyyy'
|
||||
},
|
||||
y: {
|
||||
formatter: function(value) {
|
||||
return value ? value.toFixed(2) : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const chart = new ApexCharts(document.querySelector("#chart"), options);
|
||||
chart.render();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching stock data:', error);
|
||||
document.querySelector("#chart").innerHTML = '<p>Fehler beim Laden der Kursdaten.</p>';
|
||||
});
|
||||
|
||||
// Second Chart: Dividends
|
||||
fetch('/api/yahoomaxdividends?stock=' + encodeURIComponent(stockSymbol))
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Dividend data received:', data);
|
||||
console.log('Data type:', typeof data);
|
||||
console.log('Data length:', data ? data.length : 'no length');
|
||||
|
||||
if (data && Array.isArray(data) && data.length > 0) {
|
||||
console.log('First dividend entry:', data[0]);
|
||||
|
||||
// Convert the data to the format ApexCharts expects
|
||||
const dividendData = data.map(item => ({
|
||||
x: new Date(item[0]),
|
||||
y: item[1]
|
||||
}));
|
||||
|
||||
console.log('Processed dividend data:', dividendData);
|
||||
|
||||
const options2 = {
|
||||
chart: {
|
||||
type: 'bar',
|
||||
height: 350,
|
||||
toolbar: {
|
||||
show: true
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
formatter: function(val) {
|
||||
return val.toFixed(2);
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
name: 'Dividende',
|
||||
data: dividendData
|
||||
}],
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
title: {
|
||||
text: 'Datum'
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
title: {
|
||||
text: 'Dividende'
|
||||
},
|
||||
labels: {
|
||||
formatter: function(value) {
|
||||
return value ? value.toFixed(2) : '';
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
x: {
|
||||
format: 'dd MMM yyyy'
|
||||
},
|
||||
y: {
|
||||
formatter: function(value) {
|
||||
return value ? value.toFixed(2) : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const chart2 = new ApexCharts(document.querySelector("#chart2"), options2);
|
||||
chart2.render();
|
||||
} else {
|
||||
console.log('No dividend data available or data is empty');
|
||||
document.querySelector("#chart2").innerHTML = '<p>Keine Dividendendaten verfügbar für dieses Wertpapier.</p>';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching dividend data:', error);
|
||||
document.querySelector("#chart2").innerHTML = '<p>Fehler beim Laden der Dividendendaten.</p>';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
||||
templ StockDetails(authenticated bool, username string, stock string, data model.YahooChartResponse, portfolios []model.Portfolio) {
|
||||
@components.PageLayout(authenticated, username, data.Chart.Result[0].Meta.LongName, StockDetailsContent(stock, data, portfolios), portfolios)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user