Files
perinco-CRM/src/components/calendar/EventDetailModal.tsx
T
Sascha WolfandClaude Opus 5 8b7ce448a6 fix(ui): echte Umlaute in allen sichtbaren Texten
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>
2026-08-11 09:37:24 +02:00

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>
);
};