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:
142
src/components/EditWorksheetTimeModal.jsx
Normal file
142
src/components/EditWorksheetTimeModal.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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)}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user