diff --git a/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-orders/add/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-orders/add/page.tsx new file mode 100644 index 0000000..71c0036 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-orders/add/page.tsx @@ -0,0 +1,19 @@ +import Grid from '@mui/material/Grid2' + +import PurchaseOrderAddHeader from '@/views/apps/purchase/purchase-orders/add/PurchaseOrderAddHeader' +import PurchaseOrderAddForm from '@/views/apps/purchase/purchase-orders/add/PurchaseOrderAddForm' + +const PurchaseOrderAddPage = () => { + return ( + + + + + + + + + ) +} + +export default PurchaseOrderAddPage diff --git a/src/components/ImageUpload.tsx b/src/components/ImageUpload.tsx new file mode 100644 index 0000000..464785d --- /dev/null +++ b/src/components/ImageUpload.tsx @@ -0,0 +1,291 @@ +'use client' + +// React Imports +import { useEffect, useState } from 'react' + +// MUI Imports +import type { BoxProps } from '@mui/material/Box' +import Button from '@mui/material/Button' +import IconButton from '@mui/material/IconButton' +import List from '@mui/material/List' +import ListItem from '@mui/material/ListItem' +import Typography from '@mui/material/Typography' +import { styled } from '@mui/material/styles' + +// Third-party Imports +import { useDropzone } from 'react-dropzone' + +// Component Imports +import Link from '@components/Link' +import CustomAvatar from '@core/components/mui/Avatar' + +// Styled Component Imports +import AppReactDropzone from '@/libs/styles/AppReactDropzone' + +type FileProp = { + name: string + type: string + size: number +} + +interface ImageUploadProps { + // Required props + onUpload: (file: File) => Promise | string // Returns image URL + + // Optional customization props + title?: string | null // Made nullable + currentImageUrl?: string + onImageRemove?: () => void + onImageChange?: (url: string) => void + + // Upload state + isUploading?: boolean + + // UI customization + maxFileSize?: number // in bytes + acceptedFileTypes?: string[] + showUrlOption?: boolean + uploadButtonText?: string + browseButtonText?: string + dragDropText?: string + replaceText?: string + + // Style customization + className?: string + disabled?: boolean +} + +// Styled Dropzone Component +const Dropzone = styled(AppReactDropzone)(({ theme }) => ({ + '& .dropzone': { + minHeight: 'unset', + padding: theme.spacing(12), + [theme.breakpoints.down('sm')]: { + paddingInline: theme.spacing(5) + }, + '&+.MuiList-root .MuiListItem-root .file-name': { + fontWeight: theme.typography.body1.fontWeight + } + } +})) + +const ImageUpload: React.FC = ({ + onUpload, + title = null, // Default to null + currentImageUrl = '', + onImageRemove, + onImageChange, + isUploading = false, + maxFileSize = 5 * 1024 * 1024, // 5MB default + acceptedFileTypes = ['image/*'], + showUrlOption = true, + uploadButtonText = 'Upload', + browseButtonText = 'Browse Image', + dragDropText = 'Drag and Drop Your Image Here.', + replaceText = 'Drop New Image to Replace', + className = '', + disabled = false +}) => { + // States + const [files, setFiles] = useState([]) + const [error, setError] = useState('') + + const handleUpload = async () => { + if (!files.length) return + + try { + setError('') + const imageUrl = await onUpload(files[0]) + + if (typeof imageUrl === 'string') { + onImageChange?.(imageUrl) + setFiles([]) // Clear files after successful upload + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Upload failed') + } + } + + // Hooks + const { getRootProps, getInputProps } = useDropzone({ + onDrop: (acceptedFiles: File[]) => { + setError('') + + if (acceptedFiles.length === 0) return + + const file = acceptedFiles[0] + + // Validate file size + if (file.size > maxFileSize) { + setError(`File size should be less than ${formatFileSize(maxFileSize)}`) + return + } + + // Replace files instead of adding to them + setFiles([file]) + }, + accept: acceptedFileTypes.reduce((acc, type) => ({ ...acc, [type]: [] }), {}), + disabled: disabled || isUploading + }) + + const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 Bytes' + const k = 1024 + const sizes = ['Bytes', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] + } + + const renderFilePreview = (file: FileProp) => { + if (file.type.startsWith('image')) { + return + } else { + return + } + } + + const handleRemoveFile = (file: FileProp) => { + const filtered = files.filter((i: FileProp) => i.name !== file.name) + setFiles(filtered) + setError('') + } + + const handleRemoveCurrentImage = () => { + onImageRemove?.() + } + + const handleRemoveAllFiles = () => { + setFiles([]) + setError('') + } + + const fileList = files.map((file: FileProp) => ( + + + {renderFilePreview(file)} + + + {file.name} + + + {formatFileSize(file.size)} + + + + handleRemoveFile(file)} disabled={isUploading}> + + + + )) + + return ( + + {/* Conditional title and URL option header */} + {title && ( + + + {title} + + {showUrlOption && ( + + Add media from URL + + )} + + )} + + + + + + + + {currentImageUrl && !files.length ? replaceText : dragDropText} + or + + {browseButtonText} + + + + + {/* Error Message */} + {error && ( + + {error} + + )} + + {/* Show current image if it exists */} + {currentImageUrl && !files.length && ( + + + Current Image: + + + + + + + Current image + + + Uploaded image + + + + {onImageRemove && ( + + + + )} + + + )} + + {/* File list and upload buttons */} + {files.length > 0 && ( + <> + {fileList} + + + Remove All + + + {isUploading ? 'Uploading...' : uploadButtonText} + + + > + )} + + ) +} + +export default ImageUpload + +// ===== USAGE EXAMPLES ===== + +// 1. Without title +// setImageUrl('')} +// /> + +// 2. With title +// setImageUrl('')} +// /> + +// 3. Explicitly set title to null +// setImageUrl('')} +// /> diff --git a/src/types/apps/purchaseOrderTypes.ts b/src/types/apps/purchaseOrderTypes.ts index c38cae7..987560f 100644 --- a/src/types/apps/purchaseOrderTypes.ts +++ b/src/types/apps/purchaseOrderTypes.ts @@ -9,3 +9,58 @@ export type PurchaseOrderType = { status: string 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 + name: string + amount: string +} + +export interface DropdownOption { + label: string + value: string +} diff --git a/src/views/apps/purchase/purchase-orders/add/PurchaseOrderAddForm.tsx b/src/views/apps/purchase/purchase-orders/add/PurchaseOrderAddForm.tsx new file mode 100644 index 0000000..77814ef --- /dev/null +++ b/src/views/apps/purchase/purchase-orders/add/PurchaseOrderAddForm.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 PurchaseOrderBasicInfo from './PurchaseOrderBasicInfo' +import PurchaseOrderIngredientsTable from './PurchaseOrderIngredientsTable' +import PurchaseOrderSummary from './PurchaseOrderSummary' + +const PurchaseOrderAddForm: 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 PurchaseOrderAddForm diff --git a/src/views/apps/purchase/purchase-orders/add/PurchaseOrderAddHeader.tsx b/src/views/apps/purchase/purchase-orders/add/PurchaseOrderAddHeader.tsx new file mode 100644 index 0000000..f06706a --- /dev/null +++ b/src/views/apps/purchase/purchase-orders/add/PurchaseOrderAddHeader.tsx @@ -0,0 +1,19 @@ +'use client' + +// MUI Imports +import Button from '@mui/material/Button' +import Typography from '@mui/material/Typography' + +const PurchaseOrderAddHeader = () => { + return ( + + + + Tambah Pesanan Pembelian + + + + ) +} + +export default PurchaseOrderAddHeader diff --git a/src/views/apps/purchase/purchase-orders/add/PurchaseOrderBasicInfo.tsx b/src/views/apps/purchase/purchase-orders/add/PurchaseOrderBasicInfo.tsx new file mode 100644 index 0000000..6d8adcf --- /dev/null +++ b/src/views/apps/purchase/purchase-orders/add/PurchaseOrderBasicInfo.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 PurchaseOrderBasicInfoProps { + formData: PurchaseOrderFormData + handleInputChange: (field: keyof PurchaseOrderFormData, value: any) => void +} + +const PurchaseOrderBasicInfo: 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 */} + + handleInputChange('showShippingInfo', !formData.showShippingInfo)} + sx={{ + textTransform: 'none', + fontSize: '14px', + fontWeight: 500, + padding: '8px 0', + display: 'flex', + alignItems: 'center', + gap: 1 + }} + > + {formData.showShippingInfo ? '−' : '+'} {formData.showShippingInfo ? 'Sembunyikan' : 'Tampilkan'} Informasi + Pengiriman + + + + {/* 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 PurchaseOrderBasicInfo diff --git a/src/views/apps/purchase/purchase-orders/add/PurchaseOrderIngredientsTable.tsx b/src/views/apps/purchase/purchase-orders/add/PurchaseOrderIngredientsTable.tsx new file mode 100644 index 0000000..8cb8836 --- /dev/null +++ b/src/views/apps/purchase/purchase-orders/add/PurchaseOrderIngredientsTable.tsx @@ -0,0 +1,249 @@ +'use client' + +import React from 'react' +import { Button, 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 { IngredientItem, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes' + +interface PurchaseOrderIngredientsTableProps { + formData: PurchaseOrderFormData + handleIngredientChange: (index: number, field: keyof IngredientItem, value: any) => void + addIngredientItem: () => void + removeIngredientItem: (index: number) => void +} + +const PurchaseOrderIngredientsTable: 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 + + + {/* Table Header */} + + + + Bahan Baku + + + + + Deskripsi + + + + + Kuantitas + + + + + Satuan + + + + + Discount + + + + + Harga + + + + + Pajak + + + + + Waste + + + + + Total + + + + + + {/* Ingredient Items */} + {formData.ingredientItems.map((item: IngredientItem, index: number) => ( + + + handleIngredientChange(index, 'ingredient', newValue)} + renderInput={params => ( + + )} + /> + + + ) => + handleIngredientChange(index, 'deskripsi', e.target.value) + } + placeholder='Deskripsi' + /> + + + ) => + handleIngredientChange(index, 'kuantitas', parseInt(e.target.value) || 1) + } + inputProps={{ min: 1 }} + /> + + + handleIngredientChange(index, 'satuan', newValue)} + renderInput={params => } + /> + + + ) => + handleIngredientChange(index, 'discount', e.target.value) + } + placeholder='0%' + /> + + + ) => { + const value = e.target.value + + if (value === '') { + // Jika kosong, set ke null atau undefined, bukan 0 + handleIngredientChange(index, 'harga', null) // atau undefined + return + } + + const numericValue = parseFloat(value) + handleIngredientChange(index, 'harga', isNaN(numericValue) ? 0 : numericValue) + }} + inputProps={{ min: 0, step: 'any' }} + placeholder='0' + /> + + + handleIngredientChange(index, 'pajak', newValue)} + renderInput={params => } + /> + + + handleIngredientChange(index, 'waste', newValue)} + renderInput={params => } + /> + + + + + + removeIngredientItem(index)} + sx={{ minWidth: 'auto', p: 1 }} + > + + + + + ))} + + {/* Add New Item Button */} + + + + + Tambah bahan baku + + + + + ) +} + +export default PurchaseOrderIngredientsTable diff --git a/src/views/apps/purchase/purchase-orders/add/PurchaseOrderSummary.tsx b/src/views/apps/purchase/purchase-orders/add/PurchaseOrderSummary.tsx new file mode 100644 index 0000000..2c2a51d --- /dev/null +++ b/src/views/apps/purchase/purchase-orders/add/PurchaseOrderSummary.tsx @@ -0,0 +1,578 @@ +'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 PurchaseOrderSummaryProps { + formData: PurchaseOrderFormData + handleInputChange: (field: keyof PurchaseOrderFormData, value: any) => void +} + +const PurchaseOrderSummary: 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 */} + + handleInputChange('showPesan', !formData.showPesan)} + sx={{ + textTransform: 'none', + fontSize: '14px', + fontWeight: 500, + padding: '12px 16px', + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-start', + width: '100%', + backgroundColor: '#f5f5f5', + border: '1px solid #e0e0e0', + borderRadius: '4px', + color: 'text.primary', + '&:hover': { + backgroundColor: '#eeeeee' + } + }} + > + + {formData.showPesan ? ( + + ) : ( + + )} + + Pesan + + {formData.showPesan && ( + + ) => handleInputChange('pesan', e.target.value)} + /> + + )} + + + {/* Attachment Section */} + + handleInputChange('showAttachment', !formData.showAttachment)} + sx={{ + textTransform: 'none', + fontSize: '14px', + fontWeight: 500, + padding: '12px 16px', + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-start', + width: '100%', + backgroundColor: '#f5f5f5', + border: '1px solid #e0e0e0', + borderRadius: '4px', + color: 'text.primary', + '&:hover': { + backgroundColor: '#eeeeee' + } + }} + > + + {formData.showAttachment ? ( + + ) : ( + + )} + + Attachment + + {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 */} + + handleInputChange('showTambahDiskon', !formData.showTambahDiskon)} + > + {formData.showTambahDiskon ? '- Sembunyikan Diskon' : '+ Tambahan 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 */} + + handleInputChange('showBiayaPengiriman', !formData.showBiayaPengiriman)} + > + {formData.showBiayaPengiriman ? '- Sembunyikan Biaya Pengiriman' : '+ 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 */} + + { + if (!formData.showBiayaTransaksi) { + handleInputChange('showBiayaTransaksi', true) + if (transactionCosts.length === 0) { + addTransactionCost() + } + } else { + handleInputChange('showBiayaTransaksi', false) + } + }} + > + {formData.showBiayaTransaksi ? '- Sembunyikan Biaya Transaksi' : '+ Biaya Transaksi'} + + + {/* 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 */} + + + Tambah biaya transaksi lain + + + )} + + + + {/* Total */} + + + Total + + + {new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(total)} + + + + {/* Uang Muka */} + + + + {/* 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 */} + + Simpan + + + + + + ) +} + +export default PurchaseOrderSummary diff --git a/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx b/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx index f05c540..33cbab8 100644 --- a/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx +++ b/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx @@ -41,6 +41,7 @@ import TablePaginationComponent from '@/components/TablePaginationComponent' 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' declare module '@tanstack/table-core' { interface FilterFns { @@ -367,11 +368,12 @@ const PurchaseOrderListTable = () => { } - onClick={() => setAddPOOpen(!addPOOpen)} - className='max-sm:is-full' + href={getLocalizedUrl('/apps/purchase/purchase-orders/add', locale as Locale)} > - Tambah PO Baru + Tambah