Merge branch 'dev/randy' into 'development'

[FIX/FE][US#284] Fix sapronak calculation data types

See merge request mbugroup/lti-web-client!132
This commit is contained in:
Rivaldi A N S
2026-01-02 06:55:56 +00:00
4 changed files with 228 additions and 200 deletions
@@ -37,88 +37,88 @@ const ClosingSapronakCalculationTable = ({
): ColumnDef<RowSapronakCalculation>[] => [
{
header: 'Tanggal',
accessorKey: 'tanggal',
accessorKey: 'date',
cell: (props) =>
props.row.original.tanggal
? formatDate(props.row.original.tanggal, 'DD MMM YYYY')
props.row.original.date
? formatDate(props.row.original.date, 'DD MMM YYYY')
: '-',
footer: 'Total',
},
{
header: 'No. Referensi',
accessorKey: 'no_referensi',
cell: (props) => (props.row.original.no_referensi as string) || '-',
accessorKey: 'reference_number',
cell: (props) => (props.row.original.reference_number as string) || '-',
footer: '',
},
{
header: 'QTY Masuk',
accessorKey: 'qty_masuk',
accessorKey: 'qty_in',
cell: (props) =>
props.row.original.qty_masuk
? formatNumber(props.row.original.qty_masuk as number)
props.row.original.qty_in
? formatNumber(props.row.original.qty_in as number)
: '-',
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{total?.qty_masuk ? formatNumber(total?.qty_masuk) : '-'}
{total?.qty_in ? formatNumber(total?.qty_in) : '-'}
</div>
)
: '',
},
{
header: 'QTY Keluar',
accessorKey: 'qty_keluar',
accessorKey: 'qty_out',
cell: (props) =>
props.row.original.qty_keluar
? formatNumber(props.row.original.qty_keluar as number)
props.row.original.qty_out
? formatNumber(props.row.original.qty_out as number)
: '-',
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{total?.qty_keluar ? formatNumber(total?.qty_keluar) : '-'}
{total?.qty_out ? formatNumber(total?.qty_out) : '-'}
</div>
)
: '',
},
{
header: 'QTY Pakai',
accessorKey: 'qty_pakai',
accessorKey: 'qty_used',
cell: (props) =>
props.row.original.qty_pakai
? formatNumber(props.row.original.qty_pakai as number)
props.row.original.qty_used
? formatNumber(props.row.original.qty_used as number)
: '-',
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{total?.qty_pakai ? formatNumber(total?.qty_pakai) : '-'}
{total?.qty_used ? formatNumber(total?.qty_used) : '-'}
</div>
)
: '',
},
{
header: 'Uraian',
accessorKey: 'uraian',
cell: (props) => (props.row.original.uraian as string) || '-',
accessorKey: 'description',
cell: (props) => (props.row.original.description as string) || '-',
footer: '',
},
{
header: 'Kategori Produk',
accessorKey: 'kategori_produk',
cell: (props) => (props.row.original.kategori_produk as string) || '-',
accessorKey: 'product_category',
cell: (props) => (props.row.original.product_category as string) || '-',
footer: '',
},
{
header: 'Harga Beli/Qty (Rp)',
accessorKey: 'harga_beli_per_qty',
accessorKey: 'unit_price',
cell: (props) =>
props.row.original.harga_beli_per_qty
? formatCurrency(props.row.original.harga_beli_per_qty as number)
props.row.original.unit_price
? formatCurrency(props.row.original.unit_price as number)
: '-',
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{total?.harga_beli_per_qty
? formatCurrency(total?.harga_beli_per_qty)
{total?.avg_unit_price
? formatCurrency(total?.avg_unit_price)
: '-'}
</div>
)
@@ -126,32 +126,32 @@ const ClosingSapronakCalculationTable = ({
},
{
header: 'Total Harga (Rp)',
accessorKey: 'total_harga',
accessorKey: 'total_amount',
cell: (props) =>
props.row.original.total_harga
? formatCurrency(props.row.original.total_harga as number)
props.row.original.total_amount
? formatCurrency(props.row.original.total_amount as number)
: '-',
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{total?.total_harga ? formatCurrency(total?.total_harga) : '-'}
{total?.total_amount ? formatCurrency(total?.total_amount) : '-'}
</div>
)
: '',
},
{
header: 'Keterangan',
accessorKey: 'keterangan',
cell: (props) => (props.row.original.keterangan as string) || '-',
accessorKey: 'notes',
cell: (props) => (props.row.original.notes as string) || '-',
footer: '',
},
];
// Memoize columns untuk setiap kategori
const docBroilerColumns = useMemo(
const docColumns = useMemo(
() =>
isResponseSuccess(sapronakCalculation)
? createColumns(sapronakCalculation.data?.doc_broiler?.total)
? createColumns(sapronakCalculation.data?.doc?.total)
: createColumns(),
[sapronakCalculation]
);
@@ -172,10 +172,18 @@ const ClosingSapronakCalculationTable = ({
[sapronakCalculation]
);
const pulletColumns = useMemo(
() =>
isResponseSuccess(sapronakCalculation)
? createColumns(sapronakCalculation.data?.pullet?.total)
: createColumns(),
[sapronakCalculation]
);
return (
<div className='flex flex-col gap-4'>
<Card
title='DOC Broiler'
title='DOC'
collapsible
defaultCollapsed={false}
className={{
@@ -186,10 +194,10 @@ const ClosingSapronakCalculationTable = ({
<Table<RowSapronakCalculation>
data={
isResponseSuccess(sapronakCalculation)
? (sapronakCalculation.data?.doc_broiler?.rows ?? [])
? (sapronakCalculation.data?.doc?.rows ?? [])
: []
}
columns={docBroilerColumns}
columns={docColumns}
className={{
containerClassName: 'my-4',
}}
@@ -242,6 +250,29 @@ const ClosingSapronakCalculationTable = ({
renderFooter={isResponseSuccess(sapronakCalculation)}
/>
</Card>
<Card
title='Pullet'
variant='bordered'
collapsible
defaultCollapsed={true}
className={{
wrapper: 'w-full',
}}
>
<Table<RowSapronakCalculation>
data={
isResponseSuccess(sapronakCalculation)
? (sapronakCalculation.data?.pullet?.rows ?? [])
: []
}
columns={pulletColumns}
className={{
containerClassName: 'my-4',
}}
renderFooter={isResponseSuccess(sapronakCalculation)}
/>
</Card>
</div>
);
};
+132 -109
View File
@@ -2,7 +2,10 @@
import Button from '@/components/Button';
import CheckboxInput from '@/components/input/CheckboxInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import Modal, { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
@@ -28,6 +31,8 @@ import toast from 'react-hot-toast';
import useSWR from 'swr';
import RequirePermission from '@/components/helper/RequirePermission';
import { useAuth } from '@/services/hooks/useAuth';
import { CustomerApi, ProductApi } from '@/services/api/master-data';
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
const RowsOptionsMenu = ({
type = 'dropdown',
@@ -52,7 +57,7 @@ const RowsOptionsMenu = ({
)}
>
<div className='flex flex-col gap-1'>
{/* <RequirePermission permissions='lti.marketing.delivery_order.detail'>
<RequirePermission permissions='lti.marketing.delivery_order.detail'>
<Button
href={`/marketing/detail?marketingId=${props.row.original.id}`}
variant='ghost'
@@ -62,19 +67,10 @@ const RowsOptionsMenu = ({
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission> */}
<Button
href={`/marketing/detail?marketingId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
{props.row.original.latest_approval.step_number != 1 && (
<>
{/* <RequirePermission
<RequirePermission
permissions={
props.row.original.latest_approval.step_number == 3
? 'lti.marketing.delivery_order.update'
@@ -101,32 +97,12 @@ const RowsOptionsMenu = ({
<Icon icon='mdi:truck' width={16} height={16} />
Deliver
</Button>
</RequirePermission> */}
<Button
href={
props.row.original.latest_approval.step_number == 3
? `/marketing/detail/delivery-orders/edit?marketingId=${props.row.original.id}`
: props.row.original.latest_approval.step_number == 2
? `/marketing/add/delivery-orders?marketingId=${props.row.original.id}`
: undefined
}
onClick={() => {
if (props.row.original.latest_approval.step_number == 2) {
deliveryClickHandler?.();
}
}}
variant='ghost'
color='success'
className='justify-start text-sm'
>
<Icon icon='mdi:truck' width={16} height={16} />
Deliver
</Button>
</RequirePermission>
</>
)}
{props.row.original.latest_approval.step_number != 3 && (
<>
{/* <RequirePermission permissions='lti.marketing.sales_order.update'>
<RequirePermission permissions='lti.marketing.sales_order.update'>
<Button
href={`/marketing/detail/sales-orders/edit?marketingId=${props.row.original.id}`}
variant='ghost'
@@ -136,19 +112,10 @@ const RowsOptionsMenu = ({
<Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit
</Button>
</RequirePermission> */}
<Button
href={`/marketing/detail/sales-orders/edit?marketingId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit
</Button>
</RequirePermission>
</>
)}
{/* <RequirePermission permissions='lti.marketing.sales_order.delete'>
<RequirePermission permissions='lti.marketing.sales_order.delete'>
<Button
onClick={deleteClickHandler}
variant='ghost'
@@ -158,16 +125,7 @@ const RowsOptionsMenu = ({
<Icon icon='mdi:delete-outline' width={16} height={16} />
Delete
</Button>
</RequirePermission> */}
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit justify-start text-sm'
>
<Icon icon='mdi:delete-outline' width={16} height={16} />
Delete
</Button>
</RequirePermission>
</div>
</div>
);
@@ -175,8 +133,6 @@ const RowsOptionsMenu = ({
const MarketingTable = () => {
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [approveAction, setApproveAction] = useState<'APPROVED' | 'REJECTED'>(
'APPROVED'
@@ -186,22 +142,68 @@ const MarketingTable = () => {
const { permissionCheck } = useAuth();
const router = useRouter();
const {
data: marketing,
isLoading: isLoadingMarketing,
mutate: refreshMarketing,
} = useSWR(MarketingApi.basePath, MarketingApi.getAllFetcher);
const deleteModal = useModal();
const confirmationModal = useModal();
const productsModal = useModal();
const deliveryModal = useModal();
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterToQueryString,
} = useTableFilter({
initial: {
search: '',
product_ids: '',
status: '',
customer_id: '',
page: 1,
limit: 10,
},
paramMap: {
page: 'page',
pageSize: 'limit',
product_ids: 'product_ids',
status: 'status',
customer_id: 'customer_id',
},
});
// ===== FETCH DATA =====
const {
data: marketing,
isLoading: isLoadingMarketing,
mutate: refreshMarketing,
} = useSWR(
`${MarketingApi.basePath}${getTableFilterToQueryString()}`,
MarketingApi.getAllFetcher
);
// ===== OPTIONS =====
const {
options: productsOptions,
isLoadingOptions: isLoadingProductsOptions,
} = useSelect(ProductApi.basePath, 'id', 'name', '', {
limit: 'limit',
});
const {
options: customersOptions,
isLoadingOptions: isLoadingCustomersOptions,
} = useSelect(CustomerApi.basePath, 'id', 'name', '', {
limit: 'limit',
});
const statusOptions = MARKETING_APPROVAL_LINE.map((item) => ({
value: item.step_number,
label: item.step_name,
}));
// ===== HANDLER =====
const searchChangeHandler = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
setPage(1);
updateFilter('page', 1);
updateFilter('search', e.target.value);
},
[]
);
@@ -209,7 +211,8 @@ const MarketingTable = () => {
(val: OptionType | OptionType[] | null) => {
const newVal = val as OptionType;
setPageSize(newVal.value as number);
setPage(1);
updateFilter('page', 1);
updateFilter('limit', newVal.value as number);
},
[]
);
@@ -314,20 +317,6 @@ const MarketingTable = () => {
);
};
const {
state: tableFilterState,
updateFilter,
toQueryString: getTableFilterToQueryString,
} = useTableFilter({
initial: {
search: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
},
});
const getRowCanSelect = (row: Row<Marketing>): boolean => {
const approval = row.original.latest_approval;
return approval?.step_number === 1 && approval?.action !== 'REJECTED';
@@ -353,7 +342,7 @@ const MarketingTable = () => {
}}
/>
<div className='flex flex-row gap-2'>
{/* <RequirePermission permissions='lti.marketing.sales_order.approve'>
<RequirePermission permissions='lti.marketing.sales_order.approve'>
<Button
color='success'
onClick={approveClickHandler}
@@ -363,18 +352,9 @@ const MarketingTable = () => {
<Icon icon='material-symbols:check' width={24} height={24} />
Approve
</Button>
</RequirePermission> */}
<Button
color='success'
onClick={approveClickHandler}
className='justify-start text-sm'
disabled={disableApprove}
>
<Icon icon='material-symbols:check' width={24} height={24} />
Approve
</Button>
</RequirePermission>
{/* <RequirePermission permissions='lti.marketing.sales_order.approve'>
<RequirePermission permissions='lti.marketing.sales_order.approve'>
<Button
color='error'
onClick={rejectClickHandler}
@@ -384,19 +364,10 @@ const MarketingTable = () => {
<Icon icon='material-symbols:close' width={24} height={24} />
Reject
</Button>
</RequirePermission> */}
<Button
color='error'
onClick={rejectClickHandler}
className='justify-start text-sm'
disabled={disableReject}
>
<Icon icon='material-symbols:close' width={24} height={24} />
Reject
</Button>
</RequirePermission>
</div>
<TableRowSizeSelector
value={pageSize}
value={tableFilterState.pageSize}
onChange={pageSizeChangeHandler}
options={ROWS_OPTIONS}
className='flex sm:flex-row flex-col gap-3 items-end justify-end'
@@ -406,7 +377,29 @@ const MarketingTable = () => {
label='Product'
isClearable
placeholder='Pilih product'
options={[]}
options={productsOptions}
isLoading={isLoadingProductsOptions}
value={
tableFilterState.product_ids
?.split(',')
.map((id) =>
productsOptions.find(
(option) => option.value === Number(id)
)
)
.filter(
(option): option is { value: number; label: string } =>
option !== undefined
) ?? null
}
onChange={(value: OptionType | OptionType[] | null) =>
updateFilter(
'product_ids',
(value as OptionType[])
?.map((item: OptionType) => item.value.toString())
.join(',') || ''
)
}
isMulti
/>
{/* select status */}
@@ -414,14 +407,43 @@ const MarketingTable = () => {
label='Status'
isClearable
placeholder='Pilih status'
options={[]}
options={statusOptions}
value={
tableFilterState.status
? statusOptions.find(
(option) =>
option.value === Number(tableFilterState.status)
)
: null
}
onChange={(value: OptionType | OptionType[] | null) =>
updateFilter(
'status',
(value as OptionType)?.value.toString() || ''
)
}
/>
{/* select customer */}
<SelectInput
label='Customer'
isClearable
placeholder='Pilih customer'
options={[]}
options={customersOptions}
isLoading={isLoadingCustomersOptions}
value={
tableFilterState.customer_id
? customersOptions.find(
(option) =>
option.value === Number(tableFilterState.customer_id)
)
: null
}
onChange={(value: OptionType | OptionType[] | null) =>
updateFilter(
'customer_id',
(value as OptionType)?.value.toString() || ''
)
}
/>
</TableRowSizeSelector>
</div>
@@ -587,8 +609,8 @@ const MarketingTable = () => {
},
},
]}
pageSize={pageSize}
page={page}
pageSize={tableFilterState.pageSize}
page={tableFilterState.page}
onPageChange={setPage}
className={{
tableWrapperClassName: 'overflow-x-auto min-h-full!',
@@ -712,6 +734,7 @@ const MarketingTable = () => {
'px-6 py-3 last:flex last:flex-row last:justify-end',
paginationClassName: 'hidden',
}}
isLoading={isLoadingMarketing}
/>
</Modal>
</>
+11 -38
View File
@@ -3,7 +3,6 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
// Dashboard
'/dashboard/': ['lti.dashboard.list'],
'/dashboard': ['lti.dashboard.list'],
// Production
// Production - Project Flock
@@ -58,27 +57,14 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
'/purchase/detail/edit/': ['lti.purchase.update'],
// Marketing
'/marketing/': ['lti.dashboard.list', 'lti.marketing.delivery_order.list'],
'/marketing/add/delivery-orders/': [
'lti.dashboard.list',
'lti.marketing.delivery_order.create',
],
'/marketing/add/sales-orders/': [
'lti.dashboard.list',
'lti.marketing.sales_order.create',
],
'/marketing/detail/': [
'lti.dashboard.list',
'lti.marketing.delivery_order.detail',
],
'/marketing/': ['lti.marketing.delivery_order.list'],
'/marketing/add/delivery-orders/': ['lti.marketing.delivery_order.create'],
'/marketing/add/sales-orders/': ['lti.marketing.sales_order.create'],
'/marketing/detail/': ['lti.marketing.delivery_order.detail'],
'/marketing/detail/delivery-orders/edit/': [
'lti.dashboard.list',
'lti.marketing.delivery_order.update',
],
'/marketing/detail/sales-orders/edit/': [
'lti.dashboard.list',
'lti.marketing.sales_order.update',
],
'/marketing/detail/sales-orders/edit/': ['lti.marketing.sales_order.update'],
// Expense
'/expense/': ['lti.expense.list'],
@@ -89,19 +75,12 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
'/expense/realization/edit/': ['lti.expense.update.realization'],
// Finance
'/finance/': ['lti.dashboard.list', 'lti.finance.transaction.list'],
'/finance/detail/': ['lti.dashboard.list', 'lti.finance.transaction.detail'],
'/finance/add/': ['lti.dashboard.list', 'lti.finance.payments.create'],
'/finance/detail/edit/': [
'lti.dashboard.list',
'lti.finance.payments.update',
],
'/finance/add/initial-balance/': [
'lti.dashboard.list',
'lti.finance.initial_balances.create',
],
'/finance/': ['lti.finance.transaction.list'],
'/finance/detail/': ['lti.finance.transaction.detail'],
'/finance/add/': ['lti.finance.payments.create'],
'/finance/detail/edit/': ['lti.finance.payments.update'],
'/finance/add/initial-balance/': ['lti.finance.initial_balances.create'],
'/finance/detail/edit/initial-balance/': [
'lti.dashboard.list',
'lti.finance.initial_balances.update',
],
'/finance/add/injection/': ['lti.finance.injections.create'],
@@ -203,20 +182,14 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
'/master-data/flock/detail/': ['lti.master.flocks.detail'],
'/master-data/flock/detail/edit/': ['lti.master.flocks.update'],
'/master-data/production-standard/': [
'lti.dashboard.list',
'lti.master.production_standards.list',
],
'/master-data/production-standard/': ['lti.master.production_standards.list'],
'/master-data/production-standard/add/': [
'lti.dashboard.list',
'lti.master.production_standards.create',
],
'/master-data/production-standard/detail/': [
'lti.dashboard.list',
'lti.master.production_standards.detail',
],
'/master-data/production-standard/detail/edit/': [
'lti.dashboard.list',
'lti.master.production_standards.update',
],
};
+17 -16
View File
@@ -147,25 +147,25 @@ export type ClosingProductionData = {
export type RowSapronakCalculation = {
id: number;
tanggal: string;
no_referensi: string;
qty_masuk: number;
qty_keluar: number;
qty_pakai: number;
uraian: string;
kategori_produk: string;
harga_beli_per_qty: number;
total_harga: number;
keterangan: string;
date: string;
reference_number: string;
qty_in: number;
qty_out: number;
qty_used: number;
description: string;
product_category: string;
unit_price: number;
total_amount: number;
notes: string;
};
export type TotalSapronakCalculation = {
label: string;
qty_masuk: number;
qty_keluar: number;
qty_pakai: number;
harga_beli_per_qty: number;
total_harga: number;
qty_in: number;
qty_out: number;
qty_used: number;
avg_unit_price: number;
total_amount: number;
};
export type ClosingSapronakCalculationItem = {
@@ -174,9 +174,10 @@ export type ClosingSapronakCalculationItem = {
};
export type ClosingSapronakCalculation = {
doc_broiler: ClosingSapronakCalculationItem;
doc: ClosingSapronakCalculationItem;
ovk: ClosingSapronakCalculationItem;
pakan: ClosingSapronakCalculationItem;
pullet: ClosingSapronakCalculationItem;
};
// ====== OVERHEAD ======