update purchase order

This commit is contained in:
efrilm 2025-09-25 16:20:13 +07:00
parent dc32c8553b
commit d317e8d06f
4 changed files with 370 additions and 169 deletions

84
package-lock.json generated
View File

@ -53,7 +53,8 @@
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"input-otp": "1.4.1", "input-otp": "1.4.1",
"jspdf": "^3.0.1", "jspdf": "^3.0.3",
"jspdf-autotable": "^5.0.2",
"keen-slider": "6.8.6", "keen-slider": "6.8.6",
"lucide-react": "^0.544.0", "lucide-react": "^0.544.0",
"mapbox-gl": "3.9.0", "mapbox-gl": "3.9.0",
@ -83,6 +84,7 @@
"@iconify/types": "2.0.0", "@iconify/types": "2.0.0",
"@iconify/utils": "2.2.1", "@iconify/utils": "2.2.1",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/jspdf": "^1.3.3",
"@types/mapbox-gl": "^3.4.1", "@types/mapbox-gl": "^3.4.1",
"@types/negotiator": "^0.6.3", "@types/negotiator": "^0.6.3",
"@types/node": "^22.10.2", "@types/node": "^22.10.2",
@ -3508,6 +3510,13 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/jspdf": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@types/jspdf/-/jspdf-1.3.3.tgz",
"integrity": "sha512-DqwyAKpVuv+7DniCp2Deq1xGvfdnKSNgl9Agun2w6dFvR5UKamiv4VfYUgcypd8S9ojUyARFIlZqBrYrBMQlew==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/linkify-it": { "node_modules/@types/linkify-it": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
@ -3573,6 +3582,12 @@
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/pako": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
"license": "MIT"
},
"node_modules/@types/parse-json": { "node_modules/@types/parse-json": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
@ -4281,18 +4296,6 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"license": "(MIT OR Apache-2.0)",
"bin": {
"atob": "bin/atob.js"
},
"engines": {
"node": ">= 4.5.0"
}
},
"node_modules/attr-accept": { "node_modules/attr-accept": {
"version": "2.2.5", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
@ -4510,18 +4513,6 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/btoa": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
"license": "(MIT OR Apache-2.0)",
"bin": {
"btoa": "bin/btoa.js"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/buffer-crc32": { "node_modules/buffer-crc32": {
"version": "0.2.13", "version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
@ -6642,6 +6633,17 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-png": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
"license": "MIT",
"dependencies": {
"@types/pako": "^2.0.3",
"iobuffer": "^5.3.2",
"pako": "^2.1.0"
}
},
"node_modules/fast-shallow-equal": { "node_modules/fast-shallow-equal": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz",
@ -7683,6 +7685,12 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/iobuffer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
"license": "MIT"
},
"node_modules/is-array-buffer": { "node_modules/is-array-buffer": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@ -8304,14 +8312,13 @@
} }
}, },
"node_modules/jspdf": { "node_modules/jspdf": {
"version": "3.0.1", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.1.tgz", "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.3.tgz",
"integrity": "sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==", "integrity": "sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.26.7", "@babel/runtime": "^7.26.9",
"atob": "^2.1.2", "fast-png": "^6.2.0",
"btoa": "^1.2.1",
"fflate": "^0.8.1" "fflate": "^0.8.1"
}, },
"optionalDependencies": { "optionalDependencies": {
@ -8321,6 +8328,15 @@
"html2canvas": "^1.0.0-rc.5" "html2canvas": "^1.0.0-rc.5"
} }
}, },
"node_modules/jspdf-autotable": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.2.tgz",
"integrity": "sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==",
"license": "MIT",
"peerDependencies": {
"jspdf": "^2 || ^3"
}
},
"node_modules/jsx-ast-utils": { "node_modules/jsx-ast-utils": {
"version": "3.3.5", "version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@ -9196,6 +9212,12 @@
"quansync": "^0.2.7" "quansync": "^0.2.7"
} }
}, },
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/parent-module": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",

View File

