commit 3745a8f728877588433f61a39974476378aad4fd Author: rxf Date: Mon Nov 24 14:25:24 2025 +0100 Erste ansprechende Version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c9df07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Dependencies +node_modules/ +package-lock.json +yarn.lock +pnpm-lock.yaml + +# Build outputs +dist/ +build/ +.next/ +out/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +!.vscode/settings.json +.idea/ +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Testing +coverage/ +.nyc_output/ + +# Temporary files +*.tmp +*.temp +.cache/ + +# OS files +Thumbs.db +.AppleDouble +.LSOverride + +# MongoDB +data/ + +# Misc +*.pid +*.seed +*.pid.lock diff --git a/seniorendienst-backend/.dockerignore b/seniorendienst-backend/.dockerignore new file mode 100644 index 0000000..c6cdbfa --- /dev/null +++ b/seniorendienst-backend/.dockerignore @@ -0,0 +1,5 @@ +node_modules +npm-debug.log +.env +.git +.gitignore diff --git a/seniorendienst-backend/.env.docker b/seniorendienst-backend/.env.docker new file mode 100644 index 0000000..250858b --- /dev/null +++ b/seniorendienst-backend/.env.docker @@ -0,0 +1,3 @@ +# Environment variables for Docker Compose +MONGO_ROOT_USER=root +MONGO_ROOT_PASSWD=SFluorit diff --git a/seniorendienst-backend/package.json b/seniorendienst-backend/package.json new file mode 100644 index 0000000..1f9fb3f --- /dev/null +++ b/seniorendienst-backend/package.json @@ -0,0 +1,24 @@ +{ + "name": "seniorendienst-backend", + "type": "module", + "version": "1.0.0", + "vdate": "2025-11-24 13:00 UTC", + "description": "", + "main": "index.js", + "scripts": { + "start": "node src/server.js", + "dev": "nodemon src/server.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^17.2.3", + "express": "^5.1.0", + "mongodb": "^7.0.0" + }, + "devDependencies": { + "nodemon": "^3.1.11" + } +} diff --git a/seniorendienst-backend/src/db/db.js b/seniorendienst-backend/src/db/db.js new file mode 100644 index 0000000..10bd509 --- /dev/null +++ b/seniorendienst-backend/src/db/db.js @@ -0,0 +1,40 @@ +// src/db/db.js +import 'dotenv/config'; // Importiert und lädt Umgebungsvariablen +import { MongoClient } from 'mongodb'; + +// MongoDB Verbindungs-URI aus der .env-Datei +const uri = process.env.MONGO_URI; + +let db; + +/** + * Stellt die Verbindung zur MongoDB her. + * @returns {db} Die verbundene Datenbankinstanz. + */ +export async function connectToDb() { + try { + const client = new MongoClient(uri, { + }); + + await client.connect(); + + // Die Datenbankinstanz speichern + db = client.db(); + + console.log(`✅ Erfolgreich mit MongoDB verbunden.`); + + return db; + + } catch (error) { + console.error("❌ Fehler bei der Verbindung zur MongoDB:", error); + process.exit(1); + } +} + +/** + * Gibt die gespeicherte Datenbankinstanz zurück. + * @returns {db} Die Datenbankinstanz. + */ +export function getDb() { + return db; +} \ No newline at end of file diff --git a/seniorendienst-backend/src/server.js b/seniorendienst-backend/src/server.js new file mode 100644 index 0000000..2b0e3dd --- /dev/null +++ b/seniorendienst-backend/src/server.js @@ -0,0 +1,124 @@ +// seniorendienst-backend/src/server.js +import 'dotenv/config'; +import express from 'express'; +import cors from 'cors'; +import { connectToDb, getDb } from './db/db.js'; +import { ObjectId } from 'mongodb'; + +const app = express(); +const PORT = process.env.PORT || 3002; + +// NEU: Nur Service-API +const SERVICE_API = '/api/services'; +const COLLECTION_NAME = 'serviceentries'; + +// --- Middleware --- +app.use(cors({ + // Stellen Sie sicher, dass dies 5173 oder den Port Ihres neuen Frontends ist! + origin: 'http://localhost:5173', + methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', + credentials: true, +})); +app.use(express.json()); + +// ----------------------------------------------------- +// API ROUTEN FÜR SENIORENDIENST +// ----------------------------------------------------- + +// 1. READ: Alle Einträge abrufen +app.get(SERVICE_API, async (req, res) => { + try { + const db = getDb(); + const entries = await db.collection(COLLECTION_NAME) + .find({}) + .sort({ appointmentDate: 1 }) + .toArray(); + res.json(entries); + } catch (error) { + console.error("Fehler beim Abrufen der Service-Einträge:", error); + res.status(500).json({ message: "Interner Serverfehler." }); + } +}); + +// 2. CREATE: Neuen Eintrag erstellen +app.post(SERVICE_API, async (req, res) => { + try { + const db = getDb(); + const entryData = req.body; + + if (!entryData.name || !entryData.appointmentDate) { + return res.status(400).json({ message: "Name und Termin sind erforderlich." }); + } + + const result = await db.collection(COLLECTION_NAME).insertOne(entryData); + const newEntry = { _id: result.insertedId, ...entryData }; + + res.status(201).json(newEntry); + + } catch (error) { + console.error("Fehler beim Erstellen des Service-Eintrags:", error); + res.status(500).json({ message: "Interner Serverfehler." }); + } +}); + +// 3. UPDATE: Eintrag aktualisieren +app.put(`${SERVICE_API}/:id`, async (req, res) => { + try { + const db = getDb(); + const { id } = req.params; + const updatedFields = req.body; + + const filter = { _id: new ObjectId(id) }; + delete updatedFields._id; + + const result = await db.collection(COLLECTION_NAME).updateOne( + filter, + { $set: updatedFields } + ); + + if (result.matchedCount === 0) { + return res.status(404).json({ message: "Eintrag nicht gefunden." }); + } + + const updatedEntry = await db.collection(COLLECTION_NAME).findOne(filter); + res.json(updatedEntry); + + } catch (error) { + console.error(`Fehler beim Aktualisieren des Eintrags ${req.params.id}:`, error); + res.status(400).json({ message: "Ungültige ID oder Serverfehler." }); + } +}); + +// 4. DELETE: Eintrag löschen +app.delete(`${SERVICE_API}/:id`, async (req, res) => { + try { + const db = getDb(); + const { id } = req.params; + + const filter = { _id: new ObjectId(id) }; + const result = await db.collection(COLLECTION_NAME).deleteOne(filter); + + if (result.deletedCount === 0) { + return res.status(404).json({ message: "Eintrag nicht gefunden." }); + } + + res.status(204).send(); + + } catch (error) { + console.error(`Fehler beim Löschen des Eintrags ${req.params.id}:`, error); + res.status(400).json({ message: "Ungültige ID oder Serverfehler." }); + } +}); + +// --- Server Start Logik (Bleibt unverändert) --- +async function startServer() { + try { + await connectToDb(); + app.listen(PORT, () => { + console.log(`🚀 Service Server läuft auf Port ${PORT}`); + }); + } catch (error) { + console.error("Fehler beim Starten des Servers:", error); + } +} +startServer(); \ No newline at end of file diff --git a/seniorendienst-frontend/.gitignore b/seniorendienst-frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/seniorendienst-frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/seniorendienst-frontend/README.md b/seniorendienst-frontend/README.md new file mode 100644 index 0000000..18bc70e --- /dev/null +++ b/seniorendienst-frontend/README.md @@ -0,0 +1,16 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/seniorendienst-frontend/eslint.config.js b/seniorendienst-frontend/eslint.config.js new file mode 100644 index 0000000..4fa125d --- /dev/null +++ b/seniorendienst-frontend/eslint.config.js @@ -0,0 +1,29 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + }, + }, +]) diff --git a/seniorendienst-frontend/index.html b/seniorendienst-frontend/index.html new file mode 100644 index 0000000..c20fbd3 --- /dev/null +++ b/seniorendienst-frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + frontend + + +
+ + + diff --git a/seniorendienst-frontend/package.json b/seniorendienst-frontend/package.json new file mode 100644 index 0000000..724f6ba --- /dev/null +++ b/seniorendienst-frontend/package.json @@ -0,0 +1,28 @@ +{ + "name": "seniorendienst-frontend", + "private": true, + "version": "1.0.0", + "vdate": "2025-11-24 13:00 UTC", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "vite": "^7.2.4" + } +} diff --git a/seniorendienst-frontend/public/vite.svg b/seniorendienst-frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/seniorendienst-frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/seniorendienst-frontend/src/App.css b/seniorendienst-frontend/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/seniorendienst-frontend/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/seniorendienst-frontend/src/AppStyles.css b/seniorendienst-frontend/src/AppStyles.css new file mode 100644 index 0000000..624dd42 --- /dev/null +++ b/seniorendienst-frontend/src/AppStyles.css @@ -0,0 +1,501 @@ +/* Globale Styles für App.js */ +body { + margin: 0; + background-color: #f9f9f9; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + color: #333; + /* NEU: Flexbox für die Zentrierung */ + display: flex; + justify-content: center; /* Zentriert horizontal */ + align-items: flex-start; /* Hält den Inhalt oben, falls er kürzer als der Bildschirm ist */ + min-height: 100vh; /* Stellt sicher, dass die Seite mindestens die volle Höhe des Viewports hat */ +} + +.app-container { + /* Die maximale Breite beibehalten, aber Padding anpassen */ + padding: 40px 20px; + max-width: 900px; + width: 100%; /* Wichtig, damit max-width funktioniert */ + /* min-height-Einstellung ist nun im Body */ +} + +.app-header { + text-align: center; + margin-bottom: 40px; + border-bottom: 2px solid #007bff; + padding-bottom: 10px; +} + +.app-header h1 { + color: #007bff; + margin-top: 0; +} + +.separator { + border: 0; + border-top: 1px solid #ddd; + margin: 40px 0; +} + +.debug-output { + background-color: #fff; + padding: 15px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); +} + +.debug-output pre { + font-size: 12px; + white-space: pre-wrap; + word-break: break-all; +} + +/* Styles für AppointmentForm.jsx */ +.form-container { + background-color: #fff; + padding: 30px; + border-radius: 10px; + box-shadow: 0 4px 12px rgba(0,0,0,0.1); +} + +.form-container h3 { + border-bottom: 2px solid #f0f0f0; + padding-bottom: 10px; + margin-bottom: 20px; + color: #333; +} + +.input-group { + margin-bottom: 15px; +} + +.input-group label { + display: block; + margin-bottom: 5px; + font-weight: bold; + color: #555; +} + +.input-group input, +.input-group textarea { + width: 100%; + padding: 10px; + border-radius: 5px; + border: 1px solid #ccc; + box-sizing: border-box; /* Stellt sicher, dass Padding und Border zur Gesamtbreite gehören */ +} + +.input-group textarea { + resize: vertical; +} + +.date-time-control { + display: flex; + gap: 15px; +} + +.date-time-control > div { + flex: 1; +} + +.submit-button { + background-color: #28a745; + color: white; + padding: 12px 20px; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 16px; + margin-top: 20px; + transition: background-color 0.3s; +} + +.submit-button:hover { + background-color: #218838; +} + +/* Styles für AppointmentList und AppointmentItem */ +.appointment-list { + list-style: none; + padding: 0; +} + +.appointment-item { + background-color: #fff; + padding: 15px; + margin-bottom: 10px; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0,0,0,0.05); + border-left: 5px solid #007bff; /* Offene Termine */ + transition: all 0.3s; +} + +.appointment-item.done { + opacity: 0.6; + border-left: 5px solid #28a745; /* Erledigte Termine */ + background-color: #e9f5ee; +} + +.appointment-header { + display: flex; + align-items: center; + margin-bottom: 10px; +} + +.appointment-header input[type="checkbox"] { + width: 20px; + height: 20px; + margin-right: 15px; + cursor: pointer; +} + +.doctor-info { + font-size: 1.1em; + color: #007bff; + font-weight: 600; +} + +.appointment-item.done .doctor-info, +.appointment-item.done .appointment-date, +.appointment-item.done .appointment-notes { + text-decoration: line-through; +} + +.arzt-art { + font-weight: normal; + font-size: 0.9em; + color: #6c757d; + margin-left: 5px; +} + +.appointment-date { + margin: 5px 0; + margin-left: 35px; /* Alignment mit der Checkbox */ + font-size: 0.9em; + color: #495057; +} + +.appointment-notes { + margin: 5px 0 0 35px; + padding-top: 5px; + border-top: 1px dashed #eee; + font-size: 0.85em; + color: #6c757d; +} + +.no-appointments-message { + text-align: center; + padding: 20px; + background-color: #fffae6; + border: 1px solid #ffeeba; + border-radius: 5px; + color: #856404; +} + +.cancel-button { + background-color: #dc3545; + color: white; + padding: 12px 20px; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 16px; + margin-top: 20px; + margin-left: 10px; + transition: background-color 0.3s; +} + +.cancel-button:hover { + background-color: #c82333; +} + +/* In AppStyles.css */ + +/* Service Tabelle */ +.service-table-container { + overflow-x: auto; + margin-bottom: 30px; +} + +.service-table { + width: 100%; + border-collapse: collapse; + background: #ffffff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + border-radius: 8px; + overflow: hidden; +} + +.service-table thead { + background-color: #007bff; + color: white; +} + +.service-table th { + padding: 12px 15px; + text-align: left; + font-weight: 600; + font-size: 0.95em; +} + +.service-table tbody tr { + border-bottom: 1px solid #e0e0e0; + cursor: pointer; + transition: background-color 0.2s; +} + +.service-table tbody tr:hover { + background-color: #f5f8fa; +} + +.service-table tbody tr:last-child { + border-bottom: none; +} + +.service-table td { + padding: 12px 15px; + font-size: 0.9em; +} + +.client-name { + font-weight: 600; + color: #007bff; +} + +.payment-cell { + font-weight: bold; + color: #28a745; +} + +.actions-cell { + text-align: center; +} + +.edit-button, +.delete-button { + background: none; + border: none; + cursor: pointer; + font-size: 1.2em; + padding: 5px 10px; + transition: transform 0.2s; + margin: 0 2px; +} + +.edit-button:hover, +.delete-button:hover { + transform: scale(1.2); +} + +.service-row { + cursor: default; +} + +/* Style für die Gesamtsumme oberhalb der Liste */ +.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 */ +.tab-navigation { + display: flex; + gap: 0; + margin-bottom: 0; + border-bottom: 2px solid #e0e0e0; +} + +.tab-button { + background: #f8f9fa; + border: none; + border-bottom: 3px solid transparent; + padding: 15px 30px; + font-size: 1em; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; + color: #6c757d; +} + +.tab-button:hover { + background: #e9ecef; + color: #007bff; +} + +.tab-button.active { + background: #ffffff; + color: #007bff; + border-bottom-color: #007bff; +} + +.tab-content { + padding: 30px 20px; +} + +.tab-panel { + animation: fadeIn 0.3s; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +/* CSV Import Styles */ +.csv-import-container { + background: #f8f9fa; + border: 2px dashed #007bff; + border-radius: 8px; + padding: 20px; + margin-bottom: 30px; + text-align: center; +} + +.csv-import-button { + display: inline-block; + background-color: #007bff; + color: white; + padding: 12px 24px; + border-radius: 6px; + cursor: pointer; + font-size: 1em; + font-weight: 600; + transition: background-color 0.3s, transform 0.2s; +} + +.csv-import-button:hover { + background-color: #0056b3; + transform: translateY(-2px); +} + +.csv-hint { + display: block; + margin-top: 10px; + color: #6c757d; + font-size: 0.85em; +} + +/* --- Formular Container und Layout --- */ + +.form-container { + background: #f7f7f7; + padding: 30px; + border-radius: 12px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); + margin-bottom: 30px; +} + +.form-title { + color: #007bff; + margin-top: 0; + margin-bottom: 20px; + border-bottom: 2px solid #e9ecef; + padding-bottom: 10px; +} + +/* --- Sektionen --- */ +.form-section { + background: #ffffff; + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 15px; + margin-bottom: 20px; +} + +.form-section h4 { + color: #495057; + margin-top: 0; + margin-bottom: 15px; + font-size: 1.1em; +} + +/* --- Input-Gruppen und Layout --- */ + +.input-group, +.input-group-row { + margin-bottom: 15px; +} + +.input-group-row { + display: flex; + gap: 15px; /* Abstand zwischen den Feldern in einer Reihe */ +} + +/* Anpassung der Label-Positionierung bei horizontalen Gruppen */ +.input-group-row label { + align-self: center; /* Zentriert Label vertikal */ +} + +.input-group-row input, +.input-group-row select { + flex-grow: 1; /* Lässt Felder den verfügbaren Platz gleichmäßig einnehmen */ + min-width: 0; +} + + +/* --- Allgemeine Input-Felder --- */ + +.form-container input[type="text"], +.form-container input[type="number"], +.form-container input[type="tel"], +.form-container input[type="email"], +.form-container input[type="date"], +.form-container input[type="time"], +.form-container select, +.form-container textarea { + width: 100%; /* Wichtig für die volle Breite im Input-Group-Container */ + padding: 10px 12px; + border: 1px solid #ced4da; + border-radius: 6px; + font-size: 1em; + box-sizing: border-box; /* Padding und Border werden in die Breite eingerechnet */ + transition: border-color 0.2s, box-shadow 0.2s; + background-color: #fff; +} + +.form-container input:focus, +.form-container select:focus, +.form-container textarea:focus { + border-color: #007bff; + box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25); + outline: none; +} + +.form-container textarea { + resize: vertical; +} + +/* --- Buttons --- */ +.submit-button, +.cancel-button { + padding: 12px 25px; + border: none; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s, transform 0.1s; + margin-right: 10px; +} + +.submit-button { + background-color: #28a745; /* Grün für positive Aktion */ + color: white; +} + +.submit-button:hover { + background-color: #218838; +} + +.cancel-button { + background-color: #6c757d; /* Grau für Abbrechen */ + color: white; +} + +.cancel-button:hover { + background-color: #5a6268; +} \ No newline at end of file diff --git a/seniorendienst-frontend/src/ServiceApp.jsx b/seniorendienst-frontend/src/ServiceApp.jsx new file mode 100644 index 0000000..15c9c23 --- /dev/null +++ b/seniorendienst-frontend/src/ServiceApp.jsx @@ -0,0 +1,170 @@ +// seniorendienst-frontend/src/ServiceApp.js +import React, { useState, useEffect, useMemo } from 'react'; +import ServiceForm from './components/ServiceForm'; +import ServiceList from './components/ServiceList'; +import ConfirmationModal from './components/ConfirmationModal'; +import CSVImport from './components/CSVImport'; +import './AppStyles.css'; // Wiederverwendung der Styles + +const API_URL = 'http://localhost:3002/api/services'; + +const normalizeServiceEntry = (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 +}); + +function ServiceApp() { + const [entries, setEntries] = useState([]); + const [editingEntry, setEditingEntry] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [entryToDeleteId, setEntryToDeleteId] = useState(null); + const [activeTab, setActiveTab] = useState('liste'); + + // Berechnet die laufende Summe aller bezahlten Beträge + const cumulativeSum = useMemo(() => { + return entries.reduce((sum, entry) => sum + entry.paidAmount, 0); + }, [entries]); + + // --- CRUD OPERATIONEN --- + + const fetchEntries = async () => { + // ... (fetch-Logik) ... + const response = await fetch(API_URL); + const data = await response.json(); + setEntries(data.map(normalizeServiceEntry)); + }; + + useEffect(() => { + fetchEntries(); + }, []); + + const addEntry = async (newEntry) => { + // ... (POST-Logik, wie in App.js) + const response = await fetch(API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(newEntry), + }); + const addedEntry = await response.json(); + setEntries(prev => [...prev, normalizeServiceEntry(addedEntry)]); + setEditingEntry(null); + }; + + const updateEntry = async (updatedEntry) => { + // ... (PUT-Logik, wie in App.js) + const mongoId = updatedEntry.id; + const response = await fetch(`${API_URL}/${mongoId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updatedEntry), + }); + setEntries(prev => prev.map(e => e.id === mongoId ? normalizeServiceEntry(updatedEntry) : e)); + setEditingEntry(null); + }; + + // CSV Import Handler + const handleCSVImport = async (entries) => { + for (const entry of entries) { + await addEntry(entry); + } + }; + + // Modal Logik + const openDeleteModal = (id) => { setEntryToDeleteId(id); setIsModalOpen(true); }; + const handleCancelDelete = () => { setEntryToDeleteId(null); setIsModalOpen(false); }; + + const handleConfirmDelete = async () => { + const id = entryToDeleteId; + setIsModalOpen(false); + setEntryToDeleteId(null); + + // ... (DELETE-Logik, wie in App.js) + const response = await fetch(`${API_URL}/${id}`, { method: 'DELETE' }); + if (response.status === 204) { + setEntries(prev => prev.filter(e => e.id !== id)); + if (editingEntry && editingEntry.id === id) setEditingEntry(null); + } else { + console.error("Fehler beim Löschen"); + } + }; + + return ( +
+
+

👵 Seniorendienst Protokoll

+
+ + {/* Tab Navigation */} +
+ + + +
+ + {/* Tab Content */} +
+ {activeTab === 'liste' && ( +
+

Einträge ({entries.length})

+

Gesamtsumme der Beträge: **{cumulativeSum.toFixed(2)} €**

+ + { + setEditingEntry(entry); + setActiveTab('eingabe'); + }} + onDelete={openDeleteModal} + /> +
+ )} + + {activeTab === 'eingabe' && ( +
+ setEditingEntry(null)} + cumulativeSum={cumulativeSum} + /> +
+ )} + + {activeTab === 'extra' && ( +
+

Extras

+ +
+ )} +
+ + +
+ ); +} + +export default ServiceApp; \ No newline at end of file diff --git a/seniorendienst-frontend/src/assets/react.svg b/seniorendienst-frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/seniorendienst-frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/seniorendienst-frontend/src/components/CSVImport.jsx b/seniorendienst-frontend/src/components/CSVImport.jsx new file mode 100644 index 0000000..8aabc74 --- /dev/null +++ b/seniorendienst-frontend/src/components/CSVImport.jsx @@ -0,0 +1,80 @@ +// seniorendienst-frontend/src/components/CSVImport.jsx +import React, { useRef, useState } from 'react'; + +const CSVImport = ({ onImport }) => { + const fileInputRef = useRef(null); + const [isProcessing, setIsProcessing] = useState(false); + + const handleFileUpload = async (event) => { + const file = event.target.files[0]; + if (!file) return; + + setIsProcessing(true); + + 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); + + 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 + return { + name: columns[0] || '', + firstName: columns[1] || '', + street: columns[2] || '', + zipCity: columns[3] || '', + 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(), + travelTime: Number(columns[9]) || 0, + distance: Number(columns[10]) || 0, + transport: columns[11] || 'Auto', + workDuration: Number(columns[12]) || 0, + taskDone: columns[13] || '', + remarks: columns[14] || '', + paidAmount: Number(columns[15]) || 0, + }; + }); + + await onImport(entries); + alert(`${entries.length} Einträge erfolgreich importiert!`); + + // Input zurücksetzen + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + } catch (error) { + console.error('Fehler beim Import:', error); + alert('Fehler beim Importieren der CSV-Datei. Bitte überprüfen Sie das Format.'); + } finally { + setIsProcessing(false); + } + }; + + return ( +
+ + + + Format: Name,Vorname,Straße,PLZ/Ort,Telefon,Email,Anfrage,Termin,Zeit,Fahrtzeit,Strecke,Transport,Dauer,Durchgeführt,Bemerkungen,Bezahlt + +
+ ); +}; + +export default CSVImport; diff --git a/seniorendienst-frontend/src/components/ConfirmationModal.jsx b/seniorendienst-frontend/src/components/ConfirmationModal.jsx new file mode 100644 index 0000000..bee0a42 --- /dev/null +++ b/seniorendienst-frontend/src/components/ConfirmationModal.jsx @@ -0,0 +1,40 @@ +// src/components/ConfirmationModal.jsx +import React from 'react'; +import './ModalStyles.css'; // Wird gleich definiert + +const ConfirmationModal = ({ isOpen, title, message, onConfirm, onCancel }) => { + if (!isOpen) { + return null; + } + + return ( + // Hintergrund-Overlay (schließt das Modal bei Klick außerhalb) +
+
e.stopPropagation()} + > +

