Tickets: Arbeitsblatt-Zeilen (.ws), Kennzahlen, Dringend-Schalter und Weitere Tickets im neuen Look

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-06 06:02:55 +00:00
1 parent ab42918bbc
commit 6eb5ab3679
6 files changed
+186 -381

No files matched your search

+102 -329
View File
@@ -1,342 +1,115 @@
import { useState } from 'react'
import { FaClock, FaUser, FaExchangeAlt, FaComment, FaChevronDown, FaChevronUp, FaPen, FaCheck, FaHandsHelping } from 'react-icons/fa'
import { serviceTypeLabel, needsTimeEntry, worksheetTone } from '../lib/ticketForm'
import { statusLabel } from '../lib/ticketStatus'
import { parseSupporters, supportMinutes, firstName } from '../lib/support'
import { EmptyState } from './ui'
import { needsTimeEntry, isGitWorksheet } from '../lib/ticketForm'
import { parseSupporters } from '../lib/support'
import {
showAboToggle,
statusAfterLabel,
supportLines,
timeLabel,
worksheetBadge,
worksheetKindLabel,
worksheetWhen,
} from '../lib/worksheetView'
// Randfarbe je Zustand (worksheetTone): orange = Git-Zeit fehlt, gruen = Git-Zeit eingetragen. Der Kopf ist fuer alle Zustaende
// grau (Schrift --text auf --bg-subtle); P2 baut die Liste um.
const TONES = {
'git-open': { border: 'var(--orange-fg)', base: 'var(--bg-subtle)', hover: 'var(--bg-hover)' },
'git-done': { border: 'var(--green-fg)', base: 'var(--bg-subtle)', hover: 'var(--bg-hover)' },
comment: { border: 'var(--blue-fg)', base: 'var(--bg-subtle)', hover: 'var(--bg-hover)' },
work: { border: 'var(--text-2)', base: 'var(--bg-subtle)', hover: 'var(--bg-hover)' },
}
// Hinweise im Kopf: graue Badge-Toene
const headerBadgeStyle = { background: 'var(--grey-bg)', color: 'var(--grey-fg)', marginLeft: '8px' }
const headerButtonStyle = {
background: 'var(--bg)',
color: 'var(--text)',
border: '1px solid var(--line-strong)',
borderRadius: '6px',
padding: '2px 10px',
fontSize: '0.8rem',
marginRight: '8px',
}
export default function WorksheetList({ worksheets, totalTime, loading, onTogglePlan, aboHoursAllowed = true, canEditTimes, onEditTimes, canAddSupport, onOwnSupport, currentUserId }) {
const [expandedWorksheets, setExpandedWorksheets] = useState({})
if (loading) {
return (
<div className="text-center p-4">
<div className="spinner-border" role="status">
<span className="visually-hidden">Lädt …</span>
</div>
</div>
)
}
// Arbeitsblaetter, neueste zuerst wie geliefert: links Datum und Beginn, in der Mitte Person, Art, Text und Aktionen, rechts die Zeit
export default function WorksheetList({
worksheets,
loading,
onTogglePlan,
aboHoursAllowed = true,
canEditTimes,
onEditTimes,
canAddSupport,
onOwnSupport,
currentUserId,
}) {
if (loading) return <div className="loading">Wird geladen …</div>
if (!worksheets || worksheets.length === 0) {
return (
<div className="alert alert-info" role="alert">
<FaComment className="me-2" />
Noch keine Arbeitsblätter. Leg das erste mit „Arbeitsblatt hinzufügen“ an.
</div>
)
}
const formatTime = (minutes) => {
if (!minutes || minutes === 0) return '-'
const hours = Math.floor(minutes / 60)
const mins = minutes % 60
return hours > 0 ? `${hours}h ${mins}min` : `${mins}min`
}
const formatDateTime = (date, time) => {
if (!time) return date
const hours = time.substring(0, 2)
const mins = time.substring(2, 4)
return `${date} ${hours}:${mins}`
}
const toggleWorksheet = (wsid) => {
setExpandedWorksheets(prev => ({
...prev,
[wsid]: !prev[wsid]
}))
return <EmptyState title="Noch keine Arbeitsblätter." hint="Leg das erste mit „Arbeitsblatt hinzufügen“ an." />
}
return (
<div className="worksheet-list">
{/* Worksheet-Einträge */}
<div className="timeline">
{worksheets.map((ws, index) => {
const isExpanded = expandedWorksheets[ws.wsid] || false
const isGit = ws.serviceType === 'GIT'
const tone = worksheetTone(ws)
const colors = TONES[tone]
const supporters = ws.isComment ? [] : parseSupporters(ws.supporters)
return (
<div key={ws.$id} className="timeline-item mb-4" style={{
animation: `fadeIn 0.5s ease-in-out ${index * 0.1}s backwards`
}}>
<div className="card border-0 shadow-sm overflow-hidden" style={{
borderLeft: `4px solid ${colors.border}`,
borderRadius: '8px',
transition: 'transform 0.2s ease, box-shadow 0.2s ease'
}} onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px)'
e.currentTarget.style.boxShadow = 'var(--shadow-btn)'
}} onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)'
e.currentTarget.style.boxShadow = 'none'
}}>
{/* Header - Immer sichtbar, klickbar */}
<div
className="card-header d-flex justify-content-between align-items-center py-3"
onClick={() => toggleWorksheet(ws.wsid)}
style={{
background: colors.base,
color: 'var(--text)',
border: 'none',
cursor: 'pointer',
userSelect: 'none',
transition: 'background 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = colors.hover
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = colors.base
}}
>
<div className="d-flex align-items-center gap-3">
{isExpanded ? (
<FaChevronUp style={{ fontSize: '0.9rem', opacity: 0.8 }} />
) : (
<FaChevronDown style={{ fontSize: '0.9rem', opacity: 0.8 }} />
)}
<div>
<strong className="fs-6">WSID {ws.wsid}</strong>
{ws.isComment && !isGit && (
<span className="badge ms-2" style={headerBadgeStyle}>
<FaComment className="me-1" /> Kommentar
</span>
)}
{tone === 'git-open' && (
<span className="badge ms-2" style={headerBadgeStyle}>
<FaClock className="me-1" /> Zeit offen
</span>
)}
{tone === 'git-done' && (
<span className="badge ms-2" style={headerBadgeStyle}>
<FaCheck className="me-1" /> Zeit eingetragen
</span>
)}
{supporters.length > 0 && (
<span className="badge ms-2" title="Weitere Personen haben unterstützt" style={headerBadgeStyle}>
+{supporters.length} Unterstützung
</span>
)}
{ws.countsToPlan && !ws.isComment && (
<span className="badge ms-2" title="Wird vom Stundenguthaben des Wartungs-Abos abgezogen" style={{ ...headerBadgeStyle, background: 'var(--violet-bg)', color: 'var(--violet-fg)' }}>
Abo-Stunden
</span>
)}
</div>
{/* Collapsed: Mitarbeiter & Zeit im Header */}
{!isExpanded && (
<div className="d-flex align-items-center gap-3 ms-3">
<div className="d-flex align-items-center">
<FaUser style={{ fontSize: '0.9rem', marginRight: '0.5rem' }} />
<span style={{ fontSize: '0.9rem' }}>{ws.employeeName}</span>
</div>
{!ws.isComment && (
<div className="d-flex align-items-center">
<FaClock style={{ fontSize: '0.9rem', marginRight: '0.5rem' }} />
<span style={{ fontSize: '0.9rem' }}>{formatTime(ws.totalTime)}</span>
</div>
)}
<span className="badge" style={{
background: 'var(--grey-bg)',
color: 'var(--grey-fg)',
fontSize: '0.8rem'
}}>{serviceTypeLabel(ws.serviceType)}</span>
</div>
)}
</div>
<div className="d-flex align-items-center gap-2">
{onEditTimes && canEditTimes?.(ws) && (
<button
type="button"
className="btn btn-sm"
title="Beginn, Ende und Arbeitszeit ändern"
style={headerButtonStyle}
onClick={(e) => {
e.stopPropagation()
onEditTimes(ws)
}}
>
{needsTimeEntry(ws)
? <><FaClock className="me-1" /> Zeit nachtragen</>
: <><FaPen className="me-1" /> Zeiten bearbeiten</>}
</button>
)}
{onOwnSupport && canAddSupport?.(ws) && (
<button
type="button"
className="btn btn-sm"
title="Eintragen, dass du bei dieser Arbeit unterstützt hast"
style={headerButtonStyle}
onClick={(e) => {
e.stopPropagation()
onOwnSupport(ws)
}}
>
<FaHandsHelping className="me-1" />{' '}
{parseSupporters(ws.supporters).some((s) => s.employeeId === currentUserId)
? 'Meine Unterstützung bearbeiten'
: 'Ich habe unterstützt'}
</button>
)}
{onTogglePlan && !ws.isComment && Number(ws.totalTime) > 0 && (aboHoursAllowed || ws.countsToPlan) && (
<button
type="button"
className="btn btn-sm"
title={ws.countsToPlan ? 'Nicht mehr vom Abo-Stundenkonto abziehen' : 'Vom Abo-Stundenkonto des Kunden abziehen'}
style={{
background: ws.countsToPlan ? 'var(--violet-bg)' : 'var(--bg)',
color: ws.countsToPlan ? 'var(--violet-fg)' : 'var(--text)',
border: '1px solid var(--line-strong)',
borderRadius: '6px',
padding: '2px 10px',
fontSize: '0.8rem',
marginRight: '8px',
}}
onClick={(e) => {
e.stopPropagation()
onTogglePlan(ws)
}}
>
{ws.countsToPlan ? '✓ Abo' : '+ Abo'}
</button>
)}
<small style={{ opacity: 0.9 }}>
{formatDateTime(ws.startDate, ws.startTime) || formatDateTime(ws.endDate, ws.endTime)}
</small>
</div>
</div>
{/* Body - Nur wenn expanded */}
{isExpanded && (
<div
className="card-body p-4"
style={{
animation: 'slideDown 0.3s ease-out'
}}
<div className="ws-list">
{worksheets.map((ws) => {
const when = worksheetWhen(ws)
const kind = worksheetKindLabel(ws)
const badge = worksheetBadge(ws)
const lines = supportLines(ws)
const status = statusAfterLabel(ws)
const time = timeLabel(ws)
const canEdit = Boolean(onEditTimes && canEditTimes?.(ws))
const canSupport = Boolean(onOwnSupport && canAddSupport?.(ws))
const ownSupport = parseSupporters(ws.supporters).some((s) => s.employeeId === currentUserId)
const showAbo = Boolean(onTogglePlan && showAboToggle(ws, aboHoursAllowed))
const isNote = ws.isComment && !isGitWorksheet(ws)
return (
<div key={ws.$id} className={isNote ? 'ws comment' : 'ws'}>
<div className="ws-when">
<div className="d">{when.date || '–'}</div>
{when.time && <div className="num">{when.time}</div>}
</div>
<div>
<div className="ws-head">
<span className="who">{ws.employeeName}</span>
{ws.employeeShort && <span className="faint xs">{ws.employeeShort}</span>}
{kind && <span className="muted">{kind}</span>}
<span className="faint xs num">WSID {ws.wsid}</span>
{badge && <span className={`badge ${badge.tone}`}>{badge.text}</span>}
{showAbo ? (
<button
type="button"
className={ws.countsToPlan ? 'abo-toggle on' : 'abo-toggle'}
title={ws.countsToPlan ? 'Nicht mehr vom Abo-Stundenkonto abziehen' : 'Vom Abo-Stundenkonto des Kunden abziehen'}
onClick={() => onTogglePlan(ws)}
>
{/* Mitarbeiter & Zeit */}
<div className="row mb-3">
<div className="col-md-6">
<div className="d-flex align-items-center">
<FaUser className="me-2" style={{ color: 'var(--green-fg)' }} />
<strong>{ws.employeeName}</strong>
{ws.employeeShort && (
<span className="badge ms-2" style={{
background: 'var(--green-bg)',
color: 'var(--green-fg)'
}}>{ws.employeeShort}</span>
)}
</div>
</div>
<div className="col-md-6 text-md-end">
{!ws.isComment && (
<div className="d-flex align-items-center justify-content-md-end">
<FaClock className="me-2" style={{ color: 'var(--green-fg)' }} />
<strong className="fs-5" style={{ color: 'var(--green-fg)' }}>{formatTime(ws.totalTime)}</strong>
</div>
)}
</div>
</div>
{/* Unterstuetzung: je Person die Haelfte der Arbeitszeit, aufgerundet auf 15 Minuten */}
{supporters.length > 0 && (
<div className="mb-3 worksheet-supporters">
{supporters.map((sup) => (
<div key={sup.employeeId} style={{ fontSize: '0.9rem', color: 'var(--text-muted)' }}>
↳ Unterstützung {firstName(sup.name)} · {sup.task} · {formatTime(supportMinutes(ws.totalTime))}
</div>
))}
</div>
)}
{/* Service Type */}
<div className="mb-3">
<span className="badge px-3 py-2" style={{
background: 'var(--grey-bg)',
color: 'var(--grey-fg)',
fontSize: '0.85rem'
}}>{serviceTypeLabel(ws.serviceType)}</span>
</div>
{/* Status-Änderung */}
{ws.oldStatus !== ws.newStatus && (
<div className="mb-3 p-3 rounded-3" style={{
background: 'var(--bg-subtle)'
}}>
<FaExchangeAlt className="me-2" style={{ color: 'var(--green-fg)' }} />
<span className="text-muted">Status:</span>{' '}
<span className="badge" style={{ background: 'var(--grey-bg)', color: 'var(--grey-fg)' }}>{statusLabel(ws.oldStatus)}</span>
<span className="mx-2" style={{ color: 'var(--green-fg)' }}>→</span>
<span className="badge" style={{ background: 'var(--green-bg)', color: 'var(--green-fg)' }}>{statusLabel(ws.newStatus)}</span>
</div>
)}
{/* Details */}
<div className="mt-3 p-3 rounded-3" style={{
background: 'var(--bg-subtle)',
border: '1px solid var(--line)'
}}>
<small className="text-dark" style={{ whiteSpace: 'pre-wrap', lineHeight: '1.6' }}>
{ws.details}
</small>
</div>
</div>
{ws.countsToPlan ? 'Abo' : '+ Abo'}
</button>
) : (
ws.countsToPlan && !ws.isComment && <span className="muted xs">Abo</span>
)}
</div>
{ws.details && <div className="ws-text">{ws.details}</div>}
{lines.map((line, i) => (
<div key={i} className="ws-support">{line}</div>
))}
{status && <div className="ws-support">{status}</div>}
{(canEdit || canSupport) && (
<div className="ws-acts">
{canEdit && (
<button
type="button"
className="link"
title="Beginn, Ende und Arbeitszeit ändern"
onClick={() => onEditTimes(ws)}
>
{needsTimeEntry(ws) ? 'Zeit nachtragen' : 'Zeiten bearbeiten'}
</button>
)}
{canSupport && (
<button
type="button"
className="link"
title="Eintragen, dass du bei dieser Arbeit unterstützt hast"
onClick={() => onOwnSupport(ws)}
>
{ownSupport ? 'Meine Unterstützung bearbeiten' : 'Ich habe unterstützt'}
</button>
)}
</div>
)}
</div>
)
})}
</div>
<style>{`
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideDown {
from {
opacity: 0;
max-height: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
max-height: 1000px;
transform: translateY(0);
}
}
`}</style>
<div className="ws-time">
{time && <div className={time === '–' ? 't faint' : 't'}>{time}</div>}
</div>
</div>
)
})}
</div>
)
}