mirror of
https://github.com/linuxserver/budge.git
synced 2026-03-09 00:08:38 +08:00
ui overhaul
This commit is contained in:
parent
acb63f1b9c
commit
0253b30d39
@ -79,19 +79,19 @@ export default function App(props) {
|
||||
palette: {
|
||||
mode: 'light',
|
||||
background: {
|
||||
drawer: '#333333',
|
||||
drawer: '#3a3f51',
|
||||
header: '#536067',
|
||||
tableBody: '#ffffff',
|
||||
tableHeader: '#333333',
|
||||
details: '#333333',
|
||||
detailsContent: '#333333',
|
||||
tableHeader: '#3a3f51',
|
||||
details: '#3a3f51',
|
||||
detailsContent: '#3a3f51',
|
||||
},
|
||||
action: {
|
||||
// disabledBackground: 'set color of background here',
|
||||
// disabled: '#616161',
|
||||
},
|
||||
primary: {
|
||||
main: '#333333',
|
||||
main: '#3a3f51',
|
||||
},
|
||||
secondary: {
|
||||
main: '#ffffff',
|
||||
@ -110,7 +110,7 @@ export default function App(props) {
|
||||
MuiDrawer: {
|
||||
styleOverrides: {
|
||||
paper: {
|
||||
backgroundColor: '#333333',
|
||||
backgroundColor: '#3a3f51',
|
||||
color: 'white',
|
||||
},
|
||||
},
|
||||
@ -140,7 +140,7 @@ export default function App(props) {
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<ThemeProvider theme={theme === 'dark' ? darkTheme : darkTheme}>
|
||||
<ThemeProvider theme={theme === 'dark' ? darkTheme : lightTheme}>
|
||||
{!initComplete && <Login />}
|
||||
{initComplete && (
|
||||
<Router>
|
||||
@ -148,6 +148,7 @@ export default function App(props) {
|
||||
<CssBaseline />
|
||||
{/* <Header /> */}
|
||||
<Drawer onAddAccountClick={() => setNewAccountDialogOpen(true)} />
|
||||
|
||||
<Box component="main" sx={{ flexGrow: 1, p: 0 }}>
|
||||
<AddAccountDialog isOpen={newAccountDialogOpen} close={closeNewAccountDialog} />
|
||||
<Routes>
|
||||
|
||||
@ -14,6 +14,8 @@ import { usePopupState, bindTrigger, bindPopover } from 'material-ui-popup-state
|
||||
import ReconcileForm from './ReconcileForm'
|
||||
import { createSelector } from '@reduxjs/toolkit'
|
||||
import Paper from '@mui/material/Paper'
|
||||
import BalanceCalculation from './AccountTable/BalanceCalculation'
|
||||
import EditIcon from '@mui/icons-material/Edit'
|
||||
|
||||
export default function BudgetDetails({ accountId, name }) {
|
||||
const theme = useTheme()
|
||||
@ -56,18 +58,28 @@ export default function BudgetDetails({ accountId, name }) {
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
sx={{ p: 3, height: '100%' }}
|
||||
sx={{ px: 2, pb: 2, height: '100%' }}
|
||||
>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ pb: 4 }}>
|
||||
<Box>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ fontWeight: 'bold', color: 'white', cursor: 'pointer', display: 'inline-block' }}
|
||||
<Paper sx={{ p: 2, m: 2 }}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ pb: 2 }}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="center"
|
||||
{...bindTrigger(editAccountPopupState)}
|
||||
sx={{ width: '100%', pr: 2 }}
|
||||
>
|
||||
{account.name}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ fontWeight: 'bold', color: 'white', cursor: 'pointer', display: 'inline-block', pr: 2 }}
|
||||
>
|
||||
{account.name}
|
||||
</Typography>
|
||||
|
||||
{/* <EditIcon sx={{ cursor: 'pointer' }} /> */}
|
||||
</Stack>
|
||||
|
||||
<Popover
|
||||
{...bindPopover(editAccountPopupState)}
|
||||
anchorOrigin={{
|
||||
@ -94,74 +106,32 @@ export default function BudgetDetails({ accountId, name }) {
|
||||
</Stack>
|
||||
</Box>
|
||||
</Popover>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Button {...bindTrigger(reconcilePopupState)} color="secondary" variant="outlined">
|
||||
<Typography style={{ fontSize: theme.typography.caption.fontSize, fontWeight: 'bold' }}>
|
||||
Reconcile
|
||||
</Typography>
|
||||
</Button>
|
||||
<ReconcileForm
|
||||
key={account.cleared}
|
||||
popupState={reconcilePopupState}
|
||||
accountId={account.id}
|
||||
balance={account.cleared}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Box>
|
||||
<Button {...bindTrigger(reconcilePopupState)} color="primary" variant="outlined">
|
||||
<Typography style={{ fontSize: theme.typography.caption.fontSize, fontWeight: 'bold' }}>
|
||||
Reconcile
|
||||
</Typography>
|
||||
</Button>
|
||||
<ReconcileForm
|
||||
key={account.cleared}
|
||||
popupState={reconcilePopupState}
|
||||
accountId={account.id}
|
||||
balance={account.cleared}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Paper sx={{ p: 2 }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: theme.typography.h5.fontSize,
|
||||
}}
|
||||
<BalanceCalculation account={account} />
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
sx={{ width: '100%', mt: 1, pt: 1, borderTop: `1px solid ${theme.palette.action.disabled}` }}
|
||||
>
|
||||
Summary
|
||||
</Typography>
|
||||
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
|
||||
<Stack direction="column" justifyContent="space-between" alignItems="flex-start" spacing={4}>
|
||||
<Box>Cleared</Box>
|
||||
<Box>Uncleared ({pendingTransactions})</Box>
|
||||
<Box>Balance</Box>
|
||||
</Stack>
|
||||
|
||||
<Stack direction="column" justifyContent="space-around" alignItems="center">
|
||||
<Typography
|
||||
style={{
|
||||
color: getBalanceColor(account.cleared, theme),
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
variant="h6"
|
||||
>
|
||||
{intlFormat(account.cleared)}
|
||||
</Typography>
|
||||
|
||||
<Box>+</Box>
|
||||
|
||||
<Typography
|
||||
style={{
|
||||
color: getBalanceColor(account.cleared, theme),
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
variant="h6"
|
||||
>
|
||||
{intlFormat(account.uncleared)}
|
||||
</Typography>
|
||||
|
||||
<Box>=</Box>
|
||||
|
||||
<Typography
|
||||
style={{
|
||||
color: getBalanceColor(account.cleared, theme),
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
variant="h6"
|
||||
>
|
||||
{intlFormat(account.balance)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Box>Pending Transactions</Box>
|
||||
<Box> {pendingTransactions}</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
@ -161,13 +161,6 @@ export default function Account(props) {
|
||||
|
||||
const payeesMap = useSelector(selectPayeesMap)
|
||||
|
||||
let amountFieldFocused = null
|
||||
let amountFieldModified = false
|
||||
const focusAmountField = field => {
|
||||
amountFieldFocused = field
|
||||
amountFieldModified = false
|
||||
}
|
||||
|
||||
const filter = createFilterOptions()
|
||||
const columns = [
|
||||
{
|
||||
@ -383,10 +376,6 @@ export default function Account(props) {
|
||||
)
|
||||
},
|
||||
editComponent: props => {
|
||||
// if (amountFieldModified === true && amountFieldFocused !== 'outflow') {
|
||||
// props.value.amount = 0
|
||||
// }
|
||||
|
||||
const value = dinero(props.value)
|
||||
return (
|
||||
<Box sx={{ textAlign: 'right' }}>
|
||||
@ -395,7 +384,6 @@ export default function Account(props) {
|
||||
variant="standard"
|
||||
value={toUnit(value, { digits: 2 })}
|
||||
onChange={value => {
|
||||
amountFieldModified = true
|
||||
props.onChange(toSnapshot(inputToDinero(value)))
|
||||
}}
|
||||
// onFocus={focusOutflowField}
|
||||
@ -712,7 +700,7 @@ export default function Account(props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '100%' }}>
|
||||
<Box>
|
||||
<ImportCSV accountId={props.accountId} open={importerOpen} close={closeImporter}></ImportCSV>
|
||||
<MaterialTable
|
||||
style={{
|
||||
@ -848,6 +836,7 @@ export default function Account(props) {
|
||||
padding: 'dense',
|
||||
draggable: false,
|
||||
pageSize: 20,
|
||||
pageSizeOptions: [5, 10, 25, 50, 100],
|
||||
addRowPosition: 'first',
|
||||
selection: true,
|
||||
actionsColumnIndex: 99,
|
||||
@ -893,10 +882,6 @@ export default function Account(props) {
|
||||
backgroundColor: theme.palette.background.tableHeader,
|
||||
}}
|
||||
>
|
||||
<AccountTableHeader accountId={account.id} name={account.name} />
|
||||
|
||||
<Divider />
|
||||
|
||||
<StyledMTableToolbar
|
||||
{...{ ...props, actions: [] }}
|
||||
showTextRowsSelected={false}
|
||||
@ -934,6 +919,6 @@ export default function Account(props) {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
115
frontend/src/components/AccountTable/BalanceCalculation.js
Normal file
115
frontend/src/components/AccountTable/BalanceCalculation.js
Normal file
@ -0,0 +1,115 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { accountsSelectors, editAccount } from '../../redux/slices/Accounts'
|
||||
import { FromAPI, getBalanceColor, intlFormat } from '../../utils/Currency'
|
||||
import { usePopupState } from 'material-ui-popup-state/hooks'
|
||||
import Stack from '@mui/material/Stack'
|
||||
import Box from '@mui/material/Box'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { useTheme } from '@mui/styles'
|
||||
import { createSelector } from '@reduxjs/toolkit'
|
||||
|
||||
export default function BalanceCalculation({ account }) {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Stack direction="row" justifyContent="space-evenly" alignItems="center" spacing={0}>
|
||||
<div>
|
||||
<Stack
|
||||
direction="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
// spacing={2}
|
||||
>
|
||||
<Typography
|
||||
style={{
|
||||
color: getBalanceColor(account.cleared, theme),
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
variant="subtitle1"
|
||||
>
|
||||
{intlFormat(account.cleared)}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
Cleared
|
||||
</Typography>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
...theme.typography.h6,
|
||||
}}
|
||||
>
|
||||
+
|
||||
</Box>
|
||||
|
||||
<div>
|
||||
<Stack
|
||||
direction="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
// spacing={2}
|
||||
>
|
||||
<Typography
|
||||
style={{
|
||||
color: getBalanceColor(account.uncleared, theme),
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
variant="subtitle1"
|
||||
>
|
||||
{intlFormat(account.uncleared)}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
Uncleared
|
||||
</Typography>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
...theme.typography.h6,
|
||||
}}
|
||||
>
|
||||
=
|
||||
</Box>
|
||||
|
||||
<div>
|
||||
<Stack
|
||||
direction="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
// spacing={2}
|
||||
>
|
||||
<Typography
|
||||
style={{
|
||||
color: getBalanceColor(account.balance, theme),
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
variant="subtitle1"
|
||||
>
|
||||
{intlFormat(account.balance)}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
Working Balance
|
||||
</Typography>
|
||||
</Stack>
|
||||
</div>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@ -1,26 +1,21 @@
|
||||
import Box from '@mui/material/Box'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { getBalanceColor, inputToDinero, intlFormat, valueToDinero } from '../utils/Currency'
|
||||
import Table from '@mui/material/Table'
|
||||
import TableBody from '@mui/material/TableBody'
|
||||
import TableCell from '@mui/material/TableCell'
|
||||
import TableContainer from '@mui/material/TableContainer'
|
||||
import TableRow from '@mui/material/TableRow'
|
||||
import TableHead from '@mui/material/TableHead'
|
||||
import { inputToDinero, intlFormat, valueToDinero, getBalanceColor } from '../utils/Currency'
|
||||
import { useTheme } from '@mui/styles'
|
||||
import Stack from '@mui/material/Stack'
|
||||
import { isPositive, isZero } from 'dinero.js'
|
||||
import Alert from '@mui/material/Alert'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import { formatMonthFromDateString, getDateFromString } from '../utils/Date'
|
||||
import BudgetMonthPicker from './BudgetMonthPicker'
|
||||
import Button from '@mui/material/Button'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { usePopupState, bindTrigger } from 'material-ui-popup-state/hooks'
|
||||
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIosNew'
|
||||
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'
|
||||
import { selectActiveBudget, setCurrentMonth } from '../redux/slices/Budgets'
|
||||
import { selectActiveBudget } from '../redux/slices/Budgets'
|
||||
import Paper from '@mui/material/Paper'
|
||||
import BudgetMonthCalculation from './BudgetTable/BudgetMonthCalculation'
|
||||
import CategoryMonthActivity from './CategoryMonthActivity'
|
||||
import BudgetMonthNavigator from './BudgetMonthNavigator'
|
||||
import Card from '@mui/material/Card'
|
||||
import CardActions from '@mui/material/CardActions'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Button from '@mui/material/Button'
|
||||
import { setSelectedCategory } from '../redux/slices/Categories'
|
||||
|
||||
export default function BudgetDetails(props) {
|
||||
const theme = useTheme()
|
||||
@ -31,194 +26,117 @@ export default function BudgetDetails(props) {
|
||||
return state.budgetMonths.entities[month] || null
|
||||
})
|
||||
|
||||
const income = budgetMonth ? valueToDinero(budgetMonth.income) : inputToDinero(0)
|
||||
const activity = budgetMonth ? valueToDinero(budgetMonth.activity) : inputToDinero(0)
|
||||
const budgeted = budgetMonth ? valueToDinero(budgetMonth.budgeted) : inputToDinero(0)
|
||||
const underfunded = budgetMonth ? valueToDinero(budgetMonth.underfunded) : inputToDinero(0)
|
||||
|
||||
const availableMonths = useSelector(state => state.budgets.availableMonths)
|
||||
const budget = useSelector(selectActiveBudget)
|
||||
|
||||
const toBeBudgeted = budget ? valueToDinero(budget.toBeBudgeted) : inputToDinero(0)
|
||||
|
||||
const nextMonth = getDateFromString(month)
|
||||
nextMonth.setMonth(nextMonth.getMonth() + 1)
|
||||
const nextMonthDisabled = !availableMonths.includes(formatMonthFromDateString(nextMonth))
|
||||
const selectedCategory = useSelector(state => {
|
||||
if (!state.categories.selected) {
|
||||
return null
|
||||
}
|
||||
|
||||
const prevMonth = getDateFromString(month)
|
||||
prevMonth.setMonth(prevMonth.getMonth() - 1)
|
||||
const prevMonthDisabled = !availableMonths.includes(formatMonthFromDateString(prevMonth))
|
||||
|
||||
const monthPickerPopupState = usePopupState({
|
||||
variant: 'popover',
|
||||
popupId: 'monthPicker',
|
||||
return state.categories.entities[state.categories.selected]
|
||||
})
|
||||
|
||||
const navigateMonth = direction => {
|
||||
const monthDate = new Date(Date.UTC(...month.split('-')))
|
||||
monthDate.setDate(1)
|
||||
monthDate.setMonth(monthDate.getMonth() + direction)
|
||||
dispatch(setCurrentMonth({ month: formatMonthFromDateString(monthDate) }))
|
||||
const clearSelectedCategory = () => {
|
||||
dispatch(setSelectedCategory(null))
|
||||
}
|
||||
|
||||
const isToday = month === formatMonthFromDateString(new Date())
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="column"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
sx={{ p: 2, height: '100%' }}
|
||||
sx={{ px: 2, height: '100%' }}
|
||||
>
|
||||
<Stack direction="column" justifyContent="flex-start" alignItems="center" spacing={2} sx={{ width: '100%' }}>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<BudgetMonthPicker
|
||||
popupState={monthPickerPopupState}
|
||||
currentMonth={month}
|
||||
minDate={availableMonths[0]}
|
||||
maxDate={availableMonths[availableMonths.length - 1]}
|
||||
/>
|
||||
<Stack
|
||||
className="budget-month-navigation"
|
||||
direction="row"
|
||||
justifyContent="space-around"
|
||||
alignItems="center"
|
||||
sx={{ width: '100%', margin: 'auto' }}
|
||||
>
|
||||
<IconButton
|
||||
disabled={prevMonthDisabled}
|
||||
onClick={() => navigateMonth(-1)}
|
||||
sx={{
|
||||
fontSize: theme.typography.h6.fontSize,
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
<ArrowBackIosIcon fontSize="large" variant="outlined" />
|
||||
</IconButton>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Stack direction="column" justifyContent="flex-start" alignItems="center" spacing={2} sx={{ width: '100%' }}>
|
||||
<BudgetMonthNavigator mini={false} />
|
||||
|
||||
<Stack direction="column" justifyContent="space-around" alignItems="center" sx={{ minHeight: '80px' }}>
|
||||
<Button
|
||||
{...bindTrigger(monthPickerPopupState)}
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Paper sx={{ p: 2, mx: 2 }}>
|
||||
<Stack direction="row" spacing={4} justifyContent="space-between" alignItems="center">
|
||||
<Box>
|
||||
<Typography
|
||||
style={{
|
||||
fontSize: theme.typography.h6.fontSize,
|
||||
}}
|
||||
>
|
||||
Available
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography
|
||||
style={{
|
||||
fontSize: theme.typography.h5.fontSize,
|
||||
fontWeight: 'bold',
|
||||
color: !isZero(toBeBudgeted) ? getBalanceColor(toBeBudgeted, theme) : theme.palette.grey[500],
|
||||
}}
|
||||
>
|
||||
{intlFormat(toBeBudgeted)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Paper sx={{ p: 2, mx: 2 }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: theme.typography.h6.fontSize,
|
||||
fontWeight: 'bold',
|
||||
color: theme.palette.secondary.main,
|
||||
pb: 2,
|
||||
}}
|
||||
>
|
||||
{new Date(Date.UTC(...month.split('-'))).toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
})}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
color="secondary"
|
||||
disabled={isToday}
|
||||
onClick={() => dispatch(setCurrentMonth({ month: formatMonthFromDateString(new Date()) }))}
|
||||
>
|
||||
<Typography style={{ fontSize: theme.typography.caption.fontSize, fontWeight: 'bold' }}>
|
||||
Jump to Today
|
||||
</Typography>
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<IconButton
|
||||
disabled={nextMonthDisabled}
|
||||
onClick={() => navigateMonth(1)}
|
||||
sx={{
|
||||
fontSize: theme.typography.h6.fontSize,
|
||||
// [`.Mui-disabled`]: { color: theme.palette.grey[500] },
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
<ArrowForwardIosIcon fontSize="large" />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Paper sx={{ width: '90%' }}>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
sx={{ p: 2, width: '100%' }}
|
||||
>
|
||||
<Box>
|
||||
<Typography
|
||||
style={{
|
||||
fontSize: theme.typography.h6.fontSize,
|
||||
}}
|
||||
>
|
||||
Available to Budget
|
||||
Monthly Summary
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography
|
||||
style={{
|
||||
fontSize: theme.typography.h5.fontSize,
|
||||
fontWeight: 'bold',
|
||||
color: !isZero(toBeBudgeted) ? getBalanceColor(toBeBudgeted, theme) : theme.palette.grey[500],
|
||||
}}
|
||||
<BudgetMonthCalculation />
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
sx={{ width: '100%', mt: 1, pt: 1, borderTop: `1px solid ${theme.palette.action.disabled}` }}
|
||||
>
|
||||
{intlFormat(toBeBudgeted)}
|
||||
</Typography>
|
||||
<Box>Underfunded</Box>
|
||||
<Box> {intlFormat(underfunded)}</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
{selectedCategory && (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Card sx={{ mx: 2 }}>
|
||||
<CardContent>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: theme.typography.h6.fontSize,
|
||||
pb: 1,
|
||||
}}
|
||||
>
|
||||
{selectedCategory.name} Activity
|
||||
</Typography>
|
||||
|
||||
<CategoryMonthActivity />
|
||||
</CardContent>
|
||||
|
||||
<CardActions>
|
||||
<Button size="small" onClick={clearSelectedCategory}>
|
||||
Close
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Paper sx={{ width: '90%', p: 2 }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: theme.typography.h5.fontSize,
|
||||
}}
|
||||
>
|
||||
Monthly Breakdown
|
||||
</Typography>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
sx={{ width: '100%', pb: 2, borderBottom: `1px solid ${theme.palette.action.disabled}` }}
|
||||
>
|
||||
<Box>Income</Box>
|
||||
<Box>{intlFormat(income)}</Box>
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
sx={{ width: '100%', pb: 2, pt: 2, borderBottom: `1px solid ${theme.palette.action.disabled}` }}
|
||||
>
|
||||
<Box>Activity</Box>
|
||||
<Box>{intlFormat(activity)}</Box>
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
sx={{ width: '100%', pb: 2, pt: 2, borderBottom: `1px solid ${theme.palette.action.disabled}` }}
|
||||
>
|
||||
<Box>Budgeted</Box>
|
||||
<Box>{intlFormat(budgeted)}</Box>
|
||||
</Stack>
|
||||
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ width: '100%', pt: 2 }}>
|
||||
<Box>Underfunded</Box>
|
||||
<Box> {intlFormat(underfunded)}</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
m: 4,
|
||||
pb: 2,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
|
||||
139
frontend/src/components/BudgetMonthNavigator.js
Normal file
139
frontend/src/components/BudgetMonthNavigator.js
Normal file
@ -0,0 +1,139 @@
|
||||
import Box from '@mui/material/Box'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { useTheme } from '@mui/styles'
|
||||
import Stack from '@mui/material/Stack'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import { formatMonthFromDateString, getDateFromString } from '../utils/Date'
|
||||
import BudgetMonthPicker from './BudgetMonthPicker'
|
||||
import Button from '@mui/material/Button'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { usePopupState, bindTrigger } from 'material-ui-popup-state/hooks'
|
||||
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIosNew'
|
||||
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'
|
||||
import { selectActiveBudget, setCurrentMonth } from '../redux/slices/Budgets'
|
||||
import EventIcon from '@mui/icons-material/Event'
|
||||
import List from '@mui/material/List'
|
||||
import ListItem from '@mui/material/ListItem'
|
||||
|
||||
export default function BudgetMonthNavigator({ mini }) {
|
||||
const theme = useTheme()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const month = useSelector(state => state.budgets.currentMonth)
|
||||
const availableMonths = useSelector(state => state.budgets.availableMonths)
|
||||
|
||||
const nextMonth = getDateFromString(month)
|
||||
nextMonth.setMonth(nextMonth.getMonth() + 1)
|
||||
const nextMonthDisabled = !availableMonths.includes(formatMonthFromDateString(nextMonth))
|
||||
|
||||
const prevMonth = getDateFromString(month)
|
||||
prevMonth.setMonth(prevMonth.getMonth() - 1)
|
||||
const prevMonthDisabled = !availableMonths.includes(formatMonthFromDateString(prevMonth))
|
||||
|
||||
const monthPickerPopupState = usePopupState({
|
||||
variant: 'popover',
|
||||
popupId: 'monthPicker',
|
||||
})
|
||||
|
||||
const navigateMonth = direction => {
|
||||
const monthDate = new Date(Date.UTC(...month.split('-')))
|
||||
monthDate.setDate(1)
|
||||
monthDate.setMonth(monthDate.getMonth() + direction)
|
||||
dispatch(setCurrentMonth({ month: formatMonthFromDateString(monthDate) }))
|
||||
}
|
||||
|
||||
const isToday = month === formatMonthFromDateString(new Date())
|
||||
|
||||
if (mini === true) {
|
||||
return (
|
||||
<List>
|
||||
<BudgetMonthPicker
|
||||
popupState={monthPickerPopupState}
|
||||
currentMonth={month}
|
||||
minDate={availableMonths[0]}
|
||||
maxDate={availableMonths[availableMonths.length - 1]}
|
||||
/>
|
||||
<ListItem button>
|
||||
<EventIcon
|
||||
fontSize="large"
|
||||
{...bindTrigger(monthPickerPopupState)}
|
||||
sx={{
|
||||
fontSize: theme.typography.h6.fontSize,
|
||||
// [`.Mui-disabled`]: { color: theme.palette.grey[500] },
|
||||
color: 'white',
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Stack
|
||||
className="budget-month-navigation"
|
||||
direction="row"
|
||||
justifyContent="space-evenly"
|
||||
alignItems="center"
|
||||
sx={{ width: '100%', margin: 'auto' }}
|
||||
>
|
||||
<BudgetMonthPicker
|
||||
popupState={monthPickerPopupState}
|
||||
currentMonth={month}
|
||||
minDate={availableMonths[0]}
|
||||
maxDate={availableMonths[availableMonths.length - 1]}
|
||||
/>
|
||||
<IconButton
|
||||
disabled={prevMonthDisabled}
|
||||
onClick={() => navigateMonth(-1)}
|
||||
sx={{
|
||||
fontSize: theme.typography.h6.fontSize,
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
<ArrowBackIosIcon fontSize="large" variant="outlined" />
|
||||
</IconButton>
|
||||
|
||||
<Stack direction="column" justifyContent="space-around" alignItems="center" sx={{ minHeight: '80px' }}>
|
||||
<Button
|
||||
{...bindTrigger(monthPickerPopupState)}
|
||||
sx={{
|
||||
fontSize: theme.typography.h6.fontSize,
|
||||
fontWeight: 'bold',
|
||||
color: theme.palette.secondary.main,
|
||||
}}
|
||||
>
|
||||
{new Date(Date.UTC(...month.split('-'))).toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
})}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
color="secondary"
|
||||
disabled={isToday}
|
||||
onClick={() => dispatch(setCurrentMonth({ month: formatMonthFromDateString(new Date()) }))}
|
||||
>
|
||||
<Typography style={{ fontSize: theme.typography.caption.fontSize, fontWeight: 'bold' }}>
|
||||
Jump to Today
|
||||
</Typography>
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<IconButton
|
||||
disabled={nextMonthDisabled}
|
||||
onClick={() => navigateMonth(1)}
|
||||
sx={{
|
||||
fontSize: theme.typography.h6.fontSize,
|
||||
// [`.Mui-disabled`]: { color: theme.palette.grey[500] },
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
<ArrowForwardIosIcon fontSize="large" />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
122
frontend/src/components/BudgetTable/BudgetMonthCalculation.js
Normal file
122
frontend/src/components/BudgetTable/BudgetMonthCalculation.js
Normal file
@ -0,0 +1,122 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import Stack from '@mui/material/Stack'
|
||||
import Box from '@mui/material/Box'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { useTheme } from '@mui/styles'
|
||||
import { getBalanceColor, inputToDinero, intlFormat, valueToDinero } from '../../utils/Currency'
|
||||
import { selectActiveBudget } from '../../redux/slices/Budgets'
|
||||
import { add } from 'dinero.js'
|
||||
|
||||
export default function BudgetMonthCalculation({ account }) {
|
||||
const theme = useTheme()
|
||||
|
||||
const month = useSelector(state => state.budgets.currentMonth)
|
||||
const budgetMonth = useSelector(state => {
|
||||
return state.budgetMonths.entities[month] || null
|
||||
})
|
||||
|
||||
const income = budgetMonth ? valueToDinero(budgetMonth.income) : inputToDinero(0)
|
||||
const activity = budgetMonth ? valueToDinero(budgetMonth.activity) : inputToDinero(0)
|
||||
|
||||
return (
|
||||
<Stack direction="row" justifyContent="space-evenly" alignItems="center" spacing={0}>
|
||||
<div>
|
||||
<Stack
|
||||
direction="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
// spacing={2}
|
||||
>
|
||||
<Typography
|
||||
style={{
|
||||
color: getBalanceColor(income, theme),
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
variant="subtitle1"
|
||||
>
|
||||
{intlFormat(income)}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
Income
|
||||
</Typography>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
...theme.typography.h6,
|
||||
}}
|
||||
>
|
||||
+
|
||||
</Box>
|
||||
|
||||
<div>
|
||||
<Stack
|
||||
direction="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
// spacing={2}
|
||||
>
|
||||
<Typography
|
||||
style={{
|
||||
color: getBalanceColor(activity, theme),
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
variant="subtitle1"
|
||||
>
|
||||
{intlFormat(activity)}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
Spent
|
||||
</Typography>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
...theme.typography.h6,
|
||||
}}
|
||||
>
|
||||
=
|
||||
</Box>
|
||||
|
||||
<div>
|
||||
<Stack
|
||||
direction="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
// spacing={2}
|
||||
>
|
||||
<Typography
|
||||
style={{
|
||||
color: getBalanceColor(add(income, activity), theme),
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
variant="subtitle1"
|
||||
>
|
||||
{intlFormat(add(income, activity))}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
Net
|
||||
</Typography>
|
||||
</Stack>
|
||||
</div>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@ -3,7 +3,7 @@ import { useSelector, useDispatch } from 'react-redux'
|
||||
import { createSelector } from '@reduxjs/toolkit'
|
||||
import { refreshBudget } from '../../redux/slices/Budgets'
|
||||
import { updateCategoryMonth, refreshBudgetCategory } from '../../redux/slices/BudgetMonths'
|
||||
import { updateCategory } from '../../redux/slices/Categories'
|
||||
import { setSelectedCategory, updateCategory } from '../../redux/slices/Categories'
|
||||
import { updateCategoryGroup, fetchCategories, categoryGroupsSelectors } from '../../redux/slices/CategoryGroups'
|
||||
import { categoriesSelectors } from '../../redux/slices/Categories'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
@ -502,6 +502,11 @@ export default function BudgetTable(props) {
|
||||
dispatch(refreshBudget())
|
||||
}
|
||||
|
||||
const onCategoryRowClick = row => {
|
||||
console.log(row)
|
||||
dispatch(setSelectedCategory(row.original.categoryId))
|
||||
}
|
||||
|
||||
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable(
|
||||
{
|
||||
columns,
|
||||
@ -526,8 +531,8 @@ export default function BudgetTable(props) {
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.tableBody,
|
||||
display: 'grid',
|
||||
gridTemplateColums: '1fr',
|
||||
gridTemplateRows: 'auto 1fr auto',
|
||||
// gridTemplateColums: '1fr',
|
||||
// gridTemplateRows: 'auto 1fr auto',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
@ -579,6 +584,9 @@ export default function BudgetTable(props) {
|
||||
prepareRow(row)
|
||||
return (
|
||||
<TableRow
|
||||
hover
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={() => onCategoryRowClick(row)}
|
||||
{...row.getRowProps({
|
||||
...(!row.original.groupId && { sx: { backgroundColor: theme.palette.action.hover } }),
|
||||
})}
|
||||
|
||||
112
frontend/src/components/CategoryMonthActivity.js
Normal file
112
frontend/src/components/CategoryMonthActivity.js
Normal file
@ -0,0 +1,112 @@
|
||||
import React from 'react'
|
||||
import { accountsSelectors } from '../redux/slices/Accounts'
|
||||
import { useSelector } from 'react-redux'
|
||||
import Box from '@mui/material/Box'
|
||||
import Table from '@mui/material/Table'
|
||||
import TableHead from '@mui/material/TableHead'
|
||||
import TableBody from '@mui/material/TableBody'
|
||||
import TableRow from '@mui/material/TableRow'
|
||||
import TableCell from '@mui/material/TableCell'
|
||||
import TableContainer from '@mui/material/TableContainer'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { intlFormat, FromAPI } from '../utils/Currency'
|
||||
import { formatMonthFromDateString } from '../utils/Date'
|
||||
import { useTheme } from '@mui/styles'
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'
|
||||
|
||||
export default function CategoryMonthActivity(props) {
|
||||
const theme = useTheme()
|
||||
|
||||
const month = useSelector(state => new Date(state.budgets.currentMonth))
|
||||
const accounts = useSelector(accountsSelectors.selectAll)
|
||||
const selectedCategory = useSelector(state => {
|
||||
if (!state.categories.selected) {
|
||||
return null
|
||||
}
|
||||
|
||||
return state.categories.entities[state.categories.selected]
|
||||
})
|
||||
const payees = useSelector(state => state.payees.entities)
|
||||
|
||||
if (!selectedCategory) {
|
||||
return <>No category selected</>
|
||||
}
|
||||
|
||||
const transactions = accounts.reduce((total, account) => {
|
||||
const filtered = Object.values(account.transactions.entities).filter(trx => {
|
||||
const trxDate = new Date(trx.date)
|
||||
if (trx.categoryId !== selectedCategory.id) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (trxDate.getMonth() === month.getMonth() && trxDate.getFullYear() === month.getFullYear()) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
return total.concat(filtered)
|
||||
}, [])
|
||||
|
||||
if (transactions.length === 0) {
|
||||
return <Box>No transactions for this month</Box>
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<TableContainer sx={{ maxHeight: 300 }}>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{ backgroundColor: theme.palette.background.tableHeader, pl: 1, pr: 0, width: 10, color: 'white' }}
|
||||
></TableCell>
|
||||
<TableCell sx={{ backgroundColor: theme.palette.background.tableHeader, color: 'white' }}>
|
||||
Payee
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{ backgroundColor: theme.palette.background.tableHeader, textAlign: 'right', color: 'white' }}
|
||||
>
|
||||
Amount
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{transactions.map(transaction => {
|
||||
transaction = FromAPI.transformTransaction(transaction)
|
||||
return (
|
||||
<TableRow>
|
||||
<Tooltip
|
||||
arrow
|
||||
title={
|
||||
<React.Fragment>
|
||||
<Typography sx={{ fontSize: theme.typography.caption.fontSize }}>
|
||||
Date: {formatMonthFromDateString(transaction.date)}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: theme.typography.caption.fontSize }}>
|
||||
Account: {accounts.find(acct => acct.id === transaction.accountId).name}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: theme.typography.caption.fontSize }}>
|
||||
Memo: {transaction.memo}{' '}
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
<TableCell sx={{ pl: 1, pr: 0, width: 10 }}>
|
||||
<InfoOutlinedIcon fontSize="small" sx={{ verticalAlign: 'middle' }} />
|
||||
</TableCell>
|
||||
</Tooltip>
|
||||
<TableCell>{payees[transaction.payeeId].name}</TableCell>
|
||||
<TableCell sx={{ textAlign: 'right' }}>{intlFormat(transaction.amount)}</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react'
|
||||
import Drawer from '@mui/material/Drawer'
|
||||
import MuiDrawer from '@mui/material/Drawer'
|
||||
import List from '@mui/material/List'
|
||||
import Divider from '@mui/material/Divider'
|
||||
import ListItem from '@mui/material/ListItem'
|
||||
@ -11,18 +11,13 @@ import ExpandMore from '@mui/icons-material/ExpandMore'
|
||||
import Collapse from '@mui/material/Collapse'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import AccountBalanceIcon from '@mui/icons-material/AccountBalance'
|
||||
import { inputToDinero, intlFormat, valueToDinero } from '../utils/Currency'
|
||||
import LogoutIcon from '@mui/icons-material/Logout'
|
||||
import api from '../api'
|
||||
import Grid from '@mui/material/Grid'
|
||||
import { add, isNegative } from 'dinero.js'
|
||||
import { useTheme } from '@mui/styles'
|
||||
import { styled, useTheme } from '@mui/styles'
|
||||
import AddCircleIcon from '@mui/icons-material/AddCircle'
|
||||
import Menu from '@mui/material/Menu'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import { usePopupState, bindTrigger, bindMenu } from 'material-ui-popup-state/hooks'
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
|
||||
import { usePopupState } from 'material-ui-popup-state/hooks'
|
||||
import { accountsSelectors, editAccount, fetchAccounts } from '../redux/slices/Accounts'
|
||||
import { setTheme } from '../redux/slices/App'
|
||||
import { selectActiveBudget } from '../redux/slices/Budgets'
|
||||
@ -30,9 +25,51 @@ import Brightness4Icon from '@mui/icons-material/Brightness4'
|
||||
import Brightness7Icon from '@mui/icons-material/Brightness7'
|
||||
import Settings from './Settings/Settings'
|
||||
import SettingsIcon from '@mui/icons-material/Settings'
|
||||
import MailOutlineIcon from '@mui/icons-material/MailOutline'
|
||||
import Box from '@mui/material/Box'
|
||||
import Stack from '@mui/material/Stack'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import Avatar from '@mui/material/Avatar'
|
||||
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
|
||||
import AccountBalanceIcon from '@mui/icons-material/AccountBalance'
|
||||
|
||||
const drawerWidth = 350
|
||||
const drawerWidth = 300
|
||||
|
||||
const openedMixin = theme => ({
|
||||
width: drawerWidth,
|
||||
transition: theme.transitions.create('width', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
overflowX: 'hidden',
|
||||
})
|
||||
|
||||
const closedMixin = theme => ({
|
||||
transition: theme.transitions.create('width', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
overflowX: 'hidden',
|
||||
width: `calc(${theme.spacing(9)} + 1px)`,
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
width: `calc(${theme.spacing(9)} + 1px)`,
|
||||
},
|
||||
})
|
||||
|
||||
const Drawer = styled(MuiDrawer, { shouldForwardProp: prop => prop !== 'open' })(({ theme, open }) => ({
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
boxSizing: 'border-box',
|
||||
...(open && {
|
||||
...openedMixin(theme),
|
||||
'& .MuiDrawer-paper': openedMixin(theme),
|
||||
}),
|
||||
...(!open && {
|
||||
...closedMixin(theme),
|
||||
'& .MuiDrawer-paper': closedMixin(theme),
|
||||
}),
|
||||
}))
|
||||
|
||||
export default function AppDrawer(props) {
|
||||
const dispatch = useDispatch()
|
||||
@ -49,6 +86,7 @@ export default function AppDrawer(props) {
|
||||
/**
|
||||
* State block
|
||||
*/
|
||||
const [drawerOpen, setDrawerOpen] = useState(true)
|
||||
const [accountsListOpen, setAccountsListOpen] = useState({ ACCOUNTS: true, OFF_BUDGET: true })
|
||||
const [selectedItem, setSelectedItem] = useState('Budget')
|
||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||
@ -102,39 +140,46 @@ export default function AppDrawer(props) {
|
||||
|
||||
return (
|
||||
<List dense={true}>
|
||||
<ListItemButton onClick={() => listItemClicked(key)}>
|
||||
<Grid container direction="row" justifyContent="space-between" alignItems="center">
|
||||
<div>
|
||||
<Grid container direction="row" justifyContent="space-between" alignItems="center">
|
||||
<ListItemIcon size="small" edge="end" style={{ minWidth: '20px' }}>
|
||||
{accountsListOpen[key] ? (
|
||||
<ExpandMore sx={{ color: theme.palette.secondary.main }} fontSize="small" />
|
||||
) : (
|
||||
<ChevronRightIcon sx={{ color: theme.palette.secondary.main }} fontSize="small" />
|
||||
)}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={label}
|
||||
primaryTypographyProps={{
|
||||
// variant: 'caption',
|
||||
style: { fontWeight: 'bold' },
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ListItem button onClick={() => listItemClicked(key)}>
|
||||
{drawerOpen && (
|
||||
<>
|
||||
<ListItemIcon size="small" edge="end" style={{ minWidth: '20px' }}>
|
||||
{accountsListOpen[key] ? (
|
||||
<ExpandMore sx={{ color: theme.palette.secondary.main }} fontSize="small" />
|
||||
) : (
|
||||
<ChevronRightIcon sx={{ color: theme.palette.secondary.main }} fontSize="small" />
|
||||
)}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
edge="end"
|
||||
secondary={intlFormat(balance)}
|
||||
secondaryTypographyProps={{
|
||||
fontWeight: 'bold',
|
||||
sx: { color: balanceColor },
|
||||
}}
|
||||
primary={
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Typography>{label}</Typography>
|
||||
|
||||
<Typography
|
||||
sx={{
|
||||
color: balanceColor,
|
||||
}}
|
||||
>
|
||||
{intlFormat(balance)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
</ListItemButton>
|
||||
</>
|
||||
)}
|
||||
{!drawerOpen && (
|
||||
<>
|
||||
<ListItemText primary={<AccountBalanceIcon />} />
|
||||
<ListItemIcon size="small" edge="end" style={{ minWidth: '20px' }}>
|
||||
{accountsListOpen[key] ? (
|
||||
<ExpandMore sx={{ color: theme.palette.secondary.main }} fontSize="small" />
|
||||
) : (
|
||||
<ChevronRightIcon sx={{ color: theme.palette.secondary.main }} fontSize="small" />
|
||||
)}
|
||||
</ListItemIcon>
|
||||
</>
|
||||
)}
|
||||
</ListItem>
|
||||
|
||||
<Collapse in={accountsListOpen[key]} timeout="auto" unmountOnExit>
|
||||
<List dense={true} component="div" disablePadding>
|
||||
@ -169,11 +214,16 @@ export default function AppDrawer(props) {
|
||||
props.onAddAccountClick()
|
||||
}
|
||||
|
||||
const toggleDrawer = () => {
|
||||
setDrawerOpen(!drawerOpen)
|
||||
}
|
||||
|
||||
const AccountItem = account => {
|
||||
const balance = valueToDinero(account.balance)
|
||||
const balanceColor = isNegative(balance) ? theme.palette.error.main : theme.palette.secondary.main
|
||||
return (
|
||||
<ListItemButton
|
||||
<ListItem
|
||||
button
|
||||
key={`account-${account.id}`}
|
||||
selected={selectedItem === `account-${account.id}`}
|
||||
onClick={() => listItemClicked(`account-${account.id}`, `/accounts/${account.id}`)}
|
||||
@ -195,33 +245,43 @@ export default function AppDrawer(props) {
|
||||
DragState.dropAccount = -1
|
||||
}}
|
||||
>
|
||||
<Grid container direction="row" justifyContent="space-between" alignItems="center">
|
||||
<div>
|
||||
<ListItemText
|
||||
sx={{ maxWidth: 150 }}
|
||||
primary={account.name}
|
||||
primaryTypographyProps={{
|
||||
style: {
|
||||
fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{drawerOpen === false && (
|
||||
<ListItemIcon>
|
||||
<Avatar sx={{ width: 30, height: 30 }}>{account.name[0]}</Avatar>
|
||||
</ListItemIcon>
|
||||
)}
|
||||
{drawerOpen === true && (
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ width: '100%' }}>
|
||||
<div>
|
||||
<ListItemText
|
||||
sx={{ maxWidth: 150 }}
|
||||
primary={account.name}
|
||||
primaryTypographyProps={{
|
||||
style: {
|
||||
fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ListItemText
|
||||
secondary={intlFormat(balance)}
|
||||
secondaryTypographyProps={{
|
||||
fontWeight: 'bold',
|
||||
color: balanceColor,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
</ListItemButton>
|
||||
<div>
|
||||
{/* {selectedItem === `account-${account.id}` && (
|
||||
<BalanceCalculation account={FromAPI.transformAccount(account)} />
|
||||
)} */}
|
||||
<ListItemText
|
||||
secondary={intlFormat(balance)}
|
||||
secondaryTypographyProps={{
|
||||
fontWeight: 'bold',
|
||||
color: balanceColor,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
</ListItem>
|
||||
)
|
||||
}
|
||||
|
||||
@ -235,6 +295,7 @@ export default function AppDrawer(props) {
|
||||
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
open={drawerOpen}
|
||||
sx={{
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
@ -242,38 +303,43 @@ export default function AppDrawer(props) {
|
||||
}}
|
||||
>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemText primary={budget.name} />
|
||||
<SettingsIcon
|
||||
// {...bindTrigger(menuPopupState)}
|
||||
onClick={openSettings}
|
||||
sx={{ color: theme.palette.secondary.main }}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
/>
|
||||
<ListItem button onClick={toggleDrawer}>
|
||||
{drawerOpen && <ListItemText primary={budget.name} />}
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
mr: '-30px', // wish there was a better way to position this further right?
|
||||
}}
|
||||
>
|
||||
{drawerOpen === false ? (
|
||||
<ChevronRightIcon sx={{ color: theme.palette.secondary.main }} />
|
||||
) : (
|
||||
<ChevronLeftIcon sx={{ color: theme.palette.secondary.main }} />
|
||||
)}
|
||||
</ListItemIcon>
|
||||
</ListItem>
|
||||
|
||||
{/* <Menu MenuListProps={{ dense: true }} {...bindMenu(menuPopupState)}>
|
||||
<MenuItem onClick={openSettings}>Settings</MenuItem>
|
||||
<MenuItem onClick={onAddAccountClick}>Add Account</MenuItem>
|
||||
</Menu> */}
|
||||
</List>
|
||||
|
||||
<Box sx={{ pt: 2 }}>
|
||||
<Divider />
|
||||
</Box>
|
||||
|
||||
<List>
|
||||
{menuItems.map((menuItemConfig, index) => (
|
||||
<ListItemButton
|
||||
<ListItem
|
||||
button
|
||||
key={menuItemConfig.name}
|
||||
onClick={() => listItemClicked(menuItemConfig.name, menuItemConfig.path)}
|
||||
selected={selectedItem === menuItemConfig.name}
|
||||
>
|
||||
<ListItemIcon style={{ minWidth: '40px' }}>
|
||||
<ListItemIcon>
|
||||
{index % 2 === 0 ? <MailIcon sx={{ color: theme.palette.secondary.main }} /> : <MailIcon />}
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={menuItemConfig.name} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
|
||||
<Divider />
|
||||
{/* <Divider /> */}
|
||||
|
||||
{budgetAccounts.length > 0 && AccountList('ACCOUNTS', budgetAccounts)}
|
||||
|
||||
@ -281,21 +347,46 @@ export default function AppDrawer(props) {
|
||||
|
||||
<List dense={true}>
|
||||
<ListItemButton>
|
||||
<ListItemIcon size="small" style={{ minWidth: '20px' }}>
|
||||
<AddCircleIcon
|
||||
style={{
|
||||
color: theme.palette.secondary.main,
|
||||
fontSize: theme.typography.subtitle2.fontSize,
|
||||
}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Add Account" onClick={() => props.onAddAccountClick()} />
|
||||
{drawerOpen && (
|
||||
<>
|
||||
<ListItemIcon size="small" style={{ minWidth: '20px' }}>
|
||||
<AddCircleIcon
|
||||
style={{
|
||||
color: theme.palette.secondary.main,
|
||||
fontSize: theme.typography.subtitle2.fontSize,
|
||||
}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Add Account" onClick={props.onAddAccountClick} />
|
||||
</>
|
||||
)}
|
||||
{drawerOpen === false && (
|
||||
<>
|
||||
<ListItemIcon>
|
||||
<AddCircleIcon
|
||||
onClick={props.onAddAccountClick}
|
||||
style={{
|
||||
color: theme.palette.secondary.main,
|
||||
}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
</>
|
||||
)}
|
||||
</ListItemButton>
|
||||
</List>
|
||||
|
||||
<List dense={true} style={{ marginTop: 'auto' }}>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton>
|
||||
<ListItemButton onClick={openSettings}>
|
||||
<ListItemIcon>
|
||||
<SettingsIcon sx={{ color: theme.palette.secondary.main }} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Settings" />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton onClick={toggleTheme}>
|
||||
<ListItemIcon>
|
||||
{currentTheme === 'dark' ? (
|
||||
<Brightness7Icon sx={{ color: theme.palette.secondary.main }} />
|
||||
@ -303,15 +394,16 @@ export default function AppDrawer(props) {
|
||||
<Brightness4Icon sx={{ color: theme.palette.secondary.main }} />
|
||||
)}
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Toggle Theme" onClick={toggleTheme} />
|
||||
<ListItemText primary="Toggle Theme" />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton>
|
||||
<ListItemButton onClick={logout}>
|
||||
<ListItemIcon>
|
||||
<LogoutIcon sx={{ color: theme.palette.secondary.main }} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Log Out" onClick={logout} />
|
||||
<ListItemText primary="Log Out" />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
@ -5,7 +5,7 @@ import AccountTable from '../components/AccountTable/AccountTable'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { accountsSelectors } from '../redux/slices/Accounts'
|
||||
import AccountDetails from '../components/AccountDetails'
|
||||
import Grid from '@mui/material/Grid'
|
||||
import Stack from '@mui/material/Stack'
|
||||
import Box from '@mui/material/Box'
|
||||
import { useTheme } from '@mui/styles'
|
||||
|
||||
@ -22,5 +22,35 @@ export default function Account(props) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <Box>{account && <AccountTable accountId={params.accountId} account={account} />}</Box>
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="flex-start"
|
||||
sx={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.details,
|
||||
borderLeft: `1px solid ${theme.palette.action.disabledBackground}`,
|
||||
width: 600,
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<AccountDetails accountId={params.accountId} name={account.name} />
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.tableBody,
|
||||
borderLeft: `1px solid ${theme.palette.action.disabledBackground}`,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<div style={{ maxWidth: '100%' }}>
|
||||
<Box>{account && <AccountTable accountId={params.accountId} account={account} />}</Box>
|
||||
</div>
|
||||
</Box>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,32 +1,43 @@
|
||||
import React from 'react'
|
||||
import BudgetTable from '../components/BudgetTable/BudgetTable'
|
||||
import BudgetDetails from '../components/BudgetDetails'
|
||||
import Grid from '@mui/material/Grid'
|
||||
import Box from '@mui/material/Box'
|
||||
import Stack from '@mui/material/Stack'
|
||||
import { useTheme } from '@mui/styles'
|
||||
import BudgetTableHeader from '../components/BudgetTable/BudgetTableHeader'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
export default function Budget(props) {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Box sx={{ backgroundColor: theme.palette.background.details, maxWidth: '100%' }}>
|
||||
<Grid container>
|
||||
<Grid item xs={4}>
|
||||
<BudgetDetails />
|
||||
</Grid>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="flex-start"
|
||||
sx={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.details,
|
||||
width: 600,
|
||||
maxWidth: 600,
|
||||
display: 'grid',
|
||||
height: '100vh',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<BudgetDetails />
|
||||
</Box>
|
||||
|
||||
<Grid
|
||||
item
|
||||
xs={8}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.tableBody,
|
||||
borderLeft: `1px solid ${theme.palette.action.disabledBackground}`,
|
||||
}}
|
||||
>
|
||||
<BudgetTable index={0} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.details,
|
||||
borderLeft: `1px solid ${theme.palette.action.disabledBackground}`,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<BudgetTable index={0} />
|
||||
</Box>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ const appSlice = createSlice({
|
||||
name: 'app',
|
||||
|
||||
initialState: {
|
||||
theme: localStorage.getItem('theme') || 'light',
|
||||
theme: localStorage.getItem('theme') || 'dark',
|
||||
},
|
||||
|
||||
reducers: {
|
||||
|
||||
@ -23,12 +23,18 @@ const categoriesAdapter = createEntityAdapter()
|
||||
const categoriesSlice = createSlice({
|
||||
name: 'categories',
|
||||
|
||||
initialState: categoriesAdapter.getInitialState(),
|
||||
initialState: categoriesAdapter.getInitialState({
|
||||
selected: null,
|
||||
}),
|
||||
|
||||
reducers: {
|
||||
setCategories: (state, { payload }) => {
|
||||
categoriesAdapter.setAll(state, payload)
|
||||
},
|
||||
|
||||
setSelectedCategory: (state, { payload }) => {
|
||||
state.selected = payload
|
||||
},
|
||||
},
|
||||
|
||||
extraReducers: builder => {
|
||||
@ -42,7 +48,7 @@ const categoriesSlice = createSlice({
|
||||
},
|
||||
})
|
||||
|
||||
export const { setCategories } = categoriesSlice.actions
|
||||
export const { setCategories, setSelectedCategory } = categoriesSlice.actions
|
||||
|
||||
export const categoriesSelectors = categoriesAdapter.getSelectors(state => state.categories)
|
||||
export const selectCategoryToGroupMap = createSelector(categoriesSelectors.selectAll, categories =>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user