feat(falukant): implement score threshold logic and enhance UI feedback for certificate progression
All checks were successful
Deploy to production / deploy (push) Successful in 3m3s

- Added a new function to calculate score thresholds based on certificate levels, improving the logic for determining promotion eligibility.
- Updated the FalukantService to include new properties for score and requirement checks, enhancing the decision-making process for certificate readiness.
- Enhanced the OverviewView component to display detailed hints and states regarding certificate progression, providing users with clearer feedback on their status.
- Localized new strings in multiple languages to support the updated UI elements and hints, improving user experience across different languages.
This commit is contained in:
Torsten Schulz (local)
2026-04-01 15:47:11 +02:00
parent d39cea2c01
commit 10fc78e81d
6 changed files with 143 additions and 8 deletions

View File

@@ -208,6 +208,14 @@ function getTargetCertificateByScore(score) {
return 1;
}
function getScoreThresholdForCertificate(level) {
if (level >= 5) return 3.8;
if (level === 4) return 2.8;
if (level === 3) return 1.8;
if (level === 2) return 0.9;
return 0;
}
async function calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPercent = null) {
// Wenn worthPercent nicht übergeben wurde, hole es aus der Datenbank
if (worthPercent === null) {
@@ -2944,6 +2952,7 @@ class FalukantService extends BaseService {
const targetCertificate = getTargetCertificateByScore(score);
const nextCertificate = Math.min(5, currentCertificate + 1);
const nextThreshold = CERTIFICATE_THRESHOLDS[nextCertificate] || null;
const nextScoreThreshold = getScoreThresholdForCertificate(nextCertificate);
const currentValues = {
avgKnowledge,
@@ -3000,17 +3009,22 @@ class FalukantService extends BaseService {
}] : []),
] : [];
const minimumRequirementsMet = nextRequirements.every((requirement) => requirement.met)
&& (!statusRequirement || statusRequirement.fulfilled);
const scoreRequirementMet = nextCertificate <= targetCertificate;
return {
currentCertificate,
nextCertificate,
score: Number(score.toFixed(2)),
targetCertificate,
nextScoreThreshold,
currentValues,
nextRequirements,
statusRequirement,
readyForNextCertificate: nextCertificate <= targetCertificate
&& nextRequirements.every((requirement) => requirement.met)
&& (!statusRequirement || statusRequirement.fulfilled),
scoreRequirementMet,
minimumRequirementsMet,
readyForNextCertificate: scoreRequirementMet && minimumRequirementsMet,
};
}

View File

@@ -130,8 +130,21 @@
"levelMatrix": "Mga produkto per certificate level",
"levelLabel": "Level {level}",
"score": "Puntos",
"scoreGate": "Kinahanglan nga puntos para sa sunod nga level",
"ready": "Andam na para sa sunod nga promotion",
"notReady": "Wala pa maabot ang mga kinahanglanon",
"state": {
"ready": "Pwede na ang promotion sumala sa daemon",
"minimumsMetScoreBlocked": "Naabot na ang minimum, pero gipugngan pa sa puntos",
"scoreMetMinimumsMissing": "Sakto na ang puntos, pero kulang pa ang minimum nga kinahanglanon",
"notReady": "Dili pa andam para sa promotion"
},
"hint": {
"ready": "Para sa level {next}, naabot na ang minimum nga mga kinahanglanon ug ang score threshold nga {threshold}.",
"minimumsMetScoreBlocked": "Naabot na ang makita nga minimum nga mga kinahanglanon para sa level {next}, pero motugot lang ang daemon sa promotion kung maabot sa weighted score ang threshold nga {threshold}. Sa pagkakaron, kutob ra sa level {target} ang score.",
"scoreMetMinimumsMissing": "Ang weighted score igo na unta para sa level {next}, pero naa pay kulang nga minimum nga kinahanglanon.",
"notReady": "Para sa level {next}, kinahanglan maabot ang minimum nga mga kinahanglanon ug ang score threshold nga {threshold}."
},
"factors": "Karon nga mga bili",
"requirements": "Mga kinahanglanon sa sunod nga level"
},

View File

