From c289cc3c5831306f1f6050ab19e55b005202def7 Mon Sep 17 00:00:00 2001 From: Jan Remmer Siebels Date: Sun, 5 Oct 2025 00:38:18 +0200 Subject: [PATCH] Fix payment system balance integration and add calendar functionality - Implement automated payment tracking with Django signals - Fix duplicate transaction creation with unique referenz system - Add calendar system with CRUD operations and event management - Reorganize navigation menu (rename sections, move admin functions) - Replace Geschichte editor with EasyMDE markdown editor - Add management commands for balance reconciliation - Create missing transactions for previously paid payments - Ensure account balances accurately reflect all payment activity Features added: - Calendar entries creation and administration via menu - Payment status tracking with automatic balance updates - Duplicate prevention for payment transactions - Markdown editor with live preview for Geschichte pages - Database reconciliation tools for payment/balance sync Bug fixes: - Resolved IntegrityError on payment status changes - Fixed missing account balance updates for paid payments - Prevented duplicate balance deductions on re-saves - Corrected menu structure and admin function placement --- app/stiftung/apps.py | 6 + app/stiftung/forms.py | 17 +- .../commands/create_sample_events.py | 58 +++ .../commands/fix_account_balances.py | 169 +++++++ .../management/commands/reconcile_balances.py | 145 ++++++ .../migrations/0038_allow_blank_content.py | 18 + .../0039_stiftungskalendereintrag.py | 41 ++ .../migrations/0040_add_calendar_model.py | 13 + .../0041_alter_geschichteseite_inhalt.py | 18 + app/stiftung/models.py | 127 ++++- app/stiftung/services/__init__.py | 1 + app/stiftung/services/calendar_service.py | 269 +++++++++++ app/stiftung/signals.py | 166 +++++++ app/stiftung/templatetags/help_tags.py | 16 + app/stiftung/urls.py | 18 +- app/stiftung/views.py | 448 +++++++++++++++++- app/templates/base.html | 58 ++- app/templates/stiftung/bericht_list.html | 10 +- .../stiftung/geschichte/bild_delete.html | 66 +++ app/templates/stiftung/geschichte/detail.html | 21 +- app/templates/stiftung/geschichte/form.html | 202 +++++--- app/templates/stiftung/home.html | 286 ++++++++++- app/templates/stiftung/kalender/admin.html | 346 ++++++++++++++ .../stiftung/kalender/agenda_view.html | 337 +++++++++++++ app/templates/stiftung/kalender/create.html | 125 +++++ app/templates/stiftung/kalender/delete.html | 32 ++ .../stiftung/kalender/delete_confirm.html | 108 +++++ app/templates/stiftung/kalender/detail.html | 167 +++++++ .../stiftung/kalender/detail_old.html | 32 ++ app/templates/stiftung/kalender/edit.html | 154 ++++++ app/templates/stiftung/kalender/form.html | 42 ++ app/templates/stiftung/kalender/kalender.html | 60 +++ .../stiftung/kalender/list_view.html | 140 ++++++ .../stiftung/kalender/month_view.html | 200 ++++++++ .../stiftung/kalender/week_view.html | 184 +++++++ menu-structure.csv | 38 ++ 36 files changed, 4039 insertions(+), 99 deletions(-) create mode 100644 app/stiftung/management/commands/create_sample_events.py create mode 100644 app/stiftung/management/commands/fix_account_balances.py create mode 100644 app/stiftung/management/commands/reconcile_balances.py create mode 100644 app/stiftung/migrations/0038_allow_blank_content.py create mode 100644 app/stiftung/migrations/0039_stiftungskalendereintrag.py create mode 100644 app/stiftung/migrations/0040_add_calendar_model.py create mode 100644 app/stiftung/migrations/0041_alter_geschichteseite_inhalt.py create mode 100644 app/stiftung/services/__init__.py create mode 100644 app/stiftung/services/calendar_service.py create mode 100644 app/stiftung/signals.py create mode 100644 app/templates/stiftung/geschichte/bild_delete.html create mode 100644 app/templates/stiftung/kalender/admin.html create mode 100644 app/templates/stiftung/kalender/agenda_view.html create mode 100644 app/templates/stiftung/kalender/create.html create mode 100644 app/templates/stiftung/kalender/delete.html create mode 100644 app/templates/stiftung/kalender/delete_confirm.html create mode 100644 app/templates/stiftung/kalender/detail.html create mode 100644 app/templates/stiftung/kalender/detail_old.html create mode 100644 app/templates/stiftung/kalender/edit.html create mode 100644 app/templates/stiftung/kalender/form.html create mode 100644 app/templates/stiftung/kalender/kalender.html create mode 100644 app/templates/stiftung/kalender/list_view.html create mode 100644 app/templates/stiftung/kalender/month_view.html create mode 100644 app/templates/stiftung/kalender/week_view.html create mode 100644 menu-structure.csv diff --git a/app/stiftung/apps.py b/app/stiftung/apps.py index 13100c8..73a3f00 100644 --- a/app/stiftung/apps.py +++ b/app/stiftung/apps.py @@ -14,3 +14,9 @@ class StiftungConfig(AppConfig): except ImportError: # django-otp not installed pass + + # Import signals to register them + try: + import stiftung.signals + except ImportError: + pass diff --git a/app/stiftung/forms.py b/app/stiftung/forms.py index 10517b5..784f1c9 100644 --- a/app/stiftung/forms.py +++ b/app/stiftung/forms.py @@ -1648,14 +1648,29 @@ class GeschichteSeiteForm(forms.ModelForm): def clean_slug(self): slug = self.cleaned_data.get('slug') - titel = self.cleaned_data.get('titel') + titel = self.cleaned_data.get('titel', '') if not slug and titel: # Auto-generate slug from title from django.utils.text import slugify slug = slugify(titel) + if not slug: + raise forms.ValidationError('Slug ist erforderlich. Bitte geben Sie einen Titel ein.') + return slug + + def clean(self): + cleaned_data = super().clean() + titel = cleaned_data.get('titel', '') + slug = cleaned_data.get('slug', '') + + # Auto-generate slug if empty + if titel and not slug: + from django.utils.text import slugify + cleaned_data['slug'] = slugify(titel) + + return cleaned_data class GeschichteBildForm(forms.ModelForm): diff --git a/app/stiftung/management/commands/create_sample_events.py b/app/stiftung/management/commands/create_sample_events.py new file mode 100644 index 0000000..11024bf --- /dev/null +++ b/app/stiftung/management/commands/create_sample_events.py @@ -0,0 +1,58 @@ +from django.core.management.base import BaseCommand +from stiftung.models import StiftungsKalenderEintrag +from datetime import date, timedelta + + +class Command(BaseCommand): + help = 'Creates sample calendar events for testing' + + def handle(self, *args, **options): + today = date.today() + + events = [ + { + 'titel': 'Vorstandssitzung', + 'beschreibung': 'Monatliche Vorstandssitzung zur Besprechung aktueller Stiftungsangelegenheiten', + 'datum': today + timedelta(days=7), + 'kategorie': 'termin', + 'prioritaet': 'hoch', + }, + { + 'titel': 'Zahlungserinnerung Familie Müller', + 'beschreibung': 'Quartalsweise Unterstützung €500', + 'datum': today + timedelta(days=3), + 'kategorie': 'zahlung', + 'prioritaet': 'kritisch', + }, + { + 'titel': 'Pachtvertrag Müller läuft aus', + 'beschreibung': 'Vertrag für Grundstück A123 muss verlängert werden', + 'datum': today + timedelta(days=30), + 'kategorie': 'vertrag', + 'prioritaet': 'hoch', + }, + { + 'titel': 'Geburtstag Maria Schmidt', + 'beschreibung': '75. Geburtstag', + 'datum': today + timedelta(days=5), + 'kategorie': 'geburtstag', + 'prioritaet': 'normal', + } + ] + + created_count = 0 + for event_data in events: + event, created = StiftungsKalenderEintrag.objects.get_or_create( + titel=event_data['titel'], + defaults=event_data + ) + if created: + created_count += 1 + self.stdout.write(f'Created: {event.titel}') + else: + self.stdout.write(f'Already exists: {event.titel}') + + total = StiftungsKalenderEintrag.objects.count() + self.stdout.write( + self.style.SUCCESS(f'✅ Created {created_count} new events. Total: {total}') + ) \ No newline at end of file diff --git a/app/stiftung/management/commands/fix_account_balances.py b/app/stiftung/management/commands/fix_account_balances.py new file mode 100644 index 0000000..90eb2e0 --- /dev/null +++ b/app/stiftung/management/commands/fix_account_balances.py @@ -0,0 +1,169 @@ +""" +Management command to fix account balances for existing paid payments. + +This command will: +1. Find all payments marked as 'ausgezahlt' (paid) +2. Check if corresponding bank transactions exist +3. Create missing bank transactions +4. Update account balances to reflect all paid payments + +Usage: + python manage.py fix_account_balances +""" + +from django.core.management.base import BaseCommand +from django.utils import timezone +from decimal import Decimal + +from stiftung.models import DestinataerUnterstuetzung, BankTransaction, StiftungsKonto + + +class Command(BaseCommand): + help = 'Fix account balances for existing paid payments' + + def add_arguments(self, parser): + parser.add_argument( + '--dry-run', + action='store_true', + help='Show what would be done without making changes', + ) + parser.add_argument( + '--account', + type=str, + help='Only process payments for specific account (by kontoname)', + ) + + def handle(self, *args, **options): + dry_run = options['dry_run'] + account_filter = options.get('account') + + self.stdout.write( + self.style.SUCCESS('🔍 Analyzing paid payments and account balances...') + ) + + if dry_run: + self.stdout.write( + self.style.WARNING('DRY RUN MODE - No changes will be made') + ) + + # Get all paid payments + paid_payments_query = DestinataerUnterstuetzung.objects.filter( + status='ausgezahlt' + ).select_related('konto', 'destinataer') + + if account_filter: + paid_payments_query = paid_payments_query.filter( + konto__kontoname__icontains=account_filter + ) + + paid_payments = paid_payments_query.all() + + self.stdout.write(f"Found {paid_payments.count()} paid payments") + + # Group payments by account + accounts_data = {} + missing_transactions = [] + + for payment in paid_payments: + konto_id = payment.konto.id + if konto_id not in accounts_data: + accounts_data[konto_id] = { + 'konto': payment.konto, + 'payments': [], + 'total_paid': Decimal('0.00'), + 'missing_transactions': [] + } + + accounts_data[konto_id]['payments'].append(payment) + accounts_data[konto_id]['total_paid'] += payment.betrag + + # Check if bank transaction exists for this payment + existing_transaction = BankTransaction.objects.filter( + konto=payment.konto, + betrag=-payment.betrag, # Negative for outgoing payment + kommentare__contains=f'Unterstützung {payment.id}' + ).first() + + if not existing_transaction: + accounts_data[konto_id]['missing_transactions'].append(payment) + missing_transactions.append(payment) + + # Report findings + self.stdout.write("\n📊 ANALYSIS RESULTS:") + self.stdout.write("=" * 50) + + for account_data in accounts_data.values(): + konto = account_data['konto'] + payments_count = len(account_data['payments']) + total_paid = account_data['total_paid'] + missing_count = len(account_data['missing_transactions']) + + self.stdout.write(f"\n🏦 {konto.bank_name} - {konto.kontoname}") + self.stdout.write(f" Current Balance: €{konto.saldo}") + self.stdout.write(f" Paid Payments: {payments_count} (Total: €{total_paid})") + self.stdout.write(f" Missing Transactions: {missing_count}") + + if missing_count > 0: + expected_balance = konto.saldo - sum(p.betrag for p in account_data['missing_transactions']) + self.stdout.write( + self.style.WARNING(f" Expected Balance after fix: €{expected_balance}") + ) + + if not missing_transactions: + self.stdout.write( + self.style.SUCCESS("\n✅ All paid payments have corresponding transactions!") + ) + return + + self.stdout.write(f"\n⚠️ Found {len(missing_transactions)} payments without transactions") + + if not dry_run: + self.stdout.write("\n🔧 CREATING MISSING TRANSACTIONS...") + + created_count = 0 + for payment in missing_transactions: + # Create bank transaction + transaction = BankTransaction.objects.create( + konto=payment.konto, + datum=payment.ausgezahlt_am or payment.faellig_am, + valuta=payment.ausgezahlt_am or payment.faellig_am, + betrag=-payment.betrag, # Negative for outgoing payment + waehrung='EUR', + verwendungszweck=f"Unterstützungszahlung: {payment.beschreibung or payment.destinataer.get_full_name()}", + empfaenger_zahlungspflichtiger=payment.empfaenger_name or payment.destinataer.get_full_name(), + iban_gegenpartei=payment.empfaenger_iban or '', + transaction_type='ueberweisung', + status='verified', + kommentare=f'Nachträglich erstellt für Unterstützung {payment.id} - Zahlung vom {payment.ausgezahlt_am or payment.faellig_am}', + ) + + # Update account balance + payment.konto.saldo -= payment.betrag + payment.konto.saldo_datum = payment.ausgezahlt_am or payment.faellig_am + payment.konto.save() + + created_count += 1 + self.stdout.write( + f" ✅ Created transaction for {payment.destinataer.get_full_name()}: €{payment.betrag}" + ) + + self.stdout.write( + self.style.SUCCESS(f"\n🎉 Successfully created {created_count} transactions and updated account balances!") + ) + + # Show final balances + self.stdout.write("\n📈 UPDATED ACCOUNT BALANCES:") + for account_data in accounts_data.values(): + if account_data['missing_transactions']: + konto = account_data['konto'] + konto.refresh_from_db() # Get updated balance + self.stdout.write(f" {konto.bank_name} - {konto.kontoname}: €{konto.saldo}") + + else: + self.stdout.write("\n📝 DRY RUN - Would create the following transactions:") + for payment in missing_transactions: + self.stdout.write( + f" - {payment.destinataer.get_full_name()}: €{payment.betrag} " + f"on {payment.ausgezahlt_am or payment.faellig_am} " + f"from {payment.konto.kontoname}" + ) \ No newline at end of file diff --git a/app/stiftung/management/commands/reconcile_balances.py b/app/stiftung/management/commands/reconcile_balances.py new file mode 100644 index 0000000..978d50d --- /dev/null +++ b/app/stiftung/management/commands/reconcile_balances.py @@ -0,0 +1,145 @@ +""" +Management command to reconcile account balances with actual transactions. + +This command will: +1. Calculate the correct balance based on all bank transactions +2. Update the account balance to match the calculated balance +3. Show discrepancies between stored and calculated balances + +Usage: + python manage.py reconcile_balances +""" + +from django.core.management.base import BaseCommand +from django.utils import timezone +from decimal import Decimal + +from stiftung.models import StiftungsKonto, BankTransaction + + +class Command(BaseCommand): + help = 'Reconcile account balances with bank transactions' + + def add_arguments(self, parser): + parser.add_argument( + '--dry-run', + action='store_true', + help='Show what would be done without making changes', + ) + parser.add_argument( + '--account', + type=str, + help='Only process specific account (by kontoname)', + ) + + def handle(self, *args, **options): + dry_run = options['dry_run'] + account_filter = options.get('account') + + self.stdout.write( + self.style.SUCCESS('🔍 Reconciling account balances with transactions...') + ) + + if dry_run: + self.stdout.write( + self.style.WARNING('DRY RUN MODE - No changes will be made') + ) + + # Get accounts to process + accounts_query = StiftungsKonto.objects.filter(aktiv=True) + if account_filter: + accounts_query = accounts_query.filter(kontoname__icontains=account_filter) + + accounts = accounts_query.all() + + if not accounts: + self.stdout.write( + self.style.ERROR('No accounts found matching criteria') + ) + return + + self.stdout.write(f"Processing {accounts.count()} account(s)...") + + total_discrepancies = 0 + fixed_accounts = 0 + + for account in accounts: + self.stdout.write(f"\n🏦 {account.bank_name} - {account.kontoname}") + self.stdout.write(f" Stored Balance: €{account.saldo}") + self.stdout.write(f" Last Updated: {account.saldo_datum}") + + # Calculate balance from transactions + transactions = BankTransaction.objects.filter(konto=account).order_by('datum') + calculated_balance = Decimal('0.00') + transaction_count = transactions.count() + + for transaction in transactions: + calculated_balance += transaction.betrag + + self.stdout.write(f" Transactions: {transaction_count}") + self.stdout.write(f" Calculated Balance: €{calculated_balance}") + + discrepancy = account.saldo - calculated_balance + if discrepancy != 0: + total_discrepancies += 1 + self.stdout.write( + self.style.WARNING(f" ⚠️ Discrepancy: €{discrepancy}") + ) + + if not dry_run: + # Update the account balance + old_balance = account.saldo + account.saldo = calculated_balance + + # Update the balance date to the latest transaction date or today + if transactions.exists(): + latest_transaction = transactions.order_by('-datum').first() + account.saldo_datum = latest_transaction.datum + else: + account.saldo_datum = timezone.now().date() + + account.save() + fixed_accounts += 1 + + self.stdout.write( + self.style.SUCCESS( + f" ✅ Updated: €{old_balance} → €{calculated_balance}" + ) + ) + else: + self.stdout.write( + f" 📝 Would update: €{account.saldo} → €{calculated_balance}" + ) + else: + self.stdout.write( + self.style.SUCCESS(" ✅ Balance is correct") + ) + + # Show recent transactions + if transaction_count > 0: + self.stdout.write(" Recent transactions:") + recent = transactions.order_by('-datum')[:3] + for trans in recent: + self.stdout.write( + f" {trans.datum}: €{trans.betrag} - {trans.verwendungszweck[:50]}" + ) + + # Summary + self.stdout.write("\n" + "=" * 50) + if dry_run: + self.stdout.write( + f"📊 Found {total_discrepancies} account(s) with balance discrepancies" + ) + if total_discrepancies > 0: + self.stdout.write(" Run without --dry-run to fix these discrepancies") + else: + if fixed_accounts > 0: + self.stdout.write( + self.style.SUCCESS( + f"🎉 Fixed {fixed_accounts} account balance(s)" + ) + ) + else: + self.stdout.write( + self.style.SUCCESS("✅ All account balances were already correct") + ) \ No newline at end of file diff --git a/app/stiftung/migrations/0038_allow_blank_content.py b/app/stiftung/migrations/0038_allow_blank_content.py new file mode 100644 index 0000000..2657a24 --- /dev/null +++ b/app/stiftung/migrations/0038_allow_blank_content.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.6 on 2025-10-02 20:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('stiftung', '0037_add_geschichte_models'), + ] + + operations = [ + migrations.AlterField( + model_name='geschichteseite', + name='inhalt', + field=models.TextField(blank=True, verbose_name='Inhalt'), + ), + ] diff --git a/app/stiftung/migrations/0039_stiftungskalendereintrag.py b/app/stiftung/migrations/0039_stiftungskalendereintrag.py new file mode 100644 index 0000000..a421bb3 --- /dev/null +++ b/app/stiftung/migrations/0039_stiftungskalendereintrag.py @@ -0,0 +1,41 @@ +# Generated by Django 5.0.6 on 2025-10-04 20:21 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('stiftung', '0038_allow_blank_content'), + ] + + operations = [ + migrations.CreateModel( + name='StiftungsKalenderEintrag', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('titel', models.CharField(max_length=200, verbose_name='Titel')), + ('beschreibung', models.TextField(blank=True, verbose_name='Beschreibung')), + ('datum', models.DateField(verbose_name='Datum')), + ('uhrzeit', models.TimeField(blank=True, null=True, verbose_name='Uhrzeit')), + ('ganztags', models.BooleanField(default=True, verbose_name='Ganztägig')), + ('kategorie', models.CharField(choices=[('termin', 'Termin/Meeting'), ('zahlung', 'Zahlungserinnerung'), ('deadline', 'Frist/Deadline'), ('geburtstag', 'Geburtstag'), ('vertrag', 'Vertrag läuft aus'), ('pruefung', 'Prüfung/Nachweis'), ('sonstiges', 'Sonstiges')], default='termin', max_length=20, verbose_name='Kategorie')), + ('prioritaet', models.CharField(choices=[('niedrig', 'Niedrig'), ('normal', 'Normal'), ('hoch', 'Hoch'), ('kritisch', 'Kritisch')], default='normal', max_length=20, verbose_name='Priorität')), + ('erledigt', models.BooleanField(default=False, verbose_name='Erledigt')), + ('erledigt_am', models.DateTimeField(blank=True, null=True, verbose_name='Erledigt am')), + ('erstellt_von', models.CharField(blank=True, max_length=100, null=True, verbose_name='Erstellt von')), + ('erstellt_am', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')), + ('aktualisiert_am', models.DateTimeField(auto_now=True, verbose_name='Aktualisiert am')), + ('destinataer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='stiftung.destinataer', verbose_name='Bezogener Destinatär')), + ('verpachtung', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='stiftung.landverpachtung', verbose_name='Bezogene Verpachtung')), + ], + options={ + 'verbose_name': 'Kalender Eintrag', + 'verbose_name_plural': 'Kalender Einträge', + 'ordering': ['datum', 'uhrzeit'], + 'indexes': [models.Index(fields=['datum'], name='stiftung_st_datum_9e97ed_idx'), models.Index(fields=['kategorie', 'datum'], name='stiftung_st_kategor_d7c7f9_idx'), models.Index(fields=['erledigt', 'datum'], name='stiftung_st_erledig_115235_idx')], + }, + ), + ] diff --git a/app/stiftung/migrations/0040_add_calendar_model.py b/app/stiftung/migrations/0040_add_calendar_model.py new file mode 100644 index 0000000..fc109c6 --- /dev/null +++ b/app/stiftung/migrations/0040_add_calendar_model.py @@ -0,0 +1,13 @@ +# Generated by Django 5.0.6 on 2025-10-04 20:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('stiftung', '0039_stiftungskalendereintrag'), + ] + + operations = [ + ] diff --git a/app/stiftung/migrations/0041_alter_geschichteseite_inhalt.py b/app/stiftung/migrations/0041_alter_geschichteseite_inhalt.py new file mode 100644 index 0000000..1902341 --- /dev/null +++ b/app/stiftung/migrations/0041_alter_geschichteseite_inhalt.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.6 on 2025-10-04 21:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('stiftung', '0040_add_calendar_model'), + ] + + operations = [ + migrations.AlterField( + model_name='geschichteseite', + name='inhalt', + field=models.TextField(blank=True, help_text='Sie können Markdown verwenden: **fett**, *kursiv*, # Überschriften, [Links](URL), Listen, etc.', verbose_name='Inhalt (Markdown)'), + ), + ] diff --git a/app/stiftung/models.py b/app/stiftung/models.py index 33b8990..e7d708c 100644 --- a/app/stiftung/models.py +++ b/app/stiftung/models.py @@ -2864,7 +2864,11 @@ class GeschichteSeite(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) titel = models.CharField(max_length=200, verbose_name="Titel") slug = models.SlugField(max_length=200, unique=True, verbose_name="URL-Slug") - inhalt = models.TextField(verbose_name="Inhalt") + inhalt = models.TextField( + verbose_name="Inhalt (Markdown)", + blank=True, + help_text="Sie können Markdown verwenden: **fett**, *kursiv*, # Überschriften, [Links](URL), Listen, etc." + ) # Metadata erstellt_am = models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am") @@ -2940,3 +2944,124 @@ class GeschichteBild(models.Model): def __str__(self): return f"{self.titel} ({self.seite.titel})" + + +class StiftungsKalenderEintrag(models.Model): + """Custom calendar events for foundation management""" + + KATEGORIE_CHOICES = [ + ('termin', 'Termin/Meeting'), + ('zahlung', 'Zahlungserinnerung'), + ('deadline', 'Frist/Deadline'), + ('geburtstag', 'Geburtstag'), + ('vertrag', 'Vertrag läuft aus'), + ('pruefung', 'Prüfung/Nachweis'), + ('sonstiges', 'Sonstiges'), + ] + + PRIORITAET_CHOICES = [ + ('niedrig', 'Niedrig'), + ('normal', 'Normal'), + ('hoch', 'Hoch'), + ('kritisch', 'Kritisch'), + ] + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + titel = models.CharField(max_length=200, verbose_name="Titel") + beschreibung = models.TextField(blank=True, verbose_name="Beschreibung") + + # Date and time + datum = models.DateField(verbose_name="Datum") + uhrzeit = models.TimeField(null=True, blank=True, verbose_name="Uhrzeit") + ganztags = models.BooleanField(default=True, verbose_name="Ganztägig") + + # Categorization + kategorie = models.CharField( + max_length=20, + choices=KATEGORIE_CHOICES, + default='termin', + verbose_name="Kategorie" + ) + prioritaet = models.CharField( + max_length=20, + choices=PRIORITAET_CHOICES, + default='normal', + verbose_name="Priorität" + ) + + # Links to related objects + destinataer = models.ForeignKey( + 'Destinataer', + null=True, + blank=True, + on_delete=models.CASCADE, + verbose_name="Bezogener Destinatär" + ) + verpachtung = models.ForeignKey( + 'LandVerpachtung', + null=True, + blank=True, + on_delete=models.CASCADE, + verbose_name="Bezogene Verpachtung" + ) + + # Status and completion + erledigt = models.BooleanField(default=False, verbose_name="Erledigt") + erledigt_am = models.DateTimeField(null=True, blank=True, verbose_name="Erledigt am") + + # Metadata + erstellt_von = models.CharField( + max_length=100, + null=True, + blank=True, + verbose_name="Erstellt von" + ) + erstellt_am = models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am") + aktualisiert_am = models.DateTimeField(auto_now=True, verbose_name="Aktualisiert am") + + class Meta: + verbose_name = "Kalender Eintrag" + verbose_name_plural = "Kalender Einträge" + ordering = ['datum', 'uhrzeit'] + indexes = [ + models.Index(fields=['datum']), + models.Index(fields=['kategorie', 'datum']), + models.Index(fields=['erledigt', 'datum']), + ] + + def __str__(self): + return f"{self.datum}: {self.titel}" + + def get_kategorie_icon(self): + icons = { + 'termin': 'fas fa-calendar-alt', + 'zahlung': 'fas fa-euro-sign', + 'deadline': 'fas fa-exclamation-triangle', + 'geburtstag': 'fas fa-birthday-cake', + 'vertrag': 'fas fa-file-contract', + 'pruefung': 'fas fa-clipboard-check', + 'sonstiges': 'fas fa-calendar', + } + return icons.get(self.kategorie, 'fas fa-calendar') + + def get_prioritaet_color(self): + colors = { + 'niedrig': 'success', + 'normal': 'primary', + 'hoch': 'warning', + 'kritisch': 'danger', + } + return colors.get(self.prioritaet, 'primary') + + def is_overdue(self): + """Check if event is overdue (past due and not completed)""" + if self.erledigt: + return False + return self.datum < timezone.now().date() + + def is_upcoming(self, days=7): + """Check if event is upcoming within specified days""" + if self.erledigt: + return False + today = timezone.now().date() + return today <= self.datum <= (today + timezone.timedelta(days=days)) diff --git a/app/stiftung/services/__init__.py b/app/stiftung/services/__init__.py new file mode 100644 index 0000000..c66a0b2 --- /dev/null +++ b/app/stiftung/services/__init__.py @@ -0,0 +1 @@ +# Services package \ No newline at end of file diff --git a/app/stiftung/services/calendar_service.py b/app/stiftung/services/calendar_service.py new file mode 100644 index 0000000..fe55d16 --- /dev/null +++ b/app/stiftung/services/calendar_service.py @@ -0,0 +1,269 @@ +""" +Calendar service for aggregating date-based events from the foundation management system +""" +from datetime import date, timedelta +import calendar as cal +try: + from django.utils import timezone +except ImportError: + from datetime import datetime as timezone + timezone.now = datetime.now + +try: + from stiftung.models import ( + DestinataerUnterstuetzung, + LandVerpachtung, + Destinataer, + StiftungsKalenderEintrag + ) +except ImportError: + # Fallback for when Django apps aren't ready + DestinataerUnterstuetzung = None + LandVerpachtung = None + Destinataer = None + StiftungsKalenderEintrag = None + + +class StiftungsKalenderService: + """Service to aggregate calendar events from various models""" + + def __init__(self): + self.today = timezone.now().date() + + def get_calendar_events(self, start_date=None, end_date=None): + """ + Get custom calendar entries only within date range + Returns list of event objects + """ + if not start_date: + start_date = self.today + if not end_date: + end_date = start_date + timedelta(days=30) + + return self._get_custom_calendar_events(start_date, end_date) + + def get_all_events(self, start_date=None, end_date=None): + """ + Get all calendar events from all sources within date range + Returns list of event dictionaries + """ + if not start_date: + start_date = self.today + if not end_date: + end_date = start_date + timedelta(days=30) + + events = [] + + # Add support payment due dates + events.extend(self.get_support_payment_events(start_date, end_date)) + + # Add lease expiration dates + events.extend(self.get_lease_events(start_date, end_date)) + + # Add birthdays + events.extend(self.get_birthday_events(start_date, end_date)) + + # Add custom calendar entries + events.extend(self.get_calendar_events(start_date, end_date)) + + # Sort by date + events.sort(key=lambda x: (x.date, getattr(x, 'time', '00:00'))) + + return events + + def get_support_payment_events(self, start_date, end_date): + """Get support payment due dates""" + if not DestinataerUnterstuetzung: + return [] + + events = [] + + payments = DestinataerUnterstuetzung.objects.filter( + faellig_am__range=[start_date, end_date], + status__in=['geplant', 'faellig'] + ).select_related('destinataer') + + for payment in payments: + is_overdue = payment.is_overdue() if hasattr(payment, 'is_overdue') else False + + class PaymentEvent: + def __init__(self, payment_obj, overdue): + self.id = f'payment_{payment_obj.id}' + self.title = f'Zahlung an {payment_obj.destinataer.get_full_name()}' + self.description = f'€{payment_obj.betrag} - {payment_obj.beschreibung}' + self.date = payment_obj.faellig_am + self.time = None + self.category = 'zahlung' + self.category_display = 'Zahlung' + self.priority = 'hoch' if overdue else 'normal' + self.priority_display = 'Hoch' if overdue else 'Normal' + self.icon = 'fas fa-euro-sign' + self.color = 'danger' if overdue else 'warning' + self.source = 'payment' + self.overdue = overdue + self.completed = False + + events.append(PaymentEvent(payment, is_overdue)) + + return events + + def get_lease_events(self, start_date, end_date): + """Get lease start/end dates""" + if not LandVerpachtung: + return [] + + events = [] + + # Lease expirations + leases = LandVerpachtung.objects.filter( + pachtende__range=[start_date, end_date], + status='aktiv' + ).select_related('paechter', 'land') + + for lease in leases: + # Check if expiring soon (within 90 days) + days_until_expiry = (lease.pachtende - self.today).days if lease.pachtende else 999 + priority = 'hoch' if days_until_expiry <= 30 else 'mittel' if days_until_expiry <= 90 else 'normal' + + class LeaseEvent: + def __init__(self, lease_obj, priority_val): + self.id = f'lease_end_{lease_obj.id}' + self.title = f'Pachtende: {lease_obj.paechter.get_full_name()}' + self.description = f'{lease_obj.land.bezeichnung} - {lease_obj.verpachtete_flaeche}m²' + self.date = lease_obj.pachtende + self.time = None + self.category = 'vertrag' + self.category_display = 'Vertrag' + self.priority = priority_val + self.priority_display = 'Hoch' if priority_val == 'hoch' else 'Mittel' if priority_val == 'mittel' else 'Normal' + self.icon = 'fas fa-file-contract' + self.color = 'danger' if priority_val == 'hoch' else 'warning' if priority_val == 'mittel' else 'info' + self.source = 'lease' + self.overdue = False + self.completed = False + + events.append(LeaseEvent(lease, priority)) + + return events + + def get_birthday_events(self, start_date, end_date): + """Get birthdays within date range""" + if not Destinataer: + return [] + + events = [] + + # Get all destinatäre with birth dates + destinataere = Destinataer.objects.filter( + geburtsdatum__isnull=False + ) + + for destinataer in destinataere: + # Calculate birthday for each year in range + birth_date = destinataer.geburtsdatum + for year in range(start_date.year, end_date.year + 1): + try: + birthday_this_year = birth_date.replace(year=year) + if start_date <= birthday_this_year <= end_date: + age = year - birth_date.year + + class BirthdayEvent: + def __init__(self, person, birthday_date, age_val): + self.id = f'birthday_{person.id}_{year}' + self.title = f'🎂 {person.get_full_name()}' + self.description = f'{age_val}. Geburtstag' + self.date = birthday_date + self.time = None + self.category = 'geburtstag' + self.category_display = 'Geburtstag' + self.priority = 'normal' + self.priority_display = 'Normal' + self.icon = 'fas fa-birthday-cake' + self.color = 'success' + self.source = 'birthday' + self.overdue = False + self.completed = False + + events.append(BirthdayEvent(destinataer, birthday_this_year, age)) + except ValueError: + # Handle leap year edge case (Feb 29) + pass + + return events + + def _get_custom_calendar_events(self, start_date, end_date): + """Get custom calendar entries""" + if not StiftungsKalenderEintrag: + return [] + + events = [] + + calendar_entries = StiftungsKalenderEintrag.objects.filter( + datum__range=[start_date, end_date] + ).select_related('destinataer', 'verpachtung') + + for entry in calendar_entries: + class CustomEvent: + def __init__(self, entry_obj, today_date): + self.id = entry_obj.id + self.title = entry_obj.titel + self.description = entry_obj.beschreibung + self.date = entry_obj.datum + self.time = entry_obj.uhrzeit + self.category = entry_obj.kategorie + self.category_display = entry_obj.get_kategorie_display() + self.priority = entry_obj.prioritaet + self.priority_display = entry_obj.get_prioritaet_display() + self.icon = self._get_kategorie_icon(entry_obj.kategorie) + self.color = self._get_prioritaet_color(entry_obj.prioritaet) + self.source = 'custom' + self.completed = entry_obj.erledigt + self.overdue = entry_obj.datum < today_date if entry_obj.datum else False + + def _get_kategorie_icon(self, kategorie): + icons = { + 'termin': 'fas fa-calendar-alt', + 'zahlung': 'fas fa-euro-sign', + 'deadline': 'fas fa-exclamation-triangle', + 'geburtstag': 'fas fa-birthday-cake', + 'vertrag': 'fas fa-file-contract', + 'pruefung': 'fas fa-search', + } + return icons.get(kategorie, 'fas fa-calendar') + + def _get_prioritaet_color(self, prioritaet): + colors = { + 'niedrig': 'secondary', + 'normal': 'primary', + 'mittel': 'warning', + 'hoch': 'danger', + } + return colors.get(prioritaet, 'primary') + + events.append(CustomEvent(entry, self.today)) + + return events + + def get_upcoming_events(self, days=7): + """Get upcoming events within specified days""" + end_date = self.today + timedelta(days=days) + return self.get_calendar_events(self.today, end_date) + + def get_overdue_events(self): + """Get overdue/past due events""" + events = self.get_calendar_events( + start_date=self.today - timedelta(days=30), + end_date=self.today - timedelta(days=1) + ) + + return [event for event in events if event.get('overdue', False)] + + def get_events_for_month(self, year, month): + """Get all events for a specific month""" + from calendar import monthrange + + start_date = date(year, month, 1) + _, last_day = monthrange(year, month) + end_date = date(year, month, last_day) + + return self.get_calendar_events(start_date, end_date) \ No newline at end of file diff --git a/app/stiftung/signals.py b/app/stiftung/signals.py new file mode 100644 index 0000000..f6533be --- /dev/null +++ b/app/stiftung/signals.py @@ -0,0 +1,166 @@ +""" +Django signals for the Stiftung app. +Handles automatic # Check if a bank transaction already exists for this specific payment + existing_transaction = BankTransaction.objects.filter( + konto=instance.konto, + betrag=-instance.betrag, # Negative for outgoing payment + kommentare__contains=f'Unterstützung {instance.id}' + ).first() + + if existing_transaction: + print(f"⚠️ Transaction already exists for payment {instance.id} - skipping creation") + return + + # Create a bank transaction for this payment + BankTransaction.objects.create( + konto=instance.konto, + datum=instance.ausgezahlt_am or timezone.now().date(), + valuta=instance.ausgezahlt_am or timezone.now().date(), + betrag=-instance.betrag, # Negative because it's an outgoing payment + waehrung='EUR', + verwendungszweck=f"Unterstützungszahlung: {instance.beschreibung or instance.destinataer.get_full_name()}", + empfaenger_zahlungspflichtiger=instance.empfaenger_name or instance.destinataer.get_full_name(), + iban_gegenpartei=instance.empfaenger_iban or '', + transaction_type='ueberweisung', + status='verified', + kommentare=f'Automatisch erstellt bei Markierung als ausgezahlt für Unterstützung {instance.id}', + referenz=f'PAY-{instance.id}', # Add unique reference to avoid conflicts + )model instances change. +""" + +from django.db.models.signals import post_save, pre_save +from django.dispatch import receiver +from django.utils import timezone +from decimal import Decimal + +from .models import DestinataerUnterstuetzung, BankTransaction + + +@receiver(pre_save, sender=DestinataerUnterstuetzung) +def unterstuetzung_pre_save(sender, instance, **kwargs): + """Store the old status before saving to detect status changes""" + if instance.pk: + try: + old_instance = DestinataerUnterstuetzung.objects.get(pk=instance.pk) + instance._old_status = old_instance.status + except DestinataerUnterstuetzung.DoesNotExist: + instance._old_status = None + else: + instance._old_status = None + + +@receiver(post_save, sender=DestinataerUnterstuetzung) +def update_account_balance_on_payment(sender, instance, created, **kwargs): + """ + Update account balance when a payment is marked as paid (ausgezahlt). + Creates a corresponding bank transaction and updates the account balance. + Prevents duplicate transactions by checking if one already exists. + """ + # Only process if payment was just marked as paid + old_status = getattr(instance, '_old_status', None) + + if instance.status == 'ausgezahlt' and old_status != 'ausgezahlt': + # Payment was just marked as paid + + # Check if a transaction already exists for this payment to prevent duplicates + existing_transaction = BankTransaction.objects.filter( + konto=instance.konto, + betrag=-instance.betrag, # Negative for outgoing payment + kommentare__contains=f'Unterstützung {instance.id}' + ).first() + + if existing_transaction: + print(f"⚠️ Transaction already exists for payment {instance.id} to {instance.destinataer.get_full_name()}, skipping duplicate") + return + + # Set the ausgezahlt_am date if not already set + if not instance.ausgezahlt_am: + instance.ausgezahlt_am = timezone.now().date() + # Avoid infinite recursion by updating without triggering signals + DestinataerUnterstuetzung.objects.filter(pk=instance.pk).update( + ausgezahlt_am=instance.ausgezahlt_am + ) + + # Check if a transaction already exists for this payment + existing_transaction = BankTransaction.objects.filter( + kommentare__contains=f'Unterstützung {instance.id}' + ).first() + + if not existing_transaction: + # Create a bank transaction for this payment + transaction = BankTransaction.objects.create( + konto=instance.konto, + datum=instance.ausgezahlt_am or timezone.now().date(), + valuta=instance.ausgezahlt_am or timezone.now().date(), + betrag=-instance.betrag, # Negative because it's an outgoing payment + waehrung='EUR', + verwendungszweck=f"Unterstützungszahlung: {instance.beschreibung or instance.destinataer.get_full_name()}", + empfaenger_zahlungspflichtiger=instance.empfaenger_name or instance.destinataer.get_full_name(), + iban_gegenpartei=instance.empfaenger_iban or '', + transaction_type='ueberweisung', + status='verified', + referenz=f'PAY-{instance.id}', # Unique reference to prevent duplicates + kommentare=f'Automatisch erstellt bei Markierung als ausgezahlt für Unterstützung {instance.id}', + ) + # Update account balance only for new transactions + instance.konto.saldo -= instance.betrag + instance.konto.saldo_datum = instance.ausgezahlt_am or timezone.now().date() + instance.konto.save() + print(f"✅ Account balance updated: {instance.konto.kontoname} - €{instance.betrag} (Payment to {instance.destinataer.get_full_name()}) - Transaction {transaction.id}") + else: + transaction = existing_transaction + print(f"ℹ️ Transaction already exists for payment {instance.id}, balance not modified") + + # Handle reversal if payment is changed from paid back to unpaid + elif old_status == 'ausgezahlt' and instance.status != 'ausgezahlt': + # Payment was unmarked as paid - reverse the transaction + + # Find and delete the corresponding bank transaction + try: + # Look for the transaction created for this payment + transaction = BankTransaction.objects.filter( + konto=instance.konto, + betrag=-instance.betrag, + kommentare__contains=f'Unterstützung {instance.id}' + ).first() + + if transaction: + transaction.delete() + + # Reverse the account balance update + instance.konto.saldo += instance.betrag + instance.konto.saldo_datum = timezone.now().date() + instance.konto.save() + + print(f"✅ Account balance reversed: {instance.konto.kontoname} + €{instance.betrag} (Payment reversal for {instance.destinataer.get_full_name()})") + else: + print(f"⚠️ No transaction found to reverse for payment {instance.id}") + + except Exception as e: + print(f"⚠️ Error reversing payment transaction: {e}") + + # Clear the ausgezahlt_am date + if instance.ausgezahlt_am: + # Update without triggering signals + DestinataerUnterstuetzung.objects.filter(pk=instance.pk).update( + ausgezahlt_am=None + ) + + +@receiver(post_save, sender=BankTransaction) +def update_account_balance_on_transaction(sender, instance, created, **kwargs): + """ + Update account balance when a new bank transaction is imported or created. + Only update if the transaction has a saldo_nach_buchung value or if it's manually created. + """ + if created and instance.status in ['verified', 'imported']: + # If the transaction has a balance after booking, use that + if instance.saldo_nach_buchung is not None: + instance.konto.saldo = instance.saldo_nach_buchung + instance.konto.saldo_datum = instance.datum + instance.konto.save() + else: + # Otherwise, calculate the new balance + instance.konto.saldo += instance.betrag + instance.konto.saldo_datum = instance.datum + instance.konto.save() \ No newline at end of file diff --git a/app/stiftung/templatetags/help_tags.py b/app/stiftung/templatetags/help_tags.py index 172d8d2..74661a3 100644 --- a/app/stiftung/templatetags/help_tags.py +++ b/app/stiftung/templatetags/help_tags.py @@ -32,3 +32,19 @@ def help_box(page_key, user=None): def help_box_exists(page_key): """Prüfe, ob eine Hilfs-Infobox für eine Seite existiert""" return HelpBox.get_help_for_page(page_key) is not None + + +@register.filter +def markdown_to_html(text): + """Konvertiere Markdown-Text zu HTML""" + if not text: + return "" + + md = markdown.Markdown(extensions=[ + "nl2br", + "fenced_code", + "tables", + "toc", + "codehilite" + ]) + return mark_safe(md.convert(text)) diff --git a/app/stiftung/urls.py b/app/stiftung/urls.py index f0405c3..ba1fb51 100644 --- a/app/stiftung/urls.py +++ b/app/stiftung/urls.py @@ -5,10 +5,10 @@ from . import views app_name = "stiftung" urlpatterns = [ - # Dashboard (Startseite) - path("", views.dashboard, name="dashboard"), - # Home (für Kompatibilität mit bestehenden Templates) - path("home/", views.home, name="home"), + # Home - Main landing page after login + path("", views.home, name="home"), + # Dashboard (detailed view) + path("dashboard/", views.dashboard, name="dashboard"), # CSV Import URLs path("import/", views.csv_import_list, name="csv_import_list"), path("import/neu/", views.csv_import_create, name="csv_import_create"), @@ -391,4 +391,14 @@ urlpatterns = [ path("geschichte//", views.geschichte_detail, name="geschichte_detail"), path("geschichte//bearbeiten/", views.geschichte_edit, name="geschichte_edit"), path("geschichte//bild-upload/", views.geschichte_bild_upload, name="geschichte_bild_upload"), + path("geschichte//bild//loeschen/", views.geschichte_bild_delete, name="geschichte_bild_delete"), + + # Kalender URLs + path("kalender/", views.kalender_view, name="kalender"), + path("kalender/admin/", views.kalender_admin, name="kalender_admin"), + path("kalender/neu/", views.kalender_create, name="kalender_create"), + path("kalender//", views.kalender_detail, name="kalender_detail"), + path("kalender//bearbeiten/", views.kalender_edit, name="kalender_edit"), + path("kalender//loeschen/", views.kalender_delete, name="kalender_delete"), + path("kalender/api/events/", views.kalender_api_events, name="kalender_api_events"), ] diff --git a/app/stiftung/views.py b/app/stiftung/views.py index ad1efd8..1fcfbe0 100644 --- a/app/stiftung/views.py +++ b/app/stiftung/views.py @@ -3,7 +3,7 @@ import io import json import os import time -from datetime import datetime, timedelta +from datetime import datetime, timedelta, date from decimal import Decimal import qrcode @@ -225,11 +225,38 @@ from .forms import (DestinataerForm, DestinataerNotizForm, def home(request): """Home page for the Stiftungsverwaltung application""" - return render( - request, - "stiftung/home.html", - {"title": "Stiftungsverwaltung", "description": "Foundation Management System"}, - ) + from stiftung.services.calendar_service import StiftungsKalenderService + + # Get upcoming events for the calendar widget + calendar_service = StiftungsKalenderService() + + # Get all events for the next 14 days + from datetime import timedelta + today = timezone.now().date() + end_date = today + timedelta(days=14) + all_events = calendar_service.get_all_events(today, end_date) + + # Filter for upcoming and overdue + upcoming_events = [e for e in all_events if not getattr(e, 'overdue', False)] + overdue_events = [e for e in all_events if getattr(e, 'overdue', False)] + + # Get current month events for mini calendar + from calendar import monthrange + _, last_day = monthrange(today.year, today.month) + month_start = today.replace(day=1) + month_end = today.replace(day=last_day) + current_month_events = calendar_service.get_all_events(month_start, month_end) + + context = { + "title": "Stiftungsverwaltung", + "description": "Foundation Management System", + "upcoming_events": upcoming_events[:5], # Show only 5 upcoming events + "overdue_events": overdue_events[:3], # Show only 3 overdue events + "current_month_events": current_month_events, + "today": today, + } + + return render(request, "stiftung/home.html", context) @login_required @@ -8152,3 +8179,412 @@ def geschichte_bild_upload(request, slug): } return render(request, 'stiftung/geschichte/bild_form.html', context) + + +@login_required +def geschichte_bild_delete(request, slug, bild_id): + """Delete an image from a history page""" + seite = get_object_or_404(GeschichteSeite, slug=slug) + bild = get_object_or_404(GeschichteBild, id=bild_id, seite=seite) + + if not request.user.has_perm('stiftung.delete_geschichtebild'): + messages.error(request, 'Sie haben keine Berechtigung, Bilder zu löschen.') + return redirect('stiftung:geschichte_detail', slug=slug) + + if request.method == 'POST': + bild_titel = bild.titel + bild.delete() + messages.success(request, f'Bild "{bild_titel}" wurde erfolgreich gelöscht.') + return redirect('stiftung:geschichte_detail', slug=slug) + + context = { + 'bild': bild, + 'seite': seite, + 'title': f'Bild löschen: {bild.titel}' + } + + return render(request, 'stiftung/geschichte/bild_delete.html', context) + + +# Calendar Views +@login_required +def kalender_view(request): + """Main calendar view with different view types""" + from stiftung.services.calendar_service import StiftungsKalenderService + import calendar as cal + + calendar_service = StiftungsKalenderService() + + # Get current date and view parameters + today = timezone.now().date() + view_type = request.GET.get('view', 'month') # month, week, list, agenda + year = int(request.GET.get('year', today.year)) + month = int(request.GET.get('month', today.month)) + + # Calculate date ranges based on view type + if view_type == 'month': + # Get events for the entire month + start_date = date(year, month, 1) + _, last_day = cal.monthrange(year, month) + end_date = date(year, month, last_day) + title_suffix = f"{cal.month_name[month]} {year}" + + elif view_type == 'week': + # Get current week + week_start = today - timedelta(days=today.weekday()) + start_date = week_start + end_date = week_start + timedelta(days=6) + title_suffix = f"Woche vom {start_date.strftime('%d.%m')} - {end_date.strftime('%d.%m.%Y')}" + + elif view_type == 'agenda': + # Next 30 days + start_date = today + end_date = today + timedelta(days=30) + title_suffix = "Nächste 30 Tage" + + else: # list view + # Next 90 days + start_date = today + end_date = today + timedelta(days=90) + title_suffix = "Liste (nächste 90 Tage)" + + # Get events for the date range + events = calendar_service.get_all_events(start_date, end_date) + + # Generate calendar grid for month view + calendar_grid = None + if view_type == 'month': + calendar_grid = [] + first_day = date(year, month, 1) + month_cal = cal.monthcalendar(year, month) + + for week in month_cal: + week_data = [] + for day in week: + if day == 0: + week_data.append(None) + else: + day_date = date(year, month, day) + day_events = [e for e in events if e.date == day_date] + week_data.append({ + 'day': day, + 'date': day_date, + 'is_today': day_date == today, + 'events': day_events[:3], # Show max 3 events per day + 'event_count': len(day_events) + }) + calendar_grid.append(week_data) + + # Navigation dates for month view + if month > 1: + prev_month = month - 1 + prev_year = year + else: + prev_month = 12 + prev_year = year - 1 + + if month < 12: + next_month = month + 1 + next_year = year + else: + next_month = 1 + next_year = year + 1 + + context = { + 'title': f'Kalender - {title_suffix}', + 'events': events, + 'calendar_grid': calendar_grid, + 'view_type': view_type, + 'year': year, + 'month': month, + 'today': today, + 'start_date': start_date, + 'end_date': end_date, + 'prev_year': prev_year, + 'prev_month': prev_month, + 'next_year': next_year, + 'next_month': next_month, + 'month_name': cal.month_name[month], + 'weekdays': ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'], + } + + # Choose template based on view type + if view_type == 'month': + template = 'stiftung/kalender/month_view.html' + elif view_type == 'week': + template = 'stiftung/kalender/week_view.html' + elif view_type == 'agenda': + template = 'stiftung/kalender/agenda_view.html' + else: + template = 'stiftung/kalender/list_view.html' + + return render(request, template, context) + + +@login_required +def kalender_create(request): + """Create new calendar event""" + from stiftung.models import StiftungsKalenderEintrag + + if request.method == 'POST': + # Simple form handling - you can enhance this with Django forms + titel = request.POST.get('titel') + beschreibung = request.POST.get('beschreibung', '') + datum = request.POST.get('datum') + kategorie = request.POST.get('kategorie', 'termin') + prioritaet = request.POST.get('prioritaet', 'normal') + + if titel and datum: + zeit_str = request.POST.get('zeit') + uhrzeit = zeit_str if zeit_str else None + ganztags = not bool(zeit_str) + + StiftungsKalenderEintrag.objects.create( + titel=titel, + beschreibung=beschreibung, + datum=datum, + uhrzeit=uhrzeit, + ganztags=ganztags, + kategorie=kategorie, + prioritaet=prioritaet, + erstellt_von=request.user.username + ) + messages.success(request, 'Kalendereintrag wurde erfolgreich erstellt.') + return redirect('stiftung:kalender') + else: + messages.error(request, 'Titel und Datum sind erforderlich.') + + context = { + 'title': 'Neuer Kalendereintrag', + } + + return render(request, 'stiftung/kalender/create.html', context) + + +@login_required +def kalender_detail(request, pk): + """Calendar event detail view""" + from stiftung.models import StiftungsKalenderEintrag + + event = get_object_or_404(StiftungsKalenderEintrag, pk=pk) + + context = { + 'title': f'Kalendereintrag: {event.titel}', + 'event': event, + } + + return render(request, 'stiftung/kalender/detail.html', context) + + +@login_required +def kalender_edit(request, pk): + """Edit calendar event""" + from stiftung.models import StiftungsKalenderEintrag + + event = get_object_or_404(StiftungsKalenderEintrag, pk=pk) + + if request.method == 'POST': + event.titel = request.POST.get('titel', event.titel) + event.beschreibung = request.POST.get('beschreibung', event.beschreibung) + event.datum = request.POST.get('datum', event.datum) + zeit_str = request.POST.get('zeit') + if zeit_str: + event.uhrzeit = zeit_str + event.ganztags = False + else: + event.uhrzeit = None + event.ganztags = True + event.kategorie = request.POST.get('kategorie', event.kategorie) + event.prioritaet = request.POST.get('prioritaet', event.prioritaet) + event.erledigt = 'erledigt' in request.POST + + event.save() + messages.success(request, 'Kalendereintrag wurde aktualisiert.') + return redirect('stiftung:kalender_detail', pk=pk) + + context = { + 'title': f'Bearbeiten: {event.titel}', + 'event': event, + } + + return render(request, 'stiftung/kalender/edit.html', context) + + +@login_required +def kalender_delete(request, pk): + """Delete calendar event""" + from stiftung.models import StiftungsKalenderEintrag + + event = get_object_or_404(StiftungsKalenderEintrag, pk=pk) + + if request.method == 'POST': + event_titel = event.titel + event.delete() + messages.success(request, f'Kalendereintrag "{event_titel}" wurde gelöscht.') + return redirect('stiftung:kalender') + + context = { + 'title': f'Löschen: {event.titel}', + 'event': event, + } + + return render(request, 'stiftung/kalender/delete_confirm.html', context) + + +@login_required +def kalender_admin(request): + """Calendar administration with event sources and management""" + from stiftung.models import StiftungsKalenderEintrag + from stiftung.services.calendar_service import StiftungsKalenderService + + # Get filter parameters + show_custom = request.GET.get('show_custom', 'true') == 'true' + show_payments = request.GET.get('show_payments', 'true') == 'true' + show_leases = request.GET.get('show_leases', 'true') == 'true' + show_birthdays = request.GET.get('show_birthdays', 'true') == 'true' + category_filter = request.GET.get('category', '') + priority_filter = request.GET.get('priority', '') + + # Initialize calendar service + calendar_service = StiftungsKalenderService() + + # Get events based on filters + from datetime import date, timedelta + start_date = date.today() - timedelta(days=30) + end_date = date.today() + timedelta(days=90) + + all_events = [] + + # Custom calendar entries + if show_custom: + custom_events = calendar_service.get_calendar_events(start_date, end_date) + all_events.extend(custom_events) + + # Payment events + if show_payments: + payment_events = calendar_service.get_support_payment_events(start_date, end_date) + all_events.extend(payment_events) + + # Lease events + if show_leases: + lease_events = calendar_service.get_lease_events(start_date, end_date) + all_events.extend(lease_events) + + # Birthday events + if show_birthdays: + birthday_events = calendar_service.get_birthday_events(start_date, end_date) + all_events.extend(birthday_events) + + # Filter by category and priority if specified + if category_filter: + all_events = [e for e in all_events if getattr(e, 'category', '') == category_filter] + + if priority_filter: + all_events = [e for e in all_events if getattr(e, 'priority', '') == priority_filter] + + # Sort events by date + all_events.sort(key=lambda x: x.date) + + # Get statistics + custom_count = StiftungsKalenderEintrag.objects.count() + total_events = len(all_events) + + # Event source statistics + stats = { + 'custom_events': len([e for e in all_events if getattr(e, 'source', '') == 'custom']), + 'payment_events': len([e for e in all_events if getattr(e, 'source', '') == 'payment']), + 'lease_events': len([e for e in all_events if getattr(e, 'source', '') == 'lease']), + 'birthday_events': len([e for e in all_events if getattr(e, 'source', '') == 'birthday']), + 'total_events': total_events, + 'custom_count': custom_count, + } + + context = { + 'title': 'Kalender Administration', + 'events': all_events, + 'stats': stats, + 'show_custom': show_custom, + 'show_payments': show_payments, + 'show_leases': show_leases, + 'show_birthdays': show_birthdays, + 'category_filter': category_filter, + 'priority_filter': priority_filter, + 'categories': StiftungsKalenderEintrag.KATEGORIE_CHOICES, + 'priorities': StiftungsKalenderEintrag.PRIORITAET_CHOICES, + } + + return render(request, 'stiftung/kalender/admin.html', context) + + context = { + 'title': f'Löschen: {event.titel}', + 'event': event, + } + + return render(request, 'stiftung/kalender/delete.html', context) + + +@login_required +def kalender_api_events(request): + """API endpoint for calendar events (JSON)""" + from django.http import JsonResponse + from stiftung.services.calendar_service import StiftungsKalenderService + from datetime import datetime + + calendar_service = StiftungsKalenderService() + + # Get date range from request + start_date = request.GET.get('start') + end_date = request.GET.get('end') + + if start_date and end_date: + try: + start_date = datetime.strptime(start_date, '%Y-%m-%d').date() + end_date = datetime.strptime(end_date, '%Y-%m-%d').date() + except ValueError: + return JsonResponse({'error': 'Invalid date format'}, status=400) + + events = calendar_service.get_all_events(start_date, end_date) + else: + events = calendar_service.get_all_events() + + # Convert to FullCalendar format + calendar_events = [] + for event in events: + calendar_events.append({ + 'id': getattr(event, 'id', str(event.title)), + 'title': event.title, + 'start': event.date.strftime('%Y-%m-%d'), + 'description': getattr(event, 'description', ''), + 'className': f"event-{event.category}", + 'backgroundColor': f"var(--bs-{event.color})", + 'borderColor': f"var(--bs-{event.color})", + }) + + return JsonResponse(calendar_events, safe=False) + + +# Calendar Views +@login_required +def kalender_view(request): + """Full calendar view with all events""" + from stiftung.services.calendar_service import StiftungsKalenderService + + calendar_service = StiftungsKalenderService() + + # Get current month events by default + today = timezone.now().date() + events = calendar_service.get_events_for_month(today.year, today.month) + + context = { + 'events': events, + 'title': 'Stiftungskalender', + 'current_month': today.strftime('%B %Y'), + } + + return render(request, 'stiftung/kalender/kalender.html', context) + + + context = { + 'title': 'Kalendereintrag löschen' + } + return render(request, 'stiftung/kalender/delete.html', context) diff --git a/app/templates/base.html b/app/templates/base.html index a584f20..f07d309 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -525,14 +525,19 @@ + + + + @@ -619,11 +649,6 @@ Geschichte - {% if user.is_authenticated %} @@ -672,9 +697,10 @@ {% endif %} - + - + +
diff --git a/app/templates/stiftung/bericht_list.html b/app/templates/stiftung/bericht_list.html index 33699e7..955e20f 100644 --- a/app/templates/stiftung/bericht_list.html +++ b/app/templates/stiftung/bericht_list.html @@ -24,7 +24,7 @@

