V 1.1.0: Recht gut ausgebaut jetzt

This commit is contained in:
rxf
2025-11-24 21:37:17 +01:00
parent 3745a8f728
commit b8bfa9d99f
7 changed files with 219 additions and 43 deletions

View File

@@ -11,6 +11,7 @@
"preview": "vite preview"
},
"dependencies": {
"csv-parse": "^6.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},

View File

@@ -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 */

View File

@@ -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' && (
<div className="tab-panel">
<h2>Einträge ({entries.length})</h2>
<h3 className="sum-display">Gesamtsumme der Beträge: **{cumulativeSum.toFixed(2)} **</h3>
<div className="sum-container">
<div className="sum-filter">
<label htmlFor="startDate">Summe ab:</label>
<input
type="date"
id="startDate"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="date-filter-input"
/>
{startDate && (
<button
onClick={() => setStartDate('')}
className="clear-filter-button"
title="Filter zurücksetzen"
>
</button>
)}
</div>
<div className="sum-display">
Gesamtsumme der Beträge: <strong>{cumulativeSum.toFixed(2)} </strong>
</div>
</div>
<ServiceList
entries={entries}
@@ -132,6 +179,7 @@ function ServiceApp() {
setActiveTab('eingabe');
}}
onDelete={openDeleteModal}
startDate={startDate}
/>
</div>
)}

View File

@@ -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());
// 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,
});
// Erwartetes Format: Name,Vorname,Straße,PLZ/Ort,Telefon,Email,Anfrage,Termin,Zeit,Fahrtzeit,Strecke,Transport,Dauer,Durchgeführt,Bemerkungen,Bezahlt
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'}
</label>
<small className="csv-hint">
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
</small>
</div>
);

View File

@@ -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
<div className="input-group-row">
<label>Dauer/Fahrt</label>
<input type="number" name="workDuration" value={formData.workDuration} onChange={handleChange} placeholder="Arbeitszeit (Min)" required />
<input type="number" name="travelTime" value={formData.travelTime} onChange={handleChange} placeholder="Fahrzeit (Min)" required />
<input type="number" name="distance" value={formData.distance} onChange={handleChange} placeholder="Strecke (km)" step="0.1" />
<select name="transport" value={formData.transport} onChange={handleChange}>
<div className="input-with-unit">
<input type="number" name="workDuration" value={formData.workDuration} onChange={handleChange} placeholder="Arbeitszeit" required className="narrow-input" />
<span className="unit-label">min</span>
</div>
<div className="input-with-unit">
<input type="number" name="travelTime" value={formData.travelTime} onChange={handleChange} placeholder="Fahrzeit" required className="narrow-input" />
<span className="unit-label">min</span>
</div>
<div className="input-with-unit">
<input type="number" name="distance" value={formData.distance} onChange={handleChange} placeholder="Strecke" step="0.1" className="narrow-input" />
<span className="unit-label">km</span>
</div>
<select name="transport" value={formData.transport} onChange={handleChange} className="transport-select">
<option value="Auto">Auto</option>
<option value="ÖPNV">ÖPNV</option>
<option value="Fahrrad">Fahrrad</option>

View File

@@ -3,14 +3,17 @@ import React from 'react';
const formatDateTime = (date) => {
if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
return 'N/A';
return { date: 'N/A', time: '' };
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
return {
date: `${year}-${month}-${day}`,
time: `${hours}:${minutes}`
};
};
const formatDate = (date) => {
@@ -23,12 +26,15 @@ const formatDate = (date) => {
return `${year}-${month}-${day}`;
};
const ServiceItem = ({ entry, onEdit, onDelete }) => {
const ServiceItem = ({ entry, onEdit, onDelete, startDate }) => {
const {
id, name, firstName, street, zipCity,
requestDate, appointmentDate, paidAmount
} = entry;
// Prüfen, ob dieser Eintrag in die Summenberechnung eingeht
const isIncludedInSum = !startDate || (appointmentDate && appointmentDate >= new Date(startDate));
return (
<tr className="service-row">
<td className="request-cell">{formatDate(requestDate)}</td>
@@ -37,8 +43,20 @@ const ServiceItem = ({ entry, onEdit, onDelete }) => {
</td>
<td className="address-cell">{street || '-'}</td>
<td className="address-cell">{zipCity || '-'}</td>
<td className="appointment-cell">{formatDateTime(appointmentDate)}</td>
<td className="payment-cell">{paidAmount.toFixed(2)} </td>
<td className="appointment-cell">
{(() => {
const { date, time } = formatDateTime(appointmentDate);
return (
<>
{date}
{time && <><br />{time}</>}
</>
);
})()}
</td>
<td className={`payment-cell ${isIncludedInSum ? 'included-in-sum' : ''}`}>
{paidAmount ? paidAmount.toFixed(2) : '0.00'}
</td>
<td className="actions-cell">
<button
className="edit-button"

View File

@@ -2,7 +2,7 @@
import React from 'react';
import ServiceItem from './ServiceItem';
const ServiceList = ({ entries, onEdit, onDelete }) => {
const ServiceList = ({ entries, onEdit, onDelete, startDate }) => {
if (entries.length === 0) {
return <p className="no-appointments-message">Noch keine Service-Einträge vorhanden.</p>;
@@ -24,13 +24,18 @@ const ServiceList = ({ entries, onEdit, onDelete }) => {
</thead>
<tbody>
{entries
.sort((a, b) => a.appointmentDate - b.appointmentDate)
.sort((a, b) => {
const dateA = a.requestDate instanceof Date ? a.requestDate : new Date(a.requestDate);
const dateB = b.requestDate instanceof Date ? b.requestDate : new Date(b.requestDate);
return dateB - dateA;
})
.map(entry => (
<ServiceItem
key={entry.id}
entry={entry}
onEdit={onEdit}
onDelete={onDelete}
startDate={startDate}
/>
))}
</tbody>