Compare commits
31 Commits
Author | SHA1 | Date | |
---|---|---|---|
1a79177422 | |||
0aa877d7d4 | |||
87a70ee5fa | |||
0a030eaee1 | |||
d11c0036b5 | |||
ca93e9cd55 | |||
a061ffd350 | |||
5633c029ac | |||
a97d050ead | |||
958929fd16 | |||
a61d80ee1f | |||
41c5095b8b | |||
c074dfe865 | |||
fa8a2854f2 | |||
15bb73de30 | |||
e506510fde | |||
11ac8758da | |||
3db5e1e72c | |||
4e2a783b2e | |||
bb83563bc6 | |||
0a21c59eff | |||
3308b58524 | |||
941b642f39 | |||
6a77c71df4 | |||
bf20914c1c | |||
7874ef69a2 | |||
2e719b590e | |||
95d8e4fccc | |||
7cf106eb85 | |||
148fc18cd8 | |||
47095ae6ec |
@ -7,4 +7,5 @@ config.example.json
|
||||
.gitignore
|
||||
.vscode/
|
||||
budgeteer
|
||||
budgeteer.exe
|
||||
budgeteer.exe
|
||||
**/node_modules/
|
||||
|
@ -6,8 +6,9 @@ name: budgeteer
|
||||
steps:
|
||||
- name: Taskfile.dev
|
||||
image: hub.javil.eu/budgeteer:dev
|
||||
pull: true
|
||||
commands:
|
||||
- task build
|
||||
- task
|
||||
|
||||
- name: docker
|
||||
image: plugins/docker
|
||||
|
23
.woodpecker.yml
Normal file
23
.woodpecker.yml
Normal file
@ -0,0 +1,23 @@
|
||||
pipeline:
|
||||
build:
|
||||
name: Taskfile.dev
|
||||
image: hub.javil.eu/budgeteer:dev
|
||||
pull: true
|
||||
commands:
|
||||
- task
|
||||
|
||||
docker:
|
||||
image: plugins/docker
|
||||
secrets: [ docker_username, docker_password ]
|
||||
settings:
|
||||
registry: hub.javil.eu
|
||||
repo: hub.javil.eu/budgeteer
|
||||
context: build
|
||||
dockerfile: build/Dockerfile
|
||||
tags:
|
||||
- latest
|
||||
when:
|
||||
event: [push, tag, deployment]
|
||||
|
||||
image_pull_secrets:
|
||||
- hub.javil.eu
|
@ -12,7 +12,7 @@ docker:
|
||||
WORKDIR /app
|
||||
COPY +build/budgeteer .
|
||||
ENTRYPOINT ["/app/budgeteer"]
|
||||
SAVE IMAGE budgeteer:latest
|
||||
SAVE IMAGE hub.javil.eu/budgeteer:latest
|
||||
|
||||
run:
|
||||
LOCALLY
|
||||
|
20
Taskfile.yml
20
Taskfile.yml
@ -1,9 +1,12 @@
|
||||
version: '3'
|
||||
|
||||
vars:
|
||||
IMAGE_NAME: hub.javil.eu/budgeteer
|
||||
|
||||
tasks:
|
||||
default:
|
||||
cmds:
|
||||
- task: build
|
||||
- task: build-prod
|
||||
|
||||
sqlc:
|
||||
desc: sqlc code generation
|
||||
@ -59,12 +62,13 @@ tasks:
|
||||
|
||||
frontend:
|
||||
desc: Build vue frontend
|
||||
dir: web
|
||||
sources:
|
||||
- web/src/**/*
|
||||
generates:
|
||||
- web/dist/**/*
|
||||
cmds:
|
||||
- cd web
|
||||
- yarn
|
||||
- yarn build
|
||||
|
||||
docker:
|
||||
@ -72,8 +76,18 @@ tasks:
|
||||
deps: [build-prod]
|
||||
sources:
|
||||
- ./build/budgeteer{{exeExt}}
|
||||
- ./build/Dockerfile
|
||||
cmds:
|
||||
- docker build -t budgeteer:latest -t hub.javil.eu/budgeteer:latest ./build
|
||||
- docker build -t {{.IMAGE_NAME}}:latest ./build
|
||||
- docker push {{.IMAGE_NAME}}:latest
|
||||
|
||||
dev-docker:
|
||||
desc: Build budgeeter:dev
|
||||
sources:
|
||||
- ./docker/Dockerfile
|
||||
cmds:
|
||||
- docker build -t {{.IMAGE_NAME}}:dev . -f docker/Dockerfile
|
||||
- docker push {{.IMAGE_NAME}}:dev
|
||||
|
||||
run:
|
||||
desc: Start budgeteer
|
||||
|
@ -2,9 +2,7 @@ version: '3.7'
|
||||
|
||||
services:
|
||||
app:
|
||||
image: budgeteer:dev
|
||||
build:
|
||||
context: ./docker/
|
||||
image: hub.javil.eu/budgeteer:dev
|
||||
container_name: budgeteer
|
||||
stdin_open: true # docker run -i
|
||||
tty: true # docker run -t
|
||||
|
@ -2,7 +2,7 @@ version: '3.7'
|
||||
|
||||
services:
|
||||
app:
|
||||
image: budgeteer:latest
|
||||
image: hub.javil.eu/budgeteer:latest
|
||||
container_name: budgeteer
|
||||
ports:
|
||||
- 1323:1323
|
||||
|
@ -1,13 +1,16 @@
|
||||
FROM alpine as godeps
|
||||
RUN apk add go
|
||||
RUN go install github.com/kyleconroy/sqlc/cmd/sqlc@latest
|
||||
RUN go install github.com/go-task/task/v3/cmd/task@latest
|
||||
|
||||
FROM alpine
|
||||
RUN apk add go
|
||||
RUN apk add nodejs yarn bash curl git git-perl tmux
|
||||
RUN bash -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin
|
||||
ADD build.sh /
|
||||
RUN addgroup -S dev && adduser -S dev -G dev
|
||||
USER dev
|
||||
RUN go install github.com/kyleconroy/sqlc/cmd/sqlc@latest
|
||||
RUN go install github.com/go-task/task/v3/cmd/task@latest
|
||||
ADD docker/build.sh /
|
||||
COPY --from=godeps /root/go/bin/task /root/go/bin/sqlc /usr/local/bin/
|
||||
RUN yarn global add @vue/cli
|
||||
ENV PATH="/home/dev/go/bin:/home/dev/.yarn/bin/:${PATH}"
|
||||
ENV PATH="/root/.yarn/bin/:${PATH}"
|
||||
WORKDIR /src
|
||||
ADD web/package.json /src/web/
|
||||
RUN yarn
|
||||
CMD /build.sh
|
||||
|
41
http/http.go
41
http/http.go
@ -1,8 +1,11 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"git.javil.eu/jacob1123/budgeteer"
|
||||
@ -18,6 +21,7 @@ type Handler struct {
|
||||
Service *postgres.Database
|
||||
TokenVerifier budgeteer.TokenVerifier
|
||||
CredentialsVerifier *bcrypt.Verifier
|
||||
StaticFS http.FileSystem
|
||||
}
|
||||
|
||||
const (
|
||||
@ -37,14 +41,10 @@ func (h *Handler) LoadRoutes(router *gin.Engine) {
|
||||
if err != nil {
|
||||
panic("couldn't open static files")
|
||||
}
|
||||
staticFS := http.FS(static)
|
||||
h.StaticFS = http.FS(static)
|
||||
|
||||
router.Use(enableCachingForStaticFiles())
|
||||
router.NoRoute(
|
||||
func(c *gin.Context) {
|
||||
c.FileFromFS(c.Request.URL.Path, staticFS)
|
||||
},
|
||||
)
|
||||
router.NoRoute(h.ServeStatic)
|
||||
|
||||
withLogin := router.Group("")
|
||||
withLogin.Use(h.verifyLoginWithRedirect)
|
||||
@ -81,6 +81,35 @@ func (h *Handler) LoadRoutes(router *gin.Engine) {
|
||||
transaction.POST("/new", h.newTransaction)
|
||||
transaction.POST("/:transactionid", h.newTransaction)
|
||||
}
|
||||
func (h *Handler) ServeStatic(c *gin.Context) {
|
||||
h.ServeStaticFile(c, c.Request.URL.Path)
|
||||
}
|
||||
|
||||
func (h *Handler) ServeStaticFile(c *gin.Context, fullPath string) {
|
||||
file, err := h.StaticFS.Open(fullPath)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
h.ServeStaticFile(c, path.Join("/", "/index.html"))
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if stat.IsDir() {
|
||||
h.ServeStaticFile(c, path.Join(fullPath, "index.html"))
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeContent(c.Writer, c.Request, stat.Name(), stat.ModTime(), file.(io.ReadSeeker))
|
||||
}
|
||||
|
||||
func enableCachingForStaticFiles() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
26
web/src/api.ts
Normal file
26
web/src/api.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { useSessionStore } from "./stores/session";
|
||||
|
||||
export const BASE_URL = "/api/v1"
|
||||
|
||||
export function GET(path: string) {
|
||||
const sessionStore = useSessionStore();
|
||||
return fetch(BASE_URL + path, {
|
||||
headers: sessionStore.AuthHeaders,
|
||||
})
|
||||
};
|
||||
|
||||
export function POST(path: string, body: FormData | string | null) {
|
||||
const sessionStore = useSessionStore();
|
||||
return fetch(BASE_URL + path, {
|
||||
method: "POST",
|
||||
headers: sessionStore.AuthHeaders,
|
||||
body: body,
|
||||
})
|
||||
}
|
||||
export function DELETE(path: string) {
|
||||
const sessionStore = useSessionStore();
|
||||
return fetch(BASE_URL + path, {
|
||||
method: "DELETE",
|
||||
headers: sessionStore.AuthHeaders,
|
||||
})
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue"
|
||||
import { useAPI } from "../stores/api";
|
||||
import { GET } from "../api";
|
||||
import { useBudgetsStore } from "../stores/budget";
|
||||
|
||||
export interface Suggestion {
|
||||
@ -42,9 +42,8 @@ export default defineComponent({
|
||||
return;
|
||||
}
|
||||
|
||||
const api = useAPI();
|
||||
const budgetStore = useBudgetsStore();
|
||||
api.GET("/budget/" + budgetStore.CurrentBudgetID + "/autocomplete/" + this.type + "?s=" + text)
|
||||
GET("/budget/" + budgetStore.CurrentBudgetID + "/autocomplete/" + this.type + "?s=" + text)
|
||||
.then(x=>x.json())
|
||||
.then(x => {
|
||||
let suggestions = x || [];
|
||||
|
@ -1,89 +1,89 @@
|
||||
<script lang="ts">
|
||||
import { mapState } from "pinia";
|
||||
import { defineComponent } from "vue"
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from "vue"
|
||||
import Autocomplete, { Suggestion } from '../components/Autocomplete.vue'
|
||||
import Currency from "../components/Currency.vue";
|
||||
import TransactionRow from "../components/TransactionRow.vue";
|
||||
import { useAPI } from "../stores/api";
|
||||
import { POST } from "../api";
|
||||
import { useAccountStore } from "../stores/budget-account";
|
||||
import { useSessionStore } from "../stores/session";
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
TransactionDate: new Date().toISOString().substring(0, 10),
|
||||
Payee: undefined as Suggestion | undefined,
|
||||
Category: undefined as Suggestion | undefined,
|
||||
Memo: "",
|
||||
Amount: 0
|
||||
}
|
||||
},
|
||||
components: { Autocomplete, Currency, TransactionRow },
|
||||
props: ["budgetid", "accountid"],
|
||||
computed: {
|
||||
...mapState(useAccountStore, ["CurrentAccount", "TransactionsList"]),
|
||||
},
|
||||
methods: {
|
||||
saveTransaction(e : MouseEvent) {
|
||||
e.preventDefault();
|
||||
const api = useAPI();
|
||||
api.POST("/transaction/new", JSON.stringify({
|
||||
budget_id: this.budgetid,
|
||||
account_id: this.accountid,
|
||||
date: this.$data.TransactionDate,
|
||||
payee: this.$data.Payee,
|
||||
category: this.$data.Category,
|
||||
memo: this.$data.Memo,
|
||||
amount: this.$data.Amount,
|
||||
state: "Uncleared"
|
||||
}))
|
||||
.then(x => x.json());
|
||||
},
|
||||
}
|
||||
})
|
||||
const props = defineProps<{
|
||||
budgetid: string
|
||||
accountid: string
|
||||
}>()
|
||||
|
||||
const TransactionDate = ref(new Date().toISOString().substring(0, 10));
|
||||
const Payee = ref<Suggestion | undefined>(undefined);
|
||||
const Category = ref<Suggestion | undefined>(undefined);
|
||||
const Memo = ref("");
|
||||
const Amount = ref(0);
|
||||
|
||||
function saveTransaction(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
POST("/transaction/new", JSON.stringify({
|
||||
budget_id: props.budgetid,
|
||||
account_id: props.accountid,
|
||||
date: TransactionDate.value,
|
||||
payee: Payee.value,
|
||||
category: Category.value,
|
||||
memo: Memo.value,
|
||||
amount: Amount,
|
||||
state: "Uncleared"
|
||||
}))
|
||||
.then(x => x.json());
|
||||
}
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const CurrentAccount = accountStore.CurrentAccount;
|
||||
const TransactionsList = accountStore.TransactionsList;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ CurrentAccount?.Name }}</h1>
|
||||
<p>
|
||||
Current Balance:
|
||||
<Currency :value="CurrentAccount?.Balance" />
|
||||
</p>
|
||||
<table>
|
||||
<tr class="font-bold">
|
||||
<td style="width: 90px;">Date</td>
|
||||
<td style="max-width: 150px;">Payee</td>
|
||||
<td style="max-width: 200px;">Category</td>
|
||||
<td>Memo</td>
|
||||
<td class="text-right">Amount</td>
|
||||
<td style="width: 20px;"></td>
|
||||
<td style="width: 20px;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width: 90px;" class="text-sm">
|
||||
<input class="border-b-2 border-black" type="date" v-model="TransactionDate" />
|
||||
</td>
|
||||
<td style="max-width: 150px;">
|
||||
<Autocomplete v-model="Payee" type="payees" />
|
||||
</td>
|
||||
<td style="max-width: 200px;">
|
||||
<Autocomplete v-model="Category" type="categories" />
|
||||
</td>
|
||||
<td>
|
||||
<input class="block w-full border-b-2 border-black" type="text" v-model="Memo" />
|
||||
</td>
|
||||
<td style="width: 80px;" class="text-right">
|
||||
<input class="text-right block w-full border-b-2 border-black" type="currency" v-model="Amount" />
|
||||
</td>
|
||||
<td style="width: 20px;">
|
||||
<input type="submit" @click="saveTransaction" value="Save" />
|
||||
</td>
|
||||
<td style="width: 20px;"></td>
|
||||
</tr>
|
||||
<TransactionRow v-for="(transaction, index) in TransactionsList"
|
||||
:transaction="transaction"
|
||||
:index="index" />
|
||||
</table>
|
||||
<h1>{{ CurrentAccount?.Name }}</h1>
|
||||
<p>
|
||||
Current Balance:
|
||||
<Currency :value="CurrentAccount?.Balance" />
|
||||
</p>
|
||||
<table>
|
||||
<tr class="font-bold">
|
||||
<td style="width: 90px;">Date</td>
|
||||
<td style="max-width: 150px;">Payee</td>
|
||||
<td style="max-width: 200px;">Category</td>
|
||||
<td>Memo</td>
|
||||
<td class="text-right">Amount</td>
|
||||
<td style="width: 20px;"></td>
|
||||
<td style="width: 20px;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width: 90px;" class="text-sm">
|
||||
<input class="border-b-2 border-black" type="date" v-model="TransactionDate" />
|
||||
</td>
|
||||
<td style="max-width: 150px;">
|
||||
<Autocomplete v-model="Payee" type="payees" />
|
||||
</td>
|
||||
<td style="max-width: 200px;">
|
||||
<Autocomplete v-model="Category" type="categories" />
|
||||
</td>
|
||||
<td>
|
||||
<input class="block w-full border-b-2 border-black" type="text" v-model="Memo" />
|
||||
</td>
|
||||
<td style="width: 80px;" class="text-right">
|
||||
<input
|
||||
class="text-right block w-full border-b-2 border-black"
|
||||
type="currency"
|
||||
v-model="Amount"
|
||||
/>
|
||||
</td>
|
||||
<td style="width: 20px;">
|
||||
<input type="submit" @click="saveTransaction" value="Save" />
|
||||
</td>
|
||||
<td style="width: 20px;"></td>
|
||||
</tr>
|
||||
<TransactionRow
|
||||
v-for="(transaction, index) in TransactionsList"
|
||||
:transaction="transaction"
|
||||
:index="index"
|
||||
/>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
15
web/src/pages/Admin.vue
Normal file
15
web/src/pages/Admin.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
onMounted(() => {
|
||||
document.title = "Budgeteer - Admin";
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>Danger Zone</h1>
|
||||
<div class="budget-item">
|
||||
<button>Clear database</button>
|
||||
<p>This removes all data and starts from scratch. Not undoable!</p>
|
||||
</div>
|
||||
</template>
|
@ -1,20 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { mapState } from "pinia"
|
||||
import { defineComponent } from "vue"
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import Currency from "../components/Currency.vue"
|
||||
import { useBudgetsStore } from "../stores/budget"
|
||||
import { useAccountStore } from "../stores/budget-account"
|
||||
import { useSettingsStore } from "../stores/settings"
|
||||
|
||||
export default defineComponent({
|
||||
props: ["budgetid", "accountid"],
|
||||
components: { Currency },
|
||||
computed: {
|
||||
...mapState(useSettingsStore, ["ExpandMenu"]),
|
||||
...mapState(useBudgetsStore, ["CurrentBudgetName", "CurrentBudgetID"]),
|
||||
...mapState(useAccountStore, ["OnBudgetAccounts", "OnBudgetAccountsBalance", "OffBudgetAccounts", "OffBudgetAccountsBalance"])
|
||||
}
|
||||
})
|
||||
const props = defineProps<{
|
||||
budgetid: string,
|
||||
accountid: string,
|
||||
}>();
|
||||
|
||||
const ExpandMenu = computed(() => useSettingsStore().Menu.Expand);
|
||||
|
||||
const budgetStore = useBudgetsStore();
|
||||
const CurrentBudgetName = computed(() => budgetStore.CurrentBudgetName);
|
||||
const CurrentBudgetID = computed(() => budgetStore.CurrentBudgetID);
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const OnBudgetAccounts = computed(() => accountStore.OnBudgetAccounts);
|
||||
const OffBudgetAccounts = computed(() => accountStore.OffBudgetAccounts);
|
||||
const OnBudgetAccountsBalance = computed(() => accountStore.OnBudgetAccountsBalance);
|
||||
const OffBudgetAccountsBalance = computed(() => accountStore.OffBudgetAccountsBalance);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -1,68 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { mapState } from "pinia";
|
||||
import { defineComponent, PropType } from "vue";
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineProps, onMounted, PropType, watch, watchEffect } from "vue";
|
||||
import Currency from "../components/Currency.vue";
|
||||
import { useBudgetsStore } from "../stores/budget";
|
||||
import { Category, useAccountStore } from "../stores/budget-account";
|
||||
import { useAccountStore } from "../stores/budget-account";
|
||||
|
||||
interface Date {
|
||||
Year: number,
|
||||
Month: number,
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
budgetid: {} as PropType<string>,
|
||||
year: {} as PropType<number>,
|
||||
month: {} as PropType<number>,
|
||||
},
|
||||
computed: {
|
||||
...mapState(useBudgetsStore, ["CurrentBudgetID"]),
|
||||
Categories() : Category[] {
|
||||
const accountStore = useAccountStore();
|
||||
return [...accountStore.CategoriesForMonth(this.selected.Year, this.selected.Month)];
|
||||
},
|
||||
previous() : Date {
|
||||
return {
|
||||
Year: new Date(this.selected.Year, this.selected.Month - 1, 1).getFullYear(),
|
||||
Month: new Date(this.selected.Year, this.selected.Month - 1, 1).getMonth(),
|
||||
};
|
||||
},
|
||||
current() : Date {
|
||||
return {
|
||||
Year: new Date().getFullYear(),
|
||||
Month: new Date().getMonth(),
|
||||
};
|
||||
},
|
||||
selected() : Date {
|
||||
return {
|
||||
Year: this.year ?? this.current.Year,
|
||||
Month: Number(this.month ?? this.current.Month) + 1
|
||||
}
|
||||
},
|
||||
next() : Date {
|
||||
return {
|
||||
Year: new Date(this.selected.Year, Number(this.month) + 1, 1).getFullYear(),
|
||||
Month: new Date(this.selected.Year, Number(this.month) + 1, 1).getMonth(),
|
||||
};
|
||||
}
|
||||
},
|
||||
mounted() : Promise<void> {
|
||||
document.title = "Budgeteer - Budget for " + this.selected.Month + "/" + this.selected.Year;
|
||||
return useAccountStore().FetchMonthBudget(this.budgetid ?? "", this.selected.Year, this.selected.Month);
|
||||
},
|
||||
watch: {
|
||||
year() {
|
||||
if (this.year != undefined && this.month != undefined)
|
||||
return useAccountStore().FetchMonthBudget(this.budgetid ?? "", this.year, this.month);
|
||||
},
|
||||
month() {
|
||||
if (this.year != undefined && this.month != undefined)
|
||||
return useAccountStore().FetchMonthBudget(this.budgetid ?? "", this.year, this.month);
|
||||
},
|
||||
},
|
||||
components: { Currency }
|
||||
})
|
||||
const props = defineProps<{
|
||||
budgetid: string,
|
||||
year: string,
|
||||
month: string,
|
||||
}>()
|
||||
|
||||
const budgetsStore = useBudgetsStore();
|
||||
const CurrentBudgetID = computed(() => budgetsStore.CurrentBudgetID);
|
||||
|
||||
const categoriesForMonth = useAccountStore().CategoriesForMonth;
|
||||
const Categories = computed(() => {
|
||||
return [...categoriesForMonth(selected.value.Year, selected.value.Month)];
|
||||
});
|
||||
const previous = computed(() => ({
|
||||
Year: new Date(selected.value.Year, selected.value.Month - 1, 1).getFullYear(),
|
||||
Month: new Date(selected.value.Year, selected.value.Month - 1, 1).getMonth(),
|
||||
}));
|
||||
const current = computed(() => ({
|
||||
Year: new Date().getFullYear(),
|
||||
Month: new Date().getMonth(),
|
||||
}));
|
||||
const selected = computed(() => ({
|
||||
Year: Number(props.year) ?? current.value.Year,
|
||||
Month: Number(props.month ?? current.value.Month)
|
||||
}));
|
||||
const next = computed(() => ({
|
||||
Year: new Date(selected.value.Year, Number(props.month) + 1, 1).getFullYear(),
|
||||
Month: new Date(selected.value.Year, Number(props.month) + 1, 1).getMonth(),
|
||||
}));
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.year != undefined && props.month != undefined)
|
||||
return useAccountStore().FetchMonthBudget(props.budgetid ?? "", Number(props.year), Number(props.month));
|
||||
});
|
||||
|
||||
/*{{define "title"}}
|
||||
{{printf "Budget for %s %d" .Date.Month .Date.Year}}
|
||||
@ -70,7 +50,7 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>Budget for {{ selected.Month }}/{{ selected.Year }}</h1>
|
||||
<h1>Budget for {{ selected.Month + 1 }}/{{ selected.Year }}</h1>
|
||||
<div>
|
||||
<router-link
|
||||
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + previous.Year + '/' + previous.Month"
|
||||
|
@ -1,17 +1,13 @@
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import NewBudget from '../dialogs/NewBudget.vue';
|
||||
import Card from '../components/Card.vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { mapState } from 'pinia';
|
||||
import { useSessionStore } from '../stores/session';
|
||||
|
||||
export default defineComponent({
|
||||
props: ["budgetid"],
|
||||
components: { NewBudget, Card },
|
||||
computed: {
|
||||
...mapState(useSessionStore, ["BudgetsList"]),
|
||||
}
|
||||
})
|
||||
const props = defineProps<{
|
||||
budgetid: string,
|
||||
}>();
|
||||
|
||||
const BudgetsList = useSessionStore().BudgetsList;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -1,9 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
|
||||
})
|
||||
<script lang="ts" setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -1,46 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useSessionStore } from "../stores/session";
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
error: "",
|
||||
login: {
|
||||
user: "",
|
||||
password: ""
|
||||
},
|
||||
showPassword: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.title = "Budgeteer - Login";
|
||||
},
|
||||
methods: {
|
||||
formSubmit(e : MouseEvent) {
|
||||
e.preventDefault();
|
||||
useSessionStore().login(this.$data.login)
|
||||
.then(x => {
|
||||
this.$data.error = "";
|
||||
this.$router.replace("/dashboard");
|
||||
})
|
||||
.catch(x => this.$data.error = "The entered credentials are invalid!");
|
||||
const error = ref("");
|
||||
const login = ref({ user: "", password: "" });
|
||||
|
||||
// TODO display invalidCredentials
|
||||
// TODO redirect to dashboard on success
|
||||
}
|
||||
}
|
||||
})
|
||||
onMounted(() => {
|
||||
document.title = "Budgeteer - Login";
|
||||
});
|
||||
|
||||
function formSubmit(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
useSessionStore().login(login)
|
||||
.then(x => {
|
||||
error.value = "";
|
||||
useRouter().replace("/dashboard");
|
||||
})
|
||||
.catch(x => error.value = "The entered credentials are invalid!");
|
||||
|
||||
// TODO display invalidCredentials
|
||||
// TODO redirect to dashboard on success
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<input type="text" v-model="login.user" placeholder="Username" class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
|
||||
<input type="password" v-model="login.password" placeholder="Password" class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
|
||||
</div>
|
||||
<div>{{ error }}</div>
|
||||
<button type="submit" @click="formSubmit" class="bg-blue-300 rounded-lg p-2 w-48">Login</button>
|
||||
<p>
|
||||
New user? <router-link to="/register">Register</router-link> instead!
|
||||
</p>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
v-model="login.user"
|
||||
placeholder="Username"
|
||||
class="border-2 border-black rounded-lg block px-2 my-2 w-48"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
v-model="login.password"
|
||||
placeholder="Password"
|
||||
class="border-2 border-black rounded-lg block px-2 my-2 w-48"
|
||||
/>
|
||||
</div>
|
||||
<div>{{ error }}</div>
|
||||
<button type="submit" @click="formSubmit" class="bg-blue-300 rounded-lg p-2 w-48">Login</button>
|
||||
<p>
|
||||
New user?
|
||||
<router-link to="/register">Register</router-link>instead!
|
||||
</p>
|
||||
</template>
|
@ -1,31 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { useSessionStore } from '../stores/session';
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
showPassword: false,
|
||||
error: "",
|
||||
login: {
|
||||
email: "",
|
||||
password: "",
|
||||
name: "",
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formSubmit (e : FormDataEvent) {
|
||||
e.preventDefault();
|
||||
useSessionStore().register(this.$data.login)
|
||||
.then(() => this.$data.error = "")
|
||||
.catch(() => this.$data.error = "Something went wrong!");
|
||||
const error = ref("");
|
||||
const login = ref({ email: "", password: "", name: "" });
|
||||
const showPassword = ref(false);
|
||||
|
||||
// TODO display invalidCredentials
|
||||
// TODO redirect to dashboard on success
|
||||
}
|
||||
}
|
||||
})
|
||||
function formSubmit(e: FormDataEvent) {
|
||||
e.preventDefault();
|
||||
useSessionStore().register(login)
|
||||
.then(() => error.value = "")
|
||||
.catch(() => error.value = "Something went wrong!");
|
||||
|
||||
// TODO display invalidCredentials
|
||||
// TODO redirect to dashboard on success
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -38,30 +27,35 @@ export default defineComponent({
|
||||
<v-text-field v-model="login.name" type="text" label="Name" />
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-text-field v-model="login.password" label="Password"
|
||||
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
<v-text-field
|
||||
v-model="login.password"
|
||||
label="Password"
|
||||
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
@click:append="showPassword = showPassword"
|
||||
:error-message="error"
|
||||
error-count="2"
|
||||
error />
|
||||
error
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-text-field v-model="login.password" label="Repeat password"
|
||||
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
<v-text-field
|
||||
v-model="login.password"
|
||||
label="Repeat password"
|
||||
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
@click:append="showPassword = showPassword"
|
||||
:error-message="error"
|
||||
error-count="2"
|
||||
error />
|
||||
error
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div class="form-group">
|
||||
{{ error }}
|
||||
</div>
|
||||
<div class="form-group">{{ error }}</div>
|
||||
<v-btn type="submit" @click="formSubmit">Register</v-btn>
|
||||
<p>
|
||||
Existing user? <router-link to="/login">Login</router-link> instead!
|
||||
</p>
|
||||
<p>
|
||||
Existing user?
|
||||
<router-link to="/login">Login</router-link>instead!
|
||||
</p>
|
||||
</v-container>
|
||||
</template>
|
||||
|
@ -1,67 +1,56 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue"
|
||||
import { useAPI } from "../stores/api";
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineComponent, onMounted, ref } from "vue"
|
||||
import { useRouter } from "vue-router";
|
||||
import { DELETE, POST } from "../api";
|
||||
import { useBudgetsStore } from "../stores/budget";
|
||||
import { useSessionStore } from "../stores/session";
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
transactionsFile: undefined as File | undefined,
|
||||
assignmentsFile: undefined as File | undefined
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filesIncomplete() : boolean {
|
||||
return this.$data.transactionsFile == undefined || this.$data.assignmentsFile == undefined;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.title = "Budgeteer - Settings";
|
||||
},
|
||||
methods: {
|
||||
gotAssignments(e : Event) {
|
||||
const input = (<HTMLInputElement>e.target);
|
||||
if(input.files != null)
|
||||
this.$data.assignmentsFile = input.files[0];
|
||||
},
|
||||
gotTransactions(e : Event) {
|
||||
const input = (<HTMLInputElement>e.target);
|
||||
if(input.files != null)
|
||||
this.$data.transactionsFile = input.files[0];
|
||||
},
|
||||
deleteBudget() {
|
||||
const currentBudgetID = useBudgetsStore().CurrentBudgetID;
|
||||
if (currentBudgetID == null)
|
||||
return;
|
||||
const transactionsFile = ref<File | undefined>(undefined);
|
||||
const assignmentsFile = ref<File | undefined>(undefined);
|
||||
|
||||
const api = useAPI();
|
||||
api.DELETE("/budget/" + currentBudgetID);
|
||||
const filesIncomplete = computed(() => transactionsFile.value == undefined || assignmentsFile.value == undefined);
|
||||
onMounted(() => {
|
||||
document.title = "Budgeteer - Settings";
|
||||
});
|
||||
|
||||
const budgetStore = useSessionStore();
|
||||
budgetStore.Budgets.delete(currentBudgetID);
|
||||
this.$router.push("/")
|
||||
},
|
||||
clearBudget() {
|
||||
const currentBudgetID = useBudgetsStore().CurrentBudgetID;
|
||||
const api = useAPI();
|
||||
api.POST("/budget/" + currentBudgetID + "/settings/clear", null)
|
||||
},
|
||||
cleanNegative() {
|
||||
// <a href="/budget/{{.Budget.ID}}/settings/clean-negative">Fix all historic negative category-balances</a>
|
||||
},
|
||||
ynabImport() {
|
||||
if (this.$data.transactionsFile == undefined || this.$data.assignmentsFile == undefined)
|
||||
return
|
||||
function gotAssignments(e: Event) {
|
||||
const input = (<HTMLInputElement>e.target);
|
||||
if (input.files != null)
|
||||
assignmentsFile.value = input.files[0];
|
||||
}
|
||||
function gotTransactions(e: Event) {
|
||||
const input = (<HTMLInputElement>e.target);
|
||||
if (input.files != null)
|
||||
transactionsFile.value = input.files[0];
|
||||
};
|
||||
function deleteBudget() {
|
||||
const currentBudgetID = useBudgetsStore().CurrentBudgetID;
|
||||
if (currentBudgetID == null)
|
||||
return;
|
||||
|
||||
let formData = new FormData();
|
||||
formData.append("transactions", this.$data.transactionsFile);
|
||||
formData.append("assignments", this.$data.assignmentsFile);
|
||||
const budgetStore = useBudgetsStore();
|
||||
budgetStore.ImportYNAB(formData);
|
||||
}
|
||||
}
|
||||
})
|
||||
DELETE("/budget/" + currentBudgetID);
|
||||
|
||||
const budgetStore = useSessionStore();
|
||||
budgetStore.Budgets.delete(currentBudgetID);
|
||||
useRouter().push("/")
|
||||
};
|
||||
function clearBudget() {
|
||||
const currentBudgetID = useBudgetsStore().CurrentBudgetID;
|
||||
POST("/budget/" + currentBudgetID + "/settings/clear", null)
|
||||
};
|
||||
function cleanNegative() {
|
||||
// <a href="/budget/{{.Budget.ID}}/settings/clean-negative">Fix all historic negative category-balances</a>
|
||||
};
|
||||
function ynabImport() {
|
||||
if (transactionsFile.value == undefined || assignmentsFile.value == undefined)
|
||||
return
|
||||
|
||||
let formData = new FormData();
|
||||
formData.append("transactions", transactionsFile.value);
|
||||
formData.append("assignments", assignmentsFile.value);
|
||||
const budgetStore = useBudgetsStore();
|
||||
budgetStore.ImportYNAB(formData);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -126,10 +115,7 @@ export default defineComponent({
|
||||
</label>
|
||||
|
||||
<v-card-actions class="justify-center">
|
||||
<v-btn
|
||||
:disabled="filesIncomplete"
|
||||
@click="ynabImport"
|
||||
>Importieren</v-btn>
|
||||
<v-btn :disabled="filesIncomplete" @click="ynabImport">Importieren</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
@ -1,28 +0,0 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { useSessionStore } from "./session";
|
||||
|
||||
export const useAPI = defineStore("api", {
|
||||
actions: {
|
||||
GET(path : string) {
|
||||
const sessionStore = useSessionStore();
|
||||
return fetch("/api/v1" + path, {
|
||||
headers: sessionStore.AuthHeaders,
|
||||
})
|
||||
},
|
||||
POST(path : string, body : FormData | string | null) {
|
||||
const sessionStore = useSessionStore();
|
||||
return fetch("/api/v1" + path, {
|
||||
method: "POST",
|
||||
headers: sessionStore.AuthHeaders,
|
||||
body: body,
|
||||
})
|
||||
},
|
||||
DELETE(path : string) {
|
||||
const sessionStore = useSessionStore();
|
||||
return fetch("/api/v1" + path, {
|
||||
method: "DELETE",
|
||||
headers: sessionStore.AuthHeaders,
|
||||
})
|
||||
},
|
||||
}
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
import { defineStore } from "pinia"
|
||||
import { useAPI } from "./api";
|
||||
import { GET } from "../api";
|
||||
import { useSessionStore } from "./session";
|
||||
|
||||
interface State {
|
||||
@ -81,14 +81,12 @@ export const useAccountStore = defineStore("budget/account", {
|
||||
await this.FetchAccount(accountid);
|
||||
},
|
||||
async FetchAccount(accountid : string) {
|
||||
const api = useAPI();
|
||||
const result = await api.GET("/account/" + accountid + "/transactions");
|
||||
const result = await GET("/account/" + accountid + "/transactions");
|
||||
const response = await result.json();
|
||||
this.Transactions = response.Transactions;
|
||||
},
|
||||
async FetchMonthBudget(budgetid : string, year : number, month : number) {
|
||||
const api = useAPI();
|
||||
const result = await api.GET("/budget/" + budgetid + "/" + year + "/" + month);
|
||||
const result = await GET("/budget/" + budgetid + "/" + year + "/" + month);
|
||||
const response = await result.json();
|
||||
this.addCategoriesForMonth(year, month, response.Categories);
|
||||
},
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { useAPI } from "./api";
|
||||
import { GET, POST } from "../api";
|
||||
import { useAccountStore } from "./budget-account";
|
||||
import { Budget, useSessionStore } from "./session";
|
||||
|
||||
@ -25,15 +25,13 @@ export const useBudgetsStore = defineStore('budget', {
|
||||
},
|
||||
actions: {
|
||||
ImportYNAB(formData: FormData) {
|
||||
const api = useAPI();
|
||||
return api.POST(
|
||||
return POST(
|
||||
"/budget/" + this.CurrentBudgetID + "/import/ynab",
|
||||
formData,
|
||||
);
|
||||
},
|
||||
async NewBudget(budgetName: string): Promise<void> {
|
||||
const api = useAPI();
|
||||
const result = await api.POST(
|
||||
const result = await POST(
|
||||
"/budget/new",
|
||||
JSON.stringify({ name: budgetName })
|
||||
);
|
||||
@ -51,8 +49,7 @@ export const useBudgetsStore = defineStore('budget', {
|
||||
await this.FetchBudget(budgetid);
|
||||
},
|
||||
async FetchBudget(budgetid: string) {
|
||||
const api = useAPI();
|
||||
const result = await api.GET("/budget/" + budgetid);
|
||||
const result = await GET("/budget/" + budgetid);
|
||||
const response = await result.json();
|
||||
for (const account of response.Accounts || []) {
|
||||
useAccountStore().Accounts.set(account.ID, account);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { StorageSerializers, useStorage } from '@vueuse/core';
|
||||
import { defineStore } from 'pinia'
|
||||
import { useAPI } from './api';
|
||||
import { POST } from '../api';
|
||||
|
||||
interface State {
|
||||
Session: Session | null
|
||||
@ -21,7 +21,7 @@ export interface Budget {
|
||||
export const useSessionStore = defineStore('session', {
|
||||
state: () => ({
|
||||
Session: useStorage<Session>('session', null, undefined, { serializer: StorageSerializers.object }),
|
||||
Budgets: useStorage<Map<string, Budget>>('budgets', new Map<string, Budget>()),
|
||||
Budgets: useStorage<Map<string, Budget>>('budgets', new Map<string, Budget>(), undefined, { serializer: StorageSerializers.map }),
|
||||
}),
|
||||
getters: {
|
||||
BudgetsList: (state) => [ ...state.Budgets.values() ],
|
||||
@ -40,14 +40,12 @@ export const useSessionStore = defineStore('session', {
|
||||
this.Budgets = x.Budgets;
|
||||
},
|
||||
async login(login: any) {
|
||||
const api = useAPI();
|
||||
const response = await api.POST("/user/login", JSON.stringify(login));
|
||||
const response = await POST("/user/login", JSON.stringify(login));
|
||||
const result = await response.json();
|
||||
return this.loginSuccess(result);
|
||||
},
|
||||
async register(login : any) {
|
||||
const api = useAPI();
|
||||
const response = await api.POST("/user/register", JSON.stringify(login));
|
||||
const response = await POST("/user/register", JSON.stringify(login));
|
||||
const result = await response.json();
|
||||
return this.loginSuccess(result);
|
||||
},
|
||||
|
Reference in New Issue
Block a user