Compare commits
58 Commits
version-bu
...
v0.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e894844a3 | ||
|
|
4a55879ad8 | ||
|
|
9dab3d124d | ||
|
|
a89ca5e46a | ||
|
|
f96638d913 | ||
|
|
08f2a3547e | ||
|
|
126aff7231 | ||
|
|
ba276975f3 | ||
|
|
7d4b763e48 | ||
|
|
ee964a630e | ||
|
|
c588e34b2e | ||
|
|
6871a40380 | ||
|
|
0035897f21 | ||
|
|
19680b1cc1 | ||
|
|
e6e90d9bef | ||
|
|
311ac7579a | ||
|
|
47810a8c88 | ||
|
|
f9d24bc7ef | ||
|
|
5aabeda6ba | ||
|
|
bb68c8c504 | ||
|
|
961ec30065 | ||
|
|
0b450dc462 | ||
|
|
d3ce6920ad | ||
|
|
afdfa31148 | ||
|
|
2d24c4b9e6 | ||
|
|
84cba2c7f2 | ||
|
|
1432499a90 | ||
|
|
d0704c8c6a | ||
|
|
b86795bcb6 | ||
|
|
1ccdce9ee3 | ||
|
|
5cfaf8c933 | ||
|
|
987f035198 | ||
|
|
ab94997dd6 | ||
|
|
0b715ef840 | ||
|
|
c00c6bc776 | ||
|
|
a5d4dface8 | ||
|
|
7cb9a43dfe | ||
|
|
05bb22fe4e | ||
|
|
69352af906 | ||
|
|
7a8916c9cd | ||
|
|
e471e80617 | ||
|
|
1ee032b664 | ||
|
|
cea2566e2a | ||
|
|
dcb58bbbdb | ||
|
|
24105dbaaf | ||
|
|
e3846634b5 | ||
|
|
fd52c23636 | ||
|
|
43d1ca0c66 | ||
|
|
fb742f19a7 | ||
|
|
20a1421576 | ||
|
|
8410674841 | ||
|
|
74e52c3e87 | ||
|
|
1857bb0518 | ||
|
|
a729b5eb12 | ||
|
|
d9a99d432c | ||
|
|
acba47fede | ||
|
|
04f45fe385 | ||
|
|
fca2c3e7fa |
55
.github/workflows/hub.yml
vendored
55
.github/workflows/hub.yml
vendored
@@ -1,45 +1,46 @@
|
||||
name: ci
|
||||
name: Build docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: master
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
multi:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Set up QEMU
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v1
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
- name: Available platforms
|
||||
run: echo ${{ steps.qemu.outputs.platforms }}
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
-
|
||||
name: Set up build cache
|
||||
- name: Set up build cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
-
|
||||
name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
- name: Parse the git tag
|
||||
id: get_tag
|
||||
run: echo ::set-output name=TAG::$(echo $GITHUB_REF | cut -d / -f 3)
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Login to GitHub
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
- name: Login to GitHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.CR_PAT }}
|
||||
-
|
||||
name: Build and push
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
@@ -48,10 +49,10 @@ jobs:
|
||||
#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: |
|
||||
akhilrex/hammond:latest
|
||||
akhilrex/hammond:1.0.0
|
||||
ghcr.io/akhilrex/hammond:latest
|
||||
ghcr.io/akhilrex/hammond:1.0.0
|
||||
alfhou/hammond:latest
|
||||
alfhou/hammond:${{ steps.get_tag.outputs.TAG }}
|
||||
ghcr.io/alfhou/hammond:latest
|
||||
ghcr.io/alfhou/hammond:${{ steps.get_tag.outputs.TAG }}
|
||||
|
||||
16
.github/workflows/test-go.yml
vendored
Normal file
16
.github/workflows/test-go.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
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
|
||||
@@ -9,7 +9,7 @@ RUN go mod download
|
||||
COPY ./server .
|
||||
RUN go build -o ./app ./main.go
|
||||
|
||||
FROM node:latest as build-stage
|
||||
FROM node:14 as build-stage
|
||||
WORKDIR /app
|
||||
COPY ./ui/package*.json ./
|
||||
RUN npm install
|
||||
@@ -18,7 +18,7 @@ RUN npm run build
|
||||
|
||||
|
||||
FROM alpine:latest
|
||||
LABEL org.opencontainers.image.source="https://github.com/akhilrex/hammond"
|
||||
LABEL org.opencontainers.image.source="https://github.com/alfhou/hammond"
|
||||
ENV CONFIG=/config
|
||||
ENV DATA=/assets
|
||||
ENV UID=998
|
||||
@@ -36,4 +36,4 @@ COPY --from=builder /api/app .
|
||||
#COPY dist ./dist
|
||||
COPY --from=build-stage /app/dist ./dist
|
||||
EXPOSE 3000
|
||||
ENTRYPOINT ["./app"]
|
||||
ENTRYPOINT ["./app"]
|
||||
|
||||
99
README.md
99
README.md
@@ -1,26 +1,16 @@
|
||||
[![Contributors][contributors-shield]][contributors-url] [![Forks][forks-shield]][forks-url] [![Stargazers][stars-shield]][stars-url] [![Issues][issues-shield]][issues-url] [![MIT License][license-shield]][license-url] [![LinkedIn][linkedin-shield]][linkedin-url]
|
||||
|
||||
<!-- PROJECT LOGO -->
|
||||
<br />
|
||||
<p align="center">
|
||||
<!-- <a href="https://github.com/akhilrex/hammond">
|
||||
<img src="images/logo.png" alt="Logo" width="80" height="80">
|
||||
</a> -->
|
||||
|
||||
<h1 align="center" style="margin-bottom:0">Hammond</h1>
|
||||
<p align="center">Current Version - 2021.09.20</p>
|
||||
<p align="center">Current Version - 2022.07.06</p>
|
||||
|
||||
<p align="center">
|
||||
A self-hosted vehicle expense tracking system with support for multiple users.
|
||||
<br />
|
||||
<a href="https://github.com/akhilrex/hammond"><strong>Explore the docs »</strong></a>
|
||||
<a href="https://github.com/AlfHou/hammond"><strong>Explore the docs »</strong></a>
|
||||
<br />
|
||||
<br />
|
||||
<!-- <a href="https://github.com/akhilrex/hammond">View Demo</a>
|
||||
· -->
|
||||
<a href="https://github.com/akhilrex/hammond/issues">Report Bug</a>
|
||||
<a href="https://github.com/AlfHou/hammond/issues">Report Bug</a>
|
||||
·
|
||||
<a href="https://github.com/akhilrex/hammond/issues">Request Feature</a>
|
||||
<a href="https://github.com/AlfHou/hammond/issues">Request Feature</a>
|
||||
·
|
||||
<a href="Screenshots.md">Screenshots</a>
|
||||
</p>
|
||||
@@ -35,6 +25,7 @@
|
||||
- [Built With](#built-with)
|
||||
- [Features](#features)
|
||||
- [Installation](#installation)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
- [Roadmap](#roadmap)
|
||||
- [Contact](#contact)
|
||||
@@ -43,18 +34,22 @@
|
||||
|
||||
## About The Project
|
||||
|
||||
Hammond is a self hosted vehicle management system to track fuel and other expenses related to all of your vehicles. It supports multiple users sharing multiple vehicles. It is the logical successor to Clarkson which has not been updated for quite some time now.
|
||||
Hammond is a self hosted vehicle management system to track fuel and other
|
||||
expenses related to all of your vehicles.
|
||||
It supports multiple users sharing multiple vehicles.
|
||||
It is the logical successor to Clarkson which has not been updated for quite some time now.
|
||||
This repo is again a fork of akhilrex's great [project](https://github.com/akhilrex/hammond).
|
||||
|
||||
_Developers Note: This project is under active development which means I release new updates very frequently. It is recommended that you use something like [watchtower](https://github.com/containrrr/watchtower) which will automatically update your containers whenever I release a new version or periodically rebuild the container with the latest image manually._
|
||||
|
||||
__Also check out my other self-hosted, open-source solution - [Podgrab](https://github.com/akhilrex/podgrab) - Podcast download and archive manager and player.__
|
||||
### Motivation and Developer Notes
|
||||
|
||||
I was looking for a fuel tracking system and stumbled upon Clarkson. Although it did most of what I needed it has not been updated for quite a lot of time. Since I had some bandwidth available as my previous open source project [Podgrab](http://github.com/akhilrex/podgrab) had become quite stable now, my first thought was to contribute to the Clarkson project only. I soon realized that the architecture that Clarkson had used was not really be that extensible now and would warrant a complete rewrite only. So I decided to build Hammond - The successor to Clarkson.
|
||||
As mentioned, this project is a fork of
|
||||
akhilrex's [project](https://github.com/akhilrex/hammond) which is no longer active.
|
||||
To prevent the same from happeing to this project, we are seeking to add more
|
||||
maintainers/collaborators who have access to merge PRs.
|
||||
|
||||
The current version of Hammond is written using GO for backend and Vuejs for the front end. Originally I had thought of using the same tech stack for both frontend and the backend so that it became easier for users and other developers to use, deploy and contribute. Which is why the first version of Hammond has a NestJS backend complete with all the bells and whistles (GraphQL, Prisma and what nots). But I eventually decided to rebuild the backend in GO just to keep the container size small. No matter how much you can optimize the sheer size of the node_modules will always add bulk to your containers. I host all my tools on my Raspberry Pi. It only makes sense to keep the container size as small as possible.
|
||||
We are trying our best to update with new features and feedback is very welcome.
|
||||
|
||||
Also I had initially thought of a 2 container approach (1 for backend and 1 for the frontend) so that they can be independently maintained and updated. I eventually decided against this idea for the sake of simplicity. Although it is safe to assume that most self-hosters are fairly tech capable it still is much better to have a single container that you can fire and forget.
|
||||
The project is written using Go for the backend and Vuejs for the front end.
|
||||
|
||||
![Product Name Screen Shot][product-screenshot] [More Screenshots](Screenshots.md)
|
||||
|
||||
@@ -78,7 +73,7 @@ Also I had initially thought of a 2 container approach (1 for backend and 1 for
|
||||
- Save attachment against vehicles
|
||||
- Quick Entries (take a photo of a receipt or pump screen to make entry later)
|
||||
- Vehicle level and overall reporting
|
||||
- Import from Fuelly (more apps coming soon)
|
||||
- Import from Fuelly and Drivvo
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -89,24 +84,25 @@ The easiest way to run Hammond is to run it as a docker container.
|
||||
Simple setup without mounted volumes (for testing and evaluation)
|
||||
|
||||
```sh
|
||||
docker run -d -p 3000:3000 --name=hammond akhilrex/hammond
|
||||
docker run -d -p 3000:3000 --name=hammond alfhou/hammond
|
||||
```
|
||||
|
||||
Binding local volumes to the container
|
||||
|
||||
```sh
|
||||
docker run -d -p 3000:3000 --name=hammond -v "/host/path/to/assets:/assets" -v "/host/path/to/config:/config" akhilrex/hammond
|
||||
docker run -d -p 3000:3000 --name=hammond -v "/host/path/to/assets:/assets" -v "/host/path/to/config:/config" alfhou/hammond
|
||||
```
|
||||
|
||||
### Using Docker-Compose
|
||||
|
||||
Modify the docker compose file provided [here](https://github.com/akhilrex/hammond/blob/master/docker-compose.yml) to update the volume and port binding and run the following command
|
||||
Modify the docker compose file provided [here](https://github.com/alfhou/hammond/blob/master/docker-compose.yml)
|
||||
to update the volume and port binding and run the following command
|
||||
|
||||
```yaml
|
||||
version: '2.1'
|
||||
services:
|
||||
hammond:
|
||||
image: akhilrex/hammond
|
||||
image: alfhou/hammond
|
||||
container_name: hammond
|
||||
volumes:
|
||||
- /path/to/config:/config
|
||||
@@ -122,7 +118,10 @@ services:
|
||||
|
||||
### 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)
|
||||
|
||||
@@ -157,6 +156,31 @@ 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.
|
||||
@@ -171,25 +195,6 @@ Distributed under the GPL-3.0 License. See `LICENSE` for more information.
|
||||
|
||||
## Contact
|
||||
|
||||
Akhil Gupta - [@akhilrex](https://twitter.com/akhilrex)
|
||||
Project Link: [https://github.com/AlfHou/hammond](https://github.com/AlfHou/hammond)
|
||||
|
||||
Project Link: [https://github.com/akhilrex/hammond](https://github.com/akhilrex/hammond)
|
||||
|
||||
<a href="https://www.buymeacoffee.com/akhilrex" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="width: 217px !important;height: 60px !important;" ></a>
|
||||
|
||||
<!-- MARKDOWN LINKS & IMAGES -->
|
||||
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
|
||||
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/akhilrex/hammond.svg?style=flat-square
|
||||
[contributors-url]: https://github.com/akhilrex/hammond/graphs/contributors
|
||||
[forks-shield]: https://img.shields.io/github/forks/akhilrex/hammond.svg?style=flat-square
|
||||
[forks-url]: https://github.com/akhilrex/hammond/network/members
|
||||
[stars-shield]: https://img.shields.io/github/stars/akhilrex/hammond.svg?style=flat-square
|
||||
[stars-url]: https://github.com/akhilrex/hammond/stargazers
|
||||
[issues-shield]: https://img.shields.io/github/issues/akhilrex/hammond.svg?style=flat-square
|
||||
[issues-url]: https://github.com/akhilrex/hammond/issues
|
||||
[license-shield]: https://img.shields.io/github/license/akhilrex/hammond.svg?style=flat-square
|
||||
[license-url]: https://github.com/akhilrex/hammond/blob/master/LICENSE
|
||||
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=flat-square&logo=linkedin&colorB=555
|
||||
[linkedin-url]: https://linkedin.com/in/akhilrex
|
||||
[product-screenshot]: images/screenshot.jpg
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
version: "2.1"
|
||||
services:
|
||||
hammond:
|
||||
image: akhilrex/hammond
|
||||
image: alfhou/hammond
|
||||
container_name: hammond
|
||||
environment:
|
||||
- JWT_SECRET = somethingverystrong
|
||||
- JWT_SECRET=somethingverystrong
|
||||
volumes:
|
||||
- /path/to/config:/config
|
||||
- /path/to/data:/assets
|
||||
|
||||
@@ -26,7 +26,7 @@ Following steps will only work if Go and Node are installed and configured prope
|
||||
## Clone from Git
|
||||
|
||||
``` bash
|
||||
git clone --depth 1 https://github.com/akhilrex/hammond
|
||||
git clone --depth 1 https://github.com/alfhou/hammond
|
||||
```
|
||||
|
||||
## Build and Copy dependencies
|
||||
@@ -110,7 +110,7 @@ sudo systemctl stop hammond.service
|
||||
## Clone from Git
|
||||
|
||||
``` bash
|
||||
git clone --depth 1 https://github.com/akhilrex/hammond
|
||||
git clone --depth 1 https://github.com/alfhou/hammond
|
||||
```
|
||||
|
||||
## Build and Copy dependencies
|
||||
|
||||
4
server/.gitignore
vendored
4
server/.gitignore
vendored
@@ -12,6 +12,10 @@
|
||||
*.out
|
||||
*.db
|
||||
|
||||
# MS VSCode
|
||||
.vscode
|
||||
__debug_bin
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
assets/*
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN go build -o ./app ./main.go
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/akhilrex/hammond"
|
||||
LABEL org.opencontainers.image.source="https://github.com/alfhou/hammond"
|
||||
|
||||
ENV CONFIG=/config
|
||||
ENV DATA=/assets
|
||||
@@ -38,4 +38,4 @@ COPY dist ./dist
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENTRYPOINT ["./app"]
|
||||
ENTRYPOINT ["./app"]
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/akhilrex/hammond/common"
|
||||
"github.com/akhilrex/hammond/db"
|
||||
@@ -91,20 +92,20 @@ func userLogin(c *gin.Context) {
|
||||
c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
|
||||
return
|
||||
}
|
||||
user, err := db.FindOneUser(&db.User{Email: loginRequest.Email})
|
||||
user, err := db.FindOneUser(&db.User{Email: strings.ToLower(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)
|
||||
@@ -114,7 +115,7 @@ func userLogin(c *gin.Context) {
|
||||
Email: user.Email,
|
||||
Token: token,
|
||||
RefreshToken: refreshToken,
|
||||
Role: user.RoleDetail().Long,
|
||||
Role: user.RoleDetail().Key,
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
@@ -148,7 +149,7 @@ func refresh(c *gin.Context) {
|
||||
Email: user.Email,
|
||||
Token: token,
|
||||
RefreshToken: refreshToken,
|
||||
Role: user.RoleDetail().Long,
|
||||
Role: user.RoleDetail().Key,
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
} else {
|
||||
@@ -170,16 +171,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, err := service.UpdatePassword(user.ID, request.NewPassword)
|
||||
success, _ := service.UpdatePassword(user.ID, request.NewPassword)
|
||||
c.JSON(http.StatusOK, success)
|
||||
}
|
||||
|
||||
@@ -23,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{
|
||||
request.HeaderExtractor{"Authorization"},
|
||||
stripBearerPrefixFromTokenString,
|
||||
Extractor: request.HeaderExtractor{"Authorization"},
|
||||
Filter: stripBearerPrefixFromTokenString,
|
||||
}
|
||||
|
||||
// Extractor for OAuth2 access tokens. Looks in 'Authorization'
|
||||
|
||||
@@ -51,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
|
||||
}
|
||||
|
||||
|
||||
@@ -397,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)
|
||||
|
||||
@@ -60,6 +60,7 @@ 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"`
|
||||
@@ -195,3 +196,50 @@ 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"`
|
||||
}
|
||||
|
||||
@@ -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,6 +160,11 @@ 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)
|
||||
@@ -190,6 +195,11 @@ 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)
|
||||
@@ -271,6 +281,29 @@ 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)
|
||||
@@ -332,8 +365,7 @@ func UnlockMissedJobs() {
|
||||
if (job.Date == time.Time{}) {
|
||||
continue
|
||||
}
|
||||
var duration time.Duration
|
||||
duration = time.Duration(job.Duration)
|
||||
var duration = time.Duration(job.Duration)
|
||||
d := job.Date.Add(time.Minute * duration)
|
||||
if d.Before(time.Now()) {
|
||||
fmt.Println(job.Name + " is unlocked")
|
||||
|
||||
@@ -36,76 +36,74 @@ const (
|
||||
USER
|
||||
)
|
||||
|
||||
type AlertFrequency int
|
||||
|
||||
const (
|
||||
ONETIME AlertFrequency = iota
|
||||
RECURRING
|
||||
)
|
||||
|
||||
type AlertType int
|
||||
|
||||
const (
|
||||
DISTANCE AlertType = iota
|
||||
TIME
|
||||
BOTH
|
||||
)
|
||||
|
||||
type EnumDetail struct {
|
||||
Short string `json:"short"`
|
||||
Long string `json:"long"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
var FuelUnitDetails map[FuelUnit]EnumDetail = map[FuelUnit]EnumDetail{
|
||||
LITRE: {
|
||||
Short: "Lt",
|
||||
Long: "Litre",
|
||||
Key: "litre",
|
||||
},
|
||||
GALLON: {
|
||||
Short: "Gal",
|
||||
Long: "Gallon",
|
||||
Key: "gallon",
|
||||
}, KILOGRAM: {
|
||||
Short: "Kg",
|
||||
Long: "Kilogram",
|
||||
Key: "kilogram",
|
||||
}, KILOWATT_HOUR: {
|
||||
Short: "KwH",
|
||||
Long: "Kilowatt Hour",
|
||||
Key: "kilowatthour",
|
||||
}, US_GALLON: {
|
||||
Short: "US Gal",
|
||||
Long: "US Gallon",
|
||||
Key: "usgallon",
|
||||
},
|
||||
MINUTE: {
|
||||
Short: "Mins",
|
||||
Long: "Minutes",
|
||||
Key: "minutes",
|
||||
},
|
||||
}
|
||||
|
||||
var FuelTypeDetails map[FuelType]EnumDetail = map[FuelType]EnumDetail{
|
||||
PETROL: {
|
||||
Short: "Petrol",
|
||||
Long: "Petrol",
|
||||
Key: "petrol",
|
||||
},
|
||||
DIESEL: {
|
||||
Short: "Diesel",
|
||||
Long: "Diesel",
|
||||
Key: "diesel",
|
||||
}, CNG: {
|
||||
Short: "CNG",
|
||||
Long: "CNG",
|
||||
Key: "cng",
|
||||
}, LPG: {
|
||||
Short: "LPG",
|
||||
Long: "LPG",
|
||||
Key: "lpg",
|
||||
}, ELECTRIC: {
|
||||
Short: "Electric",
|
||||
Long: "Electric",
|
||||
Key: "electric",
|
||||
}, ETHANOL: {
|
||||
Short: "Ethanol",
|
||||
Long: "Ethanol",
|
||||
Key: "ethanol",
|
||||
},
|
||||
}
|
||||
|
||||
var DistanceUnitDetails map[DistanceUnit]EnumDetail = map[DistanceUnit]EnumDetail{
|
||||
KILOMETERS: {
|
||||
Short: "Km",
|
||||
Long: "Kilometers",
|
||||
Key: "kilometers",
|
||||
},
|
||||
MILES: {
|
||||
Short: "Mi",
|
||||
Long: "Miles",
|
||||
Key: "miles",
|
||||
},
|
||||
}
|
||||
|
||||
var RoleDetails map[Role]EnumDetail = map[Role]EnumDetail{
|
||||
ADMIN: {
|
||||
Short: "Admin",
|
||||
Long: "ADMIN",
|
||||
Key: "ADMIN",
|
||||
},
|
||||
USER: {
|
||||
Short: "User",
|
||||
Long: "USER",
|
||||
Key: "USER",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -18,6 +18,15 @@ 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() {
|
||||
|
||||
21
server/models/alert.go
Normal file
21
server/models/alert.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/akhilrex/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"`
|
||||
}
|
||||
@@ -17,6 +17,7 @@ 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"`
|
||||
|
||||
172
server/service/alertSevice.go
Normal file
172
server/service/alertSevice.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/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(¬ification)
|
||||
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() {
|
||||
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package service
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -126,14 +125,14 @@ func CreateBackup() (string, error) {
|
||||
tarballFilePath := path.Join(folder, backupFileName)
|
||||
file, err := os.Create(tarballFilePath)
|
||||
if err != nil {
|
||||
return "", errors.New(fmt.Sprintf("Could not create tarball file '%s', got error '%s'", tarballFilePath, err.Error()))
|
||||
return "", fmt.Errorf("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 "", errors.New(fmt.Sprintf("Could not find db file '%s', got error '%s'", dbPath, err.Error()))
|
||||
return "", fmt.Errorf("could not find db file '%s', got error '%s'", dbPath, err.Error())
|
||||
}
|
||||
gzipWriter := gzip.NewWriter(file)
|
||||
defer gzipWriter.Close()
|
||||
@@ -151,13 +150,13 @@ func CreateBackup() (string, error) {
|
||||
func addFileToTarWriter(filePath string, tarWriter *tar.Writer) error {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return errors.New(fmt.Sprintf("Could not open file '%s', got error '%s'", filePath, err.Error()))
|
||||
return fmt.Errorf("could not open file '%s', got error '%s'", filePath, err.Error())
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return errors.New(fmt.Sprintf("Could not get stat for file '%s', got error '%s'", filePath, err.Error()))
|
||||
return fmt.Errorf("could not get stat for file '%s', got error '%s'", filePath, err.Error())
|
||||
}
|
||||
|
||||
header := &tar.Header{
|
||||
@@ -169,12 +168,12 @@ func addFileToTarWriter(filePath string, tarWriter *tar.Writer) error {
|
||||
|
||||
err = tarWriter.WriteHeader(header)
|
||||
if err != nil {
|
||||
return errors.New(fmt.Sprintf("Could not write header for file '%s', got error '%s'", filePath, err.Error()))
|
||||
return fmt.Errorf("could not write header for file '%s', got error '%s'", filePath, err.Error())
|
||||
}
|
||||
|
||||
_, err = io.Copy(tarWriter, file)
|
||||
if err != nil {
|
||||
return errors.New(fmt.Sprintf("Could not copy the file '%s' data to the tarball, got error '%s'", filePath, err.Error()))
|
||||
return fmt.Errorf("could not copy the file '%s' data to the tarball, got error '%s'", filePath, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/akhilrex/hammond/db"
|
||||
@@ -15,6 +16,9 @@ func GetMileageByVehicleId(vehicleId string, since time.Time) (mileage []models.
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
)
|
||||
@@ -8,7 +10,7 @@ import (
|
||||
func CreateUser(userModel *models.RegisterRequest, role db.Role) error {
|
||||
setting := db.GetOrCreateSetting()
|
||||
toCreate := db.User{
|
||||
Email: userModel.Email,
|
||||
Email: strings.ToLower(userModel.Email),
|
||||
Name: userModel.Name,
|
||||
Role: role,
|
||||
Currency: setting.Currency,
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
@@ -13,6 +14,7 @@ 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,
|
||||
@@ -99,6 +101,7 @@ 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
|
||||
@@ -243,6 +246,24 @@ 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)
|
||||
|
||||
28844
ui/package-lock.json
generated
28844
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -34,17 +34,19 @@
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.27",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.12.1",
|
||||
"@fortawesome/vue-fontawesome": "0.1.9",
|
||||
"axios": "0.19.2",
|
||||
"axios": "^0.27.2",
|
||||
"buefy": "^0.9.7",
|
||||
"chart.js": "^2.9.4",
|
||||
"core-js": "3.6.4",
|
||||
"currency-formatter": "^1.5.7",
|
||||
"date-fns": "2.10.0",
|
||||
"lodash": "4.17.15",
|
||||
"lodash": "^4.17.21",
|
||||
"node-gyp": "^9.3.1",
|
||||
"normalize.css": "8.0.1",
|
||||
"nprogress": "0.2.0",
|
||||
"vue": "2.6.11",
|
||||
"vue-chartjs": "^3.5.1",
|
||||
"vue-i18n": "^8.28.2",
|
||||
"vue-meta": "2.3.3",
|
||||
"vue-router": "3.1.6",
|
||||
"vuex": "3.1.2"
|
||||
@@ -70,7 +72,7 @@
|
||||
"hygen": "4.0.x",
|
||||
"imagemin-lint-staged": "0.4.x",
|
||||
"lint-staged": "10.0.x",
|
||||
"markdownlint-cli": "0.22.x",
|
||||
"markdownlint-cli": "^0.31.1",
|
||||
"npm-run-all": "4.1.x",
|
||||
"sass": "1.26.x",
|
||||
"sass-loader": "8.0.x",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 463 B After Width: | Height: | Size: 895 B |
@@ -5,6 +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>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
BIN
ui/public/touch-icon.png
Normal file
BIN
ui/public/touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
@@ -20,7 +20,7 @@ export default {
|
||||
}
|
||||
} else {
|
||||
if (this.file == null) {
|
||||
return 'Upload Photo'
|
||||
return this.$t('uploadphoto')
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
@@ -39,7 +39,7 @@ export default {
|
||||
.post(`/api/quickEntries`, formData)
|
||||
.then((data) => {
|
||||
this.$buefy.toast.open({
|
||||
message: 'Quick Entry Created Successfully',
|
||||
message: this.$t('quickentrycreatedsuccessfully'),
|
||||
type: 'is-success',
|
||||
duration: 3000,
|
||||
})
|
||||
@@ -68,9 +68,9 @@ export default {
|
||||
<div class="section box">
|
||||
<div class="columns">
|
||||
<div class="column is-two-thirds">
|
||||
<p class="title">Quick Entry</p>
|
||||
<p class="title">{{ $tc('quickentry',1) }}</p>
|
||||
<p class="subtitle"
|
||||
>Take a pic of the invoice or the fuel pump display to make an entry later.</p
|
||||
>{{ $t('quickentrydesc') }}</p
|
||||
></div
|
||||
>
|
||||
<div class="column is-one-third is-flex is-align-content-center">
|
||||
@@ -99,10 +99,10 @@ export default {
|
||||
native-type="submit"
|
||||
:disabled="tryingToCreate"
|
||||
type="is-primary"
|
||||
value="Upload File"
|
||||
:value="this.$t('uploadfile')"
|
||||
class="control"
|
||||
>
|
||||
Upload File
|
||||
{{ $t('uploadfile') }}
|
||||
</b-button>
|
||||
</div></div
|
||||
>
|
||||
|
||||
@@ -30,7 +30,7 @@ export default {
|
||||
var labels = this.chartData.map((x) => x.date.substr(0, 10))
|
||||
var dataset = {
|
||||
steppedLine: true,
|
||||
label: `Mileage (${this.user.distanceUnitDetail.short}/${this.vehicle.fuelUnitDetail.short})`,
|
||||
label: `${this.$t('odometer')} (${this.$t('unit.short.' + this.user.distanceUnitDetail.key)}/${this.$t('unit.short.' + this.vehicle.fuelUnitDetail.key)})`,
|
||||
fill: true,
|
||||
data: this.chartData.map((x) => x.mileage),
|
||||
}
|
||||
|
||||
@@ -10,42 +10,42 @@ export default {
|
||||
persistentNavRoutes: [
|
||||
{
|
||||
name: 'home',
|
||||
title: 'Home',
|
||||
title: this.$t('menu.home'),
|
||||
},
|
||||
],
|
||||
loggedInNavRoutes: [
|
||||
{
|
||||
name: 'quickEntries',
|
||||
title: () => 'Quick Entries',
|
||||
title: () => this.$t('menu.quickentries'),
|
||||
badge: () => this.unprocessedQuickEntries.length,
|
||||
},
|
||||
{
|
||||
name: 'import',
|
||||
title: () => 'Import',
|
||||
title: () => this.$t('menu.import'),
|
||||
},
|
||||
{
|
||||
name: 'settings',
|
||||
title: 'Settings',
|
||||
title: this.$t('menu.settings'),
|
||||
},
|
||||
{
|
||||
name: 'logout',
|
||||
title: 'Log out',
|
||||
title: this.$t('menu.logout'),
|
||||
},
|
||||
],
|
||||
loggedOutNavRoutes: [
|
||||
{
|
||||
name: 'login',
|
||||
title: 'Log in',
|
||||
title: this.$t('menu.login'),
|
||||
},
|
||||
],
|
||||
adminNavRoutes: [
|
||||
{
|
||||
name: 'site-settings',
|
||||
title: 'Site Settings',
|
||||
title: this.$t('menu.sitesettings'),
|
||||
},
|
||||
{
|
||||
name: 'users',
|
||||
title: 'Users',
|
||||
title: this.$t('menu.users'),
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -72,7 +72,7 @@ export default {
|
||||
<NavBarRoutes :routes="persistentNavRoutes" />
|
||||
<NavBarRoutes v-if="loggedIn" :routes="loggedInNavRoutes" />
|
||||
<NavBarRoutes v-else :routes="loggedOutNavRoutes" />
|
||||
<b-navbar-dropdown v-if="loggedIn && isAdmin" label="Admin">
|
||||
<b-navbar-dropdown v-if="loggedIn && isAdmin" :label="$t('menu.admin')">
|
||||
<NavBarRoutes :routes="adminNavRoutes" />
|
||||
</b-navbar-dropdown>
|
||||
</template>
|
||||
|
||||
@@ -50,12 +50,12 @@ export default {
|
||||
<b-select
|
||||
v-if="unprocessedQuickEntries.length"
|
||||
v-model="quickEntry"
|
||||
placeholder="Refer quick entry"
|
||||
:placeholder="this.$t('referquickentry')"
|
||||
expanded
|
||||
@input="showQuickEntry($event)"
|
||||
>
|
||||
<option v-for="option in unprocessedQuickEntries" :key="option.id" :value="option">
|
||||
Taken: {{ parseAndFormatDateTime(option.createdAt) }}
|
||||
{{ $t('created') }}: {{ parseAndFormatDateTime(option.createdAt) }}
|
||||
</option>
|
||||
</b-select>
|
||||
<p class="control">
|
||||
|
||||
@@ -55,7 +55,7 @@ export default {
|
||||
return
|
||||
}
|
||||
this.$buefy.dialog.confirm({
|
||||
title: 'Transfer Vehicle',
|
||||
title: this.$t('transfervehicle'),
|
||||
message: 'Are you sure you want to do this? You will lose ownership and all editing rights if you confirm.',
|
||||
cancelText: 'Cancel',
|
||||
confirmText: 'Go Ahead',
|
||||
@@ -90,7 +90,7 @@ export default {
|
||||
|
||||
<template>
|
||||
<div class="box" style="max-width:600px">
|
||||
<h1 class="subtitle">Share {{ vehicle.nickname }}</h1>
|
||||
<h1 class="subtitle">{{ $t('share') }} {{ vehicle.nickname }}</h1>
|
||||
<section>
|
||||
<div class="columns is-mobile" v-for="model in models" :key="model.id">
|
||||
<div class="column is-one-third">
|
||||
@@ -101,7 +101,7 @@ export default {
|
||||
</b-field> </div
|
||||
><div class="column is-three-quarters">
|
||||
<b-field>
|
||||
<b-button v-if="model.isShared && !model.isOwner" type="is-primary is-small" @click="transferVehicle(model)">Make Owner</b-button>
|
||||
<b-button v-if="model.isShared && !model.isOwner" type="is-primary is-small" @click="transferVehicle(model)">{{ $t('makeowner') }}</b-button>
|
||||
</b-field></div
|
||||
></div
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { addDays, addMonths } from 'date-fns'
|
||||
import currencyFormtter from 'currency-formatter'
|
||||
import currencyFormatter from 'currency-formatter'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
import axios from 'axios'
|
||||
@@ -14,12 +14,12 @@ export default {
|
||||
data: function() {
|
||||
return {
|
||||
dateRangeOptions: [
|
||||
{ label: 'This week', value: 'this_week' },
|
||||
{ label: 'This month', value: 'this_month' },
|
||||
{ label: 'Past 30 days', value: 'past_30_days' },
|
||||
{ label: 'Past 3 months', value: 'past_3_months' },
|
||||
{ label: 'This year', value: 'this_year' },
|
||||
{ label: 'All Time', value: 'all_time' },
|
||||
{ label: this.$t('thisweek'), value: 'this_week' },
|
||||
{ label: this.$t('thismonth'), value: 'this_month' },
|
||||
{ label: this.$tc('pastxdays', 30), value: 'past_30_days' },
|
||||
{ label: this.$tc('pastxmonths', 3), value: 'past_3_months' },
|
||||
{ label: this.$t('thisyear'), value: 'this_year' },
|
||||
{ label: this.$t('alltime'), value: 'all_time' },
|
||||
],
|
||||
dateRangeOption: 'past_30_days',
|
||||
stats: [],
|
||||
@@ -32,15 +32,15 @@ export default {
|
||||
return [
|
||||
[
|
||||
{
|
||||
label: 'Total Expenditure',
|
||||
label: this.$t('totalexpenses'),
|
||||
value: this.formatCurrency(0, this.user.currency),
|
||||
},
|
||||
{
|
||||
label: 'Fillup Costs',
|
||||
label: this.$t('fillupcost'),
|
||||
value: `${this.formatCurrency(0, this.user.currency)} (0)`,
|
||||
},
|
||||
{
|
||||
label: 'Other Expenses',
|
||||
label: this.$t('otherexpenses'),
|
||||
value: `${this.formatCurrency(0, this.user.currency)} (0)`,
|
||||
},
|
||||
],
|
||||
@@ -49,15 +49,15 @@ export default {
|
||||
return this.stats.map((x) => {
|
||||
return [
|
||||
{
|
||||
label: 'Total Expenditure',
|
||||
label: this.$t('totalexpenses'),
|
||||
value: this.formatCurrency(x.expenditureTotal, x.currency),
|
||||
},
|
||||
{
|
||||
label: 'Fillup Costs',
|
||||
label: this.$t('fillupcost'),
|
||||
value: `${this.formatCurrency(x.expenditureFillups, x.currency)} (${x.countFillups})`,
|
||||
},
|
||||
{
|
||||
label: 'Other Expenses',
|
||||
label: this.$t('otherexpenses'),
|
||||
value: `${this.formatCurrency(x.expenditureExpenses, x.currency)} (${x.countExpenses})`,
|
||||
},
|
||||
]
|
||||
@@ -80,7 +80,7 @@ export default {
|
||||
if (!currencyCode) {
|
||||
currencyCode = this.me.currency
|
||||
}
|
||||
return currencyFormtter.format(number, { code: currencyCode })
|
||||
return currencyFormatter.format(number, { code: currencyCode })
|
||||
},
|
||||
getStats() {
|
||||
axios
|
||||
@@ -106,6 +106,7 @@ 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)
|
||||
@@ -114,7 +115,7 @@ export default {
|
||||
case 'past_3_months':
|
||||
return addMonths(toDate, -3)
|
||||
case 'this_year':
|
||||
return new Date(toDate.getFullYear(), 1, 1)
|
||||
return new Date(toDate.getFullYear(), 0, 1)
|
||||
case 'all_time':
|
||||
return new Date(1969, 4, 20)
|
||||
default:
|
||||
@@ -128,7 +129,7 @@ export default {
|
||||
<template>
|
||||
<div>
|
||||
<div class="columns">
|
||||
<div class="column" :class="isMobile ? 'has-text-centered' : ''"> <h1 class="title">Stats</h1></div>
|
||||
<div class="column" :class="isMobile ? 'has-text-centered' : ''"> <h1 class="title">{{ $t('statistics') }}</h1></div>
|
||||
<div class="column">
|
||||
<b-select v-model="dateRangeOption" class="is-pulled-right is-medium">
|
||||
<option v-for="option in dateRangeOptions" :key="option.value" :value="option.value">
|
||||
|
||||
25
ui/src/i18n.js
Normal file
25
ui/src/i18n.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import Vue from 'vue';
|
||||
import VueI18n from 'vue-i18n';
|
||||
|
||||
Vue.use(VueI18n);
|
||||
|
||||
function loadLocaleMessages () {
|
||||
const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i)
|
||||
const messages = {}
|
||||
locales.keys().forEach(key => {
|
||||
const matched = key.match(/([A-Za-z0-9-_]+)\./i)
|
||||
if (matched && matched.length > 1) {
|
||||
const locale = matched[1]
|
||||
messages[locale] = locales(key)
|
||||
}
|
||||
})
|
||||
return messages
|
||||
}
|
||||
|
||||
const i18n = new VueI18n({
|
||||
locale: navigator.language.split('-')[0] || 'en',
|
||||
fallbackLocale: 'en',
|
||||
messages: loadLocaleMessages()
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
217
ui/src/locales/de.json
Normal file
217
ui/src/locales/de.json
Normal file
@@ -0,0 +1,217 @@
|
||||
{
|
||||
"quickentry": "Keine Schnelleinträge | Schnelleintrag | Schnelleinträge",
|
||||
"statistics": "Statistiken",
|
||||
"thisweek": "Diese Woche",
|
||||
"thismonth": "Dieser Monat",
|
||||
"pastxdays": "Letzter Tag | Letzte {count} Tage",
|
||||
"pastxmonths": "Letzter Monat | Letzte {count} Monate",
|
||||
"thisyear": "Dieses Jahr",
|
||||
"alltime": "Gesamt",
|
||||
"noattachments": "Keine Anhänge",
|
||||
"attachments": "Anhänge",
|
||||
"choosefile": "Datei auswählen",
|
||||
"addattachment": "Anhang hinzufügen",
|
||||
"sharedwith": "Geteilt mit",
|
||||
"share": "Teile",
|
||||
"you": "Sie",
|
||||
"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 wiederhiolen",
|
||||
"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 zum 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 zum importieren.",
|
||||
"dontimportagain": "Achte darauf, dass du die Datei nicht erneut importierst, da dies zu wiederholten Einträgen führen würde.",
|
||||
"checkpointsimportcsv": "Wenn du alle diese Punkte überprüft hast kannst du unten die CSV importieren.",
|
||||
"importhintunits": "Vergewissere dich ebenfalls, dass die <u>Kraftstoffeinheit</u> und der <u>Kraftstofftyp</u> im Fahrzeug richtig eingestellt sind.",
|
||||
"importhintcurrdist": "Stelle sicher, dass die <u>Währung</u> und die <u>Entfernungseinheit</u> in Hammond korrekt eingestellt sind. Der Import erkennt die Währung nicht automatisch aus der CSV-Datei, sondern verwendet die für den Benutzer eingestellte Währung.",
|
||||
"importhintnickname": "Vergewissere dich, dass der Fahrzeugname in Hammond genau mit dem Namen in der Fuelly-CSV-Datei übereinstimmt, sonst funktioniert der Import nicht.",
|
||||
"importhintvehiclecreated": "Vergewissere dich, dass du die Fahrzeuge bereits in Hammond erstellt hast.",
|
||||
"importhintcreatecsv": "Exportiere deine Daten aus {name} im CSV-Format. Die Schritte dazu findest du",
|
||||
"here": "hier",
|
||||
"unprocessedquickentries": "Du hast einen Schnelleintrag zum bearbeiten. | Du hast {0} Schnelleinträge zum bearbeiten.",
|
||||
"show": "Anzeigen",
|
||||
"loginerror": "Bei der Anmeldung ist ein Fehler aufgetreten. {msg}",
|
||||
"showunprocessed": "Zeige unbearbeitete",
|
||||
"unprocessed": "unbearbeitet",
|
||||
"sitesettingdesc": "Ändere die globalen Einstellungen. Diese werden als Standard für neue Benutzer verwendet.",
|
||||
"settingdesc": "Diese Einstellungen werden als Standard verwendet wenn du eine neue Ausgabe oder eine Tankfüllung erfasst.",
|
||||
"areyousure": "Bist du dir sicher?",
|
||||
"adduser": "Benutzer hinzufügen",
|
||||
"usercreatedsuccessfully": "Benutzer erfolgreich gespeichert",
|
||||
"role": "Rolle",
|
||||
"created": "Erstellt",
|
||||
"createnewuser": "Erstelle neuen Benutzer",
|
||||
"cancel": "Abbrechen",
|
||||
"novehicles": "Du hast noch kein Fahrzeug erstellt. Lege jetzt einen Eintrag für das zu Verwaltende Auto 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 zum auswä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 wirklcih 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 cc)",
|
||||
"testconn": "Teste Verbindung",
|
||||
"migrate": "Migrieren",
|
||||
"init": {
|
||||
"migrateclarkson": "Migriere von Clarkson",
|
||||
"migrateclarksondesc": "Wenn du bereits eine Instanz von Clarkson verwendest und die Daten migrieren möchtest, klicke hier.",
|
||||
"freshinstall": "Frische Installation",
|
||||
"freshinstalldesc": "Wenn du eine neue Installation von Hammond starten möchtest, klicke hier.",
|
||||
"clarkson": {
|
||||
"desc": "<p>Zuerst musst du sicherstellen, dass das Deployment von Hammond die von Clarkson verwendete MySQL Datenbank erreichen kann.</p><p>Wenn dies nicht möglich ist kannst du eine Kopie erstellen die für Hammond erreichbar ist.</p><p>Wenn das erledigt ist, füge hier den Connection String im folgenden Format ein.</p><p>Alle aus Clarkson importierten Nutzer bekommen ihren Benutzernamen als E-Mail und das Passwort wird geändert zu <span class='' style='font-weight:bold'>hammond</span></p><code>user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local</code><br/><br/>",
|
||||
"success": "Deine Daten wurden erfolgreich von Clarkson migriert. Du wirst in kürze zur Anmeldung weitergeleitet wo du dich mit deiner E-Mail und dem passwort `hammond` anmelden kannst."
|
||||
},
|
||||
"fresh": {
|
||||
"setupadminuser": "Erstelle einen Administrator",
|
||||
"yourpassword": "Dein Passwort",
|
||||
"youremail": "Deine E-Mail Adresse",
|
||||
"yourname": "Dein Name",
|
||||
"success": "Du hast dich erfolgreich registriert. Du wirst in kürze zur Anmeldung weitergeleitet und kannst Anfangen Hammond zu verwenden."
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"ADMIN": "Adminstrator",
|
||||
"USER": "Benutzer"
|
||||
},
|
||||
"profile": "Profil",
|
||||
"processedon": "Bearbeitet am",
|
||||
"enable": "Entsperren",
|
||||
"disable": "Sperren",
|
||||
"confirm": "Bestätigen",
|
||||
"labelforfile": "Bezeichnung für diese Datei"
|
||||
}
|
||||
224
ui/src/locales/en.json
Normal file
224
ui/src/locales/en.json
Normal file
@@ -0,0 +1,224 @@
|
||||
{
|
||||
"quickentry": "No Quick Entries | Quick Entry | Quick Entries",
|
||||
"statistics": "Statistics",
|
||||
"thisweek": "This week",
|
||||
"thismonth": "This month",
|
||||
"pastxdays": "Past one day | Past {count} days",
|
||||
"pastxmonths": "Past one month | Past {count} months",
|
||||
"thisyear": "This year",
|
||||
"alltime": "All Time",
|
||||
"noattachments": "No Attachments so far",
|
||||
"attachments": "Attachments",
|
||||
"choosefile": "Choose File",
|
||||
"addattachment": "Add Attachment",
|
||||
"sharedwith": "Shared with",
|
||||
"share": "Share",
|
||||
"you": "You",
|
||||
"addfillup": "Add Fillup",
|
||||
"createfillup": "Create Fillup",
|
||||
"deletefillup": "Delete this fillup",
|
||||
"addexpense": "Add Expense",
|
||||
"createexpense": "Create Expense",
|
||||
"deleteexpense": "Delete this expense",
|
||||
"nofillups": "No Fillups so far",
|
||||
"transfervehicle": "Transfer Vehicle",
|
||||
"settingssaved": "Settings saved successfully",
|
||||
"yoursettings": "Your Settings",
|
||||
"settings": "Settings",
|
||||
"changepassword": "Change password",
|
||||
"oldpassword": "Old password",
|
||||
"newpassword": "New password",
|
||||
"repeatnewpassword": "Repeat New Password",
|
||||
"passworddontmatch": "Password values don't match",
|
||||
"save": "Save",
|
||||
"supportthedeveloper": "Support the developer",
|
||||
"buyhimabeer": "Buy him a beer!",
|
||||
"featurerequest": "Feature Request",
|
||||
"foundabug": "Found a bug",
|
||||
"currentversion": "Current Version",
|
||||
"moreinfo": "More Info",
|
||||
"currency": "Currency",
|
||||
"distanceunit": "Distance Unit",
|
||||
"dateformat": "Date Format",
|
||||
"createnow": "Create Now",
|
||||
"yourvehicles": "Your Vehicles",
|
||||
"menu": {
|
||||
"quickentries": "Quick Entries",
|
||||
"logout": "Log out",
|
||||
"import": "Import",
|
||||
"home": "Home",
|
||||
"settings": "Settings",
|
||||
"admin": "Admin",
|
||||
"sitesettings": "Site Settings",
|
||||
"users": "Users",
|
||||
"login": "Log in"
|
||||
},
|
||||
"enterusername": "Enter your username",
|
||||
"enterpassword": "Enter your password",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"login": "log in",
|
||||
"totalexpenses": "Total Expenses",
|
||||
"fillupcost": "Fillup Costs",
|
||||
"otherexpenses": "Other Expenses",
|
||||
"addvehicle": "Add Vehicle",
|
||||
"editvehicle": "Edit Vehicle",
|
||||
"deletevehicle": "Delete Vehicle",
|
||||
"sharevehicle": "Share vehicle",
|
||||
"makeowner": "Make Owner",
|
||||
"lastfillup": "Last Fillup",
|
||||
"quickentrydesc": "Take a pic of the invoice or the fuel pump display to make an entry later.",
|
||||
"quickentrycreatedsuccessfully": "Quick Entry Created Successfully",
|
||||
"uploadfile": "Upload File",
|
||||
"uploadphoto": "Upload Photo",
|
||||
"details": "Details",
|
||||
"odometer": "Odometer",
|
||||
"language": "Language",
|
||||
"date": "Date",
|
||||
"pastfillups": "Past Fillups",
|
||||
"fuelsubtype": "Fuel Subtype",
|
||||
"fueltype": "Fuel Type",
|
||||
"quantity": "Quantity",
|
||||
"gasstation": "Gas Station",
|
||||
"fuel": {
|
||||
"petrol": "Petrol",
|
||||
"diesel": "Diesel",
|
||||
"cng": "CNG",
|
||||
"lpg": "LPG",
|
||||
"electric": "Electric",
|
||||
"ethanol": "Ethanol"
|
||||
},
|
||||
"unit": {
|
||||
"long": {
|
||||
"litre": "Litre",
|
||||
"gallon": "Gallon",
|
||||
"kilowatthour": "Kilowatt Hour",
|
||||
"kilogram": "Kilogram",
|
||||
"usgallon": "US Gallon",
|
||||
"minutes": "Minutes",
|
||||
"kilometers": "Kilometers",
|
||||
"miles": "Miles"
|
||||
},
|
||||
"short": {
|
||||
"litre": "Lt",
|
||||
"gallon": "Gal",
|
||||
"kilowatthour": "KwH",
|
||||
"kilogram": "Kg",
|
||||
"usgallon": "US Gal",
|
||||
"minutes": "Mins",
|
||||
"kilometers": "Km",
|
||||
"miles": "Mi"
|
||||
}
|
||||
},
|
||||
"avgfillupqty": "Avg Fillup Qty",
|
||||
"avgfillupexpense": "Avg Fillup Expense",
|
||||
"avgfuelcost": "Avg Fuel Cost",
|
||||
"per": "{0} per {1}",
|
||||
"price": "Price",
|
||||
"total": "Total",
|
||||
"fulltank": "Tank Full",
|
||||
"getafulltank": "Did you get a full tank?",
|
||||
"by": "By",
|
||||
"expenses": "Expenses",
|
||||
"expensetype": "Expense Type",
|
||||
"noexpenses": "No Expenses so far",
|
||||
"download": "Download",
|
||||
"title": "Title",
|
||||
"name": "Name",
|
||||
"delete": "Delete",
|
||||
"importdata": "Import data into Hammond",
|
||||
"importdatadesc": "Choose from the following options to import data into Hammond",
|
||||
"import": "Import",
|
||||
"importcsv": "If you have been using {name} to store your vehicle data, export the CSV file from {name} and click here to import.",
|
||||
"choosecsv": "Choose CSV",
|
||||
"choosephoto": "Choose Photo",
|
||||
"importsuccessfull": "Data Imported Successfully",
|
||||
"importerror": "There was some issue with importing the file. Please check the error message",
|
||||
"importfrom": "Import from {0}",
|
||||
"stepstoimport": "Steps to import data from {name}",
|
||||
"choosecsvimport": "Choose the {name} CSV and press the import button.",
|
||||
"dontimportagain": "Make sure that you do not import the file again because that will create repeat entries.",
|
||||
"checkpointsimportcsv": "Once you have checked all these points, just import the CSV below.",
|
||||
"importhintunits": "Similiarly, make sure that the <u>Fuel Unit</u> and <u>Fuel Type</u> are correctly set in the Vehicle.",
|
||||
"importhintcurrdist": "Make sure that the <u>Currency</u> and <u>Distance Unit</u> are set correctly in Hammond. Import will not autodetect Currency from the CSV but use the one set for the user.",
|
||||
"importhintnickname": "Make sure that the Vehicle nickname in Hammond is exactly the same as the name on Fuelly CSV or the import will not work.",
|
||||
"importhintvehiclecreated": "Make sure that you have already created the vehicles in Hammond platform.",
|
||||
"importhintcreatecsv": "Export your data from {name} in the CSV format. Steps to do that can be found",
|
||||
"here": "here",
|
||||
"unprocessedquickentries": "You have one quick entry to be processed. | You have {0} quick entries pending to be processed.",
|
||||
"show": "Show",
|
||||
"loginerror": "There was an error logging in to your account. {msg}",
|
||||
"showunprocessed": "Show unprocessed only",
|
||||
"unprocessed": "unprocessed",
|
||||
"sitesettingdesc": "Update site level settings. These will be used as default values for new users.",
|
||||
"settingdesc": "These will be used as default values whenever you create a new fillup or expense.",
|
||||
"areyousure": "Are you sure you want to do this?",
|
||||
"adduser": "Add User",
|
||||
"usercreatedsuccessfully": "User Created Successfully",
|
||||
"userdisabledsuccessfully": "User disabled successfully",
|
||||
"userenabledsuccessfully": "User enabled successfully",
|
||||
"role": "Role",
|
||||
"created": "Created",
|
||||
"createnewuser": "Create New User",
|
||||
"cancel": "Cancel",
|
||||
"novehicles": "It seems you have not yet created a vehicle in the system. Start by creating an entry for one of the vehicles you want to track.",
|
||||
"processed": "Mark Processed",
|
||||
"notfound": "Not Found",
|
||||
"timeout": "The page timed out while loading. Are you sure you're still connected to\nthe Internet?",
|
||||
"clicktoselect": "Click to select...",
|
||||
"expenseby": "Expense by",
|
||||
"selectvehicle": "Select a vehicle",
|
||||
"expensedate": "Expense Date",
|
||||
"totalamountpaid": "Total Amount Paid",
|
||||
"fillmoredetails": "Fill more details",
|
||||
"markquickentryprocessed": "Mark selected Quick Entry as processed",
|
||||
"referquickentry": "Refer quick entry",
|
||||
"deletequickentry": "This will delete this Quick Entry. This step cannot be reversed. Are you sure?",
|
||||
"fuelunit": "Fuel Unit",
|
||||
"fillingstation": "Filling Station Name",
|
||||
"comments": "Comments",
|
||||
"missfillupbefore": "Did you miss the fillup entry before this one?",
|
||||
"fillupdate": "Fillup Date",
|
||||
"fillupsavedsuccessfully": "Fillup Saved Successfully",
|
||||
"expensesavedsuccessfully": "Expense Saved Successfully",
|
||||
"vehiclesavedsuccessfully": "Vehicle Saved Successfully",
|
||||
"settingssavedsuccessfully": "Settings saved successfully",
|
||||
"back": "Back",
|
||||
"nickname": "Nickname",
|
||||
"registration": "Registration",
|
||||
"createvehicle": "Create Vehicle",
|
||||
"make": "Make / Company",
|
||||
"model": "Model",
|
||||
"yearmanufacture": "Year of Manufacture",
|
||||
"enginesize": "Engine Size (in cc)",
|
||||
"mysqlconnstr": "Mysql Connection String",
|
||||
"testconn": "Test Connection",
|
||||
"migrate": "Migrate",
|
||||
"init": {
|
||||
"migrateclarkson": "Migrate from Clarkson",
|
||||
"migrateclarksondesc": "If you have an existing Clarkson deployment and you want to migrate your data from that, press the following button.",
|
||||
"freshinstall": "Fresh Install",
|
||||
"freshinstalldesc": "If you want a fresh install of Hammond, press the following button.",
|
||||
"clarkson": {
|
||||
"desc": "<p>You need to make sure that this deployment of Hammond can access the MySQL database used by Clarkson.</p><p>If that is not directly possible, you can make a copy of that database somewhere accessible from this instance.</p><p>Once that is done, enter the connection string to the MySQL instance in the following format.</p><p>All the users imported from Clarkson will have their username as their email in Clarkson database and pasword set to<span class='' style='font-weight:bold'>hammond</span></p><code>user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local</code><br/><br/>",
|
||||
"success": "We have successfully migrated the data from Clarkson. You will be redirected to the login screen shortly where you can login using your existing email and password : hammond"
|
||||
},
|
||||
"fresh": {
|
||||
"setupadminuser": "Setup Admin Users",
|
||||
"yourpassword": "Your Password",
|
||||
"youremail": "Your Email",
|
||||
"yourname": "Your Name",
|
||||
"success": "You have been registered successfully. You will be redirected to the login screen shortly where you can login and start using the system."
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"ADMIN": "ADMIN",
|
||||
"USER": "USER"
|
||||
},
|
||||
"profile": "Profile",
|
||||
"processedon": "Processed on",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable",
|
||||
"confirm": "Go Ahead",
|
||||
"labelforfile": "Label for this file"
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
faTimesCircle,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import i18n from './i18n';
|
||||
|
||||
import App from './app.vue'
|
||||
|
||||
@@ -54,6 +55,7 @@ library.add(
|
||||
faUserFriends,
|
||||
faTimesCircle
|
||||
)
|
||||
|
||||
Vue.use(Buefy, {
|
||||
defaultIconComponent: 'vue-fontawesome',
|
||||
defaultIconPack: 'fas',
|
||||
@@ -73,6 +75,7 @@ const app = new Vue({
|
||||
store,
|
||||
|
||||
render: (h) => h(App),
|
||||
i18n,
|
||||
}).$mount('#app')
|
||||
|
||||
// If running e2e tests...
|
||||
|
||||
@@ -23,7 +23,7 @@ export default {
|
||||
<template v-if="resource">
|
||||
{{ resource }}
|
||||
</template>
|
||||
Not Found
|
||||
{{ $t('notfound') }}
|
||||
</h1>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
@@ -32,8 +32,7 @@ export default {
|
||||
<template>
|
||||
<Layout v-if="offlineConfirmed">
|
||||
<h1 :class="$style.title">
|
||||
The page timed out while loading. Are you sure you're still connected to
|
||||
the Internet?
|
||||
{{ $t('timeout') }}
|
||||
</h1>
|
||||
</Layout>
|
||||
<LoadingView v-else />
|
||||
|
||||
@@ -95,7 +95,7 @@ export default {
|
||||
.put(`/api/vehicles/${this.selectedVehicle.id}/expenses/${this.expense.id}`, this.expenseModel)
|
||||
.then((data) => {
|
||||
this.$buefy.toast.open({
|
||||
message: 'Expense Updated Successfully',
|
||||
message: this.$t('expensesavedsuccessfully'),
|
||||
type: 'is-success',
|
||||
duration: 3000,
|
||||
})
|
||||
@@ -120,7 +120,7 @@ export default {
|
||||
.post(`/api/vehicles/${this.selectedVehicle.id}/expenses`, this.expenseModel)
|
||||
.then((data) => {
|
||||
this.$buefy.toast.open({
|
||||
message: 'Expense Created Successfully',
|
||||
message: this.$t('expensesavedsuccessfully'),
|
||||
type: 'is-success',
|
||||
duration: 3000,
|
||||
})
|
||||
@@ -152,7 +152,7 @@ export default {
|
||||
<Layout>
|
||||
<div class="columns">
|
||||
<div class="column is-two-thirds">
|
||||
<h1 class="title">Create Expense</h1>
|
||||
<h1 class="title">{{ $t('createexpense') }}</h1>
|
||||
<h1 class="subtitle">
|
||||
{{ [selectedVehicle.nickname, selectedVehicle.registration, selectedVehicle.make, selectedVehicle.model].join(' | ') }}
|
||||
</h1>
|
||||
@@ -162,61 +162,61 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
<form @submit.prevent="createExpense">
|
||||
<b-field label="Select a vehicle">
|
||||
<b-select v-model="selectedVehicle" placeholder="Vehicle" required expanded :disabled="expense.id">
|
||||
<b-field :label="this.$t('selectvehicle')">
|
||||
<b-select v-model="selectedVehicle" :placeholder="this.$t('vehicle')" required expanded :disabled="expense.id">
|
||||
<option v-for="option in myVehicles" :key="option.id" :value="option">
|
||||
{{ option.nickname }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field label="Expense by">
|
||||
<b-select v-model="expenseModel.userId" placeholder="User" required expanded :disabled="expense.id">
|
||||
<b-field :label="this.$t('expenseby')">
|
||||
<b-select v-model="expenseModel.userId" :placeholder="this.$t('user')" required expanded :disabled="expense.id">
|
||||
<option v-for="option in users" :key="option.userId" :value="option.userId">
|
||||
{{ option.name }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field label="Expense Date">
|
||||
<b-field :label="this.$t('expensedate')">
|
||||
<b-datepicker
|
||||
v-model="expenseModel.date"
|
||||
:date-formatter="formatDate"
|
||||
placeholder="Click to select..."
|
||||
:placeholder="this.$t('clicktoselect')"
|
||||
icon="calendar"
|
||||
:max-date="new Date()"
|
||||
>
|
||||
</b-datepicker>
|
||||
</b-field>
|
||||
<b-field label="Expense Type*">
|
||||
<b-field :label="this.$t('expensetype') + `*`">
|
||||
<b-input v-model="expenseModel.expenseType" expanded required></b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Total Amount Paid">
|
||||
<b-field :label="this.$t('totalamountpaid')">
|
||||
<p class="control">
|
||||
<span class="button is-static">{{ me.currency }}</span>
|
||||
</p>
|
||||
<b-input v-model.number="expenseModel.amount" type="number" min="0" expanded step=".001" required></b-input>
|
||||
</b-field>
|
||||
<b-field label="Odometer Reading">
|
||||
<b-field :label="this.$t('odometer')">
|
||||
<p class="control">
|
||||
<span class="button is-static">{{ me.distanceUnitDetail.short }}</span>
|
||||
<span class="button is-static">{{ $t('unit.short.' + me.distanceUnitDetail.key) }}</span>
|
||||
</p>
|
||||
<b-input v-model.number="expenseModel.odoReading" type="number" min="0" expanded required></b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field>
|
||||
<b-switch v-model="showMore">Fill more details</b-switch>
|
||||
<b-switch v-model="showMore">{{ $t('fillmoredetails') }}</b-switch>
|
||||
</b-field>
|
||||
<fieldset v-if="showMore">
|
||||
<b-field label="Comments">
|
||||
<b-field :label="this.$t('details')">
|
||||
<b-input v-model="expenseModel.comments" type="textarea" expanded></b-input>
|
||||
</b-field>
|
||||
</fieldset>
|
||||
<b-field>
|
||||
<b-switch v-if="quickEntry" v-model="processQuickEntry">Mark selected Quick Entry as processed</b-switch>
|
||||
<b-switch v-if="quickEntry" v-model="processQuickEntry">{{ $t('markquickentryprocessed') }}</b-switch>
|
||||
</b-field>
|
||||
<br />
|
||||
<b-field>
|
||||
<b-button tag="input" native-type="submit" :disabled="tryingToCreate" type="is-primary" label="Create Expense" expanded> </b-button>
|
||||
<b-button tag="input" native-type="submit" :value="this.$t('save')" :disabled="tryingToCreate" type="is-primary" label="Create Expense" expanded> </b-button>
|
||||
</b-field>
|
||||
</form>
|
||||
</Layout>
|
||||
|
||||
@@ -126,7 +126,7 @@ export default {
|
||||
.put(`/api/vehicles/${this.selectedVehicle.id}/fillups/${this.fillup.id}`, this.fillupModel)
|
||||
.then((data) => {
|
||||
this.$buefy.toast.open({
|
||||
message: 'Fillup Updated Successfully',
|
||||
message: this.$t('fillupsavedsuccessfully'),
|
||||
type: 'is-success',
|
||||
duration: 3000,
|
||||
})
|
||||
@@ -153,7 +153,7 @@ export default {
|
||||
.post(`/api/vehicles/${this.selectedVehicle.id}/fillups`, this.fillupModel)
|
||||
.then((data) => {
|
||||
this.$buefy.toast.open({
|
||||
message: 'Fillup Created Successfully',
|
||||
message: this.$t('fillupsavedsuccessfully'),
|
||||
type: 'is-success',
|
||||
duration: 3000,
|
||||
})
|
||||
@@ -181,46 +181,44 @@ export default {
|
||||
|
||||
<template>
|
||||
<Layout>
|
||||
<div class="has-text-centered">
|
||||
<div class="columns">
|
||||
<div class="column is-two-thirds">
|
||||
<h1 class="title">Create Fillup</h1>
|
||||
<h1 class="subtitle">
|
||||
{{ [selectedVehicle.nickname, selectedVehicle.registration, selectedVehicle.make, selectedVehicle.model].join(' | ') }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="column is-one-thirds">
|
||||
<QuickEntryDisplay v-model="quickEntry" :user="user" />
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column is-two-thirds">
|
||||
<h1 class="title">{{ $t('createfillup') }}</h1>
|
||||
<h1 class="subtitle">
|
||||
{{ [selectedVehicle.nickname, selectedVehicle.registration, selectedVehicle.make, selectedVehicle.model].join(' | ') }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="column is-one-thirds">
|
||||
<QuickEntryDisplay v-model="quickEntry" :user="user" />
|
||||
</div>
|
||||
</div>
|
||||
<form class="" @submit.prevent="createFillup">
|
||||
<b-field label="Select a vehicle">
|
||||
<b-select v-model="selectedVehicle" placeholder="Vehicle" required expanded :disabled="fillup.id">
|
||||
<b-field :label="this.$t('selectvehicle')">
|
||||
<b-select v-model="selectedVehicle" :placeholder="this.$t('vehicle')" required expanded :disabled="fillup.id">
|
||||
<option v-for="option in myVehicles" :key="option.id" :value="option">
|
||||
{{ option.nickname }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field label="Expense by">
|
||||
<b-select v-model="fillupModel.userId" placeholder="User" required expanded :disabled="fillup.id">
|
||||
<b-field :label="this.$t('expenseby')">
|
||||
<b-select v-model="fillupModel.userId" :placeholder="this.$t('user')" required expanded :disabled="fillup.id">
|
||||
<option v-for="option in users" :key="option.userId" :value="option.userId">
|
||||
{{ option.name }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field label="Fillup Date">
|
||||
<b-field :label="this.$t('fillupdate')">
|
||||
<b-datepicker
|
||||
v-model="fillupModel.date"
|
||||
:date-formatter="formatDate"
|
||||
placeholder="Click to select..."
|
||||
placeholder="this.$t('clicktoselect')"
|
||||
icon="calendar"
|
||||
trap-focus
|
||||
:max-date="new Date()"
|
||||
>
|
||||
</b-datepicker>
|
||||
</b-field>
|
||||
<b-field label="Fuel Subtype">
|
||||
<b-field :label="this.$t('fuelsubtype')">
|
||||
<b-autocomplete
|
||||
v-model="fillupModel.fuelSubType"
|
||||
:data="filteredFuelSubtypes"
|
||||
@@ -231,55 +229,55 @@ export default {
|
||||
>
|
||||
</b-autocomplete>
|
||||
</b-field>
|
||||
<b-field label="Quantity*" addons>
|
||||
<b-field :label="this.$t('quantity') + `*`" addons>
|
||||
<b-input v-model.number="fillupModel.fuelQuantity" type="number" step=".001" min="0" expanded required></b-input>
|
||||
<b-select v-model="fillupModel.fuelUnit" placeholder="Fuel Unit" required>
|
||||
<b-select v-model="fillupModel.fuelUnit" :placeholder="this.$t('fuelunit')" required>
|
||||
<option v-for="(option, key) in fuelUnitMasters" :key="key" :value="key">
|
||||
{{ option.long }}
|
||||
{{ $t('unit.long.' + option.key) }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="'Price per ' + vehicle.fuelUnitDetail.short + '*'"
|
||||
<b-field :label="this.$t('per', { '0': this.$t('price'), '1': $t('unit.short.' + vehicle.fuelUnitDetail.key) })"
|
||||
><p class="control">
|
||||
<span class="button is-static">{{ me.currency }}</span>
|
||||
</p>
|
||||
<b-input v-model.number="fillupModel.perUnitPrice" type="number" min="0" step=".001" expanded required></b-input>
|
||||
</b-field>
|
||||
<b-field label="Total Amount Paid">
|
||||
<b-field :label="this.$t('totalamountpaid')">
|
||||
<p class="control">
|
||||
<span class="button is-static">{{ me.currency }}</span>
|
||||
</p>
|
||||
<b-input v-model.number="fillupModel.totalAmount" type="number" min="0" step=".001" expanded required></b-input>
|
||||
</b-field>
|
||||
<b-field label="Odometer Reading">
|
||||
<b-field :label="this.$t('odometer')">
|
||||
<p class="control">
|
||||
<span class="button is-static">{{ me.distanceUnitDetail.short }}</span>
|
||||
<span class="button is-static">{{ $t('unit.short.' + me.distanceUnitDetail.key) }}</span>
|
||||
</p>
|
||||
<b-input v-model.number="fillupModel.odoReading" type="number" min="0" expanded required></b-input>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-checkbox v-model="fillupModel.isTankFull">Did you get a full tank?</b-checkbox>
|
||||
<b-checkbox v-model="fillupModel.isTankFull">{{ $t('getafulltank') }}</b-checkbox>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-checkbox v-model="fillupModel.hasMissedFillup">Did you miss the fillup entry before this one?</b-checkbox>
|
||||
<b-checkbox v-model="fillupModel.hasMissedFillup">{{ $t('missfillupbefore') }}</b-checkbox>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-switch v-model="showMore">Fill more details</b-switch>
|
||||
<b-switch v-model="showMore">{{ $t('fillmoredetails') }}</b-switch>
|
||||
</b-field>
|
||||
<fieldset v-if="showMore">
|
||||
<b-field label="Filling Station Name">
|
||||
<b-field :label="this.$t('fillingstation')">
|
||||
<b-input v-model="fillupModel.fillingStation" type="text" expanded></b-input>
|
||||
</b-field>
|
||||
<b-field label="Comments">
|
||||
<b-field :label="this.$t('comments')">
|
||||
<b-input v-model="fillupModel.comments" type="textarea" expanded></b-input>
|
||||
</b-field>
|
||||
</fieldset>
|
||||
<b-field>
|
||||
<b-switch v-if="quickEntry" v-model="processQuickEntry">Mark selected Quick Entry as processed</b-switch>
|
||||
<b-switch v-if="quickEntry" v-model="processQuickEntry">{{ $t('markquickentryprocessed') }}</b-switch>
|
||||
</b-field>
|
||||
<br />
|
||||
<b-field>
|
||||
<b-button tag="input" native-type="submit" :disabled="tryingToCreate" type="is-primary" label="Create Fillup" expanded> </b-button>
|
||||
<b-button tag="input" native-type="submit" :disabled="tryingToCreate" type="is-primary" :value="this.$t('save')" :label="this.$t('createfillup')" expanded> </b-button>
|
||||
<p v-if="authError">
|
||||
There was an error logging in to your account.
|
||||
</p>
|
||||
|
||||
@@ -47,6 +47,7 @@ export default {
|
||||
fuelUnit: null,
|
||||
fuelType: null,
|
||||
registration: '',
|
||||
vin: '',
|
||||
nickname: '',
|
||||
engineSize: null,
|
||||
make: '',
|
||||
@@ -58,6 +59,7 @@ export default {
|
||||
fuelUnit: veh.fuelUnit,
|
||||
fuelType: veh.fuelType,
|
||||
registration: veh.registration,
|
||||
vin: veh.vin,
|
||||
nickname: veh.nickname,
|
||||
engineSize: veh.engineSize,
|
||||
make: veh.make,
|
||||
@@ -74,7 +76,7 @@ export default {
|
||||
.put(`/api/vehicles/${this.vehicle.id}`, this.vehicleModel)
|
||||
.then((data) => {
|
||||
this.$buefy.toast.open({
|
||||
message: 'Vehicle Updated Successfully',
|
||||
message: this.$t('vehiclesavedsuccessfully'),
|
||||
type: 'is-success',
|
||||
duration: 3000,
|
||||
})
|
||||
@@ -96,7 +98,7 @@ export default {
|
||||
.post(`/api/vehicles`, this.vehicleModel)
|
||||
.then((data) => {
|
||||
this.$buefy.toast.open({
|
||||
message: 'Vehicle Created Successfully',
|
||||
message: this.$t('vehiclesavedsuccessfully'),
|
||||
type: 'is-success',
|
||||
duration: 3000,
|
||||
})
|
||||
@@ -123,57 +125,60 @@ export default {
|
||||
<Layout>
|
||||
<div class="columns">
|
||||
<div class="column is-three-quarters">
|
||||
<h1 class="title">Create Vehicle</h1>
|
||||
<h1 class="title">{{ $t('createvehicle') }}</h1>
|
||||
</div>
|
||||
<div class="column is-one-quarter">
|
||||
<router-link tag="b-button" type="is-primary" to="/">
|
||||
Back to Vehicle
|
||||
{{ $t('back') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<form @submit.prevent="createVehicle">
|
||||
<b-field label="Nickname*">
|
||||
<b-field :label="this.$t('nickname') + `*`">
|
||||
<b-input v-model="vehicleModel.nickname" type="text" expanded required></b-input>
|
||||
</b-field>
|
||||
<b-field label="Registration*">
|
||||
<b-field :label="this.$t('registration') + `*`">
|
||||
<b-input v-model="vehicleModel.registration" type="text" expanded required></b-input>
|
||||
</b-field>
|
||||
<b-field label="Fuel Type*">
|
||||
<b-select v-model.number="vehicleModel.fuelType" placeholder="Fuel Type" required expanded>
|
||||
<b-field label="VIN">
|
||||
<b-input v-model="vehicleModel.vin" type="text" expanded></b-input>
|
||||
</b-field>
|
||||
<b-field :label="this.$t('fueltype') + `*`">
|
||||
<b-select v-model.number="vehicleModel.fuelType" :placeholder="this.$t('fueltype')" required expanded>
|
||||
<option v-for="(option, key) in fuelTypeMasters" :key="key" :value="key">
|
||||
{{ option.long }}
|
||||
{{ $t('fuel.' + option.key) }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Fuel Unit*">
|
||||
<b-select v-model.number="vehicleModel.fuelUnit" placeholder="Fuel Unit" required expanded>
|
||||
<b-field :label="this.$t('fuelunit') + `*`">
|
||||
<b-select v-model.number="vehicleModel.fuelUnit" :placeholder="this.$t('fuelunit')" required expanded>
|
||||
<option v-for="(option, key) in fuelUnitMasters" :key="key" :value="key">
|
||||
{{ option.long }}
|
||||
{{ $t('unit.long.' + option.key) }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Make / Company*">
|
||||
<b-field :label="this.$t('make') + `*`">
|
||||
<b-input v-model="vehicleModel.make" type="text" required expanded></b-input>
|
||||
</b-field>
|
||||
<b-field label="Model*">
|
||||
<b-field :label="this.$t('model') + `*`">
|
||||
<b-input v-model="vehicleModel.model" type="text" required expanded></b-input>
|
||||
</b-field>
|
||||
<b-field label="Year Of Manufacture">
|
||||
<b-field :label="this.$t('yearmanufacture') + `*`">
|
||||
<b-input v-model.number="vehicleModel.yearOfManufacture" type="number" expanded number></b-input>
|
||||
</b-field>
|
||||
<b-field label="Engine Size (in cc)">
|
||||
<b-field :label="this.$t('yearmanufacture')">
|
||||
<b-input v-model.number="vehicleModel.engineSize" type="number" expanded number></b-input>
|
||||
</b-field>
|
||||
|
||||
<br />
|
||||
<b-field>
|
||||
<b-button tag="input" native-type="submit" :disabled="tryingToCreate" type="is-primary" label="Create Vehicle" expanded>
|
||||
<b-button tag="input" native-type="submit" :disabled="tryingToCreate" type="is-primary" :value="this.$t('save')" :label="this.$t('createvehicle')" expanded>
|
||||
<BaseIcon v-if="tryingToCreate" name="sync" spin />
|
||||
</b-button>
|
||||
<p v-if="authError">
|
||||
There was an error logging in to your account.
|
||||
{{ $t('loginerror') }}
|
||||
</p>
|
||||
</b-field>
|
||||
</form>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import currencyFormtter from 'currency-formatter'
|
||||
import currencyFormatter from 'currency-formatter'
|
||||
|
||||
import appConfig from '@src/app.config'
|
||||
import Layout from '@layouts/main.vue'
|
||||
@@ -53,7 +53,7 @@ export default {
|
||||
return parseAndFormatDate(date)
|
||||
},
|
||||
formatCurrency(number) {
|
||||
return currencyFormtter.format(number, { code: this.me.currency })
|
||||
return currencyFormatter.format(number, { code: this.me.currency })
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -62,14 +62,13 @@ export default {
|
||||
<template>
|
||||
<Layout>
|
||||
<b-notification v-if="myVehicles.length === 0" type="is-warning is-light" :closable="false">
|
||||
<div class="columns">
|
||||
<div class="columns is-three-quarters">
|
||||
<div class="column">
|
||||
It seems you have not yet created a vehicle in the system. Start by creating an entry for
|
||||
one of the vehicles you want to track.
|
||||
{{ $t('novehicles') }}
|
||||
</div>
|
||||
<div class="column" :class="!isMobile ? 'has-text-right' : ''">
|
||||
<div class="column is-one-quarter" :class="!isMobile ? 'has-text-right' : ''">
|
||||
<b-button type="is-warning" class="" tag="router-link" :to="`/vehicles/create`"
|
||||
>Create Now</b-button
|
||||
>{{ $t('createnow') }}</b-button
|
||||
></div
|
||||
>
|
||||
</div>
|
||||
@@ -81,15 +80,11 @@ export default {
|
||||
>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
{{
|
||||
`You have ${unprocessedQuickEntries.length} quick ${
|
||||
unprocessedQuickEntries.length === 1 ? 'entry' : 'entries'
|
||||
} pending to be processed.`
|
||||
}}
|
||||
{{ $tc('unprocessedquickentries', unprocessedQuickEntries.length, { '0': unprocessedQuickEntries.length }) }}
|
||||
</div>
|
||||
<div class="column" :class="!isMobile ? 'has-text-right' : ''">
|
||||
<b-button type="is-warning" class="is-small" tag="router-link" :to="`/quickEntries`"
|
||||
>Process Now</b-button
|
||||
>{{ $t('show') }}</b-button
|
||||
></div
|
||||
>
|
||||
</div>
|
||||
@@ -99,10 +94,10 @@ export default {
|
||||
<br />
|
||||
<section>
|
||||
<div class="columns" :class="isMobile ? 'has-text-centered' : ''"
|
||||
><div class="column is-three-quarters"> <h1 class="title">Your Vehicles</h1></div>
|
||||
><div class="column is-three-quarters"> <h1 class="title">{{ $t('yourvehicles') }}</h1></div>
|
||||
<div class="column is-one-quarter buttons" :class="!isMobile ? 'has-text-right' : ''">
|
||||
<b-button type="is-primary" tag="router-link" :to="`/vehicles/create`"
|
||||
>Add Vehicle</b-button
|
||||
>{{ $t('addvehicle') }}</b-button
|
||||
>
|
||||
</div></div
|
||||
>
|
||||
@@ -125,22 +120,22 @@ export default {
|
||||
<div class="content">
|
||||
<table class="table">
|
||||
<div class="columns">
|
||||
<div class="column is-one-third">Last Fillup</div>
|
||||
<div class="column is-one-third">{{ $t('lastfillup') }}</div>
|
||||
<div class="column"
|
||||
>{{ formatDate(vehicle.fillups[0].date) }} <br />
|
||||
{{ `${formatCurrency(vehicle.fillups[0].totalAmount)}` }} ({{
|
||||
`${vehicle.fillups[0].fuelQuantity} ${vehicle.fillups[0].fuelUnitDetail.short}`
|
||||
`${vehicle.fillups[0].fuelQuantity} ${ $t('unit.short.' + vehicle.fillups[0].fuelUnitDetail.key) }`
|
||||
}}
|
||||
@ {{ `${formatCurrency(vehicle.fillups[0].perUnitPrice)}` }})</div
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-one-third">Odometer</div>
|
||||
<div class="column is-one-third">{{ $t('odometer') }}</div>
|
||||
<div class="column">
|
||||
<template v-if="vehicle.fillups.length">
|
||||
{{ vehicle.fillups[0].odoReading }} {{
|
||||
me.distanceUnitDetail.short
|
||||
$t('unit.short.' + me.distanceUnitDetail.key)
|
||||
}}</template
|
||||
>
|
||||
</div>
|
||||
@@ -150,12 +145,12 @@ export default {
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
<router-link class="card-footer-item" :to="'/vehicles/' + vehicle.id">
|
||||
Details
|
||||
{{ $t('details') }}
|
||||
</router-link>
|
||||
<router-link class="card-footer-item" :to="`/vehicles/${vehicle.id}/fillup`">
|
||||
Add Fillup </router-link
|
||||
{{ $t('addfillup') }} </router-link
|
||||
><router-link class="card-footer-item" :to="`/vehicles/${vehicle.id}/expense`">
|
||||
Add Expense
|
||||
{{ $t('addexpense') }}
|
||||
</router-link>
|
||||
</footer>
|
||||
</b-collapse>
|
||||
|
||||
@@ -14,13 +14,13 @@ export default {
|
||||
uploadButtonLabel() {
|
||||
if (this.isMobile) {
|
||||
if (this.file == null) {
|
||||
return 'Choose Photo'
|
||||
return this.$t('choosephoto')
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
} else {
|
||||
if (this.file == null) {
|
||||
return 'Choose CSV'
|
||||
return this.$t('choosecsv')
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
@@ -53,7 +53,7 @@ export default {
|
||||
.post(`/api/import/fuelly`, formData)
|
||||
.then((data) => {
|
||||
this.$buefy.toast.open({
|
||||
message: 'Data Imported Successfully',
|
||||
message: this.$t('importsuccessfull'),
|
||||
type: 'is-success',
|
||||
duration: 3000,
|
||||
})
|
||||
@@ -62,7 +62,7 @@ export default {
|
||||
.catch((ex) => {
|
||||
this.$buefy.toast.open({
|
||||
duration: 5000,
|
||||
message: 'There was some issue with importing the file. Please check the error message',
|
||||
message: this.$t('importerror'),
|
||||
position: 'is-bottom',
|
||||
type: 'is-danger',
|
||||
})
|
||||
@@ -82,33 +82,27 @@ export default {
|
||||
<Layout>
|
||||
<div class="columns box">
|
||||
<div class="column">
|
||||
<h1 class="title">Import from Fuelly</h1>
|
||||
<h1 class="title">{{ $t('importfrom', { 'name': 'Fuelly' }) }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<p class="subtitle"> Steps to import data from Fuelly</p>
|
||||
<p class="subtitle"> {{ $t('stepstoimport', { 'name': 'Fuelly' }) }}</p>
|
||||
<ol>
|
||||
<li
|
||||
>Export your data from Fuelly in the CSV format. Steps to do that can be found
|
||||
<a href="http://docs.fuelly.com/acar-import-export-center" target="_nofollow">here</a>.</li
|
||||
>
|
||||
<li>Make sure that you have already created the vehicles in Hammond platform.</li>
|
||||
<li>Make sure that the Vehicle nickname in Hammond is exactly the same as the name on Fuelly CSV or the import will not work.</li>
|
||||
<li
|
||||
>Make sure that the <u>Currency</u> and <u>Distance Unit</u> are set correctly in Hammond. Import will not autodetect Currency from the
|
||||
CSV but use the one set for the user.</li
|
||||
>
|
||||
<li>Similiarly, make sure that the <u>Fuel Unit</u> and <u>Fuel Type</u> are correctly set in the Vehicle.</li>
|
||||
<li>Once you have checked all these points,just import the CSV below.</li>
|
||||
<li><b>Make sure that you do not import the file again and that will create repeat entries.</b></li>
|
||||
<li>{{ $t('importhintcreatecsv', { 'name': 'Fuelly' }) }} <a href="http://docs.fuelly.com/acar-import-export-center" target="_nofollow">{{ $t('here') }}</a>.</li>
|
||||
<li>{{ $t('importhintvehiclecreated') }}</li>
|
||||
<li>{{ $t('importhintnickname') }}</li>
|
||||
<li v-html="$t('importhintcurrdist')"></li>
|
||||
<li v-html="$t('importhintunits')"></li>
|
||||
<li>{{ $t('checkpointsimportcsv') }}</li>
|
||||
<li><b>{{ $t('dontimportagain') }}</b></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section box">
|
||||
<div class="columns">
|
||||
<div class="column is-two-thirds"> <p class="subtitle">Choose the Fuelly CSV and press the import button.</p></div>
|
||||
<div class="column is-two-thirds"> <p class="subtitle">{{ $t('choosecsvimport', { 'name': 'Fuelly' }) }}</p></div>
|
||||
<div class="column is-one-third is-flex is-align-content-center">
|
||||
<form @submit.prevent="importFuelly">
|
||||
<div class="columns"
|
||||
@@ -126,8 +120,8 @@ export default {
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-button tag="input" native-type="submit" :disabled="tryingToCreate" type="is-primary" value="Upload File" class="control">
|
||||
Import
|
||||
<b-button tag="input" native-type="submit" :disabled="tryingToCreate" type="is-primary" :value="this.$t('uploadfile')" class="control">
|
||||
{{ $t('import') }}
|
||||
</b-button>
|
||||
</div></div
|
||||
>
|
||||
|
||||
@@ -20,17 +20,17 @@ export default {
|
||||
<Layout>
|
||||
<div class="columns box"
|
||||
><div class="column">
|
||||
<h1 class="title">Import data into Hammond</h1>
|
||||
<p class="subtitle">Choose from the following options to import data into Fuelly</p>
|
||||
<h1 class="title">{{ $t('importdata') }}</h1>
|
||||
<p class="subtitle">{{ $t('importdatadesc') }}</p>
|
||||
</div></div
|
||||
>
|
||||
<br />
|
||||
<div class="columns">
|
||||
<div class="box column is-one-third" to="/import-fuelly">
|
||||
<h1 class="title">Fuelly</h1>
|
||||
<p>If you have been using Fuelly to store your vehicle data, export the CSV file from Fuelly and click here to import.</p>
|
||||
<p>{{ $t('importcsv', { 'name': 'Fuelly' }) }}</p>
|
||||
<br />
|
||||
<b-button type="is-primary" tag="router-link" to="/import/fuelly">Import</b-button>
|
||||
<b-button type="is-primary" tag="router-link" to="/import/fuelly">{{ $t('import') }}</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
@@ -21,13 +21,27 @@ export default {
|
||||
email: '',
|
||||
password: '',
|
||||
distanceUnit: 1,
|
||||
currency: 'INR',
|
||||
currency: '',
|
||||
},
|
||||
}
|
||||
},
|
||||
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) => {})
|
||||
@@ -48,11 +62,11 @@ export default {
|
||||
var message = ''
|
||||
if (this.migrationMode === 'clarkson') {
|
||||
message =
|
||||
'We have successfully migrated the data from Clarkson. You will be redirected to the login screen shortly where you can login using your existing email and password : hammond'
|
||||
this.$t('init.clarkson.success')
|
||||
}
|
||||
if (this.migrationMode === 'fresh') {
|
||||
message =
|
||||
'You have been registered successfully. You will be redirected to the login screen shortly where you can login and start using the system.'
|
||||
this.$t('init.fresh.success')
|
||||
}
|
||||
this.$buefy.toast.open({
|
||||
duration: 10000,
|
||||
@@ -139,6 +153,9 @@ export default {
|
||||
})
|
||||
.finally(() => (this.isWorking = false))
|
||||
},
|
||||
formatCurrency(option) {
|
||||
return `${option.namePlural} (${option.code})`
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -146,114 +163,76 @@ export default {
|
||||
<template>
|
||||
<Layout>
|
||||
<div v-if="!migrationMode" class="box">
|
||||
<h1 class="title">Migrate from Clarkson</h1>
|
||||
<h1 class="title">{{ $t('init.migrateclarkson') }}</h1>
|
||||
<p>
|
||||
If you have an existing Clarkson deployment and you want to migrate your data from that,
|
||||
press the following button.
|
||||
{{ $t('init.migrateclarksondesc') }}
|
||||
</p>
|
||||
<br />
|
||||
<b-field>
|
||||
<b-button type="is-primary" @click="migrationMode = 'clarkson'"
|
||||
>Migrate from Clarkson</b-button
|
||||
></b-field
|
||||
>
|
||||
<b-field> <b-button type="is-primary" @click="migrationMode = 'clarkson'">{{ $t('init.migrateclarkson') }}</b-button></b-field>
|
||||
</div>
|
||||
<div v-if="!migrationMode" class="box">
|
||||
<h1 class="title">Fresh Install</h1>
|
||||
<h1 class="title">{{ $t('init.freshinstall') }}</h1>
|
||||
<p>
|
||||
If you want a fresh install of Hammond, press the following button.
|
||||
{{ $t('init.freshinstalldesc') }}
|
||||
</p>
|
||||
<br />
|
||||
<b-field>
|
||||
<b-button type="is-primary" @click="migrationMode = 'fresh'">Fresh Install</b-button>
|
||||
<b-button type="is-primary" @click="migrationMode = 'fresh'">{{ $t('init.freshinstall') }}</b-button>
|
||||
</b-field>
|
||||
</div>
|
||||
<div v-if="migrationMode === 'clarkson'" class="box content">
|
||||
<h1 class="title">Migrate from Clarkson</h1>
|
||||
<p
|
||||
>You need to make sure that this deployment of Hammond can access the MySQL database used by
|
||||
Clarkson.</p
|
||||
>
|
||||
<p
|
||||
>If that is not directly possible, you can make a copy of that database somewhere accessible
|
||||
from this instance.</p
|
||||
>
|
||||
<p
|
||||
>Once that is done, enter the connection string to the MySQL instance in the following
|
||||
format.</p
|
||||
>
|
||||
<p
|
||||
>All the users imported from Clarkson will have their username as their email in Clarkson
|
||||
database and pasword set to <span class="" style="font-weight:bold">hammond</span></p
|
||||
>
|
||||
<code>
|
||||
user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
|
||||
</code>
|
||||
<br />
|
||||
<br />
|
||||
<h1 class="title">{{ $t('init.migrateclarkson') }}</h1>
|
||||
<p v-html="$t('init.clarkson.desc')"></p>
|
||||
<b-notification v-if="connectionError" type="is-danger" role="alert" :closable="false">
|
||||
{{ connectionError }}
|
||||
</b-notification>
|
||||
|
||||
<b-field addons label="Mysql Connection String">
|
||||
<b-field addons :label="this.$t('mysqlconnstr')">
|
||||
<b-input v-model="url" required></b-input>
|
||||
</b-field>
|
||||
|
||||
<div class="buttons">
|
||||
<b-button
|
||||
v-if="!testSuccess"
|
||||
type="is-primary"
|
||||
:disabled="isWorking"
|
||||
@click="testConnection"
|
||||
>Test Connection</b-button
|
||||
><b-button v-if="testSuccess" type="is-success" :disabled="isWorking" @click="migrate"
|
||||
>Migrate</b-button
|
||||
>
|
||||
<b-button type="is-danger is-light" @click="resetMigrationMode">Cancel</b-button>
|
||||
<b-button v-if="!testSuccess" type="is-primary" :disabled="isWorking" @click="testConnection">{{ $t('testconn') }}</b-button>
|
||||
<b-button v-if="testSuccess" type="is-success" :disabled="isWorking" @click="migrate">{{ $t('migrate') }}</b-button>
|
||||
<b-button type="is-danger is-light" @click="resetMigrationMode">{{ $t('cancel') }}</b-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="migrationMode === 'fresh'" class="box content">
|
||||
<h1 class="title">Setup Admin Users</h1>
|
||||
<h1 class="title">{{ $t('init.fresh.setupadminuser') }}</h1>
|
||||
<form @submit.prevent="register">
|
||||
<b-field label="Your Name">
|
||||
<b-field :label="this.$t('init.fresh.yourname')">
|
||||
<b-input v-model="registerModel.name" required></b-input>
|
||||
</b-field>
|
||||
<b-field label="Your Email">
|
||||
<b-field :label="this.$t('init.fresh.youremail')">
|
||||
<b-input v-model="registerModel.email" type="email" required></b-input>
|
||||
</b-field>
|
||||
<b-field label="Your Password">
|
||||
<b-input
|
||||
v-model="registerModel.password"
|
||||
type="password"
|
||||
required
|
||||
minlength="8"
|
||||
password-reveal
|
||||
></b-input>
|
||||
<b-field :label="this.$t('init.fresh.yourpassword')">
|
||||
<b-input v-model="registerModel.password" type="password" required minlength="8" password-reveal></b-input>
|
||||
</b-field>
|
||||
<b-field label="Currency">
|
||||
<b-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"
|
||||
<b-field :label="this.$t('currency')">
|
||||
<b-autocomplete
|
||||
v-model="registerModel.currency"
|
||||
:custom-formatter="formatCurrency"
|
||||
:placeholder="this.$t('currency')"
|
||||
:data="filteredCurrencyMasters"
|
||||
:keep-first="true"
|
||||
:open-on-focus="true"
|
||||
required
|
||||
expanded
|
||||
>
|
||||
@select="(option) => (selected = option)"
|
||||
></b-autocomplete>
|
||||
</b-field>
|
||||
<b-field :label="this.$t('distanceunit')">
|
||||
<b-select v-model.number="registerModel.distanceUnit" :placeholder="this.$t('distanceunit')" required expanded>
|
||||
<option v-for="(option, key) in distanceUnitMasters" :key="key" :value="key">
|
||||
{{ `${option.long} (${option.short})` }}
|
||||
{{ `${$t('unit.long.' + option.key)} (${$t('unit.short.' + option.key)})` }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<br />
|
||||
<div class="buttons">
|
||||
<b-button type="is-primary" native-type="submit" tag="input"></b-button>
|
||||
<b-button type="is-primary" native-type="submit" tag="input" :value="this.$t('save')"></b-button>
|
||||
|
||||
<b-button type="is-danger is-light" @click="resetMigrationMode">Cancel</b-button>
|
||||
<b-button type="is-danger is-light" @click="resetMigrationMode">{{ $t('cancel') }}</b-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -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: 'Enter your username',
|
||||
password: 'Enter your password',
|
||||
username: this.$t('enterusername'),
|
||||
password: this.$t('enterpassword'),
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -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,31 +67,17 @@ export default {
|
||||
<template>
|
||||
<Layout>
|
||||
<form @submit.prevent="tryToLogIn">
|
||||
<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 :label="$t('email')"> <b-input v-model="username" tag="b-input" name="username" type="email" :placeholder="placeholders.username"/></b-field>
|
||||
<b-field :label="$t('password')">
|
||||
<b-input v-model="password" tag="b-input" name="password" type="password" :placeholder="placeholders.password" />
|
||||
</b-field>
|
||||
<b-button tag="input" native-type="submit" :disabled="tryingToLogIn" type="is-primary">
|
||||
<b-button tag="input" native-type="submit" :value="$t('login')" :disabled="tryingToLogIn" type="is-primary">
|
||||
<BaseIcon v-if="tryingToLogIn" name="sync" spin />
|
||||
<span v-else>
|
||||
Log in
|
||||
{{ $t('login') }}
|
||||
</span>
|
||||
</b-button>
|
||||
<p v-if="authError">
|
||||
There was an error logging in to your account. {{errorMessage}}
|
||||
</p>
|
||||
<p v-if="authError"> {{ $t('loginerror', { msg: errorMessage }) }}</p>
|
||||
</form>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
@@ -28,7 +28,7 @@ export default {
|
||||
<h1>
|
||||
<BaseIcon name="user" />
|
||||
{{ user.name }}
|
||||
Profile
|
||||
{{ $t('profile') }}
|
||||
</h1>
|
||||
<pre>{{ user }}</pre>
|
||||
</Layout>
|
||||
|
||||
@@ -41,7 +41,7 @@ export default {
|
||||
store.dispatch('vehicles/setQuickEntryAsProcessed', { id: entry.id }).then((data) => {})
|
||||
},
|
||||
deleteQuickEntry(entry) {
|
||||
var sure = confirm('This will delete this Quick Entry. This step cannot be reversed. Are you sure?')
|
||||
var sure = confirm(this.$t('deletequickentry'))
|
||||
if (sure) {
|
||||
store.dispatch('vehicles/deleteQuickEntry', { id: entry.id }).then((data) => {})
|
||||
}
|
||||
@@ -59,9 +59,9 @@ export default {
|
||||
|
||||
<template>
|
||||
<Layout>
|
||||
<h1 class="title">Quick Entries</h1>
|
||||
<h1 class="title">{{ $tc('quickentry', 2) }}</h1>
|
||||
<b-field>
|
||||
<b-switch v-if="unprocessedQuickEntries.length" v-model="showUnprocessedOnly">Show unprocessed only</b-switch>
|
||||
<b-switch v-if="unprocessedQuickEntries.length" v-model="showUnprocessedOnly">{{ $t('showunprocessed') }}</b-switch>
|
||||
</b-field>
|
||||
<div v-for="(chunk, index) in chunkedQuickEntries" :key="index" class="tile is-ancestor">
|
||||
<div v-for="entry in chunk" :key="entry.id" class="tile is-parent" :class="{ 'is-4': quickEntries.length <= 3 }">
|
||||
@@ -71,7 +71,7 @@ export default {
|
||||
<div class="card-header-title">
|
||||
{{ parseAndFormatDateTime(entry.createdAt) }}
|
||||
</div>
|
||||
<b-tag v-if="entry.processDate === null" class="is-align-content-center" type="is-primary">unprocessed</b-tag>
|
||||
<b-tag v-if="entry.processDate === null" class="is-align-content-center" type="is-primary">{{ $t('unprocessed') }}</b-tag>
|
||||
</div>
|
||||
<div class="card-image">
|
||||
<!-- prettier-ignore -->
|
||||
@@ -87,22 +87,22 @@ export default {
|
||||
>
|
||||
<footer class="card-footer">
|
||||
<router-link v-if="entry.processDate === null && vehicles.length" :to="`/vehicles/${vehicles[0].id}/fillup`" class="card-footer-item"
|
||||
>Create Fillup</router-link
|
||||
>{{ $t('addfillup') }}</router-link
|
||||
>
|
||||
<router-link v-if="entry.processDate === null && vehicles.length" :to="`/vehicles/${vehicles[0].id}/expense`" class="card-footer-item"
|
||||
>Create Expense</router-link
|
||||
>{{ $t('addexpense') }}</router-link
|
||||
>
|
||||
|
||||
<a v-if="entry.processDate === null" class="card-footer-item" @click="markProcessed(entry)">Mark Processed</a>
|
||||
<p v-else class="card-footer-item">Processed on {{ parseAndFormatDateTime(entry.processDate) }}</p>
|
||||
<a class="card-footer-item" type="is-danger" @click="deleteQuickEntry(entry)"> Delete</a>
|
||||
<a v-if="entry.processDate === null" class="card-footer-item" @click="markProcessed(entry)">{{ $t('processed') }}</a>
|
||||
<p v-else class="card-footer-item">{{ $t('processedon') }} {{ parseAndFormatDateTime(entry.processDate) }}</p>
|
||||
<a class="card-footer-item" type="is-danger" @click="deleteQuickEntry(entry)"> {{ $t('delete') }}</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!quickEntries.length" class="box">
|
||||
<p>No Quick Entries right now.</p>
|
||||
<p>{{ $tc('quickentry',0) }}</p>
|
||||
</div>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
@@ -44,6 +44,20 @@ 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() {
|
||||
@@ -92,7 +106,7 @@ export default {
|
||||
.dispatch(`utils/saveUserSettings`, { settings: this.settingsModel })
|
||||
.then((data) => {
|
||||
this.$buefy.toast.open({
|
||||
message: 'Settings saved successfully',
|
||||
message: this.$t('settingssaved'),
|
||||
type: 'is-success',
|
||||
duration: 3000,
|
||||
})
|
||||
@@ -109,34 +123,42 @@ export default {
|
||||
this.tryingToSave = false
|
||||
})
|
||||
},
|
||||
formatCurrency(option) {
|
||||
return `${option.namePlural} (${option.code})`
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Layout>
|
||||
<h1 class="title">Your Settings</h1>
|
||||
<h1 class="title">{{ $t('yoursettings') }}</h1>
|
||||
<div class="columns"
|
||||
><div class="column">
|
||||
<form class="box " @submit.prevent="saveSettings">
|
||||
<h1 class="subtitle">
|
||||
These will be used as default values whenever you create a new fillup or expense.
|
||||
{{ $t('settingdesc') }}
|
||||
</h1>
|
||||
<b-field label="Currency">
|
||||
<b-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 :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="Distance Unit">
|
||||
<b-field :label="$t('distanceunit')">
|
||||
<b-select v-model.number="settingsModel.distanceUnit" placeholder="Distance Unit" required expanded>
|
||||
<option v-for="(option, key) in distanceUnitMasters" :key="key" :value="key">
|
||||
{{ `${option.long} (${option.short})` }}
|
||||
{{ `${$t('unit.long.' + option.key)} (${$t('unit.short.' + option.key)})` }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field label="Date Format">
|
||||
<b-field :label="$t('dateformat')">
|
||||
<b-select v-model.number="settingsModel.dateFormat" placeholder="Date Format" required expanded>
|
||||
<option v-for="option in dateFormatMasters" :key="option" :value="option">
|
||||
{{ `${option}` }}
|
||||
@@ -145,25 +167,26 @@ export default {
|
||||
</b-field>
|
||||
<br />
|
||||
<b-field>
|
||||
<b-button tag="input" native-type="submit" :disabled="tryingToSave" type="is-primary" value="Save" expanded> </b-button>
|
||||
<b-button tag="input" native-type="submit" :disabled="tryingToSave" type="is-primary" :value="$t('save')" expanded> </b-button>
|
||||
</b-field>
|
||||
</form>
|
||||
</div>
|
||||
<div class="column">
|
||||
<form class="box" @submit.prevent="changePassword">
|
||||
<h1 class="subtitle">Change password</h1>
|
||||
<b-field label="Old Password">
|
||||
<h1 class="subtitle">{{ $t('changepassword') }}</h1>
|
||||
<b-field :label="$t('oldpassword')">
|
||||
<b-input v-model="changePassModel.old" required minlength="6" password-reveal type="password"></b-input>
|
||||
</b-field>
|
||||
<b-field label="New Password">
|
||||
<b-field :label="$t('newpassword')">
|
||||
<b-input v-model="changePassModel.new" required minlength="6" password-reveal type="password"></b-input>
|
||||
</b-field>
|
||||
<b-field label="Repeat New Password">
|
||||
<b-field :label="$t('repeatnewpassword')">
|
||||
<b-input v-model="changePassModel.renew" required minlength="6" password-reveal type="password"></b-input>
|
||||
</b-field>
|
||||
<p v-if="!passwordValid" class="help is-danger">Password values don't match</p>
|
||||
<p v-if="!passwordValid" class="help is-danger">{{ $t('passworddontmatch') }}</p>
|
||||
<b-field>
|
||||
<b-button tag="input" native-type="submit" :disabled="!passwordValid" type="is-primary" value="Change Password" expanded> </b-button>
|
||||
<b-button tag="input" native-type="submit" :disabled="!passwordValid" type="is-primary" :value="$t('changepassword')" expanded>
|
||||
</b-button>
|
||||
</b-field>
|
||||
</form>
|
||||
</div>
|
||||
@@ -171,48 +194,32 @@ export default {
|
||||
<hr />
|
||||
<div class="columns">
|
||||
<div class="twelve">
|
||||
<h3 class="title">More Info</h3>
|
||||
<p style="font-style: italic;">
|
||||
This project is under active development which means I release new updates very frequently. I will eventually build the version
|
||||
management/update checking mechanism. Until then it is recommended that you use something like watchtower which will automatically update
|
||||
your containers whenever I release a new version or periodically rebuild the container with the latest image manually.
|
||||
</p>
|
||||
<br />
|
||||
<h3 class="title">{{ $t('moreinfo') }}</h3>
|
||||
<table class="table is-hoverable">
|
||||
<tr>
|
||||
<td>Current Version</td>
|
||||
<td>2021.09.20</td>
|
||||
<td>{{ $t('currentversion') }}</td>
|
||||
<td>2022.07.06</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Website</td>
|
||||
<td><a href="https://github.com/akhilrex/hammond" target="_blank">https://github.com/akhilrex/hammond</a></td>
|
||||
<td><a href="https://github.com/alfhou/hammond" target="_blank">https://github.com/alfhou/hammond</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Found a bug</td>
|
||||
<td>{{ $t('foundabug') }}</td>
|
||||
<td
|
||||
><a
|
||||
href="https://github.com/akhilrex/hammond/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
><a href="https://github.com/alfhou/hammond/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc" target="_blank" rel="noopener noreferrer"
|
||||
>Report here</a
|
||||
></td
|
||||
>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Feature Request</td>
|
||||
<td>{{ $t('featurerequest') }}</td>
|
||||
<td
|
||||
><a
|
||||
href="https://github.com/akhilrex/hammond/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
><a href="https://github.com/alfhou/hammond/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc" target="_blank" rel="noopener noreferrer"
|
||||
>Request here</a
|
||||
></td
|
||||
>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Support the developer</td>
|
||||
<td><a href="https://www.buymeacoffee.com/akhilrex" target="_blank" rel="noopener noreferrer">Buy him a beer!</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,7 @@ export default {
|
||||
.dispatch(`utils/saveSettings`, { settings: this.settingsModel })
|
||||
.then((data) => {
|
||||
this.$buefy.toast.open({
|
||||
message: 'Settings saved successfully',
|
||||
message: this.$t('settingssaved'),
|
||||
type: 'is-success',
|
||||
duration: 3000,
|
||||
})
|
||||
@@ -63,32 +63,32 @@ export default {
|
||||
<div class="">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h1 class="title">Site Settings</h1>
|
||||
<h1 class="title">{{ $t('menu.sitesettings') }}</h1>
|
||||
<h1 class="subtitle">
|
||||
Update site level settings. These will be used as default values for new users.
|
||||
{{ $t('sitesettingdesc') }}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<form class="" @submit.prevent="saveSettings">
|
||||
<b-field label="Currency">
|
||||
<b-select v-model="settingsModel.currency" placeholder="Currency" required expanded>
|
||||
<b-field :label="this.$t('currency')">
|
||||
<b-select v-model="settingsModel.currency" :placeholder="this.$t('currency')" required expanded>
|
||||
<option v-for="option in currencyMasters" :key="option.code" :value="option.code">
|
||||
{{ `${option.namePlural} (${option.code})` }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field label="Distance Unit">
|
||||
<b-select v-model.number="settingsModel.distanceUnit" placeholder="Distance Unit" required expanded>
|
||||
<b-field :label="this.$t('distanceunit')">
|
||||
<b-select v-model.number="settingsModel.distanceUnit" :placeholder="this.$t('distanceunit')" required expanded>
|
||||
<option v-for="(option, key) in distanceUnitMasters" :key="key" :value="key">
|
||||
{{ `${option.long} (${option.short})` }}
|
||||
{{ `${$t('unit.long.' + option.key)} (${$t('unit.short.' + option.key)})` }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<br />
|
||||
<b-field>
|
||||
<b-button tag="input" native-type="submit" :disabled="tryingToSave" type="is-primary" value="Save" expanded> </b-button>
|
||||
<b-button tag="input" native-type="submit" :disabled="tryingToSave" type="is-primary" :value="this.$t('save')" expanded> </b-button>
|
||||
</b-field>
|
||||
</form>
|
||||
</Layout>
|
||||
|
||||
@@ -55,18 +55,18 @@ export default {
|
||||
},
|
||||
changeDisabledStatus(userId,status){
|
||||
this.$buefy.dialog.confirm({
|
||||
title: status?'Disable User':"Enable User",
|
||||
message: 'Are you sure you want to do this?',
|
||||
cancelText: 'Cancel',
|
||||
confirmText: 'Go Ahead',
|
||||
title: status ? this.$t('disable') : this.$t('enable'),
|
||||
message: this.$t('areyousure'),
|
||||
cancelText: this.$t('cancel'),
|
||||
confirmText: this.$t('confirm'),
|
||||
onConfirm: () => {
|
||||
|
||||
var url = `/api/users/${userId}/${status?"disable":"enable"}`
|
||||
var url = `/api/users/${userId}/${status ? "disable" : "enable"}`
|
||||
axios
|
||||
.post(url, {})
|
||||
.then((data) => {
|
||||
this.$buefy.toast.open({
|
||||
message: status?"User disabled successfully":'User enabled successfully',
|
||||
message: status ? this.$t('userdisabledsuccessfully') : this.$t('userenabledsuccessfully'),
|
||||
type: 'is-success',
|
||||
duration: 3000,
|
||||
})
|
||||
@@ -103,7 +103,7 @@ export default {
|
||||
if (success) {
|
||||
this.$buefy.toast.open({
|
||||
duration: 10000,
|
||||
message: 'User Created Successfully',
|
||||
message: this.$t('usercreatedsuccessfully'),
|
||||
position: 'is-bottom',
|
||||
type: 'is-success',
|
||||
})
|
||||
@@ -129,22 +129,22 @@ export default {
|
||||
<Layout>
|
||||
<div class="box">
|
||||
<div class="columns">
|
||||
<div class="column is-three-quarters"> <h1 class="title is-4">Users</h1> </div>
|
||||
<div class="column is-three-quarters"> <h1 class="title is-4">{{ $t('menu.users') }}</h1> </div>
|
||||
<div class="column is-one-quarter">
|
||||
<b-button type="is-primary" @click="showUserForm = true">Add User</b-button>
|
||||
<b-button type="is-primary" @click="showUserForm = true">{{ $t('adduser') }}</b-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showUserForm" class="box content">
|
||||
<h1 class="title">Create New User</h1>
|
||||
<h1 class="title">{{ $t('createnewuser') }}</h1>
|
||||
<form @submit.prevent="register">
|
||||
<b-field label="Name">
|
||||
<b-field :label="this.$t('name')">
|
||||
<b-input v-model="registerModel.name" required></b-input>
|
||||
</b-field>
|
||||
<b-field label="Email">
|
||||
<b-field :label="this.$t('email')">
|
||||
<b-input v-model="registerModel.email" type="email" required></b-input>
|
||||
</b-field>
|
||||
<b-field label="Password">
|
||||
<b-field :label="this.$t('password')">
|
||||
<b-input
|
||||
v-model="registerModel.password"
|
||||
type="password"
|
||||
@@ -153,56 +153,56 @@ export default {
|
||||
password-reveal
|
||||
></b-input>
|
||||
</b-field>
|
||||
<b-field label="Role">
|
||||
<b-select v-model.number="registerModel.role" placeholder="Role" required expanded>
|
||||
<b-field :label="this.$t('role')">
|
||||
<b-select v-model.number="registerModel.role" :placeholder="this.$t('placeholder')" required expanded>
|
||||
<option v-for="(option, key) in roleMasters" :key="key" :value="key">
|
||||
{{ `${option.long}` }}
|
||||
{{ `test` }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field label="Currency">
|
||||
<b-select v-model="registerModel.currency" placeholder="Currency" required expanded>
|
||||
<b-field :label="this.$t('currency')">
|
||||
<b-select v-model="registerModel.currency" :placeholder="this.$t('currency')" required expanded>
|
||||
<option v-for="option in currencyMasters" :key="option.code" :value="option.code">
|
||||
{{ `${option.namePlural} (${option.code})` }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field label="Distance Unit">
|
||||
<b-field :label="this.$t('distanceunit')">
|
||||
<b-select
|
||||
v-model.number="registerModel.distanceUnit"
|
||||
placeholder="Distance Unit"
|
||||
:placeholder="this.$t('distanceunit')"
|
||||
required
|
||||
expanded
|
||||
>
|
||||
<option v-for="(option, key) in distanceUnitMasters" :key="key" :value="key">
|
||||
{{ `${option.long} (${option.short})` }}
|
||||
{{ `${$t('unit.long.' + option.key)} (${$t('unit.short.' + option.key)})` }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<br />
|
||||
<div class="buttons">
|
||||
<b-button type="is-primary" native-type="submit" tag="input"></b-button>
|
||||
<b-button type="is-primary" native-type="submit" tag="input" :value="this.$t('save')"></b-button>
|
||||
|
||||
<b-button type="is-danger is-light" @click="resetUserForm">Cancel</b-button>
|
||||
<b-button type="is-danger is-light" @click="resetUserForm">{{ $t('cancel') }}</b-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<b-table :data="users" hoverable mobile-cards detail-key="id" paginated per-page="10" :row-class="(row, index) => row.isDisabled && 'is-disabled'">
|
||||
<b-table-column v-slot="props" field="name" label="Name">
|
||||
{{ `${props.row.name}` }} <template v-if="props.row.id === user.id">(You)</template>
|
||||
<b-table-column v-slot="props" field="name" :label="this.$t('name')">
|
||||
{{ `${props.row.name}` }} <template v-if="props.row.id === user.id">({{ $t('you') }})</template>
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" field="email" label="Email">
|
||||
<b-table-column v-slot="props" field="email" :label="this.$t('email')">
|
||||
{{ `${props.row.email}` }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" field="role" label="Role">
|
||||
{{ `${props.row.roleDetail.short}` }}
|
||||
<b-table-column v-slot="props" field="role" :label="this.$t('role')">
|
||||
{{ `${$t('roles.' + props.row.roleDetail.key)}` }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" field="createdAt" label="Created" sortable date>
|
||||
<b-table-column v-slot="props" field="createdAt" :label="this.$t('created')" sortable date>
|
||||
{{ formatDate(props.row.createdAt) }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props">
|
||||
<b-button type="is-success" v-if="props.row.isDisabled && props.row.roleDetail.long === 'USER'" @click="changeDisabledStatus(props.row.id, false)">Enable</b-button>
|
||||
<b-button type="is-danger" v-if="!props.row.isDisabled && props.row.roleDetail.long === 'USER'" @click="changeDisabledStatus(props.row.id, true)">Disable</b-button>
|
||||
<b-button type="is-success" v-if="props.row.isDisabled && props.row.roleDetail.key === 'USER'" @click="changeDisabledStatus(props.row.id, false)">{{ $t('enable') }}</b-button>
|
||||
<b-button type="is-danger" v-if="!props.row.isDisabled && props.row.roleDetail.key === 'USER'" @click="changeDisabledStatus(props.row.id, true)">{{ $t('disable') }}</b-button>
|
||||
</b-table-column>
|
||||
</b-table>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { parseAndFormatDate } from '@utils/format-date'
|
||||
import { mapState } from 'vuex'
|
||||
import { addDays, addMonths } from 'date-fns'
|
||||
import axios from 'axios'
|
||||
import currencyFormtter from 'currency-formatter'
|
||||
import currencyFormatter from 'currency-formatter'
|
||||
import store from '@state/store'
|
||||
import ShareVehicle from '@components/shareVehicle.vue'
|
||||
import MileageChart from '@components/mileageChart.vue'
|
||||
@@ -40,12 +40,12 @@ export default {
|
||||
stats: null,
|
||||
users: [],
|
||||
dateRangeOptions: [
|
||||
{ label: 'This week', value: 'this_week' },
|
||||
{ label: 'This month', value: 'this_month' },
|
||||
{ label: 'Past 30 days', value: 'past_30_days' },
|
||||
{ label: 'Past 3 months', value: 'past_3_months' },
|
||||
{ label: 'This year', value: 'this_year' },
|
||||
{ label: 'All Time', value: 'all_time' },
|
||||
{ label: this.$t('thisweek'), value: 'this_week' },
|
||||
{ label: this.$t('thismonth'), value: 'this_month' },
|
||||
{ label: this.$tc('pastxdays', 30), value: 'past_30_days' },
|
||||
{ label: this.$tc('pastxmonths', 3), value: 'past_3_months' },
|
||||
{ label: this.$t('thisyear'), value: 'this_year' },
|
||||
{ label: this.$t('alltime'), value: 'all_time' },
|
||||
],
|
||||
dateRangeOption: 'past_30_days',
|
||||
}
|
||||
@@ -61,32 +61,32 @@ export default {
|
||||
return this.stats.map((x) => {
|
||||
return [
|
||||
{
|
||||
label: 'Currency',
|
||||
label: this.$t('currency'),
|
||||
value: x.currency,
|
||||
},
|
||||
{
|
||||
label: 'Total Expenditure',
|
||||
label: this.$t('totalexpenses'),
|
||||
value: this.formatCurrency(x.expenditureTotal, x.currency),
|
||||
},
|
||||
{
|
||||
label: 'Fillup Costs',
|
||||
label: this.$t('fillupcost'),
|
||||
value: `${this.formatCurrency(x.expenditureFillups, x.currency)} (${x.countFillups})`,
|
||||
},
|
||||
{
|
||||
label: 'Other Expenses',
|
||||
label: this.$t('otherexpenses'),
|
||||
value: `${this.formatCurrency(x.expenditureExpenses, x.currency)} (${x.countExpenses})`,
|
||||
},
|
||||
{
|
||||
label: 'Avg Fillup Expense',
|
||||
label: this.$t('avgfillupexpense'),
|
||||
value: `${this.formatCurrency(x.avgFillupCost, x.currency)}`,
|
||||
},
|
||||
{
|
||||
label: 'Avg Fillup Qty',
|
||||
value: `${x.avgFuelQty} ${this.vehicle.fuelUnitDetail.short}`,
|
||||
label: this.$t('avgfillupqty'),
|
||||
value: `${x.avgFuelQty} ${this.$t('unit.short.' + this.vehicle.fuelUnitDetail.key)}`,
|
||||
},
|
||||
{
|
||||
label: 'Avg Fuel Cost',
|
||||
value: `${this.formatCurrency(x.avgFuelPrice, x.currency)} per ${this.vehicle.fuelUnitDetail.short}`,
|
||||
label: this.$t('avgfuelcost'),
|
||||
value: this.$t('per', {'0': this.formatCurrency(x.avgFuelPrice, x.currency), '1': this.$t('unit.short.' + this.vehicle.fuelUnitDetail.key)}),
|
||||
},
|
||||
]
|
||||
})
|
||||
@@ -199,14 +199,21 @@ export default {
|
||||
return
|
||||
}
|
||||
this.tryingToUpload = true
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', this.file, this.file.name)
|
||||
formData.append('title', this.title)
|
||||
axios
|
||||
.post(`/api/vehicles/${this.vehicle.id}/attachments`, formData)
|
||||
// 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,
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
this.$buefy.toast.open({
|
||||
message: 'Quick Entry Created Successfully',
|
||||
message: 'File uploaded Successfully',
|
||||
type: 'is-success',
|
||||
duration: 3000,
|
||||
})
|
||||
@@ -233,7 +240,7 @@ export default {
|
||||
if (!currencyCode) {
|
||||
currencyCode = this.me.currency
|
||||
}
|
||||
return currencyFormtter.format(number, { code: currencyCode })
|
||||
return currencyFormatter.format(number, { code: currencyCode })
|
||||
},
|
||||
columnTdAttrs(row, column) {
|
||||
return null
|
||||
@@ -293,18 +300,18 @@ export default {
|
||||
<template>
|
||||
<Layout>
|
||||
<div class="columns box">
|
||||
<div class="column is-two-thirds" :class="isMobile ? 'has-text-centered' : ''">
|
||||
<div class="column is-one-half" :class="isMobile ? 'has-text-centered' : ''">
|
||||
<p class="title">{{ vehicle.nickname }} - {{ vehicle.registration }}</p>
|
||||
<p class="subtitle">
|
||||
{{ [vehicle.make, vehicle.model, vehicle.fuelTypeDetail.long].join(' | ') }}
|
||||
{{ [vehicle.make, vehicle.model, this.$t('fuel.' + vehicle.fuelTypeDetail.key)].join(' | ') }}
|
||||
|
||||
<template v-if="users.length > 1">
|
||||
| Shared with :
|
||||
| {{ $t("sharedwith") }} :
|
||||
{{
|
||||
users
|
||||
.map((x) => {
|
||||
if (x.userId === me.id) {
|
||||
return 'You'
|
||||
return this.$t('you')
|
||||
} else {
|
||||
return x.name
|
||||
}
|
||||
@@ -314,13 +321,13 @@ export default {
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-one-third buttons has-text-centered">
|
||||
<b-button type="is-primary" tag="router-link" :to="`/vehicles/${vehicle.id}/fillup`">Add Fillup</b-button>
|
||||
<b-button type="is-primary" tag="router-link" :to="`/vehicles/${vehicle.id}/expense`">Add Expense</b-button>
|
||||
<div :class="(!isMobile ? 'has-text-right ' : '') + 'column is-one-half buttons'">
|
||||
<b-button type="is-primary" tag="router-link" :to="`/vehicles/${vehicle.id}/fillup`">{{ this.$t('addfillup') }}</b-button>
|
||||
<b-button type="is-primary" tag="router-link" :to="`/vehicles/${vehicle.id}/expense`">{{ this.$t('addexpense') }}</b-button>
|
||||
<b-button
|
||||
v-if="vehicle.isOwner"
|
||||
tag="router-link"
|
||||
title="Edit Vehicle"
|
||||
:title="$t('editvehicle')"
|
||||
:to="{
|
||||
name: 'vehicle-edit',
|
||||
props: { vehicle: vehicle },
|
||||
@@ -329,10 +336,10 @@ export default {
|
||||
>
|
||||
<b-icon pack="fas" icon="edit" type="is-info"> </b-icon
|
||||
></b-button>
|
||||
<b-button v-if="vehicle.isOwner" title="Share vehicle" @click="showShareVehicleModal">
|
||||
<b-button v-if="vehicle.isOwner" :title="$t('sharevehicle')" @click="showShareVehicleModal">
|
||||
<b-icon pack="fas" icon="user-friends" type="is-info"> </b-icon
|
||||
></b-button>
|
||||
<b-button v-if="vehicle.isOwner" title="Delete Vehicle" @click="deleteVehicle">
|
||||
<b-button v-if="vehicle.isOwner" :title="$t('deletevehicle')" @click="deleteVehicle">
|
||||
<b-icon pack="fas" icon="trash" type="is-danger"> </b-icon
|
||||
></b-button>
|
||||
</div>
|
||||
@@ -346,45 +353,45 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<h1 class="title is-4">Past Fillups</h1>
|
||||
<h1 class="title is-4">{{ $t('pastfillups') }}</h1>
|
||||
|
||||
<b-table :data="fillups" hoverable mobile-cards :detailed="isMobile" detail-key="id" paginated per-page="10">
|
||||
<b-table-column v-slot="props" field="date" label="Date" :td-attrs="columnTdAttrs" sortable date>
|
||||
<b-table-column v-slot="props" field="date" :label="this.$t('date')" :td-attrs="columnTdAttrs" sortable date>
|
||||
{{ formatDate(props.row.date) }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" field="fuelSubType" label="Fuel Sub Type" :td-attrs="columnTdAttrs">
|
||||
<b-table-column v-slot="props" field="fuelSubType" :label="this.$t('fuelsubtype')" :td-attrs="columnTdAttrs">
|
||||
{{ props.row.fuelSubType }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" field="fuelQuantity" label="Qty." :td-attrs="hiddenMobile" numeric>
|
||||
{{ `${props.row.fuelQuantity} ${props.row.fuelUnitDetail.short}` }}
|
||||
<b-table-column v-slot="props" field="fuelQuantity" :label="this.$t('quantity')" :td-attrs="hiddenMobile" numeric>
|
||||
{{ `${props.row.fuelQuantity} ${$t('unit.short.' + props.row.fuelUnitDetail.key)}` }}
|
||||
</b-table-column>
|
||||
<b-table-column
|
||||
v-slot="props"
|
||||
field="perUnitPrice"
|
||||
:label="'Price per ' + vehicle.fuelUnitDetail.short"
|
||||
:label="this.$t('per', { '0': this.$t('price'), '1': this.$t('unit.short.' + vehicle.fuelUnitDetail.key) })"
|
||||
:td-attrs="hiddenMobile"
|
||||
numeric
|
||||
sortable
|
||||
>
|
||||
{{ `${formatCurrency(props.row.perUnitPrice, props.row.currency)}` }}
|
||||
</b-table-column>
|
||||
<b-table-column v-if="isMobile" v-slot="props" field="totalAmount" label="Total" :td-attrs="hiddenDesktop" sortable numeric>
|
||||
{{ `${me.currency} ${props.row.totalAmount}` }} ({{ `${props.row.fuelQuantity} ${props.row.fuelUnitDetail.short}` }} @
|
||||
<b-table-column v-if="isMobile" v-slot="props" field="totalAmount" :label="this.$t('total')" :td-attrs="hiddenDesktop" sortable numeric>
|
||||
{{ `${me.currency} ${props.row.totalAmount}` }} ({{ `${props.row.fuelQuantity} ${$t('unit.short.' + props.row.fuelUnitDetail.key)}` }} @
|
||||
{{ `${me.currency} ${props.row.perUnitPrice}` }})
|
||||
</b-table-column>
|
||||
<b-table-column v-if="!isMobile" v-slot="props" field="totalAmount" label="Total" :td-attrs="hiddenMobile" sortable numeric>
|
||||
<b-table-column v-if="!isMobile" v-slot="props" field="totalAmount" :label="this.$t('total')" :td-attrs="hiddenMobile" sortable numeric>
|
||||
{{ `${formatCurrency(props.row.totalAmount, props.row.currency)}` }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" width="20" field="isTankFull" label="Tank Full" :td-attrs="hiddenMobile">
|
||||
<b-table-column v-slot="props" width="20" field="isTankFull" :label="this.$t('fulltank')" :td-attrs="hiddenMobile">
|
||||
<b-icon pack="fas" :icon="props.row.isTankFull ? 'check' : 'times'" type="is-info"> </b-icon>
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" field="odoReading" label="Odometer Reading" :td-attrs="hiddenMobile" numeric>
|
||||
{{ `${props.row.odoReading} ${me.distanceUnitDetail.short}` }}
|
||||
<b-table-column v-slot="props" field="odoReading" :label="this.$t('odometer')" :td-attrs="hiddenMobile" numeric>
|
||||
{{ `${props.row.odoReading} ${$t('unit.short.' + me.distanceUnitDetail.key)}` }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" field="fillingStation" label="Fillup Station" :td-attrs="hiddenMobile">
|
||||
<b-table-column v-slot="props" field="fillingStation" :label="this.$t('gasstation')" :td-attrs="hiddenMobile">
|
||||
{{ `${props.row.fillingStation}` }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" field="userId" label="By" :td-attrs="hiddenMobile">
|
||||
<b-table-column v-slot="props" field="userId" :label="this.$t('by')" :td-attrs="hiddenMobile">
|
||||
{{ `${props.row.user.name}` }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props">
|
||||
@@ -399,11 +406,11 @@ export default {
|
||||
>
|
||||
<b-icon pack="fas" icon="edit" type="is-info"> </b-icon
|
||||
></b-button>
|
||||
<b-button type="is-ghost" title="Delete this fillup" @click="deleteFillup(props.row.id)">
|
||||
<b-button type="is-ghost" :title="$t('deletefillup')" @click="deleteFillup(props.row.id)">
|
||||
<b-icon pack="fas" icon="trash" type="is-danger"> </b-icon
|
||||
></b-button>
|
||||
</b-table-column>
|
||||
<template v-slot:empty> No Fillups so far</template>
|
||||
<template v-slot:empty> {{ $t('nofillups') }}</template>
|
||||
<template v-slot:detail="props">
|
||||
<p>{{ props.row.id }}</p>
|
||||
</template>
|
||||
@@ -411,25 +418,25 @@ export default {
|
||||
</div>
|
||||
<br />
|
||||
<div class="box">
|
||||
<h1 class="title is-4">Past Expenses</h1>
|
||||
<h1 class="title is-4">{{ $t('expenses') }}</h1>
|
||||
|
||||
<b-table :data="expenses" hoverable mobile-cards paginated per-page="10">
|
||||
<b-table-column v-slot="props" field="date" label="Date" :td-attrs="columnTdAttrs" date>
|
||||
<b-table-column v-slot="props" field="date" :label="this.$t('date')" :td-attrs="columnTdAttrs" date>
|
||||
{{ formatDate(props.row.date) }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" field="expenseType" label="Expense Type">
|
||||
<b-table-column v-slot="props" field="expenseType" :label="this.$t('expensetype')">
|
||||
{{ `${props.row.expenseType}` }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="amount" label="Total" :td-attrs="hiddenMobile" sortable numeric>
|
||||
<b-table-column v-slot="props" field="amount" :label="this.$t('total')" :td-attrs="hiddenMobile" sortable numeric>
|
||||
{{ `${formatCurrency(props.row.amount, props.row.currency)}` }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="odoReading" label="Odometer Reading" :td-attrs="columnTdAttrs" numeric>
|
||||
{{ `${props.row.odoReading} ${me.distanceUnitDetail.short}` }}
|
||||
<b-table-column v-slot="props" field="odoReading" :label="this.$t('odometer')" :td-attrs="columnTdAttrs" numeric>
|
||||
{{ `${props.row.odoReading} ${$t('unit.short.' + me.distanceUnitDetail.key)}` }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="userId" label="By" :td-attrs="columnTdAttrs">
|
||||
<b-table-column v-slot="props" field="userId" :label="this.$t('by')" :td-attrs="columnTdAttrs">
|
||||
{{ `${props.row.user.name}` }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props">
|
||||
@@ -444,20 +451,20 @@ export default {
|
||||
>
|
||||
<b-icon pack="fas" icon="edit" type="is-info"> </b-icon
|
||||
></b-button>
|
||||
<b-button type="is-ghost" title="Delete this expense" @click="deleteExpense(props.row.id)">
|
||||
<b-button type="is-ghost" :title="$t('deleteexpense')" @click="deleteExpense(props.row.id)">
|
||||
<b-icon pack="fas" icon="trash" type="is-danger"> </b-icon
|
||||
></b-button>
|
||||
</b-table-column>
|
||||
<template v-slot:empty> No Expenses so far</template>
|
||||
<template v-slot:empty> {{ $t('noexpenses') }}</template>
|
||||
</b-table>
|
||||
</div>
|
||||
<br />
|
||||
<div class="box">
|
||||
<div class="columns">
|
||||
<div class="column is-three-quarters"> <h1 class="title is-4">Attachments</h1></div>
|
||||
<div class="column is-three-quarters"> <h1 class="title is-4">{{ $t('attachments') }}</h1></div>
|
||||
<div class="column buttons">
|
||||
<b-button type="is-primary" @click="showAttachmentForm = true">
|
||||
Add Attachment
|
||||
{{ $t('addattachment') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -471,7 +478,7 @@ export default {
|
||||
<b-upload v-model="file" class="file-label" required>
|
||||
<span class="file-cta">
|
||||
<b-icon class="file-icon" icon="upload"></b-icon>
|
||||
<span class="file-label">Choose File</span>
|
||||
<span class="file-label">{{ $t('choosefile') }}</span>
|
||||
</span>
|
||||
<span v-if="file" class="file-name" :class="isMobile ? 'file-name-mobile' : 'file-name-desktop'">
|
||||
{{ file.name }}
|
||||
@@ -479,7 +486,7 @@ export default {
|
||||
</b-upload>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-input v-model="title" required placeholder="Label for this file"></b-input>
|
||||
<b-input v-model="title" required :placeholder="this.$t('labelforfile')"></b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field class="buttons">
|
||||
@@ -503,24 +510,24 @@ export default {
|
||||
</div>
|
||||
|
||||
<b-table :data="attachments" hoverable mobile-cards>
|
||||
<b-table-column v-slot="props" field="title" label="Title" :td-attrs="columnTdAttrs">
|
||||
<b-table-column v-slot="props" field="title" :label="this.$t('title')" :td-attrs="columnTdAttrs">
|
||||
{{ `${props.row.title}` }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="originalName" label="Name" :td-attrs="columnTdAttrs">
|
||||
<b-table-column v-slot="props" field="originalName" :label="this.$t('name')" :td-attrs="columnTdAttrs">
|
||||
{{ `${props.row.originalName}` }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" field="id" label="Download" :td-attrs="columnTdAttrs">
|
||||
<b-table-column v-slot="props" field="id" :label="this.$t('download')" :td-attrs="columnTdAttrs">
|
||||
<b-button tag="a" :href="`/api/attachments/${props.row.id}/file?access_token=${currentUser.token}`" :download="props.row.originalName">
|
||||
<b-icon type="is-primary" icon="download"></b-icon>
|
||||
</b-button>
|
||||
</b-table-column>
|
||||
<template v-slot:empty> No Attachments so far</template>
|
||||
<template v-slot:empty> {{ $t('noattachments') }}</template>
|
||||
</b-table>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="columns">
|
||||
<div class="column" :class="isMobile ? 'has-text-centered' : ''"> <h1 class="title">Stats</h1></div>
|
||||
<div class="column" :class="isMobile ? 'has-text-centered' : ''"> <h1 class="title">{{ $t('statistics') }}</h1></div>
|
||||
<div class="column">
|
||||
<b-select v-model="dateRangeOption" class="is-pulled-right is-medium">
|
||||
<option v-for="option in dateRangeOptions" :key="option.value" :value="option.value">
|
||||
|
||||
24870
ui/yarn.lock
24870
ui/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user