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
- - - - + min +
+
+ + min +
+
+ + km +
+