Compare commits
132 Commits
bug/remove
...
v0.0.14
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b962565ed6 | ||
|
|
20b0e246fd | ||
|
|
f1a7a053f4 | ||
|
|
359f35c53f | ||
|
|
ea3423d32a | ||
|
|
6542a3bb28 | ||
|
|
35d2f1ca0b | ||
|
|
565d5701be | ||
|
|
2eb78ab73c | ||
|
|
edf4647549 | ||
|
|
e87a348b90 | ||
|
|
c15b22c71a | ||
|
|
1c9f9c7803 | ||
|
|
89bdfdefd4 | ||
|
|
2974fd783f | ||
|
|
bd22b5a497 | ||
|
|
8f6408a92b | ||
|
|
20bc28fffa | ||
|
|
3c89e75a34 | ||
|
|
5594356166 | ||
|
|
cea08a59be | ||
|
|
f16ed1a39f | ||
|
|
66032fcf55 | ||
|
|
34a9d56726 | ||
|
|
a0880ad5b6 | ||
|
|
17e8e5914e | ||
|
|
a14f298822 | ||
|
|
afe4078897 | ||
|
|
01f9b455cf | ||
|
|
c9c06f865c | ||
|
|
e2c14afc99 | ||
|
|
415d0abc83 | ||
|
|
d32fd8073d | ||
|
|
d6eab70ca6 | ||
|
|
cc82536970 | ||
|
|
094cf0d7c9 | ||
|
|
24f295c632 | ||
|
|
e9812e7e27 | ||
|
|
785ff9a089 | ||
|
|
b99c3921d7 | ||
|
|
d64777dca6 | ||
|
|
5208437ec2 | ||
|
|
654087b990 | ||
|
|
d294db34fc | ||
|
|
9f9f90fd1d | ||
|
|
051e3476a7 | ||
|
|
845dcb242a | ||
|
|
df165dae6e | ||
|
|
e389a9ac2a | ||
|
|
e2e4169787 | ||
|
|
2a8325c6ce | ||
|
|
cd2e9ebc61 | ||
|
|
1ac3a8b31b | ||
|
|
f07922763b | ||
|
|
9da21b2192 | ||
|
|
a8c85bcd7d | ||
|
|
d597a4ed30 | ||
|
|
45456280b4 | ||
|
|
1d5794e344 | ||
|
|
cd558ba744 | ||
|
|
3a2c82c789 | ||
|
|
d196536d74 | ||
|
|
630a7f2ec6 | ||
|
|
f2bc01289a | ||
|
|
a8d2b37087 | ||
|
|
7436399d90 | ||
|
|
bc3e1f0982 | ||
|
|
d429fa34bd | ||
|
|
5095cb4c61 | ||
|
|
e0df7ee80e | ||
|
|
431de8c3eb | ||
|
|
41793784ea | ||
|
|
85b5ad28bf | ||
|
|
b386012e13 | ||
|
|
df2d7288df | ||
|
|
7a6f796561 | ||
|
|
aee52d0594 | ||
|
|
f8b1de8d15 | ||
|
|
a7896340e1 | ||
|
|
f2a3bb2e9f | ||
|
|
d343619f13 | ||
|
|
adce0efa8b | ||
|
|
fc6f4bc00d | ||
|
|
a16bcf850f | ||
|
|
4ace38f8f3 | ||
|
|
63e330ffb0 | ||
|
|
fc9796081e | ||
|
|
440913af9c | ||
|
|
66d01afe6e | ||
|
|
ad4a399dc8 | ||
|
|
2137bf7702 | ||
|
|
47bdf7b505 | ||
|
|
669bffa955 | ||
|
|
05c5381a06 | ||
|
|
e623e3ad1a | ||
|
|
c43a2f639a | ||
|
|
e66e5b7724 | ||
|
|
adfd70fe98 | ||
|
|
ebebcacdc9 | ||
|
|
3299c13181 | ||
|
|
2661f8ae36 | ||
|
|
091cfdcc99 | ||
|
|
9771dc4c25 | ||
|
|
8e894844a3 | ||
|
|
4a55879ad8 | ||
|
|
9dab3d124d | ||
|
|
a89ca5e46a | ||
|
|
f96638d913 | ||
|
|
08f2a3547e | ||
|
|
126aff7231 | ||
|
|
ba276975f3 | ||
|
|
7d4b763e48 | ||
|
|
ee964a630e | ||
|
|
c588e34b2e | ||
|
|
6871a40380 | ||
|
|
0035897f21 | ||
|
|
bb68c8c504 | ||
|
|
961ec30065 | ||
|
|
0b450dc462 | ||
|
|
c0db2c5c1e | ||
|
|
2ecb113918 | ||
|
|
966cac280f | ||
|
|
2749707546 | ||
|
|
f1bf36bcb9 | ||
|
|
3322e2f6bd | ||
|
|
9ef929dbd5 | ||
|
|
dc33aaad49 | ||
|
|
15cf09f326 | ||
|
|
1e099ec8b6 | ||
|
|
e8f7815d8d | ||
|
|
bfaebf17d0 | ||
|
|
d314ed4a16 |
31
.github/workflows/hub.yml
vendored
31
.github/workflows/hub.yml
vendored
@@ -12,47 +12,38 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
- name: Available platforms
|
||||
run: echo ${{ steps.qemu.outputs.platforms }}
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Set up build cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Parse the git tag
|
||||
id: get_tag
|
||||
run: echo ::set-output name=TAG::$(echo $GITHUB_REF | cut -d / -f 3)
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
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
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
#platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||
#platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
# 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 }}
|
||||
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Don't track .vscode directory
|
||||
.vscode
|
||||
!.vscode/launch.json
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
ARG GO_VERSION=1.16.2
|
||||
ARG GO_VERSION=1.20.6
|
||||
FROM golang:${GO_VERSION}-alpine AS builder
|
||||
RUN apk update && apk add alpine-sdk git && rm -rf /var/cache/apk/*
|
||||
RUN mkdir -p /api
|
||||
@@ -9,16 +9,17 @@ RUN go mod download
|
||||
COPY ./server .
|
||||
RUN go build -o ./app ./main.go
|
||||
|
||||
FROM node:14 as build-stage
|
||||
FROM node:16-alpine as build-stage
|
||||
WORKDIR /app
|
||||
COPY ./ui/package*.json ./
|
||||
RUN apk add --no-cache autoconf automake build-base nasm
|
||||
RUN npm install
|
||||
COPY ./ui .
|
||||
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
|
||||
|
||||
87
README.md
87
README.md
@@ -1,26 +1,15 @@
|
||||
[![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 +33,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 +72,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 +83,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 +115,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 +209,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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
version: "2.1"
|
||||
services:
|
||||
hammond:
|
||||
image: akhilrex/hammond
|
||||
image: alfhou/hammond
|
||||
container_name: hammond
|
||||
environment:
|
||||
- JWT_SECRET=somethingverystrong
|
||||
|
||||
@@ -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
3
server/.gitignore
vendored
@@ -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/*
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"hammond/db"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
@@ -25,6 +26,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"))
|
||||
|
||||
@@ -7,10 +7,11 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/akhilrex/hammond/common"
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"github.com/akhilrex/hammond/service"
|
||||
"hammond/common"
|
||||
"hammond/db"
|
||||
"hammond/models"
|
||||
"hammond/service"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -115,7 +116,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 +150,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 {
|
||||
|
||||
@@ -5,10 +5,11 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/akhilrex/hammond/common"
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"github.com/akhilrex/hammond/service"
|
||||
"hammond/common"
|
||||
"hammond/db"
|
||||
"hammond/models"
|
||||
"hammond/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
@@ -2,13 +2,18 @@ package controllers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"hammond/models"
|
||||
"hammond/service"
|
||||
|
||||
"github.com/akhilrex/hammond/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisteImportController(router *gin.RouterGroup) {
|
||||
router.POST("/import/fuelly", fuellyImport)
|
||||
router.POST("/import/drivvo", drivvoImport)
|
||||
router.POST("/import/generic", genericImport)
|
||||
}
|
||||
|
||||
func fuellyImport(c *gin.Context) {
|
||||
@@ -24,3 +29,46 @@ 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{})
|
||||
}
|
||||
|
||||
func genericImport(c *gin.Context) {
|
||||
var json models.ImportData
|
||||
if err := c.ShouldBindJSON(&json); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if json.VehicleId == "" {
|
||||
c.JSON(http.StatusUnprocessableEntity, "Missing Vehicle ID")
|
||||
return
|
||||
}
|
||||
errors := service.GenericImport(json, c.MustGet("userId").(string))
|
||||
if len(errors) > 0 {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"errors": errors})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{})
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ package controllers
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/akhilrex/hammond/common"
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"github.com/akhilrex/hammond/service"
|
||||
"hammond/common"
|
||||
"hammond/db"
|
||||
"hammond/models"
|
||||
"hammond/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"hammond/db"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/dgrijalva/jwt-go/request"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -3,9 +3,10 @@ package controllers
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/akhilrex/hammond/common"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"github.com/akhilrex/hammond/service"
|
||||
"hammond/common"
|
||||
"hammond/models"
|
||||
"hammond/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -25,7 +26,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
|
||||
|
||||
@@ -4,10 +4,11 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/akhilrex/hammond/common"
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"github.com/akhilrex/hammond/service"
|
||||
"hammond/common"
|
||||
"hammond/db"
|
||||
"hammond/models"
|
||||
"hammond/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@ package controllers
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/akhilrex/hammond/common"
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"github.com/akhilrex/hammond/service"
|
||||
"hammond/common"
|
||||
"hammond/db"
|
||||
"hammond/models"
|
||||
"hammond/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/akhilrex/hammond/common"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"github.com/akhilrex/hammond/service"
|
||||
"hammond/common"
|
||||
"hammond/models"
|
||||
"hammond/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module github.com/akhilrex/hammond
|
||||
module hammond
|
||||
|
||||
go 1.16
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/akhilrex/hammond/controllers"
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/hammond/service"
|
||||
"hammond/controllers"
|
||||
"hammond/db"
|
||||
"hammond/service"
|
||||
|
||||
"github.com/gin-contrib/location"
|
||||
"github.com/gin-gonic/contrib/static"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -3,7 +3,7 @@ package models
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"hammond/db"
|
||||
)
|
||||
|
||||
type CreateAlertModel struct {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package models
|
||||
|
||||
import "github.com/akhilrex/hammond/db"
|
||||
import "hammond/db"
|
||||
|
||||
type LoginResponse struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
22
server/models/import.go
Normal file
22
server/models/import.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package models
|
||||
|
||||
type ImportData struct {
|
||||
Data []ImportFillup `json:"data" binding:"required"`
|
||||
VehicleId string `json:"vehicleId" binding:"required"`
|
||||
TimeZone string `json:"timezone" binding:"required"`
|
||||
}
|
||||
|
||||
type ImportFillup struct {
|
||||
VehicleID string `json:"vehicleId"`
|
||||
FuelQuantity float32 `json:"fuelQuantity"`
|
||||
PerUnitPrice float32 `json:"perUnitPrice"`
|
||||
TotalAmount float32 `json:"totalAmount"`
|
||||
OdoReading int `json:"odoReading"`
|
||||
IsTankFull *bool `json:"isTankFull"`
|
||||
HasMissedFillup *bool `json:"hasMissedFillup"`
|
||||
Comments string `json:"comments"`
|
||||
FillingStation string `json:"fillingStation"`
|
||||
UserID string `json:"userId"`
|
||||
Date string `json:"date"`
|
||||
FuelSubType string `json:"fuelSubType"`
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package models
|
||||
|
||||
import "github.com/akhilrex/hammond/db"
|
||||
import "hammond/db"
|
||||
|
||||
type UpdateSettingModel struct {
|
||||
Currency string `json:"currency" form:"currency" query:"currency"`
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"hammond/db"
|
||||
)
|
||||
|
||||
type MileageModel struct {
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ package models
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"hammond/db"
|
||||
|
||||
_ "github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"hammond/db"
|
||||
"hammond/models"
|
||||
)
|
||||
|
||||
func CreateAlert(model models.CreateAlertModel, vehicleId, userId string) (*db.VehicleAlert, error) {
|
||||
|
||||
142
server/service/drivvoImportService.go
Normal file
142
server/service/drivvoImportService.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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
|
||||
}
|
||||
@@ -13,9 +13,10 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/hammond/internal/sanitize"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"hammond/db"
|
||||
"hammond/internal/sanitize"
|
||||
"hammond/models"
|
||||
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
|
||||
141
server/service/fuellyImportService.go
Normal file
141
server/service/fuellyImportService.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"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
|
||||
}
|
||||
47
server/service/genericImportService.go
Normal file
47
server/service/genericImportService.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"hammond/db"
|
||||
"hammond/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GenericParseRefuelings(content []models.ImportFillup, user *db.User, vehicle *db.Vehicle, timezone string) ([]db.Fillup, []string) {
|
||||
var errors []string
|
||||
var fillups []db.Fillup
|
||||
dateLayout := "2006-01-02T15:04:05.000Z"
|
||||
loc, _ := time.LoadLocation(timezone)
|
||||
for _, record := range content {
|
||||
date, err := time.ParseInLocation(dateLayout, record.Date, loc)
|
||||
if err != nil {
|
||||
date = time.Date(2000, time.December, 0, 0, 0, 0, 0, loc)
|
||||
}
|
||||
|
||||
var missedFillup bool
|
||||
if record.HasMissedFillup == nil {
|
||||
missedFillup = false
|
||||
} else {
|
||||
missedFillup = *record.HasMissedFillup
|
||||
}
|
||||
|
||||
fillups = append(fillups, db.Fillup{
|
||||
VehicleID: vehicle.ID,
|
||||
UserID: user.ID,
|
||||
Date: date,
|
||||
IsTankFull: record.IsTankFull,
|
||||
HasMissedFillup: &missedFillup,
|
||||
FuelQuantity: float32(record.FuelQuantity),
|
||||
PerUnitPrice: float32(record.PerUnitPrice),
|
||||
FillingStation: record.FillingStation,
|
||||
OdoReading: record.OdoReading,
|
||||
TotalAmount: float32(record.TotalAmount),
|
||||
FuelUnit: vehicle.FuelUnit,
|
||||
Currency: user.Currency,
|
||||
DistanceUnit: user.DistanceUnit,
|
||||
Comments: record.Comments,
|
||||
Source: "Generic Import",
|
||||
})
|
||||
}
|
||||
|
||||
return fillups, errors
|
||||
}
|
||||
@@ -2,144 +2,13 @@ package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/leekchan/accounting"
|
||||
"hammond/db"
|
||||
"hammond/models"
|
||||
)
|
||||
|
||||
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 +19,114 @@ 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)
|
||||
}
|
||||
|
||||
func GenericImport(content models.ImportData, userId string) []string {
|
||||
var errors []string
|
||||
user, err := GetUserById(userId)
|
||||
if err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
return errors
|
||||
}
|
||||
|
||||
vehicle, err := GetVehicleById(content.VehicleId)
|
||||
if err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
return errors
|
||||
}
|
||||
|
||||
var fillups []db.Fillup
|
||||
fillups, errors = GenericParseRefuelings(content.Data, user, vehicle, content.TimeZone)
|
||||
|
||||
if len(errors) != 0 {
|
||||
return errors
|
||||
}
|
||||
|
||||
return WriteToDB(fillups, nil)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"hammond/db"
|
||||
)
|
||||
|
||||
func CanInitializeSystem() (bool, error) {
|
||||
|
||||
@@ -4,11 +4,12 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"hammond/common"
|
||||
"hammond/db"
|
||||
"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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ package service
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"hammond/db"
|
||||
"hammond/models"
|
||||
)
|
||||
|
||||
func CreateUser(userModel *models.RegisterRequest, role db.Role) error {
|
||||
|
||||
@@ -3,8 +3,9 @@ package service
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"hammond/db"
|
||||
"hammond/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ module.exports = {
|
||||
'no-console': process.env.PRE_COMMIT
|
||||
? ['error', { allow: ['warn', 'error'] }]
|
||||
: 'off',
|
||||
'vue/multi-word-component-names': 0,
|
||||
'import/no-relative-parent-imports': 'error',
|
||||
'import/order': 'error',
|
||||
'vue/array-bracket-spacing': 'error',
|
||||
|
||||
5
ui/.gitignore
vendored
5
ui/.gitignore
vendored
@@ -29,3 +29,8 @@ yarn-error.log*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw*
|
||||
|
||||
#Vs code files
|
||||
.vscode
|
||||
!.vscode/launch.json
|
||||
|
||||
|
||||
30
ui/.vscode/_components.code-snippets
vendored
30
ui/.vscode/_components.code-snippets
vendored
@@ -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>"
|
||||
}
|
||||
}
|
||||
26
ui/.vscode/_sfc-blocks.code-snippets
vendored
26
ui/.vscode/_sfc-blocks.code-snippets
vendored
@@ -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>"
|
||||
}
|
||||
}
|
||||
37
ui/.vscode/extensions.json
vendored
37
ui/.vscode/extensions.json
vendored
@@ -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"
|
||||
]
|
||||
}
|
||||
93
ui/.vscode/settings.json
vendored
93
ui/.vscode/settings.json
vendored
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,9 @@ WORKDIR /app
|
||||
|
||||
# Copy dependency-related files
|
||||
COPY package.json ./
|
||||
COPY yarn.lock ./
|
||||
|
||||
# Install project dependencies
|
||||
RUN yarn install
|
||||
RUN npm install
|
||||
|
||||
# Expose ports 8080, which the dev server will be bound to
|
||||
EXPOSE 8080
|
||||
|
||||
30847
ui/package-lock.json
generated
30847
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
105
ui/package.json
105
ui/package.json
@@ -6,89 +6,82 @@
|
||||
"dev": "vue-cli-service serve",
|
||||
"dev:e2e": "cross-env VUE_APP_TEST=e2e vue-cli-service test:e2e --mode=development",
|
||||
"build": "vue-cli-service build --modern",
|
||||
"build:ci": "yarn build --report",
|
||||
"build:ci": "npm build --report",
|
||||
"lint:eslint": "eslint --fix",
|
||||
"lint:stylelint": "stylelint --fix",
|
||||
"lint:markdownlint": "markdownlint",
|
||||
"lint:prettier": "prettier --write --loglevel warn",
|
||||
"lint:all:eslint": "yarn lint:eslint --ext .js,.vue .",
|
||||
"lint:all:stylelint": "yarn lint:stylelint \"src/**/*.{vue,scss}\"",
|
||||
"lint:all:markdownlint": "yarn lint:markdownlint \"docs/*.md\" \"*.md\"",
|
||||
"lint:all:prettier": "yarn lint:prettier \"**/*.{js,json,css,scss,vue,html,md}\"",
|
||||
"lint": "run-s lint:all:*",
|
||||
"test:unit": "cross-env VUE_APP_TEST=unit vue-cli-service test:unit",
|
||||
"test:unit:file": "yarn test:unit --bail --findRelatedTests",
|
||||
"test:unit:watch": "yarn test:unit --watch --notify --notifyMode change",
|
||||
"test:unit:ci": "yarn test:unit --coverage --ci",
|
||||
"test:e2e": "cross-env VUE_APP_TEST=e2e vue-cli-service test:e2e --headless",
|
||||
"test": "run-s test:unit test:e2e",
|
||||
"test:ci": "run-s test:unit:ci test:e2e",
|
||||
"new": "cross-env HYGEN_TMPLS=generators hygen new",
|
||||
"docs": "vuepress dev",
|
||||
"docker": "docker-compose exec dev yarn"
|
||||
"docs": "vuepress dev"
|
||||
},
|
||||
"gitHooks": {
|
||||
"pre-commit": "cross-env PRE_COMMIT=true lint-staged"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.27",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.12.1",
|
||||
"@fortawesome/vue-fontawesome": "0.1.9",
|
||||
"axios": "^0.27.2",
|
||||
"buefy": "^0.9.7",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.3.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.3.0",
|
||||
"@fortawesome/vue-fontawesome": "^2.0.10",
|
||||
"axios": "^1.3.2",
|
||||
"buefy": "^0.9.22",
|
||||
"chart.js": "^2.9.4",
|
||||
"core-js": "3.6.4",
|
||||
"currency-formatter": "^1.5.7",
|
||||
"date-fns": "2.10.0",
|
||||
"core-js": "^3.27.2",
|
||||
"currency-formatter": "^1.5.9",
|
||||
"date-fns": "^2.29.3",
|
||||
"lodash": "^4.17.21",
|
||||
"normalize.css": "8.0.1",
|
||||
"nprogress": "0.2.0",
|
||||
"vue": "2.6.11",
|
||||
"node-gyp": "^9.3.1",
|
||||
"normalize.css": "^8.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"vue": "^2.6.11",
|
||||
"vue-chartjs": "^3.5.1",
|
||||
"vue-meta": "2.3.3",
|
||||
"vue-router": "3.1.6",
|
||||
"vuex": "3.1.2"
|
||||
"vue-i18n": "^8.28.2",
|
||||
"vue-meta": "^2.4.0",
|
||||
"vue-router": "^3.6.5",
|
||||
"vuex": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "4.2.x",
|
||||
"@vue/cli-plugin-eslint": "4.2.x",
|
||||
"@vue/cli-plugin-unit-jest": "4.2.x",
|
||||
"@vue/cli-service": "4.2.x",
|
||||
"@vue/cli-plugin-babel": "^4.5.19",
|
||||
"@vue/cli-plugin-eslint": "^4.5.19",
|
||||
"@vue/cli-plugin-unit-jest": "^4.5.19",
|
||||
"@vue/cli-service": "^4.5.19",
|
||||
"@vue/eslint-config-prettier": "6.0.x",
|
||||
"@vue/eslint-config-standard": "5.1.x",
|
||||
"@vue/test-utils": "1.0.0-beta.31",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"@vue/eslint-config-standard": "^5.1.1",
|
||||
"@vue/test-utils": "^1.3.4",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-eslint": "10.1.x",
|
||||
"cross-env": "7.0.x",
|
||||
"eslint": "6.8.x",
|
||||
"eslint-plugin-import": "2.20.x",
|
||||
"eslint-plugin-node": "11.0.x",
|
||||
"eslint-plugin-promise": "4.2.x",
|
||||
"eslint-plugin-standard": "4.0.x",
|
||||
"eslint-plugin-vue": "6.2.x",
|
||||
"express": "4.17.x",
|
||||
"hygen": "4.0.x",
|
||||
"imagemin-lint-staged": "0.4.x",
|
||||
"lint-staged": "10.0.x",
|
||||
"markdownlint-cli": "^0.31.1",
|
||||
"npm-run-all": "4.1.x",
|
||||
"sass": "1.26.x",
|
||||
"sass-loader": "8.0.x",
|
||||
"stylelint": "13.2.x",
|
||||
"stylelint-config-css-modules": "2.2.x",
|
||||
"stylelint-config-prettier": "8.0.x",
|
||||
"stylelint-config-recess-order": "2.0.x",
|
||||
"stylelint-config-standard": "20.0.x",
|
||||
"stylelint-scss": "3.14.x",
|
||||
"vue-template-compiler": "2.6.11",
|
||||
"vuepress": "1.3.x"
|
||||
"cross-env": "^7.0.1",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.3.1",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"eslint-plugin-vue": "^9.9.0",
|
||||
"express": "^4.18.2",
|
||||
"hygen": "^6.2.11",
|
||||
"imagemin-lint-staged": "^0.5.1",
|
||||
"lint-staged": "^13.1.1",
|
||||
"markdownlint-cli": "^0.33.0",
|
||||
"npm-run-all": "^4.1.1",
|
||||
"sass": "^1.58.0",
|
||||
"sass-loader": "^8.0.2",
|
||||
"stylelint": "^14.16.1",
|
||||
"stylelint-config-css-modules": "^4.1.0",
|
||||
"stylelint-config-prettier": "^9.0.4",
|
||||
"stylelint-config-standard": "^29.0.0",
|
||||
"stylelint-scss": "^4.3.0",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vuepress": "^1.9.8"
|
||||
},
|
||||
"resolutions": {
|
||||
"@vue/cli-plugin-unit-jest/jest": "25.1.x",
|
||||
"@vue/cli-plugin-unit-jest/babel-jest": "25.1.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.3",
|
||||
"yarn": ">=1.0.0"
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
<link rel="shortcut icon" href="<%= webpackConfig.output.publicPath %>hammond.png" />
|
||||
<link rel="apple-touch-icon" href="<%= webpackConfig.output.publicPath %>touch-icon.png" />
|
||||
<title><%= webpackConfig.name %></title>
|
||||
<!-- Temporary until fontawesome 6 is supported in buefy (see issue: https://github.com/FortAwesome/Font-Awesome/issues/18663) -->
|
||||
<style>
|
||||
.icon svg { width: 1em; height: 1em; max-width: 80%; max-height: 80%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- This is where our app will be mounted. -->
|
||||
|
||||
@@ -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">
|
||||
@@ -95,14 +95,13 @@ export default {
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-button
|
||||
tag="input"
|
||||
tag="button"
|
||||
native-type="submit"
|
||||
:disabled="tryingToCreate"
|
||||
type="is-primary"
|
||||
value="Upload File"
|
||||
class="control"
|
||||
>
|
||||
Upload File
|
||||
{{ $t('uploadfile') }}
|
||||
</b-button>
|
||||
</div></div
|
||||
>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -50,12 +50,12 @@ export default {
|
||||
<b-select
|
||||
v-if="unprocessedQuickEntries.length"
|
||||
v-model="quickEntry"
|
||||
placeholder="Refer quick entry"
|
||||
:placeholder="$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">
|
||||
|
||||
@@ -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,9 +90,9 @@ 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 v-for="model in models" :key="model.id" class="columns is-mobile">
|
||||
<div class="column is-one-third">
|
||||
<b-field>
|
||||
<b-switch v-model="model.isShared" :disabled="model.isOwner" @input="changeShareStatus(model)">
|
||||
@@ -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
|
||||
>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -10,10 +10,10 @@ $size-input-padding-vertical: 0.75em;
|
||||
$size-input-padding-horizontal: 1em;
|
||||
$size-input-padding: $size-input-padding-vertical $size-input-padding-horizontal;
|
||||
$size-input-border: 1px;
|
||||
$size-input-border-radius: (1em + $size-input-padding-vertical * 2) / 10;
|
||||
$size-input-border-radius: calc((1em + $size-input-padding-vertical * 2) / 10);
|
||||
|
||||
// BUTTONS
|
||||
$size-button-padding-vertical: $size-grid-padding / 2;
|
||||
$size-button-padding-horizontal: $size-grid-padding / 1.5;
|
||||
$size-button-padding-vertical: calc($size-grid-padding / 2);
|
||||
$size-button-padding-horizontal: calc($size-grid-padding / 1.5);
|
||||
$size-button-padding: $size-button-padding-vertical
|
||||
$size-button-padding-horizontal;
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
$max-screen,
|
||||
$max-value
|
||||
) {
|
||||
$a: ($max-value - $min-value) / ($max-screen - $min-screen);
|
||||
$a: calc(($max-value - $min-value) / ($max-screen - $min-screen));
|
||||
$b: $min-value - $a * $min-screen;
|
||||
|
||||
$sign: '+';
|
||||
|
||||
25
ui/src/i18n.js
Normal file
25
ui/src/i18n.js
Normal 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
217
ui/src/locales/de.json
Normal 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 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"
|
||||
}
|
||||
231
ui/src/locales/en.json
Normal file
231
ui/src/locales/en.json
Normal file
@@ -0,0 +1,231 @@
|
||||
{
|
||||
"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",
|
||||
"partialfillup": "Partial Fillup",
|
||||
"getafulltank": "Did you get a full tank?",
|
||||
"tankpartialfull": "Which do you track?",
|
||||
"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.",
|
||||
"importgeneric": "Generic Fillups Import",
|
||||
"importgenericdesc": "Fillups CSV 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.",
|
||||
"choosedatafile": "Choose the CSV file and then 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 file 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",
|
||||
"importgenerichintdata": "Data must be in CSV format.",
|
||||
"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?",
|
||||
"missedfillup": "Missed Fillup",
|
||||
"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"
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
faCheck,
|
||||
faTimes,
|
||||
faArrowUp,
|
||||
faArrowRotateLeft,
|
||||
faAngleLeft,
|
||||
faAngleRight,
|
||||
faCalendar,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
faTimesCircle,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import i18n from './i18n';
|
||||
|
||||
import App from './app.vue'
|
||||
|
||||
@@ -33,11 +35,11 @@ import '@components/_globals'
|
||||
import 'buefy/dist/buefy.css'
|
||||
import 'nprogress/nprogress.css'
|
||||
|
||||
Vue.component('vue-fontawesome', FontAwesomeIcon)
|
||||
library.add(
|
||||
faCheck,
|
||||
faTimes,
|
||||
faArrowUp,
|
||||
faArrowRotateLeft,
|
||||
faAngleLeft,
|
||||
faAngleRight,
|
||||
faCalendar,
|
||||
@@ -53,7 +55,9 @@ library.add(
|
||||
faShare,
|
||||
faUserFriends,
|
||||
faTimesCircle
|
||||
)
|
||||
);
|
||||
Vue.component('VueFontawesome', FontAwesomeIcon)
|
||||
|
||||
Vue.use(Buefy, {
|
||||
defaultIconComponent: 'vue-fontawesome',
|
||||
defaultIconPack: 'fas',
|
||||
@@ -73,6 +77,7 @@ const app = new Vue({
|
||||
store,
|
||||
|
||||
render: (h) => h(App),
|
||||
i18n,
|
||||
}).$mount('#app')
|
||||
|
||||
// If running e2e tests...
|
||||
|
||||
@@ -410,6 +410,24 @@ 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: '/import/generic',
|
||||
name: 'import-generic',
|
||||
component: () => lazyLoadView(import('@views/import-generic.vue')),
|
||||
meta: {
|
||||
authRequired: true,
|
||||
},
|
||||
props: (route) => ({ user: store.state.auth.currentUser || {} }),
|
||||
},
|
||||
{
|
||||
path: '/logout',
|
||||
name: 'logout',
|
||||
|
||||
@@ -23,7 +23,7 @@ export default {
|
||||
<template v-if="resource">
|
||||
{{ resource }}
|
||||
</template>
|
||||
Not Found
|
||||
{{ $t('notfound') }}
|
||||
</h1>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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="$t('selectvehicle')">
|
||||
<b-select v-model="selectedVehicle" :placeholder="$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="$t('expenseby')">
|
||||
<b-select v-model="expenseModel.userId" :placeholder="$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="$t('expensedate')">
|
||||
<b-datepicker
|
||||
v-model="expenseModel.date"
|
||||
:date-formatter="formatDate"
|
||||
placeholder="Click to select..."
|
||||
:placeholder="$t('clicktoselect')"
|
||||
icon="calendar"
|
||||
:max-date="new Date()"
|
||||
>
|
||||
</b-datepicker>
|
||||
</b-field>
|
||||
<b-field label="Expense Type*">
|
||||
<b-field :label="$t('expensetype') + `*`">
|
||||
<b-input v-model="expenseModel.expenseType" expanded required></b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Total Amount Paid">
|
||||
<b-field :label="$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="$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="$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="button" native-type="submit" :value="$t('save')" :disabled="tryingToCreate" type="is-primary" label="Create Expense" expanded/>
|
||||
</b-field>
|
||||
</form>
|
||||
</Layout>
|
||||
|
||||
@@ -76,6 +76,9 @@ export default {
|
||||
this.fetchVehicleFuelSubTypes()
|
||||
if (!this.fillup.id) {
|
||||
this.fillupModel = this.getEmptyFillup()
|
||||
if (this.vehicle.fillups.length > 0) {
|
||||
this.fillupModel.odoReading = this.vehicle.fillups[0].odoReading
|
||||
}
|
||||
this.fillupModel.userId = this.me.id
|
||||
}
|
||||
},
|
||||
@@ -126,7 +129,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 +156,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 +184,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="$t('selectvehicle')">
|
||||
<b-select v-model="selectedVehicle" :placeholder="$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="$t('expenseby')">
|
||||
<b-select v-model="fillupModel.userId" :placeholder="$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="$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="$t('fuelsubtype')">
|
||||
<b-autocomplete
|
||||
v-model="fillupModel.fuelSubType"
|
||||
:data="filteredFuelSubtypes"
|
||||
@@ -231,55 +232,63 @@ export default {
|
||||
>
|
||||
</b-autocomplete>
|
||||
</b-field>
|
||||
<b-field label="Quantity*" addons>
|
||||
<b-field :label="$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="$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="$t('per', { '0': $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="$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="$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="$t('fillingstation')">
|
||||
<b-input v-model="fillupModel.fillingStation" type="text" expanded></b-input>
|
||||
</b-field>
|
||||
<b-field label="Comments">
|
||||
<b-field :label="$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="button"
|
||||
native-type="submit"
|
||||
:disabled="tryingToCreate"
|
||||
type="is-primary"
|
||||
:value="$t('save')"
|
||||
:label="$t('createfillup')"
|
||||
expanded
|
||||
/>
|
||||
<p v-if="authError">
|
||||
There was an error logging in to your account.
|
||||
</p>
|
||||
|
||||
@@ -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="$t('nickname') + `*`">
|
||||
<b-input v-model="vehicleModel.nickname" type="text" expanded required></b-input>
|
||||
</b-field>
|
||||
<b-field label="Registration*">
|
||||
<b-field :label="$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="$t('fueltype') + `*`">
|
||||
<b-select v-model.number="vehicleModel.fuelType" :placeholder="$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="$t('fuelunit') + `*`">
|
||||
<b-select v-model.number="vehicleModel.fuelUnit" :placeholder="$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="$t('make') + `*`">
|
||||
<b-input v-model="vehicleModel.make" type="text" required expanded></b-input>
|
||||
</b-field>
|
||||
<b-field label="Model*">
|
||||
<b-field :label="$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="$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="$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="button"
|
||||
native-type="submit"
|
||||
:disabled="tryingToCreate"
|
||||
type="is-primary"
|
||||
:value="$t('save')"
|
||||
:label="$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>
|
||||
|
||||
@@ -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 }} {{
|
||||
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>
|
||||
|
||||
172
ui/src/router/views/import-drivvo.vue
Normal file
172
ui/src/router/views/import-drivvo.vue
Normal 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="button" native-type="submit" :disabled="tryingToCreate" type="is-primary" 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>
|
||||
@@ -9,24 +9,6 @@ export default {
|
||||
meta: [{ name: 'description', content: 'The Import Fuelly page.' }],
|
||||
},
|
||||
components: { Layout },
|
||||
computed: {
|
||||
...mapState('utils', ['isMobile']),
|
||||
uploadButtonLabel() {
|
||||
if (this.isMobile) {
|
||||
if (this.file == null) {
|
||||
return 'Choose Photo'
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
} else {
|
||||
if (this.file == null) {
|
||||
return 'Choose CSV'
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
@@ -40,6 +22,24 @@ export default {
|
||||
errors: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('utils', ['isMobile']),
|
||||
uploadButtonLabel() {
|
||||
if (this.isMobile) {
|
||||
if (this.file == null) {
|
||||
return this.$t('choosephoto')
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
} else {
|
||||
if (this.file == null) {
|
||||
return this.$t('choosecsv')
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
importFuelly() {
|
||||
if (this.file == null) {
|
||||
@@ -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="button" native-type="submit" :disabled="tryingToCreate" type="is-primary" class="control">
|
||||
{{ $t('import') }}
|
||||
</b-button>
|
||||
</div></div
|
||||
>
|
||||
|
||||
7
ui/src/router/views/import-generic.unit.js
Normal file
7
ui/src/router/views/import-generic.unit.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import ImportGeneric from './import-generic'
|
||||
|
||||
describe('@views/import-generic', () => {
|
||||
it('is a valid view', () => {
|
||||
expect(ImportGeneric).toBeAViewComponent()
|
||||
})
|
||||
})
|
||||
411
ui/src/router/views/import-generic.vue
Normal file
411
ui/src/router/views/import-generic.vue
Normal file
@@ -0,0 +1,411 @@
|
||||
<script>
|
||||
import Layout from '@layouts/main.vue'
|
||||
import { mapState } from 'vuex'
|
||||
import axios from 'axios'
|
||||
import Papa from 'papaparse'
|
||||
|
||||
export default {
|
||||
page: {
|
||||
title: 'Generic Import',
|
||||
meta: [{ name: 'description', content: 'The Generic Import page.' }],
|
||||
},
|
||||
components: { Layout },
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
file: null,
|
||||
tryingToCreate: false,
|
||||
errors: [],
|
||||
papaConfig: { dynamicTyping: true, skipEmptyLines: true, complete: this.assignResults },
|
||||
fileData: null,
|
||||
fileHeadings: null,
|
||||
myVehicles: [],
|
||||
selectedVehicle: null,
|
||||
invertFullTank: null,
|
||||
filledValueString: '',
|
||||
notFilledValueString: '',
|
||||
isFullTankString: false,
|
||||
fileHeadingMap: {
|
||||
fuelQuantity: null,
|
||||
perUnitPrice: null,
|
||||
totalAmount: null,
|
||||
odoReading: null,
|
||||
isTankFull: null,
|
||||
hasMissedFillup: null,
|
||||
comments: [], // [int]
|
||||
fillingStation: null,
|
||||
date: null,
|
||||
fuelSubType: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('utils', ['isMobile']),
|
||||
...mapState('vehicles', ['vehicles']),
|
||||
uploadButtonLabel() {
|
||||
if (this.isMobile) {
|
||||
if (this.file == null) {
|
||||
return this.$t('choosephoto')
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
} else {
|
||||
if (this.file == null) {
|
||||
return this.$t('choosefile')
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.myVehicles = this.vehicles
|
||||
},
|
||||
methods: {
|
||||
assignResults(results, file) {
|
||||
this.fileData = results.data
|
||||
this.fileHeadings = results.data[0]
|
||||
},
|
||||
parseCSV() {
|
||||
if (this.file == null) {
|
||||
return
|
||||
}
|
||||
this.errorMessage = ''
|
||||
Papa.parse(this.file, this.papaConfig)
|
||||
},
|
||||
getUsedHeadings() {
|
||||
return Object.keys(this.fileHeadingMap).filter((k) => this.fileHeadingMap[k] != null) // filter non-null properties
|
||||
},
|
||||
getTimezone() {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
},
|
||||
csvToJson() {
|
||||
const data = []
|
||||
const headings = this.getUsedHeadings().reduce((a, k) => ({ ...a, [k]: this.fileHeadingMap[k] }), {}) // create new object from filter
|
||||
const comments = (row) => {
|
||||
return this.fileHeadingMap.comments.reduce((a, fi) => {
|
||||
// TODO: sanitize to prevent XSS
|
||||
return `${a}${this.fileHeadings[fi]}: ${row[fi]}\n`
|
||||
}, '')
|
||||
}
|
||||
const calculateTotal = (row) => {
|
||||
return this.fileHeadingMap.totalAmount === -1
|
||||
? row[this.fileHeadingMap.fuelQuantity] * row[this.fileHeadingMap.perUnitPrice]
|
||||
: row[this.fileHeadingMap.totalAmount]
|
||||
}
|
||||
|
||||
const setFullTank = (row) => {
|
||||
if (row[this.fileHeadingMap.isTankFull].toLowerCase() === this.filledValueString.toLowerCase()) {
|
||||
return true
|
||||
} else if (row[this.fileHeadingMap.isTankFull].toLowerCase() === this.notFilledValueString.toLowerCase()) {
|
||||
return false
|
||||
} else {
|
||||
// TODO: need to handle errors better
|
||||
throw Error
|
||||
}
|
||||
}
|
||||
|
||||
for (let r = 1; r < this.fileData.length; r++) {
|
||||
const row = this.fileData[r]
|
||||
const item = {}
|
||||
Object.keys(headings).forEach((k) => {
|
||||
if (k === 'comments') {
|
||||
item[k] = comments(row)
|
||||
} else if (k === 'totalAmount') {
|
||||
item[k] = calculateTotal(row)
|
||||
} else if (k === 'isTankFull') {
|
||||
if (this.isFullTankString) {
|
||||
item[k] = setFullTank(row)
|
||||
} else {
|
||||
if (this.invertFullTank) {
|
||||
item[k] = Boolean(!row[headings[k]])
|
||||
} else {
|
||||
item[k] = Boolean(row[headings[k]])
|
||||
}
|
||||
}
|
||||
} else if (k === 'hasMissedFillup') {
|
||||
// TODO: need to account for this field being a string
|
||||
item[k] = Boolean(row[headings[k]])
|
||||
} else if (k === 'date') {
|
||||
item[k] = new Date(row[headings[k]]).toISOString()
|
||||
} else {
|
||||
item[k] = row[headings[k]]
|
||||
}
|
||||
})
|
||||
data.push(item)
|
||||
}
|
||||
return data
|
||||
},
|
||||
importData() {
|
||||
if (this.errors.length === 0) {
|
||||
try {
|
||||
const content = {
|
||||
data: this.csvToJson(),
|
||||
vehicleId: this.selectedVehicle.id,
|
||||
timezone: this.getTimezone(),
|
||||
}
|
||||
axios
|
||||
.post('/api/import/generic', content)
|
||||
.then((data) => {
|
||||
this.$buefy.toast.open({
|
||||
message: this.$t('importsuccessfull'),
|
||||
type: 'is-success',
|
||||
duration: 3000,
|
||||
})
|
||||
setTimeout(() => this.$router.push({ name: 'home' }), 1000)
|
||||
})
|
||||
.catch((ex) => {
|
||||
this.$buefy.toast.open({
|
||||
duration: 5000,
|
||||
message: this.$t('importerror'),
|
||||
position: 'is-bottom',
|
||||
type: 'is-danger',
|
||||
})
|
||||
console.log(ex)
|
||||
if (ex.response && ex.response.data.error) {
|
||||
this.errors.push(ex.response.data.error)
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
// TODO: handle error
|
||||
this.errors.push(e)
|
||||
}
|
||||
} else {
|
||||
this.errors.push('fix errors')
|
||||
}
|
||||
},
|
||||
checkFieldString() {
|
||||
const tankFull = this.fileData[1][this.fileHeadingMap.isTankFull]
|
||||
if (typeof tankFull !== 'boolean' && typeof tankFull === 'string') {
|
||||
this.isFullTankString = true
|
||||
}
|
||||
},
|
||||
clearHeadingProperty(property) {
|
||||
if (property === 'comments') {
|
||||
this.fileHeadingMap[property] = []
|
||||
} else {
|
||||
this.fileHeadingMap[property] = null
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Layout>
|
||||
<div class="columns box">
|
||||
<div class="column">
|
||||
<h1 class="title">{{ $t('importgeneric') }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div v-if="fileData === null" class="columns">
|
||||
<div class="column">
|
||||
<p class="subtitle"> {{ $t('stepstoimport', { name: 'CSV' }) }}</p>
|
||||
<ol>
|
||||
<!-- <li>{{ $t('importhintcreatecsv', { 'name': 'Fuelly' }) }} <a href="http://docs.fuelly.com/acar-import-export-center" target="_nofollow">{{ $t('here') }}</a>.</li> -->
|
||||
<li>{{ $t('importgenerichintdata') }}</li>
|
||||
<li>{{ $t('importhintvehiclecreated') }}</li>
|
||||
<li v-html="$t('importhintcurrdist')"></li>
|
||||
<li v-html="$t('importhintunits')"></li>
|
||||
<li>
|
||||
<b>{{ $t('dontimportagain') }}</b>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="fileData === null" class="section box">
|
||||
<div class="columns">
|
||||
<div class="column is-two-thirds">
|
||||
<p class="subtitle">{{ $t('choosedatafile') }}</p>
|
||||
</div>
|
||||
<div class="column is-one-third is-flex is-align-content-center">
|
||||
<form @submit.prevent="parseCSV">
|
||||
<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" 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="button" native-type="submit" type="is-primary" class="control">
|
||||
{{ $t('import') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="columns">
|
||||
<div class="column">
|
||||
<p class="subtitle">Map Fields</p>
|
||||
<form class="" @submit.prevent="importData">
|
||||
<b-field :label="$t('selectvehicle')">
|
||||
<b-select v-model="selectedVehicle" :placeholder="$t('vehicle')" required expanded>
|
||||
<option v-for="option in myVehicles" :key="option.id" :value="option">
|
||||
{{ option.nickname }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<span v-if="selectedVehicle !== null">
|
||||
<b-field :label="$t('fillupdate')">
|
||||
<b-select v-model="fileHeadingMap.date" required expanded>
|
||||
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
||||
{{ option }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<template v-slot:label>
|
||||
{{ $t('fuelsubtype') }}
|
||||
<b-tooltip type="is-dark" label="Clear selection">
|
||||
<b-button
|
||||
type="is-ghost"
|
||||
size="is-small"
|
||||
icon-pack="fas"
|
||||
icon-right="arrow-rotate-left"
|
||||
@click="clearHeadingProperty('fuelSubType')"
|
||||
></b-button>
|
||||
</b-tooltip>
|
||||
</template>
|
||||
<b-select v-model="fileHeadingMap.fuelSubType" expanded>
|
||||
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
||||
{{ option }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="$t('quantity')">
|
||||
<b-select v-model="fileHeadingMap.fuelQuantity" expanded required>
|
||||
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
||||
{{ option }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="$t('per', { '0': $t('price'), '1': $t('unit.short.' + selectedVehicle.fuelUnitDetail.key) })">
|
||||
<b-select v-model.number="fileHeadingMap.perUnitPrice" type="number" min="0" step=".001" expanded required>
|
||||
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
||||
{{ option }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="$t('totalamountpaid')">
|
||||
<b-select v-model.number="fileHeadingMap.totalAmount" expanded required>
|
||||
<option value="-1">Calculated</option>
|
||||
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
||||
{{ option }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="$t('odometer')">
|
||||
<b-select v-model.number="fileHeadingMap.odoReading" expanded required>
|
||||
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
||||
{{ option }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="$t('tankpartialfull')">
|
||||
<b-radio-button v-model="invertFullTank" native-value="false">{{ $t('fulltank') }}</b-radio-button>
|
||||
<b-radio-button v-model="invertFullTank" native-value="true">{{ $t('partialfillup') }}</b-radio-button>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-select v-model="fileHeadingMap.isTankFull" required @input="checkFieldString">
|
||||
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
||||
{{ option }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<span v-if="isFullTankString === true" required>
|
||||
<b-field label="Value when tank is filled">
|
||||
<b-input v-model="filledValueString"></b-input>
|
||||
</b-field>
|
||||
<b-field label="Value when tank was not completely filled">
|
||||
<b-input v-model="notFilledValueString"></b-input>
|
||||
</b-field>
|
||||
</span>
|
||||
<b-field>
|
||||
<template v-slot:label>
|
||||
{{ $t('missedfillup') }}
|
||||
<b-tooltip type="is-dark" label="Clear selection">
|
||||
<b-button
|
||||
type="is-ghost"
|
||||
size="is-small"
|
||||
icon-pack="fas"
|
||||
icon-right="arrow-rotate-left"
|
||||
@click="clearHeadingProperty('hasMissedFillup')"
|
||||
></b-button>
|
||||
</b-tooltip>
|
||||
</template>
|
||||
<b-select v-model="fileHeadingMap.hasMissedFillup">
|
||||
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
||||
{{ option }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<template v-slot:label>
|
||||
{{ $t('fillingstation') }}
|
||||
<b-tooltip type="is-dark" label="Clear selection">
|
||||
<b-button
|
||||
type="is-ghost"
|
||||
size="is-small"
|
||||
icon-pack="fas"
|
||||
icon-right="arrow-rotate-left"
|
||||
@click="clearHeadingProperty('fillingStation')"
|
||||
></b-button>
|
||||
</b-tooltip>
|
||||
</template>
|
||||
<b-select v-model="fileHeadingMap.fillingStation">
|
||||
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
||||
{{ option }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<template v-slot:label>
|
||||
{{ $t('comments') }}
|
||||
<b-tooltip type="is-dark" label="Clear selection">
|
||||
<b-button
|
||||
type="is-ghost"
|
||||
size="is-small"
|
||||
icon-pack="fas"
|
||||
icon-right="arrow-rotate-left"
|
||||
@click="clearHeadingProperty('comments')"
|
||||
></b-button>
|
||||
</b-tooltip>
|
||||
</template>
|
||||
<b-select v-model="fileHeadingMap.comments" type="textarea" multiple expanded>
|
||||
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
||||
{{ option }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<br />
|
||||
<b-field>
|
||||
<b-button tag="button" native-type="submit" type="is-primary" :value="$t('save')" :label="$t('import')" expanded />
|
||||
<p v-if="authError"> There was an error logging in to your account. </p>
|
||||
</b-field>
|
||||
</span>
|
||||
</form>
|
||||
</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>
|
||||
@@ -18,19 +18,41 @@ export default {
|
||||
|
||||
<template>
|
||||
<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>
|
||||
</div></div
|
||||
>
|
||||
<div class="columns box">
|
||||
<div class="column">
|
||||
<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 class="column is-one-third" to="/import-generic">
|
||||
<div class="box">
|
||||
<h1 class="title">{{ $t('importgeneric') }}</h1>
|
||||
<p>{{ $t('importgenericdesc') }}</p>
|
||||
<br />
|
||||
<b-button type="is-primary" tag="router-link" to="/import/generic">{{ $t('import') }}</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
@@ -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="$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="$t('init.fresh.yourname')">
|
||||
<b-input v-model="registerModel.name" required></b-input>
|
||||
</b-field>
|
||||
<b-field label="Your Email">
|
||||
<b-field :label="$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="$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="$t('currency')">
|
||||
<b-autocomplete
|
||||
v-model="registerModel.currency"
|
||||
:custom-formatter="formatCurrency"
|
||||
placeholder="Currency"
|
||||
:placeholder="$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="$t('distanceunit')">
|
||||
<b-select v-model.number="registerModel.distanceUnit" :placeholder="$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="button" :value="$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>
|
||||
|
||||
@@ -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="button" native-type="submit" :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>
|
||||
|
||||
@@ -28,7 +28,7 @@ export default {
|
||||
<h1>
|
||||
<BaseIcon name="user" />
|
||||
{{ user.name }}
|
||||
Profile
|
||||
{{ $t('profile') }}
|
||||
</h1>
|
||||
<pre>{{ user }}</pre>
|
||||
</Layout>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,27 @@ 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="button" native-type="submit" :disabled="tryingToSave" type="is-primary" expanded> {{ $t('save') }} </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="button" native-type="submit" :disabled="!passwordValid" type="is-primary" expanded>
|
||||
{{ $t('changepassword') }}
|
||||
</b-button>
|
||||
</b-field>
|
||||
</form>
|
||||
</div>
|
||||
@@ -193,48 +195,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>
|
||||
|
||||
@@ -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="$t('currency')">
|
||||
<b-select v-model="settingsModel.currency" :placeholder="$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="$t('distanceunit')">
|
||||
<b-select v-model.number="settingsModel.distanceUnit" :placeholder="$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="button" native-type="submit" :disabled="tryingToSave" type="is-primary" expanded> {{ $t('save') }}</b-button>
|
||||
</b-field>
|
||||
</form>
|
||||
</Layout>
|
||||
|
||||
@@ -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="$t('name')">
|
||||
<b-input v-model="registerModel.name" required></b-input>
|
||||
</b-field>
|
||||
<b-field label="Email">
|
||||
<b-field :label="$t('email')">
|
||||
<b-input v-model="registerModel.email" type="email" required></b-input>
|
||||
</b-field>
|
||||
<b-field label="Password">
|
||||
<b-field :label="$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="$t('role')">
|
||||
<b-select v-model.number="registerModel.role" :placeholder="$t('role')" required expanded>
|
||||
<option v-for="(option, key) in roleMasters" :key="key" :value="key">
|
||||
{{ `${option.long}` }}
|
||||
{{ `${option.key}` }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field label="Currency">
|
||||
<b-select v-model="registerModel.currency" placeholder="Currency" required expanded>
|
||||
<b-field :label="$t('currency')">
|
||||
<b-select v-model="registerModel.currency" :placeholder="$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="$t('distanceunit')">
|
||||
<b-select
|
||||
v-model.number="registerModel.distanceUnit"
|
||||
placeholder="Distance Unit"
|
||||
:placeholder="$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="button">{{ $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="$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="$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="$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="$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 v-if="props.row.isDisabled && props.row.roleDetail.key === 'USER'" type="is-success" @click="changeDisabledStatus(props.row.id, false)">{{ $t('enable') }}</b-button>
|
||||
<b-button v-if="!props.row.isDisabled && props.row.roleDetail.key === 'USER'" type="is-danger" @click="changeDisabledStatus(props.row.id, true)">{{ $t('disable') }}</b-button>
|
||||
</b-table-column>
|
||||
</b-table>
|
||||
</div>
|
||||
|
||||
@@ -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,35 @@ 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 +249,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 +309,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, $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 $t('you')
|
||||
} else {
|
||||
return x.name
|
||||
}
|
||||
@@ -321,25 +330,24 @@ 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`">{{ $t('addfillup') }}</b-button>
|
||||
<b-button type="is-primary" tag="router-link" :to="`/vehicles/${vehicle.id}/expense`">{{ $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 },
|
||||
params: { id: vehicle.id },
|
||||
}"
|
||||
>
|
||||
<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-icon pack="fas" icon="edit" type="is-info"> </b-icon>
|
||||
</b-button>
|
||||
<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 +361,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="$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="$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="$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="$t('per', { '0': $t('price'), '1': $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="$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="$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="$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="$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="$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="$t('by')" :td-attrs="hiddenMobile">
|
||||
{{ `${props.row.user.name}` }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props">
|
||||
@@ -406,11 +414,14 @@ 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 +429,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="$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="$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="$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="$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="$t('by')" :td-attrs="columnTdAttrs">
|
||||
{{ `${props.row.user.name}` }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props">
|
||||
@@ -451,20 +462,22 @@ 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 +491,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,18 +499,18 @@ 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="$t('labelforfile')"></b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field class="buttons">
|
||||
<b-button tag="input" native-type="submit" :disabled="tryingToUpload" type="is-primary" label="Upload File" value="Upload File">
|
||||
<b-button tag="button" native-type="submit" :disabled="tryingToUpload" type="is-primary" label="Upload File" value="Upload File">
|
||||
</b-button>
|
||||
<b-button
|
||||
tag="input"
|
||||
tag="button"
|
||||
native-type="submit"
|
||||
:disabled="tryingToUpload"
|
||||
type="is-danger"
|
||||
label="Upload File"
|
||||
label="Cancel"
|
||||
value="Cancel"
|
||||
@click="showAttachmentForm = false"
|
||||
>
|
||||
@@ -510,33 +523,46 @@ 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="$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="$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="$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">
|
||||
<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="column" :class="isMobile ? 'has-text-centered' : ''">
|
||||
<h1 class="title">{{ $t('statistics') }}</h1></div
|
||||
>
|
||||
<div class="column">
|
||||
<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>
|
||||
|
||||
14736
ui/yarn.lock
14736
ui/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user