mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 584b495e4b | |||
| c8e76a8558 | |||
| a468a5948b | |||
| e05e1a4121 | |||
| e3b86e3033 | |||
| d467c56ea6 | |||
| 784d9f26ab | |||
| 978ef764ea | |||
| 928136ff18 | |||
| 79567e4f1b | |||
| 633deece21 | |||
| 46483af4c2 | |||
| c2653e5068 | |||
| 8569bda7d6 | |||
| 7da63dc542 | |||
| aeceef4361 | |||
| ff6955be54 | |||
| eccab314b3 |
@@ -45,6 +45,3 @@ next-env.d.ts
|
|||||||
|
|
||||||
# claude
|
# claude
|
||||||
.claude
|
.claude
|
||||||
|
|
||||||
# rtk
|
|
||||||
rtk.exe
|
|
||||||
|
|||||||
+2
-30
@@ -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
@@ -1,3 +1,3 @@
|
|||||||
npm run format
|
npm run format
|
||||||
npm run lint
|
npm run lint
|
||||||
npm run typecheck
|
npx tsc --noEmit
|
||||||
+1
-1
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'>
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+52
-130
@@ -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'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
+8
-20
@@ -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!')
|
||||||
|
|||||||
+75
-145
@@ -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',
|
|
||||||
};
|
|
||||||
};
|
|
||||||
+3
-13
@@ -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<
|
||||||
|
|||||||
+10
-31
@@ -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}
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
+18
-35
@@ -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'>
|
||||||
|
|||||||
+12
-12
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
|
||||||
);
|
|
||||||
@@ -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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
Reference in New Issue
Block a user