diff --git a/.gitignore b/.gitignore index 22e6c72..94e648e 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,7 @@ received_screenshots/ mosquitto/ alte/ screenshots/ +media/ dashboard/manitine_test.py dashboard/pages/test.py .gitignore diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 3373fd3..9999521 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -11,6 +11,7 @@ "@syncfusion/ej2-react-buttons": "^30.1.37", "@syncfusion/ej2-react-calendars": "^30.1.37", "@syncfusion/ej2-react-dropdowns": "^30.1.37", + "@syncfusion/ej2-react-filemanager": "^30.1.38", "@syncfusion/ej2-react-grids": "^30.1.37", "@syncfusion/ej2-react-inputs": "^30.1.38", "@syncfusion/ej2-react-kanban": "^30.1.37", @@ -1025,12 +1026,12 @@ } }, "node_modules/@syncfusion/ej2-data": { - "version": "30.1.37", - "resolved": "https://registry.npmjs.org/@syncfusion/ej2-data/-/ej2-data-30.1.37.tgz", - "integrity": "sha512-UGbF1a95BmdRcgkgekMOdJY0kcNSaOrKWLlnZxFkEl9SSvN+mxPa7ktgXHlEWRc+KY/WeNSV2kmyZ8h/htnAXQ==", + "version": "30.1.38", + "resolved": "https://registry.npmjs.org/@syncfusion/ej2-data/-/ej2-data-30.1.38.tgz", + "integrity": "sha512-BdqvjLzzK4OuUR1YlzPSG3SmeGg1mrLz/6ih5oD9dSpRXDoMG24bpO1rwCK7mjy8Dp9IJ8mliyCbPfoDycxM9Q==", "license": "SEE LICENSE IN license", "dependencies": { - "@syncfusion/ej2-base": "~30.1.37" + "@syncfusion/ej2-base": "~30.1.38" } }, "node_modules/@syncfusion/ej2-dropdowns": { @@ -1064,6 +1065,47 @@ "integrity": "sha512-1r0rWrXEsXsRJhG0yOs8Hp2nGARLsBCD4u602R4IxLDrockIPVJv4gz0rIw4dDtFW7bpAa8J+yWXtr6fW29Bhw==", "license": "SEE LICENSE IN license" }, + "node_modules/@syncfusion/ej2-filemanager": { + "version": "30.1.38", + "resolved": "https://registry.npmjs.org/@syncfusion/ej2-filemanager/-/ej2-filemanager-30.1.38.tgz", + "integrity": "sha512-QuXnn7V1/Ipit+zl1+3WJsnXytqX0VHVSJ5vzhdpE87nENdDktcOmifzZPpPd19ozh7rif2t2aP41KivUVF14A==", + "license": "SEE LICENSE IN license", + "dependencies": { + "@syncfusion/ej2-base": "~30.1.38", + "@syncfusion/ej2-buttons": "~30.1.37", + "@syncfusion/ej2-data": "~30.1.38", + "@syncfusion/ej2-grids": "~30.1.38", + "@syncfusion/ej2-inputs": "~30.1.38", + "@syncfusion/ej2-layouts": "~30.1.37", + "@syncfusion/ej2-lists": "~30.1.37", + "@syncfusion/ej2-navigations": "~30.1.37", + "@syncfusion/ej2-popups": "~30.1.37", + "@syncfusion/ej2-splitbuttons": "~30.1.37" + } + }, + "node_modules/@syncfusion/ej2-filemanager/node_modules/@syncfusion/ej2-grids": { + "version": "30.1.38", + "resolved": "https://registry.npmjs.org/@syncfusion/ej2-grids/-/ej2-grids-30.1.38.tgz", + "integrity": "sha512-0ULWC/P8AsYco3fhUfrkppEdU+IMzrIWyoP057/yp0Mktq9UI5mgvQ12ruZbEMQXl0vK5S5DKaWMDJU2vBTDWQ==", + "license": "SEE LICENSE IN license", + "dependencies": { + "@syncfusion/ej2-base": "~30.1.38", + "@syncfusion/ej2-buttons": "~30.1.37", + "@syncfusion/ej2-calendars": "~30.1.37", + "@syncfusion/ej2-compression": "~30.1.37", + "@syncfusion/ej2-data": "~30.1.38", + "@syncfusion/ej2-dropdowns": "~30.1.37", + "@syncfusion/ej2-excel-export": "~30.1.37", + "@syncfusion/ej2-file-utils": "~30.1.37", + "@syncfusion/ej2-inputs": "~30.1.38", + "@syncfusion/ej2-lists": "~30.1.37", + "@syncfusion/ej2-navigations": "~30.1.37", + "@syncfusion/ej2-notifications": "~30.1.37", + "@syncfusion/ej2-pdf-export": "~30.1.38", + "@syncfusion/ej2-popups": "~30.1.37", + "@syncfusion/ej2-splitbuttons": "~30.1.37" + } + }, "node_modules/@syncfusion/ej2-grids": { "version": "30.1.37", "resolved": "https://registry.npmjs.org/@syncfusion/ej2-grids/-/ej2-grids-30.1.37.tgz", @@ -1169,9 +1211,9 @@ } }, "node_modules/@syncfusion/ej2-pdf-export": { - "version": "30.1.37", - "resolved": "https://registry.npmjs.org/@syncfusion/ej2-pdf-export/-/ej2-pdf-export-30.1.37.tgz", - "integrity": "sha512-4vtuyp+IhB8FUdxnizwytYFYP7Uxdr0thFCEuT3p7T7v4HzwEIk1uingaO32Z4AD72LUNuzznnRTZNyd59VU7w==", + "version": "30.1.38", + "resolved": "https://registry.npmjs.org/@syncfusion/ej2-pdf-export/-/ej2-pdf-export-30.1.38.tgz", + "integrity": "sha512-iWgy/40rgKGweC3CJozn3MxcwKRg4OVoS7bnpkF+aDWFlJu4qV2bjZawacwASCitca+0jSs5aT7KOc24cmqJ4w==", "license": "SEE LICENSE IN license", "dependencies": { "@syncfusion/ej2-compression": "~30.1.37" @@ -1229,6 +1271,17 @@ "@syncfusion/ej2-react-base": "~30.1.37" } }, + "node_modules/@syncfusion/ej2-react-filemanager": { + "version": "30.1.38", + "resolved": "https://registry.npmjs.org/@syncfusion/ej2-react-filemanager/-/ej2-react-filemanager-30.1.38.tgz", + "integrity": "sha512-xwq7J24oC7zoiuSQpBe1NWvdK/4zIFSZOXGfTapUdjdczcolC6d+s3AgGmJZLvR1rqdNCJE99m5DO0LiRnJz3A==", + "license": "SEE LICENSE IN license", + "dependencies": { + "@syncfusion/ej2-base": "~30.1.38", + "@syncfusion/ej2-filemanager": "30.1.38", + "@syncfusion/ej2-react-base": "~30.1.37" + } + }, "node_modules/@syncfusion/ej2-react-grids": { "version": "30.1.37", "resolved": "https://registry.npmjs.org/@syncfusion/ej2-react-grids/-/ej2-react-grids-30.1.37.tgz", diff --git a/dashboard/package.json b/dashboard/package.json index bf2eff1..69a02f3 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -13,6 +13,7 @@ "@syncfusion/ej2-react-buttons": "^30.1.37", "@syncfusion/ej2-react-calendars": "^30.1.37", "@syncfusion/ej2-react-dropdowns": "^30.1.37", + "@syncfusion/ej2-react-filemanager": "^30.1.38", "@syncfusion/ej2-react-grids": "^30.1.37", "@syncfusion/ej2-react-inputs": "^30.1.38", "@syncfusion/ej2-react-kanban": "^30.1.37", diff --git a/dashboard/src/App.css b/dashboard/src/App.css index 0ae9be5..9b57835 100644 --- a/dashboard/src/App.css +++ b/dashboard/src/App.css @@ -1,3 +1,4 @@ +@import "../node_modules/@syncfusion/ej2-react-filemanager/styles/material.css"; @import "../node_modules/@syncfusion/ej2-base/styles/material.css"; @import "../node_modules/@syncfusion/ej2-buttons/styles/material.css"; @import "../node_modules/@syncfusion/ej2-calendars/styles/material.css"; diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index f527813..f34ba82 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -129,7 +129,7 @@ const App: React.FC = () => ( } /> } /> } /> - } /> + } /> } /> } /> @@ -146,6 +146,6 @@ import Appointments from './appointments'; import Ressourcen from './ressourcen'; import Infoscreens from './clients'; import Infoscreen_groups from './infoscreen_groups'; -import Medien from './medien'; +import Media from './media'; import Benutzer from './benutzer'; import Einstellungen from './einstellungen'; diff --git a/dashboard/src/components/CustomMediaInfoPanel.tsx b/dashboard/src/components/CustomMediaInfoPanel.tsx new file mode 100644 index 0000000..5f5ac0e --- /dev/null +++ b/dashboard/src/components/CustomMediaInfoPanel.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +interface CustomMediaInfoPanelProps { + mediaId: string; + title: string; + description: string; + eventId?: string; + onSave: (data: { title: string; description: string; eventId?: string }) => void; +} + +const CustomMediaInfoPanel: React.FC = ({ + mediaId, + title, + description, + eventId, + onSave, +}) => { + // Hier kannst du Formularfelder und Logik für die Bearbeitung einbauen + return ( +
+

Medien-Informationen bearbeiten

+ {/* Formularfelder für Titel, Beschreibung, Event-Zuordnung */} +
+ ); +}; + +export default CustomMediaInfoPanel; diff --git a/dashboard/src/media.tsx b/dashboard/src/media.tsx new file mode 100644 index 0000000..bccddbc --- /dev/null +++ b/dashboard/src/media.tsx @@ -0,0 +1,100 @@ +import React, { useEffect, useState } from 'react'; +import CustomMediaInfoPanel from './components/CustomMediaInfoPanel'; +import { + FileManagerComponent, + Inject, + NavigationPane, + DetailsView, + Toolbar, +} from '@syncfusion/ej2-react-filemanager'; + +const hostUrl = '/api/eventmedia/filemanager/'; // Dein Backend-Endpunkt für FileManager + +interface MediaItem { + id: string; + file_path: string; + url: string; + description: string; + eventId?: string; +} + +const Media: React.FC = () => { + const [mediaList, setMediaList] = useState([]); + const [selectedMedia, setSelectedMedia] = useState(null); + + // Medien vom Server laden + useEffect(() => { + fetch('/api/eventmedia') + .then(res => res.json()) + .then(setMediaList); + }, []); + + // Upload-Handler (vereinfachtes Beispiel) + const handleUpload = async (e: React.ChangeEvent) => { + if (!e.target.files?.length) return; + const formData = new FormData(); + formData.append('file', e.target.files[0]); + await fetch('/api/eventmedia/upload', { method: 'POST', body: formData }); + // Nach Upload neu laden + const res = await fetch('/api/eventmedia'); + setMediaList(await res.json()); + }; + + // Speichern von Metadaten/Event-Zuordnung + const handleSave = async (data: { title: string; description: string; eventId?: string }) => { + if (!selectedMedia) return; + await fetch(`/api/eventmedia/${selectedMedia.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + // Nach dem Speichern neu laden + const res = await fetch('/api/eventmedia'); + setMediaList(await res.json()); + }; + + return ( +
+

