|
|
@@ -0,0 +1,844 @@
|
|
|
+package com.tsystems.inwo.vgl.web;
|
|
|
+
|
|
|
+import com.fasterxml.jackson.databind.*;
|
|
|
+import com.fasterxml.jackson.databind.node.*;
|
|
|
+import com.tsystems.inwo.vgl.service.SchemaValidationService;
|
|
|
+import com.tsystems.inwo.vgl.util.FormatUtil;
|
|
|
+import com.tsystems.inwo.vgl.view.*;
|
|
|
+import org.springframework.stereotype.Controller;
|
|
|
+import org.springframework.ui.Model;
|
|
|
+import org.springframework.web.bind.annotation.*;
|
|
|
+import java.util.*;
|
|
|
+import java.util.function.Function;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+@Controller
|
|
|
+public class VergleichController {
|
|
|
+ private final ObjectMapper om;
|
|
|
+ private final SchemaValidationService validator;
|
|
|
+
|
|
|
+ public VergleichController(ObjectMapper om, SchemaValidationService v) {
|
|
|
+ this.om = om;
|
|
|
+ this.validator = v;
|
|
|
+ }
|
|
|
+
|
|
|
+ @GetMapping("/test")
|
|
|
+ public String test(Model model) {
|
|
|
+ model.addAttribute("validate", true);
|
|
|
+ model.addAttribute("schemaName", "haushalt-einkommen");
|
|
|
+ return "test";
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/vergleich")
|
|
|
+ public String vergleich(
|
|
|
+ @RequestParam String leftJson,
|
|
|
+ @RequestParam String rightJson,
|
|
|
+ @RequestParam(required = false, defaultValue = "true") boolean validate,
|
|
|
+ @RequestParam(required = false, defaultValue = "haushalt-einkommen") String schemaName,
|
|
|
+ @RequestParam(required = false) String leftVorgangId,
|
|
|
+ @RequestParam(required = false) String leftZeitraumId,
|
|
|
+ @RequestParam(required = false) String rightZeitraumId,
|
|
|
+ @RequestParam(required = false) String personId,
|
|
|
+ Model model) throws Exception {
|
|
|
+
|
|
|
+ JsonNode left = om.readTree(leftJson);
|
|
|
+ JsonNode right = om.readTree(rightJson);
|
|
|
+
|
|
|
+ if (validate) {
|
|
|
+ List<String> err = new ArrayList<>();
|
|
|
+ err.addAll(validator.validate(left, schemaName));
|
|
|
+ err.addAll(validator.validate(right, schemaName));
|
|
|
+ if (!err.isEmpty()) {
|
|
|
+ model.addAttribute("schemaErrors", err);
|
|
|
+ model.addAttribute("leftJson", leftJson);
|
|
|
+ model.addAttribute("rightJson", rightJson);
|
|
|
+ model.addAttribute("validate", validate);
|
|
|
+ model.addAttribute("schemaName", schemaName);
|
|
|
+ // versuche dennoch Periodenlisten zu füllen (best-effort) optional
|
|
|
+ return "test";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- Rechts (Master) Zeitraum wählen ---
|
|
|
+ List<Z> zeitrechte = extractZeitraeume(right);
|
|
|
+ if (zeitrechte.isEmpty()) throw new IllegalArgumentException("Right JSON hat keine Zeiträume");
|
|
|
+ if (rightZeitraumId == null) rightZeitraumId = zeitrechte.get(0).id;
|
|
|
+ JsonNode rightZeitraum = findZeitraumById(right, rightZeitraumId);
|
|
|
+
|
|
|
+ // --- Find the stammvorgang that contains the selected zeitraum ---
|
|
|
+ JsonNode stammvorgangJsonNode = findStammvorgangByZeitraumId(right, rightZeitraumId);
|
|
|
+ StammvorgangInfo stammvorgangRight = createStammvorgangInfo(stammvorgangJsonNode);
|
|
|
+
|
|
|
+ // --- Links (Baseline) Kandidaten & Auswahl ---
|
|
|
+ List<Vorgang> leftCand = parseLeftCandidates(left);
|
|
|
+ List<Z> leftZ = List.of();
|
|
|
+ JsonNode leftZeitraum = null;
|
|
|
+ if (!leftCand.isEmpty()) {
|
|
|
+ if (leftVorgangId == null) leftVorgangId = leftCand.get(0).id;
|
|
|
+ final String finalLeftVorgangId = leftVorgangId;
|
|
|
+ leftZ = leftCand.stream().filter(v -> v.id.equals(finalLeftVorgangId)).findFirst().orElse(leftCand.get(0)).zeitraeume;
|
|
|
+ if (leftZeitraumId == null && !leftZ.isEmpty()) leftZeitraumId = leftZ.get(0).id;
|
|
|
+ if (leftZeitraumId != null) leftZeitraum = findZeitraumById(getRootOfVorgang(left, leftVorgangId), leftZeitraumId);
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- Personenliste rechts + Default personId ---
|
|
|
+ List<PersonRef> personenRight = listRightPersons(rightZeitraum);
|
|
|
+ if (personId == null && !personenRight.isEmpty()) personId = personenRight.get(0).id;
|
|
|
+
|
|
|
+ // --- Personen-Tabelle (UNION via person.id) ---
|
|
|
+ Map<String, Pair<JsonNode, JsonNode>> personenPairs = unionByKey(
|
|
|
+ personsArray(rightZeitraum), personsArray(leftZeitraum), n -> n.path("person").path("id").asText()
|
|
|
+ );
|
|
|
+ List<MitgliedRow> mitgliederRows = personenPairs.entrySet().stream()
|
|
|
+ .map(e -> buildMitgliedRow(e.getKey(), e.getValue().right, e.getValue().left))
|
|
|
+ .sorted(Comparator.comparing(MitgliedRow::personId))
|
|
|
+ .toList();
|
|
|
+
|
|
|
+ // --- 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 = buildEinkommenZeilen(rightPerson, leftPerson);
|
|
|
+ List<FreibetragZeile> freibetragZeilen = buildFreibetragZeilen(rightPerson, leftPerson);
|
|
|
+ List<KitaZeile> kitaZeilen = buildKitaZeilen(rightPerson, leftPerson, personNameByIdRight);
|
|
|
+
|
|
|
+ // Summary aus Status zählen (einfach aus allen Zeilen)
|
|
|
+ Summary summary = countSummary(mitgliederRows, einkommenZeilen, freibetragZeilen, kitaZeilen);
|
|
|
+
|
|
|
+ // --- Model füllen & rendern ---
|
|
|
+ model.addAttribute("leftJson", leftJson);
|
|
|
+ model.addAttribute("rightJson", rightJson);
|
|
|
+ model.addAttribute("validate", validate);
|
|
|
+ model.addAttribute("schemaName", schemaName);
|
|
|
+ model.addAttribute("leftCandidates", leftCand);
|
|
|
+ model.addAttribute("leftZeitraeumeOfSelected", leftZ);
|
|
|
+ model.addAttribute("leftVorgangId", leftVorgangId);
|
|
|
+ model.addAttribute("leftZeitraumId", leftZeitraumId);
|
|
|
+ model.addAttribute("zeitraeumeRight", zeitrechte);
|
|
|
+ model.addAttribute("rightZeitraumId", rightZeitraumId);
|
|
|
+ model.addAttribute("stammvorgangRight", stammvorgangRight);
|
|
|
+ model.addAttribute("personenRight", personenRight);
|
|
|
+ model.addAttribute("personId", personId);
|
|
|
+ model.addAttribute("mitgliederRows", mitgliederRows);
|
|
|
+ model.addAttribute("personLabel", personLabel);
|
|
|
+ model.addAttribute("einkommenZeilen", einkommenZeilen);
|
|
|
+ model.addAttribute("freibetragZeilen", freibetragZeilen);
|
|
|
+ model.addAttribute("kitaZeilen", kitaZeilen);
|
|
|
+ model.addAttribute("summary", summary);
|
|
|
+ return "vergleich";
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===== Helper-Typen =====
|
|
|
+ public record Z(String id, String beginn, String ende) {}
|
|
|
+ public record Vorgang(String id, String aktenzeichen, List<Z> zeitraeume) {}
|
|
|
+ public record PersonRef(String id, String label) {}
|
|
|
+ public record StammvorgangInfo(String id, String aktenzeichen, String beginn, String ende, String bemerkung) {}
|
|
|
+ public static final class Pair<R, L> {
|
|
|
+ public final R right;
|
|
|
+ public final L left;
|
|
|
+ public Pair(R r, L l) {
|
|
|
+ right = r;
|
|
|
+ left = l;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===== Helper-Methoden =====
|
|
|
+ private static StammvorgangInfo createStammvorgangInfo(JsonNode stammvorgangNode) {
|
|
|
+ if (stammvorgangNode == null || stammvorgangNode.isMissingNode()) {
|
|
|
+ return new StammvorgangInfo("", "", "", "", "");
|
|
|
+ }
|
|
|
+
|
|
|
+ String id = stammvorgangNode.path("id").asText("");
|
|
|
+ String aktenzeichen = stammvorgangNode.path("aktenzeichen").asText("");
|
|
|
+ String beginn = stammvorgangNode.path("beginn").asText("");
|
|
|
+ String ende = stammvorgangNode.path("ende").asText("");
|
|
|
+ String bemerkung = stammvorgangNode.path("bemerkung").asText("");
|
|
|
+
|
|
|
+ return new StammvorgangInfo(id, aktenzeichen, beginn, ende, bemerkung);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static List<Z> extractZeitraeume(JsonNode root) {
|
|
|
+ List<Z> result = new ArrayList<>();
|
|
|
+
|
|
|
+ // Check if this is the new vorgaenge structure
|
|
|
+ if (root.has("vorgaenge") && root.get("vorgaenge").isArray()) {
|
|
|
+ // For vorgaenge structure, collect zeitraeume from all vorgaenge
|
|
|
+ for (JsonNode vorgang : root.get("vorgaenge")) {
|
|
|
+ if (vorgang.has("zeitraeume") && vorgang.get("zeitraeume").isArray()) {
|
|
|
+ for (JsonNode zeitraum : vorgang.get("zeitraeume")) {
|
|
|
+ String id = zeitraum.path("id").asText("");
|
|
|
+ String beginn = zeitraum.path("beginn").asText("");
|
|
|
+ String ende = zeitraum.path("ende").asText("");
|
|
|
+ result.add(new Z(id, beginn, ende));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (root.has("zeitraeume") && root.get("zeitraeume").isArray()) {
|
|
|
+ // Legacy single vorgang structure
|
|
|
+ for (JsonNode zeitraum : root.get("zeitraeume")) {
|
|
|
+ String id = zeitraum.path("id").asText("");
|
|
|
+ String beginn = zeitraum.path("beginn").asText("");
|
|
|
+ String ende = zeitraum.path("ende").asText("");
|
|
|
+ result.add(new Z(id, beginn, ende));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static JsonNode findZeitraumById(JsonNode root, String id) {
|
|
|
+ // Check if this is the new vorgaenge structure
|
|
|
+ if (root.has("vorgaenge") && root.get("vorgaenge").isArray()) {
|
|
|
+ // Search in all vorgaenge
|
|
|
+ for (JsonNode vorgang : root.get("vorgaenge")) {
|
|
|
+ if (vorgang.has("zeitraeume") && vorgang.get("zeitraeume").isArray()) {
|
|
|
+ for (JsonNode zeitraum : vorgang.get("zeitraeume")) {
|
|
|
+ if (id.equals(zeitraum.path("id").asText(""))) {
|
|
|
+ return zeitraum;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (root.has("zeitraeume") && root.get("zeitraeume").isArray()) {
|
|
|
+ // Legacy single vorgang structure
|
|
|
+ for (JsonNode zeitraum : root.get("zeitraeume")) {
|
|
|
+ if (id.equals(zeitraum.path("id").asText(""))) {
|
|
|
+ return zeitraum;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return NullNode.getInstance();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static JsonNode findStammvorgangByZeitraumId(JsonNode root, String zeitraumId) {
|
|
|
+ // Check if this is the new vorgaenge structure
|
|
|
+ if (root.has("vorgaenge") && root.get("vorgaenge").isArray()) {
|
|
|
+ // Search in all vorgaenge for the one containing this zeitraum
|
|
|
+ for (JsonNode vorgang : root.get("vorgaenge")) {
|
|
|
+ if (vorgang.has("zeitraeume") && vorgang.get("zeitraeume").isArray()) {
|
|
|
+ for (JsonNode zeitraum : vorgang.get("zeitraeume")) {
|
|
|
+ if (zeitraumId.equals(zeitraum.path("id").asText(""))) {
|
|
|
+ return vorgang.path("stammvorgang");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // Legacy single vorgang structure - stammvorgang is at root level
|
|
|
+ return root.path("stammvorgang");
|
|
|
+ }
|
|
|
+ return NullNode.getInstance();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static List<Vorgang> parseLeftCandidates(JsonNode left) {
|
|
|
+ List<Vorgang> result = new ArrayList<>();
|
|
|
+
|
|
|
+ if (left.has("vorgaenge") && left.get("vorgaenge").isArray()) {
|
|
|
+ // a) wenn left.vorgaenge Array
|
|
|
+ for (JsonNode v : left.get("vorgaenge")) {
|
|
|
+ JsonNode stammvorgang = v.path("stammvorgang");
|
|
|
+ String id = stammvorgang.path("id").asText("");
|
|
|
+ String aktenzeichen = stammvorgang.path("aktenzeichen").asText("");
|
|
|
+ List<Z> zeitraeume = extractZeitraeume(v);
|
|
|
+ result.add(new Vorgang(id, aktenzeichen, zeitraeume));
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // b) sonst: single vorgang
|
|
|
+ JsonNode stammvorgang = left.path("stammvorgang");
|
|
|
+ String id = stammvorgang.path("id").asText("");
|
|
|
+ String aktenzeichen = stammvorgang.path("aktenzeichen").asText("");
|
|
|
+ List<Z> zeitraeume = extractZeitraeume(left);
|
|
|
+ result.add(new Vorgang(id, aktenzeichen, zeitraeume));
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static JsonNode getRootOfVorgang(JsonNode left, String vorgangId) {
|
|
|
+ if (left.has("vorgaenge") && left.get("vorgaenge").isArray()) {
|
|
|
+ for (JsonNode v : left.get("vorgaenge")) {
|
|
|
+ if (vorgangId.equals(v.path("stammvorgang").path("id").asText(""))) {
|
|
|
+ return v;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return left;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static List<PersonRef> listRightPersons(JsonNode zeitraum) {
|
|
|
+ List<PersonRef> result = new ArrayList<>();
|
|
|
+ if (zeitraum != null && zeitraum.has("personen") && zeitraum.get("personen").isArray()) {
|
|
|
+ for (JsonNode personImZeitraum : zeitraum.get("personen")) {
|
|
|
+ JsonNode person = personImZeitraum.path("person");
|
|
|
+ String id = person.path("id").asText("");
|
|
|
+ String nachname = person.path("nachname").asText("");
|
|
|
+ String vorname = person.path("vorname").asText("");
|
|
|
+ String geburtsdatum = person.path("geburtsdatum").asText("");
|
|
|
+
|
|
|
+ String label = nachname + ", " + vorname;
|
|
|
+ if (!geburtsdatum.isEmpty()) {
|
|
|
+ label += " (" + FormatUtil.date(geburtsdatum) + ")";
|
|
|
+ }
|
|
|
+
|
|
|
+ result.add(new PersonRef(id, label));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static ArrayNode personsArray(JsonNode zeitraum) {
|
|
|
+ return zeitraum != null && zeitraum.has("personen") && zeitraum.get("personen").isArray()
|
|
|
+ ? (ArrayNode) zeitraum.get("personen")
|
|
|
+ : new ObjectMapper().createArrayNode();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static <T> Map<String, Pair<JsonNode, JsonNode>> unionByKey(ArrayNode rightArr, ArrayNode leftArr, Function<JsonNode, String> keyFn) {
|
|
|
+ Map<String, Pair<JsonNode, JsonNode>> result = new LinkedHashMap<>();
|
|
|
+
|
|
|
+ // Add all right items
|
|
|
+ for (JsonNode node : rightArr) {
|
|
|
+ String key = keyFn.apply(node);
|
|
|
+ result.put(key, new Pair<>(node, null));
|
|
|
+ }
|
|
|
+
|
|
|
+ // Merge left items
|
|
|
+ for (JsonNode node : leftArr) {
|
|
|
+ String key = keyFn.apply(node);
|
|
|
+ result.merge(key, new Pair<>(null, node), (existing, newPair) ->
|
|
|
+ new Pair<>(existing.right, newPair.left));
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static MitgliedRow buildMitgliedRow(String personId, JsonNode rightItem, JsonNode leftItem) {
|
|
|
+ // Extract person data from both items
|
|
|
+ JsonNode rightPerson = rightItem != null ? rightItem.path("person") : null;
|
|
|
+ JsonNode leftPerson = leftItem != null ? leftItem.path("person") : null;
|
|
|
+
|
|
|
+ // Determine status and detect changes
|
|
|
+ String symbol = "";
|
|
|
+ String symbolClass = "";
|
|
|
+ String rowClass = "";
|
|
|
+ boolean hasChanges = false;
|
|
|
+
|
|
|
+ if (rightItem != null && leftItem != null) {
|
|
|
+ // Both exist - check for changes
|
|
|
+ hasChanges = detectPersonChanges(rightItem, rightPerson, leftItem, leftPerson);
|
|
|
+ if (hasChanges) {
|
|
|
+ symbol = symFor("CHANGED");
|
|
|
+ symbolClass = symClassFor("CHANGED");
|
|
|
+ rowClass = "";
|
|
|
+ }
|
|
|
+ } else if (rightItem != null && leftItem == null) {
|
|
|
+ symbol = symFor("ADDED");
|
|
|
+ symbolClass = symClassFor("ADDED");
|
|
|
+ rowClass = rowClassFor("ADDED");
|
|
|
+ } else if (rightItem == null && leftItem != null) {
|
|
|
+ symbol = symFor("REMOVED");
|
|
|
+ symbolClass = symClassFor("REMOVED");
|
|
|
+ rowClass = rowClassFor("REMOVED");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Use right (current) data as primary, fall back to left for missing data
|
|
|
+ JsonNode primaryPerson = rightPerson != null ? rightPerson : leftPerson;
|
|
|
+ JsonNode primaryItem = rightItem != null ? rightItem : leftItem;
|
|
|
+
|
|
|
+ if (primaryPerson == null) {
|
|
|
+ return new MitgliedRow(personId, symbol, symbolClass, rowClass, "", "", "", "", "", "", "");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Extract data with change detection for HTML
|
|
|
+ String nameHtml = buildPersonFieldHtml(
|
|
|
+ rightPerson, leftPerson,
|
|
|
+ p -> p.path("nachname").asText("") + ", " + p.path("vorname").asText(""),
|
|
|
+ hasChanges
|
|
|
+ );
|
|
|
+
|
|
|
+ String geburtsdatumHtml = buildPersonFieldHtml(
|
|
|
+ rightPerson, leftPerson,
|
|
|
+ p -> !p.path("geburtsdatum").asText("").isEmpty() ? FormatUtil.date(p.path("geburtsdatum").asText("")) : "",
|
|
|
+ hasChanges
|
|
|
+ );
|
|
|
+
|
|
|
+ String familienstandHtml = buildPersonFieldHtml(
|
|
|
+ rightPerson, leftPerson,
|
|
|
+ p -> p.path("familienstand").asText(""),
|
|
|
+ hasChanges
|
|
|
+ );
|
|
|
+
|
|
|
+ String geschlechtHtml = buildPersonFieldHtml(
|
|
|
+ rightPerson, leftPerson,
|
|
|
+ p -> p.path("geschlecht").asText(""),
|
|
|
+ hasChanges
|
|
|
+ );
|
|
|
+
|
|
|
+ String verwandtschaftHtml = buildItemFieldHtml(
|
|
|
+ rightItem, leftItem,
|
|
|
+ i -> i.path("verwandtschaft").asText(""),
|
|
|
+ hasChanges
|
|
|
+ );
|
|
|
+
|
|
|
+ String sozialstellungHtml = buildItemFieldHtml(
|
|
|
+ rightItem, leftItem,
|
|
|
+ i -> i.path("sozialstellung").asText(""),
|
|
|
+ hasChanges
|
|
|
+ );
|
|
|
+
|
|
|
+ String istKindHtml = buildItemFieldHtml(
|
|
|
+ rightItem, leftItem,
|
|
|
+ i -> i.path("istKind").asBoolean(false) ? "Ja" : "Nein",
|
|
|
+ hasChanges
|
|
|
+ );
|
|
|
+
|
|
|
+ return new MitgliedRow(
|
|
|
+ personId,
|
|
|
+ symbol,
|
|
|
+ symbolClass,
|
|
|
+ rowClass,
|
|
|
+ nameHtml,
|
|
|
+ geburtsdatumHtml,
|
|
|
+ familienstandHtml,
|
|
|
+ geschlechtHtml,
|
|
|
+ verwandtschaftHtml,
|
|
|
+ sozialstellungHtml,
|
|
|
+ istKindHtml
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean detectPersonChanges(JsonNode rightItem, JsonNode rightPerson, JsonNode leftItem, JsonNode leftPerson) {
|
|
|
+ if (rightPerson == null || leftPerson == null) return false;
|
|
|
+
|
|
|
+ // Check person fields
|
|
|
+ return !Objects.equals(rightPerson.path("nachname").asText(""), leftPerson.path("nachname").asText("")) ||
|
|
|
+ !Objects.equals(rightPerson.path("vorname").asText(""), leftPerson.path("vorname").asText("")) ||
|
|
|
+ !Objects.equals(rightPerson.path("geburtsdatum").asText(""), leftPerson.path("geburtsdatum").asText("")) ||
|
|
|
+ !Objects.equals(rightPerson.path("familienstand").asText(""), leftPerson.path("familienstand").asText("")) ||
|
|
|
+ !Objects.equals(rightPerson.path("geschlecht").asText(""), leftPerson.path("geschlecht").asText("")) ||
|
|
|
+ !Objects.equals(rightItem.path("verwandtschaft").asText(""), leftItem.path("verwandtschaft").asText("")) ||
|
|
|
+ !Objects.equals(rightItem.path("sozialstellung").asText(""), leftItem.path("sozialstellung").asText("")) ||
|
|
|
+ rightItem.path("istKind").asBoolean(false) != leftItem.path("istKind").asBoolean(false);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String buildPersonFieldHtml(JsonNode rightPerson, JsonNode leftPerson, Function<JsonNode, String> extractor, boolean hasChanges) {
|
|
|
+ if (!hasChanges || rightPerson == null) {
|
|
|
+ JsonNode primary = rightPerson != null ? rightPerson : leftPerson;
|
|
|
+ return primary != null ? escape(extractor.apply(primary)) : "";
|
|
|
+ }
|
|
|
+
|
|
|
+ String rightVal = rightPerson != null ? extractor.apply(rightPerson) : "";
|
|
|
+ String leftVal = leftPerson != null ? extractor.apply(leftPerson) : "";
|
|
|
+
|
|
|
+ if (!Objects.equals(rightVal, leftVal)) {
|
|
|
+ return cellHtml(rightVal, leftVal, true);
|
|
|
+ } else {
|
|
|
+ return escape(rightVal);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String buildItemFieldHtml(JsonNode rightItem, JsonNode leftItem, Function<JsonNode, String> extractor, boolean hasChanges) {
|
|
|
+ if (!hasChanges || rightItem == null) {
|
|
|
+ JsonNode primary = rightItem != null ? rightItem : leftItem;
|
|
|
+ return primary != null ? escape(extractor.apply(primary)) : "";
|
|
|
+ }
|
|
|
+
|
|
|
+ String rightVal = rightItem != null ? extractor.apply(rightItem) : "";
|
|
|
+ String leftVal = leftItem != null ? extractor.apply(leftItem) : "";
|
|
|
+
|
|
|
+ if (!Objects.equals(rightVal, leftVal)) {
|
|
|
+ return cellHtml(rightVal, leftVal, true);
|
|
|
+ } else {
|
|
|
+ return escape(rightVal);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static List<EinkommenZeile> buildEinkommenZeilen(JsonNode rightPerson, JsonNode leftPerson) {
|
|
|
+ // Extract einkommen arrays from both persons
|
|
|
+ ArrayNode rightEinkommen = rightPerson != null && rightPerson.has("einkommen") && rightPerson.get("einkommen").isArray()
|
|
|
+ ? (ArrayNode) rightPerson.get("einkommen")
|
|
|
+ : new ObjectMapper().createArrayNode();
|
|
|
+ ArrayNode leftEinkommen = leftPerson != null && leftPerson.has("einkommen") && leftPerson.get("einkommen").isArray()
|
|
|
+ ? (ArrayNode) leftPerson.get("einkommen")
|
|
|
+ : new ObjectMapper().createArrayNode();
|
|
|
+
|
|
|
+ // UNION über einkommen[].id
|
|
|
+ Map<String, Pair<JsonNode, JsonNode>> einkommenPairs = unionByKey(
|
|
|
+ rightEinkommen, leftEinkommen, n -> n.path("id").asText()
|
|
|
+ );
|
|
|
+
|
|
|
+ List<EinkommenZeile> result = new ArrayList<>();
|
|
|
+ for (Map.Entry<String, Pair<JsonNode, JsonNode>> entry : einkommenPairs.entrySet()) {
|
|
|
+ JsonNode rightItem = entry.getValue().right;
|
|
|
+ JsonNode leftItem = entry.getValue().left;
|
|
|
+
|
|
|
+ // Determine status and changes
|
|
|
+ String symbol = "";
|
|
|
+ String symbolClass = "";
|
|
|
+ String rowClass = "";
|
|
|
+ boolean hasChanges = false;
|
|
|
+
|
|
|
+ if (rightItem != null && leftItem != null) {
|
|
|
+ hasChanges = detectEinkommenChanges(rightItem, leftItem);
|
|
|
+ if (hasChanges) {
|
|
|
+ symbol = symFor("CHANGED");
|
|
|
+ symbolClass = symClassFor("CHANGED");
|
|
|
+ }
|
|
|
+ } else if (rightItem != null && leftItem == null) {
|
|
|
+ symbol = symFor("ADDED");
|
|
|
+ symbolClass = symClassFor("ADDED");
|
|
|
+ rowClass = rowClassFor("ADDED");
|
|
|
+ } else if (rightItem == null && leftItem != null) {
|
|
|
+ symbol = symFor("REMOVED");
|
|
|
+ symbolClass = symClassFor("REMOVED");
|
|
|
+ rowClass = rowClassFor("REMOVED");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Use primary data (right first, then left)
|
|
|
+ JsonNode primaryItem = rightItem != null ? rightItem : leftItem;
|
|
|
+
|
|
|
+ if (primaryItem != null) {
|
|
|
+ String artHtml = buildEinkommenFieldHtml(rightItem, leftItem,
|
|
|
+ i -> {
|
|
|
+ String art = i.path("art").asText("");
|
|
|
+ String artZusatz = i.path("artZusatz").asText("");
|
|
|
+ return !artZusatz.isEmpty() ? art + " (" + artZusatz + ")" : art;
|
|
|
+ }, hasChanges);
|
|
|
+
|
|
|
+ String frequenzHtml = buildEinkommenFieldHtml(rightItem, leftItem,
|
|
|
+ i -> {
|
|
|
+ int frequenz = i.path("frequenz").asInt(1);
|
|
|
+ return frequenz == 1 ? "monatlich" :
|
|
|
+ frequenz == 12 ? "jährlich" :
|
|
|
+ frequenz + "x/Jahr";
|
|
|
+ }, hasChanges);
|
|
|
+
|
|
|
+ String betragHtml = buildEinkommenFieldHtml(rightItem, leftItem,
|
|
|
+ i -> String.format("%.2f €", i.path("betrag").asDouble(0.0)), hasChanges);
|
|
|
+
|
|
|
+ String zuschlagHtml = buildEinkommenFieldHtml(rightItem, leftItem,
|
|
|
+ i -> {
|
|
|
+ double zuschlag = i.path("zuschlag").asDouble(0.0);
|
|
|
+ return zuschlag > 0 ? String.format("%.2f €", zuschlag) : "";
|
|
|
+ }, hasChanges);
|
|
|
+
|
|
|
+ String abschlagHtml = buildEinkommenFieldHtml(rightItem, leftItem,
|
|
|
+ i -> {
|
|
|
+ double abschlag = i.path("abschlag").asDouble(0.0);
|
|
|
+ return abschlag > 0 ? String.format("%.2f €", abschlag) : "";
|
|
|
+ }, hasChanges);
|
|
|
+
|
|
|
+ String werbungskostenHtml = buildEinkommenFieldHtml(rightItem, leftItem,
|
|
|
+ i -> {
|
|
|
+ double werbungskosten = i.path("werbungskosten").asDouble(0.0);
|
|
|
+ return werbungskosten > 0 ? String.format("%.2f €", werbungskosten) : "";
|
|
|
+ }, hasChanges);
|
|
|
+
|
|
|
+ String berechnungsartHtml = buildEinkommenFieldHtml(rightItem, leftItem,
|
|
|
+ i -> i.path("berechnungsart").asText(""), hasChanges);
|
|
|
+
|
|
|
+ result.add(new EinkommenZeile(
|
|
|
+ symbol, symbolClass, rowClass,
|
|
|
+ artHtml, frequenzHtml, betragHtml,
|
|
|
+ zuschlagHtml, abschlagHtml, werbungskostenHtml, berechnungsartHtml
|
|
|
+ ));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean detectEinkommenChanges(JsonNode rightItem, JsonNode leftItem) {
|
|
|
+ return !Objects.equals(rightItem.path("art").asText(""), leftItem.path("art").asText("")) ||
|
|
|
+ !Objects.equals(rightItem.path("artZusatz").asText(""), leftItem.path("artZusatz").asText("")) ||
|
|
|
+ rightItem.path("frequenz").asInt(1) != leftItem.path("frequenz").asInt(1) ||
|
|
|
+ rightItem.path("betrag").asDouble(0.0) != leftItem.path("betrag").asDouble(0.0) ||
|
|
|
+ rightItem.path("zuschlag").asDouble(0.0) != leftItem.path("zuschlag").asDouble(0.0) ||
|
|
|
+ rightItem.path("abschlag").asDouble(0.0) != leftItem.path("abschlag").asDouble(0.0) ||
|
|
|
+ rightItem.path("werbungskosten").asDouble(0.0) != leftItem.path("werbungskosten").asDouble(0.0) ||
|
|
|
+ !Objects.equals(rightItem.path("berechnungsart").asText(""), leftItem.path("berechnungsart").asText(""));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String buildEinkommenFieldHtml(JsonNode rightItem, JsonNode leftItem, Function<JsonNode, String> extractor, boolean hasChanges) {
|
|
|
+ if (!hasChanges || rightItem == null) {
|
|
|
+ JsonNode primary = rightItem != null ? rightItem : leftItem;
|
|
|
+ return primary != null ? escape(extractor.apply(primary)) : "";
|
|
|
+ }
|
|
|
+
|
|
|
+ String rightVal = rightItem != null ? extractor.apply(rightItem) : "";
|
|
|
+ String leftVal = leftItem != null ? extractor.apply(leftItem) : "";
|
|
|
+
|
|
|
+ if (!Objects.equals(rightVal, leftVal)) {
|
|
|
+ return cellHtml(rightVal, leftVal, true);
|
|
|
+ } else {
|
|
|
+ return escape(rightVal);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static List<FreibetragZeile> buildFreibetragZeilen(JsonNode rightPerson, JsonNode leftPerson) {
|
|
|
+ // Extract freibetraege arrays from both persons
|
|
|
+ ArrayNode rightFreibetraege = rightPerson != null && rightPerson.has("freibetraege") && rightPerson.get("freibetraege").isArray()
|
|
|
+ ? (ArrayNode) rightPerson.get("freibetraege")
|
|
|
+ : new ObjectMapper().createArrayNode();
|
|
|
+ ArrayNode leftFreibetraege = leftPerson != null && leftPerson.has("freibetraege") && leftPerson.get("freibetraege").isArray()
|
|
|
+ ? (ArrayNode) leftPerson.get("freibetraege")
|
|
|
+ : new ObjectMapper().createArrayNode();
|
|
|
+
|
|
|
+ // UNION über freibetraege[].id
|
|
|
+ Map<String, Pair<JsonNode, JsonNode>> freibetragPairs = unionByKey(
|
|
|
+ rightFreibetraege, leftFreibetraege, n -> n.path("id").asText()
|
|
|
+ );
|
|
|
+
|
|
|
+ List<FreibetragZeile> result = new ArrayList<>();
|
|
|
+ for (Map.Entry<String, Pair<JsonNode, JsonNode>> entry : freibetragPairs.entrySet()) {
|
|
|
+ JsonNode rightItem = entry.getValue().right;
|
|
|
+ JsonNode leftItem = entry.getValue().left;
|
|
|
+
|
|
|
+ // Determine status and changes
|
|
|
+ String symbol = "";
|
|
|
+ String symbolClass = "";
|
|
|
+ String rowClass = "";
|
|
|
+ boolean hasChanges = false;
|
|
|
+
|
|
|
+ if (rightItem != null && leftItem != null) {
|
|
|
+ hasChanges = detectFreibetragChanges(rightItem, leftItem);
|
|
|
+ if (hasChanges) {
|
|
|
+ symbol = symFor("CHANGED");
|
|
|
+ symbolClass = symClassFor("CHANGED");
|
|
|
+ }
|
|
|
+ } else if (rightItem != null && leftItem == null) {
|
|
|
+ symbol = symFor("ADDED");
|
|
|
+ symbolClass = symClassFor("ADDED");
|
|
|
+ rowClass = rowClassFor("ADDED");
|
|
|
+ } else if (rightItem == null && leftItem != null) {
|
|
|
+ symbol = symFor("REMOVED");
|
|
|
+ symbolClass = symClassFor("REMOVED");
|
|
|
+ rowClass = rowClassFor("REMOVED");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Use primary data (right first, then left)
|
|
|
+ JsonNode primaryItem = rightItem != null ? rightItem : leftItem;
|
|
|
+
|
|
|
+ if (primaryItem != null) {
|
|
|
+ String artHtml = buildFreibetragFieldHtml(rightItem, leftItem,
|
|
|
+ i -> i.path("art").asText(""), hasChanges);
|
|
|
+
|
|
|
+ String frequenzHtml = buildFreibetragFieldHtml(rightItem, leftItem,
|
|
|
+ i -> {
|
|
|
+ int frequenz = i.path("frequenz").asInt(1);
|
|
|
+ return frequenz == 1 ? "monatlich" :
|
|
|
+ frequenz == 12 ? "jährlich" :
|
|
|
+ frequenz + "x/Jahr";
|
|
|
+ }, hasChanges);
|
|
|
+
|
|
|
+ String betragHtml = buildFreibetragFieldHtml(rightItem, leftItem,
|
|
|
+ i -> String.format("%.2f €", i.path("betrag").asDouble(0.0)), hasChanges);
|
|
|
+
|
|
|
+ String fristHtml = buildFreibetragFieldHtml(rightItem, leftItem,
|
|
|
+ i -> {
|
|
|
+ String fristDatum = i.path("fristDatum").asText("");
|
|
|
+ return !fristDatum.isEmpty() ? FormatUtil.date(fristDatum) : "";
|
|
|
+ }, hasChanges);
|
|
|
+
|
|
|
+ result.add(new FreibetragZeile(
|
|
|
+ symbol, symbolClass, rowClass,
|
|
|
+ artHtml, frequenzHtml, betragHtml, fristHtml
|
|
|
+ ));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean detectFreibetragChanges(JsonNode rightItem, JsonNode leftItem) {
|
|
|
+ return !Objects.equals(rightItem.path("art").asText(""), leftItem.path("art").asText("")) ||
|
|
|
+ rightItem.path("frequenz").asInt(1) != leftItem.path("frequenz").asInt(1) ||
|
|
|
+ rightItem.path("betrag").asDouble(0.0) != leftItem.path("betrag").asDouble(0.0) ||
|
|
|
+ !Objects.equals(rightItem.path("fristDatum").asText(""), leftItem.path("fristDatum").asText(""));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String buildFreibetragFieldHtml(JsonNode rightItem, JsonNode leftItem, Function<JsonNode, String> extractor, boolean hasChanges) {
|
|
|
+ if (!hasChanges || rightItem == null) {
|
|
|
+ JsonNode primary = rightItem != null ? rightItem : leftItem;
|
|
|
+ return primary != null ? escape(extractor.apply(primary)) : "";
|
|
|
+ }
|
|
|
+
|
|
|
+ String rightVal = rightItem != null ? extractor.apply(rightItem) : "";
|
|
|
+ String leftVal = leftItem != null ? extractor.apply(leftItem) : "";
|
|
|
+
|
|
|
+ if (!Objects.equals(rightVal, leftVal)) {
|
|
|
+ return cellHtml(rightVal, leftVal, true);
|
|
|
+ } else {
|
|
|
+ return escape(rightVal);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static List<KitaZeile> buildKitaZeilen(JsonNode rightPerson, JsonNode leftPerson, Map<String, String> personNameByIdRight) {
|
|
|
+ // Extract kinderbetreuung arrays from both persons
|
|
|
+ ArrayNode rightKinderbetreuung = rightPerson != null && rightPerson.has("kinderbetreuung") && rightPerson.get("kinderbetreuung").isArray()
|
|
|
+ ? (ArrayNode) rightPerson.get("kinderbetreuung")
|
|
|
+ : new ObjectMapper().createArrayNode();
|
|
|
+ ArrayNode leftKinderbetreuung = leftPerson != null && leftPerson.has("kinderbetreuung") && leftPerson.get("kinderbetreuung").isArray()
|
|
|
+ ? (ArrayNode) leftPerson.get("kinderbetreuung")
|
|
|
+ : new ObjectMapper().createArrayNode();
|
|
|
+
|
|
|
+ // UNION über kinderbetreuung[].id
|
|
|
+ Map<String, Pair<JsonNode, JsonNode>> kitaPairs = unionByKey(
|
|
|
+ rightKinderbetreuung, leftKinderbetreuung, n -> n.path("id").asText()
|
|
|
+ );
|
|
|
+
|
|
|
+ List<KitaZeile> result = new ArrayList<>();
|
|
|
+ for (Map.Entry<String, Pair<JsonNode, JsonNode>> entry : kitaPairs.entrySet()) {
|
|
|
+ JsonNode rightItem = entry.getValue().right;
|
|
|
+ JsonNode leftItem = entry.getValue().left;
|
|
|
+
|
|
|
+ // Determine status and changes
|
|
|
+ String symbol = "";
|
|
|
+ String symbolClass = "";
|
|
|
+ String rowClass = "";
|
|
|
+ boolean hasChanges = false;
|
|
|
+
|
|
|
+ if (rightItem != null && leftItem != null) {
|
|
|
+ hasChanges = detectKitaChanges(rightItem, leftItem);
|
|
|
+ if (hasChanges) {
|
|
|
+ symbol = symFor("CHANGED");
|
|
|
+ symbolClass = symClassFor("CHANGED");
|
|
|
+ }
|
|
|
+ } else if (rightItem != null && leftItem == null) {
|
|
|
+ symbol = symFor("ADDED");
|
|
|
+ symbolClass = symClassFor("ADDED");
|
|
|
+ rowClass = rowClassFor("ADDED");
|
|
|
+ } else if (rightItem == null && leftItem != null) {
|
|
|
+ symbol = symFor("REMOVED");
|
|
|
+ symbolClass = symClassFor("REMOVED");
|
|
|
+ rowClass = rowClassFor("REMOVED");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Use primary data (right first, then left)
|
|
|
+ JsonNode primaryItem = rightItem != null ? rightItem : leftItem;
|
|
|
+
|
|
|
+ if (primaryItem != null) {
|
|
|
+ String kindNameHtml = buildKitaFieldHtml(rightItem, leftItem,
|
|
|
+ i -> {
|
|
|
+ String kindRef = i.path("kindRef").asText("");
|
|
|
+ return personNameByIdRight.getOrDefault(kindRef, "Kind ID: " + kindRef);
|
|
|
+ }, hasChanges);
|
|
|
+
|
|
|
+ String betragHtml = buildKitaFieldHtml(rightItem, leftItem,
|
|
|
+ i -> String.format("%.2f €", i.path("betrag").asDouble(0.0)), hasChanges);
|
|
|
+
|
|
|
+ result.add(new KitaZeile(
|
|
|
+ symbol, symbolClass, rowClass,
|
|
|
+ kindNameHtml, betragHtml
|
|
|
+ ));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean detectKitaChanges(JsonNode rightItem, JsonNode leftItem) {
|
|
|
+ return !Objects.equals(rightItem.path("kindRef").asText(""), leftItem.path("kindRef").asText("")) ||
|
|
|
+ rightItem.path("betrag").asDouble(0.0) != leftItem.path("betrag").asDouble(0.0);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String buildKitaFieldHtml(JsonNode rightItem, JsonNode leftItem, Function<JsonNode, String> extractor, boolean hasChanges) {
|
|
|
+ if (!hasChanges || rightItem == null) {
|
|
|
+ JsonNode primary = rightItem != null ? rightItem : leftItem;
|
|
|
+ return primary != null ? escape(extractor.apply(primary)) : "";
|
|
|
+ }
|
|
|
+
|
|
|
+ String rightVal = rightItem != null ? extractor.apply(rightItem) : "";
|
|
|
+ String leftVal = leftItem != null ? extractor.apply(leftItem) : "";
|
|
|
+
|
|
|
+ if (!Objects.equals(rightVal, leftVal)) {
|
|
|
+ return cellHtml(rightVal, leftVal, true);
|
|
|
+ } else {
|
|
|
+ return escape(rightVal);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Summary countSummary(List<MitgliedRow> m, List<EinkommenZeile> e, List<FreibetragZeile> f, List<KitaZeile> k) {
|
|
|
+ int added = 0, removed = 0, changed = 0, typeMismatch = 0;
|
|
|
+
|
|
|
+ // Count MitgliedRow changes
|
|
|
+ for (MitgliedRow row : m) {
|
|
|
+ if ("plus".equals(row.symbolClass())) added++;
|
|
|
+ else if ("minus".equals(row.symbolClass())) removed++;
|
|
|
+ else if ("warn".equals(row.symbolClass())) changed++;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Count EinkommenZeile changes
|
|
|
+ for (EinkommenZeile row : e) {
|
|
|
+ if ("plus".equals(row.symbolClass())) added++;
|
|
|
+ else if ("minus".equals(row.symbolClass())) removed++;
|
|
|
+ else if ("warn".equals(row.symbolClass())) changed++;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Count FreibetragZeile changes
|
|
|
+ for (FreibetragZeile row : f) {
|
|
|
+ if ("plus".equals(row.symbolClass())) added++;
|
|
|
+ else if ("minus".equals(row.symbolClass())) removed++;
|
|
|
+ else if ("warn".equals(row.symbolClass())) changed++;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Count KitaZeile changes
|
|
|
+ for (KitaZeile row : k) {
|
|
|
+ if ("plus".equals(row.symbolClass())) added++;
|
|
|
+ else if ("minus".equals(row.symbolClass())) removed++;
|
|
|
+ else if ("warn".equals(row.symbolClass())) changed++;
|
|
|
+ }
|
|
|
+
|
|
|
+ int total = added + removed + changed + typeMismatch;
|
|
|
+ return new Summary(added, removed, changed, typeMismatch, total);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Hilfs-Bausteine für Status und Zellen:
|
|
|
+ private static String symFor(String status) {
|
|
|
+ return switch (status) {
|
|
|
+ case "ADDED" -> "+";
|
|
|
+ case "REMOVED" -> "−";
|
|
|
+ case "CHANGED" -> "!";
|
|
|
+ default -> "";
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String symClassFor(String status) {
|
|
|
+ return switch (status) {
|
|
|
+ case "ADDED" -> "plus";
|
|
|
+ case "REMOVED" -> "minus";
|
|
|
+ case "CHANGED" -> "warn";
|
|
|
+ default -> "";
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String rowClassFor(String status) {
|
|
|
+ return switch (status) {
|
|
|
+ case "ADDED" -> "diff-row-added";
|
|
|
+ case "REMOVED" -> "diff-row-removed";
|
|
|
+ default -> "";
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String cellHtml(String newVal, String oldVal, boolean changed) {
|
|
|
+ if (!changed) {
|
|
|
+ return escape(newVal);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Handle empty old values
|
|
|
+ String displayOldVal = (oldVal == null || oldVal.trim().isEmpty()) ? "leer" : oldVal;
|
|
|
+
|
|
|
+ // Return with cell-changed class and proper old value display
|
|
|
+ return "<span class='cell-changed'>" + escape(newVal) +
|
|
|
+ "<div class='old'>Alt: " + escape(displayOldVal) + "</div></span>";
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String escape(String s) {
|
|
|
+ return s == null ? "" : s.replace("&", "&").replace("<", "<").replace(">", ">");
|
|
|
+ }
|
|
|
+}
|