- Generieren Sie detaillierte Jahresberichte mit allen wichtigen Informationen zu Personen, + Generieren Sie detaillierte Jahresberichte mit allen wichtigen Informationen zu Destinatären, Förderungen und Ländereien.

@@ -83,8 +83,8 @@
-
Personen
-

{{ total_persons|default:"0" }}

+
Destinatäre
+

{{ total_destinataere|default:"0" }}

@@ -129,8 +129,8 @@
Schnellzugriff:
- - Alle Personen anzeigen + + Alle Destinatäre anzeigen Alle Förderungen anzeigen diff --git a/app/templates/stiftung/geschichte/bild_delete.html b/app/templates/stiftung/geschichte/bild_delete.html new file mode 100644 index 0000000..aafe071 --- /dev/null +++ b/app/templates/stiftung/geschichte/bild_delete.html @@ -0,0 +1,66 @@ +{% extends 'base.html' %} +{% load help_tags %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} + + +
+
+
+
+
+ {{ title }} +
+
+
+
+ + Achtung! Dieser Vorgang kann nicht rückgängig gemacht werden. +
+ +
+
+ {{ bild.alt_text }} +
+
+
Bild-Details:
+
    +
  • Titel: {{ bild.titel }}
  • +
  • Beschreibung: {{ bild.beschreibung|default:"Keine Beschreibung" }}
  • +
  • Hochgeladen: {{ bild.hochgeladen_am|date:"d.m.Y H:i" }}
  • + {% if bild.hochgeladen_von %} +
  • Hochgeladen von: {{ bild.hochgeladen_von.get_full_name|default:bild.hochgeladen_von.username }}
  • + {% endif %} +
