Просмотр исходного кода

Umstellung auf ajax um Seiten neuladen zu vermeiden

Leonard Schulz 1 год назад
Родитель
Сommit
b485d7cadd

+ 195 - 0
src/main/java/com/tsystems/inwo/vgl/web/VergleichController.java

@@ -6,6 +6,7 @@ import com.tsystems.inwo.vgl.view.*;
 import org.springframework.stereotype.Controller;
 import org.springframework.ui.Model;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.http.ResponseEntity;
 import java.util.*;
 import java.util.stream.Collectors;
 
@@ -155,12 +156,206 @@ public class VergleichController {
         return "vergleich";
     }
 
+    @PostMapping("/api/person-details")
+    @ResponseBody
+    public ResponseEntity<PersonDetailsResponse> getPersonDetails(
+            @RequestParam String leftJson,
+            @RequestParam String rightJson,
+            @RequestParam String leftVorgangId,
+            @RequestParam String leftZeitraumId,
+            @RequestParam String rightZeitraumId,
+            @RequestParam String personId) throws Exception {
+
+        JsonNode left = om.readTree(leftJson);
+        JsonNode right = om.readTree(rightJson);
+
+        // --- Rechts (Master) Zeitraum wählen ---
+        JsonNode rightZeitraum = jsonNavigationService.findZeitraumById(right, rightZeitraumId);
+        
+        // --- Links (Baseline) Zeitraum finden ---
+        JsonNode leftZeitraum = null;
+        if (leftZeitraumId != null) {
+            leftZeitraum = jsonNavigationService.findZeitraumById(jsonNavigationService.getRootOfVorgang(left, leftVorgangId), leftZeitraumId);
+        }
+
+        // --- Personenliste rechts ---
+        List<PersonRef> personenRight = jsonNavigationService.listRightPersons(rightZeitraum);
+
+        // --- Personen-Tabelle (UNION via person.id) ---
+        Map<String, Pair<JsonNode, JsonNode>> personenPairs = comparisonService.unionByKey(
+                jsonNavigationService.personsArray(rightZeitraum), 
+                jsonNavigationService.personsArray(leftZeitraum), 
+                n -> n.path("person").path("id").asText()
+        );
+
+        // --- Details für ausgewählte Person ---
+        JsonNode rightPerson = personenPairs.getOrDefault(personId, new Pair<>(null, null)).right;
+        JsonNode leftPerson = personenPairs.getOrDefault(personId, new Pair<>(null, null)).left;
+
+        String personLabel = (rightPerson != null)
+                ? rightPerson.path("person").path("nachname").asText("") + ", " + rightPerson.path("person").path("vorname").asText("")
+                : "(keine Person)";
+        Map<String, String> personNameByIdRight = personenRight.stream().collect(Collectors.toMap(p -> p.id, p -> p.label));
+
+        // Einkommen / Freibeträge / Kita: jeweils UNION per id
+        List<EinkommenZeile> einkommenZeilen = dataExtractor.buildEinkommenZeilen(rightPerson, leftPerson);
+        List<FreibetragZeile> freibetragZeilen = dataExtractor.buildFreibetragZeilen(rightPerson, leftPerson);
+        List<KitaZeile> kitaZeilen = dataExtractor.buildKitaZeilen(rightPerson, leftPerson, personNameByIdRight);
+
+        PersonDetailsResponse response = new PersonDetailsResponse(
+                personId,
+                personLabel,
+                einkommenZeilen,
+                freibetragZeilen,
+                kitaZeilen
+        );
+
+        return ResponseEntity.ok(response);
+    }
+
+    @PostMapping("/api/vorgang-change")
+    @ResponseBody
+    public ResponseEntity<VorgangChangeResponse> changeLeftVorgang(
+            @RequestParam String leftJson,
+            @RequestParam String rightJson,
+            @RequestParam String rightZeitraumId,
+            @RequestParam String personId,
+            @RequestParam String newLeftVorgangId) throws Exception {
+
+        JsonNode left = om.readTree(leftJson);
+        JsonNode right = om.readTree(rightJson);
+
+        // --- Links (Baseline) Kandidaten & neue Auswahl ---
+        List<Vorgang> leftCand = jsonNavigationService.parseLeftCandidates(left);
+        
+        final String finalLeftVorgangId = newLeftVorgangId;
+        List<Z> leftZ = leftCand.stream()
+                .filter(v -> v.id.equals(finalLeftVorgangId))
+                .findFirst()
+                .orElse(leftCand.get(0))
+                .zeitraeume;
+        
+        // Ersten Zeitraum des neuen Vorgangs als Default wählen
+        String newLeftZeitraumId = !leftZ.isEmpty() ? leftZ.get(0).id : null;
+        
+        // --- Rechts (Master) Zeitraum ---
+        JsonNode rightZeitraum = jsonNavigationService.findZeitraumById(right, rightZeitraumId);
+        
+        // --- Links (Baseline) Zeitraum ---
+        JsonNode leftZeitraum = null;
+        if (newLeftZeitraumId != null) {
+            leftZeitraum = jsonNavigationService.findZeitraumById(jsonNavigationService.getRootOfVorgang(left, newLeftVorgangId), newLeftZeitraumId);
+        }
+
+        // --- Personen-Tabelle neu berechnen ---
+        Map<String, Pair<JsonNode, JsonNode>> personenPairs = comparisonService.unionByKey(
+                jsonNavigationService.personsArray(rightZeitraum), 
+                jsonNavigationService.personsArray(leftZeitraum), 
+                n -> n.path("person").path("id").asText()
+        );
+        List<MitgliedRow> mitgliederRows = personenPairs.entrySet().stream()
+                .map(e -> dataExtractor.buildMitgliedRow(e.getKey(), e.getValue().right, e.getValue().left))
+                .sorted(Comparator.comparing(MitgliedRow::personId))
+                .toList();
+
+        // --- Details für ausgewählte Person ---
+        List<PersonRef> personenRight = jsonNavigationService.listRightPersons(rightZeitraum);
+        JsonNode rightPerson = personenPairs.getOrDefault(personId, new Pair<>(null, null)).right;
+        JsonNode leftPerson = personenPairs.getOrDefault(personId, new Pair<>(null, null)).left;
+
+        String personLabel = (rightPerson != null)
+                ? rightPerson.path("person").path("nachname").asText("") + ", " + rightPerson.path("person").path("vorname").asText("")
+                : "(keine Person)";
+        Map<String, String> personNameByIdRight = personenRight.stream().collect(Collectors.toMap(p -> p.id, p -> p.label));
+
+        List<EinkommenZeile> einkommenZeilen = dataExtractor.buildEinkommenZeilen(rightPerson, leftPerson);
+        List<FreibetragZeile> freibetragZeilen = dataExtractor.buildFreibetragZeilen(rightPerson, leftPerson);
+        List<KitaZeile> kitaZeilen = dataExtractor.buildKitaZeilen(rightPerson, leftPerson, personNameByIdRight);
+
+        VorgangChangeResponse response = new VorgangChangeResponse(
+                newLeftVorgangId,
+                newLeftZeitraumId,
+                leftZ,
+                mitgliederRows,
+                personId,
+                personLabel,
+                einkommenZeilen,
+                freibetragZeilen,
+                kitaZeilen
+        );
+
+        return ResponseEntity.ok(response);
+    }
+
+    @PostMapping("/api/zeitraum-change")
+    @ResponseBody
+    public ResponseEntity<ZeitraumChangeResponse> changeLeftZeitraum(
+            @RequestParam String leftJson,
+            @RequestParam String rightJson,
+            @RequestParam String leftVorgangId,
+            @RequestParam String rightZeitraumId,
+            @RequestParam String personId,
+            @RequestParam String newLeftZeitraumId) throws Exception {
+
+        JsonNode left = om.readTree(leftJson);
+        JsonNode right = om.readTree(rightJson);
+
+        // --- Rechts (Master) Zeitraum ---
+        JsonNode rightZeitraum = jsonNavigationService.findZeitraumById(right, rightZeitraumId);
+        
+        // --- Links (Baseline) Zeitraum ---
+        JsonNode leftZeitraum = null;
+        if (newLeftZeitraumId != null) {
+            leftZeitraum = jsonNavigationService.findZeitraumById(jsonNavigationService.getRootOfVorgang(left, leftVorgangId), newLeftZeitraumId);
+        }
+
+        // --- Personen-Tabelle neu berechnen ---
+        Map<String, Pair<JsonNode, JsonNode>> personenPairs = comparisonService.unionByKey(
+                jsonNavigationService.personsArray(rightZeitraum), 
+                jsonNavigationService.personsArray(leftZeitraum), 
+                n -> n.path("person").path("id").asText()
+        );
+        List<MitgliedRow> mitgliederRows = personenPairs.entrySet().stream()
+                .map(e -> dataExtractor.buildMitgliedRow(e.getKey(), e.getValue().right, e.getValue().left))
+                .sorted(Comparator.comparing(MitgliedRow::personId))
+                .toList();
+
+        // --- Details für ausgewählte Person ---
+        List<PersonRef> personenRight = jsonNavigationService.listRightPersons(rightZeitraum);
+        JsonNode rightPerson = personenPairs.getOrDefault(personId, new Pair<>(null, null)).right;
+        JsonNode leftPerson = personenPairs.getOrDefault(personId, new Pair<>(null, null)).left;
+
+        String personLabel = (rightPerson != null)
+                ? rightPerson.path("person").path("nachname").asText("") + ", " + rightPerson.path("person").path("vorname").asText("")
+                : "(keine Person)";
+        Map<String, String> personNameByIdRight = personenRight.stream().collect(Collectors.toMap(p -> p.id, p -> p.label));
+
+        List<EinkommenZeile> einkommenZeilen = dataExtractor.buildEinkommenZeilen(rightPerson, leftPerson);
+        List<FreibetragZeile> freibetragZeilen = dataExtractor.buildFreibetragZeilen(rightPerson, leftPerson);
+        List<KitaZeile> kitaZeilen = dataExtractor.buildKitaZeilen(rightPerson, leftPerson, personNameByIdRight);
+
+        ZeitraumChangeResponse response = new ZeitraumChangeResponse(
+                newLeftZeitraumId,
+                mitgliederRows,
+                personId,
+                personLabel,
+                einkommenZeilen,
+                freibetragZeilen,
+                kitaZeilen
+        );
+
+        return ResponseEntity.ok(response);
+    }
+
     // ===== Helper-Typen =====
     public record Z(String id, String beginn, String ende) {}
     public record Vorgang(String id, String vorgangstyp, List<Z> zeitraeume) {}
     public record PersonRef(String id, String label) {}
     public record StammvorgangInfo(String id, String vorgangstyp, String vorgangsstatus) {}
     public record StammakteInfo(String aktenzeichen, String antragstellername, String antragstellerGeburtsdatum, String anschrift) {}
