Files
Webklar-Kundenbereich/server/services/ticketAdmin.js
T
ClaudeandClaude Sonnet 5.5 9dd0244537 Funktions-Ticket: Text aus dem Register, Laengendeckel, Notfall-Details und gezielte Wiederholung
markPaid holt Betreff und Details des Einrichtungs-Tickets aus dem Register (ticketTexte), createFeatureTicket
bleibt die einzige Stelle, die das Ticket anlegt. Betreff auf 128, Details auf 7000 Zeichen gekuerzt (surrogatsicher,
Schnitt an der Zeilengrenze, einheitliche Schlusszeile). Wirft die Details-Funktion oder liefert sie keinen Text,
gelten die Notfall-Details (Kopfzeilen und Verweis ins WOMS). Nach einem 400 bei langen Details gibt es genau einen
zweiten Versuch mit den Notfall-Details und derselben woid; 5xx, Netzfehler und kurze Details werden ohne
Wiederholung weitergereicht. Stub termine/ticketDetails.js bis Task D4.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 12:48:02 +00:00

210 lines
7.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { config } from '../config.js'
import { berlinDateTime } from './berlinTime.js'
import {
createDocument,
deleteDocument,
getDocument,
listDocuments,
updateDocument,
Query,
ID,
} from './appwriteAdmin.js'
import { kuerze, LANGE_DETAILS, TICKET_DETAILS_MAX, TICKET_GEKUERZT, TICKET_TOPIC_MAX } from './featureRegistry.js'
const WOID_BASE = 9999
async function generateNextWoid() {
// Numerisches Maximum ueber alle Tickets - orderDesc('woid') sortiert Strings
// und liefert bei gemischt 5-/6-stelligen WOIDs falsche Ergebnisse.
const docs = await listDocuments(config.collections.workorders, [Query.limit(1000)])
const highest = docs.reduce((max, doc) => {
const n = parseInt(doc.woid, 10)
return Number.isNaN(n) ? max : Math.max(max, n)
}, WOID_BASE)
return String(highest + 1)
}
/**
* Stellt sicher, dass ein Website-Projekt ein Ticket hat, damit Git-Pushes
* als Worksheets (WSIDs) getrackt werden koennen. Legt bei Bedarf automatisch
* ein 'Webpage'-Ticket fuer den Kunden des Projekts an und verknuepft es.
* Projekte ohne Kunde bekommen kein automatisches Ticket.
*/
export async function ensureProjectTicket(project, { repoFullName } = {}) {
if (!project) return null
if (project.ticketId) return project.ticketId
if (!project.customerId) return null
const customer = await getDocument(config.collections.customers, project.customerId).catch(() => null)
if (!customer) return null
const woid = await generateNextWoid()
const now = new Date().toISOString()
const ticket = await createDocument(
config.collections.workorders,
{
topic: `Website ${project.projectName || repoFullName || project.subdomain || ''}`.trim(),
type: 'Webpage',
status: 'Open',
priority: 1,
serviceType: 'Remote',
systemType: 'n/a',
customerId: project.customerId,
customerName: customer.name || '',
customerLocation: customer.location || '',
woid,
details: `Automatisch angelegt fuer das Repository ${repoFullName || project.repoFullName || ''} (erster Git-Push).`,
createdAt: now,
},
ID.unique()
)
await updateDocument(config.collections.websiteProjects, project.$id, {
ticketId: ticket.$id,
updatedAt: now,
})
console.log(`[ticketAdmin] Webpage-Ticket #${woid} fuer Projekt ${project.$id} angelegt`)
return ticket.$id
}
/**
* Einrichtungs-Ticket fuer eine gekaufte Funktion (z. B. News-Pinnwand, Terminplaner) einer Kundenwebsite.
* Wird geschlossen, sobald die Funktion eingebaut ist; danach schaltet das Portal sie frei.
* Einzige Stelle, die dieses Ticket anlegt. topic und details sind optional (ohne: die News-Festwerte); markPaid uebergibt
* die Registertexte (ticketTexte). Beides wird auf TICKET_TOPIC_MAX / TICKET_DETAILS_MAX gekuerzt.
* Wiederholung nur, wenn das Anlegen mit 400 scheitert, fallbackDetails uebergeben wurde und die Details lang waren
* (LANGE_DETAILS): dann einmal mit fallbackDetails und derselben woid. Jeder andere Fehler (5xx, Netz: unklar, ob das Ticket
* entstand) wird ohne Wiederholung weitergereicht, sonst gaebe es Doppel-Tickets; scheitert auch der Rueckfall, wird der erste
* Fehler geworfen.
*/
export async function createFeatureTicket({
customerId,
project,
featureKey,
siteKey,
topic = null,
details = null,
fallbackDetails = null,
}) {
const customer = await getDocument(config.collections.customers, customerId).catch(() => null)
const woid = await generateNextWoid()
const projectName = project?.projectName || project?.subdomain || ''
const domain = project?.liveDomain || project?.previewUrl || project?.subdomain || ''
const embed = `<div data-webklar-news="${siteKey}"></div> + https://project.webklar.com/embed/news.js`
const text = kuerze(
details ||
[
`Funktion: ${featureKey}`,
`Projekt: ${projectName}`,
`Domain: ${domain}`,
`siteKey: ${siteKey}`,
`Einbau: ${embed}`,
'Anleitung: docs/funktionen/news-einbau.md (Repo Webklar-Kundenbereich)',
].join('\n'),
TICKET_DETAILS_MAX,
{ anZeile: true, endzeile: TICKET_GEKUERZT }
)
const fields = {
topic: kuerze(topic || `News-Pinnwand einbauen – ${projectName}`.trim(), TICKET_TOPIC_MAX, { endzeile: '…' }),
type: 'Webpage',
status: 'Open',
priority: 1,
serviceType: 'Remote',
systemType: 'n/a',
customerId,
customerName: customer?.name || '',
customerLocation: customer?.location || '',
woid,
createdAt: new Date().toISOString(),
}
let ticket
try {
ticket = await createDocument(config.collections.workorders, { ...fields, details: text }, ID.unique())
} catch (firstError) {
const hatRueckfall = typeof fallbackDetails === 'string' && fallbackDetails !== ''
if (firstError?.status !== 400 || !hatRueckfall || text.length <= LANGE_DETAILS) throw firstError
console.warn(`[ticketAdmin] Funktions-Ticket #${woid} (${featureKey}) abgelehnt (400), zweiter Versuch mit Notfall-Details:`, firstError.message)
try {
ticket = await createDocument(
config.collections.workorders,
{ ...fields, details: kuerze(fallbackDetails, TICKET_DETAILS_MAX, { anZeile: true, endzeile: TICKET_GEKUERZT }) },
ID.unique()
)
} catch (secondError) {
console.error(`[ticketAdmin] Funktions-Ticket #${woid} (${featureKey}): auch der zweite Versuch scheiterte:`, secondError.message)
throw firstError
}
}
console.log(`[ticketAdmin] Funktions-Ticket #${woid} (${featureKey}) fuer Projekt ${project?.$id} angelegt`)
return { ticketId: ticket.$id, woid }
}
/**
* Legt ein Ticket fuer ein Projekt an (Zuordnen-Flow, erst beim Abschliessen).
* Unbekannter Kunde => Fehler mit status 400.
*/
export async function createTicketForProject({ customerId, topic, type, requestedBy, startDate, details }, { now = new Date() } = {}) {
const customer = customerId
? await getDocument(config.collections.customers, customerId).catch(() => null)
: null
if (!customer) {
const err = new Error('Kunde nicht gefunden.')
err.status = 400
throw err
}
// WOMS speichert Ticket-Daten als 'TT.MM.JJJJ'; leer = heute (Berlin).
let date
if (!startDate) {
date = berlinDateTime(now).date
} else {
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(startDate))
const valid =
m &&
(() => {
const d = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3]))
return d.getUTCFullYear() === +m[1] && d.getUTCMonth() === +m[2] - 1 && d.getUTCDate() === +m[3]
})()
if (!valid) {
const err = new Error('Das Startdatum ist ungültig (erwartet: JJJJ-MM-TT).')
err.status = 400
throw err
}
date = `${m[3]}.${m[2]}.${m[1]}`
}
const woid = await generateNextWoid()
const ticket = await createDocument(
config.collections.workorders,
{
topic,
type,
status: 'Open',
priority: 1,
serviceType: 'Remote',
systemType: 'n/a',
customerId,
customerName: customer.name || '',
customerLocation: customer.location || '',
requestedBy: requestedBy || '',
startDate: date,
deadline: date,
details: details || '',
woid,
createdAt: new Date().toISOString(),
},
ID.unique()
)
console.log(`[ticketAdmin] Ticket #${woid} fuer Kunde ${customerId} angelegt`)
return { $id: ticket.$id, woid }
}
/** Loescht ein Ticket wieder (Rollback, wenn der Zuordnen-Flow scheitert). */
export async function deleteTicket(ticketId) {
await deleteDocument(config.collections.workorders, ticketId)
}