mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'development' into fix/transfer-to-laying
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
+2
-2
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user