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:
1 parent
b000dba698
commit
1d149f50b1
4 files changed
+126
-12
No files matched your search
@@ -9,7 +9,7 @@ import { speichereZustimmung, zustimmungFuerSitzung } from './featureConsents.js
|
||||
import { aboLage, laeuft } from './aboStatus.js'
|
||||
import { berlinDateTime } from './berlinTime.js'
|
||||
import { loeschTag } from './termine/frist.js'
|
||||
import { createFeatureTicket } from './ticketAdmin.js'
|
||||
import { ABRECHNUNGS_ZEILEN, createFeatureTicket } from './ticketAdmin.js'
|
||||
|
||||
export const FEATURE_KEYS = featureKeys()
|
||||
/** Bezeichnung (name) und Verneinung im Satz "Für dieses Projekt gibt es noch {keine}" (keine) je Funktion; aus dem Register. */
|
||||
@@ -144,8 +144,8 @@ export const unlockedByVon = (doc) => doc?.unlockedBy || (doc?.unlockedByPurchas
|
||||
* bei News verspraeche das Datum eine Loeschung, die nie kommt) und fuer Oeffnungszeiten live, dort nur beim Abschaltgrund
|
||||
* abo_ende (bei einer Abschaltung durch das Team gibt es keine Loeschfrist); es ist null, wenn disabledAt fehlt oder nicht lesbar ist.
|
||||
* disabledReason und disabledAt gelten fuer jede Funktion.
|
||||
* Funktionen mit aboInklusive (Oeffnungszeiten live) tragen zusaetzlich unlockedBy (kauf | abo | team, null solange die Funktion
|
||||
* aus ist), aboEndeAt (Ende des Wartungs-Abos erkannt) und einbau (angestossen | manuell, Ergebnis des Neubaus).
|
||||
* Jede Funktion traegt unlockedBy (kauf | abo | projekt | team, null solange die Funktion aus ist). Funktionen mit aboInklusive
|
||||
* (Oeffnungszeiten live) tragen zusaetzlich aboEndeAt (Ende des Wartungs-Abos erkannt) und einbau (angestossen | manuell, Ergebnis des Neubaus).
|
||||
* key: Funktionsschluessel (auch ohne Dokument); ohne Angabe der des Dokuments.
|
||||
*/
|
||||
export async function featureView(doc, key = doc?.featureKey) {
|
||||
@@ -162,9 +162,10 @@ export async function featureView(doc, key = doc?.featureKey) {
|
||||
disabledReason: abgeschaltet ? doc.disabledReason || null : null,
|
||||
disabledAt: abgeschaltet ? doc.disabledAt || null : null,
|
||||
loeschungAm: loeschtag ? loeschTag(doc.disabledAt) : null,
|
||||
// unlockedBy (kauf | abo | projekt | team, null solange die Funktion aus ist) gilt fuer jede Funktion (V7); aboEndeAt und einbau nur mit aboInklusive
|
||||
unlockedBy: doc && doc.state !== 'off' ? unlockedByVon(doc) : null,
|
||||
...(mitAbo
|
||||
? {
|
||||
unlockedBy: doc && doc.state !== 'off' ? unlockedByVon(doc) : null,
|
||||
aboEndeAt: doc?.aboEndeAt || null,
|
||||
einbau: doc?.einbau || null,
|
||||
}
|
||||
@@ -454,6 +455,8 @@ async function legeTicketAn(doc, project, customer) {
|
||||
featureKey: doc.featureKey,
|
||||
siteKey: doc.siteKey,
|
||||
...texte,
|
||||
// Abo-Modell: die Abrechnung am Funktionsdokument (abo | projekt) kommt auch bei einem ersetzten Ticket wieder ans Ticket
|
||||
...(Object.hasOwn(ABRECHNUNGS_ZEILEN, doc.einrichtungAbrechnung) ? { abrechnung: doc.einrichtungAbrechnung } : {}),
|
||||
})
|
||||
return ticketId
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user