Repository: oblassgit/3FA071_Gruppe5_UI
Pull request #36: 12 import data as json xml csv
Author: login: oblassgit
Created: 2025-05-14T06:50:29Z | Merged: 2025-05-14T10:24:13Z



Diff:
diff --git a/package-lock.json b/package-lock.json
index 8282a93..de0b14f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
         "bootstrap-vue-3": "^0.5.1",
         "moment": "^2.30.1",
         "portal-vue": "^3.0.0",
+        "papaparse": "^5.5.2",
         "vue": "^3.5.13",
         "vue-router": "^4.5.0"
       },
@@ -279,19 +280,20 @@
       "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz",
       "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==",
       "dependencies": {
-        "@babel/template": "^7.25.9",
-        "@babel/types": "^7.26.7"
+        "@babel/template": "^7.26.9",
+        "@babel/types": "^7.26.10"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.26.7",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz",
-      "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==",
+      "version": "7.26.10",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz",
+      "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==",
+      "license": "MIT",
       "dependencies": {
-        "@babel/types": "^7.26.7"
+        "@babel/types": "^7.26.10"
       },
       "bin": {
         "parser": "bin/babel-parser.js"
@@ -421,9 +423,9 @@
       "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
       "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
       "dependencies": {
-        "@babel/code-frame": "^7.25.9",
-        "@babel/parser": "^7.25.9",
-        "@babel/types": "^7.25.9"
+        "@babel/code-frame": "^7.26.2",
+        "@babel/parser": "^7.26.9",
+        "@babel/types": "^7.26.9"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -447,9 +449,10 @@
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.26.7",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz",
-      "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==",
+      "version": "7.26.10",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz",
+      "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==",
+      "license": "MIT",
       "dependencies": {
         "@babel/helper-string-parser": "^7.25.9",
         "@babel/helper-validator-identifier": "^7.25.9"
@@ -466,6 +469,7 @@
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "win32"
@@ -515,6 +519,7 @@
         "vue-demi-fix": "bin/vue-demi-fix.js",
         "vue-demi-switch": "bin/vue-demi-switch.js"
       },
+
       "engines": {
         "node": ">=12"
       },
@@ -1094,9 +1099,9 @@
       "license": "MIT"
     },
     "node_modules/axios": {
-      "version": "1.7.9",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
-      "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
+      "version": "1.8.3",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
+      "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
       "license": "MIT",
       "dependencies": {
         "follow-redirects": "^1.15.6",
@@ -1530,11 +1535,12 @@
       }
     },
     "node_modules/esbuild": {
-      "version": "0.24.2",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
-      "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
+      "version": "0.25.1",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz",
+      "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==",
       "dev": true,
       "hasInstallScript": true,
+      "license": "MIT",
       "bin": {
         "esbuild": "bin/esbuild"
       },
@@ -1542,31 +1548,31 @@
         "node": ">=18"
       },
       "optionalDependencies": {
-        "@esbuild/aix-ppc64": "0.24.2",
-        "@esbuild/android-arm": "0.24.2",
-        "@esbuild/android-arm64": "0.24.2",
-        "@esbuild/android-x64": "0.24.2",
-        "@esbuild/darwin-arm64": "0.24.2",
-        "@esbuild/darwin-x64": "0.24.2",
-        "@esbuild/freebsd-arm64": "0.24.2",
-        "@esbuild/freebsd-x64": "0.24.2",
-        "@esbuild/linux-arm": "0.24.2",
-        "@esbuild/linux-arm64": "0.24.2",
-        "@esbuild/linux-ia32": "0.24.2",
-        "@esbuild/linux-loong64": "0.24.2",
-        "@esbuild/linux-mips64el": "0.24.2",
-        "@esbuild/linux-ppc64": "0.24.2",
-        "@esbuild/linux-riscv64": "0.24.2",
-        "@esbuild/linux-s390x": "0.24.2",
-        "@esbuild/linux-x64": "0.24.2",
-        "@esbuild/netbsd-arm64": "0.24.2",
-        "@esbuild/netbsd-x64": "0.24.2",
-        "@esbuild/openbsd-arm64": "0.24.2",
-        "@esbuild/openbsd-x64": "0.24.2",
-        "@esbuild/sunos-x64": "0.24.2",
-        "@esbuild/win32-arm64": "0.24.2",
-        "@esbuild/win32-ia32": "0.24.2",
-        "@esbuild/win32-x64": "0.24.2"
+        "@esbuild/aix-ppc64": "0.25.1",
+        "@esbuild/android-arm": "0.25.1",
+        "@esbuild/android-arm64": "0.25.1",
+        "@esbuild/android-x64": "0.25.1",
+        "@esbuild/darwin-arm64": "0.25.1",
+        "@esbuild/darwin-x64": "0.25.1",
+        "@esbuild/freebsd-arm64": "0.25.1",
+        "@esbuild/freebsd-x64": "0.25.1",
+        "@esbuild/linux-arm": "0.25.1",
+        "@esbuild/linux-arm64": "0.25.1",
+        "@esbuild/linux-ia32": "0.25.1",
+        "@esbuild/linux-loong64": "0.25.1",
+        "@esbuild/linux-mips64el": "0.25.1",
+        "@esbuild/linux-ppc64": "0.25.1",
+        "@esbuild/linux-riscv64": "0.25.1",
+        "@esbuild/linux-s390x": "0.25.1",
+        "@esbuild/linux-x64": "0.25.1",
+        "@esbuild/netbsd-arm64": "0.25.1",
+        "@esbuild/netbsd-x64": "0.25.1",
+        "@esbuild/openbsd-arm64": "0.25.1",
+        "@esbuild/openbsd-x64": "0.25.1",
+        "@esbuild/sunos-x64": "0.25.1",
+        "@esbuild/win32-arm64": "0.25.1",
+        "@esbuild/win32-ia32": "0.25.1",
+        "@esbuild/win32-x64": "0.25.1"
       }
     },
     "node_modules/escalade": {
@@ -2405,6 +2411,12 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/papaparse": {
+      "version": "5.5.2",
+      "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.2.tgz",
+      "integrity": "sha512-PZXg8UuAc4PcVwLosEEDYjPyfWnTEhOrUfdv+3Bx+NuAb+5NhDmXzg5fHWmdCh1mP5p7JAZfFr3IMQfcntNAdA==",
+      "license": "MIT"
+    },
     "node_modules/parse-ms": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
@@ -2487,9 +2499,9 @@
       }
     },
     "node_modules/postcss": {
-      "version": "8.5.1",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
-      "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
+      "version": "8.5.3",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
+      "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
       "funding": [
         {
           "type": "opencollective",
@@ -2504,6 +2516,7 @@
           "url": "https://github.com/sponsors/ai"
         }
       ],
+      "license": "MIT",
       "dependencies": {
         "nanoid": "^3.3.8",
         "picocolors": "^1.1.1",
@@ -3068,14 +3081,15 @@
       "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
     },
     "node_modules/vite": {
-      "version": "6.0.11",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz",
-      "integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==",
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.1.tgz",
+      "integrity": "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "esbuild": "^0.24.2",
-        "postcss": "^8.4.49",
-        "rollup": "^4.23.0"
+        "esbuild": "^0.25.0",
+        "postcss": "^8.5.3",
+        "rollup": "^4.30.1"
       },
       "bin": {
         "vite": "bin/vite.js"
diff --git a/package.json b/package.json
index 9a73efb..131f611 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
     "bootstrap-vue-3": "^0.5.1",
     "portal-vue": "^3.0.0",
     "moment": "^2.30.1",
+    "papaparse": "^5.5.2",
     "vue": "^3.5.13",
     "vue-router": "^4.5.0"
   },
diff --git a/src/App.vue b/src/App.vue
index 393118e..5078771 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -15,6 +15,12 @@
               <li class="nav-item">
                 <router-link class="nav-link" to="/readings">Readings</router-link>
               </li>
+              <li class="import-item">
+                <router-link class="nav-link" to="/customers/importCustomer">Import Customer</router-link>
+              </li>
+              <li class="import-item">
+                <router-link class="nav-link" to="/customers/importReading">Import Reading</router-link>
+              </li>
             </ul>
           </div>
         </div>
diff --git a/src/components/CustomerDetail.vue b/src/components/CustomerDetail.vue
index b5c7319..ca6d237 100644
--- a/src/components/CustomerDetail.vue
+++ b/src/components/CustomerDetail.vue
@@ -22,7 +22,6 @@
 
 <script>
 import customerService from "@/api/customerService";
-import moment from "moment";
 
 export default {
   props: ["id"],
diff --git a/src/components/ImportCustomer.vue b/src/components/ImportCustomer.vue
new file mode 100644
index 0000000..fd47297
--- /dev/null
+++ b/src/components/ImportCustomer.vue
@@ -0,0 +1,158 @@
+<template>
+  <div class="customers">
+    <h2>Import Customers</h2>
+    <input type="file" @change="handleFile" accept=".csv, .xml, .json" />
+    <button :disabled="!parsedData.length" @click="saveToDatabase" style="margin-left: 10px;">Save to Database</button>
+    <div v-if="parsedData.length">
+      <h3>Results:</h3>
+      <table>
+        <thead>
+        <tr>
+          <th v-for="(key, index) in Object.keys(parsedData[0])" :key="index">{{ key }}</th>
+        </tr>
+        </thead>
+        <tbody>
+        <tr v-for="(row, rowIndex) in parsedData" :key="rowIndex">
+          <td v-for="(value, key) in row" :key="key">{{ value }}</td>
+        </tr>
+        </tbody>
+      </table>
+    </div>
+  </div>
+</template>
+
+<script>
+import Papa from 'papaparse';
+import axios from 'axios';
+
+const url = 'http://127.0.0.1:8080/rest/customers/import';
+
+export default {
+  data() {
+    return {
+      parsedData: [],
+    };
+  },
+  methods: {
+    handleFile(event) {
+      const file = event.target.files[0];
+      if (!file) return;
+
+      const fileExtension = file.name.split('.').pop().toLowerCase();
+
+      if (fileExtension === 'csv') {
+        Papa.parse(file, {
+          header: true,
+          dynamicTyping: true,
+          complete: (results) => {
+            this.parsedData = results.data;
+            console.log('Parsed CSV:', results.data);
+          },
+          error: (error) => {
+            console.error('CSV Parse Error:', error.message);
+          }
+        });
+      } else if (fileExtension === 'xml') {
+        const reader = new FileReader();
+        reader.onload = () => {
+          const parser = new DOMParser();
+          const xmlDoc = parser.parseFromString(reader.result, 'text/xml');
+          const items = xmlDoc.getElementsByTagName('item');
+
+          const parsed = [];
+          for (let i = 0; i < items.length; i++) {
+            const itemNode = items[i];
+            const customer = {};
+
+            for (let j = 0; j < itemNode.children.length; j++) {
+              const child = itemNode.children[j];
+              customer[child.nodeName] = child.textContent;
+            }
+
+            parsed.push(customer);
+          }
+
+          this.parsedData = parsed;
+          console.log('Parsed XML:', parsed);
+        };
+        reader.readAsText(file);
+      } else if (fileExtension === 'json') {
+        const reader = new FileReader();
+        reader.onload = () => {
+          try {
+            const jsonData = JSON.parse(reader.result);
+            if (Array.isArray(jsonData)) {
+              this.parsedData = jsonData;
+            } else if (jsonData && typeof jsonData === 'object') {
+              // Handle case where JSON is an object with an array property
+              const possibleArrays = Object.values(jsonData).filter(val => Array.isArray(val));
+              if (possibleArrays.length > 0) {
+                this.parsedData = possibleArrays[0];
+              } else {
+                // If it's just a single object, wrap it in an array
+                this.parsedData = [jsonData];
+              }
+            }
+            console.log('Parsed JSON:', this.parsedData);
+          } catch (error) {
+            console.error('JSON Parse Error:', error.message);
+            alert(`JSON Parse Error: ${error.message}`);
+          }
+        };
+        reader.readAsText(file);
+      } else {
+        alert('Unsupported file type. Please upload a CSV, XML, or JSON file.');
+      }
+    },
+
+
+    async saveToDatabase() {
+      try {
+        const response = await axios.post(url, {
+          customers: this.parsedData
+        });
+        console.log('Saved to Database:', response.data);
+        alert('Data successfully saved!');
+      } catch (error) {
+        console.error('Error saving to database:', error);
+        console.log(JSON.stringify({customers: this.parsedData}));
+        alert('Failed to save data.');
+      }
+    }
+  }
+};
+</script>
+
+
+<style scoped>
+table {
+  border-collapse: collapse;
+  width: 100%;
+  margin-top: 15px;
+  margin-bottom: 15px;
+}
+th, td {
+  border: 1px solid #ddd;
+  padding: 8px;
+  text-align: left;
+}
+th {
+  background-color: #f4f4f4;
+}
+button:disabled {
+  background-color: #ccc;
+  cursor: not-allowed;
+}
+button {
+  padding: 8px 16px;
+  margin-top: 10px;
+  background-color: #4CAF50;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+}
+button:hover:not(:disabled) {
+  background-color: #45a049;
+}
+</style>
\ No newline at end of file
diff --git a/src/components/ImportReading.vue b/src/components/ImportReading.vue
new file mode 100644
index 0000000..483b028
--- /dev/null
+++ b/src/components/ImportReading.vue
@@ -0,0 +1,301 @@
+<template>
+  <div class="readings">
+    <h2>Import Reading</h2>
+    <input type="file" @change="handleFile" accept=".csv, .xml, .json" />
+    <button :disabled="!parsedData.length" @click="saveToDatabase" style="margin-left: 10px;">Save to Database</button>
+    <div v-if="parsedData.length">
+      <h3>Results:</h3>
+      <table>
+        <thead>
+        <tr>
+          <th v-for="(key, index) in Object.keys(parsedData[0])" :key="index">{{ key }}</th>
+        </tr>
+        </thead>
+        <tbody>
+        <tr v-for="(row, rowIndex) in parsedData" :key="rowIndex">
+          <td v-for="(value, key) in row" :key="key">{{ value }}</td>
+        </tr>
+        </tbody>
+      </table>
+    </div>
+    <div v-if="error" class="error-message">
+      {{ error }}
+    </div>
+  </div>
+</template>
+
+<script>
+import Papa from 'papaparse';
+import axios from 'axios';
+import customerService from "@/api/customerService.js";
+
+const url = 'http://127.0.0.1:8080/rest/readings/import';
+
+export default {
+  data() {
+    return {
+      parsedData: [],
+      readings: [],
+      error: null,
+      fileType: null // Track the file type
+    };
+  },
+  methods: {
+    isValidDate(dateString) {
+      if (!dateString) return false;
+      if (dateString === "UNBEKANNT") return false;
+
+      // Versuche das Datum zu parsen
+      const date = new Date(dateString);
+      return !isNaN(date.getTime());
+    },
+
+    handleFile(event) {
+      this.error = null;
+      const file = event.target.files[0];
+      if (!file) return;
+      const fileExtension = file.name.split('.').pop().toLowerCase();
+      this.fileType = fileExtension; // Store the file type
+
+      if (fileExtension === 'csv') {
+        Papa.parse(file, {
+          header: true,
+          dynamicTyping: true,
+          complete: (results) => {
+            if (results.data && results.data.length) {
+              // Filter out empty rows
+              this.parsedData = results.data.filter(row => Object.values(row).some(val => val !== null && val !== ""));
+              console.log('Parsed CSV:', this.parsedData);
+              this.fetchCustomerForReading();
+            } else {
+              this.error = "No valid data found in CSV file";
+            }
+          },
+          error: (error) => {
+            this.error = `CSV Parse Error: ${error.message}`;
+            console.error('CSV Parse Error:', error.message);
+          }
+        });
+      } else if (fileExtension === 'xml') {
+        const reader = new FileReader();
+        reader.onload = () => {
+          try {
+            const parser = new DOMParser();
+            const xmlDoc = parser.parseFromString(reader.result, 'text/xml');
+
+            // Check for parsing errors
+            const parserError = xmlDoc.querySelector('parsererror');
+            if (parserError) {
+              this.error = "XML Parse Error: Invalid XML format";
+              return;
+            }
+
+            const items = xmlDoc.getElementsByTagName('item');
+            if (items.length === 0) {
+              this.error = "No 'item' elements found in XML";
+              return;
+            }
+
+            const parsed = [];
+            for (let i = 0; i < items.length; i++) {
+              const itemNode = items[i];
+              const reading = {};
+
+              for (let j = 0; j < itemNode.children.length; j++) {
+                const child = itemNode.children[j];
+                reading[child.nodeName] = child.textContent;
+              }
+
+              parsed.push(reading);
+            }
+
+            this.parsedData = parsed;
+            console.log('Parsed XML:', parsed);
+            this.fetchCustomerForReading();
+          } catch (error) {
+            this.error = `XML Parse Error: ${error.message}`;
+            console.error('XML Parse Error:', error);
+          }
+        };
+        reader.readAsText(file);
+      } else if (fileExtension === 'json') {
+        const reader = new FileReader();
+        reader.onload = () => {
+          try {
+            const jsonData = JSON.parse(reader.result);
+            if (Array.isArray(jsonData)) {
+              this.parsedData = jsonData;
+            } else if (jsonData && typeof jsonData === 'object') {
+              // Handle case where JSON is an object with an array property
+              const possibleArrays = Object.values(jsonData).filter(val => Array.isArray(val));
+              if (possibleArrays.length > 0) {
+                this.parsedData = possibleArrays[0];
+              } else {
+                // If it's just a single object, wrap it in an array
+                this.parsedData = [jsonData];
+              }
+            }
+            console.log('Parsed JSON:', this.parsedData);
+            // For JSON files, skip fetchCustomerForReading and directly prepare readings
+            this.prepareReadingsFromJson();
+          } catch (error) {
+            this.error = `JSON Parse Error: ${error.message}`;
+            console.error('JSON Parse Error:', error);
+          }
+        };
+        reader.readAsText(file);
+      } else {
+        this.error = 'Unsupported file type. Please upload a CSV, XML, or JSON file.';
+      }
+    },
+
+    async fetchCustomerForReading() {
+      if (!this.parsedData || this.parsedData.length === 0) {
+        this.error = "No data available to process";
+        return;
+      }
+
+      try {
+        this.readings = await Promise.all(
+            this.parsedData.map(async (reading) => {
+              let customer = null;
+              if (reading.customerId) {
+                try {
+                  const response = await customerService.getCustomer(reading.customerId);
+                  customer = response.data.customer;
+                } catch (customerError) {
+                  console.warn(`Failed to fetch customer with ID ${reading.customerId}:`, customerError);
+                }
+              }
+
+              // Direkt das Reading-Objekt zurückgeben ohne zusätzlichen Wrapper
+              return {
+                customer,
+                // Datum-Validierung - nur gültige Daten senden oder null verwenden
+                dateOfReading: this.isValidDate(reading.dateOfReading) ? reading.dateOfReading : null,
+                meterId: reading.meterId || '',
+                substitute: reading.substitute === true || reading.substitute === 'true' || false,
+                meterCount: Number(reading.meterCount) || 0,
+                kindOfMeter: reading.kindOfMeter || ''
+              };
+            })
+        );
+        console.log('Prepared readings for saving:', this.readings);
+      } catch (error) {
+        this.error = `Error fetching customer data: ${error.message}`;
+        console.error('Error fetching customer:', error);
+      }
+    },
+
+    // New method to handle JSON files where customers are already included
+    prepareReadingsFromJson() {
+      if (!this.parsedData || this.parsedData.length === 0) {
+        this.error = "No data available to process";
+        return;
+      }
+
+      try {
+        // Assume JSON already has the correct structure with customer data
+        this.readings = this.parsedData.map(reading => {
+          return {
+            // Use the existing customer data if available
+            customer: reading.customer || null,
+            dateOfReading: this.isValidDate(reading.dateOfReading) ? reading.dateOfReading : null,
+            meterId: reading.meterId || '',
+            substitute: reading.substitute === true || reading.substitute === 'true' || false,
+            meterCount: Number(reading.meterCount) || 0,
+            kindOfMeter: reading.kindOfMeter || ''
+          };
+        });
+        console.log('Prepared JSON readings for saving:', this.readings);
+      } catch (error) {
+        this.error = `Error preparing JSON data: ${error.message}`;
+        console.error('Error preparing JSON data:', error);
+      }
+    },
+
+    async saveToDatabase() {
+      if (!this.readings || this.readings.length === 0) {
+        this.error = "No readings to save";
+        return;
+      }
+
+      try {
+        // Überprüfe alle Readings auf gültige Datumswerte vor dem Absenden
+        const validReadings = this.readings.map(reading => {
+          // Kopiere das Reading-Objekt
+          const validReading = {...reading};
+
+          // Stelle sicher, dass dateOfReading ein gültiges Datum ist oder null
+          if (!this.isValidDate(validReading.dateOfReading)) {
+            validReading.dateOfReading = null;
+          }
+
+          // Stelle sicher, dass meterId ein String ist
+          if (typeof validReading.meterId !== 'string') {
+            validReading.meterId = String(validReading.meterId);
+          }
+
+          return validReading;
+        });
+
+        // Direkt das readings-Array senden ohne zusätzliches Wrapping
+        const payload = { readings: validReadings };
+        console.log('Sending payload:', payload);
+
+        const response = await axios.post(url, payload);
+        console.log('Saved to Database:', response.data);
+        alert('Data successfully saved!');
+
+        // Reset after successful save
+        this.parsedData = [];
+        this.readings = [];
+      } catch (error) {
+        this.error = `Failed to save data: ${error.response?.data?.message || error.message}`;
+        console.error('Error saving to database:', error);
+        console.log('Failed payload:', JSON.stringify({ readings: this.readings }));
+      }
+    }
+  }
+};
+</script>
+
+<style scoped>
+table {
+  border-collapse: collapse;
+  width: 100%;
+  margin-top: 15px;
+  margin-bottom: 15px;
+}
+th, td {
+  border: 1px solid #ddd;
+  padding: 8px;
+  text-align: left;
+}
+th {
+  background-color: #f4f4f4;
+}
+button:disabled {
+  background-color: #ccc;
+  cursor: not-allowed;
+}
+button {
+  padding: 8px 16px;
+  margin-top: 10px;
+  background-color: #4CAF50;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+}
+button:hover:not(:disabled) {
+  background-color: #45a049;
+}
+.error-message {
+  color: #d32f2f;
+  margin-top: 10px;
+  padding: 10px;
+  background-color: #ffebee;
+  border-radius: 4px;
+}
+</style>
\ No newline at end of file
diff --git a/src/router/index.js b/src/router/index.js
index 00adb61..32483dc 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -5,6 +5,8 @@ import CustomerDetail from "@/components/CustomerDetail.vue";
 import ReadingList from "@/components/ReadingList.vue";
 import ReadingForm from "@/components/ReadingForm.vue";
 import ReadingDetail from "@/components/ReadingDetail.vue";
+import ImportCustomer from "@/components/ImportCustomer.vue";
+import ImportReading from "@/components/ImportReading.vue";
 
 const routes = [
     { path: "/", component: CustomerList },
@@ -16,6 +18,8 @@ const routes = [
     { path: "/readings/new", component: ReadingForm },
     { path: "/readings/:id", component: ReadingDetail, props: true },
     { path: "/readings/edit/:id", component: ReadingForm, props: true },
+    { path: "/customers/importCustomer", component: ImportCustomer},
+    { path: "/customers/importReading", component: ImportReading}
 ];
 
 const router = createRouter({


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adds new file import functionality to handle JSON, XML, and CSV formats for both reading and customer data.  
- Added new import components and related route entries to support file uploads and database save operations.  
- Updated navigation links in the App component and added the papaparse dependency in package.json.  
- Removed an unused moment library import from CustomerDetail.

### Reviewed Changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File                         | Description                                                |
| ---------------------------- | ---------------------------------------------------------- |
| src/router/index.js          | New routes and imports for the import pages added.         |
| src/components/importReadings.vue | New reading import component; potential duplicate exists.     |
| src/components/ImportReading.vue    | Fully featured component for importing readings with parsing and save functionality.  |
| src/components/ImportCustomer.vue     | New customer import component for file upload and saving data.   |
| src/components/CustomerDetail.vue     | Removed unused moment import.                                   |
| src/App.vue                        | Added navigation links for the new import pages.             |
| package.json                         | Added papaparse dependency for CSV file parsing.           |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**src/router/index.js:21**
* [nitpick] Consider using kebab-case in route paths (e.g., "/customers/import-customer") for consistency with common URL conventions.
```
{ path: "/customers/importCustomer", component: ImportCustomer},
```
**src/components/ImportReading.vue:45**
* [nitpick] Standardize the language used in error messages so that all messages are consistent (e.g., use English or German uniformly).
```
console.error('Fehler beim Parsen (Reading):', error.message);
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-14T06:51:05Z
user: login: David-Zorn
body: passt
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-14T10:23:49Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adds import capabilities for both customers and readings via CSV, XML, and JSON formats, integrates new routes and navigation links, and updates dependencies.

- Introduces `ImportCustomer.vue` and `ImportReading.vue` components with parsing and save-to-database logic.
- Registers corresponding routes in the router and adds navigation items in `App.vue`.
- Adds `papaparse` dependency and removes the unused `moment` import.

### Reviewed Changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

<details>
<summary>Show a summary per file</summary>

| File                               | Description                                                        |
|------------------------------------|--------------------------------------------------------------------|
| src/components/ImportReading.vue   | New component for parsing and importing readings                   |
| src/components/ImportCustomer.vue  | New component for parsing and importing customers                  |
| src/router/index.js                | Added imports and routes for the new import components             |
| src/App.vue                        | Added navigation links for the import pages                        |
| package.json                       | Added `papaparse` dependency                                       |
| src/components/CustomerDetail.vue  | Removed unused `moment` import                                     |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**src/components/ImportReading.vue:1**
* This new import functionality lacks automated tests; consider adding unit tests for parsing logic and the save-to-database flow.
```
<template>
```
**src/components/ImportCustomer.vue:1**
* No tests cover the customer import component; adding tests would help ensure parsing and error handling work as expected.
```
<template>
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-14T10:26:19Z

Review comments:
diff_hunk: @@ -0,0 +1,52 @@
+<template>
path: src/components/importReadings.vue
user: login: Copilot
body: [nitpick] There appear to be two components for reading import (this file and ImportReading.vue). Clarify their intended roles or remove the redundant component to avoid confusion.
created_at: 2025-05-14T06:51:04Z
author_association: NONE
diff_hunk: @@ -16,6 +18,8 @@ const routes = [
     { path: "/readings/new", component: ReadingForm },
     { path: "/readings/:id", component: ReadingDetail, props: true },
     { path: "/readings/edit/:id", component: ReadingForm, props: true },
+    { path: "/customers/importCustomer", component: ImportCustomer},
+    { path: "/customers/importReading", component: ImportReading}
path: src/router/index.js
user: login: Copilot
body: [nitpick] Route path uses camelCase; consider using kebab-case (e.g. `/customers/import-customer`) for consistency and readability.
```suggestion
    { path: "/customers/import-customer", component: ImportCustomer},
    { path: "/customers/import-reading", component: ImportReading}
```
created_at: 2025-05-14T10:26:18Z
author_association: NONE
diff_hunk: @@ -0,0 +1,301 @@
+<template>
+  <div class="readings">
+    <h2>Import Reading</h2>
+    <input type="file" @change="handleFile" accept=".csv, .xml, .json" />
+    <button :disabled="!parsedData.length" @click="saveToDatabase" style="margin-left: 10px;">Save to Database</button>
+    <div v-if="parsedData.length">
+      <h3>Results:</h3>
+      <table>
+        <thead>
+        <tr>
+          <th v-for="(key, index) in Object.keys(parsedData[0])" :key="index">{{ key }}</th>
+        </tr>
+        </thead>
+        <tbody>
+        <tr v-for="(row, rowIndex) in parsedData" :key="rowIndex">
+          <td v-for="(value, key) in row" :key="key">{{ value }}</td>
+        </tr>
+        </tbody>
+      </table>
+    </div>
+    <div v-if="error" class="error-message">
+      {{ error }}
+    </div>
+  </div>
+</template>
+
+<script>
+import Papa from 'papaparse';
+import axios from 'axios';
+import customerService from "@/api/customerService.js";
+
+const url = 'http://127.0.0.1:8080/rest/readings/import';
path: src/components/ImportReading.vue
user: login: Copilot
body: Hardcoded backend URL; consider using an environment variable or centralized configuration for flexibility across environments.
created_at: 2025-05-14T10:26:18Z
author_association: NONE
diff_hunk: @@ -0,0 +1,301 @@
+<template>
+  <div class="readings">
+    <h2>Import Reading</h2>
+    <input type="file" @change="handleFile" accept=".csv, .xml, .json" />
+    <button :disabled="!parsedData.length" @click="saveToDatabase" style="margin-left: 10px;">Save to Database</button>
+    <div v-if="parsedData.length">
+      <h3>Results:</h3>
+      <table>
+        <thead>
+        <tr>
+          <th v-for="(key, index) in Object.keys(parsedData[0])" :key="index">{{ key }}</th>
+        </tr>
+        </thead>
+        <tbody>
+        <tr v-for="(row, rowIndex) in parsedData" :key="rowIndex">
+          <td v-for="(value, key) in row" :key="key">{{ value }}</td>
+        </tr>
+        </tbody>
+      </table>
+    </div>
+    <div v-if="error" class="error-message">
+      {{ error }}
+    </div>
+  </div>
+</template>
+
+<script>
+import Papa from 'papaparse';
+import axios from 'axios';
+import customerService from "@/api/customerService.js";
+
+const url = 'http://127.0.0.1:8080/rest/readings/import';
+
+export default {
+  data() {
+    return {
+      parsedData: [],
+      readings: [],
+      error: null,
+      fileType: null // Track the file type
+    };
+  },
+  methods: {
+    isValidDate(dateString) {
+      if (!dateString) return false;
+      if (dateString === "UNBEKANNT") return false;
+
+      // Versuche das Datum zu parsen
+      const date = new Date(dateString);
+      return !isNaN(date.getTime());
+    },
+
+    handleFile(event) {
+      this.error = null;
+      const file = event.target.files[0];
+      if (!file) return;
+      const fileExtension = file.name.split('.').pop().toLowerCase();
+      this.fileType = fileExtension; // Store the file type
+
+      if (fileExtension === 'csv') {
+        Papa.parse(file, {
+          header: true,
+          dynamicTyping: true,
+          complete: (results) => {
+            if (results.data && results.data.length) {
+              // Filter out empty rows
+              this.parsedData = results.data.filter(row => Object.values(row).some(val => val !== null && val !== ""));
+              console.log('Parsed CSV:', this.parsedData);
path: src/components/ImportReading.vue
user: login: Copilot
body: [nitpick] Multiple `console.log` statements remain; consider removing them or replacing with a logging utility to avoid cluttering the production console.
created_at: 2025-05-14T10:26:18Z
author_association: NONE
diff_hunk: @@ -0,0 +1,301 @@
+<template>
+  <div class="readings">
+    <h2>Import Reading</h2>
+    <input type="file" @change="handleFile" accept=".csv, .xml, .json" />
+    <button :disabled="!parsedData.length" @click="saveToDatabase" style="margin-left: 10px;">Save to Database</button>
+    <div v-if="parsedData.length">
+      <h3>Results:</h3>
+      <table>
+        <thead>
+        <tr>
+          <th v-for="(key, index) in Object.keys(parsedData[0])" :key="index">{{ key }}</th>
+        </tr>
+        </thead>
+        <tbody>
+        <tr v-for="(row, rowIndex) in parsedData" :key="rowIndex">
+          <td v-for="(value, key) in row" :key="key">{{ value }}</td>
+        </tr>
+        </tbody>
+      </table>
+    </div>
+    <div v-if="error" class="error-message">
+      {{ error }}
+    </div>
+  </div>
+</template>
+
+<script>
+import Papa from 'papaparse';
+import axios from 'axios';
+import customerService from "@/api/customerService.js";
+
+const url = 'http://127.0.0.1:8080/rest/readings/import';
+
+export default {
+  data() {
+    return {
+      parsedData: [],
+      readings: [],
+      error: null,
+      fileType: null // Track the file type
path: src/components/ImportReading.vue
user: login: Copilot
body: The `fileType` data property is assigned but never used elsewhere; consider removing it to simplify the component state.
```suggestion
      error: null
```
created_at: 2025-05-14T10:26:19Z
author_association: NONE
diff_hunk: @@ -0,0 +1,301 @@
+<template>
+  <div class="readings">
+    <h2>Import Reading</h2>
+    <input type="file" @change="handleFile" accept=".csv, .xml, .json" />
+    <button :disabled="!parsedData.length" @click="saveToDatabase" style="margin-left: 10px;">Save to Database</button>
+    <div v-if="parsedData.length">
+      <h3>Results:</h3>
+      <table>
+        <thead>
+        <tr>
+          <th v-for="(key, index) in Object.keys(parsedData[0])" :key="index">{{ key }}</th>
+        </tr>
+        </thead>
+        <tbody>
+        <tr v-for="(row, rowIndex) in parsedData" :key="rowIndex">
+          <td v-for="(value, key) in row" :key="key">{{ value }}</td>
+        </tr>
+        </tbody>
+      </table>
+    </div>
+    <div v-if="error" class="error-message">
+      {{ error }}
+    </div>
+  </div>
+</template>
+
+<script>
+import Papa from 'papaparse';
+import axios from 'axios';
+import customerService from "@/api/customerService.js";
+
+const url = 'http://127.0.0.1:8080/rest/readings/import';
+
+export default {
+  data() {
+    return {
+      parsedData: [],
+      readings: [],
+      error: null,
+      fileType: null // Track the file type
+    };
+  },
+  methods: {
+    isValidDate(dateString) {
+      if (!dateString) return false;
+      if (dateString === "UNBEKANNT") return false;
+
+      // Versuche das Datum zu parsen
+      const date = new Date(dateString);
+      return !isNaN(date.getTime());
+    },
+
+    handleFile(event) {
+      this.error = null;
+      const file = event.target.files[0];
+      if (!file) return;
+      const fileExtension = file.name.split('.').pop().toLowerCase();
+      this.fileType = fileExtension; // Store the file type
+
+      if (fileExtension === 'csv') {
+        Papa.parse(file, {
+          header: true,
+          dynamicTyping: true,
+          complete: (results) => {
+            if (results.data && results.data.length) {
+              // Filter out empty rows
+              this.parsedData = results.data.filter(row => Object.values(row).some(val => val !== null && val !== ""));
+              console.log('Parsed CSV:', this.parsedData);
+              this.fetchCustomerForReading();
+            } else {
+              this.error = "No valid data found in CSV file";
+            }
+          },
+          error: (error) => {
+            this.error = `CSV Parse Error: ${error.message}`;
+            console.error('CSV Parse Error:', error.message);
+          }
+        });
+      } else if (fileExtension === 'xml') {
+        const reader = new FileReader();
+        reader.onload = () => {
+          try {
+            const parser = new DOMParser();
+            const xmlDoc = parser.parseFromString(reader.result, 'text/xml');
+
+            // Check for parsing errors
+            const parserError = xmlDoc.querySelector('parsererror');
+            if (parserError) {
+              this.error = "XML Parse Error: Invalid XML format";
+              return;
+            }
+
+            const items = xmlDoc.getElementsByTagName('item');
+            if (items.length === 0) {
+              this.error = "No 'item' elements found in XML";
+              return;
+            }
+
+            const parsed = [];
+            for (let i = 0; i < items.length; i++) {
+              const itemNode = items[i];
+              const reading = {};
+
+              for (let j = 0; j < itemNode.children.length; j++) {
+                const child = itemNode.children[j];
+                reading[child.nodeName] = child.textContent;
+              }
+
+              parsed.push(reading);
+            }
+
+            this.parsedData = parsed;
+            console.log('Parsed XML:', parsed);
+            this.fetchCustomerForReading();
+          } catch (error) {
+            this.error = `XML Parse Error: ${error.message}`;
+            console.error('XML Parse Error:', error);
+          }
+        };
+        reader.readAsText(file);
+      } else if (fileExtension === 'json') {
+        const reader = new FileReader();
+        reader.onload = () => {
+          try {
+            const jsonData = JSON.parse(reader.result);
+            if (Array.isArray(jsonData)) {
+              this.parsedData = jsonData;
+            } else if (jsonData && typeof jsonData === 'object') {
+              // Handle case where JSON is an object with an array property
+              const possibleArrays = Object.values(jsonData).filter(val => Array.isArray(val));
+              if (possibleArrays.length > 0) {
+                this.parsedData = possibleArrays[0];
+              } else {
+                // If it's just a single object, wrap it in an array
+                this.parsedData = [jsonData];
+              }
+            }
+            console.log('Parsed JSON:', this.parsedData);
+            // For JSON files, skip fetchCustomerForReading and directly prepare readings
+            this.prepareReadingsFromJson();
+          } catch (error) {
+            this.error = `JSON Parse Error: ${error.message}`;
+            console.error('JSON Parse Error:', error);
+          }
+        };
+        reader.readAsText(file);
+      } else {
+        this.error = 'Unsupported file type. Please upload a CSV, XML, or JSON file.';
+      }
+    },
+
+    async fetchCustomerForReading() {
+      if (!this.parsedData || this.parsedData.length === 0) {
+        this.error = "No data available to process";
+        return;
+      }
+
+      try {
+        this.readings = await Promise.all(
+            this.parsedData.map(async (reading) => {
+              let customer = null;
+              if (reading.customerId) {
+                try {
+                  const response = await customerService.getCustomer(reading.customerId);
+                  customer = response.data.customer;
+                } catch (customerError) {
+                  console.warn(`Failed to fetch customer with ID ${reading.customerId}:`, customerError);
+                }
+              }
+
+              // Direkt das Reading-Objekt zurückgeben ohne zusätzlichen Wrapper
+              return {
+                customer,
+                // Datum-Validierung - nur gültige Daten senden oder null verwenden
+                dateOfReading: this.isValidDate(reading.dateOfReading) ? reading.dateOfReading : null,
+                meterId: reading.meterId || '',
+                substitute: reading.substitute === true || reading.substitute === 'true' || false,
+                meterCount: Number(reading.meterCount) || 0,
+                kindOfMeter: reading.kindOfMeter || ''
+              };
+            })
+        );
+        console.log('Prepared readings for saving:', this.readings);
+      } catch (error) {
+        this.error = `Error fetching customer data: ${error.message}`;
+        console.error('Error fetching customer:', error);
+      }
+    },
+
+    // New method to handle JSON files where customers are already included
+    prepareReadingsFromJson() {
+      if (!this.parsedData || this.parsedData.length === 0) {
+        this.error = "No data available to process";
+        return;
+      }
+
+      try {
+        // Assume JSON already has the correct structure with customer data
+        this.readings = this.parsedData.map(reading => {
+          return {
+            // Use the existing customer data if available
+            customer: reading.customer || null,
+            dateOfReading: this.isValidDate(reading.dateOfReading) ? reading.dateOfReading : null,
+            meterId: reading.meterId || '',
+            substitute: reading.substitute === true || reading.substitute === 'true' || false,
+            meterCount: Number(reading.meterCount) || 0,
+            kindOfMeter: reading.kindOfMeter || ''
+          };
+        });
+        console.log('Prepared JSON readings for saving:', this.readings);
+      } catch (error) {
+        this.error = `Error preparing JSON data: ${error.message}`;
+        console.error('Error preparing JSON data:', error);
+      }
+    },
+
+    async saveToDatabase() {
+      if (!this.readings || this.readings.length === 0) {
+        this.error = "No readings to save";
+        return;
+      }
+
+      try {
+        // Überprüfe alle Readings auf gültige Datumswerte vor dem Absenden
+        const validReadings = this.readings.map(reading => {
+          // Kopiere das Reading-Objekt
+          const validReading = {...reading};
+
+          // Stelle sicher, dass dateOfReading ein gültiges Datum ist oder null
+          if (!this.isValidDate(validReading.dateOfReading)) {
+            validReading.dateOfReading = null;
+          }
+
+          // Stelle sicher, dass meterId ein String ist
+          if (typeof validReading.meterId !== 'string') {
+            validReading.meterId = String(validReading.meterId);
+          }
+
+          return validReading;
+        });
+
+        // Direkt das readings-Array senden ohne zusätzliches Wrapping
+        const payload = { readings: validReadings };
+        console.log('Sending payload:', payload);
+
+        const response = await axios.post(url, payload);
+        console.log('Saved to Database:', response.data);
+        alert('Data successfully saved!');
path: src/components/ImportReading.vue
user: login: Copilot
body: [nitpick] Using `alert` blocks the UI; consider using a non-blocking notification or toast component for a better user experience.
created_at: 2025-05-14T10:26:19Z
author_association: NONE
diff_hunk: @@ -0,0 +1,158 @@
+<template>
+  <div class="customers">
+    <h2>Import Customers</h2>
+    <input type="file" @change="handleFile" accept=".csv, .xml, .json" />
+    <button :disabled="!parsedData.length" @click="saveToDatabase" style="margin-left: 10px;">Save to Database</button>
+    <div v-if="parsedData.length">
+      <h3>Results:</h3>
+      <table>
+        <thead>
+        <tr>
+          <th v-for="(key, index) in Object.keys(parsedData[0])" :key="index">{{ key }}</th>
+        </tr>
+        </thead>
+        <tbody>
+        <tr v-for="(row, rowIndex) in parsedData" :key="rowIndex">
+          <td v-for="(value, key) in row" :key="key">{{ value }}</td>
+        </tr>
+        </tbody>
+      </table>
+    </div>
+  </div>
+</template>
+
+<script>
+import Papa from 'papaparse';
+import axios from 'axios';
+
+const url = 'http://127.0.0.1:8080/rest/customers/import';
+
+export default {
+  data() {
+    return {
+      parsedData: [],
+    };
+  },
+  methods: {
+    handleFile(event) {
+      const file = event.target.files[0];
+      if (!file) return;
+
+      const fileExtension = file.name.split('.').pop().toLowerCase();
+
+      if (fileExtension === 'csv') {
+        Papa.parse(file, {
+          header: true,
+          dynamicTyping: true,
+          complete: (results) => {
+            this.parsedData = results.data;
+            console.log('Parsed CSV:', results.data);
+          },
+          error: (error) => {
+            console.error('CSV Parse Error:', error.message);
path: src/components/ImportCustomer.vue
user: login: Copilot
body: On CSV parse errors, the UI only logs to console without notifying the user; consider displaying an error message in the component.
```suggestion
            console.error('CSV Parse Error:', error.message);
            alert(`CSV Parse Error: ${error.message}`);
```
created_at: 2025-05-14T10:26:19Z
author_association: NONE

PR comments:
user: login: David-Zorn
created_at: 2025-05-14T10:14:23Z
body: uuid,comment,customerId,dateOfReading,kindOfMeter,meterCount,meterId,substitute
8403f94d-df4e-43c0-8215-dcb963800acc,Hello,591cfba5-82dc-4720-845b-048230116f05,2025-05-12,WASSER,28.0,Kein Plan,false
author_association: COLLABORATOR
user: login: oblassgit
created_at: 2025-05-14T10:15:22Z
body: uuid;comment;customerId;dateOfReading;kindOfMeter;meterCount;meterId;substitute
e95ffd03-5f15-4ea4-9ba4-05fc8a86cf8b;;49ebadf8-b64e-4b7d-8d98-000180558d7d;2000-01-01;UNBEKANNT;0.0;ewoijeooij;true
author_association: OWNER