Compare commits

..

1 Commits

Author SHA1 Message Date
Alf Sebastian Houge
728439c6c8 Bump versions 2022-03-05 19:18:51 +01:00
87 changed files with 25575 additions and 23211 deletions

View File

@@ -1,49 +1,57 @@
name: Build docker image
name: ci
on:
release:
types: [published]
push:
branches: master
jobs:
multi:
runs-on: ubuntu-latest
steps:
- name: Checkout
-
name: Checkout
uses: actions/checkout@v2
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v2
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Set up build cache
uses: actions/cache@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@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@v2
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Login to GitHub
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Login to GitHub
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
username: ${{ github.repository_owner }}
password: ${{ secrets.CR_PAT }}
-
name: Build and push
uses: docker/build-push-action@v2
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
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
tags: |
alfhou/hammond:latest
alfhou/hammond:${{ steps.get_tag.outputs.TAG }}
ghcr.io/alfhou/hammond:latest
ghcr.io/alfhou/hammond:${{ steps.get_tag.outputs.TAG }}
akhilrex/hammond:latest
akhilrex/hammond:1.0.0
ghcr.io/akhilrex/hammond:latest
ghcr.io/akhilrex/hammond:1.0.0

View File

@@ -1,16 +0,0 @@
on: [push, pull_request]
name: Test server
jobs:
test:
strategy:
matrix:
go-version: [1.17.x, 1.18.x]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
- uses: actions/checkout@v3
- run: go test ./...
working-directory: server

4
.gitignore vendored
View File

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

View File

@@ -1,4 +1,4 @@
ARG GO_VERSION=1.20.6
ARG GO_VERSION=1.16.2
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,18 +9,16 @@ RUN go mod download
COPY ./server .
RUN go build -o ./app ./main.go
FROM node:18-alpine as build-stage
FROM node:latest as build-stage
WORKDIR /app
COPY ./ui/package*.json ./
RUN apk add --no-cache autoconf automake build-base nasm libc6-compat python3 py3-pip make g++ libpng-dev zlib-dev pngquant
RUN npm install
COPY ./ui .
RUN npm run build
FROM alpine:latest
LABEL org.opencontainers.image.source="https://github.com/alfhou/hammond"
LABEL org.opencontainers.image.source="https://github.com/akhilrex/hammond"
ENV CONFIG=/config
ENV DATA=/assets
ENV UID=998
@@ -38,4 +36,4 @@ COPY --from=builder /api/app .
#COPY dist ./dist
COPY --from=build-stage /app/dist ./dist
EXPOSE 3000
ENTRYPOINT ["./app"]
ENTRYPOINT ["./app"]

113
README.md
View File

