12 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
13 changed files with 251 additions and 221 deletions

View File

@@ -35,8 +35,6 @@ func (h *Handler) newTransaction(c *gin.Context) {
return return
} }
fmt.Printf("%v\n", payload)
amount := postgres.Numeric{} amount := postgres.Numeric{}
amount.Set(payload.Amount) amount.Set(payload.Amount)
@@ -46,22 +44,40 @@ func (h *Handler) newTransaction(c *gin.Context) {
return 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 { //if !transactionUUID.Valid {
new := postgres.CreateTransactionParams{ new := postgres.CreateTransactionParams{
Memo: payload.Memo, Memo: payload.Memo,
Date: time.Time(payload.Date), Date: time.Time(payload.Date),
Amount: amount, Amount: amount,
AccountID: payload.AccountID, 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 CategoryID: payload.Category.ID, //TODO handle new category
Status: postgres.TransactionStatus(payload.State), 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 { if err != nil {
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("create transaction: %w", err)) c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("create transaction: %w", err))
return
} }
return c.JSON(http.StatusOK, transaction)
// } // }
/* /*
_, delete := c.GetPostForm("delete") _, delete := c.GetPostForm("delete")

View File

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

View File

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

View File

@@ -1,19 +1,15 @@
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from "vue"; import { computed } from 'vue';
export default defineComponent({ const props = defineProps<{ value: number | undefined }>();
props: ["value"],
computed: { const internalValue = computed(() => Number(props.value ?? 0));
formattedValue() {
return Number(this.value).toLocaleString(undefined, { const formattedValue = computed(() => internalValue.value.toLocaleString(undefined, {
minimumFractionDigits: 2, minimumFractionDigits: 2,
}); }));
}
}
})
</script> </script>
<template> <template>
<span class="text-right" :class="value < 0 ? 'negative' : ''">{{formattedValue}} </span> <span class="text-right" :class="internalValue < 0 ? 'negative' : ''">{{ formattedValue }} </span>
</template> </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"> <script lang="ts" setup>
import { mapState } from "pinia"; import { computed } from "vue";
import { defineComponent } from "vue";
import { useBudgetsStore } from "../stores/budget"; import { useBudgetsStore } from "../stores/budget";
import { Transaction } from "../stores/budget-account";
import Currency from "./Currency.vue"; import Currency from "./Currency.vue";
export default defineComponent({ const props = defineProps<{
props: [ "transaction", "index" ], transaction: Transaction,
components: { Currency }, index: number,
computed: { }>();
...mapState(useBudgetsStore, ["CurrentBudgetID"])
} const CurrentBudgetID = computed(()=> useBudgetsStore().CurrentBudgetID);
})
</script> </script>
<template> <template>

View File

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

View File

@@ -1,9 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from "vue" import { computed, ref } from "vue"
import Autocomplete, { Suggestion } from '../components/Autocomplete.vue'
import Currency from "../components/Currency.vue"; import Currency from "../components/Currency.vue";
import TransactionRow from "../components/TransactionRow.vue"; import TransactionRow from "../components/TransactionRow.vue";
import { POST } from "../api"; import TransactionInputRow from "../components/TransactionInputRow.vue";
import { useAccountStore } from "../stores/budget-account"; import { useAccountStore } from "../stores/budget-account";
const props = defineProps<{ const props = defineProps<{
@@ -11,30 +10,9 @@ const props = defineProps<{
accountid: 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 accountStore = useAccountStore();
const CurrentAccount = accountStore.CurrentAccount; const CurrentAccount = computed(() => accountStore.CurrentAccount);
const TransactionsList = accountStore.TransactionsList; const TransactionsList = computed(() => accountStore.TransactionsList);
</script> </script>
<template> <template>
@@ -53,31 +31,7 @@ const TransactionsList = accountStore.TransactionsList;
<td style="width: 20px;"></td> <td style="width: 20px;"></td>
<td style="width: 20px;"></td> <td style="width: 20px;"></td>
</tr> </tr>
<tr> <TransactionInputRow :budgetid="budgetid" :accountid="accountid" />
<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 <TransactionRow
v-for="(transaction, index) in TransactionsList" v-for="(transaction, index) in TransactionsList"
:transaction="transaction" :transaction="transaction"

View File

@@ -1,8 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import { useSessionStore } from '../stores/session';
onMounted(() => { onMounted(() => {
document.title = "Budgeteer - Admin"; useSessionStore().setTitle("Admin");
}) })
</script> </script>

View File

@@ -1,13 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, defineProps, onMounted, PropType, watch, watchEffect } from "vue"; import { computed, defineProps, onMounted, watchEffect } from "vue";
import Currency from "../components/Currency.vue"; import Currency from "../components/Currency.vue";
import { useBudgetsStore } from "../stores/budget"; import { useBudgetsStore } from "../stores/budget";
import { 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<{ const props = defineProps<{
budgetid: string, budgetid: string,
@@ -22,6 +18,7 @@ const categoriesForMonth = useAccountStore().CategoriesForMonth;
const Categories = computed(() => { const Categories = computed(() => {
return [...categoriesForMonth(selected.value.Year, selected.value.Month)]; return [...categoriesForMonth(selected.value.Year, selected.value.Month)];
}); });
const previous = computed(() => ({ const previous = computed(() => ({
Year: new Date(selected.value.Year, selected.value.Month - 1, 1).getFullYear(), Year: new Date(selected.value.Year, selected.value.Month - 1, 1).getFullYear(),
Month: new Date(selected.value.Year, selected.value.Month - 1, 1).getMonth(), Month: new Date(selected.value.Year, selected.value.Month - 1, 1).getMonth(),
@@ -44,9 +41,9 @@ watchEffect(() => {
return useAccountStore().FetchMonthBudget(props.budgetid ?? "", Number(props.year), Number(props.month)); return useAccountStore().FetchMonthBudget(props.budgetid ?? "", Number(props.year), Number(props.month));
}); });
/*{{define "title"}} onMounted(() => {
{{printf "Budget for %s %d" .Date.Month .Date.Year}} useSessionStore().setTitle("Budget for " + selected.value.Month + "/" + selected.value.Year);
{{end}}*/ })
</script> </script>
<template> <template>

