Skip to content
This repository has been archived by the owner on May 11, 2021. It is now read-only.

Commit

Permalink
Merge pull request #148 from igorkamyshev/categories
Browse files Browse the repository at this point in the history
Categories page
  • Loading branch information
igorkamyshev committed Apr 17, 2019
2 parents 41485dd + 1362e27 commit 7849626
Show file tree
Hide file tree
Showing 18 changed files with 453 additions and 17 deletions.
47 changes: 47 additions & 0 deletions front/pages/internal/categories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as React from 'react'

import { AppContext } from '@front/domain/AppContext'
import { fetchFirstTransactionDate } from '@front/domain/money/actions/fetchFirstTransactionDate'
import { fetchStatsCategories } from '@front/domain/money/actions/fetchStatsCategories'
import { getDefaultCurrency } from '@front/domain/user/selectors/getDefaultCurrency'
import { Categories } from '@front/features/categories'
import { GroupBy } from '@shared/enum/GroupBy'
import { createRangeForGroup } from '@front/helpers/createRangeForGroup'
import { getFirstTransactionDate } from '@front/domain/money/selectors/getFirstTransactionDate'

interface Query {
group?: GroupBy
}

export default class CateogiesPage extends React.Component<Query> {
public static isSecure = true

public static async getInitialProps({
reduxStore,
query,
}: AppContext<Query>) {
const { group } = query

await reduxStore.dispatch(fetchFirstTransactionDate() as any)
const firstTransactionDate = getFirstTransactionDate(reduxStore.getState())

const { from, to } = !!group
? createRangeForGroup(group)
: {
from: firstTransactionDate,
to: new Date(),
}

const currency = getDefaultCurrency(reduxStore.getState())

await reduxStore.dispatch(fetchStatsCategories(from, to, currency) as any)

return { group }
}

public render() {
const { group } = this.props

return <Categories group={group} />
}
}
4 changes: 4 additions & 0 deletions front/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@ module.exports = nextRoutes()
.add({ pattern: '/hello', page: 'internal/hello' })
.add({ pattern: '/app', page: 'internal/app' })
.add({ pattern: '/app/stats', page: 'internal/stats' })
.add({
pattern: '/app/stats/categories/:group?',
page: 'internal/categories',
})
.add({ pattern: '/app/history', page: 'internal/history' })
.add({ pattern: '/app/profile', page: 'internal/profile' })
3 changes: 2 additions & 1 deletion front/src/domain/AppContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ interface Request {
cookies: Params
}

