Menüs, Dialoge, Schaltflächen, Platzhalter, Hinweise und die
Fehlermeldungen der API standen in Ersatzschreibweise (oe/ae/ue). Sie
erscheinen als Toast oder Beschriftung direkt in der Oberfläche und
gehören damit richtig geschrieben; sechs sichtbare Texte mit ss statt ß
sind gleich mit erledigt ("Schließen", "Straße", "zu groß").
Unangetastet bleibt bewusst:
- Kommentare im Quelltext - die sind durchgängig ASCII, und der Diff wäre
dreimal so groß ohne Nutzen für den Benutzer
- Protokollzeilen (console.*) - die landen im journal, nicht in der App
- Datenbankwerte und interne Kennungen: 'uebernommen', 'empfaenger',
'vorschlaege', 'laeuft-bereits' stecken in Enums, SQL und
Zustandsvergleichen. Ein Umlaut darin hätte still falsche Ergebnisse
geliefert
- server/src/mail/extract.js - die Muster decken absichtlich beide
Schreibweisen ab: /gr(ü|ue)(ß|ss)e/
- scanner/utils/crypto.js - der Ersatzschlüssel darf sich nicht ändern,
sonst sind gespeicherte Portal-Passwörter nicht mehr lesbar
Geprüft mit einem Rundgang durch die Ansichten und Dialoge: sichtbarer
Text plus title/aria-label/placeholder enthalten keine
Ersatzschreibweisen mehr.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
170 lines
5.9 KiB
TypeScript
170 lines
5.9 KiB
TypeScript
import React from 'react';
|
|
import { Modal } from '../ui/Modal';
|
|
import { UserAvatar } from '../ui/UserAvatar';
|
|
import { MentionText } from './mentions';
|
|
import { formatDayLong, formatEventRange, parseSqlDateTime } from './calendarUtils';
|
|
import { eventColor, userColor, type AppUser, type CalendarEvent, type Lead } from '../../types';
|
|
|
|
interface EventDetailModalProps {
|
|
event: CalendarEvent;
|
|
leads: Lead[];
|
|
users: AppUser[];
|
|
canModify: boolean;
|
|
onEdit: () => void;
|
|
onDelete: () => void;
|
|
onClose: () => void;
|
|
onOpenLead: (leadId: number) => void;
|
|
onOpenUser: (userId: number) => void;
|
|
}
|
|
|
|
const Row: React.FC<{ icon: string; children: React.ReactNode }> = ({ icon, children }) => (
|
|
<div className="flex items-start gap-3">
|
|
<svg
|
|
className="mt-0.5 h-4 w-4 shrink-0 text-ink-400"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d={icon} />
|
|
</svg>
|
|
<div className="min-w-0 flex-1 text-sm text-ink-700">{children}</div>
|
|
</div>
|
|
);
|
|
|
|
/** Leseansicht eines Termins - hier sind die @-Mentions anklickbar. */
|
|
export const EventDetailModal: React.FC<EventDetailModalProps> = ({
|
|
event,
|
|
leads,
|
|
users,
|
|
canModify,
|
|
onEdit,
|
|
onDelete,
|
|
onClose,
|
|
onOpenLead,
|
|
onOpenUser,
|
|
}) => {
|
|
const color = eventColor(event);
|
|
const start = parseSqlDateTime(event.starts_at);
|
|
|
|
return (
|
|
<Modal
|
|
title={event.title}
|
|
subtitle={`${formatDayLong(start)} · ${formatEventRange(event)}`}
|
|
onClose={onClose}
|
|
size="lg"
|
|
footer={
|
|
<div className="flex gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="flex-1 rounded-xl border border-ink-200 py-3 text-sm font-bold text-ink-600 transition hover:bg-ink-100"
|
|
>
|
|
Schließen
|
|
</button>
|
|
{canModify && (
|
|
<>
|
|
<button
|
|
type="button"
|
|
onClick={onDelete}
|
|
className="rounded-xl border border-red-200 px-4 py-3 text-sm font-bold text-red-600 transition hover:bg-red-50"
|
|
>
|
|
Löschen
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={onEdit}
|
|
className="flex-1 rounded-xl bg-brand-500 py-3 text-sm font-bold text-white shadow-card transition hover:bg-brand-600"
|
|
>
|
|
Bearbeiten
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
}
|
|
>
|
|
<div className="space-y-5">
|
|
<div className="flex items-center gap-3 rounded-xl border border-ink-200 bg-ink-50/60 px-4 py-3">
|
|
<span className="h-10 w-1.5 shrink-0 rounded-full" style={{ backgroundColor: color }} />
|
|
{event.created_by !== null && (
|
|
<UserAvatar
|
|
user={{
|
|
id: event.created_by,
|
|
name: event.created_by_name ?? '?',
|
|
color: event.created_by_color ?? null,
|
|
avatar: event.created_by_avatar ?? null,
|
|
}}
|
|
size={36}
|
|
/>
|
|
)}
|
|
<div className="min-w-0">
|
|
<p className="text-xs font-extrabold uppercase tracking-wider text-ink-400">
|
|
Angelegt von
|
|
</p>
|
|
<p className="truncate text-sm font-bold text-ink-900">
|
|
{event.created_by_name ?? 'unbekannt'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Row icon="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z">
|
|
<span className="font-semibold">{formatDayLong(start)}</span>
|
|
<span className="text-ink-400"> · {formatEventRange(event)}</span>
|
|
</Row>
|
|
|
|
{event.location && (
|
|
<Row icon="M17.657 16.657L13.414 20.9a2 2 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0zM15 11a3 3 0 11-6 0 3 3 0 016 0z">
|
|
{event.location}
|
|
</Row>
|
|
)}
|
|
|
|
<Row icon="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z">
|
|
{event.participants.length === 0 ? (
|
|
<span className="text-ink-400">Keine Teilnehmer eingetragen.</span>
|
|
) : (
|
|
<div className="flex flex-wrap gap-2">
|
|
{event.participants.map((participant) => {
|
|
const tint = userColor(participant);
|
|
return (
|
|
<button
|
|
key={participant.id}
|
|
type="button"
|
|
onClick={() => onOpenUser(participant.id)}
|
|
className="flex items-center gap-2 rounded-lg py-1 pl-1 pr-2.5 text-xs font-bold transition hover:brightness-95"
|
|
style={{ backgroundColor: `${tint}1f`, color: tint }}
|
|
>
|
|
<UserAvatar user={participant} size={20} />
|
|
{participant.name}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</Row>
|
|
</div>
|
|
|
|
<section>
|
|
<h3 className="mb-2 text-[11px] font-extrabold uppercase tracking-[0.18em] text-ink-400">
|
|
Beschreibung
|
|
</h3>
|
|
{event.description ? (
|
|
<div className="rounded-xl border border-ink-200 bg-surface px-4 py-3 text-sm leading-relaxed text-ink-700">
|
|
<MentionText
|
|
text={event.description}
|
|
leads={leads}
|
|
users={users}
|
|
onOpenLead={onOpenLead}
|
|
onOpenUser={onOpenUser}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<p className="rounded-xl border border-dashed border-ink-200 px-4 py-6 text-center text-xs font-semibold text-ink-400">
|
|
Keine Beschreibung hinterlegt.
|
|
</p>
|
|
)}
|
|
</section>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
};
|