From afa4cfad0de8d1f64bfa20bd91eeedb35b8bdcd5 Mon Sep 17 00:00:00 2001 From: efrilm Date: Wed, 10 Sep 2025 15:45:34 +0700 Subject: [PATCH] Expense Add --- .../(private)/apps/expense/add/page.tsx | 18 + src/data/dummy/expense.ts | 2 +- src/types/apps/expenseType.ts | 11 - src/types/apps/expenseTypes.ts | 68 ++++ .../apps/expense/add/ExpenseAddAccount.tsx | 137 +++++++ .../apps/expense/add/ExpenseAddBasicInfo.tsx | 166 +++++++++ src/views/apps/expense/add/ExpenseAddForm.tsx | 149 ++++++++ .../apps/expense/add/ExpenseAddHeader.tsx | 19 + .../apps/expense/add/ExpenseAddSummary.tsx | 198 ++++++++++ .../apps/expense/list/ExpenseListTable.tsx | 4 +- .../PurchaseIngredientsTable.tsx | 340 ++++++++---------- .../purchase-form/PurchaseSummary.tsx | 117 +++--- 12 files changed, 980 insertions(+), 249 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/expense/add/page.tsx delete mode 100644 src/types/apps/expenseType.ts create mode 100644 src/types/apps/expenseTypes.ts create mode 100644 src/views/apps/expense/add/ExpenseAddAccount.tsx create mode 100644 src/views/apps/expense/add/ExpenseAddBasicInfo.tsx create mode 100644 src/views/apps/expense/add/ExpenseAddForm.tsx create mode 100644 src/views/apps/expense/add/ExpenseAddHeader.tsx create mode 100644 src/views/apps/expense/add/ExpenseAddSummary.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/expense/add/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/expense/add/page.tsx new file mode 100644 index 0000000..65f30df --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/expense/add/page.tsx @@ -0,0 +1,18 @@ +import ExpenseAddForm from '@/views/apps/expense/add/ExpenseAddForm' +import ExpenseAddHeader from '@/views/apps/expense/add/ExpenseAddHeader' +import Grid from '@mui/material/Grid2' + +const ExpenseAddPage = () => { + return ( + + + + + + + + + ) +} + +export default ExpenseAddPage diff --git a/src/data/dummy/expense.ts b/src/data/dummy/expense.ts index 85c5ec1..7d87601 100644 --- a/src/data/dummy/expense.ts +++ b/src/data/dummy/expense.ts @@ -1,4 +1,4 @@ -import { ExpenseType } from '@/types/apps/expenseType' +import { ExpenseType } from '@/types/apps/expenseTypes' export const expenseData: ExpenseType[] = [ { diff --git a/src/types/apps/expenseType.ts b/src/types/apps/expenseType.ts deleted file mode 100644 index 0c7ef2f..0000000 --- a/src/types/apps/expenseType.ts +++ /dev/null @@ -1,11 +0,0 @@ -export type ExpenseType = { - id: number - date: string - number: string - reference: string - benefeciaryName: string - benefeciaryCompany: string - status: string - balanceDue: number - total: number -} diff --git a/src/types/apps/expenseTypes.ts b/src/types/apps/expenseTypes.ts new file mode 100644 index 0000000..811cf6e --- /dev/null +++ b/src/types/apps/expenseTypes.ts @@ -0,0 +1,68 @@ +export type ExpenseType = { + id: number + date: string + number: string + reference: string + benefeciaryName: string + benefeciaryCompany: string + status: string + balanceDue: number + total: number +} + +export interface ExpenseRecipient { + id: string + name: string +} + +export interface ExpenseAccount { + id: string + name: string + code: string +} + +export interface ExpenseTax { + id: string + name: string + rate: number +} + +export interface ExpenseTag { + id: string + name: string + color: string +} + +export interface ExpenseItem { + id: number + account: ExpenseAccount | null + description: string + tax: ExpenseTax | null + total: number +} + +export interface ExpenseFormData { + // Header fields + paidFrom: string // "Dibayar Dari" + payLater: boolean // "Bayar Nanti" toggle + recipient: ExpenseRecipient | null // "Penerima" + transactionDate: string // "Tgl. Transaksi" + + // Reference fields + number: string // "Nomor" + reference: string // "Referensi" + tag: ExpenseTag | null // "Tag" + includeTax: boolean // "Harga termasuk pajak" + + // Expense items + expenseItems: ExpenseItem[] + + // Totals + subtotal: number + total: number + + // Optional sections + showMessage: boolean + showAttachment: boolean + message: string +} diff --git a/src/views/apps/expense/add/ExpenseAddAccount.tsx b/src/views/apps/expense/add/ExpenseAddAccount.tsx new file mode 100644 index 0000000..19bef61 --- /dev/null +++ b/src/views/apps/expense/add/ExpenseAddAccount.tsx @@ -0,0 +1,137 @@ +'use client' + +import React from 'react' +import { + Button, + IconButton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + MenuItem +} from '@mui/material' +import Grid from '@mui/material/Grid2' +import { ExpenseFormData, ExpenseItem, ExpenseAccount, ExpenseTax } from '@/types/apps/expenseTypes' +import CustomTextField from '@core/components/mui/TextField' +import CustomAutocomplete from '@core/components/mui/Autocomplete' + +interface ExpenseAddAccountProps { + formData: ExpenseFormData + mockAccounts: ExpenseAccount[] + mockTaxes: ExpenseTax[] + onExpenseItemChange: (index: number, field: keyof ExpenseItem, value: any) => void + onAddExpenseItem: () => void + onRemoveExpenseItem: (index: number) => void +} + +const ExpenseAddAccount: React.FC = ({ + formData, + mockAccounts, + mockTaxes, + onExpenseItemChange, + onAddExpenseItem, + onRemoveExpenseItem +}) => { + return ( + + + + + + Akun Biaya + Deskripsi + Pajak + Total + + + + + {formData.expenseItems.map((item, index) => ( + + + `${option.code} ${option.name}`} + value={item.account} + onChange={(_, value) => onExpenseItemChange(index, 'account', value)} + renderInput={params => } + /> + + + onExpenseItemChange(index, 'description', e.target.value)} + /> + + + { + const tax = mockTaxes.find(t => t.id === e.target.value) + onExpenseItemChange(index, 'tax', tax || null) + }} + SelectProps={{ + displayEmpty: true + }} + > + ... + {mockTaxes.map(tax => ( + + {tax.name} + + ))} + + + + onExpenseItemChange(index, 'total', parseFloat(e.target.value) || 0)} + sx={{ + '& .MuiInputBase-input': { + textAlign: 'right' + } + }} + /> + + + onRemoveExpenseItem(index)} + disabled={formData.expenseItems.length === 1} + > + + + + + ))} + +
+
+ + +
+ ) +} + +export default ExpenseAddAccount diff --git a/src/views/apps/expense/add/ExpenseAddBasicInfo.tsx b/src/views/apps/expense/add/ExpenseAddBasicInfo.tsx new file mode 100644 index 0000000..b268fed --- /dev/null +++ b/src/views/apps/expense/add/ExpenseAddBasicInfo.tsx @@ -0,0 +1,166 @@ +'use client' + +import React from 'react' +import { Switch, FormControlLabel, Box } from '@mui/material' +import Grid from '@mui/material/Grid2' +import { ExpenseFormData, ExpenseAccount, ExpenseRecipient, ExpenseTag } from '@/types/apps/expenseTypes' +import CustomTextField from '@core/components/mui/TextField' +import CustomAutocomplete from '@core/components/mui/Autocomplete' + +interface ExpenseAddBasicInfoProps { + formData: ExpenseFormData + mockAccounts: ExpenseAccount[] + mockRecipients: ExpenseRecipient[] + mockTags: ExpenseTag[] + onInputChange: (field: keyof ExpenseFormData, value: any) => void +} + +const ExpenseAddBasicInfo: React.FC = ({ + formData, + mockAccounts, + mockRecipients, + mockTags, + onInputChange +}) => { + return ( + <> + {/* Row 1: Dibayar Dari (6) + Bayar Nanti (6) */} + + `${option.code} ${option.name}`} + value={mockAccounts.find(acc => `${acc.code} ${acc.name}` === formData.paidFrom) || null} + onChange={(_, value) => onInputChange('paidFrom', value ? `${value.code} ${value.name}` : '')} + renderInput={params => } + /> + + + + onInputChange('payLater', e.target.checked)} + size='small' + /> + } + label='Bayar Nanti' + /> + + + + {/* Row 2: Penerima (6) */} + + option.name} + value={formData.recipient} + onChange={(_, value) => onInputChange('recipient', value)} + renderInput={params => } + /> + + + {/* Row 3: Tgl. Transaksi (6) */} + + { + const date = new Date(e.target.value) + const formattedDate = `${date.getDate().toString().padStart(2, '0')}/${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getFullYear()}` + onInputChange('transactionDate', formattedDate) + }} + slotProps={{ + input: { + endAdornment: + } + }} + /> + + + {/* Row 4: Nomor, Referensi, Tag */} + + + + + Nomor + + + } + value={formData.number} + onChange={e => onInputChange('number', e.target.value)} + /> + + + + Referensi + + + } + placeholder='Referensi' + value={formData.reference} + onChange={e => onInputChange('reference', e.target.value)} + /> + + + + Tag + + + } + placeholder='Pilih Tag' + value={formData.tag?.name || ''} + onChange={e => { + const tag = mockTags.find(t => t.name === e.target.value) + onInputChange('tag', tag || null) + }} + /> + + + + + {/* Row 5: Harga termasuk pajak */} + + + onInputChange('includeTax', e.target.checked)} + size='small' + /> + } + label='Harga termasuk pajak' + /> + + + + ) +} + +export default ExpenseAddBasicInfo diff --git a/src/views/apps/expense/add/ExpenseAddForm.tsx b/src/views/apps/expense/add/ExpenseAddForm.tsx new file mode 100644 index 0000000..60425d0 --- /dev/null +++ b/src/views/apps/expense/add/ExpenseAddForm.tsx @@ -0,0 +1,149 @@ +'use client' + +import React, { useState } from 'react' +import { Card, CardContent } from '@mui/material' +import Grid from '@mui/material/Grid2' +import { + ExpenseFormData, + ExpenseItem, + ExpenseAccount, + ExpenseTax, + ExpenseTag, + ExpenseRecipient +} from '@/types/apps/expenseTypes' +import ExpenseAddBasicInfo from './ExpenseAddBasicInfo' +import ExpenseAddAccount from './ExpenseAddAccount' +import ExpenseAddSummary from './ExpenseAddSummary' + +// Mock data +const mockAccounts: ExpenseAccount[] = [ + { id: '1', name: 'Kas', code: '1-10001' }, + { id: '2', name: 'Bank BCA', code: '1-10002' }, + { id: '3', name: 'Bank Mandiri', code: '1-10003' } +] + +const mockRecipients: ExpenseRecipient[] = [ + { id: '1', name: 'PT ABC Company' }, + { id: '2', name: 'CV XYZ Trading' }, + { id: '3', name: 'John Doe' }, + { id: '4', name: 'Jane Smith' } +] + +const mockTaxes: ExpenseTax[] = [ + { id: '1', name: 'PPN 11%', rate: 11 }, + { id: '2', name: 'PPh 23', rate: 2 }, + { id: '3', name: 'Bebas Pajak', rate: 0 } +] + +const mockTags: ExpenseTag[] = [ + { id: '1', name: 'Operasional', color: '#2196F3' }, + { id: '2', name: 'Marketing', color: '#4CAF50' }, + { id: '3', name: 'IT', color: '#FF9800' } +] + +const ExpenseAddForm: React.FC = () => { + const [formData, setFormData] = useState({ + paidFrom: '1-10001 Kas', + payLater: false, + recipient: null, + transactionDate: '13/09/2025', + number: 'EXP/00042', + reference: '', + tag: null, + includeTax: false, + expenseItems: [ + { + id: 1, + account: null, + description: '', + tax: null, + total: 0 + } + ], + subtotal: 0, + total: 0, + showMessage: false, + showAttachment: false, + message: '' + }) + + const handleInputChange = (field: keyof ExpenseFormData, value: any): void => { + setFormData(prev => ({ + ...prev, + [field]: value + })) + } + + const handleExpenseItemChange = (index: number, field: keyof ExpenseItem, value: any): void => { + setFormData(prev => { + const newItems = [...prev.expenseItems] + newItems[index] = { ...newItems[index], [field]: value } + + const subtotal = newItems.reduce((sum, item) => sum + item.total, 0) + const total = subtotal + + return { + ...prev, + expenseItems: newItems, + subtotal, + total + } + }) + } + + const addExpenseItem = (): void => { + const newItem: ExpenseItem = { + id: Date.now(), + account: null, + description: '', + tax: null, + total: 0 + } + setFormData(prev => ({ + ...prev, + expenseItems: [...prev.expenseItems, newItem] + })) + } + + const removeExpenseItem = (index: number): void => { + if (formData.expenseItems.length > 1) { + setFormData(prev => ({ + ...prev, + expenseItems: prev.expenseItems.filter((_, i) => i !== index) + })) + } + } + + const formatCurrency = (amount: number): string => { + return new Intl.NumberFormat('id-ID').format(amount) + } + + return ( + + + + + + + + + + + + ) +} + +export default ExpenseAddForm diff --git a/src/views/apps/expense/add/ExpenseAddHeader.tsx b/src/views/apps/expense/add/ExpenseAddHeader.tsx new file mode 100644 index 0000000..5cbd1ca --- /dev/null +++ b/src/views/apps/expense/add/ExpenseAddHeader.tsx @@ -0,0 +1,19 @@ +'use client' + +// MUI Imports +import Button from '@mui/material/Button' +import Typography from '@mui/material/Typography' + +const ExpenseAddHeader = () => { + return ( +
+
+ + Tambah Biaya + +
+
+ ) +} + +export default ExpenseAddHeader diff --git a/src/views/apps/expense/add/ExpenseAddSummary.tsx b/src/views/apps/expense/add/ExpenseAddSummary.tsx new file mode 100644 index 0000000..63f071e --- /dev/null +++ b/src/views/apps/expense/add/ExpenseAddSummary.tsx @@ -0,0 +1,198 @@ +'use client' + +import React from 'react' +import { Button, Accordion, AccordionSummary, AccordionDetails, Typography, Box } from '@mui/material' +import Grid from '@mui/material/Grid2' +import { ExpenseFormData } from '@/types/apps/expenseTypes' +import CustomTextField from '@core/components/mui/TextField' +import ImageUpload from '@/components/ImageUpload' + +interface ExpenseAddSummaryProps { + formData: ExpenseFormData + onInputChange: (field: keyof ExpenseFormData, value: any) => void + formatCurrency: (amount: number) => string +} + +const ExpenseAddSummary: React.FC = ({ formData, onInputChange, formatCurrency }) => { + const handleUpload = async (file: File): Promise => { + // Simulate upload + return new Promise(resolve => { + setTimeout(() => { + resolve(URL.createObjectURL(file)) + }, 1000) + }) + } + + return ( + + + {/* Left Side - Pesan and Attachment */} + + {/* Pesan Section */} + + + {formData.showMessage && ( + + ) => onInputChange('message', e.target.value)} + /> + + )} + + + {/* Attachment Section */} + + + {formData.showAttachment && ( + + )} + + + + {/* Right Side - Totals */} + + + {/* Sub Total */} + + + Sub Total + + + {new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(formData.subtotal || 0)} + + + + {/* Additional Options */} + + {/* Total */} + + + Total + + + RP. 120.000 + + + + {/* Save Button */} + + + + + + ) +} + +export default ExpenseAddSummary diff --git a/src/views/apps/expense/list/ExpenseListTable.tsx b/src/views/apps/expense/list/ExpenseListTable.tsx index dde2737..83d527f 100644 --- a/src/views/apps/expense/list/ExpenseListTable.tsx +++ b/src/views/apps/expense/list/ExpenseListTable.tsx @@ -40,7 +40,7 @@ import { useDispatch } from 'react-redux' import TablePaginationComponent from '@/components/TablePaginationComponent' import Loading from '@/components/layout/shared/Loading' import { getLocalizedUrl } from '@/utils/i18n' -import { ExpenseType } from '@/types/apps/expenseType' +import { ExpenseType } from '@/types/apps/expenseTypes' import { expenseData } from '@/data/dummy/expense' import StatusFilterTabs from '@/components/StatusFilterTab' import DateRangePicker from '@/components/RangeDatePicker' @@ -387,7 +387,7 @@ const ExpenseListTable = () => { component={Link} className='max-sm:is-full is-auto' startIcon={} - href={getLocalizedUrl('/apps/expenses/add', locale as Locale)} + href={getLocalizedUrl('/apps/expense/add', locale as Locale)} > Tambah diff --git a/src/views/apps/purchase/purchase-form/PurchaseIngredientsTable.tsx b/src/views/apps/purchase/purchase-form/PurchaseIngredientsTable.tsx index d319568..8514a0a 100644 --- a/src/views/apps/purchase/purchase-form/PurchaseIngredientsTable.tsx +++ b/src/views/apps/purchase/purchase-form/PurchaseIngredientsTable.tsx @@ -1,7 +1,18 @@ 'use client' import React from 'react' -import { Button, Typography } from '@mui/material' +import { + Button, + Typography, + IconButton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper +} from '@mui/material' import Grid from '@mui/material/Grid2' import CustomAutocomplete from '@/@core/components/mui/Autocomplete' import CustomTextField from '@/@core/components/mui/TextField' @@ -53,195 +64,160 @@ const PurchaseIngredientsTable: React.FC = ({ ] return ( - + Bahan Baku / Ingredients - {/* Table Header */} - - - - Bahan Baku - - - - - Deskripsi - - - - - Kuantitas - - - - - Satuan - - - - - Discount - - - - - Harga - - - - - Pajak - - - - - Waste - - - - - Total - - - - + + + + + Bahan Baku + Deskripsi + Kuantitas + Satuan + Discount + Harga + Pajak + Waste + Total + + + + + {formData.ingredientItems.map((item: IngredientItem, index: number) => ( + + + handleIngredientChange(index, 'ingredient', newValue)} + renderInput={params => } + /> + + + ) => + handleIngredientChange(index, 'deskripsi', e.target.value) + } + placeholder='Deskripsi' + /> + + + ) => + handleIngredientChange(index, 'kuantitas', parseInt(e.target.value) || 1) + } + inputProps={{ min: 1 }} + /> + + + handleIngredientChange(index, 'satuan', newValue)} + renderInput={params => } + /> + + + ) => + handleIngredientChange(index, 'discount', e.target.value) + } + placeholder='0%' + /> + + + ) => { + const value = e.target.value - {/* Ingredient Items */} - {formData.ingredientItems.map((item: IngredientItem, index: number) => ( - - - handleIngredientChange(index, 'ingredient', newValue)} - renderInput={params => ( - - )} - /> - - - ) => - handleIngredientChange(index, 'deskripsi', e.target.value) - } - placeholder='Deskripsi' - /> - - - ) => - handleIngredientChange(index, 'kuantitas', parseInt(e.target.value) || 1) - } - inputProps={{ min: 1 }} - /> - - - handleIngredientChange(index, 'satuan', newValue)} - renderInput={params => } - /> - - - ) => - handleIngredientChange(index, 'discount', e.target.value) - } - placeholder='0%' - /> - - - ) => { - const value = e.target.value + if (value === '') { + handleIngredientChange(index, 'harga', null) + return + } - if (value === '') { - // Jika kosong, set ke null atau undefined, bukan 0 - handleIngredientChange(index, 'harga', null) // atau undefined - return - } - - const numericValue = parseFloat(value) - handleIngredientChange(index, 'harga', isNaN(numericValue) ? 0 : numericValue) - }} - inputProps={{ min: 0, step: 'any' }} - placeholder='0' - /> - - - handleIngredientChange(index, 'pajak', newValue)} - renderInput={params => } - /> - - - handleIngredientChange(index, 'waste', newValue)} - renderInput={params => } - /> - - - - - - - - - ))} + const numericValue = parseFloat(value) + handleIngredientChange(index, 'harga', isNaN(numericValue) ? 0 : numericValue) + }} + inputProps={{ min: 0, step: 'any' }} + placeholder='0' + /> + + + handleIngredientChange(index, 'pajak', newValue)} + renderInput={params => } + /> + + + handleIngredientChange(index, 'waste', newValue)} + renderInput={params => } + /> + + + + + + removeIngredientItem(index)} + disabled={formData.ingredientItems.length === 1} + > + + + + + ))} + +
+
{/* Add New Item Button */} - - - - - +
) } diff --git a/src/views/apps/purchase/purchase-form/PurchaseSummary.tsx b/src/views/apps/purchase/purchase-form/PurchaseSummary.tsx index 298686a..3bc71cf 100644 --- a/src/views/apps/purchase/purchase-form/PurchaseSummary.tsx +++ b/src/views/apps/purchase/purchase-form/PurchaseSummary.tsx @@ -464,64 +464,75 @@ const PurchaseSummary: React.FC = ({ formData, handleInput } }} > - - - {/* Dropdown */} - (typeof option === 'string' ? option : option.label)} - value={{ label: '1-10003 Gi...', value: '1-10003' }} - onChange={(_, newValue) => { - // Handle change if needed - }} - renderInput={params => } - sx={{ minWidth: 120 }} - /> + + {formData.showUangMuka && ( + + + {/* Dropdown */} + (typeof option === 'string' ? option : option.label)} + value={{ label: '1-10003 Gi...', value: '1-10003' }} + onChange={(_, newValue) => { + // Handle change if needed + }} + renderInput={params => } + sx={{ minWidth: 120 }} + /> - {/* Amount input */} - ) => - handleInputChange('downPayment', e.target.value) - } - sx={{ width: '80px' }} - inputProps={{ - style: { textAlign: 'center' } - }} - /> + {/* Amount input */} + ) => + handleInputChange('downPayment', e.target.value) + } + sx={{ width: '80px' }} + inputProps={{ + style: { textAlign: 'center' } + }} + /> - {/* Percentage/Fixed toggle */} - { - if (newValue) handleInputChange('downPaymentType', newValue) + {/* Percentage/Fixed toggle */} + { + if (newValue) handleInputChange('downPaymentType', newValue) + }} + size='small' + > + + % + + + Rp + + + + + {/* Right side text */} + - - % - - - Rp - - + Uang muka {downPayment > 0 ? downPayment.toLocaleString('id-ID') : '0'} + - - {/* Right side text */} - - Uang muka {downPayment > 0 ? downPayment.toLocaleString('id-ID') : '0'} - - + )} {/* Sisa Tagihan */}