Compare commits

..

18 Commits

Author SHA1 Message Date
rstubryan 584b495e4b refactor(FE): Fix missing dependency in useEffect hook 2026-03-07 12:01:46 +07:00
rstubryan c8e76a8558 refactor(FE): Refactor ExpensePDF component for improved readability 2026-03-07 10:14:10 +07:00
rstubryan a468a5948b refactor(FE): Refactor useEffect hooks and loadAssignments function 2026-03-07 09:34:55 +07:00
rstubryan e05e1a4121 chore(FE): Fix missing dependency in useEffect dependency array 2026-03-07 09:20:14 +07:00
rstubryan e3b86e3033 refactor(FE): Add omit utility and refactor clearTabActions to use
it
2026-03-07 09:19:11 +07:00
rstubryan d467c56ea6 refactor(FE): Refactor form reset logic to use useCallback with
dependencies
2026-03-06 15:03:37 +07:00
rstubryan 784d9f26ab refactor(FE): Refactor fetchChecklistDetail with useCallback 2026-03-06 15:00:33 +07:00
rstubryan 978ef764ea refactor(FE): Refactor productsClickHandler to be defined inline 2026-03-06 14:45:27 +07:00
rstubryan 928136ff18 refactor(FE): Refactor handleViewUniformityDetails to inline definition 2026-03-06 14:29:21 +07:00
rstubryan 79567e4f1b refactor(FE): Remove unnecessary dependencies from useMemo in table
columns
2026-03-06 14:07:24 +07:00
rstubryan 633deece21 refactor(FE): Fix dependency array in ExpenseRealizationForm useCallback 2026-03-06 14:05:11 +07:00
rstubryan 46483af4c2 refactor(FE): Refactor formik field methods to use destructured helpers 2026-03-06 13:53:15 +07:00
rstubryan c2653e5068 Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into fix/cleanup-warning 2026-03-06 13:41:30 +07:00
rstubryan 8569bda7d6 refactor(FE): Refactor afterSubmit calls to improve readability 2026-03-06 13:41:29 +07:00
rstubryan 7da63dc542 refactor(FE): Prevent duplicate initial value loading in form modals 2026-03-06 13:37:35 +07:00
rstubryan aeceef4361 refactor(FE): Refactor handleBlurField to use useCallback 2026-03-06 13:35:10 +07:00
rstubryan ff6955be54 refactor(FE): Remove unused useSearchParams and related code 2026-03-06 13:33:46 +07:00
rstubryan eccab314b3 refactor(FE): Fix missing dependencies in MarketingTable useMemo 2026-03-06 11:23:01 +07:00
102 changed files with 2026 additions and 4942 deletions
-3
View File
@@ -45,6 +45,3 @@ next-env.d.ts
# claude
.claude
# rtk
rtk.exe
+2 -30
View File
@@ -15,7 +15,7 @@ default:
# ==========================================================
.build_template: &build_template
stage: build
image: public.ecr.aws/docker/library/node:20-alpine
image: node:20-alpine
cache:
key: npm-cache
paths:
@@ -56,7 +56,7 @@ default:
.deploy_template: &deploy_template
stage: deploy
image:
name: public.ecr.aws/aws-cli/aws-cli:latest
name: amazon/aws-cli:latest
entrypoint: ['/bin/sh', '-c']
script:
- set -e
@@ -183,31 +183,3 @@ deploy:staging:
environment:
name: staging
url: https://stg-lti-erp.mbugroup.id
# ==========================================================
# ====== STAGING (Branch production) ======
# ==========================================================
build:production:
<<: *build_template
rules:
- if: '$CI_COMMIT_BRANCH == "production"'
environment:
name: staging
variables:
NEXT_PUBLIC_LTI_URL: 'https://lti-erp.mbugroup.id'
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://auth-erp.mbugroup.id'
NEXT_PUBLIC_API_BASE_URL: 'https://api-lti.mbugroup.id/api'
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
deploy:production:
<<: *deploy_template
needs: ['build:production']
rules:
- if: '$CI_COMMIT_BRANCH == "production"'
variables:
S3_BUCKET: 'production-lti-erp.mbugroup.id'
AWS_REGION: 'ap-southeast-3'
CLOUDFRONT_DISTRIBUTION_ID: 'E1SSLXKYYITASJ'
environment:
name: staging
url: https://lti-erp.mbugroup.id
+1 -1
View File
@@ -1,3 +1,3 @@
npm run format
npm run lint
npm run typecheck
npx tsc --noEmit
+1 -1
View File
@@ -1,4 +1,4 @@
FROM public.ecr.aws/docker/library/node:20-alpine
FROM node:20-alpine
RUN apk add --no-cache git bash build-base curl
+1 -3
View File
@@ -7,10 +7,8 @@
"build": "next build --turbopack",
"start": "next start",
"lint": "eslint",
"typecheck": "next typegen && tsc --noEmit",
"prepare": "husky",
"format": "prettier --write .",
"pre-commit": "npm run format && npm run lint && npm run typecheck && npm run build"
"format": "prettier --write ."
},
"dependencies": {
"@react-pdf/renderer": "^4.3.1",
@@ -1,11 +0,0 @@
import { MasterKandangContent } from '@/figma-make/components/pages/master-data/kandang/MasterKandangContent';
const MasterKandangPage = () => {
return (
<section className='w-full'>
<MasterKandangContent />
</section>
);
};
export default MasterKandangPage;
@@ -11,13 +11,10 @@ const RecordingEdit = () => {
const searchParams = useSearchParams();
const recordingId = searchParams.get('recordingId');
const recordingDetailKey = recordingId
? ['recording-detail', recordingId]
: null;
const { data: recording, isLoading: isLoadingRecording } = useSWR(
recordingDetailKey,
([, id]: [string, string]) => RecordingApi.getSingle(parseInt(id))
recordingId,
(id: string) => RecordingApi.getSingle(parseInt(id))
);
if (!recordingId) {
+2 -5
View File
@@ -11,13 +11,10 @@ const RecordingDetail = () => {
const searchParams = useSearchParams();
const recordingId = searchParams.get('recordingId');
const recordingDetailKey = recordingId
? ['recording-detail', recordingId]
: null;
const { data: recording, isLoading: isLoadingRecording } = useSWR(
recordingDetailKey,
([, id]: [string, string]) => RecordingApi.getSingle(parseInt(id))
recordingId,
(id: string) => RecordingApi.getSingle(parseInt(id))
);
if (!recordingId) {
+3 -3
View File
@@ -51,7 +51,7 @@ const Button = ({
return (
<>
{(!href || (href && disabled)) && (
{!href && (
<button
{...props}
type={type}
@@ -68,9 +68,9 @@ const Button = ({
</button>
)}
{href && !disabled && (
{href && (
<Link
href={href}
href={disabled ? '#' : href}
target={target}
rel={rel}
aria-disabled={disabled}
+1 -3
View File
@@ -35,9 +35,7 @@ const NumberInput = ({
| undefined;
if (newChangeEvent) {
newChangeEvent.target.value = parseFloat(
numberFormatValues.value
) as unknown as string;
newChangeEvent.target.value = numberFormatValues.value;
onChange?.(newChangeEvent);
}
+16 -24
View File
@@ -24,8 +24,8 @@ import {
} from '@/types/api/api-general';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
export interface OptionType<T = string | number> {
value: T;
export interface OptionType {
value: string | number;
label: string;
className?: string;
labelClassName?: string;
@@ -566,31 +566,23 @@ const useSelect = <T,>(
setSize(size + 1);
};
let formattedSuccessRawData: SuccessApiResponse<T[]> | undefined = undefined;
let formattedErrorRawData: ErrorApiResponse | undefined = undefined;
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])) {
successData = {
...pages![latestPagesIndex],
data:
pages?.flatMap((page) =>
isResponseSuccess(page) ? page.data : []
) ?? [],
};
}
if (isResponseError(pages?.[latestPagesIndex])) {
errorData = pages![latestPagesIndex];
}
return {
formattedSuccessRawData: successData,
formattedErrorRawData: errorData,
if (isResponseSuccess(pages?.[latestPagesIndex])) {
formattedSuccessRawData = {
...pages?.[latestPagesIndex],
data:
pages?.flatMap((page) => (isResponseSuccess(page) ? page.data : [])) ??
[],
};
}, [pages, latestPagesIndex]);
}
if (isResponseError(pages?.[latestPagesIndex])) {
formattedErrorRawData = pages?.[latestPagesIndex];
}
return {
inputValue,
@@ -112,11 +112,12 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
kandangData={kandangData}
/>
<ClosingKandangList
initialValue={initialValue}
projectData={projectData}
selectedKandangId={kandangData?.id}
/>
{!kandangData && (
<ClosingKandangList
initialValue={initialValue}
projectData={projectData}
/>
)}
<Tabs
activeTabId={activeTabId}
@@ -5,11 +5,9 @@ import { ProjectFlock } from '@/types/api/production/project-flock';
const ClosingKandangList = ({
initialValue,
projectData,
selectedKandangId,
}: {
initialValue?: ClosingGeneralInformation;
projectData?: ProjectFlock;
selectedKandangId?: number;
}) => {
return (
<div className='w-full py-3 @container relative before:absolute before:top-0 before:left-0 before:right-0 before:-mx-4 before:border-t before:border-base-content/10'>
@@ -24,9 +22,6 @@ const ClosingKandangList = ({
variant='outline'
className='px-3 py-2.5 w-fit text-sm rounded-lg shadow-sm'
href={`/closing/detail/?closingId=${initialValue?.flock_id}&kandangId=${kandang.project_flock_kandang_id}`}
disabled={
selectedKandangId === kandang.project_flock_kandang_id
}
>
{kandang.name}
</Button>
@@ -276,7 +276,7 @@ const SalesClosingTable = ({ projectFlockId }: SalesClosingTableProps) => {
{
id: 'kandang',
accessorKey: 'kandang',
header: 'Kandang Atribusi',
header: 'Kandang',
cell: (props) => {
const kandang = props.getValue() as Kandang;
return kandang?.name || '-';
@@ -127,11 +127,11 @@ const ClosingOutgoingSapronaksTable = ({
},
{
accessorKey: 'source_warehouse',
header: 'Gudang Asal (Fisik)',
header: 'Gudang Asal',
},
{
accessorKey: 'destination_warehouse',
header: 'Gudang Tujuan (Fisik)',
header: 'Gudang Tujuan',
},
{
accessorKey: 'quantity',
@@ -9,11 +9,8 @@ import { useState, useEffect, useRef, useCallback } from 'react';
import useSWR from 'swr';
import { DashboardApi } from '@/services/api/dashboard';
import { useFormik } from 'formik';
import {
ProjectFlockApi,
ProjectFlockKandangApi,
} from '@/services/api/production';
import { LocationApi } from '@/services/api/master-data';
import { ProjectFlockApi } from '@/services/api/production';
import { KandangApi, LocationApi } from '@/services/api/master-data';
import { generateDashboardPDF } from '@/components/pages/dashboard/export/DashboardPDF';
import {
DashboardFilterType,
@@ -25,7 +22,10 @@ import DashboardExportCharts, {
DashboardExportChartsRef,
} from '@/components/pages/dashboard/export/DashboardExportCharts';
import { RadioGroup, RadioGroupItem } from '@/components/input/RadioInput';
import { DashboardMeta } from '@/types/api/dashboard/dashboard';
import {
DashboardFilter,
DashboardMeta,
} from '@/types/api/dashboard/dashboard';
import DashboardStats from '@/components/pages/dashboard/chart/DashboardStats';
import { isResponseSuccess } from '@/lib/api-helper';
import AlertErrorList from '@/components/helper/form/FormErrors';
@@ -42,8 +42,6 @@ import { cn } from '@/lib/helper';
import DashboardExportStats, {
DashboardExportStatsRef,
} from '@/components/pages/dashboard/export/DashboardExportStats';
import { ProjectFlock } from '@/types/api/production/project-flock';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
// Helper function to normalize values to array
const normalizeToArray = (
@@ -70,6 +68,7 @@ const DashboardProduction = () => {
const [analysisMode, setAnalysisMode] = useState<'OVERVIEW' | 'COMPARISON'>(
(filterValues.analysisMode as 'OVERVIEW' | 'COMPARISON') || 'OVERVIEW'
);
const [endpointUrl, setEndpointUrl] = useState('/dashboards');
const [selectedLocationIds, setSelectedLocationIds] = useState<number[]>(
normalizeToArray(filterValues.location)
);
@@ -81,29 +80,9 @@ const DashboardProduction = () => {
const {
data: dashboardProductionResponse,
isLoading: isLoadingDashboardProductionData,
} = useSWR(
[
'dashboard-production',
filterValues.startDate ?? '',
filterValues.endDate ?? '',
filterValues.analysisMode ?? 'OVERVIEW',
normalizeToArray(filterValues.location).toString(),
normalizeToArray(filterValues.flock).toString(),
normalizeToArray(filterValues.kandang).toString(),
filterValues.comparisonType ?? '',
],
() =>
DashboardApi.getDashboardProductionFetcher({
start_date: filterValues.startDate || '',
end_date: filterValues.endDate || '',
analysis_mode:
(filterValues.analysisMode as 'OVERVIEW' | 'COMPARISON') ||
'OVERVIEW',
location_ids: normalizeToArray(filterValues.location),
flock_ids: normalizeToArray(filterValues.flock),
kandang_ids: normalizeToArray(filterValues.kandang),
comparison_type: filterValues.comparisonType || '',
})
mutate: refreshDashboardProductionData,
} = useSWR(endpointUrl, () =>
DashboardApi.getDashboardProductionFetcher(endpointUrl)
);
const dashboardProductionData = isResponseSuccess(dashboardProductionResponse)
@@ -116,23 +95,23 @@ const DashboardProduction = () => {
options: flockOptions,
isLoadingOptions: isLoadingFlockOptions,
loadMore: loadMoreFlock,
} = useSelect<ProjectFlock>(
ProjectFlockApi.basePath,
'id',
'flock_name',
'search',
{
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
}
);
} = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', '', {
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
});
const {
setInputValue: setInputValueLocation,
options: locationOptions,
isLoadingOptions: isLoadingLocationOptions,
loadMore: loadMoreLocation,
} = useSelect(LocationApi.basePath, 'id', 'name');
const {
setInputValue: setInputValueKandang,
options: kandangOptions,
isLoadingOptions: isLoadingKandangOptions,
loadMore: loadMoreKandang,
} = useSelect(KandangApi.basePath, 'id', 'name', '', {
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
});
const comparisonTypeOptions = [
{ value: 'FARM', label: 'Farm' },
{ value: 'FLOCK', label: 'Flock' },
@@ -156,43 +135,68 @@ const DashboardProduction = () => {
enableReinitialize: true,
validationSchema: getDashboardFilterSchema(analysisMode),
onSubmit: (values) => {
// Save filter values to store
setFilterValues(values);
filterModal.closeModal();
handleApplyFilter({
start_date: values.startDate || '',
end_date: values.endDate || '',
analysis_mode: values.analysisMode as 'OVERVIEW' | 'COMPARISON',
location_ids: normalizeToArray(values.location),
flock_ids: normalizeToArray(values.flock),
kandang_ids: normalizeToArray(values.kandang),
comparison_type: values.comparisonType,
});
},
});
const { resetForm } = formik;
const selectedLocationValues = normalizeToArray(formik.values.location);
const selectedFlockValues = normalizeToArray(formik.values.flock);
const {
setInputValue: setInputValueKandang,
options: kandangOptions,
isLoadingOptions: isLoadingKandangOptions,
loadMore: loadMoreKandang,
} = useSelect<ProjectFlockKandang>(
ProjectFlockKandangApi.basePath,
'kandang_id',
'kandang.name',
'search',
{
location_id:
selectedLocationValues.length > 0
? selectedLocationValues.toString()
: '',
project_flock_id:
selectedFlockValues.length > 0 ? selectedFlockValues.toString() : '',
}
);
const handleResetFilter = useCallback(() => {
resetForm();
resetFilterValues(); // Clear stored filter values
setAnalysisMode('OVERVIEW');
setEndpointUrl('/dashboards');
setSelectedLocationIds([]);
filterModal.closeModal();
}, [filterModal, resetForm, resetFilterValues]);
}, [resetForm, resetFilterValues]);
const handleApplyFilter = useCallback(
(values: DashboardFilter) => {
// Build query params object, only include non-empty values
const params: Record<string, string> = {};
if (values.start_date) params.start_date = values.start_date;
if (values.end_date) params.end_date = values.end_date;
if (values.analysis_mode) params.analysis_mode = values.analysis_mode;
if (values.location_ids.length > 0)
params.location_ids = values.location_ids.toString();
if (values.flock_ids.length > 0)
params.flock_ids = values.flock_ids.toString();
if (values.kandang_ids.length > 0)
params.kandang_ids = values.kandang_ids.toString();
if (values.comparison_type)
params.comparison_type = values.comparison_type;
setEndpointUrl(`/dashboards?${new URLSearchParams(params).toString()}`);
filterModal.closeModal();
refreshDashboardProductionData();
},
[filterModal, refreshDashboardProductionData]
);
// ===== Load filter from store on mount =====
useEffect(() => {
if (!filterValues) return;
handleApplyFilter({
start_date: filterValues.startDate,
end_date: filterValues.endDate,
analysis_mode: filterValues.analysisMode as 'OVERVIEW' | 'COMPARISON',
location_ids: normalizeToArray(filterValues.location),
flock_ids: normalizeToArray(filterValues.flock),
kandang_ids: normalizeToArray(filterValues.kandang),
comparison_type: filterValues.comparisonType,
});
}, [filterValues, handleApplyFilter]);
// ===== Formik Error List =====
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
@@ -264,6 +268,14 @@ const DashboardProduction = () => {
};
}, [clearNavbarActions]);
if (isLoadingDashboardProductionData) {
return (
<div className='w-full min-h-screen flex items-center justify-center'>
<span className='loading loading-spinner loading-xl'></span>
</div>
);
}
return (
<>
<section className='w-full p-3 space-y-3'>
@@ -315,15 +327,9 @@ const DashboardProduction = () => {
</div>
{/* Dashboard Stats */}
<div>
{isLoadingDashboardProductionData ? (
<div className='w-full min-h-screen flex items-center justify-center'>
<span className='loading loading-spinner loading-xl'></span>
</div>
) : (
<DashboardStats
data={dashboardProductionData?.statistics_data ?? []}
/>
)}
<DashboardStats
data={dashboardProductionData?.statistics_data ?? []}
/>
</div>
{/* Use DashboardLineChart component or skeleton */}
@@ -531,7 +537,6 @@ const DashboardProduction = () => {
className={{
select: 'rounded-lg text-sm border-base-content/10',
}}
isClearable={true}
/>
)}
@@ -568,7 +573,6 @@ const DashboardProduction = () => {
className={{
select: 'rounded-lg text-sm border-base-content/10',
}}
isClearable={true}
/>
) : (
<SelectInputRadio
@@ -600,7 +604,6 @@ const DashboardProduction = () => {
className={{
select: 'rounded-lg text-sm border-base-content/10',
}}
isClearable={true}
/>
)}
@@ -640,7 +643,6 @@ const DashboardProduction = () => {
className={{
select: 'rounded-lg text-sm border-base-content/10',
}}
isClearable={true}
/>
) : (
<SelectInputRadio
@@ -667,7 +669,6 @@ const DashboardProduction = () => {
className={{
select: 'rounded-lg text-sm border-base-content/10',
}}
isClearable={true}
/>
)}
</>
@@ -706,7 +707,6 @@ const DashboardProduction = () => {
className={{
select: 'rounded-lg text-sm border-base-content/10',
}}
isClearable={true}
/>
) : (
<SelectInputRadio
@@ -733,7 +733,6 @@ const DashboardProduction = () => {
className={{
select: 'rounded-lg text-sm border-base-content/10',
}}
isClearable={true}
/>
)}
</>
@@ -279,6 +279,8 @@ const ExpenseRequestContent = ({
)}
<div className='w-full mt-4 flex flex-col gap-4'>
{/* TODO: apply RBAC */}
<div className='w-full mx-auto flex flex-col sm:flex-row justify-end gap-2'>
{isCurrentApprovalOnHeadArea && (
<RequirePermission permissions='lti.expense.approve.head_area'>
@@ -66,7 +66,7 @@ const ExpenseRealizationForm = ({
toast.success(createExpenseRes?.message as string);
router.push('/expense');
},
[router]
[router, initialValues?.id]
);
const updateExpenseHandler = useCallback(
@@ -207,7 +207,7 @@ const ExpenseRealizationForm = ({
// add new realizations for each kandang
kandangs.forEach((kandangItem) => {
if (isNaN(Number(kandangItem.id))) return;
if (!kandangItem.id) return;
const existingRealization = formik.values.realizations?.find(
(realizationItem) => realizationItem.kandang_id === kandangItem.id
@@ -35,7 +35,6 @@ const ExpenseRealizationKandangDetailExpense: React.FC<
setInputValue: setNonstockInputValue,
options: nonstockOptions,
isLoadingOptions: isLoadingNonstockOptions,
loadMore: loadMoreNonstocks,
} = useSelect<Nonstock>(
NonstockApi.basePath,
'id',
@@ -165,7 +164,6 @@ const ExpenseRealizationKandangDetailExpense: React.FC<
options={nonstockOptions}
isLoading={isLoadingNonstockOptions}
onInputChange={setNonstockInputValue}
onMenuScrollToBottom={loadMoreNonstocks}
className={{ wrapper: 'min-w-48' }}
isDisabled
/>
@@ -178,14 +178,14 @@ const ExpenseRequestForm = ({
setInputValue: setLocationInputValue,
options: locationOptions,
isLoadingOptions: isLoadingLocationOptions,
loadMore: loadMoreLocations,
loadMore: loadMoreLocationOptions,
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
const {
setInputValue: setVendorInputValue,
options: supplierOptions,
isLoadingOptions: isLoadingVendorOptions,
loadMore: loadMoreSuppliers,
loadMore: loadMoreVendorOptions,
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -410,13 +410,13 @@ const ExpenseRequestForm = ({
options={locationOptions}
onInputChange={setLocationInputValue}
isLoading={isLoadingLocationOptions}
onMenuScrollToBottom={loadMoreLocations}
isError={
formik.touched.location_id && Boolean(formik.errors.location_id)
}
errorMessage={formik.errors.location_id as string}
isClearable
className={{ wrapper: 'col-span-12 sm:col-span-4' }}
onMenuScrollToBottom={loadMoreLocationOptions}
/>
<DateInput
@@ -455,12 +455,12 @@ const ExpenseRequestForm = ({
options={supplierOptions}
onInputChange={setVendorInputValue}
isLoading={isLoadingVendorOptions}
onMenuScrollToBottom={loadMoreSuppliers}
isError={
formik.touched.supplier_id && Boolean(formik.errors.supplier_id)
}
errorMessage={formik.errors.supplier_id as string}
className={{ wrapper: 'col-span-12' }}
onMenuScrollToBottom={loadMoreVendorOptions}
/>
<RequirePermission permissions='lti.expense.document'>
+236 -559
View File
@@ -1,212 +1,154 @@
'use client';
import {
Document,
Image,
Link,
Page,
StyleSheet,
Text,
View,
} from '@react-pdf/renderer';
import React from 'react';
import { Document, Page, StyleSheet, View, Text } from '@react-pdf/renderer';
import { Expense } from '@/types/api/expense';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography';
import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge';
import { PdfPageNumber } from '@/components/helper/pdf/layout/PdfPageNumber';
import { PdfTable, PdfColumn } from '@/components/helper/pdf/table';
interface ExpensePDFProps {
expense?: Expense;
}
const ExpensePDFStyle = StyleSheet.create({
const styles = StyleSheet.create({
page: {
paddingTop: 24,
paddingBottom: 64,
paddingHorizontal: 32,
fontSize: 10,
fontFamily: 'Helvetica',
padding: 20,
backgroundColor: '#FFFFFF',
},
companyInfoHeader: {
width: '100%',
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 8,
},
companyLogo: {
width: 64,
height: 'auto',
},
companyInfoHeaderDate: {
paddingTop: 8,
fontSize: 12,
},
companyName: {
fontSize: 12,
fontWeight: 'bold',
marginBottom: 4,
},
companyAddress: {
fontSize: 8,
maxWidth: 400,
titleSection: {
marginBottom: 10,
},
title: {
marginTop: 16,
fontSize: 16,
lineHeight: '150%',
textAlign: 'center',
fontFamily: 'Times-Roman',
fontWeight: 'bold',
},
footer: {
width: '100%',
display: 'flex',
parameterContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 32,
position: 'absolute',
flexWrap: 'wrap',
marginBottom: 8,
},
infoTableSection: {
marginBottom: 12,
},
infoTableTitle: {
fontSize: 10,
bottom: 30,
left: 0,
right: 0,
textAlign: 'center',
color: 'grey',
},
// wrapper
generalInfoTable: {
width: '100%',
marginTop: 8,
borderWidth: 1,
borderColor: '#000000',
borderBottomWidth: 0,
fontSize: 12,
},
generalInfoTableRow: {
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: '#000000',
},
// columns
generalInfoTableColLabel: {
width: '30%',
paddingVertical: 6,
paddingHorizontal: 8,
},
generalInfoTableColSeparator: {
width: '3%',
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 6,
},
generalInfoTableColValue: {
width: '67%',
paddingVertical: 6,
paddingHorizontal: 8,
},
generalInfoTableLabelText: {
fontWeight: 'bold',
marginBottom: 6,
color: '#333',
},
generalInfoTableValueText: {},
// expense detail table
expenseDetailContainer: {
width: '100%',
marginTop: 12,
tableSection: {
marginBottom: 12,
},
expenseDetailTitle: {
fontSize: 14,
lineHeight: '150%',
fontFamily: 'Times-Roman',
fontWeight: 'bold',
textAlign: 'center',
},
kandangExpenseContainer: {
width: '100%',
marginTop: 8,
},
kandangExpenseTitle: {
fontSize: 14,
lineHeight: '150%',
fontFamily: 'Times-Roman',
fontWeight: 'bold',
textAlign: 'center',
},
kandangExpenseTable: {
width: '100%',
marginTop: 8,
borderWidth: 1,
borderColor: '#000000',
borderBottomWidth: 0,
fontSize: 12,
},
kandangExpenseTableRow: {
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: '#000000',
},
kandangExpenseTableColLabel: {
width: '20%',
paddingVertical: 6,
paddingHorizontal: 8,
},
kandangExpenseTableColLabelBorderRight: {
borderRight: '1px solid #000000',
},
kandangExpenseTableColNonstock: {
width: '20%',
},
kandangExpenseTableColNote: {
width: '40%',
},
kandangExpenseHeaderLabelText: {
fontWeight: 'bold',
},
kandangExpenseLabelText: {
tableTitle: {
fontSize: 10,
fontWeight: 'bold',
marginBottom: 6,
color: '#333',
},
kandangExpenseTableFooterColTotalExpenseCaption: {
width: '40%',
paddingVertical: 6,
paddingHorizontal: 8,
textAlign: 'right',
},
kandangExpenseTableFooterColTotalExpenseValue: {
width: '60%',
paddingVertical: 6,
paddingHorizontal: 8,
},
// utils
doubleDivider: {
width: '100%',
height: 6,
borderTop: '2px solid black',
borderBottom: '2px solid black',
emptyText: {
fontSize: 8,
color: '#666',
fontStyle: 'italic',
},
});
const ExpensePDF = ({ expense }: ExpensePDFProps) => {
type ExpenseKandang = Expense['kandangs'][number];
type PengajuanItem = NonNullable<ExpenseKandang['pengajuans']>[number];
type RealisasiItem = NonNullable<ExpenseKandang['realisasi']>[number];
const valueText = (v: unknown) => {
if (v === null || v === undefined) return '-';
if (typeof v === 'number') return formatNumber(v);
return String(v);
};
const getPengajuanColumns = (): PdfColumn<PengajuanItem>[] => [
{
key: 'no',
header: 'No',
flex: 0.5,
align: 'center',
cell: ({ index }) => index + 1,
},
{
key: 'nonstock',
header: 'Nonstock',
flex: 1.5,
cell: ({ row }) => row.nonstock.name,
},
{
key: 'qty',
header: 'Kuantitas',
flex: 1,
align: 'right',
cell: ({ row }) => valueText(row.qty),
},
{
key: 'price',
header: 'Harga Satuan',
flex: 1.2,
align: 'right',
cell: ({ row }) => formatCurrency(row.price),
},
{
key: 'notes',
header: 'Catatan',
flex: 1.5,
cell: ({ row }) => row.notes || '-',
},
];
const getRealisasiColumns = (): PdfColumn<RealisasiItem>[] => [
{
key: 'no',
header: 'No',
flex: 0.5,
align: 'center',
cell: ({ index }) => index + 1,
},
{
key: 'nonstock',
header: 'Nonstock',
flex: 1.5,
cell: ({ row }) => row.nonstock.name,
},
{
key: 'qty',
header: 'Kuantitas',
flex: 1,
align: 'right',
cell: ({ row }) => valueText(row.qty),
},
{
key: 'price',
header: 'Harga Satuan',
flex: 1.2,
align: 'right',
cell: ({ row }) => formatCurrency(row.price),
},
{
key: 'notes',
header: 'Catatan',
flex: 1.5,
cell: ({ row }) => row.notes || '-',
},
];
const getInfoTableRows = (expense?: Expense) => {
const isLatestApprovalRejected =
expense?.latest_approval?.action === 'REJECTED';
const isExpenseRealized =
expense?.latest_approval?.step_number &&
expense?.latest_approval.step_number >= 5;
const realizationStatus = isExpenseRealized
? 'Sudah Realisasi'
: 'Belum Realisasi';
const rows = [
{ label: 'Nomor PO', value: expense?.po_number },
{ label: 'Nomor Referensi', value: expense?.reference_number },
return [
{ label: 'Nomor PO', value: expense?.po_number || '-' },
{ label: 'Nomor Referensi', value: expense?.reference_number || '-' },
{
label: 'Kategori',
value:
@@ -214,9 +156,9 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
? 'Biaya Operasional'
: expense?.category === 'NON-BOP'
? 'Non Biaya Operasional'
: '',
: '-',
},
{ label: 'Lokasi', value: expense?.location.name },
{ label: 'Lokasi', value: expense?.location?.name || '-' },
{
label: 'Kandang',
value:
@@ -227,7 +169,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
.join(', ')
: '-',
},
{ label: 'Vendor', value: expense?.supplier.name },
{ label: 'Vendor', value: expense?.supplier?.name || '-' },
{
label: 'Tanggal Transaksi',
value: formatDate(expense?.transaction_date, 'DD MMMM YYYY'),
@@ -238,12 +180,12 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
? formatDate(expense?.realization_date, 'DD MMMM YYYY')
: '-',
},
{ label: 'Nama Pengaju', value: expense?.created_user.name },
{ label: 'Nama Pengaju', value: expense?.created_user?.name || '-' },
{
label: 'Nominal Biaya',
value: formatCurrency(
expense?.latest_approval.step_number === 5 ||
expense?.latest_approval.step_number === 6
expense?.latest_approval?.step_number === 5 ||
expense?.latest_approval?.step_number === 6
? (expense?.total_realisasi ?? 0)
: (expense?.total_pengajuan ?? 0)
),
@@ -263,401 +205,136 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
label: 'Status Biaya',
value: isLatestApprovalRejected
? 'Ditolak'
: expense?.latest_approval?.step_name,
: expense?.latest_approval?.step_name || '-',
},
];
};
interface InfoRow {
label: string;
value: string;
}
const getInfoTableColumns = (): PdfColumn<InfoRow>[] => [
{
key: 'label',
header: 'Field',
flex: 1,
cell: ({ row }) => row.label,
},
{
key: 'value',
header: 'Value',
flex: 2,
cell: ({ row }) => row.value,
},
];
const ExpensePDF = ({ expense }: ExpensePDFProps) => {
const kandangs = expense?.kandangs || [];
const infoRows = getInfoTableRows(expense);
return (
<Document>
<Page style={ExpensePDFStyle.page}>
<View>
<View style={ExpensePDFStyle.companyInfoHeader}>
<Image
style={ExpensePDFStyle.companyLogo}
src='/assets/img/lti-logo.png'
/>
<Text style={ExpensePDFStyle.companyInfoHeaderDate}>
{formatDate(Date.now(), 'DD MMMM YYYY')}
</Text>
</View>
<View>
<Text style={ExpensePDFStyle.companyName}>
PT LUMBUNG TELUR INDONESIA
</Text>
<Text style={ExpensePDFStyle.companyAddress}>
Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten
Bandung Barat, Jawa Barat 40514
</Text>
<View style={ExpensePDFStyle.doubleDivider} />
<Page style={styles.page} size='A4'>
{/* Title Section */}
<View style={styles.titleSection}>
<PdfTypography size='h1' variant='primary'>
Laporan{' '}
{expense?.category === 'BOP'
? 'Biaya Operasional'
: 'Non-Biaya Operasional'}
</PdfTypography>
<PdfTypography size='h2'>{expense?.po_number || '-'}</PdfTypography>
<View style={styles.parameterContainer}>
<PdfParamBadge>
Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')}
</PdfParamBadge>
<PdfParamBadge>
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
</PdfParamBadge>
</View>
</View>
<Text style={ExpensePDFStyle.title}>
Laporan{' '}
{expense?.category === 'BOP'
? 'Biaya Operasional'
: 'Non-Biaya Operasional'}{' '}
{expense?.po_number}
</Text>
{/* General info table */}
<View style={ExpensePDFStyle.generalInfoTable}>
{rows.map((row) => (
<View style={ExpensePDFStyle.generalInfoTableRow} key={row.label}>
<View style={ExpensePDFStyle.generalInfoTableColLabel}>
<Text style={ExpensePDFStyle.generalInfoTableLabelText}>
{row.label}
</Text>
</View>
<View style={ExpensePDFStyle.generalInfoTableColSeparator}>
<Text>:</Text>
</View>
<View style={ExpensePDFStyle.generalInfoTableColValue}>
<Text style={ExpensePDFStyle.generalInfoTableValueText}>
{row.value}
</Text>
</View>
</View>
))}
{/* Info Table Section */}
<View style={styles.infoTableSection}>
<Text style={styles.infoTableTitle}>Informasi Biaya</Text>
<PdfTable columns={getInfoTableColumns()} data={infoRows} />
</View>
{/* Detail expense request */}
<View
minPresenceAhead={80}
style={ExpensePDFStyle.expenseDetailContainer}
>
<Text style={ExpensePDFStyle.expenseDetailTitle}>
Rincian Pengajuan Biaya Operasional
</Text>
{/* Rincian Pengajuan Section */}
<View style={styles.tableSection}>
<Text style={styles.tableTitle}>1. Rincian Pengajuan Biaya</Text>
{kandangs.length === 0 ? (
<Text style={styles.emptyText}>Tidak ada data pengajuan.</Text>
) : (
kandangs.map((kandang, idx) => {
const pengajuans = kandang.pengajuans || [];
const kandangName =
kandang.kandang_id && kandang.name
? kandang.name
: expense?.location?.name || 'Umum';
{expense?.kandangs.map((kandangExpense, kandangExpenseIdx) => {
let expenseRequestTotal = 0;
kandangExpense.pengajuans?.forEach(
(item) => (expenseRequestTotal += item.qty * item.price)
);
return (
<View
key={kandangExpenseIdx}
style={ExpensePDFStyle.kandangExpenseContainer}
>
<Text style={ExpensePDFStyle.kandangExpenseTitle}>
{kandangExpense.kandang_id && kandangExpense.name
? `Biaya ${kandangExpense.name}`
: `Biaya ${expense?.location.name || 'Umum'}`}
</Text>
<View style={ExpensePDFStyle.kandangExpenseTable}>
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
ExpensePDFStyle.kandangExpenseTableColNonstock,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Nonstock
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Kuantitas
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Harga Satuan
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColNote,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Catatan
</Text>
</View>
</View>
{kandangExpense.pengajuans?.map((pengajuan, pengajuanIdx) => (
<View
key={pengajuanIdx}
style={ExpensePDFStyle.kandangExpenseTableRow}
>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
ExpensePDFStyle.kandangExpenseTableColNonstock,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{pengajuan.nonstock.name}
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{formatNumber(pengajuan.qty)}
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{formatCurrency(pengajuan.price)}
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColNote,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{pengajuan.notes}
</Text>
</View>
</View>
))}
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
<View
style={[
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseCaption,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Total Biaya Keseluruhan
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseValue,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
{formatCurrency(expenseRequestTotal)}
</Text>
</View>
</View>
return (
<View key={idx} style={{ marginBottom: 12 }}>
<PdfTypography size='h3' style={{ paddingLeft: 12 }}>
{idx + 1}) {kandangName}
</PdfTypography>
{pengajuans.length > 0 ? (
<PdfTable
columns={getPengajuanColumns()}
data={pengajuans}
showFooter={true}
footerLabel='Total'
/>
) : (
<Text style={styles.emptyText}>
Tidak ada item pengajuan untuk kandang ini.
</Text>
)}
</View>
</View>
);
})}
);
})
)}
</View>
{/* Detail expense realization */}
<View
minPresenceAhead={80}
style={ExpensePDFStyle.expenseDetailContainer}
>
<Text style={ExpensePDFStyle.expenseDetailTitle}>
Rincian Realisasi Biaya Operasional
</Text>
{/* Rincian Realisasi Section */}
<View style={styles.tableSection}>
<Text style={styles.tableTitle}>2. Rincian Realisasi Biaya</Text>
{kandangs.length === 0 ? (
<Text style={styles.emptyText}>Tidak ada data realisasi.</Text>
) : (
kandangs.map((kandang, idx) => {
const realisasi = kandang.realisasi || [];
const kandangName =
kandang.kandang_id && kandang.name
? kandang.name
: expense?.location?.name || 'Umum';
{expense?.kandangs.map((kandangExpense, kandangExpenseIdx) => {
let expenseRealizationTotal = 0;
kandangExpense.realisasi?.forEach(
(item) => (expenseRealizationTotal += item.qty * item.price)
);
return (
<View
key={kandangExpenseIdx}
style={ExpensePDFStyle.kandangExpenseContainer}
>
<Text style={ExpensePDFStyle.kandangExpenseTitle}>
{kandangExpense.kandang_id && kandangExpense.name
? `Biaya ${kandangExpense.name}`
: `Biaya ${expense?.location.name || 'Umum'}`}
</Text>
<View style={ExpensePDFStyle.kandangExpenseTable}>
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
ExpensePDFStyle.kandangExpenseTableColNonstock,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Nonstock
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Kuantitas
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Harga Satuan
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColNote,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Catatan
</Text>
</View>
</View>
{kandangExpense.realisasi?.map((realisasi, realisasiIdx) => (
<View
key={realisasiIdx}
style={ExpensePDFStyle.kandangExpenseTableRow}
>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
ExpensePDFStyle.kandangExpenseTableColNonstock,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{realisasi.nonstock.name}
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{formatNumber(realisasi.qty)}
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{formatCurrency(realisasi.price)}
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColNote,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{realisasi.notes}
</Text>
</View>
</View>
))}
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
<View
style={[
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseCaption,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Total Biaya Keseluruhan
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseValue,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
{formatCurrency(expenseRealizationTotal)}
</Text>
</View>
</View>
return (
<View key={idx} style={{ marginBottom: 12 }}>
<PdfTypography size='h3' style={{ paddingLeft: 12 }}>
{idx + 1}) {kandangName}
</PdfTypography>
{realisasi.length > 0 ? (
<PdfTable
columns={getRealisasiColumns()}
data={realisasi}
showFooter={true}
footerLabel='Total'
/>
) : (
<Text style={styles.emptyText}>
Tidak ada item realisasi untuk kandang ini.
</Text>
)}
</View>
</View>
);
})}
);
})
)}
</View>
<View style={ExpensePDFStyle.footer} fixed>
<Link
src={`${process.env.NEXT_PUBLIC_LTI_URL}expense/detail?expenseId=${expense?.id}`}
>
{expense?.po_number}
</Link>
<Text
render={({ pageNumber, totalPages }) =>
`${pageNumber} / ${totalPages}`
}
fixed
/>
</View>
<PdfPageNumber />
</Page>
</Document>
);
@@ -8,7 +8,7 @@ import {
useState,
} from 'react';
import { usePathname } from 'next/navigation';
import useSWR, { mutate } from 'swr';
import useSWR from 'swr';
import { Icon } from '@iconify/react';
import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table';
import { useFormik } from 'formik';
@@ -26,10 +26,6 @@ import { InventoryAdjustmentApi } from '@/services/api/inventory';
import { WarehouseApi, ProductApi } from '@/services/api/master-data';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { useUiStore } from '@/stores/ui/ui.store';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent';
import toast from 'react-hot-toast';
import { InventoryAdjustment } from '@/types/api/inventory/adjustment';
import { Warehouse } from '@/types/api/master-data/warehouse';
import { TRANSACTION_SUBTYPE_OPTIONS } from '@/config/constant';
@@ -42,62 +38,6 @@ import {
AdjustmentFilterType,
} from '@/components/pages/inventory/adjustment/filter/AdjustmentFilter';
import SelectInputRadio from '@/components/input/SelectInputRadio';
import { CellContext } from '@tanstack/react-table';
const RowOptionsMenu = ({
popoverPosition = 'bottom',
props,
deleteClickHandler,
}: {
popoverPosition: 'bottom' | 'top';
props: CellContext<InventoryAdjustment, unknown>;
deleteClickHandler: () => void;
}) => {
const popoverId = `adjustment#${props.row.original.id}`;
const popoverAnchorName = `--anchor-adjustment#${props.row.original.id}`;
const closePopover = () => {
document.getElementById(popoverId)?.hidePopover();
};
return (
<div className='relative'>
<PopoverButton
tabIndex={0}
variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
>
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</PopoverButton>
<PopoverContent
id={popoverId}
anchorName={popoverAnchorName}
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
>
<div className='flex flex-col bg-base-100 rounded-xl'>
<RequirePermission permissions='lti.inventory.delete'>
<Button
onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost'
color='error'
className='p-3 justify-start text-sm font-semibold w-full focus-visible:text-error-content hover:text-error-content'
>
<Icon icon='mdi:delete-outline' width={20} height={20} />
Delete
</Button>
</RequirePermission>
</div>
</PopoverContent>
</div>
);
};
const InventoryAdjustmentTable = () => {
const { searchValue, setSearchValue, setTableState } = useUiStore();
@@ -140,13 +80,13 @@ const InventoryAdjustmentTable = () => {
const formik = useFormik<AdjustmentFilterType>({
initialValues: {
product_id: null,
warehouse: null,
warehouse_id: null,
transaction_type: null,
},
validationSchema: AdjustmentFilterSchema,
onSubmit: (values, { setSubmitting }) => {
updateFilter('productFilter', values.product_id || '');
updateFilter('warehouseFilter', String(values.warehouse?.value) || '');
updateFilter('warehouseFilter', values.warehouse_id || '');
updateFilter('transactionTypeFilter', values.transaction_type || '');
filterModal.closeModal();
setSubmitting(false);
@@ -202,11 +142,14 @@ const InventoryAdjustmentTable = () => {
[formik]
);
const handleFilterWarehouseChange = (
val: OptionType | OptionType[] | null
) => {
formik.setFieldValue('warehouse', val);
};
const handleFilterWarehouseChange = useCallback(
(val: OptionType | OptionType[] | null) => {
const warehouse = val as OptionType | null;
const warehouseId = warehouse?.value ? String(warehouse.value) : null;
formik.setFieldValue('warehouse_id', warehouseId);
},
[formik]
);
const handleFilterTransactionTypeChange = useCallback(
(val: OptionType | OptionType[] | null) => {
@@ -227,6 +170,15 @@ const InventoryAdjustmentTable = () => {
);
}, [formik.values.product_id, productOptions]);
const warehouseIdValue = useMemo(() => {
if (!formik.values.warehouse_id) return null;
return (
warehouseOptions.find(
(opt) => String(opt.value) === formik.values.warehouse_id
) || null
);
}, [formik.values.warehouse_id, warehouseOptions]);
const transactionTypeValue = useMemo(() => {
if (!formik.values.transaction_type) return null;
return (
@@ -242,39 +194,12 @@ const InventoryAdjustmentTable = () => {
formik.validateForm();
};
const {
data: inventoryAdjustments,
isLoading,
mutate: refreshAdjustments,
} = useSWR(
const { data: inventoryAdjustments, isLoading } = useSWR(
`${InventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`,
InventoryAdjustmentApi.getAllFetcher
);
const singleDeleteHandler = async () => {
setIsDeleteLoading(true);
const response = await InventoryAdjustmentApi.delete(
selectedAdjustment?.id as number
);
singleDeleteModal.closeModal();
setIsDeleteLoading(false);
if (isResponseSuccess(response)) {
toast.success(response?.message || 'Successfully delete Adjustment!');
refreshAdjustments();
} else {
toast.error(response?.message || 'Failed to delete Adjustment');
}
};
const [sorting, setSorting] = useState<SortingState>([]);
const [selectedAdjustment, setSelectedAdjustment] = useState<
InventoryAdjustment | undefined
>(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const singleDeleteModal = useModal();
useEffect(() => {
updateFilter('search', searchValue);
@@ -389,39 +314,8 @@ const InventoryAdjustmentTable = () => {
header: 'Oleh',
accessorFn: (row) => row.created_user?.name ?? '-',
},
{
id: 'actions',
header: 'Aksi',
cell: (props: CellContext<InventoryAdjustment, unknown>) => {
const currentPageSize =
props.table.getPaginationRowModel().rows.length;
const currentPageRows = props.table.getPaginationRowModel().flatRows;
const currentRowRelativeIndex =
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
const deleteClickHandler = () => {
setSelectedAdjustment(props.row.original);
singleDeleteModal.openModal();
};
return (
<RowOptionsMenu
props={props}
deleteClickHandler={deleteClickHandler}
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
/>
);
},
},
],
[
tableFilterState.pageSize,
tableFilterState.page,
singleDeleteModal,
setSelectedAdjustment,
]
[]
);
const updateSortingFilter = useCallback(
@@ -608,7 +502,7 @@ const InventoryAdjustmentTable = () => {
label='Gudang'
placeholder='Pilih Gudang'
options={warehouseOptions}
value={formik.values.warehouse}
value={warehouseIdValue}
onChange={handleFilterWarehouseChange}
onInputChange={setWarehouseInputValue}
isLoading={isLoadingWarehouseOptions}
@@ -650,21 +544,6 @@ const InventoryAdjustmentTable = () => {
</div>
</form>
</Modal>
<ConfirmationModal
ref={singleDeleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Adjustment ini?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isDeleteLoading,
onClick: singleDeleteHandler,
}}
/>
</>
);
};
@@ -1,5 +1,4 @@
import { string, object } from 'yup';
import { OptionType } from '@/components/input/SelectInput';
export const AdjustmentFilterSchema = object().shape({
product_id: string().nullable(),
@@ -9,6 +8,6 @@ export const AdjustmentFilterSchema = object().shape({
export type AdjustmentFilterType = {
product_id: string | null;
warehouse_id: string | null;
transaction_type: string | null;
warehouse: OptionType<number> | null;
};
@@ -15,7 +15,7 @@ import {
InventoryAdjustmentFormSchema,
InventoryAdjustmentFormValues,
} from '@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema';
import { LocationApi } from '@/services/api/master-data';
import { KandangApi, LocationApi } from '@/services/api/master-data';
import {
ProjectFlockApi,
ProjectFlockKandangApi,
@@ -32,6 +32,8 @@ import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import AlertErrorList from '@/components/helper/form/FormErrors';
import { Location } from '@/types/api/master-data/location';
import { ProjectFlock } from '@/types/api/production/project-flock';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
import { Kandang } from '@/types/api/master-data/kandang';
import { Product } from '@/types/api/master-data/product';
import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock';
import { BaseApiResponse } from '@/types/api/api-general';
@@ -117,19 +119,40 @@ const InventoryAdjustmentForm = ({
}
);
const { rawData: approvedProjectFlockKandangsRawData } =
useSelect<ProjectFlockKandang>(
ProjectFlockKandangApi.basePath,
'id',
'id',
'search',
{
step_name: 'Disetujui',
limit: '100',
}
);
const approvedProjectFlockKandangs = useMemo(() => {
if (
approvedProjectFlockKandangsRawData &&
'data' in approvedProjectFlockKandangsRawData
) {
return approvedProjectFlockKandangsRawData.data as ProjectFlockKandang[];
}
return [];
}, [approvedProjectFlockKandangsRawData]);
const {
options: projectFlockKandangOptions,
loadMore: loadMoreProjectFlockKandangs,
setInputValue: setProjectFlockKandangInputValue,
isLoadingOptions: isLoadingProjectFlockKandangOptions,
} = useSelect(
selectedProjectFlock ? ProjectFlockKandangApi.basePath : '',
'kandang.id',
'kandang.name',
setInputValue: setKandangInputValue,
options: kandangOptionsFromApi,
isLoadingOptions: isLoadingKandangOptions,
loadMore: loadMoreKandangs,
} = useSelect<Kandang>(
selectedProjectFlock ? KandangApi.basePath : '',
'id',
'name',
'search',
{
step_name: 'Disetujui',
project_flock_id: String(selectedProjectFlock?.value),
location_id: selectedProjectFlockLocationId,
}
);
@@ -162,9 +185,7 @@ const InventoryAdjustmentForm = ({
isLoadingOptions: isLoadingProductOptions,
loadMore: loadMoreProducts,
rawData: products,
} = useSelect<Product>(ProductApi.basePath, 'id', 'name', 'search', {
include_all: 'true',
});
} = useSelect<Product>(ProductApi.basePath, 'id', 'name', 'search');
const {
setInputValue: setDepletionProductInputValue,
@@ -199,6 +220,26 @@ const InventoryAdjustmentForm = ({
return (product?.flags as string[]) || [];
}, [selectedProduct, productOptions]);
const kandangOptions = useMemo(() => {
let options: OptionType[] = [];
if (selectedProjectFlock) {
const approvedKandangIds = approvedProjectFlockKandangs
.filter((pfk) => pfk.project_flock_id === selectedProjectFlock.value)
.map((pfk) => pfk.kandang_id);
options = kandangOptionsFromApi.filter((kandang) =>
approvedKandangIds.includes(kandang.value as number)
);
}
return options;
}, [
selectedProjectFlock,
kandangOptionsFromApi,
approvedProjectFlockKandangs,
]);
const formikInitialValues = useMemo<Partial<InventoryAdjustmentFormValues>>(
() => ({
location: null,
@@ -650,10 +691,10 @@ const InventoryAdjustmentForm = ({
label='Kandang'
value={selectedKandang}
onChange={kandangChangeHandler}
onInputChange={setProjectFlockKandangInputValue}
options={projectFlockKandangOptions}
onMenuScrollToBottom={loadMoreProjectFlockKandangs}
isLoading={isLoadingProjectFlockKandangOptions}
onInputChange={setKandangInputValue}
options={kandangOptions}
onMenuScrollToBottom={loadMoreKandangs}
isLoading={isLoadingKandangOptions}
isError={
formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
}
@@ -8,7 +8,7 @@ import {
useState,
} from 'react';
import { usePathname } from 'next/navigation';
import useSWR, { mutate } from 'swr';
import useSWR from 'swr';
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
import { useFormik } from 'formik';
@@ -21,8 +21,6 @@ import { cn } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { useUiStore } from '@/stores/ui/ui.store';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import toast from 'react-hot-toast';
import Button from '@/components/Button';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import SelectInput, { useSelect } from '@/components/input/SelectInput';
@@ -43,11 +41,9 @@ import {
const RowOptionsMenu = ({
popoverPosition = 'bottom',
props,
deleteClickHandler,
}: {
popoverPosition: 'bottom' | 'top';
props: CellContext<Movement, unknown>;
deleteClickHandler: () => void;
}) => {
const popoverId = `movement#${props.row.original.id}`;
const popoverAnchorName = `--anchor-movement#${props.row.original.id}`;
@@ -87,20 +83,6 @@ const RowOptionsMenu = ({
Detail
</Button>
</RequirePermission>
<RequirePermission permissions='lti.inventory.transfer.delete'>
<Button
onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost'
color='error'
className='p-3 justify-start text-sm font-semibold w-full focus-visible:text-error-content hover:text-error-content'
>
<Icon icon='mdi:delete-outline' width={20} height={20} />
Delete
</Button>
</RequirePermission>
</div>
</PopoverContent>
</div>
@@ -224,37 +206,12 @@ const MovementTable = () => {
};
const [sorting, setSorting] = useState<SortingState>([]);
const [selectedMovement, setSelectedMovement] = useState<
Movement | undefined
>(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const singleDeleteModal = useModal();
const {
data: movements,
isLoading,
mutate: refreshMovements,
} = useSWR(
const { data: movements, isLoading } = useSWR(
`${MovementApi.basePath}${getTableFilterQueryString()}`,
MovementApi.getAllFetcher
);
const singleDeleteHandler = async () => {
setIsDeleteLoading(true);
const response = await MovementApi.delete(selectedMovement?.id as number);
singleDeleteModal.closeModal();
setIsDeleteLoading(false);
if (isResponseSuccess(response)) {
toast.success(response?.message || 'Successfully delete Movement!');
refreshMovements();
} else {
toast.error(response?.message || 'Failed to delete Movement');
}
};
useEffect(() => {
updateFilter('search', searchValue);
}, [searchValue, updateFilter]);
@@ -318,27 +275,16 @@ const MovementTable = () => {
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
const deleteClickHandler = () => {
setSelectedMovement(props.row.original);
singleDeleteModal.openModal();
};
return (
<RowOptionsMenu
props={props}
deleteClickHandler={deleteClickHandler}
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
/>
);
},
},
],
[
tableFilterState.pageSize,
tableFilterState.page,
singleDeleteModal,
setSelectedMovement,
]
[tableFilterState.pageSize, tableFilterState.page]
);
return (
@@ -509,21 +455,6 @@ const MovementTable = () => {
</div>
</form>
</Modal>
<ConfirmationModal
ref={singleDeleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Movement ini?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isDeleteLoading,
onClick: singleDeleteHandler,
}}
/>
</>
);
};
@@ -82,7 +82,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
warehouse_id: number;
warehouse_name: string;
quantity: number;
transfer_available_qty?: number;
}
// ===== USE SELECT HOOKS =====
@@ -324,6 +323,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
},
});
const { setFieldValue, setFieldTouched, setFieldError } = formik;
const prevSourceWarehouseIdRef = useRef<number | null>(
formik.values.source_warehouse_id
);
@@ -337,14 +338,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
prevSourceWarehouseId !== currentSourceWarehouseId &&
prevSourceWarehouseId !== null
) {
formik.setFieldValue('products', [
setFieldValue('products', [
{
product: null,
product_id: 0,
product_qty: '',
},
]);
formik.setFieldTouched('products', false);
setFieldTouched('products', false);
const updatedDeliveries = formik.values.deliveries.map(
(delivery: DeliverySchema) => ({
@@ -358,12 +359,17 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
],
})
);
formik.setFieldValue('deliveries', updatedDeliveries);
formik.setFieldTouched('deliveries', false);
setFieldValue('deliveries', updatedDeliveries);
setFieldTouched('deliveries', false);
}
prevSourceWarehouseIdRef.current = currentSourceWarehouseId;
}, [formik.values.source_warehouse_id, formik.values.deliveries]);
}, [
formik.values.source_warehouse_id,
formik.values.deliveries,
setFieldValue,
setFieldTouched,
]);
// ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
const {
@@ -380,8 +386,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
warehouse_id: formik.values.source_warehouse_id
? formik.values.source_warehouse_id.toString()
: '',
transfer_context: 'inventory_transfer',
stock_mode: 'exclude_chickin',
}
);
@@ -394,7 +398,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
warehouse_id: pw.warehouse.id,
warehouse_name: pw.warehouse.name,
quantity: pw.quantity,
transfer_available_qty: pw.transfer_available_qty,
}))
: [];
}, [productWarehouses]);
@@ -459,9 +462,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
// ===== EVENT HANDLERS =====
const handleTransferDateChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
formik.setFieldValue('transfer_date', e.target.value);
setFieldValue('transfer_date', e.target.value);
},
[]
[setFieldValue]
);
const handleSourceWarehouseChange = useCallback(
@@ -481,14 +484,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
return;
}
formik.setFieldTouched('source_warehouse', true);
formik.setFieldValue('source_warehouse', val);
formik.setFieldTouched('source_warehouse_id', true);
formik.setFieldValue('source_warehouse_id', newSourceWarehouseId);
setFieldTouched('source_warehouse', true);
setFieldValue('source_warehouse', val);
setFieldTouched('source_warehouse_id', true);
setFieldValue('source_warehouse_id', newSourceWarehouseId);
},
[
formik.values.destination_warehouse_id,
formik.values.destination_warehouse,
setFieldTouched,
setFieldValue,
]
);
@@ -509,15 +514,17 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
return;
}
formik.setFieldTouched('destination_warehouse', true);
formik.setFieldValue('destination_warehouse', val);
formik.setFieldTouched('destination_warehouse_id', true);
formik.setFieldValue(
'destination_warehouse_id',
newDestinationWarehouseId
);
setFieldTouched('destination_warehouse', true);
setFieldValue('destination_warehouse', val);
setFieldTouched('destination_warehouse_id', true);
setFieldValue('destination_warehouse_id', newDestinationWarehouseId);
},
[formik.values.source_warehouse_id, formik.values.source_warehouse]
[
formik.values.source_warehouse_id,
formik.values.source_warehouse,
setFieldTouched,
setFieldValue,
]
);
const addProduct = useCallback(() => {
@@ -529,15 +536,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
product_qty: '',
},
];
formik.setFieldValue('products', newProducts);
}, [formik.values.products]);
setFieldValue('products', newProducts);
}, [formik.values.products, setFieldValue]);
const removeProduct = useCallback(
(i: number) => {
const updatedProducts = formik.values.products?.filter(
(_, idx) => idx !== i
);
formik.setFieldValue('products', updatedProducts);
setFieldValue('products', updatedProducts);
setSelectedProducts([]);
@@ -546,7 +553,12 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
setProductQtyErrorShown(false);
}
},
[formik.values.products, productQtyErrorShown, setSelectedProducts]
[
formik.values.products,
productQtyErrorShown,
setSelectedProducts,
setFieldValue,
]
);
const bulkRemoveProduct = useCallback(() => {
@@ -554,26 +566,32 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
formik.values.products?.filter(
(_, idx) => !selectedProducts.includes(idx)
) ?? [];
formik.setFieldValue('products', updatedProducts);
setFieldValue('products', updatedProducts);
setSelectedProducts([]);
if (productQtyErrorShown) {
toast.dismiss();
setProductQtyErrorShown(false);
}
}, [formik, selectedProducts, setSelectedProducts, productQtyErrorShown]);
}, [
selectedProducts,
setSelectedProducts,
productQtyErrorShown,
setFieldValue,
formik.values.products,
]);
const handleProductChange = useCallback(
(idx: number, val: OptionType | OptionType[] | null) => {
formik.setFieldTouched(`products.${idx}.product`, true);
formik.setFieldValue(`products.${idx}.product`, val);
formik.setFieldTouched(`products.${idx}.product_id`, true);
formik.setFieldValue(
setFieldTouched(`products.${idx}.product`, true);
setFieldValue(`products.${idx}.product`, val);
setFieldTouched(`products.${idx}.product_id`, true);
setFieldValue(
`products.${idx}.product_id`,
(val as ProductWarehouseOptionType)?.value
);
},
[]
[setFieldTouched, setFieldValue]
);
const handleProductSelectAllChange = useCallback(
@@ -600,7 +618,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
);
const addDelivery = useCallback(() => {
formik.setFieldValue('deliveries', [
setFieldValue('deliveries', [
...(formik.values.deliveries || []),
{
delivery_cost: '',
@@ -619,14 +637,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
],
},
]);
}, [formik.values.deliveries]);
}, [formik.values.deliveries, setFieldValue]);
const removeDelivery = useCallback(
(i: number) => {
const updatedDeliveries = formik.values.deliveries?.filter(
(_, idx) => idx !== i
);
formik.setFieldValue('deliveries', updatedDeliveries);
setFieldValue('deliveries', updatedDeliveries);
setSelectedDeliveries([]);
@@ -635,7 +653,12 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
setDeliveryQtyErrorShown(false);
}
},
[formik.values.deliveries, deliveryQtyErrorShown, setSelectedDeliveries]
[
formik.values.deliveries,
deliveryQtyErrorShown,
setSelectedDeliveries,
setFieldValue,
]
);
const bulkRemoveDelivery = useCallback(() => {
@@ -643,7 +666,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
formik.values.deliveries?.filter(
(_, idx) => !selectedDeliveries.includes(idx)
) ?? [];
formik.setFieldValue('deliveries', updatedDeliveries);
setFieldValue('deliveries', updatedDeliveries);
setSelectedDeliveries([]);
if (deliveryQtyErrorShown) {
@@ -651,10 +674,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
setDeliveryQtyErrorShown(false);
}
}, [
formik,
selectedDeliveries,
setSelectedDeliveries,
deliveryQtyErrorShown,
setFieldValue,
formik.values.deliveries,
]);
const handleDeliverySelectAllChange = useCallback(
@@ -684,34 +708,28 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
const handleDeliveryProductChange = useCallback(
(deliveryIdx: number, val: OptionType | OptionType[] | null) => {
formik.setFieldTouched(
`deliveries.${deliveryIdx}.products.0.product`,
true
);
formik.setFieldValue(`deliveries.${deliveryIdx}.products.0.product`, val);
formik.setFieldTouched(
`deliveries.${deliveryIdx}.products.0.product_id`,
true
);
formik.setFieldValue(
setFieldTouched(`deliveries.${deliveryIdx}.products.0.product`, true);
setFieldValue(`deliveries.${deliveryIdx}.products.0.product`, val);
setFieldTouched(`deliveries.${deliveryIdx}.products.0.product_id`, true);
setFieldValue(
`deliveries.${deliveryIdx}.products.0.product_id`,
(val as OptionType)?.value
);
},
[]
[setFieldTouched, setFieldValue]
);
const handleDeliverySupplierChange = useCallback(
(deliveryIdx: number, val: OptionType | OptionType[] | null) => {
formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier`, true);
formik.setFieldValue(`deliveries.${deliveryIdx}.supplier`, val);
formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier_id`, true);
formik.setFieldValue(
setFieldTouched(`deliveries.${deliveryIdx}.supplier`, true);
setFieldValue(`deliveries.${deliveryIdx}.supplier`, val);
setFieldTouched(`deliveries.${deliveryIdx}.supplier_id`, true);
setFieldValue(
`deliveries.${deliveryIdx}.supplier_id`,
(val as OptionType)?.value
);
},
[]
[setFieldTouched, setFieldValue]
);
const handleDeliveryDocumentChange = useCallback(
@@ -723,15 +741,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
e.target.value = '';
return;
}
formik.setFieldValue(`deliveries.${deliveryIdx}.document`, file);
setFieldValue(`deliveries.${deliveryIdx}.document`, file);
}
},
[]
[setFieldValue]
);
const handleDeliveryCostChange = useCallback(
(idx: number, value: number) => {
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value);
setFieldValue(`deliveries.${idx}.delivery_cost`, value);
const delivery = formik.values.deliveries?.[idx];
if (delivery) {
@@ -741,21 +759,18 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
);
if (productQty > 0 && value > 0) {
const perItem = value / productQty;
formik.setFieldValue(
`deliveries.${idx}.delivery_cost_per_item`,
perItem
);
setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, perItem);
} else if (value === 0) {
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
}
}
},
[formik.values.deliveries]
[formik.values.deliveries, setFieldValue]
);
const handleDeliveryCostPerItemChange = useCallback(
(idx: number, value: number) => {
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, value);
setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, value);
const delivery = formik.values.deliveries?.[idx];
if (delivery) {
@@ -765,13 +780,13 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
);
if (productQty > 0 && value > 0) {
const totalCost = value * productQty;
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
} else if (value === 0) {
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, 0);
setFieldValue(`deliveries.${idx}.delivery_cost`, 0);
}
}
},
[formik.values.deliveries]
[formik.values.deliveries, setFieldValue]
);
const handleDeliveryCostChangeWrapper = useCallback(
@@ -838,22 +853,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
}, [formik.values.products, formik.values.deliveries]);
const getAvailableStock = useCallback(
(productId: number) => {
if (type === 'detail') return 0;
const productWarehouse = productWarehouseOptions.find(
(pw) => pw.product_id === productId
);
return (
productWarehouse?.transfer_available_qty ??
productWarehouse?.quantity ??
0
);
},
[productWarehouseOptions, type]
);
const getTotalStock = useCallback(
(productId: number) => {
if (type === 'detail') return 0;
const productWarehouse = productWarehouseOptions.find(
@@ -864,16 +863,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
[productWarehouseOptions, type]
);
const hasAvailableQty = useCallback(
(productId: number) => {
const productWarehouse = productWarehouseOptions.find(
(pw) => pw.product_id === productId
);
return productWarehouse?.transfer_available_qty !== undefined;
},
[productWarehouseOptions]
);
const getProductQtyBottomLabel = useCallback(
(productIdx: number) => {
if (type === 'detail') return undefined;
@@ -881,31 +870,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
if (!product || !product.product_id) return undefined;
const availableStock = getAvailableStock(product.product_id);
const totalStock = getTotalStock(product.product_id);
const requestedQty = Number(product.product_qty) || 0;
const remainingStock = availableStock - requestedQty;
const isAyamProduct = hasAvailableQty(product.product_id);
if (requestedQty > 0) {
if (isAyamProduct) {
return `Sisa: ${formatNumber(remainingStock)} (Total: ${formatNumber(totalStock)})`;
}
return `Sisa: ${formatNumber(remainingStock)}`;
}
if (isAyamProduct) {
return `Tersedia: ${formatNumber(availableStock)} (Total: ${formatNumber(totalStock)})`;
}
return `Tersedia: ${formatNumber(availableStock)}`;
},
[
formik.values.products,
getAvailableStock,
getTotalStock,
hasAvailableQty,
type,
]
[formik.values.products, getAvailableStock, type]
);
const getDeliveryProductQtyBottomLabel = useCallback(
@@ -967,26 +941,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
if (!product || !product.product_id) return null;
const availableStock = getAvailableStock(product.product_id);
const totalStock = getTotalStock(product.product_id);
const requestedQty = Number(product.product_qty) || 0;
const isAyamProduct = hasAvailableQty(product.product_id);
if (requestedQty > availableStock) {
if (isAyamProduct) {
return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)} (Total: ${formatNumber(totalStock)}, terpakai untuk chickin: ${formatNumber(totalStock - availableStock)})`;
}
return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)}`;
}
return null;
},
[
formik.values.products,
getAvailableStock,
getTotalStock,
hasAvailableQty,
type,
]
[formik.values.products, getAvailableStock, type]
);
const validateDeliveryQty = useCallback(
@@ -1100,12 +1063,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
return !validateDeliveryQty(deliveryIdx, productIdx, qty);
})
) ?? []),
[
formik.values.deliveries,
formik.values.products,
validateDeliveryQty,
type,
]
[formik.values.deliveries, validateDeliveryQty, type]
);
const hasInvalidQty = useMemo(
@@ -1122,6 +1080,27 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
);
}, [formik.values.products, getProductQtyError, type]);
const deliveryCostDepString = useMemo(
() =>
formik.values.deliveries
?.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('|'),
[formik.values.deliveries]
);
// ===== EFFECTS =====
useEffect(() => {
formik.values.deliveries?.forEach((delivery, idx) => {
@@ -1138,36 +1117,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
if (deliveryCost > 0 && productQty > 0) {
const perItem = deliveryCost / productQty;
if (Math.abs(deliveryCostPerItem - perItem) > 0.01) {
formik.setFieldValue(
`deliveries.${idx}.delivery_cost_per_item`,
perItem
);
setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, perItem);
}
} else if (deliveryCostPerItem > 0 && productQty > 0) {
const totalCost = deliveryCostPerItem * productQty;
if (Math.abs(deliveryCost - totalCost) > 0.01) {
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
}
}
});
}, [
formik.values.deliveries
?.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('|'),
]);
}, [deliveryCostDepString, setFieldValue, formik.values.deliveries]);
useEffect(() => {
if (
@@ -1177,7 +1136,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
!isInitialized
) {
if (formik.values.products.length === 0) {
formik.setFieldValue('products', [
setFieldValue('products', [
{
product: null,
product_id: 0,
@@ -1186,7 +1145,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
]);
}
if (formik.values.deliveries.length === 0) {
formik.setFieldValue('deliveries', [
setFieldValue('deliveries', [
{
delivery_cost: undefined,
delivery_cost_per_item: undefined,
@@ -1208,7 +1167,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
}
setIsInitialized(true);
}
}, [formik.values.source_warehouse_id, isInitialized, type]);
}, [
formik.values.source_warehouse_id,
isInitialized,
type,
setFieldValue,
formik.values.products.length,
formik.values.deliveries.length,
]);
useEffect(() => {
if (
@@ -1217,7 +1183,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
formik.values.source_warehouse_id ===
formik.values.destination_warehouse_id
) {
formik.setFieldError(
setFieldError(
'destination_warehouse_id',
'Gudang tujuan tidak boleh sama dengan gudang asal!'
);
@@ -1226,13 +1192,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
formik.errors.destination_warehouse_id ===
'Gudang tujuan tidak boleh sama dengan gudang asal!'
) {
formik.setFieldError('destination_warehouse_id', undefined);
setFieldError('destination_warehouse_id', undefined);
}
}
}, [
formik.values.source_warehouse_id,
formik.values.destination_warehouse_id,
formik.errors.destination_warehouse_id,
setFieldError,
]);
useEffect(() => {
@@ -1268,29 +1235,37 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
);
if (hasChanges) {
formik.setFieldValue('deliveries', updatedDeliveries);
setFieldValue('deliveries', updatedDeliveries);
}
}
}, [formik.values.products]);
}, [formik.values.products, formik.values.deliveries, setFieldValue]);
const productQtyDepString = useMemo(
() => formik.values.products?.map((p) => p.product_qty).join(','),
[formik.values.products]
);
useEffect(() => {
if (productQtyErrorShown) {
toast.dismiss();
setProductQtyErrorShown(false);
}
}, [formik.values.products?.map((p) => p.product_qty).join(',')]);
}, [productQtyErrorShown]);
const deliveryProductQtyDepString = useMemo(
() =>
formik.values.deliveries
?.map((d) => d.products.map((p) => p.product_qty).join(','))
.join('|'),
[formik.values.deliveries]
);
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(','),
]);
}, [deliveryProductQtyDepString, productQtyDepString, deliveryQtyErrorShown]);
useEffect(() => {
if (hasExceededStock && !productQtyErrorShown && type !== 'detail') {
@@ -199,9 +199,6 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
'yyyy-MM-DD'
),
vehicle_number: product.vehicle_number,
weight_per_convertion: parseFloat(
String(product.weight_per_convertion ?? 0)
),
};
}
})
@@ -371,9 +368,7 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
const currentProducts = deliveryOrderValues?.find(
(product) => product.id == id
);
setSelectedDeliveryProduct(currentProducts ?? values ?? null);
setSelectedDeliveryProduct(values ?? currentProducts ?? null);
if (id) {
setStep(2);
}
@@ -435,9 +430,6 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
'yyyy-MM-DD'
),
vehicle_number: product.vehicle_number,
weight_per_convertion: parseFloat(
String(product.weight_per_convertion ?? 0)
),
};
}
})
@@ -544,9 +536,13 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
formModal.closeModal();
};
const hasLoadedInitialValues = useRef(false);
useEffect(() => {
const getFilledInitialValues = async () => {
if (marketingId && isResponseSuccess(marketing)) {
if (hasLoadedInitialValues.current) return;
hasLoadedInitialValues.current = true;
const filledInitialValues = await getFilledMarketingFormInitialValues(
marketing.data
);
@@ -590,9 +586,15 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
setFormErrorMessage('');
}, [step]);
// sync delivery order values to formik
const prevDeliveryOrderValuesRef = useRef(deliveryOrderValues);
useEffect(() => {
formik.setFieldValue('delivery_order', deliveryOrderValues);
if (
JSON.stringify(prevDeliveryOrderValuesRef.current) !==
JSON.stringify(deliveryOrderValues)
) {
prevDeliveryOrderValuesRef.current = deliveryOrderValues;
formik.setFieldValue('delivery_order', deliveryOrderValues);
}
}, [deliveryOrderValues]);
const grandTotal = useMemo(() => {
@@ -10,14 +10,9 @@ import SelectInput, {
useSelect,
} from '@/components/input/SelectInput';
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
import {
MarketingFilterFormValues,
MarketingFilterSchema,
} from '@/components/pages/marketing/filter/MarketingFilter';
import { MarketingFilter } from '@/types/api/marketing/marketing';
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
import { MarketingApi } from '@/services/api/marketing/marketing';
import { CustomerApi } from '@/services/api/master-data';
import { isResponseSuccess } from '@/lib/api-helper';
import { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing';
@@ -42,12 +37,9 @@ const MarketingFilterModal = ({
isLoadingOptions: isLoadingProductsOptions,
setInputValue: setProductsInputValue,
loadMore: loadMoreProducts,
} = useSelect<BaseMarketing>(
MarketingApi.basePath,
'id',
'so_number',
'search'
);
} = useSelect<BaseMarketing>(MarketingApi.basePath, 'id', 'so_number', '', {
limit: 'limit',
});
const productsOptions = useMemo(() => {
if (!productsRawData || !isResponseSuccess(productsRawData)) return [];
@@ -74,10 +66,19 @@ const MarketingFilterModal = ({
isLoadingOptions: isLoadingCustomersOptions,
setInputValue: setCustomersInputValue,
loadMore: loadMoreCustomers,
} = useSelect(CustomerApi.basePath, 'id', 'name', 'search', {
has_marketing: 'true',
} = useSelect(MarketingApi.basePath, 'customer.id', 'customer.name', '', {
limit: 'limit',
});
const uniqueCustomersOptions = useMemo(() => {
const seen = new Set();
return customersOptions.filter((customer) => {
if (seen.has(customer.value)) return false;
seen.add(customer.value);
return true;
});
}, [customersOptions]);
const statusOptions = [
...MARKETING_APPROVAL_LINE.map((item) => ({
value: item.step_name.split(' ').join('_').toUpperCase(),
@@ -86,19 +87,23 @@ const MarketingFilterModal = ({
{ value: 'DITOLAK', label: 'Ditolak' },
];
const formik = useFormik<MarketingFilterFormValues>({
const formik = useFormik<{
product_ids: OptionType[];
status: OptionType | null;
customer_id: OptionType | null;
}>({
initialValues: {
product_ids: [],
status: null,
customer: null,
customer_id: null,
},
validationSchema: MarketingFilterSchema,
onSubmit: async (values) => {
const formattedValues: MarketingFilter = {
const formattedValues = {
...values,
product_ids: values.product_ids.map((item) => Number(item.value)),
status: values.status?.value.toString() || '',
customer_id: Number(values.customer?.value),
customer_id: Number(values.customer_id?.value),
};
onSubmit?.(formattedValues);
@@ -116,10 +121,7 @@ const MarketingFilterModal = ({
};
const customerChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue(
'customer',
!Array.isArray(val) ? (val as OptionType<number> | null) : null
);
formik.setFieldValue('customer_id', val as OptionType);
};
const statusChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -185,9 +187,9 @@ const MarketingFilterModal = ({
label='Customer'
isClearable
placeholder='Pilih customer'
options={customersOptions}
options={uniqueCustomersOptions}
isLoading={isLoadingCustomersOptions}
value={formik.values.customer}
value={formik.values.customer_id}
onChange={customerChangeHandler}
onInputChange={setCustomersInputValue}
onMenuScrollToBottom={loadMoreCustomers}
@@ -226,11 +226,6 @@ const MarketingTable = () => {
confirmationModal.openModal();
};
const productsClickHandler = (item: Marketing) => {
setSelectedItem(item);
productsModal.openModal();
};
const deleteMarketingHandler = async () => {
const deleteMarketingRes = await MarketingApi.delete(
selectedItem?.id as number
@@ -450,6 +445,11 @@ const MarketingTable = () => {
accessorKey: 'marketing_products.length',
header: 'Product Details',
cell: (props) => {
const productsClickHandler = (item: Marketing) => {
setSelectedItem(item);
productsModal.openModal();
};
if (props?.row?.original?.sales_order?.length) {
if (props?.row?.original?.sales_order?.length > 1) {
return (
@@ -504,7 +504,7 @@ const MarketingTable = () => {
},
},
];
}, []);
}, [deleteModal, deliveryModal, setSelectedItem, productsModal]);
return (
<>
@@ -746,7 +746,7 @@ const MarketingTable = () => {
}
columns={[
{
header: 'Gudang Fisik',
header: 'Kandang',
accessorFn(row) {
return row.product_warehouse.warehouse.name;
},
@@ -195,9 +195,7 @@ const SalesOrderFormModal = ({
product.marketing_type?.value?.toLowerCase() === 'telur'
? convertionUnitValue === 'PETI'
? 'PETI'
: convertionUnitValue === 'QTY'
? 'QTY'
: 'KG'
: 'KG' // termasuk "QTY" dan "KG"
: undefined;
// Jika value dari data product ada week, kirim "AYAM_PULLET, jika tidak ada kirim "AYAM"
@@ -209,6 +207,7 @@ const SalesOrderFormModal = ({
return {
vehicle_number: product.vehicle_number as string,
kandang_id: product.kandang_id as number,
product_warehouse_id: product.product_warehouse_id as number,
unit_price: parseFloat(String(product.unit_price || 0)),
total_weight: parseFloat(String(product.total_weight || 0)),
@@ -459,9 +458,13 @@ const SalesOrderFormModal = ({
);
}, [memoSalesOrder]);
const hasLoadedInitialValues = useRef(false);
useEffect(() => {
const getFilledInitialValues = async () => {
if (marketingId && isResponseSuccess(marketing)) {
if (hasLoadedInitialValues.current) return;
hasLoadedInitialValues.current = true;
const filledInitialValues = await getFilledMarketingFormInitialValues(
marketing.data
);
@@ -1,14 +0,0 @@
import { array, mixed, object } from 'yup';
import { OptionType } from '@/components/input/SelectInput';
export const MarketingFilterSchema = object({
product_ids: array().of(mixed<OptionType<number>>().required()).required(),
status: mixed<OptionType<string>>().nullable(),
customer: mixed<OptionType<number>>().nullable(),
});
export type MarketingFilterFormValues = {
product_ids: OptionType<number>[];
status: OptionType<string> | null;
customer: OptionType<number> | null;
};
@@ -13,7 +13,6 @@ import {
Marketing,
} from '@/types/api/marketing/marketing';
import { formatDate, formatTitleCase } from '@/lib/helper';
import { getProductWarehouseOptionLabel } from '@/lib/product-warehouse';
type MarketingSchemaType = {
customer_id: number | undefined;
@@ -98,21 +97,17 @@ export type DeliveryOrderFormValues = Yup.InferType<typeof DeliveryOrderSchema>;
export const SalesProductToFieldValues = (
product: BaseSalesOrder
): SalesOrderProductFormValues => {
const warehouseOption = {
value: product.product_warehouse.warehouse.id,
label: product.product_warehouse.warehouse.name,
};
return {
id: product.id,
vehicle_number: product.vehicle_number,
warehouse_id: product.product_warehouse.warehouse.id,
warehouse: warehouseOption,
kandang_id: product.product_warehouse.warehouse.id,
kandang: warehouseOption,
kandang: {
value: product.product_warehouse.warehouse.id,
label: product.product_warehouse.warehouse.name,
},
product_warehouse: {
value: product.product_warehouse.id,
label: getProductWarehouseOptionLabel(product.product_warehouse),
label: product.product_warehouse.product.name,
},
product_warehouse_data: product.product_warehouse,
product_warehouse_id: product.product_warehouse.id,
@@ -144,34 +139,11 @@ export const DeliveryProductToFieldValues = (
delivery: BaseDeliveryOrder
): DeliveryOrderProductFormValues[] => {
const data = delivery.deliveries.map((item) => {
const salesOrder = salesOrders.find(
const soId = salesOrders.find(
(so) => so.product_warehouse.id === item.product_warehouse.id
);
const warehouseOption = {
value: item.product_warehouse.warehouse.id,
label: item.product_warehouse.warehouse.name,
};
const initialSisaBerat =
item?.total_weight &&
salesOrder?.weight_per_convertion &&
salesOrder?.total_peti
? Number(item.total_weight) -
Number(salesOrder.weight_per_convertion) *
Number(salesOrder.total_peti)
: 0;
const initialPricePerConvertion =
item?.total_price &&
salesOrder?.total_peti &&
Number(salesOrder.total_peti) !== 0
? (Number(item.total_price) -
initialSisaBerat * Number(item.unit_price || 0)) /
Number(salesOrder.total_peti)
: Number(item?.unit_price || 0);
)?.id;
return {
id: salesOrder?.id,
id: soId,
unit_price: item.unit_price,
total_weight: item.total_weight,
qty: item.qty,
@@ -180,31 +152,19 @@ export const DeliveryProductToFieldValues = (
vehicle_number: item.vehicle_number,
delivery_date: formatDate(delivery.delivery_date, 'yyyy-MM-DD'),
do_number: delivery.do_number,
marketing_product_id: salesOrder?.id,
marketing_type: salesOrder?.marketing_type
? {
value: salesOrder?.marketing_type,
label: formatTitleCase(salesOrder?.marketing_type),
}
: null,
convertion_unit: salesOrder?.convertion_unit
? {
value: salesOrder?.convertion_unit.toLowerCase(),
label: formatTitleCase(salesOrder?.convertion_unit),
}
: null,
marketing_product_id: soId,
marketing_product: {
id: salesOrder?.id,
id: soId,
vehicle_number: item.vehicle_number,
warehouse_id: item.product_warehouse.warehouse.id,
warehouse: warehouseOption,
kandang_id: item.product_warehouse.warehouse.id,
kandang: warehouseOption,
kandang: {
value: item.product_warehouse.warehouse.id,
label: item.product_warehouse.warehouse.name,
},
product_warehouse: {
value: item.product_warehouse.id,
label: getProductWarehouseOptionLabel(item.product_warehouse),
label: item.product_warehouse.product.name,
},
product_warehouse_data: item.product_warehouse,
product_warehouse_id: item.product_warehouse.id,
unit_price: item.unit_price,
total_weight: item.total_weight,
@@ -212,13 +172,8 @@ export const DeliveryProductToFieldValues = (
avg_weight: item.avg_weight,
total_price: item.total_price,
},
total_peti: salesOrder?.total_peti,
weight_per_convertion:
item?.weight_per_convertion ?? salesOrder?.weight_per_convertion ?? 0,
price_per_convertion: initialPricePerConvertion,
} as DeliveryOrderProductFormValues;
});
return data;
};
export const mergeSOwithDO = (
@@ -226,25 +181,10 @@ export const mergeSOwithDO = (
deliveryOrders: DeliveryOrderProductFormValues[],
autofill?: boolean
): DeliveryOrderProductFormValues[] => {
const hasDeliveryOrders = deliveryOrders.length > 0;
return salesOrders.map((so) => {
const delivery = deliveryOrders.find(
(d) => d?.marketing_product_id === so.id
);
const isTelurQty =
so.marketing_type?.value?.toLowerCase() === 'telur' &&
so.convertion_unit?.value?.toLowerCase() === 'qty';
const salesOrderUnitPrice =
isTelurQty && Number(so.total_price || 0) > 0 && Number(so.qty || 0) > 0
? Number(so.total_price) / Number(so.qty)
: so.unit_price;
const salesOrderPricePerQty =
isTelurQty &&
Number(so.total_price || 0) > 0 &&
Number(so.total_weight || 0) > 0
? Number(so.total_price) / Number(so.total_weight)
: so.price_per_qty;
return {
...so, // nilai dasar dari sales order
@@ -252,50 +192,30 @@ export const mergeSOwithDO = (
delivery_date: delivery?.delivery_date || undefined,
do_number: delivery?.do_number || undefined,
vehicle_number: delivery?.vehicle_number || so.vehicle_number,
unit_price:
autofill && hasDeliveryOrders
? delivery?.unit_price
: salesOrderUnitPrice,
total_weight:
autofill && hasDeliveryOrders
? delivery?.total_weight
: so.total_weight,
qty: autofill && hasDeliveryOrders ? delivery?.qty : so.qty,
avg_weight:
autofill && hasDeliveryOrders ? delivery?.avg_weight : so.avg_weight,
total_price:
autofill && hasDeliveryOrders ? delivery?.total_price : so.total_price,
unit_price: autofill ? so.unit_price : delivery?.unit_price,
total_weight: autofill ? so.total_weight : delivery?.total_weight,
qty: autofill ? so.qty : delivery?.qty,
avg_weight: autofill ? so.avg_weight : delivery?.avg_weight,
total_price: autofill ? so.total_price : delivery?.total_price,
marketing_product: so, // jika ada, override
uom: autofill && hasDeliveryOrders ? delivery?.uom : so.uom,
weight_per_convertion:
autofill && hasDeliveryOrders
? delivery?.weight_per_convertion
: so.weight_per_convertion,
price_per_convertion:
autofill && hasDeliveryOrders
? delivery?.price_per_convertion
: so.price_per_convertion,
convertion_unit:
autofill && hasDeliveryOrders
? delivery?.convertion_unit
: so.convertion_unit,
marketing_type:
autofill && hasDeliveryOrders
? delivery?.marketing_type
: so.marketing_type,
total_peti:
autofill && hasDeliveryOrders ? delivery?.total_peti : so.total_peti,
price_per_qty:
autofill && hasDeliveryOrders
? delivery?.price_per_qty
: salesOrderPricePerQty,
sisa_berat:
autofill && hasDeliveryOrders ? delivery?.sisa_berat : so.sisa_berat,
price_sisa_berat:
autofill && hasDeliveryOrders
? delivery?.price_sisa_berat
: so.price_sisa_berat,
week: autofill && hasDeliveryOrders ? delivery?.week : so.week,
uom: autofill ? so.uom : delivery?.uom,
weight_per_convertion: autofill
? so.weight_per_convertion
: delivery?.weight_per_convertion,
price_per_convertion: autofill
? so.price_per_convertion
: delivery?.price_per_convertion,
convertion_unit: autofill
? so.convertion_unit
: delivery?.convertion_unit,
marketing_type: autofill ? so.marketing_type : delivery?.marketing_type,
total_peti: autofill ? so.total_peti : delivery?.total_peti,
price_per_qty: autofill ? so.price_per_qty : delivery?.price_per_qty,
sisa_berat: autofill ? so.sisa_berat : delivery?.sisa_berat,
price_sisa_berat: autofill
? so.price_sisa_berat
: delivery?.price_sisa_berat,
week: autofill ? so.week : delivery?.week,
} as DeliveryOrderProductFormValues;
});
};
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
DeliveryOrderProductFormValues,
DeliveryOrderProductSchema,
@@ -32,63 +32,6 @@ import Dropdown from '@/components/Dropdown';
import { Icon } from '@iconify/react';
import { handleMarketingCalculation } from '@/lib/marketing-calculation';
type PricingOption =
| string
| {
value: string;
label: string;
}
| null
| undefined;
type PricingSource =
| {
marketing_type?: PricingOption;
convertion_unit?: PricingOption;
total_price?: string | number | null;
qty?: string | number | null;
total_weight?: string | number | null;
unit_price?: string | number | null;
price_per_qty?: number | null;
}
| null
| undefined;
const getOptionValue = (value?: PricingOption) => {
if (!value) return undefined;
if (typeof value === 'string') return value.toLowerCase();
return value.value?.toLowerCase();
};
const isTelurQtyProduct = (value?: PricingSource) =>
getOptionValue(value?.marketing_type) === 'telur' &&
getOptionValue(value?.convertion_unit) === 'qty';
const getDisplayedUnitPrice = (value?: PricingSource) => {
if (
isTelurQtyProduct(value) &&
Number(value?.total_price || 0) > 0 &&
Number(value?.qty || 0) > 0
) {
return Number(value?.total_price) / Number(value?.qty);
}
return value?.unit_price ?? undefined;
};
const getDisplayedPricePerQty = (value?: PricingSource) => {
if (
isTelurQtyProduct(value) &&
Number(value?.total_price || 0) > 0 &&
Number(value?.total_weight || 0) > 0
) {
return Number(value?.total_price) / Number(value?.total_weight);
}
return value?.price_per_qty ?? null;
};
const DeliveryOrderProductForm = ({
formState,
salesOrders,
@@ -133,7 +76,7 @@ const DeliveryOrderProductForm = ({
? (Number(initialValues.total_price) -
initialSisaBerat * Number(initialValues.unit_price || 0)) /
Number(initialValues.total_peti)
: Number(initialValues?.unit_price || 0);
: 0;
const initialPriceSisaBerat =
initialValues?.total_price && initialValues?.total_peti
@@ -169,7 +112,7 @@ const DeliveryOrderProductForm = ({
if (!Boolean(item.qty)) {
return {
value: item.id,
label: `${item.marketing_product?.product_warehouse?.label} - ${item.marketing_product?.warehouse?.label ?? item.marketing_product?.kandang?.label}`,
label: `${item.marketing_product?.product_warehouse?.label} - ${item.marketing_product?.kandang?.label}`,
} as OptionType;
} else {
return null;
@@ -211,27 +154,6 @@ const DeliveryOrderProductForm = ({
(item) => item.id === initialValues?.marketing_product_id
);
const defaultPricingSource: PricingSource = {
marketing_type:
initialValues?.marketing_type ?? salesOrder?.marketing_type ?? null,
convertion_unit:
initialValues?.convertion_unit ?? salesOrder?.convertion_unit ?? null,
total_price:
deliveryOrder?.total_price ??
initialValues?.total_price ??
salesOrder?.total_price,
qty: deliveryOrder?.qty ?? initialValues?.qty ?? salesOrder?.qty,
total_weight:
deliveryOrder?.total_weight ??
initialValues?.total_weight ??
salesOrder?.total_weight,
unit_price:
deliveryOrder?.unit_price ??
initialValues?.unit_price ??
salesOrder?.unit_price,
price_per_qty: initialValues?.price_per_qty ?? null,
};
const formik = useFormik<DeliveryOrderProductFormValues>({
enableReinitialize: true,
initialValues: {
@@ -245,7 +167,8 @@ const DeliveryOrderProductForm = ({
undefined,
marketing_product_id:
salesOrder?.id || initialValues?.marketing_product_id || undefined,
unit_price: getDisplayedUnitPrice(defaultPricingSource),
unit_price:
deliveryOrder?.unit_price ?? initialValues?.unit_price ?? undefined,
total_weight:
deliveryOrder?.total_weight ?? initialValues?.total_weight ?? undefined,
qty: deliveryOrder?.qty ?? initialValues?.qty ?? undefined,
@@ -263,7 +186,7 @@ const DeliveryOrderProductForm = ({
convertion_unit: initialValues?.convertion_unit || null,
marketing_type: initialValues?.marketing_type || null,
total_peti: initialValues?.total_peti ?? null,
price_per_qty: getDisplayedPricePerQty(defaultPricingSource),
price_per_qty: initialValues?.price_per_qty ?? null,
sisa_berat: initialSisaBerat,
price_sisa_berat: initialPriceSisaBerat,
week: initialValues?.week ?? null,
@@ -301,6 +224,8 @@ const DeliveryOrderProductForm = ({
},
});
const { resetForm } = formik;
const hasWeekField = useMemo(() => {
const marketingType = formik.values.marketing_type?.value?.toLowerCase();
if (marketingType === 'ayam_pullet') {
@@ -320,9 +245,9 @@ const DeliveryOrderProductForm = ({
return false;
}, [formik.values.marketing_product, formik.values.marketing_type]);
const handleResetForm = () => {
const handleResetForm = useCallback(() => {
setFormErrorMessage('');
formik.resetForm({
resetForm({
values: {
delivery_date: '',
vehicle_number: '',
@@ -346,17 +271,20 @@ const DeliveryOrderProductForm = ({
},
});
// setSelectedProduct(null);
};
}, [resetForm]);
const handleBlurField = (field: string) => {
setCurrentInput(field);
const handleBlurField = useCallback(
(field: string) => {
setCurrentInput(field);
handleMarketingCalculation(field, {
values: formik.values,
setFieldValue: formik.setFieldValue,
hasSisaBerat,
});
};
handleMarketingCalculation(field, {
values: formik.values,
setFieldValue: formik.setFieldValue,
hasSisaBerat,
});
},
[formik.values, formik.setFieldValue, hasSisaBerat]
);
// Handler untuk onChange - auto calculation real-time untuk field yang mempengaruhi total_price (total_peti, weight_per_convertion, price_per_convertion, sisa_berat, price_sisa_berat, price_per_qty, qty)
const handleFieldChange = (
@@ -401,25 +329,25 @@ const DeliveryOrderProductForm = ({
const { setValues: setFormikValues } = formik;
const processedInitialValuesRef = useRef<number | null>(null);
useEffect(() => {
if (initialValues) {
if (processedInitialValuesRef.current === initialValues.id) return;
processedInitialValuesRef.current = initialValues.id as number;
if (!Boolean(initialValues.qty)) {
handleResetForm();
} else {
setFormikValues({
...initialValues,
unit_price: getDisplayedUnitPrice(initialValues),
price_per_qty: getDisplayedPricePerQty(initialValues),
});
setFormikValues(initialValues);
if (initialValues?.marketing_product_id) {
setSelectedProduct({
value: initialValues?.id,
label: `${initialValues?.marketing_product?.product_warehouse?.label} - ${initialValues?.marketing_product?.warehouse?.label ?? initialValues?.marketing_product?.kandang?.label}`,
label: `${initialValues?.marketing_product?.product_warehouse?.label} - ${initialValues?.marketing_product?.kandang?.label}`,
} as OptionType);
}
}
}
}, [initialValues]);
}, [handleResetForm, initialValues, setFormikValues]);
// ===== Formik Error List =====
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(
@@ -437,8 +365,10 @@ const DeliveryOrderProductForm = ({
);
useEffect(() => {
handleBlurField('week');
}, [formik.values.week]);
if (formik.values.week) {
handleBlurField('week');
}
}, [formik.values.week, handleBlurField]);
return (
<>
@@ -539,11 +469,10 @@ const DeliveryOrderProductForm = ({
marketing_product_id: selected.value as number,
marketing_product: soFieldValues,
qty: so.qty,
unit_price: getDisplayedUnitPrice(so),
unit_price: so.unit_price,
total_price: so.total_price,
avg_weight: so.avg_weight,
total_weight: so.total_weight,
price_per_qty: getDisplayedPricePerQty(so),
vehicle_number: so.vehicle_number,
week: soFieldValues.week ?? null,
});
@@ -554,11 +483,7 @@ const DeliveryOrderProductForm = ({
text={
exisitingValues?.find(
(item) => item.id === selectedProduct?.value
)?.marketing_product?.warehouse?.label ??
exisitingValues?.find(
(item) => item.id === selectedProduct?.value
)?.marketing_product?.kandang?.label ??
''
)?.marketing_product?.kandang?.label ?? ''
}
color='success'
className={{
@@ -724,7 +649,7 @@ const DeliveryOrderProductForm = ({
placeholder='Masukan Total Peti'
endAdornment={
<div className='flex items-center gap-2'>
<span className='text-sm text-base-content/50'>Peti</span>
<span className='text-sm text-base-content/50'>Kg</span>
</div>
}
bottomLabel={`1 ${formik.values.convertion_unit?.value.toLowerCase()} = ${formik.values.weight_per_convertion ?? 0} Kg`}
@@ -774,9 +699,6 @@ const DeliveryOrderProductForm = ({
}
errorMessage={formik.errors.total_weight}
placeholder='Masukan Total Bobot'
disabled={
formik.values.convertion_unit?.value.toLowerCase() === 'peti'
}
/>
)}
@@ -846,32 +768,12 @@ const DeliveryOrderProductForm = ({
/>
)}
{/* Harga Satuan */}
{formik.values.convertion_unit?.value.toLowerCase() !== 'peti' &&
formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && (
<NumberInput
required
label={`Harga / ${formik.values.convertion_unit?.label.toLowerCase() !== 'qty' ? 'Kg' : 'Butir'} (Rp)`}
name='unit_price'
value={formik.values.unit_price}
onChange={(e) => {
const value = Number(e.target.value);
handleFieldChange('unit_price', value, () =>
setCurrentInput(e.target.name)
);
}}
isError={Boolean(formik.errors.unit_price)}
errorMessage={formik.errors.unit_price}
placeholder='Masukan Harga Satuan'
/>
)}
{/* Harga per kg untuk TELUR + QTY */}
{/* Harga per butir untuk TELUR + QTY */}
{formik.values.marketing_type?.value.toLowerCase() === 'telur' &&
formik.values.convertion_unit?.value.toLowerCase() === 'qty' && (
<NumberInput
required
label='Harga / Kg (Rp)'
label='Harga / Butir (Rp)'
name='price_per_qty'
value={formik.values.price_per_qty ?? undefined}
onChange={(e) => {
@@ -885,7 +787,27 @@ const DeliveryOrderProductForm = ({
Boolean(formik.errors.price_per_qty)
}
errorMessage={formik.errors.price_per_qty}
placeholder='Masukan Harga per Kg'
placeholder='Masukan Harga per Butir'
/>
)}
{/* Harga Satuan */}
{formik.values.convertion_unit?.value.toLowerCase() !== 'peti' &&
formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && (
<NumberInput
required
label={`Harga / ${isResponseSuccess(productData) ? productData?.data?.uom?.name : 'Produk'} (Rp)`}
name='unit_price'
value={formik.values.unit_price}
onChange={(e) => {
const value = Number(e.target.value);
handleFieldChange('unit_price', value, () =>
setCurrentInput(e.target.name)
);
}}
isError={Boolean(formik.errors.unit_price)}
errorMessage={formik.errors.unit_price}
placeholder='Masukan Harga Satuan'
/>
)}
@@ -3,11 +3,6 @@ import * as Yup from 'yup';
type SalesOrderProductSchemaType = {
id?: number | undefined;
warehouse_id?: number;
warehouse?: {
value: number;
label: string;
} | null;
kandang_id?: number;
kandang?: {
value: number;
@@ -49,22 +44,15 @@ export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaTy
Yup.object({
id: Yup.number(),
vehicle_number: Yup.string().required('Nomor Kendaraan wajib diisi!'),
warehouse: Yup.object({
value: Yup.number()
.min(1, 'Gudang fisik wajib diisi!')
.required('Gudang fisik wajib diisi!'),
label: Yup.string().required('Gudang fisik wajib diisi!'),
}).nullable(),
warehouse_id: Yup.number()
.min(1, 'Gudang fisik 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(),
value: Yup.number()
.min(1, 'Kandang wajib diisi!')
.required('Kandang wajib diisi!'),
label: Yup.string().required('Kandang wajib diisi!'),
}).nullable(),
kandang_id: Yup.number()
.min(1, 'Kandang wajib diisi!')
.required('Kandang wajib diisi!'),
product_warehouse: Yup.object({
value: Yup.number()
.min(1, 'Produk wajib diisi!')
@@ -5,9 +5,9 @@ import {
SalesOrderProductFormValues,
SalesOrderProductSchema,
} from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
import { RefObject, useEffect, useMemo, useState } from 'react';
import { RefObject, useCallback, useEffect, useMemo, useState } from 'react';
import { OptionType, useSelect } from '@/components/input/SelectInput';
import { Warehouse } from '@/types/api/master-data/warehouse';
import { Kandang } from '@/types/api/master-data/kandang';
import { WarehouseApi } from '@/services/api/master-data';
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
import { ProductWarehouseApi } from '@/services/api/inventory';
@@ -31,7 +31,6 @@ import {
import { Icon } from '@iconify/react';
import Dropdown from '@/components/Dropdown';
import { handleMarketingCalculation } from '@/lib/marketing-calculation';
import { getProductWarehouseOptionLabel } from '@/lib/product-warehouse';
const SalesOrderProductForm = ({
initialValues,
@@ -68,25 +67,7 @@ const SalesOrderProductForm = ({
? (Number(initialValues.total_price) -
initialSisaBerat * Number(initialValues.unit_price || 0)) /
Number(initialValues.total_peti)
: Number(initialValues?.unit_price || 0);
const isInitialTelurQty =
initialValues?.marketing_type?.value?.toLowerCase() === 'telur' &&
initialValues?.convertion_unit?.value?.toLowerCase() === 'qty';
const initialUnitPrice =
isInitialTelurQty &&
Number(initialValues?.total_price || 0) > 0 &&
Number(initialValues?.qty || 0) > 0
? Number(initialValues?.total_price) / Number(initialValues?.qty)
: initialValues?.unit_price || '';
const initialPricePerQty =
isInitialTelurQty &&
Number(initialValues?.total_price || 0) > 0 &&
Number(initialValues?.total_weight || 0) > 0
? Number(initialValues?.total_price) / Number(initialValues?.total_weight)
: (initialValues?.price_per_qty ?? null);
: 0;
const initialPriceSisaBerat =
initialValues?.total_price && initialValues?.total_peti
@@ -103,15 +84,11 @@ const SalesOrderProductForm = ({
enableReinitialize: true,
initialValues: {
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: initialValues?.kandang || null,
product_warehouse: initialValues?.product_warehouse || null,
product_warehouse_data: initialValues?.product_warehouse_data || null,
product_warehouse_id: initialValues?.product_warehouse_id || undefined,
unit_price: initialUnitPrice,
unit_price: initialValues?.unit_price || '',
total_weight: initialValues?.total_weight || '',
qty: initialValues?.qty || '',
avg_weight: initialValues?.avg_weight || '',
@@ -125,7 +102,7 @@ const SalesOrderProductForm = ({
convertion_unit: initialValues?.convertion_unit || null,
marketing_type: initialValues?.marketing_type || null,
total_peti: initialValues?.total_peti ?? null,
price_per_qty: initialPricePerQty,
price_per_qty: initialValues?.price_per_qty ?? null,
sisa_berat: initialSisaBerat,
price_sisa_berat: initialPriceSisaBerat,
week: initialValues?.week ?? null,
@@ -155,11 +132,11 @@ const SalesOrderProductForm = ({
// ===== Options =====
const {
options: warehouseOptions,
isLoadingOptions: isLoadingWarehouseOptions,
setInputValue: setWarehouseSearchValue,
loadMore: loadMoreWarehouses,
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name');
options: kandangSourceOptions,
isLoadingOptions: isLoadingKandangSourceOptions,
setInputValue: setKandangInputValue,
loadMore: loadMoreKandang,
} = useSelect<Kandang>(WarehouseApi.basePath, 'id', 'name');
// Options Week dari minggu 1 - 22
// const optionsWeek = useMemo(() => {
@@ -170,6 +147,7 @@ const SalesOrderProductForm = ({
// }, []);
const {
options: warehouseSourceOptions,
rawData: warehouseSourceRawData,
isLoadingOptions: isLoadingWarehouseSourceOptions,
setInputValue: setWarehouseInputValue,
@@ -178,69 +156,32 @@ const SalesOrderProductForm = ({
ProductWarehouseApi.basePath,
'id',
'product.name',
'search',
'',
{
limit: '100',
available_only: 'true',
warehouse_id: formik.values.warehouse_id?.toString() ?? '',
warehouse_id: formik.values.kandang_id?.toString() ?? '',
type: formik.values.marketing_type?.value.toLocaleUpperCase() ?? '',
}
);
const productOptionsFiltered = useMemo(() => {
if (!isResponseSuccess(warehouseSourceRawData)) {
return initialValues?.product_warehouse
? [initialValues.product_warehouse]
: [];
}
const selectedProductIds = new Set(
exisitingValues
?.filter((item) => item.id !== initialValues?.id)
.map((item) => Number(item.product_warehouse_id))
.filter((item) => item > 0) ?? []
return warehouseSourceOptions.filter(
(product) =>
!exisitingValues
?.map((item) => item.product_warehouse_id)
.includes(product.value)
);
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]);
}, [warehouseSourceOptions, exisitingValues]);
// ===== Handler =====
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
const warehouse = (val as OptionType | null) ?? null;
formik.setFieldValue('warehouse', warehouse);
formik.setFieldValue('warehouse_id', warehouse?.value);
formik.setFieldValue('kandang', warehouse);
formik.setFieldValue('kandang_id', warehouse?.value);
const kandangChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('kandang', val as OptionType);
formik.setFieldValue('kandang_id', (val as OptionType)?.value);
formik.setFieldValue('product_warehouse_id', null);
formik.setFieldValue('product_warehouse', null);
formik.setFieldValue('product_warehouse_data', null);
formik.setFieldValue('qty', '');
};
const productWarehouseChangeHandler = (
val: OptionType | OptionType[] | null
) => {
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('product_warehouse', val as OptionType);
const newId = (val as OptionType)?.value;
formik.setFieldValue('product_warehouse_id', newId);
@@ -250,7 +191,6 @@ const SalesOrderProductForm = ({
(item: ProductWarehouse) => item.id === newId
);
setSelectedProductWarehouse(productWarehouse || null);
formik.setFieldValue('product_warehouse_data', productWarehouse || null);
formik.setFieldValue('qty', productWarehouse?.quantity);
formik.setFieldValue('uom', productWarehouse?.product?.uom?.name || '');
if (
@@ -264,8 +204,6 @@ const SalesOrderProductForm = ({
}
handleBlurField('qty');
} else {
setSelectedProductWarehouse(null);
formik.setFieldValue('product_warehouse_data', null);
formik.setFieldValue('qty', '');
formik.setFieldValue('uom', '');
formik.setFieldValue('week', null);
@@ -279,12 +217,9 @@ const SalesOrderProductForm = ({
formik.resetForm({
values: {
vehicle_number: '',
warehouse_id: undefined,
warehouse: null,
kandang_id: undefined,
kandang: null,
product_warehouse: null,
product_warehouse_data: null,
product_warehouse_id: undefined,
unit_price: '',
total_weight: '',
@@ -305,15 +240,18 @@ const SalesOrderProductForm = ({
});
};
const handleBlurField = (field: string) => {
setCurrentInput(field);
const handleBlurField = useCallback(
(field: string) => {
setCurrentInput(field);
handleMarketingCalculation(field, {
values: formik.values,
setFieldValue: formik.setFieldValue,
hasSisaBerat,
});
};
handleMarketingCalculation(field, {
values: formik.values,
setFieldValue: formik.setFieldValue,
hasSisaBerat,
});
},
[formik.values, formik.setFieldValue, hasSisaBerat]
);
// Handler untuk onChange - auto calculation real-time untuk field yang mempengaruhi total_price (total_peti, weight_per_convertion, price_per_convertion, sisa_berat, price_sisa_berat, price_per_qty, qty)
const handleFieldChange = (
@@ -372,12 +310,10 @@ const SalesOrderProductForm = ({
);
useEffect(() => {
handleBlurField('week');
}, [formik.values.week]);
useEffect(() => {
setSelectedProductWarehouse(initialValues?.product_warehouse_data || null);
}, [initialValues?.product_warehouse_data]);
if (formik.values.week) {
handleBlurField('week');
}
}, [formik.values.week, handleBlurField]);
return (
<>
@@ -417,22 +353,22 @@ const SalesOrderProductForm = ({
errorMessage={formik.errors.vehicle_number}
/>
{/* Gudang Fisik */}
{/* Gudang */}
<SelectInputRadio
required
label='Gudang Fisik'
options={warehouseOptions}
isLoading={isLoadingWarehouseOptions}
value={formik.values.warehouse}
onChange={warehouseChangeHandler}
label='Gudang'
options={kandangSourceOptions}
isLoading={isLoadingKandangSourceOptions}
value={formik.values.kandang}
onChange={kandangChangeHandler}
isClearable
onInputChange={setWarehouseSearchValue}
onMenuScrollToBottom={loadMoreWarehouses}
onInputChange={setKandangInputValue}
onMenuScrollToBottom={loadMoreKandang}
isError={
formik.touched.warehouse_id && Boolean(formik.errors.warehouse_id)
formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
}
errorMessage={formik.errors.warehouse_id}
placeholder='Pilih Gudang Fisik'
errorMessage={formik.errors.kandang_id}
placeholder='Pilih Gudang'
/>
{/* Kategori */}
@@ -443,9 +379,8 @@ const SalesOrderProductForm = ({
value={formik.values.marketing_type}
onChange={(val) => {
formik.setFieldValue('marketing_type', val);
productWarehouseChangeHandler(null);
warehouseChangeHandler(null);
formik.setFieldValue('product_warehouse', null);
formik.setFieldValue('product_warehouse_data', null);
formik.setFieldValue('product_warehouse_id', null);
formik.setFieldValue('convertion_unit', null);
formik.setFieldValue('weight_per_convertion', null);
@@ -462,18 +397,18 @@ const SalesOrderProductForm = ({
options={productOptionsFiltered}
isLoading={isLoadingWarehouseSourceOptions}
value={formik.values.product_warehouse}
onChange={productWarehouseChangeHandler}
onChange={warehouseChangeHandler}
onInputChange={setWarehouseInputValue}
onMenuScrollToBottom={loadMoreWarehouse}
isClearable
placeholder={
formik.values.warehouse_id
formik.values.kandang_id
? productOptionsFiltered.length == 0
? 'Tidak ada produk yang tersedia'
: 'Pilih produk'
: 'Pilih Gudang Fisik Terlebih Dahulu'
: 'Pilih Kandang Terlebih Dahulu'
}
isDisabled={!formik.values.warehouse_id}
isDisabled={!formik.values.kandang_id}
isError={
formik.touched.product_warehouse_id &&
Boolean(formik.errors.product_warehouse_id)
@@ -541,7 +476,7 @@ const SalesOrderProductForm = ({
<input
type='radio'
checked={
formik.values.convertion_unit?.value.toLowerCase() ===
formik.values.convertion_unit?.value ===
option.value
}
onChange={() => null}
@@ -564,9 +499,7 @@ const SalesOrderProductForm = ({
} per ${formik.values.convertion_unit?.value}`}
value={formik.values.weight_per_convertion ?? ''}
onChange={(e) => {
const value = Number(e.target.value)
? Number(e.target.value)
: '';
const value = Number(e.target.value);
handleFieldChange('weight_per_convertion', value, () =>
setCurrentInput(e.target.name)
);
@@ -620,7 +553,7 @@ const SalesOrderProductForm = ({
placeholder='Masukan Total Peti'
endAdornment={
<div className='flex items-center gap-2'>
<span className='text-sm text-base-content/50'>Peti</span>
<span className='text-sm text-base-content/50'>Kg</span>
</div>
}
bottomLabel={`1 ${formik.values.convertion_unit?.value.toLowerCase()} = ${formik.values.weight_per_convertion ?? 0} Kg`}
@@ -670,9 +603,6 @@ const SalesOrderProductForm = ({
}
errorMessage={formik.errors.total_weight}
placeholder='Masukan Total Bobot'
disabled={
formik.values.convertion_unit?.value.toLowerCase() === 'peti'
}
/>
)}
@@ -740,34 +670,12 @@ const SalesOrderProductForm = ({
/>
)}
{/* Harga Satuan per Uom Produk Warehouse */}
{formik.values.convertion_unit?.value.toLowerCase() !== 'peti' &&
formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && (
<NumberInput
required
label={`Harga / ${formik.values.convertion_unit?.label.toLowerCase() !== 'qty' ? 'Kg' : 'Butir'} (Rp)`}
name='unit_price'
value={formik.values.unit_price}
onChange={(e) => {
const value = Number(e.target.value);
handleFieldChange('unit_price', value, () =>
setCurrentInput(e.target.name)
);
}}
isError={
formik.touched.unit_price && Boolean(formik.errors.unit_price)
}
errorMessage={formik.errors.unit_price}
placeholder='Masukan Harga Satuan...'
/>
)}
{/* Harga per kg untuk TELUR + QTY */}
{/* Harga per butir untuk TELUR + QTY */}
{formik.values.marketing_type?.value.toLowerCase() === 'telur' &&
formik.values.convertion_unit?.value.toLowerCase() === 'qty' && (
<NumberInput
required
label='Harga / Kg (Rp)'
label='Harga / Butir (Rp)'
name='price_per_qty'
value={formik.values.price_per_qty ?? undefined}
onChange={(e) => {
@@ -781,7 +689,29 @@ const SalesOrderProductForm = ({
Boolean(formik.errors.price_per_qty)
}
errorMessage={formik.errors.price_per_qty}
placeholder='Masukan Harga per Kg'
placeholder='Masukan Harga per Butir'
/>
)}
{/* Harga Satuan per Uom Produk Warehouse */}
{formik.values.convertion_unit?.value.toLowerCase() !== 'peti' &&
formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && (
<NumberInput
required
label={`Harga / ${formik.values.convertion_unit?.label !== 'qty' ? 'Kg' : (selectedProductWarehouse?.product?.uom?.name ?? 'Produk')} (Rp)`}
name='unit_price'
value={formik.values.unit_price}
onChange={(e) => {
const value = Number(e.target.value);
handleFieldChange('unit_price', value, () =>
setCurrentInput(e.target.name)
);
}}
isError={
formik.touched.unit_price && Boolean(formik.errors.unit_price)
}
errorMessage={formik.errors.unit_price}
placeholder='Masukan Harga Satuan'
/>
)}
@@ -5,9 +5,8 @@ import { Icon } from '@iconify/react';
import { useRef, useMemo } from 'react';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport';
import { Marketing } from '@/types/api/marketing/marketing';
import { Marketing, BaseDelivery } from '@/types/api/marketing/marketing';
import { Warehouse } from '@/types/api/master-data/warehouse';
import { DeliveryProductToFieldValues } from '@/components/pages/marketing/form/MarketingForm.schema';
type DeliveryOrderProductTableProps = {
data: DeliveryOrderProductFormValues[];
@@ -56,17 +55,14 @@ const DeliveryOrderProductTable = ({
const deliveryItems = useMemo(() => {
if (!hasDeliveryOrder) return [];
return (
marketing?.delivery_order?.flatMap((doItem) =>
DeliveryProductToFieldValues(marketing?.sales_order, doItem).map(
(delivery) => ({
...delivery,
do_number: doItem.do_number,
delivery_date: doItem.delivery_date,
warehouse: doItem.warehouse,
})
)
doItem.deliveries.map((delivery) => ({
...delivery,
do_number: doItem.do_number,
delivery_date: doItem.delivery_date,
warehouse: doItem.warehouse,
}))
) ?? []
);
}, [marketing?.delivery_order, hasDeliveryOrder]);
@@ -85,7 +81,7 @@ const DeliveryOrderProductTable = ({
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
<div>Value</div>
{/* {formType !== 'success' &&
{formType !== 'success' &&
(formType === 'add_delivery' ||
formType === 'edit_delivery' ||
formType === 'detail') && (
@@ -102,16 +98,15 @@ const DeliveryOrderProductTable = ({
<Icon icon='heroicons:pencil' width={20} height={20} />
</Button>
</div>
)} */}
)}
</div>
</th>
</tr>
<>
<tr>
<td className='text-sm px-4 py-3'>Gudang Fisik</td>
<td className='text-sm px-4 py-3'>Gudang</td>
<td className='text-sm px-4 py-3'>
{doItem?.warehouse?.name ||
item.marketing_product?.warehouse?.label ||
item.marketing_product?.product_warehouse_data?.warehouse?.name}
</td>
</tr>
@@ -141,15 +136,12 @@ const DeliveryOrderProductTable = ({
<tr>
<td className='text-sm px-4 py-3'>Total Bobot</td>
<td className='text-sm px-4 py-3'>
{formatNumber(Number(item.total_weight))} Kg
{formatNumber(Number(item.total_weight))}
</td>
</tr>
)}
<tr>
<td className='text-sm px-4 py-3'>
Total Harga Satuan
{item.convertion_unit?.label.toLowerCase() === 'peti' && ' (Kg)'}
</td>
<td className='text-sm px-4 py-3'>Total Harga Satuan</td>
<td className='text-sm px-4 py-3'>
{formatCurrency(parseFloat(item.unit_price as string))}
</td>
@@ -219,7 +211,7 @@ const DeliveryOrderProductTable = ({
};
const renderDeliveryOrderContent = (
item: DeliveryOrderProductFormValues & {
item: BaseDelivery & {
do_number: string;
delivery_date: string;
warehouse: Warehouse;
@@ -238,43 +230,25 @@ const DeliveryOrderProductTable = ({
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
<div>Value</div>
{formType !== 'success' &&
(formType === 'add_delivery' ||
formType === 'edit_delivery' ||
formType === 'detail') && (
<div className='flex flex-row gap-1.5 items-center'>
<Button
type='button'
variant='ghost'
color='none'
onClick={() => {
onEditRef.current(item.id as number, item);
}}
className='p-0 hover:text-base-content'
>
<Icon icon='heroicons:pencil' width={20} height={20} />
</Button>
</div>
)}
</div>
</th>
</tr>
<>
<tr>
<td className='text-sm px-4 py-3'>Gudang Fisik</td>
<td className='text-sm px-4 py-3'>Gudang</td>
<td className='text-sm px-4 py-3'>{item.warehouse?.name}</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Produk</td>
<td className='text-sm px-4 py-3'>
{item.marketing_product?.product_warehouse_data?.product.name}
{item.product_warehouse?.product?.name}
</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Qty</td>
<td className='text-sm px-4 py-3'>
{item.qty
? `${formatNumber(Number(item.qty))} ${item.marketing_product?.product_warehouse_data?.product.uom.name ?? ''}`
? `${formatNumber(item.qty)} ${item.product_warehouse?.product?.uom?.name ?? ''}`
: '-'}
</td>
</tr>
@@ -297,13 +271,13 @@ const DeliveryOrderProductTable = ({
<tr>
<td className='text-sm px-4 py-3'>Total Harga Satuan</td>
<td className='text-sm px-4 py-3'>
{formatCurrency(Number(item.unit_price))}
{formatCurrency(item.unit_price)}
</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Total Penjualan</td>
<td className='text-sm px-4 py-3'>
{formatCurrency(Number(item.total_price))}
{formatCurrency(item.total_price)}
</td>
</tr>
</>
@@ -359,9 +333,7 @@ const DeliveryOrderProductTable = ({
<div className='size-full flex flex-col relative overflow-x-hidden gap-3'>
{hasDeliveryOrder
? deliveryItems.map((item, index) => (
<div
key={`do-table-${item.marketing_product?.product_warehouse?.value}-${index}`}
>
<div key={`do-table-${item.product_warehouse?.id}-${index}`}>
{formType === 'success' ? (
<div className='rounded-lg border border-tools-table-outline border-base-content/5'>
<table
@@ -377,11 +349,8 @@ const DeliveryOrderProductTable = ({
</div>
) : (
<Card
key={`do-table-${item.marketing_product?.product_warehouse?.value}-${index}`}
title={
item.marketing_product?.product_warehouse_data?.product
.name || 'Produk'
}
key={`do-table-${item.product_warehouse?.id}-${index}`}
title={item.product_warehouse?.product?.name || 'Produk'}
collapsible={true}
defaultCollapsed={false}
variant='bordered'
@@ -73,10 +73,8 @@ const SalesOrderProductTable = ({
<td className='text-sm px-4 py-3'>{item.vehicle_number}</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Gudang Fisik</td>
<td className='text-sm px-4 py-3'>
{item.warehouse?.label ?? item.kandang?.label}
</td>
<td className='text-sm px-4 py-3'>Gudang</td>
<td className='text-sm px-4 py-3'>{item.kandang?.label}</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Kategori</td>
@@ -137,22 +135,8 @@ const SalesOrderProductTable = ({
{`${formatNumber(parseFloat(item.qty as string))} ${item.uom || ''}`}
</td>
</tr>
{item.convertion_unit?.value.toLowerCase() === 'peti' && (
<tr>
<td className='text-sm px-4 py-3'>Harga Satuan Per Peti</td>
<td className='text-sm px-4 py-3'>
{formatCurrency(
parseFloat(item.unit_price as string) *
parseFloat(String(item.weight_per_convertion))
)}
</td>
</tr>
)}
<tr>
<td className='text-sm px-4 py-3'>
Harga Satuan
{item.convertion_unit?.value.toLowerCase() === 'peti' && ' (Kg)'}
</td>
<td className='text-sm px-4 py-3'>Harga Satuan</td>
<td className='text-sm px-4 py-3'>
{formatCurrency(parseFloat(item.unit_price as string))}
</td>
@@ -6,7 +6,6 @@ import { useMemo, useState } from 'react';
import { formatDate, formatNumber, formatVechicleNumber } from '@/lib/helper';
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
import toast from 'react-hot-toast';
import { useSearchParams } from 'next/navigation';
interface DeliveryOrderExportProps {
data?: Marketing;
@@ -20,9 +19,6 @@ const DeliveryOrderExport = ({
}: DeliveryOrderExportProps) => {
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
const salesData = data;
const searchParams = useSearchParams();
const action = searchParams.get('action');
const id = searchParams.get('id');
const handleDownloadPDF = async () => {
if (!salesData) {
@@ -53,7 +49,6 @@ const DeliveryOrderExport = ({
toast.error('Failed to generate PDF. Please try again.');
} finally {
setIsGeneratingPDF(false);
window.location.href = `/marketing?action=${action}&id=${id}`;
}
};
@@ -92,7 +87,7 @@ const PDFDocument = ({
return (
deliveryOrder.deliveries?.reduce((a, b) => a + b.total_price, 0) ?? 0
);
}, []);
}, [deliveryOrder.deliveries]);
return (
<Document>
@@ -101,8 +96,8 @@ const PDFDocument = ({
<View style={pdfStyles.header}>
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
<Text style={pdfStyles.address}>
Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten
Bandung Barat, Jawa Barat 40514
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
Cipedes, Kec. Sukajadi, Kota Bandung 40162
</Text>
<View style={pdfStyles.divider} />
</View>
@@ -6,7 +6,6 @@ import { useMemo, useState } from 'react';
import { formatDate, formatNumber } from '@/lib/helper';
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
import toast from 'react-hot-toast';
import { useSearchParams } from 'next/navigation';
interface SalesOrderExportProps {
data?: Marketing;
@@ -16,9 +15,6 @@ interface SalesOrderExportProps {
const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
const salesData = data;
const searchParams = useSearchParams();
const action = searchParams.get('action');
const id = searchParams.get('id');
const handleDownloadPDF = async () => {
if (!salesData) {
@@ -47,7 +43,6 @@ const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
toast.error('Failed to generate PDF. Please try again.');
} finally {
setIsGeneratingPDF(false);
window.location.href = `/marketing?action=${action}&id=${id}`;
}
};
@@ -87,8 +82,8 @@ const PDFDocument = ({ data }: { data: Marketing }) => {
<View style={pdfStyles.header}>
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
<Text style={pdfStyles.address}>
Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten
Bandung Barat, Jawa Barat 40514
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
Cipedes, Kec. Sukajadi, Kota Bandung 40162
</Text>
<View style={pdfStyles.divider} />
</View>
@@ -314,10 +314,6 @@ const KandangsTable = () => {
accessorFn: (row) => row.pic?.name ?? '-',
header: 'PIC',
},
{
accessorFn: (row) => row.kandang_group?.name ?? '-',
header: 'Kandang Group',
},
{
header: 'Aksi',
cell: (props: CellContext<Kandang, unknown>) => {
@@ -1,4 +1,3 @@
import { OptionType } from '@/components/input/SelectInput';
import * as Yup from 'yup';
type KandangFormSchemaType = {
@@ -20,7 +19,6 @@ type KandangFormSchemaType = {
}
| undefined
| null;
group?: OptionType;
};
export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
@@ -44,11 +42,6 @@ export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
group: Yup.object({
value: Yup.number().min(1).required('Kandang Grup wajib diisi!'),
label: Yup.string().required('Kandang Grup wajib diisi!'),
}).required('Kandang Grup wajib diisi!'),
});
export const UpdateKandangFormSchema = KandangFormSchema;
@@ -2,7 +2,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { getIn, useFormik } from 'formik';
import { useFormik } from 'formik';
import { toast } from 'react-hot-toast';
import { Icon } from '@iconify/react';
@@ -34,8 +34,6 @@ import NumberInput from '@/components/input/NumberInput';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import AlertErrorList from '@/components/helper/form/FormErrors';
import { User } from '@/types/api/api-general';
import { DailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
interface KandangFormProps {
type?: 'add' | 'edit' | 'detail';
@@ -98,12 +96,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
label: initialValues.pic.name,
}
: null,
group: initialValues?.kandang_group
? {
value: initialValues.kandang_group.id,
label: initialValues.kandang_group.name,
}
: undefined,
};
}, [initialValues]);
@@ -119,7 +111,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
location_id: values.locationId!,
capacity: values.capacity ? parseInt(values.capacity.toString()) : 0,
pic_id: values.picId!,
group_id: values.group?.value as number,
};
switch (type) {
@@ -171,23 +162,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
formik.setFieldValue('picId', (val as OptionType)?.value);
};
// Kandang Group
const {
setInputValue: setKandangGroupSelectInputValue,
options: kandangGroupOptions,
isLoadingOptions: isLoadingKandangGroupOptions,
loadMore: loadMoreKandangGroups,
} = useSelect<DailyChecklistKandang>(
DailyChecklistKandangApi.basePath,
'id',
'name'
);
const kandangGroupChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('group', true);
formik.setFieldValue('group', val);
};
const deleteKandangClickHandler = () => {
deleteModal.openModal();
};
@@ -295,24 +269,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
isDisabled={type === 'detail'}
isClearable
/>
<SelectInput
required
label='Kandang Group'
value={formik.values.group ?? undefined}
onChange={kandangGroupChangeHandler}
options={kandangGroupOptions}
onInputChange={setKandangGroupSelectInputValue}
onMenuScrollToBottom={loadMoreKandangGroups}
isLoading={isLoadingKandangGroupOptions}
isError={formik.touched.group && Boolean(formik.errors.group)}
errorMessage={
getIn(formik.errors.group, 'value') ??
(formik.errors.group as string)
}
isDisabled={type === 'detail'}
isClearable
/>
</div>
<div className='flex flex-row justify-between gap-2 flex-wrap'>
@@ -154,17 +154,17 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
sku: values.sku,
uom_id: values.uom_id,
product_category_id: values.product_category_id,
product_price: parseFloat(values.product_price.toString()) || 0,
product_price: parseInt(values.product_price.toString()) || 0,
selling_price: values.selling_price
? parseFloat(values.selling_price.toString()) || 0
? parseInt(values.selling_price.toString()) || 0
: undefined,
tax: values.tax ? parseFloat(values.tax.toString()) || 0 : undefined,
tax: values.tax ? parseInt(values.tax.toString()) || 0 : undefined,
expiry_period: values.expiry_period
? parseFloat(values.expiry_period.toString()) || 0
? parseInt(values.expiry_period.toString()) || 0
: undefined,
suppliers: values.suppliers.map((s) => ({
supplier_id: s.supplier?.value as number,
price: parseFloat(s.price.toString()) || 0,
price: parseInt(s.price.toString()) || 0,
})),
flag: values.flag,
sub_flags: values.sub_flags,
@@ -44,7 +44,9 @@ const ChickinFormKandang = ({
const afterSubmitFormChickin = () => {
setOpenChickin(true);
afterSubmit && afterSubmit();
if (afterSubmit) {
afterSubmit();
}
refreshApprovals();
};
@@ -23,7 +23,7 @@ const ChickinLogsView = ({
rawDataApprovals: BaseApproval[];
}) => {
const [chickinErrorMessage, setChickinErrorMessage] = useState('');
const { openChickinApproveModal, openChickinDeleteModal } = useChickinStore();
const { openChickinApproveModal } = useChickinStore();
const handleClickApprove = () => {
openChickinApproveModal(initialValues, async (notes?: string) => {
@@ -40,21 +40,8 @@ const ChickinLogsView = ({
toast.error(approveChickinRes?.message as string);
setChickinErrorMessage(approveChickinRes?.message as string);
}
afterSubmit && afterSubmit();
});
};
const handleDeleteChickin = (chickinId: number) => {
openChickinDeleteModal(chickinId, async () => {
const deleteRes = await ChickinApi.delete(chickinId);
if (isResponseSuccess(deleteRes)) {
toast.success(deleteRes?.message || 'Chickin berhasil dihapus');
afterSubmit && afterSubmit();
}
if (isResponseError(deleteRes)) {
toast.error(deleteRes?.message || 'Gagal menghapus chickin');
if (afterSubmit) {
afterSubmit();
}
});
};
@@ -101,30 +88,14 @@ const ChickinLogsView = ({
<div className='text-lg font-semibold'>
Chick In #{index + 1} - {latestApproval?.step_number}
</div>
<div className='flex flex-row gap-2 items-center'>
<PillBadge
content={
isApproved ? 'Disetujui' : isPending ? 'Pending' : '-'
}
color={
isApproved ? 'green' : isPending ? 'yellow' : 'gray'
}
/>
{isApproved && (
<Button
color='error'
className='w-fit text-sm text-base-100 rounded-lg shadow-sm btn-xs!'
onClick={() => handleDeleteChickin(chickin.id)}
>
<Icon
icon='heroicons:trash-solid'
width={10}
height={10}
/>
</Button>
)}
</div>
<PillBadge
content={
isApproved ? 'Disetujui' : isPending ? 'Pending' : '-'
}
color={
isApproved ? 'green' : isPending ? 'yellow' : 'gray'
}
/>
</div>
{/* Tanggal Chick In */}
@@ -59,7 +59,8 @@ const RowOptionsMenu = ({
detailClickHandler: (id: number) => void;
deleteClickHandler: () => void;
}) => {
const showEditButton = props.row.original.approval?.step_number !== 2;
// TODO: change this to real condition
const showEditButton = true;
const showDeleteButton = showEditButton;
@@ -199,7 +200,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
const confirmModal = useModal();
const successModal = useModal();
const chickinApproveModal = useModal();
const chickinDeleteModal = useModal();
const closingModal = useModal();
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
'APPROVED'
@@ -214,11 +214,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
chickinApproveCallback,
closeChickinApproveModal,
setChickinApproveLoading,
isChickinDeleteModalOpen,
isChickinDeleteLoading,
chickinDeleteCallback,
closeChickinDeleteModal,
setChickinDeleteLoading,
} = useChickinStore();
const {
@@ -483,14 +478,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
}
}, [isChickinApproveModalOpen, chickinApproveModal]);
useEffect(() => {
if (isChickinDeleteModalOpen) {
chickinDeleteModal.openModal();
} else {
chickinDeleteModal.closeModal();
}
}, [isChickinDeleteModalOpen, chickinDeleteModal]);
useEffect(() => {
if (isClosingModalOpen) {
closingModal.openModal();
@@ -1221,38 +1208,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
}}
/>
{/* Chickin Delete Modal */}
<ConfirmationModal
ref={chickinDeleteModal.ref}
type='error'
text='Apakah anda yakin ingin menghapus data chick in ini?'
secondaryButton={{
text: 'Tidak',
onClick: () => {
closeChickinDeleteModal();
},
}}
className={{
modal: 'z-9999',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isChickinDeleteLoading,
onClick: async () => {
if (chickinDeleteCallback) {
setChickinDeleteLoading(true);
try {
await chickinDeleteCallback();
} finally {
setChickinDeleteLoading(false);
closeChickinDeleteModal();
}
}
},
}}
/>
{/* Filter Modal */}
<Modal
ref={filterModal.ref}
@@ -21,7 +21,6 @@ import SelectInput, { useSelect } from '@/components/input/SelectInput';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent';
import Tooltip from '@/components/Tooltip';
import { useFormik } from 'formik';
import { AreaApi } from '@/services/api/master-data';
import { LocationApi } from '@/services/api/master-data';
@@ -37,7 +36,6 @@ import {
import RecordingTableSkeleton from '@/components/pages/production/recording/skeleton/RecordingTableSkeleton';
import Table from '@/components/Table';
import { type Recording } from '@/types/api/production/recording';
import { getRecordingRestriction } from './recording-utils';
import { RecordingApi } from '@/services/api/production';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
@@ -48,7 +46,6 @@ import { useUiStore } from '@/stores/ui/ui.store';
import { usePathname } from 'next/navigation';
import { Color } from '@/types/theme';
import ButtonFilter from '@/components/helper/ButtonFilter';
import Dropdown from '@/components/Dropdown';
// ===== STATUS BADGE UTILITIES =====
const statusTextMap: Record<string, string> = {
@@ -107,76 +104,20 @@ const RowOptionsMenu = ({
return recording.approval?.action === 'REJECTED';
};
const isRecordingEditable = (recording: Recording) => {
const isGrowingCategory =
recording.project_flock?.project_flock_category === 'GROWING';
const isGrowingLockedByLaying = isGrowingCategory && recording.is_laying;
if (isGrowingLockedByLaying) {
return false;
}
const currentIsLaying =
recording.project_flock?.project_flock_category === 'LAYING';
const restriction = getRecordingRestriction(
recording.is_laying,
recording.is_transition,
currentIsLaying
);
if (restriction.isLocked) {
return false;
}
return true;
};
const getRecordingRestrictionInfo = (recording: Recording) => {
const isGrowingCategory =
recording.project_flock?.project_flock_category === 'GROWING';
const isGrowingLockedByLaying = isGrowingCategory && recording.is_laying;
if (isGrowingLockedByLaying) {
return {
canEditStock: false,
canEditDepletion: false,
canEditEgg: false,
isLocked: true,
lockReason:
'Recording Growing tidak dapat diubah karena sudah masuk fase laying dan dipakai pada recording laying',
};
}
const currentIsLaying =
recording.project_flock?.project_flock_category === 'LAYING';
return getRecordingRestriction(
recording.is_laying,
recording.is_transition,
currentIsLaying
);
};
const isApproved = isRecordingApproved(props.row.original);
const isRejected = isRecordingRejected(props.row.original);
const isEditable = isRecordingEditable(props.row.original);
const restrictionInfo = getRecordingRestrictionInfo(props.row.original);
return (
<div className='relative'>
<Tooltip
content={restrictionInfo.isLocked ? restrictionInfo.lockReason : ''}
position='top'
<PopoverButton
tabIndex={0}
variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
>
<PopoverButton
tabIndex={0}
variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
className={restrictionInfo.isLocked ? 'text-error' : ''}
>
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</PopoverButton>
</Tooltip>
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</PopoverButton>
<PopoverContent
id={popoverId}
@@ -197,20 +138,18 @@ const RowOptionsMenu = ({
View Details
</Button>
</RequirePermission>
{isEditable && (
<RequirePermission permissions='lti.production.recording.update'>
<Button
href={`/production/recording/detail/edit/?recordingId=${props.row.original.id}`}
variant='ghost'
color='none'
className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
>
<Icon icon='mdi:pencil-outline' width={20} height={20} />
Edit
</Button>
</RequirePermission>
)}
<RequirePermission permissions='lti.production.recording.update'>
<Button
href={`/production/recording/detail/edit/?recordingId=${props.row.original.id}`}
variant='ghost'
color='none'
className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
>
<Icon icon='mdi:pencil-outline' width={20} height={20} />
Edit
</Button>
</RequirePermission>
{!isApproved && !isRejected && (
<RequirePermission permissions='lti.production.recording.approve'>
<Button
@@ -243,22 +182,20 @@ const RowOptionsMenu = ({
</Button>
</RequirePermission>
)}
{isEditable && (
<RequirePermission permissions='lti.production.recording.delete'>
<Button
onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost'
color='error'
className='p-3 justify-start text-sm font-semibold w-full focus-visible:text-error-content hover:text-error-content'
>
<Icon icon='mdi:delete-outline' width={20} height={20} />
Delete
</Button>
</RequirePermission>
)}
<RequirePermission permissions='lti.production.recording.delete'>
<Button
onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost'
color='error'
className='p-3 justify-start text-sm font-semibold w-full focus-visible:text-error-content hover:text-error-content'
>
<Icon icon='mdi:delete-outline' width={20} height={20} />
Delete
</Button>
</RequirePermission>
</div>
</PopoverContent>
</div>
@@ -353,9 +290,6 @@ const RecordingTable = () => {
const [isRejectLoading, setIsRejectLoading] = useState(false);
const [, setApprovalNotes] = useState('');
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
useState(false);
const singleDeleteModal = useModal();
const approveModal = useModal();
const rejectModal = useModal();
@@ -611,17 +545,12 @@ const RecordingTable = () => {
const singleDeleteHandler = async () => {
setIsDeleteLoading(true);
const response = await RecordingApi.delete(selectedRecording?.id as number);
await RecordingApi.delete(selectedRecording?.id as number);
refreshRecordings();
singleDeleteModal.closeModal();
toast.success('Successfully delete Recording!');
setIsDeleteLoading(false);
if (isResponseSuccess(response)) {
toast.success(response?.message || 'Successfully delete Recording!');
refreshRecordings();
} else {
toast.error(response?.message || 'Failed to delete Recording');
}
};
const approveHandler = async (notes: string) => {
@@ -690,14 +619,6 @@ const RecordingTable = () => {
});
}, [selectedRowIds, recordings, isRecordingApproved]);
const exportToExcelHandler = async () => {
setIsLoadingExportingToExcel(true);
await RecordingApi.exportToExcel(getTableFilterQueryString());
setIsLoadingExportingToExcel(false);
};
useEffect(() => {
if (isResponseSuccess(recordings) && recordings.data) {
const newSelection: Record<string, boolean> = {};
@@ -825,30 +746,11 @@ const RecordingTable = () => {
{
header: 'Kategori',
cell: (props) => {
const isTransition = props.row.original.is_transition;
const category =
props.row.original.project_flock?.project_flock_category ||
'GROWING';
props.row.original.project_flock?.project_flock_category;
if (!category) return '-';
const color = category === 'LAYING' ? 'info' : 'warning';
const isGrowingLocked =
category === 'GROWING' && props.row.original.is_laying;
return (
<div className='flex flex-col gap-1'>
<StatusBadge color={color} text={formatTitleCase(category)} />
{isTransition && (
<span className='text-xs text-warning font-medium'>
(Transisi)
</span>
)}
{isGrowingLocked && (
<span className='text-xs text-error font-medium'>
(Penguncian)
</span>
)}
</div>
);
return <StatusBadge color={color} text={formatTitleCase(category)} />;
},
},
{
@@ -1325,50 +1227,6 @@ const RecordingTable = () => {
onClick={handleFilterModalOpen}
className='px-3 py-2.5'
/>
<Dropdown
align='end'
direction='bottom'
trigger={
<Button
variant='outline'
color='none'
className={cn(
'px-3 py-2.5 rounded-lg font-semibold text-sm gap-1.5',
'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft'
)}
>
<Icon
width={20}
height={20}
icon='heroicons:cloud-arrow-down'
/>
Export
<div className='w-6.5 h-5 flex items-center justify-center border-l border-base-content/10'>
<Icon
width={14}
height={14}
icon='heroicons:chevron-down'
/>
</div>
</Button>
}
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
>
<Button
variant='ghost'
color='none'
onClick={exportToExcelHandler}
isLoading={isLoadingExportingToExcel}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:table-cells' width={20} height={20} />
Export to Excel
</Button>
</Dropdown>
</div>
</div>
@@ -34,7 +34,6 @@ type RecordingGrowingFormSchemaType = {
}[];
depletions: {
product_warehouse_id?: number;
source_product_warehouse_id?: number;
qty?: number | string;
}[];
};
@@ -54,7 +53,6 @@ export type StockSchema = {
export type DepletionSchema = {
product_warehouse_id?: number;
source_product_warehouse_id?: number;
qty?: number | string;
};
@@ -71,7 +69,7 @@ const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
.typeError('Produk harus berupa angka!'),
qty: Yup.number()
.required('Jumlah penggunaan wajib diisi!')
.moreThan(0, 'Jumlah penggunaan harus lebih dari 0!')
.min(1, 'Jumlah penggunaan tidak boleh 0!')
.typeError('Jumlah penggunaan harus berupa angka!'),
});
@@ -79,9 +77,6 @@ const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({
product_warehouse_id: Yup.number()
.optional()
.typeError('Depletions harus berupa angka!'),
source_product_warehouse_id: Yup.number()
.optional()
.typeError('Gudang sumber harus berupa angka!'),
qty: Yup.number()
.optional()
.typeError('Jumlah depletions harus berupa angka!'),
@@ -264,7 +259,6 @@ export const getRecordingGrowingFormInitialValues = (
depletion: NonNullable<CreateGrowingRecordingPayload['depletions']>[0]
) => ({
product_warehouse_id: depletion.product_warehouse_id,
source_product_warehouse_id: depletion.source_product_warehouse_id,
qty: depletion.qty,
})
) ?? [
File diff suppressed because it is too large Load Diff
@@ -1,73 +0,0 @@
export type RecordingRestriction = {
canEditStock: boolean;
canEditDepletion: boolean;
canEditEgg: boolean;
isLocked: boolean;
lockReason?: string;
};
export const getRecordingRestriction = (
isLaying: boolean,
isTransition: boolean,
currentIsLaying?: boolean
): RecordingRestriction => {
if (isTransition && !isLaying) {
const isLayingKandangInTransition = currentIsLaying === true;
if (isLayingKandangInTransition) {
return {
canEditStock: false,
canEditDepletion: true,
canEditEgg: true,
isLocked: false,
lockReason: undefined,
};
} else {
return {
canEditStock: true,
canEditDepletion: false,
canEditEgg: false,
isLocked: false,
lockReason: undefined,
};
}
}
if (!isLaying && !isTransition && currentIsLaying) {
return {
canEditStock: false,
canEditDepletion: false,
canEditEgg: false,
isLocked: true,
lockReason:
'Recording Growing telah terkunci karena Project Flock sudah masuk fase Laying',
};
}
if (!isLaying && !isTransition) {
return {
canEditStock: true,
canEditDepletion: true,
canEditEgg: false,
isLocked: false,
lockReason: undefined,
};
}
if (isLaying && !isTransition) {
return {
canEditStock: true,
canEditDepletion: true,
canEditEgg: true,
isLocked: false,
lockReason: undefined,
};
}
return {
canEditStock: false,
canEditDepletion: false,
canEditEgg: false,
isLocked: true,
lockReason: 'Kondisi transisi tidak valid',
};
};
@@ -50,18 +50,12 @@ const TransferToLayingConfirmationModalTable = ({
transferToLayingForm?: TransferToLayingFormValues;
transferToLayingId?: number;
}) => {
const isValidId =
transferToLayingId !== undefined &&
transferToLayingId !== null &&
!isNaN(transferToLayingId) &&
transferToLayingId > 0;
const { data: transferToLaying, isLoading: isLoadingTransferToLaying } =
useSWR(
isValidId
transferToLayingId
? ['detail-transfer-to-laying', String(transferToLayingId)]
: undefined,
([, id]) => TransferToLayingApi.getSingle(Number(id))
([id]) => TransferToLayingApi.getSingle(Number(id))
);
const confirmationTableColumns: ColumnDef<TransferToLayingConfirmationTableDataType>[] =
@@ -279,16 +273,12 @@ const TransferToLayingConfirmationModal = ({
{transferToLayingIds &&
!transferToLayingForm &&
transferToLayingIds
.filter(
(id) => id !== undefined && id !== null && !isNaN(id) && id > 0
)
.map((transferToLayingId, idx) => (
<TransferToLayingConfirmationModalTable
key={idx}
transferToLayingId={transferToLayingId}
/>
))}
transferToLayingIds.map((transferToLayingId, idx) => (
<TransferToLayingConfirmationModalTable
key={idx}
transferToLayingId={transferToLayingId}
/>
))}
{withNote && (
<TextArea
@@ -82,7 +82,7 @@ const TransferToLayingDetailModal = () => {
if (modalAction === 'detail') {
detailModal.openModal();
}
}, [modalAction]);
}, [modalAction, detailModal]);
return (
<Modal
@@ -229,8 +229,6 @@ const TransferToLayingFormModal = () => {
ProjectFlock | undefined
>(undefined);
const [maxSourceQuantity, setMaxSourceQuantity] = useState<number>(0);
const selectedFlockDestinationRawData = isResponseSuccess(
flockDestinationRawData
)
@@ -355,14 +353,19 @@ const TransferToLayingFormModal = () => {
return { available: countAvailable, unavailable: countUnavailable };
}, [mappedFlockDestinationKandangsMaxTargetQty]);
const totalEnteredChickenForTransfer =
formik.values.flockSourceKandangs.reduce(
(acc, item) => acc + Number(item.quantity),
0
);
const totalTransferedChicken = formik.values.flockDestinationKandangs.reduce(
(acc, item) => acc + Number(item.quantity),
0
);
// Sisa transfer = Max available dari kandang asal - Total yang sudah diisi di kandang tujuan
const totalAvailableChickenForTransfer =
maxSourceQuantity - totalTransferedChicken;
totalEnteredChickenForTransfer - totalTransferedChicken;
const isNextButtonDisabled = useMemo(() => {
if (step === 1) {
@@ -394,7 +397,6 @@ const TransferToLayingFormModal = () => {
formik.setFieldValue('maxTotalQuantity', '');
formik.setFieldValue('reason', '');
formik.setFieldTouched('reason', false);
setMaxSourceQuantity(0);
setStep(2);
};
@@ -402,7 +404,6 @@ const TransferToLayingFormModal = () => {
const flockSourceChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('flockSource', val);
formik.setFieldValue('flockSourceKandangs', []);
setMaxSourceQuantity(0);
};
const flockDestinationChangeHandler = (
@@ -468,26 +469,6 @@ const TransferToLayingFormModal = () => {
formik.setFieldValue('maxTotalQuantity', totalTransferedChicken);
}, [totalTransferedChicken, formik.values.flockDestinationKandangs]);
// Auto-fill source kandang quantity from total destination quantity
useEffect(() => {
if (formik.values.flockSourceKandangs.length > 0) {
formik.setFieldValue(
'flockSourceKandangs.0.quantity',
totalTransferedChicken
);
}
}, [totalTransferedChicken]);
useEffect(() => {
if (
formik.values.flockSourceKandangs.length > 0 &&
formik.values.flockSourceKandangs[0].maxQuantity &&
maxSourceQuantity === 0
) {
setMaxSourceQuantity(formik.values.flockSourceKandangs[0].maxQuantity);
}
}, [formik.values.flockSourceKandangs, maxSourceQuantity]);
return (
<>
<Modal
@@ -602,9 +583,14 @@ const TransferToLayingFormModal = () => {
k.kandang.value === item.project_flock_kandang_id
);
const flockSourceKandangRadioChangeHandler = () => {
if (isAvailable) {
const flockSourceKandangCheckboxChangeHandler: FormEventHandler<
HTMLInputElement
> = (e) => {
const checked = (e.target as HTMLInputElement)
.checked;
if (checked) {
formik.setFieldValue('flockSourceKandangs', [
...formik.values.flockSourceKandangs,
{
kandang: {
value: item.project_flock_kandang_id,
@@ -614,7 +600,15 @@ const TransferToLayingFormModal = () => {
maxQuantity: item.available_qty,
},
]);
setMaxSourceQuantity(item.available_qty);
} else {
formik.setFieldValue(
'flockSourceKandangs',
formik.values.flockSourceKandangs.filter(
(k) =>
k.kandang.value !==
item.project_flock_kandang_id
)
);
}
};
@@ -624,22 +618,28 @@ const TransferToLayingFormModal = () => {
className='w-full p-3 flex flex-row items-center justify-between'
>
<div className='flex flex-row items-center gap-3'>
<input
type='radio'
name='flockSourceKandang'
<CheckboxInput
name={`flockSourceKandang.${itemIdx}.value`}
value={item.project_flock_kandang_id}
checked={isChecked}
onChange={flockSourceKandangRadioChangeHandler}
onChange={
flockSourceKandangCheckboxChangeHandler
}
size='md'
disabled={!isAvailable}
className={cn('radio radio-md radio-primary', {
'opacity-50 cursor-not-allowed': !isAvailable,
})}
classNames={{
checkbox: cn({
'bg-base-200 border border-base-content/10 opacity-100':
!isAvailable,
}),
}}
/>
<label
htmlFor={`flockSourceKandang.${itemIdx}.value`}
className={cn('text-sm text-base-content/50', {
'cursor-pointer': isAvailable,
'cursor-not-allowed opacity-50': !isAvailable,
'cursor-not-allowed': !isAvailable,
})}
>
{item.kandang_name}{' '}
@@ -858,7 +858,7 @@ const TransferToLayingFormModal = () => {
<NumberInput
key={`flockSourceKandangs-${item.kandang.value}-${index}`}
name={`flockSourceKandangs.${index}.quantity`}
placeholder='Masukkan Kuantitas pada Kandang Tujuan'
placeholder='Masukkan Kuantitas'
value={item.quantity}
onChange={formik.handleChange}
isError={isInvalid}
@@ -875,8 +875,6 @@ const TransferToLayingFormModal = () => {
<div className='w-px bg-base-content/10' />
</div>
}
readOnly
disabled
className={{
inputPrefix:
'py-0 px-0 pl-3 text-base-content/50 bg-transparent border-r-0',
@@ -1002,7 +1000,7 @@ const TransferToLayingFormModal = () => {
isError={totalAvailableChickenForTransfer < 0}
errorMessage={
totalAvailableChickenForTransfer < 0
? `Jumlah transfer melebihi ketersediaan (${formatNumber(maxSourceQuantity, 'en-US')} ayam)`
? `Jumlah transfer melebihi ketersediaan (${formatNumber(totalEnteredChickenForTransfer, 'en-US')} ayam)`
: ''
}
disabled
@@ -48,11 +48,11 @@ const RowOptionsMenu = ({
popoverPosition: 'bottom' | 'top';
deleteClickHandler: () => void;
}) => {
const showEditButton = props.row.original.approval.action !== 'APPROVED';
const showEditButton =
props.row.original.approval.action !== 'APPROVED' &&
props.row.original.approval.action !== 'REJECTED';
const showDeleteButton =
props.row.original.approval.action === 'APPROVED' ||
props.row.original.approval.step_name.toLowerCase() === 'pengajuan';
const showDeleteButton = showEditButton;
const popoverId = `transferToLaying#${props.row.original.id}`;
const popoverAnchorName = `--anchor-transferToLaying#${props.row.original.id}`;
@@ -463,7 +463,7 @@ const TransferToLayingsTable = () => {
updateFilter('filter_by', '');
updateFilter('sort_by', '');
}
}, [sorting]);
}, [sorting, updateFilter]);
return (
<>
@@ -60,25 +60,6 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
router.push(`/production/uniformity?action=reject&id=${initialValues.id}`);
};
const handleViewUniformityDetails = () => {
if (!uniformity_details || uniformity_details.length === 0) {
setShouldFetchDetails(true);
return;
}
setExpandedDrawerContent(
<UniformityDetailsPreview
info_umum={initialValues.info_umum}
uniformity_details={uniformity_details}
uniformityId={initialValues.id}
/>
);
setTimeout(() => {
setExpandedDrawerOpen(true);
}, 0);
};
useEffect(() => {
if (
shouldFetchDetails &&
@@ -202,6 +183,25 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
}
if (id === 'document-name') {
const handleViewUniformityDetails = () => {
if (!uniformity_details || uniformity_details.length === 0) {
setShouldFetchDetails(true);
return;
}
setExpandedDrawerContent(
<UniformityDetailsPreview
info_umum={initialValues.info_umum}
uniformity_details={uniformity_details}
uniformityId={initialValues.id}
/>
);
setTimeout(() => {
setExpandedDrawerOpen(true);
}, 0);
};
return (
<div className='flex items-center gap-2'>
<span>{valueMap[id]}</span>
@@ -231,7 +231,14 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
},
},
],
[initialValues, handleViewUniformityDetails, isLoading]
[
initialValues,
isLoading,
uniformity_details,
setShouldFetchDetails,
setExpandedDrawerContent,
setExpandedDrawerOpen,
]
);
const samplingTableData: DetailOptionType[] = useMemo(() => {
@@ -597,7 +597,6 @@ const UniformityForm = ({
onBlur={formik.handleBlur}
isError={formik.touched.date && Boolean(formik.errors.date)}
errorMessage={formik.errors.date as string}
disabled={isNextStep}
/>
<SelectInput
@@ -616,7 +615,6 @@ const UniformityForm = ({
errorMessage={formik.errors.location_id as string}
isClearable
className={{ wrapper: 'w-full' }}
isDisabled={isNextStep}
/>
<SelectInput
@@ -629,7 +627,7 @@ const UniformityForm = ({
onInputChange={setProjectFlockSearchValue}
isLoading={isLoadingProjectFlocks}
onMenuScrollToBottom={loadMoreProjectFlocks}
isDisabled={!formik.values.location_id || isNextStep}
isDisabled={!formik.values.location_id}
isError={
formik.touched.project_flock_id &&
Boolean(formik.errors.project_flock_id)
@@ -646,7 +644,7 @@ const UniformityForm = ({
value={formik.values.kandang}
onChange={handleKandangChange}
options={kandangOptions}
isDisabled={!formik.values.project_flock_id || isNextStep}
isDisabled={!formik.values.project_flock_id}
isError={
formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
}
@@ -1,201 +0,0 @@
'use client';
import { RefObject, useState, useEffect } from 'react';
import { useFormik } from 'formik';
import toast from 'react-hot-toast';
import { Icon } from '@iconify/react';
import Modal from '@/components/Modal';
import Button from '@/components/Button';
import DateInput from '@/components/input/DateInput';
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
import { OptionType, useSelect } from '@/components/input/SelectInput';
import { PurchaseFilter } from '@/types/api/purchase/purchase';
import { ProductCategory } from '@/types/api/master-data/product-category';
import { ProductCategoryApi } from '@/services/api/master-data';
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
interface PurchaseFilterModalProps {
ref: RefObject<HTMLDialogElement | null>;
onSubmit?: (values: PurchaseFilter) => void;
onReset?: () => void;
}
const PurchaseFilterModal = ({
ref,
onSubmit,
onReset,
}: PurchaseFilterModalProps) => {
const closeModalHandler = () => {
ref.current?.close();
};
// ===== DATE ERROR STATE =====
const [dateErrorShown, setDateErrorShown] = useState(false);
const [hasDateError, setHasDateError] = useState(false);
// ===== CLEANUP TOAST ON UNMOUNT =====
useEffect(() => {
return () => {
if (dateErrorShown) {
toast.dismiss();
}
};
}, [dateErrorShown]);
// ===== CLEANUP TOAST WHEN MODAL CLOSES =====
useEffect(() => {
const dialogElement = ref.current;
const handleModalClose = () => {
if (dateErrorShown) {
toast.dismiss();
setDateErrorShown(false);
}
};
dialogElement?.addEventListener('close', handleModalClose);
return () => {
dialogElement?.removeEventListener('close', handleModalClose);
};
}, [ref, dateErrorShown]);
const {
setInputValue: setProductCategoryInputValue,
options: productCategoryOptions,
isLoadingOptions: isLoadingProductCategoryOptions,
loadMore: loadMoreProductCategory,
} = useSelect<ProductCategory>(
ProductCategoryApi.basePath,
'id',
'name',
'search'
);
const formik = useFormik<{
poDate: string;
category: { label: string; value: number }[];
status: { label: string; value: string }[];
}>({
initialValues: {
poDate: '',
category: [],
status: [],
},
onSubmit: async (values) => {
const formattedValues = {
...values,
category: values.category.map((item) => String(item.value)),
status: values.status.map((item) => String(item.value)),
};
onSubmit?.(formattedValues);
closeModalHandler();
},
onReset: () => {
onReset?.();
closeModalHandler();
},
});
const productCategoryChangeHandler = (
val: OptionType | OptionType[] | null
) => {
formik.setFieldValue('category', val);
};
const statusChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('status', val);
};
return (
<Modal
ref={ref}
className={{
modalBox: 'p-0 rounded-xl',
}}
>
<form
onSubmit={formik.handleSubmit}
onReset={formik.handleReset}
className='w-full flex flex-col'
>
{/* Modal Header */}
<div className='p-4 flex items-center justify-between gap-2 border-b border-gray-300'>
<div className='flex items-center gap-2 text-primary'>
<Icon icon='heroicons:funnel' width={20} height={20} />
<h3 className='text-sm font-medium'>Filter Data</h3>
</div>
<Button
type='button'
variant='ghost'
color='none'
onClick={closeModalHandler}
className='p-0 text-base-content/50 hover:text-base-content'
>
<Icon icon='heroicons:x-mark' width={20} height={20} />
</Button>
</div>
{/* Modal Body */}
<div className='p-4 flex flex-col gap-1.5'>
<div className='flex flex-col'>
<DateInput
label='PO Date'
name='poDate'
placeholder='Pilih Tanggal'
value={formik.values.poDate}
onChange={formik.handleChange}
isNestedModal
/>
<SelectInputCheckbox
label='Kategori'
placeholder='Pilih Kategori'
value={formik.values.category}
onChange={productCategoryChangeHandler}
options={productCategoryOptions}
isLoading={isLoadingProductCategoryOptions}
onInputChange={setProductCategoryInputValue}
onMenuScrollToBottom={loadMoreProductCategory}
/>
<SelectInputCheckbox
label='Status'
placeholder='Status'
value={formik.values.status}
onChange={statusChangeHandler}
options={PURCHASE_ORDER_APPROVAL_LINE.map((item) => ({
label: item.step_name,
value: item.step_name,
}))}
/>
</div>
</div>
{/* Modal Footer */}
<div className='p-4 flex justify-between gap-4 border-t border-gray-300 bg-gray-100'>
<Button
type='reset'
variant='ghost'
color='none'
className='p-3 rounded-lg text-base-content/65'
>
Reset Filter
</Button>
<Button
type='submit'
className='p-3 rounded-lg w-fit sm:w-full max-w-40 text-base-100 text-sm'
>
Apply Filter
</Button>
</div>
</form>
</Modal>
);
};
export default PurchaseFilterModal;
@@ -14,7 +14,6 @@ import useSWRInfinite from 'swr/infinite';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import toast from 'react-hot-toast';
import Link from 'next/link';
import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
@@ -26,19 +25,18 @@ import PopoverContent from '@/components/popover/PopoverContent';
import RequirePermission from '@/components/helper/RequirePermission';
import StatusBadge from '@/components/helper/StatusBadge';
import PurchaseTableSkeleton from '@/components/pages/purchase/skeleton/PurchaseTableSkeleton';
import ButtonFilter from '@/components/helper/ButtonFilter';
import PurchaseFilterModal from '@/components/pages/purchase/PurchaseFilterModal';
import { cn, formatDate } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { BaseApiResponse } from '@/types/api/api-general';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { Purchase, PurchaseFilter } from '@/types/api/purchase/purchase';
import { Purchase } from '@/types/api/purchase/purchase';
import { PurchaseApi } from '@/services/api/purchase';
import { ExpenseApi } from '@/services/api/expense';
import { Expense } from '@/types/api/expense';
import { Color } from '@/types/theme';
import Link from 'next/link';
// ===== STATUS BADGE UTILITIES =====
const statusTextMap: Record<string, string> = {
@@ -167,21 +165,14 @@ const PurchaseTable = () => {
} = useTableFilter({
initial: {
search: '',
po_date: '',
approval_status: '',
product_category_id: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
po_date: 'po_date',
approval_status: 'approval_status',
product_category_id: 'product_category_id',
},
});
// ===== MODAL HOOKS =====
const filterModal = useModal();
const deleteModal = useModal();
// ===== API DATA FETCHING =====
@@ -419,17 +410,13 @@ const PurchaseTable = () => {
[updateFilter, setSearchValue]
);
const filterSubmitHandler = (values: PurchaseFilter) => {
updateFilter('po_date', values.poDate);
updateFilter('product_category_id', values.category.join(','));
updateFilter('approval_status', values.status.join(','));
};
const filterResetHandler = () => {
updateFilter('po_date', '');
updateFilter('product_category_id', '');
updateFilter('approval_status', '');
};
// const pageSizeChangeHandler = useCallback(
// (val: OptionType | OptionType[] | null) => {
// const newVal = val as OptionType;
// setPageSize(newVal.value as number);
// },
// [setPageSize]
// );
return (
<>
@@ -468,20 +455,6 @@ const PurchaseTable = () => {
'placeholder:font-semibold placeholder:text-base-content/50',
}}
/>
<ButtonFilter
values={tableFilterState}
excludeFields={[
'page',
'pageSize',
'search',
'filter_by',
'sort_by',
]}
fieldGroups={[['startDate', 'endDate']]}
onClick={filterModal.openModal}
className='px-3 py-2.5'
/>
</div>
</div>
@@ -540,13 +513,6 @@ const PurchaseTable = () => {
</div>
{/* ===== MODAL COMPONENTS ===== */}
<PurchaseFilterModal
ref={filterModal.ref}
onSubmit={filterSubmitHandler}
onReset={filterResetHandler}
/>
<ConfirmationModal
ref={deleteModal.ref}
type='error'
@@ -308,7 +308,7 @@ const PurchaseOrderAcceptApprovalForm = ({
}
: null,
expedition_vendor_id: expeditionVendorId,
received_qty: item.sub_qty || '',
received_qty: item.total_qty || '',
transport_per_item: item.transport_per_item || '',
};
});
@@ -367,9 +367,6 @@ const PurchaseOrderAcceptApprovalForm = ({
);
} else {
formik.setFieldValue(`items.${idx}.expedition_vendor_id`, null);
formik.setFieldValue(`items.${idx}.transport_per_item`, null);
formik.setFieldValue(`items.${idx}.vehicle_number`, null);
}
};
@@ -556,7 +553,6 @@ const PurchaseOrderAcceptApprovalForm = ({
)
}
onBlur={formik.handleBlur}
disabled={!Boolean(formItem?.expedition_vendor)}
isError={
isRepeaterInputError(idx, 'vehicle_number').isError
}
@@ -661,7 +657,6 @@ const PurchaseOrderAcceptApprovalForm = ({
thousandSeparator=','
decimalSeparator='.'
inputPrefix={'Rp'}
disabled={!Boolean(formItem?.expedition_vendor)}
isError={
isRepeaterInputError(idx, 'transport_per_item')
.isError
@@ -185,12 +185,7 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApp
.typeError('No. Surat jalan wajib diisi!'),
vehicle_number: Yup.string()
.nullable()
.when('expedition_vendor', {
is: (expeditionVendor?: { value?: number; label?: string } | null) =>
Boolean(expeditionVendor?.value),
then: (schema) => schema.required('Nomor kendaraan wajib diisi!'),
otherwise: (schema) => schema.optional(),
})
.optional()
.typeError('Nomor kendaraan harus berupa plat nomor!'),
expedition_vendor: Yup.object({
value: Yup.number().min(1).required(),
@@ -218,13 +213,7 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApp
.typeError('Jumlah diterima harus berupa angka!'),
transport_per_item: Yup.mixed<string | number>()
.nullable()
.when('expedition_vendor', {
is: (expeditionVendor?: { value?: number; label?: string } | null) =>
Boolean(expeditionVendor?.value),
then: (schema) =>
schema.required('Biaya transport per item wajib diisi!'),
otherwise: (schema) => schema.optional(),
})
.optional()
.test(
'is-valid-transport-per-item',
'Biaya transport per item harus berupa angka lebih dari atau sama dengan 0!',
@@ -34,7 +34,7 @@ const pdfStyles = StyleSheet.create({
marginBottom: 20,
},
logo: {
width: 30,
width: 120,
height: 30,
marginBottom: 8,
},
@@ -265,7 +265,7 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
<View style={pdfStyles.header}>
{/* eslint-disable-next-line jsx-a11y/alt-text */}
<Image
src='/assets/img/lti-logo.png'
src={'https://placehold.co/120x30/png'}
style={pdfStyles.logo}
id={'mbu-logo'}
/>
@@ -273,8 +273,8 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
PT LUMBUNG TELUR INDONESIA
</Text>
<Text style={pdfStyles.address}>
Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten
Bandung Barat, Jawa Barat 40514
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
Cipedes, Kec. Sukajadi, Kota Bandung 40162
</Text>
<View style={pdfStyles.divider} />
</View>
@@ -47,7 +47,7 @@ export const generateReportExpensePDF = async (
doc.setFontSize(7);
doc.setTextColor(102, 102, 102);
doc.text(
'Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten Bandung Barat, Jawa Barat 40514',
'SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. Cipedes, Kec. Sukajadi, Kota Bandung 40162',
marginX,
25
);
@@ -33,18 +33,18 @@ import { generateReportExpensePDF } from '../export/ReportExpenseExportPDF';
import { generateReportExpenseExcel } from '../export/ReportExpenseExportXLSX';
import toast from 'react-hot-toast';
import {
KandangApi,
LocationApi,
NonstockApi,
SupplierApi,
} from '@/services/api/master-data';
import { Supplier } from '@/types/api/master-data/supplier';
import { Kandang } from '@/types/api/master-data/kandang';
import { Nonstock } from '@/types/api/master-data/nonstock';
import { ColumnDef } from '@tanstack/react-table';
import { httpClient } from '@/services/http/client';
import { BaseApiResponse } from '@/types/api/api-general';
import ButtonFilter from '@/components/helper/ButtonFilter';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
import { ProjectFlockKandangApi } from '@/services/api/production/project-flock-kandang';
interface ReportExpenseTabProps {
tabId: string;
@@ -67,6 +67,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
// ===== SUBMISSION STATE =====
const [isSubmitted, setIsSubmitted] = useState(false);
const [filterParams, setFilterParams] = useState<FilterParams>({});
// ===== PAGINATION STATE =====
@@ -116,10 +117,12 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
: undefined,
});
filterModal.closeModal();
setIsSubmitted(true);
setPage(1);
},
onReset: () => {
setFilterParams({});
setIsSubmitted(false);
setPage(1);
filterModal.closeModal();
},
@@ -136,7 +139,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
options: locationOptions,
isLoadingOptions: isLoadingLocations,
loadMore: loadMoreLocations,
} = useSelect<Location>(LocationApi.basePath, 'id', 'name', 'search');
} = useSelect<Kandang>(LocationApi.basePath, 'id', 'name', 'search');
const {
setInputValue: setSupplierInputValue,
@@ -146,14 +149,14 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search');
const {
setInputValue: setProjectFlockKandangInputValue,
options: projectFlockKandangOptions,
isLoadingOptions: isLoadingProjectFlockKandangs,
loadMore: loadMoreProjectFlockKandangs,
} = useSelect<ProjectFlockKandang>(
ProjectFlockKandangApi.basePath,
setInputValue: setKandangInputValue,
options: kandangOptions,
isLoadingOptions: isLoadingKandangs,
loadMore: loadMoreKandangs,
} = useSelect<Kandang>(
KandangApi.basePath,
'id',
'name_with_period',
'name',
'search',
formik.values.location_id?.value
? { location_id: String(formik.values.location_id.value) }
@@ -191,25 +194,27 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
// ===== DATA FETCHING =====
const { data: reportExpenseResponse, isLoading } = useSWR(
() => {
const params = new URLSearchParams();
if (filterParams.location_id)
params.append('location_id', filterParams.location_id);
if (filterParams.supplier_id)
params.append('supplier_id', filterParams.supplier_id);
if (filterParams.kandang_id)
params.append('project_flock_kandang_id', filterParams.kandang_id);
if (filterParams.nonstock_id)
params.append('nonstock_id', filterParams.nonstock_id);
if (filterParams.realization_date)
params.append('realization_date', filterParams.realization_date);
if (filterParams.category)
params.append('category', filterParams.category);
params.append('page', String(page));
params.append('limit', String(pageSize));
isSubmitted
? () => {
const params = new URLSearchParams();
if (filterParams.location_id)
params.append('location_id', filterParams.location_id);
if (filterParams.supplier_id)
params.append('supplier_id', filterParams.supplier_id);
if (filterParams.kandang_id)
params.append('kandang_id', filterParams.kandang_id);
if (filterParams.nonstock_id)
params.append('nonstock_id', filterParams.nonstock_id);
if (filterParams.realization_date)
params.append('realization_date', filterParams.realization_date);
if (filterParams.category)
params.append('category', filterParams.category);
params.append('page', String(page));
params.append('limit', String(pageSize));
return [`${ReportExpenseApi.basePath}?${params.toString()}`];
},
return [`${ReportExpenseApi.basePath}?${params.toString()}`];
}
: null,
([url]: string[]) => httpClient<BaseApiResponse<ReportExpense[]>>(url)
);
@@ -524,13 +529,25 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
<>
{TabActionsElement}
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{isLoading && (
{!isSubmitted ? (
<ReportExpenseSkeleton
columns={columns}
icon={
<Icon
icon='heroicons:funnel'
className='text-white'
width={20}
height={20}
/>
}
title='No Filters Selected'
subtitle='Please choose filters to narrow down your results and make your search easier.'
/>
) : isLoading ? (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
)}
{!isLoading && (!data || data.length === 0) && (
) : !data || data.length === 0 ? (
<ReportExpenseSkeleton
columns={columns}
icon={
@@ -544,9 +561,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
title='Data Not Yet Available'
subtitle='Please change your filters to get the data.'
/>
)}
{!isLoading && data.length > 0 && (
) : (
<>
<Table
data={data}
@@ -643,14 +658,14 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
<SelectInput
label='Kandang'
placeholder='Pilih Kandang'
options={projectFlockKandangOptions}
isLoading={isLoadingProjectFlockKandangs}
options={kandangOptions}
isLoading={isLoadingKandangs}
value={kandangValue}
onChange={(val) => {
formik.setFieldValue('kandang_id', val);
}}
onInputChange={setProjectFlockKandangInputValue}
onMenuScrollToBottom={loadMoreProjectFlockKandangs}
onInputChange={setKandangInputValue}
onMenuScrollToBottom={loadMoreKandangs}
isClearable
isDisabled={!formik.values.location_id}
className={{ wrapper: 'w-full' }}
@@ -61,6 +61,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
const [pageSize] = useState(10);
// ===== SUBMISSION STATE =====
const [isSubmitted, setIsSubmitted] = useState(false);
const [filterParams, setFilterParams] = useState<FilterParams>({});
const [dateErrorShown, setDateErrorShown] = useState(false);
const [hasDateError, setHasDateError] = useState(false);
@@ -101,11 +102,13 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
filter_by: values.filter_by || undefined,
});
filterModal.closeModal();
setIsSubmitted(true);
setCurrentPage(1);
setSubmitting(false);
},
onReset: () => {
setFilterParams({});
setIsSubmitted(false);
setCurrentPage(1);
setHasDateError(false);
if (dateErrorShown) {
@@ -215,21 +218,23 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
// ===== DATA FETCHING =====
const { data: customerPayment, isLoading } = useSWR(
() => {
const params = {
customer_ids: filterParams.customer_ids,
filter_by: filterParams.filter_by as
| 'trans_date'
| 'realization_date'
| undefined,
start_date: filterParams.start_date,
end_date: filterParams.end_date,
page: currentPage,
limit: pageSize,
};
isSubmitted
? () => {
const params = {
customer_ids: filterParams.customer_ids,
filter_by: filterParams.filter_by as
| 'trans_date'
| 'realization_date'
| undefined,
start_date: filterParams.start_date,
end_date: filterParams.end_date,
page: currentPage,
limit: pageSize,
};
return ['customer-payment-report', params];
},
return ['customer-payment-report', params];
}
: null,
([, params]) =>
FinanceApi.getCustomerPaymentReport(
params.customer_ids,
@@ -695,13 +700,25 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
<>
{TabActionsElement}
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{isLoading && (
{!isSubmitted ? (
<CustomerSupplierSkeleton
columns={getTableColumns({} as CustomerPaymentSummary)}
icon={
<Icon
icon='heroicons:funnel'
className='text-white'
width={20}
height={20}
/>
}
title='No Filters Selected'
subtitle='Please choose filters to narrow down your results and make your search easier.'
/>
) : isLoading ? (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
)}
{!isLoading && data.length === 0 && (
) : data.length === 0 ? (
<CustomerSupplierSkeleton
columns={getTableColumns({} as CustomerPaymentSummary)}
icon={
@@ -715,10 +732,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
title='Data Not Yet Available'
subtitle='Please change your filters to get the data.'
/>
)}
{!isLoading &&
data.length > 0 &&
) : (
data.map((customerReport) => {
const summary = customerReport.summary || {
total_qty: 0,
@@ -747,6 +761,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
}}
variant='bordered'
collapsible={true}
defaultCollapsed={true}
>
<Table
data={[
@@ -810,7 +825,8 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
/>
</Card>
);
})}
})
)}
</div>
{/* Filter Modal */}
@@ -85,6 +85,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
supplier_ids: undefined,
filter_by: undefined,
});
const [isSubmitted, setIsSubmitted] = useState(false);
// ===== DATE ERROR STATE =====
const [dateErrorShown, setDateErrorShown] = useState(false);
@@ -128,7 +129,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
filter_by: values.filterBy?.value?.toString() || undefined,
});
filterModal.closeModal();
// setIsSubmitted(true);
setIsSubmitted(true);
},
onReset: () => {
setFilterParams({
@@ -137,7 +138,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
supplier_ids: undefined,
filter_by: undefined,
});
// setIsSubmitted(false);
setIsSubmitted(false);
filterModal.closeModal();
},
});
@@ -149,16 +150,18 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
// ===== DATA FETCHING =====
const { data: debtSupplier, isLoading } = useSWR(
() => {
const params = {
supplier_ids: filterParams.supplier_ids,
filter_by: filterParams.filter_by,
start_date: filterParams.start_date,
end_date: filterParams.end_date,
};
isSubmitted
? () => {
const params = {
supplier_ids: filterParams.supplier_ids,
filter_by: filterParams.filter_by,
start_date: filterParams.start_date,
end_date: filterParams.end_date,
};
return ['debt-supplier-report', params];
},
return ['debt-supplier-report', params];
}
: null,
([, params]) =>
DebtSupplierApi.getDebtSupplierReport(
params.supplier_ids,
@@ -608,13 +611,25 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
<>
{TabActionsElement}
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{isLoading && (
{!isSubmitted ? (
<DebtSupplierSkeleton
columns={getTableColumns()}
icon={
<Icon
icon='heroicons:funnel'
className='text-white'
width={20}
height={20}
/>
}
title='No Filters Selected'
subtitle='Please choose filters to narrow down your results and make your search easier.'
/>
) : isLoading ? (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
)}
{!isLoading && data.length === 0 && (
) : data.length === 0 ? (
<DebtSupplierSkeleton
columns={getTableColumns()}
icon={
@@ -628,10 +643,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
title='Data Not Yet Available'
subtitle='Please change your filters to get the data.'
/>
)}
{!isLoading &&
data.length > 0 &&
) : (
data.map((supplierReport) => {
return (
<Card
@@ -646,6 +658,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
}}
variant='bordered'
collapsible={true}
defaultCollapsed={true}
>
<Table
data={[
@@ -716,7 +729,8 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
/>
</Card>
);
})}
})
)}
</div>
{/* Filter Modal */}
@@ -61,6 +61,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
// ===== SUBMISSION STATE =====
const [filterParams, setFilterParams] = useState<FilterParams>({});
const [isSubmitted, setIsSubmitted] = useState(false);
const [dateErrorShown, setDateErrorShown] = useState(false);
const [hasDateError, setHasDateError] = useState(false);
@@ -69,34 +70,24 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
const filterModal = useModal();
// ===== OPTIONS =====
const {
options: areaOptions,
isLoadingOptions: isLoadingAreas,
setInputValue: setAreaInputValue,
loadMore: loadMoreArea,
} = useSelect(AreaApi.basePath, 'id', 'name', 'search');
const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect(
AreaApi.basePath,
'id',
'name',
'search'
);
const {
options: supplierOptions,
isLoadingOptions: isLoadingSuppliers,
setInputValue: setSupplierInputValue,
loadMore: loadMoreSupplier,
} = useSelect(SupplierApi.basePath, 'id', 'name', 'search', {
category: 'SAPRONAK',
});
const { options: supplierOptions, isLoadingOptions: isLoadingSuppliers } =
useSelect(SupplierApi.basePath, 'id', 'name', 'search', {
category: 'SAPRONAK',
});
const {
options: productOptions,
isLoadingOptions: isLoadingProducts,
setInputValue: setProductInputValue,
loadMore: loadMoreProduct,
} = useSelect(ProductApi.basePath, 'id', 'name', 'search');
const { options: productOptions, isLoadingOptions: isLoadingProducts } =
useSelect(ProductApi.basePath, 'id', 'name', 'search');
const {
options: productCategoryOptions,
isLoadingOptions: isLoadingProductCategories,
setInputValue: setProductCategoryInputValue,
loadMore: loadMoreProductCategory,
} = useSelect(ProductCategoryApi.basePath, 'id', 'name', 'search');
const dataTypeOptions = useMemo(
@@ -140,11 +131,13 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
sort_by: values.sort_by || undefined,
});
filterModal.closeModal();
setIsSubmitted(true);
setCurrentPage(1);
setSubmitting(false);
},
onReset: () => {
setFilterParams({});
setIsSubmitted(false);
setCurrentPage(1);
setHasDateError(false);
if (dateErrorShown) {
@@ -268,22 +261,24 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
// ===== DATA FETCHING =====
const { data: purchasePerSupplier, isLoading } = useSWR(
() => {
const params = {
area_id: filterParams.area_id,
supplier_id: filterParams.supplier_id,
product_id: filterParams.product_id,
product_category_id: filterParams.product_category_id,
start_date: filterParams.start_date,
end_date: filterParams.end_date,
sort_by: filterParams.sort_by,
filter_by: filterParams.filter_by,
page: currentPage,
limit: pageSize,
};
isSubmitted
? () => {
const params = {
area_id: filterParams.area_id,
supplier_id: filterParams.supplier_id,
product_id: filterParams.product_id,
product_category_id: filterParams.product_category_id,
start_date: filterParams.start_date,
end_date: filterParams.end_date,
sort_by: filterParams.sort_by,
filter_by: filterParams.filter_by,
page: currentPage,
limit: pageSize,
};
return ['logistic-purchase-report', params];
},
return ['logistic-purchase-report', params];
}
: null,
([, params]) =>
LogisticApi.getLogisticPurchasePerSupplierReport(
params.area_id,
@@ -731,7 +726,21 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
<>
{TabActionsElement}
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{isLoading && (
{!isSubmitted ? (
<PurchasePerSupplierSkeleton
columns={getTableColumns({} as LogisticPurchasePerSupplierSummary)}
icon={
<Icon
icon='heroicons:funnel'
className='text-white'
width={20}
height={20}
/>
}
title='No Filters Selected'
subtitle='Please choose filters to narrow down your results and make your search easier.'
/>
) : isLoading ? (
<PurchasePerSupplierSkeleton
columns={getTableColumns({} as LogisticPurchasePerSupplierSummary)}
icon={
@@ -745,9 +754,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
title='Memuat Data Pembelian Per Supplier'
subtitle='Silakan tunggu sebentar...'
/>
)}
{!isLoading && data.length === 0 && (
) : data.length === 0 ? (
<PurchasePerSupplierSkeleton
columns={getTableColumns({} as LogisticPurchasePerSupplierSummary)}
icon={
@@ -761,10 +768,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
title='Data Not Yet Available'
subtitle='Please change your filters to get the data.'
/>
)}
{!isLoading &&
data.length > 0 &&
) : (
data.map((supplierReport) => {
const summary = supplierReport.summary || {
total_qty: 0,
@@ -794,6 +798,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
}}
variant='bordered'
collapsible={true}
defaultCollapsed={true}
>
<Table
data={supplierReport.rows}
@@ -822,7 +827,8 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
/>
</Card>
);
})}
})
)}
</div>
{/* Filter Modal */}
@@ -901,8 +907,6 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
isLoading={isLoadingAreas}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setAreaInputValue}
onMenuScrollToBottom={loadMoreArea}
/>
{/* Supplier Filter */}
@@ -922,8 +926,6 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
isLoading={isLoadingSuppliers}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setSupplierInputValue}
onMenuScrollToBottom={loadMoreSupplier}
/>
{/* Product Filter */}
@@ -943,8 +945,6 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
isLoading={isLoadingProducts}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setProductInputValue}
onMenuScrollToBottom={loadMoreProduct}
/>
{/* Product Category Filter */}
@@ -964,8 +964,6 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
isLoading={isLoadingProductCategories}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setProductCategoryInputValue}
onMenuScrollToBottom={loadMoreProductCategory}
/>
{/* Filter By Type */}
@@ -81,7 +81,7 @@ const getTableColumns = (
},
{
key: 'warehouse',
header: 'Gudang Fisik',
header: 'Gudang',
flex: 1.2,
align: 'left',
cell: ({ row }) => row.warehouse?.name ?? '-',
@@ -30,7 +30,7 @@ export const generateDailyMarketingExcel = async (
{ header: 'Tanggal Jual', key: 'soDate', width: 15 },
{ header: 'Tanggal Realisasi', key: 'realizationDate', width: 18 },
{ header: 'Aging', key: 'aging', width: 10 },
{ header: 'Gudang Fisik', key: 'warehouse', width: 25 },
{ header: 'Gudang', key: 'warehouse', width: 25 },
{ header: 'Pelanggan', key: 'customer', width: 25 },
{ header: 'No. DO', key: 'doNumber', width: 15 },
{ header: 'Sales/Marketing', key: 'sales', width: 20 },
@@ -97,7 +97,7 @@ export const generateDailyMarketingExcel = async (
});
}
worksheet.columns.forEach((column: { width?: number }) => {
worksheet.columns.forEach((column) => {
if (column.width && column.width < 10) {
column.width = 10;
}
@@ -70,6 +70,9 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
// ===== SUBMISSION STATE =====
const [isSubmitted, setIsSubmitted] = useState(false);
// ===== SEARCH STATE =====
const [searchValue, setSearchValue] = useState<string>('');
@@ -85,33 +88,21 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
const filterModal = useModal();
// ===== OPTIONS =====
const {
options: areaOptions,
isLoadingOptions: isLoadingAreas,
setInputValue: setAreaInputValue,
loadMore: loadMoreArea,
} = useSelect(AreaApi.basePath, 'id', 'name', 'search');
const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect(
AreaApi.basePath,
'id',
'name',
'search'
);
const {
options: locationOptions,
isLoadingOptions: isLoadingLocations,
setInputValue: setLocationInputValue,
loadMore: loadMoreLocation,
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
const { options: locationOptions, isLoadingOptions: isLoadingLocations } =
useSelect(LocationApi.basePath, 'id', 'name', 'search');
const {
options: warehouseOptions,
isLoadingOptions: isLoadingWarehouses,
setInputValue: setWarehouseInputValue,
loadMore: loadMoreWarehouse,
} = useSelect(WarehouseApi.basePath, 'id', 'name', 'search');
const { options: warehouseOptions, isLoadingOptions: isLoadingWarehouses } =
useSelect(WarehouseApi.basePath, 'id', 'name', 'search');
const {
options: customerOptions,
isLoadingOptions: isLoadingCustomers,
setInputValue: setCustomerInputValue,
loadMore: loadMoreCustomer,
} = useSelect(CustomerApi.basePath, 'id', 'name', 'search');
const { options: customerOptions, isLoadingOptions: isLoadingCustomers } =
useSelect(CustomerApi.basePath, 'id', 'name', 'search');
// ===== FORMIK SETUP =====
const formik = useFormik<DailyMarketingReportFilterType>({
@@ -141,10 +132,12 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
sort_by: values.sort_by || undefined,
});
filterModal.closeModal();
setIsSubmitted(true);
setSubmitting(false);
},
onReset: () => {
setFilterParams({});
setIsSubmitted(false);
filterModal.closeModal();
},
});
@@ -218,28 +211,31 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
// ===== DATA FETCHING =====
const { data: dailyMarketings, isLoading } = useSWR(
() => {
const params = new URLSearchParams();
isSubmitted
? () => {
const params = new URLSearchParams();
if (searchValue) params.set('search', searchValue);
if (filterParams.area_id) params.set('area_id', filterParams.area_id);
if (filterParams.location_id)
params.set('location_id', filterParams.location_id);
if (filterParams.warehouse_id)
params.set('warehouse_id', filterParams.warehouse_id);
if (filterParams.customer_id)
params.set('customer_id', filterParams.customer_id);
if (filterParams.start_date)
params.set('start_date', filterParams.start_date);
if (filterParams.end_date) params.set('end_date', filterParams.end_date);
if (filterParams.filter_by)
params.set('filter_by', filterParams.filter_by);
if (filterParams.marketing_type)
params.set('marketing_type', filterParams.marketing_type);
if (filterParams.sort_by) params.set('sort_by', filterParams.sort_by);
if (searchValue) params.set('search', searchValue);
if (filterParams.area_id) params.set('area_id', filterParams.area_id);
if (filterParams.location_id)
params.set('location_id', filterParams.location_id);
if (filterParams.warehouse_id)
params.set('warehouse_id', filterParams.warehouse_id);
if (filterParams.customer_id)
params.set('customer_id', filterParams.customer_id);
if (filterParams.start_date)
params.set('start_date', filterParams.start_date);
if (filterParams.end_date)
params.set('end_date', filterParams.end_date);
if (filterParams.filter_by)
params.set('filter_by', filterParams.filter_by);
if (filterParams.marketing_type)
params.set('marketing_type', filterParams.marketing_type);
if (filterParams.sort_by) params.set('sort_by', filterParams.sort_by);
return ['daily-marketing-report', params.toString()];
},
return ['daily-marketing-report', params.toString()];
}
: null,
([, params]) =>
MarketingReportApi.getAllDailyMarketingFetcher(
`${MarketingReportApi.basePath}?${params}`
@@ -512,7 +508,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
},
{
id: 'warehouse',
header: 'Gudang Fisik',
header: 'Gudang',
accessorKey: 'warehouse',
cell: ({ row }) => row.original.warehouse.name,
footer: () => <div className='font-semibold text-gray-900'>-</div>,
@@ -652,7 +648,21 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
<>
{TabActionsElement}
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{isLoading && (
{!isSubmitted ? (
<DailyMarketingReportSkeleton
columns={getTableColumns()}
icon={
<Icon
icon='heroicons:funnel'
className='text-white'
width={20}
height={20}
/>
}
title='No Filters Selected'
subtitle='Please choose filters to narrow down your results and make your search easier.'
/>
) : isLoading ? (
<DailyMarketingReportSkeleton
columns={getTableColumns()}
icon={
@@ -666,9 +676,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
title='Memuat Data Penjualan Harian'
subtitle='Silakan tunggu sebentar...'
/>
)}
{!isLoading && data.length === 0 && (
) : data.length === 0 ? (
<DailyMarketingReportSkeleton
columns={getTableColumns()}
icon={
@@ -682,9 +690,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
title='Data Not Yet Available'
subtitle='Please change your filters to get the data.'
/>
)}
{!isLoading && data.length > 0 && (
) : (
<Table
data={data}
columns={getTableColumns()}
@@ -831,8 +837,6 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
}}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setAreaInputValue}
onMenuScrollToBottom={loadMoreArea}
/>
{/* Location Filter */}
@@ -850,14 +854,12 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
}}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setLocationInputValue}
onMenuScrollToBottom={loadMoreLocation}
/>
{/* Warehouse Filter */}
<SelectInput
label='Gudang Fisik'
placeholder='Pilih Gudang Fisik'
label='Gudang'
placeholder='Pilih Gudang'
options={warehouseOptions}
isLoading={isLoadingWarehouses}
value={warehouseValue}
@@ -869,8 +871,6 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
}}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setWarehouseInputValue}
onMenuScrollToBottom={loadMoreWarehouse}
/>
{/* Customer Filter */}
@@ -888,8 +888,6 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
}}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setCustomerInputValue}
onMenuScrollToBottom={loadMoreCustomer}
/>
{/* Filter By Date Type */}
@@ -71,32 +71,24 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
const filterModal = useModal();
// ===== OPTIONS =====
const {
options: areaOptions,
isLoadingOptions: isLoadingAreas,
setInputValue: setAreaInputValue,
loadMore: loadMoreArea,
} = useSelect(AreaApi.basePath, 'id', 'name', 'search');
const {
options: locationOptions,
isLoadingOptions: isLoadingLocations,
setInputValue: setLocationInputValue,
loadMore: loadMoreLocation,
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
const {
options: kandangOptions,
isLoadingOptions: isLoadingKandangs,
setInputValue: setKandangInputValue,
loadMore: loadMoreKandang,
} = useSelect(
ProjectFlockKandangApi.basePath,
const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect(
AreaApi.basePath,
'id',
'name_with_period',
'name',
'search'
);
const { options: locationOptions, isLoadingOptions: isLoadingLocations } =
useSelect(LocationApi.basePath, 'id', 'name', 'search');
const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } =
useSelect(
ProjectFlockKandangApi.basePath,
'id',
'name_with_period',
'search'
);
const showUnrecordedOptions = useMemo(
() => [
{ value: 'false', label: 'Sembunyikan' },
@@ -791,10 +783,6 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
[data, perWeightRangeSummary]
);
useEffectHook(() => {
filterModal.openModal();
}, []);
return (
<>
{TabActionsElement}
@@ -930,8 +918,6 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
isLoading={isLoadingAreas}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setAreaInputValue}
onMenuScrollToBottom={loadMoreArea}
/>
{/* Location Filter */}
@@ -951,8 +937,6 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
isLoading={isLoadingLocations}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setLocationInputValue}
onMenuScrollToBottom={loadMoreLocation}
/>
{/* Kandang Filter */}
@@ -972,8 +956,6 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
isLoading={isLoadingKandangs}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setKandangInputValue}
onMenuScrollToBottom={loadMoreKandang}
/>
{/* Weight Range Filter */}
@@ -43,7 +43,15 @@ export const ProductionResultFilterSchema = yup.object({
}
return !!value;
}),
kandang_id: yup.mixed<OptionType>().nullable(),
kandang_id: 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<ProductionResultFilterFormType>;
export type ProductionResultFilterValues = yup.InferType<
@@ -46,7 +46,6 @@ import Modal, { useModal } from '@/components/Modal';
import { formatNumber } from '@/lib/helper';
import Pagination from '@/components/Pagination';
import ProductionResultSkeleton from '@/components/pages/report/production-result/skeleton/ProductionResultSkeleton';
import { ProjectFlock } from '@/types/api/production/project-flock';
interface ProductionResultTabProps {
tabId: string;
@@ -239,17 +238,6 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
? String(values.kandang_id.value)
: undefined,
});
const selectedProjectFlockKandangRawData = isResponseSuccess(
projectFlockKandangsRawData
)
? projectFlockKandangsRawData.data.find(
(item) => item.id === values.kandang_id?.value
)
: undefined;
setSelectedProjectFlockKandang(selectedProjectFlockKandangRawData);
filterModal.closeModal();
setIsSubmitted(true);
setPage(1);
@@ -267,9 +255,6 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
formik.validateForm();
};
const [selectedProjectFlockKandang, setSelectedProjectFlockKandang] =
useState<ProjectFlockKandang | undefined>();
// ===== OPTIONS =====
const {
setInputValue: setAreaInputValue,
@@ -294,7 +279,7 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
options: projectFlockOptions,
isLoadingOptions: isLoadingProjectFlocks,
loadMore: loadMoreProjectFlocks,
} = useSelect<ProjectFlock>(
} = useSelect<BaseKandang>(
ProjectFlockApi.basePath,
'id',
'flock_name',
@@ -315,11 +300,10 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
options: projectFlockKandangOptions,
isLoadingOptions: isLoadingProjectFlockKandangs,
loadMore: loadMoreProjectFlockKandangs,
rawData: projectFlockKandangsRawData,
} = useSelect<ProjectFlockKandang>(
} = useSelect<BaseKandang>(
ProjectFlockKandangApi.basePath,
'id',
'name_with_period',
'kandang.name',
'search',
{
area_id: formik.values.area_id?.value
@@ -375,15 +359,13 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
([url]: string[]) => httpClient<BaseApiResponse<ProjectFlockKandang[]>>(url)
);
const projectFlockKandangs = useMemo(() => {
if (selectedProjectFlockKandang) {
return [selectedProjectFlockKandang];
}
return isResponseSuccess(projectFlockKandangsData)
? projectFlockKandangsData.data
: null;
}, [projectFlockKandangsData, selectedProjectFlockKandang]);
const projectFlockKandangs = useMemo(
() =>
isResponseSuccess(projectFlockKandangsData)
? projectFlockKandangsData.data
: null,
[projectFlockKandangsData]
);
const projectFlockKandangMetadata = useMemo(
() =>
@@ -649,10 +631,6 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
// Render the TabActions component
const TabActionsElement = useMemo(() => <TabActions />, [TabActions]);
useEffect(() => {
filterModal.openModal();
}, []);
return (
<>
{TabActionsElement}
@@ -822,6 +800,7 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
/>
<SelectInput
required
label='Kandang'
placeholder='Pilih Kandang'
options={projectFlockKandangOptions}
-12
View File
@@ -20,7 +20,6 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
'lti.daily_checklist.master_data.employee',
'lti.daily_checklist.master_data.activity',
'lti.daily_checklist.master_data.configuration',
'lti.daily_checklist.master_data.kandang',
],
submenu: [
{
@@ -67,11 +66,6 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
link: '/daily-checklist/master-data/activity',
permission: ['lti.daily_checklist.master_data.activity'],
},
{
text: 'Kandang',
link: '/daily-checklist/master-data/kandang',
permission: ['lti.daily_checklist.master_data.kandang'],
},
{
text: 'Konfigurasi',
link: '/daily-checklist/master-data/configuration',
@@ -555,12 +549,6 @@ export const APPROVAL_WORKFLOWS = {
],
};
export const PROJECT_FLOCK_STATUS = {
PENGAJUAN: APPROVAL_WORKFLOWS.PROJECT_FLOCKS[0].step_name,
AKTIF: APPROVAL_WORKFLOWS.PROJECT_FLOCKS[1].step_name,
SELESAI: APPROVAL_WORKFLOWS.PROJECT_FLOCKS[2].step_name,
} as const;
export const ACCEPTED_FILE_TYPE = {
PDF: {
'application/pdf': ['.pdf'],
-3
View File
@@ -21,9 +21,6 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
'/daily-checklist/master-data/configuration/': [
'lti.daily_checklist.master_data.configuration',
],
'/daily-checklist/master-data/kandang/': [
'lti.daily_checklist.master_data.kandang',
],
// Production
// Production - Project Flock
@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { Check, ChevronsUpDown, X, Loader2 } from 'lucide-react';
import { Check, ChevronsUpDown, X } from 'lucide-react';
import { cn } from '@/lib/helper';
import { Button } from '@/figma-make/components/base/button';
import {
@@ -29,8 +29,6 @@ interface MultiSelectProps {
selected: string[];
onChange: (selected: string[]) => void;
onSearchChange?: (value: string) => void;
onLoadMore?: () => void;
isLoadingMore?: boolean;
placeholder?: string;
className?: string;
disabled?: boolean;
@@ -41,8 +39,6 @@ export function MultiSelect({
selected,
onChange,
onSearchChange,
onLoadMore,
isLoadingMore,
placeholder = 'Select items...',
className,
disabled,
@@ -119,18 +115,7 @@ export function MultiSelect({
onValueChange={onSearchChange}
/>
<CommandEmpty>No item found.</CommandEmpty>
<CommandList
className='max-h-[300px] overflow-y-auto'
onScroll={(e) => {
const target = e.currentTarget;
if (
target.scrollHeight - target.scrollTop <=
target.clientHeight + 1
) {
onLoadMore?.();
}
}}
>
<CommandList className='max-h-[300px] overflow-y-auto'>
<CommandGroup className='overflow-visible'>
{options.map((option) => (
<CommandItem
@@ -149,11 +134,6 @@ export function MultiSelect({
{option.label}
</CommandItem>
))}
{isLoadingMore && (
<div className='py-4 flex justify-center w-full'>
<Loader2 className='h-4 w-4 animate-spin text-muted-foreground' />
</div>
)}
</CommandGroup>
</CommandList>
</Command>
+2 -7
View File
@@ -55,11 +55,7 @@ function SelectContent({
children,
position = 'popper',
...props
}: React.ComponentProps<typeof SelectPrimitive.Content> & {
onScroll?: React.UIEventHandler<HTMLDivElement>;
}) {
const { onScroll, ...restProps } = props;
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
@@ -71,7 +67,7 @@ function SelectContent({
className
)}
position={position}
{...restProps}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
@@ -80,7 +76,6 @@ function SelectContent({
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1'
)}
onScroll={onScroll}
>
{children}
</SelectPrimitive.Viewport>
@@ -2,16 +2,7 @@
import * as React from 'react';
import { useState, useEffect } from 'react';
import {
Plus,
X,
Save,
Send,
Info,
FilePlus,
ListChecks,
Loader2,
} from 'lucide-react';
import { Plus, X, Save, Send, Info, FilePlus, ListChecks } from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button';
import { Label } from '@/figma-make/components/base/label';
@@ -35,6 +26,7 @@ import {
import { DatePicker } from '@/figma-make/components/base/date-picker';
import { toast } from 'sonner';
import { useSelect } from '@/components/input/SelectInput';
import { KandangApi } from '@/services/api/master-data';
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import useSWR from 'swr';
@@ -51,7 +43,6 @@ import DropFileInput from '@/components/input/DropFileInput';
import Link from 'next/link';
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
import { Icon } from '@iconify/react';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
// Static categories
const CATEGORIES = [
@@ -95,11 +86,16 @@ export function DailyChecklistContent() {
searchParams.get('category') || ''
);
const {
options: kandangOptions,
isLoadingMore: isLoadingMoreKandang,
loadMore: loadMoreKandang,
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
const { options: kandangOptions } = useSelect(
KandangApi.basePath,
'id',
'name',
'search',
{
page: '1',
limit: '100',
}
);
const { data: phases } = useSWR<
BaseApiResponse<Phase[] | undefined>,
@@ -172,16 +168,6 @@ export function DailyChecklistContent() {
const [documents, setDocuments] = useState<File[]>([]);
const [deletedDocumentIds, setDeletedDocumentIds] = useState<number[]>([]);
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement;
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
}
};
// Sync state to URL query params
useEffect(() => {
const params = new URLSearchParams(searchParams.toString());
@@ -315,8 +301,93 @@ export function DailyChecklistContent() {
checkAndLoadChecklist();
}, [date, kandangId, selectedCategory]);
// Load employees when kandang changes
useEffect(() => {
if (kandangId) {
// ✅ Clear selected employees ketika kandang berubah (reset ABK assignment)
setSelectedEmployees([]);
setAssignments({});
} else {
setSelectedEmployees([]);
setAssignments({});
}
}, [kandangId]);
// Load activities and tasks when phases change
useEffect(() => {
const loadAssignments = async (taskIds: string[]) => {
if (taskIds.length === 0) return;
try {
const existingDailyChecklist =
await DailyChecklistApi.getOneDailyChecklist(
String(dailyChecklistId)
);
if (isResponseError(existingDailyChecklist)) {
console.error(
'Error loading assignments:',
existingDailyChecklist.message
);
return;
}
// set existing document
setExistingDocuments(existingDailyChecklist?.data.document_urls || []);
// Build assignments map
const assignmentMap: {
[taskId: string]: {
[employeeId: string]: { checked: boolean; note: string };
};
} = {};
(existingDailyChecklist?.data.tasks || []).forEach(
(dailyChecklistTask) => {
if (!assignmentMap[dailyChecklistTask.id]) {
assignmentMap[dailyChecklistTask.id] = {};
}
dailyChecklistTask.assignments.forEach((assignment) => {
if (!assignmentMap[dailyChecklistTask.id]) {
assignmentMap[dailyChecklistTask.id] = {};
}
assignmentMap[dailyChecklistTask.id][assignment.employee.id] = {
checked: assignment.checked,
note: assignment.note || '',
};
});
}
);
setAssignments(assignmentMap);
// Load employees from assignments
const employeeIds = Array.from(
new Set(
(existingDailyChecklist?.data.assigned_employees || []).map(
(a) => a.id
)
)
);
if (employeeIds.length > 0) {
const existingDailyChecklist =
await DailyChecklistApi.getOneDailyChecklist(
String(dailyChecklistId)
);
if (isResponseSuccess(existingDailyChecklist)) {
setSelectedEmployees(
existingDailyChecklist.data.assigned_employees
);
}
}
} catch (error) {
console.error('Error loading assignments:', error);
}
};
const loadActivitiesAndTasks = async () => {
if (!dailyChecklistId || selectedPhaseIds.length === 0) {
setActivitiesByPhase({});
@@ -391,87 +462,6 @@ export function DailyChecklistContent() {
loadActivitiesAndTasks();
}, [dailyChecklistId, selectedPhaseIds]);
// Load employees when kandang changes
useEffect(() => {
if (kandangId) {
// ✅ Clear selected employees ketika kandang berubah (reset ABK assignment)
setSelectedEmployees([]);
setAssignments({});
} else {
setSelectedEmployees([]);
setAssignments({});
}
}, [kandangId]);
const loadAssignments = async (taskIds: string[]) => {
if (taskIds.length === 0) return;
try {
const existingDailyChecklist =
await DailyChecklistApi.getOneDailyChecklist(String(dailyChecklistId));
if (isResponseError(existingDailyChecklist)) {
console.error(
'Error loading assignments:',
existingDailyChecklist.message
);
return;
}
// set existing document
setExistingDocuments(existingDailyChecklist?.data.document_urls || []);
// Build assignments map
const assignmentMap: {
[taskId: string]: {
[employeeId: string]: { checked: boolean; note: string };
};
} = {};
(existingDailyChecklist?.data.tasks || []).forEach(
(dailyChecklistTask) => {
if (!assignmentMap[dailyChecklistTask.id]) {
assignmentMap[dailyChecklistTask.id] = {};
}
dailyChecklistTask.assignments.forEach((assignment) => {
if (!assignmentMap[dailyChecklistTask.id]) {
assignmentMap[dailyChecklistTask.id] = {};
}
assignmentMap[dailyChecklistTask.id][assignment.employee.id] = {
checked: assignment.checked,
note: assignment.note || '',
};
});
}
);
setAssignments(assignmentMap);
// Load employees from assignments
const employeeIds = Array.from(
new Set(
(existingDailyChecklist?.data.assigned_employees || []).map(
(a) => a.id
)
)
);
if (employeeIds.length > 0) {
const existingDailyChecklist =
await DailyChecklistApi.getOneDailyChecklist(
String(dailyChecklistId)
);
if (isResponseSuccess(existingDailyChecklist)) {
setSelectedEmployees(existingDailyChecklist.data.assigned_employees);
}
}
} catch (error) {
console.error('Error loading assignments:', error);
}
};
// Phase selection modal
const handleAddPhase = () => {
if (!selectedCategory) {
@@ -1008,7 +998,7 @@ export function DailyChecklistContent() {
>
<SelectValue placeholder='Pilih kandang' />
</SelectTrigger>
<SelectContent onScroll={handleKandangScroll}>
<SelectContent>
{kandangOptions.map((kandang) => (
<SelectItem
key={kandang.value}
@@ -1017,12 +1007,6 @@ export function DailyChecklistContent() {
{kandang.label}
</SelectItem>
))}
{isLoadingMoreKandang && (
<div className='flex justify-center p-2'>
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
</div>
)}
</SelectContent>
</Select>
</div>
@@ -1,7 +1,6 @@
'use client';
import { useState } from 'react';
import moment from 'moment';
import {
Card,
CardContent,
@@ -17,7 +16,7 @@ import {
SelectValue,
} from '@/figma-make/components/base/select';
import { Badge } from '@/figma-make/components/base/badge';
import { Users, AlertCircle, Info, Loader2 } from 'lucide-react';
import { Users, AlertCircle, Info } from 'lucide-react';
import { DateRangePicker } from '@/figma-make/components/base/date-range-picker';
import {
BarChart,
@@ -37,10 +36,10 @@ import { DailyChecklistSummary } from '@/types/api/daily-checklist/daily-checkli
import { AxiosError } from 'axios';
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
import { KandangApi } from '@/services/api/master-data';
import { useSelect } from '@/components/input/SelectInput';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { formatDate } from '@/lib/helper';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
const KANDANG_COLORS = [
'#0069e0', // Blue (primary)
@@ -60,17 +59,10 @@ const CATEGORY_LABELS: { [key: string]: string } = {
produksi_close: 'Produksi Close',
};
const getThisMonthRange = () => ({
dateFrom: moment().startOf('month').format('YYYY-MM-DD'),
dateTo: moment().endOf('month').format('YYYY-MM-DD'),
});
export function Dashboard() {
const defaultDateRange = getThisMonthRange();
// Filters
const [dateFrom, setDateFrom] = useState(defaultDateRange.dateFrom);
const [dateTo, setDateTo] = useState(defaultDateRange.dateTo);
const [dateFrom, setDateFrom] = useState('');
const [dateTo, setDateTo] = useState('');
const [kandangFilter, setKandangFilter] = useState('ALL');
const [categoryFilter, setCategoryFilter] = useState('ALL');
@@ -85,20 +77,16 @@ export function Dashboard() {
httpClientFetcher
);
const {
options: kandangOptions,
loadMore: loadMoreKandang,
isLoadingMore: isLoadingMoreKandang,
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement;
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
const { options: kandangOptions } = useSelect(
KandangApi.basePath,
'id',
'name',
'search',
{
page: '1',
limit: '100',
}
};
);
const kandangColorMap: { [key: string]: string } = {};
(kandangOptions || []).forEach((k, index) => {
@@ -176,7 +164,7 @@ export function Dashboard() {
>
<SelectValue placeholder='Semua Kandang' />
</SelectTrigger>
<SelectContent onScroll={handleKandangScroll}>
<SelectContent>
<SelectItem value='ALL'>Semua Kandang</SelectItem>
{kandangOptions.map((kandang) => (
<SelectItem
@@ -186,11 +174,6 @@ export function Dashboard() {
{kandang.label}
</SelectItem>
))}
{isLoadingMoreKandang && (
<div className='flex justify-center p-2'>
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
</div>
)}
</SelectContent>
</Select>
</div>
@@ -1,15 +1,7 @@
'use client';
import { useState } from 'react';
import {
Eye,
CheckCircle,
XCircle,
Search,
Trash2,
Edit,
Loader2,
} from 'lucide-react';
import { Eye, CheckCircle, XCircle, Search, Trash2, Edit } from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button';
import { Badge } from '@/figma-make/components/base/badge';
@@ -42,9 +34,9 @@ import { DailyChecklist } from '@/types/api/daily-checklist/daily-checklist';
import { cn } from '@/lib/helper';
import { ColumnDef } from '@tanstack/react-table';
import { useSelect } from '@/components/input/SelectInput';
import { KandangApi } from '@/services/api/master-data';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import RequirePermission from '@/components/helper/RequirePermission';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
const STATUS_OPTIONS = [
{ value: 'ALL', label: 'Semua Status' },
@@ -101,25 +93,21 @@ export function ListDailyChecklistContent() {
}
);
const {
options: kandangOptions,
isLoadingMore: isLoadingMoreKandang,
loadMore: loadMoreKandang,
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
const { options: kandangOptions } = useSelect(
KandangApi.basePath,
'id',
'name',
'search',
{
page: '1',
limit: '100',
}
);
const checklistList = isResponseSuccess(checklistListRes)
? checklistListRes.data || []
: [];
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement;
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
}
};
// Modals
const [showApproveModal, setShowApproveModal] = useState(false);
const [showRejectModal, setShowRejectModal] = useState(false);
@@ -136,7 +124,7 @@ export function ListDailyChecklistContent() {
const handleEdit = (item: DailyChecklist) => {
const formattedDate = new Date(item.date).toISOString().split('T')[0];
const kandangId = item.kandang?.id ?? '';
const kandangId = item.kandang.id;
const category = item.category;
router.push(
@@ -335,7 +323,7 @@ export function ListDailyChecklistContent() {
accessorKey: 'kandang',
header: 'Kandang',
enableSorting: false,
cell: ({ row }) => row.original.kandang?.name ?? '-',
cell: ({ row }) => row.original.kandang.name,
},
{
accessorKey: 'category',
@@ -502,7 +490,7 @@ export function ListDailyChecklistContent() {
>
<SelectValue placeholder='Semua Kandang' />
</SelectTrigger>
<SelectContent onScroll={handleKandangScroll}>
<SelectContent>
<SelectItem value='ALL'>Semua Kandang</SelectItem>
{kandangOptions.map((kandang) => (
<SelectItem
@@ -512,11 +500,6 @@ export function ListDailyChecklistContent() {
{kandang.label}
</SelectItem>
))}
{isLoadingMoreKandang && (
<div className='flex justify-center p-2'>
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
</div>
)}
</SelectContent>
</Select>
</div>
@@ -627,7 +610,7 @@ export function ListDailyChecklistContent() {
<div className='flex justify-between text-sm'>
<span className='text-gray-600'>Kandang:</span>
<span className='font-medium text-gray-900'>
{selectedItem.kandang?.name ?? '-'}
{selectedItem.kandang.name}
</span>
</div>
<div className='flex justify-between text-sm'>
@@ -687,7 +670,7 @@ export function ListDailyChecklistContent() {
<div className='flex justify-between text-sm'>
<span className='text-gray-600'>Kandang:</span>
<span className='font-medium text-gray-900'>
{selectedItem.kandang?.name ?? '-'}
{selectedItem.kandang.name}
</span>
</div>
<div className='flex justify-between text-sm'>
@@ -760,7 +743,7 @@ export function ListDailyChecklistContent() {
<div className='flex justify-between text-sm'>
<span className='text-gray-600'>Kandang:</span>
<span className='font-medium text-gray-900'>
{selectedItem.kandang?.name ?? '-'}
{selectedItem.kandang.name}
</span>
</div>
<div className='flex justify-between text-sm'>
@@ -1,6 +1,6 @@
'use client';
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import * as React from 'react';
import { ArrowLeft, CheckCircle, XCircle, AlertCircle } from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card';
@@ -137,15 +137,7 @@ export function DetailDailyChecklistContent() {
const [rejectReason, setRejectReason] = useState('');
const [actionLoading, setActionLoading] = useState(false);
useEffect(() => {
if (checklistId) {
fetchChecklistDetail();
} else {
router.push('/404');
}
}, [checklistId]);
const fetchChecklistDetail = async () => {
const fetchChecklistDetail = useCallback(async () => {
if (!checklistId) {
console.warn('checklistId missing');
setLoading(false);
@@ -172,7 +164,7 @@ export function DetailDailyChecklistContent() {
const checklistData = {
id: rawDetailChecklist?.id,
date: rawDetailChecklist?.date,
kandang_id: rawDetailChecklist?.kandang?.id,
kandang_id: rawDetailChecklist?.kandang.id,
category: rawDetailChecklist?.category,
status: rawDetailChecklist?.status,
reject_reason: rawDetailChecklist?.reject_reason,
@@ -320,7 +312,15 @@ export function DetailDailyChecklistContent() {
} finally {
setLoading(false);
}
};
}, [checklistId, router]);
useEffect(() => {
if (checklistId) {
fetchChecklistDetail();
} else {
router.push('/404');
}
}, [checklistId, fetchChecklistDetail, router]);
const groupDetailData = (rows: ChecklistDetailRow[]) => {
// Group by phase_id
@@ -1,14 +1,7 @@
'use client';
import { useState } from 'react';
import {
Plus,
MoreVertical,
Pencil,
Trash2,
Search,
Loader2,
} from 'lucide-react';
import { Plus, MoreVertical, Pencil, Trash2, Search } from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button';
import { Label } from '@/figma-make/components/base/label';
@@ -56,8 +49,8 @@ import { cn } from '@/lib/helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ColumnDef } from '@tanstack/react-table';
import { useSelect } from '@/components/input/SelectInput';
import { KandangApi } from '@/services/api/master-data';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
export function MasterEmployeeContent() {
const {
@@ -92,20 +85,16 @@ export function MasterEmployeeContent() {
keepPreviousData: true,
}
);
const {
options: kandangOptions,
loadMore: loadMoreKandang,
isLoadingMore: isLoadingMoreKandang,
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement;
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
const { options: kandangOptions } = useSelect(
KandangApi.basePath,
'id',
'name',
'search',
{
page: '1',
limit: '100',
}
};
);
const [showModal, setShowModal] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
@@ -362,7 +351,7 @@ export function MasterEmployeeContent() {
<SelectTrigger className='w-[180px] border-gray-200'>
<SelectValue placeholder='Semua Kandang' />
</SelectTrigger>
<SelectContent onScroll={handleKandangScroll}>
<SelectContent>
<SelectItem value='all'>Semua Kandang</SelectItem>
{kandangOptions.map((kandang) => (
<SelectItem
@@ -372,11 +361,6 @@ export function MasterEmployeeContent() {
{kandang.label}
</SelectItem>
))}
{isLoadingMoreKandang && (
<div className='flex justify-center p-2'>
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
</div>
)}
</SelectContent>
</Select>
@@ -487,12 +471,6 @@ export function MasterEmployeeContent() {
kandang_ids: selected.map((id) => Number(id)),
})
}
onLoadMore={() => {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
}}
isLoadingMore={isLoadingMoreKandang}
placeholder='Pilih kandang'
className='mt-1.5'
/>
@@ -1,585 +0,0 @@
'use client';
import { useState } from 'react';
import { Plus, MoreVertical, Pencil, Trash2, Search } from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button';
import { Label } from '@/figma-make/components/base/label';
import { Input } from '@/figma-make/components/base/input';
import { Badge } from '@/figma-make/components/base/badge';
import { MultiSelect } from '@/figma-make/components/base/multi-select';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/figma-make/components/base/select';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/figma-make/components/base/dialog';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/figma-make/components/base/alert-dialog';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/figma-make/components/base/dropdown-menu';
import { toast } from 'sonner';
import useSWR from 'swr';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
import Table from '@/components/Table';
import { DailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { cn } from '@/lib/helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ColumnDef } from '@tanstack/react-table';
import { useSelect } from '@/components/input/SelectInput';
import { KandangApi, LocationApi } from '@/services/api/master-data';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import { BaseDailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
import { UserApi } from '@/services/api/user';
export function MasterKandangContent() {
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
location_id: '',
status: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
search: 'search',
location_id: 'location_id',
},
});
const {
data: dailyChecklistKandangs,
isLoading: isLoadingDailyChecklistKandangs,
mutate: refreshDailyChecklistKandangs,
} = useSWR(
`${DailyChecklistKandangApi.basePath}${getTableFilterQueryString()}`,
DailyChecklistKandangApi.getAllFetcher,
{
keepPreviousData: true,
}
);
const { options: locationOptions } = useSelect(
LocationApi.basePath,
'id',
'name',
'search',
{
page: '1',
limit: '100',
}
);
const { options: picOptions } = useSelect(
UserApi.basePath,
'id',
'name',
'search',
{
page: '1',
limit: '100',
}
);
const {
options: kandangOptions,
isLoadingMore: isLoadingKandangOptionsMore,
loadMore: loadMoreKandang,
} = useSelect(KandangApi.basePath, 'id', 'name');
const [showModal, setShowModal] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [kandangToDelete, setKandangToDelete] = useState<number | null>(null);
const [loading, setLoading] = useState(false);
const [modalMode, setModalMode] = useState<'create' | 'edit'>('create');
const [kandangForm, setKandangForm] = useState({
id: 0,
name: '',
location_id: 0,
pic_id: 0,
// recording_kandangs: [] as number[],
});
const dailyChecklistKandangColumns: ColumnDef<DailyChecklistKandang>[] = [
{
id: 'name',
header: 'Nama',
accessorKey: 'name',
enableSorting: false,
},
{
id: 'location',
header: 'Lokasi',
accessorKey: 'location',
enableSorting: false,
cell: ({ row }) => row.original.location.name ?? '-',
},
{
id: 'pic',
header: 'PIC',
accessorKey: 'pic',
enableSorting: false,
cell: ({ row }) => row.original.pic.name ?? '-',
},
{
id: 'recording_kandangs',
header: 'Kandang Recording',
accessorKey: 'recording_kandangs',
enableSorting: false,
cell: ({ row }) =>
row.original.recording_kandangs?.length > 0
? row.original.recording_kandangs.map((item) => item.name).join(', ')
: '-',
},
{
id: 'action',
header: 'Aksi',
accessorKey: 'action',
enableSorting: false,
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='ghost'
size='sm'
className='h-8 w-8 p-0 hover:bg-gray-100'
>
<MoreVertical className='h-4 w-4 text-gray-600' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem onClick={() => handleEdit(row.original)}>
<Pencil className='mr-2 h-4 w-4' />
Edit
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDeleteClick(row.original.id)}
className='text-red-600'
>
<Trash2 className='mr-2 h-4 w-4' />
Hapus
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
},
];
const handleAdd = () => {
setModalMode('create');
setKandangForm({
id: 0,
name: '',
location_id: 0,
pic_id: 0,
// recording_kandangs: []
});
setShowModal(true);
};
const handleEdit = (dailyChecklistKandang: DailyChecklistKandang) => {
setModalMode('edit');
setKandangForm({
id: dailyChecklistKandang.id,
name: dailyChecklistKandang.name,
location_id: dailyChecklistKandang.location.id,
pic_id: dailyChecklistKandang.pic.id,
// recording_kandangs:
// dailyChecklistKandang.recording_kandangs.map((item) => item.id) ?? [],
});
setShowModal(true);
};
const handleSave = async () => {
if (!kandangForm.name.trim()) {
toast.error('Nama harus diisi');
return;
}
if (!kandangForm.location_id) {
toast.error('Lokasi wajib diisi');
return;
}
// if (!kandangForm.recording_kandangs.length) {
// toast.error('Kandang recording wajib diisi');
// return;
// }
setLoading(true);
try {
if (modalMode === 'create') {
const createDailyChecklistKandangResponse =
await DailyChecklistKandangApi.create({
name: kandangForm.name.trim(),
location_id: kandangForm.location_id,
pic_id: kandangForm.pic_id,
// recording_kandang_ids: kandangForm.recording_kandangs,
});
if (isResponseError(createDailyChecklistKandangResponse)) {
console.error(
'Error creating kandang:',
createDailyChecklistKandangResponse.message
);
toast.error('Gagal menambahkan kandang');
return;
}
refreshDailyChecklistKandangs();
toast.success('Kandang berhasil ditambahkan');
} else {
const updateDailyChecklistKandangResponse =
await DailyChecklistKandangApi.update(kandangForm.id, {
name: kandangForm.name.trim(),
location_id: kandangForm.location_id,
pic_id: kandangForm.pic_id,
// recording_kandang_ids: kandangForm.recording_kandangs,
});
if (isResponseError(updateDailyChecklistKandangResponse)) {
console.error(
'Error updating kandang:',
updateDailyChecklistKandangResponse.message
);
toast.error('Gagal menambahkan Kandang');
return;
}
refreshDailyChecklistKandangs();
toast.success('Kandang berhasil diubah');
}
setShowModal(false);
setKandangForm({
id: 0,
name: '',
location_id: 0,
pic_id: 0,
// recording_kandangs: [],
});
} catch (error) {
console.error('Error saving kandang:', error);
toast.error('Terjadi kesalahan saat menyimpan kandang');
} finally {
setLoading(false);
}
};
const handleDeleteClick = (kandangId: number) => {
setKandangToDelete(kandangId);
setShowDeleteConfirm(true);
};
const handleConfirmDelete = async () => {
if (!kandangToDelete) return;
setLoading(true);
try {
const deleteKandangResponse =
await DailyChecklistKandangApi.delete(kandangToDelete);
if (isResponseError(deleteKandangResponse)) {
console.error('Error deleting kandang:', deleteKandangResponse.message);
toast.error('Gagal menghapus kandang');
return;
}
refreshDailyChecklistKandangs();
toast.success('Kandang berhasil dihapus');
setShowDeleteConfirm(false);
setKandangToDelete(null);
} catch (error) {
console.error('Error deleting kandang:', error);
toast.error('Terjadi kesalahan saat menghapus kandang');
} finally {
setLoading(false);
}
};
if (isLoadingDailyChecklistKandangs && !dailyChecklistKandangs) {
return (
<div className='min-h-screen'>
<div className='p-6'>
<div className='mb-6'>
<h1 className='text-2xl font-semibold text-gray-900'>
Master Kandang
</h1>
<p className='text-sm text-gray-600 mt-1'>
Master Data <span className='text-[#0069e0]'>Kandang</span>
</p>
</div>
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
<CardContent className='p-12 text-center text-gray-500'>
Memuat data...
</CardContent>
</Card>
</div>
</div>
);
}
return (
<div className='min-h-screen'>
<div className='p-6'>
{/* Page Title */}
<div className='mb-6'>
<h1 className='text-2xl font-semibold text-gray-900'>
Master Kandang
</h1>
<p className='text-sm text-gray-600 mt-1'>
Master Data <span className='text-[#0069e0]'>Kandang</span>
</p>
</div>
{/* Main Card */}
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
<CardContent className='p-0'>
{/* Single Toolbar Row */}
<div className='flex flex-wrap items-center justify-between gap-4 p-6 border-b border-gray-200/60'>
{/* LEFT: Search + Filters */}
<div className='flex items-center gap-3 flex-wrap'>
<div className='relative'>
<Search className='absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4' />
<DebouncedTextInput
name='search'
placeholder='Cari kandang...'
value={tableFilterState.search}
onChange={(e) => updateFilter('search', e.target.value)}
className={{
wrapper: 'w-full sm:w-[280px] border-gray-200',
inputWrapper: 'px-3 py-2 h-fit rounded-md',
input: 'text-sm',
}}
startAdornment={
<Search className='text-gray-400 w-4 h-4' />
}
/>
</div>
<Select
value={tableFilterState.location_id}
onValueChange={(value) =>
updateFilter('location_id', value === 'all' ? '' : value)
}
>
<SelectTrigger className='w-[180px] border-gray-200'>
<SelectValue placeholder='Semua Lokasi' />
</SelectTrigger>
<SelectContent>
<SelectItem value='all'>Semua Lokasi</SelectItem>
{locationOptions.map((kandang) => (
<SelectItem
key={kandang.value}
value={String(kandang.value)}
>
{kandang.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* RIGHT: Export + Add */}
<div className='flex items-center gap-2 flex-wrap'>
<Button
onClick={handleAdd}
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
>
<Plus className='w-4 h-4 mr-2' />
Tambah Kandang
</Button>
</div>
</div>
{/* Table */}
<Table<DailyChecklistKandang>
data={
isResponseSuccess(dailyChecklistKandangs)
? dailyChecklistKandangs?.data
: []
}
columns={dailyChecklistKandangColumns}
pageSize={tableFilterState.pageSize}
onPageSizeChange={setPageSize}
rowOptions={[10, 20, 50, 100]}
page={
isResponseSuccess(dailyChecklistKandangs)
? dailyChecklistKandangs?.meta?.page
: 0
}
totalItems={
isResponseSuccess(dailyChecklistKandangs)
? dailyChecklistKandangs?.meta?.total_results
: 0
}
onPageChange={setPage}
isLoading={isLoadingDailyChecklistKandangs}
className={{
containerClassName: cn({
'w-full mb-20':
isResponseSuccess(dailyChecklistKandangs) &&
dailyChecklistKandangs?.data?.length === 0,
}),
tableWrapperClassName:
'overflow-x-auto border border-solid border-base-content/10 rounded-none',
headerRowClassName: 'bg-gray-50/50',
headerColumnClassName:
'text-left py-3.5 px-6 text-sm font-semibold text-gray-700',
paginationClassName: 'px-4',
}}
/>
</CardContent>
</Card>
</div>
{/* Add/Edit Modal */}
<Dialog open={showModal} onOpenChange={setShowModal}>
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
<DialogHeader>
<DialogTitle>
{modalMode === 'create' ? 'Tambah Kandang' : 'Edit Kandang'}
</DialogTitle>
<DialogDescription>
{modalMode === 'create'
? 'Masukkan detail Kandang baru'
: 'Ubah detail Kandang'}
</DialogDescription>
</DialogHeader>
<div className='space-y-4 py-4'>
<div>
<Label htmlFor='nama-kandang'>
Nama Kandang <span className='text-red-500'>*</span>
</Label>
<Input
id='nama-kandang'
value={kandangForm.name}
onChange={(e) =>
setKandangForm({ ...kandangForm, name: e.target.value })
}
placeholder='Masukkan nama Kandang'
className='mt-1.5'
disabled={loading}
/>
</div>
<div>
<Label htmlFor='category'>
Lokasi <span className='text-red-500'>*</span>
</Label>
<Select
value={
kandangForm.location_id ? String(kandangForm.location_id) : ''
}
onValueChange={(value) =>
setKandangForm({ ...kandangForm, location_id: Number(value) })
}
>
<SelectTrigger id='category' className='mt-1.5'>
<SelectValue placeholder='Pilih lokasi' />
</SelectTrigger>
<SelectContent>
{locationOptions.map((cat) => (
<SelectItem key={cat.value} value={String(cat.value)}>
{cat.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor='pic'>
PIC <span className='text-red-500'>*</span>
</Label>
<Select
value={kandangForm.pic_id ? String(kandangForm.pic_id) : ''}
onValueChange={(value) =>
setKandangForm({ ...kandangForm, pic_id: Number(value) })
}
>
<SelectTrigger id='pic' className='mt-1.5'>
<SelectValue placeholder='Pilih PIC' />
</SelectTrigger>
<SelectContent>
{picOptions.map((cat) => (
<SelectItem key={cat.value} value={String(cat.value)}>
{cat.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button
variant='outline'
onClick={() => setShowModal(false)}
disabled={loading}
>
Batal
</Button>
<Button
onClick={handleSave}
disabled={loading}
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
>
{loading ? 'Menyimpan...' : 'Simpan'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Confirmation */}
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
<AlertDialogContent className='bg-white rounded-xl shadow-lg sm:max-w-md'>
<AlertDialogHeader>
<AlertDialogTitle>Hapus Kandang?</AlertDialogTitle>
<AlertDialogDescription>
Data Kandang akan dihapus secara permanen.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={loading}>Batal</AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirmDelete}
disabled={loading}
className='bg-red-600 hover:bg-red-700 text-white'
>
{loading ? 'Menghapus...' : 'Hapus'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
@@ -11,7 +11,7 @@ import {
SelectValue,
} from '@/figma-make/components/base/select';
import { useSelect } from '@/components/input/SelectInput';
import { AreaApi, LocationApi } from '@/services/api/master-data';
import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data';
import useSWR from 'swr';
import { BaseApiResponse } from '@/types/api/api-general';
import { DailyChecklistReport } from '@/types/api/daily-checklist/daily-checklist';
@@ -26,8 +26,7 @@ import { ColumnDef } from '@tanstack/react-table';
import { PhaseApi } from '@/services/api/daily-checklist/phase';
import { EmployeeApi } from '@/services/api/daily-checklist/employee';
import { Button } from '@/figma-make/components/base/button';
import { Download, Loader2 } from 'lucide-react';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
import { Download } from 'lucide-react';
const MONTH_OPTIONS = [
{ value: '1', label: 'Januari' },
@@ -130,23 +129,18 @@ export function DailyChecklistReportsContent() {
}
);
const {
options: kandangOptions,
loadMore: loadMoreKandang,
isLoadingMore: isLoadingMoreKandang,
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name', 'search', {
area_id: tableFilterState.area_id,
location_id: tableFilterState.location_id,
});
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement;
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
const { options: kandangOptions } = useSelect(
KandangApi.basePath,
'id',
'name',
'search',
{
page: '1',
limit: '100',
area_id: tableFilterState.area_id,
location_id: tableFilterState.location_id,
}
};
);
const { options: phaseOptions } = useSelect(
PhaseApi.basePath,
@@ -441,7 +435,7 @@ export function DailyChecklistReportsContent() {
>
<SelectValue placeholder='Semua Kandang' />
</SelectTrigger>
<SelectContent onScroll={handleKandangScroll}>
<SelectContent>
<SelectItem value='ALL'>Semua Kandang</SelectItem>
{kandangOptions.map((kandang) => (
<SelectItem
@@ -451,11 +445,6 @@ export function DailyChecklistReportsContent() {
{kandang.label}
</SelectItem>
))}
{isLoadingMoreKandang && (
<div className='flex justify-center p-2'>
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
</div>
)}
</SelectContent>
</Select>
</div>
+14
View File
@@ -305,3 +305,17 @@ export function transformConstants(
},
};
}
export function omit<T extends Record<string, unknown>, K extends keyof T>(
obj: T,
keys: K | K[]
): Omit<T, K> {
const keysArray = Array.isArray(keys) ? keys : [keys];
const result = { ...obj };
keysArray.forEach((key) => {
delete result[key];
});
return result;
}
+55 -57
View File
@@ -76,13 +76,13 @@ export const calculateTrading = (
case 'unit_price':
case 'qty': {
if (unitPrice > 0 && qty > 0) {
setFieldValue('total_price', unitPrice * qty);
setFieldValue('total_price', roundPrice(unitPrice * qty));
}
break;
}
case 'total_price': {
if (totalPrice > 0 && qty > 0) {
setFieldValue('unit_price', totalPrice / qty);
setFieldValue('unit_price', roundPrice(totalPrice / qty));
}
break;
}
@@ -112,7 +112,7 @@ export const calculateAyamPullet = (
case 'qty': {
// total_price = unit_price × week × qty
if (unitPrice > 0 && week > 0 && qty > 0) {
setFieldValue('total_price', unitPrice * week * qty);
setFieldValue('total_price', roundPrice(unitPrice * week * qty));
}
// total_weight = avg_weight × qty
if (avgWeight > 0 && qty > 0) {
@@ -135,7 +135,7 @@ export const calculateAyamPullet = (
case 'total_price': {
// Reverse: unit_price = total_price / (week × qty)
if (totalPrice > 0 && week > 0 && qty > 0) {
setFieldValue('unit_price', totalPrice / (week * qty));
setFieldValue('unit_price', roundPrice(totalPrice / (week * qty)));
}
break;
}
@@ -164,7 +164,7 @@ export const calculateAyam = (field: string, ctx: CalculationContext): void => {
setFieldValue('total_weight', tw);
// total_price = total_weight × unit_price
if (unitPrice > 0) {
setFieldValue('total_price', tw * unitPrice);
setFieldValue('total_price', roundPrice(tw * unitPrice));
}
}
break;
@@ -176,21 +176,21 @@ export const calculateAyam = (field: string, ctx: CalculationContext): void => {
}
// total_price = total_weight × unit_price
if (unitPrice > 0 && totalWeight > 0) {
setFieldValue('total_price', totalWeight * unitPrice);
setFieldValue('total_price', roundPrice(totalWeight * unitPrice));
}
break;
}
case 'unit_price': {
// total_price = total_weight × unit_price
if (unitPrice > 0 && totalWeight > 0) {
setFieldValue('total_price', totalWeight * unitPrice);
setFieldValue('total_price', roundPrice(totalWeight * unitPrice));
}
break;
}
case 'total_price': {
// unit_price = total_price / total_weight
if (totalPrice > 0 && totalWeight > 0) {
setFieldValue('unit_price', totalPrice / totalWeight);
setFieldValue('unit_price', roundPrice(totalPrice / totalWeight));
}
break;
}
@@ -223,8 +223,7 @@ export const calculateTelurPeti = (
// Helper untuk menghitung dan set unit_price = total_price / total_weight
const updateUnitPrice = (tp: number, tw: number) => {
if (tw > 0 && tp > 0) {
const unitPrice = tp / tw;
setFieldValue('unit_price', unitPrice);
setFieldValue('unit_price', roundPrice(tp / tw));
}
};
@@ -233,7 +232,7 @@ export const calculateTelurPeti = (
// Recalculate total_price = (price_per_convertion × total_peti) + price_sisa_berat
if (pricePerConvertion > 0 && totalPeti > 0) {
const totalPrice = pricePerConvertion * totalPeti + priceSisaBerat;
setFieldValue('total_price', totalPrice);
setFieldValue('total_price', roundPrice(totalPrice));
// Recalculate unit_price = total_price / total_weight
const totalWeight = weightPerConvertion * totalPeti + sisaBerat;
updateUnitPrice(totalPrice, totalWeight);
@@ -254,8 +253,8 @@ export const calculateTelurPeti = (
// Recalculate total_price = (price_per_convertion × total_peti) + price_sisa_berat
if (pricePerConvertion > 0 && totalPeti > 0) {
const totalPrice = pricePerConvertion * totalPeti + priceSisaBerat;
setFieldValue('total_price', totalPrice);
// Recalculate unit_price = total_price / totalWeight
setFieldValue('total_price', roundPrice(totalPrice));
// Recalculate unit_price = total_price / total_weight
updateUnitPrice(totalPrice, totalWeight);
}
break;
@@ -264,7 +263,7 @@ export const calculateTelurPeti = (
// Recalculate total_price
if (pricePerConvertion > 0 && totalPeti > 0) {
const totalPrice = pricePerConvertion * totalPeti + priceSisaBerat;
setFieldValue('total_price', totalPrice);
setFieldValue('total_price', roundPrice(totalPrice));
// Recalculate unit_price = total_price / total_weight
const totalWeight = weightPerConvertion * totalPeti + sisaBerat;
updateUnitPrice(totalPrice, totalWeight);
@@ -307,7 +306,7 @@ export const calculateTelurPeti = (
if (totalPeti > 0 && totalPrice > priceSisaBerat) {
setFieldValue(
'price_per_convertion',
(totalPrice - priceSisaBerat) / totalPeti
roundPrice((totalPrice - priceSisaBerat) / totalPeti)
);
}
// Update unit_price = total_price / total_weight
@@ -315,15 +314,6 @@ export const calculateTelurPeti = (
updateUnitPrice(totalPrice, totalWeight);
break;
}
case 'qty':
// Recalculate avg_weight = total_weight / qty
if (qty > 0 && values.total_weight) {
setFieldValue(
'avg_weight',
preciseWeight(Number(values.total_weight) / qty)
);
}
break;
}
};
@@ -351,7 +341,10 @@ export const calculateTelurKg = (
}
// total_price = total_weight × unit_price
if (pricePerConvertion > 0 && totalWeight > 0) {
setFieldValue('total_price', totalWeight * pricePerConvertion);
setFieldValue(
'total_price',
roundPrice(totalWeight * pricePerConvertion)
);
setFieldValue('unit_price', pricePerConvertion);
}
break;
@@ -359,7 +352,10 @@ export const calculateTelurKg = (
case 'price_per_convertion': {
// total_price = total_weight × price_per_convertion
if (pricePerConvertion > 0 && totalWeight > 0) {
setFieldValue('total_price', totalWeight * pricePerConvertion);
setFieldValue(
'total_price',
roundPrice(totalWeight * pricePerConvertion)
);
setFieldValue('unit_price', pricePerConvertion);
}
break;
@@ -367,8 +363,11 @@ export const calculateTelurKg = (
case 'total_price': {
// unit_price = total_price / total_weight
if (totalPrice > 0 && totalWeight > 0) {
setFieldValue('unit_price', totalPrice / totalWeight);
setFieldValue('price_per_convertion', totalPrice / totalWeight);
setFieldValue('unit_price', roundPrice(totalPrice / totalWeight));
setFieldValue(
'price_per_convertion',
roundPrice(totalPrice / totalWeight)
);
}
break;
}
@@ -377,11 +376,13 @@ export const calculateTelurKg = (
/**
* TELUR + QTY Workaround:
* - User inputs: qty, avg_weight, unit_price (harga per butir)
* - User inputs: qty, avg_weight, price_per_qty (harga per butir)
* - FE calculates:
* - total_weight = avg_weight × qty
* - total_price = qty × unit_price
* - price_per_qty = total_price / total_weight (harga per kg)
* - total_price = qty × price_per_qty
* - unit_price = total_price / total_weight (normalisasi untuk BE)
* - Kirim convertion_unit: "KG" karena BE tidak support "QTY"
* - BE akan hitung: total_price = total_weight × unit_price (hasil sama)
*/
export const calculateTelurQty = (
field: string,
@@ -402,13 +403,13 @@ export const calculateTelurQty = (
if (avgWeight > 0 && qty > 0) {
const tw = roundWeight(avgWeight * qty);
setFieldValue('total_weight', tw);
// total_price = qty × unit_price
if (unitPrice > 0) {
const tp = qty * unitPrice;
// total_price = qty × price_per_qty
if (pricePerQty > 0) {
const tp = roundPrice(qty * pricePerQty);
setFieldValue('total_price', tp);
// price_per_qty = total_price / total_weight
// unit_price = total_price / total_weight (untuk BE)
if (tw > 0) {
setFieldValue('price_per_qty', tp / tw);
setFieldValue('unit_price', roundPrice(tp / tw));
}
}
}
@@ -418,47 +419,44 @@ export const calculateTelurQty = (
// avg_weight = total_weight / qty
if (totalWeight > 0 && qty > 0) {
setFieldValue('avg_weight', preciseWeight(totalWeight / qty));
// Recalculate total_price jika ada harga per butir
// Recalculate total_price jika ada unit_price
if (unitPrice > 0) {
setFieldValue('total_price', qty * unitPrice);
setFieldValue('total_price', roundPrice(totalWeight * unitPrice));
}
}
break;
}
case 'price_per_qty': {
// total_price = total_weight × price_per_qty
if (pricePerQty > 0 && totalWeight > 0) {
const tp = totalWeight * pricePerQty;
// total_price = qty × price_per_qty
if (pricePerQty > 0 && qty > 0) {
const tp = roundPrice(qty * pricePerQty);
setFieldValue('total_price', tp);
// unit_price = total_price / qty
if (qty > 0) {
setFieldValue('unit_price', tp / qty);
// unit_price = total_price / total_weight (untuk BE)
if (totalWeight > 0) {
setFieldValue('unit_price', roundPrice(tp / totalWeight));
}
}
break;
}
case 'total_price': {
// unit_price = total_price / qty
// price_per_qty = total_price / qty
if (totalPrice > 0 && qty > 0) {
setFieldValue('unit_price', totalPrice / qty);
// price_per_qty = total_price / total_weight
setFieldValue('price_per_qty', roundPrice(totalPrice / qty));
// unit_price = total_price / total_weight (untuk BE)
if (totalWeight > 0) {
setFieldValue('price_per_qty', totalPrice / totalWeight);
setFieldValue('unit_price', roundPrice(totalPrice / totalWeight));
}
}
break;
}
case 'unit_price': {
// total_price = qty × unit_price
const newTotalPrice = qty * unitPrice;
if (unitPrice > 0 && qty > 0) {
setFieldValue('total_price', newTotalPrice);
// total_price = total_weight × unit_price
if (unitPrice > 0 && totalWeight > 0) {
setFieldValue('total_price', roundPrice(totalWeight * unitPrice));
}
// price_per_qty = total_price / total_weight
if (newTotalPrice > 0 && totalWeight > 0) {
setFieldValue('price_per_qty', newTotalPrice / totalWeight);
// price_per_qty = total_price / qty
if (totalPrice > 0 && qty > 0) {
setFieldValue('price_per_qty', roundPrice(totalPrice / qty));
}
break;
}
-61
View File
@@ -1,61 +0,0 @@
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;
};
@@ -1,20 +0,0 @@
import { BaseApiService } from '@/services/api/base';
import {
DailyChecklistKandang,
CreateDailyChecklistKandangPayload,
UpdateDailyChecklistKandangPayload,
} from '@/types/api/daily-checklist/kandang';
export class DailyChecklistKandangApiService extends BaseApiService<
DailyChecklistKandang,
CreateDailyChecklistKandangPayload,
UpdateDailyChecklistKandangPayload
> {
constructor(basePath: string = '/master-data/kandang-groups') {
super(basePath);
}
}
export const DailyChecklistKandangApi = new DailyChecklistKandangApiService(
'/master-data/kandang-groups'
);
+6 -20
View File
@@ -1,6 +1,7 @@
import { BaseApiService } from '@/services/api/base';
import { BaseApiResponse } from '@/types/api/api-general';
import { Dashboard, DashboardFilter } from '@/types/api/dashboard/dashboard';
import { Dashboard } from '@/types/api/dashboard/dashboard';
import { httpClientFetcher } from '@/services/http/client';
class DashboardService extends BaseApiService<Dashboard, unknown, unknown> {
constructor(basePath: string) {
@@ -13,26 +14,11 @@ class DashboardService extends BaseApiService<Dashboard, unknown, unknown> {
* @returns Promise with BaseApiResponse containing DashboardProduction
*/
async getDashboardProductionFetcher(
params: DashboardFilter
endpoint: string
): Promise<BaseApiResponse<Dashboard> | undefined> {
return await this.customRequest<BaseApiResponse<Dashboard>>('', {
method: 'GET',
params: {
start_date: params.start_date || undefined,
end_date: params.end_date || undefined,
analysis_mode: params.analysis_mode || undefined,
location_ids: params.location_ids.length
? params.location_ids.toString()
: undefined,
flock_ids: params.flock_ids.length
? params.flock_ids.toString()
: undefined,
kandang_ids: params.kandang_ids.length
? params.kandang_ids.toString()
: undefined,
comparison_type: params.comparison_type || undefined,
},
});
return await httpClientFetcher<BaseApiResponse<Dashboard>>(
`${endpoint ? endpoint : this.basePath}`
);
}
}
-26
View File
@@ -12,8 +12,6 @@ import {
NextDayRecording,
} from '@/types/api/production/recording';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
import { httpClient } from '@/services/http/client';
import { formatDate } from '@/lib/helper';
export const ProjectFlockKandangApi = new BaseApiService<
ProjectFlockKandang,
@@ -90,30 +88,6 @@ export class RecordingService extends BaseApiService<
}
);
}
async exportToExcel(initialQueryString: string) {
const params = new URLSearchParams(initialQueryString);
params.set('export', 'excel');
const queryString = `?${params.toString()}`;
const res = await httpClient<Blob>(`${this.basePath}${queryString}`, {
method: 'GET',
responseType: 'blob',
});
const url = window.URL.createObjectURL(new Blob([res]));
const link = document.createElement('a');
link.href = url;
const fileName = `recording-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`;
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
link.remove();
}
}
export const RecordingApi = new RecordingService('/production/recordings');
-7
View File
@@ -9,13 +9,6 @@ export type RequestOptions<B = unknown> = {
auth?: AuthMode; // 'cookie' | 'bearer' | 'none'
token?: string; // required if auth === 'bearer'
timeoutMs?: number;
responseType?:
| 'arraybuffer'
| 'blob'
| 'document'
| 'json'
| 'text'
| 'stream';
};
export class HttpError extends Error {
+1 -5
View File
@@ -10,10 +10,7 @@ const axiosClient = axios.create({ baseURL: BASE_URL, timeout: 10_000 });
axiosClient.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
if (
error.response?.status === 401 &&
error.config?.url !== '/sso/refresh'
) {
if (error.response?.status === 401) {
redirectToSSO();
}
@@ -40,7 +37,6 @@ export async function httpClient<T, B = unknown>(
data: opts.body,
timeout: opts.timeoutMs ?? 10_000,
withCredentials: isCookieAuth && !isBearerAuth,
responseType: opts.responseType,
headers: {
...(isFormData ? {} : { 'Content-Type': 'application/json' }),
...(opts.headers ?? {}),
@@ -4,16 +4,13 @@ import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { createChickinApprovalSlice } from '@/stores/production/chickin/slices/chickin-approval.slice';
import { ChickinApprovalSlice } from '@/stores/production/chickin/slices/chickin-approval.slice';
import { createChickinDeleteSlice } from '@/stores/production/chickin/slices/chickin-delete.slice';
import { ChickinDeleteSlice } from '@/stores/production/chickin/slices/chickin-delete.slice';
export type ChickinStore = ChickinApprovalSlice & ChickinDeleteSlice;
export type ChickinStore = ChickinApprovalSlice;
export const useChickinStore = create<ChickinStore>()(
devtools(
(...args) => ({
...createChickinApprovalSlice(...args),
...createChickinDeleteSlice(...args),
}),
{
name: 'ChickinStore',
@@ -1,57 +0,0 @@
import { StateCreator } from 'zustand';
export type ChickinDeleteSlice = {
// State
isChickinDeleteModalOpen: boolean;
selectedChickinIdForDelete: number | null;
isChickinDeleteLoading: boolean;
chickinDeleteCallback: (() => Promise<void>) | null;
// Actions
openChickinDeleteModal: (
chickinId: number,
callback: () => Promise<void>
) => void;
closeChickinDeleteModal: () => void;
setChickinDeleteLoading: (loading: boolean) => void;
resetChickinDelete: () => void;
};
export const createChickinDeleteSlice: StateCreator<
ChickinDeleteSlice,
[],
[],
ChickinDeleteSlice
> = (set) => ({
// Initial state
isChickinDeleteModalOpen: false,
selectedChickinIdForDelete: null,
isChickinDeleteLoading: false,
chickinDeleteCallback: null,
// Actions
openChickinDeleteModal: (chickinId, callback) =>
set({
isChickinDeleteModalOpen: true,
selectedChickinIdForDelete: chickinId,
chickinDeleteCallback: callback,
}),
closeChickinDeleteModal: () =>
set({
isChickinDeleteModalOpen: false,
selectedChickinIdForDelete: null,
chickinDeleteCallback: null,
}),
setChickinDeleteLoading: (loading) =>
set({ isChickinDeleteLoading: loading }),
resetChickinDelete: () =>
set({
isChickinDeleteModalOpen: false,
selectedChickinIdForDelete: null,
isChickinDeleteLoading: false,
chickinDeleteCallback: null,
}),
});
@@ -1,4 +1,5 @@
import { TabActionsSlice } from '@/stores/tab-actions/tab-actions.store';
import { omit } from '@/lib/helper';
import { StateCreator } from 'zustand';
export const createTabActionsSlice: StateCreator<
@@ -20,10 +21,9 @@ export const createTabActionsSlice: StateCreator<
})),
clearTabActions: (tabId) =>
set((state) => {
const { [tabId]: _, ...rest } = state.tabActions;
return { tabActions: rest };
}),
set((state) => ({
tabActions: omit(state.tabActions, tabId),
})),
clearAllTabActions: () => set({ tabActions: {} }),
});
+1 -1
View File
@@ -12,7 +12,7 @@ export type BaseDailyChecklist = {
status: string;
category: string;
date: string;
kandang?: Pick<BaseKandang, 'id' | 'name' | 'status' | 'capacity'>;
kandang: Pick<BaseKandang, 'id' | 'name' | 'status' | 'capacity'>;
total_phase: number;
total_activity: number;
progress: number;
-24
View File
@@ -1,24 +0,0 @@
import { BaseMetadata } from '@/types/api/api-general';
import { BaseKandang } from '@/types/api/master-data/kandang';
import { BaseLocation } from '@/types/api/master-data/location';
import { BaseUser } from '@/types/api/user';
export type BaseDailyChecklistKandang = {
id: number;
name: string;
location: BaseLocation;
recording_kandangs: Pick<BaseKandang, 'id' | 'name'>[];
pic: BaseUser;
};
export type DailyChecklistKandang = BaseMetadata & BaseDailyChecklistKandang;
export type CreateDailyChecklistKandangPayload = {
name: string;
location_id: number;
pic_id: number;
// recording_kandang_ids: number[];
};
export type UpdateDailyChecklistKandangPayload =
CreateDailyChecklistKandangPayload;
-11
View File
@@ -9,19 +9,8 @@ export type BaseProductWarehouse = {
warehouse_id: number;
uom: Uom;
quantity: number;
transfer_available_qty?: number;
product: Product;
warehouse: Warehouse;
project_flock_kandang?: {
id: number;
project_flock_id: number;
kandang_id: number;
period: number;
project_flock?: {
id: number;
flock_name: string;
};
};
week?: number | null;
};
+3 -5
View File
@@ -5,6 +5,7 @@ import {
CreatedUser,
} from '@/types/api/api-general';
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
import { Kandang } from '@/types/api/master-data/kandang';
import { Warehouse } from '@/types/api/master-data/warehouse';
/**
@@ -61,7 +62,6 @@ export type BaseDelivery = {
avg_weight: number;
total_price: number;
vehicle_number: string;
weight_per_convertion: number;
};
export type MarketingProduct = {
@@ -110,8 +110,7 @@ export type BaseCreateMarketingPayload = {
export type BaseCreateMarketingProductPayload = {
vehicle_number: string;
warehouse_id?: string | number | undefined;
kandang_id?: string | number | undefined;
kandang_id: string | number | undefined;
product_warehouse_id: string | number | undefined;
unit_price: string | number | undefined;
total_weight: string | number | undefined;
@@ -137,8 +136,7 @@ export type CreateSalesOrderPayload = BaseCreateMarketingPayload & {
export type CreateSalesOrderProductPayload =
BaseCreateMarketingProductPayload & {
id?: number;
warehouse?: Warehouse | undefined;
kandang?: Warehouse | undefined;
kandang?: Kandang | undefined;
product_warehouse?: ProductWarehouse | undefined;
};
-3
View File
@@ -1,7 +1,6 @@
import { BaseMetadata } from '@/types/api/api-general';
import { BaseLocation } from '@/types/api/master-data/location';
import { BaseUser } from '@/types/api/user';
import { BaseDailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
export type BaseKandang = {
id: number;
@@ -11,7 +10,6 @@ export type BaseKandang = {
capacity: number;
pic: BaseUser;
project_flock_kandang_id?: number;
kandang_group: Pick<BaseDailyChecklistKandang, 'id' | 'name'>;
};
export type Kandang = BaseMetadata & BaseKandang;
@@ -21,7 +19,6 @@ export type CreateKandangPayload = {
location_id: number;
capacity: number;
pic_id: number;
group_id: number;
};
export type UpdateKandangPayload = CreateKandangPayload;
-2
View File
@@ -74,8 +74,6 @@ export type ProjectFlockKandangLookup = {
available_quantity?: number;
population: number;
chick_in_date: string;
is_transition: boolean;
is_laying: boolean;
};
export type ProjectFlockAvailableQuantity = {

Some files were not shown because too many files have changed in this diff Show More