Arbeitsblätter: Abo-Vorgabe bei Einrichtungs-Tickets

Bei Tickets mit einrichtungAbrechnung 'abo' steht "Vom Abo-Stundenkonto abziehen" im Dialog von Anfang an auf an (auch wenn der Abo-Status erst nach dem Öffnen ankommt), beim Nachtragen der Zeit eines Git-Blatts wird countsToPlan beim ersten Eintragen gesetzt. 'projekt', Kommentare und Kunden ohne Wartungs-Abo bekommen keine Vorgabe; ein von Hand abgehakter Haken bleibt aus.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-16 13:36:34 +00:00
1 parent 5e59ad44bd
commit f0d39f2eee
5 files changed
+86 -7

No files matched your search

+11 -3
View File
@@ -7,6 +7,7 @@ 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'
import { countsToPlanWert } from '../lib/planVorgabe'
// 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']
@@ -31,7 +32,7 @@ const initialForm = (workorder, newStatus) => ({
endTime: '', endTime: '',
details: '', details: '',
isComment: false, isComment: false,
countsToPlan: false, countsToPlan: null, // null = unberührt: dann gilt die Abo-Vorgabe des Tickets (planVorgabe.js)
supporters: [] supporters: []
}) })
@@ -50,6 +51,13 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
: (workorder?.status || 'Open') : (workorder?.status || 'Open')
const [formData, setFormData] = useState(() => initialForm(workorder, defaultStatus)) const [formData, setFormData] = useState(() => initialForm(workorder, defaultStatus))
// Der Abo-Status kommt oft erst nach dem Öffnen: der Wert wird bei jedem Rendern aus Handwahl und Vorgabe berechnet
const countsToPlan = countsToPlanWert({
gewaehlt: formData.countsToPlan,
ticket: workorder,
aboHoursAllowed,
isComment: formData.isComment
})
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState('') const [error, setError] = useState('')
@@ -130,7 +138,7 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
endTime: timeToWoms(formData.endTime), endTime: timeToWoms(formData.endTime),
details: formData.details, details: formData.details,
isComment: formData.isComment, isComment: formData.isComment,
countsToPlan: aboHoursAllowed && !formData.isComment && formData.countsToPlan, countsToPlan,
employeeShort: user?.prefs?.shortCode || '', // Aus User-Preferences employeeShort: user?.prefs?.shortCode || '', // Aus User-Preferences
supporters supporters
} }
@@ -222,7 +230,7 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
<label className="check" title="Zeit wird vom Stundenguthaben des Wartungs-Abos abgezogen und im Kundenportal angezeigt"> <label className="check" title="Zeit wird vom Stundenguthaben des Wartungs-Abos abgezogen und im Kundenportal angezeigt">
<input <input
type="checkbox" type="checkbox"
checked={formData.countsToPlan && !formData.isComment} checked={countsToPlan}
disabled={formData.isComment} disabled={formData.isComment}
onChange={(e) => handleChange('countsToPlan', e.target.checked)} onChange={(e) => handleChange('countsToPlan', e.target.checked)}
/> />
+4 -3
View File
@@ -228,15 +228,16 @@ export function useWorksheets(woid = null) {
/** /**
* Aendert nachtraeglich Beginn, Ende und Arbeitszeit - nur fuer den Ersteller * Aendert nachtraeglich Beginn, Ende und Arbeitszeit - nur fuer den Ersteller
* (employee = eigener Eintrag aus "employees", fuer Git-Pushes) * (employee = eigener Eintrag aus "employees", fuer Git-Pushes).
* extra: weitere Felder fuer dasselbe Update (z. B. countsToPlan aus der Abo-Vorgabe)
*/ */
const updateWorksheetTimes = async (worksheet, times, currentUser, employee) => { const updateWorksheetTimes = async (worksheet, times, currentUser, employee, extra = {}) => {
if (!canEditWorksheetTimes(worksheet, currentUser, employee)) { if (!canEditWorksheetTimes(worksheet, currentUser, employee)) {
return { success: false, error: 'Nur wer das Arbeitsblatt erstellt hat, kann die Zeiten ändern.' } return { success: false, error: 'Nur wer das Arbeitsblatt erstellt hat, kann die Zeiten ändern.' }
} }
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, ...extra }
// Unterstuetzung nur schreiben, wenn sie sich geaendert hat. Hat inzwischen jemand anderes // Unterstuetzung nur schreiben, wenn sie sich geaendert hat. Hat inzwischen jemand anderes
// (z. B. ueber "Ich habe unterstuetzt") etwas geaendert, nicht still ueberschreiben. // (z. B. ueber "Ich habe unterstuetzt") etwas geaendert, nicht still ueberschreiben.
if (times.supporters) { if (times.supporters) {
+32
View File
@@ -0,0 +1,32 @@
// Abo-Vorgabe für Arbeitsblätter an Einrichtungs-Tickets (V9): Hängt das Ticket an einer Funktion, die aus dem Abo
// eingerichtet wird (einrichtungAbrechnung 'abo'), steht "Vom Abo-Stundenkonto abziehen" von Anfang an auf an.
// 'projekt' (im Projekt enthalten) und alle anderen Tickets bekommen keine Vorgabe. Nur Vergleiche, keine Zeitlogik.
import { isGitWorksheet, needsTimeEntry } from './ticketForm'
export const EINRICHTUNG_ABRECHNUNGEN = ['abo', 'projekt']
// 'abo' | 'projekt' | '' – nur genau diese Werte, alles andere (Großschreibung, Zahl, fehlendes Ticket) gilt als nicht gesetzt
export function einrichtungAbrechnung(ticket) {
const wert = ticket?.einrichtungAbrechnung
return EINRICHTUNG_ABRECHNUNGEN.includes(wert) ? wert : ''
}
// Vorgabe für countsToPlan: nur bei Abo-Einrichtung und wenn das Wartungs-Abo Stunden erlaubt
export function countsToPlanVorgabe(ticket, aboHoursAllowed) {
return einrichtungAbrechnung(ticket) === 'abo' && aboHoursAllowed === true
}
// Wert des Hakens im Dialog. gewaehlt: null = unberührt (dann gilt die Vorgabe, auch wenn der Abo-Status erst nach dem
// Öffnen ankommt), true/false = von Hand gesetzt. Ohne erlaubte Abo-Stunden und bei Kommentar ist der Wert immer false.
export function countsToPlanWert({ gewaehlt, ticket, aboHoursAllowed, isComment }) {
if (isComment || aboHoursAllowed !== true) return false
if (gewaehlt === null || gewaehlt === undefined) return countsToPlanVorgabe(ticket, aboHoursAllowed)
return Boolean(gewaehlt)
}
// Felder, die beim Nachtragen der Zeit eines Git-Blatts mitgeschrieben werden: nur beim ersten Eintragen der Zeit
// (das Blatt hat noch keine) und nur, wenn die Vorgabe greift. Eine bestehende Entscheidung bleibt unberührt.
export function nachtragPlanFelder({ worksheet, ticket, aboHoursAllowed }) {
if (!isGitWorksheet(worksheet) || !needsTimeEntry(worksheet) || worksheet.countsToPlan) return {}
return countsToPlanVorgabe(ticket, aboHoursAllowed) ? { countsToPlan: true } : {}
}
+2 -1
View File
@@ -18,6 +18,7 @@ import { databases, DATABASE_ID, COLLECTIONS } from '../lib/appwrite'
import { elapsedLabel } from '../lib/elapsed' import { elapsedLabel } from '../lib/elapsed'
import { canAddOwnSupport } from '../lib/support' import { canAddOwnSupport } from '../lib/support'
import { canEditWorksheetTimes, serviceTypeLabel } from '../lib/ticketForm' import { canEditWorksheetTimes, serviceTypeLabel } from '../lib/ticketForm'
import { nachtragPlanFelder } from '../lib/planVorgabe'
import { listBackPath, resolveTicketTab, ticketPath, ticketRef } from '../lib/ticketRoutes' import { listBackPath, resolveTicketTab, ticketPath, ticketRef } from '../lib/ticketRoutes'
import { statusSelectOptions, ticketTypeLabel } from '../lib/ticketStatus' import { statusSelectOptions, ticketTypeLabel } from '../lib/ticketStatus'
import { appointmentText, assigneeOptions, dueText, ticketNumber } from '../lib/ticketView' import { appointmentText, assigneeOptions, dueText, ticketNumber } from '../lib/ticketView'
@@ -307,7 +308,7 @@ function TicketView({ ticket, updateWorkorder, tab }) {
worksheet={editTimesWorksheet} worksheet={editTimesWorksheet}
currentUserId={user?.$id} 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, nachtragPlanFelder({ worksheet: ws, ticket, aboHoursAllowed }))}
employeesState={employeesState} employeesState={employeesState}
/> />
)} )}
+37
View File
@@ -0,0 +1,37 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { EINRICHTUNG_ABRECHNUNGEN, einrichtungAbrechnung, countsToPlanVorgabe, countsToPlanWert, nachtragPlanFelder } = await loadModule('src/lib/planVorgabe.js')
const abo = { einrichtungAbrechnung: 'abo' }, projekt = { einrichtungAbrechnung: 'projekt' }
test('einrichtungAbrechnung: nur exakt abo oder projekt', () => {
assert.deepEqual(EINRICHTUNG_ABRECHNUNGEN, ['abo', 'projekt'])
assert.deepEqual([abo, projekt, {}, null, undefined, { einrichtungAbrechnung: 'Abo' }, { einrichtungAbrechnung: 5 }].map(einrichtungAbrechnung), ['abo', 'projekt', '', '', '', '', ''])
})
test('countsToPlanVorgabe: nur Abo-Einrichtung mit erlaubten Stunden', () => {
assert.equal(countsToPlanVorgabe(abo, true), true)
for (const [t, ok] of [[abo, false], [abo, undefined], [projekt, true], [{}, true], [null, true]]) assert.equal(countsToPlanVorgabe(t, ok), false)
})
test('countsToPlanWert: Vorgabe, späte Antwort des Abo-Status, Handwahl, Kommentar', () => {
const w = (o) => countsToPlanWert({ gewaehlt: null, ticket: abo, aboHoursAllowed: true, isComment: false, ...o })
assert.equal(w({}), true) // unberührt: Vorgabe
assert.equal(w({ aboHoursAllowed: false }), false) // Abo-Status lädt noch
assert.equal(w({ aboHoursAllowed: true }), true) // …und kommt danach: die Vorgabe greift trotzdem
assert.equal(w({ gewaehlt: false }), false) // von Hand abgehakt bleibt aus
assert.equal(w({ gewaehlt: true, ticket: projekt }), true) // von Hand anhaken ist immer möglich
assert.equal(w({ gewaehlt: true, aboHoursAllowed: false }), false)
assert.equal(w({ ticket: projekt }), false)
assert.equal(w({ ticket: {} }), false)
for (const gewaehlt of [null, true, false]) assert.equal(w({ gewaehlt, isComment: true }), false)
})
test('nachtragPlanFelder: nur das erste Nachtragen einer Git-Zeit auf einer Abo-Einrichtung', () => {
const git = { serviceType: 'GIT', isComment: true, startTime: '', totalTime: 0, countsToPlan: false }
const f = (o) => nachtragPlanFelder({ worksheet: git, ticket: abo, aboHoursAllowed: true, ...o })
assert.deepEqual(f({}), { countsToPlan: true })
assert.deepEqual(f({ ticket: projekt }), {})
assert.deepEqual(f({ ticket: {} }), {})
assert.deepEqual(f({ aboHoursAllowed: false }), {})
assert.deepEqual(f({ worksheet: { ...git, startTime: '0900', totalTime: 45 } }), {}) // Zeit ist schon eingetragen: Entscheidung bleibt
assert.deepEqual(f({ worksheet: { ...git, countsToPlan: true } }), {})
assert.deepEqual(f({ worksheet: { serviceType: 'Remote', totalTime: 0, startTime: '' } }), {})
})