Skip to content

Commit

Permalink
feat(mobile): add statistic view and filter transactions by period (#240
Browse files Browse the repository at this point in the history
  • Loading branch information
bkdev98 authored Aug 21, 2024
1 parent 1ffb7b4 commit f12ee0b
Show file tree
Hide file tree
Showing 13 changed files with 777 additions and 113 deletions.
92 changes: 80 additions & 12 deletions apps/mobile/app/(app)/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { AmountFormat } from '@/components/common/amount-format'
import { ListSkeleton } from '@/components/common/list-skeleton'
import { Toolbar } from '@/components/common/toolbar'
import { HomeHeader } from '@/components/home/header'
import { WalletStatistics } from '@/components/home/wallet-statistics'
import { HomeFilter } from '@/components/home/select-filter'
import { TimeRangeControl } from '@/components/home/time-range-control'
import { HomeView, WalletStatistics } from '@/components/home/wallet-statistics'
import { HandyArrow } from '@/components/transaction/handy-arrow'
import { TransactionItem } from '@/components/transaction/transaction-item'
import { Text } from '@/components/ui/text'
Expand All @@ -28,13 +30,30 @@ export default function HomeScreen() {
const { colorScheme } = useColorScheme()
const [walletAccountId, setWalletAccountId] = useState<string | undefined>()
const queryClient = useQueryClient()
const [filter, setFilter] = useState<HomeFilter>(HomeFilter.All)
const [view, setView] = useState<HomeView>(HomeView.SpentThisWeek)
const [customTimeRange, setCustomTimeRange] = useState<{
from: Date
to: Date
}>({
from: dayjsExtended().subtract(10, 'year').startOf('year').toDate(),
to: dayjsExtended().add(10, 'year').endOf('year').toDate(),
})

const timeRange = useMemo(() => {
if (filter !== HomeFilter.All) {
return customTimeRange
}
return {
from: dayjsExtended().subtract(10, 'year').startOf('year').toDate(),
to: dayjsExtended().add(10, 'year').endOf('year').toDate(),
}
}, [customTimeRange, filter])

const { transactions, isLoading, isRefetching, refetch } = useTransactionList(
{
walletAccountId,
// FIXME: This should be dynamic @bkdev98
from: dayjsExtended().subtract(10, 'year').startOf('year').toDate(),
to: dayjsExtended().add(10, 'year').endOf('year').toDate(),
...timeRange,
},
)

Expand All @@ -43,6 +62,26 @@ export default function HomeScreen() {
queryClient.invalidateQueries({ queryKey: walletQueries.list._def })
}

const handleSetFilter = (filter: HomeFilter) => {
if (filter === HomeFilter.ByDay) {
setCustomTimeRange({
from: dayjsExtended().startOf('day').toDate(),
to: dayjsExtended().endOf('day').toDate(),
})
} else if (filter === HomeFilter.ByWeek) {
setCustomTimeRange({
from: dayjsExtended().startOf('week').toDate(),
to: dayjsExtended().endOf('week').toDate(),
})
} else if (filter === HomeFilter.ByMonth) {
setCustomTimeRange({
from: dayjsExtended().startOf('month').toDate(),
to: dayjsExtended().endOf('month').toDate(),
})
}
setFilter(filter)
}

const transactionsGroupByDate = useMemo(() => {
const groupedByDay = groupBy(transactions, (transaction) =>
format(new Date(transaction.date), 'yyyy-MM-dd'),
Expand All @@ -52,7 +91,7 @@ export default function HomeScreen() {
key,
title: formatDateShort(new Date(key)),
data: orderBy(transactions, 'date', 'desc'),
sum: sumBy(transactions, 'amount'),
sum: sumBy(transactions, 'amountInVnd'),
}))

return Object.values(sectionDict)
Expand All @@ -63,12 +102,27 @@ export default function HomeScreen() {
<HomeHeader
walletAccountId={walletAccountId}
onWalletAccountChange={setWalletAccountId}
filter={filter}
onFilterChange={handleSetFilter}
/>
{filter !== HomeFilter.All && (
<TimeRangeControl
filter={filter}
timeRange={timeRange}
onTimeRangeChange={setCustomTimeRange}
/>
)}
<SectionList
ListHeaderComponent={
<View className="p-6">
<WalletStatistics />
</View>
filter === HomeFilter.All ? (
<View className="p-6">
<WalletStatistics
view={view}
onViewChange={setView}
walletAccountId={walletAccountId}
/>
</View>
) : null
}
className="flex-1 bg-card"
contentContainerStyle={{ paddingBottom: bottom + 32 }}
Expand Down Expand Up @@ -97,12 +151,26 @@ export default function HomeScreen() {
// }}
onEndReachedThreshold={0.5}
ListFooterComponent={isLoading ? <ListSkeleton /> : null}
ListEmptyComponent={
filter !== HomeFilter.All && !isLoading ? (
<View className="mx-auto my-4">
<Text className="text-muted-foreground">{t(
i18n,
)`No transactions`}</Text>
</View>
) : null
}
extraData={filter}
/>
{!transactions.length && !isLoading && (
<View className="absolute right-6 bottom-20 z-50 flex-row gap-3">
<Text>{t(i18n)`Add your first transaction here`}</Text>
<HandyArrow className="mt-4 text-muted-foreground" />
</View>
<>
{filter === HomeFilter.All ? (
<View className="absolute right-6 bottom-20 z-50 flex-row gap-3">
<Text>{t(i18n)`Add your first transaction here`}</Text>
<HandyArrow className="mt-4 text-muted-foreground" />
</View>
) : null}
</>
)}
<LinearGradient
colors={[
Expand Down
11 changes: 6 additions & 5 deletions apps/mobile/components/budget/period-control.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { formatDateShort } from '@/lib/date'
import { formatDateRange } from '@/lib/date'
import type { BudgetPeriodConfig } from '@6pm/validation'
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react-native'
import { View } from 'react-native'
Expand All @@ -20,7 +20,7 @@ export function PeriodControl({
const couldGoForward = index < periodConfigs.length - 1

return (
<View className="h-12 w-full flex-row items-center justify-between gap-2 bg-muted px-4">
<View className="z-50 h-12 w-full flex-row items-center justify-between gap-2 bg-muted px-4">
<Button
size="icon"
variant="ghost"
Expand All @@ -30,9 +30,10 @@ export function PeriodControl({
<ChevronLeftIcon className="size-6 text-muted-foreground" />
</Button>
<Text className="font-medium">
{formatDateShort(periodConfigs[index].startDate!)}
{' - '}
{formatDateShort(periodConfigs[index].endDate!)}
{formatDateRange(
periodConfigs[index].startDate!,
periodConfigs[index].endDate!,
)}
</Text>
<Button
size="icon"
Expand Down
19 changes: 10 additions & 9 deletions apps/mobile/components/home/header.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
import { useUser } from '@clerk/clerk-expo'
import { Bell } from 'lucide-react-native'
import { Text, View } from 'react-native'
import { UserAvatar } from '../common/user-avatar'
import { Button } from '../ui/button'
import { type HomeFilter, SelectFilter } from './select-filter'
import { SelectWalletAccount } from './select-wallet-account'

type HomeHeaderProps = {
walletAccountId?: string
onWalletAccountChange?: (walletAccountId?: string) => void
filter?: HomeFilter
onFilterChange?: (filter: HomeFilter) => void
}

export function HomeHeader({
walletAccountId,
onWalletAccountChange,
filter,
onFilterChange,
}: HomeHeaderProps) {
const { user } = useUser()

return (
<View className="flex flex-row items-center justify-between bg-card px-6 pb-3">
<View className="flex flex-row items-center gap-3">
<View className="flex flex-row items-center justify-between gap-4 bg-card px-6 pb-3">
<View className="flex flex-1 flex-row items-center gap-3">
<UserAvatar user={user!} />
<View>
<Text className="font-medium font-sans text-muted-foreground text-sm">
<View className="flex-1">
<Text className="line-clamp-1 font-medium font-sans text-muted-foreground text-sm">
{user?.fullName ?? user?.primaryEmailAddress?.emailAddress}
</Text>
<SelectWalletAccount
Expand All @@ -30,9 +33,7 @@ export function HomeHeader({
/>
</View>
</View>
<Button variant="secondary" size="icon">
<Bell className="h-5 w-5 text-primary" />
</Button>
<SelectFilter value={filter} onSelect={onFilterChange} />
</View>
)
}
97 changes: 97 additions & 0 deletions apps/mobile/components/home/select-filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { cn } from '@/lib/utils'
import { t } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { FilterIcon } from 'lucide-react-native'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '../ui/select'

export enum HomeFilter {
All = 'ALL',
ByDay = 'BY_DAY',
ByWeek = 'BY_WEEK',
ByMonth = 'BY_MONTH',
}

type SelectFilterProps = {
value?: HomeFilter
onSelect?: (type: HomeFilter) => void
}

export function SelectFilter({
value = HomeFilter.All,
onSelect,
}: SelectFilterProps) {
const { i18n } = useLingui()

const options = [
{
value: HomeFilter.All,
label: t(i18n)`All entries`,
},
{
value: HomeFilter.ByDay,
label: t(i18n)`By day`,
},
{
value: HomeFilter.ByWeek,
label: t(i18n)`By week`,
},
{
value: HomeFilter.ByMonth,
label: t(i18n)`By month`,
},
]

return (
<Select
value={options.find((option) => option.value === value) ?? options[0]}
onValueChange={(selected) => {
onSelect?.(selected?.value as HomeFilter)
}}
>
<SelectTrigger
hideArrow
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
className={cn(
'!h-10 !px-2.5 flex-row items-center gap-2',
value !== HomeFilter.All && 'border-primary bg-primary',
)}
>
<FilterIcon
className={cn(
'h-5 w-5 text-primary',
value !== HomeFilter.All && 'text-primary-foreground',
)}
/>
{value !== HomeFilter.All && (
<SelectValue
className={cn('font-medium font-sans text-primary-foreground')}
placeholder={t(i18n)`All Accounts`}
>
{value}
</SelectValue>
)}
</SelectTrigger>
<SelectContent sideOffset={6} align="end">
<SelectGroup className="max-w-[260px] px-1">
{options.map((option) => (
<SelectItem
key={option.value}
value={option.value}
label={option.label}
className="flex-row items-center justify-between"
>
{option.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
)
}
Loading

0 comments on commit f12ee0b

Please sign in to comment.