diff --git a/dashboard/src/appointments.tsx b/dashboard/src/appointments.tsx index 83ff77c..704e9f9 100644 --- a/dashboard/src/appointments.tsx +++ b/dashboard/src/appointments.tsx @@ -23,6 +23,12 @@ import { getGroupColor } from './groupColors'; import { deleteEvent } from './apiEvents'; import CustomEventModal from './components/CustomEventModal'; import { fetchMediaById } from './apiClients'; +import { Presentation, Globe, Video, MessageSquare, School } from 'lucide-react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import caGregorian from 'cldr-data/main/de/ca-gregorian.json'; +import numbers from 'cldr-data/main/de/numbers.json'; +import timeZoneNames from 'cldr-data/main/de/timeZoneNames.json'; +import numberingSystems from 'cldr-data/supplemental/numberingSystems.json'; // Typ für Gruppe ergänzen type Group = { @@ -37,9 +43,11 @@ type Event = { StartTime: Date; EndTime: Date; IsAllDay: boolean; - MediaId?: string | number; // Nur die MediaId wird gespeichert! + MediaId?: string | number; SlideshowInterval?: number; WebsiteUrl?: string; + Icon?: string; // <--- Icon ergänzen! + Type?: string; // <--- Typ ergänzen, falls benötigt }; type RawEvent = { @@ -48,21 +56,13 @@ type RawEvent = { StartTime: string; EndTime: string; IsAllDay: boolean; - MediaId?: string | number; // Nur die MediaId wird gespeichert! + MediaId?: string | number; + Icon?: string; // <--- Icon ergänzen! + Type?: string; }; -import * as de from 'cldr-data/main/de/ca-gregorian.json'; -import * as numbers from 'cldr-data/main/de/numbers.json'; -import * as timeZoneNames from 'cldr-data/main/de/timeZoneNames.json'; -import * as numberingSystems from 'cldr-data/supplemental/numberingSystems.json'; - -// CLDR-Daten laden -loadCldr( - (de as unknown as { default: object }).default, - (numbers as unknown as { default: object }).default, - (timeZoneNames as unknown as { default: object }).default, - (numberingSystems as unknown as { default: object }).default -); +// CLDR-Daten laden (direkt die JSON-Objekte übergeben) +loadCldr(caGregorian, numbers, timeZoneNames, numberingSystems); // Deutsche Lokalisierung für den Scheduler L10n.load({ @@ -100,6 +100,37 @@ L10n.load({ // Kultur setzen setCulture('de'); +// Mapping für Lucide-Icons +const iconMap: Record = { + Presentation, + Globe, + Video, + MessageSquare, + School, +}; + +const eventTemplate = (event: Event) => { + const IconComponent = iconMap[event.Icon ?? ''] || null; + // Zeitangabe formatieren + const start = event.StartTime instanceof Date ? event.StartTime : new Date(event.StartTime); + const end = event.EndTime instanceof Date ? event.EndTime : new Date(event.EndTime); + const timeString = `${start.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} - ${end.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`; + + return ( +
+
+ {IconComponent && ( + + + + )} + {event.Subject} +
+
{timeString}
+
+ ); +}; + const Appointments: React.FC = () => { const [groups, setGroups] = useState([]); const [selectedGroupId, setSelectedGroupId] = useState(null); @@ -137,6 +168,8 @@ const Appointments: React.FC = () => { EndTime: new Date(e.EndTime.endsWith('Z') ? e.EndTime : e.EndTime + 'Z'), IsAllDay: e.IsAllDay, MediaId: e.MediaId, + Icon: e.Icon, // <--- Icon übernehmen! + Type: e.Type, // <--- Typ übernehmen! })); setEvents(mapped); } catch (err) { @@ -247,6 +280,7 @@ const Appointments: React.FC = () => { currentView="Week" eventSettings={{ dataSource: events, + template: eventTemplate, // <--- Hier das Template setzen! }} cellClick={args => { // args.startTime und args.endTime sind Date-Objekte @@ -317,14 +351,44 @@ const Appointments: React.FC = () => { if (selectedGroupId && args.data && args.data.Id) { const groupColor = getGroupColor(selectedGroupId, groups); const now = new Date(); + + let IconComponent: React.ElementType | null = null; + switch (args.data.Type) { + case 'presentation': + IconComponent = Presentation; + break; + case 'website': + IconComponent = Globe; + break; + case 'video': + IconComponent = Video; + break; + case 'message': + IconComponent = MessageSquare; + break; + case 'webuntis': + IconComponent = School; + break; + default: + IconComponent = null; + } + + // Nur .e-subject verwenden! + const titleElement = args.element.querySelector('.e-subject'); + if (titleElement && IconComponent) { + const svgString = renderToStaticMarkup(); + // Immer nur den reinen Text nehmen, kein vorhandenes Icon! + const subjectText = (titleElement as HTMLElement).textContent ?? ''; + (titleElement as HTMLElement).innerHTML = + `${svgString}` + + subjectText; + } + + // Vergangene Termine: Raumgruppenfarbe mit Transparenz if (args.data.EndTime && args.data.EndTime < now) { - // Vergangene Termine: Raumgruppenfarbe mit Transparenz und grauer Schrift - args.element.style.backgroundColor = groupColor - ? `${groupColor}` // 100 = ~100% Transparenz für hex-Farben - : '#f3f3f3'; + args.element.style.backgroundColor = groupColor ? `${groupColor}` : '#f3f3f3'; args.element.style.color = ''; } else if (groupColor) { - // Aktuelle/future Termine: normale Raumgruppenfarbe args.element.style.backgroundColor = groupColor; args.element.style.color = ''; } diff --git a/models/models.py b/models/models.py index 0c030e6..9cdccfb 100644 --- a/models/models.py +++ b/models/models.py @@ -93,6 +93,8 @@ class MediaType(enum.Enum): svg = "svg" # HTML-Mitteilung html = "html" + # Webseiten + website = "website" class Event(Base): diff --git a/server/alembic/versions/e6eaede720aa_add_website_to_mediatype_enum.py b/server/alembic/versions/e6eaede720aa_add_website_to_mediatype_enum.py new file mode 100644 index 0000000..17db3c6 --- /dev/null +++ b/server/alembic/versions/e6eaede720aa_add_website_to_mediatype_enum.py @@ -0,0 +1,34 @@ +"""Add website to MediaType enum + +Revision ID: e6eaede720aa +Revises: 0c47280d3e2d +Create Date: 2025-07-24 13:40:50.553863 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'e6eaede720aa' +down_revision: Union[str, None] = '0c47280d3e2d' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + 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','website') NOT NULL;" + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/server/routes/events.py b/server/routes/events.py index d9425cd..a4b8b44 100644 --- a/server/routes/events.py +++ b/server/routes/events.py @@ -9,6 +9,17 @@ sys.path.append('/workspace') events_bp = Blueprint("events", __name__, url_prefix="/api/events") +def get_icon_for_type(event_type): + # Lucide-Icon-Namen als String + return { + "presentation": "Presentation", # <--- geändert! + "website": "Globe", + "video": "Video", + "message": "MessageSquare", + "webuntis": "School", + }.get(event_type, "") + + @events_bp.route("", methods=["GET"]) def get_events(): session = Session() @@ -45,6 +56,8 @@ def get_events(): "EndTime": e.end.isoformat() if e.end else None, "IsAllDay": False, "MediaId": e.event_media_id, + "Type": e.event_type.value if e.event_type else None, # <-- Enum zu String! + "Icon": get_icon_for_type(e.event_type.value if e.event_type else None), }) session.close() return jsonify(result)