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>
210 lines
7.4 KiB
JavaScript
210 lines
7.4 KiB
JavaScript
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)
|
||
}
|