+
+
+ +
+ +

Möchten Sie dieses Bild wirklich löschen?

+ +
+ {% csrf_token %} +
+ + Abbrechen + + +
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/stiftung/geschichte/detail.html b/app/templates/stiftung/geschichte/detail.html index f07c226..159a3bb 100644 --- a/app/templates/stiftung/geschichte/detail.html +++ b/app/templates/stiftung/geschichte/detail.html @@ -67,7 +67,7 @@
- {{ seite.inhalt|linebreaks }} + {{ seite.inhalt|markdown_to_html }}
{% if bilder %} @@ -75,13 +75,22 @@

Bildergalerie

{% for bild in bilder %}
- {{ bild.alt_text|default:bild.titel }} -
- {{ bild.titel }} - {% if bild.beschreibung %} -
{{ bild.beschreibung }} +
+
{{ bild.titel }}
+ {% if perms.stiftung.delete_geschichtebild %} + + + {% endif %}
+ {{ bild.alt_text|default:bild.titel }} + {% if bild.beschreibung %} +
+ {{ bild.beschreibung }} +
+ {% endif %}
{% endfor %}
diff --git a/app/templates/stiftung/geschichte/form.html b/app/templates/stiftung/geschichte/form.html index 26c5521..5e7974f 100644 --- a/app/templates/stiftung/geschichte/form.html +++ b/app/templates/stiftung/geschichte/form.html @@ -4,19 +4,37 @@ {% block title %}{{ title }}{% endblock %} {% block extra_css %} - + {% endblock %} @@ -75,13 +93,37 @@
- -
- + + + + {{ form.inhalt }} + {% if form.inhalt.errors %}
{{ form.inhalt.errors }}
{% endif %}
{{ form.inhalt.help_text }}
+ +
+
Markdown-Hilfe
+
+
+ + Formatierung:
+ **Fett**Fett
+ *Kursiv*Kursiv
+ ~~Durchgestrichen~~Durchgestrichen +
+
+
+ + Überschriften:
+ # Überschrift 1
+ ## Überschrift 2
+ ### Überschrift 3 +
+
+
+
@@ -110,18 +152,26 @@
-
Rich Text Editor
+
Markdown Editor
-