{title}

+

{message}

+ +
+ + +
+
+
+ ); +}; + +export default ConfirmationModal; \ No newline at end of file diff --git a/seniorendienst-frontend/src/components/ModalStyles.css b/seniorendienst-frontend/src/components/ModalStyles.css new file mode 100644 index 0000000..a27153e --- /dev/null +++ b/seniorendienst-frontend/src/components/ModalStyles.css @@ -0,0 +1,74 @@ +/* src/components/ModalStyles.css */ + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.6); /* Dunkles, halbtransparentes Overlay */ + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; /* Stellt sicher, dass es über allem liegt */ +} + +.modal-content { + background: white; + padding: 30px; + border-radius: 12px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); + width: 90%; + max-width: 450px; + animation: fadeIn 0.3s ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-20px); } + to { opacity: 1; transform: translateY(0); } +} + +.modal-title { + color: #dc3545; + border-bottom: 2px solid #f0f0f0; + padding-bottom: 10px; + margin-top: 0; +} + +.modal-message { + color: #333; + margin-bottom: 30px; +} + +.modal-actions { + display: flex; + justify-content: flex-end; /* Buttons nach rechts ausrichten */ + gap: 10px; +} + +.modal-button { + padding: 10px 18px; + border: none; + border-radius: 6px; + font-weight: bold; + cursor: pointer; + transition: background-color 0.2s; +} + +.modal-button.cancel { + background-color: #f0f0f0; + color: #333; +} + +.modal-button.cancel:hover { + background-color: #ddd; +} + +.modal-button.confirm { + background-color: #dc3545; /* Rot für Löschaktion */ + color: white; +} + +.modal-button.confirm:hover { + background-color: #c82333; +} \ No newline at end of file diff --git a/seniorendienst-frontend/src/components/ServiceForm.jsx b/seniorendienst-frontend/src/components/ServiceForm.jsx new file mode 100644 index 0000000..60be15f --- /dev/null +++ b/seniorendienst-frontend/src/components/ServiceForm.jsx @@ -0,0 +1,160 @@ +// seniorendienst-frontend/src/components/ServiceForm.jsx +import React, { useState, useEffect } from 'react'; + +const formatTimeToInput = (date) => { + if (!date || !(date instanceof Date) || isNaN(date.getTime())) { + return '00:00'; + } + return date.toTimeString().slice(0, 5); +}; + +const formatDateToInput = (date) => { + if (!date || !(date instanceof Date) || isNaN(date.getTime())) { + return new Date().toISOString().split('T')[0]; + } + return date.toISOString().split('T')[0]; +}; + +const ServiceForm = ({ onAddEntry, editingEntry, onUpdateEntry, onCancelEdit, cumulativeSum }) => { + + const initialFormData = { + 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); + + useEffect(() => { + if (editingEntry) { + setFormData({ + name: editingEntry.name || '', + firstName: editingEntry.firstName || '', + street: editingEntry.street || '', + zipCity: editingEntry.zipCity || '', + phone: editingEntry.phone || '', + email: editingEntry.email || '', + requestDate: formatDateToInput(editingEntry.requestDate), + appointmentDate: formatDateToInput(editingEntry.appointmentDate), + appointmentTime: formatTimeToInput(editingEntry.appointmentDate), + travelTime: editingEntry.travelTime || 0, + distance: editingEntry.distance || 0, + transport: editingEntry.transport || 'Auto', + workDuration: editingEntry.workDuration || 0, + taskDone: editingEntry.taskDone || '', + remarks: editingEntry.remarks || '', + paidAmount: editingEntry.paidAmount || 0, + }); + } else { + setFormData(initialFormData); + } + }, [editingEntry]); + + const handleChange = (e) => { + const { name, value, type } = e.target; + setFormData(prevData => ({ + ...prevData, + [name]: (type === 'number' || name === 'paidAmount' || name === 'travelTime' || name === 'workDuration' || name === 'distance') ? Number(value) : value + })); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + + if (!formData.name || !formData.appointmentDate) { + alert("Name und Termin sind erforderlich."); + return; + } + + const baseEntry = { + ...formData, + // Daten zusammenführen und Date-Objekte erstellen + appointmentDate: new Date(`${formData.appointmentDate}T${formData.appointmentTime}`), + requestDate: new Date(formData.requestDate), + // HIER: Wichtig, die laufende Summe muss korrekt gesendet werden + cumulativeSum: editingEntry + ? cumulativeSum - editingEntry.paidAmount + formData.paidAmount // Bei Edit: Alte Summe abziehen, neue addieren + : cumulativeSum + formData.paidAmount, // Bei Add: Neue Summe addieren + }; + + if (editingEntry) { + onUpdateEntry({ ...baseEntry, id: editingEntry.id }); + } else { + onAddEntry(baseEntry); + } + }; + + return ( +
+

+ {editingEntry ? '✍️ Service-Eintrag bearbeiten' : '📝 Neuen Service-Eintrag erfassen'} +

+ + {/* --- BLOCK 1: Kundeninformationen --- */} +
+

Kunden-Details

+
+ + +
+
+ + +
+
+ + +
+
+ + {/* --- BLOCK 2: Termin & Dauer --- */} +
+

Termin-Details

+
+ + {/* HINWEIS: Die label-Tags wurden hier außerhalb der Input-Gruppe platziert, + weshalb die Styles für input-group-row jetzt besser greifen. */} + + + +
+ +
+ + + + + +
+
+ + {/* --- BLOCK 3: Leistung & Bezahlung --- */} +
+

Leistung & Finanzen

+