From cc3a0c07a93f53b6e98a7cbec87128280279c47b Mon Sep 17 00:00:00 2001 From: efrilm Date: Wed, 10 Sep 2025 00:19:11 +0700 Subject: [PATCH] Sales Invoice Page --- .../(private)/apps/sales/sales-bills/page.tsx | 7 + .../layout/vertical/VerticalMenu.tsx | 1 + src/data/dictionaries/en.json | 3 +- src/data/dictionaries/id.json | 3 +- src/data/dummy/sales.ts | 244 +++++++++ src/types/apps/salesTypes.ts | 12 + .../sales-bill/list/SalesBillListTable.tsx | 464 ++++++++++++++++++ .../apps/sales/sales-bill/list/index.tsx | 19 + 8 files changed, 751 insertions(+), 2 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/sales/sales-bills/page.tsx create mode 100644 src/data/dummy/sales.ts create mode 100644 src/types/apps/salesTypes.ts create mode 100644 src/views/apps/sales/sales-bill/list/SalesBillListTable.tsx create mode 100644 src/views/apps/sales/sales-bill/list/index.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/sales/sales-bills/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/sales/sales-bills/page.tsx new file mode 100644 index 0000000..314d4d3 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/sales/sales-bills/page.tsx @@ -0,0 +1,7 @@ +import SalesBillList from '@/views/apps/sales/sales-bill/list' + +const SalesBillPage = () => { + return +} + +export default SalesBillPage diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index 0de51bc..7e30be1 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -93,6 +93,7 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { }> {dictionary['navigation'].overview} + {dictionary['navigation'].invoices} {/* {dictionary['navigation'].view} */} }> diff --git a/src/data/dictionaries/en.json b/src/data/dictionaries/en.json index 63e0ece..86399b1 100644 --- a/src/data/dictionaries/en.json +++ b/src/data/dictionaries/en.json @@ -118,6 +118,7 @@ "purchase_orders": "Purchase Orders", "purchase_bills": "Purchase Bills", "purchase_delivery": "Purchase Delivery", - "purchase_quotes": "Purchase Quotes" + "purchase_quotes": "Purchase Quotes", + "invoices": "Invoices" } } diff --git a/src/data/dictionaries/id.json b/src/data/dictionaries/id.json index ecdc0a1..9126d73 100644 --- a/src/data/dictionaries/id.json +++ b/src/data/dictionaries/id.json @@ -118,6 +118,7 @@ "purchase_orders": "Pesanan Pembelian", "purchase_bills": "Tagihan Pembelian", "purchase_delivery": "Pengiriman Pembelian", - "purchase_quotes": "Penawaran Pembelian" + "purchase_quotes": "Penawaran Pembelian", + "invoices": "Tagihan" } } diff --git a/src/data/dummy/sales.ts b/src/data/dummy/sales.ts new file mode 100644 index 0000000..72f4452 --- /dev/null +++ b/src/data/dummy/sales.ts @@ -0,0 +1,244 @@ +import { SalesBillType } from '@/types/apps/salesTypes' + +export const salesBillData: SalesBillType[] = [ + { + id: 1, + number: 'INV/001', + customerName: 'Andi Wijaya', + customerCompany: 'PT Nusantara Abadi', + reference: 'REF-SB-001', + date: '2025-09-01', + dueDate: '2025-09-15', + status: 'Belum Dibayar', + remainingBill: 5000000, + total: 5000000 + }, + { + id: 2, + number: 'INV/002', + customerName: 'Siti Rahma', + customerCompany: 'CV Cahaya Abadi', + reference: 'REF-SB-002', + date: '2025-09-02', + dueDate: '2025-09-16', + status: 'Dibayar Sebagian', + remainingBill: 2000000, + total: 8000000 + }, + { + id: 3, + number: 'INV/003', + customerName: 'Budi Santoso', + customerCompany: 'UD Sejahtera', + reference: 'REF-SB-003', + date: '2025-09-03', + dueDate: '2025-09-17', + status: 'Lunas', + remainingBill: 0, + total: 3500000 + }, + { + id: 4, + number: 'INV/004', + customerName: 'Rina Kartika', + customerCompany: 'PT Mitra Jaya', + reference: 'REF-SB-004', + date: '2025-09-04', + dueDate: '2025-09-18', + status: 'Void', + remainingBill: 0, + total: 4200000 + }, + { + id: 5, + number: 'INV/005', + customerName: 'Agus Salim', + customerCompany: 'CV Bumi Persada', + reference: 'REF-SB-005', + date: '2025-09-05', + dueDate: '2025-09-19', + status: 'Retur', + remainingBill: 0, + total: 6100000 + }, + { + id: 6, + number: 'INV/006', + customerName: 'Maya Lestari', + customerCompany: 'PT Tunas Baru', + reference: 'REF-SB-006', + date: '2025-09-06', + dueDate: '2025-09-20', + status: 'Belum Dibayar', + remainingBill: 9000000, + total: 9000000 + }, + { + id: 7, + number: 'INV/007', + customerName: 'Hendra Gunawan', + customerCompany: 'UD Prima Sentosa', + reference: 'REF-SB-007', + date: '2025-09-07', + dueDate: '2025-09-21', + status: 'Dibayar Sebagian', + remainingBill: 1500000, + total: 7500000 + }, + { + id: 8, + number: 'INV/008', + customerName: 'Dewi Anggraini', + customerCompany: 'CV Inti Mandiri', + reference: 'REF-SB-008', + date: '2025-09-08', + dueDate: '2025-09-22', + status: 'Lunas', + remainingBill: 0, + total: 4500000 + }, + { + id: 9, + number: 'INV/009', + customerName: 'Yusuf Arifin', + customerCompany: 'PT Surya Kencana', + reference: 'REF-SB-009', + date: '2025-09-09', + dueDate: '2025-09-23', + status: 'Void', + remainingBill: 0, + total: 5000000 + }, + { + id: 10, + number: 'INV/010', + customerName: 'Nurhayati', + customerCompany: 'UD Cahaya Mulia', + reference: 'REF-SB-010', + date: '2025-09-10', + dueDate: '2025-09-24', + status: 'Retur', + remainingBill: 0, + total: 3000000 + }, + { + id: 11, + number: 'INV/011', + customerName: 'Fajar Hidayat', + customerCompany: 'PT Bina Karya', + reference: 'REF-SB-011', + date: '2025-09-11', + dueDate: '2025-09-25', + status: 'Belum Dibayar', + remainingBill: 6000000, + total: 6000000 + }, + { + id: 12, + number: 'INV/012', + customerName: 'Ratna Sari', + customerCompany: 'CV Mega Utama', + reference: 'REF-SB-012', + date: '2025-09-12', + dueDate: '2025-09-26', + status: 'Dibayar Sebagian', + remainingBill: 2500000, + total: 10000000 + }, + { + id: 13, + number: 'INV/013', + customerName: 'Tono Prasetyo', + customerCompany: 'UD Karya Indah', + reference: 'REF-SB-013', + date: '2025-09-13', + dueDate: '2025-09-27', + status: 'Lunas', + remainingBill: 0, + total: 7000000 + }, + { + id: 14, + number: 'INV/014', + customerName: 'Lina Marlina', + customerCompany: 'PT Harmoni Sejati', + reference: 'REF-SB-014', + date: '2025-09-14', + dueDate: '2025-09-28', + status: 'Void', + remainingBill: 0, + total: 3200000 + }, + { + id: 15, + number: 'INV/015', + customerName: 'Arman Saputra', + customerCompany: 'CV Sentra Niaga', + reference: 'REF-SB-015', + date: '2025-09-15', + dueDate: '2025-09-29', + status: 'Retur', + remainingBill: 0, + total: 5400000 + }, + { + id: 16, + number: 'INV/016', + customerName: 'Indah Permata', + customerCompany: 'PT Citra Abadi', + reference: 'REF-SB-016', + date: '2025-09-16', + dueDate: '2025-09-30', + status: 'Belum Dibayar', + remainingBill: 8000000, + total: 8000000 + }, + { + id: 17, + number: 'INV/017', + customerName: 'Adi Putra', + customerCompany: 'UD Makmur Bersama', + reference: 'REF-SB-017', + date: '2025-09-17', + dueDate: '2025-10-01', + status: 'Dibayar Sebagian', + remainingBill: 1200000, + total: 6200000 + }, + { + id: 18, + number: 'INV/018', + customerName: 'Sri Wahyuni', + customerCompany: 'CV Bintang Terang', + reference: 'REF-SB-018', + date: '2025-09-18', + dueDate: '2025-10-02', + status: 'Lunas', + remainingBill: 0, + total: 9200000 + }, + { + id: 19, + number: 'INV/019', + customerName: 'Eko Prabowo', + customerCompany: 'PT Mandiri Jaya', + reference: 'REF-SB-019', + date: '2025-09-19', + dueDate: '2025-10-03', + status: 'Void', + remainingBill: 0, + total: 2500000 + }, + { + id: 20, + number: 'INV/020', + customerName: 'Novi Astuti', + customerCompany: 'UD Sinar Harapan', + reference: 'REF-SB-020', + date: '2025-09-20', + dueDate: '2025-10-04', + status: 'Retur', + remainingBill: 0, + total: 4800000 + } +] diff --git a/src/types/apps/salesTypes.ts b/src/types/apps/salesTypes.ts new file mode 100644 index 0000000..0c57b4e --- /dev/null +++ b/src/types/apps/salesTypes.ts @@ -0,0 +1,12 @@ +export type SalesBillType = { + id: number + number: string + customerName: string + customerCompany: string + reference: string + date: string + dueDate: string + status: string + remainingBill: number + total: number +} diff --git a/src/views/apps/sales/sales-bill/list/SalesBillListTable.tsx b/src/views/apps/sales/sales-bill/list/SalesBillListTable.tsx new file mode 100644 index 0000000..6245605 --- /dev/null +++ b/src/views/apps/sales/sales-bill/list/SalesBillListTable.tsx @@ -0,0 +1,464 @@ +'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 { SalesBillType } from '@/types/apps/salesTypes' +import { salesBillData } from '@/data/dummy/sales' + +declare module '@tanstack/table-core' { + interface FilterFns { + fuzzy: FilterFn + } + interface FilterMeta { + itemRank: RankingInfo + } +} + +type SalesBillTypeWithAction = SalesBillType & { + 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 Sales Bill +const getStatusColor = (status: string) => { + switch (status) { + case 'Belum Dibayar': + return 'error' + case 'Dibayar Sebagian': + return 'warning' + case 'Lunas': + return 'success' + case 'Void': + return 'default' + case 'Retur': + return 'info' + default: + return 'default' + } +} + +// Format currency +const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(amount) +} + +// Column Definitions +const columnHelper = createColumnHelper() + +const SalesBillListTable = () => { + const dispatch = useDispatch() + + // States + const [addBillOpen, setAddBillOpen] = useState(false) + const [rowSelection, setRowSelection] = useState({}) + const [currentPage, setCurrentPage] = useState(0) + const [pageSize, setPageSize] = useState(10) + const [openConfirm, setOpenConfirm] = useState(false) + const [billId, setBillId] = useState('') + const [search, setSearch] = useState('') + const [statusFilter, setStatusFilter] = useState('Semua') + const [filteredData, setFilteredData] = useState(salesBillData) + + // Hooks + const { lang: locale } = useParams() + + // Filter data based on search and status + useEffect(() => { + let filtered = salesBillData + + // Filter by search + if (search) { + filtered = filtered.filter( + bill => + bill.number.toLowerCase().includes(search.toLowerCase()) || + bill.customerName.toLowerCase().includes(search.toLowerCase()) || + bill.customerCompany.toLowerCase().includes(search.toLowerCase()) || + bill.status.toLowerCase().includes(search.toLowerCase()) + ) + } + + // Filter by status + if (statusFilter !== 'Semua') { + filtered = filtered.filter(bill => bill.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]) + + 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 handleBillClick = (billId: string) => { + console.log('Navigasi ke detail Sales Bill:', billId) + } + + const handleStatusFilter = (status: string) => { + setStatusFilter(status) + } + + const columns = useMemo[]>( + () => [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ) + }, + columnHelper.accessor('number', { + header: 'Nomor Bill', + cell: ({ row }) => ( + + ) + }), + columnHelper.accessor('customerName', { + header: 'Customer', + cell: ({ row }) => ( +
+ + {row.original.customerName} + + + {row.original.customerCompany} + +
+ ) + }), + columnHelper.accessor('reference', { + header: 'Referensi', + cell: ({ row }) => {row.original.reference || '-'} + }), + columnHelper.accessor('date', { + header: 'Tanggal', + cell: ({ row }) => {row.original.date} + }), + columnHelper.accessor('dueDate', { + header: 'Tanggal Jatuh Tempo', + cell: ({ row }) => {row.original.dueDate} + }), + columnHelper.accessor('status', { + header: 'Status', + cell: ({ row }) => ( +
+ +
+ ) + }), + columnHelper.accessor('remainingBill', { + header: 'Sisa Tagihan', + cell: ({ row }) => ( + 0 ? 'text-red-600' : 'text-green-600'}`}> + {formatCurrency(row.original.remainingBill)} + + ) + }), + columnHelper.accessor('total', { + header: 'Total', + cell: ({ row }) => {formatCurrency(row.original.total)} + }) + ], + [] + ) + + const table = useReactTable({ + data: paginatedData as SalesBillType[], + 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 */} +
+
+ {['Semua', 'Belum Dibayar', 'Dibayar Sebagian', 'Lunas', 'Void', 'Retur'].map(status => ( + + ))} +
+
+ +
+ setSearch(value as string)} + placeholder='Cari Sales Bill' + className='max-sm:is-full' + /> +
+ + 10 + 25 + 50 + + + +
+
+
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + {filteredData.length === 0 ? ( + + + + + + ) : ( + + {table.getRowModel().rows.map(row => { + return ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + + )} +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: + }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} +
+ + )} +
+ Tidak ada data tersedia +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ + ( + + )} + count={totalCount} + rowsPerPage={pageSize} + page={currentPage} + onPageChange={handlePageChange} + onRowsPerPageChange={handlePageSizeChange} + rowsPerPageOptions={[10, 25, 50]} + /> +
+ + ) +} + +export default SalesBillListTable diff --git a/src/views/apps/sales/sales-bill/list/index.tsx b/src/views/apps/sales/sales-bill/list/index.tsx new file mode 100644 index 0000000..777f7be --- /dev/null +++ b/src/views/apps/sales/sales-bill/list/index.tsx @@ -0,0 +1,19 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Type Imports + +// Component Imports +import SalesBillListTable from './SalesBillListTable' + +const SalesBillList = () => { + return ( + + + + + + ) +} + +export default SalesBillList