Compare commits

...

50 Commits

Author SHA1 Message Date
Alf Sebastian Houge
440913af9c Merge pull request #26 from AlfHou/fix/clean-vscode-files
Remove .vscode specific files from being tracked
2023-01-30 22:46:57 +01:00
Alf Sebastian Houge
66d01afe6e Merge pull request #25 from AlfHou/helm
Add notes about Helm/Kubernetes
2023-01-30 22:39:39 +01:00
Alf Sebastian Houge
ad4a399dc8 Remove .vscode specific files 2023-01-30 22:18:36 +01:00
Jonathan Gazeley
2137bf7702 Add notes about Helm/Kubernetes 2023-01-30 21:15:41 +00:00
Alf Sebastian Houge
47bdf7b505 Merge pull request #24 from AlfHou/fix/translation-fixes
Fix 'engine size' field's label
2023-01-30 22:11:36 +01:00
Alf Sebastian Houge
669bffa955 Fix wrong translation for engine size field 2023-01-30 22:06:47 +01:00
Alf Sebastian Houge
05c5381a06 Remove ui .vscode directory 2023-01-30 22:03:23 +01:00
Alf Sebastian Houge
e623e3ad1a Ignore .vscode directory in git 2023-01-30 22:01:19 +01:00
Alf Sebastian Houge
c43a2f639a Merge pull request #23 from AlfHou/update-gitignore
Add vscode specific files to gitignore
2023-01-30 21:56:01 +01:00
Alf Sebastian Houge
e66e5b7724 Merge pull request #18 from solluh/master
Update de.json
2023-01-28 22:53:50 +01:00
Alf Sebastian Houge
adfd70fe98 Merge pull request #20 from AlfHou/feat/drivvo-import
Add importer for Drivvo
2023-01-28 22:51:32 +01:00
Alf Sebastian Houge
ebebcacdc9 Fix pricePerUnit parsing 2023-01-28 22:49:38 +01:00
Alf Sebastian Houge
3299c13181 Merge branch 'master' into feat/drivvo-import 2023-01-28 22:22:24 +01:00
Alf Sebastian Houge
2661f8ae36 Merge pull request #19 from AlfHou/feat/state_mileage_option
Add option in vehicle stats to choose what mileage metric to show
2023-01-28 22:20:21 +01:00
Alf Sebastian Houge
091cfdcc99 Merge branch 'master' into feat/state_mileage_option 2023-01-28 22:19:02 +01:00
solluh
9771dc4c25 Update de.json
Einige kleine Fehler korrigiert
2023-01-27 09:03:38 +01:00
Alf Sebastian Houge
8e894844a3 Merge pull request #13 from AlfHou/chore/docker-push
Publish new releases to docker repo
2023-01-26 23:11:32 +01:00
Alf Sebastian Houge
4a55879ad8 Update links in more info view in settings 2023-01-25 23:51:33 +01:00
Alf Sebastian Houge
9dab3d124d Point to this repo in ubuntu install docs 2023-01-25 23:43:12 +01:00
Alf Sebastian Houge
a89ca5e46a Update links in docker files to point to this repo 2023-01-25 23:42:43 +01:00
Alf Sebastian Houge
f96638d913 Update readme with the correct url's and remove some specifics from forked repo 2023-01-25 23:37:01 +01:00
Alf Sebastian Houge
08f2a3547e Rename docker secret variable 2023-01-25 23:37:01 +01:00
Alf Sebastian Houge
126aff7231 Update workflow action for publishing to new docker hub repo 2023-01-25 23:37:01 +01:00
Alf Sebastian Houge
ba276975f3 Merge pull request #5 from boerniee/i18n
Added translations
2023-01-25 23:02:17 +01:00
Alf Sebastian Houge
7d4b763e48 Merge branch 'master' into i18n 2023-01-25 23:00:25 +01:00
boerniee
ee964a630e Fixed another translations 2023-01-25 22:03:53 +01:00
boerniee
c588e34b2e translating fuelly import page 2023-01-25 21:52:44 +01:00
Bernhard Großer
6871a40380 added not translated texts, changed translations 2023-01-25 21:23:50 +01:00
Alf Sebastian Houge
0035897f21 Merge pull request #12 from AlfHou/bug/remove-npm-github-action
Remove Github Action for running npm tests
2023-01-24 23:08:19 +01:00
Alf Sebastian Houge
19680b1cc1 Remove the github action which ran npm tests 2023-01-24 22:59:59 +01:00
Alf Sebastian Houge
e6e90d9bef Merge pull request #9 from AlfHou/chore/update-package-lock
Update the package lock file
2023-01-22 23:09:37 +01:00
Alf Sebastian Houge
311ac7579a Update the package lock file 2023-01-22 23:05:40 +01:00
Alf Sebastian Houge
47810a8c88 Merge pull request #3 from AlfHou/add-vin
Add VIN field to vehicle data
2023-01-19 14:53:29 +01:00
Bernhard Großer
5aabeda6ba update axios version fixes #104 (#4) 2023-01-19 14:44:39 +01:00
boerniee
bb68c8c504 translate units 2023-01-13 23:51:40 +01:00
boerniee
961ec30065 set locale to browser locale 2023-01-13 23:51:32 +01:00
boerniee
0b450dc462 added translations
#80 #19
2023-01-13 23:51:19 +01:00
Alf Sebastian Houge
c0db2c5c1e Calculate mileage based on mileageOption 2022-10-15 20:18:26 +02:00
Alf Sebastian Houge
2ecb113918 Add mileage options to frontend 2022-10-15 20:17:41 +02:00
Alf Sebastian Houge
966cac280f Add utils for converting imperial and metric 2022-10-15 20:17:19 +02:00
Alf Sebastian Houge
2749707546 Change logic in how indexes of sections in csv work
Change the logic in how indexes of the different sections in drivvos csv work
2022-04-06 17:56:58 +02:00
Alf Sebastian Houge
f1bf36bcb9 Separate imports into provider specific files 2022-04-06 17:56:58 +02:00
Alf Sebastian Houge
3322e2f6bd Don't redirect from import page on erro 2022-04-06 17:56:58 +02:00
Alf Sebastian Houge
9ef929dbd5 Redirect to home after importing 2022-04-06 17:56:58 +02:00
Alf Sebastian Houge
dc33aaad49 Add option for not importing location 2022-04-06 17:56:58 +02:00
Alf Sebastian Houge
15cf09f326 Add notice about what fields aren't imported from drivvo 2022-04-06 17:56:58 +02:00
Alf Sebastian Houge
1e099ec8b6 Create db.Expense and db.Fillup in one place
Create Expense and Fillup in one place instead of throughout function
2022-04-06 17:56:58 +02:00
Alf Sebastian Houge
e8f7815d8d Select vehicle when importing drivvo csv.
Select what vehicle to import for on frontend. Actually import the data to the db
2022-04-06 17:56:58 +02:00
Alf Sebastian Houge
bfaebf17d0 Add functions for parsing drivvo CSVs. 2022-04-06 17:56:58 +02:00
Alf Sebastian Houge
d314ed4a16 Add vscode specific files to gitignore 2022-03-05 19:21:10 +01:00
55 changed files with 41622 additions and 13060 deletions

View File

@@ -33,13 +33,13 @@ jobs:
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Login to GitHub
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.CR_PAT }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
with:
@@ -52,7 +52,7 @@ jobs:
# cache-from: type=local,src=/tmp/.buildx-cache
# cache-to: type=local,dest=/tmp/.buildx-cache
tags: |
akhilrex/hammond:latest
akhilrex/hammond:${{ steps.get_tag.outputs.TAG }}
ghcr.io/akhilrex/hammond:latest
ghcr.io/akhilrex/hammond:${{ steps.get_tag.outputs.TAG }}
alfhou/hammond:latest
alfhou/hammond:${{ steps.get_tag.outputs.TAG }}
ghcr.io/alfhou/hammond:latest
ghcr.io/alfhou/hammond:${{ steps.get_tag.outputs.TAG }}

View File

@@ -1,22 +0,0 @@
name: Test UI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x, 14.x, 15.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
working-directory: ui
- run: npm run build --if-present
working-directory: ui
- run: npm test
working-directory: ui

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
# Don't track .vscode directory
.vscode
!.vscode/launch.json

View File

@@ -18,7 +18,7 @@ RUN npm run build
FROM alpine:latest
LABEL org.opencontainers.image.source="https://github.com/akhilrex/hammond"
LABEL org.opencontainers.image.source="https://github.com/alfhou/hammond"
ENV CONFIG=/config
ENV DATA=/assets
ENV UID=998

View File

