708 lines
21 KiB
Vue
708 lines
21 KiB
Vue
<template>
|
|
<div class="container px-4 safe-area-inset">
|
|
<TopHeader :title="$t('sessions.title')" />
|
|
<div class="row">
|
|
<main class="col-12">
|
|
<div class="header-outer sticky-top">
|
|
<div class="container px-4">
|
|
<div
|
|
class="row py-3 py-sm-3 d-flex flex-column flex-sm-row gap-3 gap-lg-0 mb-lg-2"
|
|
>
|
|
<div class="col-lg-5 d-flex mb-lg-0">
|
|
<PeriodSelector
|
|
:period="period"
|
|
:periodOptions="periodOptions"
|
|
@update:period="changePeriod"
|
|
/>
|
|
</div>
|
|
<div v-if="showDateNavigator" class="col-lg-6 offset-lg-1">
|
|
<DateNavigator
|
|
:month="month"
|
|
:year="year"
|
|
:startDate="startDate"
|
|
:showMonth="showMonthNavigation"
|
|
:showYear="showYearNavigation"
|
|
@update-date="updateDate"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex gap-3 mb-5 justify-content-between flex-wrap pt-1">
|
|
<IconSelectGroup>
|
|
<template v-for="largeScreen in [true, false]">
|
|
<IconSelectItem
|
|
v-for="{ value, label, disabled, active } in typeOptions"
|
|
:key="value + largeScreen"
|
|
:label="largeScreen ? label : undefined"
|
|
:class="{
|
|
'd-none d-lg-block': largeScreen,
|
|
'd-block d-lg-none': !largeScreen,
|
|
}"
|
|
:disabled="disabled"
|
|
:active="active"
|
|
@click="updateType(value)"
|
|
>
|
|
<component :is="typeIcons[value]"></component>
|
|
</IconSelectItem>
|
|
</template>
|
|
</IconSelectGroup>
|
|
<IconSelectGroup>
|
|
<template v-for="largeScreen in [true, false]">
|
|
<IconSelectItem
|
|
v-for="group in Object.values(groups)"
|
|
:key="group + largeScreen"
|
|
:active="selectedGroup === group"
|
|
:label="
|
|
largeScreen
|
|
? $t(`sessions.groupBy.${group.toLowerCase()}`)
|
|
: undefined
|
|
"
|
|
:class="{
|
|
'd-none d-lg-block': largeScreen,
|
|
'd-block d-lg-none': !largeScreen,
|
|
}"
|
|
@click="updateGroup(group)"
|
|
>
|
|
<component :is="groupIcons[group]"></component>
|
|
</IconSelectItem>
|
|
</template>
|
|
</IconSelectGroup>
|
|
</div>
|
|
|
|
<h3
|
|
class="fw-normal my-0 d-flex gap-2 flex-wrap d-flex align-items-baseline overflow-hidden justify-content-between"
|
|
>
|
|
<span class="d-block no-wrap text-truncate">{{ historyTitle }}</span>
|
|
<small class="d-block no-wrap text-truncate">{{ historySubTitle }}</small>
|
|
</h3>
|
|
<EnergyHistoryChart
|
|
v-if="activeType === types.SOLAR"
|
|
class="mb-5"
|
|
:sessions="currentSessions"
|
|
:color-mappings="colorMappings"
|
|
:group-by="selectedGroup"
|
|
:period="period"
|
|
/>
|
|
<CostHistoryChart
|
|
v-else
|
|
class="mb-5"
|
|
:sessions="currentTypeSessions"
|
|
:color-mappings="colorMappings"
|
|
:group-by="selectedGroup"
|
|
:cost-type="activeType"
|
|
:period="period"
|
|
:suggested-max-cost="suggestedMaxCost"
|
|
/>
|
|
<div v-if="showExtraCharts" class="row align-items-start">
|
|
<div class="col-12 col-lg-6 mb-5">
|
|
<h3 class="fw-normal my-4">{{ firstExtraTitle }}</h3>
|
|
<div v-if="activeType === types.SOLAR">
|
|
<SolarYearChart
|
|
v-if="showSolarYearChart"
|
|
:period="period"
|
|
:sessions="currentSessions"
|
|
/>
|
|
<SolarGroupedChart
|
|
v-else
|
|
:sessions="currentSessions"
|
|
:color-mappings="colorMappings"
|
|
:group-by="selectedGroup"
|
|
/>
|
|
</div>
|
|
<AvgCostGroupedChart
|
|
v-else
|
|
:sessions="currentTypeSessions"
|
|
:color-mappings="colorMappings"
|
|
:suggested-max-price="suggestedMaxCost"
|
|
:group-by="selectedGroup"
|
|
:cost-type="activeType"
|
|
/>
|
|
</div>
|
|
<div class="col-12 col-lg-6 mb-5">
|
|
<h3 class="fw-normal my-4">{{ secondExtraTitle }}</h3>
|
|
<EnergyGroupedChart
|
|
v-if="activeType === types.SOLAR"
|
|
:sessions="currentSessions"
|
|
:color-mappings="colorMappings"
|
|
:group-by="selectedGroup"
|
|
/>
|
|
<CostGroupedChart
|
|
v-else
|
|
:sessions="currentTypeSessions"
|
|
:color-mappings="colorMappings"
|
|
:group-by="selectedGroup"
|
|
:cost-type="activeType"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="showTable">
|
|
<SessionTable
|
|
:sessions="currentSessions"
|
|
:vehicleFilter="vehicleFilter"
|
|
:loadpointFilter="loadpointFilter"
|
|
:currency="currency"
|
|
@show-session="showDetails"
|
|
/>
|
|
<div class="d-grid gap-2 d-md-block mt-3 mb-5">
|
|
<a
|
|
v-if="currentSessions.length"
|
|
class="btn btn-sm btn-outline-secondary text-nowrap me-md-2"
|
|
:href="csvLink"
|
|
download
|
|
>
|
|
{{ $t("sessions.csvMonth", { month: headline }) }}
|
|
</a>
|
|
<a
|
|
v-if="sessions.length"
|
|
class="btn btn-sm btn-outline-secondary text-nowrap"
|
|
:href="csvTotalLink"
|
|
download
|
|
>
|
|
{{ $t("sessions.csvTotal") }}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<button
|
|
v-else
|
|
class="btn btn-link text-muted mb-5"
|
|
@click="changePeriod(periods.MONTH)"
|
|
>
|
|
{{ $t("sessions.showIndividualEntries") }}
|
|
</button>
|
|
</main>
|
|
<SessionDetailsModal
|
|
:session="selectedSession"
|
|
:vehicles="vehicleList"
|
|
:currency="currency"
|
|
@session-changed="loadSessions"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import Modal from "bootstrap/js/dist/modal";
|
|
import "@h2d2/shopicons/es/regular/cablecharge";
|
|
import "@h2d2/shopicons/es/regular/car3";
|
|
import "@h2d2/shopicons/es/regular/sun";
|
|
import formatter, { POWER_UNIT } from "../mixins/formatter";
|
|
import api from "../api";
|
|
import store from "../store";
|
|
import SessionDetailsModal from "../components/Sessions/SessionDetailsModal.vue";
|
|
import SessionTable from "../components/Sessions/SessionTable.vue";
|
|
import EnergyHistoryChart from "../components/Sessions/EnergyHistoryChart.vue";
|
|
import EnergyGroupedChart from "../components/Sessions/EnergyGroupedChart.vue";
|
|
import SolarGroupedChart from "../components/Sessions/SolarGroupedChart.vue";
|
|
import SolarYearChart from "../components/Sessions/SolarYearChart.vue";
|
|
import CostHistoryChart from "../components/Sessions/CostHistoryChart.vue";
|
|
import CostGroupedChart from "../components/Sessions/CostGroupedChart.vue";
|
|
import AvgCostGroupedChart from "../components/Sessions/AvgCostGroupedChart.vue";
|
|
import TopHeader from "../components/TopHeader.vue";
|
|
import IconSelectGroup from "../components/IconSelectGroup.vue";
|
|
import IconSelectItem from "../components/IconSelectItem.vue";
|
|
import SelectGroup from "../components/SelectGroup.vue";
|
|
import CustomSelect from "../components/CustomSelect.vue";
|
|
import colors from "../colors";
|
|
import settings from "../settings";
|
|
import PeriodSelector from "../components/Sessions/PeriodSelector.vue";
|
|
import DateNavigator from "../components/Sessions/DateNavigator.vue";
|
|
import DynamicPriceIcon from "../components/MaterialIcon/DynamicPrice.vue";
|
|
import TotalIcon from "../components/MaterialIcon/Total.vue";
|
|
import { TYPES, GROUPS, PERIODS } from "../components/Sessions/types";
|
|
export default {
|
|
name: "Sessions",
|
|
components: {
|
|
SessionDetailsModal,
|
|
SessionTable,
|
|
TopHeader,
|
|
EnergyHistoryChart,
|
|
EnergyGroupedChart,
|
|
IconSelectGroup,
|
|
IconSelectItem,
|
|
SelectGroup,
|
|
CustomSelect,
|
|
SolarGroupedChart,
|
|
SolarYearChart,
|
|
PeriodSelector,
|
|
DateNavigator,
|
|
DynamicPriceIcon,
|
|
CostHistoryChart,
|
|
CostGroupedChart,
|
|
AvgCostGroupedChart,
|
|
},
|
|
mixins: [formatter],
|
|
props: {
|
|
notifications: Array,
|
|
month: { type: Number, default: () => new Date().getMonth() + 1 },
|
|
year: { type: Number, default: () => new Date().getFullYear() },
|
|
period: { type: String, default: PERIODS.MONTH },
|
|
loadpointFilter: { type: String, default: "" },
|
|
vehicleFilter: { type: String, default: "" },
|
|
offline: Boolean,
|
|
},
|
|
data() {
|
|
return {
|
|
sessions: [],
|
|
selectedType: settings.sessionsType || TYPES.SOLAR,
|
|
selectedGroup: settings.sessionsGroup || GROUPS.NONE,
|
|
selectedSessionId: undefined,
|
|
periods: PERIODS,
|
|
types: TYPES,
|
|
groups: GROUPS,
|
|
};
|
|
},
|
|
head() {
|
|
return { title: `${this.$t("sessions.title")} | evcc` };
|
|
},
|
|
computed: {
|
|
currency() {
|
|
return store.state.currency || "EUR";
|
|
},
|
|
energyTitle() {
|
|
return this.$t("sessions.energyTitle", { percent: this.solarPercentageFmt });
|
|
},
|
|
historyTitle() {
|
|
if (this.activeType === TYPES.SOLAR) {
|
|
return this.energyTitle;
|
|
}
|
|
return this.costTitle;
|
|
},
|
|
historySubTitle() {
|
|
if (this.activeType === TYPES.SOLAR) {
|
|
return this.energySubTitle;
|
|
}
|
|
return this.costSubTitle;
|
|
},
|
|
firstExtraTitle() {
|
|
if (this.activeType === TYPES.SOLAR) {
|
|
return this.solarTitle;
|
|
}
|
|
return this.avgCostTitle;
|
|
},
|
|
secondExtraTitle() {
|
|
if (this.activeType === TYPES.SOLAR) {
|
|
return this.energyGroupedTitle;
|
|
}
|
|
return this.costGroupedTitle;
|
|
},
|
|
solarPercentageFmt() {
|
|
return this.fmtPercentage(
|
|
this.totalEnergy > 0 ? (100 / this.totalEnergy) * this.selfEnergy : 0
|
|
);
|
|
},
|
|
energySumFmt() {
|
|
return this.fmtWh(this.totalEnergy * 1e3, POWER_UNIT.AUTO);
|
|
},
|
|
energySubTitle() {
|
|
return this.$t("sessions.energySubTitle", { energy: this.energySumFmt });
|
|
},
|
|
solarTitle() {
|
|
return this.selectedGroup === GROUPS.NONE
|
|
? this.$t("sessions.solarTitle")
|
|
: this.$t("sessions.solarTitleByGroup", { byGroup: this.byGroupTitle });
|
|
},
|
|
byGroupTitle() {
|
|
if (this.selectedGroup === GROUPS.LOADPOINT) {
|
|
return this.$t("sessions.byGroupLoadpoint");
|
|
} else if (this.selectedGroup === GROUPS.VEHICLE) {
|
|
return this.$t("sessions.byGroupVehicle");
|
|
}
|
|
return "";
|
|
},
|
|
energyGroupedTitle() {
|
|
if (this.selectedGroup === GROUPS.NONE) {
|
|
return this.$t("sessions.energyGroupedTitle", { energy: this.energySumFmt });
|
|
}
|
|
return this.$t("sessions.energyGroupedTitleByGroup", { byGroup: this.byGroupTitle });
|
|
},
|
|
avgCostTitle() {
|
|
const type = this.activeType === TYPES.PRICE ? "Price" : "Co2";
|
|
return this.$t(`sessions.avg${type}TitleByGroup`, { byGroup: this.byGroupTitle });
|
|
},
|
|
costGroupedTitle() {
|
|
const type = this.activeType === TYPES.PRICE ? "Price" : "Co2";
|
|
return this.$t(`sessions.grouped${type}TitleByGroup`, { byGroup: this.byGroupTitle });
|
|
},
|
|
periodOptions() {
|
|
return Object.entries(PERIODS).map(([key, value]) => ({
|
|
name: this.$t(`sessions.period.${key.toLowerCase()}`),
|
|
value,
|
|
}));
|
|
},
|
|
typeOptions() {
|
|
const options = Object.values(TYPES).map((value) => {
|
|
const disabled =
|
|
(value === TYPES.PRICE && !this.typePriceAvailable) ||
|
|
(value === TYPES.CO2 && !this.typeCo2Available);
|
|
const active = this.activeType === value;
|
|
const label = this.$t(`sessions.type.${value}`);
|
|
return { label, value, disabled, active };
|
|
});
|
|
return options;
|
|
},
|
|
totalEnergy() {
|
|
return this.currentSessions.reduce((acc, session) => acc + session.chargedEnergy, 0);
|
|
},
|
|
selfEnergy() {
|
|
return this.currentSessions.reduce(
|
|
(acc, session) => acc + (session.chargedEnergy / 100) * session.solarPercentage,
|
|
0
|
|
);
|
|
},
|
|
currentSessionsWithPrice() {
|
|
return this.currentSessions.filter((s) => s.price !== null);
|
|
},
|
|
currentTypeSessions() {
|
|
if (this.activeType === TYPES.PRICE) {
|
|
return this.currentSessionsWithPrice;
|
|
} else if (this.activeType === TYPES.CO2) {
|
|
return this.currentSessionsWithCo2;
|
|
} else {
|
|
return this.currentSessions;
|
|
}
|
|
},
|
|
totalPrice() {
|
|
return this.currentSessionsWithPrice.reduce((acc, s) => acc + s.price, 0);
|
|
},
|
|
pricePerKWh() {
|
|
const list = this.currentSessionsWithPrice;
|
|
const energy = list.reduce((acc, s) => acc + s.chargedEnergy, 0);
|
|
return energy ? this.totalPrice / energy : null;
|
|
},
|
|
currentSessionsWithCo2() {
|
|
return this.currentSessions.filter((s) => s.co2PerKWh !== null);
|
|
},
|
|
totalCo2() {
|
|
const list = this.currentSessionsWithCo2;
|
|
return list.reduce((acc, s) => acc + s.co2PerKWh * s.chargedEnergy, 0);
|
|
},
|
|
co2PerKWh() {
|
|
const list = this.currentSessionsWithCo2;
|
|
const energy = list.reduce((acc, s) => acc + s.chargedEnergy, 0);
|
|
return energy ? this.totalCo2 / energy : null;
|
|
},
|
|
costTitle() {
|
|
const value =
|
|
this.activeType === TYPES.PRICE
|
|
? this.fmtPricePerKWh(this.pricePerKWh, this.currency)
|
|
: this.fmtCo2Medium(this.co2PerKWh);
|
|
const type = this.activeType === TYPES.PRICE ? "Price" : "Co2";
|
|
return this.$t(`sessions.history${type}Title`, { value });
|
|
},
|
|
costSubTitle() {
|
|
const type = this.activeType === TYPES.PRICE ? "Price" : "Co2";
|
|
const value =
|
|
this.activeType === TYPES.PRICE
|
|
? this.fmtMoney(this.totalPrice, this.currency, true, true)
|
|
: this.fmtGrams(this.totalCo2);
|
|
return this.$t(`sessions.history${type}SubTitle`, { value });
|
|
},
|
|
activeType() {
|
|
if (this.selectedType === TYPES.PRICE && this.typePriceAvailable) {
|
|
return TYPES.PRICE;
|
|
} else if (this.selectedType === TYPES.CO2 && this.typeCo2Available) {
|
|
return TYPES.CO2;
|
|
}
|
|
return TYPES.SOLAR;
|
|
},
|
|
showCostCharts() {
|
|
return this.typePriceAvailable || this.typeCo2Available;
|
|
},
|
|
typePriceAvailable() {
|
|
return this.currentSessionsWithPrice.length > 0;
|
|
},
|
|
typeCo2Available() {
|
|
return this.currentSessionsWithCo2.length > 0;
|
|
},
|
|
startDate() {
|
|
return new Date(this.sessions[0]?.created || Date.now());
|
|
},
|
|
topNavigation: function () {
|
|
const vehicleLogins = store.state.auth ? store.state.auth.vehicles : {};
|
|
return { vehicleLogins, ...this.collectProps(TopNavigation, store.state) };
|
|
},
|
|
sessionsWithDefaults() {
|
|
return this.sessions.map((session) => {
|
|
const loadpoint = session.loadpoint || this.$t("main.loadpoint.fallbackName");
|
|
const vehicle = session.vehicle || this.$t("main.vehicle.unknown");
|
|
return { ...session, loadpoint, vehicle };
|
|
});
|
|
},
|
|
currentSessions() {
|
|
return this.sessionsWithDefaults.filter((session) => {
|
|
const date = new Date(session.created);
|
|
switch (this.period) {
|
|
case PERIODS.MONTH:
|
|
return (
|
|
date.getFullYear() === this.year && date.getMonth() + 1 === this.month
|
|
);
|
|
case PERIODS.YEAR:
|
|
return date.getFullYear() === this.year;
|
|
case PERIODS.TOTAL:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
});
|
|
},
|
|
vehicleList() {
|
|
const vehicles = store.state.vehicles || {};
|
|
return Object.entries(vehicles).map(([name, vehicle]) => ({ name, ...vehicle }));
|
|
},
|
|
selectedSession() {
|
|
return this.sessions.find((s) => s.id == this.selectedSessionId);
|
|
},
|
|
monthName() {
|
|
const date = new Date();
|
|
date.setMonth(this.month - 1, 1);
|
|
return this.fmtMonth(date);
|
|
},
|
|
headline() {
|
|
const date = new Date();
|
|
date.setMonth(this.month - 1, 1);
|
|
date.setFullYear(this.year);
|
|
return this.fmtMonthYear(date);
|
|
},
|
|
csvLink() {
|
|
return this.csvHrefLink(this.year, this.month);
|
|
},
|
|
csvTotalLink() {
|
|
return this.csvHrefLink();
|
|
},
|
|
colorMappings() {
|
|
const lastThreeMonths = new Date();
|
|
lastThreeMonths.setMonth(lastThreeMonths.getMonth() - 3);
|
|
|
|
// Aggregate energy to get sorted list of loadpoints/vehicles for coloring
|
|
const aggregateEnergy = (group) => {
|
|
return this.sessionsWithDefaults.reduce((acc, session) => {
|
|
if (new Date(session.created) >= lastThreeMonths) {
|
|
const key = session[group];
|
|
acc[key] = (acc[key] || 0) + session.chargedEnergy;
|
|
}
|
|
return acc;
|
|
}, {});
|
|
};
|
|
|
|
// Assign colors based on energy usage
|
|
const assignColors = (energyAggregation, colorType) => {
|
|
const result = {};
|
|
let colorIndex = 0;
|
|
|
|
// Assign colors by used energy in the last three months
|
|
const sortedEntries = Object.entries(energyAggregation).sort((a, b) => b[1] - a[1]);
|
|
sortedEntries.forEach(([key]) => {
|
|
if (!result[key]) {
|
|
result[key] = colors.palette[colorIndex % colors.palette.length];
|
|
colorIndex++;
|
|
}
|
|
});
|
|
|
|
// Assign colors to remaining entries
|
|
this.sessionsWithDefaults.forEach((session) => {
|
|
const key = session[colorType];
|
|
if (!result[key]) {
|
|
result[key] = colors.palette[colorIndex % colors.palette.length];
|
|
colorIndex++;
|
|
}
|
|
});
|
|
|
|
return result;
|
|
};
|
|
|
|
const loadpointEnergy = aggregateEnergy("loadpoint");
|
|
const loadpointColors = assignColors(loadpointEnergy, "loadpoint");
|
|
|
|
const vehicleEnergy = aggregateEnergy("vehicle");
|
|
const vehicleColors = assignColors(vehicleEnergy, "vehicle");
|
|
|
|
const solar = { self: colors.self, grid: colors.grid };
|
|
const cost = { price: colors.price, co2: colors.co2 };
|
|
|
|
return {
|
|
loadpoint: loadpointColors,
|
|
vehicle: vehicleColors,
|
|
solar,
|
|
cost,
|
|
};
|
|
},
|
|
groupIcons() {
|
|
return {
|
|
[GROUPS.NONE]: TotalIcon,
|
|
[GROUPS.LOADPOINT]: "shopicon-regular-cablecharge",
|
|
[GROUPS.VEHICLE]: "shopicon-regular-car3",
|
|
};
|
|
},
|
|
typeIcons() {
|
|
return {
|
|
[TYPES.SOLAR]: "shopicon-regular-sun",
|
|
[TYPES.PRICE]: DynamicPriceIcon,
|
|
[TYPES.CO2]: "shopicon-regular-eco1",
|
|
};
|
|
},
|
|
costTypeIcons() {
|
|
return {
|
|
[TYPES.PRICE]: DynamicPriceIcon,
|
|
[TYPES.CO2]: "shopicon-regular-eco1",
|
|
};
|
|
},
|
|
showTable() {
|
|
return this.period === PERIODS.MONTH;
|
|
},
|
|
showMonthNavigation() {
|
|
return this.period === PERIODS.MONTH;
|
|
},
|
|
showYearNavigation() {
|
|
return [PERIODS.MONTH, PERIODS.YEAR].includes(this.period);
|
|
},
|
|
showDateNavigator() {
|
|
return this.showMonthNavigation || this.showYearNavigation;
|
|
},
|
|
groupEntriesAvailable() {
|
|
if (this.selectedGroup === GROUPS.NONE || !this.currentSessions.length) return false;
|
|
return new Set(this.currentSessions.map((s) => s[this.selectedGroup])).size > 1;
|
|
},
|
|
showSolarYearChart() {
|
|
return this.period !== PERIODS.MONTH && this.selectedGroup === GROUPS.NONE;
|
|
},
|
|
showExtraCharts() {
|
|
const hasMultipleEntries =
|
|
new Set(this.currentTypeSessions.map((s) => s[this.selectedGroup])).size > 1;
|
|
const isGrouped = [GROUPS.LOADPOINT, GROUPS.VEHICLE].includes(this.selectedGroup);
|
|
const isSolar = this.activeType === TYPES.SOLAR;
|
|
const isNotMonth = this.period !== PERIODS.MONTH;
|
|
|
|
return (isGrouped && hasMultipleEntries) || (isSolar && isNotMonth && !isGrouped);
|
|
},
|
|
suggestedMaxPrice() {
|
|
// returns the 90th percentile of all prices
|
|
const sessionsWithPrice = this.sessions.filter((s) => s.pricePerKWh !== null);
|
|
const prices = sessionsWithPrice.map((s) => s.pricePerKWh);
|
|
return this.percentile(prices, 90);
|
|
},
|
|
suggestedMaxCo2() {
|
|
// returns the 90th percentile of all co2 emissions
|
|
const sessionsWithCo2 = this.sessions.filter((s) => s.co2PerKWh !== null);
|
|
const co2 = sessionsWithCo2.map((s) => s.co2PerKWh);
|
|
return this.percentile(co2, 90);
|
|
},
|
|
suggestedMaxCost() {
|
|
return this.activeType === TYPES.PRICE ? this.suggestedMaxPrice : this.suggestedMaxCo2;
|
|
},
|
|
},
|
|
watch: {
|
|
offline: function () {
|
|
this.loadSessions();
|
|
},
|
|
},
|
|
mounted() {
|
|
this.loadSessions();
|
|
},
|
|
methods: {
|
|
changePeriod(newPeriod) {
|
|
let month = this.month;
|
|
let year = this.year;
|
|
let period = newPeriod;
|
|
switch (period) {
|
|
case PERIODS.TOTAL:
|
|
month = undefined;
|
|
year = undefined;
|
|
break;
|
|
case PERIODS.YEAR:
|
|
month = undefined;
|
|
break;
|
|
default:
|
|
period = undefined;
|
|
}
|
|
this.$router.push({ query: { ...this.$route.query, period, month, year } });
|
|
},
|
|
async loadSessions() {
|
|
const response = await api.get("sessions");
|
|
// ensure sessions are sorted by created date
|
|
const sortedSessions = response.data?.result.sort((a, b) => {
|
|
return new Date(a.created) - new Date(b.created);
|
|
});
|
|
this.sessions = sortedSessions;
|
|
},
|
|
showDetails(sessionId) {
|
|
this.selectedSessionId = sessionId;
|
|
const modal = Modal.getOrCreateInstance(document.getElementById("sessionDetailsModal"));
|
|
modal.show();
|
|
},
|
|
csvHrefLink(year, month) {
|
|
const params = new URLSearchParams({
|
|
format: "csv",
|
|
lang: this.$i18n?.locale,
|
|
});
|
|
if (year && month) {
|
|
params.append("year", year);
|
|
params.append("month", month);
|
|
}
|
|
return `./api/sessions?${params.toString()}`;
|
|
},
|
|
updateType(type) {
|
|
this.selectedType = type;
|
|
settings.sessionsType = type;
|
|
},
|
|
updateGroup(group) {
|
|
this.selectedGroup = group;
|
|
settings.sessionsGroup = group;
|
|
},
|
|
updateDate({ year, month }) {
|
|
this.$router.push({ query: { ...this.$route.query, year, month } });
|
|
},
|
|
percentile(arr, p) {
|
|
if (arr.length === 0) return null;
|
|
const sorted = arr.sort((a, b) => a - b);
|
|
const index = (p / 100) * (sorted.length - 1);
|
|
return sorted[Math.floor(index)];
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.header-outer {
|
|
--vertical-shift: 0rem;
|
|
left: 0;
|
|
right: 0;
|
|
margin: 0 calc(calc(1.5rem + var(--vertical-shift)) * -1);
|
|
-webkit-backdrop-filter: blur(35px);
|
|
backdrop-filter: blur(35px);
|
|
background-color: #0000;
|
|
box-shadow: 0 1px 8px 0px var(--evcc-background);
|
|
}
|
|
|
|
@media (min-width: 576px) {
|
|
.header-outer {
|
|
--vertical-shift: calc((100vw - 540px) / 2);
|
|
}
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.header-outer {
|
|
--vertical-shift: calc((100vw - 720px) / 2);
|
|
}
|
|
}
|
|
|
|
@media (min-width: 992px) {
|
|
.header-outer {
|
|
--vertical-shift: calc((100vw - 960px) / 2);
|
|
}
|
|
}
|
|
|
|
@media (min-width: 1200px) {
|
|
.header-outer {
|
|
--vertical-shift: calc((100vw - 1140px) / 2);
|
|
}
|
|
}
|
|
|
|
@media (min-width: 1400px) {
|
|
.header-outer {
|
|
--vertical-shift: calc((100vw - 1320px) / 2);
|
|
}
|
|
}
|
|
</style>
|