export interface AppContext {
export interface AppContext<Query = never> {
reduxStore: Store
req: IncomingMessage & Request
res: OutgoingMessage & Response
query: Query
}
39 changes: 39 additions & 0 deletions front/src/features/categories/Categories.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.categories {
display: grid;

grid-template:
'aside chart'
/ 1fr 2fr;

gap: 16px;

@media (max-width: 1024px) {
grid-template:
'aside chart'
/ 1fr 1fr;
}

@media (max-width: 768px) {
grid-template:
'aside'
'chart';
}
}

.chart {
grid-area: chart;

& > * {
@media (max-width: 768px) {
margin-bottom: 32px;
}
}
}

.aside {
grid-area: aside;

& > *:not(:last-child) {
margin-bottom: 16px;
}
}
79 changes: 79 additions & 0 deletions front/src/features/categories/Categories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import useMedia from 'use-media'
import { useMappedState } from 'redux-react-hook'
import { useState } from 'react'

import { GroupBy } from '@shared/enum/GroupBy'
import { Container } from '@front/ui/components/layout/container'
import { getStatsCategoriesFetchingStatus } from '@front/domain/money/selectors/getStatsCategoriesFetchingStatus'
import { useMemoState } from '@front/domain/store'
import { getStatsCategories } from '@front/domain/money/selectors/getStatsCategories'
import { fetchStatsCategories } from '@front/domain/money/actions/fetchStatsCategories'
import { getDefaultCurrency } from '@front/domain/user/selectors/getDefaultCurrency'
import { PageHeader } from '@front/ui/components/layout/page-header'
import { Loader } from '@front/ui/components/layout/loader'
import { displayMoney } from '@shared/helpers/displayMoney'
import { PieChart } from '@front/ui/components/chart/pie-chart'

import { pushRoute } from '../routing'
import { PeriodChooser } from './features/period-chooser'
import { useDateRange } from './helpers/useDateRange'
import * as styles from './Categories.css'
import { GroupChooser } from './features/group-chooser'

interface Props {
group?: GroupBy
}

export const Categories = ({ group }: Props) => {
const fetching = useMappedState(getStatsCategoriesFetchingStatus)
const currency = useMappedState(getDefaultCurrency)
const isSmall = useMedia({ maxWidth: 768 })

const [previousPeriodNumber, setPreviousPeriodNumber] = useState(0)

const { from, to } = useDateRange(previousPeriodNumber, group)

const stats = useMemoState(
() => getStatsCategories(from, to, currency),
() => fetchStatsCategories(from, to, currency),
[from, to, currency],
)

return (
<Container>
<PageHeader title="Categories" onBack={() => pushRoute('/app/stats')} />

<section className={styles.categories}>
<aside className={styles.aside}>
<GroupChooser group={group} />
{group && (
<PeriodChooser
setPreviousPeriodNumber={setPreviousPeriodNumber}
previousPeriodNumber={previousPeriodNumber}
from={from}
to={to}
group={group}
/>
)}
</aside>

<div className={styles.chart}>
<Loader status={fetching}>
{stats.nonEmpty() && (
<PieChart
dataSets={stats.get().map(({ category, outcome }) => ({
name: category,
data: outcome,
}))}
displayValue={value =>
displayMoney(currency)(value, { withPenny: false })
}
fitToContainer={isSmall}
/>
)}
</Loader>
</div>
</section>
</Container>
)
}
49 changes: 49 additions & 0 deletions front/src/features/categories/features/group-chooser.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { GroupBy } from '@shared/enum/GroupBy'
import { Button, ButtonType } from '@front/ui/components/form/button'
import { Card } from '@front/ui/components/layout/card'
import { pushRoute } from '@front/features/routing'

interface Props {
group?: GroupBy
}

export const GroupChooser = ({ group }: Props) => {
const showYear = !group || group !== GroupBy.Year
const showMonth = !group || group !== GroupBy.Month
const showWhole = !!group

return (
<Card
title={'Group'}
actions={[
showYear && (
<Button
onClick={() => pushRoute('/app/stats/categories/year')}
type={ButtonType.Text}
>
Show year
</Button>
),
showMonth && (
<Button
onClick={() => pushRoute('/app/stats/categories/month')}
type={ButtonType.Text}
>
Show month
</Button>
),
showWhole && (
<Button
onClick={() => pushRoute('/app/stats/categories')}
type={ButtonType.Text}
>
Show all time
</Button>
),
].filter(Boolean)}
>
{!!group && <p>You see data for one {group}</p>}
{!group && <p>You see data for all time</p>}
</Card>
)
}
51 changes: 51 additions & 0 deletions front/src/features/categories/features/period-chooser.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { format } from 'date-fns'
import { GroupBy } from '@shared/enum/GroupBy'
import { useCallback } from 'react'
import { Button, ButtonType } from '@front/ui/components/form/button'
import { Card } from '@front/ui/components/layout/card'

interface Props {
previousPeriodNumber: number
setPreviousPeriodNumber: (t: (v: number) => number) => void
from: Date
to: Date
group: GroupBy
}

export const PeriodChooser = ({
from,
to,
group,
setPreviousPeriodNumber,
previousPeriodNumber,
}: Props) => {
const back = useCallback(() => setPreviousPeriodNumber(v => v + 1), [
setPreviousPeriodNumber,
])
const next = useCallback(() => setPreviousPeriodNumber(v => v - 1), [
setPreviousPeriodNumber,
])

return (
<Card
title={'Period'}
actions={[
<Button
onClick={back}
type={ButtonType.Text}
>{`Previous ${group}`}</Button>,
previousPeriodNumber > 0 && (
<Button
onClick={next}
type={ButtonType.Text}
>{`Next ${group}`}</Button>
),
].filter(Boolean)}
>
<p>
List of categories for the period from{' '}
<b>{format(from, 'YYYY.MM.DD')}</b> to <b>{format(to, 'YYYY.MM.DD')}</b>
</p>
</Card>
)
}
23 changes: 23 additions & 0 deletions front/src/features/categories/helpers/useDateRange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useMemo } from 'react'
import { useMappedState } from 'redux-react-hook'