@ -59,7 +59,8 @@
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"input-otp": "1.4.1", "input-otp": "1.4.1",
"jspdf": "^3.0.1", "jspdf": "^3.0.3",
"jspdf-autotable": "^5.0.2",
"keen-slider": "6.8.6", "keen-slider": "6.8.6",
"lucide-react": "^0.544.0", "lucide-react": "^0.544.0",
"mapbox-gl": "3.9.0", "mapbox-gl": "3.9.0",
@ -89,6 +90,7 @@
"@iconify/types": "2.0.0", "@iconify/types": "2.0.0",
"@iconify/utils": "2.2.1", "@iconify/utils": "2.2.1",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/jspdf": "^1.3.3",
"@types/mapbox-gl": "^3.4.1", "@types/mapbox-gl": "^3.4.1",
"@types/negotiator": "^0.6.3", "@types/negotiator": "^0.6.3",
"@types/node": "^22.10.2", "@types/node": "^22.10.2",

View File

@ -26,7 +26,7 @@ export const usePurchaseOrdersMutation = () => {
return response.data return response.data
}, },
onSuccess: () => { onSuccess: () => {
toast.success('Purchase Order created successfully!') toast.success('Purchase Order Payment successfully!')
queryClient.invalidateQueries({ queryKey: ['purchase-orders'] }) queryClient.invalidateQueries({ queryKey: ['purchase-orders'] })
}, },
onError: (error: any) => { onError: (error: any) => {
@ -34,5 +34,19 @@ export const usePurchaseOrdersMutation = () => {
} }
}) })
return { createPurchaseOrder, sendPaymentPurchaseOrder } const updateStatus = useMutation({
mutationFn: async ({ id, payload }: { id: string; payload: 'approved' | 'rejected' }) => {
const response = await api.put(`/purchase-orders/${id}`, { status: payload })
return response.data
},
onSuccess: () => {
toast.success('Purchase Order Status successfully!')
queryClient.invalidateQueries({ queryKey: ['purchase-orders'] })
},
onError: (error: any) => {
toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed')
}
})
return { createPurchaseOrder, sendPaymentPurchaseOrder, updateStatus }
} }

View File

