mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 07:45:47 +00:00
refactor(FE-361,363): Refactor purchases-per-supplier report to use API
This commit is contained in:
@@ -1,23 +1,69 @@
|
|||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo, useCallback } from 'react';
|
||||||
|
import { ChangeEventHandler } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
import SelectInput, {
|
||||||
|
useSelect,
|
||||||
|
OptionType,
|
||||||
|
} from '@/components/input/SelectInput';
|
||||||
import DateInput from '@/components/input/DateInput';
|
import DateInput from '@/components/input/DateInput';
|
||||||
import { AreaApi } from '@/services/api/master-data';
|
import { AreaApi } from '@/services/api/master-data';
|
||||||
import { SupplierApi } from '@/services/api/master-data';
|
import { SupplierApi } from '@/services/api/master-data';
|
||||||
import { ProductApi } from '@/services/api/master-data';
|
import { ProductApi } from '@/services/api/master-data';
|
||||||
|
import { LogisticService } from '@/services/api/logistic';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import { formatCurrency, formatDate } from '@/lib/helper';
|
import { formatCurrency, formatDate } from '@/lib/helper';
|
||||||
|
import {
|
||||||
|
SupplierWithItems,
|
||||||
|
LogisticPurchasePerSupplierItems,
|
||||||
|
} from '@/types/api/report/logistic-stock';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
|
||||||
|
interface Totals {
|
||||||
|
totalQty: number;
|
||||||
|
totalPrice: number;
|
||||||
|
totalPurchaseAmount: number;
|
||||||
|
totalTransport: number;
|
||||||
|
totalValueTransport: number;
|
||||||
|
totalJumlah: number;
|
||||||
|
}
|
||||||
|
|
||||||
const PurchasesPerSupplierTab = () => {
|
const PurchasesPerSupplierTab = () => {
|
||||||
const [selectedArea, setSelectedArea] = useState<number | null>(null);
|
|
||||||
const [selectedSupplier, setSelectedSupplier] = useState<number | null>(null);
|
|
||||||
const [selectedProduct, setSelectedProduct] = useState<number | null>(null);
|
|
||||||
const [dataType, setDataType] = useState<'received_date' | 'po_date'>(
|
const [dataType, setDataType] = useState<'received_date' | 'po_date'>(
|
||||||
'received_date'
|
'received_date'
|
||||||
);
|
);
|
||||||
const [startDate, setStartDate] = useState<string | null>(null);
|
|
||||||
const [endDate, setEndDate] = useState<string | null>(null);
|
// ===== TABLE FILTER STATE =====
|
||||||
|
const {
|
||||||
|
state: tableFilterState,
|
||||||
|
updateFilter,
|
||||||
|
toQueryString: getTableFilterQueryString,
|
||||||
|
} = useTableFilter({
|
||||||
|
initial: {
|
||||||
|
area_id: '',
|
||||||
|
supplier_id: '',
|
||||||
|
product_id: '',
|
||||||
|
received_date: '',
|
||||||
|
po_date: '',
|
||||||
|
start_date: '',
|
||||||
|
end_date: '',
|
||||||
|
},
|
||||||
|
paramMap: {
|
||||||
|
page: 'page',
|
||||||
|
pageSize: 'limit',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateDataType = useCallback(
|
||||||
|
(newDataType: 'received_date' | 'po_date') => {
|
||||||
|
setDataType(newDataType);
|
||||||
|
updateFilter('received_date', '');
|
||||||
|
updateFilter('po_date', '');
|
||||||
|
},
|
||||||
|
[updateFilter]
|
||||||
|
);
|
||||||
|
|
||||||
const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect(
|
const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect(
|
||||||
AreaApi.basePath,
|
AreaApi.basePath,
|
||||||
@@ -40,147 +86,77 @@ const PurchasesPerSupplierTab = () => {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = useMemo(
|
const areaChangeHandler = useCallback(
|
||||||
() => [
|
(val: OptionType | OptionType[] | null) => {
|
||||||
{
|
const newVal = val as OptionType;
|
||||||
id: 1,
|
updateFilter('area_id', newVal?.value ? String(newVal.value) : '');
|
||||||
supplier: 'PT. RAJAWALI MITRA PAKANINDO',
|
},
|
||||||
items: [
|
[updateFilter]
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
received_date: '2025-09-26',
|
|
||||||
po_date: '2025-09-30',
|
|
||||||
po_number: 'PO-MBU-00670',
|
|
||||||
product_name: 'KAPORIT BESAR (TCCA) 200 GR @1 KG',
|
|
||||||
destination_warehouse: 'GUDANG CIMARAGAS 1',
|
|
||||||
qty: 5,
|
|
||||||
price: 45000,
|
|
||||||
purchase_amount: 225000,
|
|
||||||
transport: 0,
|
|
||||||
value_transport: 0,
|
|
||||||
total: 225000,
|
|
||||||
expedition_vendor_name: 'PT. RAJAWALI MITRA PAKANINDO',
|
|
||||||
travel_number: '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
received_date: '2025-09-30',
|
|
||||||
po_date: '2025-09-30',
|
|
||||||
po_number: 'PO-MBU-00670',
|
|
||||||
product_name: 'KAPORIT BESAR (TCCA) 200 GR @1 KG',
|
|
||||||
destination_warehouse: 'GUDANG CIMARAGAS 2',
|
|
||||||
qty: 5,
|
|
||||||
price: 45000,
|
|
||||||
purchase_amount: 225000,
|
|
||||||
transport: 0,
|
|
||||||
value_transport: 0,
|
|
||||||
total: 225000,
|
|
||||||
expedition_vendor_name: 'PT. RAJAWALI MITRA PAKANINDO',
|
|
||||||
travel_number: '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
received_date: '2025-09-30',
|
|
||||||
po_date: '2025-09-30',
|
|
||||||
po_number: 'PO-MBU-00670',
|
|
||||||
product_name: 'KAPORIT BESAR (TCCA) 200 GR @1 KG',
|
|
||||||
destination_warehouse: 'GUDANG CIMARAGAS 3',
|
|
||||||
qty: 5,
|
|
||||||
price: 45000,
|
|
||||||
purchase_amount: 225000,
|
|
||||||
transport: 0,
|
|
||||||
value_transport: 0,
|
|
||||||
total: 225000,
|
|
||||||
expedition_vendor_name: 'PT. RAJAWALI MITRA PAKANINDO',
|
|
||||||
travel_number: '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
received_date: '2025-09-30',
|
|
||||||
po_date: '2025-09-30',
|
|
||||||
po_number: 'PO-MBU-00670',
|
|
||||||
product_name: 'KAPORIT BESAR (TCCA) 200 GR @1 KG',
|
|
||||||
destination_warehouse: 'GUDANG CIMARAGAS 4',
|
|
||||||
qty: 5,
|
|
||||||
price: 45000,
|
|
||||||
purchase_amount: 225000,
|
|
||||||
transport: 0,
|
|
||||||
value_transport: 0,
|
|
||||||
total: 225000,
|
|
||||||
expedition_vendor_name: 'PT. RAJAWALI MITRA PAKANINDO',
|
|
||||||
travel_number: '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
received_date: '2025-09-04',
|
|
||||||
po_date: '2025-09-30',
|
|
||||||
po_number: 'PO-MBU-00606',
|
|
||||||
product_name: 'DESINFEKTAN C 100 @20L',
|
|
||||||
destination_warehouse: 'GUDANG MANDALAWANGI 1',
|
|
||||||
qty: 1,
|
|
||||||
price: 800000,
|
|
||||||
purchase_amount: 800000,
|
|
||||||
transport: 0,
|
|
||||||
value_transport: 0,
|
|
||||||
total: 800000,
|
|
||||||
expedition_vendor_name: 'PT. RAJAWALI MITRA PAKANINDO',
|
|
||||||
travel_number: '-',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
supplier: 'Supplier B',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
received_date: '2024-01-18',
|
|
||||||
po_date: '2024-01-13',
|
|
||||||
po_number: 'PO-2024-004',
|
|
||||||
product_name: 'Produk D',
|
|
||||||
destination_warehouse: 'Gudang Pusat',
|
|
||||||
qty: 200,
|
|
||||||
price: 25000,
|
|
||||||
purchase_amount: 5000000,
|
|
||||||
transport: 200000,
|
|
||||||
value_transport: 200000,
|
|
||||||
total: 5200000,
|
|
||||||
expedition_vendor_name: 'Ekspedisi GHI',
|
|
||||||
travel_number: 'SJ-004',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
supplier: 'Supplier C',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
received_date: '2024-01-20',
|
|
||||||
po_date: '2024-01-15',
|
|
||||||
po_number: 'PO-2024-006',
|
|
||||||
product_name: 'Produk F',
|
|
||||||
destination_warehouse: 'Gudang Cabang',
|
|
||||||
qty: 80,
|
|
||||||
price: 55000,
|
|
||||||
purchase_amount: 4400000,
|
|
||||||
transport: 80000,
|
|
||||||
value_transport: 80000,
|
|
||||||
total: 4480000,
|
|
||||||
expedition_vendor_name: 'Ekspedisi MNO',
|
|
||||||
travel_number: 'SJ-006',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO START
|
const supplierChangeHandler = useCallback(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
(val: OptionType | OptionType[] | null) => {
|
||||||
const getTableColumns = (totals: any) => {
|
const newVal = val as OptionType;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
updateFilter('supplier_id', newVal?.value ? String(newVal.value) : '');
|
||||||
const tableColumns: ColumnDef<any>[] = [
|
},
|
||||||
|
[updateFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const productChangeHandler = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const newVal = val as OptionType;
|
||||||
|
updateFilter('product_id', newVal?.value ? String(newVal.value) : '');
|
||||||
|
},
|
||||||
|
[updateFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const dataTypeChangeHandler = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const newVal = val as OptionType;
|
||||||
|
updateDataType(
|
||||||
|
(newVal?.value as 'received_date' | 'po_date') || 'received_date'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[updateDataType]
|
||||||
|
);
|
||||||
|
|
||||||
|
const startDateChangeHandler = useCallback<
|
||||||
|
ChangeEventHandler<HTMLInputElement>
|
||||||
|
>(
|
||||||
|
(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
updateFilter('start_date', val || '');
|
||||||
|
if (val && dataType) {
|
||||||
|
updateFilter(dataType, val);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[updateFilter, dataType]
|
||||||
|
);
|
||||||
|
|
||||||
|
const endDateChangeHandler = useCallback<
|
||||||
|
ChangeEventHandler<HTMLInputElement>
|
||||||
|
>(
|
||||||
|
(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
updateFilter('end_date', val || '');
|
||||||
|
},
|
||||||
|
[updateFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== DATA FETCHING =====
|
||||||
|
const { data: response, isLoading } = useSWR(
|
||||||
|
`${LogisticService.basePath}/purchase-supplier${getTableFilterQueryString()}`,
|
||||||
|
LogisticService.getAllFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const data: SupplierWithItems[] = isResponseSuccess(response)
|
||||||
|
? (response?.data as unknown as SupplierWithItems[])
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const getTableColumns = (
|
||||||
|
totals: Totals
|
||||||
|
): ColumnDef<LogisticPurchasePerSupplierItems>[] => {
|
||||||
|
const tableColumns: ColumnDef<LogisticPurchasePerSupplierItems>[] = [
|
||||||
{
|
{
|
||||||
id: 'no',
|
id: 'no',
|
||||||
header: 'No',
|
header: 'No',
|
||||||
@@ -191,38 +167,53 @@ const PurchasesPerSupplierTab = () => {
|
|||||||
id: 'received_date',
|
id: 'received_date',
|
||||||
header: 'Tanggal Terima',
|
header: 'Tanggal Terima',
|
||||||
accessorKey: 'received_date',
|
accessorKey: 'received_date',
|
||||||
cell: (props) => formatDate(props.getValue() as string, 'DD MMM YYYY'),
|
cell: (props) => {
|
||||||
|
const value = props.row.original.received_date;
|
||||||
|
return formatDate(value, 'DD MMM YYYY');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'po_date',
|
id: 'po_date',
|
||||||
header: 'Tanggal PO',
|
header: 'Tanggal PO',
|
||||||
accessorKey: 'po_date',
|
accessorKey: 'po_date',
|
||||||
cell: (props) => formatDate(props.getValue() as string, 'DD MMM YYYY'),
|
cell: (props) => {
|
||||||
|
const value = props.row.original.po_date;
|
||||||
|
return formatDate(value, 'DD MMM YYYY');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'po_number',
|
id: 'po_number',
|
||||||
header: 'No. Referensi',
|
header: 'No. Referensi',
|
||||||
accessorKey: 'po_number',
|
accessorKey: 'po_number',
|
||||||
cell: (props) => props.getValue() || '-',
|
cell: (props) => {
|
||||||
|
const value = props.row.original.po_number;
|
||||||
|
return value || '-';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'product_name',
|
id: 'product_name',
|
||||||
header: 'Nama Produk',
|
header: 'Nama Produk',
|
||||||
accessorKey: 'product_name',
|
accessorKey: 'product.name',
|
||||||
cell: (props) => props.getValue() || '-',
|
cell: (props) => {
|
||||||
|
const product = props.row.original.product;
|
||||||
|
return product?.name || '-';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'destination_warehouse',
|
id: 'destination_warehouse',
|
||||||
header: 'Tujuan',
|
header: 'Tujuan',
|
||||||
accessorKey: 'destination_warehouse',
|
accessorKey: 'destination_warehouse',
|
||||||
cell: (props) => props.getValue() || '-',
|
cell: (props) => {
|
||||||
|
const value = props.row.original.destination_warehouse;
|
||||||
|
return value || '-';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'qty',
|
id: 'qty',
|
||||||
header: 'QTY',
|
header: 'QTY',
|
||||||
accessorKey: 'qty',
|
accessorKey: 'qty',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.getValue() as number;
|
const value = props.row.original.qty;
|
||||||
return <div className='text-right'>{value.toLocaleString()}</div>;
|
return <div className='text-right'>{value.toLocaleString()}</div>;
|
||||||
},
|
},
|
||||||
footer: () => (
|
footer: () => (
|
||||||
@@ -236,7 +227,7 @@ const PurchasesPerSupplierTab = () => {
|
|||||||
header: 'Harga Beli (Rp)',
|
header: 'Harga Beli (Rp)',
|
||||||
accessorKey: 'price',
|
accessorKey: 'price',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.getValue() as number;
|
const value = props.row.original.price;
|
||||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
},
|
},
|
||||||
footer: () => (
|
footer: () => (
|
||||||
@@ -248,9 +239,9 @@ const PurchasesPerSupplierTab = () => {
|
|||||||
{
|
{
|
||||||
id: 'purchase_amount',
|
id: 'purchase_amount',
|
||||||
header: 'Value Harga Beli (Rp)',
|
header: 'Value Harga Beli (Rp)',
|
||||||
accessorKey: 'purchase_amount',
|
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.getValue() as number;
|
const item = props.row.original;
|
||||||
|
const value = (item.price || 0) * (item.qty || 0);
|
||||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
},
|
},
|
||||||
footer: () => (
|
footer: () => (
|
||||||
@@ -262,9 +253,9 @@ const PurchasesPerSupplierTab = () => {
|
|||||||
{
|
{
|
||||||
id: 'transport',
|
id: 'transport',
|
||||||
header: 'Transport (Rp)',
|
header: 'Transport (Rp)',
|
||||||
accessorKey: 'transport',
|
accessorKey: 'transport_per_item',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.getValue() as number;
|
const value = props.row.original.transport_per_item;
|
||||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
},
|
},
|
||||||
footer: () => (
|
footer: () => (
|
||||||
@@ -276,9 +267,9 @@ const PurchasesPerSupplierTab = () => {
|
|||||||
{
|
{
|
||||||
id: 'value_transport',
|
id: 'value_transport',
|
||||||
header: 'Value Transport (Rp)',
|
header: 'Value Transport (Rp)',
|
||||||
accessorKey: 'value_transport',
|
accessorKey: 'transport_total',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.getValue() as number;
|
const value = props.row.original.transport_total;
|
||||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
},
|
},
|
||||||
footer: () => (
|
footer: () => (
|
||||||
@@ -290,9 +281,10 @@ const PurchasesPerSupplierTab = () => {
|
|||||||
{
|
{
|
||||||
id: 'total',
|
id: 'total',
|
||||||
header: 'Jumlah (Rp)',
|
header: 'Jumlah (Rp)',
|
||||||
accessorKey: 'total',
|
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.getValue() as number;
|
const item = props.row.original;
|
||||||
|
const value =
|
||||||
|
(item.price || 0) * (item.qty || 0) + (item.transport_total || 0);
|
||||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
},
|
},
|
||||||
footer: () => (
|
footer: () => (
|
||||||
@@ -305,66 +297,88 @@ const PurchasesPerSupplierTab = () => {
|
|||||||
id: 'expedition_vendor_name',
|
id: 'expedition_vendor_name',
|
||||||
header: 'Ekspedisi',
|
header: 'Ekspedisi',
|
||||||
accessorKey: 'expedition_vendor_name',
|
accessorKey: 'expedition_vendor_name',
|
||||||
cell: (props) => props.getValue() || '-',
|
cell: (props) => {
|
||||||
|
const value = props.row.original.expedition_vendor_name;
|
||||||
|
return value || '-';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'travel_number',
|
id: 'travel_number',
|
||||||
header: 'Surat Jalan',
|
header: 'Surat Jalan',
|
||||||
accessorKey: 'travel_number',
|
accessorKey: 'travel_number',
|
||||||
cell: (props) => props.getValue() || '-',
|
cell: (props) => {
|
||||||
|
const value = props.row.original.travel_number;
|
||||||
|
return value || '-';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
return tableColumns;
|
return tableColumns;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className='w-full p-0 sm:p-4'>
|
||||||
<Card
|
<Card
|
||||||
subtitle='Laporan > Rekapitulasi Pembelian Per Supplier'
|
subtitle='Laporan > Rekapitulasi Pembelian Per Supplier'
|
||||||
className={{ wrapper: 'w-full', body: 'p-1!' }}
|
className={{ wrapper: 'w-full', body: 'p-1!' }}
|
||||||
>
|
>
|
||||||
<div className='grid grid-cols-1 md:grid-cols-3 gap-4'>
|
<div className='grid grid-cols-12 gap-4'>
|
||||||
{/* TODO START */}
|
{/* TODO START */}
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Area'
|
label='Area'
|
||||||
placeholder='Pilih Area'
|
placeholder='Pilih Area'
|
||||||
options={areaOptions}
|
options={areaOptions}
|
||||||
value={
|
value={
|
||||||
areaOptions.find((option) => option.value === selectedArea) ||
|
tableFilterState.area_id
|
||||||
null
|
? areaOptions.find(
|
||||||
|
(option) =>
|
||||||
|
option.value === Number(tableFilterState.area_id)
|
||||||
|
) || null
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
// @ts-expect-error TS2345
|
onChange={areaChangeHandler}
|
||||||
onChange={(val) => setSelectedArea(val?.value || null)}
|
|
||||||
isLoading={isLoadingAreas}
|
isLoading={isLoadingAreas}
|
||||||
isClearable
|
isClearable
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 sm:col-span-4',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Supplier'
|
label='Supplier'
|
||||||
placeholder='Pilih Supplier'
|
placeholder='Pilih Supplier'
|
||||||
options={supplierOptions}
|
options={supplierOptions}
|
||||||
value={
|
value={
|
||||||
supplierOptions.find(
|
tableFilterState.supplier_id
|
||||||
(option) => option.value === selectedSupplier
|
? supplierOptions.find(
|
||||||
) || null
|
(option) =>
|
||||||
|
option.value === Number(tableFilterState.supplier_id)
|
||||||
|
) || null
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
// @ts-expect-error TS2345
|
onChange={supplierChangeHandler}
|
||||||
onChange={(val) => setSelectedSupplier(val?.value || null)}
|
|
||||||
isLoading={isLoadingSuppliers}
|
isLoading={isLoadingSuppliers}
|
||||||
isClearable
|
isClearable
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 sm:col-span-4',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Produk'
|
label='Produk'
|
||||||
placeholder='Pilih Produk'
|
placeholder='Pilih Produk'
|
||||||
options={productOptions}
|
options={productOptions}
|
||||||
value={
|
value={
|
||||||
productOptions.find(
|
tableFilterState.product_id
|
||||||
(option) => option.value === selectedProduct
|
? productOptions.find(
|
||||||
) || null
|
(option) =>
|
||||||
|
option.value === Number(tableFilterState.product_id)
|
||||||
|
) || null
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
// @ts-expect-error TS2345
|
onChange={productChangeHandler}
|
||||||
onChange={(val) => setSelectedProduct(val?.value || null)}
|
|
||||||
isLoading={isLoadingProducts}
|
isLoading={isLoadingProducts}
|
||||||
isClearable
|
isClearable
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 sm:col-span-4',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Tipe Data'
|
label='Tipe Data'
|
||||||
@@ -374,31 +388,39 @@ const PurchasesPerSupplierTab = () => {
|
|||||||
dataTypeOptions?.find((option) => option.value === dataType) ||
|
dataTypeOptions?.find((option) => option.value === dataType) ||
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
// @ts-expect-error TS2345
|
onChange={dataTypeChangeHandler}
|
||||||
onChange={(val) => setDataType(val?.value || 'received_date')}
|
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
isClearable={false}
|
isClearable={false}
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 sm:col-span-4',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<DateInput
|
<DateInput
|
||||||
label='Tanggal Awal'
|
label='Tanggal Awal'
|
||||||
|
name='start_date'
|
||||||
placeholder='Pilih Tanggal Awal'
|
placeholder='Pilih Tanggal Awal'
|
||||||
// @ts-expect-error TS2345
|
value={tableFilterState.start_date}
|
||||||
value={startDate}
|
onChange={startDateChangeHandler}
|
||||||
// @ts-expect-error TS2345
|
className={{
|
||||||
onChange={setStartDate}
|
wrapper: 'col-span-12 sm:col-span-4',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<DateInput
|
<DateInput
|
||||||
label='Tanggal Akhir'
|
label='Tanggal Akhir'
|
||||||
|
name='end_date'
|
||||||
placeholder='Pilih Tanggal Akhir'
|
placeholder='Pilih Tanggal Akhir'
|
||||||
// @ts-expect-error TS2345
|
value={tableFilterState.end_date}
|
||||||
value={endDate}
|
onChange={endDateChangeHandler}
|
||||||
// @ts-expect-error TS2345
|
className={{
|
||||||
onChange={setEndDate}
|
wrapper: 'col-span-12 sm:col-span-4',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{/* TODO END */}
|
{/* TODO END */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data.length === 0 ? (
|
{isLoading ? (
|
||||||
|
<div className='mt-6 text-center text-gray-500'>Memuat data...</div>
|
||||||
|
) : data.length === 0 ? (
|
||||||
<div className='mt-6 text-center text-gray-500'>
|
<div className='mt-6 text-center text-gray-500'>
|
||||||
Tidak ada data untuk ditampilkan.
|
Tidak ada data untuk ditampilkan.
|
||||||
</div>
|
</div>
|
||||||
@@ -409,30 +431,30 @@ const PurchasesPerSupplierTab = () => {
|
|||||||
0
|
0
|
||||||
);
|
);
|
||||||
const totalPrice = supplier.items.reduce(
|
const totalPrice = supplier.items.reduce(
|
||||||
(sum, item) => sum + (item.price || 0),
|
(sum, item) => sum + (item.price || 0) * (item.qty || 0),
|
||||||
0
|
|
||||||
);
|
|
||||||
const totalPurchaseAmount = supplier.items.reduce(
|
|
||||||
(sum, item) => sum + (item.purchase_amount || 0),
|
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
const totalTransport = supplier.items.reduce(
|
const totalTransport = supplier.items.reduce(
|
||||||
(sum, item) => sum + (item.transport || 0),
|
(sum, item) =>
|
||||||
|
sum + (item.transport_per_item || 0) * (item.qty || 0),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
const totalValueTransport = supplier.items.reduce(
|
const totalValueTransport = supplier.items.reduce(
|
||||||
(sum, item) => sum + (item.value_transport || 0),
|
(sum, item) => sum + (item.transport_total || 0),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
const totalJumlah = supplier.items.reduce(
|
const totalJumlah = supplier.items.reduce(
|
||||||
(sum, item) => sum + (item.total || 0),
|
(sum, item) =>
|
||||||
|
sum +
|
||||||
|
(item.price || 0) * (item.qty || 0) +
|
||||||
|
(item.transport_total || 0),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
const totals = {
|
const totals = {
|
||||||
totalQty,
|
totalQty,
|
||||||
totalPrice,
|
totalPrice,
|
||||||
totalPurchaseAmount,
|
totalPurchaseAmount: totalPrice,
|
||||||
totalTransport,
|
totalTransport,
|
||||||
totalValueTransport,
|
totalValueTransport,
|
||||||
totalJumlah,
|
totalJumlah,
|
||||||
@@ -444,7 +466,7 @@ const PurchasesPerSupplierTab = () => {
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={supplier.id}
|
key={supplier.id}
|
||||||
title={supplier.supplier}
|
title={supplier.supplier.name}
|
||||||
subtitle={`Total Pembelian: ${formatCurrency(totalPurchase)}`}
|
subtitle={`Total Pembelian: ${formatCurrency(totalPurchase)}`}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
collapsible={true}
|
collapsible={true}
|
||||||
@@ -455,7 +477,7 @@ const PurchasesPerSupplierTab = () => {
|
|||||||
pageSize={10}
|
pageSize={10}
|
||||||
renderFooter={supplier.items.length > 0}
|
renderFooter={supplier.items.length > 0}
|
||||||
className={{
|
className={{
|
||||||
containerClassName: 'mb-0!',
|
containerClassName: 'w-full',
|
||||||
tableWrapperClassName: 'overflow-x-auto mt-4',
|
tableWrapperClassName: 'overflow-x-auto mt-4',
|
||||||
tableClassName: 'w-full table-auto text-sm',
|
tableClassName: 'w-full table-auto text-sm',
|
||||||
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
||||||
@@ -470,7 +492,6 @@ const PurchasesPerSupplierTab = () => {
|
|||||||
footerRowClassName: 'border-t-2 border-gray-300',
|
footerRowClassName: 'border-t-2 border-gray-300',
|
||||||
footerColumnClassName:
|
footerColumnClassName:
|
||||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||||
paginationClassName: 'hidden',
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -478,7 +499,7 @@ const PurchasesPerSupplierTab = () => {
|
|||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ import { BaseApiService } from '@/services/api/base';
|
|||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock';
|
import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock';
|
||||||
|
|
||||||
|
const baseLogisticApi = new BaseApiService<
|
||||||
|
LogisticPurchasePerSupplierReport,
|
||||||
|
unknown,
|
||||||
|
unknown
|
||||||
|
>('/reports');
|
||||||
|
|
||||||
export class LogisticApi extends BaseApiService<
|
export class LogisticApi extends BaseApiService<
|
||||||
LogisticPurchasePerSupplierReport,
|
LogisticPurchasePerSupplierReport,
|
||||||
unknown,
|
unknown,
|
||||||
@@ -37,4 +43,33 @@ export class LogisticApi extends BaseApiService<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LogisticService = new LogisticApi('/reports');
|
export const LogisticService = {
|
||||||
|
basePath: baseLogisticApi.basePath,
|
||||||
|
header: baseLogisticApi.header,
|
||||||
|
getAllFetcher: baseLogisticApi.getAllFetcher.bind(baseLogisticApi),
|
||||||
|
getSingle: baseLogisticApi.getSingle.bind(baseLogisticApi),
|
||||||
|
create: baseLogisticApi.create.bind(baseLogisticApi),
|
||||||
|
update: baseLogisticApi.update.bind(baseLogisticApi),
|
||||||
|
delete: baseLogisticApi.delete.bind(baseLogisticApi),
|
||||||
|
customRequest: baseLogisticApi.customRequest.bind(baseLogisticApi),
|
||||||
|
|
||||||
|
// Custom method for specific endpoint
|
||||||
|
getLogisticStockReport: (
|
||||||
|
area_id?: number,
|
||||||
|
supplier_id?: number,
|
||||||
|
product_id?: number,
|
||||||
|
received_date?: string,
|
||||||
|
po_date?: string,
|
||||||
|
start_date?: string,
|
||||||
|
end_date?: string
|
||||||
|
) =>
|
||||||
|
new LogisticApi().getLogisticStockReport(
|
||||||
|
area_id,
|
||||||
|
supplier_id,
|
||||||
|
product_id,
|
||||||
|
received_date,
|
||||||
|
po_date,
|
||||||
|
start_date,
|
||||||
|
end_date
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|||||||
+6
-5
@@ -1,7 +1,7 @@
|
|||||||
import { BaseMetadata } from '@/types/api/api-general';
|
import { BaseMetadata } from '@/types/api/api-general';
|
||||||
import { Supplier } from '@/types/api/supplier/supplier';
|
import { Supplier } from '@/types/api/supplier/supplier';
|
||||||
import { Product } from '@/types/api/product/product';
|
import { Product } from '@/types/api/product/product';
|
||||||
import { Area } from '@types/api/area/area';
|
import { Area } from '@/types/api/area/area';
|
||||||
|
|
||||||
export type LogisticPurchasePerSupplierItems = {
|
export type LogisticPurchasePerSupplierItems = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -20,11 +20,12 @@ export type LogisticPurchasePerSupplierItems = {
|
|||||||
travel_number: string;
|
travel_number: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BaseLogisticPurchasePerSupplierReport = {
|
export type SupplierWithItems = {
|
||||||
id: number;
|
id: number;
|
||||||
supplier: Supplier;
|
supplier: Supplier;
|
||||||
items: LogisticStockItems[];
|
items: LogisticPurchasePerSupplierItems[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LogisticPurchasePerSupplierReport = BaseMetadata &
|
export type LogisticPurchasePerSupplierReport = BaseMetadata & {
|
||||||
BaseLogisticStockReport;
|
data: SupplierWithItems[];
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user