feat: Zeit nachtragen fuer Git-Push-Worksheets

Git-Pushes erzeugen Worksheets mit automatischer Endzeit (Push-Zeitpunkt),
aber ohne Startzeit. Neu:
- WorksheetList: Pille 'Zeit offen' + Button 'Zeit nachtragen' bei
  GIT-Worksheets ohne Startzeit/Arbeitszeit
- EditWorksheetTimeModal: Startdatum/-zeit erfassen, Arbeitszeit wird live
  berechnet (inkl. Uebernacht-Logik), speichert via updateWorksheet und setzt
  isComment=false, damit der Eintrag in der Arbeitszeit-Statistik zaehlt

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 18:10:10 +00:00
parent 4be20237a1
commit ff536b048f
3 changed files with 195 additions and 7 deletions

View File

@@ -0,0 +1,142 @@
import { useEffect, useMemo, useState } from 'react'
import { FaClock } from 'react-icons/fa'
/** Minuten zwischen hhmm-Zeiten, Übernacht (+24h) wie useWorksheets.calculateTime. */
function calculateMinutes(startTime, endTime) {
if (!startTime || !endTime) return null
const sh = parseInt(startTime.substring(0, 2), 10)
const sm = parseInt(startTime.substring(2, 4), 10)
const eh = parseInt(endTime.substring(0, 2), 10)
const em = parseInt(endTime.substring(2, 4), 10)
if ([sh, sm, eh, em].some(Number.isNaN)) return null
let diff = eh * 60 + em - (sh * 60 + sm)
if (diff < 0) diff += 24 * 60
return diff
}
function formatMinutes(minutes) {
if (minutes === null || minutes === undefined) return ''
const h = Math.floor(minutes / 60)
const m = minutes % 60
return h > 0 ? `${h}h ${m}min` : `${m}min`
}
/**
* Startzeit fuer Git-Push-Worksheets nachtragen: Die Endzeit wurde beim Push
* automatisch gesetzt, hier wird der Arbeitsbeginn erfasst. Beim Speichern
* wird totalTime berechnet und isComment auf false gesetzt, damit der Eintrag
* in der Arbeitszeit-Statistik zaehlt.
*/
export default function EditWorksheetTimeModal({ isOpen, onClose, worksheet, onSave }) {
const [startDate, setStartDate] = useState('')
const [startTime, setStartTime] = useState('')
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
useEffect(() => {
if (isOpen && worksheet) {
setStartDate(worksheet.startDate || worksheet.endDate || '')
setStartTime(worksheet.startTime || '')
setError('')
}
}, [isOpen, worksheet])
const totalTime = useMemo(
() => calculateMinutes(startTime, worksheet?.endTime),
[startTime, worksheet]
)
if (!isOpen || !worksheet) return null
const handleSubmit = async (e) => {
e.preventDefault()
if (!/^\d{2}\.\d{2}\.\d{4}$/.test(startDate)) {
setError('Startdatum bitte als TT.MM.JJJJ angeben.')
return
}
if (!/^\d{4}$/.test(startTime)) {
setError('Startzeit bitte als HHMM angeben (z.B. 0930).')
return
}
if (totalTime === null) {
setError('Zeit konnte nicht berechnet werden.')
return
}
setSaving(true)
setError('')
const result = await onSave(worksheet, {
startDate,
startTime,
totalTime,
isComment: false,
})
setSaving(false)
if (result?.success) {
onClose()
} else {
setError(result?.error || 'Speichern fehlgeschlagen')
}
}
return (
<div className="overlay">
<span className="overlay-close" onClick={onClose}>
×
</span>
<div className="overlay-content">
<h2 className="mb-2">
<FaClock style={{ marginRight: 8 }} />
Zeit nachtragen WSID {worksheet.wsid}
</h2>
<p className="text-grey" style={{ fontSize: '14px' }}>
Die Endzeit wurde beim Git-Push automatisch gesetzt
({worksheet.endDate} {worksheet.endTime ? `${worksheet.endTime.substring(0, 2)}:${worksheet.endTime.substring(2, 4)}` : ''}).
Trage hier ein, wann die Arbeit begonnen hat.
</p>
{error && (
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
{error}
</div>
)}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label className="form-label">Startdatum (TT.MM.JJJJ)</label>
<input
type="text"
className="form-control"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
placeholder="11.07.2026"
required
/>
</div>
<div className="form-group">
<label className="form-label">Startzeit (HHMM)</label>
<input
type="text"
className="form-control"
value={startTime}
onChange={(e) => setStartTime(e.target.value.replace(/[^0-9]/g, '').slice(0, 4))}
placeholder="0930"
maxLength={4}
required
/>
</div>
<div className="form-group">
<label className="form-label">Berechnete Arbeitszeit</label>
<div style={{ fontSize: '18px', fontWeight: 600, color: '#10b981' }}>
{formatMinutes(totalTime)}
</div>
</div>
<div className="text-center mt-2">
<button type="submit" className="btn btn-green" disabled={saving}>
{saving ? 'Speichert...' : 'Zeit speichern'}
</button>
</div>
</form>
</div>
</div>
)
}

