efril #6

Merged
aefril merged 43 commits from efril into main 2025-09-11 18:58:35 +00:00
6 changed files with 608 additions and 2 deletions
Showing only changes of commit 66589abdd4 - Show all commits

View File

@ -0,0 +1,7 @@
import CashBankList from '@/views/apps/cash-bank/CashBankList'
const CashBankPage = () => {
return <CashBankList />
}
export default CashBankPage

View File

@ -116,6 +116,14 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => {
<SubMenu label={dictionary['navigation'].expenses} icon={<i className='tabler-cash' />}>
<MenuItem href={`/${locale}/apps/expense`}>{dictionary['navigation'].list}</MenuItem>
</SubMenu>
<MenuItem
href={`/${locale}/apps/cash-bank`}
icon={<i className='tabler-mail' />}
exactMatch={false}
activeUrl='/apps/cash-bank'
>
{dictionary['navigation'].cash_and_bank}
</MenuItem>
<SubMenu label={dictionary['navigation'].inventory} icon={<i className='tabler-salad' />}>
<SubMenu label={dictionary['navigation'].products}>
<MenuItem href={`/${locale}/apps/inventory/products/list`}>{dictionary['navigation'].list}</MenuItem>

View File

@ -123,6 +123,7 @@
"deliveries": "Deliveries",
"sales_orders": "Orders",
"quotes": "Quotes",
"expenses": "Expenses"
"expenses": "Expenses",
"cash_and_bank": "Cash & Bank"
}
}

View File

@ -123,6 +123,7 @@
"deliveries": "Pengiriman",
"sales_orders": "Pemesanan",
"quotes": "Penawaran",
"expenses": "Biaya"
"expenses": "Biaya",
"cash_and_bank": "Kas & Bank"
}
}

View File

