Compare commits
203 Commits
update-git
...
v0.0.24
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b4f773fe0 | ||
|
|
79a9357a62 | ||
|
|
9e34b6c6f8 | ||
|
|
c4389f6a80 | ||
|
|
dd1d646a8a | ||
|
|
bd4b389139 | ||
|
|
996b146d2c | ||
|
|
c5fdfd4ca9 | ||
|
|
6804be4f7a | ||
|
|
79e4f6ab4d | ||
|
|
5cecc82d95 | ||
|
|
298f58a738 | ||
|
|
793bac40a3 | ||
|
|
9cb65e4cda | ||
|
|
a67c460434 | ||
|
|
355739a966 | ||
|
|
71bbd6b443 | ||
|
|
6e59156c24 | ||
|
|
d786e1ad08 | ||
|
|
bb0dc22630 | ||
|
|
d81701c4fa | ||
|
|
c4f5d93b8b | ||
|
|
6132aa36af | ||
|
|
49cc485866 | ||
|
|
4ed813cd2e | ||
|
|
9bcc2cc2a0 | ||
|
|
d3d8cc268d | ||
|
|
62dcf663ff | ||
|
|
b016dadb9d | ||
|
|
b962565ed6 | ||
|
|
20b0e246fd | ||
|
|
f1a7a053f4 | ||
|
|
359f35c53f | ||
|
|
ea3423d32a | ||
|
|
6542a3bb28 | ||
|
|
35d2f1ca0b | ||
|
|
565d5701be | ||
|
|
2eb78ab73c | ||
|
|
edf4647549 | ||
|
|
e87a348b90 | ||
|
|
c15b22c71a | ||
|
|
1c9f9c7803 | ||
|
|
89bdfdefd4 | ||
|
|
2974fd783f | ||
|
|
bd22b5a497 | ||
|
|
8f6408a92b | ||
|
|
20bc28fffa | ||
|
|
3c89e75a34 | ||
|
|
5594356166 | ||
|
|
cea08a59be | ||
|
|
f16ed1a39f | ||
|
|
66032fcf55 | ||
|
|
34a9d56726 | ||
|
|
32a5d932c6 | ||
|
|
a0880ad5b6 | ||
|
|
17e8e5914e | ||
|
|
a14f298822 | ||
|
|
afe4078897 | ||
|
|
01f9b455cf | ||
|
|
c9c06f865c | ||
|
|
e2c14afc99 | ||
|
|
415d0abc83 | ||
|
|
d32fd8073d | ||
|
|
d6eab70ca6 | ||
|
|
cc82536970 | ||
|
|
094cf0d7c9 | ||
|
|
24f295c632 | ||
|
|
e9812e7e27 | ||
|
|
785ff9a089 | ||
|
|
b99c3921d7 | ||
|
|
d64777dca6 | ||
|
|
5208437ec2 | ||
|
|
654087b990 | ||
|
|
d294db34fc | ||
|
|
9f9f90fd1d | ||
|
|
051e3476a7 | ||
|
|
845dcb242a | ||
|
|
df165dae6e | ||
|
|
e389a9ac2a | ||
|
|
e2e4169787 | ||
|
|
2a8325c6ce | ||
|
|
cd2e9ebc61 | ||
|
|
1ac3a8b31b | ||
|
|
f07922763b | ||
|
|
9da21b2192 | ||
|
|
a8c85bcd7d | ||
|
|
d597a4ed30 | ||
|
|
45456280b4 | ||
|
|
1d5794e344 | ||
|
|
cd558ba744 | ||
|
|
3a2c82c789 | ||
|
|
d196536d74 | ||
|
|
630a7f2ec6 | ||
|
|
f2bc01289a | ||
|
|
a8d2b37087 | ||
|
|
7436399d90 | ||
|
|
bc3e1f0982 | ||
|
|
d429fa34bd | ||
|
|
5095cb4c61 | ||
|
|
e0df7ee80e | ||
|
|
431de8c3eb | ||
|
|
41793784ea | ||
|
|
85b5ad28bf | ||
|
|
b386012e13 | ||
|
|
df2d7288df | ||
|
|
7a6f796561 | ||
|
|
aee52d0594 | ||
|
|
f8b1de8d15 | ||
|
|
a7896340e1 | ||
|
|
f2a3bb2e9f | ||
|
|
d343619f13 | ||
|
|
adce0efa8b | ||
|
|
fc6f4bc00d | ||
|
|
a16bcf850f | ||
|
|
4ace38f8f3 | ||
|
|
63e330ffb0 | ||
|
|
fc9796081e | ||
|
|
440913af9c | ||
|
|
66d01afe6e | ||
|
|
ad4a399dc8 | ||
|
|
2137bf7702 | ||
|
|
47bdf7b505 | ||
|
|
669bffa955 | ||
|
|
05c5381a06 | ||
|
|
e623e3ad1a | ||
|
|
c43a2f639a | ||
|
|
e66e5b7724 | ||
|
|
adfd70fe98 | ||
|
|
ebebcacdc9 | ||
|
|
3299c13181 | ||
|
|
2661f8ae36 | ||
|
|
091cfdcc99 | ||
|
|
9771dc4c25 | ||
|
|
8e894844a3 | ||
|
|
4a55879ad8 | ||
|
|
9dab3d124d | ||
|
|
a89ca5e46a | ||
|
|
f96638d913 | ||
|
|
08f2a3547e | ||
|
|
126aff7231 | ||
|
|
ba276975f3 | ||
|
|
7d4b763e48 | ||
|
|
ee964a630e | ||
|
|
c588e34b2e | ||
|
|
6871a40380 | ||
|
|
0035897f21 | ||
|
|
19680b1cc1 | ||
|
|
e6e90d9bef | ||
|
|
311ac7579a | ||
|
|
47810a8c88 | ||
|
|
f9d24bc7ef | ||
|
|
5aabeda6ba | ||
|
|
bb68c8c504 | ||
|
|
961ec30065 | ||
|
|
0b450dc462 | ||
|
|
d3ce6920ad | ||
|
|
afdfa31148 | ||
|
|
2d24c4b9e6 | ||
|
|
c0db2c5c1e | ||
|
|
2ecb113918 | ||
|
|
966cac280f | ||
|
|
84cba2c7f2 | ||
|
|
1432499a90 | ||
|
|
d0704c8c6a | ||
|
|
b86795bcb6 | ||
|
|
1ccdce9ee3 | ||
|
|
5cfaf8c933 | ||
|
|
987f035198 | ||
|
|
ab94997dd6 | ||
|
|
0b715ef840 | ||
|
|
c00c6bc776 | ||
|
|
a5d4dface8 | ||
|
|
7cb9a43dfe | ||
|
|
05bb22fe4e | ||
|
|
69352af906 | ||
|
|
7a8916c9cd | ||
|
|
e471e80617 | ||
|
|
1ee032b664 | ||
|
|
cea2566e2a | ||
|
|
dcb58bbbdb | ||
|
|
24105dbaaf | ||
|
|
e3846634b5 | ||
|
|
fd52c23636 | ||
|
|
43d1ca0c66 | ||
|
|
fb742f19a7 | ||
|
|
2749707546 | ||
|
|
f1bf36bcb9 | ||
|
|
3322e2f6bd | ||
|
|
9ef929dbd5 | ||
|
|
dc33aaad49 | ||
|
|
15cf09f326 | ||
|
|
1e099ec8b6 | ||
|
|
e8f7815d8d | ||
|
|
bfaebf17d0 | ||
|
|
20a1421576 | ||
|
|
8410674841 | ||
|
|
74e52c3e87 | ||
|
|
1857bb0518 | ||
|
|
a729b5eb12 | ||
|
|
d9a99d432c | ||
|
|
acba47fede | ||
|
|
04f45fe385 | ||
|
|
fca2c3e7fa |
66
.github/workflows/hub.yml
vendored
66
.github/workflows/hub.yml
vendored
@@ -1,57 +1,49 @@
|
||||
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
|
||||
uses: docker/setup-qemu-action@v1
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
-
|
||||
name: Set up build cache
|
||||
uses: actions/cache@v2
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@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
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
- name: Available platforms
|
||||
run: echo ${{ steps.qemu.outputs.platforms }}
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Parse the git tag
|
||||
id: get_tag
|
||||
run: echo ::set-output name=TAG::$(echo $GITHUB_REF | cut -d / -f 3)
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
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
|
||||
uses: docker/build-push-action@v2
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
#platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||
#platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
# 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
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Don't track .vscode directory
|
||||
.vscode
|
||||
!.vscode/launch.json
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@@ -1,4 +1,4 @@
|
||||
ARG GO_VERSION=1.16.2
|
||||
ARG GO_VERSION=1.20.6
|
||||
FROM golang:${GO_VERSION}-alpine AS builder
|
||||
RUN apk update && apk add alpine-sdk git && rm -rf /var/cache/apk/*
|
||||
RUN mkdir -p /api
|
||||
@@ -9,23 +9,25 @@ RUN go mod download
|
||||
COPY ./server .
|
||||
RUN go build -o ./app ./main.go
|
||||
|
||||
FROM node:latest as build-stage
|
||||
FROM node:16-alpine as build-stage
|
||||
WORKDIR /app
|
||||
COPY ./ui/package*.json ./
|
||||
RUN apk add --no-cache autoconf automake build-base nasm libc6-compat python3 py3-pip make g++ libpng-dev zlib-dev pngquant
|
||||
|
||||
RUN npm install
|
||||
COPY ./ui .
|
||||
RUN npm run build
|
||||
|
||||
|
||||
FROM alpine:latest
|
||||
LABEL org.opencontainers.image.source="https://github.com/akhilrex/hammond"
|
||||
LABEL org.opencontainers.image.source="https://github.com/alfhou/hammond"
|
||||
ENV CONFIG=/config
|
||||
ENV DATA=/assets
|
||||
ENV UID=998
|
||||
ENV PID=100
|
||||
ENV GIN_MODE=release
|
||||
VOLUME ["/config", "/assets"]
|
||||
RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/*
|
||||
RUN apk update && apk add ca-certificates tzdata && rm -rf /var/cache/apk/*
|
||||
RUN mkdir -p /config; \
|
||||
mkdir -p /assets; \
|
||||
mkdir -p /api
|
||||
@@ -36,4 +38,4 @@ COPY --from=builder /api/app .
|
||||
#COPY dist ./dist
|
||||
COPY --from=build-stage /app/dist ./dist
|
||||
EXPOSE 3000
|
||||
ENTRYPOINT ["./app"]
|
||||
ENTRYPOINT ["./app"]
|
||||
|
||||
113
README.md
113
README.md
@@ -1,26 +1,15 @@
|
||||
[![Contributors][contributors-shield]][contributors-url] [![Forks][forks-shield]][forks-url] [![Stargazers][stars-shield]][stars-url] [![Issues][issues-shield]][issues-url] [![MIT License][license-shield]][license-url] [![LinkedIn][linkedin-shield]][linkedin-url]
|
||||
|
||||
<!-- PROJECT LOGO -->
|
||||
<br />
|
||||
<p align="center">
|
||||
<!-- <a href="https://github.com/akhilrex/hammond">
|
||||
<img src="images/logo.png" alt="Logo" width="80" height="80">
|
||||
</a> -->
|
||||
|
||||
<h1 align="center" style="margin-bottom:0">Hammond</h1>
|
||||
<p align="center">Current Version - 2021.09.20</p>
|
||||
|
||||
<p align="center">
|
||||
A self-hosted vehicle expense tracking system with support for multiple users.
|
||||
<br />
|
||||
<a href="https://github.com/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 +24,7 @@
|
||||
- [Built With](#built-with)
|
||||
- [Features](#features)
|
||||
- [Installation](#installation)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
- [Roadmap](#roadmap)
|
||||
- [Contact](#contact)
|
||||
@@ -43,18 +33,22 @@
|
||||
|
||||
## About The Project
|
||||
|
||||
Hammond is a self hosted vehicle management system to track fuel and other expenses related to all of your vehicles. It supports multiple users sharing multiple vehicles. It is the logical successor to Clarkson which has not been updated for quite some time now.
|
||||
Hammond is a self hosted vehicle management system to track fuel and other
|
||||
expenses related to all of your vehicles.
|
||||
It supports multiple users sharing multiple vehicles.
|
||||
It is the logical successor to Clarkson which has not been updated for quite some time now.
|
||||
This repo is again a fork of akhilrex's great [project](https://github.com/akhilrex/hammond).
|
||||
|
||||
_Developers Note: This project is under active development which means I release new updates very frequently. It is recommended that you use something like [watchtower](https://github.com/containrrr/watchtower) which will automatically update your containers whenever I release a new version or periodically rebuild the container with the latest image manually._
|
||||
|
||||
__Also check out my other self-hosted, open-source solution - [Podgrab](https://github.com/akhilrex/podgrab) - Podcast download and archive manager and player.__
|
||||
### Motivation and Developer Notes
|
||||
|
||||
I was looking for a fuel tracking system and stumbled upon Clarkson. Although it did most of what I needed it has not been updated for quite a lot of time. Since I had some bandwidth available as my previous open source project [Podgrab](http://github.com/akhilrex/podgrab) had become quite stable now, my first thought was to contribute to the Clarkson project only. I soon realized that the architecture that Clarkson had used was not really be that extensible now and would warrant a complete rewrite only. So I decided to build Hammond - The successor to Clarkson.
|
||||
As mentioned, this project is a fork of
|
||||
akhilrex's [project](https://github.com/akhilrex/hammond) which is no longer active.
|
||||
To prevent the same from happeing to this project, we are seeking to add more
|
||||
maintainers/collaborators who have access to merge PRs.
|
||||
|
||||
The current version of Hammond is written using GO for backend and Vuejs for the front end. Originally I had thought of using the same tech stack for both frontend and the backend so that it became easier for users and other developers to use, deploy and contribute. Which is why the first version of Hammond has a NestJS backend complete with all the bells and whistles (GraphQL, Prisma and what nots). But I eventually decided to rebuild the backend in GO just to keep the container size small. No matter how much you can optimize the sheer size of the node_modules will always add bulk to your containers. I host all my tools on my Raspberry Pi. It only makes sense to keep the container size as small as possible.
|
||||
We are trying our best to update with new features and feedback is very welcome.
|
||||
|
||||
Also I had initially thought of a 2 container approach (1 for backend and 1 for the frontend) so that they can be independently maintained and updated. I eventually decided against this idea for the sake of simplicity. Although it is safe to assume that most self-hosters are fairly tech capable it still is much better to have a single container that you can fire and forget.
|
||||
The project is written using Go for the backend and Vuejs for the front end.
|
||||
|
||||
![Product Name Screen Shot][product-screenshot] [More Screenshots](Screenshots.md)
|
||||
|
||||
@@ -78,7 +72,7 @@ Also I had initially thought of a 2 container approach (1 for backend and 1 for
|
||||
- Save attachment against vehicles
|
||||
- Quick Entries (take a photo of a receipt or pump screen to make entry later)
|
||||
- Vehicle level and overall reporting
|
||||
- Import from Fuelly (more apps coming soon)
|
||||
- Import from Fuelly and Drivvo
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -89,24 +83,25 @@ The easiest way to run Hammond is to run it as a docker container.
|
||||
Simple setup without mounted volumes (for testing and evaluation)
|
||||
|
||||
```sh
|
||||
docker run -d -p 3000:3000 --name=hammond akhilrex/hammond
|
||||
docker run -d -p 3000:3000 --name=hammond alfhou/hammond
|
||||
```
|
||||
|
||||
Binding local volumes to the container
|
||||
|
||||
```sh
|
||||
docker run -d -p 3000:3000 --name=hammond -v "/host/path/to/assets:/assets" -v "/host/path/to/config:/config" akhilrex/hammond
|
||||
docker run -d -p 3000:3000 --name=hammond -v "/host/path/to/assets:/assets" -v "/host/path/to/config:/config" alfhou/hammond
|
||||
```
|
||||
|
||||
### Using Docker-Compose
|
||||
|
||||
Modify the docker compose file provided [here](https://github.com/akhilrex/hammond/blob/master/docker-compose.yml) to update the volume and port binding and run the following command
|
||||
Modify the docker compose file provided [here](https://github.com/alfhou/hammond/blob/master/docker-compose.yml)
|
||||
to update the volume and port binding and run the following command
|
||||
|
||||
```yaml
|
||||
version: '2.1'
|
||||
services:
|
||||
hammond:
|
||||
image: akhilrex/hammond
|
||||
image: alfhou/hammond
|
||||
container_name: hammond
|
||||
volumes:
|
||||
- /path/to/config:/config
|
||||
@@ -120,9 +115,27 @@ services:
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Install on Kubernetes
|
||||
|
||||
You can install Hammond on Kubernetes by using Helm. The
|
||||
[Helm chart for Hammond](https://github.com/djjudas21/charts/tree/main/charts/hammond)
|
||||
is maintained by djjudas21.
|
||||
|
||||
Check out the default [`values.yaml`](https://github.com/djjudas21/charts/blob/main/charts/hammond/values.yaml)
|
||||
to see what you can override.
|
||||
|
||||
```console
|
||||
helm repo add djjudas21 https://djjudas21.github.io/charts/
|
||||
helm repo update djjudas21
|
||||
helm install djjudas21/hammond
|
||||
```
|
||||
|
||||
### Build from Source / Ubuntu Installation
|
||||
|
||||
Although personally I feel that using the docker container is the best way of using and enjoying something like hammond, a lot of people in the community are still not comfortable with using Docker and wanted to host it natively on their Linux servers. Follow the link below to get a guide on how to build hammond from source.
|
||||
Although personally I feel that using the docker container is the best way of using
|
||||
and enjoying something like hammond, a lot of people in the community are still not
|
||||
comfortable with using Docker and wanted to host it natively on their Linux servers.
|
||||
Follow the link below to get a guide on how to build hammond from source.
|
||||
|
||||
[Build from source / Ubuntu Guide](docs/ubuntu-install.md)
|
||||
|
||||
@@ -157,6 +170,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 +209,6 @@ Distributed under the GPL-3.0 License. See `LICENSE` for more information.
|
||||
|
||||
## Contact
|
||||
|
||||
Akhil Gupta - [@akhilrex](https://twitter.com/akhilrex)
|
||||
Project Link: [https://github.com/AlfHou/hammond](https://github.com/AlfHou/hammond)
|
||||
|
||||
Project Link: [https://github.com/akhilrex/hammond](https://github.com/akhilrex/hammond)
|
||||
|
||||
<a href="https://www.buymeacoffee.com/akhilrex" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="width: 217px !important;height: 60px !important;" ></a>
|
||||
|
||||
<!-- MARKDOWN LINKS & IMAGES -->
|
||||
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
|
||||
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/akhilrex/hammond.svg?style=flat-square
|
||||
[contributors-url]: https://github.com/akhilrex/hammond/graphs/contributors
|
||||
[forks-shield]: https://img.shields.io/github/forks/akhilrex/hammond.svg?style=flat-square
|
||||
[forks-url]: https://github.com/akhilrex/hammond/network/members
|
||||
[stars-shield]: https://img.shields.io/github/stars/akhilrex/hammond.svg?style=flat-square
|
||||
[stars-url]: https://github.com/akhilrex/hammond/stargazers
|
||||
[issues-shield]: https://img.shields.io/github/issues/akhilrex/hammond.svg?style=flat-square
|
||||
[issues-url]: https://github.com/akhilrex/hammond/issues
|
||||
[license-shield]: https://img.shields.io/github/license/akhilrex/hammond.svg?style=flat-square
|
||||
[license-url]: https://github.com/akhilrex/hammond/blob/master/LICENSE
|
||||
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=flat-square&logo=linkedin&colorB=555
|
||||
[linkedin-url]: https://linkedin.com/in/akhilrex
|
||||
[product-screenshot]: images/screenshot.jpg
|
||||
|
||||
@@ -1,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
|
||||
|
||||
3
server/.gitignore
vendored
3
server/.gitignore
vendored
@@ -14,6 +14,7 @@
|
||||
|
||||
# MS VSCode
|
||||
.vscode
|
||||
!.vscode/launch.json
|
||||
__debug_bin
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
@@ -22,4 +23,4 @@ assets/*
|
||||
keys/*
|
||||
backups/*
|
||||
nodemon.json
|
||||
dist/*
|
||||
dist/*
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN go build -o ./app ./main.go
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/akhilrex/hammond"
|
||||
LABEL org.opencontainers.image.source="https://github.com/alfhou/hammond"
|
||||
|
||||
ENV CONFIG=/config
|
||||
ENV DATA=/assets
|
||||
@@ -38,4 +38,4 @@ COPY dist ./dist
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENTRYPOINT ["./app"]
|
||||
ENTRYPOINT ["./app"]
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"hammond/db"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
@@ -25,6 +26,33 @@ func RandString(n int) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// A helper to convert from litres to gallon
|
||||
func LitreToGallon(litres float32) float32 {
|
||||
gallonConversionFactor := 0.21997
|
||||
return litres * float32(gallonConversionFactor);
|
||||
}
|
||||
|
||||
// A helper to convert from gallon to litres
|
||||
func GallonToLitre(gallons float32) float32 {
|
||||
litreConversionFactor := 3.785412
|
||||
return gallons * float32(litreConversionFactor);
|
||||
}
|
||||
|
||||
|
||||
// A helper to convert from km to miles
|
||||
func KmToMiles(km float32) float32 {
|
||||
kmConversionFactor := 0.62137119
|
||||
return km * float32(kmConversionFactor);
|
||||
}
|
||||
|
||||
// A helper to convert from miles to km
|
||||
func MilesToKm(miles float32) float32 {
|
||||
milesConversionFactor := 1.609344
|
||||
return miles * float32(milesConversionFactor);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// A Util function to generate jwt_token which can be used in the request header
|
||||
func GenToken(id string, role db.Role) (string, string) {
|
||||
jwt_token := jwt.New(jwt.GetSigningMethod("HS256"))
|
||||
|
||||
@@ -5,11 +5,13 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"hammond/common"
|
||||
"hammond/db"
|
||||
"hammond/models"
|
||||
"hammond/service"
|
||||
|
||||
"github.com/akhilrex/hammond/common"
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"github.com/akhilrex/hammond/service"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -91,20 +93,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 +116,7 @@ func userLogin(c *gin.Context) {
|
||||
Email: user.Email,
|
||||
Token: token,
|
||||
RefreshToken: refreshToken,
|
||||
Role: user.RoleDetail().Long,
|
||||
Role: user.RoleDetail().Key,
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
@@ -148,7 +150,7 @@ func refresh(c *gin.Context) {
|
||||
Email: user.Email,
|
||||
Token: token,
|
||||
RefreshToken: refreshToken,
|
||||
Role: user.RoleDetail().Long,
|
||||
Role: user.RoleDetail().Key,
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
} else {
|
||||
@@ -170,16 +172,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)
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/akhilrex/hammond/common"
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"github.com/akhilrex/hammond/service"
|
||||
"hammond/common"
|
||||
"hammond/db"
|
||||
"hammond/models"
|
||||
"hammond/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
@@ -2,13 +2,18 @@ package controllers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"hammond/models"
|
||||
"hammond/service"
|
||||
|
||||
"github.com/akhilrex/hammond/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisteImportController(router *gin.RouterGroup) {
|
||||
router.POST("/import/fuelly", fuellyImport)
|
||||
router.POST("/import/drivvo", drivvoImport)
|
||||
router.POST("/import/generic", genericImport)
|
||||
}
|
||||
|
||||
func fuellyImport(c *gin.Context) {
|
||||
@@ -24,3 +29,46 @@ func fuellyImport(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{})
|
||||
}
|
||||
|
||||
func drivvoImport(c *gin.Context) {
|
||||
bytes, err := getFileBytes(c, "file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
vehicleId := c.PostForm("vehicleID")
|
||||
if vehicleId == "" {
|
||||
c.JSON(http.StatusUnprocessableEntity, "Missing Vehicle ID")
|
||||
return
|
||||
}
|
||||
importLocation, err := strconv.ParseBool(c.PostForm("importLocation"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, "Please include importLocation option.")
|
||||
return
|
||||
}
|
||||
|
||||
errors := service.DrivvoImport(bytes, c.MustGet("userId").(string), vehicleId, importLocation)
|
||||
if len(errors) > 0 {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"errors": errors})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{})
|
||||
}
|
||||
|
||||
func genericImport(c *gin.Context) {
|
||||
var json models.ImportData
|
||||
if err := c.ShouldBindJSON(&json); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if json.VehicleId == "" {
|
||||
c.JSON(http.StatusUnprocessableEntity, "Missing Vehicle ID")
|
||||
return
|
||||
}
|
||||
errors := service.GenericImport(json, c.MustGet("userId").(string))
|
||||
if len(errors) > 0 {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"errors": errors})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{})
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ package controllers
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/akhilrex/hammond/common"
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"github.com/akhilrex/hammond/service"
|
||||
"hammond/common"
|
||||
"hammond/db"
|
||||
"hammond/models"
|
||||
"hammond/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"hammond/db"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/dgrijalva/jwt-go/request"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -23,8 +24,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'
|
||||
|
||||
@@ -3,9 +3,10 @@ package controllers
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/akhilrex/hammond/common"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"github.com/akhilrex/hammond/service"
|
||||
"hammond/common"
|
||||
"hammond/models"
|
||||
"hammond/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -25,7 +26,7 @@ func getMileageForVehicle(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
fillups, err := service.GetMileageByVehicleId(searchByIdQuery.Id, model.Since)
|
||||
fillups, err := service.GetMileageByVehicleId(searchByIdQuery.Id, model.Since, model.MileageOption)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, common.NewError("getMileageForVehicle", err))
|
||||
return
|
||||
|
||||
@@ -4,10 +4,11 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/akhilrex/hammond/common"
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"github.com/akhilrex/hammond/service"
|
||||
"hammond/common"
|
||||
"hammond/db"
|
||||
"hammond/models"
|
||||
"hammond/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -51,7 +52,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
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@ package controllers
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/akhilrex/hammond/common"
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"github.com/akhilrex/hammond/service"
|
||||
"hammond/common"
|
||||
"hammond/db"
|
||||
"hammond/models"
|
||||
"hammond/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/akhilrex/hammond/common"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"github.com/akhilrex/hammond/service"
|
||||
"hammond/common"
|
||||
"hammond/models"
|
||||
"hammond/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -397,7 +398,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() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module github.com/akhilrex/hammond
|
||||
module hammond
|
||||
|
||||
go 1.16
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/akhilrex/hammond/controllers"
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/hammond/service"
|
||||
"hammond/controllers"
|
||||
"hammond/db"
|
||||
"hammond/service"
|
||||
|
||||
"github.com/gin-contrib/location"
|
||||
"github.com/gin-gonic/contrib/static"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
21
server/models/alert.go
Normal file
21
server/models/alert.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"hammond/db"
|
||||
)
|
||||
|
||||
type CreateAlertModel struct {
|
||||
Comments string `json:"comments"`
|
||||
Title string `json:"title"`
|
||||
StartDate time.Time `json:"date"`
|
||||
StartOdoReading int `json:"startOdoReading"`
|
||||
DistanceUnit *db.DistanceUnit `json:"distanceUnit"`
|
||||
AlertFrequency *db.AlertFrequency `json:"alertFrequency"`
|
||||
OdoFrequency int `json:"odoFrequency"`
|
||||
DayFrequency int `json:"dayFrequency"`
|
||||
AlertAllUsers bool `json:"alertAllUsers"`
|
||||
IsActive bool `json:"isActive"`
|
||||
AlertType *db.AlertType `json:"alertType"`
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package models
|
||||
|
||||
import "github.com/akhilrex/hammond/db"
|
||||
import "hammond/db"
|
||||
|
||||
type LoginResponse struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
22
server/models/import.go
Normal file
22
server/models/import.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package models
|
||||
|
||||
type ImportData struct {
|
||||
Data []ImportFillup `json:"data" binding:"required"`
|
||||
VehicleId string `json:"vehicleId" binding:"required"`
|
||||
TimeZone string `json:"timezone" binding:"required"`
|
||||
}
|
||||
|
||||
type ImportFillup struct {
|
||||
VehicleID string `json:"vehicleId"`
|
||||
FuelQuantity float32 `json:"fuelQuantity"`
|
||||
PerUnitPrice float32 `json:"perUnitPrice"`
|
||||
TotalAmount float32 `json:"totalAmount"`
|
||||
OdoReading int `json:"odoReading"`
|
||||
IsTankFull *bool `json:"isTankFull"`
|
||||
HasMissedFillup *bool `json:"hasMissedFillup"`
|
||||
Comments string `json:"comments"`
|
||||
FillingStation string `json:"fillingStation"`
|
||||
UserID string `json:"userId"`
|
||||
Date string `json:"date"`
|
||||
FuelSubType string `json:"fuelSubType"`
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package models
|
||||
|
||||
import "github.com/akhilrex/hammond/db"
|
||||
import "hammond/db"
|
||||
|
||||
type UpdateSettingModel struct {
|
||||
Currency string `json:"currency" form:"currency" query:"currency"`
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"hammond/db"
|
||||
)
|
||||
|
||||
type MileageModel struct {
|
||||
@@ -14,7 +14,7 @@ type MileageModel struct {
|
||||
FuelQuantity float32 `form:"fuelQuantity" json:"fuelQuantity" binding:"required"`
|
||||
PerUnitPrice float32 `form:"perUnitPrice" json:"perUnitPrice" binding:"required"`
|
||||
Currency string `json:"currency"`
|
||||
|
||||
DistanceUnit db.DistanceUnit `form:"distanceUnit" json:"distanceUnit"`
|
||||
Mileage float32 `form:"mileage" json:"mileage" binding:"mileage"`
|
||||
CostPerMile float32 `form:"costPerMile" json:"costPerMile" binding:"costPerMile"`
|
||||
OdoReading int `form:"odoReading" json:"odoReading" binding:"odoReading"`
|
||||
@@ -35,4 +35,5 @@ func (b *MileageModel) MarshalJSON() ([]byte, error) {
|
||||
|
||||
type MileageQueryModel struct {
|
||||
Since time.Time `json:"since" query:"since" form:"since"`
|
||||
MileageOption string `json:"mileageOption" query:"mileageOption" form:"mileageOption"`
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ package models
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"hammond/db"
|
||||
|
||||
_ "github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
@@ -17,6 +18,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"
|
||||
|
||||
"hammond/db"
|
||||
"hammond/models"
|
||||
)
|
||||
|
||||
func CreateAlert(model models.CreateAlertModel, vehicleId, userId string) (*db.VehicleAlert, error) {
|
||||
alert := db.VehicleAlert{
|
||||
VehicleID: vehicleId,
|
||||
UserID: userId,
|
||||
Title: model.Title,
|
||||
Comments: model.Comments,
|
||||
StartDate: model.StartDate,
|
||||
StartOdoReading: model.StartOdoReading,
|
||||
DistanceUnit: *model.DistanceUnit,
|
||||
AlertFrequency: *model.AlertFrequency,
|
||||
OdoFrequency: model.OdoFrequency,
|
||||
DayFrequency: model.DayFrequency,
|
||||
AlertAllUsers: model.AlertAllUsers,
|
||||
IsActive: model.IsActive,
|
||||
AlertType: *model.AlertType,
|
||||
}
|
||||
tx := db.DB.Create(&alert)
|
||||
if tx.Error != nil {
|
||||
return nil, tx.Error
|
||||
}
|
||||
go CreateAlertInstance(alert.ID)
|
||||
return &alert, nil
|
||||
}
|
||||
|
||||
func CreateAlertInstance(alertId string) error {
|
||||
alert, err := db.GeAlertById(alertId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
existingOccurence, err := db.GetAlertOccurenceByAlertId(alertId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var lastOccurance db.AlertOccurance
|
||||
useOccurance := false
|
||||
|
||||
if len(*existingOccurence) > 0 {
|
||||
lastOccurance = (*existingOccurence)[0]
|
||||
useOccurance = true
|
||||
if alert.AlertFrequency == db.ONETIME {
|
||||
return errors.New("Only single occurance is possible for this kind of alert")
|
||||
}
|
||||
}
|
||||
users := []string{alert.UserID}
|
||||
if alert.AlertAllUsers {
|
||||
allUsers, err := db.GetVehicleUsers(alert.VehicleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
users = make([]string, len(*allUsers))
|
||||
for i, user := range *allUsers {
|
||||
users[i] = user.UserID
|
||||
}
|
||||
}
|
||||
|
||||
for _, userId := range users {
|
||||
model := db.AlertOccurance{
|
||||
VehicleID: alert.VehicleID,
|
||||
UserID: userId,
|
||||
VehicleAlertID: alertId,
|
||||
}
|
||||
|
||||
if alert.AlertType == db.DISTANCE || alert.AlertType == db.BOTH {
|
||||
model.OdoReading = alert.StartOdoReading + alert.OdoFrequency
|
||||
if useOccurance {
|
||||
model.OdoReading = lastOccurance.OdoReading + alert.OdoFrequency
|
||||
}
|
||||
}
|
||||
if alert.AlertType == db.TIME || alert.AlertType == db.BOTH {
|
||||
date := alert.StartDate.Add(time.Duration(alert.DayFrequency) * 24 * time.Hour)
|
||||
if useOccurance {
|
||||
date = lastOccurance.Date.Add(time.Duration(alert.DayFrequency) * 24 * time.Hour)
|
||||
}
|
||||
model.Date = &date
|
||||
}
|
||||
tx := db.DB.Create(&model)
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func ProcessAlertOccurance(occurance db.AlertOccurance, today time.Time) error {
|
||||
if occurance.ProcessDate != nil {
|
||||
return errors.New("Alert occurence already processed")
|
||||
}
|
||||
alert := occurance.VehicleAlert
|
||||
if !alert.IsActive {
|
||||
return errors.New("Alert is not active")
|
||||
}
|
||||
notification := db.Notification{
|
||||
Title: alert.Title,
|
||||
Content: alert.Comments,
|
||||
UserID: occurance.UserID,
|
||||
VehicleID: occurance.VehicleID,
|
||||
Date: today,
|
||||
ParentID: occurance.ID,
|
||||
ParentType: "AlertOccurance",
|
||||
}
|
||||
var alertProcessType db.AlertType
|
||||
if alert.AlertType == db.DISTANCE || alert.AlertType == db.BOTH {
|
||||
odoReading, err := GetLatestOdoReadingForVehicle(occurance.VehicleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if odoReading >= occurance.OdoReading {
|
||||
alertProcessType = db.DISTANCE
|
||||
}
|
||||
}
|
||||
if alert.AlertType == db.TIME || alert.AlertType == db.BOTH {
|
||||
if occurance.Date.Before(today) {
|
||||
alertProcessType = db.TIME
|
||||
}
|
||||
}
|
||||
|
||||
db.DB.Create(¬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() {
|
||||
|
||||
}
|
||||
142
server/service/drivvoImportService.go
Normal file
142
server/service/drivvoImportService.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hammond/db"
|
||||
)
|
||||
|
||||
func DrivvoParseExpenses(content []byte, user *db.User, vehicle *db.Vehicle) ([]db.Expense, []string) {
|
||||
expenseReader := csv.NewReader(bytes.NewReader(content))
|
||||
expenseReader.Comment = '#'
|
||||
// Read headers (there is a trailing comma at the end, that's why we have to read the first line)
|
||||
expenseReader.Read()
|
||||
expenseReader.FieldsPerRecord = 6
|
||||
expenseRecords, err := expenseReader.ReadAll()
|
||||
|
||||
var errors []string
|
||||
if err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
println(err.Error())
|
||||
return nil, errors
|
||||
}
|
||||
|
||||
var expenses []db.Expense
|
||||
for index, record := range expenseRecords {
|
||||
date, err := time.Parse("2006-01-02 15:04:05", record[1])
|
||||
if err != nil {
|
||||
errors = append(errors, "Found an invalid date/time at service/expense row "+strconv.Itoa(index+1))
|
||||
}
|
||||
|
||||
totalCost, err := strconv.ParseFloat(record[2], 32)
|
||||
if err != nil {
|
||||
errors = append(errors, "Found and invalid total cost at service/expense row "+strconv.Itoa(index+1))
|
||||
}
|
||||
|
||||
odometer, err := strconv.Atoi(record[0])
|
||||
if err != nil {
|
||||
errors = append(errors, "Found an invalid odometer reading at service/expense row "+strconv.Itoa(index+1))
|
||||
}
|
||||
|
||||
notes := fmt.Sprintf("Location: %s\nNotes: %s\n", record[4], record[5])
|
||||
|
||||
expenses = append(expenses, db.Expense{
|
||||
UserID: user.ID,
|
||||
VehicleID: vehicle.ID,
|
||||
Date: date,
|
||||
OdoReading: odometer,
|
||||
Amount: float32(totalCost),
|
||||
ExpenseType: record[3],
|
||||
Currency: user.Currency,
|
||||
DistanceUnit: user.DistanceUnit,
|
||||
Comments: notes,
|
||||
Source: "Drivvo",
|
||||
})
|
||||
}
|
||||
|
||||
return expenses, errors
|
||||
}
|
||||
|
||||
func DrivvoParseRefuelings(content []byte, user *db.User, vehicle *db.Vehicle, importLocation bool) ([]db.Fillup, []string) {
|
||||
refuelingReader := csv.NewReader(bytes.NewReader(content))
|
||||
refuelingReader.Comment = '#'
|
||||
refuelingRecords, err := refuelingReader.ReadAll()
|
||||
|
||||
var errors []string
|
||||
if err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
println(err.Error())
|
||||
return nil, errors
|
||||
}
|
||||
|
||||
var fillups []db.Fillup
|
||||
for index, record := range refuelingRecords {
|
||||
// Skip column titles
|
||||
if index == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
date, err := time.Parse("2006-01-02 15:04:05", record[1])
|
||||
if err != nil {
|
||||
errors = append(errors, "Found an invalid date/time at refuel row "+strconv.Itoa(index+1))
|
||||
}
|
||||
|
||||
totalCost, err := strconv.ParseFloat(record[4], 32)
|
||||
if err != nil {
|
||||
errors = append(errors, "Found and invalid total cost at refuel row "+strconv.Itoa(index+1))
|
||||
}
|
||||
|
||||
odometer, err := strconv.Atoi(record[0])
|
||||
if err != nil {
|
||||
errors = append(errors, "Found an invalid odometer reading at refuel row "+strconv.Itoa(index+1))
|
||||
}
|
||||
|
||||
location := ""
|
||||
if importLocation {
|
||||
location = record[17]
|
||||
}
|
||||
|
||||
pricePerUnit, err := strconv.ParseFloat(record[3], 32)
|
||||
if err != nil {
|
||||
unit := strings.ToLower(db.FuelUnitDetails[vehicle.FuelUnit].Key)
|
||||
errors = append(errors, fmt.Sprintf("Found an invalid cost per %s at refuel row %d", unit, index+1))
|
||||
}
|
||||
|
||||
quantity, err := strconv.ParseFloat(record[5], 32)
|
||||
if err != nil {
|
||||
errors = append(errors, "Found an invalid quantity at refuel row "+strconv.Itoa(index+1))
|
||||
}
|
||||
|
||||
isTankFull := record[6] == "Yes"
|
||||
|
||||
// Unfortunatly, drivvo doesn't expose this info in their export
|
||||
fal := false
|
||||
|
||||
notes := fmt.Sprintf("Reason: %s\nNotes: %s\nFuel: %s\n", record[18], record[19], record[2])
|
||||
|
||||
fillups = append(fillups, db.Fillup{
|
||||
VehicleID: vehicle.ID,
|
||||
UserID: user.ID,
|
||||
Date: date,
|
||||
HasMissedFillup: &fal,
|
||||
IsTankFull: &isTankFull,
|
||||
FuelQuantity: float32(quantity),
|
||||
PerUnitPrice: float32(pricePerUnit),
|
||||
FillingStation: location,
|
||||
OdoReading: odometer,
|
||||
TotalAmount: float32(totalCost),
|
||||
FuelUnit: vehicle.FuelUnit,
|
||||
Currency: user.Currency,
|
||||
DistanceUnit: user.DistanceUnit,
|
||||
Comments: notes,
|
||||
Source: "Drivvo",
|
||||
})
|
||||
|
||||
}
|
||||
return fillups, errors
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package service
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -14,9 +13,10 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/hammond/internal/sanitize"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"hammond/db"
|
||||
"hammond/internal/sanitize"
|
||||
"hammond/models"
|
||||
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
@@ -126,14 +126,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 +151,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 +169,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
|
||||
|
||||
141
server/service/fuellyImportService.go
Normal file
141
server/service/fuellyImportService.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"hammond/db"
|
||||
|
||||
"github.com/leekchan/accounting"
|
||||
)
|
||||
|
||||
func FuellyParseAll(content []byte, userId string) ([]db.Fillup, []db.Expense, []string) {
|
||||
stream := bytes.NewReader(content)
|
||||
reader := csv.NewReader(stream)
|
||||
records, err := reader.ReadAll()
|
||||
|
||||
var errors []string
|
||||
user, err := GetUserById(userId)
|
||||
if err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
return nil, nil, errors
|
||||
}
|
||||
|
||||
vehicles, err := GetUserVehicles(userId)
|
||||
if err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
return nil, nil, errors
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
return nil, nil, errors
|
||||
}
|
||||
|
||||
var vehicleMap map[string]db.Vehicle = make(map[string]db.Vehicle)
|
||||
for _, vehicle := range *vehicles {
|
||||
vehicleMap[vehicle.Nickname] = vehicle
|
||||
}
|
||||
|
||||
var fillups []db.Fillup
|
||||
var expenses []db.Expense
|
||||
layout := "2006-01-02 15:04"
|
||||
altLayout := "2006-01-02 3:04 PM"
|
||||
|
||||
for index, record := range records {
|
||||
if index == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var vehicle db.Vehicle
|
||||
var ok bool
|
||||
if vehicle, ok = vehicleMap[record[4]]; !ok {
|
||||
errors = append(errors, "Found an unmapped vehicle entry at row "+strconv.Itoa(index+1))
|
||||
}
|
||||
dateStr := record[2] + " " + record[3]
|
||||
date, err := time.Parse(layout, dateStr)
|
||||
if err != nil {
|
||||
date, err = time.Parse(altLayout, dateStr)
|
||||
}
|
||||
if err != nil {
|
||||
errors = append(errors, "Found an invalid date/time at row "+strconv.Itoa(index+1))
|
||||
}
|
||||
|
||||
totalCostStr := accounting.UnformatNumber(record[9], 3, user.Currency)
|
||||
totalCost64, err := strconv.ParseFloat(totalCostStr, 32)
|
||||
if err != nil {
|
||||
errors = append(errors, "Found an invalid total cost at row "+strconv.Itoa(index+1))
|
||||
}
|
||||
|
||||
totalCost := float32(totalCost64)
|
||||
odoStr := accounting.UnformatNumber(record[5], 0, user.Currency)
|
||||
odoreading, err := strconv.Atoi(odoStr)
|
||||
if err != nil {
|
||||
errors = append(errors, "Found an invalid odo reading at row "+strconv.Itoa(index+1))
|
||||
}
|
||||
location := record[12]
|
||||
|
||||
//Create Fillup
|
||||
if record[0] == "Gas" {
|
||||
rateStr := accounting.UnformatNumber(record[7], 3, user.Currency)
|
||||
ratet64, err := strconv.ParseFloat(rateStr, 32)
|
||||
if err != nil {
|
||||
errors = append(errors, "Found an invalid cost per gallon at row "+strconv.Itoa(index+1))
|
||||
}
|
||||
rate := float32(ratet64)
|
||||
|
||||
quantity64, err := strconv.ParseFloat(record[8], 32)
|
||||
if err != nil {
|
||||
errors = append(errors, "Found an invalid quantity at row "+strconv.Itoa(index+1))
|
||||
}
|
||||
quantity := float32(quantity64)
|
||||
|
||||
notes := fmt.Sprintf("Octane:%s\nGas Brand:%s\nLocation%s\nTags:%s\nPayment Type:%s\nTire Pressure:%s\nNotes:%s\nMPG:%s",
|
||||
record[10], record[11], record[12], record[13], record[14], record[15], record[16], record[1],
|
||||
)
|
||||
|
||||
isTankFull := record[6] == "Full"
|
||||
fal := false
|
||||
fillups = append(fillups, db.Fillup{
|
||||
VehicleID: vehicle.ID,
|
||||
FuelUnit: vehicle.FuelUnit,
|
||||
FuelQuantity: quantity,
|
||||
PerUnitPrice: rate,
|
||||
TotalAmount: totalCost,
|
||||
OdoReading: odoreading,
|
||||
IsTankFull: &isTankFull,
|
||||
Comments: notes,
|
||||
FillingStation: location,
|
||||
HasMissedFillup: &fal,
|
||||
UserID: userId,
|
||||
Date: date,
|
||||
Currency: user.Currency,
|
||||
DistanceUnit: user.DistanceUnit,
|
||||
Source: "Fuelly",
|
||||
})
|
||||
|
||||
}
|
||||
if record[0] == "Service" {
|
||||
notes := fmt.Sprintf("Tags:%s\nPayment Type:%s\nNotes:%s",
|
||||
record[13], record[14], record[16],
|
||||
)
|
||||
expenses = append(expenses, db.Expense{
|
||||
VehicleID: vehicle.ID,
|
||||
Amount: totalCost,
|
||||
OdoReading: odoreading,
|
||||
Comments: notes,
|
||||
ExpenseType: record[17],
|
||||
UserID: userId,
|
||||
Currency: user.Currency,
|
||||
Date: date,
|
||||
DistanceUnit: user.DistanceUnit,
|
||||
Source: "Fuelly",
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
return fillups, expenses, errors
|
||||
}
|
||||
47
server/service/genericImportService.go
Normal file
47
server/service/genericImportService.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"hammond/db"
|
||||
"hammond/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GenericParseRefuelings(content []models.ImportFillup, user *db.User, vehicle *db.Vehicle, timezone string) ([]db.Fillup, []string) {
|
||||
var errors []string
|
||||
var fillups []db.Fillup
|
||||
dateLayout := "2006-01-02T15:04:05.000Z"
|
||||
loc, _ := time.LoadLocation(timezone)
|
||||
for _, record := range content {
|
||||
date, err := time.ParseInLocation(dateLayout, record.Date, loc)
|
||||
if err != nil {
|
||||
date = time.Date(2000, time.December, 0, 0, 0, 0, 0, loc)
|
||||
}
|
||||
|
||||
var missedFillup bool
|
||||
if record.HasMissedFillup == nil {
|
||||
missedFillup = false
|
||||
} else {
|
||||
missedFillup = *record.HasMissedFillup
|
||||
}
|
||||
|
||||
fillups = append(fillups, db.Fillup{
|
||||
VehicleID: vehicle.ID,
|
||||
UserID: user.ID,
|
||||
Date: date,
|
||||
IsTankFull: record.IsTankFull,
|
||||
HasMissedFillup: &missedFillup,
|
||||
FuelQuantity: float32(record.FuelQuantity),
|
||||
PerUnitPrice: float32(record.PerUnitPrice),
|
||||
FillingStation: record.FillingStation,
|
||||
OdoReading: record.OdoReading,
|
||||
TotalAmount: float32(record.TotalAmount),
|
||||
FuelUnit: vehicle.FuelUnit,
|
||||
Currency: user.Currency,
|
||||
DistanceUnit: user.DistanceUnit,
|
||||
Comments: record.Comments,
|
||||
Source: "Generic Import",
|
||||
})
|
||||
}
|
||||
|
||||
return fillups, errors
|
||||
}
|
||||
@@ -2,144 +2,13 @@ package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/leekchan/accounting"
|
||||
"hammond/db"
|
||||
"hammond/models"
|
||||
)
|
||||
|
||||
func FuellyImport(content []byte, userId string) []string {
|
||||
stream := bytes.NewReader(content)
|
||||
reader := csv.NewReader(stream)
|
||||
records, err := reader.ReadAll()
|
||||
|
||||
func WriteToDB(fillups []db.Fillup, expenses []db.Expense) []string {
|
||||
var errors []string
|
||||
if err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
return errors
|
||||
}
|
||||
|
||||
vehicles, err := GetUserVehicles(userId)
|
||||
if err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
return errors
|
||||
}
|
||||
user, err := GetUserById(userId)
|
||||
|
||||
if err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
return errors
|
||||
}
|
||||
|
||||
var vehicleMap map[string]db.Vehicle = make(map[string]db.Vehicle)
|
||||
for _, vehicle := range *vehicles {
|
||||
vehicleMap[vehicle.Nickname] = vehicle
|
||||
}
|
||||
|
||||
var fillups []db.Fillup
|
||||
var expenses []db.Expense
|
||||
layout := "2006-01-02 15:04"
|
||||
altLayout := "2006-01-02 3:04 PM"
|
||||
|
||||
for index, record := range records {
|
||||
if index == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var vehicle db.Vehicle
|
||||
var ok bool
|
||||
if vehicle, ok = vehicleMap[record[4]]; !ok {
|
||||
errors = append(errors, "Found an unmapped vehicle entry at row "+strconv.Itoa(index+1))
|
||||
}
|
||||
dateStr := record[2] + " " + record[3]
|
||||
date, err := time.Parse(layout, dateStr)
|
||||
if err != nil {
|
||||
date, err = time.Parse(altLayout, dateStr)
|
||||
}
|
||||
if err != nil {
|
||||
errors = append(errors, "Found an invalid date/time at row "+strconv.Itoa(index+1))
|
||||
}
|
||||
|
||||
totalCostStr := accounting.UnformatNumber(record[9], 3, user.Currency)
|
||||
totalCost64, err := strconv.ParseFloat(totalCostStr, 32)
|
||||
if err != nil {
|
||||
errors = append(errors, "Found an invalid total cost at row "+strconv.Itoa(index+1))
|
||||
}
|
||||
|
||||
totalCost := float32(totalCost64)
|
||||
odoStr := accounting.UnformatNumber(record[5], 0, user.Currency)
|
||||
odoreading, err := strconv.Atoi(odoStr)
|
||||
if err != nil {
|
||||
errors = append(errors, "Found an invalid odo reading at row "+strconv.Itoa(index+1))
|
||||
}
|
||||
location := record[12]
|
||||
|
||||
//Create Fillup
|
||||
if record[0] == "Gas" {
|
||||
rateStr := accounting.UnformatNumber(record[7], 3, user.Currency)
|
||||
ratet64, err := strconv.ParseFloat(rateStr, 32)
|
||||
if err != nil {
|
||||
errors = append(errors, "Found an invalid cost per gallon at row "+strconv.Itoa(index+1))
|
||||
}
|
||||
rate := float32(ratet64)
|
||||
|
||||
quantity64, err := strconv.ParseFloat(record[8], 32)
|
||||
if err != nil {
|
||||
errors = append(errors, "Found an invalid quantity at row "+strconv.Itoa(index+1))
|
||||
}
|
||||
quantity := float32(quantity64)
|
||||
|
||||
notes := fmt.Sprintf("Octane:%s\nGas Brand:%s\nLocation%s\nTags:%s\nPayment Type:%s\nTire Pressure:%s\nNotes:%s\nMPG:%s",
|
||||
record[10], record[11], record[12], record[13], record[14], record[15], record[16], record[1],
|
||||
)
|
||||
|
||||
isTankFull := record[6] == "Full"
|
||||
fal := false
|
||||
fillups = append(fillups, db.Fillup{
|
||||
VehicleID: vehicle.ID,
|
||||
FuelUnit: vehicle.FuelUnit,
|
||||
FuelQuantity: quantity,
|
||||
PerUnitPrice: rate,
|
||||
TotalAmount: totalCost,
|
||||
OdoReading: odoreading,
|
||||
IsTankFull: &isTankFull,
|
||||
Comments: notes,
|
||||
FillingStation: location,
|
||||
HasMissedFillup: &fal,
|
||||
UserID: userId,
|
||||
Date: date,
|
||||
Currency: user.Currency,
|
||||
DistanceUnit: user.DistanceUnit,
|
||||
Source: "Fuelly",
|
||||
})
|
||||
|
||||
}
|
||||
if record[0] == "Service" {
|
||||
notes := fmt.Sprintf("Tags:%s\nPayment Type:%s\nNotes:%s",
|
||||
record[13], record[14], record[16],
|
||||
)
|
||||
expenses = append(expenses, db.Expense{
|
||||
VehicleID: vehicle.ID,
|
||||
Amount: totalCost,
|
||||
OdoReading: odoreading,
|
||||
Comments: notes,
|
||||
ExpenseType: record[17],
|
||||
UserID: userId,
|
||||
Currency: user.Currency,
|
||||
Date: date,
|
||||
DistanceUnit: user.DistanceUnit,
|
||||
Source: "Fuelly",
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
if len(errors) != 0 {
|
||||
return errors
|
||||
}
|
||||
|
||||
tx := db.DB.Begin()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@@ -150,19 +19,114 @@ func FuellyImport(content []byte, userId string) []string {
|
||||
errors = append(errors, err.Error())
|
||||
return errors
|
||||
}
|
||||
if err := tx.Create(&fillups).Error; err != nil {
|
||||
tx.Rollback()
|
||||
errors = append(errors, err.Error())
|
||||
return errors
|
||||
if fillups != nil {
|
||||
if err := tx.Create(&fillups).Error; err != nil {
|
||||
tx.Rollback()
|
||||
errors = append(errors, err.Error())
|
||||
return errors
|
||||
}
|
||||
}
|
||||
if err := tx.Create(&expenses).Error; err != nil {
|
||||
tx.Rollback()
|
||||
errors = append(errors, err.Error())
|
||||
return errors
|
||||
if expenses != nil {
|
||||
if err := tx.Create(&expenses).Error; err != nil {
|
||||
tx.Rollback()
|
||||
errors = append(errors, err.Error())
|
||||
return errors
|
||||
}
|
||||
}
|
||||
err = tx.Commit().Error
|
||||
err := tx.Commit().Error
|
||||
if err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
}
|
||||
return errors
|
||||
|
||||
}
|
||||
|
||||
func DrivvoImport(content []byte, userId string, vehicleId string, importLocation bool) []string {
|
||||
var errors []string
|
||||
user, err := GetUserById(userId)
|
||||
if err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
return errors
|
||||
}
|
||||
|
||||
vehicle, err := GetVehicleById(vehicleId)
|
||||
if err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
return errors
|
||||
}
|
||||
|
||||
endParseIndex := bytes.Index(content, []byte("#Income"))
|
||||
if endParseIndex == -1 {
|
||||
endParseIndex = bytes.Index(content, []byte("#Route"))
|
||||
if endParseIndex == -1 {
|
||||
endParseIndex = len(content)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
serviceEndIndex := bytes.Index(content, []byte("#Expense"))
|
||||
if serviceEndIndex == -1 {
|
||||
serviceEndIndex = endParseIndex
|
||||
}
|
||||
|
||||
refuelEndIndex := bytes.Index(content, []byte("#Service"))
|
||||
if refuelEndIndex == -1 {
|
||||
refuelEndIndex = serviceEndIndex
|
||||
}
|
||||
|
||||
var fillups []db.Fillup
|
||||
fillups, errors = DrivvoParseRefuelings(content[:refuelEndIndex], user, vehicle, importLocation)
|
||||
|
||||
var allExpenses []db.Expense
|
||||
services, parseErrors := DrivvoParseExpenses(content[refuelEndIndex:serviceEndIndex], user, vehicle)
|
||||
if parseErrors != nil {
|
||||
errors = append(errors, parseErrors...)
|
||||
}
|
||||
allExpenses = append(allExpenses, services...)
|
||||
|
||||
expenses, parseErrors := DrivvoParseExpenses(content[serviceEndIndex:endParseIndex], user, vehicle)
|
||||
if parseErrors != nil {
|
||||
errors = append(errors, parseErrors...)
|
||||
}
|
||||
|
||||
allExpenses = append(allExpenses, expenses...)
|
||||
|
||||
if len(errors) != 0 {
|
||||
return errors
|
||||
}
|
||||
|
||||
return WriteToDB(fillups, allExpenses)
|
||||
}
|
||||
|
||||
func FuellyImport(content []byte, userId string) []string {
|
||||
fillups, expenses, errors := FuellyParseAll(content, userId)
|
||||
if len(errors) != 0 {
|
||||
return errors
|
||||
}
|
||||
|
||||
return WriteToDB(fillups, expenses)
|
||||
}
|
||||
|
||||
func GenericImport(content models.ImportData, userId string) []string {
|
||||
var errors []string
|
||||
user, err := GetUserById(userId)
|
||||
if err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
return errors
|
||||
}
|
||||
|
||||
vehicle, err := GetVehicleById(content.VehicleId)
|
||||
if err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
return errors
|
||||
}
|
||||
|
||||
var fillups []db.Fillup
|
||||
fillups, errors = GenericParseRefuelings(content.Data, user, vehicle, content.TimeZone)
|
||||
|
||||
if len(errors) != 0 {
|
||||
return errors
|
||||
}
|
||||
|
||||
return WriteToDB(fillups, nil)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"hammond/db"
|
||||
)
|
||||
|
||||
func CanInitializeSystem() (bool, error) {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"hammond/common"
|
||||
"hammond/db"
|
||||
"hammond/models"
|
||||
)
|
||||
|
||||
func GetMileageByVehicleId(vehicleId string, since time.Time) (mileage []models.MileageModel, err error) {
|
||||
func GetMileageByVehicleId(vehicleId string, since time.Time, mileageOption string) (mileage []models.MileageModel, err error) {
|
||||
data, err := db.GetFillupsByVehicleIdSince(vehicleId, since)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -15,6 +17,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
|
||||
|
||||
@@ -32,14 +37,48 @@ func GetMileageByVehicleId(vehicleId string, since time.Time) (mileage []models.
|
||||
PerUnitPrice: currentFillup.PerUnitPrice,
|
||||
OdoReading: currentFillup.OdoReading,
|
||||
Currency: currentFillup.Currency,
|
||||
DistanceUnit: currentFillup.DistanceUnit,
|
||||
Mileage: 0,
|
||||
CostPerMile: 0,
|
||||
}
|
||||
|
||||
if currentFillup.IsTankFull != nil && *currentFillup.IsTankFull && (currentFillup.HasMissedFillup == nil || !(*currentFillup.HasMissedFillup)) {
|
||||
distance := float32(currentFillup.OdoReading - lastFillup.OdoReading)
|
||||
mileage.Mileage = distance / currentFillup.FuelQuantity
|
||||
mileage.CostPerMile = distance / currentFillup.TotalAmount
|
||||
currentOdoReading := float32(currentFillup.OdoReading);
|
||||
lastFillupOdoReading := float32(lastFillup.OdoReading);
|
||||
currentFuelQuantity := float32(currentFillup.FuelQuantity);
|
||||
// If miles per gallon option and distanceUnit is km, convert from km to miles
|
||||
// then check if fuel unit is litres. If it is, convert to gallons
|
||||
if (mileageOption == "mpg" && mileage.DistanceUnit == db.KILOMETERS) {
|
||||
currentOdoReading = common.KmToMiles(currentOdoReading);
|
||||
lastFillupOdoReading = common.KmToMiles(lastFillupOdoReading);
|
||||
if (mileage.FuelUnit == db.LITRE) {
|
||||
currentFuelQuantity = common.LitreToGallon(currentFuelQuantity);
|
||||
}
|
||||
}
|
||||
|
||||
// If km_litre option or litre per 100km and distanceUnit is miles, convert from miles to km
|
||||
// then check if fuel unit is not litres. If it isn't, convert to litres
|
||||
|
||||
if ((mileageOption == "km_litre" || mileageOption == "litre_100km") && mileage.DistanceUnit == db.MILES) {
|
||||
currentOdoReading = common.MilesToKm(currentOdoReading);
|
||||
lastFillupOdoReading = common.MilesToKm(lastFillupOdoReading);
|
||||
|
||||
if (mileage.FuelUnit == db.US_GALLON) {
|
||||
currentFuelQuantity = common.GallonToLitre(currentFuelQuantity);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
distance := float32(currentOdoReading - lastFillupOdoReading);
|
||||
if (mileageOption == "litre_100km") {
|
||||
mileage.Mileage = currentFuelQuantity / distance * 100;
|
||||
} else {
|
||||
mileage.Mileage = distance / currentFuelQuantity;
|
||||
}
|
||||
|
||||
mileage.CostPerMile = distance / currentFillup.TotalAmount;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"strings"
|
||||
|
||||
"hammond/db"
|
||||
"hammond/models"
|
||||
)
|
||||
|
||||
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,
|
||||
|
||||
@@ -3,8 +3,10 @@ package service
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/akhilrex/hammond/db"
|
||||
"github.com/akhilrex/hammond/models"
|
||||
"hammond/db"
|
||||
"hammond/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
@@ -13,6 +15,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 +102,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 +247,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)
|
||||
|
||||
@@ -20,6 +20,7 @@ module.exports = {
|
||||
'no-console': process.env.PRE_COMMIT
|
||||
? ['error', { allow: ['warn', 'error'] }]
|
||||
: 'off',
|
||||
'vue/multi-word-component-names': 0,
|
||||
'import/no-relative-parent-imports': 'error',
|
||||
'import/order': 'error',
|
||||
'vue/array-bracket-spacing': 'error',
|
||||
|
||||
5
ui/.gitignore
vendored
5
ui/.gitignore
vendored
@@ -29,3 +29,8 @@ yarn-error.log*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw*
|
||||
|
||||
#Vs code files
|
||||
.vscode
|
||||
!.vscode/launch.json
|
||||
|
||||
|
||||
30
ui/.vscode/_components.code-snippets
vendored
30
ui/.vscode/_components.code-snippets
vendored
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"BaseButton": {
|
||||
"scope": "vue-html",
|
||||
"prefix": "BaseButton",
|
||||
"body": ["<BaseButton>", "\t${3}", "</BaseButton>"],
|
||||
"description": "<BaseButton>"
|
||||
},
|
||||
"BaseIcon": {
|
||||
"scope": "vue-html",
|
||||
"prefix": "BaseIcon",
|
||||
"body": ["<BaseIcon name=\"${1}\">", "\t${2}", "</BaseIcon>"],
|
||||
"description": "<BaseIcon>"
|
||||
},
|
||||
"BaseInputText": {
|
||||
"scope": "vue-html",
|
||||
"prefix": "BaseInputText",
|
||||
"body": ["<BaseInputText ${1}/>"],
|
||||
"description": "<BaseInputText>"
|
||||
},
|
||||
"BaseLink": {
|
||||
"scope": "vue-html",
|
||||
"prefix": "BaseLink",
|
||||
"body": [
|
||||
"<BaseLink ${1|name,:to,href|}=\"${2:route}\">",
|
||||
"\t${3}",
|
||||
"</BaseLink>"
|
||||
],
|
||||
"description": "<BaseLink>"
|
||||
}
|
||||
}
|
||||
26
ui/.vscode/_sfc-blocks.code-snippets
vendored
26
ui/.vscode/_sfc-blocks.code-snippets
vendored
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"script": {
|
||||
"scope": "vue",
|
||||
"prefix": "script",
|
||||
"body": ["<script>", "export default {", "\t${0}", "}", "</script>"],
|
||||
"description": "<script>"
|
||||
},
|
||||
"template": {
|
||||
"scope": "vue",
|
||||
"prefix": "template",
|
||||
"body": ["<template>", "\t${0}", "</template>"],
|
||||
"description": "<template>"
|
||||
},
|
||||
"style": {
|
||||
"scope": "vue",
|
||||
"prefix": "style",
|
||||
"body": [
|
||||
"<style lang=\"scss\" module>",
|
||||
"@import '@design';",
|
||||
"",
|
||||
"${0}",
|
||||
"</style>"
|
||||
],
|
||||
"description": "<style lang=\"scss\" module>"
|
||||
}
|
||||
}
|
||||
37
ui/.vscode/extensions.json
vendored
37
ui/.vscode/extensions.json
vendored
@@ -1,37 +0,0 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
// Syntax highlighting and more for .vue files
|
||||
// https://github.com/vuejs/vetur
|
||||
"octref.vetur",
|
||||
|
||||
// Peek and go-to-definition for .vue files
|
||||
// https://github.com/fuzinato/vscode-vue-peek
|
||||
"dariofuzinato.vue-peek",
|
||||
|
||||
// Lint-on-save with ESLint
|
||||
// https://github.com/microsoft/vscode-eslint
|
||||
"dbaeumer.vscode-eslint",
|
||||
|
||||
// Lint-on-save with Stylelint
|
||||
// https://github.com/stylelint/vscode-stylelint
|
||||
"stylelint.vscode-stylelint",
|
||||
|
||||
// Lint-on-save markdown in README files
|
||||
// https://github.com/DavidAnson/vscode-markdownlint
|
||||
"DavidAnson.vscode-markdownlint",
|
||||
|
||||
// Format-on-save with Prettier
|
||||
// https://github.com/prettier/prettier-vscode
|
||||
"esbenp.prettier-vscode",
|
||||
|
||||
// SCSS intellisense
|
||||
// https://github.com/mrmlnc/vscode-scss
|
||||
"mrmlnc.vscode-scss",
|
||||
|
||||
// Test `.unit.js` files on save with Jest
|
||||
// https://github.com/jest-community/vscode-jest
|
||||
"Orta.vscode-jest"
|
||||
]
|
||||
}
|
||||
93
ui/.vscode/settings.json
vendored
93
ui/.vscode/settings.json
vendored
@@ -1,93 +0,0 @@
|
||||
{
|
||||
// ===
|
||||
// Spacing
|
||||
// ===
|
||||
|
||||
"editor.insertSpaces": true,
|
||||
"editor.tabSize": 2,
|
||||
"editor.trimAutoWhitespace": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"files.eol": "\n",
|
||||
"files.insertFinalNewline": true,
|
||||
"files.trimFinalNewlines": true,
|
||||
|
||||
// ===
|
||||
// Files
|
||||
// ===
|
||||
|
||||
"files.exclude": {
|
||||
"**/*.log": true,
|
||||
"**/*.log*": true,
|
||||
"**/dist": true,
|
||||
"**/coverage": true
|
||||
},
|
||||
"files.associations": {
|
||||
".markdownlintrc": "jsonc"
|
||||
},
|
||||
|
||||
// ===
|
||||
// Event Triggers
|
||||
// ===
|
||||
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true,
|
||||
"source.fixAll.stylelint": true,
|
||||
"source.fixAll.markdownlint": true
|
||||
},
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"vue",
|
||||
"vue-html",
|
||||
"html"
|
||||
],
|
||||
"vetur.format.enable": false,
|
||||
"vetur.completion.scaffoldSnippetSources": {
|
||||
"user": "🗒️",
|
||||
"workspace": "💼",
|
||||
"vetur": ""
|
||||
},
|
||||
"prettier.disableLanguages": [],
|
||||
|
||||
// ===
|
||||
// HTML
|
||||
// ===
|
||||
|
||||
"html.format.enable": false,
|
||||
"vetur.validation.template": false,
|
||||
"emmet.triggerExpansionOnTab": true,
|
||||
"emmet.includeLanguages": {
|
||||
"vue-html": "html"
|
||||
},
|
||||
"vetur.completion.tagCasing": "initial",
|
||||
|
||||
// ===
|
||||
// JS(ON)
|
||||
// ===
|
||||
|
||||
"jest.autoEnable": false,
|
||||
"jest.enableCodeLens": false,
|
||||
"javascript.format.enable": false,
|
||||
"json.format.enable": false,
|
||||
"vetur.validation.script": false,
|
||||
|
||||
// ===
|
||||
// CSS
|
||||
// ===
|
||||
|
||||
"stylelint.enable": true,
|
||||
"css.validate": false,
|
||||
"scss.validate": false,
|
||||
"vetur.validation.style": false,
|
||||
|
||||
// ===
|
||||
// MARKDOWN
|
||||
// ===
|
||||
|
||||
"[markdown]": {
|
||||
"editor.wordWrap": "wordWrapColumn",
|
||||
"editor.wordWrapColumn": 80
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,9 @@ WORKDIR /app
|
||||
|
||||
# Copy dependency-related files
|
||||
COPY package.json ./
|
||||
COPY yarn.lock ./
|
||||
|
||||
# Install project dependencies
|
||||
RUN yarn install
|
||||
RUN npm install
|
||||
|
||||
# Expose ports 8080, which the dev server will be bound to
|
||||
EXPOSE 8080
|
||||
|
||||
47507
ui/package-lock.json
generated
47507
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
105
ui/package.json
105
ui/package.json
@@ -6,89 +6,80 @@
|
||||
"dev": "vue-cli-service serve",
|
||||
"dev:e2e": "cross-env VUE_APP_TEST=e2e vue-cli-service test:e2e --mode=development",
|
||||
"build": "vue-cli-service build --modern",
|
||||
"build:ci": "yarn build --report",
|
||||
"build:ci": "npm build --report",
|
||||
"lint:eslint": "eslint --fix",
|
||||
"lint:stylelint": "stylelint --fix",
|
||||
"lint:markdownlint": "markdownlint",
|
||||
"lint:prettier": "prettier --write --loglevel warn",
|
||||
"lint:all:eslint": "yarn lint:eslint --ext .js,.vue .",
|
||||
"lint:all:stylelint": "yarn lint:stylelint \"src/**/*.{vue,scss}\"",
|
||||
"lint:all:markdownlint": "yarn lint:markdownlint \"docs/*.md\" \"*.md\"",
|
||||
"lint:all:prettier": "yarn lint:prettier \"**/*.{js,json,css,scss,vue,html,md}\"",
|
||||
"lint": "run-s lint:all:*",
|
||||
"test:unit": "cross-env VUE_APP_TEST=unit vue-cli-service test:unit",
|
||||
"test:unit:file": "yarn test:unit --bail --findRelatedTests",
|
||||
"test:unit:watch": "yarn test:unit --watch --notify --notifyMode change",
|
||||
"test:unit:ci": "yarn test:unit --coverage --ci",
|
||||
"test:e2e": "cross-env VUE_APP_TEST=e2e vue-cli-service test:e2e --headless",
|
||||
"test": "run-s test:unit test:e2e",
|
||||
"test:ci": "run-s test:unit:ci test:e2e",
|
||||
"new": "cross-env HYGEN_TMPLS=generators hygen new",
|
||||
"docs": "vuepress dev",
|
||||
"docker": "docker-compose exec dev yarn"
|
||||
"docs": "vuepress dev"
|
||||
},
|
||||
"gitHooks": {
|
||||
"pre-commit": "cross-env PRE_COMMIT=true lint-staged"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.27",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.12.1",
|
||||
"@fortawesome/vue-fontawesome": "0.1.9",
|
||||
"axios": "0.19.2",
|
||||
"buefy": "^0.9.7",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.3.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.3.0",
|
||||
"@fortawesome/vue-fontawesome": "^2.0.10",
|
||||
"axios": "^1.3.2",
|
||||
"buefy": "^0.9.22",
|
||||
"chart.js": "^2.9.4",
|
||||
"core-js": "3.6.4",
|
||||
"currency-formatter": "^1.5.7",
|
||||
"date-fns": "2.10.0",
|
||||
"lodash": "4.17.15",
|
||||
"normalize.css": "8.0.1",
|
||||
"nprogress": "0.2.0",
|
||||
"vue": "2.6.11",
|
||||
"core-js": "^3.27.2",
|
||||
"currency-formatter": "^1.5.9",
|
||||
"date-fns": "^2.29.3",
|
||||
"lodash": "^4.17.21",
|
||||
"node-gyp": "^9.3.1",
|
||||
"normalize.css": "^8.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"vue": "^2.6.11",
|
||||
"vue-chartjs": "^3.5.1",
|
||||
"vue-meta": "2.3.3",
|
||||
"vue-router": "3.1.6",
|
||||
"vuex": "3.1.2"
|
||||
"vue-i18n": "^8.28.2",
|
||||
"vue-meta": "^2.4.0",
|
||||
"vue-router": "^3.6.5",
|
||||
"vuex": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "4.2.x",
|
||||
"@vue/cli-plugin-eslint": "4.2.x",
|
||||
"@vue/cli-plugin-unit-jest": "4.2.x",
|
||||
"@vue/cli-service": "4.2.x",
|
||||
"@vue/cli-plugin-babel": "^4.5.19",
|
||||
"@vue/cli-plugin-eslint": "^4.5.19",
|
||||
"@vue/cli-plugin-unit-jest": "^4.5.19",
|
||||
"@vue/cli-service": "^4.5.19",
|
||||
"@vue/eslint-config-prettier": "6.0.x",
|
||||
"@vue/eslint-config-standard": "5.1.x",
|
||||
"@vue/test-utils": "1.0.0-beta.31",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"@vue/eslint-config-standard": "^5.1.1",
|
||||
"@vue/test-utils": "^1.3.4",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-eslint": "10.1.x",
|
||||
"cross-env": "7.0.x",
|
||||
"eslint": "6.8.x",
|
||||
"eslint-plugin-import": "2.20.x",
|
||||
"eslint-plugin-node": "11.0.x",
|
||||
"eslint-plugin-promise": "4.2.x",
|
||||
"eslint-plugin-standard": "4.0.x",
|
||||
"eslint-plugin-vue": "6.2.x",
|
||||
"express": "4.17.x",
|
||||
"hygen": "4.0.x",
|
||||
"imagemin-lint-staged": "0.4.x",
|
||||
"lint-staged": "10.0.x",
|
||||
"markdownlint-cli": "0.22.x",
|
||||
"npm-run-all": "4.1.x",
|
||||
"sass": "1.26.x",
|
||||
"sass-loader": "8.0.x",
|
||||
"stylelint": "13.2.x",
|
||||
"stylelint-config-css-modules": "2.2.x",
|
||||
"stylelint-config-prettier": "8.0.x",
|
||||
"stylelint-config-recess-order": "2.0.x",
|
||||
"stylelint-config-standard": "20.0.x",
|
||||
"stylelint-scss": "3.14.x",
|
||||
"vue-template-compiler": "2.6.11",
|
||||
"vuepress": "1.3.x"
|
||||
"cross-env": "^7.0.1",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.3.1",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"eslint-plugin-vue": "^9.9.0",
|
||||
"express": "^4.18.2",
|
||||
"hygen": "^6.2.11",
|
||||
"markdownlint-cli": "^0.33.0",
|
||||
"npm-run-all": "^4.1.1",
|
||||
"sass": "^1.58.0",
|
||||
"sass-loader": "^8.0.2",
|
||||
"stylelint": "^14.16.1",
|
||||
"stylelint-config-css-modules": "^4.1.0",
|
||||
"stylelint-config-prettier": "^9.0.4",
|
||||
"stylelint-config-standard": "^29.0.0",
|
||||
"stylelint-scss": "^4.3.0",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vuepress": "^1.9.8"
|
||||
},
|
||||
"resolutions": {
|
||||
"@vue/cli-plugin-unit-jest/jest": "25.1.x",
|
||||
"@vue/cli-plugin-unit-jest/babel-jest": "25.1.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.3",
|
||||
"yarn": ">=1.0.0"
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 463 B After Width: | Height: | Size: 895 B |
@@ -5,7 +5,12 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<link rel="shortcut icon" href="<%= webpackConfig.output.publicPath %>hammond.png" />
|
||||
<link rel="apple-touch-icon" href="<%= webpackConfig.output.publicPath %>touch-icon.png" />
|
||||
<title><%= webpackConfig.name %></title>
|
||||
<!-- Temporary until fontawesome 6 is supported in buefy (see issue: https://github.com/FortAwesome/Font-Awesome/issues/18663) -->
|
||||
<style>
|
||||
.icon svg { width: 1em; height: 1em; max-width: 80%; max-height: 80%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- This is where our app will be mounted. -->
|
||||
|
||||
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">
|
||||
@@ -95,14 +95,13 @@ export default {
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-button
|
||||
tag="input"
|
||||
tag="button"
|
||||
native-type="submit"
|
||||
:disabled="tryingToCreate"
|
||||
type="is-primary"
|
||||
value="Upload File"
|
||||
class="control"
|
||||
>
|
||||
Upload File
|
||||
{{ $t('uploadfile') }}
|
||||
</b-button>
|
||||
</div></div
|
||||
>
|
||||
|
||||
@@ -3,9 +3,20 @@ import { Line } from 'vue-chartjs'
|
||||
|
||||
import axios from 'axios'
|
||||
import { mapState } from 'vuex'
|
||||
import { string } from 'yargs'
|
||||
export default {
|
||||
extends: Line,
|
||||
props: { vehicle: { type: Object, required: true }, since: { type: Date, default: '' }, user: { type: Object, required: true } },
|
||||
props: {
|
||||
vehicle: { type: Object, required: true },
|
||||
since: { type: Date, default: '' },
|
||||
user: { type: Object, required: true },
|
||||
mileageOption: { type: string, default: 'litre_100km' },
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
chartData: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('utils', ['isMobile']),
|
||||
},
|
||||
@@ -17,20 +28,28 @@ export default {
|
||||
this.fetchMileage()
|
||||
},
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
chartData: [],
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchMileage()
|
||||
},
|
||||
methods: {
|
||||
showChart() {
|
||||
let mileageLabel = ''
|
||||
switch (this.mileageOption) {
|
||||
case 'litre_100km':
|
||||
mileageLabel = 'L/100km'
|
||||
break
|
||||
case 'km_litre':
|
||||
mileageLabel = 'km/L'
|
||||
break
|
||||
case 'mpg':
|
||||
mileageLabel = 'mpg'
|
||||
break
|
||||
}
|
||||
|
||||
var labels = this.chartData.map((x) => x.date.substr(0, 10))
|
||||
var dataset = {
|
||||
steppedLine: true,
|
||||
label: `Mileage (${this.user.distanceUnitDetail.short}/${this.vehicle.fuelUnitDetail.short})`,
|
||||
label: `Mileage (${mileageLabel})`,
|
||||
fill: true,
|
||||
data: this.chartData.map((x) => x.mileage),
|
||||
}
|
||||
@@ -41,6 +60,7 @@ export default {
|
||||
.get(`/api/vehicles/${this.vehicle.id}/mileage`, {
|
||||
params: {
|
||||
since: this.since,
|
||||
mileageOption: this.mileageOption,
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
|
||||
@@ -10,42 +10,42 @@ export default {
|
||||
persistentNavRoutes: [
|
||||
{
|
||||
name: 'home',
|
||||
title: 'Home',
|
||||
title: this.$t('menu.home'),
|
||||
},
|
||||
],
|
||||
loggedInNavRoutes: [
|
||||
{
|
||||
name: 'quickEntries',
|
||||
title: () => 'Quick Entries',
|
||||
title: () => this.$t('menu.quickentries'),
|
||||
badge: () => this.unprocessedQuickEntries.length,
|
||||
},
|
||||
{
|
||||
name: 'import',
|
||||
title: () => 'Import',
|
||||
title: () => this.$t('menu.import'),
|
||||
},
|
||||
{
|
||||
name: 'settings',
|
||||
title: 'Settings',
|
||||
title: this.$t('menu.settings'),
|
||||
},
|
||||
{
|
||||
name: 'logout',
|
||||
title: 'Log out',
|
||||
title: this.$t('menu.logout'),
|
||||
},
|
||||
],
|
||||
loggedOutNavRoutes: [
|
||||
{
|
||||
name: 'login',
|
||||
title: 'Log in',
|
||||
title: this.$t('menu.login'),
|
||||
},
|
||||
],
|
||||
adminNavRoutes: [
|
||||
{
|
||||
name: 'site-settings',
|
||||
title: 'Site Settings',
|
||||
title: this.$t('menu.sitesettings'),
|
||||
},
|
||||
{
|
||||
name: 'users',
|
||||
title: 'Users',
|
||||
title: this.$t('menu.users'),
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -72,7 +72,7 @@ export default {
|
||||
<NavBarRoutes :routes="persistentNavRoutes" />
|
||||
<NavBarRoutes v-if="loggedIn" :routes="loggedInNavRoutes" />
|
||||
<NavBarRoutes v-else :routes="loggedOutNavRoutes" />
|
||||
<b-navbar-dropdown v-if="loggedIn && isAdmin" label="Admin">
|
||||
<b-navbar-dropdown v-if="loggedIn && isAdmin" :label="$t('menu.admin')">
|
||||
<NavBarRoutes :routes="adminNavRoutes" />
|
||||
</b-navbar-dropdown>
|
||||
</template>
|
||||
|
||||
@@ -50,12 +50,12 @@ export default {
|
||||
<b-select
|
||||
v-if="unprocessedQuickEntries.length"
|
||||
v-model="quickEntry"
|
||||
placeholder="Refer quick entry"
|
||||
:placeholder="$t('referquickentry')"
|
||||
expanded
|
||||
@input="showQuickEntry($event)"
|
||||
>
|
||||
<option v-for="option in unprocessedQuickEntries" :key="option.id" :value="option">
|
||||
Taken: {{ parseAndFormatDateTime(option.createdAt) }}
|
||||
{{ $t('created') }}: {{ parseAndFormatDateTime(option.createdAt) }}
|
||||
</option>
|
||||
</b-select>
|
||||
<p class="control">
|
||||
|
||||
@@ -55,7 +55,7 @@ export default {
|
||||
return
|
||||
}
|
||||
this.$buefy.dialog.confirm({
|
||||
title: 'Transfer Vehicle',
|
||||
title: this.$t('transfervehicle'),
|
||||
message: 'Are you sure you want to do this? You will lose ownership and all editing rights if you confirm.',
|
||||
cancelText: 'Cancel',
|
||||
confirmText: 'Go Ahead',
|
||||
@@ -90,9 +90,9 @@ export default {
|
||||
|
||||
<template>
|
||||
<div class="box" style="max-width:600px">
|
||||
<h1 class="subtitle">Share {{ vehicle.nickname }}</h1>
|
||||
<h1 class="subtitle">{{ $t('share') }} {{ vehicle.nickname }}</h1>
|
||||
<section>
|
||||
<div class="columns is-mobile" v-for="model in models" :key="model.id">
|
||||
<div v-for="model in models" :key="model.id" class="columns is-mobile">
|
||||
<div class="column is-one-third">
|
||||
<b-field>
|
||||
<b-switch v-model="model.isShared" :disabled="model.isOwner" @input="changeShareStatus(model)">
|
||||
@@ -101,7 +101,7 @@ export default {
|
||||
</b-field> </div
|
||||
><div class="column is-three-quarters">
|
||||
<b-field>
|
||||
<b-button v-if="model.isShared && !model.isOwner" type="is-primary is-small" @click="transferVehicle(model)">Make Owner</b-button>
|
||||
<b-button v-if="model.isShared && !model.isOwner" type="is-primary is-small" @click="transferVehicle(model)">{{ $t('makeowner') }}</b-button>
|
||||
</b-field></div
|
||||
></div
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { addDays, addMonths } from 'date-fns'
|
||||
import currencyFormtter from 'currency-formatter'
|
||||
import currencyFormatter from 'currency-formatter'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
import axios from 'axios'
|
||||
@@ -14,12 +14,12 @@ export default {
|
||||
data: function() {
|
||||
return {
|
||||
dateRangeOptions: [
|
||||
{ label: 'This week', value: 'this_week' },
|
||||
{ label: 'This month', value: 'this_month' },
|
||||
{ label: 'Past 30 days', value: 'past_30_days' },
|
||||
{ label: 'Past 3 months', value: 'past_3_months' },
|
||||
{ label: 'This year', value: 'this_year' },
|
||||
{ label: 'All Time', value: 'all_time' },
|
||||
{ label: this.$t('thisweek'), value: 'this_week' },
|
||||
{ label: this.$t('thismonth'), value: 'this_month' },
|
||||
{ label: this.$tc('pastxdays', 30), value: 'past_30_days' },
|
||||
{ label: this.$tc('pastxmonths', 3), value: 'past_3_months' },
|
||||
{ label: this.$t('thisyear'), value: 'this_year' },
|
||||
{ label: this.$t('alltime'), value: 'all_time' },
|
||||
],
|
||||
dateRangeOption: 'past_30_days',
|
||||
stats: [],
|
||||
@@ -32,15 +32,15 @@ export default {
|
||||
return [
|
||||
[
|
||||
{
|
||||
label: 'Total Expenditure',
|
||||
label: this.$t('totalexpenses'),
|
||||
value: this.formatCurrency(0, this.user.currency),
|
||||
},
|
||||
{
|
||||
label: 'Fillup Costs',
|
||||
label: this.$t('fillupcost'),
|
||||
value: `${this.formatCurrency(0, this.user.currency)} (0)`,
|
||||
},
|
||||
{
|
||||
label: 'Other Expenses',
|
||||
label: this.$t('otherexpenses'),
|
||||
value: `${this.formatCurrency(0, this.user.currency)} (0)`,
|
||||
},
|
||||
],
|
||||
@@ -49,15 +49,15 @@ export default {
|
||||
return this.stats.map((x) => {
|
||||
return [
|
||||
{
|
||||
label: 'Total Expenditure',
|
||||
label: this.$t('totalexpenses'),
|
||||
value: this.formatCurrency(x.expenditureTotal, x.currency),
|
||||
},
|
||||
{
|
||||
label: 'Fillup Costs',
|
||||
label: this.$t('fillupcost'),
|
||||
value: `${this.formatCurrency(x.expenditureFillups, x.currency)} (${x.countFillups})`,
|
||||
},
|
||||
{
|
||||
label: 'Other Expenses',
|
||||
label: this.$t('otherexpenses'),
|
||||
value: `${this.formatCurrency(x.expenditureExpenses, x.currency)} (${x.countExpenses})`,
|
||||
},
|
||||
]
|
||||
@@ -80,7 +80,7 @@ export default {
|
||||
if (!currencyCode) {
|
||||
currencyCode = this.me.currency
|
||||
}
|
||||
return currencyFormtter.format(number, { code: currencyCode })
|
||||
return currencyFormatter.format(number, { code: currencyCode })
|
||||
},
|
||||
getStats() {
|
||||
axios
|
||||
@@ -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">
|
||||
|
||||
@@ -10,10 +10,10 @@ $size-input-padding-vertical: 0.75em;
|
||||
$size-input-padding-horizontal: 1em;
|
||||
$size-input-padding: $size-input-padding-vertical $size-input-padding-horizontal;
|
||||
$size-input-border: 1px;
|
||||
$size-input-border-radius: (1em + $size-input-padding-vertical * 2) / 10;
|
||||
$size-input-border-radius: calc((1em + $size-input-padding-vertical * 2) / 10);
|
||||
|
||||
// BUTTONS
|
||||
$size-button-padding-vertical: $size-grid-padding / 2;
|
||||
$size-button-padding-horizontal: $size-grid-padding / 1.5;
|
||||
$size-button-padding-vertical: calc($size-grid-padding / 2);
|
||||
$size-button-padding-horizontal: calc($size-grid-padding / 1.5);
|
||||
$size-button-padding: $size-button-padding-vertical
|
||||
$size-button-padding-horizontal;
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
$max-screen,
|
||||
$max-value
|
||||
) {
|
||||
$a: ($max-value - $min-value) / ($max-screen - $min-screen);
|
||||
$a: calc(($max-value - $min-value) / ($max-screen - $min-screen));
|
||||
$b: $min-value - $a * $min-screen;
|
||||
|
||||
$sign: '+';
|
||||
|
||||
25
ui/src/i18n.js
Normal file
25
ui/src/i18n.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import Vue from 'vue';
|
||||
import VueI18n from 'vue-i18n';
|
||||
|
||||
Vue.use(VueI18n);
|
||||
|
||||
function loadLocaleMessages () {
|
||||
const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i)
|
||||
const messages = {}
|
||||
locales.keys().forEach(key => {
|
||||
const matched = key.match(/([A-Za-z0-9-_]+)\./i)
|
||||
if (matched && matched.length > 1) {
|
||||
const locale = matched[1]
|
||||
messages[locale] = locales(key)
|
||||
}
|
||||
})
|
||||
return messages
|
||||
}
|
||||
|
||||
const i18n = new VueI18n({
|
||||
locale: navigator.language.split('-')[0] || 'en',
|
||||
fallbackLocale: 'en',
|
||||
messages: loadLocaleMessages()
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
231
ui/src/locales/da.json
Normal file
231
ui/src/locales/da.json
Normal file
@@ -0,0 +1,231 @@
|
||||
{
|
||||
"quickentry": "Ingen hurtige indtastninger | Hurtig indtastning | Hurtige indtastninger",
|
||||
"statistics": "Statistik",
|
||||
"thisweek": "Denne uge",
|
||||
"thismonth": "Denne måned",
|
||||
"pastxdays": "Forrige en dag | Forrige {count} dage",
|
||||
"pastxmonths": "Forrige en måned | Forrige {count} måneder",
|
||||
"thisyear": "Dette år",
|
||||
"alltime": "Hele tiden",
|
||||
"noattachments": "Ingen vedhæftede filer indtil videre",
|
||||
"attachments": "Vedhæftede filer",
|
||||
"choosefile": "Vælg fil",
|
||||
"addattachment": "Tilføj vedhæftning",
|
||||
"sharedwith": "Delt med",
|
||||
"share": "Del",
|
||||
"you": "Du",
|
||||
"addfillup": "Tilføj påfyldning",
|
||||
"createfillup": "Opret påfyldning",
|
||||
"deletefillup": "Slet denne påfyldning",
|
||||
"addexpense": "Tilføj udgift",
|
||||
"createexpense": "Opret udgift",
|
||||
"deleteexpense": "Slet denne udgift",
|
||||
"nofillups": "Ingen påfyldninger indtil videre",
|
||||
"transfervehicle": "Overfør køretøj",
|
||||
"settingssaved": "Indstillingerne er gemt",
|
||||
"yoursettings": "Dine indstillinger",
|
||||
"settings": "Indstillinger",
|
||||
"changepassword": "Skift adgangskode",
|
||||
"oldpassword": "Gammel adgangskode",
|
||||
"newpassword": "Ny adgangskode",
|
||||
"repeatnewpassword": "Gentag ny adgangskode",
|
||||
"passworddontmatch": "Adgangskoderne stemmer ikke overens",
|
||||
"save": "Gem",
|
||||
"supportthedeveloper": "Støt udvikleren",
|
||||
"buyhimabeer": "Køb ham en øl!",
|
||||
"featurerequest": "Ønske om funktion",
|
||||
"foundabug": "Fundet en fejl",
|
||||
"currentversion": "Nuværende version",
|
||||
"moreinfo": "Mere info",
|
||||
"currency": "Valuta",
|
||||
"distanceunit": "Afstandsenhed",
|
||||
"dateformat": "Datoformat",
|
||||
"createnow": "Opret nu",
|
||||
"yourvehicles": "Dine køretøjer",
|
||||
"menu": {
|
||||
"quickentries": "Hurtige indtastninger",
|
||||
"logout": "Log ud",
|
||||
"import": "Import",
|
||||
"home": "Hjem",
|
||||
"settings": "Indstillinger",
|
||||
"admin": "Admin",
|
||||
"sitesettings": "Webstedsindstillinger",
|
||||
"users": "Brugere",
|
||||
"login": "Log ind"
|
||||
},
|
||||
"enterusername": "Indtast dit brugernavn",
|
||||
"enterpassword": "Indtast din adgangskode",
|
||||
"email": "E-mail",
|
||||
"password": "Adgangskode",
|
||||
"login": "Log ind",
|
||||
"totalexpenses": "Samlede udgifter",
|
||||
"fillupcost": "Tankningsomkostninger",
|
||||
"otherexpenses": "Andre udgifter",
|
||||
"addvehicle": "Tilføj køretøj",
|
||||
"editvehicle": "Rediger køretøj",
|
||||
"deletevehicle": "Slet køretøj",
|
||||
"sharevehicle": "Del køretøj",
|
||||
"makeowner": "Gør til ejer",
|
||||
"lastfillup": "Seneste tankning",
|
||||
"quickentrydesc": "Tag et billede af fakturaen eller brændstofpumpens display for at foretage en indtastning senere.",
|
||||
"quickentrycreatedsuccessfully": "Hurtig indtastning oprettet med succes",
|
||||
"uploadfile": "Upload fil",
|
||||
"uploadphoto": "Upload foto",
|
||||
"details": "Detaljer",
|
||||
"odometer": "Kilometertæller",
|
||||
"language": "Sprog",
|
||||
"date": "Dato",
|
||||
"pastfillups": "Tidligere tankninger",
|
||||
"fuelsubtype": "Brændstoftype",
|
||||
"fueltype": "Brændstoftype",
|
||||
"quantity": "Mængde",
|
||||
"gasstation": "Tankstation",
|
||||
"fuel": {
|
||||
"petrol": "Benzin",
|
||||
"diesel": "Diesel",
|
||||
"cng": "CNG",
|
||||
"lpg": "LPG",
|
||||
"electric": "Elektrisk",
|
||||
"ethanol": "Ethanol"
|
||||
},
|
||||
"unit": {
|
||||
"long": {
|
||||
"litre": "Liter",
|
||||
"gallon": "Gallon",
|
||||
"kilowatthour": "Kilowatt-time",
|
||||
"kilogram": "Kilogram",
|
||||
"usgallon": "US Gallon",
|
||||
"minutes": "Minutter",
|
||||
"kilometers": "Kilometer",
|
||||
"miles": "Miles"
|
||||
},
|
||||
"short": {
|
||||
"litre": "L",
|
||||
"gallon": "Gal",
|
||||
"kilowatthour": "KwH",
|
||||
"kilogram": "Kg",
|
||||
"usgallon": "US Gal",
|
||||
"minutes": "Min",
|
||||
"kilometers": "Km",
|
||||
"miles": "Mi"
|
||||
}
|
||||
},
|
||||
"avgfillupqty": "Gns. påfyldningsmængde",
|
||||
"avgfillupexpense": "Gns. påfyldningsudgift",
|
||||
"avgfuelcost": "Gns. brændstofpris",
|
||||
"per": "{0} per {1}",
|
||||
"price": "Pris",
|
||||
"total": "Total",
|
||||
"fulltank": "Tank fuld",
|
||||
"partialfillup": "Delvis påfyldning",
|
||||
"getafulltank": "Fik du en fuld tank?",
|
||||
"tankpartialfull": "Hvad sporer du?",
|
||||
"by": "Ved",
|
||||
"expenses": "Udgifter",
|
||||
"expensetype": "Udgiftstype",
|
||||
"noexpenses": "Ingen udgifter indtil videre",
|
||||
"download": "Download",
|
||||
"title": "Titel",
|
||||
"name": "Navn",
|
||||
"delete": "Slet",
|
||||
"importdata": "Importer data til Hammond",
|
||||
"importdatadesc": "Vælg en af følgende muligheder for at importere data til Hammond",
|
||||
"import": "Importer",
|
||||
"importcsv": "Hvis du har brugt {name} til at gemme dine køretøjsdata, skal du eksportere CSV-filen fra {name} og klikke her for at importere.",
|
||||
"importgeneric": "Generisk påfyldningsimport",
|
||||
"importgenericdesc": "CSV-import af påfyldninger.",
|
||||
"choosecsv": "Vælg CSV",
|
||||
"choosephoto": "Vælg foto",
|
||||
"importsuccessfull": "Data importeret med succes",
|
||||
"importerror": "Der opstod et problem med at importere filen. Kontrollér fejlmeddelelsen",
|
||||
"importfrom": "Importer fra {0}",
|
||||
"stepstoimport": "Trin til import af data fra {name}",
|
||||
"choosecsvimport": "Vælg {name} CSV-filen og klik på importknappen.",
|
||||
"choosedatafile": "Vælg CSV-filen og klik derefter på importknappen.",
|
||||
"dontimportagain": "Sørg for ikke at importere filen igen, da det vil skabe gentagne indtastninger.",
|
||||
"checkpointsimportcsv": "Når du har kontrolleret alle disse punkter, kan du blot importere CSV'en nedenfor.",
|
||||
"importhintunits": "På samme måde skal du sørge for, at <u>Brændstofenhed</u> og <u>Brændstoftype</u> er korrekt indstillet for køretøjet.",
|
||||
"importhintcurrdist": "Sørg for, at <u>Valuta</u> og <u>Afstandsenhed</u> er korrekt indstillet i Hammond. Importen vil ikke automatisk registrere valutaen fra filen, men bruge den valuta, der er indstillet for brugeren.",
|
||||
"importhintnickname": "Sørg for, at køretøjets kaldenavn i Hammond er præcis det samme som navnet i Fuelly CSV-filen, ellers vil importen ikke fungere.",
|
||||
"importhintvehiclecreated": "Sørg for, at du allerede har oprettet køretøjerne i Hammond-platformen.",
|
||||
"importhintcreatecsv": "Eksportér dine data fra {name} i CSV-format. Instruktioner til at gøre dette kan findes",
|
||||
"importgenerichintdata": "Data skal være i CSV-format.",
|
||||
"here": "her",
|
||||
"unprocessedquickentries": "Du har en hurtig indtastning, der skal behandles. | Du har {0} hurtige indtastninger, der venter på at blive behandlet.",
|
||||
"show": "Vis",
|
||||
"loginerror": "Der opstod en fejl ved login til din konto. {msg}",
|
||||
"showunprocessed": "Vis kun ubehandlede",
|
||||
"unprocessed": "ubehandlede",
|
||||
"sitesettingdesc": "Opdater indstillinger på webstedsniveau. Disse vil blive brugt som standardværdier for nye brugere.",
|
||||
"settingdesc": "Disse vil blive brugt som standardværdier, når du opretter en ny påfyldning eller udgift.",
|
||||
"areyousure": "Er du sikker på, at du vil fortsætte med dette?",
|
||||
"adduser": "Tilføj bruger",
|
||||
"usercreatedsuccessfully": "Bruger oprettet med succes",
|
||||
"userdisabledsuccessfully": "Bruger deaktiveret med succes",
|
||||
"userenabledsuccessfully": "Bruger aktiveret med succes",
|
||||
"role": "Rolle",
|
||||
"created": "Oprettet",
|
||||
"createnewuser": "Opret ny bruger",
|
||||
"cancel": "Annuller",
|
||||
"novehicles": "Det ser ud til, at du endnu ikke har oprettet et køretøj i systemet. Begynd med at oprette en indtastning for et af de køretøjer, du gerne vil spore.",
|
||||
"processed": "Marker som behandlet",
|
||||
"notfound": "Ikke fundet",
|
||||
"timeout": "Siden fik en timeout under indlæsningen. Er du stadig forbundet til internettet?",
|
||||
"clicktoselect": "Klik for at vælge...",
|
||||
"expenseby": "Udgift af",
|
||||
"selectvehicle": "Vælg et køretøj",
|
||||
"expensedate": "Udgiftsdato",
|
||||
"totalamountpaid": "Samlet betalt beløb",
|
||||
"fillmoredetails": "Udfyld flere oplysninger",
|
||||
"markquickentryprocessed": "Marker valgt hurtig indtastning som behandlet",
|
||||
"referquickentry": "Henvis hurtig indtastning",
|
||||
"deletequickentry": "Dette vil slette denne hurtige indtastning. Dette trin kan ikke fortrydes. Er du sikker?",
|
||||
"fuelunit": "Brændstofenhed",
|
||||
"fillingstation": "Navn på tankstation",
|
||||
"comments": "Kommentarer",
|
||||
"missfillupbefore": "Glemte du at indtaste påfyldningen før denne?",
|
||||
"missedfillup": "Glemt påfyldning",
|
||||
"fillupdate": "Påfyldningsdato",
|
||||
"fillupsavedsuccessfully": "Påfyldning gemt med succes",
|
||||
"expensesavedsuccessfully": "Udgift gemt med succes",
|
||||
"vehiclesavedsuccessfully": "Køretøj gemt med succes",
|
||||
"settingssavedsuccessfully": "Indstillinger gemt med succes",
|
||||
"back": "Tilbage",
|
||||
"nickname": "Kælenavn",
|
||||
"registration": "Registrering",
|
||||
"createvehicle": "Opret køretøj",
|
||||
"make": "Mærke / Firma",
|
||||
"model": "Model",
|
||||
"yearmanufacture": "Produktionsår",
|
||||
"enginesize": "Motorsize (i cc)",
|
||||
"mysqlconnstr": "MySQL-forbindelsesstreng",
|
||||
"testconn": "Test forbindelse",
|
||||
"migrate": "Migrer",
|
||||
"init": {
|
||||
"migrateclarkson": "Migrer fra Clarkson",
|
||||
"migrateclarksondesc": "Hvis du har en eksisterende Clarkson-installation, og du vil migrere dine data fra den, skal du trykke på følgende knap.",
|
||||
"freshinstall": "Ny installation",
|
||||
"freshinstalldesc": "Hvis du vil have en ny installation af Hammond, skal du trykke på følgende knap.",
|
||||
"clarkson": {
|
||||
"desc": "<p>Du skal sørge for, at denne installation af Hammond kan få adgang til MySQL-databasen, der bruges af Clarkson.</p><p>Hvis det ikke er direkte muligt, kan du lave en kopi af den database et sted, der er tilgængeligt fra denne instans.</p><p>Når det er gjort, skal du indtaste forbindelsesstrengen til MySQL-instansen i følgende format.</p><p>Alle brugerne importeret fra Clarkson vil have deres brugernavn som deres e-mail i Clarkson-databasen, og adgangskoden er indstillet til<span class='' style='font-weight:bold'>hammond</span></p><code>bruger:adgangskode@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local</code><br/><br/>",
|
||||
"success": "Vi har migreret dataene fra Clarkson med succes. Du vil snart blive omdirigeret til login-siden, hvor du kan logge ind med din eksisterende e-mail og adgangskode: hammond"
|
||||
},
|
||||
"fresh": {
|
||||
"setupadminuser": "Opsætning af administratorbrugere",
|
||||
"yourpassword": "Din adgangskode",
|
||||
"youremail": "Din e-mail",
|
||||
"yourname": "Dit navn",
|
||||
"success": "Du er blevet registreret med succes. Du vil snart blive omdirigeret til login-siden, hvor du kan logge ind og begynde at bruge systemet."
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"ADMIN": "ADMIN",
|
||||
"USER": "BRUGER"
|
||||
},
|
||||
"profile": "Profil",
|
||||
"processedon": "Behandlet den",
|
||||
"enable": "Aktivér",
|
||||
"disable": "Deaktiver",
|
||||
"confirm": "Fortsæt",
|
||||
"labelforfile": "Mærke for denne fil"
|
||||
}
|
||||
217
ui/src/locales/de.json
Normal file
217
ui/src/locales/de.json
Normal file
@@ -0,0 +1,217 @@
|
||||
{
|
||||
"quickentry": "Keine Schnelleinträge | Schnelleintrag | Schnelleinträge",
|
||||
"statistics": "Statistiken",
|
||||
"thisweek": "Diese Woche",
|
||||
"thismonth": "Dieser Monat",
|
||||
"pastxdays": "Letzter Tag | Letzte {count} Tage",
|
||||
"pastxmonths": "Letzter Monat | Letzte {count} Monate",
|
||||
"thisyear": "Dieses Jahr",
|
||||
"alltime": "Gesamt",
|
||||
"noattachments": "Keine Anhänge",
|
||||
"attachments": "Anhänge",
|
||||
"choosefile": "Datei auswählen",
|
||||
"addattachment": "Anhang hinzufügen",
|
||||
"sharedwith": "Geteilt mit",
|
||||
"share": "Teile",
|
||||
"you": "du",
|
||||
"addfillup": "Tankfüllung erfassen",
|
||||
"createfillup": "Erfasse Tankfüllung",
|
||||
"deletefillup": "Lösche diese Tankfüllung",
|
||||
"addexpense": "Ausgabe erfassen",
|
||||
"createexpense": "Erfasse Ausgabe",
|
||||
"deleteexpense": "Lösche diese Ausgabe",
|
||||
"nofillups": "Keine Tankfüllungen",
|
||||
"transfervehicle": "Fahrzeug übertragen",
|
||||
"settingssaved": "Einstellungen erfolgreich gespeichert",
|
||||
"yoursettings": "Deine Einstellungen",
|
||||
"settings": "Einstellungen",
|
||||
"changepassword": "Passwort ändern",
|
||||
"oldpassword": "Bisheriges Passwort",
|
||||
"newpassword": "Neues Passwort",
|
||||
"repeatnewpassword": "Neues Passwort wiederholen",
|
||||
"passworddontmatch": "Passwörter stimmen nicht überein",
|
||||
"save": "Speichern",
|
||||
"supportthedeveloper": "Unterstütze den Entwickler",
|
||||
"buyhimabeer": "Kauf ihm ein Bier!",
|
||||
"moreinfo": "Mehr Info",
|
||||
"currency": "Währung",
|
||||
"distanceunit": "Entfernungseinheit",
|
||||
"dateformat": "Datumsformat",
|
||||
"createnow": "Jetzt erstellen",
|
||||
"yourvehicles": "Deine Fahrzeuge",
|
||||
"menu": {
|
||||
"quickentries": "Schnellinträge",
|
||||
"logout": "Abmelden",
|
||||
"import": "Import",
|
||||
"home": "Start",
|
||||
"settings": "Einstellungen",
|
||||
"admin": "Verwalten",
|
||||
"sitesettings": "Globale Einstellungen",
|
||||
"users": "Benutzer",
|
||||
"login": "Anmelden"
|
||||
},
|
||||
"enterusername": "E-Mail eingeben",
|
||||
"enterpassword": "Passwort eingeben",
|
||||
"email": "E-Mail",
|
||||
"password": "Passwort",
|
||||
"login": "Anmelden",
|
||||
"totalexpenses": "Gesamtausgaben",
|
||||
"fillupcost": "Tank-Ausgaben",
|
||||
"otherexpenses": "Andere Ausgaben",
|
||||
"addvehicle": "Fahrzeug hinzufügen",
|
||||
"editvehicle": "Fahrzeug bearbeiten",
|
||||
"deletevehicle": "Fahrzeug löschen",
|
||||
"sharevehicle": "Fahrzeug teilen",
|
||||
"makeowner": "zum Besitzer machen",
|
||||
"lastfillup": "Letztes Tanken",
|
||||
"quickentrydesc": "Mach ein Foto deiner Rechnung oder der Zapfsäule um den Eintrag später zu ergänzen.",
|
||||
"quickentrycreatedsuccessfully": "Schnelleintrag erfolgreich erstellt",
|
||||
"uploadfile": "Datei hochladen",
|
||||
"uploadphoto": "Foto hochladen",
|
||||
"details": "Details",
|
||||
"odometer": "Kilometerzähler",
|
||||
"language": "Sprache",
|
||||
"date": "Datum",
|
||||
"pastfillups": "Tankfüllungen",
|
||||
"fuelsubtype": "Kraftstofftyp",
|
||||
"fueltype": "Kraftstoff",
|
||||
"quantity": "Menge",
|
||||
"gasstation": "Tankstelle",
|
||||
"fuel": {
|
||||
"petrol": "Benzin",
|
||||
"diesel": "Diesel",
|
||||
"cng": "CNG",
|
||||
"lpg": "LPG",
|
||||
"electric": "Strom",
|
||||
"ethanol": "Ethanol"
|
||||
},
|
||||
"unit": {
|
||||
"long": {
|
||||
"litre": "Liter",
|
||||
"gallon": "Gallone",
|
||||
"kilowatthour": "Kilowattstunde",
|
||||
"kilogram": "Kilogramm",
|
||||
"usgallon": "US-Gallone",
|
||||
"minutes": "Minuten",
|
||||
"kilometers": "Kilometer",
|
||||
"miles": "Meilen"
|
||||
},
|
||||
"short": {
|
||||
"litre": "L",
|
||||
"gallon": "Gal",
|
||||
"kilowatthour": "KwH",
|
||||
"kilogram": "Kg",
|
||||
"usgallon": "US-Gal",
|
||||
"minutes": "Min",
|
||||
"kilometers": "Km",
|
||||
"miles": "Mi"
|
||||
}
|
||||
},
|
||||
"avgfillupqty": "Ø Tankmenge",
|
||||
"avgfillupexpense": "Ø Tankwert",
|
||||
"avgfuelcost": "Ø Spritpreis",
|
||||
"per": "{0} pro {1}",
|
||||
"price": "Preis",
|
||||
"total": "Gesamt",
|
||||
"fulltank": "Voller Tank",
|
||||
"getafulltank": "Hast du vollgetankt?",
|
||||
"by": "Von",
|
||||
"expenses": "Ausgaben",
|
||||
"expensetype": "Ausgaben Typ",
|
||||
"noexpenses": "Keine Ausgaben",
|
||||
"download": "Herunterladen",
|
||||
"title": "Titel",
|
||||
"name": "Name",
|
||||
"delete": "Löschen",
|
||||
"importdata": "Importiere Daten in Hammond",
|
||||
"importdatadesc": "Wähle eine der folgenden Optionen, um Daten in Hammond zu importieren",
|
||||
"import": "Importieren",
|
||||
"importcsv": "Wenn du {name} nutzt, um deine Fahrzeugdaten zu verwalten, exportiere die CSV Datei aus {name} und klicke hier, um zu importieren.",
|
||||
"choosecsv": "CSV auswählen",
|
||||
"choosephoto": "Foto auswählen",
|
||||
"importsuccessfull": "Daten erfolgreich importiert",
|
||||
"importerror": "Beim Importieren der Datei ist ein Fehler aufgetreten. Details findest du in der Fehlermeldung",
|
||||
"importfrom": "Importiere von {name}",
|
||||
"stepstoimport": "Schritte, um Daten aus {name} zu importieren",
|
||||
"choosecsvimport": "Wähle die {name} CSV aus und klicke den Button, um zu importieren.",
|
||||
"dontimportagain": "Achte darauf, dass du die Datei nicht erneut importierst, da dies zu mehrfachen Einträgen führen würde.",
|
||||
"checkpointsimportcsv": "Wenn du alle diese Punkte überprüft hast kannst du unten die CSV importieren.",
|
||||
"importhintunits": "Vergewissere dich ebenfalls, dass die <u>Kraftstoffeinheit</u> und der <u>Kraftstofftyp</u> im Fahrzeug richtig eingestellt sind.",
|
||||
"importhintcurrdist": "Stelle sicher, dass die <u>Währung</u> und die <u>Entfernungseinheit</u> in Hammond korrekt eingestellt sind. Der Import erkennt die Währung nicht automatisch aus der datei, sondern verwendet die für den Benutzer eingestellte Währung.",
|
||||
"importhintnickname": "Vergewissere dich, dass der Fahrzeugname in Hammond genau mit dem Namen in der Fuelly-CSV-Datei übereinstimmt, sonst funktioniert der Import nicht.",
|
||||
"importhintvehiclecreated": "Vergewissere dich, dass du die Fahrzeuge bereits in Hammond erstellt hast.",
|
||||
"importhintcreatecsv": "Exportiere deine Daten aus {name} im CSV-Format. Die Schritte dazu findest du",
|
||||
"here": "hier",
|
||||
"unprocessedquickentries": "Du hast einen Schnelleintrag zum bearbeiten. | Du hast {0} Schnelleinträge zum bearbeiten.",
|
||||
"show": "Anzeigen",
|
||||
"loginerror": "Bei der Anmeldung ist ein Fehler aufgetreten. {msg}",
|
||||
"showunprocessed": "Zeige unbearbeitete",
|
||||
"unprocessed": "unbearbeitet",
|
||||
"sitesettingdesc": "Ändere die globalen Einstellungen. Diese werden als Standard für neue Benutzer verwendet.",
|
||||
"settingdesc": "Diese Einstellungen werden als Standard verwendet wenn du eine neue Ausgabe oder eine Tankfüllung erfasst.",
|
||||
"areyousure": "Bist du dir sicher?",
|
||||
"adduser": "Benutzer hinzufügen",
|
||||
"usercreatedsuccessfully": "Benutzer erfolgreich gespeichert",
|
||||
"role": "Rolle",
|
||||
"created": "Erstellt",
|
||||
"createnewuser": "Erstelle neuen Benutzer",
|
||||
"cancel": "Abbrechen",
|
||||
"novehicles": "Du hast noch kein Fahrzeug erstellt. Lege jetzt einen Eintrag für das zu verwaltende Fahrzeug an.",
|
||||
"processed": "Bearbeitet",
|
||||
"notfound": "Nicht gefunden",
|
||||
"timeout": "Das Laden der Seite hat eine Zeitüberschreitung verursacht. Bist du sicher, dass du noch mit dem Internet verbunden bist?",
|
||||
"clicktoselect": "Klicke, um auszuwählen...",
|
||||
"expenseby": "Ausgabe von",
|
||||
"selectvehicle": "Wähle ein Fahrzeug aus",
|
||||
"expensedate": "Datum der Ausgabe",
|
||||
"totalamountpaid": "Gezahlter Gesamtbetrag",
|
||||
"fillmoredetails": "Weitere Details ausfüllen",
|
||||
"markquickentryprocessed": "Markiere gewählten Schnelleintrag als bearbeitet",
|
||||
"referquickentry": "Wähle Schnelleintrag",
|
||||
"deletequickentry": "Willst du diesen Schnelleintrag wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden!",
|
||||
"fuelunit": "Kraftstoffeinheit",
|
||||
"fillingstation": "Tankstelle",
|
||||
"comments": "Kommentare",
|
||||
"missfillupbefore": "Hast du vergessen, die vorherige Tankfüllung zu erfassen?",
|
||||
"fillupdate": "Tankdatum",
|
||||
"fillupsavedsuccessfully": "Tankfüllung erfolgreich gespeichert",
|
||||
"expensesavedsuccessfully": "Ausgabe erfolgreich gespeichert",
|
||||
"vehiclesavedsuccessfully": "Fahrzeug erfolgreich gespeichert",
|
||||
"back": "Zurück",
|
||||
"nickname": "Bezeichnung",
|
||||
"registration": "Nummernschild",
|
||||
"createvehicle": "Fahrzeug erstellen",
|
||||
"make": "Marke",
|
||||
"model": "Modell",
|
||||
"yearmanufacture": "Jahr der Erstzulassung",
|
||||
"enginesize": "Hubraum (in ccm)",
|
||||
"testconn": "Teste Verbindung",
|
||||
"migrate": "Migrieren",
|
||||
"init": {
|
||||
"migrateclarkson": "Migriere von Clarkson",
|
||||
"migrateclarksondesc": "Wenn du bereits eine Instanz von Clarkson verwendest und die Daten migrieren möchtest, klicke hier.",
|
||||
"freshinstall": "Frische Installation",
|
||||
"freshinstalldesc": "Wenn du eine neue Installation von Hammond starten möchtest, klicke hier.",
|
||||
"clarkson": {
|
||||
"desc": "<p>Zuerst musst du sicherstellen, dass das Deployment von Hammond die von Clarkson verwendete MySQL Datenbank erreichen kann.</p><p>Wenn dies nicht möglich ist kannst du eine Kopie erstellen die für Hammond erreichbar ist.</p><p>Wenn das erledigt ist, füge hier den Connection String im folgenden Format ein.</p><p>Alle aus Clarkson importierten Nutzer bekommen ihren Benutzernamen als E-Mail und das Passwort wird geändert zu <span class='' style='font-weight:bold'>hammond</span></p><code>user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local</code><br/><br/>",
|
||||
"success": "Deine Daten wurden erfolgreich von Clarkson migriert. Du wirst in kürze zur Anmeldung weitergeleitet wo du dich mit deiner E-Mail und dem passwort `hammond` anmelden kannst."
|
||||
},
|
||||
"fresh": {
|
||||
"setupadminuser": "Erstelle einen Administrator",
|
||||
"yourpassword": "Dein Passwort",
|
||||
"youremail": "Deine E-Mail-Adresse",
|
||||
"yourname": "Dein Name",
|
||||
"success": "Du hast dich erfolgreich registriert. Du wirst in kürze zur Anmeldung weitergeleitet und kannst Anfangen Hammond zu verwenden."
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"ADMIN": "Adminstrator",
|
||||
"USER": "Benutzer"
|
||||
},
|
||||
"profile": "Profil",
|
||||
"processedon": "Bearbeitet am",
|
||||
"enable": "Entsperren",
|
||||
"disable": "Sperren",
|
||||
"confirm": "Bestätigen",
|
||||
"labelforfile": "Bezeichnung für diese Datei"
|
||||
}
|
||||
231
ui/src/locales/en.json
Normal file
231
ui/src/locales/en.json
Normal file
@@ -0,0 +1,231 @@
|
||||
{
|
||||
"quickentry": "No Quick Entries | Quick Entry | Quick Entries",
|
||||
"statistics": "Statistics",
|
||||
"thisweek": "This week",
|
||||
"thismonth": "This month",
|
||||
"pastxdays": "Past one day | Past {count} days",
|
||||
"pastxmonths": "Past one month | Past {count} months",
|
||||
"thisyear": "This year",
|
||||
"alltime": "All Time",
|
||||
"noattachments": "No Attachments so far",
|
||||
"attachments": "Attachments",
|
||||
"choosefile": "Choose File",
|
||||
"addattachment": "Add Attachment",
|
||||
"sharedwith": "Shared with",
|
||||
"share": "Share",
|
||||
"you": "You",
|
||||
"addfillup": "Add Fillup",
|
||||
"createfillup": "Create Fillup",
|
||||
"deletefillup": "Delete this fillup",
|
||||
"addexpense": "Add Expense",
|
||||
"createexpense": "Create Expense",
|
||||
"deleteexpense": "Delete this expense",
|
||||
"nofillups": "No Fillups so far",
|
||||
"transfervehicle": "Transfer Vehicle",
|
||||
"settingssaved": "Settings saved successfully",
|
||||
"yoursettings": "Your Settings",
|
||||
"settings": "Settings",
|
||||
"changepassword": "Change password",
|
||||
"oldpassword": "Old password",
|
||||
"newpassword": "New password",
|
||||
"repeatnewpassword": "Repeat New Password",
|
||||
"passworddontmatch": "Password values don't match",
|
||||
"save": "Save",
|
||||
"supportthedeveloper": "Support the developer",
|
||||
"buyhimabeer": "Buy him a beer!",
|
||||
"featurerequest": "Feature Request",
|
||||
"foundabug": "Found a bug",
|
||||
"currentversion": "Current Version",
|
||||
"moreinfo": "More Info",
|
||||
"currency": "Currency",
|
||||
"distanceunit": "Distance Unit",
|
||||
"dateformat": "Date Format",
|
||||
"createnow": "Create Now",
|
||||
"yourvehicles": "Your Vehicles",
|
||||
"menu": {
|
||||
"quickentries": "Quick Entries",
|
||||
"logout": "Log out",
|
||||
"import": "Import",
|
||||
"home": "Home",
|
||||
"settings": "Settings",
|
||||
"admin": "Admin",
|
||||
"sitesettings": "Site Settings",
|
||||
"users": "Users",
|
||||
"login": "Log in"
|
||||
},
|
||||
"enterusername": "Enter your username",
|
||||
"enterpassword": "Enter your password",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"login": "log in",
|
||||
"totalexpenses": "Total Expenses",
|
||||
"fillupcost": "Fillup Costs",
|
||||
"otherexpenses": "Other Expenses",
|
||||
"addvehicle": "Add Vehicle",
|
||||
"editvehicle": "Edit Vehicle",
|
||||
"deletevehicle": "Delete Vehicle",
|
||||
"sharevehicle": "Share vehicle",
|
||||
"makeowner": "Make Owner",
|
||||
"lastfillup": "Last Fillup",
|
||||
"quickentrydesc": "Take a pic of the invoice or the fuel pump display to make an entry later.",
|
||||
"quickentrycreatedsuccessfully": "Quick Entry Created Successfully",
|
||||
"uploadfile": "Upload File",
|
||||
"uploadphoto": "Upload Photo",
|
||||
"details": "Details",
|
||||
"odometer": "Odometer",
|
||||
"language": "Language",
|
||||
"date": "Date",
|
||||
"pastfillups": "Past Fillups",
|
||||
"fuelsubtype": "Fuel Subtype",
|
||||
"fueltype": "Fuel Type",
|
||||
"quantity": "Quantity",
|
||||
"gasstation": "Gas Station",
|
||||
"fuel": {
|
||||
"petrol": "Petrol",
|
||||
"diesel": "Diesel",
|
||||
"cng": "CNG",
|
||||
"lpg": "LPG",
|
||||
"electric": "Electric",
|
||||
"ethanol": "Ethanol"
|
||||
},
|
||||
"unit": {
|
||||
"long": {
|
||||
"litre": "Litre",
|
||||
"gallon": "Gallon",
|
||||
"kilowatthour": "Kilowatt Hour",
|
||||
"kilogram": "Kilogram",
|
||||
"usgallon": "US Gallon",
|
||||
"minutes": "Minutes",
|
||||
"kilometers": "Kilometers",
|
||||
"miles": "Miles"
|
||||
},
|
||||
"short": {
|
||||
"litre": "Lt",
|
||||
"gallon": "Gal",
|
||||
"kilowatthour": "KwH",
|
||||
"kilogram": "Kg",
|
||||
"usgallon": "US Gal",
|
||||
"minutes": "Mins",
|
||||
"kilometers": "Km",
|
||||
"miles": "Mi"
|
||||
}
|
||||
},
|
||||
"avgfillupqty": "Avg Fillup Qty",
|
||||
"avgfillupexpense": "Avg Fillup Expense",
|
||||
"avgfuelcost": "Avg Fuel Cost",
|
||||
"per": "{0} per {1}",
|
||||
"price": "Price",
|
||||
"total": "Total",
|
||||
"fulltank": "Tank Full",
|
||||
"partialfillup": "Partial Fillup",
|
||||
"getafulltank": "Did you get a full tank?",
|
||||
"tankpartialfull": "Which do you track?",
|
||||
"by": "By",
|
||||
"expenses": "Expenses",
|
||||
"expensetype": "Expense Type",
|
||||
"noexpenses": "No Expenses so far",
|
||||
"download": "Download",
|
||||
"title": "Title",
|
||||
"name": "Name",
|
||||
"delete": "Delete",
|
||||
"importdata": "Import data into Hammond",
|
||||
"importdatadesc": "Choose from the following options to import data into Hammond",
|
||||
"import": "Import",
|
||||
"importcsv": "If you have been using {name} to store your vehicle data, export the CSV file from {name} and click here to import.",
|
||||
"importgeneric": "Generic Fillups Import",
|
||||
"importgenericdesc": "Fillups CSV import.",
|
||||
"choosecsv": "Choose CSV",
|
||||
"choosephoto": "Choose Photo",
|
||||
"importsuccessfull": "Data Imported Successfully",
|
||||
"importerror": "There was some issue with importing the file. Please check the error message",
|
||||
"importfrom": "Import from {0}",
|
||||
"stepstoimport": "Steps to import data from {name}",
|
||||
"choosecsvimport": "Choose the {name} CSV and press the import button.",
|
||||
"choosedatafile": "Choose the CSV file and then press the import button.",
|
||||
"dontimportagain": "Make sure that you do not import the file again because that will create repeat entries.",
|
||||
"checkpointsimportcsv": "Once you have checked all these points, just import the CSV below.",
|
||||
"importhintunits": "Similiarly, make sure that the <u>Fuel Unit</u> and <u>Fuel Type</u> are correctly set in the Vehicle.",
|
||||
"importhintcurrdist": "Make sure that the <u>Currency</u> and <u>Distance Unit</u> are set correctly in Hammond. Import will not autodetect Currency from the file but use the one set for the user.",
|
||||
"importhintnickname": "Make sure that the Vehicle nickname in Hammond is exactly the same as the name on Fuelly CSV or the import will not work.",
|
||||
"importhintvehiclecreated": "Make sure that you have already created the vehicles in Hammond platform.",
|
||||
"importhintcreatecsv": "Export your data from {name} in the CSV format. Steps to do that can be found",
|
||||
"importgenerichintdata": "Data must be in CSV format.",
|
||||
"here": "here",
|
||||
"unprocessedquickentries": "You have one quick entry to be processed. | You have {0} quick entries pending to be processed.",
|
||||
"show": "Show",
|
||||
"loginerror": "There was an error logging in to your account. {msg}",
|
||||
"showunprocessed": "Show unprocessed only",
|
||||
"unprocessed": "unprocessed",
|
||||
"sitesettingdesc": "Update site level settings. These will be used as default values for new users.",
|
||||
"settingdesc": "These will be used as default values whenever you create a new fillup or expense.",
|
||||
"areyousure": "Are you sure you want to do this?",
|
||||
"adduser": "Add User",
|
||||
"usercreatedsuccessfully": "User Created Successfully",
|
||||
"userdisabledsuccessfully": "User disabled successfully",
|
||||
"userenabledsuccessfully": "User enabled successfully",
|
||||
"role": "Role",
|
||||
"created": "Created",
|
||||
"createnewuser": "Create New User",
|
||||
"cancel": "Cancel",
|
||||
"novehicles": "It seems you have not yet created a vehicle in the system. Start by creating an entry for one of the vehicles you want to track.",
|
||||
"processed": "Mark Processed",
|
||||
"notfound": "Not Found",
|
||||
"timeout": "The page timed out while loading. Are you sure you're still connected to\nthe Internet?",
|
||||
"clicktoselect": "Click to select...",
|
||||
"expenseby": "Expense by",
|
||||
"selectvehicle": "Select a vehicle",
|
||||
"expensedate": "Expense Date",
|
||||
"totalamountpaid": "Total Amount Paid",
|
||||
"fillmoredetails": "Fill more details",
|
||||
"markquickentryprocessed": "Mark selected Quick Entry as processed",
|
||||
"referquickentry": "Refer quick entry",
|
||||
"deletequickentry": "This will delete this Quick Entry. This step cannot be reversed. Are you sure?",
|
||||
"fuelunit": "Fuel Unit",
|
||||
"fillingstation": "Filling Station Name",
|
||||
"comments": "Comments",
|
||||
"missfillupbefore": "Did you miss the fillup entry before this one?",
|
||||
"missedfillup": "Missed Fillup",
|
||||
"fillupdate": "Fillup Date",
|
||||
"fillupsavedsuccessfully": "Fillup Saved Successfully",
|
||||
"expensesavedsuccessfully": "Expense Saved Successfully",
|
||||
"vehiclesavedsuccessfully": "Vehicle Saved Successfully",
|
||||
"settingssavedsuccessfully": "Settings saved successfully",
|
||||
"back": "Back",
|
||||
"nickname": "Nickname",
|
||||
"registration": "Registration",
|
||||
"createvehicle": "Create Vehicle",
|
||||
"make": "Make / Company",
|
||||
"model": "Model",
|
||||
"yearmanufacture": "Year of Manufacture",
|
||||
"enginesize": "Engine Size (in cc)",
|
||||
"mysqlconnstr": "Mysql Connection String",
|
||||
"testconn": "Test Connection",
|
||||
"migrate": "Migrate",
|
||||
"init": {
|
||||
"migrateclarkson": "Migrate from Clarkson",
|
||||
"migrateclarksondesc": "If you have an existing Clarkson deployment and you want to migrate your data from that, press the following button.",
|
||||
"freshinstall": "Fresh Install",
|
||||
"freshinstalldesc": "If you want a fresh install of Hammond, press the following button.",
|
||||
"clarkson": {
|
||||
"desc": "<p>You need to make sure that this deployment of Hammond can access the MySQL database used by Clarkson.</p><p>If that is not directly possible, you can make a copy of that database somewhere accessible from this instance.</p><p>Once that is done, enter the connection string to the MySQL instance in the following format.</p><p>All the users imported from Clarkson will have their username as their email in Clarkson database and pasword set to<span class='' style='font-weight:bold'>hammond</span></p><code>user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local</code><br/><br/>",
|
||||
"success": "We have successfully migrated the data from Clarkson. You will be redirected to the login screen shortly where you can login using your existing email and password : hammond"
|
||||
},
|
||||
"fresh": {
|
||||
"setupadminuser": "Setup Admin Users",
|
||||
"yourpassword": "Your Password",
|
||||
"youremail": "Your Email",
|
||||
"yourname": "Your Name",
|
||||
"success": "You have been registered successfully. You will be redirected to the login screen shortly where you can login and start using the system."
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"ADMIN": "ADMIN",
|
||||
"USER": "USER"
|
||||
},
|
||||
"profile": "Profile",
|
||||
"processedon": "Processed on",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable",
|
||||
"confirm": "Go Ahead",
|
||||
"labelforfile": "Label for this file"
|
||||
}
|
||||
231
ui/src/locales/fr.json
Normal file
231
ui/src/locales/fr.json
Normal file
@@ -0,0 +1,231 @@
|
||||
{
|
||||
"quickentry": "Pas d'entrée rapide | Entrée rapide | Entrées rapides",
|
||||
"statistics": "Statistiques",
|
||||
"thisweek": "Cette semaine",
|
||||
"thismonth": "Ce mois",
|
||||
"pastxdays": "Dernier jour | Derniers {count} jours",
|
||||
"pastxmonths": "Dernier mois | Derniers {count} mois",
|
||||
"thisyear": "Cette année",
|
||||
"alltime": "Tout le temps",
|
||||
"noattachments": "Pas de piece jointe",
|
||||
"attachments": "Pièces jointes",
|
||||
"choosefile": "Choose File",
|
||||
"addattachment": "Ajouter une pièce jointe",
|
||||
"sharedwith": "Partager avec",
|
||||
"share": "Partager",
|
||||
"you": "Vous",
|
||||
"addfillup": "Ajouter un plein",
|
||||
"createfillup": "Créer un plein",
|
||||
"deletefillup": "Supprimer ce plein",
|
||||
"addexpense": "Ajouter une dépense",
|
||||
"createexpense": "Créer une dépense",
|
||||
"deleteexpense": "Supprimer cette dépense",
|
||||
"nofillups": "Pas de plein",
|
||||
"transfervehicle": "Transferer le Véhicule",
|
||||
"settingssaved": "Paramètres sauvegardés avec succès",
|
||||
"yoursettings": "Vos paramètres",
|
||||
"settings": "Paramètres",
|
||||
"changepassword": "Changer votre mot de passe",
|
||||
"oldpassword": "Ancien mot de passe",
|
||||
"newpassword": "Nouveau mot de passe",
|
||||
"repeatnewpassword": "Répéter votre nouveau mot de passe",
|
||||
"passworddontmatch": "Les mots de passe ne correspondent pas",
|
||||
"save": "Sauvegarder",
|
||||
"supportthedeveloper": "Supporter le développeur",
|
||||
"buyhimabeer": "Acheter lui un café!",
|
||||
"featurerequest": "Demande de fonctionnalité",
|
||||
"foundabug": "Trouvé un bug",
|
||||
"currentversion": "Version actuelle",
|
||||
"moreinfo": "Plus d'informations",
|
||||
"currency": "Monnaie",
|
||||
"distanceunit": "Unité de distance",
|
||||
"dateformat": "Format de data",
|
||||
"createnow": "Créer Maintenant",
|
||||
"yourvehicles": "Vos Véhicules",
|
||||
"menu": {
|
||||
"quickentries": "Entrée rapide",
|
||||
"logout": "Se déconnecter",
|
||||
"import": "Importer",
|
||||
"home": "Accueil",
|
||||
"settings": "Paramètres",
|
||||
"admin": "Admin",
|
||||
"sitesettings": "Paramètres du site",
|
||||
"users": "Utilisateurs",
|
||||
"login": "Connexion"
|
||||
},
|
||||
"enterusername": "Entrez votre nom d'utilisateur",
|
||||
"enterpassword": "Entrez votre mot de passe",
|
||||
"email": "Email",
|
||||
"password": "Mot de passe",
|
||||
"login": "connexion",
|
||||
"totalexpenses": "Dépenses totales",
|
||||
"fillupcost": "Coût des pleins",
|
||||
"otherexpenses": "Autres dépenses",
|
||||
"addvehicle": "Ajouter un Véhicule",
|
||||
"editvehicle": "Editer un Véhicule",
|
||||
"deletevehicle": "Supprimer un Véhicule",
|
||||
"sharevehicle": "Partager un Véhicule",
|
||||
"makeowner": "Changer le propriétaire",
|
||||
"lastfillup": "Dernier plein",
|
||||
"quickentrydesc": "Prendre une photo de la facture ou de l'écran de la pompe à essence pour créer une entrée plus tard.",
|
||||
"quickentrycreatedsuccessfully": "Entrée rapide créée avec succès",
|
||||
"uploadfile": "Téléverser un fichier",
|
||||
"uploadphoto": "Téléverser une photo",
|
||||
"details": "Détails",
|
||||
"odometer": "Odomètre",
|
||||
"language": "Langue",
|
||||
"date": "Date",
|
||||
"pastfillups": "Derniers pleins",
|
||||
"fuelsubtype": "Sous-type de combustible",
|
||||
"fueltype": "Type de combustible",
|
||||
"quantity": "Quantité",
|
||||
"gasstation": "Station service",
|
||||
"fuel": {
|
||||
"petrol": "Pétrol",
|
||||
"diesel": "Diesel",
|
||||
"cng": "CNG",
|
||||
"lpg": "LPG",
|
||||
"electric": "Electrique",
|
||||
"ethanol": "Éthanol"
|
||||
},
|
||||
"unit": {
|
||||
"long": {
|
||||
"litre": "Litre",
|
||||
"gallon": "Gallon",
|
||||
"kilowatthour": "Kilowatt Heure",
|
||||
"kilogram": "Kilogram",
|
||||
"usgallon": "US Gallon",
|
||||
"minutes": "Minutes",
|
||||
"kilometers": "Kilometres",
|
||||
"miles": "Miles"
|
||||
},
|
||||
"short": {
|
||||
"litre": "Lt",
|
||||
"gallon": "Gal",
|
||||
"kilowatthour": "KwH",
|
||||
"kilogram": "Kg",
|
||||
"usgallon": "US Gal",
|
||||
"minutes": "Mins",
|
||||
"kilometers": "Km",
|
||||
"miles": "Mi"
|
||||
}
|
||||
},
|
||||
"avgfillupqty": "Qté de plein moyen",
|
||||
"avgfillupexpense": "Prix du plein moyen",
|
||||
"avgfuelcost": "Prix de l'essence moyen",
|
||||
"per": "{0} par {1}",
|
||||
"price": "Prix",
|
||||
"total": "Total",
|
||||
"fulltank": "Reservoir complet",
|
||||
"partialfillup": "Plein partiel",
|
||||
"getafulltank": "Est-ce que vous avez rempli tout votre reservoir?",
|
||||
"tankpartialfull": "Le quel traquez-vous?",
|
||||
"by": "Par",
|
||||
"expenses": "Dépenses",
|
||||
"expensetype": "Type de dépense",
|
||||
"noexpenses": "Pas de dépense",
|
||||
"download": "Télécharger",
|
||||
"title": "Titre",
|
||||
"name": "Nom",
|
||||
"delete": "Supprimer",
|
||||
"importdata": "Importer des données dans Hammond",
|
||||
"importdatadesc": "Choisissez une option pour importer des données dans Hammond",
|
||||
"import": "Importer",
|
||||
"importcsv": "Si vous utilisiez {name} pour stocker les données de vos véhicules, exportez les données en format CSV depuis {name} et cliquez ici pour importer.",
|
||||
"importgeneric": "Importation de plein générique",
|
||||
"importgenericdesc": "Importation de plein avec un SVC.",
|
||||
"choosecsv": "Choisir un CSV",
|
||||
"choosephoto": "Choisir une Photo",
|
||||
"importsuccessfull": "Données importée avec succès",
|
||||
"importerror": "Il y a eu un problème lors de l'importation. Veuillez regarder le message d'erreur",
|
||||
"importfrom": "Importer depuis {0}",
|
||||
"stepstoimport": "Étapes pour importer des données depuis {name}",
|
||||
"choosecsvimport": "Choisissez le fichier CSV de {name} et appuyez sur le bouton pour importer.",
|
||||
"choosedatafile": "Choisissez le fichier CSV et appuyez sur le bouton pour importer.",
|
||||
"dontimportagain": "Faites attention à ne pas importer le fichier à nouveau car cela va créer des entrées dupliquées.",
|
||||
"checkpointsimportcsv": "Dès que vous avez vérifié tous ces points, importez le CSV ci-dessous.",
|
||||
"importhintunits": "De la même manière, make sure that the <u>Fuel Unit</u> and <u>Fuel Type</u> are correctly set in the Vehicle.",
|
||||
"importhintcurrdist": "Soyez sûre que la <u>Monnaie</u> et l'<u>Unité de distance</u> sont mises correctement dans Hammond. L'importation ne detectera pas automatiquement la Monnaie du fichier mais utilisera les valeurs de l'utilisateur.",
|
||||
"importhintnickname": "Soyez sûre que le nom du véhicule dans Hammon est exactement le même que le nom dans Fuelly, sinon, l'importation ne fonctionnera pas.",
|
||||
"importhintvehiclecreated": "Soyez sûre d'avoir déjà créé le véhicule dans la plate-forme Hammond.",
|
||||
"importhintcreatecsv": "Exportez vos données depuis {name} en format CSV. Les étapes pour faire ceci peuvent être trouvées",
|
||||
"importgenerichintdata": "Les données doivent être au format CSV.",
|
||||
"here": "ici",
|
||||
"unprocessedquickentries": "Vous avez 1 entrée rapide en attente d'être traîtée. | Vous avez {0} entrée rapide en attente d'être traîtée.",
|
||||
"show": "montrer",
|
||||
"loginerror": "Il y a eu une erreur lors de la connexion a votre compte: {msg}",
|
||||
"showunprocessed": "Montrer seulement les non-traîtées",
|
||||
"unprocessed": "non-traîtée",
|
||||
"sitesettingdesc": "Mettre à jour les paramètres du site. Ces valeurs seront utilisées par défaut pour les nouveaux utilisateurs.",
|
||||
"settingdesc": "Ces valeurs seront utilisées par défaut lorsque vous créez un nouveau plein ou une nouvelle dépense.",
|
||||
"areyousure": "Êtes-vous sûre de vouloir faire ceci?",
|
||||
"adduser": "Ajouter un utilisateur",
|
||||
"usercreatedsuccessfully": "Utilisateur créé avec succès",
|
||||
"userdisabledsuccessfully": "Utilisateur désactivé avec succès",
|
||||
"userenabledsuccessfully": "Utilisateur activé avec succès",
|
||||
"role": "Rôle",
|
||||
"created": "Créé",
|
||||
"createnewuser": "Créer un nouvel utilisateur",
|
||||
"cancel": "Annuler",
|
||||
"novehicles": "Il semble que vous n'avez pas encore créé de véhicule dans le système pour le moment. Commencez par créer une entrée pour un des véhicule que vous voulez traquer.",
|
||||
"processed": "Marquer en tant que traîté",
|
||||
"notfound": "Non Trouvé",
|
||||
"timeout": "La page a expiré lors du chargement. Êtes-vous sûre d'être toujours connecté à Internet?",
|
||||
"clicktoselect": "Cliquer pour sélectionner...",
|
||||
"expenseby": "Dépense par",
|
||||
"selectvehicle": "Selectionner un véhicule",
|
||||
"expensedate": "Date de la dépense",
|
||||
"totalamountpaid": "Montant payé total",
|
||||
"fillmoredetails": "Entrer plus de détails",
|
||||
"markquickentryprocessed": "Marquer l'entrée rapide séléctionnée en tant que traîtée",
|
||||
"referquickentry": "Faire référence à une entrée rapide",
|
||||
"deletequickentry": "Ceci va supprimer l'entrée rapide. Cette action ne peut pas être annulée. Êtes-vous sûre?",
|
||||
"fuelunit": "Unité de combustible",
|
||||
"fillingstation": "Nom de la station service",
|
||||
"comments": "Commentaires",
|
||||
"missfillupbefore": "Est-ce que vous avez manqué un plein avant celui-ci?",
|
||||
"missedfillup": "Plein manqué",
|
||||
"fillupdate": "Date du plein",
|
||||
"fillupsavedsuccessfully": "Plein sauvegardé avec succès",
|
||||
"expensesavedsuccessfully": "Dépense sauvegardé avec succès",
|
||||
"vehiclesavedsuccessfully": "Véhicule sauvegardé avec succès",
|
||||
"settingssavedsuccessfully": "Paramètres sauvegardés avec succès",
|
||||
"back": "Retour",
|
||||
"nickname": "Surnom",
|
||||
"registration": "Immatriculation",
|
||||
"createvehicle": "Créer un Véhicule",
|
||||
"make": "Marque",
|
||||
"model": "Modèle",
|
||||
"yearmanufacture": "Année de production",
|
||||
"enginesize": "Taille du moteur (en chevaux)",
|
||||
"mysqlconnstr": "Chaîne de caractère pour la connexion MySQL",
|
||||
"testconn": "Tester la Connexion",
|
||||
"migrate": "Migrer",
|
||||
"init": {
|
||||
"migrateclarkson": "Migrer depuis Clarkson",
|
||||
"migrateclarksondesc": "Si vous avez un déploiement Clarkson existant et que vous souhaitez migrer vos données à partir de celui-ci, appuyez sur le bouton suivant.",
|
||||
"freshinstall": "Nouvelle Installation",
|
||||
"freshinstalldesc": "Si vous voulez une nouvelle installation de Hammond, appuyez sur le bouton suivant.",
|
||||
"clarkson": {
|
||||
"desc": "<p>Vous devez vous assurer que ce déploiement de Hammond peut accéder à la base de données MySQL utilisée par Clarkson.</p><p>Si ce n'est pas directement possible, vous pouvez faire une copie de cette base de données autre part qui est accessible à partir de cette instance.</p><p>Une fois cela fait, entrez la chaîne de connexion à l'instance MySQL au format suivant.</p><p>Tous les utilisateurs importés de Clarkson auront leur nom d'utilisateur comme e-mail dans la base de données Clarkson et le mot de passe défini sur <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": "Nous avons migré avec succès les données depuis Clarkson. Vous serez bientôt redirigé vers l'écran de connexion où vous pourrez vous connecter en utilisant votre adresse e-mail et votre mot de passe existants : hammond"
|
||||
},
|
||||
"fresh": {
|
||||
"setupadminuser": "Configurer le compte administrateur",
|
||||
"yourpassword": "Votre Mot de passe",
|
||||
"youremail": "Votre Email",
|
||||
"yourname": "Votre Nom",
|
||||
"success": "Vous avez été inscrit avec succès. Vous allez être redirigé vers la page de connexion très bientôt, vous pourrez vous connecter et commencer à utiliser le système."
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"ADMIN": "ADMIN",
|
||||
"USER": "USER"
|
||||
},
|
||||
"profile": "Profile",
|
||||
"processedon": "Traîté le",
|
||||
"enable": "Activer",
|
||||
"disable": "Désactiver",
|
||||
"confirm": "Continuer",
|
||||
"labelforfile": "Label pour ce fichier"
|
||||
}
|
||||
231
ui/src/locales/sl.json
Normal file
231
ui/src/locales/sl.json
Normal file
@@ -0,0 +1,231 @@
|
||||
{
|
||||
"quickentry": "Ni hitrih vnosov | Hiter vnos | Hitri vnosi",
|
||||
"statistics": "Statistika",
|
||||
"thisweek": "Ta teden",
|
||||
"thismonth": "Ta mesec",
|
||||
"pastxdays": "Zadnji dan | Zadnjih {count} dni",
|
||||
"pastxmonths": "Zadnji mesec | Zadnji {count} mesecev",
|
||||
"thisyear": "To leto",
|
||||
"alltime": "Celoten čas",
|
||||
"noattachments": "Zaenkrat še ni priponk",
|
||||
"attachments": "Priloge",
|
||||
"choosefile": "Izberi datoteko",
|
||||
"addattachment": "Dodaj prilogo",
|
||||
"sharedwith": "V skupni rabi z",
|
||||
"share": "Deli",
|
||||
"you": "Ti",
|
||||
"addfillup": "Dodaj polnjenje",
|
||||
"createfillup": "Ustvarite polnjenje",
|
||||
"deletefillup": "Izbriši to polnjenje",
|
||||
"addexpense": "Dodaj strošek",
|
||||
"createexpense": "Ustvari strošek",
|
||||
"deleteexpense": "Izbriši ta strošek",
|
||||
"nofillups": "Zaenkrat še brez polnjenja",
|
||||
"transfervehicle": "Prevozno sredstvo",
|
||||
"settingssaved": "Nastavitve so uspešno shranjene",
|
||||
"yoursettings": "Vaše nastavitve",
|
||||
"settings": "Nastavitve",
|
||||
"changepassword": "Spremeni geslo",
|
||||
"oldpassword": "Staro geslo",
|
||||
"newpassword": "Novo geslo",
|
||||
"repeatnewpassword": "Ponovite novo geslo",
|
||||
"passworddontmatch": "Vrednosti gesel se ne ujemajo",
|
||||
"save": "Shrani",
|
||||
"supportthedeveloper": "Podprite razvijalca",
|
||||
"buyhimabeer": "Kupi mu pivo!",
|
||||
"featurerequest": "Nova funkcionalnost",
|
||||
"foundabug": "Našel sem hrošča",
|
||||
"currentversion": "Trenutna verzija",
|
||||
"moreinfo": "Več informacij",
|
||||
"currency": "Valuta",
|
||||
"distanceunit": "Enota razdalje",
|
||||
"dateformat": "Format datuma",
|
||||
"createnow": "Ustvari zdaj",
|
||||
"yourvehicles": "Vaša vozila",
|
||||
"menu": {
|
||||
"quickentries": "Hitri vnosi",
|
||||
"logout": "Odjava",
|
||||
"import": "Uvoz",
|
||||
"home": "Domov",
|
||||
"settings": "Nastavitve",
|
||||
"admin": "Skrbnik",
|
||||
"sitesettings": "Nastavitve spletnega mesta",
|
||||
"users": "Uporabniki",
|
||||
"login": "Vpiši se"
|
||||
},
|
||||
"enterusername": "Vnesite svoje uporabniško ime",
|
||||
"enterpassword": "Vnesite vaše geslo",
|
||||
"email": "E-naslov",
|
||||
"password": "Geslo",
|
||||
"login": "Vpiši se",
|
||||
"totalexpenses": "Skupni stroški",
|
||||
"fillupcost": "Stroški polnjenja",
|
||||
"otherexpenses": "Drugi stroški",
|
||||
"addvehicle": "Dodaj vozilo",
|
||||
"editvehicle": "Uredi vozilo",
|
||||
"deletevehicle": "Izbriši vozilo",
|
||||
"sharevehicle": "Deli vozilo",
|
||||
"makeowner": "Postani lastnik",
|
||||
"lastfillup": "Zadnje polnjenje",
|
||||
"quickentrydesc": "Posnemite sliko računa ali zaslona črpalke za gorivo, da ju lahko vnesete pozneje.",
|
||||
"quickentrycreatedsuccessfully": "Hitri vnos je bil uspešno ustvarjen",
|
||||
"uploadfile": "Naloži datoteko",
|
||||
"uploadphoto": "Naloži fotografijo",
|
||||
"details": "Podrobnosti",
|
||||
"odometer": "Odometer",
|
||||
"language": "Jezik",
|
||||
"date": "Datum",
|
||||
"pastfillups": "Prejšnja polnjenja",
|
||||
"fuelsubtype": "Podvrsta goriva",
|
||||
"fueltype": "Vrsta goriva",
|
||||
"quantity": "Količina",
|
||||
"gasstation": "Bencinska črpalka",
|
||||
"fuel": {
|
||||
"petrol": "Bencin",
|
||||
"diesel": "Dizelsko gorivo",
|
||||
"cng": "CNG",
|
||||
"lpg": "LPG",
|
||||
"electric": "Elektrika",
|
||||
"ethanol": "Etanol"
|
||||
},
|
||||
"unit": {
|
||||
"long": {
|
||||
"litre": "Liter",
|
||||
"gallon": "Galon",
|
||||
"kilowatthour": "Kilovatna ura",
|
||||
"kilogram": "Kilogram",
|
||||
"usgallon": "Ameriška galona",
|
||||
"minutes": "Minute",
|
||||
"kilometers": "Kilometri",
|
||||
"miles": "Milje"
|
||||
},
|
||||
"short": {
|
||||
"litre": "Lit",
|
||||
"gallon": "Gal",
|
||||
"kilowatthour": "KwH",
|
||||
"kilogram": "Kg",
|
||||
"usgallon": "US Gal",
|
||||
"minutes": "Min",
|
||||
"kilometers": "Km",
|
||||
"miles": "Mi"
|
||||
}
|
||||
},
|
||||
"avgfillupqty": "Povprečna količina polnjenja",
|
||||
"avgfillupexpense": "Povprečni stroški polnjenja",
|
||||
"avgfuelcost": "Povprečni strošek goriva",
|
||||
"per": "{0} na {1}",
|
||||
"price": "Cena",
|
||||
"total": "Skupaj",
|
||||
"fulltank": "Rezervoar poln",
|
||||
"partialfillup": "Delno polnjenje",
|
||||
"getafulltank": "Ste dobili poln rezervoar?",
|
||||
"tankpartialfull": "Kateremu sledite?",
|
||||
"by": "Od",
|
||||
"expenses": "Stroški",
|
||||
"expensetype": "Vrsta stroška",
|
||||
"noexpenses": "Zaenkrat ni bilo stroškov",
|
||||
"download": "Prenesi",
|
||||
"title": "Naslov",
|
||||
"name": "Ime",
|
||||
"delete": "Izbriši",
|
||||
"importdata": "Uvozite podatke v Hammond",
|
||||
"importdatadesc": "Za uvoz podatkov v Hammond izberite med naslednjimi možnostmi",
|
||||
"import": "Uvozi",
|
||||
"importcsv": "Če ste za shranjevanje podatkov o vozilu uporabljali {name}, izvozite datoteko CSV iz {name} in kliknite tukaj za uvoz.",
|
||||
"importgeneric": "Generični uvoz polnjenja",
|
||||
"importgenericdesc": "CSV uvoz poljenja.",
|
||||
"choosecsv": "Izberite CSV",
|
||||
"choosephoto": "Izberite fotografijo",
|
||||
"importsuccessfull": "Podatki so bili uspešno uvoženi",
|
||||
"importerror": "Pri uvozu datoteke je prišlo do težave. ",
|
||||
"importfrom": "Uvoz iz {0}",
|
||||
"stepstoimport": "Koraki za uvoz podatkov iz {name}",
|
||||
"choosecsvimport": "Izberite {name} CSV in pritisnite gumb za uvoz.",
|
||||
"choosedatafile": "Izberite datoteko CSV in pritisnite gumb za uvoz.",
|
||||
"dontimportagain": "Prepričajte se, da datoteke ne uvozite znova, ker boste s tem ustvarili ponavljajoče se vnose.",
|
||||
"checkpointsimportcsv": "Ko preverite vse te točke, preprosto uvozite spodnji CSV.",
|
||||
"importhintunits": "Podobno se prepričajte, da sta <u>Enota za gorivo</u> in <u>Vrsta goriva</u> pravilno nameščeni pod Vozilo.",
|
||||
"importhintcurrdist": "Prepričajte se, da sta <u>Valuta</u> in <u>Enota razdalje</u> pri Hammondu uporabniku pravilno nastavljeni.",
|
||||
"importhintnickname": "Prepričajte se, da je vzdevek vozila v Hammondu popolnoma enak imenu v CSV datoteki polnjenj, sicer uvoz ne bo deloval.",
|
||||
"importhintvehiclecreated": "Prepričajte se, da ste že ustvarili vozila na platformi Hammond.",
|
||||
"importhintcreatecsv": "Izvozite svoje podatke iz {name} v formatu CSV. ",
|
||||
"importgenerichintdata": "Podatki morajo biti v formatu CSV.",
|
||||
"here": "tukaj",
|
||||
"unprocessedquickentries": "Za obdelavo imate en hiter vnos. | Za obdelavo imate {0} hitrih vnosov.",
|
||||
"show": "Prikaži",
|
||||
"loginerror": "Pri prijavi v vaš račun je prišlo do napake. ",
|
||||
"showunprocessed": "Pokaži samo neobdelano",
|
||||
"unprocessed": "neobdelano",
|
||||
"sitesettingdesc": "Posodobite nastavitve na ravni aplikacije. Te bodo uporabljene kot privzete vrednosti za nove uporabnike.",
|
||||
"settingdesc": "Te bodo uporabljene kot privzete vrednosti vsakič, ko ustvarite novo polnjenje ali strošek.",
|
||||
"areyousure": "Ste prepričani, da želite to narediti?",
|
||||
"adduser": "Dodaj uporabnika",
|
||||
"usercreatedsuccessfully": "Uporabnik je bil uspešno ustvarjen",
|
||||
"userdisabledsuccessfully": "Uporabnik uspešno onemogočen",
|
||||
"userenabledsuccessfully": "Uporabnik je uspešno omogočen",
|
||||
"role": "Vloga",
|
||||
"created": "Ustvarjeno",
|
||||
"createnewuser": "Ustvari novega uporabnika",
|
||||
"cancel": "Prekliči",
|
||||
"novehicles": "Videti je, da še niste ustvarili vozila v sistemu. ",
|
||||
"processed": "Označi obdelano",
|
||||
"notfound": "Ni najdeno",
|
||||
"timeout": "Med nalaganjem strani je potekla časovna omejitev. ",
|
||||
"clicktoselect": "Kliknite za izbiro ...",
|
||||
"expenseby": "Stroški po",
|
||||
"selectvehicle": "Izberite vozilo",
|
||||
"expensedate": "Datum izdatka",
|
||||
"totalamountpaid": "Skupni plačani znesek",
|
||||
"fillmoredetails": "Izpolnite več podrobnosti",
|
||||
"markquickentryprocessed": "Označi izbrani hitri vnos kot obdelan",
|
||||
"referquickentry": "Oglejte si hiter vnos",
|
||||
"deletequickentry": "S tem boste izbrisali ta hitri vnos. ",
|
||||
"fuelunit": "Enota za gorivo",
|
||||
"fillingstation": "Ime bencinske postaje",
|
||||
"comments": "Komentarji",
|
||||
"missfillupbefore": "Ste pred tem zamudili vnos zapolnitve?",
|
||||
"missedfillup": "Zamujeno polnjenje",
|
||||
"fillupdate": "Datum polnjenja",
|
||||
"fillupsavedsuccessfully": "Polnjenje je bil uspešno shranjeno",
|
||||
"expensesavedsuccessfully": "Stroški so uspešno shranjeni",
|
||||
"vehiclesavedsuccessfully": "Vozilo je uspešno shranjeno",
|
||||
"settingssavedsuccessfully": "Nastavitve so uspešno shranjene",
|
||||
"back": "Nazaj",
|
||||
"nickname": "Vzdevek",
|
||||
"registration": "Registracija",
|
||||
"createvehicle": "Ustvarite vozilo",
|
||||
"make": "Znamka / Podjetje",
|
||||
"model": "Model",
|
||||
"yearmanufacture": "Leto izdelave",
|
||||
"enginesize": "Prostornina motorja (v cc)",
|
||||
"mysqlconnstr": "Niz povezave Mysql",
|
||||
"testconn": "Testna povezava",
|
||||
"migrate": "Preseli",
|
||||
"init": {
|
||||
"migrateclarkson": "Selitev iz Clarksona",
|
||||
"migrateclarksondesc": "Če imate obstoječo Clarkson namestitev in želite iz nje preseliti svoje podatke, pritisnite naslednji gumb.",
|
||||
"freshinstall": "Sveža namestitev",
|
||||
"freshinstalldesc": "Če želite novo namestitev Hammonda, pritisnite naslednji gumb.",
|
||||
"clarkson": {
|
||||
"desc": "<p>Zagotoviti morate, da lahko ta uvedba Hammonda dostopa do baze podatkov MySQL, ki jo uporablja Clarkson.</p><p>Če to ni neposredno mogoče, lahko naredite kopijo te zbirke podatkov nekje, kjer je dostopna iz tega primerka.</p><p>Ko je to storjeno, vnesite povezovalni niz v primerek MySQL v naslednji obliki.</p><p>Vsi uporabniki, uvoženi iz Clarksona, bodo imeli svoje uporabniško ime kot e-pošto v bazi podatkov Clarkson in geslo nastavljeno na<span class='' style='font-weight:bold'>hammond</span></p><code>uporabnik:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4</code><br/><br/>",
|
||||
"success": "Uspešno smo prenesli podatke iz Clarksona. "
|
||||
},
|
||||
"fresh": {
|
||||
"setupadminuser": "Nastavitev skrbniških uporabnikov",
|
||||
"yourpassword": "Vaše geslo",
|
||||
"youremail": "Vaš e-poštni naslov",
|
||||
"yourname": "Vaše ime",
|
||||
"success": "Uspešno ste se registrirali. "
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"ADMIN": "SKRBNIK",
|
||||
"USER": "UPORABNIK"
|
||||
},
|
||||
"profile": "Profil",
|
||||
"processedon": "Obdelano dne",
|
||||
"enable": "Omogoči",
|
||||
"disable": "Onemogoči",
|
||||
"confirm": "Kar daj",
|
||||
"labelforfile": "Oznaka za to datoteko"
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
faCheck,
|
||||
faTimes,
|
||||
faArrowUp,
|
||||
faArrowRotateLeft,
|
||||
faAngleLeft,
|
||||
faAngleRight,
|
||||
faCalendar,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
faTimesCircle,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import i18n from './i18n';
|
||||
|
||||
import App from './app.vue'
|
||||
|
||||
@@ -33,11 +35,11 @@ import '@components/_globals'
|
||||
import 'buefy/dist/buefy.css'
|
||||
import 'nprogress/nprogress.css'
|
||||
|
||||
Vue.component('vue-fontawesome', FontAwesomeIcon)
|
||||
library.add(
|
||||
faCheck,
|
||||
faTimes,
|
||||
faArrowUp,
|
||||
faArrowRotateLeft,
|
||||
faAngleLeft,
|
||||
faAngleRight,
|
||||
faCalendar,
|
||||
@@ -53,7 +55,9 @@ library.add(
|
||||
faShare,
|
||||
faUserFriends,
|
||||
faTimesCircle
|
||||
)
|
||||
);
|
||||
Vue.component('VueFontawesome', FontAwesomeIcon)
|
||||
|
||||
Vue.use(Buefy, {
|
||||
defaultIconComponent: 'vue-fontawesome',
|
||||
defaultIconPack: 'fas',
|
||||
@@ -73,6 +77,7 @@ const app = new Vue({
|
||||
store,
|
||||
|
||||
render: (h) => h(App),
|
||||
i18n,
|
||||
}).$mount('#app')
|
||||
|
||||
// If running e2e tests...
|
||||
|
||||
@@ -410,6 +410,24 @@ export default [
|
||||
},
|
||||
props: (route) => ({ user: store.state.auth.currentUser || {} }),
|
||||
},
|
||||
{
|
||||
path: '/import/drivvo',
|
||||
name: 'import-drivvo',
|
||||
component: () => lazyLoadView(import('@views/import-drivvo.vue')),
|
||||
meta: {
|
||||
authRequired: true,
|
||||
},
|
||||
props: (route) => ({ user: store.state.auth.currentUser || {} }),
|
||||
},
|
||||
{
|
||||
path: '/import/generic',
|
||||
name: 'import-generic',
|
||||
component: () => lazyLoadView(import('@views/import-generic.vue')),
|
||||
meta: {
|
||||
authRequired: true,
|
||||
},
|
||||
props: (route) => ({ user: store.state.auth.currentUser || {} }),
|
||||
},
|
||||
{
|
||||
path: '/logout',
|
||||
name: 'logout',
|
||||
|
||||
@@ -23,7 +23,7 @@ export default {
|
||||
<template v-if="resource">
|
||||
{{ resource }}
|
||||
</template>
|
||||
Not Found
|
||||
{{ $t('notfound') }}
|
||||
</h1>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
@@ -32,8 +32,7 @@ export default {
|
||||
<template>
|
||||
<Layout v-if="offlineConfirmed">
|
||||
<h1 :class="$style.title">
|
||||
The page timed out while loading. Are you sure you're still connected to
|
||||
the Internet?
|
||||
{{ $t('timeout') }}
|
||||
</h1>
|
||||
</Layout>
|
||||
<LoadingView v-else />
|
||||
|
||||
@@ -95,7 +95,7 @@ export default {
|
||||
.put(`/api/vehicles/${this.selectedVehicle.id}/expenses/${this.expense.id}`, this.expenseModel)
|
||||
.then((data) => {
|
||||
this.$buefy.toast.open({
|
||||
message: 'Expense Updated Successfully',
|
||||
message: this.$t('expensesavedsuccessfully'),
|
||||
type: 'is-success',
|
||||
duration: 3000,
|
||||
})
|
||||
@@ -120,7 +120,7 @@ export default {
|
||||
.post(`/api/vehicles/${this.selectedVehicle.id}/expenses`, this.expenseModel)
|
||||
.then((data) => {
|
||||
this.$buefy.toast.open({
|
||||
message: 'Expense Created Successfully',
|
||||
message: this.$t('expensesavedsuccessfully'),
|
||||
type: 'is-success',
|
||||
duration: 3000,
|
||||
})
|
||||
@@ -152,7 +152,7 @@ export default {
|
||||
<Layout>
|
||||
<div class="columns">
|
||||
<div class="column is-two-thirds">
|
||||
<h1 class="title">Create Expense</h1>
|
||||
<h1 class="title">{{ $t('createexpense') }}</h1>
|
||||
<h1 class="subtitle">
|
||||
{{ [selectedVehicle.nickname, selectedVehicle.registration, selectedVehicle.make, selectedVehicle.model].join(' | ') }}
|
||||
</h1>
|
||||
@@ -162,61 +162,61 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
<form @submit.prevent="createExpense">
|
||||
<b-field label="Select a vehicle">
|
||||
<b-select v-model="selectedVehicle" placeholder="Vehicle" required expanded :disabled="expense.id">
|
||||
<b-field :label="$t('selectvehicle')">
|
||||
<b-select v-model="selectedVehicle" :placeholder="$t('vehicle')" required expanded :disabled="expense.id">
|
||||
<option v-for="option in myVehicles" :key="option.id" :value="option">
|
||||
{{ option.nickname }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field label="Expense by">
|
||||
<b-select v-model="expenseModel.userId" placeholder="User" required expanded :disabled="expense.id">
|
||||
<b-field :label="$t('expenseby')">
|
||||
<b-select v-model="expenseModel.userId" :placeholder="$t('user')" required expanded :disabled="expense.id">
|
||||
<option v-for="option in users" :key="option.userId" :value="option.userId">
|
||||
{{ option.name }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field label="Expense Date">
|
||||
<b-field :label="$t('expensedate')">
|
||||
<b-datepicker
|
||||
v-model="expenseModel.date"
|
||||
:date-formatter="formatDate"
|
||||
placeholder="Click to select..."
|
||||
:placeholder="$t('clicktoselect')"
|
||||
icon="calendar"
|
||||
:max-date="new Date()"
|
||||
>
|
||||
</b-datepicker>
|
||||
</b-field>
|
||||
<b-field label="Expense Type*">
|
||||
<b-field :label="$t('expensetype') + `*`">
|
||||
<b-input v-model="expenseModel.expenseType" expanded required></b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Total Amount Paid">
|
||||
<b-field :label="$t('totalamountpaid')">
|
||||
<p class="control">
|
||||
<span class="button is-static">{{ me.currency }}</span>
|
||||
</p>
|
||||
<b-input v-model.number="expenseModel.amount" type="number" min="0" expanded step=".001" required></b-input>
|
||||
</b-field>
|
||||
<b-field label="Odometer Reading">
|
||||
<b-field :label="$t('odometer')">
|
||||
<p class="control">
|
||||
<span class="button is-static">{{ me.distanceUnitDetail.short }}</span>
|
||||
<span class="button is-static">{{ $t('unit.short.' + me.distanceUnitDetail.key) }}</span>
|
||||
</p>
|
||||
<b-input v-model.number="expenseModel.odoReading" type="number" min="0" expanded required></b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field>
|
||||
<b-switch v-model="showMore">Fill more details</b-switch>
|
||||
<b-switch v-model="showMore">{{ $t('fillmoredetails') }}</b-switch>
|
||||
</b-field>
|
||||
<fieldset v-if="showMore">
|
||||
<b-field label="Comments">
|
||||
<b-field :label="$t('details')">
|
||||
<b-input v-model="expenseModel.comments" type="textarea" expanded></b-input>
|
||||
</b-field>
|
||||
</fieldset>
|
||||
<b-field>
|
||||
<b-switch v-if="quickEntry" v-model="processQuickEntry">Mark selected Quick Entry as processed</b-switch>
|
||||
<b-switch v-if="quickEntry" v-model="processQuickEntry">{{ $t('markquickentryprocessed') }}</b-switch>
|
||||
</b-field>
|
||||
<br />
|
||||
<b-field>
|
||||
<b-button tag="input" native-type="submit" :disabled="tryingToCreate" type="is-primary" label="Create Expense" expanded> </b-button>
|
||||
<b-button tag="button" native-type="submit" :value="$t('save')" :disabled="tryingToCreate" type="is-primary" label="Create Expense" expanded/>
|
||||
</b-field>
|
||||
</form>
|
||||
</Layout>
|
||||
|
||||
@@ -76,6 +76,9 @@ export default {
|
||||
this.fetchVehicleFuelSubTypes()
|
||||
if (!this.fillup.id) {
|
||||
this.fillupModel = this.getEmptyFillup()
|
||||
if (this.vehicle.fillups.length > 0) {
|
||||
this.fillupModel.odoReading = this.vehicle.fillups[0].odoReading
|
||||
}
|
||||
this.fillupModel.userId = this.me.id
|
||||
}
|
||||
},
|
||||
@@ -126,7 +129,7 @@ export default {
|
||||
.put(`/api/vehicles/${this.selectedVehicle.id}/fillups/${this.fillup.id}`, this.fillupModel)
|
||||
.then((data) => {
|
||||
this.$buefy.toast.open({
|
||||
message: 'Fillup Updated Successfully',
|
||||
message: this.$t('fillupsavedsuccessfully'),
|
||||
type: 'is-success',
|
||||
duration: 3000,
|
||||
})
|
||||
@@ -153,7 +156,7 @@ export default {
|
||||
.post(`/api/vehicles/${this.selectedVehicle.id}/fillups`, this.fillupModel)
|
||||
.then((data) => {
|
||||
this.$buefy.toast.open({
|
||||
message: 'Fillup Created Successfully',
|
||||
message: this.$t('fillupsavedsuccessfully'),
|
||||
type: 'is-success',
|
||||
duration: 3000,
|
||||
})
|
||||
@@ -181,46 +184,44 @@ export default {
|
||||
|
||||
<template>
|
||||
<Layout>
|
||||
<div class="has-text-centered">
|
||||
<div class="columns">
|
||||
<div class="column is-two-thirds">
|
||||
<h1 class="title">Create Fillup</h1>
|
||||
<h1 class="subtitle">
|
||||
{{ [selectedVehicle.nickname, selectedVehicle.registration, selectedVehicle.make, selectedVehicle.model].join(' | ') }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="column is-one-thirds">
|
||||
<QuickEntryDisplay v-model="quickEntry" :user="user" />
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column is-two-thirds">
|
||||
<h1 class="title">{{ $t('createfillup') }}</h1>
|
||||
<h1 class="subtitle">
|
||||
{{ [selectedVehicle.nickname, selectedVehicle.registration, selectedVehicle.make, selectedVehicle.model].join(' | ') }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="column is-one-thirds">
|
||||
<QuickEntryDisplay v-model="quickEntry" :user="user" />
|
||||
</div>
|
||||
</div>
|
||||
<form class="" @submit.prevent="createFillup">
|
||||
<b-field label="Select a vehicle">
|
||||
<b-select v-model="selectedVehicle" placeholder="Vehicle" required expanded :disabled="fillup.id">
|
||||
<b-field :label="$t('selectvehicle')">
|
||||
<b-select v-model="selectedVehicle" :placeholder="$t('vehicle')" required expanded :disabled="fillup.id">
|
||||
<option v-for="option in myVehicles" :key="option.id" :value="option">
|
||||
{{ option.nickname }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field label="Expense by">
|
||||
<b-select v-model="fillupModel.userId" placeholder="User" required expanded :disabled="fillup.id">
|
||||
<b-field :label="$t('expenseby')">
|
||||
<b-select v-model="fillupModel.userId" :placeholder="$t('user')" required expanded :disabled="fillup.id">
|
||||
<option v-for="option in users" :key="option.userId" :value="option.userId">
|
||||
{{ option.name }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field label="Fillup Date">
|
||||
<b-field :label="$t('fillupdate')">
|
||||
<b-datepicker
|
||||
v-model="fillupModel.date"
|
||||
:date-formatter="formatDate"
|
||||
placeholder="Click to select..."
|
||||
placeholder="this.$t('clicktoselect')"
|
||||
icon="calendar"
|
||||
trap-focus
|
||||
:max-date="new Date()"
|
||||
>
|
||||
</b-datepicker>
|
||||
</b-field>
|
||||
<b-field label="Fuel Subtype">
|
||||
<b-field :label="$t('fuelsubtype')">
|
||||
<b-autocomplete
|
||||
v-model="fillupModel.fuelSubType"
|
||||
:data="filteredFuelSubtypes"
|
||||
@@ -231,55 +232,63 @@ export default {
|
||||
>
|
||||
</b-autocomplete>
|
||||
</b-field>
|
||||
<b-field label="Quantity*" addons>
|
||||
<b-field :label="$t('quantity') + `*`" addons>
|
||||
<b-input v-model.number="fillupModel.fuelQuantity" type="number" step=".001" min="0" expanded required></b-input>
|
||||
<b-select v-model="fillupModel.fuelUnit" placeholder="Fuel Unit" required>
|
||||
<b-select v-model="fillupModel.fuelUnit" :placeholder="$t('fuelunit')" required>
|
||||
<option v-for="(option, key) in fuelUnitMasters" :key="key" :value="key">
|
||||
{{ option.long }}
|
||||
{{ $t('unit.long.' + option.key) }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="'Price per ' + vehicle.fuelUnitDetail.short + '*'"
|
||||
<b-field :label="$t('per', { '0': $t('price'), '1': $t('unit.short.' + vehicle.fuelUnitDetail.key) })"
|
||||
><p class="control">
|
||||
<span class="button is-static">{{ me.currency }}</span>
|
||||
</p>
|
||||
<b-input v-model.number="fillupModel.perUnitPrice" type="number" min="0" step=".001" expanded required></b-input>
|
||||
</b-field>
|
||||
<b-field label="Total Amount Paid">
|
||||
<b-field :label="$t('totalamountpaid')">
|
||||
<p class="control">
|
||||
<span class="button is-static">{{ me.currency }}</span>
|
||||
</p>
|
||||
<b-input v-model.number="fillupModel.totalAmount" type="number" min="0" step=".001" expanded required></b-input>
|
||||
</b-field>
|
||||
<b-field label="Odometer Reading">
|
||||
<b-field :label="$t('odometer')">
|
||||
<p class="control">
|
||||
<span class="button is-static">{{ me.distanceUnitDetail.short }}</span>
|
||||
<span class="button is-static">{{ $t('unit.short.' + me.distanceUnitDetail.key) }}</span>
|
||||
</p>
|
||||
<b-input v-model.number="fillupModel.odoReading" type="number" min="0" expanded required></b-input>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-checkbox v-model="fillupModel.isTankFull">Did you get a full tank?</b-checkbox>
|
||||
<b-checkbox v-model="fillupModel.isTankFull">{{ $t('getafulltank') }}</b-checkbox>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-checkbox v-model="fillupModel.hasMissedFillup">Did you miss the fillup entry before this one?</b-checkbox>
|
||||
<b-checkbox v-model="fillupModel.hasMissedFillup">{{ $t('missfillupbefore') }}</b-checkbox>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-switch v-model="showMore">Fill more details</b-switch>
|
||||
<b-switch v-model="showMore">{{ $t('fillmoredetails') }}</b-switch>
|
||||
</b-field>
|
||||
<fieldset v-if="showMore">
|
||||
<b-field label="Filling Station Name">
|
||||
<b-field :label="$t('fillingstation')">
|
||||
<b-input v-model="fillupModel.fillingStation" type="text" expanded></b-input>
|
||||
</b-field>
|
||||
<b-field label="Comments">
|
||||
<b-field :label="$t('comments')">
|
||||
<b-input v-model="fillupModel.comments" type="textarea" expanded></b-input>
|
||||
</b-field>
|
||||
</fieldset>
|
||||
<b-field>
|
||||
<b-switch v-if="quickEntry" v-model="processQuickEntry">Mark selected Quick Entry as processed</b-switch>
|
||||
<b-switch v-if="quickEntry" v-model="processQuickEntry">{{ $t('markquickentryprocessed') }}</b-switch>
|
||||
</b-field>
|
||||
<br />
|
||||
<b-field>
|
||||
<b-button tag="input" native-type="submit" :disabled="tryingToCreate" type="is-primary" label="Create Fillup" expanded> </b-button>
|
||||
<b-button
|
||||
tag="button"
|
||||
native-type="submit"
|
||||
:disabled="tryingToCreate"
|
||||
type="is-primary"
|
||||
:value="$t('save')"
|
||||
:label="$t('createfillup')"
|
||||
expanded
|
||||
/>
|
||||
<p v-if="authError">
|
||||
There was an error logging in to your account.
|
||||
</p>
|
||||
|
||||
@@ -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,68 @@ export default {
|
||||
<Layout>
|
||||
<div class="columns">
|
||||
<div class="column is-three-quarters">
|
||||
<h1 class="title">Create Vehicle</h1>
|
||||
<h1 class="title">{{ $t('createvehicle') }}</h1>
|
||||
</div>
|
||||
<div class="column is-one-quarter">
|
||||
<router-link tag="b-button" type="is-primary" to="/">
|
||||
Back to Vehicle
|
||||
{{ $t('back') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<form @submit.prevent="createVehicle">
|
||||
<b-field label="Nickname*">
|
||||
<b-field :label="$t('nickname') + `*`">
|
||||
<b-input v-model="vehicleModel.nickname" type="text" expanded required></b-input>
|
||||
</b-field>
|
||||
<b-field label="Registration*">
|
||||
<b-field :label="$t('registration') + `*`">
|
||||
<b-input v-model="vehicleModel.registration" type="text" expanded required></b-input>
|
||||
</b-field>
|
||||
<b-field label="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="$t('fueltype') + `*`">
|
||||
<b-select v-model.number="vehicleModel.fuelType" :placeholder="$t('fueltype')" required expanded>
|
||||
<option v-for="(option, key) in fuelTypeMasters" :key="key" :value="key">
|
||||
{{ option.long }}
|
||||
{{ $t('fuel.' + option.key) }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Fuel Unit*">
|
||||
<b-select v-model.number="vehicleModel.fuelUnit" placeholder="Fuel Unit" required expanded>
|
||||
<b-field :label="$t('fuelunit') + `*`">
|
||||
<b-select v-model.number="vehicleModel.fuelUnit" :placeholder="$t('fuelunit')" required expanded>
|
||||
<option v-for="(option, key) in fuelUnitMasters" :key="key" :value="key">
|
||||
{{ option.long }}
|
||||
{{ $t('unit.long.' + option.key) }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Make / Company*">
|
||||
<b-field :label="$t('make') + `*`">
|
||||
<b-input v-model="vehicleModel.make" type="text" required expanded></b-input>
|
||||
</b-field>
|
||||
<b-field label="Model*">
|
||||
<b-field :label="$t('model') + `*`">
|
||||
<b-input v-model="vehicleModel.model" type="text" required expanded></b-input>
|
||||
</b-field>
|
||||
<b-field label="Year Of Manufacture">
|
||||
<b-field :label="$t('yearmanufacture') + `*`">
|
||||
<b-input v-model.number="vehicleModel.yearOfManufacture" type="number" expanded number></b-input>
|
||||
</b-field>
|
||||
<b-field label="Engine Size (in cc)">
|
||||
<b-field :label="$t('enginesize')">
|
||||
<b-input v-model.number="vehicleModel.engineSize" type="number" expanded number></b-input>
|
||||
</b-field>
|
||||
|
||||
<br />
|
||||
<b-field>
|
||||
<b-button tag="input" native-type="submit" :disabled="tryingToCreate" type="is-primary" label="Create Vehicle" expanded>
|
||||
<b-button
|
||||
tag="button"
|
||||
native-type="submit"
|
||||
:disabled="tryingToCreate"
|
||||
type="is-primary"
|
||||
:value="$t('save')"
|
||||
:label="$t('createvehicle')"
|
||||
expanded
|
||||
>
|
||||
<BaseIcon v-if="tryingToCreate" name="sync" spin />
|
||||
</b-button>
|
||||
<p v-if="authError">
|
||||
There was an error logging in to your account.
|
||||
{{ $t('loginerror') }}
|
||||
</p>
|
||||
</b-field>
|
||||
</form>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import currencyFormtter from 'currency-formatter'
|
||||
import currencyFormatter from 'currency-formatter'
|
||||
|
||||
import appConfig from '@src/app.config'
|
||||
import Layout from '@layouts/main.vue'
|
||||
@@ -53,7 +53,7 @@ export default {
|
||||
return parseAndFormatDate(date)
|
||||
},
|
||||
formatCurrency(number) {
|
||||
return currencyFormtter.format(number, { code: this.me.currency })
|
||||
return currencyFormatter.format(number, { code: this.me.currency })
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -62,14 +62,13 @@ export default {
|
||||
<template>
|
||||
<Layout>
|
||||
<b-notification v-if="myVehicles.length === 0" type="is-warning is-light" :closable="false">
|
||||
<div class="columns">
|
||||
<div class="columns is-three-quarters">
|
||||
<div class="column">
|
||||
It seems you have not yet created a vehicle in the system. Start by creating an entry for
|
||||
one of the vehicles you want to track.
|
||||
{{ $t('novehicles') }}
|
||||
</div>
|
||||
<div class="column" :class="!isMobile ? 'has-text-right' : ''">
|
||||
<div class="column is-one-quarter" :class="!isMobile ? 'has-text-right' : ''">
|
||||
<b-button type="is-warning" class="" tag="router-link" :to="`/vehicles/create`"
|
||||
>Create Now</b-button
|
||||
>{{ $t('createnow') }}</b-button
|
||||
></div
|
||||
>
|
||||
</div>
|
||||
@@ -81,15 +80,11 @@ export default {
|
||||
>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
{{
|
||||
`You have ${unprocessedQuickEntries.length} quick ${
|
||||
unprocessedQuickEntries.length === 1 ? 'entry' : 'entries'
|
||||
} pending to be processed.`
|
||||
}}
|
||||
{{ $tc('unprocessedquickentries', unprocessedQuickEntries.length, { '0': unprocessedQuickEntries.length }) }}
|
||||
</div>
|
||||
<div class="column" :class="!isMobile ? 'has-text-right' : ''">
|
||||
<b-button type="is-warning" class="is-small" tag="router-link" :to="`/quickEntries`"
|
||||
>Process Now</b-button
|
||||
>{{ $t('show') }}</b-button
|
||||
></div
|
||||
>
|
||||
</div>
|
||||
@@ -99,10 +94,10 @@ export default {
|
||||
<br />
|
||||
<section>
|
||||
<div class="columns" :class="isMobile ? 'has-text-centered' : ''"
|
||||
><div class="column is-three-quarters"> <h1 class="title">Your Vehicles</h1></div>
|
||||
><div class="column is-three-quarters"> <h1 class="title">{{ $t('yourvehicles') }}</h1></div>
|
||||
<div class="column is-one-quarter buttons" :class="!isMobile ? 'has-text-right' : ''">
|
||||
<b-button type="is-primary" tag="router-link" :to="`/vehicles/create`"
|
||||
>Add Vehicle</b-button
|
||||
>{{ $t('addvehicle') }}</b-button
|
||||
>
|
||||
</div></div
|
||||
>
|
||||
@@ -125,22 +120,22 @@ export default {
|
||||
<div class="content">
|
||||
<table class="table">
|
||||
<div class="columns">
|
||||
<div class="column is-one-third">Last Fillup</div>
|
||||
<div class="column is-one-third">{{ $t('lastfillup') }}</div>
|
||||
<div class="column"
|
||||
>{{ formatDate(vehicle.fillups[0].date) }} <br />
|
||||
{{ `${formatCurrency(vehicle.fillups[0].totalAmount)}` }} ({{
|
||||
`${vehicle.fillups[0].fuelQuantity} ${vehicle.fillups[0].fuelUnitDetail.short}`
|
||||
`${vehicle.fillups[0].fuelQuantity} ${ $t('unit.short.' + vehicle.fillups[0].fuelUnitDetail.key) }`
|
||||
}}
|
||||
@ {{ `${formatCurrency(vehicle.fillups[0].perUnitPrice)}` }})</div
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-one-third">Odometer</div>
|
||||
<div class="column is-one-third">{{ $t('odometer') }}</div>
|
||||
<div class="column">
|
||||
<template v-if="vehicle.fillups.length">
|
||||
{{ vehicle.fillups[0].odoReading }} {{
|
||||
me.distanceUnitDetail.short
|
||||
$t('unit.short.' + me.distanceUnitDetail.key)
|
||||
}}</template
|
||||
>
|
||||
</div>
|
||||
@@ -150,12 +145,12 @@ export default {
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
<router-link class="card-footer-item" :to="'/vehicles/' + vehicle.id">
|
||||
Details
|
||||
{{ $t('details') }}
|
||||
</router-link>
|
||||
<router-link class="card-footer-item" :to="`/vehicles/${vehicle.id}/fillup`">
|
||||
Add Fillup </router-link
|
||||
{{ $t('addfillup') }} </router-link
|
||||
><router-link class="card-footer-item" :to="`/vehicles/${vehicle.id}/expense`">
|
||||
Add Expense
|
||||
{{ $t('addexpense') }}
|
||||
</router-link>
|
||||
</footer>
|
||||
</b-collapse>
|
||||
|
||||
172
ui/src/router/views/import-drivvo.vue
Normal file
172
ui/src/router/views/import-drivvo.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<script>
|
||||
import Layout from '@layouts/main.vue'
|
||||
import { mapState } from 'vuex'
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
page: {
|
||||
title: 'Import Drivvo',
|
||||
meta: [{ name: 'description', content: 'The Import Drivvo page.' }],
|
||||
},
|
||||
components: { Layout },
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
myVehicles: [],
|
||||
file: null,
|
||||
selectedVehicle: null,
|
||||
tryingToCreate: false,
|
||||
errors: [],
|
||||
importLocation: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('utils', ['isMobile']),
|
||||
...mapState('vehicles', ['vehicles']),
|
||||
uploadButtonLabel() {
|
||||
if (this.isMobile) {
|
||||
if (this.file == null) {
|
||||
return 'Choose Photo'
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
} else {
|
||||
if (this.file == null) {
|
||||
return 'Choose CSV'
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.myVehicles = this.vehicles
|
||||
},
|
||||
methods: {
|
||||
importDrivvo() {
|
||||
console.log('Import from drivvo')
|
||||
if (this.file == null) {
|
||||
return
|
||||
}
|
||||
this.tryingToCreate = true
|
||||
this.errorMessage = ''
|
||||
const formData = new FormData()
|
||||
formData.append('vehicleID', this.selectedVehicle)
|
||||
formData.append('importLocation', this.importLocation)
|
||||
formData.append('file', this.file, this.file.name)
|
||||
axios
|
||||
.post(`/api/import/drivvo`, formData)
|
||||
.then((data) => {
|
||||
this.$buefy.toast.open({
|
||||
message: 'Data Imported Successfully',
|
||||
type: 'is-success',
|
||||
duration: 3000,
|
||||
})
|
||||
this.file = null
|
||||
setTimeout(() => this.$router.push({ name: 'home' }), 1000)
|
||||
})
|
||||
.catch((ex) => {
|
||||
this.$buefy.toast.open({
|
||||
duration: 5000,
|
||||
message: 'There was some issue with importing the file. Please check the error message',
|
||||
position: 'is-bottom',
|
||||
type: 'is-danger',
|
||||
})
|
||||
if (ex.response && ex.response.data.errors) {
|
||||
this.errors = ex.response.data.errors
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.tryingToCreate = false
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Layout>
|
||||
<div class="columns box">
|
||||
<div class="column">
|
||||
<h1 class="title">Import from Drivvo</h1>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<p class="subtitle"> Steps to import data from Drivvo</p>
|
||||
<ol>
|
||||
<li>Export your data from Drivvo in the CSV format.</li>
|
||||
<li>Select the vehicle the exported data is for. You may need to create the vehicle in Hammond first if you haven't already done so</li>
|
||||
<li
|
||||
>Make sure that the <u>Currency</u> and <u>Distance Unit</u> are set correctly in Hammond. Drivvo does not include this information in
|
||||
their export, instead Hammond will use the values set for the user.</li
|
||||
>
|
||||
<li>Similiarly, make sure that the <u>Fuel Unit</u> and <u>Fuel Type</u> are correctly set in the Vehicle.</li>
|
||||
<li>Once you have checked all these points, select the vehicle and import the CSV below.</li>
|
||||
<li><b>Make sure that you do not import the file again as that will create repeat entries.</b></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
><b>PS:</b> If you have <em>'income'</em> and <em>'trips'</em> in your export, they will not be imported to Hammond. The fields
|
||||
<em>'Second fuel'</em> and <em>'Third fuel'</em> are are are also ignored as the use case for these is not understood by us. If you have a use
|
||||
case for this, please open a issue on
|
||||
<a href="https://github.com/akhilrex/hammond/issues">issue tracker</a>
|
||||
</p>
|
||||
<div class="section box">
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-full"> <p class="subtitle">Choose the vehicle, then select the Drivvo CSV and press the import button.</p></div>
|
||||
<div class="column is-full is-flex is-align-content-center">
|
||||
<form @submit.prevent="importDrivvo">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<b-field label="Vehicle" label-position="on-border">
|
||||
<b-select v-model="selectedVehicle" placeholder="Select Vehicle" required>
|
||||
<option v-for="vehicle in myVehicles" :key="vehicle.id" :value="vehicle.id">{{ vehicle.nickname }}</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-field>
|
||||
<b-tooltip label="Whether to import the location for fillups and services or not." multilined>
|
||||
<b-checkbox v-model="importLocation">Import Location?</b-checkbox>
|
||||
</b-tooltip>
|
||||
</b-field>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<b-field class="file is-primary" :class="{ 'has-name': !!file }">
|
||||
<b-upload v-model="file" class="file-label" accept=".csv" required>
|
||||
<span class="file-cta">
|
||||
<b-icon class="file-icon" icon="upload"></b-icon>
|
||||
<span class="file-label">{{ uploadButtonLabel }}</span>
|
||||
</span>
|
||||
<span v-if="file" class="file-name" :class="isMobile ? 'file-name-mobile' : 'file-name-desktop'">
|
||||
{{ file.name }}
|
||||
</span>
|
||||
</b-upload>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-button tag="button" native-type="submit" :disabled="tryingToCreate" type="is-primary" class="control">
|
||||
Import
|
||||
</b-button>
|
||||
</div></div
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<b-message v-if="errors.length" type="is-danger">
|
||||
<ul>
|
||||
<li v-for="error in errors" :key="error">{{ error }}</li>
|
||||
</ul>
|
||||
</b-message>
|
||||
</Layout>
|
||||
</template>
|
||||
@@ -9,24 +9,6 @@ export default {
|
||||
meta: [{ name: 'description', content: 'The Import Fuelly page.' }],
|
||||
},
|
||||
components: { Layout },
|
||||
computed: {
|
||||
...mapState('utils', ['isMobile']),
|
||||
uploadButtonLabel() {
|
||||
if (this.isMobile) {
|
||||
if (this.file == null) {
|
||||
return 'Choose Photo'
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
} else {
|
||||
if (this.file == null) {
|
||||
return 'Choose CSV'
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
@@ -40,6 +22,24 @@ export default {
|
||||
errors: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('utils', ['isMobile']),
|
||||
uploadButtonLabel() {
|
||||
if (this.isMobile) {
|
||||
if (this.file == null) {
|
||||
return this.$t('choosephoto')
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
} else {
|
||||
if (this.file == null) {
|
||||
return this.$t('choosecsv')
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
importFuelly() {
|
||||
if (this.file == null) {
|
||||
@@ -53,16 +53,17 @@ export default {
|
||||
.post(`/api/import/fuelly`, formData)
|
||||
.then((data) => {
|
||||
this.$buefy.toast.open({
|
||||
message: 'Data Imported Successfully',
|
||||
message: this.$t('importsuccessfull'),
|
||||
type: 'is-success',
|
||||
duration: 3000,
|
||||
})
|
||||
this.file = null
|
||||
setTimeout(() => this.$router.push({ name: 'home' }), 1000)
|
||||
})
|
||||
.catch((ex) => {
|
||||
this.$buefy.toast.open({
|
||||
duration: 5000,
|
||||
message: 'There was some issue with importing the file. Please check the error message',
|
||||
message: this.$t('importerror'),
|
||||
position: 'is-bottom',
|
||||
type: 'is-danger',
|
||||
})
|
||||
@@ -82,39 +83,33 @@ export default {
|
||||
<Layout>
|
||||
<div class="columns box">
|
||||
<div class="column">
|
||||
<h1 class="title">Import from Fuelly</h1>
|
||||
<h1 class="title">{{ $t('importfrom', { 'name': 'Fuelly' }) }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<p class="subtitle"> Steps to import data from Fuelly</p>
|
||||
<p class="subtitle"> {{ $t('stepstoimport', { 'name': 'Fuelly' }) }}</p>
|
||||
<ol>
|
||||
<li
|
||||
>Export your data from Fuelly in the CSV format. Steps to do that can be found
|
||||
<a href="http://docs.fuelly.com/acar-import-export-center" target="_nofollow">here</a>.</li
|
||||
>
|
||||
<li>Make sure that you have already created the vehicles in Hammond platform.</li>
|
||||
<li>Make sure that the Vehicle nickname in Hammond is exactly the same as the name on Fuelly CSV or the import will not work.</li>
|
||||
<li
|
||||
>Make sure that the <u>Currency</u> and <u>Distance Unit</u> are set correctly in Hammond. Import will not autodetect Currency from the
|
||||
CSV but use the one set for the user.</li
|
||||
>
|
||||
<li>Similiarly, make sure that the <u>Fuel Unit</u> and <u>Fuel Type</u> are correctly set in the Vehicle.</li>
|
||||
<li>Once you have checked all these points,just import the CSV below.</li>
|
||||
<li><b>Make sure that you do not import the file again and that will create repeat entries.</b></li>
|
||||
<li>{{ $t('importhintcreatecsv', { 'name': 'Fuelly' }) }} <a href="http://docs.fuelly.com/acar-import-export-center" target="_nofollow">{{ $t('here') }}</a>.</li>
|
||||
<li>{{ $t('importhintvehiclecreated') }}</li>
|
||||
<li>{{ $t('importhintnickname') }}</li>
|
||||
<li v-html="$t('importhintcurrdist')"></li>
|
||||
<li v-html="$t('importhintunits')"></li>
|
||||
<li>{{ $t('checkpointsimportcsv') }}</li>
|
||||
<li><b>{{ $t('dontimportagain') }}</b></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section box">
|
||||
<div class="columns">
|
||||
<div class="column is-two-thirds"> <p class="subtitle">Choose the Fuelly CSV and press the import button.</p></div>
|
||||
<div class="column is-two-thirds"> <p class="subtitle">{{ $t('choosecsvimport', { 'name': 'Fuelly' }) }}</p></div>
|
||||
<div class="column is-one-third is-flex is-align-content-center">
|
||||
<form @submit.prevent="importFuelly">
|
||||
<div class="columns"
|
||||
><div class="column">
|
||||
<b-field class="file is-primary" :class="{ 'has-name': !!file }">
|
||||
<b-upload v-model="file" class="file-label" accept=".csv">
|
||||
<b-upload v-model="file" class="file-label" accept=".csv" required>
|
||||
<span class="file-cta">
|
||||
<b-icon class="file-icon" icon="upload"></b-icon>
|
||||
<span class="file-label">{{ uploadButtonLabel }}</span>
|
||||
@@ -126,8 +121,8 @@ export default {
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-button tag="input" native-type="submit" :disabled="tryingToCreate" type="is-primary" value="Upload File" class="control">
|
||||
Import
|
||||
<b-button tag="button" native-type="submit" :disabled="tryingToCreate" type="is-primary" class="control">
|
||||
{{ $t('import') }}
|
||||
</b-button>
|
||||
</div></div
|
||||
>
|
||||
|
||||
7
ui/src/router/views/import-generic.unit.js
Normal file
7
ui/src/router/views/import-generic.unit.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import ImportGeneric from './import-generic'
|
||||
|
||||
describe('@views/import-generic', () => {
|
||||
it('is a valid view', () => {
|
||||
expect(ImportGeneric).toBeAViewComponent()
|
||||
})
|
||||
})
|
||||
411
ui/src/router/views/import-generic.vue
Normal file
411
ui/src/router/views/import-generic.vue
Normal file
@@ -0,0 +1,411 @@
|
||||
<script>
|
||||
import Layout from '@layouts/main.vue'
|
||||
import { mapState } from 'vuex'
|
||||
import axios from 'axios'
|
||||
import Papa from 'papaparse'
|
||||
|
||||
export default {
|
||||
page: {
|
||||
title: 'Generic Import',
|
||||
meta: [{ name: 'description', content: 'The Generic Import page.' }],
|
||||
},
|
||||
components: { Layout },
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
file: null,
|
||||
tryingToCreate: false,
|
||||
errors: [],
|
||||
papaConfig: { dynamicTyping: true, skipEmptyLines: true, complete: this.assignResults },
|
||||
fileData: null,
|
||||
fileHeadings: null,
|
||||
myVehicles: [],
|
||||
selectedVehicle: null,
|
||||
invertFullTank: null,
|
||||
filledValueString: '',
|
||||
notFilledValueString: '',
|
||||
isFullTankString: false,
|
||||
fileHeadingMap: {
|
||||
fuelQuantity: null,
|
||||
perUnitPrice: null,
|
||||
totalAmount: null,
|
||||
odoReading: null,
|
||||
isTankFull: null,
|
||||
hasMissedFillup: null,
|
||||
comments: [], // [int]
|
||||
fillingStation: null,
|
||||
date: null,
|
||||
fuelSubType: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('utils', ['isMobile']),
|
||||
...mapState('vehicles', ['vehicles']),
|
||||
uploadButtonLabel() {
|
||||
if (this.isMobile) {
|
||||
if (this.file == null) {
|
||||
return this.$t('choosephoto')
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
} else {
|
||||
if (this.file == null) {
|
||||
return this.$t('choosefile')
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.myVehicles = this.vehicles
|
||||
},
|
||||
methods: {
|
||||
assignResults(results, file) {
|
||||
this.fileData = results.data
|
||||
this.fileHeadings = results.data[0]
|
||||
},
|
||||
parseCSV() {
|
||||
if (this.file == null) {
|
||||
return
|
||||
}
|
||||
this.errorMessage = ''
|
||||
Papa.parse(this.file, this.papaConfig)
|
||||
},
|
||||
getUsedHeadings() {
|
||||
return Object.keys(this.fileHeadingMap).filter((k) => this.fileHeadingMap[k] != null) // filter non-null properties
|
||||
},
|
||||
getTimezone() {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
},
|
||||
csvToJson() {
|
||||
const data = []
|
||||
const headings = this.getUsedHeadings().reduce((a, k) => ({ ...a, [k]: this.fileHeadingMap[k] }), {}) // create new object from filter
|
||||
const comments = (row) => {
|
||||
return this.fileHeadingMap.comments.reduce((a, fi) => {
|
||||
// TODO: sanitize to prevent XSS
|
||||
return `${a}${this.fileHeadings[fi]}: ${row[fi]}\n`
|
||||
}, '')
|
||||
}
|
||||
const calculateTotal = (row) => {
|
||||
return this.fileHeadingMap.totalAmount === -1
|
||||
? row[this.fileHeadingMap.fuelQuantity] * row[this.fileHeadingMap.perUnitPrice]
|
||||
: row[this.fileHeadingMap.totalAmount]
|
||||
}
|
||||
|
||||
const setFullTank = (row) => {
|
||||
if (row[this.fileHeadingMap.isTankFull].toLowerCase() === this.filledValueString.toLowerCase()) {
|
||||
return true
|
||||
} else if (row[this.fileHeadingMap.isTankFull].toLowerCase() === this.notFilledValueString.toLowerCase()) {
|
||||
return false
|
||||
} else {
|
||||
// TODO: need to handle errors better
|
||||
throw Error
|
||||
}
|
||||
}
|
||||
|
||||
for (let r = 1; r < this.fileData.length; r++) {
|
||||
const row = this.fileData[r]
|
||||
const item = {}
|
||||
Object.keys(headings).forEach((k) => {
|
||||
if (k === 'comments') {
|
||||
item[k] = comments(row)
|
||||
} else if (k === 'totalAmount') {
|
||||
item[k] = calculateTotal(row)
|
||||
} else if (k === 'isTankFull') {
|
||||
if (this.isFullTankString) {
|
||||
item[k] = setFullTank(row)
|
||||
} else {
|
||||
if (this.invertFullTank) {
|
||||
item[k] = Boolean(!row[headings[k]])
|
||||
} else {
|
||||
item[k] = Boolean(row[headings[k]])
|
||||
}
|
||||
}
|
||||
} else if (k === 'hasMissedFillup') {
|
||||
// TODO: need to account for this field being a string
|
||||
item[k] = Boolean(row[headings[k]])
|
||||
} else if (k === 'date') {
|
||||
item[k] = new Date(row[headings[k]]).toISOString()
|
||||
} else {
|
||||
item[k] = row[headings[k]]
|
||||
}
|
||||
})
|
||||
data.push(item)
|
||||
}
|
||||
return data
|
||||
},
|
||||
importData() {
|
||||
if (this.errors.length === 0) {
|
||||
try {
|
||||
const content = {
|
||||
data: this.csvToJson(),
|
||||
vehicleId: this.selectedVehicle.id,
|
||||
timezone: this.getTimezone(),
|
||||
}
|
||||
axios
|
||||
.post('/api/import/generic', content)
|
||||
.then((data) => {
|
||||
this.$buefy.toast.open({
|
||||
message: this.$t('importsuccessfull'),
|
||||
type: 'is-success',
|
||||
duration: 3000,
|
||||
})
|
||||
setTimeout(() => this.$router.push({ name: 'home' }), 1000)
|
||||
})
|
||||
.catch((ex) => {
|
||||
this.$buefy.toast.open({
|
||||
duration: 5000,
|
||||
message: this.$t('importerror'),
|
||||
position: 'is-bottom',
|
||||
type: 'is-danger',
|
||||
})
|
||||
console.log(ex)
|
||||
if (ex.response && ex.response.data.error) {
|
||||
this.errors.push(ex.response.data.error)
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
// TODO: handle error
|
||||
this.errors.push(e)
|
||||
}
|
||||
} else {
|
||||
this.errors.push('fix errors')
|
||||
}
|
||||
},
|
||||
checkFieldString() {
|
||||
const tankFull = this.fileData[1][this.fileHeadingMap.isTankFull]
|
||||
if (typeof tankFull !== 'boolean' && typeof tankFull === 'string') {
|
||||
this.isFullTankString = true
|
||||
}
|
||||
},
|
||||
clearHeadingProperty(property) {
|
||||
if (property === 'comments') {
|
||||
this.fileHeadingMap[property] = []
|
||||
} else {
|
||||
this.fileHeadingMap[property] = null
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Layout>
|
||||
<div class="columns box">
|
||||
<div class="column">
|
||||
<h1 class="title">{{ $t('importgeneric') }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div v-if="fileData === null" class="columns">
|
||||
<div class="column">
|
||||
<p class="subtitle"> {{ $t('stepstoimport', { name: 'CSV' }) }}</p>
|
||||
<ol>
|
||||
<!-- <li>{{ $t('importhintcreatecsv', { 'name': 'Fuelly' }) }} <a href="http://docs.fuelly.com/acar-import-export-center" target="_nofollow">{{ $t('here') }}</a>.</li> -->
|
||||
<li>{{ $t('importgenerichintdata') }}</li>
|
||||
<li>{{ $t('importhintvehiclecreated') }}</li>
|
||||
<li v-html="$t('importhintcurrdist')"></li>
|
||||
<li v-html="$t('importhintunits')"></li>
|
||||
<li>
|
||||
<b>{{ $t('dontimportagain') }}</b>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="fileData === null" class="section box">
|
||||
<div class="columns">
|
||||
<div class="column is-two-thirds">
|
||||
<p class="subtitle">{{ $t('choosedatafile') }}</p>
|
||||
</div>
|
||||
<div class="column is-one-third is-flex is-align-content-center">
|
||||
<form @submit.prevent="parseCSV">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<b-field class="file is-primary" :class="{ 'has-name': !!file }">
|
||||
<b-upload v-model="file" class="file-label" accept=".csv" required>
|
||||
<span class="file-cta">
|
||||
<b-icon class="file-icon" icon="upload"></b-icon>
|
||||
<span class="file-label">{{ uploadButtonLabel }}</span>
|
||||
</span>
|
||||
<span v-if="file" class="file-name" :class="isMobile ? 'file-name-mobile' : 'file-name-desktop'">
|
||||
{{ file.name }}
|
||||
</span>
|
||||
</b-upload>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-button tag="button" native-type="submit" type="is-primary" class="control">
|
||||
{{ $t('import') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="columns">
|
||||
<div class="column">
|
||||
<p class="subtitle">Map Fields</p>
|
||||
<form class="" @submit.prevent="importData">
|
||||
<b-field :label="$t('selectvehicle')">
|
||||
<b-select v-model="selectedVehicle" :placeholder="$t('vehicle')" required expanded>
|
||||
<option v-for="option in myVehicles" :key="option.id" :value="option">
|
||||
{{ option.nickname }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<span v-if="selectedVehicle !== null">
|
||||
<b-field :label="$t('fillupdate')">
|
||||
<b-select v-model="fileHeadingMap.date" required expanded>
|
||||
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
||||
{{ option }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<template v-slot:label>
|
||||
{{ $t('fuelsubtype') }}
|
||||
<b-tooltip type="is-dark" label="Clear selection">
|
||||
<b-button
|
||||
type="is-ghost"
|
||||
size="is-small"
|
||||
icon-pack="fas"
|
||||
icon-right="arrow-rotate-left"
|
||||
@click="clearHeadingProperty('fuelSubType')"
|
||||
></b-button>
|
||||
</b-tooltip>
|
||||
</template>
|
||||
<b-select v-model="fileHeadingMap.fuelSubType" expanded>
|
||||
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
||||
{{ option }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="$t('quantity')">
|
||||
<b-select v-model="fileHeadingMap.fuelQuantity" expanded required>
|
||||
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
||||
{{ option }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="$t('per', { '0': $t('price'), '1': $t('unit.short.' + selectedVehicle.fuelUnitDetail.key) })">
|
||||
<b-select v-model.number="fileHeadingMap.perUnitPrice" type="number" min="0" step=".001" expanded required>
|
||||
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
||||
{{ option }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="$t('totalamountpaid')">
|
||||
<b-select v-model.number="fileHeadingMap.totalAmount" expanded required>
|
||||
<option value="-1">Calculated</option>
|
||||
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
||||
{{ option }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="$t('odometer')">
|
||||
<b-select v-model.number="fileHeadingMap.odoReading" expanded required>
|
||||
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
||||
{{ option }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="$t('tankpartialfull')">
|
||||
<b-radio-button v-model="invertFullTank" native-value="false">{{ $t('fulltank') }}</b-radio-button>
|
||||
<b-radio-button v-model="invertFullTank" native-value="true">{{ $t('partialfillup') }}</b-radio-button>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-select v-model="fileHeadingMap.isTankFull" required @input="checkFieldString">
|
||||
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
||||
{{ option }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<span v-if="isFullTankString === true" required>
|
||||
<b-field label="Value when tank is filled">
|
||||
<b-input v-model="filledValueString"></b-input>
|
||||
</b-field>
|
||||
<b-field label="Value when tank was not completely filled">
|
||||
<b-input v-model="notFilledValueString"></b-input>
|
||||
</b-field>
|
||||
</span>
|
||||
<b-field>
|
||||
<template v-slot:label>
|
||||
{{ $t('missedfillup') }}
|
||||
<b-tooltip type="is-dark" label="Clear selection">
|
||||
<b-button
|
||||
type="is-ghost"
|
||||
size="is-small"
|
||||
icon-pack="fas"
|
||||
icon-right="arrow-rotate-left"
|
||||
@click="clearHeadingProperty('hasMissedFillup')"
|
||||
></b-button>
|
||||
</b-tooltip>
|
||||
</template>
|
||||
<b-select v-model="fileHeadingMap.hasMissedFillup">
|
||||
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
||||
{{ option }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<template v-slot:label>
|
||||
{{ $t('fillingstation') }}
|
||||
<b-tooltip type="is-dark" label="Clear selection">
|
||||
<b-button
|
||||
type="is-ghost"
|
||||
size="is-small"
|
||||
icon-pack="fas"
|
||||
icon-right="arrow-rotate-left"
|
||||
@click="clearHeadingProperty('fillingStation')"
|
||||
></b-button>
|
||||
</b-tooltip>
|
||||
</template>
|
||||
<b-select v-model="fileHeadingMap.fillingStation">
|
||||
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
||||
{{ option }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<template v-slot:label>
|
||||
{{ $t('comments') }}
|
||||
<b-tooltip type="is-dark" label="Clear selection">
|
||||
<b-button
|
||||
type="is-ghost"
|
||||
size="is-small"
|
||||
icon-pack="fas"
|
||||
icon-right="arrow-rotate-left"
|
||||
@click="clearHeadingProperty('comments')"
|
||||
></b-button>
|
||||
</b-tooltip>
|
||||
</template>
|
||||
<b-select v-model="fileHeadingMap.comments" type="textarea" multiple expanded>
|
||||
<option v-for="(option, index) in fileHeadings" :key="index" :value="index">
|
||||
{{ option }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<br />
|
||||
<b-field>
|
||||
<b-button tag="button" native-type="submit" type="is-primary" :value="$t('save')" :label="$t('import')" expanded />
|
||||
<p v-if="authError"> There was an error logging in to your account. </p>
|
||||
</b-field>
|
||||
</span>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<b-message v-if="errors.length" type="is-danger">
|
||||
<ul>
|
||||
<li v-for="error in errors" :key="error">{{ error }}</li>
|
||||
</ul>
|
||||
</b-message>
|
||||
</Layout>
|
||||
</template>
|
||||
@@ -18,19 +18,41 @@ export default {
|
||||
|
||||
<template>
|
||||
<Layout>
|
||||
<div class="columns box"
|
||||
><div class="column">
|
||||
<h1 class="title">Import data into Hammond</h1>
|
||||
<p class="subtitle">Choose from the following options to import data into Fuelly</p>
|
||||
</div></div
|
||||
>
|
||||
<div class="columns box">
|
||||
<div class="column">
|
||||
<h1 class="title">{{ $t('importdata') }}</h1>
|
||||
<p class="subtitle">{{ $t('importdatadesc') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="columns">
|
||||
<div class="box column is-one-third" to="/import-fuelly">
|
||||
<h1 class="title">Fuelly</h1>
|
||||
<p>If you have been using Fuelly to store your vehicle data, export the CSV file from Fuelly and click here to import.</p>
|
||||
<br />
|
||||
<b-button type="is-primary" tag="router-link" to="/import/fuelly">Import</b-button>
|
||||
<div class="column is-one-third">
|
||||
<div class="box">
|
||||
<h1 class="title">Fuelly</h1>
|
||||
<p>If you have been using Fuelly to store your vehicle data, export the CSV file from Fuelly and click here to
|
||||
import.</p>
|
||||
<br />
|
||||
<b-button type="is-primary" tag="router-link" to="/import/fuelly">{{ $t('import') }}</b-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-one-third" to="/import-fuelly">
|
||||
<div class="box">
|
||||
<h1 class="title">Drivvo</h1>
|
||||
<p>{{ $t('importcsv', { 'name': 'Fuelly' }) }}</p>
|
||||
<br />
|
||||
<b-button type="is-primary" tag="router-link" to="/import/drivvo">{{ $t('import') }}</b-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="column is-one-third" to="/import-generic">
|
||||
<div class="box">
|
||||
<h1 class="title">{{ $t('importgeneric') }}</h1>
|
||||
<p>{{ $t('importgenericdesc') }}</p>
|
||||
<br />
|
||||
<b-button type="is-primary" tag="router-link" to="/import/generic">{{ $t('import') }}</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
@@ -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="$t('mysqlconnstr')">
|
||||
<b-input v-model="url" required></b-input>
|
||||
</b-field>
|
||||
|
||||
<div class="buttons">
|
||||
<b-button
|
||||
v-if="!testSuccess"
|
||||
type="is-primary"
|
||||
:disabled="isWorking"
|
||||
@click="testConnection"
|
||||
>Test Connection</b-button
|
||||
><b-button v-if="testSuccess" type="is-success" :disabled="isWorking" @click="migrate"
|
||||
>Migrate</b-button
|
||||
>
|
||||
<b-button type="is-danger is-light" @click="resetMigrationMode">Cancel</b-button>
|
||||
<b-button v-if="!testSuccess" type="is-primary" :disabled="isWorking" @click="testConnection">{{ $t('testconn') }}</b-button>
|
||||
<b-button v-if="testSuccess" type="is-success" :disabled="isWorking" @click="migrate">{{ $t('migrate') }}</b-button>
|
||||
<b-button type="is-danger is-light" @click="resetMigrationMode">{{ $t('cancel') }}</b-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="migrationMode === 'fresh'" class="box content">
|
||||
<h1 class="title">Setup Admin Users</h1>
|
||||
<h1 class="title">{{ $t('init.fresh.setupadminuser') }}</h1>
|
||||
<form @submit.prevent="register">
|
||||
<b-field label="Your Name">
|
||||
<b-field :label="$t('init.fresh.yourname')">
|
||||
<b-input v-model="registerModel.name" required></b-input>
|
||||
</b-field>
|
||||
<b-field label="Your Email">
|
||||
<b-field :label="$t('init.fresh.youremail')">
|
||||
<b-input v-model="registerModel.email" type="email" required></b-input>
|
||||
</b-field>
|
||||
<b-field label="Your Password">
|
||||
<b-input
|
||||
v-model="registerModel.password"
|
||||
type="password"
|
||||
required
|
||||
minlength="8"
|
||||
password-reveal
|
||||
></b-input>
|
||||
<b-field :label="$t('init.fresh.yourpassword')">
|
||||
<b-input v-model="registerModel.password" type="password" required minlength="8" password-reveal></b-input>
|
||||
</b-field>
|
||||
<b-field label="Currency">
|
||||
<b-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="$t('currency')">
|
||||
<b-autocomplete
|
||||
v-model="registerModel.currency"
|
||||
:custom-formatter="formatCurrency"
|
||||
:placeholder="$t('currency')"
|
||||
:data="filteredCurrencyMasters"
|
||||
:keep-first="true"
|
||||
:open-on-focus="true"
|
||||
required
|
||||
expanded
|
||||
>
|
||||
@select="(option) => (selected = option)"
|
||||
></b-autocomplete>
|
||||
</b-field>
|
||||
<b-field :label="$t('distanceunit')">
|
||||
<b-select v-model.number="registerModel.distanceUnit" :placeholder="$t('distanceunit')" required expanded>
|
||||
<option v-for="(option, key) in distanceUnitMasters" :key="key" :value="key">
|
||||
{{ `${option.long} (${option.short})` }}
|
||||
{{ `${$t('unit.long.' + option.key)} (${$t('unit.short.' + option.key)})` }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<br />
|
||||
<div class="buttons">
|
||||
<b-button type="is-primary" native-type="submit" tag="input"></b-button>
|
||||
<b-button type="is-primary" native-type="submit" tag="button" :value="$t('save')">{{ $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="button" native-type="submit" :disabled="tryingToLogIn" type="is-primary">
|
||||
<BaseIcon v-if="tryingToLogIn" name="sync" spin />
|
||||
<span v-else>
|
||||
Log in
|
||||
{{ $t('login') }}
|
||||
</span>
|
||||
</b-button>
|
||||
<p v-if="authError">
|
||||
There was an error logging in to your account. {{errorMessage}}
|
||||
</p>
|
||||
<p v-if="authError"> {{ $t('loginerror', { msg: errorMessage }) }}</p>
|
||||
</form>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
@@ -28,7 +28,7 @@ export default {
|
||||
<h1>
|
||||
<BaseIcon name="user" />
|
||||
{{ user.name }}
|
||||
Profile
|
||||
{{ $t('profile') }}
|
||||
</h1>
|
||||
<pre>{{ user }}</pre>
|
||||
</Layout>
|
||||
|
||||
@@ -41,7 +41,7 @@ export default {
|
||||
store.dispatch('vehicles/setQuickEntryAsProcessed', { id: entry.id }).then((data) => {})
|
||||
},
|
||||
deleteQuickEntry(entry) {
|
||||
var sure = confirm('This will delete this Quick Entry. This step cannot be reversed. Are you sure?')
|
||||
var sure = confirm(this.$t('deletequickentry'))
|
||||
if (sure) {
|
||||
store.dispatch('vehicles/deleteQuickEntry', { id: entry.id }).then((data) => {})
|
||||
}
|
||||
@@ -59,9 +59,9 @@ export default {
|
||||
|
||||
<template>
|
||||
<Layout>
|
||||
<h1 class="title">Quick Entries</h1>
|
||||
<h1 class="title">{{ $tc('quickentry', 2) }}</h1>
|
||||
<b-field>
|
||||
<b-switch v-if="unprocessedQuickEntries.length" v-model="showUnprocessedOnly">Show unprocessed only</b-switch>
|
||||
<b-switch v-if="unprocessedQuickEntries.length" v-model="showUnprocessedOnly">{{ $t('showunprocessed') }}</b-switch>
|
||||
</b-field>
|
||||
<div v-for="(chunk, index) in chunkedQuickEntries" :key="index" class="tile is-ancestor">
|
||||
<div v-for="entry in chunk" :key="entry.id" class="tile is-parent" :class="{ 'is-4': quickEntries.length <= 3 }">
|
||||
@@ -71,7 +71,7 @@ export default {
|
||||
<div class="card-header-title">
|
||||
{{ parseAndFormatDateTime(entry.createdAt) }}
|
||||
</div>
|
||||
<b-tag v-if="entry.processDate === null" class="is-align-content-center" type="is-primary">unprocessed</b-tag>
|
||||
<b-tag v-if="entry.processDate === null" class="is-align-content-center" type="is-primary">{{ $t('unprocessed') }}</b-tag>
|
||||
</div>
|
||||
<div class="card-image">
|
||||
<!-- prettier-ignore -->
|
||||
@@ -87,22 +87,22 @@ export default {
|
||||
>
|
||||
<footer class="card-footer">
|
||||
<router-link v-if="entry.processDate === null && vehicles.length" :to="`/vehicles/${vehicles[0].id}/fillup`" class="card-footer-item"
|
||||
>Create Fillup</router-link
|
||||
>{{ $t('addfillup') }}</router-link
|
||||
>
|
||||
<router-link v-if="entry.processDate === null && vehicles.length" :to="`/vehicles/${vehicles[0].id}/expense`" class="card-footer-item"
|
||||
>Create Expense</router-link
|
||||
>{{ $t('addexpense') }}</router-link
|
||||
>
|
||||
|
||||
<a v-if="entry.processDate === null" class="card-footer-item" @click="markProcessed(entry)">Mark Processed</a>
|
||||
<p v-else class="card-footer-item">Processed on {{ parseAndFormatDateTime(entry.processDate) }}</p>
|
||||
<a class="card-footer-item" type="is-danger" @click="deleteQuickEntry(entry)"> Delete</a>
|
||||
<a v-if="entry.processDate === null" class="card-footer-item" @click="markProcessed(entry)">{{ $t('processed') }}</a>
|
||||
<p v-else class="card-footer-item">{{ $t('processedon') }} {{ parseAndFormatDateTime(entry.processDate) }}</p>
|
||||
<a class="card-footer-item" type="is-danger" @click="deleteQuickEntry(entry)"> {{ $t('delete') }}</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!quickEntries.length" class="box">
|
||||
<p>No Quick Entries right now.</p>
|
||||
<p>{{ $tc('quickentry',0) }}</p>
|
||||
</div>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
@@ -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,27 @@ export default {
|
||||
</b-field>
|
||||
<br />
|
||||
<b-field>
|
||||
<b-button tag="input" native-type="submit" :disabled="tryingToSave" type="is-primary" value="Save" expanded> </b-button>
|
||||
<b-button tag="button" native-type="submit" :disabled="tryingToSave" type="is-primary" expanded> {{ $t('save') }} </b-button>
|
||||
</b-field>
|
||||
</form>
|
||||
</div>
|
||||
<div class="column">
|
||||
<form class="box" @submit.prevent="changePassword">
|
||||
<h1 class="subtitle">Change password</h1>
|
||||
<b-field label="Old Password">
|
||||
<h1 class="subtitle">{{ $t('changepassword') }}</h1>
|
||||
<b-field :label="$t('oldpassword')">
|
||||
<b-input v-model="changePassModel.old" required minlength="6" password-reveal type="password"></b-input>
|
||||
</b-field>
|
||||
<b-field label="New Password">
|
||||
<b-field :label="$t('newpassword')">
|
||||
<b-input v-model="changePassModel.new" required minlength="6" password-reveal type="password"></b-input>
|
||||
</b-field>
|
||||
<b-field label="Repeat New Password">
|
||||
<b-field :label="$t('repeatnewpassword')">
|
||||
<b-input v-model="changePassModel.renew" required minlength="6" password-reveal type="password"></b-input>
|
||||
</b-field>
|
||||
<p v-if="!passwordValid" class="help is-danger">Password values don't match</p>
|
||||
<p v-if="!passwordValid" class="help is-danger">{{ $t('passworddontmatch') }}</p>
|
||||
<b-field>
|
||||
<b-button tag="input" native-type="submit" :disabled="!passwordValid" type="is-primary" value="Change Password" expanded> </b-button>
|
||||
<b-button tag="button" native-type="submit" :disabled="!passwordValid" type="is-primary" expanded>
|
||||
{{ $t('changepassword') }}
|
||||
</b-button>
|
||||
</b-field>
|
||||
</form>
|
||||
</div>
|
||||
@@ -171,48 +195,32 @@ export default {
|
||||
<hr />
|
||||
<div class="columns">
|
||||
<div class="twelve">
|
||||
<h3 class="title">More Info</h3>
|
||||
<p style="font-style: italic;">
|
||||
This project is under active development which means I release new updates very frequently. I will eventually build the version
|
||||
management/update checking mechanism. Until then it is recommended that you use something like watchtower which will automatically update
|
||||
your containers whenever I release a new version or periodically rebuild the container with the latest image manually.
|
||||
</p>
|
||||
<br />
|
||||
<h3 class="title">{{ $t('moreinfo') }}</h3>
|
||||
<table class="table is-hoverable">
|
||||
<tr>
|
||||
<td>Current Version</td>
|
||||
<td>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="$t('currency')">
|
||||
<b-select v-model="settingsModel.currency" :placeholder="$t('currency')" required expanded>
|
||||
<option v-for="option in currencyMasters" :key="option.code" :value="option.code">
|
||||
{{ `${option.namePlural} (${option.code})` }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field label="Distance Unit">
|
||||
<b-select v-model.number="settingsModel.distanceUnit" placeholder="Distance Unit" required expanded>
|
||||
<b-field :label="$t('distanceunit')">
|
||||
<b-select v-model.number="settingsModel.distanceUnit" :placeholder="$t('distanceunit')" required expanded>
|
||||
<option v-for="(option, key) in distanceUnitMasters" :key="key" :value="key">
|
||||
{{ `${option.long} (${option.short})` }}
|
||||
{{ `${$t('unit.long.' + option.key)} (${$t('unit.short.' + option.key)})` }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<br />
|
||||
<b-field>
|
||||
<b-button tag="input" native-type="submit" :disabled="tryingToSave" type="is-primary" value="Save" expanded> </b-button>
|
||||
<b-button tag="button" native-type="submit" :disabled="tryingToSave" type="is-primary" expanded> {{ $t('save') }}</b-button>
|
||||
</b-field>
|
||||
</form>
|
||||
</Layout>
|
||||
|
||||
@@ -55,18 +55,18 @@ export default {
|
||||
},
|
||||
changeDisabledStatus(userId,status){
|
||||
this.$buefy.dialog.confirm({
|
||||
title: status?'Disable User':"Enable User",
|
||||
message: 'Are you sure you want to do this?',
|
||||
cancelText: 'Cancel',
|
||||
confirmText: 'Go Ahead',
|
||||
title: status ? this.$t('disable') : this.$t('enable'),
|
||||
message: this.$t('areyousure'),
|
||||
cancelText: this.$t('cancel'),
|
||||
confirmText: this.$t('confirm'),
|
||||
onConfirm: () => {
|
||||
|
||||
var url = `/api/users/${userId}/${status?"disable":"enable"}`
|
||||
var url = `/api/users/${userId}/${status ? "disable" : "enable"}`
|
||||
axios
|
||||
.post(url, {})
|
||||
.then((data) => {
|
||||
this.$buefy.toast.open({
|
||||
message: status?"User disabled successfully":'User enabled successfully',
|
||||
message: status ? this.$t('userdisabledsuccessfully') : this.$t('userenabledsuccessfully'),
|
||||
type: 'is-success',
|
||||
duration: 3000,
|
||||
})
|
||||
@@ -103,7 +103,7 @@ export default {
|
||||
if (success) {
|
||||
this.$buefy.toast.open({
|
||||
duration: 10000,
|
||||
message: 'User Created Successfully',
|
||||
message: this.$t('usercreatedsuccessfully'),
|
||||
position: 'is-bottom',
|
||||
type: 'is-success',
|
||||
})
|
||||
@@ -129,22 +129,22 @@ export default {
|
||||
<Layout>
|
||||
<div class="box">
|
||||
<div class="columns">
|
||||
<div class="column is-three-quarters"> <h1 class="title is-4">Users</h1> </div>
|
||||
<div class="column is-three-quarters"> <h1 class="title is-4">{{ $t('menu.users') }}</h1> </div>
|
||||
<div class="column is-one-quarter">
|
||||
<b-button type="is-primary" @click="showUserForm = true">Add User</b-button>
|
||||
<b-button type="is-primary" @click="showUserForm = true">{{ $t('adduser') }}</b-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showUserForm" class="box content">
|
||||
<h1 class="title">Create New User</h1>
|
||||
<h1 class="title">{{ $t('createnewuser') }}</h1>
|
||||
<form @submit.prevent="register">
|
||||
<b-field label="Name">
|
||||
<b-field :label="$t('name')">
|
||||
<b-input v-model="registerModel.name" required></b-input>
|
||||
</b-field>
|
||||
<b-field label="Email">
|
||||
<b-field :label="$t('email')">
|
||||
<b-input v-model="registerModel.email" type="email" required></b-input>
|
||||
</b-field>
|
||||
<b-field label="Password">
|
||||
<b-field :label="$t('password')">
|
||||
<b-input
|
||||
v-model="registerModel.password"
|
||||
type="password"
|
||||
@@ -153,56 +153,56 @@ export default {
|
||||
password-reveal
|
||||
></b-input>
|
||||
</b-field>
|
||||
<b-field label="Role">
|
||||
<b-select v-model.number="registerModel.role" placeholder="Role" required expanded>
|
||||
<b-field :label="$t('role')">
|
||||
<b-select v-model.number="registerModel.role" :placeholder="$t('role')" required expanded>
|
||||
<option v-for="(option, key) in roleMasters" :key="key" :value="key">
|
||||
{{ `${option.long}` }}
|
||||
{{ `${option.key}` }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field label="Currency">
|
||||
<b-select v-model="registerModel.currency" placeholder="Currency" required expanded>
|
||||
<b-field :label="$t('currency')">
|
||||
<b-select v-model="registerModel.currency" :placeholder="$t('currency')" required expanded>
|
||||
<option v-for="option in currencyMasters" :key="option.code" :value="option.code">
|
||||
{{ `${option.namePlural} (${option.code})` }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field label="Distance Unit">
|
||||
<b-field :label="$t('distanceunit')">
|
||||
<b-select
|
||||
v-model.number="registerModel.distanceUnit"
|
||||
placeholder="Distance Unit"
|
||||
:placeholder="$t('distanceunit')"
|
||||
required
|
||||
expanded
|
||||
>
|
||||
<option v-for="(option, key) in distanceUnitMasters" :key="key" :value="key">
|
||||
{{ `${option.long} (${option.short})` }}
|
||||
{{ `${$t('unit.long.' + option.key)} (${$t('unit.short.' + option.key)})` }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<br />
|
||||
<div class="buttons">
|
||||
<b-button type="is-primary" native-type="submit" tag="input"></b-button>
|
||||
<b-button type="is-primary" native-type="submit" tag="button">{{ $t('save') }}</b-button>
|
||||
|
||||
<b-button type="is-danger is-light" @click="resetUserForm">Cancel</b-button>
|
||||
<b-button type="is-danger is-light" @click="resetUserForm">{{ $t('cancel') }}</b-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<b-table :data="users" hoverable mobile-cards detail-key="id" paginated per-page="10" :row-class="(row, index) => row.isDisabled && 'is-disabled'">
|
||||
<b-table-column v-slot="props" field="name" label="Name">
|
||||
{{ `${props.row.name}` }} <template v-if="props.row.id === user.id">(You)</template>
|
||||
<b-table-column v-slot="props" field="name" :label="$t('name')">
|
||||
{{ `${props.row.name}` }} <template v-if="props.row.id === user.id">({{ $t('you') }})</template>
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" field="email" label="Email">
|
||||
<b-table-column v-slot="props" field="email" :label="$t('email')">
|
||||
{{ `${props.row.email}` }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" field="role" label="Role">
|
||||
{{ `${props.row.roleDetail.short}` }}
|
||||
<b-table-column v-slot="props" field="role" :label="$t('role')">
|
||||
{{ `${$t('roles.' + props.row.roleDetail.key)}` }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" field="createdAt" label="Created" sortable date>
|
||||
<b-table-column v-slot="props" field="createdAt" :label="$t('created')" sortable date>
|
||||
{{ formatDate(props.row.createdAt) }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props">
|
||||
<b-button type="is-success" v-if="props.row.isDisabled && props.row.roleDetail.long === 'USER'" @click="changeDisabledStatus(props.row.id, false)">Enable</b-button>
|
||||
<b-button type="is-danger" v-if="!props.row.isDisabled && props.row.roleDetail.long === 'USER'" @click="changeDisabledStatus(props.row.id, true)">Disable</b-button>
|
||||
<b-button v-if="props.row.isDisabled && props.row.roleDetail.key === 'USER'" type="is-success" @click="changeDisabledStatus(props.row.id, false)">{{ $t('enable') }}</b-button>
|
||||
<b-button v-if="!props.row.isDisabled && props.row.roleDetail.key === 'USER'" type="is-danger" @click="changeDisabledStatus(props.row.id, true)">{{ $t('disable') }}</b-button>
|
||||
</b-table-column>
|
||||
</b-table>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { parseAndFormatDate } from '@utils/format-date'
|
||||
import { mapState } from 'vuex'
|
||||
import { addDays, addMonths } from 'date-fns'
|
||||
import axios from 'axios'
|
||||
import currencyFormtter from 'currency-formatter'
|
||||
import currencyFormatter from 'currency-formatter'
|
||||
import store from '@state/store'
|
||||
import ShareVehicle from '@components/shareVehicle.vue'
|
||||
import MileageChart from '@components/mileageChart.vue'
|
||||
@@ -40,14 +40,20 @@ export default {
|
||||
stats: null,
|
||||
users: [],
|
||||
dateRangeOptions: [
|
||||
{ label: 'This week', value: 'this_week' },
|
||||
{ label: 'This month', value: 'this_month' },
|
||||
{ label: 'Past 30 days', value: 'past_30_days' },
|
||||
{ label: 'Past 3 months', value: 'past_3_months' },
|
||||
{ label: 'This year', value: 'this_year' },
|
||||
{ label: 'All Time', value: 'all_time' },
|
||||
{ label: this.$t('thisweek'), value: 'this_week' },
|
||||
{ label: this.$t('thismonth'), value: 'this_month' },
|
||||
{ label: this.$tc('pastxdays', 30), value: 'past_30_days' },
|
||||
{ label: this.$tc('pastxmonths', 3), value: 'past_3_months' },
|
||||
{ label: this.$t('thisyear'), value: 'this_year' },
|
||||
{ label: this.$t('alltime'), value: 'all_time' },
|
||||
],
|
||||
dateRangeOption: 'past_30_days',
|
||||
mileageOptions: [
|
||||
{ label: 'L/100km', value: 'litre_100km' },
|
||||
{ label: 'km/L', value: 'km_litre' },
|
||||
{ label: 'mpg', value: 'mpg' },
|
||||
],
|
||||
mileageOption: 'litre_100km',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -61,32 +67,35 @@ export default {
|
||||
return this.stats.map((x) => {
|
||||
return [
|
||||
{
|
||||
label: 'Currency',
|
||||
label: this.$t('currency'),
|
||||
value: x.currency,
|
||||
},
|
||||
{
|
||||
label: 'Total Expenditure',
|
||||
label: this.$t('totalexpenses'),
|
||||
value: this.formatCurrency(x.expenditureTotal, x.currency),
|
||||
},
|
||||
{
|
||||
label: 'Fillup Costs',
|
||||
label: this.$t('fillupcost'),
|
||||
value: `${this.formatCurrency(x.expenditureFillups, x.currency)} (${x.countFillups})`,
|
||||
},
|
||||
{
|
||||
label: 'Other Expenses',
|
||||
label: this.$t('otherexpenses'),
|
||||
value: `${this.formatCurrency(x.expenditureExpenses, x.currency)} (${x.countExpenses})`,
|
||||
},
|
||||
{
|
||||
label: 'Avg Fillup Expense',
|
||||
label: this.$t('avgfillupexpense'),
|
||||
value: `${this.formatCurrency(x.avgFillupCost, x.currency)}`,
|
||||
},
|
||||
{
|
||||
label: 'Avg Fillup Qty',
|
||||
value: `${x.avgFuelQty} ${this.vehicle.fuelUnitDetail.short}`,
|
||||
label: this.$t('avgfillupqty'),
|
||||
value: `${x.avgFuelQty} ${this.$t('unit.short.' + this.vehicle.fuelUnitDetail.key)}`,
|
||||
},
|
||||
{
|
||||
label: 'Avg Fuel Cost',
|
||||
value: `${this.formatCurrency(x.avgFuelPrice, x.currency)} per ${this.vehicle.fuelUnitDetail.short}`,
|
||||
label: this.$t('avgfuelcost'),
|
||||
value: this.$t('per', {
|
||||
0: this.formatCurrency(x.avgFuelPrice, x.currency),
|
||||
1: this.$t('unit.short.' + this.vehicle.fuelUnitDetail.key),
|
||||
}),
|
||||
},
|
||||
]
|
||||
})
|
||||
@@ -199,14 +208,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 +249,7 @@ export default {
|
||||
if (!currencyCode) {
|
||||
currencyCode = this.me.currency
|
||||
}
|
||||
return currencyFormtter.format(number, { code: currencyCode })
|
||||
return currencyFormatter.format(number, { code: currencyCode })
|
||||
},
|
||||
columnTdAttrs(row, column) {
|
||||
return null
|
||||
@@ -293,18 +309,18 @@ export default {
|
||||
<template>
|
||||
<Layout>
|
||||
<div class="columns box">
|
||||
<div class="column is-two-thirds" :class="isMobile ? 'has-text-centered' : ''">
|
||||
<div class="column is-one-half" :class="isMobile ? 'has-text-centered' : ''">
|
||||
<p class="title">{{ vehicle.nickname }} - {{ vehicle.registration }}</p>
|
||||
<p class="subtitle">
|
||||
{{ [vehicle.make, vehicle.model, vehicle.fuelTypeDetail.long].join(' | ') }}
|
||||
{{ [vehicle.make, vehicle.model, $t('fuel.' + vehicle.fuelTypeDetail.key)].join(' | ') }}
|
||||
|
||||
<template v-if="users.length > 1">
|
||||
| Shared with :
|
||||
| {{ $t('sharedwith') }} :
|
||||
{{
|
||||
users
|
||||
.map((x) => {
|
||||
if (x.userId === me.id) {
|
||||
return 'You'
|
||||
return $t('you')
|
||||
} else {
|
||||
return x.name
|
||||
}
|
||||
@@ -314,25 +330,24 @@ export default {
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-one-third buttons has-text-centered">
|
||||
<b-button type="is-primary" tag="router-link" :to="`/vehicles/${vehicle.id}/fillup`">Add Fillup</b-button>
|
||||
<b-button type="is-primary" tag="router-link" :to="`/vehicles/${vehicle.id}/expense`">Add Expense</b-button>
|
||||
<div :class="(!isMobile ? 'has-text-right ' : '') + 'column is-one-half buttons'">
|
||||
<b-button type="is-primary" tag="router-link" :to="`/vehicles/${vehicle.id}/fillup`">{{ $t('addfillup') }}</b-button>
|
||||
<b-button type="is-primary" tag="router-link" :to="`/vehicles/${vehicle.id}/expense`">{{ $t('addexpense') }}</b-button>
|
||||
<b-button
|
||||
v-if="vehicle.isOwner"
|
||||
tag="router-link"
|
||||
title="Edit Vehicle"
|
||||
:title="$t('editvehicle')"
|
||||
:to="{
|
||||
name: 'vehicle-edit',
|
||||
props: { vehicle: vehicle },
|
||||
params: { id: vehicle.id },
|
||||
}"
|
||||
>
|
||||
<b-icon pack="fas" icon="edit" type="is-info"> </b-icon
|
||||
></b-button>
|
||||
<b-button v-if="vehicle.isOwner" title="Share vehicle" @click="showShareVehicleModal">
|
||||
><b-icon pack="fas" icon="edit" type="is-info"> </b-icon>
|
||||
</b-button>
|
||||
<b-button v-if="vehicle.isOwner" :title="$t('sharevehicle')" @click="showShareVehicleModal">
|
||||
<b-icon pack="fas" icon="user-friends" type="is-info"> </b-icon
|
||||
></b-button>
|
||||
<b-button v-if="vehicle.isOwner" title="Delete Vehicle" @click="deleteVehicle">
|
||||
<b-button v-if="vehicle.isOwner" :title="$t('deletevehicle')" @click="deleteVehicle">
|
||||
<b-icon pack="fas" icon="trash" type="is-danger"> </b-icon
|
||||
></b-button>
|
||||
</div>
|
||||
@@ -346,45 +361,45 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<h1 class="title is-4">Past Fillups</h1>
|
||||
<h1 class="title is-4">{{ $t('pastfillups') }}</h1>
|
||||
|
||||
<b-table :data="fillups" hoverable mobile-cards :detailed="isMobile" detail-key="id" paginated per-page="10">
|
||||
<b-table-column v-slot="props" field="date" label="Date" :td-attrs="columnTdAttrs" sortable date>
|
||||
<b-table-column v-slot="props" field="date" :label="$t('date')" :td-attrs="columnTdAttrs" sortable date>
|
||||
{{ formatDate(props.row.date) }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" field="fuelSubType" label="Fuel Sub Type" :td-attrs="columnTdAttrs">
|
||||
<b-table-column v-slot="props" field="fuelSubType" :label="$t('fuelsubtype')" :td-attrs="columnTdAttrs">
|
||||
{{ props.row.fuelSubType }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" field="fuelQuantity" label="Qty." :td-attrs="hiddenMobile" numeric>
|
||||
{{ `${props.row.fuelQuantity} ${props.row.fuelUnitDetail.short}` }}
|
||||
<b-table-column v-slot="props" field="fuelQuantity" :label="$t('quantity')" :td-attrs="hiddenMobile" numeric>
|
||||
{{ `${props.row.fuelQuantity} ${$t('unit.short.' + props.row.fuelUnitDetail.key)}` }}
|
||||
</b-table-column>
|
||||
<b-table-column
|
||||
v-slot="props"
|
||||
field="perUnitPrice"
|
||||
:label="'Price per ' + vehicle.fuelUnitDetail.short"
|
||||
:label="$t('per', { '0': $t('price'), '1': $t('unit.short.' + vehicle.fuelUnitDetail.key) })"
|
||||
:td-attrs="hiddenMobile"
|
||||
numeric
|
||||
sortable
|
||||
>
|
||||
{{ `${formatCurrency(props.row.perUnitPrice, props.row.currency)}` }}
|
||||
</b-table-column>
|
||||
<b-table-column v-if="isMobile" v-slot="props" field="totalAmount" label="Total" :td-attrs="hiddenDesktop" sortable numeric>
|
||||
{{ `${me.currency} ${props.row.totalAmount}` }} ({{ `${props.row.fuelQuantity} ${props.row.fuelUnitDetail.short}` }} @
|
||||
<b-table-column v-if="isMobile" v-slot="props" field="totalAmount" :label="$t('total')" :td-attrs="hiddenDesktop" sortable numeric>
|
||||
{{ `${me.currency} ${props.row.totalAmount}` }} ({{ `${props.row.fuelQuantity} ${$t('unit.short.' + props.row.fuelUnitDetail.key)}` }} @
|
||||
{{ `${me.currency} ${props.row.perUnitPrice}` }})
|
||||
</b-table-column>
|
||||
<b-table-column v-if="!isMobile" v-slot="props" field="totalAmount" label="Total" :td-attrs="hiddenMobile" sortable numeric>
|
||||
<b-table-column v-if="!isMobile" v-slot="props" field="totalAmount" :label="$t('total')" :td-attrs="hiddenMobile" sortable numeric>
|
||||
{{ `${formatCurrency(props.row.totalAmount, props.row.currency)}` }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" width="20" field="isTankFull" label="Tank Full" :td-attrs="hiddenMobile">
|
||||
<b-table-column v-slot="props" width="20" field="isTankFull" :label="$t('fulltank')" :td-attrs="hiddenMobile">
|
||||
<b-icon pack="fas" :icon="props.row.isTankFull ? 'check' : 'times'" type="is-info"> </b-icon>
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" field="odoReading" label="Odometer Reading" :td-attrs="hiddenMobile" numeric>
|
||||
{{ `${props.row.odoReading} ${me.distanceUnitDetail.short}` }}
|
||||
<b-table-column v-slot="props" field="odoReading" :label="$t('odometer')" :td-attrs="hiddenMobile" numeric>
|
||||
{{ `${props.row.odoReading} ${$t('unit.short.' + me.distanceUnitDetail.key)}` }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" field="fillingStation" label="Fillup Station" :td-attrs="hiddenMobile">
|
||||
<b-table-column v-slot="props" field="fillingStation" :label="$t('gasstation')" :td-attrs="hiddenMobile">
|
||||
{{ `${props.row.fillingStation}` }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" field="userId" label="By" :td-attrs="hiddenMobile">
|
||||
<b-table-column v-slot="props" field="userId" :label="$t('by')" :td-attrs="hiddenMobile">
|
||||
{{ `${props.row.user.name}` }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props">
|
||||
@@ -399,11 +414,14 @@ export default {
|
||||
>
|
||||
<b-icon pack="fas" icon="edit" type="is-info"> </b-icon
|
||||
></b-button>
|
||||
<b-button type="is-ghost" title="Delete this fillup" @click="deleteFillup(props.row.id)">
|
||||
<b-button
|
||||
type="is-ghost"
|
||||
:title="$t('deletefillup')"
|
||||
@click="deleteFillup(props.row.id)">
|
||||
<b-icon pack="fas" icon="trash" type="is-danger"> </b-icon
|
||||
></b-button>
|
||||
</b-table-column>
|
||||
<template v-slot:empty> No Fillups so far</template>
|
||||
<template v-slot:empty> {{ $t('nofillups') }}</template>
|
||||
<template v-slot:detail="props">
|
||||
<p>{{ props.row.id }}</p>
|
||||
</template>
|
||||
@@ -411,25 +429,25 @@ export default {
|
||||
</div>
|
||||
<br />
|
||||
<div class="box">
|
||||
<h1 class="title is-4">Past Expenses</h1>
|
||||
<h1 class="title is-4">{{ $t('expenses') }}</h1>
|
||||
|
||||
<b-table :data="expenses" hoverable mobile-cards paginated per-page="10">
|
||||
<b-table-column v-slot="props" field="date" label="Date" :td-attrs="columnTdAttrs" date>
|
||||
<b-table-column v-slot="props" field="date" :label="$t('date')" :td-attrs="columnTdAttrs" date>
|
||||
{{ formatDate(props.row.date) }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" field="expenseType" label="Expense Type">
|
||||
<b-table-column v-slot="props" field="expenseType" :label="$t('expensetype')">
|
||||
{{ `${props.row.expenseType}` }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="amount" label="Total" :td-attrs="hiddenMobile" sortable numeric>
|
||||
<b-table-column v-slot="props" field="amount" :label="$t('total')" :td-attrs="hiddenMobile" sortable numeric>
|
||||
{{ `${formatCurrency(props.row.amount, props.row.currency)}` }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="odoReading" label="Odometer Reading" :td-attrs="columnTdAttrs" numeric>
|
||||
{{ `${props.row.odoReading} ${me.distanceUnitDetail.short}` }}
|
||||
<b-table-column v-slot="props" field="odoReading" :label="$t('odometer')" :td-attrs="columnTdAttrs" numeric>
|
||||
{{ `${props.row.odoReading} ${$t('unit.short.' + me.distanceUnitDetail.key)}` }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="userId" label="By" :td-attrs="columnTdAttrs">
|
||||
<b-table-column v-slot="props" field="userId" :label="$t('by')" :td-attrs="columnTdAttrs">
|
||||
{{ `${props.row.user.name}` }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props">
|
||||
@@ -444,20 +462,22 @@ export default {
|
||||
>
|
||||
<b-icon pack="fas" icon="edit" type="is-info"> </b-icon
|
||||
></b-button>
|
||||
<b-button type="is-ghost" title="Delete this expense" @click="deleteExpense(props.row.id)">
|
||||
<b-button type="is-ghost" :title="$t('deleteexpense')" @click="deleteExpense(props.row.id)">
|
||||
<b-icon pack="fas" icon="trash" type="is-danger"> </b-icon
|
||||
></b-button>
|
||||
</b-table-column>
|
||||
<template v-slot:empty> No Expenses so far</template>
|
||||
<template v-slot:empty> {{ $t('noexpenses') }}</template>
|
||||
</b-table>
|
||||
</div>
|
||||
<br />
|
||||
<div class="box">
|
||||
<div class="columns">
|
||||
<div class="column is-three-quarters"> <h1 class="title is-4">Attachments</h1></div>
|
||||
<div class="column is-three-quarters">
|
||||
<h1 class="title is-4">{{ $t('attachments') }}</h1></div
|
||||
>
|
||||
<div class="column buttons">
|
||||
<b-button type="is-primary" @click="showAttachmentForm = true">
|
||||
Add Attachment
|
||||
{{ $t('addattachment') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -471,7 +491,7 @@ export default {
|
||||
<b-upload v-model="file" class="file-label" required>
|
||||
<span class="file-cta">
|
||||
<b-icon class="file-icon" icon="upload"></b-icon>
|
||||
<span class="file-label">Choose File</span>
|
||||
<span class="file-label">{{ $t('choosefile') }}</span>
|
||||
</span>
|
||||
<span v-if="file" class="file-name" :class="isMobile ? 'file-name-mobile' : 'file-name-desktop'">
|
||||
{{ file.name }}
|
||||
@@ -479,18 +499,18 @@ export default {
|
||||
</b-upload>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-input v-model="title" required placeholder="Label for this file"></b-input>
|
||||
<b-input v-model="title" required :placeholder="$t('labelforfile')"></b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field class="buttons">
|
||||
<b-button tag="input" native-type="submit" :disabled="tryingToUpload" type="is-primary" label="Upload File" value="Upload File">
|
||||
<b-button tag="button" native-type="submit" :disabled="tryingToUpload" type="is-primary" label="Upload File" value="Upload File">
|
||||
</b-button>
|
||||
<b-button
|
||||
tag="input"
|
||||
tag="button"
|
||||
native-type="submit"
|
||||
:disabled="tryingToUpload"
|
||||
type="is-danger"
|
||||
label="Upload File"
|
||||
label="Cancel"
|
||||
value="Cancel"
|
||||
@click="showAttachmentForm = false"
|
||||
>
|
||||
@@ -503,33 +523,46 @@ export default {
|
||||
</div>
|
||||
|
||||
<b-table :data="attachments" hoverable mobile-cards>
|
||||
<b-table-column v-slot="props" field="title" label="Title" :td-attrs="columnTdAttrs">
|
||||
<b-table-column v-slot="props" field="title" :label="$t('title')" :td-attrs="columnTdAttrs">
|
||||
{{ `${props.row.title}` }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="originalName" label="Name" :td-attrs="columnTdAttrs">
|
||||
<b-table-column v-slot="props" field="originalName" :label="$t('name')" :td-attrs="columnTdAttrs">
|
||||
{{ `${props.row.originalName}` }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" field="id" label="Download" :td-attrs="columnTdAttrs">
|
||||
<b-table-column v-slot="props" field="id" :label="$t('download')" :td-attrs="columnTdAttrs">
|
||||
<b-button tag="a" :href="`/api/attachments/${props.row.id}/file?access_token=${currentUser.token}`" :download="props.row.originalName">
|
||||
<b-icon type="is-primary" icon="download"></b-icon>
|
||||
</b-button>
|
||||
</b-table-column>
|
||||
<template v-slot:empty> No Attachments so far</template>
|
||||
<template v-slot:empty> {{ $t('noattachments') }}</template>
|
||||
</b-table>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="columns">
|
||||
<div class="column" :class="isMobile ? 'has-text-centered' : ''"> <h1 class="title">Stats</h1></div>
|
||||
<div class="column">
|
||||
<b-select v-model="dateRangeOption" class="is-pulled-right is-medium">
|
||||
<option v-for="option in dateRangeOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</b-select></div
|
||||
<div class="column" :class="isMobile ? 'has-text-centered' : ''">
|
||||
<h1 class="title">{{ $t('statistics') }}</h1></div
|
||||
>
|
||||
<div class="column">
|
||||
<div class="columns is-pulled-right is-medium">
|
||||
<div class="column">
|
||||
<b-select v-model="mileageOption">
|
||||
<option v-for="option in mileageOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</b-select>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-select v-model="dateRangeOption">
|
||||
<option v-for="option in dateRangeOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</b-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MileageChart :vehicle="vehicle" :since="getStartDate()" :user="me" :height="300" />
|
||||
<MileageChart :vehicle="vehicle" :since="getStartDate()" :user="me" :height="300" :mileage-option="mileageOption" />
|
||||
</div>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
14272
ui/yarn.lock
14272
ui/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user