Der Editor unterstützt:

+

Der Markdown-Editor unterstützt:

    -
  • Formatierung (Fett, Kursiv, Unterstrichen)
  • -
  • Überschriften (H1, H2, H3)
  • -
  • Listen (nummeriert und Aufzählungen)
  • -
  • Links
  • -
  • Bilder (über separaten Upload)
  • +
  • Live-Vorschau beim Schreiben
  • +
  • Toolbar für häufige Formatierungen
  • +
  • Vollbild-Modus für fokussiertes Schreiben
  • +
  • Automatisches Speichern im Browser

+
Häufige Markdown-Syntax:
+
+ - Listen-Element
+ 1. Nummerierte Liste
+ [Link Text](URL)
+ > Zitat
+ `Code` +
+

Tipp: Verwenden Sie die Bild-Upload-Funktion, um Bilder zur Seite hinzuzufügen.

@@ -132,46 +182,88 @@ {% endblock %} {% block extra_js %} - + {% endblock %} \ No newline at end of file diff --git a/app/templates/stiftung/home.html b/app/templates/stiftung/home.html index 1aba82d..ec3a9a1 100644 --- a/app/templates/stiftung/home.html +++ b/app/templates/stiftung/home.html @@ -8,6 +8,18 @@
+ +
+
+
+ +
Logo hier
einfügen
+
+
+ + +
+

