From 19464fe20d92d3ab6e1c1c53da758ce1658e7cb0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 22:09:22 +0000 Subject: [PATCH] Planboard: echte Termine aus den Tickets statt leerer Wochenansicht Das Planboard war eine Huelle - sieben Spalten mit "Keine Termine". Es zeigt jetzt die terminierten Tickets der Woche: Uhrzeit, Kunde, Betreff, Kuerzel des Zustaendigen. Klick fuehrt ueber den vorhandenen Deep-Link /tickets?woid=... ins Ticket. Heute ist hervorgehoben, "Heute" springt zurueck. Ein Termin ist bewusst kein eigener Datensatz, sondern die Ticketfelder startDate/startTime/endTime/assignedTo. Die Rechnungsanalyse im Kundenportal schreibt genau diese Felder, deshalb steht ein dort vereinbarter Termin ohne Umweg hier - und Ticket und Termin koennen nicht auseinanderlaufen. Die Aufbereitung liegt in src/lib/planboard.js, damit sie ohne UI testbar ist: Datum kommt als Text und real auch einstellig ("22.5.2026"), Uhrzeit als "hhmm"; beides wird tolerant gelesen, Unsinn faellt raus statt zu verrutschen. Co-Authored-By: Claude Opus 5 --- src/lib/planboard.js | 75 ++++++++++++++++++++++++++ src/pages/PlanboardPage.jsx | 104 +++++++++++++++++++++++++++++++----- 2 files changed, 165 insertions(+), 14 deletions(-) create mode 100644 src/lib/planboard.js diff --git a/src/lib/planboard.js b/src/lib/planboard.js new file mode 100644 index 0000000..29d94d1 --- /dev/null +++ b/src/lib/planboard.js @@ -0,0 +1,75 @@ +/* Planboard-Logik: aus Tickets werden Termine. + Termine sind kein eigener Datensatz - sie haengen an den Tickets selbst + (startDate/startTime/endTime/assignedTo). Die Rechnungsanalyse im + Kundenportal schreibt genau diese Felder, deshalb erscheint ein dort + vereinbarter Termin hier ohne Umweg. + WOMS speichert Datum als Text "dd.mm.yyyy" (auch einstellig: "2.5.2026") + und Uhrzeit als "hhmm". Beides wird hier tolerant gelesen. */ + +export function parseGermanDate(value) { + const m = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/.exec(String(value || '').trim()) + if (!m) return null + const day = Number(m[1]) + const month = Number(m[2]) + const year = Number(m[3]) + const date = new Date(year, month - 1, day) + // "31.02.2026" wuerde sonst still zum 3. Maerz werden + if (date.getDate() !== day || date.getMonth() !== month - 1 || date.getFullYear() !== year) return null + return date +} + +export function formatTime(value) { + const m = /^(\d{1,2}):?(\d{2})$/.exec(String(value || '').trim()) + if (!m) return '' + const hour = Number(m[1]) + const minute = Number(m[2]) + if (hour > 23 || minute > 59) return '' + return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}` +} + +export function dayKey(date) { + const pad = (n) => String(n).padStart(2, '0') + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` +} + +export function toAppointment(workorder, employeeByUserId = {}) { + const date = parseGermanDate(workorder.startDate) + if (!date) return null + const employee = employeeByUserId[workorder.assignedTo] + return { + id: workorder.$id, + woid: workorder.woid || '', + date, + key: dayKey(date), + time: formatTime(workorder.startTime), + endTime: formatTime(workorder.endTime), + topic: workorder.topic || workorder.title || 'Ohne Betreff', + type: workorder.type || '', + status: workorder.status || '', + customerName: workorder.customerName || '', + employeeName: employee?.displayName || workorder.assignedName || '', + employeeShort: employee?.shortcode || '', + } +} + +/* Termine nach Tag ("yyyy-mm-dd"), innerhalb des Tages nach Uhrzeit. + Eintraege ohne Uhrzeit landen am Ende des Tages. */ +export function groupAppointmentsByDay(workorders = [], employees = []) { + const employeeByUserId = {} + for (const e of employees) employeeByUserId[e.userId] = e + + const map = {} + for (const wo of workorders) { + const appointment = toAppointment(wo, employeeByUserId) + if (!appointment) continue + ;(map[appointment.key] = map[appointment.key] || []).push(appointment) + } + for (const key of Object.keys(map)) { + map[key].sort((a, b) => (a.time || '99:99').localeCompare(b.time || '99:99')) + } + return map +} + +export function countInRange(byDay, days = []) { + return days.reduce((sum, d) => sum + (byDay[dayKey(d)]?.length || 0), 0) +} diff --git a/src/pages/PlanboardPage.jsx b/src/pages/PlanboardPage.jsx index f844110..2e333b5 100644 --- a/src/pages/PlanboardPage.jsx +++ b/src/pages/PlanboardPage.jsx @@ -1,19 +1,40 @@ -import { useState } from 'react' -import { format, addDays, startOfWeek } from 'date-fns' +import { useState, useMemo } from 'react' +import { useNavigate } from 'react-router-dom' +import { format, addDays, startOfWeek, isSameDay } from 'date-fns' import { de } from 'date-fns/locale' import { FaChevronLeft, FaChevronRight } from 'react-icons/fa6' import { PageHeader, Card, Button } from '../components/ui' +import { useWorkorders } from '../hooks/useWorkorders' +import { useEmployees } from '../hooks/useEmployees' +import { groupAppointmentsByDay, countInRange, dayKey } from '../lib/planboard' + +/* Termine haengen an den Tickets (startDate/startTime/endTime/assignedTo); + die Aufbereitung steckt in src/lib/planboard.js. */ + +const TYPE_COLORS = { + akquise: { bg: 'rgba(198,113,57,0.16)', fg: '#c67139' }, + webpage: { bg: 'rgba(122,138,94,0.18)', fg: '#7a8a5e' }, + project: { bg: 'rgba(122,138,94,0.18)', fg: '#7a8a5e' }, +} export default function PlanboardPage() { + const navigate = useNavigate() const [currentWeek, setCurrentWeek] = useState(startOfWeek(new Date(), { weekStartsOn: 1 })) + const { workorders, loading } = useWorkorders({ limit: 500 }) + const { employees } = useEmployees() + const weekDays = Array.from({ length: 7 }, (_, i) => addDays(currentWeek, i)) const navigateWeek = (dir) => setCurrentWeek((prev) => addDays(prev, dir * 7)) + const goToday = () => setCurrentWeek(startOfWeek(new Date(), { weekStartsOn: 1 })) + + const byDay = useMemo(() => groupAppointmentsByDay(workorders || [], employees || []), [workorders, employees]) + const weekCount = countInRange(byDay, weekDays) return (
@@ -21,22 +42,77 @@ export default function PlanboardPage() { {format(currentWeek, 'dd.MM.yyyy')} - {format(addDays(currentWeek, 6), 'dd.MM.yyyy')} +
} />
- {weekDays.map((day) => ( - -
- {format(day, 'EEEE', { locale: de })} - {format(day, 'dd.MM')} -
-
-

Keine Termine

-
-
- ))} + {weekDays.map((day) => { + const entries = byDay[dayKey(day)] || [] + const today = isSameDay(day, new Date()) + return ( + +
+ {format(day, 'EEEE', { locale: de })} + {format(day, 'dd.MM')} +
+ +
+ {entries.length === 0 && ( +

Keine Termine

+ )} + + {entries.map((entry) => { + const color = TYPE_COLORS[String(entry.type).toLowerCase()] + || { bg: 'rgba(127,127,127,0.10)', fg: 'var(--border)' } + return ( + + ) + })} +
+
+ ) + })}
)