Arbeitsblatt-Formular erneuert: Kalender, deutsche Service-Art
- Start und Ende mit Kalender und Uhrzeitfeld (gemeinsame Komponente DateTimeInput, auch im Ticketformular); gespeichert weiter als dd.mm.yyyy / hhmm, das Ende rückt mit, wenn der Start später liegt - Service Type nur noch "Fernwartung (remote)" oder "Vor Ort beim Kunden", vorbelegt vom Ticket; COMMENT entfällt (dafür gibt es "Nur Kommentar") - Minuten werden aus den Uhrzeiten berechnet (über Mitternacht weiter) - Minutenfeld nimmt ganze Minuten: mit step 15 ließ der Browser berechnete Zeiten wie 70 Minuten nicht absenden - Arbeitsblatt-Liste und -Statistik zeigen die Service-Art auf Deutsch (Remote und altes Off Site zusammen als Fernwartung) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8cb0bb8146
commit
e678899141
7 files changed
+159
-173
No files matched your search
@@ -11,7 +11,7 @@ import {
|
|||||||
buildRequesterSuggestions,
|
buildRequesterSuggestions,
|
||||||
rankSuggestions,
|
rankSuggestions,
|
||||||
} from '../lib/ticketForm'
|
} from '../lib/ticketForm'
|
||||||
import { AutocompleteInput } from './ui'
|
import { AutocompleteInput, DateTimeInput } from './ui'
|
||||||
|
|
||||||
// Neue Tickets gibt es aktuell nur fuer diese Arten; alte Tickets behalten ihre Art.
|
// Neue Tickets gibt es aktuell nur fuer diese Arten; alte Tickets behalten ihre Art.
|
||||||
// Migration = ein Kunde, den wir von einem anderen Anbieter uebernehmen.
|
// Migration = ein Kunde, den wir von einem anderen Anbieter uebernehmen.
|
||||||
@@ -45,15 +45,6 @@ const emptyForm = () => ({
|
|||||||
details: ''
|
details: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// Klick irgendwo ins Datumsfeld oeffnet den Kalender, nicht nur das kleine Symbol
|
|
||||||
const openCalendar = (e) => {
|
|
||||||
try {
|
|
||||||
e.currentTarget.showPicker?.()
|
|
||||||
} catch {
|
|
||||||
// aeltere Browser: Kalender bleibt ueber das Symbol erreichbar
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CreateTicketModal({ isOpen, onClose, onCreate, customers = [] }) {
|
export default function CreateTicketModal({ isOpen, onClose, onCreate, customers = [] }) {
|
||||||
const { config } = useAdminConfig()
|
const { config } = useAdminConfig()
|
||||||
const { employees } = useEmployees()
|
const { employees } = useEmployees()
|
||||||
@@ -260,45 +251,27 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
|
|||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label" htmlFor="ticket-start-date">Start Date & Time</label>
|
<label className="form-label" htmlFor="ticket-start-date">Start Date & Time</label>
|
||||||
<div className="datetime-pair">
|
<DateTimeInput
|
||||||
<input
|
id="ticket-start-date"
|
||||||
id="ticket-start-date"
|
timeLabel="Start time"
|
||||||
type="date"
|
date={formData.startDate}
|
||||||
className="form-control"
|
time={formData.startTime}
|
||||||
value={formData.startDate}
|
onDateChange={handleStartDateChange}
|
||||||
onChange={(e) => handleStartDateChange(e.target.value)}
|
onTimeChange={(value) => handleChange('startTime', value)}
|
||||||
onClick={openCalendar}
|
/>
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
className="form-control"
|
|
||||||
aria-label="Start time"
|
|
||||||
value={formData.startTime}
|
|
||||||
onChange={(e) => handleChange('startTime', e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label" htmlFor="ticket-end-date">End Date & Time (Deadline)</label>
|
<label className="form-label" htmlFor="ticket-end-date">End Date & Time (Deadline)</label>
|
||||||
<div className="datetime-pair">
|
<DateTimeInput
|
||||||
<input
|
id="ticket-end-date"
|
||||||
id="ticket-end-date"
|
timeLabel="End time"
|
||||||
type="date"
|
min={formData.startDate}
|
||||||
className="form-control"
|
date={formData.deadline}
|
||||||
min={formData.startDate || undefined}
|
time={formData.endTime}
|
||||||
value={formData.deadline}
|
onDateChange={(value) => handleChange('deadline', value)}
|
||||||
onChange={(e) => handleChange('deadline', e.target.value)}
|
onTimeChange={(value) => handleChange('endTime', value)}
|
||||||
onClick={openCalendar}
|
/>
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
className="form-control"
|
|
||||||
aria-label="End time"
|
|
||||||
value={formData.endTime}
|
|
||||||
onChange={(e) => handleChange('endTime', e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { FaTimes } from 'react-icons/fa'
|
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'
|
||||||
const SERVICE_TYPES = ['Remote', 'On Site', 'Off Site', 'COMMENT']
|
import { DateTimeInput } from './ui'
|
||||||
|
|
||||||
const STATUS_OPTIONS = [
|
const STATUS_OPTIONS = [
|
||||||
'Open',
|
'Open',
|
||||||
@@ -20,30 +20,25 @@ const STATUS_OPTIONS = [
|
|||||||
// 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']
|
||||||
|
|
||||||
|
// 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
|
||||||
|
})
|
||||||
|
|
||||||
export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCreate }) {
|
export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCreate }) {
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
const today = new Date().toLocaleDateString('de-DE', {
|
|
||||||
day: '2-digit',
|
|
||||||
month: '2-digit',
|
|
||||||
year: 'numeric'
|
|
||||||
})
|
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
serviceType: 'Remote',
|
|
||||||
newStatus: workorder?.status || 'Open',
|
|
||||||
totalTime: 0,
|
|
||||||
startDate: today,
|
|
||||||
startTime: '',
|
|
||||||
endDate: today,
|
|
||||||
endTime: '',
|
|
||||||
details: '',
|
|
||||||
isComment: false,
|
|
||||||
countsToPlan: false
|
|
||||||
})
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [error, setError] = useState('')
|
|
||||||
const [autoCalculate, setAutoCalculate] = useState(true)
|
|
||||||
|
|
||||||
const isAcquisition = workorder?.type === 'Akquise'
|
const isAcquisition = workorder?.type === 'Akquise'
|
||||||
const statusOptions = isAcquisition ? ACQUISITION_STATUS : STATUS_OPTIONS
|
const statusOptions = isAcquisition ? ACQUISITION_STATUS : STATUS_OPTIONS
|
||||||
@@ -51,66 +46,53 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
? (ACQUISITION_STATUS.includes(workorder?.status) ? workorder.status : 'In Kontakt')
|
? (ACQUISITION_STATUS.includes(workorder?.status) ? workorder.status : 'In Kontakt')
|
||||||
: (workorder?.status || 'Open')
|
: (workorder?.status || 'Open')
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState(() => initialForm(workorder, defaultStatus))
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [autoCalculate, setAutoCalculate] = useState(true)
|
||||||
|
|
||||||
// Reset form wenn Modal geöffnet wird
|
// Reset form wenn Modal geöffnet wird
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && workorder) {
|
if (isOpen && workorder) {
|
||||||
setFormData({
|
setFormData(initialForm(workorder, defaultStatus))
|
||||||
serviceType: 'Remote',
|
|
||||||
newStatus: defaultStatus,
|
|
||||||
totalTime: 0,
|
|
||||||
startDate: today,
|
|
||||||
startTime: '',
|
|
||||||
endDate: today,
|
|
||||||
endTime: '',
|
|
||||||
details: '',
|
|
||||||
isComment: false,
|
|
||||||
countsToPlan: false
|
|
||||||
})
|
|
||||||
setError('')
|
setError('')
|
||||||
setAutoCalculate(true)
|
setAutoCalculate(true)
|
||||||
}
|
}
|
||||||
}, [isOpen, workorder, today])
|
}, [isOpen, workorder, defaultStatus])
|
||||||
|
|
||||||
// Automatische Zeitberechnung
|
// Automatische Zeitberechnung
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoCalculate && formData.startTime && formData.endTime && !formData.isComment) {
|
if (!autoCalculate || formData.isComment) return
|
||||||
try {
|
const minutes = minutesBetween(formData.startTime, formData.endTime)
|
||||||
const startHour = parseInt(formData.startTime.substring(0, 2))
|
if (minutes !== null) {
|
||||||
const startMin = parseInt(formData.startTime.substring(2, 4))
|
setFormData(prev => ({ ...prev, totalTime: minutes }))
|
||||||
const endHour = parseInt(formData.endTime.substring(0, 2))
|
|
||||||
const endMin = parseInt(formData.endTime.substring(2, 4))
|
|
||||||
|
|
||||||
if (!isNaN(startHour) && !isNaN(startMin) && !isNaN(endHour) && !isNaN(endMin)) {
|
|
||||||
const startTotal = startHour * 60 + startMin
|
|
||||||
const endTotal = endHour * 60 + endMin
|
|
||||||
|
|
||||||
let diff = endTotal - startTotal
|
|
||||||
if (diff < 0) {
|
|
||||||
diff += 24 * 60 // Overnight
|
|
||||||
}
|
|
||||||
|
|
||||||
setFormData(prev => ({ ...prev, totalTime: diff }))
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Ignoriere Fehler
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [formData.startTime, formData.endTime, formData.isComment, autoCalculate])
|
}, [formData.startTime, formData.endTime, formData.isComment, autoCalculate])
|
||||||
|
|
||||||
const handleChange = (field, value) => {
|
const handleChange = (field, value) => {
|
||||||
setFormData(prev => ({ ...prev, [field]: value }))
|
setFormData(prev => ({ ...prev, [field]: value }))
|
||||||
|
|
||||||
// Wenn totalTime manuell geändert wird, deaktiviere Auto-Berechnung
|
// Wenn totalTime manuell geändert wird, deaktiviere Auto-Berechnung
|
||||||
if (field === 'totalTime') {
|
if (field === 'totalTime') {
|
||||||
setAutoCalculate(false)
|
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) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError('')
|
setError('')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!formData.details.trim()) {
|
if (!formData.details.trim()) {
|
||||||
setError('Bitte Details eingeben')
|
setError('Bitte Details eingeben')
|
||||||
@@ -125,10 +107,10 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
oldStatus: workorder.status,
|
oldStatus: workorder.status,
|
||||||
newStatus: formData.newStatus,
|
newStatus: formData.newStatus,
|
||||||
totalTime: formData.isComment ? 0 : parseInt(formData.totalTime) || 0,
|
totalTime: formData.isComment ? 0 : parseInt(formData.totalTime) || 0,
|
||||||
startDate: formData.startDate,
|
startDate: isoToGermanDate(formData.startDate),
|
||||||
startTime: formData.startTime,
|
startTime: timeToWoms(formData.startTime),
|
||||||
endDate: formData.endDate,
|
endDate: isoToGermanDate(formData.endDate),
|
||||||
endTime: formData.endTime,
|
endTime: timeToWoms(formData.endTime),
|
||||||
details: formData.details,
|
details: formData.details,
|
||||||
isComment: formData.isComment,
|
isComment: formData.isComment,
|
||||||
countsToPlan: !formData.isComment && formData.countsToPlan,
|
countsToPlan: !formData.isComment && formData.countsToPlan,
|
||||||
@@ -136,7 +118,7 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await onCreate(worksheetData, user)
|
const result = await onCreate(worksheetData, user)
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
onClose()
|
onClose()
|
||||||
} else {
|
} else {
|
||||||
@@ -159,13 +141,13 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
</span>
|
</span>
|
||||||
<div className="overlay-content">
|
<div className="overlay-content">
|
||||||
<h2 className="mb-2">Create New Worksheet - WOID {workorder.woid}</h2>
|
<h2 className="mb-2">Create New Worksheet - WOID {workorder.woid}</h2>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
|
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col col-6">
|
<div className="col col-6">
|
||||||
@@ -178,7 +160,7 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
required
|
required
|
||||||
>
|
>
|
||||||
{SERVICE_TYPES.map(type => (
|
{SERVICE_TYPES.map(type => (
|
||||||
<option key={type} value={type}>{type}</option>
|
<option key={type.value} value={type.value}>{type.label}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -227,72 +209,52 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col col-6">
|
<div className="col col-6">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label" htmlFor="worksheet-start-date">Start Date & Time</label>
|
||||||
|
<DateTimeInput
|
||||||
|
id="worksheet-start-date"
|
||||||
|
timeLabel="Start time"
|
||||||
|
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-end-date">End Date & Time</label>
|
||||||
|
<DateTimeInput
|
||||||
|
id="worksheet-end-date"
|
||||||
|
timeLabel="End time"
|
||||||
|
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">
|
<div className="form-group">
|
||||||
<label className="form-label">Total Time (Minuten)</label>
|
<label className="form-label">Total Time (Minuten)</label>
|
||||||
|
{/* Ganze Minuten: mit step 15 blockierte der Browser berechnete Zeiten wie 70 */}
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
min="0"
|
min="0"
|
||||||
step="15"
|
step="1"
|
||||||
value={formData.totalTime}
|
value={formData.totalTime}
|
||||||
onChange={(e) => handleChange('totalTime', e.target.value)}
|
onChange={(e) => handleChange('totalTime', e.target.value)}
|
||||||
disabled={formData.isComment}
|
disabled={formData.isComment}
|
||||||
placeholder="0"
|
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'
|
? '✓ Automatisch berechnet'
|
||||||
: 'Manuell eingeben'}
|
: 'Manuell eingeben'}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label">Start Date</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
placeholder="dd.mm.yyyy"
|
|
||||||
value={formData.startDate}
|
|
||||||
onChange={(e) => handleChange('startDate', e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label">End Date</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
placeholder="dd.mm.yyyy"
|
|
||||||
value={formData.endDate}
|
|
||||||
onChange={(e) => handleChange('endDate', e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label">Start Time</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
placeholder="hhmm"
|
|
||||||
value={formData.startTime}
|
|
||||||
onChange={(e) => handleChange('startTime', e.target.value)}
|
|
||||||
maxLength="4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label">End Time</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
placeholder="hhmm"
|
|
||||||
value={formData.endTime}
|
|
||||||
onChange={(e) => handleChange('endTime', e.target.value)}
|
|
||||||
maxLength="4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -309,8 +271,8 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center mt-2">
|
<div className="text-center mt-2">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-dark"
|
className="btn btn-dark"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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 } from 'react-icons/fa'
|
||||||
|
import { serviceTypeLabel } 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))
|
||||||
@@ -147,7 +148,7 @@ export default function WorksheetList({ worksheets, totalTime, loading, onEditTi
|
|||||||
<span className="badge" style={{
|
<span className="badge" style={{
|
||||||
background: 'rgba(255,255,255,0.2)',
|
background: 'rgba(255,255,255,0.2)',
|
||||||
fontSize: '0.8rem'
|
fontSize: '0.8rem'
|
||||||
}}>{ws.serviceType}</span>
|
}}>{serviceTypeLabel(ws.serviceType)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -237,7 +238,7 @@ export default function WorksheetList({ worksheets, totalTime, loading, onEditTi
|
|||||||
background: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)',
|
background: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
fontSize: '0.85rem'
|
fontSize: '0.85rem'
|
||||||
}}>{ws.serviceType}</span>
|
}}>{serviceTypeLabel(ws.serviceType)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status-Änderung */}
|
{/* Status-Änderung */}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { FaClock, FaUsers, FaChartLine } from 'react-icons/fa'
|
import { FaClock, FaUsers, FaChartLine } from 'react-icons/fa'
|
||||||
|
import { serviceTypeLabel } from '../lib/ticketForm'
|
||||||
|
|
||||||
export default function WorksheetStats({ worksheets }) {
|
export default function WorksheetStats({ worksheets }) {
|
||||||
if (!worksheets || worksheets.length === 0) {
|
if (!worksheets || worksheets.length === 0) {
|
||||||
@@ -29,9 +30,9 @@ export default function WorksheetStats({ worksheets }) {
|
|||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
|
|
||||||
// Service Type Verteilung
|
// Service Type Verteilung (Remote und das alte Off Site zusammen als Fernwartung)
|
||||||
const byServiceType = worksheets.reduce((acc, ws) => {
|
const byServiceType = worksheets.reduce((acc, ws) => {
|
||||||
const type = ws.serviceType || 'Unknown'
|
const type = serviceTypeLabel(ws.serviceType) || 'Unknown'
|
||||||
acc[type] = (acc[type] || 0) + 1
|
acc[type] = (acc[type] || 0) + 1
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Klick irgendwo ins Datumsfeld oeffnet den Kalender, nicht nur das kleine Symbol
|
||||||
|
const openCalendar = (e) => {
|
||||||
|
try {
|
||||||
|
e.currentTarget.showPicker?.()
|
||||||
|
} catch {
|
||||||
|
// aeltere Browser: Kalender bleibt ueber das Symbol erreichbar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Datum mit Kalender und Uhrzeit nebeneinander. Werte wie die Browser-Felder:
|
||||||
|
Datum "yyyy-mm-dd", Uhrzeit "hh:mm" - umgerechnet wird erst beim Speichern. */
|
||||||
|
export default function DateTimeInput({ id, date, time, onDateChange, onTimeChange, min, required, timeLabel }) {
|
||||||
|
return (
|
||||||
|
<div className="datetime-pair">
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
type="date"
|
||||||
|
className="form-control"
|
||||||
|
min={min || undefined}
|
||||||
|
required={required}
|
||||||
|
value={date}
|
||||||
|
onChange={(e) => onDateChange(e.target.value)}
|
||||||
|
onClick={openCalendar}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
className="form-control"
|
||||||
|
aria-label={timeLabel}
|
||||||
|
value={time}
|
||||||
|
onChange={(e) => onTimeChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -8,3 +8,4 @@ export { default as EmptyState } from './EmptyState'
|
|||||||
export { default as Field } from './Field'
|
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'
|
||||||
+17
-3
@@ -1,4 +1,4 @@
|
|||||||
/* Hilfen fuer "Neues Ticket" und die Anzeige der Tickets.
|
/* Hilfen fuer die Formulare "Neues Ticket" und "Arbeitsblatt" und die Anzeige.
|
||||||
WOMS speichert Datum als Text "dd.mm.yyyy" und Uhrzeit als "hhmm"
|
WOMS speichert Datum als Text "dd.mm.yyyy" und Uhrzeit als "hhmm"
|
||||||
(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. */
|
||||||
@@ -9,8 +9,13 @@ export const SERVICE_TYPES = [
|
|||||||
{ value: 'On Site', label: 'Vor Ort beim Kunden' },
|
{ value: 'On Site', label: 'Vor Ort beim Kunden' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// Kurzform fuer Badges. Das alte "Off Site" meinte ebenfalls Arbeit aus der Ferne.
|
// Kurzform fuer Badges und Listen. Das alte "Off Site" meinte ebenfalls Arbeit aus der Ferne.
|
||||||
const SERVICE_TYPE_SHORT = { remote: 'Fernwartung', 'off site': 'Fernwartung', 'on site': 'Vor Ort' }
|
const SERVICE_TYPE_SHORT = {
|
||||||
|
remote: 'Fernwartung',
|
||||||
|
'off site': 'Fernwartung',
|
||||||
|
'on site': 'Vor Ort',
|
||||||
|
comment: 'Kommentar',
|
||||||
|
}
|
||||||
|
|
||||||
export function serviceTypeLabel(value) {
|
export function serviceTypeLabel(value) {
|
||||||
return SERVICE_TYPE_SHORT[String(value || '').trim().toLowerCase()] || value
|
return SERVICE_TYPE_SHORT[String(value || '').trim().toLowerCase()] || value
|
||||||
@@ -35,6 +40,15 @@ export function timeToWoms(value) {
|
|||||||
return m ? `${m[1].padStart(2, '0')}${m[2]}` : ''
|
return m ? `${m[1].padStart(2, '0')}${m[2]}` : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 || ''))
|
||||||
|
const b = /^(\d{1,2}):(\d{2})/.exec(String(end || ''))
|
||||||
|
if (!a || !b) return null
|
||||||
|
const diff = Number(b[1]) * 60 + Number(b[2]) - (Number(a[1]) * 60 + Number(a[2]))
|
||||||
|
return diff < 0 ? diff + 24 * 60 : diff
|
||||||
|
}
|
||||||
|
|
||||||
/* Vorschlaege fuer "Requested by" / "Requested for": Kunden, Mitarbeiter
|
/* Vorschlaege fuer "Requested by" / "Requested for": Kunden, Mitarbeiter
|
||||||
und Namen aus frueheren Tickets. Pro Name (ohne Gross-/Kleinschreibung)
|
und Namen aus frueheren Tickets. Pro Name (ohne Gross-/Kleinschreibung)
|
||||||
ein Eintrag; customerIds merkt sich, zu welchen Kunden er gehoert. */
|
ein Eintrag; customerIds merkt sich, zu welchen Kunden er gehoert. */
|
||||||
|
|||||||
Reference in new issue
Block a user