From 0b51b29474d64407d5310fd1a43a596311ffe325 Mon Sep 17 00:00:00 2001 From: efrilm Date: Fri, 12 Sep 2025 21:54:27 +0700 Subject: [PATCH] Vendor at Purchase Form --- src/services/mutations/purchaseOrder.ts | 22 + src/services/queries/vendor.ts | 10 + src/types/services/purchaseOrder.ts | 19 + .../purchase-form copy/PurchaseAddForm.tsx | 121 ++++ .../purchase-form copy/PurchaseBasicInfo.tsx | 197 ++++++ .../PurchaseIngredientsTable.tsx | 225 +++++++ .../purchase-form copy/PurchaseSummary.tsx | 589 ++++++++++++++++++ .../purchase-form/PurchaseBasicInfo.tsx | 73 ++- 8 files changed, 1247 insertions(+), 9 deletions(-) create mode 100644 src/services/mutations/purchaseOrder.ts create mode 100644 src/types/services/purchaseOrder.ts create mode 100644 src/views/apps/purchase/purchase-form copy/PurchaseAddForm.tsx create mode 100644 src/views/apps/purchase/purchase-form copy/PurchaseBasicInfo.tsx create mode 100644 src/views/apps/purchase/purchase-form copy/PurchaseIngredientsTable.tsx create mode 100644 src/views/apps/purchase/purchase-form copy/PurchaseSummary.tsx diff --git a/src/services/mutations/purchaseOrder.ts b/src/services/mutations/purchaseOrder.ts new file mode 100644 index 0000000..6fc2d3b --- /dev/null +++ b/src/services/mutations/purchaseOrder.ts @@ -0,0 +1,22 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { toast } from 'react-toastify' +import { api } from '../api' +import { PurchaseOrderRequest } from '@/types/services/purchaseOrder' + +export const useVendorsMutation = () => { + const queryClient = useQueryClient() + + const createVendor = useMutation({ + mutationFn: async (newVendor: PurchaseOrderRequest) => { + 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') + } + }) +} diff --git a/src/services/queries/vendor.ts b/src/services/queries/vendor.ts index 33dff13..47aeb4d 100644 --- a/src/services/queries/vendor.ts +++ b/src/services/queries/vendor.ts @@ -35,6 +35,16 @@ export function useVendors(params: VendorQueryParams = {}) { }) } +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], diff --git a/src/types/services/purchaseOrder.ts b/src/types/services/purchaseOrder.ts new file mode 100644 index 0000000..6337ad7 --- /dev/null +++ b/src/types/services/purchaseOrder.ts @@ -0,0 +1,19 @@ +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 +} diff --git a/src/views/apps/purchase/purchase-form copy/PurchaseAddForm.tsx b/src/views/apps/purchase/purchase-form copy/PurchaseAddForm.tsx new file mode 100644 index 0000000..7a158d9 --- /dev/null +++ b/src/views/apps/purchase/purchase-form copy/PurchaseAddForm.tsx @@ -0,0 +1,121 @@ +'use client' + +import React, { useState } from 'react' +import { Card, CardContent } from '@mui/material' +import Grid from '@mui/material/Grid2' +import { IngredientItem, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes' +import PurchaseBasicInfo from './PurchaseBasicInfo' +import PurchaseIngredientsTable from './PurchaseIngredientsTable' +import PurchaseSummary from './PurchaseSummary' + +const PurchaseAddForm: React.FC = () => { + const [formData, setFormData] = useState({ + vendor: null, + nomor: 'PO/00043', + tglTransaksi: '2025-09-09', + tglJatuhTempo: '2025-09-10', + referensi: '', + termin: null, + hargaTermasukPajak: true, + // Shipping info + showShippingInfo: false, + tanggalPengiriman: '', + ekspedisi: null, + noResi: '', + // Bottom section toggles + showPesan: false, + showAttachment: false, + showTambahDiskon: false, + showBiayaPengiriman: false, + showBiayaTransaksi: false, + showUangMuka: false, + pesan: '', + // Ingredient items (updated from productItems) + ingredientItems: [ + { + id: 1, + ingredient: null, + deskripsi: '', + kuantitas: 1, + satuan: null, + discount: '0', + harga: 0, + pajak: null, + waste: null, + total: 0 + } + ] + }) + + const handleInputChange = (field: keyof PurchaseOrderFormData, value: any): void => { + setFormData(prev => ({ + ...prev, + [field]: value + })) + } + + const handleIngredientChange = (index: number, field: keyof IngredientItem, value: any): void => { + setFormData(prev => { + const newItems = [...prev.ingredientItems] + newItems[index] = { ...newItems[index], [field]: value } + + // Auto-calculate total if price or quantity changes + if (field === 'harga' || field === 'kuantitas') { + const item = newItems[index] + item.total = item.harga * item.kuantitas + } + + return { ...prev, ingredientItems: newItems } + }) + } + + const addIngredientItem = (): void => { + const newItem: IngredientItem = { + id: Date.now(), + ingredient: null, + deskripsi: '', + kuantitas: 1, + satuan: null, + discount: '0%', + harga: 0, + pajak: null, + waste: null, + total: 0 + } + setFormData(prev => ({ + ...prev, + ingredientItems: [...prev.ingredientItems, newItem] + })) + } + + const removeIngredientItem = (index: number): void => { + setFormData(prev => ({ + ...prev, + ingredientItems: prev.ingredientItems.filter((_, i) => i !== index) + })) + } + + return ( + + + + {/* Basic Info Section */} + + + {/* Ingredients Table Section */} + + + {/* Summary Section */} + + + + + ) +} + +export default PurchaseAddForm diff --git a/src/views/apps/purchase/purchase-form copy/PurchaseBasicInfo.tsx b/src/views/apps/purchase/purchase-form copy/PurchaseBasicInfo.tsx new file mode 100644 index 0000000..6cce349 --- /dev/null +++ b/src/views/apps/purchase/purchase-form copy/PurchaseBasicInfo.tsx @@ -0,0 +1,197 @@ +'use client' + +import React from 'react' +import { Button, Switch, FormControlLabel } from '@mui/material' +import Grid from '@mui/material/Grid2' +import CustomAutocomplete from '@/@core/components/mui/Autocomplete' +import CustomTextField from '@/@core/components/mui/TextField' +import { DropdownOption, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes' + +interface PurchaseBasicInfoProps { + formData: PurchaseOrderFormData + handleInputChange: (field: keyof PurchaseOrderFormData, value: any) => void +} + +const PurchaseBasicInfo: React.FC = ({ formData, handleInputChange }) => { + // Sample data for dropdowns + const vendorOptions: DropdownOption[] = [ + { label: 'Vendor A', value: 'vendor_a' }, + { label: 'Vendor B', value: 'vendor_b' }, + { label: 'Vendor C', value: 'vendor_c' } + ] + + const terminOptions: DropdownOption[] = [ + { label: 'Net 30', value: 'net_30' }, + { label: 'Net 15', value: 'net_15' }, + { label: 'Net 60', value: 'net_60' }, + { label: 'Cash on Delivery', value: 'cod' } + ] + + const ekspedisiOptions: DropdownOption[] = [ + { label: 'JNE', value: 'jne' }, + { label: 'J&T Express', value: 'jnt' }, + { label: 'SiCepat', value: 'sicepat' }, + { label: 'Pos Indonesia', value: 'pos' }, + { label: 'TIKI', value: 'tiki' } + ] + + return ( + <> + {/* Row 1 - Vendor dan Nomor */} + + handleInputChange('vendor', newValue)} + renderInput={params => } + /> + + + ) => handleInputChange('nomor', e.target.value)} + InputProps={{ + readOnly: true + }} + /> + + + {/* Row 2 - Tgl. Transaksi, Tgl. Jatuh Tempo, Termin */} + + ) => handleInputChange('tglTransaksi', e.target.value)} + InputLabelProps={{ + shrink: true + }} + /> + + + ) => handleInputChange('tglJatuhTempo', e.target.value)} + InputLabelProps={{ + shrink: true + }} + /> + + + handleInputChange('termin', newValue)} + renderInput={params => } + /> + + + {/* Row 3 - Tampilkan Informasi Pengiriman */} + + + + + {/* 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/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 ?? '-'} + + + + )}