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

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