Merge branch 'development' into 'staging'

Development

See merge request mbugroup/lti-web-client!243
This commit is contained in:
Adnan Zahir
2026-01-24 11:16:16 +07:00
26 changed files with 855 additions and 395 deletions
@@ -13,7 +13,6 @@ interface HppExpeditionReportTableProps {
}
const HppExpeditionReportTable = ({
type = 'detail',
initialValues,
}: HppExpeditionReportTableProps) => {
const costOfRevenueExpeditionData: BaseExpeditionCost[] = useMemo(() => {
@@ -4,7 +4,6 @@ import React, { useMemo } from 'react';
import { ColumnDef } from '@tanstack/react-table';
import Table from '@/components/Table';
import Card from '@/components/Card';
import Badge from '@/components/Badge';
import { formatCurrency, formatNumber, formatDate } from '@/lib/helper';
import {
BaseClosingSales,
@@ -20,10 +19,7 @@ interface SalesReportTableProps {
initialValues?: BaseClosingSales;
}
const SalesReportTable = ({
type = 'detail',
initialValues,
}: SalesReportTableProps) => {
const SalesReportTable = ({ initialValues }: SalesReportTableProps) => {
const salesData: BaseSales[] = useMemo(() => {
return initialValues?.sales || [];
}, [initialValues]);
@@ -1,9 +1,8 @@
'use client';
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useFormik } from 'formik';
import useSWR from 'swr';
import toast from 'react-hot-toast';
import { Icon } from '@iconify/react';
@@ -57,10 +56,6 @@ const ExpenseRequestContent = ({
const isLatestApprovalRejected =
initialValues?.latest_approval.action === 'REJECTED';
const isLatestApprovalRejectedOrDone =
isLatestApprovalRejected ||
initialValues?.latest_approval.step_number === 6;
const isCurrentApprovalOnHeadArea =
!isLatestApprovalRejected &&
initialValues?.latest_approval.step_number === 1;
@@ -35,7 +35,6 @@ import { ExpenseApi } from '@/services/api/expense';
import { cn, formatCurrency, formatDate } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ROWS_OPTIONS } from '@/config/constant';
import { LocationApi, SupplierApi } from '@/services/api/master-data';
import { Location } from '@/types/api/master-data/location';
import { Supplier } from '@/types/api/master-data/supplier';
@@ -44,8 +43,6 @@ import { BaseApiResponse } from '@/types/api/api-general';
const RowOptionsMenu = ({
type = 'dropdown',
props,
approveClickHandler,
rejectClickHandler,
deleteClickHandler,
}: {
type: 'dropdown' | 'collapse';
@@ -186,7 +183,6 @@ const ExpensesTable = () => {
undefined
);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [isCompleteLoading, setIsCompleteLoading] = useState(false);
const [isApproveLoading, setIsApproveLoading] = useState(false);
const [isRejectLoading, setIsRejectLoading] = useState(false);
@@ -247,23 +243,6 @@ const ExpensesTable = () => {
});
}, [expenses, selectedRowIds]);
const isAllSelectedRowLatestApprovalOnRealization = useMemo(() => {
return selectedRowIds.every((rowId) => {
if (!isResponseSuccess(expenses)) return false;
const expenseItem = expenses.data.find((item) => item.id === rowId);
const isLatestApprovalRejected =
expenseItem?.latest_approval.action === 'REJECTED';
const isCurrentApprovalOnRealization =
!isLatestApprovalRejected &&
expenseItem?.latest_approval.step_number === 5;
return isCurrentApprovalOnRealization;
});
}, [expenses, selectedRowIds]);
const expensesColumns: ColumnDef<Expense>[] = [
{
id: 'select',
@@ -589,12 +568,6 @@ const ExpensesTable = () => {
updateFilter('realizationDate', e.target.value);
};
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
const newVal = val as OptionType;
setPageSize(newVal.value as number);
};
// track sorting
useEffect(() => {
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
@@ -30,7 +30,7 @@ interface ExpenseRealizationKandangDetailExpenseProps {
const ExpenseRealizationKandangDetailExpense: React.FC<
ExpenseRealizationKandangDetailExpenseProps
> = ({ type, formik, supplierId, location, className }) => {
> = ({ formik, supplierId, location, className }) => {
const {
setInputValue: setNonstockInputValue,
options: nonstockOptions,
+35 -3
View File
@@ -1,4 +1,10 @@
import { ChangeEventHandler, useMemo, useState } from 'react';
import {
ChangeEventHandler,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { CellContext } from '@tanstack/react-table';
import { useSearchParams } from 'next/navigation';
import useSWR from 'swr';
@@ -33,6 +39,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
import { Icon } from '@iconify/react';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import { useUiStore } from '@/stores/ui/ui.store';
const RowOptionsMenu = ({
type = 'dropdown',
@@ -133,6 +140,9 @@ const RowOptionsMenu = ({
};
const FinanceTable = () => {
const { searchValue, setSearchValue, resetSearchValue } = useUiStore();
const previousPathRef = useRef<string | null>(null);
const {
state: tableFilterState,
updateFilter,
@@ -141,7 +151,7 @@ const FinanceTable = () => {
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
search: searchValue,
transactionType: '',
bankId: '',
customerId: '',
@@ -167,7 +177,7 @@ const FinanceTable = () => {
const [searchParams, setSearchParams] = useSearchParams();
const deleteModal = useModal();
const [pendingFilters, setPendingFilters] = useState({
search: '',
search: searchValue,
transactionType: '',
bankId: '',
customerId: '',
@@ -296,6 +306,7 @@ const FinanceTable = () => {
};
const submitFilterHandler = () => {
updateFilter('search', pendingFilters.search);
setSearchValue(pendingFilters.search);
updateFilter('transactionType', pendingFilters.transactionType);
updateFilter('bankId', pendingFilters.bankId);
updateFilter('customerId', pendingFilters.customerId);
@@ -324,6 +335,7 @@ const FinanceTable = () => {
setPendingFilters(emptyFilters);
updateFilter('search', '');
resetSearchValue();
updateFilter('transactionType', '');
updateFilter('bankId', '');
updateFilter('customerId', '');
@@ -447,6 +459,26 @@ const FinanceTable = () => {
},
];
}, []);
useEffect(() => {
// Store current path on mount
previousPathRef.current = window.location.pathname;
return () => {
const currentPath = window.location.pathname;
// if both paths are within /finance module
const isCurrentPathFinance = currentPath.includes('/finance');
const isPreviousPathFinance =
previousPathRef.current?.includes('/finance');
// reset if we outside finance module entirely
if (isPreviousPathFinance && !isCurrentPathFinance) {
resetSearchValue();
}
};
}, [resetSearchValue]);
return (
<section className='size-full p-6 flex flex-col gap-6'>
<div className='flex justify-end gap-2'>
@@ -251,7 +251,11 @@ const FormFinanceAdd = ({
}
required
isClearable
isDisabled={!formik.values.party_type_option?.value}
isDisabled={
!formik.values.party_type_option?.value ||
(type === 'edit' &&
formik.values.party_type_option.value == 'SUPPLIER')
}
/>
<DateInput
label='Tanggal'
@@ -423,7 +427,7 @@ const FormFinanceAdd = ({
<Button
type='submit'
className='w-min-24'
disabled={formik.isSubmitting || !formik.isValid}
disabled={formik.isSubmitting}
>
Submit
</Button>
@@ -396,7 +396,7 @@ const FormFinanceAddInitialBalance = ({
<Button
type='submit'
className='w-min-24'
disabled={formik.isSubmitting || !formik.isValid}
disabled={formik.isSubmitting}
>
Submit
</Button>
@@ -257,7 +257,7 @@ const FormFinanceInjection = ({
<Button
type='submit'
className='w-min-24'
disabled={formik.isSubmitting || !formik.isValid}
disabled={formik.isSubmitting}
>
Submit
</Button>
@@ -2,7 +2,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useFormik } from 'formik';
import useSWR from 'swr';
import { Icon } from '@iconify/react';
import Button from '@/components/Button';
@@ -18,6 +17,7 @@ import {
Movement,
} from '@/types/api/inventory/movement';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { formatNumber } from '@/lib/helper';
import { useRouter } from 'next/navigation';
import {
MovementFormSchema,
@@ -54,6 +54,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
const [selectedProducts, setSelectedProducts] = useState<number[]>([]);
const [selectedDeliveries, setSelectedDeliveries] = useState<number[]>([]);
const [formErrorList, setFormErrorList] = useState<string[]>([]);
const [productQtyErrorShown, setProductQtyErrorShown] = useState(false);
const [deliveryQtyErrorShown, setDeliveryQtyErrorShown] = useState(false);
const [isInitialized, setIsInitialized] = useState(false);
// ===== FORM HANDLERS =====
const createMovementHandler = useCallback(
@@ -82,22 +85,21 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
quantity: number;
}
// ===== API DATA FETCHING =====
const allProductWarehousesUrl = `${ProductWarehouseApi.basePath}`;
const { data: allProductWarehouses } = useSWR(
allProductWarehousesUrl,
ProductWarehouseApi.getAllFetcher
);
// ===== USE SELECT HOOKS =====
const {
setInputValue: setWarehouseSelectInputValue,
isLoadingOptions: isLoadingWarehouses,
loadMore: loadMoreWarehouses,
rawData: warehouses,
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name', 'search', {
flag: 'EKSPEDISI',
});
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name', 'search');
const { rawData: allProductWarehouses } = useSelect<ProductWarehouse>(
ProductWarehouseApi.basePath,
'id',
'product.name',
'search',
{ limit: '100' }
);
// ===== SELECT INPUT DATA =====
const {
@@ -106,6 +108,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
isLoadingOptions: isLoadingSuppliers,
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search', {
category: 'BOP',
flag: 'EKSPEDISI',
});
// ===== DATA PROCESSING =====
@@ -322,16 +325,18 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
}
);
const productWarehouseOptions = isResponseSuccess(productWarehouses)
? productWarehouses?.data.map((pw) => ({
value: pw.product.id,
label: pw.product.name,
product_id: pw.product.id,
warehouse_id: pw.warehouse.id,
warehouse_name: pw.warehouse.name,
quantity: pw.quantity,
}))
: [];
const productWarehouseOptions = useMemo(() => {
return isResponseSuccess(productWarehouses)
? productWarehouses?.data.map((pw) => ({
value: pw.product.id,
label: pw.product.name,
product_id: pw.product.id,
warehouse_id: pw.warehouse.id,
warehouse_name: pw.warehouse.name,
quantity: pw.quantity,
}))
: [];
}, [productWarehouses]);
// ===== HELPER FUNCTIONS =====
const isRepeaterInputError = <T extends 'products' | 'deliveries'>(
@@ -464,19 +469,24 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
},
];
formik.setFieldValue('products', newProducts);
}, []);
}, [formik.values.products]);
const removeProduct = useCallback((i: number) => {
const updatedProducts =
formik.values.products?.reduce((acc: ProductSchema[], item, index) => {
if (index !== i) {
acc.push(item);
}
return acc;
}, []) ?? [];
const removeProduct = useCallback(
(i: number) => {
const updatedProducts = formik.values.products?.filter(
(_, idx) => idx !== i
);
formik.setFieldValue('products', updatedProducts);
formik.setFieldValue('products', updatedProducts);
}, []);
setSelectedProducts([]);
if (productQtyErrorShown) {
toast.dismiss();
setProductQtyErrorShown(false);
}
},
[formik.values.products, productQtyErrorShown, setSelectedProducts]
);
const bulkRemoveProduct = useCallback(() => {
const updatedProducts =
@@ -485,7 +495,12 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
) ?? [];
formik.setFieldValue('products', updatedProducts);
setSelectedProducts([]);
}, [formik, selectedProducts, setSelectedProducts]);
if (productQtyErrorShown) {
toast.dismiss();
setProductQtyErrorShown(false);
}
}, [formik, selectedProducts, setSelectedProducts, productQtyErrorShown]);
const handleProductChange = useCallback(
(idx: number, val: OptionType | OptionType[] | null) => {
@@ -543,19 +558,24 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
],
},
]);
}, []);
}, [formik.values.deliveries]);
const removeDelivery = useCallback((i: number) => {
const updatedDeliveries =
formik.values.deliveries?.reduce((acc: DeliverySchema[], item, index) => {
if (index !== i) {
acc.push(item);
}
return acc;
}, []) ?? [];
const removeDelivery = useCallback(
(i: number) => {
const updatedDeliveries = formik.values.deliveries?.filter(
(_, idx) => idx !== i
);
formik.setFieldValue('deliveries', updatedDeliveries);
formik.setFieldValue('deliveries', updatedDeliveries);
}, []);
setSelectedDeliveries([]);
if (deliveryQtyErrorShown) {
toast.dismiss();
setDeliveryQtyErrorShown(false);
}
},
[formik.values.deliveries, deliveryQtyErrorShown, setSelectedDeliveries]
);
const bulkRemoveDelivery = useCallback(() => {
const updatedDeliveries =
@@ -564,7 +584,17 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
) ?? [];
formik.setFieldValue('deliveries', updatedDeliveries);
setSelectedDeliveries([]);
}, [formik, selectedDeliveries, setSelectedDeliveries]);
if (deliveryQtyErrorShown) {
toast.dismiss();
setDeliveryQtyErrorShown(false);
}
}, [
formik,
selectedDeliveries,
setSelectedDeliveries,
deliveryQtyErrorShown,
]);
const handleDeliverySelectAllChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -638,26 +668,29 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
[]
);
const handleDeliveryCostChange = useCallback((idx: number, value: number) => {
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value);
const handleDeliveryCostChange = useCallback(
(idx: number, value: number) => {
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value);
const delivery = formik.values.deliveries?.[idx];
if (delivery) {
const productQty = delivery.products.reduce(
(sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
0
);
if (productQty > 0 && value > 0) {
const perItem = value / productQty;
formik.setFieldValue(
`deliveries.${idx}.delivery_cost_per_item`,
perItem
const delivery = formik.values.deliveries?.[idx];
if (delivery) {
const productQty = delivery.products.reduce(
(sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
0
);
} else if (value === 0) {
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
if (productQty > 0 && value > 0) {
const perItem = value / productQty;
formik.setFieldValue(
`deliveries.${idx}.delivery_cost_per_item`,
perItem
);
} else if (value === 0) {
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
}
}
}
}, []);
},
[formik.values.deliveries]
);
const handleDeliveryCostPerItemChange = useCallback(
(idx: number, value: number) => {
@@ -677,7 +710,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
}
}
},
[]
[formik.values.deliveries]
);
const handleDeliveryCostChangeWrapper = useCallback(
@@ -696,17 +729,52 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
[handleDeliveryCostPerItemChange]
);
// UTILITY FUNCTIONS
const getAvailableProductOptions = useCallback(
(currentIdx: number) => {
const selectedProductIds =
formik.values.products
?.filter((p, idx) => {
return idx !== currentIdx && p.product_id && p.product_id !== 0;
})
.map((p) => p.product_id) || [];
return productWarehouseOptions.filter(
(pw) => !selectedProductIds.includes(pw.product_id)
);
},
[formik.values.products, productWarehouseOptions]
);
const getFilteredProductWarehouseOptions = useCallback(() => {
return (
formik.values.products
?.filter((p) => p.product)
.map((p) => ({
value: p.product_id,
label: (p.product as OptionType)?.label,
})) ?? []
.map((p) => {
const totalQtyUsed =
formik.values.deliveries?.reduce((total, d) => {
const productQty = d.products.reduce((sum, deliveryProduct) => {
if (deliveryProduct.product_id === p.product_id) {
return sum + (Number(deliveryProduct.product_qty) || 0);
}
return sum;
}, 0);
return total + productQty;
}, 0) || 0;
const availableQty = Number(p.product_qty) - totalQtyUsed;
if (availableQty > 0) {
return {
value: p.product_id,
label: (p.product as OptionType)?.label,
};
}
return null;
})
.filter((option) => option !== null) ?? []
);
}, [formik.values.products]);
}, [formik.values.products, formik.values.deliveries]);
const getAvailableStock = useCallback(
(productId: number) => {
@@ -730,10 +798,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
const remainingStock = availableStock - requestedQty;
if (requestedQty > 0) {
return `Sisa: ${remainingStock.toLocaleString('en-US')}`;
return `Sisa: ${formatNumber(remainingStock)}`;
}
return `Tersedia: ${availableStock.toLocaleString('en-US')}`;
return `Tersedia: ${formatNumber(availableStock)}`;
},
[formik.values.products, getAvailableStock, type]
);
@@ -753,12 +821,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
if (!relatedProduct) return undefined;
const totalQtyUsed =
formik.values.deliveries?.reduce((total, d, dIdx) => {
const productQty = d.products.reduce((sum, p, pIdx) => {
if (
p.product_id === deliveryProduct.product_id &&
!(dIdx === deliveryIdx && pIdx === productIdx)
) {
formik.values.deliveries?.reduce((total, d) => {
const productQty = d.products.reduce((sum, p) => {
if (p.product_id === deliveryProduct.product_id) {
return sum + (Number(p.product_qty) || 0);
}
return sum;
@@ -767,7 +832,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
}, 0) || 0;
const availableQty = Number(relatedProduct.product_qty) - totalQtyUsed;
return `Tersedia: ${availableQty.toLocaleString('en-US')}`;
const displayQty = availableQty > 0 ? availableQty : 0;
return `Tersedia: ${formatNumber(displayQty)}`;
},
[formik.values.deliveries, formik.values.products, type]
);
@@ -817,7 +885,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
const requestedQty = Number(product.product_qty) || 0;
if (requestedQty > availableStock) {
return `Qty melebihi stok tersedia! Maksimal: ${availableStock.toLocaleString('en-US')}`;
return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)}`;
}
return null;
@@ -966,20 +1034,29 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
});
}, [
formik.values.deliveries
?.map((d) =>
d.products.reduce(
?.map((d, idx) => ({
idx,
productQty: d.products.reduce(
(sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
0
)
),
deliveryCost: parseInt((d.delivery_cost || '').toString()) || 0,
deliveryCostPerItem:
parseInt((d.delivery_cost_per_item || '').toString()) || 0,
}))
.map(
(item) =>
`${item.idx}:${item.productQty}:${item.deliveryCost}:${item.deliveryCostPerItem}`
)
.join(','),
.join('|'),
]);
useEffect(() => {
if (
formik.values.source_warehouse_id &&
type !== 'edit' &&
type !== 'detail'
type !== 'detail' &&
!isInitialized
) {
if (formik.values.products.length === 0) {
formik.setFieldValue('products', [
@@ -1011,8 +1088,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
},
]);
}
setIsInitialized(true);
}
}, [formik.values.source_warehouse_id]);
}, [formik.values.source_warehouse_id, isInitialized, type]);
useEffect(() => {
if (
@@ -1039,6 +1117,113 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
formik.errors.destination_warehouse_id,
]);
useEffect(() => {
if (formik.values.products && formik.values.deliveries) {
const productIds = formik.values.products.map((p) => p.product_id);
const updatedDeliveries = formik.values.deliveries.map((delivery) => {
const deliveryProduct = delivery.products[0];
if (deliveryProduct && deliveryProduct.product_id !== 0) {
if (!productIds.includes(deliveryProduct.product_id)) {
return {
...delivery,
products: [
{
product: null,
product_id: 0,
product_qty: '',
},
],
delivery_cost: '',
delivery_cost_per_item: '',
};
}
}
return delivery;
});
const hasChanges = formik.values.deliveries.some(
(delivery, idx) =>
delivery.products[0]?.product_id !==
updatedDeliveries[idx]?.products[0]?.product_id ||
delivery.delivery_cost !== updatedDeliveries[idx]?.delivery_cost
);
if (hasChanges) {
formik.setFieldValue('deliveries', updatedDeliveries);
}
}
}, [formik.values.products]);
useEffect(() => {
if (productQtyErrorShown) {
toast.dismiss();
setProductQtyErrorShown(false);
}
}, [formik.values.products?.map((p) => p.product_qty).join(',')]);
useEffect(() => {
if (deliveryQtyErrorShown) {
toast.dismiss();
setDeliveryQtyErrorShown(false);
}
}, [
formik.values.deliveries
?.map((d) => d.products.map((p) => p.product_qty).join(','))
.join('|'),
formik.values.products?.map((p) => p.product_qty).join(','),
]);
useEffect(() => {
if (hasExceededStock && !productQtyErrorShown && type !== 'detail') {
const firstErrorIndex = formik.values.products?.findIndex(
(product, idx) => getProductQtyError(idx) !== null
);
if (firstErrorIndex !== undefined && firstErrorIndex >= 0) {
const errorMsg = getProductQtyError(firstErrorIndex);
if (errorMsg) {
toast.error(errorMsg, { duration: Infinity });
setProductQtyErrorShown(true);
}
}
}
}, [
hasExceededStock,
productQtyErrorShown,
type,
formik.values.products,
getProductQtyError,
]);
useEffect(() => {
if (hasInvalidQty && !deliveryQtyErrorShown && type !== 'detail') {
const firstError = formik.values.deliveries?.find(
(delivery, deliveryIdx) =>
delivery.products.some(
(product, productIdx) =>
getDeliveryQtyError(deliveryIdx, productIdx) !== null
)
);
if (firstError) {
const deliveryIdx = formik.values.deliveries?.indexOf(firstError);
if (deliveryIdx !== undefined && deliveryIdx >= 0) {
const errorMsg = getDeliveryQtyError(deliveryIdx, 0);
if (errorMsg) {
toast.error(errorMsg, { duration: Infinity });
setDeliveryQtyErrorShown(true);
}
}
}
}
}, [
hasInvalidQty,
deliveryQtyErrorShown,
type,
formik.values.deliveries,
formik.values.products,
getDeliveryQtyError,
]);
const handleValidateForm = async () => {
const errors = await formik.validateForm();
@@ -1340,7 +1525,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
required
value={product.product ?? undefined}
onChange={(val) => handleProductChange(idx, val)}
options={productWarehouseOptions}
options={getAvailableProductOptions(idx)}
onInputChange={setProductWarehouseSelectInputValue}
onMenuScrollToBottom={loadMoreProductWarehouses}
isLoading={isLoadingProductWarehouses}
@@ -1543,7 +1728,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
</thead>
<tbody>
{formik.values.deliveries?.map((delivery, idx) => (
<tr key={`delivery-row-${idx}`}>
<tr
key={`delivery-row-${idx}-${delivery.supplier_id}-${delivery.vehicle_plate}`}
>
{type !== 'detail' && (
<td className='align-middle!'>
<CheckboxInput
@@ -1857,8 +2044,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
className='px-4'
isLoading={formik.isSubmitting}
disabled={
hasInvalidQty ||
hasExceededStock ||
formik.isSubmitting ||
(formik.values.source_warehouse_id ===
formik.values.destination_warehouse_id &&
@@ -4,7 +4,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useFormik } from 'formik';
import { toast } from 'react-hot-toast';
import useSWR from 'swr';
import { Icon } from '@iconify/react';
import Button from '@/components/Button';
@@ -17,7 +16,6 @@ import SelectInput, {
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission';
import { getUniqueFormikErrors } from '@/lib/formik-helper';
import AlertErrorList from '@/components/helper/form/FormErrors';
import {
@@ -25,7 +23,7 @@ import {
ProductFormValues,
UpdateProductFormSchema,
} from '@/components/pages/master-data/product/form/ProductForm.schema';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { isResponseError } from '@/lib/api-helper';
import {
Product,
CreateProductPayload,
@@ -71,7 +71,6 @@ import {
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
import { formatDate, formatNumber } from '@/lib/helper';
import { getUniqueFormikErrors } from '@/lib/formik-helper';
import toast from 'react-hot-toast';
import ApprovalSteps, {
useApprovalSteps,
@@ -423,7 +422,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
options: locationOptions,
isLoadingOptions: isLoadingLocations,
loadMore: loadMoreLocations,
hasMore: hasMoreLocations,
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
const {
@@ -432,7 +430,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
rawData: projectFlocksRawData,
isLoadingOptions: isLoadingProjectFlocks,
loadMore: loadMoreProjectFlocks,
hasMore: hasMoreProjectFlocks,
} = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', 'search', {
location_id: selectedProjectFlockLocationId,
});
@@ -531,7 +528,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
rawData: stockProducts,
isLoadingOptions: isLoadingStockProducts,
loadMore: loadMoreStockProducts,
hasMore: hasMoreStockProducts,
} = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', '', {
flags: 'PAKAN,OVK',
location_id: stockProductsLocationId,
@@ -539,11 +535,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
});
const {
options: depletionProductOptions,
rawData: depletionProductsData,
isLoadingOptions: isLoadingDepletionProducts,
loadMore: loadMoreDepletionProducts,
hasMore: hasMoreDepletionProducts,
} = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', '', {
location_id: depletionProductsLocationId,
kandang_id: depletionProductsKandangId,
@@ -584,11 +578,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
}, [nextDayRecordingData]);
const {
options: eggProductOptions,
rawData: eggProductsData,
isLoadingOptions: isLoadingEggProducts,
loadMore: loadMoreEggProducts,
hasMore: hasMoreEggProducts,
} = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', {
search: 'telur',
location_id: eggProductsLocationId,
@@ -51,6 +51,13 @@ import { generateUniformityExcel } from '@/components/pages/production/uniformit
import Dropdown from '@/components/Dropdown';
import Menu from '@/components/menu/Menu';
import MenuItem from '@/components/menu/MenuItem';
import { useFormik } from 'formik';
import {
UniformityTableFilterSchema,
type UniformityTableFilterValues,
} from '@/components/pages/production/uniformity/UniformityTableFilter.schema';
import AlertErrorList from '@/components/helper/form/FormErrors';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
const UniformityConfirmationPreview = ({
uniformity,
@@ -241,7 +248,6 @@ const UniformityTable = () => {
options: filterLocationOptions,
isLoadingOptions: isLoadingFilterLocations,
loadMore: loadMoreFilterLocations,
hasMore: hasMoreFilterLocations,
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
// ===== FETCH PROJECT FLOCKS DATA FOR FILTER =====
@@ -251,7 +257,6 @@ const UniformityTable = () => {
rawData: filterProjectFlocksRawData,
isLoadingOptions: isLoadingFilterProjectFlocks,
loadMore: loadMoreFilterProjectFlocks,
hasMore: hasMoreFilterProjectFlocks,
} = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', 'search', {
location_id: filterProjectFlockLocationId,
});
@@ -316,6 +321,34 @@ const UniformityTable = () => {
}
}, [projectFlockKandangLookup]);
// ===== FORMIK FILTER =====
const filterFormik = useFormik<UniformityTableFilterValues>({
initialValues: {
start_date: filterStartDate,
end_date: filterEndDate,
location: filterLocation,
project_flock: filterProjectFlock,
project_flock_kandang_id: filterProjectFlockKandangId,
kandang: filterKandang,
},
validationSchema: UniformityTableFilterSchema,
enableReinitialize: true,
onSubmit: async (values) => {
setFilterStartDate(values.start_date);
setFilterEndDate(values.end_date);
setFilterLocation(values.location ?? null);
setFilterProjectFlock(values.project_flock ?? null);
setFilterKandang(values.kandang ?? null);
setIsSubmitted(true);
filterModal.closeModal();
},
});
// ===== FORMIK ERROR LIST =====
const { formErrorList, close, handleFormSubmit } =
useFormikErrorList(filterFormik);
// ===== BUILD SWR KEY WITH FILTERS =====
const uniformitySwrKey = useMemo(() => {
const basePath = UniformityApi.basePath;
@@ -372,29 +405,54 @@ const UniformityTable = () => {
const handleFilterLocationChange = useCallback(
(val: OptionType | OptionType[] | null) => {
const location = val as OptionType | null;
const locationId = Number(location?.value) || 0;
filterFormik.setFieldValue('location', location);
filterFormik.setFieldValue('location_id', locationId);
setFilterLocation(location);
setFilterProjectFlock(null);
setFilterKandang(null);
setFilterProjectFlockLocationId(
location ? location.value.toString() : ''
);
filterFormik.setFieldValue('project_flock', null);
filterFormik.setFieldValue('project_flock_id', 0);
filterFormik.setFieldValue('kandang', null);
filterFormik.setFieldValue('kandang_id', 0);
},
[]
[filterFormik]
);
const handleFilterProjectFlockChange = useCallback(
(val: OptionType | OptionType[] | null) => {
setFilterProjectFlock(val as OptionType | null);
const projectFlock = val as OptionType | null;
const projectFlockId = Number(projectFlock?.value) || 0;
filterFormik.setFieldValue('project_flock', projectFlock);
filterFormik.setFieldValue('project_flock_id', projectFlockId);
setFilterProjectFlock(projectFlock);
setFilterKandang(null);
filterFormik.setFieldValue('kandang', null);
filterFormik.setFieldValue('kandang_id', 0);
},
[]
[filterFormik]
);
const handleFilterKandangChange = useCallback(
(val: OptionType | OptionType[] | null) => {
setFilterKandang(val as OptionType | null);
const kandang = val as OptionType | null;
const kandangId = Number(kandang?.value) || 0;
filterFormik.setFieldValue('kandang', kandang);
filterFormik.setFieldValue('kandang_id', kandangId);
setFilterKandang(kandang);
},
[]
[filterFormik]
);
const handleResetFilters = useCallback(() => {
@@ -405,41 +463,34 @@ const UniformityTable = () => {
setFilterProjectFlockKandangId(undefined);
setFilterStartDate('');
setFilterEndDate('');
}, []);
setFilterErrors({});
filterFormik.resetForm();
}, [filterFormik]);
const handleFilterStartDateChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setFilterStartDate(value);
filterFormik.setFieldValue('start_date', value);
},
[filterFormik]
);
const handleFilterEndDateChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setFilterEndDate(value);
filterFormik.setFieldValue('end_date', value);
},
[filterFormik]
);
const handleApplyFilters = useCallback(() => {
const errors: Record<string, string> = {};
if (!filterStartDate) {
errors.start_date = 'Tanggal mulai wajib diisi';
}
if (!filterEndDate) {
errors.end_date = 'Tanggal akhir wajib diisi';
}
if (!filterLocation) {
errors.location = 'Lokasi wajib dipilih';
}
if (!filterProjectFlock) {
errors.project_flock = 'Project Flock wajib dipilih';
}
if (!filterKandang) {
errors.kandang = 'Kandang wajib dipilih';
}
setFilterErrors(errors);
if (Object.keys(errors).length === 0) {
setIsSubmitted(true);
filterModal.closeModal();
}
}, [
filterModal,
filterStartDate,
filterEndDate,
filterLocation,
filterProjectFlock,
filterKandang,
]);
handleFormSubmit(
new Event('submit') as unknown as React.FormEvent<HTMLFormElement>
);
}, [handleFormSubmit]);
const selectedRowIds = useMemo(() => {
return Object.keys(rowSelection)
@@ -1136,108 +1187,117 @@ const UniformityTable = () => {
<Icon icon='heroicons:x-mark' width={20} height={20} />
</Button>
</div>
{/* Error List Alert */}
{formErrorList.length > 0 && (
<div className='w-full px-4'>
<AlertErrorList formErrorList={formErrorList} onClose={close} />
</div>
)}
<div className='space-y-4 px-4'>
<div className='grid grid-cols-1 sm:grid-cols-2 sm:gap-4'>
<div>
<DateInput
label='Tanggal'
required
label='Tanggal mulai'
name='start_date'
value={filterStartDate}
onChange={(e) => {
setFilterStartDate(e.target.value);
setFilterErrors((prev) => ({ ...prev, start_date: '' }));
}}
value={filterFormik.values.start_date}
onChange={handleFilterStartDateChange}
onBlur={filterFormik.handleBlur}
isError={
filterFormik.touched.start_date &&
Boolean(filterFormik.errors.start_date)
}
errorMessage={filterFormik.errors.start_date}
className={{ wrapper: 'w-full' }}
/>
{filterErrors.start_date && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.start_date}
</p>
)}
</div>
<div>
<DateInput
label=' '
required
label='Tanggal akhir'
name='end_date'
value={filterEndDate}
onChange={(e) => {
setFilterEndDate(e.target.value);
setFilterErrors((prev) => ({ ...prev, end_date: '' }));
}}
value={filterFormik.values.end_date}
onChange={handleFilterEndDateChange}
onBlur={filterFormik.handleBlur}
isError={
filterFormik.touched.end_date &&
Boolean(filterFormik.errors.end_date)
}
errorMessage={filterFormik.errors.end_date}
className={{ wrapper: 'w-full' }}
/>
{filterErrors.end_date && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.end_date}
</p>
)}
</div>
</div>
<div>
<SelectInput
required
label='Lokasi'
placeholder='Pilih Lokasi...'
value={filterLocation}
value={filterFormik.values.location}
onChange={(value) => {
handleFilterLocationChange(value);
setFilterErrors((prev) => ({ ...prev, location: '' }));
}}
options={filterLocationOptions}
onInputChange={setFilterLocationInputValue}
isLoading={isLoadingFilterLocations}
onMenuScrollToBottom={loadMoreFilterLocations}
isError={
filterFormik.touched.location &&
Boolean(filterFormik.errors.location)
}
errorMessage={filterFormik.errors.location}
isClearable
className={{ wrapper: 'w-full' }}
/>
{filterErrors.location && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.location}
</p>
)}
</div>
<div>
<SelectInput
required
label='Project Flock'
placeholder='Pilih Project Flock...'
value={filterProjectFlock}
value={filterFormik.values.project_flock}
onChange={(value) => {
handleFilterProjectFlockChange(value);
setFilterErrors((prev) => ({ ...prev, project_flock: '' }));
}}
options={filterProjectFlockOptions}
onInputChange={setFilterProjectFlockSearchValue}
isLoading={isLoadingFilterProjectFlocks}
onMenuScrollToBottom={loadMoreFilterProjectFlocks}
isDisabled={!filterLocation}
isDisabled={!filterFormik.values.location}
isError={
filterFormik.touched.project_flock &&
Boolean(filterFormik.errors.project_flock)
}
errorMessage={filterFormik.errors.project_flock}
isClearable
className={{ wrapper: 'w-full' }}
/>
{filterErrors.project_flock && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.project_flock}
</p>
)}
</div>
<div>
<SelectInput
required
label='Kandang'
placeholder='Pilih Kandang...'
value={filterKandang}
value={filterFormik.values.kandang}
onChange={(value) => {
handleFilterKandangChange(value);
setFilterErrors((prev) => ({ ...prev, kandang: '' }));
}}
options={filterKandangOptions}
isDisabled={!filterProjectFlock}
isDisabled={!filterFormik.values.project_flock}
isError={
filterFormik.touched.kandang &&
Boolean(filterFormik.errors.kandang)
}
errorMessage={filterFormik.errors.kandang}
isClearable
className={{ wrapper: 'w-full' }}
/>
{filterErrors.kandang && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.kandang}
</p>
)}
</div>
</div>
@@ -0,0 +1,59 @@
import { OptionType } from '@/components/input/SelectInput';
import * as yup from 'yup';
export type UniformityTableFilterType = {
start_date: string;
end_date: string;
location: OptionType | null;
project_flock: OptionType | null;
project_flock_kandang_id: number | undefined;
kandang: OptionType | null;
};
export const UniformityTableFilterSchema = yup.object({
start_date: yup.string().required('Tanggal mulai wajib diisi'),
end_date: yup
.string()
.required('Tanggal akhir wajib diisi')
.test(
'is-greater-than-start',
'Tanggal akhir tidak boleh masa lampau',
function (value) {
const { start_date } = this.parent;
if (!start_date || !value) return true;
return new Date(value) >= new Date(start_date);
}
),
location: yup
.mixed<OptionType>()
.required('Lokasi wajib dipilih')
.test('is-not-empty', 'Lokasi wajib dipilih', (value) => {
if (Array.isArray(value)) {
return value.length > 0;
}
return !!value;
}),
project_flock: yup
.mixed<OptionType>()
.required('Project Flock wajib dipilih')
.test('is-not-empty', 'Project Flock wajib dipilih', (value) => {
if (Array.isArray(value)) {
return value.length > 0;
}
return !!value;
}),
project_flock_kandang_id: yup.number().optional(),
kandang: yup
.mixed<OptionType>()
.required('Kandang wajib dipilih')
.test('is-not-empty', 'Kandang wajib dipilih', (value) => {
if (Array.isArray(value)) {
return value.length > 0;
}
return !!value;
}),
}) as yup.ObjectSchema<UniformityTableFilterType>;
export type UniformityTableFilterValues = yup.InferType<
typeof UniformityTableFilterSchema
>;
@@ -49,7 +49,7 @@ function CustomTooltip({ payload, label, active }: CustomTooltipProps) {
<div className='flex flex-col gap-2 mt-2'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<div className='w-5 h-5 bg-[#0069E0] rounded-md'></div>
<div className='w-5 h-5 bg-primary rounded-md'></div>
<span className='text-sm'>Ideal</span>
</div>
<span className='text-sm font-medium'>
@@ -84,7 +84,7 @@ function CustomTooltip({ payload, label, active }: CustomTooltipProps) {
<p className='m-0 font-bold text-white/50'>Uniformity 2025</p>
<div className='flex items-center gap-2 mt-2 justify-between'>
<div className='flex items-center gap-2'>
<div className='w-5 h-5 bg-[#0069E0] rounded-md'></div>
<div className='w-5 h-5 bg-primary rounded-md'></div>
<span className='text-sm'>Ideal</span>
</div>
<span className='text-sm font-medium'>{chartData.idealRange}</span>
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';
import { Cell, Pie, PieChart, ResponsiveContainer } from 'recharts';
import Card from '@/components/Card';
import { formatNumber } from '@/lib/helper';
@@ -5,7 +5,6 @@ import { useFormik } from 'formik';
import { useRouter } from 'next/navigation';
import { Icon } from '@iconify/react';
import { toast } from 'react-hot-toast';
import moment from 'moment';
import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
import { useUiStore } from '@/stores/ui/ui.store';
import { useUniformityStore } from '@/stores/uniformity/uniformity.store';
@@ -28,6 +27,7 @@ import { LocationApi } from '@/services/api/master-data';
import {
ProjectFlockApi,
ProjectFlockKandangApi,
RecordingApi,
} from '@/services/api/production';
import { UniformityApi } from '@/services/api/uniformity';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
@@ -40,6 +40,7 @@ import {
ProjectFlockKandangLookup,
ProjectFlock,
} from '@/types/api/production/project-flock';
import { Recording } from '@/types/api/production/recording';
import { Kandang } from '@/types/api/master-data/kandang';
import UniformityPreviewForm from '@/components/pages/production/uniformity/form/UniformityPreviewForm';
import UniformityResultForm from '@/components/pages/production/uniformity/form/UniformityResultForm';
@@ -87,9 +88,7 @@ const UniformityForm = ({
const fileInputRef = useRef<HTMLInputElement>(null);
// ===== SELECT INPUT DATA =====
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
null
);
const [, setSelectedLocation] = useState<OptionType | null>(null);
const [selectedProjectFlockLocationId, setSelectedProjectFlockLocationId] =
useState<string>('');
@@ -106,7 +105,6 @@ const UniformityForm = ({
options: locationOptions,
isLoadingOptions: isLoadingLocations,
loadMore: loadMoreLocations,
hasMore: hasMoreLocations,
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
const {
@@ -115,7 +113,6 @@ const UniformityForm = ({
rawData: projectFlocksRawData,
isLoadingOptions: isLoadingProjectFlocks,
loadMore: loadMoreProjectFlocks,
hasMore: hasMoreProjectFlocks,
} = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', 'search', {
location_id: selectedProjectFlockLocationId,
});
@@ -204,6 +201,20 @@ const UniformityForm = ({
? projectFlockKandangLookupData.data
: undefined;
// ===== RECORDINGS DATA (FOR WEEK CALCULATION) =====
const recordingsUrl = useMemo(() => {
const params = new URLSearchParams({
page: '1',
limit: '100',
});
return `${RecordingApi.basePath}?${params.toString()}`;
}, []);
const { data: recordingsData } = useSWR(
recordingsUrl,
RecordingApi.getAllFetcher
);
// ===== FORM CONFIGURATION =====
const formikInitialValues = useMemo<UniformityFormValues>(
() => getUniformityFormInitialValues(initialValues),
@@ -387,14 +398,24 @@ const UniformityForm = ({
// ===== SIDE EFFECTS =====
useEffect(() => {
if (formik.values.date) {
const date = moment(formik.values.date);
const weekNumber = date.week() - moment(date).startOf('month').week() + 1;
const adjustedWeekNumber = weekNumber <= 0 ? weekNumber + 52 : weekNumber;
if (
projectFlockKandangLookup?.project_flock_kandang_id &&
isResponseSuccess(recordingsData) &&
recordingsData.data
) {
const matchingRecording = recordingsData.data.find(
(recording: Recording) =>
recording.project_flock?.project_flock_kandang_id ===
projectFlockKandangLookup.project_flock_kandang_id
);
formik.setFieldValue('week', adjustedWeekNumber);
if (matchingRecording?.project_flock?.production_standart?.week) {
const weekValue =
matchingRecording.project_flock.production_standart.week;
formik.setFieldValue('week', weekValue);
}
}
}, [formik.values.date]);
}, [projectFlockKandangLookup?.project_flock_kandang_id, recordingsData]);
useEffect(() => {
const unsub = subscribeValidate(() => {
@@ -598,7 +619,7 @@ const UniformityForm = ({
<div className='border border-[#18181B]/25 rounded-2xl p-1 flex items-center justify-center'>
<Button
type='button'
className='rounded-2xl border border-sky-500 bg-[#0069E0] text-white'
className='rounded-2xl border border-sky-500 bg-primary text-white'
onClick={(e) => {
e.stopPropagation();
document.getElementById('file-upload-input')?.click();
@@ -622,7 +643,7 @@ const UniformityForm = ({
<div className='border border-[#18181B]/25 rounded-2xl p-1 flex items-center justify-center'>
<Button
type='button'
className='rounded-2xl border border-sky-500 bg-[#0069E0] text-white'
className='rounded-2xl border border-sky-500 bg-primary text-white'
onClick={(e) => {
e.stopPropagation();
document
@@ -68,7 +68,7 @@ const EmptyState = () => {
<>
<div className='absolute inset-0 flex flex-col items-center justify-center z-10 gap-2'>
<div className='border border-[#18181B]/25 rounded-2xl p-1 flex items-center justify-center my-2'>
<Button className='rounded-2xl border border-sky-500 bg-[#0069E0] text-white'>
<Button className='rounded-2xl border border-sky-500 bg-primary text-white'>
<Icon icon={'heroicons:funnel'} className='text-4xl text-whitd' />
</Button>
</div>
@@ -29,7 +29,7 @@ const UniformityGaugeChartSkeleton: React.FC<
return (
<div className='flex flex-col w-full items-center'>
<div className='h-64 w-full relative flex justify-center min-h-[256px]'>
<div className='h-64 w-full relative flex justify-center min-h-64'>
<div className='relative w-full h-full flex flex-col items-center justify-end min-w-0'>
<ResponsiveContainer width='100%' height={256}>
<PieChart>
@@ -57,7 +57,7 @@ const UniformityGaugeChartSkeleton: React.FC<
</ResponsiveContainer>
<div className='absolute inset-x-0 top-24 flex flex-col items-center justify-center'>
<div className='border border-[#18181B]/25 rounded-2xl p-1 flex items-center justify-center mt-5'>
<Button className='rounded-2xl border border-sky-500 bg-[#0069E0] text-white'>
<Button className='rounded-2xl border border-sky-500 bg-primary text-white'>
<Icon
icon={'heroicons:funnel'}
className='text-4xl text-whitd'
@@ -5,7 +5,7 @@ const UniformityTableSkeleton = () => {
return (
<div className='flex flex-col items-center justify-center gap-2 my-20'>
<div className='border border-[#18181B]/25 rounded-2xl p-1 flex items-center justify-center'>
<Button className='rounded-2xl border border-sky-500 bg-[#0069E0] text-white'>
<Button className='rounded-2xl border border-sky-500 bg-primary text-white'>
<Icon
icon={'heroicons-outline:chart-bar'}
className='text-4xl text-whitd'
@@ -148,6 +148,8 @@ const PurchaseRequestForm = ({
options: supplierOptions,
isLoadingOptions: isLoadingSuppliers,
rawData: supplierRawData,
loadMore: loadMoreSuppliers,
hasMore: hasMoreSuppliers,
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search', {
category: 'SAPRONAK',
});
@@ -528,6 +530,7 @@ const PurchaseRequestForm = ({
onChange={handleSupplierChange}
options={supplierOptions}
onInputChange={setSupplierSelectInputValue}
onMenuScrollToBottom={loadMoreSuppliers}
isLoading={isLoadingSuppliers}
isError={
formik.touched.supplier_id &&
@@ -320,110 +320,203 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>Pengambilan</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1.5 }]}>
<View
style={[
pdfStyles.tableCellHeader,
{ flex: 1.5, borderRightWidth: 0 },
]}
>
<Text>Sales</Text>
</View>
</View>
{/* Table Body */}
{customerReport.rows.map((item, index) => (
<View
key={index}
style={[
pdfStyles.tableRow,
index < customerReport.rows.length - 1
? pdfStyles.tableBorderBottom
: {},
]}
>
<>
{/* Initial Balance Row */}
<View style={[pdfStyles.tableRow, pdfStyles.tableBorderBottom]}>
<View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}>
<Text>{index + 1}</Text>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 1.2 }]}>
<Text>
{item.trans_date
? formatDate(item.trans_date, 'DD MMM YY')
: '-'}
</Text>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 1.2 }]}>
<Text>
{item.delivery_date
? formatDate(item.delivery_date, 'DD MMM YY')
: '-'}
</Text>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 0.8 }]}>
<Text>
{item.aging_day ? formatNumber(item.aging_day) : '-'} hari
</Text>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
<Text>{item.reference || '-'}</Text>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
<Text>
{Array.isArray(item.vehicle_numbers)
? item.vehicle_numbers.join(', ')
: item.vehicle_numbers || '-'}
</Text>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
<Text>{formatNumber(item.qty)}</Text>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
<Text>{formatNumber(item.weight)}</Text>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
<Text>{formatNumber(item.average_weight)}</Text>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text>{formatCurrency(item.unit_price)}</Text>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text>{formatCurrency(item.final_price)}</Text>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text>{formatCurrency(item.total_price)}</Text>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text>{formatCurrency(item.payment_amount)}</Text>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text style={pdfStyles.textError}>
{formatCurrency(item.accounts_receivable)}
<View
style={[
pdfStyles.tableCellRight,
{
flex: 1.2,
color:
typeof customerReport.initial_balance === 'number' &&
customerReport.initial_balance < 0
? 'red'
: 'black',
},
]}
>
<Text>
{formatCurrency(customerReport.initial_balance || 0)}
</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
{item.status ? (
<View
style={[
pdfStyles.badge,
item.status === 'LUNAS'
? pdfStyles.badgeLunas
: pdfStyles.badgeBelumLunas,
]}
>
<Text>
{item.status === 'LUNAS' ? 'Lunas' : 'Belum Lunas'}
</Text>
</View>
) : (
<Text>-</Text>
)}
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text>
{Array.isArray(item.pickup_info)
? item.pickup_info.join(', ')
: item.pickup_info || '-'}
</Text>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
<Text>{item.sales_person || '-'}</Text>
<View
style={[
pdfStyles.tableCell,
{ flex: 1.5, borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
</View>
))}
{/* Data Rows */}
{customerReport.rows.map((item, index) => (
<View
key={index}
style={[
pdfStyles.tableRow,
index < customerReport.rows.length - 1
? pdfStyles.tableBorderBottom
: {},
]}
>
<View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}>
<Text>{index + 1}</Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 1.2 }]}>
<Text>
{item.trans_date
? formatDate(item.trans_date, 'DD MMM YY')
: '-'}
</Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 1.2 }]}>
<Text>
{item.delivery_date
? formatDate(item.delivery_date, 'DD MMM YY')
: '-'}
</Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 0.8 }]}>
<Text>
{item.aging_day != null
? `${formatNumber(item.aging_day)} hari`
: '-'}
</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
<Text>{item.reference || '-'}</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
<Text>
{Array.isArray(item.vehicle_numbers)
? item.vehicle_numbers.length > 0
? item.vehicle_numbers.join(', ')
: '-'
: '-'}
</Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
<Text>{formatNumber(item.qty)}</Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
<Text>{formatNumber(item.weight)}</Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
<Text>{formatNumber(item.average_weight)}</Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text>{formatCurrency(item.unit_price)}</Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text>{formatCurrency(item.final_price)}</Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text>{formatCurrency(item.total_price)}</Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text>{formatCurrency(item.payment_amount)}</Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text style={pdfStyles.textError}>
{formatCurrency(item.accounts_receivable)}
</Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 1.5 }]}>
{item.status ? (
<View
style={[
pdfStyles.badge,
item.status === 'LUNAS'
? pdfStyles.badgeLunas
: pdfStyles.badgeBelumLunas,
]}
>
<Text>
{item.status === 'LUNAS' ? 'Lunas' : 'Belum Lunas'}
</Text>
</View>
) : (
<Text>-</Text>
)}
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text>
{Array.isArray(item.pickup_info)
? item.pickup_info.length > 0
? item.pickup_info.join(', ')
: '-'
: '-'}
</Text>
</View>
<View
style={[
pdfStyles.tableCell,
{ flex: 1.5, borderRightWidth: 0 },
]}
>
<Text>{item.sales_person || '-'}</Text>
</View>
</View>
))}
</>
{/* Summary Row */}
{customerReport.summary && (
@@ -488,7 +581,12 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellLast, { flex: 1.5 }]}>
<View
style={[
pdfStyles.tableCell,
{ flex: 1.5, borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
</View>
@@ -44,20 +44,50 @@ export const generateCustomerPaymentExcel = async (
const worksheet = workbook.addWorksheet(customerName.substring(0, 31));
worksheet.columns = columns;
const initialRow = worksheet.addRow({
no: '',
transDate: '',
deliveryDate: '',
aging: '',
reference: '',
vehicleNumbers: '',
qty: '',
weight: '',
avgWeight: '',
unitPrice: '',
finalPrice: '',
totalPrice: '',
paymentAmount: '',
accountsReceivable: formatCurrency(customerReport.initial_balance || 0),
status: '',
pickupInfo: '',
salesPerson: '',
});
const initialBalanceCell = initialRow.getCell('accountsReceivable');
if (
typeof customerReport.initial_balance === 'number' &&
customerReport.initial_balance < 0
) {
initialBalanceCell.font = { color: { argb: 'FFFF0000' } };
}
customerData.forEach((item, index) => {
const row = worksheet.addRow({
no: index + 1,
transDate: item.trans_date
? formatDate(item.trans_date, 'DD MMM YYYY')
: '',
: '-',
deliveryDate: item.delivery_date
? formatDate(item.delivery_date, 'DD MMM YYYY')
: '',
aging: formatNumber(item.aging_day || 0),
reference: item.reference || '',
: '-',
aging: item.aging_day != null ? formatNumber(item.aging_day) : '-',
reference: item.reference || '-',
vehicleNumbers: Array.isArray(item.vehicle_numbers)
? item.vehicle_numbers.join(', ')
: '',
? item.vehicle_numbers.length > 0
? item.vehicle_numbers.join(', ')
: '-'
: '-',
qty: formatNumber(item.qty || 0),
weight: formatNumber(item.weight || 0),
avgWeight: formatNumber(item.average_weight || 0),
@@ -66,11 +96,13 @@ export const generateCustomerPaymentExcel = async (
totalPrice: formatCurrency(item.total_price || 0),
paymentAmount: formatCurrency(item.payment_amount || 0),
accountsReceivable: formatCurrency(item.accounts_receivable || 0),
status: item.status || '',
status: item.status || '-',
pickupInfo: Array.isArray(item.pickup_info)
? item.pickup_info.join(', ')
: '',
salesPerson: item.sales_person || '',
? item.pickup_info.length > 0
? item.pickup_info.join(', ')
: '-'
: '-',
salesPerson: item.sales_person || '-',
});
const accountsReceivableCell = row.getCell('accountsReceivable');
@@ -3,10 +3,7 @@ import useSWR from 'swr';
import { Icon } from '@iconify/react';
import Card from '@/components/Card';
import Badge from '@/components/Badge';
import SelectInput, {
useSelect,
OptionType,
} from '@/components/input/SelectInput';
import SelectInput, { useSelect } from '@/components/input/SelectInput';
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
import DateInput from '@/components/input/DateInput';
import { CustomerApi } from '@/services/api/master-data';
@@ -81,14 +78,14 @@ const CustomerPaymentTab = () => {
const normalizedValue = notes.toLowerCase();
if (normalizedValue === 'lunas') {
return 'bg-info/10 text-info border-info';
return 'bg-info/10 text-black border-info';
}
if (normalizedValue.includes('belum')) {
return 'bg-warning/10 text-warning border-warning';
return 'bg-warning/10 text-black border-warning';
}
return 'bg-gray-100 text-gray-600 border-gray-300';
return 'bg-gray-100 text-black border-gray-300';
};
const getPaymentStatusIndicatorColor = (notes: string) => {
@@ -364,7 +361,11 @@ const CustomerPaymentTab = () => {
enableSorting: false,
cell: (props) => {
const value = props.row.original.vehicle_numbers;
return Array.isArray(value) ? value.join(', ') : value || '-';
return Array.isArray(value)
? value.length > 0
? value.join(', ')
: '-'
: '-';
},
},
{
@@ -510,13 +511,14 @@ const CustomerPaymentTab = () => {
return (
<Badge
statusIndicator={true}
size='sm'
variant='soft'
className={{
badge: `rounded-xl justify-start border border-gray-200 ${getPaymentStatusColor(value)}`,
badge: `py-2.5 px-2 font-thin text-xs border border-gray-200 rounded-xl justify-start ${getPaymentStatusColor(value)}`,
status: getPaymentStatusIndicatorColor(value),
}}
>
<span className='capitalize'>{getPaymentStatusText(value)}</span>
{getPaymentStatusText(value)}
</Badge>
);
},
@@ -528,7 +530,11 @@ const CustomerPaymentTab = () => {
enableSorting: false,
cell: (props) => {
const value = props.row.original.pickup_info;
return Array.isArray(value) ? value.join(', ') : value || '-';
return Array.isArray(value)
? value.length > 0
? value.join(', ')
: '-'
: '-';
},
},
{
@@ -754,9 +760,9 @@ const CustomerPaymentTab = () => {
wrapper: 'w-full rounded-2xl',
body: 'p-0',
title:
'py-1.5 px-3 bg-[#0069E0] text-white text-lg font-normal',
'py-1.5 px-3 bg-primary text-white text-lg font-normal',
subtitle:
'px-3 pb-1 bg-[#0069E0] text-white text-sm font-normal',
'px-3 pb-1 bg-primary text-white text-sm font-normal',
}}
variant='bordered'
collapsible={true}
@@ -772,7 +778,7 @@ const CustomerPaymentTab = () => {
pageSize={customerReport.rows.length + 1}
renderFooter={customerReport.rows.length > 0}
className={{
containerClassName: 'w-full',
containerClassName: 'w-full mb-0!',
tableWrapperClassName: 'overflow-x-auto',
tableClassName: 'w-full table-auto text-sm',
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
+5
View File
@@ -31,6 +31,11 @@ export const formatNumber = (
}).format(value);
};
export const safeRound = (num: number, decimals: number) => {
const factor = 10 ** decimals;
return Math.round((num + Number.EPSILON) * factor) / factor;
};
export const formatTitleCase = (value: string) => {
return value
.toLowerCase()
+54 -52
View File
@@ -1,6 +1,6 @@
import * as XLSX from 'xlsx';
import toast from 'react-hot-toast';
import { formatDate } from '@/lib/helper';
import { formatDate, safeRound } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { BaseApiService } from '@/services/api/base';
import { httpClient, httpClientFetcher } from '@/services/http/client';
@@ -32,21 +32,21 @@ export class ProductionResultReportApiService extends BaseApiService<
const mappedProductionResults: {
projectFlockKandang: BaseProjectFlockKandang;
productionResult: ProductionResult[] | null;
}[] = [];
}[] = await Promise.all(
(projectFlockKandangs || []).map(async (projectFlockKandang) => {
const getProductionResultPath = `${this.basePath}/${projectFlockKandang.id}?page=1&limit=99999999`;
const getProductionResultRes = await httpClient<
BaseApiResponse<ProductionResult[]>
>(getProductionResultPath);
projectFlockKandangs?.forEach(async (projectFlockKandang) => {
const getProductionResultPath = `${this.basePath}/${projectFlockKandang.id}?page=1&limit=99999999`;
const getProductionResultRes = await httpClient<
BaseApiResponse<ProductionResult[]>
>(getProductionResultPath);
mappedProductionResults.push({
projectFlockKandang,
productionResult: isResponseSuccess(getProductionResultRes)
? getProductionResultRes.data
: null,
});
});
return {
projectFlockKandang,
productionResult: isResponseSuccess(getProductionResultRes)
? getProductionResultRes.data
: null,
};
})
);
const rows = mappedProductionResults;
if (!rows || rows.length === 0) {
@@ -68,44 +68,46 @@ export class ProductionResultReportApiService extends BaseApiService<
row.productionResult?.forEach((productionResult) => {
groupedData[kandangName].push({
woa: productionResult.woa,
bw: productionResult.bw,
std_bw: productionResult.std_bw,
uniformity: productionResult.uniformity,
woa: safeRound(productionResult.woa, 2),
bw: safeRound(productionResult.bw, 2),
std_bw: safeRound(productionResult.std_bw, 2),
uniformity: safeRound(productionResult.uniformity, 2),
std_uniformity: productionResult.std_uniformity,
dep_kum: productionResult.dep_kum,
dep_std: productionResult.dep_std,
butiran_utuh: productionResult.butiran_utuh,
butiran_putih: productionResult.butiran_putih,
butiran_retak: productionResult.butiran_retak,
butiran_pecah: productionResult.butiran_pecah,
butiran_jumlah: productionResult.butiran_jumlah,
total_butir: productionResult.total_butir,
kg_utuh: productionResult.kg_utuh,
kg_putih: productionResult.kg_putih,
kg_retak: productionResult.kg_retak,
kg_pecah: productionResult.kg_pecah,
kg_jumlah: productionResult.kg_jumlah,
total_kg: productionResult.total_kg,
persen_utuh: productionResult.persen_utuh,
persen_putih: productionResult.persen_putih,
persen_retak: productionResult.persen_retak,
persen_pecah: productionResult.persen_pecah,
hd: productionResult.hd,
hd_std: productionResult.hd_std,
fi: productionResult.fi,
fi_std: productionResult.fi_std,
em: productionResult.em,
em_std: productionResult.em_std,
ew: productionResult.ew,
ew_std: productionResult.ew_std,
fcr: productionResult.fcr,
fcr_std: productionResult.fcr_std,
hh: productionResult.hh,
hh_std: productionResult.hh_std,
project_flock_name: productionResult.project_flock.name,
project_flock_category: productionResult.project_flock.category,
kandang_name: productionResult.project_flock.kandang.name,
dep_kum: safeRound(productionResult.dep_kum, 2),
dep_std: safeRound(productionResult.dep_std, 2),
butiran_utuh: safeRound(productionResult.butiran_utuh, 2),
butiran_putih: safeRound(productionResult.butiran_putih, 2),
butiran_retak: safeRound(productionResult.butiran_retak, 2),
butiran_pecah: safeRound(productionResult.butiran_pecah, 2),
butiran_jumlah: safeRound(productionResult.butiran_jumlah, 2),
total_butir: safeRound(productionResult.total_butir, 2),
kg_utuh: safeRound(productionResult.kg_utuh, 2),
kg_putih: safeRound(productionResult.kg_putih, 2),
kg_retak: safeRound(productionResult.kg_retak, 2),
kg_pecah: safeRound(productionResult.kg_pecah, 2),
kg_jumlah: safeRound(productionResult.kg_jumlah, 2),
total_kg: safeRound(productionResult.total_kg, 2),
persen_utuh: safeRound(productionResult.persen_utuh, 2),
persen_putih: safeRound(productionResult.persen_putih, 2),
persen_retak: safeRound(productionResult.persen_retak, 2),
persen_pecah: safeRound(productionResult.persen_pecah, 2),
hd: safeRound(productionResult.hd, 2),
hd_std: safeRound(productionResult.hd_std, 2),
fi: safeRound(productionResult.fi, 2),
fi_std: safeRound(productionResult.fi_std, 2),
em: safeRound(productionResult.em, 2),
em_std: safeRound(productionResult.em_std, 2),
ew: safeRound(productionResult.ew, 2),
ew_std: safeRound(productionResult.ew_std, 2),
fcr: safeRound(productionResult.fcr, 2),
fcr_std: safeRound(productionResult.fcr_std, 2),
hh: safeRound(productionResult.hh, 2),
hh_std: safeRound(productionResult.hh_std, 2),
project_flock_name:
row.projectFlockKandang.project_flock.flock_name,
project_flock_category:
row.projectFlockKandang.project_flock.category,
kandang_name: row.projectFlockKandang.kandang.name,
created_at: formatDate(productionResult.created_at, 'YYYY-MM-DD'),
updated_at: formatDate(productionResult.updated_at, 'YYYY-MM-DD'),
});