first commit

This commit is contained in:
Matthias Hinrichs
2025-08-26 03:17:49 +02:00
commit 189e7a2329
34 changed files with 8835 additions and 0 deletions
+265
View File
@@ -0,0 +1,265 @@
package views
import (
"whereismymoney/internal/models"
"fmt"
)
templ Accounts(userName string, bankAccounts []models.BankAccount, depots []models.Depot) {
@Layout("Konten & Depots - WhereIsMyMoney") {
@Navigation(userName)
<!-- Main Content -->
<div class="min-h-screen bg-gray-50">
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<!-- Page Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">Konten & Depots</h1>
<p class="mt-2 text-gray-600">Verwalte deine Bankkonten und Wertpapierdepots</p>
</div>
<!-- Action Buttons -->
<div class="mb-6 flex flex-wrap gap-4">
<button
onclick="showModal('bankAccountModal')"
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
Bankkonto hinzufügen
</button>
<button
onclick="showModal('depotModal')"
class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
Depot hinzufügen
</button>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Bankkonten Section -->
<div>
<h2 class="text-xl font-semibold text-gray-900 mb-4">Bankkonten</h2>
if len(bankAccounts) == 0 {
<div class="bg-white rounded-lg shadow p-6 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/>
</svg>
<h3 class="mt-4 text-lg font-medium text-gray-900">Keine Bankkonten</h3>
<p class="mt-2 text-gray-500">Füge dein erstes Bankkonto hinzu, um zu beginnen.</p>
</div>
} else {
<div class="space-y-4">
for _, account := range bankAccounts {
<div class="bg-white rounded-lg shadow p-6">
<div class="flex justify-between items-start">
<div>
<h3 class="text-lg font-medium text-gray-900">{ account.Name }</h3>
<p class="text-sm text-gray-500">{ account.Bank }</p>
<p class="text-sm text-gray-500 capitalize">{ account.AccountType }</p>
if account.IBAN != "" {
<p class="text-sm text-gray-400 mt-1">{ account.IBAN }</p>
}
</div>
<div class="text-right">
<p class="text-2xl font-bold text-gray-900">
{ fmt.Sprintf("%.2f", account.Balance) } €
</p>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
Aktiv
</span>
</div>
</div>
<div class="mt-4 flex justify-end space-x-2">
<button class="text-blue-600 hover:text-blue-800 text-sm font-medium">
Bearbeiten
</button>
<button class="text-red-600 hover:text-red-800 text-sm font-medium">
Löschen
</button>
</div>
</div>
}
</div>
}
</div>
<!-- Depots Section -->
<div>
<h2 class="text-xl font-semibold text-gray-900 mb-4">Depots</h2>
if len(depots) == 0 {
<div class="bg-white rounded-lg shadow p-6 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
<h3 class="mt-4 text-lg font-medium text-gray-900">Keine Depots</h3>
<p class="mt-2 text-gray-500">Füge dein erstes Depot hinzu, um zu beginnen.</p>
</div>
} else {
<div class="space-y-4">
for _, depot := range depots {
<div class="bg-white rounded-lg shadow p-6">
<div class="flex justify-between items-start">
<div>
<h3 class="text-lg font-medium text-gray-900">{ depot.Name }</h3>
<p class="text-sm text-gray-500">{ depot.Broker }</p>
if depot.DepotNumber != "" {
<p class="text-sm text-gray-400 mt-1">Depot: { depot.DepotNumber }</p>
}
</div>
<div class="text-right">
<p class="text-2xl font-bold text-gray-900">
{ fmt.Sprintf("%.2f", depot.TotalValue) } €
</p>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
Aktiv
</span>
</div>
</div>
<div class="mt-4 flex justify-end space-x-2">
<button class="text-blue-600 hover:text-blue-800 text-sm font-medium">
Bearbeiten
</button>
<button class="text-red-600 hover:text-red-800 text-sm font-medium">
Löschen
</button>
</div>
</div>
}
</div>
}
</div>
</div>
</div>
</div>
</div>
<!-- Bank Account Modal -->
<div id="bankAccountModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden z-50">
<div class="flex items-center justify-center min-h-screen px-4">
<div class="bg-white rounded-lg shadow-xl max-w-md w-full">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">Bankkonto hinzufügen</h3>
</div>
<form action="/accounts/bank" method="POST" class="px-6 py-4">
<div class="space-y-4">
<div>
<label for="bank-name" class="block text-sm font-medium text-gray-700">Kontoname</label>
<input type="text" id="bank-name" name="name" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
placeholder="z.B. Hauptkonto">
</div>
<div>
<label for="bank-bank-name" class="block text-sm font-medium text-gray-700">Bankname</label>
<input type="text" id="bank-bank-name" name="bank_name" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
placeholder="z.B. Sparkasse">
</div>
<div>
<label for="bank-account-type" class="block text-sm font-medium text-gray-700">Kontotyp</label>
<select id="bank-account-type" name="account_type" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
<option value="">Wähle einen Typ</option>
<option value="checking">Girokonto</option>
<option value="savings">Sparkonto</option>
<option value="credit">Kreditkonto</option>
</select>
</div>
<div>
<label for="bank-iban" class="block text-sm font-medium text-gray-700">IBAN (optional)</label>
<input type="text" id="bank-iban" name="iban"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
placeholder="DE89 3704 0044 0532 0130 00">
</div>
<div>
<label for="bank-balance" class="block text-sm font-medium text-gray-700">Aktueller Kontostand</label>
<input type="number" id="bank-balance" name="balance" step="0.01"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
placeholder="0.00">
</div>
</div>
<div class="mt-6 flex justify-end space-x-3">
<button type="button" onclick="hideModal('bankAccountModal')"
class="bg-gray-300 hover:bg-gray-400 text-gray-800 px-4 py-2 rounded-md">
Abbrechen
</button>
<button type="submit"
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md">
Hinzufügen
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Depot Modal -->
<div id="depotModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden z-50">
<div class="flex items-center justify-center min-h-screen px-4">
<div class="bg-white rounded-lg shadow-xl max-w-md w-full">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">Depot hinzufügen</h3>
</div>
<form action="/accounts/depot" method="POST" class="px-6 py-4">
<div class="space-y-4">
<div>
<label for="depot-name" class="block text-sm font-medium text-gray-700">Depotname</label>
<input type="text" id="depot-name" name="name" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
placeholder="z.B. Hauptdepot">
</div>
<div>
<label for="depot-broker-name" class="block text-sm font-medium text-gray-700">Broker</label>
<input type="text" id="depot-broker-name" name="broker_name" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
placeholder="z.B. Trade Republic">
</div>
<div>
<label for="depot-number" class="block text-sm font-medium text-gray-700">Depotnummer (optional)</label>
<input type="text" id="depot-number" name="depot_number"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
placeholder="123456789">
</div>
<div>
<label for="depot-value" class="block text-sm font-medium text-gray-700">Aktueller Gesamtwert</label>
<input type="number" id="depot-value" name="total_value" step="0.01"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
placeholder="0.00">
</div>
</div>
<div class="mt-6 flex justify-end space-x-3">
<button type="button" onclick="hideModal('depotModal')"
class="bg-gray-300 hover:bg-gray-400 text-gray-800 px-4 py-2 rounded-md">
Abbrechen
</button>
<button type="submit"
class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md">
Hinzufügen
</button>
</div>
</form>
</div>
</div>
</div>
<script>
function showModal(modalId) {
document.getElementById(modalId).classList.remove('hidden');
}
function hideModal(modalId) {
document.getElementById(modalId).classList.add('hidden');
}
// Close modal when clicking outside
document.addEventListener('click', function(event) {
if (event.target.classList.contains('bg-opacity-50')) {
event.target.classList.add('hidden');
}
});
</script>
}
}
File diff suppressed because one or more lines are too long
+138
View File
@@ -0,0 +1,138 @@
package views
import (
"fmt"
"whereismymoney/internal/models"
)
templ Dashboard(data *models.DashboardData) {
@Layout("WhereIsMyMoney") {
@Navigation(data.UserName)
<!-- Main Content -->
<div class="min-h-screen bg-gray-50">
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<!-- Welcome Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">Dashboard</h1>
<p class="mt-2 text-gray-600">Willkommen zurück, { data.UserName }!</p>
</div>
<!-- Vermögensübersicht -->
<div class="mb-8 grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-medium text-gray-900 mb-2">Depots</h3>
<p class="text-3xl font-bold text-blue-600">€{ fmt.Sprintf("%.2f", data.AssetOverview.TotalDepotValue) }</p>
<p class="text-sm text-gray-500">{ fmt.Sprintf("%d Depots", len(data.AssetOverview.Depots)) }</p>
</div>
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-medium text-gray-900 mb-2">Gesamtvermögen</h3>
<p class="text-3xl font-bold text-purple-600">€{ fmt.Sprintf("%.2f", data.AssetOverview.TotalAssets) }</p>
<p class="text-sm text-gray-500">Bank + Depot</p>
</div>
</div>
<!-- Charts und Statistiken -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<!-- Monatliche Entwicklung -->
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Monatliche Entwicklung</h3>
<div class="h-64" id="monthly-chart">
<canvas id="monthlyChart"></canvas>
</div>
</div>
<!-- Einnahmen vs Ausgaben Vergleich -->
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Einnahmen vs Ausgaben</h3>
<div class="h-64">
<canvas id="incomeExpenseChart"></canvas>
</div>
</div>
</div>
<!-- Verlaufs-Chart -->
<div class="bg-white rounded-lg shadow p-6 mb-8">
<h3 class="text-lg font-medium text-gray-900 mb-4">12-Monats Vorausschau</h3>
<div class="h-80">
<canvas id="trendChart"></canvas>
</div>
</div>
<!-- Detaillierte Übersichten -->
<div class="grid grid-cols-1 gap-8 mb-8">
<!-- Depot-Details -->
if len(data.AssetOverview.Depots) > 0 {
<div class="bg-white rounded-lg shadow">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">Depots</h3>
</div>
<div class="divide-y divide-gray-200">
for _, depot := range data.AssetOverview.Depots {
<div class="px-6 py-4">
<div class="flex justify-between items-center">
<div>
<p class="text-sm font-medium text-gray-900">{ depot.Name }</p>
<p class="text-sm text-gray-500">{ depot.Broker }</p>
</div>
<p class="text-lg font-semibold text-gray-900">€{ fmt.Sprintf("%.2f", depot.TotalValue) }</p>
</div>
</div>
}
</div>
</div>
}
</div>
<!-- Aktuelle Transaktionen -->
if len(data.RecentTransactions) > 0 {
<div class="bg-white rounded-lg shadow">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">Aktuelle Transaktionen</h3>
</div>
<div class="divide-y divide-gray-200">
for _, transaction := range data.RecentTransactions {
<div class="px-6 py-4">
<div class="flex justify-between items-center">
<div>
<p class="text-sm font-medium text-gray-900">{ transaction.Description }</p>
<div class="flex items-center text-sm text-gray-500 space-x-2">
<span>{ transaction.Date.Format("02.01.2006") }</span>
if transaction.Category != nil {
<span>•</span>
<span>{ transaction.Category.Name }</span>
}
if transaction.BankAccount != nil {
<span>•</span>
<span>{ transaction.BankAccount.Name }</span>
}
</div>
</div>
<div class="text-right">
if transaction.Type == "income" {
<p class="text-lg font-semibold text-green-600">+€{ fmt.Sprintf("%.2f", transaction.Amount) }</p>
} else {
<p class="text-lg font-semibold text-red-600">-€{ fmt.Sprintf("%.2f", transaction.Amount) }</p>
}
<p class="text-xs text-gray-400">{ transaction.Type }</p>
</div>
</div>
</div>
}
</div>
<div class="px-6 py-3 bg-gray-50 text-center">
<a href="/transactions" class="text-sm text-blue-600 hover:text-blue-500">Alle Transaktionen anzeigen →</a>
</div>
</div>
}
<!-- JavaScript für Charts -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="/static/js/dashboard-charts.js"></script>
</div>
</div>
</div>
}
}
+320
View File
@@ -0,0 +1,320 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.778
package views
//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"
"whereismymoney/internal/models"
)
func Dashboard(data *models.DashboardData) 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_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
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_Err = Navigation(data.UserName).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <!-- Main Content --> <div class=\"min-h-screen bg-gray-50\"><div class=\"max-w-7xl mx-auto py-6 sm:px-6 lg:px-8\"><div class=\"px-4 py-6 sm:px-0\"><!-- Welcome Header --><div class=\"mb-8\"><h1 class=\"text-3xl font-bold text-gray-900\">Dashboard</h1><p class=\"mt-2 text-gray-600\">Willkommen zurück, ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.UserName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 19, Col: 71}
}
_, 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("!</p></div><!-- Vermögensübersicht --><div class=\"mb-8 grid grid-cols-1 md:grid-cols-2 gap-6\"><div class=\"bg-white rounded-lg shadow p-6\"><h3 class=\"text-lg font-medium text-gray-900 mb-2\">Depots</h3><p class=\"text-3xl font-bold text-blue-600\">€")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2f", data.AssetOverview.TotalDepotValue))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 26, Col: 111}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p class=\"text-sm text-gray-500\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d Depots", len(data.AssetOverview.Depots)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 27, Col: 98}
}
_, 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("</p></div><div class=\"bg-white rounded-lg shadow p-6\"><h3 class=\"text-lg font-medium text-gray-900 mb-2\">Gesamtvermögen</h3><p class=\"text-3xl font-bold text-purple-600\">€")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2f", data.AssetOverview.TotalAssets))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 32, Col: 109}
}
_, 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("</p><p class=\"text-sm text-gray-500\">Bank + Depot</p></div></div><!-- Charts und Statistiken --><div class=\"grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8\"><!-- Monatliche Entwicklung --><div class=\"bg-white rounded-lg shadow p-6\"><h3 class=\"text-lg font-medium text-gray-900 mb-4\">Monatliche Entwicklung</h3><div class=\"h-64\" id=\"monthly-chart\"><canvas id=\"monthlyChart\"></canvas></div></div><!-- Einnahmen vs Ausgaben Vergleich --><div class=\"bg-white rounded-lg shadow p-6\"><h3 class=\"text-lg font-medium text-gray-900 mb-4\">Einnahmen vs Ausgaben</h3><div class=\"h-64\"><canvas id=\"incomeExpenseChart\"></canvas></div></div></div><!-- Verlaufs-Chart --><div class=\"bg-white rounded-lg shadow p-6 mb-8\"><h3 class=\"text-lg font-medium text-gray-900 mb-4\">12-Monats Vorausschau</h3><div class=\"h-80\"><canvas id=\"trendChart\"></canvas></div></div><!-- Detaillierte Übersichten --><div class=\"grid grid-cols-1 gap-8 mb-8\"><!-- Depot-Details -->")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(data.AssetOverview.Depots) > 0 {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"bg-white rounded-lg shadow\"><div class=\"px-6 py-4 border-b border-gray-200\"><h3 class=\"text-lg font-medium text-gray-900\">Depots</h3></div><div class=\"divide-y divide-gray-200\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, depot := range data.AssetOverview.Depots {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"px-6 py-4\"><div class=\"flex justify-between items-center\"><div><p class=\"text-sm font-medium text-gray-900\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(depot.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 77, Col: 70}
}
_, 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("</p><p class=\"text-sm text-gray-500\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(depot.Broker)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 78, Col: 60}
}
_, 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("</p></div><p class=\"text-lg font-semibold text-gray-900\">€")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2f", depot.TotalValue))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 80, Col: 101}
}
_, 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("</p></div></div>")
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 = templ_7745c5c3_Buffer.WriteString("</div><!-- Aktuelle Transaktionen -->")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(data.RecentTransactions) > 0 {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"bg-white rounded-lg shadow\"><div class=\"px-6 py-4 border-b border-gray-200\"><h3 class=\"text-lg font-medium text-gray-900\">Aktuelle Transaktionen</h3></div><div class=\"divide-y divide-gray-200\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, transaction := range data.RecentTransactions {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"px-6 py-4\"><div class=\"flex justify-between items-center\"><div><p class=\"text-sm font-medium text-gray-900\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(transaction.Description)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 100, Col: 82}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><div class=\"flex items-center text-sm text-gray-500 space-x-2\"><span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(transaction.Date.Format("02.01.2006"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 102, Col: 58}
}
_, 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("</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if transaction.Category != nil {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<span>•</span> <span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(transaction.Category.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 105, Col: 47}
}
_, 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("</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if transaction.BankAccount != nil {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<span>•</span> <span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(transaction.BankAccount.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 109, Col: 50}
}
_, 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("</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div><div class=\"text-right\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if transaction.Type == "income" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<p class=\"text-lg font-semibold text-green-600\">+€")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2f", transaction.Amount))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 115, Col: 106}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<p class=\"text-lg font-semibold text-red-600\">-€")
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", transaction.Amount))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 117, Col: 104}
}
_, 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("</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<p class=\"text-xs text-gray-400\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(transaction.Type)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 119, Col: 63}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><div class=\"px-6 py-3 bg-gray-50 text-center\"><a href=\"/transactions\" class=\"text-sm text-blue-600 hover:text-blue-500\">Alle Transaktionen anzeigen →</a></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!-- JavaScript für Charts --><script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script><script src=\"/static/js/dashboard-charts.js\"></script></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = Layout("WhereIsMyMoney").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate
+16
View File
@@ -0,0 +1,16 @@
package views
templ Layout(title string) {
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{ title }</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-50">
{ children... }
</body>
</html>
}
+61
View File
@@ -0,0 +1,61 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.778
package views
//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 Layout(title 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("<!doctype html><html lang=\"de\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><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/views/layout.templ`, Line: 9, Col: 16}
}
_, 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><script src=\"https://cdn.tailwindcss.com\"></script></head><body class=\"bg-gray-50\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate
+66
View File
@@ -0,0 +1,66 @@
package views
templ LoginPage(errorMsg string) {
@Layout("Anmelden") {
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-md w-full space-y-8">
<div>
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
Bei WhereIsMyMoney anmelden
</h2>
<p class="mt-2 text-center text-sm text-gray-600">
Oder
<a href="/register" class="font-medium text-indigo-600 hover:text-indigo-500">
neues Konto erstellen
</a>
</p>
</div>
if errorMsg != "" {
<div class="rounded-md bg-red-50 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">
{ errorMsg }
</h3>
</div>
</div>
</div>
}
<form class="mt-8 space-y-6" action="/login" method="POST">
<div class="rounded-md shadow-sm -space-y-px">
<div>
<label for="email" class="sr-only">E-Mail-Adresse</label>
<input id="email" name="email" type="email" required class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" placeholder="E-Mail-Adresse"/>
</div>
<div>
<label for="password" class="sr-only">Passwort</label>
<input id="password" name="password" type="password" required class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" placeholder="Passwort"/>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
<input id="remember_me" name="remember_me" type="checkbox" class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"/>
<label for="remember_me" class="ml-2 block text-sm text-gray-900">
Angemeldet bleiben
</label>
</div>
</div>
<div>
<button type="submit" class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Anmelden
</button>
</div>
</form>
</div>
</div>
}
}
+81
View File
@@ -0,0 +1,81 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.778
package views
//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 LoginPage(errorMsg 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_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
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_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8\"><div class=\"max-w-md w-full space-y-8\"><div><h2 class=\"mt-6 text-center text-3xl font-extrabold text-gray-900\">Bei WhereIsMyMoney anmelden</h2><p class=\"mt-2 text-center text-sm text-gray-600\">Oder <a href=\"/register\" class=\"font-medium text-indigo-600 hover:text-indigo-500\">neues Konto erstellen</a></p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if errorMsg != "" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"rounded-md bg-red-50 p-4\"><div class=\"flex\"><div class=\"flex-shrink-0\"><svg class=\"h-5 w-5 text-red-400\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path fill-rule=\"evenodd\" d=\"M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z\" clip-rule=\"evenodd\"></path></svg></div><div class=\"ml-3\"><h3 class=\"text-sm font-medium text-red-800\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(errorMsg)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/login.templ`, Line: 29, Col: 19}
}
_, 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("</h3></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<form class=\"mt-8 space-y-6\" action=\"/login\" method=\"POST\"><div class=\"rounded-md shadow-sm -space-y-px\"><div><label for=\"email\" class=\"sr-only\">E-Mail-Adresse</label> <input id=\"email\" name=\"email\" type=\"email\" required class=\"appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm\" placeholder=\"E-Mail-Adresse\"></div><div><label for=\"password\" class=\"sr-only\">Passwort</label> <input id=\"password\" name=\"password\" type=\"password\" required class=\"appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm\" placeholder=\"Passwort\"></div></div><div class=\"flex items-center justify-between\"><div class=\"flex items-center\"><input id=\"remember_me\" name=\"remember_me\" type=\"checkbox\" class=\"h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded\"> <label for=\"remember_me\" class=\"ml-2 block text-sm text-gray-900\">Angemeldet bleiben</label></div></div><div><button type=\"submit\" class=\"group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500\">Anmelden</button></div></form></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = Layout("Anmelden").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate
+129
View File
@@ -0,0 +1,129 @@
package views
templ Navigation(userName string) {
<!-- Header Navigation -->
<nav class="bg-white shadow-lg">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
<div class="flex-shrink-0">
<h1 class="text-xl font-bold text-indigo-600">WhereIsMyMoney</h1>
</div>
<div class="hidden md:block">
<div class="ml-10 flex items-baseline space-x-4">
<a href="/" class="text-gray-900 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Dashboard</a>
<a href="/accounts" class="text-gray-500 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Konten & Depots</a>
<a href="/transactions" class="text-gray-500 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Transaktionen</a>
<a href="/recurring" class="text-gray-500 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Wiederkehrend</a>
<a href="/categories" class="text-gray-500 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Kategorien</a>
<a href="/reports" class="text-gray-500 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Berichte</a>
</div>
</div>
</div>
<div class="flex items-center">
<div class="ml-4 flex items-center md:ml-6">
<!-- User dropdown -->
<div class="relative">
<button type="button" class="bg-white rounded-full flex text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 p-1" onclick="toggleUserMenu()" aria-expanded="false" aria-haspopup="true">
<span class="sr-only">Open user menu</span>
<div class="h-8 w-8 rounded-full bg-indigo-500 flex items-center justify-center">
<span class="text-sm font-medium text-white">{ userName[0:1] }</span>
</div>
</button>
<!-- Dropdown menu -->
<div id="user-menu" class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" style="display: none;">
<div class="px-4 py-2 text-sm text-gray-700 border-b border-gray-100">
<div class="font-medium">{ userName }</div>
<div class="text-gray-500 text-xs">Angemeldet</div>
</div>
<a href="/profile" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem">
<div class="flex items-center">
<svg class="mr-3 h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
Profil
</div>
</a>
<a href="/settings" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem">
<div class="flex items-center">
<svg class="mr-3 h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Einstellungen
</div>
</a>
<div class="border-t border-gray-100">
<form action="/logout" method="POST" class="block">
<button type="submit" class="w-full text-left px-4 py-2 text-sm text-red-700 hover:bg-red-50 flex items-center" role="menuitem">
<svg class="mr-3 h-4 w-4 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
Abmelden
</button>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Mobile menu button -->
<div class="md:hidden flex items-center">
<button class="bg-gray-50 p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" onclick="toggleMobileMenu()">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div>
</div>
<!-- Mobile menu -->
<div class="md:hidden" id="mobile-menu" style="display: none;">
<div class="px-2 pt-2 pb-3 space-y-1 sm:px-3">
<a href="/" class="text-gray-900 hover:bg-gray-50 block px-3 py-2 rounded-md text-base font-medium">Dashboard</a>
<a href="/accounts" class="text-gray-500 hover:bg-gray-50 hover:text-gray-900 block px-3 py-2 rounded-md text-base font-medium">Konten & Depots</a>
<a href="/transactions" class="text-gray-500 hover:bg-gray-50 hover:text-gray-900 block px-3 py-2 rounded-md text-base font-medium">Transaktionen</a>
<a href="/categories" class="text-gray-500 hover:bg-gray-50 hover:text-gray-900 block px-3 py-2 rounded-md text-base font-medium">Kategorien</a>
<a href="/reports" class="text-gray-500 hover:bg-gray-50 hover:text-gray-900 block px-3 py-2 rounded-md text-base font-medium">Berichte</a>
</div>
<div class="pt-4 pb-3 border-t border-gray-200">
<div class="flex items-center px-5">
<div class="flex-shrink-0">
<span class="text-gray-800 text-sm font-medium">{ userName }</span>
</div>
<div class="ml-auto">
<form action="/logout" method="POST" class="inline">
<button type="submit" class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md text-sm font-medium">
Abmelden
</button>
</form>
</div>
</div>
</div>
</div>
</nav>
<script>
function toggleMobileMenu() {
const menu = document.getElementById('mobile-menu');
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
}
function toggleUserMenu() {
const menu = document.getElementById('user-menu');
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
}
// Close dropdown when clicking outside
document.addEventListener('click', function(event) {
const userButton = event.target.closest('[onclick="toggleUserMenu()"]');
const userMenu = document.getElementById('user-menu');
if (!userButton && userMenu && userMenu.style.display === 'block') {
userMenu.style.display = 'none';
}
});
</script>
}
+79
View File
@@ -0,0 +1,79 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.778
package views
//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 Navigation(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("<!-- Header Navigation --><nav class=\"bg-white shadow-lg\"><div class=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\"><div class=\"flex justify-between h-16\"><div class=\"flex items-center\"><div class=\"flex-shrink-0\"><h1 class=\"text-xl font-bold text-indigo-600\">WhereIsMyMoney</h1></div><div class=\"hidden md:block\"><div class=\"ml-10 flex items-baseline space-x-4\"><a href=\"/\" class=\"text-gray-900 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium\">Dashboard</a> <a href=\"/accounts\" class=\"text-gray-500 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium\">Konten & Depots</a> <a href=\"/transactions\" class=\"text-gray-500 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium\">Transaktionen</a> <a href=\"/recurring\" class=\"text-gray-500 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium\">Wiederkehrend</a> <a href=\"/categories\" class=\"text-gray-500 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium\">Kategorien</a> <a href=\"/reports\" class=\"text-gray-500 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium\">Berichte</a></div></div></div><div class=\"flex items-center\"><div class=\"ml-4 flex items-center md:ml-6\"><!-- User dropdown --><div class=\"relative\"><button type=\"button\" class=\"bg-white rounded-full flex text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 p-1\" onclick=\"toggleUserMenu()\" aria-expanded=\"false\" aria-haspopup=\"true\"><span class=\"sr-only\">Open user menu</span><div class=\"h-8 w-8 rounded-full bg-indigo-500 flex items-center justify-center\"><span class=\"text-sm font-medium text-white\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(userName[0:1])
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/navigation.templ`, Line: 30, Col: 69}
}
_, 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("</span></div></button><!-- Dropdown menu --><div id=\"user-menu\" class=\"origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50\" role=\"menu\" aria-orientation=\"vertical\" aria-labelledby=\"user-menu-button\" style=\"display: none;\"><div class=\"px-4 py-2 text-sm text-gray-700 border-b border-gray-100\"><div class=\"font-medium\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(userName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/navigation.templ`, Line: 37, Col: 44}
}
_, 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("</div><div class=\"text-gray-500 text-xs\">Angemeldet</div></div><a href=\"/profile\" class=\"block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100\" role=\"menuitem\"><div class=\"flex items-center\"><svg class=\"mr-3 h-4 w-4 text-gray-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z\"></path></svg> Profil</div></a> <a href=\"/settings\" class=\"block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100\" role=\"menuitem\"><div class=\"flex items-center\"><svg class=\"mr-3 h-4 w-4 text-gray-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z\"></path> <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 12a3 3 0 11-6 0 3 3 0 016 0z\"></path></svg> Einstellungen</div></a><div class=\"border-t border-gray-100\"><form action=\"/logout\" method=\"POST\" class=\"block\"><button type=\"submit\" class=\"w-full text-left px-4 py-2 text-sm text-red-700 hover:bg-red-50 flex items-center\" role=\"menuitem\"><svg class=\"mr-3 h-4 w-4 text-red-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1\"></path></svg> Abmelden</button></form></div></div></div></div></div><!-- Mobile menu button --><div class=\"md:hidden flex items-center\"><button class=\"bg-gray-50 p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500\" onclick=\"toggleMobileMenu()\"><svg class=\"h-6 w-6\" stroke=\"currentColor\" fill=\"none\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 6h16M4 12h16M4 18h16\"></path></svg></button></div></div></div><!-- Mobile menu --><div class=\"md:hidden\" id=\"mobile-menu\" style=\"display: none;\"><div class=\"px-2 pt-2 pb-3 space-y-1 sm:px-3\"><a href=\"/\" class=\"text-gray-900 hover:bg-gray-50 block px-3 py-2 rounded-md text-base font-medium\">Dashboard</a> <a href=\"/accounts\" class=\"text-gray-500 hover:bg-gray-50 hover:text-gray-900 block px-3 py-2 rounded-md text-base font-medium\">Konten & Depots</a> <a href=\"/transactions\" class=\"text-gray-500 hover:bg-gray-50 hover:text-gray-900 block px-3 py-2 rounded-md text-base font-medium\">Transaktionen</a> <a href=\"/categories\" class=\"text-gray-500 hover:bg-gray-50 hover:text-gray-900 block px-3 py-2 rounded-md text-base font-medium\">Kategorien</a> <a href=\"/reports\" class=\"text-gray-500 hover:bg-gray-50 hover:text-gray-900 block px-3 py-2 rounded-md text-base font-medium\">Berichte</a></div><div class=\"pt-4 pb-3 border-t border-gray-200\"><div class=\"flex items-center px-5\"><div class=\"flex-shrink-0\"><span class=\"text-gray-800 text-sm font-medium\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(userName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/navigation.templ`, Line: 94, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span></div><div class=\"ml-auto\"><form action=\"/logout\" method=\"POST\" class=\"inline\"><button type=\"submit\" class=\"bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md text-sm font-medium\">Abmelden</button></form></div></div></div></div></nav><script>\n\t\tfunction toggleMobileMenu() {\n\t\t\tconst menu = document.getElementById('mobile-menu');\n\t\t\tmenu.style.display = menu.style.display === 'none' ? 'block' : 'none';\n\t\t}\n\n\t\tfunction toggleUserMenu() {\n\t\t\tconst menu = document.getElementById('user-menu');\n\t\t\tmenu.style.display = menu.style.display === 'none' ? 'block' : 'none';\n\t\t}\n\n\t\t// Close dropdown when clicking outside\n\t\tdocument.addEventListener('click', function(event) {\n\t\t\tconst userButton = event.target.closest('[onclick=\"toggleUserMenu()\"]');\n\t\t\tconst userMenu = document.getElementById('user-menu');\n\t\t\t\n\t\t\tif (!userButton && userMenu && userMenu.style.display === 'block') {\n\t\t\t\tuserMenu.style.display = 'none';\n\t\t\t}\n\t\t});\n\t</script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate
+324
View File
@@ -0,0 +1,324 @@
package views
import (
"fmt"
"whereismymoney/internal/models"
)
templ RecurringTransactions(userName string, rules []models.RecurrenceRule) {
@Layout("Wiederkehrende Transaktionen - WhereIsMyMoney") {
@Navigation(userName)
<!-- Main Content -->
<div class="min-h-screen bg-gray-50">
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<!-- Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">Wiederkehrende Transaktionen</h1>
<p class="mt-2 text-gray-600">Verwalten Sie Ihre regelmäßigen Ein- und Ausgaben</p>
</div>
<!-- Regeln Liste -->
<div class="bg-white rounded-lg shadow">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">Ihre wiederkehrenden Transaktionen</h3>
</div>
if len(rules) == 0 {
<div class="px-6 py-8 text-center">
<p class="text-gray-500">Noch keine wiederkehrenden Transaktionen vorhanden.</p>
</div>
} else {
<div class="divide-y divide-gray-200">
for _, rule := range rules {
<div class="px-6 py-4" data-rule-id={ fmt.Sprintf("%d", rule.ID) }>
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="flex items-center space-x-3">
<h4 class="text-lg font-medium text-gray-900">{ rule.Description }</h4>
if rule.IsActive {
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
Aktiv
</span>
} else {
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
Inaktiv
</span>
}
</div>
<div class="mt-2 flex items-center space-x-6 text-sm text-gray-500">
<div class="flex items-center">
<span class="font-medium">Betrag:</span>
if rule.Type == "income" {
<span class="ml-1 text-green-600 font-semibold">+€{ fmt.Sprintf("%.2f", rule.Amount) }</span>
} else {
<span class="ml-1 text-red-600 font-semibold">-€{ fmt.Sprintf("%.2f", rule.Amount) }</span>
}
</div>
<div class="flex items-center">
<span class="font-medium">Intervall:</span>
<span class="ml-1">
switch rule.Interval {
case "daily":
if rule.IntervalCount == 1 {
Täglich
} else {
Alle { fmt.Sprintf("%d", rule.IntervalCount) } Tage
}
case "weekly":
if rule.IntervalCount == 1 {
Wöchentlich
} else {
Alle { fmt.Sprintf("%d", rule.IntervalCount) } Wochen
}
case "monthly":
if rule.IntervalCount == 1 {
Monatlich
} else {
Alle { fmt.Sprintf("%d", rule.IntervalCount) } Monate
}
case "yearly":
if rule.IntervalCount == 1 {
Jährlich
} else {
Alle { fmt.Sprintf("%d", rule.IntervalCount) } Jahre
}
default:
{ rule.Interval }
}
</span>
</div>
if rule.Category != nil {
<div class="flex items-center">
<span class="font-medium">Kategorie:</span>
<span class="ml-1">{ rule.Category.Name }</span>
</div>
}
<div class="flex items-center">
<span class="font-medium">Start:</span>
<span class="ml-1">{ rule.StartDate.Format("02.01.2006") }</span>
</div>
if rule.EndDate != nil {
<div class="flex items-center">
<span class="font-medium">Ende:</span>
<span class="ml-1">{ rule.EndDate.Format("02.01.2006") }</span>
</div>
}
</div>
</div>
<div class="flex items-center space-x-2 ml-4">
<button
class="edit-rule-btn inline-flex items-center px-3 py-1 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
data-rule-id={ fmt.Sprintf("%d", rule.ID) }
data-description={ rule.Description }
data-amount={ fmt.Sprintf("%.2f", rule.Amount) }
data-type={ rule.Type }
data-interval={ rule.Interval }
data-start-date={ rule.StartDate.Format("2006-01-02") }
if rule.EndDate != nil {
data-end-date={ rule.EndDate.Format("2006-01-02") }
}
>
Bearbeiten
</button>
<button
class="toggle-rule-btn inline-flex items-center px-3 py-1 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
data-rule-id={ fmt.Sprintf("%d", rule.ID) }
data-active={ fmt.Sprintf("%t", rule.IsActive) }
>
if rule.IsActive {
Deaktivieren
} else {
Aktivieren
}
</button>
<button
class="delete-rule-btn inline-flex items-center px-3 py-1 border border-red-300 shadow-sm text-sm font-medium rounded-md text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
data-rule-id={ fmt.Sprintf("%d", rule.ID) }
>
Löschen
</button>
</div>
</div>
</div>
}
</div>
}
</div>
<!-- Edit Modal (wird per JavaScript eingeblendet) -->
<div id="editModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden">
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<div class="mt-3">
<h3 class="text-lg font-medium text-gray-900 mb-4">Wiederkehrende Transaktion bearbeiten</h3>
<form id="editForm">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">Beschreibung</label>
<input type="text" id="editDescription" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Betrag (€)</label>
<input type="number" step="0.01" id="editAmount" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Typ</label>
<select id="editType" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
<option value="income">Einnahme</option>
<option value="expense">Ausgabe</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Intervall</label>
<select id="editInterval" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
<option value="daily">Täglich</option>
<option value="weekly">Wöchentlich</option>
<option value="monthly">Monatlich</option>
<option value="yearly">Jährlich</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Startdatum</label>
<input type="date" id="editStartDate" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Enddatum (optional)</label>
<input type="date" id="editEndDate" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
<p class="mt-1 text-sm text-gray-500">Leer lassen für unbegrenzte Laufzeit</p>
</div>
</div>
<div class="flex justify-end space-x-3 mt-6">
<button type="button" id="cancelEdit" class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-300 rounded-md hover:bg-gray-400">
Abbrechen
</button>
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
Speichern
</button>
</div>
</form>
</div>
</div>
</div>
<!-- JavaScript für Interaktionen -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Edit Modal Funktionalität
const modal = document.getElementById('editModal');
const editForm = document.getElementById('editForm');
const cancelBtn = document.getElementById('cancelEdit');
let currentRuleId = null;
// Edit Button Event Listener
document.querySelectorAll('.edit-rule-btn').forEach(btn => {
btn.addEventListener('click', function() {
currentRuleId = this.dataset.ruleId;
// Formular mit aktuellen Werten füllen
document.getElementById('editDescription').value = this.dataset.description;
document.getElementById('editAmount').value = this.dataset.amount;
document.getElementById('editType').value = this.dataset.type;
document.getElementById('editInterval').value = this.dataset.interval;
document.getElementById('editStartDate').value = this.dataset.startDate;
document.getElementById('editEndDate').value = this.dataset.endDate || '';
modal.classList.remove('hidden');
});
});
// Cancel Button
cancelBtn.addEventListener('click', function() {
modal.classList.add('hidden');
});
// Form Submit
editForm.addEventListener('submit', function(e) {
e.preventDefault();
const formData = {
description: document.getElementById('editDescription').value,
amount: parseFloat(document.getElementById('editAmount').value),
type: document.getElementById('editType').value,
interval: document.getElementById('editInterval').value,
start_date: document.getElementById('editStartDate').value,
end_date: document.getElementById('editEndDate').value
};
fetch(`/api/recurring-rules/${currentRuleId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Fehler beim Speichern: ' + (data.error || 'Unbekannter Fehler'));
}
})
.catch(error => {
alert('Fehler: ' + error.message);
});
});
// Toggle Button Event Listener
document.querySelectorAll('.toggle-rule-btn').forEach(btn => {
btn.addEventListener('click', function() {
const ruleId = this.dataset.ruleId;
fetch(`/transactions/recurring/${ruleId}/toggle`, {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
}
});
});
});
// Delete Button Event Listener
document.querySelectorAll('.delete-rule-btn').forEach(btn => {
btn.addEventListener('click', function() {
if (confirm('Sind Sie sicher, dass Sie diese wiederkehrende Transaktion löschen möchten?')) {
const ruleId = this.dataset.ruleId;
fetch(`/api/recurring-rules/${ruleId}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
}
});
}
});
});
});
</script>
</div>
</div>
</div>
}
}
File diff suppressed because one or more lines are too long
+65
View File
@@ -0,0 +1,65 @@
package views
templ RegisterPage(errorMsg string) {
@Layout("Registrieren") {
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-md w-full space-y-8">
<div>
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
Neues Konto erstellen
</h2>
<p class="mt-2 text-center text-sm text-gray-600">
Oder
<a href="/login" class="font-medium text-indigo-600 hover:text-indigo-500">
mit bestehendem Konto anmelden
</a>
</p>
</div>
if errorMsg != "" {
<div class="rounded-md bg-red-50 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">
{ errorMsg }
</h3>
</div>
</div>
</div>
}
<form class="mt-8 space-y-6" action="/register" method="POST">
<div class="rounded-md shadow-sm -space-y-px">
<div>
<label for="name" class="sr-only">Name</label>
<input id="name" name="name" type="text" required class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" placeholder="Vollständiger Name"/>
</div>
<div>
<label for="email" class="sr-only">E-Mail-Adresse</label>
<input id="email" name="email" type="email" required class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" placeholder="E-Mail-Adresse"/>
</div>
<div>
<label for="password" class="sr-only">Passwort</label>
<input id="password" name="password" type="password" required class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" placeholder="Passwort"/>
</div>
<div>
<label for="password_confirm" class="sr-only">Passwort bestätigen</label>
<input id="password_confirm" name="password_confirm" type="password" required class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" placeholder="Passwort bestätigen"/>
</div>
</div>
<div>
<button type="submit" class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Registrieren
</button>
</div>
</form>
</div>
</div>
}
}
+81
View File
@@ -0,0 +1,81 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.778
package views
//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 RegisterPage(errorMsg 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_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
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_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8\"><div class=\"max-w-md w-full space-y-8\"><div><h2 class=\"mt-6 text-center text-3xl font-extrabold text-gray-900\">Neues Konto erstellen</h2><p class=\"mt-2 text-center text-sm text-gray-600\">Oder <a href=\"/login\" class=\"font-medium text-indigo-600 hover:text-indigo-500\">mit bestehendem Konto anmelden</a></p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if errorMsg != "" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"rounded-md bg-red-50 p-4\"><div class=\"flex\"><div class=\"flex-shrink-0\"><svg class=\"h-5 w-5 text-red-400\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path fill-rule=\"evenodd\" d=\"M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z\" clip-rule=\"evenodd\"></path></svg></div><div class=\"ml-3\"><h3 class=\"text-sm font-medium text-red-800\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(errorMsg)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/register.templ`, Line: 29, Col: 19}
}
_, 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("</h3></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<form class=\"mt-8 space-y-6\" action=\"/register\" method=\"POST\"><div class=\"rounded-md shadow-sm -space-y-px\"><div><label for=\"name\" class=\"sr-only\">Name</label> <input id=\"name\" name=\"name\" type=\"text\" required class=\"appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm\" placeholder=\"Vollständiger Name\"></div><div><label for=\"email\" class=\"sr-only\">E-Mail-Adresse</label> <input id=\"email\" name=\"email\" type=\"email\" required class=\"appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm\" placeholder=\"E-Mail-Adresse\"></div><div><label for=\"password\" class=\"sr-only\">Passwort</label> <input id=\"password\" name=\"password\" type=\"password\" required class=\"appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm\" placeholder=\"Passwort\"></div><div><label for=\"password_confirm\" class=\"sr-only\">Passwort bestätigen</label> <input id=\"password_confirm\" name=\"password_confirm\" type=\"password\" required class=\"appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm\" placeholder=\"Passwort bestätigen\"></div></div><div><button type=\"submit\" class=\"group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500\">Registrieren</button></div></form></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = Layout("Registrieren").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate
+312
View File
@@ -0,0 +1,312 @@
package views
import (
"whereismymoney/internal/models"
"fmt"
)
templ Settings(userName string, user models.User, categories []models.Category, bankAccounts []models.BankAccount) {
@Layout("Einstellungen - WhereIsMyMoney") {
<div class="min-h-screen bg-gray-50">
@Navigation(userName)
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">Einstellungen</h1>
<p class="mt-2 text-gray-600">Verwalte deine Kontoinformationen und App-Einstellungen</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Benutzereinstellungen -->
<div class="lg:col-span-2">
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">Benutzereinstellungen</h2>
</div>
<div class="px-6 py-4">
<form id="userSettingsForm" class="space-y-4">
<div>
<label for="username" class="block text-sm font-medium text-gray-700">Benutzername</label>
<input type="text" id="username" name="username" value={ user.Name } class="mt-1 w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-700">E-Mail</label>
<input type="email" id="email" name="email" value={ user.Email } readonly class="mt-1 w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-50 text-gray-500 cursor-not-allowed" title="E-Mail-Adresse kann nicht geändert werden">
<p class="mt-1 text-xs text-gray-500">Die E-Mail-Adresse dient als eindeutige Benutzer-ID und kann nicht geändert werden.</p>
</div>
<div class="pt-4">
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
Speichern
</button>
</div>
</form>
</div>
</div>
<!-- Passwort ändern -->
<div class="bg-white shadow rounded-lg mt-6">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">Passwort ändern</h2>
</div>
<div class="px-6 py-4">
<form id="passwordForm" class="space-y-4">
<div>
<label for="current_password" class="block text-sm font-medium text-gray-700">Aktuelles Passwort</label>
<input type="password" id="current_password" name="current_password" class="mt-1 w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label for="new_password" class="block text-sm font-medium text-gray-700">Neues Passwort</label>
<input type="password" id="new_password" name="new_password" class="mt-1 w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label for="confirm_password" class="block text-sm font-medium text-gray-700">Passwort bestätigen</label>
<input type="password" id="confirm_password" name="confirm_password" class="mt-1 w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500">
</div>
<div class="pt-4">
<button type="submit" class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500">
Passwort ändern
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Seitenleiste -->
<div class="space-y-6">
<!-- Kategorien verwalten -->
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">Kategorien</h3>
</div>
<div class="px-6 py-4">
<div class="space-y-2 max-h-48 overflow-y-auto">
for _, category := range categories {
<div class="flex items-center justify-between p-2 hover:bg-gray-50 rounded">
<span class="text-sm">{ category.Icon } { category.Name }</span>
<button class="delete-category text-red-600 hover:text-red-800 text-sm" data-category-id={ fmt.Sprintf("%d", category.ID) }>
🗑️
</button>
</div>
}
</div>
<button onclick="showModal('categoryModal')" class="mt-4 w-full px-3 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 text-sm">
+ Neue Kategorie
</button>
</div>
</div>
<!-- Konten verwalten -->
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">Bankkonten</h3>
</div>
<div class="px-6 py-4">
<div class="space-y-2 max-h-48 overflow-y-auto">
for _, account := range bankAccounts {
<div class="flex items-center justify-between p-2 hover:bg-gray-50 rounded">
<div class="text-sm">
<div class="font-medium">{ account.Name }</div>
<div class="text-gray-500">{ account.Bank }</div>
</div>
<button class="delete-account text-red-600 hover:text-red-800 text-sm" data-account-id={ fmt.Sprintf("%d", account.ID) }>
🗑️
</button>
</div>
}
</div>
<a href="/accounts" class="mt-4 w-full block px-3 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 text-sm text-center">
Konten verwalten
</a>
</div>
</div>
<!-- App-Informationen -->
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">App-Informationen</h3>
</div>
<div class="px-6 py-4 space-y-2 text-sm text-gray-600">
<div>Version: 1.0.0</div>
<div>Erstellt mit Go & Templ</div>
<div>© 2025 WhereIsMyMoney</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Kategorie Modal -->
<div id="categoryModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden z-50">
<div class="flex items-center justify-center min-h-screen px-4">
<div class="bg-white rounded-lg shadow-xl max-w-md w-full">
<form id="categoryForm" method="POST" action="/settings/categories">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">Neue Kategorie erstellen</h3>
</div>
<div class="px-6 py-4 space-y-4">
<div>
<label for="category_name" class="block text-sm font-medium text-gray-700">Name</label>
<input type="text" id="category_name" name="name" required class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500" placeholder="z.B. Lebensmittel">
</div>
<div>
<label for="category_icon" class="block text-sm font-medium text-gray-700">Icon (Emoji)</label>
<input type="text" id="category_icon" name="icon" required class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500" placeholder="🛒" maxlength="2">
</div>
<div>
<label for="category_color" class="block text-sm font-medium text-gray-700">Farbe</label>
<input type="color" id="category_color" name="color" value="#3B82F6" class="w-full h-10 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500">
</div>
</div>
<div class="px-6 py-4 border-t border-gray-200 flex justify-end space-x-3">
<button type="button" onclick="hideModal('categoryModal')" class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200">
Abbrechen
</button>
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700">
Erstellen
</button>
</div>
</form>
</div>
</div>
</div>
<script>
function showModal(modalId) {
document.getElementById(modalId).classList.remove('hidden');
}
function hideModal(modalId) {
document.getElementById(modalId).classList.add('hidden');
}
// Event Listeners für Delete-Buttons
document.addEventListener('DOMContentLoaded', function() {
// Delete Category Buttons
document.querySelectorAll('.delete-category').forEach(button => {
button.addEventListener('click', function() {
const categoryId = this.getAttribute('data-category-id');
deleteCategory(categoryId);
});
});
// Delete Account Buttons
document.querySelectorAll('.delete-account').forEach(button => {
button.addEventListener('click', function() {
const accountId = this.getAttribute('data-account-id');
deleteAccount(accountId);
});
});
});
function deleteCategory(categoryId) {
if (!confirm('Möchten Sie diese Kategorie wirklich löschen?')) {
return;
}
fetch(`/settings/categories/${categoryId}`, {
method: 'DELETE'
}).then(response => {
if (response.ok) {
location.reload();
} else {
alert('Fehler beim Löschen der Kategorie');
}
});
}
function deleteAccount(accountId) {
if (!confirm('Möchten Sie dieses Konto wirklich löschen?')) {
return;
}
fetch(`/settings/accounts/${accountId}`, {
method: 'DELETE'
}).then(response => {
if (response.ok) {
location.reload();
} else {
alert('Fehler beim Löschen des Kontos');
}
});
}
// Benutzereinstellungen speichern
document.getElementById('userSettingsForm').addEventListener('submit', async function(e) {
e.preventDefault();
// Nur den Benutzernamen senden, E-Mail wird ignoriert
const formObject = {
username: document.getElementById('username').value
};
try {
const response = await fetch('/settings/user', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formObject)
});
if (response.ok) {
alert('Einstellungen gespeichert!');
} else {
alert('Fehler beim Speichern der Einstellungen');
}
} catch (error) {
alert('Fehler beim Speichern der Einstellungen');
}
});
// Passwort ändern
document.getElementById('passwordForm').addEventListener('submit', async function(e) {
e.preventDefault();
const newPassword = document.getElementById('new_password').value;
const confirmPassword = document.getElementById('confirm_password').value;
if (newPassword !== confirmPassword) {
alert('Die Passwörter stimmen nicht überein!');
return;
}
const formData = new FormData(this);
const formObject = {};
for (let [key, value] of formData.entries()) {
formObject[key] = value;
}
try {
const response = await fetch('/settings/password', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formObject)
});
if (response.ok) {
alert('Passwort erfolgreich geändert!');
this.reset();
} else {
const error = await response.text();
alert('Fehler: ' + error);
}
} catch (error) {
alert('Fehler beim Ändern des Passworts');
}
});
// Modal schließen beim Klick außerhalb
document.addEventListener('click', function(event) {
if (event.target.classList.contains('bg-opacity-50')) {
event.target.classList.add('hidden');
}
});
</script>
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long