van Hees-Theyssen-Vogel'sche Stiftung

@@ -40,10 +52,10 @@
- -
💰 Förderungsverwaltung
-

Erfassen und verfolgen Sie Förderungen, Beträge und Verwendungsnachweise systematisch.

- + +
💰 Unterstützungsverwaltung
+

Erfassen und verfolgen Sie Unterstützungen, Beträge und Verwendungsnachweise systematisch.

+
Öffnen
@@ -64,6 +76,109 @@
+ +
+
+
+
+
+ Anstehende Termine & Ereignisse +
+
+
+ {% if overdue_events %} + + {% endif %} + + {% if upcoming_events %} +
Nächste {{ upcoming_events|length }} Termine
+ {% for event in upcoming_events %} +
+
+ + {{ event.title }} + {% if event.description %} + {{ event.description }} + {% endif %} +
+
+ {{ event.date }} + {% if event.time %} + {{ event.time }} + {% endif %} +
+
+ {% empty %} +
+ +

Keine anstehenden Termine in den nächsten 14 Tagen.

+
+ {% endfor %} + + + {% endif %} +
+
+
+ +
+ +
+
+
+ {{ today|date:"F Y" }} +
+
+
+
+ +
+ +
+
+
+
+ + + +
+
+
@@ -97,7 +212,7 @@
🤝 Verpachtungsverwaltung