@@ -150,8 +150,21 @@
"levelMatrix": "Produkte nach Zertifikatsstufe",
"levelLabel": "Stufe {level}",
"score": "Wertung",
"scoreGate": "Wertungsgrenze für die nächste Stufe",
"ready": "Für den nächsten Aufstieg bereit",
"notReady": "Bedingungen noch nicht erfüllt",
"state": {
"ready": "Aufstieg aus Daemon-Sicht möglich",
"minimumsMetScoreBlocked": "Mindestanforderungen erfüllt, Wertung blockiert noch",
"scoreMetMinimumsMissing": "Wertung reicht, Mindestanforderungen fehlen noch",
"notReady": "Noch nicht aufstiegsbereit"
},
"hint": {
"ready": "Für Stufe {next} sind sowohl die Mindestanforderungen als auch die Wertungsgrenze von {threshold} erreicht.",
"minimumsMetScoreBlocked": "Die sichtbaren Mindestanforderungen für Stufe {next} sind erfüllt, aber der Daemon lässt den Aufstieg erst zu, wenn die gewichtete Wertung die Schwelle {threshold} erreicht. Aktuell reicht die Wertung nur bis Stufe {target}.",
"scoreMetMinimumsMissing": "Die gewichtete Wertung reicht grundsätzlich für Stufe {next}, aber mindestens eine Mindestanforderung ist noch nicht erfüllt.",
"notReady": "Für Stufe {next} müssen sowohl die Mindestanforderungen als auch die Wertungsgrenze von {threshold} erfüllt sein."
},
"factors": "Aktuelle Werte",
"requirements": "Bedingungen für die nächste Stufe",
"factor": {

View File

@@ -131,8 +131,21 @@
"levelMatrix": "Products by certificate level",
"levelLabel": "Level {level}",
"score": "Score",
"scoreGate": "Score threshold for the next level",
"ready": "Ready for the next promotion",
"notReady": "Requirements not met yet",
"state": {
"ready": "Promotion possible from the daemon's perspective",
"minimumsMetScoreBlocked": "Minimum requirements met, but score still blocks promotion",
"scoreMetMinimumsMissing": "Score is sufficient, minimum requirements still missing",
"notReady": "Not ready for promotion yet"
},
"hint": {
"ready": "For level {next}, both the minimum requirements and the score threshold of {threshold} are met.",
"minimumsMetScoreBlocked": "The visible minimum requirements for level {next} are met, but the daemon will only allow promotion once the weighted score reaches the threshold {threshold}. Right now the score only reaches level {target}.",
"scoreMetMinimumsMissing": "The weighted score is generally high enough for level {next}, but at least one minimum requirement is still missing.",
"notReady": "For level {next}, both the minimum requirements and the score threshold of {threshold} must be met."
},
"factors": "Current values",
"requirements": "Requirements for the next level",
"factor": {

View File

@@ -136,8 +136,21 @@
"levelMatrix": "Productos por nivel de certificado",
"levelLabel": "Nivel {level}",
"score": "Puntuación",
"scoreGate": "Umbral de puntuación para el siguiente nivel",
"ready": "Listo para el siguiente ascenso",
"notReady": "Condiciones aún no cumplidas",
"state": {
"ready": "Ascenso posible desde la perspectiva del daemon",
"minimumsMetScoreBlocked": "Requisitos mínimos cumplidos, pero la puntuación aún bloquea el ascenso",
"scoreMetMinimumsMissing": "La puntuación alcanza, pero aún faltan requisitos mínimos",
"notReady": "Todavía no está listo para ascender"
},
"hint": {
"ready": "Para el nivel {next} se cumplen tanto los requisitos mínimos como el umbral de puntuación de {threshold}.",
"minimumsMetScoreBlocked": "Los requisitos mínimos visibles para el nivel {next} están cumplidos, pero el daemon solo permitirá el ascenso cuando la puntuación ponderada alcance el umbral {threshold}. Ahora mismo la puntuación solo alcanza hasta el nivel {target}.",
"scoreMetMinimumsMissing": "La puntuación ponderada ya sería suficiente para el nivel {next}, pero todavía falta al menos un requisito mínimo.",
"notReady": "Para el nivel {next} deben cumplirse tanto los requisitos mínimos como el umbral de puntuación de {threshold}."
},
"factors": "Valores actuales",
"requirements": "Condiciones para el siguiente nivel",
"factor": {

View File

@@ -112,12 +112,13 @@
<div class="certificate-panel__score">
<span>{{ $t('falukant.overview.certificate.score') }}</span>
<strong>{{ certificateProgress.score }}</strong>
<span class="certificate-panel__state" :class="{ 'is-ready': certificateProgress.readyForNextCertificate }">
{{ certificateProgress.readyForNextCertificate
? $t('falukant.overview.certificate.ready')
: $t('falukant.overview.certificate.notReady') }}
<span class="certificate-panel__state" :class="certificateProgressStateClass">
{{ $t(`falukant.overview.certificate.state.${certificateProgressStateKey}`) }}
</span>
</div>
<p class="certificate-panel__hint">
{{ certificateProgressHint }}
</p>
<div class="certificate-panel__grid">
<article class="certificate-panel__block">
@@ -157,6 +158,13 @@
<article class="certificate-panel__block">
<h4>{{ $t('falukant.overview.certificate.requirements') }}</h4>
<div class="certificate-requirements">
<div
class="certificate-requirement"
:class="{ 'is-met': certificateProgress.scoreRequirementMet }"
>
<span>{{ $t('falukant.overview.certificate.scoreGate') }}</span>
<strong>{{ formatCertificateRequirement(certificateProgress.score, certificateProgress.nextScoreThreshold) }}</strong>
</div>
<div
v-for="requirement in certificateProgress.nextRequirements"
:key="requirement.type"
@@ -438,6 +446,52 @@ export default {
products: entry.products.map((productKey) => this.$t(`falukant.product.${productKey}`)),
}));
},
certificateProgressStateKey() {
const progress = this.certificateProgress;
if (!progress) return 'notReady';
if (progress.readyForNextCertificate) return 'ready';
if (progress.minimumRequirementsMet && !progress.scoreRequirementMet) return 'minimumsMetScoreBlocked';
if (!progress.minimumRequirementsMet && progress.scoreRequirementMet) return 'scoreMetMinimumsMissing';
return 'notReady';
},
certificateProgressStateClass() {
switch (this.certificateProgressStateKey) {
case 'ready':
return 'is-ready';
case 'minimumsMetScoreBlocked':
return 'is-warning';
case 'scoreMetMinimumsMissing':
return 'is-info';
default:
return '';
}
},
certificateProgressHint() {
const progress = this.certificateProgress;
if (!progress) return '';
if (progress.readyForNextCertificate) {
return this.$t('falukant.overview.certificate.hint.ready', {
next: progress.nextCertificate,
threshold: this.formatCertificateValue(progress.nextScoreThreshold, 1),
});
}
if (progress.minimumRequirementsMet && !progress.scoreRequirementMet) {
return this.$t('falukant.overview.certificate.hint.minimumsMetScoreBlocked', {
next: progress.nextCertificate,
target: progress.targetCertificate,
threshold: this.formatCertificateValue(progress.nextScoreThreshold, 1),
});
}
if (!progress.minimumRequirementsMet && progress.scoreRequirementMet) {
return this.$t('falukant.overview.certificate.hint.scoreMetMinimumsMissing', {
next: progress.nextCertificate,
});
}
return this.$t('falukant.overview.certificate.hint.notReady', {
next: progress.nextCertificate,
threshold: this.formatCertificateValue(progress.nextScoreThreshold, 1),
});
},
routineActions() {
return [
{
@@ -820,7 +874,7 @@ export default {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
margin-bottom: 8px;
}
.certificate-panel__state {
@@ -838,6 +892,21 @@ export default {
color: #2f6b3d;
}
.certificate-panel__state.is-warning {
background: rgba(185, 99, 24, 0.16);
color: #8d5412;
}
.certificate-panel__state.is-info {
background: rgba(34, 96, 164, 0.14);
color: #21598f;
}
.certificate-panel__hint {
margin: 0 0 16px;
color: var(--color-text-secondary);
}
.certificate-panel__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));