Skip to content

Auslaufende Termine erfordern manuelle Aktualisierung bei Filter-Änderungen #9

@bwl21

Description

@bwl21

Issue: Auslaufende Termine erfordern manuelle Aktualisierung bei Filter-Änderungen

🐛 Problem Description

Im Expiring Appointments Admin Panel werden Daten nicht automatisch aktualisiert, wenn Filter geändert werden. Benutzer müssen manuell den "Aktualisieren"-Button klicken, um die gefilterten Ergebnisse zu sehen. Dies ist inkonsistent mit dem erwarteten Verhalten und führt zu Verwirrung.

🔍 Current Behavior

Beobachtetes Verhalten:

  1. Filter ändern: Benutzer wählt anderen Zeitraum (z.B. "7 Tage" → "30 Tage")
  2. Keine Aktualisierung: Tabelle zeigt weiterhin alte Daten
  3. Manueller Refresh nötig: Benutzer muss "Aktualisieren"-Button klicken
  4. Dann Update: Erst jetzt werden neue Daten geladen

Betroffene Filter:

  • Tage-Filter: "1, 7, 14, 30, 60, 90, 180, 365 Tage" oder "alle"
  • Kalender-Filter: Dropdown mit verfügbaren Kalendern
  • Status-Filter: "Läuft bald ab", "Abgelaufen", "Aktiv"

Aktuelle Implementierung (Problematisch):

<!-- ExpiringAppointmentsAdmin.vue -->
<template>
  <!-- Tage-Filter -->
  <select v-model="daysInAdvance" @change="refreshData" class="ct-select">
    <option value="alle">alle</option>
    <option value="1">1</option>
    <option value="7">7</option>
    <!-- ... -->
  </select>

  <!-- Kalender-Filter -->
  <select v-model="calendarFilter" class="ct-select filter-select">
    <!-- KEIN @change Event! -->
    <option value="">Alle Kalender</option>
    <!-- ... -->
  </select>

  <!-- Status-Filter -->
  <select v-model="statusFilter" class="ct-select filter-select">
    <!-- KEIN @change Event! -->
    <option value="">Alle Status</option>
    <!-- ... -->
  </select>
</template>

<script setup lang="ts">
// Nur daysInAdvance triggert refreshData()
// calendarFilter und statusFilter sind nur reactive, aber triggern keine API-Calls
</script>

⚠️ Inconsistent Behavior

Problem-Analyse:

  1. Tage-Filter: ✅ Triggert refreshData() bei Änderung
  2. Kalender-Filter: ❌ Nur client-seitige Filterung
  3. Status-Filter: ❌ Nur client-seitige Filterung

Warum ist das problematisch?

Tage-Filter Verhalten:

// Lädt neue Daten vom Server
const changeDaysFilter = () => {
  refreshData() // API-Call mit neuem daysInAdvance
}

Andere Filter Verhalten:

// Nur client-seitige Filterung der bereits geladenen Daten
const filteredAppointments = computed(() => {
  let filtered = appointments.value

  // Kalender-Filter
  if (calendarFilter.value) {
    filtered = filtered.filter(apt => 
      apt.base?.calendar?.id?.toString() === calendarFilter.value
    )
  }

  // Status-Filter  
  if (statusFilter.value) {
    filtered = filtered.filter(apt => 
      getAppointmentStatus(apt) === statusFilter.value
    )
  }

  return filtered
})

🎯 Expected Behavior

Konsistentes Verhalten für alle Filter:

  1. Sofortige Aktualisierung: Alle Filter sollten sofort wirken
  2. Keine manuellen Refreshs: Benutzer sollte nie "Aktualisieren" klicken müssen
  3. Einheitliche UX: Alle Filter verhalten sich gleich

Zwei mögliche Ansätze:

Ansatz 1: Alle Filter client-seitig (Empfohlen)

// Lade einmal ALLE Daten, filtere dann client-seitig
const fetchData = async () => {
  // Lade alle Termine (großer Zeitraum)
  const allAppointments = await findExpiringSeries(365 * 24 * 60 * 60 * 1000) // 1 Jahr
  appointments.value = allAppointments
}

const filteredAppointments = computed(() => {
  let filtered = appointments.value

  // Tage-Filter (client-seitig)
  if (daysInAdvance.value !== 'alle') {
    const days = parseInt(daysInAdvance.value)
    const maxDate = new Date(Date.now() + days * 24 * 60 * 60 * 1000)
    
    filtered = filtered.filter(apt => {
      const endDate = getEffectiveEndDate(apt)
      return endDate && endDate <= maxDate
    })
  }

  // Kalender-Filter
  if (calendarFilter.value) {
    filtered = filtered.filter(apt => 
      apt.base?.calendar?.id?.toString() === calendarFilter.value
    )
  }

  // Status-Filter
  if (statusFilter.value) {
    filtered = filtered.filter(apt => 
      getAppointmentStatus(apt) === statusFilter.value
    )
  }

  return filtered
})