Organisieren Sie Pachtverträge und deren Verwaltung effizient.

- + Öffnen
@@ -176,4 +291,165 @@
+{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block extra_js %} + + + {% endblock %} diff --git a/app/templates/stiftung/kalender/admin.html b/app/templates/stiftung/kalender/admin.html new file mode 100644 index 0000000..aaa2e67 --- /dev/null +++ b/app/templates/stiftung/kalender/admin.html @@ -0,0 +1,346 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+ + + + +
+
+
+
+
+
+
+ Benutzerdefinierte Termine +
+
{{ stats.custom_count }}
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ Zahlungsereignisse +
+
{{ stats.payment_events }}
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ Pachtverträge +
+
{{ stats.lease_events }}
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ Geburtstage +
+
{{ stats.birthday_events }}
+
+
+ +
+
+
+
+
+
+ + +
+
+
+
+
+ Ereignisquellen & Filter +
+
+
+
+ +
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+ + +
+ +
+ + +
+ +
+ +
+ + + Zurücksetzen + +
+
+
+
+
+
+
+ + +
+
+
+
+
+ Ereignisse ({{ stats.total_events }} Einträge) +
+
+
+ {% if events %} +
+ + + + + + + + + + + + + + {% for event in events %} + + + + + + + + + + {% endfor %} + +
DatumTitelKategoriePrioritätQuelleStatusAktionen
+ {{ event.date|date:"d.m.Y" }} + {% if event.time %} +
{{ event.time|time:"H:i" }} + {% endif %} +
+ {{ event.title }} + {% if event.description %} +
{{ event.description|truncatechars:50 }} + {% endif %} +
+ {{ event.category_display }} + + + {% if event.priority == 'hoch' %} + + {% elif event.priority == 'mittel' %} + + {% else %} + + {% endif %} + {{ event.priority_display|default:"Normal" }} + + + {% if event.source == 'custom' %} + Benutzerdefiniert + {% elif event.source == 'payment' %} + Zahlung + {% elif event.source == 'lease' %} + Pacht + {% elif event.source == 'birthday' %} + Geburtstag + {% else %} + System + {% endif %} + + {% if event.source == 'custom' %} + {% if event.completed %} + + Erledigt + + {% else %} + + Ausstehend + + {% endif %} + {% else %} + + Automatisch + + {% endif %} + + {% if event.source == 'custom' and event.id %} + + {% else %} + + Systemereignis + + {% endif %} +
+
+ {% else %} +
+ +
Keine Ereignisse gefunden
+

