efril #7
@ -0,0 +1,18 @@
|
|||||||
|
import PurchaseDetailContent from '@/views/apps/purchase/purchase-detail/PurchaseDetailContent'
|
||||||
|
import PurchaseDetailHeader from '@/views/apps/purchase/purchase-detail/PurchaseDetailHeader'
|
||||||
|
import Grid from '@mui/material/Grid2'
|
||||||
|
|
||||||
|
const PurchaseOrderDetailPage = () => {
|
||||||
|
return (
|
||||||
|
<Grid container spacing={6}>
|
||||||
|
<Grid size={{ xs: 12 }}>
|
||||||
|
<PurchaseDetailHeader title='Detail Pesanan Pembelian' />
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12 }}>
|
||||||
|
<PurchaseDetailContent />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PurchaseOrderDetailPage
|
||||||
@ -7,6 +7,14 @@ import Menu from '@mui/material/Menu'
|
|||||||
import MenuItem from '@mui/material/MenuItem'
|
import MenuItem from '@mui/material/MenuItem'
|
||||||
import { styled } from '@mui/material/styles'
|
import { styled } from '@mui/material/styles'
|
||||||
|
|
||||||
|
function toTitleCase(str: string): string {
|
||||||
|
return str
|
||||||
|
.toLowerCase()
|
||||||
|
.split(/\s+/) // split by spaces
|
||||||
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
const DropdownButton = styled(Button)(({ theme }) => ({
|
const DropdownButton = styled(Button)(({ theme }) => ({
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
@ -102,7 +110,7 @@ const StatusFilterTabs: React.FC<StatusFilterTabsProps> = ({
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{status}
|
{toTitleCase(status)}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -135,7 +143,7 @@ const StatusFilterTabs: React.FC<StatusFilterTabsProps> = ({
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{status}
|
{toTitleCase(status)}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@ -158,7 +166,7 @@ const StatusFilterTabs: React.FC<StatusFilterTabsProps> = ({
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isDropdownItemSelected ? selectedStatus : dropdownLabel}
|
{isDropdownItemSelected ? toTitleCase(selectedStatus) : dropdownLabel}
|
||||||
</DropdownButton>
|
</DropdownButton>
|
||||||
|
|
||||||
<Menu
|
<Menu
|
||||||
@ -187,7 +195,7 @@ const StatusFilterTabs: React.FC<StatusFilterTabsProps> = ({
|
|||||||
color: selectedStatus === status ? 'primary.main' : 'text.primary'
|
color: selectedStatus === status ? 'primary.main' : 'text.primary'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{status}
|
{toTitleCase(status)}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@ -91,27 +91,27 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => {
|
|||||||
<MenuItem href={`/${locale}/dashboards/daily-report`}>{dictionary['navigation'].dailyReport}</MenuItem>
|
<MenuItem href={`/${locale}/dashboards/daily-report`}>{dictionary['navigation'].dailyReport}</MenuItem>
|
||||||
</SubMenu>
|
</SubMenu>
|
||||||
<MenuSection label={dictionary['navigation'].appsPages}>
|
<MenuSection label={dictionary['navigation'].appsPages}>
|
||||||
<SubMenu label={dictionary['navigation'].sales} icon={<i className='tabler-receipt-2' />}>
|
{/* <SubMenu label={dictionary['navigation'].sales} icon={<i className='tabler-receipt-2' />}>
|
||||||
<MenuItem href={`/${locale}/apps/sales/overview`}>{dictionary['navigation'].overview}</MenuItem>
|
<MenuItem href={`/${locale}/apps/sales/overview`}>{dictionary['navigation'].overview}</MenuItem>
|
||||||
<MenuItem href={`/${locale}/apps/sales/sales-bills`}>{dictionary['navigation'].invoices}</MenuItem>
|
<MenuItem href={`/${locale}/apps/sales/sales-bills`}>{dictionary['navigation'].invoices}</MenuItem>
|
||||||
<MenuItem href={`/${locale}/apps/sales/sales-deliveries`}>{dictionary['navigation'].deliveries}</MenuItem>
|
<MenuItem href={`/${locale}/apps/sales/sales-deliveries`}>{dictionary['navigation'].deliveries}</MenuItem>
|
||||||
<MenuItem href={`/${locale}/apps/sales/sales-orders`}>{dictionary['navigation'].sales_orders}</MenuItem>
|
<MenuItem href={`/${locale}/apps/sales/sales-orders`}>{dictionary['navigation'].sales_orders}</MenuItem>
|
||||||
<MenuItem href={`/${locale}/apps/sales/sales-quotes`}>{dictionary['navigation'].quotes}</MenuItem>
|
<MenuItem href={`/${locale}/apps/sales/sales-quotes`}>{dictionary['navigation'].quotes}</MenuItem>
|
||||||
</SubMenu>
|
</SubMenu> */}
|
||||||
<SubMenu label={dictionary['navigation'].purchase_text} icon={<i className='tabler-shopping-cart' />}>
|
<SubMenu label={dictionary['navigation'].purchase_text} icon={<i className='tabler-shopping-cart' />}>
|
||||||
<MenuItem href={`/${locale}/apps/purchase/overview`}>{dictionary['navigation'].overview}</MenuItem>
|
<MenuItem href={`/${locale}/apps/purchase/overview`}>{dictionary['navigation'].overview}</MenuItem>
|
||||||
<MenuItem href={`/${locale}/apps/purchase/purchase-bills`}>
|
{/* <MenuItem href={`/${locale}/apps/purchase/purchase-bills`}>
|
||||||
{dictionary['navigation'].purchase_bills}
|
{dictionary['navigation'].purchase_bills}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem href={`/${locale}/apps/purchase/purchase-deliveries`}>
|
<MenuItem href={`/${locale}/apps/purchase/purchase-deliveries`}>
|
||||||
{dictionary['navigation'].purchase_delivery}
|
{dictionary['navigation'].purchase_delivery}
|
||||||
</MenuItem>
|
</MenuItem> */}
|
||||||
<MenuItem href={`/${locale}/apps/purchase/purchase-orders`}>
|
<MenuItem href={`/${locale}/apps/purchase/purchase-orders`}>
|
||||||
{dictionary['navigation'].purchase_orders}
|
{dictionary['navigation'].purchase_orders}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem href={`/${locale}/apps/purchase/purchase-quotes`}>
|
{/* <MenuItem href={`/${locale}/apps/purchase/purchase-quotes`}>
|
||||||
{dictionary['navigation'].purchase_quotes}
|
{dictionary['navigation'].purchase_quotes}
|
||||||
</MenuItem>
|
</MenuItem> */}
|
||||||
</SubMenu>
|
</SubMenu>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
href={`/${locale}/apps/expense`}
|
href={`/${locale}/apps/expense`}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import orderReducer from '@/redux-store/slices/order'
|
|||||||
import productRecipeReducer from '@/redux-store/slices/productRecipe'
|
import productRecipeReducer from '@/redux-store/slices/productRecipe'
|
||||||
import organizationReducer from '@/redux-store/slices/organization'
|
import organizationReducer from '@/redux-store/slices/organization'
|
||||||
import userReducer from '@/redux-store/slices/user'
|
import userReducer from '@/redux-store/slices/user'
|
||||||
|
import vendorReducer from '@/redux-store/slices/vendor'
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
@ -19,7 +20,8 @@ export const store = configureStore({
|
|||||||
orderReducer,
|
orderReducer,
|
||||||
productRecipeReducer,
|
productRecipeReducer,
|
||||||
organizationReducer,
|
organizationReducer,
|
||||||
userReducer
|
userReducer,
|
||||||
|
vendorReducer
|
||||||
},
|
},
|
||||||
middleware: getDefaultMiddleware => getDefaultMiddleware({ serializableCheck: false })
|
middleware: getDefaultMiddleware => getDefaultMiddleware({ serializableCheck: false })
|
||||||
})
|
})
|
||||||
|
|||||||
43
src/redux-store/slices/vendor.ts
Normal file
43
src/redux-store/slices/vendor.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// Third-party Imports
|
||||||
|
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||||
|
import { createSlice } from '@reduxjs/toolkit'
|
||||||
|
|
||||||
|
// Type Imports
|
||||||
|
|
||||||
|
// Data Imports
|
||||||
|
import { Vendor } from '../../types/services/vendor'
|
||||||
|
|
||||||
|
const initialState: { currentVendor: Vendor } = {
|
||||||
|
currentVendor: {
|
||||||
|
id: '',
|
||||||
|
organization_id: '',
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
phone_number: '',
|
||||||
|
address: '',
|
||||||
|
contact_person: '',
|
||||||
|
tax_number: '',
|
||||||
|
payment_terms: '',
|
||||||
|
notes: '',
|
||||||
|
is_active: true,
|
||||||
|
created_at: '',
|
||||||
|
updated_at: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VendorSlice = createSlice({
|
||||||
|
name: 'vendor',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setVendor: (state, action: PayloadAction<Vendor>) => {
|
||||||
|
state.currentVendor = action.payload
|
||||||
|
},
|
||||||
|
resetVendor: state => {
|
||||||
|
state.currentVendor = initialState.currentVendor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const { setVendor, resetVendor } = VendorSlice.actions
|
||||||
|
|
||||||
|
export default VendorSlice.reducer
|
||||||
@ -6,7 +6,7 @@ const getToken = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const api = axios.create({
|
export const api = axios.create({
|
||||||
baseURL: 'https://api-pos.apskel.id/api/v1',
|
baseURL: 'http://127.0.0.1:4000/api/v1',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
|
|||||||
52
src/services/mutations/account.ts
Normal file
52
src/services/mutations/account.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { api } from '../api'
|
||||||
|
import { AccountRequest } from '../queries/chartOfAccountType'
|
||||||
|
|
||||||
|
export const useAccountsMutation = () => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const createAccount = useMutation({
|
||||||
|
mutationFn: async (newAccount: AccountRequest) => {
|
||||||
|
const response = await api.post('/accounts', newAccount)
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('Account created successfully!')
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['accounts'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateAccount = useMutation({
|
||||||
|
mutationFn: async ({ id, payload }: { id: string; payload: AccountRequest }) => {
|
||||||
|
const response = await api.put(`/accounts/${id}`, payload)
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('Account updated successfully!')
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['accounts'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.response?.data?.errors?.[0]?.cause || 'Update failed')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteAccount = useMutation({
|
||||||
|
mutationFn: async (id: string) => {
|
||||||
|
const response = await api.delete(`/accounts/${id}`)
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('Account deleted successfully!')
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['accounts'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.response?.data?.errors?.[0]?.cause || 'Delete failed')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { createAccount, updateAccount, deleteAccount }
|
||||||
|
}
|
||||||
24
src/services/mutations/purchaseOrder.ts
Normal file
24
src/services/mutations/purchaseOrder.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { api } from '../api'
|
||||||
|
import { PurchaseOrderRequest } from '@/types/services/purchaseOrder'
|
||||||
|
|
||||||
|
export const usePurchaseOrdersMutation = () => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const createPurchaseOrder = useMutation({
|
||||||
|
mutationFn: async (newPurchaseOrder: PurchaseOrderRequest) => {
|
||||||
|
const response = await api.post('/purchase-orders', newPurchaseOrder)
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('Purchase Order created successfully!')
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['purchase-orders'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { createPurchaseOrder }
|
||||||
|
}
|
||||||
52
src/services/mutations/unitConventor.ts
Normal file
52
src/services/mutations/unitConventor.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { api } from '../api'
|
||||||
|
import { IngredientUnitConverterRequest } from '@/types/services/productRecipe'
|
||||||
|
|
||||||
|
export const useUnitConventorMutation = () => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const createUnitConventer = useMutation({
|
||||||
|
mutationFn: async (newUnitConventer: IngredientUnitConverterRequest) => {
|
||||||
|
const response = await api.post('/unit-converters', newUnitConventer)
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('UnitConventer created successfully!')
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['unitConventers'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateUnitConventer = useMutation({
|
||||||
|
mutationFn: async ({ id, payload }: { id: string; payload: IngredientUnitConverterRequest }) => {
|
||||||
|
const response = await api.put(`/unit-converters/${id}`, payload)
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('UnitConventer updated successfully!')
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['unit-converters'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.response?.data?.errors?.[0]?.cause || 'Update failed')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteUnitConventer = useMutation({
|
||||||
|
mutationFn: async (id: string) => {
|
||||||
|
const response = await api.delete(`/unit-converters/${id}`)
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('UnitConventer deleted successfully!')
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['unitConventers'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.response?.data?.errors?.[0]?.cause || 'Delete failed')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { createUnitConventer, updateUnitConventer, deleteUnitConventer }
|
||||||
|
}
|
||||||
52
src/services/mutations/vendor.ts
Normal file
52
src/services/mutations/vendor.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { VendorRequest } from '@/types/services/vendor'
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { api } from '../api'
|
||||||
|
|
||||||
|
export const useVendorsMutation = () => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const createVendor = useMutation({
|
||||||
|
mutationFn: async (newVendor: VendorRequest) => {
|
||||||
|
const response = await api.post('/vendors', newVendor)
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('Vendor created successfully!')
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['vendors'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateVendor = useMutation({
|
||||||
|
mutationFn: async ({ id, payload }: { id: string; payload: VendorRequest }) => {
|
||||||
|
const response = await api.put(`/vendors/${id}`, payload)
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('Vendor updated successfully!')
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['vendors'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.response?.data?.errors?.[0]?.cause || 'Update failed')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteVendor = useMutation({
|
||||||
|
mutationFn: async (id: string) => {
|
||||||
|
const response = await api.delete(`/vendors/${id}`)
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('Vendor deleted successfully!')
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['vendors'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.response?.data?.errors?.[0]?.cause || 'Delete failed')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { createVendor, updateVendor, deleteVendor }
|
||||||
|
}
|
||||||
36
src/services/queries/account.ts
Normal file
36
src/services/queries/account.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { api } from '../api'
|
||||||
|
import { Accounts } from '@/types/services/chartOfAccount'
|
||||||
|
|
||||||
|
interface AccountQueryParams {
|
||||||
|
page?: number
|
||||||
|
limit?: number
|
||||||
|
search?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAccounts(params: AccountQueryParams = {}) {
|
||||||
|
const { page = 1, limit = 10, search = '', ...filters } = params
|
||||||
|
|
||||||
|
return useQuery<Accounts>({
|
||||||
|
queryKey: ['accounts', { page, limit, search, ...filters }],
|
||||||
|
queryFn: async () => {
|
||||||
|
const queryParams = new URLSearchParams()
|
||||||
|
|
||||||
|
queryParams.append('page', page.toString())
|
||||||
|
queryParams.append('limit', limit.toString())
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
queryParams.append('search', search)
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(filters).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
queryParams.append(key, value.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await api.get(`/accounts?${queryParams.toString()}`)
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
36
src/services/queries/chartOfAccount.ts
Normal file
36
src/services/queries/chartOfAccount.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { api } from '../api'
|
||||||
|
import { ChartOfAccounts } from '@/types/services/chartOfAccount'
|
||||||
|
|
||||||
|
interface ChartOfAccountQueryParams {
|
||||||
|
page?: number
|
||||||
|
limit?: number
|
||||||
|
search?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useChartOfAccount(params: ChartOfAccountQueryParams = {}) {
|
||||||
|
const { page = 1, limit = 10, search = '', ...filters } = params
|
||||||
|
|
||||||
|
return useQuery<ChartOfAccounts>({
|
||||||
|
queryKey: ['chart-of-accounts', { page, limit, search, ...filters }],
|
||||||
|
queryFn: async () => {
|
||||||
|
const queryParams = new URLSearchParams()
|
||||||
|
|
||||||
|
queryParams.append('page', page.toString())
|
||||||
|
queryParams.append('limit', limit.toString())
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
queryParams.append('search', search)
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(filters).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
queryParams.append(key, value.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await api.get(`/chart-of-accounts?${queryParams.toString()}`)
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
45
src/services/queries/chartOfAccountType.ts
Normal file
45
src/services/queries/chartOfAccountType.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { ChartOfAccountTypes } from '@/types/services/chartOfAccount'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { api } from '../api'
|
||||||
|
|
||||||
|
interface ChartOfAccountQueryParams {
|
||||||
|
page?: number
|
||||||
|
limit?: number
|
||||||
|
search?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useChartOfAccountTypes(params: ChartOfAccountQueryParams = {}) {
|
||||||
|
const { page = 1, limit = 10, search = '', ...filters } = params
|
||||||
|
|
||||||
|
return useQuery<ChartOfAccountTypes>({
|
||||||
|
queryKey: ['chart-of-account-types', { page, limit, search, ...filters }],
|
||||||
|
queryFn: async () => {
|
||||||
|
const queryParams = new URLSearchParams()
|
||||||
|
|
||||||
|
queryParams.append('page', page.toString())
|
||||||
|
queryParams.append('limit', limit.toString())
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
queryParams.append('search', search)
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(filters).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
queryParams.append(key, value.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await api.get(`/chart-of-account-types?${queryParams.toString()}`)
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccountRequest {
|
||||||
|
chart_of_account_id: string
|
||||||
|
name: string
|
||||||
|
number: string
|
||||||
|
account_type: string
|
||||||
|
opening_balance: number
|
||||||
|
description: string
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { Ingredients } from '../../types/services/ingredient'
|
import { Ingredients } from '../../types/services/ingredient'
|
||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
|
import { Ingredient } from '@/types/services/productRecipe'
|
||||||
|
|
||||||
interface IngredientsQueryParams {
|
interface IngredientsQueryParams {
|
||||||
page?: number
|
page?: number
|
||||||
@ -34,3 +35,13 @@ export function useIngredients(params: IngredientsQueryParams = {}) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useIngredientById(id: string) {
|
||||||
|
return useQuery<Ingredient>({
|
||||||
|
queryKey: ['ingredients', id],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await api.get(`/ingredients/${id}`)
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
51
src/services/queries/purchaseOrder.ts
Normal file
51
src/services/queries/purchaseOrder.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { PurchaseOrder, PurchaseOrders } from '@/types/services/purchaseOrder'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { api } from '../api'
|
||||||
|
|
||||||
|
interface PurchaseOrderQueryParams {
|
||||||
|
page?: number
|
||||||
|
limit?: number
|
||||||
|
search?: string
|
||||||
|
status?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePurchaseOrders(params: PurchaseOrderQueryParams = {}) {
|
||||||
|
const { page = 1, limit = 10, search = '', status = '', ...filters } = params
|
||||||
|
|
||||||
|
return useQuery<PurchaseOrders>({
|
||||||
|
queryKey: ['purchase-orders', { page, limit, search, status, ...filters }],
|
||||||
|
queryFn: async () => {
|
||||||
|
const queryParams = new URLSearchParams()
|
||||||
|
|
||||||
|
queryParams.append('page', page.toString())
|
||||||
|
queryParams.append('limit', limit.toString())
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
queryParams.append('search', search)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
queryParams.append('status', status)
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(filters).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
queryParams.append(key, value.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await api.get(`/purchase-orders?${queryParams.toString()}`)
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePurchaseOrderById(id: string) {
|
||||||
|
return useQuery<PurchaseOrder>({
|
||||||
|
queryKey: ['purchase-orders', id],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await api.get(`/purchase-orders/${id}`)
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
13
src/services/queries/unitConverter.ts
Normal file
13
src/services/queries/unitConverter.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { UnitConversion } from '@/types/services/productRecipe'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { api } from '../api'
|
||||||
|
|
||||||
|
export function useUnitConverterByIngredient(IngredientId: string) {
|
||||||
|
return useQuery<UnitConversion[]>({
|
||||||
|
queryKey: ['unit-converters/ingredient', IngredientId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await api.get(`/unit-converters/ingredient/${IngredientId}`)
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
56
src/services/queries/vendor.ts
Normal file
56
src/services/queries/vendor.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { api } from '../api'
|
||||||
|
import { Vendor, Vendors } from '@/types/services/vendor'
|
||||||
|
|
||||||
|
interface VendorQueryParams {
|
||||||
|
page?: number
|
||||||
|
limit?: number
|
||||||
|
search?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useVendors(params: VendorQueryParams = {}) {
|
||||||
|
const { page = 1, limit = 10, search = '', ...filters } = params
|
||||||
|
|
||||||
|
return useQuery<Vendors>({
|
||||||
|
queryKey: ['vendors', { page, limit, search, ...filters }],
|
||||||
|
queryFn: async () => {
|
||||||
|
const queryParams = new URLSearchParams()
|
||||||
|
|
||||||
|
queryParams.append('page', page.toString())
|
||||||
|
queryParams.append('limit', limit.toString())
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
queryParams.append('search', search)
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(filters).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
queryParams.append(key, value.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await api.get(`/vendors?${queryParams.toString()}`)
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useVendorActive() {
|
||||||
|
return useQuery<Vendor[]>({
|
||||||
|
queryKey: ['vendors/active'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await api.get(`/vendors/active`)
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useVendorById(id: string) {
|
||||||
|
return useQuery<Vendor>({
|
||||||
|
queryKey: ['vendors', id],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await api.get(`/vendors/${id}`)
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -10,49 +10,6 @@ export type PurchaseOrderType = {
|
|||||||
total: number
|
total: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IngredientItem {
|
|
||||||
id: number
|
|
||||||
ingredient: { label: string; value: string } | null
|
|
||||||
deskripsi: string
|
|
||||||
kuantitas: number
|
|
||||||
satuan: { label: string; value: string } | null
|
|
||||||
discount: string
|
|
||||||
harga: number
|
|
||||||
pajak: { label: string; value: string } | null
|
|
||||||
waste: { label: string; value: string } | null
|
|
||||||
total: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PurchaseOrderFormData {
|
|
||||||
vendor: { label: string; value: string } | null
|
|
||||||
nomor: string
|
|
||||||
tglTransaksi: string
|
|
||||||
tglJatuhTempo: string
|
|
||||||
referensi: string
|
|
||||||
termin: { label: string; value: string } | null
|
|
||||||
hargaTermasukPajak: boolean
|
|
||||||
showShippingInfo: boolean
|
|
||||||
tanggalPengiriman: string
|
|
||||||
ekspedisi: { label: string; value: string } | null
|
|
||||||
noResi: string
|
|
||||||
showPesan: boolean
|
|
||||||
showAttachment: boolean
|
|
||||||
showTambahDiskon: boolean
|
|
||||||
showBiayaPengiriman: boolean
|
|
||||||
showBiayaTransaksi: boolean
|
|
||||||
showUangMuka: boolean
|
|
||||||
pesan: string
|
|
||||||
ingredientItems: IngredientItem[]
|
|
||||||
transactionCosts?: TransactionCost[]
|
|
||||||
subtotal?: number
|
|
||||||
discountType?: 'percentage' | 'fixed'
|
|
||||||
downPaymentType?: 'percentage' | 'fixed'
|
|
||||||
discountValue?: string
|
|
||||||
shippingCost?: string
|
|
||||||
transactionCost?: string
|
|
||||||
downPayment?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TransactionCost {
|
export interface TransactionCost {
|
||||||
id: string
|
id: string
|
||||||
type: string
|
type: string
|
||||||
|
|||||||
64
src/types/services/chartOfAccount.ts
Normal file
64
src/types/services/chartOfAccount.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
export interface ChartOfAccountType {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
description: string
|
||||||
|
is_active: boolean
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChartOfAccountTypes {
|
||||||
|
data: ChartOfAccountType[]
|
||||||
|
limit: number
|
||||||
|
page: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChartOfAccount {
|
||||||
|
id: string
|
||||||
|
organization_id: string
|
||||||
|
outlet_id: string
|
||||||
|
chart_of_account_type_id: string
|
||||||
|
parent_id: string
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
description: string
|
||||||
|
is_active: boolean
|
||||||
|
is_system: boolean
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
chart_of_account_type: ChartOfAccountType
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChartOfAccounts {
|
||||||
|
data: ChartOfAccount[]
|
||||||
|
limit: number
|
||||||
|
page: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Account {
|
||||||
|
id: string
|
||||||
|
organization_id: string
|
||||||
|
outlet_id: string
|
||||||
|
chart_of_account_id: string
|
||||||
|
name: string
|
||||||
|
number: string
|
||||||
|
account_type: string
|
||||||
|
opening_balance: number
|
||||||
|
current_balance: number
|
||||||
|
description: string
|
||||||
|
is_active: true
|
||||||
|
is_system: false
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
chart_of_account: ChartOfAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Accounts {
|
||||||
|
data: Account[]
|
||||||
|
limit: number
|
||||||
|
page: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
@ -1,56 +1,103 @@
|
|||||||
export interface Product {
|
export interface Product {
|
||||||
ID: string;
|
ID: string
|
||||||
OrganizationID: string;
|
OrganizationID: string
|
||||||
CategoryID: string;
|
CategoryID: string
|
||||||
SKU: string;
|
SKU: string
|
||||||
Name: string;
|
Name: string
|
||||||
Description: string | null;
|
Description: string | null
|
||||||
Price: number;
|
Price: number
|
||||||
Cost: number;
|
Cost: number
|
||||||
BusinessType: string;
|
BusinessType: string
|
||||||
ImageURL: string;
|
ImageURL: string
|
||||||
PrinterType: string;
|
PrinterType: string
|
||||||
UnitID: string | null;
|
UnitID: string | null
|
||||||
HasIngredients: boolean;
|
HasIngredients: boolean
|
||||||
Metadata: Record<string, any>;
|
Metadata: Record<string, any>
|
||||||
IsActive: boolean;
|
IsActive: boolean
|
||||||
CreatedAt: string; // ISO date string
|
CreatedAt: string // ISO date string
|
||||||
UpdatedAt: string; // ISO date string
|
UpdatedAt: string // ISO date string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Ingredient {
|
export interface Ingredient {
|
||||||
id: string;
|
id: string
|
||||||
organization_id: string;
|
organization_id: string
|
||||||
outlet_id: string | null;
|
outlet_id: string | null
|
||||||
name: string;
|
name: string
|
||||||
unit_id: string;
|
unit_id: string
|
||||||
cost: number;
|
cost: number
|
||||||
stock: number;
|
stock: number
|
||||||
is_semi_finished: boolean;
|
is_semi_finished: boolean
|
||||||
is_active: boolean;
|
is_active: boolean
|
||||||
metadata: Record<string, any>;
|
metadata: Record<string, any>
|
||||||
created_at: string;
|
created_at: string
|
||||||
updated_at: string;
|
updated_at: string
|
||||||
|
unit: IngredientUnit
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProductRecipe {
|
export interface ProductRecipe {
|
||||||
id: string;
|
id: string
|
||||||
organization_id: string;
|
organization_id: string
|
||||||
outlet_id: string | null;
|
outlet_id: string | null
|
||||||
product_id: string;
|
product_id: string
|
||||||
variant_id: string | null;
|
variant_id: string | null
|
||||||
ingredient_id: string;
|
ingredient_id: string
|
||||||
quantity: number;
|
quantity: number
|
||||||
created_at: string;
|
waste: number
|
||||||
updated_at: string;
|
created_at: string
|
||||||
product: Product;
|
updated_at: string
|
||||||
ingredient: Ingredient;
|
product: Product
|
||||||
|
ingredient: Ingredient
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProductRecipeRequest {
|
export interface ProductRecipeRequest {
|
||||||
product_id: string;
|
product_id: string
|
||||||
variant_id: string | null;
|
variant_id: string | null
|
||||||
ingredient_id: string;
|
ingredient_id: string
|
||||||
quantity: number;
|
quantity: number
|
||||||
outlet_id: string | null;
|
outlet_id: string | null
|
||||||
|
waste: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IngredientUnit {
|
||||||
|
id: string
|
||||||
|
organization_id: string
|
||||||
|
outlet_id: string
|
||||||
|
name: string
|
||||||
|
abbreviation: string
|
||||||
|
is_active: boolean
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IngredientUnitConverterRequest {
|
||||||
|
ingredient_id: string
|
||||||
|
from_unit_id: string
|
||||||
|
to_unit_id: string
|
||||||
|
conversion_factor: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnitConversion {
|
||||||
|
id: string
|
||||||
|
organization_id: string
|
||||||
|
ingredient_id: string
|
||||||
|
from_unit_id: string
|
||||||
|
to_unit_id: string
|
||||||
|
conversion_factor: number
|
||||||
|
is_active: boolean
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
created_by: string
|
||||||
|
updated_by: string
|
||||||
|
from_unit: UnitConversionFrom
|
||||||
|
to_unit: UnitConversionTo
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnitConversionFrom {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnitConversionTo {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
}
|
}
|
||||||
|
|||||||
120
src/types/services/purchaseOrder.ts
Normal file
120
src/types/services/purchaseOrder.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import { IngredientItem } from './ingredient'
|
||||||
|
import { Vendor } from './vendor'
|
||||||
|
|
||||||
|
export interface PurchaseOrderRequest {
|
||||||
|
vendor_id: string // uuid.UUID
|
||||||
|
po_number: string
|
||||||
|
transaction_date: string // ISO date string
|
||||||
|
due_date: string // ISO date string
|
||||||
|
reference?: string
|
||||||
|
status?: 'draft' | 'sent' | 'approved' | 'received' | 'cancelled'
|
||||||
|
message?: string
|
||||||
|
items: PurchaseOrderItemRequest[]
|
||||||
|
attachment_file_ids?: string[] // uuid.UUID[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PurchaseOrderItemRequest {
|
||||||
|
ingredient_id: string // uuid.UUID
|
||||||
|
description?: string
|
||||||
|
quantity: number
|
||||||
|
unit_id: string // uuid.UUID
|
||||||
|
amount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PurchaseOrders {
|
||||||
|
purchase_orders: PurchaseOrder[]
|
||||||
|
total_count: number
|
||||||
|
page: number
|
||||||
|
limit: number
|
||||||
|
total_pages: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PurchaseOrder {
|
||||||
|
id: string
|
||||||
|
organization_id: string
|
||||||
|
vendor_id: string
|
||||||
|
po_number: string
|
||||||
|
transaction_date: string // RFC3339
|
||||||
|
due_date: string // RFC3339
|
||||||
|
reference: string | null
|
||||||
|
status: string
|
||||||
|
message: string | null
|
||||||
|
total_amount: number
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
vendor: Vendor
|
||||||
|
items: PurchaseOrderItem[]
|
||||||
|
attachments: PurchaseOrderAttachment[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PurchaseOrderItem {
|
||||||
|
id: string
|
||||||
|
purchase_order_id: string
|
||||||
|
ingredient_id: string
|
||||||
|
description: string
|
||||||
|
quantity: number
|
||||||
|
unit_id: string
|
||||||
|
amount: number
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
ingredient: PurchaseOrderIngredient
|
||||||
|
unit: PurchaseOrderUnit
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PurchaseOrderIngredient {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PurchaseOrderUnit {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PurchaseOrderAttachment {
|
||||||
|
id: string
|
||||||
|
purchase_order_id: string
|
||||||
|
file_id: string
|
||||||
|
created_at: string
|
||||||
|
file: PurchaseOrderFile
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PurchaseOrderFile {
|
||||||
|
id: string
|
||||||
|
organization_id: string
|
||||||
|
user_id: string
|
||||||
|
file_name: string
|
||||||
|
original_name: string
|
||||||
|
file_url: string
|
||||||
|
file_size: number
|
||||||
|
mime_type: string
|
||||||
|
file_type: string
|
||||||
|
upload_path: string
|
||||||
|
is_public: boolean
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PurchaseOrderFormData {
|
||||||
|
vendor: { label: string; value: string } | null
|
||||||
|
po_number: string
|
||||||
|
transaction_date: string
|
||||||
|
due_date: string
|
||||||
|
reference: string
|
||||||
|
status: 'draft' | 'sent' | 'approved' | 'received' | 'cancelled'
|
||||||
|
showPesan: boolean
|
||||||
|
showAttachment: boolean
|
||||||
|
message: string
|
||||||
|
items: PurchaseOrderFormItem[]
|
||||||
|
attachment_file_ids: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PurchaseOrderFormItem {
|
||||||
|
id: number // for UI tracking
|
||||||
|
ingredient: { label: string; value: string; originalData?: IngredientItem } | null
|
||||||
|
description: string
|
||||||
|
quantity: number
|
||||||
|
unit: { label: string; value: string } | null
|
||||||
|
amount: number
|
||||||
|
total: number // calculated field for UI
|
||||||
|
}
|
||||||
35
src/types/services/vendor.ts
Normal file
35
src/types/services/vendor.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
export interface Vendor {
|
||||||
|
id: string
|
||||||
|
organization_id: string
|
||||||
|
name: string
|
||||||
|
email?: string
|
||||||
|
phone_number?: string
|
||||||
|
address?: string
|
||||||
|
contact_person?: string
|
||||||
|
tax_number?: string
|
||||||
|
payment_terms?: string
|
||||||
|
notes?: string
|
||||||
|
is_active: boolean
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Vendors {
|
||||||
|
vendors: Vendor[]
|
||||||
|
total_count: number
|
||||||
|
page: number
|
||||||
|
limit: number
|
||||||
|
total_pages: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VendorRequest {
|
||||||
|
name: string
|
||||||
|
email?: string
|
||||||
|
phone_number?: string
|
||||||
|
address?: string
|
||||||
|
contact_person?: string
|
||||||
|
tax_number?: string
|
||||||
|
payment_terms?: string
|
||||||
|
notes?: string
|
||||||
|
is_active: boolean
|
||||||
|
}
|
||||||
@ -5,7 +5,6 @@ import { useState, useEffect } from 'react'
|
|||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Drawer from '@mui/material/Drawer'
|
import Drawer from '@mui/material/Drawer'
|
||||||
import IconButton from '@mui/material/IconButton'
|
import IconButton from '@mui/material/IconButton'
|
||||||
import MenuItem from '@mui/material/MenuItem'
|
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
|
|
||||||
@ -15,64 +14,52 @@ import { useForm, Controller } from 'react-hook-form'
|
|||||||
// Component Imports
|
// Component Imports
|
||||||
import CustomTextField from '@core/components/mui/TextField'
|
import CustomTextField from '@core/components/mui/TextField'
|
||||||
import CustomAutocomplete from '@/@core/components/mui/Autocomplete'
|
import CustomAutocomplete from '@/@core/components/mui/Autocomplete'
|
||||||
|
import { AccountRequest } from '@/services/queries/chartOfAccountType'
|
||||||
// Account Type
|
import { useChartOfAccount } from '@/services/queries/chartOfAccount'
|
||||||
export type AccountType = {
|
import { Account, ChartOfAccount } from '@/types/services/chartOfAccount'
|
||||||
id: number
|
import { useAccountsMutation } from '@/services/mutations/account'
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
category: string
|
|
||||||
balance: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
open: boolean
|
open: boolean
|
||||||
handleClose: () => void
|
handleClose: () => void
|
||||||
accountData?: AccountType[]
|
accountData?: Account[]
|
||||||
setData: (data: AccountType[]) => void
|
setData: (data: Account[]) => void
|
||||||
editingAccount?: AccountType | null
|
editingAccount?: Account | null
|
||||||
}
|
}
|
||||||
|
|
||||||
type FormValidateType = {
|
type FormValidateType = {
|
||||||
name: string
|
name: string
|
||||||
code: string
|
code: string
|
||||||
category: string
|
account_type: string
|
||||||
parentAccount?: string
|
opening_balance: number
|
||||||
|
description: string
|
||||||
|
chart_of_account_id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Categories available for accounts
|
|
||||||
const accountCategories = [
|
|
||||||
'Kas & Bank',
|
|
||||||
'Piutang',
|
|
||||||
'Persediaan',
|
|
||||||
'Aset Tetap',
|
|
||||||
'Hutang',
|
|
||||||
'Ekuitas',
|
|
||||||
'Pendapatan',
|
|
||||||
'Beban'
|
|
||||||
]
|
|
||||||
|
|
||||||
// Parent accounts (dummy data for dropdown)
|
|
||||||
const parentAccounts = [
|
|
||||||
{ id: 1, code: '1-10001', name: 'Kas' },
|
|
||||||
{ id: 2, code: '1-10002', name: 'Bank BCA' },
|
|
||||||
{ id: 3, code: '1-10003', name: 'Bank Mandiri' },
|
|
||||||
{ id: 4, code: '1-10101', name: 'Piutang Usaha' },
|
|
||||||
{ id: 5, code: '1-10201', name: 'Persediaan Barang' },
|
|
||||||
{ id: 6, code: '2-20001', name: 'Hutang Usaha' },
|
|
||||||
{ id: 7, code: '3-30001', name: 'Modal Pemilik' },
|
|
||||||
{ id: 8, code: '4-40001', name: 'Penjualan' },
|
|
||||||
{ id: 9, code: '5-50001', name: 'Beban Gaji' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// Vars
|
// Vars
|
||||||
const initialData = {
|
const initialData = {
|
||||||
name: '',
|
name: '',
|
||||||
code: '',
|
code: '',
|
||||||
category: '',
|
account_type: '',
|
||||||
parentAccount: ''
|
opening_balance: 0,
|
||||||
|
description: '',
|
||||||
|
chart_of_account_id: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Static Account Types
|
||||||
|
const staticAccountTypes = [
|
||||||
|
{ id: '1', name: 'Cash', code: 'cash', description: 'Cash account' },
|
||||||
|
{ id: '2', name: 'Wallet', code: 'wallet', description: 'Digital wallet account' },
|
||||||
|
{ id: '3', name: 'Bank', code: 'bank', description: 'Bank account' },
|
||||||
|
{ id: '4', name: 'Credit', code: 'credit', description: 'Credit account' },
|
||||||
|
{ id: '5', name: 'Debit', code: 'debit', description: 'Debit account' },
|
||||||
|
{ id: '6', name: 'Asset', code: 'asset', description: 'Asset account' },
|
||||||
|
{ id: '7', name: 'Liability', code: 'liability', description: 'Liability account' },
|
||||||
|
{ id: '8', name: 'Equity', code: 'equity', description: 'Equity account' },
|
||||||
|
{ id: '9', name: 'Revenue', code: 'revenue', description: 'Revenue account' },
|
||||||
|
{ id: '10', name: 'Expense', code: 'expense', description: 'Expense account' }
|
||||||
|
]
|
||||||
|
|
||||||
const AccountFormDrawer = (props: Props) => {
|
const AccountFormDrawer = (props: Props) => {
|
||||||
// Props
|
// Props
|
||||||
const { open, handleClose, accountData, setData, editingAccount } = props
|
const { open, handleClose, accountData, setData, editingAccount } = props
|
||||||
@ -80,6 +67,28 @@ const AccountFormDrawer = (props: Props) => {
|
|||||||
// Determine if we're editing
|
// Determine if we're editing
|
||||||
const isEdit = !!editingAccount
|
const isEdit = !!editingAccount
|
||||||
|
|
||||||
|
const { data: accounts, isLoading: isLoadingAccounts } = useChartOfAccount({
|
||||||
|
page: 1,
|
||||||
|
limit: 100
|
||||||
|
})
|
||||||
|
|
||||||
|
const { createAccount, updateAccount } = useAccountsMutation()
|
||||||
|
|
||||||
|
// Use static account types
|
||||||
|
const accountTypeOptions = staticAccountTypes
|
||||||
|
|
||||||
|
// Process chart of accounts for the dropdown
|
||||||
|
const chartOfAccountOptions = accounts?.data.length
|
||||||
|
? accounts.data
|
||||||
|
.filter(account => account.is_active) // Only show active accounts
|
||||||
|
.map(account => ({
|
||||||
|
id: account.id,
|
||||||
|
code: account.code,
|
||||||
|
name: account.name,
|
||||||
|
description: account.description
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@ -97,9 +106,11 @@ const AccountFormDrawer = (props: Props) => {
|
|||||||
// Populate form with existing data
|
// Populate form with existing data
|
||||||
resetForm({
|
resetForm({
|
||||||
name: editingAccount.name,
|
name: editingAccount.name,
|
||||||
code: editingAccount.code,
|
code: editingAccount.number,
|
||||||
category: editingAccount.category,
|
account_type: editingAccount.account_type,
|
||||||
parentAccount: ''
|
opening_balance: editingAccount.opening_balance,
|
||||||
|
description: editingAccount.description || '',
|
||||||
|
chart_of_account_id: editingAccount.chart_of_account_id
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Reset to initial data for new account
|
// Reset to initial data for new account
|
||||||
@ -110,36 +121,41 @@ const AccountFormDrawer = (props: Props) => {
|
|||||||
|
|
||||||
const onSubmit = (data: FormValidateType) => {
|
const onSubmit = (data: FormValidateType) => {
|
||||||
if (isEdit && editingAccount) {
|
if (isEdit && editingAccount) {
|
||||||
// Update existing account
|
const accountRequest: AccountRequest = {
|
||||||
const updatedAccounts =
|
chart_of_account_id: data.chart_of_account_id,
|
||||||
accountData?.map(account =>
|
|
||||||
account.id === editingAccount.id
|
|
||||||
? {
|
|
||||||
...account,
|
|
||||||
code: data.code,
|
|
||||||
name: data.name,
|
name: data.name,
|
||||||
category: data.category
|
number: data.code,
|
||||||
|
account_type: data.account_type,
|
||||||
|
opening_balance: data.opening_balance,
|
||||||
|
description: data.description
|
||||||
}
|
}
|
||||||
: account
|
updateAccount.mutate(
|
||||||
) || []
|
{ id: editingAccount.id, payload: accountRequest },
|
||||||
|
{
|
||||||
setData(updatedAccounts)
|
onSuccess: () => {
|
||||||
} else {
|
|
||||||
// Create new account
|
|
||||||
const newAccount: AccountType = {
|
|
||||||
id: accountData?.length ? Math.max(...accountData.map(a => a.id)) + 1 : 1,
|
|
||||||
code: data.code,
|
|
||||||
name: data.name,
|
|
||||||
category: data.category,
|
|
||||||
balance: '0'
|
|
||||||
}
|
|
||||||
|
|
||||||
setData([...(accountData ?? []), newAccount])
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClose()
|
handleClose()
|
||||||
resetForm(initialData)
|
resetForm(initialData)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Create new account - this would typically be sent as AccountRequest to API
|
||||||
|
const accountRequest: AccountRequest = {
|
||||||
|
chart_of_account_id: data.chart_of_account_id,
|
||||||
|
name: data.name,
|
||||||
|
number: data.code,
|
||||||
|
account_type: data.account_type,
|
||||||
|
opening_balance: data.opening_balance,
|
||||||
|
description: data.description
|
||||||
|
}
|
||||||
|
createAccount.mutate(accountRequest, {
|
||||||
|
onSuccess: () => {
|
||||||
|
handleClose()
|
||||||
|
resetForm(initialData)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
handleClose()
|
handleClose()
|
||||||
@ -225,55 +241,127 @@ const AccountFormDrawer = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Kategori */}
|
{/* Tipe Akun */}
|
||||||
<div>
|
<div>
|
||||||
<Typography variant='body2' className='mb-2'>
|
<Typography variant='body2' className='mb-2'>
|
||||||
Kategori <span className='text-red-500'>*</span>
|
Tipe Akun <span className='text-red-500'>*</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Controller
|
<Controller
|
||||||
name='category'
|
name='account_type'
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: true }}
|
rules={{ required: true }}
|
||||||
render={({ field: { onChange, value, ...field } }) => (
|
render={({ field: { onChange, value, ...field } }) => (
|
||||||
<CustomAutocomplete
|
<CustomAutocomplete
|
||||||
{...field}
|
{...field}
|
||||||
options={accountCategories}
|
options={accountTypeOptions}
|
||||||
value={value || null}
|
value={accountTypeOptions.find(option => option.code === value) || null}
|
||||||
onChange={(_, newValue) => onChange(newValue || '')}
|
onChange={(_, newValue) => onChange(newValue?.code || '')}
|
||||||
|
getOptionLabel={option => option.name}
|
||||||
|
renderOption={(props, option) => (
|
||||||
|
<Box component='li' {...props}>
|
||||||
|
<div>
|
||||||
|
<Typography variant='body2'>{option.name}</Typography>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
renderInput={params => (
|
renderInput={params => (
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
{...params}
|
{...params}
|
||||||
placeholder='Pilih kategori'
|
placeholder='Pilih tipe akun'
|
||||||
{...(errors.category && { error: true, helperText: 'Field ini wajib diisi.' })}
|
{...(errors.account_type && { error: true, helperText: 'Field ini wajib diisi.' })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
isOptionEqualToValue={(option, value) => option === value}
|
isOptionEqualToValue={(option, value) => option.code === value.code}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sub Akun dari */}
|
{/* Chart of Account */}
|
||||||
<div>
|
<div>
|
||||||
<Typography variant='body2' className='mb-2'>
|
<Typography variant='body2' className='mb-2'>
|
||||||
Sub Akun dari
|
Chart of Account <span className='text-red-500'>*</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Controller
|
<Controller
|
||||||
name='parentAccount'
|
name='chart_of_account_id'
|
||||||
control={control}
|
control={control}
|
||||||
|
rules={{ required: true }}
|
||||||
render={({ field: { onChange, value, ...field } }) => (
|
render={({ field: { onChange, value, ...field } }) => (
|
||||||
<CustomAutocomplete
|
<CustomAutocomplete
|
||||||
{...field}
|
{...field}
|
||||||
options={parentAccounts}
|
loading={isLoadingAccounts}
|
||||||
value={parentAccounts.find(account => `${account.code} ${account.name}` === value) || null}
|
options={chartOfAccountOptions}
|
||||||
onChange={(_, newValue) => onChange(newValue ? `${newValue.code} ${newValue.name}` : '')}
|
value={chartOfAccountOptions.find(option => option.id === value) || null}
|
||||||
getOptionLabel={option => `${option.code} ${option.name}`}
|
onChange={(_, newValue) => onChange(newValue?.id || '')}
|
||||||
renderInput={params => <CustomTextField {...params} placeholder='Pilih akun' />}
|
getOptionLabel={option => `${option.code} - ${option.name}`}
|
||||||
isOptionEqualToValue={(option, value) =>
|
renderOption={(props, option) => (
|
||||||
`${option.code} ${option.name}` === `${value.code} ${value.name}`
|
<Box component='li' {...props}>
|
||||||
}
|
<div>
|
||||||
|
<Typography variant='body2'>
|
||||||
|
{option.code} - {option.name}
|
||||||
|
</Typography>
|
||||||
|
{option.description && (
|
||||||
|
<Typography variant='caption' color='textSecondary'>
|
||||||
|
{option.description}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
renderInput={params => (
|
||||||
|
<CustomTextField
|
||||||
|
{...params}
|
||||||
|
placeholder={isLoadingAccounts ? 'Loading chart of accounts...' : 'Pilih chart of account'}
|
||||||
|
{...(errors.chart_of_account_id && { error: true, helperText: 'Field ini wajib diisi.' })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||||
|
disabled={isLoadingAccounts}
|
||||||
|
noOptionsText={isLoadingAccounts ? 'Loading...' : 'Tidak ada chart of account tersedia'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Opening Balance */}
|
||||||
|
<div>
|
||||||
|
<Typography variant='body2' className='mb-2'>
|
||||||
|
Saldo Awal <span className='text-red-500'>*</span>
|
||||||
|
</Typography>
|
||||||
|
<Controller
|
||||||
|
name='opening_balance'
|
||||||
|
control={control}
|
||||||
|
rules={{ required: true, min: 0 }}
|
||||||
|
render={({ field }) => (
|
||||||
|
<CustomTextField
|
||||||
|
{...field}
|
||||||
|
fullWidth
|
||||||
|
type='number'
|
||||||
|
placeholder='0'
|
||||||
|
onChange={e => field.onChange(Number(e.target.value))}
|
||||||
|
{...(errors.opening_balance && {
|
||||||
|
error: true,
|
||||||
|
helperText:
|
||||||
|
errors.opening_balance.type === 'min'
|
||||||
|
? 'Saldo awal tidak boleh negatif.'
|
||||||
|
: 'Field ini wajib diisi.'
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Deskripsi */}
|
||||||
|
<div>
|
||||||
|
<Typography variant='body2' className='mb-2'>
|
||||||
|
Deskripsi
|
||||||
|
</Typography>
|
||||||
|
<Controller
|
||||||
|
name='description'
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<CustomTextField {...field} fullWidth multiline rows={3} placeholder='Deskripsi akun' />
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -41,6 +41,11 @@ import TablePaginationComponent from '@/components/TablePaginationComponent'
|
|||||||
import Loading from '@/components/layout/shared/Loading'
|
import Loading from '@/components/layout/shared/Loading'
|
||||||
import { getLocalizedUrl } from '@/utils/i18n'
|
import { getLocalizedUrl } from '@/utils/i18n'
|
||||||
import AccountFormDrawer from './AccountFormDrawer'
|
import AccountFormDrawer from './AccountFormDrawer'
|
||||||
|
import { useChartOfAccount } from '@/services/queries/chartOfAccount'
|
||||||
|
import { Account, ChartOfAccount } from '@/types/services/chartOfAccount'
|
||||||
|
import { useAccounts } from '@/services/queries/account'
|
||||||
|
import { formatCurrency } from '@/utils/transform'
|
||||||
|
import { useChartOfAccountTypes } from '@/services/queries/chartOfAccountType'
|
||||||
|
|
||||||
// Account Type
|
// Account Type
|
||||||
export type AccountType = {
|
export type AccountType = {
|
||||||
@ -60,119 +65,10 @@ declare module '@tanstack/table-core' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccountTypeWithAction = AccountType & {
|
type AccountTypeWithAction = Account & {
|
||||||
actions?: string
|
actions?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dummy Account Data
|
|
||||||
export const accountsData: AccountType[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
code: '1-10001',
|
|
||||||
name: 'Kas',
|
|
||||||
category: 'Kas & Bank',
|
|
||||||
balance: '20000000'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
code: '1-10002',
|
|
||||||
name: 'Bank BCA',
|
|
||||||
category: 'Kas & Bank',
|
|
||||||
balance: '150000000'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
code: '1-10003',
|
|
||||||
name: 'Bank Mandiri',
|
|
||||||
category: 'Kas & Bank',
|
|
||||||
balance: '75000000'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
code: '1-10101',
|
|
||||||
name: 'Piutang Usaha',
|
|
||||||
category: 'Piutang',
|
|
||||||
balance: '50000000'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
code: '1-10102',
|
|
||||||
name: 'Piutang Karyawan',
|
|
||||||
category: 'Piutang',
|
|
||||||
balance: '5000000'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
code: '1-10201',
|
|
||||||
name: 'Persediaan Barang',
|
|
||||||
category: 'Persediaan',
|
|
||||||
balance: '100000000'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
code: '1-10301',
|
|
||||||
name: 'Peralatan Kantor',
|
|
||||||
category: 'Aset Tetap',
|
|
||||||
balance: '25000000'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
code: '1-10302',
|
|
||||||
name: 'Kendaraan',
|
|
||||||
category: 'Aset Tetap',
|
|
||||||
balance: '200000000'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 9,
|
|
||||||
code: '2-20001',
|
|
||||||
name: 'Hutang Usaha',
|
|
||||||
category: 'Hutang',
|
|
||||||
balance: '-30000000'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 10,
|
|
||||||
code: '2-20002',
|
|
||||||
name: 'Hutang Gaji',
|
|
||||||
category: 'Hutang',
|
|
||||||
balance: '-15000000'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
code: '3-30001',
|
|
||||||
name: 'Modal Pemilik',
|
|
||||||
category: 'Ekuitas',
|
|
||||||
balance: '500000000'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 12,
|
|
||||||
code: '4-40001',
|
|
||||||
name: 'Penjualan',
|
|
||||||
category: 'Pendapatan',
|
|
||||||
balance: '250000000'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 13,
|
|
||||||
code: '5-50001',
|
|
||||||
name: 'Beban Gaji',
|
|
||||||
category: 'Beban',
|
|
||||||
balance: '-80000000'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 14,
|
|
||||||
code: '5-50002',
|
|
||||||
name: 'Beban Listrik',
|
|
||||||
category: 'Beban',
|
|
||||||
balance: '-5000000'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 15,
|
|
||||||
code: '5-50003',
|
|
||||||
name: 'Beban Telepon',
|
|
||||||
category: 'Beban',
|
|
||||||
balance: '-2000000'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// Styled Components
|
// Styled Components
|
||||||
const Icon = styled('i')({})
|
const Icon = styled('i')({})
|
||||||
|
|
||||||
@ -242,16 +138,6 @@ const getCategoryColor = (category: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format currency
|
|
||||||
const formatCurrency = (amount: string) => {
|
|
||||||
const numAmount = parseInt(amount)
|
|
||||||
return new Intl.NumberFormat('id-ID', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'IDR',
|
|
||||||
minimumFractionDigits: 0
|
|
||||||
}).format(Math.abs(numAmount))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Column Definitions
|
// Column Definitions
|
||||||
const columnHelper = createColumnHelper<AccountTypeWithAction>()
|
const columnHelper = createColumnHelper<AccountTypeWithAction>()
|
||||||
|
|
||||||
@ -261,53 +147,25 @@ const AccountListTable = () => {
|
|||||||
// States
|
// States
|
||||||
const [addAccountOpen, setAddAccountOpen] = useState(false)
|
const [addAccountOpen, setAddAccountOpen] = useState(false)
|
||||||
const [rowSelection, setRowSelection] = useState({})
|
const [rowSelection, setRowSelection] = useState({})
|
||||||
const [currentPage, setCurrentPage] = useState(0)
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
const [pageSize, setPageSize] = useState(10)
|
const [pageSize, setPageSize] = useState(10)
|
||||||
const [openConfirm, setOpenConfirm] = useState(false)
|
const [openConfirm, setOpenConfirm] = useState(false)
|
||||||
const [accountId, setAccountId] = useState('')
|
const [accountId, setAccountId] = useState('')
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [categoryFilter, setCategoryFilter] = useState<string>('Semua')
|
const [categoryFilter, setCategoryFilter] = useState<string>('Semua')
|
||||||
const [filteredData, setFilteredData] = useState<AccountType[]>(accountsData)
|
const [editingAccount, setEditingAccount] = useState<Account | null>(null)
|
||||||
const [data, setData] = useState<AccountType[]>(accountsData)
|
|
||||||
const [editingAccount, setEditingAccount] = useState<AccountType | null>(null)
|
const { data, isLoading } = useAccounts({
|
||||||
|
page: currentPage,
|
||||||
|
limit: pageSize,
|
||||||
|
search
|
||||||
|
})
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
const { lang: locale } = useParams()
|
const { lang: locale } = useParams()
|
||||||
|
|
||||||
// Get unique categories for filter
|
const accounts = data?.data ?? []
|
||||||
const categories = useMemo(() => {
|
const totalCount = data?.total ?? 0
|
||||||
const uniqueCategories = [...new Set(data.map(account => account.category))]
|
|
||||||
return ['Semua', ...uniqueCategories]
|
|
||||||
}, [data])
|
|
||||||
|
|
||||||
// Filter data based on search and category
|
|
||||||
useEffect(() => {
|
|
||||||
let filtered = data
|
|
||||||
|
|
||||||
// Filter by search
|
|
||||||
if (search) {
|
|
||||||
filtered = filtered.filter(
|
|
||||||
account =>
|
|
||||||
account.code.toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
account.name.toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
account.category.toLowerCase().includes(search.toLowerCase())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by category
|
|
||||||
if (categoryFilter !== 'Semua') {
|
|
||||||
filtered = filtered.filter(account => account.category === categoryFilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
setFilteredData(filtered)
|
|
||||||
setCurrentPage(0)
|
|
||||||
}, [search, categoryFilter, data])
|
|
||||||
|
|
||||||
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) => {
|
const handlePageChange = useCallback((event: unknown, newPage: number) => {
|
||||||
setCurrentPage(newPage)
|
setCurrentPage(newPage)
|
||||||
@ -319,12 +177,8 @@ const AccountListTable = () => {
|
|||||||
setCurrentPage(0)
|
setCurrentPage(0)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleDelete = () => {
|
|
||||||
setOpenConfirm(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle row click for edit
|
// Handle row click for edit
|
||||||
const handleRowClick = (account: AccountType, event: React.MouseEvent) => {
|
const handleRowClick = (account: Account, event: React.MouseEvent) => {
|
||||||
// Don't trigger row click if clicking on checkbox or link
|
// Don't trigger row click if clicking on checkbox or link
|
||||||
const target = event.target as HTMLElement
|
const target = event.target as HTMLElement
|
||||||
if (target.closest('input[type="checkbox"]') || target.closest('a') || target.closest('button')) {
|
if (target.closest('input[type="checkbox"]') || target.closest('a') || target.closest('button')) {
|
||||||
@ -365,13 +219,17 @@ const AccountListTable = () => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
columnHelper.accessor('code', {
|
columnHelper.accessor('number', {
|
||||||
header: 'Kode Akun',
|
header: 'Kode Akun',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Button
|
<Button
|
||||||
variant='text'
|
variant='text'
|
||||||
color='primary'
|
color='primary'
|
||||||
className='p-0 min-w-0 font-medium normal-case justify-start'
|
className='p-0 min-w-0 font-medium normal-case justify-start'
|
||||||
|
onClick={() => {
|
||||||
|
setEditingAccount(row.original)
|
||||||
|
setAddAccountOpen(true)
|
||||||
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
@ -381,7 +239,7 @@ const AccountListTable = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{row.original.code}
|
{row.original.number}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
@ -393,26 +251,21 @@ const AccountListTable = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('category', {
|
columnHelper.accessor('chart_of_account.name', {
|
||||||
header: 'Kategori',
|
header: 'Kategori',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Chip
|
<Typography color='text.primary' className='font-medium'>
|
||||||
variant='tonal'
|
{row.original.chart_of_account.name}
|
||||||
label={row.original.category}
|
</Typography>
|
||||||
size='small'
|
|
||||||
color={getCategoryColor(row.original.category) as any}
|
|
||||||
className='capitalize'
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('balance', {
|
columnHelper.accessor('current_balance', {
|
||||||
header: 'Saldo',
|
header: 'Saldo',
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const balance = parseInt(row.original.balance)
|
|
||||||
return (
|
return (
|
||||||
<Typography className='font-medium text-right text-primary'>
|
<Typography className='font-medium text-right text-primary'>
|
||||||
{balance < 0 ? '-' : ''}
|
{row.original.current_balance < 0 ? '-' : ''}
|
||||||
{formatCurrency(row.original.balance)}
|
{formatCurrency(row.original.current_balance)}
|
||||||
</Typography>
|
</Typography>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -422,7 +275,7 @@ const AccountListTable = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: paginatedData as AccountType[],
|
data: accounts as Account[],
|
||||||
columns,
|
columns,
|
||||||
filterFns: {
|
filterFns: {
|
||||||
fuzzy: fuzzyFilter
|
fuzzy: fuzzyFilter
|
||||||
@ -484,6 +337,9 @@ const AccountListTable = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='overflow-x-auto'>
|
<div className='overflow-x-auto'>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loading />
|
||||||
|
) : (
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
{table.getHeaderGroups().map(headerGroup => (
|
{table.getHeaderGroups().map(headerGroup => (
|
||||||
@ -512,7 +368,7 @@ const AccountListTable = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</thead>
|
</thead>
|
||||||
{filteredData.length === 0 ? (
|
{accounts.length === 0 ? (
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
|
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
|
||||||
@ -541,6 +397,7 @@ const AccountListTable = () => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
)}
|
)}
|
||||||
</table>
|
</table>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TablePagination
|
<TablePagination
|
||||||
@ -558,13 +415,14 @@ const AccountListTable = () => {
|
|||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
onRowsPerPageChange={handlePageSizeChange}
|
onRowsPerPageChange={handlePageSizeChange}
|
||||||
rowsPerPageOptions={[10, 25, 50]}
|
rowsPerPageOptions={[10, 25, 50]}
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
<AccountFormDrawer
|
<AccountFormDrawer
|
||||||
open={addAccountOpen}
|
open={addAccountOpen}
|
||||||
handleClose={handleCloseDrawer}
|
handleClose={handleCloseDrawer}
|
||||||
accountData={data}
|
accountData={accounts}
|
||||||
setData={setData}
|
setData={() => {}}
|
||||||
editingAccount={editingAccount}
|
editingAccount={editingAccount}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -11,16 +11,18 @@ import FormControl from '@mui/material/FormControl'
|
|||||||
import InputLabel from '@mui/material/InputLabel'
|
import InputLabel from '@mui/material/InputLabel'
|
||||||
import Select from '@mui/material/Select'
|
import Select from '@mui/material/Select'
|
||||||
import MenuItem from '@mui/material/MenuItem'
|
import MenuItem from '@mui/material/MenuItem'
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress'
|
||||||
import CashBankCard from './CashBankCard' // Adjust import path as needed
|
import CashBankCard from './CashBankCard' // Adjust import path as needed
|
||||||
import CustomTextField from '@/@core/components/mui/TextField'
|
import CustomTextField from '@/@core/components/mui/TextField'
|
||||||
import { getLocalizedUrl } from '@/utils/i18n'
|
import { getLocalizedUrl } from '@/utils/i18n'
|
||||||
import { Locale } from '@/configs/i18n'
|
import { Locale } from '@/configs/i18n'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import AccountFormDrawer, { AccountType } from '../account/AccountFormDrawer'
|
import AccountFormDrawer from '../account/AccountFormDrawer'
|
||||||
import { accountsData } from '../account/AccountListTable'
|
|
||||||
import { Button } from '@mui/material'
|
import { Button } from '@mui/material'
|
||||||
|
import { Account } from '@/types/services/chartOfAccount'
|
||||||
|
import { useAccounts } from '@/services/queries/account'
|
||||||
|
import { formatCurrency } from '@/utils/transform'
|
||||||
|
|
||||||
// Types
|
|
||||||
interface BankAccount {
|
interface BankAccount {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
@ -41,188 +43,28 @@ interface BankAccount {
|
|||||||
status: 'active' | 'inactive' | 'blocked'
|
status: 'active' | 'inactive' | 'blocked'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dummy Data
|
// Static chart data for fallback/demo purposes
|
||||||
const dummyAccounts: BankAccount[] = [
|
const generateChartData = (accountType: string, balance: number) => {
|
||||||
{
|
const baseValue = balance || 1000000
|
||||||
id: '1',
|
const variation = baseValue * 0.2
|
||||||
title: 'Giro',
|
|
||||||
accountNumber: '1-10003',
|
return Array.from({ length: 12 }, (_, i) => {
|
||||||
balances: [
|
const randomVariation = (Math.random() - 0.5) * variation
|
||||||
{ amount: '7.313.321', label: 'Saldo di bank' },
|
return Math.max(baseValue + randomVariation, baseValue * 0.5)
|
||||||
{ amount: '30.631.261', label: 'Saldo di kledo' }
|
})
|
||||||
],
|
|
||||||
chartData: [
|
|
||||||
{
|
|
||||||
name: 'Saldo',
|
|
||||||
data: [
|
|
||||||
20000000, 21000000, 20500000, 20800000, 21500000, 22000000, 25000000, 26000000, 28000000, 29000000, 30000000,
|
|
||||||
31000000
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
|
||||||
categories: ['Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des', 'Jan', 'Feb', 'Mar'],
|
const getChartColor = (accountType: string) => {
|
||||||
chartColor: '#ff6b9d',
|
const colors = {
|
||||||
currency: 'IDR',
|
giro: '#ff6b9d',
|
||||||
accountType: 'giro',
|
savings: '#4285f4',
|
||||||
bank: 'Bank Mandiri',
|
investment: '#00bcd4',
|
||||||
status: 'active'
|
credit: '#ff9800',
|
||||||
},
|
cash: '#4caf50'
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
title: 'Tabungan Premium',
|
|
||||||
accountNumber: 'SAV-001234',
|
|
||||||
balances: [
|
|
||||||
{ amount: 15420000, label: 'Saldo Tersedia' },
|
|
||||||
{ amount: 18750000, label: 'Total Saldo' }
|
|
||||||
],
|
|
||||||
chartData: [
|
|
||||||
{
|
|
||||||
name: 'Balance',
|
|
||||||
data: [
|
|
||||||
12000000, 13500000, 14200000, 15000000, 15800000, 16200000, 17000000, 17500000, 18000000, 18200000, 18500000,
|
|
||||||
18750000
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
return colors[accountType as keyof typeof colors] || '#757575'
|
||||||
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
|
|
||||||
chartColor: '#4285f4',
|
|
||||||
currency: 'IDR',
|
|
||||||
accountType: 'savings',
|
|
||||||
bank: 'Bank BCA',
|
|
||||||
status: 'active'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
title: 'Investment Portfolio',
|
|
||||||
accountNumber: 'INV-789012',
|
|
||||||
balances: [
|
|
||||||
{ amount: 125000, label: 'Portfolio Value' },
|
|
||||||
{ amount: 8750, label: 'Total Gains' }
|
|
||||||
],
|
|
||||||
chartData: [
|
|
||||||
{
|
|
||||||
name: 'Portfolio Value',
|
|
||||||
data: [110000, 115000, 112000, 118000, 122000, 119000, 125000, 128000, 126000, 130000, 127000, 125000]
|
|
||||||
}
|
}
|
||||||
],
|
|
||||||
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
|
||||||
currency: 'USD',
|
|
||||||
accountType: 'investment',
|
|
||||||
bank: 'Charles Schwab',
|
|
||||||
status: 'active'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
title: 'Kartu Kredit Platinum',
|
|
||||||
accountNumber: 'CC-456789',
|
|
||||||
balances: [
|
|
||||||
{ amount: 2500000, label: 'Saldo Saat Ini' },
|
|
||||||
{ amount: 47500000, label: 'Limit Tersedia' }
|
|
||||||
],
|
|
||||||
chartData: [
|
|
||||||
{
|
|
||||||
name: 'Spending',
|
|
||||||
data: [
|
|
||||||
1200000, 1800000, 2200000, 1900000, 2100000, 2400000, 2800000, 2600000, 2300000, 2500000, 2700000, 2500000
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
|
|
||||||
currency: 'IDR',
|
|
||||||
accountType: 'credit',
|
|
||||||
bank: 'Bank BNI',
|
|
||||||
status: 'active'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
title: 'Deposito Berjangka',
|
|
||||||
accountNumber: 'DEP-334455',
|
|
||||||
balances: [
|
|
||||||
{ amount: 50000000, label: 'Pokok Deposito' },
|
|
||||||
{ amount: 2500000, label: 'Bunga Terkumpul' }
|
|
||||||
],
|
|
||||||
chartData: [
|
|
||||||
{
|
|
||||||
name: 'Deposito Growth',
|
|
||||||
data: [
|
|
||||||
50000000, 50200000, 50420000, 50650000, 50880000, 51120000, 51360000, 51610000, 51860000, 52120000, 52380000,
|
|
||||||
52500000
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
|
|
||||||
currency: 'IDR',
|
|
||||||
accountType: 'savings',
|
|
||||||
bank: 'Bank BRI',
|
|
||||||
status: 'active'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '6',
|
|
||||||
title: 'Cash Management',
|
|
||||||
accountNumber: 'CSH-111222',
|
|
||||||
balances: [{ amount: 5000, label: 'Available Cash' }],
|
|
||||||
chartData: [
|
|
||||||
{
|
|
||||||
name: 'Cash Flow',
|
|
||||||
data: [4000, 4500, 4200, 4800, 5200, 4900, 5000, 5300, 5100, 5400, 5200, 5000]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
categories: ['Q1', 'Q2', 'Q3', 'Q4', 'Q1', 'Q2', 'Q3', 'Q4', 'Q1', 'Q2', 'Q3', 'Q4'],
|
|
||||||
chartColor: '#00bcd4',
|
|
||||||
currency: 'USD',
|
|
||||||
accountType: 'cash',
|
|
||||||
bank: 'Wells Fargo',
|
|
||||||
status: 'active'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '7',
|
|
||||||
title: 'Rekening Bisnis',
|
|
||||||
accountNumber: 'BIZ-998877',
|
|
||||||
balances: [
|
|
||||||
{ amount: 85000000, label: 'Saldo Operasional' },
|
|
||||||
{ amount: 15000000, label: 'Dana Cadangan' }
|
|
||||||
],
|
|
||||||
chartData: [
|
|
||||||
{
|
|
||||||
name: 'Business Account',
|
|
||||||
data: [
|
|
||||||
70000000, 75000000, 80000000, 82000000, 85000000, 88000000, 90000000, 87000000, 85000000, 89000000, 92000000,
|
|
||||||
100000000
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
|
|
||||||
chartColor: '#ff9800',
|
|
||||||
currency: 'IDR',
|
|
||||||
accountType: 'giro',
|
|
||||||
bank: 'Bank Mandiri',
|
|
||||||
status: 'active'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '8',
|
|
||||||
title: 'Tabungan Pendidikan',
|
|
||||||
accountNumber: 'EDU-567890',
|
|
||||||
balances: [
|
|
||||||
{ amount: 25000000, label: 'Dana Pendidikan' },
|
|
||||||
{ amount: 3500000, label: 'Bunga Terkumpul' }
|
|
||||||
],
|
|
||||||
chartData: [
|
|
||||||
{
|
|
||||||
name: 'Education Savings',
|
|
||||||
data: [
|
|
||||||
20000000, 21000000, 22000000, 23000000, 24000000, 24500000, 25000000, 25500000, 26000000, 27000000, 28000000,
|
|
||||||
28500000
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
|
|
||||||
chartColor: '#3f51b5',
|
|
||||||
currency: 'IDR',
|
|
||||||
accountType: 'savings',
|
|
||||||
bank: 'Bank BCA',
|
|
||||||
status: 'inactive'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
const DebouncedInput = ({
|
const DebouncedInput = ({
|
||||||
value: initialValue,
|
value: initialValue,
|
||||||
onChange,
|
onChange,
|
||||||
@ -251,28 +93,122 @@ const DebouncedInput = ({
|
|||||||
|
|
||||||
return <CustomTextField {...props} value={value} onChange={e => setValue(e.target.value)} />
|
return <CustomTextField {...props} value={value} onChange={e => setValue(e.target.value)} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const CashBankList = () => {
|
const CashBankList = () => {
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
const [editingAccount, setEditingAccount] = useState<AccountType | null>(null)
|
const [editingAccount, setEditingAccount] = useState<Account | null>(null)
|
||||||
const [addAccountOpen, setAddAccountOpen] = useState(false)
|
const [addAccountOpen, setAddAccountOpen] = useState(false)
|
||||||
const [data, setData] = useState<AccountType[]>(accountsData)
|
const [data, setData] = useState<Account[]>([])
|
||||||
const { lang: locale } = useParams()
|
const { lang: locale } = useParams()
|
||||||
|
|
||||||
|
// Use the accounts hook with search parameter
|
||||||
|
const { data: accountsResponse, isLoading } = useAccounts({
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
search: searchQuery
|
||||||
|
})
|
||||||
|
|
||||||
const handleCloseDrawer = () => {
|
const handleCloseDrawer = () => {
|
||||||
setAddAccountOpen(false)
|
setAddAccountOpen(false)
|
||||||
setEditingAccount(null)
|
setEditingAccount(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter and search logic
|
// Transform API data to match our BankAccount interface
|
||||||
|
const transformedAccounts = useMemo((): BankAccount[] => {
|
||||||
|
if (!accountsResponse?.data) return []
|
||||||
|
|
||||||
|
return accountsResponse.data.map((account: Account) => {
|
||||||
|
const chartData = generateChartData(account.account_type, account.current_balance)
|
||||||
|
|
||||||
|
// Map account type to display type
|
||||||
|
const typeMapping = {
|
||||||
|
current_asset: 'giro' as const,
|
||||||
|
non_current_asset: 'investment' as const,
|
||||||
|
current_liability: 'credit' as const,
|
||||||
|
non_current_liability: 'credit' as const,
|
||||||
|
other_current_asset: 'cash' as const,
|
||||||
|
other_current_liability: 'credit' as const,
|
||||||
|
equity: 'savings' as const,
|
||||||
|
revenue: 'savings' as const,
|
||||||
|
expense: 'cash' as const
|
||||||
|
}
|
||||||
|
const displayAccountType = typeMapping[account.account_type as keyof typeof typeMapping] || 'giro'
|
||||||
|
|
||||||
|
// Get bank name from account
|
||||||
|
const getBankName = (acc: Account): string => {
|
||||||
|
if (acc.chart_of_account?.name) {
|
||||||
|
return acc.chart_of_account.name
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeToBank = {
|
||||||
|
current_asset: 'Bank Account',
|
||||||
|
non_current_asset: 'Investment Account',
|
||||||
|
current_liability: 'Credit Account',
|
||||||
|
other_current_asset: 'Cash Account',
|
||||||
|
equity: 'Equity Account',
|
||||||
|
revenue: 'Revenue Account',
|
||||||
|
expense: 'Expense Account'
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeToBank[acc.account_type as keyof typeof typeToBank] || 'General Account'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create balance information
|
||||||
|
const balances = []
|
||||||
|
|
||||||
|
if (account.current_balance !== account.opening_balance) {
|
||||||
|
balances.push({
|
||||||
|
amount: formatCurrency(account.current_balance),
|
||||||
|
label: 'Saldo Saat Ini'
|
||||||
|
})
|
||||||
|
balances.push({
|
||||||
|
amount: formatCurrency(account.opening_balance),
|
||||||
|
label: 'Saldo Awal'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
balances.push({
|
||||||
|
amount: formatCurrency(account.current_balance),
|
||||||
|
label: 'Saldo'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: account.id,
|
||||||
|
title: account.name,
|
||||||
|
accountNumber: account.number,
|
||||||
|
balances,
|
||||||
|
chartData: [
|
||||||
|
{
|
||||||
|
name: 'Saldo',
|
||||||
|
data: chartData
|
||||||
|
}
|
||||||
|
],
|
||||||
|
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
|
||||||
|
chartColor: getChartColor(account.account_type),
|
||||||
|
currency: 'IDR', // Assuming IDR as default, adjust as needed
|
||||||
|
accountType: displayAccountType,
|
||||||
|
bank: getBankName(account),
|
||||||
|
status: account.is_active ? 'active' : 'inactive'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [accountsResponse])
|
||||||
|
|
||||||
|
// Filter accounts based on search (if not handled by API)
|
||||||
const filteredAccounts = useMemo(() => {
|
const filteredAccounts = useMemo(() => {
|
||||||
return dummyAccounts.filter(account => {
|
if (!searchQuery || accountsResponse) {
|
||||||
|
// If using API search or no search, return transformed accounts as is
|
||||||
|
return transformedAccounts
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local filtering fallback
|
||||||
|
return transformedAccounts.filter(account => {
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
account.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
account.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
account.accountNumber.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
account.accountNumber.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
account.bank.toLowerCase().includes(searchQuery.toLowerCase())
|
account.bank.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
return matchesSearch
|
return matchesSearch
|
||||||
})
|
})
|
||||||
}, [searchQuery])
|
}, [transformedAccounts, searchQuery, accountsResponse])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -283,8 +219,16 @@ const CashBankList = () => {
|
|||||||
<DebouncedInput
|
<DebouncedInput
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={value => setSearchQuery(value as string)}
|
onChange={value => setSearchQuery(value as string)}
|
||||||
placeholder='Cari '
|
placeholder='Cari akun...'
|
||||||
className='max-sm:is-full'
|
className='max-sm:is-full'
|
||||||
|
disabled={isLoading}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position='start'>
|
||||||
|
<i className='tabler-search' />
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Box>
|
<Box>
|
||||||
<Button
|
<Button
|
||||||
@ -295,6 +239,7 @@ const CashBankList = () => {
|
|||||||
setEditingAccount(null)
|
setEditingAccount(null)
|
||||||
setAddAccountOpen(true)
|
setAddAccountOpen(true)
|
||||||
}}
|
}}
|
||||||
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
Tambah Akun
|
Tambah Akun
|
||||||
</Button>
|
</Button>
|
||||||
@ -302,7 +247,15 @@ const CashBankList = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Loading State */}
|
||||||
|
{isLoading && (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Account Cards */}
|
{/* Account Cards */}
|
||||||
|
{!isLoading && (
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
{filteredAccounts.length > 0 ? (
|
{filteredAccounts.length > 0 ? (
|
||||||
filteredAccounts.map(account => (
|
filteredAccounts.map(account => (
|
||||||
@ -331,16 +284,40 @@ const CashBankList = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant='h6' color='text.secondary' gutterBottom>
|
<Typography variant='h6' color='text.secondary' gutterBottom>
|
||||||
Tidak ada akun yang ditemukan
|
{searchQuery ? 'Tidak ada akun yang ditemukan' : 'Belum ada akun'}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='body2' color='text.secondary'>
|
<Typography variant='body2' color='text.secondary'>
|
||||||
Coba ubah kata kunci pencarian atau filter yang digunakan
|
{searchQuery
|
||||||
|
? 'Coba ubah kata kunci pencarian yang digunakan'
|
||||||
|
: 'Mulai dengan menambahkan akun baru'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error State (if needed) */}
|
||||||
|
{!isLoading && !accountsResponse && (
|
||||||
|
<Grid size={{ xs: 12 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
textAlign: 'center',
|
||||||
|
py: 8,
|
||||||
|
backgroundColor: 'error.light',
|
||||||
|
borderRadius: 2,
|
||||||
|
color: 'error.contrastText'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant='h6' gutterBottom>
|
||||||
|
Terjadi kesalahan saat memuat data
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body2'>Silakan coba lagi atau hubungi administrator</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
<AccountFormDrawer
|
<AccountFormDrawer
|
||||||
open={addAccountOpen}
|
open={addAccountOpen}
|
||||||
handleClose={handleCloseDrawer}
|
handleClose={handleCloseDrawer}
|
||||||
|
|||||||
@ -11,8 +11,6 @@ import Typography from '@mui/material/Typography'
|
|||||||
// Third-party Imports
|
// Third-party Imports
|
||||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||||
|
|
||||||
// Type Imports
|
|
||||||
|
|
||||||
// Component Imports
|
// Component Imports
|
||||||
import CustomTextField from '@core/components/mui/TextField'
|
import CustomTextField from '@core/components/mui/TextField'
|
||||||
import { Autocomplete } from '@mui/material'
|
import { Autocomplete } from '@mui/material'
|
||||||
@ -25,6 +23,7 @@ import { useOutlets } from '../../../../../services/queries/outlets'
|
|||||||
import { Product } from '../../../../../types/services/product'
|
import { Product } from '../../../../../types/services/product'
|
||||||
import { ProductRecipeRequest } from '../../../../../types/services/productRecipe'
|
import { ProductRecipeRequest } from '../../../../../types/services/productRecipe'
|
||||||
import { resetProductVariant } from '../../../../../redux-store/slices/productRecipe'
|
import { resetProductVariant } from '../../../../../redux-store/slices/productRecipe'
|
||||||
|
import { IngredientItem } from '@/types/services/ingredient'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
open: boolean
|
open: boolean
|
||||||
@ -38,7 +37,8 @@ const initialData = {
|
|||||||
product_id: '',
|
product_id: '',
|
||||||
variant_id: '',
|
variant_id: '',
|
||||||
ingredient_id: '',
|
ingredient_id: '',
|
||||||
quantity: 0
|
quantity: 0,
|
||||||
|
waste: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddRecipeDrawer = (props: Props) => {
|
const AddRecipeDrawer = (props: Props) => {
|
||||||
@ -55,23 +55,45 @@ const AddRecipeDrawer = (props: Props) => {
|
|||||||
const [ingredientDebouncedInput] = useDebounce(ingredientInput, 500)
|
const [ingredientDebouncedInput] = useDebounce(ingredientInput, 500)
|
||||||
const [formData, setFormData] = useState<ProductRecipeRequest>(initialData)
|
const [formData, setFormData] = useState<ProductRecipeRequest>(initialData)
|
||||||
|
|
||||||
|
// Add state untuk menyimpan selected ingredient
|
||||||
|
const [selectedIngredient, setSelectedIngredient] = useState<IngredientItem | null>(null)
|
||||||
|
|
||||||
const { data: outlets, isLoading: outletsLoading } = useOutlets({
|
const { data: outlets, isLoading: outletsLoading } = useOutlets({
|
||||||
search: outletDebouncedInput
|
search: outletDebouncedInput
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Modifikasi query ingredients dengan enabled condition
|
||||||
const { data: ingredients, isLoading: ingredientsLoading } = useIngredients({
|
const { data: ingredients, isLoading: ingredientsLoading } = useIngredients({
|
||||||
search: ingredientDebouncedInput
|
search: ingredientDebouncedInput
|
||||||
})
|
})
|
||||||
|
|
||||||
const outletOptions = useMemo(() => outlets?.outlets || [], [outlets])
|
const outletOptions = useMemo(() => outlets?.outlets || [], [outlets])
|
||||||
const ingredientOptions = useMemo(() => ingredients?.data || [], [ingredients])
|
|
||||||
|
// Perbaiki ingredient options untuk include selected ingredient
|
||||||
|
const ingredientOptions = useMemo(() => {
|
||||||
|
const options = ingredients?.data || []
|
||||||
|
|
||||||
|
// Jika ada selected ingredient dan tidak ada di current options, tambahkan
|
||||||
|
if (selectedIngredient && !options.find(opt => opt.id === selectedIngredient.id)) {
|
||||||
|
return [selectedIngredient, ...options]
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
}, [ingredients, selectedIngredient])
|
||||||
|
|
||||||
const { createProductRecipe, updateProductRecipe } = useProductRecipesMutation()
|
const { createProductRecipe, updateProductRecipe } = useProductRecipesMutation()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentProductRecipe.id) {
|
if (currentProductRecipe.id) {
|
||||||
setFormData(currentProductRecipe)
|
setFormData(currentProductRecipe)
|
||||||
|
|
||||||
|
// Set selected ingredient dari current product recipe
|
||||||
|
const currentIngredient = ingredients?.data?.find(ing => ing.id === currentProductRecipe.ingredient_id)
|
||||||
|
if (currentIngredient) {
|
||||||
|
setSelectedIngredient(currentIngredient)
|
||||||
}
|
}
|
||||||
}, [currentProductRecipe])
|
}
|
||||||
|
}, [currentProductRecipe, ingredients])
|
||||||
|
|
||||||
const handleSubmit = (e: any) => {
|
const handleSubmit = (e: any) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@ -101,24 +123,16 @@ const AddRecipeDrawer = (props: Props) => {
|
|||||||
handleClose()
|
handleClose()
|
||||||
dispatch(resetProductVariant())
|
dispatch(resetProductVariant())
|
||||||
setFormData(initialData)
|
setFormData(initialData)
|
||||||
}
|
setSelectedIngredient(null) // Reset selected ingredient
|
||||||
|
setIngredientInput('') // Reset input
|
||||||
const handleInputChange = (e: any) => {
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
[e.target.name]: e.target.value
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const setTitleDrawer = (recipe: any) => {
|
const setTitleDrawer = (recipe: any) => {
|
||||||
const addOrEdit = currentProductRecipe.id ? 'Edit ' : 'Add '
|
const addOrEdit = currentProductRecipe.id ? 'Edit ' : 'Add '
|
||||||
|
|
||||||
let title = 'Original'
|
let title = 'Original'
|
||||||
|
|
||||||
if (recipe?.name) {
|
if (recipe?.name) {
|
||||||
title = recipe?.name
|
title = recipe?.name
|
||||||
}
|
}
|
||||||
|
|
||||||
return addOrEdit + title
|
return addOrEdit + title
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,13 +158,14 @@ const AddRecipeDrawer = (props: Props) => {
|
|||||||
<Typography color='text.primary' className='font-medium'>
|
<Typography color='text.primary' className='font-medium'>
|
||||||
Basic Information
|
Basic Information
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
options={outletOptions}
|
options={outletOptions}
|
||||||
loading={outletsLoading}
|
loading={outletsLoading}
|
||||||
getOptionLabel={option => option.name}
|
getOptionLabel={option => option.name}
|
||||||
value={outletOptions.find(p => p.id === formData.outlet_id) || null}
|
value={outletOptions.find(p => p.id === formData.outlet_id) || null}
|
||||||
onInputChange={(event, newOutlettInput) => {
|
onInputChange={(event, newOutletInput) => {
|
||||||
setOutletInput(newOutlettInput)
|
setOutletInput(newOutletInput)
|
||||||
}}
|
}}
|
||||||
onChange={(event, newValue) => {
|
onChange={(event, newValue) => {
|
||||||
setFormData({
|
setFormData({
|
||||||
@ -161,7 +176,6 @@ const AddRecipeDrawer = (props: Props) => {
|
|||||||
renderInput={params => (
|
renderInput={params => (
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
{...params}
|
{...params}
|
||||||
className=''
|
|
||||||
label='Outlet'
|
label='Outlet'
|
||||||
fullWidth
|
fullWidth
|
||||||
InputProps={{
|
InputProps={{
|
||||||
@ -171,24 +185,35 @@ const AddRecipeDrawer = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Perbaiki Autocomplete untuk Ingredients */}
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
options={ingredientOptions || []}
|
options={ingredientOptions || []}
|
||||||
loading={ingredientsLoading}
|
loading={ingredientsLoading}
|
||||||
getOptionLabel={option => option.name}
|
getOptionLabel={option => option.name}
|
||||||
value={ingredientOptions?.find(p => p.id === formData.ingredient_id) || null}
|
value={selectedIngredient}
|
||||||
onInputChange={(event, newIngredientInput) => {
|
onInputChange={(event, newIngredientInput) => {
|
||||||
setIngredientInput(newIngredientInput)
|
setIngredientInput(newIngredientInput)
|
||||||
}}
|
}}
|
||||||
onChange={(event, newValue) => {
|
onChange={(event, newValue) => {
|
||||||
|
setSelectedIngredient(newValue) // Set selected ingredient
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
ingredient_id: newValue?.id || ''
|
ingredient_id: newValue?.id || ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Clear input search setelah selection
|
||||||
|
if (newValue) {
|
||||||
|
setIngredientInput('')
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
// Tambahkan props untuk mencegah clear on blur
|
||||||
|
clearOnBlur={false}
|
||||||
|
// Handle case ketika input kosong tapi ada selected value
|
||||||
|
inputValue={selectedIngredient ? selectedIngredient.name : ingredientInput}
|
||||||
renderInput={params => (
|
renderInput={params => (
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
{...params}
|
{...params}
|
||||||
className=''
|
|
||||||
label='Ingredient'
|
label='Ingredient'
|
||||||
fullWidth
|
fullWidth
|
||||||
InputProps={{
|
InputProps={{
|
||||||
@ -198,6 +223,18 @@ const AddRecipeDrawer = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Unit Field - Disabled, value from selected ingredient */}
|
||||||
|
<CustomTextField
|
||||||
|
label='Unit'
|
||||||
|
fullWidth
|
||||||
|
disabled
|
||||||
|
value={selectedIngredient?.unit?.name || ''}
|
||||||
|
InputProps={{
|
||||||
|
readOnly: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
type='number'
|
type='number'
|
||||||
label='Quantity'
|
label='Quantity'
|
||||||
@ -205,6 +242,15 @@ const AddRecipeDrawer = (props: Props) => {
|
|||||||
value={formData.quantity}
|
value={formData.quantity}
|
||||||
onChange={e => setFormData({ ...formData, quantity: Number(e.target.value) })}
|
onChange={e => setFormData({ ...formData, quantity: Number(e.target.value) })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<CustomTextField
|
||||||
|
type='number'
|
||||||
|
label='Waste'
|
||||||
|
fullWidth
|
||||||
|
value={formData.waste}
|
||||||
|
onChange={e => setFormData({ ...formData, waste: Number(e.target.value) })}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className='flex items-center gap-4'>
|
<div className='flex items-center gap-4'>
|
||||||
<Button
|
<Button
|
||||||
variant='contained'
|
variant='contained'
|
||||||
|
|||||||
@ -161,7 +161,7 @@ const ProductDetail = () => {
|
|||||||
<TableCell className='font-semibold text-center'>
|
<TableCell className='font-semibold text-center'>
|
||||||
<div className='flex items-center justify-center gap-2'>
|
<div className='flex items-center justify-center gap-2'>
|
||||||
<i className='tabler-package text-blue-600' />
|
<i className='tabler-package text-blue-600' />
|
||||||
Stock Available
|
Waste
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className='font-semibold text-right'>
|
<TableCell className='font-semibold text-right'>
|
||||||
@ -197,12 +197,7 @@ const ProductDetail = () => {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className='text-center'>{formatCurrency(item.ingredient.cost)}</TableCell>
|
<TableCell className='text-center'>{formatCurrency(item.ingredient.cost)}</TableCell>
|
||||||
<TableCell className='text-center'>
|
<TableCell className='text-center'>
|
||||||
<Chip
|
<Chip label={item.waste ?? 0} size='small' color={'success'} variant='outlined' />
|
||||||
label={item.ingredient.stock}
|
|
||||||
size='small'
|
|
||||||
color={item.ingredient.stock > 5 ? 'success' : 'warning'}
|
|
||||||
variant='outlined'
|
|
||||||
/>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className='text-right font-medium'>
|
<TableCell className='text-right font-medium'>
|
||||||
{formatCurrency(item.ingredient.cost * item.quantity)}
|
{formatCurrency(item.ingredient.cost * item.quantity)}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
// React Imports
|
// React Imports
|
||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
// MUI Imports
|
// MUI Imports
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
@ -17,101 +17,121 @@ import { useForm, Controller } from 'react-hook-form'
|
|||||||
|
|
||||||
// Component Imports
|
// Component Imports
|
||||||
import CustomTextField from '@core/components/mui/TextField'
|
import CustomTextField from '@core/components/mui/TextField'
|
||||||
|
import { Ingredient } from '@/types/services/productRecipe'
|
||||||
|
import { useUnits } from '@/services/queries/units'
|
||||||
|
import { useUnitConventorMutation } from '@/services/mutations/unitConventor'
|
||||||
|
|
||||||
|
// Interface Integration
|
||||||
|
export interface IngredientUnitConverterRequest {
|
||||||
|
ingredient_id: string
|
||||||
|
from_unit_id: string
|
||||||
|
to_unit_id: string
|
||||||
|
conversion_factor: number
|
||||||
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
open: boolean
|
open: boolean
|
||||||
handleClose: () => void
|
handleClose: () => void
|
||||||
setData?: (data: any) => void
|
setData?: (data: IngredientUnitConverterRequest) => void
|
||||||
|
data?: Ingredient // Contains ingredientId, unit info, and cost
|
||||||
}
|
}
|
||||||
|
|
||||||
type UnitConversionType = {
|
type UnitConversionType = {
|
||||||
satuan: string
|
satuan: string // This will be from_unit_id
|
||||||
quantity: number
|
quantity: number
|
||||||
unit: string
|
unit: string // This will be to_unit_id (from data)
|
||||||
hargaBeli: number
|
hargaBeli: number // Calculated as factor * ingredientCost
|
||||||
hargaJual: number
|
hargaJual: number
|
||||||
isDefault: boolean
|
isDefault: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type FormValidateType = {
|
|
||||||
conversions: UnitConversionType[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vars
|
|
||||||
const initialConversion: UnitConversionType = {
|
|
||||||
satuan: 'Box',
|
|
||||||
quantity: 12,
|
|
||||||
unit: 'Pcs',
|
|
||||||
hargaBeli: 3588000,
|
|
||||||
hargaJual: 5988000,
|
|
||||||
isDefault: false
|
|
||||||
}
|
|
||||||
|
|
||||||
const IngedientUnitConversionDrawer = (props: Props) => {
|
const IngedientUnitConversionDrawer = (props: Props) => {
|
||||||
// Props
|
// Props
|
||||||
const { open, handleClose, setData } = props
|
const { open, handleClose, setData, data } = props
|
||||||
|
|
||||||
|
// Extract values from data prop with safe defaults
|
||||||
|
const ingredientId = data?.id || ''
|
||||||
|
const toUnitId = data?.unit_id || data?.unit?.id || ''
|
||||||
|
const ingredientCost = data?.cost || 0
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: units,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
isFetching
|
||||||
|
} = useUnits({
|
||||||
|
page: 1,
|
||||||
|
limit: 20
|
||||||
|
})
|
||||||
|
|
||||||
|
// Vars - initial state with values from data
|
||||||
|
const getInitialConversion = () => ({
|
||||||
|
satuan: '',
|
||||||
|
quantity: 1,
|
||||||
|
unit: toUnitId, // Set from data
|
||||||
|
hargaBeli: ingredientCost, // Will be calculated as factor * ingredientCost
|
||||||
|
hargaJual: 0,
|
||||||
|
isDefault: true
|
||||||
|
})
|
||||||
|
|
||||||
// States
|
// States
|
||||||
const [conversions, setConversions] = useState<UnitConversionType[]>([initialConversion])
|
const [conversion, setConversion] = useState<UnitConversionType>(getInitialConversion())
|
||||||
|
const { createUnitConventer } = useUnitConventorMutation()
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
reset: resetForm,
|
reset: resetForm,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
formState: { errors }
|
formState: { errors }
|
||||||
} = useForm<FormValidateType>({
|
} = useForm<UnitConversionType>({
|
||||||
defaultValues: {
|
defaultValues: getInitialConversion()
|
||||||
conversions: [initialConversion]
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Update form when data changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (toUnitId || ingredientCost) {
|
||||||
|
const updatedConversion = getInitialConversion()
|
||||||
|
setConversion(updatedConversion)
|
||||||
|
resetForm(updatedConversion)
|
||||||
|
}
|
||||||
|
}, [toUnitId, ingredientCost, resetForm])
|
||||||
|
|
||||||
// Functions untuk konversi unit
|
// Functions untuk konversi unit
|
||||||
const handleTambahBaris = () => {
|
const handleChangeConversion = (field: keyof UnitConversionType, value: any) => {
|
||||||
const newConversion: UnitConversionType = {
|
const newConversion = { ...conversion, [field]: value }
|
||||||
satuan: '',
|
setConversion(newConversion)
|
||||||
quantity: 0,
|
setValue(field, value)
|
||||||
unit: '',
|
|
||||||
hargaBeli: 0,
|
|
||||||
hargaJual: 0,
|
|
||||||
isDefault: false
|
|
||||||
}
|
|
||||||
setConversions([...conversions, newConversion])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleHapusBaris = (index: number) => {
|
const onSubmit = (data: UnitConversionType) => {
|
||||||
if (conversions.length > 1) {
|
// Transform form data to IngredientUnitConverterRequest
|
||||||
const newConversions = conversions.filter((_, i) => i !== index)
|
const converterRequest: IngredientUnitConverterRequest = {
|
||||||
setConversions(newConversions)
|
ingredient_id: ingredientId,
|
||||||
}
|
from_unit_id: conversion.satuan,
|
||||||
|
to_unit_id: toUnitId, // Use toUnitId from data prop
|
||||||
|
conversion_factor: conversion.quantity
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChangeConversion = (index: number, field: keyof UnitConversionType, value: any) => {
|
console.log('Unit conversion request:', converterRequest)
|
||||||
const newConversions = [...conversions]
|
|
||||||
newConversions[index] = { ...newConversions[index], [field]: value }
|
|
||||||
setConversions(newConversions)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleToggleDefault = (index: number) => {
|
// if (setData) {
|
||||||
const newConversions = conversions.map((conversion, i) => ({
|
// setData(converterRequest)
|
||||||
...conversion,
|
// }
|
||||||
isDefault: i === index
|
createUnitConventer.mutate(converterRequest, {
|
||||||
}))
|
onSuccess: () => {
|
||||||
setConversions(newConversions)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSubmit = (data: FormValidateType) => {
|
|
||||||
console.log('Unit conversions:', conversions)
|
|
||||||
if (setData) {
|
|
||||||
setData(conversions)
|
|
||||||
}
|
|
||||||
handleClose()
|
handleClose()
|
||||||
|
resetForm(getInitialConversion())
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
handleClose()
|
handleClose()
|
||||||
setConversions([initialConversion])
|
const resetData = getInitialConversion()
|
||||||
resetForm({ conversions: [initialConversion] })
|
setConversion(resetData)
|
||||||
|
resetForm(resetData)
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatNumber = (value: number) => {
|
const formatNumber = (value: number) => {
|
||||||
@ -122,6 +142,12 @@ const IngedientUnitConversionDrawer = (props: Props) => {
|
|||||||
return parseInt(value.replace(/\./g, '')) || 0
|
return parseInt(value.replace(/\./g, '')) || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate total purchase price: factor * ingredientCost
|
||||||
|
const totalPurchasePrice = conversion.quantity * ingredientCost
|
||||||
|
|
||||||
|
// Validation to ensure all required fields are provided
|
||||||
|
const isValidForSubmit = ingredientId && conversion.satuan && toUnitId && conversion.quantity > 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
open={open}
|
open={open}
|
||||||
@ -155,17 +181,37 @@ const IngedientUnitConversionDrawer = (props: Props) => {
|
|||||||
<i className='tabler-x text-2xl text-textPrimary' />
|
<i className='tabler-x text-2xl text-textPrimary' />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
|
{!ingredientId && (
|
||||||
|
<Box sx={{ px: 3, pb: 2 }}>
|
||||||
|
<Typography variant='body2' color='error'>
|
||||||
|
Warning: Ingredient data is required for conversion
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{ingredientId && (
|
||||||
|
<Box sx={{ px: 3, pb: 2 }}>
|
||||||
|
<Typography variant='body2' color='text.secondary'>
|
||||||
|
Converting for: {data?.name || `Ingredient ${ingredientId}`}
|
||||||
|
</Typography>
|
||||||
|
{ingredientCost > 0 && (
|
||||||
|
<Typography variant='body2' color='text.secondary'>
|
||||||
|
Base cost per {units?.data.find(u => u.id === toUnitId)?.name || 'unit'}: Rp{' '}
|
||||||
|
{formatNumber(ingredientCost)}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Scrollable Content */}
|
{/* Scrollable Content */}
|
||||||
<Box sx={{ flex: 1, overflowY: 'auto' }}>
|
<Box sx={{ flex: 1, overflowY: 'auto' }}>
|
||||||
<form id='unit-conversion-form' onSubmit={handleSubmit(data => onSubmit(data))}>
|
<form id='unit-conversion-form' onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className='flex flex-col gap-6 p-6'>
|
<div className='flex flex-col gap-6 p-6'>
|
||||||
{/* Header Kolom */}
|
{/* Header Kolom */}
|
||||||
<Grid container spacing={2} alignItems='center' className='bg-gray-50 p-3 rounded-lg'>
|
<Grid container spacing={2} alignItems='center' className='bg-gray-50 p-3 rounded-lg'>
|
||||||
<Grid size={2}>
|
<Grid size={2}>
|
||||||
<Typography variant='body2' fontWeight='medium'>
|
<Typography variant='body2' fontWeight='medium'>
|
||||||
Satuan
|
From Unit
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={1} className='text-center'>
|
<Grid size={1} className='text-center'>
|
||||||
@ -175,20 +221,20 @@ const IngedientUnitConversionDrawer = (props: Props) => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={1.5}>
|
<Grid size={1.5}>
|
||||||
<Typography variant='body2' fontWeight='medium'>
|
<Typography variant='body2' fontWeight='medium'>
|
||||||
Jumlah
|
Factor
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={1.5}>
|
<Grid size={1.5}>
|
||||||
<Typography variant='body2' fontWeight='medium'>
|
<Typography variant='body2' fontWeight='medium'>
|
||||||
Unit
|
To Unit
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={2}>
|
<Grid size={2.5}>
|
||||||
<Typography variant='body2' fontWeight='medium'>
|
<Typography variant='body2' fontWeight='medium'>
|
||||||
Harga Beli
|
Harga Beli
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={2}>
|
<Grid size={2.5}>
|
||||||
<Typography variant='body2' fontWeight='medium'>
|
<Typography variant='body2' fontWeight='medium'>
|
||||||
Harga Jual
|
Harga Jual
|
||||||
</Typography>
|
</Typography>
|
||||||
@ -198,37 +244,48 @@ const IngedientUnitConversionDrawer = (props: Props) => {
|
|||||||
Default
|
Default
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={1}>
|
</Grid>
|
||||||
|
|
||||||
|
{/* Form Input Row */}
|
||||||
|
<Grid container spacing={2} alignItems='center' className='py-2'>
|
||||||
|
{/* From Unit (Satuan) */}
|
||||||
|
<Grid size={2}>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
<Typography variant='body2' fontWeight='medium'>
|
<Typography variant='body2' fontWeight='medium'>
|
||||||
Action
|
1
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
<Controller
|
||||||
</Grid>
|
name='satuan'
|
||||||
|
control={control}
|
||||||
{/* Baris Konversi */}
|
rules={{ required: 'From unit wajib dipilih' }}
|
||||||
{conversions.map((conversion, index) => (
|
render={({ field }) => (
|
||||||
<Grid container spacing={2} alignItems='center' key={index} className='py-2'>
|
|
||||||
<Grid size={0.5}>
|
|
||||||
<Typography variant='body2' color='text.secondary'>
|
|
||||||
{index + 1}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Satuan */}
|
|
||||||
<Grid size={1.5}>
|
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
|
{...field}
|
||||||
select
|
select
|
||||||
fullWidth
|
fullWidth
|
||||||
size='small'
|
size='small'
|
||||||
value={conversion.satuan}
|
error={!!errors.satuan}
|
||||||
onChange={e => handleChangeConversion(index, 'satuan', e.target.value)}
|
onChange={e => {
|
||||||
|
field.onChange(e.target.value)
|
||||||
|
handleChangeConversion('satuan', e.target.value)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem value='Box'>Box</MenuItem>
|
{units?.data
|
||||||
<MenuItem value='Kg'>Kg</MenuItem>
|
.filter(unit => unit.id !== toUnitId) // Prevent selecting same unit as target
|
||||||
<MenuItem value='Liter'>Liter</MenuItem>
|
.map(unit => (
|
||||||
<MenuItem value='Pack'>Pack</MenuItem>
|
<MenuItem key={unit.id} value={unit.id}>
|
||||||
<MenuItem value='Pcs'>Pcs</MenuItem>
|
{unit.name}
|
||||||
|
</MenuItem>
|
||||||
|
)) ?? []}
|
||||||
</CustomTextField>
|
</CustomTextField>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.satuan && (
|
||||||
|
<Typography variant='caption' color='error' className='mt-1'>
|
||||||
|
{errors.satuan.message}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Tanda sama dengan */}
|
{/* Tanda sama dengan */}
|
||||||
@ -236,59 +293,106 @@ const IngedientUnitConversionDrawer = (props: Props) => {
|
|||||||
<Typography variant='h6'>=</Typography>
|
<Typography variant='h6'>=</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Quantity */}
|
{/* Conversion Factor (Quantity) */}
|
||||||
<Grid size={1.5}>
|
<Grid size={1.5}>
|
||||||
|
<Controller
|
||||||
|
name='quantity'
|
||||||
|
control={control}
|
||||||
|
rules={{
|
||||||
|
required: 'Conversion factor wajib diisi',
|
||||||
|
min: { value: 0.01, message: 'Minimal 0.01' }
|
||||||
|
}}
|
||||||
|
render={({ field }) => (
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
|
{...field}
|
||||||
fullWidth
|
fullWidth
|
||||||
size='small'
|
size='small'
|
||||||
type='number'
|
type='number'
|
||||||
value={conversion.quantity}
|
error={!!errors.quantity}
|
||||||
onChange={e => handleChangeConversion(index, 'quantity', parseInt(e.target.value) || 0)}
|
onChange={e => {
|
||||||
|
const value = parseFloat(e.target.value) || 0
|
||||||
|
field.onChange(value)
|
||||||
|
handleChangeConversion('quantity', value)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.quantity && (
|
||||||
|
<Typography variant='caption' color='error' className='mt-1'>
|
||||||
|
{errors.quantity.message}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Unit */}
|
{/* To Unit - Disabled because it comes from data */}
|
||||||
<Grid size={1.5}>
|
<Grid size={1.5}>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
select
|
select
|
||||||
fullWidth
|
fullWidth
|
||||||
size='small'
|
size='small'
|
||||||
value={conversion.unit}
|
value={toUnitId}
|
||||||
onChange={e => handleChangeConversion(index, 'unit', e.target.value)}
|
disabled
|
||||||
|
InputProps={{
|
||||||
|
sx: { backgroundColor: 'grey.100' }
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem value='Pcs'>Pcs</MenuItem>
|
{units?.data.map(unit => (
|
||||||
<MenuItem value='Kg'>Kg</MenuItem>
|
<MenuItem key={unit.id} value={unit.id}>
|
||||||
<MenuItem value='Gram'>Gram</MenuItem>
|
{unit.name}
|
||||||
<MenuItem value='Liter'>Liter</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem value='ML'>ML</MenuItem>
|
)) ?? []}
|
||||||
</CustomTextField>
|
</CustomTextField>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Harga Beli */}
|
{/* Harga Beli - Calculated as factor * ingredientCost */}
|
||||||
<Grid size={2}>
|
<Grid size={2.5}>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
fullWidth
|
fullWidth
|
||||||
size='small'
|
size='small'
|
||||||
value={formatNumber(conversion.hargaBeli)}
|
value={formatNumber(totalPurchasePrice)}
|
||||||
onChange={e => handleChangeConversion(index, 'hargaBeli', parseNumber(e.target.value))}
|
disabled
|
||||||
|
InputProps={{
|
||||||
|
sx: { backgroundColor: 'grey.100' }
|
||||||
|
}}
|
||||||
|
placeholder='Calculated purchase price'
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Harga Jual */}
|
{/* Harga Jual */}
|
||||||
<Grid size={2}>
|
<Grid size={2.5}>
|
||||||
|
<Controller
|
||||||
|
name='hargaJual'
|
||||||
|
control={control}
|
||||||
|
rules={{
|
||||||
|
min: { value: 0, message: 'Tidak boleh negatif' }
|
||||||
|
}}
|
||||||
|
render={({ field }) => (
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
fullWidth
|
fullWidth
|
||||||
size='small'
|
size='small'
|
||||||
|
error={!!errors.hargaJual}
|
||||||
value={formatNumber(conversion.hargaJual)}
|
value={formatNumber(conversion.hargaJual)}
|
||||||
onChange={e => handleChangeConversion(index, 'hargaJual', parseNumber(e.target.value))}
|
onChange={e => {
|
||||||
|
const value = parseNumber(e.target.value)
|
||||||
|
field.onChange(value)
|
||||||
|
handleChangeConversion('hargaJual', value)
|
||||||
|
}}
|
||||||
|
placeholder='Optional'
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.hargaJual && (
|
||||||
|
<Typography variant='caption' color='error' className='mt-1'>
|
||||||
|
{errors.hargaJual.message}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Default Star */}
|
{/* Default Star */}
|
||||||
<Grid size={1} className='text-center'>
|
<Grid size={1} className='text-center'>
|
||||||
<IconButton
|
<IconButton
|
||||||
size='small'
|
size='small'
|
||||||
onClick={() => handleToggleDefault(index)}
|
onClick={() => handleChangeConversion('isDefault', !conversion.isDefault)}
|
||||||
sx={{
|
sx={{
|
||||||
color: conversion.isDefault ? 'warning.main' : 'grey.400'
|
color: conversion.isDefault ? 'warning.main' : 'grey.400'
|
||||||
}}
|
}}
|
||||||
@ -296,48 +400,67 @@ const IngedientUnitConversionDrawer = (props: Props) => {
|
|||||||
<i className={conversion.isDefault ? 'tabler-star-filled' : 'tabler-star'} />
|
<i className={conversion.isDefault ? 'tabler-star-filled' : 'tabler-star'} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
{/* Delete Button */}
|
{/* Conversion Preview */}
|
||||||
<Grid size={1} className='text-center'>
|
{conversion.quantity > 0 && conversion.satuan && toUnitId && (
|
||||||
{conversions.length > 1 && (
|
<Box className='bg-green-50 p-4 rounded-lg border-l-4 border-green-500'>
|
||||||
<IconButton
|
<Typography variant='body2' fontWeight='medium' className='mb-2'>
|
||||||
size='small'
|
Conversion Preview:
|
||||||
onClick={() => handleHapusBaris(index)}
|
</Typography>
|
||||||
sx={{
|
<Typography variant='body2' className='mb-1'>
|
||||||
color: 'error.main',
|
<strong>1 {units?.data.find(u => u.id === conversion.satuan)?.name || 'Unit'}</strong> ={' '}
|
||||||
border: 1,
|
<strong>
|
||||||
borderColor: 'error.main',
|
{conversion.quantity} {units?.data.find(u => u.id === toUnitId)?.name || 'Unit'}
|
||||||
'&:hover': {
|
</strong>
|
||||||
backgroundColor: 'error.light',
|
</Typography>
|
||||||
borderColor: 'error.main'
|
<Typography variant='caption' color='text.secondary'>
|
||||||
}
|
Conversion Factor: {conversion.quantity}
|
||||||
}}
|
</Typography>
|
||||||
>
|
</Box>
|
||||||
<i className='tabler-trash' />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
)}
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Tambah Baris Button */}
|
{/* Price Summary */}
|
||||||
<div className='flex items-center justify-start'>
|
{conversion.quantity > 0 && (ingredientCost > 0 || conversion.hargaJual > 0) && (
|
||||||
<Button
|
<Box className='bg-blue-50 p-4 rounded-lg border-l-4 border-blue-500'>
|
||||||
variant='outlined'
|
<Typography variant='body2' fontWeight='medium' className='mb-2'>
|
||||||
startIcon={<i className='tabler-plus' />}
|
Price Summary:
|
||||||
onClick={handleTambahBaris}
|
</Typography>
|
||||||
sx={{
|
{ingredientCost > 0 && (
|
||||||
color: 'primary.main',
|
<>
|
||||||
borderColor: 'primary.main',
|
<Typography variant='body2'>
|
||||||
'&:hover': {
|
Total Purchase Price (1 {units?.data.find(u => u.id === conversion.satuan)?.name || 'From Unit'}):
|
||||||
backgroundColor: 'primary.light',
|
Rp {formatNumber(totalPurchasePrice)}
|
||||||
borderColor: 'primary.main'
|
</Typography>
|
||||||
}
|
<Typography variant='body2'>
|
||||||
}}
|
Unit Cost per {units?.data.find(u => u.id === toUnitId)?.name || 'To Unit'}: Rp{' '}
|
||||||
>
|
{formatNumber(ingredientCost)}
|
||||||
Tambah baris
|
</Typography>
|
||||||
</Button>
|
</>
|
||||||
</div>
|
)}
|
||||||
|
{conversion.hargaJual > 0 && (
|
||||||
|
<>
|
||||||
|
<Typography variant='body2'>
|
||||||
|
Total Selling Price (1 {units?.data.find(u => u.id === conversion.satuan)?.name || 'From Unit'}):
|
||||||
|
Rp {formatNumber(conversion.hargaJual)}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body2'>
|
||||||
|
Unit Selling Price per {units?.data.find(u => u.id === toUnitId)?.name || 'To Unit'}: Rp{' '}
|
||||||
|
{formatNumber(Math.round(conversion.hargaJual / conversion.quantity))}
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{ingredientCost > 0 && conversion.hargaJual > 0 && (
|
||||||
|
<Typography variant='body2' className='mt-2 text-blue-700'>
|
||||||
|
Total Margin: Rp {formatNumber(conversion.hargaJual - totalPurchasePrice)} (
|
||||||
|
{totalPurchasePrice > 0
|
||||||
|
? (((conversion.hargaJual - totalPurchasePrice) / totalPurchasePrice) * 100).toFixed(1)
|
||||||
|
: 0}
|
||||||
|
%)
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Box>
|
</Box>
|
||||||
@ -355,13 +478,21 @@ const IngedientUnitConversionDrawer = (props: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className='flex items-center gap-4'>
|
<div className='flex items-center gap-4'>
|
||||||
<Button variant='contained' type='submit' form='unit-conversion-form'>
|
<Button variant='contained' type='submit' form='unit-conversion-form' disabled={!isValidForSubmit}>
|
||||||
Simpan
|
Simpan Konversi
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='tonal' color='error' onClick={() => handleReset()}>
|
<Button variant='tonal' color='error' onClick={handleReset}>
|
||||||
Batal
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{!isValidForSubmit && (
|
||||||
|
<Typography variant='caption' color='error' className='mt-2'>
|
||||||
|
Please fill in all required fields: {!ingredientId && 'Ingredient Data, '}
|
||||||
|
{!conversion.satuan && 'From Unit, '}
|
||||||
|
{!toUnitId && 'To Unit (from ingredient data), '}
|
||||||
|
{conversion.quantity <= 0 && 'Conversion Factor'}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,14 +1,19 @@
|
|||||||
|
import { Ingredient } from '@/types/services/productRecipe'
|
||||||
import { formatCurrency } from '@/utils/transform'
|
import { formatCurrency } from '@/utils/transform'
|
||||||
import { Card, CardHeader, Chip, Typography } from '@mui/material'
|
import { Card, CardHeader, Chip, Typography } from '@mui/material'
|
||||||
|
|
||||||
const IngredientDetailInfo = () => {
|
interface Props {
|
||||||
|
data: Ingredient | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const IngredientDetailInfo = ({ data }: Props) => {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
title={
|
title={
|
||||||
<div className='flex items-center gap-3'>
|
<div className='flex items-center gap-3'>
|
||||||
<Typography variant='h4' component='h1' className='font-bold'>
|
<Typography variant='h4' component='h1' className='font-bold'>
|
||||||
Tepung Terigu
|
{data?.name ?? '-'}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Chip label={'Active'} color={'success'} size='small' />
|
<Chip label={'Active'} color={'success'} size='small' />
|
||||||
</div>
|
</div>
|
||||||
@ -17,7 +22,7 @@ const IngredientDetailInfo = () => {
|
|||||||
<div className='flex flex-col gap-1 mt-2'>
|
<div className='flex flex-col gap-1 mt-2'>
|
||||||
<div className='flex gap-4'>
|
<div className='flex gap-4'>
|
||||||
<Typography variant='body2'>
|
<Typography variant='body2'>
|
||||||
<span className='font-semibold'>Cost:</span> {formatCurrency(5000)}
|
<span className='font-semibold'>Cost:</span> {formatCurrency(data?.cost ?? 0)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,11 +2,19 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Card, CardContent, CardHeader, Typography, Button, Box, Stack } from '@mui/material'
|
import { Card, CardContent, CardHeader, Typography, Button, Box, Stack } from '@mui/material'
|
||||||
import IngedientUnitConversionDrawer from './IngedientUnitConversionDrawer' // Sesuaikan dengan path file Anda
|
import IngedientUnitConversionDrawer from './IngedientUnitConversionDrawer' // Sesuaikan dengan path file Anda
|
||||||
|
import { Ingredient } from '@/types/services/productRecipe'
|
||||||
|
import { useUnitConverterByIngredient } from '@/services/queries/unitConverter'
|
||||||
|
|
||||||
const IngredientDetailUnit = () => {
|
interface Props {
|
||||||
|
data: Ingredient | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const IngredientDetailUnit = ({ data }: Props) => {
|
||||||
// State untuk mengontrol drawer
|
// State untuk mengontrol drawer
|
||||||
const [openConversionDrawer, setOpenConversionDrawer] = useState(false)
|
const [openConversionDrawer, setOpenConversionDrawer] = useState(false)
|
||||||
|
|
||||||
|
const { data: unitConverters, isLoading } = useUnitConverterByIngredient(data?.id as string)
|
||||||
|
|
||||||
// Function untuk membuka drawer
|
// Function untuk membuka drawer
|
||||||
const handleOpenConversionDrawer = () => {
|
const handleOpenConversionDrawer = () => {
|
||||||
setOpenConversionDrawer(true)
|
setOpenConversionDrawer(true)
|
||||||
@ -34,9 +42,19 @@ const IngredientDetailUnit = () => {
|
|||||||
Satuan Dasar
|
Satuan Dasar
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='body1' sx={{ fontWeight: 'medium' }}>
|
<Typography variant='body1' sx={{ fontWeight: 'medium' }}>
|
||||||
: Pcs
|
: {data?.unit.name ?? '-'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
{unitConverters?.map(unitConverter => (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<Typography variant='body1' color='text.secondary'>
|
||||||
|
1 {unitConverter.from_unit.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body1' sx={{ fontWeight: 'medium' }}>
|
||||||
|
: {unitConverter.conversion_factor} {unitConverter.to_unit.name}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)) ?? []}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -61,6 +79,7 @@ const IngredientDetailUnit = () => {
|
|||||||
open={openConversionDrawer}
|
open={openConversionDrawer}
|
||||||
handleClose={handleCloseConversionDrawer}
|
handleClose={handleCloseConversionDrawer}
|
||||||
setData={handleSetConversionData}
|
setData={handleSetConversionData}
|
||||||
|
data={data}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -6,11 +6,18 @@ import IngredientDetailInfo from './IngredientDetailInfo'
|
|||||||
import IngredientDetailUnit from './IngredientDetailUnit'
|
import IngredientDetailUnit from './IngredientDetailUnit'
|
||||||
import IngredientDetailStockAdjustmentDrawer from './IngredientDetailStockAdjustmentDrawer' // Sesuaikan dengan path file Anda
|
import IngredientDetailStockAdjustmentDrawer from './IngredientDetailStockAdjustmentDrawer' // Sesuaikan dengan path file Anda
|
||||||
import { Button } from '@mui/material'
|
import { Button } from '@mui/material'
|
||||||
|
import { useParams } from 'next/navigation'
|
||||||
|
import { useIngredientById } from '@/services/queries/ingredients'
|
||||||
|
|
||||||
const IngredientDetail = () => {
|
const IngredientDetail = () => {
|
||||||
// State untuk mengontrol stock adjustment drawer
|
// State untuk mengontrol stock adjustment drawer
|
||||||
const [openStockAdjustmentDrawer, setOpenStockAdjustmentDrawer] = useState(false)
|
const [openStockAdjustmentDrawer, setOpenStockAdjustmentDrawer] = useState(false)
|
||||||
|
|
||||||
|
const params = useParams()
|
||||||
|
const id = params?.id
|
||||||
|
|
||||||
|
const { data, isLoading } = useIngredientById(id as string)
|
||||||
|
|
||||||
// Function untuk membuka stock adjustment drawer
|
// Function untuk membuka stock adjustment drawer
|
||||||
const handleOpenStockAdjustmentDrawer = () => {
|
const handleOpenStockAdjustmentDrawer = () => {
|
||||||
setOpenStockAdjustmentDrawer(true)
|
setOpenStockAdjustmentDrawer(true)
|
||||||
@ -32,7 +39,7 @@ const IngredientDetail = () => {
|
|||||||
<>
|
<>
|
||||||
<Grid container spacing={6}>
|
<Grid container spacing={6}>
|
||||||
<Grid size={{ xs: 12, lg: 8, md: 7 }}>
|
<Grid size={{ xs: 12, lg: 8, md: 7 }}>
|
||||||
<IngredientDetailInfo />
|
<IngredientDetailInfo data={data} />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 12, lg: 4, md: 5 }}>
|
<Grid size={{ xs: 12, lg: 4, md: 5 }}>
|
||||||
<Button
|
<Button
|
||||||
@ -51,7 +58,7 @@ const IngredientDetail = () => {
|
|||||||
>
|
>
|
||||||
Penyesuaian Stok
|
Penyesuaian Stok
|
||||||
</Button>
|
</Button>
|
||||||
<IngredientDetailUnit />
|
<IngredientDetailUnit data={data} />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|||||||
@ -1,25 +1,40 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
import Grid from '@mui/material/Grid2'
|
import Grid from '@mui/material/Grid2'
|
||||||
import PurchaseDetailInformation from './PurchaseDetailInformation'
|
import PurchaseDetailInformation from './PurchaseDetailInformation'
|
||||||
import PurchaseDetailSendPayment from './PurchaseDetailSendPayment'
|
import PurchaseDetailSendPayment from './PurchaseDetailSendPayment'
|
||||||
import PurchaseDetailLog from './PurchaseDetailLog'
|
import PurchaseDetailLog from './PurchaseDetailLog'
|
||||||
import PurchaseDetailTransaction from './PurchaseDetailTransaction'
|
import PurchaseDetailTransaction from './PurchaseDetailTransaction'
|
||||||
|
import { useParams } from 'next/navigation'
|
||||||
|
import { usePurchaseOrderById } from '@/services/queries/purchaseOrder'
|
||||||
|
import Loading from '@/components/layout/shared/Loading'
|
||||||
|
|
||||||
const PurchaseDetailContent = () => {
|
const PurchaseDetailContent = () => {
|
||||||
|
const params = useParams()
|
||||||
|
const { data, isLoading, error, isFetching } = usePurchaseOrderById(params.id as string)
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loading />
|
||||||
|
) : (
|
||||||
<Grid container spacing={6}>
|
<Grid container spacing={6}>
|
||||||
<Grid size={{ xs: 12 }}>
|
<Grid size={{ xs: 12 }}>
|
||||||
<PurchaseDetailInformation />
|
<PurchaseDetailInformation data={data} />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
{data?.status == 'sent' && (
|
||||||
<Grid size={{ xs: 12 }}>
|
<Grid size={{ xs: 12 }}>
|
||||||
<PurchaseDetailSendPayment />
|
<PurchaseDetailSendPayment />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 12 }}>
|
)}
|
||||||
|
{/* <Grid size={{ xs: 12 }}>
|
||||||
<PurchaseDetailTransaction />
|
<PurchaseDetailTransaction />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 12 }}>
|
<Grid size={{ xs: 12 }}>
|
||||||
<PurchaseDetailLog />
|
<PurchaseDetailLog />
|
||||||
|
</Grid> */}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@ -15,87 +17,62 @@ import {
|
|||||||
IconButton
|
IconButton
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import Grid from '@mui/material/Grid2'
|
import Grid from '@mui/material/Grid2'
|
||||||
|
import { PurchaseOrder } from '@/types/services/purchaseOrder'
|
||||||
|
|
||||||
interface Product {
|
interface Props {
|
||||||
produk: string
|
data?: PurchaseOrder
|
||||||
deskripsi: string
|
|
||||||
kuantitas: number
|
|
||||||
satuan: string
|
|
||||||
discount: string
|
|
||||||
harga: number
|
|
||||||
pajak: string
|
|
||||||
jumlah: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PurchaseData {
|
const PurchaseDetailInformation = ({ data }: Props) => {
|
||||||
vendor: string
|
const purchaseOrder = data
|
||||||
nomor: string
|
|
||||||
tglTransaksi: string
|
|
||||||
tglJatuhTempo: string
|
|
||||||
gudang: string
|
|
||||||
status: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const PurchaseDetailInformation: React.FC = () => {
|
// Helper functions
|
||||||
const purchaseData: PurchaseData = {
|
const formatDate = (dateString: string): string => {
|
||||||
vendor: 'Bagas Rizki Sihotang S.Farm Widodo',
|
const date = new Date(dateString)
|
||||||
nomor: 'PI/00053',
|
return date.toLocaleDateString('id-ID', {
|
||||||
tglTransaksi: '08/09/2025',
|
day: '2-digit',
|
||||||
tglJatuhTempo: '06/10/2025',
|
month: '2-digit',
|
||||||
gudang: 'Unassigned',
|
year: 'numeric'
|
||||||
status: 'Belum Dibayar'
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const products: Product[] = [
|
|
||||||
{
|
|
||||||
produk: 'CB1 - Chelsea Boots',
|
|
||||||
deskripsi: 'Ukuran XS',
|
|
||||||
kuantitas: 3,
|
|
||||||
satuan: 'Pcs',
|
|
||||||
discount: '0%',
|
|
||||||
harga: 299000,
|
|
||||||
pajak: 'PPN',
|
|
||||||
jumlah: 897000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
produk: 'CB1 - Chelsea Boots',
|
|
||||||
deskripsi: 'Ukuran M',
|
|
||||||
kuantitas: 1,
|
|
||||||
satuan: 'Pcs',
|
|
||||||
discount: '0%',
|
|
||||||
harga: 299000,
|
|
||||||
pajak: 'PPN',
|
|
||||||
jumlah: 299000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
produk: 'KH1 - Kneel High Boots',
|
|
||||||
deskripsi: 'Ukuran XL',
|
|
||||||
kuantitas: 1,
|
|
||||||
satuan: 'Pcs',
|
|
||||||
discount: '0%',
|
|
||||||
harga: 299000,
|
|
||||||
pajak: 'PPN',
|
|
||||||
jumlah: 299000
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const totalKuantitas: number = products.reduce((sum, product) => sum + product.kuantitas, 0)
|
|
||||||
const subTotal: number = 1495000
|
|
||||||
const ppn: number = 98670
|
|
||||||
const total: number = 1593670
|
|
||||||
const sisaTagihan: number = 1593670
|
|
||||||
|
|
||||||
const formatCurrency = (amount: number): string => {
|
const formatCurrency = (amount: number): string => {
|
||||||
return new Intl.NumberFormat('id-ID').format(amount)
|
return new Intl.NumberFormat('id-ID').format(amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getStatusLabel = (status: string): string => {
|
||||||
|
const statusMap: Record<string, string> = {
|
||||||
|
draft: 'Draft',
|
||||||
|
sent: 'Dikirim',
|
||||||
|
approved: 'Disetujui',
|
||||||
|
received: 'Diterima',
|
||||||
|
cancelled: 'Dibatalkan'
|
||||||
|
}
|
||||||
|
return statusMap[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusColor = (status: string): 'error' | 'success' | 'warning' | 'info' | 'default' => {
|
||||||
|
const colorMap: Record<string, 'error' | 'success' | 'warning' | 'info' | 'default'> = {
|
||||||
|
draft: 'default',
|
||||||
|
sent: 'warning',
|
||||||
|
approved: 'success',
|
||||||
|
received: 'info',
|
||||||
|
cancelled: 'error'
|
||||||
|
}
|
||||||
|
return colorMap[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculations
|
||||||
|
const totalQuantity = (purchaseOrder?.items ?? []).reduce((sum, item) => sum + (item?.quantity ?? 0), 0)
|
||||||
|
const total = (purchaseOrder?.items ?? []).reduce((sum, item) => sum + (item?.amount ?? 0) * item?.quantity, 0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card sx={{ width: '100%' }}>
|
<Card sx={{ width: '100%' }}>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
title={
|
title={
|
||||||
<Box display='flex' justifyContent='space-between' alignItems='center'>
|
<Box display='flex' justifyContent='space-between' alignItems='center'>
|
||||||
<Typography variant='h5' color='error' sx={{ fontWeight: 'bold' }}>
|
<Typography variant='h5' color={getStatusColor(purchaseOrder?.status ?? '')} sx={{ fontWeight: 'bold' }}>
|
||||||
Belum Dibayar
|
{getStatusLabel(purchaseOrder?.status ?? '')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box>
|
<Box>
|
||||||
<Button startIcon={<i className='tabler-share' />} variant='outlined' size='small' sx={{ mr: 1 }}>
|
<Button startIcon={<i className='tabler-share' />} variant='outlined' size='small' sx={{ mr: 1 }}>
|
||||||
@ -121,24 +98,15 @@ const PurchaseDetailInformation: React.FC = () => {
|
|||||||
Vendor
|
Vendor
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='body1' color='primary' sx={{ fontWeight: 'medium', cursor: 'pointer' }}>
|
<Typography variant='body1' color='primary' sx={{ fontWeight: 'medium', cursor: 'pointer' }}>
|
||||||
{purchaseData.vendor}
|
{purchaseOrder?.vendor?.name ?? ''}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ mb: 2 }}>
|
|
||||||
<Typography variant='subtitle2' color='text.secondary'>
|
|
||||||
Tgl. Transaksi
|
|
||||||
</Typography>
|
|
||||||
<Typography variant='body1'>{purchaseData.tglTransaksi}</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant='subtitle2' color='text.secondary'>
|
<Typography variant='subtitle2' color='text.secondary'>
|
||||||
Gudang
|
Tgl. Transaksi
|
||||||
</Typography>
|
|
||||||
<Typography variant='body1' color='primary' sx={{ cursor: 'pointer' }}>
|
|
||||||
{purchaseData.gudang}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<Typography variant='body1'>{formatDate(purchaseOrder?.transaction_date ?? '')}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@ -147,14 +115,14 @@ const PurchaseDetailInformation: React.FC = () => {
|
|||||||
<Typography variant='subtitle2' color='text.secondary'>
|
<Typography variant='subtitle2' color='text.secondary'>
|
||||||
Nomor
|
Nomor
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='body1'>{purchaseData.nomor}</Typography>
|
<Typography variant='body1'>{purchaseOrder?.po_number}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant='subtitle2' color='text.secondary'>
|
<Typography variant='subtitle2' color='text.secondary'>
|
||||||
Tgl. Jatuh Tempo
|
Tgl. Jatuh Tempo
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='body1'>{purchaseData.tglJatuhTempo}</Typography>
|
<Typography variant='body1'>{formatDate(purchaseOrder?.due_date ?? '')}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -168,43 +136,38 @@ const PurchaseDetailInformation: React.FC = () => {
|
|||||||
<TableCell>Deskripsi</TableCell>
|
<TableCell>Deskripsi</TableCell>
|
||||||
<TableCell align='center'>Kuantitas</TableCell>
|
<TableCell align='center'>Kuantitas</TableCell>
|
||||||
<TableCell align='center'>Satuan</TableCell>
|
<TableCell align='center'>Satuan</TableCell>
|
||||||
<TableCell align='center'>Discount</TableCell>
|
|
||||||
<TableCell align='right'>Harga</TableCell>
|
<TableCell align='right'>Harga</TableCell>
|
||||||
<TableCell align='center'>Pajak</TableCell>
|
|
||||||
<TableCell align='right'>Jumlah</TableCell>
|
<TableCell align='right'>Jumlah</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{products.map((product, index) => (
|
{(purchaseOrder?.items ?? []).map((item, index) => {
|
||||||
<TableRow key={index}>
|
return (
|
||||||
|
<TableRow key={item.id}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Typography variant='body2' color='primary' sx={{ cursor: 'pointer' }}>
|
<Typography variant='body2' color='primary' sx={{ cursor: 'pointer' }}>
|
||||||
{product.produk}
|
{item.ingredient.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{product.deskripsi}</TableCell>
|
<TableCell>{item.description}</TableCell>
|
||||||
<TableCell align='center'>{product.kuantitas}</TableCell>
|
<TableCell align='center'>{item.quantity}</TableCell>
|
||||||
<TableCell align='center'>{product.satuan}</TableCell>
|
<TableCell align='center'>{item.unit.name}</TableCell>
|
||||||
<TableCell align='center'>{product.discount}</TableCell>
|
<TableCell align='right'>{formatCurrency(item.amount)}</TableCell>
|
||||||
<TableCell align='right'>{formatCurrency(product.harga)}</TableCell>
|
<TableCell align='right'>{formatCurrency(item.amount * item.quantity)}</TableCell>
|
||||||
<TableCell align='center'>{product.pajak}</TableCell>
|
|
||||||
<TableCell align='right'>{formatCurrency(product.jumlah)}</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
{/* Total Kuantitas Row */}
|
{/* Total Quantity Row */}
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={2} sx={{ fontWeight: 'bold', borderTop: '2px solid #e0e0e0' }}>
|
<TableCell colSpan={2} sx={{ fontWeight: 'bold', borderTop: '2px solid #e0e0e0' }}>
|
||||||
Total Kuantitas
|
Total Kuantitas
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align='center' sx={{ fontWeight: 'bold', borderTop: '2px solid #e0e0e0' }}>
|
<TableCell align='center' sx={{ fontWeight: 'bold', borderTop: '2px solid #e0e0e0' }}>
|
||||||
{totalKuantitas}
|
{totalQuantity}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell sx={{ borderTop: '2px solid #e0e0e0' }}></TableCell>
|
<TableCell sx={{ borderTop: '2px solid #e0e0e0' }}></TableCell>
|
||||||
<TableCell sx={{ borderTop: '2px solid #e0e0e0' }}></TableCell>
|
<TableCell sx={{ borderTop: '2px solid #e0e0e0' }}></TableCell>
|
||||||
<TableCell sx={{ borderTop: '2px solid #e0e0e0' }}></TableCell>
|
|
||||||
<TableCell sx={{ borderTop: '2px solid #e0e0e0' }}></TableCell>
|
|
||||||
<TableCell sx={{ borderTop: '2px solid #e0e0e0' }}></TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
@ -222,82 +185,19 @@ const PurchaseDetailInformation: React.FC = () => {
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
py: 2,
|
py: 2,
|
||||||
borderBottom: '1px solid #e0e0e0',
|
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
||||||
transition: 'background-color 0.15s ease'
|
transition: 'background-color 0.15s ease'
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant='body1' sx={{ fontWeight: 'medium' }}>
|
<Typography variant='h6' sx={{ fontWeight: 'bold' }}>
|
||||||
Sub Total
|
|
||||||
</Typography>
|
|
||||||
<Typography variant='body1' sx={{ fontWeight: 'medium' }}>
|
|
||||||
{formatCurrency(subTotal)}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
py: 2,
|
|
||||||
borderBottom: '1px solid #e0e0e0',
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
|
||||||
transition: 'background-color 0.15s ease'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant='body1' sx={{ fontWeight: 'medium' }}>
|
|
||||||
PPN
|
|
||||||
</Typography>
|
|
||||||
<Typography variant='body1' sx={{ fontWeight: 'medium' }}>
|
|
||||||
{formatCurrency(ppn)}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
py: 2,
|
|
||||||
borderBottom: '1px solid #e0e0e0',
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
|
||||||
transition: 'background-color 0.15s ease'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant='body1' sx={{ fontWeight: 'bold' }}>
|
|
||||||
Total
|
Total
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='body1' sx={{ fontWeight: 'bold' }}>
|
<Typography variant='h6' sx={{ fontWeight: 'bold' }}>
|
||||||
{formatCurrency(total)}
|
{formatCurrency(total)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
py: 2,
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
|
||||||
transition: 'background-color 0.15s ease'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant='h6' sx={{ fontWeight: 'bold' }}>
|
|
||||||
Sisa Tagihan
|
|
||||||
</Typography>
|
|
||||||
<Typography variant='h6' sx={{ fontWeight: 'bold' }}>
|
|
||||||
{formatCurrency(sisaTagihan)}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
121
src/views/apps/purchase/purchase-form copy/PurchaseAddForm.tsx
Normal file
121
src/views/apps/purchase/purchase-form copy/PurchaseAddForm.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { Card, CardContent } from '@mui/material'
|
||||||
|
import Grid from '@mui/material/Grid2'
|
||||||
|
import { IngredientItem, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes'
|
||||||
|
import PurchaseBasicInfo from './PurchaseBasicInfo'
|
||||||
|
import PurchaseIngredientsTable from './PurchaseIngredientsTable'
|
||||||
|
import PurchaseSummary from './PurchaseSummary'
|
||||||
|
|
||||||
|
const PurchaseAddForm: React.FC = () => {
|
||||||
|
const [formData, setFormData] = useState<PurchaseOrderFormData>({
|
||||||
|
vendor: null,
|
||||||
|
nomor: 'PO/00043',
|
||||||
|
tglTransaksi: '2025-09-09',
|
||||||
|
tglJatuhTempo: '2025-09-10',
|
||||||
|
referensi: '',
|
||||||
|
termin: null,
|
||||||
|
hargaTermasukPajak: true,
|
||||||
|
// Shipping info
|
||||||
|
showShippingInfo: false,
|
||||||
|
tanggalPengiriman: '',
|
||||||
|
ekspedisi: null,
|
||||||
|
noResi: '',
|
||||||
|
// Bottom section toggles
|
||||||
|
showPesan: false,
|
||||||
|
showAttachment: false,
|
||||||
|
showTambahDiskon: false,
|
||||||
|
showBiayaPengiriman: false,
|
||||||
|
showBiayaTransaksi: false,
|
||||||
|
showUangMuka: false,
|
||||||
|
pesan: '',
|
||||||
|
// Ingredient items (updated from productItems)
|
||||||
|
ingredientItems: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
ingredient: null,
|
||||||
|
deskripsi: '',
|
||||||
|
kuantitas: 1,
|
||||||
|
satuan: null,
|
||||||
|
discount: '0',
|
||||||
|
harga: 0,
|
||||||
|
pajak: null,
|
||||||
|
waste: null,
|
||||||
|
total: 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleInputChange = (field: keyof PurchaseOrderFormData, value: any): void => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleIngredientChange = (index: number, field: keyof IngredientItem, value: any): void => {
|
||||||
|
setFormData(prev => {
|
||||||
|
const newItems = [...prev.ingredientItems]
|
||||||
|
newItems[index] = { ...newItems[index], [field]: value }
|
||||||
|
|
||||||
|
// Auto-calculate total if price or quantity changes
|
||||||
|
if (field === 'harga' || field === 'kuantitas') {
|
||||||
|
const item = newItems[index]
|
||||||
|
item.total = item.harga * item.kuantitas
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...prev, ingredientItems: newItems }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const addIngredientItem = (): void => {
|
||||||
|
const newItem: IngredientItem = {
|
||||||
|
id: Date.now(),
|
||||||
|
ingredient: null,
|
||||||
|
deskripsi: '',
|
||||||
|
kuantitas: 1,
|
||||||
|
satuan: null,
|
||||||
|
discount: '0%',
|
||||||
|
harga: 0,
|
||||||
|
pajak: null,
|
||||||
|
waste: null,
|
||||||
|
total: 0
|
||||||
|
}
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
ingredientItems: [...prev.ingredientItems, newItem]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeIngredientItem = (index: number): void => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
ingredientItems: prev.ingredientItems.filter((_, i) => i !== index)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
{/* Basic Info Section */}
|
||||||
|
<PurchaseBasicInfo formData={formData} handleInputChange={handleInputChange} />
|
||||||
|
|
||||||
|
{/* Ingredients Table Section */}
|
||||||
|
<PurchaseIngredientsTable
|
||||||
|
formData={formData}
|
||||||
|
handleIngredientChange={handleIngredientChange}
|
||||||
|
addIngredientItem={addIngredientItem}
|
||||||
|
removeIngredientItem={removeIngredientItem}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Summary Section */}
|
||||||
|
<PurchaseSummary formData={formData} handleInputChange={handleInputChange} />
|
||||||
|
</Grid>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PurchaseAddForm
|
||||||
197
src/views/apps/purchase/purchase-form copy/PurchaseBasicInfo.tsx
Normal file
197
src/views/apps/purchase/purchase-form copy/PurchaseBasicInfo.tsx
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { Button, Switch, FormControlLabel } from '@mui/material'
|
||||||
|
import Grid from '@mui/material/Grid2'
|
||||||
|
import CustomAutocomplete from '@/@core/components/mui/Autocomplete'
|
||||||
|
import CustomTextField from '@/@core/components/mui/TextField'
|
||||||
|
import { DropdownOption, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes'
|
||||||
|
|
||||||
|
interface PurchaseBasicInfoProps {
|
||||||
|
formData: PurchaseOrderFormData
|
||||||
|
handleInputChange: (field: keyof PurchaseOrderFormData, value: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PurchaseBasicInfo: React.FC<PurchaseBasicInfoProps> = ({ formData, handleInputChange }) => {
|
||||||
|
// Sample data for dropdowns
|
||||||
|
const vendorOptions: DropdownOption[] = [
|
||||||
|
{ label: 'Vendor A', value: 'vendor_a' },
|
||||||
|
{ label: 'Vendor B', value: 'vendor_b' },
|
||||||
|
{ label: 'Vendor C', value: 'vendor_c' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const terminOptions: DropdownOption[] = [
|
||||||
|
{ label: 'Net 30', value: 'net_30' },
|
||||||
|
{ label: 'Net 15', value: 'net_15' },
|
||||||
|
{ label: 'Net 60', value: 'net_60' },
|
||||||
|
{ label: 'Cash on Delivery', value: 'cod' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const ekspedisiOptions: DropdownOption[] = [
|
||||||
|
{ label: 'JNE', value: 'jne' },
|
||||||
|
{ label: 'J&T Express', value: 'jnt' },
|
||||||
|
{ label: 'SiCepat', value: 'sicepat' },
|
||||||
|
{ label: 'Pos Indonesia', value: 'pos' },
|
||||||
|
{ label: 'TIKI', value: 'tiki' }
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Row 1 - Vendor dan Nomor */}
|
||||||
|
<Grid size={{ xs: 12, sm: 6, md: 6 }}>
|
||||||
|
<CustomAutocomplete
|
||||||
|
fullWidth
|
||||||
|
options={vendorOptions}
|
||||||
|
value={formData.vendor}
|
||||||
|
onChange={(event, newValue) => handleInputChange('vendor', newValue)}
|
||||||
|
renderInput={params => <CustomTextField {...params} label='Vendor' placeholder='Pilih kontak' fullWidth />}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 6, md: 6 }}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
label='Nomor'
|
||||||
|
value={formData.nomor}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('nomor', e.target.value)}
|
||||||
|
InputProps={{
|
||||||
|
readOnly: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Row 2 - Tgl. Transaksi, Tgl. Jatuh Tempo, Termin */}
|
||||||
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
label='Tgl. Transaksi'
|
||||||
|
type='date'
|
||||||
|
value={formData.tglTransaksi}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('tglTransaksi', e.target.value)}
|
||||||
|
InputLabelProps={{
|
||||||
|
shrink: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
label='Tgl. Jatuh Tempo'
|
||||||
|
type='date'
|
||||||
|
value={formData.tglJatuhTempo}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('tglJatuhTempo', e.target.value)}
|
||||||
|
InputLabelProps={{
|
||||||
|
shrink: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
|
<CustomAutocomplete
|
||||||
|
fullWidth
|
||||||
|
options={terminOptions}
|
||||||
|
value={formData.termin}
|
||||||
|
onChange={(event, newValue) => handleInputChange('termin', newValue)}
|
||||||
|
renderInput={params => <CustomTextField {...params} label='Termin' placeholder='Net 30' fullWidth />}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Row 3 - Tampilkan Informasi Pengiriman */}
|
||||||
|
<Grid size={12}>
|
||||||
|
<Button
|
||||||
|
variant='text'
|
||||||
|
color='primary'
|
||||||
|
onClick={() => handleInputChange('showShippingInfo', !formData.showShippingInfo)}
|
||||||
|
sx={{
|
||||||
|
textTransform: 'none',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 500,
|
||||||
|
padding: '8px 0',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formData.showShippingInfo ? '−' : '+'} {formData.showShippingInfo ? 'Sembunyikan' : 'Tampilkan'} Informasi
|
||||||
|
Pengiriman
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Shipping Information - Conditional */}
|
||||||
|
{formData.showShippingInfo && (
|
||||||
|
<>
|
||||||
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
label='Tanggal Pengiriman'
|
||||||
|
type='date'
|
||||||
|
placeholder='Pilih tanggal'
|
||||||
|
value={formData.tanggalPengiriman}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleInputChange('tanggalPengiriman', e.target.value)
|
||||||
|
}
|
||||||
|
InputLabelProps={{
|
||||||
|
shrink: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
|
<CustomAutocomplete
|
||||||
|
fullWidth
|
||||||
|
options={ekspedisiOptions}
|
||||||
|
value={formData.ekspedisi}
|
||||||
|
onChange={(event, newValue) => handleInputChange('ekspedisi', newValue)}
|
||||||
|
renderInput={params => (
|
||||||
|
<CustomTextField {...params} label='Ekspedisi' placeholder='Pilih ekspedisi' fullWidth />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
label='No. Resi'
|
||||||
|
placeholder='No. Resi'
|
||||||
|
value={formData.noResi}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('noResi', e.target.value)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Row 4 - Referensi, SKU, Switch Pajak */}
|
||||||
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
label='Referensi'
|
||||||
|
placeholder='Referensi'
|
||||||
|
value={formData.referensi}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('referensi', e.target.value)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
|
<CustomTextField fullWidth label='SKU' placeholder='Scan Barcode/SKU' variant='outlined' />
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 4, md: 4 }} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={formData.hargaTermasukPajak}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleInputChange('hargaTermasukPajak', e.target.checked)
|
||||||
|
}
|
||||||
|
color='primary'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label='Harga termasuk pajak'
|
||||||
|
sx={{
|
||||||
|
marginLeft: 0,
|
||||||
|
'& .MuiFormControlLabel-label': {
|
||||||
|
fontSize: '14px',
|
||||||
|
color: 'text.secondary'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PurchaseBasicInfo
|
||||||
@ -0,0 +1,225 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
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'
|
||||||
|
import { IngredientItem, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes'
|
||||||
|
|
||||||
|
interface PurchaseIngredientsTableProps {
|
||||||
|
formData: PurchaseOrderFormData
|
||||||
|
handleIngredientChange: (index: number, field: keyof IngredientItem, value: any) => void
|
||||||
|
addIngredientItem: () => void
|
||||||
|
removeIngredientItem: (index: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PurchaseIngredientsTable: React.FC<PurchaseIngredientsTableProps> = ({
|
||||||
|
formData,
|
||||||
|
handleIngredientChange,
|
||||||
|
addIngredientItem,
|
||||||
|
removeIngredientItem
|
||||||
|
}) => {
|
||||||
|
const ingredientOptions = [
|
||||||
|
{ label: 'Tepung Terigu Premium', value: 'tepung_terigu_premium' },
|
||||||
|
{ label: 'Gula Pasir Halus', value: 'gula_pasir_halus' },
|
||||||
|
{ label: 'Mentega Unsalted', value: 'mentega_unsalted' },
|
||||||
|
{ label: 'Telur Ayam Grade A', value: 'telur_ayam_grade_a' },
|
||||||
|
{ label: 'Vanilla Extract', value: 'vanilla_extract' },
|
||||||
|
{ label: 'Coklat Chips', value: 'coklat_chips' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const satuanOptions = [
|
||||||
|
{ label: 'KG', value: 'kg' },
|
||||||
|
{ label: 'GRAM', value: 'gram' },
|
||||||
|
{ label: 'LITER', value: 'liter' },
|
||||||
|
{ label: 'ML', value: 'ml' },
|
||||||
|
{ label: 'PCS', value: 'pcs' },
|
||||||
|
{ label: 'PACK', value: 'pack' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const pajakOptions = [
|
||||||
|
{ label: 'PPN 11%', value: 'ppn_11' },
|
||||||
|
{ label: 'PPN 0%', value: 'ppn_0' },
|
||||||
|
{ label: 'Bebas Pajak', value: 'tax_free' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const wasteOptions = [
|
||||||
|
{ label: '2%', value: '2' },
|
||||||
|
{ label: '5%', value: '5' },
|
||||||
|
{ label: '10%', value: '10' },
|
||||||
|
{ label: '15%', value: '15' },
|
||||||
|
{ label: 'Custom', value: 'custom' }
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid size={{ xs: 12 }} sx={{ mt: 4 }}>
|
||||||
|
<Typography variant='h6' sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
|
Bahan Baku / Ingredients
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<TableContainer component={Paper} variant='outlined'>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow sx={{ backgroundColor: 'grey.50' }}>
|
||||||
|
<TableCell sx={{ fontWeight: 'bold', minWidth: 180 }}>Bahan Baku</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 'bold', minWidth: 150 }}>Deskripsi</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 'bold', width: 100 }}>Kuantitas</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 'bold', width: 120 }}>Satuan</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 'bold', width: 100 }}>Discount</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 'bold', width: 120 }}>Harga</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 'bold', width: 120 }}>Pajak</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 'bold', width: 120 }}>Waste</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 'bold', width: 100, textAlign: 'right' }}>Total</TableCell>
|
||||||
|
<TableCell sx={{ width: 50 }}></TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{formData.ingredientItems.map((item: IngredientItem, index: number) => (
|
||||||
|
<TableRow key={item.id}>
|
||||||
|
<TableCell>
|
||||||
|
<CustomAutocomplete
|
||||||
|
size='small'
|
||||||
|
options={ingredientOptions}
|
||||||
|
value={item.ingredient}
|
||||||
|
onChange={(event, newValue) => handleIngredientChange(index, 'ingredient', newValue)}
|
||||||
|
renderInput={params => <CustomTextField {...params} placeholder='Pilih Bahan Baku' />}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
size='small'
|
||||||
|
value={item.deskripsi}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleIngredientChange(index, 'deskripsi', e.target.value)
|
||||||
|
}
|
||||||
|
placeholder='Deskripsi'
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
size='small'
|
||||||
|
type='number'
|
||||||
|
value={item.kuantitas}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleIngredientChange(index, 'kuantitas', parseInt(e.target.value) || 1)
|
||||||
|
}
|
||||||
|
inputProps={{ min: 1 }}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<CustomAutocomplete
|
||||||
|
size='small'
|
||||||
|
options={satuanOptions}
|
||||||
|
value={item.satuan}
|
||||||
|
onChange={(event, newValue) => handleIngredientChange(index, 'satuan', newValue)}
|
||||||
|
renderInput={params => <CustomTextField {...params} placeholder='Pilih...' />}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
size='small'
|
||||||
|
value={item.discount}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleIngredientChange(index, 'discount', e.target.value)
|
||||||
|
}
|
||||||
|
placeholder='0%'
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
size='small'
|
||||||
|
type='number'
|
||||||
|
value={item.harga === 0 ? '' : item.harga?.toString() || ''}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value
|
||||||
|
|
||||||
|
if (value === '') {
|
||||||
|
handleIngredientChange(index, 'harga', null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const numericValue = parseFloat(value)
|
||||||
|
handleIngredientChange(index, 'harga', isNaN(numericValue) ? 0 : numericValue)
|
||||||
|
}}
|
||||||
|
inputProps={{ min: 0, step: 'any' }}
|
||||||
|
placeholder='0'
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<CustomAutocomplete
|
||||||
|
size='small'
|
||||||
|
options={pajakOptions}
|
||||||
|
value={item.pajak}
|
||||||
|
onChange={(event, newValue) => handleIngredientChange(index, 'pajak', newValue)}
|
||||||
|
renderInput={params => <CustomTextField {...params} placeholder='...' />}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<CustomAutocomplete
|
||||||
|
size='small'
|
||||||
|
options={wasteOptions}
|
||||||
|
value={item.waste}
|
||||||
|
onChange={(event, newValue) => handleIngredientChange(index, 'waste', newValue)}
|
||||||
|
renderInput={params => <CustomTextField {...params} placeholder='...' />}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
size='small'
|
||||||
|
value={item.total}
|
||||||
|
InputProps={{ readOnly: true }}
|
||||||
|
sx={{
|
||||||
|
'& .MuiInputBase-input': {
|
||||||
|
textAlign: 'right'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<IconButton
|
||||||
|
size='small'
|
||||||
|
color='error'
|
||||||
|
onClick={() => removeIngredientItem(index)}
|
||||||
|
disabled={formData.ingredientItems.length === 1}
|
||||||
|
>
|
||||||
|
<i className='tabler-trash' />
|
||||||
|
</IconButton>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
|
||||||
|
{/* Add New Item Button */}
|
||||||
|
<Button
|
||||||
|
startIcon={<i className='tabler-plus' />}
|
||||||
|
onClick={addIngredientItem}
|
||||||
|
variant='outlined'
|
||||||
|
size='small'
|
||||||
|
sx={{ mt: 1 }}
|
||||||
|
>
|
||||||
|
Tambah bahan baku
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PurchaseIngredientsTable
|
||||||
589
src/views/apps/purchase/purchase-form copy/PurchaseSummary.tsx
Normal file
589
src/views/apps/purchase/purchase-form copy/PurchaseSummary.tsx
Normal file
@ -0,0 +1,589 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { Button, Typography, Box, ToggleButton, ToggleButtonGroup, InputAdornment, IconButton } from '@mui/material'
|
||||||
|
import Grid from '@mui/material/Grid2'
|
||||||
|
import CustomTextField from '@/@core/components/mui/TextField'
|
||||||
|
import { PurchaseOrderFormData, TransactionCost } from '@/types/apps/purchaseOrderTypes'
|
||||||
|
import CustomAutocomplete from '@/@core/components/mui/Autocomplete'
|
||||||
|
import ImageUpload from '@/components/ImageUpload'
|
||||||
|
|
||||||
|
interface PurchaseSummaryProps {
|
||||||
|
formData: PurchaseOrderFormData
|
||||||
|
handleInputChange: (field: keyof PurchaseOrderFormData, value: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PurchaseSummary: React.FC<PurchaseSummaryProps> = ({ formData, handleInputChange }) => {
|
||||||
|
// Initialize transaction costs if not exist
|
||||||
|
const transactionCosts = formData.transactionCosts || []
|
||||||
|
|
||||||
|
// Options for transaction cost types
|
||||||
|
const transactionCostOptions = [
|
||||||
|
{ label: 'Biaya Admin', value: 'admin' },
|
||||||
|
{ label: 'Pajak', value: 'pajak' },
|
||||||
|
{ label: 'Materai', value: 'materai' },
|
||||||
|
{ label: 'Lainnya', value: 'lainnya' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// Add new transaction cost
|
||||||
|
const addTransactionCost = () => {
|
||||||
|
const newCost: TransactionCost = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
type: '',
|
||||||
|
name: '',
|
||||||
|
amount: ''
|
||||||
|
}
|
||||||
|
handleInputChange('transactionCosts', [...transactionCosts, newCost])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove transaction cost
|
||||||
|
const removeTransactionCost = (id: string) => {
|
||||||
|
const filtered = transactionCosts.filter((cost: TransactionCost) => cost.id !== id)
|
||||||
|
handleInputChange('transactionCosts', filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update transaction cost
|
||||||
|
const updateTransactionCost = (id: string, field: keyof TransactionCost, value: string) => {
|
||||||
|
const updated = transactionCosts.map((cost: TransactionCost) =>
|
||||||
|
cost.id === id ? { ...cost, [field]: value } : cost
|
||||||
|
)
|
||||||
|
handleInputChange('transactionCosts', updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate discount amount based on percentage or fixed amount
|
||||||
|
const calculateDiscount = () => {
|
||||||
|
if (!formData.discountValue) return 0
|
||||||
|
|
||||||
|
const subtotal = formData.subtotal || 0
|
||||||
|
if (formData.discountType === 'percentage') {
|
||||||
|
return (subtotal * parseFloat(formData.discountValue)) / 100
|
||||||
|
}
|
||||||
|
return parseFloat(formData.discountValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const discountAmount = calculateDiscount()
|
||||||
|
const shippingCost = parseFloat(formData.shippingCost || '0')
|
||||||
|
|
||||||
|
// Calculate total transaction costs
|
||||||
|
const totalTransactionCost = transactionCosts.reduce((sum: number, cost: TransactionCost) => {
|
||||||
|
return sum + parseFloat(cost.amount || '0')
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
const downPayment = parseFloat(formData.downPayment || '0')
|
||||||
|
|
||||||
|
// Calculate total (subtotal - discount + shipping + transaction costs)
|
||||||
|
const total = (formData.subtotal || 0) - discountAmount + shippingCost + totalTransactionCost
|
||||||
|
|
||||||
|
// Calculate remaining balance (total - down payment)
|
||||||
|
const remainingBalance = total - downPayment
|
||||||
|
|
||||||
|
const handleUpload = async (file: File): Promise<string> => {
|
||||||
|
// Simulate upload
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(URL.createObjectURL(file))
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid size={12} sx={{ mt: 4 }}>
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
{/* Left Side - Pesan and Attachment */}
|
||||||
|
<Grid size={{ xs: 12, md: 7 }}>
|
||||||
|
{/* Pesan Section */}
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<Button
|
||||||
|
variant='text'
|
||||||
|
color='inherit'
|
||||||
|
onClick={() => handleInputChange('showPesan', !formData.showPesan)}
|
||||||
|
sx={{
|
||||||
|
textTransform: 'none',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 500,
|
||||||
|
padding: '12px 16px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
width: '100%',
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
border: '1px solid #e0e0e0',
|
||||||
|
borderRadius: '4px',
|
||||||
|
color: 'text.primary',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#eeeeee'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box component='span' sx={{ mr: 1 }}>
|
||||||
|
{formData.showPesan ? (
|
||||||
|
<i className='tabler-chevron-down w-4 h-4' />
|
||||||
|
) : (
|
||||||
|
<i className='tabler-chevron-right w-4 h-4' />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
Pesan
|
||||||
|
</Button>
|
||||||
|
{formData.showPesan && (
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={3}
|
||||||
|
placeholder='Tambahkan pesan...'
|
||||||
|
value={formData.pesan || ''}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('pesan', e.target.value)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Attachment Section */}
|
||||||
|
<Box>
|
||||||
|
<Button
|
||||||
|
variant='text'
|
||||||
|
color='inherit'
|
||||||
|
onClick={() => handleInputChange('showAttachment', !formData.showAttachment)}
|
||||||
|
sx={{
|
||||||
|
textTransform: 'none',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 500,
|
||||||
|
padding: '12px 16px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
width: '100%',
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
border: '1px solid #e0e0e0',
|
||||||
|
borderRadius: '4px',
|
||||||
|
color: 'text.primary',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#eeeeee'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box component='span' sx={{ mr: 1 }}>
|
||||||
|
{formData.showAttachment ? (
|
||||||
|
<i className='tabler-chevron-down w-4 h-4' />
|
||||||
|
) : (
|
||||||
|
<i className='tabler-chevron-right w-4 h-4' />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
Attachment
|
||||||
|
</Button>
|
||||||
|
{formData.showAttachment && (
|
||||||
|
<ImageUpload
|
||||||
|
onUpload={handleUpload}
|
||||||
|
maxFileSize={1 * 1024 * 1024} // 1MB
|
||||||
|
showUrlOption={false}
|
||||||
|
dragDropText='Drop your image here'
|
||||||
|
browseButtonText='Choose Image'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Right Side - Totals */}
|
||||||
|
<Grid size={{ xs: 12, md: 5 }}>
|
||||||
|
<Box sx={{ backgroundColor: '#ffffff', p: 3, borderRadius: '8px' }}>
|
||||||
|
{/* Sub Total */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
py: 2,
|
||||||
|
borderBottom: '1px solid #e0e0e0',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#f8f8f8'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant='body1' color='text.secondary' sx={{ fontSize: '16px' }}>
|
||||||
|
Sub Total
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body1' fontWeight={600} sx={{ fontSize: '16px', textAlign: 'right' }}>
|
||||||
|
{new Intl.NumberFormat('id-ID', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'IDR',
|
||||||
|
minimumFractionDigits: 0
|
||||||
|
}).format(formData.subtotal || 0)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Additional Options */}
|
||||||
|
<Box>
|
||||||
|
{/* Tambah Diskon */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
py: 2,
|
||||||
|
borderBottom: '1px solid #e0e0e0',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#f8f8f8'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant='text'
|
||||||
|
color='primary'
|
||||||
|
size='small'
|
||||||
|
sx={{ textTransform: 'none', fontSize: '14px', p: 0, textAlign: 'left' }}
|
||||||
|
onClick={() => handleInputChange('showTambahDiskon', !formData.showTambahDiskon)}
|
||||||
|
>
|
||||||
|
{formData.showTambahDiskon ? '- Sembunyikan Diskon' : '+ Tambahan Diskon'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Show input form when showTambahDiskon is true */}
|
||||||
|
{formData.showTambahDiskon && (
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', mb: 1 }}>
|
||||||
|
<CustomTextField
|
||||||
|
size='small'
|
||||||
|
placeholder='0'
|
||||||
|
value={formData.discountValue || ''}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleInputChange('discountValue', e.target.value)
|
||||||
|
}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment:
|
||||||
|
formData.discountType === 'percentage' ? (
|
||||||
|
<InputAdornment position='end'>%</InputAdornment>
|
||||||
|
) : undefined
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
value={formData.discountType || 'percentage'}
|
||||||
|
exclusive
|
||||||
|
onChange={(_, newValue) => {
|
||||||
|
if (newValue) handleInputChange('discountType', newValue)
|
||||||
|
}}
|
||||||
|
size='small'
|
||||||
|
>
|
||||||
|
<ToggleButton value='percentage' sx={{ px: 2 }}>
|
||||||
|
%
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value='fixed' sx={{ px: 2 }}>
|
||||||
|
Rp
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Biaya Pengiriman */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
py: 2,
|
||||||
|
borderBottom: '1px solid #e0e0e0',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#f8f8f8'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant='text'
|
||||||
|
color='primary'
|
||||||
|
size='small'
|
||||||
|
sx={{ textTransform: 'none', fontSize: '14px', p: 0, textAlign: 'left' }}
|
||||||
|
onClick={() => handleInputChange('showBiayaPengiriman', !formData.showBiayaPengiriman)}
|
||||||
|
>
|
||||||
|
{formData.showBiayaPengiriman ? '- Sembunyikan Biaya Pengiriman' : '+ Biaya pengiriman'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Show input form when showBiayaPengiriman is true */}
|
||||||
|
{formData.showBiayaPengiriman && (
|
||||||
|
<Box sx={{ mt: 2, display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<Typography variant='body2' sx={{ minWidth: '140px' }}>
|
||||||
|
Biaya pengiriman
|
||||||
|
</Typography>
|
||||||
|
<CustomTextField
|
||||||
|
size='small'
|
||||||
|
placeholder='0'
|
||||||
|
value={formData.shippingCost || ''}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleInputChange('shippingCost', e.target.value)
|
||||||
|
}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: <InputAdornment position='start'>Rp</InputAdornment>
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Biaya Transaksi - Multiple */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
py: 2,
|
||||||
|
borderBottom: '1px solid #e0e0e0',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#f8f8f8'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant='text'
|
||||||
|
color='primary'
|
||||||
|
size='small'
|
||||||
|
sx={{ textTransform: 'none', fontSize: '14px', p: 0, textAlign: 'left' }}
|
||||||
|
onClick={() => {
|
||||||
|
if (!formData.showBiayaTransaksi) {
|
||||||
|
handleInputChange('showBiayaTransaksi', true)
|
||||||
|
if (transactionCosts.length === 0) {
|
||||||
|
addTransactionCost()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleInputChange('showBiayaTransaksi', false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formData.showBiayaTransaksi ? '- Sembunyikan Biaya Transaksi' : '+ Biaya Transaksi'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Show multiple transaction cost inputs */}
|
||||||
|
{formData.showBiayaTransaksi && (
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
{transactionCosts.map((cost: TransactionCost, index: number) => (
|
||||||
|
<Box key={cost.id} sx={{ display: 'flex', gap: 1, alignItems: 'center', mb: 2 }}>
|
||||||
|
{/* Remove button */}
|
||||||
|
<IconButton
|
||||||
|
size='small'
|
||||||
|
onClick={() => removeTransactionCost(cost.id)}
|
||||||
|
sx={{
|
||||||
|
color: 'error.main',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'error.main',
|
||||||
|
borderRadius: '50%',
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'error.lighter'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className='tabler-trash' />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
{/* Type AutoComplete */}
|
||||||
|
<CustomAutocomplete
|
||||||
|
size='small'
|
||||||
|
options={transactionCostOptions}
|
||||||
|
getOptionLabel={option => (typeof option === 'string' ? option : option.label)}
|
||||||
|
value={transactionCostOptions.find(option => option.value === cost.type) || null}
|
||||||
|
onChange={(_, newValue) => {
|
||||||
|
updateTransactionCost(cost.id, 'type', newValue ? newValue.value : '')
|
||||||
|
}}
|
||||||
|
renderInput={params => (
|
||||||
|
<CustomTextField {...params} size='small' placeholder='Pilih biaya transaksi...' />
|
||||||
|
)}
|
||||||
|
sx={{ minWidth: 180 }}
|
||||||
|
noOptionsText='Tidak ada pilihan'
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Name input */}
|
||||||
|
<CustomTextField
|
||||||
|
size='small'
|
||||||
|
placeholder='Nama'
|
||||||
|
value={cost.name}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
updateTransactionCost(cost.id, 'name', e.target.value)
|
||||||
|
}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Amount input */}
|
||||||
|
<CustomTextField
|
||||||
|
size='small'
|
||||||
|
placeholder='0'
|
||||||
|
value={cost.amount}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
updateTransactionCost(cost.id, 'amount', e.target.value)
|
||||||
|
}
|
||||||
|
sx={{ width: 120 }}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: <InputAdornment position='start'>Rp</InputAdornment>
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Add more button */}
|
||||||
|
<Button
|
||||||
|
variant='text'
|
||||||
|
color='primary'
|
||||||
|
size='small'
|
||||||
|
onClick={addTransactionCost}
|
||||||
|
sx={{
|
||||||
|
textTransform: 'none',
|
||||||
|
fontSize: '13px',
|
||||||
|
mt: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+ Tambah biaya transaksi lain
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Total */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
py: 2,
|
||||||
|
borderBottom: '1px solid #e0e0e0',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#f8f8f8'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant='h6' fontWeight={600} sx={{ fontSize: '18px' }}>
|
||||||
|
Total
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='h6' fontWeight={600} sx={{ fontSize: '18px', textAlign: 'right' }}>
|
||||||
|
{new Intl.NumberFormat('id-ID', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'IDR',
|
||||||
|
minimumFractionDigits: 0
|
||||||
|
}).format(total)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Uang Muka */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
py: 2,
|
||||||
|
borderBottom: '1px solid #e0e0e0',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#f8f8f8'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant='text'
|
||||||
|
color='primary'
|
||||||
|
size='small'
|
||||||
|
sx={{ textTransform: 'none', fontSize: '14px', p: 0, textAlign: 'left' }}
|
||||||
|
onClick={() => handleInputChange('showUangMuka', !formData.showUangMuka)}
|
||||||
|
>
|
||||||
|
{formData.showUangMuka ? '- Sembunyikan Uang Muka' : '+ Uang Muka'}
|
||||||
|
</Button>
|
||||||
|
{formData.showUangMuka && (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
{/* Dropdown */}
|
||||||
|
<CustomAutocomplete
|
||||||
|
size='small'
|
||||||
|
options={[{ label: '1-10003 Gi...', value: '1-10003' }]}
|
||||||
|
getOptionLabel={option => (typeof option === 'string' ? option : option.label)}
|
||||||
|
value={{ label: '1-10003 Gi...', value: '1-10003' }}
|
||||||
|
onChange={(_, newValue) => {
|
||||||
|
// Handle change if needed
|
||||||
|
}}
|
||||||
|
renderInput={params => <CustomTextField {...params} size='small' />}
|
||||||
|
sx={{ minWidth: 120 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Amount input */}
|
||||||
|
<CustomTextField
|
||||||
|
size='small'
|
||||||
|
placeholder='0'
|
||||||
|
value={formData.downPayment || '0'}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleInputChange('downPayment', e.target.value)
|
||||||
|
}
|
||||||
|
sx={{ width: '80px' }}
|
||||||
|
inputProps={{
|
||||||
|
style: { textAlign: 'center' }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Percentage/Fixed toggle */}
|
||||||
|
<ToggleButtonGroup
|
||||||
|
value={formData.downPaymentType || 'fixed'}
|
||||||
|
exclusive
|
||||||
|
onChange={(_, newValue) => {
|
||||||
|
if (newValue) handleInputChange('downPaymentType', newValue)
|
||||||
|
}}
|
||||||
|
size='small'
|
||||||
|
>
|
||||||
|
<ToggleButton value='percentage' sx={{ px: 1.5 }}>
|
||||||
|
%
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value='fixed' sx={{ px: 1.5 }}>
|
||||||
|
Rp
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Right side text */}
|
||||||
|
<Typography
|
||||||
|
variant='body1'
|
||||||
|
sx={{
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: 400
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Uang muka {downPayment > 0 ? downPayment.toLocaleString('id-ID') : '0'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Sisa Tagihan */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
py: 2,
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
borderRadius: '4px',
|
||||||
|
mb: 3,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#eeeeee'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant='body1' color='text.primary' sx={{ fontSize: '16px', fontWeight: 600 }}>
|
||||||
|
Sisa Tagihan
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body1' fontWeight={600} sx={{ fontSize: '16px', textAlign: 'right' }}>
|
||||||
|
{new Intl.NumberFormat('id-ID', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'IDR',
|
||||||
|
minimumFractionDigits: 0
|
||||||
|
}).format(remainingBalance)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
<Button
|
||||||
|
variant='contained'
|
||||||
|
color='primary'
|
||||||
|
fullWidth
|
||||||
|
sx={{
|
||||||
|
textTransform: 'none',
|
||||||
|
fontWeight: 600,
|
||||||
|
py: 1.5,
|
||||||
|
boxShadow: 'none',
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PurchaseSummary
|
||||||
@ -1,118 +1,929 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
import React, { useState, useMemo } from 'react'
|
||||||
import { Card, CardContent } from '@mui/material'
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Button,
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
IconButton,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Paper,
|
||||||
|
CircularProgress,
|
||||||
|
Alert,
|
||||||
|
Popover,
|
||||||
|
Divider
|
||||||
|
} from '@mui/material'
|
||||||
import Grid from '@mui/material/Grid2'
|
import Grid from '@mui/material/Grid2'
|
||||||
import { IngredientItem, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes'
|
import CustomAutocomplete from '@/@core/components/mui/Autocomplete'
|
||||||
import PurchaseBasicInfo from './PurchaseBasicInfo'
|
import CustomTextField from '@/@core/components/mui/TextField'
|
||||||
import PurchaseIngredientsTable from './PurchaseIngredientsTable'
|
import ImageUpload from '@/components/ImageUpload'
|
||||||
import PurchaseSummary from './PurchaseSummary'
|
import { DropdownOption } from '@/types/apps/purchaseOrderTypes'
|
||||||
|
import { useVendorActive } from '@/services/queries/vendor'
|
||||||
|
import { useIngredients } from '@/services/queries/ingredients'
|
||||||
|
import { useUnits } from '@/services/queries/units'
|
||||||
|
import { useFilesMutation } from '@/services/mutations/files'
|
||||||
|
import { usePurchaseOrdersMutation } from '@/services/mutations/purchaseOrder'
|
||||||
|
import { PurchaseOrderFormData, PurchaseOrderFormItem, PurchaseOrderRequest } from '@/types/services/purchaseOrder'
|
||||||
|
import { IngredientItem } from '@/types/services/ingredient'
|
||||||
|
|
||||||
|
export type Unit = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ValidationErrors {
|
||||||
|
vendor?: string
|
||||||
|
po_number?: string
|
||||||
|
transaction_date?: string
|
||||||
|
due_date?: string
|
||||||
|
items?: string
|
||||||
|
general?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PopoverState {
|
||||||
|
isOpen: boolean
|
||||||
|
anchorEl: HTMLElement | null
|
||||||
|
itemIndex: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Komponen PricePopover
|
||||||
|
const PricePopover: React.FC<{
|
||||||
|
anchorEl: HTMLElement | null
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
ingredientData: any
|
||||||
|
}> = ({ anchorEl, open, onClose, ingredientData }) => {
|
||||||
|
if (!ingredientData) return null
|
||||||
|
|
||||||
|
const lastPrice = ingredientData.originalData?.cost || 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={onClose}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'left'
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'left'
|
||||||
|
}}
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
minWidth: 300,
|
||||||
|
maxWidth: 350,
|
||||||
|
boxShadow: '0 8px 32px rgba(0,0,0,0.12)',
|
||||||
|
borderRadius: 2
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||||
|
<Typography variant='body2' color='text.secondary'>
|
||||||
|
Harga beli terakhir
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='h6' color='primary' fontWeight={600}>
|
||||||
|
{new Intl.NumberFormat('id-ID').format(lastPrice)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ mb: 2 }} />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant='text'
|
||||||
|
size='small'
|
||||||
|
sx={{
|
||||||
|
color: 'primary.main',
|
||||||
|
textTransform: 'none',
|
||||||
|
p: 0,
|
||||||
|
minWidth: 'auto'
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
console.log('Navigate to purchase history')
|
||||||
|
onClose()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Riwayat harga beli
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const PurchaseAddForm: React.FC = () => {
|
const PurchaseAddForm: React.FC = () => {
|
||||||
|
const [imageUrl, setImageUrl] = useState<string>('')
|
||||||
|
const [errors, setErrors] = useState<ValidationErrors>({})
|
||||||
|
const [popoverState, setPopoverState] = useState<PopoverState>({
|
||||||
|
isOpen: false,
|
||||||
|
anchorEl: null,
|
||||||
|
itemIndex: null
|
||||||
|
})
|
||||||
const [formData, setFormData] = useState<PurchaseOrderFormData>({
|
const [formData, setFormData] = useState<PurchaseOrderFormData>({
|
||||||
vendor: null,
|
vendor: null,
|
||||||
nomor: 'PO/00043',
|
po_number: '',
|
||||||
tglTransaksi: '2025-09-09',
|
transaction_date: '',
|
||||||
tglJatuhTempo: '2025-09-10',
|
due_date: '',
|
||||||
referensi: '',
|
reference: '',
|
||||||
termin: null,
|
status: 'sent',
|
||||||
hargaTermasukPajak: true,
|
|
||||||
// Shipping info
|
|
||||||
showShippingInfo: false,
|
|
||||||
tanggalPengiriman: '',
|
|
||||||
ekspedisi: null,
|
|
||||||
noResi: '',
|
|
||||||
// Bottom section toggles
|
|
||||||
showPesan: false,
|
showPesan: false,
|
||||||
showAttachment: false,
|
showAttachment: false,
|
||||||
showTambahDiskon: false,
|
message: '',
|
||||||
showBiayaPengiriman: false,
|
items: [
|
||||||
showBiayaTransaksi: false,
|
|
||||||
showUangMuka: false,
|
|
||||||
pesan: '',
|
|
||||||
// Ingredient items (updated from productItems)
|
|
||||||
ingredientItems: [
|
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
ingredient: null,
|
ingredient: null,
|
||||||
deskripsi: '',
|
description: '',
|
||||||
kuantitas: 1,
|
quantity: 1,
|
||||||
satuan: null,
|
unit: null,
|
||||||
discount: '0',
|
amount: 0,
|
||||||
harga: 0,
|
|
||||||
pajak: null,
|
|
||||||
waste: null,
|
|
||||||
total: 0
|
total: 0
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
attachment_file_ids: []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// API Hooks
|
||||||
|
const { data: vendors, isLoading: isLoadingVendors } = useVendorActive()
|
||||||
|
const { data: ingredients, isLoading: isLoadingIngredients } = useIngredients()
|
||||||
|
const { data: units, isLoading: isLoadingUnits } = useUnits({
|
||||||
|
page: 1,
|
||||||
|
limit: 50
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutate, isPending } = useFilesMutation().uploadFile
|
||||||
|
const { createPurchaseOrder } = usePurchaseOrdersMutation()
|
||||||
|
|
||||||
|
// Transform vendors data to dropdown options
|
||||||
|
const vendorOptions: DropdownOption[] = useMemo(() => {
|
||||||
|
return (
|
||||||
|
vendors?.map(vendor => ({
|
||||||
|
label: vendor.name,
|
||||||
|
value: vendor.id
|
||||||
|
})) || []
|
||||||
|
)
|
||||||
|
}, [vendors])
|
||||||
|
|
||||||
|
// Transform ingredients data to autocomplete options format
|
||||||
|
const ingredientOptions = useMemo(() => {
|
||||||
|
if (!ingredients || isLoadingIngredients) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return ingredients?.data.map((ingredient: IngredientItem) => ({
|
||||||
|
label: ingredient.name,
|
||||||
|
value: ingredient.id,
|
||||||
|
id: ingredient.id,
|
||||||
|
originalData: ingredient
|
||||||
|
}))
|
||||||
|
}, [ingredients, isLoadingIngredients])
|
||||||
|
|
||||||
|
// Transform units data to dropdown options
|
||||||
|
const unitOptions = useMemo(() => {
|
||||||
|
if (!units || isLoadingUnits) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
units?.data?.map((unit: any) => ({
|
||||||
|
label: unit.name || unit.nama || unit.unit_name,
|
||||||
|
value: unit.id || unit.code || unit.value
|
||||||
|
})) || []
|
||||||
|
)
|
||||||
|
}, [units, isLoadingUnits])
|
||||||
|
|
||||||
|
// Handle price field click untuk menampilkan popover
|
||||||
|
const handlePriceFieldClick = (event: React.MouseEvent<HTMLElement>, itemIndex: number) => {
|
||||||
|
const item = formData.items[itemIndex]
|
||||||
|
if (item.ingredient) {
|
||||||
|
setPopoverState({
|
||||||
|
isOpen: true,
|
||||||
|
anchorEl: event.currentTarget,
|
||||||
|
itemIndex: itemIndex
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close popover
|
||||||
|
const handleClosePopover = () => {
|
||||||
|
setPopoverState({
|
||||||
|
isOpen: false,
|
||||||
|
anchorEl: null,
|
||||||
|
itemIndex: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fungsi validasi
|
||||||
|
const validateForm = (): boolean => {
|
||||||
|
const newErrors: ValidationErrors = {}
|
||||||
|
|
||||||
|
if (!formData.vendor || !formData.vendor.value) {
|
||||||
|
newErrors.vendor = 'Vendor wajib dipilih'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.po_number.trim()) {
|
||||||
|
newErrors.po_number = 'Nomor PO wajib diisi'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.transaction_date) {
|
||||||
|
newErrors.transaction_date = 'Tanggal transaksi wajib diisi'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.due_date) {
|
||||||
|
newErrors.due_date = 'Tanggal jatuh tempo wajib diisi'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.transaction_date && formData.due_date) {
|
||||||
|
if (new Date(formData.due_date) < new Date(formData.transaction_date)) {
|
||||||
|
newErrors.due_date = 'Tanggal jatuh tempo tidak boleh sebelum tanggal transaksi'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validItems = formData.items.filter(
|
||||||
|
item => item.ingredient && item.unit && item.quantity > 0 && item.amount > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if (validItems.length === 0) {
|
||||||
|
newErrors.items = 'Minimal harus ada 1 item yang valid dengan bahan, satuan, kuantitas dan harga yang terisi'
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors(newErrors)
|
||||||
|
return Object.keys(newErrors).length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler Functions
|
||||||
const handleInputChange = (field: keyof PurchaseOrderFormData, value: any): void => {
|
const handleInputChange = (field: keyof PurchaseOrderFormData, value: any): void => {
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[field]: value
|
[field]: value
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
if (errors[field as keyof ValidationErrors]) {
|
||||||
|
setErrors(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: undefined
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleIngredientChange = (index: number, field: keyof IngredientItem, value: any): void => {
|
const handleItemChange = (index: number, field: keyof PurchaseOrderFormItem, value: any): void => {
|
||||||
setFormData(prev => {
|
setFormData(prev => {
|
||||||
const newItems = [...prev.ingredientItems]
|
const newItems = [...prev.items]
|
||||||
newItems[index] = { ...newItems[index], [field]: value }
|
newItems[index] = { ...newItems[index], [field]: value }
|
||||||
|
|
||||||
// Auto-calculate total if price or quantity changes
|
if (field === 'amount' || field === 'quantity') {
|
||||||
if (field === 'harga' || field === 'kuantitas') {
|
|
||||||
const item = newItems[index]
|
const item = newItems[index]
|
||||||
item.total = item.harga * item.kuantitas
|
item.total = item.amount * item.quantity
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...prev, ingredientItems: newItems }
|
return { ...prev, items: newItems }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (errors.items) {
|
||||||
|
setErrors(prev => ({
|
||||||
|
...prev,
|
||||||
|
items: undefined
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addIngredientItem = (): void => {
|
const handleIngredientSelection = (index: number, selectedIngredient: any) => {
|
||||||
const newItem: IngredientItem = {
|
handleItemChange(index, 'ingredient', selectedIngredient)
|
||||||
|
|
||||||
|
if (selectedIngredient) {
|
||||||
|
const ingredientData: IngredientItem = selectedIngredient.originalData || selectedIngredient
|
||||||
|
|
||||||
|
if (ingredientData.unit_id || ingredientData.unit) {
|
||||||
|
let unitToFind = null
|
||||||
|
|
||||||
|
if (ingredientData.unit && typeof ingredientData.unit === 'object') {
|
||||||
|
unitToFind = ingredientData.unit
|
||||||
|
} else if (ingredientData.unit_id) {
|
||||||
|
unitToFind = unitOptions.find(option => option.value === ingredientData.unit_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unitToFind) {
|
||||||
|
const unitOption = {
|
||||||
|
label: (unitToFind as any).label || (unitToFind as any).name || (unitToFind as any).unit_name,
|
||||||
|
value: (unitToFind as any).value || ingredientData.unit_id
|
||||||
|
}
|
||||||
|
|
||||||
|
handleItemChange(index, 'unit', unitOption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ingredientData.cost !== undefined && ingredientData.cost !== null) {
|
||||||
|
handleItemChange(index, 'amount', ingredientData.cost)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ingredientData.name) {
|
||||||
|
handleItemChange(index, 'description', ingredientData.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addItem = (): void => {
|
||||||
|
const newItem: PurchaseOrderFormItem = {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
ingredient: null,
|
ingredient: null,
|
||||||
deskripsi: '',
|
description: '',
|
||||||
kuantitas: 1,
|
quantity: 1,
|
||||||
satuan: null,
|
unit: null,
|
||||||
discount: '0%',
|
amount: 0,
|
||||||
harga: 0,
|
|
||||||
pajak: null,
|
|
||||||
waste: null,
|
|
||||||
total: 0
|
total: 0
|
||||||
}
|
}
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
ingredientItems: [...prev.ingredientItems, newItem]
|
items: [...prev.items, newItem]
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeIngredientItem = (index: number): void => {
|
const removeItem = (index: number): void => {
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
ingredientItems: prev.ingredientItems.filter((_, i) => i !== index)
|
items: prev.items.filter((_, i) => i !== index)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getSelectedVendorData = () => {
|
||||||
|
if (!formData.vendor?.value || !vendors) return null
|
||||||
|
const selectedVendor = vendors.find(vendor => vendor.id === (formData?.vendor?.value ?? ''))
|
||||||
|
return selectedVendor
|
||||||
|
}
|
||||||
|
|
||||||
|
const upsertAttachment = (attachments: string[], newId: string, index = 0) => {
|
||||||
|
if (attachments.length === 0) {
|
||||||
|
return [newId]
|
||||||
|
}
|
||||||
|
return attachments.map((id, i) => (i === index ? newId : id))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpload = async (file: File): Promise<string> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
formData.append('file_type', 'image')
|
||||||
|
formData.append('description', 'Gambar Purchase Order')
|
||||||
|
|
||||||
|
mutate(formData, {
|
||||||
|
onSuccess: data => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
attachment_file_ids: upsertAttachment(prev.attachment_file_ids, data.id)
|
||||||
|
}))
|
||||||
|
setImageUrl(data.file_url)
|
||||||
|
resolve(data.id)
|
||||||
|
},
|
||||||
|
onError: error => {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const subtotal = formData.items.reduce((sum, item) => sum + (item.total || 0), 0)
|
||||||
|
|
||||||
|
const convertToApiRequest = (): PurchaseOrderRequest => {
|
||||||
|
return {
|
||||||
|
vendor_id: formData.vendor?.value || '',
|
||||||
|
po_number: formData.po_number,
|
||||||
|
transaction_date: formData.transaction_date,
|
||||||
|
due_date: formData.due_date,
|
||||||
|
reference: formData.reference || undefined,
|
||||||
|
status: formData.status,
|
||||||
|
message: formData.message || undefined,
|
||||||
|
items: formData.items
|
||||||
|
.filter(item => item.ingredient && item.unit)
|
||||||
|
.map(item => ({
|
||||||
|
ingredient_id: item.ingredient!.value,
|
||||||
|
description: item.description || undefined,
|
||||||
|
quantity: item.quantity,
|
||||||
|
unit_id: item.unit!.value,
|
||||||
|
amount: item.amount
|
||||||
|
})),
|
||||||
|
attachment_file_ids: formData.attachment_file_ids.length > 0 ? formData.attachment_file_ids : undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!validateForm()) {
|
||||||
|
setErrors(prev => ({
|
||||||
|
...prev,
|
||||||
|
general: 'Mohon lengkapi semua field yang wajib diisi'
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
createPurchaseOrder.mutate(convertToApiRequest(), {
|
||||||
|
onSuccess: () => {
|
||||||
|
window.history.back()
|
||||||
|
},
|
||||||
|
onError: error => {
|
||||||
|
setErrors(prev => ({
|
||||||
|
...prev,
|
||||||
|
general: 'Terjadi kesalahan saat menyimpan data. Silakan coba lagi.'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current ingredient data for popover
|
||||||
|
const getCurrentIngredientData = () => {
|
||||||
|
if (popoverState.itemIndex !== null) {
|
||||||
|
return formData.items[popoverState.itemIndex]?.ingredient
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
{errors.general && (
|
||||||
|
<Alert severity='error' sx={{ mb: 3 }}>
|
||||||
|
{errors.general}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
{/* Basic Info Section */}
|
{/* BASIC INFO SECTION */}
|
||||||
<PurchaseBasicInfo formData={formData} handleInputChange={handleInputChange} />
|
<Grid size={{ xs: 12, sm: 6, md: 6 }}>
|
||||||
|
<CustomAutocomplete
|
||||||
{/* Ingredients Table Section */}
|
fullWidth
|
||||||
<PurchaseIngredientsTable
|
options={vendorOptions}
|
||||||
formData={formData}
|
value={formData.vendor}
|
||||||
handleIngredientChange={handleIngredientChange}
|
onChange={(event, newValue) => {
|
||||||
addIngredientItem={addIngredientItem}
|
handleInputChange('vendor', newValue)
|
||||||
removeIngredientItem={removeIngredientItem}
|
if (newValue?.value) {
|
||||||
|
const selectedVendorData = vendors?.find(vendor => vendor.id === newValue.value)
|
||||||
|
console.log('Vendor terpilih:', selectedVendorData)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
loading={isLoadingVendors}
|
||||||
|
renderInput={params => (
|
||||||
|
<CustomTextField
|
||||||
|
{...params}
|
||||||
|
label='Vendor *'
|
||||||
|
placeholder={isLoadingVendors ? 'Memuat vendor...' : 'Pilih vendor'}
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.vendor}
|
||||||
|
helperText={errors.vendor}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{/* Summary Section */}
|
/>
|
||||||
<PurchaseSummary formData={formData} handleInputChange={handleInputChange} />
|
{getSelectedVendorData() && (
|
||||||
|
<Box className='space-y-1 mt-3'>
|
||||||
|
<Box className='flex items-center space-x-2'>
|
||||||
|
<i className='tabler-user text-gray-500 w-3 h-3' />
|
||||||
|
<Typography className='text-gray-700 font-medium text-xs'>
|
||||||
|
{getSelectedVendorData()?.contact_person ?? ''}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box className='flex items-start space-x-2'>
|
||||||
|
<i className='tabler-map text-gray-500 w-3 h-3' />
|
||||||
|
<Typography className='text-gray-700 font-medium text-xs'>
|
||||||
|
{getSelectedVendorData()?.address ?? '-'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box className='flex items-center space-x-2'>
|
||||||
|
<i className='tabler-phone text-gray-500 w-3 h-3' />
|
||||||
|
<Typography className='text-gray-700 font-medium text-xs'>
|
||||||
|
{getSelectedVendorData()?.phone_number ?? '-'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 6, md: 6 }}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
label='Nomor PO *'
|
||||||
|
value={formData.po_number}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('po_number', e.target.value)}
|
||||||
|
error={!!errors.po_number}
|
||||||
|
helperText={errors.po_number}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Row 2 - Transaction Date, Due Date, Status */}
|
||||||
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
label='Tanggal Transaksi *'
|
||||||
|
type='date'
|
||||||
|
value={formData.transaction_date}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleInputChange('transaction_date', e.target.value)
|
||||||
|
}
|
||||||
|
InputLabelProps={{
|
||||||
|
shrink: true
|
||||||
|
}}
|
||||||
|
error={!!errors.transaction_date}
|
||||||
|
helperText={errors.transaction_date}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
label='Tanggal Jatuh Tempo *'
|
||||||
|
type='date'
|
||||||
|
value={formData.due_date}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('due_date', e.target.value)}
|
||||||
|
InputLabelProps={{
|
||||||
|
shrink: true
|
||||||
|
}}
|
||||||
|
error={!!errors.due_date}
|
||||||
|
helperText={errors.due_date}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
label='Referensi'
|
||||||
|
placeholder='Referensi'
|
||||||
|
value={formData.reference}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('reference', e.target.value)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* ITEMS TABLE SECTION */}
|
||||||
|
<Grid size={{ xs: 12 }} sx={{ mt: 4 }}>
|
||||||
|
<Typography variant='h6' sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
|
Item Purchase Order
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{errors.items && (
|
||||||
|
<Alert severity='error' sx={{ mb: 2 }}>
|
||||||
|
{errors.items}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TableContainer component={Paper} variant='outlined'>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow sx={{ backgroundColor: 'grey.50' }}>
|
||||||
|
<TableCell sx={{ fontWeight: 'bold', minWidth: 180 }}>Bahan</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 'bold', minWidth: 150 }}>Deskripsi</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 'bold', width: 100 }}>Kuantitas</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 'bold', width: 120 }}>Satuan</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 'bold', width: 120 }}>Harga</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 'bold', width: 100, textAlign: 'right' }}>Total</TableCell>
|
||||||
|
<TableCell sx={{ width: 50 }}></TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{formData.items.map((item: PurchaseOrderFormItem, index: number) => (
|
||||||
|
<TableRow key={item.id}>
|
||||||
|
<TableCell>
|
||||||
|
<CustomAutocomplete
|
||||||
|
size='small'
|
||||||
|
options={ingredientOptions}
|
||||||
|
value={item.ingredient || null}
|
||||||
|
onChange={(event, newValue) => handleIngredientSelection(index, newValue)}
|
||||||
|
loading={isLoadingIngredients}
|
||||||
|
getOptionLabel={(option: any) => {
|
||||||
|
if (!option) return ''
|
||||||
|
return option.label || option.name || option.nama || ''
|
||||||
|
}}
|
||||||
|
isOptionEqualToValue={(option: any, value: any) => {
|
||||||
|
if (!option || !value) return false
|
||||||
|
const optionId = option.value || option.id
|
||||||
|
const valueId = value.value || value.id
|
||||||
|
return optionId === valueId
|
||||||
|
}}
|
||||||
|
renderInput={params => (
|
||||||
|
<CustomTextField
|
||||||
|
{...params}
|
||||||
|
placeholder={isLoadingIngredients ? 'Memuat bahan...' : 'Pilih Bahan'}
|
||||||
|
InputProps={{
|
||||||
|
...params.InputProps,
|
||||||
|
endAdornment: (
|
||||||
|
<>
|
||||||
|
{isLoadingIngredients ? <CircularProgress color='inherit' size={20} /> : null}
|
||||||
|
{params.InputProps.endAdornment}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
disabled={isLoadingIngredients}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
size='small'
|
||||||
|
value={item.description}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleItemChange(index, 'description', e.target.value)
|
||||||
|
}
|
||||||
|
placeholder='Deskripsi'
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
size='small'
|
||||||
|
type='number'
|
||||||
|
value={item.quantity}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleItemChange(index, 'quantity', parseInt(e.target.value) || 1)
|
||||||
|
}
|
||||||
|
inputProps={{ min: 1 }}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<CustomAutocomplete
|
||||||
|
size='small'
|
||||||
|
options={unitOptions}
|
||||||
|
value={item.unit}
|
||||||
|
onChange={(event, newValue) => handleItemChange(index, 'unit', newValue)}
|
||||||
|
loading={isLoadingUnits}
|
||||||
|
getOptionLabel={(option: any) => {
|
||||||
|
if (!option) return ''
|
||||||
|
return option.label || option.name || option.nama || ''
|
||||||
|
}}
|
||||||
|
renderInput={params => (
|
||||||
|
<CustomTextField
|
||||||
|
{...params}
|
||||||
|
placeholder={isLoadingUnits ? 'Memuat satuan...' : 'Pilih satuan...'}
|
||||||
|
InputProps={{
|
||||||
|
...params.InputProps,
|
||||||
|
endAdornment: (
|
||||||
|
<>
|
||||||
|
{isLoadingUnits ? <CircularProgress color='inherit' size={20} /> : null}
|
||||||
|
{params.InputProps.endAdornment}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
disabled={isLoadingUnits}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
size='small'
|
||||||
|
type='number'
|
||||||
|
value={item.amount === 0 ? '' : item.amount?.toString() || ''}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value
|
||||||
|
if (value === '') {
|
||||||
|
handleItemChange(index, 'amount', 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const numericValue = parseFloat(value)
|
||||||
|
handleItemChange(index, 'amount', isNaN(numericValue) ? 0 : numericValue)
|
||||||
|
}}
|
||||||
|
onClick={e => handlePriceFieldClick(e, index)}
|
||||||
|
inputProps={{ min: 0, step: 'any' }}
|
||||||
|
placeholder='0'
|
||||||
|
sx={{ cursor: item.ingredient ? 'pointer' : 'text' }}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
size='small'
|
||||||
|
value={item.total}
|
||||||
|
InputProps={{ readOnly: true }}
|
||||||
|
sx={{
|
||||||
|
'& .MuiInputBase-input': {
|
||||||
|
textAlign: 'right'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<IconButton
|
||||||
|
size='small'
|
||||||
|
color='error'
|
||||||
|
onClick={() => removeItem(index)}
|
||||||
|
disabled={formData.items.length === 1}
|
||||||
|
>
|
||||||
|
<i className='tabler-trash' />
|
||||||
|
</IconButton>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
|
||||||
|
{/* Add New Item Button */}
|
||||||
|
<Button
|
||||||
|
startIcon={<i className='tabler-plus' />}
|
||||||
|
onClick={addItem}
|
||||||
|
variant='outlined'
|
||||||
|
size='small'
|
||||||
|
sx={{ mt: 1 }}
|
||||||
|
disabled={isLoadingIngredients || isLoadingUnits}
|
||||||
|
>
|
||||||
|
Tambah Item
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* SUMMARY SECTION */}
|
||||||
|
<Grid size={12} sx={{ mt: 4 }}>
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
{/* Left Side - Message and Attachment */}
|
||||||
|
<Grid size={{ xs: 12, md: 7 }}>
|
||||||
|
{/* Message Section */}
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<Button
|
||||||
|
variant='text'
|
||||||
|
color='inherit'
|
||||||
|
onClick={() => handleInputChange('showPesan', !formData.showPesan)}
|
||||||
|
sx={{
|
||||||
|
textTransform: 'none',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 500,
|
||||||
|
padding: '12px 16px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
width: '100%',
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
border: '1px solid #e0e0e0',
|
||||||
|
borderRadius: '4px',
|
||||||
|
color: 'text.primary',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#eeeeee'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box component='span' sx={{ mr: 1 }}>
|
||||||
|
{formData.showPesan ? (
|
||||||
|
<i className='tabler-chevron-down w-4 h-4' />
|
||||||
|
) : (
|
||||||
|
<i className='tabler-chevron-right w-4 h-4' />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
Pesan
|
||||||
|
</Button>
|
||||||
|
{formData.showPesan && (
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={3}
|
||||||
|
placeholder='Tambahkan pesan...'
|
||||||
|
value={formData.message || ''}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleInputChange('message', e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Attachment Section */}
|
||||||
|
<Box>
|
||||||
|
<Button
|
||||||
|
variant='text'
|
||||||
|
color='inherit'
|
||||||
|
onClick={() => handleInputChange('showAttachment', !formData.showAttachment)}
|
||||||
|
sx={{
|
||||||
|
textTransform: 'none',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 500,
|
||||||
|
padding: '12px 16px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
width: '100%',
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
border: '1px solid #e0e0e0',
|
||||||
|
borderRadius: '4px',
|
||||||
|
color: 'text.primary',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#eeeeee'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box component='span' sx={{ mr: 1 }}>
|
||||||
|
{formData.showAttachment ? (
|
||||||
|
<i className='tabler-chevron-down w-4 h-4' />
|
||||||
|
) : (
|
||||||
|
<i className='tabler-chevron-right w-4 h-4' />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
Lampiran
|
||||||
|
</Button>
|
||||||
|
{formData.showAttachment && (
|
||||||
|
<ImageUpload
|
||||||
|
onUpload={handleUpload}
|
||||||
|
maxFileSize={1 * 1024 * 1024}
|
||||||
|
showUrlOption={false}
|
||||||
|
currentImageUrl={imageUrl}
|
||||||
|
dragDropText='Letakkan gambar Anda di sini'
|
||||||
|
browseButtonText='Pilih Gambar'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Right Side - Totals */}
|
||||||
|
<Grid size={{ xs: 12, md: 5 }}>
|
||||||
|
<Box sx={{ backgroundColor: '#ffffff', p: 3, borderRadius: '8px' }}>
|
||||||
|
{/* Sub Total */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
py: 2,
|
||||||
|
borderBottom: '1px solid #e0e0e0',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#f8f8f8'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant='body1' color='text.secondary' sx={{ fontSize: '16px' }}>
|
||||||
|
Sub Total
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body1' fontWeight={600} sx={{ fontSize: '16px', textAlign: 'right' }}>
|
||||||
|
{new Intl.NumberFormat('id-ID', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'IDR',
|
||||||
|
minimumFractionDigits: 0
|
||||||
|
}).format(subtotal)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Total */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
py: 2,
|
||||||
|
borderBottom: '1px solid #e0e0e0',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#f8f8f8'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant='h6' fontWeight={600} sx={{ fontSize: '18px' }}>
|
||||||
|
Total
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='h6' fontWeight={600} sx={{ fontSize: '18px', textAlign: 'right' }}>
|
||||||
|
{new Intl.NumberFormat('id-ID', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'IDR',
|
||||||
|
minimumFractionDigits: 0
|
||||||
|
}).format(subtotal)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
<Button
|
||||||
|
variant='contained'
|
||||||
|
color='primary'
|
||||||
|
fullWidth
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={createPurchaseOrder.isPending}
|
||||||
|
sx={{
|
||||||
|
textTransform: 'none',
|
||||||
|
fontWeight: 600,
|
||||||
|
py: 1.5,
|
||||||
|
mt: 3,
|
||||||
|
boxShadow: 'none',
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{createPurchaseOrder.isPending ? (
|
||||||
|
<>
|
||||||
|
<CircularProgress size={16} sx={{ mr: 1 }} />
|
||||||
|
Menyimpan...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Simpan'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Price Popover */}
|
||||||
|
<PricePopover
|
||||||
|
anchorEl={popoverState.anchorEl}
|
||||||
|
open={popoverState.isOpen}
|
||||||
|
onClose={handleClosePopover}
|
||||||
|
ingredientData={getCurrentIngredientData()}
|
||||||
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Button, Switch, FormControlLabel } from '@mui/material'
|
import { Button, Switch, FormControlLabel, Box, Typography } from '@mui/material'
|
||||||
import Grid from '@mui/material/Grid2'
|
import Grid from '@mui/material/Grid2'
|
||||||
import CustomAutocomplete from '@/@core/components/mui/Autocomplete'
|
import CustomAutocomplete from '@/@core/components/mui/Autocomplete'
|
||||||
import CustomTextField from '@/@core/components/mui/TextField'
|
import CustomTextField from '@/@core/components/mui/TextField'
|
||||||
import { DropdownOption, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes'
|
import { DropdownOption, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes'
|
||||||
|
import { useVendorActive } from '@/services/queries/vendor'
|
||||||
|
|
||||||
interface PurchaseBasicInfoProps {
|
interface PurchaseBasicInfoProps {
|
||||||
formData: PurchaseOrderFormData
|
formData: PurchaseOrderFormData
|
||||||
@ -13,12 +14,22 @@ interface PurchaseBasicInfoProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PurchaseBasicInfo: React.FC<PurchaseBasicInfoProps> = ({ formData, handleInputChange }) => {
|
const PurchaseBasicInfo: React.FC<PurchaseBasicInfoProps> = ({ formData, handleInputChange }) => {
|
||||||
// Sample data for dropdowns
|
const { data: vendors, isLoading } = useVendorActive()
|
||||||
const vendorOptions: DropdownOption[] = [
|
|
||||||
{ label: 'Vendor A', value: 'vendor_a' },
|
// Transform vendors data to dropdown options
|
||||||
{ label: 'Vendor B', value: 'vendor_b' },
|
const vendorOptions: DropdownOption[] =
|
||||||
{ label: 'Vendor C', value: 'vendor_c' }
|
vendors?.map(vendor => ({
|
||||||
]
|
label: vendor.name,
|
||||||
|
value: vendor.id
|
||||||
|
})) || []
|
||||||
|
|
||||||
|
// Function to get selected vendor data
|
||||||
|
const getSelectedVendorData = () => {
|
||||||
|
if (!formData.vendor?.value || !vendors) return null
|
||||||
|
|
||||||
|
const selectedVendor = vendors.find(vendor => vendor.id === (formData?.vendor?.value ?? ''))
|
||||||
|
return selectedVendor
|
||||||
|
}
|
||||||
|
|
||||||
const terminOptions: DropdownOption[] = [
|
const terminOptions: DropdownOption[] = [
|
||||||
{ label: 'Net 30', value: 'net_30' },
|
{ label: 'Net 30', value: 'net_30' },
|
||||||
@ -43,9 +54,53 @@ const PurchaseBasicInfo: React.FC<PurchaseBasicInfoProps> = ({ formData, handleI
|
|||||||
fullWidth
|
fullWidth
|
||||||
options={vendorOptions}
|
options={vendorOptions}
|
||||||
value={formData.vendor}
|
value={formData.vendor}
|
||||||
onChange={(event, newValue) => handleInputChange('vendor', newValue)}
|
onChange={(event, newValue) => {
|
||||||
renderInput={params => <CustomTextField {...params} label='Vendor' placeholder='Pilih kontak' fullWidth />}
|
handleInputChange('vendor', newValue)
|
||||||
|
|
||||||
|
// Optional: Bisa langsung akses full data vendor saat berubah
|
||||||
|
if (newValue?.value) {
|
||||||
|
const selectedVendorData = vendors?.find(vendor => vendor.id === newValue.value)
|
||||||
|
console.log('Vendor selected:', selectedVendorData)
|
||||||
|
// Atau bisa trigger callback lain jika dibutuhkan
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
loading={isLoading}
|
||||||
|
renderInput={params => (
|
||||||
|
<CustomTextField
|
||||||
|
{...params}
|
||||||
|
label='Vendor'
|
||||||
|
placeholder={isLoading ? 'Loading vendors...' : 'Pilih kontak'}
|
||||||
|
fullWidth
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{getSelectedVendorData() && (
|
||||||
|
<Box className='space-y-1 mt-3'>
|
||||||
|
{/* Nama Perum */}
|
||||||
|
<Box className='flex items-center space-x-2'>
|
||||||
|
<i className='tabler-user text-gray-500 w-3 h-3' />
|
||||||
|
<Typography className='text-gray-700 font-medium text-xs'>
|
||||||
|
{getSelectedVendorData()?.contact_person ?? ''}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Alamat */}
|
||||||
|
<Box className='flex items-start space-x-2'>
|
||||||
|
<i className='tabler-map text-gray-500 w-3 h-3' />
|
||||||
|
<Typography className='text-gray-700 font-medium text-xs'>
|
||||||
|
{getSelectedVendorData()?.address ?? '-'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Nomor Telepon */}
|
||||||
|
<Box className='flex items-center space-x-2'>
|
||||||
|
<i className='tabler-phone text-gray-500 w-3 h-3' />
|
||||||
|
<Typography className='text-gray-700 font-medium text-xs'>
|
||||||
|
{getSelectedVendorData()?.phone_number ?? '-'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 12, sm: 6, md: 6 }}>
|
<Grid size={{ xs: 12, sm: 6, md: 6 }}>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Typography,
|
Typography,
|
||||||
@ -11,12 +11,14 @@ import {
|
|||||||
TableContainer,
|
TableContainer,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableRow,
|
TableRow,
|
||||||
Paper
|
Paper,
|
||||||
|
CircularProgress
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import Grid from '@mui/material/Grid2'
|
import Grid from '@mui/material/Grid2'
|
||||||
import CustomAutocomplete from '@/@core/components/mui/Autocomplete'
|
import CustomAutocomplete from '@/@core/components/mui/Autocomplete'
|
||||||
import CustomTextField from '@/@core/components/mui/TextField'
|
import CustomTextField from '@/@core/components/mui/TextField'
|
||||||
import { IngredientItem, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes'
|
import { IngredientItem, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes'
|
||||||
|
import { useIngredients } from '@/services/queries/ingredients'
|
||||||
|
|
||||||
interface PurchaseIngredientsTableProps {
|
interface PurchaseIngredientsTableProps {
|
||||||
formData: PurchaseOrderFormData
|
formData: PurchaseOrderFormData
|
||||||
@ -31,14 +33,21 @@ const PurchaseIngredientsTable: React.FC<PurchaseIngredientsTableProps> = ({
|
|||||||
addIngredientItem,
|
addIngredientItem,
|
||||||
removeIngredientItem
|
removeIngredientItem
|
||||||
}) => {
|
}) => {
|
||||||
const ingredientOptions = [
|
const { data: ingredients, isLoading } = useIngredients()
|
||||||
{ label: 'Tepung Terigu Premium', value: 'tepung_terigu_premium' },
|
|
||||||
{ label: 'Gula Pasir Halus', value: 'gula_pasir_halus' },
|
// Transform ingredients data to autocomplete options format
|
||||||
{ label: 'Mentega Unsalted', value: 'mentega_unsalted' },
|
const ingredientOptions = useMemo(() => {
|
||||||
{ label: 'Telur Ayam Grade A', value: 'telur_ayam_grade_a' },
|
if (!ingredients || isLoading) {
|
||||||
{ label: 'Vanilla Extract', value: 'vanilla_extract' },
|
return []
|
||||||
{ label: 'Coklat Chips', value: 'coklat_chips' }
|
}
|
||||||
]
|
|
||||||
|
return ingredients?.data.map((ingredient: any) => ({
|
||||||
|
label: ingredient.name || ingredient.nama || ingredient.ingredient_name,
|
||||||
|
value: ingredient.id || ingredient.code || ingredient.value,
|
||||||
|
id: ingredient.id || ingredient.code || ingredient.value,
|
||||||
|
originalData: ingredient
|
||||||
|
}))
|
||||||
|
}, [ingredients, isLoading])
|
||||||
|
|
||||||
const satuanOptions = [
|
const satuanOptions = [
|
||||||
{ label: 'KG', value: 'kg' },
|
{ label: 'KG', value: 'kg' },
|
||||||
@ -63,6 +72,40 @@ const PurchaseIngredientsTable: React.FC<PurchaseIngredientsTableProps> = ({
|
|||||||
{ label: 'Custom', value: 'custom' }
|
{ label: 'Custom', value: 'custom' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Handle ingredient selection with additional data population
|
||||||
|
const handleIngredientSelection = (index: number, selectedIngredient: any) => {
|
||||||
|
handleIngredientChange(index, 'ingredient', selectedIngredient)
|
||||||
|
|
||||||
|
// Auto-populate related fields if available in the ingredient data
|
||||||
|
if (selectedIngredient) {
|
||||||
|
// Get ingredient data from originalData or directly from selectedIngredient
|
||||||
|
const ingredientData = selectedIngredient.originalData || selectedIngredient
|
||||||
|
|
||||||
|
// Auto-fill unit if available
|
||||||
|
if (ingredientData.unit || ingredientData.satuan) {
|
||||||
|
const unit = ingredientData.unit || ingredientData.satuan
|
||||||
|
// Convert unit to string and make it safe
|
||||||
|
const unitString = String(unit).toLowerCase()
|
||||||
|
const unitOption = satuanOptions.find(
|
||||||
|
option => option.value === unit || option.label.toLowerCase() === unitString
|
||||||
|
)
|
||||||
|
if (unitOption) {
|
||||||
|
handleIngredientChange(index, 'satuan', unitOption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-fill price if available
|
||||||
|
if (ingredientData.price || ingredientData.harga) {
|
||||||
|
handleIngredientChange(index, 'harga', ingredientData.price || ingredientData.harga)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-fill description if available
|
||||||
|
if (ingredientData.description || ingredientData.deskripsi) {
|
||||||
|
handleIngredientChange(index, 'deskripsi', ingredientData.description || ingredientData.deskripsi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid size={{ xs: 12 }} sx={{ mt: 4 }}>
|
<Grid size={{ xs: 12 }} sx={{ mt: 4 }}>
|
||||||
<Typography variant='h6' sx={{ mb: 2, fontWeight: 600 }}>
|
<Typography variant='h6' sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
@ -92,9 +135,36 @@ const PurchaseIngredientsTable: React.FC<PurchaseIngredientsTableProps> = ({
|
|||||||
<CustomAutocomplete
|
<CustomAutocomplete
|
||||||
size='small'
|
size='small'
|
||||||
options={ingredientOptions}
|
options={ingredientOptions}
|
||||||
value={item.ingredient}
|
value={item.ingredient || null}
|
||||||
onChange={(event, newValue) => handleIngredientChange(index, 'ingredient', newValue)}
|
onChange={(event, newValue) => handleIngredientSelection(index, newValue)}
|
||||||
renderInput={params => <CustomTextField {...params} placeholder='Pilih Bahan Baku' />}
|
loading={isLoading}
|
||||||
|
getOptionLabel={(option: any) => {
|
||||||
|
if (!option) return ''
|
||||||
|
return option.label || option.name || option.nama || ''
|
||||||
|
}}
|
||||||
|
isOptionEqualToValue={(option: any, value: any) => {
|
||||||
|
if (!option || !value) return false
|
||||||
|
// Handle different value structures
|
||||||
|
const optionId = option.value || option.id
|
||||||
|
const valueId = value.value || value.id
|
||||||
|
return optionId === valueId
|
||||||
|
}}
|
||||||
|
renderInput={params => (
|
||||||
|
<CustomTextField
|
||||||
|
{...params}
|
||||||
|
placeholder={isLoading ? 'Loading ingredients...' : 'Pilih Bahan Baku'}
|
||||||
|
InputProps={{
|
||||||
|
...params.InputProps,
|
||||||
|
endAdornment: (
|
||||||
|
<>
|
||||||
|
{isLoading ? <CircularProgress color='inherit' size={20} /> : null}
|
||||||
|
{params.InputProps.endAdornment}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@ -215,6 +285,7 @@ const PurchaseIngredientsTable: React.FC<PurchaseIngredientsTableProps> = ({
|
|||||||
variant='outlined'
|
variant='outlined'
|
||||||
size='small'
|
size='small'
|
||||||
sx={{ mt: 1 }}
|
sx={{ mt: 1 }}
|
||||||
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
Tambah bahan baku
|
Tambah bahan baku
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -42,6 +42,9 @@ import Loading from '@/components/layout/shared/Loading'
|
|||||||
import { PurchaseOrderType } from '@/types/apps/purchaseOrderTypes'
|
import { PurchaseOrderType } from '@/types/apps/purchaseOrderTypes'
|
||||||
import { purchaseOrdersData } from '@/data/dummy/purchase-order'
|
import { purchaseOrdersData } from '@/data/dummy/purchase-order'
|
||||||
import { getLocalizedUrl } from '@/utils/i18n'
|
import { getLocalizedUrl } from '@/utils/i18n'
|
||||||
|
import { PurchaseOrder } from '@/types/services/purchaseOrder'
|
||||||
|
import { usePurchaseOrders } from '@/services/queries/purchaseOrder'
|
||||||
|
import StatusFilterTabs from '@/components/StatusFilterTab'
|
||||||
|
|
||||||
declare module '@tanstack/table-core' {
|
declare module '@tanstack/table-core' {
|
||||||
interface FilterFns {
|
interface FilterFns {
|
||||||
@ -52,7 +55,7 @@ declare module '@tanstack/table-core' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseOrderTypeWithAction = PurchaseOrderType & {
|
type PurchaseOrderTypeWithAction = PurchaseOrder & {
|
||||||
actions?: string
|
actions?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,14 +107,16 @@ const DebouncedInput = ({
|
|||||||
// Status color mapping
|
// Status color mapping
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'Draft':
|
case 'draft':
|
||||||
return 'secondary'
|
return 'secondary'
|
||||||
case 'Disetujui':
|
case 'approved':
|
||||||
return 'primary'
|
return 'primary'
|
||||||
case 'Dikirim Sebagian':
|
case 'sent':
|
||||||
return 'warning'
|
return 'warning'
|
||||||
case 'Selesai':
|
case 'received':
|
||||||
return 'success'
|
return 'success'
|
||||||
|
case 'cancelled':
|
||||||
|
return 'error'
|
||||||
default:
|
default:
|
||||||
return 'default'
|
return 'default'
|
||||||
}
|
}
|
||||||
@ -135,46 +140,24 @@ const PurchaseOrderListTable = () => {
|
|||||||
// States
|
// States
|
||||||
const [addPOOpen, setAddPOOpen] = useState(false)
|
const [addPOOpen, setAddPOOpen] = useState(false)
|
||||||
const [rowSelection, setRowSelection] = useState({})
|
const [rowSelection, setRowSelection] = useState({})
|
||||||
const [currentPage, setCurrentPage] = useState(0)
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
const [pageSize, setPageSize] = useState(10)
|
const [pageSize, setPageSize] = useState(10)
|
||||||
const [openConfirm, setOpenConfirm] = useState(false)
|
const [openConfirm, setOpenConfirm] = useState(false)
|
||||||
const [poId, setPOId] = useState('')
|
const [poId, setPOId] = useState('')
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [statusFilter, setStatusFilter] = useState<string>('Semua')
|
const [statusFilter, setStatusFilter] = useState<string>('Semua')
|
||||||
const [filteredData, setFilteredData] = useState<PurchaseOrderType[]>(purchaseOrdersData)
|
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
const { lang: locale } = useParams()
|
const { lang: locale } = useParams()
|
||||||
|
|
||||||
// Filter data based on search and status
|
const { data, isLoading, error, isFetching } = usePurchaseOrders({
|
||||||
useEffect(() => {
|
page: currentPage,
|
||||||
let filtered = purchaseOrdersData
|
limit: pageSize,
|
||||||
|
search,
|
||||||
|
status: statusFilter === 'Semua' ? '' : statusFilter
|
||||||
|
})
|
||||||
|
|
||||||
// Filter by search
|
const purchaseOrders = data?.purchase_orders ?? []
|
||||||
if (search) {
|
const totalCount = data?.total_count ?? 0
|
||||||
filtered = filtered.filter(
|
|
||||||
po =>
|
|
||||||
po.number.toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
po.vendorName.toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
po.vendorCompany.toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
po.status.toLowerCase().includes(search.toLowerCase())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by status
|
|
||||||
if (statusFilter !== 'Semua') {
|
|
||||||
filtered = filtered.filter(po => po.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) => {
|
const handlePageChange = useCallback((event: unknown, newPage: number) => {
|
||||||
setCurrentPage(newPage)
|
setCurrentPage(newPage)
|
||||||
@ -222,14 +205,15 @@ const PurchaseOrderListTable = () => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
columnHelper.accessor('number', {
|
columnHelper.accessor('po_number', {
|
||||||
header: 'Nomor PO',
|
header: 'Nomor PO',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Button
|
<Button
|
||||||
variant='text'
|
variant='text'
|
||||||
color='primary'
|
color='primary'
|
||||||
className='p-0 min-w-0 font-medium normal-case justify-start'
|
className='p-0 min-w-0 font-medium normal-case justify-start'
|
||||||
onClick={() => handlePOClick(row.original.id.toString())}
|
component={Link}
|
||||||
|
href={getLocalizedUrl(`/apps/purchase/purchase-orders/${row.original.id}/detail`, locale as Locale)}
|
||||||
sx={{
|
sx={{
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
@ -239,19 +223,19 @@ const PurchaseOrderListTable = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{row.original.number}
|
{row.original.po_number}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('vendorName', {
|
columnHelper.accessor('vendor.name', {
|
||||||
header: 'Vendor',
|
header: 'Vendor',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<Typography color='text.primary' className='font-medium'>
|
<Typography color='text.primary' className='font-medium'>
|
||||||
{row.original.vendorName}
|
{row.original.vendor.contact_person}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='body2' color='text.secondary'>
|
<Typography variant='body2' color='text.secondary'>
|
||||||
{row.original.vendorCompany}
|
{row.original.vendor.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -260,13 +244,13 @@ const PurchaseOrderListTable = () => {
|
|||||||
header: 'Referensi',
|
header: 'Referensi',
|
||||||
cell: ({ row }) => <Typography color='text.secondary'>{row.original.reference || '-'}</Typography>
|
cell: ({ row }) => <Typography color='text.secondary'>{row.original.reference || '-'}</Typography>
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('date', {
|
columnHelper.accessor('transaction_date', {
|
||||||
header: 'Tanggal',
|
header: 'Tanggal',
|
||||||
cell: ({ row }) => <Typography>{row.original.date}</Typography>
|
cell: ({ row }) => <Typography>{row.original.transaction_date}</Typography>
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('dueDate', {
|
columnHelper.accessor('due_date', {
|
||||||
header: 'Tanggal Jatuh Tempo',
|
header: 'Tanggal Jatuh Tempo',
|
||||||
cell: ({ row }) => <Typography>{row.original.dueDate}</Typography>
|
cell: ({ row }) => <Typography>{row.original.due_date}</Typography>
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('status', {
|
columnHelper.accessor('status', {
|
||||||
header: 'Status',
|
header: 'Status',
|
||||||
@ -282,16 +266,16 @@ const PurchaseOrderListTable = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('total', {
|
columnHelper.accessor('total_amount', {
|
||||||
header: 'Total',
|
header: 'Total',
|
||||||
cell: ({ row }) => <Typography className='font-medium'>{formatCurrency(row.original.total)}</Typography>
|
cell: ({ row }) => <Typography className='font-medium'>{formatCurrency(row.original.total_amount)}</Typography>
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: paginatedData as PurchaseOrderType[],
|
data: purchaseOrders as PurchaseOrder[],
|
||||||
columns,
|
columns,
|
||||||
filterFns: {
|
filterFns: {
|
||||||
fuzzy: fuzzyFilter
|
fuzzy: fuzzyFilter
|
||||||
@ -316,27 +300,11 @@ const PurchaseOrderListTable = () => {
|
|||||||
{/* Filter Status Tabs */}
|
{/* Filter Status Tabs */}
|
||||||
<div className='p-6 border-bs'>
|
<div className='p-6 border-bs'>
|
||||||
<div className='flex flex-wrap gap-2'>
|
<div className='flex flex-wrap gap-2'>
|
||||||
{['Semua', 'Draft', 'Disetujui', 'Dikirim Sebagian', 'Selesai', 'Lainnya'].map(status => (
|
<StatusFilterTabs
|
||||||
<Button
|
statusOptions={['Semua', 'draft', 'sent', 'approved', 'received', 'cancelled']}
|
||||||
key={status}
|
selectedStatus={statusFilter}
|
||||||
variant={statusFilter === status ? 'contained' : 'outlined'}
|
onStatusChange={handleStatusFilter}
|
||||||
color={statusFilter === status ? 'primary' : 'inherit'}
|
/>
|
||||||
onClick={() => handleStatusFilter(status)}
|
|
||||||
size='small'
|
|
||||||
className='rounded-lg'
|
|
||||||
sx={{
|
|
||||||
textTransform: 'none',
|
|
||||||
fontWeight: statusFilter === status ? 600 : 400,
|
|
||||||
borderRadius: '8px',
|
|
||||||
...(statusFilter !== status && {
|
|
||||||
borderColor: '#e0e0e0',
|
|
||||||
color: '#666'
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{status}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -378,6 +346,9 @@ const PurchaseOrderListTable = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='overflow-x-auto'>
|
<div className='overflow-x-auto'>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loading />
|
||||||
|
) : (
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
{table.getHeaderGroups().map(headerGroup => (
|
{table.getHeaderGroups().map(headerGroup => (
|
||||||
@ -406,7 +377,7 @@ const PurchaseOrderListTable = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</thead>
|
</thead>
|
||||||
{filteredData.length === 0 ? (
|
{purchaseOrders.length === 0 ? (
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
|
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
|
||||||
@ -428,6 +399,7 @@ const PurchaseOrderListTable = () => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
)}
|
)}
|
||||||
</table>
|
</table>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TablePagination
|
<TablePagination
|
||||||
@ -445,6 +417,7 @@ const PurchaseOrderListTable = () => {
|
|||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
onRowsPerPageChange={handlePageSizeChange}
|
onRowsPerPageChange={handlePageSizeChange}
|
||||||
rowsPerPageOptions={[10, 25, 50]}
|
rowsPerPageOptions={[10, 25, 50]}
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,20 +1,28 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
// MUI Imports
|
// MUI Imports
|
||||||
import Card from '@mui/material/Card'
|
import Card from '@mui/material/Card'
|
||||||
import CardContent from '@mui/material/CardContent'
|
import CardContent from '@mui/material/CardContent'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import Chip from '@mui/material/Chip'
|
import Chip from '@mui/material/Chip'
|
||||||
import Divider from '@mui/material/Divider'
|
import Divider from '@mui/material/Divider'
|
||||||
import Button from '@mui/material/Button'
|
|
||||||
import type { ButtonProps } from '@mui/material/Button'
|
|
||||||
|
|
||||||
// Type Imports
|
|
||||||
import type { ThemeColor } from '@core/types'
|
|
||||||
|
|
||||||
// Component Imports
|
// Component Imports
|
||||||
import EditUserInfo from '@components/dialogs/edit-user-info'
|
|
||||||
import ConfirmationDialog from '@components/dialogs/confirmation-dialog'
|
|
||||||
import OpenDialogOnElementClick from '@components/dialogs/OpenDialogOnElementClick'
|
|
||||||
import CustomAvatar from '@core/components/mui/Avatar'
|
import CustomAvatar from '@core/components/mui/Avatar'
|
||||||
|
import { useParams } from 'next/navigation'
|
||||||
|
import { useVendorById } from '@/services/queries/vendor'
|
||||||
|
import Loading from '@/components/layout/shared/Loading'
|
||||||
|
import { getInitials } from '@/utils/getInitials'
|
||||||
|
import OpenDialogOnElementClick from '@/components/dialogs/OpenDialogOnElementClick'
|
||||||
|
import { Box, Button, ButtonProps, CircularProgress } from '@mui/material'
|
||||||
|
import ConfirmationDialog from '@/components/dialogs/confirmation-dialog'
|
||||||
|
import EditUserInfo from '@/components/dialogs/edit-user-info'
|
||||||
|
import { ThemeColor } from '@/@core/types'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import AddVendorDrawer from '../../list/AddVendorDrawer'
|
||||||
|
import ConfirmDeleteDialog from '@/components/dialogs/confirm-delete'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { useVendorsMutation } from '@/services/mutations/vendor'
|
||||||
|
|
||||||
// Vars
|
// Vars
|
||||||
const userData = {
|
const userData = {
|
||||||
@ -33,7 +41,25 @@ const userData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const VendorDetails = () => {
|
const VendorDetails = () => {
|
||||||
// Vars
|
const [editVendorOpen, setEditVendorOpen] = useState(false)
|
||||||
|
const [openConfirm, setOpenConfirm] = useState(false)
|
||||||
|
|
||||||
|
const params = useParams()
|
||||||
|
const id = params?.id ?? ''
|
||||||
|
|
||||||
|
const { data: vendor, isLoading, error } = useVendorById(id as string)
|
||||||
|
|
||||||
|
const { deleteVendor } = useVendorsMutation()
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
deleteVendor.mutate(id as string, {
|
||||||
|
onSuccess: () => {
|
||||||
|
setOpenConfirm(false)
|
||||||
|
window.history.back()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const buttonProps = (children: string, color: ThemeColor, variant: ButtonProps['variant']): ButtonProps => ({
|
const buttonProps = (children: string, color: ThemeColor, variant: ButtonProps['variant']): ButtonProps => ({
|
||||||
children,
|
children,
|
||||||
color,
|
color,
|
||||||
@ -42,13 +68,31 @@ const VendorDetails = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{isLoading ? (
|
||||||
|
<Box
|
||||||
|
position='absolute'
|
||||||
|
top={0}
|
||||||
|
left={0}
|
||||||
|
right={0}
|
||||||
|
bottom={0}
|
||||||
|
display='flex'
|
||||||
|
alignItems='center'
|
||||||
|
justifyContent='center'
|
||||||
|
bgcolor='rgba(255,255,255,0.7)'
|
||||||
|
zIndex={1}
|
||||||
|
>
|
||||||
|
<CircularProgress size={24} />
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className='flex flex-col pbs-12 gap-6'>
|
<CardContent className='flex flex-col pbs-12 gap-6'>
|
||||||
<div className='flex flex-col gap-6'>
|
<div className='flex flex-col gap-6'>
|
||||||
<div className='flex items-center justify-center flex-col gap-4'>
|
<div className='flex items-center justify-center flex-col gap-4'>
|
||||||
<div className='flex flex-col items-center gap-4'>
|
<div className='flex flex-col items-center gap-4'>
|
||||||
<CustomAvatar alt='user-profile' src='/images/avatars/1.png' variant='rounded' size={120} />
|
{/* <CustomAvatar alt='vendor-profile' variant='rounded' size={120}>
|
||||||
<Typography variant='h5'>{`${userData.firstName} ${userData.lastName}`}</Typography>
|
{getInitials(vendor?.name as string)}
|
||||||
|
</CustomAvatar> */}
|
||||||
|
<Typography variant='h5'>{vendor?.name}</Typography>
|
||||||
</div>
|
</div>
|
||||||
<Chip label='Vendor' color='primary' size='small' variant='tonal' />
|
<Chip label='Vendor' color='primary' size='small' variant='tonal' />
|
||||||
</div>
|
</div>
|
||||||
@ -61,22 +105,22 @@ const VendorDetails = () => {
|
|||||||
<div className='flex flex-col gap-2'>
|
<div className='flex flex-col gap-2'>
|
||||||
<div className='flex items-center flex-wrap gap-x-1.5'>
|
<div className='flex items-center flex-wrap gap-x-1.5'>
|
||||||
<Typography className='font-medium' color='text.primary'>
|
<Typography className='font-medium' color='text.primary'>
|
||||||
Nama:
|
Contact Person:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography>{`${userData.firstName} ${userData.lastName}`}</Typography>
|
<Typography>{vendor?.contact_person}</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center flex-wrap gap-x-1.5'>
|
<div className='flex items-center flex-wrap gap-x-1.5'>
|
||||||
<Typography className='font-medium' color='text.primary'>
|
<Typography className='font-medium' color='text.primary'>
|
||||||
Perusahaan:
|
Perusahaan:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography>{userData.perusahaan}</Typography>
|
<Typography>{vendor?.name}</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center flex-wrap gap-x-1.5'>
|
<div className='flex items-center flex-wrap gap-x-1.5'>
|
||||||
<Typography className='font-medium' color='text.primary'>
|
<Typography className='font-medium' color='text.primary'>
|
||||||
Email:
|
Email:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography color='primary' sx={{ textDecoration: 'none', cursor: 'pointer' }}>
|
<Typography color='primary' sx={{ textDecoration: 'none', cursor: 'pointer' }}>
|
||||||
{userData.email}
|
{vendor?.email}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center flex-wrap gap-x-1.5'>
|
<div className='flex items-center flex-wrap gap-x-1.5'>
|
||||||
@ -84,7 +128,7 @@ const VendorDetails = () => {
|
|||||||
Telepon:
|
Telepon:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography color='primary' sx={{ textDecoration: 'none', cursor: 'pointer' }}>
|
<Typography color='primary' sx={{ textDecoration: 'none', cursor: 'pointer' }}>
|
||||||
{userData.telepon}
|
{vendor?.phone_number}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center flex-wrap gap-x-1.5'>
|
<div className='flex items-center flex-wrap gap-x-1.5'>
|
||||||
@ -92,7 +136,7 @@ const VendorDetails = () => {
|
|||||||
Alamat Penagihan:
|
Alamat Penagihan:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography color='primary' sx={{ textDecoration: 'none', cursor: 'pointer' }}>
|
<Typography color='primary' sx={{ textDecoration: 'none', cursor: 'pointer' }}>
|
||||||
{userData.alamatPenagihan}
|
{vendor?.address ?? '-'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -125,8 +169,31 @@ const VendorDetails = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className='flex gap-4 justify-center'>
|
||||||
|
<Button variant='contained' onClick={() => setEditVendorOpen(!editVendorOpen)} className='max-sm:is-full'>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='contained'
|
||||||
|
color='error'
|
||||||
|
onClick={() => setOpenConfirm(!openConfirm)}
|
||||||
|
className='max-sm:is-full'
|
||||||
|
>
|
||||||
|
Hapus
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
)}
|
||||||
|
<AddVendorDrawer open={editVendorOpen} handleClose={() => setEditVendorOpen(!editVendorOpen)} data={vendor} />
|
||||||
|
<ConfirmDeleteDialog
|
||||||
|
open={openConfirm}
|
||||||
|
onClose={() => setOpenConfirm(false)}
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
isLoading={deleteVendor.isPending}
|
||||||
|
title='Delete Vendor'
|
||||||
|
message='Are you sure you want to delete this Vendor? This action cannot be undone.'
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
757
src/views/apps/vendor/list/AddVendorDrawer.tsx
vendored
757
src/views/apps/vendor/list/AddVendorDrawer.tsx
vendored
@ -1,5 +1,5 @@
|
|||||||
// React Imports
|
// React Imports
|
||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
// MUI Imports
|
// MUI Imports
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
@ -10,54 +10,60 @@ import Typography from '@mui/material/Typography'
|
|||||||
import Divider from '@mui/material/Divider'
|
import Divider from '@mui/material/Divider'
|
||||||
import Grid from '@mui/material/Grid2'
|
import Grid from '@mui/material/Grid2'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
|
import Switch from '@mui/material/Switch'
|
||||||
|
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||||
|
|
||||||
// Third-party Imports
|
// Third-party Imports
|
||||||
import { useForm, Controller } from 'react-hook-form'
|
import { useForm, Controller } from 'react-hook-form'
|
||||||
|
|
||||||
// Types Imports
|
|
||||||
import type { VendorType } from '@/types/apps/vendorTypes'
|
|
||||||
|
|
||||||
// Component Imports
|
// Component Imports
|
||||||
import CustomTextField from '@core/components/mui/TextField'
|
import CustomTextField from '@core/components/mui/TextField'
|
||||||
|
import { Vendor, VendorRequest } from '@/types/services/vendor'
|
||||||
|
import { useVendorsMutation } from '@/services/mutations/vendor'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
open: boolean
|
open: boolean
|
||||||
handleClose: () => void
|
handleClose: () => void
|
||||||
vendorData?: VendorType[]
|
data?: Vendor // Data vendor untuk edit (jika ada)
|
||||||
setData: (data: VendorType[]) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type FormValidateType = {
|
type FormValidateType = {
|
||||||
name: string
|
name: string
|
||||||
company: string
|
|
||||||
email: string
|
email: string
|
||||||
telephone: string
|
phone_number: string
|
||||||
|
address: string
|
||||||
|
contact_person: string
|
||||||
|
tax_number: string
|
||||||
|
payment_terms: string
|
||||||
|
notes: string
|
||||||
|
is_active: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vars
|
// Initial form data
|
||||||
const initialData = {
|
const initialData: FormValidateType = {
|
||||||
name: '',
|
name: '',
|
||||||
company: '',
|
|
||||||
email: '',
|
email: '',
|
||||||
telephone: ''
|
phone_number: '',
|
||||||
|
address: '',
|
||||||
|
contact_person: '',
|
||||||
|
tax_number: '',
|
||||||
|
payment_terms: '',
|
||||||
|
notes: '',
|
||||||
|
is_active: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddVendorDrawer = (props: Props) => {
|
const AddEditVendorDrawer = (props: Props) => {
|
||||||
// Props
|
// Props
|
||||||
const { open, handleClose, vendorData, setData } = props
|
const { open, handleClose, data } = props
|
||||||
|
|
||||||
// States
|
// States
|
||||||
const [showMore, setShowMore] = useState(false)
|
const [showMore, setShowMore] = useState(false)
|
||||||
const [alamatPengiriman, setAlamatPengiriman] = useState([''])
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
const [rekeningBank, setRekeningBank] = useState([
|
|
||||||
{
|
const { createVendor, updateVendor } = useVendorsMutation()
|
||||||
bank: '',
|
|
||||||
cabang: '',
|
// Determine if this is edit mode
|
||||||
namaPemilik: '',
|
const isEditMode = Boolean(data?.id)
|
||||||
nomorRekening: ''
|
|
||||||
}
|
|
||||||
])
|
|
||||||
const [showPemetaanAkun, setShowPemetaanAkun] = useState(false)
|
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
const {
|
const {
|
||||||
@ -69,92 +75,85 @@ const AddVendorDrawer = (props: Props) => {
|
|||||||
defaultValues: initialData
|
defaultValues: initialData
|
||||||
})
|
})
|
||||||
|
|
||||||
// Functions untuk alamat
|
// Effect to populate form when editing
|
||||||
const handleTambahAlamat = () => {
|
useEffect(() => {
|
||||||
setAlamatPengiriman([...alamatPengiriman, ''])
|
if (isEditMode && data) {
|
||||||
|
// Populate form with existing data
|
||||||
|
const formData: FormValidateType = {
|
||||||
|
name: data.name || '',
|
||||||
|
email: data.email || '',
|
||||||
|
phone_number: data.phone_number || '',
|
||||||
|
address: data.address || '',
|
||||||
|
contact_person: data.contact_person || '',
|
||||||
|
tax_number: data.tax_number || '',
|
||||||
|
payment_terms: data.payment_terms || '',
|
||||||
|
notes: data.notes || '',
|
||||||
|
is_active: data.is_active ?? true
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleHapusAlamat = (index: number) => {
|
resetForm(formData)
|
||||||
if (alamatPengiriman.length > 1) {
|
|
||||||
const newAlamat = alamatPengiriman.filter((_, i) => i !== index)
|
|
||||||
setAlamatPengiriman(newAlamat)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChangeAlamat = (index: number, value: string) => {
|
// Show more fields if any optional field has data
|
||||||
const newAlamat = [...alamatPengiriman]
|
const hasOptionalData = data.address || data.tax_number || data.payment_terms || data.notes
|
||||||
newAlamat[index] = value
|
if (hasOptionalData) {
|
||||||
setAlamatPengiriman(newAlamat)
|
setShowMore(true)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// Functions untuk rekening bank
|
// Reset to initial data for add mode
|
||||||
const handleTambahRekening = () => {
|
|
||||||
setRekeningBank([
|
|
||||||
...rekeningBank,
|
|
||||||
{
|
|
||||||
bank: '',
|
|
||||||
cabang: '',
|
|
||||||
namaPemilik: '',
|
|
||||||
nomorRekening: ''
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleHapusRekening = (index: number) => {
|
|
||||||
if (rekeningBank.length > 1) {
|
|
||||||
const newRekening = rekeningBank.filter((_, i) => i !== index)
|
|
||||||
setRekeningBank(newRekening)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChangeRekening = (index: number, field: string, value: string) => {
|
|
||||||
const newRekening = [...rekeningBank]
|
|
||||||
newRekening[index] = { ...newRekening[index], [field]: value }
|
|
||||||
setRekeningBank(newRekening)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSubmit = (data: FormValidateType) => {
|
|
||||||
const newVendor: VendorType = {
|
|
||||||
id: (vendorData?.length && vendorData?.length + 1) || 1,
|
|
||||||
photo: '',
|
|
||||||
name: data.name,
|
|
||||||
company: data.company,
|
|
||||||
email: data.email,
|
|
||||||
telephone: data.telephone,
|
|
||||||
youPayable: 0,
|
|
||||||
theyPayable: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
setData([...(vendorData ?? []), newVendor])
|
|
||||||
handleClose()
|
|
||||||
resetForm(initialData)
|
resetForm(initialData)
|
||||||
setAlamatPengiriman([''])
|
|
||||||
setRekeningBank([
|
|
||||||
{
|
|
||||||
bank: '',
|
|
||||||
cabang: '',
|
|
||||||
namaPemilik: '',
|
|
||||||
nomorRekening: ''
|
|
||||||
}
|
|
||||||
])
|
|
||||||
setShowMore(false)
|
setShowMore(false)
|
||||||
setShowPemetaanAkun(false)
|
}
|
||||||
|
}, [data, isEditMode, resetForm])
|
||||||
|
|
||||||
|
const handleFormSubmit = async (formData: FormValidateType) => {
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true)
|
||||||
|
|
||||||
|
// Create VendorRequest object
|
||||||
|
const vendorRequest: VendorRequest = {
|
||||||
|
name: formData.name,
|
||||||
|
email: formData.email || undefined,
|
||||||
|
phone_number: formData.phone_number || undefined,
|
||||||
|
address: formData.address || undefined,
|
||||||
|
contact_person: formData.contact_person || undefined,
|
||||||
|
tax_number: formData.tax_number || undefined,
|
||||||
|
payment_terms: formData.payment_terms || undefined,
|
||||||
|
notes: formData.notes || undefined,
|
||||||
|
is_active: formData.is_active
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEditMode && data?.id) {
|
||||||
|
// Update existing vendor
|
||||||
|
updateVendor.mutate(
|
||||||
|
{ id: data.id, payload: vendorRequest },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
handleReset()
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Create new vendor
|
||||||
|
createVendor.mutate(vendorRequest, {
|
||||||
|
onSuccess: () => {
|
||||||
|
handleReset()
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error submitting vendor:', error)
|
||||||
|
// Handle error (show toast, etc.)
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
handleClose()
|
handleClose()
|
||||||
resetForm(initialData)
|
resetForm(initialData)
|
||||||
setAlamatPengiriman([''])
|
|
||||||
setRekeningBank([
|
|
||||||
{
|
|
||||||
bank: '',
|
|
||||||
cabang: '',
|
|
||||||
namaPemilik: '',
|
|
||||||
nomorRekening: ''
|
|
||||||
}
|
|
||||||
])
|
|
||||||
setShowMore(false)
|
setShowMore(false)
|
||||||
setShowPemetaanAkun(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -185,7 +184,7 @@ const AddVendorDrawer = (props: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className='flex items-center justify-between plb-5 pli-6'>
|
<div className='flex items-center justify-between plb-5 pli-6'>
|
||||||
<Typography variant='h5'>Tambah Vendor Baru</Typography>
|
<Typography variant='h5'>{isEditMode ? 'Edit Vendor' : 'Tambah Vendor Baru'}</Typography>
|
||||||
<IconButton size='small' onClick={handleReset}>
|
<IconButton size='small' onClick={handleReset}>
|
||||||
<i className='tabler-x text-2xl text-textPrimary' />
|
<i className='tabler-x text-2xl text-textPrimary' />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@ -194,472 +193,200 @@ const AddVendorDrawer = (props: Props) => {
|
|||||||
|
|
||||||
{/* Scrollable Content */}
|
{/* Scrollable Content */}
|
||||||
<Box sx={{ flex: 1, overflowY: 'auto' }}>
|
<Box sx={{ flex: 1, overflowY: 'auto' }}>
|
||||||
<form id='vendor-form' onSubmit={handleSubmit(data => onSubmit(data))}>
|
<form id='vendor-form' onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
<div className='flex flex-col gap-6 p-6'>
|
<div className='flex flex-col gap-6 p-6'>
|
||||||
{/* Tampilkan Foto */}
|
{/* Nama Vendor */}
|
||||||
<div className='flex items-center gap-3'>
|
|
||||||
<i className='tabler-plus text-blue-500' />
|
|
||||||
<Typography variant='body1' color='primary' className='cursor-pointer'>
|
|
||||||
Tampilkan Foto
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Nama */}
|
|
||||||
<div>
|
<div>
|
||||||
<Typography variant='body2' className='mb-2'>
|
<Typography variant='body2' className='mb-2'>
|
||||||
Nama <span className='text-red-500'>*</span>
|
Nama Vendor <span className='text-red-500'>*</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid size={4}>
|
|
||||||
<CustomTextField select fullWidth defaultValue='Tuan'>
|
|
||||||
<MenuItem value='Tuan'>Tuan</MenuItem>
|
|
||||||
<MenuItem value='Nyonya'>Nyonya</MenuItem>
|
|
||||||
<MenuItem value='Nona'>Nona</MenuItem>
|
|
||||||
<MenuItem value='Bapak'>Bapak</MenuItem>
|
|
||||||
<MenuItem value='Ibu'>Ibu</MenuItem>
|
|
||||||
</CustomTextField>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={8}>
|
|
||||||
<Controller
|
<Controller
|
||||||
name='name'
|
name='name'
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: true }}
|
rules={{ required: 'Nama vendor wajib diisi' }}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
{...field}
|
{...field}
|
||||||
fullWidth
|
fullWidth
|
||||||
placeholder='Nama'
|
placeholder='Masukkan nama vendor'
|
||||||
{...(errors.name && { error: true, helperText: 'Field ini wajib diisi.' })}
|
error={!!errors.name}
|
||||||
|
helperText={errors.name?.message}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Perusahaan dan Telepon */}
|
|
||||||
<Grid container spacing={6}>
|
|
||||||
<Grid size={6}>
|
|
||||||
<Controller
|
|
||||||
name='company'
|
|
||||||
control={control}
|
|
||||||
rules={{ required: true }}
|
|
||||||
render={({ field }) => (
|
|
||||||
<CustomTextField
|
|
||||||
{...field}
|
|
||||||
fullWidth
|
|
||||||
label='Perusahaan'
|
|
||||||
placeholder='Perusahaan'
|
|
||||||
{...(errors.company && { error: true, helperText: 'Field ini wajib diisi.' })}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={6}>
|
|
||||||
<Controller
|
|
||||||
name='telephone'
|
|
||||||
control={control}
|
|
||||||
rules={{ required: true }}
|
|
||||||
render={({ field }) => (
|
|
||||||
<CustomTextField
|
|
||||||
{...field}
|
|
||||||
fullWidth
|
|
||||||
label='Telepon'
|
|
||||||
placeholder='Telepon'
|
|
||||||
{...(errors.telephone && { error: true, helperText: 'Field ini wajib diisi.' })}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Email */}
|
{/* Email */}
|
||||||
|
<div>
|
||||||
|
<Typography variant='body2' className='mb-2'>
|
||||||
|
Email <span className='text-red-500'>*</span>
|
||||||
|
</Typography>
|
||||||
<Controller
|
<Controller
|
||||||
name='email'
|
name='email'
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: true }}
|
rules={{
|
||||||
|
required: 'Email wajib diisi',
|
||||||
|
pattern: {
|
||||||
|
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||||
|
message: 'Format email tidak valid'
|
||||||
|
}
|
||||||
|
}}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
{...field}
|
{...field}
|
||||||
fullWidth
|
fullWidth
|
||||||
type='email'
|
type='email'
|
||||||
label='Email'
|
placeholder='vendor@example.com'
|
||||||
placeholder='Email'
|
error={!!errors.email}
|
||||||
{...(errors.email && { error: true, helperText: 'Field ini wajib diisi.' })}
|
helperText={errors.email?.message}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nomor Telepon */}
|
||||||
|
<div>
|
||||||
|
<Typography variant='body2' className='mb-2'>
|
||||||
|
Nomor Telepon <span className='text-red-500'>*</span>
|
||||||
|
</Typography>
|
||||||
|
<Controller
|
||||||
|
name='phone_number'
|
||||||
|
control={control}
|
||||||
|
rules={{ required: 'Telepon wajib diisi' }}
|
||||||
|
render={({ field }) => (
|
||||||
|
<CustomTextField {...field} fullWidth placeholder='Harus Diawali 62' error={!!errors.phone_number} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contact Person */}
|
||||||
|
<div>
|
||||||
|
<Typography variant='body2' className='mb-2'>
|
||||||
|
Contact Person <span className='text-red-500'>*</span>
|
||||||
|
</Typography>
|
||||||
|
<Controller
|
||||||
|
name='contact_person'
|
||||||
|
control={control}
|
||||||
|
rules={{ required: 'Contact Person wajib diisi' }}
|
||||||
|
render={({ field }) => (
|
||||||
|
<CustomTextField
|
||||||
|
{...field}
|
||||||
|
fullWidth
|
||||||
|
placeholder='Nama contact person'
|
||||||
|
error={!!errors.contact_person}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Aktif */}
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
name='is_active'
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControlLabel
|
||||||
|
control={<Switch checked={field.value} onChange={field.onChange} color='primary' />}
|
||||||
|
label='Vendor Aktif'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Tampilkan selengkapnya */}
|
{/* Tampilkan selengkapnya */}
|
||||||
{!showMore && (
|
{!showMore && (
|
||||||
<div className='flex items-center gap-3' onClick={() => setShowMore(true)}>
|
<Button
|
||||||
<i className='tabler-plus text-blue-500' />
|
variant='text'
|
||||||
<Typography variant='body1' color='primary' className='cursor-pointer'>
|
color='primary'
|
||||||
Tampilkan selengkapnya
|
size='small'
|
||||||
</Typography>
|
sx={{ textTransform: 'none', fontSize: '14px', p: 0, textAlign: 'left', width: '200px' }}
|
||||||
</div>
|
onClick={() => setShowMore(true)}
|
||||||
|
>
|
||||||
|
+ Tampilkan selengkapnya
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Konten tambahan yang muncul saat showMore true */}
|
{/* Konten tambahan */}
|
||||||
{showMore && (
|
{showMore && (
|
||||||
<>
|
<>
|
||||||
{/* Alamat Penagihan */}
|
{/* Alamat */}
|
||||||
<div>
|
<div>
|
||||||
<Typography variant='body2' className='mb-2'>
|
<Typography variant='body2' className='mb-2'>
|
||||||
Alamat Penagihan
|
Alamat
|
||||||
</Typography>
|
</Typography>
|
||||||
<CustomTextField fullWidth placeholder='Alamat Penagihan' multiline rows={3} />
|
<Controller
|
||||||
|
name='address'
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<CustomTextField {...field} fullWidth placeholder='Alamat lengkap vendor' multiline rows={3} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Negara */}
|
{/* NPWP/Tax Number */}
|
||||||
<div>
|
|
||||||
<Typography variant='body2' className='mb-2'>
|
|
||||||
Negara
|
|
||||||
</Typography>
|
|
||||||
<CustomTextField select fullWidth defaultValue='Indonesia'>
|
|
||||||
<MenuItem value='Indonesia'>Indonesia</MenuItem>
|
|
||||||
</CustomTextField>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Provinsi dan Kota */}
|
|
||||||
<Grid container spacing={6}>
|
|
||||||
<Grid size={6}>
|
|
||||||
<Typography variant='body2' className='mb-2'>
|
|
||||||
Provinsi
|
|
||||||
</Typography>
|
|
||||||
<CustomTextField select fullWidth placeholder='Provinsi'>
|
|
||||||
<MenuItem value=''>Pilih Provinsi</MenuItem>
|
|
||||||
<MenuItem value='DKI Jakarta'>DKI Jakarta</MenuItem>
|
|
||||||
<MenuItem value='Jawa Barat'>Jawa Barat</MenuItem>
|
|
||||||
<MenuItem value='Jawa Tengah'>Jawa Tengah</MenuItem>
|
|
||||||
<MenuItem value='Jawa Timur'>Jawa Timur</MenuItem>
|
|
||||||
</CustomTextField>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={6}>
|
|
||||||
<Typography variant='body2' className='mb-2'>
|
|
||||||
Kota
|
|
||||||
</Typography>
|
|
||||||
<CustomTextField select fullWidth placeholder='Kota'>
|
|
||||||
<MenuItem value=''>Pilih Kota</MenuItem>
|
|
||||||
<MenuItem value='Jakarta'>Jakarta</MenuItem>
|
|
||||||
<MenuItem value='Bandung'>Bandung</MenuItem>
|
|
||||||
<MenuItem value='Surabaya'>Surabaya</MenuItem>
|
|
||||||
</CustomTextField>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Kecamatan dan Kelurahan */}
|
|
||||||
<Grid container spacing={6}>
|
|
||||||
<Grid size={6}>
|
|
||||||
<Typography variant='body2' className='mb-2'>
|
|
||||||
Kecamatan
|
|
||||||
</Typography>
|
|
||||||
<CustomTextField select fullWidth placeholder='Kecamatan'>
|
|
||||||
<MenuItem value=''>Pilih Kecamatan</MenuItem>
|
|
||||||
</CustomTextField>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={6}>
|
|
||||||
<Typography variant='body2' className='mb-2'>
|
|
||||||
Kelurahan
|
|
||||||
</Typography>
|
|
||||||
<CustomTextField select fullWidth placeholder='Kelurahan'>
|
|
||||||
<MenuItem value=''>Pilih Kelurahan</MenuItem>
|
|
||||||
</CustomTextField>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Tipe Kartu Identitas dan ID */}
|
|
||||||
<Grid container spacing={6}>
|
|
||||||
<Grid size={6}>
|
|
||||||
<Typography variant='body2' className='mb-2'>
|
|
||||||
Tipe Kartu Identitas
|
|
||||||
</Typography>
|
|
||||||
<CustomTextField select fullWidth placeholder='Silahkan pilih tipe kartu identitas'>
|
|
||||||
<MenuItem value=''>Pilih Tipe Kartu Identitas</MenuItem>
|
|
||||||
<MenuItem value='KTP'>KTP</MenuItem>
|
|
||||||
<MenuItem value='SIM'>SIM</MenuItem>
|
|
||||||
<MenuItem value='Paspor'>Paspor</MenuItem>
|
|
||||||
</CustomTextField>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={6}>
|
|
||||||
<Typography variant='body2' className='mb-2'>
|
|
||||||
ID Kartu Identitas
|
|
||||||
</Typography>
|
|
||||||
<CustomTextField fullWidth placeholder='ID Kartu Identitas' />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* NPWP */}
|
|
||||||
<div>
|
<div>
|
||||||
<Typography variant='body2' className='mb-2'>
|
<Typography variant='body2' className='mb-2'>
|
||||||
NPWP
|
NPWP
|
||||||
</Typography>
|
</Typography>
|
||||||
<CustomTextField fullWidth placeholder='NPWP' />
|
<Controller
|
||||||
|
name='tax_number'
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => <CustomTextField {...field} fullWidth placeholder='Nomor NPWP' />}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Alamat Pengiriman */}
|
{/* Payment Terms */}
|
||||||
<div>
|
<div>
|
||||||
<Typography variant='body2' className='mb-2 font-medium'>
|
<Typography variant='body2' className='mb-2'>
|
||||||
Alamat Pengiriman
|
Syarat Pembayaran
|
||||||
</Typography>
|
</Typography>
|
||||||
{alamatPengiriman.map((alamat, index) => (
|
<Controller
|
||||||
<div key={index} className='flex items-center gap-3 mb-3'>
|
name='payment_terms'
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<CustomTextField {...field} select fullWidth placeholder='Pilih syarat pembayaran'>
|
||||||
|
<MenuItem value=''>Pilih Syarat Pembayaran</MenuItem>
|
||||||
|
<MenuItem value='CASH'>Cash</MenuItem>
|
||||||
|
<MenuItem value='NET_7'>Net 7 Hari</MenuItem>
|
||||||
|
<MenuItem value='NET_14'>Net 14 Hari</MenuItem>
|
||||||
|
<MenuItem value='NET_30'>Net 30 Hari</MenuItem>
|
||||||
|
<MenuItem value='NET_60'>Net 60 Hari</MenuItem>
|
||||||
|
<MenuItem value='NET_90'>Net 90 Hari</MenuItem>
|
||||||
|
</CustomTextField>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Notes */}
|
||||||
|
<div>
|
||||||
|
<Typography variant='body2' className='mb-2'>
|
||||||
|
Catatan
|
||||||
|
</Typography>
|
||||||
|
<Controller
|
||||||
|
name='notes'
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
|
{...field}
|
||||||
fullWidth
|
fullWidth
|
||||||
placeholder='Alamat'
|
placeholder='Catatan tambahan tentang vendor'
|
||||||
multiline
|
multiline
|
||||||
rows={2}
|
rows={3}
|
||||||
value={alamat}
|
|
||||||
onChange={e => handleChangeAlamat(index, e.target.value)}
|
|
||||||
sx={{
|
|
||||||
'& .MuiOutlinedInput-root': {
|
|
||||||
borderColor: index === 1 ? 'primary.main' : 'default'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{alamatPengiriman.length > 1 && (
|
)}
|
||||||
<IconButton
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sembunyikan */}
|
||||||
|
<Button
|
||||||
|
variant='text'
|
||||||
|
color='primary'
|
||||||
size='small'
|
size='small'
|
||||||
onClick={() => handleHapusAlamat(index)}
|
sx={{ textTransform: 'none', fontSize: '14px', p: 0, textAlign: 'left', width: '200px' }}
|
||||||
sx={{
|
onClick={() => setShowMore(false)}
|
||||||
color: 'error.main',
|
|
||||||
border: 1,
|
|
||||||
borderColor: 'error.main',
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: 'error.light',
|
|
||||||
borderColor: 'error.main'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<i className='tabler-trash' />
|
- Sembunyikan
|
||||||
</IconButton>
|
</Button>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tambah Alamat Pengiriman */}
|
|
||||||
<div className='flex items-center gap-3' onClick={handleTambahAlamat}>
|
|
||||||
<i className='tabler-plus text-blue-500' />
|
|
||||||
<Typography variant='body1' color='primary' className='cursor-pointer'>
|
|
||||||
Tambah Alamat Pengiriman
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Rekening Bank */}
|
|
||||||
<div>
|
|
||||||
<Typography variant='body2' className='mb-2 font-medium'>
|
|
||||||
Rekening Bank
|
|
||||||
</Typography>
|
|
||||||
{rekeningBank.map((rekening, index) => (
|
|
||||||
<div key={index} className='mb-4'>
|
|
||||||
<div className='flex items-start gap-3'>
|
|
||||||
<div className='flex-1'>
|
|
||||||
{/* Baris pertama: Bank & Cabang */}
|
|
||||||
<Grid container spacing={3} className='mb-3'>
|
|
||||||
<Grid size={6}>
|
|
||||||
<CustomTextField
|
|
||||||
select
|
|
||||||
fullWidth
|
|
||||||
placeholder='Bank'
|
|
||||||
value={rekening.bank}
|
|
||||||
onChange={e => handleChangeRekening(index, 'bank', e.target.value)}
|
|
||||||
>
|
|
||||||
<MenuItem value=''>Pilih Bank</MenuItem>
|
|
||||||
<MenuItem value='BCA'>BCA</MenuItem>
|
|
||||||
<MenuItem value='Mandiri'>Mandiri</MenuItem>
|
|
||||||
<MenuItem value='BNI'>BNI</MenuItem>
|
|
||||||
<MenuItem value='BRI'>BRI</MenuItem>
|
|
||||||
</CustomTextField>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={6}>
|
|
||||||
<CustomTextField
|
|
||||||
fullWidth
|
|
||||||
placeholder='Cabang'
|
|
||||||
value={rekening.cabang}
|
|
||||||
onChange={e => handleChangeRekening(index, 'cabang', e.target.value)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Baris kedua: Nama Pemilik & Nomor Rekening */}
|
|
||||||
<Grid container spacing={3}>
|
|
||||||
<Grid size={6}>
|
|
||||||
<CustomTextField
|
|
||||||
fullWidth
|
|
||||||
placeholder='Nama Pemilik'
|
|
||||||
value={rekening.namaPemilik}
|
|
||||||
onChange={e => handleChangeRekening(index, 'namaPemilik', e.target.value)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={6}>
|
|
||||||
<CustomTextField
|
|
||||||
fullWidth
|
|
||||||
placeholder='Nomor Rekening'
|
|
||||||
value={rekening.nomorRekening}
|
|
||||||
onChange={e => handleChangeRekening(index, 'nomorRekening', e.target.value)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tombol hapus di samping, sejajar dengan tengah kedua baris */}
|
|
||||||
{rekeningBank.length > 1 && (
|
|
||||||
<div className='flex items-center' style={{ height: '120px' }}>
|
|
||||||
<IconButton
|
|
||||||
size='small'
|
|
||||||
onClick={() => handleHapusRekening(index)}
|
|
||||||
sx={{
|
|
||||||
color: 'error.main',
|
|
||||||
border: 1,
|
|
||||||
borderColor: 'error.main',
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: 'error.light',
|
|
||||||
borderColor: 'error.main'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i className='tabler-trash' />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div className='flex items-center gap-3 mt-4' onClick={handleTambahRekening}>
|
|
||||||
<i className='tabler-plus text-blue-500' />
|
|
||||||
<Typography variant='body1' color='primary' className='cursor-pointer'>
|
|
||||||
Tambah Rekening Bank
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex items-center gap-3 mt-2' onClick={() => setShowPemetaanAkun(!showPemetaanAkun)}>
|
|
||||||
<i className={showPemetaanAkun ? 'tabler-minus text-blue-500' : 'tabler-plus text-blue-500'} />
|
|
||||||
<Typography variant='body1' color='primary' className='cursor-pointer'>
|
|
||||||
{showPemetaanAkun ? 'Sembunyikan pemetaan akun' : 'Tampilkan pemetaan akun'}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Konten Pemetaan Akun */}
|
|
||||||
{showPemetaanAkun && (
|
|
||||||
<div className='mt-6 p-4 border border-gray-200 rounded-lg'>
|
|
||||||
{/* Akun Hutang */}
|
|
||||||
<Grid container spacing={6} className='mb-4'>
|
|
||||||
<Grid size={6}>
|
|
||||||
<Typography variant='body2' className='mb-2 font-medium'>
|
|
||||||
Akun Hutang
|
|
||||||
</Typography>
|
|
||||||
<CustomTextField select fullWidth defaultValue='2-20100 Hutang Usaha'>
|
|
||||||
<MenuItem value='2-20100 Hutang Usaha'>2-20100 Hutang Usaha</MenuItem>
|
|
||||||
<MenuItem value='2-20200 Hutang Bank'>2-20200 Hutang Bank</MenuItem>
|
|
||||||
<MenuItem value='2-20300 Hutang Lainnya'>2-20300 Hutang Lainnya</MenuItem>
|
|
||||||
</CustomTextField>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={6}>
|
|
||||||
<Typography variant='body2' className='mb-2 font-medium'>
|
|
||||||
Maksimal Hutang
|
|
||||||
<i className='tabler-help-circle text-gray-400 ml-1' />
|
|
||||||
</Typography>
|
|
||||||
<CustomTextField fullWidth type='number' defaultValue='0' placeholder='0' />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Akun Piutang */}
|
|
||||||
<Grid container spacing={6} className='mb-4'>
|
|
||||||
<Grid size={6}>
|
|
||||||
<Typography variant='body2' className='mb-2 font-medium'>
|
|
||||||
Akun Piutang
|
|
||||||
</Typography>
|
|
||||||
<CustomTextField select fullWidth defaultValue='1-10100 Piutang Usaha'>
|
|
||||||
<MenuItem value='1-10100 Piutang Usaha'>1-10100 Piutang Usaha</MenuItem>
|
|
||||||
<MenuItem value='1-10200 Piutang Karyawan'>1-10200 Piutang Karyawan</MenuItem>
|
|
||||||
<MenuItem value='1-10300 Piutang Lainnya'>1-10300 Piutang Lainnya</MenuItem>
|
|
||||||
</CustomTextField>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={6}>
|
|
||||||
<Typography variant='body2' className='mb-2 font-medium'>
|
|
||||||
Maksimal Piutang
|
|
||||||
<i className='tabler-help-circle text-gray-400 ml-1' />
|
|
||||||
</Typography>
|
|
||||||
<CustomTextField fullWidth type='number' defaultValue='0' placeholder='0' />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Kena Pajak */}
|
|
||||||
<div className='mb-4'>
|
|
||||||
<Typography variant='body2' className='mb-3 font-medium'>
|
|
||||||
Kena pajak ?
|
|
||||||
</Typography>
|
|
||||||
<div className='flex items-center'>
|
|
||||||
<input
|
|
||||||
type='checkbox'
|
|
||||||
className='toggle-switch'
|
|
||||||
style={{
|
|
||||||
appearance: 'none',
|
|
||||||
width: '60px',
|
|
||||||
height: '30px',
|
|
||||||
backgroundColor: '#3b82f6',
|
|
||||||
borderRadius: '15px',
|
|
||||||
position: 'relative',
|
|
||||||
cursor: 'pointer',
|
|
||||||
outline: 'none'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<style>
|
|
||||||
{`
|
|
||||||
.toggle-switch::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 3px;
|
|
||||||
left: 3px;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 50%;
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
.toggle-switch:checked::before {
|
|
||||||
transform: translateX(30px);
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Grid container spacing={6} className='mt-4'>
|
|
||||||
<Grid size={6}>
|
|
||||||
<Typography variant='body2' className='mb-2'>
|
|
||||||
Nomor
|
|
||||||
</Typography>
|
|
||||||
<CustomTextField fullWidth placeholder='Nomor' />
|
|
||||||
</Grid>
|
|
||||||
<Grid size={6}>
|
|
||||||
<Typography variant='body2' className='mb-2'>
|
|
||||||
Tanggal Lahir
|
|
||||||
</Typography>
|
|
||||||
<CustomTextField fullWidth type='date' placeholder='Tanggal Lahir' />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<div className='mt-4'>
|
|
||||||
<Typography variant='body2' className='mb-2'>
|
|
||||||
Deskripsi
|
|
||||||
</Typography>
|
|
||||||
<CustomTextField fullWidth placeholder='Deskripsi' multiline rows={3} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Button Sembunyikan di dalam konten */}
|
|
||||||
<div className='flex items-center gap-3 mt-6 mb-6' onClick={() => setShowMore(false)}>
|
|
||||||
<i className='tabler-minus text-blue-500' />
|
|
||||||
<Typography variant='body1' color='primary' className='cursor-pointer'>
|
|
||||||
Sembunyikan
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -679,10 +406,10 @@ const AddVendorDrawer = (props: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className='flex items-center gap-4'>
|
<div className='flex items-center gap-4'>
|
||||||
<Button variant='contained' type='submit' form='vendor-form'>
|
<Button variant='contained' type='submit' form='vendor-form' disabled={isSubmitting}>
|
||||||
Simpan
|
{isSubmitting ? (isEditMode ? 'Mengupdate...' : 'Menyimpan...') : isEditMode ? 'Update' : 'Simpan'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='tonal' color='error' onClick={() => handleReset()}>
|
<Button variant='outlined' color='error' onClick={handleReset} disabled={isSubmitting}>
|
||||||
Batal
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -691,4 +418,4 @@ const AddVendorDrawer = (props: Props) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AddVendorDrawer
|
export default AddEditVendorDrawer
|
||||||
|
|||||||
142
src/views/apps/vendor/list/VendorListTable.tsx
vendored
142
src/views/apps/vendor/list/VendorListTable.tsx
vendored
@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
// React Imports
|
// React Imports
|
||||||
import { useEffect, useState, useMemo } from 'react'
|
import { useEffect, useState, useMemo, useCallback } from 'react'
|
||||||
|
|
||||||
// Next Imports
|
// Next Imports
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
@ -58,6 +58,9 @@ import { getLocalizedUrl } from '@/utils/i18n'
|
|||||||
// Style Imports
|
// Style Imports
|
||||||
import tableStyles from '@core/styles/table.module.css'
|
import tableStyles from '@core/styles/table.module.css'
|
||||||
import { formatCurrency } from '@/utils/transform'
|
import { formatCurrency } from '@/utils/transform'
|
||||||
|
import { useVendors } from '@/services/queries/vendor'
|
||||||
|
import { Vendor } from '@/types/services/vendor'
|
||||||
|
import Loading from '@/components/layout/shared/Loading'
|
||||||
|
|
||||||
declare module '@tanstack/table-core' {
|
declare module '@tanstack/table-core' {
|
||||||
interface FilterFns {
|
interface FilterFns {
|
||||||
@ -68,7 +71,7 @@ declare module '@tanstack/table-core' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type VendorTypeWithAction = VendorType & {
|
type VendorTypeWithAction = Vendor & {
|
||||||
action?: string
|
action?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,17 +123,37 @@ const DebouncedInput = ({
|
|||||||
// Column Definitions
|
// Column Definitions
|
||||||
const columnHelper = createColumnHelper<VendorTypeWithAction>()
|
const columnHelper = createColumnHelper<VendorTypeWithAction>()
|
||||||
|
|
||||||
const VendorListTable = ({ tableData }: { tableData?: VendorType[] }) => {
|
const VendorListTable = () => {
|
||||||
// States
|
// States
|
||||||
const [addVendorOpen, setAddVendorOpen] = useState(false)
|
const [addVendorOpen, setAddVendorOpen] = useState(false)
|
||||||
const [rowSelection, setRowSelection] = useState({})
|
const [rowSelection, setRowSelection] = useState({})
|
||||||
const [data, setData] = useState(...[tableData])
|
|
||||||
const [filteredData, setFilteredData] = useState(data)
|
|
||||||
const [globalFilter, setGlobalFilter] = useState('')
|
const [globalFilter, setGlobalFilter] = useState('')
|
||||||
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
const [pageSize, setPageSize] = useState(10)
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
|
||||||
|
const { data, isLoading, error, isFetching } = useVendors({
|
||||||
|
page: currentPage,
|
||||||
|
limit: pageSize,
|
||||||
|
search
|
||||||
|
})
|
||||||
|
|
||||||
|
const vendors = data?.vendors ?? []
|
||||||
|
const totalCount = data?.total_count ?? 0
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
const { lang: locale } = useParams()
|
const { lang: locale } = useParams()
|
||||||
|
|
||||||
|
const handlePageChange = useCallback((event: unknown, newPage: number) => {
|
||||||
|
setCurrentPage(newPage)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newPageSize = parseInt(event.target.value, 10)
|
||||||
|
setPageSize(newPageSize)
|
||||||
|
setCurrentPage(1) // Reset to first page
|
||||||
|
}, [])
|
||||||
|
|
||||||
const columns = useMemo<ColumnDef<VendorTypeWithAction, any>[]>(
|
const columns = useMemo<ColumnDef<VendorTypeWithAction, any>[]>(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@ -155,103 +178,64 @@ const VendorListTable = ({ tableData }: { tableData?: VendorType[] }) => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
columnHelper.accessor('name', {
|
columnHelper.accessor('contact_person', {
|
||||||
header: 'Vendor',
|
header: 'Vendor',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className='flex items-center gap-4'>
|
<div className='flex items-center gap-4'>
|
||||||
{getAvatar({ photo: row.original.photo, name: row.original.name })}
|
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<Link href={getLocalizedUrl(`/apps/vendor/detail`, locale as Locale)}>
|
<Link href={getLocalizedUrl(`/apps/vendor/${row.original.id}/detail`, locale as Locale)}>
|
||||||
<Typography color='primary' className='font-medium cursor-pointer hover:underline'>
|
<Typography className='font-medium cursor-pointer hover:underline text-primary'>
|
||||||
{row.original.name}
|
{row.original.contact_person}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Link>
|
|
||||||
<Typography variant='body2'>{row.original.email}</Typography>
|
<Typography variant='body2'>{row.original.email}</Typography>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('company', {
|
columnHelper.accessor('name', {
|
||||||
header: 'Perusahaan',
|
header: 'Perusahaan',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Icon className='tabler-building' sx={{ color: 'var(--mui-palette-primary-main)' }} />
|
<Icon className='tabler-building' sx={{ color: 'var(--mui-palette-primary-main)' }} />
|
||||||
<Typography color='text.primary'>{row.original.company}</Typography>
|
<Typography color='text.primary'>{row.original.name}</Typography>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('telephone', {
|
columnHelper.accessor('phone_number', {
|
||||||
header: 'Telepon',
|
header: 'Telepon',
|
||||||
cell: ({ row }) => <Typography color='text.primary'>{row.original.telephone}</Typography>
|
cell: ({ row }) => <Typography color='text.primary'>{row.original.phone_number}</Typography>
|
||||||
}),
|
|
||||||
columnHelper.accessor('youPayable', {
|
|
||||||
header: () => <div className='text-right'>Anda Hutang</div>,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-right'>
|
|
||||||
<Typography color='text.primary' className='font-medium'>
|
|
||||||
{formatCurrency(row.original.youPayable)}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('theyPayable', {
|
|
||||||
header: () => <div className='text-right'>Mereka Hutang</div>,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-right'>
|
|
||||||
<Typography color='text.primary' className='font-medium'>
|
|
||||||
{formatCurrency(row.original.theyPayable)}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[data, filteredData]
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: filteredData as VendorType[],
|
data: vendors as Vendor[],
|
||||||
columns,
|
columns,
|
||||||
filterFns: {
|
filterFns: {
|
||||||
fuzzy: fuzzyFilter
|
fuzzy: fuzzyFilter
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
rowSelection,
|
rowSelection,
|
||||||
globalFilter
|
globalFilter,
|
||||||
},
|
|
||||||
initialState: {
|
|
||||||
pagination: {
|
pagination: {
|
||||||
pageSize: 10
|
pageIndex: currentPage,
|
||||||
|
pageSize
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enableRowSelection: true,
|
enableRowSelection: true,
|
||||||
globalFilterFn: fuzzyFilter,
|
|
||||||
onRowSelectionChange: setRowSelection,
|
onRowSelectionChange: setRowSelection,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
onGlobalFilterChange: setGlobalFilter,
|
manualPagination: true,
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
pageCount: Math.ceil(totalCount / pageSize)
|
||||||
getSortedRowModel: getSortedRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
getFacetedRowModel: getFacetedRowModel(),
|
|
||||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
|
||||||
getFacetedMinMaxValues: getFacetedMinMaxValues()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const getAvatar = (params: Pick<VendorType, 'photo' | 'name'>) => {
|
|
||||||
const { photo, name } = params
|
|
||||||
|
|
||||||
if (photo) {
|
|
||||||
return <CustomAvatar src={photo} size={34} />
|
|
||||||
} else {
|
|
||||||
return <CustomAvatar size={34}>{getInitials(name as string)}</CustomAvatar>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader title='Filter' className='pbe-4' />
|
{/* <TableFilters setData={setFilteredData} tableData={data} /> */}
|
||||||
<TableFilters setData={setFilteredData} tableData={data} />
|
|
||||||
<div className='flex justify-between flex-col items-start md:flex-row md:items-center p-6 border-bs gap-4'>
|
<div className='flex justify-between flex-col items-start md:flex-row md:items-center p-6 border-bs gap-4'>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
select
|
select
|
||||||
@ -265,8 +249,8 @@ const VendorListTable = ({ tableData }: { tableData?: VendorType[] }) => {
|
|||||||
</CustomTextField>
|
</CustomTextField>
|
||||||
<div className='flex flex-col sm:flex-row max-sm:is-full items-start sm:items-center gap-4'>
|
<div className='flex flex-col sm:flex-row max-sm:is-full items-start sm:items-center gap-4'>
|
||||||
<DebouncedInput
|
<DebouncedInput
|
||||||
value={globalFilter ?? ''}
|
value={search ?? ''}
|
||||||
onChange={value => setGlobalFilter(String(value))}
|
onChange={value => setSearch(value as string)}
|
||||||
placeholder='Cari Vendor'
|
placeholder='Cari Vendor'
|
||||||
className='max-sm:is-full'
|
className='max-sm:is-full'
|
||||||
/>
|
/>
|
||||||
@ -289,6 +273,9 @@ const VendorListTable = ({ tableData }: { tableData?: VendorType[] }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='overflow-x-auto'>
|
<div className='overflow-x-auto'>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loading />
|
||||||
|
) : (
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
{table.getHeaderGroups().map(headerGroup => (
|
{table.getHeaderGroups().map(headerGroup => (
|
||||||
@ -342,22 +329,27 @@ const VendorListTable = ({ tableData }: { tableData?: VendorType[] }) => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
)}
|
)}
|
||||||
</table>
|
</table>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<TablePagination
|
||||||
|
component={() => (
|
||||||
<TablePaginationComponent
|
<TablePaginationComponent
|
||||||
pageIndex={table.getState().pagination.pageIndex}
|
pageIndex={currentPage}
|
||||||
pageSize={table.getState().pagination.pageSize}
|
pageSize={pageSize}
|
||||||
totalCount={table.getFilteredRowModel().rows.length}
|
totalCount={totalCount}
|
||||||
onPageChange={(_, page) => {
|
onPageChange={handlePageChange}
|
||||||
table.setPageIndex(page)
|
/>
|
||||||
}}
|
)}
|
||||||
|
count={totalCount}
|
||||||
|
rowsPerPage={pageSize}
|
||||||
|
page={currentPage}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
onRowsPerPageChange={handlePageSizeChange}
|
||||||
|
rowsPerPageOptions={[10, 25, 50]}
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
<AddVendorDrawer
|
<AddVendorDrawer open={addVendorOpen} handleClose={() => setAddVendorOpen(!addVendorOpen)} />
|
||||||
open={addVendorOpen}
|
|
||||||
handleClose={() => setAddVendorOpen(!addVendorOpen)}
|
|
||||||
vendorData={data}
|
|
||||||
setData={setData}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user