@ -0,0 +1,263 @@
'use client'
// Next Imports
import dynamic from 'next/dynamic'
// 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 Box from '@mui/material/Box'
import Button from '@mui/material/Button'
// Third-party Imports
import type { ApexOptions } from 'apexcharts'
// Styled Component Imports
const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts'))
// Types
interface Balance {
amount: string | number
label: string
}
interface ChartData {
name: string
data: number[]
}
interface CashBankCardProps {
title: string
accountNumber: string
balances: Balance[]
chartData: ChartData[]
categories: string[]
buttonText?: string
buttonIcon?: string
onButtonClick?: () => void
chartColor?: string
height?: number
currency?: 'IDR' | 'USD' | 'EUR'
showButton?: boolean
maxValue?: number
enableHover?: boolean
}
const CashBankCard = ({
title,
accountNumber,
balances,
chartData,
categories,
onButtonClick,
chartColor = '#ff6b9d',
height = 300,
currency = 'IDR',
showButton = true,
maxValue,
enableHover = true
}: CashBankCardProps) => {
// Vars
const divider = 'var(--mui-palette-divider)'
const disabledText = 'var(--mui-palette-text-disabled)'
const primaryText = 'var(--mui-palette-text-primary)'
// Auto calculate maxValue if not provided
const calculatedMaxValue = maxValue || Math.max(...chartData.flatMap(series => series.data)) * 1.2
// Currency formatter
const formatCurrency = (value: number) => {
const formatters = {
IDR: new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}),
USD: new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0
}),
EUR: new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 0
})
}
return formatters[currency].format(value)
}
// Format balance display
const formatBalance = (amount: string | number) => {
if (typeof amount === 'string') return amount
return new Intl.NumberFormat('id-ID').format(amount)
}
// Y-axis label formatter based on currency
const formatYAxisLabel = (val: number) => {
if (currency === 'IDR') {
return (val / 1000000).toFixed(0) + '.000.000'
}
return (val / 1000).toFixed(0) + 'K'
}
const options: ApexOptions = {
chart: {
height: height,
type: 'area',
parentHeightOffset: 0,
zoom: { enabled: false },
toolbar: { show: false }
},
colors: [chartColor],
fill: {
type: 'gradient',
gradient: {
shade: 'light',
type: 'vertical',
shadeIntensity: 0.4,
gradientToColors: [chartColor],
inverseColors: false,
opacityFrom: 0.8,
opacityTo: 0.1,
stops: [0, 100]
}
},
stroke: {
curve: 'smooth',
width: 3
},
dataLabels: { enabled: false },
grid: {
show: true,
borderColor: divider,
strokeDashArray: 3,
padding: {
top: -10,
bottom: -10,
left: 20,
right: 20
},
xaxis: {
lines: { show: true }
},
yaxis: {
lines: { show: true }
}
},
tooltip: {
enabled: true,
y: {
formatter: function (val: number) {
return formatCurrency(val)
}
}
},
yaxis: {
min: 0,
max: calculatedMaxValue,
tickAmount: 7,
labels: {
style: {
colors: disabledText,
fontSize: '12px',
fontWeight: '400'
},
formatter: function (val: number) {
return formatYAxisLabel(val)
}
}
},
xaxis: {
axisBorder: { show: false },
axisTicks: { show: false },
crosshairs: {
stroke: { color: divider }
},
labels: {
style: {
colors: disabledText,
fontSize: '12px',
fontWeight: '400'
}
},
categories: categories
},
legend: {
show: false
}
}
return (
<Card
sx={{
height: '100%',
transition: 'transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out',
'&:hover': enableHover
? {
transform: 'translateY(-4px)',
boxShadow: '0 12px 35px rgba(0, 0, 0, 0.15)'
}
: {},
cursor: enableHover ? 'pointer' : 'default'
}}
>
<CardHeader
title={
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
<Box>
<Typography variant='h6' sx={{ fontWeight: 600, color: primaryText }}>
{title}
</Typography>
<Typography variant='body2' sx={{ color: disabledText }}>
{accountNumber}
</Typography>
</Box>
{showButton && (
<Button
variant='contained'
size='small'
onClick={onButtonClick}
className='bg-primary '
sx={{
textTransform: 'none',
fontSize: '12px'
}}
>
<i className='tabler-wallet text-sm mr-2' /> Atur Akun
</Button>
)}
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3 }}>
{balances.map((balance, index) => (
<Box key={index}>
<Typography variant='h4' sx={{ fontWeight: 700, color: primaryText }}>
{formatBalance(balance.amount)}
</Typography>
<Typography variant='body2' sx={{ color: disabledText }}>
{balance.label}
</Typography>
</Box>
))}
</Box>
</Box>
}
sx={{
pb: 0,
'& .MuiCardHeader-content': {
width: '100%'
}
}}
/>
<CardContent sx={{ pt: 0 }}>
<AppReactApexCharts type='area' width='100%' height={height} options={options} series={chartData} />
</CardContent>
</Card>
)
}
export default CashBankCard

View File