Medien

+ + + + {selectedMedia && ( + + )} +
+ ); +}; + +export default Media; diff --git a/dashboard/src/medien.tsx b/dashboard/src/medien.tsx deleted file mode 100644 index 15d96a4..0000000 --- a/dashboard/src/medien.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; -const Medien: React.FC = () => ( -
-

Medien

-

Willkommen im Infoscreen-Management Medien.

-
-); -export default Medien; diff --git a/server/alembic/versions/a0f3f9325e05_update_media_type_enum_for_event_media.py b/server/alembic/versions/a0f3f9325e05_update_media_type_enum_for_event_media.py new file mode 100644 index 0000000..715e3f7 --- /dev/null +++ b/server/alembic/versions/a0f3f9325e05_update_media_type_enum_for_event_media.py @@ -0,0 +1,34 @@ +"""Update media_type enum for event_media + +Revision ID: a0f3f9325e05 +Revises: bb29b5524f5c +Create Date: 2025-07-05 07:49:37.696162 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'a0f3f9325e05' +down_revision: Union[str, None] = 'bb29b5524f5c' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade(): + op.execute(""" + ALTER TABLE event_media MODIFY COLUMN media_type ENUM( + 'pdf','ppt','pptx','odp', + 'mp4','avi','mkv','mov','wmv','flv','webm','mpg','mpeg','ogv', + 'jpg','jpeg','png','gif','bmp','tiff','svg', + 'html' + ) NOT NULL; + """) + + +def downgrade() -> None: + """Downgrade schema.""" + pass diff --git a/server/models.py b/server/models.py index f246e39..9b84aab 100644 --- a/server/models.py +++ b/server/models.py @@ -116,7 +116,16 @@ class Event(Base): class EventMedia(Base): __tablename__ = 'event_media' id = Column(Integer, primary_key=True, autoincrement=True) - media_type = Column(Enum(MediaType), nullable=False) # Enum angepasst! + media_type = Column(Enum(MediaType), nullable=False) url = Column(String(255), nullable=False) file_path = Column(String(255), nullable=True) message_content = Column(Text, nullable=True) + + def to_dict(self): + return { + "id": self.id, + "media_type": self.media_type.value if self.media_type else None, + "url": self.url, + "file_path": self.file_path, + "message_content": self.message_content, + } diff --git a/server/routes/eventmedia.py b/server/routes/eventmedia.py new file mode 100644 index 0000000..8620902 --- /dev/null +++ b/server/routes/eventmedia.py @@ -0,0 +1,130 @@ +from re import A +from flask import Blueprint, request, jsonify, send_from_directory +from database import Session +from models import EventMedia +import os + +eventmedia_bp = Blueprint('eventmedia', __name__, url_prefix='/api/eventmedia') + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + + +def get_param(key, default=None): + # Reihenfolge: form > json > args + if request.form and key in request.form: + return request.form.get(key, default) + if request.is_json and request.json and key in request.json: + return request.json.get(key, default) + return request.args.get(key, default) + +# --- FileManager: List, Create Folder, Rename, Delete, Move --- + + +@eventmedia_bp.route('/filemanager/operations', methods=['GET', 'POST']) +def filemanager_operations(): + action = get_param('action') + path = get_param('path', '/') + name = get_param('name') + new_name = get_param('newName') + target_path = get_param('targetPath') + + full_path = os.path.join(MEDIA_ROOT, path.lstrip('/')) + + print(action, path, name, new_name, target_path, full_path) # Debug-Ausgabe + + if action == 'read': + # List files and folders + items = [] + for entry in os.scandir(full_path): + items.append({ + 'name': entry.name, + 'isFile': entry.is_file(), + 'size': entry.stat().st_size, + 'dateModified': entry.stat().st_mtime, + 'type': os.path.splitext(entry.name)[1][1:] if entry.is_file() else '', + 'hasChild': entry.is_dir() + }) + return jsonify({'files': items, 'cwd': {'name': os.path.basename(full_path), 'path': path}}) + elif action == 'delete': + for item in request.form.getlist('names[]'): + item_path = os.path.join(full_path, item) + if os.path.isdir(item_path): + os.rmdir(item_path) + else: + os.remove(item_path) + return jsonify({'success': True}) + elif action == 'rename': + src = os.path.join(full_path, name) + dst = os.path.join(full_path, new_name) + os.rename(src, dst) + return jsonify({'success': True}) + elif action == 'move': + src = os.path.join(full_path, name) + dst = os.path.join(MEDIA_ROOT, target_path.lstrip('/'), name) + os.rename(src, dst) + return jsonify({'success': True}) + elif action == 'create': + os.makedirs(os.path.join(full_path, name), exist_ok=True) + return jsonify({'success': True}) + else: + return jsonify({'error': 'Unknown action'}), 400 + +# --- FileManager: Upload --- + + +@eventmedia_bp.route('/filemanager/upload', methods=['POST']) +def filemanager_upload(): + path = request.args.get('path', '/') + upload_path = os.path.join(MEDIA_ROOT, path.lstrip('/')) + os.makedirs(upload_path, exist_ok=True) + for file in request.files.getlist('uploadFiles'): + file.save(os.path.join(upload_path, file.filename)) + return jsonify({'success': True}) + +# --- FileManager: Download --- + + +@eventmedia_bp.route('/filemanager/download', methods=['GET']) +def filemanager_download(): + path = request.args.get('path', '/') + names = request.args.getlist('names[]') + # Nur Einzel-Download für Beispiel + if names: + file_path = os.path.join(MEDIA_ROOT, path.lstrip('/'), names[0]) + return send_from_directory(os.path.dirname(file_path), os.path.basename(file_path), as_attachment=True) + return jsonify({'error': 'No file specified'}), 400 + +# --- FileManager: Get Image (optional, für Thumbnails) --- + + +@eventmedia_bp.route('/filemanager/get-image', methods=['GET']) +def filemanager_get_image(): + path = request.args.get('path', '/') + file_path = os.path.join(MEDIA_ROOT, path.lstrip('/')) + return send_from_directory(os.path.dirname(file_path), os.path.basename(file_path)) + +# --- EventMedia-API: Metadaten-Liste (wie gehabt) --- + + +@eventmedia_bp.route('', methods=['GET']) +def list_media(): + session = Session() + media = session.query(EventMedia).all() + return jsonify([m.to_dict() for m in media]) + +# --- EventMedia-API: Metadaten-Update --- + + +@eventmedia_bp.route('/', methods=['PUT']) +def update_media(media_id): + session = Session() + media = session.query(EventMedia).get(media_id) + if not media: + return jsonify({'error': 'Not found'}), 404 + data = request.json + media.url = data.get('title', media.url) + media.message_content = data.get('description', media.message_content) + # Event-Zuordnung ggf. ergänzen + session.commit() + return jsonify(media.to_dict()) diff --git a/server/wsgi.py b/server/wsgi.py index 77e96cc..ece9248 100644 --- a/server/wsgi.py +++ b/server/wsgi.py @@ -1,21 +1,23 @@ # server/wsgi.py +from routes.eventmedia import eventmedia_bp +from routes.events import events_bp +from routes.groups import groups_bp +from routes.clients import clients_bp +from database import Session, engine +from flask import Flask, jsonify, send_from_directory, request import glob import os import sys sys.path.append('/workspace') -from flask import Flask, jsonify, send_from_directory, request -from database import Session, engine app = Flask(__name__) # Blueprints importieren und registrieren -from routes.clients import clients_bp -from routes.groups import groups_bp -from routes.events import events_bp app.register_blueprint(clients_bp) app.register_blueprint(groups_bp) app.register_blueprint(events_bp) +app.register_blueprint(eventmedia_bp) @app.route("/health")