diff --git a/seniorendienst-frontend/package.json b/seniorendienst-frontend/package.json
index 724f6ba..ca9d1cd 100644
--- a/seniorendienst-frontend/package.json
+++ b/seniorendienst-frontend/package.json
@@ -11,6 +11,7 @@
"preview": "vite preview"
},
"dependencies": {
+ "csv-parse": "^6.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
diff --git a/seniorendienst-frontend/src/AppStyles.css b/seniorendienst-frontend/src/AppStyles.css
index 624dd42..bf455f3 100644
--- a/seniorendienst-frontend/src/AppStyles.css
+++ b/seniorendienst-frontend/src/AppStyles.css
@@ -257,6 +257,16 @@ body {
font-size: 0.9em;
}
+.request-cell {
+ white-space: nowrap;
+ min-width: 110px;
+}
+
+.appointment-cell {
+ min-width: 110px;
+ white-space: nowrap;
+}
+
.client-name {
font-weight: 600;
color: #007bff;
@@ -267,6 +277,11 @@ body {
color: #28a745;
}
+.payment-cell.included-in-sum {
+ color: #0066cc;
+ font-weight: 700;
+}
+
.actions-cell {
text-align: center;
}
@@ -292,14 +307,58 @@ body {
}
/* Style für die Gesamtsumme oberhalb der Liste */
+.sum-container {
+ display: flex;
+ align-items: center;
+ gap: 20px;
+ margin-bottom: 20px;
+ flex-wrap: wrap;
+}
+
+.sum-filter {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ background: #f8f9fa;
+ padding: 10px 15px;
+ border-radius: 8px;
+ border: 1px solid #dee2e6;
+}
+
+.sum-filter label {
+ font-weight: 600;
+ color: #495057;
+ white-space: nowrap;
+}
+
+.date-filter-input {
+ padding: 6px 10px;
+ border: 1px solid #ced4da;
+ border-radius: 4px;
+ font-size: 0.95em;
+}
+
+.clear-filter-button {
+ background: #dc3545;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 4px 8px;
+ cursor: pointer;
+ font-size: 0.9em;
+ transition: background-color 0.2s;
+}
+
+.clear-filter-button:hover {
+ background: #c82333;
+}
+
.sum-display {
color: #28a745;
background-color: #e2f0d9;
padding: 10px 15px;
border-radius: 8px;
font-size: 1.1em;
- display: inline-block;
- margin-bottom: 20px;
}
/* Tab Navigation */
@@ -421,6 +480,31 @@ body {
margin-bottom: 15px;
}
+.input-with-unit {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex: 1;
+}
+
+.narrow-input {
+ flex: 1;
+ min-width: 60px;
+}
+
+.unit-label {
+ color: #495057;
+ font-size: 0.9em;
+ font-weight: 500;
+ white-space: nowrap;
+ min-width: 30px;
+}
+
+.transport-select {
+ flex: 1;
+ max-width: none;
+}
+
.input-group-row {
display: flex;
gap: 15px; /* Abstand zwischen den Feldern in einer Reihe */
diff --git a/seniorendienst-frontend/src/ServiceApp.jsx b/seniorendienst-frontend/src/ServiceApp.jsx
index 15c9c23..20a46fb 100644
--- a/seniorendienst-frontend/src/ServiceApp.jsx
+++ b/seniorendienst-frontend/src/ServiceApp.jsx
@@ -9,10 +9,11 @@ import './AppStyles.css'; // Wiederverwendung der Styles
const API_URL = 'http://localhost:3002/api/services';
const normalizeServiceEntry = (entry) => ({
+ ...entry,
id: entry._id,
// Sicherstellen, dass alle Date-Felder Date-Objekte sind
requestDate: entry.requestDate ? new Date(entry.requestDate) : null,
- appointmentDate: entry.appointmentDate ? new Date(entry.appointmentDate) : null, ...entry
+ appointmentDate: entry.appointmentDate ? new Date(entry.appointmentDate) : null,
});
function ServiceApp() {
@@ -21,22 +22,44 @@ function ServiceApp() {
const [isModalOpen, setIsModalOpen] = useState(false);
const [entryToDeleteId, setEntryToDeleteId] = useState(null);
const [activeTab, setActiveTab] = useState('liste');
+ const [startDate, setStartDate] = useState(() => {
+ return localStorage.getItem('sumStartDate') || '';
+ });
- // Berechnet die laufende Summe aller bezahlten Beträge
+ // Startdatum im localStorage speichern, wenn es sich ändert
+ useEffect(() => {
+ if (startDate) {
+ localStorage.setItem('sumStartDate', startDate);
+ } else {
+ localStorage.removeItem('sumStartDate');
+ }
+ }, [startDate]);
+
+ // Berechnet die laufende Summe aller bezahlten Beträge ab dem Startdatum
const cumulativeSum = useMemo(() => {
- return entries.reduce((sum, entry) => sum + entry.paidAmount, 0);
- }, [entries]);
+ return entries
+ .filter(entry => {
+ if (!startDate) return true;
+ const entryDate = entry.appointmentDate;
+ const filterDate = new Date(startDate);
+ return entryDate >= filterDate;
+ })
+ .reduce((sum, entry) => sum + entry.paidAmount, 0);
+ }, [entries, startDate]);
// --- CRUD OPERATIONEN ---
- const fetchEntries = async () => {
- // ... (fetch-Logik) ...
- const response = await fetch(API_URL);
- const data = await response.json();
- setEntries(data.map(normalizeServiceEntry));
- };
-
useEffect(() => {
+ const fetchEntries = async () => {
+ try {
+ const response = await fetch(API_URL);
+ const data = await response.json();
+ setEntries(data.map(normalizeServiceEntry));
+ } catch (error) {
+ console.error('Fehler beim Laden der Einträge:', error);
+ }
+ };
+
fetchEntries();
}, []);
@@ -55,7 +78,7 @@ function ServiceApp() {
const updateEntry = async (updatedEntry) => {
// ... (PUT-Logik, wie in App.js)
const mongoId = updatedEntry.id;
- const response = await fetch(`${API_URL}/${mongoId}`, {
+ await fetch(`${API_URL}/${mongoId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updatedEntry),
@@ -123,7 +146,31 @@ function ServiceApp() {
{activeTab === 'liste' && (
Einträge ({entries.length})
-
Gesamtsumme der Beträge: **{cumulativeSum.toFixed(2)} €**
+
+
+
+
+ setStartDate(e.target.value)}
+ className="date-filter-input"
+ />
+ {startDate && (
+
+ )}
+
+
+ Gesamtsumme der Beträge: {cumulativeSum.toFixed(2)} €
+
+
)}
diff --git a/seniorendienst-frontend/src/components/CSVImport.jsx b/seniorendienst-frontend/src/components/CSVImport.jsx
index 8aabc74..bc2753b 100644
--- a/seniorendienst-frontend/src/components/CSVImport.jsx
+++ b/seniorendienst-frontend/src/components/CSVImport.jsx
@@ -1,5 +1,6 @@
// seniorendienst-frontend/src/components/CSVImport.jsx
import React, { useRef, useState } from 'react';
+import { parse } from 'csv-parse/browser/esm/sync';
const CSVImport = ({ onImport }) => {
const fileInputRef = useRef(null);
@@ -13,16 +14,24 @@ const CSVImport = ({ onImport }) => {
try {
const text = await file.text();
- const lines = text.split('\n').filter(line => line.trim());
- // Erste Zeile als Header überspringen
- const dataLines = lines.slice(1);
+ // Führendes Semikolon aus jeder Zeile entfernen
+ const cleanedText = text
+ .split('\n')
+ .map(line => line.startsWith(';') ? line.substring(1) : line)
+ .join('\n');
- const entries = dataLines.map(line => {
- // CSV-Zeile parsen (einfaches Komma-getrennt)
- const columns = line.split(',').map(col => col.trim());
-
- // Erwartetes Format: Name,Vorname,Straße,PLZ/Ort,Telefon,Email,Anfrage,Termin,Zeit,Fahrtzeit,Strecke,Transport,Dauer,Durchgeführt,Bemerkungen,Bezahlt
+ // CSV mit csv-parse parsen
+ const records = parse(cleanedText, {
+ delimiter: ';',
+ skip_empty_lines: true,
+ from_line: 2, // Erste Zeile (Header) überspringen
+ relax_column_count: true, // Erlaubt unterschiedliche Spaltenanzahlen
+ trim: true,
+ });
+
+ const entries = records.map(columns => {
+ // Erwartetes Format: Name,Vorname,Straße,PLZ/Ort,Telefon,Email,Anfrage,Termin,Dauer,Fahrtzeit,Strecke,Transport,Bezahlt,Durchgeführt,Bemerkungen
return {
name: columns[0] || '',
firstName: columns[1] || '',
@@ -31,14 +40,14 @@ const CSVImport = ({ onImport }) => {
phone: columns[4] || '',
email: columns[5] || '',
requestDate: columns[6] ? new Date(columns[6]) : new Date(),
- appointmentDate: columns[7] && columns[8] ? new Date(`${columns[7]}T${columns[8]}`) : new Date(),
+ appointmentDate: columns[7] ? new Date(columns[7]) : new Date(),
+ workDuration: Number(columns[8]) || 0,
travelTime: Number(columns[9]) || 0,
distance: Number(columns[10]) || 0,
transport: columns[11] || 'Auto',
- workDuration: Number(columns[12]) || 0,
+ paidAmount: Number(columns[12]) || 0,
taskDone: columns[13] || '',
remarks: columns[14] || '',
- paidAmount: Number(columns[15]) || 0,
};
});
@@ -71,7 +80,7 @@ const CSVImport = ({ onImport }) => {
{isProcessing ? '⏳ Importiere...' : '📁 CSV importieren'}
- Format: Name,Vorname,Straße,PLZ/Ort,Telefon,Email,Anfrage,Termin,Zeit,Fahrtzeit,Strecke,Transport,Dauer,Durchgeführt,Bemerkungen,Bezahlt
+ Format: Name,Vorname,Straße,PLZ/Ort,Telefon,Email,Anfrage,Termin (YYYY-MM-DD HH:MM),Dauer,Fahrtzeit,Strecke,Transport,Bezahlt,Durchgeführt,Bemerkungen
);
diff --git a/seniorendienst-frontend/src/components/ServiceForm.jsx b/seniorendienst-frontend/src/components/ServiceForm.jsx
index 60be15f..997c1bf 100644
--- a/seniorendienst-frontend/src/components/ServiceForm.jsx
+++ b/seniorendienst-frontend/src/components/ServiceForm.jsx
@@ -17,16 +17,18 @@ const formatDateToInput = (date) => {
const ServiceForm = ({ onAddEntry, editingEntry, onUpdateEntry, onCancelEdit, cumulativeSum }) => {
- const initialFormData = {
+ const getInitialFormData = () => ({
name: '', firstName: '', street: '', zipCity: '', phone: '', email: '',
requestDate: formatDateToInput(new Date()),
appointmentDate: formatDateToInput(new Date()), appointmentTime: formatTimeToInput(new Date()),
travelTime: 0, distance: 0, transport: 'Auto', workDuration: 0,
taskDone: '', remarks: '', paidAmount: 0,
- };
- const [formData, setFormData] = useState(initialFormData);
+ });
+
+ const [formData, setFormData] = useState(getInitialFormData);
useEffect(() => {
+ // Formular mit editingEntry-Daten vorausfüllen
if (editingEntry) {
setFormData({
name: editingEntry.name || '',
@@ -47,7 +49,7 @@ const ServiceForm = ({ onAddEntry, editingEntry, onUpdateEntry, onCancelEdit, cu
paidAmount: editingEntry.paidAmount || 0,
});
} else {
- setFormData(initialFormData);
+ setFormData(getInitialFormData());
}
}, [editingEntry]);
@@ -122,10 +124,19 @@ const ServiceForm = ({ onAddEntry, editingEntry, onUpdateEntry, onCancelEdit, cu
-
-
-
-