diff --git a/docs/05-frontend.md b/docs/05-frontend.md index dcc2de7..4882093 100644 --- a/docs/05-frontend.md +++ b/docs/05-frontend.md @@ -81,7 +81,7 @@ src/ Sidebar.tsx Menue inkl. aufklappbarer Scanner-Gruppe Header.tsx Titel, Untertitel, Dark-Mode-Schalter opportunities/ Kachel, Liste, Pipeline (Drag & Drop), Formular - calendar/ Monat/Woche/Tag, Termine, @-Mentions + calendar/ Monat/Woche/Tag, Termine (Drag & Drop), @-Mentions leads/ Kachel, Liste, Netzwerkdiagramm (d3-force) scanner/ Dashboard, Unpassend, Suchagenten, Boersen-Konten settings/ Reiter: Nutzerverwaltung, Konto, Scanner, Whisper diff --git a/docs/07-anleitung-benutzer.md b/docs/07-anleitung-benutzer.md index 0cb559b..62cfcaa 100644 --- a/docs/07-anleitung-benutzer.md +++ b/docs/07-anleitung-benutzer.md @@ -86,6 +86,12 @@ eintragen. * **Monat, Woche, Tag** oben umschalten, `Heute` und die Pfeile blaettern. * Klick auf einen Monatstag oder eine Stundenzelle legt sofort einen Termin an. * Klick auf einen Wochentagskopf springt in die Tagesansicht. +* **Verschieben per Ziehen**: Termin anfassen und auf einen anderen Tag oder eine + andere Uhrzeit ziehen. Im Monatsraster wechselt nur der Tag, die Uhrzeit bleibt + stehen; in Woche und Tag rastet der Termin auf **15 Minuten** ein, und eine + gestrichelte Linie zeigt vorab die neue Startzeit. Die **Dauer bleibt** immer + gleich, und der Punkt, an dem man den Termin angefasst hat, wandert mit. Ziehen + darf nur, wer den Termin auch aendern darf. * **Farben** gehoeren zum Ersteller. Die eigene Farbe aendert man unter *Einstellungen → Mein Konto*; die Legende unter dem Kalender zeigt, welche Farbe zu wem gehoert. diff --git a/src/components/calendar/Calendar.tsx b/src/components/calendar/Calendar.tsx index bd15015..7f42d25 100644 --- a/src/components/calendar/Calendar.tsx +++ b/src/components/calendar/Calendar.tsx @@ -22,6 +22,7 @@ import { positionEvents, startOfDay, startOfWeek, + toSqlDateTime, weekDays, } from './calendarUtils'; import { useToast } from '../Toast'; @@ -55,6 +56,50 @@ const VIEW_BUTTONS: { mode: CalendarViewMode; label: string }[] = [ const HOUR_HEIGHT = 48; /** Ab wie vielen Terminen pro Monatstag auf "+N weitere" gekuerzt wird. */ const MONTH_CHIP_LIMIT = 3; +/** Raster, auf das ein gezogener Termin im Zeitraster einrastet. */ +const SNAP_MS = 15 * 60 * 1000; + +// ---------- Verschieben per Drag & Drop ---------- + +interface DragInfo { + event: CalendarEvent; + /** Tag, an dem der Termin angefasst wurde - mehrtaegige haengen an mehreren. */ + sourceDay: Date; + /** Abstand vom Terminstart bis zum Greifpunkt; nur das Zeitraster fuellt ihn. */ + grabOffsetMs: number; +} + +interface CalendarDrag { + canDrag: (event: CalendarEvent) => boolean; + active: () => boolean; + begin: (info: DragInfo) => void; + finish: () => void; + /** Ganzer Tagesversatz - Monatsraster und Ganztagszeile. */ + dropOnDay: (day: Date) => void; + /** Neuer Startzeitpunkt aus der Zeigerposition im Zeitraster. */ + dropOnTime: (day: Date, minutes: number) => void; + /** Vorschaulinie unter dem Zeiger; null bei ganztaegigen Terminen. */ + previewStart: (day: Date, minutes: number) => Date | null; +} + +/** Zeigerposition innerhalb einer Tagesspalte als Minuten seit 00:00. */ +function pointerMinutes(clientY: number, column: HTMLElement): number { + const rect = column.getBoundingClientRect(); + if (rect.height === 0) return 0; + const minutes = ((clientY - rect.top) / rect.height) * 24 * 60; + return Math.min(Math.max(minutes, 0), 24 * 60); +} + +/** Setzt die Daten, ohne die Firefox gar keinen Drag startet. */ +function armDrag(transfer: DataTransfer, event: CalendarEvent) { + transfer.effectAllowed = 'move'; + transfer.setData('text/plain', String(event.id)); +} + +/** Verlaesst der Zeiger das Element wirklich - oder nur ein Kindelement? */ +function leftElement(dragEvent: React.DragEvent): boolean { + return !dragEvent.currentTarget.contains(dragEvent.relatedTarget as Node | null); +} // ---------- Bausteine ---------- // Bewusst auf Modulebene: als verschachtelte Funktionskomponenten wuerde React @@ -63,27 +108,42 @@ const MONTH_CHIP_LIMIT = 3; const EventChip: React.FC<{ event: CalendarEvent; + /** Tag, an dem dieser Chip haengt - Ausgangspunkt fuer den Tagesversatz. */ + day: Date; + drag: CalendarDrag; onOpen: (id: number) => void; showTime?: boolean; -}> = ({ event, onOpen, showTime = false }) => ( - -); +}> = ({ event, day, drag, onOpen, showTime = false }) => { + const draggable = drag.canDrag(event); + + return ( + + ); +}; interface GridProps { today: Date; @@ -91,6 +151,7 @@ interface GridProps { onCreate: (start: Date) => void; onOpenEvent: (id: number) => void; onShowDay: (day: Date) => void; + drag: CalendarDrag; } const MonthView: React.FC = ({ @@ -100,11 +161,18 @@ const MonthView: React.FC = ({ onCreate, onOpenEvent, onShowDay, + drag, }) => { const days = monthMatrix(cursor); + const [dropDay, setDropDay] = useState(null); return ( -
+ // Bricht der Benutzer den Drag mit Esc ab, kommt kein dragleave mehr - das + // dragend der Quelle blubbert aber hierher und raeumt die Markierung weg. +
setDropDay(null)} + className="overflow-hidden rounded-2xl border border-ink-200 bg-surface" + >
{WEEKDAY_LABELS.map((label) => (
= ({ const visible = list.slice(0, MONTH_CHIP_LIMIT); const hidden = list.length - visible.length; const createAt = new Date(day.getFullYear(), day.getMonth(), day.getDate(), 9); + const key = day.toDateString(); return (
onCreate(createAt)} @@ -137,10 +206,27 @@ const MonthView: React.FC = ({ onCreate(createAt); } }} + onDragOver={(dragEvent) => { + if (!drag.active()) return; + dragEvent.preventDefault(); + dragEvent.dataTransfer.dropEffect = 'move'; + setDropDay(key); + }} + onDragLeave={(dragEvent) => { + if (leftElement(dragEvent)) setDropDay((current) => (current === key ? null : current)); + }} + onDrop={(dragEvent) => { + if (!drag.active()) return; + dragEvent.preventDefault(); + setDropDay(null); + drag.dropOnDay(day); + }} title="Klicken, um an diesem Tag einen Termin anzulegen" className={`min-h-[104px] cursor-pointer border-b border-ink-100 p-1.5 transition hover:bg-brand-50/40 ${ index % 7 === 6 ? '' : 'border-r' - } ${isCurrentMonth ? '' : 'bg-ink-50/50'}`} + } ${isCurrentMonth ? '' : 'bg-ink-50/50'} ${ + dropDay === key ? 'bg-brand-50 ring-2 ring-inset ring-brand-400' : '' + }`} >
= ({
{visible.map((event) => ( - + ))} {hidden > 0 && (