diff --git a/src/app/[lang]/(dashboard)/(private)/apps/cash-bank/[id]/detail/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/cash-bank/[id]/detail/page.tsx new file mode 100644 index 0000000..8f2955f --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/cash-bank/[id]/detail/page.tsx @@ -0,0 +1,7 @@ +import CashBankDetail from '@/views/apps/cash-bank/detail' + +const CashBankDetailPage = () => { + return +} + +export default CashBankDetailPage diff --git a/src/types/apps/cashBankTypes.ts b/src/types/apps/cashBankTypes.ts new file mode 100644 index 0000000..c790274 --- /dev/null +++ b/src/types/apps/cashBankTypes.ts @@ -0,0 +1,10 @@ +export type CashBankType = { + id: number + date: string + description: string + reference: string + accept: number + send: number + balance: number + status: string +} diff --git a/src/views/apps/cash-bank/CashBankCard.tsx b/src/views/apps/cash-bank/CashBankCard.tsx index 94f1b10..e2116b9 100644 --- a/src/views/apps/cash-bank/CashBankCard.tsx +++ b/src/views/apps/cash-bank/CashBankCard.tsx @@ -13,6 +13,7 @@ import Button from '@mui/material/Button' // Third-party Imports import type { ApexOptions } from 'apexcharts' +import Link from 'next/link' // Styled Component Imports const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) @@ -36,7 +37,7 @@ interface CashBankCardProps { categories: string[] buttonText?: string buttonIcon?: string - onButtonClick?: () => void + href?: string chartColor?: string height?: number currency?: 'IDR' | 'USD' | 'EUR' @@ -51,7 +52,7 @@ const CashBankCard = ({ balances, chartData, categories, - onButtonClick, + href, chartColor = '#ff6b9d', height = 300, currency = 'IDR', @@ -220,7 +221,8 @@ const CashBankCard = ({ { const [searchQuery, setSearchQuery] = useState('') + const { lang: locale } = useParams() // Handle button clicks - const handleAccountAction = (accountId: string, action: string) => { - console.log(`Action: ${action} for account: ${accountId}`) - // Implement your action logic here - } + const handleAccountAction = () => {} // Filter and search logic const filteredAccounts = useMemo(() => { @@ -295,7 +296,7 @@ const CashBankList = () => { chartColor={account.chartColor} currency={account.currency} showButton={account.accountType !== 'cash'} - onButtonClick={() => handleAccountAction(account.id, account.accountType || 'default')} + href={getLocalizedUrl(`/apps/cash-bank/${account.accountNumber}/detail`, locale as Locale)} /> )) diff --git a/src/views/apps/cash-bank/detail/CashBankDetailCard.tsx b/src/views/apps/cash-bank/detail/CashBankDetailCard.tsx new file mode 100644 index 0000000..83efd63 --- /dev/null +++ b/src/views/apps/cash-bank/detail/CashBankDetailCard.tsx @@ -0,0 +1,62 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Type Imports +import type { UserDataType } from '@components/card-statistics/HorizontalWithSubtitle' + +// Component Imports +import HorizontalWithSubtitle from '@components/card-statistics/HorizontalWithSubtitle' + +// Vars +const data: UserDataType[] = [ + { + title: 'SALDO', + stats: '30.631.261', + avatarIcon: 'tabler-wallet', + avatarColor: 'success', + trend: 'positive', + trendNumber: '22.3%', + subtitle: 'Hari ini vs 30 hari lalu' + }, + { + title: 'MASUK', + stats: '3.486.871', + avatarIcon: 'tabler-arrow-down-circle', + avatarColor: 'success', + trend: 'negative', + trendNumber: '44.1%', + subtitle: 'Bulan ini vs tanggal sama bulan lalu' + }, + { + title: 'KELUAR', + stats: '3.163.000', + avatarIcon: 'tabler-arrow-up-circle', + avatarColor: 'info', + trend: 'positive', + trendNumber: '137.8%', + subtitle: 'Bulan ini vs tanggal sama bulan lalu' + }, + { + title: 'NET', + stats: '323.871', + avatarIcon: 'tabler-trending-up', + avatarColor: 'error', + trend: 'negative', + trendNumber: '93.4%', + subtitle: 'Bulan ini vs tanggal sama bulan lalu' + } +] + +const CashBankDetailCard = () => { + return ( + + {data.map((item, i) => ( + + + + ))} + + ) +} + +export default CashBankDetailCard diff --git a/src/views/apps/cash-bank/detail/CashBankDetailHeader.tsx b/src/views/apps/cash-bank/detail/CashBankDetailHeader.tsx new file mode 100644 index 0000000..ffd2b47 --- /dev/null +++ b/src/views/apps/cash-bank/detail/CashBankDetailHeader.tsx @@ -0,0 +1,19 @@ +import { Typography } from '@mui/material' + +interface Props { + title: string +} + +const CashBankDetailHeader = ({ title }: Props) => { + return ( + + + + {title} + + + + ) +} + +export default CashBankDetailHeader diff --git a/src/views/apps/cash-bank/detail/CashBankDetailTable.tsx b/src/views/apps/cash-bank/detail/CashBankDetailTable.tsx new file mode 100644 index 0000000..e2bb454 --- /dev/null +++ b/src/views/apps/cash-bank/detail/CashBankDetailTable.tsx @@ -0,0 +1,615 @@ +'use client' + +// React Imports +import { useCallback, useEffect, useMemo, useState } from 'react' + +// Next Imports +import Link from 'next/link' +import { useParams } from 'next/navigation' + +// MUI Imports +import Button from '@mui/material/Button' +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import Checkbox from '@mui/material/Checkbox' +import Chip from '@mui/material/Chip' +import IconButton from '@mui/material/IconButton' +import MenuItem from '@mui/material/MenuItem' +import { styled } from '@mui/material/styles' +import type { TextFieldProps } from '@mui/material/TextField' +import Typography from '@mui/material/Typography' + +// Third-party Imports +import type { RankingInfo } from '@tanstack/match-sorter-utils' +import { rankItem } from '@tanstack/match-sorter-utils' +import type { ColumnDef, FilterFn } from '@tanstack/react-table' +import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table' +import classnames from 'classnames' + +// Type Imports +import type { Locale } from '@configs/i18n' + +// Component Imports +import CustomTextField from '@core/components/mui/TextField' +import OptionMenu from '@core/components/option-menu' + +// Style Imports +import tableStyles from '@core/styles/table.module.css' +import { Box, CircularProgress, TablePagination } from '@mui/material' +import { useDispatch } from 'react-redux' +import TablePaginationComponent from '@/components/TablePaginationComponent' +import Loading from '@/components/layout/shared/Loading' +import { getLocalizedUrl } from '@/utils/i18n' +import StatusFilterTabs from '@/components/StatusFilterTab' +import DateRangePicker from '@/components/RangeDatePicker' + +// Updated CashBankType +export type CashBankType = { + id: number + date: string + description: string + reference: string + accept: number + send: number + balance: number + status: string +} + +declare module '@tanstack/table-core' { + interface FilterFns { + fuzzy: FilterFn + } + interface FilterMeta { + itemRank: RankingInfo + } +} + +type CashBankTypeWithAction = CashBankType & { + actions?: string +} + +// Styled Components +const Icon = styled('i')({}) + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ + itemRank + }) + + // Return if the item should be filtered in/out + return itemRank.passed +} + +const DebouncedInput = ({ + value: initialValue, + onChange, + debounce = 500, + ...props +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number +} & Omit) => { + // 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 setValue(e.target.value)} /> +} + +// Status color mapping for CashBank +const getStatusColor = (status: string) => { + switch (status) { + case 'Transaksi di Apskel': + return 'info' + case 'Transaksi di Bank': + return 'primary' + case 'Rekonsiliasi': + return 'success' + case 'Transaksi Berulang': + return 'secondary' + case 'Void': + return 'error' + case 'Menunggu Persetujuan': + return 'warning' + case 'Di tolak': + return 'error' + default: + return 'default' + } +} + +// Format currency +const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(amount) +} + +// Sample CashBank data +const cashBankData: CashBankType[] = [ + { + id: 1, + date: '2024-01-15', + description: 'Transfer ke supplier ABC', + reference: 'TRF-001', + accept: 0, + send: 5000000, + balance: 45000000, + status: 'Transaksi di Bank' + }, + { + id: 2, + date: '2024-01-16', + description: 'Pembayaran dari customer XYZ', + reference: 'PAY-002', + accept: 10000000, + send: 0, + balance: 55000000, + status: 'Rekonsiliasi' + }, + { + id: 3, + date: '2024-01-17', + description: 'Pembayaran gaji karyawan', + reference: 'SALARY-001', + accept: 0, + send: 25000000, + balance: 30000000, + status: 'Menunggu Persetujuan' + }, + { + id: 4, + date: '2024-01-18', + description: 'Transfer antar rekening', + reference: 'TRF-002', + accept: 0, + send: 2000000, + balance: 28000000, + status: 'Transaksi di Apskel' + }, + { + id: 5, + date: '2024-01-19', + description: 'Pembayaran otomatis tagihan listrik', + reference: 'AUTO-001', + accept: 0, + send: 1500000, + balance: 26500000, + status: 'Transaksi Berulang' + }, + { + id: 6, + date: '2024-01-20', + description: 'Transfer yang dibatalkan', + reference: 'TRF-003', + accept: 0, + send: 3000000, + balance: 26500000, + status: 'Void' + }, + { + id: 7, + date: '2024-01-21', + description: 'Pembayaran yang ditolak sistem', + reference: 'PAY-003', + accept: 0, + send: 5000000, + balance: 26500000, + status: 'Di tolak' + } +] + +// Column Definitions +const columnHelper = createColumnHelper() + +const CashBankDetailTable = () => { + const dispatch = useDispatch() + + // States + const [addTransactionOpen, setAddTransactionOpen] = useState(false) + const [rowSelection, setRowSelection] = useState({}) + const [currentPage, setCurrentPage] = useState(0) + const [pageSize, setPageSize] = useState(10) + const [openConfirm, setOpenConfirm] = useState(false) + const [transactionId, setTransactionId] = useState('') + const [search, setSearch] = useState('') + const [statusFilter, setStatusFilter] = useState('Semua') + const [filteredData, setFilteredData] = useState(cashBankData) + const [startDate, setStartDate] = useState(new Date()) + const [endDate, setEndDate] = useState(new Date()) + + // Hooks + const { lang: locale } = useParams() + + // Filter data based on search and status + useEffect(() => { + let filtered = cashBankData + + // Filter by search + if (search) { + filtered = filtered.filter( + transaction => + transaction.description.toLowerCase().includes(search.toLowerCase()) || + transaction.reference.toLowerCase().includes(search.toLowerCase()) || + transaction.status.toLowerCase().includes(search.toLowerCase()) + ) + } + + // Filter by status + if (statusFilter !== 'Semua') { + filtered = filtered.filter(transaction => transaction.status === statusFilter) + } + + setFilteredData(filtered) + setCurrentPage(0) + }, [search, statusFilter]) + + const totalCount = filteredData.length + const paginatedData = useMemo(() => { + const startIndex = currentPage * pageSize + return filteredData.slice(startIndex, startIndex + pageSize) + }, [filteredData, currentPage, pageSize]) + + // Calculate totals from filtered data + const subtotalAccept = useMemo(() => { + return filteredData.reduce((sum, transaction) => sum + transaction.accept, 0) + }, [filteredData]) + + const subtotalSend = useMemo(() => { + return filteredData.reduce((sum, transaction) => sum + transaction.send, 0) + }, [filteredData]) + + const netBalance = subtotalAccept - subtotalSend + + const handlePageChange = useCallback((event: unknown, newPage: number) => { + setCurrentPage(newPage) + }, []) + + const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10) + setPageSize(newPageSize) + setCurrentPage(0) + }, []) + + const handleDelete = () => { + setOpenConfirm(false) + } + + const handleTransactionClick = (transactionId: string) => { + console.log('Navigasi ke detail Transaksi:', transactionId) + } + + const handleStatusFilter = (status: string) => { + setStatusFilter(status) + } + + const columns = useMemo[]>( + () => [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ) + }, + columnHelper.accessor('date', { + header: 'Tanggal', + cell: ({ row }) => {row.original.date} + }), + columnHelper.accessor('description', { + header: 'Keterangan', + cell: ({ row }) => ( + + {row.original.description} + + ) + }), + columnHelper.accessor('reference', { + header: 'Referensi', + cell: ({ row }) => {row.original.reference || '-'} + }), + columnHelper.accessor('status', { + header: 'Status', + cell: ({ row }) => ( + + + + ) + }), + columnHelper.accessor('accept', { + header: 'Penerimaan', + cell: ({ row }) => ( + 0 ? 'text-green-600' : 'text-gray-500'}`}> + {row.original.accept > 0 ? formatCurrency(row.original.accept) : '-'} + + ) + }), + columnHelper.accessor('send', { + header: 'Pengeluaran', + cell: ({ row }) => ( + 0 ? 'text-red-600' : 'text-gray-500'}`}> + {row.original.send > 0 ? formatCurrency(row.original.send) : '-'} + + ) + }), + columnHelper.accessor('balance', { + header: 'Saldo', + cell: ({ row }) => ( + = 0 ? 'text-green-600' : 'text-red-600'}`}> + {formatCurrency(row.original.balance)} + + ) + }) + ], + [] + ) + + const table = useReactTable({ + data: paginatedData as CashBankType[], + columns, + filterFns: { + fuzzy: fuzzyFilter + }, + state: { + rowSelection, + pagination: { + pageIndex: currentPage, + pageSize + } + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount: Math.ceil(totalCount / pageSize) + }) + + return ( + <> + + {/* Filter Status Tabs and Range Date */} + + + + + + + + + setSearch(value as string)} + placeholder='Cari Transaksi Cash/Bank' + className='max-sm:is-full' + /> + + + 10 + 25 + 50 + + } + className='max-sm:is-full' + > + Ekspor + + } + href={getLocalizedUrl('/apps/cashbank/add', locale as Locale)} + > + Tambah Transaksi + + + + + + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + {header.isPlaceholder ? null : ( + <> + + {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: + }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} + + > + )} + + ))} + + ))} + + {filteredData.length === 0 ? ( + + + + Tidak ada data tersedia + + + + ) : ( + + {table.getRowModel().rows.map(row => { + return ( + + {row.getVisibleCells().map(cell => ( + {flexRender(cell.column.columnDef.cell, cell.getContext())} + ))} + + ) + })} + + {/* Subtotal Row */} + + + + Subtotal + + + + + + + + + {formatCurrency(subtotalAccept)} + + + + + {formatCurrency(subtotalSend)} + + + + = 0 ? 'text-green-600' : 'text-red-600'}`} + > + {formatCurrency(netBalance)} + + + + + {/* Total Row */} + + + + Total + + + + + + + + + + = 0 ? 'text-green-600' : 'text-red-600'}`} + > + {formatCurrency(netBalance)} + + + + + )} + + + + ( + + )} + count={totalCount} + rowsPerPage={pageSize} + page={currentPage} + onPageChange={handlePageChange} + onRowsPerPageChange={handlePageSizeChange} + rowsPerPageOptions={[10, 25, 50]} + /> + + > + ) +} + +export default CashBankDetailTable diff --git a/src/views/apps/cash-bank/detail/index.tsx b/src/views/apps/cash-bank/detail/index.tsx new file mode 100644 index 0000000..c213c42 --- /dev/null +++ b/src/views/apps/cash-bank/detail/index.tsx @@ -0,0 +1,23 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' +import CashBankDetailHeader from './CashBankDetailHeader' +import CashBankDetailTable from './CashBankDetailTable' +import CashBankDetailCard from './CashBankDetailCard' + +const CashBankDetail = () => { + return ( + + + + + + + + + + + + ) +} + +export default CashBankDetail