Build out more frontend views
Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
parent
a5e349e776
commit
69c21ad37b
18 changed files with 745 additions and 79 deletions
|
@ -1,5 +1,8 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: [
|
extends: [
|
||||||
'nextcloud'
|
'nextcloud'
|
||||||
]
|
],
|
||||||
|
"rules": {
|
||||||
|
"indent": ["error", 4]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -23,7 +23,7 @@ return [
|
||||||
[
|
[
|
||||||
'name' => 'transaction#sum',
|
'name' => 'transaction#sum',
|
||||||
'url' => '/api/v1.0/transactions/sum',
|
'url' => '/api/v1.0/transactions/sum',
|
||||||
'verb' => 'POST',
|
'verb' => 'GET',
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,3 +1,22 @@
|
||||||
#hello {
|
:root {
|
||||||
color: red;
|
--good-color: green;
|
||||||
|
--warn-color: yellow;
|
||||||
|
--danger-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2, h3 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.good {
|
||||||
|
color: var(--good-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warn {
|
||||||
|
color: var(--warn-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger {
|
||||||
|
color: var(--danger-color);
|
||||||
}
|
}
|
|
@ -176,4 +176,14 @@ class BudgetController extends Controller
|
||||||
$this->budgetMapper->delete($budget);
|
$this->budgetMapper->delete($budget);
|
||||||
return new DataResponse($budget);
|
return new DataResponse($budget);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function stats(int $budgetId) {
|
||||||
|
try {
|
||||||
|
$userPermission = $this->userPermissionMapper->find($id, $this->userId);
|
||||||
|
$budget = $this->budgetMapper->find($userPermission->getBudgetId());
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,8 @@ class TransactionController extends Controller
|
||||||
private $transactionMapper;
|
private $transactionMapper;
|
||||||
private $userPermissionMapper;
|
private $userPermissionMapper;
|
||||||
private $logger;
|
private $logger;
|
||||||
|
private $DATE_FORMAT = DateTime::RFC3339_EXTENDED;
|
||||||
|
private const DATE_FORMAT = "Y-m-d\TH:i:s.v\Z";
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
$AppName,
|
$AppName,
|
||||||
|
@ -49,16 +51,19 @@ class TransactionController extends Controller
|
||||||
* @NoAdminRequired
|
* @NoAdminRequired
|
||||||
* @NoCSRFRequired
|
* @NoCSRFRequired
|
||||||
*/
|
*/
|
||||||
public function index()
|
public function index(?int $budgetId, ?int $categoryId, ?int $count)
|
||||||
{
|
{
|
||||||
$budgetId = $_GET['budgetId'];
|
try {
|
||||||
$categoryId = $_GET['categoryId'];
|
if ($budgetId != null) {
|
||||||
if ($budgetId == null) {
|
$this->userPermissionMapper->find($budgetId, $this->userId);
|
||||||
|
} else if ($categoryId != null) {
|
||||||
|
$category = $this->categoryMapper->find($categoryId);
|
||||||
|
$budgetId = $category->getBudgetId();
|
||||||
|
$this->userPermissionMapper->find($budgetId, $this->userId);
|
||||||
|
} else {
|
||||||
return new DataResponse([], Http::STATUS_BAD_REQUEST);
|
return new DataResponse([], Http::STATUS_BAD_REQUEST);
|
||||||
}
|
}
|
||||||
try {
|
return new DataResponse($this->transactionMapper->findAll($budgetId, $categoryId, $count));
|
||||||
$this->userPermissionMapper->find($budgetId, $this->userId);
|
|
||||||
return new DataResponse($this->transactionMapper->findAll($budgetId, $categoryId));
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
@ -95,7 +100,7 @@ class TransactionController extends Controller
|
||||||
*/
|
*/
|
||||||
public function create(
|
public function create(
|
||||||
string $name,
|
string $name,
|
||||||
string $description,
|
?string $description,
|
||||||
int $amount,
|
int $amount,
|
||||||
string $date,
|
string $date,
|
||||||
bool $expense,
|
bool $expense,
|
||||||
|
@ -115,9 +120,9 @@ class TransactionController extends Controller
|
||||||
$transaction->setDescription($description);
|
$transaction->setDescription($description);
|
||||||
$transaction->setAmount($amount);
|
$transaction->setAmount($amount);
|
||||||
$transaction->setExpense($expense);
|
$transaction->setExpense($expense);
|
||||||
$dateTime = DateTime::createFromFormat(DateTime::ATOM, $date);
|
$dateTime = DateTime::createFromFormat($this->DATE_FORMAT, $date);
|
||||||
if (!$dateTime) {
|
if (!$dateTime) {
|
||||||
return new DataResponse([], Http::STATUS_BAD_REQUEST);
|
return new DataResponse(["message" => "Invalid date format: '$date'"], Http::STATUS_BAD_REQUEST);
|
||||||
}
|
}
|
||||||
$transaction->setDate($dateTime->getTimestamp());
|
$transaction->setDate($dateTime->getTimestamp());
|
||||||
$this->logger->error("Setting category $categoryId for new transaction");
|
$this->logger->error("Setting category $categoryId for new transaction");
|
||||||
|
@ -168,7 +173,7 @@ class TransactionController extends Controller
|
||||||
$transaction->setDescription($description);
|
$transaction->setDescription($description);
|
||||||
$transaction->setAmount($amount);
|
$transaction->setAmount($amount);
|
||||||
$transaction->setExpense($expense);
|
$transaction->setExpense($expense);
|
||||||
$dateTime = DateTime::createFromFormat(DateTime::ATOM, $date);
|
$dateTime = DateTime::createFromFormat($this->DATE_FORMAT, $date);
|
||||||
if (!$dateTime) {
|
if (!$dateTime) {
|
||||||
return new DataResponse([], Http::STATUS_BAD_REQUEST);
|
return new DataResponse([], Http::STATUS_BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
@ -235,7 +240,7 @@ class TransactionController extends Controller
|
||||||
);
|
);
|
||||||
$startDateTime->setTime(0, 0, 0, 0);
|
$startDateTime->setTime(0, 0, 0, 0);
|
||||||
} else {
|
} else {
|
||||||
$startDateTime = DateTime::createFromFormat(DateTime::ATOM, $startDate);
|
$startDateTime = DateTime::createFromFormat($this->DATE_FORMAT, $startDate);
|
||||||
}
|
}
|
||||||
if (!$startDateTime) {
|
if (!$startDateTime) {
|
||||||
return new DataResponse([], Http::STATUS_BAD_REQUEST);
|
return new DataResponse([], Http::STATUS_BAD_REQUEST);
|
||||||
|
@ -250,7 +255,7 @@ class TransactionController extends Controller
|
||||||
);
|
);
|
||||||
$endDateTime->setTime(23, 59, 59, 999);
|
$endDateTime->setTime(23, 59, 59, 999);
|
||||||
} else {
|
} else {
|
||||||
$endDateTime = DateTime::createFromFormat(DateTime::ATOM, $endDate);
|
$endDateTime = DateTime::createFromFormat($this->DATE_FORMAT, $endDate);
|
||||||
}
|
}
|
||||||
if (!$endDateTime) {
|
if (!$endDateTime) {
|
||||||
return new DataResponse([], Http::STATUS_BAD_REQUEST);
|
return new DataResponse([], Http::STATUS_BAD_REQUEST);
|
||||||
|
|
|
@ -29,8 +29,11 @@ class TransactionMapper extends QBMapper
|
||||||
return $this->findEntity($qb);
|
return $this->findEntity($qb);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findAll(int $budgetId, ?int $categoryId)
|
public function findAll(
|
||||||
{
|
int $budgetId,
|
||||||
|
?int $categoryId,
|
||||||
|
?int $count
|
||||||
|
) {
|
||||||
$qb = $this->db->getQueryBuilder();
|
$qb = $this->db->getQueryBuilder();
|
||||||
|
|
||||||
$qb->select('*')
|
$qb->select('*')
|
||||||
|
@ -45,6 +48,12 @@ class TransactionMapper extends QBMapper
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$qb->orderBy('date', 'desc');
|
||||||
|
|
||||||
|
if ($count) {
|
||||||
|
$qb->setMaxResults($count);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->findEntities($qb);
|
return $this->findEntities($qb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,4 +124,8 @@ class TransactionMapper extends QBMapper
|
||||||
$statement->execute();
|
$statement->execute();
|
||||||
return (int) $statement->fetch(FetchMode::COLUMN);
|
return (int) $statement->fetch(FetchMode::COLUMN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function countByBudgetId(int $budgetId) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
10
src/App.vue
10
src/App.vue
|
@ -25,3 +25,13 @@ export default {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
<style>
|
||||||
|
.app-twigs {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-twigs > div {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
100
src/components/AddEditTransaction.vue
Normal file
100
src/components/AddEditTransaction.vue
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="!loading && transaction" class="add-edit-transaction">
|
||||||
|
<h2>{{ transaction.id ? 'Edit' : 'Add' }} Transaction</h2>
|
||||||
|
<input v-model="transaction.name" type="text" placeholder="Name" title="Name" />
|
||||||
|
<textarea v-model="transaction.description" placeholder="Description" title="Description"></textarea>
|
||||||
|
<input v-model.number="transaction.amount" type="number" placeholder="Amount" title="Amount" />
|
||||||
|
<DatetimePicker :value="transaction.date" type="datetime" />
|
||||||
|
<div class="radio-container">
|
||||||
|
<input v-model="transaction.expense" type="radio" id="expense" :value="true" />
|
||||||
|
<label for="expense">Expense</label>
|
||||||
|
<input v-model="transaction.expense" type="radio" id="income" :value="false" />
|
||||||
|
<label for="income">Income</label>
|
||||||
|
</div>
|
||||||
|
<select v-model="transaction.budgetId" v-on:change="updateCategories()">
|
||||||
|
<option disabled value>Select a budget</option>
|
||||||
|
<option v-for="budget in budgets" :key="budget.id" :value="budget.id">{{ budget.name }}</option>
|
||||||
|
</select>
|
||||||
|
<select v-model="transaction.categoryId">
|
||||||
|
<option disabled value>Select a category</option>
|
||||||
|
<option
|
||||||
|
v-for="category in filteredCategories"
|
||||||
|
:key="category.id"
|
||||||
|
:value="category.id"
|
||||||
|
>{{ category.name }}</option>
|
||||||
|
</select>
|
||||||
|
<button @click="saveTransaction()">Save Transaction</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="loading" class="icon-loading"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { DatetimePicker } from "@nextcloud/vue/dist/Components/DatetimePicker";
|
||||||
|
import { mapGetters } from "vuex";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "add-edit-transaction",
|
||||||
|
components: {
|
||||||
|
DatetimePicker
|
||||||
|
},
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
transaction: Object,
|
||||||
|
loading: true
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(["budgets"]),
|
||||||
|
filteredCategories: function(state) {
|
||||||
|
return this.$store.getters.categories.filter(function(category) {
|
||||||
|
return category.expense === state.transaction.expense;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateCategories() {
|
||||||
|
this.$store.dispatch(
|
||||||
|
"addEditTransactionBudgetSelected",
|
||||||
|
this.transaction.budgetId
|
||||||
|
);
|
||||||
|
},
|
||||||
|
saveTransaction() {
|
||||||
|
this.loading = true
|
||||||
|
this.$store.dispatch('addEditTransactionSaveClicked', this.transaction)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.$route.params.id) {
|
||||||
|
this.transaction = this.$store.getters.transaction(this.$route.params.id);
|
||||||
|
} else {
|
||||||
|
this.transaction = {
|
||||||
|
date: new Date(),
|
||||||
|
expense: true,
|
||||||
|
budgetId: this.$store.state.currentBudget,
|
||||||
|
categoryId: this.$store.state.currentCategory
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
this.updateCategories();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.add-edit-transaction > * {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
.radio-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-container label {
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
.icon-loading {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,39 +1,110 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="budget">
|
<div v-if="budget">
|
||||||
<h1 v-text="budget.name"></h1>
|
<div class="header">
|
||||||
<div class="card">
|
<div class="header-info">
|
||||||
|
<h2>{{ budget.name }}</h2>
|
||||||
|
<h3
|
||||||
|
v-if="balance"
|
||||||
|
>Balance: {{ balance.toLocaleString(undefined, {style: 'currency', currency: 'USD'}) }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button @click="addTransaction()"><span class="icon-add"></span> Add Transaction</button>
|
||||||
|
<button><span class="icon-add"></span> Add Category</button>
|
||||||
|
<Actions>
|
||||||
|
<ActionButton icon="icon-edit" text="Edit" @click="alert('Delete')">Edit</ActionButton>
|
||||||
|
<ActionButton icon="icon-delete" text="Delete" @click="alert('Delete')">Delete</ActionButton>
|
||||||
|
</Actions>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="budget-details">
|
||||||
|
<div class="card income">
|
||||||
|
<h3>Income</h3>
|
||||||
|
<CategoryList v-bind:budget-id="budget.id" v-bind:expense="false"></CategoryList>
|
||||||
|
</div>
|
||||||
|
<div class="card expenses">
|
||||||
|
<h3>Expenses</h3>
|
||||||
<CategoryList v-bind:budget-id="budget.id" v-bind:expense="true"></CategoryList>
|
<CategoryList v-bind:budget-id="budget.id" v-bind:expense="true"></CategoryList>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card transactions">
|
||||||
<CategoryList v-bind:budget-id="budget.id" v-bind:expense="false"></CategoryList>
|
<h3>Recent Transactions</h3>
|
||||||
|
<TransactionList :budget-id="budget.id"></TransactionList>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from "vuex";
|
import { mapGetters, mapState } from "vuex";
|
||||||
|
import { Actions } from "@nextcloud/vue/dist/Components/Actions";
|
||||||
|
import { ActionButton } from "@nextcloud/vue/dist/Components/ActionButton";
|
||||||
import CategoryList from "./CategoryList";
|
import CategoryList from "./CategoryList";
|
||||||
|
import TransactionList from "./TransactionList";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "budget-details",
|
name: "budget-details",
|
||||||
components: {
|
components: {
|
||||||
CategoryList
|
Actions,
|
||||||
|
ActionButton,
|
||||||
|
CategoryList,
|
||||||
|
TransactionList
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapState(["budgets", "currentBudget"]),
|
||||||
budget: function(state) {
|
budget: function(state) {
|
||||||
const budgetId = this.$route.params.id;
|
if (state.budgets.length === 0 || !state.currentBudget) {
|
||||||
const budget = this.$store.getters.budgets.find(
|
return false;
|
||||||
budget => budget.id === budgetId
|
}
|
||||||
);
|
return state.budgets.find(budget => budget.id === state.currentBudget);
|
||||||
return budget;
|
},
|
||||||
|
balance: function(state) {
|
||||||
|
if (!state.currentBudget) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return this.$store.getters.budgetBalance(state.currentBudget) / 100;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
load() {
|
load() {
|
||||||
this.$store.dispatch("budgetDetailsViewed", this.$route.params.id);
|
this.$store.dispatch("budgetDetailsViewed", this.$route.params.id);
|
||||||
|
},
|
||||||
|
addTransaction() {
|
||||||
|
this.$store.dispatch('addTransactionClicked')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.load()
|
this.load();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-details {
|
||||||
|
display: grid;
|
||||||
|
justify-content: space-between;
|
||||||
|
grid-gap: 0.5em;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.income {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expenses {
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transactions {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,13 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<ul>
|
||||||
|
<AppNavigationNew text="New Budget"></AppNavigationNew>
|
||||||
<AppNavigationItem
|
<AppNavigationItem
|
||||||
v-for="budget in budgets"
|
v-for="budget in budgets"
|
||||||
:key="budget.id"
|
:key="budget.id"
|
||||||
:title="budget.name"
|
:title="budget.name"
|
||||||
v-on:click="view(budget.id)"
|
:to="{ name: 'budgetDetails', params: { id: budget.id } }"
|
||||||
></AppNavigationItem>
|
/>
|
||||||
<AppNavigationNew text="New Budget"></AppNavigationNew>
|
</ul>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { AppNavigationItem } from "@nextcloud/vue/dist/Components/AppNavigationItem";
|
import { AppNavigationItem } from "@nextcloud/vue/dist/Components/AppNavigationItem";
|
||||||
|
@ -27,9 +27,6 @@ export default {
|
||||||
load: function() {
|
load: function() {
|
||||||
this.$store.dispatch('budgetListViewed')
|
this.$store.dispatch('budgetListViewed')
|
||||||
},
|
},
|
||||||
view: function(id) {
|
|
||||||
this.$router.push({ name: "budgetDetails", params: { id: id } })
|
|
||||||
},
|
|
||||||
new: function() {}
|
new: function() {}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
43
src/components/CategoryDetails.vue
Normal file
43
src/components/CategoryDetails.vue
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="category" class="category-details">
|
||||||
|
<h2>{{ category.name }}</h2>
|
||||||
|
<h3 v-if="balance">Balance: {{ balance.toLocaleString(undefined, {style: 'currency', currency: 'USD'}) }}</h3>
|
||||||
|
<h3>Transactions</h3>
|
||||||
|
<TransactionList :category-id="category.id"></TransactionList>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters, mapState } from "vuex";
|
||||||
|
import TransactionList from './TransactionList'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "category-details",
|
||||||
|
components: {
|
||||||
|
TransactionList
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(["categories", "currentCategory"]),
|
||||||
|
category: function(state) {
|
||||||
|
if (state.categories.length === 0 || !state.currentCategory) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return state.categories.find((category) => category.id === Number.parseInt(state.currentCategory));
|
||||||
|
},
|
||||||
|
balance: function(state) {
|
||||||
|
if (!state.currentCategory) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return this.$store.getters.categoryBalance(state.currentCategory) / 100;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
load() {
|
||||||
|
this.$store.dispatch("categoryDetailsViewed", this.$route.params.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
console.log("CategoryDetails mounted")
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -1,29 +1,73 @@
|
||||||
<template>
|
<template>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="category in categories" :key="category.id">
|
<li v-for="category in filteredCategories" :key="category.id">
|
||||||
<p>{{ category.name }}</p>
|
<a v-on:click="view(category.id)" class="category-summary">
|
||||||
|
<div class="category-info">
|
||||||
|
<p class="category-name">{{ category.name }}</p>
|
||||||
|
<p
|
||||||
|
class="category-balance"
|
||||||
|
>{{ category.expense ? 'Remaining' : 'Pending' }}: {{ (categoryRemainingBalance(category) / 100).toLocaleString(undefined, {style: 'currency', currency: 'USD'}) }}</p>
|
||||||
|
</div>
|
||||||
|
<ProgressBar
|
||||||
|
:max="category.amount"
|
||||||
|
:value="Math.abs(categoryBalance(category.id))"
|
||||||
|
:invert-colors="!category.expense"
|
||||||
|
></ProgressBar>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from "vuex";
|
import { mapGetters, mapState } from "vuex";
|
||||||
|
import ProgressBar from "./ProgressBar";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "category-list",
|
name: "category-list",
|
||||||
components: {},
|
components: {
|
||||||
|
ProgressBar
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
budgetId: Number,
|
budgetId: Number,
|
||||||
expense: Boolean
|
expense: Boolean
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
categories: function(state) {
|
...mapState(["categories", "currentCategory"]),
|
||||||
const categories = this.$store.getters.categories(this.budgetId)
|
...mapGetters(["categoryBalance", "categoryRemainingBalance"]),
|
||||||
if (categories) {
|
filteredCategories: function(state) {
|
||||||
return categories.filter(category => category.expense === this.expense)
|
return state.categories.filter(
|
||||||
} else {
|
category => category.expense === this.expense
|
||||||
return [];
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
view: function(id) {
|
||||||
|
this.$store.dispatch("categoryClicked", id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
<style>
|
||||||
|
.category-summary {
|
||||||
|
padding: 0.5em;
|
||||||
|
height: 4em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-summary * {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-summary:hover {
|
||||||
|
background: var(--color-background-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-summary .category-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
57
src/components/ProgressBar.vue
Normal file
57
src/components/ProgressBar.vue
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<template>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress" :class="status" :style="{ width: progress + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "progress-bar",
|
||||||
|
props: {
|
||||||
|
value: Number,
|
||||||
|
max: Number,
|
||||||
|
invertColors: Boolean
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
progress: function(state) {
|
||||||
|
return Math.round((this.value / this.max) * 100);
|
||||||
|
},
|
||||||
|
status: function(state) {
|
||||||
|
if (this.progress <= 33) {
|
||||||
|
return this.invertColors ? "danger" : "good";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.progress <= 66) {
|
||||||
|
return "warn";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.progress <= 100) {
|
||||||
|
return this.invertColors ? "good" : "danger";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.progress-bar {
|
||||||
|
height: 0.5em;
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 1em;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.progress {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
transition: width ease-in-out 0.5s;
|
||||||
|
}
|
||||||
|
.progress.good {
|
||||||
|
background: var(--good-color);
|
||||||
|
}
|
||||||
|
.progress.warn {
|
||||||
|
background: var(--warn-color);
|
||||||
|
}
|
||||||
|
.progress.danger {
|
||||||
|
background: var(--danger-color);
|
||||||
|
}
|
||||||
|
</style>
|
64
src/components/TransactionDetails.vue
Normal file
64
src/components/TransactionDetails.vue
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="transaction" class="transaction-details">
|
||||||
|
<h2>{{ transaction.name }}</h2>
|
||||||
|
<h3
|
||||||
|
:class="transaction.expense ? 'danger' : 'good'"
|
||||||
|
>{{ (transaction.amount / 100).toLocaleString(undefined, {style: 'currency', currency: 'USD'}) }} {{ transaction.expense ? 'Expense' : 'Income' }}</h3>
|
||||||
|
<p class="transaction-info date">{{ new Date(transaction.date).toLocaleDateString() }}</p>
|
||||||
|
<p class="transaction-info description">{{ transaction.description }}</p>
|
||||||
|
<p v-if="category" class="transaction-info category">Category: {{ category.name }}</p>
|
||||||
|
<p v-if="budget" class="transaction-info budget">Budget: {{ budget.name }}</p>
|
||||||
|
<p class="transaction-info registered-by">
|
||||||
|
Registered By:
|
||||||
|
<UserBubble
|
||||||
|
:user="transaction.createdBy"
|
||||||
|
:display-name="transaction.createdBy"
|
||||||
|
></UserBubble>
|
||||||
|
{{ new Date(transaction.createdDate).toLocaleDateString() }}
|
||||||
|
</p>
|
||||||
|
<p v-if="transaction.updatedBy" class="transaction-info updated-by">
|
||||||
|
Updated By:
|
||||||
|
<UserBubble
|
||||||
|
:user="transaction.updatedBy"
|
||||||
|
:display-name="transaction.updatedBy"
|
||||||
|
></UserBubble>
|
||||||
|
{{ new Date(transaction.updatedDate).toLocaleDateString() }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters, mapState } from "vuex";
|
||||||
|
import { UserBubble } from "@nextcloud/vue/dist/Components/UserBubble";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "transaction-details",
|
||||||
|
components: {
|
||||||
|
UserBubble
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(["transaction"]),
|
||||||
|
category: function(state, getters) {
|
||||||
|
const transaction = this.$store.getters.transaction
|
||||||
|
if (!transaction || !transaction.categoryId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this.$store.getters.category(transaction.categoryId)
|
||||||
|
},
|
||||||
|
budget: function(state) {
|
||||||
|
const transaction = this.$store.getters.transaction
|
||||||
|
if (!transaction || !transaction.budgetId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this.$store.getters.budget(transaction.budgetId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
load() {
|
||||||
|
this.$store.dispatch("transactionDetailsViewed", this.$route.params.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
74
src/components/TransactionList.vue
Normal file
74
src/components/TransactionList.vue
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
<template>
|
||||||
|
<ul>
|
||||||
|
<li v-for="transaction in filteredTransactions" :key="transaction.id">
|
||||||
|
<a v-on:click="view(transaction.id)" class="transaction">
|
||||||
|
<div class="transaction-details">
|
||||||
|
<p class="transaction-name">{{ transaction.name }}</p>
|
||||||
|
<p class="transaction-date">{{ new Date(transaction.date).toLocaleDateString() }}</p>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="transaction-amount"
|
||||||
|
:class="transaction.expense ? 'danger' : 'good'"
|
||||||
|
>{{ (transaction.amount / 100).toLocaleString(undefined, {style: 'currency', currency: 'USD'}) }}</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters, mapState } from "vuex";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "transaction-list",
|
||||||
|
components: {
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
budgetId: Number,
|
||||||
|
categoryId: Number,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(["transactions", "currentTransaction"]),
|
||||||
|
filteredTransactions: function(state) {
|
||||||
|
return state.transactions.filter(function(transaction) {
|
||||||
|
console.log(transaction.date)
|
||||||
|
if (state.budgetId) {
|
||||||
|
return transaction.budgetId === state.budgetId
|
||||||
|
}
|
||||||
|
if (state.categoryId) {
|
||||||
|
return transaction.categoryId === state.categoryId
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
view: function(id) {
|
||||||
|
this.$store.dispatch("transactionClicked", id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.transaction {
|
||||||
|
padding: 0.5em;
|
||||||
|
height: 4em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction * {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction:hover {
|
||||||
|
background: var(--color-background-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,6 +1,9 @@
|
||||||
import VueRouter from 'vue-router'
|
import VueRouter from 'vue-router'
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import BudgetDetails from '../components/BudgetDetails'
|
import BudgetDetails from '../components/BudgetDetails'
|
||||||
|
import CategoryDetails from '../components/CategoryDetails'
|
||||||
|
import AddEditTransaction from '../components/AddEditTransaction'
|
||||||
|
import TransactionDetails from '../components/TransactionDetails'
|
||||||
|
|
||||||
Vue.use(VueRouter)
|
Vue.use(VueRouter)
|
||||||
|
|
||||||
|
@ -9,7 +12,27 @@ const routes = [
|
||||||
path: '/budgets/:id',
|
path: '/budgets/:id',
|
||||||
name: 'budgetDetails',
|
name: 'budgetDetails',
|
||||||
component: BudgetDetails,
|
component: BudgetDetails,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
path: '/categories/:id',
|
||||||
|
name: 'categoryDetails',
|
||||||
|
component: CategoryDetails,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/transactions/new',
|
||||||
|
name: 'newTransaction',
|
||||||
|
component: AddEditTransaction,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/transactions/:id',
|
||||||
|
name: 'transactionDetails',
|
||||||
|
component: TransactionDetails,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/transactions/:id/edit',
|
||||||
|
name: 'editTransaction',
|
||||||
|
component: AddEditTransaction,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export default new VueRouter({
|
export default new VueRouter({
|
||||||
|
|
|
@ -1,56 +1,189 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
import axios from '@nextcloud/axios'
|
import axios from '@nextcloud/axios'
|
||||||
|
import router from '../router'
|
||||||
|
|
||||||
Vue.use(Vuex)
|
Vue.use(Vuex)
|
||||||
|
|
||||||
export default new Vuex.Store({
|
export default new Vuex.Store({
|
||||||
state: {
|
state: {
|
||||||
budgets: [],
|
budgets: [],
|
||||||
|
budgetBalances: {},
|
||||||
currentBudget: 0,
|
currentBudget: 0,
|
||||||
categories: {},
|
categories: [],
|
||||||
|
categoryBalances: {},
|
||||||
currentCategory: 0,
|
currentCategory: 0,
|
||||||
transactions: [],
|
transactions: [],
|
||||||
currentTransaction: 0,
|
currentTransaction: 0,
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
budgets: (state) => {
|
budgets: (state) => state.budgets,
|
||||||
return state.budgets
|
budget: (state) => (id) => state.budgets.find(budget => budget.id === id),
|
||||||
|
budgetBalance: (state) => (id) => state.budgetBalances[id],
|
||||||
|
categories: (state) => state.categories,
|
||||||
|
category: (state) => (id) => {
|
||||||
|
return state.categories.find(category => category.id === id)
|
||||||
},
|
},
|
||||||
budget: (state) => (id) => {
|
categoryBalance: (state) => (categoryId) => {
|
||||||
return state.budgets.find(budget => budget.id === id)
|
return state.categoryBalances[categoryId];
|
||||||
},
|
},
|
||||||
categories: (state) => (budgetId) => {
|
categoryRemainingBalance: (state, getters) => (category) => {
|
||||||
return state.categories[budgetId]
|
const modifier = category.expense ? -1 : 1;
|
||||||
|
return category.amount - (getters.categoryBalance(category.id) * modifier);
|
||||||
},
|
},
|
||||||
|
transactions: (state) => state.transactions,
|
||||||
|
transaction: (state) => state.transactions.find(transaction => transaction.id === state.currentTransaction),
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
budgetListViewed({ commit }) {
|
budgetListViewed({ commit }) {
|
||||||
axios.get(OC.generateUrl('/apps/twigs/api/v1.0/budgets'))
|
axios.get(OC.generateUrl('/apps/twigs/api/v1.0/budgets'))
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
commit('setBudgets', response.data)
|
commit('setBudgets', response.data)
|
||||||
})
|
response.data.forEach(budget => {
|
||||||
},
|
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/transactions/sum?budgetId=${budget.id}`))
|
||||||
budgetDetailsViewed({ commit }, budgetId) {
|
|
||||||
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/categories?budgetId=${budgetId}`))
|
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
commit({
|
commit({
|
||||||
type: 'setCategories',
|
type: 'setBudgetBalance',
|
||||||
budgetId: budgetId,
|
...response.data
|
||||||
categories: response.data
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
budgetClicked({ commit }, budgetId) {
|
||||||
|
router.push({ name: "budgetDetails", params: { id: budgetId } })
|
||||||
|
},
|
||||||
|
budgetDetailsViewed({ commit }, budgetId) {
|
||||||
|
commit('setCurrentBudget', budgetId)
|
||||||
|
commit('setCategories', [])
|
||||||
|
commit('setTransactions', [])
|
||||||
|
commit('setCurrentCategory', undefined)
|
||||||
|
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/categories?budgetId=${budgetId}`))
|
||||||
|
.then(function (response) {
|
||||||
|
commit('setCategories', response.data)
|
||||||
|
response.data.forEach(category => {
|
||||||
|
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/transactions/sum?categoryId=${category.id}`))
|
||||||
|
.then(function (response) {
|
||||||
|
commit({
|
||||||
|
type: 'setCategoryBalance',
|
||||||
|
...response.data
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
})
|
||||||
|
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/transactions?budgetId=${budgetId}?count=10`))
|
||||||
|
.then((response) => commit('setTransactions', response.data))
|
||||||
|
},
|
||||||
|
categoryClicked({ commit }, categoryId) {
|
||||||
|
router.push({ name: "categoryDetails", params: { id: categoryId } })
|
||||||
|
},
|
||||||
|
categoryDetailsViewed({ commit, state }, categoryId) {
|
||||||
|
commit('setCurrentCategory', categoryId)
|
||||||
|
if (state.categories.length === 0) {
|
||||||
|
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/categories/${categoryId}`))
|
||||||
|
.then((response) => {
|
||||||
|
commit('setCategories', [response.data])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/transactions?categoryId=${categoryId}`))
|
||||||
|
.then((response) => commit('setTransactions', response.data))
|
||||||
|
},
|
||||||
|
addTransactionClicked({ commit }) {
|
||||||
|
router.push({ name: "newTransaction" })
|
||||||
|
},
|
||||||
|
addEditTransactionViewed({ commit, state, getters }, transactionId) {
|
||||||
|
if (transactionId && getters.transaction(transactionId) === undefined) {
|
||||||
|
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/transactions/${transactionId}`))
|
||||||
|
.then((response) => {
|
||||||
|
commit('setTransactions', [response.data])
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
addEditTransactionBudgetSelected({ commit, state }, budgetId) {
|
||||||
|
commit('setCategories', [])
|
||||||
|
if (!budgetId) return;
|
||||||
|
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/categories?budgetId=${budgetId}`))
|
||||||
|
.then(function (response) {
|
||||||
|
commit('setCategories', response.data)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
addEditTransactionSaveClicked({ commit }, transaction) {
|
||||||
|
let request;
|
||||||
|
if (transaction.id) {
|
||||||
|
request = axios.put(OC.generateUrl(`/apps/twigs/api/v1.0/transactions/${transaction.id}`), transaction)
|
||||||
|
} else {
|
||||||
|
request = axios.post(OC.generateUrl(`/apps/twigs/api/v1.0/transactions`), transaction)
|
||||||
|
}
|
||||||
|
request.then(response => {
|
||||||
|
commit('addTransaction', response.data)
|
||||||
|
router.push({ name: "transactionDetails", params: { id: response.data.id } })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
transactionClicked({ commit }, transactionId) {
|
||||||
|
router.push({ name: "transactionDetails", params: { id: transactionId } })
|
||||||
|
},
|
||||||
|
transactionDetailsViewed({ commit, state }, transactionId) {
|
||||||
|
commit('setCurrentTransaction', transactionId)
|
||||||
|
|
||||||
|
if (state.transactions.length === 0) {
|
||||||
|
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/transactions/${transactionId}`))
|
||||||
|
.then((response) => {
|
||||||
|
commit('setTransactions', [response.data])
|
||||||
|
if (state.categories.length === 0) {
|
||||||
|
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/categories?budgetId=${response.data.budgetId}`))
|
||||||
|
.then(function (response) {
|
||||||
|
commit('setCategories', response.data)
|
||||||
|
response.data.forEach(category => {
|
||||||
|
axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/transactions/sum?categoryId=${category.id}`))
|
||||||
|
.then(function (response) {
|
||||||
|
commit({
|
||||||
|
type: 'setCategoryBalance',
|
||||||
|
...response.data
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
|
setCurrentBudget(state, budgetId) {
|
||||||
|
state.currentBudget = Number.parseInt(budgetId)
|
||||||
|
},
|
||||||
|
setBudgetBalance(state, data) {
|
||||||
|
state.budgetBalances = {
|
||||||
|
...state.budgetBalances,
|
||||||
|
[data.budgetId]: data.sum
|
||||||
|
}
|
||||||
|
},
|
||||||
setBudgets(state, budgets) {
|
setBudgets(state, budgets) {
|
||||||
state.budgets = budgets
|
state.budgets = budgets
|
||||||
},
|
},
|
||||||
|
setCurrentCategory(state, categoryId) {
|
||||||
|
state.currentCategory = Number.parseInt(categoryId)
|
||||||
|
},
|
||||||
setCategories(state, data) {
|
setCategories(state, data) {
|
||||||
state.categories = {
|
state.categories = data
|
||||||
...state.categories,
|
},
|
||||||
[data.budgetId]: data.categories
|
setCategoryBalance(state, data) {
|
||||||
}
|
state.categoryBalances = {
|
||||||
|
...state.categoryBalances,
|
||||||
|
[data.categoryId]: data.sum
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
addTransaction(state, transaction) {
|
||||||
|
state.transactions = [
|
||||||
|
...state.transactions.filter(t => t.id !== transaction.id),
|
||||||
|
transaction
|
||||||
|
]
|
||||||
|
},
|
||||||
|
setTransactions(state, data) {
|
||||||
|
state.transactions = data
|
||||||
|
},
|
||||||
|
setCurrentTransaction(state, transactionId) {
|
||||||
|
state.currentTransaction = Number.parseInt(transactionId)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue