introduce icons in events

This commit is contained in:
2025-07-24 14:11:27 +00:00
parent 49e9f9eade
commit 270bad5980
4 changed files with 132 additions and 19 deletions

View File

@@ -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<string, React.ElementType> = {
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 (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
<div style={{ display: 'flex', alignItems: 'center', color: '#fff', marginBottom: 2 }}>
{IconComponent && (
<span style={{ verticalAlign: 'middle', display: 'inline-block', marginRight: 6 }}>
<IconComponent size={18} color="#fff" />
</span>
)}
<span style={{ marginTop: 3 }}>{event.Subject}</span>
</div>
<div style={{ fontSize: '0.95em', color: '#fff', marginTop: -2 }}>{timeString}</div>
</div>
);
};
const Appointments: React.FC = () => {
const [groups, setGroups] = useState<Group[]>([]);
const [selectedGroupId, setSelectedGroupId] = useState<string | null>(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(<IconComponent size={18} color="#78591c" />);
// Immer nur den reinen Text nehmen, kein vorhandenes Icon!
const subjectText = (titleElement as HTMLElement).textContent ?? '';
(titleElement as HTMLElement).innerHTML =
`<span style="vertical-align:middle;display:inline-block;margin-right:6px;">${svgString}</span>` +
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 = '';
}

View File

@@ -93,6 +93,8 @@ class MediaType(enum.Enum):
svg = "svg"
# HTML-Mitteilung
html = "html"
# Webseiten
website = "website"
class Event(Base):

View File

@@ -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 ###

View File

@@ -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)