@ -0,0 +1,326 @@
'use client'
import { useState, useMemo, useEffect } from 'react'
import Grid from '@mui/material/Grid2'
import TextField, { TextFieldProps } from '@mui/material/TextField'
import InputAdornment from '@mui/material/InputAdornment'
import Box from '@mui/material/Box'
import Typography from '@mui/material/Typography'
import Chip from '@mui/material/Chip'
import FormControl from '@mui/material/FormControl'
import InputLabel from '@mui/material/InputLabel'
import Select from '@mui/material/Select'
import MenuItem from '@mui/material/MenuItem'
import CashBankCard from './CashBankCard' // Adjust import path as needed
import CustomTextField from '@/@core/components/mui/TextField'
// Types
interface BankAccount {
id: string
title: string
accountNumber: string
balances: Array<{
amount: string | number
label: string
}>
chartData: Array<{
name: string
data: number[]
}>
categories: string[]
chartColor?: string
currency: 'IDR' | 'USD' | 'EUR'
accountType: 'giro' | 'savings' | 'investment' | 'credit' | 'cash'
bank: string
status: 'active' | 'inactive' | 'blocked'
}
// Dummy Data
const dummyAccounts: BankAccount[] = [
{
id: '1',
title: 'Giro',
accountNumber: '1-10003',
balances: [
{ amount: '7.313.321', label: 'Saldo di bank' },
{ amount: '30.631.261', label: 'Saldo di kledo' }
],
chartData: [
{
name: 'Saldo',
data: [
20000000, 21000000, 20500000, 20800000, 21500000, 22000000, 25000000, 26000000, 28000000, 29000000, 30000000,
31000000
]
}
],
categories: ['Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des', 'Jan', 'Feb', 'Mar'],
chartColor: '#ff6b9d',
currency: 'IDR',
accountType: 'giro',
bank: 'Bank Mandiri',
status: 'active'
},
{
id: '2',
title: 'Tabungan Premium',
accountNumber: 'SAV-001234',
balances: [
{ amount: 15420000, label: 'Saldo Tersedia' },
{ amount: 18750000, label: 'Total Saldo' }
],
chartData: [
{
name: 'Balance',
data: [
12000000, 13500000, 14200000, 15000000, 15800000, 16200000, 17000000, 17500000, 18000000, 18200000, 18500000,
18750000
]
}
],
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
chartColor: '#4285f4',
currency: 'IDR',
accountType: 'savings',
bank: 'Bank BCA',
status: 'active'
},
{
id: '3',
title: 'Investment Portfolio',
accountNumber: 'INV-789012',
balances: [
{ amount: 125000, label: 'Portfolio Value' },
{ amount: 8750, label: 'Total Gains' }
],
chartData: [
{
name: 'Portfolio Value',
data: [110000, 115000, 112000, 118000, 122000, 119000, 125000, 128000, 126000, 130000, 127000, 125000]
}
],
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
currency: 'USD',
accountType: 'investment',
bank: 'Charles Schwab',
status: 'active'
},
{
id: '4',
title: 'Kartu Kredit Platinum',
accountNumber: 'CC-456789',
balances: [
{ amount: 2500000, label: 'Saldo Saat Ini' },
{ amount: 47500000, label: 'Limit Tersedia' }
],
chartData: [
{
name: 'Spending',
data: [
1200000, 1800000, 2200000, 1900000, 2100000, 2400000, 2800000, 2600000, 2300000, 2500000, 2700000, 2500000
]
}
],
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
currency: 'IDR',
accountType: 'credit',
bank: 'Bank BNI',
status: 'active'
},
{
id: '5',
title: 'Deposito Berjangka',
accountNumber: 'DEP-334455',
balances: [
{ amount: 50000000, label: 'Pokok Deposito' },
{ amount: 2500000, label: 'Bunga Terkumpul' }
],
chartData: [
{
name: 'Deposito Growth',
data: [
50000000, 50200000, 50420000, 50650000, 50880000, 51120000, 51360000, 51610000, 51860000, 52120000, 52380000,
52500000
]
}
],
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
currency: 'IDR',
accountType: 'savings',
bank: 'Bank BRI',
status: 'active'
},
{
id: '6',
title: 'Cash Management',
accountNumber: 'CSH-111222',
balances: [{ amount: 5000, label: 'Available Cash' }],
chartData: [
{
name: 'Cash Flow',
data: [4000, 4500, 4200, 4800, 5200, 4900, 5000, 5300, 5100, 5400, 5200, 5000]
}
],
categories: ['Q1', 'Q2', 'Q3', 'Q4', 'Q1', 'Q2', 'Q3', 'Q4', 'Q1', 'Q2', 'Q3', 'Q4'],
chartColor: '#00bcd4',
currency: 'USD',
accountType: 'cash',
bank: 'Wells Fargo',
status: 'active'
},
{
id: '7',
title: 'Rekening Bisnis',
accountNumber: 'BIZ-998877',
balances: [
{ amount: 85000000, label: 'Saldo Operasional' },
{ amount: 15000000, label: 'Dana Cadangan' }
],
chartData: [
{
name: 'Business Account',
data: [
70000000, 75000000, 80000000, 82000000, 85000000, 88000000, 90000000, 87000000, 85000000, 89000000, 92000000,
100000000
]
}
],
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
chartColor: '#ff9800',
currency: 'IDR',
accountType: 'giro',
bank: 'Bank Mandiri',
status: 'active'
},
{
id: '8',
title: 'Tabungan Pendidikan',
accountNumber: 'EDU-567890',
balances: [
{ amount: 25000000, label: 'Dana Pendidikan' },
{ amount: 3500000, label: 'Bunga Terkumpul' }
],
chartData: [
{
name: 'Education Savings',
data: [
20000000, 21000000, 22000000, 23000000, 24000000, 24500000, 25000000, 25500000, 26000000, 27000000, 28000000,
28500000
]
}
],
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
chartColor: '#3f51b5',
currency: 'IDR',
accountType: 'savings',
bank: 'Bank BCA',
status: 'inactive'
}
]
const DebouncedInput = ({
value: initialValue,
onChange,
debounce = 500,
...props
}: {
value: string | number
onChange: (value: string | number) => void
debounce?: number
} & Omit<TextFieldProps, 'onChange'>) => {
// States
const [value, setValue] = useState(initialValue)
useEffect(() => {
setValue(initialValue)
}, [initialValue])
useEffect(() => {
const timeout = setTimeout(() => {
onChange(value)
}, debounce)
return () => clearTimeout(timeout)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value])
return <CustomTextField {...props} value={value} onChange={e => setValue(e.target.value)} />
}
const CashBankList = () => {
const [searchQuery, setSearchQuery] = useState('')
// Handle button clicks
const handleAccountAction = (accountId: string, action: string) => {
console.log(`Action: ${action} for account: ${accountId}`)
// Implement your action logic here
}
// Filter and search logic
const filteredAccounts = useMemo(() => {
return dummyAccounts.filter(account => {
const matchesSearch =
account.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
account.accountNumber.toLowerCase().includes(searchQuery.toLowerCase()) ||
account.bank.toLowerCase().includes(searchQuery.toLowerCase())
return matchesSearch
})
}, [searchQuery])
return (
<Box sx={{ p: 3 }}>
{/* Search and Filters */}
<Box sx={{ mb: 4 }}>
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid size={{ xs: 12, md: 6 }}>
<DebouncedInput
value={searchQuery}
onChange={value => setSearchQuery(value as string)}
placeholder='Cari '
className='max-sm:is-full'
/>
</Grid>
</Grid>
</Box>
{/* Account Cards */}
<Grid container spacing={3}>
{filteredAccounts.length > 0 ? (
filteredAccounts.map(account => (
<Grid key={account.id} size={{ xs: 12, lg: 6, xl: 4 }}>
<CashBankCard
title={account.title}
accountNumber={account.accountNumber}
balances={account.balances}
chartData={account.chartData}
categories={account.categories}
chartColor={account.chartColor}
currency={account.currency}
showButton={account.accountType !== 'cash'}
onButtonClick={() => handleAccountAction(account.id, account.accountType || 'default')}
/>
</Grid>
))
) : (
<Grid size={{ xs: 12 }}>
<Box
sx={{
textAlign: 'center',
py: 8,
backgroundColor: 'grey.50',
borderRadius: 2
}}
>
<Typography variant='h6' color='text.secondary' gutterBottom>
Tidak ada akun yang ditemukan
</Typography>
<Typography variant='body2' color='text.secondary'>
Coba ubah kata kunci pencarian atau filter yang digunakan
</Typography>
</Box>
</Grid>
)}
</Grid>
</Box>
)
}
export default CashBankList