380 lines
9.3 KiB
Vue
380 lines
9.3 KiB
Vue
<template>
|
|
<div class="mb-5 mb-lg-4" data-testid="plan-entry">
|
|
<h5
|
|
v-if="multiplePlans"
|
|
class="d-flex gap-3 align-items-baseline d-lg-none mb-4 fw-normal evcc-gray"
|
|
data-testid="repeating-plan-title"
|
|
>
|
|
<span class="text-uppercase fs-6">
|
|
{{ `${$t("main.chargingPlan.planNumber", { number: "#1" })}` }}
|
|
</span>
|
|
</h5>
|
|
|
|
<div class="row d-none d-lg-flex mb-2">
|
|
<div v-if="multiplePlans" class="plan-id d-flex"></div>
|
|
<div class="col-3">
|
|
<label :for="formId('day')">
|
|
{{ $t("main.chargingPlan.day") }}
|
|
</label>
|
|
</div>
|
|
<div class="col-2">
|
|
<label :for="formId('time')">
|
|
{{ $t("main.chargingPlan.time") }}
|
|
</label>
|
|
</div>
|
|
<div class="col-3">
|
|
<label :for="formId('goal')">
|
|
{{ $t("main.chargingPlan.goal") }}
|
|
</label>
|
|
</div>
|
|
<div class="col-2">
|
|
<label :for="formId('active')"> {{ $t("main.chargingPlan.active") }} </label>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div
|
|
v-if="multiplePlans"
|
|
class="plan-id d-none d-lg-flex align-items-center justify-content-start fs-6"
|
|
>
|
|
#1
|
|
</div>
|
|
<div class="col-5 d-lg-none col-form-label">
|
|
<label :for="formId('day')">
|
|
{{ $t("main.chargingPlan.day") }}
|
|
</label>
|
|
</div>
|
|
<div class="col-7 col-lg-3 mb-2 mb-lg-0">
|
|
<select
|
|
:id="formId('day')"
|
|
v-model="selectedDay"
|
|
class="form-select me-2"
|
|
data-testid="static-plan-day"
|
|
@change="preview()"
|
|
>
|
|
<option v-for="opt in dayOptions()" :key="opt.value" :value="opt.value">
|
|
{{ opt.name }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-5 d-lg-none col-form-label">
|
|
<label :for="formId('day')">
|
|
{{ $t("main.chargingPlan.time") }}
|
|
</label>
|
|
</div>
|
|
<div class="col-7 col-lg-2 mb-2 mb-lg-0">
|
|
<input
|
|
:id="formId('time')"
|
|
v-model="selectedTime"
|
|
type="time"
|
|
class="form-control mx-0"
|
|
:step="60 * 5"
|
|
data-testid="static-plan-time"
|
|
required
|
|
@change="preview()"
|
|
/>
|
|
</div>
|
|
<div class="col-5 d-lg-none col-form-label">
|
|
<label :for="formId('goal')">
|
|
{{ $t("main.chargingPlan.goal") }}
|
|
</label>
|
|
</div>
|
|
<div class="col-7 col-lg-3 mb-2 mb-lg-0">
|
|
<select
|
|
v-if="socBasedPlanning"
|
|
:id="formId('goal')"
|
|
v-model="selectedSoc"
|
|
class="form-select mx-0"
|
|
data-testid="static-plan-soc"
|
|
@change="preview()"
|
|
>
|
|
<option v-for="opt in socOptions" :key="opt.value" :value="opt.value">
|
|
{{ opt.name }}
|
|
</option>
|
|
</select>
|
|
<select
|
|
v-else
|
|
:id="formId('goal')"
|
|
v-model="selectedEnergy"
|
|
class="form-select mx-0"
|
|
data-testid="static-plan-energy"
|
|
@change="preview()"
|
|
>
|
|
<option v-for="opt in energyOptions" :key="opt.energy" :value="opt.energy">
|
|
{{ opt.text }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-5 d-lg-none col-form-label">
|
|
<label :for="formId('active')">
|
|
{{ $t("main.chargingPlan.active") }}
|
|
</label>
|
|
</div>
|
|
<div class="col-3 col-lg-1 d-flex align-items-center">
|
|
<div class="form-check form-switch my-1">
|
|
<input
|
|
:id="formId('active')"
|
|
class="form-check-input"
|
|
type="checkbox"
|
|
role="switch"
|
|
data-testid="static-plan-active"
|
|
:checked="!isNew"
|
|
:disabled="timeInThePast"
|
|
tabindex="0"
|
|
@change="toggle"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="col-4 col-lg-2 d-flex align-items-center justify-content-end justify-content-lg-start"
|
|
>
|
|
<button
|
|
v-if="dataChanged && !isNew"
|
|
type="button"
|
|
class="btn btn-sm btn-outline-primary border-0 text-decoration-underline text-truncate"
|
|
data-testid="static-plan-apply"
|
|
:disabled="timeInThePast"
|
|
tabindex="0"
|
|
@click="update"
|
|
>
|
|
{{ $t("main.chargingPlan.update") }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<p class="mb-0" data-testid="plan-entry-warnings">
|
|
<span v-if="timeInThePast" class="d-block text-danger my-2">
|
|
{{ $t("main.targetCharge.targetIsInThePast") }}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { distanceUnit } from "@/units";
|
|
|
|
import formatter from "@/mixins/formatter";
|
|
import { energyOptions } from "@/utils/energyOptions.ts";
|
|
import { defineComponent } from "vue";
|
|
import settings from "@/settings";
|
|
|
|
const DEFAULT_TARGET_TIME = "7:00";
|
|
|
|
export default defineComponent({
|
|
name: "ChargingPlanStaticSettings",
|
|
mixins: [formatter],
|
|
props: {
|
|
id: [String, Number],
|
|
soc: Number,
|
|
energy: Number,
|
|
time: Date,
|
|
rangePerSoc: Number,
|
|
socPerKwh: Number,
|
|
capacity: Number,
|
|
socBasedPlanning: Boolean,
|
|
multiplePlans: Boolean,
|
|
},
|
|
emits: ["static-plan-updated", "static-plan-removed", "plan-preview"],
|
|
data() {
|
|
return {
|
|
selectedDay: null as string | null,
|
|
selectedTime: null as string | null,
|
|
selectedSoc: this.soc,
|
|
selectedEnergy: this.energy,
|
|
active: false,
|
|
};
|
|
},
|
|
computed: {
|
|
selectedDate() {
|
|
return new Date(`${this.selectedDay}T${this.selectedTime || "00:00"}`);
|
|
},
|
|
socOptions() {
|
|
// a list of entries from 5 to 100 with a step of 5
|
|
const options = Array.from(Array(20).keys())
|
|
.map((i) => 5 + i * 5)
|
|
.map(this.socOption);
|
|
|
|
// add current soc value if it's not in the list
|
|
if (this.selectedSoc && !options.find((o) => o.value === this.selectedSoc)) {
|
|
options.push(this.socOption(this.selectedSoc));
|
|
options.sort((a, b) => a.value - b.value);
|
|
}
|
|
|
|
return options;
|
|
},
|
|
energyOptions() {
|
|
const options = energyOptions(
|
|
0,
|
|
this.capacity || 100,
|
|
this.fmtWh,
|
|
this.fmtPercentage,
|
|
"-",
|
|
this.socPerKwh,
|
|
Number(this.selectedEnergy)
|
|
);
|
|
// remove the first entry (0)
|
|
return options.slice(1);
|
|
},
|
|
originalData() {
|
|
if (this.isNew) {
|
|
return {};
|
|
}
|
|
const t = this.time || new Date();
|
|
return {
|
|
soc: this.soc,
|
|
energy: this.energy,
|
|
day: this.fmtDayString(t),
|
|
time: this.fmtTimeString(t),
|
|
};
|
|
},
|
|
dataChanged() {
|
|
const dateChanged =
|
|
this.originalData.day != this.selectedDay ||
|
|
this.originalData.time != this.selectedTime;
|
|
const goalChanged = this.socBasedPlanning
|
|
? this.originalData.soc != this.selectedSoc
|
|
: this.originalData.energy != this.selectedEnergy;
|
|
return dateChanged || goalChanged;
|
|
},
|
|
isNew() {
|
|
return !this.time && (!this.soc || !this.energy);
|
|
},
|
|
timeInThePast() {
|
|
const now = new Date();
|
|
return now >= this.selectedDate;
|
|
},
|
|
},
|
|
watch: {
|
|
time() {
|
|
this.initInputFields();
|
|
},
|
|
soc() {
|
|
if (this.soc) {
|
|
this.selectedSoc = this.soc;
|
|
}
|
|
},
|
|
energy() {
|
|
if (this.energy) {
|
|
this.selectedEnergy = this.energy;
|
|
}
|
|
},
|
|
isNew(value) {
|
|
this.active = !value;
|
|
},
|
|
},
|
|
mounted() {
|
|
this.initInputFields();
|
|
this.preview();
|
|
},
|
|
methods: {
|
|
formId(name: string) {
|
|
return `chargingplan-${this.id}-${name}`;
|
|
},
|
|
socOption(value: number) {
|
|
const name = this.fmtSocOption(value, this.rangePerSoc, distanceUnit());
|
|
return { value, name };
|
|
},
|
|
initInputFields() {
|
|
if (!this.selectedSoc) {
|
|
this.selectedSoc = settings.lastSocGoal ?? 100;
|
|
}
|
|
if (!this.selectedEnergy) {
|
|
this.selectedEnergy = settings.lastEnergyGoal ?? (this.capacity || 10);
|
|
}
|
|
|
|
let t = this.time;
|
|
if (!t) {
|
|
// no time but existing selection, keep it
|
|
if (this.selectedDay && this.selectedTime) {
|
|
return;
|
|
}
|
|
t = this.defaultTime();
|
|
}
|
|
const date = new Date(t);
|
|
this.selectedDay = this.fmtDayString(date);
|
|
this.selectedTime = this.fmtTimeString(date);
|
|
},
|
|
dayOptions() {
|
|
const options = [];
|
|
const date = new Date();
|
|
const labels = [
|
|
this.$t("main.targetCharge.today"),
|
|
this.$t("main.targetCharge.tomorrow"),
|
|
];
|
|
for (let i = 0; i < 7; i++) {
|
|
const dayNumber = date.toLocaleDateString(this.$i18n?.locale, {
|
|
day: "numeric",
|
|
month: "short",
|
|
});
|
|
const dayName =
|
|
labels[i] || date.toLocaleDateString(this.$i18n?.locale, { weekday: "short" });
|
|
options.push({
|
|
value: this.fmtDayString(date),
|
|
name: `${dayNumber} (${dayName})`,
|
|
});
|
|
date.setDate(date.getDate() + 1);
|
|
}
|
|
return options;
|
|
},
|
|
update() {
|
|
try {
|
|
const hours = this.selectedDate.getHours();
|
|
const minutes = this.selectedDate.getMinutes();
|
|
settings.lastTargetTime = `${hours}:${minutes}`;
|
|
settings.lastSocGoal = this.selectedSoc;
|
|
settings.lastEnergyGoal = this.selectedEnergy;
|
|
} catch (e) {
|
|
console.warn(e);
|
|
}
|
|
this.$emit("static-plan-updated", {
|
|
time: this.selectedDate,
|
|
soc: this.selectedSoc,
|
|
energy: this.selectedEnergy,
|
|
});
|
|
},
|
|
preview(force = false) {
|
|
if (!this.isNew && !force) {
|
|
return;
|
|
}
|
|
this.$emit("plan-preview", {
|
|
time: this.selectedDate,
|
|
soc: this.selectedSoc,
|
|
energy: this.selectedEnergy,
|
|
});
|
|
},
|
|
toggle(e: Event) {
|
|
const { checked } = e.target as HTMLInputElement;
|
|
if (checked) {
|
|
this.update();
|
|
} else {
|
|
this.$emit("static-plan-removed");
|
|
this.preview(true);
|
|
}
|
|
this.active = checked;
|
|
},
|
|
defaultTime() {
|
|
const lastTargetTime = (settings.lastTargetTime || DEFAULT_TARGET_TIME).split(":");
|
|
const hours = Number(lastTargetTime[0]);
|
|
const minutes = Number(lastTargetTime[1]);
|
|
|
|
const target = new Date();
|
|
target.setSeconds(0);
|
|
target.setMinutes(minutes);
|
|
target.setHours(hours);
|
|
// today or tomorrow?
|
|
const isInPast = target < new Date();
|
|
if (isInPast) {
|
|
target.setDate(target.getDate() + 1);
|
|
}
|
|
return target;
|
|
},
|
|
},
|
|
});
|
|
</script>
|
|
<style scoped>
|
|
.plan-id-insert {
|
|
margin-left: 2.5rem;
|
|
}
|
|
.plan-id {
|
|
width: 2.5rem;
|
|
color: var(--evcc-gray);
|
|
}
|
|
</style>
|