ui overhaul

This commit is contained in:
Alex Phillips 2022-02-09 20:22:36 -05:00
parent acb63f1b9c
commit 0253b30d39
14 changed files with 908 additions and 399 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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>
)
}

View 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>
)
}

View File

@ -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%',
}}
>

View 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>
)
}

View 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>
)
}

View File

@ -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 } }),
})}

View 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>
)
}

View File

@ -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>

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -4,7 +4,7 @@ const appSlice = createSlice({
name: 'app',
initialState: {
theme: localStorage.getItem('theme') || 'light',
theme: localStorage.getItem('theme') || 'dark',
},
reducers: {

View File

@ -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 =>