Tickets: vier Ticket-Dialoge auf Dialog (Esc, Sperre beim Speichern), Unterstuetzung als Feldblock
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
39dfb386ac
commit
200d68f20d
6 files changed
+439
-436
No files matched your search
@@ -1,6 +1,5 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react'
|
import { useState, useEffect, useMemo, useRef } from 'react'
|
||||||
import { FaTimes } from 'react-icons/fa'
|
import { useScrollIntoView } from '../hooks/useScrollIntoView'
|
||||||
import { useEmployees } from '../hooks/useEmployees'
|
|
||||||
import { databases, DATABASE_ID, COLLECTIONS, Query } from '../lib/appwrite'
|
import { databases, DATABASE_ID, COLLECTIONS, Query } from '../lib/appwrite'
|
||||||
import {
|
import {
|
||||||
SERVICE_TYPES,
|
SERVICE_TYPES,
|
||||||
@@ -11,7 +10,7 @@ import {
|
|||||||
rankSuggestions,
|
rankSuggestions,
|
||||||
} from '../lib/ticketForm'
|
} from '../lib/ticketForm'
|
||||||
import { priorityFor } from '../lib/ticketStatus'
|
import { priorityFor } from '../lib/ticketStatus'
|
||||||
import { AutocompleteInput, DateTimeInput } from './ui'
|
import { AutocompleteInput, Button, DateTimeInput, Dialog, Field } 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.
|
||||||
@@ -36,14 +35,18 @@ const emptyForm = () => ({
|
|||||||
details: ''
|
details: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
export default function CreateTicketModal({ isOpen, onClose, onCreate, customers = [] }) {
|
// Stabile Vorgabe, damit die Vorschlagsliste nicht bei jedem Render neu entsteht
|
||||||
const { employees } = useEmployees()
|
const NO_EMPLOYEES = []
|
||||||
|
|
||||||
|
// `employees` kommt von der Seite (useEmployees laeuft dort einmal), der Dialog ruft den Hook nicht selbst auf
|
||||||
|
export default function CreateTicketModal({ isOpen, onClose, onCreate, customers = [], employees = NO_EMPLOYEES }) {
|
||||||
const [formData, setFormData] = useState(emptyForm)
|
const [formData, setFormData] = useState(emptyForm)
|
||||||
const [recentTickets, setRecentTickets] = useState([])
|
const [recentTickets, setRecentTickets] = useState([])
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
const errorRef = useRef(null)
|
||||||
|
useScrollIntoView(errorRef, error)
|
||||||
|
|
||||||
// Reset error when modal opens
|
// Reset error when modal opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -122,145 +125,132 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
|
|||||||
if (!isOpen) return null
|
if (!isOpen) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overlay">
|
<Dialog
|
||||||
<span className="overlay-close" onClick={onClose}>
|
as="form"
|
||||||
<FaTimes />
|
wide
|
||||||
</span>
|
title="Neues Ticket"
|
||||||
<div className="overlay-content">
|
busy={loading}
|
||||||
<h2 className="mb-2">Neues Ticket</h2>
|
onClose={onClose}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<span className="grow" />
|
||||||
|
<Button variant="primary" type="submit" disabled={loading}>
|
||||||
|
{loading ? 'Wird angelegt …' : 'Ticket anlegen'}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{error && (
|
||||||
|
<div className="msg err" role="alert" ref={errorRef}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{error && (
|
<div className="field-row">
|
||||||
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
|
<Field label="Kunde" htmlFor="ticket-customer">
|
||||||
{error}
|
<select
|
||||||
</div>
|
id="ticket-customer"
|
||||||
)}
|
className="select"
|
||||||
|
value={formData.customerId}
|
||||||
|
onChange={(e) => handleChange('customerId', e.target.value)}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Kunde wählen</option>
|
||||||
|
{customers.map(c => (
|
||||||
|
<option key={c.$id} value={c.$id}>({c.code || ''}) {c.name || 'Unnamed'}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<Field label="Art" htmlFor="ticket-type">
|
||||||
<div className="row">
|
<select
|
||||||
<div className="col col-6">
|
id="ticket-type"
|
||||||
<div className="form-group">
|
className="select"
|
||||||
<label className="form-label">Kunde</label>
|
value={formData.type}
|
||||||
<select
|
onChange={(e) => handleChange('type', e.target.value)}
|
||||||
className="form-control"
|
>
|
||||||
value={formData.customerId}
|
{TICKET_TYPES.map(type => (
|
||||||
onChange={(e) => handleChange('customerId', e.target.value)}
|
<option key={type.value} value={type.value}>{type.label}</option>
|
||||||
required
|
))}
|
||||||
>
|
</select>
|
||||||
<option value="">Kunde wählen</option>
|
</Field>
|
||||||
{customers.map(c => (
|
|
||||||
<option key={c.$id} value={c.$id}>({c.code || ''}) {c.name || 'Unnamed'}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label">Art</label>
|
|
||||||
<select
|
|
||||||
className="form-control"
|
|
||||||
value={formData.type}
|
|
||||||
onChange={(e) => handleChange('type', e.target.value)}
|
|
||||||
>
|
|
||||||
{TICKET_TYPES.map(type => (
|
|
||||||
<option key={type.value} value={type.value}>{type.label}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={formData.urgent}
|
|
||||||
onChange={(e) => handleChange('urgent', e.target.checked)}
|
|
||||||
/>
|
|
||||||
Dringend
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group automatic-assignment-note">
|
|
||||||
<label className="form-label">Zuweisung</label>
|
|
||||||
<p>
|
|
||||||
{employees.length > 0
|
|
||||||
? `Automatisch an den Mitarbeiter mit der geringsten Anzahl offener Tickets (${employees.length} verfügbar).`
|
|
||||||
: 'Es ist noch kein Mitarbeiter verfügbar. Das Ticket bleibt zunächst offen.'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col col-6">
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label">Betreff</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
placeholder="Worum geht es?"
|
|
||||||
value={formData.topic}
|
|
||||||
onChange={(e) => handleChange('topic', e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label" htmlFor="ticket-requested-by">Angefragt von</label>
|
|
||||||
<AutocompleteInput
|
|
||||||
id="ticket-requested-by"
|
|
||||||
placeholder="Name"
|
|
||||||
value={formData.requestedBy}
|
|
||||||
onChange={(value) => handleChange('requestedBy', value)}
|
|
||||||
getSuggestions={getSuggestions}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label" htmlFor="ticket-start-date">Beginn</label>
|
|
||||||
<DateTimeInput
|
|
||||||
id="ticket-start-date"
|
|
||||||
timeLabel="Uhrzeit Beginn"
|
|
||||||
date={formData.startDate}
|
|
||||||
time={formData.startTime}
|
|
||||||
onDateChange={handleStartDateChange}
|
|
||||||
onTimeChange={(value) => handleChange('startTime', value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label" htmlFor="ticket-end-date">Deadline</label>
|
|
||||||
<DateTimeInput
|
|
||||||
id="ticket-end-date"
|
|
||||||
timeLabel="Uhrzeit Deadline"
|
|
||||||
min={formData.startDate}
|
|
||||||
date={formData.deadline}
|
|
||||||
time={formData.endTime}
|
|
||||||
onDateChange={(value) => handleChange('deadline', value)}
|
|
||||||
onTimeChange={(value) => handleChange('endTime', value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label">Beschreibung</label>
|
|
||||||
<textarea
|
|
||||||
className="form-control"
|
|
||||||
rows={5}
|
|
||||||
placeholder="Was soll gemacht werden?"
|
|
||||||
value={formData.details}
|
|
||||||
onChange={(e) => handleChange('details', e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center mt-2">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="btn btn-dark"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{loading ? 'Wird angelegt …' : 'Ticket anlegen'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<Field label="Betreff" htmlFor="ticket-topic">
|
||||||
|
<input
|
||||||
|
id="ticket-topic"
|
||||||
|
type="text"
|
||||||
|
className="input"
|
||||||
|
placeholder="Worum geht es?"
|
||||||
|
value={formData.topic}
|
||||||
|
onChange={(e) => handleChange('topic', e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Angefragt von" htmlFor="ticket-requested-by">
|
||||||
|
<AutocompleteInput
|
||||||
|
id="ticket-requested-by"
|
||||||
|
placeholder="Name"
|
||||||
|
value={formData.requestedBy}
|
||||||
|
onChange={(value) => handleChange('requestedBy', value)}
|
||||||
|
getSuggestions={getSuggestions}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<div className="field-row">
|
||||||
|
<Field label="Beginn" htmlFor="ticket-start-date">
|
||||||
|
<DateTimeInput
|
||||||
|
id="ticket-start-date"
|
||||||
|
timeLabel="Uhrzeit Beginn"
|
||||||
|
date={formData.startDate}
|
||||||
|
time={formData.startTime}
|
||||||
|
onDateChange={handleStartDateChange}
|
||||||
|
onTimeChange={(value) => handleChange('startTime', value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Deadline" htmlFor="ticket-end-date">
|
||||||
|
<DateTimeInput
|
||||||
|
id="ticket-end-date"
|
||||||
|
timeLabel="Uhrzeit Deadline"
|
||||||
|
min={formData.startDate}
|
||||||
|
date={formData.deadline}
|
||||||
|
time={formData.endTime}
|
||||||
|
onDateChange={(value) => handleChange('deadline', value)}
|
||||||
|
onTimeChange={(value) => handleChange('endTime', value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Field label="Beschreibung" htmlFor="ticket-details">
|
||||||
|
<textarea
|
||||||
|
id="ticket-details"
|
||||||
|
className="textarea"
|
||||||
|
rows={5}
|
||||||
|
placeholder="Was soll gemacht werden?"
|
||||||
|
value={formData.details}
|
||||||
|
onChange={(e) => handleChange('details', e.target.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<label className="check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.urgent}
|
||||||
|
onChange={(e) => handleChange('urgent', e.target.checked)}
|
||||||
|
/>
|
||||||
|
Dringend
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label="Zuweisung"
|
||||||
|
hint={employees.length > 0
|
||||||
|
? `Automatisch an den Mitarbeiter mit der geringsten Anzahl offener Tickets (${employees.length} verfügbar).`
|
||||||
|
: 'Es ist noch kein Mitarbeiter verfügbar. Das Ticket bleibt zunächst offen.'}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { FaTimes } from 'react-icons/fa'
|
import { useScrollIntoView } from '../hooks/useScrollIntoView'
|
||||||
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, DurationSelect } from './ui'
|
import { Button, DateTimeInput, Dialog, DurationSelect, Field } from './ui'
|
||||||
import { roundUpToStep, isValidStep } from '../lib/timeSteps'
|
import { roundUpToStep, isValidStep } from '../lib/timeSteps'
|
||||||
import { validateSupporters } from '../lib/support'
|
import { validateSupporters } from '../lib/support'
|
||||||
import SupportersEditor from './SupportersEditor'
|
import SupportersEditor from './SupportersEditor'
|
||||||
@@ -35,7 +35,10 @@ const initialForm = (workorder, newStatus) => ({
|
|||||||
supporters: []
|
supporters: []
|
||||||
})
|
})
|
||||||
|
|
||||||
export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCreate, aboHoursAllowed = true }) {
|
// 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 { user } = useAuth()
|
||||||
|
|
||||||
const isAcquisition = workorder?.type === 'Akquise'
|
const isAcquisition = workorder?.type === 'Akquise'
|
||||||
@@ -51,6 +54,8 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [autoCalculate, setAutoCalculate] = useState(true)
|
const [autoCalculate, setAutoCalculate] = useState(true)
|
||||||
|
const errorRef = useRef(null)
|
||||||
|
useScrollIntoView(errorRef, error)
|
||||||
|
|
||||||
// Reset form wenn Modal geöffnet wird
|
// Reset form wenn Modal geöffnet wird
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -148,159 +153,146 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
if (!isOpen || !workorder) return null
|
if (!isOpen || !workorder) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overlay">
|
<Dialog
|
||||||
<span className="overlay-close" onClick={onClose}>
|
as="form"
|
||||||
<FaTimes />
|
wide
|
||||||
</span>
|
title={`Neues Arbeitsblatt – Ticket ${workorder.woid}`}
|
||||||
<div className="overlay-content">
|
busy={loading}
|
||||||
<h2 className="mb-2">Neues Arbeitsblatt – Ticket {workorder.woid}</h2>
|
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>
|
||||||
|
)}
|
||||||
|
|
||||||
{error && (
|
<div className="field-row">
|
||||||
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
|
<Field label="Art" htmlFor="worksheet-service-type">
|
||||||
{error}
|
<select
|
||||||
</div>
|
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>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<Field
|
||||||
<div className="row">
|
label={isAcquisition ? 'Akquise-Ergebnis' : 'Neuer Status'}
|
||||||
<div className="col col-6">
|
htmlFor="worksheet-new-status"
|
||||||
<div className="form-group">
|
hint={isAcquisition ? '"Zugesagt" macht den Lead automatisch zum festen Kunden.' : undefined}
|
||||||
<label className="form-label">Art</label>
|
>
|
||||||
<select
|
<select
|
||||||
className="form-control"
|
id="worksheet-new-status"
|
||||||
value={formData.serviceType}
|
className="select"
|
||||||
onChange={(e) => handleChange('serviceType', e.target.value)}
|
value={formData.newStatus}
|
||||||
required
|
onChange={(e) => handleChange('newStatus', e.target.value)}
|
||||||
>
|
required
|
||||||
{SERVICE_TYPES.map(type => (
|
>
|
||||||
<option key={type.value} value={type.value}>{type.label}</option>
|
{statusOptions.map(status => (
|
||||||
))}
|
<option key={status.value} value={status.value}>{status.label}</option>
|
||||||
</select>
|
))}
|
||||||
</div>
|
</select>
|
||||||
|
</Field>
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label">{isAcquisition ? 'Akquise-Ergebnis' : 'Neuer Status'}</label>
|
|
||||||
<select
|
|
||||||
className="form-control"
|
|
||||||
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>
|
|
||||||
{isAcquisition && (
|
|
||||||
<small style={{ color: 'var(--text-2)', fontSize: '12px' }}>
|
|
||||||
"Zugesagt" macht den Lead automatisch zum festen Kunden.
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={formData.isComment}
|
|
||||||
onChange={(e) => handleChange('isComment', e.target.checked)}
|
|
||||||
/>
|
|
||||||
Nur Kommentar (keine Arbeitszeit)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{aboHoursAllowed && (
|
|
||||||
<div className="form-group">
|
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px' }} 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>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col col-6">
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label" htmlFor="worksheet-start-date">Beginn</label>
|
|
||||||
<DateTimeInput
|
|
||||||
id="worksheet-start-date"
|
|
||||||
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-end-date">Ende</label>
|
|
||||||
<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)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label" htmlFor="worksheet-total">Arbeitszeit</label>
|
|
||||||
<DurationSelect
|
|
||||||
id="worksheet-total"
|
|
||||||
value={formData.isComment ? 0 : formData.totalTime}
|
|
||||||
onChange={(value) => handleChange('totalTime', value)}
|
|
||||||
disabled={formData.isComment}
|
|
||||||
/>
|
|
||||||
<small style={{ color: 'var(--text-2)', fontSize: '12px' }}>
|
|
||||||
{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'}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!formData.isComment && (
|
|
||||||
<SupportersEditor
|
|
||||||
value={formData.supporters}
|
|
||||||
onChange={(list) => handleChange('supporters', list)}
|
|
||||||
excludeUserId={user?.$id}
|
|
||||||
minutes={formData.totalTime}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label">Was wurde gemacht?</label>
|
|
||||||
<textarea
|
|
||||||
className="form-control"
|
|
||||||
rows={5}
|
|
||||||
placeholder="Beschreibe die durchgeführten Arbeiten..."
|
|
||||||
value={formData.details}
|
|
||||||
onChange={(e) => handleChange('details', e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center mt-2">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="btn btn-dark"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{loading ? 'Speichert …' : 'Speichern'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { FaTimes } from 'react-icons/fa'
|
import { useScrollIntoView } from '../hooks/useScrollIntoView'
|
||||||
import { parseSupporters, supportMinutes } from '../lib/support'
|
import { parseSupporters, supportMinutes } from '../lib/support'
|
||||||
import { stepLabel } from '../lib/timeSteps'
|
import { stepLabel } from '../lib/timeSteps'
|
||||||
|
import { Button, Dialog, Field } from './ui'
|
||||||
|
|
||||||
/* "Ich habe unterstuetzt": die eigene Unterstuetzung bei einem fremden Arbeitsblatt
|
/* "Ich habe unterstuetzt": die eigene Unterstuetzung bei einem fremden Arbeitsblatt
|
||||||
eintragen, aendern oder entfernen. Andere Eintraege und die Zeiten bleiben unberuehrt. */
|
eintragen, aendern oder entfernen. Andere Eintraege und die Zeiten bleiben unberuehrt. */
|
||||||
@@ -10,6 +11,14 @@ export default function OwnSupportModal({ worksheet, currentUserId, onClose, onS
|
|||||||
const [task, setTask] = useState(existing?.task || '')
|
const [task, setTask] = useState(existing?.task || '')
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
const taskRef = useRef(null)
|
||||||
|
const errorRef = useRef(null)
|
||||||
|
useScrollIntoView(errorRef, error)
|
||||||
|
|
||||||
|
// Der Dialog setzt den Fokus beim Oeffnen auf sich selbst; dieser Effekt laeuft danach (Kinder vor Eltern) und holt ihn ins Feld
|
||||||
|
useEffect(() => {
|
||||||
|
taskRef.current?.focus()
|
||||||
|
}, [])
|
||||||
|
|
||||||
const save = async (value) => {
|
const save = async (value) => {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
@@ -33,48 +42,48 @@ export default function OwnSupportModal({ worksheet, currentUserId, onClose, onS
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overlay">
|
<Dialog
|
||||||
<span className="overlay-close" onClick={onClose}>
|
as="form"
|
||||||
<FaTimes />
|
title="Unterstützung eintragen"
|
||||||
</span>
|
busy={saving}
|
||||||
<div className="overlay-content" style={{ maxWidth: 480 }}>
|
onClose={onClose}
|
||||||
<h2 className="mb-2">Unterstützung eintragen</h2>
|
onSubmit={handleSubmit}
|
||||||
<p className="text-grey" style={{ fontSize: '14px', marginBottom: '16px' }}>
|
footer={
|
||||||
WSID {worksheet.wsid} · {worksheet.employeeName}
|
<>
|
||||||
<br />Dir werden {stepLabel(supportMinutes(worksheet.totalTime))} angerechnet.
|
{existing && (
|
||||||
</p>
|
<Button variant="danger" type="button" disabled={saving} onClick={() => save(null)}>
|
||||||
|
Eintrag entfernen
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<span className="grow" />
|
||||||
|
<Button variant="primary" type="submit" disabled={saving}>
|
||||||
|
{saving ? 'Speichert …' : 'Speichern'}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{error && (
|
||||||
|
<div className="msg err" role="alert" ref={errorRef}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{error && (
|
<p className="muted">
|
||||||
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
|
WSID {worksheet.wsid} · {worksheet.employeeName}
|
||||||
{error}
|
<br />Dir werden {stepLabel(supportMinutes(worksheet.totalTime))} angerechnet.
|
||||||
</div>
|
</p>
|
||||||
)}
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<Field label="Wobei?" htmlFor="own-support-task">
|
||||||
<div className="form-group">
|
<input
|
||||||
<label className="form-label" htmlFor="own-support-task">Wobei?</label>
|
id="own-support-task"
|
||||||
<input
|
ref={taskRef}
|
||||||
id="own-support-task"
|
className="input"
|
||||||
className="form-control"
|
maxLength={200}
|
||||||
maxLength={200}
|
required
|
||||||
required
|
value={task}
|
||||||
value={task}
|
onChange={(e) => setTask(e.target.value)}
|
||||||
onChange={(e) => setTask(e.target.value)}
|
/>
|
||||||
autoFocus
|
</Field>
|
||||||
/>
|
</Dialog>
|
||||||
</div>
|
|
||||||
<div className="text-center mt-2 flex gap-2" style={{ justifyContent: 'center' }}>
|
|
||||||
<button type="submit" className="btn btn-dark" disabled={saving}>
|
|
||||||
{saving ? 'Speichert...' : 'Speichern'}
|
|
||||||
</button>
|
|
||||||
{existing && (
|
|
||||||
<button type="button" className="btn" disabled={saving} onClick={() => save(null)}>
|
|
||||||
Eintrag entfernen
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { FaTimes } from 'react-icons/fa'
|
|
||||||
import { useEmployees } from '../hooks/useEmployees'
|
|
||||||
import { supportMinutes } from '../lib/support'
|
import { supportMinutes } from '../lib/support'
|
||||||
import { stepLabel } from '../lib/timeSteps'
|
import { stepLabel } from '../lib/timeSteps'
|
||||||
|
|
||||||
|
// Stabile Vorgabe, damit `choices` und die Effekte der Aufrufer nicht bei jedem Render neu entstehen
|
||||||
|
const NO_EMPLOYEES_STATE = { employees: [], loading: false, error: null }
|
||||||
|
|
||||||
/* Haken "Unterstuetzung" mit Zeilen Mitarbeiter + "Wobei?". Nur fuer die Person, die das
|
/* Haken "Unterstuetzung" mit Zeilen Mitarbeiter + "Wobei?". Nur fuer die Person, die das
|
||||||
Arbeitsblatt angelegt hat (sie selbst steht nicht in der Auswahl). Die Mitarbeiterliste
|
Arbeitsblatt angelegt hat (sie selbst steht nicht in der Auswahl). Die Mitarbeiterliste
|
||||||
laedt erst hier, damit nicht jede Ticket-Karte sie abruft. */
|
kommt von der Seite (employeesState = Werte von useEmployees, dort einmal geladen),
|
||||||
export default function SupportersEditor({ value, onChange, excludeUserId, minutes }) {
|
der Editor ruft den Hook nicht selbst auf. */
|
||||||
const { employees, loading, error } = useEmployees()
|
export default function SupportersEditor({ value, onChange, excludeUserId, minutes, employeesState = NO_EMPLOYEES_STATE }) {
|
||||||
|
const { employees, loading, error } = employeesState
|
||||||
const [enabled, setEnabled] = useState(value.length > 0)
|
const [enabled, setEnabled] = useState(value.length > 0)
|
||||||
const perPerson = stepLabel(supportMinutes(minutes))
|
const perPerson = stepLabel(supportMinutes(minutes))
|
||||||
const choices = employees.filter((e) => e.userId && e.userId !== excludeUserId)
|
const choices = employees.filter((e) => e.userId && e.userId !== excludeUserId)
|
||||||
@@ -29,20 +31,20 @@ export default function SupportersEditor({ value, onChange, excludeUserId, minut
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="form-group supporters-editor">
|
<div className="field supporters-editor">
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
<label className="check">
|
||||||
<input type="checkbox" checked={enabled} onChange={(e) => toggle(e.target.checked)} />
|
<input type="checkbox" checked={enabled} onChange={(e) => toggle(e.target.checked)} />
|
||||||
Unterstützung
|
Unterstützung
|
||||||
</label>
|
</label>
|
||||||
{enabled && (
|
{enabled && (
|
||||||
<div style={{ marginTop: 8 }}>
|
<>
|
||||||
{!loading && (error || !choices.length) && (
|
{!loading && (error || !choices.length) && (
|
||||||
<p className="text-grey" style={{ fontSize: 13 }}>Mitarbeiterliste konnte nicht geladen werden.</p>
|
<div className="hint">Mitarbeiterliste konnte nicht geladen werden.</div>
|
||||||
)}
|
)}
|
||||||
{value.map((row, index) => (
|
{value.map((row, index) => (
|
||||||
<div key={index} className="flex gap-2" style={{ marginBottom: 8, alignItems: 'center' }}>
|
<div key={index} className="supporter-row">
|
||||||
<select
|
<select
|
||||||
className="form-control"
|
className="select"
|
||||||
aria-label="Mitarbeiter"
|
aria-label="Mitarbeiter"
|
||||||
value={row.employeeId}
|
value={row.employeeId}
|
||||||
onChange={(e) => pick(index, e.target.value)}
|
onChange={(e) => pick(index, e.target.value)}
|
||||||
@@ -53,23 +55,27 @@ export default function SupportersEditor({ value, onChange, excludeUserId, minut
|
|||||||
.map((e) => <option key={e.userId} value={e.userId}>{e.displayName}</option>)}
|
.map((e) => <option key={e.userId} value={e.userId}>{e.displayName}</option>)}
|
||||||
</select>
|
</select>
|
||||||
<input
|
<input
|
||||||
className="form-control"
|
className="input"
|
||||||
aria-label="Wobei?"
|
aria-label="Wobei?"
|
||||||
placeholder="Wobei?"
|
placeholder="Wobei?"
|
||||||
maxLength={200}
|
maxLength={200}
|
||||||
value={row.task}
|
value={row.task}
|
||||||
onChange={(e) => update(index, { task: e.target.value })}
|
onChange={(e) => update(index, { task: e.target.value })}
|
||||||
/>
|
/>
|
||||||
<span className="text-grey" style={{ whiteSpace: 'nowrap', fontSize: 13 }}>= {perPerson}</span>
|
<div className="sup-meta">
|
||||||
<button type="button" className="btn btn-sm" aria-label="Zeile entfernen" onClick={() => remove(index)}>
|
<span>= {perPerson}</span>
|
||||||
<FaTimes />
|
<button type="button" className="link" aria-label="Zeile entfernen" onClick={() => remove(index)}>
|
||||||
</button>
|
Entfernen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<button type="button" className="btn btn-sm" onClick={() => onChange([...value, { employeeId: '', name: '', task: '' }])}>
|
<div>
|
||||||
+ weitere Person
|
<button type="button" className="link" onClick={() => onChange([...value, { employeeId: '', name: '', task: '' }])}>
|
||||||
</button>
|
+ weitere Person
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useRef, useState } from 'react'
|
||||||
import { FaTimes } from 'react-icons/fa'
|
import { useScrollIntoView } from '../hooks/useScrollIntoView'
|
||||||
import { isoToGermanDate, timeToWoms, germanDateToIso, womsTimeToInput, minutesBetween, isGitWorksheet, needsTimeEntry } from '../lib/ticketForm'
|
import { isoToGermanDate, timeToWoms, germanDateToIso, womsTimeToInput, minutesBetween, isGitWorksheet, needsTimeEntry } from '../lib/ticketForm'
|
||||||
import { DateTimeInput, DurationSelect } from './ui'
|
import { Button, DateTimeInput, Dialog, DurationSelect, Field } from './ui'
|
||||||
import { roundUpToStep, isValidStep } from '../lib/timeSteps'
|
import { roundUpToStep, isValidStep } from '../lib/timeSteps'
|
||||||
import { parseSupporters, validateSupporters } from '../lib/support'
|
import { parseSupporters, validateSupporters } from '../lib/support'
|
||||||
import SupportersEditor from './SupportersEditor'
|
import SupportersEditor from './SupportersEditor'
|
||||||
@@ -14,11 +14,14 @@ const formFromWorksheet = (worksheet) => ({
|
|||||||
endTime: womsTimeToInput(worksheet.endTime)
|
endTime: womsTimeToInput(worksheet.endTime)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Mitarbeiterliste der Seite (useEmployees laeuft dort einmal); der Editor ruft den Hook nicht selbst auf
|
||||||
|
const NO_EMPLOYEES_STATE = { employees: [], loading: false, error: null }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Beginn, Ende und Arbeitszeit eines Arbeitsblatts nachtraeglich aendern.
|
* Beginn, Ende und Arbeitszeit eines Arbeitsblatts nachtraeglich aendern.
|
||||||
* Wer das darf, entscheidet canEditWorksheetTimes (lib/ticketForm).
|
* Wer das darf, entscheidet canEditWorksheetTimes (lib/ticketForm).
|
||||||
*/
|
*/
|
||||||
export default function WorksheetTimesModal({ worksheet, currentUserId, onClose, onSave }) {
|
export default function WorksheetTimesModal({ worksheet, currentUserId, onClose, onSave, employeesState = NO_EMPLOYEES_STATE }) {
|
||||||
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 (aufgerundet auf 15 Minuten). Passte sie
|
// null = die Arbeitszeit folgt Beginn und Ende (aufgerundet auf 15 Minuten). Passte sie
|
||||||
@@ -33,6 +36,8 @@ export default function WorksheetTimesModal({ worksheet, currentUserId, onClose,
|
|||||||
const [supporters, setSupporters] = useState(() => parseSupporters(worksheet.supporters))
|
const [supporters, setSupporters] = useState(() => parseSupporters(worksheet.supporters))
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
const errorRef = useRef(null)
|
||||||
|
useScrollIntoView(errorRef, error)
|
||||||
|
|
||||||
const calculatedMinutes = minutesBetween(formData.startTime, formData.endTime)
|
const calculatedMinutes = minutesBetween(formData.startTime, formData.endTime)
|
||||||
const calculatedTotal = calculatedMinutes === null ? null : roundUpToStep(calculatedMinutes)
|
const calculatedTotal = calculatedMinutes === null ? null : roundUpToStep(calculatedMinutes)
|
||||||
@@ -85,82 +90,81 @@ export default function WorksheetTimesModal({ worksheet, currentUserId, onClose,
|
|||||||
setSaving(false)
|
setSaving(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kurzer Titel: auf dem Handy laeuft ein langer unter das Schliessen-X
|
||||||
return (
|
return (
|
||||||
<div className="overlay">
|
<Dialog
|
||||||
<span className="overlay-close" onClick={onClose}>
|
as="form"
|
||||||
<FaTimes />
|
title={needsTimeEntry(worksheet) ? 'Zeit nachtragen' : 'Zeiten bearbeiten'}
|
||||||
</span>
|
busy={saving}
|
||||||
<div className="overlay-content" style={{ maxWidth: 560 }}>
|
onClose={onClose}
|
||||||
{/* Kurzer Titel: auf dem Handy laeuft ein langer unter das Schliessen-X */}
|
onSubmit={handleSubmit}
|
||||||
<h2 className="mb-2">{needsTimeEntry(worksheet) ? 'Zeit nachtragen' : 'Zeiten bearbeiten'}</h2>
|
footer={
|
||||||
<p className="text-grey" style={{ fontSize: '14px', marginBottom: '16px' }}>
|
<>
|
||||||
WSID {worksheet.wsid}
|
<span className="grow" />
|
||||||
{isGit && <><br />Das Ende ist der Zeitpunkt des Git-Pushs. Trag ein, wann die Arbeit begonnen hat.</>}
|
<Button variant="primary" type="submit" disabled={saving}>
|
||||||
</p>
|
{saving ? 'Speichert …' : 'Speichern'}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{error && (
|
||||||
|
<div className="msg err" role="alert" ref={errorRef}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{error && (
|
<p className="muted">
|
||||||
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
|
WSID {worksheet.wsid}
|
||||||
{error}
|
{isGit && <><br />Das Ende ist der Zeitpunkt des Git-Pushs. Trag ein, wann die Arbeit begonnen hat.</>}
|
||||||
</div>
|
</p>
|
||||||
)}
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<Field label="Beginn" htmlFor="worksheet-times-start">
|
||||||
<div className="form-group">
|
<DateTimeInput
|
||||||
<label className="form-label" htmlFor="worksheet-times-start">Beginn</label>
|
id="worksheet-times-start"
|
||||||
<DateTimeInput
|
timeLabel="Uhrzeit Beginn"
|
||||||
id="worksheet-times-start"
|
required
|
||||||
timeLabel="Uhrzeit Beginn"
|
timeRequired={isGit}
|
||||||
required
|
date={formData.startDate}
|
||||||
timeRequired={isGit}
|
time={formData.startTime}
|
||||||
date={formData.startDate}
|
onDateChange={handleStartDateChange}
|
||||||
time={formData.startTime}
|
onTimeChange={(value) => handleChange('startTime', value)}
|
||||||
onDateChange={handleStartDateChange}
|
/>
|
||||||
onTimeChange={(value) => handleChange('startTime', value)}
|
</Field>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
<Field label="Ende" htmlFor="worksheet-times-end">
|
||||||
<label className="form-label" htmlFor="worksheet-times-end">Ende</label>
|
<DateTimeInput
|
||||||
<DateTimeInput
|
id="worksheet-times-end"
|
||||||
id="worksheet-times-end"
|
timeLabel="Uhrzeit Ende"
|
||||||
timeLabel="Uhrzeit Ende"
|
required
|
||||||
required
|
min={formData.startDate}
|
||||||
min={formData.startDate}
|
date={formData.endDate}
|
||||||
date={formData.endDate}
|
time={formData.endTime}
|
||||||
time={formData.endTime}
|
onDateChange={(value) => handleChange('endDate', value)}
|
||||||
onDateChange={(value) => handleChange('endDate', value)}
|
onTimeChange={(value) => handleChange('endTime', value)}
|
||||||
onTimeChange={(value) => handleChange('endTime', value)}
|
/>
|
||||||
/>
|
</Field>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
<Field
|
||||||
<label className="form-label" htmlFor="worksheet-times-total">Arbeitszeit</label>
|
label="Arbeitszeit"
|
||||||
<DurationSelect
|
htmlFor="worksheet-times-total"
|
||||||
id="worksheet-times-total"
|
hint={manualTotal === null && calculatedTotal !== null
|
||||||
value={totalTime}
|
? 'Aus Beginn und Ende berechnet, aufgerundet auf 15 Minuten'
|
||||||
onChange={(value) => setManualTotal(value)}
|
: 'Von Hand gewählt'}
|
||||||
/>
|
>
|
||||||
<small style={{ color: 'var(--text-2)', fontSize: '12px' }}>
|
<DurationSelect
|
||||||
{manualTotal === null && calculatedTotal !== null
|
id="worksheet-times-total"
|
||||||
? '✓ Aus Beginn und Ende berechnet, aufgerundet auf 15 Minuten'
|
value={totalTime}
|
||||||
: 'Von Hand gewählt'}
|
onChange={(value) => setManualTotal(value)}
|
||||||
</small>
|
/>
|
||||||
</div>
|
</Field>
|
||||||
|
|
||||||
<SupportersEditor
|
<SupportersEditor
|
||||||
value={supporters}
|
value={supporters}
|
||||||
onChange={setSupporters}
|
onChange={setSupporters}
|
||||||
excludeUserId={currentUserId}
|
excludeUserId={currentUserId}
|
||||||
minutes={totalTime}
|
minutes={totalTime}
|
||||||
/>
|
employeesState={employeesState}
|
||||||
|
/>
|
||||||
<div className="text-center mt-2">
|
</Dialog>
|
||||||
<button type="submit" className="btn btn-dark" disabled={saving}>
|
|
||||||
{saving ? 'Speichert...' : 'Speichern'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1051,6 +1051,8 @@ button.link { font-weight: 500; } /* font: inherit setzt sonst das Gewicht von .
|
|||||||
/* ---------- Unterstuetzung im Dialog ---------- */
|
/* ---------- Unterstuetzung im Dialog ---------- */
|
||||||
.supporter-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 8px 12px; align-items: center; margin-top: 8px; }
|
.supporter-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 8px 12px; align-items: center; margin-top: 8px; }
|
||||||
.supporter-row .sup-meta { grid-column: 1 / -1; display: flex; justify-content: space-between; align-items: center; font-size: 13px; color: var(--text-2); }
|
.supporter-row .sup-meta { grid-column: 1 / -1; display: flex; justify-content: space-between; align-items: center; font-size: 13px; color: var(--text-2); }
|
||||||
|
/* Kaestchen im Dialog stehen in einer Spalte (flex): ohne das waere die ganze Zeile klickbar */
|
||||||
|
.dialog-body > .check, .supporters-editor > .check { align-self: flex-start; }
|
||||||
|
|
||||||
@media (max-width: 860px) {
|
@media (max-width: 860px) {
|
||||||
.ws { grid-template-columns: minmax(0, 1fr) auto; }
|
.ws { grid-template-columns: minmax(0, 1fr) auto; }
|
||||||
|
|||||||
Reference in new issue
Block a user