Unterstützung: Ersteller trägt helfende Personen im Arbeitsblatt ein

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-09-30 14:57:46 +00:00
1 parent 3535f18a2b
commit d83bbfb48d
5 files changed
+139 -4

No files matched your search

+22 -2
View File
@@ -5,6 +5,8 @@ import { SERVICE_TYPES, todayIso, isoToGermanDate, timeToWoms, minutesBetween }
import { TICKET_STATUSES, statusLabel } from '../lib/ticketStatus'
import { DateTimeInput, DurationSelect } 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']
@@ -29,7 +31,8 @@ const initialForm = (workorder, newStatus) => ({
endTime: '',
details: '',
isComment: false,
countsToPlan: false
countsToPlan: false,
supporters: []
})
export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCreate, aboHoursAllowed = true }) {
@@ -101,6 +104,13 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
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,
@@ -116,7 +126,8 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
details: formData.details,
isComment: formData.isComment,
countsToPlan: aboHoursAllowed && !formData.isComment && formData.countsToPlan,
employeeShort: user?.prefs?.shortCode || '' // Aus User-Preferences
employeeShort: user?.prefs?.shortCode || '', // Aus User-Preferences
supporters
}
const result = await onCreate(worksheetData, user)
@@ -257,6 +268,15 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
</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
+76
View File
@@ -0,0 +1,76 @@
import { useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { useEmployees } from '../hooks/useEmployees'
import { supportMinutes } from '../lib/support'
import { stepLabel } from '../lib/timeSteps'
/* Haken "Unterstuetzung" mit Zeilen Mitarbeiter + "Wobei?". Nur fuer die Person, die das
Arbeitsblatt angelegt hat (sie selbst steht nicht in der Auswahl). Die Mitarbeiterliste
laedt erst hier, damit nicht jede Ticket-Karte sie abruft. */
export default function SupportersEditor({ value, onChange, excludeUserId, minutes }) {
const { employees, loading, error } = useEmployees()
const [enabled, setEnabled] = useState(value.length > 0)
const perPerson = stepLabel(supportMinutes(minutes))
const choices = employees.filter((e) => e.userId && e.userId !== excludeUserId)
const toggle = (checked) => {
setEnabled(checked)
onChange(checked ? [{ employeeId: '', name: '', task: '' }] : [])
}
const update = (index, patch) => onChange(value.map((row, i) => (i === index ? { ...row, ...patch } : row)))
const pick = (index, userId) => {
const employee = choices.find((e) => e.userId === userId)
update(index, { employeeId: userId, name: employee?.displayName || '' })
}
const remove = (index) => {
const next = value.filter((_, i) => i !== index)
onChange(next)
if (!next.length) setEnabled(false)
}
return (
<div className="form-group supporters-editor">
<label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<input type="checkbox" checked={enabled} onChange={(e) => toggle(e.target.checked)} />
Unterstützung
</label>
{enabled && (
<div style={{ marginTop: 8 }}>
{!loading && (error || !choices.length) && (
<p className="text-grey" style={{ fontSize: 13 }}>Mitarbeiterliste konnte nicht geladen werden.</p>
)}
{value.map((row, index) => (
<div key={index} className="flex gap-2" style={{ marginBottom: 8, alignItems: 'center' }}>
<select
className="form-control"
aria-label="Mitarbeiter"
value={row.employeeId}
onChange={(e) => pick(index, e.target.value)}
>
<option value="">Person wählen …</option>
{choices
.filter((e) => e.userId === row.employeeId || !value.some((r) => r.employeeId === e.userId))
.map((e) => <option key={e.userId} value={e.userId}>{e.displayName}</option>)}
</select>
<input
className="form-control"
aria-label="Wobei?"
placeholder="Wobei?"
maxLength={200}
value={row.task}
onChange={(e) => update(index, { task: e.target.value })}
/>
<span className="text-grey" style={{ whiteSpace: 'nowrap', fontSize: 13 }}>= {perPerson}</span>
<button type="button" className="btn btn-sm" aria-label="Zeile entfernen" onClick={() => remove(index)}>
<FaTimes />
</button>
</div>
))}
<button type="button" className="btn btn-sm" onClick={() => onChange([...value, { employeeId: '', name: '', task: '' }])}>
+ weitere Person
</button>
</div>
)}
</div>
)
}
+1
View File
@@ -159,6 +159,7 @@ export default function TicketCard({ ticket, onUpdate }) {
{editTimesWorksheet && (
<WorksheetTimesModal
worksheet={editTimesWorksheet}
currentUserId={user?.$id}
onClose={() => setEditTimesWorksheet(null)}
onSave={(ws, times) => updateWorksheetTimes(ws, times, user, employee)}
/>
+19 -2
View File
@@ -3,6 +3,8 @@ import { FaTimes } from 'react-icons/fa'
import { isoToGermanDate, timeToWoms, germanDateToIso, womsTimeToInput, minutesBetween, isGitWorksheet, needsTimeEntry } from '../lib/ticketForm'
import { DateTimeInput, DurationSelect } from './ui'
import { roundUpToStep, isValidStep } from '../lib/timeSteps'
import { parseSupporters, validateSupporters } from '../lib/support'
import SupportersEditor from './SupportersEditor'
// Gespeichert ist dd.mm.yyyy / hhmm, die Kalender-Felder brauchen yyyy-mm-dd / hh:mm
const formFromWorksheet = (worksheet) => ({
@@ -16,7 +18,7 @@ const formFromWorksheet = (worksheet) => ({
* Beginn, Ende und Arbeitszeit eines Arbeitsblatts nachtraeglich aendern.
* Wer das darf, entscheidet canEditWorksheetTimes (lib/ticketForm).
*/
export default function WorksheetTimesModal({ worksheet, onClose, onSave }) {
export default function WorksheetTimesModal({ worksheet, currentUserId, onClose, onSave }) {
const isGit = isGitWorksheet(worksheet)
const [formData, setFormData] = useState(() => formFromWorksheet(worksheet))
// null = die Arbeitszeit folgt Beginn und Ende (aufgerundet auf 15 Minuten). Passte sie
@@ -28,6 +30,7 @@ export default function WorksheetTimesModal({ worksheet, onClose, onSave }) {
if (!stored || (calculated !== null && stored === roundUpToStep(calculated))) return null
return isValidStep(stored) ? stored : roundUpToStep(stored)
})
const [supporters, setSupporters] = useState(() => parseSupporters(worksheet.supporters))
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
@@ -54,6 +57,11 @@ export default function WorksheetTimesModal({ worksheet, onClose, onSave }) {
setError('Bitte die Arbeitszeit auswählen.')
return
}
const supportError = validateSupporters(supporters, currentUserId)
if (supportError) {
setError(supportError)
return
}
setSaving(true)
setError('')
try {
@@ -62,7 +70,9 @@ export default function WorksheetTimesModal({ worksheet, onClose, onSave }) {
startTime: timeToWoms(formData.startTime),
endDate: isoToGermanDate(formData.endDate),
endTime: timeToWoms(formData.endTime),
totalTime: Number(totalTime)
totalTime: Number(totalTime),
supporters,
supportersBefore: worksheet.supporters
})
if (result?.success) {
onClose()
@@ -137,6 +147,13 @@ export default function WorksheetTimesModal({ worksheet, onClose, onSave }) {
</small>
</div>
<SupportersEditor
value={supporters}
onChange={setSupporters}
excludeUserId={currentUserId}
minutes={totalTime}
/>
<div className="text-center mt-2">
<button type="submit" className="btn btn-dark" disabled={saving}>
{saving ? 'Speichert...' : 'Speichern'}
+21
View File
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react'
import { databases, DATABASE_ID, COLLECTIONS, Query, ID } from '../lib/appwrite'
import { canEditWorksheetTimes, isGitWorksheet } from '../lib/ticketForm'
import { roundUpToStep, isValidStep } from '../lib/timeSteps'
import { parseSupporters, serializeSupporters } from '../lib/support'
const STEP_ERROR = 'Die Arbeitszeit muss in 15-Minuten-Schritten angegeben werden.'
@@ -181,6 +182,10 @@ export function useWorksheets(woid = null) {
countsToPlan: !data.isComment && Boolean(data.countsToPlan),
createdAt: new Date().toISOString()
}
// Nur mitschicken, wenn es Unterstuetzung gibt
if (!data.isComment && data.supporters?.length) {
worksheetData.supporters = serializeSupporters(data.supporters)
}
console.log('Creating worksheet with data:', worksheetData)
@@ -232,6 +237,22 @@ export function useWorksheets(woid = null) {
const { startDate, startTime, endDate, endTime, totalTime } = times
if (!isValidStep(totalTime)) return { success: false, error: STEP_ERROR }
const data = { startDate, startTime, endDate, endTime, totalTime }
// Unterstuetzung nur schreiben, wenn sie sich geaendert hat. Hat inzwischen jemand anderes
// (z. B. ueber "Ich habe unterstuetzt") etwas geaendert, nicht still ueberschreiben.
if (times.supporters) {
const next = serializeSupporters(times.supporters)
if (next !== serializeSupporters(parseSupporters(times.supportersBefore))) {
try {
const fresh = await databases.getDocument(DATABASE_ID, COLLECTIONS.WORKSHEETS, worksheet.$id)
if ((fresh.supporters ?? '') !== (times.supportersBefore ?? '')) {
return { success: false, error: 'Inzwischen hat jemand die Unterstützung geändert. Bitte das Formular schließen und neu öffnen.' }
}
} catch (err) {
return { success: false, error: err.message }
}
data.supporters = next
}
}
// Mit nachgetragener Zeit zaehlt ein Git-Push als Arbeitszeit, nicht mehr als Kommentar
if (isGitWorksheet(worksheet)) data.isComment = false
return updateWorksheet(worksheet.$id, data)