43 Commits

Author SHA1 Message Date
8fbdd78cb3 Add Transaction to list after saving
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-15 09:14:06 +00:00
b3ff5cf055 Extract component for new Transaction 2022-02-15 09:14:06 +00:00
a09511061f Handle new Payees 2022-02-15 09:14:06 +00:00
368ac7f15d Merge pull request 'Use vue's Composition API in components' (#11) from vue-composition into master
All checks were successful
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #11
2022-02-15 10:13:47 +01:00
0d20d9bfb8 Use setTitle everywhere
All checks were successful
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
continuous-integration/drone/pr Build is passing
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-15 08:35:29 +00:00
4276c51268 Remove unused interface
All checks were successful
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
2022-02-15 08:27:09 +00:00
57930d0e5d Rewrite addCategoriesForMonth with patch call
All checks were successful
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
2022-02-15 08:25:41 +00:00
fe018e1953 Reformat 2022-02-15 08:25:30 +00:00
e7a085273b Fix missing computed calls in Account 2022-02-15 08:25:12 +00:00
5bbd096fc8 Convert other components 2022-02-15 08:20:04 +00:00
452d63c329 Define Transaction interface and use number instead of Number 2022-02-15 08:04:42 +00:00
d28c894d21 Convert NewBudget and TransactionRow 2022-02-15 08:04:25 +00:00
1a79177422 Fix missing computed calls in BudgetSidebar
All checks were successful
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
2022-02-14 22:49:30 +00:00
0aa877d7d4 Merge pull request 'Use vue's Composition API' (#10) from vue-composition into master
All checks were successful
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #10
2022-02-14 23:44:00 +01:00
87a70ee5fa Revert
All checks were successful
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
continuous-integration/drone/pr Build is passing
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-14 22:39:14 +00:00
0a030eaee1 Convert other pages to composition API 2022-02-14 22:24:42 +00:00
d11c0036b5 Do not use a store for API 2022-02-14 08:12:41 +00:00
ca93e9cd55 Migrate Account.vue to composition API
All checks were successful
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
2022-02-14 08:06:16 +00:00
a061ffd350 Merge pull request 'Implement minor fixes' (#9) from minor-fixes into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
continuous-integration/drone/push Build is passing
Reviewed-on: #9
2022-02-14 08:48:07 +01:00
5633c029ac Update Earthfile and production docker-compose.yml
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-13 13:20:18 +00:00
a97d050ead Specify Map serializer for budgets
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-13 13:16:50 +00:00
958929fd16 Update docker-compose to use new tag 2022-02-13 13:16:50 +00:00
a61d80ee1f Implement SPA handling in Backend 2022-02-13 13:16:50 +00:00
41c5095b8b Merge pull request 'Use woodpecker for CI' (#8) from woodpecker into master
All checks were successful
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #8
2022-02-13 14:16:38 +01:00
c074dfe865 Fix Taskfile
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-12 00:00:33 +00:00
fa8a2854f2 Add node packages to image
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-12 00:00:19 +00:00
15bb73de30 Add secrects
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-11 23:54:26 +00:00
e506510fde Remove user and add go deps from builder image 2022-02-11 23:49:43 +00:00
11ac8758da Disable docker from taskfile
Some checks failed
continuous-integration/drone/pr Build is passing
ci/woodpecker/push/woodpecker Pipeline failed
continuous-integration/drone/push Build is passing
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-11 23:43:55 +00:00
3db5e1e72c Reenable docker push
Some checks failed
continuous-integration/drone/push Build is failing
ci/woodpecker/push/woodpecker Pipeline failed
continuous-integration/drone/pr Build is failing
ci/woodpecker/pr/woodpecker Pipeline failed
2022-02-11 23:42:03 +00:00
4e2a783b2e Extract variable 2022-02-11 23:40:46 +00:00
bb83563bc6 Use docker build-target
Some checks failed
continuous-integration/drone/push Build is failing
ci/woodpecker/push/woodpecker Pipeline failed
2022-02-11 23:35:15 +00:00
0a21c59eff Fetch deps before build
Some checks failed
continuous-integration/drone/push Build is failing
ci/woodpecker/push/woodpecker Pipeline failed
2022-02-11 23:33:47 +00:00
3308b58524 Fix sources in Taskfile
Some checks failed
continuous-integration/drone/push Build is failing
ci/woodpecker/push/woodpecker Pipeline failed
2022-02-11 23:30:53 +00:00
941b642f39 Build docker within task
Some checks failed
continuous-integration/drone/push Build is failing
ci/woodpecker/push/woodpecker Pipeline failed
2022-02-11 23:16:57 +00:00
6a77c71df4 Always pull dev image
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
continuous-integration/drone/push Build is failing
2022-02-11 22:59:38 +00:00
bf20914c1c Use default build name
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
continuous-integration/drone/push Build is failing
2022-02-11 22:58:31 +00:00
7874ef69a2 Fix Taskfile
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
continuous-integration/drone/push Build is failing
2022-02-11 22:57:26 +00:00
2e719b590e Add admin page 2022-02-11 22:52:00 +00:00
95d8e4fccc Rewrite event filter
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
continuous-integration/drone/push Build is failing
2022-02-11 22:49:06 +00:00
7cf106eb85 Rename 2022-02-11 22:41:35 +00:00
148fc18cd8 Fix schema 2022-02-11 22:41:07 +00:00
47095ae6ec Add .woodpecker.yaml 2022-02-11 22:39:19 +00:00
30 changed files with 602 additions and 539 deletions

View File

@@ -7,4 +7,5 @@ config.example.json
.gitignore
.vscode/
budgeteer
budgeteer.exe
budgeteer.exe
**/node_modules/

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -2,7 +2,7 @@ version: '3.7'
services:
app:
image: budgeteer:latest
image: hub.javil.eu/budgeteer:latest
container_name: budgeteer
ports:
- 1323:1323

View File

@@ -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

View File

@@ -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) {

View File

@@ -35,8 +35,6 @@ func (h *Handler) newTransaction(c *gin.Context) {
return
}
fmt.Printf("%v\n", payload)
amount := postgres.Numeric{}
amount.Set(payload.Amount)
@@ -46,22 +44,40 @@ func (h *Handler) newTransaction(c *gin.Context) {
return
}*/
payeeID := payload.Payee.ID
if !payeeID.Valid && payload.Payee.Name != "" {
newPayee := postgres.CreatePayeeParams{
Name: payload.Payee.Name,
BudgetID: payload.BudgetID,
}
payee, err := h.Service.CreatePayee(c.Request.Context(), newPayee)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("create payee: %w", err))
}
payeeID = uuid.NullUUID{
UUID: payee.ID,
Valid: true,
}
}
//if !transactionUUID.Valid {
new := postgres.CreateTransactionParams{
Memo: payload.Memo,
Date: time.Time(payload.Date),
Amount: amount,
AccountID: payload.AccountID,
PayeeID: payload.Payee.ID, //TODO handle new payee
PayeeID: payeeID, //TODO handle new payee
CategoryID: payload.Category.ID, //TODO handle new category
Status: postgres.TransactionStatus(payload.State),
}
_, err = h.Service.CreateTransaction(c.Request.Context(), new)
transaction, err := h.Service.CreateTransaction(c.Request.Context(), new)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("create transaction: %w", err))
return
}
return
c.JSON(http.StatusOK, transaction)
// }
/*
_, delete := c.GetPostForm("delete")

26
web/src/api.ts Normal file
View 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,
})
}

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import { defineComponent, PropType } from "vue"
import { useAPI } from "../stores/api";
<script lang="ts" setup>
import { defineComponent, PropType, ref, watch } from "vue"
import { GET } from "../api";
import { useBudgetsStore } from "../stores/budget";
export interface Suggestion {
ID : string
Name : string
ID: string
Name: string
}
interface Data {
@@ -14,89 +14,88 @@ interface Data {
Suggestions: Suggestion[]
}
export default defineComponent({
data() {
return {
Selected: undefined,
SearchQuery: this.modelValue || "",
Suggestions: new Array<Suggestion>(),
} as Data
},
props: {
modelValue: Object as PropType<Suggestion>,
type: String
},
watch: {
SearchQuery() {
this.load(this.$data.SearchQuery);
}
},
methods: {
saveTransaction(e : MouseEvent) {
e.preventDefault();
},
load(text : String) {
this.$emit('update:modelValue', {ID: null, Name: text});
if (text == ""){
this.$data.Suggestions = [];
return;
}
const api = useAPI();
const budgetStore = useBudgetsStore();
api.GET("/budget/" + budgetStore.CurrentBudgetID + "/autocomplete/" + this.type + "?s=" + text)
.then(x=>x.json())
.then(x => {
let suggestions = x || [];
if(suggestions.length > 10){
suggestions = suggestions.slice(0, 10);
}
this.$data.Suggestions = suggestions;
});
},
keypress(e : KeyboardEvent) {
console.log(e.key);
if(e.key == "Enter") {
const selected = this.$data.Suggestions[0];
this.selectElement(selected);
const el = (<HTMLInputElement>e.target);
const inputElements = Array.from(el.ownerDocument.querySelectorAll('input:not([disabled]):not([readonly])'));
const currentIndex = inputElements.indexOf(el);
const nextElement = inputElements[currentIndex < inputElements.length - 1 ? currentIndex + 1 : 0];
(<HTMLInputElement>nextElement).focus();
const props = defineProps<{
modelValue: Suggestion | undefined,
type: String
}>();
}
},
selectElement(element : Suggestion) {
this.$data.Selected = element;
this.$data.Suggestions = [];
this.$emit('update:modelValue', element);
},
select(e : MouseEvent) {
const target = (<HTMLInputElement>e.target);
const valueAttribute = target.attributes.getNamedItem("value");
let selectedID = "";
if(valueAttribute != null)
selectedID = valueAttribute.value;
const selected = this.$data.Suggestions.filter(x => x.ID == selectedID)[0];
this.selectElement(selected);
},
clear() {
this.$data.Selected = undefined;
this.$emit('update:modelValue', {ID: null, Name: this.$data.SearchQuery});
}
const Selected = ref<Suggestion | undefined>(props.modelValue || undefined);
const SearchQuery = ref(props.modelValue?.Name || "");
const Suggestions = ref<Array<Suggestion>>([]);
const emit = defineEmits(["update:modelValue"]);
watch(SearchQuery, () => {
load(SearchQuery.value);
});
function saveTransaction(e: MouseEvent) {
e.preventDefault();
};
function load(text: String) {
emit('update:modelValue', { ID: null, Name: text });
if (text == "") {
Suggestions.value = [];
return;
}
})
const budgetStore = useBudgetsStore();
GET("/budget/" + budgetStore.CurrentBudgetID + "/autocomplete/" + props.type + "?s=" + text)
.then(x => x.json())
.then(x => {
let suggestions = x || [];
if (suggestions.length > 10) {
suggestions = suggestions.slice(0, 10);
}
Suggestions.value = suggestions;
});
};
function keypress(e: KeyboardEvent) {
console.log(e.key);
if (e.key == "Enter") {
const selected = Suggestions.value[0];
selectElement(selected);
const el = (<HTMLInputElement>e.target);
const inputElements = Array.from(el.ownerDocument.querySelectorAll('input:not([disabled]):not([readonly])'));
const currentIndex = inputElements.indexOf(el);
const nextElement = inputElements[currentIndex < inputElements.length - 1 ? currentIndex + 1 : 0];
(<HTMLInputElement>nextElement).focus();
}
};
function selectElement(element: Suggestion) {
Selected.value = element;
Suggestions.value = [];
emit('update:modelValue', element);
};
function select(e: MouseEvent) {
const target = (<HTMLInputElement>e.target);
const valueAttribute = target.attributes.getNamedItem("value");
let selectedID = "";
if (valueAttribute != null)
selectedID = valueAttribute.value;
const selected = Suggestions.value.filter(x => x.ID == selectedID)[0];
selectElement(selected);
};
function clear() {
Selected.value = undefined;
emit('update:modelValue', { ID: null, Name: SearchQuery.value });
};
</script>
<template>
<div>
<input class="border-b-2 border-black" @keypress="keypress" v-if="Selected == undefined" v-model="SearchQuery" />
<span @click="clear" v-if="Selected != undefined" class="bg-gray-300">{{Selected.Name}}</span>
<input
class="border-b-2 border-black"
@keypress="keypress"
v-if="Selected == undefined"
v-model="SearchQuery"
/>
<span @click="clear" v-if="Selected != undefined" class="bg-gray-300">{{ Selected.Name }}</span>
<div v-if="Suggestions.length > 0" class="absolute bg-gray-400 w-64 p-2">
<span v-for="suggestion in Suggestions" class="block" @click="select" :value="suggestion.ID">
{{suggestion.Name}}
</span>
<span
v-for="suggestion in Suggestions"
class="block"
@click="select"
:value="suggestion.ID"
>{{ suggestion.Name }}</span>
</div>
</div>
</template>

View File

@@ -1,9 +1,4 @@
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
})
<script lang="ts" setup>
</script>
<template>

View File

@@ -1,19 +1,15 @@
<script lang="ts">
import { defineComponent } from "vue";
<script lang="ts" setup>
import { computed } from 'vue';
export default defineComponent({
props: ["value"],
computed: {
formattedValue() {
return Number(this.value).toLocaleString(undefined, {
minimumFractionDigits: 2,
});
}
}
})
const props = defineProps<{ value: number | undefined }>();
const internalValue = computed(() => Number(props.value ?? 0));
const formattedValue = computed(() => internalValue.value.toLocaleString(undefined, {
minimumFractionDigits: 2,
}));
</script>
<template>
<span class="text-right" :class="value < 0 ? 'negative' : ''">{{formattedValue}} </span>
<span class="text-right" :class="internalValue < 0 ? 'negative' : ''">{{ formattedValue }} </span>
</template>

View File

@@ -0,0 +1,61 @@
<script lang="ts" setup>
import { computed, ref } from "vue";
import Autocomplete, { Suggestion } from '../components/Autocomplete.vue'
import { useAccountStore } from '../stores/budget-account'
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");
const payload = computed(() => JSON.stringify({
budget_id: props.budgetid,
account_id: props.accountid,
date: TransactionDate.value,
payee: Payee.value,
category: Category.value,
memo: Memo.value,
amount: Amount.value,
state: "Uncleared"
}));
const accountStore = useAccountStore();
function saveTransaction(e: MouseEvent) {
e.preventDefault();
accountStore.saveTransaction(payload.value);
}
</script>
<template>
<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>
</template>

View File

@@ -1,16 +1,15 @@
<script lang="ts">
import { mapState } from "pinia";
import { defineComponent } from "vue";
<script lang="ts" setup>
import { computed } from "vue";
import { useBudgetsStore } from "../stores/budget";
import { Transaction } from "../stores/budget-account";
import Currency from "./Currency.vue";
export default defineComponent({
props: [ "transaction", "index" ],
components: { Currency },
computed: {
...mapState(useBudgetsStore, ["CurrentBudgetID"])
}
})
const props = defineProps<{
transaction: Transaction,
index: number,
}>();
const CurrentBudgetID = computed(()=> useBudgetsStore().CurrentBudgetID);
</script>
<template>

View File

@@ -1,26 +1,17 @@
<script lang="ts">
<script lang="ts" setup>
import Card from '../components/Card.vue';
import { defineComponent } from "vue";
import { ref } from "vue";
import { useBudgetsStore } from '../stores/budget';
export default defineComponent({
data() {
return {
dialog: false,
budgetName: ""
}
},
components: { Card },
methods: {
saveBudget() {
useBudgetsStore().NewBudget(this.$data.budgetName);
this.$data.dialog = false;
},
newBudget() {
this.$data.dialog = true;
}
}
})
const dialog = ref(false);
const budgetName = ref("");
function saveBudget() {
useBudgetsStore().NewBudget(budgetName.value);
dialog.value = false;
};
function newBudget() {
dialog.value = true;
};
</script>
<template>

View File

@@ -1,89 +1,43 @@
<script lang="ts">
import { mapState } from "pinia";
import { defineComponent } from "vue"
import Autocomplete, { Suggestion } from '../components/Autocomplete.vue'
<script lang="ts" setup>
import { computed, ref } from "vue"
import Currency from "../components/Currency.vue";
import TransactionRow from "../components/TransactionRow.vue";
import { useAPI } from "../stores/api";
import TransactionInputRow from "../components/TransactionInputRow.vue";
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 accountStore = useAccountStore();
const CurrentAccount = computed(() => accountStore.CurrentAccount);
const TransactionsList = computed(() => 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>
<TransactionInputRow :budgetid="budgetid" :accountid="accountid" />
<TransactionRow
v-for="(transaction, index) in TransactionsList"
:transaction="transaction"
:index="index"
/>
</table>
</template>
<style>

16
web/src/pages/Admin.vue Normal file
View File

@@ -0,0 +1,16 @@
<script lang="ts" setup>
import { onMounted } from 'vue';
import { useSessionStore } from '../stores/session';
onMounted(() => {
useSessionStore().setTitle("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>

View File

@@ -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>

View File

@@ -1,76 +1,53 @@
<script lang="ts">
import { mapState } from "pinia";
import { defineComponent, PropType } from "vue";
<script lang="ts" setup>
import { computed, defineProps, onMounted, 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";
import { useSessionStore } from "../stores/session";
interface Date {
Year: number,
Month: number,
}
const props = defineProps<{
budgetid: string,
year: string,
month: string,
}>()
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 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));
});
onMounted(() => {
useSessionStore().setTitle("Budget for " + selected.value.Month + "/" + selected.value.Year);
})
/*{{define "title"}}
{{printf "Budget for %s %d" .Date.Month .Date.Year}}
{{end}}*/
</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"
@@ -112,4 +89,4 @@ export default defineComponent({
</td>
</tr>
</table>
</template>
</template>

View File

@@ -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>

View File

@@ -1,9 +1,4 @@
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
})
<script lang="ts" setup>
</script>
<template>