+ Aktivieren Sie Ereignisquellen oder fügen Sie neue Termine hinzu. +

+ + Neuen Termin hinzufügen + +
+ {% endif %} +
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/app/templates/stiftung/kalender/agenda_view.html b/app/templates/stiftung/kalender/agenda_view.html new file mode 100644 index 0000000..345f80a --- /dev/null +++ b/app/templates/stiftung/kalender/agenda_view.html @@ -0,0 +1,337 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+ + + + +
+
+
+
+
+
+ Agenda - Chronologische Übersicht +
+
+ + +
+
+
+
+ {% if events %} +
+ {% regroup events by date as events_by_date %} + {% for date_group in events_by_date %} +
+
+ +
+
{{ date_group.grouper|date:"d" }}
+
+ {{ date_group.grouper|date:"M Y" }} +
+
{{ date_group.grouper|date:"l" }}
+
+ +
+ +
+ {% for event in date_group.list %} +
+
+ {% if event.time %} +
{{ event.time|time:"H:i" }}
+ {% else %} +
+ + ganztags +
+ {% endif %} +
+ +
+
+
+ {{ event.title }} + {% if event.priority == 'hoch' %} + + + + {% endif %} +
+
+ {{ event.category_display }} + {% if event.destinataer %} + + {{ event.destinataer }} + + {% endif %} +
+
+ + {% if event.description %} +
+ {{ event.description }} +
+ {% endif %} + + {% if event.location %} +
+ {{ event.location }} +
+ {% endif %} + + +
+
+ {% endfor %} +
+
+ {% endfor %} +
+ {% else %} +
+ +
Keine Termine gefunden
+

Fügen Sie einen neuen Termin hinzu oder erweitern Sie den Zeitraum.

+ + Termin hinzufügen + +
+ {% endif %} +
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/app/templates/stiftung/kalender/create.html b/app/templates/stiftung/kalender/create.html new file mode 100644 index 0000000..d9e55f8 --- /dev/null +++ b/app/templates/stiftung/kalender/create.html @@ -0,0 +1,125 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+
+

+ {{ title }} +

+
+ +
+
+
+
+
+ {% csrf_token %} + +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + + Leer lassen für ganztägig +
+
+
+ +
+ + +
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+ + Abbrechen + + +
+
+
+
+
+ +
+
+
+
+ Hilfe +
+

+ Erstellen Sie hier neue Kalenderereignisse für wichtige Termine, + Zahlungserinnerungen oder andere stiftungsbezogene Ereignisse. +

+
+
Kategorien:
+
    +
  • Termin: Meetings, Besprechungen
  • +
  • Zahlung: Fällige Unterstützungen
  • +
  • Deadline: Fristen für Nachweise
  • +
  • Vertrag: Auslaufende Pachtverträge
  • +
+
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/app/templates/stiftung/kalender/delete.html b/app/templates/stiftung/kalender/delete.html new file mode 100644 index 0000000..ce6bdbd --- /dev/null +++ b/app/templates/stiftung/kalender/delete.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+
+
+

{{ title }}

+
+
+ +
+
+
+
+ + + +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/stiftung/kalender/delete_confirm.html b/app/templates/stiftung/kalender/delete_confirm.html new file mode 100644 index 0000000..e9c78e5 --- /dev/null +++ b/app/templates/stiftung/kalender/delete_confirm.html @@ -0,0 +1,108 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+ +
+

+ {{ title }} +

+ +
+ + +
+
+
+
+
+ Termin löschen - Bestätigung erforderlich +
+
+
+ + +

+ Sind Sie sicher, dass Sie den folgenden Kalendereintrag permanent löschen möchten? +

+ + +
+
Zu löschender Termin:
+ +
+
Titel:
+
{{ event.titel }}
+
+ +
+
Datum:
+
+ {{ event.datum|date:"d.m.Y" }} + {% if event.uhrzeit %}um {{ event.uhrzeit|time:"H:i" }} Uhr{% endif %} +
+
+ +
+
Kategorie:
+
+ {{ event.get_kategorie_display }} +
+
+ + {% if event.beschreibung %} +
+
Beschreibung:
+
{{ event.beschreibung|truncatechars:100 }}
+
+ {% endif %} + +
+
Erstellt:
+
+ {{ event.erstellt_am|date:"d.m.Y H:i" }} + {% if event.erstellt_von %}von {{ event.erstellt_von }}{% endif %} +
+
+
+ +
+ {% csrf_token %} + + + Abbrechen + + + +
+
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/app/templates/stiftung/kalender/detail.html b/app/templates/stiftung/kalender/detail.html new file mode 100644 index 0000000..edf70a8 --- /dev/null +++ b/app/templates/stiftung/kalender/detail.html @@ -0,0 +1,167 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+ + + + +
+
+
+
+
+ Termindetails +
+
+
+
+
+
Titel
+

{{ event.titel }}

+
+
+
Datum
+

+ {{ event.datum|date:"d.m.Y" }} + {% if event.uhrzeit %} +
+ {{ event.uhrzeit|time:"H:i" }} Uhr + + {% endif %} +

+
+
+ +
+
+
Kategorie
+ + {{ event.get_kategorie_display }} + +
+
+
Priorität
+ + {{ event.get_prioritaet_display }} + +
+
+ + {% if event.beschreibung %} +
+
Beschreibung
+
+ {{ event.beschreibung|linebreaks }} +
+
+ {% endif %} + + {% if event.destinataer %} +
+
+
Bezogen auf Destinatär
+

+ + + {{ event.destinataer }} + +

+
+
+ {% endif %} + + {% if event.verpachtung %} +
+
+
Bezogen auf Verpachtung
+

+ + + {{ event.verpachtung }} + +

+
+
+ {% endif %} +
+
+
+ +
+ +
+
+
+ Status +
+
+
+ {% if event.erledigt %} +
+ +
Erledigt
+

Dieser Termin wurde als erledigt markiert.

+
+ {% else %} +
+ +
Ausstehend
+

Dieser Termin steht noch aus.