@ -1,6 +1,6 @@
'use client' 'use client'
import React from 'react' import React, { useState } from 'react'
import { import {
Card, Card,
CardHeader, CardHeader,
@ -14,10 +14,19 @@ import {
TableRow, TableRow,
Box, Box,
Button, Button,
IconButton IconButton,
Menu,
MenuItem,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
CircularProgress
} from '@mui/material' } from '@mui/material'
import Grid from '@mui/material/Grid2' import Grid from '@mui/material/Grid2'
import { PurchaseOrder } from '@/types/services/purchaseOrder' import { PurchaseOrder, SendPaymentPurchaseOrderRequest } from '@/types/services/purchaseOrder'
import { usePurchaseOrdersMutation } from '@/services/mutations/purchaseOrder'
interface Props { interface Props {
data?: PurchaseOrder data?: PurchaseOrder
@ -26,6 +35,88 @@ interface Props {
const PurchaseDetailInformation = ({ data }: Props) => { const PurchaseDetailInformation = ({ data }: Props) => {
const purchaseOrder = data const purchaseOrder = data
// State for menu and dialog
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
const [isSubmitting, setIsSubmitting] = useState(false)
const [confirmDialog, setConfirmDialog] = useState<{
open: boolean
type: 'approve' | 'reject' | null
title: string
message: string
}>({
open: false,
type: null,
title: '',
message: ''
})
const { updateStatus } = usePurchaseOrdersMutation()
const handleMenuClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget)
}
const handleMenuClose = () => {
setAnchorEl(null)
}
const handleApproveClick = () => {
setConfirmDialog({
open: true,
type: 'approve',
title: 'Konfirmasi Persetujuan',
message: 'Apakah Anda yakin ingin menyetujui purchase order ini?'
})
handleMenuClose()
}
const handleRejectClick = () => {
setConfirmDialog({
open: true,
type: 'reject',
title: 'Konfirmasi Penolakan',
message: 'Apakah Anda yakin ingin menolak purchase order ini?'
})
handleMenuClose()
}
const handleConfirmAction = async () => {
if (!purchaseOrder?.id) return
setIsSubmitting(true)
try {
const status = confirmDialog.type === 'approve' ? 'approved' : 'rejected'
updateStatus.mutate({
id: purchaseOrder.id,
payload: status
})
// Close dialog after successful submission
setConfirmDialog({
open: false,
type: null,
title: '',
message: ''
})
} catch (error) {
console.error('Error updating status:', error)
// Handle error (you might want to show a toast or error message)
} finally {
setIsSubmitting(false)
}
}
const handleCancelAction = () => {
setConfirmDialog({
open: false,
type: null,
title: '',
message: ''
})
}
// Helper functions // Helper functions
const formatDate = (dateString: string): string => { const formatDate = (dateString: string): string => {
const date = new Date(dateString) const date = new Date(dateString)
@ -46,7 +137,8 @@ const PurchaseDetailInformation = ({ data }: Props) => {
sent: 'Dikirim', sent: 'Dikirim',
approved: 'Disetujui', approved: 'Disetujui',
received: 'Diterima', received: 'Diterima',
cancelled: 'Dibatalkan' cancelled: 'Dibatalkan',
rejected: 'Ditolak'
} }
return statusMap[status] || status return statusMap[status] || status
} }
@ -57,7 +149,8 @@ const PurchaseDetailInformation = ({ data }: Props) => {
sent: 'warning', sent: 'warning',
approved: 'success', approved: 'success',
received: 'info', received: 'info',
cancelled: 'error' cancelled: 'error',
rejected: 'error'
} }
return colorMap[status] || 'info' return colorMap[status] || 'info'
} }
@ -66,144 +159,214 @@ const PurchaseDetailInformation = ({ data }: Props) => {
const totalQuantity = (purchaseOrder?.items ?? []).reduce((sum, item) => sum + (item?.quantity ?? 0), 0) 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) const total = (purchaseOrder?.items ?? []).reduce((sum, item) => sum + (item?.amount ?? 0) * item?.quantity, 0)
// Check if actions should be available (only for certain statuses)
const canApproveOrReject = purchaseOrder?.status === 'received'
return ( return (
<Card sx={{ width: '100%' }}> <>
<CardHeader <Card sx={{ width: '100%' }}>
title={ <CardHeader
<Box display='flex' justifyContent='space-between' alignItems='center'> title={
<Typography variant='h5' color={getStatusColor(purchaseOrder?.status ?? '')} sx={{ fontWeight: 'bold' }}> <Box display='flex' justifyContent='space-between' alignItems='center'>
{getStatusLabel(purchaseOrder?.status ?? '')} <Typography variant='h5' color={getStatusColor(purchaseOrder?.status ?? '')} sx={{ fontWeight: 'bold' }}>
</Typography> {getStatusLabel(purchaseOrder?.status ?? '')}
<Box>
<Button startIcon={<i className='tabler-share' />} variant='outlined' size='small' sx={{ mr: 1 }}>
Bagikan
</Button>
<Button startIcon={<i className='tabler-printer' />} variant='outlined' size='small' sx={{ mr: 1 }}>
Print
</Button>
<IconButton>
<i className='tabler-dots-vertical' />
</IconButton>
</Box>
</Box>
}
/>
<CardContent>
{/* Purchase Information */}
<Grid container spacing={3} sx={{ mb: 4 }}>
<Grid size={{ xs: 12, md: 6 }}>
<Box sx={{ mb: 2 }}>
<Typography variant='subtitle2' color='text.secondary'>
Vendor
</Typography>
<Typography variant='body1' color='primary' sx={{ fontWeight: 'medium', cursor: 'pointer' }}>
{purchaseOrder?.vendor?.name ?? ''}
</Typography> </Typography>
<Box>
<Button startIcon={<i className='tabler-share' />} variant='outlined' size='small' sx={{ mr: 1 }}>
Bagikan
</Button>
<Button startIcon={<i className='tabler-printer' />} variant='outlined' size='small' sx={{ mr: 1 }}>
Print
</Button>
<IconButton onClick={handleMenuClick}>
<i className='tabler-dots-vertical' />
</IconButton>
</Box>
</Box> </Box>
}
/>
<Box> <CardContent>
<Typography variant='subtitle2' color='text.secondary'> {/* Purchase Information */}
Tgl. Transaksi <Grid container spacing={3} sx={{ mb: 4 }}>
</Typography>
<Typography variant='body1'>{formatDate(purchaseOrder?.transaction_date ?? '')}</Typography>
</Box>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Box sx={{ mb: 2 }}>
<Typography variant='subtitle2' color='text.secondary'>
Nomor
</Typography>
<Typography variant='body1'>{purchaseOrder?.po_number}</Typography>
</Box>
<Box>
<Typography variant='subtitle2' color='text.secondary'>
Tgl. Jatuh Tempo
</Typography>
<Typography variant='body1'>{formatDate(purchaseOrder?.due_date ?? '')}</Typography>
</Box>
</Grid>
</Grid>
{/* Products Table */}
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>Produk</TableCell>
<TableCell>Deskripsi</TableCell>
<TableCell align='center'>Kuantitas</TableCell>
<TableCell align='center'>Satuan</TableCell>
<TableCell align='right'>Harga</TableCell>
<TableCell align='right'>Jumlah</TableCell>
</TableRow>
</TableHead>
<TableBody>
{(purchaseOrder?.items ?? []).map((item, index) => {
return (
<TableRow key={item.id}>
<TableCell>
<Typography variant='body2' color='primary' sx={{ cursor: 'pointer' }}>
{item.ingredient.name}
</Typography>
</TableCell>
<TableCell>{item.description}</TableCell>
<TableCell align='center'>{item.quantity}</TableCell>
<TableCell align='center'>{item.unit.name}</TableCell>
<TableCell align='right'>{formatCurrency(item.amount)}</TableCell>
<TableCell align='right'>{formatCurrency(item.amount * item.quantity)}</TableCell>
</TableRow>
)
})}
{/* Total Quantity Row */}
<TableRow>
<TableCell colSpan={2} sx={{ fontWeight: 'bold', borderTop: '2px solid #e0e0e0' }}>
Total Kuantitas
</TableCell>
<TableCell align='center' sx={{ fontWeight: 'bold', borderTop: '2px solid #e0e0e0' }}>
{totalQuantity}
</TableCell>
<TableCell sx={{ borderTop: '2px solid #e0e0e0' }}></TableCell>
<TableCell sx={{ borderTop: '2px solid #e0e0e0' }}></TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
{/* Summary Section */}
<Box sx={{ mt: 3 }}>
<Grid container spacing={2}>
<Grid size={{ xs: 12, md: 6 }}>{/* Empty space for left side */}</Grid>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Box sx={{ display: 'flex', flexDirection: 'column' }}> <Box sx={{ mb: 2 }}>
<Box <Typography variant='subtitle2' color='text.secondary'>
sx={{ Vendor
display: 'flex', </Typography>
justifyContent: 'space-between', <Typography variant='body1' color='primary' sx={{ fontWeight: 'medium', cursor: 'pointer' }}>
alignItems: 'center', {purchaseOrder?.vendor?.name ?? ''}
py: 2, </Typography>
'&:hover': { </Box>
backgroundColor: 'rgba(0, 0, 0, 0.04)',
transition: 'background-color 0.15s ease' <Box>
} <Typography variant='subtitle2' color='text.secondary'>
}} Tgl. Transaksi
> </Typography>
<Typography variant='h6' sx={{ fontWeight: 'bold' }}> <Typography variant='body1'>{formatDate(purchaseOrder?.transaction_date ?? '')}</Typography>
Total </Box>
</Typography> </Grid>
<Typography variant='h6' sx={{ fontWeight: 'bold' }}>
{formatCurrency(total)} <Grid size={{ xs: 12, md: 6 }}>
</Typography> <Box sx={{ mb: 2 }}>
</Box> <Typography variant='subtitle2' color='text.secondary'>
Nomor
</Typography>
<Typography variant='body1'>{purchaseOrder?.po_number}</Typography>
</Box>
<Box>
<Typography variant='subtitle2' color='text.secondary'>
Tgl. Jatuh Tempo
</Typography>
<Typography variant='body1'>{formatDate(purchaseOrder?.due_date ?? '')}</Typography>
</Box> </Box>
</Grid> </Grid>
</Grid> </Grid>
</Box>
</CardContent> {/* Products Table */}
</Card> <TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>Produk</TableCell>
<TableCell>Deskripsi</TableCell>
<TableCell align='center'>Kuantitas</TableCell>
<TableCell align='center'>Satuan</TableCell>
<TableCell align='right'>Harga</TableCell>
<TableCell align='right'>Jumlah</TableCell>
</TableRow>
</TableHead>
<TableBody>
{(purchaseOrder?.items ?? []).map((item, index) => {
return (
<TableRow key={item.id}>
<TableCell>
<Typography variant='body2' color='primary' sx={{ cursor: 'pointer' }}>
{item.ingredient.name}
</Typography>
</TableCell>
<TableCell>{item.description}</TableCell>
<TableCell align='center'>{item.quantity}</TableCell>
<TableCell align='center'>{item.unit.name}</TableCell>
<TableCell align='right'>{formatCurrency(item.amount)}</TableCell>
<TableCell align='right'>{formatCurrency(item.amount * item.quantity)}</TableCell>
</TableRow>
)
})}
{/* Total Quantity Row */}
<TableRow>
<TableCell colSpan={2} sx={{ fontWeight: 'bold', borderTop: '2px solid #e0e0e0' }}>
Total Kuantitas
</TableCell>
<TableCell align='center' sx={{ fontWeight: 'bold', borderTop: '2px solid #e0e0e0' }}>
{totalQuantity}
</TableCell>
<TableCell sx={{ borderTop: '2px solid #e0e0e0' }}></TableCell>
<TableCell sx={{ borderTop: '2px solid #e0e0e0' }}></TableCell>
<TableCell sx={{ borderTop: '2px solid #e0e0e0' }}></TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
{/* Summary Section */}
<Box sx={{ mt: 3 }}>
<Grid container spacing={2}>
<Grid size={{ xs: 12, md: 6 }}>{/* Empty space for left side */}</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
py: 2,
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.04)',
transition: 'background-color 0.15s ease'
}
}}
>
<Typography variant='h6' sx={{ fontWeight: 'bold' }}>
Total
</Typography>
<Typography variant='h6' sx={{ fontWeight: 'bold' }}>
{formatCurrency(total)}
</Typography>
</Box>
</Box>
</Grid>
</Grid>
</Box>
</CardContent>
</Card>
{/* Menu */}
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
PaperProps={{
sx: {
minWidth: 160
}
}}
>
<MenuItem
onClick={handleApproveClick}
disabled={!canApproveOrReject}
sx={{
color: 'success.main',
'&:hover': {
backgroundColor: 'success.light',
color: 'success.dark'
}
}}
>
<i className='tabler-check' style={{ marginRight: 8 }} />
Disetujui
</MenuItem>
<MenuItem
onClick={handleRejectClick}
disabled={!canApproveOrReject}
sx={{
color: 'error.main',
'&:hover': {
backgroundColor: 'error.light',
color: 'error.dark'
}
}}
>
<i className='tabler-x' style={{ marginRight: 8 }} />
Ditolak
</MenuItem>
</Menu>
{/* Confirmation Dialog */}
<Dialog open={confirmDialog.open} onClose={handleCancelAction} maxWidth='sm' fullWidth>
<DialogTitle>{confirmDialog.title}</DialogTitle>
<DialogContent>
<DialogContentText>{confirmDialog.message}</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleCancelAction} color='inherit' disabled={isSubmitting}>
Batal
</Button>
<Button
onClick={handleConfirmAction}
variant='contained'
color={confirmDialog.type === 'approve' ? 'success' : 'error'}
disabled={isSubmitting}
startIcon={isSubmitting ? <CircularProgress size={16} /> : null}
autoFocus
>
{isSubmitting ? 'Memproses...' : confirmDialog.type === 'approve' ? 'Setujui' : 'Tolak'}
</Button>
</DialogActions>
</Dialog>
</>
) )
} }