diff --git a/src/app/[lang]/(dashboard)/(private)/apps/expense/[id]/detail/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/expense/[id]/detail/page.tsx new file mode 100644 index 0000000..68c6855 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/expense/[id]/detail/page.tsx @@ -0,0 +1,18 @@ +import ExpenseDetailContent from '@/views/apps/expense/detail/ExpenseDetailContent' +import ExpenseDetailHeader from '@/views/apps/expense/detail/ExpenseDetailHeader' +import Grid from '@mui/material/Grid2' + +const ExpenseDetailPage = () => { + return ( + + + + + + + + + ) +} + +export default ExpenseDetailPage diff --git a/src/views/apps/expense/detail/ExpenseDetailContent.tsx b/src/views/apps/expense/detail/ExpenseDetailContent.tsx new file mode 100644 index 0000000..accf703 --- /dev/null +++ b/src/views/apps/expense/detail/ExpenseDetailContent.tsx @@ -0,0 +1,22 @@ +import Grid from '@mui/material/Grid2' +import ExpenseDetailInformation from './ExpenseDetailInformation' +import ExpenseDetailSendPayment from './ExpenseDetailSendPayment' +import ExpenseDetailLog from './ExpenseDetailLog' + +const ExpenseDetailContent = () => { + return ( + + + + + + + + + + + + ) +} + +export default ExpenseDetailContent diff --git a/src/views/apps/expense/detail/ExpenseDetailHeader.tsx b/src/views/apps/expense/detail/ExpenseDetailHeader.tsx new file mode 100644 index 0000000..f0b5681 --- /dev/null +++ b/src/views/apps/expense/detail/ExpenseDetailHeader.tsx @@ -0,0 +1,15 @@ +import { Typography } from '@mui/material' + +const ExpenseDetailHeader = () => { + return ( +
+
+ + Detail Biaya + +
+
+ ) +} + +export default ExpenseDetailHeader diff --git a/src/views/apps/expense/detail/ExpenseDetailInformation.tsx b/src/views/apps/expense/detail/ExpenseDetailInformation.tsx new file mode 100644 index 0000000..fe58ae7 --- /dev/null +++ b/src/views/apps/expense/detail/ExpenseDetailInformation.tsx @@ -0,0 +1,265 @@ +import React from 'react' +import { Card, CardHeader, CardContent, Typography, Box, Button, IconButton } from '@mui/material' +import Grid from '@mui/material/Grid2' + +interface Product { + produk: string + deskripsi: string + kuantitas: number + satuan: string + discount: string + harga: number + pajak: string + jumlah: number +} + +interface PurchaseData { + vendor: string + nomor: string + tglTransaksi: string + tglJatuhTempo: string + gudang: string + status: string +} + +const ExpenseDetailInformation: React.FC = () => { + const purchaseData: PurchaseData = { + vendor: 'Bagas Rizki Sihotang S.Farm Widodo', + nomor: 'PI/00053', + tglTransaksi: '08/09/2025', + tglJatuhTempo: '06/10/2025', + gudang: 'Unassigned', + status: 'Belum Dibayar' + } + + const products: Product[] = [ + { + produk: 'Komisi penjualan tim IT', + deskripsi: '6-60002 - Komisi & Fee (PPN)', + kuantitas: 1, + satuan: 'Pcs', + discount: '0%', + harga: 810000, + pajak: 'PPN', + jumlah: 810000 + }, + { + produk: 'Hotel Ibis 3 Malam, Surabaya', + deskripsi: '6-60004 - Perjalanan Dinas - Penjualan (PPN)', + kuantitas: 1, + satuan: 'Pcs', + discount: '0%', + harga: 400000, + pajak: 'PPN', + jumlah: 400000 + }, + { + produk: 'Biaya telp fixed', + deskripsi: '6-60005 - Komunikasi - Penjualan', + kuantitas: 1, + satuan: 'Pcs', + discount: '0%', + harga: 950000, + pajak: 'PPN', + jumlah: 950000 + } + ] + + const subTotal: number = products.reduce((sum, product) => sum + product.jumlah, 0) + const ppn: number = Math.round(subTotal * 0.11) + const total: number = subTotal + ppn + const sisaTagihan: number = total + + const formatCurrency = (amount: number): string => { + return new Intl.NumberFormat('id-ID').format(amount) + } + + return ( + + + + Belum Dibayar + + + + + + + + + + } + /> + + + {/* Purchase Information */} + + + + + Penerima + + + {purchaseData.vendor} + + + + + + Tgl. Transaksi + + {purchaseData.tglTransaksi} + + + + + + + Nomor + + {purchaseData.nomor} + + + + + Tgl. Jatuh Tempo + + {purchaseData.tglJatuhTempo} + + + + + {/* Products List */} + + {products.map((product, index) => ( + + + + {product.produk} + + + {product.deskripsi} + + + + + {formatCurrency(product.jumlah)} + + + + ))} + + + {/* Summary Section */} + + + {/* Empty space for left side */} + + + + + Sub Total + + + {formatCurrency(subTotal)} + + + + + + PPN + + + {formatCurrency(ppn)} + + + + + + Total + + + {formatCurrency(total)} + + + + + + Sisa Tagihan + + + {formatCurrency(sisaTagihan)} + + + + + + + + + ) +} + +export default ExpenseDetailInformation diff --git a/src/views/apps/expense/detail/ExpenseDetailLog.tsx b/src/views/apps/expense/detail/ExpenseDetailLog.tsx new file mode 100644 index 0000000..0ee010a --- /dev/null +++ b/src/views/apps/expense/detail/ExpenseDetailLog.tsx @@ -0,0 +1,59 @@ +'use client' + +import React from 'react' +import { Card, CardContent, CardHeader, Typography, Box, Link } from '@mui/material' + +interface LogEntry { + id: string + action: string + timestamp: string + user: string +} + +const ExpenseDetailLog: React.FC = () => { + const logEntries: LogEntry[] = [ + { + id: '1', + action: 'Terakhir diubah oleh', + timestamp: '08 Sep 2025 18:26', + user: 'pada' + } + ] + + return ( + + + Pantau log perubahan data + + } + sx={{ pb: 1 }} + /> + + {logEntries.map(entry => ( + + + + + {entry.action} {entry.user} {entry.timestamp} + + + + ))} + + + ) +} + +export default ExpenseDetailLog diff --git a/src/views/apps/expense/detail/ExpenseDetailSendPayment.tsx b/src/views/apps/expense/detail/ExpenseDetailSendPayment.tsx new file mode 100644 index 0000000..8f2ac20 --- /dev/null +++ b/src/views/apps/expense/detail/ExpenseDetailSendPayment.tsx @@ -0,0 +1,417 @@ +'use client' + +import React, { useState } from 'react' +import { + Card, + CardContent, + Typography, + Button, + Box, + Accordion, + AccordionSummary, + AccordionDetails, + Tooltip, + IconButton, + CardHeader +} from '@mui/material' +import Grid from '@mui/material/Grid2' +import CustomTextField from '@/@core/components/mui/TextField' +import CustomAutocomplete from '@/@core/components/mui/Autocomplete' + +interface PaymentFormData { + totalDibayar: string + tglTransaksi: string + referensi: string + nomor: string + dibayarDari: string +} + +interface PemotonganItem { + id: string + dipotong: string + persentase: string + nominal: string + tipe: 'persen' | 'rupiah' +} + +const ExpenseDetailSendPayment: React.FC = () => { + const [formData, setFormData] = useState({ + totalDibayar: '849.000', + tglTransaksi: '10/09/2025', + referensi: '', + nomor: 'PP/00025', + dibayarDari: '1-10001 Kas' + }) + + const [expanded, setExpanded] = useState(false) + const [pemotonganItems, setPemotonganItems] = useState([]) + + const dibayarDariOptions = [ + { label: '1-10001 Kas', value: '1-10001 Kas' }, + { label: '1-10002 Bank BCA', value: '1-10002 Bank BCA' }, + { label: '1-10003 Bank Mandiri', value: '1-10003 Bank Mandiri' }, + { label: '1-10004 Petty Cash', value: '1-10004 Petty Cash' } + ] + + const pemotonganOptions = [ + { label: 'PPN 11%', value: 'ppn' }, + { label: 'PPh 21', value: 'pph21' }, + { label: 'PPh 23', value: 'pph23' }, + { label: 'Biaya Admin', value: 'admin' } + ] + + const handleChange = + (field: keyof PaymentFormData) => (event: React.ChangeEvent | any) => { + setFormData(prev => ({ + ...prev, + [field]: event.target.value + })) + } + + const handleDibayarDariChange = (value: { label: string; value: string } | null) => { + setFormData(prev => ({ + ...prev, + dibayarDari: value?.value || '' + })) + } + + const addPemotongan = () => { + const newItem: PemotonganItem = { + id: Date.now().toString(), + dipotong: '', + persentase: '0', + nominal: '', + tipe: 'persen' + } + setPemotonganItems(prev => [...prev, newItem]) + } + + const removePemotongan = (id: string) => { + setPemotonganItems(prev => prev.filter(item => item.id !== id)) + } + + const updatePemotongan = (id: string, field: keyof PemotonganItem, value: string) => { + setPemotonganItems(prev => prev.map(item => (item.id === id ? { ...item, [field]: value } : item))) + } + + const handleAccordionChange = () => { + setExpanded(!expanded) + } + + const calculatePemotongan = (item: PemotonganItem): number => { + const totalDibayar = parseInt(formData.totalDibayar.replace(/\D/g, '')) || 0 + const nilai = parseFloat(item.persentase) || 0 + + if (item.tipe === 'persen') { + return (totalDibayar * nilai) / 100 + } else { + return nilai + } + } + + const formatCurrency = (amount: string | number): string => { + const numAmount = typeof amount === 'string' ? parseInt(amount.replace(/\D/g, '')) : amount + return new Intl.NumberFormat('id-ID').format(numAmount) + } + + return ( + + + + Kirim Pembayaran + + + } + /> + + + {/* Left Column */} + + {/* Total Dibayar */} + + + * Total Dibayar + + } + value={formData.totalDibayar} + onChange={handleChange('totalDibayar')} + sx={{ + '& .MuiInputBase-root': { + textAlign: 'right' + } + }} + /> + + + {/* Tgl. Transaksi */} + + + * Tgl. Transaksi + + } + type='date' + value={formData.tglTransaksi.split('/').reverse().join('-')} + onChange={handleChange('tglTransaksi')} + slotProps={{ + input: { + endAdornment: + } + }} + /> + + + {/* Referensi */} + + + + Referensi + + + + + + + + + + + {/* Attachment Accordion */} + + + } + sx={{ + backgroundColor: '#f8f9fa', + borderRadius: '8px', + minHeight: '48px', + '& .MuiAccordionSummary-content': { + margin: '12px 0' + } + }} + > + + Attachment + + + + + Drag and drop files here or click to upload + + + + + + {/* Right Column */} + + {/* Nomor */} + + + + Nomor + + + + + + + + + + + {/* Dibayar Dari */} + + + * Dibayar Dari + + option.label || ''} + value={dibayarDariOptions.find(option => option.value === formData.dibayarDari) || null} + onChange={(_, value: { label: string; value: string } | null) => handleDibayarDariChange(value)} + renderInput={(params: any) => } + noOptionsText='Tidak ada pilihan' + /> + + + {/* Empty space to match Referensi height */} + {/* Empty space */} + + {/* Pemotongan Button - aligned with Attachment */} + + + + + + + {/* Pemotongan Items */} + {pemotonganItems.length > 0 && ( + + {pemotonganItems.map((item, index) => ( + + + + removePemotongan(item.id)} + sx={{ + backgroundColor: '#fff', + border: '1px solid #f44336', + '&:hover': { backgroundColor: '#ffebee' } + }} + > + + + + + + option.label || ''} + value={pemotonganOptions.find(option => option.value === item.dipotong) || null} + onChange={(_, value: { label: string; value: string } | null) => + updatePemotongan(item.id, 'dipotong', value?.value || '') + } + renderInput={(params: any) => } + noOptionsText='Tidak ada pilihan' + /> + + + + updatePemotongan(item.id, 'persentase', e.target.value)} + placeholder='0' + sx={{ + '& .MuiInputBase-root': { + textAlign: 'center' + } + }} + /> + + + + + + + + + + + + + {formatCurrency(calculatePemotongan(item))} + + + + + + ))} + + )} + + {/* Bottom Section */} + + + {/* Empty space */} + + + + Total + + + {formatCurrency(formData.totalDibayar)} + + + + + + + + + + ) +} + +export default ExpenseDetailSendPayment diff --git a/src/views/apps/expense/list/ExpenseListTable.tsx b/src/views/apps/expense/list/ExpenseListTable.tsx index 83d527f..16a87e4 100644 --- a/src/views/apps/expense/list/ExpenseListTable.tsx +++ b/src/views/apps/expense/list/ExpenseListTable.tsx @@ -244,7 +244,8 @@ const ExpenseListTable = () => { variant='text' color='primary' className='p-0 min-w-0 font-medium normal-case justify-start' - onClick={() => handleExpenseClick(row.original.id.toString())} + component={Link} + href={getLocalizedUrl(`/apps/expense/${row.original.id}/detail`, locale as Locale)} sx={{ textTransform: 'none', fontWeight: 500,