1229 lines
52 KiB
Plaintext
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")
|
|
}
|