View File

@@ -7,6 +7,7 @@ import PriorityDropdown from './PriorityDropdown'
import EditorDropdown from './EditorDropdown'
import ResponseDropdown from './ResponseDropdown'
import CreateWorksheetModal from './CreateWorksheetModal'
import EditWorksheetTimeModal from './EditWorksheetTimeModal'
import StatusHistoryModal from './StatusHistoryModal'
import WorksheetList from './WorksheetList'
import WorksheetStats from './WorksheetStats'
@@ -52,6 +53,7 @@ export default function TicketRow({ ticket, onUpdate, onExpand }) {
const [locked, setLocked] = useState(true)
const [activeTab, setActiveTab] = useState('details')
const [showCreateWorksheet, setShowCreateWorksheet] = useState(false)
const [editTimeWorksheet, setEditTimeWorksheet] = useState(null)
const [showHistoryModal, setShowHistoryModal] = useState(false)
const [showProjectsModal, setShowProjectsModal] = useState(false)
const [projectsRefreshKey, setProjectsRefreshKey] = useState(0)
@@ -60,9 +62,13 @@ export default function TicketRow({ ticket, onUpdate, onExpand }) {
worksheets,
loading: worksheetsLoading,
createWorksheet,
updateWorksheet,
getTotalTime
} = useWorksheets(expanded ? ticket.woid : null)
// Startzeit fuer Git-Push-Worksheets nachtragen (Endzeit kam vom Push)
const handleSaveWorksheetTime = async (ws, data) => updateWorksheet(ws.$id, data)
const createdAt = new Date(ticket.$createdAt || ticket.createdAt)
const elapsed = formatDistanceToNow(createdAt, { locale: de })
@@ -237,7 +243,7 @@ export default function TicketRow({ ticket, onUpdate, onExpand }) {
</div>
)}
<WorksheetList worksheets={worksheets} totalTime={getTotalTime()} loading={worksheetsLoading} />
<WorksheetList worksheets={worksheets} totalTime={getTotalTime()} loading={worksheetsLoading} onEditTime={setEditTimeWorksheet} />
</div>
)}
@@ -272,6 +278,13 @@ export default function TicketRow({ ticket, onUpdate, onExpand }) {
onCreate={handleCreateWorksheet}
/>
<EditWorksheetTimeModal
isOpen={Boolean(editTimeWorksheet)}
onClose={() => setEditTimeWorksheet(null)}
worksheet={editTimeWorksheet}
onSave={handleSaveWorksheetTime}
/>
<StatusHistoryModal
isOpen={showHistoryModal}
onClose={() => setShowHistoryModal(false)}

View File

@@ -1,7 +1,11 @@
import { useState } from 'react'
import { FaClock, FaUser, FaExchangeAlt, FaComment, FaChevronDown, FaChevronUp } from 'react-icons/fa'
export default function WorksheetList({ worksheets, totalTime, loading }) {
function needsTimeEntry(ws) {
return ws.serviceType === 'GIT' && (!ws.startTime || !Number(ws.totalTime))
}
export default function WorksheetList({ worksheets, totalTime, loading, onEditTime }) {
const [expandedWorksheets, setExpandedWorksheets] = useState({})
if (loading) {
return (
@@ -105,13 +109,20 @@ export default function WorksheetList({ worksheets, totalTime, loading }) {
)}
<div>
<strong className="fs-6">WSID {ws.wsid}</strong>
{ws.isComment && (
<span className="badge ms-2" style={{
{ws.isComment && !isGit && (
<span className="badge ms-2" style={{
background: 'rgba(255,255,255,0.3)'
}}>
<FaComment className="me-1" /> Kommentar
</span>
)}
{needsTimeEntry(ws) && (
<span className="badge ms-2" style={{
background: 'rgba(255,255,255,0.3)'
}}>
<FaClock className="me-1" /> Zeit offen
</span>
)}
</div>
{/* Collapsed: Mitarbeiter & Zeit im Header */}
{!isExpanded && (
@@ -133,9 +144,31 @@ export default function WorksheetList({ worksheets, totalTime, loading }) {
</div>
)}
</div>
<small style={{ opacity: 0.9 }}>
{formatDateTime(ws.startDate, ws.startTime)}
</small>
<div className="d-flex align-items-center gap-2">
{needsTimeEntry(ws) && onEditTime && (
<button
type="button"
className="btn btn-sm"
style={{
background: 'rgba(255,255,255,0.25)',
color: 'white',
border: '1px solid rgba(255,255,255,0.4)',
borderRadius: '6px',
padding: '2px 10px',
fontSize: '0.8rem',
}}
onClick={(e) => {
e.stopPropagation()
onEditTime(ws)
}}
>
<FaClock className="me-1" /> Zeit nachtragen
</button>
)}
<small style={{ opacity: 0.9 }}>
{formatDateTime(ws.startDate, ws.startTime) || formatDateTime(ws.endDate, ws.endTime)}
</small>
</div>
</div>
{/* Body - Nur wenn expanded */}