Merge branch 'development' into 'production'

Development

See merge request mbugroup/lti-web-client!373
This commit is contained in:
Adnan Zahir
2026-04-07 16:53:36 +07:00
25 changed files with 553 additions and 253 deletions
+3
View File
@@ -45,3 +45,6 @@ next-env.d.ts
# claude # claude
.claude .claude
# rtk
rtk.exe
+1 -1
View File
@@ -1,3 +1,3 @@
npm run format npm run format
npm run lint npm run lint
npx tsc --noEmit npm run typecheck
+2 -1
View File
@@ -7,9 +7,10 @@
"build": "next build --turbopack", "build": "next build --turbopack",
"start": "next start", "start": "next start",
"lint": "eslint", "lint": "eslint",
"typecheck": "next typegen && tsc --noEmit",
"prepare": "husky", "prepare": "husky",
"format": "prettier --write .", "format": "prettier --write .",
"pre-commit": "npm run format && npm run lint && npx tsc --noEmit && npm run build" "pre-commit": "npm run format && npm run lint && npm run typecheck && npm run build"
}, },
"dependencies": { "dependencies": {
"@react-pdf/renderer": "^4.3.1", "@react-pdf/renderer": "^4.3.1",
@@ -11,10 +11,13 @@ const RecordingEdit = () => {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const recordingId = searchParams.get('recordingId'); const recordingId = searchParams.get('recordingId');
const recordingDetailKey = recordingId
? ['recording-detail', recordingId]
: null;
const { data: recording, isLoading: isLoadingRecording } = useSWR( const { data: recording, isLoading: isLoadingRecording } = useSWR(
recordingId, recordingDetailKey,
(id: string) => RecordingApi.getSingle(parseInt(id)) ([, id]: [string, string]) => RecordingApi.getSingle(parseInt(id))
); );
if (!recordingId) { if (!recordingId) {
+5 -2
View File
@@ -11,10 +11,13 @@ const RecordingDetail = () => {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const recordingId = searchParams.get('recordingId'); const recordingId = searchParams.get('recordingId');
const recordingDetailKey = recordingId
? ['recording-detail', recordingId]
: null;
const { data: recording, isLoading: isLoadingRecording } = useSWR( const { data: recording, isLoading: isLoadingRecording } = useSWR(
recordingId, recordingDetailKey,
(id: string) => RecordingApi.getSingle(parseInt(id)) ([, id]: [string, string]) => RecordingApi.getSingle(parseInt(id))
); );
if (!recordingId) { if (!recordingId) {
+16 -8
View File
@@ -566,24 +566,32 @@ const useSelect = <T,>(
setSize(size + 1); setSize(size + 1);
}; };
let formattedSuccessRawData: SuccessApiResponse<T[]> | undefined = undefined;
let formattedErrorRawData: ErrorApiResponse | undefined = undefined;
const latestPagesIndex = pages?.length ? pages.length - 1 : 0; const latestPagesIndex = pages?.length ? pages.length - 1 : 0;
const { formattedSuccessRawData, formattedErrorRawData } = useMemo(() => {
let successData: SuccessApiResponse<T[]> | undefined = undefined;
let errorData: ErrorApiResponse | undefined = undefined;
if (isResponseSuccess(pages?.[latestPagesIndex])) { if (isResponseSuccess(pages?.[latestPagesIndex])) {
formattedSuccessRawData = { successData = {
...pages?.[latestPagesIndex], ...pages![latestPagesIndex],
data: data:
pages?.flatMap((page) => (isResponseSuccess(page) ? page.data : [])) ?? pages?.flatMap((page) =>
[], isResponseSuccess(page) ? page.data : []
) ?? [],
}; };
} }
if (isResponseError(pages?.[latestPagesIndex])) { if (isResponseError(pages?.[latestPagesIndex])) {
formattedErrorRawData = pages?.[latestPagesIndex]; errorData = pages![latestPagesIndex];
} }
return {
formattedSuccessRawData: successData,
formattedErrorRawData: errorData,
};
}, [pages, latestPagesIndex]);
return { return {
inputValue, inputValue,
setInputValue, setInputValue,
@@ -276,7 +276,7 @@ const SalesClosingTable = ({ projectFlockId }: SalesClosingTableProps) => {
{ {
id: 'kandang', id: 'kandang',
accessorKey: 'kandang', accessorKey: 'kandang',
header: 'Kandang', header: 'Kandang Atribusi',
cell: (props) => { cell: (props) => {
const kandang = props.getValue() as Kandang; const kandang = props.getValue() as Kandang;
return kandang?.name || '-'; return kandang?.name || '-';
@@ -127,11 +127,11 @@ const ClosingOutgoingSapronaksTable = ({
}, },
{ {
accessorKey: 'source_warehouse', accessorKey: 'source_warehouse',
header: 'Gudang Asal', header: 'Gudang Asal (Fisik)',
}, },
{ {
accessorKey: 'destination_warehouse', accessorKey: 'destination_warehouse',
header: 'Gudang Tujuan', header: 'Gudang Tujuan (Fisik)',
}, },
{ {
accessorKey: 'quantity', accessorKey: 'quantity',
@@ -746,7 +746,7 @@ const MarketingTable = () => {
} }
columns={[ columns={[
{ {
header: 'Kandang', header: 'Gudang Fisik',
accessorFn(row) { accessorFn(row) {
return row.product_warehouse.warehouse.name; return row.product_warehouse.warehouse.name;
}, },
@@ -207,7 +207,6 @@ const SalesOrderFormModal = ({
return { return {
vehicle_number: product.vehicle_number as string, vehicle_number: product.vehicle_number as string,
kandang_id: product.kandang_id as number,
product_warehouse_id: product.product_warehouse_id as number, product_warehouse_id: product.product_warehouse_id as number,
unit_price: parseFloat(String(product.unit_price || 0)), unit_price: parseFloat(String(product.unit_price || 0)),
total_weight: parseFloat(String(product.total_weight || 0)), total_weight: parseFloat(String(product.total_weight || 0)),
@@ -13,6 +13,7 @@ import {
Marketing, Marketing,
} from '@/types/api/marketing/marketing'; } from '@/types/api/marketing/marketing';
import { formatDate, formatTitleCase } from '@/lib/helper'; import { formatDate, formatTitleCase } from '@/lib/helper';
import { getProductWarehouseOptionLabel } from '@/lib/product-warehouse';
type MarketingSchemaType = { type MarketingSchemaType = {
customer_id: number | undefined; customer_id: number | undefined;
@@ -97,17 +98,21 @@ export type DeliveryOrderFormValues = Yup.InferType<typeof DeliveryOrderSchema>;
export const SalesProductToFieldValues = ( export const SalesProductToFieldValues = (
product: BaseSalesOrder product: BaseSalesOrder
): SalesOrderProductFormValues => { ): SalesOrderProductFormValues => {
const warehouseOption = {
value: product.product_warehouse.warehouse.id,
label: product.product_warehouse.warehouse.name,
};
return { return {
id: product.id, id: product.id,
vehicle_number: product.vehicle_number, vehicle_number: product.vehicle_number,
warehouse_id: product.product_warehouse.warehouse.id,
warehouse: warehouseOption,
kandang_id: product.product_warehouse.warehouse.id, kandang_id: product.product_warehouse.warehouse.id,
kandang: { kandang: warehouseOption,
value: product.product_warehouse.warehouse.id,
label: product.product_warehouse.warehouse.name,
},
product_warehouse: { product_warehouse: {
value: product.product_warehouse.id, value: product.product_warehouse.id,
label: product.product_warehouse.product.name, label: getProductWarehouseOptionLabel(product.product_warehouse),
}, },
product_warehouse_data: product.product_warehouse, product_warehouse_data: product.product_warehouse,
product_warehouse_id: product.product_warehouse.id, product_warehouse_id: product.product_warehouse.id,
@@ -142,6 +147,10 @@ export const DeliveryProductToFieldValues = (
const soId = salesOrders.find( const soId = salesOrders.find(
(so) => so.product_warehouse.id === item.product_warehouse.id (so) => so.product_warehouse.id === item.product_warehouse.id
)?.id; )?.id;
const warehouseOption = {
value: item.product_warehouse.warehouse.id,
label: item.product_warehouse.warehouse.name,
};
return { return {
id: soId, id: soId,
unit_price: item.unit_price, unit_price: item.unit_price,
@@ -156,15 +165,15 @@ export const DeliveryProductToFieldValues = (
marketing_product: { marketing_product: {
id: soId, id: soId,
vehicle_number: item.vehicle_number, vehicle_number: item.vehicle_number,
warehouse_id: item.product_warehouse.warehouse.id,
warehouse: warehouseOption,
kandang_id: item.product_warehouse.warehouse.id, kandang_id: item.product_warehouse.warehouse.id,
kandang: { kandang: warehouseOption,
value: item.product_warehouse.warehouse.id,
label: item.product_warehouse.warehouse.name,
},
product_warehouse: { product_warehouse: {
value: item.product_warehouse.id, value: item.product_warehouse.id,
label: item.product_warehouse.product.name, label: getProductWarehouseOptionLabel(item.product_warehouse),
}, },
product_warehouse_data: item.product_warehouse,
product_warehouse_id: item.product_warehouse.id, product_warehouse_id: item.product_warehouse.id,
unit_price: item.unit_price, unit_price: item.unit_price,
total_weight: item.total_weight, total_weight: item.total_weight,
@@ -112,7 +112,7 @@ const DeliveryOrderProductForm = ({
if (!Boolean(item.qty)) { if (!Boolean(item.qty)) {
return { return {
value: item.id, value: item.id,
label: `${item.marketing_product?.product_warehouse?.label} - ${item.marketing_product?.kandang?.label}`, label: `${item.marketing_product?.product_warehouse?.label} - ${item.marketing_product?.warehouse?.label ?? item.marketing_product?.kandang?.label}`,
} as OptionType; } as OptionType;
} else { } else {
return null; return null;
@@ -333,7 +333,7 @@ const DeliveryOrderProductForm = ({
if (initialValues?.marketing_product_id) { if (initialValues?.marketing_product_id) {
setSelectedProduct({ setSelectedProduct({
value: initialValues?.id, value: initialValues?.id,
label: `${initialValues?.marketing_product?.product_warehouse?.label} - ${initialValues?.marketing_product?.kandang?.label}`, label: `${initialValues?.marketing_product?.product_warehouse?.label} - ${initialValues?.marketing_product?.warehouse?.label ?? initialValues?.marketing_product?.kandang?.label}`,
} as OptionType); } as OptionType);
} }
} }
@@ -472,7 +472,11 @@ const DeliveryOrderProductForm = ({
text={ text={
exisitingValues?.find( exisitingValues?.find(
(item) => item.id === selectedProduct?.value (item) => item.id === selectedProduct?.value
)?.marketing_product?.kandang?.label ?? '' )?.marketing_product?.warehouse?.label ??
exisitingValues?.find(
(item) => item.id === selectedProduct?.value
)?.marketing_product?.kandang?.label ??
''
} }
color='success' color='success'
className={{ className={{
@@ -3,6 +3,11 @@ import * as Yup from 'yup';
type SalesOrderProductSchemaType = { type SalesOrderProductSchemaType = {
id?: number | undefined; id?: number | undefined;
warehouse_id?: number;
warehouse?: {
value: number;
label: string;
} | null;
kandang_id?: number; kandang_id?: number;
kandang?: { kandang?: {
value: number; value: number;
@@ -44,15 +49,22 @@ export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaTy
Yup.object({ Yup.object({
id: Yup.number(), id: Yup.number(),
vehicle_number: Yup.string().required('Nomor Kendaraan wajib diisi!'), vehicle_number: Yup.string().required('Nomor Kendaraan wajib diisi!'),
kandang: Yup.object({ warehouse: Yup.object({
value: Yup.number() value: Yup.number()
.min(1, 'Kandang wajib diisi!') .min(1, 'Gudang fisik wajib diisi!')
.required('Kandang wajib diisi!'), .required('Gudang fisik wajib diisi!'),
label: Yup.string().required('Kandang wajib diisi!'), label: Yup.string().required('Gudang fisik wajib diisi!'),
}).nullable(), }).nullable(),
kandang_id: Yup.number() warehouse_id: Yup.number()
.min(1, 'Kandang wajib diisi!') .min(1, 'Gudang fisik wajib diisi!')
.required('Kandang wajib diisi!'), .required('Gudang fisik wajib diisi!'),
kandang: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
})
.nullable()
.optional(),
kandang_id: Yup.number().optional(),
product_warehouse: Yup.object({ product_warehouse: Yup.object({
value: Yup.number() value: Yup.number()
.min(1, 'Produk wajib diisi!') .min(1, 'Produk wajib diisi!')
@@ -7,7 +7,7 @@ import {
} from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema'; } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
import { RefObject, useEffect, useMemo, useState } from 'react'; import { RefObject, useEffect, useMemo, useState } from 'react';
import { OptionType, useSelect } from '@/components/input/SelectInput'; import { OptionType, useSelect } from '@/components/input/SelectInput';
import { Kandang } from '@/types/api/master-data/kandang'; import { Warehouse } from '@/types/api/master-data/warehouse';
import { WarehouseApi } from '@/services/api/master-data'; import { WarehouseApi } from '@/services/api/master-data';
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
import { ProductWarehouseApi } from '@/services/api/inventory'; import { ProductWarehouseApi } from '@/services/api/inventory';
@@ -31,6 +31,7 @@ import {
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Dropdown from '@/components/Dropdown'; import Dropdown from '@/components/Dropdown';
import { handleMarketingCalculation } from '@/lib/marketing-calculation'; import { handleMarketingCalculation } from '@/lib/marketing-calculation';
import { getProductWarehouseOptionLabel } from '@/lib/product-warehouse';
const SalesOrderProductForm = ({ const SalesOrderProductForm = ({
initialValues, initialValues,
@@ -84,9 +85,13 @@ const SalesOrderProductForm = ({
enableReinitialize: true, enableReinitialize: true,
initialValues: { initialValues: {
vehicle_number: initialValues?.vehicle_number || '', vehicle_number: initialValues?.vehicle_number || '',
warehouse_id:
initialValues?.warehouse_id ?? initialValues?.kandang_id ?? undefined,
warehouse: initialValues?.warehouse ?? initialValues?.kandang ?? null,
kandang_id: initialValues?.kandang_id || undefined, kandang_id: initialValues?.kandang_id || undefined,
kandang: initialValues?.kandang || null, kandang: initialValues?.kandang || null,
product_warehouse: initialValues?.product_warehouse || null, product_warehouse: initialValues?.product_warehouse || null,
product_warehouse_data: initialValues?.product_warehouse_data || null,
product_warehouse_id: initialValues?.product_warehouse_id || undefined, product_warehouse_id: initialValues?.product_warehouse_id || undefined,
unit_price: initialValues?.unit_price || '', unit_price: initialValues?.unit_price || '',
total_weight: initialValues?.total_weight || '', total_weight: initialValues?.total_weight || '',
@@ -132,11 +137,11 @@ const SalesOrderProductForm = ({
// ===== Options ===== // ===== Options =====
const { const {
options: kandangSourceOptions, options: warehouseOptions,
isLoadingOptions: isLoadingKandangSourceOptions, isLoadingOptions: isLoadingWarehouseOptions,
setInputValue: setKandangInputValue, setInputValue: setWarehouseSearchValue,
loadMore: loadMoreKandang, loadMore: loadMoreWarehouses,
} = useSelect<Kandang>(WarehouseApi.basePath, 'id', 'name'); } = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name');
// Options Week dari minggu 1 - 22 // Options Week dari minggu 1 - 22
// const optionsWeek = useMemo(() => { // const optionsWeek = useMemo(() => {
@@ -147,7 +152,6 @@ const SalesOrderProductForm = ({
// }, []); // }, []);
const { const {
options: warehouseSourceOptions,
rawData: warehouseSourceRawData, rawData: warehouseSourceRawData,
isLoadingOptions: isLoadingWarehouseSourceOptions, isLoadingOptions: isLoadingWarehouseSourceOptions,
setInputValue: setWarehouseInputValue, setInputValue: setWarehouseInputValue,
@@ -156,32 +160,69 @@ const SalesOrderProductForm = ({
ProductWarehouseApi.basePath, ProductWarehouseApi.basePath,
'id', 'id',
'product.name', 'product.name',
'', 'search',
{ {
warehouse_id: formik.values.kandang_id?.toString() ?? '', limit: '100',
available_only: 'true',
warehouse_id: formik.values.warehouse_id?.toString() ?? '',
type: formik.values.marketing_type?.value.toLocaleUpperCase() ?? '', type: formik.values.marketing_type?.value.toLocaleUpperCase() ?? '',
} }
); );
const productOptionsFiltered = useMemo(() => { const productOptionsFiltered = useMemo(() => {
return warehouseSourceOptions.filter( if (!isResponseSuccess(warehouseSourceRawData)) {
(product) => return initialValues?.product_warehouse
!exisitingValues ? [initialValues.product_warehouse]
?.map((item) => item.product_warehouse_id) : [];
.includes(product.value) }
const selectedProductIds = new Set(
exisitingValues
?.filter((item) => item.id !== initialValues?.id)
.map((item) => Number(item.product_warehouse_id))
.filter((item) => item > 0) ?? []
); );
}, [warehouseSourceOptions, exisitingValues]);
const options = warehouseSourceRawData.data
.filter((item: ProductWarehouse) => !selectedProductIds.has(item.id))
.map((item: ProductWarehouse) => ({
value: item.id,
label: getProductWarehouseOptionLabel(item),
}));
if (
initialValues?.product_warehouse &&
initialValues?.product_warehouse_id
) {
const exists = options.find(
(option) =>
Number(option.value) === Number(initialValues.product_warehouse_id)
);
if (!exists) {
options.push(initialValues.product_warehouse);
}
}
return options;
}, [warehouseSourceRawData, exisitingValues, initialValues]);
// ===== Handler ===== // ===== Handler =====
const kandangChangeHandler = (val: OptionType | OptionType[] | null) => { const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('kandang', val as OptionType); const warehouse = (val as OptionType | null) ?? null;
formik.setFieldValue('kandang_id', (val as OptionType)?.value);
formik.setFieldValue('warehouse', warehouse);
formik.setFieldValue('warehouse_id', warehouse?.value);
formik.setFieldValue('kandang', warehouse);
formik.setFieldValue('kandang_id', warehouse?.value);
formik.setFieldValue('product_warehouse_id', null); formik.setFieldValue('product_warehouse_id', null);
formik.setFieldValue('product_warehouse', null); formik.setFieldValue('product_warehouse', null);
formik.setFieldValue('product_warehouse_data', null);
formik.setFieldValue('qty', ''); formik.setFieldValue('qty', '');
}; };
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => { const productWarehouseChangeHandler = (
val: OptionType | OptionType[] | null
) => {
formik.setFieldValue('product_warehouse', val as OptionType); formik.setFieldValue('product_warehouse', val as OptionType);
const newId = (val as OptionType)?.value; const newId = (val as OptionType)?.value;
formik.setFieldValue('product_warehouse_id', newId); formik.setFieldValue('product_warehouse_id', newId);
@@ -191,6 +232,7 @@ const SalesOrderProductForm = ({
(item: ProductWarehouse) => item.id === newId (item: ProductWarehouse) => item.id === newId
); );
setSelectedProductWarehouse(productWarehouse || null); setSelectedProductWarehouse(productWarehouse || null);
formik.setFieldValue('product_warehouse_data', productWarehouse || null);
formik.setFieldValue('qty', productWarehouse?.quantity); formik.setFieldValue('qty', productWarehouse?.quantity);
formik.setFieldValue('uom', productWarehouse?.product?.uom?.name || ''); formik.setFieldValue('uom', productWarehouse?.product?.uom?.name || '');
if ( if (
@@ -204,6 +246,8 @@ const SalesOrderProductForm = ({
} }
handleBlurField('qty'); handleBlurField('qty');
} else { } else {
setSelectedProductWarehouse(null);
formik.setFieldValue('product_warehouse_data', null);
formik.setFieldValue('qty', ''); formik.setFieldValue('qty', '');
formik.setFieldValue('uom', ''); formik.setFieldValue('uom', '');
formik.setFieldValue('week', null); formik.setFieldValue('week', null);
@@ -217,9 +261,12 @@ const SalesOrderProductForm = ({
formik.resetForm({ formik.resetForm({
values: { values: {
vehicle_number: '', vehicle_number: '',
warehouse_id: undefined,
warehouse: null,
kandang_id: undefined, kandang_id: undefined,
kandang: null, kandang: null,
product_warehouse: null, product_warehouse: null,
product_warehouse_data: null,
product_warehouse_id: undefined, product_warehouse_id: undefined,
unit_price: '', unit_price: '',
total_weight: '', total_weight: '',
@@ -310,6 +357,10 @@ const SalesOrderProductForm = ({
handleBlurField('week'); handleBlurField('week');
}, [formik.values.week]); }, [formik.values.week]);
useEffect(() => {
setSelectedProductWarehouse(initialValues?.product_warehouse_data || null);
}, [initialValues?.product_warehouse_data]);
return ( return (
<> <>
<form <form
@@ -348,22 +399,22 @@ const SalesOrderProductForm = ({
errorMessage={formik.errors.vehicle_number} errorMessage={formik.errors.vehicle_number}
/> />
{/* Gudang */} {/* Gudang Fisik */}
<SelectInputRadio <SelectInputRadio
required required
label='Gudang' label='Gudang Fisik'
options={kandangSourceOptions} options={warehouseOptions}
isLoading={isLoadingKandangSourceOptions} isLoading={isLoadingWarehouseOptions}
value={formik.values.kandang} value={formik.values.warehouse}
onChange={kandangChangeHandler} onChange={warehouseChangeHandler}
isClearable isClearable
onInputChange={setKandangInputValue} onInputChange={setWarehouseSearchValue}
onMenuScrollToBottom={loadMoreKandang} onMenuScrollToBottom={loadMoreWarehouses}
isError={ isError={
formik.touched.kandang_id && Boolean(formik.errors.kandang_id) formik.touched.warehouse_id && Boolean(formik.errors.warehouse_id)
} }
errorMessage={formik.errors.kandang_id} errorMessage={formik.errors.warehouse_id}
placeholder='Pilih Gudang' placeholder='Pilih Gudang Fisik'
/> />
{/* Kategori */} {/* Kategori */}
@@ -374,8 +425,9 @@ const SalesOrderProductForm = ({
value={formik.values.marketing_type} value={formik.values.marketing_type}
onChange={(val) => { onChange={(val) => {
formik.setFieldValue('marketing_type', val); formik.setFieldValue('marketing_type', val);
warehouseChangeHandler(null); productWarehouseChangeHandler(null);
formik.setFieldValue('product_warehouse', null); formik.setFieldValue('product_warehouse', null);
formik.setFieldValue('product_warehouse_data', null);
formik.setFieldValue('product_warehouse_id', null); formik.setFieldValue('product_warehouse_id', null);
formik.setFieldValue('convertion_unit', null); formik.setFieldValue('convertion_unit', null);
formik.setFieldValue('weight_per_convertion', null); formik.setFieldValue('weight_per_convertion', null);
@@ -392,18 +444,18 @@ const SalesOrderProductForm = ({
options={productOptionsFiltered} options={productOptionsFiltered}
isLoading={isLoadingWarehouseSourceOptions} isLoading={isLoadingWarehouseSourceOptions}
value={formik.values.product_warehouse} value={formik.values.product_warehouse}
onChange={warehouseChangeHandler} onChange={productWarehouseChangeHandler}
onInputChange={setWarehouseInputValue} onInputChange={setWarehouseInputValue}
onMenuScrollToBottom={loadMoreWarehouse} onMenuScrollToBottom={loadMoreWarehouse}
isClearable isClearable
placeholder={ placeholder={
formik.values.kandang_id formik.values.warehouse_id
? productOptionsFiltered.length == 0 ? productOptionsFiltered.length == 0
? 'Tidak ada produk yang tersedia' ? 'Tidak ada produk yang tersedia'
: 'Pilih produk' : 'Pilih produk'
: 'Pilih Kandang Terlebih Dahulu' : 'Pilih Gudang Fisik Terlebih Dahulu'
} }
isDisabled={!formik.values.kandang_id} isDisabled={!formik.values.warehouse_id}
isError={ isError={
formik.touched.product_warehouse_id && formik.touched.product_warehouse_id &&
Boolean(formik.errors.product_warehouse_id) Boolean(formik.errors.product_warehouse_id)
@@ -706,7 +758,7 @@ const SalesOrderProductForm = ({
formik.touched.unit_price && Boolean(formik.errors.unit_price) formik.touched.unit_price && Boolean(formik.errors.unit_price)
} }
errorMessage={formik.errors.unit_price} errorMessage={formik.errors.unit_price}
placeholder='Masukan Harga Satuan' placeholder='Masukan Harga Satuan...'
/> />
)} )}
@@ -104,9 +104,10 @@ const DeliveryOrderProductTable = ({
</tr> </tr>
<> <>
<tr> <tr>
<td className='text-sm px-4 py-3'>Gudang</td> <td className='text-sm px-4 py-3'>Gudang Fisik</td>
<td className='text-sm px-4 py-3'> <td className='text-sm px-4 py-3'>
{doItem?.warehouse?.name || {doItem?.warehouse?.name ||
item.marketing_product?.warehouse?.label ||
item.marketing_product?.product_warehouse_data?.warehouse?.name} item.marketing_product?.product_warehouse_data?.warehouse?.name}
</td> </td>
</tr> </tr>
@@ -235,7 +236,7 @@ const DeliveryOrderProductTable = ({
</tr> </tr>
<> <>
<tr> <tr>
<td className='text-sm px-4 py-3'>Gudang</td> <td className='text-sm px-4 py-3'>Gudang Fisik</td>
<td className='text-sm px-4 py-3'>{item.warehouse?.name}</td> <td className='text-sm px-4 py-3'>{item.warehouse?.name}</td>
</tr> </tr>
<tr> <tr>
@@ -73,8 +73,10 @@ const SalesOrderProductTable = ({
<td className='text-sm px-4 py-3'>{item.vehicle_number}</td> <td className='text-sm px-4 py-3'>{item.vehicle_number}</td>
</tr> </tr>
<tr> <tr>
<td className='text-sm px-4 py-3'>Gudang</td> <td className='text-sm px-4 py-3'>Gudang Fisik</td>
<td className='text-sm px-4 py-3'>{item.kandang?.label}</td> <td className='text-sm px-4 py-3'>
{item.warehouse?.label ?? item.kandang?.label}
</td>
</tr> </tr>
<tr> <tr>
<td className='text-sm px-4 py-3'>Kategori</td> <td className='text-sm px-4 py-3'>Kategori</td>
@@ -34,6 +34,7 @@ type RecordingGrowingFormSchemaType = {
}[]; }[];
depletions: { depletions: {
product_warehouse_id?: number; product_warehouse_id?: number;
source_product_warehouse_id?: number;
qty?: number | string; qty?: number | string;
}[]; }[];
}; };
@@ -53,6 +54,7 @@ export type StockSchema = {
export type DepletionSchema = { export type DepletionSchema = {
product_warehouse_id?: number; product_warehouse_id?: number;
source_product_warehouse_id?: number;
qty?: number | string; qty?: number | string;
}; };
@@ -69,7 +71,7 @@ const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
.typeError('Produk harus berupa angka!'), .typeError('Produk harus berupa angka!'),
qty: Yup.number() qty: Yup.number()
.required('Jumlah penggunaan wajib diisi!') .required('Jumlah penggunaan wajib diisi!')
.min(1, 'Jumlah penggunaan tidak boleh 0!') .moreThan(0, 'Jumlah penggunaan harus lebih dari 0!')
.typeError('Jumlah penggunaan harus berupa angka!'), .typeError('Jumlah penggunaan harus berupa angka!'),
}); });
@@ -77,6 +79,9 @@ const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({
product_warehouse_id: Yup.number() product_warehouse_id: Yup.number()
.optional() .optional()
.typeError('Depletions harus berupa angka!'), .typeError('Depletions harus berupa angka!'),
source_product_warehouse_id: Yup.number()
.optional()
.typeError('Gudang sumber harus berupa angka!'),
qty: Yup.number() qty: Yup.number()
.optional() .optional()
.typeError('Jumlah depletions harus berupa angka!'), .typeError('Jumlah depletions harus berupa angka!'),
@@ -259,6 +264,7 @@ export const getRecordingGrowingFormInitialValues = (
depletion: NonNullable<CreateGrowingRecordingPayload['depletions']>[0] depletion: NonNullable<CreateGrowingRecordingPayload['depletions']>[0]
) => ({ ) => ({
product_warehouse_id: depletion.product_warehouse_id, product_warehouse_id: depletion.product_warehouse_id,
source_product_warehouse_id: depletion.source_product_warehouse_id,
qty: depletion.qty, qty: depletion.qty,
}) })
) ?? [ ) ?? [
@@ -4,7 +4,7 @@ import { useMemo, useState, useEffect, useCallback } 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 useSWR, { useSWRConfig } from 'swr';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Button from '@/components/Button'; import Button from '@/components/Button';
@@ -71,6 +71,10 @@ import {
import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
import { formatDate, formatNumber, cn } from '@/lib/helper'; import { formatDate, formatNumber, cn } from '@/lib/helper';
import {
getProductWarehouseOptionLabel,
isProductWarehouseSelectableForKandang,
} from '@/lib/product-warehouse';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import ApprovalSteps, { import ApprovalSteps, {
useApprovalSteps, useApprovalSteps,
@@ -179,6 +183,7 @@ const productionStandardColumns: ColumnDef<StandardDetails>[] = [
const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
// ===== HOOKS & ROUTER ===== // ===== HOOKS & ROUTER =====
const router = useRouter(); const router = useRouter();
const { mutate } = useSWRConfig();
// ===== STATE MANAGEMENT ===== // ===== STATE MANAGEMENT =====
const [selectedRecordDate, setSelectedRecordDate] = useState<string>( const [selectedRecordDate, setSelectedRecordDate] = useState<string>(
@@ -202,15 +207,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
useState<string>(''); useState<string>('');
const [stockProductsLocationId, setStockProductsLocationId] = const [stockProductsLocationId, setStockProductsLocationId] =
useState<string>(''); useState<string>('');
const [stockProductsKandangId, setStockProductsKandangId] =
useState<string>('');
const [depletionProductsLocationId, setDepletionProductsLocationId] = const [depletionProductsLocationId, setDepletionProductsLocationId] =
useState<string>(''); useState<string>('');
const [depletionProductsKandangId, setDepletionProductsKandangId] = // const [eggProductsLocationId, setEggProductsLocationId] =
useState<string>(''); // useState<string>('');
const [eggProductsLocationId, setEggProductsLocationId] = const [knownProductWarehouses, setKnownProductWarehouses] = useState<
useState<string>(''); Record<number, ProductWarehouse>
const [eggProductsKandangId, setEggProductsKandangId] = useState<string>(''); >({});
const [isApproveLoading, setIsApproveLoading] = useState(false); const [isApproveLoading, setIsApproveLoading] = useState(false);
const [isRejectLoading, setIsRejectLoading] = useState(false); const [isRejectLoading, setIsRejectLoading] = useState(false);
@@ -317,11 +320,12 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
setRecordingFormErrorMessage(res.message); setRecordingFormErrorMessage(res.message);
return; return;
} }
await mutate(['recording-detail', recordingId.toString()]);
toast.success(res?.message as string); toast.success(res?.message as string);
router.refresh(); router.refresh();
router.push('/production/recording'); router.push('/production/recording');
}, },
[router] [mutate, router]
); );
const deleteRecordingClickHandler = useCallback(() => { const deleteRecordingClickHandler = useCallback(() => {
@@ -448,22 +452,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
? projectFlockKandangDetailData.data ? projectFlockKandangDetailData.data
: undefined; : undefined;
const selectedProjectFlockKandangId = useMemo(() => { const selectedKandangId = useMemo(() => {
if (type === 'add') { if (!selectedKandang?.value) {
return projectFlockKandangLookup?.project_flock_kandang_id ?? null; return null;
} }
return ( return Number(selectedKandang.value);
projectFlockKandangDetail?.id ?? }, [selectedKandang]);
initialValues?.project_flock?.project_flock_kandang_id ??
null
);
}, [
type,
projectFlockKandangLookup,
projectFlockKandangDetail,
initialValues,
]);
// ===== TRANSITION RESTRICTION LOGIC ===== // ===== TRANSITION RESTRICTION LOGIC =====
const isTransitionPeriod = useMemo(() => { const isTransitionPeriod = useMemo(() => {
@@ -512,6 +507,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
?.filter((d) => d.product_warehouse_id && d.qty) ?.filter((d) => d.product_warehouse_id && d.qty)
.map((depletion) => ({ .map((depletion) => ({
product_warehouse_id: depletion.product_warehouse_id!, product_warehouse_id: depletion.product_warehouse_id!,
...(depletion.source_product_warehouse_id && {
source_product_warehouse_id:
depletion.source_product_warehouse_id,
}),
qty: Number(depletion.qty) || 0, qty: Number(depletion.qty) || 0,
})) }))
: []; : [];
@@ -541,6 +540,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
?.filter((d) => d.product_warehouse_id && d.qty) ?.filter((d) => d.product_warehouse_id && d.qty)
.map((depletion) => ({ .map((depletion) => ({
product_warehouse_id: depletion.product_warehouse_id!, product_warehouse_id: depletion.product_warehouse_id!,
...(depletion.source_product_warehouse_id && {
source_product_warehouse_id: depletion.source_product_warehouse_id,
}),
qty: Number(depletion.qty) || 0, qty: Number(depletion.qty) || 0,
})); }));
@@ -602,24 +604,27 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
}, []); }, []);
const { const {
options: stockProductOptions,
setInputValue: setStockProductInputValue, setInputValue: setStockProductInputValue,
rawData: stockProducts, rawData: stockProducts,
isLoadingOptions: isLoadingStockProducts, isLoadingOptions: isLoadingStockProducts,
loadMore: loadMoreStockProducts, loadMore: loadMoreStockProducts,
} = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', { } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', {
flags: 'PAKAN,OVK', flags: 'PAKAN,OVK',
limit: '100',
available_only: 'true',
location_id: stockProductsLocationId, location_id: stockProductsLocationId,
kandang_id: stockProductsKandangId, ...(selectedKandangId ? { kandang_id: selectedKandangId.toString() } : {}),
}); });
const { const {
rawData: depletionProductsData, rawData: depletionProductsData,
isLoadingOptions: isLoadingDepletionProducts, isLoadingOptions: isLoadingDepletionProducts,
loadMore: loadMoreDepletionProducts, loadMore: loadMoreDepletionProducts,
} = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', '', { } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', {
limit: '100',
available_only: 'false',
location_id: depletionProductsLocationId, location_id: depletionProductsLocationId,
kandang_id: depletionProductsKandangId, ...(selectedKandangId ? { kandang_id: selectedKandangId.toString() } : {}),
type: 'AYAM', type: 'AYAM',
}); });
@@ -684,9 +689,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
isLoadingOptions: isLoadingEggProducts, isLoadingOptions: isLoadingEggProducts,
loadMore: loadMoreEggProducts, loadMore: loadMoreEggProducts,
} = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', { } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', {
limit: '100',
available_only: 'false',
type: 'TELUR', type: 'TELUR',
location_id: eggProductsLocationId, // location_id: eggProductsLocationId,
kandang_id: eggProductsKandangId, ...(selectedKandangId ? { kandang_id: selectedKandangId.toString() } : {}),
}); });
const approvedProjectFlockKandangsUrl = useMemo(() => { const approvedProjectFlockKandangsUrl = useMemo(() => {
@@ -934,39 +941,134 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
projectFlockKandangDetail, projectFlockKandangDetail,
]); ]);
const isProductWarehouseBelongsToSelectedProjectFlockKandang = useCallback( const appendProductWarehouseOption = useCallback(
(productWarehouse: ProductWarehouse) => { (options: OptionType[], productWarehouse?: ProductWarehouse | null) => {
if (!selectedProjectFlockKandangId) return false; if (!productWarehouse) {
return;
return (
productWarehouse.project_flock_kandang?.id ===
selectedProjectFlockKandangId
);
},
[selectedProjectFlockKandangId]
);
const scopedStockProductIds = useMemo(() => {
if (!isResponseSuccess(stockProducts) || !selectedProjectFlockKandangId) {
return new Set<number>();
} }
const data = stockProducts.data as unknown as ProductWarehouse[]; const existingOption = options.find(
return new Set( (opt) => Number(opt.value) === productWarehouse.id
data
.filter(isProductWarehouseBelongsToSelectedProjectFlockKandang)
.map((product) => product.id)
); );
if (!existingOption) {
options.push({
value: productWarehouse.id,
label: getProductWarehouseOptionLabel(productWarehouse),
});
}
},
[]
);
const mergeKnownProductWarehouses = useCallback(
(items: Array<ProductWarehouse | null | undefined>) => {
if (items.length === 0) {
return;
}
setKnownProductWarehouses((prev) => {
const next = { ...prev };
let changed = false;
items.forEach((item) => {
if (!item?.id) {
return;
}
const existing = next[item.id];
if (existing !== item) {
// Check deep equality to avoid triggering state changes
// when identical data comes from different sources (e.g. initialValues vs SWR)
if (
!existing ||
JSON.stringify(existing) !== JSON.stringify(item)
) {
next[item.id] = item;
changed = true;
}
}
});
return changed ? next : prev;
});
},
[]
);
useEffect(() => {
const items: Array<ProductWarehouse | null | undefined> = [];
if (isResponseSuccess(stockProducts)) {
items.push(
...((stockProducts.data as unknown as ProductWarehouse[]) ?? [])
);
}
if (isResponseSuccess(depletionProductsData)) {
items.push(
...((depletionProductsData.data as unknown as ProductWarehouse[]) ?? [])
);
}
if (isResponseSuccess(eggProductsData)) {
items.push(
...((eggProductsData.data as unknown as ProductWarehouse[]) ?? [])
);
}
initialValues?.stocks?.forEach((stock) => {
items.push(
(stock.product_warehouse as ProductWarehouse | undefined) ?? null
);
});
initialValues?.depletions?.forEach((depletion) => {
items.push(
(depletion.product_warehouse as ProductWarehouse | undefined) ?? null
);
});
initialValues?.eggs?.forEach((egg) => {
items.push(
(egg.product_warehouse as ProductWarehouse | undefined) ?? null
);
});
mergeKnownProductWarehouses(items);
}, [ }, [
stockProducts, stockProducts,
selectedProjectFlockKandangId, depletionProductsData,
isProductWarehouseBelongsToSelectedProjectFlockKandang, eggProductsData,
initialValues,
mergeKnownProductWarehouses,
]); ]);
const getKnownProductWarehouse = useCallback(
(productWarehouseId: number) =>
knownProductWarehouses[productWarehouseId] ?? null,
[knownProductWarehouses]
);
const buildProductWarehouseOptions = useCallback(
(productWarehouses: ProductWarehouse[]) =>
productWarehouses
.filter((productWarehouse) =>
isProductWarehouseSelectableForKandang(
productWarehouse,
selectedKandangId
)
)
.map((productWarehouse) => ({
value: productWarehouse.id,
label: getProductWarehouseOptionLabel(productWarehouse),
}))
.sort((a, b) => a.label.localeCompare(b.label)),
[selectedKandangId]
);
const unifiedStockProducts = useMemo(() => { const unifiedStockProducts = useMemo(() => {
const options = selectedProjectFlockKandangId const options = isResponseSuccess(stockProducts)
? stockProductOptions.filter((option) => ? buildProductWarehouseOptions(
scopedStockProductIds.has(Number(option.value)) stockProducts.data as unknown as ProductWarehouse[]
) )
: []; : [];
@@ -977,113 +1079,61 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
type !== 'add' type !== 'add'
) { ) {
initialValues.stocks?.forEach((stock) => { initialValues.stocks?.forEach((stock) => {
if (stock.product_warehouse && stock.product_warehouse.product) { appendProductWarehouseOption(options, stock.product_warehouse);
const existingOption = options.find(
(opt) => opt.value === stock.product_warehouse_id
);
if (!existingOption) {
options.push({
value: stock.product_warehouse_id,
label: stock.product_warehouse.product.name,
});
}
}
}); });
} }
return options; return options;
}, [ }, [
stockProductOptions, stockProducts,
buildProductWarehouseOptions,
initialValues, initialValues,
type, type,
selectedProjectFlockKandangId, appendProductWarehouseOption,
scopedStockProductIds,
]); ]);
const depletionProducts = useMemo(() => { const depletionProducts = useMemo(() => {
const options: OptionType[] = []; const options = isResponseSuccess(depletionProductsData)
? buildProductWarehouseOptions(
if ( depletionProductsData.data as unknown as ProductWarehouse[]
isResponseSuccess(depletionProductsData) && )
selectedProjectFlockKandangId : [];
) {
const data = depletionProductsData.data as unknown as ProductWarehouse[];
data
.filter(isProductWarehouseBelongsToSelectedProjectFlockKandang)
.forEach((product) => {
options.push({
value: product.id,
label: product.product.name,
});
});
}
if (initialValues && initialValues.depletions && type !== 'add') { if (initialValues && initialValues.depletions && type !== 'add') {
initialValues.depletions.forEach((depletion) => { initialValues.depletions.forEach((depletion) => {
if ( appendProductWarehouseOption(options, depletion.product_warehouse);
depletion.product_warehouse &&
depletion.product_warehouse.product
) {
const existingOption = options.find(
(opt) => opt.value === depletion.product_warehouse_id
);
if (!existingOption) {
options.push({
value: depletion.product_warehouse_id,
label: depletion.product_warehouse.product.name,
});
}
}
}); });
} }
return options; return options;
}, [ }, [
depletionProductsData, depletionProductsData,
buildProductWarehouseOptions,
initialValues, initialValues,
type, type,
selectedProjectFlockKandangId, appendProductWarehouseOption,
isProductWarehouseBelongsToSelectedProjectFlockKandang,
]); ]);
const eggProducts = useMemo(() => { const eggProducts = useMemo(() => {
const options: OptionType[] = []; const options = isResponseSuccess(eggProductsData)
? buildProductWarehouseOptions(
if (isResponseSuccess(eggProductsData) && selectedProjectFlockKandangId) { eggProductsData.data as unknown as ProductWarehouse[]
const data = eggProductsData.data as unknown as ProductWarehouse[]; )
data : [];
.filter(isProductWarehouseBelongsToSelectedProjectFlockKandang)
.forEach((product) => {
options.push({
value: product.id,
label: product.product.name,
});
});
}
if (initialValues && initialValues.eggs && type !== 'add') { if (initialValues && initialValues.eggs && type !== 'add') {
initialValues.eggs.forEach((egg) => { initialValues.eggs.forEach((egg) => {
if (egg.product_warehouse && egg.product_warehouse.product) { appendProductWarehouseOption(options, egg.product_warehouse);
const existingOption = options.find(
(opt) => opt.value === egg.product_warehouse_id
);
if (!existingOption) {
options.push({
value: egg.product_warehouse_id,
label: egg.product_warehouse.product.name,
});
}
}
}); });
} }
return options; return options;
}, [ }, [
eggProductsData, eggProductsData,
buildProductWarehouseOptions,
initialValues, initialValues,
type, type,
selectedProjectFlockKandangId, appendProductWarehouseOption,
isProductWarehouseBelongsToSelectedProjectFlockKandang,
]); ]);
// ===== FORMIK SETUP ===== // ===== FORMIK SETUP =====
@@ -1291,12 +1341,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const getAvailableStock = useCallback( const getAvailableStock = useCallback(
(productWarehouseId: number) => { (productWarehouseId: number) => {
if ((type as 'add' | 'edit' | 'detail') === 'detail') return 0; if ((type as 'add' | 'edit' | 'detail') === 'detail') return 0;
if (!isResponseSuccess(stockProducts)) return 0; const productWarehouse = getKnownProductWarehouse(productWarehouseId);
const data = stockProducts.data as unknown as ProductWarehouse[];
const productWarehouse = data.find((pw) => pw.id === productWarehouseId);
return productWarehouse?.quantity ?? 0; return productWarehouse?.quantity ?? 0;
}, },
[stockProducts, type] [getKnownProductWarehouse, type]
); );
const getStockUsageError = useCallback( const getStockUsageError = useCallback(
@@ -1390,10 +1438,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const getProductFlagBadgeAdornment = useCallback( const getProductFlagBadgeAdornment = useCallback(
(productWarehouseId: number) => { (productWarehouseId: number) => {
if (!isResponseSuccess(stockProducts)) return null; const productWarehouse = getKnownProductWarehouse(productWarehouseId);
const data = stockProducts.data as unknown as ProductWarehouse[];
const productWarehouse = data.find((pw) => pw.id === productWarehouseId);
if (!productWarehouse) return null; if (!productWarehouse) return null;
const hasPakanFlag = productWarehouse.product.flags?.includes('PAKAN'); const hasPakanFlag = productWarehouse.product.flags?.includes('PAKAN');
@@ -1409,7 +1454,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
return null; return null;
}, },
[stockProducts] [getKnownProductWarehouse]
); );
const getProductUomSuffix = useCallback( const getProductUomSuffix = useCallback(
@@ -1434,23 +1479,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
} }
} }
let rawData; const productWarehouse = getKnownProductWarehouse(productWarehouseId);
if (dataSource === 'stock') {
rawData = stockProducts;
} else if (dataSource === 'depletion') {
rawData = depletionProductsData;
} else if (dataSource === 'egg') {
rawData = eggProductsData;
}
if (!isResponseSuccess(rawData)) return null;
const data = rawData.data as unknown as ProductWarehouse[];
const productWarehouse = data.find((pw) => pw.id === productWarehouseId);
return productWarehouse?.product.uom.name || null; return productWarehouse?.product.uom.name || null;
}, },
[stockProducts, depletionProductsData, eggProductsData, initialValues, type] [getKnownProductWarehouse, initialValues, type]
); );
const getAvailableStockProductOptions = useCallback( const getAvailableStockProductOptions = useCallback(
@@ -1566,10 +1599,52 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
formik.setFieldValue('location_id', locationId); formik.setFieldValue('location_id', locationId);
setSelectedLocation(location); setSelectedLocation(location);
formik.setFieldTouched('project_flock', false, false);
formik.setFieldValue('project_flock', null);
formik.setFieldTouched('project_flock_id', false, false);
formik.setFieldValue('project_flock_id', 0);
formik.setFieldTouched('kandang', false, false);
formik.setFieldValue('kandang', null);
formik.setFieldTouched('kandang_id', false, false);
formik.setFieldValue('kandang_id', 0);
formik.setFieldTouched('project_flock_kandang', false, false);
formik.setFieldValue('project_flock_kandang', null);
formik.setFieldTouched('project_flock_kandang_id', false, false);
formik.setFieldValue('project_flock_kandang_id', 0);
formik.setFieldTouched('stocks', false, false);
formik.setFieldValue('stocks', [
{
product_warehouse_id: 0,
qty: '',
},
]);
formik.setFieldTouched('depletions', false, false);
formik.setFieldValue('depletions', [
{
product_warehouse_id: 0,
qty: '',
},
]);
if (isLayingCategory) {
formik.setFieldTouched('eggs', false, false);
formik.setFieldValue('eggs', [
{
product_warehouse_id: 0,
qty: '',
weight: '',
},
]);
}
setSelectedStocks([]);
setSelectedDepletions([]);
setSelectedEggs([]);
setSelectedProjectFlock(null); setSelectedProjectFlock(null);
setSelectedKandang(null); setSelectedKandang(null);
setProductionStandards(null); setProductionStandards(null);
setNextDayRecording(null); setNextDayRecording(null);
setStockProductsLocationId('');
setDepletionProductsLocationId('');
// setEggProductsLocationId('');
if (duplicateErrorShown) { if (duplicateErrorShown) {
toast.dismiss(); toast.dismiss();
setDuplicateErrorShown(false); setDuplicateErrorShown(false);
@@ -1592,10 +1667,48 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
formik.setFieldTouched('project_flock_id', true); formik.setFieldTouched('project_flock_id', true);
formik.setFieldValue('project_flock_id', projectFlockId); formik.setFieldValue('project_flock_id', projectFlockId);
formik.setFieldTouched('kandang', false, false);
formik.setFieldValue('kandang', null);
formik.setFieldTouched('kandang_id', false, false);
formik.setFieldValue('kandang_id', 0);
formik.setFieldTouched('project_flock_kandang', false, false);
formik.setFieldValue('project_flock_kandang', null);
formik.setFieldTouched('project_flock_kandang_id', false, false);
formik.setFieldValue('project_flock_kandang_id', 0);
formik.setFieldTouched('stocks', false, false);
formik.setFieldValue('stocks', [
{
product_warehouse_id: 0,
qty: '',
},
]);
formik.setFieldTouched('depletions', false, false);
formik.setFieldValue('depletions', [
{
product_warehouse_id: 0,
qty: '',
},
]);
if (isLayingCategory) {
formik.setFieldTouched('eggs', false, false);
formik.setFieldValue('eggs', [
{
product_warehouse_id: 0,
qty: '',
weight: '',
},
]);
}
setSelectedStocks([]);
setSelectedDepletions([]);
setSelectedEggs([]);
setSelectedProjectFlock(projectFlock); setSelectedProjectFlock(projectFlock);
setSelectedKandang(null); setSelectedKandang(null);
setProductionStandards(null); setProductionStandards(null);
setNextDayRecording(null); setNextDayRecording(null);
setStockProductsLocationId('');
setDepletionProductsLocationId('');
// setEggProductsLocationId('');
if (duplicateErrorShown) { if (duplicateErrorShown) {
toast.dismiss(); toast.dismiss();
setDuplicateErrorShown(false); setDuplicateErrorShown(false);
@@ -1615,6 +1728,33 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
formik.setFieldTouched('kandang_id', true); formik.setFieldTouched('kandang_id', true);
formik.setFieldValue('kandang_id', kandangId); formik.setFieldValue('kandang_id', kandangId);
formik.setFieldTouched('stocks', false, false);
formik.setFieldValue('stocks', [
{
product_warehouse_id: 0,
qty: '',
},
]);
formik.setFieldTouched('depletions', false, false);
formik.setFieldValue('depletions', [
{
product_warehouse_id: 0,
qty: '',
},
]);
if (isLayingCategory) {
formik.setFieldTouched('eggs', false, false);
formik.setFieldValue('eggs', [
{
product_warehouse_id: 0,
qty: '',
weight: '',
},
]);
}
setSelectedStocks([]);
setSelectedDepletions([]);
setSelectedEggs([]);
setSelectedKandang(kandang); setSelectedKandang(kandang);
setProductionStandards(null); setProductionStandards(null);
setNextDayRecording(null); setNextDayRecording(null);
@@ -1628,18 +1768,12 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
} }
if (selectedLocation && kandang) { if (selectedLocation && kandang) {
setStockProductsLocationId(selectedLocation.value.toString()); setStockProductsLocationId(selectedLocation.value.toString());
setStockProductsKandangId(kandang.value.toString());
setDepletionProductsLocationId(selectedLocation.value.toString()); setDepletionProductsLocationId(selectedLocation.value.toString());
setDepletionProductsKandangId(kandang.value.toString()); // setEggProductsLocationId(selectedLocation.value.toString());
setEggProductsLocationId(selectedLocation.value.toString());
setEggProductsKandangId(kandang.value.toString());
} else { } else {
setStockProductsLocationId(''); setStockProductsLocationId('');
setStockProductsKandangId('');
setDepletionProductsLocationId(''); setDepletionProductsLocationId('');
setDepletionProductsKandangId(''); // setEggProductsLocationId('');
setEggProductsLocationId('');
setEggProductsKandangId('');
} }
formik.setFieldTouched('project_flock_kandang', true); formik.setFieldTouched('project_flock_kandang', true);
formik.setFieldTouched('project_flock_kandang_id', true); formik.setFieldTouched('project_flock_kandang_id', true);
@@ -1746,11 +1880,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
setSelectedKandang(kandangOption); setSelectedKandang(kandangOption);
setStockProductsLocationId(location.id.toString()); setStockProductsLocationId(location.id.toString());
setStockProductsKandangId(kandang.id.toString());
setDepletionProductsLocationId(location.id.toString()); setDepletionProductsLocationId(location.id.toString());
setDepletionProductsKandangId(kandang.id.toString()); // setEggProductsLocationId(location.id.toString());
setEggProductsLocationId(location.id.toString());
setEggProductsKandangId(kandang.id.toString());
if ( if (
formik.values.project_flock_kandang_id !== formik.values.project_flock_kandang_id !==
@@ -2724,7 +2855,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
value={stock.qty ?? ''} value={stock.qty ?? ''}
onChange={handleStockUsageQtyChangeWrapper(idx)} onChange={handleStockUsageQtyChangeWrapper(idx)}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
decimalScale={0} decimalScale={3}
allowNegative={false} allowNegative={false}
thousandSeparator=',' thousandSeparator=','
decimalSeparator='.' decimalSeparator='.'
@@ -478,6 +478,7 @@ const PurchaseTable = () => {
'filter_by', 'filter_by',
'sort_by', 'sort_by',
]} ]}
fieldGroups={[['startDate', 'endDate']]}
onClick={filterModal.openModal} onClick={filterModal.openModal}
className='px-3 py-2.5' className='px-3 py-2.5'
/> />
@@ -539,6 +540,7 @@ const PurchaseTable = () => {
</div> </div>
{/* ===== MODAL COMPONENTS ===== */} {/* ===== MODAL COMPONENTS ===== */}
<PurchaseFilterModal <PurchaseFilterModal
ref={filterModal.ref} ref={filterModal.ref}
onSubmit={filterSubmitHandler} onSubmit={filterSubmitHandler}
@@ -81,7 +81,7 @@ const getTableColumns = (
}, },
{ {
key: 'warehouse', key: 'warehouse',
header: 'Gudang', header: 'Gudang Fisik',
flex: 1.2, flex: 1.2,
align: 'left', align: 'left',
cell: ({ row }) => row.warehouse?.name ?? '-', cell: ({ row }) => row.warehouse?.name ?? '-',
@@ -30,7 +30,7 @@ export const generateDailyMarketingExcel = async (
{ header: 'Tanggal Jual', key: 'soDate', width: 15 }, { header: 'Tanggal Jual', key: 'soDate', width: 15 },
{ header: 'Tanggal Realisasi', key: 'realizationDate', width: 18 }, { header: 'Tanggal Realisasi', key: 'realizationDate', width: 18 },
{ header: 'Aging', key: 'aging', width: 10 }, { header: 'Aging', key: 'aging', width: 10 },
{ header: 'Gudang', key: 'warehouse', width: 25 }, { header: 'Gudang Fisik', key: 'warehouse', width: 25 },
{ header: 'Pelanggan', key: 'customer', width: 25 }, { header: 'Pelanggan', key: 'customer', width: 25 },
{ header: 'No. DO', key: 'doNumber', width: 15 }, { header: 'No. DO', key: 'doNumber', width: 15 },
{ header: 'Sales/Marketing', key: 'sales', width: 20 }, { header: 'Sales/Marketing', key: 'sales', width: 20 },
@@ -97,7 +97,7 @@ export const generateDailyMarketingExcel = async (
}); });
} }
worksheet.columns.forEach((column) => { worksheet.columns.forEach((column: { width?: number }) => {
if (column.width && column.width < 10) { if (column.width && column.width < 10) {
column.width = 10; column.width = 10;
} }
@@ -508,7 +508,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
}, },
{ {
id: 'warehouse', id: 'warehouse',
header: 'Gudang', header: 'Gudang Fisik',
accessorKey: 'warehouse', accessorKey: 'warehouse',
cell: ({ row }) => row.original.warehouse.name, cell: ({ row }) => row.original.warehouse.name,
footer: () => <div className='font-semibold text-gray-900'>-</div>, footer: () => <div className='font-semibold text-gray-900'>-</div>,
@@ -858,8 +858,8 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
{/* Warehouse Filter */} {/* Warehouse Filter */}
<SelectInput <SelectInput
label='Gudang' label='Gudang Fisik'
placeholder='Pilih Gudang' placeholder='Pilih Gudang Fisik'
options={warehouseOptions} options={warehouseOptions}
isLoading={isLoadingWarehouses} isLoading={isLoadingWarehouses}
value={warehouseValue} value={warehouseValue}
+61
View File
@@ -0,0 +1,61 @@
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
import { Warehouse } from '@/types/api/master-data/warehouse';
export const getWarehouseScopeLabel = (
warehouse?: Warehouse | null
): string => {
if (!warehouse) {
return 'Gudang';
}
if (warehouse.type === 'KANDANG') {
return warehouse.kandang?.name
? `Kandang ${warehouse.kandang.name}`
: 'Gudang Kandang';
}
if (warehouse.type === 'LOKASI') {
return 'Gudang Farm';
}
return 'Gudang Area';
};
export const getProductWarehouseOptionLabel = (
productWarehouse?: ProductWarehouse | null
): string => {
if (!productWarehouse) {
return '';
}
const productName = productWarehouse.product?.name || 'Produk';
const warehouseName = productWarehouse.warehouse?.name || 'Gudang';
const warehouseScope = getWarehouseScopeLabel(productWarehouse.warehouse);
return `${productName}${warehouseName} (${warehouseScope})`;
};
export const isProductWarehouseSelectableForKandang = (
productWarehouse: ProductWarehouse,
kandangId?: number | null
): boolean => {
const warehouse = productWarehouse.warehouse;
if (!warehouse) {
return false;
}
if (warehouse.type === 'LOKASI') {
return true;
}
if (warehouse.type === 'KANDANG') {
return (
Boolean(kandangId) &&
(warehouse.kandang?.id === kandangId ||
productWarehouse.project_flock_kandang?.kandang_id === kandangId)
);
}
return false;
};
+4 -3
View File
@@ -5,7 +5,6 @@ import {
CreatedUser, CreatedUser,
} from '@/types/api/api-general'; } from '@/types/api/api-general';
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
import { Kandang } from '@/types/api/master-data/kandang';
import { Warehouse } from '@/types/api/master-data/warehouse'; import { Warehouse } from '@/types/api/master-data/warehouse';
/** /**
@@ -110,7 +109,8 @@ export type BaseCreateMarketingPayload = {
export type BaseCreateMarketingProductPayload = { export type BaseCreateMarketingProductPayload = {
vehicle_number: string; vehicle_number: string;
kandang_id: string | number | undefined; warehouse_id?: string | number | undefined;
kandang_id?: string | number | undefined;
product_warehouse_id: string | number | undefined; product_warehouse_id: string | number | undefined;
unit_price: string | number | undefined; unit_price: string | number | undefined;
total_weight: string | number | undefined; total_weight: string | number | undefined;
@@ -136,7 +136,8 @@ export type CreateSalesOrderPayload = BaseCreateMarketingPayload & {
export type CreateSalesOrderProductPayload = export type CreateSalesOrderProductPayload =
BaseCreateMarketingProductPayload & { BaseCreateMarketingProductPayload & {
id?: number; id?: number;
kandang?: Kandang | undefined; warehouse?: Warehouse | undefined;
kandang?: Warehouse | undefined;
product_warehouse?: ProductWarehouse | undefined; product_warehouse?: ProductWarehouse | undefined;
}; };
+2
View File
@@ -55,6 +55,7 @@ export type BaseRecording = {
export type RecordingDepletion = { export type RecordingDepletion = {
product_warehouse_id: number; product_warehouse_id: number;
source_product_warehouse_id?: number;
qty: number; qty: number;
product_warehouse: ProductWarehouse; product_warehouse: ProductWarehouse;
}; };
@@ -114,6 +115,7 @@ export type CreateGrowingRecordingPayload = {
}[]; }[];
depletions?: { depletions?: {
product_warehouse_id?: number; product_warehouse_id?: number;
source_product_warehouse_id?: number;
qty?: number; qty?: number;
}[]; }[];
}; };