Arbeitsblätter: Zeiten nachträglich bearbeiten, nur für den Ersteller

Eigene Arbeitsblätter (employeeId = angemeldeter Benutzer, kein reiner
Kommentar) bekommen im Kopf den Button "Zeiten bearbeiten". Der Dialog
ändert Beginn, Ende und Arbeitszeit; die Minuten rechnen wie beim Anlegen
mit, von Hand eingetragene Minuten bleiben stehen. Gespeichert werden nur
die fünf Zeitfelder, wieder als dd.mm.yyyy / hhmm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-09-27 09:36:19 +00:00
1 parent e678899141
commit 149b38b94b
5 files changed
+219 -13

No files matched your search

+19 -2
View File
@@ -8,6 +8,7 @@ import EditorDropdown from './EditorDropdown'
import CreateWorksheetModal from './CreateWorksheetModal' import CreateWorksheetModal from './CreateWorksheetModal'
import StatusHistoryModal from './StatusHistoryModal' import StatusHistoryModal from './StatusHistoryModal'
import WorksheetList from './WorksheetList' import WorksheetList from './WorksheetList'
import WorksheetTimesModal from './WorksheetTimesModal'
import WorksheetStats from './WorksheetStats' import WorksheetStats from './WorksheetStats'
import TicketAssignedProjectsPanel from './TicketAssignedProjectsPanel' import TicketAssignedProjectsPanel from './TicketAssignedProjectsPanel'
import TicketProjectsModal from './TicketProjectsModal' import TicketProjectsModal from './TicketProjectsModal'
@@ -19,6 +20,7 @@ import Tabs from './ui/Tabs'
import Badge from './ui/Badge' import Badge from './ui/Badge'
import { StatusPill, PriorityPill } from './ui/StatusPill' import { StatusPill, PriorityPill } from './ui/StatusPill'
import { useWorksheets } from '../hooks/useWorksheets' import { useWorksheets } from '../hooks/useWorksheets'
import { useAuth } from '../context/AuthContext'
import { databases, DATABASE_ID, COLLECTIONS } from '../lib/appwrite' import { databases, DATABASE_ID, COLLECTIONS } from '../lib/appwrite'
import { serviceTypeLabel } from '../lib/ticketForm' import { serviceTypeLabel } from '../lib/ticketForm'
@@ -31,14 +33,16 @@ const TABS = [
] ]
export default function TicketCard({ ticket, onUpdate }) { export default function TicketCard({ ticket, onUpdate }) {
const { user } = useAuth()
const [expanded, setExpanded] = useState(false) const [expanded, setExpanded] = useState(false)
const [activeTab, setActiveTab] = useState('details') const [activeTab, setActiveTab] = useState('details')
const [showCreateWorksheet, setShowCreateWorksheet] = useState(false) const [showCreateWorksheet, setShowCreateWorksheet] = useState(false)
const [editTimesWorksheet, setEditTimesWorksheet] = useState(null)
const [showHistoryModal, setShowHistoryModal] = useState(false) const [showHistoryModal, setShowHistoryModal] = useState(false)
const [showProjectsModal, setShowProjectsModal] = useState(false) const [showProjectsModal, setShowProjectsModal] = useState(false)
const [projectsRefreshKey, setProjectsRefreshKey] = useState(0) const [projectsRefreshKey, setProjectsRefreshKey] = useState(0)
const { worksheets, loading: worksheetsLoading, createWorksheet, getTotalTime } = useWorksheets( const { worksheets, loading: worksheetsLoading, createWorksheet, updateWorksheetTimes, getTotalTime } = useWorksheets(
expanded ? ticket.woid : null expanded ? ticket.woid : null
) )
@@ -140,7 +144,13 @@ export default function TicketCard({ ticket, onUpdate }) {
<WorksheetStats worksheets={worksheets} /> <WorksheetStats worksheets={worksheets} />
</div> </div>
)} )}
<WorksheetList worksheets={worksheets} totalTime={getTotalTime()} loading={worksheetsLoading} /> <WorksheetList
worksheets={worksheets}
totalTime={getTotalTime()}
loading={worksheetsLoading}
currentUser={user}
onEditTimes={setEditTimesWorksheet}
/>
</div> </div>
)} )}
@@ -160,6 +170,13 @@ export default function TicketCard({ ticket, onUpdate }) {
)} )}
<CreateWorksheetModal isOpen={showCreateWorksheet} onClose={() => setShowCreateWorksheet(false)} workorder={ticket} onCreate={handleCreateWorksheet} /> <CreateWorksheetModal isOpen={showCreateWorksheet} onClose={() => setShowCreateWorksheet(false)} workorder={ticket} onCreate={handleCreateWorksheet} />
{editTimesWorksheet && (
<WorksheetTimesModal
worksheet={editTimesWorksheet}
onClose={() => setEditTimesWorksheet(null)}
onSave={(ws, times) => updateWorksheetTimes(ws, times, user)}
/>
)}
<StatusHistoryModal isOpen={showHistoryModal} onClose={() => setShowHistoryModal(false)} worksheets={worksheets} ticket={ticket} /> <StatusHistoryModal isOpen={showHistoryModal} onClose={() => setShowHistoryModal(false)} worksheets={worksheets} ticket={ticket} />
<TicketProjectsModal isOpen={showProjectsModal} onClose={() => setShowProjectsModal(false)} ticket={ticket} onUpdated={() => setProjectsRefreshKey((k) => k + 1)} /> <TicketProjectsModal isOpen={showProjectsModal} onClose={() => setShowProjectsModal(false)} ticket={ticket} onUpdated={() => setProjectsRefreshKey((k) => k + 1)} />
</div> </div>
+28 -11
View File
@@ -1,12 +1,22 @@
import { useState } from 'react' import { useState } from 'react'
import { FaClock, FaUser, FaExchangeAlt, FaComment, FaChevronDown, FaChevronUp } from 'react-icons/fa' import { FaClock, FaUser, FaExchangeAlt, FaComment, FaChevronDown, FaChevronUp, FaPen } from 'react-icons/fa'
import { serviceTypeLabel } from '../lib/ticketForm' import { serviceTypeLabel, canEditWorksheetTimes } from '../lib/ticketForm'
function needsTimeEntry(ws) { function needsTimeEntry(ws) {
return ws.serviceType === 'GIT' && (!ws.startTime || !Number(ws.totalTime)) return ws.serviceType === 'GIT' && (!ws.startTime || !Number(ws.totalTime))
} }
export default function WorksheetList({ worksheets, totalTime, loading, onEditTime, onTogglePlan }) { const headerButtonStyle = {
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',
marginRight: '8px',
}
export default function WorksheetList({ worksheets, totalTime, loading, onEditTime, onTogglePlan, currentUser, onEditTimes }) {
const [expandedWorksheets, setExpandedWorksheets] = useState({}) const [expandedWorksheets, setExpandedWorksheets] = useState({})
if (loading) { if (loading) {
return ( return (
@@ -157,14 +167,7 @@ export default function WorksheetList({ worksheets, totalTime, loading, onEditTi
<button <button
type="button" type="button"
className="btn btn-sm" className="btn btn-sm"
style={{ style={headerButtonStyle}
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) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onEditTime(ws) onEditTime(ws)
@@ -173,6 +176,20 @@ export default function WorksheetList({ worksheets, totalTime, loading, onEditTi
<FaClock className="me-1" /> Zeit nachtragen <FaClock className="me-1" /> Zeit nachtragen
</button> </button>
)} )}
{onEditTimes && canEditWorksheetTimes(ws, currentUser) && (
<button
type="button"
className="btn btn-sm"
title="Beginn, Ende und Arbeitszeit ändern"
style={headerButtonStyle}
onClick={(e) => {
e.stopPropagation()
onEditTimes(ws)
}}
>
<FaPen className="me-1" /> Zeiten bearbeiten
</button>
)}
{onTogglePlan && !ws.isComment && Number(ws.totalTime) > 0 && ( {onTogglePlan && !ws.isComment && Number(ws.totalTime) > 0 && (
<button <button
type="button" type="button"
+140
View File
@@ -0,0 +1,140 @@
import { useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { isoToGermanDate, timeToWoms, germanDateToIso, womsTimeToInput, minutesBetween } from '../lib/ticketForm'
import { DateTimeInput } from './ui'
// Gespeichert ist dd.mm.yyyy / hhmm, die Kalender-Felder brauchen yyyy-mm-dd / hh:mm
const formFromWorksheet = (worksheet) => ({
startDate: germanDateToIso(worksheet.startDate),
startTime: womsTimeToInput(worksheet.startTime),
endDate: germanDateToIso(worksheet.endDate || worksheet.startDate),
endTime: womsTimeToInput(worksheet.endTime)
})
/**
* Beginn, Ende und Arbeitszeit eines Arbeitsblatts nachtraeglich aendern.
* Wer das darf, entscheidet canEditWorksheetTimes (useWorksheets).
*/
export default function WorksheetTimesModal({ worksheet, onClose, onSave }) {
const [formData, setFormData] = useState(() => formFromWorksheet(worksheet))
// null = die Arbeitszeit folgt Beginn und Ende. Passte sie schon vorher nicht dazu,
// wurde sie von Hand eingetragen (z.B. ohne Pause) und bleibt stehen.
const [manualTotal, setManualTotal] = useState(() => {
const stored = Number(worksheet.totalTime) || 0
return !stored || stored === minutesBetween(formData.startTime, formData.endTime) ? null : stored
})
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
const calculatedTotal = minutesBetween(formData.startTime, formData.endTime)
const totalTime = manualTotal ?? calculatedTotal ?? 0
const handleChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }))
}
// Das Ende darf nicht vor dem Start liegen, deshalb rueckt es mit
const handleStartDateChange = (value) => {
setFormData(prev => ({
...prev,
startDate: value,
endDate: value && prev.endDate && prev.endDate < value ? value : prev.endDate
}))
}
const handleSubmit = async (e) => {
e.preventDefault()
setSaving(true)
setError('')
try {
const result = await onSave(worksheet, {
startDate: isoToGermanDate(formData.startDate),
startTime: timeToWoms(formData.startTime),
endDate: isoToGermanDate(formData.endDate),
endTime: timeToWoms(formData.endTime),
totalTime: parseInt(totalTime, 10) || 0
})
if (result?.success) {
onClose()
return
}
setError(result?.error || 'Speichern fehlgeschlagen')
} catch (err) {
setError(err.message || 'Speichern fehlgeschlagen')
}
setSaving(false)
}
return (
<div className="overlay">
<span className="overlay-close" onClick={onClose}>
<FaTimes />
</span>
<div className="overlay-content" style={{ maxWidth: 560 }}>
{/* Kurzer Titel: auf dem Handy laeuft ein langer unter das Schliessen-X */}
<h2 className="mb-2">Zeiten bearbeiten</h2>
<p className="text-grey" style={{ fontSize: '14px', marginBottom: '16px' }}>WSID {worksheet.wsid}</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" htmlFor="worksheet-times-start">Beginn</label>
<DateTimeInput
id="worksheet-times-start"
timeLabel="Uhrzeit Beginn"
required
date={formData.startDate}
time={formData.startTime}
onDateChange={handleStartDateChange}
onTimeChange={(value) => handleChange('startTime', value)}
/>
</div>
<div className="form-group">
<label className="form-label" htmlFor="worksheet-times-end">Ende</label>
<DateTimeInput
id="worksheet-times-end"
timeLabel="Uhrzeit Ende"
required
min={formData.startDate}
date={formData.endDate}
time={formData.endTime}
onDateChange={(value) => handleChange('endDate', value)}
onTimeChange={(value) => handleChange('endTime', value)}
/>
</div>
<div className="form-group">
<label className="form-label" htmlFor="worksheet-times-total">Arbeitszeit (Minuten)</label>
<input
id="worksheet-times-total"
type="number"
className="form-control"
min="0"
step="1"
value={totalTime}
onChange={(e) => setManualTotal(e.target.value)}
placeholder="0"
/>
<small style={{ color: '#a0aec0', fontSize: '12px' }}>
{manualTotal === null && calculatedTotal !== null
? '✓ Automatisch berechnet'
: 'Manuell eingeben'}
</small>
</div>
<div className="text-center mt-2">
<button type="submit" className="btn btn-dark" disabled={saving}>
{saving ? 'Speichert...' : 'Speichern'}
</button>
</div>
</form>
</div>
</div>
)
}
+13
View File
@@ -1,5 +1,6 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { databases, DATABASE_ID, COLLECTIONS, Query, ID, isDemoMode } from '../lib/appwrite' import { databases, DATABASE_ID, COLLECTIONS, Query, ID, isDemoMode } from '../lib/appwrite'
import { canEditWorksheetTimes } from '../lib/ticketForm'
const DEMO_MODE = isDemoMode const DEMO_MODE = isDemoMode
@@ -388,6 +389,17 @@ export function useWorksheets(woid = null) {
} }
} }
/**
* Aendert nachtraeglich Beginn, Ende und Arbeitszeit - nur fuer den Ersteller
*/
const updateWorksheetTimes = async (worksheet, times, currentUser) => {
if (!canEditWorksheetTimes(worksheet, currentUser)) {
return { success: false, error: 'Nur wer das Arbeitsblatt erstellt hat, kann die Zeiten ändern.' }
}
const { startDate, startTime, endDate, endTime, totalTime } = times
return updateWorksheet(worksheet.$id, { startDate, startTime, endDate, endTime, totalTime })
}
/** /**
* Löscht ein Worksheet (sollte normalerweise nicht erlaubt sein - Audit Trail!) * Löscht ein Worksheet (sollte normalerweise nicht erlaubt sein - Audit Trail!)
*/ */
@@ -468,6 +480,7 @@ export function useWorksheets(woid = null) {
error, error,
createWorksheet, createWorksheet,
updateWorksheet, updateWorksheet,
updateWorksheetTimes,
deleteWorksheet, deleteWorksheet,
refresh: fetchWorksheets, refresh: fetchWorksheets,
getTotalTime, getTotalTime,
+19
View File
@@ -3,6 +3,8 @@
(siehe planboard.js). Die Kalender-Felder des Browsers liefern dagegen (siehe planboard.js). Die Kalender-Felder des Browsers liefern dagegen
"yyyy-mm-dd" bzw. "hh:mm" - umgerechnet wird erst beim Speichern. */ "yyyy-mm-dd" bzw. "hh:mm" - umgerechnet wird erst beim Speichern. */
import { parseGermanDate, formatTime, dayKey } from './planboard'
// Service Type: gespeichert werden die bisherigen Werte, angezeigt wird Deutsch // Service Type: gespeichert werden die bisherigen Werte, angezeigt wird Deutsch
export const SERVICE_TYPES = [ export const SERVICE_TYPES = [
{ value: 'Remote', label: 'Fernwartung (remote)' }, { value: 'Remote', label: 'Fernwartung (remote)' },
@@ -21,6 +23,12 @@ export function serviceTypeLabel(value) {
return SERVICE_TYPE_SHORT[String(value || '').trim().toLowerCase()] || value return SERVICE_TYPE_SHORT[String(value || '').trim().toLowerCase()] || value
} }
// Zeiten eines Arbeitsblatts darf nur aendern, wer es angelegt hat.
// Reine Kommentare haben keine Arbeitszeit und bleiben aussen vor.
export function canEditWorksheetTimes(worksheet, user) {
return Boolean(user?.$id) && worksheet?.employeeId === user.$id && !worksheet.isComment
}
const pad = (n) => String(n).padStart(2, '0') const pad = (n) => String(n).padStart(2, '0')
// Heutiges Datum in Ortszeit als "yyyy-mm-dd" (Wert fuer <input type="date">) // Heutiges Datum in Ortszeit als "yyyy-mm-dd" (Wert fuer <input type="date">)
@@ -40,6 +48,17 @@ export function timeToWoms(value) {
return m ? `${m[1].padStart(2, '0')}${m[2]}` : '' return m ? `${m[1].padStart(2, '0')}${m[2]}` : ''
} }
// Rueckweg fuers Bearbeiten: "26.09.2026" (auch "2.5.2026") -> "2026-09-26", sonst ""
export function germanDateToIso(value) {
const date = parseGermanDate(value)
return date ? dayKey(date) : ''
}
// "0930" -> "09:30", sonst ""
export function womsTimeToInput(value) {
return formatTime(value)
}
// Minuten von "hh:mm" bis "hh:mm"; liegt das Ende davor, geht es ueber Mitternacht // Minuten von "hh:mm" bis "hh:mm"; liegt das Ende davor, geht es ueber Mitternacht
export function minutesBetween(start, end) { export function minutesBetween(start, end) {
const a = /^(\d{1,2}):(\d{2})/.exec(String(start || '')) const a = /^(\d{1,2}):(\d{2})/.exec(String(start || ''))