+    public record PersonDetailsResponse(String personId, String personLabel, List<EinkommenZeile> einkommenZeilen, List<FreibetragZeile> freibetragZeilen, List<KitaZeile> kitaZeilen) {}
+    public record VorgangChangeResponse(String leftVorgangId, String leftZeitraumId, List<Z> leftZeitraeume, List<MitgliedRow> mitgliederRows, String personId, String personLabel, List<EinkommenZeile> einkommenZeilen, List<FreibetragZeile> freibetragZeilen, List<KitaZeile> kitaZeilen) {}
+    public record ZeitraumChangeResponse(String leftZeitraumId, List<MitgliedRow> mitgliederRows, String personId, String personLabel, List<EinkommenZeile> einkommenZeilen, List<FreibetragZeile> freibetragZeilen, List<KitaZeile> kitaZeilen) {}
     public static final class Pair<R, L> {
         public final R right;
         public final L left;

+ 1 - 58
src/main/resources/static/styles.css

@@ -104,7 +104,7 @@ select:hover {
 button {
   padding: 0.5rem 0.9rem;
   border: 2px solid var(--grau-stark);
-  background: #fff;
+  background: var(--weiss);
   cursor: pointer;
   font-size: 14px;
   min-height: 44px; 
@@ -270,39 +270,6 @@ button:active {
   min-height: 32px;
 }
 
-/* Legacy Styles für Kompatibilität */
-.zeitraum-controls {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-}
-
-.zeitraum-form {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-}
-
-.zeitraum-form label {
-  font-size: 13px;
-  font-weight: bold;
-  white-space: nowrap;
-}
-
-.zeitraum-select {
-  width: 200px;
-  padding: 4px 8px;
-  font-size: 13px;
-  min-height: 32px; 
-}
-
-.wechseln-btn {
-  padding: 4px 8px;
-  font-size: 12px;
-  min-width: 32px;
-  min-height: 32px;
-}
-
 /* Vergleichslegende */
 .vergleich-legende {
   display: flex;
@@ -354,7 +321,6 @@ button:active {
   padding: 2px 4px;
   border-radius: 2px;
   display: inline-block;
-  background: var(--diwo-blau-hell);
 }
 
 table {
@@ -776,26 +742,3 @@ tr:focus {
   outline: 3px solid var(--focus-color);
   outline-offset: 2px;
 }
-
-/* Animation für das Auf- und Zuklappen */
-@media (prefers-reduced-motion: no-preference) {
-  .person-details-row {
-    animation-duration: 0.3s;
-    animation-fill-mode: both;
-  }
-  
-  .person-details-row:not(.hidden) {
-    animation-name: slideDown;
-  }
-}
-
-@keyframes slideDown {
-  from {
-    opacity: 0;
-    transform: translateY(-10px);
-  }
-  to {
-    opacity: 1;
-    transform: translateY(0);
-  }
-}

+ 470 - 49
src/main/resources/templates/vergleich.html

@@ -55,46 +55,29 @@
       
       <div class="vergleich-controls">
         <div class="vergleich-controls-wrapper">
-          <!-- Vorgangsauswahl (links/alt) -->
-          <form method="post" action="/vergleich" class="vorgang-form" role="form" aria-label="Vorgangsauswahl">
-            <input type="hidden" name="schemaName" th:value="${schemaName}"/>
-            <input type="hidden" name="validate" th:value="${validate}"/>
-            <input type="hidden" name="leftJson" th:value="${leftJson}"/>
-            <input type="hidden" name="rightJson" th:value="${rightJson}"/>
-            <input type="hidden" name="rightZeitraumId" th:value="${rightZeitraumId}"/>
-            <input type="hidden" name="personId" th:value="${personId}"/>
-            <input type="hidden" name="kunde" th:value="${activeKunde}"/>
-            
+          <!-- Vorgangsauswahl (links/alt) - jetzt mit AJAX -->
+          <div class="vorgang-form" role="form" aria-label="Vorgangsauswahl">
             <div class="control-group">
-              <label for="leftVorgangId">Vergleichsvorgang:</label>
-              <select name="leftVorgangId" id="leftVorgangId" class="vorgang-select" onchange="this.form.submit()">
+              <label for="leftVorgangSelect">Vergleichsvorgang:</label>
+              <select name="leftVorgangId" id="leftVorgangSelect" class="vorgang-select" onchange="changeLeftVorgang(this.value)">
                 <option th:each="v : ${leftCandidates}" th:value="${v.id}"
                         th:selected="${v.id == leftVorgangId}"
                         th:text="${v.vorgangstyp + (!#strings.isEmpty(v.vorgangstyp) ? ' (' + v.id + ')' : v.id)}"></option>
               </select>
             </div>
-          </form>
+          </div>
           
-          <!-- Zeitraumauswahl (links/alt) -->
-          <form method="post" action="/vergleich" class="zeitraum-form" role="form" aria-label="Zeitraumauswahl">
-            <input type="hidden" name="schemaName" th:value="${schemaName}"/>
-            <input type="hidden" name="validate" th:value="${validate}"/>
-            <input type="hidden" name="leftJson" th:value="${leftJson}"/>
-            <input type="hidden" name="rightJson" th:value="${rightJson}"/>
-            <input type="hidden" name="leftVorgangId" th:value="${leftVorgangId}"/>
-            <input type="hidden" name="rightZeitraumId" th:value="${rightZeitraumId}"/>
-            <input type="hidden" name="personId" th:value="${personId}"/>
-            <input type="hidden" name="kunde" th:value="${activeKunde}"/>
-            
+          <!-- Zeitraumauswahl (links/alt) - jetzt mit AJAX -->
+          <div class="zeitraum-form" role="form" aria-label="Zeitraumauswahl">
             <div class="control-group">
-              <label for="leftZeitraumId">Zeitraum:</label>
-              <select name="leftZeitraumId" id="leftZeitraumId" class="zeitraum-select" onchange="this.form.submit()">
+              <label for="leftZeitraumSelect">Zeitraum:</label>
+              <select name="leftZeitraumId" id="leftZeitraumSelect" class="zeitraum-select" onchange="changeLeftZeitraum(this.value)">
                 <option th:each="z : ${leftZeitraeumeOfSelected}" th:value="${z.id}"
                         th:selected="${z.id == leftZeitraumId}"
                         th:text="${z.beginn} + ' - ' + ${z.ende}"></option>
               </select>
             </div>
-          </form>
+          </div>
         </div>
       </div>
     </div>
@@ -146,7 +129,7 @@
         <h3>Haushaltsmitglieder</h3>
       </div>
       <div class="section-content" id="haushaltsmitglieder-content">
-        <table role="table" aria-labelledby="haushaltsmitglieder-caption" class="expandable-table">
+        <table role="table" aria-labelledby="haushaltsmitglieder-caption" class="expandable-table" id="haushaltsmitglieder-table">
           <thead>
             <tr>
               <th scope="col" class="expand-col"></th>
@@ -243,20 +226,17 @@
       </div>
     </div>
 
-    <!-- Hidden form for person selection -->
-    <form id="personForm" method="post" action="/vergleich" style="display: none;" aria-hidden="true">
-      <input type="hidden" name="leftJson" th:value="${leftJson}"/>
-      <input type="hidden" name="rightJson" th:value="${rightJson}"/>
-      <input type="hidden" name="validate" th:value="${validate}"/>
-      <input type="hidden" name="schemaName" th:value="${schemaName}"/>
-      <input type="hidden" name="leftVorgangId" th:value="${leftVorgangId}"/>
-      <input type="hidden" name="leftZeitraumId" th:value="${leftZeitraumId}"/>
-      <input type="hidden" name="rightZeitraumId" th:value="${rightZeitraumId}"/>
-      <input type="hidden" name="kunde" th:value="${activeKunde}"/>
-      <input type="hidden" name="personId" id="selectedPersonId"/>
-    </form>
-
-    <h2>Details zu <span th:if="${personLabel != null and !#strings.isEmpty(personLabel) and personLabel != '(keine Person)'}"><span th:text="${personLabel}"> - </span></span></h2>
+    <!-- Hidden data for AJAX requests -->
+    <div id="ajax-data" style="display: none;" aria-hidden="true">
+      <input type="hidden" id="leftJson" th:value="${leftJson}"/>
+      <input type="hidden" id="rightJson" th:value="${rightJson}"/>
+      <input type="hidden" id="leftVorgangId" th:value="${leftVorgangId}"/>
+      <input type="hidden" id="leftZeitraumId" th:value="${leftZeitraumId}"/>
+      <input type="hidden" id="rightZeitraumId" th:value="${rightZeitraumId}"/>
+      <input type="hidden" id="currentPersonId" th:value="${personId}"/>
+    </div>
+
+    <h2 id="person-details-title">Details zu <span th:if="${personLabel != null and !#strings.isEmpty(personLabel) and personLabel != '(keine Person)'}"><span th:text="${personLabel}"> - </span></span></h2>
     
     <!-- Einkommen -->
     <div class="section-container">
@@ -266,7 +246,7 @@
         <h3>Einkommen</h3>
       </div>
       <div class="section-content" id="einkommen-content">
-        <table role="table" aria-labelledby="einkommen-caption">
+        <table role="table" aria-labelledby="einkommen-caption" id="einkommen-table">
           <thead>
             <tr>
               <th scope="col">Änderung</th>
@@ -305,7 +285,7 @@
             <h3>Pauschale Abzüge</h3>
           </div>
           <div class="section-content" id="pauschale-content">
-            <table role="table" aria-labelledby="pauschale-caption">
+            <table role="table" aria-labelledby="pauschale-caption" id="pauschale-table">
               <thead>
                 <tr>
                   <th scope="col">Änderung</th>
@@ -336,7 +316,7 @@
             <h3>Freibeträge</h3>
           </div>
           <div class="section-content" id="freibetraege-content">
-            <table role="table" aria-labelledby="freibetraege-caption">
+            <table role="table" aria-labelledby="freibetraege-caption" id="freibetraege-table">
               <thead>
                 <tr>
                   <th scope="col">Änderung</th>
@@ -371,7 +351,7 @@
           <h3>Kinderbetreuungskosten</h3>
         </div>
         <div class="section-content" id="kinderbetreuung-content">
-          <table role="table" aria-labelledby="kinderbetreuung-caption">
+          <table role="table" aria-labelledby="kinderbetreuung-caption" id="kinderbetreuung-table">
             <thead>
               <tr>
                 <th scope="col">Änderung</th>
@@ -461,6 +441,441 @@
       }
     }
 
+    // AJAX-Funktion für Vorgangsauswahl - MIT DEBUG LOGGING
+    function changeLeftVorgang(newLeftVorgangId) {
+      console.log('🚀 changeLeftVorgang aufgerufen mit:', newLeftVorgangId);
+      
+      const currentLeftVorgangId = document.getElementById('leftVorgangId').value;
+      console.log('📊 Aktueller leftVorgangId:', currentLeftVorgangId);
+      
+      // Prüfen ob bereits der ausgewählte Vorgang
+      if (currentLeftVorgangId === newLeftVorgangId) {
+        console.log('⚠️ Gleicher Vorgang bereits ausgewählt, überspringe');
+        return; // Nichts zu tun
+      }
+
+      // Loading-Zustand anzeigen
+      showLoadingState();
+      console.log('⏳ Loading-Zustand angezeigt');
+      
+      // Parameter sammeln
+      const leftJson = document.getElementById('leftJson').value;
+      const rightJson = document.getElementById('rightJson').value;
+      const rightZeitraumId = document.getElementById('rightZeitraumId').value;
+      const personId = document.getElementById('currentPersonId').value;
+      
+      console.log('📋 Request Parameter:');
+      console.log('  leftJson length:', leftJson ? leftJson.length : 'null');
+      console.log('  rightJson length:', rightJson ? rightJson.length : 'null');
+      console.log('  rightZeitraumId:', rightZeitraumId);
+      console.log('  personId:', personId);
+      console.log('  newLeftVorgangId:', newLeftVorgangId);
+      
+      // AJAX-Request senden
+      const formData = new FormData();
+      formData.append('leftJson', leftJson);
+      formData.append('rightJson', rightJson);
+      formData.append('rightZeitraumId', rightZeitraumId);
+      formData.append('personId', personId);
+      formData.append('newLeftVorgangId', newLeftVorgangId);
+
+      console.log('🌐 Sende AJAX-Request an /api/vorgang-change');
+      
+      fetch('/api/vorgang-change', {
+        method: 'POST',
+        body: formData
+      })
+      .then(response => {
+        console.log('📡 Response erhalten:', response.status, response.statusText);
+        if (!response.ok) {
+          throw new Error('Netzwerk-Antwort war nicht ok: ' + response.status);
+        }
+        return response.json();
+      })
+      .then(data => {
+        console.log('✅ Erfolgreiche Antwort:', data);
+        // Erfolgreiche Antwort verarbeiten
+        updateAfterVorgangChange(data);
+        hideLoadingState();
+        console.log('🔄 UI erfolgreich aktualisiert');
+      })
+      .catch(error => {
+        console.error('❌ Fehler beim Ändern des Vorgangs:', error);
+        hideLoadingState();
+        alert('Fehler beim Ändern des Vorgangs: ' + error.message + '. Bitte versuchen Sie es erneut.');
+      });
+    }
+
+    // AJAX-Funktion für Zeitraumauswahl - MIT DEBUG LOGGING  
+    function changeLeftZeitraum(newLeftZeitraumId) {
+      console.log('🚀 changeLeftZeitraum aufgerufen mit:', newLeftZeitraumId);
+      
+      const currentLeftZeitraumId = document.getElementById('leftZeitraumId').value;
+      console.log('📊 Aktueller leftZeitraumId:', currentLeftZeitraumId);
+      
+      // Prüfen ob bereits der ausgewählte Zeitraum
+      if (currentLeftZeitraumId === newLeftZeitraumId) {
+        console.log('⚠️ Gleicher Zeitraum bereits ausgewählt, überspringe');
+        return; // Nichts zu tun
+      }
+
+      // Loading-Zustand anzeigen
+      showLoadingState();
+      console.log('⏳ Loading-Zustand angezeigt');
+      
+      // AJAX-Request senden
+      const formData = new FormData();
+      formData.append('leftJson', document.getElementById('leftJson').value);
+      formData.append('rightJson', document.getElementById('rightJson').value);
+      formData.append('leftVorgangId', document.getElementById('leftVorgangId').value);
+      formData.append('rightZeitraumId', document.getElementById('rightZeitraumId').value);
+      formData.append('personId', document.getElementById('currentPersonId').value);
+      formData.append('newLeftZeitraumId', newLeftZeitraumId);
+
+      console.log('🌐 Sende AJAX-Request an /api/zeitraum-change');
+
+      fetch('/api/zeitraum-change', {
+        method: 'POST',
+        body: formData
+      })
+      .then(response => {
+        console.log('📡 Response erhalten:', response.status, response.statusText);
+        if (!response.ok) {
+          throw new Error('Netzwerk-Antwort war nicht ok: ' + response.status);
+        }
+        return response.json();
+      })
+      .then(data => {
+        console.log('✅ Erfolgreiche Antwort:', data);
+        // Erfolgreiche Antwort verarbeiten
+        updateAfterZeitraumChange(data);
+        hideLoadingState();
+        console.log('🔄 UI erfolgreich aktualisiert');
+      })
+      .catch(error => {
+        console.error('❌ Fehler beim Ändern des Zeitraums:', error);
+        hideLoadingState();
+        alert('Fehler beim Ändern des Zeitraums: ' + error.message + '. Bitte versuchen Sie es erneut.');
+      });
+    }
+
+    // AJAX-Funktion für Personenauswahl
+    function selectPersonAjax(personId) {
+      const currentPersonId = document.getElementById('currentPersonId');
+      
+      // Prüfen ob bereits die ausgewählte Person
+      if (currentPersonId.value === personId) {
+        return; // Nichts zu tun
+      }
+
+      // Loading-Zustand anzeigen
+      const detailsTitle = document.getElementById('person-details-title');
+      const originalTitle = detailsTitle.innerHTML;
+      detailsTitle.innerHTML = 'Details werden geladen...';
+      
+      // AJAX-Request senden
+      const formData = new FormData();
+      formData.append('leftJson', document.getElementById('leftJson').value);
+      formData.append('rightJson', document.getElementById('rightJson').value);
+      formData.append('leftVorgangId', document.getElementById('leftVorgangId').value);
+      formData.append('leftZeitraumId', document.getElementById('leftZeitraumId').value);
+      formData.append('rightZeitraumId', document.getElementById('rightZeitraumId').value);
+      formData.append('personId', personId);
+
+      fetch('/api/person-details', {
+        method: 'POST',
+        body: formData
+      })
+      .then(response => {
+        if (!response.ok) {
+          throw new Error('Netzwerk-Antwort war nicht ok');
+        }
+        return response.json();
+      })
+      .then(data => {
+        // Erfolgreiche Antwort verarbeiten
+        updatePersonDetails(data);
+        
+        // Aktuelle Person aktualisieren
+        currentPersonId.value = personId;
+        
+        // Person-Selected-Klasse aktualisieren
+        updateSelectedPersonRow(personId);
+        
+        // Fokus beibehalten falls möglich
+        const activeElement = document.activeElement;
+        if (activeElement && activeElement.closest('.person-row')) {
+          activeElement.focus();
+        }
+      })
+      .catch(error => {
+        console.error('Fehler beim Laden der Personendetails:', error);
+        detailsTitle.innerHTML = originalTitle;
+        alert('Fehler beim Laden der Personendetails. Bitte versuchen Sie es erneut.');
+      });
+    }
+
+    function updateAfterVorgangChange(data) {
+      console.log('🔄 updateAfterVorgangChange aufgerufen mit:', data);
+      
+      // Hidden fields aktualisieren
+      document.getElementById('leftVorgangId').value = data.leftVorgangId;
+      document.getElementById('leftZeitraumId').value = data.leftZeitraumId;
+      document.getElementById('currentPersonId').value = data.personId;
+      
+      // Vorgang-Dropdown aktualisieren (falls erforderlich)
+      const vorgangSelect = document.getElementById('leftVorgangSelect');
+      if (vorgangSelect.value !== data.leftVorgangId) {
+        vorgangSelect.value = data.leftVorgangId;
+      }
+      
+      // Zeitraum-Dropdown aktualisieren
+      const zeitraumSelect = document.getElementById('leftZeitraumSelect');
+      zeitraumSelect.innerHTML = '';
+      data.leftZeitraeume.forEach(zeitraum => {
+        const option = document.createElement('option');
+        option.value = zeitraum.id;
+        option.textContent = zeitraum.beginn + ' - ' + zeitraum.ende;
+        if (zeitraum.id === data.leftZeitraumId) {
+          option.selected = true;
+        }
+        zeitraumSelect.appendChild(option);
+      });
+      
+      console.log('📋 Zeitraum-Dropdown aktualisiert mit', data.leftZeitraeume.length, 'Einträgen');
+      
+      // Haushaltsmitglieder-Tabelle aktualisieren
+      updateHaushaltsmitgliederTable(data.mitgliederRows, data.personId);
+      
+      // Personendetails aktualisieren
+      updatePersonDetails(data);
+      
+      console.log('✅ updateAfterVorgangChange abgeschlossen');
+    }
+
+    function updateAfterZeitraumChange(data) {
+      console.log('🔄 updateAfterZeitraumChange aufgerufen mit:', data);
+      
+      // Hidden field aktualisieren
+      document.getElementById('leftZeitraumId').value = data.leftZeitraumId;
+      document.getElementById('currentPersonId').value = data.personId;
+      
+      // Zeitraum-Dropdown aktualisieren (falls erforderlich)
+      const zeitraumSelect = document.getElementById('leftZeitraumSelect');
+      if (zeitraumSelect.value !== data.leftZeitraumId) {
+        zeitraumSelect.value = data.leftZeitraumId;
+      }
+      
+      // Haushaltsmitglieder-Tabelle aktualisieren
+      updateHaushaltsmitgliederTable(data.mitgliederRows, data.personId);
+      
+      // Personendetails aktualisieren
+      updatePersonDetails(data);
+      
+      console.log('✅ updateAfterZeitraumChange abgeschlossen');
+    }
+
+    function updateHaushaltsmitgliederTable(mitgliederRows, selectedPersonId) {
+      const table = document.getElementById('haushaltsmitglieder-table');
+      const tbody = table.querySelector('tbody');
+      
+      // Tbody leeren
+      tbody.innerHTML = '';
+      
+      // Neue Zeilen hinzufügen
+      mitgliederRows.forEach(row => {
+        // Hauptzeile
+        const tr = document.createElement('tr');
+        tr.className = 'person-row ' + (row.rowClass || '') + (row.personId === selectedPersonId ? ' person-selected' : '');
+        tr.setAttribute('data-person-id', row.personId);
+        tr.setAttribute('role', 'button');
+        tr.setAttribute('tabindex', '0');
+        tr.setAttribute('aria-label', 'Person auswählen: ' + (row.nameHtml || '').replace(/<[^>]*>/g, ''));
+        tr.setAttribute('onkeypress', 'handleRowKeyPress(event, this)');
+        
+        // Spalten hinzufügen
+        const expandTd = document.createElement('td');
+        expandTd.className = 'expand-col';
+        expandTd.innerHTML = `
+          <button type="button" 
+                  class="expand-btn" 
+                  data-person-id="${row.personId}"
+                  onclick="togglePersonDetailsFromButton(this)"
+                  onkeypress="handleExpandKeyPressFromButton(event, this)"
+                  aria-expanded="false"
+                  aria-controls="person-details-${row.personId}"
+                  aria-label="Details für ${(row.nameHtml || '').replace(/<[^>]*>/g, '')} anzeigen">
+            <span aria-hidden="true">▶</span>
+          </button>
+        `;
+        tr.appendChild(expandTd);
+        
+        const symbolTd = document.createElement('td');
+        symbolTd.className = 'sym ' + (row.symbolClass || '');
+        symbolTd.textContent = row.symbol || '';
+        const ariaLabel = row.symbol === '+' ? 'Hinzugefügt' : 
+                         (row.symbol === '-' ? 'Gelöscht' : 
+                         (row.symbol === '!' ? 'Geändert' : 'Unverändert'));
+        symbolTd.setAttribute('aria-label', ariaLabel);
+        tr.appendChild(symbolTd);
+        
+        // Weitere Spalten
+        const columns = ['nameHtml', 'blocknameHtml', 'geburtsdatumHtml', 'sterbedatumHtml', 
+                        'familienstandHtml', 'geschlechtHtml', 'anredeHtml', 'verwandtschaftHtml',
+                        'ausschlussgrundHtml', 'sozialstellungHtml', 'antragstellerHtml', 
+                        'zahlungsempfaengerHtml', 'aufenthaltstitelEndeHtml'];
+        
+        columns.forEach(column => {
+          const td = document.createElement('td');
+          td.innerHTML = row[column] || '';
+          tr.appendChild(td);
+        });
+        
+        tbody.appendChild(tr);
+        
+        // Detailzeile hinzufügen
+        const detailTr = document.createElement('tr');
+        detailTr.id = 'person-details-' + row.personId;
+        detailTr.className = 'person-details-row hidden';
+        detailTr.setAttribute('aria-labelledby', 'person-details-label-' + row.personId);
+        
+        const detailTd = document.createElement('td');
+        detailTd.setAttribute('colspan', '15');
+        detailTd.className = 'details-cell';
+        detailTd.innerHTML = `
+          <div class="details-container">
+            <table class="details-table">
+              <tbody>
+                <tr>
+                  <td class="detail-label">Titel:</td>
+                  <td>${row.titelHtml || ''}</td>
+                  <td class="detail-label">Geburtsort:</td>
+                  <td>${row.geburtsortHtml || ''}</td>
+                  <td class="detail-label">Telefon:</td>
+                  <td>${row.telefonHtml || ''}</td>
+                  <td class="detail-label">Aktenzeichen Soz.:</td>
+                  <td>${row.aktenzeichenSozialHtml || ''}</td>
+                </tr>
+                <tr>
+                  <td class="detail-label">Geburtsname:</td>
+                  <td>${row.geburtsnameHtml || ''}</td>
+                  <td class="detail-label">Alter Name:</td>
+                  <td>${row.alterNameHtml || ''}</td>
+                  <td class="detail-label">E-Mail:</td>
+                  <td>${row.emailHtml || ''}</td>
+                  <td class="detail-label">Postabrutionsnummer:</td>
+                  <td>${row.postabrutionsnummerHtml || ''}</td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+        `;
+        detailTr.appendChild(detailTd);
+        tbody.appendChild(detailTr);
+      });
+      
+      // Event Listeners für neue Zeilen hinzufügen
+      const newPersonRows = tbody.querySelectorAll('.person-row');
+      newPersonRows.forEach(function(row) {
+        row.addEventListener('click', function(event) {
+          // Prüfen ob das Event von einem Expand-Button kommt
+          if (event.target.classList.contains('expand-btn') || event.target.closest('.expand-btn')) {
+            return; // Button hat sein eigenes Click-Event
+          }
+          
+          const personId = this.dataset.personId;
+          if (personId) {
+            selectPersonAjax(personId);
+          }
+        });
+      });
+    }
+
+    function updatePersonDetails(data) {
+      // Titel aktualisieren
+      const detailsTitle = document.getElementById('person-details-title');
+      detailsTitle.innerHTML = 'Details zu ' + (data.personLabel && data.personLabel !== '(keine Person)' ? data.personLabel : '');
+      
+      // Einkommen-Tabelle aktualisieren
+      updateTable('einkommen-table', data.einkommenZeilen, [
+        'symbol', 'artHtml', 'artZusatzHtml', 'faktorHtml', 'betragHtml', 
+        'zuschlagHtml', 'abschlagHtml', 'werbungskostenHtml'
+      ]);
+      
+      // Pauschale Abzüge aktualisieren
+      const pauschaleZeilen = data.freibetragZeilen.filter(z => z.typ === 'P');
+      updateTable('pauschale-table', pauschaleZeilen, [
+        'symbol', 'artHtml', 'faktorHtml', 'betragHtml'
+      ]);
+      
+      // Freibeträge aktualisieren
+      const freibetraege = data.freibetragZeilen.filter(z => z.typ === 'F');
+      updateTable('freibetraege-table', freibetraege, [
+        'symbol', 'artHtml', 'faktorHtml', 'betragHtml', 'fristHtml'
+      ]);
+      
+      // Kinderbetreuung aktualisieren
+      updateTable('kinderbetreuung-table', data.kitaZeilen, [
+        'symbol', 'kindNameHtml', 'betragHtml'
+      ]);
+    }
+
+    function updateTable(tableId, data, columns) {
+      const table = document.getElementById(tableId);
+      const tbody = table.querySelector('tbody');
+      
+      // Tbody leeren
+      tbody.innerHTML = '';
+      
+      // Neue Zeilen hinzufügen
+      data.forEach(row => {
+        const tr = document.createElement('tr');
+        tr.className = row.rowClass || '';
+        
+        columns.forEach((column, index) => {
+          const td = document.createElement('td');
+          
+          if (index === 0) { // Symbol-Spalte
+            td.className = 'sym ' + (row.symbolClass || '');
+            td.textContent = row.symbol || '';
+            const ariaLabel = row.symbol === '+' ? 'Hinzugefügt' : 
+                             (row.symbol === '-' ? 'Gelöscht' : 
+                             (row.symbol === '!' ? 'Geändert' : 'Unverändert'));
+            td.setAttribute('aria-label', ariaLabel);
+          } else {
+            td.innerHTML = row[column] || '';
+          }
+          
+          tr.appendChild(td);
+        });
+        
+        tbody.appendChild(tr);
+      });
+    }
+
+    function updateSelectedPersonRow(personId) {
+      // Alle person-selected Klassen entfernen
+      const allPersonRows = document.querySelectorAll('.person-row');
+      allPersonRows.forEach(row => {
+        row.classList.remove('person-selected');
+      });
+      
+      // Neue person-selected Klasse setzen
+      const selectedRow = document.querySelector(`[data-person-id="${personId}"]`);
+      if (selectedRow) {
+        selectedRow.classList.add('person-selected');
+      }
+    }
+
+    function showLoadingState() {
+      const detailsTitle = document.getElementById('person-details-title');
+      detailsTitle.innerHTML = 'Daten werden geladen...';
+    }
+
+    function hideLoadingState() {
+      // Loading-Zustand wird durch updatePersonDetails überschrieben
+    }
+
     // Keyboard support for toggles
     function handleKeyPress(event, sectionId) {
       if (event.key === 'Enter' || event.key === ' ') {
@@ -601,7 +1016,7 @@
       // Initialisiere das Farbschema
       initializeColorScheme();
       
-      // Add click event listeners to all person rows
+      // Add click event listeners to all person rows - jetzt mit AJAX
       const personRows = document.querySelectorAll('.person-row');
       personRows.forEach(function(row) {
         row.addEventListener('click', function(event) {
@@ -612,11 +1027,17 @@
           
           const personId = this.dataset.personId;
           if (personId) {
-            document.getElementById('selectedPersonId').value = personId;
-            document.getElementById('personForm').submit();
+            selectPersonAjax(personId);
           }
         });
       });
+      
+      // Debug-Info ausgeben
+      console.log('🔧 DOM geladen - Debug Info:');
+      console.log('  leftVorgangSelect:', document.getElementById('leftVorgangSelect'));
+      console.log('  leftZeitraumSelect:', document.getElementById('leftZeitraumSelect'));
+      console.log('  leftVorgangId (hidden):', document.getElementById('leftVorgangId'));
+      console.log('  leftZeitraumId (hidden):', document.getElementById('leftZeitraumId'));
     });
   </script>
 </body>