Arbeitszeit nur in 15-Minuten-Schritten, berechnete Zeit wird aufgerundet
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
32bc3d7c3d
commit
3535f18a2b
5 files changed
+70
-37
No files matched your search
@@ -3,7 +3,8 @@ import { FaTimes } from 'react-icons/fa'
|
|||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
import { SERVICE_TYPES, todayIso, isoToGermanDate, timeToWoms, minutesBetween } from '../lib/ticketForm'
|
import { SERVICE_TYPES, todayIso, isoToGermanDate, timeToWoms, minutesBetween } from '../lib/ticketForm'
|
||||||
import { TICKET_STATUSES, statusLabel } from '../lib/ticketStatus'
|
import { TICKET_STATUSES, statusLabel } from '../lib/ticketStatus'
|
||||||
import { DateTimeInput } from './ui'
|
import { DateTimeInput, DurationSelect } from './ui'
|
||||||
|
import { roundUpToStep, isValidStep } from '../lib/timeSteps'
|
||||||
|
|
||||||
// Akquise-Tickets: Ergebnis des Kundenkontakts (statt technischer Status)
|
// Akquise-Tickets: Ergebnis des Kundenkontakts (statt technischer Status)
|
||||||
const ACQUISITION_STATUS = ['In Kontakt', 'Zugesagt', 'Abgesagt']
|
const ACQUISITION_STATUS = ['In Kontakt', 'Zugesagt', 'Abgesagt']
|
||||||
@@ -57,12 +58,12 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
}
|
}
|
||||||
}, [isOpen, workorder, defaultStatus])
|
}, [isOpen, workorder, defaultStatus])
|
||||||
|
|
||||||
// Automatische Zeitberechnung
|
// Automatische Zeitberechnung, aufgerundet auf 15 Minuten
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!autoCalculate || formData.isComment) return
|
if (!autoCalculate || formData.isComment) return
|
||||||
const minutes = minutesBetween(formData.startTime, formData.endTime)
|
const minutes = minutesBetween(formData.startTime, formData.endTime)
|
||||||
if (minutes !== null) {
|
if (minutes !== null) {
|
||||||
setFormData(prev => ({ ...prev, totalTime: minutes }))
|
setFormData(prev => ({ ...prev, totalTime: roundUpToStep(minutes) }))
|
||||||
}
|
}
|
||||||
}, [formData.startTime, formData.endTime, formData.isComment, autoCalculate])
|
}, [formData.startTime, formData.endTime, formData.isComment, autoCalculate])
|
||||||
|
|
||||||
@@ -95,6 +96,11 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!formData.isComment && !isValidStep(formData.totalTime)) {
|
||||||
|
setError('Bitte die Arbeitszeit auswählen.')
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const worksheetData = {
|
const worksheetData = {
|
||||||
woid: workorder.woid,
|
woid: workorder.woid,
|
||||||
@@ -102,7 +108,7 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
serviceType: formData.serviceType,
|
serviceType: formData.serviceType,
|
||||||
oldStatus: workorder.status,
|
oldStatus: workorder.status,
|
||||||
newStatus: formData.newStatus,
|
newStatus: formData.newStatus,
|
||||||
totalTime: formData.isComment ? 0 : parseInt(formData.totalTime) || 0,
|
totalTime: formData.isComment ? 0 : Number(formData.totalTime),
|
||||||
startDate: isoToGermanDate(formData.startDate),
|
startDate: isoToGermanDate(formData.startDate),
|
||||||
startTime: timeToWoms(formData.startTime),
|
startTime: timeToWoms(formData.startTime),
|
||||||
endDate: isoToGermanDate(formData.endDate),
|
endDate: isoToGermanDate(formData.endDate),
|
||||||
@@ -235,22 +241,17 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">Arbeitszeit (Minuten)</label>
|
<label className="form-label" htmlFor="worksheet-total">Arbeitszeit</label>
|
||||||
{/* Ganze Minuten: mit step 15 blockierte der Browser berechnete Zeiten wie 70 */}
|
<DurationSelect
|
||||||
<input
|
id="worksheet-total"
|
||||||
type="number"
|
value={formData.isComment ? 0 : formData.totalTime}
|
||||||
className="form-control"
|
onChange={(value) => handleChange('totalTime', value)}
|
||||||
min="0"
|
|
||||||
step="1"
|
|
||||||
value={formData.totalTime}
|
|
||||||
onChange={(e) => handleChange('totalTime', e.target.value)}
|
|
||||||
disabled={formData.isComment}
|
disabled={formData.isComment}
|
||||||
placeholder="0"
|
|
||||||
/>
|
/>
|
||||||
<small style={{ color: '#a0aec0', fontSize: '12px' }}>
|
<small style={{ color: '#a0aec0', fontSize: '12px' }}>
|
||||||
{autoCalculate && formData.startTime && formData.endTime
|
{autoCalculate && formData.startTime && formData.endTime
|
||||||
? '✓ Automatisch berechnet'
|
? '✓ Aus Beginn und Ende berechnet, aufgerundet auf 15 Minuten'
|
||||||
: 'Manuell eingeben'}
|
: autoCalculate ? 'Beginn und Ende eintragen oder Zeit wählen' : 'Von Hand gewählt'}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { FaTimes } from 'react-icons/fa'
|
import { FaTimes } from 'react-icons/fa'
|
||||||
import { isoToGermanDate, timeToWoms, germanDateToIso, womsTimeToInput, minutesBetween, isGitWorksheet, needsTimeEntry } from '../lib/ticketForm'
|
import { isoToGermanDate, timeToWoms, germanDateToIso, womsTimeToInput, minutesBetween, isGitWorksheet, needsTimeEntry } from '../lib/ticketForm'
|
||||||
import { DateTimeInput } from './ui'
|
import { DateTimeInput, DurationSelect } from './ui'
|
||||||
|
import { roundUpToStep, isValidStep } from '../lib/timeSteps'
|
||||||
|
|
||||||
// Gespeichert ist dd.mm.yyyy / hhmm, die Kalender-Felder brauchen yyyy-mm-dd / hh:mm
|
// Gespeichert ist dd.mm.yyyy / hhmm, die Kalender-Felder brauchen yyyy-mm-dd / hh:mm
|
||||||
const formFromWorksheet = (worksheet) => ({
|
const formFromWorksheet = (worksheet) => ({
|
||||||
@@ -18,16 +19,20 @@ const formFromWorksheet = (worksheet) => ({
|
|||||||
export default function WorksheetTimesModal({ worksheet, onClose, onSave }) {
|
export default function WorksheetTimesModal({ worksheet, onClose, onSave }) {
|
||||||
const isGit = isGitWorksheet(worksheet)
|
const isGit = isGitWorksheet(worksheet)
|
||||||
const [formData, setFormData] = useState(() => formFromWorksheet(worksheet))
|
const [formData, setFormData] = useState(() => formFromWorksheet(worksheet))
|
||||||
// null = die Arbeitszeit folgt Beginn und Ende. Passte sie schon vorher nicht dazu,
|
// null = die Arbeitszeit folgt Beginn und Ende (aufgerundet auf 15 Minuten). Passte sie
|
||||||
// wurde sie von Hand eingetragen (z.B. ohne Pause) und bleibt stehen.
|
// schon vorher nicht dazu, wurde sie von Hand gewaehlt (z.B. ohne Pause) und bleibt stehen.
|
||||||
|
// Alte Zeiten ausserhalb des Rasters (z.B. 70) werden auf den naechsten Schritt aufgerundet.
|
||||||
const [manualTotal, setManualTotal] = useState(() => {
|
const [manualTotal, setManualTotal] = useState(() => {
|
||||||
const stored = Number(worksheet.totalTime) || 0
|
const stored = Number(worksheet.totalTime) || 0
|
||||||
return !stored || stored === minutesBetween(formData.startTime, formData.endTime) ? null : stored
|
const calculated = minutesBetween(formData.startTime, formData.endTime)
|
||||||
|
if (!stored || (calculated !== null && stored === roundUpToStep(calculated))) return null
|
||||||
|
return isValidStep(stored) ? stored : roundUpToStep(stored)
|
||||||
})
|
})
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
const calculatedTotal = minutesBetween(formData.startTime, formData.endTime)
|
const calculatedMinutes = minutesBetween(formData.startTime, formData.endTime)
|
||||||
|
const calculatedTotal = calculatedMinutes === null ? null : roundUpToStep(calculatedMinutes)
|
||||||
const totalTime = manualTotal ?? calculatedTotal ?? 0
|
const totalTime = manualTotal ?? calculatedTotal ?? 0
|
||||||
|
|
||||||
const handleChange = (field, value) => {
|
const handleChange = (field, value) => {
|
||||||
@@ -45,6 +50,10 @@ export default function WorksheetTimesModal({ worksheet, onClose, onSave }) {
|
|||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
if (!isValidStep(totalTime)) {
|
||||||
|
setError('Bitte die Arbeitszeit auswählen.')
|
||||||
|
return
|
||||||
|
}
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
setError('')
|
setError('')
|
||||||
try {
|
try {
|
||||||
@@ -53,7 +62,7 @@ export default function WorksheetTimesModal({ worksheet, onClose, onSave }) {
|
|||||||
startTime: timeToWoms(formData.startTime),
|
startTime: timeToWoms(formData.startTime),
|
||||||
endDate: isoToGermanDate(formData.endDate),
|
endDate: isoToGermanDate(formData.endDate),
|
||||||
endTime: timeToWoms(formData.endTime),
|
endTime: timeToWoms(formData.endTime),
|
||||||
totalTime: parseInt(totalTime, 10) || 0
|
totalTime: Number(totalTime)
|
||||||
})
|
})
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
onClose()
|
onClose()
|
||||||
@@ -115,21 +124,16 @@ export default function WorksheetTimesModal({ worksheet, onClose, onSave }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label" htmlFor="worksheet-times-total">Arbeitszeit (Minuten)</label>
|
<label className="form-label" htmlFor="worksheet-times-total">Arbeitszeit</label>
|
||||||
<input
|
<DurationSelect
|
||||||
id="worksheet-times-total"
|
id="worksheet-times-total"
|
||||||
type="number"
|
|
||||||
className="form-control"
|
|
||||||
min="0"
|
|
||||||
step="1"
|
|
||||||
value={totalTime}
|
value={totalTime}
|
||||||
onChange={(e) => setManualTotal(e.target.value)}
|
onChange={(value) => setManualTotal(value)}
|
||||||
placeholder="0"
|
|
||||||
/>
|
/>
|
||||||
<small style={{ color: '#a0aec0', fontSize: '12px' }}>
|
<small style={{ color: '#a0aec0', fontSize: '12px' }}>
|
||||||
{manualTotal === null && calculatedTotal !== null
|
{manualTotal === null && calculatedTotal !== null
|
||||||
? '✓ Automatisch berechnet'
|
? '✓ Aus Beginn und Ende berechnet, aufgerundet auf 15 Minuten'
|
||||||
: 'Manuell eingeben'}
|
: 'Von Hand gewählt'}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { stepOptions } from '../../lib/timeSteps'
|
||||||
|
|
||||||
|
/* Arbeitszeit nur in 15-Minuten-Schritten (15 min bis 12 h), siehe lib/timeSteps */
|
||||||
|
export default function DurationSelect({ id, value, onChange, disabled }) {
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
id={id}
|
||||||
|
className="form-control"
|
||||||
|
value={value}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(e) => onChange(Number(e.target.value))}
|
||||||
|
>
|
||||||
|
{/* Kommentar ohne Arbeitszeit */}
|
||||||
|
{!value && <option value={0}>–</option>}
|
||||||
|
{stepOptions(value).map((o) => (
|
||||||
|
<option key={o.value} value={o.value}>{o.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -9,3 +9,4 @@ export { default as Field } from './Field'
|
|||||||
export { default as Kpi } from './Kpi'
|
export { default as Kpi } from './Kpi'
|
||||||
export { default as AutocompleteInput } from './AutocompleteInput'
|
export { default as AutocompleteInput } from './AutocompleteInput'
|
||||||
export { default as DateTimeInput } from './DateTimeInput'
|
export { default as DateTimeInput } from './DateTimeInput'
|
||||||
|
export { default as DurationSelect } from './DurationSelect'
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { databases, DATABASE_ID, COLLECTIONS, Query, ID } from '../lib/appwrite'
|
import { databases, DATABASE_ID, COLLECTIONS, Query, ID } from '../lib/appwrite'
|
||||||
import { canEditWorksheetTimes, isGitWorksheet } from '../lib/ticketForm'
|
import { canEditWorksheetTimes, isGitWorksheet } from '../lib/ticketForm'
|
||||||
|
import { roundUpToStep, isValidStep } from '../lib/timeSteps'
|
||||||
|
|
||||||
|
const STEP_ERROR = 'Die Arbeitszeit muss in 15-Minuten-Schritten angegeben werden.'
|
||||||
|
|
||||||
export function useWorksheets(woid = null) {
|
export function useWorksheets(woid = null) {
|
||||||
const [worksheets, setWorksheets] = useState([])
|
const [worksheets, setWorksheets] = useState([])
|
||||||
@@ -143,14 +146,17 @@ export function useWorksheets(woid = null) {
|
|||||||
// WSID generieren
|
// WSID generieren
|
||||||
const wsid = await generateWSID()
|
const wsid = await generateWSID()
|
||||||
|
|
||||||
// Automatische Zeitberechnung (wenn nicht manuell angegeben)
|
// Automatische Zeitberechnung (wenn nicht manuell angegeben), aufgerundet auf 15 Minuten
|
||||||
let totalTime = data.totalTime || 0
|
let totalTime = data.isComment ? 0 : Number(data.totalTime) || 0
|
||||||
if (!data.isComment && data.startTime && data.endTime && !data.totalTime) {
|
if (!data.isComment && data.startTime && data.endTime && !totalTime) {
|
||||||
const calculatedTime = calculateTime(data.startTime, data.endTime)
|
const calculatedTime = calculateTime(data.startTime, data.endTime)
|
||||||
if (calculatedTime !== null) {
|
if (calculatedTime !== null) {
|
||||||
totalTime = calculatedTime
|
totalTime = roundUpToStep(calculatedTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!data.isComment && !isValidStep(totalTime)) {
|
||||||
|
return { success: false, error: STEP_ERROR }
|
||||||
|
}
|
||||||
|
|
||||||
// Worksheet-Daten vorbereiten
|
// Worksheet-Daten vorbereiten
|
||||||
const worksheetData = {
|
const worksheetData = {
|
||||||
@@ -165,7 +171,7 @@ export function useWorksheets(woid = null) {
|
|||||||
newStatus: data.newStatus || data.oldStatus || '',
|
newStatus: data.newStatus || data.oldStatus || '',
|
||||||
oldResponseLevel: data.oldResponseLevel || '',
|
oldResponseLevel: data.oldResponseLevel || '',
|
||||||
newResponseLevel: data.newResponseLevel || data.oldResponseLevel || '',
|
newResponseLevel: data.newResponseLevel || data.oldResponseLevel || '',
|
||||||
totalTime: parseInt(totalTime) || 0,
|
totalTime,
|
||||||
startDate: data.startDate || '',
|
startDate: data.startDate || '',
|
||||||
startTime: data.startTime || '',
|
startTime: data.startTime || '',
|
||||||
endDate: data.endDate || data.startDate || '',
|
endDate: data.endDate || data.startDate || '',
|
||||||
@@ -224,6 +230,7 @@ export function useWorksheets(woid = null) {
|
|||||||
return { success: false, error: 'Nur wer das Arbeitsblatt erstellt hat, kann die Zeiten ändern.' }
|
return { success: false, error: 'Nur wer das Arbeitsblatt erstellt hat, kann die Zeiten ändern.' }
|
||||||
}
|
}
|
||||||
const { startDate, startTime, endDate, endTime, totalTime } = times
|
const { startDate, startTime, endDate, endTime, totalTime } = times
|
||||||
|
if (!isValidStep(totalTime)) return { success: false, error: STEP_ERROR }
|
||||||
const data = { startDate, startTime, endDate, endTime, totalTime }
|
const data = { startDate, startTime, endDate, endTime, totalTime }
|
||||||
// Mit nachgetragener Zeit zaehlt ein Git-Push als Arbeitszeit, nicht mehr als Kommentar
|
// Mit nachgetragener Zeit zaehlt ein Git-Push als Arbeitszeit, nicht mehr als Kommentar
|
||||||
if (isGitWorksheet(worksheet)) data.isComment = false
|
if (isGitWorksheet(worksheet)) data.isComment = false
|
||||||
|
|||||||
Reference in new issue
Block a user