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:
1 parent
5e59ad44bd
commit
f0d39f2eee
5 files changed
+86
-7
No files matched your search
@@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 } : {}
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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: '' } }), {})
|
||||||
|
})
|
||||||
Reference in new issue
Block a user