Implement certificate progress feature in FalukantService and frontend: Add methods to calculate and retrieve certificate progress based on user attributes. Update localization files for English, German, and Spanish to include new terms related to certificate progress. Enhance OverviewView to display certificate details and requirements, improving user experience and clarity.

This commit is contained in:
Torsten Schulz (local)
2026-03-25 11:59:43 +01:00
parent b61a533eac
commit 44991743d2
5 changed files with 520 additions and 2 deletions

View File

@@ -96,6 +96,99 @@
</button>
</article>
</section>
<section v-if="certificateProgress" class="certificate-panel surface-card">
<div class="certificate-panel__header">
<div>
<h3>{{ $t('falukant.overview.certificate.title') }}</h3>
<p>{{ $t('falukant.overview.certificate.description') }}</p>
</div>
<div class="certificate-panel__badges">
<span class="summary-card__label">{{ $t('falukant.overview.certificate.current') }}: {{ certificateProgress.currentCertificate }}</span>
<span class="summary-card__label">{{ $t('falukant.overview.certificate.next') }}: {{ certificateProgress.nextCertificate }}</span>
</div>
</div>
<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>
</div>
<div class="certificate-panel__grid">
<article class="certificate-panel__block">
<h4>{{ $t('falukant.overview.certificate.factors') }}</h4>
<div class="detail-list">
<div class="detail-list__item">
<span>{{ $t('falukant.overview.certificate.factor.avgKnowledge') }}</span>
<strong>{{ formatCertificateValue(certificateProgress.currentValues.avgKnowledge, 1) }}</strong>
</div>
<div class="detail-list__item">
<span>{{ $t('falukant.overview.certificate.factor.completedProductions') }}</span>
<strong>{{ certificateProgress.currentValues.completedProductions }}</strong>
</div>
<div class="detail-list__item">
<span>{{ $t('falukant.overview.certificate.factor.reputation') }}</span>
<strong>{{ certificateProgress.currentValues.reputation }}</strong>
</div>
<div class="detail-list__item">
<span>{{ $t('falukant.overview.certificate.factor.housePosition') }}</span>
<strong>{{ certificateProgress.currentValues.housePosition }}</strong>
</div>
<div class="detail-list__item">
<span>{{ $t('falukant.overview.certificate.factor.highestPoliticalOfficeRank') }}</span>
<strong>{{ certificateProgress.currentValues.highestPoliticalOfficeRank }}</strong>
</div>
<div class="detail-list__item">
<span>{{ $t('falukant.overview.certificate.factor.highestChurchOfficeRank') }}</span>
<strong>{{ certificateProgress.currentValues.highestChurchOfficeRank }}</strong>
</div>
<div class="detail-list__item">
<span>{{ $t('falukant.overview.certificate.factor.nobilityLevel') }}</span>
<strong>{{ certificateProgress.currentValues.nobilityLevel }}</strong>
</div>
</div>
</article>
<article class="certificate-panel__block">
<h4>{{ $t('falukant.overview.certificate.requirements') }}</h4>
<div class="certificate-requirements">
<div
v-for="requirement in certificateProgress.nextRequirements"
:key="requirement.type"
class="certificate-requirement"
:class="{ 'is-met': requirement.met }"
>
<span>{{ certificateRequirementLabel(requirement.type) }}</span>
<strong>{{ formatCertificateRequirement(requirement.current, requirement.required) }}</strong>
</div>
<div
v-if="certificateProgress.statusRequirement"
class="certificate-requirement"
:class="{ 'is-met': certificateProgress.statusRequirement.fulfilled }"
>
<span>{{ certificateStatusRequirementLabel(certificateProgress.statusRequirement.mode) }}</span>
<strong>{{ certificateProgress.statusRequirement.metCount }}/{{ certificateProgress.statusRequirement.requiredCount }}</strong>
</div>
</div>
<ul v-if="certificateProgress.statusRequirement?.options?.length" class="certificate-status-options">
<li
v-for="option in certificateProgress.statusRequirement.options"
:key="option.type"
:class="{ 'is-met': option.met }"
>
{{ certificateRequirementLabel(option.type) }}:
{{ formatCertificateRequirement(option.current, option.required) }}
</li>
</ul>
</article>
</div>
</section>
<!-- Erben-Auswahl wenn kein Charakter vorhanden -->
<div v-if="!falukantUser?.character" class="heir-selection-container">
@@ -313,6 +406,9 @@ export default {
stockEntryCount() {
return this.allStock.length;
},
certificateProgress() {
return this.falukantUser?.certificateProgress || null;
},
routineActions() {
return [
{
@@ -553,6 +649,24 @@ export default {
formatDate(timestamp) {
return new Date(timestamp).toLocaleString();
},
formatCertificateValue(value, digits = 0) {
if (value == null) return '---';
return new Intl.NumberFormat(this.locale, {
minimumFractionDigits: digits,
maximumFractionDigits: digits,
}).format(value);
},
formatCertificateRequirement(current, required) {
const currentDigits = typeof current === 'number' && !Number.isInteger(current) ? 1 : 0;
const requiredDigits = typeof required === 'number' && !Number.isInteger(required) ? 1 : 0;
return `${this.formatCertificateValue(current, currentDigits)} / ${this.formatCertificateValue(required, requiredDigits)}`;
},
certificateRequirementLabel(type) {
return this.$t(`falukant.overview.certificate.factor.${type}`);
},
certificateStatusRequirementLabel(mode) {
return this.$t(`falukant.overview.certificate.statusMode.${mode}`);
},
async fetchPotentialHeirs() {
this.loadingHeirs = true;
try {
@@ -649,6 +763,94 @@ export default {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.certificate-panel {
margin-bottom: 16px;
padding: 18px;
}
.certificate-panel__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
margin-bottom: 14px;
}
.certificate-panel__header p {
margin: 6px 0 0;
color: var(--color-text-secondary);
}
.certificate-panel__badges {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.certificate-panel__score {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.certificate-panel__state {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 999px;
background: rgba(185, 99, 24, 0.12);
color: #8d5412;
font-weight: 700;
}
.certificate-panel__state.is-ready {
background: rgba(68, 138, 86, 0.14);
color: #2f6b3d;
}
.certificate-panel__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.certificate-panel__block h4 {
margin: 0 0 10px;
}
.certificate-requirements {
display: grid;
gap: 10px;
}
.certificate-requirement {
display: flex;
justify-content: space-between;
gap: 16px;
padding: 12px 14px;
border-radius: var(--radius-md);
background: rgba(185, 99, 24, 0.08);
}
.certificate-requirement.is-met {
background: rgba(68, 138, 86, 0.12);
}
.certificate-status-options {
margin: 12px 0 0;
padding-left: 18px;
}
.certificate-status-options li {
margin-bottom: 6px;
}
.certificate-status-options li.is-met {
color: #2f6b3d;
font-weight: 600;
}
.summary-card,
.routine-card {
padding: 18px;
@@ -936,9 +1138,17 @@ export default {
@media (max-width: 900px) {
.falukant-summary-grid,
.falukant-routine-grid,
.certificate-panel__grid,
.overviewcontainer {
grid-template-columns: 1fr;
}
.certificate-panel__header,
.certificate-panel__score,
.certificate-requirement {
flex-direction: column;
align-items: flex-start;
}
}
.select-heir-button {