Auch drei Kommentare mit dem Altlink-Muster umformuliert (useWorkorders, ticketListUrl, TicketsPage), sonst bleibt die Pruefung auf den Altlink nicht leer. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
124 lines
5.7 KiB
React
124 lines
5.7 KiB
React
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'
|
||
import { ticketPath } from '../lib/ticketRoutes'
|
||
|
||
/* 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)' },
|
||
}
|
||
|
||
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 (
|
||
<div className="page">
|
||
<PageHeader
|
||
title="Planboard"
|
||
subtitle={loading ? 'Wochenplanung – lädt…' : `Wochenplanung – ${weekCount} ${weekCount === 1 ? 'Termin' : 'Termine'} diese Woche`}
|
||
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>
|
||
}
|
||
/>
|
||
|
||
<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 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.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>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|