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:
1 parent
e678899141
commit
149b38b94b
5 files changed
+219
-13
No files matched your search
@@ -8,6 +8,7 @@ import EditorDropdown from './EditorDropdown'
|
||||
import CreateWorksheetModal from './CreateWorksheetModal'
|
||||
import StatusHistoryModal from './StatusHistoryModal'
|
||||
import WorksheetList from './WorksheetList'
|
||||
import WorksheetTimesModal from './WorksheetTimesModal'
|
||||
import WorksheetStats from './WorksheetStats'
|
||||
import TicketAssignedProjectsPanel from './TicketAssignedProjectsPanel'
|
||||
import TicketProjectsModal from './TicketProjectsModal'
|
||||
@@ -19,6 +20,7 @@ import Tabs from './ui/Tabs'
|
||||
import Badge from './ui/Badge'
|
||||
import { StatusPill, PriorityPill } from './ui/StatusPill'
|
||||
import { useWorksheets } from '../hooks/useWorksheets'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { databases, DATABASE_ID, COLLECTIONS } from '../lib/appwrite'
|
||||
import { serviceTypeLabel } from '../lib/ticketForm'
|
||||
|
||||
@@ -31,14 +33,16 @@ const TABS = [
|
||||
]
|
||||
|
||||
export default function TicketCard({ ticket, onUpdate }) {
|
||||
const { user } = useAuth()
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState('details')
|
||||
const [showCreateWorksheet, setShowCreateWorksheet] = useState(false)
|
||||
const [editTimesWorksheet, setEditTimesWorksheet] = useState(null)
|
||||
const [showHistoryModal, setShowHistoryModal] = useState(false)
|
||||
const [showProjectsModal, setShowProjectsModal] = useState(false)
|
||||
const [projectsRefreshKey, setProjectsRefreshKey] = useState(0)
|
||||
|
||||
const { worksheets, loading: worksheetsLoading, createWorksheet, getTotalTime } = useWorksheets(
|
||||
const { worksheets, loading: worksheetsLoading, createWorksheet, updateWorksheetTimes, getTotalTime } = useWorksheets(
|
||||
expanded ? ticket.woid : null
|
||||
)
|
||||
|
||||
@@ -140,7 +144,13 @@ export default function TicketCard({ ticket, onUpdate }) {
|
||||
<WorksheetStats worksheets={worksheets} />
|
||||
</div>
|
||||
)}
|
||||
<WorksheetList worksheets={worksheets} totalTime={getTotalTime()} loading={worksheetsLoading} />
|
||||
<WorksheetList
|
||||
worksheets={worksheets}
|
||||
totalTime={getTotalTime()}
|
||||
loading={worksheetsLoading}
|
||||
currentUser={user}
|
||||
onEditTimes={setEditTimesWorksheet}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -160,6 +170,13 @@ export default function TicketCard({ ticket, onUpdate }) {
|
||||
)}
|
||||
|
||||
<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} />
|
||||
<TicketProjectsModal isOpen={showProjectsModal} onClose={() => setShowProjectsModal(false)} ticket={ticket} onUpdated={() => setProjectsRefreshKey((k) => k + 1)} />
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import { useState } from 'react'
|
||||
import { FaClock, FaUser, FaExchangeAlt, FaComment, FaChevronDown, FaChevronUp } from 'react-icons/fa'
|
||||
import { serviceTypeLabel } from '../lib/ticketForm'
|
||||
import { FaClock, FaUser, FaExchangeAlt, FaComment, FaChevronDown, FaChevronUp, FaPen } from 'react-icons/fa'
|
||||
import { serviceTypeLabel, canEditWorksheetTimes } from '../lib/ticketForm'
|
||||
|
||||
function needsTimeEntry(ws) {
|
||||
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({})
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -157,14 +167,7 @@ export default function WorksheetList({ worksheets, totalTime, loading, onEditTi
|
||||
<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',
|
||||
}}
|
||||
style={headerButtonStyle}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onEditTime(ws)
|
||||
@@ -173,6 +176,20 @@ export default function WorksheetList({ worksheets, totalTime, loading, onEditTi
|
||||
<FaClock className="me-1" /> Zeit nachtragen
|
||||
</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 && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { databases, DATABASE_ID, COLLECTIONS, Query, ID, isDemoMode } from '../lib/appwrite'
|
||||
import { canEditWorksheetTimes } from '../lib/ticketForm'
|
||||
|
||||
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!)
|
||||
*/
|
||||
@@ -468,6 +480,7 @@ export function useWorksheets(woid = null) {
|
||||
error,
|
||||
createWorksheet,
|
||||
updateWorksheet,
|
||||
updateWorksheetTimes,
|
||||
deleteWorksheet,
|
||||
refresh: fetchWorksheets,
|
||||
getTotalTime,
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
(siehe planboard.js). Die Kalender-Felder des Browsers liefern dagegen
|
||||
"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
|
||||
export const SERVICE_TYPES = [
|
||||
{ value: 'Remote', label: 'Fernwartung (remote)' },
|
||||
@@ -21,6 +23,12 @@ export function serviceTypeLabel(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')
|
||||
|
||||
// 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]}` : ''
|
||||
}
|
||||
|
||||
// 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
|
||||
export function minutesBetween(start, end) {
|
||||
const a = /^(\d{1,2}):(\d{2})/.exec(String(start || ''))
|
||||
|
||||
Reference in new issue
Block a user