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 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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 { 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,
|
||||||
|
|||||||
@@ -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 || ''))
|
||||||
|
|||||||
Reference in new issue
Block a user