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
This commit is contained in:
@@ -14,3 +14,9 @@ class StiftungConfig(AppConfig):
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
# django-otp not installed
|
# django-otp not installed
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Import signals to register them
|
||||||
|
try:
|
||||||
|
import stiftung.signals
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|||||||
@@ -1648,15 +1648,30 @@ class GeschichteSeiteForm(forms.ModelForm):
|
|||||||
|
|
||||||
def clean_slug(self):
|
def clean_slug(self):
|
||||||
slug = self.cleaned_data.get('slug')
|
slug = self.cleaned_data.get('slug')
|
||||||
titel = self.cleaned_data.get('titel')
|
titel = self.cleaned_data.get('titel', '')
|
||||||
|
|
||||||
if not slug and titel:
|
if not slug and titel:
|
||||||
# Auto-generate slug from title
|
# Auto-generate slug from title
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
slug = slugify(titel)
|
slug = slugify(titel)
|
||||||
|
|
||||||
|
if not slug:
|
||||||
|
raise forms.ValidationError('Slug ist erforderlich. Bitte geben Sie einen Titel ein.')
|
||||||
|
|
||||||
return slug
|
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):
|
class GeschichteBildForm(forms.ModelForm):
|
||||||
"""Form for uploading images to history pages"""
|
"""Form for uploading images to history pages"""
|
||||||
|
|||||||
58
app/stiftung/management/commands/create_sample_events.py
Normal file
58
app/stiftung/management/commands/create_sample_events.py
Normal file
@@ -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}')
|
||||||
|
)
|
||||||
169
app/stiftung/management/commands/fix_account_balances.py
Normal file
169
app/stiftung/management/commands/fix_account_balances.py
Normal file
@@ -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}"
|
||||||
|
)
|
||||||
145
app/stiftung/management/commands/reconcile_balances.py
Normal file
145
app/stiftung/management/commands/reconcile_balances.py
Normal file
@@ -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")
|
||||||
|
)
|
||||||
18
app/stiftung/migrations/0038_allow_blank_content.py
Normal file
18
app/stiftung/migrations/0038_allow_blank_content.py
Normal file
@@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
41
app/stiftung/migrations/0039_stiftungskalendereintrag.py
Normal file
41
app/stiftung/migrations/0039_stiftungskalendereintrag.py
Normal file
@@ -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')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
13
app/stiftung/migrations/0040_add_calendar_model.py
Normal file
13
app/stiftung/migrations/0040_add_calendar_model.py
Normal file
@@ -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 = [
|
||||||
|
]
|
||||||
18
app/stiftung/migrations/0041_alter_geschichteseite_inhalt.py
Normal file
18
app/stiftung/migrations/0041_alter_geschichteseite_inhalt.py
Normal file
@@ -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)'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -2864,7 +2864,11 @@ class GeschichteSeite(models.Model):
|
|||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
titel = models.CharField(max_length=200, verbose_name="Titel")
|
titel = models.CharField(max_length=200, verbose_name="Titel")
|
||||||
slug = models.SlugField(max_length=200, unique=True, verbose_name="URL-Slug")
|
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
|
# Metadata
|
||||||
erstellt_am = models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am")
|
erstellt_am = models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am")
|
||||||
@@ -2940,3 +2944,124 @@ class GeschichteBild(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.titel} ({self.seite.titel})"
|
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))
|
||||||
|
|||||||
1
app/stiftung/services/__init__.py
Normal file
1
app/stiftung/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Services package
|
||||||
269
app/stiftung/services/calendar_service.py
Normal file
269
app/stiftung/services/calendar_service.py
Normal file
@@ -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)
|
||||||
166
app/stiftung/signals.py
Normal file
166
app/stiftung/signals.py
Normal file
@@ -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()
|
||||||
@@ -32,3 +32,19 @@ def help_box(page_key, user=None):
|
|||||||
def help_box_exists(page_key):
|
def help_box_exists(page_key):
|
||||||
"""Prüfe, ob eine Hilfs-Infobox für eine Seite existiert"""
|
"""Prüfe, ob eine Hilfs-Infobox für eine Seite existiert"""
|
||||||
return HelpBox.get_help_for_page(page_key) is not None
|
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))
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ from . import views
|
|||||||
app_name = "stiftung"
|
app_name = "stiftung"
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Dashboard (Startseite)
|
# Home - Main landing page after login
|
||||||
path("", views.dashboard, name="dashboard"),
|
path("", views.home, name="home"),
|
||||||
# Home (für Kompatibilität mit bestehenden Templates)
|
# Dashboard (detailed view)
|
||||||
path("home/", views.home, name="home"),
|
path("dashboard/", views.dashboard, name="dashboard"),
|
||||||
# CSV Import URLs
|
# CSV Import URLs
|
||||||
path("import/", views.csv_import_list, name="csv_import_list"),
|
path("import/", views.csv_import_list, name="csv_import_list"),
|
||||||
path("import/neu/", views.csv_import_create, name="csv_import_create"),
|
path("import/neu/", views.csv_import_create, name="csv_import_create"),
|
||||||
@@ -391,4 +391,14 @@ urlpatterns = [
|
|||||||
path("geschichte/<slug:slug>/", views.geschichte_detail, name="geschichte_detail"),
|
path("geschichte/<slug:slug>/", views.geschichte_detail, name="geschichte_detail"),
|
||||||
path("geschichte/<slug:slug>/bearbeiten/", views.geschichte_edit, name="geschichte_edit"),
|
path("geschichte/<slug:slug>/bearbeiten/", views.geschichte_edit, name="geschichte_edit"),
|
||||||
path("geschichte/<slug:slug>/bild-upload/", views.geschichte_bild_upload, name="geschichte_bild_upload"),
|
path("geschichte/<slug:slug>/bild-upload/", views.geschichte_bild_upload, name="geschichte_bild_upload"),
|
||||||
|
path("geschichte/<slug:slug>/bild/<uuid:bild_id>/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/<uuid:pk>/", views.kalender_detail, name="kalender_detail"),
|
||||||
|
path("kalender/<uuid:pk>/bearbeiten/", views.kalender_edit, name="kalender_edit"),
|
||||||
|
path("kalender/<uuid:pk>/loeschen/", views.kalender_delete, name="kalender_delete"),
|
||||||
|
path("kalender/api/events/", views.kalender_api_events, name="kalender_api_events"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import io
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import qrcode
|
import qrcode
|
||||||
@@ -225,11 +225,38 @@ from .forms import (DestinataerForm, DestinataerNotizForm,
|
|||||||
|
|
||||||
def home(request):
|
def home(request):
|
||||||
"""Home page for the Stiftungsverwaltung application"""
|
"""Home page for the Stiftungsverwaltung application"""
|
||||||
return render(
|
from stiftung.services.calendar_service import StiftungsKalenderService
|
||||||
request,
|
|
||||||
"stiftung/home.html",
|
# Get upcoming events for the calendar widget
|
||||||
{"title": "Stiftungsverwaltung", "description": "Foundation Management System"},
|
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
|
@login_required
|
||||||
@@ -8152,3 +8179,412 @@ def geschichte_bild_upload(request, slug):
|
|||||||
}
|
}
|
||||||
|
|
||||||
return render(request, 'stiftung/geschichte/bild_form.html', context)
|
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)
|
||||||
|
|||||||
@@ -525,14 +525,19 @@
|
|||||||
<ul class="navbar-nav me-auto">
|
<ul class="navbar-nav me-auto">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'stiftung:home' %}">
|
<a class="nav-link" href="{% url 'stiftung:home' %}">
|
||||||
|
<i class="fas fa-home me-1"></i>Home
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'stiftung:dashboard' %}">
|
||||||
<i class="fas fa-tachometer-alt me-1"></i>Dashboard
|
<i class="fas fa-tachometer-alt me-1"></i>Dashboard
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- Menschen & Finanzen -->
|
<!-- Destinatäre -->
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="personenDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
<a class="nav-link dropdown-toggle" href="#" id="personenDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<i class="fas fa-users me-1"></i>Menschen & Finanzen
|
<i class="fas fa-users me-1"></i>Destinatäre
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu" aria-labelledby="personenDropdown">
|
<ul class="dropdown-menu" aria-labelledby="personenDropdown">
|
||||||
<li><h6 class="dropdown-header">Destinatäre</h6></li>
|
<li><h6 class="dropdown-header">Destinatäre</h6></li>
|
||||||
@@ -558,18 +563,13 @@
|
|||||||
<li><a class="dropdown-item" href="{% url 'stiftung:unterstuetzung_create' %}">
|
<li><a class="dropdown-item" href="{% url 'stiftung:unterstuetzung_create' %}">
|
||||||
<i class="fas fa-plus me-2"></i>Neue Unterstützung
|
<i class="fas fa-plus me-2"></i>Neue Unterstützung
|
||||||
</a></li>
|
</a></li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
|
||||||
<li><h6 class="dropdown-header">Pächter</h6></li>
|
|
||||||
<li><a class="dropdown-item" href="{% url 'stiftung:paechter_list' %}">
|
|
||||||
<i class="fas fa-user-tie me-2"></i>Alle Pächter
|
|
||||||
</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- Immobilien & Land -->
|
<!-- Land & Pacht -->
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="immobilienDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
<a class="nav-link dropdown-toggle" href="#" id="immobilienDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<i class="fas fa-tree me-1"></i>Immobilien & Land
|
<i class="fas fa-tree me-1"></i>Land & Pacht
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu" aria-labelledby="immobilienDropdown">
|
<ul class="dropdown-menu" aria-labelledby="immobilienDropdown">
|
||||||
<li><h6 class="dropdown-header">Ländereien</h6></li>
|
<li><h6 class="dropdown-header">Ländereien</h6></li>
|
||||||
@@ -595,6 +595,11 @@
|
|||||||
<li><a class="dropdown-item" href="{% url 'stiftung:land_abrechnung_create' %}">
|
<li><a class="dropdown-item" href="{% url 'stiftung:land_abrechnung_create' %}">
|
||||||
<i class="fas fa-plus me-2"></i>Neue Abrechnung
|
<i class="fas fa-plus me-2"></i>Neue Abrechnung
|
||||||
</a></li>
|
</a></li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><h6 class="dropdown-header">Pächter</h6></li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'stiftung:paechter_list' %}">
|
||||||
|
<i class="fas fa-user-tie me-2"></i>Alle Pächter
|
||||||
|
</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
@@ -610,6 +615,31 @@
|
|||||||
<li><a class="dropdown-item" href="{% url 'stiftung:geschaeftsfuehrung' %}">
|
<li><a class="dropdown-item" href="{% url 'stiftung:geschaeftsfuehrung' %}">
|
||||||
<i class="fas fa-briefcase me-2"></i>Geschäftsführung
|
<i class="fas fa-briefcase me-2"></i>Geschäftsführung
|
||||||
</a></li>
|
</a></li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'stiftung:administration' %}">
|
||||||
|
<i class="fas fa-cogs me-2"></i>Administration
|
||||||
|
</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- Kalender -->
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" id="kalenderDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fas fa-calendar-alt me-1"></i>Kalender
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="kalenderDropdown">
|
||||||
|
<li><h6 class="dropdown-header">Ansichten</h6></li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'stiftung:kalender' %}">
|
||||||
|
<i class="fas fa-calendar me-2"></i>Kalender anzeigen
|
||||||
|
</a></li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><h6 class="dropdown-header">Verwaltung</h6></li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'stiftung:kalender_admin' %}">
|
||||||
|
<i class="fas fa-cogs me-2"></i>Kalender Administration
|
||||||
|
</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'stiftung:kalender_create' %}">
|
||||||
|
<i class="fas fa-plus me-2"></i>Neuer Termin
|
||||||
|
</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
@@ -619,11 +649,6 @@
|
|||||||
<i class="fas fa-book-open me-1"></i>Geschichte
|
<i class="fas fa-book-open me-1"></i>Geschichte
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="{% url 'stiftung:administration' %}">
|
|
||||||
<i class="fas fa-cogs me-1"></i>Administration
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<!-- User Menu -->
|
<!-- User Menu -->
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
@@ -672,9 +697,10 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<!-- Content Wrapper -->
|
<!-- Content Wrapper -->
|
||||||
<div id="content-wrapper" class="d-flex flex-column">
|
<div id="content-wrapper" class="d-flex flex-column">
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="text-muted mb-4">
|
<p class="text-muted mb-4">
|
||||||
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.
|
Förderungen und Ländereien.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -83,8 +83,8 @@
|
|||||||
<div class="card bg-primary text-white">
|
<div class="card bg-primary text-white">
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<i class="fas fa-users fa-2x mb-2"></i>
|
<i class="fas fa-users fa-2x mb-2"></i>
|
||||||
<h5 class="card-title">Personen</h5>
|
<h5 class="card-title">Destinatäre</h5>
|
||||||
<h3 class="card-text">{{ total_persons|default:"0" }}</h3>
|
<h3 class="card-text">{{ total_destinataere|default:"0" }}</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,8 +129,8 @@
|
|||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<h6 class="text-primary">Schnellzugriff:</h6>
|
<h6 class="text-primary">Schnellzugriff:</h6>
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<a href="{% url 'stiftung:person_list' %}" class="btn btn-outline-primary">
|
<a href="{% url 'stiftung:destinataer_list' %}" class="btn btn-outline-primary">
|
||||||
<i class="fas fa-users me-2"></i>Alle Personen anzeigen
|
<i class="fas fa-users me-2"></i>Alle Destinatäre anzeigen
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'stiftung:foerderung_list' %}" class="btn btn-outline-success">
|
<a href="{% url 'stiftung:foerderung_list' %}" class="btn btn-outline-success">
|
||||||
<i class="fas fa-gift me-2"></i>Alle Förderungen anzeigen
|
<i class="fas fa-gift me-2"></i>Alle Förderungen anzeigen
|
||||||
|
|||||||
66
app/templates/stiftung/geschichte/bild_delete.html
Normal file
66
app/templates/stiftung/geschichte/bild_delete.html
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load help_tags %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'stiftung:dashboard' %}">Dashboard</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'stiftung:geschichte_list' %}">Geschichte</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ seite.get_absolute_url }}">{{ seite.titel }}</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Bild löschen</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-header bg-danger text-white">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="fas fa-trash me-2"></i>{{ title }}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||||
|
<strong>Achtung!</strong> Dieser Vorgang kann nicht rückgängig gemacht werden.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<img src="{{ bild.bild.url }}" alt="{{ bild.alt_text }}" class="img-fluid rounded">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h6>Bild-Details:</h6>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li><strong>Titel:</strong> {{ bild.titel }}</li>
|
||||||
|
<li><strong>Beschreibung:</strong> {{ bild.beschreibung|default:"Keine Beschreibung" }}</li>
|
||||||
|
<li><strong>Hochgeladen:</strong> {{ bild.hochgeladen_am|date:"d.m.Y H:i" }}</li>
|
||||||
|
{% if bild.hochgeladen_von %}
|
||||||
|
<li><strong>Hochgeladen von:</strong> {{ bild.hochgeladen_von.get_full_name|default:bild.hochgeladen_von.username }}</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<p>Möchten Sie dieses Bild wirklich löschen?</p>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="{{ seite.get_absolute_url }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i>Abbrechen
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-danger">
|
||||||
|
<i class="fas fa-trash me-1"></i>Endgültig löschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="geschichte-content">
|
<div class="geschichte-content">
|
||||||
{{ seite.inhalt|linebreaks }}
|
{{ seite.inhalt|markdown_to_html }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if bilder %}
|
{% if bilder %}
|
||||||
@@ -75,13 +75,22 @@
|
|||||||
<h4><i class="fas fa-images me-2"></i>Bildergalerie</h4>
|
<h4><i class="fas fa-images me-2"></i>Bildergalerie</h4>
|
||||||
{% for bild in bilder %}
|
{% for bild in bilder %}
|
||||||
<div class="geschichte-bild">
|
<div class="geschichte-bild">
|
||||||
<img src="{{ bild.bild.url }}" alt="{{ bild.alt_text|default:bild.titel }}" class="img-fluid">
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
<div class="bild-beschreibung">
|
<h6 class="mb-0">{{ bild.titel }}</h6>
|
||||||
<strong>{{ bild.titel }}</strong>
|
{% if perms.stiftung.delete_geschichtebild %}
|
||||||
{% if bild.beschreibung %}
|
<a href="{% url 'stiftung:geschichte_bild_delete' slug=seite.slug bild_id=bild.id %}"
|
||||||
<br>{{ bild.beschreibung }}
|
class="btn btn-outline-danger btn-sm"
|
||||||
|
title="Bild löschen">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<img src="{{ bild.bild.url }}" alt="{{ bild.alt_text|default:bild.titel }}" class="img-fluid">
|
||||||
|
{% if bild.beschreibung %}
|
||||||
|
<div class="bild-beschreibung mt-2">
|
||||||
|
{{ bild.beschreibung }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,19 +4,37 @@
|
|||||||
{% block title %}{{ title }}{% endblock %}
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde@2.18.0/dist/easymde.min.css">
|
||||||
<style>
|
<style>
|
||||||
.ql-editor {
|
.EasyMDEContainer {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.EasyMDEContainer .editor-toolbar {
|
||||||
|
border-color: #dee2e6;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.EasyMDEContainer .CodeMirror {
|
||||||
|
border-color: #dee2e6;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
}
|
}
|
||||||
.ql-toolbar {
|
|
||||||
border-top-left-radius: 0.375rem;
|
.EasyMDEContainer .editor-preview,
|
||||||
border-top-right-radius: 0.375rem;
|
.EasyMDEContainer .editor-preview-side {
|
||||||
}
|
|
||||||
.ql-container {
|
|
||||||
border-bottom-left-radius: 0.375rem;
|
|
||||||
border-bottom-right-radius: 0.375rem;
|
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-help {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -75,13 +93,37 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">{{ form.inhalt.label }}</label>
|
<label class="form-label" for="{{ form.inhalt.id_for_label }}">{{ form.inhalt.label }}</label>
|
||||||
<div id="quill-editor" style="height: 400px;"></div>
|
|
||||||
<textarea name="inhalt" id="id_inhalt" style="display: none;">{{ form.inhalt.value|default:"" }}</textarea>
|
<!-- Markdown Editor -->
|
||||||
|
{{ form.inhalt }}
|
||||||
|
|
||||||
{% if form.inhalt.errors %}
|
{% if form.inhalt.errors %}
|
||||||
<div class="text-danger">{{ form.inhalt.errors }}</div>
|
<div class="text-danger">{{ form.inhalt.errors }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="form-text">{{ form.inhalt.help_text }}</div>
|
<div class="form-text">{{ form.inhalt.help_text }}</div>
|
||||||
|
|
||||||
|
<div class="editor-help mt-2">
|
||||||
|
<h6><i class="fas fa-info-circle me-1"></i>Markdown-Hilfe</h6>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<small>
|
||||||
|
<strong>Formatierung:</strong><br>
|
||||||
|
<code>**Fett**</code> → <strong>Fett</strong><br>
|
||||||
|
<code>*Kursiv*</code> → <em>Kursiv</em><br>
|
||||||
|
<code>~~Durchgestrichen~~</code> → <del>Durchgestrichen</del>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<small>
|
||||||
|
<strong>Überschriften:</strong><br>
|
||||||
|
<code># Überschrift 1</code><br>
|
||||||
|
<code>## Überschrift 2</code><br>
|
||||||
|
<code>### Überschrift 3</code>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3 form-check">
|
<div class="mb-3 form-check">
|
||||||
@@ -110,18 +152,26 @@
|
|||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header bg-info text-white">
|
<div class="card-header bg-info text-white">
|
||||||
<h6 class="mb-0"><i class="fas fa-info-circle me-2"></i>Rich Text Editor</h6>
|
<h6 class="mb-0"><i class="fas fa-markdown me-2"></i>Markdown Editor</h6>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="small">Der Editor unterstützt:</p>
|
<p class="small">Der Markdown-Editor unterstützt:</p>
|
||||||
<ul class="small">
|
<ul class="small">
|
||||||
<li>Formatierung (Fett, Kursiv, Unterstrichen)</li>
|
<li>Live-Vorschau beim Schreiben</li>
|
||||||
<li>Überschriften (H1, H2, H3)</li>
|
<li>Toolbar für häufige Formatierungen</li>
|
||||||
<li>Listen (nummeriert und Aufzählungen)</li>
|
<li>Vollbild-Modus für fokussiertes Schreiben</li>
|
||||||
<li>Links</li>
|
<li>Automatisches Speichern im Browser</li>
|
||||||
<li>Bilder (über separaten Upload)</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<hr>
|
<hr>
|
||||||
|
<h6 class="small">Häufige Markdown-Syntax:</h6>
|
||||||
|
<div class="small">
|
||||||
|
<code>- Listen-Element</code><br>
|
||||||
|
<code>1. Nummerierte Liste</code><br>
|
||||||
|
<code>[Link Text](URL)</code><br>
|
||||||
|
<code>> Zitat</code><br>
|
||||||
|
<code>`Code`</code>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
<p class="small text-muted">
|
<p class="small text-muted">
|
||||||
Tipp: Verwenden Sie die Bild-Upload-Funktion, um Bilder zur Seite hinzuzufügen.
|
Tipp: Verwenden Sie die Bild-Upload-Funktion, um Bilder zur Seite hinzuzufügen.
|
||||||
</p>
|
</p>
|
||||||
@@ -132,46 +182,88 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script src="https://cdn.quilljs.com/1.3.6/quill.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/easymde@2.18.0/dist/easymde.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Initialize Quill editor
|
console.log('Initializing EasyMDE markdown editor...');
|
||||||
var quill = new Quill('#quill-editor', {
|
|
||||||
theme: 'snow',
|
var textarea = document.getElementById('id_inhalt');
|
||||||
modules: {
|
|
||||||
|
if (textarea) {
|
||||||
|
var easyMDE = new EasyMDE({
|
||||||
|
element: textarea,
|
||||||
|
autofocus: false,
|
||||||
|
autosave: {
|
||||||
|
enabled: true,
|
||||||
|
delay: 1000,
|
||||||
|
uniqueId: 'geschichte_' + (window.location.pathname.match(/\/(\w+)\//) || ['', 'new'])[1]
|
||||||
|
},
|
||||||
|
spellChecker: false,
|
||||||
toolbar: [
|
toolbar: [
|
||||||
[{ 'header': [1, 2, 3, false] }],
|
'bold', 'italic', 'strikethrough', '|',
|
||||||
['bold', 'italic', 'underline', 'strike'],
|
'heading-1', 'heading-2', 'heading-3', '|',
|
||||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
'unordered-list', 'ordered-list', '|',
|
||||||
[{ 'color': [] }, { 'background': [] }],
|
'link', 'quote', 'code', '|',
|
||||||
['link'],
|
'preview', 'side-by-side', 'fullscreen', '|',
|
||||||
['clean']
|
'guide'
|
||||||
]
|
],
|
||||||
},
|
placeholder: 'Schreiben Sie hier den Inhalt der Geschichtsseite...\n\nSie können Markdown verwenden für:\n- **Fettdruck**\n- *Kursiv*\n- # Überschriften\n- [Links](URL)\n- Listen und vieles mehr',
|
||||||
placeholder: 'Schreiben Sie hier den Inhalt der Geschichtsseite...'
|
renderingConfig: {
|
||||||
});
|
singleLineBreaks: false,
|
||||||
|
codeSyntaxHighlighting: false
|
||||||
|
},
|
||||||
|
previewRender: function(plainText) {
|
||||||
|
// Simple markdown to HTML conversion for preview
|
||||||
|
return plainText
|
||||||
|
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||||||
|
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||||||
|
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||||||
|
.replace(/^\> (.*$)/gim, '<blockquote>$1</blockquote>')
|
||||||
|
.replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
|
||||||
|
.replace(/\*(.*)\*/gim, '<em>$1</em>')
|
||||||
|
.replace(/~~(.*)~~/gim, '<del>$1</del>')
|
||||||
|
.replace(/`([^`]*)`/gim, '<code>$1</code>')
|
||||||
|
.replace(/\[([^\]]*)\]\(([^)]*)\)/gim, '<a href="$2">$1</a>')
|
||||||
|
.replace(/^\* (.*$)/gim, '<li>$1</li>')
|
||||||
|
.replace(/^\d+\. (.*$)/gim, '<li>$1</li>')
|
||||||
|
.replace(/\n/gim, '<br>');
|
||||||
|
},
|
||||||
|
status: ['autosave', 'lines', 'words', 'cursor'],
|
||||||
|
insertTexts: {
|
||||||
|
horizontalRule: ["", "\n\n-----\n\n"],
|
||||||
|
image: [""],
|
||||||
|
link: ["[", "](http://)"],
|
||||||
|
table: ["", "\n\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n\n"],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Load existing content
|
console.log('EasyMDE initialized successfully');
|
||||||
var existingContent = document.getElementById('id_inhalt').value;
|
|
||||||
if (existingContent) {
|
// Auto-generate slug from title
|
||||||
quill.root.innerHTML = existingContent;
|
var titleField = document.querySelector('input[name="titel"]');
|
||||||
|
var slugField = document.querySelector('input[name="slug"]');
|
||||||
|
|
||||||
|
if (titleField && slugField) {
|
||||||
|
titleField.addEventListener('input', function() {
|
||||||
|
// Only auto-generate if slug is empty or matches previous title
|
||||||
|
if (!slugField.value || slugField.getAttribute('data-auto-generated') === 'true') {
|
||||||
|
var title = this.value;
|
||||||
|
var slug = title.toLowerCase()
|
||||||
|
.replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue').replace(/ß/g, 'ss')
|
||||||
|
.replace(/[^\w\s-]/g, '')
|
||||||
|
.replace(/[-\s]+/g, '-')
|
||||||
|
.replace(/^-+|-+$/g, '');
|
||||||
|
slugField.value = slug;
|
||||||
|
slugField.setAttribute('data-auto-generated', 'true');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark slug as manually edited if user changes it
|
||||||
|
slugField.addEventListener('input', function() {
|
||||||
|
slugField.setAttribute('data-auto-generated', 'false');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-generate slug from title
|
|
||||||
document.getElementById('{{ form.titel.id_for_label }}').addEventListener('input', function() {
|
|
||||||
var title = this.value;
|
|
||||||
var slug = title.toLowerCase()
|
|
||||||
.replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue').replace(/ß/g, 'ss')
|
|
||||||
.replace(/[^\w\s-]/g, '')
|
|
||||||
.replace(/[-\s]+/g, '-')
|
|
||||||
.trim();
|
|
||||||
document.getElementById('{{ form.slug.id_for_label }}').value = slug;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update textarea on form submit
|
|
||||||
document.getElementById('geschichteForm').addEventListener('submit', function() {
|
|
||||||
document.getElementById('id_inhalt').value = quill.root.innerHTML;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -8,6 +8,18 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card shadow">
|
<div class="card shadow">
|
||||||
<div class="card-body text-center py-5">
|
<div class="card-body text-center py-5">
|
||||||
|
<!-- Logo Placeholder -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="logo-placeholder mx-auto mb-3" style="width: 150px; height: 150px; border: 3px dashed #dee2e6; border-radius: 50%; display: flex; align-items: center; justify-content: center; background-color: #f8f9fa;">
|
||||||
|
<div class="text-center text-muted">
|
||||||
|
<i class="fas fa-image fa-2x mb-2"></i>
|
||||||
|
<div style="font-size: 0.8rem;">Logo hier<br>einfügen</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Alternative: Replace above div with actual logo when available -->
|
||||||
|
<!-- <img src="{% load static %}{% static 'images/stiftung-logo.png' %}" alt="Stiftung Logo" class="img-fluid mb-3" style="max-height: 150px;"> -->
|
||||||
|
</div>
|
||||||
|
|
||||||
<h1 class="display-4 mb-4">
|
<h1 class="display-4 mb-4">
|
||||||
<i class="fas fa-landmark text-primary me-3"></i>van Hees-Theyssen-Vogel'sche Stiftung
|
<i class="fas fa-landmark text-primary me-3"></i>van Hees-Theyssen-Vogel'sche Stiftung
|
||||||
</h1>
|
</h1>
|
||||||
@@ -40,10 +52,10 @@
|
|||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="card h-100 border-0 shadow-sm">
|
<div class="card h-100 border-0 shadow-sm">
|
||||||
<div class="card-body text-center p-4">
|
<div class="card-body text-center p-4">
|
||||||
<i class="fas fa-gift fa-3x text-success mb-3"></i>
|
<i class="fas fa-hand-holding-usd fa-3x text-success mb-3"></i>
|
||||||
<h5 class="card-title">💰 Förderungsverwaltung</h5>
|
<h5 class="card-title">💰 Unterstützungsverwaltung</h5>
|
||||||
<p class="card-text">Erfassen und verfolgen Sie Förderungen, Beträge und Verwendungsnachweise systematisch.</p>
|
<p class="card-text">Erfassen und verfolgen Sie Unterstützungen, Beträge und Verwendungsnachweise systematisch.</p>
|
||||||
<a href="{% url 'stiftung:foerderung_list' %}" class="btn btn-outline-success btn-sm mt-2">
|
<a href="{% url 'stiftung:unterstuetzungen_all' %}" class="btn btn-outline-success btn-sm mt-2">
|
||||||
<i class="fas fa-external-link-alt me-1"></i>Öffnen
|
<i class="fas fa-external-link-alt me-1"></i>Öffnen
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,6 +76,109 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Calendar and Events Section -->
|
||||||
|
<div class="row g-4 mb-5">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card h-100 border-0 shadow-sm">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="fas fa-calendar-alt me-2"></i>Anstehende Termine & Ereignisse
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if overdue_events %}
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
<h6><i class="fas fa-exclamation-triangle me-2"></i>Überfällige Termine ({{ overdue_events|length }})</h6>
|
||||||
|
{% for event in overdue_events %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center border-bottom py-2">
|
||||||
|
<div>
|
||||||
|
<i class="{{ event.icon }} me-2"></i>
|
||||||
|
<strong>{{ event.title }}</strong>
|
||||||
|
<small class="text-muted d-block">{{ event.description }}</small>
|
||||||
|
</div>
|
||||||
|
<span class="badge bg-danger">{{ event.date }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if upcoming_events %}
|
||||||
|
<h6 class="text-muted mb-3">Nächste {{ upcoming_events|length }} Termine</h6>
|
||||||
|
{% for event in upcoming_events %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center border-bottom py-2">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<i class="{{ event.icon }} me-2 text-{{ event.color }}"></i>
|
||||||
|
<strong>{{ event.title }}</strong>
|
||||||
|
{% if event.description %}
|
||||||
|
<small class="text-muted d-block">{{ event.description }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<span class="badge bg-{{ event.color }}">{{ event.date }}</span>
|
||||||
|
{% if event.time %}
|
||||||
|
<small class="d-block text-muted">{{ event.time }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="text-center py-4 text-muted">
|
||||||
|
<i class="fas fa-calendar-check fa-3x mb-3"></i>
|
||||||
|
<p>Keine anstehenden Termine in den nächsten 14 Tagen.</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<a href="{% url 'stiftung:kalender' %}" class="btn btn-outline-primary btn-sm">
|
||||||
|
<i class="fas fa-calendar me-1"></i>Vollständigen Kalender anzeigen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<!-- Mini Calendar -->
|
||||||
|
<div class="card border-0 shadow-sm mb-3">
|
||||||
|
<div class="card-header bg-info text-white">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="fas fa-calendar me-2"></i>{{ today|date:"F Y" }}
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<div class="mini-calendar">
|
||||||
|
<!-- Calendar will be generated by JavaScript -->
|
||||||
|
<div id="mini-calendar" data-events="{{ current_month_events|length }}">
|
||||||
|
<!-- Mini calendar grid will be inserted here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="fas fa-plus me-2"></i>Schnellzugriff
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<a href="{% url 'stiftung:kalender_create' %}" class="btn btn-outline-primary btn-sm">
|
||||||
|
<i class="fas fa-calendar-plus me-1"></i>Termin hinzufügen
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'stiftung:unterstuetzung_create' %}" class="btn btn-outline-success btn-sm">
|
||||||
|
<i class="fas fa-euro-sign me-1"></i>Zahlung planen
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'stiftung:destinataer_create' %}" class="btn btn-outline-info btn-sm">
|
||||||
|
<i class="fas fa-user-plus me-1"></i>Destinatär anlegen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row g-4 mb-5">
|
<div class="row g-4 mb-5">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="card h-100 border-0 shadow-sm">
|
<div class="card h-100 border-0 shadow-sm">
|
||||||
@@ -97,7 +212,7 @@
|
|||||||
<i class="fas fa-handshake fa-3x text-secondary mb-3"></i>
|
<i class="fas fa-handshake fa-3x text-secondary mb-3"></i>
|
||||||
<h5 class="card-title">🤝 Verpachtungsverwaltung</h5>
|
<h5 class="card-title">🤝 Verpachtungsverwaltung</h5>
|
||||||
<p class="card-text">Organisieren Sie Pachtverträge und deren Verwaltung effizient.</p>
|
<p class="card-text">Organisieren Sie Pachtverträge und deren Verwaltung effizient.</p>
|
||||||
<a href="{% url 'stiftung:land_list' %}" class="btn btn-outline-secondary btn-sm mt-2">
|
<a href="{% url 'stiftung:verpachtung_list' %}" class="btn btn-outline-secondary btn-sm mt-2">
|
||||||
<i class="fas fa-external-link-alt me-1"></i>Öffnen
|
<i class="fas fa-external-link-alt me-1"></i>Öffnen
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -177,3 +292,164 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.mini-calendar {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-calendar .calendar-day {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 1px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-calendar .calendar-day.today {
|
||||||
|
background-color: var(--bs-primary);
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-calendar .calendar-day.has-events {
|
||||||
|
background-color: var(--bs-success);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-calendar .calendar-day:hover {
|
||||||
|
background-color: var(--bs-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-calendar .calendar-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-calendar .calendar-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 1px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-calendar .calendar-weekday {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--bs-secondary);
|
||||||
|
padding: 4px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Generate mini calendar for current month
|
||||||
|
const today = new Date();
|
||||||
|
const currentMonth = today.getMonth();
|
||||||
|
const currentYear = today.getFullYear();
|
||||||
|
|
||||||
|
function generateMiniCalendar(year, month) {
|
||||||
|
const firstDay = new Date(year, month, 1);
|
||||||
|
const lastDay = new Date(year, month + 1, 0);
|
||||||
|
const startDate = new Date(firstDay);
|
||||||
|
startDate.setDate(startDate.getDate() - firstDay.getDay()); // Start from Sunday
|
||||||
|
|
||||||
|
const calendarEl = document.getElementById('mini-calendar');
|
||||||
|
|
||||||
|
let html = '<div class="calendar-grid">';
|
||||||
|
|
||||||
|
// Weekday headers
|
||||||
|
const weekdays = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
|
||||||
|
weekdays.forEach(day => {
|
||||||
|
html += `<div class="calendar-weekday">${day}</div>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate calendar days
|
||||||
|
const currentDate = new Date(startDate);
|
||||||
|
for (let i = 0; i < 42; i++) { // 6 weeks
|
||||||
|
const isCurrentMonth = currentDate.getMonth() === month;
|
||||||
|
const isToday = currentDate.toDateString() === today.toDateString();
|
||||||
|
|
||||||
|
let classes = ['calendar-day'];
|
||||||
|
if (isToday) classes.push('today');
|
||||||
|
if (!isCurrentMonth) classes.push('text-muted');
|
||||||
|
|
||||||
|
html += `<div class="${classes.join(' ')}">${currentDate.getDate()}</div>`;
|
||||||
|
currentDate.setDate(currentDate.getDate() + 1);
|
||||||
|
|
||||||
|
if (currentDate > lastDay && currentDate.getDay() === 0) {
|
||||||
|
break; // Stop at end of month on Sunday
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
calendarEl.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the current month calendar
|
||||||
|
generateMiniCalendar(currentYear, currentMonth);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Mini Calendar Styles */
|
||||||
|
.mini-calendar .calendar-day {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-calendar .calendar-day:hover {
|
||||||
|
background-color: var(--bs-primary-bg-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-calendar .calendar-day.today {
|
||||||
|
background-color: var(--bs-primary);
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-calendar .calendar-header {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--bs-secondary);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar event indicators */
|
||||||
|
.calendar-events-indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2px;
|
||||||
|
right: 2px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background-color: var(--bs-warning);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overdue events styling */
|
||||||
|
.alert-danger .border-bottom:last-child {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar card hover effects */
|
||||||
|
.card.border-left-primary { border-left-color: var(--bs-primary) !important; }
|
||||||
|
.card.border-left-success { border-left-color: var(--bs-success) !important; }
|
||||||
|
.card.border-left-warning { border-left-color: var(--bs-warning) !important; }
|
||||||
|
.card.border-left-danger { border-left-color: var(--bs-danger) !important; }
|
||||||
|
.card.border-left-info { border-left-color: var(--bs-info) !important; }
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
346
app/templates/stiftung/kalender/admin.html
Normal file
346
app/templates/stiftung/kalender/admin.html
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||||
|
<h1 class="h3 mb-0 text-gray-800">
|
||||||
|
<i class="fas fa-cogs me-2"></i>{{ title }}
|
||||||
|
</h1>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="{% url 'stiftung:kalender' %}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i>Zurück zum Kalender
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'stiftung:kalender_create' %}" class="btn btn-success">
|
||||||
|
<i class="fas fa-plus me-1"></i>Neuer Termin
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistics Cards -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-xl-3 col-md-6 mb-4">
|
||||||
|
<div class="card border-left-primary shadow h-100 py-2">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row no-gutters align-items-center">
|
||||||
|
<div class="col mr-2">
|
||||||
|
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
|
||||||
|
Benutzerdefinierte Termine
|
||||||
|
</div>
|
||||||
|
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ stats.custom_count }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<i class="fas fa-calendar fa-2x text-gray-300"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-3 col-md-6 mb-4">
|
||||||
|
<div class="card border-left-success shadow h-100 py-2">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row no-gutters align-items-center">
|
||||||
|
<div class="col mr-2">
|
||||||
|
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
|
||||||
|
Zahlungsereignisse
|
||||||
|
</div>
|
||||||
|
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ stats.payment_events }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<i class="fas fa-euro-sign fa-2x text-gray-300"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-3 col-md-6 mb-4">
|
||||||
|
<div class="card border-left-info shadow h-100 py-2">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row no-gutters align-items-center">
|
||||||
|
<div class="col mr-2">
|
||||||
|
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
|
||||||
|
Pachtverträge
|
||||||
|
</div>
|
||||||
|
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ stats.lease_events }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<i class="fas fa-handshake fa-2x text-gray-300"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-3 col-md-6 mb-4">
|
||||||
|
<div class="card border-left-warning shadow h-100 py-2">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row no-gutters align-items-center">
|
||||||
|
<div class="col mr-2">
|
||||||
|
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
|
||||||
|
Geburtstage
|
||||||
|
</div>
|
||||||
|
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ stats.birthday_events }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<i class="fas fa-birthday-cake fa-2x text-gray-300"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Controls -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">
|
||||||
|
<i class="fas fa-filter me-2"></i>Ereignisquellen & Filter
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="get" class="row g-3">
|
||||||
|
<!-- Event Source Toggles -->
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label fw-bold">Ereignisquellen anzeigen:</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" id="show_custom"
|
||||||
|
name="show_custom" value="true"
|
||||||
|
{% if show_custom %}checked{% endif %}
|
||||||
|
onchange="this.form.submit()">
|
||||||
|
<label class="form-check-label" for="show_custom">
|
||||||
|
<i class="fas fa-calendar text-primary me-1"></i>Benutzerdefinierte Termine
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" id="show_payments"
|
||||||
|
name="show_payments" value="true"
|
||||||
|
{% if show_payments %}checked{% endif %}
|
||||||
|
onchange="this.form.submit()">
|
||||||
|
<label class="form-check-label" for="show_payments">
|
||||||
|
<i class="fas fa-euro-sign text-success me-1"></i>Zahlungserinnerungen
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" id="show_leases"
|
||||||
|
name="show_leases" value="true"
|
||||||
|
{% if show_leases %}checked{% endif %}
|
||||||
|
onchange="this.form.submit()">
|
||||||
|
<label class="form-check-label" for="show_leases">
|
||||||
|
<i class="fas fa-handshake text-info me-1"></i>Pachtverträge
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" id="show_birthdays"
|
||||||
|
name="show_birthdays" value="true"
|
||||||
|
{% if show_birthdays %}checked{% endif %}
|
||||||
|
onchange="this.form.submit()">
|
||||||
|
<label class="form-check-label" for="show_birthdays">
|
||||||
|
<i class="fas fa-birthday-cake text-warning me-1"></i>Geburtstage
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Additional Filters -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="category" class="form-label">Kategorie:</label>
|
||||||
|
<select class="form-control" id="category" name="category" onchange="this.form.submit()">
|
||||||
|
<option value="">Alle Kategorien</option>
|
||||||
|
{% for value, display in categories %}
|
||||||
|
<option value="{{ value }}" {% if category_filter == value %}selected{% endif %}>
|
||||||
|
{{ display }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="priority" class="form-label">Priorität:</label>
|
||||||
|
<select class="form-control" id="priority" name="priority" onchange="this.form.submit()">
|
||||||
|
<option value="">Alle Prioritäten</option>
|
||||||
|
{% for value, display in priorities %}
|
||||||
|
<option value="{{ value }}" {% if priority_filter == value %}selected{% endif %}>
|
||||||
|
{{ display }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label"> </label>
|
||||||
|
<div>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-search me-1"></i>Filter anwenden
|
||||||
|
</button>
|
||||||
|
<a href="{% url 'stiftung:kalender_admin' %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-undo me-1"></i>Zurücksetzen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Events Table -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">
|
||||||
|
<i class="fas fa-list me-2"></i>Ereignisse ({{ stats.total_events }} Einträge)
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if events %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered" id="eventsTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Titel</th>
|
||||||
|
<th>Kategorie</th>
|
||||||
|
<th>Priorität</th>
|
||||||
|
<th>Quelle</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for event in events %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>{{ event.date|date:"d.m.Y" }}</strong>
|
||||||
|
{% if event.time %}
|
||||||
|
<br><small class="text-muted">{{ event.time|time:"H:i" }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<i class="{{ event.icon }} me-2"></i>{{ event.title }}
|
||||||
|
{% if event.description %}
|
||||||
|
<br><small class="text-muted">{{ event.description|truncatechars:50 }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-secondary">{{ event.category_display }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-{% if event.priority == 'hoch' %}danger{% elif event.priority == 'mittel' %}warning{% else %}secondary{% endif %}">
|
||||||
|
{% if event.priority == 'hoch' %}
|
||||||
|
<i class="fas fa-exclamation-triangle me-1"></i>
|
||||||
|
{% elif event.priority == 'mittel' %}
|
||||||
|
<i class="fas fa-exclamation-circle me-1"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="fas fa-info-circle me-1"></i>
|
||||||
|
{% endif %}
|
||||||
|
{{ event.priority_display|default:"Normal" }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if event.source == 'custom' %}
|
||||||
|
<span class="badge bg-primary">Benutzerdefiniert</span>
|
||||||
|
{% elif event.source == 'payment' %}
|
||||||
|
<span class="badge bg-success">Zahlung</span>
|
||||||
|
{% elif event.source == 'lease' %}
|
||||||
|
<span class="badge bg-info">Pacht</span>
|
||||||
|
{% elif event.source == 'birthday' %}
|
||||||
|
<span class="badge bg-warning">Geburtstag</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">System</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if event.source == 'custom' %}
|
||||||
|
{% if event.completed %}
|
||||||
|
<span class="badge bg-success">
|
||||||
|
<i class="fas fa-check me-1"></i>Erledigt
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-warning">
|
||||||
|
<i class="fas fa-clock me-1"></i>Ausstehend
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-light text-dark">
|
||||||
|
<i class="fas fa-robot me-1"></i>Automatisch
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if event.source == 'custom' and event.id %}
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<a href="{% url 'stiftung:kalender_detail' event.id %}"
|
||||||
|
class="btn btn-outline-primary btn-sm" title="Details">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'stiftung:kalender_edit' event.id %}"
|
||||||
|
class="btn btn-outline-secondary btn-sm" title="Bearbeiten">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'stiftung:kalender_delete' event.id %}"
|
||||||
|
class="btn btn-outline-danger btn-sm" title="Löschen">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted small">
|
||||||
|
<i class="fas fa-lock me-1"></i>Systemereignis
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas fa-calendar-times fa-3x text-muted mb-3"></i>
|
||||||
|
<h5 class="text-muted">Keine Ereignisse gefunden</h5>
|
||||||
|
<p class="text-muted">
|
||||||
|
Aktivieren Sie Ereignisquellen oder fügen Sie neue Termine hinzu.
|
||||||
|
</p>
|
||||||
|
<a href="{% url 'stiftung:kalender_create' %}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus me-1"></i>Neuen Termin hinzufügen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.border-left-primary { border-left: 0.25rem solid #4e73df !important; }
|
||||||
|
.border-left-success { border-left: 0.25rem solid #1cc88a !important; }
|
||||||
|
.border-left-info { border-left: 0.25rem solid #36b9cc !important; }
|
||||||
|
.border-left-warning { border-left: 0.25rem solid #f6c23e !important; }
|
||||||
|
|
||||||
|
.form-switch .form-check-input {
|
||||||
|
width: 2rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eventsTable th {
|
||||||
|
background-color: #f8f9fc;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
337
app/templates/stiftung/kalender/agenda_view.html
Normal file
337
app/templates/stiftung/kalender/agenda_view.html
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Calendar Header with View Controls -->
|
||||||
|
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||||
|
<h1 class="h3 mb-0 text-gray-800">
|
||||||
|
<i class="fas fa-list me-2"></i>{{ title }}
|
||||||
|
</h1>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<!-- View Type Buttons -->
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="?view=month&year={{ year }}&month={{ month }}"
|
||||||
|
class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="fas fa-calendar me-1"></i>Monat
|
||||||
|
</a>
|
||||||
|
<a href="?view=week"
|
||||||
|
class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="fas fa-calendar-week me-1"></i>Woche
|
||||||
|
</a>
|
||||||
|
<a href="?view=agenda"
|
||||||
|
class="btn btn-sm btn-primary">
|
||||||
|
<i class="fas fa-list me-1"></i>Agenda
|
||||||
|
</a>
|
||||||
|
<a href="?view=list"
|
||||||
|
class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="fas fa-list-ul me-1"></i>Liste
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{% url 'stiftung:kalender_create' %}" class="btn btn-success">
|
||||||
|
<i class="fas fa-plus me-1"></i>Neuer Termin
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Agenda View -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="fas fa-clock me-2"></i>Agenda - Chronologische Übersicht
|
||||||
|
</h6>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<!-- Date range filters -->
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
|
||||||
|
id="dateRangeDropdown" data-bs-toggle="dropdown">
|
||||||
|
Zeitraum
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="?view=agenda&range=today">Heute</a></li>
|
||||||
|
<li><a class="dropdown-item" href="?view=agenda&range=week">Diese Woche</a></li>
|
||||||
|
<li><a class="dropdown-item" href="?view=agenda&range=month">Dieser Monat</a></li>
|
||||||
|
<li><a class="dropdown-item" href="?view=agenda&range=3months">Nächste 3 Monate</a></li>
|
||||||
|
<li><a class="dropdown-item" href="?view=agenda">Alle</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if events %}
|
||||||
|
<div class="agenda-timeline">
|
||||||
|
{% regroup events by date as events_by_date %}
|
||||||
|
{% for date_group in events_by_date %}
|
||||||
|
<div class="timeline-date-group">
|
||||||
|
<div class="date-divider">
|
||||||
|
<div class="date-line"></div>
|
||||||
|
<div class="date-badge {% if date_group.grouper == today %}today{% elif date_group.grouper < today %}past{% endif %}">
|
||||||
|
<div class="date-day">{{ date_group.grouper|date:"d" }}</div>
|
||||||
|
<div class="date-month-year">
|
||||||
|
{{ date_group.grouper|date:"M Y" }}
|
||||||
|
</div>
|
||||||
|
<div class="date-weekday">{{ date_group.grouper|date:"l" }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="date-line"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="events-for-date">
|
||||||
|
{% for event in date_group.list %}
|
||||||
|
<div class="timeline-event event-{{ event.category }} {% if event.priority == 'hoch' %}high-priority{% elif event.priority == 'mittel' %}medium-priority{% endif %}">
|
||||||
|
<div class="event-time-indicator">
|
||||||
|
{% if event.time %}
|
||||||
|
<div class="event-time">{{ event.time|time:"H:i" }}</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="event-time all-day">
|
||||||
|
<i class="fas fa-clock"></i>
|
||||||
|
<span>ganztags</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="event-content">
|
||||||
|
<div class="event-header">
|
||||||
|
<h6 class="event-title mb-1">
|
||||||
|
<i class="{{ event.icon }} me-2"></i>{{ event.title }}
|
||||||
|
{% if event.priority == 'hoch' %}
|
||||||
|
<span class="badge bg-danger ms-2">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</h6>
|
||||||
|
<div class="event-meta">
|
||||||
|
<span class="badge bg-secondary me-2">{{ event.category_display }}</span>
|
||||||
|
{% if event.destinataer %}
|
||||||
|
<span class="badge bg-info">
|
||||||
|
<i class="fas fa-user me-1"></i>{{ event.destinataer }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if event.description %}
|
||||||
|
<div class="event-description">
|
||||||
|
{{ event.description }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if event.location %}
|
||||||
|
<div class="event-location">
|
||||||
|
<i class="fas fa-map-marker-alt me-1"></i>{{ event.location }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="event-actions">
|
||||||
|
<a href="{% url 'stiftung:kalender_detail' event.id %}"
|
||||||
|
class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="fas fa-eye me-1"></i>Details
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'stiftung:kalender_edit' event.id %}"
|
||||||
|
class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="fas fa-edit me-1"></i>Bearbeiten
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas fa-calendar-times fa-3x text-muted mb-3"></i>
|
||||||
|
<h5 class="text-muted">Keine Termine gefunden</h5>
|
||||||
|
<p class="text-muted">Fügen Sie einen neuen Termin hinzu oder erweitern Sie den Zeitraum.</p>
|
||||||
|
<a href="{% url 'stiftung:kalender_create' %}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus me-1"></i>Termin hinzufügen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.agenda-timeline {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-date-group {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-divider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-line {
|
||||||
|
flex: 1;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(to right, transparent, #dee2e6, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-badge {
|
||||||
|
background: white;
|
||||||
|
border: 2px solid #dee2e6;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 15px 25px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 20px;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-badge.today {
|
||||||
|
background: #e3f2fd;
|
||||||
|
border-color: #2196f3;
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-badge.past {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-color: #bdbdbd;
|
||||||
|
color: #757575;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-day {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-month-year {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-weekday {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-for-date {
|
||||||
|
padding-left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-event {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-left: 4px solid;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-event:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
transform: translateX(5px);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-event.high-priority {
|
||||||
|
box-shadow: 0 2px 8px rgba(244, 67, 54, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-time-indicator {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px 15px;
|
||||||
|
min-width: 120px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-right: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-time {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-time.all-day {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-time.all-day i {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-title {
|
||||||
|
color: #343a40;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-meta {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-description {
|
||||||
|
color: #6c757d;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-location {
|
||||||
|
color: #6c757d;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event category colors */
|
||||||
|
.event-termin { border-left-color: #2196f3; }
|
||||||
|
.event-zahlung { border-left-color: #ff9800; }
|
||||||
|
.event-deadline { border-left-color: #f44336; }
|
||||||
|
.event-geburtstag { border-left-color: #4caf50; }
|
||||||
|
.event-vertrag { border-left-color: #9c27b0; }
|
||||||
|
.event-pruefung { border-left-color: #009688; }
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.timeline-event {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-time-indicator {
|
||||||
|
min-width: auto;
|
||||||
|
padding: 15px;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-for-date {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-badge {
|
||||||
|
margin: 0 10px;
|
||||||
|
min-width: 100px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
125
app/templates/stiftung/kalender/create.html
Normal file
125
app/templates/stiftung/kalender/create.html
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||||
|
<h1 class="h3 mb-0 text-gray-800">
|
||||||
|
<i class="fas fa-calendar-plus me-2"></i>{{ title }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="titel" class="form-label">Titel *</label>
|
||||||
|
<input type="text" class="form-control" id="titel" name="titel" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="datum" class="form-label">Datum *</label>
|
||||||
|
<input type="date" class="form-control" id="datum" name="datum" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="zeit" class="form-label">Uhrzeit</label>
|
||||||
|
<input type="time" class="form-control" id="zeit" name="zeit">
|
||||||
|
<small class="text-muted">Leer lassen für ganztägig</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="beschreibung" class="form-label">Beschreibung</label>
|
||||||
|
<textarea class="form-control" id="beschreibung" name="beschreibung" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="kategorie" class="form-label">Kategorie</label>
|
||||||
|
<select class="form-select" id="kategorie" name="kategorie">
|
||||||
|
<option value="termin">Termin/Meeting</option>
|
||||||
|
<option value="zahlung">Zahlungserinnerung</option>
|
||||||
|
<option value="deadline">Frist/Deadline</option>
|
||||||
|
<option value="geburtstag">Geburtstag</option>
|
||||||
|
<option value="vertrag">Vertrag läuft aus</option>
|
||||||
|
<option value="pruefung">Prüfung/Nachweis</option>
|
||||||
|
<option value="sonstiges">Sonstiges</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="prioritaet" class="form-label">Priorität</label>
|
||||||
|
<select class="form-select" id="prioritaet" name="prioritaet">
|
||||||
|
<option value="niedrig">Niedrig</option>
|
||||||
|
<option value="normal" selected>Normal</option>
|
||||||
|
<option value="hoch">Hoch</option>
|
||||||
|
<option value="kritisch">Kritisch</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="{% url 'stiftung:kalender' %}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i>Abbrechen
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save me-1"></i>Speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card border-0 bg-light">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>Hilfe
|
||||||
|
</h6>
|
||||||
|
<p class="small text-muted">
|
||||||
|
Erstellen Sie hier neue Kalenderereignisse für wichtige Termine,
|
||||||
|
Zahlungserinnerungen oder andere stiftungsbezogene Ereignisse.
|
||||||
|
</p>
|
||||||
|
<hr>
|
||||||
|
<h6 class="small">Kategorien:</h6>
|
||||||
|
<ul class="small text-muted">
|
||||||
|
<li><strong>Termin:</strong> Meetings, Besprechungen</li>
|
||||||
|
<li><strong>Zahlung:</strong> Fällige Unterstützungen</li>
|
||||||
|
<li><strong>Deadline:</strong> Fristen für Nachweise</li>
|
||||||
|
<li><strong>Vertrag:</strong> Auslaufende Pachtverträge</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Set today as default date
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const dateInput = document.getElementById('datum');
|
||||||
|
if (dateInput && !dateInput.value) {
|
||||||
|
const today = new Date();
|
||||||
|
const yyyy = today.getFullYear();
|
||||||
|
const mm = String(today.getMonth() + 1).padStart(2, '0');
|
||||||
|
const dd = String(today.getDate()).padStart(2, '0');
|
||||||
|
dateInput.value = `${yyyy}-${mm}-${dd}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
32
app/templates/stiftung/kalender/delete.html
Normal file
32
app/templates/stiftung/kalender/delete.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h1><i class="fas fa-trash-alt me-2"></i>{{ title }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||||
|
Löschfunktion wird implementiert.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<a href="{% url 'stiftung:kalender' %}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i>Zurück zum Kalender
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
108
app/templates/stiftung/kalender/delete_confirm.html
Normal file
108
app/templates/stiftung/kalender/delete_confirm.html
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||||
|
<h1 class="h3 mb-0 text-gray-800 text-danger">
|
||||||
|
<i class="fas fa-exclamation-triangle me-2"></i>{{ title }}
|
||||||
|
</h1>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="{% url 'stiftung:kalender_detail' event.pk %}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i>Zurück
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirmation Form -->
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card shadow border-danger">
|
||||||
|
<div class="card-header bg-danger text-white">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="fas fa-trash me-2"></i>Termin löschen - Bestätigung erforderlich
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||||
|
<strong>Achtung!</strong> Diese Aktion kann nicht rückgängig gemacht werden.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mb-4">
|
||||||
|
Sind Sie sicher, dass Sie den folgenden Kalendereintrag permanent löschen möchten?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Event Details Summary -->
|
||||||
|
<div class="bg-light p-4 rounded mb-4">
|
||||||
|
<h6 class="text-muted mb-3">Zu löschender Termin:</h6>
|
||||||
|
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-sm-3"><strong>Titel:</strong></div>
|
||||||
|
<div class="col-sm-9">{{ event.titel }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-sm-3"><strong>Datum:</strong></div>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
{{ event.datum|date:"d.m.Y" }}
|
||||||
|
{% if event.uhrzeit %}um {{ event.uhrzeit|time:"H:i" }} Uhr{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-sm-3"><strong>Kategorie:</strong></div>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<span class="badge bg-info">{{ event.get_kategorie_display }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if event.beschreibung %}
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-sm-3"><strong>Beschreibung:</strong></div>
|
||||||
|
<div class="col-sm-9">{{ event.beschreibung|truncatechars:100 }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-3"><strong>Erstellt:</strong></div>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
{{ event.erstellt_am|date:"d.m.Y H:i" }}
|
||||||
|
{% if event.erstellt_von %}von {{ event.erstellt_von }}{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" class="d-flex justify-content-between align-items-center">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<a href="{% url 'stiftung:kalender_detail' event.pk %}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-times me-1"></i>Abbrechen
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-danger">
|
||||||
|
<i class="fas fa-trash me-1"></i>Ja, endgültig löschen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Add confirmation dialog as extra safety measure
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const deleteForm = document.querySelector('form');
|
||||||
|
if (deleteForm) {
|
||||||
|
deleteForm.addEventListener('submit', function(e) {
|
||||||
|
if (!confirm('Sind Sie wirklich sicher? Diese Aktion kann nicht rückgängig gemacht werden!')) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
167
app/templates/stiftung/kalender/detail.html
Normal file
167
app/templates/stiftung/kalender/detail.html
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||||
|
<h1 class="h3 mb-0 text-gray-800">
|
||||||
|
<i class="fas fa-calendar-alt me-2"></i>{{ title }}
|
||||||
|
</h1>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="{% url 'stiftung:kalender' %}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i>Zurück zum Kalender
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'stiftung:kalender_edit' event.pk %}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-edit me-1"></i>Bearbeiten
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'stiftung:kalender_delete' event.pk %}" class="btn btn-danger">
|
||||||
|
<i class="fas fa-trash me-1"></i>Löschen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Event Details -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>Termindetails
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6 class="text-muted">Titel</h6>
|
||||||
|
<p class="h5">{{ event.titel }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6 class="text-muted">Datum</h6>
|
||||||
|
<p class="h5">
|
||||||
|
<i class="fas fa-calendar me-2"></i>{{ event.datum|date:"d.m.Y" }}
|
||||||
|
{% if event.uhrzeit %}
|
||||||
|
<br><small class="text-muted">
|
||||||
|
<i class="fas fa-clock me-1"></i>{{ event.uhrzeit|time:"H:i" }} Uhr
|
||||||
|
</small>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6 class="text-muted">Kategorie</h6>
|
||||||
|
<span class="badge bg-info fs-6">
|
||||||
|
<i class="fas fa-tag me-1"></i>{{ event.get_kategorie_display }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6 class="text-muted">Priorität</h6>
|
||||||
|
<span class="badge bg-{% if event.prioritaet == 'hoch' %}danger{% elif event.prioritaet == 'mittel' %}warning{% else %}secondary{% endif %} fs-6">
|
||||||
|
<i class="fas fa-{% if event.prioritaet == 'hoch' %}exclamation-triangle{% elif event.prioritaet == 'mittel' %}exclamation-circle{% else %}info-circle{% endif %} me-1"></i>{{ event.get_prioritaet_display }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if event.beschreibung %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="text-muted">Beschreibung</h6>
|
||||||
|
<div class="bg-light p-3 rounded">
|
||||||
|
{{ event.beschreibung|linebreaks }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if event.destinataer %}
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h6 class="text-muted">Bezogen auf Destinatär</h6>
|
||||||
|
<p>
|
||||||
|
<i class="fas fa-user me-2"></i>
|
||||||
|
<a href="{% url 'stiftung:destinataer_detail' event.destinataer.pk %}">
|
||||||
|
{{ event.destinataer }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if event.verpachtung %}
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h6 class="text-muted">Bezogen auf Verpachtung</h6>
|
||||||
|
<p>
|
||||||
|
<i class="fas fa-handshake me-2"></i>
|
||||||
|
<a href="{% url 'stiftung:verpachtung_detail' event.verpachtung.pk %}">
|
||||||
|
{{ event.verpachtung }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<!-- Status Card -->
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header bg-{% if event.erledigt %}success{% else %}warning{% endif %} text-white">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="fas fa-{% if event.erledigt %}check-circle{% else %}clock{% endif %} me-2"></i>Status
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if event.erledigt %}
|
||||||
|
<div class="text-center">
|
||||||
|
<i class="fas fa-check-circle fa-3x text-success mb-3"></i>
|
||||||
|
<h6 class="text-success">Erledigt</h6>
|
||||||
|
<p class="text-muted">Dieser Termin wurde als erledigt markiert.</p>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center">
|
||||||
|
<i class="fas fa-clock fa-3x text-warning mb-3"></i>
|
||||||
|
<h6 class="text-warning">Ausstehend</h6>
|
||||||
|
<p class="text-muted">Dieser Termin steht noch aus.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Meta Information -->
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-header bg-secondary text-white">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="fas fa-info me-2"></i>Meta-Informationen
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<small class="text-muted">Erstellt von:</small><br>
|
||||||
|
<strong>{{ event.erstellt_von|default:"System" }}</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<small class="text-muted">Erstellt am:</small><br>
|
||||||
|
<strong>{{ event.erstellt_am|date:"d.m.Y H:i" }}</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if event.aktualisiert_am %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<small class="text-muted">Zuletzt aktualisiert:</small><br>
|
||||||
|
<strong>{{ event.aktualisiert_am|date:"d.m.Y H:i" }}</strong>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<small class="text-muted">Termin-ID:</small><br>
|
||||||
|
<code class="small">{{ event.pk }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
32
app/templates/stiftung/kalender/detail_old.html
Normal file
32
app/templates/stiftung/kalender/detail_old.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h1><i class="fas fa-calendar-alt me-2"></i>{{ title }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-info" role="alert">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>
|
||||||
|
Termin-Details werden hier angezeigt, sobald die Funktionalität vollständig implementiert ist.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<a href="{% url 'stiftung:kalender' %}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i>Zurück zum Kalender
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
154
app/templates/stiftung/kalender/edit.html
Normal file
154
app/templates/stiftung/kalender/edit.html
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||||
|
<h1 class="h3 mb-0 text-gray-800">
|
||||||
|
<i class="fas fa-edit me-2"></i>{{ title }}
|
||||||
|
</h1>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="{% url 'stiftung:kalender_detail' event.pk %}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i>Zurück
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Form -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="fas fa-calendar-edit me-2"></i>Termin bearbeiten
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label for="titel" class="form-label">
|
||||||
|
<i class="fas fa-heading me-1"></i>Titel *
|
||||||
|
</label>
|
||||||
|
<input type="text" class="form-control" id="titel" name="titel"
|
||||||
|
value="{{ event.titel }}" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="datum" class="form-label">
|
||||||
|
<i class="fas fa-calendar me-1"></i>Datum *
|
||||||
|
</label>
|
||||||
|
<input type="date" class="form-control" id="datum" name="datum"
|
||||||
|
value="{{ event.datum|date:'Y-m-d' }}" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="zeit" class="form-label">
|
||||||
|
<i class="fas fa-clock me-1"></i>Uhrzeit
|
||||||
|
</label>
|
||||||
|
<input type="time" class="form-control" id="zeit" name="zeit"
|
||||||
|
value="{% if event.uhrzeit %}{{ event.uhrzeit|time:'H:i' }}{% endif %}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="kategorie" class="form-label">
|
||||||
|
<i class="fas fa-tag me-1"></i>Kategorie
|
||||||
|
</label>
|
||||||
|
<select class="form-control" id="kategorie" name="kategorie">
|
||||||
|
<option value="termin" {% if event.kategorie == 'termin' %}selected{% endif %}>Termin</option>
|
||||||
|
<option value="zahlung" {% if event.kategorie == 'zahlung' %}selected{% endif %}>Zahlung</option>
|
||||||
|
<option value="deadline" {% if event.kategorie == 'deadline' %}selected{% endif %}>Deadline</option>
|
||||||
|
<option value="geburtstag" {% if event.kategorie == 'geburtstag' %}selected{% endif %}>Geburtstag</option>
|
||||||
|
<option value="vertrag" {% if event.kategorie == 'vertrag' %}selected{% endif %}>Vertrag</option>
|
||||||
|
<option value="pruefung" {% if event.kategorie == 'pruefung' %}selected{% endif %}>Prüfung</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="prioritaet" class="form-label">
|
||||||
|
<i class="fas fa-exclamation-circle me-1"></i>Priorität
|
||||||
|
</label>
|
||||||
|
<select class="form-control" id="prioritaet" name="prioritaet">
|
||||||
|
<option value="niedrig" {% if event.prioritaet == 'niedrig' %}selected{% endif %}>Niedrig</option>
|
||||||
|
<option value="normal" {% if event.prioritaet == 'normal' %}selected{% endif %}>Normal</option>
|
||||||
|
<option value="hoch" {% if event.prioritaet == 'hoch' %}selected{% endif %}>Hoch</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="beschreibung" class="form-label">
|
||||||
|
<i class="fas fa-align-left me-1"></i>Beschreibung
|
||||||
|
</label>
|
||||||
|
<textarea class="form-control" id="beschreibung" name="beschreibung"
|
||||||
|
rows="4" placeholder="Optionale Beschreibung...">{{ event.beschreibung }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ort" class="form-label">
|
||||||
|
<i class="fas fa-map-marker-alt me-1"></i>Ort
|
||||||
|
</label>
|
||||||
|
<input type="text" class="form-control" id="ort" name="ort"
|
||||||
|
value="{{ event.ort }}" placeholder="Optional...">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="erledigt"
|
||||||
|
name="erledigt" {% if event.erledigt %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="erledigt">
|
||||||
|
<i class="fas fa-check-circle me-1"></i>Als erledigt markieren
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="{% url 'stiftung:kalender_detail' event.pk %}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-times me-1"></i>Abbrechen
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-success">
|
||||||
|
<i class="fas fa-save me-1"></i>Änderungen speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<!-- Help Card -->
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-header bg-info text-white">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="fas fa-question-circle me-2"></i>Hilfe
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h6>Kategorien:</h6>
|
||||||
|
<ul class="small">
|
||||||
|
<li><strong>Termin:</strong> Allgemeine Termine und Meetings</li>
|
||||||
|
<li><strong>Zahlung:</strong> Zahlungserinnerungen</li>
|
||||||
|
<li><strong>Deadline:</strong> Wichtige Fristen</li>
|
||||||
|
<li><strong>Geburtstag:</strong> Geburtstage</li>
|
||||||
|
<li><strong>Vertrag:</strong> Vertragsereignisse</li>
|
||||||
|
<li><strong>Prüfung:</strong> Überprüfungen und Kontrollen</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h6 class="mt-3">Prioritäten:</h6>
|
||||||
|
<ul class="small">
|
||||||
|
<li><span class="badge bg-secondary me-1">Niedrig</span> Optionale Termine</li>
|
||||||
|
<li><span class="badge bg-warning me-1">Normal</span> Standard-Priorität</li>
|
||||||
|
<li><span class="badge bg-danger me-1">Hoch</span> Wichtige/dringende Termine</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
42
app/templates/stiftung/kalender/form.html
Normal file
42
app/templates/stiftung/kalender/form.html
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h1><i class="fas fa-calendar-plus me-2"></i>{{ title }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-info" role="alert">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>
|
||||||
|
Die Kalender-Funktionalität wird derzeit implementiert.
|
||||||
|
Bald können Sie hier Termine, Zahlungserinnerungen und wichtige Fristen verwalten.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Geplante Funktionen:</p>
|
||||||
|
<ul>
|
||||||
|
<li>📅 Termine und Meetings</li>
|
||||||
|
<li>💰 Zahlungserinnerungen</li>
|
||||||
|
<li>⏰ Fristen und Deadlines</li>
|
||||||
|
<li>🎂 Geburtstage</li>
|
||||||
|
<li>📄 Vertragsauslauf-Erinnerungen</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<a href="{% url 'stiftung:kalender' %}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i>Zurück zum Kalender
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
60
app/templates/stiftung/kalender/kalender.html
Normal file
60
app/templates/stiftung/kalender/kalender.html
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1><i class="fas fa-calendar-alt me-2"></i>{{ title }}</h1>
|
||||||
|
<a href="{% url 'stiftung:kalender_create' %}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus me-1"></i>Neuer Eintrag
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0">{{ current_month }}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if events %}
|
||||||
|
<div class="list-group">
|
||||||
|
{% for event in events %}
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex w-100 justify-content-between">
|
||||||
|
<div>
|
||||||
|
<i class="{{ event.icon }} me-2 text-{{ event.color }}"></i>
|
||||||
|
<strong>{{ event.title }}</strong>
|
||||||
|
{% if event.description %}
|
||||||
|
<p class="mb-1 text-muted">{{ event.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<small class="text-{{ event.color }}">
|
||||||
|
{{ event.date }}
|
||||||
|
{% if event.time %} {{ event.time }}{% endif %}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas fa-calendar-times fa-3x text-muted mb-3"></i>
|
||||||
|
<h5 class="text-muted">Keine Termine im aktuellen Monat</h5>
|
||||||
|
<p class="text-muted">Erstellen Sie Ihren ersten Kalendereintrag.</p>
|
||||||
|
<a href="{% url 'stiftung:kalender_create' %}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus me-1"></i>Termin hinzufügen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
140
app/templates/stiftung/kalender/list_view.html
Normal file
140
app/templates/stiftung/kalender/list_view.html
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Calendar Header with View Controls -->
|
||||||
|
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||||
|
<h1 class="h3 mb-0 text-gray-800">
|
||||||
|
<i class="fas fa-list me-2"></i>{{ title }}
|
||||||
|
</h1>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<!-- View Type Buttons -->
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="?view=month&year={{ year }}&month={{ month }}"
|
||||||
|
class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="fas fa-calendar me-1"></i>Monat
|
||||||
|
</a>
|
||||||
|
<a href="?view=week"
|
||||||
|
class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="fas fa-calendar-week me-1"></i>Woche
|
||||||
|
</a>
|
||||||
|
<a href="?view=agenda"
|
||||||
|
class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="fas fa-list me-1"></i>Agenda
|
||||||
|
</a>
|
||||||
|
<a href="?view=list"
|
||||||
|
class="btn btn-sm btn-primary">
|
||||||
|
<i class="fas fa-list-ul me-1"></i>Liste
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{% url 'stiftung:kalender_create' %}" class="btn btn-success">
|
||||||
|
<i class="fas fa-plus me-1"></i>Neuer Termin
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Event List -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-body">
|
||||||
|
{% if events %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="100">Datum</th>
|
||||||
|
<th width="60">Zeit</th>
|
||||||
|
<th>Titel</th>
|
||||||
|
<th>Beschreibung</th>
|
||||||
|
<th width="120">Kategorie</th>
|
||||||
|
<th width="100">Priorität</th>
|
||||||
|
<th width="80">Status</th>
|
||||||
|
<th width="100">Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for event in events %}
|
||||||
|
<tr class="{% if event.overdue %}table-danger{% elif event.completed %}table-success{% endif %}">
|
||||||
|
<td>
|
||||||
|
<strong>{{ event.date|date:"d.m.Y" }}</strong>
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">{{ event.date|date:"l" }}</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if event.time %}
|
||||||
|
{{ event.time }}
|
||||||
|
{% else %}
|
||||||
|
<small class="text-muted">ganztags</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="{{ event.icon }} me-2 text-{{ event.color }}"></i>
|
||||||
|
<strong>{{ event.title }}</strong>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<small>{{ event.description|default:"-" }}</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-{{ event.color }}">
|
||||||
|
{{ event.category|title }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if event.priority == 'kritisch' %}
|
||||||
|
<span class="badge bg-danger">{{ event.priority|title }}</span>
|
||||||
|
{% elif event.priority == 'hoch' %}
|
||||||
|
<span class="badge bg-warning">{{ event.priority|title }}</span>
|
||||||
|
{% elif event.priority == 'normal' %}
|
||||||
|
<span class="badge bg-primary">{{ event.priority|title }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">{{ event.priority|title }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if event.overdue %}
|
||||||
|
<span class="badge bg-danger">Überfällig</span>
|
||||||
|
{% elif event.completed %}
|
||||||
|
<span class="badge bg-success">Erledigt</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-info">Offen</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
{% if event.url %}
|
||||||
|
<a href="{{ event.url }}" class="btn btn-outline-primary" title="Details">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{% url 'stiftung:kalender_edit' event.id %}" class="btn btn-outline-secondary" title="Bearbeiten">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas fa-calendar-times fa-3x text-muted mb-3"></i>
|
||||||
|
<h5 class="text-muted">Keine Termine im gewählten Zeitraum</h5>
|
||||||
|
<p class="text-muted">Fügen Sie Ihren ersten Kalendereintrag hinzu.</p>
|
||||||
|
<a href="{% url 'stiftung:kalender_create' %}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus me-1"></i>Termin hinzufügen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
200
app/templates/stiftung/kalender/month_view.html
Normal file
200
app/templates/stiftung/kalender/month_view.html
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Calendar Header with View Controls -->
|
||||||
|
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||||
|
<h1 class="h3 mb-0 text-gray-800">
|
||||||
|
<i class="fas fa-calendar-alt me-2"></i>{{ title }}
|
||||||
|
</h1>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<!-- View Type Buttons -->
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="?view=month&year={{ year }}&month={{ month }}"
|
||||||
|
class="btn btn-sm btn-primary">
|
||||||
|
<i class="fas fa-calendar me-1"></i>Monat
|
||||||
|
</a>
|
||||||
|
<a href="?view=week"
|
||||||
|
class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="fas fa-calendar-week me-1"></i>Woche
|
||||||
|
</a>
|
||||||
|
<a href="?view=agenda"
|
||||||
|
class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="fas fa-list me-1"></i>Agenda
|
||||||
|
</a>
|
||||||
|
<a href="?view=list"
|
||||||
|
class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="fas fa-list-ul me-1"></i>Liste
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{% url 'stiftung:kalender_create' %}" class="btn btn-success">
|
||||||
|
<i class="fas fa-plus me-1"></i>Neuer Termin
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation for Month View -->
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="?view=month&year={{ prev_year }}&month={{ prev_month }}"
|
||||||
|
class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-chevron-left me-1"></i>Vorheriger Monat
|
||||||
|
</a>
|
||||||
|
<a href="?view=month&year={{ today.year }}&month={{ today.month }}"
|
||||||
|
class="btn btn-secondary">Heute</a>
|
||||||
|
<a href="?view=month&year={{ next_year }}&month={{ next_month }}"
|
||||||
|
class="btn btn-outline-secondary">
|
||||||
|
Nächster Monat<i class="fas fa-chevron-right ms-1"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h4 class="mb-0">{{ month_name }} {{ year }}</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Month Calendar Grid -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="calendar-grid">
|
||||||
|
<!-- Weekday Headers -->
|
||||||
|
<div class="calendar-header">
|
||||||
|
{% for weekday in weekdays %}
|
||||||
|
<div class="weekday-header">{{ weekday }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Calendar Days -->
|
||||||
|
{% for week in calendar_grid %}
|
||||||
|
<div class="calendar-week">
|
||||||
|
{% for day_data in week %}
|
||||||
|
<div class="calendar-day {% if day_data.is_today %}today{% endif %} {% if not day_data %}empty{% endif %}">
|
||||||
|
{% if day_data %}
|
||||||
|
<div class="day-number">{{ day_data.day }}</div>
|
||||||
|
{% if day_data.events %}
|
||||||
|
<div class="day-events">
|
||||||
|
{% for event in day_data.events %}
|
||||||
|
<div class="event-item event-{{ event.category }}" title="{{ event.title }} - {{ event.description }}">
|
||||||
|
<i class="{{ event.icon }}"></i>
|
||||||
|
<span>{{ event.title|truncatechars:20 }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% if day_data.event_count > 3 %}
|
||||||
|
<div class="more-events">+{{ day_data.event_count|add:"-3" }} weitere</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.calendar-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto repeat(6, 1fr);
|
||||||
|
gap: 1px;
|
||||||
|
background-color: #dee2e6;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-header {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-week {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day {
|
||||||
|
background-color: white;
|
||||||
|
min-height: 120px;
|
||||||
|
padding: 8px;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.empty {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.today {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
border: 2px solid #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-number {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.today .day-number {
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-events {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-item {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-item i {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-termin { background-color: #e3f2fd; color: #1565c0; }
|
||||||
|
.event-zahlung { background-color: #fff3e0; color: #ef6c00; }
|
||||||
|
.event-deadline { background-color: #ffebee; color: #c62828; }
|
||||||
|
.event-geburtstag { background-color: #e8f5e8; color: #2e7d32; }
|
||||||
|
.event-vertrag { background-color: #f3e5f5; color: #7b1fa2; }
|
||||||
|
.event-pruefung { background-color: #e0f2f1; color: #00695c; }
|
||||||
|
|
||||||
|
.more-events {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
color: #757575;
|
||||||
|
font-style: italic;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
184
app/templates/stiftung/kalender/week_view.html
Normal file
184
app/templates/stiftung/kalender/week_view.html
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Calendar Header with View Controls -->
|
||||||
|
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||||
|
<h1 class="h3 mb-0 text-gray-800">
|
||||||
|
<i class="fas fa-calendar-week me-2"></i>{{ title }}
|
||||||
|
</h1>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<!-- View Type Buttons -->
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="?view=month&year={{ year }}&month={{ month }}"
|
||||||
|
class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="fas fa-calendar me-1"></i>Monat
|
||||||
|
</a>
|
||||||
|
<a href="?view=week"
|
||||||
|
class="btn btn-sm btn-primary">
|
||||||
|
<i class="fas fa-calendar-week me-1"></i>Woche
|
||||||
|
</a>
|
||||||
|
<a href="?view=agenda"
|
||||||
|
class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="fas fa-list me-1"></i>Agenda
|
||||||
|
</a>
|
||||||
|
<a href="?view=list"
|
||||||
|
class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="fas fa-list-ul me-1"></i>Liste
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{% url 'stiftung:kalender_create' %}" class="btn btn-success">
|
||||||
|
<i class="fas fa-plus me-1"></i>Neuer Termin
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Week View -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="mb-0">{{ start_date|date:"d.m.Y" }} - {{ end_date|date:"d.m.Y" }}</h6>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<a href="?view=week&date={{ start_date|add:'-7'|date:'Y-m-d' }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-chevron-left"></i> Vorherige Woche
|
||||||
|
</a>
|
||||||
|
<a href="?view=week" class="btn btn-secondary">Diese Woche</a>
|
||||||
|
<a href="?view=week&date={{ start_date|add:'7'|date:'Y-m-d' }}" class="btn btn-outline-secondary">
|
||||||
|
Nächste Woche <i class="fas fa-chevron-right"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if events %}
|
||||||
|
<div class="week-timeline">
|
||||||
|
{% for i in "1234567"|make_list %}
|
||||||
|
{% with day_date=start_date|add:forloop.counter0 %}
|
||||||
|
<div class="day-column">
|
||||||
|
<div class="day-header {% if day_date == today %}today{% endif %}">
|
||||||
|
<div class="day-name">{{ day_date|date:"l" }}</div>
|
||||||
|
<div class="day-date">{{ day_date|date:"d.m" }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="day-events">
|
||||||
|
{% for event in events %}
|
||||||
|
{% if event.date == day_date %}
|
||||||
|
<div class="event-block event-{{ event.category }}">
|
||||||
|
<div class="event-time">
|
||||||
|
{% if event.time %}{{ event.time }}{% else %}ganztags{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="event-title">
|
||||||
|
<i class="{{ event.icon }} me-1"></i>{{ event.title }}
|
||||||
|
</div>
|
||||||
|
{% if event.description %}
|
||||||
|
<div class="event-desc">{{ event.description|truncatechars:50 }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas fa-calendar-times fa-3x text-muted mb-3"></i>
|
||||||
|
<h5 class="text-muted">Keine Termine diese Woche</h5>
|
||||||
|
<p class="text-muted">Fügen Sie einen neuen Termin hinzu.</p>
|
||||||
|
<a href="{% url 'stiftung:kalender_create' %}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus me-1"></i>Termin hinzufügen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.week-timeline {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 1px;
|
||||||
|
background-color: #dee2e6;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-column {
|
||||||
|
background-color: white;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-header {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-header.today {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
color: #1976d2;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-name {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-date {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-events {
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-block {
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 4px solid;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-block:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-time {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-title {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-desc {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-termin { background-color: #e3f2fd; border-left-color: #2196f3; }
|
||||||
|
.event-zahlung { background-color: #fff3e0; border-left-color: #ff9800; }
|
||||||
|
.event-deadline { background-color: #ffebee; border-left-color: #f44336; }
|
||||||
|
.event-geburtstag { background-color: #e8f5e8; border-left-color: #4caf50; }
|
||||||
|
.event-vertrag { background-color: #f3e5f5; border-left-color: #9c27b0; }
|
||||||
|
.event-pruefung { background-color: #e0f2f1; border-left-color: #009688; }
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
38
menu-structure.csv
Normal file
38
menu-structure.csv
Normal file
@@ -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
|
||||||
|
Reference in New Issue
Block a user