View File

@@ -7,7 +7,7 @@ const error = ref("");
const login = ref({ user: "", password: "" }); const login = ref({ user: "", password: "" });
onMounted(() => { onMounted(() => {
document.title = "Budgeteer - Login"; useSessionStore().setTitle("Login");
}); });
function formSubmit(e: MouseEvent) { function formSubmit(e: MouseEvent) {

View File

@@ -10,7 +10,7 @@ const assignmentsFile = ref<File | undefined>(undefined);
const filesIncomplete = computed(() => transactionsFile.value == undefined || assignmentsFile.value == undefined); const filesIncomplete = computed(() => transactionsFile.value == undefined || assignmentsFile.value == undefined);
onMounted(() => { onMounted(() => {
document.title = "Budgeteer - Settings"; useSessionStore().setTitle("Settings");
}); });
function gotAssignments(e: Event) { function gotAssignments(e: Event) {

View File

@@ -1,5 +1,5 @@
import { defineStore } from "pinia" import { defineStore } from "pinia"
import { GET } from "../api"; import { GET, POST } from "../api";
import { useSessionStore } from "./session"; import { useSessionStore } from "./session";
interface State { interface State {
@@ -7,15 +7,28 @@ interface State {
CurrentAccountID: string | null, CurrentAccountID: string | null,
Categories: Map<string, Category>, Categories: Map<string, Category>,
Months: Map<number, Map<number, Map<string, Category>>>, Months: Map<number, Map<number, Map<string, Category>>>,
Transactions: [], Transactions: any[],
Assignments: [] 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 { export interface Account {
ID: string ID: string
Name: string Name: string
OnBudget: boolean OnBudget: boolean
Balance: Number Balance: number
} }
export interface Category { export interface Category {
@@ -42,9 +55,10 @@ export const useAccountStore = defineStore("budget/account", {
return [...state.Accounts.values()]; return [...state.Accounts.values()];
}, },
CategoriesForMonth: (state) => (year: number, month: number) => { CategoriesForMonth: (state) => (year: number, month: number) => {
console.log("MTH", state.Months)
const yearMap = state.Months.get(year); 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) if (state.CurrentAccountID == null)
@@ -55,13 +69,13 @@ export const useAccountStore = defineStore("budget/account", {
OnBudgetAccounts(state) { 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); return this.OnBudgetAccounts.reduce((prev, curr) => prev + Number(curr.Balance), 0);
}, },
OffBudgetAccounts(state) { 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); return this.OffBudgetAccounts.reduce((prev, curr) => prev + Number(curr.Balance), 0);
}, },
TransactionsList(state) { TransactionsList(state) {
@@ -91,19 +105,25 @@ export const useAccountStore = defineStore("budget/account", {
this.addCategoriesForMonth(year, month, response.Categories); this.addCategoriesForMonth(year, month, response.Categories);
}, },
addCategoriesForMonth(year: number, month: number, categories: Category[]): void { addCategoriesForMonth(year: number, month: number, categories: Category[]): void {
const yearMap = this.Months.get(year) || new Map<number, Map<string, Category>>(); this.$patch((state) => {
this.Months.set(year, yearMap); const yearMap = state.Months.get(year) || new Map<number, Map<string, Category>>();
const monthMap = yearMap.get(month) || new Map<string, Category>(); const monthMap = yearMap.get(month) || new Map<string, Category>();
yearMap.set(month, monthMap);
for (const category of categories) { for (const category of categories) {
monthMap.set(category.ID, category); monthMap.set(category.ID, category);
} }
yearMap.set(month, monthMap);
state.Months.set(year, yearMap);
});
}, },
logout() { logout() {
this.$reset() this.$reset()
}, },
async saveTransaction(payload: string) {
const result = await POST("/transaction/new", payload);
const response = await result.json();
this.Transactions.unshift(response);
}
} }
}) })