Ansatz 2: Alle Filter server-seitig

// Jeder Filter triggert neuen API-Call
watch([daysInAdvance, calendarFilter, statusFilter], () => {
  refreshData()
}, { deep: true })

const fetchData = async () => {
  const params = {
    daysInAdvance: daysInAdvance.value,
    calendarId: calendarFilter.value,
    status: statusFilter.value
  }
  
  const filteredAppointments = await findExpiringSeriesFiltered(params)
  appointments.value = filteredAppointments
}

🔧 Recommended Solution: Client-Side Filtering

Warum client-seitig besser ist:

  1. Performance: Keine API-Calls bei Filter-Änderungen
  2. Instant Response: Sofortige UI-Updates
  3. Reduced Server Load: Weniger ChurchTools API-Aufrufe
  4. Better UX: Keine Loading-Spinner bei Filter-Änderungen
  5. Caching-Friendly: Funktioniert gut mit TanStack Query

Implementation Plan:

1. Einmaliger Daten-Load:

// Lade alle relevanten Termine einmal
const fetchAllAppointments = async () => {
  isLoading.value = true
  try {
    // Lade großen Zeitraum (z.B. 1 Jahr)
    const allAppointments = await findExpiringSeries(365 * 24 * 60 * 60 * 1000)
    allAppointmentsData.value = allAppointments
  } finally {
    isLoading.value = false
  }
}

2. Reactive Filtering:

const filteredAppointments = computed(() => {
  if (!allAppointmentsData.value) return []
  
  let filtered = [...allAppointmentsData.value]
  
  // Tage-Filter
  if (daysInAdvance.value !== 'alle') {
    const days = parseInt(daysInAdvance.value)
    const cutoffDate = new Date(Date.now() + days * 24 * 60 * 60 * 1000)
    
    filtered = filtered.filter(appointment => {
      const endDate = getEffectiveEndDate(appointment)
      return endDate && endDate <= cutoffDate
    })
  }
  
  // Kalender-Filter
  if (calendarFilter.value) {
    filtered = filtered.filter(appointment => 
      appointment.base?.calendar?.id?.toString() === calendarFilter.value
    )
  }
  
  // Status-Filter
  if (statusFilter.value) {
    filtered = filtered.filter(appointment => 
      getAppointmentStatus(appointment) === statusFilter.value
    )
  }
  
  return filtered
})

3. Remove Manual Refresh Dependency:

<template>
  <!-- Alle Filter wirken sofort -->
  <select v-model="daysInAdvance" class="ct-select">
    <!-- Kein @change nötig -->
  </select>
  
  <select v-model="calendarFilter" class="ct-select">
    <!-- Kein @change nötig -->
  </select>
  
  <select v-model="statusFilter" class="ct-select">
    <!-- Kein @change nötig -->
  </select>
  
  <!-- Refresh-Button nur für neue Daten vom Server -->
  <button @click="fetchAllAppointments" class="ct-btn ct-btn-primary">
    {{ isLoading ? 'Lädt...' : 'Daten aktualisieren' }}
  </button>
</template>

4. Smart Data Management:

// Intelligente Refresh-Strategie
const shouldRefreshData = computed(() => {
  if (!lastFetchTime.value) return true
  
  const now = new Date()
  const lastFetch = new Date(lastFetchTime.value)
  const hoursSinceLastFetch = (now.getTime() - lastFetch.getTime()) / (1000 * 60 * 60)
  
  // Refresh wenn Daten älter als 1 Stunde
  return hoursSinceLastFetch > 1
})

// Auto-refresh bei Mount wenn nötig
onMounted(() => {
  if (shouldRefreshData.value) {
    fetchAllAppointments()
  }
})

🧪 Test Cases

Zu testende Szenarien:

  1. Filter-Responsiveness:

    • Tage-Filter ändert sofort die Anzeige
    • Kalender-Filter ändert sofort die Anzeige
    • Status-Filter ändert sofort die Anzeige
    • Kombinierte Filter funktionieren korrekt
  2. Performance:

    • Filter-Änderungen < 100ms Response-Zeit
    • Keine API-Calls bei Filter-Änderungen
    • Smooth UI-Updates ohne Flackern
  3. Data Consistency:

    • Alle Filter-Kombinationen zeigen korrekte Ergebnisse
    • Reset-Funktionalität funktioniert
    • Refresh lädt aktuelle Server-Daten
  4. Edge Cases:

    • Leere Filter-Ergebnisse
    • Sehr große Datenmengen
    • Schnelle Filter-Änderungen

