From 3a6af1caca04d244c2bf61ac58e09a7031e568f7 Mon Sep 17 00:00:00 2001 From: rxf Date: Sun, 23 Nov 2025 12:14:58 +0100 Subject: [PATCH] =?UTF-8?q?V=201.0.0=20(front/backend)=20Erste=20lauff?= =?UTF-8?q?=C3=A4hige=20Version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 32 +++ backend/docker-compose.yml | 18 ++ backend/package.json | 24 +++ backend/src/db/db.js | 40 ++++ backend/src/server.js | 154 ++++++++++++++ frontend/README.md | 16 ++ frontend/eslint.config.js | 29 +++ frontend/index.html | 13 ++ frontend/package.json | 27 +++ frontend/public/vite.svg | 1 + frontend/src/App.css_org | 42 ++++ frontend/src/App.jsx | 161 +++++++++++++++ frontend/src/AppStyles.css | 210 ++++++++++++++++++++ frontend/src/assets/react.svg | 1 + frontend/src/components/AppointmentForm.jsx | 158 +++++++++++++++ frontend/src/components/AppointmentItem.jsx | 46 +++++ frontend/src/components/AppointmentList.jsx | 28 +++ frontend/src/index.css | 68 +++++++ frontend/src/main.jsx | 10 + frontend/vite.config.js | 7 + 20 files changed, 1085 insertions(+) create mode 100644 .gitignore create mode 100644 backend/docker-compose.yml create mode 100644 backend/package.json create mode 100644 backend/src/db/db.js create mode 100644 backend/src/server.js create mode 100644 frontend/README.md create mode 100644 frontend/eslint.config.js create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/public/vite.svg create mode 100644 frontend/src/App.css_org create mode 100644 frontend/src/App.jsx create mode 100644 frontend/src/AppStyles.css create mode 100644 frontend/src/assets/react.svg create mode 100644 frontend/src/components/AppointmentForm.jsx create mode 100644 frontend/src/components/AppointmentItem.jsx create mode 100644 frontend/src/components/AppointmentList.jsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.jsx create mode 100644 frontend/vite.config.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad9e48f --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Dependencies +node_modules/ +package-lock.json + +# Environment variables +.env +.env.local +.env.*.local + +# Build output +dist/ +build/ + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Testing +coverage/ + +# Misc +.cache/ diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..6446c3b --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,18 @@ + +services: + mongodb: + image: mongo:latest + container_name: mongodb + restart: unless-stopped + ports: + - "27017:27017" + environment: + MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USER} + MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWD} + volumes: + - mongodb_data:/data/db + - mongodb_config:/data/configdb + +volumes: + mongodb_data: + mongodb_config: \ No newline at end of file diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..3068f49 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,24 @@ +{ + "name": "backend", + "type": "module", + "version": "1.0.0", + "vdate": "2025-11-23 11: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/backend/src/db/db.js b/backend/src/db/db.js new file mode 100644 index 0000000..10bd509 --- /dev/null +++ b/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/backend/src/server.js b/backend/src/server.js new file mode 100644 index 0000000..3c3f4be --- /dev/null +++ b/backend/src/server.js @@ -0,0 +1,154 @@ +// 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'; // Wichtig für die Arbeit mit MongoDB IDs + +// Initialisiert Express +const app = express(); +const PORT = process.env.PORT || 3001; + +// Middleware konfigurieren - CORS muss vor allen Routen kommen +app.use((req, res, next) => { + console.log(`📥 ${req.method} ${req.path} from ${req.get('origin') || 'unknown origin'}`); + res.header('Access-Control-Allow-Origin', 'http://localhost:5173'); + res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); + res.header('Access-Control-Allow-Credentials', 'true'); + + // Handle preflight requests + if (req.method === 'OPTIONS') { + console.log('✅ Preflight request handled'); + return res.sendStatus(200); + } + next(); +}); +app.use(express.json()); + +// ----------------------------------------------------- +// API ROUTEN +// ----------------------------------------------------- + +// 1. READ: Alle Termine abrufen (GET /api/appointments) +app.get('/api/appointments', async (req, res) => { + try { + const db = getDb(); + // Finde alle Dokumente in der Collection 'appointments' + const appointments = await db.collection('appointments') + .find({}) + // Optional: Nach Termindatum sortieren (aufsteigend) + .sort({ termin: 1 }) + .toArray(); + + // Sende die Daten zurück an das Frontend + res.json(appointments); + + } catch (error) { + console.error("Fehler beim Abrufen der Termine:", error); + res.status(500).json({ message: "Interner Serverfehler beim Abrufen der Daten." }); + } +}); + +// 2. CREATE: Neuen Termin erstellen (POST /api/appointments) +app.post('/api/appointments', async (req, res) => { + try { + const db = getDb(); + const appointmentData = req.body; // Die Daten kommen vom React-Formular + + // Validiere, dass der Termin ein gültiges Datum hat (Optional, aber gut) + if (!appointmentData.arztName || !appointmentData.termin) { + return res.status(400).json({ message: "Arztname und Termin sind erforderlich." }); + } + + // Mongo generiert die _id automatisch. + const result = await db.collection('appointments').insertOne(appointmentData); + + // Der eingefügte Termin, inklusive der von Mongo erstellten _id + const newAppointment = { _id: result.insertedId, ...appointmentData }; + + // Sende den neu erstellten Termin zurück an das Frontend (Status 201 Created) + res.status(201).json(newAppointment); + + } catch (error) { + console.error("Fehler beim Erstellen des Termins:", error); + res.status(500).json({ message: "Interner Serverfehler beim Speichern der Daten." }); + } +}); + +// 3. UPDATE: Termin aktualisieren (PUT /api/appointments/:id) +app.put('/api/appointments/:id', async (req, res) => { + try { + const db = getDb(); + const { id } = req.params; + const updatedFields = req.body; // Enthält alle Felder (oder nur die geänderten) + + // Die _id des Dokuments muss ein gültiges ObjectId-Objekt sein + const filter = { _id: new ObjectId(id) }; + + // Entferne das _id Feld aus den aktualisierten Daten, um Fehler zu vermeiden + delete updatedFields._id; + + // Führe die Aktualisierung durch + const result = await db.collection('appointments').updateOne( + filter, + { $set: updatedFields } + ); + + if (result.matchedCount === 0) { + return res.status(404).json({ message: "Termin nicht gefunden." }); + } + + // Hole das aktualisierte Dokument, um es zurückzusenden (optional, aber nützlich) + const updatedAppointment = await db.collection('appointments').findOne(filter); + + res.json(updatedAppointment); + + } catch (error) { + console.error(`Fehler beim Aktualisieren des Termins ${req.params.id}:`, error); + // Gib einen 400 Bad Request zurück, falls die ID ungültig ist (z.B. falsches Format) + res.status(400).json({ message: "Ungültige ID oder Serverfehler." }); + } +}); + +// 4. DELETE: Termin löschen (DELETE /api/appointments/:id) +app.delete('/api/appointments/:id', async (req, res) => { + try { + const db = getDb(); + const { id } = req.params; + + const filter = { _id: new ObjectId(id) }; + + const result = await db.collection('appointments').deleteOne(filter); + + if (result.deletedCount === 0) { + return res.status(404).json({ message: "Termin nicht gefunden." }); + } + + // Sende Status 204 (No Content), um den erfolgreichen Löschvorgang zu signalisieren + res.status(204).send(); + + } catch (error) { + console.error(`Fehler beim Löschen des Termins ${req.params.id}:`, error); + res.status(400).json({ message: "Ungültige ID oder Serverfehler." }); + } +}); + + +// ----------------------------------------------------- +// Server Start Logik +// ----------------------------------------------------- +async function startServer() { + try { + await connectToDb(); + + app.listen(PORT, () => { + console.log(`🚀 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/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..18bc70e --- /dev/null +++ b/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/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..4fa125d --- /dev/null +++ b/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/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..4687c24 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + aerzte + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..512c340 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,27 @@ +{ + "name": "aerzte", + "type": "module", + "private": true, + "version": "0.0.0", + "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/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.css_org b/frontend/src/App.css_org new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/frontend/src/App.css_org @@ -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/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..98cc0a1 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,161 @@ +import React, { useState, useEffect } from 'react'; +import AppointmentForm from './components/AppointmentForm'; +import AppointmentList from './components/AppointmentList'; +import './AppStyles.css'; + +// Die Basis-URL der Express-API +const API_URL = 'http://localhost:3001/api/appointments'; + +// Funktion zur Umwandlung von MongoDBs _id in die interne id des Frontends +const normalizeAppointment = (appointment) => ({ + // Wichtig: _id von Mongo in id umbenennen + id: appointment._id, + arztName: appointment.arztName, + arztArt: appointment.arztArt, + // Termine kommen als String vom Backend, muss in Date-Objekt umgewandelt werden + termin: new Date(appointment.termin), + erledigt: appointment.erledigt, + bemerkungen: appointment.bemerkungen, +}); + +function App() { + const [appointments, setAppointments] = useState([]); + const [editingAppointment, setEditingAppointment] = useState(null); + + // NEU: Lädt Termine vom Backend beim Start + useEffect(() => { + fetchAppointments(); + }, []); // [] sorgt dafür, dass es nur einmal beim Mounten ausgeführt wird + + const fetchAppointments = async () => { + try { + const response = await fetch(API_URL); + if (!response.ok) throw new Error('Netzwerkantwort war nicht ok.'); + + const data = await response.json(); + + // Daten aus dem Backend aufbereiten (konvertiere _id und termin) + const normalizedData = data.map(normalizeAppointment); + setAppointments(normalizedData); + + } catch (error) { + console.error("Fehler beim Laden der Termine:", error); + // Optional: Fehlermeldung anzeigen + } + }; + + // 1. CREATE: Neuen Termin erstellen + const addAppointment = async (newApp) => { + // Vor dem Senden die temporäre ID und das Date-Objekt entfernen/umwandeln + const appToSend = { + ...newApp, + // Date-Objekt in ISO-String umwandeln (von Mongo erwartet) + termin: newApp.termin.toISOString(), + id: undefined // Lokale ID entfernen + }; + + try { + const response = await fetch(API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(appToSend), + }); + + if (!response.ok) throw new Error('Fehler beim Hinzufügen des Termins.'); + + const addedApp = await response.json(); + + // Fügt den normalisierten Termin (mit Mongo _id) zum State hinzu + setAppointments(prev => [...prev, normalizeAppointment(addedApp)]); + setEditingAppointment(null); + + } catch (error) { + console.error("Fehler beim Speichern des Termins:", error); + } + }; + + + // 2. UPDATE: Bestehenden Termin aktualisieren (vom Formular) + const updateAppointment = async (updatedApp) => { + const mongoId = updatedApp.id; // Die ID ist jetzt die MongoDB _id + + const appToSend = { + ...updatedApp, + termin: updatedApp.termin.toISOString(), + id: undefined, // Lokale ID entfernen + }; + + try { + const response = await fetch(`${API_URL}/${mongoId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(appToSend), + }); + + if (!response.ok) throw new Error('Fehler beim Aktualisieren des Termins.'); + + // Nach erfolgreichem Update den State lokal aktualisieren + setAppointments(prev => + prev.map(app => + app.id === mongoId ? normalizeAppointment(updatedApp) : app + ) + ); + setEditingAppointment(null); + + } catch (error) { + console.error("Fehler beim Aktualisieren des Termins:", error); + } + }; + + // 3. UPDATE: Statuswechsel (Toggle Done) + const toggleDone = async (id) => { + const currentApp = appointments.find(app => app.id === id); + if (!currentApp) return; + + const newStatus = !currentApp.erledigt; + + try { + // Nur das erledigt-Feld senden + const response = await fetch(`${API_URL}/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ erledigt: newStatus }), + }); + + if (!response.ok) throw new Error('Fehler beim Statuswechsel.'); + + // Lokalen State synchronisieren, wenn Backend erfolgreich war + setAppointments(prev => + prev.map(app => + app.id === id ? { ...app, erledigt: newStatus } : app + ) + ); + + } catch (error) { + console.error("Fehler beim Statuswechsel:", error); + } + }; + + // ... (startEdit und cancelEdit bleiben unverändert, sie manipulieren nur den lokalen State) + + return ( +
+ {/* ... (Header und Formular-Einbindung) ... */} + setEditingAppointment(null)} + /> + {/* ... */} + setEditingAppointment(app)} + /> + {/* Debug Output kann nun entfernt werden */} +
+ ); +} + +export default App; \ No newline at end of file diff --git a/frontend/src/AppStyles.css b/frontend/src/AppStyles.css new file mode 100644 index 0000000..0063fb6 --- /dev/null +++ b/frontend/src/AppStyles.css @@ -0,0 +1,210 @@ +/* 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; +} \ No newline at end of file diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/AppointmentForm.jsx b/frontend/src/components/AppointmentForm.jsx new file mode 100644 index 0000000..4a5b358 --- /dev/null +++ b/frontend/src/components/AppointmentForm.jsx @@ -0,0 +1,158 @@ +import React, { useState, useEffect } from 'react'; + +// Hilfsfunktion: Format-Konvertierung vom Date-Objekt zurück ins Formular-Format +const formatTimeToInput = (date) => date.toTimeString().slice(0, 5); // HH:MM +const formatDateToInput = (date) => date.toISOString().split('T')[0]; // YYYY-MM-DD + +const AppointmentForm = ({ onAddAppointment, editingAppointment, onUpdateAppointment, onCancelEdit }) => { + + const initialFormData = { + arztName: '', + arztArt: '', + terminDatum: '', + terminUhrzeit: '', + bemerkungen: '', + }; + const [formData, setFormData] = useState(initialFormData); + + // Füllt das Formular, wenn editingAppointment gesetzt wird + useEffect(() => { + if (editingAppointment) { + setFormData({ + arztName: editingAppointment.arztName, + arztArt: editingAppointment.arztArt, + terminDatum: formatDateToInput(editingAppointment.termin), + terminUhrzeit: formatTimeToInput(editingAppointment.termin), + bemerkungen: editingAppointment.bemerkungen, + }); + } else { + // Leert das Formular, wenn Bearbeitung abgeschlossen/abgebrochen + setFormData(initialFormData); + } + }, [editingAppointment]); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData(prevData => ({ + ...prevData, + [name]: value + })); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + + if (!formData.arztName || !formData.terminDatum || !formData.terminUhrzeit) { + alert("Bitte füllen Sie Name, Datum und Uhrzeit aus."); + return; + } + + const baseAppointment = { + ...formData, + // Datum und Uhrzeit zusammenführen + termin: new Date(`${formData.terminDatum}T${formData.terminUhrzeit}`), + erledigt: editingAppointment ? editingAppointment.erledigt : false + }; + + if (editingAppointment) { + // UPDATE: Füge die Mongo-ID hinzu und rufe die Update-Funktion auf + onUpdateAppointment({ + ...baseAppointment, + id: editingAppointment.id + }); + } else { + // ADD: Ruft die Add-Funktion auf (keine temporäre ID mehr erforderlich) + onAddAppointment(baseAppointment); + } +}; + + return ( +
+

+ {editingAppointment ? '✍️ Termin bearbeiten' : '🗓️ Neuen Termin hinzufügen'} +

+ + {/* Name des Arztes */} +
+ + +
+ + {/* Art des Arztes */} +
+ + +
+ + {/* Datum und Uhrzeit */} +
+ +
+
+ +
+
+ +
+
+
+ + {/* Bemerkungen */} +
+ +