@@ -1,15 +1,26 @@
[![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 - 2021.09.20</p>
<p align="center">
A self-hosted vehicle expense tracking system with support for multiple users.
<br />
<a href="https://github.com/AlfHou/hammond"><strong>Explore the docs »</strong></a>
<a href="https://github.com/akhilrex/hammond"><strong>Explore the docs »</strong></a>
<br />
<br />
<a href="https://github.com/AlfHou/hammond/issues">Report Bug</a>
<!-- <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">Request Feature</a>
<a href="https://github.com/akhilrex/hammond/issues">Request Feature</a>
·
<a href="Screenshots.md">Screenshots</a>
</p>
@@ -24,7 +35,6 @@
- [Built With](#built-with)
- [Features](#features)
- [Installation](#installation)
- [Contributing](#contributing)
- [License](#license)
- [Roadmap](#roadmap)
- [Contact](#contact)
@@ -33,22 +43,18 @@
## 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.
This repo is again a fork of akhilrex's great [project](https://github.com/akhilrex/hammond).
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.
_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
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.
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.
We are trying our best to update with new features and feedback is very welcome.
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.
The project is written using Go for the backend and Vuejs for the front end.
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.
![Product Name Screen Shot][product-screenshot] [More Screenshots](Screenshots.md)
@@ -72,7 +78,7 @@ The project is written using Go for the backend and Vuejs for the front end.
- 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 and Drivvo
- Import from Fuelly (more apps coming soon)
## Installation
@@ -83,25 +89,24 @@ 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 alfhou/hammond
docker run -d -p 3000:3000 --name=hammond akhilrex/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" alfhou/hammond
docker run -d -p 3000:3000 --name=hammond -v "/host/path/to/assets:/assets" -v "/host/path/to/config:/config" akhilrex/hammond
```
### Using Docker-Compose
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
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
```yaml
version: '2.1'
services:
hammond:
image: alfhou/hammond
image: akhilrex/hammond
container_name: hammond
volumes:
- /path/to/config:/config
@@ -115,27 +120,9 @@ 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)
@@ -170,31 +157,6 @@ Once done you will be taken to the login page.
Go through the settings page once and change relevant settings before you start adding vehicles and expenses.
## Contributing
### Dev Setup
If you want to contribute to the project you need to set it up
for development first.
Fork and clone the project. Once you have it on your own machine,
open up a terminal and navigate to the `server/` directory.
In the `server/` directory run the command `go run main.go`.
After some initial
setup, the server should be listening on at port `3000`.
Next, open a new terminal. Navigate to the `ui/` directory and run `npm install`.
This will install all the dependencies for the frontend.
After the command is done running, run `npm run dev`. After some output, the
frontend should be accessible at `http://localhost:8080`.
If you are sent straight to the login screen, try closing the page and opening
it again. You should be greeted with a setup wizard the first time you run the
project.
Now, simply follow the instructions in order to set up your fresh install.
## License
Distributed under the GPL-3.0 License. See `LICENSE` for more information.
@@ -209,6 +171,25 @@ Distributed under the GPL-3.0 License. See `LICENSE` for more information.
## Contact
Project Link: [https://github.com/AlfHou/hammond](https://github.com/AlfHou/hammond)
Akhil Gupta - [@akhilrex](https://twitter.com/akhilrex)
Project Link: [https://github.com/akhilrex/hammond](https://github.com/akhilrex/hammond)
<a href="https://www.buymeacoffee.com/akhilrex" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="width: 217px !important;height: 60px !important;" ></a>
<!-- MARKDOWN LINKS & IMAGES -->
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
[contributors-shield]: https://img.shields.io/github/contributors/akhilrex/hammond.svg?style=flat-square
[contributors-url]: https://github.com/akhilrex/hammond/graphs/contributors
[forks-shield]: https://img.shields.io/github/forks/akhilrex/hammond.svg?style=flat-square
[forks-url]: https://github.com/akhilrex/hammond/network/members
[stars-shield]: https://img.shields.io/github/stars/akhilrex/hammond.svg?style=flat-square
[stars-url]: https://github.com/akhilrex/hammond/stargazers
[issues-shield]: https://img.shields.io/github/issues/akhilrex/hammond.svg?style=flat-square
[issues-url]: https://github.com/akhilrex/hammond/issues
[license-shield]: https://img.shields.io/github/license/akhilrex/hammond.svg?style=flat-square
[license-url]: https://github.com/akhilrex/hammond/blob/master/LICENSE
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=flat-square&logo=linkedin&colorB=555
[linkedin-url]: https://linkedin.com/in/akhilrex
[product-screenshot]: images/screenshot.jpg

View File

@@ -1,10 +1,10 @@
version: "2.1"
services:
hammond:
image: alfhou/hammond
image: akhilrex/hammond
container_name: hammond
environment:
- JWT_SECRET=somethingverystrong
- JWT_SECRET = somethingverystrong
volumes:
- /path/to/config:/config
- /path/to/data:/assets

View File

@@ -26,7 +26,7 @@ Following steps will only work if Go and Node are installed and configured prope
## Clone from Git
``` bash
git clone --depth 1 https://github.com/alfhou/hammond
git clone --depth 1 https://github.com/akhilrex/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/alfhou/hammond
git clone --depth 1 https://github.com/akhilrex/hammond
```
## Build and Copy dependencies

7
server/.gitignore vendored
View File

@@ -12,15 +12,10 @@
*.out
*.db
# MS VSCode
.vscode
!.vscode/launch.json
__debug_bin
# Dependency directories (remove the comment below to include it)
# vendor/
assets/*
keys/*
backups/*
nodemon.json
dist/*
dist/*

View File

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

View File

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

View File

@@ -5,13 +5,11 @@ import (
"fmt"
"net/http"
"os"
"strings"
"hammond/common"
"hammond/db"
"hammond/models"
"hammond/service"
"github.com/akhilrex/hammond/common"
"github.com/akhilrex/hammond/db"
"github.com/akhilrex/hammond/models"
"github.com/akhilrex/hammond/service"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
)
@@ -93,20 +91,20 @@ func userLogin(c *gin.Context) {
c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
return
}
user, err := db.FindOneUser(&db.User{Email: strings.ToLower(loginRequest.Email)})
user, err := db.FindOneUser(&db.User{Email: loginRequest.Email})
if err != nil {
c.JSON(http.StatusForbidden, common.NewError("login", errors.New("not Registered email or invalid password")))
c.JSON(http.StatusForbidden, common.NewError("login", errors.New("Not Registered email or invalid password")))
return
}
if user.CheckPassword(loginRequest.Password) != nil {
c.JSON(http.StatusForbidden, common.NewError("login", errors.New("not Registered email or invalid password")))
c.JSON(http.StatusForbidden, common.NewError("login", errors.New("Not Registered email or invalid password")))
return
}
if user.IsDisabled {
c.JSON(http.StatusForbidden, common.NewError("login", errors.New("your user has been disabled by the admin. Please contact them to get it re-enabled")))
c.JSON(http.StatusForbidden, common.NewError("login", errors.New("Your user has been disabled by the admin. Please contact them to get it re-enabled.")))
return
}
UpdateContextUserModel(c, user.ID)
@@ -116,7 +114,7 @@ func userLogin(c *gin.Context) {
Email: user.Email,
Token: token,
RefreshToken: refreshToken,
Role: user.RoleDetail().Key,
Role: user.RoleDetail().Long,
}
c.JSON(http.StatusOK, response)
}
@@ -150,7 +148,7 @@ func refresh(c *gin.Context) {
Email: user.Email,
Token: token,
RefreshToken: refreshToken,
Role: user.RoleDetail().Key,
Role: user.RoleDetail().Long,
}
c.JSON(http.StatusOK, response)
} else {
@@ -172,16 +170,16 @@ func changePassword(c *gin.Context) {
user, err := service.GetUserById(c.GetString("userId"))
if err != nil {
c.JSON(http.StatusForbidden, common.NewError("changePassword", errors.New("not Registered email or invalid password")))
c.JSON(http.StatusForbidden, common.NewError("changePassword", errors.New("Not Registered email or invalid password")))
return
}
if user.CheckPassword(request.OldPassword) != nil {
c.JSON(http.StatusForbidden, common.NewError("changePassword", errors.New("incorrect old password")))
c.JSON(http.StatusForbidden, common.NewError("changePassword", errors.New("Incorrect old password")))
return
}
user.SetPassword(request.NewPassword)
success, _ := service.UpdatePassword(user.ID, request.NewPassword)
success, err := service.UpdatePassword(user.ID, request.NewPassword)
c.JSON(http.StatusOK, success)
}

View File

@@ -5,11 +5,10 @@ import (
"net/http"
"os"
"hammond/common"
"hammond/db"
"hammond/models"
"hammond/service"
"github.com/akhilrex/hammond/common"
"github.com/akhilrex/hammond/db"
"github.com/akhilrex/hammond/models"
"github.com/akhilrex/hammond/service"
"github.com/gin-gonic/gin"
)

View File

@@ -2,18 +2,13 @@ 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) {
@@ -29,46 +24,3 @@ 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{})
}

View File

@@ -3,11 +3,10 @@ package controllers
import (
"net/http"
"hammond/common"
"hammond/db"
"hammond/models"
"hammond/service"
"github.com/akhilrex/hammond/common"
"github.com/akhilrex/hammond/db"
"github.com/akhilrex/hammond/models"
"github.com/akhilrex/hammond/service"
"github.com/gin-gonic/gin"
)

View File

@@ -5,8 +5,7 @@ import (
"os"
"strings"
"hammond/db"
"github.com/akhilrex/hammond/db"
"github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go/request"
"github.com/gin-gonic/gin"
@@ -24,8 +23,8 @@ func stripBearerPrefixFromTokenString(tok string) (string, error) {
// Extract token from Authorization header
// Uses PostExtractionFilter to strip "TOKEN " prefix from header
var AuthorizationHeaderExtractor = &request.PostExtractionFilter{
Extractor: request.HeaderExtractor{"Authorization"},
Filter: stripBearerPrefixFromTokenString,
request.HeaderExtractor{"Authorization"},
stripBearerPrefixFromTokenString,
}
// Extractor for OAuth2 access tokens. Looks in 'Authorization'

View File

@@ -3,10 +3,9 @@ package controllers
import (
"net/http"
"hammond/common"
"hammond/models"
"hammond/service"
"github.com/akhilrex/hammond/common"
"github.com/akhilrex/hammond/models"
"github.com/akhilrex/hammond/service"
"github.com/gin-gonic/gin"
)
@@ -26,7 +25,7 @@ func getMileageForVehicle(c *gin.Context) {
return
}
fillups, err := service.GetMileageByVehicleId(searchByIdQuery.Id, model.Since, model.MileageOption)
fillups, err := service.GetMileageByVehicleId(searchByIdQuery.Id, model.Since)
if err != nil {
c.JSON(http.StatusUnprocessableEntity, common.NewError("getMileageForVehicle", err))
return

View File

@@ -4,11 +4,10 @@ import (
"fmt"
"net/http"
"hammond/common"
"hammond/db"
"hammond/models"
"hammond/service"
"github.com/akhilrex/hammond/common"
"github.com/akhilrex/hammond/db"
"github.com/akhilrex/hammond/models"
"github.com/akhilrex/hammond/service"
"github.com/gin-gonic/gin"
)
@@ -52,7 +51,7 @@ func migrate(c *gin.Context) {
canMigrate, _, _ := db.CanMigrate(request.Url)
if !canMigrate {
c.JSON(http.StatusBadRequest, fmt.Errorf("cannot migrate database. please check connection string"))
c.JSON(http.StatusBadRequest, fmt.Errorf("cannot migrate database. please check connection string."))
return
}

View File

@@ -3,11 +3,10 @@ package controllers
import (
"net/http"
"hammond/common"
"hammond/db"
"hammond/models"
"hammond/service"
"github.com/akhilrex/hammond/common"
"github.com/akhilrex/hammond/db"
"github.com/akhilrex/hammond/models"
"github.com/akhilrex/hammond/service"
"github.com/gin-gonic/gin"
)

View File

@@ -4,10 +4,9 @@ import (
"errors"
"net/http"
"hammond/common"
"hammond/models"
"hammond/service"
"github.com/akhilrex/hammond/common"
"github.com/akhilrex/hammond/models"
"github.com/akhilrex/hammond/service"
"github.com/gin-gonic/gin"
)
@@ -398,7 +397,7 @@ func deleteVehicle(c *gin.Context) {
return
}
if !canDelete {
c.JSON(http.StatusUnprocessableEntity, common.NewError("shareVehicle", errors.New("you are not allowed to delete this vehicle")))
c.JSON(http.StatusUnprocessableEntity, common.NewError("shareVehicle", errors.New("You are not allowed to delete this vehicle.")))
return
}
err = service.DeleteVehicle(searchByIdQuery.Id)

View File

@@ -60,7 +60,6 @@ type Vehicle struct {
Base
Nickname string `json:"nickname"`
Registration string `json:"registration"`
VIN string `json:"vin"`
Make string `json:"make"`
Model string `json:"model"`
YearOfManufacture int `json:"yearOfManufacture"`
@@ -196,50 +195,3 @@ type VehicleAttachment struct {
VehicleID string `gorm:"primaryKey" json:"vehicleId"`
Title string `json:"title"`
}
type VehicleAlert struct {
Base
VehicleID string `json:"vehicleId"`
Vehicle Vehicle `json:"-"`
UserID string `json:"userId"`
User User `json:"user"`
Title string `json:"title"`
Comments string `json:"comments"`
StartDate time.Time `json:"date"`
StartOdoReading int `json:"startOdoReading"`
DistanceUnit DistanceUnit `json:"distanceUnit"`
AlertFrequency AlertFrequency `json:"alertFrequency"`
OdoFrequency int `json:"odoFrequency"`
DayFrequency int `json:"dayFrequency"`
AlertAllUsers bool `json:"alertAllUsers"`
IsActive bool `json:"isActive"`
EndDate *time.Time `json:"endDate"`
AlertType AlertType `json:"alertType"`
}
type AlertOccurance struct {
Base
VehicleID string `json:"vehicleId"`
Vehicle Vehicle `json:"-"`
VehicleAlertID string `json:"vehicleAlertId"`
VehicleAlert VehicleAlert `json:"-"`
UserID string `json:"userId"`
User User `json:"-"`
OdoReading int `json:"odoReading"`
Date *time.Time `json:"date"`
ProcessDate *time.Time `json:"processDate"`
AlertProcessType AlertType `json:"alertProcessType"`
CompleteDate *time.Time `json:"completeDate"`
}
type Notification struct {
Base
Title string `json:"title"`
Content string `json:"content"`
UserID string `json:"userId"`
VehicleID string `json:"vehicleId"`
User User `json:"-"`
Date time.Time `json:"date"`
ReadDate *time.Time `json:"readDate"`
ParentID string `json:"parentId"`
ParentType string `json:"parentType"`
}

View File

@@ -117,7 +117,7 @@ func UnshareVehicle(vehicleId, userId string) error {
return nil
}
if mapping.IsOwner {
return fmt.Errorf("cannot unshare owner")
return fmt.Errorf("Cannot unshare owner")
}
result := DB.Where("id=?", mapping.ID).Delete(&UserVehicle{})
return result.Error
@@ -160,11 +160,6 @@ func GetFillupsByVehicleId(id string) (*[]Fillup, error) {
result := DB.Preload(clause.Associations).Order("date desc").Find(&obj, &Fillup{VehicleID: id})
return &obj, result.Error
}
func GetLatestFillupsByVehicleId(id string) (*Fillup, error) {
var obj Fillup
result := DB.Preload(clause.Associations).Order("date desc").First(&obj, &Fillup{VehicleID: id})
return &obj, result.Error
}
func GetFillupsByVehicleIdSince(id string, since time.Time) (*[]Fillup, error) {
var obj []Fillup
result := DB.Where("date >= ? AND vehicle_id = ?", since, id).Preload(clause.Associations).Order("date desc").Find(&obj)
@@ -195,11 +190,6 @@ func GetExpensesByVehicleId(id string) (*[]Expense, error) {
result := DB.Preload(clause.Associations).Order("date desc").Find(&obj, &Expense{VehicleID: id})
return &obj, result.Error
}
func GetLatestExpenseByVehicleId(id string) (*Expense, error) {
var obj Expense
result := DB.Preload(clause.Associations).Order("date desc").First(&obj, &Expense{VehicleID: id})
return &obj, result.Error
}
func GetExpenseById(id string) (*Expense, error) {
var obj Expense
result := DB.Preload(clause.Associations).First(&obj, "id=?", id)
@@ -281,29 +271,6 @@ func GetVehicleAttachments(vehicleId string) (*[]Attachment, error) {
}
return &attachments, nil
}
func GeAlertById(id string) (*VehicleAlert, error) {
var alert VehicleAlert
result := DB.Preload(clause.Associations).First(&alert, "id=?", id)
return &alert, result.Error
}
func GetAlertOccurenceByAlertId(id string) (*[]AlertOccurance, error) {
var alertOccurance []AlertOccurance
result := DB.Preload(clause.Associations).Order("created_at desc").Find(&alertOccurance, "vehicle_alert_id=?", id)
return &alertOccurance, result.Error
}
func GetUnprocessedAlertOccurances() (*[]AlertOccurance, error) {
var alertOccurance []AlertOccurance
result := DB.Preload(clause.Associations).Order("created_at desc").Find(&alertOccurance, "process_date is NULL")
return &alertOccurance, result.Error
}
func MarkAlertOccuranceAsProcessed(id string, alertProcessType AlertType, date time.Time) error {
tx := DB.Debug().Model(&AlertOccurance{}).Where("id= ?", id).
Update("alert_process_type", alertProcessType).
Update("process_date", date)
return tx.Error
}
func UpdateSettings(setting *Setting) error {
tx := DB.Save(&setting)
@@ -365,7 +332,8 @@ func UnlockMissedJobs() {
if (job.Date == time.Time{}) {
continue
}
var duration = time.Duration(job.Duration)
var duration time.Duration
duration = time.Duration(job.Duration)
d := job.Date.Add(time.Minute * duration)
if d.Before(time.Now()) {
fmt.Println(job.Name + " is unlocked")

View File

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

View File

@@ -18,15 +18,6 @@ var migrations = []localMigration{
Name: "2021_06_24_04_42_SetUserDisabledFalse",
Query: "update users set is_disabled=0",
},
{
Name: "2021_02_07_00_09_LowerCaseEmails",
Query: "update users set email=lower(email)",
},
{
Name: "2022_03_08_13_16_AddVIN",
Query: "ALTER TABLE vehicles ADD COLUMN vin text",
},
}
func RunMigrations() {

View File

@@ -1,21 +1,32 @@
module hammond
module github.com/akhilrex/hammond
go 1.16
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-contrib/location v0.0.2
github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19 // indirect
github.com/gin-gonic/gin v1.7.1
github.com/go-playground/validator/v10 v10.4.1
github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19
github.com/gin-gonic/gin v1.7.7
github.com/go-playground/validator/v10 v10.10.0
github.com/golang/protobuf v1.5.2 // indirect
github.com/jasonlvhit/gocron v0.0.1
github.com/joho/godotenv v1.3.0
github.com/leekchan/accounting v1.0.0 // indirect
github.com/joho/godotenv v1.4.0
github.com/json-iterator/go v1.1.12 // indirect
github.com/leekchan/accounting v1.0.0
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/satori/go.uuid v1.2.0
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1
github.com/shopspring/decimal v1.3.1 // indirect
github.com/ugorji/go v1.2.7 // indirect
golang.org/x/crypto v0.0.0-20220214200702-86341886e292
golang.org/x/net v0.0.0-20220225172249-27dd8689420f
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gorm.io/driver/mysql v1.0.5
gorm.io/driver/sqlite v1.1.4
gorm.io/gorm v1.21.3
gopkg.in/yaml.v2 v2.4.0 // indirect
gorm.io/driver/mysql v1.3.2
gorm.io/driver/sqlite v1.3.1
gorm.io/gorm v1.23.2
)

View File

@@ -1,5 +1,6 @@
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -15,21 +16,35 @@ github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19/go.mod h1:iqneQ2
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.7.1 h1:qC89GU3p8TvKWMAVhEpmpB2CIb1hnqt2UdKZaP93mS8=
github.com/gin-gonic/gin v1.7.1/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-redis/redis v6.15.5+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jasonlvhit/gocron v0.0.1 h1:qTt5qF3b3srDjeOIR4Le1LfeyvoYzJlYpqvG7tJX5YU=
@@ -38,55 +53,95 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leekchan/accounting v1.0.0 h1:+Wd7dJ//dFPa28rc1hjyy+qzCbXPMR91Fb6F1VGTQHg=
github.com/leekchan/accounting v1.0.0/go.mod h1:3timm6YPhY3YDaGxl0q3eaflX0eoSx3FXn7ckHe4tO0=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ=
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 h1:4qWs8cYYH6PoEFy4dfhDFgoMGkwAcETd+MmPdCPMzUc=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -96,24 +151,53 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.0.5 h1:WAAmvLK2rG0tCOqrf5XcLi2QUwugd4rcVJ/W3aoon9o=
gorm.io/driver/mysql v1.0.5/go.mod h1:N1OIhHAIhx5SunkMGqWbGFVeh4yTNWKmMo1GOAsohLI=
gorm.io/driver/mysql v1.3.2 h1:QJryWiqQ91EvZ0jZL48NOpdlPdMjdip1hQ8bTgo4H7I=
gorm.io/driver/mysql v1.3.2/go.mod h1:ChK6AHbHgDCFZyJp0F+BmVGb06PSIoh9uVYKAlRbb2U=
gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
gorm.io/driver/sqlite v1.3.1 h1:bwfE+zTEWklBYoEodIOIBwuWHpnx52Z9zJFW5F33WLk=
gorm.io/driver/sqlite v1.3.1/go.mod h1:wJx0hJspfycZ6myN38x1O/AqLtNS6c5o9TndewFbELg=
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.21.3 h1:qDFi55ZOsjZTwk5eN+uhAmHi8GysJ/qCTichM/yO7ME=
gorm.io/gorm v1.21.3/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.2 h1:xmq9QRMWL8HTJyhAUBXy8FqIIQCYESeKfJL4DoGKiWQ=
gorm.io/gorm v1.23.2/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=

View File

@@ -5,10 +5,9 @@ import (
"log"
"os"
"hammond/controllers"
"hammond/db"
"hammond/service"
"github.com/akhilrex/hammond/controllers"
"github.com/akhilrex/hammond/db"
"github.com/akhilrex/hammond/service"
"github.com/gin-contrib/location"
"github.com/gin-gonic/contrib/static"
"github.com/gin-gonic/gin"

View File

@@ -1,21 +0,0 @@
package models
import (
"time"
"hammond/db"
)
type CreateAlertModel struct {
Comments string `json:"comments"`
Title string `json:"title"`
StartDate time.Time `json:"date"`
StartOdoReading int `json:"startOdoReading"`
DistanceUnit *db.DistanceUnit `json:"distanceUnit"`
AlertFrequency *db.AlertFrequency `json:"alertFrequency"`
OdoFrequency int `json:"odoFrequency"`
DayFrequency int `json:"dayFrequency"`
AlertAllUsers bool `json:"alertAllUsers"`
IsActive bool `json:"isActive"`
AlertType *db.AlertType `json:"alertType"`
}

View File

@@ -1,6 +1,6 @@
package models
import "hammond/db"
import "github.com/akhilrex/hammond/db"
type LoginResponse struct {
Name string `json:"name"`

View File

@@ -1,22 +0,0 @@
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"`
}

View File

@@ -1,6 +1,6 @@
package models
import "hammond/db"
import "github.com/akhilrex/hammond/db"
type UpdateSettingModel struct {
Currency string `json:"currency" form:"currency" query:"currency"`

View File

@@ -4,7 +4,7 @@ import (
"encoding/json"
"time"
"hammond/db"
"github.com/akhilrex/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,5 +35,4 @@ func (b *MileageModel) MarshalJSON() ([]byte, error) {
type MileageQueryModel struct {
Since time.Time `json:"since" query:"since" form:"since"`
MileageOption string `json:"mileageOption" query:"mileageOption" form:"mileageOption"`
}

View File

@@ -3,8 +3,7 @@ package models
import (
"time"
"hammond/db"
"github.com/akhilrex/hammond/db"
_ "github.com/go-playground/validator/v10"
)
@@ -18,7 +17,6 @@ type SubItemQuery struct {
type CreateVehicleRequest struct {
Nickname string `form:"nickname" json:"nickname" binding:"required"`
Registration string `form:"registration" json:"registration" binding:"required"`
VIN string `form:"vin" json:"vin"`
Make string `form:"make" json:"make" binding:"required"`
Model string `form:"model" json:"model" binding:"required"`
YearOfManufacture int `form:"yearOfManufacture" json:"yearOfManufacture"`

View File

@@ -1,172 +0,0 @@
package service
import (
"errors"
"time"
"hammond/db"
"hammond/models"
)
func CreateAlert(model models.CreateAlertModel, vehicleId, userId string) (*db.VehicleAlert, error) {
alert := db.VehicleAlert{
VehicleID: vehicleId,
UserID: userId,
Title: model.Title,
Comments: model.Comments,
StartDate: model.StartDate,
StartOdoReading: model.StartOdoReading,
DistanceUnit: *model.DistanceUnit,
AlertFrequency: *model.AlertFrequency,
OdoFrequency: model.OdoFrequency,
DayFrequency: model.DayFrequency,
AlertAllUsers: model.AlertAllUsers,
IsActive: model.IsActive,
AlertType: *model.AlertType,
}
tx := db.DB.Create(&alert)
if tx.Error != nil {
return nil, tx.Error
}
go CreateAlertInstance(alert.ID)
return &alert, nil
}
func CreateAlertInstance(alertId string) error {
alert, err := db.GeAlertById(alertId)
if err != nil {
return err
}
existingOccurence, err := db.GetAlertOccurenceByAlertId(alertId)
if err != nil {
return err
}
var lastOccurance db.AlertOccurance
useOccurance := false
if len(*existingOccurence) > 0 {
lastOccurance = (*existingOccurence)[0]
useOccurance = true
if alert.AlertFrequency == db.ONETIME {
return errors.New("Only single occurance is possible for this kind of alert")
}
}
users := []string{alert.UserID}
if alert.AlertAllUsers {
allUsers, err := db.GetVehicleUsers(alert.VehicleID)
if err != nil {
return err
}
users = make([]string, len(*allUsers))
for i, user := range *allUsers {
users[i] = user.UserID
}
}
for _, userId := range users {
model := db.AlertOccurance{
VehicleID: alert.VehicleID,
UserID: userId,
VehicleAlertID: alertId,
}
if alert.AlertType == db.DISTANCE || alert.AlertType == db.BOTH {
model.OdoReading = alert.StartOdoReading + alert.OdoFrequency
if useOccurance {
model.OdoReading = lastOccurance.OdoReading + alert.OdoFrequency
}
}
if alert.AlertType == db.TIME || alert.AlertType == db.BOTH {
date := alert.StartDate.Add(time.Duration(alert.DayFrequency) * 24 * time.Hour)
if useOccurance {
date = lastOccurance.Date.Add(time.Duration(alert.DayFrequency) * 24 * time.Hour)
}
model.Date = &date
}
tx := db.DB.Create(&model)
if tx.Error != nil {
return tx.Error
}
}
return nil
}
func ProcessAlertOccurance(occurance db.AlertOccurance, today time.Time) error {
if occurance.ProcessDate != nil {
return errors.New("Alert occurence already processed")
}
alert := occurance.VehicleAlert
if !alert.IsActive {
return errors.New("Alert is not active")
}
notification := db.Notification{
Title: alert.Title,
Content: alert.Comments,
UserID: occurance.UserID,
VehicleID: occurance.VehicleID,
Date: today,
ParentID: occurance.ID,
ParentType: "AlertOccurance",
}
var alertProcessType db.AlertType
if alert.AlertType == db.DISTANCE || alert.AlertType == db.BOTH {
odoReading, err := GetLatestOdoReadingForVehicle(occurance.VehicleID)
if err != nil {
return err
}
if odoReading >= occurance.OdoReading {
alertProcessType = db.DISTANCE
}
}
if alert.AlertType == db.TIME || alert.AlertType == db.BOTH {
if occurance.Date.Before(today) {
alertProcessType = db.TIME
}
}
db.DB.Create(&notification)
return db.MarkAlertOccuranceAsProcessed(occurance.ID, alertProcessType, today)
}
func FindAlertOccurancesToProcess(today time.Time) ([]db.AlertOccurance, error) {
occurances, err := db.GetUnprocessedAlertOccurances()
if err != nil {
return nil, err
}
if len(*occurances) == 0 {
return make([]db.AlertOccurance, 0), nil
}
var toReturn []db.AlertOccurance
for _, occurance := range *occurances {
alert := occurance.VehicleAlert
if !alert.IsActive {
continue
}
if alert.AlertType == db.DISTANCE || alert.AlertType == db.BOTH {
odoReading, err := GetLatestOdoReadingForVehicle(occurance.VehicleID)
if err != nil {
return nil, err
}
if odoReading >= occurance.OdoReading {
toReturn = append(toReturn, occurance)
continue
}
}
if alert.AlertType == db.TIME || alert.AlertType == db.BOTH {
if occurance.Date.Before(today) {
toReturn = append(toReturn, occurance)
continue
}
}
}
return toReturn, nil
}
func MarkAlertOccuranceAsCompleted() {
}

View File

@@ -1,142 +0,0 @@
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
}

View File

@@ -3,6 +3,7 @@ package service
import (
"archive/tar"
"compress/gzip"
"errors"
"fmt"
"io"
"net/http"
@@ -13,10 +14,9 @@ import (
"strconv"
"time"
"hammond/db"
"hammond/internal/sanitize"
"hammond/models"
"github.com/akhilrex/hammond/db"
"github.com/akhilrex/hammond/internal/sanitize"
"github.com/akhilrex/hammond/models"
uuid "github.com/satori/go.uuid"
)
@@ -126,14 +126,14 @@ func CreateBackup() (string, error) {
tarballFilePath := path.Join(folder, backupFileName)
file, err := os.Create(tarballFilePath)
if err != nil {
return "", fmt.Errorf("could not create tarball file '%s', got error '%s'", tarballFilePath, err.Error())
return "", errors.New(fmt.Sprintf("Could not create tarball file '%s', got error '%s'", tarballFilePath, err.Error()))
}
defer file.Close()
dbPath := path.Join(configPath, "hammond.db")
_, err = os.Stat(dbPath)
if err != nil {
return "", fmt.Errorf("could not find db file '%s', got error '%s'", dbPath, err.Error())
return "", errors.New(fmt.Sprintf("Could not find db file '%s', got error '%s'", dbPath, err.Error()))
}
gzipWriter := gzip.NewWriter(file)
defer gzipWriter.Close()
@@ -151,13 +151,13 @@ func CreateBackup() (string, error) {
func addFileToTarWriter(filePath string, tarWriter *tar.Writer) error {
file, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("could not open file '%s', got error '%s'", filePath, err.Error())
return errors.New(fmt.Sprintf("Could not open file '%s', got error '%s'", filePath, err.Error()))
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
return fmt.Errorf("could not get stat for file '%s', got error '%s'", filePath, err.Error())
return errors.New(fmt.Sprintf("Could not get stat for file '%s', got error '%s'", filePath, err.Error()))
}
header := &tar.Header{
@@ -169,12 +169,12 @@ func addFileToTarWriter(filePath string, tarWriter *tar.Writer) error {
err = tarWriter.WriteHeader(header)
if err != nil {
return fmt.Errorf("could not write header for file '%s', got error '%s'", filePath, err.Error())
return errors.New(fmt.Sprintf("Could not write header for file '%s', got error '%s'", filePath, err.Error()))
}
_, err = io.Copy(tarWriter, file)
if err != nil {
return fmt.Errorf("could not copy the file '%s' data to the tarball, got error '%s'", filePath, err.Error())
return errors.New(fmt.Sprintf("Could not copy the file '%s' data to the tarball, got error '%s'", filePath, err.Error()))
}
return nil

View File

@@ -1,141 +0,0 @@
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
}

View File

@@ -1,47 +0,0 @@
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
}

View File

@@ -2,13 +2,144 @@ package service
import (
"bytes"
"encoding/csv"
"fmt"
"strconv"
"time"
"hammond/db"
"hammond/models"
"github.com/akhilrex/hammond/db"
"github.com/leekchan/accounting"
)
func WriteToDB(fillups []db.Fillup, expenses []db.Expense) []string {
func FuellyImport(content []byte, userId string) []string {
stream := bytes.NewReader(content)
reader := csv.NewReader(stream)
records, err := reader.ReadAll()
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 {
@@ -19,114 +150,19 @@ func WriteToDB(fillups []db.Fillup, expenses []db.Expense) []string {
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(&fillups).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
}
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)
}

View File

@@ -1,7 +1,7 @@
package service
import (
"hammond/db"
"github.com/akhilrex/hammond/db"
)
func CanInitializeSystem() (bool, error) {

View File

@@ -1,15 +1,13 @@
package service
import (
"sort"
"time"
"hammond/common"
"hammond/db"
"hammond/models"
"github.com/akhilrex/hammond/db"
"github.com/akhilrex/hammond/models"
)
func GetMileageByVehicleId(vehicleId string, since time.Time, mileageOption string) (mileage []models.MileageModel, err error) {
func GetMileageByVehicleId(vehicleId string, since time.Time) (mileage []models.MileageModel, err error) {
data, err := db.GetFillupsByVehicleIdSince(vehicleId, since)
if err != nil {
return nil, err
@@ -17,9 +15,6 @@ func GetMileageByVehicleId(vehicleId string, since time.Time, mileageOption stri
fillups := make([]db.Fillup, len(*data))
copy(fillups, *data)
sort.Slice(fillups, func(i, j int) bool {
return fillups[i].OdoReading > fillups[j].OdoReading
})
var mileages []models.MileageModel
@@ -37,48 +32,14 @@ func GetMileageByVehicleId(vehicleId string, since time.Time, mileageOption stri
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)) {
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;
distance := float32(currentFillup.OdoReading - lastFillup.OdoReading)
mileage.Mileage = distance / currentFillup.FuelQuantity
mileage.CostPerMile = distance / currentFillup.TotalAmount
}

View File

@@ -1,16 +1,14 @@
package service
import (
"strings"
"hammond/db"
"hammond/models"
"github.com/akhilrex/hammond/db"
"github.com/akhilrex/hammond/models"
)
func CreateUser(userModel *models.RegisterRequest, role db.Role) error {
setting := db.GetOrCreateSetting()
toCreate := db.User{
Email: strings.ToLower(userModel.Email),
Email: userModel.Email,
Name: userModel.Name,
Role: role,
Currency: setting.Currency,

View File

@@ -3,10 +3,8 @@ package service
import (
"fmt"
"hammond/db"
"hammond/models"
"gorm.io/gorm"
"github.com/akhilrex/hammond/db"
"github.com/akhilrex/hammond/models"
"gorm.io/gorm/clause"
)
@@ -15,7 +13,6 @@ func CreateVehicle(model models.CreateVehicleRequest, userId string) (*db.Vehicl
Nickname: model.Nickname,
Registration: model.Registration,
Model: model.Model,
VIN: model.VIN,
Make: model.Make,
YearOfManufacture: model.YearOfManufacture,
EngineSize: model.EngineSize,
@@ -102,7 +99,6 @@ func UpdateVehicle(vehicleID string, model models.UpdateVehicleRequest) error {
//return db.DB.Model(&toUpdate).Updates(db.Vehicle{
toUpdate.Nickname = model.Nickname
toUpdate.Registration = model.Registration
toUpdate.VIN = model.VIN
toUpdate.Model = model.Model
toUpdate.Make = model.Make
toUpdate.YearOfManufacture = model.YearOfManufacture
@@ -247,24 +243,6 @@ func GetDistinctFuelSubtypesForVehicle(vehicleId string) ([]string, error) {
return names, tx.Error
}
func GetLatestOdoReadingForVehicle(vehicleId string) (int, error) {
odoReading := 0
latestFillup, err := db.GetLatestExpenseByVehicleId(vehicleId)
if err != nil && err != gorm.ErrRecordNotFound {
return 0, err
}
odoReading = latestFillup.OdoReading
latestExpense, err := db.GetLatestExpenseByVehicleId(vehicleId)
if err != nil && err != gorm.ErrRecordNotFound {
return 0, err
}
if latestExpense.OdoReading > odoReading {
odoReading = latestExpense.OdoReading
}
return odoReading, nil
}
func GetUserStats(userId string, model models.UserStatsQueryModel) ([]models.VehicleStatsModel, error) {
vehicles, err := GetUserVehicles(userId)

View File

@@ -20,7 +20,6 @@ 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
View File

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

30
ui/.vscode/_components.code-snippets vendored Normal file
View File

@@ -0,0 +1,30 @@
{
"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 Normal file
View File

@@ -0,0 +1,26 @@
{
"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 Normal file
View File

@@ -0,0 +1,37 @@
{
// 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 Normal file
View File

@@ -0,0 +1,93 @@
{
// ===
// 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
}
}

View File

@@ -6,9 +6,10 @@ WORKDIR /app
# Copy dependency-related files
COPY package.json ./
COPY yarn.lock ./
# Install project dependencies
RUN npm install
RUN yarn install
# Expose ports 8080, which the dev server will be bound to
EXPOSE 8080

30161
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,82 +6,89 @@
"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": "npm build --report",
"build:ci": "yarn 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"
"docs": "vuepress dev",
"docker": "docker-compose exec dev yarn"
},
"gitHooks": {
"pre-commit": "cross-env PRE_COMMIT=true lint-staged"
},
"dependencies": {
"@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",
"@fortawesome/fontawesome-svg-core": "^1.2.27",
"@fortawesome/free-solid-svg-icons": "^5.12.1",
"@fortawesome/vue-fontawesome": "0.1.9",
"axios": "0.19.2",
"buefy": "^0.9.7",
"chart.js": "^2.9.4",
"core-js": "^3.27.2",
"currency-formatter": "^1.5.9",
"date-fns": "^2.29.3",
"lodash": "^4.17.21",
"node-gyp": "^9.3.1",
"normalize.css": "^8.0.1",
"nprogress": "^0.2.0",
"papaparse": "^5.4.1",
"vue": "^2.6.11",
"core-js": "3.6.4",
"currency-formatter": "^1.5.7",
"date-fns": "2.10.0",
"lodash": "4.17.15",
"normalize.css": "8.0.1",
"nprogress": "0.2.0",
"vue": "2.6.11",
"vue-chartjs": "^3.5.1",
"vue-i18n": "^8.28.2",
"vue-meta": "^2.4.0",
"vue-router": "^3.6.5",
"vuex": "^3.6.2"
"vue-meta": "2.3.3",
"vue-router": "3.1.6",
"vuex": "3.1.2"
},
"devDependencies": {
"@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/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/eslint-config-prettier": "6.0.x",
"@vue/eslint-config-standard": "^5.1.1",
"@vue/test-utils": "^1.3.4",
"babel-core": "^7.0.0-bridge.0",
"@vue/eslint-config-standard": "5.1.x",
"@vue/test-utils": "1.0.0-beta.31",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "10.1.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"
"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.22.x",
"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"
},
"resolutions": {
"@vue/cli-plugin-unit-jest/jest": "25.1.x",
"@vue/cli-plugin-unit-jest/babel-jest": "25.1.x"
},
"engines": {
"node": ">=16.0.0"
"node": ">=10.13.3",
"yarn": ">=1.0.0"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 895 B

After

Width:  |  Height:  |  Size: 463 B

View File

@@ -5,12 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<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. -->

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

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

View File

@@ -3,20 +3,9 @@ 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 },
mileageOption: { type: string, default: 'litre_100km' },
},
data: function() {
return {
chartData: [],
}
},
props: { vehicle: { type: Object, required: true }, since: { type: Date, default: '' }, user: { type: Object, required: true } },
computed: {
...mapState('utils', ['isMobile']),
},
@@ -28,28 +17,20 @@ 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 (${mileageLabel})`,
label: `Mileage (${this.user.distanceUnitDetail.short}/${this.vehicle.fuelUnitDetail.short})`,
fill: true,
data: this.chartData.map((x) => x.mileage),
}
@@ -60,7 +41,6 @@ export default {
.get(`/api/vehicles/${this.vehicle.id}/mileage`, {
params: {
since: this.since,
mileageOption: this.mileageOption,
},
})
.then((response) => {

View File

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

View File

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

View File

@@ -55,7 +55,7 @@ export default {
return
}
this.$buefy.dialog.confirm({
title: this.$t('transfervehicle'),
title: 'Transfer Vehicle',
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">{{ $t('share') }} {{ vehicle.nickname }}</h1>
<h1 class="subtitle">Share {{ vehicle.nickname }}</h1>
<section>
<div v-for="model in models" :key="model.id" class="columns is-mobile">
<div class="columns is-mobile" v-for="model in models" :key="model.id">
<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)">{{ $t('makeowner') }}</b-button>
<b-button v-if="model.isShared && !model.isOwner" type="is-primary is-small" @click="transferVehicle(model)">Make Owner</b-button>
</b-field></div
></div
>

View File

@@ -1,6 +1,6 @@
<script>
import { addDays, addMonths } from 'date-fns'
import currencyFormatter from 'currency-formatter'
import currencyFormtter from 'currency-formatter'
import { mapState } from 'vuex'
import axios from 'axios'
@@ -14,12 +14,12 @@ export default {
data: function() {
return {
dateRangeOptions: [
{ 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' },
{ 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' },
],
dateRangeOption: 'past_30_days',
stats: [],
@@ -32,15 +32,15 @@ export default {
return [
[
{
label: this.$t('totalexpenses'),
label: 'Total Expenditure',
value: this.formatCurrency(0, this.user.currency),
},
{
label: this.$t('fillupcost'),
label: 'Fillup Costs',
value: `${this.formatCurrency(0, this.user.currency)} (0)`,
},
{
label: this.$t('otherexpenses'),
label: 'Other Expenses',
value: `${this.formatCurrency(0, this.user.currency)} (0)`,
},
],
@@ -49,15 +49,15 @@ export default {
return this.stats.map((x) => {
return [
{
label: this.$t('totalexpenses'),
label: 'Total Expenditure',
value: this.formatCurrency(x.expenditureTotal, x.currency),
},
{
label: this.$t('fillupcost'),
label: 'Fillup Costs',
value: `${this.formatCurrency(x.expenditureFillups, x.currency)} (${x.countFillups})`,
},
{
label: this.$t('otherexpenses'),
label: 'Other Expenses',
value: `${this.formatCurrency(x.expenditureExpenses, x.currency)} (${x.countExpenses})`,
},
]
@@ -80,7 +80,7 @@ export default {
if (!currencyCode) {
currencyCode = this.me.currency
}
return currencyFormatter.format(number, { code: currencyCode })
return currencyFormtter.format(number, { code: currencyCode })
},
getStats() {
axios
@@ -106,7 +106,6 @@ export default {
if (currentDayOfWeek > 1) {
toSubtract = -1 * (currentDayOfWeek - 1)
}
toDate.setHours(0, 0, 0, 0)
return addDays(toDate, toSubtract)
case 'this_month':
return new Date(toDate.getFullYear(), toDate.getMonth(), 1)
@@ -115,7 +114,7 @@ export default {
case 'past_3_months':
return addMonths(toDate, -3)
case 'this_year':
return new Date(toDate.getFullYear(), 0, 1)
return new Date(toDate.getFullYear(), 1, 1)
case 'all_time':
return new Date(1969, 4, 20)
default:
@@ -129,7 +128,7 @@ export default {
<template>
<div>
<div class="columns">
<div class="column" :class="isMobile ? 'has-text-centered' : ''"> <h1 class="title">{{ $t('statistics') }}</h1></div>
<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">

View File

@@ -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: calc((1em + $size-input-padding-vertical * 2) / 10);
$size-input-border-radius: (1em + $size-input-padding-vertical * 2) / 10;
// BUTTONS
$size-button-padding-vertical: calc($size-grid-padding / 2);
$size-button-padding-horizontal: calc($size-grid-padding / 1.5);
$size-button-padding-vertical: $size-grid-padding / 2;
$size-button-padding-horizontal: $size-grid-padding / 1.5;
$size-button-padding: $size-button-padding-vertical
$size-button-padding-horizontal;

View File

@@ -147,7 +147,7 @@
$max-screen,
$max-value
) {
$a: calc(($max-value - $min-value) / ($max-screen - $min-screen));
$a: ($max-value - $min-value) / ($max-screen - $min-screen);
$b: $min-value - $a * $min-screen;
$sign: '+';

View File

@@ -1,25 +0,0 @@
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;

View File

@@ -1,217 +0,0 @@
{
"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"
}

View File

@@ -1,231 +0,0 @@
{
"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"
}

View File

@@ -7,7 +7,6 @@ import {
faCheck,
faTimes,
faArrowUp,
faArrowRotateLeft,
faAngleLeft,
faAngleRight,
faCalendar,
@@ -25,7 +24,6 @@ import {
faTimesCircle,
} from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import i18n from './i18n';
import App from './app.vue'
@@ -35,11 +33,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,
@@ -55,9 +53,7 @@ library.add(
faShare,
faUserFriends,
faTimesCircle
);
Vue.component('VueFontawesome', FontAwesomeIcon)
)
Vue.use(Buefy, {
defaultIconComponent: 'vue-fontawesome',
defaultIconPack: 'fas',
@@ -77,7 +73,6 @@ const app = new Vue({
store,
render: (h) => h(App),
i18n,
}).$mount('#app')
// If running e2e tests...

View File

@@ -410,24 +410,6 @@ 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',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,172 +0,0 @@
<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>

View File

@@ -9,6 +9,24 @@ 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,
@@ -22,24 +40,6 @@ 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,17 +53,16 @@ export default {
.post(`/api/import/fuelly`, formData)
.then((data) => {
this.$buefy.toast.open({
message: this.$t('importsuccessfull'),
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: this.$t('importerror'),
message: 'There was some issue with importing the file. Please check the error message',
position: 'is-bottom',
type: 'is-danger',
})
@@ -83,33 +82,39 @@ export default {
<Layout>
<div class="columns box">
<div class="column">
<h1 class="title">{{ $t('importfrom', { 'name': 'Fuelly' }) }}</h1>
<h1 class="title">Import from Fuelly</h1>
</div>
</div>
<br />
<div class="columns">
<div class="column">
<p class="subtitle"> {{ $t('stepstoimport', { 'name': 'Fuelly' }) }}</p>
<p class="subtitle"> Steps to import data from Fuelly</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('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>
<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>
</ol>
</div>
</div>
<div class="section box">
<div class="columns">
<div class="column is-two-thirds"> <p class="subtitle">{{ $t('choosecsvimport', { 'name': 'Fuelly' }) }}</p></div>
<div class="column is-two-thirds"> <p class="subtitle">Choose the Fuelly CSV and press the import button.</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" required>
<b-upload v-model="file" class="file-label" accept=".csv">
<span class="file-cta">
<b-icon class="file-icon" icon="upload"></b-icon>
<span class="file-label">{{ uploadButtonLabel }}</span>
@@ -121,8 +126,8 @@ export default {
</b-field>
</div>
<div class="column">
<b-button tag="button" native-type="submit" :disabled="tryingToCreate" type="is-primary" class="control">
{{ $t('import') }}
<b-button tag="input" native-type="submit" :disabled="tryingToCreate" type="is-primary" value="Upload File" class="control">
Import
</b-button>
</div></div
>

View File

@@ -1,7 +0,0 @@
import ImportGeneric from './import-generic'
describe('@views/import-generic', () => {
it('is a valid view', () => {
expect(ImportGeneric).toBeAViewComponent()
})
})

View File

@@ -1,411 +0,0 @@
<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>

View File

@@ -18,41 +18,19 @@ export default {
<template>
<Layout>
<div class="columns box">
<div class="column">
<h1 class="title">{{ $t('importdata') }}</h1>
<p class="subtitle">{{ $t('importdatadesc') }}</p>
</div>
</div>
<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
>
<br />
<div class="columns">
<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 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>
</div>
</Layout>

View File

@@ -21,27 +21,13 @@ export default {
email: '',
password: '',
distanceUnit: 1,
currency: '',
currency: 'INR',
},
}
},
computed: {
...mapGetters('auth', ['isInitialized']),
...mapState('vehicles', ['currencyMasters', 'distanceUnitMasters']),
filteredCurrencyMasters() {
return this.currencyMasters.filter((option) => {
return (
option.namePlural
.toString()
.toLowerCase()
.indexOf(this.registerModel.currency.toLowerCase()) >= 0 ||
option.code
.toString()
.toLowerCase()
.indexOf(this.registerModel.currency.toLowerCase()) >= 0
)
})
},
},
mounted() {
store.dispatch('vehicles/fetchMasters').then((data) => {})
@@ -62,11 +48,11 @@ export default {
var message = ''
if (this.migrationMode === 'clarkson') {
message =
this.$t('init.clarkson.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'
}
if (this.migrationMode === 'fresh') {
message =
this.$t('init.fresh.success')
'You have been registered successfully. You will be redirected to the login screen shortly where you can login and start using the system.'
}
this.$buefy.toast.open({
duration: 10000,
@@ -153,9 +139,6 @@ export default {
})
.finally(() => (this.isWorking = false))
},
formatCurrency(option) {
return `${option.namePlural} (${option.code})`
},
},
}
</script>
@@ -163,76 +146,114 @@ export default {
<template>
<Layout>
<div v-if="!migrationMode" class="box">
<h1 class="title">{{ $t('init.migrateclarkson') }}</h1>
<h1 class="title">Migrate from Clarkson</h1>
<p>
{{ $t('init.migrateclarksondesc') }}
</p>
<br />
<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">{{ $t('init.freshinstall') }}</h1>
<p>
{{ $t('init.freshinstalldesc') }}
If you have an existing Clarkson deployment and you want to migrate your data from that,
press the following button.
</p>
<br />
<b-field>
<b-button type="is-primary" @click="migrationMode = 'fresh'">{{ $t('init.freshinstall') }}</b-button>
<b-button type="is-primary" @click="migrationMode = 'clarkson'"
>Migrate from Clarkson</b-button
></b-field
>
</div>
<div v-if="!migrationMode" class="box">
<h1 class="title">Fresh Install</h1>
<p>
If you want a fresh install of Hammond, press the following button.
</p>
<br />
<b-field>
<b-button type="is-primary" @click="migrationMode = 'fresh'">Fresh Install</b-button>
</b-field>
</div>
<div v-if="migrationMode === 'clarkson'" class="box content">
<h1 class="title">{{ $t('init.migrateclarkson') }}</h1>
<p v-html="$t('init.clarkson.desc')"></p>
<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 />
<b-notification v-if="connectionError" type="is-danger" role="alert" :closable="false">
{{ connectionError }}
</b-notification>
<b-field addons :label="$t('mysqlconnstr')">
<b-field addons label="Mysql Connection String">
<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">{{ $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>
<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>
</div>
</div>
<div v-if="migrationMode === 'fresh'" class="box content">
<h1 class="title">{{ $t('init.fresh.setupadminuser') }}</h1>
<h1 class="title">Setup Admin Users</h1>
<form @submit.prevent="register">
<b-field :label="$t('init.fresh.yourname')">
<b-field label="Your Name">
<b-input v-model="registerModel.name" required></b-input>
</b-field>
<b-field :label="$t('init.fresh.youremail')">
<b-field label="Your Email">
<b-input v-model="registerModel.email" type="email" required></b-input>
</b-field>
<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="$t('currency')">
<b-autocomplete
v-model="registerModel.currency"
:custom-formatter="formatCurrency"
:placeholder="$t('currency')"
:data="filteredCurrencyMasters"
:keep-first="true"
:open-on-focus="true"
<b-field label="Your Password">
<b-input
v-model="registerModel.password"
type="password"
required
@select="(option) => (selected = option)"
></b-autocomplete>
minlength="8"
password-reveal
></b-input>
</b-field>
<b-field :label="$t('distanceunit')">
<b-select v-model.number="registerModel.distanceUnit" :placeholder="$t('distanceunit')" required expanded>
<b-field label="Currency">
<b-select v-model="registerModel.currency" placeholder="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="registerModel.distanceUnit"
placeholder="Distance Unit"
required
expanded
>
<option v-for="(option, key) in distanceUnitMasters" :key="key" :value="key">
{{ `${$t('unit.long.' + option.key)} (${$t('unit.short.' + option.key)})` }}
{{ `${option.long} (${option.short})` }}
</option>
</b-select>
</b-field>
<br />
<div class="buttons">
<b-button type="is-primary" native-type="submit" tag="button" :value="$t('save')"></b-button>
<b-button type="is-primary" native-type="submit" tag="input"></b-button>
<b-button type="is-danger is-light" @click="resetMigrationMode">{{ $t('cancel') }}</b-button>
<b-button type="is-danger is-light" @click="resetMigrationMode">Cancel</b-button>
</div>
</form>
</div>

View File

@@ -16,7 +16,7 @@ export default {
password: '',
authError: null,
tryingToLogIn: false,
errorMessage: '',
errorMessage:''
}
},
computed: {
@@ -24,8 +24,8 @@ export default {
return process.env.NODE_ENV === 'production'
? {}
: {
username: this.$t('enterusername'),
password: this.$t('enterpassword'),
username: 'Enter your username',
password: 'Enter your password',
}
},
},
@@ -38,7 +38,7 @@ export default {
// and password they provided.
tryToLogIn() {
this.tryingToLogIn = true
this.errorMessage = ''
this.errorMessage='';
// Reset the authError if it existed.
this.authError = null
return this.logIn({
@@ -53,9 +53,9 @@ export default {
// Redirect to the originally requested page, or to the home page
})
.catch((error) => {
if (error.response.data?.errors?.login) {
this.errorMessage = error.response.data.errors.login
}
if(error.response.data?.errors?.login){
this.errorMessage=error.response.data.errors.login
}
this.tryingToLogIn = false
this.authError = error
})
@@ -67,17 +67,31 @@ export default {
<template>
<Layout>
<form @submit.prevent="tryToLogIn">
<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 label="Email">
<b-input
v-model="username"
tag="b-input"
name="username"
:placeholder="placeholders.username"
/></b-field>
<b-field label="Password">
<b-input
v-model="password"
tag="b-input"
name="password"
type="password"
:placeholder="placeholders.password"
/>
</b-field>
<b-button tag="button" native-type="submit" :disabled="tryingToLogIn" type="is-primary">
<b-button tag="input" native-type="submit" :disabled="tryingToLogIn" type="is-primary">
<BaseIcon v-if="tryingToLogIn" name="sync" spin />
<span v-else>
{{ $t('login') }}
Log in
</span>
</b-button>
<p v-if="authError"> {{ $t('loginerror', { msg: errorMessage }) }}</p>
<p v-if="authError">
There was an error logging in to your account. {{errorMessage}}
</p>
</form>
</Layout>
</template>

View File

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

View File

@@ -41,7 +41,7 @@ export default {
store.dispatch('vehicles/setQuickEntryAsProcessed', { id: entry.id }).then((data) => {})
},
deleteQuickEntry(entry) {
var sure = confirm(this.$t('deletequickentry'))
var sure = confirm('This will delete this Quick Entry. This step cannot be reversed. Are you sure?')
if (sure) {
store.dispatch('vehicles/deleteQuickEntry', { id: entry.id }).then((data) => {})
}
@@ -59,9 +59,9 @@ export default {
<template>
<Layout>
<h1 class="title">{{ $tc('quickentry', 2) }}</h1>
<h1 class="title">Quick Entries</h1>
<b-field>
<b-switch v-if="unprocessedQuickEntries.length" v-model="showUnprocessedOnly">{{ $t('showunprocessed') }}</b-switch>
<b-switch v-if="unprocessedQuickEntries.length" v-model="showUnprocessedOnly">Show unprocessed only</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">{{ $t('unprocessed') }}</b-tag>
<b-tag v-if="entry.processDate === null" class="is-align-content-center" type="is-primary">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"
>{{ $t('addfillup') }}</router-link
>Create Fillup</router-link
>
<router-link v-if="entry.processDate === null && vehicles.length" :to="`/vehicles/${vehicles[0].id}/expense`" class="card-footer-item"
>{{ $t('addexpense') }}</router-link
>Create Expense</router-link
>
<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>
<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>
</footer>
</div>
</div>
</div>
</div>
<div v-if="!quickEntries.length" class="box">
<p>{{ $tc('quickentry',0) }}</p>
<p>No Quick Entries right now.</p>
</div>
</Layout>
</template>

View File

@@ -44,20 +44,6 @@ export default {
return this.changePassModel.new === this.changePassModel.renew
},
filteredCurrencyMasters() {
return this.currencyMasters.filter((option) => {
return (
option.namePlural
.toString()
.toLowerCase()
.indexOf(this.settingsModel.currency.toLowerCase()) >= 0 ||
option.code
.toString()
.toLowerCase()
.indexOf(this.settingsModel.currency.toLowerCase()) >= 0
)
})
},
},
methods: {
changePassword() {
@@ -106,7 +92,7 @@ export default {
.dispatch(`utils/saveUserSettings`, { settings: this.settingsModel })
.then((data) => {
this.$buefy.toast.open({
message: this.$t('settingssaved'),
message: 'Settings saved successfully',
type: 'is-success',
duration: 3000,
})
@@ -123,42 +109,34 @@ export default {
this.tryingToSave = false
})
},
formatCurrency(option) {
return `${option.namePlural} (${option.code})`
},
},
}
</script>
<template>
<Layout>
<h1 class="title">{{ $t('yoursettings') }}</h1>
<h1 class="title">Your Settings</h1>
<div class="columns"
><div class="column">
<form class="box " @submit.prevent="saveSettings">
<h1 class="subtitle">
{{ $t('settingdesc') }}
These will be used as default values whenever you create a new fillup or expense.
</h1>
<b-field :label="$t('currency')">
<b-autocomplete
v-model="settingsModel.currency"
:custom-formatter="formatCurrency"
:placeholder="$t('currency')"
:data="filteredCurrencyMasters"
:keep-first="true"
:open-on-focus="true"
required
@select="(option) => (selected = option)"
></b-autocomplete>
</b-field>
<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">
{{ `${$t('unit.long.' + option.key)} (${$t('unit.short.' + option.key)})` }}
<b-field label="Currency">
<b-select v-model="settingsModel.currency" placeholder="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="$t('dateformat')">
<b-field label="Distance Unit">
<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})` }}
</option>
</b-select>
</b-field>
<b-field label="Date Format">
<b-select v-model.number="settingsModel.dateFormat" placeholder="Date Format" required expanded>
<option v-for="option in dateFormatMasters" :key="option" :value="option">
{{ `${option}` }}
@@ -167,27 +145,25 @@ export default {
</b-field>
<br />
<b-field>
<b-button tag="button" native-type="submit" :disabled="tryingToSave" type="is-primary" expanded> {{ $t('save') }} </b-button>
<b-button tag="input" native-type="submit" :disabled="tryingToSave" type="is-primary" value="Save" expanded> </b-button>
</b-field>
</form>
</div>
<div class="column">
<form class="box" @submit.prevent="changePassword">
<h1 class="subtitle">{{ $t('changepassword') }}</h1>
<b-field :label="$t('oldpassword')">
<h1 class="subtitle">Change password</h1>
<b-field label="Old Password">
<b-input v-model="changePassModel.old" required minlength="6" password-reveal type="password"></b-input>
</b-field>
<b-field :label="$t('newpassword')">
<b-field label="New Password">
<b-input v-model="changePassModel.new" required minlength="6" password-reveal type="password"></b-input>
</b-field>
<b-field :label="$t('repeatnewpassword')">
<b-field label="Repeat New Password">
<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">{{ $t('passworddontmatch') }}</p>
<p v-if="!passwordValid" class="help is-danger">Password values don't match</p>
<b-field>
<b-button tag="button" native-type="submit" :disabled="!passwordValid" type="is-primary" expanded>
{{ $t('changepassword') }}
</b-button>
<b-button tag="input" native-type="submit" :disabled="!passwordValid" type="is-primary" value="Change Password" expanded> </b-button>
</b-field>
</form>
</div>
@@ -195,32 +171,48 @@ export default {
<hr />
<div class="columns">
<div class="twelve">
<h3 class="title">{{ $t('moreinfo') }}</h3>
<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 />
<table class="table is-hoverable">
<tr>
<td>{{ $t('currentversion') }}</td>
<td>2022.07.06</td>
<td>Current Version</td>
<td>2021.09.20</td>
</tr>
<tr>
<td>Website</td>
<td><a href="https://github.com/alfhou/hammond" target="_blank">https://github.com/alfhou/hammond</a></td>
<td><a href="https://github.com/akhilrex/hammond" target="_blank">https://github.com/akhilrex/hammond</a></td>
</tr>
<tr>
<td>{{ $t('foundabug') }}</td>
<td>Found a bug</td>
<td
><a href="https://github.com/alfhou/hammond/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc" target="_blank" rel="noopener noreferrer"
><a
href="https://github.com/akhilrex/hammond/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc"
target="_blank"
rel="noopener noreferrer"
>Report here</a
></td
>
</tr>
<tr>
<td>{{ $t('featurerequest') }}</td>
<td>Feature Request</td>
<td
><a href="https://github.com/alfhou/hammond/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc" target="_blank" rel="noopener noreferrer"
><a
href="https://github.com/akhilrex/hammond/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc"
target="_blank"
rel="noopener noreferrer"
>Request here</a
></td
>
</tr>
<tr>
<td>Support the developer</td>
<td><a href="https://www.buymeacoffee.com/akhilrex" target="_blank" rel="noopener noreferrer">Buy him a beer!</a></td>
</tr>
</table>
</div>
</div>

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import { parseAndFormatDate } from '@utils/format-date'
import { mapState } from 'vuex'
import { addDays, addMonths } from 'date-fns'
import axios from 'axios'
import currencyFormatter from 'currency-formatter'
import currencyFormtter from 'currency-formatter'
import store from '@state/store'
import ShareVehicle from '@components/shareVehicle.vue'
import MileageChart from '@components/mileageChart.vue'
@@ -40,20 +40,14 @@ export default {
stats: null,
users: [],
dateRangeOptions: [
{ 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' },
{ 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' },
],
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: {
@@ -67,35 +61,32 @@ export default {
return this.stats.map((x) => {
return [
{
label: this.$t('currency'),
label: 'Currency',
value: x.currency,
},
{
label: this.$t('totalexpenses'),
label: 'Total Expenditure',
value: this.formatCurrency(x.expenditureTotal, x.currency),
},
{
label: this.$t('fillupcost'),
label: 'Fillup Costs',
value: `${this.formatCurrency(x.expenditureFillups, x.currency)} (${x.countFillups})`,
},
{
label: this.$t('otherexpenses'),
label: 'Other Expenses',
value: `${this.formatCurrency(x.expenditureExpenses, x.currency)} (${x.countExpenses})`,
},
{
label: this.$t('avgfillupexpense'),
label: 'Avg Fillup Expense',
value: `${this.formatCurrency(x.avgFillupCost, x.currency)}`,
},
{
label: this.$t('avgfillupqty'),
value: `${x.avgFuelQty} ${this.$t('unit.short.' + this.vehicle.fuelUnitDetail.key)}`,
label: 'Avg Fillup Qty',
value: `${x.avgFuelQty} ${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),
}),
label: 'Avg Fuel Cost',
value: `${this.formatCurrency(x.avgFuelPrice, x.currency)} per ${this.vehicle.fuelUnitDetail.short}`,
},
]
})
@@ -208,21 +199,14 @@ export default {
return
}
this.tryingToUpload = true
const formData = new FormData()
formData.append('file', this.file, this.file.name)
formData.append('title', this.title)
// const config = { headers: { 'Content-Type': 'multipart/form-data; boundary=' + formData._boundary } }
fetch(`/api/vehicles/${this.vehicle.id}/attachments`, {
method: 'POST',
body: formData,
headers: {
Authorization: this.currentUser.token,
},
})
axios
.post(`/api/vehicles/${this.vehicle.id}/attachments`, formData)
.then((data) => {
this.$buefy.toast.open({
message: 'File uploaded Successfully',
message: 'Quick Entry Created Successfully',
type: 'is-success',
duration: 3000,
})
@@ -249,7 +233,7 @@ export default {
if (!currencyCode) {
currencyCode = this.me.currency
}
return currencyFormatter.format(number, { code: currencyCode })
return currencyFormtter.format(number, { code: currencyCode })
},
columnTdAttrs(row, column) {
return null
@@ -309,18 +293,18 @@ export default {
<template>
<Layout>
<div class="columns box">
<div class="column is-one-half" :class="isMobile ? 'has-text-centered' : ''">
<div class="column is-two-thirds" :class="isMobile ? 'has-text-centered' : ''">
<p class="title">{{ vehicle.nickname }} - {{ vehicle.registration }}</p>
<p class="subtitle">
{{ [vehicle.make, vehicle.model, $t('fuel.' + vehicle.fuelTypeDetail.key)].join(' | ') }}
{{ [vehicle.make, vehicle.model, vehicle.fuelTypeDetail.long].join(' | ') }}
<template v-if="users.length > 1">
| {{ $t('sharedwith') }} :
| Shared with :
{{
users
.map((x) => {
if (x.userId === me.id) {
return $t('you')
return 'You'
} else {
return x.name
}
@@ -330,24 +314,25 @@ export default {
</template>
</p>
</div>
<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>
<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>
<b-button
v-if="vehicle.isOwner"
tag="router-link"
:title="$t('editvehicle')"
title="Edit Vehicle"
: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="$t('sharevehicle')" @click="showShareVehicleModal">
>
<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="user-friends" type="is-info"> </b-icon
></b-button>
<b-button v-if="vehicle.isOwner" :title="$t('deletevehicle')" @click="deleteVehicle">
<b-button v-if="vehicle.isOwner" title="Delete Vehicle" @click="deleteVehicle">
<b-icon pack="fas" icon="trash" type="is-danger"> </b-icon
></b-button>
</div>
@@ -361,45 +346,45 @@ export default {
</div>
</div>
<div class="box">
<h1 class="title is-4">{{ $t('pastfillups') }}</h1>
<h1 class="title is-4">Past Fillups</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="$t('date')" :td-attrs="columnTdAttrs" sortable date>
<b-table-column v-slot="props" field="date" label="Date" :td-attrs="columnTdAttrs" sortable date>
{{ formatDate(props.row.date) }}
</b-table-column>
<b-table-column v-slot="props" field="fuelSubType" :label="$t('fuelsubtype')" :td-attrs="columnTdAttrs">
<b-table-column v-slot="props" field="fuelSubType" label="Fuel Sub Type" :td-attrs="columnTdAttrs">
{{ props.row.fuelSubType }}
</b-table-column>
<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 v-slot="props" field="fuelQuantity" label="Qty." :td-attrs="hiddenMobile" numeric>
{{ `${props.row.fuelQuantity} ${props.row.fuelUnitDetail.short}` }}
</b-table-column>
<b-table-column
v-slot="props"
field="perUnitPrice"
:label="$t('per', { '0': $t('price'), '1': $t('unit.short.' + vehicle.fuelUnitDetail.key) })"
:label="'Price per ' + vehicle.fuelUnitDetail.short"
: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="$t('total')" :td-attrs="hiddenDesktop" sortable numeric>
{{ `${me.currency} ${props.row.totalAmount}` }} ({{ `${props.row.fuelQuantity} ${$t('unit.short.' + props.row.fuelUnitDetail.key)}` }} @
<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}` }} @
{{ `${me.currency} ${props.row.perUnitPrice}` }})
</b-table-column>
<b-table-column v-if="!isMobile" v-slot="props" field="totalAmount" :label="$t('total')" :td-attrs="hiddenMobile" sortable numeric>
<b-table-column v-if="!isMobile" v-slot="props" field="totalAmount" label="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="$t('fulltank')" :td-attrs="hiddenMobile">
<b-table-column v-slot="props" width="20" field="isTankFull" label="Tank Full" :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="$t('odometer')" :td-attrs="hiddenMobile" numeric>
{{ `${props.row.odoReading} ${$t('unit.short.' + me.distanceUnitDetail.key)}` }}
<b-table-column v-slot="props" field="odoReading" label="Odometer Reading" :td-attrs="hiddenMobile" numeric>
{{ `${props.row.odoReading} ${me.distanceUnitDetail.short}` }}
</b-table-column>
<b-table-column v-slot="props" field="fillingStation" :label="$t('gasstation')" :td-attrs="hiddenMobile">
<b-table-column v-slot="props" field="fillingStation" label="Fillup Station" :td-attrs="hiddenMobile">
{{ `${props.row.fillingStation}` }}
</b-table-column>
<b-table-column v-slot="props" field="userId" :label="$t('by')" :td-attrs="hiddenMobile">
<b-table-column v-slot="props" field="userId" label="By" :td-attrs="hiddenMobile">
{{ `${props.row.user.name}` }}
</b-table-column>
<b-table-column v-slot="props">
@@ -414,14 +399,11 @@ export default {
>
<b-icon pack="fas" icon="edit" type="is-info"> </b-icon
></b-button>
<b-button
type="is-ghost"
:title="$t('deletefillup')"
@click="deleteFillup(props.row.id)">
<b-button type="is-ghost" title="Delete this fillup" @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> {{ $t('nofillups') }}</template>
<template v-slot:empty> No Fillups so far</template>
<template v-slot:detail="props">
<p>{{ props.row.id }}</p>
</template>
@@ -429,25 +411,25 @@ export default {
</div>
<br />
<div class="box">
<h1 class="title is-4">{{ $t('expenses') }}</h1>
<h1 class="title is-4">Past Expenses</h1>
<b-table :data="expenses" hoverable mobile-cards paginated per-page="10">
<b-table-column v-slot="props" field="date" :label="$t('date')" :td-attrs="columnTdAttrs" date>
<b-table-column v-slot="props" field="date" label="Date" :td-attrs="columnTdAttrs" date>
{{ formatDate(props.row.date) }}
</b-table-column>
<b-table-column v-slot="props" field="expenseType" :label="$t('expensetype')">
<b-table-column v-slot="props" field="expenseType" label="Expense Type">
{{ `${props.row.expenseType}` }}
</b-table-column>
<b-table-column v-slot="props" field="amount" :label="$t('total')" :td-attrs="hiddenMobile" sortable numeric>
<b-table-column v-slot="props" field="amount" label="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="$t('odometer')" :td-attrs="columnTdAttrs" numeric>
{{ `${props.row.odoReading} ${$t('unit.short.' + me.distanceUnitDetail.key)}` }}
<b-table-column v-slot="props" field="odoReading" label="Odometer Reading" :td-attrs="columnTdAttrs" numeric>
{{ `${props.row.odoReading} ${me.distanceUnitDetail.short}` }}
</b-table-column>
<b-table-column v-slot="props" field="userId" :label="$t('by')" :td-attrs="columnTdAttrs">
<b-table-column v-slot="props" field="userId" label="By" :td-attrs="columnTdAttrs">
{{ `${props.row.user.name}` }}
</b-table-column>
<b-table-column v-slot="props">
@@ -462,22 +444,20 @@ export default {
>
<b-icon pack="fas" icon="edit" type="is-info"> </b-icon
></b-button>
<b-button type="is-ghost" :title="$t('deleteexpense')" @click="deleteExpense(props.row.id)">
<b-button type="is-ghost" title="Delete this expense" @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> {{ $t('noexpenses') }}</template>
<template v-slot:empty> No Expenses so far</template>
</b-table>
</div>
<br />
<div class="box">
<div class="columns">
<div class="column is-three-quarters">
<h1 class="title is-4">{{ $t('attachments') }}</h1></div
>
<div class="column is-three-quarters"> <h1 class="title is-4">Attachments</h1></div>
<div class="column buttons">
<b-button type="is-primary" @click="showAttachmentForm = true">
{{ $t('addattachment') }}
Add Attachment
</b-button>
</div>
</div>
@@ -491,7 +471,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">{{ $t('choosefile') }}</span>
<span class="file-label">Choose File</span>
</span>
<span v-if="file" class="file-name" :class="isMobile ? 'file-name-mobile' : 'file-name-desktop'">
{{ file.name }}
@@ -499,18 +479,18 @@ export default {
</b-upload>
</b-field>
<b-field>
<b-input v-model="title" required :placeholder="$t('labelforfile')"></b-input>
<b-input v-model="title" required placeholder="Label for this file"></b-input>
</b-field>
<b-field class="buttons">
<b-button tag="button" native-type="submit" :disabled="tryingToUpload" type="is-primary" label="Upload File" value="Upload File">
<b-button tag="input" native-type="submit" :disabled="tryingToUpload" type="is-primary" label="Upload File" value="Upload File">
</b-button>
<b-button
tag="button"
tag="input"
native-type="submit"
:disabled="tryingToUpload"
type="is-danger"
label="Cancel"
label="Upload File"
value="Cancel"
@click="showAttachmentForm = false"
>
@@ -523,46 +503,33 @@ export default {
</div>
<b-table :data="attachments" hoverable mobile-cards>
<b-table-column v-slot="props" field="title" :label="$t('title')" :td-attrs="columnTdAttrs">
<b-table-column v-slot="props" field="title" label="Title" :td-attrs="columnTdAttrs">
{{ `${props.row.title}` }}
</b-table-column>
<b-table-column v-slot="props" field="originalName" :label="$t('name')" :td-attrs="columnTdAttrs">
<b-table-column v-slot="props" field="originalName" label="Name" :td-attrs="columnTdAttrs">
{{ `${props.row.originalName}` }}
</b-table-column>
<b-table-column v-slot="props" field="id" :label="$t('download')" :td-attrs="columnTdAttrs">
<b-table-column v-slot="props" field="id" label="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> {{ $t('noattachments') }}</template>
<template v-slot:empty> No Attachments so far</template>
</b-table>
</div>
<div class="box">
<div class="columns">
<div class="column" :class="isMobile ? 'has-text-centered' : ''">
<h1 class="title">{{ $t('statistics') }}</h1></div
>
<div class="column" :class="isMobile ? 'has-text-centered' : ''"> <h1 class="title">Stats</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>
<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>
<MileageChart :vehicle="vehicle" :since="getStartDate()" :user="me" :height="300" :mileage-option="mileageOption" />
<MileageChart :vehicle="vehicle" :since="getStartDate()" :user="me" :height="300" />
</div>
</Layout>
</template>

14624
ui/yarn.lock Normal file

File diff suppressed because it is too large Load Diff