222 lines
6 KiB
Vue
222 lines
6 KiB
Vue
<template>
|
|
<div data-testid="plan-strategy" class="mb-5 mb-lg-4">
|
|
<div :class="{ 'mb-2': open }">
|
|
<div v-if="disabled">
|
|
<span class="fw-bold evcc-gray">{{ $t("main.chargingPlan.strategy.label") }}:</span>
|
|
{{ " " }}
|
|
<span class="small">{{ summary }}</span>
|
|
</div>
|
|
<button
|
|
v-else
|
|
type="button"
|
|
class="btn btn-link btn-sm text-gray p-0 border-0 d-flex align-items-center text-start"
|
|
:class="{ 'text-primary': open }"
|
|
@click="open = !open"
|
|
>
|
|
<span class="fw-bold">
|
|
{{ $t("main.chargingPlan.strategy.label") }}<span v-if="!open">:</span>
|
|
</span>
|
|
<span v-if="!open" class="ms-1">{{ summary }}</span>
|
|
<DropdownIcon class="icon flex-shrink-0" :class="{ iconUp: open }" />
|
|
</button>
|
|
</div>
|
|
<div class="collapsible-wrapper" :class="{ open }">
|
|
<div class="collapsible-content ring-space">
|
|
<div v-if="disabled" class="row mb-4">
|
|
<div class="small text-muted">
|
|
<strong class="text-primary">{{ $t("general.note") }}</strong>
|
|
{{ $t("main.chargingPlan.strategyDisabledDescription") }}
|
|
</div>
|
|
</div>
|
|
<div v-else class="row">
|
|
<div class="col-12 col-lg-6 mb-3">
|
|
<div class="row">
|
|
<label :for="formId('continuous')" class="col-form-label col-5 col-lg-12">
|
|
{{ $t("main.chargingPlan.optimization.label") }}
|
|
</label>
|
|
<div class="col-7 col-lg-12">
|
|
<select
|
|
:id="formId('continuous')"
|
|
v-model="localContinuous"
|
|
class="form-select"
|
|
@change="updateStrategy"
|
|
>
|
|
<option :value="false">
|
|
{{ $t(`main.chargingPlan.optimization.${cheapestKey}`) }}
|
|
</option>
|
|
<option :value="true">
|
|
{{ $t("main.chargingPlan.optimization.continuous") }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="small text-muted mt-1">
|
|
{{ optimizationDescription }}
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-lg-6 mb-3">
|
|
<div class="row">
|
|
<label :for="formId('precondition')" class="col-form-label col-5 col-lg-12">
|
|
{{ $t("main.chargingPlan.precondition.label") }}
|
|
</label>
|
|
<div class="col-7 col-lg-12">
|
|
<select
|
|
:id="formId('precondition')"
|
|
v-model="localPrecondition"
|
|
class="form-select"
|
|
@change="updateStrategy"
|
|
>
|
|
<option :value="0">
|
|
{{ $t("main.chargingPlan.precondition.optionNo") }}
|
|
</option>
|
|
<option
|
|
v-for="opt in preconditionOptions"
|
|
:key="opt.value"
|
|
:value="opt.value"
|
|
>
|
|
{{ opt.name }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="small text-muted mt-1">
|
|
{{ $t("main.chargingPlan.precondition.description") }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { defineComponent, type PropType } from "vue";
|
|
import formatter from "@/mixins/formatter";
|
|
import { SMART_COST_TYPE, type PlanStrategy } from "@/types/evcc";
|
|
import DropdownIcon from "../MaterialIcon/Dropdown.vue";
|
|
|
|
export default defineComponent({
|
|
name: "ChargingPlanStrategy",
|
|
components: { DropdownIcon },
|
|
mixins: [formatter],
|
|
props: {
|
|
id: [String, Number],
|
|
precondition: { type: Number, default: 0 },
|
|
continuous: { type: Boolean, default: false },
|
|
disabled: Boolean,
|
|
smartCostType: String as PropType<SMART_COST_TYPE>,
|
|
},
|
|
emits: ["update"],
|
|
data() {
|
|
return {
|
|
open: false,
|
|
localPrecondition: this.precondition,
|
|
localContinuous: this.continuous,
|
|
};
|
|
},
|
|
computed: {
|
|
isCo2(): boolean {
|
|
return this.smartCostType === SMART_COST_TYPE.CO2;
|
|
},
|
|
cheapestKey(): string {
|
|
return this.isCo2 ? "cleanest" : "cheapest";
|
|
},
|
|
summary(): string {
|
|
if (this.disabled) {
|
|
return this.$t("main.chargingPlan.strategyDisabledDescription");
|
|
}
|
|
const parts = [
|
|
this.$t(
|
|
`main.chargingPlan.optimization.${this.continuous ? "continuous" : this.cheapestKey}`
|
|
),
|
|
];
|
|
if (this.precondition) {
|
|
parts.push(
|
|
this.$t("main.chargingPlan.precondition.summary", {
|
|
precondition: this.fmtDurationLong(this.precondition),
|
|
})
|
|
);
|
|
}
|
|
return parts.join(", ");
|
|
},
|
|
optimizationDescription(): string {
|
|
const variant = this.localContinuous
|
|
? this.isCo2
|
|
? "continuousCo2"
|
|
: "continuousPrice"
|
|
: this.cheapestKey;
|
|
return this.$t(`main.chargingPlan.optimization.${variant}Description`);
|
|
},
|
|
preconditionOptions() {
|
|
const HOUR = 60 * 60;
|
|
const QUARTER_HOUR = 0.25 * HOUR;
|
|
const HALF_HOUR = 0.5 * HOUR;
|
|
const ONE_HOUR = 1 * HOUR;
|
|
const TWO_HOURS = 2 * HOUR;
|
|
const EVERYTHING = 7 * 24 * HOUR;
|
|
|
|
const options = [QUARTER_HOUR, HALF_HOUR, ONE_HOUR, TWO_HOURS, EVERYTHING];
|
|
|
|
// support custom values (via API)
|
|
if (this.localPrecondition && !options.includes(this.localPrecondition)) {
|
|
options.push(this.localPrecondition);
|
|
}
|
|
|
|
return options.map((s) => ({
|
|
value: s,
|
|
name:
|
|
s === EVERYTHING
|
|
? this.$t("main.chargingPlan.precondition.optionAll")
|
|
: this.fmtDurationLong(s),
|
|
}));
|
|
},
|
|
},
|
|
watch: {
|
|
precondition: {
|
|
handler(newValue: number) {
|
|
// Only update if value actually changed from external source
|
|
if (newValue !== this.localPrecondition) {
|
|
this.localPrecondition = newValue;
|
|
}
|
|
},
|
|
immediate: true,
|
|
},
|
|
continuous: {
|
|
handler(newValue: boolean) {
|
|
// Only update if value actually changed from external source
|
|
if (newValue !== this.localContinuous) {
|
|
this.localContinuous = newValue;
|
|
}
|
|
},
|
|
immediate: true,
|
|
},
|
|
},
|
|
methods: {
|
|
formId(name: string) {
|
|
return `chargingplan-${this.id}-${name}`;
|
|
},
|
|
updateStrategy(): void {
|
|
const strategy: PlanStrategy = {
|
|
continuous: this.localContinuous,
|
|
precondition: this.localPrecondition,
|
|
};
|
|
this.$emit("update", strategy);
|
|
},
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* overflow-hidden would clip the select focus rings */
|
|
.ring-space {
|
|
padding: 0.5rem;
|
|
margin: -0.5rem;
|
|
}
|
|
.icon {
|
|
transform: rotate(0deg);
|
|
transition: transform var(--evcc-transition-medium) ease;
|
|
}
|
|
.iconUp {
|
|
transform: rotate(-180deg);
|
|
}
|
|
</style>
|