Kundenansicht P5: Einrichtungs-Ticket mit Abrechnung, unlockedBy für alle Funktionen

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-16 16:00:00 +00:00
1 parent b000dba698
commit 1d149f50b1
4 files changed
+126 -12

No files matched your search

+28 -6
View File
@@ -69,6 +69,15 @@ export async function ensureProjectTicket(project, { repoFullName } = {}) {
return ticket.$id
}
/**
* Schlusszeile der Ticket-Details im Abo-Modell (V7): sagt dem Team, wie die Einrichtung abgerechnet wird. Steht immer als letzte
* Zeile, auch nach dem Kuerzen und im Notfallweg.
*/
export const ABRECHNUNGS_ZEILEN = {
abo: 'Zeit im Arbeitsblatt erfassen und aufs Abo-Stundenkonto anrechnen (Einrichtung aus dem Abo, Vorschuss möglich).',
projekt: 'Zeit im Arbeitsblatt erfassen, nicht aufs Abo anrechnen (im Projekt enthalten).',
}
/**
* 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.
@@ -78,6 +87,10 @@ export async function ensureProjectTicket(project, { repoFullName } = {}) {
* (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.
* abrechnung ('' | 'abo' | 'projekt', Abo-Modell): setzt am Ticket einrichtungFuer (= featureKey) und einrichtungAbrechnung und haengt
* ABRECHNUNGS_ZEILEN[abrechnung] als letzte Zeile an die Details (der Text wird dafuer auf TICKET_DETAILS_MAX minus Zeile gekuerzt).
* Ohne abrechnung schreibt das Ticket weder die Felder noch die Zeile (alte Ansicht unveraendert). Ein anderer Wert ist ein
* TypeError vor jedem Schreibzugriff.
*/
export async function createFeatureTicket({
customerId,
@@ -87,14 +100,24 @@ export async function createFeatureTicket({
topic = null,
details = null,
fallbackDetails = null,
abrechnung = '',
}) {
if (abrechnung !== '' && !(typeof abrechnung === 'string' && Object.hasOwn(ABRECHNUNGS_ZEILEN, abrechnung))) {
throw new TypeError(`createFeatureTicket: abrechnung muss '', 'abo' oder 'projekt' sein, nicht ${String(abrechnung)}`)
}
const zeile = abrechnung ? ABRECHNUNGS_ZEILEN[abrechnung] : ''
// Details auf TICKET_DETAILS_MAX kuerzen; mit Abrechnung bleibt Platz fuer die Schlusszeile (und ihren Zeilenumbruch)
const kuerzeDetails = (roh) => {
const gekuerzt = kuerze(roh, zeile ? TICKET_DETAILS_MAX - zeile.length - 1 : TICKET_DETAILS_MAX, { anZeile: true, endzeile: TICKET_GEKUERZT })
return { gekuerzt, text: zeile ? `${gekuerzt}\n${zeile}` : gekuerzt }
}
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(
const { gekuerzt, text } = kuerzeDetails(
details ||
[
`Funktion: ${featureKey}`,
@@ -103,9 +126,7 @@ export async function createFeatureTicket({
`siteKey: ${siteKey}`,
`Einbau: ${embed}`,
'Anleitung: docs/funktionen/news-einbau.md (Repo Webklar-Kundenbereich)',
].join('\n'),
TICKET_DETAILS_MAX,
{ anZeile: true, endzeile: TICKET_GEKUERZT }
].join('\n')
)
const fields = {
topic: kuerze(topic || `News-Pinnwand einbauen – ${projectName}`.trim(), TICKET_TOPIC_MAX, { endzeile: '…' }),
@@ -119,6 +140,7 @@ export async function createFeatureTicket({
customerLocation: customer?.location || '',
woid,
createdAt: new Date().toISOString(),
...(abrechnung ? { einrichtungFuer: featureKey, einrichtungAbrechnung: abrechnung } : {}),
}
let ticket
@@ -126,12 +148,12 @@ export async function createFeatureTicket({
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
if (firstError?.status !== 400 || !hatRueckfall || gekuerzt.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 }) },
{ ...fields, details: kuerzeDetails(fallbackDetails).text },
ID.unique()
)
} catch (secondError) {