+
+ {% endif %} +
+
+ + +
+
+
+ Meta-Informationen +
+
+
+
+ Erstellt von:
+ {{ event.erstellt_von|default:"System" }} +
+ +
+ Erstellt am:
+ {{ event.erstellt_am|date:"d.m.Y H:i" }} +
+ + {% if event.aktualisiert_am %} +
+ Zuletzt aktualisiert:
+ {{ event.aktualisiert_am|date:"d.m.Y H:i" }} +
+ {% endif %} + +
+ Termin-ID:
+ {{ event.pk }} +
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/stiftung/kalender/detail_old.html b/app/templates/stiftung/kalender/detail_old.html new file mode 100644 index 0000000..529495e --- /dev/null +++ b/app/templates/stiftung/kalender/detail_old.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+
+
+

{{ title }}

+
+
+ +
+
+
+
+ + + +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/stiftung/kalender/edit.html b/app/templates/stiftung/kalender/edit.html new file mode 100644 index 0000000..4149150 --- /dev/null +++ b/app/templates/stiftung/kalender/edit.html @@ -0,0 +1,154 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+ +
+

+ {{ title }} +

+ +
+ + +
+
+
+
+
+ Termin bearbeiten +
+
+
+
+ {% csrf_token %} + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ +
+ + Abbrechen + + +
+
+
+
+
+ +
+ +
+
+
+ Hilfe +
+
+
+
Kategorien:
+
    +
  • Termin: Allgemeine Termine und Meetings
  • +
  • Zahlung: Zahlungserinnerungen
  • +
  • Deadline: Wichtige Fristen
  • +
  • Geburtstag: Geburtstage
  • +
  • Vertrag: Vertragsereignisse
  • +
  • Prüfung: Überprüfungen und Kontrollen
  • +
+ +
Prioritäten:
+
    +
  • Niedrig Optionale Termine
  • +
  • Normal Standard-Priorität
  • +
  • Hoch Wichtige/dringende Termine
  • +
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/stiftung/kalender/form.html b/app/templates/stiftung/kalender/form.html new file mode 100644 index 0000000..1e350ee --- /dev/null +++ b/app/templates/stiftung/kalender/form.html @@ -0,0 +1,42 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+
+
+

{{ title }}

+
+
+ +
+
+
+
+ + +

Geplante Funktionen:

+
    +
  • 📅 Termine und Meetings
  • +
  • 💰 Zahlungserinnerungen
  • +
  • ⏰ Fristen und Deadlines
  • +
  • 🎂 Geburtstage
  • +
  • 📄 Vertragsauslauf-Erinnerungen
  • +
+ + +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/stiftung/kalender/kalender.html b/app/templates/stiftung/kalender/kalender.html new file mode 100644 index 0000000..2deffa1 --- /dev/null +++ b/app/templates/stiftung/kalender/kalender.html @@ -0,0 +1,60 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+
+
+
+

{{ title }}

+ + Neuer Eintrag + +
+
+
+ +
+
+
+
+
{{ current_month }}
+
+
+ {% if events %} +
+ {% for event in events %} +
+
+
+ + {{ event.title }} + {% if event.description %} +

{{ event.description }}

+ {% endif %} +
+ + {{ event.date }} + {% if event.time %} {{ event.time }}{% endif %} + +
+
+ {% endfor %} +
+ {% else %} +
+ +
Keine Termine im aktuellen Monat
+

Erstellen Sie Ihren ersten Kalendereintrag.

+ + Termin hinzufügen + +
+ {% endif %} +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/stiftung/kalender/list_view.html b/app/templates/stiftung/kalender/list_view.html new file mode 100644 index 0000000..4cde989 --- /dev/null +++ b/app/templates/stiftung/kalender/list_view.html @@ -0,0 +1,140 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+ + + + +
+
+
+
+ {% if events %} +
+ + + + + + + + + + + + + + + {% for event in events %} + + + + + + + + + + + {% endfor %} + +
DatumZeitTitelBeschreibungKategoriePrioritätStatusAktionen
+ {{ event.date|date:"d.m.Y" }} +
+ {{ event.date|date:"l" }} +
+ {% if event.time %} + {{ event.time }} + {% else %} + ganztags + {% endif %} + +
+ + {{ event.title }} +
+
+ {{ event.description|default:"-" }} + + + {{ event.category|title }} + + + {% if event.priority == 'kritisch' %} + {{ event.priority|title }} + {% elif event.priority == 'hoch' %} + {{ event.priority|title }} + {% elif event.priority == 'normal' %} + {{ event.priority|title }} + {% else %} + {{ event.priority|title }} + {% endif %} + + {% if event.overdue %} + Überfällig + {% elif event.completed %} + Erledigt + {% else %} + Offen + {% endif %} + +
+ {% if event.url %} + + + + {% endif %} + + + +
+
+
+ {% else %} +
+ +
Keine Termine im gewählten Zeitraum
+

Fügen Sie Ihren ersten Kalendereintrag hinzu.

+ + Termin hinzufügen + +
+ {% endif %} +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/stiftung/kalender/month_view.html b/app/templates/stiftung/kalender/month_view.html new file mode 100644 index 0000000..9620ce6 --- /dev/null +++ b/app/templates/stiftung/kalender/month_view.html @@ -0,0 +1,200 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+ + + + +
+
+
+ +

{{ month_name }} {{ year }}

+
+
+
+ + +
+
+
+
+
+ +
+ {% for weekday in weekdays %} +
{{ weekday }}
+ {% endfor %} +
+ + + {% for week in calendar_grid %} +
+ {% for day_data in week %} +
+ {% if day_data %} +
{{ day_data.day }}
+ {% if day_data.events %} +
+ {% for event in day_data.events %} +
+ + {{ event.title|truncatechars:20 }} +
+ {% endfor %} + {% if day_data.event_count > 3 %} +
+{{ day_data.event_count|add:"-3" }} weitere
+ {% endif %} +
+ {% endif %} + {% endif %} +
+ {% endfor %} +
+ {% endfor %} +
+
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/app/templates/stiftung/kalender/week_view.html b/app/templates/stiftung/kalender/week_view.html new file mode 100644 index 0000000..33d6688 --- /dev/null +++ b/app/templates/stiftung/kalender/week_view.html @@ -0,0 +1,184 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+ + + + +
+
+
+
+
+
{{ start_date|date:"d.m.Y" }} - {{ end_date|date:"d.m.Y" }}
+ +
+
+
+ {% if events %} +
+ {% for i in "1234567"|make_list %} + {% with day_date=start_date|add:forloop.counter0 %} +
+
+
{{ day_date|date:"l" }}
+
{{ day_date|date:"d.m" }}
+
+
+ {% for event in events %} + {% if event.date == day_date %} +
+
+ {% if event.time %}{{ event.time }}{% else %}ganztags{% endif %} +
+
+ {{ event.title }} +
+ {% if event.description %} +
{{ event.description|truncatechars:50 }}
+ {% endif %} +
+ {% endif %} + {% endfor %} +
+
+ {% endwith %} + {% endfor %} +
+ {% else %} +
+ +
Keine Termine diese Woche
+

Fügen Sie einen neuen Termin hinzu.

+ + Termin hinzufügen + +
+ {% endif %} +
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/menu-structure.csv b/menu-structure.csv new file mode 100644 index 0000000..9febf5c --- /dev/null +++ b/menu-structure.csv @@ -0,0 +1,38 @@ +Menu Level,Item Name,URL Name,Icon,Description,Current Order +1,Home,stiftung:home,fas fa-home,Main landing page,1 +1,Dashboard,stiftung:dashboard,fas fa-tachometer-alt,Statistics and overview dashboard,2 +2,Menschen & Finanzen (Dropdown),personenDropdown,fas fa-users,People and financial management,3 +3,Destinatäre Header,N/A,N/A,Section header,3.1 +3,Alle Destinatäre,stiftung:destinataer_list,fas fa-list,List all beneficiaries,3.2 +3,Neuer Destinatär,stiftung:destinataer_create,fas fa-plus,Create new beneficiary,3.3 +3,Förderungen Header,N/A,N/A,Section header,3.4 +3,Alle Förderungen,stiftung:foerderung_list,fas fa-gift,List all grants,3.5 +3,Neue Förderung,stiftung:foerderung_create,fas fa-plus,Create new grant,3.6 +3,Unterstützungen Header,N/A,N/A,Section header,3.7 +3,Alle Unterstützungen,stiftung:unterstuetzungen_all,fas fa-hand-holding-usd,List all support payments,3.8 +3,Neue Unterstützung,stiftung:unterstuetzung_create,fas fa-plus,Create new support,3.9 +3,Pächter Header,N/A,N/A,Section header,3.10 +3,Alle Pächter,stiftung:paechter_list,fas fa-user-tie,List all tenants,3.11 +2,Immobilien & Land (Dropdown),immobilienDropdown,fas fa-tree,Real estate and land management,4 +3,Ländereien Header,N/A,N/A,Section header,4.1 +3,Alle Ländereien,stiftung:land_list,fas fa-list,List all land parcels,4.2 +3,Neue Länderei,stiftung:land_create,fas fa-plus,Create new land parcel,4.3 +3,Verpachtungen Header,N/A,N/A,Section header,4.4 +3,Alle Verpachtungen,stiftung:verpachtung_list,fas fa-handshake,List all leases,4.5 +3,Neue Verpachtung,stiftung:verpachtung_create,fas fa-plus,Create new lease,4.6 +3,Abrechnungen Header,N/A,N/A,Section header,4.7 +3,Alle Abrechnungen,stiftung:land_abrechnung_list,fas fa-calculator,List all settlements,4.8 +3,Neue Abrechnung,stiftung:land_abrechnung_create,fas fa-plus,Create new settlement,4.9 +2,Verwaltung (Dropdown),verwaltungDropdown,fas fa-briefcase,Administration and documents,5 +3,Dokumente,stiftung:dokument_management,fas fa-folder-open,Document management,5.1 +3,Geschäftsführung,stiftung:geschaeftsfuehrung,fas fa-briefcase,Business management,5.2 +1,Geschichte,stiftung:geschichte_list,fas fa-book-open,Foundation history wiki,6 +1,Administration,stiftung:administration,fas fa-cogs,System administration,7 +2,User Menu (Dropdown),userDropdown,fas fa-user,User account management,8 +3,User Profile Header,N/A,N/A,Dynamic user name header,8.1 +3,Mein Profil,stiftung:user_detail,fas fa-user,View user profile,8.2 +3,2FA verwalten,stiftung:two_factor_setup,fas fa-shield-alt,Manage two-factor authentication,8.3 +3,Benutzerverwaltung,stiftung:user_management,fas fa-users,Manage users (admin only),8.4 +3,Administration,stiftung:administration,fas fa-cogs,System admin (admin only),8.5 +3,Abmelden,stiftung:logout,fas fa-sign-out-alt,Logout,8.6 +1,Anmelden,stiftung:login,fas fa-sign-in-alt,Login (unauthenticated users only),9 \ No newline at end of file