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:
1 parent
3535f18a2b
commit
d83bbfb48d
5 files changed
+139
-4
No files matched your search
@@ -5,6 +5,8 @@ import { SERVICE_TYPES, todayIso, isoToGermanDate, timeToWoms, minutesBetween }
|
|||||||
import { TICKET_STATUSES, statusLabel } from '../lib/ticketStatus'
|
import { TICKET_STATUSES, statusLabel } from '../lib/ticketStatus'
|
||||||
import { DateTimeInput, DurationSelect } from './ui'
|
import { DateTimeInput, DurationSelect } from './ui'
|
||||||
import { roundUpToStep, isValidStep } from '../lib/timeSteps'
|
import { roundUpToStep, isValidStep } from '../lib/timeSteps'
|
||||||
|
import { validateSupporters } from '../lib/support'
|
||||||
|
import SupportersEditor from './SupportersEditor'
|
||||||
|
|
||||||
// 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']
|
||||||
@@ -29,7 +31,8 @@ const initialForm = (workorder, newStatus) => ({
|
|||||||
endTime: '',
|
endTime: '',
|
||||||
details: '',
|
details: '',
|
||||||
isComment: false,
|
isComment: false,
|
||||||
countsToPlan: false
|
countsToPlan: false,
|
||||||
|
supporters: []
|
||||||
})
|
})
|
||||||
|
|
||||||
export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCreate, aboHoursAllowed = true }) {
|
export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCreate, aboHoursAllowed = true }) {
|
||||||
@@ -101,6 +104,13 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const supporters = formData.isComment ? [] : formData.supporters
|
||||||
|
const supportError = validateSupporters(supporters, user?.$id)
|
||||||
|
if (supportError) {
|
||||||
|
setError(supportError)
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const worksheetData = {
|
const worksheetData = {
|
||||||
woid: workorder.woid,
|
woid: workorder.woid,
|
||||||
@@ -116,7 +126,8 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
details: formData.details,
|
details: formData.details,
|
||||||
isComment: formData.isComment,
|
isComment: formData.isComment,
|
||||||
countsToPlan: aboHoursAllowed && !formData.isComment && formData.countsToPlan,
|
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)
|
const result = await onCreate(worksheetData, user)
|
||||||
@@ -257,6 +268,15 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!formData.isComment && (
|
||||||
|
<SupportersEditor
|
||||||
|
value={formData.supporters}
|
||||||
|
onChange={(list) => handleChange('supporters', list)}
|
||||||
|
excludeUserId={user?.$id}
|
||||||
|
minutes={formData.totalTime}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">Was wurde gemacht?</label>
|
<label className="form-label">Was wurde gemacht?</label>
|
||||||
<textarea
|
<textarea
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -159,6 +159,7 @@ export default function TicketCard({ ticket, onUpdate }) {
|
|||||||
{editTimesWorksheet && (
|
{editTimesWorksheet && (
|
||||||
<WorksheetTimesModal
|
<WorksheetTimesModal
|
||||||
worksheet={editTimesWorksheet}
|
worksheet={editTimesWorksheet}
|
||||||
|
currentUserId={user?.$id}
|
||||||
onClose={() => setEditTimesWorksheet(null)}
|
onClose={() => setEditTimesWorksheet(null)}
|
||||||
onSave={(ws, times) => updateWorksheetTimes(ws, times, user, employee)}
|
onSave={(ws, times) => updateWorksheetTimes(ws, times, user, employee)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { FaTimes } from 'react-icons/fa'
|
|||||||
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 { DateTimeInput, DurationSelect } from './ui'
|
||||||
import { roundUpToStep, isValidStep } from '../lib/timeSteps'
|
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
|
// Gespeichert ist dd.mm.yyyy / hhmm, die Kalender-Felder brauchen yyyy-mm-dd / hh:mm
|
||||||
const formFromWorksheet = (worksheet) => ({
|
const formFromWorksheet = (worksheet) => ({
|
||||||
@@ -16,7 +18,7 @@ const formFromWorksheet = (worksheet) => ({
|
|||||||
* 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, onClose, onSave }) {
|
export default function WorksheetTimesModal({ worksheet, currentUserId, onClose, onSave }) {
|
||||||
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
|
||||||
@@ -28,6 +30,7 @@ export default function WorksheetTimesModal({ worksheet, onClose, onSave }) {
|
|||||||
if (!stored || (calculated !== null && stored === roundUpToStep(calculated))) return null
|
if (!stored || (calculated !== null && stored === roundUpToStep(calculated))) return null
|
||||||
return isValidStep(stored) ? stored : roundUpToStep(stored)
|
return isValidStep(stored) ? stored : roundUpToStep(stored)
|
||||||
})
|
})
|
||||||
|
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('')
|
||||||
|
|
||||||
@@ -54,6 +57,11 @@ export default function WorksheetTimesModal({ worksheet, onClose, onSave }) {
|
|||||||
setError('Bitte die Arbeitszeit auswählen.')
|
setError('Bitte die Arbeitszeit auswählen.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const supportError = validateSupporters(supporters, currentUserId)
|
||||||
|
if (supportError) {
|
||||||
|
setError(supportError)
|
||||||
|
return
|
||||||
|
}
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
setError('')
|
setError('')
|
||||||
try {
|
try {
|
||||||
@@ -62,7 +70,9 @@ export default function WorksheetTimesModal({ worksheet, onClose, onSave }) {
|
|||||||
startTime: timeToWoms(formData.startTime),
|
startTime: timeToWoms(formData.startTime),
|
||||||
endDate: isoToGermanDate(formData.endDate),
|
endDate: isoToGermanDate(formData.endDate),
|
||||||
endTime: timeToWoms(formData.endTime),
|
endTime: timeToWoms(formData.endTime),
|
||||||
totalTime: Number(totalTime)
|
totalTime: Number(totalTime),
|
||||||
|
supporters,
|
||||||
|
supportersBefore: worksheet.supporters
|
||||||
})
|
})
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
onClose()
|
onClose()
|
||||||
@@ -137,6 +147,13 @@ export default function WorksheetTimesModal({ worksheet, onClose, onSave }) {
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SupportersEditor
|
||||||
|
value={supporters}
|
||||||
|
onChange={setSupporters}
|
||||||
|
excludeUserId={currentUserId}
|
||||||
|
minutes={totalTime}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="text-center mt-2">
|
<div className="text-center mt-2">
|
||||||
<button type="submit" className="btn btn-dark" disabled={saving}>
|
<button type="submit" className="btn btn-dark" disabled={saving}>
|
||||||
{saving ? 'Speichert...' : 'Speichern'}
|
{saving ? 'Speichert...' : 'Speichern'}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react'
|
|||||||
import { databases, DATABASE_ID, COLLECTIONS, Query, ID } from '../lib/appwrite'
|
import { databases, DATABASE_ID, COLLECTIONS, Query, ID } from '../lib/appwrite'
|
||||||
import { canEditWorksheetTimes, isGitWorksheet } from '../lib/ticketForm'
|
import { canEditWorksheetTimes, isGitWorksheet } from '../lib/ticketForm'
|
||||||
import { roundUpToStep, isValidStep } from '../lib/timeSteps'
|
import { roundUpToStep, isValidStep } from '../lib/timeSteps'
|
||||||
|
import { parseSupporters, serializeSupporters } from '../lib/support'
|
||||||
|
|
||||||
const STEP_ERROR = 'Die Arbeitszeit muss in 15-Minuten-Schritten angegeben werden.'
|
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),
|
countsToPlan: !data.isComment && Boolean(data.countsToPlan),
|
||||||
createdAt: new Date().toISOString()
|
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)
|
console.log('Creating worksheet with data:', worksheetData)
|
||||||
|
|
||||||
@@ -232,6 +237,22 @@ export function useWorksheets(woid = null) {
|
|||||||
const { startDate, startTime, endDate, endTime, totalTime } = times
|
const { startDate, startTime, endDate, endTime, totalTime } = times
|
||||||
if (!isValidStep(totalTime)) return { success: false, error: STEP_ERROR }
|
if (!isValidStep(totalTime)) return { success: false, error: STEP_ERROR }
|
||||||
const data = { startDate, startTime, endDate, endTime, totalTime }
|
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
|
// Mit nachgetragener Zeit zaehlt ein Git-Push als Arbeitszeit, nicht mehr als Kommentar
|
||||||
if (isGitWorksheet(worksheet)) data.isComment = false
|
if (isGitWorksheet(worksheet)) data.isComment = false
|
||||||
return updateWorksheet(worksheet.$id, data)
|
return updateWorksheet(worksheet.$id, data)
|
||||||
|
|||||||
Reference in new issue
Block a user