diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js index 5547926..1535a9b 100644 --- a/backend/services/falukantService.js +++ b/backend/services/falukantService.js @@ -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, }; } diff --git a/frontend/src/i18n/locales/ceb/falukant.json b/frontend/src/i18n/locales/ceb/falukant.json index e50b553..8037c90 100644 --- a/frontend/src/i18n/locales/ceb/falukant.json +++ b/frontend/src/i18n/locales/ceb/falukant.json @@ -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" }, diff --git a/frontend/src/i18n/locales/de/falukant.json b/frontend/src/i18n/locales/de/falukant.json index ad7e43c..44b883b 100644 --- a/frontend/src/i18n/locales/de/falukant.json +++ b/frontend/src/i18n/locales/de/falukant.json @@ -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": { diff --git a/frontend/src/i18n/locales/en/falukant.json b/frontend/src/i18n/locales/en/falukant.json index 12b28de..1052dc4 100644 --- a/frontend/src/i18n/locales/en/falukant.json +++ b/frontend/src/i18n/locales/en/falukant.json @@ -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": { diff --git a/frontend/src/i18n/locales/es/falukant.json b/frontend/src/i18n/locales/es/falukant.json index 3e4c737..d092a67 100644 --- a/frontend/src/i18n/locales/es/falukant.json +++ b/frontend/src/i18n/locales/es/falukant.json @@ -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": { diff --git a/frontend/src/views/falukant/OverviewView.vue b/frontend/src/views/falukant/OverviewView.vue index eba09d9..a71a980 100644 --- a/frontend/src/views/falukant/OverviewView.vue +++ b/frontend/src/views/falukant/OverviewView.vue @@ -112,12 +112,13 @@
{{ $t('falukant.overview.certificate.score') }} {{ certificateProgress.score }} - - {{ certificateProgress.readyForNextCertificate - ? $t('falukant.overview.certificate.ready') - : $t('falukant.overview.certificate.notReady') }} + + {{ $t(`falukant.overview.certificate.state.${certificateProgressStateKey}`) }}
+

+ {{ certificateProgressHint }} +

@@ -157,6 +158,13 @@

{{ $t('falukant.overview.certificate.requirements') }}

+
+ {{ $t('falukant.overview.certificate.scoreGate') }} + {{ formatCertificateRequirement(certificateProgress.score, certificateProgress.nextScoreThreshold) }} +
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));