import { createRangeForGroup } from '@front/helpers/createRangeForGroup'
import { GroupBy } from '@shared/enum/GroupBy'
import { getFirstTransactionDate } from '@front/domain/money/selectors/getFirstTransactionDate'

export const useDateRange = (previousPeriodNumber: number, group?: GroupBy) => {
const firstTransactionDate = useMappedState(getFirstTransactionDate)

const { from, to } = useMemo(() => {
if (group) {
return createRangeForGroup(group, previousPeriodNumber)
}

return {
from: firstTransactionDate,
to: new Date(),
}
}, [group, previousPeriodNumber])

return { from, to }
}
1 change: 1 addition & 0 deletions front/src/features/categories/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Categories } from './Categories'
10 changes: 10 additions & 0 deletions front/src/features/routing/transitions/animations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,14 @@ export const routeAnimations = [
nextRoute: '/app',
styles: backwards,
},
{
prevRoute: '/app/stats',
nextRoute: '/app/stats/(.+)',
styles: forwards,
},
{
prevRoute: '/app/stats/(.+)',
nextRoute: '/app/stats',
styles: backwards,
},
]
20 changes: 12 additions & 8 deletions front/src/features/statistics/features/categories/Categories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { startOfMonth, startOfYear, endOfMonth, endOfYear } from 'date-fns'
import { useMappedState } from 'redux-react-hook'
import { sortBy, take } from 'lodash'
import { useMemo } from 'react'
Expand All @@ -7,11 +6,13 @@ import { Currency } from '@shared/enum/Currency'
import { displayMoney } from '@shared/helpers/displayMoney'
import { GroupBy } from '@shared/enum/GroupBy'
import { getStatsCategoriesFetchingStatus } from '@front/domain/money/selectors/getStatsCategoriesFetchingStatus'
import { wantUTC } from '@front/helpers/wantUTC'
import { useMemoState } from '@front/domain/store'
import { getStatsCategories } from '@front/domain/money/selectors/getStatsCategories'
import { fetchStatsCategories } from '@front/domain/money/actions/fetchStatsCategories'
import { LoaderTable } from '@front/ui/components/layout/loader-table'
import { Button, ButtonType } from '@front/ui/components/form/button'
import { pushRoute } from '@front/features/routing'
import { createRangeForGroup } from '@front/helpers/createRangeForGroup'

interface Props {
className?: string
Expand Down Expand Up @@ -44,12 +45,7 @@ export const Categories = ({

const fetching = useMappedState(getStatsCategoriesFetchingStatus)

const [from, to] = useMemo(() => {
const start = group === GroupBy.Month ? startOfMonth : startOfYear
const end = group === GroupBy.Month ? endOfMonth : endOfYear

return [wantUTC(start)(new Date()), wantUTC(end)(new Date())]
}, [group])
const { from, to } = useMemo(() => createRangeForGroup(group), [group])

const stats = useMemoState(
() => getStatsCategories(from, to, currency),
Expand All @@ -72,6 +68,14 @@ export const Categories = ({
expectedRows={maxLength}
className={className}
hideHeader
footer={
<Button
type={ButtonType.Text}
onClick={() => pushRoute(`/app/stats/categories/${group}`)}
>
Details
</Button>
}
/>
)
}
Loading

0 comments on commit 7849626

Please sign in to comment.