Sessions: make vehicle editable (#5844)
This commit is contained in:
parent
d1d882959e
commit
ad9a5d0fed
8 changed files with 211 additions and 113 deletions
|
|
@ -19,7 +19,7 @@
|
|||
></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<table class="table">
|
||||
<table class="table align-middle">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>
|
||||
|
|
@ -34,7 +34,22 @@
|
|||
{{ $t("sessions.vehicle") }}
|
||||
</th>
|
||||
<td>
|
||||
{{ session.vehicle }}
|
||||
<VehicleOptions
|
||||
:id="session.vehicle"
|
||||
class="options"
|
||||
:vehicles="vehicles"
|
||||
:is-unknown="false"
|
||||
@change-vehicle="changeVehicle"
|
||||
@remove-vehicle="removeVehicle"
|
||||
>
|
||||
<span class="flex-grow-1 text-truncate vehicle-name">
|
||||
{{
|
||||
session.vehicle
|
||||
? session.vehicle
|
||||
: $t("main.vehicle.unknown")
|
||||
}}
|
||||
</span>
|
||||
</VehicleOptions>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
@ -93,7 +108,7 @@
|
|||
type="button"
|
||||
class="btn btn-outline-danger"
|
||||
data-bs-dismiss="modal"
|
||||
@click="confirmRemoving()"
|
||||
@click="openRemoveConfirmationModal"
|
||||
>
|
||||
{{ $t("session.delete") }}
|
||||
</button>
|
||||
|
|
@ -140,42 +155,69 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from "bootstrap/js/dist/modal";
|
||||
import "@h2d2/shopicons/es/regular/checkmark";
|
||||
import formatter from "../mixins/formatter";
|
||||
import api from "../api";
|
||||
import { distanceUnit, distanceValue } from "../units";
|
||||
import formatter from "../mixins/formatter";
|
||||
import Modal from "bootstrap/js/dist/modal";
|
||||
import api from "../api";
|
||||
|
||||
import VehicleOptions from "./VehicleOptions.vue";
|
||||
|
||||
export default {
|
||||
name: "ChargingSessionModal",
|
||||
components: { VehicleOptions },
|
||||
mixins: [formatter],
|
||||
props: {
|
||||
session: Object,
|
||||
vehicles: [Object],
|
||||
},
|
||||
emits: ["session-deleted"],
|
||||
emits: ["session-changed"],
|
||||
methods: {
|
||||
openSessionDetailsModal() {
|
||||
const modal = Modal.getOrCreateInstance(document.getElementById("sessionDetailsModal"));
|
||||
modal.show();
|
||||
},
|
||||
confirmRemoving() {
|
||||
openRemoveConfirmationModal() {
|
||||
const modal = Modal.getOrCreateInstance(
|
||||
document.getElementById("deleteSessionConfirmationModal")
|
||||
);
|
||||
modal.show();
|
||||
},
|
||||
async removeSession() {
|
||||
formatKm: function (value) {
|
||||
return `${distanceValue(value)} ${distanceUnit()}`;
|
||||
},
|
||||
async changeVehicle(index) {
|
||||
await this.updateSession({
|
||||
vehicle: this.vehicles[index - 1].title,
|
||||
});
|
||||
},
|
||||
async removeVehicle() {
|
||||
await this.updateSession({
|
||||
vehicle: null,
|
||||
});
|
||||
},
|
||||
async updateSession(data) {
|
||||
try {
|
||||
await api.delete("sessions/" + this.session.id);
|
||||
this.$emit("session-deleted");
|
||||
await api.put("session/" + this.session.id, data);
|
||||
this.$emit("session-changed");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
formatKm: function (value) {
|
||||
return `${distanceValue(value)} ${distanceUnit()}`;
|
||||
async removeSession() {
|
||||
try {
|
||||
await api.delete("session/" + this.session.id);
|
||||
this.$emit("session-changed");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
||||
<style scoped>
|
||||
.options .vehicle-name {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@
|
|||
v-for="(session, id) in loadpoint.sessions"
|
||||
:key="id"
|
||||
role="button"
|
||||
@click="showDetails(session)"
|
||||
@click="showDetails(session.id)"
|
||||
>
|
||||
<td class="align-middle">
|
||||
{{ session.vehicle }}
|
||||
|
|
@ -118,7 +118,11 @@
|
|||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<ChargingSessionModal :session="selectedSession" @session-deleted="loadSessions" />
|
||||
<ChargingSessionModal
|
||||
:session="selectedSession"
|
||||
:vehicles="vehicles"
|
||||
@session-changed="loadSessions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -130,6 +134,7 @@ import "@h2d2/shopicons/es/bold/arrowback";
|
|||
import "@h2d2/shopicons/es/regular/trash";
|
||||
import formatter from "../mixins/formatter";
|
||||
import api from "../api";
|
||||
import store from "../store";
|
||||
import ChargingSessionModal from "../components/ChargingSessionModal.vue";
|
||||
|
||||
export default {
|
||||
|
|
@ -140,7 +145,7 @@ export default {
|
|||
notifications: Array,
|
||||
},
|
||||
data() {
|
||||
return { sessions: [], selectedSession: undefined };
|
||||
return { sessions: [], selectedSessionId: undefined };
|
||||
},
|
||||
computed: {
|
||||
sessionsByMonthAndLoadpoint() {
|
||||
|
|
@ -162,6 +167,14 @@ export default {
|
|||
return { month, loadpoints };
|
||||
});
|
||||
},
|
||||
vehicles() {
|
||||
return store.state.vehicles.map((v, index) => {
|
||||
return { id: index, title: v };
|
||||
});
|
||||
},
|
||||
selectedSession() {
|
||||
return this.sessions.find((s) => s.id == this.selectedSessionId);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadSessions();
|
||||
|
|
@ -210,8 +223,8 @@ export default {
|
|||
date.setFullYear(year);
|
||||
return this.fmtMonthYear(date);
|
||||
},
|
||||
showDetails(session) {
|
||||
this.selectedSession = session;
|
||||
showDetails(sessionId) {
|
||||
this.selectedSessionId = sessionId;
|
||||
const modal = Modal.getOrCreateInstance(document.getElementById("sessionDetailsModal"));
|
||||
modal.show();
|
||||
},
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
|
|
@ -20,8 +20,8 @@
|
|||
<meta name="theme-color" content="#020318" />
|
||||
|
||||
<title>evcc</title>
|
||||
<script type="module" crossorigin src="./assets/index.75753078.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.5861eb97.css">
|
||||
<script type="module" crossorigin src="./assets/index.724774dd.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.bbc121e2.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -102,7 +102,8 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, cache *util.Cache) {
|
|||
"residualpower": {[]string{"POST", "OPTIONS"}, "/residualpower/{value:[-0-9.]+}", floatHandler(site.SetResidualPower, site.GetResidualPower)},
|
||||
"tariff": {[]string{"GET"}, "/tariff/{tariff:[a-z]+}", tariffHandler(site)},
|
||||
"sessions": {[]string{"GET"}, "/sessions", sessionHandler},
|
||||
"sessions2": {[]string{"DELETE"}, "/sessions/{id:[0-9]+}", sessionHandler},
|
||||
"session1": {[]string{"PUT"}, "/session/{id:[0-9]+}", updateSessionHandler},
|
||||
"session2": {[]string{"DELETE"}, "/session/{id:[0-9]+}", deleteSessionHandler},
|
||||
"telemetry": {[]string{"GET"}, "/settings/telemetry", boolGetHandler(telemetry.Enabled)},
|
||||
"telemetry2": {[]string{"POST", "OPTIONS"}, "/settings/telemetry/{value:[a-z]+}", boolHandler(telemetry.Enable, telemetry.Enabled)},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -13,15 +12,11 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/core/db"
|
||||
"github.com/evcc-io/evcc/core/loadpoint"
|
||||
"github.com/evcc-io/evcc/core/site"
|
||||
"github.com/evcc-io/evcc/server/assets"
|
||||
dbserver "github.com/evcc-io/evcc/server/db"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/locale"
|
||||
"github.com/gorilla/mux"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
var ignoreState = []string{"releaseNotes"} // excessive size
|
||||
|
|
@ -75,17 +70,6 @@ func jsonError(w http.ResponseWriter, status int, err error) {
|
|||
jsonWrite(w, map[string]interface{}{"error": err.Error()})
|
||||
}
|
||||
|
||||
func csvResult(ctx context.Context, w http.ResponseWriter, res any) {
|
||||
w.Header().Set("Content-Type", "text/csv")
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="sessions.csv"`)
|
||||
|
||||
if ww, ok := res.(api.CsvWriter); ok {
|
||||
_ = ww.WriteCsv(ctx, w)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// pass converts a simple api without return value to api with nil error return value
|
||||
func pass[T any](f func(T)) func(T) error {
|
||||
return func(v T) error {
|
||||
|
|
@ -226,48 +210,6 @@ func tariffHandler(site site.API) http.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// sessionHandler returns the list of charging sessions
|
||||
func sessionHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if dbserver.Instance == nil {
|
||||
jsonError(w, http.StatusBadRequest, errors.New("database offline"))
|
||||
return
|
||||
}
|
||||
|
||||
var res db.Sessions
|
||||
|
||||
if r.Method == "DELETE" {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
if txn := dbserver.Instance.Delete(&res, id); txn.Error != nil {
|
||||
jsonError(w, http.StatusBadRequest, txn.Error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if txn := dbserver.Instance.Where("charged_kwh>=0.05").Order("created desc").Find(&res); txn.Error != nil {
|
||||
jsonError(w, http.StatusInternalServerError, txn.Error)
|
||||
return
|
||||
}
|
||||
|
||||
if r.URL.Query().Get("format") == "csv" {
|
||||
lang := r.URL.Query().Get("lang")
|
||||
if lang == "" {
|
||||
// get request language
|
||||
lang = r.Header.Get("Accept-Language")
|
||||
if tags, _, err := language.ParseAcceptLanguage(lang); err == nil && len(tags) > 0 {
|
||||
lang = tags[0].String()
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), locale.Locale, lang)
|
||||
csvResult(ctx, w, &res)
|
||||
return
|
||||
}
|
||||
|
||||
jsonResult(w, res)
|
||||
}
|
||||
|
||||
// chargeModeHandler updates charge mode
|
||||
func chargeModeHandler(lp loadpoint.API) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
|||
100
server/http_session_handler.go
Normal file
100
server/http_session_handler.go
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/core/db"
|
||||
dbserver "github.com/evcc-io/evcc/server/db"
|
||||
"github.com/evcc-io/evcc/util/locale"
|
||||
"github.com/gorilla/mux"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
func csvResult(ctx context.Context, w http.ResponseWriter, res any) {
|
||||
w.Header().Set("Content-Type", "text/csv")
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="sessions.csv"`)
|
||||
|
||||
if ww, ok := res.(api.CsvWriter); ok {
|
||||
_ = ww.WriteCsv(ctx, w)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// sessionHandler returns the list of charging sessions
|
||||
func sessionHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if dbserver.Instance == nil {
|
||||
jsonError(w, http.StatusBadRequest, errors.New("database offline"))
|
||||
return
|
||||
}
|
||||
|
||||
var res db.Sessions
|
||||
|
||||
if txn := dbserver.Instance.Table("sessions").Where("charged_kwh>=0.05").Order("created desc").Find(&res); txn.Error != nil {
|
||||
jsonError(w, http.StatusInternalServerError, txn.Error)
|
||||
return
|
||||
}
|
||||
|
||||
if r.URL.Query().Get("format") == "csv" {
|
||||
lang := r.URL.Query().Get("lang")
|
||||
if lang == "" {
|
||||
// get request language
|
||||
lang = r.Header.Get("Accept-Language")
|
||||
if tags, _, err := language.ParseAcceptLanguage(lang); err == nil && len(tags) > 0 {
|
||||
lang = tags[0].String()
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), locale.Locale, lang)
|
||||
csvResult(ctx, w, &res)
|
||||
return
|
||||
}
|
||||
|
||||
jsonResult(w, res)
|
||||
}
|
||||
|
||||
// deleteSessionHandler removes session in sessions table with given id
|
||||
func deleteSessionHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if dbserver.Instance == nil {
|
||||
jsonError(w, http.StatusBadRequest, errors.New("database offline"))
|
||||
return
|
||||
}
|
||||
|
||||
var res db.Sessions
|
||||
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
if txn := dbserver.Instance.Table("sessions").Delete(&res, id); txn.Error != nil {
|
||||
jsonError(w, http.StatusBadRequest, txn.Error)
|
||||
return
|
||||
}
|
||||
|
||||
jsonResult(w, res)
|
||||
}
|
||||
|
||||
// updateSessionHandler updates the data of an existing session
|
||||
func updateSessionHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if dbserver.Instance == nil {
|
||||
jsonError(w, http.StatusBadRequest, errors.New("database offline"))
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
var session map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&session); err != nil {
|
||||
jsonError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
if txn := dbserver.Instance.Table("sessions").Where("id = ?", id).Updates(&session); txn.Error != nil {
|
||||
jsonError(w, http.StatusBadRequest, txn.Error)
|
||||
return
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue