Revised plan UI (#11086)
* revised plan ui * adjust tests * add german translation
This commit is contained in:
parent
855185b186
commit
cddea4f872
8 changed files with 159 additions and 78 deletions
|
|
@ -428,3 +428,10 @@ input[type="time"]::-webkit-calendar-picker-indicator {
|
|||
.table {
|
||||
--bs-table-bg: transparent;
|
||||
}
|
||||
|
||||
/* larger check switch */
|
||||
.form-switch .form-check-input {
|
||||
height: 1.1rem;
|
||||
width: calc(1.2rem + 0.75rem);
|
||||
border-radius: 3rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,9 @@ export default {
|
|||
return hourSum ? priceSum / hourSum : undefined;
|
||||
},
|
||||
fmtAvgPrice() {
|
||||
if (!this.targetTime) {
|
||||
return "—";
|
||||
}
|
||||
let price = this.activeSlot ? this.activeSlot.price : this.avgPrice;
|
||||
if (price === undefined) {
|
||||
return this.$t("main.targetChargePlan.unknownPrice");
|
||||
|
|
@ -109,7 +112,7 @@ export default {
|
|||
end.setHours(startHour + 1);
|
||||
const endHour = end.getHours();
|
||||
const day = this.weekdayShort(start);
|
||||
const toLate = this.targetTime.getTime() <= start.getTime();
|
||||
const toLate = this.targetTime && this.targetTime.getTime() <= start.getTime();
|
||||
// TODO: handle multiple matching time slots
|
||||
const price = this.findSlotInRange(start, end, rates)?.price;
|
||||
const charging = this.findSlotInRange(start, end, plan) != null;
|
||||
|
|
|
|||
|
|
@ -1,41 +1,28 @@
|
|||
<template>
|
||||
<div class="mt-4">
|
||||
<div class="form-group d-lg-flex align-items-baseline justify-content-between">
|
||||
<div v-if="plans.length > 0" class="container px-0 mb-3">
|
||||
<div class="container px-0 mb-3">
|
||||
<ChargingPlanSettingsEntry
|
||||
v-for="(p, index) in plans"
|
||||
:id="`${id}_${index}`"
|
||||
:key="index"
|
||||
:id="`${id}_0`"
|
||||
class="mb-2"
|
||||
v-bind="p"
|
||||
v-bind="plans[0] || {}"
|
||||
:vehicle-capacity="vehicleCapacity"
|
||||
:range-per-soc="rangePerSoc"
|
||||
:soc-per-kwh="socPerKwh"
|
||||
:soc-based-planning="socBasedPlanning"
|
||||
@plan-updated="(data) => updatePlan({ index, ...data })"
|
||||
@plan-removed="() => removePlan(index)"
|
||||
@plan-updated="(data) => updatePlan({ index: 0, ...data })"
|
||||
@plan-removed="() => removePlan(0)"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>
|
||||
{{ $t("main.targetCharge.planDescription") }}
|
||||
</p>
|
||||
<button class="btn btn-outline-primary" type="button" @click="addPlan">
|
||||
{{ $t("main.targetCharge.setPlan") }}
|
||||
</button>
|
||||
</div>
|
||||
<ChargingPlanWarnings v-bind="chargingPlanWarningsProps" class="mb-4" />
|
||||
<hr />
|
||||
<h5>
|
||||
<div class="inner">
|
||||
{{ $t(`main.targetCharge.${plans.length ? "currentPlan" : "noActivePlan"}`) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="plans.length > 0">
|
||||
<ChargingPlanWarnings v-bind="chargingPlanWarningsProps" class="mb-4" />
|
||||
<hr />
|
||||
<h5>
|
||||
<div class="inner">{{ $t("main.targetCharge.preview") }}</div>
|
||||
</h5>
|
||||
<ChargingPlanPreview
|
||||
v-if="chargingPlanPreviewProps"
|
||||
v-bind="chargingPlanPreviewProps"
|
||||
/>
|
||||
</div>
|
||||
</h5>
|
||||
<ChargingPlanPreview v-if="chargingPlanPreviewProps" v-bind="chargingPlanPreviewProps" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -86,11 +73,8 @@ export default {
|
|||
chargingPlanWarningsProps: function () {
|
||||
return this.collectProps(ChargingPlanWarnings);
|
||||
},
|
||||
selectedTargetTime: function () {
|
||||
return new Date(this.effectivePlanTime);
|
||||
},
|
||||
chargingPlanPreviewProps: function () {
|
||||
const targetTime = this.selectedTargetTime;
|
||||
const targetTime = this.effectivePlanTime ? new Date(this.effectivePlanTime) : null;
|
||||
const { rates } = this.tariff;
|
||||
const { duration, plan, power } = this.plan;
|
||||
const { currency, smartCostType } = this;
|
||||
|
|
@ -112,7 +96,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
fetchPlan: async function () {
|
||||
if (this.plans.length > 0 && !this.loading) {
|
||||
if (!this.loading) {
|
||||
try {
|
||||
this.loading = true;
|
||||
this.plan = (await api.get(`loadpoints/${this.id}/plan`)).data.result;
|
||||
|
|
@ -168,6 +152,7 @@ h5 {
|
|||
position: relative;
|
||||
display: flex;
|
||||
top: -25px;
|
||||
margin-bottom: -0.5rem;
|
||||
padding: 0 0.5rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
{{ $t("main.chargingPlan.day") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="col-6 col-lg-2">
|
||||
<label :for="formId('time')">
|
||||
{{ $t("main.chargingPlan.time") }}
|
||||
</label>
|
||||
|
|
@ -16,7 +16,9 @@
|
|||
{{ $t("main.chargingPlan.goal") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-2"></div>
|
||||
<div class="col-6 col-lg-1">
|
||||
<label :for="formId('active')"> {{ $t("main.chargingPlan.active") }} </label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 d-lg-none col-form-label">
|
||||
|
|
@ -41,7 +43,7 @@
|
|||
{{ $t("main.chargingPlan.time") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3 mb-2 mb-lg-0">
|
||||
<div class="col-6 col-lg-2 mb-2 mb-lg-0">
|
||||
<input
|
||||
:id="formId('time')"
|
||||
v-model="selectedTime"
|
||||
|
|
@ -81,26 +83,53 @@
|
|||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-lg-2 d-flex justify-content-end align-items-baseline">
|
||||
<div class="col-6 d-lg-none col-form-label">
|
||||
<label :for="formId('active')">
|
||||
{{ $t("main.chargingPlan.active") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-2 d-flex align-items-center justify-content-start">
|
||||
<div class="form-check form-switch">
|
||||
<input
|
||||
:id="formId('active')"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
data-testid="plan-active"
|
||||
:checked="!isNew"
|
||||
:disabled="timeInThePast"
|
||||
@change="toggle"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
v-if="dataChanged && !isNew"
|
||||
type="button"
|
||||
class="btn evcc-default-text text-decoration-underline"
|
||||
@click="removePlan"
|
||||
class="btn btn-sm btn-outline-primary ms-3 border-0 text-decoration-underline"
|
||||
data-testid="plan-apply"
|
||||
:disabled="timeInThePast"
|
||||
@click="update"
|
||||
>
|
||||
{{ $t("main.chargingPlan.remove") }}
|
||||
{{ $t("main.chargingPlan.update") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mb-0">
|
||||
<span v-if="timeInThePast" class="d-block text-danger my-2">
|
||||
{{ $t("main.targetCharge.targetIsInThePast") }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "@h2d2/shopicons/es/regular/checkmark";
|
||||
import { distanceUnit } from "../units";
|
||||
|
||||
import formatter from "../mixins/formatter";
|
||||
import { energyOptions } from "../utils/energyOptions";
|
||||
|
||||
const LAST_TARGET_TIME_KEY = "last_target_time";
|
||||
const DEFAULT_TARGET_TIME = "7:00";
|
||||
|
||||
export default {
|
||||
name: "ChargingPlanSettingsEntry",
|
||||
|
|
@ -122,13 +151,10 @@ export default {
|
|||
selectedTime: null,
|
||||
selectedSoc: this.soc,
|
||||
selectedEnergy: this.energy,
|
||||
enabled: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
timeInThePast: function () {
|
||||
const now = new Date();
|
||||
return now >= this.selectedDate;
|
||||
},
|
||||
selectedDate: function () {
|
||||
return new Date(`${this.selectedDay}T${this.selectedTime || "00:00"}`);
|
||||
},
|
||||
|
|
@ -149,25 +175,47 @@ export default {
|
|||
// remove the first entry (0)
|
||||
return options.slice(1);
|
||||
},
|
||||
originalData: function () {
|
||||
if (this.isNew) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
soc: this.soc,
|
||||
energy: this.energy,
|
||||
day: this.fmtDayString(new Date(this.time)),
|
||||
time: this.fmtTimeString(new Date(this.time)),
|
||||
};
|
||||
},
|
||||
dataChanged: function () {
|
||||
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: function () {
|
||||
return !this.time && (!this.soc || !this.energy);
|
||||
},
|
||||
timeInThePast: function () {
|
||||
const now = new Date();
|
||||
return now >= this.selectedDate;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
time() {
|
||||
this.initInputFields();
|
||||
},
|
||||
selectedDate() {
|
||||
this.updatePlan();
|
||||
},
|
||||
selectedSoc() {
|
||||
this.updatePlan();
|
||||
},
|
||||
selectedEnergy() {
|
||||
this.updatePlan();
|
||||
},
|
||||
soc() {
|
||||
this.selectedSoc = this.soc;
|
||||
if (this.soc) {
|
||||
this.selectedSoc = this.soc;
|
||||
}
|
||||
},
|
||||
energy() {
|
||||
this.selectedEnergy = this.energy;
|
||||
if (this.energy) {
|
||||
this.selectedEnergy = this.energy;
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
|
@ -182,7 +230,22 @@ export default {
|
|||
return { value, name };
|
||||
},
|
||||
initInputFields: function () {
|
||||
const date = new Date(this.time);
|
||||
if (!this.selectedSoc) {
|
||||
this.selectedSoc = 100;
|
||||
}
|
||||
if (!this.selectedEnergy) {
|
||||
this.selectedEnergy = this.vehicleCapacity || 10;
|
||||
}
|
||||
|
||||
let time = this.time;
|
||||
if (!time) {
|
||||
// no time but existing selection, keep it
|
||||
if (this.selectedDay && this.selectedTime) {
|
||||
return;
|
||||
}
|
||||
time = this.defaultTime();
|
||||
}
|
||||
const date = new Date(time);
|
||||
this.selectedDay = this.fmtDayString(date);
|
||||
this.selectedTime = this.fmtTimeString(date);
|
||||
},
|
||||
|
|
@ -208,7 +271,7 @@ export default {
|
|||
}
|
||||
return options;
|
||||
},
|
||||
updatePlan: function () {
|
||||
update: function () {
|
||||
try {
|
||||
const hours = this.selectedDate.getHours();
|
||||
const minutes = this.selectedDate.getMinutes();
|
||||
|
|
@ -222,8 +285,30 @@ export default {
|
|||
energy: this.selectedEnergy,
|
||||
});
|
||||
},
|
||||
removePlan: function () {
|
||||
this.$emit("plan-removed");
|
||||
toggle: function (e) {
|
||||
const { checked } = e.target;
|
||||
if (checked) {
|
||||
this.update();
|
||||
} else {
|
||||
this.$emit("plan-removed");
|
||||
}
|
||||
this.enabled = checked;
|
||||
},
|
||||
defaultTime: function () {
|
||||
const [hours, minutes] = (
|
||||
window.localStorage[LAST_TARGET_TIME_KEY] || DEFAULT_TARGET_TIME
|
||||
).split(":");
|
||||
|
||||
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;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
<template>
|
||||
<p class="mb-0">
|
||||
<span v-if="timeInThePast" class="d-block text-danger mb-1">
|
||||
{{ $t("main.targetCharge.targetIsInThePast") }}
|
||||
</span>
|
||||
<span v-if="targetIsAboveVehicleLimit" class="d-block text-danger mb-1">
|
||||
{{ $t("main.targetCharge.targetIsAboveVehicleLimit", { limit: vehicleLimitFmt }) }}
|
||||
</span>
|
||||
<span v-if="['off', 'now'].includes(mode)" class="d-block text-secondary mb-1">
|
||||
{{ $t("main.targetCharge.onlyInPvMode") }}
|
||||
</span>
|
||||
<span v-if="targetIsAboveLimit" class="d-block text-secondary mb-1">
|
||||
{{ $t("main.targetCharge.targetIsAboveLimit", { limit: limitFmt }) }}
|
||||
</span>
|
||||
<span v-if="['off', 'now'].includes(mode)" class="d-block text-secondary mb-1">
|
||||
{{ $t("main.targetCharge.onlyInPvMode") }}
|
||||
</span>
|
||||
<span v-if="timeTooFarInTheFuture" class="d-block text-secondary mb-1">
|
||||
{{ $t("main.targetCharge.targetIsTooFarInTheFuture") }}
|
||||
</span>
|
||||
|
|
@ -43,20 +40,18 @@ export default {
|
|||
currency: String,
|
||||
mode: String,
|
||||
tariff: Object,
|
||||
selectedTargetTime: Date,
|
||||
vehicleTargetSoc: Number,
|
||||
},
|
||||
computed: {
|
||||
timeInThePast: function () {
|
||||
const now = new Date();
|
||||
return now >= this.selectedTargetTime;
|
||||
},
|
||||
timeTooFarInTheFuture: function () {
|
||||
if (!this.effectivePlanTime) {
|
||||
return false;
|
||||
}
|
||||
if (this.tariff?.rates) {
|
||||
const lastRate = this.tariff.rates[this.tariff.rates.length - 1];
|
||||
if (lastRate?.end) {
|
||||
const end = new Date(lastRate.end);
|
||||
return this.selectedTargetTime >= end;
|
||||
return new Date(this.effectivePlanTime) >= end;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
|
|
|||
11
i18n/de.toml
11
i18n/de.toml
|
|
@ -164,13 +164,19 @@ modalTitle = "Sicher, dass du neu starten möchtest?"
|
|||
vehicles = "Parkplatz"
|
||||
|
||||
[main.chargingPlan]
|
||||
active = "Aktiv"
|
||||
arrivalTab = "Ankunft"
|
||||
day = "Tag"
|
||||
departureTab = "Abfahrt"
|
||||
goal = "Ladeziel"
|
||||
modalTitle = "Ladeplanung"
|
||||
none = "keiner"
|
||||
remove = "Entfernen"
|
||||
time = "Zeit"
|
||||
title = "Plan"
|
||||
titleMinSoc = "Min. Ladung"
|
||||
titleTargetCharge = "Abfahrt"
|
||||
update = "Anwenden"
|
||||
|
||||
[main.energyflow]
|
||||
battery = "Batterie"
|
||||
|
|
@ -243,19 +249,18 @@ logout = "abmelden"
|
|||
activate = "Aktivieren"
|
||||
co2Limit = "CO₂-Grenze von {co2}"
|
||||
costLimitIgnore = "Die eingestellte {limit} wird in diesem Zeitraum ignoriert."
|
||||
currentPlan = "Geplante Zeiten"
|
||||
descriptionEnergy = "Bis wann sollen {targetEnergy} ins Fahrzeug geladen sein?"
|
||||
descriptionSoc = "Wann soll das Fahrzeug auf {targetSoc}% geladen sein?"
|
||||
inactiveLabel = "Zielzeit"
|
||||
noActivePlan = "Kein aktiver Plan"
|
||||
onlyInPvMode = "Ladeplanung ist nur im PV-Modus aktiv."
|
||||
planDescription = "Gib eine Abfahrtszeit an, und evcc lädt das Fahrzeug so kostengünstig und umweltfreundlich wie möglich."
|
||||
planDuration = "Ladedauer"
|
||||
planPeriodLabel = "Zeitraum"
|
||||
planPeriodValue = "{start} bis {end}"
|
||||
planUnknown = "noch unbekannt"
|
||||
preview = "Vorschau"
|
||||
priceLimit = "Preisgrenze von {price}"
|
||||
remove = "Entfernen"
|
||||
setPlan = "Ladeplan festlegen"
|
||||
setTargetTime = "keine"
|
||||
targetIsAboveLimit = "Das eingestellte Ladelimit von {limit} wird in diesem Zeitraum ignoriert."
|
||||
targetIsAboveVehicleLimit = "Erhöhe das Fahrzeuglimit ({limit}), um das Ladeziel zu erreichen."
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@ modalTitle = "Are you sure you want to restart?"
|
|||
vehicles = "Parking"
|
||||
|
||||
[main.chargingPlan]
|
||||
active = "Active"
|
||||
arrivalTab = "Arrival"
|
||||
day = "Day"
|
||||
departureTab = "Departure"
|
||||
|
|
@ -172,6 +173,7 @@ time = "Time"
|
|||
title = "Plan"
|
||||
titleMinSoc = "Min charge"
|
||||
titleTargetCharge = "Departure"
|
||||
update = "Apply"
|
||||
|
||||
[main.energyflow]
|
||||
battery = "Battery"
|
||||
|
|
@ -244,19 +246,18 @@ logout = "log out"
|
|||
activate = "Activate"
|
||||
co2Limit = "CO₂ limit of {co2}"
|
||||
costLimitIgnore = "The configured {limit} will be ignored during this period."
|
||||
currentPlan = "Charging schedule"
|
||||
descriptionEnergy = "Until when should {targetEnergy} be loaded into the vehicle?"
|
||||
descriptionSoc = "When should the vehicle be charged to {targetSoc}%?"
|
||||
inactiveLabel = "Target time"
|
||||
noActivePlan = "No Active Plan"
|
||||
onlyInPvMode = "Charging plan only works in solar mode."
|
||||
planDescription = "Enter a departure time, and evcc will charge the vehicle as cost-effectively or environmentally friendly as possible."
|
||||
planDuration = "Charging time"
|
||||
planPeriodLabel = "Period"
|
||||
planPeriodValue = "{start} to {end}"
|
||||
planUnknown = "not known yet"
|
||||
preview = "Preview"
|
||||
priceLimit = "price limit of {price}"
|
||||
remove = "Remove"
|
||||
setPlan = "Set a charging plan"
|
||||
setTargetTime = "none"
|
||||
targetIsAboveLimit = "The configured charging limit of {limit} will be ignored during this period."
|
||||
targetIsAboveVehicleLimit = "Increase vehicle limit ({limit}) to reach the charging goal."
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ test.afterEach(async () => {
|
|||
|
||||
async function setAndVerifyPlan(page, lp, { soc, energy }) {
|
||||
await lp.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
await page.getByRole("button", { name: "Set a charging plan" }).click();
|
||||
|
||||
if (soc) {
|
||||
await page.getByTestId("plan-soc").selectOption(soc);
|
||||
|
|
@ -26,6 +25,7 @@ async function setAndVerifyPlan(page, lp, { soc, energy }) {
|
|||
.textContent();
|
||||
await page.getByTestId("plan-energy").selectOption(optionText);
|
||||
}
|
||||
await page.getByTestId("plan-active").click();
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
await expect(lp.getByTestId("charging-plan")).toContainText(soc || energy);
|
||||
}
|
||||
|
|
@ -41,11 +41,11 @@ test.describe("basic functionality", async () => {
|
|||
|
||||
await lp1.getByTestId("limit-soc").getByRole("combobox").selectOption("90%");
|
||||
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
await page.getByRole("button", { name: "Set a charging plan" }).click();
|
||||
|
||||
await page.getByTestId("plan-day").selectOption({ index: 1 });
|
||||
await page.getByTestId("plan-time").fill("09:30");
|
||||
await page.getByTestId("plan-soc").selectOption("80%");
|
||||
await page.getByTestId("plan-active").click();
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
|
||||
await expect(lp1.getByTestId("plan-marker")).toBeVisible();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue