74 lines
3.1 KiB
TypeScript
74 lines
3.1 KiB
TypeScript
import type { FeedbackCriterionItem, FeedbackRating } from '../db'
|
|
import { RATING_NUM_MAP, RATING_TREND_DELTA_THRESHOLD } from '../config/constants'
|
|
|
|
export interface MeetingScorePoint {
|
|
meetingId: number
|
|
date: string
|
|
score: number
|
|
}
|
|
|
|
export interface TrendResult {
|
|
suggestion: number | null
|
|
history: MeetingScorePoint[]
|
|
trend: 'up' | 'down' | 'stable' | null
|
|
delta: number | null
|
|
}
|
|
|
|
/**
|
|
* Ein Wert pro Meeting für eine Gruppe von Kriterien (eine Kategorie oder alle Items für "overall").
|
|
* Meetings ohne bewertetes Item aus der Gruppe werden übersprungen — ein Delivery-Call, der eine
|
|
* Kategorie nicht abfragt, darf deren Zeitreihe nicht verfälschen.
|
|
*/
|
|
export function buildGroupMeetingScores(
|
|
items: FeedbackCriterionItem[],
|
|
meetingsChronological: { id: number; date: string }[],
|
|
getScore: (meetingId: number, itemId: number) => FeedbackRating | null,
|
|
): MeetingScorePoint[] {
|
|
const points: MeetingScorePoint[] = []
|
|
for (const meeting of meetingsChronological) {
|
|
const rated = items
|
|
.map(i => ({ score: getScore(meeting.id, i.id!), w: i.weight ?? 1 }))
|
|
.filter((x): x is { score: FeedbackRating; w: number } => x.score !== null && x.score !== 'na')
|
|
if (rated.length === 0) continue
|
|
const wSum = rated.reduce((s, x) => s + x.w, 0)
|
|
if (wSum === 0) continue // alle beteiligten Kriterien auf Gewicht 0 → keine Aussage möglich, sonst NaN < x ist überall false und fällt auf "Fully" durch
|
|
const score = rated.reduce((s, x) => s + RATING_NUM_MAP[x.score] * x.w, 0) / wSum
|
|
points.push({ meetingId: meeting.id, date: meeting.date, score })
|
|
}
|
|
return points
|
|
}
|
|
|
|
/**
|
|
* Lineare Zeitgewichtung: rank r (1-basiert, 1 = ältester Datenpunkt) → weight = 1 + (r-1) * weightStep.
|
|
* Trend wird aus dem Delta zwischen erstem und letztem Rohwert abgeleitet (nicht dem gewichteten Wert),
|
|
* da das ohnehin nur ab 2 Datenpunkten sinnvoll ist und der gewichtete Wert selbst schon Teil des
|
|
* Vorschlags ist.
|
|
*/
|
|
export function computeTrend(history: MeetingScorePoint[], weightStep: number): TrendResult {
|
|
if (history.length === 0) return { suggestion: null, history, trend: null, delta: null }
|
|
|
|
const weights = previewWeights(history.length, weightStep)
|
|
const wSum = weights.reduce((s, w) => s + w, 0)
|
|
const suggestion = history.reduce((s, p, i) => s + p.score * weights[i], 0) / wSum
|
|
|
|
if (history.length < 2) return { suggestion, history, trend: null, delta: null }
|
|
|
|
const delta = history[history.length - 1].score - history[0].score
|
|
const trend = delta >= RATING_TREND_DELTA_THRESHOLD ? 'up'
|
|
: delta <= -RATING_TREND_DELTA_THRESHOLD ? 'down'
|
|
: 'stable'
|
|
return { suggestion, history, trend, delta }
|
|
}
|
|
|
|
export function numberToRating(avg: number): FeedbackRating {
|
|
if (avg < 1.5) return 'not_client_ready'
|
|
if (avg < 2.5) return 'partially_client_ready'
|
|
if (avg < 3.5) return 'nearly_client_ready'
|
|
return 'client_ready'
|
|
}
|
|
|
|
/** Resultierende Gewichte für k chronologische Datenpunkte, z.B. previewWeights(3, 0.5) => [1, 1.5, 2]. */
|
|
export function previewWeights(k: number, weightStep: number): number[] {
|
|
return Array.from({ length: k }, (_, i) => 1 + i * weightStep)
|
|
}
|