- Git-Pushes bleiben orange mit "Zeit offen", bis Beginn und Minuten nachgetragen sind; danach grün mit "Zeit eingetragen". - Kommentare sind blau statt grün, damit Grün nur noch "erledigt" heißt. - Hinweise im farbigen Kopf in weißer Schrift (vorher kaum lesbar). - Pro Ticket werden alle Arbeitsblätter geladen (Appwrite lieferte ohne limit nur 25), zugeklappte Ticketkarten laden gar keine mehr. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
171 lines
6.7 KiB
JavaScript
171 lines
6.7 KiB
JavaScript
/* Hilfen fuer die Formulare "Neues Ticket" und "Arbeitsblatt" und die Anzeige.
|
|
WOMS speichert Datum als Text "dd.mm.yyyy" und Uhrzeit als "hhmm"
|
|
(siehe planboard.js). Die Kalender-Felder des Browsers liefern dagegen
|
|
"yyyy-mm-dd" bzw. "hh:mm" - umgerechnet wird erst beim Speichern. */
|
|
|
|
import { parseGermanDate, formatTime, dayKey } from './planboard'
|
|
|
|
// Service Type: gespeichert werden die bisherigen Werte, angezeigt wird Deutsch
|
|
export const SERVICE_TYPES = [
|
|
{ value: 'Remote', label: 'Fernwartung (remote)' },
|
|
{ value: 'On Site', label: 'Vor Ort beim Kunden' },
|
|
]
|
|
|
|
// Kurzform fuer Badges und Listen. Das alte "Off Site" meinte ebenfalls Arbeit aus der Ferne.
|
|
const SERVICE_TYPE_SHORT = {
|
|
remote: 'Fernwartung',
|
|
'off site': 'Fernwartung',
|
|
'on site': 'Vor Ort',
|
|
comment: 'Kommentar',
|
|
}
|
|
|
|
export function serviceTypeLabel(value) {
|
|
return SERVICE_TYPE_SHORT[String(value || '').trim().toLowerCase()] || value
|
|
}
|
|
|
|
// Git-Pushes legt das Kundenportal als Arbeitsblatt an (employeeId "gitea"): Ende = Push,
|
|
// der Beginn wird im Ticketsystem nachgetragen
|
|
export function isGitWorksheet(worksheet) {
|
|
return worksheet?.serviceType === 'GIT'
|
|
}
|
|
|
|
export function needsTimeEntry(worksheet) {
|
|
return isGitWorksheet(worksheet) && (!worksheet.startTime || !Number(worksheet.totalTime))
|
|
}
|
|
|
|
// Farbe in der Arbeitsblatt-Liste: Git-Pushes sind orange, bis ihre Zeit nachgetragen
|
|
// ist, danach gruen. Kommentare blau, normale Arbeitszeit grau.
|
|
export function worksheetTone(worksheet) {
|
|
if (isGitWorksheet(worksheet)) return needsTimeEntry(worksheet) ? 'git-open' : 'git-done'
|
|
return worksheet?.isComment ? 'comment' : 'work'
|
|
}
|
|
|
|
const sameName = (a, b) => String(a || '').trim().toLowerCase() === String(b || '').trim().toLowerCase()
|
|
|
|
/* Wer hat das Arbeitsblatt erstellt? Normale Eintraege tragen die Appwrite-ID des
|
|
Mitarbeiters. Bei Git-Pushes steht in employeeName der Gitea-Name des Pushers
|
|
(Benutzername wie "knso" oder voller Name); er gehoert dem Mitarbeiter mit diesem
|
|
Kuerzel oder Namen. employee ist der eigene Eintrag aus "employees". */
|
|
export function isWorksheetCreator(worksheet, user, employee) {
|
|
if (!user?.$id || !worksheet) return false
|
|
if (!isGitWorksheet(worksheet)) return worksheet.employeeId === user.$id
|
|
const pusher = String(worksheet.employeeName || '').trim()
|
|
return Boolean(pusher) && [employee?.shortcode, employee?.displayName, user.name].some((name) => sameName(name, pusher))
|
|
}
|
|
|
|
// Zeiten darf nur aendern, wer das Arbeitsblatt erstellt hat. Reine Kommentare haben
|
|
// keine Arbeitszeit; Git-Pushes sind Kommentare, bis ihre Zeit nachgetragen ist.
|
|
export function canEditWorksheetTimes(worksheet, user, employee) {
|
|
return isWorksheetCreator(worksheet, user, employee) && (isGitWorksheet(worksheet) || !worksheet.isComment)
|
|
}
|
|
|
|
const pad = (n) => String(n).padStart(2, '0')
|
|
|
|
// Heutiges Datum in Ortszeit als "yyyy-mm-dd" (Wert fuer <input type="date">)
|
|
export function todayIso(now = new Date()) {
|
|
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`
|
|
}
|
|
|
|
// "2026-09-26" -> "26.09.2026"
|
|
export function isoToGermanDate(iso) {
|
|
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(iso || ''))
|
|
return m ? `${m[3]}.${m[2]}.${m[1]}` : ''
|
|
}
|
|
|
|
// "09:30" -> "0930"
|
|
export function timeToWoms(value) {
|
|
const m = /^(\d{1,2}):(\d{2})/.exec(String(value || ''))
|
|
return m ? `${m[1].padStart(2, '0')}${m[2]}` : ''
|
|
}
|
|
|
|
// Rueckweg fuers Bearbeiten: "26.09.2026" (auch "2.5.2026") -> "2026-09-26", sonst ""
|
|
export function germanDateToIso(value) {
|
|
const date = parseGermanDate(value)
|
|
return date ? dayKey(date) : ''
|
|
}
|
|
|
|
// "0930" -> "09:30", sonst ""
|
|
export function womsTimeToInput(value) {
|
|
return formatTime(value)
|
|
}
|
|
|
|
// Minuten von "hh:mm" bis "hh:mm"; liegt das Ende davor, geht es ueber Mitternacht
|
|
export function minutesBetween(start, end) {
|
|
const a = /^(\d{1,2}):(\d{2})/.exec(String(start || ''))
|
|
const b = /^(\d{1,2}):(\d{2})/.exec(String(end || ''))
|
|
if (!a || !b) return null
|
|
const diff = Number(b[1]) * 60 + Number(b[2]) - (Number(a[1]) * 60 + Number(a[2]))
|
|
return diff < 0 ? diff + 24 * 60 : diff
|
|
}
|
|
|
|
/* Vorschlaege fuer "Requested by" / "Requested for": Kunden, Mitarbeiter
|
|
und Namen aus frueheren Tickets. Pro Name (ohne Gross-/Kleinschreibung)
|
|
ein Eintrag; customerIds merkt sich, zu welchen Kunden er gehoert. */
|
|
export function buildRequesterSuggestions({ customers = [], employees = [], tickets = [] }) {
|
|
const byKey = new Map()
|
|
const add = (value, { hint = '', search = '', customerId = '', used = false } = {}) => {
|
|
const text = String(value || '').trim()
|
|
if (!text) return
|
|
const key = text.toLowerCase()
|
|
let entry = byKey.get(key)
|
|
if (!entry) {
|
|
entry = { value: text, hint: '', search: key, customerIds: new Set(), uses: 0 }
|
|
byKey.set(key, entry)
|
|
}
|
|
if (hint && !entry.hint) entry.hint = hint
|
|
if (search) entry.search += ` ${search.toLowerCase()}`
|
|
if (customerId) entry.customerIds.add(customerId)
|
|
if (used) entry.uses += 1
|
|
}
|
|
|
|
for (const customer of customers) {
|
|
add(customer.name || customer.companyName, {
|
|
hint: customer.email ? `Kunde · ${customer.email}` : 'Kunde',
|
|
search: customer.email || '',
|
|
customerId: customer.$id,
|
|
})
|
|
}
|
|
for (const employee of employees) {
|
|
add(employee.displayName, { hint: 'Mitarbeiter', search: employee.email || '' })
|
|
}
|
|
for (const ticket of tickets) {
|
|
add(ticket.requestedBy, { customerId: ticket.customerId, used: true })
|
|
add(ticket.requestedFor, { customerId: ticket.customerId, used: true })
|
|
}
|
|
for (const entry of byKey.values()) {
|
|
if (!entry.hint) entry.hint = 'aus früheren Tickets'
|
|
}
|
|
return [...byKey.values()]
|
|
}
|
|
|
|
/* Passende Vorschlaege zur Eingabe, beste zuerst. Namen des gewaehlten
|
|
Kunden stehen oben; ohne Eingabe gibt es nur diese und schon benutzte
|
|
Namen, damit die Liste nicht einfach alle Kunden aufzaehlt. */
|
|
export function rankSuggestions(entries, query, { customerId = '', limit = 8 } = {}) {
|
|
const q = String(query || '').trim().toLowerCase()
|
|
const ranked = []
|
|
for (const entry of entries) {
|
|
const value = entry.value.toLowerCase()
|
|
if (q && value === q) continue
|
|
const forCustomer = Boolean(customerId) && entry.customerIds.has(customerId)
|
|
let score
|
|
if (!q) {
|
|
if (!forCustomer && !entry.uses) continue
|
|
score = 0
|
|
} else if (value.startsWith(q)) {
|
|
score = 3
|
|
} else if (value.split(/[\s.@-]+/).some((word) => word.startsWith(q))) {
|
|
score = 2
|
|
} else if (entry.search.includes(q)) {
|
|
score = 1
|
|
} else {
|
|
continue
|
|
}
|
|
if (forCustomer) score += 2
|
|
score += Math.min(entry.uses, 10) / 10
|
|
ranked.push({ entry, score })
|
|
}
|
|
ranked.sort((a, b) => b.score - a.score || a.entry.value.localeCompare(b.entry.value, 'de'))
|
|
return ranked.slice(0, limit).map(({ entry }) => entry)
|
|
}
|