V 1.1.0: Recht gut ausgebaut jetzt
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"csv-parse": "^6.1.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
},
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
useEffect(() => {
|
||||
const fetchEntries = async () => {
|
||||
// ... (fetch-Logik) ...
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user