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 => (
+
+ ))}
+
+
+
+ onExpenseItemChange(index, 'total', parseFloat(e.target.value) || 0)}
+ sx={{
+ '& .MuiInputBase-input': {
+ textAlign: 'right'
+ }
+ }}
+ />
+
+
+ onRemoveExpenseItem(index)}
+ disabled={formData.expenseItems.length === 1}
+ >
+
+
+
+
+ ))}
+
+
+
+
+ }
+ onClick={onAddExpenseItem}
+ variant='outlined'
+ size='small'
+ sx={{ mt: 1 }}
+ >
+ Tambah baris
+
+
+ )
+}
+
+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 (
+
+ )
+}
+
+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 */}
-
-
-
-
-
+ }
+ onClick={addIngredientItem}
+ variant='outlined'
+ size='small'
+ sx={{ mt: 1 }}
+ >
+ Tambah bahan baku
+
)
}
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 */}