View File

@@ -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(() => {
useSessionStore().setTitle("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>

View File

@@ -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>

View File

@@ -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(() => {
useSessionStore().setTitle("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>

View File

@@ -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,
})
},
}
});

View File

@@ -1,5 +1,5 @@
import { defineStore } from "pinia"
import { useAPI } from "./api";
import { GET, POST } from "../api";
import { useSessionStore } from "./session";
interface State {
@@ -7,15 +7,28 @@ interface State {
CurrentAccountID: string | null,
Categories: Map<string, Category>,
Months: Map<number, Map<number, Map<string, Category>>>,
Transactions: [],
Transactions: any[],
Assignments: []
}
export interface Transaction {
ID: string,
Date: string,
TransferAccount: string,
CategoryGroup: string,
Category: string,
Memo: string,
Status: string,
GroupID: string,
Payee: string,
Amount: number,
}
export interface Account {
ID: string
Name: string
OnBudget: boolean
Balance: Number
Balance: number
}
export interface Category {
@@ -39,29 +52,30 @@ export const useAccountStore = defineStore("budget/account", {
}),
getters: {
AccountsList(state) {
return [ ...state.Accounts.values() ];
return [...state.Accounts.values()];
},
CategoriesForMonth: (state) => (year : number, month : number) => {
console.log("MTH", state.Months)
CategoriesForMonth: (state) => (year: number, month: number) => {
const yearMap = state.Months.get(year);
return [ ...yearMap?.get(month)?.values() || [] ];
const monthMap = yearMap?.get(month);
console.log("MTH", monthMap)
return [...monthMap?.values() || []];
},
CurrentAccount(state) : Account | undefined {
CurrentAccount(state): Account | undefined {
if (state.CurrentAccountID == null)
return undefined;
return state.Accounts.get(state.CurrentAccountID);
},
OnBudgetAccounts(state) {
return [ ...state.Accounts.values() ].filter(x => x.OnBudget);
return [...state.Accounts.values()].filter(x => x.OnBudget);
},
OnBudgetAccountsBalance(state) : Number {
OnBudgetAccountsBalance(state): number {
return this.OnBudgetAccounts.reduce((prev, curr) => prev + Number(curr.Balance), 0);
},
OffBudgetAccounts(state) {
return [ ...state.Accounts.values() ].filter(x => !x.OnBudget);
return [...state.Accounts.values()].filter(x => !x.OnBudget);
},
OffBudgetAccountsBalance(state) : Number {
OffBudgetAccountsBalance(state): number {
return this.OffBudgetAccounts.reduce((prev, curr) => prev + Number(curr.Balance), 0);
},
TransactionsList(state) {
@@ -69,7 +83,7 @@ export const useAccountStore = defineStore("budget/account", {
}
},
actions: {
async SetCurrentAccount(budgetid : string, accountid : string) {
async SetCurrentAccount(budgetid: string, accountid: string) {
if (budgetid == null)
return
@@ -80,32 +94,36 @@ export const useAccountStore = defineStore("budget/account", {
useSessionStore().setTitle(this.CurrentAccount.Name);
await this.FetchAccount(accountid);
},
async FetchAccount(accountid : string) {
const api = useAPI();
const result = await api.GET("/account/" + accountid + "/transactions");
async FetchAccount(accountid: string) {
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);
async FetchMonthBudget(budgetid: string, year: number, month: number) {
const result = await GET("/budget/" + budgetid + "/" + year + "/" + month);
const response = await result.json();
this.addCategoriesForMonth(year, month, response.Categories);
},
addCategoriesForMonth(year : number, month : number, categories : Category[]) : void {
const yearMap = this.Months.get(year) || new Map<number, Map<string, Category>>();
this.Months.set(year, yearMap);
addCategoriesForMonth(year: number, month: number, categories: Category[]): void {
this.$patch((state) => {
const yearMap = state.Months.get(year) || new Map<number, Map<string, Category>>();
const monthMap = yearMap.get(month) || new Map<string, Category>();
for (const category of categories) {
monthMap.set(category.ID, category);
}
const monthMap = yearMap.get(month) || new Map<string, Category>();
yearMap.set(month, monthMap);
for (const category of categories){
monthMap.set(category.ID, category);
}
yearMap.set(month, monthMap);
state.Months.set(year, yearMap);
});
},
logout() {
this.$reset()
},
async saveTransaction(payload: string) {
const result = await POST("/transaction/new", payload);
const response = await result.json();
this.Transactions.unshift(response);
}
}
})
})

View File

@@ -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);

View File

@@ -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);
},