Files
Matthias Hinrichs 189e7a2329 first commit
2025-08-26 03:17:49 +02:00

1229 lines
52 KiB
Plaintext

package views
import (
"whereismymoney/internal/models"
"fmt"
"time"
)
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
templ Transactions(userName string, transactions []models.Transaction, bankAccounts []models.BankAccount, categories []models.Category, recurrenceRules []models.RecurrenceRule, currentPage int, totalPages int, totalCount int64) {
@Layout("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">
<!-- Page Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">Transaktionen</h1>
<p class="mt-2 text-gray-600">Verwalte deine Einnahmen und Ausgaben</p>
</div>
<!-- Action Buttons -->
<div class="mb-6 flex gap-4">
<button onclick="showModal('transactionModal')" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg font-medium">
📝 Neue Transaktion
</button>
<button onclick="showModal('recurringModal')" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg font-medium">
🔄 Regelmäßige Transaktion
</button>
</div>
<!-- Filter Section -->
<div class="bg-white shadow-sm rounded-lg mb-6">
<div class="px-6 py-4">
<h3 class="text-lg font-medium text-gray-900 mb-4">Filter & Suche</h3>
<form id="filterForm" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<!-- Datumsbereich -->
<div>
<label for="date_from" class="block text-sm font-medium text-gray-700">Von Datum</label>
<input type="date" id="date_from" name="date_from" 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="date_to" class="block text-sm font-medium text-gray-700">Bis Datum</label>
<input type="date" id="date_to" name="date_to" 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>
<!-- Beschreibung -->
<div>
<label for="search_description" class="block text-sm font-medium text-gray-700">Beschreibung</label>
<input type="text" id="search_description" name="search_description" placeholder="z.B. Supermarkt" 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>
<!-- Kategorie -->
<div>
<label for="filter_category" class="block text-sm font-medium text-gray-700">Kategorie</label>
<select id="filter_category" name="filter_category" 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">
<option value="">Alle Kategorien</option>
for _, category := range categories {
<option value={ fmt.Sprintf("%d", category.ID) }>{ category.Icon } { category.Name }</option>
}
</select>
</div>
<!-- Konto -->
<div>
<label for="filter_account" class="block text-sm font-medium text-gray-700">Konto</label>
<select id="filter_account" name="filter_account" 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">
<option value="">Alle Konten</option>
for _, account := range bankAccounts {
<option value={ fmt.Sprintf("%d", account.ID) }>{ account.Name } ({ account.Bank })</option>
}
</select>
</div>
<!-- Transaktionstyp -->
<div>
<label for="filter_type" class="block text-sm font-medium text-gray-700">Typ</label>
<select id="filter_type" name="filter_type" 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">
<option value="">Alle Typen</option>
<option value="income">Einnahmen</option>
<option value="expense">Ausgaben</option>
</select>
</div>
<!-- Betrag -->
<div>
<label for="amount_min" class="block text-sm font-medium text-gray-700">Min. Betrag</label>
<input type="number" id="amount_min" name="amount_min" step="0.01" placeholder="0.00" 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="amount_max" class="block text-sm font-medium text-gray-700">Max. Betrag</label>
<input type="number" id="amount_max" name="amount_max" step="0.01" placeholder="9999.99" 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>
</form>
<!-- Filter Buttons -->
<div class="mt-4 flex flex-wrap gap-2">
<button type="button" id="applyFilter" onclick="applyFilters()" 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">
Filter anwenden
</button>
<button type="button" id="clearFilter" onclick="clearFilters()" class="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500">
Filter zurücksetzen
</button>
<div id="filterStatus" class="flex items-center text-sm text-gray-600">
<span id="filterCount">Alle Transaktionen werden angezeigt</span>
</div>
</div>
</div>
</div>
<!-- Tabs -->
<div class="mb-6">
<div class="border-b border-gray-200">
<nav class="-mb-px flex space-x-8">
<button onclick="showTab('transactions')" id="tab-transactions" class="tab-button border-b-2 border-blue-500 text-blue-600 py-2 px-1 text-sm font-medium">
Alle Transaktionen
</button>
<button onclick="showTab('recurring')" id="tab-recurring" class="tab-button border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 py-2 px-1 text-sm font-medium">
Regelmäßige Transaktionen
</button>
</nav>
</div>
</div>
<!-- Transactions Table -->
<div id="transactions-content" class="tab-content">
<div class="bg-white shadow-sm rounded-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<h2 class="text-lg font-medium text-gray-900">Letzte Transaktionen</h2>
<div id="multiEditControls" class="hidden flex space-x-2">
<button onclick="openMultiEditModal()" 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">
<span id="selectedCount">0</span> ausgewählte bearbeiten
</button>
<button onclick="openMultiDeleteConfirm()" 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">
<span id="selectedCountDelete">0</span> ausgewählte löschen
</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-3 py-3 text-left">
<input type="checkbox" id="selectAll" class="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50" onchange="toggleSelectAll()">
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Datum</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Beschreibung</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Kategorie</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Konto</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Betrag</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Aktionen</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
if len(transactions) == 0 {
<tr>
<td colspan="7" class="px-6 py-4 text-center text-gray-500">
Noch keine Transaktionen vorhanden
</td>
</tr>
}
for _, transaction := range transactions {
<tr class="hover:bg-gray-50">
<td class="px-3 py-4 whitespace-nowrap">
<input type="checkbox" class="transaction-checkbox rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
value={ fmt.Sprintf("%d", transaction.ID) } onchange="updateMultiEditControls()">
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{ transaction.Date.Format("02.01.2006") }
</td>
<td class="px-6 py-4 text-sm text-gray-900">
{ transaction.Description }
if transaction.IsRecurring {
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 ml-2">
🔄 Regelmäßig
</span>
}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
if transaction.Category != nil {
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
{ transaction.Category.Icon } { transaction.Category.Name }
</span>
} else {
<span class="text-gray-400">Ohne Kategorie</span>
}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
if transaction.BankAccount != nil {
{ transaction.BankAccount.Name }
} else {
<span class="text-gray-400">Ohne Konto</span>
}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-right">
if transaction.Type == "income" {
<span class="text-green-600 font-medium">+{ fmt.Sprintf("%.2f", transaction.Amount) } €</span>
} else {
<span class="text-red-600 font-medium">-{ fmt.Sprintf("%.2f", transaction.Amount) } €</span>
}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button data-transaction-id={ fmt.Sprintf("%d", transaction.ID) } class="edit-transaction text-blue-600 hover:text-blue-900 mr-2" title="Transaktion bearbeiten">
✏️
</button>
<button data-transaction-id={ fmt.Sprintf("%d", transaction.ID) } class="delete-transaction text-red-600 hover:text-red-900" title="Transaktion löschen">
🗑️
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
<!-- Pagination -->
if totalPages > 1 {
<div class="px-6 py-4 border-t border-gray-200 bg-gray-50">
<div class="flex items-center justify-between">
<div class="text-sm text-gray-700">
Zeige { fmt.Sprintf("%d", (currentPage-1)*20+1) } bis { fmt.Sprintf("%d", min(currentPage*20, int(totalCount))) } von { fmt.Sprintf("%d", totalCount) } Transaktionen
</div>
<div class="flex items-center space-x-2">
<!-- Previous Button -->
if currentPage > 1 {
<a href={ templ.URL(fmt.Sprintf("/transactions?page=%d", currentPage-1)) } class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:text-gray-700 hover:bg-gray-50">
Vorherige
</a>
} else {
<span class="px-3 py-2 text-sm font-medium text-gray-300 bg-gray-100 border border-gray-300 rounded-md cursor-not-allowed">
Vorherige
</span>
}
<!-- Page Numbers -->
<div class="flex items-center space-x-1">
if currentPage > 3 {
<a href={ templ.URL("/transactions?page=1") } class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:text-gray-700 hover:bg-gray-50">
1
</a>
if currentPage > 4 {
<span class="px-2 py-2 text-sm text-gray-500">...</span>
}
}
for i := max(1, currentPage-2); i <= min(totalPages, currentPage+2); i++ {
if i == currentPage {
<span class="px-3 py-2 text-sm font-medium text-white bg-blue-600 border border-blue-600 rounded-md">
{ fmt.Sprintf("%d", i) }
</span>
} else {
<a href={ templ.URL(fmt.Sprintf("/transactions?page=%d", i)) } class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:text-gray-700 hover:bg-gray-50">
{ fmt.Sprintf("%d", i) }
</a>
}
}
if currentPage < totalPages-2 {
if currentPage < totalPages-3 {
<span class="px-2 py-2 text-sm text-gray-500">...</span>
}
<a href={ templ.URL(fmt.Sprintf("/transactions?page=%d", totalPages)) } class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:text-gray-700 hover:bg-gray-50">
{ fmt.Sprintf("%d", totalPages) }
</a>
}
</div>
<!-- Next Button -->
if currentPage < totalPages {
<a href={ templ.URL(fmt.Sprintf("/transactions?page=%d", currentPage+1)) } class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:text-gray-700 hover:bg-gray-50">
Nächste
</a>
} else {
<span class="px-3 py-2 text-sm font-medium text-gray-300 bg-gray-100 border border-gray-300 rounded-md cursor-not-allowed">
Nächste
</span>
}
</div>
</div>
</div>
}
</div>
</div>
<!-- Recurring Transactions Table -->
<div id="recurring-content" class="tab-content hidden">
<div class="bg-white shadow-sm rounded-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">Regelmäßige Transaktionen</h2>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Beschreibung</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Intervall</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Nächste Ausführung</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Betrag</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Aktionen</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
if len(recurrenceRules) == 0 {
<tr>
<td colspan="6" class="px-6 py-4 text-center text-gray-500">
Noch keine regelmäßigen Transaktionen vorhanden
</td>
</tr>
}
for _, rule := range recurrenceRules {
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 text-sm text-gray-900">
{ rule.Description }
if rule.Category != nil {
<div class="text-xs text-gray-500 mt-1">
{ rule.Category.Icon } { rule.Category.Name }
</div>
}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{ getIntervalText(rule.Interval, rule.IntervalCount) }
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{ getNextExecutionDate(rule) }
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-right">
if rule.Type == "income" {
<span class="text-green-600 font-medium">+{ fmt.Sprintf("%.2f", rule.Amount) } €</span>
} else {
<span class="text-red-600 font-medium">-{ fmt.Sprintf("%.2f", rule.Amount) } €</span>
}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
if rule.IsActive {
<span class="inline-flex items-center px-2 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 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
Pausiert
</span>
}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button data-rule-id={ fmt.Sprintf("%d", rule.ID) } class="toggle-recurrence text-blue-600 hover:text-blue-900 mr-2">
if rule.IsActive {
⏸️
} else {
▶️
}
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Transaction Modal -->
<div id="transactionModal" 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 method="POST" action="/transactions">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">Neue Transaktion</h3>
</div>
<div class="px-6 py-4 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Beschreibung</label>
<input type="text" name="description" 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. Supermarkt Einkauf">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Betrag</label>
<input type="number" name="amount" step="0.01" 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="0.00">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Typ</label>
<select name="type" 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">
<option value="">Wählen...</option>
<option value="income">💰 Einnahme</option>
<option value="expense">💸 Ausgabe</option>
</select>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Datum</label>
<input type="date" name="date" 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" value={ time.Now().Format("2006-01-02") }>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Kategorie (optional)</label>
<select name="category_id" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500">
<option value="">Ohne Kategorie</option>
for _, category := range categories {
<option value={ fmt.Sprintf("%d", category.ID) }>{ category.Icon } { category.Name }</option>
}
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Bankkonto (optional)</label>
<select name="bank_account_id" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500">
<option value="">Ohne Konto</option>
for _, account := range bankAccounts {
<option value={ fmt.Sprintf("%d", account.ID) }>{ account.Name } ({ account.Bank })</option>
}
</select>
</div>
</div>
<div class="px-6 py-4 bg-gray-50 flex justify-end space-x-3">
<button type="button" onclick="hideModal('transactionModal')" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
Abbrechen
</button>
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700">
Erstellen
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Edit Transaction Modal -->
<div id="editTransactionModal" 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="editTransactionForm" method="POST">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">Transaktion bearbeiten</h3>
</div>
<div class="px-6 py-4 space-y-4">
<div>
<label for="edit_description" class="block text-sm font-medium text-gray-700">Beschreibung</label>
<input type="text" id="edit_description" name="description" 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">
</div>
<div>
<label for="edit_amount" class="block text-sm font-medium text-gray-700">Betrag</label>
<input type="number" id="edit_amount" name="amount" step="0.01" 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">
</div>
<div>
<label for="edit_type" class="block text-sm font-medium text-gray-700">Typ</label>
<select id="edit_type" name="type" 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">
<option value="expense">Ausgabe</option>
<option value="income">Einnahme</option>
</select>
</div>
<div>
<label for="edit_date" class="block text-sm font-medium text-gray-700">Datum</label>
<input type="date" id="edit_date" name="date" 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">
</div>
<div>
<label for="edit_category_id" class="block text-sm font-medium text-gray-700">Kategorie</label>
<select id="edit_category_id" name="category_id" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500">
<option value="">Keine Kategorie</option>
for _, category := range categories {
<option value={ fmt.Sprintf("%d", category.ID) }>{ category.Icon } { category.Name }</option>
}
</select>
</div>
<div>
<label for="edit_bank_account_id" class="block text-sm font-medium text-gray-700">Konto</label>
<select id="edit_bank_account_id" name="bank_account_id" 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">
for _, account := range bankAccounts {
<option value={ fmt.Sprintf("%d", account.ID) }>{ account.Name } ({ account.Bank })</option>
}
</select>
</div>
</div>
<div class="px-6 py-4 border-t border-gray-200 flex justify-end space-x-3">
<button type="button" onclick="closeEditTransactionModal()" 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">
Speichern
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Recurring Transaction Modal -->
<div id="recurringModal" 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-lg w-full max-h-screen overflow-y-auto">
<form method="POST" action="/transactions/recurring">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">Regelmäßige Transaktion</h3>
</div>
<div class="px-6 py-4 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Beschreibung</label>
<input type="text" name="description" 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. Miete">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Betrag</label>
<input type="number" name="amount" step="0.01" 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="0.00">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Typ</label>
<select name="type" 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">
<option value="">Wählen...</option>
<option value="income">💰 Einnahme</option>
<option value="expense">💸 Ausgabe</option>
</select>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Startdatum</label>
<input type="date" name="start_date" 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" value={ time.Now().Format("2006-01-02") }>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Enddatum (optional)</label>
<input type="date" name="end_date" class="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>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Intervall</label>
<select name="interval" required onchange="toggleIntervalOptions()" id="interval-select" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500">
<option value="">Wählen...</option>
<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 mb-1">Alle X</label>
<input type="number" name="interval_count" min="1" value="1" class="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>
<!-- Day of Month (for monthly) -->
<div id="day-of-month-group" class="hidden">
<label class="block text-sm font-medium text-gray-700 mb-1">Tag im Monat</label>
<input type="number" name="day_of_month" min="1" max="31" 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. 1 für jeden ersten">
</div>
<!-- Day of Week (for weekly) -->
<div id="day-of-week-group" class="hidden">
<label class="block text-sm font-medium text-gray-700 mb-1">Wochentag</label>
<select name="day_of_week" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500">
<option value="">Aktueller Wochentag</option>
<option value="1">Montag</option>
<option value="2">Dienstag</option>
<option value="3">Mittwoch</option>
<option value="4">Donnerstag</option>
<option value="5">Freitag</option>
<option value="6">Samstag</option>
<option value="0">Sonntag</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Kategorie (optional)</label>
<select name="category_id" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500">
<option value="">Ohne Kategorie</option>
for _, category := range categories {
<option value={ fmt.Sprintf("%d", category.ID) }>{ category.Icon } { category.Name }</option>
}
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Bankkonto (optional)</label>
<select name="bank_account_id" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500">
<option value="">Ohne Konto</option>
for _, account := range bankAccounts {
<option value={ fmt.Sprintf("%d", account.ID) }>{ account.Name } ({ account.Bank })</option>
}
</select>
</div>
</div>
<div class="px-6 py-4 bg-gray-50 flex justify-end space-x-3">
<button type="button" onclick="hideModal('recurringModal')" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
Abbrechen
</button>
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-green-600 border border-transparent rounded-md hover:bg-green-700">
Erstellen
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Multi-Edit Modal -->
<div id="multiEditModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 shadow-lg rounded-md bg-white">
<div class="mt-3">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium text-gray-900">Mehrere Transaktionen bearbeiten</h3>
<button onclick="hideModal('multiEditModal')" class="text-gray-400 hover:text-gray-600">
<span class="sr-only">Schließen</span>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<p class="text-sm text-gray-600 mb-4">
Nur die Felder ausfüllen, die für alle ausgewählten Transaktionen geändert werden sollen. Leere Felder bleiben unverändert.
</p>
<form id="multiEditForm" onsubmit="submitMultiEdit(event)">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Beschreibung</label>
<input type="text" id="multiDescription" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500">
<p class="text-xs text-gray-500 mt-1">Leer lassen um unverändert zu lassen</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Betrag</label>
<input type="number" step="0.01" id="multiAmount" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500">
<p class="text-xs text-gray-500 mt-1">Leer lassen um unverändert zu lassen</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Typ</label>
<select id="multiType" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500">
<option value="">Unverändert lassen</option>
<option value="income">Einnahme</option>
<option value="expense">Ausgabe</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Kategorie</label>
<select id="multiCategory" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500">
<option value="">Unverändert lassen</option>
<option value="null">Ohne Kategorie</option>
for _, category := range categories {
<option value={ fmt.Sprintf("%d", category.ID) }>{ category.Icon } { category.Name }</option>
}
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Bankkonto</label>
<select id="multiAccount" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500">
<option value="">Unverändert lassen</option>
<option value="null">Ohne Konto</option>
for _, account := range bankAccounts {
<option value={ fmt.Sprintf("%d", account.ID) }>{ account.Name } ({ account.Bank })</option>
}
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Datum</label>
<input type="date" id="multiDate" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500">
<p class="text-xs text-gray-500 mt-1">Leer lassen um unverändert zu lassen</p>
</div>
</div>
<div class="flex justify-end space-x-3">
<button type="button" onclick="hideModal('multiEditModal')" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
Abbrechen
</button>
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700">
Ausgewählte Transaktionen aktualisieren
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Multi-Delete Confirmation Modal -->
<div id="multiDeleteModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 shadow-lg rounded-md bg-white">
<div class="mt-3">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium text-gray-900">Transaktionen löschen</h3>
<button onclick="hideModal('multiDeleteModal')" class="text-gray-400 hover:text-gray-600">
<span class="sr-only">Schließen</span>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="mb-4">
<div class="flex items-center mb-3">
<svg class="h-12 w-12 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
</svg>
<div class="ml-4">
<h3 class="text-lg font-medium text-gray-900">Sind Sie sicher?</h3>
<p class="text-sm text-gray-600">
Sie sind dabei, <span id="deleteCountText" class="font-semibold text-red-600">0</span> Transaktionen zu löschen.
Diese Aktion kann nicht rückgängig gemacht werden.
</p>
</div>
</div>
<div class="bg-red-50 border border-red-200 rounded-md p-3">
<p class="text-sm text-red-700">
<strong>Warnung:</strong> Alle ausgewählten Transaktionen werden permanent gelöscht.
Stellen Sie sicher, dass Sie die richtigen Transaktionen ausgewählt haben.
</p>
</div>
</div>
<div class="flex justify-end space-x-3">
<button type="button" onclick="hideModal('multiDeleteModal')" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
Abbrechen
</button>
<button type="button" onclick="confirmMultiDelete()" class="px-4 py-2 text-sm font-medium text-white bg-red-600 border border-transparent rounded-md hover:bg-red-700">
Ja, löschen
</button>
</div>
</div>
</div>
</div>
<script>
function showModal(modalId) {
document.getElementById(modalId).classList.remove('hidden');
}
function hideModal(modalId) {
document.getElementById(modalId).classList.add('hidden');
}
function showTab(tabName) {
// Hide all content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.add('hidden');
});
// Remove active class from all tabs
document.querySelectorAll('.tab-button').forEach(tab => {
tab.classList.remove('border-blue-500', 'text-blue-600');
tab.classList.add('border-transparent', 'text-gray-500');
});
// Show selected content
document.getElementById(tabName + '-content').classList.remove('hidden');
// Activate selected tab
const activeTab = document.getElementById('tab-' + tabName);
activeTab.classList.add('border-blue-500', 'text-blue-600');
activeTab.classList.remove('border-transparent', 'text-gray-500');
}
function showEditTransactionModal(transaction) {
// Populate form fields with transaction data
document.getElementById('edit_description').value = transaction.description;
document.getElementById('edit_amount').value = transaction.amount;
document.getElementById('edit_type').value = transaction.type;
document.getElementById('edit_date').value = transaction.date;
document.getElementById('edit_category_id').value = transaction.category_id || '';
document.getElementById('edit_bank_account_id').value = transaction.bank_account_id;
// Set form action to update URL
document.getElementById('editTransactionForm').action = '/transactions/' + transaction.id;
// Show modal
showModal('editTransactionModal');
}
function closeEditTransactionModal() {
hideModal('editTransactionModal');
}
function toggleIntervalOptions() {
const interval = document.getElementById('interval-select').value;
const dayOfMonthGroup = document.getElementById('day-of-month-group');
const dayOfWeekGroup = document.getElementById('day-of-week-group');
// Hide all
dayOfMonthGroup.classList.add('hidden');
dayOfWeekGroup.classList.add('hidden');
// Show relevant option
if (interval === 'monthly') {
dayOfMonthGroup.classList.remove('hidden');
} else if (interval === 'weekly') {
dayOfWeekGroup.classList.remove('hidden');
}
}
// Multi-Edit Funktionalität
function toggleSelectAll() {
const selectAllCheckbox = document.getElementById('selectAll');
const transactionCheckboxes = document.querySelectorAll('.transaction-checkbox');
transactionCheckboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
});
updateMultiEditControls();
}
function updateMultiEditControls() {
const transactionCheckboxes = document.querySelectorAll('.transaction-checkbox');
const checkedBoxes = document.querySelectorAll('.transaction-checkbox:checked');
const multiEditControls = document.getElementById('multiEditControls');
const selectedCount = document.getElementById('selectedCount');
const selectedCountDelete = document.getElementById('selectedCountDelete');
if (checkedBoxes.length > 0) {
multiEditControls.classList.remove('hidden');
selectedCount.textContent = checkedBoxes.length;
selectedCountDelete.textContent = checkedBoxes.length;
} else {
multiEditControls.classList.add('hidden');
}
// Update "Select All" checkbox state
const selectAllCheckbox = document.getElementById('selectAll');
if (checkedBoxes.length === 0) {
selectAllCheckbox.indeterminate = false;
selectAllCheckbox.checked = false;
} else if (checkedBoxes.length === transactionCheckboxes.length) {
selectAllCheckbox.indeterminate = false;
selectAllCheckbox.checked = true;
} else {
selectAllCheckbox.indeterminate = true;
}
}
function openMultiEditModal() {
const checkedBoxes = document.querySelectorAll('.transaction-checkbox:checked');
if (checkedBoxes.length === 0) {
alert('Bitte wählen Sie mindestens eine Transaktion aus.');
return;
}
// Reset form
document.getElementById('multiEditForm').reset();
showModal('multiEditModal');
}
function submitMultiEdit(event) {
event.preventDefault();
const checkedBoxes = document.querySelectorAll('.transaction-checkbox:checked');
const transactionIds = Array.from(checkedBoxes).map(cb => cb.value);
if (transactionIds.length === 0) {
alert('Keine Transaktionen ausgewählt.');
return;
}
const formData = {
transaction_ids: transactionIds,
description: document.getElementById('multiDescription').value,
amount: document.getElementById('multiAmount').value,
type: document.getElementById('multiType').value,
category_id: document.getElementById('multiCategory').value,
bank_account_id: document.getElementById('multiAccount').value,
date: document.getElementById('multiDate').value
};
// Remove empty values
Object.keys(formData).forEach(key => {
if (formData[key] === '' || formData[key] === null) {
delete formData[key];
}
});
// Special handling for null values
if (document.getElementById('multiCategory').value === 'null') {
formData.category_id = null;
}
if (document.getElementById('multiAccount').value === 'null') {
formData.bank_account_id = null;
}
fetch('/transactions/multi-update', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
})
.then(response => {
if (response.ok) {
hideModal('multiEditModal');
location.reload();
} else {
response.text().then(text => {
alert('Fehler beim Aktualisieren: ' + text);
});
}
})
.catch(error => {
console.error('Error:', error);
alert('Fehler beim Aktualisieren der Transaktionen');
});
}
function openMultiDeleteConfirm() {
const checkedBoxes = document.querySelectorAll('.transaction-checkbox:checked');
if (checkedBoxes.length === 0) {
alert('Bitte wählen Sie mindestens eine Transaktion aus.');
return;
}
// Update delete count in modal
document.getElementById('deleteCountText').textContent = checkedBoxes.length;
showModal('multiDeleteModal');
}
function confirmMultiDelete() {
const checkedBoxes = document.querySelectorAll('.transaction-checkbox:checked');
const transactionIds = Array.from(checkedBoxes).map(cb => cb.value);
if (transactionIds.length === 0) {
alert('Keine Transaktionen ausgewählt.');
return;
}
// Disable button to prevent double clicks
const deleteButton = event.target;
deleteButton.disabled = true;
deleteButton.textContent = 'Lösche...';
fetch('/transactions/multi-delete', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
transaction_ids: transactionIds
})
})
.then(response => {
if (response.ok) {
hideModal('multiDeleteModal');
location.reload();
} else {
response.text().then(text => {
alert('Fehler beim Löschen: ' + text);
deleteButton.disabled = false;
deleteButton.textContent = 'Ja, löschen';
});
}
})
.catch(error => {
console.error('Error:', error);
alert('Fehler beim Löschen der Transaktionen');
deleteButton.disabled = false;
deleteButton.textContent = 'Ja, löschen';
});
}
// Filter-Funktionalität
function applyFilters() {
const filterData = {
date_from: document.getElementById('date_from').value,
date_to: document.getElementById('date_to').value,
search_description: document.getElementById('search_description').value,
filter_category: document.getElementById('filter_category').value,
filter_account: document.getElementById('filter_account').value,
filter_type: document.getElementById('filter_type').value,
amount_min: document.getElementById('amount_min').value,
amount_max: document.getElementById('amount_max').value
};
// Build URL with filter parameters
const params = new URLSearchParams();
Object.entries(filterData).forEach(([key, value]) => {
if (value && value.trim() !== '') {
params.append(key, value);
}
});
// Redirect to filtered page
window.location.href = '/transactions?' + params.toString();
}
function clearFilters() {
document.getElementById('filterForm').reset();
window.location.href = '/transactions';
}
function updateFilterStatus() {
const params = new URLSearchParams(window.location.search);
const activeFilters = [];
if (params.get('date_from') || params.get('date_to')) {
activeFilters.push('Datum');
}
if (params.get('search_description')) {
activeFilters.push('Beschreibung');
}
if (params.get('filter_category')) {
activeFilters.push('Kategorie');
}
if (params.get('filter_account')) {
activeFilters.push('Konto');
}
if (params.get('filter_type')) {
activeFilters.push('Typ');
}
if (params.get('amount_min') || params.get('amount_max')) {
activeFilters.push('Betrag');
}
const filterCountElement = document.getElementById('filterCount');
if (activeFilters.length > 0) {
filterCountElement.textContent = `Gefiltert nach: ${activeFilters.join(', ')}`;
} else {
filterCountElement.textContent = 'Alle Transaktionen werden angezeigt';
}
}
function buildPaginationURL(page) {
const params = new URLSearchParams(window.location.search);
params.set('page', page);
return '/transactions?' + params.toString();
}
function updatePaginationLinks() {
// Update all pagination links to preserve filters
const paginationLinks = document.querySelectorAll('a[href*="/transactions?page="]');
paginationLinks.forEach(link => {
const href = link.getAttribute('href');
const pageMatch = href.match(/page=(\d+)/);
if (pageMatch) {
const page = pageMatch[1];
link.setAttribute('href', buildPaginationURL(page));
}
});
}
function loadFilterFromURL() {
const params = new URLSearchParams(window.location.search);
document.getElementById('date_from').value = params.get('date_from') || '';
document.getElementById('date_to').value = params.get('date_to') || '';
document.getElementById('search_description').value = params.get('search_description') || '';
document.getElementById('filter_category').value = params.get('filter_category') || '';
document.getElementById('filter_account').value = params.get('filter_account') || '';
document.getElementById('filter_type').value = params.get('filter_type') || '';
document.getElementById('amount_min').value = params.get('amount_min') || '';
document.getElementById('amount_max').value = params.get('amount_max') || '';
}
// Event Listeners
document.addEventListener('DOMContentLoaded', function() {
loadFilterFromURL();
updateFilterStatus();
updatePaginationLinks();
// Enter key to apply filters
const form = document.getElementById('filterForm');
if (form) {
form.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
applyFilters();
}
});
}
});
document.addEventListener('DOMContentLoaded', function() {
// Delete transaction buttons
document.querySelectorAll('.delete-transaction').forEach(button => {
button.addEventListener('click', async function() {
const id = this.getAttribute('data-transaction-id');
if (!confirm('Möchtest du diese Transaktion wirklich löschen?')) {
return;
}
try {
const response = await fetch(`/transactions/${id}`, {
method: 'DELETE'
});
if (response.ok) {
location.reload();
} else {
alert('Fehler beim Löschen der Transaktion');
}
} catch (error) {
alert('Fehler beim Löschen der Transaktion');
}
});
});
// Edit transaction buttons
document.querySelectorAll('.edit-transaction').forEach(button => {
button.addEventListener('click', async function() {
const id = this.getAttribute('data-transaction-id');
try {
const response = await fetch(`/transactions/${id}/data`);
if (response.ok) {
const transaction = await response.json();
showEditTransactionModal(transaction);
} else {
alert('Fehler beim Laden der Transaktionsdaten');
}
} catch (error) {
alert('Fehler beim Laden der Transaktionsdaten');
}
});
});
// Toggle recurrence rule buttons
document.querySelectorAll('.toggle-recurrence').forEach(button => {
button.addEventListener('click', async function() {
const id = this.getAttribute('data-rule-id');
try {
const response = await fetch(`/transactions/recurring/${id}/toggle`, {
method: 'POST'
});
if (response.ok) {
location.reload();
} else {
alert('Fehler beim Umschalten der Regel');
}
} catch (error) {
alert('Fehler beim Umschalten der Regel');
}
});
});
// Edit transaction form submit
document.getElementById('editTransactionForm').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
const formObject = {};
for (let [key, value] of formData.entries()) {
formObject[key] = value;
}
try {
const response = await fetch(this.action, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formObject)
});
if (response.ok) {
closeEditTransactionModal();
location.reload();
} else {
alert('Fehler beim Speichern der Transaktion');
}
} catch (error) {
alert('Fehler beim Speichern der Transaktion');
}
});
});
// Close modal when clicking outside
document.addEventListener('click', function(event) {
if (event.target.classList.contains('bg-opacity-50')) {
event.target.classList.add('hidden');
}
});
</script>
}
}
func getIntervalText(interval string, count int) string {
var base string
switch interval {
case "daily":
base = "Tag"
if count > 1 {
base = "Tage"
}
case "weekly":
base = "Woche"
if count > 1 {
base = "Wochen"
}
case "monthly":
base = "Monat"
if count > 1 {
base = "Monate"
}
case "yearly":
base = "Jahr"
if count > 1 {
base = "Jahre"
}
default:
return interval
}
if count == 1 {
return fmt.Sprintf("Jeden %s", base)
}
return fmt.Sprintf("Alle %d %s", count, base)
}
func getNextExecutionDate(rule models.RecurrenceRule) string {
if !rule.IsActive {
return "Pausiert"
}
lastDate := rule.StartDate
if rule.LastGenerated != nil {
lastDate = *rule.LastGenerated
}
nextDate := rule.GetNextOccurrence(lastDate)
if nextDate == nil {
return "Beendet"
}
return nextDate.Format("02.01.2006")
}