diff --git a/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-orders/[id]/detail/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-orders/[id]/detail/page.tsx new file mode 100644 index 0000000..b96007f --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-orders/[id]/detail/page.tsx @@ -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 ( + + + + + + + + + ) +} + +export default PurchaseOrderDetailPage diff --git a/src/app/[lang]/(dashboard)/(private)/apps/vendor/detail/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/vendor/[id]/detail/page.tsx similarity index 100% rename from src/app/[lang]/(dashboard)/(private)/apps/vendor/detail/page.tsx rename to src/app/[lang]/(dashboard)/(private)/apps/vendor/[id]/detail/page.tsx diff --git a/src/components/StatusFilterTab.tsx b/src/components/StatusFilterTab.tsx index 861d2ae..9108a8e 100644 --- a/src/components/StatusFilterTab.tsx +++ b/src/components/StatusFilterTab.tsx @@ -7,6 +7,14 @@ import Menu from '@mui/material/Menu' import MenuItem from '@mui/material/MenuItem' 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 }) => ({ textTransform: 'none', fontWeight: 400, @@ -102,7 +110,7 @@ const StatusFilterTabs: React.FC = ({ }) }} > - {status} + {toTitleCase(status)} ))} @@ -135,7 +143,7 @@ const StatusFilterTabs: React.FC = ({ }) }} > - {status} + {toTitleCase(status)} ))} @@ -158,7 +166,7 @@ const StatusFilterTabs: React.FC = ({ }) }} > - {isDropdownItemSelected ? selectedStatus : dropdownLabel} + {isDropdownItemSelected ? toTitleCase(selectedStatus) : dropdownLabel} = ({ color: selectedStatus === status ? 'primary.main' : 'text.primary' }} > - {status} + {toTitleCase(status)} ))} diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index 00e3a3d..8359ee6 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -91,27 +91,27 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { {dictionary['navigation'].dailyReport} - }> + {/* }> {dictionary['navigation'].overview} {dictionary['navigation'].invoices} {dictionary['navigation'].deliveries} {dictionary['navigation'].sales_orders} {dictionary['navigation'].quotes} - + */} }> {dictionary['navigation'].overview} - + {/* {dictionary['navigation'].purchase_bills} {dictionary['navigation'].purchase_delivery} - + */} {dictionary['navigation'].purchase_orders} - + {/* {dictionary['navigation'].purchase_quotes} - + */} getDefaultMiddleware({ serializableCheck: false }) }) diff --git a/src/redux-store/slices/vendor.ts b/src/redux-store/slices/vendor.ts new file mode 100644 index 0000000..e9e67ae --- /dev/null +++ b/src/redux-store/slices/vendor.ts @@ -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) => { + state.currentVendor = action.payload + }, + resetVendor: state => { + state.currentVendor = initialState.currentVendor + } + } +}) + +export const { setVendor, resetVendor } = VendorSlice.actions + +export default VendorSlice.reducer diff --git a/src/services/api.ts b/src/services/api.ts index beec3a6..7d72506 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -6,7 +6,7 @@ const getToken = () => { } export const api = axios.create({ - baseURL: 'https://api-pos.apskel.id/api/v1', + baseURL: 'http://127.0.0.1:4000/api/v1', headers: { 'Content-Type': 'application/json' }, diff --git a/src/services/mutations/account.ts b/src/services/mutations/account.ts new file mode 100644 index 0000000..1f8616c --- /dev/null +++ b/src/services/mutations/account.ts @@ -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 } +} diff --git a/src/services/mutations/purchaseOrder.ts b/src/services/mutations/purchaseOrder.ts new file mode 100644 index 0000000..7ec250f --- /dev/null +++ b/src/services/mutations/purchaseOrder.ts @@ -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 } +} diff --git a/src/services/mutations/unitConventor.ts b/src/services/mutations/unitConventor.ts new file mode 100644 index 0000000..7fb0e4c --- /dev/null +++ b/src/services/mutations/unitConventor.ts @@ -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 } +} diff --git a/src/services/mutations/vendor.ts b/src/services/mutations/vendor.ts new file mode 100644 index 0000000..499d42d --- /dev/null +++ b/src/services/mutations/vendor.ts @@ -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 } +} diff --git a/src/services/queries/account.ts b/src/services/queries/account.ts new file mode 100644 index 0000000..bcb8aa3 --- /dev/null +++ b/src/services/queries/account.ts @@ -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({ + 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 + } + }) +} diff --git a/src/services/queries/chartOfAccount.ts b/src/services/queries/chartOfAccount.ts new file mode 100644 index 0000000..241e8ef --- /dev/null +++ b/src/services/queries/chartOfAccount.ts @@ -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({ + 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 + } + }) +} diff --git a/src/services/queries/chartOfAccountType.ts b/src/services/queries/chartOfAccountType.ts new file mode 100644 index 0000000..504c9f0 --- /dev/null +++ b/src/services/queries/chartOfAccountType.ts @@ -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({ + 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 +} diff --git a/src/services/queries/ingredients.ts b/src/services/queries/ingredients.ts index f355566..1885f6b 100644 --- a/src/services/queries/ingredients.ts +++ b/src/services/queries/ingredients.ts @@ -1,6 +1,7 @@ import { useQuery } from '@tanstack/react-query' import { Ingredients } from '../../types/services/ingredient' import { api } from '../api' +import { Ingredient } from '@/types/services/productRecipe' interface IngredientsQueryParams { page?: number @@ -34,3 +35,13 @@ export function useIngredients(params: IngredientsQueryParams = {}) { } }) } + +export function useIngredientById(id: string) { + return useQuery({ + queryKey: ['ingredients', id], + queryFn: async () => { + const res = await api.get(`/ingredients/${id}`) + return res.data.data + } + }) +} diff --git a/src/services/queries/purchaseOrder.ts b/src/services/queries/purchaseOrder.ts new file mode 100644 index 0000000..b20262e --- /dev/null +++ b/src/services/queries/purchaseOrder.ts @@ -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({ + 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({ + queryKey: ['purchase-orders', id], + queryFn: async () => { + const res = await api.get(`/purchase-orders/${id}`) + return res.data.data + } + }) +} diff --git a/src/services/queries/unitConverter.ts b/src/services/queries/unitConverter.ts new file mode 100644 index 0000000..8615bad --- /dev/null +++ b/src/services/queries/unitConverter.ts @@ -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({ + queryKey: ['unit-converters/ingredient', IngredientId], + queryFn: async () => { + const res = await api.get(`/unit-converters/ingredient/${IngredientId}`) + return res.data.data + } + }) +} diff --git a/src/services/queries/vendor.ts b/src/services/queries/vendor.ts new file mode 100644 index 0000000..47aeb4d --- /dev/null +++ b/src/services/queries/vendor.ts @@ -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({ + 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({ + queryKey: ['vendors/active'], + queryFn: async () => { + const res = await api.get(`/vendors/active`) + return res.data.data + } + }) +} + +export function useVendorById(id: string) { + return useQuery({ + queryKey: ['vendors', id], + queryFn: async () => { + const res = await api.get(`/vendors/${id}`) + return res.data.data + } + }) +} diff --git a/src/types/apps/purchaseOrderTypes.ts b/src/types/apps/purchaseOrderTypes.ts index 987560f..3fece1c 100644 --- a/src/types/apps/purchaseOrderTypes.ts +++ b/src/types/apps/purchaseOrderTypes.ts @@ -10,49 +10,6 @@ export type PurchaseOrderType = { 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 { id: string type: string diff --git a/src/types/services/chartOfAccount.ts b/src/types/services/chartOfAccount.ts new file mode 100644 index 0000000..ece2a46 --- /dev/null +++ b/src/types/services/chartOfAccount.ts @@ -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 +} diff --git a/src/types/services/productRecipe.ts b/src/types/services/productRecipe.ts index 6a43cc2..8185cbe 100644 --- a/src/types/services/productRecipe.ts +++ b/src/types/services/productRecipe.ts @@ -1,56 +1,103 @@ export interface Product { - ID: string; - OrganizationID: string; - CategoryID: string; - SKU: string; - Name: string; - Description: string | null; - Price: number; - Cost: number; - BusinessType: string; - ImageURL: string; - PrinterType: string; - UnitID: string | null; - HasIngredients: boolean; - Metadata: Record; - IsActive: boolean; - CreatedAt: string; // ISO date string - UpdatedAt: string; // ISO date string + ID: string + OrganizationID: string + CategoryID: string + SKU: string + Name: string + Description: string | null + Price: number + Cost: number + BusinessType: string + ImageURL: string + PrinterType: string + UnitID: string | null + HasIngredients: boolean + Metadata: Record + IsActive: boolean + CreatedAt: string // ISO date string + UpdatedAt: string // ISO date string } export interface Ingredient { - id: string; - organization_id: string; - outlet_id: string | null; - name: string; - unit_id: string; - cost: number; - stock: number; - is_semi_finished: boolean; - is_active: boolean; - metadata: Record; - created_at: string; - updated_at: string; + id: string + organization_id: string + outlet_id: string | null + name: string + unit_id: string + cost: number + stock: number + is_semi_finished: boolean + is_active: boolean + metadata: Record + created_at: string + updated_at: string + unit: IngredientUnit } export interface ProductRecipe { - id: string; - organization_id: string; - outlet_id: string | null; - product_id: string; - variant_id: string | null; - ingredient_id: string; - quantity: number; - created_at: string; - updated_at: string; - product: Product; - ingredient: Ingredient; + id: string + organization_id: string + outlet_id: string | null + product_id: string + variant_id: string | null + ingredient_id: string + quantity: number + waste: number + created_at: string + updated_at: string + product: Product + ingredient: Ingredient } export interface ProductRecipeRequest { - product_id: string; - variant_id: string | null; - ingredient_id: string; - quantity: number; - outlet_id: string | null; + product_id: string + variant_id: string | null + ingredient_id: string + quantity: number + 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 } diff --git a/src/types/services/purchaseOrder.ts b/src/types/services/purchaseOrder.ts new file mode 100644 index 0000000..92a3def --- /dev/null +++ b/src/types/services/purchaseOrder.ts @@ -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 +} diff --git a/src/types/services/vendor.ts b/src/types/services/vendor.ts new file mode 100644 index 0000000..2d098ae --- /dev/null +++ b/src/types/services/vendor.ts @@ -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 +} diff --git a/src/views/apps/account/AccountFormDrawer.tsx b/src/views/apps/account/AccountFormDrawer.tsx index b6f7512..2651457 100644 --- a/src/views/apps/account/AccountFormDrawer.tsx +++ b/src/views/apps/account/AccountFormDrawer.tsx @@ -5,7 +5,6 @@ import { useState, useEffect } from 'react' import Button from '@mui/material/Button' import Drawer from '@mui/material/Drawer' import IconButton from '@mui/material/IconButton' -import MenuItem from '@mui/material/MenuItem' import Typography from '@mui/material/Typography' import Box from '@mui/material/Box' @@ -15,64 +14,52 @@ import { useForm, Controller } from 'react-hook-form' // Component Imports import CustomTextField from '@core/components/mui/TextField' import CustomAutocomplete from '@/@core/components/mui/Autocomplete' - -// Account Type -export type AccountType = { - id: number - code: string - name: string - category: string - balance: string -} +import { AccountRequest } from '@/services/queries/chartOfAccountType' +import { useChartOfAccount } from '@/services/queries/chartOfAccount' +import { Account, ChartOfAccount } from '@/types/services/chartOfAccount' +import { useAccountsMutation } from '@/services/mutations/account' type Props = { open: boolean handleClose: () => void - accountData?: AccountType[] - setData: (data: AccountType[]) => void - editingAccount?: AccountType | null + accountData?: Account[] + setData: (data: Account[]) => void + editingAccount?: Account | null } type FormValidateType = { name: string code: string - category: string - parentAccount?: string + account_type: 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 const initialData = { name: '', code: '', - category: '', - parentAccount: '' + account_type: '', + 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) => { // Props const { open, handleClose, accountData, setData, editingAccount } = props @@ -80,6 +67,28 @@ const AccountFormDrawer = (props: Props) => { // Determine if we're editing 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 const { control, @@ -97,9 +106,11 @@ const AccountFormDrawer = (props: Props) => { // Populate form with existing data resetForm({ name: editingAccount.name, - code: editingAccount.code, - category: editingAccount.category, - parentAccount: '' + code: editingAccount.number, + account_type: editingAccount.account_type, + opening_balance: editingAccount.opening_balance, + description: editingAccount.description || '', + chart_of_account_id: editingAccount.chart_of_account_id }) } else { // Reset to initial data for new account @@ -110,35 +121,40 @@ const AccountFormDrawer = (props: Props) => { const onSubmit = (data: FormValidateType) => { if (isEdit && editingAccount) { - // Update existing account - const updatedAccounts = - accountData?.map(account => - account.id === editingAccount.id - ? { - ...account, - code: data.code, - name: data.name, - category: data.category - } - : account - ) || [] - - setData(updatedAccounts) - } else { - // Create new account - const newAccount: AccountType = { - id: accountData?.length ? Math.max(...accountData.map(a => a.id)) + 1 : 1, - code: data.code, + const accountRequest: AccountRequest = { + chart_of_account_id: data.chart_of_account_id, name: data.name, - category: data.category, - balance: '0' + number: data.code, + account_type: data.account_type, + opening_balance: data.opening_balance, + description: data.description } - - setData([...(accountData ?? []), newAccount]) + updateAccount.mutate( + { id: editingAccount.id, payload: accountRequest }, + { + onSuccess: () => { + handleClose() + 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) + } + }) } - - handleClose() - resetForm(initialData) } const handleReset = () => { @@ -225,57 +241,129 @@ const AccountFormDrawer = (props: Props) => { /> - {/* Kategori */} + {/* Tipe Akun */}
- Kategori * + Tipe Akun * ( onChange(newValue || '')} + options={accountTypeOptions} + value={accountTypeOptions.find(option => option.code === value) || null} + onChange={(_, newValue) => onChange(newValue?.code || '')} + getOptionLabel={option => option.name} + renderOption={(props, option) => ( + +
+ {option.name} +
+
+ )} renderInput={params => ( )} - isOptionEqualToValue={(option, value) => option === value} + isOptionEqualToValue={(option, value) => option.code === value.code} /> )} />
- {/* Sub Akun dari */} + {/* Chart of Account */}
- Sub Akun dari + Chart of Account * ( `${account.code} ${account.name}` === value) || null} - onChange={(_, newValue) => onChange(newValue ? `${newValue.code} ${newValue.name}` : '')} - getOptionLabel={option => `${option.code} ${option.name}`} - renderInput={params => } - isOptionEqualToValue={(option, value) => - `${option.code} ${option.name}` === `${value.code} ${value.name}` - } + loading={isLoadingAccounts} + options={chartOfAccountOptions} + value={chartOfAccountOptions.find(option => option.id === value) || null} + onChange={(_, newValue) => onChange(newValue?.id || '')} + getOptionLabel={option => `${option.code} - ${option.name}`} + renderOption={(props, option) => ( + +
+ + {option.code} - {option.name} + + {option.description && ( + + {option.description} + + )} +
+
+ )} + renderInput={params => ( + + )} + isOptionEqualToValue={(option, value) => option.id === value.id} + disabled={isLoadingAccounts} + noOptionsText={isLoadingAccounts ? 'Loading...' : 'Tidak ada chart of account tersedia'} /> )} />
+ + {/* Opening Balance */} +
+ + Saldo Awal * + + ( + 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.' + })} + /> + )} + /> +
+ + {/* Deskripsi */} +
+ + Deskripsi + + ( + + )} + /> +
diff --git a/src/views/apps/account/AccountListTable.tsx b/src/views/apps/account/AccountListTable.tsx index 0df4835..5ceabc8 100644 --- a/src/views/apps/account/AccountListTable.tsx +++ b/src/views/apps/account/AccountListTable.tsx @@ -41,6 +41,11 @@ import TablePaginationComponent from '@/components/TablePaginationComponent' import Loading from '@/components/layout/shared/Loading' import { getLocalizedUrl } from '@/utils/i18n' 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 export type AccountType = { @@ -60,119 +65,10 @@ declare module '@tanstack/table-core' { } } -type AccountTypeWithAction = AccountType & { +type AccountTypeWithAction = Account & { 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 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 const columnHelper = createColumnHelper() @@ -261,53 +147,25 @@ const AccountListTable = () => { // States const [addAccountOpen, setAddAccountOpen] = useState(false) const [rowSelection, setRowSelection] = useState({}) - const [currentPage, setCurrentPage] = useState(0) + const [currentPage, setCurrentPage] = useState(1) const [pageSize, setPageSize] = useState(10) const [openConfirm, setOpenConfirm] = useState(false) const [accountId, setAccountId] = useState('') const [search, setSearch] = useState('') const [categoryFilter, setCategoryFilter] = useState('Semua') - const [filteredData, setFilteredData] = useState(accountsData) - const [data, setData] = useState(accountsData) - const [editingAccount, setEditingAccount] = useState(null) + const [editingAccount, setEditingAccount] = useState(null) + + const { data, isLoading } = useAccounts({ + page: currentPage, + limit: pageSize, + search + }) // Hooks const { lang: locale } = useParams() - // Get unique categories for filter - const categories = useMemo(() => { - 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 accounts = data?.data ?? [] + const totalCount = data?.total ?? 0 const handlePageChange = useCallback((event: unknown, newPage: number) => { setCurrentPage(newPage) @@ -319,12 +177,8 @@ const AccountListTable = () => { setCurrentPage(0) }, []) - const handleDelete = () => { - setOpenConfirm(false) - } - // 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 const target = event.target as HTMLElement 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', cell: ({ row }) => ( ) }), @@ -393,26 +251,21 @@ const AccountListTable = () => { ) }), - columnHelper.accessor('category', { + columnHelper.accessor('chart_of_account.name', { header: 'Kategori', cell: ({ row }) => ( - + + {row.original.chart_of_account.name} + ) }), - columnHelper.accessor('balance', { + columnHelper.accessor('current_balance', { header: 'Saldo', cell: ({ row }) => { - const balance = parseInt(row.original.balance) return ( - {balance < 0 ? '-' : ''} - {formatCurrency(row.original.balance)} + {row.original.current_balance < 0 ? '-' : ''} + {formatCurrency(row.original.current_balance)} ) } @@ -422,7 +275,7 @@ const AccountListTable = () => { ) const table = useReactTable({ - data: paginatedData as AccountType[], + data: accounts as Account[], columns, filterFns: { fuzzy: fuzzyFilter @@ -484,63 +337,67 @@ const AccountListTable = () => {
- - - {table.getHeaderGroups().map(headerGroup => ( - - {headerGroup.headers.map(header => ( - - ))} - - ))} - - {filteredData.length === 0 ? ( - - - - - - ) : ( - - {table.getRowModel().rows.map(row => { - return ( - handleRowClick(row.original, e)} - > - {row.getVisibleCells().map(cell => ( - - ))} - - ) - })} - - )} -
- {header.isPlaceholder ? null : ( - <> -
- {flexRender(header.column.columnDef.header, header.getContext())} - {{ - asc: , - desc: - }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} -
- - )} -
- Tidak ada data tersedia -
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+ {isLoading ? ( + + ) : ( + + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + {accounts.length === 0 ? ( + + + + + + ) : ( + + {table.getRowModel().rows.map(row => { + return ( + handleRowClick(row.original, e)} + > + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + + )} +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: + }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} +
+ + )} +
+ Tidak ada data tersedia +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+ )}
{ onPageChange={handlePageChange} onRowsPerPageChange={handlePageSizeChange} rowsPerPageOptions={[10, 25, 50]} + disabled={isLoading} /> {}} editingAccount={editingAccount} /> diff --git a/src/views/apps/cash-bank/CashBankList.tsx b/src/views/apps/cash-bank/CashBankList.tsx index 92aa8f5..ffda00a 100644 --- a/src/views/apps/cash-bank/CashBankList.tsx +++ b/src/views/apps/cash-bank/CashBankList.tsx @@ -11,16 +11,18 @@ import FormControl from '@mui/material/FormControl' import InputLabel from '@mui/material/InputLabel' import Select from '@mui/material/Select' import MenuItem from '@mui/material/MenuItem' +import CircularProgress from '@mui/material/CircularProgress' import CashBankCard from './CashBankCard' // Adjust import path as needed import CustomTextField from '@/@core/components/mui/TextField' import { getLocalizedUrl } from '@/utils/i18n' import { Locale } from '@/configs/i18n' import { useParams } from 'next/navigation' -import AccountFormDrawer, { AccountType } from '../account/AccountFormDrawer' -import { accountsData } from '../account/AccountListTable' +import AccountFormDrawer from '../account/AccountFormDrawer' 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 { id: string title: string @@ -41,188 +43,28 @@ interface BankAccount { status: 'active' | 'inactive' | 'blocked' } -// Dummy Data -const dummyAccounts: BankAccount[] = [ - { - id: '1', - title: 'Giro', - accountNumber: '1-10003', - balances: [ - { amount: '7.313.321', label: 'Saldo di bank' }, - { 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'], - chartColor: '#ff6b9d', - currency: 'IDR', - accountType: 'giro', - bank: 'Bank Mandiri', - status: 'active' - }, - { - 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 - ] - } - ], - 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' +// Static chart data for fallback/demo purposes +const generateChartData = (accountType: string, balance: number) => { + const baseValue = balance || 1000000 + const variation = baseValue * 0.2 + + return Array.from({ length: 12 }, (_, i) => { + const randomVariation = (Math.random() - 0.5) * variation + return Math.max(baseValue + randomVariation, baseValue * 0.5) + }) +} + +const getChartColor = (accountType: string) => { + const colors = { + giro: '#ff6b9d', + savings: '#4285f4', + investment: '#00bcd4', + credit: '#ff9800', + cash: '#4caf50' } -] + return colors[accountType as keyof typeof colors] || '#757575' +} + const DebouncedInput = ({ value: initialValue, onChange, @@ -251,28 +93,122 @@ const DebouncedInput = ({ return setValue(e.target.value)} /> } + const CashBankList = () => { const [searchQuery, setSearchQuery] = useState('') - const [editingAccount, setEditingAccount] = useState(null) + const [editingAccount, setEditingAccount] = useState(null) const [addAccountOpen, setAddAccountOpen] = useState(false) - const [data, setData] = useState(accountsData) + const [data, setData] = useState([]) const { lang: locale } = useParams() + // Use the accounts hook with search parameter + const { data: accountsResponse, isLoading } = useAccounts({ + page: 1, + limit: 10, + search: searchQuery + }) + const handleCloseDrawer = () => { setAddAccountOpen(false) 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(() => { - 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 = account.title.toLowerCase().includes(searchQuery.toLowerCase()) || account.accountNumber.toLowerCase().includes(searchQuery.toLowerCase()) || account.bank.toLowerCase().includes(searchQuery.toLowerCase()) return matchesSearch }) - }, [searchQuery]) + }, [transformedAccounts, searchQuery, accountsResponse]) return ( <> @@ -283,8 +219,16 @@ const CashBankList = () => { setSearchQuery(value as string)} - placeholder='Cari ' + placeholder='Cari akun...' className='max-sm:is-full' + disabled={isLoading} + InputProps={{ + startAdornment: ( + + + + ) + }} /> @@ -302,45 +247,77 @@ const CashBankList = () => { + {/* Loading State */} + {isLoading && ( + + + + )} + {/* Account Cards */} - - {filteredAccounts.length > 0 ? ( - filteredAccounts.map(account => ( - - + {!isLoading && ( + + {filteredAccounts.length > 0 ? ( + filteredAccounts.map(account => ( + + + + )) + ) : ( + + + + {searchQuery ? 'Tidak ada akun yang ditemukan' : 'Belum ada akun'} + + + {searchQuery + ? 'Coba ubah kata kunci pencarian yang digunakan' + : 'Mulai dengan menambahkan akun baru'} + + - )) - ) : ( - - - - Tidak ada akun yang ditemukan - - - Coba ubah kata kunci pencarian atau filter yang digunakan - - - - )} - + )} + + )} + + {/* Error State (if needed) */} + {!isLoading && !accountsResponse && ( + + + + Terjadi kesalahan saat memuat data + + Silakan coba lagi atau hubungi administrator + + + )} + { @@ -55,23 +55,45 @@ const AddRecipeDrawer = (props: Props) => { const [ingredientDebouncedInput] = useDebounce(ingredientInput, 500) const [formData, setFormData] = useState(initialData) + // Add state untuk menyimpan selected ingredient + const [selectedIngredient, setSelectedIngredient] = useState(null) + const { data: outlets, isLoading: outletsLoading } = useOutlets({ search: outletDebouncedInput }) + + // Modifikasi query ingredients dengan enabled condition const { data: ingredients, isLoading: ingredientsLoading } = useIngredients({ search: ingredientDebouncedInput }) 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() useEffect(() => { if (currentProductRecipe.id) { 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) => { e.preventDefault() @@ -101,24 +123,16 @@ const AddRecipeDrawer = (props: Props) => { handleClose() dispatch(resetProductVariant()) setFormData(initialData) - } - - const handleInputChange = (e: any) => { - setFormData({ - ...formData, - [e.target.name]: e.target.value - }) + setSelectedIngredient(null) // Reset selected ingredient + setIngredientInput('') // Reset input } const setTitleDrawer = (recipe: any) => { const addOrEdit = currentProductRecipe.id ? 'Edit ' : 'Add ' - let title = 'Original' - if (recipe?.name) { title = recipe?.name } - return addOrEdit + title } @@ -144,13 +158,14 @@ const AddRecipeDrawer = (props: Props) => { Basic Information + option.name} value={outletOptions.find(p => p.id === formData.outlet_id) || null} - onInputChange={(event, newOutlettInput) => { - setOutletInput(newOutlettInput) + onInputChange={(event, newOutletInput) => { + setOutletInput(newOutletInput) }} onChange={(event, newValue) => { setFormData({ @@ -161,7 +176,6 @@ const AddRecipeDrawer = (props: Props) => { renderInput={params => ( { /> )} /> + + {/* Perbaiki Autocomplete untuk Ingredients */} option.name} - value={ingredientOptions?.find(p => p.id === formData.ingredient_id) || null} + value={selectedIngredient} onInputChange={(event, newIngredientInput) => { setIngredientInput(newIngredientInput) }} onChange={(event, newValue) => { + setSelectedIngredient(newValue) // Set selected ingredient setFormData({ ...formData, 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 => ( { /> )} /> + + {/* Unit Field - Disabled, value from selected ingredient */} + + { value={formData.quantity} onChange={e => setFormData({ ...formData, quantity: Number(e.target.value) })} /> + + setFormData({ ...formData, waste: Number(e.target.value) })} + /> +
+ {!ingredientId && ( + + + Warning: Ingredient data is required for conversion + + + )} + {ingredientId && ( + + + Converting for: {data?.name || `Ingredient ${ingredientId}`} + + {ingredientCost > 0 && ( + + Base cost per {units?.data.find(u => u.id === toUnitId)?.name || 'unit'}: Rp{' '} + {formatNumber(ingredientCost)} + + )} + + )} {/* Scrollable Content */} -
onSubmit(data))}> +
{/* Header Kolom */} - Satuan + From Unit @@ -175,20 +221,20 @@ const IngedientUnitConversionDrawer = (props: Props) => { - Jumlah + Factor - Unit + To Unit - + Harga Beli - + Harga Jual @@ -198,146 +244,223 @@ const IngedientUnitConversionDrawer = (props: Props) => { Default - - - Action - + + + {/* Form Input Row */} + + {/* From Unit (Satuan) */} + +
+ + 1 + + ( + { + field.onChange(e.target.value) + handleChangeConversion('satuan', e.target.value) + }} + > + {units?.data + .filter(unit => unit.id !== toUnitId) // Prevent selecting same unit as target + .map(unit => ( + + {unit.name} + + )) ?? []} + + )} + /> +
+ {errors.satuan && ( + + {errors.satuan.message} + + )} +
+ + {/* Tanda sama dengan */} + + = + + + {/* Conversion Factor (Quantity) */} + + ( + { + const value = parseFloat(e.target.value) || 0 + field.onChange(value) + handleChangeConversion('quantity', value) + }} + /> + )} + /> + {errors.quantity && ( + + {errors.quantity.message} + + )} + + + {/* To Unit - Disabled because it comes from data */} + + + {units?.data.map(unit => ( + + {unit.name} + + )) ?? []} + + + + {/* Harga Beli - Calculated as factor * ingredientCost */} + + + + + {/* Harga Jual */} + + ( + { + const value = parseNumber(e.target.value) + field.onChange(value) + handleChangeConversion('hargaJual', value) + }} + placeholder='Optional' + /> + )} + /> + {errors.hargaJual && ( + + {errors.hargaJual.message} + + )} + + + {/* Default Star */} + + handleChangeConversion('isDefault', !conversion.isDefault)} + sx={{ + color: conversion.isDefault ? 'warning.main' : 'grey.400' + }} + > + +
- {/* Baris Konversi */} - {conversions.map((conversion, index) => ( - - - - {index + 1} + {/* Conversion Preview */} + {conversion.quantity > 0 && conversion.satuan && toUnitId && ( + + + Conversion Preview: + + + 1 {units?.data.find(u => u.id === conversion.satuan)?.name || 'Unit'} ={' '} + + {conversion.quantity} {units?.data.find(u => u.id === toUnitId)?.name || 'Unit'} + + + + Conversion Factor: {conversion.quantity} + + + )} + + {/* Price Summary */} + {conversion.quantity > 0 && (ingredientCost > 0 || conversion.hargaJual > 0) && ( + + + Price Summary: + + {ingredientCost > 0 && ( + <> + + Total Purchase Price (1 {units?.data.find(u => u.id === conversion.satuan)?.name || 'From Unit'}): + Rp {formatNumber(totalPurchasePrice)} + + + Unit Cost per {units?.data.find(u => u.id === toUnitId)?.name || 'To Unit'}: Rp{' '} + {formatNumber(ingredientCost)} + + + )} + {conversion.hargaJual > 0 && ( + <> + + Total Selling Price (1 {units?.data.find(u => u.id === conversion.satuan)?.name || 'From Unit'}): + Rp {formatNumber(conversion.hargaJual)} + + + Unit Selling Price per {units?.data.find(u => u.id === toUnitId)?.name || 'To Unit'}: Rp{' '} + {formatNumber(Math.round(conversion.hargaJual / conversion.quantity))} + + + )} + {ingredientCost > 0 && conversion.hargaJual > 0 && ( + + Total Margin: Rp {formatNumber(conversion.hargaJual - totalPurchasePrice)} ( + {totalPurchasePrice > 0 + ? (((conversion.hargaJual - totalPurchasePrice) / totalPurchasePrice) * 100).toFixed(1) + : 0} + %) - - - {/* Satuan */} - - handleChangeConversion(index, 'satuan', e.target.value)} - > - Box - Kg - Liter - Pack - Pcs - - - - {/* Tanda sama dengan */} - - = - - - {/* Quantity */} - - handleChangeConversion(index, 'quantity', parseInt(e.target.value) || 0)} - /> - - - {/* Unit */} - - handleChangeConversion(index, 'unit', e.target.value)} - > - Pcs - Kg - Gram - Liter - ML - - - - {/* Harga Beli */} - - handleChangeConversion(index, 'hargaBeli', parseNumber(e.target.value))} - /> - - - {/* Harga Jual */} - - handleChangeConversion(index, 'hargaJual', parseNumber(e.target.value))} - /> - - - {/* Default Star */} - - handleToggleDefault(index)} - sx={{ - color: conversion.isDefault ? 'warning.main' : 'grey.400' - }} - > - - - - - {/* Delete Button */} - - {conversions.length > 1 && ( - handleHapusBaris(index)} - sx={{ - color: 'error.main', - border: 1, - borderColor: 'error.main', - '&:hover': { - backgroundColor: 'error.light', - borderColor: 'error.main' - } - }} - > - - - )} - - - ))} - - {/* Tambah Baris Button */} -
- -
+ )} + + )}
@@ -355,13 +478,21 @@ const IngedientUnitConversionDrawer = (props: Props) => { }} >
- -
+ {!isValidForSubmit && ( + + Please fill in all required fields: {!ingredientId && 'Ingredient Data, '} + {!conversion.satuan && 'From Unit, '} + {!toUnitId && 'To Unit (from ingredient data), '} + {conversion.quantity <= 0 && 'Conversion Factor'} + + )} ) diff --git a/src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailInfo.tsx b/src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailInfo.tsx index 1774198..355a092 100644 --- a/src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailInfo.tsx +++ b/src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailInfo.tsx @@ -1,14 +1,19 @@ +import { Ingredient } from '@/types/services/productRecipe' import { formatCurrency } from '@/utils/transform' import { Card, CardHeader, Chip, Typography } from '@mui/material' -const IngredientDetailInfo = () => { +interface Props { + data: Ingredient | undefined +} + +const IngredientDetailInfo = ({ data }: Props) => { return ( - Tepung Terigu + {data?.name ?? '-'} @@ -17,7 +22,7 @@ const IngredientDetailInfo = () => {
- Cost: {formatCurrency(5000)} + Cost: {formatCurrency(data?.cost ?? 0)}
diff --git a/src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailUnit.tsx b/src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailUnit.tsx index 05f4784..31e8cde 100644 --- a/src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailUnit.tsx +++ b/src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailUnit.tsx @@ -2,11 +2,19 @@ import React, { useState } from 'react' import { Card, CardContent, CardHeader, Typography, Button, Box, Stack } from '@mui/material' 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 const [openConversionDrawer, setOpenConversionDrawer] = useState(false) + const { data: unitConverters, isLoading } = useUnitConverterByIngredient(data?.id as string) + // Function untuk membuka drawer const handleOpenConversionDrawer = () => { setOpenConversionDrawer(true) @@ -34,9 +42,19 @@ const IngredientDetailUnit = () => { Satuan Dasar - : Pcs + : {data?.unit.name ?? '-'} + {unitConverters?.map(unitConverter => ( + + + 1 {unitConverter.from_unit.name} + + + : {unitConverter.conversion_factor} {unitConverter.to_unit.name} + + + )) ?? []} - +
diff --git a/src/views/apps/purchase/purchase-detail/PurchaseDetailContent.tsx b/src/views/apps/purchase/purchase-detail/PurchaseDetailContent.tsx index 391a0b3..7d41779 100644 --- a/src/views/apps/purchase/purchase-detail/PurchaseDetailContent.tsx +++ b/src/views/apps/purchase/purchase-detail/PurchaseDetailContent.tsx @@ -1,25 +1,40 @@ +'use client' + import Grid from '@mui/material/Grid2' import PurchaseDetailInformation from './PurchaseDetailInformation' import PurchaseDetailSendPayment from './PurchaseDetailSendPayment' import PurchaseDetailLog from './PurchaseDetailLog' 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 params = useParams() + const { data, isLoading, error, isFetching } = usePurchaseOrderById(params.id as string) return ( - - - - - - - - - - - - - - + <> + {isLoading ? ( + + ) : ( + + + + + {data?.status == 'sent' && ( + + + + )} + {/* + + + + + */} + + )} + ) } diff --git a/src/views/apps/purchase/purchase-detail/PurchaseDetailInformation.tsx b/src/views/apps/purchase/purchase-detail/PurchaseDetailInformation.tsx index 232b5c6..dfdae30 100644 --- a/src/views/apps/purchase/purchase-detail/PurchaseDetailInformation.tsx +++ b/src/views/apps/purchase/purchase-detail/PurchaseDetailInformation.tsx @@ -1,3 +1,5 @@ +'use client' + import React from 'react' import { Card, @@ -15,87 +17,62 @@ import { IconButton } from '@mui/material' import Grid from '@mui/material/Grid2' +import { PurchaseOrder } from '@/types/services/purchaseOrder' -interface Product { - produk: string - deskripsi: string - kuantitas: number - satuan: string - discount: string - harga: number - pajak: string - jumlah: number +interface Props { + data?: PurchaseOrder } -interface PurchaseData { - vendor: string - nomor: string - tglTransaksi: string - tglJatuhTempo: string - gudang: string - status: string -} +const PurchaseDetailInformation = ({ data }: Props) => { + const purchaseOrder = data -const PurchaseDetailInformation: React.FC = () => { - const purchaseData: PurchaseData = { - vendor: 'Bagas Rizki Sihotang S.Farm Widodo', - nomor: 'PI/00053', - tglTransaksi: '08/09/2025', - tglJatuhTempo: '06/10/2025', - gudang: 'Unassigned', - status: 'Belum Dibayar' + // Helper functions + const formatDate = (dateString: string): string => { + const date = new Date(dateString) + return date.toLocaleDateString('id-ID', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + }) } - 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 => { return new Intl.NumberFormat('id-ID').format(amount) } + const getStatusLabel = (status: string): string => { + const statusMap: Record = { + 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 = { + 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 ( - - Belum Dibayar + + {getStatusLabel(purchaseOrder?.status ?? '')} + + + {/* Shipping Information - Conditional */} + {formData.showShippingInfo && ( + <> + + ) => + handleInputChange('tanggalPengiriman', e.target.value) + } + InputLabelProps={{ + shrink: true + }} + /> + + + handleInputChange('ekspedisi', newValue)} + renderInput={params => ( + + )} + /> + + + ) => handleInputChange('noResi', e.target.value)} + /> + + + )} + + {/* Row 4 - Referensi, SKU, Switch Pajak */} + + ) => handleInputChange('referensi', e.target.value)} + /> + + + + + + ) => + handleInputChange('hargaTermasukPajak', e.target.checked) + } + color='primary' + /> + } + label='Harga termasuk pajak' + sx={{ + marginLeft: 0, + '& .MuiFormControlLabel-label': { + fontSize: '14px', + color: 'text.secondary' + } + }} + /> + + + ) +} + +export default PurchaseBasicInfo diff --git a/src/views/apps/purchase/purchase-form copy/PurchaseIngredientsTable.tsx b/src/views/apps/purchase/purchase-form copy/PurchaseIngredientsTable.tsx new file mode 100644 index 0000000..8514a0a --- /dev/null +++ b/src/views/apps/purchase/purchase-form copy/PurchaseIngredientsTable.tsx @@ -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 = ({ + 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 ( + + + Bahan Baku / Ingredients + + + + + + + Bahan Baku + Deskripsi + Kuantitas + Satuan + Discount + Harga + Pajak + Waste + Total + + + + + {formData.ingredientItems.map((item: IngredientItem, index: number) => ( + + + handleIngredientChange(index, 'ingredient', newValue)} + renderInput={params => } + /> + + + ) => + handleIngredientChange(index, 'deskripsi', e.target.value) + } + placeholder='Deskripsi' + /> + + + ) => + handleIngredientChange(index, 'kuantitas', parseInt(e.target.value) || 1) + } + inputProps={{ min: 1 }} + /> + + + handleIngredientChange(index, 'satuan', newValue)} + renderInput={params => } + /> + + + ) => + handleIngredientChange(index, 'discount', e.target.value) + } + placeholder='0%' + /> + + + ) => { + const value = e.target.value + + 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' + /> + + + handleIngredientChange(index, 'pajak', newValue)} + renderInput={params => } + /> + + + handleIngredientChange(index, 'waste', newValue)} + renderInput={params => } + /> + + + + + + removeIngredientItem(index)} + disabled={formData.ingredientItems.length === 1} + > + + + + + ))} + +
+
+ + {/* Add New Item Button */} + +
+ ) +} + +export default PurchaseIngredientsTable diff --git a/src/views/apps/purchase/purchase-form copy/PurchaseSummary.tsx b/src/views/apps/purchase/purchase-form copy/PurchaseSummary.tsx new file mode 100644 index 0000000..3bc71cf --- /dev/null +++ b/src/views/apps/purchase/purchase-form copy/PurchaseSummary.tsx @@ -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 = ({ 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 => { + // Simulate upload + return new Promise(resolve => { + setTimeout(() => { + resolve(URL.createObjectURL(file)) + }, 1000) + }) + } + + return ( + + + {/* Left Side - Pesan and Attachment */} + + {/* Pesan Section */} + + + {formData.showPesan && ( + + ) => handleInputChange('pesan', e.target.value)} + /> + + )} + + + {/* Attachment Section */} + + + {formData.showAttachment && ( + + )} + + + + {/* Right Side - Totals */} + + + {/* Sub Total */} + + + Sub Total + + + {new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(formData.subtotal || 0)} + + + + {/* Additional Options */} + + {/* Tambah Diskon */} + + + + {/* Show input form when showTambahDiskon is true */} + {formData.showTambahDiskon && ( + + + ) => + handleInputChange('discountValue', e.target.value) + } + sx={{ flex: 1 }} + InputProps={{ + endAdornment: + formData.discountType === 'percentage' ? ( + % + ) : undefined + }} + /> + { + if (newValue) handleInputChange('discountType', newValue) + }} + size='small' + > + + % + + + Rp + + + + + )} + + + {/* Biaya Pengiriman */} + + + + {/* Show input form when showBiayaPengiriman is true */} + {formData.showBiayaPengiriman && ( + + + Biaya pengiriman + + ) => + handleInputChange('shippingCost', e.target.value) + } + sx={{ flex: 1 }} + InputProps={{ + startAdornment: Rp + }} + /> + + )} + + + {/* Biaya Transaksi - Multiple */} + + + + {/* Show multiple transaction cost inputs */} + {formData.showBiayaTransaksi && ( + + {transactionCosts.map((cost: TransactionCost, index: number) => ( + + {/* Remove button */} + removeTransactionCost(cost.id)} + sx={{ + color: 'error.main', + border: '1px solid', + borderColor: 'error.main', + borderRadius: '50%', + width: 28, + height: 28, + '&:hover': { + backgroundColor: 'error.lighter' + } + }} + > + + + + {/* Type AutoComplete */} + (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 => ( + + )} + sx={{ minWidth: 180 }} + noOptionsText='Tidak ada pilihan' + /> + + {/* Name input */} + ) => + updateTransactionCost(cost.id, 'name', e.target.value) + } + sx={{ flex: 1 }} + /> + + {/* Amount input */} + ) => + updateTransactionCost(cost.id, 'amount', e.target.value) + } + sx={{ width: 120 }} + InputProps={{ + startAdornment: Rp + }} + /> + + ))} + + {/* Add more button */} + + + )} + + + + {/* Total */} + + + Total + + + {new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(total)} + + + + {/* Uang Muka */} + + + {formData.showUangMuka && ( + + + {/* Dropdown */} + (typeof option === 'string' ? option : option.label)} + value={{ label: '1-10003 Gi...', value: '1-10003' }} + onChange={(_, newValue) => { + // Handle change if needed + }} + renderInput={params => } + sx={{ minWidth: 120 }} + /> + + {/* Amount input */} + ) => + handleInputChange('downPayment', e.target.value) + } + sx={{ width: '80px' }} + inputProps={{ + style: { textAlign: 'center' } + }} + /> + + {/* Percentage/Fixed toggle */} + { + if (newValue) handleInputChange('downPaymentType', newValue) + }} + size='small' + > + + % + + + Rp + + + + + {/* Right side text */} + + Uang muka {downPayment > 0 ? downPayment.toLocaleString('id-ID') : '0'} + + + )} + + + {/* Sisa Tagihan */} + + + Sisa Tagihan + + + {new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(remainingBalance)} + + + + {/* Save Button */} + + + + + + ) +} + +export default PurchaseSummary diff --git a/src/views/apps/purchase/purchase-form/PurchaseAddForm.tsx b/src/views/apps/purchase/purchase-form/PurchaseAddForm.tsx index 7a158d9..89a74e2 100644 --- a/src/views/apps/purchase/purchase-form/PurchaseAddForm.tsx +++ b/src/views/apps/purchase/purchase-form/PurchaseAddForm.tsx @@ -1,118 +1,929 @@ 'use client' -import React, { useState } from 'react' -import { Card, CardContent } from '@mui/material' +import React, { useState, useMemo } from 'react' +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 { IngredientItem, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes' -import PurchaseBasicInfo from './PurchaseBasicInfo' -import PurchaseIngredientsTable from './PurchaseIngredientsTable' -import PurchaseSummary from './PurchaseSummary' +import CustomAutocomplete from '@/@core/components/mui/Autocomplete' +import CustomTextField from '@/@core/components/mui/TextField' +import ImageUpload from '@/components/ImageUpload' +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 ( + + + + + Harga beli terakhir + + + {new Intl.NumberFormat('id-ID').format(lastPrice)} + + + + + + + + + ) +} const PurchaseAddForm: React.FC = () => { + const [imageUrl, setImageUrl] = useState('') + const [errors, setErrors] = useState({}) + const [popoverState, setPopoverState] = useState({ + isOpen: false, + anchorEl: null, + itemIndex: null + }) const [formData, setFormData] = useState({ 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 + po_number: '', + transaction_date: '', + due_date: '', + reference: '', + status: 'sent', showPesan: false, showAttachment: false, - showTambahDiskon: false, - showBiayaPengiriman: false, - showBiayaTransaksi: false, - showUangMuka: false, - pesan: '', - // Ingredient items (updated from productItems) - ingredientItems: [ + message: '', + items: [ { id: 1, ingredient: null, - deskripsi: '', - kuantitas: 1, - satuan: null, - discount: '0', - harga: 0, - pajak: null, - waste: null, + description: '', + quantity: 1, + unit: null, + amount: 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, 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 => { setFormData(prev => ({ ...prev, [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 => { - const newItems = [...prev.ingredientItems] + const newItems = [...prev.items] newItems[index] = { ...newItems[index], [field]: value } - // Auto-calculate total if price or quantity changes - if (field === 'harga' || field === 'kuantitas') { + if (field === 'amount' || field === 'quantity') { 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 newItem: IngredientItem = { + const handleIngredientSelection = (index: number, selectedIngredient: any) => { + 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(), ingredient: null, - deskripsi: '', - kuantitas: 1, - satuan: null, - discount: '0%', - harga: 0, - pajak: null, - waste: null, + description: '', + quantity: 1, + unit: null, + amount: 0, total: 0 } setFormData(prev => ({ ...prev, - ingredientItems: [...prev.ingredientItems, newItem] + items: [...prev.items, newItem] })) } - const removeIngredientItem = (index: number): void => { + const removeItem = (index: number): void => { setFormData(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 => { + 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 ( + {errors.general && ( + + {errors.general} + + )} + - {/* Basic Info Section */} - + {/* BASIC INFO SECTION */} + + { + handleInputChange('vendor', newValue) + if (newValue?.value) { + const selectedVendorData = vendors?.find(vendor => vendor.id === newValue.value) + console.log('Vendor terpilih:', selectedVendorData) + } + }} + loading={isLoadingVendors} + renderInput={params => ( + + )} + /> + {getSelectedVendorData() && ( + + + + + {getSelectedVendorData()?.contact_person ?? ''} + + + + + + {getSelectedVendorData()?.address ?? '-'} + + + + + + {getSelectedVendorData()?.phone_number ?? '-'} + + + + )} + + + ) => handleInputChange('po_number', e.target.value)} + error={!!errors.po_number} + helperText={errors.po_number} + /> + - {/* Ingredients Table Section */} - + {/* Row 2 - Transaction Date, Due Date, Status */} + + ) => + handleInputChange('transaction_date', e.target.value) + } + InputLabelProps={{ + shrink: true + }} + error={!!errors.transaction_date} + helperText={errors.transaction_date} + /> + + + ) => handleInputChange('due_date', e.target.value)} + InputLabelProps={{ + shrink: true + }} + error={!!errors.due_date} + helperText={errors.due_date} + /> + + + ) => handleInputChange('reference', e.target.value)} + /> + - {/* Summary Section */} - + {/* ITEMS TABLE SECTION */} + + + Item Purchase Order + + + {errors.items && ( + + {errors.items} + + )} + + + + + + Bahan + Deskripsi + Kuantitas + Satuan + Harga + Total + + + + + {formData.items.map((item: PurchaseOrderFormItem, index: number) => ( + + + 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 => ( + + {isLoadingIngredients ? : null} + {params.InputProps.endAdornment} + + ) + }} + /> + )} + disabled={isLoadingIngredients} + /> + + + ) => + handleItemChange(index, 'description', e.target.value) + } + placeholder='Deskripsi' + /> + + + ) => + handleItemChange(index, 'quantity', parseInt(e.target.value) || 1) + } + inputProps={{ min: 1 }} + /> + + + handleItemChange(index, 'unit', newValue)} + loading={isLoadingUnits} + getOptionLabel={(option: any) => { + if (!option) return '' + return option.label || option.name || option.nama || '' + }} + renderInput={params => ( + + {isLoadingUnits ? : null} + {params.InputProps.endAdornment} + + ) + }} + /> + )} + disabled={isLoadingUnits} + /> + + + ) => { + 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' }} + /> + + + + + + removeItem(index)} + disabled={formData.items.length === 1} + > + + + + + ))} + +
+
+ + {/* Add New Item Button */} + +
+ + {/* SUMMARY SECTION */} + + + {/* Left Side - Message and Attachment */} + + {/* Message Section */} + + + {formData.showPesan && ( + + ) => + handleInputChange('message', e.target.value) + } + /> + + )} + + + {/* Attachment Section */} + + + {formData.showAttachment && ( + + )} + + + + {/* Right Side - Totals */} + + + {/* Sub Total */} + + + Sub Total + + + {new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(subtotal)} + + + + {/* Total */} + + + Total + + + {new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(subtotal)} + + + + {/* Save Button */} + + + + +
+ + {/* Price Popover */} +
) diff --git a/src/views/apps/purchase/purchase-form/PurchaseBasicInfo.tsx b/src/views/apps/purchase/purchase-form/PurchaseBasicInfo.tsx index 6cce349..dc33e61 100644 --- a/src/views/apps/purchase/purchase-form/PurchaseBasicInfo.tsx +++ b/src/views/apps/purchase/purchase-form/PurchaseBasicInfo.tsx @@ -1,11 +1,12 @@ 'use client' 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 CustomAutocomplete from '@/@core/components/mui/Autocomplete' import CustomTextField from '@/@core/components/mui/TextField' import { DropdownOption, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes' +import { useVendorActive } from '@/services/queries/vendor' interface PurchaseBasicInfoProps { formData: PurchaseOrderFormData @@ -13,12 +14,22 @@ interface PurchaseBasicInfoProps { } const PurchaseBasicInfo: React.FC = ({ 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 { data: vendors, isLoading } = useVendorActive() + + // Transform vendors data to dropdown options + const vendorOptions: DropdownOption[] = + 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[] = [ { label: 'Net 30', value: 'net_30' }, @@ -43,9 +54,53 @@ const PurchaseBasicInfo: React.FC = ({ formData, handleI fullWidth options={vendorOptions} value={formData.vendor} - onChange={(event, newValue) => handleInputChange('vendor', newValue)} - renderInput={params => } + onChange={(event, newValue) => { + 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 => ( + + )} /> + {getSelectedVendorData() && ( + + {/* Nama Perum */} + + + + {getSelectedVendorData()?.contact_person ?? ''} + + + + {/* Alamat */} + + + + {getSelectedVendorData()?.address ?? '-'} + + + + {/* Nomor Telepon */} + + + + {getSelectedVendorData()?.phone_number ?? '-'} + + + + )} = ({ 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 { data: ingredients, isLoading } = useIngredients() + + // Transform ingredients data to autocomplete options format + const ingredientOptions = useMemo(() => { + if (!ingredients || isLoading) { + return [] + } + + 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 = [ { label: 'KG', value: 'kg' }, @@ -63,6 +72,40 @@ const PurchaseIngredientsTable: React.FC = ({ { 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 ( @@ -92,9 +135,36 @@ const PurchaseIngredientsTable: React.FC = ({ handleIngredientChange(index, 'ingredient', newValue)} - renderInput={params => } + value={item.ingredient || null} + onChange={(event, newValue) => handleIngredientSelection(index, newValue)} + 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 => ( + + {isLoading ? : null} + {params.InputProps.endAdornment} + + ) + }} + /> + )} + disabled={isLoading} /> @@ -215,6 +285,7 @@ const PurchaseIngredientsTable: React.FC = ({ variant='outlined' size='small' sx={{ mt: 1 }} + disabled={isLoading} > Tambah bahan baku diff --git a/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx b/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx index 33cbab8..6d242c4 100644 --- a/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx +++ b/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx @@ -42,6 +42,9 @@ import Loading from '@/components/layout/shared/Loading' import { PurchaseOrderType } from '@/types/apps/purchaseOrderTypes' import { purchaseOrdersData } from '@/data/dummy/purchase-order' 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' { interface FilterFns { @@ -52,7 +55,7 @@ declare module '@tanstack/table-core' { } } -type PurchaseOrderTypeWithAction = PurchaseOrderType & { +type PurchaseOrderTypeWithAction = PurchaseOrder & { actions?: string } @@ -104,14 +107,16 @@ const DebouncedInput = ({ // Status color mapping const getStatusColor = (status: string) => { switch (status) { - case 'Draft': + case 'draft': return 'secondary' - case 'Disetujui': + case 'approved': return 'primary' - case 'Dikirim Sebagian': + case 'sent': return 'warning' - case 'Selesai': + case 'received': return 'success' + case 'cancelled': + return 'error' default: return 'default' } @@ -135,46 +140,24 @@ const PurchaseOrderListTable = () => { // States const [addPOOpen, setAddPOOpen] = useState(false) const [rowSelection, setRowSelection] = useState({}) - const [currentPage, setCurrentPage] = useState(0) + const [currentPage, setCurrentPage] = useState(1) const [pageSize, setPageSize] = useState(10) const [openConfirm, setOpenConfirm] = useState(false) const [poId, setPOId] = useState('') const [search, setSearch] = useState('') const [statusFilter, setStatusFilter] = useState('Semua') - const [filteredData, setFilteredData] = useState(purchaseOrdersData) - // Hooks const { lang: locale } = useParams() - // Filter data based on search and status - useEffect(() => { - let filtered = purchaseOrdersData + const { data, isLoading, error, isFetching } = usePurchaseOrders({ + page: currentPage, + limit: pageSize, + search, + status: statusFilter === 'Semua' ? '' : statusFilter + }) - // Filter by search - if (search) { - 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 purchaseOrders = data?.purchase_orders ?? [] + const totalCount = data?.total_count ?? 0 const handlePageChange = useCallback((event: unknown, newPage: number) => { setCurrentPage(newPage) @@ -222,14 +205,15 @@ const PurchaseOrderListTable = () => { /> ) }, - columnHelper.accessor('number', { + columnHelper.accessor('po_number', { header: 'Nomor PO', cell: ({ row }) => ( ) }), - columnHelper.accessor('vendorName', { + columnHelper.accessor('vendor.name', { header: 'Vendor', cell: ({ row }) => (
- {row.original.vendorName} + {row.original.vendor.contact_person} - {row.original.vendorCompany} + {row.original.vendor.name}
) @@ -260,13 +244,13 @@ const PurchaseOrderListTable = () => { header: 'Referensi', cell: ({ row }) => {row.original.reference || '-'} }), - columnHelper.accessor('date', { + columnHelper.accessor('transaction_date', { header: 'Tanggal', - cell: ({ row }) => {row.original.date} + cell: ({ row }) => {row.original.transaction_date} }), - columnHelper.accessor('dueDate', { + columnHelper.accessor('due_date', { header: 'Tanggal Jatuh Tempo', - cell: ({ row }) => {row.original.dueDate} + cell: ({ row }) => {row.original.due_date} }), columnHelper.accessor('status', { header: 'Status', @@ -282,16 +266,16 @@ const PurchaseOrderListTable = () => { ) }), - columnHelper.accessor('total', { + columnHelper.accessor('total_amount', { header: 'Total', - cell: ({ row }) => {formatCurrency(row.original.total)} + cell: ({ row }) => {formatCurrency(row.original.total_amount)} }) ], [] ) const table = useReactTable({ - data: paginatedData as PurchaseOrderType[], + data: purchaseOrders as PurchaseOrder[], columns, filterFns: { fuzzy: fuzzyFilter @@ -316,27 +300,11 @@ const PurchaseOrderListTable = () => { {/* Filter Status Tabs */}
- {['Semua', 'Draft', 'Disetujui', 'Dikirim Sebagian', 'Selesai', 'Lainnya'].map(status => ( - - ))} +
@@ -378,56 +346,60 @@ const PurchaseOrderListTable = () => {
- - - {table.getHeaderGroups().map(headerGroup => ( - - {headerGroup.headers.map(header => ( - - ))} - - ))} - - {filteredData.length === 0 ? ( - - - - - - ) : ( - - {table.getRowModel().rows.map(row => { - return ( - - {row.getVisibleCells().map(cell => ( - - ))} - - ) - })} - - )} -
- {header.isPlaceholder ? null : ( - <> -
- {flexRender(header.column.columnDef.header, header.getContext())} - {{ - asc: , - desc: - }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} -
- - )} -
- Tidak ada data tersedia -
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+ {isLoading ? ( + + ) : ( + + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + {purchaseOrders.length === 0 ? ( + + + + + + ) : ( + + {table.getRowModel().rows.map(row => { + return ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + + )} +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: + }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} +
+ + )} +
+ Tidak ada data tersedia +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+ )}
{ onPageChange={handlePageChange} onRowsPerPageChange={handlePageSizeChange} rowsPerPageOptions={[10, 25, 50]} + disabled={isLoading} />
diff --git a/src/views/apps/vendor/detail/vendor-overview/VendorDetails.tsx b/src/views/apps/vendor/detail/vendor-overview/VendorDetails.tsx index f0f4615..c9e3b6a 100644 --- a/src/views/apps/vendor/detail/vendor-overview/VendorDetails.tsx +++ b/src/views/apps/vendor/detail/vendor-overview/VendorDetails.tsx @@ -1,20 +1,28 @@ +'use client' + // MUI Imports import Card from '@mui/material/Card' import CardContent from '@mui/material/CardContent' import Typography from '@mui/material/Typography' import Chip from '@mui/material/Chip' 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 -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 { 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 const userData = { @@ -33,7 +41,25 @@ const userData = { } 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 => ({ children, color, @@ -42,91 +68,132 @@ const VendorDetails = () => { return ( <> - - -
-
-
- - {`${userData.firstName} ${userData.lastName}`} + {isLoading ? ( + + + + ) : ( + + +
+
+
+ {/* + {getInitials(vendor?.name as string)} + */} + {vendor?.name} +
+
-
-
- {/* Detail Kontak Section */} -
- Detail Kontak - -
-
- - Nama: - - {`${userData.firstName} ${userData.lastName}`} -
-
- - Perusahaan: - - {userData.perusahaan} -
-
- - Email: - - - {userData.email} - -
-
- - Telepon: - - - {userData.telepon} - -
-
- - Alamat Penagihan: - - - {userData.alamatPenagihan} - + {/* Detail Kontak Section */} +
+ Detail Kontak + +
+
+ + Contact Person: + + {vendor?.contact_person} +
+
+ + Perusahaan: + + {vendor?.name} +
+
+ + Email: + + + {vendor?.email} + +
+
+ + Telepon: + + + {vendor?.phone_number} + +
+
+ + Alamat Penagihan: + + + {vendor?.address ?? '-'} + +
-
- {/* Pemetaan Akun Section */} -
- Pemetaan Akun - -
-
- - Akun Hutang: - - - {userData.akunHutang} - -
-
- - Akun Piutang: - - {userData.akunPiutang || '-'} -
-
- - Kena Pajak: - - {userData.kenaPajak} + {/* Pemetaan Akun Section */} +
+ Pemetaan Akun + +
+
+ + Akun Hutang: + + + {userData.akunHutang} + +
+
+ + Akun Piutang: + + {userData.akunPiutang || '-'} +
+
+ + Kena Pajak: + + {userData.kenaPajak} +
-
- - +
+ + +
+ + + )} + setEditVendorOpen(!editVendorOpen)} data={vendor} /> + 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.' + /> ) } diff --git a/src/views/apps/vendor/list/AddVendorDrawer.tsx b/src/views/apps/vendor/list/AddVendorDrawer.tsx index 42a1c3d..3c8a7b3 100644 --- a/src/views/apps/vendor/list/AddVendorDrawer.tsx +++ b/src/views/apps/vendor/list/AddVendorDrawer.tsx @@ -1,5 +1,5 @@ // React Imports -import { useState } from 'react' +import { useState, useEffect } from 'react' // MUI Imports import Button from '@mui/material/Button' @@ -10,54 +10,60 @@ import Typography from '@mui/material/Typography' import Divider from '@mui/material/Divider' import Grid from '@mui/material/Grid2' import Box from '@mui/material/Box' +import Switch from '@mui/material/Switch' +import FormControlLabel from '@mui/material/FormControlLabel' // Third-party Imports import { useForm, Controller } from 'react-hook-form' -// Types Imports -import type { VendorType } from '@/types/apps/vendorTypes' - // Component Imports import CustomTextField from '@core/components/mui/TextField' +import { Vendor, VendorRequest } from '@/types/services/vendor' +import { useVendorsMutation } from '@/services/mutations/vendor' type Props = { open: boolean handleClose: () => void - vendorData?: VendorType[] - setData: (data: VendorType[]) => void + data?: Vendor // Data vendor untuk edit (jika ada) } type FormValidateType = { name: string - company: 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 -const initialData = { +// Initial form data +const initialData: FormValidateType = { name: '', - company: '', email: '', - telephone: '' + phone_number: '', + address: '', + contact_person: '', + tax_number: '', + payment_terms: '', + notes: '', + is_active: true } -const AddVendorDrawer = (props: Props) => { +const AddEditVendorDrawer = (props: Props) => { // Props - const { open, handleClose, vendorData, setData } = props + const { open, handleClose, data } = props // States const [showMore, setShowMore] = useState(false) - const [alamatPengiriman, setAlamatPengiriman] = useState(['']) - const [rekeningBank, setRekeningBank] = useState([ - { - bank: '', - cabang: '', - namaPemilik: '', - nomorRekening: '' - } - ]) - const [showPemetaanAkun, setShowPemetaanAkun] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + + const { createVendor, updateVendor } = useVendorsMutation() + + // Determine if this is edit mode + const isEditMode = Boolean(data?.id) // Hooks const { @@ -69,92 +75,85 @@ const AddVendorDrawer = (props: Props) => { defaultValues: initialData }) - // Functions untuk alamat - const handleTambahAlamat = () => { - setAlamatPengiriman([...alamatPengiriman, '']) - } - - const handleHapusAlamat = (index: number) => { - if (alamatPengiriman.length > 1) { - const newAlamat = alamatPengiriman.filter((_, i) => i !== index) - setAlamatPengiriman(newAlamat) - } - } - - const handleChangeAlamat = (index: number, value: string) => { - const newAlamat = [...alamatPengiriman] - newAlamat[index] = value - setAlamatPengiriman(newAlamat) - } - - // Functions untuk rekening bank - const handleTambahRekening = () => { - setRekeningBank([ - ...rekeningBank, - { - bank: '', - cabang: '', - namaPemilik: '', - nomorRekening: '' + // Effect to populate form when editing + useEffect(() => { + 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 handleHapusRekening = (index: number) => { - if (rekeningBank.length > 1) { - const newRekening = rekeningBank.filter((_, i) => i !== index) - setRekeningBank(newRekening) - } - } + resetForm(formData) - 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) - setAlamatPengiriman(['']) - setRekeningBank([ - { - bank: '', - cabang: '', - namaPemilik: '', - nomorRekening: '' + // Show more fields if any optional field has data + const hasOptionalData = data.address || data.tax_number || data.payment_terms || data.notes + if (hasOptionalData) { + setShowMore(true) } - ]) - setShowMore(false) - setShowPemetaanAkun(false) + } else { + // Reset to initial data for add mode + resetForm(initialData) + setShowMore(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 = () => { handleClose() resetForm(initialData) - setAlamatPengiriman(['']) - setRekeningBank([ - { - bank: '', - cabang: '', - namaPemilik: '', - nomorRekening: '' - } - ]) setShowMore(false) - setShowPemetaanAkun(false) } return ( @@ -185,7 +184,7 @@ const AddVendorDrawer = (props: Props) => { }} >
- Tambah Vendor Baru + {isEditMode ? 'Edit Vendor' : 'Tambah Vendor Baru'} @@ -194,472 +193,200 @@ const AddVendorDrawer = (props: Props) => { {/* Scrollable Content */} -
onSubmit(data))}> +
- {/* Tampilkan Foto */} -
- - - Tampilkan Foto - -
- - {/* Nama */} + {/* Nama Vendor */}
- Nama * + Nama Vendor * - - - - Tuan - Nyonya - Nona - Bapak - Ibu - - - - ( - - )} + ( + - - + )} + />
- {/* Perusahaan dan Telepon */} - - - ( - - )} - /> - - - ( - - )} - /> - - - {/* Email */} - ( - - )} - /> +
+ + Email * + + ( + + )} + /> +
+ + {/* Nomor Telepon */} +
+ + Nomor Telepon * + + ( + + )} + /> +
+ + {/* Contact Person */} +
+ + Contact Person * + + ( + + )} + /> +
+ + {/* Status Aktif */} +
+ ( + } + label='Vendor Aktif' + /> + )} + /> +
{/* Tampilkan selengkapnya */} {!showMore && ( -
setShowMore(true)}> - - - Tampilkan selengkapnya - -
+ )} - {/* Konten tambahan yang muncul saat showMore true */} + {/* Konten tambahan */} {showMore && ( <> - {/* Alamat Penagihan */} + {/* Alamat */}
- Alamat Penagihan + Alamat - + ( + + )} + />
- {/* Negara */} -
- - Negara - - - Indonesia - -
- - {/* Provinsi dan Kota */} - - - - Provinsi - - - Pilih Provinsi - DKI Jakarta - Jawa Barat - Jawa Tengah - Jawa Timur - - - - - Kota - - - Pilih Kota - Jakarta - Bandung - Surabaya - - - - - {/* Kecamatan dan Kelurahan */} - - - - Kecamatan - - - Pilih Kecamatan - - - - - Kelurahan - - - Pilih Kelurahan - - - - - {/* Tipe Kartu Identitas dan ID */} - - - - Tipe Kartu Identitas - - - Pilih Tipe Kartu Identitas - KTP - SIM - Paspor - - - - - ID Kartu Identitas - - - - - - {/* NPWP */} + {/* NPWP/Tax Number */}
NPWP - + } + />
- {/* Alamat Pengiriman */} + {/* Payment Terms */}
- - Alamat Pengiriman + + Syarat Pembayaran - {alamatPengiriman.map((alamat, index) => ( -
+ ( + + Pilih Syarat Pembayaran + Cash + Net 7 Hari + Net 14 Hari + Net 30 Hari + Net 60 Hari + Net 90 Hari + + )} + /> +
+ + {/* Notes */} +
+ + Catatan + + ( handleChangeAlamat(index, e.target.value)} - sx={{ - '& .MuiOutlinedInput-root': { - borderColor: index === 1 ? 'primary.main' : 'default' - } - }} + rows={3} /> - {alamatPengiriman.length > 1 && ( - handleHapusAlamat(index)} - sx={{ - color: 'error.main', - border: 1, - borderColor: 'error.main', - '&:hover': { - backgroundColor: 'error.light', - borderColor: 'error.main' - } - }} - > - - - )} -
- ))} + )} + />
- {/* Tambah Alamat Pengiriman */} -
- - - Tambah Alamat Pengiriman - -
- - {/* Rekening Bank */} -
- - Rekening Bank - - {rekeningBank.map((rekening, index) => ( -
-
-
- {/* Baris pertama: Bank & Cabang */} - - - handleChangeRekening(index, 'bank', e.target.value)} - > - Pilih Bank - BCA - Mandiri - BNI - BRI - - - - handleChangeRekening(index, 'cabang', e.target.value)} - /> - - - - {/* Baris kedua: Nama Pemilik & Nomor Rekening */} - - - handleChangeRekening(index, 'namaPemilik', e.target.value)} - /> - - - handleChangeRekening(index, 'nomorRekening', e.target.value)} - /> - - -
- - {/* Tombol hapus di samping, sejajar dengan tengah kedua baris */} - {rekeningBank.length > 1 && ( -
- handleHapusRekening(index)} - sx={{ - color: 'error.main', - border: 1, - borderColor: 'error.main', - '&:hover': { - backgroundColor: 'error.light', - borderColor: 'error.main' - } - }} - > - - -
- )} -
-
- ))} - -
- - - Tambah Rekening Bank - -
- -
setShowPemetaanAkun(!showPemetaanAkun)}> - - - {showPemetaanAkun ? 'Sembunyikan pemetaan akun' : 'Tampilkan pemetaan akun'} - -
- - {/* Konten Pemetaan Akun */} - {showPemetaanAkun && ( -
- {/* Akun Hutang */} - - - - Akun Hutang - - - 2-20100 Hutang Usaha - 2-20200 Hutang Bank - 2-20300 Hutang Lainnya - - - - - Maksimal Hutang - - - - - - - {/* Akun Piutang */} - - - - Akun Piutang - - - 1-10100 Piutang Usaha - 1-10200 Piutang Karyawan - 1-10300 Piutang Lainnya - - - - - Maksimal Piutang - - - - - - - {/* Kena Pajak */} -
- - Kena pajak ? - -
- - -
-
-
- )} - - - - - Nomor - - - - - - Tanggal Lahir - - - - - -
- - Deskripsi - - -
- - {/* Button Sembunyikan di dalam konten */} -
setShowMore(false)}> - - - Sembunyikan - -
-
+ {/* Sembunyikan */} + )}
@@ -679,10 +406,10 @@ const AddVendorDrawer = (props: Props) => { }} >
- -
@@ -691,4 +418,4 @@ const AddVendorDrawer = (props: Props) => { ) } -export default AddVendorDrawer +export default AddEditVendorDrawer diff --git a/src/views/apps/vendor/list/VendorListTable.tsx b/src/views/apps/vendor/list/VendorListTable.tsx index 754d059..a637161 100644 --- a/src/views/apps/vendor/list/VendorListTable.tsx +++ b/src/views/apps/vendor/list/VendorListTable.tsx @@ -1,7 +1,7 @@ 'use client' // React Imports -import { useEffect, useState, useMemo } from 'react' +import { useEffect, useState, useMemo, useCallback } from 'react' // Next Imports import Link from 'next/link' @@ -58,6 +58,9 @@ import { getLocalizedUrl } from '@/utils/i18n' // Style Imports import tableStyles from '@core/styles/table.module.css' 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' { interface FilterFns { @@ -68,7 +71,7 @@ declare module '@tanstack/table-core' { } } -type VendorTypeWithAction = VendorType & { +type VendorTypeWithAction = Vendor & { action?: string } @@ -120,17 +123,37 @@ const DebouncedInput = ({ // Column Definitions const columnHelper = createColumnHelper() -const VendorListTable = ({ tableData }: { tableData?: VendorType[] }) => { +const VendorListTable = () => { // States const [addVendorOpen, setAddVendorOpen] = useState(false) const [rowSelection, setRowSelection] = useState({}) - const [data, setData] = useState(...[tableData]) - const [filteredData, setFilteredData] = useState(data) 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 const { lang: locale } = useParams() + const handlePageChange = useCallback((event: unknown, newPage: number) => { + setCurrentPage(newPage) + }, []) + + const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10) + setPageSize(newPageSize) + setCurrentPage(1) // Reset to first page + }, []) + const columns = useMemo[]>( () => [ { @@ -155,103 +178,64 @@ const VendorListTable = ({ tableData }: { tableData?: VendorType[] }) => { /> ) }, - columnHelper.accessor('name', { + columnHelper.accessor('contact_person', { header: 'Vendor', cell: ({ row }) => (
- {getAvatar({ photo: row.original.photo, name: row.original.name })}
- - - {row.original.name} + + + {row.original.contact_person} + {row.original.email} - {row.original.email}
) }), - columnHelper.accessor('company', { + columnHelper.accessor('name', { header: 'Perusahaan', cell: ({ row }) => (
- {row.original.company} + {row.original.name}
) }), - columnHelper.accessor('telephone', { + columnHelper.accessor('phone_number', { header: 'Telepon', - cell: ({ row }) => {row.original.telephone} - }), - columnHelper.accessor('youPayable', { - header: () =>
Anda Hutang
, - cell: ({ row }) => ( -
- - {formatCurrency(row.original.youPayable)} - -
- ) - }), - columnHelper.accessor('theyPayable', { - header: () =>
Mereka Hutang
, - cell: ({ row }) => ( -
- - {formatCurrency(row.original.theyPayable)} - -
- ) + cell: ({ row }) => {row.original.phone_number} }) ], // eslint-disable-next-line react-hooks/exhaustive-deps - [data, filteredData] + [] ) const table = useReactTable({ - data: filteredData as VendorType[], + data: vendors as Vendor[], columns, filterFns: { fuzzy: fuzzyFilter }, state: { rowSelection, - globalFilter - }, - initialState: { + globalFilter, pagination: { - pageSize: 10 + pageIndex: currentPage, + pageSize } }, enableRowSelection: true, - globalFilterFn: fuzzyFilter, onRowSelectionChange: setRowSelection, getCoreRowModel: getCoreRowModel(), - onGlobalFilterChange: setGlobalFilter, - getFilteredRowModel: getFilteredRowModel(), - getSortedRowModel: getSortedRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getFacetedRowModel: getFacetedRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues(), - getFacetedMinMaxValues: getFacetedMinMaxValues() + manualPagination: true, + pageCount: Math.ceil(totalCount / pageSize) }) - const getAvatar = (params: Pick) => { - const { photo, name } = params - - if (photo) { - return - } else { - return {getInitials(name as string)} - } - } - return ( <> - - + {/* */}
{
setGlobalFilter(String(value))} + value={search ?? ''} + onChange={value => setSearch(value as string)} placeholder='Cari Vendor' className='max-sm:is-full' /> @@ -289,75 +273,83 @@ const VendorListTable = ({ tableData }: { tableData?: VendorType[] }) => {
- - - {table.getHeaderGroups().map(headerGroup => ( - - {headerGroup.headers.map(header => ( - - ))} - - ))} - - {table.getFilteredRowModel().rows.length === 0 ? ( - - - - - - ) : ( - - {table - .getRowModel() - .rows.slice(0, table.getState().pagination.pageSize) - .map(row => { - return ( - - {row.getVisibleCells().map(cell => ( - - ))} - - ) - })} - - )} -
- {header.isPlaceholder ? null : ( - <> -
- {flexRender(header.column.columnDef.header, header.getContext())} - {{ - asc: , - desc: - }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} -
- - )} -
- Tidak ada data tersedia -
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+ {isLoading ? ( + + ) : ( + + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + {table.getFilteredRowModel().rows.length === 0 ? ( + + + + + + ) : ( + + {table + .getRowModel() + .rows.slice(0, table.getState().pagination.pageSize) + .map(row => { + return ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + + )} +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: + }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} +
+ + )} +
+ Tidak ada data tersedia +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+ )}
- { - table.setPageIndex(page) - }} + ( + + )} + count={totalCount} + rowsPerPage={pageSize} + page={currentPage} + onPageChange={handlePageChange} + onRowsPerPageChange={handlePageSizeChange} + rowsPerPageOptions={[10, 25, 50]} + disabled={isLoading} />
- setAddVendorOpen(!addVendorOpen)} - vendorData={data} - setData={setData} - /> + setAddVendorOpen(!addVendorOpen)} /> ) }