@@ -1,26 +1,16 @@
[![Contributors][contributors-shield]][contributors-url] [![Forks][forks-shield]][forks-url] [![Stargazers][stars-shield]][stars-url] [![Issues][issues-shield]][issues-url] [![MIT License][license-shield]][license-url] [![LinkedIn][linkedin-shield]][linkedin-url]
<!-- PROJECT LOGO -->
<br />
<p align="center">
<!-- <a href="https://github.com/akhilrex/hammond">
<img src="images/logo.png" alt="Logo" width="80" height="80">
</a> -->
<h1 align="center" style="margin-bottom:0">Hammond</h1>
<p align="center">Current Version - 2022.07.06</p>
<p align="center">
A self-hosted vehicle expense tracking system with support for multiple users.
<br />
<a href="https://github.com/akhilrex/hammond"><strong>Explore the docs »</strong></a>
<a href="https://github.com/AlfHou/hammond"><strong>Explore the docs »</strong></a>
<br />
<br />
<!-- <a href="https://github.com/akhilrex/hammond">View Demo</a>
· -->
<a href="https://github.com/akhilrex/hammond/issues">Report Bug</a>
<a href="https://github.com/AlfHou/hammond/issues">Report Bug</a>
·
<a href="https://github.com/akhilrex/hammond/issues">Request Feature</a>
<a href="https://github.com/AlfHou/hammond/issues">Request Feature</a>
·
<a href="Screenshots.md">Screenshots</a>
</p>
@@ -44,18 +34,22 @@
## About The Project
Hammond is a self hosted vehicle management system to track fuel and other expenses related to all of your vehicles. It supports multiple users sharing multiple vehicles. It is the logical successor to Clarkson which has not been updated for quite some time now.
Hammond is a self hosted vehicle management system to track fuel and other
expenses related to all of your vehicles.
It supports multiple users sharing multiple vehicles.
It is the logical successor to Clarkson which has not been updated for quite some time now.
This repo is again a fork of akhilrex's great [project](https://github.com/akhilrex/hammond).
_Developers Note: This project is under active development which means I release new updates very frequently. It is recommended that you use something like [watchtower](https://github.com/containrrr/watchtower) which will automatically update your containers whenever I release a new version or periodically rebuild the container with the latest image manually._
__Also check out my other self-hosted, open-source solution - [Podgrab](https://github.com/akhilrex/podgrab) - Podcast download and archive manager and player.__
### Motivation and Developer Notes
I was looking for a fuel tracking system and stumbled upon Clarkson. Although it did most of what I needed it has not been updated for quite a lot of time. Since I had some bandwidth available as my previous open source project [Podgrab](http://github.com/akhilrex/podgrab) had become quite stable now, my first thought was to contribute to the Clarkson project only. I soon realized that the architecture that Clarkson had used was not really be that extensible now and would warrant a complete rewrite only. So I decided to build Hammond - The successor to Clarkson.
As mentioned, this project is a fork of
akhilrex's [project](https://github.com/akhilrex/hammond) which is no longer active.
To prevent the same from happeing to this project, we are seeking to add more
maintainers/collaborators who have access to merge PRs.
The current version of Hammond is written using GO for backend and Vuejs for the front end. Originally I had thought of using the same tech stack for both frontend and the backend so that it became easier for users and other developers to use, deploy and contribute. Which is why the first version of Hammond has a NestJS backend complete with all the bells and whistles (GraphQL, Prisma and what nots). But I eventually decided to rebuild the backend in GO just to keep the container size small. No matter how much you can optimize the sheer size of the node_modules will always add bulk to your containers. I host all my tools on my Raspberry Pi. It only makes sense to keep the container size as small as possible.
We are trying our best to update with new features and feedback is very welcome.
Also I had initially thought of a 2 container approach (1 for backend and 1 for the frontend) so that they can be independently maintained and updated. I eventually decided against this idea for the sake of simplicity. Although it is safe to assume that most self-hosters are fairly tech capable it still is much better to have a single container that you can fire and forget.
The project is written using Go for the backend and Vuejs for the front end.
![Product Name Screen Shot][product-screenshot] [More Screenshots](Screenshots.md)
@@ -79,7 +73,7 @@ Also I had initially thought of a 2 container approach (1 for backend and 1 for
- Save attachment against vehicles
- Quick Entries (take a photo of a receipt or pump screen to make entry later)
- Vehicle level and overall reporting
- Import from Fuelly (more apps coming soon)
- Import from Fuelly and Drivvo
## Installation
@@ -90,24 +84,25 @@ The easiest way to run Hammond is to run it as a docker container.
Simple setup without mounted volumes (for testing and evaluation)
```sh
docker run -d -p 3000:3000 --name=hammond akhilrex/hammond
docker run -d -p 3000:3000 --name=hammond alfhou/hammond
```
Binding local volumes to the container
```sh
docker run -d -p 3000:3000 --name=hammond -v "/host/path/to/assets:/assets" -v "/host/path/to/config:/config" akhilrex/hammond
docker run -d -p 3000:3000 --name=hammond -v "/host/path/to/assets:/assets" -v "/host/path/to/config:/config" alfhou/hammond
```
### Using Docker-Compose
Modify the docker compose file provided [here](https://github.com/akhilrex/hammond/blob/master/docker-compose.yml) to update the volume and port binding and run the following command
Modify the docker compose file provided [here](https://github.com/alfhou/hammond/blob/master/docker-compose.yml)
to update the volume and port binding and run the following command
```yaml
version: '2.1'
services:
hammond:
image: akhilrex/hammond
image: alfhou/hammond
container_name: hammond
volumes:
- /path/to/config:/config
@@ -121,9 +116,27 @@ services:
docker-compose up -d
```
### Install on Kubernetes
You can install Hammond on Kubernetes by using Helm. The
[Helm chart for Hammond](https://github.com/djjudas21/charts/tree/main/charts/hammond)
is maintained by djjudas21.
Check out the default [`values.yaml`](https://github.com/djjudas21/charts/blob/main/charts/hammond/values.yaml)
to see what you can override.
```console
helm repo add djjudas21 https://djjudas21.github.io/charts/
helm repo update djjudas21
helm install djjudas21/hammond
```
### Build from Source / Ubuntu Installation
Although personally I feel that using the docker container is the best way of using and enjoying something like hammond, a lot of people in the community are still not comfortable with using Docker and wanted to host it natively on their Linux servers. Follow the link below to get a guide on how to build hammond from source.
Although personally I feel that using the docker container is the best way of using
and enjoying something like hammond, a lot of people in the community are still not
comfortable with using Docker and wanted to host it natively on their Linux servers.
Follow the link below to get a guide on how to build hammond from source.
[Build from source / Ubuntu Guide](docs/ubuntu-install.md)
@@ -197,25 +210,6 @@ Distributed under the GPL-3.0 License. See `LICENSE` for more information.
## Contact
Akhil Gupta - [@akhilrex](https://twitter.com/akhilrex)
Project Link: [https://github.com/AlfHou/hammond](https://github.com/AlfHou/hammond)
Project Link: [https://github.com/akhilrex/hammond](https://github.com/akhilrex/hammond)
<a href="https://www.buymeacoffee.com/akhilrex" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="width: 217px !important;height: 60px !important;" ></a>
<!-- MARKDOWN LINKS & IMAGES -->
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
[contributors-shield]: https://img.shields.io/github/contributors/akhilrex/hammond.svg?style=flat-square
[contributors-url]: https://github.com/akhilrex/hammond/graphs/contributors
[forks-shield]: https://img.shields.io/github/forks/akhilrex/hammond.svg?style=flat-square
[forks-url]: https://github.com/akhilrex/hammond/network/members
[stars-shield]: https://img.shields.io/github/stars/akhilrex/hammond.svg?style=flat-square
[stars-url]: https://github.com/akhilrex/hammond/stargazers
[issues-shield]: https://img.shields.io/github/issues/akhilrex/hammond.svg?style=flat-square
[issues-url]: https://github.com/akhilrex/hammond/issues
[license-shield]: https://img.shields.io/github/license/akhilrex/hammond.svg?style=flat-square
[license-url]: https://github.com/akhilrex/hammond/blob/master/LICENSE
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=flat-square&logo=linkedin&colorB=555
[linkedin-url]: https://linkedin.com/in/akhilrex
[product-screenshot]: images/screenshot.jpg

View File

@@ -1,7 +1,7 @@
version: "2.1"
services:
hammond:
image: akhilrex/hammond
image: alfhou/hammond
container_name: hammond
environment:
- JWT_SECRET=somethingverystrong

View File

@@ -26,7 +26,7 @@ Following steps will only work if Go and Node are installed and configured prope
## Clone from Git
``` bash
git clone --depth 1 https://github.com/akhilrex/hammond
git clone --depth 1 https://github.com/alfhou/hammond
```
## Build and Copy dependencies
@@ -110,7 +110,7 @@ sudo systemctl stop hammond.service
## Clone from Git
``` bash
git clone --depth 1 https://github.com/akhilrex/hammond
git clone --depth 1 https://github.com/alfhou/hammond
```
## Build and Copy dependencies

3
server/.gitignore vendored
View File

@@ -14,6 +14,7 @@
# MS VSCode
.vscode
!.vscode/launch.json
__debug_bin
# Dependency directories (remove the comment below to include it)
@@ -22,4 +23,4 @@ assets/*
keys/*
backups/*
nodemon.json
dist/*
dist/*

View File

@@ -16,7 +16,7 @@ RUN go build -o ./app ./main.go
FROM alpine:latest
LABEL org.opencontainers.image.source="https://github.com/akhilrex/hammond"
LABEL org.opencontainers.image.source="https://github.com/alfhou/hammond"
ENV CONFIG=/config
ENV DATA=/assets
@@ -38,4 +38,4 @@ COPY dist ./dist
EXPOSE 3000
ENTRYPOINT ["./app"]
ENTRYPOINT ["./app"]

View File

@@ -25,6 +25,33 @@ func RandString(n int) string {
return string(b)
}
// A helper to convert from litres to gallon
func LitreToGallon(litres float32) float32 {
gallonConversionFactor := 0.21997
return litres * float32(gallonConversionFactor);
}
// A helper to convert from gallon to litres
func GallonToLitre(gallons float32) float32 {
litreConversionFactor := 3.785412
return gallons * float32(litreConversionFactor);
}
// A helper to convert from km to miles
func KmToMiles(km float32) float32 {
kmConversionFactor := 0.62137119
return km * float32(kmConversionFactor);
}
// A helper to convert from miles to km
func MilesToKm(miles float32) float32 {
milesConversionFactor := 1.609344
return miles * float32(milesConversionFactor);
}
// A Util function to generate jwt_token which can be used in the request header
func GenToken(id string, role db.Role) (string, string) {
jwt_token := jwt.New(jwt.GetSigningMethod("HS256"))

View File

@@ -115,7 +115,7 @@ func userLogin(c *gin.Context) {
Email: user.Email,
Token: token,
RefreshToken: refreshToken,
Role: user.RoleDetail().Long,
Role: user.RoleDetail().Key,
}
c.JSON(http.StatusOK, response)
}
@@ -149,7 +149,7 @@ func refresh(c *gin.Context) {
Email: user.Email,
Token: token,
RefreshToken: refreshToken,
Role: user.RoleDetail().Long,
Role: user.RoleDetail().Key,
}
c.JSON(http.StatusOK, response)
} else {

View File

@@ -2,6 +2,7 @@ package controllers
import (
"net/http"
"strconv"
"github.com/akhilrex/hammond/service"
"github.com/gin-gonic/gin"
@@ -9,6 +10,7 @@ import (
func RegisteImportController(router *gin.RouterGroup) {
router.POST("/import/fuelly", fuellyImport)
router.POST("/import/drivvo", drivvoImport)
}
func fuellyImport(c *gin.Context) {
@@ -24,3 +26,28 @@ func fuellyImport(c *gin.Context) {
}
c.JSON(http.StatusOK, gin.H{})
}
func drivvoImport(c *gin.Context) {
bytes, err := getFileBytes(c, "file")
if err != nil {
c.JSON(http.StatusUnprocessableEntity, err)
return
}
vehicleId := c.PostForm("vehicleID")
if vehicleId == "" {
c.JSON(http.StatusUnprocessableEntity, "Missing Vehicle ID")
return
}
importLocation, err := strconv.ParseBool(c.PostForm("importLocation"))
if err != nil {
c.JSON(http.StatusUnprocessableEntity, "Please include importLocation option.")
return
}
errors := service.DrivvoImport(bytes, c.MustGet("userId").(string), vehicleId, importLocation)
if len(errors) > 0 {
c.JSON(http.StatusUnprocessableEntity, gin.H{"errors": errors})
return
}
c.JSON(http.StatusOK, gin.H{})
}

View File

@@ -25,7 +25,7 @@ func getMileageForVehicle(c *gin.Context) {
return
}
fillups, err := service.GetMileageByVehicleId(searchByIdQuery.Id, model.Since)
fillups, err := service.GetMileageByVehicleId(searchByIdQuery.Id, model.Since, model.MileageOption)
if err != nil {
c.JSON(http.StatusUnprocessableEntity, common.NewError("getMileageForVehicle", err))
return

View File

@@ -52,75 +52,58 @@ const (
)
type EnumDetail struct {
Short string `json:"short"`
Long string `json:"long"`
Key string `json:"key"`
}
var FuelUnitDetails map[FuelUnit]EnumDetail = map[FuelUnit]EnumDetail{
LITRE: {
Short: "Lt",
Long: "Litre",
Key: "litre",
},
GALLON: {
Short: "Gal",
Long: "Gallon",
Key: "gallon",
}, KILOGRAM: {
Short: "Kg",
Long: "Kilogram",
Key: "kilogram",
}, KILOWATT_HOUR: {
Short: "KwH",
Long: "Kilowatt Hour",
Key: "kilowatthour",
}, US_GALLON: {
Short: "US Gal",
Long: "US Gallon",
Key: "usgallon",
},
MINUTE: {
Short: "Mins",
Long: "Minutes",
Key: "minutes",
},
}
var FuelTypeDetails map[FuelType]EnumDetail = map[FuelType]EnumDetail{
PETROL: {
Short: "Petrol",
Long: "Petrol",
Key: "petrol",
},
DIESEL: {
Short: "Diesel",
Long: "Diesel",
Key: "diesel",
}, CNG: {
Short: "CNG",
Long: "CNG",
Key: "cng",
}, LPG: {
Short: "LPG",
Long: "LPG",
Key: "lpg",
}, ELECTRIC: {
Short: "Electric",
Long: "Electric",
Key: "electric",
}, ETHANOL: {
Short: "Ethanol",
Long: "Ethanol",
Key: "ethanol",
},
}
var DistanceUnitDetails map[DistanceUnit]EnumDetail = map[DistanceUnit]EnumDetail{
KILOMETERS: {
Short: "Km",
Long: "Kilometers",
Key: "kilometers",
},
MILES: {
Short: "Mi",
Long: "Miles",
Key: "miles",
},
}
var RoleDetails map[Role]EnumDetail = map[Role]EnumDetail{
ADMIN: {
Short: "Admin",
Long: "ADMIN",
Key: "ADMIN",
},
USER: {
Short: "User",
Long: "USER",
Key: "USER",
},
}

View File

@@ -14,7 +14,7 @@ type MileageModel struct {
FuelQuantity float32 `form:"fuelQuantity" json:"fuelQuantity" binding:"required"`
PerUnitPrice float32 `form:"perUnitPrice" json:"perUnitPrice" binding:"required"`
Currency string `json:"currency"`
DistanceUnit db.DistanceUnit `form:"distanceUnit" json:"distanceUnit"`
Mileage float32 `form:"mileage" json:"mileage" binding:"mileage"`
CostPerMile float32 `form:"costPerMile" json:"costPerMile" binding:"costPerMile"`
OdoReading int `form:"odoReading" json:"odoReading" binding:"odoReading"`
@@ -35,4 +35,5 @@ func (b *MileageModel) MarshalJSON() ([]byte, error) {
type MileageQueryModel struct {
Since time.Time `json:"since" query:"since" form:"since"`
MileageOption string `json:"mileageOption" query:"mileageOption" form:"mileageOption"`
}

View File

@@ -0,0 +1,142 @@
package service
import (
"bytes"
"encoding/csv"
"fmt"
"strconv"
"strings"
"time"
"github.com/akhilrex/hammond/db"
)
func DrivvoParseExpenses(content []byte, user *db.User, vehicle *db.Vehicle) ([]db.Expense, []string) {
expenseReader := csv.NewReader(bytes.NewReader(content))
expenseReader.Comment = '#'
// Read headers (there is a trailing comma at the end, that's why we have to read the first line)
expenseReader.Read()
expenseReader.FieldsPerRecord = 6
expenseRecords, err := expenseReader.ReadAll()
var errors []string
if err != nil {
errors = append(errors, err.Error())
println(err.Error())
return nil, errors
}
var expenses []db.Expense
for index, record := range expenseRecords {
date, err := time.Parse("2006-01-02 15:04:05", record[1])
if err != nil {
errors = append(errors, "Found an invalid date/time at service/expense row "+strconv.Itoa(index+1))
}
totalCost, err := strconv.ParseFloat(record[2], 32)
if err != nil {
errors = append(errors, "Found and invalid total cost at service/expense row "+strconv.Itoa(index+1))
}
odometer, err := strconv.Atoi(record[0])
if err != nil {
errors = append(errors, "Found an invalid odometer reading at service/expense row "+strconv.Itoa(index+1))
}
notes := fmt.Sprintf("Location: %s\nNotes: %s\n", record[4], record[5])
expenses = append(expenses, db.Expense{
UserID: user.ID,
VehicleID: vehicle.ID,
Date: date,
OdoReading: odometer,
Amount: float32(totalCost),
ExpenseType: record[3],
Currency: user.Currency,
DistanceUnit: user.DistanceUnit,
Comments: notes,
Source: "Drivvo",
})
}
return expenses, errors
}
func DrivvoParseRefuelings(content []byte, user *db.User, vehicle *db.Vehicle, importLocation bool) ([]db.Fillup, []string) {
refuelingReader := csv.NewReader(bytes.NewReader(content))
refuelingReader.Comment = '#'
refuelingRecords, err := refuelingReader.ReadAll()
var errors []string
if err != nil {
errors = append(errors, err.Error())
println(err.Error())
return nil, errors
}
var fillups []db.Fillup
for index, record := range refuelingRecords {
// Skip column titles
if index == 0 {
continue
}
date, err := time.Parse("2006-01-02 15:04:05", record[1])
if err != nil {
errors = append(errors, "Found an invalid date/time at refuel row "+strconv.Itoa(index+1))
}
totalCost, err := strconv.ParseFloat(record[4], 32)
if err != nil {
errors = append(errors, "Found and invalid total cost at refuel row "+strconv.Itoa(index+1))
}
odometer, err := strconv.Atoi(record[0])
if err != nil {
errors = append(errors, "Found an invalid odometer reading at refuel row "+strconv.Itoa(index+1))
}
location := ""
if importLocation {
location = record[17]
}
pricePerUnit, err := strconv.ParseFloat(record[3], 32)
if err != nil {
unit := strings.ToLower(db.FuelUnitDetails[vehicle.FuelUnit].Key)
errors = append(errors, fmt.Sprintf("Found an invalid cost per %s at refuel row %d", unit, index+1))
}
quantity, err := strconv.ParseFloat(record[5], 32)
if err != nil {
errors = append(errors, "Found an invalid quantity at refuel row "+strconv.Itoa(index+1))
}
isTankFull := record[6] == "Yes"
// Unfortunatly, drivvo doesn't expose this info in their export
fal := false
notes := fmt.Sprintf("Reason: %s\nNotes: %s\nFuel: %s\n", record[18], record[19], record[2])
fillups = append(fillups, db.Fillup{
VehicleID: vehicle.ID,
UserID: user.ID,
Date: date,
HasMissedFillup: &fal,
IsTankFull: &isTankFull,
FuelQuantity: float32(quantity),
PerUnitPrice: float32(pricePerUnit),
FillingStation: location,
OdoReading: odometer,
TotalAmount: float32(totalCost),
FuelUnit: vehicle.FuelUnit,
Currency: user.Currency,
DistanceUnit: user.DistanceUnit,
Comments: notes,
Source: "Drivvo",
})
}
return fillups, errors
}

View File

@@ -0,0 +1,140 @@
package service
import (
"bytes"
"encoding/csv"
"fmt"
"strconv"
"time"
"github.com/akhilrex/hammond/db"
"github.com/leekchan/accounting"
)
func FuellyParseAll(content []byte, userId string) ([]db.Fillup, []db.Expense, []string) {
stream := bytes.NewReader(content)
reader := csv.NewReader(stream)
records, err := reader.ReadAll()
var errors []string
user, err := GetUserById(userId)
if err != nil {
errors = append(errors, err.Error())
return nil, nil, errors
}
vehicles, err := GetUserVehicles(userId)
if err != nil {
errors = append(errors, err.Error())
return nil, nil, errors
}
if err != nil {
errors = append(errors, err.Error())
return nil, nil, errors
}
var vehicleMap map[string]db.Vehicle = make(map[string]db.Vehicle)
for _, vehicle := range *vehicles {
vehicleMap[vehicle.Nickname] = vehicle
}
var fillups []db.Fillup
var expenses []db.Expense
layout := "2006-01-02 15:04"
altLayout := "2006-01-02 3:04 PM"
for index, record := range records {
if index == 0 {
continue
}
var vehicle db.Vehicle
var ok bool
if vehicle, ok = vehicleMap[record[4]]; !ok {
errors = append(errors, "Found an unmapped vehicle entry at row "+strconv.Itoa(index+1))
}
dateStr := record[2] + " " + record[3]
date, err := time.Parse(layout, dateStr)
if err != nil {
date, err = time.Parse(altLayout, dateStr)
}
if err != nil {
errors = append(errors, "Found an invalid date/time at row "+strconv.Itoa(index+1))
}
totalCostStr := accounting.UnformatNumber(record[9], 3, user.Currency)
totalCost64, err := strconv.ParseFloat(totalCostStr, 32)
if err != nil {
errors = append(errors, "Found an invalid total cost at row "+strconv.Itoa(index+1))
}
totalCost := float32(totalCost64)
odoStr := accounting.UnformatNumber(record[5], 0, user.Currency)
odoreading, err := strconv.Atoi(odoStr)
if err != nil {
errors = append(errors, "Found an invalid odo reading at row "+strconv.Itoa(index+1))
}
location := record[12]
//Create Fillup
if record[0] == "Gas" {
rateStr := accounting.UnformatNumber(record[7], 3, user.Currency)
ratet64, err := strconv.ParseFloat(rateStr, 32)
if err != nil {
errors = append(errors, "Found an invalid cost per gallon at row "+strconv.Itoa(index+1))
}
rate := float32(ratet64)
quantity64, err := strconv.ParseFloat(record[8], 32)
if err != nil {
errors = append(errors, "Found an invalid quantity at row "+strconv.Itoa(index+1))
}
quantity := float32(quantity64)
notes := fmt.Sprintf("Octane:%s\nGas Brand:%s\nLocation%s\nTags:%s\nPayment Type:%s\nTire Pressure:%s\nNotes:%s\nMPG:%s",
record[10], record[11], record[12], record[13], record[14], record[15], record[16], record[1],
)
isTankFull := record[6] == "Full"
fal := false
fillups = append(fillups, db.Fillup{
VehicleID: vehicle.ID,
FuelUnit: vehicle.FuelUnit,
FuelQuantity: quantity,
PerUnitPrice: rate,
TotalAmount: totalCost,
OdoReading: odoreading,
IsTankFull: &isTankFull,
Comments: notes,
FillingStation: location,
HasMissedFillup: &fal,
UserID: userId,
Date: date,
Currency: user.Currency,
DistanceUnit: user.DistanceUnit,
Source: "Fuelly",
})
}
if record[0] == "Service" {
notes := fmt.Sprintf("Tags:%s\nPayment Type:%s\nNotes:%s",
record[13], record[14], record[16],
)
expenses = append(expenses, db.Expense{
VehicleID: vehicle.ID,
Amount: totalCost,
OdoReading: odoreading,
Comments: notes,
ExpenseType: record[17],
UserID: userId,
Currency: user.Currency,
Date: date,
DistanceUnit: user.DistanceUnit,
Source: "Fuelly",
})
}
}
return fillups, expenses, errors
}

View File

@@ -2,144 +2,12 @@ package service
import (
"bytes"
"encoding/csv"
"fmt"
"strconv"
"time"
"github.com/akhilrex/hammond/db"
"github.com/leekchan/accounting"
)
func FuellyImport(content []byte, userId string) []string {
stream := bytes.NewReader(content)
reader := csv.NewReader(stream)
records, err := reader.ReadAll()
func WriteToDB(fillups []db.Fillup, expenses []db.Expense) []string {
var errors []string
if err != nil {
errors = append(errors, err.Error())
return errors
}
vehicles, err := GetUserVehicles(userId)
if err != nil {
errors = append(errors, err.Error())
return errors
}
user, err := GetUserById(userId)
if err != nil {
errors = append(errors, err.Error())
return errors
}
var vehicleMap map[string]db.Vehicle = make(map[string]db.Vehicle)
for _, vehicle := range *vehicles {
vehicleMap[vehicle.Nickname] = vehicle
}
var fillups []db.Fillup
var expenses []db.Expense
layout := "2006-01-02 15:04"
altLayout := "2006-01-02 3:04 PM"
for index, record := range records {
if index == 0 {
continue
}
var vehicle db.Vehicle
var ok bool
if vehicle, ok = vehicleMap[record[4]]; !ok {
errors = append(errors, "Found an unmapped vehicle entry at row "+strconv.Itoa(index+1))
}
dateStr := record[2] + " " + record[3]
date, err := time.Parse(layout, dateStr)
if err != nil {
date, err = time.Parse(altLayout, dateStr)
}
if err != nil {
errors = append(errors, "Found an invalid date/time at row "+strconv.Itoa(index+1))
}
totalCostStr := accounting.UnformatNumber(record[9], 3, user.Currency)
totalCost64, err := strconv.ParseFloat(totalCostStr, 32)
if err != nil {
errors = append(errors, "Found an invalid total cost at row "+strconv.Itoa(index+1))
}
totalCost := float32(totalCost64)
odoStr := accounting.UnformatNumber(record[5], 0, user.Currency)
odoreading, err := strconv.Atoi(odoStr)
if err != nil {
errors = append(errors, "Found an invalid odo reading at row "+strconv.Itoa(index+1))
}
location := record[12]
//Create Fillup
if record[0] == "Gas" {
rateStr := accounting.UnformatNumber(record[7], 3, user.Currency)
ratet64, err := strconv.ParseFloat(rateStr, 32)
if err != nil {
errors = append(errors, "Found an invalid cost per gallon at row "+strconv.Itoa(index+1))
}
rate := float32(ratet64)
quantity64, err := strconv.ParseFloat(record[8], 32)
if err != nil {
errors = append(errors, "Found an invalid quantity at row "+strconv.Itoa(index+1))
}
quantity := float32(quantity64)
notes := fmt.Sprintf("Octane:%s\nGas Brand:%s\nLocation%s\nTags:%s\nPayment Type:%s\nTire Pressure:%s\nNotes:%s\nMPG:%s",
record[10], record[11], record[12], record[13], record[14], record[15], record[16], record[1],
)
isTankFull := record[6] == "Full"
fal := false
fillups = append(fillups, db.Fillup{
VehicleID: vehicle.ID,
FuelUnit: vehicle.FuelUnit,
FuelQuantity: quantity,
PerUnitPrice: rate,
TotalAmount: totalCost,
OdoReading: odoreading,
IsTankFull: &isTankFull,
Comments: notes,
FillingStation: location,
HasMissedFillup: &fal,
UserID: userId,
Date: date,
Currency: user.Currency,
DistanceUnit: user.DistanceUnit,
Source: "Fuelly",
})
}
if record[0] == "Service" {
notes := fmt.Sprintf("Tags:%s\nPayment Type:%s\nNotes:%s",
record[13], record[14], record[16],
)
expenses = append(expenses, db.Expense{
VehicleID: vehicle.ID,
Amount: totalCost,
OdoReading: odoreading,
Comments: notes,
ExpenseType: record[17],
UserID: userId,
Currency: user.Currency,
Date: date,
DistanceUnit: user.DistanceUnit,
Source: "Fuelly",
})
}
}
if len(errors) != 0 {
return errors
}
tx := db.DB.Begin()
defer func() {
if r := recover(); r != nil {
@@ -150,19 +18,90 @@ func FuellyImport(content []byte, userId string) []string {
errors = append(errors, err.Error())
return errors
}
if err := tx.Create(&fillups).Error; err != nil {
tx.Rollback()
errors = append(errors, err.Error())
return errors
if fillups != nil {
if err := tx.Create(&fillups).Error; err != nil {
tx.Rollback()
errors = append(errors, err.Error())
return errors
}
}
if err := tx.Create(&expenses).Error; err != nil {
tx.Rollback()
errors = append(errors, err.Error())
return errors
if expenses != nil {
if err := tx.Create(&expenses).Error; err != nil {
tx.Rollback()
errors = append(errors, err.Error())
return errors
}
}
err = tx.Commit().Error
err := tx.Commit().Error
if err != nil {
errors = append(errors, err.Error())
}
return errors
}
func DrivvoImport(content []byte, userId string, vehicleId string, importLocation bool) []string {
var errors []string
user, err := GetUserById(userId)
if err != nil {
errors = append(errors, err.Error())
return errors
}
vehicle, err := GetVehicleById(vehicleId)
if err != nil {
errors = append(errors, err.Error())
return errors
}
endParseIndex := bytes.Index(content, []byte("#Income"))
if endParseIndex == -1 {
endParseIndex = bytes.Index(content, []byte("#Route"))
if endParseIndex == -1 {
endParseIndex = len(content)
}
}
serviceEndIndex := bytes.Index(content, []byte("#Expense"))
if serviceEndIndex == -1 {
serviceEndIndex = endParseIndex
}
refuelEndIndex := bytes.Index(content, []byte("#Service"))
if refuelEndIndex == -1 {
refuelEndIndex = serviceEndIndex
}
var fillups []db.Fillup
fillups, errors = DrivvoParseRefuelings(content[:refuelEndIndex], user, vehicle, importLocation)
var allExpenses []db.Expense
services, parseErrors := DrivvoParseExpenses(content[refuelEndIndex:serviceEndIndex], user, vehicle)
if parseErrors != nil {
errors = append(errors, parseErrors...)
}
allExpenses = append(allExpenses, services...)
expenses, parseErrors := DrivvoParseExpenses(content[serviceEndIndex:endParseIndex], user, vehicle)
if parseErrors != nil {
errors = append(errors, parseErrors...)
}
allExpenses = append(allExpenses, expenses...)
if len(errors) != 0 {
return errors
}
return WriteToDB(fillups, allExpenses)
}
func FuellyImport(content []byte, userId string) []string {
fillups, expenses, errors := FuellyParseAll(content, userId)
if len(errors) != 0 {
return errors
}
return WriteToDB(fillups, expenses)
}

View File

@@ -4,11 +4,12 @@ import (
"sort"
"time"
"github.com/akhilrex/hammond/common"
"github.com/akhilrex/hammond/db"
"github.com/akhilrex/hammond/models"
)
func GetMileageByVehicleId(vehicleId string, since time.Time) (mileage []models.MileageModel, err error) {
func GetMileageByVehicleId(vehicleId string, since time.Time, mileageOption string) (mileage []models.MileageModel, err error) {
data, err := db.GetFillupsByVehicleIdSince(vehicleId, since)
if err != nil {
return nil, err
@@ -36,14 +37,48 @@ func GetMileageByVehicleId(vehicleId string, since time.Time) (mileage []models.
PerUnitPrice: currentFillup.PerUnitPrice,
OdoReading: currentFillup.OdoReading,
Currency: currentFillup.Currency,
DistanceUnit: currentFillup.DistanceUnit,
Mileage: 0,
CostPerMile: 0,
}
if currentFillup.IsTankFull != nil && *currentFillup.IsTankFull && (currentFillup.HasMissedFillup == nil || !(*currentFillup.HasMissedFillup)) {
distance := float32(currentFillup.OdoReading - lastFillup.OdoReading)
mileage.Mileage = distance / currentFillup.FuelQuantity
mileage.CostPerMile = distance / currentFillup.TotalAmount
currentOdoReading := float32(currentFillup.OdoReading);
lastFillupOdoReading := float32(lastFillup.OdoReading);
currentFuelQuantity := float32(currentFillup.FuelQuantity);
// If miles per gallon option and distanceUnit is km, convert from km to miles
// then check if fuel unit is litres. If it is, convert to gallons
if (mileageOption == "mpg" && mileage.DistanceUnit == db.KILOMETERS) {
currentOdoReading = common.KmToMiles(currentOdoReading);
lastFillupOdoReading = common.KmToMiles(lastFillupOdoReading);
if (mileage.FuelUnit == db.LITRE) {
currentFuelQuantity = common.LitreToGallon(currentFuelQuantity);
}
}
// If km_litre option or litre per 100km and distanceUnit is miles, convert from miles to km
// then check if fuel unit is not litres. If it isn't, convert to litres
if ((mileageOption == "km_litre" || mileageOption == "litre_100km") && mileage.DistanceUnit == db.MILES) {
currentOdoReading = common.MilesToKm(currentOdoReading);
lastFillupOdoReading = common.MilesToKm(lastFillupOdoReading);
if (mileage.FuelUnit == db.US_GALLON) {
currentFuelQuantity = common.GallonToLitre(currentFuelQuantity);
}
}
distance := float32(currentOdoReading - lastFillupOdoReading);
if (mileageOption == "litre_100km") {
mileage.Mileage = currentFuelQuantity / distance * 100;
} else {
mileage.Mileage = distance / currentFuelQuantity;
}
mileage.CostPerMile = distance / currentFillup.TotalAmount;
}

5
ui/.gitignore vendored
View File

@@ -29,3 +29,8 @@ yarn-error.log*
*.njsproj
*.sln
*.sw*
#Vs code files
.vscode
!.vscode/launch.json

View File

@@ -1,30 +0,0 @@
{
"BaseButton": {
"scope": "vue-html",
"prefix": "BaseButton",
"body": ["<BaseButton>", "\t${3}", "</BaseButton>"],
"description": "<BaseButton>"
},
"BaseIcon": {
"scope": "vue-html",
"prefix": "BaseIcon",
"body": ["<BaseIcon name=\"${1}\">", "\t${2}", "</BaseIcon>"],
"description": "<BaseIcon>"
},
"BaseInputText": {
"scope": "vue-html",
"prefix": "BaseInputText",
"body": ["<BaseInputText ${1}/>"],
"description": "<BaseInputText>"
},
"BaseLink": {
"scope": "vue-html",
"prefix": "BaseLink",
"body": [
"<BaseLink ${1|name,:to,href|}=\"${2:route}\">",
"\t${3}",
"</BaseLink>"
],
"description": "<BaseLink>"
}
}

View File

@@ -1,26 +0,0 @@
{
"script": {
"scope": "vue",
"prefix": "script",
"body": ["<script>", "export default {", "\t${0}", "}", "</script>"],
"description": "<script>"
},
"template": {
"scope": "vue",
"prefix": "template",
"body": ["<template>", "\t${0}", "</template>"],
"description": "<template>"
},
"style": {
"scope": "vue",
"prefix": "style",
"body": [
"<style lang=\"scss\" module>",
"@import '@design';",
"",
"${0}",
"</style>"
],
"description": "<style lang=\"scss\" module>"
}
}

View File

@@ -1,37 +0,0 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
// Syntax highlighting and more for .vue files
// https://github.com/vuejs/vetur
"octref.vetur",
// Peek and go-to-definition for .vue files
// https://github.com/fuzinato/vscode-vue-peek
"dariofuzinato.vue-peek",
// Lint-on-save with ESLint
// https://github.com/microsoft/vscode-eslint
"dbaeumer.vscode-eslint",
// Lint-on-save with Stylelint
// https://github.com/stylelint/vscode-stylelint
"stylelint.vscode-stylelint",
// Lint-on-save markdown in README files
// https://github.com/DavidAnson/vscode-markdownlint
"DavidAnson.vscode-markdownlint",
// Format-on-save with Prettier
// https://github.com/prettier/prettier-vscode
"esbenp.prettier-vscode",
// SCSS intellisense
// https://github.com/mrmlnc/vscode-scss
"mrmlnc.vscode-scss",
// Test `.unit.js` files on save with Jest
// https://github.com/jest-community/vscode-jest
"Orta.vscode-jest"
]
}

View File

@@ -1,93 +0,0 @@
{
// ===
// Spacing
// ===
"editor.insertSpaces": true,
"editor.tabSize": 2,
"editor.trimAutoWhitespace": true,
"files.trimTrailingWhitespace": true,
"files.eol": "\n",
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
// ===
// Files
// ===
"files.exclude": {
"**/*.log": true,
"**/*.log*": true,
"**/dist": true,
"**/coverage": true
},
"files.associations": {
".markdownlintrc": "jsonc"
},
// ===
// Event Triggers
// ===
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.fixAll.stylelint": true,
"source.fixAll.markdownlint": true
},
"eslint.validate": [
"javascript",
"javascriptreact",
"vue",
"vue-html",
"html"
],
"vetur.format.enable": false,
"vetur.completion.scaffoldSnippetSources": {
"user": "🗒️",
"workspace": "💼",
"vetur": ""
},
"prettier.disableLanguages": [],
// ===
// HTML
// ===
"html.format.enable": false,
"vetur.validation.template": false,
"emmet.triggerExpansionOnTab": true,
"emmet.includeLanguages": {
"vue-html": "html"
},
"vetur.completion.tagCasing": "initial",
// ===
// JS(ON)
// ===
"jest.autoEnable": false,
"jest.enableCodeLens": false,
"javascript.format.enable": false,
"json.format.enable": false,
"vetur.validation.script": false,
// ===
// CSS
// ===
"stylelint.enable": true,
"css.validate": false,
"scss.validate": false,
"vetur.validation.style": false,
// ===
// MARKDOWN
// ===
"[markdown]": {
"editor.wordWrap": "wordWrapColumn",
"editor.wordWrapColumn": 80
}
}

27415
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -34,17 +34,19 @@
"@fortawesome/fontawesome-svg-core": "^1.2.27",
"@fortawesome/free-solid-svg-icons": "^5.12.1",
"@fortawesome/vue-fontawesome": "0.1.9",
"axios": "^0.27.0",
"axios": "^0.27.2",
"buefy": "^0.9.7",
"chart.js": "^2.9.4",
"core-js": "3.6.4",
"currency-formatter": "^1.5.7",
"date-fns": "2.10.0",
"lodash": "^4.17.21",
"node-gyp": "^9.3.1",
"normalize.css": "8.0.1",
"nprogress": "0.2.0",
"vue": "2.6.11",
"vue-chartjs": "^3.5.1",
"vue-i18n": "^8.28.2",
"vue-meta": "2.3.3",
"vue-router": "3.1.6",
"vuex": "3.1.2"

View File

@@ -20,7 +20,7 @@ export default {
}
} else {
if (this.file == null) {
return 'Upload Photo'
return this.$t('uploadphoto')
} else {
return ''
}
@@ -39,7 +39,7 @@ export default {
.post(`/api/quickEntries`, formData)
.then((data) => {
this.$buefy.toast.open({
message: 'Quick Entry Created Successfully',
message: this.$t('quickentrycreatedsuccessfully'),
type: 'is-success',
duration: 3000,
})
@@ -68,9 +68,9 @@ export default {
<div class="section box">
<div class="columns">
<div class="column is-two-thirds">
<p class="title">Quick Entry</p>
<p class="title">{{ $tc('quickentry',1) }}</p>
<p class="subtitle"
>Take a pic of the invoice or the fuel pump display to make an entry later.</p
>{{ $t('quickentrydesc') }}</p
></div
>
<div class="column is-one-third is-flex is-align-content-center">
@@ -99,10 +99,10 @@ export default {
native-type="submit"
:disabled="tryingToCreate"
type="is-primary"
value="Upload File"
:value="this.$t('uploadfile')"
class="control"
>
Upload File
{{ $t('uploadfile') }}
</b-button>
</div></div
>

View File

@@ -3,9 +3,20 @@ import { Line } from 'vue-chartjs'
import axios from 'axios'
import { mapState } from 'vuex'
import { string } from 'yargs'
export default {
extends: Line,
props: { vehicle: { type: Object, required: true }, since: { type: Date, default: '' }, user: { type: Object, required: true } },
props: {
vehicle: { type: Object, required: true },
since: { type: Date, default: '' },
user: { type: Object, required: true },
mileageOption: { type: string, default: 'litre_100km' },
},
data: function() {
return {
chartData: [],
}
},
computed: {
...mapState('utils', ['isMobile']),
},
@@ -17,20 +28,28 @@ export default {
this.fetchMileage()
},
},
data: function() {
return {
chartData: [],
}
},
mounted() {
this.fetchMileage()
},
methods: {
showChart() {
let mileageLabel = ''
switch (this.mileageOption) {
case 'litre_100km':
mileageLabel = 'L/100km'
break
case 'km_litre':
mileageLabel = 'km/L'
break
case 'mpg':
mileageLabel = 'mpg'
break
}
var labels = this.chartData.map((x) => x.date.substr(0, 10))
var dataset = {
steppedLine: true,
label: `Mileage (${this.user.distanceUnitDetail.short}/${this.vehicle.fuelUnitDetail.short})`,
label: `Mileage (${mileageLabel})`,
fill: true,
data: this.chartData.map((x) => x.mileage),
}
@@ -41,6 +60,7 @@ export default {
.get(`/api/vehicles/${this.vehicle.id}/mileage`, {
params: {
since: this.since,
mileageOption: this.mileageOption,
},
})
.then((response) => {

View File

@@ -10,42 +10,42 @@ export default {
persistentNavRoutes: [
{
name: 'home',
title: 'Home',
title: this.$t('menu.home'),
},
],
loggedInNavRoutes: [
{
name: 'quickEntries',
title: () => 'Quick Entries',
title: () => this.$t('menu.quickentries'),
badge: () => this.unprocessedQuickEntries.length,
},
{
name: 'import',
title: () => 'Import',
title: () => this.$t('menu.import'),
},
{
name: 'settings',
title: 'Settings',
title: this.$t('menu.settings'),
},
{
name: 'logout',
title: 'Log out',
title: this.$t('menu.logout'),
},
],
loggedOutNavRoutes: [
{
name: 'login',
title: 'Log in',
title: this.$t('menu.login'),
},
],
adminNavRoutes: [
{
name: 'site-settings',
title: 'Site Settings',
title: this.$t('menu.sitesettings'),
},
{
name: 'users',
title: 'Users',
title: this.$t('menu.users'),
},
],
}
@@ -72,7 +72,7 @@ export default {
<NavBarRoutes :routes="persistentNavRoutes" />
<NavBarRoutes v-if="loggedIn" :routes="loggedInNavRoutes" />
<NavBarRoutes v-else :routes="loggedOutNavRoutes" />
<b-navbar-dropdown v-if="loggedIn && isAdmin" label="Admin">
<b-navbar-dropdown v-if="loggedIn && isAdmin" :label="$t('menu.admin')">
<NavBarRoutes :routes="adminNavRoutes" />
</b-navbar-dropdown>
</template>

View File

@@ -50,12 +50,12 @@ export default {
<b-select
v-if="unprocessedQuickEntries.length"
v-model="quickEntry"
placeholder="Refer quick entry"
:placeholder="this.$t('referquickentry')"
expanded
@input="showQuickEntry($event)"
>
<option v-for="option in unprocessedQuickEntries" :key="option.id" :value="option">
Taken: {{ parseAndFormatDateTime(option.createdAt) }}
{{ $t('created') }}: {{ parseAndFormatDateTime(option.createdAt) }}
</option>
</b-select>
<p class="control">

View File

@@ -55,7 +55,7 @@ export default {
return
}
this.$buefy.dialog.confirm({
title: 'Transfer Vehicle',
title: this.$t('transfervehicle'),
message: 'Are you sure you want to do this? You will lose ownership and all editing rights if you confirm.',
cancelText: 'Cancel',
confirmText: 'Go Ahead',
@@ -90,7 +90,7 @@ export default {
<template>
<div class="box" style="max-width:600px">
<h1 class="subtitle">Share {{ vehicle.nickname }}</h1>
<h1 class="subtitle">{{ $t('share') }} {{ vehicle.nickname }}</h1>
<section>
<div class="columns is-mobile" v-for="model in models" :key="model.id">
<div class="column is-one-third">
@@ -101,7 +101,7 @@ export default {
</b-field> </div
><div class="column is-three-quarters">
<b-field>
<b-button v-if="model.isShared && !model.isOwner" type="is-primary is-small" @click="transferVehicle(model)">Make Owner</b-button>
<b-button v-if="model.isShared && !model.isOwner" type="is-primary is-small" @click="transferVehicle(model)">{{ $t('makeowner') }}</b-button>
</b-field></div
></div
>

View File

@@ -1,6 +1,6 @@
<script>
import { addDays, addMonths } from 'date-fns'
import currencyFormtter from 'currency-formatter'
import currencyFormatter from 'currency-formatter'
import { mapState } from 'vuex'
import axios from 'axios'
@@ -14,12 +14,12 @@ export default {
data: function() {
return {
dateRangeOptions: [
{ label: 'This week', value: 'this_week' },
{ label: 'This month', value: 'this_month' },
{ label: 'Past 30 days', value: 'past_30_days' },
{ label: 'Past 3 months', value: 'past_3_months' },
{ label: 'This year', value: 'this_year' },
{ label: 'All Time', value: 'all_time' },
{ label: this.$t('thisweek'), value: 'this_week' },
{ label: this.$t('thismonth'), value: 'this_month' },
{ label: this.$tc('pastxdays', 30), value: 'past_30_days' },
{ label: this.$tc('pastxmonths', 3), value: 'past_3_months' },
{ label: this.$t('thisyear'), value: 'this_year' },
{ label: this.$t('alltime'), value: 'all_time' },
],
dateRangeOption: 'past_30_days',
stats: [],
@@ -32,15 +32,15 @@ export default {
return [
[
{
label: 'Total Expenditure',
label: this.$t('totalexpenses'),
value: this.formatCurrency(0, this.user.currency),
},
{
label: 'Fillup Costs',
label: this.$t('fillupcost'),
value: `${this.formatCurrency(0, this.user.currency)} (0)`,
},
{
label: 'Other Expenses',
label: this.$t('otherexpenses'),
value: `${this.formatCurrency(0, this.user.currency)} (0)`,
},
],
@@ -49,15 +49,15 @@ export default {
return this.stats.map((x) => {
return [
{
label: 'Total Expenditure',
label: this.$t('totalexpenses'),
value: this.formatCurrency(x.expenditureTotal, x.currency),
},
{
label: 'Fillup Costs',
label: this.$t('fillupcost'),
value: `${this.formatCurrency(x.expenditureFillups, x.currency)} (${x.countFillups})`,
},
{
label: 'Other Expenses',
label: this.$t('otherexpenses'),
value: `${this.formatCurrency(x.expenditureExpenses, x.currency)} (${x.countExpenses})`,
},
]
@@ -80,7 +80,7 @@ export default {
if (!currencyCode) {
currencyCode = this.me.currency
}
return currencyFormtter.format(number, { code: currencyCode })
return currencyFormatter.format(number, { code: currencyCode })
},
getStats() {
axios
@@ -129,7 +129,7 @@ export default {
<template>
<div>
<div class="columns">
<div class="column" :class="isMobile ? 'has-text-centered' : ''"> <h1 class="title">Stats</h1></div>
<div class="column" :class="isMobile ? 'has-text-centered' : ''"> <h1 class="title">{{ $t('statistics') }}</h1></div>
<div class="column">
<b-select v-model="dateRangeOption" class="is-pulled-right is-medium">
<option v-for="option in dateRangeOptions" :key="option.value" :value="option.value">

25
ui/src/i18n.js Normal file
View File

@@ -0,0 +1,25 @@
import Vue from 'vue';
import VueI18n from 'vue-i18n';
Vue.use(VueI18n);
function loadLocaleMessages () {
const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i)
const messages = {}
locales.keys().forEach(key => {
const matched = key.match(/([A-Za-z0-9-_]+)\./i)
if (matched && matched.length > 1) {
const locale = matched[1]
messages[locale] = locales(key)
}
})
return messages
}
const i18n = new VueI18n({
locale: navigator.language.split('-')[0] || 'en',
fallbackLocale: 'en',
messages: loadLocaleMessages()
});
export default i18n;

217
ui/src/locales/de.json Normal file
View File

@@ -0,0 +1,217 @@
{
"quickentry": "Keine Schnelleinträge | Schnelleintrag | Schnelleinträge",
"statistics": "Statistiken",
"thisweek": "Diese Woche",
"thismonth": "Dieser Monat",
"pastxdays": "Letzter Tag | Letzte {count} Tage",
"pastxmonths": "Letzter Monat | Letzte {count} Monate",
"thisyear": "Dieses Jahr",
"alltime": "Gesamt",
"noattachments": "Keine Anhänge",
"attachments": "Anhänge",
"choosefile": "Datei auswählen",
"addattachment": "Anhang hinzufügen",
"sharedwith": "Geteilt mit",
"share": "Teile",
"you": "du",
"addfillup": "Tankfüllung erfassen",
"createfillup": "Erfasse Tankfüllung",
"deletefillup": "Lösche diese Tankfüllung",
"addexpense": "Ausgabe erfassen",
"createexpense": "Erfasse Ausgabe",
"deleteexpense": "Lösche diese Ausgabe",
"nofillups": "Keine Tankfüllungen",
"transfervehicle": "Fahrzeug übertragen",
"settingssaved": "Einstellungen erfolgreich gespeichert",
"yoursettings": "Deine Einstellungen",
"settings": "Einstellungen",
"changepassword": "Passwort ändern",
"oldpassword": "Bisheriges Passwort",
"newpassword": "Neues Passwort",
"repeatnewpassword": "Neues Passwort wiederholen",
"passworddontmatch": "Passwörter stimmen nicht überein",
"save": "Speichern",
"supportthedeveloper": "Unterstütze den Entwickler",
"buyhimabeer": "Kauf ihm ein Bier!",
"moreinfo": "Mehr Info",
"currency": "Währung",
"distanceunit": "Entfernungseinheit",
"dateformat": "Datumsformat",
"createnow": "Jetzt erstellen",
"yourvehicles": "Deine Fahrzeuge",
"menu": {
"quickentries": "Schnellinträge",
"logout": "Abmelden",
"import": "Import",
"home": "Start",
"settings": "Einstellungen",
"admin": "Verwalten",
"sitesettings": "Globale Einstellungen",
"users": "Benutzer",
"login": "Anmelden"
},
"enterusername": "E-Mail eingeben",
"enterpassword": "Passwort eingeben",
"email": "E-Mail",
"password": "Passwort",
"login": "Anmelden",
"totalexpenses": "Gesamtausgaben",
"fillupcost": "Tank-Ausgaben",
"otherexpenses": "Andere Ausgaben",
"addvehicle": "Fahrzeug hinzufügen",
"editvehicle": "Fahrzeug bearbeiten",
"deletevehicle": "Fahrzeug löschen",
"sharevehicle": "Fahrzeug teilen",
"makeowner": "zum Besitzer machen",
"lastfillup": "Letztes Tanken",
"quickentrydesc": "Mach ein Foto deiner Rechnung oder der Zapfsäule um den Eintrag später zu ergänzen.",
"quickentrycreatedsuccessfully": "Schnelleintrag erfolgreich erstellt",
"uploadfile": "Datei hochladen",
"uploadphoto": "Foto hochladen",
"details": "Details",
"odometer": "Kilometerzähler",
"language": "Sprache",
"date": "Datum",
"pastfillups": "Tankfüllungen",
"fuelsubtype": "Kraftstofftyp",
"fueltype": "Kraftstoff",
"quantity": "Menge",
"gasstation": "Tankstelle",
"fuel": {
"petrol": "Benzin",
"diesel": "Diesel",
"cng": "CNG",
"lpg": "LPG",
"electric": "Strom",
"ethanol": "Ethanol"
},
"unit": {
"long": {
"litre": "Liter",
"gallon": "Gallone",
"kilowatthour": "Kilowattstunde",
"kilogram": "Kilogramm",
"usgallon": "US-Gallone",
"minutes": "Minuten",
"kilometers": "Kilometer",
"miles": "Meilen"
},
"short": {
"litre": "L",
"gallon": "Gal",
"kilowatthour": "KwH",
"kilogram": "Kg",
"usgallon": "US-Gal",
"minutes": "Min",
"kilometers": "Km",
"miles": "Mi"
}
},
"avgfillupqty": "Ø Tankmenge",
"avgfillupexpense": "Ø Tankwert",
"avgfuelcost": "Ø Spritpreis",
"per": "{0} pro {1}",
"price": "Preis",
"total": "Gesamt",
"fulltank": "Voller Tank",
"getafulltank": "Hast du vollgetankt?",
"by": "Von",
"expenses": "Ausgaben",
"expensetype": "Ausgaben Typ",
"noexpenses": "Keine Ausgaben",
"download": "Herunterladen",
"title": "Titel",
"name": "Name",
"delete": "Löschen",
"importdata": "Importiere Daten in Hammond",
"importdatadesc": "Wähle eine der folgenden Optionen, um Daten in Hammond zu importieren",
"import": "Importieren",
"importcsv": "Wenn du {name} nutzt, um deine Fahrzeugdaten zu verwalten, exportiere die CSV Datei aus {name} und klicke hier, um zu importieren.",
"choosecsv": "CSV auswählen",
"choosephoto": "Foto auswählen",
"importsuccessfull": "Daten erfolgreich importiert",
"importerror": "Beim Importieren der Datei ist ein Fehler aufgetreten. Details findest du in der Fehlermeldung",
"importfrom": "Importiere von {name}",
"stepstoimport": "Schritte, um Daten aus {name} zu importieren",
"choosecsvimport": "Wähle die {name} CSV aus und klicke den Button, um zu importieren.",
"dontimportagain": "Achte darauf, dass du die Datei nicht erneut importierst, da dies zu mehrfachen Einträgen führen würde.",
"checkpointsimportcsv": "Wenn du alle diese Punkte überprüft hast kannst du unten die CSV importieren.",
"importhintunits": "Vergewissere dich ebenfalls, dass die <u>Kraftstoffeinheit</u> und der <u>Kraftstofftyp</u> im Fahrzeug richtig eingestellt sind.",
"importhintcurrdist": "Stelle sicher, dass die <u>Währung</u> und die <u>Entfernungseinheit</u> in Hammond korrekt eingestellt sind. Der Import erkennt die Währung nicht automatisch aus der CSV-Datei, sondern verwendet die für den Benutzer eingestellte Währung.",
"importhintnickname": "Vergewissere dich, dass der Fahrzeugname in Hammond genau mit dem Namen in der Fuelly-CSV-Datei übereinstimmt, sonst funktioniert der Import nicht.",
"importhintvehiclecreated": "Vergewissere dich, dass du die Fahrzeuge bereits in Hammond erstellt hast.",
"importhintcreatecsv": "Exportiere deine Daten aus {name} im CSV-Format. Die Schritte dazu findest du",
"here": "hier",
"unprocessedquickentries": "Du hast einen Schnelleintrag zum bearbeiten. | Du hast {0} Schnelleinträge zum bearbeiten.",
"show": "Anzeigen",
"loginerror": "Bei der Anmeldung ist ein Fehler aufgetreten. {msg}",
"showunprocessed": "Zeige unbearbeitete",
"unprocessed": "unbearbeitet",
"sitesettingdesc": "Ändere die globalen Einstellungen. Diese werden als Standard für neue Benutzer verwendet.",
"settingdesc": "Diese Einstellungen werden als Standard verwendet wenn du eine neue Ausgabe oder eine Tankfüllung erfasst.",
"areyousure": "Bist du dir sicher?",
"adduser": "Benutzer hinzufügen",
"usercreatedsuccessfully": "Benutzer erfolgreich gespeichert",
"role": "Rolle",
"created": "Erstellt",
"createnewuser": "Erstelle neuen Benutzer",
"cancel": "Abbrechen",
"novehicles": "Du hast noch kein Fahrzeug erstellt. Lege jetzt einen Eintrag für das zu verwaltende Fahrzeug an.",
"processed": "Bearbeitet",
"notfound": "Nicht gefunden",
"timeout": "Das Laden der Seite hat eine Zeitüberschreitung verursacht. Bist du sicher, dass du noch mit dem Internet verbunden bist?",
"clicktoselect": "Klicke, um auszuwählen...",
"expenseby": "Ausgabe von",
"selectvehicle": "Wähle ein Fahrzeug aus",
"expensedate": "Datum der Ausgabe",
"totalamountpaid": "Gezahlter Gesamtbetrag",
"fillmoredetails": "Weitere Details ausfüllen",
"markquickentryprocessed": "Markiere gewählten Schnelleintrag als bearbeitet",
"referquickentry": "Wähle Schnelleintrag",
"deletequickentry": "Willst du diesen Schnelleintrag wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden!",
"fuelunit": "Kraftstoffeinheit",
"fillingstation": "Tankstelle",
"comments": "Kommentare",
"missfillupbefore": "Hast du vergessen, die vorherige Tankfüllung zu erfassen?",
"fillupdate": "Tankdatum",
"fillupsavedsuccessfully": "Tankfüllung erfolgreich gespeichert",
"expensesavedsuccessfully": "Ausgabe erfolgreich gespeichert",
"vehiclesavedsuccessfully": "Fahrzeug erfolgreich gespeichert",
"back": "Zurück",
"nickname": "Bezeichnung",
"registration": "Nummernschild",
"createvehicle": "Fahrzeug erstellen",
"make": "Marke",
"model": "Modell",
"yearmanufacture": "Jahr der Erstzulassung",
"enginesize": "Hubraum (in ccm)",
"testconn": "Teste Verbindung",
"migrate": "Migrieren",
"init": {
"migrateclarkson": "Migriere von Clarkson",
"migrateclarksondesc": "Wenn du bereits eine Instanz von Clarkson verwendest und die Daten migrieren möchtest, klicke hier.",
"freshinstall": "Frische Installation",
"freshinstalldesc": "Wenn du eine neue Installation von Hammond starten möchtest, klicke hier.",
"clarkson": {
"desc": "<p>Zuerst musst du sicherstellen, dass das Deployment von Hammond die von Clarkson verwendete MySQL Datenbank erreichen kann.</p><p>Wenn dies nicht möglich ist kannst du eine Kopie erstellen die für Hammond erreichbar ist.</p><p>Wenn das erledigt ist, füge hier den Connection String im folgenden Format ein.</p><p>Alle aus Clarkson importierten Nutzer bekommen ihren Benutzernamen als E-Mail und das Passwort wird geändert zu <span class='' style='font-weight:bold'>hammond</span></p><code>user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local</code><br/><br/>",
"success": "Deine Daten wurden erfolgreich von Clarkson migriert. Du wirst in kürze zur Anmeldung weitergeleitet wo du dich mit deiner E-Mail und dem passwort `hammond` anmelden kannst."
},
"fresh": {
"setupadminuser": "Erstelle einen Administrator",
"yourpassword": "Dein Passwort",
"youremail": "Deine E-Mail-Adresse",
"yourname": "Dein Name",
"success": "Du hast dich erfolgreich registriert. Du wirst in kürze zur Anmeldung weitergeleitet und kannst Anfangen Hammond zu verwenden."
}
},
"roles": {
"ADMIN": "Adminstrator",
"USER": "Benutzer"
},
"profile": "Profil",
"processedon": "Bearbeitet am",
"enable": "Entsperren",
"disable": "Sperren",
"confirm": "Bestätigen",
"labelforfile": "Bezeichnung für diese Datei"
}

224
ui/src/locales/en.json Normal file
View File

@@ -0,0 +1,224 @@
{
"quickentry": "No Quick Entries | Quick Entry | Quick Entries",
"statistics": "Statistics",
"thisweek": "This week",
"thismonth": "This month",
"pastxdays": "Past one day | Past {count} days",
"pastxmonths": "Past one month | Past {count} months",
"thisyear": "This year",
"alltime": "All Time",
"noattachments": "No Attachments so far",
"attachments": "Attachments",
"choosefile": "Choose File",
"addattachment": "Add Attachment",
"sharedwith": "Shared with",
"share": "Share",
"you": "You",
"addfillup": "Add Fillup",
"createfillup": "Create Fillup",
"deletefillup": "Delete this fillup",
"addexpense": "Add Expense",
"createexpense": "Create Expense",
"deleteexpense": "Delete this expense",
"nofillups": "No Fillups so far",
"transfervehicle": "Transfer Vehicle",
"settingssaved": "Settings saved successfully",
"yoursettings": "Your Settings",
"settings": "Settings",
"changepassword": "Change password",
"oldpassword": "Old password",
"newpassword": "New password",
"repeatnewpassword": "Repeat New Password",
"passworddontmatch": "Password values don't match",
"save": "Save",
"supportthedeveloper": "Support the developer",
"buyhimabeer": "Buy him a beer!",
"featurerequest": "Feature Request",
"foundabug": "Found a bug",
"currentversion": "Current Version",
"moreinfo": "More Info",
"currency": "Currency",
"distanceunit": "Distance Unit",
"dateformat": "Date Format",
"createnow": "Create Now",
"yourvehicles": "Your Vehicles",
"menu": {
"quickentries": "Quick Entries",
"logout": "Log out",
"import": "Import",
"home": "Home",
"settings": "Settings",
"admin": "Admin",
"sitesettings": "Site Settings",
"users": "Users",
"login": "Log in"
},
"enterusername": "Enter your username",
"enterpassword": "Enter your password",
"email": "Email",
"password": "Password",
"login": "log in",
"totalexpenses": "Total Expenses",
"fillupcost": "Fillup Costs",
"otherexpenses": "Other Expenses",
"addvehicle": "Add Vehicle",
"editvehicle": "Edit Vehicle",
"deletevehicle": "Delete Vehicle",
"sharevehicle": "Share vehicle",
"makeowner": "Make Owner",
"lastfillup": "Last Fillup",
"quickentrydesc": "Take a pic of the invoice or the fuel pump display to make an entry later.",
"quickentrycreatedsuccessfully": "Quick Entry Created Successfully",
"uploadfile": "Upload File",
"uploadphoto": "Upload Photo",
"details": "Details",
"odometer": "Odometer",
"language": "Language",
"date": "Date",
"pastfillups": "Past Fillups",
"fuelsubtype": "Fuel Subtype",
"fueltype": "Fuel Type",
"quantity": "Quantity",
"gasstation": "Gas Station",
"fuel": {
"petrol": "Petrol",
"diesel": "Diesel",
"cng": "CNG",
"lpg": "LPG",
"electric": "Electric",
"ethanol": "Ethanol"
},
"unit": {
"long": {
"litre": "Litre",
"gallon": "Gallon",
"kilowatthour": "Kilowatt Hour",
"kilogram": "Kilogram",
"usgallon": "US Gallon",
"minutes": "Minutes",
"kilometers": "Kilometers",
"miles": "Miles"
},
"short": {
"litre": "Lt",
"gallon": "Gal",
"kilowatthour": "KwH",
"kilogram": "Kg",
"usgallon": "US Gal",
"minutes": "Mins",
"kilometers": "Km",
"miles": "Mi"
}
},
"avgfillupqty": "Avg Fillup Qty",
"avgfillupexpense": "Avg Fillup Expense",
"avgfuelcost": "Avg Fuel Cost",
"per": "{0} per {1}",
"price": "Price",
"total": "Total",
"fulltank": "Tank Full",
"getafulltank": "Did you get a full tank?",
"by": "By",
"expenses": "Expenses",
"expensetype": "Expense Type",
"noexpenses": "No Expenses so far",
"download": "Download",
"title": "Title",
"name": "Name",
"delete": "Delete",
"importdata": "Import data into Hammond",
"importdatadesc": "Choose from the following options to import data into Hammond",
"import": "Import",
"importcsv": "If you have been using {name} to store your vehicle data, export the CSV file from {name} and click here to import.",
"choosecsv": "Choose CSV",
"choosephoto": "Choose Photo",
"importsuccessfull": "Data Imported Successfully",
"importerror": "There was some issue with importing the file. Please check the error message",
"importfrom": "Import from {0}",
"stepstoimport": "Steps to import data from {name}",
"choosecsvimport": "Choose the {name} CSV and press the import button.",
"dontimportagain": "Make sure that you do not import the file again because that will create repeat entries.",
"checkpointsimportcsv": "Once you have checked all these points, just import the CSV below.",
"importhintunits": "Similiarly, make sure that the <u>Fuel Unit</u> and <u>Fuel Type</u> are correctly set in the Vehicle.",
"importhintcurrdist": "Make sure that the <u>Currency</u> and <u>Distance Unit</u> are set correctly in Hammond. Import will not autodetect Currency from the CSV but use the one set for the user.",
"importhintnickname": "Make sure that the Vehicle nickname in Hammond is exactly the same as the name on Fuelly CSV or the import will not work.",
"importhintvehiclecreated": "Make sure that you have already created the vehicles in Hammond platform.",
"importhintcreatecsv": "Export your data from {name} in the CSV format. Steps to do that can be found",
"here": "here",
"unprocessedquickentries": "You have one quick entry to be processed. | You have {0} quick entries pending to be processed.",
"show": "Show",
"loginerror": "There was an error logging in to your account. {msg}",
"showunprocessed": "Show unprocessed only",
"unprocessed": "unprocessed",
"sitesettingdesc": "Update site level settings. These will be used as default values for new users.",
"settingdesc": "These will be used as default values whenever you create a new fillup or expense.",
"areyousure": "Are you sure you want to do this?",
"adduser": "Add User",
"usercreatedsuccessfully": "User Created Successfully",
"userdisabledsuccessfully": "User disabled successfully",
"userenabledsuccessfully": "User enabled successfully",
"role": "Role",
"created": "Created",
"createnewuser": "Create New User",
"cancel": "Cancel",
"novehicles": "It seems you have not yet created a vehicle in the system. Start by creating an entry for one of the vehicles you want to track.",
"processed": "Mark Processed",
"notfound": "Not Found",
"timeout": "The page timed out while loading. Are you sure you're still connected to\nthe Internet?",
"clicktoselect": "Click to select...",
"expenseby": "Expense by",
"selectvehicle": "Select a vehicle",
"expensedate": "Expense Date",
"totalamountpaid": "Total Amount Paid",
"fillmoredetails": "Fill more details",
"markquickentryprocessed": "Mark selected Quick Entry as processed",
"referquickentry": "Refer quick entry",
"deletequickentry": "This will delete this Quick Entry. This step cannot be reversed. Are you sure?",
"fuelunit": "Fuel Unit",
"fillingstation": "Filling Station Name",
"comments": "Comments",
"missfillupbefore": "Did you miss the fillup entry before this one?",
"fillupdate": "Fillup Date",
"fillupsavedsuccessfully": "Fillup Saved Successfully",
"expensesavedsuccessfully": "Expense Saved Successfully",
"vehiclesavedsuccessfully": "Vehicle Saved Successfully",
"settingssavedsuccessfully": "Settings saved successfully",
"back": "Back",
"nickname": "Nickname",
"registration": "Registration",
"createvehicle": "Create Vehicle",
"make": "Make / Company",
"model": "Model",
"yearmanufacture": "Year of Manufacture",
"enginesize": "Engine Size (in cc)",
"mysqlconnstr": "Mysql Connection String",
"testconn": "Test Connection",
"migrate": "Migrate",
"init": {
"migrateclarkson": "Migrate from Clarkson",
"migrateclarksondesc": "If you have an existing Clarkson deployment and you want to migrate your data from that, press the following button.",
"freshinstall": "Fresh Install",
"freshinstalldesc": "If you want a fresh install of Hammond, press the following button.",
"clarkson": {
"desc": "<p>You need to make sure that this deployment of Hammond can access the MySQL database used by Clarkson.</p><p>If that is not directly possible, you can make a copy of that database somewhere accessible from this instance.</p><p>Once that is done, enter the connection string to the MySQL instance in the following format.</p><p>All the users imported from Clarkson will have their username as their email in Clarkson database and pasword set to<span class='' style='font-weight:bold'>hammond</span></p><code>user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local</code><br/><br/>",
"success": "We have successfully migrated the data from Clarkson. You will be redirected to the login screen shortly where you can login using your existing email and password : hammond"
},
"fresh": {
"setupadminuser": "Setup Admin Users",
"yourpassword": "Your Password",
"youremail": "Your Email",
"yourname": "Your Name",
"success": "You have been registered successfully. You will be redirected to the login screen shortly where you can login and start using the system."
}
},
"roles": {
"ADMIN": "ADMIN",
"USER": "USER"
},
"profile": "Profile",
"processedon": "Processed on",
"enable": "Enable",
"disable": "Disable",
"confirm": "Go Ahead",
"labelforfile": "Label for this file"
}

View File

@@ -24,6 +24,7 @@ import {
faTimesCircle,
} from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import i18n from './i18n';
import App from './app.vue'
@@ -54,6 +55,7 @@ library.add(
faUserFriends,
faTimesCircle
)
Vue.use(Buefy, {
defaultIconComponent: 'vue-fontawesome',
defaultIconPack: 'fas',
@@ -73,6 +75,7 @@ const app = new Vue({
store,
render: (h) => h(App),
i18n,
}).$mount('#app')
// If running e2e tests...

View File

@@ -410,6 +410,15 @@ export default [
},
props: (route) => ({ user: store.state.auth.currentUser || {} }),
},
{
path: '/import/drivvo',
name: 'import-drivvo',
component: () => lazyLoadView(import('@views/import-drivvo.vue')),
meta: {
authRequired: true,
},
props: (route) => ({ user: store.state.auth.currentUser || {} }),
},
{
path: '/logout',
name: 'logout',

View File

@@ -23,7 +23,7 @@ export default {
<template v-if="resource">
{{ resource }}
</template>
Not Found
{{ $t('notfound') }}
</h1>
</Layout>
</template>

View File

@@ -32,8 +32,7 @@ export default {
<template>
<Layout v-if="offlineConfirmed">
<h1 :class="$style.title">
The page timed out while loading. Are you sure you're still connected to
the Internet?
{{ $t('timeout') }}
</h1>
</Layout>
<LoadingView v-else />

View File

@@ -95,7 +95,7 @@ export default {
.put(`/api/vehicles/${this.selectedVehicle.id}/expenses/${this.expense.id}`, this.expenseModel)
.then((data) => {
this.$buefy.toast.open({
message: 'Expense Updated Successfully',
message: this.$t('expensesavedsuccessfully'),
type: 'is-success',
duration: 3000,
})
@@ -120,7 +120,7 @@ export default {
.post(`/api/vehicles/${this.selectedVehicle.id}/expenses`, this.expenseModel)
.then((data) => {
this.$buefy.toast.open({
message: 'Expense Created Successfully',
message: this.$t('expensesavedsuccessfully'),
type: 'is-success',
duration: 3000,
})
@@ -152,7 +152,7 @@ export default {
<Layout>
<div class="columns">
<div class="column is-two-thirds">
<h1 class="title">Create Expense</h1>
<h1 class="title">{{ $t('createexpense') }}</h1>
<h1 class="subtitle">
{{ [selectedVehicle.nickname, selectedVehicle.registration, selectedVehicle.make, selectedVehicle.model].join(' | ') }}
</h1>
@@ -162,61 +162,61 @@ export default {
</div>
</div>
<form @submit.prevent="createExpense">
<b-field label="Select a vehicle">
<b-select v-model="selectedVehicle" placeholder="Vehicle" required expanded :disabled="expense.id">
<b-field :label="this.$t('selectvehicle')">
<b-select v-model="selectedVehicle" :placeholder="this.$t('vehicle')" required expanded :disabled="expense.id">
<option v-for="option in myVehicles" :key="option.id" :value="option">
{{ option.nickname }}
</option>
</b-select>
</b-field>
<b-field label="Expense by">
<b-select v-model="expenseModel.userId" placeholder="User" required expanded :disabled="expense.id">
<b-field :label="this.$t('expenseby')">
<b-select v-model="expenseModel.userId" :placeholder="this.$t('user')" required expanded :disabled="expense.id">
<option v-for="option in users" :key="option.userId" :value="option.userId">
{{ option.name }}
</option>
</b-select>
</b-field>
<b-field label="Expense Date">
<b-field :label="this.$t('expensedate')">
<b-datepicker
v-model="expenseModel.date"
:date-formatter="formatDate"
placeholder="Click to select..."
:placeholder="this.$t('clicktoselect')"
icon="calendar"
:max-date="new Date()"
>
</b-datepicker>
</b-field>
<b-field label="Expense Type*">
<b-field :label="this.$t('expensetype') + `*`">
<b-input v-model="expenseModel.expenseType" expanded required></b-input>
</b-field>
<b-field label="Total Amount Paid">
<b-field :label="this.$t('totalamountpaid')">
<p class="control">
<span class="button is-static">{{ me.currency }}</span>
</p>
<b-input v-model.number="expenseModel.amount" type="number" min="0" expanded step=".001" required></b-input>
</b-field>
<b-field label="Odometer Reading">
<b-field :label="this.$t('odometer')">
<p class="control">
<span class="button is-static">{{ me.distanceUnitDetail.short }}</span>
<span class="button is-static">{{ $t('unit.short.' + me.distanceUnitDetail.key) }}</span>
</p>
<b-input v-model.number="expenseModel.odoReading" type="number" min="0" expanded required></b-input>
</b-field>
<b-field>
<b-switch v-model="showMore">Fill more details</b-switch>
<b-switch v-model="showMore">{{ $t('fillmoredetails') }}</b-switch>
</b-field>
<fieldset v-if="showMore">
<b-field label="Comments">
<b-field :label="this.$t('details')">
<b-input v-model="expenseModel.comments" type="textarea" expanded></b-input>
</b-field>
</fieldset>
<b-field>
<b-switch v-if="quickEntry" v-model="processQuickEntry">Mark selected Quick Entry as processed</b-switch>
<b-switch v-if="quickEntry" v-model="processQuickEntry">{{ $t('markquickentryprocessed') }}</b-switch>
</b-field>
<br />
<b-field>
<b-button tag="input" native-type="submit" :disabled="tryingToCreate" type="is-primary" label="Create Expense" expanded> </b-button>
<b-button tag="input" native-type="submit" :value="this.$t('save')" :disabled="tryingToCreate" type="is-primary" label="Create Expense" expanded> </b-button>
</b-field>
</form>
</Layout>

View File

@@ -126,7 +126,7 @@ export default {
.put(`/api/vehicles/${this.selectedVehicle.id}/fillups/${this.fillup.id}`, this.fillupModel)
.then((data) => {
this.$buefy.toast.open({
message: 'Fillup Updated Successfully',
message: this.$t('fillupsavedsuccessfully'),
type: 'is-success',
duration: 3000,
})
@@ -153,7 +153,7 @@ export default {
.post(`/api/vehicles/${this.selectedVehicle.id}/fillups`, this.fillupModel)
.then((data) => {
this.$buefy.toast.open({
message: 'Fillup Created Successfully',
message: this.$t('fillupsavedsuccessfully'),
type: 'is-success',
duration: 3000,
})
@@ -181,46 +181,44 @@ export default {
<template>
<Layout>
<div class="has-text-centered">
<div class="columns">
<div class="column is-two-thirds">
<h1 class="title">Create Fillup</h1>
<h1 class="subtitle">
{{ [selectedVehicle.nickname, selectedVehicle.registration, selectedVehicle.make, selectedVehicle.model].join(' | ') }}
</h1>
</div>
<div class="column is-one-thirds">
<QuickEntryDisplay v-model="quickEntry" :user="user" />
</div>
<div class="columns">
<div class="column is-two-thirds">
<h1 class="title">{{ $t('createfillup') }}</h1>
<h1 class="subtitle">
{{ [selectedVehicle.nickname, selectedVehicle.registration, selectedVehicle.make, selectedVehicle.model].join(' | ') }}
</h1>
</div>
<div class="column is-one-thirds">
<QuickEntryDisplay v-model="quickEntry" :user="user" />
</div>
</div>
<form class="" @submit.prevent="createFillup">
<b-field label="Select a vehicle">
<b-select v-model="selectedVehicle" placeholder="Vehicle" required expanded :disabled="fillup.id">
<b-field :label="this.$t('selectvehicle')">
<b-select v-model="selectedVehicle" :placeholder="this.$t('vehicle')" required expanded :disabled="fillup.id">
<option v-for="option in myVehicles" :key="option.id" :value="option">
{{ option.nickname }}
</option>
</b-select>
</b-field>
<b-field label="Expense by">
<b-select v-model="fillupModel.userId" placeholder="User" required expanded :disabled="fillup.id">
<b-field :label="this.$t('expenseby')">
<b-select v-model="fillupModel.userId" :placeholder="this.$t('user')" required expanded :disabled="fillup.id">
<option v-for="option in users" :key="option.userId" :value="option.userId">
{{ option.name }}
</option>
</b-select>
</b-field>
<b-field label="Fillup Date">
<b-field :label="this.$t('fillupdate')">
<b-datepicker
v-model="fillupModel.date"
:date-formatter="formatDate"
placeholder="Click to select..."
placeholder="this.$t('clicktoselect')"
icon="calendar"
trap-focus
:max-date="new Date()"
>
</b-datepicker>
</b-field>
<b-field label="Fuel Subtype">
<b-field :label="this.$t('fuelsubtype')">
<b-autocomplete
v-model="fillupModel.fuelSubType"
:data="filteredFuelSubtypes"
@@ -231,55 +229,55 @@ export default {
>
</b-autocomplete>
</b-field>
<b-field label="Quantity*" addons>
<b-field :label="this.$t('quantity') + `*`" addons>
<b-input v-model.number="fillupModel.fuelQuantity" type="number" step=".001" min="0" expanded required></b-input>
<b-select v-model="fillupModel.fuelUnit" placeholder="Fuel Unit" required>
<b-select v-model="fillupModel.fuelUnit" :placeholder="this.$t('fuelunit')" required>
<option v-for="(option, key) in fuelUnitMasters" :key="key" :value="key">
{{ option.long }}
{{ $t('unit.long.' + option.key) }}
</option>
</b-select>
</b-field>
<b-field :label="'Price per ' + vehicle.fuelUnitDetail.short + '*'"
<b-field :label="this.$t('per', { '0': this.$t('price'), '1': $t('unit.short.' + vehicle.fuelUnitDetail.key) })"
><p class="control">
<span class="button is-static">{{ me.currency }}</span>
</p>
<b-input v-model.number="fillupModel.perUnitPrice" type="number" min="0" step=".001" expanded required></b-input>
</b-field>
<b-field label="Total Amount Paid">
<b-field :label="this.$t('totalamountpaid')">
<p class="control">
<span class="button is-static">{{ me.currency }}</span>
</p>
<b-input v-model.number="fillupModel.totalAmount" type="number" min="0" step=".001" expanded required></b-input>
</b-field>
<b-field label="Odometer Reading">
<b-field :label="this.$t('odometer')">
<p class="control">
<span class="button is-static">{{ me.distanceUnitDetail.short }}</span>
<span class="button is-static">{{ $t('unit.short.' + me.distanceUnitDetail.key) }}</span>
</p>
<b-input v-model.number="fillupModel.odoReading" type="number" min="0" expanded required></b-input>
</b-field>
<b-field>
<b-checkbox v-model="fillupModel.isTankFull">Did you get a full tank?</b-checkbox>
<b-checkbox v-model="fillupModel.isTankFull">{{ $t('getafulltank') }}</b-checkbox>
</b-field>
<b-field>
<b-checkbox v-model="fillupModel.hasMissedFillup">Did you miss the fillup entry before this one?</b-checkbox>
<b-checkbox v-model="fillupModel.hasMissedFillup">{{ $t('missfillupbefore') }}</b-checkbox>
</b-field>
<b-field>
<b-switch v-model="showMore">Fill more details</b-switch>
<b-switch v-model="showMore">{{ $t('fillmoredetails') }}</b-switch>
</b-field>
<fieldset v-if="showMore">
<b-field label="Filling Station Name">
<b-field :label="this.$t('fillingstation')">
<b-input v-model="fillupModel.fillingStation" type="text" expanded></b-input>
</b-field>
<b-field label="Comments">
<b-field :label="this.$t('comments')">
<b-input v-model="fillupModel.comments" type="textarea" expanded></b-input>
</b-field>
</fieldset>
<b-field>
<b-switch v-if="quickEntry" v-model="processQuickEntry">Mark selected Quick Entry as processed</b-switch>
<b-switch v-if="quickEntry" v-model="processQuickEntry">{{ $t('markquickentryprocessed') }}</b-switch>
</b-field>
<br />
<b-field>
<b-button tag="input" native-type="submit" :disabled="tryingToCreate" type="is-primary" label="Create Fillup" expanded> </b-button>
<b-button tag="input" native-type="submit" :disabled="tryingToCreate" type="is-primary" :value="this.$t('save')" :label="this.$t('createfillup')" expanded> </b-button>
<p v-if="authError">
There was an error logging in to your account.
</p>

View File

@@ -76,7 +76,7 @@ export default {
.put(`/api/vehicles/${this.vehicle.id}`, this.vehicleModel)
.then((data) => {
this.$buefy.toast.open({
message: 'Vehicle Updated Successfully',
message: this.$t('vehiclesavedsuccessfully'),
type: 'is-success',
duration: 3000,
})
@@ -98,7 +98,7 @@ export default {
.post(`/api/vehicles`, this.vehicleModel)
.then((data) => {
this.$buefy.toast.open({
message: 'Vehicle Created Successfully',
message: this.$t('vehiclesavedsuccessfully'),
type: 'is-success',
duration: 3000,
})
@@ -125,60 +125,68 @@ export default {
<Layout>
<div class="columns">
<div class="column is-three-quarters">
<h1 class="title">Create Vehicle</h1>
<h1 class="title">{{ $t('createvehicle') }}</h1>
</div>
<div class="column is-one-quarter">
<router-link tag="b-button" type="is-primary" to="/">
Back to Vehicle
{{ $t('back') }}
</router-link>
</div>
</div>
<form @submit.prevent="createVehicle">
<b-field label="Nickname*">
<b-field :label="this.$t('nickname') + `*`">
<b-input v-model="vehicleModel.nickname" type="text" expanded required></b-input>
</b-field>
<b-field label="Registration*">
<b-field :label="this.$t('registration') + `*`">
<b-input v-model="vehicleModel.registration" type="text" expanded required></b-input>
</b-field>
<b-field label="VIN">
<b-input v-model="vehicleModel.vin" type="text" expanded></b-input>
</b-field>
<b-field label="Fuel Type*">
<b-select v-model.number="vehicleModel.fuelType" placeholder="Fuel Type" required expanded>
<b-field :label="this.$t('fueltype') + `*`">
<b-select v-model.number="vehicleModel.fuelType" :placeholder="this.$t('fueltype')" required expanded>
<option v-for="(option, key) in fuelTypeMasters" :key="key" :value="key">
{{ option.long }}
{{ $t('fuel.' + option.key) }}
</option>
</b-select>
</b-field>
<b-field label="Fuel Unit*">
<b-select v-model.number="vehicleModel.fuelUnit" placeholder="Fuel Unit" required expanded>
<b-field :label="this.$t('fuelunit') + `*`">
<b-select v-model.number="vehicleModel.fuelUnit" :placeholder="this.$t('fuelunit')" required expanded>
<option v-for="(option, key) in fuelUnitMasters" :key="key" :value="key">
{{ option.long }}
{{ $t('unit.long.' + option.key) }}
</option>
</b-select>
</b-field>
<b-field label="Make / Company*">
<b-field :label="this.$t('make') + `*`">
<b-input v-model="vehicleModel.make" type="text" required expanded></b-input>
</b-field>
<b-field label="Model*">
<b-field :label="this.$t('model') + `*`">
<b-input v-model="vehicleModel.model" type="text" required expanded></b-input>
</b-field>
<b-field label="Year Of Manufacture">
<b-field :label="this.$t('yearmanufacture') + `*`">
<b-input v-model.number="vehicleModel.yearOfManufacture" type="number" expanded number></b-input>
</b-field>
<b-field label="Engine Size (in cc)">
<b-field :label="this.$t('enginesize')">
<b-input v-model.number="vehicleModel.engineSize" type="number" expanded number></b-input>
</b-field>
<br />
<b-field>
<b-button tag="input" native-type="submit" :disabled="tryingToCreate" type="is-primary" label="Create Vehicle" expanded>
<b-button
tag="input"
native-type="submit"
:disabled="tryingToCreate"
type="is-primary"
:value="this.$t('save')"
:label="this.$t('createvehicle')"
expanded
>
<BaseIcon v-if="tryingToCreate" name="sync" spin />
</b-button>
<p v-if="authError">
There was an error logging in to your account.
{{ $t('loginerror') }}
</p>
</b-field>
</form>

View File

@@ -1,5 +1,5 @@
<script>
import currencyFormtter from 'currency-formatter'
import currencyFormatter from 'currency-formatter'
import appConfig from '@src/app.config'
import Layout from '@layouts/main.vue'
@@ -53,7 +53,7 @@ export default {
return parseAndFormatDate(date)
},
formatCurrency(number) {
return currencyFormtter.format(number, { code: this.me.currency })
return currencyFormatter.format(number, { code: this.me.currency })
},
},
}
@@ -62,14 +62,13 @@ export default {
<template>
<Layout>
<b-notification v-if="myVehicles.length === 0" type="is-warning is-light" :closable="false">
<div class="columns">
<div class="columns is-three-quarters">
<div class="column">
It seems you have not yet created a vehicle in the system. Start by creating an entry for
one of the vehicles you want to track.
{{ $t('novehicles') }}
</div>
<div class="column" :class="!isMobile ? 'has-text-right' : ''">
<div class="column is-one-quarter" :class="!isMobile ? 'has-text-right' : ''">
<b-button type="is-warning" class="" tag="router-link" :to="`/vehicles/create`"
>Create Now</b-button
>{{ $t('createnow') }}</b-button
></div
>
</div>
@@ -81,15 +80,11 @@ export default {
>
<div class="columns">
<div class="column">
{{
`You have ${unprocessedQuickEntries.length} quick ${
unprocessedQuickEntries.length === 1 ? 'entry' : 'entries'
} pending to be processed.`
}}
{{ $tc('unprocessedquickentries', unprocessedQuickEntries.length, { '0': unprocessedQuickEntries.length }) }}
</div>
<div class="column" :class="!isMobile ? 'has-text-right' : ''">
<b-button type="is-warning" class="is-small" tag="router-link" :to="`/quickEntries`"
>Process Now</b-button
>{{ $t('show') }}</b-button
></div
>
</div>
@@ -99,10 +94,10 @@ export default {
<br />
<section>
<div class="columns" :class="isMobile ? 'has-text-centered' : ''"
><div class="column is-three-quarters"> <h1 class="title">Your Vehicles</h1></div>
><div class="column is-three-quarters"> <h1 class="title">{{ $t('yourvehicles') }}</h1></div>
<div class="column is-one-quarter buttons" :class="!isMobile ? 'has-text-right' : ''">
<b-button type="is-primary" tag="router-link" :to="`/vehicles/create`"
>Add Vehicle</b-button
>{{ $t('addvehicle') }}</b-button
>
</div></div
>
@@ -125,22 +120,22 @@ export default {
<div class="content">
<table class="table">
<div class="columns">
<div class="column is-one-third">Last Fillup</div>
<div class="column is-one-third">{{ $t('lastfillup') }}</div>
<div class="column"
>{{ formatDate(vehicle.fillups[0].date) }} <br />
{{ `${formatCurrency(vehicle.fillups[0].totalAmount)}` }} ({{
`${vehicle.fillups[0].fuelQuantity} ${vehicle.fillups[0].fuelUnitDetail.short}`
`${vehicle.fillups[0].fuelQuantity} ${ $t('unit.short.' + vehicle.fillups[0].fuelUnitDetail.key) }`
}}
@ {{ `${formatCurrency(vehicle.fillups[0].perUnitPrice)}` }})</div
>
</div>
<div class="columns">
<div class="column is-one-third">Odometer</div>
<div class="column is-one-third">{{ $t('odometer') }}</div>
<div class="column">
<template v-if="vehicle.fillups.length">
{{ vehicle.fillups[0].odoReading }}&nbsp;{{
me.distanceUnitDetail.short
$t('unit.short.' + me.distanceUnitDetail.key)
}}</template
>
</div>
@@ -150,12 +145,12 @@ export default {
</div>
<footer class="card-footer">
<router-link class="card-footer-item" :to="'/vehicles/' + vehicle.id">
Details
{{ $t('details') }}
</router-link>
<router-link class="card-footer-item" :to="`/vehicles/${vehicle.id}/fillup`">
Add Fillup </router-link
{{ $t('addfillup') }} </router-link
><router-link class="card-footer-item" :to="`/vehicles/${vehicle.id}/expense`">
Add Expense
{{ $t('addexpense') }}
</router-link>
</footer>
</b-collapse>

View File

@@ -0,0 +1,172 @@
<script>
import Layout from '@layouts/main.vue'
import { mapState } from 'vuex'
import axios from 'axios'
export default {
page: {
title: 'Import Drivvo',
meta: [{ name: 'description', content: 'The Import Drivvo page.' }],
},
components: { Layout },
props: {
user: {
type: Object,
required: true,
},
},
data: function() {
return {
myVehicles: [],
file: null,
selectedVehicle: null,
tryingToCreate: false,
errors: [],
importLocation: true,
}
},
computed: {
...mapState('utils', ['isMobile']),
...mapState('vehicles', ['vehicles']),
uploadButtonLabel() {
if (this.isMobile) {
if (this.file == null) {
return 'Choose Photo'
} else {
return ''
}
} else {
if (this.file == null) {
return 'Choose CSV'
} else {
return ''
}
}
},
},
mounted() {
this.myVehicles = this.vehicles
},
methods: {
importDrivvo() {
console.log('Import from drivvo')
if (this.file == null) {
return
}
this.tryingToCreate = true
this.errorMessage = ''
const formData = new FormData()
formData.append('vehicleID', this.selectedVehicle)
formData.append('importLocation', this.importLocation)
formData.append('file', this.file, this.file.name)
axios
.post(`/api/import/drivvo`, formData)
.then((data) => {
this.$buefy.toast.open({
message: 'Data Imported Successfully',
type: 'is-success',
duration: 3000,
})
this.file = null
setTimeout(() => this.$router.push({ name: 'home' }), 1000)
})
.catch((ex) => {
this.$buefy.toast.open({
duration: 5000,
message: 'There was some issue with importing the file. Please check the error message',
position: 'is-bottom',
type: 'is-danger',
})
if (ex.response && ex.response.data.errors) {
this.errors = ex.response.data.errors
}
})
.finally(() => {
this.tryingToCreate = false
})
},
},
}
</script>
<template>
<Layout>
<div class="columns box">
<div class="column">
<h1 class="title">Import from Drivvo</h1>
</div>
</div>
<br />
<div class="columns">
<div class="column">
<p class="subtitle"> Steps to import data from Drivvo</p>
<ol>
<li>Export your data from Drivvo in the CSV format.</li>
<li>Select the vehicle the exported data is for. You may need to create the vehicle in Hammond first if you haven't already done so</li>
<li
>Make sure that the <u>Currency</u> and <u>Distance Unit</u> are set correctly in Hammond. Drivvo does not include this information in
their export, instead Hammond will use the values set for the user.</li
>
<li>Similiarly, make sure that the <u>Fuel Unit</u> and <u>Fuel Type</u> are correctly set in the Vehicle.</li>
<li>Once you have checked all these points, select the vehicle and import the CSV below.</li>
<li><b>Make sure that you do not import the file again as that will create repeat entries.</b></li>
</ol>
</div>
</div>
<p
><b>PS:</b> If you have <em>'income'</em> and <em>'trips'</em> in your export, they will not be imported to Hammond. The fields
<em>'Second fuel'</em> and <em>'Third fuel'</em> are are are also ignored as the use case for these is not understood by us. If you have a use
case for this, please open a issue on
<a href="https://github.com/akhilrex/hammond/issues">issue tracker</a>
</p>
<div class="section box">
<div class="columns is-multiline">
<div class="column is-full"> <p class="subtitle">Choose the vehicle, then select the Drivvo CSV and press the import button.</p></div>
<div class="column is-full is-flex is-align-content-center">
<form @submit.prevent="importDrivvo">
<div class="columns">
<div class="column">
<b-field label="Vehicle" label-position="on-border">
<b-select v-model="selectedVehicle" placeholder="Select Vehicle" required>
<option v-for="vehicle in myVehicles" :key="vehicle.id" :value="vehicle.id">{{ vehicle.nickname }}</option>
</b-select>
</b-field>
</div>
<div class="column">
<b-field>
<b-tooltip label="Whether to import the location for fillups and services or not." multilined>
<b-checkbox v-model="importLocation">Import Location?</b-checkbox>
</b-tooltip>
</b-field>
</div>
<div class="column">
<b-field class="file is-primary" :class="{ 'has-name': !!file }">
<b-upload v-model="file" class="file-label" accept=".csv" required>
<span class="file-cta">
<b-icon class="file-icon" icon="upload"></b-icon>
<span class="file-label">{{ uploadButtonLabel }}</span>
</span>
<span v-if="file" class="file-name" :class="isMobile ? 'file-name-mobile' : 'file-name-desktop'">
{{ file.name }}
</span>
</b-upload>
</b-field>
</div>
<div class="column">
<b-button tag="input" native-type="submit" :disabled="tryingToCreate" type="is-primary" value="Upload File" class="control">
Import
</b-button>
</div></div
>
</form>
</div>
</div>
</div>
<b-message v-if="errors.length" type="is-danger">
<ul>
<li v-for="error in errors" :key="error">{{ error }}</li>
</ul>
</b-message>
</Layout>
</template>

View File

@@ -14,13 +14,13 @@ export default {
uploadButtonLabel() {
if (this.isMobile) {
if (this.file == null) {
return 'Choose Photo'
return this.$t('choosephoto')
} else {
return ''
}
} else {
if (this.file == null) {
return 'Choose CSV'
return this.$t('choosecsv')
} else {
return ''
}
@@ -53,16 +53,17 @@ export default {
.post(`/api/import/fuelly`, formData)
.then((data) => {
this.$buefy.toast.open({
message: 'Data Imported Successfully',
message: this.$t('importsuccessfull'),
type: 'is-success',
duration: 3000,
})
this.file = null
setTimeout(() => this.$router.push({ name: 'home' }), 1000)
})
.catch((ex) => {
this.$buefy.toast.open({
duration: 5000,
message: 'There was some issue with importing the file. Please check the error message',
message: this.$t('importerror'),
position: 'is-bottom',
type: 'is-danger',
})
@@ -82,39 +83,33 @@ export default {
<Layout>
<div class="columns box">
<div class="column">
<h1 class="title">Import from Fuelly</h1>
<h1 class="title">{{ $t('importfrom', { 'name': 'Fuelly' }) }}</h1>
</div>
</div>
<br />
<div class="columns">
<div class="column">
<p class="subtitle"> Steps to import data from Fuelly</p>
<p class="subtitle"> {{ $t('stepstoimport', { 'name': 'Fuelly' }) }}</p>
<ol>
<li
>Export your data from Fuelly in the CSV format. Steps to do that can be found
<a href="http://docs.fuelly.com/acar-import-export-center" target="_nofollow">here</a>.</li
>
<li>Make sure that you have already created the vehicles in Hammond platform.</li>
<li>Make sure that the Vehicle nickname in Hammond is exactly the same as the name on Fuelly CSV or the import will not work.</li>
<li
>Make sure that the <u>Currency</u> and <u>Distance Unit</u> are set correctly in Hammond. Import will not autodetect Currency from the
CSV but use the one set for the user.</li
>
<li>Similiarly, make sure that the <u>Fuel Unit</u> and <u>Fuel Type</u> are correctly set in the Vehicle.</li>
<li>Once you have checked all these points,just import the CSV below.</li>
<li><b>Make sure that you do not import the file again and that will create repeat entries.</b></li>
<li>{{ $t('importhintcreatecsv', { 'name': 'Fuelly' }) }} <a href="http://docs.fuelly.com/acar-import-export-center" target="_nofollow">{{ $t('here') }}</a>.</li>
<li>{{ $t('importhintvehiclecreated') }}</li>
<li>{{ $t('importhintnickname') }}</li>
<li v-html="$t('importhintcurrdist')"></li>
<li v-html="$t('importhintunits')"></li>
<li>{{ $t('checkpointsimportcsv') }}</li>
<li><b>{{ $t('dontimportagain') }}</b></li>
</ol>
</div>
</div>
<div class="section box">
<div class="columns">
<div class="column is-two-thirds"> <p class="subtitle">Choose the Fuelly CSV and press the import button.</p></div>
<div class="column is-two-thirds"> <p class="subtitle">{{ $t('choosecsvimport', { 'name': 'Fuelly' }) }}</p></div>
<div class="column is-one-third is-flex is-align-content-center">
<form @submit.prevent="importFuelly">
<div class="columns"
><div class="column">
<b-field class="file is-primary" :class="{ 'has-name': !!file }">
<b-upload v-model="file" class="file-label" accept=".csv">
<b-upload v-model="file" class="file-label" accept=".csv" required>
<span class="file-cta">
<b-icon class="file-icon" icon="upload"></b-icon>
<span class="file-label">{{ uploadButtonLabel }}</span>
@@ -126,8 +121,8 @@ export default {
</b-field>
</div>
<div class="column">
<b-button tag="input" native-type="submit" :disabled="tryingToCreate" type="is-primary" value="Upload File" class="control">
Import
<b-button tag="input" native-type="submit" :disabled="tryingToCreate" type="is-primary" :value="this.$t('uploadfile')" class="control">
{{ $t('import') }}
</b-button>
</div></div
>

View File

@@ -20,17 +20,28 @@ export default {
<Layout>
<div class="columns box"
><div class="column">
<h1 class="title">Import data into Hammond</h1>
<p class="subtitle">Choose from the following options to import data into Fuelly</p>
<h1 class="title">{{ $t('importdata') }}</h1>
<p class="subtitle">{{ $t('importdatadesc') }}</p>
</div></div
>
<br />
<div class="columns">
<div class="box column is-one-third" to="/import-fuelly">
<h1 class="title">Fuelly</h1>
<p>If you have been using Fuelly to store your vehicle data, export the CSV file from Fuelly and click here to import.</p>
<br />
<b-button type="is-primary" tag="router-link" to="/import/fuelly">Import</b-button>
<div class="column is-one-third">
<div class="box">
<h1 class="title">Fuelly</h1>
<p>If you have been using Fuelly to store your vehicle data, export the CSV file from Fuelly and click here to import.</p>
<br />
<b-button type="is-primary" tag="router-link" to="/import/fuelly">{{ $t('import') }}</b-button>
</div>
</div>
<div class="column is-one-third" to="/import-fuelly">
<div class="box">
<h1 class="title">Drivvo</h1>
<p>{{ $t('importcsv', { 'name': 'Fuelly' }) }}</p>
<br />
<b-button type="is-primary" tag="router-link" to="/import/drivvo">{{ $t('import') }}</b-button>
</div>
</div>
</div>
</Layout>

View File

@@ -62,11 +62,11 @@ export default {
var message = ''
if (this.migrationMode === 'clarkson') {
message =
'We have successfully migrated the data from Clarkson. You will be redirected to the login screen shortly where you can login using your existing email and password : hammond'
this.$t('init.clarkson.success')
}
if (this.migrationMode === 'fresh') {
message =
'You have been registered successfully. You will be redirected to the login screen shortly where you can login and start using the system.'
this.$t('init.fresh.success')
}
this.$buefy.toast.open({
duration: 10000,
@@ -163,68 +163,57 @@ export default {
<template>
<Layout>
<div v-if="!migrationMode" class="box">
<h1 class="title">Migrate from Clarkson</h1>
<h1 class="title">{{ $t('init.migrateclarkson') }}</h1>
<p>
If you have an existing Clarkson deployment and you want to migrate your data from that, press the following button.
{{ $t('init.migrateclarksondesc') }}
</p>
<br />
<b-field> <b-button type="is-primary" @click="migrationMode = 'clarkson'">Migrate from Clarkson</b-button></b-field>
<b-field> <b-button type="is-primary" @click="migrationMode = 'clarkson'">{{ $t('init.migrateclarkson') }}</b-button></b-field>
</div>
<div v-if="!migrationMode" class="box">
<h1 class="title">Fresh Install</h1>
<h1 class="title">{{ $t('init.freshinstall') }}</h1>
<p>
If you want a fresh install of Hammond, press the following button.
{{ $t('init.freshinstalldesc') }}
</p>
<br />
<b-field>
<b-button type="is-primary" @click="migrationMode = 'fresh'">Fresh Install</b-button>
<b-button type="is-primary" @click="migrationMode = 'fresh'">{{ $t('init.freshinstall') }}</b-button>
</b-field>
</div>
<div v-if="migrationMode === 'clarkson'" class="box content">
<h1 class="title">Migrate from Clarkson</h1>
<p>You need to make sure that this deployment of Hammond can access the MySQL database used by Clarkson.</p>
<p>If that is not directly possible, you can make a copy of that database somewhere accessible from this instance.</p>
<p>Once that is done, enter the connection string to the MySQL instance in the following format.</p>
<p
>All the users imported from Clarkson will have their username as their email in Clarkson database and pasword set to
<span class="" style="font-weight:bold">hammond</span></p
>
<code>
user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
</code>
<br />
<br />
<h1 class="title">{{ $t('init.migrateclarkson') }}</h1>
<p v-html="$t('init.clarkson.desc')"></p>
<b-notification v-if="connectionError" type="is-danger" role="alert" :closable="false">
{{ connectionError }}
</b-notification>
<b-field addons label="Mysql Connection String">
<b-field addons :label="this.$t('mysqlconnstr')">
<b-input v-model="url" required></b-input>
</b-field>
<div class="buttons">
<b-button v-if="!testSuccess" type="is-primary" :disabled="isWorking" @click="testConnection">Test Connection</b-button
><b-button v-if="testSuccess" type="is-success" :disabled="isWorking" @click="migrate">Migrate</b-button>
<b-button type="is-danger is-light" @click="resetMigrationMode">Cancel</b-button>
<b-button v-if="!testSuccess" type="is-primary" :disabled="isWorking" @click="testConnection">{{ $t('testconn') }}</b-button>
<b-button v-if="testSuccess" type="is-success" :disabled="isWorking" @click="migrate">{{ $t('migrate') }}</b-button>
<b-button type="is-danger is-light" @click="resetMigrationMode">{{ $t('cancel') }}</b-button>
</div>
</div>
<div v-if="migrationMode === 'fresh'" class="box content">
<h1 class="title">Setup Admin Users</h1>
<h1 class="title">{{ $t('init.fresh.setupadminuser') }}</h1>
<form @submit.prevent="register">
<b-field label="Your Name">
<b-field :label="this.$t('init.fresh.yourname')">
<b-input v-model="registerModel.name" required></b-input>
</b-field>
<b-field label="Your Email">
<b-field :label="this.$t('init.fresh.youremail')">
<b-input v-model="registerModel.email" type="email" required></b-input>
</b-field>
<b-field label="Your Password">
<b-field :label="this.$t('init.fresh.yourpassword')">
<b-input v-model="registerModel.password" type="password" required minlength="8" password-reveal></b-input>
</b-field>
<b-field label="Currency">
<b-field :label="this.$t('currency')">
<b-autocomplete
v-model="registerModel.currency"
:custom-formatter="formatCurrency"
placeholder="Currency"
:placeholder="this.$t('currency')"
:data="filteredCurrencyMasters"
:keep-first="true"
:open-on-focus="true"
@@ -232,18 +221,18 @@ export default {
@select="(option) => (selected = option)"
></b-autocomplete>
</b-field>
<b-field label="Distance Unit">
<b-select v-model.number="registerModel.distanceUnit" placeholder="Distance Unit" required expanded>
<b-field :label="this.$t('distanceunit')">
<b-select v-model.number="registerModel.distanceUnit" :placeholder="this.$t('distanceunit')" required expanded>
<option v-for="(option, key) in distanceUnitMasters" :key="key" :value="key">
{{ `${option.long} (${option.short})` }}
{{ `${$t('unit.long.' + option.key)} (${$t('unit.short.' + option.key)})` }}
</option>
</b-select>
</b-field>
<br />
<div class="buttons">
<b-button type="is-primary" native-type="submit" tag="input"></b-button>
<b-button type="is-primary" native-type="submit" tag="input" :value="this.$t('save')"></b-button>
<b-button type="is-danger is-light" @click="resetMigrationMode">Cancel</b-button>
<b-button type="is-danger is-light" @click="resetMigrationMode">{{ $t('cancel') }}</b-button>
</div>
</form>
</div>

View File

@@ -24,8 +24,8 @@ export default {
return process.env.NODE_ENV === 'production'
? {}
: {
username: 'Enter your username',
password: 'Enter your password',
username: this.$t('enterusername'),
password: this.$t('enterpassword'),
}
},
},
@@ -67,17 +67,17 @@ export default {
<template>
<Layout>
<form @submit.prevent="tryToLogIn">
<b-field label="Email"> <b-input v-model="username" tag="b-input" name="username" type="email" :placeholder="placeholders.username"/></b-field>
<b-field label="Password">
<b-field :label="$t('email')"> <b-input v-model="username" tag="b-input" name="username" type="email" :placeholder="placeholders.username"/></b-field>
<b-field :label="$t('password')">
<b-input v-model="password" tag="b-input" name="password" type="password" :placeholder="placeholders.password" />
</b-field>
<b-button tag="input" native-type="submit" :disabled="tryingToLogIn" type="is-primary">
<b-button tag="input" native-type="submit" :value="$t('login')" :disabled="tryingToLogIn" type="is-primary">
<BaseIcon v-if="tryingToLogIn" name="sync" spin />
<span v-else>
Log in
{{ $t('login') }}
</span>
</b-button>
<p v-if="authError"> There was an error logging in to your account. {{ errorMessage }} </p>
<p v-if="authError"> {{ $t('loginerror', { msg: errorMessage }) }}</p>
</form>
</Layout>
</template>

View File

@@ -28,7 +28,7 @@ export default {
<h1>
<BaseIcon name="user" />
{{ user.name }}
Profile
{{ $t('profile') }}
</h1>
<pre>{{ user }}</pre>
</Layout>

View File

@@ -41,7 +41,7 @@ export default {
store.dispatch('vehicles/setQuickEntryAsProcessed', { id: entry.id }).then((data) => {})
},
deleteQuickEntry(entry) {
var sure = confirm('This will delete this Quick Entry. This step cannot be reversed. Are you sure?')
var sure = confirm(this.$t('deletequickentry'))
if (sure) {
store.dispatch('vehicles/deleteQuickEntry', { id: entry.id }).then((data) => {})
}
@@ -59,9 +59,9 @@ export default {
<template>
<Layout>
<h1 class="title">Quick Entries</h1>
<h1 class="title">{{ $tc('quickentry', 2) }}</h1>
<b-field>
<b-switch v-if="unprocessedQuickEntries.length" v-model="showUnprocessedOnly">Show unprocessed only</b-switch>
<b-switch v-if="unprocessedQuickEntries.length" v-model="showUnprocessedOnly">{{ $t('showunprocessed') }}</b-switch>
</b-field>
<div v-for="(chunk, index) in chunkedQuickEntries" :key="index" class="tile is-ancestor">
<div v-for="entry in chunk" :key="entry.id" class="tile is-parent" :class="{ 'is-4': quickEntries.length <= 3 }">
@@ -71,7 +71,7 @@ export default {
<div class="card-header-title">
{{ parseAndFormatDateTime(entry.createdAt) }}
</div>
<b-tag v-if="entry.processDate === null" class="is-align-content-center" type="is-primary">unprocessed</b-tag>
<b-tag v-if="entry.processDate === null" class="is-align-content-center" type="is-primary">{{ $t('unprocessed') }}</b-tag>
</div>
<div class="card-image">
<!-- prettier-ignore -->
@@ -87,22 +87,22 @@ export default {
>
<footer class="card-footer">
<router-link v-if="entry.processDate === null && vehicles.length" :to="`/vehicles/${vehicles[0].id}/fillup`" class="card-footer-item"
>Create Fillup</router-link
>{{ $t('addfillup') }}</router-link
>
<router-link v-if="entry.processDate === null && vehicles.length" :to="`/vehicles/${vehicles[0].id}/expense`" class="card-footer-item"
>Create Expense</router-link
>{{ $t('addexpense') }}</router-link
>
<a v-if="entry.processDate === null" class="card-footer-item" @click="markProcessed(entry)">Mark Processed</a>
<p v-else class="card-footer-item">Processed on {{ parseAndFormatDateTime(entry.processDate) }}</p>
<a class="card-footer-item" type="is-danger" @click="deleteQuickEntry(entry)"> Delete</a>
<a v-if="entry.processDate === null" class="card-footer-item" @click="markProcessed(entry)">{{ $t('processed') }}</a>
<p v-else class="card-footer-item">{{ $t('processedon') }} {{ parseAndFormatDateTime(entry.processDate) }}</p>
<a class="card-footer-item" type="is-danger" @click="deleteQuickEntry(entry)"> {{ $t('delete') }}</a>
</footer>
</div>
</div>
</div>
</div>
<div v-if="!quickEntries.length" class="box">
<p>No Quick Entries right now.</p>
<p>{{ $tc('quickentry',0) }}</p>
</div>
</Layout>
</template>

View File

@@ -106,7 +106,7 @@ export default {
.dispatch(`utils/saveUserSettings`, { settings: this.settingsModel })
.then((data) => {
this.$buefy.toast.open({
message: 'Settings saved successfully',
message: this.$t('settingssaved'),
type: 'is-success',
duration: 3000,
})
@@ -132,18 +132,18 @@ export default {
<template>
<Layout>
<h1 class="title">Your Settings</h1>
<h1 class="title">{{ $t('yoursettings') }}</h1>
<div class="columns"
><div class="column">
<form class="box " @submit.prevent="saveSettings">
<h1 class="subtitle">
These will be used as default values whenever you create a new fillup or expense.
{{ $t('settingdesc') }}
</h1>
<b-field label="Currency">
<b-field :label="$t('currency')">
<b-autocomplete
v-model="settingsModel.currency"
:custom-formatter="formatCurrency"
placeholder="Currency"
:placeholder="$t('currency')"
:data="filteredCurrencyMasters"
:keep-first="true"
:open-on-focus="true"
@@ -151,14 +151,14 @@ export default {
@select="(option) => (selected = option)"
></b-autocomplete>
</b-field>
<b-field label="Distance Unit">
<b-field :label="$t('distanceunit')">
<b-select v-model.number="settingsModel.distanceUnit" placeholder="Distance Unit" required expanded>
<option v-for="(option, key) in distanceUnitMasters" :key="key" :value="key">
{{ `${option.long} (${option.short})` }}
{{ `${$t('unit.long.' + option.key)} (${$t('unit.short.' + option.key)})` }}
</option>
</b-select>
</b-field>
<b-field label="Date Format">
<b-field :label="$t('dateformat')">
<b-select v-model.number="settingsModel.dateFormat" placeholder="Date Format" required expanded>
<option v-for="option in dateFormatMasters" :key="option" :value="option">
{{ `${option}` }}
@@ -167,25 +167,26 @@ export default {
</b-field>
<br />
<b-field>
<b-button tag="input" native-type="submit" :disabled="tryingToSave" type="is-primary" value="Save" expanded> </b-button>
<b-button tag="input" native-type="submit" :disabled="tryingToSave" type="is-primary" :value="$t('save')" expanded> </b-button>
</b-field>
</form>
</div>
<div class="column">
<form class="box" @submit.prevent="changePassword">
<h1 class="subtitle">Change password</h1>
<b-field label="Old Password">
<h1 class="subtitle">{{ $t('changepassword') }}</h1>
<b-field :label="$t('oldpassword')">
<b-input v-model="changePassModel.old" required minlength="6" password-reveal type="password"></b-input>
</b-field>
<b-field label="New Password">
<b-field :label="$t('newpassword')">
<b-input v-model="changePassModel.new" required minlength="6" password-reveal type="password"></b-input>
</b-field>
<b-field label="Repeat New Password">
<b-field :label="$t('repeatnewpassword')">
<b-input v-model="changePassModel.renew" required minlength="6" password-reveal type="password"></b-input>
</b-field>
<p v-if="!passwordValid" class="help is-danger">Password values don't match</p>
<p v-if="!passwordValid" class="help is-danger">{{ $t('passworddontmatch') }}</p>
<b-field>
<b-button tag="input" native-type="submit" :disabled="!passwordValid" type="is-primary" value="Change Password" expanded> </b-button>
<b-button tag="input" native-type="submit" :disabled="!passwordValid" type="is-primary" :value="$t('changepassword')" expanded>
</b-button>
</b-field>
</form>
</div>
@@ -193,48 +194,32 @@ export default {
<hr />
<div class="columns">
<div class="twelve">
<h3 class="title">More Info</h3>
<p style="font-style: italic;">
This project is under active development which means I release new updates very frequently. I will eventually build the version
management/update checking mechanism. Until then it is recommended that you use something like watchtower which will automatically update
your containers whenever I release a new version or periodically rebuild the container with the latest image manually.
</p>
<br />
<h3 class="title">{{ $t('moreinfo') }}</h3>
<table class="table is-hoverable">
<tr>
<td>Current Version</td>
<td>{{ $t('currentversion') }}</td>
<td>2022.07.06</td>
</tr>
<tr>
<td>Website</td>
<td><a href="https://github.com/akhilrex/hammond" target="_blank">https://github.com/akhilrex/hammond</a></td>
<td><a href="https://github.com/alfhou/hammond" target="_blank">https://github.com/alfhou/hammond</a></td>
</tr>
<tr>
<td>Found a bug</td>
<td>{{ $t('foundabug') }}</td>
<td
><a
href="https://github.com/akhilrex/hammond/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc"
target="_blank"
rel="noopener noreferrer"
><a href="https://github.com/alfhou/hammond/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc" target="_blank" rel="noopener noreferrer"
>Report here</a
></td
>
</tr>
<tr>
<td>Feature Request</td>
<td>{{ $t('featurerequest') }}</td>
<td
><a
href="https://github.com/akhilrex/hammond/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc"
target="_blank"
rel="noopener noreferrer"
><a href="https://github.com/alfhou/hammond/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc" target="_blank" rel="noopener noreferrer"
>Request here</a
></td
>
</tr>
<tr>
<td>Support the developer</td>
<td><a href="https://www.buymeacoffee.com/akhilrex" target="_blank" rel="noopener noreferrer">Buy him a beer!</a></td>
</tr>
</table>
</div>
</div>

View File

@@ -37,7 +37,7 @@ export default {
.dispatch(`utils/saveSettings`, { settings: this.settingsModel })
.then((data) => {
this.$buefy.toast.open({
message: 'Settings saved successfully',
message: this.$t('settingssaved'),
type: 'is-success',
duration: 3000,
})
@@ -63,32 +63,32 @@ export default {
<div class="">
<div class="columns">
<div class="column">
<h1 class="title">Site Settings</h1>
<h1 class="title">{{ $t('menu.sitesettings') }}</h1>
<h1 class="subtitle">
Update site level settings. These will be used as default values for new users.
{{ $t('sitesettingdesc') }}
</h1>
</div>
</div>
</div>
<br />
<form class="" @submit.prevent="saveSettings">
<b-field label="Currency">
<b-select v-model="settingsModel.currency" placeholder="Currency" required expanded>
<b-field :label="this.$t('currency')">
<b-select v-model="settingsModel.currency" :placeholder="this.$t('currency')" required expanded>
<option v-for="option in currencyMasters" :key="option.code" :value="option.code">
{{ `${option.namePlural} (${option.code})` }}
</option>
</b-select>
</b-field>
<b-field label="Distance Unit">
<b-select v-model.number="settingsModel.distanceUnit" placeholder="Distance Unit" required expanded>
<b-field :label="this.$t('distanceunit')">
<b-select v-model.number="settingsModel.distanceUnit" :placeholder="this.$t('distanceunit')" required expanded>
<option v-for="(option, key) in distanceUnitMasters" :key="key" :value="key">
{{ `${option.long} (${option.short})` }}
{{ `${$t('unit.long.' + option.key)} (${$t('unit.short.' + option.key)})` }}
</option>
</b-select>
</b-field>
<br />
<b-field>
<b-button tag="input" native-type="submit" :disabled="tryingToSave" type="is-primary" value="Save" expanded> </b-button>
<b-button tag="input" native-type="submit" :disabled="tryingToSave" type="is-primary" :value="this.$t('save')" expanded> </b-button>
</b-field>
</form>
</Layout>

View File

@@ -55,18 +55,18 @@ export default {
},
changeDisabledStatus(userId,status){
this.$buefy.dialog.confirm({
title: status?'Disable User':"Enable User",
message: 'Are you sure you want to do this?',
cancelText: 'Cancel',
confirmText: 'Go Ahead',
title: status ? this.$t('disable') : this.$t('enable'),
message: this.$t('areyousure'),
cancelText: this.$t('cancel'),
confirmText: this.$t('confirm'),
onConfirm: () => {
var url = `/api/users/${userId}/${status?"disable":"enable"}`
var url = `/api/users/${userId}/${status ? "disable" : "enable"}`
axios
.post(url, {})
.then((data) => {
this.$buefy.toast.open({
message: status?"User disabled successfully":'User enabled successfully',
message: status ? this.$t('userdisabledsuccessfully') : this.$t('userenabledsuccessfully'),
type: 'is-success',
duration: 3000,
})
@@ -103,7 +103,7 @@ export default {
if (success) {
this.$buefy.toast.open({
duration: 10000,
message: 'User Created Successfully',
message: this.$t('usercreatedsuccessfully'),
position: 'is-bottom',
type: 'is-success',
})
@@ -129,22 +129,22 @@ export default {
<Layout>
<div class="box">
<div class="columns">
<div class="column is-three-quarters"> <h1 class="title is-4">Users</h1> </div>
<div class="column is-three-quarters"> <h1 class="title is-4">{{ $t('menu.users') }}</h1> </div>
<div class="column is-one-quarter">
<b-button type="is-primary" @click="showUserForm = true">Add User</b-button>
<b-button type="is-primary" @click="showUserForm = true">{{ $t('adduser') }}</b-button>
</div>
</div>
<div v-if="showUserForm" class="box content">
<h1 class="title">Create New User</h1>
<h1 class="title">{{ $t('createnewuser') }}</h1>
<form @submit.prevent="register">
<b-field label="Name">
<b-field :label="this.$t('name')">
<b-input v-model="registerModel.name" required></b-input>
</b-field>
<b-field label="Email">
<b-field :label="this.$t('email')">
<b-input v-model="registerModel.email" type="email" required></b-input>
</b-field>
<b-field label="Password">
<b-field :label="this.$t('password')">
<b-input
v-model="registerModel.password"
type="password"
@@ -153,56 +153,56 @@ export default {
password-reveal
></b-input>
</b-field>
<b-field label="Role">
<b-select v-model.number="registerModel.role" placeholder="Role" required expanded>
<b-field :label="this.$t('role')">
<b-select v-model.number="registerModel.role" :placeholder="this.$t('placeholder')" required expanded>
<option v-for="(option, key) in roleMasters" :key="key" :value="key">
{{ `${option.long}` }}
{{ `test` }}
</option>
</b-select>
</b-field>
<b-field label="Currency">
<b-select v-model="registerModel.currency" placeholder="Currency" required expanded>
<b-field :label="this.$t('currency')">
<b-select v-model="registerModel.currency" :placeholder="this.$t('currency')" required expanded>
<option v-for="option in currencyMasters" :key="option.code" :value="option.code">
{{ `${option.namePlural} (${option.code})` }}
</option>
</b-select>
</b-field>
<b-field label="Distance Unit">
<b-field :label="this.$t('distanceunit')">
<b-select
v-model.number="registerModel.distanceUnit"
placeholder="Distance Unit"
:placeholder="this.$t('distanceunit')"
required
expanded
>
<option v-for="(option, key) in distanceUnitMasters" :key="key" :value="key">
{{ `${option.long} (${option.short})` }}
{{ `${$t('unit.long.' + option.key)} (${$t('unit.short.' + option.key)})` }}
</option>
</b-select>
</b-field>
<br />
<div class="buttons">
<b-button type="is-primary" native-type="submit" tag="input"></b-button>
<b-button type="is-primary" native-type="submit" tag="input" :value="this.$t('save')"></b-button>
<b-button type="is-danger is-light" @click="resetUserForm">Cancel</b-button>
<b-button type="is-danger is-light" @click="resetUserForm">{{ $t('cancel') }}</b-button>
</div>
</form>
</div>
<b-table :data="users" hoverable mobile-cards detail-key="id" paginated per-page="10" :row-class="(row, index) => row.isDisabled && 'is-disabled'">
<b-table-column v-slot="props" field="name" label="Name">
{{ `${props.row.name}` }} <template v-if="props.row.id === user.id">(You)</template>
<b-table-column v-slot="props" field="name" :label="this.$t('name')">
{{ `${props.row.name}` }} <template v-if="props.row.id === user.id">({{ $t('you') }})</template>
</b-table-column>
<b-table-column v-slot="props" field="email" label="Email">
<b-table-column v-slot="props" field="email" :label="this.$t('email')">
{{ `${props.row.email}` }}
</b-table-column>
<b-table-column v-slot="props" field="role" label="Role">
{{ `${props.row.roleDetail.short}` }}
<b-table-column v-slot="props" field="role" :label="this.$t('role')">
{{ `${$t('roles.' + props.row.roleDetail.key)}` }}
</b-table-column>
<b-table-column v-slot="props" field="createdAt" label="Created" sortable date>
<b-table-column v-slot="props" field="createdAt" :label="this.$t('created')" sortable date>
{{ formatDate(props.row.createdAt) }}
</b-table-column>
<b-table-column v-slot="props">
<b-button type="is-success" v-if="props.row.isDisabled && props.row.roleDetail.long === 'USER'" @click="changeDisabledStatus(props.row.id, false)">Enable</b-button>
<b-button type="is-danger" v-if="!props.row.isDisabled && props.row.roleDetail.long === 'USER'" @click="changeDisabledStatus(props.row.id, true)">Disable</b-button>
<b-button type="is-success" v-if="props.row.isDisabled && props.row.roleDetail.key === 'USER'" @click="changeDisabledStatus(props.row.id, false)">{{ $t('enable') }}</b-button>
<b-button type="is-danger" v-if="!props.row.isDisabled && props.row.roleDetail.key === 'USER'" @click="changeDisabledStatus(props.row.id, true)">{{ $t('disable') }}</b-button>
</b-table-column>
</b-table>
</div>

View File

@@ -4,7 +4,7 @@ import { parseAndFormatDate } from '@utils/format-date'
import { mapState } from 'vuex'
import { addDays, addMonths } from 'date-fns'
import axios from 'axios'
import currencyFormtter from 'currency-formatter'
import currencyFormatter from 'currency-formatter'
import store from '@state/store'
import ShareVehicle from '@components/shareVehicle.vue'
import MileageChart from '@components/mileageChart.vue'
@@ -40,14 +40,20 @@ export default {
stats: null,
users: [],
dateRangeOptions: [
{ label: 'This week', value: 'this_week' },
{ label: 'This month', value: 'this_month' },
{ label: 'Past 30 days', value: 'past_30_days' },
{ label: 'Past 3 months', value: 'past_3_months' },
{ label: 'This year', value: 'this_year' },
{ label: 'All Time', value: 'all_time' },
{ label: this.$t('thisweek'), value: 'this_week' },
{ label: this.$t('thismonth'), value: 'this_month' },
{ label: this.$tc('pastxdays', 30), value: 'past_30_days' },
{ label: this.$tc('pastxmonths', 3), value: 'past_3_months' },
{ label: this.$t('thisyear'), value: 'this_year' },
{ label: this.$t('alltime'), value: 'all_time' },
],
dateRangeOption: 'past_30_days',
mileageOptions: [
{ label: 'L/100km', value: 'litre_100km' },
{ label: 'km/L', value: 'km_litre' },
{ label: 'mpg', value: 'mpg' },
],
mileageOption: 'litre_100km',
}
},
computed: {
@@ -61,32 +67,32 @@ export default {
return this.stats.map((x) => {
return [
{
label: 'Currency',
label: this.$t('currency'),
value: x.currency,
},
{
label: 'Total Expenditure',
label: this.$t('totalexpenses'),
value: this.formatCurrency(x.expenditureTotal, x.currency),
},
{
label: 'Fillup Costs',
label: this.$t('fillupcost'),
value: `${this.formatCurrency(x.expenditureFillups, x.currency)} (${x.countFillups})`,
},
{
label: 'Other Expenses',
label: this.$t('otherexpenses'),
value: `${this.formatCurrency(x.expenditureExpenses, x.currency)} (${x.countExpenses})`,
},
{
label: 'Avg Fillup Expense',
label: this.$t('avgfillupexpense'),
value: `${this.formatCurrency(x.avgFillupCost, x.currency)}`,
},
{
label: 'Avg Fillup Qty',
value: `${x.avgFuelQty} ${this.vehicle.fuelUnitDetail.short}`,
label: this.$t('avgfillupqty'),
value: `${x.avgFuelQty} ${this.$t('unit.short.' + this.vehicle.fuelUnitDetail.key)}`,
},
{
label: 'Avg Fuel Cost',
value: `${this.formatCurrency(x.avgFuelPrice, x.currency)} per ${this.vehicle.fuelUnitDetail.short}`,
label: this.$t('avgfuelcost'),
value: this.$t('per', {'0': this.formatCurrency(x.avgFuelPrice, x.currency), '1': this.$t('unit.short.' + this.vehicle.fuelUnitDetail.key)}),
},
]
})
@@ -240,7 +246,7 @@ export default {
if (!currencyCode) {
currencyCode = this.me.currency
}
return currencyFormtter.format(number, { code: currencyCode })
return currencyFormatter.format(number, { code: currencyCode })
},
columnTdAttrs(row, column) {
return null
@@ -300,18 +306,18 @@ export default {
<template>
<Layout>
<div class="columns box">
<div class="column is-two-thirds" :class="isMobile ? 'has-text-centered' : ''">
<div class="column is-one-half" :class="isMobile ? 'has-text-centered' : ''">
<p class="title">{{ vehicle.nickname }} - {{ vehicle.registration }}</p>
<p class="subtitle">
{{ [vehicle.make, vehicle.model, vehicle.fuelTypeDetail.long].join(' | ') }}
{{ [vehicle.make, vehicle.model, this.$t('fuel.' + vehicle.fuelTypeDetail.key)].join(' | ') }}
<template v-if="users.length > 1">
| Shared with :
| {{ $t("sharedwith") }} :
{{
users
.map((x) => {
if (x.userId === me.id) {
return 'You'
return this.$t('you')
} else {
return x.name
}
@@ -321,13 +327,13 @@ export default {
</template>
</p>
</div>
<div class="column is-one-third buttons has-text-centered">
<b-button type="is-primary" tag="router-link" :to="`/vehicles/${vehicle.id}/fillup`">Add Fillup</b-button>
<b-button type="is-primary" tag="router-link" :to="`/vehicles/${vehicle.id}/expense`">Add Expense</b-button>
<div :class="(!isMobile ? 'has-text-right ' : '') + 'column is-one-half buttons'">
<b-button type="is-primary" tag="router-link" :to="`/vehicles/${vehicle.id}/fillup`">{{ this.$t('addfillup') }}</b-button>
<b-button type="is-primary" tag="router-link" :to="`/vehicles/${vehicle.id}/expense`">{{ this.$t('addexpense') }}</b-button>
<b-button
v-if="vehicle.isOwner"
tag="router-link"
title="Edit Vehicle"
:title="$t('editvehicle')"
:to="{
name: 'vehicle-edit',
props: { vehicle: vehicle },
@@ -336,10 +342,10 @@ export default {
>
<b-icon pack="fas" icon="edit" type="is-info"> </b-icon
></b-button>
<b-button v-if="vehicle.isOwner" title="Share vehicle" @click="showShareVehicleModal">
<b-button v-if="vehicle.isOwner" :title="$t('sharevehicle')" @click="showShareVehicleModal">
<b-icon pack="fas" icon="user-friends" type="is-info"> </b-icon
></b-button>
<b-button v-if="vehicle.isOwner" title="Delete Vehicle" @click="deleteVehicle">
<b-button v-if="vehicle.isOwner" :title="$t('deletevehicle')" @click="deleteVehicle">
<b-icon pack="fas" icon="trash" type="is-danger"> </b-icon
></b-button>
</div>
@@ -353,45 +359,45 @@ export default {
</div>
</div>
<div class="box">
<h1 class="title is-4">Past Fillups</h1>
<h1 class="title is-4">{{ $t('pastfillups') }}</h1>
<b-table :data="fillups" hoverable mobile-cards :detailed="isMobile" detail-key="id" paginated per-page="10">
<b-table-column v-slot="props" field="date" label="Date" :td-attrs="columnTdAttrs" sortable date>
<b-table-column v-slot="props" field="date" :label="this.$t('date')" :td-attrs="columnTdAttrs" sortable date>
{{ formatDate(props.row.date) }}
</b-table-column>
<b-table-column v-slot="props" field="fuelSubType" label="Fuel Sub Type" :td-attrs="columnTdAttrs">
<b-table-column v-slot="props" field="fuelSubType" :label="this.$t('fuelsubtype')" :td-attrs="columnTdAttrs">
{{ props.row.fuelSubType }}
</b-table-column>
<b-table-column v-slot="props" field="fuelQuantity" label="Qty." :td-attrs="hiddenMobile" numeric>
{{ `${props.row.fuelQuantity} ${props.row.fuelUnitDetail.short}` }}
<b-table-column v-slot="props" field="fuelQuantity" :label="this.$t('quantity')" :td-attrs="hiddenMobile" numeric>
{{ `${props.row.fuelQuantity} ${$t('unit.short.' + props.row.fuelUnitDetail.key)}` }}
</b-table-column>
<b-table-column
v-slot="props"
field="perUnitPrice"
:label="'Price per ' + vehicle.fuelUnitDetail.short"
:label="this.$t('per', { '0': this.$t('price'), '1': this.$t('unit.short.' + vehicle.fuelUnitDetail.key) })"
:td-attrs="hiddenMobile"
numeric
sortable
>
{{ `${formatCurrency(props.row.perUnitPrice, props.row.currency)}` }}
</b-table-column>
<b-table-column v-if="isMobile" v-slot="props" field="totalAmount" label="Total" :td-attrs="hiddenDesktop" sortable numeric>
{{ `${me.currency} ${props.row.totalAmount}` }} ({{ `${props.row.fuelQuantity} ${props.row.fuelUnitDetail.short}` }} @
<b-table-column v-if="isMobile" v-slot="props" field="totalAmount" :label="this.$t('total')" :td-attrs="hiddenDesktop" sortable numeric>
{{ `${me.currency} ${props.row.totalAmount}` }} ({{ `${props.row.fuelQuantity} ${$t('unit.short.' + props.row.fuelUnitDetail.key)}` }} @
{{ `${me.currency} ${props.row.perUnitPrice}` }})
</b-table-column>
<b-table-column v-if="!isMobile" v-slot="props" field="totalAmount" label="Total" :td-attrs="hiddenMobile" sortable numeric>
<b-table-column v-if="!isMobile" v-slot="props" field="totalAmount" :label="this.$t('total')" :td-attrs="hiddenMobile" sortable numeric>
{{ `${formatCurrency(props.row.totalAmount, props.row.currency)}` }}
</b-table-column>
<b-table-column v-slot="props" width="20" field="isTankFull" label="Tank Full" :td-attrs="hiddenMobile">
<b-table-column v-slot="props" width="20" field="isTankFull" :label="this.$t('fulltank')" :td-attrs="hiddenMobile">
<b-icon pack="fas" :icon="props.row.isTankFull ? 'check' : 'times'" type="is-info"> </b-icon>
</b-table-column>
<b-table-column v-slot="props" field="odoReading" label="Odometer Reading" :td-attrs="hiddenMobile" numeric>
{{ `${props.row.odoReading} ${me.distanceUnitDetail.short}` }}
<b-table-column v-slot="props" field="odoReading" :label="this.$t('odometer')" :td-attrs="hiddenMobile" numeric>
{{ `${props.row.odoReading} ${$t('unit.short.' + me.distanceUnitDetail.key)}` }}
</b-table-column>
<b-table-column v-slot="props" field="fillingStation" label="Fillup Station" :td-attrs="hiddenMobile">
<b-table-column v-slot="props" field="fillingStation" :label="this.$t('gasstation')" :td-attrs="hiddenMobile">
{{ `${props.row.fillingStation}` }}
</b-table-column>
<b-table-column v-slot="props" field="userId" label="By" :td-attrs="hiddenMobile">
<b-table-column v-slot="props" field="userId" :label="this.$t('by')" :td-attrs="hiddenMobile">
{{ `${props.row.user.name}` }}
</b-table-column>
<b-table-column v-slot="props">
@@ -406,11 +412,11 @@ export default {
>
<b-icon pack="fas" icon="edit" type="is-info"> </b-icon
></b-button>
<b-button type="is-ghost" title="Delete this fillup" @click="deleteFillup(props.row.id)">
<b-button type="is-ghost" :title="$t('deletefillup')" @click="deleteFillup(props.row.id)">
<b-icon pack="fas" icon="trash" type="is-danger"> </b-icon
></b-button>
</b-table-column>
<template v-slot:empty> No Fillups so far</template>
<template v-slot:empty> {{ $t('nofillups') }}</template>
<template v-slot:detail="props">
<p>{{ props.row.id }}</p>
</template>
@@ -418,25 +424,25 @@ export default {
</div>
<br />
<div class="box">
<h1 class="title is-4">Past Expenses</h1>
<h1 class="title is-4">{{ $t('expenses') }}</h1>
<b-table :data="expenses" hoverable mobile-cards paginated per-page="10">
<b-table-column v-slot="props" field="date" label="Date" :td-attrs="columnTdAttrs" date>
<b-table-column v-slot="props" field="date" :label="this.$t('date')" :td-attrs="columnTdAttrs" date>
{{ formatDate(props.row.date) }}
</b-table-column>
<b-table-column v-slot="props" field="expenseType" label="Expense Type">
<b-table-column v-slot="props" field="expenseType" :label="this.$t('expensetype')">
{{ `${props.row.expenseType}` }}
</b-table-column>
<b-table-column v-slot="props" field="amount" label="Total" :td-attrs="hiddenMobile" sortable numeric>
<b-table-column v-slot="props" field="amount" :label="this.$t('total')" :td-attrs="hiddenMobile" sortable numeric>
{{ `${formatCurrency(props.row.amount, props.row.currency)}` }}
</b-table-column>
<b-table-column v-slot="props" field="odoReading" label="Odometer Reading" :td-attrs="columnTdAttrs" numeric>
{{ `${props.row.odoReading} ${me.distanceUnitDetail.short}` }}
<b-table-column v-slot="props" field="odoReading" :label="this.$t('odometer')" :td-attrs="columnTdAttrs" numeric>
{{ `${props.row.odoReading} ${$t('unit.short.' + me.distanceUnitDetail.key)}` }}
</b-table-column>
<b-table-column v-slot="props" field="userId" label="By" :td-attrs="columnTdAttrs">
<b-table-column v-slot="props" field="userId" :label="this.$t('by')" :td-attrs="columnTdAttrs">
{{ `${props.row.user.name}` }}
</b-table-column>
<b-table-column v-slot="props">
@@ -451,20 +457,20 @@ export default {
>
<b-icon pack="fas" icon="edit" type="is-info"> </b-icon
></b-button>
<b-button type="is-ghost" title="Delete this expense" @click="deleteExpense(props.row.id)">
<b-button type="is-ghost" :title="$t('deleteexpense')" @click="deleteExpense(props.row.id)">
<b-icon pack="fas" icon="trash" type="is-danger"> </b-icon
></b-button>
</b-table-column>
<template v-slot:empty> No Expenses so far</template>
<template v-slot:empty> {{ $t('noexpenses') }}</template>
</b-table>
</div>
<br />
<div class="box">
<div class="columns">
<div class="column is-three-quarters"> <h1 class="title is-4">Attachments</h1></div>
<div class="column is-three-quarters"> <h1 class="title is-4">{{ $t('attachments') }}</h1></div>
<div class="column buttons">
<b-button type="is-primary" @click="showAttachmentForm = true">
Add Attachment
{{ $t('addattachment') }}
</b-button>
</div>
</div>
@@ -478,7 +484,7 @@ export default {
<b-upload v-model="file" class="file-label" required>
<span class="file-cta">
<b-icon class="file-icon" icon="upload"></b-icon>
<span class="file-label">Choose File</span>
<span class="file-label">{{ $t('choosefile') }}</span>
</span>
<span v-if="file" class="file-name" :class="isMobile ? 'file-name-mobile' : 'file-name-desktop'">
{{ file.name }}
@@ -486,7 +492,7 @@ export default {
</b-upload>
</b-field>
<b-field>
<b-input v-model="title" required placeholder="Label for this file"></b-input>
<b-input v-model="title" required :placeholder="this.$t('labelforfile')"></b-input>
</b-field>
<b-field class="buttons">
@@ -510,33 +516,44 @@ export default {
</div>
<b-table :data="attachments" hoverable mobile-cards>
<b-table-column v-slot="props" field="title" label="Title" :td-attrs="columnTdAttrs">
<b-table-column v-slot="props" field="title" :label="this.$t('title')" :td-attrs="columnTdAttrs">
{{ `${props.row.title}` }}
</b-table-column>
<b-table-column v-slot="props" field="originalName" label="Name" :td-attrs="columnTdAttrs">
<b-table-column v-slot="props" field="originalName" :label="this.$t('name')" :td-attrs="columnTdAttrs">
{{ `${props.row.originalName}` }}
</b-table-column>
<b-table-column v-slot="props" field="id" label="Download" :td-attrs="columnTdAttrs">
<b-table-column v-slot="props" field="id" :label="this.$t('download')" :td-attrs="columnTdAttrs">
<b-button tag="a" :href="`/api/attachments/${props.row.id}/file?access_token=${currentUser.token}`" :download="props.row.originalName">
<b-icon type="is-primary" icon="download"></b-icon>
</b-button>
</b-table-column>
<template v-slot:empty> No Attachments so far</template>
<template v-slot:empty> {{ $t('noattachments') }}</template>
</b-table>
</div>
<div class="box">
<div class="columns">
<div class="column" :class="isMobile ? 'has-text-centered' : ''"> <h1 class="title">Stats</h1></div>
<div class="column" :class="isMobile ? 'has-text-centered' : ''"> <h1 class="title">{{ $t('statistics') }}</h1></div>
<div class="column">
<b-select v-model="dateRangeOption" class="is-pulled-right is-medium">
<option v-for="option in dateRangeOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</b-select></div
>
<div class="columns is-pulled-right is-medium">
<div class="column">
<b-select v-model="mileageOption">
<option v-for="option in mileageOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</b-select>
</div>
<div class="column">
<b-select v-model="dateRangeOption">
<option v-for="option in dateRangeOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</b-select>
</div>
</div>
</div>
</div>
<MileageChart :vehicle="vehicle" :since="getStartDate()" :user="me" :height="300" />
<MileageChart :vehicle="vehicle" :since="getStartDate()" :user="me" :height="300" :mileage-option="mileageOption" />
</div>
</Layout>
</template>

24870
ui/yarn.lock

File diff suppressed because it is too large Load Diff