From 0e8216b0c9492f97b715b2dcd90ee3413e502cf3 Mon Sep 17 00:00:00 2001 From: efrilm Date: Fri, 12 Sep 2025 03:11:01 +0700 Subject: [PATCH 01/23] Vendor Api --- src/services/api.ts | 2 +- src/services/queries/vendor.ts | 36 +++ src/types/services/vendor.ts | 23 ++ .../apps/vendor/list/VendorListTable.tsx | 240 +++++++++--------- 4 files changed, 179 insertions(+), 122 deletions(-) create mode 100644 src/services/queries/vendor.ts create mode 100644 src/types/services/vendor.ts 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/queries/vendor.ts b/src/services/queries/vendor.ts new file mode 100644 index 0000000..ce35d99 --- /dev/null +++ b/src/services/queries/vendor.ts @@ -0,0 +1,36 @@ +import { useQuery } from '@tanstack/react-query' +import { api } from '../api' +import { 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 + } + }) +} diff --git a/src/types/services/vendor.ts b/src/types/services/vendor.ts new file mode 100644 index 0000000..367a7d1 --- /dev/null +++ b/src/types/services/vendor.ts @@ -0,0 +1,23 @@ +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 +} diff --git a/src/views/apps/vendor/list/VendorListTable.tsx b/src/views/apps/vendor/list/VendorListTable.tsx index 754d059..9cf4527 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,15 +178,14 @@ 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} @@ -171,87 +193,50 @@ const VendorListTable = ({ tableData }: { tableData?: VendorType[] }) => {
) }), - 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 +274,88 @@ 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} - /> + /> */} ) } From 8026004630f5406c9a2552c50522256f6baba6ac Mon Sep 17 00:00:00 2001 From: efrilm Date: Fri, 12 Sep 2025 03:27:11 +0700 Subject: [PATCH 02/23] Update Vendor drawer --- src/types/services/vendor.ts | 12 + .../apps/vendor/list/AddVendorDrawer.tsx | 758 ++++++------------ .../apps/vendor/list/VendorListTable.tsx | 7 +- 3 files changed, 244 insertions(+), 533 deletions(-) diff --git a/src/types/services/vendor.ts b/src/types/services/vendor.ts index 367a7d1..2d098ae 100644 --- a/src/types/services/vendor.ts +++ b/src/types/services/vendor.ts @@ -21,3 +21,15 @@ export interface Vendors { 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/vendor/list/AddVendorDrawer.tsx b/src/views/apps/vendor/list/AddVendorDrawer.tsx index 42a1c3d..d84704b 100644 --- a/src/views/apps/vendor/list/AddVendorDrawer.tsx +++ b/src/views/apps/vendor/list/AddVendorDrawer.tsx @@ -10,54 +10,82 @@ 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' +// Backend Types +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 +} + +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 +} + type Props = { open: boolean handleClose: () => void - vendorData?: VendorType[] - setData: (data: VendorType[]) => void + onSubmit?: (vendorRequest: VendorRequest) => Promise } 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) => { // Props - const { open, handleClose, vendorData, setData } = props + const { open, handleClose, onSubmit } = 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) // Hooks const { @@ -69,92 +97,58 @@ const AddVendorDrawer = (props: Props) => { defaultValues: initialData }) - // Functions untuk alamat - const handleTambahAlamat = () => { - setAlamatPengiriman([...alamatPengiriman, '']) - } + const handleFormSubmit = async (data: FormValidateType) => { + try { + setIsSubmitting(true) - 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: '' + // Create VendorRequest object + const vendorRequest: VendorRequest = { + name: data.name, + email: data.email || undefined, + phone_number: data.phone_number || undefined, + address: data.address || undefined, + contact_person: data.contact_person || undefined, + tax_number: data.tax_number || undefined, + payment_terms: data.payment_terms || undefined, + notes: data.notes || undefined, + is_active: data.is_active } - ]) - } - const handleHapusRekening = (index: number) => { - if (rekeningBank.length > 1) { - const newRekening = rekeningBank.filter((_, i) => i !== index) - setRekeningBank(newRekening) - } - } - - const handleChangeRekening = (index: number, field: string, value: string) => { - const newRekening = [...rekeningBank] - newRekening[index] = { ...newRekening[index], [field]: value } - setRekeningBank(newRekening) - } - - const onSubmit = (data: FormValidateType) => { - const newVendor: VendorType = { - id: (vendorData?.length && vendorData?.length + 1) || 1, - photo: '', - name: data.name, - company: data.company, - email: data.email, - telephone: data.telephone, - youPayable: 0, - theyPayable: 0 - } - - setData([...(vendorData ?? []), newVendor]) - handleClose() - resetForm(initialData) - setAlamatPengiriman(['']) - setRekeningBank([ - { - bank: '', - cabang: '', - namaPemilik: '', - nomorRekening: '' + // Call the onSubmit prop if provided (for API call) + if (onSubmit) { + await onSubmit(vendorRequest) + } else { + // Fallback: Create local vendor object for local state update + const newVendor: Vendor = { + id: `temp-${Date.now()}`, // Temporary ID + organization_id: 'current-org', // Should be provided by context + name: vendorRequest.name, + email: vendorRequest.email, + phone_number: vendorRequest.phone_number, + address: vendorRequest.address, + contact_person: vendorRequest.contact_person, + tax_number: vendorRequest.tax_number, + payment_terms: vendorRequest.payment_terms, + notes: vendorRequest.notes, + is_active: vendorRequest.is_active, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + } } - ]) - setShowMore(false) - setShowPemetaanAkun(false) + + handleReset() + } 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 ( @@ -194,472 +188,182 @@ 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)}> +
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 + {/* Sembunyikan */} +
setShowMore(false)}> + + + Sembunyikan
- - {/* 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 - -
-
)}
@@ -679,10 +383,10 @@ const AddVendorDrawer = (props: Props) => { }} >
- -
diff --git a/src/views/apps/vendor/list/VendorListTable.tsx b/src/views/apps/vendor/list/VendorListTable.tsx index 9cf4527..064fa4c 100644 --- a/src/views/apps/vendor/list/VendorListTable.tsx +++ b/src/views/apps/vendor/list/VendorListTable.tsx @@ -350,12 +350,7 @@ const VendorListTable = () => { disabled={isLoading} /> - {/* setAddVendorOpen(!addVendorOpen)} - vendorData={data} - setData={setData} - /> */} + setAddVendorOpen(!addVendorOpen)} /> ) } From 3a74e32e64c583f4ef1e04a10ae7d1b746aa3318 Mon Sep 17 00:00:00 2001 From: efrilm Date: Fri, 12 Sep 2025 13:52:11 +0700 Subject: [PATCH 03/23] Create Vendor --- src/services/mutations/vendor.ts | 52 ++++++++ .../apps/vendor/list/AddVendorDrawer.tsx | 115 +++++++----------- .../apps/vendor/list/VendorListTable.tsx | 1 - 3 files changed, 97 insertions(+), 71 deletions(-) create mode 100644 src/services/mutations/vendor.ts 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/views/apps/vendor/list/AddVendorDrawer.tsx b/src/views/apps/vendor/list/AddVendorDrawer.tsx index d84704b..c867682 100644 --- a/src/views/apps/vendor/list/AddVendorDrawer.tsx +++ b/src/views/apps/vendor/list/AddVendorDrawer.tsx @@ -18,40 +18,12 @@ import { useForm, Controller } from 'react-hook-form' // Component Imports import CustomTextField from '@core/components/mui/TextField' - -// Backend Types -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 -} - -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 -} +import { VendorRequest } from '@/types/services/vendor' +import { useVendorsMutation } from '@/services/mutations/vendor' type Props = { open: boolean handleClose: () => void - onSubmit?: (vendorRequest: VendorRequest) => Promise } type FormValidateType = { @@ -81,12 +53,14 @@ const initialData: FormValidateType = { const AddVendorDrawer = (props: Props) => { // Props - const { open, handleClose, onSubmit } = props + const { open, handleClose } = props // States const [showMore, setShowMore] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) + const { createVendor, updateVendor } = useVendorsMutation() + // Hooks const { control, @@ -114,29 +88,12 @@ const AddVendorDrawer = (props: Props) => { is_active: data.is_active } - // Call the onSubmit prop if provided (for API call) - if (onSubmit) { - await onSubmit(vendorRequest) - } else { - // Fallback: Create local vendor object for local state update - const newVendor: Vendor = { - id: `temp-${Date.now()}`, // Temporary ID - organization_id: 'current-org', // Should be provided by context - name: vendorRequest.name, - email: vendorRequest.email, - phone_number: vendorRequest.phone_number, - address: vendorRequest.address, - contact_person: vendorRequest.contact_person, - tax_number: vendorRequest.tax_number, - payment_terms: vendorRequest.payment_terms, - notes: vendorRequest.notes, - is_active: vendorRequest.is_active, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString() + createVendor.mutate(vendorRequest, { + onSuccess: () => { + handleReset() + handleClose() } - } - - handleReset() + }) } catch (error) { console.error('Error submitting vendor:', error) // Handle error (show toast, etc.) @@ -214,12 +171,13 @@ const AddVendorDrawer = (props: Props) => { {/* Email */}
- Email + Email * { {/* Nomor Telepon */}
- Nomor Telepon + Nomor Telepon * } + rules={{ required: 'Telepon wajib diisi' }} + render={({ field }) => ( + + )} />
{/* Contact Person */}
- Contact Person + Contact Person * } + rules={{ required: 'Contact Person wajib diisi' }} + render={({ field }) => ( + + )} />
@@ -278,12 +247,15 @@ const AddVendorDrawer = (props: Props) => { {/* Tampilkan selengkapnya */} {!showMore && ( -
setShowMore(true)}> - - - Tampilkan selengkapnya - -
+ )} {/* Konten tambahan */} @@ -358,12 +330,15 @@ const AddVendorDrawer = (props: Props) => {
{/* Sembunyikan */} -
setShowMore(false)}> - - - Sembunyikan - -
+ )}
diff --git a/src/views/apps/vendor/list/VendorListTable.tsx b/src/views/apps/vendor/list/VendorListTable.tsx index 064fa4c..9339348 100644 --- a/src/views/apps/vendor/list/VendorListTable.tsx +++ b/src/views/apps/vendor/list/VendorListTable.tsx @@ -235,7 +235,6 @@ const VendorListTable = () => { return ( <> - {/* */}
Date: Fri, 12 Sep 2025 14:02:57 +0700 Subject: [PATCH 04/23] Vendor Slice --- src/redux-store/index.ts | 4 +- src/redux-store/slices/vendor.ts | 43 +++++++++++++++++++ .../apps/vendor/list/VendorListTable.tsx | 2 +- 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 src/redux-store/slices/vendor.ts diff --git a/src/redux-store/index.ts b/src/redux-store/index.ts index d29167b..1bc3401 100644 --- a/src/redux-store/index.ts +++ b/src/redux-store/index.ts @@ -9,6 +9,7 @@ import orderReducer from '@/redux-store/slices/order' import productRecipeReducer from '@/redux-store/slices/productRecipe' import organizationReducer from '@/redux-store/slices/organization' import userReducer from '@/redux-store/slices/user' +import vendorReducer from '@/redux-store/slices/vendor' export const store = configureStore({ reducer: { @@ -19,7 +20,8 @@ export const store = configureStore({ orderReducer, productRecipeReducer, organizationReducer, - userReducer + userReducer, + vendorReducer }, middleware: getDefaultMiddleware => 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/views/apps/vendor/list/VendorListTable.tsx b/src/views/apps/vendor/list/VendorListTable.tsx index 9339348..b631f36 100644 --- a/src/views/apps/vendor/list/VendorListTable.tsx +++ b/src/views/apps/vendor/list/VendorListTable.tsx @@ -184,7 +184,7 @@ const VendorListTable = () => {
- + {row.original.contact_person} From 40c417ec72894cc9b8edeeb017704c48fbe3ea84 Mon Sep 17 00:00:00 2001 From: efrilm Date: Fri, 12 Sep 2025 15:23:19 +0700 Subject: [PATCH 05/23] Delete and Edit Vendor --- .../apps/vendor/{ => [id]}/detail/page.tsx | 0 src/services/queries/vendor.ts | 12 +- .../detail/vendor-overview/VendorDetails.tsx | 239 +++++++++++------- .../apps/vendor/list/AddVendorDrawer.tsx | 94 +++++-- .../apps/vendor/list/VendorListTable.tsx | 4 +- 5 files changed, 237 insertions(+), 112 deletions(-) rename src/app/[lang]/(dashboard)/(private)/apps/vendor/{ => [id]}/detail/page.tsx (100%) 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/services/queries/vendor.ts b/src/services/queries/vendor.ts index ce35d99..33dff13 100644 --- a/src/services/queries/vendor.ts +++ b/src/services/queries/vendor.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query' import { api } from '../api' -import { Vendors } from '@/types/services/vendor' +import { Vendor, Vendors } from '@/types/services/vendor' interface VendorQueryParams { page?: number @@ -34,3 +34,13 @@ export function useVendors(params: VendorQueryParams = {}) { } }) } + +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/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 c867682..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' @@ -18,12 +18,13 @@ import { useForm, Controller } from 'react-hook-form' // Component Imports import CustomTextField from '@core/components/mui/TextField' -import { VendorRequest } from '@/types/services/vendor' +import { Vendor, VendorRequest } from '@/types/services/vendor' import { useVendorsMutation } from '@/services/mutations/vendor' type Props = { open: boolean handleClose: () => void + data?: Vendor // Data vendor untuk edit (jika ada) } type FormValidateType = { @@ -51,9 +52,9 @@ const initialData: FormValidateType = { is_active: true } -const AddVendorDrawer = (props: Props) => { +const AddEditVendorDrawer = (props: Props) => { // Props - const { open, handleClose } = props + const { open, handleClose, data } = props // States const [showMore, setShowMore] = useState(false) @@ -61,6 +62,9 @@ const AddVendorDrawer = (props: Props) => { const { createVendor, updateVendor } = useVendorsMutation() + // Determine if this is edit mode + const isEditMode = Boolean(data?.id) + // Hooks const { control, @@ -71,29 +75,73 @@ const AddVendorDrawer = (props: Props) => { defaultValues: initialData }) - const handleFormSubmit = async (data: FormValidateType) => { + // 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 + } + + resetForm(formData) + + // 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) + } + } 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: data.name, - email: data.email || undefined, - phone_number: data.phone_number || undefined, - address: data.address || undefined, - contact_person: data.contact_person || undefined, - tax_number: data.tax_number || undefined, - payment_terms: data.payment_terms || undefined, - notes: data.notes || undefined, - is_active: data.is_active + 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 } - createVendor.mutate(vendorRequest, { - onSuccess: () => { - handleReset() - handleClose() - } - }) + 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.) @@ -136,7 +184,7 @@ const AddVendorDrawer = (props: Props) => { }} >
- Tambah Vendor Baru + {isEditMode ? 'Edit Vendor' : 'Tambah Vendor Baru'} @@ -359,7 +407,7 @@ const AddVendorDrawer = (props: Props) => { >
+ {!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..d5d1bf5 100644 --- a/src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailUnit.tsx +++ b/src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailUnit.tsx @@ -2,8 +2,13 @@ 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' -const IngredientDetailUnit = () => { +interface Props { + data: Ingredient | undefined +} + +const IngredientDetailUnit = ({ data }: Props) => { // State untuk mengontrol drawer const [openConversionDrawer, setOpenConversionDrawer] = useState(false) @@ -34,7 +39,7 @@ const IngredientDetailUnit = () => { Satuan Dasar - : Pcs + : {data?.unit.name ?? '-'} @@ -61,6 +66,7 @@ const IngredientDetailUnit = () => { open={openConversionDrawer} handleClose={handleCloseConversionDrawer} setData={handleSetConversionData} + data={data} /> ) diff --git a/src/views/apps/ecommerce/products/ingredient/detail/index.tsx b/src/views/apps/ecommerce/products/ingredient/detail/index.tsx index d3b4ccf..c9dd062 100644 --- a/src/views/apps/ecommerce/products/ingredient/detail/index.tsx +++ b/src/views/apps/ecommerce/products/ingredient/detail/index.tsx @@ -6,11 +6,18 @@ import IngredientDetailInfo from './IngredientDetailInfo' import IngredientDetailUnit from './IngredientDetailUnit' import IngredientDetailStockAdjustmentDrawer from './IngredientDetailStockAdjustmentDrawer' // Sesuaikan dengan path file Anda import { Button } from '@mui/material' +import { useParams } from 'next/navigation' +import { useIngredientById } from '@/services/queries/ingredients' const IngredientDetail = () => { // State untuk mengontrol stock adjustment drawer const [openStockAdjustmentDrawer, setOpenStockAdjustmentDrawer] = useState(false) + const params = useParams() + const id = params?.id + + const { data, isLoading } = useIngredientById(id as string) + // Function untuk membuka stock adjustment drawer const handleOpenStockAdjustmentDrawer = () => { setOpenStockAdjustmentDrawer(true) @@ -32,7 +39,7 @@ const IngredientDetail = () => { <> - + - + From 76ee71e7fed8e1a0de51cbe3bbe14fb7f460aed5 Mon Sep 17 00:00:00 2001 From: efrilm Date: Fri, 12 Sep 2025 18:57:23 +0700 Subject: [PATCH 07/23] Get Unit Converter --- src/services/queries/unitConverter.ts | 13 ++++++++++ src/types/services/productRecipe.ts | 26 +++++++++++++++++++ .../detail/IngredientDetailUnit.tsx | 13 ++++++++++ 3 files changed, 52 insertions(+) create mode 100644 src/services/queries/unitConverter.ts 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/types/services/productRecipe.ts b/src/types/services/productRecipe.ts index fe1733f..d13ce3f 100644 --- a/src/types/services/productRecipe.ts +++ b/src/types/services/productRecipe.ts @@ -73,3 +73,29 @@ export interface IngredientUnitConverterRequest { 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/views/apps/ecommerce/products/ingredient/detail/IngredientDetailUnit.tsx b/src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailUnit.tsx index d5d1bf5..31e8cde 100644 --- a/src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailUnit.tsx +++ b/src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailUnit.tsx @@ -3,6 +3,7 @@ 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' interface Props { data: Ingredient | undefined @@ -12,6 +13,8 @@ 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) @@ -42,6 +45,16 @@ const IngredientDetailUnit = ({ data }: Props) => { : {data?.unit.name ?? '-'} + {unitConverters?.map(unitConverter => ( + + + 1 {unitConverter.from_unit.name} + + + : {unitConverter.conversion_factor} {unitConverter.to_unit.name} + + + )) ?? []} ) }), @@ -393,26 +247,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 +271,7 @@ const AccountListTable = () => { ) const table = useReactTable({ - data: paginatedData as AccountType[], + data: accounts as Account[], columns, filterFns: { fuzzy: fuzzyFilter @@ -512,7 +361,7 @@ const AccountListTable = () => { ))} - {filteredData.length === 0 ? ( + {accounts.length === 0 ? ( @@ -558,15 +407,16 @@ const AccountListTable = () => { onPageChange={handlePageChange} onRowsPerPageChange={handlePageSizeChange} rowsPerPageOptions={[10, 25, 50]} + disabled={isLoading} /> - + /> */} ) } From ce344e4a9821754e73f1ccee25cd6f3dea2ce1aa Mon Sep 17 00:00:00 2001 From: efrilm Date: Fri, 12 Sep 2025 19:52:46 +0700 Subject: [PATCH 12/23] Get Account --- src/views/apps/account/AccountListTable.tsx | 118 ++++++++++---------- 1 file changed, 61 insertions(+), 57 deletions(-) diff --git a/src/views/apps/account/AccountListTable.tsx b/src/views/apps/account/AccountListTable.tsx index 235645c..8a11b5e 100644 --- a/src/views/apps/account/AccountListTable.tsx +++ b/src/views/apps/account/AccountListTable.tsx @@ -333,63 +333,67 @@ const AccountListTable = () => {
- - - {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())}
+ {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())}
+ )}
Date: Fri, 12 Sep 2025 20:35:49 +0700 Subject: [PATCH 13/23] Create And Update Account --- src/services/mutations/account.ts | 52 +++++ src/services/queries/chartOfAccountType.ts | 9 + src/views/apps/account/AccountFormDrawer.tsx | 220 +++++++++++-------- src/views/apps/account/AccountListTable.tsx | 12 +- 4 files changed, 202 insertions(+), 91 deletions(-) create mode 100644 src/services/mutations/account.ts 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/queries/chartOfAccountType.ts b/src/services/queries/chartOfAccountType.ts index 0173626..504c9f0 100644 --- a/src/services/queries/chartOfAccountType.ts +++ b/src/services/queries/chartOfAccountType.ts @@ -34,3 +34,12 @@ export function useChartOfAccountTypes(params: ChartOfAccountQueryParams = {}) { } }) } + +export interface AccountRequest { + chart_of_account_id: string + name: string + number: string + account_type: string + opening_balance: number + description: string +} diff --git a/src/views/apps/account/AccountFormDrawer.tsx b/src/views/apps/account/AccountFormDrawer.tsx index 64f2c8a..2651457 100644 --- a/src/views/apps/account/AccountFormDrawer.tsx +++ b/src/views/apps/account/AccountFormDrawer.tsx @@ -14,41 +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' -import { useChartOfAccountTypes } from '@/services/queries/chartOfAccountType' +import { AccountRequest } from '@/services/queries/chartOfAccountType' import { useChartOfAccount } from '@/services/queries/chartOfAccount' - -// Account Type -export type AccountType = { - id: number - code: string - name: string - category: string - balance: string -} +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 } // 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 @@ -56,30 +67,20 @@ const AccountFormDrawer = (props: Props) => { // Determine if we're editing const isEdit = !!editingAccount - const { data: accountTypes, isLoading } = useChartOfAccountTypes() - const { data: accounts, isLoading: isLoadingAccounts } = useChartOfAccount({ page: 1, limit: 100 }) - // Process account types for the dropdown - const categoryOptions = accountTypes?.data.length - ? accountTypes.data - .filter(type => type.is_active) // Only show active types - .map(type => ({ - id: type.id, - name: type.name, - code: type.code, - description: type.description - })) - : [] + const { createAccount, updateAccount } = useAccountsMutation() - // Process accounts for parent account dropdown - const parentAccountOptions = accounts?.data.length + // 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 - .filter(account => (editingAccount ? account.id !== editingAccount.id.toString() : true)) // Exclude current account when editing .map(account => ({ id: account.id, code: account.code, @@ -105,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 @@ -118,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 = () => { @@ -233,61 +241,58 @@ const AccountFormDrawer = (props: Props) => { />
- {/* Kategori */} + {/* Tipe Akun */}
- Kategori * + Tipe Akun * ( option.name === value) || null} - onChange={(_, newValue) => onChange(newValue?.name || '')} + options={accountTypeOptions} + value={accountTypeOptions.find(option => option.code === value) || null} + onChange={(_, newValue) => onChange(newValue?.code || '')} getOptionLabel={option => option.name} renderOption={(props, option) => (
- - {option.code} - {option.name} - + {option.name}
)} renderInput={params => ( )} - isOptionEqualToValue={(option, value) => option.name === value.name} - disabled={isLoading} + 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}` : '')} + options={chartOfAccountOptions} + value={chartOfAccountOptions.find(option => option.id === value) || null} + onChange={(_, newValue) => onChange(newValue?.id || '')} getOptionLabel={option => `${option.code} - ${option.name}`} renderOption={(props, option) => ( @@ -306,18 +311,59 @@ const AccountFormDrawer = (props: Props) => { renderInput={params => ( )} - isOptionEqualToValue={(option, value) => - `${option.code} ${option.name}` === `${value.code} ${value.name}` - } + isOptionEqualToValue={(option, value) => option.id === value.id} disabled={isLoadingAccounts} - noOptionsText={isLoadingAccounts ? 'Loading...' : 'Tidak ada akun tersedia'} + 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 8a11b5e..5ceabc8 100644 --- a/src/views/apps/account/AccountListTable.tsx +++ b/src/views/apps/account/AccountListTable.tsx @@ -226,6 +226,10 @@ const AccountListTable = () => { variant='text' color='primary' className='p-0 min-w-0 font-medium normal-case justify-start' + onClick={() => { + setEditingAccount(row.original) + setAddAccountOpen(true) + }} sx={{ textTransform: 'none', fontWeight: 500, @@ -414,13 +418,13 @@ const AccountListTable = () => { disabled={isLoading} /> - {/* {}} editingAccount={editingAccount} - /> */} + /> ) } From 20203942d21c6e984ab5f21250ffe68604da9f38 Mon Sep 17 00:00:00 2001 From: efrilm Date: Fri, 12 Sep 2025 21:03:38 +0700 Subject: [PATCH 14/23] Cash Bank List --- src/views/apps/cash-bank/CashBankList.tsx | 429 ++++++++++------------ 1 file changed, 203 insertions(+), 226 deletions(-) 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 + + + )} + Date: Fri, 12 Sep 2025 21:54:27 +0700 Subject: [PATCH 15/23] 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 ?? '-'} + + + + )}
Date: Fri, 12 Sep 2025 22:15:16 +0700 Subject: [PATCH 16/23] Ingredient in purchase --- .../PurchaseIngredientsTable.tsx | 97 ++++++++++++++++--- 1 file changed, 84 insertions(+), 13 deletions(-) diff --git a/src/views/apps/purchase/purchase-form/PurchaseIngredientsTable.tsx b/src/views/apps/purchase/purchase-form/PurchaseIngredientsTable.tsx index 8514a0a..9c587d7 100644 --- a/src/views/apps/purchase/purchase-form/PurchaseIngredientsTable.tsx +++ b/src/views/apps/purchase/purchase-form/PurchaseIngredientsTable.tsx @@ -1,6 +1,6 @@ 'use client' -import React from 'react' +import React, { useMemo } from 'react' import { Button, Typography, @@ -11,12 +11,14 @@ import { TableContainer, TableHead, TableRow, - Paper + Paper, + CircularProgress } 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' +import { useIngredients } from '@/services/queries/ingredients' interface PurchaseIngredientsTableProps { formData: PurchaseOrderFormData @@ -31,14 +33,21 @@ const PurchaseIngredientsTable: React.FC = ({ 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 From d54d623d4cea850c35515490129e28eb016832b2 Mon Sep 17 00:00:00 2001 From: efrilm Date: Sat, 13 Sep 2025 01:34:43 +0700 Subject: [PATCH 17/23] Create Purchase --- src/services/mutations/purchaseOrder.ts | 14 +- src/types/apps/purchaseOrderTypes.ts | 3 - .../purchase-form/PurchaseAddForm.tsx | 786 ++++++++++++++++-- 3 files changed, 733 insertions(+), 70 deletions(-) diff --git a/src/services/mutations/purchaseOrder.ts b/src/services/mutations/purchaseOrder.ts index 6fc2d3b..7ec250f 100644 --- a/src/services/mutations/purchaseOrder.ts +++ b/src/services/mutations/purchaseOrder.ts @@ -3,20 +3,22 @@ import { toast } from 'react-toastify' import { api } from '../api' import { PurchaseOrderRequest } from '@/types/services/purchaseOrder' -export const useVendorsMutation = () => { +export const usePurchaseOrdersMutation = () => { const queryClient = useQueryClient() - const createVendor = useMutation({ - mutationFn: async (newVendor: PurchaseOrderRequest) => { - const response = await api.post('/vendors', newVendor) + const createPurchaseOrder = useMutation({ + mutationFn: async (newPurchaseOrder: PurchaseOrderRequest) => { + const response = await api.post('/purchase-orders', newPurchaseOrder) return response.data }, onSuccess: () => { - toast.success('Vendor created successfully!') - queryClient.invalidateQueries({ queryKey: ['vendors'] }) + 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/types/apps/purchaseOrderTypes.ts b/src/types/apps/purchaseOrderTypes.ts index 987560f..1072bd8 100644 --- a/src/types/apps/purchaseOrderTypes.ts +++ b/src/types/apps/purchaseOrderTypes.ts @@ -16,10 +16,7 @@ export interface IngredientItem { 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 } diff --git a/src/views/apps/purchase/purchase-form/PurchaseAddForm.tsx b/src/views/apps/purchase/purchase-form/PurchaseAddForm.tsx index 7a158d9..a6feab3 100644 --- a/src/views/apps/purchase/purchase-form/PurchaseAddForm.tsx +++ b/src/views/apps/purchase/purchase-form/PurchaseAddForm.tsx @@ -1,52 +1,178 @@ '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 +} 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' + +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 type IngredientItem = { + id: string + organization_id: string + outlet_id: string + 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: Unit +} + +export type Unit = { + id: string + name: string + // Add other unit properties as needed +} + +// Internal form state interface for UI management +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[] +} + +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 +} const PurchaseAddForm: React.FC = () => { + const [imageUrl, setImageUrl] = useState('') 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: '', + po_number: '', + transaction_date: '', + due_date: '', + reference: '', + status: 'draft', // Bottom section toggles showPesan: false, showAttachment: false, - showTambahDiskon: false, - showBiayaPengiriman: false, - showBiayaTransaksi: false, - showUangMuka: false, - pesan: '', - // Ingredient items (updated from productItems) - ingredientItems: [ + message: '', + // Items + 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 // This includes the full IngredientItem with unit, cost, etc. + })) + }, [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]) + + // Handler Functions const handleInputChange = (field: keyof PurchaseOrderFormData, value: any): void => { setFormData(prev => ({ ...prev, @@ -54,64 +180,602 @@ const PurchaseAddForm: React.FC = () => { })) } - 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') { + // Auto-calculate total if amount or quantity changes + 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 } }) } - const addIngredientItem = (): void => { - const newItem: IngredientItem = { + const handleIngredientSelection = (index: number, selectedIngredient: any) => { + handleItemChange(index, 'ingredient', selectedIngredient) + + // Auto-populate related fields if available in the ingredient data + if (selectedIngredient) { + const ingredientData: IngredientItem = selectedIngredient.originalData || selectedIngredient + + // Auto-fill unit based on IngredientItem structure + if (ingredientData.unit_id || ingredientData.unit) { + let unitToFind = null + + // If ingredient has unit object (populated relation) + if (ingredientData.unit && typeof ingredientData.unit === 'object') { + unitToFind = ingredientData.unit + } + // If ingredient has unit_id, find the unit from unitOptions + else if (ingredientData.unit_id) { + unitToFind = unitOptions.find(option => option.value === ingredientData.unit_id) + } + + if (unitToFind) { + // Create unit option object + 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) + } + } + + // Auto-fill amount with cost from IngredientItem + if (ingredientData.cost !== undefined && ingredientData.cost !== null) { + handleItemChange(index, 'amount', ingredientData.cost) + } + + // Auto-fill description with ingredient name + 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) })) } + // 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 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', 'Purchase image') + + mutate(formData, { + onSuccess: data => { + // pemakaian: + setFormData(prev => ({ + ...prev, + attachment_file_ids: upsertAttachment(prev.attachment_file_ids, data.id) + })) + setImageUrl(data.file_url) + resolve(data.id) // <-- balikin id file yang berhasil diupload + }, + onError: error => { + reject(error) // biar async/await bisa tangkep error + } + }) + }) + } + + // Calculate subtotal from items + const subtotal = formData.items.reduce((sum, item) => sum + (item.total || 0), 0) + + // Convert form data to API request format + 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) // Only include valid items + .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 = () => { + createPurchaseOrder.mutate(convertToApiRequest(), { + onSuccess: () => { + window.history.back() + } + }) + } + return ( - {/* Basic Info Section */} - + {/* BASIC INFO SECTION */} + {/* Row 1 - Vendor and PO Number */} + + { + handleInputChange('vendor', newValue) + if (newValue?.value) { + const selectedVendorData = vendors?.find(vendor => vendor.id === newValue.value) + console.log('Vendor selected:', selectedVendorData) + } + }} + loading={isLoadingVendors} + renderInput={params => ( + + )} + /> + {getSelectedVendorData() && ( + + + + + {getSelectedVendorData()?.contact_person ?? ''} + + + + + + {getSelectedVendorData()?.address ?? '-'} + + + + + + {getSelectedVendorData()?.phone_number ?? '-'} + + + + )} + + + ) => handleInputChange('po_number', e.target.value)} + /> + - {/* Ingredients Table Section */} - + {/* Row 2 - Transaction Date, Due Date, Status */} + + ) => + handleInputChange('transaction_date', e.target.value) + } + InputLabelProps={{ + shrink: true + }} + /> + + + ) => handleInputChange('due_date', e.target.value)} + InputLabelProps={{ + shrink: true + }} + /> + + + ) => handleInputChange('reference', e.target.value)} + /> + - {/* Summary Section */} - + {/* ITEMS TABLE SECTION */} + + + Purchase Order Items + + + + + + + Ingredient + Description + Quantity + Unit + Amount + 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='Description' + /> + + + ) => + 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) + }} + inputProps={{ min: 0, step: 'any' }} + placeholder='0' + /> + + + + + + 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 */} + + + + +
From 98d6446b0ce5276fdfa215c1d761a355a24f45f3 Mon Sep 17 00:00:00 2001 From: efrilm Date: Sat, 13 Sep 2025 02:42:39 +0700 Subject: [PATCH 18/23] Purchase Order table --- src/components/StatusFilterTab.tsx | 16 +- src/services/queries/purchaseOrder.ts | 41 ++++ src/types/services/purchaseOrder.ts | 76 +++++++ .../list/PurchaseOrderListTable.tsx | 200 ++++++++---------- 4 files changed, 214 insertions(+), 119 deletions(-) create mode 100644 src/services/queries/purchaseOrder.ts 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/services/queries/purchaseOrder.ts b/src/services/queries/purchaseOrder.ts new file mode 100644 index 0000000..916f8b5 --- /dev/null +++ b/src/services/queries/purchaseOrder.ts @@ -0,0 +1,41 @@ +import { 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 + } + }) +} diff --git a/src/types/services/purchaseOrder.ts b/src/types/services/purchaseOrder.ts index 6337ad7..ee9153b 100644 --- a/src/types/services/purchaseOrder.ts +++ b/src/types/services/purchaseOrder.ts @@ -1,3 +1,5 @@ +import { Vendor } from './vendor' + export interface PurchaseOrderRequest { vendor_id: string // uuid.UUID po_number: string @@ -17,3 +19,77 @@ export interface PurchaseOrderItemRequest { 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 +} diff --git a/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx b/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx index 33cbab8..4591bf9 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 } @@ -135,46 +138,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,7 +203,7 @@ 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 +241,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 +263,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 +297,11 @@ const PurchaseOrderListTable = () => { {/* Filter Status Tabs */}
- {['Semua', 'Draft', 'Disetujui', 'Dikirim Sebagian', 'Selesai', 'Lainnya'].map(status => ( - - ))} +
@@ -378,56 +343,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} />
From 222beb5043315979267f7ae93b31165c51fdce94 Mon Sep 17 00:00:00 2001 From: efrilm Date: Sat, 13 Sep 2025 03:03:08 +0700 Subject: [PATCH 19/23] Purchase create --- src/types/apps/purchaseOrderTypes.ts | 40 -- src/types/services/purchaseOrder.ts | 25 ++ .../purchase-form/PurchaseAddForm.tsx | 359 ++++++++++++------ 3 files changed, 278 insertions(+), 146 deletions(-) diff --git a/src/types/apps/purchaseOrderTypes.ts b/src/types/apps/purchaseOrderTypes.ts index 1072bd8..3fece1c 100644 --- a/src/types/apps/purchaseOrderTypes.ts +++ b/src/types/apps/purchaseOrderTypes.ts @@ -10,46 +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 - harga: number - 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/purchaseOrder.ts b/src/types/services/purchaseOrder.ts index ee9153b..92a3def 100644 --- a/src/types/services/purchaseOrder.ts +++ b/src/types/services/purchaseOrder.ts @@ -1,3 +1,4 @@ +import { IngredientItem } from './ingredient' import { Vendor } from './vendor' export interface PurchaseOrderRequest { @@ -93,3 +94,27 @@ export interface PurchaseOrderFile { 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/views/apps/purchase/purchase-form/PurchaseAddForm.tsx b/src/views/apps/purchase/purchase-form/PurchaseAddForm.tsx index a6feab3..89a74e2 100644 --- a/src/views/apps/purchase/purchase-form/PurchaseAddForm.tsx +++ b/src/views/apps/purchase/purchase-form/PurchaseAddForm.tsx @@ -15,7 +15,10 @@ import { TableHead, TableRow, Paper, - CircularProgress + CircularProgress, + Alert, + Popover, + Divider } from '@mui/material' import Grid from '@mui/material/Grid2' import CustomAutocomplete from '@/@core/components/mui/Autocomplete' @@ -27,88 +30,113 @@ import { useIngredients } from '@/services/queries/ingredients' import { useUnits } from '@/services/queries/units' import { useFilesMutation } from '@/services/mutations/files' import { usePurchaseOrdersMutation } from '@/services/mutations/purchaseOrder' - -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 type IngredientItem = { - id: string - organization_id: string - outlet_id: string - 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: Unit -} +import { PurchaseOrderFormData, PurchaseOrderFormItem, PurchaseOrderRequest } from '@/types/services/purchaseOrder' +import { IngredientItem } from '@/types/services/ingredient' export type Unit = { id: string name: string - // Add other unit properties as needed } -// Internal form state interface for UI management -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[] +interface ValidationErrors { + vendor?: string + po_number?: string + transaction_date?: string + due_date?: string + items?: string + general?: string } -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 +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, po_number: '', transaction_date: '', due_date: '', reference: '', - status: 'draft', - // Bottom section toggles + status: 'sent', showPesan: false, showAttachment: false, message: '', - // Items items: [ { id: 1, @@ -154,7 +182,7 @@ const PurchaseAddForm: React.FC = () => { label: ingredient.name, value: ingredient.id, id: ingredient.id, - originalData: ingredient // This includes the full IngredientItem with unit, cost, etc. + originalData: ingredient })) }, [ingredients, isLoadingIngredients]) @@ -172,12 +200,78 @@ const PurchaseAddForm: React.FC = () => { ) }, [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 handleItemChange = (index: number, field: keyof PurchaseOrderFormItem, value: any): void => { @@ -185,7 +279,6 @@ const PurchaseAddForm: React.FC = () => { const newItems = [...prev.items] newItems[index] = { ...newItems[index], [field]: value } - // Auto-calculate total if amount or quantity changes if (field === 'amount' || field === 'quantity') { const item = newItems[index] item.total = item.amount * item.quantity @@ -193,30 +286,31 @@ const PurchaseAddForm: React.FC = () => { return { ...prev, items: newItems } }) + + if (errors.items) { + setErrors(prev => ({ + ...prev, + items: undefined + })) + } } const handleIngredientSelection = (index: number, selectedIngredient: any) => { handleItemChange(index, 'ingredient', selectedIngredient) - // Auto-populate related fields if available in the ingredient data if (selectedIngredient) { const ingredientData: IngredientItem = selectedIngredient.originalData || selectedIngredient - // Auto-fill unit based on IngredientItem structure if (ingredientData.unit_id || ingredientData.unit) { let unitToFind = null - // If ingredient has unit object (populated relation) if (ingredientData.unit && typeof ingredientData.unit === 'object') { unitToFind = ingredientData.unit - } - // If ingredient has unit_id, find the unit from unitOptions - else if (ingredientData.unit_id) { + } else if (ingredientData.unit_id) { unitToFind = unitOptions.find(option => option.value === ingredientData.unit_id) } if (unitToFind) { - // Create unit option object const unitOption = { label: (unitToFind as any).label || (unitToFind as any).name || (unitToFind as any).unit_name, value: (unitToFind as any).value || ingredientData.unit_id @@ -226,12 +320,10 @@ const PurchaseAddForm: React.FC = () => { } } - // Auto-fill amount with cost from IngredientItem if (ingredientData.cost !== undefined && ingredientData.cost !== null) { handleItemChange(index, 'amount', ingredientData.cost) } - // Auto-fill description with ingredient name if (ingredientData.name) { handleItemChange(index, 'description', ingredientData.name) } @@ -261,7 +353,6 @@ const PurchaseAddForm: React.FC = () => { })) } - // 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 ?? '')) @@ -280,29 +371,26 @@ const PurchaseAddForm: React.FC = () => { const formData = new FormData() formData.append('file', file) formData.append('file_type', 'image') - formData.append('description', 'Purchase image') + formData.append('description', 'Gambar Purchase Order') mutate(formData, { onSuccess: data => { - // pemakaian: setFormData(prev => ({ ...prev, attachment_file_ids: upsertAttachment(prev.attachment_file_ids, data.id) })) setImageUrl(data.file_url) - resolve(data.id) // <-- balikin id file yang berhasil diupload + resolve(data.id) }, onError: error => { - reject(error) // biar async/await bisa tangkep error + reject(error) } }) }) } - // Calculate subtotal from items const subtotal = formData.items.reduce((sum, item) => sum + (item.total || 0), 0) - // Convert form data to API request format const convertToApiRequest = (): PurchaseOrderRequest => { return { vendor_id: formData.vendor?.value || '', @@ -313,7 +401,7 @@ const PurchaseAddForm: React.FC = () => { status: formData.status, message: formData.message || undefined, items: formData.items - .filter(item => item.ingredient && item.unit) // Only include valid items + .filter(item => item.ingredient && item.unit) .map(item => ({ ingredient_id: item.ingredient!.value, description: item.description || undefined, @@ -326,19 +414,46 @@ const PurchaseAddForm: React.FC = () => { } 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 */} - {/* Row 1 - Vendor and PO Number */} { handleInputChange('vendor', newValue) if (newValue?.value) { const selectedVendorData = vendors?.find(vendor => vendor.id === newValue.value) - console.log('Vendor selected:', selectedVendorData) + console.log('Vendor terpilih:', selectedVendorData) } }} loading={isLoadingVendors} renderInput={params => ( )} /> @@ -387,9 +504,11 @@ const PurchaseAddForm: React.FC = () => { ) => handleInputChange('po_number', e.target.value)} + error={!!errors.po_number} + helperText={errors.po_number} /> @@ -397,7 +516,7 @@ const PurchaseAddForm: React.FC = () => { ) => @@ -406,25 +525,29 @@ const PurchaseAddForm: React.FC = () => { 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)} /> @@ -433,18 +556,24 @@ const PurchaseAddForm: React.FC = () => { {/* ITEMS TABLE SECTION */} - Purchase Order Items + Item Purchase Order + {errors.items && ( + + {errors.items} + + )} + - Ingredient - Description - Quantity - Unit - Amount + Bahan + Deskripsi + Kuantitas + Satuan + Harga Total @@ -472,7 +601,7 @@ const PurchaseAddForm: React.FC = () => { renderInput={params => ( { onChange={(e: React.ChangeEvent) => handleItemChange(index, 'description', e.target.value) } - placeholder='Description' + placeholder='Deskripsi' /> @@ -524,7 +653,7 @@ const PurchaseAddForm: React.FC = () => { renderInput={params => ( { 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' }} /> @@ -596,7 +727,7 @@ const PurchaseAddForm: React.FC = () => { sx={{ mt: 1 }} disabled={isLoadingIngredients || isLoadingUnits} > - Add Item + Tambah Item @@ -636,7 +767,7 @@ const PurchaseAddForm: React.FC = () => { )} - Message + Pesan {formData.showPesan && ( @@ -644,7 +775,7 @@ const PurchaseAddForm: React.FC = () => { fullWidth multiline rows={3} - placeholder='Add message...' + placeholder='Tambahkan pesan...' value={formData.message || ''} onChange={(e: React.ChangeEvent) => handleInputChange('message', e.target.value) @@ -685,7 +816,7 @@ const PurchaseAddForm: React.FC = () => { )} - Attachment + Lampiran {formData.showAttachment && ( { maxFileSize={1 * 1024 * 1024} showUrlOption={false} currentImageUrl={imageUrl} - dragDropText='Drop your image here' - browseButtonText='Choose Image' + dragDropText='Letakkan gambar Anda di sini' + browseButtonText='Pilih Gambar' /> )} @@ -759,6 +890,7 @@ const PurchaseAddForm: React.FC = () => { color='primary' fullWidth onClick={handleSave} + disabled={createPurchaseOrder.isPending} sx={{ textTransform: 'none', fontWeight: 600, @@ -770,13 +902,28 @@ const PurchaseAddForm: React.FC = () => { } }} > - Save + {createPurchaseOrder.isPending ? ( + <> + + Menyimpan... + + ) : ( + 'Simpan' + )} + + {/* Price Popover */} + ) From e1084277e05d62ecb282d8e1e49408479fd0b0d7 Mon Sep 17 00:00:00 2001 From: efrilm Date: Sat, 13 Sep 2025 03:05:45 +0700 Subject: [PATCH 20/23] status color --- .../purchase-orders/list/PurchaseOrderListTable.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx b/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx index 4591bf9..046402f 100644 --- a/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx +++ b/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx @@ -107,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' } From 2d3274d3bf9d2310f120635e96c25911dbb38fe7 Mon Sep 17 00:00:00 2001 From: efrilm Date: Sat, 13 Sep 2025 03:50:57 +0700 Subject: [PATCH 21/23] Purchase Detail --- .../purchase-orders/[id]/detail/page.tsx | 18 ++ src/services/queries/purchaseOrder.ts | 12 +- .../purchase-detail/PurchaseDetailContent.tsx | 43 ++-- .../PurchaseDetailInformation.tsx | 236 +++++------------- .../list/PurchaseOrderListTable.tsx | 3 +- 5 files changed, 128 insertions(+), 184 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-orders/[id]/detail/page.tsx 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/services/queries/purchaseOrder.ts b/src/services/queries/purchaseOrder.ts index 916f8b5..b20262e 100644 --- a/src/services/queries/purchaseOrder.ts +++ b/src/services/queries/purchaseOrder.ts @@ -1,4 +1,4 @@ -import { PurchaseOrders } from '@/types/services/purchaseOrder' +import { PurchaseOrder, PurchaseOrders } from '@/types/services/purchaseOrder' import { useQuery } from '@tanstack/react-query' import { api } from '../api' @@ -39,3 +39,13 @@ export function usePurchaseOrders(params: PurchaseOrderQueryParams = {}) { } }) } + +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/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 ?? '')}
@@ -222,82 +185,19 @@ const PurchaseDetailInformation: React.FC = () => { justifyContent: 'space-between', alignItems: 'center', py: 2, - borderBottom: '1px solid #e0e0e0', '&:hover': { backgroundColor: 'rgba(0, 0, 0, 0.04)', transition: 'background-color 0.15s ease' } }} > - - Sub Total - - - {formatCurrency(subTotal)} - - - - - - PPN - - - {formatCurrency(ppn)} - - - - - + Total - + {formatCurrency(total)} - - - - Sisa Tagihan - - - {formatCurrency(sisaTagihan)} - -
diff --git a/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx b/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx index 046402f..6d242c4 100644 --- a/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx +++ b/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx @@ -212,7 +212,8 @@ const PurchaseOrderListTable = () => { variant='text' color='primary' className='p-0 min-w-0 font-medium normal-case justify-start' - onClick={() => handlePOClick(row.original.id.toString())} + component={Link} + href={getLocalizedUrl(`/apps/purchase/purchase-orders/${row.original.id}/detail`, locale as Locale)} sx={{ textTransform: 'none', fontWeight: 500, From aa2946e627d39cec9cab75934adbd1d459d14fa9 Mon Sep 17 00:00:00 2001 From: efrilm Date: Sat, 13 Sep 2025 03:52:11 +0700 Subject: [PATCH 22/23] menu --- src/components/layout/vertical/VerticalMenu.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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} - + */} Date: Sat, 13 Sep 2025 04:08:49 +0700 Subject: [PATCH 23/23] Add Waste at Product Recipe --- src/types/services/productRecipe.ts | 2 + .../products/detail/AddRecipeDrawer.tsx | 86 ++++++++++++++----- .../products/detail/ProductDetail.tsx | 9 +- 3 files changed, 70 insertions(+), 27 deletions(-) diff --git a/src/types/services/productRecipe.ts b/src/types/services/productRecipe.ts index d13ce3f..8185cbe 100644 --- a/src/types/services/productRecipe.ts +++ b/src/types/services/productRecipe.ts @@ -42,6 +42,7 @@ export interface ProductRecipe { variant_id: string | null ingredient_id: string quantity: number + waste: number created_at: string updated_at: string product: Product @@ -54,6 +55,7 @@ export interface ProductRecipeRequest { ingredient_id: string quantity: number outlet_id: string | null + waste: number } export interface IngredientUnit { diff --git a/src/views/apps/ecommerce/products/detail/AddRecipeDrawer.tsx b/src/views/apps/ecommerce/products/detail/AddRecipeDrawer.tsx index c4debed..fbab2a7 100644 --- a/src/views/apps/ecommerce/products/detail/AddRecipeDrawer.tsx +++ b/src/views/apps/ecommerce/products/detail/AddRecipeDrawer.tsx @@ -11,8 +11,6 @@ import Typography from '@mui/material/Typography' // Third-party Imports import PerfectScrollbar from 'react-perfect-scrollbar' -// Type Imports - // Component Imports import CustomTextField from '@core/components/mui/TextField' import { Autocomplete } from '@mui/material' @@ -25,6 +23,7 @@ import { useOutlets } from '../../../../../services/queries/outlets' import { Product } from '../../../../../types/services/product' import { ProductRecipeRequest } from '../../../../../types/services/productRecipe' import { resetProductVariant } from '../../../../../redux-store/slices/productRecipe' +import { IngredientItem } from '@/types/services/ingredient' type Props = { open: boolean @@ -38,7 +37,8 @@ const initialData = { product_id: '', variant_id: '', ingredient_id: '', - quantity: 0 + quantity: 0, + waste: 0 } const AddRecipeDrawer = (props: Props) => { @@ -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) })} + /> +