Skip to content

Instantly share code, notes, and snippets.

@Vetrivel-VP
Last active August 17, 2024 09:16
Show Gist options
  • Save Vetrivel-VP/ef10c6474926201f1f27184238b36112 to your computer and use it in GitHub Desktop.
Save Vetrivel-VP/ef10c6474926201f1f27184238b36112 to your computer and use it in GitHub Desktop.
Expence Tracker - Themeselection NextJs, Typescript, Mongodb
Source Link : https://themeselection.com/?ref=55
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
relationMode = "prisma"
}
model Budgets {
id String @id @default(auto()) @map("_id") @db.ObjectId
userId String
name String
amount String
icon String
expences Expences[] @relation("BudgetExpences")
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
}
model Expences {
id String @id @default(auto()) @map("_id") @db.ObjectId
userId String
name String
amount String
budgetId String @db.ObjectId
budget Budgets @relation("BudgetExpences", fields: [budgetId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
}
---------------------------------------------------------------------------------------------------------------------
custom-bread-cumb.tsx
---------------------------------------------------------------------------------------------------------------------
'use client'
import { Breadcrumbs, Typography } from '@mui/material'
import { Home } from 'lucide-react'
import Link from 'next/link'
import React from 'react'
interface CustomBreadCrumbProps {
breadCrumbPage: string
breadCrumbItem?: { link: string; label: string }[]
}
export const CustomBreadCrumb = ({ breadCrumbItem, breadCrumbPage }: CustomBreadCrumbProps) => {
return (
<Breadcrumbs aria-label='breadcrumbs' separator={'›'}>
<Link color='primary' href='/' className='flex items-center text-purple-500'>
<Home className='w-4 h-4 mr-2 p-0' />
Analytics
</Link>
{breadCrumbItem?.map(item => (
<Link key={item.link} color='neutral' href={item.link}>
{item.label}
</Link>
))}
<Typography>{breadCrumbPage}</Typography>
</Breadcrumbs>
)
}
---------------------------------------------------------------------------------------------------------------------
expense-table.tsx
---------------------------------------------------------------------------------------------------------------------
'use client'
// MUI Imports
import Typography from '@mui/material/Typography'
import Card from '@mui/material/Card'
import Chip from '@mui/material/Chip'
// Third-party Imports
import classnames from 'classnames'
// Components Imports
import CustomAvatar from '@core/components/mui/Avatar'
// Styles Imports
import tableStyles from '@core/styles/table.module.css'
import { BadgeDollarSign, DollarSign, Trash, Trash2 } from 'lucide-react'
import { Button } from '@mui/material'
import { useState } from 'react'
import toast from 'react-hot-toast'
import { useRouter } from 'next/navigation'
import axios from 'axios'
export type ExpenceTableColumns = {
id: string
budgetIcon?: string
name?: string
overallBudget?: string
expenceName: string
amount: string
date: string
}
interface ExpenceTable {
rowsData: ExpenceTableColumns[]
isBudgetDataInclude?: boolean
}
const Table = ({ rowsData, isBudgetDataInclude }: ExpenceTable) => {
const router = useRouter()
const [isDeleting, setIsDeleting] = useState(false)
const handleDelete = async (id: string) => {
setIsDeleting(true)
const response = await axios.delete(`/api/expences/${id}`)
toast.success('Expence Removed')
router.refresh()
try {
} catch (error) {
toast.error((error as Error)?.message)
} finally {
setIsDeleting(false)
}
}
return (
<Card>
<div className='overflow-x-auto'>
<table className={tableStyles.table}>
<thead>
<tr>
{isBudgetDataInclude && <th>Budget</th>}
<th>Expence</th>
<th>Amount</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{rowsData.map((row, index) => (
<tr key={index}>
{isBudgetDataInclude && (
<td className='!plb-1'>
<div className='flex items-center gap-3'>
<Typography variant='h3'>{row.budgetIcon}</Typography>
<div className='flex flex-col'>
<Typography color='text.primary' className='font-medium'>
{row.name}
</Typography>
<Typography variant='body2' className='flex items-center gap-1'>
<DollarSign className='w-3 h-3' />
{row.overallBudget}
</Typography>
</div>
</div>
</td>
)}
<td className='!plb-1'>
<Typography>{row.expenceName}</Typography>
</td>
<td className='!plb-1'>
<div className='flex gap-2 items-center '>
<DollarSign className='w-4 h-4' />
<Typography color='text.primary'>{row.amount}</Typography>
</div>
</td>
<td className='!pb-1'>
<Typography color='text.primary'>{row.date}</Typography>
</td>
<td className='!pb-1'>
<Button className='text-red-500' disabled={isDeleting} onClick={() => handleDelete(row.id)}>
<Trash2 />
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
)
}
export default Table
---------------------------------------------------------------------------------------------------------------------
Dashboard Analytics
---------------------------------------------------------------------------------------------------------------------
import { db } from '@/libs/db'
import { startOfMonth, endOfMonth, subMonths, format, endOfWeek, startOfWeek } from 'date-fns'
// Function to format numbers as 'K' or 'M'
export const formatNumber = (amount: number): string => {
if (amount >= 1_000_000) {
return `${(amount / 1_000_000).toFixed(1)} M` // Millions
} else if (amount >= 1_000) {
return `${(amount / 1_000).toFixed(1)} K` // Thousands
} else {
return amount.toFixed(2) // Less than 1000
}
}
export const getCurrentMonthBudgetsDetails = async () => {
// Determine the start and end dates of the current month
const now = new Date()
const startDate = startOfMonth(now)
const endDate = endOfMonth(now)
// Fetch all budgets with their expenses
const budgets = await db.budgets.findMany({
include: { expences: true },
where: {
expences: {
some: {
createdAt: {
gte: startDate,
lte: endDate
}
}
}
}
})
// Calculate total budget amount and total expenses for the current month
const totalBudgetAmount = budgets.reduce((total, budget) => total + parseFloat(budget.amount), 0)
const totalExpenses = budgets.reduce(
(acc, budget) =>
acc +
budget.expences.reduce((expAcc, expense) => {
const expenseDate = new Date(expense.createdAt)
// Only include expenses within the current month
return expenseDate >= startDate && expenseDate <= endDate ? expAcc + parseFloat(expense.amount) : expAcc
}, 0),
0
)
const savings = totalBudgetAmount - totalExpenses
const percentageUsed = totalBudgetAmount > 0 ? (totalExpenses / totalBudgetAmount) * 100 : 0
return {
totalBudgetAmount: formatNumber(totalBudgetAmount), // Apply formatting here
totalExpenses: formatNumber(totalExpenses),
savings: formatNumber(savings), // Apply formatting here
percentageUsed: `${percentageUsed.toFixed(2)}%` // Add percentage sign
}
}
// Function to get the metrics for a given month
const getMonthlyMetrics = async (startDate: Date, endDate: Date) => {
const budgets = await db.budgets.findMany({
include: { expences: true },
where: {
expences: {
some: {
createdAt: {
gte: startDate,
lte: endDate
}
}
}
}
})
// Calculate total budget amount and total expenses
const totalBudgetAmount = budgets.reduce((acc, budget) => acc + parseFloat(budget.amount), 0)
const totalExpenses = budgets.reduce(
(acc, budget) => acc + budget.expences.reduce((expAcc, expense) => expAcc + parseFloat(expense.amount), 0),
0
)
const savings = totalBudgetAmount - totalExpenses
const percentageUsed = totalBudgetAmount > 0 ? (totalExpenses / totalBudgetAmount) * 100 : 0
return {
totalBudgetAmount,
totalExpenses,
savings,
percentageUsed
}
}
// Function to get current and previous month metrics
export const getComparisonMetrics = async () => {
const now = new Date()
const startOfCurrentMonth = startOfMonth(now)
const endOfCurrentMonth = endOfMonth(now)
const startOfPreviousMonth = startOfMonth(subMonths(now, 1))
const endOfPreviousMonth = endOfMonth(subMonths(now, 1))
const currentMonthMetrics = await getMonthlyMetrics(startOfCurrentMonth, endOfCurrentMonth)
const previousMonthMetrics = await getMonthlyMetrics(startOfPreviousMonth, endOfPreviousMonth)
return {
currentMonth: {
monthName: format(startOfCurrentMonth, 'MMMM yyyy'),
totalBudgetAmount: formatNumber(currentMonthMetrics.totalBudgetAmount),
totalExpenses: formatNumber(currentMonthMetrics.totalExpenses),
savings: formatNumber(currentMonthMetrics.savings),
percentageUsed: `${currentMonthMetrics.percentageUsed.toFixed(2)}%`
},
previousMonth: {
monthName: format(startOfPreviousMonth, 'MMMM yyyy'),
totalBudgetAmount: formatNumber(previousMonthMetrics.totalBudgetAmount),
totalExpenses: formatNumber(previousMonthMetrics.totalExpenses),
savings: formatNumber(previousMonthMetrics.savings),
percentageUsed: `${previousMonthMetrics.percentageUsed.toFixed(2)}%`
}
}
}
export const getWeeklyExpenses = async (userId: string) => {
// Define the start date as today
const startDate = new Date()
// Calculate the end date as the end of the week from the start date
const endDate = endOfWeek(startDate)
// Query expenses within the given date range
const expenses = await db.expences.findMany({
where: {
userId,
createdAt: {
gte: startOfWeek(startDate), // Start of the week
lte: endDate // End of the week
}
}
})
// Initialize an array for each day of the week
const weeklyExpenses = Array(7).fill(0)
expenses.forEach(expense => {
const date = new Date(expense.createdAt)
const dayOfWeek = date.getDay() // Get day of the week (0 for Sunday, 6 for Saturday)
weeklyExpenses[dayOfWeek] += parseFloat(expense.amount) // Aggregate expenses by day
})
// Fetch total budget for the user
const budgets = await db.budgets.findMany({
where: {
userId
},
select: {
amount: true
}
})
// Calculate the total budget amount
const totalBudget = budgets.reduce((total, budget) => total + parseFloat(budget.amount), 0)
// Calculate total expenses for the week
const totalExpenses = weeklyExpenses.reduce((total, dailyExpense) => total + dailyExpense, 0)
// Calculate the percentage of expenses relative to the total budget
const percentage = totalBudget > 0 ? (totalExpenses / totalBudget) * 100 : 0
return {
weeklyExpenses,
totalExpenses: formatNumber(totalExpenses),
totalBudget: formatNumber(totalBudget),
percentage: `${percentage.toFixed(2)}%`
}
}
export const getTopExpenses = async (userId: string) => {
// Determine the start and end dates of the current month
const now = new Date()
const startDate = startOfMonth(now)
const endDate = endOfMonth(now)
// Query expenses for the current month greater than $100
const expenses = await db.expences.findMany({
where: {
userId,
amount: {
gte: '100' // Ensure amount is in the right format for comparison
},
createdAt: {
gte: startDate,
lte: endDate
}
},
orderBy: {
amount: 'desc' // Sort expenses in descending order by amount
}
})
return expenses.slice(0, 5)
}
---------------------------------------------------------------------------------------------------------------------
Transactions.tsx
'use client'
// MUI Imports
import Card from '@mui/material/Card'
import CardHeader from '@mui/material/CardHeader'
import CardContent from '@mui/material/CardContent'
import Typography from '@mui/material/Typography'
import Grid from '@mui/material/Grid'
// Type Imports
import type { ThemeColor } from '@core/types'
// Components Imports
import OptionMenu from '@core/components/option-menu'
import CustomAvatar from '@core/components/mui/Avatar'
import { useRouter } from 'next/navigation'
import toast from 'react-hot-toast'
// Props type for the component
interface TransactionsProps {
currentMonth: {
totalBudgetAmount: string
totalExpenses: string
savings: string
percentageUsed: string
monthName: string
}
previousMonth: {
totalBudgetAmount: string
totalExpenses: string
savings: string
percentageUsed: string
monthName: string
}
}
// Component
const Transactions = ({ currentMonth, previousMonth }: TransactionsProps) => {
// Prepare data
const data = [
{
stats: `$${currentMonth.totalBudgetAmount}`,
title: `Budget (${currentMonth.monthName})`,
color: 'primary' as ThemeColor,
icon: 'ri-pie-chart-2-line'
},
{
stats: `$${currentMonth.totalExpenses}`,
title: 'Expenses',
color: 'warning' as ThemeColor,
icon: 'ri-group-line'
},
{
stats: `$${currentMonth.savings}`,
title: 'Savings',
color: 'success' as ThemeColor,
icon: 'ri-money-dollar-circle-line'
},
{
stats: currentMonth.percentageUsed,
title: 'Used Percentage',
color: 'info' as ThemeColor,
icon: 'ri-macbook-line'
},
{
stats: `$${previousMonth.totalBudgetAmount}`,
title: `Budget (${previousMonth.monthName})`,
color: 'primary' as ThemeColor,
icon: 'ri-pie-chart-2-line'
},
{
stats: `$${previousMonth.totalExpenses}`,
title: 'Expenses',
color: 'warning' as ThemeColor,
icon: 'ri-group-line'
},
{
stats: `$${previousMonth.savings}`,
title: 'Savings',
color: 'success' as ThemeColor,
icon: 'ri-money-dollar-circle-line'
},
{
stats: previousMonth.percentageUsed,
title: 'Used Percentage',
color: 'info' as ThemeColor,
icon: 'ri-macbook-line'
}
]
const router = useRouter()
const handleRefresh = async () => {
router.refresh()
toast.success('Data Reloaded')
}
return (
<Card className='bs-full'>
<CardHeader
title='Budget Comparison Overview'
action={
<OptionMenu
iconClassName='text-textPrimary'
options={[{ text: 'Refresh', menuItemProps: { onClick: handleRefresh } }]}
/>
}
subheader={
<p className='mbs-3'>
<span className='font-medium text-textPrimary'>
Comparison between {currentMonth.monthName} and {previousMonth.monthName} Month
</span>
</p>
}
/>
<CardContent className='!pbs-5'>
<Grid container spacing={2}>
{data.map((item, index) => (
<Grid item xs={6} md={3} key={index}>
<div className='flex items-center gap-3'>
<CustomAvatar variant='rounded' color={item.color} className='shadow-xs'>
<i className={item.icon}></i>
</CustomAvatar>
<div>
<Typography>{item.title}</Typography>
<Typography variant='h5'>{item.stats}</Typography>
</div>
</div>
</Grid>
))}
</Grid>
</CardContent>
</Card>
)
}
export default Transactions
---------------------------------------------------------------------------------------------------------------------
Total Earnings
// MUI Imports
import Card from '@mui/material/Card'
import CardHeader from '@mui/material/CardHeader'
import CardContent from '@mui/material/CardContent'
import Avatar from '@mui/material/Avatar'
import LinearProgress from '@mui/material/LinearProgress'
import Typography from '@mui/material/Typography'
// Type Imports
import type { ThemeColor } from '@core/types'
// Components Imports
import OptionMenu from '@core/components/option-menu'
import { Budgets, Expences } from '@prisma/client'
import { formatNumber } from '../../../actions/analytics'
type DataType = {
title: string
imgSrc: string
amount: string
progress: number
subtitle: string
color?: ThemeColor
}
// Vars
const data: DataType[] = [
{
progress: 75,
title: 'Zipcar',
amount: '$24,895.65',
subtitle: 'Vuejs, React & HTML',
imgSrc: '/images/cards/zipcar.png'
},
{
progress: 50,
color: 'info',
title: 'Bitbank',
amount: '$8,650.20',
subtitle: 'Sketch, Figma & XD',
imgSrc: '/images/cards/bitbank.png'
},
{
progress: 20,
title: 'Aviato',
color: 'secondary',
amount: '$1,245.80',
subtitle: 'HTML & Angular',
imgSrc: '/images/cards/aviato.png'
}
]
interface BudgetClientProps {
budgets: (Budgets & { expences: Expences[] })[]
percentageUsed: string
}
const TotalEarning = ({ budgets, percentageUsed }: BudgetClientProps) => {
// Calculate the overall budget sum
const overallbudgetsum = budgets.reduce((total, budget) => {
const amount = parseFloat(budget.amount) || 0
return total + amount
}, 0)
// Calculate the total expenses
const totalExpenses = budgets.reduce((total, budget) => {
const budgetExpenses = budget.expences.reduce((expTotal, expense) => {
const amount = parseFloat(expense.amount) || 0
return expTotal + amount
}, 0)
return total + budgetExpenses
}, 0)
// Calculate the remaining overall budget
const remainingBudget = overallbudgetsum - totalExpenses
return (
<Card>
<CardHeader title='Budgets'></CardHeader>
<CardContent className='flex flex-col gap-11 md:mbs-2.5'>
<div>
<div className='flex items-center'>
<Typography variant='h3'>${formatNumber(overallbudgetsum)}</Typography>
<i className='ri-arrow-up-s-line align-bottom text-success'></i>
<Typography component='span' color='success.main'>
{percentageUsed} (used)
</Typography>
</div>
<Typography>
Overall expences - ${formatNumber(totalExpenses)} | Remaining Budget : ${formatNumber(remainingBudget)}
</Typography>
</div>
<div className='flex flex-col gap-6'>
{budgets.map((item, index) => (
<div key={index} className='flex items-center gap-3'>
{/* <Avatar src={item.icon} variant='rounded' className='bg-actionHover' /> */}
<Typography variant='h2' color='text.primary' className='font-medium'>
{item.icon}
</Typography>
<div className='flex justify-between items-center is-full flex-wrap gap-x-4 gap-y-2'>
<div className='flex flex-col gap-0.5'>
<Typography color='text.primary' className='font-medium'>
{item.name}
</Typography>
<Typography>Total Expences : {item.expences.length}</Typography>
</div>
<div className='flex flex-col gap-2 items-center'>
<Typography color='text.primary' className='font-medium'>
${formatNumber(parseFloat(item.amount))}
</Typography>
<BudgetProgress budget={item} />
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)
}
interface BudgetCardItemProps {
budget: Budgets & { expences: Expences[] }
}
const BudgetProgress = ({ budget }: BudgetCardItemProps) => {
// Calculate total expenses
const totalExpenses = budget.expences.reduce((total, expence) => {
const amount = parseFloat(expence.amount) || 0
return total + amount
}, 0)
// Calculate the budget amount and remaining amount
const budgetAmount = parseFloat(budget.amount) || 0
const remainingAmount = budgetAmount - totalExpenses
// Calculate the percentage of the budget used
const progress = budgetAmount > 0 ? (totalExpenses / budgetAmount) * 100 : 0
return <LinearProgress variant='determinate' value={progress} className='is-20 bs-1' color={'primary'} />
}
export default TotalEarning
---------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------------
Sales Over
// MUI Imports
import Card from '@mui/material/Card'
import CardHeader from '@mui/material/CardHeader'
import CardContent from '@mui/material/CardContent'
import Typography from '@mui/material/Typography'
// Third-party Imports
import classnames from 'classnames'
// Type Imports
import type { ThemeColor } from '@core/types'
// Components Imports
import OptionMenu from '@core/components/option-menu'
import CustomAvatar from '@core/components/mui/Avatar'
import { Expences } from '@prisma/client'
import { format } from 'date-fns'
import { formatNumber } from '../../../actions/analytics'
import { DollarSign } from 'lucide-react'
// type DataType = {
// avatarLabel: string
// avatarColor?: ThemeColor
// title: string
// subtitle: string
// sales: string
// trend: 'up' | 'down'
// trendPercentage: string
// }
// // Vars
// const data: DataType[] = [
// {
// avatarLabel: 'US',
// avatarColor: 'success',
// title: '$8,656k',
// subtitle: 'United states of america',
// sales: '894k',
// trend: 'up',
// trendPercentage: '25.8%'
// },
// {
// avatarLabel: 'UK',
// avatarColor: 'error',
// title: '$2,415k',
// subtitle: 'United kingdom',
// sales: '645k',
// trend: 'down',
// trendPercentage: '6.2%'
// },
// {
// avatarLabel: 'IN',
// avatarColor: 'warning',
// title: '$865k',
// subtitle: 'India',
// sales: '148k',
// trend: 'up',
// trendPercentage: '12.4%'
// },
// {
// avatarLabel: 'JA',
// avatarColor: 'secondary',
// title: '$745k',
// subtitle: 'Japan',
// sales: '86k',
// trend: 'down',
// trendPercentage: '11.9%'
// },
// {
// avatarLabel: 'KO',
// avatarColor: 'error',
// title: '$45k',
// subtitle: 'Korea',
// sales: '42k',
// trend: 'up',
// trendPercentage: '16.2%'
// }
// ]
interface PageClientProps {
expences: Expences[] | []
}
const SalesByCountries = ({ expences }: PageClientProps) => {
return (
<Card>
<CardHeader title='Recent Expences' />
<CardContent className='flex flex-col gap-[0.875rem]'>
{expences.map((item, index) => (
<div key={index} className='flex items-center gap-4'>
<CustomAvatar skin='light' color={'primary'}>
<DollarSign />
</CustomAvatar>
<div className='flex items-center justify-between is-full flex-wrap gap-x-4 gap-y-2'>
<div className='flex flex-col gap-1'>
<div className='flex items-center gap-1'>
<Typography color='text.primary' className='font-medium'>
{item.name}
</Typography>
{/* <div className={'flex items-center gap-1'}>
<i
className={classnames(
item.trend === 'up' ? 'ri-arrow-up-s-line' : 'ri-arrow-down-s-line',
item.trend === 'up' ? 'text-success' : 'text-error'
)}
></i>
<Typography color={item.trend === 'up' ? 'success.main' : 'error.main'}>
{item.trendPercentage}
</Typography>
</div> */}
</div>
<Typography variant='body2' color='text.disabled'>
{format(new Date(item.createdAt), 'MMMM dd, yyyy')}
</Typography>
</div>
<div className='flex flex-col gap-1'>
<Typography variant='body2' color='text.primary' className='font-medium'>
${formatNumber(parseFloat(item.amount))}
</Typography>
</div>
</div>
</div>
))}
</CardContent>
</Card>
)
}
export default SalesByCountries
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment