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

Version 1.0.0

parent b463e8cc
{
"appId": "com.axcorn.dreiprozentplus",
"appName": "dreiprozent-plus",
"webDir": "dist",
"bundledWebRuntime": false
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>3% Plus - Hochschule für Technik Stutttgart</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
{
"name": "dreiprozent-plus-client",
"version": "1.0.0",
"description": "",
"main": "src/main.js",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview --port 4173",
"test:unit": "vitest --environment jsdom",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
},
"repository": {
"type": "git",
"url": "git+https://github.com/abergavenny/dreiprozent-plus.git"
},
"keywords": [],
"author": "abergavenny",
"license": "MIT",
"bugs": {
"url": "https://github.com/abergavenny/dreiprozent-plus/issues"
},
"homepage": "https://github.com/abergavenny/dreiprozent-plus#readme",
"dependencies": {
"@capacitor/android": "4.0.1",
"@capacitor/cli": "4.0.1",
"@capacitor/core": "4.0.1",
"@fortawesome/fontawesome-svg-core": "6.1.2",
"@fortawesome/free-regular-svg-icons": "6.1.2",
"@fortawesome/free-solid-svg-icons": "6.1.2",
"@fortawesome/vue-fontawesome": "3.0.1",
"@vuelidate/core": "2.0.0-alpha.44",
"@vuelidate/validators": "2.0.0-alpha.31",
"axios": "0.27.2",
"jwt-decode": "3.1.2",
"pinia": "2.0.17",
"vue": "3.2.37",
"vue-router": "4.1.3"
},
"devDependencies": {
"@vitejs/plugin-vue": "^3.0.1",
"@vue/test-utils": "^2.0.2",
"eslint": "^8.5.0",
"eslint-plugin-vue": "^9.0.0",
"jsdom": "^20.0.0",
"vite": "^3.0.4",
"vitest": "^0.21.1"
}
}
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { RouterView } from 'vue-router'
import { useSessionStore } from '@/stores/session'
const session = useSessionStore()
function onConnectionChange(event) {
if (event.type === 'online') {
session.setConnectionState(true)
} else {
session.setConnectionState(false)
}
}
onMounted(() => {
if ('onLine' in navigator) {
session.setConnectionState(navigator.onLine)
window.addEventListener('online', onConnectionChange)
window.addEventListener('offline', onConnectionChange)
}
})
onUnmounted(() => {
window.removeEventListener('online', onConnectionChange)
window.removeEventListener('offline', onConnectionChange)
})
</script>
<template>
<RouterView />
</template>
<style>
#app {
overflow: hidden;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
</style>
\ No newline at end of file
:root {
--clr-background: 0deg 0% 95%;
--clr-background-shade: 0deg 0% 85%;
--clr-content: 0deg 100% 100%;
--clr-content-shade: 0deg 100% 100%;
--clr-shadow: 0deg 100% 0%;
--clr-primary: 4.3deg 69.3% 47.3%;
--clr-primary-shade: 4deg 83% 36%;
--clr-secondary: 0deg 100% 100%;
--clr-secondary-shade: 0deg 0% 95%;
--clr-basic: 0deg 0% 95%;
--clr-critical: 354deg 80% 25%;
--clr-danger: 354deg 70% 54%;
--clr-success: 152deg 69% 31%;
--clr-warning: 45deg 100% 51%;
--clr-black: 0deg 0% 0%;
--clr-white: white;
--clr-text: hsl(var(--clr-black));
--border: 2px;
--border-l: 4px;
--element-size: 3em;
--element-size-s: 1.5em;
--radius: 5px;
--spacing: 1em;
--spacing-l: 2em;
--spacing-s: 0.5em;
--text-l: 1.2em;
--text-s: 0.8em;
--t-background-color: background-color 250ms;
--t-background-image: background-image 250ms;
--grid-layout: 1fr 1fr 2fr;
--shadow: 0 5px 10px 0 hsl(var(--clr-shadow) / 0.1);
}
@media (prefers-color-scheme: dark) {
:root {
--clr-background: 0deg 0% 5%;
--clr-background-shade: 0deg 0% 85%;
--clr-content: 0deg 0% 10%;
--clr-content-shade: 0deg 0% 20%;
--clr-shadow: 0deg 100% 0%;
--clr-background: 0deg 0% 5%;
--clr-background-shade: 0deg 0% 85%;
--clr-content: 0deg 0% 12%;
--clr-primary: 4.3deg 69.3% 47.3%;
--clr-primary-shade: 4deg 83% 36%;
--clr-secondary: 0deg 0% 12%;
--clr-secondary-shade: 0deg 0% 5%;
--clr-basic: 0deg 0% 5%;
--clr-black: 0deg 0% 60%;
--clr-text: hsl(var(--clr-black));
}
}
@import "./base.css";
@keyframes pulse {
0% {
background-color: hsl(var(--clr-primary));
}
50% {
background-color: hsl(var(--clr-primary-shade));
}
100% {
background-color: hsl(var(--clr-primary));
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
position: relative;
font-weight: normal;
user-select: none;
}
html {
height: 100%;
}
body {
overflow: hidden;
display: flex;
height: 100%;
color: var(--clr-text);
background: hsl(var(--clr-background));
transition: color 0.5s, background-color 0.5s;
line-height: 1.6;
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a,
a:active,
a:hover,
a:visited {
color: var(--clr-text);
text-decoration: none;
}
a.button {
color: var(--clr-white);
text-decoration: none;
}
button {
appearance: none;
background-color: transparent;
margin: 0;
padding: 0;
border: 0;
font-size: inherit;
border-radius: var(--radius);
}
form {
height: 100%;
display: flex;
flex-direction: column;
}
input[type="checkbox"] {
position: relative;
appearance: none;
margin: 0;
padding: 0;
min-width: var(--element-size-s);
height: var(--element-size-s);
border: var(--border) solid hsl(var(--clr-primary));
border-radius: var(--radius);
cursor: pointer;
}
input[type="checkbox"]:checked::after {
position: absolute;
margin: 3px;
content: "";
width: 10px;
height: 10px;
background-color: hsl(var(--clr-primary));
font-size: inherit;
border-radius: 3px;
}
input[type="email"],
input[type="number"],
input[type="password"],
input[type="text"] {
appearance: none;
padding: 0.2em 0.5em;
height: var(--element-size);
border: var(--border) solid hsl(var(--clr-primary));
border-radius: var(--radius);
}
input {
background-color: hsl(var(--clr-content-shade));
}
input:disabled {
border: var(--border) solid hsl(var(--clr-background));
}
input:autofill,
input:autofill:focus {
transition: background-color 600000s 0s, color 600000s 0s;
}
@media (prefers-color-scheme: dark) {
input {
color-scheme: dark;
}
}
input.border {
border: var(--border) solid hsl(var(--clr-content));
border-radius: var(--radius);
}
select {
height: var(--element-size);
border: var(--border) solid hsl(var(--clr-primary));
border-radius: var(--radius);
}
select:disabled {
border: var(--border) solid hsl(var(--clr-background));
}
textarea {
padding: 0.5em;
appearance: none;
border: var(--border) solid hsl(var(--clr-primary));
border-radius: var(--radius);
}
@media (max-width: 48em) {
input[type="number"],
input[type="password"],
input[type="text"] {
width: 100%;
}
}
* {
scrollbar-width: thin;
scrollbar-color: hsl(var(--clr-primary)) transparent;
}
*::-webkit-scrollbar {
width: 5px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: hsl(var(--clr-primary));
border-radius: var(--radius);
border: var(--border) solid transparent;
}
.button {
padding-inline: var(--spacing);
min-width: var(--element-size);
height: var(--element-size);
display: flex;
gap: var(--spacing);
justify-content: center;
align-items: center;
border-radius: var(--radius);
cursor: pointer;
transition: var(--t-background-color);
}
.button > span {
white-space: nowrap;
}
.button:not(.primary) {
color: var(--clr-text);
}
.button:hover:not(:disabled) {
background-color: hsl(var(--clr-primary-shade));
}
.button:hover:not(.primary) {
background-color: hsl(var(--clr-secondary-shade));
}
.button:disabled {
background-color: hsl(var(--clr-background));
color: var(--clr-text);
cursor: no-drop;
}
.button-content {
display: none;
}
.button-icon {
display: flex;
justify-content: center;
align-items: center;
}
@media (min-width: 48em) {
.button-content {
display: flex;
}
.button-icon {
padding-left: var(--spacing);
height: 50%;
border-left: var(--border) solid hsl(var(--clr-primary));
}
}
.container {
padding: var(--spacing);
display: flex;
flex-direction: column;
gap: var(--spacing);
}
@media (min-width: 48em) {
.container {
overflow: hidden;
}
}
.content {
overflow: hidden;
padding: var(--spacing);
display: flex;
flex-direction: column;
gap: var(--spacing);
}
@media (min-width: 48em) {
.content.stretched {
flex: 1;
}
}
.content.scroll {
overflow-y: auto;
}
.content-wrapper {
overflow: hidden;
flex: 1;
display: flex;
flex-direction: column;
}
.form {
flex: 1;
display: flex;
flex-direction: column;
}
@media (min-width: 48em) {
.form {
overflow: hidden;
}
}
.form-bg {
background-color: hsl(var(--clr-content));
border-radius: var(--radius);
}
.form-section {
padding: var(--spacing);
display: flex;
flex-direction: column;
gap: var(--spacing);
}
.form-content {
overflow-y: auto;
padding: var(--spacing);
flex: 1;
display: flex;
flex-direction: column;
gap: var(--spacing);
}
.headline {
display: flex;
}
.headline > span {
font-size: var(--text-l);
font-weight: bold;
}
.info-box {
display: flex;
flex-direction: column;
gap: 0.5em;
}
.input-group > label {
font-size: var(--text-s);
font-weight: bold;
}
.legal {
background-color: hsl(var(--clr-content));
overflow-y: auto;
padding: var(--spacing);
width: min(100%, 48em);
display: flex;
flex-direction: column;
gap: var(--spacing);
border-radius: 0;
}
@media (min-width: 48em) {
.legal {
margin: 2em auto;
padding: 1em;
border-radius: var(--radius);
}
}
.legal-button {
flex: 1;
display: flex;
flex-direction: column;
}
.link {
text-decoration: underline;
}
.number {
width: 7em;
}
.pad {
padding: var(--spacing);
}
.section.center {
padding: var(--spacing);
display: flex;
justify-content: center;
}
.shadow {
box-shadow: var(--shadow);
}
.static-text {
font-size: var(--text-s);
color: hsl(var(--clr-background-shade));
}
.primary,
.secondary,
.danger,
.success,
.warning {
color: var(--clr-white);
}
.primary {
background-color: hsl(var(--clr-primary));
}
.secondary {
background-color: hsl(var(--clr-secondary));
}
.basic {
background-color: hsl(var(--clr-basic));
}
.danger {
background-color: hsl(var(--clr-danger));
}
.success {
background-color: hsl(var(--clr-success));
}
.warning {
background-color: hsl(var(--clr-warning));
color: var(--clr-text);
}
<script setup>
import { ref } from 'vue'
import TabComponent from '@/components/TabComponent.vue'
import ApartmentHeatingForm from '@/components/forms/ApartmentHeatingForm.vue'
import WindowForm from '@/components/forms/WindowForm.vue'
const activeTab = ref('windows')
function setActiveTab(value) {
activeTab.value = value
}
</script>
<template>
<div class="apartment-capture">
<TabComponent :active="activeTab" :tabs="[
{ key: 'windows', value: 'windows', name: 'Fenster' },
{ key: 'apartment', value: 'apartment', name: 'Wohnung' }
]" @on-tab-select="setActiveTab" />
<div class="apartment-capture__content">
<WindowForm v-if="activeTab === 'windows'" />
<ApartmentHeatingForm v-else-if="activeTab === 'apartment'" />
</div>
</div>
</template>
<style scoped>
.apartment-capture {
display: flex;
flex-direction: column;
}
@media (min-width: 48em) {
.apartment-capture {
overflow: hidden;
height: 100%;
}
}
.apartment-capture__content {
overflow: hidden;
flex: 1;
display: flex;
flex-direction: column;
}
</style>
\ No newline at end of file
<script setup>
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { calculateHeaterEfficiency, calculateWindowEfficiency } from '@/helpers'
import { useApartmentStore } from '@/stores/apartments'
const props = defineProps(['data'])
const router = useRouter()
const apartments = useApartmentStore()
function onSelect() {
apartments.setActiveApartment(props.data._id)
router.push({ name: 'buildings.dashboard.apartment' })
}
function convertHeaterEfficiencyColor(apartment) {
if (apartment) {
const { largeHeatingElements, mediumHeatingElements, smallHeatingElements, workingHeatingInstallations } = apartment.data
let sum = largeHeatingElements + mediumHeatingElements + smallHeatingElements
if (sum === 0) return '--icon-color: var(--clr-basic)'
const [performance] = calculateHeaterEfficiency(sum, workingHeatingInstallations)
if (performance < 0.75) return '--icon-color: var(--clr-critical)'
else if (performance < 0.80) return '--icon-color: var(--clr-danger)'
else if (performance < 0.95) return '--icon-color: var(--clr-warning)'
else if (performance <= 1) return '--icon-color: var(--clr-success)'
}
return '--icon-color: var(--clr-basic)'
}
function convertWindowEfficiencyColor(apartment) {
if (apartment) {
const { averageWindowAge, averageWindowCondition, windowGlazing } = apartment.data
const [performance] = calculateWindowEfficiency(averageWindowAge, averageWindowCondition, windowGlazing)
if (performance < 1.5) return '--icon-color: var(--clr-critical)'
else if (performance < 2.5) return '--icon-color: var(--clr-danger)'
else if (performance < 3.5) return '--icon-color: var(--clr-warning)'
else if (performance >= 3.5) return '--icon-color: var(--clr-success)'
}
return '--icon-color: var(--clr-basic)'
}
onMounted(async () => {
await apartments.fetchFile(props.data._id)
})
</script>
<template>
<div class="apartment-card" @click="onSelect">
<div class="apartment-card__wrapper">
<div class="apartment-card__headline">
<span class="static-text">Id: {{ props.data._id }}</span>
</div>
<div class="apartment-card__content">
<span>{{ props.data.name }}</span>
<span class="text"><i class="apartment-card__icon"
:style="convertWindowEfficiencyColor(props.data)"></i>Fenster</span>
<span class="text"><i class="apartment-card__icon"
:style="convertHeaterEfficiencyColor(props.data)"></i>Heizkörper</span>
</div>
</div>
</div>
</template>
<style scoped>
.apartment-card {
position: relative;
background-color: hsl(var(--clr-primary));
display: flex;
flex-direction: column;
border-radius: var(--radius);
color: var(--clr-white);
cursor: pointer;
}
.apartment-card:hover {
background-color: hsl(var(--clr-primary-shade));
box-shadow: var(--shadow);
transition: background-color ease-in 150ms;
}
.apartment-card__wrapper {
padding: var(--spacing-s);
display: flex;
flex-direction: column;
}
.apartment-card__content {
display: flex;
flex-direction: column;
}
.apartment-card__content>span:first-of-type {
font-weight: bold;
}
.apartment-card__content>span:not(:first-of-type) {
display: flex;
align-items: center;
font-size: var(--text-s);
}
.apartment-card__headline {
display: flex;
}
.apartment-card__icon {
background-color: hsl(var(--icon-color));
margin-right: var(--spacing);
width: 2em;
height: 1em;
display: flex;
border-radius: var(--radius);
border: var(--border) solid hsl(var(--clr-content))
}
</style>
\ No newline at end of file
<script setup>
import ApartmentData from '@/components/ApartmentData.vue'
</script>
<template>
<div class="apartment-dashboard">
<ApartmentData />
</div>
</template>
<style scoped>
.apartment-dashboard {
overflow-y: auto;
flex: 1;
display: grid;
grid-template-columns: 1fr;
grid-auto-rows: min-content;
gap: var(--spacing);
}
</style>
\ No newline at end of file
<script setup>
import { ref } from 'vue'
import ApartmentCapture from '@/components/ApartmentCapture.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 ImageBanner from '@/components/ImageBanner.vue'
import MessageBox from '@/components/MessageBox.vue'
import ModalContainer from '@/components/ModalContainer.vue'
import SectionTitle from '@/components/SectionTitle.vue'
import WaitingForData from '@/components/WaitingForData.vue'
import { REFERENCE_ENERGY_PERFORMANCE } from '@/data/parameters'
import {
convertHeaterEfficiency,
convertWindowEfficiency
} from '@/helpers'
import { useApartmentStore } from '@/stores/apartments'
import { useBuildingStore } from '@/stores/buildings'
const showModal = ref(false)
const apartments = useApartmentStore()
const buildings = useBuildingStore()
function onEdit() {
showModal.value = true
}
// 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 `Das Verbesserungspotential Ihrer Energieeffizienz liegt bei ${potential}%.`
else return null
}
// DEVINFO Hier kann der Bewertungstext (Verglasung) angepasst werden
function convertWindowGlazingMessage(value) {
if (value === 'SIMPLE_GLAZING') return `Schlechte Verglasung (U-Wert ca. 4,7 W/(m²K)). Einsparung von 491 kWh sind möglich (Einsparung je ausgetauschtes Fenster ca. 158€ jährlich ). Gebäude-Energie-Gesetz (GEG) schreibt bei Austausch einen Wert von 1,3 W/(m²K) vor.`
else if (value === 'DOUBLE_GLAZING') return `Mittelmäßige Verglasung (U-Wert 1,4 bis 3 W/m²K). Empfehlung: Fenster mit zweifacher, unbeschichteter Isolierverglasung (vor 1995 verbaut) gegen dreifach verglaste, wärmegedämmte Fenster austauschen. Der Austausch von zweifachem, unbeschichtetem Isolierglas spart 216 kWh. (Einsparung je ausgetauschtes Fenster ca. 70€ jährlich). Bei neueren Fenstern ist individuell zu entscheiden. Gebäude-Energie-Gesetz (GEG) schreibt bei Austausch einen Wert von 1,3 W/(m²K) vor.`
else if (value === 'TRIPLE_GLAZING') return `3-fach Verglasung vorhanden und damit keine wesentlich Verbesserung möglich. Austausch wirtschaftlich nicht sinnvoll.`
else return null
}
</script>
<template>
<ContentPanel label="Ihre Wohneinheit">
<DataPage :stretch="true">
<div class="content-wrapper">
<div class="content stretched scroll">
<span class="static-text">Id: {{ apartments.active }}</span>
<SectionTitle value="Energieeffizienz" />
<div class="info-box" v-if="apartments.heatingEfficiency">
<DataItem :label="`Effizienzklasse (${apartments.heatingEfficiency.distict ? 'Wohnung' : 'Gebäude'})`"
:data="`${apartments.heatingEfficiency.performance} kWh/m²`" />
<MessageBox
:msg="convertHeatingEfficiencyMessage(apartments.heatingEfficiency.performance, apartments.heatingEfficiency.potential)"
type="basic" />
<EnergyPerformanceCertificate :data="apartments.heatingEfficiency.performance" />
</div>
<WaitingForData v-else />
<SectionTitle value="Energieeffizienz - Fenster" />
<div class="info-box" v-if="apartments.windowEfficiency">
<DataItem label="Energetische Berwertung Fenster"
:data="convertWindowEfficiency(apartments.windowEfficiency.performance)" />
<MessageBox :msg="convertWindowGlazingMessage(apartments.current?.data.windowGlazing)" type="basic" />
</div>
<WaitingForData v-else />
<SectionTitle value="Energieeffizienz - Heizkörper" />
<div class="info-box" v-if="apartments.heaterEfficiency">
<DataItem label="Bewertung Heizkörper" :data="convertHeaterEfficiency(apartments.heaterEfficiency.delta)" />
<MessageBox
msg="Durch regelmäßiges Warten und Entlüften können Sie die Effizienz Ihrer Heizkörper steigern."
type="basic" />
</div>
<WaitingForData v-else />
<SectionTitle value="Emissionen Heizung" />
<div class="info-box"
v-if="apartments.current?.data.centralHeating ? buildings.co2Efficiency : apartments.co2Efficiency">
<DataItem label="CO2 Emissionen"
:data="apartments.current?.data.centralHeating ? buildings.co2Efficiency.performance : apartments.co2Efficiency.performance"
unit="kg" />
<DataItem label="Entspricht Fahrstrecke"
:data="apartments.current?.data.centralHeating ? buildings.co2Efficiency.proportion : apartments.co2Efficiency.proportion"
unit="km" />
</div>
<WaitingForData v-else />
<SectionTitle value="Gebäudedaten" />
<ImageBanner v-if="apartments.files[apartments.active]" :url="apartments.files[apartments.active]" />
<DataItem label="Baujahr" :data="buildings.single?.data.yearOfConstruction" :as-text="true" />
<DataItem label="Stockwerke" :data="buildings.single?.data.numberOfFloors" />
<DataItem label="Energieklasse" :data="buildings.single?.data.energyPerformanceCertificate" unit="kWh/m²" />
<DataItem v-if="!buildings.single?.data.selfContainedCentralHeating" label="Heizung"
:data="buildings.heatingInstallationString" />
<DataItem v-if="!buildings.single?.data.selfContainedCentralHeating" label="Wärmebedarf Gebäude Vorjahr"
:data="buildings.consumption" unit="kWh" />
<DataItem v-if="!buildings.single?.data.photovoltaic" label="PV Ertrag"
:data="buildings.single?.data.photovoltaicYield" unit="kWh/m²" />
<SectionTitle value="Fenster" />
<DataItem label="Klein" :data="apartments.current?.data.smallWindows" />
<DataItem label="Normal" :data="apartments.current?.data.mediumWindows" />
<DataItem label="Groß" :data="apartments.current?.data.largeWindows" />
<DataItem label="Dach" :data="apartments.current?.data.roofWindows" />
<DataItem label="Rahmen" :data="apartments.windowFrameString" />
<DataItem label="Verglasung" :data="apartments.windowGlazingString" />
<DataItem label="Zustand (Durchschnitt)" :data="apartments.windowConditionString" />
<DataItem label="Alter (Durchschnitt)" :data="apartments.current?.data.averageWindowAge" />
<MessageBox v-if="apartments.current?.data.windowComment" :msg="apartments.current?.data.windowComment"
type="basic" />
<SectionTitle value="Heizung" />
<DataItem label="Wohnfläche" :data="apartments.current?.data.area" unit="m²" />
<DataItem label="Deckenhöhe" :data="apartments.current?.data.ceilingHeight" unit="m" />
<DataItem label="Heizkörper (klein)" :data="apartments.current?.data.smallHeatingElements" />
<DataItem label="Heizkörper (normal)" :data="apartments.current?.data.mediumHeatingElements" />
<DataItem label="Heizkörper (groß)" :data="apartments.current?.data.largeHeatingElements" />
<DataItem v-if="apartments.current?.data.panelHeating" label="Flächenheizung"
:data="apartments.current?.data.panelHeating" unit="m²" />
<DataItem v-if="apartments.current?.data.centralHeating" label="Zentralheizung"
:data="apartments.current?.data.centralHeating ? 'ja' : 'nein'" />
<DataItem v-if="!apartments.current?.data.centralHeating" label="Wärmebedarf"
:data="apartments.current?.data.heatDemand" unit="kWh" />
<DataItem label="Strombedarf" :data="apartments.current?.data.powerDemand" unit="kWh" />
<MessageBox v-if="apartments.current?.data.apartmentComment" :msg="apartments.current?.data.apartmentComment"
type="basic" />
</div>
<div class="content">
<button class="button primary" type="button" @click="onEdit">
<span>Daten eingeben</span>
</button>
</div>
</div>
</DataPage>
</ContentPanel>
<ModalContainer v-if="showModal" label="Wohnungsdaten eingeben" @close-modal="() => showModal = false">
<ApartmentCapture />
</ModalContainer>
</template>
\ No newline at end of file
<script setup>
import ApartmentCard from '@/components/ApartmentCard.vue'
import NoData from '@/components/NoData.vue'
import { useApartmentStore } from '@/stores/apartments'
import { useBuildingStore } from '@/stores/buildings'
defineEmits(['onCreate'])
const apartments = useApartmentStore()
const buildings = useBuildingStore()
</script>
<template>
<div class="apartment-list">
<div class="apartment-list__grid" v-if="buildings.active && apartments.ofBuilding.length > 0">
<ApartmentCard v-for="apartment in apartments.ofBuilding" :key="apartment._id" :data="apartment" />
</div>
<NoData v-else />
<div class="content">
<button class="button primary" type="button" @click="$emit('onCreate')" :disabled="!buildings.active">
<span>Wohneinheit anlegen</span>
</button>
</div>
</div>
</template>
<style scoped>
.apartment-list {
overflow: hidden;
flex: 1;
display: flex;
flex-direction: column;
}
.apartment-list__grid {
overflow-y: auto;
padding: var(--spacing);
flex: 1;
display: grid;
grid-template-columns: repeat(1, 1fr);
grid-auto-rows: min-content;
gap: var(--spacing);
}
@media (min-width: 48em) {
.apartment-list__grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
\ No newline at end of file
<script setup>
import { ref } from 'vue'
import { ROLE_OPTIONS } from '@/data/options'
import { useBuildingStore } from '@/stores/buildings'
import { useSessionStore } from '@/stores/session'
const openMenu = ref(false)
const buildings = useBuildingStore()
const session = useSessionStore()
function toggleMenu() {
openMenu.value = !openMenu.value
}
function convertRole(value) {
const role = ROLE_OPTIONS.find(element => element.value === value)
return role ? role.name : null
}
</script>
<template>
<div class="app-bar">
<div class="app-bar__left">
<slot />
</div>
<div class="app-bar__right">
<div class="app-bar__role bar">Sie sind angemeldet als {{ convertRole(session.currentUser?.role) }}.</div>
<router-link class="button secondary" to="/">
<font-awesome-icon icon="fa-solid fa-arrow-right-from-bracket" />
</router-link>
</div>
<div class="app-bar__mobile">
<button class="button secondary" type="button" @click="toggleMenu">
<font-awesome-icon v-if="!openMenu" icon="fa-solid fa-bars" />
<font-awesome-icon v-else icon="fa-solid fa-xmark" />
</button>
<div class="app-bar__dropdown shadow" v-if="openMenu">
<div v-if="session.currentUser?.role === 'user'">
<span class="static-text">{{ buildings.single.name }}</span>
</div>
<div class="app-bar__role">Sie sind angemeldet als {{ convertRole(session.currentUser?.role) }}.</div>
<router-link class="button primary" to="/">
<font-awesome-icon icon="fa-solid fa-arrow-right-from-bracket" />
</router-link>
</div>
</div>
</div>
</template>
<style scoped>
.app-bar {
position: relative;
background-color: hsl(var(--clr-primary));
padding: var(--spacing);
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
}
@media (min-width: 48em) {
.app-bar {
flex-direction: row;
}
}
.app-bar__role {
display: flex;
}
.app-bar__role.bar {
color: var(--clr-white);
}
.app-bar__left,
.app-bar__right {
height: var(--element-size);
display: flex;
gap: var(--spacing);
align-items: center;
}
.app-bar__right {
display: none;
}
.app-bar__mobile {
position: relative;
display: flex;
}
@media (min-width: 48em) {
.app-bar__mobile {
display: none;
}
.app-bar__right {
display: flex;
}
}
.app-bar__dropdown {
background-color: hsl(var(--clr-content));
z-index: 1;
position: fixed;
top: calc(var(--element-size) + var(--spacing) * 2);
left: 0;
right: 0;
padding: var(--spacing);
display: flex;
flex-direction: column;
gap: var(--spacing);
border-bottom: var(--border) solid hsl(var(--clr-primary));
}
</style>
\ No newline at end of file
<template>
<div class="brand">
<!-- DEVINFO Text oder Logo einfügen -->
<span>Drei Prozent Plus</span>
</div>
</template>
<style scoped>
.brand {
min-height: calc(var(--element-size) * 2);
display: flex;
justify-content: center;
align-items: center;
color: var(--clr-text);
}
@media (min-width: 48em) {
.brand {
margin-top: var(--element-size);
}
}
.brand>span {
font-size: var(--text-l);
font-weight: bold;
}
</style>
\ No newline at end of file
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import ContentPanel from '@/components/ContentPanel.vue'
import DataItem from '@/components/DataItem.vue'
import DataPage from '@/components/DataPage.vue'
import FileUpload from '@/components/FileUpload.vue'
import ImageBanner from '@/components/ImageBanner.vue'
import ModalContainer from '@/components/ModalContainer.vue'
import SectionTitle from '@/components/SectionTitle.vue'
import UserBox from '@/components/UserBox.vue'
import ChangePasswordForm from '@/components/forms/ChangePasswordForm.vue'
import {
convertHeaterEfficiency,
convertWindowEfficiency
} from '@/helpers'
import { useApartmentStore } from '@/stores/apartments'
const showModal = ref(false)
const router = useRouter()
const apartments = useApartmentStore()
function handleClose() {
router.push({ name: 'buildings.dashboard.home' })
}
</script>
<template>
<ContentPanel label="Wohneinheit">
<DataPage :stretch="true">
<div class="content-wrapper">
<div class="content stretched scroll">
<ImageBanner v-if="apartments.files[apartments.active]" :url="apartments.files[apartments.active]" />
<span class="static-text">Id: {{ apartments.active }}</span>
<SectionTitle value="Energieeffizienz" />
<div class="info-box" v-if="apartments.windowEfficiency">
<DataItem label="Energetische Berwertung Fenster"
:data="convertWindowEfficiency(apartments.windowEfficiency.performance)" />
</div>
<div class="info-box" v-if="apartments.heaterEfficiency">
<DataItem label="Bewertung Heizkörper" :data="convertHeaterEfficiency(apartments.heaterEfficiency.delta)" />
</div>
<SectionTitle value="Wohnungsdaten" />
<DataItem label="Wohnfläche" :data="apartments.current?.data.area" unit="m²" />
<DataItem label="Deckenhöhe" :data="apartments.current?.data.ceilingHeight" unit="m" />
<SectionTitle value="Fenster" />
<DataItem label="Klein (bis 1 m²)" :data="apartments.current?.data.smallWindows" />
<DataItem label="Normal (1-2 m²)" :data="apartments.current?.data.mediumWindows" />
<DataItem label="Groß (ab 2 m²)" :data="apartments.current?.data.largeWindows" />
<DataItem label="Dachfenster" :data="apartments.current?.data.roofWindows" />
<DataItem label="Zustand (Durchschnitt)" :data="apartments.windowConditionString" />
<DataItem label="Alter (Durchschnitt)" :data="apartments.current?.data.averageWindowAge" />
<DataItem label="Rahmen" :data="apartments.windowFrameString" />
<DataItem label="Verglasung" :data="apartments.windowGlazingString" />
<SectionTitle value="Heizkörper" />
<DataItem label="Klein" :data="apartments.current?.data.smallHeatingElements" />
<DataItem label="Normal" :data="apartments.current?.data.mediumHeatingElements" />
<DataItem label="Groß" :data="apartments.current?.data.largeHeatingElements" />
<DataItem label="Flächenheizung" :data="apartments.current?.data.panelHeating" unit="m²" />
<DataItem v-if="apartments.current?.data.centralHeating" label="Zentralheizung"
:data="apartments.current?.data.centralHeating" />
<DataItem v-if="!apartments.current?.data.centralHeating" label="Wärmebedarf"
:data="apartments.current?.data.heatDemand" unit="kWh" />
<DataItem label="Strombedarf Vorjahr" :data="apartments.current?.data.powerDemand" unit="kWh" />
</div>
<div class="content">
<FileUpload label="Grundriss hochladen" />
<UserBox @on-change-password="() => showModal = true" />
</div>
<div class="content">
<button class="button primary" type="button" @click="handleClose">
<span>Zurück</span>
</button>
</div>
</div>
</DataPage>
</ContentPanel>
<ModalContainer v-if="showModal" label="Zugangsdaten - Wohneinheit" @close-modal="() => showModal = false">
<ChangePasswordForm @close-modal="() => showModal = false" />
</ModalContainer>
</template>
\ No newline at end of file
<script setup>
import { ref } from 'vue'
import ApartmentList from '@/components/ApartmentList.vue'
import ContentPanel from '@/components/ContentPanel.vue'
import DataItem from '@/components/DataItem.vue'
import DataPage from '@/components/DataPage.vue'
import ModalContainer from '@/components/ModalContainer.vue'
import NoData from '@/components/NoData.vue'
import ApartmentForm from '@/components/forms/ApartmentForm.vue'
import { useApartmentStore } from '@/stores/apartments'
import { useBuildingStore } from '@/stores/buildings'
const showModal = ref(false)
const buildings = useBuildingStore()
const apartments = useApartmentStore()
function handleModal() {
showModal.value = !showModal.value
}
</script>
<template>
<ContentPanel label="Ihre Wohneinheiten">
<DataPage>
<div class="apartment-summary" v-if="buildings.active && apartments.count > 0">
<div class="content stretched">
<DataItem label="Wohneinheiten" :data="apartments.summary?.apartments" />
<DataItem label="Fläche gesamt" :data="apartments.summary?.area" unit="m²" />
<DataItem label="Fenster gesamt" :data="apartments.summary?.windows" />
</div>
<div class="content stretched">
<DataItem label="Heizkörper" :data="apartments.summary?.heatingElements" />
<DataItem label="Flächenheizung" :data="apartments.summary?.panelHeating" unit="m²" />
</div>
</div>
<NoData v-else />
</DataPage>
<DataPage :stretch="true">
<ApartmentList @on-create="() => showModal = true" />
</DataPage>
</ContentPanel>
<ModalContainer v-if="showModal" label="Wohneinheit anlegen" @close-modal="handleModal">
<ApartmentForm @close-modal="handleModal" />
</ModalContainer>
</template>
<style scoped>
.apartment-summary {
height: 100%;
display: flex;
flex-direction: column;
}
@media (min-width: 48em) {
.apartment-summary {
flex-direction: row;
gap: var(--spacing);
}
}
</style>
\ No newline at end of file
<script setup>
import { ref } from 'vue'
import TabComponent from '@/components/TabComponent.vue'
import CharacteristicsForm from '@/components/forms/CharacteristicsForm.vue'
import HeatingForm from '@/components/forms/HeatingForm.vue'
import FacadeForm from '@/components/forms/FacadeForm.vue'
import RoofForm from '@/components/forms/RoofForm.vue'
import BasementForm from '@/components/forms/BasementForm.vue'
const activeTab = ref('characteristics')
function setActiveTab(value) {
activeTab.value = value
}
</script>
<template>
<div class="building-capture">
<TabComponent :active="activeTab" :tabs="[
{ key: 'characteristics', value: 'characteristics', name: 'Allgemein' },
{ key: 'heating', value: 'heating', name: 'Heizung' },
{ key: 'facade', value: 'facade', name: 'Fassade' },
{ key: 'roof', value: 'roof', name: 'Dach' },
{ key: 'basement', value: 'basement', name: 'Keller' }
]" @on-tab-select="setActiveTab" />
<div class="building-capture__content">
<CharacteristicsForm v-if="activeTab === 'characteristics'" />
<HeatingForm v-else-if="activeTab === 'heating'" />
<FacadeForm v-else-if="activeTab === 'facade'" />
<RoofForm v-else-if="activeTab === 'roof'" />
<BasementForm v-else-if="activeTab === 'basement'" />
</div>
</div>
</template>
<style scoped>
.building-capture {
display: flex;
flex-direction: column;
}
@media (min-width: 48em) {
.building-capture {
overflow: hidden;
height: 100%;
}
}
.building-capture__content {
overflow: hidden;
flex: 1;
display: flex;
flex-direction: column;
}
</style>
\ No newline at end of file
<script setup>
import BuildingData from '@/components/BuildingData.vue'
import BuildingEvaluation from '@/components/BuildingEvaluation.vue'
</script>
<template>
<div class="building-dashboard">
<BuildingEvaluation />
<BuildingData />
<router-view></router-view>
</div>
</template>
<style scoped>
.building-dashboard {
overflow-y: auto;
flex: 1;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: var(--grid-layout);
}
@media (min-width: 48em) {
.building-dashboard {
overflow: hidden;
padding: var(--spacing);
grid-template-columns: var(--grid-layout);
grid-template-rows: 1fr;
}
}
</style>
\ No newline at end of file
<script setup>
import { ref } from 'vue'
import BuildingCapture from '@/components/BuildingCapture.vue'
import ContentPanel from '@/components/ContentPanel.vue'
import DataItem from '@/components/DataItem.vue'
import DataPage from '@/components/DataPage.vue'
import MessageBox from '@/components/MessageBox.vue'
import ModalContainer from '@/components/ModalContainer.vue'
import NoData from '@/components/NoData.vue'
import { useBuildingStore } from '@/stores/buildings'
const showModal = ref(false)
const buildings = useBuildingStore()
function onEdit() {
showModal.value = true
}
</script>
<template>
<ContentPanel label="Ihr Gebäude">
<DataPage :stretch="true">
<div class="content-wrapper">
<div class="content stretched scroll" v-if="buildings.current">
<span class="static-text">Id: {{ buildings.current?._id }}</span>
<DataItem label="Baujahr" :data="buildings.current?.data.yearOfConstruction" :as-text="true" />
<DataItem label="Stockwerke" :data="buildings.current?.data.numberOfFloors" />
<DataItem label="Fläche" :data="buildings.current?.data.livingSpace" unit="m²" />
<DataItem label="Denkmal geschützt" :data="buildings.current?.data.listedBuilding ? 'ja' : 'nein'" />
<DataItem label="Energieklasse" :data="buildings.current?.data.energyPerformanceCertificate" unit="kWh/m²" />
<MessageBox v-if="buildings.current?.data.characteristicsComment"
:msg="buildings.current?.data.characteristicsComment" type="basic" />
<DataItem v-if="buildings.current?.data.selfContainedCentralHeating" label="Etagenheizung"
:data="buildings.current?.data.selfContainedCentralHeating ? 'ja' : 'nein'" />
<DataItem v-if="!buildings.current?.data.selfContainedCentralHeating" label="Heizsystem"
:data="buildings.heatingInstallationString" />
<DataItem label="Wärmebedarf Gebäude Vorjahr" :data="buildings.consumption" unit="kWh" />
<DataItem v-if="buildings.current?.data.pipeSystem" label="1-Rohrsystem"
:data="buildings.current?.data.pipeSystem ? 'ja' : 'nein'" />
<DataItem v-if="!buildings.current?.data.solarHeat" label="Solarthermie vorhanden" data="nein" />
<DataItem v-else label="Größe Solarthermie" :data="buildings.current?.data.solarHeatArea" unit="m²" />
<template v-if="buildings.current?.data.photovoltaic">
<DataItem label="Größe PV" :data="buildings.current?.data.photovoltaicArea" unit="m²" />
<DataItem label="Ertag PV" :data="buildings.current?.data.photovoltaicYield" unit="m²" />
</template>
<MessageBox v-if="buildings.current?.data.heatingInstallationComment"
:msg="buildings.current?.data.heatingInstallationComment" type="basic" />
<DataItem label="Fassadendämmung" :data="buildings.facadeInsulationString" />
<DataItem label="Baustruktur" :data="buildings.buildingStructureString" />
<DataItem label="Mauerdicke" :data="buildings.current?.data.buildingStructureThickness" unit="cm" />
<DataItem label="Seiten (N/W/S/O)" :data="buildings.insulatedSides" />
<MessageBox v-if="buildings.current?.data.facadeRefurbishmentComment"
:msg="buildings.current?.data.facadeRefurbishmentComment" type="basic" />
<DataItem v-if="buildings.current?.data.flatRoof" label="Flachdach" data="ja" />
<DataItem v-if="buildings.current?.data.heatedAttic" label="Verschattung" data="ja" />
<DataItem label="Verschattung" :data="buildings.current?.data.clouding" />
<DataItem label="Dachdämmung" :data="buildings.roofInsulationString" />
<MessageBox v-if="buildings.current?.data.roofRefurbishmentComment"
:msg="buildings.current?.data.roofRefurbishmentComment" type="basic" />
<DataItem label="Kellerdämmung" :data="buildings.basementInsulationString" />
<DataItem v-if="buildings.current?.data.insulatedBasementCeiling" label="Kellerdecke gedämmt" data="ja" />
<DataItem v-if="buildings.current?.data.insulatedBasementFloor" label="Kellerboden gedämmt" data="ja" />
<DataItem v-if="buildings.current?.data.heatedBasement" label="Keller beheizt" data="ja" />
<MessageBox v-if="buildings.current?.data.basementRefurbishmentComment"
:msg="buildings.current?.data.basementRefurbishmentComment" type="basic" />
</div>
<NoData v-else />
<div class="content">
<button class="button primary" type="button" @click="onEdit" :disabled="buildings.active === null">
<span>Daten eingeben</span>
</button>
</div>
</div>
</DataPage>
</ContentPanel>
<ModalContainer v-if="showModal" label="Gebäudedaten eingeben" @close-modal="() => showModal = false">
<BuildingCapture />
</ModalContainer>
</template>
\ 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