/* Termine der Rechnungsanalyse. Ein Termin ist kein eigener Datensatz, sondern haengt am Akquise-Ticket des Kunden (workorders): startDate + startTime + endTime + assignedTo. Das WOMS- Planboard liest genau diese Felder, damit Termin und Ticket nie auseinander laufen. Achtung: WOMS speichert Datum als Text "dd.mm.yyyy" und Uhrzeit als "hhmm" - beides wird hier erzeugt und beim Lesen tolerant geparst. */ import { config } from '../config.js' import { listDocuments, updateDocument, Query } from './appwriteAdmin.js' const TICKET_LIMIT = 500 /* ------------------------------------------------------------- Formate --- */ // "2026-08-12" -> "12.08.2026" export function isoToGerman(iso) { const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(iso || '')) if (!m) return '' return `${m[3]}.${m[2]}.${m[1]}` } // "12.08.2026" und "12.8.2026" -> "2026-08-12" export function germanToIso(value) { const m = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/.exec(String(value || '').trim()) if (!m) return '' return `${m[3]}-${m[2].padStart(2, '0')}-${m[1].padStart(2, '0')}` } // "09:00" -> "0900" export function timeToWoms(value) { const m = /^(\d{1,2}):?(\d{2})$/.exec(String(value || '').trim()) if (!m) return '' return `${m[1].padStart(2, '0')}${m[2]}` } // "0900" -> "09:00" export function womsToTime(value) { const raw = String(value || '').trim() if (!raw) return '' const m = /^(\d{1,2}):?(\d{2})$/.exec(raw) if (!m) return '' return `${m[1].padStart(2, '0')}:${m[2]}` } function addHour(time) { const m = /^(\d{2}):(\d{2})$/.exec(womsToTime(time)) if (!m) return '' const hour = (Number(m[1]) + 1) % 24 return `${String(hour).padStart(2, '0')}${m[2]}` } /* ---------------------------------------------------------- Mitarbeiter --- */ export async function findEmployeeByEmail(email) { const wanted = String(email || '').trim().toLowerCase() if (!wanted) return null const employees = await listDocuments(config.collections.employees, [Query.limit(100)]) return employees.find((e) => String(e.email || '').toLowerCase() === wanted) || null } /* -------------------------------------------------------------- Tickets --- */ function isClosed(ticket) { return String(ticket.status || '').toLowerCase() === 'closed' } /* Das Ticket, an das der Termin gehoert: bevorzugt das offene Akquise-Ticket. Gibt es keins, kommt nur ein anderes offenes Ticket in Frage, das noch KEIN Datum traegt - bei einem Projekt- oder Webpage-Ticket bedeutet startDate den Projektstart, den wuerden wir sonst still ueberschreiben. */ export async function findAcquisitionTicket(customerId) { const tickets = await listDocuments(config.collections.workorders, [ Query.equal('customerId', customerId), Query.limit(100), ]) if (!tickets.length) return null const newestFirst = [...tickets].sort((a, b) => String(b.$createdAt || '').localeCompare(String(a.$createdAt || ''))) const akquise = newestFirst.filter((t) => String(t.type || '').toLowerCase() === 'akquise') if (akquise.length) return akquise.find((t) => !isClosed(t)) || akquise[0] return newestFirst.find((t) => !isClosed(t) && !t.startDate) || null } function toAppointment(ticket, employeesByUserId) { const dateIso = germanToIso(ticket.startDate) const time = womsToTime(ticket.startTime) if (!dateIso) return null const employee = employeesByUserId[ticket.assignedTo] || null return { ticketId: ticket.$id, woid: ticket.woid || '', topic: ticket.topic || ticket.title || '', type: ticket.type || '', status: ticket.status || '', customerId: ticket.customerId || '', customerName: ticket.customerName || '', customerLocation: ticket.customerLocation || '', date: dateIso, time, endTime: womsToTime(ticket.endTime), employeeUserId: ticket.assignedTo || '', employeeName: employee?.displayName || ticket.assignedName || '', employeeEmail: employee?.email || '', } } /* Alle terminierten Tickets in einem Zeitraum (ISO-Datum, inklusive). */ export async function listAppointments({ from = '', to = '' } = {}) { const [tickets, employees] = await Promise.all([ listDocuments(config.collections.workorders, [Query.limit(TICKET_LIMIT)]), listDocuments(config.collections.employees, [Query.limit(100)]), ]) const employeesByUserId = {} for (const e of employees) employeesByUserId[e.userId] = e return tickets .map((t) => toAppointment(t, employeesByUserId)) .filter((a) => a && (!from || a.date >= from) && (!to || a.date <= to)) .sort((a, b) => (a.date + a.time).localeCompare(b.date + b.time)) } /* Termin am Akquise-Ticket eintragen. */ export async function saveAppointment({ customerId, employeeEmail, date, time }) { const germanDate = isoToGerman(date) const womsTime = timeToWoms(time) if (!germanDate) throw badRequest('Ungültiges Datum') if (!womsTime) throw badRequest('Ungültige Uhrzeit') const employee = await findEmployeeByEmail(employeeEmail) if (!employee) throw badRequest(`Kein Mitarbeiter mit der E-Mail ${employeeEmail} in WOMS gefunden`) const ticket = await findAcquisitionTicket(customerId) if (!ticket) { throw badRequest('Kein passendes Ticket gefunden. Bitte in WOMS ein Akquise-Ticket für diesen Kunden anlegen.') } const updated = await updateDocument(config.collections.workorders, ticket.$id, { startDate: germanDate, startTime: womsTime, endTime: addHour(womsTime), assignedTo: employee.userId, assignedName: employee.displayName || '', }) return { ticketId: updated.$id, woid: updated.woid || '', topic: updated.topic || '', type: updated.type || '', date, time: womsToTime(womsTime), employeeName: employee.displayName || '', } } function badRequest(message) { const error = new Error(message) error.status = 400 return error }