📊 Performance Impact

Vorher (Problematisch):

  • Tage-Filter: API-Call (2-5s)
  • Kalender-Filter: Client-seitig (instant)
  • Status-Filter: Client-seitig (instant)
  • Inkonsistente UX: Verwirrend für Benutzer

Nachher (Optimiert):

  • Alle Filter: Client-seitig (instant)
  • Initial Load: Einmalig (2-5s)
  • Filter-Changes: <100ms
  • Konsistente UX: Alle Filter verhalten sich gleich

Memory Considerations:

// Speicher-effiziente Implementierung
const MAX_APPOINTMENTS_IN_MEMORY = 10000
const CACHE_DURATION = 60 * 60 * 1000 // 1 Stunde

const manageMemoryUsage = () => {
  if (allAppointmentsData.value.length > MAX_APPOINTMENTS_IN_MEMORY) {
    // Älteste Termine entfernen oder Paging implementieren
    console.warn('Large dataset detected, consider implementing pagination')
  }
}

🔄 Alternative Solutions

Alternative 1: Hybrid Approach

// Kritische Filter server-seitig, andere client-seitig
const criticalFilters = ['daysInAdvance'] // Große Datenmengen-Reduktion
const clientFilters = ['calendarFilter', 'statusFilter'] // Kleine Filterung

watch(criticalFilters, () => refreshData())
// clientFilters werden nur computed

Alternative 2: Debounced Server Calls

// Verzögerte API-Calls um Spam zu vermeiden
const debouncedRefresh = debounce(() => {
  refreshData()
}, 500)

watch([daysInAdvance, calendarFilter, statusFilter], () => {
  debouncedRefresh()
})

Alternative 3: Progressive Loading

// Lade Daten schrittweise basierend auf Filtern
const loadDataProgressively = async () => {
  // 1. Lade Basis-Daten
  const baseData = await findExpiringSeries(30 * 24 * 60 * 60 * 1000)
  
  // 2. Erweitere bei Bedarf
  if (daysInAdvance.value > 30) {
    const extendedData = await findExpiringSeries(daysInAdvance.value * 24 * 60 * 60 * 1000)
    allAppointmentsData.value = extendedData
  }
}

🎯 Acceptance Criteria

✅ Definition of Done:

  1. Consistent Filter Behavior:

    • Alle Filter wirken sofort ohne manuellen Refresh
    • Keine Unterschiede zwischen Filter-Typen
    • Kombinierte Filter funktionieren korrekt
  2. Performance:

    • Filter-Änderungen < 100ms Response-Zeit
    • Keine unnötigen API-Calls bei Filter-Änderungen
    • Initial Load-Zeit bleibt akzeptabel
  3. User Experience:

    • Intuitive Bedienung ohne Verwirrung
    • Klare Indikation wenn Daten geladen werden
    • Smooth Transitions zwischen Filter-States
  4. Technical Quality:

    • Clean, maintainable Code
    • Proper error handling
    • Memory-efficient implementation

📁 Files to Modify

Primary:

  • src/components/expiring-appointments/ExpiringAppointmentsAdmin.vue - Haupt-Filter-Logik

Secondary:

  • src/services/churchtools.ts - Möglicherweise API-Optimierungen
  • src/composables/useExpiringAppointments.ts - Falls TanStack Query implementiert

🏷️ Labels

  • bug - Inkonsistentes Verhalten
  • user-experience - UX-Problem
  • expiring-appointments - Betrifft Expiring Appointments Modul
  • filter-behavior - Filter-Funktionalität
  • priority-medium - Beeinträchtigt Benutzerfreundlichkeit

🔗 Related Issues

  • Performance & Caching Issue (TanStack Query)
  • Filter System Improvements
  • User Experience Optimization

Reporter: Development Team
Priority: Medium
Complexity: Low-Medium
Estimated Effort: 2-4 hours

📝 User Story

Als Administrator
möchte ich dass alle Filter sofort wirken
damit ich nicht manuell aktualisieren muss und eine konsistente Benutzererfahrung habe.

Acceptance Criteria:

  • Wenn ich einen Filter ändere, sehe ich sofort die Ergebnisse
  • Alle Filter verhalten sich gleich (keine Inkonsistenzen)
  • Ich muss nie manuell "Aktualisieren" klicken für Filter-Änderungen

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions