Source: Pages/Budget/Budget.js

  1. import './Budget.css';
  2. import dayjs from 'dayjs';
  3. import IconButton from '@mui/material/IconButton';
  4. import DeleteIcon from '@mui/icons-material/Delete';
  5. import EditTransaction from './Transaction/EditTransaction';
  6. import AddTransaction from './Transaction/AddTransaction';
  7. import Button from '@mui/material/Button';
  8. import React, { useEffect, useState } from 'react';
  9. import { Slider } from '../../Components/Slider';
  10. import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
  11. import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
  12. import { DatePicker } from '@mui/x-date-pickers/DatePicker';
  13. import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
  14. import { faArrowRightLong } from '@fortawesome/free-solid-svg-icons';
  15. import { HttpInterface } from '../../Session/HttpInterface';
  16. import { BUDGET, SESSION, EXPENSES, INCOMES } from '../../Session/Session';
  17. import { DataGrid, GridToolbarContainer, GridToolbarExport, gridClasses } from '@mui/x-data-grid';
  18. /**
  19. * Component which are to be displayed on the budget-page. It contains
  20. * a Slider component for adjusting the boundary of the budget as well
  21. * as DatePicker components for adjusting the starting date and ending
  22. * date of the budget. It also displays DataGrid tables which contains incomes
  23. * and transactions with the possibility of adding, editing, and deleting
  24. * a transaction. Lastly, it displays a short summary of the status of the
  25. * budget as clean text.
  26. */
  27. export function Budget() {
  28. /**
  29. * Row cell which contains a button for editing a transaction
  30. * labeled as an income.
  31. */
  32. const editIncomeCell = {
  33. field: '_links.transaction.href',
  34. headerName: '',
  35. sortable: false,
  36. filterable: false,
  37. width: 10,
  38. renderCell: row =>
  39. <EditTransaction
  40. data = {row}
  41. updateTransaction = {updateTransaction}
  42. type = {'Income'}
  43. />
  44. };
  45. /**
  46. * Row cell which contains a button for editing a transaction
  47. * labeled as an expense.
  48. */
  49. const editExpenseCell = {
  50. field: '_links.transaction.href',
  51. headerName: '',
  52. sortable: false,
  53. filterable: false,
  54. width: 10,
  55. renderCell: row =>
  56. <EditTransaction
  57. data = {row}
  58. updateTransaction = {updateTransaction}
  59. type = {'Expense'}
  60. />
  61. };
  62. /**
  63. * Row cell which contains a button for deleting a transaction.
  64. */
  65. const deleteCell = {
  66. field: '_links.self.href',
  67. headerName: '',
  68. sortable: false,
  69. filterable: false,
  70. width: 10,
  71. renderCell: row =>
  72. <IconButton onClick = {() => onDelClick(row.id)}>
  73. <DeleteIcon color = "error" />
  74. </IconButton>
  75. };
  76. /**
  77. * Row cells which contains the name, date, and value of an income
  78. * as well as the edit button and the delete button.
  79. */
  80. const incomeColumns = [
  81. {field: 'tname', headerName: 'Title', width: 122},
  82. {field: 'date', headerName: 'Date', width: 98},
  83. {field: 'value', headerName: 'Price', width: 72},
  84. editIncomeCell,
  85. deleteCell
  86. ];
  87. /**
  88. * Row cells which contains the name, date, and value of an expense
  89. * as well as the edit button and the delete button.
  90. */
  91. const expenseColumns = [
  92. {field: 'tname', headerName: 'Title', width: 122},
  93. {field: 'date', headerName: 'Date', width: 98},
  94. {field: 'value', headerName: 'Price', width: 72},
  95. editExpenseCell,
  96. deleteCell
  97. ];
  98. /** Array of all transactions categorized as expenses. */
  99. const [expenses, setExpenses] = useState([]);
  100. /** Array of all transactions categorized as incomes. */
  101. const [incomes, setIncomes] = useState([]);
  102. /** Sum of all income transactions. */
  103. const [totalIncome, setTotalIncome] = useState();
  104. /** Sum of all expense transactions. */
  105. const [totalExpense, setTotalExpense] = useState();
  106. /** Start-date of the budget. */
  107. const [budgetStartDate, setBudgetStartDate] = useState('2023-03-22');
  108. /** Start-date of the budget. */
  109. const [budgetEndDate, setBudgetEndDate] = useState('2023-03-22');
  110. /**
  111. * Fetches all expenses associated with the budget ordered by
  112. * the date of the transactions. Since each expense is stored
  113. * in the database with a negative value, each transaction is
  114. * iterated through in order to convert the value into a positive
  115. * integer instead.
  116. */
  117. const getExpenses = async () => {
  118. /** Retrieves the array of expenses from the HTTP Interface. */
  119. const exp = await HttpInterface.fetchAllExpenses2();
  120. if (exp !== undefined) {
  121. /* Iterates through each element in the array. */
  122. for (let trans of exp) {
  123. /* Iterates through each field of the given transaction object. */
  124. for (let field in trans) {
  125. /* If the field is a number, the value is multiplied with -1. */
  126. if (!isNaN(trans[field])) {
  127. trans[field] = trans[field] * -1;
  128. break;
  129. }
  130. }
  131. }
  132. /* Updates the expenses variable. */
  133. setExpenses(exp);
  134. }
  135. };
  136. /**
  137. * Fetches all incomes associated with the budget ordered by
  138. * the date of the transactions.
  139. */
  140. const getIncomes = async () => {
  141. /** Retrieves the array of incomes from the HTTP Interface. */
  142. const inc = await HttpInterface.fetchAllIncomes2();
  143. /* Updates the incomes variable. */
  144. setIncomes(inc);
  145. };
  146. /**
  147. * Updates a given transaction with new data. The new transaction
  148. * is forwarded to the HTTP Interface, which again sends a /PUT
  149. * request to the server.
  150. *
  151. * @param trans The new transaction.
  152. * @param link The link to the transaction.
  153. * @param type The type of the transaction which is either "Income" or "Expense".
  154. */
  155. const updateTransaction = (trans, link, type) => {
  156. /* If the transaction is an expense, the value is turned to a negative number. */
  157. if (type === 'Expense') {
  158. trans.value = trans.value * (-1);
  159. }
  160. /* Splits the provided link at each '/'. */
  161. let splitted = link.split('/');
  162. /* Retrieves the ID of the transaction which is after the last '/'. */
  163. let transactionId = Number(splitted[splitted.length - 1]);
  164. /** The updated transaction on a format which can be sent to the server. */
  165. const newTransaction = {
  166. name: trans.tname,
  167. value: Number(trans.value),
  168. description: trans.description,
  169. date: trans.date,
  170. bid: Number(BUDGET.budgetId)
  171. };
  172. /* HTTP Interface makes a /PUT request to the server with the ID and the new transaction. */
  173. HttpInterface.updateTransaction(transactionId, newTransaction);
  174. /* Fetches the expenses and incomes again. */
  175. getExpenses();
  176. getIncomes();
  177. };
  178. /**
  179. * Adds a new transaction to the budget. The transaction is forwarded to the
  180. * HTTP Interface which again sends a /POST request to the server.
  181. */
  182. const addTransaction = (trans, type) => {
  183. /* If the transaction is an expense, the value is turned to a negative number. */
  184. if (type === 'Expense') {
  185. trans.value = trans.value * (-1);
  186. }
  187. /** The new transaction on a format which can be sent to the server. */
  188. const newTransaction = {
  189. name: trans.tname,
  190. value: Number(trans.value),
  191. description: trans.description,
  192. date: trans.date,
  193. bid: Number(BUDGET.budgetId)
  194. };
  195. /* HTTP Interface makes a /POST request to the server with the new transaction. */
  196. HttpInterface.addTransaction(newTransaction);
  197. /* Fetches the expenses and incomes again. */
  198. getExpenses();
  199. getIncomes();
  200. };
  201. /**
  202. * Deletes a given transaction by having the HTTP Interface send a /DELETE
  203. * request to the server.
  204. *
  205. * @param link The link to the transaction to be deleted.
  206. */
  207. const onDelClick = (link) => {
  208. /* Prompts the user to confirm deletion. */
  209. const confirmed = window.confirm("Delete this transaction?");
  210. if (confirmed) {
  211. /* Retrieves the ID of the transaction from the link. */
  212. let splitted = link.split('/');
  213. let transactionId = Number(splitted[splitted.length - 1]);
  214. /* HTTP Interface makes a /DELETE request to the server of the given transaction. */
  215. HttpInterface.deleteTransaction(transactionId);
  216. }
  217. };
  218. /**
  219. * Updates the budget upon pressing the apply changes button by updating
  220. * the start- and end-date and having the HTTP Interface send a /PUT request
  221. * to the server.
  222. */
  223. const handleApply = () => {
  224. BUDGET.setStartDate(budgetStartDate.format('YYYY-MM-DD'));
  225. BUDGET.setEndDate(budgetEndDate.format('YYYY-MM-DD'));
  226. HttpInterface.updateBudget();
  227. };
  228. /**
  229. * Updates the values which are to be displayed in the summary of
  230. * the budget status.
  231. */
  232. const updateStatus = () => {
  233. let sumExp = 0;
  234. let sumInc = 0;
  235. /* Sums all expense values. */
  236. for (let e of EXPENSES) {
  237. sumExp += e.value;
  238. }
  239. /* Sums all income values. */
  240. for (let i of INCOMES) {
  241. sumInc += i.value;
  242. }
  243. /* Updates the totalExpense and totalIncome variables. */
  244. setTotalExpense(sumExp * (-1));
  245. setTotalIncome(sumInc);
  246. };
  247. /**
  248. * Updates the start- and ending-date of the budget.
  249. */
  250. const updateBudgetDates = () => {
  251. let start = BUDGET.getStartDate();
  252. let end = BUDGET.getEndDate();
  253. setBudgetStartDate(dayjs(start));
  254. setBudgetEndDate(dayjs(end));
  255. };
  256. /**
  257. * Activates upon changing the date in the DatePicker
  258. * for budget start-date.
  259. *
  260. * @param {String} value The new date in YYYY-MM-DD format.
  261. */
  262. const updateBudgetStartDate = (value) => {
  263. BUDGET.setStartDate(value);
  264. setBudgetStartDate(value);
  265. }
  266. /**
  267. * Activates upon changing the date in the DatePicker
  268. * for budget end-date.
  269. *
  270. * @param {String} value The new date in YYYY-MM-DD format.
  271. */
  272. const updateBudgetEndDate = (value) => {
  273. BUDGET.setEndDate(value);
  274. setBudgetEndDate(value);
  275. }
  276. /**
  277. * UseEffect hook which activates upon changes
  278. * to the incomes or expenses variables. Upon
  279. * activation, it refreshes all data which are to
  280. * be displayed on the budget-page.
  281. */
  282. useEffect( () => {
  283. let interval = setInterval(() => {
  284. if (SESSION.getAuth()) {
  285. getExpenses();
  286. getIncomes();
  287. updateStatus();
  288. updateBudgetDates();
  289. clearInterval(interval);
  290. }
  291. }, 10);
  292. }, [incomes, expenses]);
  293. return(
  294. <article>
  295. <header>
  296. <h1>Budget</h1>
  297. </header>
  298. <hr/>
  299. <div id="expense-container">
  300. <h2 id="expense-container-label">Expense</h2>
  301. <div style={{ minHeight: '100%', maxHeight: '100%', width: '100%' }}>
  302. <DataGrid
  303. rows = {expenses}
  304. autoHeight = {true}
  305. getRowHeight={({ id, densityFactor }) => {
  306. if (expenses.length %2 !== 0 && id == expenses[expenses.length - 1]._links.self.href) {
  307. return 104 * densityFactor;
  308. }
  309. return null;
  310. }
  311. }
  312. columns = {expenseColumns}
  313. disableRowSelectionOnClick = {true}
  314. getRowId = {row => row._links.self.href}
  315. components = {{Toolbar: CustomToolbar}}
  316. initialState={{
  317. pagination: { paginationModel: { pageSize: 2 } },
  318. }}
  319. pageSizeOptions={[2]}
  320. />
  321. </div>
  322. <br/>
  323. <AddTransaction addTransaction = { addTransaction } type = {'Expense'}/>
  324. </div>
  325. <div id="income-container">
  326. <h2 id="income-container-label">Income</h2>
  327. <div style={{ minHeight: '100%', maxHeight: '100%', width: '100%' }}>
  328. <DataGrid
  329. rows = {incomes}
  330. autoHeight = {true}
  331. getRowHeight={({ id, densityFactor }) => {
  332. if (incomes.length %2 !== 0 && id == incomes[incomes.length - 1]._links.self.href ) {
  333. return 104 * densityFactor;
  334. }
  335. return null;
  336. }
  337. }
  338. columns = {incomeColumns}
  339. disableRowSelectionOnClick = {true}
  340. getRowId = {row => row._links.self.href}
  341. components = {{Toolbar: CustomToolbar}}
  342. initialState={{
  343. pagination: { paginationModel: { pageSize: 2 } },
  344. }}
  345. pageSizeOptions={[2]}
  346. />
  347. </div>
  348. <br/>
  349. <AddTransaction addTransaction = { addTransaction } type = {'Income'} />
  350. </div>
  351. <div id="budget-margin">
  352. <h2 id="margin-label">Margin</h2>
  353. <Slider/>
  354. <br/><br/>
  355. <Button
  356. variant = "contained"
  357. onClick = {handleApply}
  358. >
  359. Apply Changes
  360. </Button>
  361. <br/>
  362. <br/>
  363. <div id="budget-date-range-container">
  364. <div id="start-date-container">
  365. <LocalizationProvider dateAdapter={AdapterDayjs}>
  366. <DatePicker
  367. name = "Start"
  368. label = "Start"
  369. value = {dayjs(budgetStartDate)}
  370. onChange = {(value) => updateBudgetStartDate(value.format('YYYY-MM-DD'))}
  371. maxDate = {budgetEndDate}
  372. />
  373. </LocalizationProvider>
  374. </div>
  375. <div id="end-date-container">
  376. <LocalizationProvider dateAdapter={AdapterDayjs}>
  377. <DatePicker
  378. name = "End"
  379. label = "End"
  380. value = {dayjs(budgetEndDate)}
  381. onChange = {(value) => updateBudgetEndDate(value.format('YYYY-MM-DD'))}
  382. minDate = {budgetStartDate}
  383. />
  384. </LocalizationProvider>
  385. </div>
  386. </div>
  387. </div>
  388. <div id="date-range-divider">
  389. <FontAwesomeIcon icon={faArrowRightLong} id="arrow"/>
  390. </div>
  391. <div id="status-container">
  392. <h2 id="status-container-label">Staus</h2>
  393. <p>Total income: <i id="total-income"> KR {totalIncome}</i></p>
  394. <p>Total expense: <i id="total-expense"> KR {totalExpense}</i></p>
  395. <p>Budget spendings: <i id="budget-spendings"> KR {totalExpense - totalIncome} / {BUDGET.getBoundary()}</i></p>
  396. <p>Margin: <i id="margin"> KR {BUDGET.getBoundary() + totalIncome - totalExpense}</i></p>
  397. </div>
  398. </article>
  399. );
  400. }
  401. /**
  402. * Customized ToolBar component for the DataGrid components which
  403. * displays the transactions.
  404. */
  405. function CustomToolbar() {
  406. return (
  407. <GridToolbarContainer
  408. className = {gridClasses.toolbarContainer}>
  409. <GridToolbarExport />
  410. </GridToolbarContainer>
  411. );
  412. };