diff --git a/.eslintrc.js b/.eslintrc.js index 3a676f5..7b7addd 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,8 @@ module.exports = { - extends: [ - 'nextcloud' - ] + extends: [ + 'nextcloud' + ], + "rules": { + "indent": ["error", 4] + } }; diff --git a/appinfo/routes.php b/appinfo/routes.php index f65ccad..3229e8f 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -23,7 +23,7 @@ return [ [ 'name' => 'transaction#sum', 'url' => '/api/v1.0/transactions/sum', - 'verb' => 'POST', + 'verb' => 'GET', ] ] ]; diff --git a/css/style.css b/css/style.css index ce350c6..0dfbee5 100644 --- a/css/style.css +++ b/css/style.css @@ -1,3 +1,22 @@ -#hello { - color: red; +:root { + --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); +} \ No newline at end of file diff --git a/lib/Controller/BudgetController.php b/lib/Controller/BudgetController.php index 2beadd1..39f83fe 100644 --- a/lib/Controller/BudgetController.php +++ b/lib/Controller/BudgetController.php @@ -176,4 +176,14 @@ class BudgetController extends Controller $this->budgetMapper->delete($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); + } + + } } diff --git a/lib/Controller/TransactionController.php b/lib/Controller/TransactionController.php index a8fe07e..da47ba7 100644 --- a/lib/Controller/TransactionController.php +++ b/lib/Controller/TransactionController.php @@ -25,6 +25,8 @@ class TransactionController extends Controller private $transactionMapper; private $userPermissionMapper; private $logger; + private $DATE_FORMAT = DateTime::RFC3339_EXTENDED; + private const DATE_FORMAT = "Y-m-d\TH:i:s.v\Z"; public function __construct( $AppName, @@ -49,16 +51,19 @@ class TransactionController extends Controller * @NoAdminRequired * @NoCSRFRequired */ - public function index() + public function index(?int $budgetId, ?int $categoryId, ?int $count) { - $budgetId = $_GET['budgetId']; - $categoryId = $_GET['categoryId']; - if ($budgetId == null) { - return new DataResponse([], Http::STATUS_BAD_REQUEST); - } try { - $this->userPermissionMapper->find($budgetId, $this->userId); - return new DataResponse($this->transactionMapper->findAll($budgetId, $categoryId)); + 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($this->transactionMapper->findAll($budgetId, $categoryId, $count)); } catch (Exception $e) { return new DataResponse([], Http::STATUS_NOT_FOUND); } @@ -95,7 +100,7 @@ class TransactionController extends Controller */ public function create( string $name, - string $description, + ?string $description, int $amount, string $date, bool $expense, @@ -115,9 +120,9 @@ class TransactionController extends Controller $transaction->setDescription($description); $transaction->setAmount($amount); $transaction->setExpense($expense); - $dateTime = DateTime::createFromFormat(DateTime::ATOM, $date); + $dateTime = DateTime::createFromFormat($this->DATE_FORMAT, $date); 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()); $this->logger->error("Setting category $categoryId for new transaction"); @@ -168,7 +173,7 @@ class TransactionController extends Controller $transaction->setDescription($description); $transaction->setAmount($amount); $transaction->setExpense($expense); - $dateTime = DateTime::createFromFormat(DateTime::ATOM, $date); + $dateTime = DateTime::createFromFormat($this->DATE_FORMAT, $date); if (!$dateTime) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } @@ -235,7 +240,7 @@ class TransactionController extends Controller ); $startDateTime->setTime(0, 0, 0, 0); } else { - $startDateTime = DateTime::createFromFormat(DateTime::ATOM, $startDate); + $startDateTime = DateTime::createFromFormat($this->DATE_FORMAT, $startDate); } if (!$startDateTime) { return new DataResponse([], Http::STATUS_BAD_REQUEST); @@ -250,7 +255,7 @@ class TransactionController extends Controller ); $endDateTime->setTime(23, 59, 59, 999); } else { - $endDateTime = DateTime::createFromFormat(DateTime::ATOM, $endDate); + $endDateTime = DateTime::createFromFormat($this->DATE_FORMAT, $endDate); } if (!$endDateTime) { return new DataResponse([], Http::STATUS_BAD_REQUEST); diff --git a/lib/Db/BudgetMapper.php b/lib/Db/BudgetMapper.php index 3636ad7..22c02af 100644 --- a/lib/Db/BudgetMapper.php +++ b/lib/Db/BudgetMapper.php @@ -101,5 +101,5 @@ class BudgetMapper extends QBMapper $qb->execute(); return $entity; - } + } } diff --git a/lib/Db/TransactionMapper.php b/lib/Db/TransactionMapper.php index 56dbee8..d00977c 100644 --- a/lib/Db/TransactionMapper.php +++ b/lib/Db/TransactionMapper.php @@ -29,8 +29,11 @@ class TransactionMapper extends QBMapper 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->select('*') @@ -45,6 +48,12 @@ class TransactionMapper extends QBMapper ); } + $qb->orderBy('date', 'desc'); + + if ($count) { + $qb->setMaxResults($count); + } + return $this->findEntities($qb); } @@ -115,4 +124,8 @@ class TransactionMapper extends QBMapper $statement->execute(); return (int) $statement->fetch(FetchMode::COLUMN); } + + public function countByBudgetId(int $budgetId) { + + } } diff --git a/src/App.vue b/src/App.vue index a69efe0..35d5e5b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -24,4 +24,14 @@ export default { BudgetList, }, }; - \ No newline at end of file + + \ No newline at end of file diff --git a/src/components/AddEditTransaction.vue b/src/components/AddEditTransaction.vue new file mode 100644 index 0000000..8bedb48 --- /dev/null +++ b/src/components/AddEditTransaction.vue @@ -0,0 +1,100 @@ + + + \ No newline at end of file diff --git a/src/components/BudgetDetails.vue b/src/components/BudgetDetails.vue index 0443799..343e09d 100644 --- a/src/components/BudgetDetails.vue +++ b/src/components/BudgetDetails.vue @@ -1,39 +1,110 @@ \ No newline at end of file + + \ No newline at end of file diff --git a/src/components/BudgetList.vue b/src/components/BudgetList.vue index 50d131e..91e6ada 100644 --- a/src/components/BudgetList.vue +++ b/src/components/BudgetList.vue @@ -1,13 +1,13 @@ diff --git a/src/components/CategoryList.vue b/src/components/CategoryList.vue index 3000a8f..2cd15ab 100644 --- a/src/components/CategoryList.vue +++ b/src/components/CategoryList.vue @@ -1,29 +1,73 @@ + \ No newline at end of file diff --git a/src/components/ProgressBar.vue b/src/components/ProgressBar.vue new file mode 100644 index 0000000..59879a3 --- /dev/null +++ b/src/components/ProgressBar.vue @@ -0,0 +1,57 @@ + + + \ No newline at end of file diff --git a/src/components/TransactionDetails.vue b/src/components/TransactionDetails.vue new file mode 100644 index 0000000..9d8cbbf --- /dev/null +++ b/src/components/TransactionDetails.vue @@ -0,0 +1,64 @@ + + diff --git a/src/components/TransactionList.vue b/src/components/TransactionList.vue new file mode 100644 index 0000000..4c8a8f0 --- /dev/null +++ b/src/components/TransactionList.vue @@ -0,0 +1,74 @@ + + + \ No newline at end of file diff --git a/src/router/index.js b/src/router/index.js index 7bf204a..b6f6373 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,6 +1,9 @@ import VueRouter from 'vue-router' import Vue from 'vue' import BudgetDetails from '../components/BudgetDetails' +import CategoryDetails from '../components/CategoryDetails' +import AddEditTransaction from '../components/AddEditTransaction' +import TransactionDetails from '../components/TransactionDetails' Vue.use(VueRouter) @@ -9,7 +12,27 @@ const routes = [ path: '/budgets/:id', name: '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({ diff --git a/src/store/index.js b/src/store/index.js index 079ff5a..18b1785 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,56 +1,189 @@ import Vue from 'vue' import Vuex from 'vuex' import axios from '@nextcloud/axios' +import router from '../router' Vue.use(Vuex) export default new Vuex.Store({ state: { budgets: [], + budgetBalances: {}, currentBudget: 0, - categories: {}, + categories: [], + categoryBalances: {}, currentCategory: 0, transactions: [], currentTransaction: 0, }, getters: { - budgets: (state) => { - return state.budgets + budgets: (state) => 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) => { - return state.budgets.find(budget => budget.id === id) + categoryBalance: (state) => (categoryId) => { + return state.categoryBalances[categoryId]; }, - categories: (state) => (budgetId) => { - return state.categories[budgetId] + categoryRemainingBalance: (state, getters) => (category) => { + 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: { budgetListViewed({ commit }) { axios.get(OC.generateUrl('/apps/twigs/api/v1.0/budgets')) .then(function (response) { commit('setBudgets', response.data) - }) - }, - budgetDetailsViewed({ commit }, budgetId) { - axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/categories?budgetId=${budgetId}`)) - .then(function (response) { - commit({ - type: 'setCategories', - budgetId: budgetId, - categories: response.data + response.data.forEach(budget => { + axios.get(OC.generateUrl(`/apps/twigs/api/v1.0/transactions/sum?budgetId=${budget.id}`)) + .then(function (response) { + commit({ + type: 'setBudgetBalance', + ...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: { + setCurrentBudget(state, budgetId) { + state.currentBudget = Number.parseInt(budgetId) + }, + setBudgetBalance(state, data) { + state.budgetBalances = { + ...state.budgetBalances, + [data.budgetId]: data.sum + } + }, setBudgets(state, budgets) { state.budgets = budgets }, + setCurrentCategory(state, categoryId) { + state.currentCategory = Number.parseInt(categoryId) + }, setCategories(state, data) { - state.categories = { - ...state.categories, - [data.budgetId]: data.categories + state.categories = data + }, + 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) + }, } })