Commit 628e5bf2 authored by abergavenny's avatar abergavenny
Browse files

Version 1.0.0

parent b463e8cc
<script setup>
import { ref } from 'vue'
import ContentPanel from '@/components/ContentPanel.vue'
import DataItem from '@/components/DataItem.vue'
import DataPage from '@/components/DataPage.vue'
import EnergyPerformanceCertificate from '@/components/EnergyPerformanceCertificate.vue'
import ExpertList from '@/components/ExpertList.vue'
import MessageBox from '@/components/MessageBox.vue'
import ModalContainer from '@/components/ModalContainer.vue'
import NoData from '@/components/NoData.vue'
import SectionTitle from '@/components/SectionTitle.vue'
import WaitingForData from '@/components/WaitingForData.vue'
import { HEATING_INSTALLATION_OPTIONS } from '@/data/options'
import {
REFERENCE_ENERGY_PERFORMANCE,
REFERENCE_INSULATION_FACADE,
REFERENCE_INSULATION_ROOF
} from '@/data/parameters'
import { useBuildingStore } from '@/stores/buildings'
import { numberOrText } from '../helpers'
const showModal = ref(false)
const buildings = useBuildingStore()
// DEVINFO Hier wird der Link zum CS-T erzeugt
function buildUrl(building = {}) {
if (building) {
const BASE_URI = import.meta.env.VITE_CST_URI
const { heatedAttic, heatedBasement, numberOfFloors, yearOfConstruction } = building.data
const addr = building.address ? building.address.split('::') : null
const encodedAddr = encodeURIComponent(`${addr[0]} ${addr[1]}, ${addr[2]} ${addr[3]}`)
return `${BASE_URI}?y=${yearOfConstruction}&ad=${encodedAddr}&s=${numberOfFloors}&a=${+heatedAttic}&b=${+heatedBasement}`
}
}
// DEVINFO Hier kann der Bewertungstext (CO2) angepasst werden
function convertCO2EfficiencyMessage(value, item) {
if (value) {
const str = HEATING_INSTALLATION_OPTIONS.find(element => element.value === item)
return `Im Vergleich zu Ihrer Heizung mit ${str.name} können Sie mit einer Pelletheizung ${numberOrText(value)} kg CO2 einsparen.`
}
else return `Sie sind auf einem guten Niveau.`
}
// DEVINFO Hier kann der Bewertungstext (Energieausweis) angepasst werden
function convertHeatingEfficiencyMessage(value, potential) {
if (value < REFERENCE_ENERGY_PERFORMANCE) return `Sie sind auf einem guten Niveau.`
else if (value === REFERENCE_ENERGY_PERFORMANCE) return `Sie sind im Mittel (Klasse E).`
else if (value > REFERENCE_ENERGY_PERFORMANCE) return `Im Vergleich zu einem durchschnittlichen Wohngebäude in Deutschland, liegt das Verbesserungspotenzial Ihrer Energieeffizienz bei ${potential}%.`
else return null
}
// DEVINFO Hier kann der Bewertungstext (Fassade - Wert) angepasst werden
function convertFacadeInsulationEfficiency(value) {
if (value < 0.22) return `sehr gut (U-Wert: ${value})`
else if (value <= 0.28) return `gut (U-Wert: ${value})`
else if (value <= 0.35) return `akzeptabel (U-Wert: ${value})`
else if (value > 0.35) return `schlecht (U-Wert: ${value})`
else return null
}
// DEVINFO Hier kann der Bewertungstext (Fassade - Empfehlung) angepasst werden
function convertFacadeInsulationEfficiencyMessage(value, potential) {
if (value <= REFERENCE_INSULATION_FACADE) return `Sie sind auf einem guten Niveau. Es besteht keine signifikantes Verbesserungspotential.`
else if (value > REFERENCE_INSULATION_FACADE) {
const range = potential < 40 ? ' ' : ' deutlich '
return `Der Wärmeverlust Ihres Gebäudes ist${range}höher, als er mit einer guten Dämmung sein könnte – hier besteht Verbesserungspotenzial (ca. ${potential}%).`
}
else return null
}
// DEVINFO Hier kann der Bewertungstext (Dach - Wert) angepasst werden
function convertRoofInsulationEfficiency(value) {
if (value < 0.18) return `sehr gut (U-Wert: ${value})`
else if (value <= 0.24) return `gut (U-Wert: ${value})`
else if (value <= 0.31) return `akzeptabel (U-Wert: ${value})`
else if (value > 0.31) return `schlecht (U-Wert: ${value})`
else return null
}
// DEVINFO Hier kann der Bewertungstext (Dach - Empfehlung) angepasst werden
function convertRoofInsulationEfficiencyMessage(value, potential) {
if (value <= REFERENCE_INSULATION_ROOF) return `Sie sind auf einem guten Niveau. Es besteht keine signifikantes Verbesserungspotential.`
else if (value > REFERENCE_INSULATION_ROOF) {
const range = potential < 40 ? ' ' : ' deutlich '
return `Der Wärmeverlust von Ihrem Dach ist${range}höher, als er mit einer guten Dämmung sein könnte – hier besteht Verbesserungspotenzial (ca. ${potential}%).`
}
else return null
}
function handleModal() {
showModal.value = !showModal.value
}
</script>
<template>
<ContentPanel label="Ihre Effizienz">
<DataPage :stretch="true">
<div class="content-wrapper" v-if="buildings.current && buildings.active">
<div class="content stretched scroll">
<SectionTitle value="Wärmebedarf" />
<div class="info-box" v-if="buildings.heatingEfficiency">
<DataItem label="Effizienzklasse" :data="`${buildings.heatingEfficiency.performance} kWh/m²`" />
<MessageBox
:msg="convertHeatingEfficiencyMessage(buildings.heatingEfficiency.performance, buildings.heatingEfficiency.potential)"
type="basic" />
<EnergyPerformanceCertificate :data="buildings.heatingEfficiency.performance" />
</div>
<WaitingForData v-else />
<SectionTitle value="Fassadendämmung" />
<div class="info-box" v-if="buildings.facadeInsulationEfficiency">
<DataItem label="Niveau"
:data="convertFacadeInsulationEfficiency(buildings.facadeInsulationEfficiency.performance)" />
<MessageBox
:msg="convertFacadeInsulationEfficiencyMessage(buildings.facadeInsulationEfficiency.performance, buildings.facadeInsulationEfficiency.potential)"
type="basic" />
</div>
<WaitingForData v-else />
<SectionTitle value="Dachdämmung" />
<div class="info-box" v-if="buildings.roofInsulationEfficiency">
<DataItem label="Niveau"
:data="convertRoofInsulationEfficiency(buildings.roofInsulationEfficiency.performance)" />
<MessageBox
:msg="convertRoofInsulationEfficiencyMessage(buildings.roofInsulationEfficiency.performance, buildings.roofInsulationEfficiency.potential)"
type="basic" />
</div>
<WaitingForData v-else />
<SectionTitle value="Emissionen Heizung" />
<div class="info-box" v-if="buildings.co2Efficiency">
<DataItem label="CO2 Emissionen" :data="buildings.co2Efficiency.performance" unit="kg" />
<DataItem label="Entspricht Fahrstrecke" :data="buildings.co2Efficiency.proportion" unit="km" />
<MessageBox
:msg="convertCO2EfficiencyMessage(buildings.co2Efficiency.potential, buildings.current?.data.heatingInstallation)"
type="basic" />
</div>
<WaitingForData v-else />
</div>
<div class="content">
<a class="button primary" :href="buildUrl(buildings.current)">CS-T Simulation (extern)</a>
<button class="button primary" type="button" @click="handleModal()">DEN-Expertensuche (extern)</button>
</div>
</div>
<NoData v-else />
</DataPage>
</ContentPanel>
<ModalContainer v-if="showModal" label="DEN e.V. Expertensuche" @close-modal="handleModal()">
<ExpertList />
</ModalContainer>
</template>
\ No newline at end of file
<script setup>
import { onMounted, ref } from 'vue'
import ModalContainer from '@/components/ModalContainer.vue'
import SetupForm from '@/components/forms/SetupForm.vue'
import { useBuildingStore } from '@/stores/buildings'
const isOpen = ref()
const setupRequired = ref(false)
const showModal = ref(false)
const buildings = useBuildingStore()
function togglePicker() {
isOpen.value = !isOpen.value
}
function onSelect(id) {
buildings.setActiveBuilding(id)
isOpen.value = false
}
function openModal() {
showModal.value = true
}
function closeModal() {
showModal.value = false
}
function onComplete() {
setupRequired.value = false
}
function getAddressString(value) {
if (value) {
const el = value.split('::')
return `${el[0]} ${el[1]}, ${el[2]} ${el[3]}`
}
return null
}
onMounted(() => {
if (buildings.firstBuilding?.setupCompleted === false) {
setupRequired.value = true
showModal.value = true
}
})
</script>
<template>
<div class="building-picker">
<div class="building-picker__dropdown">
<button class="button secondary" type="button" @click="togglePicker" :disabled="setupRequired">
<div class="building-picker__button" v-if="buildings.current">
<span class="building-picker__prefix">{{ buildings.current?.prefix }}</span>
<span>{{ buildings.current?.name }}</span>
</div>
<div v-else>
<span>Bitte Gebäude auswählen</span>
</div>
<div class="button-icon">
<font-awesome-icon v-if="isOpen" icon="fa-solid fa-chevron-up" />
<font-awesome-icon v-else icon="fa-solid fa-chevron-down" />
</div>
</button>
<ul class="building-picker__list shadow" v-if="isOpen">
<li class="building-picker__item" v-for="building of buildings.data" :key="building._id"
@click="onSelect(building._id)">
<div class="building-picker__title">
<span class="building-picker__prefix">{{ building.prefix }}</span>
<span>{{ building.name || building._id }}</span>
</div>
<div class="building-picker__subtitle">
<span>{{ getAddressString(building.address) }}</span>
</div>
</li>
</ul>
</div>
<div v-if="setupRequired && buildings.firstBuilding">
<button class="button secondary" type="button" @click="openModal">
<div class="button-content">Einrichtung abchließen</div>
<div class="button-icon">
<font-awesome-icon icon="fa-solid fa-pen" />
</div>
</button>
</div>
<ModalContainer v-if="showModal" label="Stammdaten eingeben" @close-modal="closeModal">
<SetupForm @close-modal="closeModal" @setup-completed="onComplete" />
</ModalContainer>
</div>
</template>
<style scoped>
.building-picker {
z-index: 1;
height: var(--element-size);
display: flex;
gap: var(--spacing);
}
.building-picker__button {
height: var(--element-size);
display: flex;
gap: var(--spacing);
align-items: center;
}
.building-picker__dropdown {
position: relative;
height: var(--element-size);
flex: 1;
display: flex;
border-radius: var(--radius);
cursor: pointer;
}
.building-picker__list {
position: fixed;
background-color: hsl(var(--clr-content));
top: 4em;
left: 0;
margin: var(--spacing-s);
padding: var(--spacing-s);
width: calc(100% - var(--spacing-s) * 2);
display: flex;
flex-direction: column;
gap: var(--spacing-s);
border: var(--border) solid hsl(var(--clr-primary));
border-radius: var(--radius);
list-style: none;
}
@media (min-width: 48em) {
.building-picker__list {
position: absolute;
top: 3.5em;
margin: 0;
width: auto;
}
}
.building-picker__item {
overflow: hidden;
padding: var(--spacing-s);
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
border-radius: var(--radius);
}
.building-picker__item:hover {
background-color: hsl(var(--clr-background));
}
.building-picker__prefix {
background-color: hsl(var(--clr-primary));
padding: 0.2em var(--spacing-s);
border-radius: var(--radius);
color: var(--clr-white);
font-size: var(--text-s);
}
.building-picker__title,
.building-picker__subtitle {
width: 100%;
display: flex;
}
.building-picker__title {
gap: var(--spacing-s);
align-items: center;
}
.building-picker__subtitle {
white-space: nowrap;
}
.building-picker__subtitle>span {
font-size: var(--text-s);
}
</style>
\ No newline at end of file
<script setup>
defineProps(['value'])
</script>
<template>
<label class="checkbox-wrapper">
<slot />
<span>{{ value }}</span>
</label>
</template>
<style scoped>
.checkbox-wrapper {
display: flex;
gap: var(--spacing);
}
</style>
\ No newline at end of file
<script setup>
import { MESSAGES } from '@/data/messages'
</script>
<template>
<div class="content-loading">
<span>{{ MESSAGES['LOADING_DATA'] }}</span>
</div>
</template>
<style scoped>
.content-loading {
background-color: hsl(var(--clr-primary));
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
display: flex;
justify-content: center;
align-items: center;
color: var(--clr-white);
animation: pulse 750ms infinite ease-in-out;
}
</style>
\ No newline at end of file
<script setup>
defineProps(['label'])
</script>
<template>
<div class="container">
<div class="headline">
<span>{{ label }}</span>
</div>
<slot />
</div>
</template>
\ No newline at end of file
<script setup>
import { numberOrText } from '@/helpers'
defineProps(['asText', 'data', 'label', 'data', 'unit'])
</script>
<template>
<div class="data-item">
<span class="data-item__label">{{ label }}</span>
<span class="data-item__value">{{ !data ? '-' : unit ? `${numberOrText(data, asText)} ${unit}` :
numberOrText(data, asText)
}}</span>
</div>
</template>
<style scoped>
.data-item {
display: flex;
justify-content: space-between;
border-bottom: 1px solid hsl(var(--clr-background));
}
.data-item__label {
font-size: var(--text-s);
}
.data-item__value {
font-size: var(--text-s);
}
</style>
\ No newline at end of file
<script setup>
const props = defineProps(['stretch'])
</script>
<template>
<div class="data-page shadow" :class="{ 'data-page__stretch': props.stretch }">
<slot />
</div>
</template>
<style scoped>
.data-page {
overflow: hidden;
background-color: hsl(var(--clr-content));
width: 100%;
display: flex;
flex-direction: column;
border-radius: var(--radius);
}
.data-page__stretch {
flex: 1;
}
</style>
\ No newline at end of file
<script setup>
import { onMounted, onUpdated, ref } from 'vue'
const props = defineProps(['data'])
const pos = ref(0)
function calculatePosition(pos) {
const offset = pos * 0.25
return `${pos + offset}em`
}
function getCurrentRange(value) {
if (value >= 0 && value < 30) {
pos.value = 0
}
else if (value < 50) {
pos.value = 1
}
else if (value < 75) {
pos.value = 2
}
else if (value < 100) {
pos.value = 3
}
else if (value < 130) {
pos.value = 4
}
else if (value < 160) {
pos.value = 5
}
else if (value < 200) {
pos.value = 6
}
else if (value < 250) {
pos.value = 7
}
else if (value >= 250) {
pos.value = 8
} else {
return
}
}
onMounted(() => {
getCurrentRange(props.data)
})
onUpdated(() => {
getCurrentRange(props.data)
})
</script>
<template>
<div class="epc" v-if="props.data">
<div class="epc__bar" :style="{ backgroundColor: '#449881', color: '#449881', width: '55%' }">
<span>A+</span>
</div>
<div class="epc__bar" :style="{ backgroundColor: '#5ea742', color: '#5ea742', width: '60%' }">
<span>A</span>
</div>
<div class="epc__bar" :style="{ backgroundColor: '#a3c93e', color: '#a3c93e', width: '65%' }">
<span>B</span>
</div>
<div class="epc__bar" :style="{ backgroundColor: '#beda76', color: '#beda76', width: '70%' }">
<span>C</span>
</div>
<div class="epc__bar" :style="{ backgroundColor: '#fbe24a', color: '#fbe24a', width: '75%' }">
<span>D</span>
</div>
<div class="epc__bar" :style="{ backgroundColor: '#f9c268', color: '#f9c268', width: '80%' }">
<span>E</span>
</div>
<div class="epc__bar" :style="{ backgroundColor: '#f6b247', color: '#f6b247', width: '85%' }">
<span>F</span>
</div>
<div class="epc__bar" :style="{ backgroundColor: '#ee736c', color: '#ee736c', width: '90%' }">
<span>G</span>
</div>
<div class="epc__bar" :style="{ backgroundColor: '#ea554e', color: '#ea554e', width: '95%' }">
<span>H</span>
</div>
<div class="epc__marker" :style="{ top: calculatePosition(pos) }"></div>
</div>
</template>
<style scoped>
.epc {
width: calc(100% - 1em);
display: flex;
flex-direction: column;
gap: 0.25em;
}
.epc__bar {
position: relative;
padding-left: var(--spacing);
height: 1em;
display: flex;
align-items: center;
border-radius: 0.5em 0 0 0.5em;
}
.epc__bar::after {
position: absolute;
content: '';
right: -0.5em;
border-top: 0.5em solid transparent;
border-bottom: 0.5em solid transparent;
border-left: 0.5em solid;
}
.epc__bar>span {
color: var(--clr-text);
font-weight: bold;
font-size: var(--text-s);
}
.epc__marker {
z-index: 1;
position: absolute;
left: 0;
width: 0;
height: 0;
border-top: 0.5em solid transparent;
border-bottom: 0.5em solid transparent;
border-left: 0.5em solid hsl(var(--clr-black));
transition: top ease-out 250ms;
}
</style>
\ No newline at end of file
<script setup>
defineProps(['label', 'data', 'unit'])
</script>
<template>
<div class="consultant-list">
<!-- DEVINFO Hier kann die Liste für die Energieberater entfernt werden -->
<iframe loading="lazy" src="https://www.deutsches-energieberaternetzwerk.de/eb_suche/den/index.php" frameborder="0" width="100%"
height="100%"></iframe>
</div>
</template>
<style scoped>
.consultant-list {
width: 100%;
height: 100%;
display: flex;
flex: 1;
border: 0;
}
.consultant-list__label {
font-size: var(--text-s);
}
.consultant-list__value {
font-size: var(--text-s);
}
</style>
\ No newline at end of file
<script setup>
import { useApartmentStore } from '@/stores/apartments'
defineProps(['label'])
const apartments = useApartmentStore()
async function handleFileUpload(event) {
try {
const fileBlob = event.target.files
const formData = new FormData()
formData.append('image', fileBlob[0])
const response = await apartments.uploadFile(apartments.active, formData)
if (response.success) {
await apartments.fetchFile(apartments.active, true)
}
} catch (error) {
console.error('ERROR:', error.name)
}
}
</script>
<template>
<div class="file-upload button primary">
<input type="file" @change="handleFileUpload" />
<div class="file-upload__label">
<span>{{ label }}</span>
</div>
</div>
</template>
<style scoped>
.file-upload {
position: relative;
display: flex;
}
.file-upload>input[type=file] {
position: absolute;
appearance: none;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
.file-upload__label {
pointer-events: none;
}
</style>
\ No newline at end of file
<script setup>
import { ref } from 'vue'
defineProps(['url'])
const isOpen = ref(false)
function show() {
isOpen.value = !isOpen.value
}
</script>
<template>
<div class="image-banner shadow" :class="{ open: isOpen }" @click.prevent="show">
<img :src="url" />
</div>
</template>
<style scoped>
.image-banner {
height: 5em;
display: flex;
border-radius: var(--radius);
border: var(--border) solid hsl(var(--clr-primary));
cursor: pointer;
}
.image-banner>img {
width: 100%;
object-fit: cover;
border-radius: var(--radius);
}
.open {
height: unset;
}
</style>
\ No newline at end of file
<script setup>
const props = defineProps(['min', 'row', 'stretch'])
</script>
<template>
<div class="input-group" :class="{ min: props.min, row: props.row, stretch: props.stretch }">
<slot />
</div>
</template>
<style scoped>
.input-group {
display: flex;
flex-direction: column;
gap: var(--spacing-s);
}
.input-group > label {
font-size: var(--text-s);
font-weight: bold;
}
.input-group.min {
width: min-content;
}
.input-group.row {
flex-direction: column;
}
@media (min-width: 48em) {
.input-group.row {
flex-direction: row;
gap: var(--spacing-l);
}
}
.input-group.stretch {
flex: 1;
}
</style>
\ No newline at end of file
<script setup>
const props = defineProps(['row'])
</script>
<template>
<div class="input-wrapper" :class="{ row: props.row }">
<slot />
</div>
</template>
<style scoped>
.input-wrapper {
display: flex;
flex-direction: column-reverse;
gap: var(--spacing);
}
.input-wrapper.row {
flex-direction: row;
}
@media (min-width: 48em) {
.input-wrapper {
display: flex;
flex-direction: row;
gap: var(--spacing);
}
}
</style>
\ No newline at end of file
<script setup>
const props = defineProps(['type', 'msg'])
</script>
<template>
<div class="message-box" :class="{
basic: props.type === 'basic',
danger: props.type === 'danger',
success: props.type === 'success',
warning: props.type === 'warning',
}">
<span class="message-box__text">{{ props.msg }}</span>
</div>
</template>
<style scoped>
.message-box {
padding: var(--spacing-s);
display: flex;
border-radius: var(--radius);
}
.message-box.danger {
color: var(--clr-white);
}
.message-box__text {
display: flex;
align-items: center;
font-size: var(--text-s);
}
</style>
\ No newline at end of file
<script setup>
defineEmits(['closeModal'])
defineProps(['label', 'permanent'])
</script>
<template>
<div class="modal-container">
<div class="modal-content shadow">
<div v-if="!permanent" class="modal-content__title">
<div class="headline">
<span>{{ label }}</span>
</div>
<button class="button primary" type="button" @click="$emit('closeModal')">
<font-awesome-icon icon="fa-solid fa-xmark" />
</button>
</div>
<slot />
</div>
</div>
</template>
<style scoped>
.modal-container {
overflow: hidden;
position: fixed;
background-color: hsl(var(--clr-background) / 0.8);
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: var(--spacing);
height: auto;
display: flex;
justify-content: center;
align-items: center;
z-index: 1;
}
@media (min-width: 48em) {
}
.modal-content {
overflow-y: auto;
background-color: hsl(var(--clr-content));
width: min(50em, 100%);
height: 100%;
display: flex;
flex-direction: column;
border-radius: var(--radius);
}
@media (min-width: 48em) {
.modal-content {
overflow: hidden;
height: 90%;
}
}
.modal-content__title {
padding: var(--spacing);
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
\ No newline at end of file
<script setup>
import { MESSAGES } from '@/data/messages'
const props = defineProps(['compact', 'msg'])
</script>
<template>
<div class="content pad" :class="{ stretched: !props.compact }">
{{ msg || MESSAGES['NO_DATA_AVAILABLE'] }}
</div>
</template>
\ No newline at end of file
<template>
<div class="page-container">
<slot />
</div>
</template>
<style scoped>
.page-container {
overflow: hidden;
flex: 1;
display: flex;
flex-direction: column;
}
</style>
\ No newline at end of file
<template>
<div class="page-content">
<slot />
</div>
</template>
<style scoped>
.page-content {
overflow: hidden;
background-color: hsl(var(--clr-background));
flex: 1;
display: flex;
}
</style>
\ No newline at end of file
<template>
<div class="page-menu">
<slot />
</div>
</template>
<style scoped>
.page-menu {
display: flex;
}
</style>
\ No newline at end of file
<script setup>
defineProps(['value'])
</script>
<template>
<div class="section-title">
<span>{{ value }}</span>
</div>
</template>
<style scoped>
.section-title {
display: flex;
flex-direction: column;
}
.section-title>span {
font-weight: bold;
}
</style>
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment