Planboard: Wochenansicht ohne Karten (Tagesspalten, heute als Pille, Termine als Links)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-06 06:03:14 +00:00
1 parent a0149137ea
commit e54a30b006
2 files changed
+78 -94

No files matched your search

+48 -90
View File
@@ -1,26 +1,19 @@
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 { Link } from 'react-router-dom'
import { addDays, startOfWeek, isSameDay } from 'date-fns'
import { PageHeader, Button } from '../components/ui'
import { useWorkorders } from '../hooks/useWorkorders'
import { useEmployees } from '../hooks/useEmployees'
import { groupAppointmentsByDay, countInRange, dayKey } from '../lib/planboard'
import { ticketPath } from '../lib/ticketRoutes'
import { ticketPath, ticketRef } from '../lib/ticketRoutes'
import {
planboardSubtitle, dayLabels, appointmentClass, appointmentTimeLabel, appointmentOwnerLabel, appointmentTitle,
} from '../lib/planboardView'
/* Termine haengen an den Tickets (startDate/startTime/endTime/assignedTo);
die Aufbereitung steckt in src/lib/planboard.js. */
// Farben nur ueber Tokens (P3 baut die Seite um)
const TYPE_COLORS = {
akquise: { bg: 'var(--appt-akquise)', fg: 'var(--orange-fg)' },
webpage: { bg: 'var(--bg-hover)', fg: 'var(--text-2)' },
project: { bg: 'var(--bg-hover)', fg: 'var(--text-2)' },
}
die Aufbereitung steckt in src/lib/planboard.js, die Texte in src/lib/planboardView.js. */
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()
@@ -36,87 +29,52 @@ export default function PlanboardPage() {
<div className="page">
<PageHeader
title="Planboard"
subtitle={loading ? 'Wochenplanung – lädt…' : `Wochenplanung – ${weekCount} ${weekCount === 1 ? 'Termin' : 'Termine'} diese Woche`}
subtitle={planboardSubtitle({ weekStart: currentWeek, count: weekCount, loading })}
actions={
<div className="flex gap-2 items-center">
<Button variant="ghost" onClick={() => navigateWeek(-1)}><FaChevronLeft /></Button>
<span className="muted" style={{ minWidth: 200, textAlign: 'center' }}>
{format(currentWeek, 'dd.MM.yyyy')} - {format(addDays(currentWeek, 6), 'dd.MM.yyyy')}
</span>
<Button variant="ghost" onClick={() => navigateWeek(1)}><FaChevronRight /></Button>
<Button variant="ghost" onClick={goToday}>Heute</Button>
</div>
<>
<Button variant="quiet" aria-label="Vorherige Woche" onClick={() => navigateWeek(-1)}>←</Button>
<Button onClick={goToday}>Heute</Button>
<Button variant="quiet" aria-label="Nächste Woche" onClick={() => navigateWeek(1)}>→</Button>
</>
}
/>
<div className="planboard-scroll">
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 12 }}>
{weekDays.map((day) => {
const entries = byDay[dayKey(day)] || []
const today = isSameDay(day, new Date())
return (
<Card key={day.toISOString()} pad={false}>
<div style={{
padding: '10px 12px',
borderBottom: '1px solid var(--border)',
display: 'flex',
flexDirection: 'column',
background: today ? 'var(--appt-akquise)' : undefined,
}}>
<strong style={{ fontSize: 13 }}>{format(day, 'EEEE', { locale: de })}</strong>
<span className="muted" style={{ fontSize: 12 }}>{format(day, 'dd.MM')}</span>
</div>
<div className="week">
{weekDays.map((day, index) => {
const entries = byDay[dayKey(day)] || []
const today = isSameDay(day, new Date())
const labels = dayLabels(day)
return (
<div
key={day.toISOString()}
className={`day${today ? ' today' : ''}${index >= 5 ? ' weekend' : ''}`}
role="group"
aria-label={labels.full}
aria-current={today ? 'date' : undefined}
>
<div className="day-head">
<span className="wd">{labels.weekday}</span>
<span className="dn">{labels.number}</span>
</div>
<div style={{ padding: 8, minHeight: 120, display: 'flex', flexDirection: 'column', gap: 6 }}>
{entries.length === 0 && (
<p className="faint" style={{ fontSize: 12, padding: '4px 4px 0' }}>Keine Termine</p>
)}
{entries.length === 0 && <div className="none">Keine Termine</div>}
{entries.map((entry) => {
const color = TYPE_COLORS[String(entry.type).toLowerCase()]
|| { bg: 'var(--bg-subtle)', fg: 'var(--line-strong)' }
return (
<button
key={entry.id}
type="button"
title={`Ticket #${entry.woid} öffnen`}
onClick={() => navigate(ticketPath(entry.woid || entry.id))}
style={{
textAlign: 'left',
border: '1px solid var(--border)',
borderLeft: `3px solid ${color.fg}`,
borderRadius: 8,
background: color.bg,
color: 'inherit',
padding: '8px 10px',
cursor: 'pointer',
font: 'inherit',
display: 'flex',
flexDirection: 'column',
gap: 2,
}}
>
<span style={{ fontSize: 12, fontWeight: 700 }}>
{entry.time ? `${entry.time}${entry.endTime ? `–${entry.endTime}` : ''}` : 'ohne Uhrzeit'}
</span>
<span style={{ fontSize: 12.5, fontWeight: 600, lineHeight: 1.25 }}>
{entry.customerName || entry.topic}
</span>
{entry.customerName && (
<span className="muted" style={{ fontSize: 11.5, lineHeight: 1.25 }}>{entry.topic}</span>
)}
<span className="muted" style={{ fontSize: 11 }}>
#{entry.woid}
{entry.employeeName ? ` · ${entry.employeeShort || entry.employeeName}` : ' · niemand zugewiesen'}
</span>
</button>
)
})}
</div>
</Card>
)
})}
</div>
{entries.map((entry) => (
<Link
key={entry.id}
className={appointmentClass(entry.type)}
to={ticketPath(ticketRef({ woid: entry.woid, $id: entry.id }))}
title={appointmentTitle(entry)}
>
<div className="tm">{appointmentTimeLabel(entry)}</div>
<div className="tt">{entry.customerName || entry.topic}</div>
{entry.customerName && <div className="cu">{entry.topic}</div>}
<div className="cu">{appointmentOwnerLabel(entry)}</div>
</Link>
))}
</div>
)
})}
</div>
</div>
)