import './Budget.css';
import dayjs from 'dayjs';
import IconButton from '@mui/material/IconButton';
import DeleteIcon from '@mui/icons-material/Delete';
import EditTransaction from './Transaction/EditTransaction';
import AddTransaction from './Transaction/AddTransaction';
import Button from '@mui/material/Button';
import React, { useEffect, useState } from 'react';
import { Slider } from '../../Components/Slider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faArrowRightLong } from '@fortawesome/free-solid-svg-icons';
import { HttpInterface } from '../../Session/HttpInterface';
import { BUDGET, SESSION, EXPENSES, INCOMES } from '../../Session/Session';
import { DataGrid, GridToolbarContainer, GridToolbarExport, gridClasses } from '@mui/x-data-grid';
/**
* Component which are to be displayed on the budget-page. It contains
* a Slider component for adjusting the boundary of the budget as well
* as DatePicker components for adjusting the starting date and ending
* date of the budget. It also displays DataGrid tables which contains incomes
* and transactions with the possibility of adding, editing, and deleting
* a transaction. Lastly, it displays a short summary of the status of the
* budget as clean text.
*/
export function Budget() {
/**
* Row cell which contains a button for editing a transaction
* labeled as an income.
*/
const editIncomeCell = {
field: '_links.transaction.href',
headerName: '',
sortable: false,
filterable: false,
width: 10,
renderCell: row =>
<EditTransaction
data = {row}
updateTransaction = {updateTransaction}
type = {'Income'}
/>
};
/**
* Row cell which contains a button for editing a transaction
* labeled as an expense.
*/
const editExpenseCell = {
field: '_links.transaction.href',
headerName: '',
sortable: false,
filterable: false,
width: 10,
renderCell: row =>
<EditTransaction
data = {row}
updateTransaction = {updateTransaction}
type = {'Expense'}
/>
};
/**
* Row cell which contains a button for deleting a transaction.
*/
const deleteCell = {
field: '_links.self.href',
headerName: '',
sortable: false,
filterable: false,
width: 10,
renderCell: row =>
<IconButton onClick = {() => onDelClick(row.id)}>
<DeleteIcon color = "error" />
</IconButton>
};
/**
* Row cells which contains the name, date, and value of an income
* as well as the edit button and the delete button.
*/
const incomeColumns = [
{field: 'tname', headerName: 'Title', width: 122},
{field: 'date', headerName: 'Date', width: 98},
{field: 'value', headerName: 'Price', width: 72},
editIncomeCell,
deleteCell
];
/**
* Row cells which contains the name, date, and value of an expense
* as well as the edit button and the delete button.
*/
const expenseColumns = [
{field: 'tname', headerName: 'Title', width: 122},
{field: 'date', headerName: 'Date', width: 98},
{field: 'value', headerName: 'Price', width: 72},
editExpenseCell,
deleteCell
];
/** Array of all transactions categorized as expenses. */
const [expenses, setExpenses] = useState([]);
/** Array of all transactions categorized as incomes. */
const [incomes, setIncomes] = useState([]);
/** Sum of all income transactions. */
const [totalIncome, setTotalIncome] = useState();
/** Sum of all expense transactions. */
const [totalExpense, setTotalExpense] = useState();
/** Start-date of the budget. */
const [budgetStartDate, setBudgetStartDate] = useState('2023-03-22');
/** Start-date of the budget. */
const [budgetEndDate, setBudgetEndDate] = useState('2023-03-22');
/**
* Fetches all expenses associated with the budget ordered by
* the date of the transactions. Since each expense is stored
* in the database with a negative value, each transaction is
* iterated through in order to convert the value into a positive
* integer instead.
*/
const getExpenses = async () => {
/** Retrieves the array of expenses from the HTTP Interface. */
const exp = await HttpInterface.fetchAllExpenses2();
if (exp !== undefined) {
/* Iterates through each element in the array. */
for (let trans of exp) {
/* Iterates through each field of the given transaction object. */
for (let field in trans) {
/* If the field is a number, the value is multiplied with -1. */
if (!isNaN(trans[field])) {
trans[field] = trans[field] * -1;
break;
}
}
}
/* Updates the expenses variable. */
setExpenses(exp);
}
};
/**
* Fetches all incomes associated with the budget ordered by
* the date of the transactions.
*/
const getIncomes = async () => {
/** Retrieves the array of incomes from the HTTP Interface. */
const inc = await HttpInterface.fetchAllIncomes2();
/* Updates the incomes variable. */
setIncomes(inc);
};
/**
* Updates a given transaction with new data. The new transaction
* is forwarded to the HTTP Interface, which again sends a /PUT
* request to the server.
*
* @param trans The new transaction.
* @param link The link to the transaction.
* @param type The type of the transaction which is either "Income" or "Expense".
*/
const updateTransaction = (trans, link, type) => {
/* If the transaction is an expense, the value is turned to a negative number. */
if (type === 'Expense') {
trans.value = trans.value * (-1);
}
/* Splits the provided link at each '/'. */
let splitted = link.split('/');
/* Retrieves the ID of the transaction which is after the last '/'. */
let transactionId = Number(splitted[splitted.length - 1]);
/** The updated transaction on a format which can be sent to the server. */
const newTransaction = {
name: trans.tname,
value: Number(trans.value),
description: trans.description,
date: trans.date,
bid: Number(BUDGET.budgetId)
};
/* HTTP Interface makes a /PUT request to the server with the ID and the new transaction. */
HttpInterface.updateTransaction(transactionId, newTransaction);
/* Fetches the expenses and incomes again. */
getExpenses();
getIncomes();
};
/**
* Adds a new transaction to the budget. The transaction is forwarded to the
* HTTP Interface which again sends a /POST request to the server.
*/
const addTransaction = (trans, type) => {
/* If the transaction is an expense, the value is turned to a negative number. */
if (type === 'Expense') {
trans.value = trans.value * (-1);
}
/** The new transaction on a format which can be sent to the server. */
const newTransaction = {
name: trans.tname,
value: Number(trans.value),
description: trans.description,
date: trans.date,
bid: Number(BUDGET.budgetId)
};
/* HTTP Interface makes a /POST request to the server with the new transaction. */
HttpInterface.addTransaction(newTransaction);
/* Fetches the expenses and incomes again. */
getExpenses();
getIncomes();
};
/**
* Deletes a given transaction by having the HTTP Interface send a /DELETE
* request to the server.
*
* @param link The link to the transaction to be deleted.
*/
const onDelClick = (link) => {
/* Prompts the user to confirm deletion. */
const confirmed = window.confirm("Delete this transaction?");
if (confirmed) {
/* Retrieves the ID of the transaction from the link. */
let splitted = link.split('/');
let transactionId = Number(splitted[splitted.length - 1]);
/* HTTP Interface makes a /DELETE request to the server of the given transaction. */
HttpInterface.deleteTransaction(transactionId);
}
};
/**
* Updates the budget upon pressing the apply changes button by updating
* the start- and end-date and having the HTTP Interface send a /PUT request
* to the server.
*/
const handleApply = () => {
BUDGET.setStartDate(budgetStartDate.format('YYYY-MM-DD'));
BUDGET.setEndDate(budgetEndDate.format('YYYY-MM-DD'));
HttpInterface.updateBudget();
};
/**
* Updates the values which are to be displayed in the summary of
* the budget status.
*/
const updateStatus = () => {
let sumExp = 0;
let sumInc = 0;
/* Sums all expense values. */
for (let e of EXPENSES) {
sumExp += e.value;
}
/* Sums all income values. */
for (let i of INCOMES) {
sumInc += i.value;
}
/* Updates the totalExpense and totalIncome variables. */
setTotalExpense(sumExp * (-1));
setTotalIncome(sumInc);
};
/**
* Updates the start- and ending-date of the budget.
*/
const updateBudgetDates = () => {
let start = BUDGET.getStartDate();
let end = BUDGET.getEndDate();
setBudgetStartDate(dayjs(start));
setBudgetEndDate(dayjs(end));
};
/**
* Activates upon changing the date in the DatePicker
* for budget start-date.
*
* @param {String} value The new date in YYYY-MM-DD format.
*/
const updateBudgetStartDate = (value) => {
BUDGET.setStartDate(value);
setBudgetStartDate(value);
}
/**
* Activates upon changing the date in the DatePicker
* for budget end-date.
*
* @param {String} value The new date in YYYY-MM-DD format.
*/
const updateBudgetEndDate = (value) => {
BUDGET.setEndDate(value);
setBudgetEndDate(value);
}
/**
* UseEffect hook which activates upon changes
* to the incomes or expenses variables. Upon
* activation, it refreshes all data which are to
* be displayed on the budget-page.
*/
useEffect( () => {
let interval = setInterval(() => {
if (SESSION.getAuth()) {
getExpenses();
getIncomes();
updateStatus();
updateBudgetDates();
clearInterval(interval);
}
}, 10);
}, [incomes, expenses]);
return(
<article>
<header>
<h1>Budget</h1>
</header>
<hr/>
<div id="expense-container">
<h2 id="expense-container-label">Expense</h2>
<div style={{ minHeight: '100%', maxHeight: '100%', width: '100%' }}>
<DataGrid
rows = {expenses}
autoHeight = {true}
getRowHeight={({ id, densityFactor }) => {
if (expenses.length %2 !== 0 && id == expenses[expenses.length - 1]._links.self.href) {
return 104 * densityFactor;
}
return null;
}
}
columns = {expenseColumns}
disableRowSelectionOnClick = {true}
getRowId = {row => row._links.self.href}
components = {{Toolbar: CustomToolbar}}
initialState={{
pagination: { paginationModel: { pageSize: 2 } },
}}
pageSizeOptions={[2]}
/>
</div>
<br/>
<AddTransaction addTransaction = { addTransaction } type = {'Expense'}/>
</div>
<div id="income-container">
<h2 id="income-container-label">Income</h2>
<div style={{ minHeight: '100%', maxHeight: '100%', width: '100%' }}>
<DataGrid
rows = {incomes}
autoHeight = {true}
getRowHeight={({ id, densityFactor }) => {
if (incomes.length %2 !== 0 && id == incomes[incomes.length - 1]._links.self.href ) {
return 104 * densityFactor;
}
return null;
}
}
columns = {incomeColumns}
disableRowSelectionOnClick = {true}
getRowId = {row => row._links.self.href}
components = {{Toolbar: CustomToolbar}}
initialState={{
pagination: { paginationModel: { pageSize: 2 } },
}}
pageSizeOptions={[2]}
/>
</div>
<br/>
<AddTransaction addTransaction = { addTransaction } type = {'Income'} />
</div>
<div id="budget-margin">
<h2 id="margin-label">Margin</h2>
<Slider/>
<br/><br/>
<Button
variant = "contained"
onClick = {handleApply}
>
Apply Changes
</Button>
<br/>
<br/>
<div id="budget-date-range-container">
<div id="start-date-container">
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
name = "Start"
label = "Start"
value = {dayjs(budgetStartDate)}
onChange = {(value) => updateBudgetStartDate(value.format('YYYY-MM-DD'))}
maxDate = {budgetEndDate}
/>
</LocalizationProvider>
</div>
<div id="end-date-container">
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
name = "End"
label = "End"
value = {dayjs(budgetEndDate)}
onChange = {(value) => updateBudgetEndDate(value.format('YYYY-MM-DD'))}
minDate = {budgetStartDate}
/>
</LocalizationProvider>
</div>
</div>
</div>
<div id="date-range-divider">
<FontAwesomeIcon icon={faArrowRightLong} id="arrow"/>
</div>
<div id="status-container">
<h2 id="status-container-label">Staus</h2>
<p>Total income: <i id="total-income"> KR {totalIncome}</i></p>
<p>Total expense: <i id="total-expense"> KR {totalExpense}</i></p>
<p>Budget spendings: <i id="budget-spendings"> KR {totalExpense - totalIncome} / {BUDGET.getBoundary()}</i></p>
<p>Margin: <i id="margin"> KR {BUDGET.getBoundary() + totalIncome - totalExpense}</i></p>
</div>
</article>
);
}
/**
* Customized ToolBar component for the DataGrid components which
* displays the transactions.
*/
function CustomToolbar() {
return (
<GridToolbarContainer
className = {gridClasses.toolbarContainer}>
<GridToolbarExport />
</GridToolbarContainer>
);
};