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