diff --git a/src/components/CreateTicketModal.jsx b/src/components/CreateTicketModal.jsx index 82ab1d9..dd51a2c 100644 --- a/src/components/CreateTicketModal.jsx +++ b/src/components/CreateTicketModal.jsx @@ -11,7 +11,7 @@ import { buildRequesterSuggestions, rankSuggestions, } 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. // Migration = ein Kunde, den wir von einem anderen Anbieter uebernehmen. @@ -45,15 +45,6 @@ const emptyForm = () => ({ 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 = [] }) { const { config } = useAdminConfig() const { employees } = useEmployees() @@ -260,45 +251,27 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
-
- handleStartDateChange(e.target.value)} - onClick={openCalendar} - /> - handleChange('startTime', e.target.value)} - /> -
+ handleChange('startTime', value)} + />
-
- handleChange('deadline', e.target.value)} - onClick={openCalendar} - /> - handleChange('endTime', e.target.value)} - /> -
+ handleChange('deadline', value)} + onTimeChange={(value) => handleChange('endTime', value)} + />
diff --git a/src/components/CreateWorksheetModal.jsx b/src/components/CreateWorksheetModal.jsx index f5a27d5..89fa4f2 100644 --- a/src/components/CreateWorksheetModal.jsx +++ b/src/components/CreateWorksheetModal.jsx @@ -1,8 +1,8 @@ import { useState, useEffect } from 'react' import { FaTimes } from 'react-icons/fa' import { useAuth } from '../context/AuthContext' - -const SERVICE_TYPES = ['Remote', 'On Site', 'Off Site', 'COMMENT'] +import { SERVICE_TYPES, todayIso, isoToGermanDate, timeToWoms, minutesBetween } from '../lib/ticketForm' +import { DateTimeInput } from './ui' const STATUS_OPTIONS = [ 'Open', @@ -20,30 +20,25 @@ const STATUS_OPTIONS = [ // Akquise-Tickets: Ergebnis des Kundenkontakts (statt technischer Status) 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 }) { 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 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') : (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 useEffect(() => { if (isOpen && workorder) { - setFormData({ - serviceType: 'Remote', - newStatus: defaultStatus, - totalTime: 0, - startDate: today, - startTime: '', - endDate: today, - endTime: '', - details: '', - isComment: false, - countsToPlan: false - }) + setFormData(initialForm(workorder, defaultStatus)) setError('') setAutoCalculate(true) } - }, [isOpen, workorder, today]) + }, [isOpen, workorder, defaultStatus]) // Automatische Zeitberechnung useEffect(() => { - if (autoCalculate && formData.startTime && formData.endTime && !formData.isComment) { - try { - const startHour = parseInt(formData.startTime.substring(0, 2)) - const startMin = parseInt(formData.startTime.substring(2, 4)) - 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 - } + if (!autoCalculate || formData.isComment) return + const minutes = minutesBetween(formData.startTime, formData.endTime) + if (minutes !== null) { + setFormData(prev => ({ ...prev, totalTime: 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') @@ -125,10 +107,10 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre oldStatus: workorder.status, newStatus: formData.newStatus, totalTime: formData.isComment ? 0 : parseInt(formData.totalTime) || 0, - startDate: formData.startDate, - startTime: formData.startTime, - endDate: formData.endDate, - endTime: formData.endTime, + startDate: isoToGermanDate(formData.startDate), + startTime: timeToWoms(formData.startTime), + endDate: isoToGermanDate(formData.endDate), + endTime: timeToWoms(formData.endTime), details: formData.details, isComment: formData.isComment, countsToPlan: !formData.isComment && formData.countsToPlan, @@ -136,7 +118,7 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre } const result = await onCreate(worksheetData, user) - + if (result.success) { onClose() } else { @@ -159,13 +141,13 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre

Create New Worksheet - WOID {workorder.woid}

- + {error && (
{error}
)} - +
@@ -178,7 +160,7 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre required > {SERVICE_TYPES.map(type => ( - + ))}
@@ -227,72 +209,52 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
+
+ + handleChange('startTime', value)} + /> +
+ +
+ + handleChange('endDate', value)} + onTimeChange={(value) => handleChange('endTime', value)} + /> +
+
+ {/* Ganze Minuten: mit step 15 blockierte der Browser berechnete Zeiten wie 70 */} handleChange('totalTime', e.target.value)} disabled={formData.isComment} placeholder="0" /> - {autoCalculate && formData.startTime && formData.endTime - ? '✓ Automatisch berechnet' + {autoCalculate && formData.startTime && formData.endTime + ? '✓ Automatisch berechnet' : 'Manuell eingeben'}
- -
- - handleChange('startDate', e.target.value)} - required - /> -
- -
- - handleChange('endDate', e.target.value)} - required - /> -
- -
- - handleChange('startTime', e.target.value)} - maxLength="4" - /> -
- -
- - handleChange('endTime', e.target.value)} - maxLength="4" - /> -
@@ -309,8 +271,8 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
-
)} @@ -237,7 +238,7 @@ export default function WorksheetList({ worksheets, totalTime, loading, onEditTi background: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)', color: 'white', fontSize: '0.85rem' - }}>{ws.serviceType} + }}>{serviceTypeLabel(ws.serviceType)} {/* Status-Änderung */} diff --git a/src/components/WorksheetStats.jsx b/src/components/WorksheetStats.jsx index 706e959..0643ae4 100644 --- a/src/components/WorksheetStats.jsx +++ b/src/components/WorksheetStats.jsx @@ -1,4 +1,5 @@ import { FaClock, FaUsers, FaChartLine } from 'react-icons/fa' +import { serviceTypeLabel } from '../lib/ticketForm' export default function WorksheetStats({ worksheets }) { 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 type = ws.serviceType || 'Unknown' + const type = serviceTypeLabel(ws.serviceType) || 'Unknown' acc[type] = (acc[type] || 0) + 1 return acc }, {}) diff --git a/src/components/ui/DateTimeInput.jsx b/src/components/ui/DateTimeInput.jsx new file mode 100644 index 0000000..56c0a15 --- /dev/null +++ b/src/components/ui/DateTimeInput.jsx @@ -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 ( +
+ onDateChange(e.target.value)} + onClick={openCalendar} + /> + onTimeChange(e.target.value)} + /> +
+ ) +} diff --git a/src/components/ui/index.js b/src/components/ui/index.js index fea9fb9..2222079 100644 --- a/src/components/ui/index.js +++ b/src/components/ui/index.js @@ -8,3 +8,4 @@ export { default as EmptyState } from './EmptyState' export { default as Field } from './Field' export { default as Kpi } from './Kpi' export { default as AutocompleteInput } from './AutocompleteInput' +export { default as DateTimeInput } from './DateTimeInput' diff --git a/src/lib/ticketForm.js b/src/lib/ticketForm.js index 5dae260..e025158 100644 --- a/src/lib/ticketForm.js +++ b/src/lib/ticketForm.js @@ -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" (siehe planboard.js). Die Kalender-Felder des Browsers liefern dagegen "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' }, ] -// Kurzform fuer Badges. Das alte "Off Site" meinte ebenfalls Arbeit aus der Ferne. -const SERVICE_TYPE_SHORT = { remote: 'Fernwartung', 'off site': 'Fernwartung', 'on site': 'Vor Ort' } +// 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', + comment: 'Kommentar', +} export function serviceTypeLabel(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]}` : '' } +// 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 und Namen aus frueheren Tickets. Pro Name (ohne Gross-/Kleinschreibung) ein Eintrag; customerIds merkt sich, zu welchen Kunden er gehoert. */