299 lines
10 KiB
React
299 lines
10 KiB
React
import { useState, useEffect, useRef } from 'react'
|
||
import { useScrollIntoView } from '../hooks/useScrollIntoView'
|
||
import { useAuth } from '../context/AuthContext'
|
||
import { SERVICE_TYPES, todayIso, isoToGermanDate, timeToWoms, minutesBetween } from '../lib/ticketForm'
|
||
import { TICKET_STATUSES, statusLabel } from '../lib/ticketStatus'
|
||
import { Button, DateTimeInput, Dialog, DurationSelect, Field } from './ui'
|
||
import { roundUpToStep, isValidStep } from '../lib/timeSteps'
|
||
import { validateSupporters } from '../lib/support'
|
||
import SupportersEditor from './SupportersEditor'
|
||
|
||
// Akquise-Tickets: Ergebnis des Kundenkontakts (statt technischer Status)
|
||
const ACQUISITION_STATUS = ['In Kontakt', 'Zugesagt', 'Abgesagt']
|
||
|
||
// Die sechs Status; ein alter Status (z. B. "Assigned") bleibt als aktuelle Auswahl sichtbar
|
||
function statusOptionsFor(current) {
|
||
if (!current || TICKET_STATUSES.some((s) => s.value === current)) return TICKET_STATUSES
|
||
return [{ value: current, label: `${statusLabel(current)} (bisher)` }, ...TICKET_STATUSES]
|
||
}
|
||
|
||
// Datum als "yyyy-mm-dd", Uhrzeit als "hh:mm" (Werte der Kalender-Felder).
|
||
// Die Service-Art kommt vom Ticket; das alte "Off Site" zaehlt als Fernwartung.
|
||
const initialForm = (workorder, newStatus) => ({
|
||
serviceType: SERVICE_TYPES.some(t => t.value === workorder?.serviceType)
|
||
? workorder.serviceType
|
||
: SERVICE_TYPES[0].value,
|
||
newStatus,
|
||
totalTime: 0,
|
||
startDate: todayIso(),
|
||
startTime: '',
|
||
endDate: todayIso(),
|
||
endTime: '',
|
||
details: '',
|
||
isComment: false,
|
||
countsToPlan: false,
|
||
supporters: []
|
||
})
|
||
|
||
// Mitarbeiterliste der Seite (useEmployees laeuft dort einmal); der Editor ruft den Hook nicht selbst auf
|
||
const NO_EMPLOYEES_STATE = { employees: [], loading: false, error: null }
|
||
|
||
export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCreate, aboHoursAllowed = true, employeesState = NO_EMPLOYEES_STATE }) {
|
||
const { user } = useAuth()
|
||
|
||
const isAcquisition = workorder?.type === 'Akquise'
|
||
const statusOptions = isAcquisition
|
||
? ACQUISITION_STATUS.map((s) => ({ value: s, label: s }))
|
||
: statusOptionsFor(workorder?.status)
|
||
const defaultStatus = isAcquisition
|
||
? (ACQUISITION_STATUS.includes(workorder?.status) ? workorder.status : 'In Kontakt')
|
||
: (workorder?.status || 'Open')
|
||
|
||
const [formData, setFormData] = useState(() => initialForm(workorder, defaultStatus))
|
||
|
||
const [loading, setLoading] = useState(false)
|
||
const [error, setError] = useState('')
|
||
const [autoCalculate, setAutoCalculate] = useState(true)
|
||
const errorRef = useRef(null)
|
||
useScrollIntoView(errorRef, error)
|
||
|
||
// Reset form wenn Modal geöffnet wird
|
||
useEffect(() => {
|
||
if (isOpen && workorder) {
|
||
setFormData(initialForm(workorder, defaultStatus))
|
||
setError('')
|
||
setAutoCalculate(true)
|
||
}
|
||
}, [isOpen, workorder, defaultStatus])
|
||
|
||
// Automatische Zeitberechnung, aufgerundet auf 15 Minuten
|
||
useEffect(() => {
|
||
if (!autoCalculate || formData.isComment) return
|
||
const minutes = minutesBetween(formData.startTime, formData.endTime)
|
||
if (minutes !== null) {
|
||
setFormData(prev => ({ ...prev, totalTime: roundUpToStep(minutes) }))
|
||
}
|
||
}, [formData.startTime, formData.endTime, formData.isComment, autoCalculate])
|
||
|
||
const handleChange = (field, value) => {
|
||
setFormData(prev => ({ ...prev, [field]: value }))
|
||
|
||
// Wenn totalTime manuell geändert wird, deaktiviere Auto-Berechnung
|
||
if (field === 'totalTime') {
|
||
setAutoCalculate(false)
|
||
}
|
||
}
|
||
|
||
// 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()
|
||
setLoading(true)
|
||
setError('')
|
||
|
||
try {
|
||
if (!formData.details.trim()) {
|
||
setError('Bitte Details eingeben')
|
||
setLoading(false)
|
||
return
|
||
}
|
||
if (!formData.isComment && !isValidStep(formData.totalTime)) {
|
||
setError('Bitte die Arbeitszeit auswählen.')
|
||
setLoading(false)
|
||
return
|
||
}
|
||
const supporters = formData.isComment ? [] : formData.supporters
|
||
const supportError = validateSupporters(supporters, user?.$id)
|
||
if (supportError) {
|
||
setError(supportError)
|
||
setLoading(false)
|
||
return
|
||
}
|
||
|
||
const worksheetData = {
|
||
woid: workorder.woid,
|
||
workorderId: workorder.$id,
|
||
serviceType: formData.serviceType,
|
||
oldStatus: workorder.status,
|
||
newStatus: formData.newStatus,
|
||
totalTime: formData.isComment ? 0 : Number(formData.totalTime),
|
||
startDate: isoToGermanDate(formData.startDate),
|
||
startTime: timeToWoms(formData.startTime),
|
||
endDate: isoToGermanDate(formData.endDate),
|
||
endTime: timeToWoms(formData.endTime),
|
||
details: formData.details,
|
||
isComment: formData.isComment,
|
||
countsToPlan: aboHoursAllowed && !formData.isComment && formData.countsToPlan,
|
||
employeeShort: user?.prefs?.shortCode || '', // Aus User-Preferences
|
||
supporters
|
||
}
|
||
|
||
const result = await onCreate(worksheetData, user)
|
||
|
||
if (result.success) {
|
||
onClose()
|
||
} else {
|
||
setError(result.error || 'Fehler beim Speichern des Arbeitsblatts')
|
||
}
|
||
} catch (err) {
|
||
console.error('Error creating worksheet:', err)
|
||
setError(err.message || 'Ein unerwarteter Fehler ist aufgetreten')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
if (!isOpen || !workorder) return null
|
||
|
||
return (
|
||
<Dialog
|
||
as="form"
|
||
wide
|
||
title={`Neues Arbeitsblatt – Ticket ${workorder.woid}`}
|
||
busy={loading}
|
||
onClose={onClose}
|
||
onSubmit={handleSubmit}
|
||
footer={
|
||
<>
|
||
<span className="grow" />
|
||
<Button variant="primary" type="submit" disabled={loading}>
|
||
{loading ? 'Speichert …' : 'Speichern'}
|
||
</Button>
|
||
</>
|
||
}
|
||
>
|
||
{error && (
|
||
<div className="msg err" role="alert" ref={errorRef}>
|
||
{error}
|
||
</div>
|
||
)}
|
||
|
||
<div className="field-row">
|
||
<Field label="Art" htmlFor="worksheet-service-type">
|
||
<select
|
||
id="worksheet-service-type"
|
||
className="select"
|
||
value={formData.serviceType}
|
||
onChange={(e) => handleChange('serviceType', e.target.value)}
|
||
required
|
||
>
|
||
{SERVICE_TYPES.map(type => (
|
||
<option key={type.value} value={type.value}>{type.label}</option>
|
||
))}
|
||
</select>
|
||
</Field>
|
||
|
||
<Field
|
||
label={isAcquisition ? 'Akquise-Ergebnis' : 'Neuer Status'}
|
||
htmlFor="worksheet-new-status"
|
||
hint={isAcquisition ? '"Zugesagt" macht den Lead automatisch zum festen Kunden.' : undefined}
|
||
>
|
||
<select
|
||
id="worksheet-new-status"
|
||
className="select"
|
||
value={formData.newStatus}
|
||
onChange={(e) => handleChange('newStatus', e.target.value)}
|
||
required
|
||
>
|
||
{statusOptions.map(status => (
|
||
<option key={status.value} value={status.value}>{status.label}</option>
|
||
))}
|
||
</select>
|
||
</Field>
|
||
</div>
|
||
|
||
<label className="check">
|
||
<input
|
||
type="checkbox"
|
||
checked={formData.isComment}
|
||
onChange={(e) => handleChange('isComment', e.target.checked)}
|
||
/>
|
||
Nur Kommentar (keine Arbeitszeit)
|
||
</label>
|
||
|
||
{aboHoursAllowed && (
|
||
<label className="check" title="Zeit wird vom Stundenguthaben des Wartungs-Abos abgezogen und im Kundenportal angezeigt">
|
||
<input
|
||
type="checkbox"
|
||
checked={formData.countsToPlan && !formData.isComment}
|
||
disabled={formData.isComment}
|
||
onChange={(e) => handleChange('countsToPlan', e.target.checked)}
|
||
/>
|
||
Vom Abo-Stundenkonto abziehen
|
||
</label>
|
||
)}
|
||
|
||
<div className="field-row">
|
||
<Field label="Beginn" htmlFor="worksheet-start-date">
|
||
<DateTimeInput
|
||
id="worksheet-start-date"
|
||
timeLabel="Uhrzeit Beginn"
|
||
required
|
||
date={formData.startDate}
|
||
time={formData.startTime}
|
||
onDateChange={handleStartDateChange}
|
||
onTimeChange={(value) => handleChange('startTime', value)}
|
||
/>
|
||
</Field>
|
||
|
||
<Field label="Ende" htmlFor="worksheet-end-date">
|
||
<DateTimeInput
|
||
id="worksheet-end-date"
|
||
timeLabel="Uhrzeit Ende"
|
||
required
|
||
min={formData.startDate}
|
||
date={formData.endDate}
|
||
time={formData.endTime}
|
||
onDateChange={(value) => handleChange('endDate', value)}
|
||
onTimeChange={(value) => handleChange('endTime', value)}
|
||
/>
|
||
</Field>
|
||
</div>
|
||
|
||
<Field
|
||
label="Arbeitszeit"
|
||
htmlFor="worksheet-total"
|
||
hint={autoCalculate && formData.startTime && formData.endTime
|
||
? 'Aus Beginn und Ende berechnet, aufgerundet auf 15 Minuten'
|
||
: autoCalculate ? 'Beginn und Ende eintragen oder Zeit wählen' : 'Von Hand gewählt'}
|
||
>
|
||
<DurationSelect
|
||
id="worksheet-total"
|
||
value={formData.isComment ? 0 : formData.totalTime}
|
||
onChange={(value) => handleChange('totalTime', value)}
|
||
disabled={formData.isComment}
|
||
/>
|
||
</Field>
|
||
|
||
{!formData.isComment && (
|
||
<SupportersEditor
|
||
value={formData.supporters}
|
||
onChange={(list) => handleChange('supporters', list)}
|
||
excludeUserId={user?.$id}
|
||
minutes={formData.totalTime}
|
||
employeesState={employeesState}
|
||
/>
|
||
)}
|
||
|
||
<Field label="Was wurde gemacht?" htmlFor="worksheet-details">
|
||
<textarea
|
||
id="worksheet-details"
|
||
className="textarea"
|
||
rows={5}
|
||
placeholder="Beschreibe die durchgeführten Arbeiten..."
|
||
value={formData.details}
|
||
onChange={(e) => handleChange('details', e.target.value)}
|
||
required
|
||
/>
|
||
</Field>
|
||
</Dialog>
|
||
)
|
||
}
|