Merge pull request 'Display problematic transactions' (#53) from problematic-transactions into master
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #53
This commit is contained in:
commit
0667e5a779
@ -23,9 +23,10 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- task ci
|
- task ci
|
||||||
when:
|
when:
|
||||||
|
branch:
|
||||||
|
- master
|
||||||
event:
|
event:
|
||||||
exclude:
|
- push
|
||||||
- pull_request
|
|
||||||
|
|
||||||
- name: docker
|
- name: docker
|
||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
|
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -9,5 +9,7 @@
|
|||||||
},
|
},
|
||||||
"gopls": {
|
"gopls": {
|
||||||
"formatting.gofumpt": true,
|
"formatting.gofumpt": true,
|
||||||
}
|
},
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
"editor.tabSize": 2
|
||||||
}
|
}
|
@ -45,4 +45,20 @@ AND accounts.id = transactions.account_id;
|
|||||||
-- name: GetTransactionsByMonthAndCategory :many
|
-- name: GetTransactionsByMonthAndCategory :many
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM transactions_by_month
|
FROM transactions_by_month
|
||||||
WHERE transactions_by_month.budget_id = @budget_id;
|
WHERE transactions_by_month.budget_id = @budget_id;
|
||||||
|
|
||||||
|
-- name: GetProblematicTransactions :many
|
||||||
|
SELECT transactions.*
|
||||||
|
FROM display_transactions AS transactions
|
||||||
|
LEFT JOIN accounts
|
||||||
|
ON transactions.account_id = accounts.id
|
||||||
|
LEFT JOIN transactions AS otherGroupTransaction
|
||||||
|
ON transactions.group_id = otherGroupTransaction.group_id
|
||||||
|
AND transactions.id != otherGroupTransaction.id
|
||||||
|
AND transactions.account_id != otherGroupTransaction.account_id
|
||||||
|
LEFT JOIn accounts AS otherGroupAccount
|
||||||
|
ON otherGroupTransaction.account_id = otherGroupAccount.id
|
||||||
|
WHERE transactions.category_id IS NULL
|
||||||
|
AND accounts.on_budget
|
||||||
|
AND (otherGroupAccount.id IS NULL OR NOT otherGroupAccount.on_budget)
|
||||||
|
AND accounts.budget_id = $1;
|
@ -117,6 +117,62 @@ func (q *Queries) GetAllTransactionsForBudget(ctx context.Context, budgetID uuid
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getProblematicTransactions = `-- name: GetProblematicTransactions :many
|
||||||
|
SELECT transactions.id, transactions.date, transactions.memo, transactions.amount, transactions.group_id, transactions.status, transactions.account, transactions.payee_id, transactions.category_id, transactions.payee, transactions.category_group, transactions.category, transactions.transfer_account, transactions.budget_id, transactions.account_id
|
||||||
|
FROM display_transactions AS transactions
|
||||||
|
LEFT JOIN accounts
|
||||||
|
ON transactions.account_id = accounts.id
|
||||||
|
LEFT JOIN transactions AS otherGroupTransaction
|
||||||
|
ON transactions.group_id = otherGroupTransaction.group_id
|
||||||
|
AND transactions.id != otherGroupTransaction.id
|
||||||
|
AND transactions.account_id != otherGroupTransaction.account_id
|
||||||
|
LEFT JOIn accounts AS otherGroupAccount
|
||||||
|
ON otherGroupTransaction.account_id = otherGroupAccount.id
|
||||||
|
WHERE transactions.category_id IS NULL
|
||||||
|
AND accounts.on_budget
|
||||||
|
AND (otherGroupAccount.id IS NULL OR NOT otherGroupAccount.on_budget)
|
||||||
|
AND accounts.budget_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetProblematicTransactions(ctx context.Context, budgetID uuid.UUID) ([]DisplayTransaction, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getProblematicTransactions, budgetID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []DisplayTransaction
|
||||||
|
for rows.Next() {
|
||||||
|
var i DisplayTransaction
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Date,
|
||||||
|
&i.Memo,
|
||||||
|
&i.Amount,
|
||||||
|
&i.GroupID,
|
||||||
|
&i.Status,
|
||||||
|
&i.Account,
|
||||||
|
&i.PayeeID,
|
||||||
|
&i.CategoryID,
|
||||||
|
&i.Payee,
|
||||||
|
&i.CategoryGroup,
|
||||||
|
&i.Category,
|
||||||
|
&i.TransferAccount,
|
||||||
|
&i.BudgetID,
|
||||||
|
&i.AccountID,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
const getTransaction = `-- name: GetTransaction :one
|
const getTransaction = `-- name: GetTransaction :one
|
||||||
SELECT id, date, memo, amount, group_id, status, account, payee_id, category_id, payee, category_group, category, transfer_account, budget_id, account_id FROM display_transactions
|
SELECT id, date, memo, amount, group_id, status, account, payee_id, category_id, payee, category_group, category, transfer_account, budget_id, account_id FROM display_transactions
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
|
@ -8,6 +8,23 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (h *Handler) problematicTransactions(c *gin.Context) {
|
||||||
|
budgetID := c.Param("budgetid")
|
||||||
|
budgetUUID, err := uuid.Parse(budgetID)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
transactions, err := h.Service.GetProblematicTransactions(c.Request.Context(), budgetUUID)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, TransactionsResponse{nil, transactions})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) transactionsForAccount(c *gin.Context) {
|
func (h *Handler) transactionsForAccount(c *gin.Context) {
|
||||||
accountID := c.Param("accountid")
|
accountID := c.Param("accountid")
|
||||||
accountUUID, err := uuid.Parse(accountID)
|
accountUUID, err := uuid.Parse(accountID)
|
||||||
@ -28,11 +45,11 @@ func (h *Handler) transactionsForAccount(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, TransactionsResponse{account, transactions})
|
c.JSON(http.StatusOK, TransactionsResponse{&account, transactions})
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransactionsResponse struct {
|
type TransactionsResponse struct {
|
||||||
Account postgres.Account
|
Account *postgres.Account
|
||||||
Transactions []postgres.DisplayTransaction
|
Transactions []postgres.DisplayTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,28 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (h *Handler) autocompleteAccounts(c *gin.Context) {
|
||||||
|
budgetID := c.Param("budgetid")
|
||||||
|
budgetUUID, err := uuid.Parse(budgetID)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{"budgetid missing from URL"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := c.Request.URL.Query().Get("s")
|
||||||
|
searchParams := postgres.SearchAccountsParams{
|
||||||
|
BudgetID: budgetUUID,
|
||||||
|
Search: "%" + query + "%",
|
||||||
|
}
|
||||||
|
categories, err := h.Service.SearchAccounts(c.Request.Context(), searchParams)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, categories)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) autocompleteCategories(c *gin.Context) {
|
func (h *Handler) autocompleteCategories(c *gin.Context) {
|
||||||
budgetID := c.Param("budgetid")
|
budgetID := c.Param("budgetid")
|
||||||
budgetUUID, err := uuid.Parse(budgetID)
|
budgetUUID, err := uuid.Parse(budgetID)
|
||||||
|
@ -65,7 +65,9 @@ func (h *Handler) LoadRoutes(router *gin.Engine) {
|
|||||||
budget.GET("/:budgetid/:year/:month", h.budgetingForMonth)
|
budget.GET("/:budgetid/:year/:month", h.budgetingForMonth)
|
||||||
budget.POST("/:budgetid/category/:categoryid/:year/:month", h.setCategoryAssignment)
|
budget.POST("/:budgetid/category/:categoryid/:year/:month", h.setCategoryAssignment)
|
||||||
budget.GET("/:budgetid/autocomplete/payees", h.autocompletePayee)
|
budget.GET("/:budgetid/autocomplete/payees", h.autocompletePayee)
|
||||||
|
budget.GET("/:budgetid/autocomplete/accounts", h.autocompleteAccounts)
|
||||||
budget.GET("/:budgetid/autocomplete/categories", h.autocompleteCategories)
|
budget.GET("/:budgetid/autocomplete/categories", h.autocompleteCategories)
|
||||||
|
budget.GET("/:budgetid/problematic-transactions", h.problematicTransactions)
|
||||||
budget.DELETE("/:budgetid", h.deleteBudget)
|
budget.DELETE("/:budgetid", h.deleteBudget)
|
||||||
budget.POST("/:budgetid/import/ynab", h.importYNAB)
|
budget.POST("/:budgetid/import/ynab", h.importYNAB)
|
||||||
budget.POST("/:budgetid/export/ynab/transactions", h.exportYNABTransactions)
|
budget.POST("/:budgetid/export/ynab/transactions", h.exportYNABTransactions)
|
||||||
|
@ -7,7 +7,10 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
// override/add rules settings here, such as:
|
// override/add rules settings here, such as:
|
||||||
'vue/max-attributes-per-line': 'off'
|
'vue/max-attributes-per-line': 'off',
|
||||||
|
'vue/singleline-html-element-content-newline': 'off',
|
||||||
|
'vue/first-attribute-linebreak': 'off',
|
||||||
|
'vue/html-closing-bracket-newline': 'off',
|
||||||
// 'vue/no-unused-vars': 'error'
|
// 'vue/no-unused-vars': 'error'
|
||||||
},
|
},
|
||||||
parser: "vue-eslint-parser",
|
parser: "vue-eslint-parser",
|
||||||
|
@ -10,37 +10,35 @@ const router = useRouter();
|
|||||||
const CurrentBudgetName = computed(() => useBudgetsStore().CurrentBudgetName);
|
const CurrentBudgetName = computed(() => useBudgetsStore().CurrentBudgetName);
|
||||||
const LoggedIn = computed(() => useSessionStore().LoggedIn);
|
const LoggedIn = computed(() => useSessionStore().LoggedIn);
|
||||||
function logout() {
|
function logout() {
|
||||||
useSessionStore().logout();
|
useSessionStore().logout();
|
||||||
router.push("/login");
|
router.push("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMenu() {
|
function toggleMenu() {
|
||||||
useSettingsStore().toggleMenu();
|
useSettingsStore().toggleMenu();
|
||||||
}
|
}
|
||||||
function toggleMenuSize() {
|
function toggleMenuSize() {
|
||||||
useSettingsStore().toggleMenuSize();
|
useSettingsStore().toggleMenuSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
router.afterEach(function(to, from) {
|
router.afterEach(function(to, from) {
|
||||||
useSettingsStore().Menu.Show = false;
|
useSettingsStore().Menu.Show = false;
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex bg-gray-400 dark:bg-gray-600 p-4 static top-0 left-0 w-full h-14">
|
<div class="flex bg-gray-400 dark:bg-gray-600 p-4 static top-0 left-0 w-full h-14">
|
||||||
<span class="flex-1 font-bold text-5xl -my-3 hidden md:inline" @click="toggleMenuSize">≡</span>
|
<span class="flex-1 font-bold text-5xl -my-3 hidden md:inline" @click="toggleMenuSize">≡</span>
|
||||||
<span class="flex-1 font-bold text-5xl -my-3 md:hidden" @click="toggleMenu">≡</span>
|
<span class="flex-1 font-bold text-5xl -my-3 md:hidden" @click="toggleMenu">≡</span>
|
||||||
|
<span class="flex-1">{{ CurrentBudgetName }}</span>
|
||||||
<span class="flex-1">{{ CurrentBudgetName }}</span>
|
<div class="flex flex-1 flex-row justify-end -mx-4">
|
||||||
|
<router-link v-if="LoggedIn" class="mx-4" to="/dashboard">
|
||||||
<div class="flex flex-1 flex-row justify-end -mx-4">
|
Dashboard
|
||||||
<router-link v-if="LoggedIn" class="mx-4" to="/dashboard">
|
</router-link>
|
||||||
Dashboard
|
<router-link v-if="!LoggedIn" class="mx-4" to="/login">
|
||||||
</router-link>
|
Login
|
||||||
<router-link v-if="!LoggedIn" class="mx-4" to="/login">
|
</router-link>
|
||||||
Login
|
<a v-if="LoggedIn" class="mx-4" @click="logout">Logout</a>
|
||||||
</router-link>
|
|
||||||
<a v-if="LoggedIn" class="mx-4" @click="logout">Logout</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -8,7 +8,8 @@ import Input from "./Input.vue";
|
|||||||
import Button from "./SimpleButton.vue";
|
import Button from "./SimpleButton.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
transactionid: string
|
transactionid: string,
|
||||||
|
withAccount: boolean,
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits(["save"]);
|
const emit = defineEmits(["save"]);
|
||||||
@ -18,67 +19,47 @@ const TX = transactionsStore.Transactions.get(props.transactionid)!;
|
|||||||
const payeeType = ref<string|undefined>(undefined);
|
const payeeType = ref<string|undefined>(undefined);
|
||||||
|
|
||||||
const payload = computed(() => JSON.stringify({
|
const payload = computed(() => JSON.stringify({
|
||||||
date: TX.Date.toISOString().split("T")[0],
|
date: TX.Date.toISOString().split("T")[0],
|
||||||
payee: {
|
payee: {
|
||||||
Name: TX.Payee,
|
Name: TX.Payee,
|
||||||
ID: TX.PayeeID,
|
ID: TX.PayeeID,
|
||||||
Type: payeeType.value,
|
Type: payeeType.value,
|
||||||
},
|
},
|
||||||
categoryId: TX.CategoryID,
|
categoryId: TX.CategoryID,
|
||||||
memo: TX.Memo,
|
memo: TX.Memo,
|
||||||
amount: TX.Amount.toString(),
|
amount: TX.Amount.toString(),
|
||||||
state: "Uncleared"
|
state: "Uncleared"
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function saveTransaction(e: MouseEvent) {
|
function saveTransaction(e: MouseEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
transactionsStore.editTransaction(TX.ID, payload.value);
|
transactionsStore.editTransaction(TX.ID, payload.value);
|
||||||
emit('save');
|
emit('save');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-sm">
|
<td class="text-sm">
|
||||||
<DateInput
|
<DateInput v-model="TX.Date" class="border-b-2 border-black" />
|
||||||
v-model="TX.Date"
|
</td>
|
||||||
class="border-b-2 border-black"
|
<td v-if="withAccount">
|
||||||
/>
|
<Autocomplete v-model:text="TX.Account" v-model:id="TX.AccountID" model="accounts" />
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Autocomplete
|
<Autocomplete v-model:text="TX.Payee" v-model:id="TX.PayeeID" v-model:type="payeeType" model="payees" />
|
||||||
v-model:text="TX.Payee"
|
|
||||||
v-model:id="TX.PayeeID"
|
|
||||||
v-model:type="payeeType"
|
|
||||||
model="payees"
|
|
||||||
/>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Autocomplete
|
<Autocomplete v-model:text="TX.Category" v-model:id="TX.CategoryID" model="categories" />
|
||||||
v-model:text="TX.Category"
|
|
||||||
v-model:id="TX.CategoryID"
|
|
||||||
model="categories"
|
|
||||||
/>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Input
|
<Input v-model="TX.Memo" class="block w-full border-b-2 border-black" type="text" />
|
||||||
v-model="TX.Memo"
|
|
||||||
class="block w-full border-b-2 border-black"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<Input
|
<Input v-model="TX.Amount" class="text-right block w-full border-b-2 border-black" type="currency" />
|
||||||
v-model="TX.Amount"
|
|
||||||
class="text-right block w-full border-b-2 border-black"
|
|
||||||
type="currency"
|
|
||||||
/>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Button
|
<Button class="bg-blue-500" @click="saveTransaction">
|
||||||
class="bg-blue-500"
|
|
||||||
@click="saveTransaction"
|
|
||||||
>
|
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
|
@ -24,7 +24,9 @@ const TX = ref<Transaction>({
|
|||||||
ID: "",
|
ID: "",
|
||||||
Status: "Uncleared",
|
Status: "Uncleared",
|
||||||
TransferAccount: "",
|
TransferAccount: "",
|
||||||
Reconciled: false
|
Reconciled: false,
|
||||||
|
Account: "",
|
||||||
|
AccountID: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const payeeType = ref<string|undefined>(undefined);
|
const payeeType = ref<string|undefined>(undefined);
|
||||||
|
@ -11,6 +11,7 @@ import Checkbox from "./Checkbox.vue";
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
transactionid: string,
|
transactionid: string,
|
||||||
index: number,
|
index: number,
|
||||||
|
withAccount: boolean,
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const edit = ref(false);
|
const edit = ref(false);
|
||||||
@ -21,19 +22,6 @@ const Reconciling = computed(() => useTransactionsStore().Reconciling);
|
|||||||
const transactionsStore = useTransactionsStore();
|
const transactionsStore = useTransactionsStore();
|
||||||
const TX = transactionsStore.Transactions.get(props.transactionid)!;
|
const TX = transactionsStore.Transactions.get(props.transactionid)!;
|
||||||
|
|
||||||
function dateChanged() {
|
|
||||||
const currentAccount = useAccountStore().CurrentAccount;
|
|
||||||
if (currentAccount == null)
|
|
||||||
return true;
|
|
||||||
const transactionIndex = currentAccount.Transactions.indexOf(props.transactionid);
|
|
||||||
if(transactionIndex<=0)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
const previousTransactionId = currentAccount.Transactions[transactionIndex-1];
|
|
||||||
const previousTransaction = transactionsStore.Transactions.get(previousTransactionId);
|
|
||||||
return TX.Date.getTime() != previousTransaction?.Date.getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStatusSymbol() {
|
function getStatusSymbol() {
|
||||||
if(TX.Status == "Reconciled")
|
if(TX.Status == "Reconciled")
|
||||||
return "✔";
|
return "✔";
|
||||||
@ -46,22 +34,17 @@ function getStatusSymbol() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<tr v-if="dateChanged()" class="table-row md:hidden">
|
|
||||||
<td class="py-2" colspan="5">
|
|
||||||
<span class="bg-gray-400 dark:bg-slate-600 rounded-lg p-1 px-2 w-full block">
|
|
||||||
{{ formatDate(TX.Date) }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr
|
<tr
|
||||||
v-if="!edit"
|
v-if="!edit"
|
||||||
class="{{new Date(TX.Date) > new Date() ? 'future' : ''}}"
|
class="{{new Date(TX.Date) > new Date() ? 'future' : ''}}"
|
||||||
:class="[index % 6 < 3 ? 'md:bg-gray-300 dark:md:bg-gray-700' : 'md:bg-gray-100 dark:md:bg-gray-900']"
|
:class="[index % 2 < 1 ? 'md:bg-gray-300 dark:md:bg-gray-700' : 'md:bg-gray-100 dark:md:bg-gray-900']"
|
||||||
>
|
>
|
||||||
<!--:class="[index % 6 < 3 ? index % 6 === 1 ? 'bg-gray-400' : 'bg-gray-300' : index % 6 !== 4 ? 'bg-gray-100' : '']">-->
|
|
||||||
<td class="hidden md:block">
|
<td class="hidden md:block">
|
||||||
{{ formatDate(TX.Date) }}
|
{{ formatDate(TX.Date) }}
|
||||||
</td>
|
</td>
|
||||||
|
<td v-if="withAccount" class="pl-2 md:pl-0">
|
||||||
|
{{ TX.Account }}
|
||||||
|
</td>
|
||||||
<td class="pl-2 md:pl-0">
|
<td class="pl-2 md:pl-0">
|
||||||
{{ TX.TransferAccount ? "Transfer : " + TX.TransferAccount : TX.Payee }}
|
{{ TX.TransferAccount ? "Transfer : " + TX.TransferAccount : TX.Payee }}
|
||||||
</td>
|
</td>
|
||||||
@ -92,6 +75,7 @@ function getStatusSymbol() {
|
|||||||
<TransactionEditRow
|
<TransactionEditRow
|
||||||
v-if="edit"
|
v-if="edit"
|
||||||
:transactionid="TX.ID"
|
:transactionid="TX.ID"
|
||||||
|
:with-account="withAccount"
|
||||||
@save="edit = false"
|
@save="edit = false"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -6,3 +6,12 @@ export function formatDate(date: Date): string {
|
|||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function groupBy<T, K extends keyof any>(list: T[], getKey: (item: T) => K) {
|
||||||
|
return list.reduce((previous, currentItem) => {
|
||||||
|
const group = getKey(currentItem);
|
||||||
|
if (!previous[group]) previous[group] = [];
|
||||||
|
previous[group].push(currentItem);
|
||||||
|
return previous;
|
||||||
|
}, {} as Record<K, T[]>);
|
||||||
|
}
|
@ -10,6 +10,7 @@ import { useTransactionsStore } from "../stores/transactions";
|
|||||||
import Modal from "../components/Modal.vue";
|
import Modal from "../components/Modal.vue";
|
||||||
import Input from "../components/Input.vue";
|
import Input from "../components/Input.vue";
|
||||||
import Checkbox from "../components/Checkbox.vue";
|
import Checkbox from "../components/Checkbox.vue";
|
||||||
|
import { formatDate } from "../date";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
budgetid: string
|
budgetid: string
|
||||||
@ -116,27 +117,13 @@ function createReconcilationTransaction() {
|
|||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr class="font-bold">
|
<tr class="font-bold">
|
||||||
<td
|
<td class="hidden md:block" style="width: 90px;">Date</td>
|
||||||
class="hidden md:block"
|
<td style="max-width: 150px;">Payee</td>
|
||||||
style="width: 90px;"
|
<td style="max-width: 200px;">Category</td>
|
||||||
>
|
|
||||||
Date
|
|
||||||
</td>
|
|
||||||
<td style="max-width: 150px;">
|
|
||||||
Payee
|
|
||||||
</td>
|
|
||||||
<td style="max-width: 200px;">
|
|
||||||
Category
|
|
||||||
</td>
|
|
||||||
<td>Memo</td>
|
<td>Memo</td>
|
||||||
<td class="text-right">
|
<td class="text-right">Amount</td>
|
||||||
Amount
|
|
||||||
</td>
|
|
||||||
<td style="width: 80px;">
|
<td style="width: 80px;">
|
||||||
<Checkbox
|
<Checkbox v-if="transactions.Reconciling" @input="setReconciled" />
|
||||||
v-if="transactions.Reconciling"
|
|
||||||
@input="setReconciled"
|
|
||||||
/>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<TransactionInputRow
|
<TransactionInputRow
|
||||||
@ -144,12 +131,22 @@ function createReconcilationTransaction() {
|
|||||||
:budgetid="budgetid"
|
:budgetid="budgetid"
|
||||||
:accountid="accountid"
|
:accountid="accountid"
|
||||||
/>
|
/>
|
||||||
<TransactionRow
|
<template v-for="(dayTransactions, key, index) in transactions.TransactionsByDate" :key="key">
|
||||||
v-for="(transaction, index) in transactions.TransactionsList"
|
<tr class="table-row md:hidden">
|
||||||
:key="transaction.ID"
|
<td class="py-2" colspan="5">
|
||||||
:transactionid="transaction.ID"
|
<span class="bg-gray-400 dark:bg-slate-600 rounded-lg p-1 px-2 w-full block">
|
||||||
:index="index"
|
{{ key }}
|
||||||
/>
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<TransactionRow
|
||||||
|
v-for="transaction in dayTransactions"
|
||||||
|
:key="transaction.ID"
|
||||||
|
:index="index"
|
||||||
|
:transactionid="transaction.ID"
|
||||||
|
:with-account="false"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</table>
|
</table>
|
||||||
<div class="md:hidden">
|
<div class="md:hidden">
|
||||||
<Modal @submit="submitModal">
|
<Modal @submit="submitModal">
|
||||||
|
69
web/src/pages/AllAccounts.vue
Normal file
69
web/src/pages/AllAccounts.vue
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref, onMounted } from "vue"
|
||||||
|
import Currency from "../components/Currency.vue";
|
||||||
|
import TransactionRow from "../components/TransactionRow.vue";
|
||||||
|
import TransactionInputRow from "../components/TransactionInputRow.vue";
|
||||||
|
import { useAccountStore } from "../stores/budget-account";
|
||||||
|
import EditAccount from "../dialogs/EditAccount.vue";
|
||||||
|
import Button from "../components/SimpleButton.vue";
|
||||||
|
import { useTransactionsStore } from "../stores/transactions";
|
||||||
|
import Modal from "../components/Modal.vue";
|
||||||
|
import Input from "../components/Input.vue";
|
||||||
|
import Checkbox from "../components/Checkbox.vue";
|
||||||
|
import { formatDate } from "../date";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
budgetid: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const modalInputRow = ref<typeof TransactionInputRow | null>(null);
|
||||||
|
|
||||||
|
function submitModal() {
|
||||||
|
modalInputRow.value!.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
const accounts = useAccountStore();
|
||||||
|
const transactions = useTransactionsStore();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
transactions.GetProblematicTransactions();
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="grid grid-cols-[1fr_auto]">
|
||||||
|
<div>
|
||||||
|
<h1 class="inline">
|
||||||
|
Problematic Transactions
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr class="font-bold">
|
||||||
|
<td class="hidden md:block" style="width: 90px;">Date</td>
|
||||||
|
<td style="max-width: 200px;">Account</td>
|
||||||
|
<td style="max-width: 150px;">Payee</td>
|
||||||
|
<td style="max-width: 200px;">Category</td>
|
||||||
|
<td>Memo</td>
|
||||||
|
<td class="text-right">Amount</td>
|
||||||
|
</tr>
|
||||||
|
<TransactionRow
|
||||||
|
v-for="(transaction, index) in transactions.ProblematicTransactionsList"
|
||||||
|
:key="transaction.ID"
|
||||||
|
:with-account="true"
|
||||||
|
:transactionid="transaction.ID"
|
||||||
|
:index="index"
|
||||||
|
/>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
.negative {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
@ -27,7 +27,7 @@ function toggleMenu() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="[ExpandMenu ? 'md:w-72' : 'md:w-36', ShowMenu ? '' : 'hidden']"
|
<div :class="[ExpandMenu ? 'md:w-72' : 'md:w-36', ShowMenu ? '' : 'hidden']"
|
||||||
class="md:block flex-shrink-0 w-full bg-gray-500 border-r-4 border-black">
|
class="md:block flex-shrink-0 w-full bg-gray-500 border-r-4 border-black">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="m-2 md:px-3 p-1">
|
<div class="m-2 md:px-3 p-1">
|
||||||
<span class="font-bold-my-3 md:hidden font-bold text-5xl pr-3" @click="toggleMenu">≡</span>
|
<span class="font-bold-my-3 md:hidden font-bold text-5xl pr-3" @click="toggleMenu">≡</span>
|
||||||
@ -40,7 +40,7 @@ function toggleMenu() {
|
|||||||
<router-link :to="'/budget/' + CurrentBudgetID + '/budgeting'">Budget</router-link>
|
<router-link :to="'/budget/' + CurrentBudgetID + '/budgeting'">Budget</router-link>
|
||||||
<br>
|
<br>
|
||||||
<!--<router-link :to="'/budget/'+CurrentBudgetID+'/reports'">Reports</router-link>-->
|
<!--<router-link :to="'/budget/'+CurrentBudgetID+'/reports'">Reports</router-link>-->
|
||||||
<!--<router-link :to="'/budget/'+CurrentBudgetID+'/all-accounts'">All Accounts</router-link>-->
|
<router-link :to="'/budget/'+CurrentBudgetID+'/problematic-transactions'">Problematic Transactions</router-link>
|
||||||
</span>
|
</span>
|
||||||
<li class="bg-slate-200 dark:bg-slate-700 my-2 p-2 px-3">
|
<li class="bg-slate-200 dark:bg-slate-700 my-2 p-2 px-3">
|
||||||
<div class="flex flex-row justify-between font-bold">
|
<div class="flex flex-row justify-between font-bold">
|
||||||
|
@ -3,7 +3,8 @@ import {
|
|||||||
createWebHistory,
|
createWebHistory,
|
||||||
RouteLocationNormalized,
|
RouteLocationNormalized,
|
||||||
} from "vue-router";
|
} from "vue-router";
|
||||||
import Account from "@/pages/Account.vue";
|
import Account from "../pages/Account.vue";
|
||||||
|
import AllAccounts from "../pages/AllAccounts.vue";
|
||||||
import Budgeting from "../pages/Budgeting.vue";
|
import Budgeting from "../pages/Budgeting.vue";
|
||||||
import BudgetSidebar from "../pages/BudgetSidebar.vue";
|
import BudgetSidebar from "../pages/BudgetSidebar.vue";
|
||||||
import Dashboard from "../pages/Dashboard.vue";
|
import Dashboard from "../pages/Dashboard.vue";
|
||||||
@ -13,7 +14,12 @@ import Settings from "../pages/Settings.vue";
|
|||||||
import WelcomePage from "../pages/WelcomePage.vue";
|
import WelcomePage from "../pages/WelcomePage.vue";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ path: "/", name: "Index", component: WelcomePage },
|
{
|
||||||
|
path: "/",
|
||||||
|
name: "Index",
|
||||||
|
component: WelcomePage,
|
||||||
|
meta: { hideForAuth: true },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/dashboard",
|
path: "/dashboard",
|
||||||
name: "Dashboard",
|
name: "Dashboard",
|
||||||
@ -58,6 +64,13 @@ const routes = [
|
|||||||
props: true,
|
props: true,
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/budget/:budgetid/problematic-transactions",
|
||||||
|
name: "Problematic Transactions",
|
||||||
|
components: { default: AllAccounts, sidebar: BudgetSidebar },
|
||||||
|
props: true,
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/budget/:budgetid/account/:accountid",
|
path: "/budget/:budgetid/account/:accountid",
|
||||||
name: "Account",
|
name: "Account",
|
||||||
|
@ -201,10 +201,9 @@ export const useAccountStore = defineStore("budget/account", {
|
|||||||
);
|
);
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
const transactionsStore = useTransactionsStore();
|
const transactionsStore = useTransactionsStore();
|
||||||
const transactions = transactionsStore.AddTransactions(
|
transactionsStore.AddTransactions(
|
||||||
response.Transactions
|
response.Transactions
|
||||||
);
|
);
|
||||||
account.Transactions = transactions;
|
|
||||||
},
|
},
|
||||||
async FetchMonthBudget(budgetid: string, year: number, month: number) {
|
async FetchMonthBudget(budgetid: string, year: number, month: number) {
|
||||||
const result = await GET(
|
const result = await GET(
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { POST } from "../api";
|
import { GET, POST } from "../api";
|
||||||
|
import { formatDate, groupBy } from "../date";
|
||||||
|
import { useBudgetsStore } from "./budget";
|
||||||
import { useAccountStore } from "./budget-account";
|
import { useAccountStore } from "./budget-account";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
Transactions: Map<string, Transaction>;
|
Transactions: Map<string, Transaction>;
|
||||||
Reconciling: boolean;
|
Reconciling: boolean;
|
||||||
|
ProblematicTransactions: Array<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Transaction {
|
export interface Transaction {
|
||||||
@ -21,12 +24,15 @@ export interface Transaction {
|
|||||||
PayeeID: string | undefined;
|
PayeeID: string | undefined;
|
||||||
Amount: number;
|
Amount: number;
|
||||||
Reconciled: boolean;
|
Reconciled: boolean;
|
||||||
|
Account: string;
|
||||||
|
AccountID: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useTransactionsStore = defineStore("budget/transactions", {
|
export const useTransactionsStore = defineStore("budget/transactions", {
|
||||||
state: (): State => ({
|
state: (): State => ({
|
||||||
Transactions: new Map<string, Transaction>(),
|
Transactions: new Map<string, Transaction>(),
|
||||||
Reconciling: false,
|
Reconciling: false,
|
||||||
|
ProblematicTransactions: new Array<string>(),
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
ReconcilingBalance(state): number {
|
ReconcilingBalance(state): number {
|
||||||
@ -39,24 +45,29 @@ export const useTransactionsStore = defineStore("budget/transactions", {
|
|||||||
}
|
}
|
||||||
return reconciledBalance;
|
return reconciledBalance;
|
||||||
},
|
},
|
||||||
TransactionsList(state): Transaction[] {
|
TransactionsByDate(state) : Record<string, Transaction[]> {
|
||||||
const accountsStore = useAccountStore();
|
const accountsStore = useAccountStore();
|
||||||
return accountsStore.CurrentAccount!.Transactions.map((x) => {
|
const allTransactions = [...this.Transactions.values()];
|
||||||
return this.Transactions.get(x)!;
|
return groupBy(allTransactions, x => formatDate(x.Date));
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
TransactionsList(state) : Transaction[] {
|
||||||
|
const accountsStore = useAccountStore();
|
||||||
|
const accountID = accountsStore.CurrentAccountID;
|
||||||
|
const allTransactions = [...this.Transactions.values()];
|
||||||
|
return allTransactions.filter(x => x.AccountID == accountID);
|
||||||
|
},
|
||||||
|
ProblematicTransactionsList(state) : Transaction[] {
|
||||||
|
return [...state.ProblematicTransactions.map(x => state.Transactions!.get(x)!)];
|
||||||
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
AddTransactions(transactions: Array<Transaction>) {
|
AddTransactions(transactions: Array<Transaction>) {
|
||||||
const transactionIds = [] as Array<string>;
|
|
||||||
this.$patch(() => {
|
this.$patch(() => {
|
||||||
for (const transaction of transactions) {
|
for (const transaction of transactions) {
|
||||||
transaction.Date = new Date(transaction.Date);
|
transaction.Date = new Date(transaction.Date);
|
||||||
this.Transactions.set(transaction.ID, transaction);
|
this.Transactions.set(transaction.ID, transaction);
|
||||||
transactionIds.push(transaction.ID);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return transactionIds;
|
|
||||||
},
|
},
|
||||||
SetReconciledForAllTransactions(value: boolean) {
|
SetReconciledForAllTransactions(value: boolean) {
|
||||||
for (const transaction of this.TransactionsList) {
|
for (const transaction of this.TransactionsList) {
|
||||||
@ -65,6 +76,13 @@ export const useTransactionsStore = defineStore("budget/transactions", {
|
|||||||
transaction.Reconciled = value;
|
transaction.Reconciled = value;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async GetProblematicTransactions() {
|
||||||
|
const budgetStore = useBudgetsStore();
|
||||||
|
const result = await GET("/budget/" + budgetStore.CurrentBudgetID + "/problematic-transactions");
|
||||||
|
const response = await result.json();
|
||||||
|
this.AddTransactions(response.Transactions);
|
||||||
|
this.ProblematicTransactions = [...response.Transactions.map((x : Transaction) => x.ID)];
|
||||||
|
},
|
||||||
async SubmitReconcilation(reconciliationTransactionAmount: number) {
|
async SubmitReconcilation(reconciliationTransactionAmount: number) {
|
||||||
const accountsStore = useAccountStore();
|
const accountsStore = useAccountStore();
|
||||||
const account = accountsStore.CurrentAccount!;
|
const account = accountsStore.CurrentAccount!;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user