Compare commits

..

4 Commits

Author SHA1 Message Date
rstubryan 3b9bd3c5bd Revert "refactor(FE): Prevent adding recordings for kandangs in transition"
This reverts commit 9dc30c1f58.
2026-03-09 03:50:33 +07:00
rstubryan 9dc30c1f58 refactor(FE): Prevent adding recordings for kandangs in transition 2026-03-09 03:35:03 +07:00
rstubryan 671fd72141 refactor(FE): Make stock fields optional during transition to laying 2026-03-09 03:32:44 +07:00
rstubryan d236138aa7 refactor(FE): Update recording editability logic and extend
BaseRecording type
2026-03-09 03:18:41 +07:00
91 changed files with 1372 additions and 3834 deletions
-3
View File
@@ -45,6 +45,3 @@ next-env.d.ts
# claude
.claude
# rtk
rtk.exe
+2 -30
View File
@@ -15,7 +15,7 @@ default:
# ==========================================================
.build_template: &build_template
stage: build
image: public.ecr.aws/docker/library/node:20-alpine
image: node:20-alpine
cache:
key: npm-cache
paths:
@@ -56,7 +56,7 @@ default:
.deploy_template: &deploy_template
stage: deploy
image:
name: public.ecr.aws/aws-cli/aws-cli:latest
name: amazon/aws-cli:latest
entrypoint: ['/bin/sh', '-c']
script:
- set -e
@@ -183,31 +183,3 @@ deploy:staging:
environment:
name: staging
url: https://stg-lti-erp.mbugroup.id
# ==========================================================
# ====== STAGING (Branch production) ======
# ==========================================================
build:production:
<<: *build_template
rules:
- if: '$CI_COMMIT_BRANCH == "production"'
environment:
name: staging
variables:
NEXT_PUBLIC_LTI_URL: 'https://lti-erp.mbugroup.id'
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://auth-erp.mbugroup.id'
NEXT_PUBLIC_API_BASE_URL: 'https://api-lti.mbugroup.id/api'
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
deploy:production:
<<: *deploy_template
needs: ['build:production']
rules:
- if: '$CI_COMMIT_BRANCH == "production"'
variables:
S3_BUCKET: 'production-lti-erp.mbugroup.id'
AWS_REGION: 'ap-southeast-3'
CLOUDFRONT_DISTRIBUTION_ID: 'E1SSLXKYYITASJ'
environment:
name: staging
url: https://lti-erp.mbugroup.id
+1 -1
View File
@@ -1,3 +1,3 @@
npm run format
npm run lint
npm run typecheck
npx tsc --noEmit
+2 -2
View File
@@ -1,4 +1,4 @@
FROM public.ecr.aws/docker/library/node:20-alpine
FROM node:20-alpine
RUN apk add --no-cache git bash build-base curl
@@ -22,4 +22,4 @@ RUN mkdir -p .next/server/app/_next && \
EXPOSE 3000
CMD ["npx", "serve", ".next/server/app", "-l", "3000"]
CMD ["npx", "serve", ".next/server/app", "-l", "3000"]
+1 -3
View File
@@ -7,10 +7,8 @@
"build": "next build --turbopack",
"start": "next start",
"lint": "eslint",
"typecheck": "next typegen && tsc --noEmit",
"prepare": "husky",
"format": "prettier --write .",
"pre-commit": "npm run format && npm run lint && npm run typecheck && npm run build"
"format": "prettier --write ."
},
"dependencies": {
"@react-pdf/renderer": "^4.3.1",
@@ -1,11 +0,0 @@
import { MasterKandangContent } from '@/figma-make/components/pages/master-data/kandang/MasterKandangContent';
const MasterKandangPage = () => {
return (
<section className='w-full'>
<MasterKandangContent />
</section>
);
};
export default MasterKandangPage;
@@ -11,13 +11,10 @@ const RecordingEdit = () => {
const searchParams = useSearchParams();
const recordingId = searchParams.get('recordingId');
const recordingDetailKey = recordingId
? ['recording-detail', recordingId]
: null;
const { data: recording, isLoading: isLoadingRecording } = useSWR(
recordingDetailKey,
([, id]: [string, string]) => RecordingApi.getSingle(parseInt(id))
recordingId,
(id: string) => RecordingApi.getSingle(parseInt(id))
);
if (!recordingId) {
+2 -5
View File
@@ -11,13 +11,10 @@ const RecordingDetail = () => {
const searchParams = useSearchParams();
const recordingId = searchParams.get('recordingId');
const recordingDetailKey = recordingId
? ['recording-detail', recordingId]
: null;
const { data: recording, isLoading: isLoadingRecording } = useSWR(
recordingDetailKey,
([, id]: [string, string]) => RecordingApi.getSingle(parseInt(id))
recordingId,
(id: string) => RecordingApi.getSingle(parseInt(id))
);
if (!recordingId) {
+3 -3
View File
@@ -51,7 +51,7 @@ const Button = ({
return (
<>
{(!href || (href && disabled)) && (
{!href && (
<button
{...props}
type={type}
@@ -68,9 +68,9 @@ const Button = ({
</button>
)}
{href && !disabled && (
{href && (
<Link
href={href}
href={disabled ? '#' : href}
target={target}
rel={rel}
aria-disabled={disabled}
+1 -3
View File
@@ -35,9 +35,7 @@ const NumberInput = ({
| undefined;
if (newChangeEvent) {
newChangeEvent.target.value = parseFloat(
numberFormatValues.value
) as unknown as string;
newChangeEvent.target.value = numberFormatValues.value;
onChange?.(newChangeEvent);
}
+16 -24
View File
@@ -24,8 +24,8 @@ import {
} from '@/types/api/api-general';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
export interface OptionType<T = string | number> {
value: T;
export interface OptionType {
value: string | number;
label: string;
className?: string;
labelClassName?: string;
@@ -566,31 +566,23 @@ const useSelect = <T,>(
setSize(size + 1);
};
let formattedSuccessRawData: SuccessApiResponse<T[]> | undefined = undefined;
let formattedErrorRawData: ErrorApiResponse | undefined = undefined;
const latestPagesIndex = pages?.length ? pages.length - 1 : 0;
const { formattedSuccessRawData, formattedErrorRawData } = useMemo(() => {
let successData: SuccessApiResponse<T[]> | undefined = undefined;
let errorData: ErrorApiResponse | undefined = undefined;
if (isResponseSuccess(pages?.[latestPagesIndex])) {
successData = {
...pages![latestPagesIndex],
data:
pages?.flatMap((page) =>
isResponseSuccess(page) ? page.data : []
) ?? [],
};
}
if (isResponseError(pages?.[latestPagesIndex])) {
errorData = pages![latestPagesIndex];
}
return {
formattedSuccessRawData: successData,
formattedErrorRawData: errorData,
if (isResponseSuccess(pages?.[latestPagesIndex])) {
formattedSuccessRawData = {
...pages?.[latestPagesIndex],
data:
pages?.flatMap((page) => (isResponseSuccess(page) ? page.data : [])) ??
[],
};
}, [pages, latestPagesIndex]);
}
if (isResponseError(pages?.[latestPagesIndex])) {
formattedErrorRawData = pages?.[latestPagesIndex];
}
return {
inputValue,
@@ -112,11 +112,12 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
kandangData={kandangData}
/>
<ClosingKandangList
initialValue={initialValue}
projectData={projectData}
selectedKandangId={kandangData?.id}
/>
{!kandangData && (
<ClosingKandangList
initialValue={initialValue}
projectData={projectData}
/>
)}
<Tabs
activeTabId={activeTabId}
@@ -5,11 +5,9 @@ import { ProjectFlock } from '@/types/api/production/project-flock';
const ClosingKandangList = ({
initialValue,
projectData,
selectedKandangId,
}: {
initialValue?: ClosingGeneralInformation;
projectData?: ProjectFlock;
selectedKandangId?: number;
}) => {
return (
<div className='w-full py-3 @container relative before:absolute before:top-0 before:left-0 before:right-0 before:-mx-4 before:border-t before:border-base-content/10'>
@@ -24,9 +22,6 @@ const ClosingKandangList = ({
variant='outline'
className='px-3 py-2.5 w-fit text-sm rounded-lg shadow-sm'
href={`/closing/detail/?closingId=${initialValue?.flock_id}&kandangId=${kandang.project_flock_kandang_id}`}
disabled={
selectedKandangId === kandang.project_flock_kandang_id
}
>
{kandang.name}
</Button>
@@ -276,7 +276,7 @@ const SalesClosingTable = ({ projectFlockId }: SalesClosingTableProps) => {
{
id: 'kandang',
accessorKey: 'kandang',
header: 'Kandang Atribusi',
header: 'Kandang',
cell: (props) => {
const kandang = props.getValue() as Kandang;
return kandang?.name || '-';
@@ -127,11 +127,11 @@ const ClosingOutgoingSapronaksTable = ({
},
{
accessorKey: 'source_warehouse',
header: 'Gudang Asal (Fisik)',
header: 'Gudang Asal',
},
{
accessorKey: 'destination_warehouse',
header: 'Gudang Tujuan (Fisik)',
header: 'Gudang Tujuan',
},
{
accessorKey: 'quantity',
@@ -9,11 +9,8 @@ import { useState, useEffect, useRef, useCallback } from 'react';
import useSWR from 'swr';
import { DashboardApi } from '@/services/api/dashboard';
import { useFormik } from 'formik';
import {
ProjectFlockApi,
ProjectFlockKandangApi,
} from '@/services/api/production';
import { LocationApi } from '@/services/api/master-data';
import { ProjectFlockApi } from '@/services/api/production';
import { KandangApi, LocationApi } from '@/services/api/master-data';
import { generateDashboardPDF } from '@/components/pages/dashboard/export/DashboardPDF';
import {
DashboardFilterType,
@@ -25,7 +22,10 @@ import DashboardExportCharts, {
DashboardExportChartsRef,
} from '@/components/pages/dashboard/export/DashboardExportCharts';
import { RadioGroup, RadioGroupItem } from '@/components/input/RadioInput';
import { DashboardMeta } from '@/types/api/dashboard/dashboard';
import {
DashboardFilter,
DashboardMeta,
} from '@/types/api/dashboard/dashboard';
import DashboardStats from '@/components/pages/dashboard/chart/DashboardStats';
import { isResponseSuccess } from '@/lib/api-helper';
import AlertErrorList from '@/components/helper/form/FormErrors';
@@ -42,8 +42,6 @@ import { cn } from '@/lib/helper';
import DashboardExportStats, {
DashboardExportStatsRef,
} from '@/components/pages/dashboard/export/DashboardExportStats';
import { ProjectFlock } from '@/types/api/production/project-flock';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
// Helper function to normalize values to array
const normalizeToArray = (
@@ -70,6 +68,7 @@ const DashboardProduction = () => {
const [analysisMode, setAnalysisMode] = useState<'OVERVIEW' | 'COMPARISON'>(
(filterValues.analysisMode as 'OVERVIEW' | 'COMPARISON') || 'OVERVIEW'
);
const [endpointUrl, setEndpointUrl] = useState('/dashboards');
const [selectedLocationIds, setSelectedLocationIds] = useState<number[]>(
normalizeToArray(filterValues.location)
);
@@ -81,29 +80,9 @@ const DashboardProduction = () => {
const {
data: dashboardProductionResponse,
isLoading: isLoadingDashboardProductionData,
} = useSWR(
[
'dashboard-production',
filterValues.startDate ?? '',
filterValues.endDate ?? '',
filterValues.analysisMode ?? 'OVERVIEW',
normalizeToArray(filterValues.location).toString(),
normalizeToArray(filterValues.flock).toString(),
normalizeToArray(filterValues.kandang).toString(),
filterValues.comparisonType ?? '',
],
() =>
DashboardApi.getDashboardProductionFetcher({
start_date: filterValues.startDate || '',
end_date: filterValues.endDate || '',
analysis_mode:
(filterValues.analysisMode as 'OVERVIEW' | 'COMPARISON') ||
'OVERVIEW',
location_ids: normalizeToArray(filterValues.location),
flock_ids: normalizeToArray(filterValues.flock),
kandang_ids: normalizeToArray(filterValues.kandang),
comparison_type: filterValues.comparisonType || '',
})
mutate: refreshDashboardProductionData,
} = useSWR(endpointUrl, () =>
DashboardApi.getDashboardProductionFetcher(endpointUrl)
);
const dashboardProductionData = isResponseSuccess(dashboardProductionResponse)
@@ -116,23 +95,23 @@ const DashboardProduction = () => {
options: flockOptions,
isLoadingOptions: isLoadingFlockOptions,
loadMore: loadMoreFlock,
} = useSelect<ProjectFlock>(
ProjectFlockApi.basePath,
'id',
'flock_name',
'search',
{
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
}
);
} = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', '', {
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
});
const {
setInputValue: setInputValueLocation,
options: locationOptions,
isLoadingOptions: isLoadingLocationOptions,
loadMore: loadMoreLocation,
} = useSelect(LocationApi.basePath, 'id', 'name');
const {
setInputValue: setInputValueKandang,
options: kandangOptions,
isLoadingOptions: isLoadingKandangOptions,
loadMore: loadMoreKandang,
} = useSelect(KandangApi.basePath, 'id', 'name', '', {
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
});
const comparisonTypeOptions = [
{ value: 'FARM', label: 'Farm' },
{ value: 'FLOCK', label: 'Flock' },
@@ -156,43 +135,68 @@ const DashboardProduction = () => {
enableReinitialize: true,
validationSchema: getDashboardFilterSchema(analysisMode),
onSubmit: (values) => {
// Save filter values to store
setFilterValues(values);
filterModal.closeModal();
handleApplyFilter({
start_date: values.startDate || '',
end_date: values.endDate || '',
analysis_mode: values.analysisMode as 'OVERVIEW' | 'COMPARISON',
location_ids: normalizeToArray(values.location),
flock_ids: normalizeToArray(values.flock),
kandang_ids: normalizeToArray(values.kandang),
comparison_type: values.comparisonType,
});
},
});
const { resetForm } = formik;
const selectedLocationValues = normalizeToArray(formik.values.location);
const selectedFlockValues = normalizeToArray(formik.values.flock);
const {
setInputValue: setInputValueKandang,
options: kandangOptions,
isLoadingOptions: isLoadingKandangOptions,
loadMore: loadMoreKandang,
} = useSelect<ProjectFlockKandang>(
ProjectFlockKandangApi.basePath,
'kandang_id',
'kandang.name',
'search',
{
location_id:
selectedLocationValues.length > 0
? selectedLocationValues.toString()
: '',
project_flock_id:
selectedFlockValues.length > 0 ? selectedFlockValues.toString() : '',
}
);
const handleResetFilter = useCallback(() => {
resetForm();
resetFilterValues(); // Clear stored filter values
setAnalysisMode('OVERVIEW');
setEndpointUrl('/dashboards');
setSelectedLocationIds([]);
filterModal.closeModal();
}, [filterModal, resetForm, resetFilterValues]);
}, [resetForm, resetFilterValues]);
const handleApplyFilter = useCallback(
(values: DashboardFilter) => {
// Build query params object, only include non-empty values
const params: Record<string, string> = {};
if (values.start_date) params.start_date = values.start_date;
if (values.end_date) params.end_date = values.end_date;
if (values.analysis_mode) params.analysis_mode = values.analysis_mode;
if (values.location_ids.length > 0)
params.location_ids = values.location_ids.toString();
if (values.flock_ids.length > 0)
params.flock_ids = values.flock_ids.toString();
if (values.kandang_ids.length > 0)
params.kandang_ids = values.kandang_ids.toString();
if (values.comparison_type)
params.comparison_type = values.comparison_type;
setEndpointUrl(`/dashboards?${new URLSearchParams(params).toString()}`);
filterModal.closeModal();
refreshDashboardProductionData();
},
[filterModal, refreshDashboardProductionData]
);
// ===== Load filter from store on mount =====
useEffect(() => {
if (!filterValues) return;
handleApplyFilter({
start_date: filterValues.startDate,
end_date: filterValues.endDate,
analysis_mode: filterValues.analysisMode as 'OVERVIEW' | 'COMPARISON',
location_ids: normalizeToArray(filterValues.location),
flock_ids: normalizeToArray(filterValues.flock),
kandang_ids: normalizeToArray(filterValues.kandang),
comparison_type: filterValues.comparisonType,
});
}, [filterValues, handleApplyFilter]);
// ===== Formik Error List =====
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
@@ -264,6 +268,14 @@ const DashboardProduction = () => {
};
}, [clearNavbarActions]);
if (isLoadingDashboardProductionData) {
return (
<div className='w-full min-h-screen flex items-center justify-center'>
<span className='loading loading-spinner loading-xl'></span>
</div>
);
}
return (
<>
<section className='w-full p-3 space-y-3'>
@@ -315,15 +327,9 @@ const DashboardProduction = () => {
</div>
{/* Dashboard Stats */}
<div>
{isLoadingDashboardProductionData ? (
<div className='w-full min-h-screen flex items-center justify-center'>
<span className='loading loading-spinner loading-xl'></span>
</div>
) : (
<DashboardStats
data={dashboardProductionData?.statistics_data ?? []}
/>
)}
<DashboardStats
data={dashboardProductionData?.statistics_data ?? []}
/>
</div>
{/* Use DashboardLineChart component or skeleton */}
@@ -531,7 +537,6 @@ const DashboardProduction = () => {
className={{
select: 'rounded-lg text-sm border-base-content/10',
}}
isClearable={true}
/>
)}
@@ -568,7 +573,6 @@ const DashboardProduction = () => {
className={{
select: 'rounded-lg text-sm border-base-content/10',
}}
isClearable={true}
/>
) : (
<SelectInputRadio
@@ -600,7 +604,6 @@ const DashboardProduction = () => {
className={{
select: 'rounded-lg text-sm border-base-content/10',
}}
isClearable={true}
/>
)}
@@ -640,7 +643,6 @@ const DashboardProduction = () => {
className={{
select: 'rounded-lg text-sm border-base-content/10',
}}
isClearable={true}
/>
) : (
<SelectInputRadio
@@ -667,7 +669,6 @@ const DashboardProduction = () => {
className={{
select: 'rounded-lg text-sm border-base-content/10',
}}
isClearable={true}
/>
)}
</>
@@ -706,7 +707,6 @@ const DashboardProduction = () => {
className={{
select: 'rounded-lg text-sm border-base-content/10',
}}
isClearable={true}
/>
) : (
<SelectInputRadio
@@ -733,7 +733,6 @@ const DashboardProduction = () => {
className={{
select: 'rounded-lg text-sm border-base-content/10',
}}
isClearable={true}
/>
)}
</>
@@ -279,6 +279,8 @@ const ExpenseRequestContent = ({
)}
<div className='w-full mt-4 flex flex-col gap-4'>
{/* TODO: apply RBAC */}
<div className='w-full mx-auto flex flex-col sm:flex-row justify-end gap-2'>
{isCurrentApprovalOnHeadArea && (
<RequirePermission permissions='lti.expense.approve.head_area'>
@@ -207,7 +207,7 @@ const ExpenseRealizationForm = ({
// add new realizations for each kandang
kandangs.forEach((kandangItem) => {
if (isNaN(Number(kandangItem.id))) return;
if (!kandangItem.id) return;
const existingRealization = formik.values.realizations?.find(
(realizationItem) => realizationItem.kandang_id === kandangItem.id
@@ -35,7 +35,6 @@ const ExpenseRealizationKandangDetailExpense: React.FC<
setInputValue: setNonstockInputValue,
options: nonstockOptions,
isLoadingOptions: isLoadingNonstockOptions,
loadMore: loadMoreNonstocks,
} = useSelect<Nonstock>(
NonstockApi.basePath,
'id',
@@ -165,7 +164,6 @@ const ExpenseRealizationKandangDetailExpense: React.FC<
options={nonstockOptions}
isLoading={isLoadingNonstockOptions}
onInputChange={setNonstockInputValue}
onMenuScrollToBottom={loadMoreNonstocks}
className={{ wrapper: 'min-w-48' }}
isDisabled
/>
@@ -178,14 +178,12 @@ const ExpenseRequestForm = ({
setInputValue: setLocationInputValue,
options: locationOptions,
isLoadingOptions: isLoadingLocationOptions,
loadMore: loadMoreLocations,
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
const {
setInputValue: setVendorInputValue,
options: supplierOptions,
isLoadingOptions: isLoadingVendorOptions,
loadMore: loadMoreSuppliers,
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -410,7 +408,6 @@ const ExpenseRequestForm = ({
options={locationOptions}
onInputChange={setLocationInputValue}
isLoading={isLoadingLocationOptions}
onMenuScrollToBottom={loadMoreLocations}
isError={
formik.touched.location_id && Boolean(formik.errors.location_id)
}
@@ -455,7 +452,6 @@ const ExpenseRequestForm = ({
options={supplierOptions}
onInputChange={setVendorInputValue}
isLoading={isLoadingVendorOptions}
onMenuScrollToBottom={loadMoreSuppliers}
isError={
formik.touched.supplier_id && Boolean(formik.errors.supplier_id)
}
@@ -287,8 +287,8 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
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
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
Cipedes, Kec. Sukajadi, Kota Bandung 40162
</Text>
<View style={ExpensePDFStyle.doubleDivider} />
@@ -8,7 +8,7 @@ import {
useState,
} from 'react';
import { usePathname } from 'next/navigation';
import useSWR, { mutate } from 'swr';
import useSWR from 'swr';
import { Icon } from '@iconify/react';
import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table';
import { useFormik } from 'formik';
@@ -26,10 +26,6 @@ import { InventoryAdjustmentApi } from '@/services/api/inventory';
import { WarehouseApi, ProductApi } from '@/services/api/master-data';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { useUiStore } from '@/stores/ui/ui.store';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent';
import toast from 'react-hot-toast';
import { InventoryAdjustment } from '@/types/api/inventory/adjustment';
import { Warehouse } from '@/types/api/master-data/warehouse';
import { TRANSACTION_SUBTYPE_OPTIONS } from '@/config/constant';
@@ -42,62 +38,6 @@ import {
AdjustmentFilterType,
} from '@/components/pages/inventory/adjustment/filter/AdjustmentFilter';
import SelectInputRadio from '@/components/input/SelectInputRadio';
import { CellContext } from '@tanstack/react-table';
const RowOptionsMenu = ({
popoverPosition = 'bottom',
props,
deleteClickHandler,
}: {
popoverPosition: 'bottom' | 'top';
props: CellContext<InventoryAdjustment, unknown>;
deleteClickHandler: () => void;
}) => {
const popoverId = `adjustment#${props.row.original.id}`;
const popoverAnchorName = `--anchor-adjustment#${props.row.original.id}`;
const closePopover = () => {
document.getElementById(popoverId)?.hidePopover();
};
return (
<div className='relative'>
<PopoverButton
tabIndex={0}
variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
>
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</PopoverButton>
<PopoverContent
id={popoverId}
anchorName={popoverAnchorName}
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
>
<div className='flex flex-col bg-base-100 rounded-xl'>
<RequirePermission permissions='lti.inventory.delete'>
<Button
onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost'
color='error'
className='p-3 justify-start text-sm font-semibold w-full focus-visible:text-error-content hover:text-error-content'
>
<Icon icon='mdi:delete-outline' width={20} height={20} />
Delete
</Button>
</RequirePermission>
</div>
</PopoverContent>
</div>
);
};
const InventoryAdjustmentTable = () => {
const { searchValue, setSearchValue, setTableState } = useUiStore();
@@ -140,13 +80,13 @@ const InventoryAdjustmentTable = () => {
const formik = useFormik<AdjustmentFilterType>({
initialValues: {
product_id: null,
warehouse: null,
warehouse_id: null,
transaction_type: null,
},
validationSchema: AdjustmentFilterSchema,
onSubmit: (values, { setSubmitting }) => {
updateFilter('productFilter', values.product_id || '');
updateFilter('warehouseFilter', String(values.warehouse?.value) || '');
updateFilter('warehouseFilter', values.warehouse_id || '');
updateFilter('transactionTypeFilter', values.transaction_type || '');
filterModal.closeModal();
setSubmitting(false);
@@ -202,11 +142,14 @@ const InventoryAdjustmentTable = () => {
[formik]
);
const handleFilterWarehouseChange = (
val: OptionType | OptionType[] | null
) => {
formik.setFieldValue('warehouse', val);
};
const handleFilterWarehouseChange = useCallback(
(val: OptionType | OptionType[] | null) => {
const warehouse = val as OptionType | null;
const warehouseId = warehouse?.value ? String(warehouse.value) : null;
formik.setFieldValue('warehouse_id', warehouseId);
},
[formik]
);
const handleFilterTransactionTypeChange = useCallback(
(val: OptionType | OptionType[] | null) => {
@@ -227,6 +170,15 @@ const InventoryAdjustmentTable = () => {
);
}, [formik.values.product_id, productOptions]);
const warehouseIdValue = useMemo(() => {
if (!formik.values.warehouse_id) return null;
return (
warehouseOptions.find(
(opt) => String(opt.value) === formik.values.warehouse_id
) || null
);
}, [formik.values.warehouse_id, warehouseOptions]);
const transactionTypeValue = useMemo(() => {
if (!formik.values.transaction_type) return null;
return (
@@ -242,39 +194,12 @@ const InventoryAdjustmentTable = () => {
formik.validateForm();
};
const {
data: inventoryAdjustments,
isLoading,
mutate: refreshAdjustments,
} = useSWR(
const { data: inventoryAdjustments, isLoading } = useSWR(
`${InventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`,
InventoryAdjustmentApi.getAllFetcher
);
const singleDeleteHandler = async () => {
setIsDeleteLoading(true);
const response = await InventoryAdjustmentApi.delete(
selectedAdjustment?.id as number
);
singleDeleteModal.closeModal();
setIsDeleteLoading(false);
if (isResponseSuccess(response)) {
toast.success(response?.message || 'Successfully delete Adjustment!');
refreshAdjustments();
} else {
toast.error(response?.message || 'Failed to delete Adjustment');
}
};
const [sorting, setSorting] = useState<SortingState>([]);
const [selectedAdjustment, setSelectedAdjustment] = useState<
InventoryAdjustment | undefined
>(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const singleDeleteModal = useModal();
useEffect(() => {
updateFilter('search', searchValue);
@@ -389,39 +314,8 @@ const InventoryAdjustmentTable = () => {
header: 'Oleh',
accessorFn: (row) => row.created_user?.name ?? '-',
},
{
id: 'actions',
header: 'Aksi',
cell: (props: CellContext<InventoryAdjustment, unknown>) => {
const currentPageSize =
props.table.getPaginationRowModel().rows.length;
const currentPageRows = props.table.getPaginationRowModel().flatRows;
const currentRowRelativeIndex =
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
const deleteClickHandler = () => {
setSelectedAdjustment(props.row.original);
singleDeleteModal.openModal();
};
return (
<RowOptionsMenu
props={props}
deleteClickHandler={deleteClickHandler}
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
/>
);
},
},
],
[
tableFilterState.pageSize,
tableFilterState.page,
singleDeleteModal,
setSelectedAdjustment,
]
[tableFilterState.pageSize, tableFilterState.page]
);
const updateSortingFilter = useCallback(
@@ -608,7 +502,7 @@ const InventoryAdjustmentTable = () => {
label='Gudang'
placeholder='Pilih Gudang'
options={warehouseOptions}
value={formik.values.warehouse}
value={warehouseIdValue}
onChange={handleFilterWarehouseChange}
onInputChange={setWarehouseInputValue}
isLoading={isLoadingWarehouseOptions}
@@ -650,21 +544,6 @@ const InventoryAdjustmentTable = () => {
</div>
</form>
</Modal>
<ConfirmationModal
ref={singleDeleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Adjustment ini?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isDeleteLoading,
onClick: singleDeleteHandler,
}}
/>
</>
);
};
@@ -1,5 +1,4 @@
import { string, object } from 'yup';
import { OptionType } from '@/components/input/SelectInput';
export const AdjustmentFilterSchema = object().shape({
product_id: string().nullable(),
@@ -9,6 +8,6 @@ export const AdjustmentFilterSchema = object().shape({
export type AdjustmentFilterType = {
product_id: string | null;
warehouse_id: string | null;
transaction_type: string | null;
warehouse: OptionType<number> | null;
};
@@ -15,7 +15,7 @@ import {
InventoryAdjustmentFormSchema,
InventoryAdjustmentFormValues,
} from '@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema';
import { LocationApi } from '@/services/api/master-data';
import { KandangApi, LocationApi } from '@/services/api/master-data';
import {
ProjectFlockApi,
ProjectFlockKandangApi,
@@ -32,6 +32,8 @@ import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import AlertErrorList from '@/components/helper/form/FormErrors';
import { Location } from '@/types/api/master-data/location';
import { ProjectFlock } from '@/types/api/production/project-flock';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
import { Kandang } from '@/types/api/master-data/kandang';
import { Product } from '@/types/api/master-data/product';
import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock';
import { BaseApiResponse } from '@/types/api/api-general';
@@ -117,19 +119,40 @@ const InventoryAdjustmentForm = ({
}
);
const { rawData: approvedProjectFlockKandangsRawData } =
useSelect<ProjectFlockKandang>(
ProjectFlockKandangApi.basePath,
'id',
'id',
'search',
{
step_name: 'Disetujui',
limit: '100',
}
);
const approvedProjectFlockKandangs = useMemo(() => {
if (
approvedProjectFlockKandangsRawData &&
'data' in approvedProjectFlockKandangsRawData
) {
return approvedProjectFlockKandangsRawData.data as ProjectFlockKandang[];
}
return [];
}, [approvedProjectFlockKandangsRawData]);
const {
options: projectFlockKandangOptions,
loadMore: loadMoreProjectFlockKandangs,
setInputValue: setProjectFlockKandangInputValue,
isLoadingOptions: isLoadingProjectFlockKandangOptions,
} = useSelect(
selectedProjectFlock ? ProjectFlockKandangApi.basePath : '',
'kandang.id',
'kandang.name',
setInputValue: setKandangInputValue,
options: kandangOptionsFromApi,
isLoadingOptions: isLoadingKandangOptions,
loadMore: loadMoreKandangs,
} = useSelect<Kandang>(
selectedProjectFlock ? KandangApi.basePath : '',
'id',
'name',
'search',
{
step_name: 'Disetujui',
project_flock_id: String(selectedProjectFlock?.value),
location_id: selectedProjectFlockLocationId,
}
);
@@ -199,6 +222,26 @@ const InventoryAdjustmentForm = ({
return (product?.flags as string[]) || [];
}, [selectedProduct, productOptions]);
const kandangOptions = useMemo(() => {
let options: OptionType[] = [];
if (selectedProjectFlock) {
const approvedKandangIds = approvedProjectFlockKandangs
.filter((pfk) => pfk.project_flock_id === selectedProjectFlock.value)
.map((pfk) => pfk.kandang_id);
options = kandangOptionsFromApi.filter((kandang) =>
approvedKandangIds.includes(kandang.value as number)
);
}
return options;
}, [
selectedProjectFlock,
kandangOptionsFromApi,
approvedProjectFlockKandangs,
]);
const formikInitialValues = useMemo<Partial<InventoryAdjustmentFormValues>>(
() => ({
location: null,
@@ -650,10 +693,10 @@ const InventoryAdjustmentForm = ({
label='Kandang'
value={selectedKandang}
onChange={kandangChangeHandler}
onInputChange={setProjectFlockKandangInputValue}
options={projectFlockKandangOptions}
onMenuScrollToBottom={loadMoreProjectFlockKandangs}
isLoading={isLoadingProjectFlockKandangOptions}
onInputChange={setKandangInputValue}
options={kandangOptions}
onMenuScrollToBottom={loadMoreKandangs}
isLoading={isLoadingKandangOptions}
isError={
formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
}
@@ -8,7 +8,7 @@ import {
useState,
} from 'react';
import { usePathname } from 'next/navigation';
import useSWR, { mutate } from 'swr';
import useSWR from 'swr';
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
import { useFormik } from 'formik';
@@ -21,8 +21,6 @@ import { cn } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { useUiStore } from '@/stores/ui/ui.store';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import toast from 'react-hot-toast';
import Button from '@/components/Button';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import SelectInput, { useSelect } from '@/components/input/SelectInput';
@@ -43,11 +41,9 @@ import {
const RowOptionsMenu = ({
popoverPosition = 'bottom',
props,
deleteClickHandler,
}: {
popoverPosition: 'bottom' | 'top';
props: CellContext<Movement, unknown>;
deleteClickHandler: () => void;
}) => {
const popoverId = `movement#${props.row.original.id}`;
const popoverAnchorName = `--anchor-movement#${props.row.original.id}`;
@@ -87,20 +83,6 @@ const RowOptionsMenu = ({
Detail
</Button>
</RequirePermission>
<RequirePermission permissions='lti.inventory.transfer.delete'>
<Button
onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost'
color='error'
className='p-3 justify-start text-sm font-semibold w-full focus-visible:text-error-content hover:text-error-content'
>
<Icon icon='mdi:delete-outline' width={20} height={20} />
Delete
</Button>
</RequirePermission>
</div>
</PopoverContent>
</div>
@@ -224,37 +206,12 @@ const MovementTable = () => {
};
const [sorting, setSorting] = useState<SortingState>([]);
const [selectedMovement, setSelectedMovement] = useState<
Movement | undefined
>(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const singleDeleteModal = useModal();
const {
data: movements,
isLoading,
mutate: refreshMovements,
} = useSWR(
const { data: movements, isLoading } = useSWR(
`${MovementApi.basePath}${getTableFilterQueryString()}`,
MovementApi.getAllFetcher
);
const singleDeleteHandler = async () => {
setIsDeleteLoading(true);
const response = await MovementApi.delete(selectedMovement?.id as number);
singleDeleteModal.closeModal();
setIsDeleteLoading(false);
if (isResponseSuccess(response)) {
toast.success(response?.message || 'Successfully delete Movement!');
refreshMovements();
} else {
toast.error(response?.message || 'Failed to delete Movement');
}
};
useEffect(() => {
updateFilter('search', searchValue);
}, [searchValue, updateFilter]);
@@ -318,27 +275,16 @@ const MovementTable = () => {
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
const deleteClickHandler = () => {
setSelectedMovement(props.row.original);
singleDeleteModal.openModal();
};
return (
<RowOptionsMenu
props={props}
deleteClickHandler={deleteClickHandler}
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
/>
);
},
},
],
[
tableFilterState.pageSize,
tableFilterState.page,
singleDeleteModal,
setSelectedMovement,
]
[tableFilterState.pageSize, tableFilterState.page]
);
return (
@@ -509,21 +455,6 @@ const MovementTable = () => {
</div>
</form>
</Modal>
<ConfirmationModal
ref={singleDeleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Movement ini?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isDeleteLoading,
onClick: singleDeleteHandler,
}}
/>
</>
);
};
@@ -82,7 +82,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
warehouse_id: number;
warehouse_name: string;
quantity: number;
transfer_available_qty?: number;
}
// ===== USE SELECT HOOKS =====
@@ -380,8 +379,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
warehouse_id: formik.values.source_warehouse_id
? formik.values.source_warehouse_id.toString()
: '',
transfer_context: 'inventory_transfer',
stock_mode: 'exclude_chickin',
}
);
@@ -394,7 +391,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
warehouse_id: pw.warehouse.id,
warehouse_name: pw.warehouse.name,
quantity: pw.quantity,
transfer_available_qty: pw.transfer_available_qty,
}))
: [];
}, [productWarehouses]);
@@ -838,22 +834,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
}, [formik.values.products, formik.values.deliveries]);
const getAvailableStock = useCallback(
(productId: number) => {
if (type === 'detail') return 0;
const productWarehouse = productWarehouseOptions.find(
(pw) => pw.product_id === productId
);
return (
productWarehouse?.transfer_available_qty ??
productWarehouse?.quantity ??
0
);
},
[productWarehouseOptions, type]
);
const getTotalStock = useCallback(
(productId: number) => {
if (type === 'detail') return 0;
const productWarehouse = productWarehouseOptions.find(
@@ -864,16 +844,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
[productWarehouseOptions, type]
);
const hasAvailableQty = useCallback(
(productId: number) => {
const productWarehouse = productWarehouseOptions.find(
(pw) => pw.product_id === productId
);
return productWarehouse?.transfer_available_qty !== undefined;
},
[productWarehouseOptions]
);
const getProductQtyBottomLabel = useCallback(
(productIdx: number) => {
if (type === 'detail') return undefined;
@@ -881,31 +851,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
if (!product || !product.product_id) return undefined;
const availableStock = getAvailableStock(product.product_id);
const totalStock = getTotalStock(product.product_id);
const requestedQty = Number(product.product_qty) || 0;
const remainingStock = availableStock - requestedQty;
const isAyamProduct = hasAvailableQty(product.product_id);
if (requestedQty > 0) {
if (isAyamProduct) {
return `Sisa: ${formatNumber(remainingStock)} (Total: ${formatNumber(totalStock)})`;
}
return `Sisa: ${formatNumber(remainingStock)}`;
}
if (isAyamProduct) {
return `Tersedia: ${formatNumber(availableStock)} (Total: ${formatNumber(totalStock)})`;
}
return `Tersedia: ${formatNumber(availableStock)}`;
},
[
formik.values.products,
getAvailableStock,
getTotalStock,
hasAvailableQty,
type,
]
[formik.values.products, getAvailableStock, type]
);
const getDeliveryProductQtyBottomLabel = useCallback(
@@ -967,26 +922,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
if (!product || !product.product_id) return null;
const availableStock = getAvailableStock(product.product_id);
const totalStock = getTotalStock(product.product_id);
const requestedQty = Number(product.product_qty) || 0;
const isAyamProduct = hasAvailableQty(product.product_id);
if (requestedQty > availableStock) {
if (isAyamProduct) {
return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)} (Total: ${formatNumber(totalStock)}, terpakai untuk chickin: ${formatNumber(totalStock - availableStock)})`;
}
return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)}`;
}
return null;
},
[
formik.values.products,
getAvailableStock,
getTotalStock,
hasAvailableQty,
type,
]
[formik.values.products, getAvailableStock, type]
);
const validateDeliveryQty = useCallback(
@@ -199,9 +199,6 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
'yyyy-MM-DD'
),
vehicle_number: product.vehicle_number,
weight_per_convertion: parseFloat(
String(product.weight_per_convertion ?? 0)
),
};
}
})
@@ -371,9 +368,7 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
const currentProducts = deliveryOrderValues?.find(
(product) => product.id == id
);
setSelectedDeliveryProduct(currentProducts ?? values ?? null);
setSelectedDeliveryProduct(values ?? currentProducts ?? null);
if (id) {
setStep(2);
}
@@ -435,9 +430,6 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
'yyyy-MM-DD'
),
vehicle_number: product.vehicle_number,
weight_per_convertion: parseFloat(
String(product.weight_per_convertion ?? 0)
),
};
}
})
@@ -10,14 +10,9 @@ import SelectInput, {
useSelect,
} from '@/components/input/SelectInput';
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
import {
MarketingFilterFormValues,
MarketingFilterSchema,
} from '@/components/pages/marketing/filter/MarketingFilter';
import { MarketingFilter } from '@/types/api/marketing/marketing';
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
import { MarketingApi } from '@/services/api/marketing/marketing';
import { CustomerApi } from '@/services/api/master-data';
import { isResponseSuccess } from '@/lib/api-helper';
import { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing';
@@ -42,12 +37,9 @@ const MarketingFilterModal = ({
isLoadingOptions: isLoadingProductsOptions,
setInputValue: setProductsInputValue,
loadMore: loadMoreProducts,
} = useSelect<BaseMarketing>(
MarketingApi.basePath,
'id',
'so_number',
'search'
);
} = useSelect<BaseMarketing>(MarketingApi.basePath, 'id', 'so_number', '', {
limit: 'limit',
});
const productsOptions = useMemo(() => {
if (!productsRawData || !isResponseSuccess(productsRawData)) return [];
@@ -74,10 +66,19 @@ const MarketingFilterModal = ({
isLoadingOptions: isLoadingCustomersOptions,
setInputValue: setCustomersInputValue,
loadMore: loadMoreCustomers,
} = useSelect(CustomerApi.basePath, 'id', 'name', 'search', {
has_marketing: 'true',
} = useSelect(MarketingApi.basePath, 'customer.id', 'customer.name', '', {
limit: 'limit',
});
const uniqueCustomersOptions = useMemo(() => {
const seen = new Set();
return customersOptions.filter((customer) => {
if (seen.has(customer.value)) return false;
seen.add(customer.value);
return true;
});
}, [customersOptions]);
const statusOptions = [
...MARKETING_APPROVAL_LINE.map((item) => ({
value: item.step_name.split(' ').join('_').toUpperCase(),
@@ -86,19 +87,23 @@ const MarketingFilterModal = ({
{ value: 'DITOLAK', label: 'Ditolak' },
];
const formik = useFormik<MarketingFilterFormValues>({
const formik = useFormik<{
product_ids: OptionType[];
status: OptionType | null;
customer_id: OptionType | null;
}>({
initialValues: {
product_ids: [],
status: null,
customer: null,
customer_id: null,
},
validationSchema: MarketingFilterSchema,
onSubmit: async (values) => {
const formattedValues: MarketingFilter = {
const formattedValues = {
...values,
product_ids: values.product_ids.map((item) => Number(item.value)),
status: values.status?.value.toString() || '',
customer_id: Number(values.customer?.value),
customer_id: Number(values.customer_id?.value),
};
onSubmit?.(formattedValues);
@@ -116,10 +121,7 @@ const MarketingFilterModal = ({
};
const customerChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue(
'customer',
!Array.isArray(val) ? (val as OptionType<number> | null) : null
);
formik.setFieldValue('customer_id', val as OptionType);
};
const statusChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -185,9 +187,9 @@ const MarketingFilterModal = ({
label='Customer'
isClearable
placeholder='Pilih customer'
options={customersOptions}
options={uniqueCustomersOptions}
isLoading={isLoadingCustomersOptions}
value={formik.values.customer}
value={formik.values.customer_id}
onChange={customerChangeHandler}
onInputChange={setCustomersInputValue}
onMenuScrollToBottom={loadMoreCustomers}
@@ -746,7 +746,7 @@ const MarketingTable = () => {
}
columns={[
{
header: 'Gudang Fisik',
header: 'Kandang',
accessorFn(row) {
return row.product_warehouse.warehouse.name;
},
@@ -195,9 +195,7 @@ const SalesOrderFormModal = ({
product.marketing_type?.value?.toLowerCase() === 'telur'
? convertionUnitValue === 'PETI'
? 'PETI'
: convertionUnitValue === 'QTY'
? 'QTY'
: 'KG'
: 'KG' // termasuk "QTY" dan "KG"
: undefined;
// Jika value dari data product ada week, kirim "AYAM_PULLET, jika tidak ada kirim "AYAM"
@@ -209,6 +207,7 @@ const SalesOrderFormModal = ({
return {
vehicle_number: product.vehicle_number as string,
kandang_id: product.kandang_id as number,
product_warehouse_id: product.product_warehouse_id as number,
unit_price: parseFloat(String(product.unit_price || 0)),
total_weight: parseFloat(String(product.total_weight || 0)),
@@ -1,14 +0,0 @@
import { array, mixed, object } from 'yup';
import { OptionType } from '@/components/input/SelectInput';
export const MarketingFilterSchema = object({
product_ids: array().of(mixed<OptionType<number>>().required()).required(),
status: mixed<OptionType<string>>().nullable(),
customer: mixed<OptionType<number>>().nullable(),
});
export type MarketingFilterFormValues = {
product_ids: OptionType<number>[];
status: OptionType<string> | null;
customer: OptionType<number> | null;
};
@@ -13,7 +13,6 @@ import {
Marketing,
} from '@/types/api/marketing/marketing';
import { formatDate, formatTitleCase } from '@/lib/helper';
import { getProductWarehouseOptionLabel } from '@/lib/product-warehouse';
type MarketingSchemaType = {
customer_id: number | undefined;
@@ -98,21 +97,17 @@ export type DeliveryOrderFormValues = Yup.InferType<typeof DeliveryOrderSchema>;
export const SalesProductToFieldValues = (
product: BaseSalesOrder
): SalesOrderProductFormValues => {
const warehouseOption = {
value: product.product_warehouse.warehouse.id,
label: product.product_warehouse.warehouse.name,
};
return {
id: product.id,
vehicle_number: product.vehicle_number,
warehouse_id: product.product_warehouse.warehouse.id,
warehouse: warehouseOption,
kandang_id: product.product_warehouse.warehouse.id,
kandang: warehouseOption,
kandang: {
value: product.product_warehouse.warehouse.id,
label: product.product_warehouse.warehouse.name,
},
product_warehouse: {
value: product.product_warehouse.id,
label: getProductWarehouseOptionLabel(product.product_warehouse),
label: product.product_warehouse.product.name,
},
product_warehouse_data: product.product_warehouse,
product_warehouse_id: product.product_warehouse.id,
@@ -144,34 +139,11 @@ export const DeliveryProductToFieldValues = (
delivery: BaseDeliveryOrder
): DeliveryOrderProductFormValues[] => {
const data = delivery.deliveries.map((item) => {
const salesOrder = salesOrders.find(
const soId = salesOrders.find(
(so) => so.product_warehouse.id === item.product_warehouse.id
);
const warehouseOption = {
value: item.product_warehouse.warehouse.id,
label: item.product_warehouse.warehouse.name,
};
const initialSisaBerat =
item?.total_weight &&
salesOrder?.weight_per_convertion &&
salesOrder?.total_peti
? Number(item.total_weight) -
Number(salesOrder.weight_per_convertion) *
Number(salesOrder.total_peti)
: 0;
const initialPricePerConvertion =
item?.total_price &&
salesOrder?.total_peti &&
Number(salesOrder.total_peti) !== 0
? (Number(item.total_price) -
initialSisaBerat * Number(item.unit_price || 0)) /
Number(salesOrder.total_peti)
: Number(item?.unit_price || 0);
)?.id;
return {
id: salesOrder?.id,
id: soId,
unit_price: item.unit_price,
total_weight: item.total_weight,
qty: item.qty,
@@ -180,31 +152,19 @@ export const DeliveryProductToFieldValues = (
vehicle_number: item.vehicle_number,
delivery_date: formatDate(delivery.delivery_date, 'yyyy-MM-DD'),
do_number: delivery.do_number,
marketing_product_id: salesOrder?.id,
marketing_type: salesOrder?.marketing_type
? {
value: salesOrder?.marketing_type,
label: formatTitleCase(salesOrder?.marketing_type),
}
: null,
convertion_unit: salesOrder?.convertion_unit
? {
value: salesOrder?.convertion_unit.toLowerCase(),
label: formatTitleCase(salesOrder?.convertion_unit),
}
: null,
marketing_product_id: soId,
marketing_product: {
id: salesOrder?.id,
id: soId,
vehicle_number: item.vehicle_number,
warehouse_id: item.product_warehouse.warehouse.id,
warehouse: warehouseOption,
kandang_id: item.product_warehouse.warehouse.id,
kandang: warehouseOption,
kandang: {
value: item.product_warehouse.warehouse.id,
label: item.product_warehouse.warehouse.name,
},
product_warehouse: {
value: item.product_warehouse.id,
label: getProductWarehouseOptionLabel(item.product_warehouse),
label: item.product_warehouse.product.name,
},
product_warehouse_data: item.product_warehouse,
product_warehouse_id: item.product_warehouse.id,
unit_price: item.unit_price,
total_weight: item.total_weight,
@@ -212,13 +172,8 @@ export const DeliveryProductToFieldValues = (
avg_weight: item.avg_weight,
total_price: item.total_price,
},
total_peti: salesOrder?.total_peti,
weight_per_convertion:
item?.weight_per_convertion ?? salesOrder?.weight_per_convertion ?? 0,
price_per_convertion: initialPricePerConvertion,
} as DeliveryOrderProductFormValues;
});
return data;
};
export const mergeSOwithDO = (
@@ -226,25 +181,10 @@ export const mergeSOwithDO = (
deliveryOrders: DeliveryOrderProductFormValues[],
autofill?: boolean
): DeliveryOrderProductFormValues[] => {
const hasDeliveryOrders = deliveryOrders.length > 0;
return salesOrders.map((so) => {
const delivery = deliveryOrders.find(
(d) => d?.marketing_product_id === so.id
);
const isTelurQty =
so.marketing_type?.value?.toLowerCase() === 'telur' &&
so.convertion_unit?.value?.toLowerCase() === 'qty';
const salesOrderUnitPrice =
isTelurQty && Number(so.total_price || 0) > 0 && Number(so.qty || 0) > 0
? Number(so.total_price) / Number(so.qty)
: so.unit_price;
const salesOrderPricePerQty =
isTelurQty &&
Number(so.total_price || 0) > 0 &&
Number(so.total_weight || 0) > 0
? Number(so.total_price) / Number(so.total_weight)
: so.price_per_qty;
return {
...so, // nilai dasar dari sales order
@@ -252,50 +192,30 @@ export const mergeSOwithDO = (
delivery_date: delivery?.delivery_date || undefined,
do_number: delivery?.do_number || undefined,
vehicle_number: delivery?.vehicle_number || so.vehicle_number,
unit_price:
autofill && hasDeliveryOrders
? delivery?.unit_price
: salesOrderUnitPrice,
total_weight:
autofill && hasDeliveryOrders
? delivery?.total_weight
: so.total_weight,
qty: autofill && hasDeliveryOrders ? delivery?.qty : so.qty,
avg_weight:
autofill && hasDeliveryOrders ? delivery?.avg_weight : so.avg_weight,
total_price:
autofill && hasDeliveryOrders ? delivery?.total_price : so.total_price,
unit_price: autofill ? so.unit_price : delivery?.unit_price,
total_weight: autofill ? so.total_weight : delivery?.total_weight,
qty: autofill ? so.qty : delivery?.qty,
avg_weight: autofill ? so.avg_weight : delivery?.avg_weight,
total_price: autofill ? so.total_price : delivery?.total_price,
marketing_product: so, // jika ada, override
uom: autofill && hasDeliveryOrders ? delivery?.uom : so.uom,
weight_per_convertion:
autofill && hasDeliveryOrders
? delivery?.weight_per_convertion
: so.weight_per_convertion,
price_per_convertion:
autofill && hasDeliveryOrders
? delivery?.price_per_convertion
: so.price_per_convertion,
convertion_unit:
autofill && hasDeliveryOrders
? delivery?.convertion_unit
: so.convertion_unit,
marketing_type:
autofill && hasDeliveryOrders
? delivery?.marketing_type
: so.marketing_type,
total_peti:
autofill && hasDeliveryOrders ? delivery?.total_peti : so.total_peti,
price_per_qty:
autofill && hasDeliveryOrders
? delivery?.price_per_qty
: salesOrderPricePerQty,
sisa_berat:
autofill && hasDeliveryOrders ? delivery?.sisa_berat : so.sisa_berat,
price_sisa_berat:
autofill && hasDeliveryOrders
? delivery?.price_sisa_berat
: so.price_sisa_berat,
week: autofill && hasDeliveryOrders ? delivery?.week : so.week,
uom: autofill ? so.uom : delivery?.uom,
weight_per_convertion: autofill
? so.weight_per_convertion
: delivery?.weight_per_convertion,
price_per_convertion: autofill
? so.price_per_convertion
: delivery?.price_per_convertion,
convertion_unit: autofill
? so.convertion_unit
: delivery?.convertion_unit,
marketing_type: autofill ? so.marketing_type : delivery?.marketing_type,
total_peti: autofill ? so.total_peti : delivery?.total_peti,
price_per_qty: autofill ? so.price_per_qty : delivery?.price_per_qty,
sisa_berat: autofill ? so.sisa_berat : delivery?.sisa_berat,
price_sisa_berat: autofill
? so.price_sisa_berat
: delivery?.price_sisa_berat,
week: autofill ? so.week : delivery?.week,
} as DeliveryOrderProductFormValues;
});
};
@@ -32,63 +32,6 @@ import Dropdown from '@/components/Dropdown';
import { Icon } from '@iconify/react';
import { handleMarketingCalculation } from '@/lib/marketing-calculation';
type PricingOption =
| string
| {
value: string;
label: string;
}
| null
| undefined;
type PricingSource =
| {
marketing_type?: PricingOption;
convertion_unit?: PricingOption;
total_price?: string | number | null;
qty?: string | number | null;
total_weight?: string | number | null;
unit_price?: string | number | null;
price_per_qty?: number | null;
}
| null
| undefined;
const getOptionValue = (value?: PricingOption) => {
if (!value) return undefined;
if (typeof value === 'string') return value.toLowerCase();
return value.value?.toLowerCase();
};
const isTelurQtyProduct = (value?: PricingSource) =>
getOptionValue(value?.marketing_type) === 'telur' &&
getOptionValue(value?.convertion_unit) === 'qty';
const getDisplayedUnitPrice = (value?: PricingSource) => {
if (
isTelurQtyProduct(value) &&
Number(value?.total_price || 0) > 0 &&
Number(value?.qty || 0) > 0
) {
return Number(value?.total_price) / Number(value?.qty);
}
return value?.unit_price ?? undefined;
};
const getDisplayedPricePerQty = (value?: PricingSource) => {
if (
isTelurQtyProduct(value) &&
Number(value?.total_price || 0) > 0 &&
Number(value?.total_weight || 0) > 0
) {
return Number(value?.total_price) / Number(value?.total_weight);
}
return value?.price_per_qty ?? null;
};
const DeliveryOrderProductForm = ({
formState,
salesOrders,
@@ -133,7 +76,7 @@ const DeliveryOrderProductForm = ({
? (Number(initialValues.total_price) -
initialSisaBerat * Number(initialValues.unit_price || 0)) /
Number(initialValues.total_peti)
: Number(initialValues?.unit_price || 0);
: 0;
const initialPriceSisaBerat =
initialValues?.total_price && initialValues?.total_peti
@@ -169,7 +112,7 @@ const DeliveryOrderProductForm = ({
if (!Boolean(item.qty)) {
return {
value: item.id,
label: `${item.marketing_product?.product_warehouse?.label} - ${item.marketing_product?.warehouse?.label ?? item.marketing_product?.kandang?.label}`,
label: `${item.marketing_product?.product_warehouse?.label} - ${item.marketing_product?.kandang?.label}`,
} as OptionType;
} else {
return null;
@@ -211,27 +154,6 @@ const DeliveryOrderProductForm = ({
(item) => item.id === initialValues?.marketing_product_id
);
const defaultPricingSource: PricingSource = {
marketing_type:
initialValues?.marketing_type ?? salesOrder?.marketing_type ?? null,
convertion_unit:
initialValues?.convertion_unit ?? salesOrder?.convertion_unit ?? null,
total_price:
deliveryOrder?.total_price ??
initialValues?.total_price ??
salesOrder?.total_price,
qty: deliveryOrder?.qty ?? initialValues?.qty ?? salesOrder?.qty,
total_weight:
deliveryOrder?.total_weight ??
initialValues?.total_weight ??
salesOrder?.total_weight,
unit_price:
deliveryOrder?.unit_price ??
initialValues?.unit_price ??
salesOrder?.unit_price,
price_per_qty: initialValues?.price_per_qty ?? null,
};
const formik = useFormik<DeliveryOrderProductFormValues>({
enableReinitialize: true,
initialValues: {
@@ -245,7 +167,8 @@ const DeliveryOrderProductForm = ({
undefined,
marketing_product_id:
salesOrder?.id || initialValues?.marketing_product_id || undefined,
unit_price: getDisplayedUnitPrice(defaultPricingSource),
unit_price:
deliveryOrder?.unit_price ?? initialValues?.unit_price ?? undefined,
total_weight:
deliveryOrder?.total_weight ?? initialValues?.total_weight ?? undefined,
qty: deliveryOrder?.qty ?? initialValues?.qty ?? undefined,
@@ -263,7 +186,7 @@ const DeliveryOrderProductForm = ({
convertion_unit: initialValues?.convertion_unit || null,
marketing_type: initialValues?.marketing_type || null,
total_peti: initialValues?.total_peti ?? null,
price_per_qty: getDisplayedPricePerQty(defaultPricingSource),
price_per_qty: initialValues?.price_per_qty ?? null,
sisa_berat: initialSisaBerat,
price_sisa_berat: initialPriceSisaBerat,
week: initialValues?.week ?? null,
@@ -406,15 +329,11 @@ const DeliveryOrderProductForm = ({
if (!Boolean(initialValues.qty)) {
handleResetForm();
} else {
setFormikValues({
...initialValues,
unit_price: getDisplayedUnitPrice(initialValues),
price_per_qty: getDisplayedPricePerQty(initialValues),
});
setFormikValues(initialValues);
if (initialValues?.marketing_product_id) {
setSelectedProduct({
value: initialValues?.id,
label: `${initialValues?.marketing_product?.product_warehouse?.label} - ${initialValues?.marketing_product?.warehouse?.label ?? initialValues?.marketing_product?.kandang?.label}`,
label: `${initialValues?.marketing_product?.product_warehouse?.label} - ${initialValues?.marketing_product?.kandang?.label}`,
} as OptionType);
}
}
@@ -539,11 +458,10 @@ const DeliveryOrderProductForm = ({
marketing_product_id: selected.value as number,
marketing_product: soFieldValues,
qty: so.qty,
unit_price: getDisplayedUnitPrice(so),
unit_price: so.unit_price,
total_price: so.total_price,
avg_weight: so.avg_weight,
total_weight: so.total_weight,
price_per_qty: getDisplayedPricePerQty(so),
vehicle_number: so.vehicle_number,
week: soFieldValues.week ?? null,
});
@@ -554,11 +472,7 @@ const DeliveryOrderProductForm = ({
text={
exisitingValues?.find(
(item) => item.id === selectedProduct?.value
)?.marketing_product?.warehouse?.label ??
exisitingValues?.find(
(item) => item.id === selectedProduct?.value
)?.marketing_product?.kandang?.label ??
''
)?.marketing_product?.kandang?.label ?? ''
}
color='success'
className={{
@@ -724,7 +638,7 @@ const DeliveryOrderProductForm = ({
placeholder='Masukan Total Peti'
endAdornment={
<div className='flex items-center gap-2'>
<span className='text-sm text-base-content/50'>Peti</span>
<span className='text-sm text-base-content/50'>Kg</span>
</div>
}
bottomLabel={`1 ${formik.values.convertion_unit?.value.toLowerCase()} = ${formik.values.weight_per_convertion ?? 0} Kg`}
@@ -774,9 +688,6 @@ const DeliveryOrderProductForm = ({
}
errorMessage={formik.errors.total_weight}
placeholder='Masukan Total Bobot'
disabled={
formik.values.convertion_unit?.value.toLowerCase() === 'peti'
}
/>
)}
@@ -846,32 +757,12 @@ const DeliveryOrderProductForm = ({
/>
)}
{/* Harga Satuan */}
{formik.values.convertion_unit?.value.toLowerCase() !== 'peti' &&
formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && (
<NumberInput
required
label={`Harga / ${formik.values.convertion_unit?.label.toLowerCase() !== 'qty' ? 'Kg' : 'Butir'} (Rp)`}
name='unit_price'
value={formik.values.unit_price}
onChange={(e) => {
const value = Number(e.target.value);
handleFieldChange('unit_price', value, () =>
setCurrentInput(e.target.name)
);
}}
isError={Boolean(formik.errors.unit_price)}
errorMessage={formik.errors.unit_price}
placeholder='Masukan Harga Satuan'
/>
)}
{/* Harga per kg untuk TELUR + QTY */}
{/* Harga per butir untuk TELUR + QTY */}
{formik.values.marketing_type?.value.toLowerCase() === 'telur' &&
formik.values.convertion_unit?.value.toLowerCase() === 'qty' && (
<NumberInput
required
label='Harga / Kg (Rp)'
label='Harga / Butir (Rp)'
name='price_per_qty'
value={formik.values.price_per_qty ?? undefined}
onChange={(e) => {
@@ -885,7 +776,27 @@ const DeliveryOrderProductForm = ({
Boolean(formik.errors.price_per_qty)
}
errorMessage={formik.errors.price_per_qty}
placeholder='Masukan Harga per Kg'
placeholder='Masukan Harga per Butir'
/>
)}
{/* Harga Satuan */}
{formik.values.convertion_unit?.value.toLowerCase() !== 'peti' &&
formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && (
<NumberInput
required
label={`Harga / ${isResponseSuccess(productData) ? productData?.data?.uom?.name : 'Produk'} (Rp)`}
name='unit_price'
value={formik.values.unit_price}
onChange={(e) => {
const value = Number(e.target.value);
handleFieldChange('unit_price', value, () =>
setCurrentInput(e.target.name)
);
}}
isError={Boolean(formik.errors.unit_price)}
errorMessage={formik.errors.unit_price}
placeholder='Masukan Harga Satuan'
/>
)}
@@ -3,11 +3,6 @@ import * as Yup from 'yup';
type SalesOrderProductSchemaType = {
id?: number | undefined;
warehouse_id?: number;
warehouse?: {
value: number;
label: string;
} | null;
kandang_id?: number;
kandang?: {
value: number;
@@ -49,22 +44,15 @@ export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaTy
Yup.object({
id: Yup.number(),
vehicle_number: Yup.string().required('Nomor Kendaraan wajib diisi!'),
warehouse: Yup.object({
value: Yup.number()
.min(1, 'Gudang fisik wajib diisi!')
.required('Gudang fisik wajib diisi!'),
label: Yup.string().required('Gudang fisik wajib diisi!'),
}).nullable(),
warehouse_id: Yup.number()
.min(1, 'Gudang fisik wajib diisi!')
.required('Gudang fisik wajib diisi!'),
kandang: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
})
.nullable()
.optional(),
kandang_id: Yup.number().optional(),
value: Yup.number()
.min(1, 'Kandang wajib diisi!')
.required('Kandang wajib diisi!'),
label: Yup.string().required('Kandang wajib diisi!'),
}).nullable(),
kandang_id: Yup.number()
.min(1, 'Kandang wajib diisi!')
.required('Kandang wajib diisi!'),
product_warehouse: Yup.object({
value: Yup.number()
.min(1, 'Produk wajib diisi!')
@@ -7,7 +7,7 @@ import {
} from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
import { RefObject, useEffect, useMemo, useState } from 'react';
import { OptionType, useSelect } from '@/components/input/SelectInput';
import { Warehouse } from '@/types/api/master-data/warehouse';
import { Kandang } from '@/types/api/master-data/kandang';
import { WarehouseApi } from '@/services/api/master-data';
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
import { ProductWarehouseApi } from '@/services/api/inventory';
@@ -31,7 +31,6 @@ import {
import { Icon } from '@iconify/react';
import Dropdown from '@/components/Dropdown';
import { handleMarketingCalculation } from '@/lib/marketing-calculation';
import { getProductWarehouseOptionLabel } from '@/lib/product-warehouse';
const SalesOrderProductForm = ({
initialValues,
@@ -68,25 +67,7 @@ const SalesOrderProductForm = ({
? (Number(initialValues.total_price) -
initialSisaBerat * Number(initialValues.unit_price || 0)) /
Number(initialValues.total_peti)
: Number(initialValues?.unit_price || 0);
const isInitialTelurQty =
initialValues?.marketing_type?.value?.toLowerCase() === 'telur' &&
initialValues?.convertion_unit?.value?.toLowerCase() === 'qty';
const initialUnitPrice =
isInitialTelurQty &&
Number(initialValues?.total_price || 0) > 0 &&
Number(initialValues?.qty || 0) > 0
? Number(initialValues?.total_price) / Number(initialValues?.qty)
: initialValues?.unit_price || '';
const initialPricePerQty =
isInitialTelurQty &&
Number(initialValues?.total_price || 0) > 0 &&
Number(initialValues?.total_weight || 0) > 0
? Number(initialValues?.total_price) / Number(initialValues?.total_weight)
: (initialValues?.price_per_qty ?? null);
: 0;
const initialPriceSisaBerat =
initialValues?.total_price && initialValues?.total_peti
@@ -103,15 +84,11 @@ const SalesOrderProductForm = ({
enableReinitialize: true,
initialValues: {
vehicle_number: initialValues?.vehicle_number || '',
warehouse_id:
initialValues?.warehouse_id ?? initialValues?.kandang_id ?? undefined,
warehouse: initialValues?.warehouse ?? initialValues?.kandang ?? null,
kandang_id: initialValues?.kandang_id || undefined,
kandang: initialValues?.kandang || null,
product_warehouse: initialValues?.product_warehouse || null,
product_warehouse_data: initialValues?.product_warehouse_data || null,
product_warehouse_id: initialValues?.product_warehouse_id || undefined,
unit_price: initialUnitPrice,
unit_price: initialValues?.unit_price || '',
total_weight: initialValues?.total_weight || '',
qty: initialValues?.qty || '',
avg_weight: initialValues?.avg_weight || '',
@@ -125,7 +102,7 @@ const SalesOrderProductForm = ({
convertion_unit: initialValues?.convertion_unit || null,
marketing_type: initialValues?.marketing_type || null,
total_peti: initialValues?.total_peti ?? null,
price_per_qty: initialPricePerQty,
price_per_qty: initialValues?.price_per_qty ?? null,
sisa_berat: initialSisaBerat,
price_sisa_berat: initialPriceSisaBerat,
week: initialValues?.week ?? null,
@@ -155,11 +132,11 @@ const SalesOrderProductForm = ({
// ===== Options =====
const {
options: warehouseOptions,
isLoadingOptions: isLoadingWarehouseOptions,
setInputValue: setWarehouseSearchValue,
loadMore: loadMoreWarehouses,
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name');
options: kandangSourceOptions,
isLoadingOptions: isLoadingKandangSourceOptions,
setInputValue: setKandangInputValue,
loadMore: loadMoreKandang,
} = useSelect<Kandang>(WarehouseApi.basePath, 'id', 'name');
// Options Week dari minggu 1 - 22
// const optionsWeek = useMemo(() => {
@@ -170,6 +147,7 @@ const SalesOrderProductForm = ({
// }, []);
const {
options: warehouseSourceOptions,
rawData: warehouseSourceRawData,
isLoadingOptions: isLoadingWarehouseSourceOptions,
setInputValue: setWarehouseInputValue,
@@ -178,69 +156,32 @@ const SalesOrderProductForm = ({
ProductWarehouseApi.basePath,
'id',
'product.name',
'search',
'',
{
limit: '100',
available_only: 'true',
warehouse_id: formik.values.warehouse_id?.toString() ?? '',
warehouse_id: formik.values.kandang_id?.toString() ?? '',
type: formik.values.marketing_type?.value.toLocaleUpperCase() ?? '',
}
);
const productOptionsFiltered = useMemo(() => {
if (!isResponseSuccess(warehouseSourceRawData)) {
return initialValues?.product_warehouse
? [initialValues.product_warehouse]
: [];
}
const selectedProductIds = new Set(
exisitingValues
?.filter((item) => item.id !== initialValues?.id)
.map((item) => Number(item.product_warehouse_id))
.filter((item) => item > 0) ?? []
return warehouseSourceOptions.filter(
(product) =>
!exisitingValues
?.map((item) => item.product_warehouse_id)
.includes(product.value)
);
const options = warehouseSourceRawData.data
.filter((item: ProductWarehouse) => !selectedProductIds.has(item.id))
.map((item: ProductWarehouse) => ({
value: item.id,
label: getProductWarehouseOptionLabel(item),
}));
if (
initialValues?.product_warehouse &&
initialValues?.product_warehouse_id
) {
const exists = options.find(
(option) =>
Number(option.value) === Number(initialValues.product_warehouse_id)
);
if (!exists) {
options.push(initialValues.product_warehouse);
}
}
return options;
}, [warehouseSourceRawData, exisitingValues, initialValues]);
}, [warehouseSourceOptions, exisitingValues]);
// ===== Handler =====
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
const warehouse = (val as OptionType | null) ?? null;
formik.setFieldValue('warehouse', warehouse);
formik.setFieldValue('warehouse_id', warehouse?.value);
formik.setFieldValue('kandang', warehouse);
formik.setFieldValue('kandang_id', warehouse?.value);
const kandangChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('kandang', val as OptionType);
formik.setFieldValue('kandang_id', (val as OptionType)?.value);
formik.setFieldValue('product_warehouse_id', null);
formik.setFieldValue('product_warehouse', null);
formik.setFieldValue('product_warehouse_data', null);
formik.setFieldValue('qty', '');
};
const productWarehouseChangeHandler = (
val: OptionType | OptionType[] | null
) => {
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('product_warehouse', val as OptionType);
const newId = (val as OptionType)?.value;
formik.setFieldValue('product_warehouse_id', newId);
@@ -250,7 +191,6 @@ const SalesOrderProductForm = ({
(item: ProductWarehouse) => item.id === newId
);
setSelectedProductWarehouse(productWarehouse || null);
formik.setFieldValue('product_warehouse_data', productWarehouse || null);
formik.setFieldValue('qty', productWarehouse?.quantity);
formik.setFieldValue('uom', productWarehouse?.product?.uom?.name || '');
if (
@@ -264,8 +204,6 @@ const SalesOrderProductForm = ({
}
handleBlurField('qty');
} else {
setSelectedProductWarehouse(null);
formik.setFieldValue('product_warehouse_data', null);
formik.setFieldValue('qty', '');
formik.setFieldValue('uom', '');
formik.setFieldValue('week', null);
@@ -279,12 +217,9 @@ const SalesOrderProductForm = ({
formik.resetForm({
values: {
vehicle_number: '',
warehouse_id: undefined,
warehouse: null,
kandang_id: undefined,
kandang: null,
product_warehouse: null,
product_warehouse_data: null,
product_warehouse_id: undefined,
unit_price: '',
total_weight: '',
@@ -375,10 +310,6 @@ const SalesOrderProductForm = ({
handleBlurField('week');
}, [formik.values.week]);
useEffect(() => {
setSelectedProductWarehouse(initialValues?.product_warehouse_data || null);
}, [initialValues?.product_warehouse_data]);
return (
<>
<form
@@ -417,22 +348,22 @@ const SalesOrderProductForm = ({
errorMessage={formik.errors.vehicle_number}
/>
{/* Gudang Fisik */}
{/* Gudang */}
<SelectInputRadio
required
label='Gudang Fisik'
options={warehouseOptions}
isLoading={isLoadingWarehouseOptions}
value={formik.values.warehouse}
onChange={warehouseChangeHandler}
label='Gudang'
options={kandangSourceOptions}
isLoading={isLoadingKandangSourceOptions}
value={formik.values.kandang}
onChange={kandangChangeHandler}
isClearable
onInputChange={setWarehouseSearchValue}
onMenuScrollToBottom={loadMoreWarehouses}
onInputChange={setKandangInputValue}
onMenuScrollToBottom={loadMoreKandang}
isError={
formik.touched.warehouse_id && Boolean(formik.errors.warehouse_id)
formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
}
errorMessage={formik.errors.warehouse_id}
placeholder='Pilih Gudang Fisik'
errorMessage={formik.errors.kandang_id}
placeholder='Pilih Gudang'
/>
{/* Kategori */}
@@ -443,9 +374,8 @@ const SalesOrderProductForm = ({
value={formik.values.marketing_type}
onChange={(val) => {
formik.setFieldValue('marketing_type', val);
productWarehouseChangeHandler(null);
warehouseChangeHandler(null);
formik.setFieldValue('product_warehouse', null);
formik.setFieldValue('product_warehouse_data', null);
formik.setFieldValue('product_warehouse_id', null);
formik.setFieldValue('convertion_unit', null);
formik.setFieldValue('weight_per_convertion', null);
@@ -462,18 +392,18 @@ const SalesOrderProductForm = ({
options={productOptionsFiltered}
isLoading={isLoadingWarehouseSourceOptions}
value={formik.values.product_warehouse}
onChange={productWarehouseChangeHandler}
onChange={warehouseChangeHandler}
onInputChange={setWarehouseInputValue}
onMenuScrollToBottom={loadMoreWarehouse}
isClearable
placeholder={
formik.values.warehouse_id
formik.values.kandang_id
? productOptionsFiltered.length == 0
? 'Tidak ada produk yang tersedia'
: 'Pilih produk'
: 'Pilih Gudang Fisik Terlebih Dahulu'
: 'Pilih Kandang Terlebih Dahulu'
}
isDisabled={!formik.values.warehouse_id}
isDisabled={!formik.values.kandang_id}
isError={
formik.touched.product_warehouse_id &&
Boolean(formik.errors.product_warehouse_id)
@@ -541,7 +471,7 @@ const SalesOrderProductForm = ({
<input
type='radio'
checked={
formik.values.convertion_unit?.value.toLowerCase() ===
formik.values.convertion_unit?.value ===
option.value
}
onChange={() => null}
@@ -564,9 +494,7 @@ const SalesOrderProductForm = ({
} per ${formik.values.convertion_unit?.value}`}
value={formik.values.weight_per_convertion ?? ''}
onChange={(e) => {
const value = Number(e.target.value)
? Number(e.target.value)
: '';
const value = Number(e.target.value);
handleFieldChange('weight_per_convertion', value, () =>
setCurrentInput(e.target.name)
);
@@ -620,7 +548,7 @@ const SalesOrderProductForm = ({
placeholder='Masukan Total Peti'
endAdornment={
<div className='flex items-center gap-2'>
<span className='text-sm text-base-content/50'>Peti</span>
<span className='text-sm text-base-content/50'>Kg</span>
</div>
}
bottomLabel={`1 ${formik.values.convertion_unit?.value.toLowerCase()} = ${formik.values.weight_per_convertion ?? 0} Kg`}
@@ -670,9 +598,6 @@ const SalesOrderProductForm = ({
}
errorMessage={formik.errors.total_weight}
placeholder='Masukan Total Bobot'
disabled={
formik.values.convertion_unit?.value.toLowerCase() === 'peti'
}
/>
)}
@@ -740,34 +665,12 @@ const SalesOrderProductForm = ({
/>
)}
{/* Harga Satuan per Uom Produk Warehouse */}
{formik.values.convertion_unit?.value.toLowerCase() !== 'peti' &&
formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && (
<NumberInput
required
label={`Harga / ${formik.values.convertion_unit?.label.toLowerCase() !== 'qty' ? 'Kg' : 'Butir'} (Rp)`}
name='unit_price'
value={formik.values.unit_price}
onChange={(e) => {
const value = Number(e.target.value);
handleFieldChange('unit_price', value, () =>
setCurrentInput(e.target.name)
);
}}
isError={
formik.touched.unit_price && Boolean(formik.errors.unit_price)
}
errorMessage={formik.errors.unit_price}
placeholder='Masukan Harga Satuan...'
/>
)}
{/* Harga per kg untuk TELUR + QTY */}
{/* Harga per butir untuk TELUR + QTY */}
{formik.values.marketing_type?.value.toLowerCase() === 'telur' &&
formik.values.convertion_unit?.value.toLowerCase() === 'qty' && (
<NumberInput
required
label='Harga / Kg (Rp)'
label='Harga / Butir (Rp)'
name='price_per_qty'
value={formik.values.price_per_qty ?? undefined}
onChange={(e) => {
@@ -781,7 +684,29 @@ const SalesOrderProductForm = ({
Boolean(formik.errors.price_per_qty)
}
errorMessage={formik.errors.price_per_qty}
placeholder='Masukan Harga per Kg'
placeholder='Masukan Harga per Butir'
/>
)}
{/* Harga Satuan per Uom Produk Warehouse */}
{formik.values.convertion_unit?.value.toLowerCase() !== 'peti' &&
formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && (
<NumberInput
required
label={`Harga / ${formik.values.convertion_unit?.label !== 'qty' ? 'Kg' : (selectedProductWarehouse?.product?.uom?.name ?? 'Produk')} (Rp)`}
name='unit_price'
value={formik.values.unit_price}
onChange={(e) => {
const value = Number(e.target.value);
handleFieldChange('unit_price', value, () =>
setCurrentInput(e.target.name)
);
}}
isError={
formik.touched.unit_price && Boolean(formik.errors.unit_price)
}
errorMessage={formik.errors.unit_price}
placeholder='Masukan Harga Satuan'
/>
)}
@@ -5,9 +5,8 @@ import { Icon } from '@iconify/react';
import { useRef, useMemo } from 'react';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport';
import { Marketing } from '@/types/api/marketing/marketing';
import { Marketing, BaseDelivery } from '@/types/api/marketing/marketing';
import { Warehouse } from '@/types/api/master-data/warehouse';
import { DeliveryProductToFieldValues } from '@/components/pages/marketing/form/MarketingForm.schema';
type DeliveryOrderProductTableProps = {
data: DeliveryOrderProductFormValues[];
@@ -56,17 +55,14 @@ const DeliveryOrderProductTable = ({
const deliveryItems = useMemo(() => {
if (!hasDeliveryOrder) return [];
return (
marketing?.delivery_order?.flatMap((doItem) =>
DeliveryProductToFieldValues(marketing?.sales_order, doItem).map(
(delivery) => ({
...delivery,
do_number: doItem.do_number,
delivery_date: doItem.delivery_date,
warehouse: doItem.warehouse,
})
)
doItem.deliveries.map((delivery) => ({
...delivery,
do_number: doItem.do_number,
delivery_date: doItem.delivery_date,
warehouse: doItem.warehouse,
}))
) ?? []
);
}, [marketing?.delivery_order, hasDeliveryOrder]);
@@ -85,7 +81,7 @@ const DeliveryOrderProductTable = ({
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
<div>Value</div>
{/* {formType !== 'success' &&
{formType !== 'success' &&
(formType === 'add_delivery' ||
formType === 'edit_delivery' ||
formType === 'detail') && (
@@ -102,16 +98,15 @@ const DeliveryOrderProductTable = ({
<Icon icon='heroicons:pencil' width={20} height={20} />
</Button>
</div>
)} */}
)}
</div>
</th>
</tr>
<>
<tr>
<td className='text-sm px-4 py-3'>Gudang Fisik</td>
<td className='text-sm px-4 py-3'>Gudang</td>
<td className='text-sm px-4 py-3'>
{doItem?.warehouse?.name ||
item.marketing_product?.warehouse?.label ||
item.marketing_product?.product_warehouse_data?.warehouse?.name}
</td>
</tr>
@@ -141,15 +136,12 @@ const DeliveryOrderProductTable = ({
<tr>
<td className='text-sm px-4 py-3'>Total Bobot</td>
<td className='text-sm px-4 py-3'>
{formatNumber(Number(item.total_weight))} Kg
{formatNumber(Number(item.total_weight))}
</td>
</tr>
)}
<tr>
<td className='text-sm px-4 py-3'>
Total Harga Satuan
{item.convertion_unit?.label.toLowerCase() === 'peti' && ' (Kg)'}
</td>
<td className='text-sm px-4 py-3'>Total Harga Satuan</td>
<td className='text-sm px-4 py-3'>
{formatCurrency(parseFloat(item.unit_price as string))}
</td>
@@ -219,7 +211,7 @@ const DeliveryOrderProductTable = ({
};
const renderDeliveryOrderContent = (
item: DeliveryOrderProductFormValues & {
item: BaseDelivery & {
do_number: string;
delivery_date: string;
warehouse: Warehouse;
@@ -238,43 +230,25 @@ const DeliveryOrderProductTable = ({
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
<div>Value</div>
{formType !== 'success' &&
(formType === 'add_delivery' ||
formType === 'edit_delivery' ||
formType === 'detail') && (
<div className='flex flex-row gap-1.5 items-center'>
<Button
type='button'
variant='ghost'
color='none'
onClick={() => {
onEditRef.current(item.id as number, item);
}}
className='p-0 hover:text-base-content'
>
<Icon icon='heroicons:pencil' width={20} height={20} />
</Button>
</div>
)}
</div>
</th>
</tr>
<>
<tr>
<td className='text-sm px-4 py-3'>Gudang Fisik</td>
<td className='text-sm px-4 py-3'>Gudang</td>
<td className='text-sm px-4 py-3'>{item.warehouse?.name}</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Produk</td>
<td className='text-sm px-4 py-3'>
{item.marketing_product?.product_warehouse_data?.product.name}
{item.product_warehouse?.product?.name}
</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Qty</td>
<td className='text-sm px-4 py-3'>
{item.qty
? `${formatNumber(Number(item.qty))} ${item.marketing_product?.product_warehouse_data?.product.uom.name ?? ''}`
? `${formatNumber(item.qty)} ${item.product_warehouse?.product?.uom?.name ?? ''}`
: '-'}
</td>
</tr>
@@ -297,13 +271,13 @@ const DeliveryOrderProductTable = ({
<tr>
<td className='text-sm px-4 py-3'>Total Harga Satuan</td>
<td className='text-sm px-4 py-3'>
{formatCurrency(Number(item.unit_price))}
{formatCurrency(item.unit_price)}
</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Total Penjualan</td>
<td className='text-sm px-4 py-3'>
{formatCurrency(Number(item.total_price))}
{formatCurrency(item.total_price)}
</td>
</tr>
</>
@@ -359,9 +333,7 @@ const DeliveryOrderProductTable = ({
<div className='size-full flex flex-col relative overflow-x-hidden gap-3'>
{hasDeliveryOrder
? deliveryItems.map((item, index) => (
<div
key={`do-table-${item.marketing_product?.product_warehouse?.value}-${index}`}
>
<div key={`do-table-${item.product_warehouse?.id}-${index}`}>
{formType === 'success' ? (
<div className='rounded-lg border border-tools-table-outline border-base-content/5'>
<table
@@ -377,11 +349,8 @@ const DeliveryOrderProductTable = ({
</div>
) : (
<Card
key={`do-table-${item.marketing_product?.product_warehouse?.value}-${index}`}
title={
item.marketing_product?.product_warehouse_data?.product
.name || 'Produk'
}
key={`do-table-${item.product_warehouse?.id}-${index}`}
title={item.product_warehouse?.product?.name || 'Produk'}
collapsible={true}
defaultCollapsed={false}
variant='bordered'
@@ -73,10 +73,8 @@ const SalesOrderProductTable = ({
<td className='text-sm px-4 py-3'>{item.vehicle_number}</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Gudang Fisik</td>
<td className='text-sm px-4 py-3'>
{item.warehouse?.label ?? item.kandang?.label}
</td>
<td className='text-sm px-4 py-3'>Gudang</td>
<td className='text-sm px-4 py-3'>{item.kandang?.label}</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Kategori</td>
@@ -137,22 +135,8 @@ const SalesOrderProductTable = ({
{`${formatNumber(parseFloat(item.qty as string))} ${item.uom || ''}`}
</td>
</tr>
{item.convertion_unit?.value.toLowerCase() === 'peti' && (
<tr>
<td className='text-sm px-4 py-3'>Harga Satuan Per Peti</td>
<td className='text-sm px-4 py-3'>
{formatCurrency(
parseFloat(item.unit_price as string) *
parseFloat(String(item.weight_per_convertion))
)}
</td>
</tr>
)}
<tr>
<td className='text-sm px-4 py-3'>
Harga Satuan
{item.convertion_unit?.value.toLowerCase() === 'peti' && ' (Kg)'}
</td>
<td className='text-sm px-4 py-3'>Harga Satuan</td>
<td className='text-sm px-4 py-3'>
{formatCurrency(parseFloat(item.unit_price as string))}
</td>
@@ -101,8 +101,8 @@ const PDFDocument = ({
<View style={pdfStyles.header}>
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
<Text style={pdfStyles.address}>
Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten
Bandung Barat, Jawa Barat 40514
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
Cipedes, Kec. Sukajadi, Kota Bandung 40162
</Text>
<View style={pdfStyles.divider} />
</View>
@@ -87,8 +87,8 @@ const PDFDocument = ({ data }: { data: Marketing }) => {
<View style={pdfStyles.header}>
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
<Text style={pdfStyles.address}>
Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten
Bandung Barat, Jawa Barat 40514
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
Cipedes, Kec. Sukajadi, Kota Bandung 40162
</Text>
<View style={pdfStyles.divider} />
</View>
@@ -314,10 +314,6 @@ const KandangsTable = () => {
accessorFn: (row) => row.pic?.name ?? '-',
header: 'PIC',
},
{
accessorFn: (row) => row.kandang_group?.name ?? '-',
header: 'Kandang Group',
},
{
header: 'Aksi',
cell: (props: CellContext<Kandang, unknown>) => {
@@ -1,4 +1,3 @@
import { OptionType } from '@/components/input/SelectInput';
import * as Yup from 'yup';
type KandangFormSchemaType = {
@@ -20,7 +19,6 @@ type KandangFormSchemaType = {
}
| undefined
| null;
group?: OptionType;
};
export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
@@ -44,11 +42,6 @@ export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
group: Yup.object({
value: Yup.number().min(1).required('Kandang Grup wajib diisi!'),
label: Yup.string().required('Kandang Grup wajib diisi!'),
}).required('Kandang Grup wajib diisi!'),
});
export const UpdateKandangFormSchema = KandangFormSchema;
@@ -2,7 +2,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { getIn, useFormik } from 'formik';
import { useFormik } from 'formik';
import { toast } from 'react-hot-toast';
import { Icon } from '@iconify/react';
@@ -34,8 +34,6 @@ import NumberInput from '@/components/input/NumberInput';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import AlertErrorList from '@/components/helper/form/FormErrors';
import { User } from '@/types/api/api-general';
import { DailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
interface KandangFormProps {
type?: 'add' | 'edit' | 'detail';
@@ -98,12 +96,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
label: initialValues.pic.name,
}
: null,
group: initialValues?.kandang_group
? {
value: initialValues.kandang_group.id,
label: initialValues.kandang_group.name,
}
: undefined,
};
}, [initialValues]);
@@ -119,7 +111,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
location_id: values.locationId!,
capacity: values.capacity ? parseInt(values.capacity.toString()) : 0,
pic_id: values.picId!,
group_id: values.group?.value as number,
};
switch (type) {
@@ -171,23 +162,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
formik.setFieldValue('picId', (val as OptionType)?.value);
};
// Kandang Group
const {
setInputValue: setKandangGroupSelectInputValue,
options: kandangGroupOptions,
isLoadingOptions: isLoadingKandangGroupOptions,
loadMore: loadMoreKandangGroups,
} = useSelect<DailyChecklistKandang>(
DailyChecklistKandangApi.basePath,
'id',
'name'
);
const kandangGroupChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('group', true);
formik.setFieldValue('group', val);
};
const deleteKandangClickHandler = () => {
deleteModal.openModal();
};
@@ -295,24 +269,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
isDisabled={type === 'detail'}
isClearable
/>
<SelectInput
required
label='Kandang Group'
value={formik.values.group ?? undefined}
onChange={kandangGroupChangeHandler}
options={kandangGroupOptions}
onInputChange={setKandangGroupSelectInputValue}
onMenuScrollToBottom={loadMoreKandangGroups}
isLoading={isLoadingKandangGroupOptions}
isError={formik.touched.group && Boolean(formik.errors.group)}
errorMessage={
getIn(formik.errors.group, 'value') ??
(formik.errors.group as string)
}
isDisabled={type === 'detail'}
isClearable
/>
</div>
<div className='flex flex-row justify-between gap-2 flex-wrap'>
@@ -154,17 +154,17 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
sku: values.sku,
uom_id: values.uom_id,
product_category_id: values.product_category_id,
product_price: parseFloat(values.product_price.toString()) || 0,
product_price: parseInt(values.product_price.toString()) || 0,
selling_price: values.selling_price
? parseFloat(values.selling_price.toString()) || 0
? parseInt(values.selling_price.toString()) || 0
: undefined,
tax: values.tax ? parseFloat(values.tax.toString()) || 0 : undefined,
tax: values.tax ? parseInt(values.tax.toString()) || 0 : undefined,
expiry_period: values.expiry_period
? parseFloat(values.expiry_period.toString()) || 0
? parseInt(values.expiry_period.toString()) || 0
: undefined,
suppliers: values.suppliers.map((s) => ({
supplier_id: s.supplier?.value as number,
price: parseFloat(s.price.toString()) || 0,
price: parseInt(s.price.toString()) || 0,
})),
flag: values.flag,
sub_flags: values.sub_flags,
@@ -59,7 +59,8 @@ const RowOptionsMenu = ({
detailClickHandler: (id: number) => void;
deleteClickHandler: () => void;
}) => {
const showEditButton = props.row.original.approval?.step_number !== 2;
// TODO: change this to real condition
const showEditButton = true;
const showDeleteButton = showEditButton;
@@ -21,7 +21,6 @@ import SelectInput, { useSelect } from '@/components/input/SelectInput';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent';
import Tooltip from '@/components/Tooltip';
import { useFormik } from 'formik';
import { AreaApi } from '@/services/api/master-data';
import { LocationApi } from '@/services/api/master-data';
@@ -37,7 +36,6 @@ import {
import RecordingTableSkeleton from '@/components/pages/production/recording/skeleton/RecordingTableSkeleton';
import Table from '@/components/Table';
import { type Recording } from '@/types/api/production/recording';
import { getRecordingRestriction } from './recording-utils';
import { RecordingApi } from '@/services/api/production';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
@@ -48,7 +46,6 @@ import { useUiStore } from '@/stores/ui/ui.store';
import { usePathname } from 'next/navigation';
import { Color } from '@/types/theme';
import ButtonFilter from '@/components/helper/ButtonFilter';
import Dropdown from '@/components/Dropdown';
// ===== STATUS BADGE UTILITIES =====
const statusTextMap: Record<string, string> = {
@@ -108,75 +105,30 @@ const RowOptionsMenu = ({
};
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;
if (recording.project_flock?.project_flock_category === 'GROWING') {
if (recording.transfer_executed) {
return false;
}
return recording.population_can_change === true;
}
return true;
};
const getRecordingRestrictionInfo = (recording: Recording) => {
const isGrowingCategory =
recording.project_flock?.project_flock_category === 'GROWING';
const isGrowingLockedByLaying = isGrowingCategory && recording.is_laying;
if (isGrowingLockedByLaying) {
return {
canEditStock: false,
canEditDepletion: false,
canEditEgg: false,
isLocked: true,
lockReason:
'Recording Growing tidak dapat diubah karena sudah masuk fase laying dan dipakai pada recording laying',
};
}
const currentIsLaying =
recording.project_flock?.project_flock_category === 'LAYING';
return getRecordingRestriction(
recording.is_laying,
recording.is_transition,
currentIsLaying
);
};
const isApproved = isRecordingApproved(props.row.original);
const isRejected = isRecordingRejected(props.row.original);
const isEditable = isRecordingEditable(props.row.original);
const restrictionInfo = getRecordingRestrictionInfo(props.row.original);
return (
<div className='relative'>
<Tooltip
content={restrictionInfo.isLocked ? restrictionInfo.lockReason : ''}
position='top'
<PopoverButton
tabIndex={0}
variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
>
<PopoverButton
tabIndex={0}
variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
className={restrictionInfo.isLocked ? 'text-error' : ''}
>
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</PopoverButton>
</Tooltip>
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</PopoverButton>
<PopoverContent
id={popoverId}
@@ -353,9 +305,6 @@ const RecordingTable = () => {
const [isRejectLoading, setIsRejectLoading] = useState(false);
const [, setApprovalNotes] = useState('');
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
useState(false);
const singleDeleteModal = useModal();
const approveModal = useModal();
const rejectModal = useModal();
@@ -611,17 +560,12 @@ const RecordingTable = () => {
const singleDeleteHandler = async () => {
setIsDeleteLoading(true);
const response = await RecordingApi.delete(selectedRecording?.id as number);
await RecordingApi.delete(selectedRecording?.id as number);
refreshRecordings();
singleDeleteModal.closeModal();
toast.success('Successfully delete Recording!');
setIsDeleteLoading(false);
if (isResponseSuccess(response)) {
toast.success(response?.message || 'Successfully delete Recording!');
refreshRecordings();
} else {
toast.error(response?.message || 'Failed to delete Recording');
}
};
const approveHandler = async (notes: string) => {
@@ -690,14 +634,6 @@ const RecordingTable = () => {
});
}, [selectedRowIds, recordings, isRecordingApproved]);
const exportToExcelHandler = async () => {
setIsLoadingExportingToExcel(true);
await RecordingApi.exportToExcel(getTableFilterQueryString());
setIsLoadingExportingToExcel(false);
};
useEffect(() => {
if (isResponseSuccess(recordings) && recordings.data) {
const newSelection: Record<string, boolean> = {};
@@ -825,30 +761,11 @@ const RecordingTable = () => {
{
header: 'Kategori',
cell: (props) => {
const isTransition = props.row.original.is_transition;
const category =
props.row.original.project_flock?.project_flock_category ||
'GROWING';
props.row.original.project_flock?.project_flock_category;
if (!category) return '-';
const color = category === 'LAYING' ? 'info' : 'warning';
const isGrowingLocked =
category === 'GROWING' && props.row.original.is_laying;
return (
<div className='flex flex-col gap-1'>
<StatusBadge color={color} text={formatTitleCase(category)} />
{isTransition && (
<span className='text-xs text-warning font-medium'>
(Transisi)
</span>
)}
{isGrowingLocked && (
<span className='text-xs text-error font-medium'>
(Penguncian)
</span>
)}
</div>
);
return <StatusBadge color={color} text={formatTitleCase(category)} />;
},
},
{
@@ -1325,50 +1242,6 @@ const RecordingTable = () => {
onClick={handleFilterModalOpen}
className='px-3 py-2.5'
/>
<Dropdown
align='end'
direction='bottom'
trigger={
<Button
variant='outline'
color='none'
className={cn(
'px-3 py-2.5 rounded-lg font-semibold text-sm gap-1.5',
'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft'
)}
>
<Icon
width={20}
height={20}
icon='heroicons:cloud-arrow-down'
/>
Export
<div className='w-6.5 h-5 flex items-center justify-center border-l border-base-content/10'>
<Icon
width={14}
height={14}
icon='heroicons:chevron-down'
/>
</div>
</Button>
}
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
>
<Button
variant='ghost'
color='none'
onClick={exportToExcelHandler}
isLoading={isLoadingExportingToExcel}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:table-cells' width={20} height={20} />
Export to Excel
</Button>
</Dropdown>
</div>
</div>
@@ -29,12 +29,11 @@ type RecordingGrowingFormSchemaType = {
} | null;
project_flock_kandang_id: number;
stocks: {
product_warehouse_id: number;
qty: number | string;
product_warehouse_id?: number;
qty?: number | string;
}[];
depletions: {
product_warehouse_id?: number;
source_product_warehouse_id?: number;
qty?: number | string;
}[];
};
@@ -54,7 +53,6 @@ export type StockSchema = {
export type DepletionSchema = {
product_warehouse_id?: number;
source_product_warehouse_id?: number;
qty?: number | string;
};
@@ -71,7 +69,19 @@ const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
.typeError('Produk harus berupa angka!'),
qty: Yup.number()
.required('Jumlah penggunaan wajib diisi!')
.moreThan(0, 'Jumlah penggunaan harus lebih dari 0!')
.min(1, 'Jumlah penggunaan tidak boleh 0!')
.typeError('Jumlah penggunaan harus berupa angka!'),
});
const OptionalStockObjectSchema: Yup.ObjectSchema<{
product_warehouse_id?: number;
qty?: number | string;
}> = Yup.object({
product_warehouse_id: Yup.number()
.optional()
.typeError('Produk harus berupa angka!'),
qty: Yup.number()
.optional()
.typeError('Jumlah penggunaan harus berupa angka!'),
});
@@ -79,9 +89,6 @@ const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({
product_warehouse_id: Yup.number()
.optional()
.typeError('Depletions harus berupa angka!'),
source_product_warehouse_id: Yup.number()
.optional()
.typeError('Gudang sumber harus berupa angka!'),
qty: Yup.number()
.optional()
.typeError('Jumlah depletions harus berupa angka!'),
@@ -95,7 +102,9 @@ const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
weight: Yup.number().optional().typeError('Berat telur harus berupa angka!'),
});
export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSchemaType> =
export const RecordingGrowingFormSchema = (
isTransitioningToLaying = false
): Yup.ObjectSchema<RecordingGrowingFormSchemaType> =>
Yup.object({
record_date: Yup.string()
.required('Tanggal recording wajib diisi!')
@@ -155,20 +164,24 @@ export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSc
return true;
}
),
stocks: Yup.array()
.of(StockObjectSchema)
.min(1, 'Minimal harus ada 1 data stok!')
.required('Data stok wajib diisi!'),
stocks: isTransitioningToLaying
? Yup.array().of(OptionalStockObjectSchema).default([])
: Yup.array()
.of(StockObjectSchema)
.min(1, 'Minimal harus ada 1 data stok!')
.required('Data stok wajib diisi!'),
depletions: Yup.array().of(DepletionObjectSchema).default([]),
});
export const RecordingLayingFormSchema: Yup.ObjectSchema<RecordingLayingFormSchemaType> =
RecordingGrowingFormSchema.shape({
RecordingGrowingFormSchema().shape({
eggs: Yup.array().of(EggObjectSchema).default([]),
});
export const UpdateRecordingGrowingFormSchema =
RecordingGrowingFormSchema.shape({
export const UpdateRecordingGrowingFormSchema = (
isTransitioningToLaying = false
) =>
RecordingGrowingFormSchema(isTransitioningToLaying).shape({
location_id: Yup.number().nullable().optional(),
project_flock_id: Yup.number().nullable().optional(),
kandang_id: Yup.number().nullable().optional(),
@@ -198,10 +211,13 @@ export const UpdateRecordingLayingFormSchema = RecordingLayingFormSchema.shape({
.required('Project Flock Kandang wajib diisi!'),
});
export type RecordingGrowingFormValues = Yup.InferType<
type RecordingGrowingFormSchemaFn = ReturnType<
typeof RecordingGrowingFormSchema
>;
export type RecordingGrowingFormValues =
Yup.InferType<RecordingGrowingFormSchemaFn>;
export type RecordingLayingFormValues = Yup.InferType<
typeof RecordingLayingFormSchema
>;
@@ -264,7 +280,6 @@ export const getRecordingGrowingFormInitialValues = (
depletion: NonNullable<CreateGrowingRecordingPayload['depletions']>[0]
) => ({
product_warehouse_id: depletion.product_warehouse_id,
source_product_warehouse_id: depletion.source_product_warehouse_id,
qty: depletion.qty,
})
) ?? [
File diff suppressed because it is too large Load Diff
@@ -1,73 +0,0 @@
export type RecordingRestriction = {
canEditStock: boolean;
canEditDepletion: boolean;
canEditEgg: boolean;
isLocked: boolean;
lockReason?: string;
};
export const getRecordingRestriction = (
isLaying: boolean,
isTransition: boolean,
currentIsLaying?: boolean
): RecordingRestriction => {
if (isTransition && !isLaying) {
const isLayingKandangInTransition = currentIsLaying === true;
if (isLayingKandangInTransition) {
return {
canEditStock: false,
canEditDepletion: true,
canEditEgg: true,
isLocked: false,
lockReason: undefined,
};
} else {
return {
canEditStock: true,
canEditDepletion: false,
canEditEgg: false,
isLocked: false,
lockReason: undefined,
};
}
}
if (!isLaying && !isTransition && currentIsLaying) {
return {
canEditStock: false,
canEditDepletion: false,
canEditEgg: false,
isLocked: true,
lockReason:
'Recording Growing telah terkunci karena Project Flock sudah masuk fase Laying',
};
}
if (!isLaying && !isTransition) {
return {
canEditStock: true,
canEditDepletion: true,
canEditEgg: false,
isLocked: false,
lockReason: undefined,
};
}
if (isLaying && !isTransition) {
return {
canEditStock: true,
canEditDepletion: true,
canEditEgg: true,
isLocked: false,
lockReason: undefined,
};
}
return {
canEditStock: false,
canEditDepletion: false,
canEditEgg: false,
isLocked: true,
lockReason: 'Kondisi transisi tidak valid',
};
};
@@ -597,7 +597,6 @@ const UniformityForm = ({
onBlur={formik.handleBlur}
isError={formik.touched.date && Boolean(formik.errors.date)}
errorMessage={formik.errors.date as string}
disabled={isNextStep}
/>
<SelectInput
@@ -616,7 +615,6 @@ const UniformityForm = ({
errorMessage={formik.errors.location_id as string}
isClearable
className={{ wrapper: 'w-full' }}
isDisabled={isNextStep}
/>
<SelectInput
@@ -629,7 +627,7 @@ const UniformityForm = ({
onInputChange={setProjectFlockSearchValue}
isLoading={isLoadingProjectFlocks}
onMenuScrollToBottom={loadMoreProjectFlocks}
isDisabled={!formik.values.location_id || isNextStep}
isDisabled={!formik.values.location_id}
isError={
formik.touched.project_flock_id &&
Boolean(formik.errors.project_flock_id)
@@ -646,7 +644,7 @@ const UniformityForm = ({
value={formik.values.kandang}
onChange={handleKandangChange}
options={kandangOptions}
isDisabled={!formik.values.project_flock_id || isNextStep}
isDisabled={!formik.values.project_flock_id}
isError={
formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
}
@@ -1,201 +0,0 @@
'use client';
import { RefObject, useState, useEffect } from 'react';
import { useFormik } from 'formik';
import toast from 'react-hot-toast';
import { Icon } from '@iconify/react';
import Modal from '@/components/Modal';
import Button from '@/components/Button';
import DateInput from '@/components/input/DateInput';
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
import { OptionType, useSelect } from '@/components/input/SelectInput';
import { PurchaseFilter } from '@/types/api/purchase/purchase';
import { ProductCategory } from '@/types/api/master-data/product-category';
import { ProductCategoryApi } from '@/services/api/master-data';
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
interface PurchaseFilterModalProps {
ref: RefObject<HTMLDialogElement | null>;
onSubmit?: (values: PurchaseFilter) => void;
onReset?: () => void;
}
const PurchaseFilterModal = ({
ref,
onSubmit,
onReset,
}: PurchaseFilterModalProps) => {
const closeModalHandler = () => {
ref.current?.close();
};
// ===== DATE ERROR STATE =====
const [dateErrorShown, setDateErrorShown] = useState(false);
const [hasDateError, setHasDateError] = useState(false);
// ===== CLEANUP TOAST ON UNMOUNT =====
useEffect(() => {
return () => {
if (dateErrorShown) {
toast.dismiss();
}
};
}, [dateErrorShown]);
// ===== CLEANUP TOAST WHEN MODAL CLOSES =====
useEffect(() => {
const dialogElement = ref.current;
const handleModalClose = () => {
if (dateErrorShown) {
toast.dismiss();
setDateErrorShown(false);
}
};
dialogElement?.addEventListener('close', handleModalClose);
return () => {
dialogElement?.removeEventListener('close', handleModalClose);
};
}, [ref, dateErrorShown]);
const {
setInputValue: setProductCategoryInputValue,
options: productCategoryOptions,
isLoadingOptions: isLoadingProductCategoryOptions,
loadMore: loadMoreProductCategory,
} = useSelect<ProductCategory>(
ProductCategoryApi.basePath,
'id',
'name',
'search'
);
const formik = useFormik<{
poDate: string;
category: { label: string; value: number }[];
status: { label: string; value: string }[];
}>({
initialValues: {
poDate: '',
category: [],
status: [],
},
onSubmit: async (values) => {
const formattedValues = {
...values,
category: values.category.map((item) => String(item.value)),
status: values.status.map((item) => String(item.value)),
};
onSubmit?.(formattedValues);
closeModalHandler();
},
onReset: () => {
onReset?.();
closeModalHandler();
},
});
const productCategoryChangeHandler = (
val: OptionType | OptionType[] | null
) => {
formik.setFieldValue('category', val);
};
const statusChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('status', val);
};
return (
<Modal
ref={ref}
className={{
modalBox: 'p-0 rounded-xl',
}}
>
<form
onSubmit={formik.handleSubmit}
onReset={formik.handleReset}
className='w-full flex flex-col'
>
{/* Modal Header */}
<div className='p-4 flex items-center justify-between gap-2 border-b border-gray-300'>
<div className='flex items-center gap-2 text-primary'>
<Icon icon='heroicons:funnel' width={20} height={20} />
<h3 className='text-sm font-medium'>Filter Data</h3>
</div>
<Button
type='button'
variant='ghost'
color='none'
onClick={closeModalHandler}
className='p-0 text-base-content/50 hover:text-base-content'
>
<Icon icon='heroicons:x-mark' width={20} height={20} />
</Button>
</div>
{/* Modal Body */}
<div className='p-4 flex flex-col gap-1.5'>
<div className='flex flex-col'>
<DateInput
label='PO Date'
name='poDate'
placeholder='Pilih Tanggal'
value={formik.values.poDate}
onChange={formik.handleChange}
isNestedModal
/>
<SelectInputCheckbox
label='Kategori'
placeholder='Pilih Kategori'
value={formik.values.category}
onChange={productCategoryChangeHandler}
options={productCategoryOptions}
isLoading={isLoadingProductCategoryOptions}
onInputChange={setProductCategoryInputValue}
onMenuScrollToBottom={loadMoreProductCategory}
/>
<SelectInputCheckbox
label='Status'
placeholder='Status'
value={formik.values.status}
onChange={statusChangeHandler}
options={PURCHASE_ORDER_APPROVAL_LINE.map((item) => ({
label: item.step_name,
value: item.step_name,
}))}
/>
</div>
</div>
{/* Modal Footer */}
<div className='p-4 flex justify-between gap-4 border-t border-gray-300 bg-gray-100'>
<Button
type='reset'
variant='ghost'
color='none'
className='p-3 rounded-lg text-base-content/65'
>
Reset Filter
</Button>
<Button
type='submit'
className='p-3 rounded-lg w-fit sm:w-full max-w-40 text-base-100 text-sm'
>
Apply Filter
</Button>
</div>
</form>
</Modal>
);
};
export default PurchaseFilterModal;
@@ -14,7 +14,6 @@ import useSWRInfinite from 'swr/infinite';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import toast from 'react-hot-toast';
import Link from 'next/link';
import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
@@ -26,19 +25,18 @@ import PopoverContent from '@/components/popover/PopoverContent';
import RequirePermission from '@/components/helper/RequirePermission';
import StatusBadge from '@/components/helper/StatusBadge';
import PurchaseTableSkeleton from '@/components/pages/purchase/skeleton/PurchaseTableSkeleton';
import ButtonFilter from '@/components/helper/ButtonFilter';
import PurchaseFilterModal from '@/components/pages/purchase/PurchaseFilterModal';
import { cn, formatDate } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { BaseApiResponse } from '@/types/api/api-general';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { Purchase, PurchaseFilter } from '@/types/api/purchase/purchase';
import { Purchase } from '@/types/api/purchase/purchase';
import { PurchaseApi } from '@/services/api/purchase';
import { ExpenseApi } from '@/services/api/expense';
import { Expense } from '@/types/api/expense';
import { Color } from '@/types/theme';
import Link from 'next/link';
// ===== STATUS BADGE UTILITIES =====
const statusTextMap: Record<string, string> = {
@@ -167,21 +165,14 @@ const PurchaseTable = () => {
} = useTableFilter({
initial: {
search: '',
po_date: '',
approval_status: '',
product_category_id: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
po_date: 'po_date',
approval_status: 'approval_status',
product_category_id: 'product_category_id',
},
});
// ===== MODAL HOOKS =====
const filterModal = useModal();
const deleteModal = useModal();
// ===== API DATA FETCHING =====
@@ -419,17 +410,13 @@ const PurchaseTable = () => {
[updateFilter, setSearchValue]
);
const filterSubmitHandler = (values: PurchaseFilter) => {
updateFilter('po_date', values.poDate);
updateFilter('product_category_id', values.category.join(','));
updateFilter('approval_status', values.status.join(','));
};
const filterResetHandler = () => {
updateFilter('po_date', '');
updateFilter('product_category_id', '');
updateFilter('approval_status', '');
};
// const pageSizeChangeHandler = useCallback(
// (val: OptionType | OptionType[] | null) => {
// const newVal = val as OptionType;
// setPageSize(newVal.value as number);
// },
// [setPageSize]
// );
return (
<>
@@ -468,20 +455,6 @@ const PurchaseTable = () => {
'placeholder:font-semibold placeholder:text-base-content/50',
}}
/>
<ButtonFilter
values={tableFilterState}
excludeFields={[
'page',
'pageSize',
'search',
'filter_by',
'sort_by',
]}
fieldGroups={[['startDate', 'endDate']]}
onClick={filterModal.openModal}
className='px-3 py-2.5'
/>
</div>
</div>
@@ -540,13 +513,6 @@ const PurchaseTable = () => {
</div>
{/* ===== MODAL COMPONENTS ===== */}
<PurchaseFilterModal
ref={filterModal.ref}
onSubmit={filterSubmitHandler}
onReset={filterResetHandler}
/>
<ConfirmationModal
ref={deleteModal.ref}
type='error'
@@ -308,7 +308,7 @@ const PurchaseOrderAcceptApprovalForm = ({
}
: null,
expedition_vendor_id: expeditionVendorId,
received_qty: item.sub_qty || '',
received_qty: item.total_qty || '',
transport_per_item: item.transport_per_item || '',
};
});
@@ -367,9 +367,6 @@ const PurchaseOrderAcceptApprovalForm = ({
);
} else {
formik.setFieldValue(`items.${idx}.expedition_vendor_id`, null);
formik.setFieldValue(`items.${idx}.transport_per_item`, null);
formik.setFieldValue(`items.${idx}.vehicle_number`, null);
}
};
@@ -556,7 +553,6 @@ const PurchaseOrderAcceptApprovalForm = ({
)
}
onBlur={formik.handleBlur}
disabled={!Boolean(formItem?.expedition_vendor)}
isError={
isRepeaterInputError(idx, 'vehicle_number').isError
}
@@ -661,7 +657,6 @@ const PurchaseOrderAcceptApprovalForm = ({
thousandSeparator=','
decimalSeparator='.'
inputPrefix={'Rp'}
disabled={!Boolean(formItem?.expedition_vendor)}
isError={
isRepeaterInputError(idx, 'transport_per_item')
.isError
@@ -185,12 +185,7 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApp
.typeError('No. Surat jalan wajib diisi!'),
vehicle_number: Yup.string()
.nullable()
.when('expedition_vendor', {
is: (expeditionVendor?: { value?: number; label?: string } | null) =>
Boolean(expeditionVendor?.value),
then: (schema) => schema.required('Nomor kendaraan wajib diisi!'),
otherwise: (schema) => schema.optional(),
})
.optional()
.typeError('Nomor kendaraan harus berupa plat nomor!'),
expedition_vendor: Yup.object({
value: Yup.number().min(1).required(),
@@ -218,13 +213,7 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApp
.typeError('Jumlah diterima harus berupa angka!'),
transport_per_item: Yup.mixed<string | number>()
.nullable()
.when('expedition_vendor', {
is: (expeditionVendor?: { value?: number; label?: string } | null) =>
Boolean(expeditionVendor?.value),
then: (schema) =>
schema.required('Biaya transport per item wajib diisi!'),
otherwise: (schema) => schema.optional(),
})
.optional()
.test(
'is-valid-transport-per-item',
'Biaya transport per item harus berupa angka lebih dari atau sama dengan 0!',
@@ -34,7 +34,7 @@ const pdfStyles = StyleSheet.create({
marginBottom: 20,
},
logo: {
width: 30,
width: 120,
height: 30,
marginBottom: 8,
},
@@ -265,7 +265,7 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
<View style={pdfStyles.header}>
{/* eslint-disable-next-line jsx-a11y/alt-text */}
<Image
src='/assets/img/lti-logo.png'
src={'https://placehold.co/120x30/png'}
style={pdfStyles.logo}
id={'mbu-logo'}
/>
@@ -273,8 +273,8 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
PT LUMBUNG TELUR INDONESIA
</Text>
<Text style={pdfStyles.address}>
Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten
Bandung Barat, Jawa Barat 40514
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
Cipedes, Kec. Sukajadi, Kota Bandung 40162
</Text>
<View style={pdfStyles.divider} />
</View>
@@ -47,7 +47,7 @@ export const generateReportExpensePDF = async (
doc.setFontSize(7);
doc.setTextColor(102, 102, 102);
doc.text(
'Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten Bandung Barat, Jawa Barat 40514',
'SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. Cipedes, Kec. Sukajadi, Kota Bandung 40162',
marginX,
25
);
@@ -33,18 +33,18 @@ import { generateReportExpensePDF } from '../export/ReportExpenseExportPDF';
import { generateReportExpenseExcel } from '../export/ReportExpenseExportXLSX';
import toast from 'react-hot-toast';
import {
KandangApi,
LocationApi,
NonstockApi,
SupplierApi,
} from '@/services/api/master-data';
import { Supplier } from '@/types/api/master-data/supplier';
import { Kandang } from '@/types/api/master-data/kandang';
import { Nonstock } from '@/types/api/master-data/nonstock';
import { ColumnDef } from '@tanstack/react-table';
import { httpClient } from '@/services/http/client';
import { BaseApiResponse } from '@/types/api/api-general';
import ButtonFilter from '@/components/helper/ButtonFilter';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
import { ProjectFlockKandangApi } from '@/services/api/production/project-flock-kandang';
interface ReportExpenseTabProps {
tabId: string;
@@ -67,6 +67,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
// ===== SUBMISSION STATE =====
const [isSubmitted, setIsSubmitted] = useState(false);
const [filterParams, setFilterParams] = useState<FilterParams>({});
// ===== PAGINATION STATE =====
@@ -116,10 +117,12 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
: undefined,
});
filterModal.closeModal();
setIsSubmitted(true);
setPage(1);
},
onReset: () => {
setFilterParams({});
setIsSubmitted(false);
setPage(1);
filterModal.closeModal();
},
@@ -136,7 +139,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
options: locationOptions,
isLoadingOptions: isLoadingLocations,
loadMore: loadMoreLocations,
} = useSelect<Location>(LocationApi.basePath, 'id', 'name', 'search');
} = useSelect<Kandang>(LocationApi.basePath, 'id', 'name', 'search');
const {
setInputValue: setSupplierInputValue,
@@ -146,14 +149,14 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search');
const {
setInputValue: setProjectFlockKandangInputValue,
options: projectFlockKandangOptions,
isLoadingOptions: isLoadingProjectFlockKandangs,
loadMore: loadMoreProjectFlockKandangs,
} = useSelect<ProjectFlockKandang>(
ProjectFlockKandangApi.basePath,
setInputValue: setKandangInputValue,
options: kandangOptions,
isLoadingOptions: isLoadingKandangs,
loadMore: loadMoreKandangs,
} = useSelect<Kandang>(
KandangApi.basePath,
'id',
'name_with_period',
'name',
'search',
formik.values.location_id?.value
? { location_id: String(formik.values.location_id.value) }
@@ -191,25 +194,27 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
// ===== DATA FETCHING =====
const { data: reportExpenseResponse, isLoading } = useSWR(
() => {
const params = new URLSearchParams();
if (filterParams.location_id)
params.append('location_id', filterParams.location_id);
if (filterParams.supplier_id)
params.append('supplier_id', filterParams.supplier_id);
if (filterParams.kandang_id)
params.append('project_flock_kandang_id', filterParams.kandang_id);
if (filterParams.nonstock_id)
params.append('nonstock_id', filterParams.nonstock_id);
if (filterParams.realization_date)
params.append('realization_date', filterParams.realization_date);
if (filterParams.category)
params.append('category', filterParams.category);
params.append('page', String(page));
params.append('limit', String(pageSize));
isSubmitted
? () => {
const params = new URLSearchParams();
if (filterParams.location_id)
params.append('location_id', filterParams.location_id);
if (filterParams.supplier_id)
params.append('supplier_id', filterParams.supplier_id);
if (filterParams.kandang_id)
params.append('kandang_id', filterParams.kandang_id);
if (filterParams.nonstock_id)
params.append('nonstock_id', filterParams.nonstock_id);
if (filterParams.realization_date)
params.append('realization_date', filterParams.realization_date);
if (filterParams.category)
params.append('category', filterParams.category);
params.append('page', String(page));
params.append('limit', String(pageSize));
return [`${ReportExpenseApi.basePath}?${params.toString()}`];
},
return [`${ReportExpenseApi.basePath}?${params.toString()}`];
}
: null,
([url]: string[]) => httpClient<BaseApiResponse<ReportExpense[]>>(url)
);
@@ -524,13 +529,25 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
<>
{TabActionsElement}
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{isLoading && (
{!isSubmitted ? (
<ReportExpenseSkeleton
columns={columns}
icon={
<Icon
icon='heroicons:funnel'
className='text-white'
width={20}
height={20}
/>
}
title='No Filters Selected'
subtitle='Please choose filters to narrow down your results and make your search easier.'
/>
) : isLoading ? (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
)}
{!isLoading && (!data || data.length === 0) && (
) : !data || data.length === 0 ? (
<ReportExpenseSkeleton
columns={columns}
icon={
@@ -544,9 +561,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
title='Data Not Yet Available'
subtitle='Please change your filters to get the data.'
/>
)}
{!isLoading && data.length > 0 && (
) : (
<>
<Table
data={data}
@@ -643,14 +658,14 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
<SelectInput
label='Kandang'
placeholder='Pilih Kandang'
options={projectFlockKandangOptions}
isLoading={isLoadingProjectFlockKandangs}
options={kandangOptions}
isLoading={isLoadingKandangs}
value={kandangValue}
onChange={(val) => {
formik.setFieldValue('kandang_id', val);
}}
onInputChange={setProjectFlockKandangInputValue}
onMenuScrollToBottom={loadMoreProjectFlockKandangs}
onInputChange={setKandangInputValue}
onMenuScrollToBottom={loadMoreKandangs}
isClearable
isDisabled={!formik.values.location_id}
className={{ wrapper: 'w-full' }}
@@ -61,6 +61,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
const [pageSize] = useState(10);
// ===== SUBMISSION STATE =====
const [isSubmitted, setIsSubmitted] = useState(false);
const [filterParams, setFilterParams] = useState<FilterParams>({});
const [dateErrorShown, setDateErrorShown] = useState(false);
const [hasDateError, setHasDateError] = useState(false);
@@ -101,11 +102,13 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
filter_by: values.filter_by || undefined,
});
filterModal.closeModal();
setIsSubmitted(true);
setCurrentPage(1);
setSubmitting(false);
},
onReset: () => {
setFilterParams({});
setIsSubmitted(false);
setCurrentPage(1);
setHasDateError(false);
if (dateErrorShown) {
@@ -215,21 +218,23 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
// ===== DATA FETCHING =====
const { data: customerPayment, isLoading } = useSWR(
() => {
const params = {
customer_ids: filterParams.customer_ids,
filter_by: filterParams.filter_by as
| 'trans_date'
| 'realization_date'
| undefined,
start_date: filterParams.start_date,
end_date: filterParams.end_date,
page: currentPage,
limit: pageSize,
};
isSubmitted
? () => {
const params = {
customer_ids: filterParams.customer_ids,
filter_by: filterParams.filter_by as
| 'trans_date'
| 'realization_date'
| undefined,
start_date: filterParams.start_date,
end_date: filterParams.end_date,
page: currentPage,
limit: pageSize,
};
return ['customer-payment-report', params];
},
return ['customer-payment-report', params];
}
: null,
([, params]) =>
FinanceApi.getCustomerPaymentReport(
params.customer_ids,
@@ -695,13 +700,25 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
<>
{TabActionsElement}
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{isLoading && (
{!isSubmitted ? (
<CustomerSupplierSkeleton
columns={getTableColumns({} as CustomerPaymentSummary)}
icon={
<Icon
icon='heroicons:funnel'
className='text-white'
width={20}
height={20}
/>
}
title='No Filters Selected'
subtitle='Please choose filters to narrow down your results and make your search easier.'
/>
) : isLoading ? (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
)}
{!isLoading && data.length === 0 && (
) : data.length === 0 ? (
<CustomerSupplierSkeleton
columns={getTableColumns({} as CustomerPaymentSummary)}
icon={
@@ -715,10 +732,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
title='Data Not Yet Available'
subtitle='Please change your filters to get the data.'
/>
)}
{!isLoading &&
data.length > 0 &&
) : (
data.map((customerReport) => {
const summary = customerReport.summary || {
total_qty: 0,
@@ -747,6 +761,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
}}
variant='bordered'
collapsible={true}
defaultCollapsed={true}
>
<Table
data={[
@@ -810,7 +825,8 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
/>
</Card>
);
})}
})
)}
</div>
{/* Filter Modal */}
@@ -85,6 +85,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
supplier_ids: undefined,
filter_by: undefined,
});
const [isSubmitted, setIsSubmitted] = useState(false);
// ===== DATE ERROR STATE =====
const [dateErrorShown, setDateErrorShown] = useState(false);
@@ -128,7 +129,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
filter_by: values.filterBy?.value?.toString() || undefined,
});
filterModal.closeModal();
// setIsSubmitted(true);
setIsSubmitted(true);
},
onReset: () => {
setFilterParams({
@@ -137,7 +138,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
supplier_ids: undefined,
filter_by: undefined,
});
// setIsSubmitted(false);
setIsSubmitted(false);
filterModal.closeModal();
},
});
@@ -149,16 +150,18 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
// ===== DATA FETCHING =====
const { data: debtSupplier, isLoading } = useSWR(
() => {
const params = {
supplier_ids: filterParams.supplier_ids,
filter_by: filterParams.filter_by,
start_date: filterParams.start_date,
end_date: filterParams.end_date,
};
isSubmitted
? () => {
const params = {
supplier_ids: filterParams.supplier_ids,
filter_by: filterParams.filter_by,
start_date: filterParams.start_date,
end_date: filterParams.end_date,
};
return ['debt-supplier-report', params];
},
return ['debt-supplier-report', params];
}
: null,
([, params]) =>
DebtSupplierApi.getDebtSupplierReport(
params.supplier_ids,
@@ -608,13 +611,25 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
<>
{TabActionsElement}
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{isLoading && (
{!isSubmitted ? (
<DebtSupplierSkeleton
columns={getTableColumns()}
icon={
<Icon
icon='heroicons:funnel'
className='text-white'
width={20}
height={20}
/>
}
title='No Filters Selected'
subtitle='Please choose filters to narrow down your results and make your search easier.'
/>
) : isLoading ? (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
)}
{!isLoading && data.length === 0 && (
) : data.length === 0 ? (
<DebtSupplierSkeleton
columns={getTableColumns()}
icon={
@@ -628,10 +643,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
title='Data Not Yet Available'
subtitle='Please change your filters to get the data.'
/>
)}
{!isLoading &&
data.length > 0 &&
) : (
data.map((supplierReport) => {
return (
<Card
@@ -646,6 +658,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
}}
variant='bordered'
collapsible={true}
defaultCollapsed={true}
>
<Table
data={[
@@ -716,7 +729,8 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
/>
</Card>
);
})}
})
)}
</div>
{/* Filter Modal */}
@@ -61,6 +61,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
// ===== SUBMISSION STATE =====
const [filterParams, setFilterParams] = useState<FilterParams>({});
const [isSubmitted, setIsSubmitted] = useState(false);
const [dateErrorShown, setDateErrorShown] = useState(false);
const [hasDateError, setHasDateError] = useState(false);
@@ -69,34 +70,24 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
const filterModal = useModal();
// ===== OPTIONS =====
const {
options: areaOptions,
isLoadingOptions: isLoadingAreas,
setInputValue: setAreaInputValue,
loadMore: loadMoreArea,
} = useSelect(AreaApi.basePath, 'id', 'name', 'search');
const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect(
AreaApi.basePath,
'id',
'name',
'search'
);
const {
options: supplierOptions,
isLoadingOptions: isLoadingSuppliers,
setInputValue: setSupplierInputValue,
loadMore: loadMoreSupplier,
} = useSelect(SupplierApi.basePath, 'id', 'name', 'search', {
category: 'SAPRONAK',
});
const { options: supplierOptions, isLoadingOptions: isLoadingSuppliers } =
useSelect(SupplierApi.basePath, 'id', 'name', 'search', {
category: 'SAPRONAK',
});
const {
options: productOptions,
isLoadingOptions: isLoadingProducts,
setInputValue: setProductInputValue,
loadMore: loadMoreProduct,
} = useSelect(ProductApi.basePath, 'id', 'name', 'search');
const { options: productOptions, isLoadingOptions: isLoadingProducts } =
useSelect(ProductApi.basePath, 'id', 'name', 'search');
const {
options: productCategoryOptions,
isLoadingOptions: isLoadingProductCategories,
setInputValue: setProductCategoryInputValue,
loadMore: loadMoreProductCategory,
} = useSelect(ProductCategoryApi.basePath, 'id', 'name', 'search');
const dataTypeOptions = useMemo(
@@ -140,11 +131,13 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
sort_by: values.sort_by || undefined,
});
filterModal.closeModal();
setIsSubmitted(true);
setCurrentPage(1);
setSubmitting(false);
},
onReset: () => {
setFilterParams({});
setIsSubmitted(false);
setCurrentPage(1);
setHasDateError(false);
if (dateErrorShown) {
@@ -268,22 +261,24 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
// ===== DATA FETCHING =====
const { data: purchasePerSupplier, isLoading } = useSWR(
() => {
const params = {
area_id: filterParams.area_id,
supplier_id: filterParams.supplier_id,
product_id: filterParams.product_id,
product_category_id: filterParams.product_category_id,
start_date: filterParams.start_date,
end_date: filterParams.end_date,
sort_by: filterParams.sort_by,
filter_by: filterParams.filter_by,
page: currentPage,
limit: pageSize,
};
isSubmitted
? () => {
const params = {
area_id: filterParams.area_id,
supplier_id: filterParams.supplier_id,
product_id: filterParams.product_id,
product_category_id: filterParams.product_category_id,
start_date: filterParams.start_date,
end_date: filterParams.end_date,
sort_by: filterParams.sort_by,
filter_by: filterParams.filter_by,
page: currentPage,
limit: pageSize,
};
return ['logistic-purchase-report', params];
},
return ['logistic-purchase-report', params];
}
: null,
([, params]) =>
LogisticApi.getLogisticPurchasePerSupplierReport(
params.area_id,
@@ -731,7 +726,21 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
<>
{TabActionsElement}
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{isLoading && (
{!isSubmitted ? (
<PurchasePerSupplierSkeleton
columns={getTableColumns({} as LogisticPurchasePerSupplierSummary)}
icon={
<Icon
icon='heroicons:funnel'
className='text-white'
width={20}
height={20}
/>
}
title='No Filters Selected'
subtitle='Please choose filters to narrow down your results and make your search easier.'
/>
) : isLoading ? (
<PurchasePerSupplierSkeleton
columns={getTableColumns({} as LogisticPurchasePerSupplierSummary)}
icon={
@@ -745,9 +754,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
title='Memuat Data Pembelian Per Supplier'
subtitle='Silakan tunggu sebentar...'
/>
)}
{!isLoading && data.length === 0 && (
) : data.length === 0 ? (
<PurchasePerSupplierSkeleton
columns={getTableColumns({} as LogisticPurchasePerSupplierSummary)}
icon={
@@ -761,10 +768,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
title='Data Not Yet Available'
subtitle='Please change your filters to get the data.'
/>
)}
{!isLoading &&
data.length > 0 &&
) : (
data.map((supplierReport) => {
const summary = supplierReport.summary || {
total_qty: 0,
@@ -794,6 +798,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
}}
variant='bordered'
collapsible={true}
defaultCollapsed={true}
>
<Table
data={supplierReport.rows}
@@ -822,7 +827,8 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
/>
</Card>
);
})}
})
)}
</div>
{/* Filter Modal */}
@@ -901,8 +907,6 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
isLoading={isLoadingAreas}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setAreaInputValue}
onMenuScrollToBottom={loadMoreArea}
/>
{/* Supplier Filter */}
@@ -922,8 +926,6 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
isLoading={isLoadingSuppliers}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setSupplierInputValue}
onMenuScrollToBottom={loadMoreSupplier}
/>
{/* Product Filter */}
@@ -943,8 +945,6 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
isLoading={isLoadingProducts}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setProductInputValue}
onMenuScrollToBottom={loadMoreProduct}
/>
{/* Product Category Filter */}
@@ -964,8 +964,6 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
isLoading={isLoadingProductCategories}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setProductCategoryInputValue}
onMenuScrollToBottom={loadMoreProductCategory}
/>
{/* Filter By Type */}
@@ -81,7 +81,7 @@ const getTableColumns = (
},
{
key: 'warehouse',
header: 'Gudang Fisik',
header: 'Gudang',
flex: 1.2,
align: 'left',
cell: ({ row }) => row.warehouse?.name ?? '-',
@@ -30,7 +30,7 @@ export const generateDailyMarketingExcel = async (
{ header: 'Tanggal Jual', key: 'soDate', width: 15 },
{ header: 'Tanggal Realisasi', key: 'realizationDate', width: 18 },
{ header: 'Aging', key: 'aging', width: 10 },
{ header: 'Gudang Fisik', key: 'warehouse', width: 25 },
{ header: 'Gudang', key: 'warehouse', width: 25 },
{ header: 'Pelanggan', key: 'customer', width: 25 },
{ header: 'No. DO', key: 'doNumber', width: 15 },
{ header: 'Sales/Marketing', key: 'sales', width: 20 },
@@ -97,7 +97,7 @@ export const generateDailyMarketingExcel = async (
});
}
worksheet.columns.forEach((column: { width?: number }) => {
worksheet.columns.forEach((column) => {
if (column.width && column.width < 10) {
column.width = 10;
}
@@ -70,6 +70,9 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
// ===== SUBMISSION STATE =====
const [isSubmitted, setIsSubmitted] = useState(false);
// ===== SEARCH STATE =====
const [searchValue, setSearchValue] = useState<string>('');
@@ -85,33 +88,21 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
const filterModal = useModal();
// ===== OPTIONS =====
const {
options: areaOptions,
isLoadingOptions: isLoadingAreas,
setInputValue: setAreaInputValue,
loadMore: loadMoreArea,
} = useSelect(AreaApi.basePath, 'id', 'name', 'search');
const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect(
AreaApi.basePath,
'id',
'name',
'search'
);
const {
options: locationOptions,
isLoadingOptions: isLoadingLocations,
setInputValue: setLocationInputValue,
loadMore: loadMoreLocation,
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
const { options: locationOptions, isLoadingOptions: isLoadingLocations } =
useSelect(LocationApi.basePath, 'id', 'name', 'search');
const {
options: warehouseOptions,
isLoadingOptions: isLoadingWarehouses,
setInputValue: setWarehouseInputValue,
loadMore: loadMoreWarehouse,
} = useSelect(WarehouseApi.basePath, 'id', 'name', 'search');
const { options: warehouseOptions, isLoadingOptions: isLoadingWarehouses } =
useSelect(WarehouseApi.basePath, 'id', 'name', 'search');
const {
options: customerOptions,
isLoadingOptions: isLoadingCustomers,
setInputValue: setCustomerInputValue,
loadMore: loadMoreCustomer,
} = useSelect(CustomerApi.basePath, 'id', 'name', 'search');
const { options: customerOptions, isLoadingOptions: isLoadingCustomers } =
useSelect(CustomerApi.basePath, 'id', 'name', 'search');
// ===== FORMIK SETUP =====
const formik = useFormik<DailyMarketingReportFilterType>({
@@ -141,10 +132,12 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
sort_by: values.sort_by || undefined,
});
filterModal.closeModal();
setIsSubmitted(true);
setSubmitting(false);
},
onReset: () => {
setFilterParams({});
setIsSubmitted(false);
filterModal.closeModal();
},
});
@@ -218,28 +211,31 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
// ===== DATA FETCHING =====
const { data: dailyMarketings, isLoading } = useSWR(
() => {
const params = new URLSearchParams();
isSubmitted
? () => {
const params = new URLSearchParams();
if (searchValue) params.set('search', searchValue);
if (filterParams.area_id) params.set('area_id', filterParams.area_id);
if (filterParams.location_id)
params.set('location_id', filterParams.location_id);
if (filterParams.warehouse_id)
params.set('warehouse_id', filterParams.warehouse_id);
if (filterParams.customer_id)
params.set('customer_id', filterParams.customer_id);
if (filterParams.start_date)
params.set('start_date', filterParams.start_date);
if (filterParams.end_date) params.set('end_date', filterParams.end_date);
if (filterParams.filter_by)
params.set('filter_by', filterParams.filter_by);
if (filterParams.marketing_type)
params.set('marketing_type', filterParams.marketing_type);
if (filterParams.sort_by) params.set('sort_by', filterParams.sort_by);
if (searchValue) params.set('search', searchValue);
if (filterParams.area_id) params.set('area_id', filterParams.area_id);
if (filterParams.location_id)
params.set('location_id', filterParams.location_id);
if (filterParams.warehouse_id)
params.set('warehouse_id', filterParams.warehouse_id);
if (filterParams.customer_id)
params.set('customer_id', filterParams.customer_id);
if (filterParams.start_date)
params.set('start_date', filterParams.start_date);
if (filterParams.end_date)
params.set('end_date', filterParams.end_date);
if (filterParams.filter_by)
params.set('filter_by', filterParams.filter_by);
if (filterParams.marketing_type)
params.set('marketing_type', filterParams.marketing_type);
if (filterParams.sort_by) params.set('sort_by', filterParams.sort_by);
return ['daily-marketing-report', params.toString()];
},
return ['daily-marketing-report', params.toString()];
}
: null,
([, params]) =>
MarketingReportApi.getAllDailyMarketingFetcher(
`${MarketingReportApi.basePath}?${params}`
@@ -512,7 +508,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
},
{
id: 'warehouse',
header: 'Gudang Fisik',
header: 'Gudang',
accessorKey: 'warehouse',
cell: ({ row }) => row.original.warehouse.name,
footer: () => <div className='font-semibold text-gray-900'>-</div>,
@@ -652,7 +648,21 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
<>
{TabActionsElement}
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{isLoading && (
{!isSubmitted ? (
<DailyMarketingReportSkeleton
columns={getTableColumns()}
icon={
<Icon
icon='heroicons:funnel'
className='text-white'
width={20}
height={20}
/>
}
title='No Filters Selected'
subtitle='Please choose filters to narrow down your results and make your search easier.'
/>
) : isLoading ? (
<DailyMarketingReportSkeleton
columns={getTableColumns()}
icon={
@@ -666,9 +676,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
title='Memuat Data Penjualan Harian'
subtitle='Silakan tunggu sebentar...'
/>
)}
{!isLoading && data.length === 0 && (
) : data.length === 0 ? (
<DailyMarketingReportSkeleton
columns={getTableColumns()}
icon={
@@ -682,9 +690,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
title='Data Not Yet Available'
subtitle='Please change your filters to get the data.'
/>
)}
{!isLoading && data.length > 0 && (
) : (
<Table
data={data}
columns={getTableColumns()}
@@ -831,8 +837,6 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
}}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setAreaInputValue}
onMenuScrollToBottom={loadMoreArea}
/>
{/* Location Filter */}
@@ -850,14 +854,12 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
}}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setLocationInputValue}
onMenuScrollToBottom={loadMoreLocation}
/>
{/* Warehouse Filter */}
<SelectInput
label='Gudang Fisik'
placeholder='Pilih Gudang Fisik'
label='Gudang'
placeholder='Pilih Gudang'
options={warehouseOptions}
isLoading={isLoadingWarehouses}
value={warehouseValue}
@@ -869,8 +871,6 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
}}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setWarehouseInputValue}
onMenuScrollToBottom={loadMoreWarehouse}
/>
{/* Customer Filter */}
@@ -888,8 +888,6 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
}}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setCustomerInputValue}
onMenuScrollToBottom={loadMoreCustomer}
/>
{/* Filter By Date Type */}
@@ -71,32 +71,24 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
const filterModal = useModal();
// ===== OPTIONS =====
const {
options: areaOptions,
isLoadingOptions: isLoadingAreas,
setInputValue: setAreaInputValue,
loadMore: loadMoreArea,
} = useSelect(AreaApi.basePath, 'id', 'name', 'search');
const {
options: locationOptions,
isLoadingOptions: isLoadingLocations,
setInputValue: setLocationInputValue,
loadMore: loadMoreLocation,
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
const {
options: kandangOptions,
isLoadingOptions: isLoadingKandangs,
setInputValue: setKandangInputValue,
loadMore: loadMoreKandang,
} = useSelect(
ProjectFlockKandangApi.basePath,
const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect(
AreaApi.basePath,
'id',
'name_with_period',
'name',
'search'
);
const { options: locationOptions, isLoadingOptions: isLoadingLocations } =
useSelect(LocationApi.basePath, 'id', 'name', 'search');
const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } =
useSelect(
ProjectFlockKandangApi.basePath,
'id',
'name_with_period',
'search'
);
const showUnrecordedOptions = useMemo(
() => [
{ value: 'false', label: 'Sembunyikan' },
@@ -791,10 +783,6 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
[data, perWeightRangeSummary]
);
useEffectHook(() => {
filterModal.openModal();
}, []);
return (
<>
{TabActionsElement}
@@ -930,8 +918,6 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
isLoading={isLoadingAreas}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setAreaInputValue}
onMenuScrollToBottom={loadMoreArea}
/>
{/* Location Filter */}
@@ -951,8 +937,6 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
isLoading={isLoadingLocations}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setLocationInputValue}
onMenuScrollToBottom={loadMoreLocation}
/>
{/* Kandang Filter */}
@@ -972,8 +956,6 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
isLoading={isLoadingKandangs}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setKandangInputValue}
onMenuScrollToBottom={loadMoreKandang}
/>
{/* Weight Range Filter */}
@@ -43,7 +43,15 @@ export const ProductionResultFilterSchema = yup.object({
}
return !!value;
}),
kandang_id: yup.mixed<OptionType>().nullable(),
kandang_id: yup
.mixed<OptionType>()
.required('Kandang wajib dipilih')
.test('is-not-empty', 'Kandang wajib dipilih', (value) => {
if (Array.isArray(value)) {
return value.length > 0;
}
return !!value;
}),
}) as yup.ObjectSchema<ProductionResultFilterFormType>;
export type ProductionResultFilterValues = yup.InferType<
@@ -46,7 +46,6 @@ import Modal, { useModal } from '@/components/Modal';
import { formatNumber } from '@/lib/helper';
import Pagination from '@/components/Pagination';
import ProductionResultSkeleton from '@/components/pages/report/production-result/skeleton/ProductionResultSkeleton';
import { ProjectFlock } from '@/types/api/production/project-flock';
interface ProductionResultTabProps {
tabId: string;
@@ -239,17 +238,6 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
? String(values.kandang_id.value)
: undefined,
});
const selectedProjectFlockKandangRawData = isResponseSuccess(
projectFlockKandangsRawData
)
? projectFlockKandangsRawData.data.find(
(item) => item.id === values.kandang_id?.value
)
: undefined;
setSelectedProjectFlockKandang(selectedProjectFlockKandangRawData);
filterModal.closeModal();
setIsSubmitted(true);
setPage(1);
@@ -267,9 +255,6 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
formik.validateForm();
};
const [selectedProjectFlockKandang, setSelectedProjectFlockKandang] =
useState<ProjectFlockKandang | undefined>();
// ===== OPTIONS =====
const {
setInputValue: setAreaInputValue,
@@ -294,7 +279,7 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
options: projectFlockOptions,
isLoadingOptions: isLoadingProjectFlocks,
loadMore: loadMoreProjectFlocks,
} = useSelect<ProjectFlock>(
} = useSelect<BaseKandang>(
ProjectFlockApi.basePath,
'id',
'flock_name',
@@ -315,11 +300,10 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
options: projectFlockKandangOptions,
isLoadingOptions: isLoadingProjectFlockKandangs,
loadMore: loadMoreProjectFlockKandangs,
rawData: projectFlockKandangsRawData,
} = useSelect<ProjectFlockKandang>(
} = useSelect<BaseKandang>(
ProjectFlockKandangApi.basePath,
'id',
'name_with_period',
'kandang.name',
'search',
{
area_id: formik.values.area_id?.value
@@ -375,15 +359,13 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
([url]: string[]) => httpClient<BaseApiResponse<ProjectFlockKandang[]>>(url)
);
const projectFlockKandangs = useMemo(() => {
if (selectedProjectFlockKandang) {
return [selectedProjectFlockKandang];
}
return isResponseSuccess(projectFlockKandangsData)
? projectFlockKandangsData.data
: null;
}, [projectFlockKandangsData, selectedProjectFlockKandang]);
const projectFlockKandangs = useMemo(
() =>
isResponseSuccess(projectFlockKandangsData)
? projectFlockKandangsData.data
: null,
[projectFlockKandangsData]
);
const projectFlockKandangMetadata = useMemo(
() =>
@@ -649,10 +631,6 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
// Render the TabActions component
const TabActionsElement = useMemo(() => <TabActions />, [TabActions]);
useEffect(() => {
filterModal.openModal();
}, []);
return (
<>
{TabActionsElement}
@@ -822,6 +800,7 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
/>
<SelectInput
required
label='Kandang'
placeholder='Pilih Kandang'
options={projectFlockKandangOptions}
-12
View File
@@ -20,7 +20,6 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
'lti.daily_checklist.master_data.employee',
'lti.daily_checklist.master_data.activity',
'lti.daily_checklist.master_data.configuration',
'lti.daily_checklist.master_data.kandang',
],
submenu: [
{
@@ -67,11 +66,6 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
link: '/daily-checklist/master-data/activity',
permission: ['lti.daily_checklist.master_data.activity'],
},
{
text: 'Kandang',
link: '/daily-checklist/master-data/kandang',
permission: ['lti.daily_checklist.master_data.kandang'],
},
{
text: 'Konfigurasi',
link: '/daily-checklist/master-data/configuration',
@@ -555,12 +549,6 @@ export const APPROVAL_WORKFLOWS = {
],
};
export const PROJECT_FLOCK_STATUS = {
PENGAJUAN: APPROVAL_WORKFLOWS.PROJECT_FLOCKS[0].step_name,
AKTIF: APPROVAL_WORKFLOWS.PROJECT_FLOCKS[1].step_name,
SELESAI: APPROVAL_WORKFLOWS.PROJECT_FLOCKS[2].step_name,
} as const;
export const ACCEPTED_FILE_TYPE = {
PDF: {
'application/pdf': ['.pdf'],
-3
View File
@@ -21,9 +21,6 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
'/daily-checklist/master-data/configuration/': [
'lti.daily_checklist.master_data.configuration',
],
'/daily-checklist/master-data/kandang/': [
'lti.daily_checklist.master_data.kandang',
],
// Production
// Production - Project Flock
@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { Check, ChevronsUpDown, X, Loader2 } from 'lucide-react';
import { Check, ChevronsUpDown, X } from 'lucide-react';
import { cn } from '@/lib/helper';
import { Button } from '@/figma-make/components/base/button';
import {
@@ -29,8 +29,6 @@ interface MultiSelectProps {
selected: string[];
onChange: (selected: string[]) => void;
onSearchChange?: (value: string) => void;
onLoadMore?: () => void;
isLoadingMore?: boolean;
placeholder?: string;
className?: string;
disabled?: boolean;
@@ -41,8 +39,6 @@ export function MultiSelect({
selected,
onChange,
onSearchChange,
onLoadMore,
isLoadingMore,
placeholder = 'Select items...',
className,
disabled,
@@ -119,18 +115,7 @@ export function MultiSelect({
onValueChange={onSearchChange}
/>
<CommandEmpty>No item found.</CommandEmpty>
<CommandList
className='max-h-[300px] overflow-y-auto'
onScroll={(e) => {
const target = e.currentTarget;
if (
target.scrollHeight - target.scrollTop <=
target.clientHeight + 1
) {
onLoadMore?.();
}
}}
>
<CommandList className='max-h-[300px] overflow-y-auto'>
<CommandGroup className='overflow-visible'>
{options.map((option) => (
<CommandItem
@@ -149,11 +134,6 @@ export function MultiSelect({
{option.label}
</CommandItem>
))}
{isLoadingMore && (
<div className='py-4 flex justify-center w-full'>
<Loader2 className='h-4 w-4 animate-spin text-muted-foreground' />
</div>
)}
</CommandGroup>
</CommandList>
</Command>
+2 -7
View File
@@ -55,11 +55,7 @@ function SelectContent({
children,
position = 'popper',
...props
}: React.ComponentProps<typeof SelectPrimitive.Content> & {
onScroll?: React.UIEventHandler<HTMLDivElement>;
}) {
const { onScroll, ...restProps } = props;
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
@@ -71,7 +67,7 @@ function SelectContent({
className
)}
position={position}
{...restProps}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
@@ -80,7 +76,6 @@ function SelectContent({
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1'
)}
onScroll={onScroll}
>
{children}
</SelectPrimitive.Viewport>
@@ -2,16 +2,7 @@
import * as React from 'react';
import { useState, useEffect } from 'react';
import {
Plus,
X,
Save,
Send,
Info,
FilePlus,
ListChecks,
Loader2,
} from 'lucide-react';
import { Plus, X, Save, Send, Info, FilePlus, ListChecks } from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button';
import { Label } from '@/figma-make/components/base/label';
@@ -35,6 +26,7 @@ import {
import { DatePicker } from '@/figma-make/components/base/date-picker';
import { toast } from 'sonner';
import { useSelect } from '@/components/input/SelectInput';
import { KandangApi } from '@/services/api/master-data';
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import useSWR from 'swr';
@@ -51,7 +43,6 @@ import DropFileInput from '@/components/input/DropFileInput';
import Link from 'next/link';
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
import { Icon } from '@iconify/react';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
// Static categories
const CATEGORIES = [
@@ -95,11 +86,16 @@ export function DailyChecklistContent() {
searchParams.get('category') || ''
);
const {
options: kandangOptions,
isLoadingMore: isLoadingMoreKandang,
loadMore: loadMoreKandang,
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
const { options: kandangOptions } = useSelect(
KandangApi.basePath,
'id',
'name',
'search',
{
page: '1',
limit: '100',
}
);
const { data: phases } = useSWR<
BaseApiResponse<Phase[] | undefined>,
@@ -172,16 +168,6 @@ export function DailyChecklistContent() {
const [documents, setDocuments] = useState<File[]>([]);
const [deletedDocumentIds, setDeletedDocumentIds] = useState<number[]>([]);
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement;
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
}
};
// Sync state to URL query params
useEffect(() => {
const params = new URLSearchParams(searchParams.toString());
@@ -1008,7 +994,7 @@ export function DailyChecklistContent() {
>
<SelectValue placeholder='Pilih kandang' />
</SelectTrigger>
<SelectContent onScroll={handleKandangScroll}>
<SelectContent>
{kandangOptions.map((kandang) => (
<SelectItem
key={kandang.value}
@@ -1017,12 +1003,6 @@ export function DailyChecklistContent() {
{kandang.label}
</SelectItem>
))}
{isLoadingMoreKandang && (
<div className='flex justify-center p-2'>
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
</div>
)}
</SelectContent>
</Select>
</div>
@@ -1,7 +1,6 @@
'use client';
import { useState } from 'react';
import moment from 'moment';
import {
Card,
CardContent,
@@ -17,7 +16,7 @@ import {
SelectValue,
} from '@/figma-make/components/base/select';
import { Badge } from '@/figma-make/components/base/badge';
import { Users, AlertCircle, Info, Loader2 } from 'lucide-react';
import { Users, AlertCircle, Info } from 'lucide-react';
import { DateRangePicker } from '@/figma-make/components/base/date-range-picker';
import {
BarChart,
@@ -37,10 +36,10 @@ import { DailyChecklistSummary } from '@/types/api/daily-checklist/daily-checkli
import { AxiosError } from 'axios';
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
import { KandangApi } from '@/services/api/master-data';
import { useSelect } from '@/components/input/SelectInput';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { formatDate } from '@/lib/helper';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
const KANDANG_COLORS = [
'#0069e0', // Blue (primary)
@@ -60,17 +59,10 @@ const CATEGORY_LABELS: { [key: string]: string } = {
produksi_close: 'Produksi Close',
};
const getThisMonthRange = () => ({
dateFrom: moment().startOf('month').format('YYYY-MM-DD'),
dateTo: moment().endOf('month').format('YYYY-MM-DD'),
});
export function Dashboard() {
const defaultDateRange = getThisMonthRange();
// Filters
const [dateFrom, setDateFrom] = useState(defaultDateRange.dateFrom);
const [dateTo, setDateTo] = useState(defaultDateRange.dateTo);
const [dateFrom, setDateFrom] = useState('');
const [dateTo, setDateTo] = useState('');
const [kandangFilter, setKandangFilter] = useState('ALL');
const [categoryFilter, setCategoryFilter] = useState('ALL');
@@ -85,20 +77,16 @@ export function Dashboard() {
httpClientFetcher
);
const {
options: kandangOptions,
loadMore: loadMoreKandang,
isLoadingMore: isLoadingMoreKandang,
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement;
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
const { options: kandangOptions } = useSelect(
KandangApi.basePath,
'id',
'name',
'search',
{
page: '1',
limit: '100',
}
};
);
const kandangColorMap: { [key: string]: string } = {};
(kandangOptions || []).forEach((k, index) => {
@@ -176,7 +164,7 @@ export function Dashboard() {
>
<SelectValue placeholder='Semua Kandang' />
</SelectTrigger>
<SelectContent onScroll={handleKandangScroll}>
<SelectContent>
<SelectItem value='ALL'>Semua Kandang</SelectItem>
{kandangOptions.map((kandang) => (
<SelectItem
@@ -186,11 +174,6 @@ export function Dashboard() {
{kandang.label}
</SelectItem>
))}
{isLoadingMoreKandang && (
<div className='flex justify-center p-2'>
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
</div>
)}
</SelectContent>
</Select>
</div>
@@ -1,15 +1,7 @@
'use client';
import { useState } from 'react';
import {
Eye,
CheckCircle,
XCircle,
Search,
Trash2,
Edit,
Loader2,
} from 'lucide-react';
import { Eye, CheckCircle, XCircle, Search, Trash2, Edit } from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button';
import { Badge } from '@/figma-make/components/base/badge';
@@ -42,9 +34,9 @@ import { DailyChecklist } from '@/types/api/daily-checklist/daily-checklist';
import { cn } from '@/lib/helper';
import { ColumnDef } from '@tanstack/react-table';
import { useSelect } from '@/components/input/SelectInput';
import { KandangApi } from '@/services/api/master-data';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import RequirePermission from '@/components/helper/RequirePermission';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
const STATUS_OPTIONS = [
{ value: 'ALL', label: 'Semua Status' },
@@ -101,25 +93,21 @@ export function ListDailyChecklistContent() {
}
);
const {
options: kandangOptions,
isLoadingMore: isLoadingMoreKandang,
loadMore: loadMoreKandang,
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
const { options: kandangOptions } = useSelect(
KandangApi.basePath,
'id',
'name',
'search',
{
page: '1',
limit: '100',
}
);
const checklistList = isResponseSuccess(checklistListRes)
? checklistListRes.data || []
: [];
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement;
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
}
};
// Modals
const [showApproveModal, setShowApproveModal] = useState(false);
const [showRejectModal, setShowRejectModal] = useState(false);
@@ -136,7 +124,7 @@ export function ListDailyChecklistContent() {
const handleEdit = (item: DailyChecklist) => {
const formattedDate = new Date(item.date).toISOString().split('T')[0];
const kandangId = item.kandang?.id ?? '';
const kandangId = item.kandang.id;
const category = item.category;
router.push(
@@ -335,7 +323,7 @@ export function ListDailyChecklistContent() {
accessorKey: 'kandang',
header: 'Kandang',
enableSorting: false,
cell: ({ row }) => row.original.kandang?.name ?? '-',
cell: ({ row }) => row.original.kandang.name,
},
{
accessorKey: 'category',
@@ -502,7 +490,7 @@ export function ListDailyChecklistContent() {
>
<SelectValue placeholder='Semua Kandang' />
</SelectTrigger>
<SelectContent onScroll={handleKandangScroll}>
<SelectContent>
<SelectItem value='ALL'>Semua Kandang</SelectItem>
{kandangOptions.map((kandang) => (
<SelectItem
@@ -512,11 +500,6 @@ export function ListDailyChecklistContent() {
{kandang.label}
</SelectItem>
))}
{isLoadingMoreKandang && (
<div className='flex justify-center p-2'>
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
</div>
)}
</SelectContent>
</Select>
</div>
@@ -627,7 +610,7 @@ export function ListDailyChecklistContent() {
<div className='flex justify-between text-sm'>
<span className='text-gray-600'>Kandang:</span>
<span className='font-medium text-gray-900'>
{selectedItem.kandang?.name ?? '-'}
{selectedItem.kandang.name}
</span>
</div>
<div className='flex justify-between text-sm'>
@@ -687,7 +670,7 @@ export function ListDailyChecklistContent() {
<div className='flex justify-between text-sm'>
<span className='text-gray-600'>Kandang:</span>
<span className='font-medium text-gray-900'>
{selectedItem.kandang?.name ?? '-'}
{selectedItem.kandang.name}
</span>
</div>
<div className='flex justify-between text-sm'>
@@ -760,7 +743,7 @@ export function ListDailyChecklistContent() {
<div className='flex justify-between text-sm'>
<span className='text-gray-600'>Kandang:</span>
<span className='font-medium text-gray-900'>
{selectedItem.kandang?.name ?? '-'}
{selectedItem.kandang.name}
</span>
</div>
<div className='flex justify-between text-sm'>
@@ -172,7 +172,7 @@ export function DetailDailyChecklistContent() {
const checklistData = {
id: rawDetailChecklist?.id,
date: rawDetailChecklist?.date,
kandang_id: rawDetailChecklist?.kandang?.id,
kandang_id: rawDetailChecklist?.kandang.id,
category: rawDetailChecklist?.category,
status: rawDetailChecklist?.status,
reject_reason: rawDetailChecklist?.reject_reason,
@@ -1,14 +1,7 @@
'use client';
import { useState } from 'react';
import {
Plus,
MoreVertical,
Pencil,
Trash2,
Search,
Loader2,
} from 'lucide-react';
import { Plus, MoreVertical, Pencil, Trash2, Search } from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button';
import { Label } from '@/figma-make/components/base/label';
@@ -56,8 +49,8 @@ import { cn } from '@/lib/helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ColumnDef } from '@tanstack/react-table';
import { useSelect } from '@/components/input/SelectInput';
import { KandangApi } from '@/services/api/master-data';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
export function MasterEmployeeContent() {
const {
@@ -92,20 +85,16 @@ export function MasterEmployeeContent() {
keepPreviousData: true,
}
);
const {
options: kandangOptions,
loadMore: loadMoreKandang,
isLoadingMore: isLoadingMoreKandang,
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement;
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
const { options: kandangOptions } = useSelect(
KandangApi.basePath,
'id',
'name',
'search',
{
page: '1',
limit: '100',
}
};
);
const [showModal, setShowModal] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
@@ -362,7 +351,7 @@ export function MasterEmployeeContent() {
<SelectTrigger className='w-[180px] border-gray-200'>
<SelectValue placeholder='Semua Kandang' />
</SelectTrigger>
<SelectContent onScroll={handleKandangScroll}>
<SelectContent>
<SelectItem value='all'>Semua Kandang</SelectItem>
{kandangOptions.map((kandang) => (
<SelectItem
@@ -372,11 +361,6 @@ export function MasterEmployeeContent() {
{kandang.label}
</SelectItem>
))}
{isLoadingMoreKandang && (
<div className='flex justify-center p-2'>
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
</div>
)}
</SelectContent>
</Select>
@@ -487,12 +471,6 @@ export function MasterEmployeeContent() {
kandang_ids: selected.map((id) => Number(id)),
})
}
onLoadMore={() => {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
}}
isLoadingMore={isLoadingMoreKandang}
placeholder='Pilih kandang'
className='mt-1.5'
/>
@@ -1,585 +0,0 @@
'use client';
import { useState } from 'react';
import { Plus, MoreVertical, Pencil, Trash2, Search } from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button';
import { Label } from '@/figma-make/components/base/label';
import { Input } from '@/figma-make/components/base/input';
import { Badge } from '@/figma-make/components/base/badge';
import { MultiSelect } from '@/figma-make/components/base/multi-select';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/figma-make/components/base/select';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/figma-make/components/base/dialog';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/figma-make/components/base/alert-dialog';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/figma-make/components/base/dropdown-menu';
import { toast } from 'sonner';
import useSWR from 'swr';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
import Table from '@/components/Table';
import { DailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { cn } from '@/lib/helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ColumnDef } from '@tanstack/react-table';
import { useSelect } from '@/components/input/SelectInput';
import { KandangApi, LocationApi } from '@/services/api/master-data';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import { BaseDailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
import { UserApi } from '@/services/api/user';
export function MasterKandangContent() {
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
location_id: '',
status: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
search: 'search',
location_id: 'location_id',
},
});
const {
data: dailyChecklistKandangs,
isLoading: isLoadingDailyChecklistKandangs,
mutate: refreshDailyChecklistKandangs,
} = useSWR(
`${DailyChecklistKandangApi.basePath}${getTableFilterQueryString()}`,
DailyChecklistKandangApi.getAllFetcher,
{
keepPreviousData: true,
}
);
const { options: locationOptions } = useSelect(
LocationApi.basePath,
'id',
'name',
'search',
{
page: '1',
limit: '100',
}
);
const { options: picOptions } = useSelect(
UserApi.basePath,
'id',
'name',
'search',
{
page: '1',
limit: '100',
}
);
const {
options: kandangOptions,
isLoadingMore: isLoadingKandangOptionsMore,
loadMore: loadMoreKandang,
} = useSelect(KandangApi.basePath, 'id', 'name');
const [showModal, setShowModal] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [kandangToDelete, setKandangToDelete] = useState<number | null>(null);
const [loading, setLoading] = useState(false);
const [modalMode, setModalMode] = useState<'create' | 'edit'>('create');
const [kandangForm, setKandangForm] = useState({
id: 0,
name: '',
location_id: 0,
pic_id: 0,
// recording_kandangs: [] as number[],
});
const dailyChecklistKandangColumns: ColumnDef<DailyChecklistKandang>[] = [
{
id: 'name',
header: 'Nama',
accessorKey: 'name',
enableSorting: false,
},
{
id: 'location',
header: 'Lokasi',
accessorKey: 'location',
enableSorting: false,
cell: ({ row }) => row.original.location.name ?? '-',
},
{
id: 'pic',
header: 'PIC',
accessorKey: 'pic',
enableSorting: false,
cell: ({ row }) => row.original.pic.name ?? '-',
},
{
id: 'recording_kandangs',
header: 'Kandang Recording',
accessorKey: 'recording_kandangs',
enableSorting: false,
cell: ({ row }) =>
row.original.recording_kandangs?.length > 0
? row.original.recording_kandangs.map((item) => item.name).join(', ')
: '-',
},
{
id: 'action',
header: 'Aksi',
accessorKey: 'action',
enableSorting: false,
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='ghost'
size='sm'
className='h-8 w-8 p-0 hover:bg-gray-100'
>
<MoreVertical className='h-4 w-4 text-gray-600' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem onClick={() => handleEdit(row.original)}>
<Pencil className='mr-2 h-4 w-4' />
Edit
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDeleteClick(row.original.id)}
className='text-red-600'
>
<Trash2 className='mr-2 h-4 w-4' />
Hapus
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
},
];
const handleAdd = () => {
setModalMode('create');
setKandangForm({
id: 0,
name: '',
location_id: 0,
pic_id: 0,
// recording_kandangs: []
});
setShowModal(true);
};
const handleEdit = (dailyChecklistKandang: DailyChecklistKandang) => {
setModalMode('edit');
setKandangForm({
id: dailyChecklistKandang.id,
name: dailyChecklistKandang.name,
location_id: dailyChecklistKandang.location.id,
pic_id: dailyChecklistKandang.pic.id,
// recording_kandangs:
// dailyChecklistKandang.recording_kandangs.map((item) => item.id) ?? [],
});
setShowModal(true);
};
const handleSave = async () => {
if (!kandangForm.name.trim()) {
toast.error('Nama harus diisi');
return;
}
if (!kandangForm.location_id) {
toast.error('Lokasi wajib diisi');
return;
}
// if (!kandangForm.recording_kandangs.length) {
// toast.error('Kandang recording wajib diisi');
// return;
// }
setLoading(true);
try {
if (modalMode === 'create') {
const createDailyChecklistKandangResponse =
await DailyChecklistKandangApi.create({
name: kandangForm.name.trim(),
location_id: kandangForm.location_id,
pic_id: kandangForm.pic_id,
// recording_kandang_ids: kandangForm.recording_kandangs,
});
if (isResponseError(createDailyChecklistKandangResponse)) {
console.error(
'Error creating kandang:',
createDailyChecklistKandangResponse.message
);
toast.error('Gagal menambahkan kandang');
return;
}
refreshDailyChecklistKandangs();
toast.success('Kandang berhasil ditambahkan');
} else {
const updateDailyChecklistKandangResponse =
await DailyChecklistKandangApi.update(kandangForm.id, {
name: kandangForm.name.trim(),
location_id: kandangForm.location_id,
pic_id: kandangForm.pic_id,
// recording_kandang_ids: kandangForm.recording_kandangs,
});
if (isResponseError(updateDailyChecklistKandangResponse)) {
console.error(
'Error updating kandang:',
updateDailyChecklistKandangResponse.message
);
toast.error('Gagal menambahkan Kandang');
return;
}
refreshDailyChecklistKandangs();
toast.success('Kandang berhasil diubah');
}
setShowModal(false);
setKandangForm({
id: 0,
name: '',
location_id: 0,
pic_id: 0,
// recording_kandangs: [],
});
} catch (error) {
console.error('Error saving kandang:', error);
toast.error('Terjadi kesalahan saat menyimpan kandang');
} finally {
setLoading(false);
}
};
const handleDeleteClick = (kandangId: number) => {
setKandangToDelete(kandangId);
setShowDeleteConfirm(true);
};
const handleConfirmDelete = async () => {
if (!kandangToDelete) return;
setLoading(true);
try {
const deleteKandangResponse =
await DailyChecklistKandangApi.delete(kandangToDelete);
if (isResponseError(deleteKandangResponse)) {
console.error('Error deleting kandang:', deleteKandangResponse.message);
toast.error('Gagal menghapus kandang');
return;
}
refreshDailyChecklistKandangs();
toast.success('Kandang berhasil dihapus');
setShowDeleteConfirm(false);
setKandangToDelete(null);
} catch (error) {
console.error('Error deleting kandang:', error);
toast.error('Terjadi kesalahan saat menghapus kandang');
} finally {
setLoading(false);
}
};
if (isLoadingDailyChecklistKandangs && !dailyChecklistKandangs) {
return (
<div className='min-h-screen'>
<div className='p-6'>
<div className='mb-6'>
<h1 className='text-2xl font-semibold text-gray-900'>
Master Kandang
</h1>
<p className='text-sm text-gray-600 mt-1'>
Master Data <span className='text-[#0069e0]'>Kandang</span>
</p>
</div>
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
<CardContent className='p-12 text-center text-gray-500'>
Memuat data...
</CardContent>
</Card>
</div>
</div>
);
}
return (
<div className='min-h-screen'>
<div className='p-6'>
{/* Page Title */}
<div className='mb-6'>
<h1 className='text-2xl font-semibold text-gray-900'>
Master Kandang
</h1>
<p className='text-sm text-gray-600 mt-1'>
Master Data <span className='text-[#0069e0]'>Kandang</span>
</p>
</div>
{/* Main Card */}
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
<CardContent className='p-0'>
{/* Single Toolbar Row */}
<div className='flex flex-wrap items-center justify-between gap-4 p-6 border-b border-gray-200/60'>
{/* LEFT: Search + Filters */}
<div className='flex items-center gap-3 flex-wrap'>
<div className='relative'>
<Search className='absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4' />
<DebouncedTextInput
name='search'
placeholder='Cari kandang...'
value={tableFilterState.search}
onChange={(e) => updateFilter('search', e.target.value)}
className={{
wrapper: 'w-full sm:w-[280px] border-gray-200',
inputWrapper: 'px-3 py-2 h-fit rounded-md',
input: 'text-sm',
}}
startAdornment={
<Search className='text-gray-400 w-4 h-4' />
}
/>
</div>
<Select
value={tableFilterState.location_id}
onValueChange={(value) =>
updateFilter('location_id', value === 'all' ? '' : value)
}
>
<SelectTrigger className='w-[180px] border-gray-200'>
<SelectValue placeholder='Semua Lokasi' />
</SelectTrigger>
<SelectContent>
<SelectItem value='all'>Semua Lokasi</SelectItem>
{locationOptions.map((kandang) => (
<SelectItem
key={kandang.value}
value={String(kandang.value)}
>
{kandang.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* RIGHT: Export + Add */}
<div className='flex items-center gap-2 flex-wrap'>
<Button
onClick={handleAdd}
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
>
<Plus className='w-4 h-4 mr-2' />
Tambah Kandang
</Button>
</div>
</div>
{/* Table */}
<Table<DailyChecklistKandang>
data={
isResponseSuccess(dailyChecklistKandangs)
? dailyChecklistKandangs?.data
: []
}
columns={dailyChecklistKandangColumns}
pageSize={tableFilterState.pageSize}
onPageSizeChange={setPageSize}
rowOptions={[10, 20, 50, 100]}
page={
isResponseSuccess(dailyChecklistKandangs)
? dailyChecklistKandangs?.meta?.page
: 0
}
totalItems={
isResponseSuccess(dailyChecklistKandangs)
? dailyChecklistKandangs?.meta?.total_results
: 0
}
onPageChange={setPage}
isLoading={isLoadingDailyChecklistKandangs}
className={{
containerClassName: cn({
'w-full mb-20':
isResponseSuccess(dailyChecklistKandangs) &&
dailyChecklistKandangs?.data?.length === 0,
}),
tableWrapperClassName:
'overflow-x-auto border border-solid border-base-content/10 rounded-none',
headerRowClassName: 'bg-gray-50/50',
headerColumnClassName:
'text-left py-3.5 px-6 text-sm font-semibold text-gray-700',
paginationClassName: 'px-4',
}}
/>
</CardContent>
</Card>
</div>
{/* Add/Edit Modal */}
<Dialog open={showModal} onOpenChange={setShowModal}>
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
<DialogHeader>
<DialogTitle>
{modalMode === 'create' ? 'Tambah Kandang' : 'Edit Kandang'}
</DialogTitle>
<DialogDescription>
{modalMode === 'create'
? 'Masukkan detail Kandang baru'
: 'Ubah detail Kandang'}
</DialogDescription>
</DialogHeader>
<div className='space-y-4 py-4'>
<div>
<Label htmlFor='nama-kandang'>
Nama Kandang <span className='text-red-500'>*</span>
</Label>
<Input
id='nama-kandang'
value={kandangForm.name}
onChange={(e) =>
setKandangForm({ ...kandangForm, name: e.target.value })
}
placeholder='Masukkan nama Kandang'
className='mt-1.5'
disabled={loading}
/>
</div>
<div>
<Label htmlFor='category'>
Lokasi <span className='text-red-500'>*</span>
</Label>
<Select
value={
kandangForm.location_id ? String(kandangForm.location_id) : ''
}
onValueChange={(value) =>
setKandangForm({ ...kandangForm, location_id: Number(value) })
}
>
<SelectTrigger id='category' className='mt-1.5'>
<SelectValue placeholder='Pilih lokasi' />
</SelectTrigger>
<SelectContent>
{locationOptions.map((cat) => (
<SelectItem key={cat.value} value={String(cat.value)}>
{cat.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor='pic'>
PIC <span className='text-red-500'>*</span>
</Label>
<Select
value={kandangForm.pic_id ? String(kandangForm.pic_id) : ''}
onValueChange={(value) =>
setKandangForm({ ...kandangForm, pic_id: Number(value) })
}
>
<SelectTrigger id='pic' className='mt-1.5'>
<SelectValue placeholder='Pilih PIC' />
</SelectTrigger>
<SelectContent>
{picOptions.map((cat) => (
<SelectItem key={cat.value} value={String(cat.value)}>
{cat.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button
variant='outline'
onClick={() => setShowModal(false)}
disabled={loading}
>
Batal
</Button>
<Button
onClick={handleSave}
disabled={loading}
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
>
{loading ? 'Menyimpan...' : 'Simpan'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Confirmation */}
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
<AlertDialogContent className='bg-white rounded-xl shadow-lg sm:max-w-md'>
<AlertDialogHeader>
<AlertDialogTitle>Hapus Kandang?</AlertDialogTitle>
<AlertDialogDescription>
Data Kandang akan dihapus secara permanen.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={loading}>Batal</AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirmDelete}
disabled={loading}
className='bg-red-600 hover:bg-red-700 text-white'
>
{loading ? 'Menghapus...' : 'Hapus'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
@@ -11,7 +11,7 @@ import {
SelectValue,
} from '@/figma-make/components/base/select';
import { useSelect } from '@/components/input/SelectInput';
import { AreaApi, LocationApi } from '@/services/api/master-data';
import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data';
import useSWR from 'swr';
import { BaseApiResponse } from '@/types/api/api-general';
import { DailyChecklistReport } from '@/types/api/daily-checklist/daily-checklist';
@@ -26,8 +26,7 @@ import { ColumnDef } from '@tanstack/react-table';
import { PhaseApi } from '@/services/api/daily-checklist/phase';
import { EmployeeApi } from '@/services/api/daily-checklist/employee';
import { Button } from '@/figma-make/components/base/button';
import { Download, Loader2 } from 'lucide-react';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
import { Download } from 'lucide-react';
const MONTH_OPTIONS = [
{ value: '1', label: 'Januari' },
@@ -130,23 +129,18 @@ export function DailyChecklistReportsContent() {
}
);
const {
options: kandangOptions,
loadMore: loadMoreKandang,
isLoadingMore: isLoadingMoreKandang,
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name', 'search', {
area_id: tableFilterState.area_id,
location_id: tableFilterState.location_id,
});
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement;
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
const { options: kandangOptions } = useSelect(
KandangApi.basePath,
'id',
'name',
'search',
{
page: '1',
limit: '100',
area_id: tableFilterState.area_id,
location_id: tableFilterState.location_id,
}
};
);
const { options: phaseOptions } = useSelect(
PhaseApi.basePath,
@@ -441,7 +435,7 @@ export function DailyChecklistReportsContent() {
>
<SelectValue placeholder='Semua Kandang' />
</SelectTrigger>
<SelectContent onScroll={handleKandangScroll}>
<SelectContent>
<SelectItem value='ALL'>Semua Kandang</SelectItem>
{kandangOptions.map((kandang) => (
<SelectItem
@@ -451,11 +445,6 @@ export function DailyChecklistReportsContent() {
{kandang.label}
</SelectItem>
))}
{isLoadingMoreKandang && (
<div className='flex justify-center p-2'>
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
</div>
)}
</SelectContent>
</Select>
</div>
+55 -57
View File
@@ -76,13 +76,13 @@ export const calculateTrading = (
case 'unit_price':
case 'qty': {
if (unitPrice > 0 && qty > 0) {
setFieldValue('total_price', unitPrice * qty);
setFieldValue('total_price', roundPrice(unitPrice * qty));
}
break;
}
case 'total_price': {
if (totalPrice > 0 && qty > 0) {
setFieldValue('unit_price', totalPrice / qty);
setFieldValue('unit_price', roundPrice(totalPrice / qty));
}
break;
}
@@ -112,7 +112,7 @@ export const calculateAyamPullet = (
case 'qty': {
// total_price = unit_price × week × qty
if (unitPrice > 0 && week > 0 && qty > 0) {
setFieldValue('total_price', unitPrice * week * qty);
setFieldValue('total_price', roundPrice(unitPrice * week * qty));
}
// total_weight = avg_weight × qty
if (avgWeight > 0 && qty > 0) {
@@ -135,7 +135,7 @@ export const calculateAyamPullet = (
case 'total_price': {
// Reverse: unit_price = total_price / (week × qty)
if (totalPrice > 0 && week > 0 && qty > 0) {
setFieldValue('unit_price', totalPrice / (week * qty));
setFieldValue('unit_price', roundPrice(totalPrice / (week * qty)));
}
break;
}
@@ -164,7 +164,7 @@ export const calculateAyam = (field: string, ctx: CalculationContext): void => {
setFieldValue('total_weight', tw);
// total_price = total_weight × unit_price
if (unitPrice > 0) {
setFieldValue('total_price', tw * unitPrice);
setFieldValue('total_price', roundPrice(tw * unitPrice));
}
}
break;
@@ -176,21 +176,21 @@ export const calculateAyam = (field: string, ctx: CalculationContext): void => {
}
// total_price = total_weight × unit_price
if (unitPrice > 0 && totalWeight > 0) {
setFieldValue('total_price', totalWeight * unitPrice);
setFieldValue('total_price', roundPrice(totalWeight * unitPrice));
}
break;
}
case 'unit_price': {
// total_price = total_weight × unit_price
if (unitPrice > 0 && totalWeight > 0) {
setFieldValue('total_price', totalWeight * unitPrice);
setFieldValue('total_price', roundPrice(totalWeight * unitPrice));
}
break;
}
case 'total_price': {
// unit_price = total_price / total_weight
if (totalPrice > 0 && totalWeight > 0) {
setFieldValue('unit_price', totalPrice / totalWeight);
setFieldValue('unit_price', roundPrice(totalPrice / totalWeight));
}
break;
}
@@ -223,8 +223,7 @@ export const calculateTelurPeti = (
// Helper untuk menghitung dan set unit_price = total_price / total_weight
const updateUnitPrice = (tp: number, tw: number) => {
if (tw > 0 && tp > 0) {
const unitPrice = tp / tw;
setFieldValue('unit_price', unitPrice);
setFieldValue('unit_price', roundPrice(tp / tw));
}
};
@@ -233,7 +232,7 @@ export const calculateTelurPeti = (
// Recalculate total_price = (price_per_convertion × total_peti) + price_sisa_berat
if (pricePerConvertion > 0 && totalPeti > 0) {
const totalPrice = pricePerConvertion * totalPeti + priceSisaBerat;
setFieldValue('total_price', totalPrice);
setFieldValue('total_price', roundPrice(totalPrice));
// Recalculate unit_price = total_price / total_weight
const totalWeight = weightPerConvertion * totalPeti + sisaBerat;
updateUnitPrice(totalPrice, totalWeight);
@@ -254,8 +253,8 @@ export const calculateTelurPeti = (
// Recalculate total_price = (price_per_convertion × total_peti) + price_sisa_berat
if (pricePerConvertion > 0 && totalPeti > 0) {
const totalPrice = pricePerConvertion * totalPeti + priceSisaBerat;
setFieldValue('total_price', totalPrice);
// Recalculate unit_price = total_price / totalWeight
setFieldValue('total_price', roundPrice(totalPrice));
// Recalculate unit_price = total_price / total_weight
updateUnitPrice(totalPrice, totalWeight);
}
break;
@@ -264,7 +263,7 @@ export const calculateTelurPeti = (
// Recalculate total_price
if (pricePerConvertion > 0 && totalPeti > 0) {
const totalPrice = pricePerConvertion * totalPeti + priceSisaBerat;
setFieldValue('total_price', totalPrice);
setFieldValue('total_price', roundPrice(totalPrice));
// Recalculate unit_price = total_price / total_weight
const totalWeight = weightPerConvertion * totalPeti + sisaBerat;
updateUnitPrice(totalPrice, totalWeight);
@@ -307,7 +306,7 @@ export const calculateTelurPeti = (
if (totalPeti > 0 && totalPrice > priceSisaBerat) {
setFieldValue(
'price_per_convertion',
(totalPrice - priceSisaBerat) / totalPeti
roundPrice((totalPrice - priceSisaBerat) / totalPeti)
);
}
// Update unit_price = total_price / total_weight
@@ -315,15 +314,6 @@ export const calculateTelurPeti = (
updateUnitPrice(totalPrice, totalWeight);
break;
}
case 'qty':
// Recalculate avg_weight = total_weight / qty
if (qty > 0 && values.total_weight) {
setFieldValue(
'avg_weight',
preciseWeight(Number(values.total_weight) / qty)
);
}
break;
}
};
@@ -351,7 +341,10 @@ export const calculateTelurKg = (
}
// total_price = total_weight × unit_price
if (pricePerConvertion > 0 && totalWeight > 0) {
setFieldValue('total_price', totalWeight * pricePerConvertion);
setFieldValue(
'total_price',
roundPrice(totalWeight * pricePerConvertion)
);
setFieldValue('unit_price', pricePerConvertion);
}
break;
@@ -359,7 +352,10 @@ export const calculateTelurKg = (
case 'price_per_convertion': {
// total_price = total_weight × price_per_convertion
if (pricePerConvertion > 0 && totalWeight > 0) {
setFieldValue('total_price', totalWeight * pricePerConvertion);
setFieldValue(
'total_price',
roundPrice(totalWeight * pricePerConvertion)
);
setFieldValue('unit_price', pricePerConvertion);
}
break;
@@ -367,8 +363,11 @@ export const calculateTelurKg = (
case 'total_price': {
// unit_price = total_price / total_weight
if (totalPrice > 0 && totalWeight > 0) {
setFieldValue('unit_price', totalPrice / totalWeight);
setFieldValue('price_per_convertion', totalPrice / totalWeight);
setFieldValue('unit_price', roundPrice(totalPrice / totalWeight));
setFieldValue(
'price_per_convertion',
roundPrice(totalPrice / totalWeight)
);
}
break;
}
@@ -377,11 +376,13 @@ export const calculateTelurKg = (
/**
* TELUR + QTY Workaround:
* - User inputs: qty, avg_weight, unit_price (harga per butir)
* - User inputs: qty, avg_weight, price_per_qty (harga per butir)
* - FE calculates:
* - total_weight = avg_weight × qty
* - total_price = qty × unit_price
* - price_per_qty = total_price / total_weight (harga per kg)
* - total_price = qty × price_per_qty
* - unit_price = total_price / total_weight (normalisasi untuk BE)
* - Kirim convertion_unit: "KG" karena BE tidak support "QTY"
* - BE akan hitung: total_price = total_weight × unit_price (hasil sama)
*/
export const calculateTelurQty = (
field: string,
@@ -402,13 +403,13 @@ export const calculateTelurQty = (
if (avgWeight > 0 && qty > 0) {
const tw = roundWeight(avgWeight * qty);
setFieldValue('total_weight', tw);
// total_price = qty × unit_price
if (unitPrice > 0) {
const tp = qty * unitPrice;
// total_price = qty × price_per_qty
if (pricePerQty > 0) {
const tp = roundPrice(qty * pricePerQty);
setFieldValue('total_price', tp);
// price_per_qty = total_price / total_weight
// unit_price = total_price / total_weight (untuk BE)
if (tw > 0) {
setFieldValue('price_per_qty', tp / tw);
setFieldValue('unit_price', roundPrice(tp / tw));
}
}
}
@@ -418,47 +419,44 @@ export const calculateTelurQty = (
// avg_weight = total_weight / qty
if (totalWeight > 0 && qty > 0) {
setFieldValue('avg_weight', preciseWeight(totalWeight / qty));
// Recalculate total_price jika ada harga per butir
// Recalculate total_price jika ada unit_price
if (unitPrice > 0) {
setFieldValue('total_price', qty * unitPrice);
setFieldValue('total_price', roundPrice(totalWeight * unitPrice));
}
}
break;
}
case 'price_per_qty': {
// total_price = total_weight × price_per_qty
if (pricePerQty > 0 && totalWeight > 0) {
const tp = totalWeight * pricePerQty;
// total_price = qty × price_per_qty
if (pricePerQty > 0 && qty > 0) {
const tp = roundPrice(qty * pricePerQty);
setFieldValue('total_price', tp);
// unit_price = total_price / qty
if (qty > 0) {
setFieldValue('unit_price', tp / qty);
// unit_price = total_price / total_weight (untuk BE)
if (totalWeight > 0) {
setFieldValue('unit_price', roundPrice(tp / totalWeight));
}
}
break;
}
case 'total_price': {
// unit_price = total_price / qty
// price_per_qty = total_price / qty
if (totalPrice > 0 && qty > 0) {
setFieldValue('unit_price', totalPrice / qty);
// price_per_qty = total_price / total_weight
setFieldValue('price_per_qty', roundPrice(totalPrice / qty));
// unit_price = total_price / total_weight (untuk BE)
if (totalWeight > 0) {
setFieldValue('price_per_qty', totalPrice / totalWeight);
setFieldValue('unit_price', roundPrice(totalPrice / totalWeight));
}
}
break;
}
case 'unit_price': {
// total_price = qty × unit_price
const newTotalPrice = qty * unitPrice;
if (unitPrice > 0 && qty > 0) {
setFieldValue('total_price', newTotalPrice);
// total_price = total_weight × unit_price
if (unitPrice > 0 && totalWeight > 0) {
setFieldValue('total_price', roundPrice(totalWeight * unitPrice));
}
// price_per_qty = total_price / total_weight
if (newTotalPrice > 0 && totalWeight > 0) {
setFieldValue('price_per_qty', newTotalPrice / totalWeight);
// price_per_qty = total_price / qty
if (totalPrice > 0 && qty > 0) {
setFieldValue('price_per_qty', roundPrice(totalPrice / qty));
}
break;
}
-61
View File
@@ -1,61 +0,0 @@
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
import { Warehouse } from '@/types/api/master-data/warehouse';
export const getWarehouseScopeLabel = (
warehouse?: Warehouse | null
): string => {
if (!warehouse) {
return 'Gudang';
}
if (warehouse.type === 'KANDANG') {
return warehouse.kandang?.name
? `Kandang ${warehouse.kandang.name}`
: 'Gudang Kandang';
}
if (warehouse.type === 'LOKASI') {
return 'Gudang Farm';
}
return 'Gudang Area';
};
export const getProductWarehouseOptionLabel = (
productWarehouse?: ProductWarehouse | null
): string => {
if (!productWarehouse) {
return '';
}
const productName = productWarehouse.product?.name || 'Produk';
const warehouseName = productWarehouse.warehouse?.name || 'Gudang';
const warehouseScope = getWarehouseScopeLabel(productWarehouse.warehouse);
return `${productName}${warehouseName} (${warehouseScope})`;
};
export const isProductWarehouseSelectableForKandang = (
productWarehouse: ProductWarehouse,
kandangId?: number | null
): boolean => {
const warehouse = productWarehouse.warehouse;
if (!warehouse) {
return false;
}
if (warehouse.type === 'LOKASI') {
return true;
}
if (warehouse.type === 'KANDANG') {
return (
Boolean(kandangId) &&
(warehouse.kandang?.id === kandangId ||
productWarehouse.project_flock_kandang?.kandang_id === kandangId)
);
}
return false;
};
@@ -1,20 +0,0 @@
import { BaseApiService } from '@/services/api/base';
import {
DailyChecklistKandang,
CreateDailyChecklistKandangPayload,
UpdateDailyChecklistKandangPayload,
} from '@/types/api/daily-checklist/kandang';
export class DailyChecklistKandangApiService extends BaseApiService<
DailyChecklistKandang,
CreateDailyChecklistKandangPayload,
UpdateDailyChecklistKandangPayload
> {
constructor(basePath: string = '/master-data/kandang-groups') {
super(basePath);
}
}
export const DailyChecklistKandangApi = new DailyChecklistKandangApiService(
'/master-data/kandang-groups'
);
+6 -20
View File
@@ -1,6 +1,7 @@
import { BaseApiService } from '@/services/api/base';
import { BaseApiResponse } from '@/types/api/api-general';
import { Dashboard, DashboardFilter } from '@/types/api/dashboard/dashboard';
import { Dashboard } from '@/types/api/dashboard/dashboard';
import { httpClientFetcher } from '@/services/http/client';
class DashboardService extends BaseApiService<Dashboard, unknown, unknown> {
constructor(basePath: string) {
@@ -13,26 +14,11 @@ class DashboardService extends BaseApiService<Dashboard, unknown, unknown> {
* @returns Promise with BaseApiResponse containing DashboardProduction
*/
async getDashboardProductionFetcher(
params: DashboardFilter
endpoint: string
): Promise<BaseApiResponse<Dashboard> | undefined> {
return await this.customRequest<BaseApiResponse<Dashboard>>('', {
method: 'GET',
params: {
start_date: params.start_date || undefined,
end_date: params.end_date || undefined,
analysis_mode: params.analysis_mode || undefined,
location_ids: params.location_ids.length
? params.location_ids.toString()
: undefined,
flock_ids: params.flock_ids.length
? params.flock_ids.toString()
: undefined,
kandang_ids: params.kandang_ids.length
? params.kandang_ids.toString()
: undefined,
comparison_type: params.comparison_type || undefined,
},
});
return await httpClientFetcher<BaseApiResponse<Dashboard>>(
`${endpoint ? endpoint : this.basePath}`
);
}
}
-26
View File
@@ -12,8 +12,6 @@ import {
NextDayRecording,
} from '@/types/api/production/recording';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
import { httpClient } from '@/services/http/client';
import { formatDate } from '@/lib/helper';
export const ProjectFlockKandangApi = new BaseApiService<
ProjectFlockKandang,
@@ -90,30 +88,6 @@ export class RecordingService extends BaseApiService<
}
);
}
async exportToExcel(initialQueryString: string) {
const params = new URLSearchParams(initialQueryString);
params.set('export', 'excel');
const queryString = `?${params.toString()}`;
const res = await httpClient<Blob>(`${this.basePath}${queryString}`, {
method: 'GET',
responseType: 'blob',
});
const url = window.URL.createObjectURL(new Blob([res]));
const link = document.createElement('a');
link.href = url;
const fileName = `recording-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`;
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
link.remove();
}
}
export const RecordingApi = new RecordingService('/production/recordings');
-7
View File
@@ -9,13 +9,6 @@ export type RequestOptions<B = unknown> = {
auth?: AuthMode; // 'cookie' | 'bearer' | 'none'
token?: string; // required if auth === 'bearer'
timeoutMs?: number;
responseType?:
| 'arraybuffer'
| 'blob'
| 'document'
| 'json'
| 'text'
| 'stream';
};
export class HttpError extends Error {
+1 -5
View File
@@ -10,10 +10,7 @@ const axiosClient = axios.create({ baseURL: BASE_URL, timeout: 10_000 });
axiosClient.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
if (
error.response?.status === 401 &&
error.config?.url !== '/sso/refresh'
) {
if (error.response?.status === 401) {
redirectToSSO();
}
@@ -40,7 +37,6 @@ export async function httpClient<T, B = unknown>(
data: opts.body,
timeout: opts.timeoutMs ?? 10_000,
withCredentials: isCookieAuth && !isBearerAuth,
responseType: opts.responseType,
headers: {
...(isFormData ? {} : { 'Content-Type': 'application/json' }),
...(opts.headers ?? {}),
+1 -1
View File
@@ -12,7 +12,7 @@ export type BaseDailyChecklist = {
status: string;
category: string;
date: string;
kandang?: Pick<BaseKandang, 'id' | 'name' | 'status' | 'capacity'>;
kandang: Pick<BaseKandang, 'id' | 'name' | 'status' | 'capacity'>;
total_phase: number;
total_activity: number;
progress: number;
-24
View File
@@ -1,24 +0,0 @@
import { BaseMetadata } from '@/types/api/api-general';
import { BaseKandang } from '@/types/api/master-data/kandang';
import { BaseLocation } from '@/types/api/master-data/location';
import { BaseUser } from '@/types/api/user';
export type BaseDailyChecklistKandang = {
id: number;
name: string;
location: BaseLocation;
recording_kandangs: Pick<BaseKandang, 'id' | 'name'>[];
pic: BaseUser;
};
export type DailyChecklistKandang = BaseMetadata & BaseDailyChecklistKandang;
export type CreateDailyChecklistKandangPayload = {
name: string;
location_id: number;
pic_id: number;
// recording_kandang_ids: number[];
};
export type UpdateDailyChecklistKandangPayload =
CreateDailyChecklistKandangPayload;
-11
View File
@@ -9,19 +9,8 @@ export type BaseProductWarehouse = {
warehouse_id: number;
uom: Uom;
quantity: number;
transfer_available_qty?: number;
product: Product;
warehouse: Warehouse;
project_flock_kandang?: {
id: number;
project_flock_id: number;
kandang_id: number;
period: number;
project_flock?: {
id: number;
flock_name: string;
};
};
week?: number | null;
};
+3 -5
View File
@@ -5,6 +5,7 @@ import {
CreatedUser,
} from '@/types/api/api-general';
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
import { Kandang } from '@/types/api/master-data/kandang';
import { Warehouse } from '@/types/api/master-data/warehouse';
/**
@@ -61,7 +62,6 @@ export type BaseDelivery = {
avg_weight: number;
total_price: number;
vehicle_number: string;
weight_per_convertion: number;
};
export type MarketingProduct = {
@@ -110,8 +110,7 @@ export type BaseCreateMarketingPayload = {
export type BaseCreateMarketingProductPayload = {
vehicle_number: string;
warehouse_id?: string | number | undefined;
kandang_id?: string | number | undefined;
kandang_id: string | number | undefined;
product_warehouse_id: string | number | undefined;
unit_price: string | number | undefined;
total_weight: string | number | undefined;
@@ -137,8 +136,7 @@ export type CreateSalesOrderPayload = BaseCreateMarketingPayload & {
export type CreateSalesOrderProductPayload =
BaseCreateMarketingProductPayload & {
id?: number;
warehouse?: Warehouse | undefined;
kandang?: Warehouse | undefined;
kandang?: Kandang | undefined;
product_warehouse?: ProductWarehouse | undefined;
};
-3
View File
@@ -1,7 +1,6 @@
import { BaseMetadata } from '@/types/api/api-general';
import { BaseLocation } from '@/types/api/master-data/location';
import { BaseUser } from '@/types/api/user';
import { BaseDailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
export type BaseKandang = {
id: number;
@@ -11,7 +10,6 @@ export type BaseKandang = {
capacity: number;
pic: BaseUser;
project_flock_kandang_id?: number;
kandang_group: Pick<BaseDailyChecklistKandang, 'id' | 'name'>;
};
export type Kandang = BaseMetadata & BaseKandang;
@@ -21,7 +19,6 @@ export type CreateKandangPayload = {
location_id: number;
capacity: number;
pic_id: number;
group_id: number;
};
export type UpdateKandangPayload = CreateKandangPayload;
-2
View File
@@ -74,8 +74,6 @@ export type ProjectFlockKandangLookup = {
available_quantity?: number;
population: number;
chick_in_date: string;
is_transition: boolean;
is_laying: boolean;
};
export type ProjectFlockAvailableQuantity = {
+2 -4
View File
@@ -49,13 +49,12 @@ export type BaseRecording = {
project_flock: ProjectFlock;
record_datetime: string;
day: number;
is_transition: boolean;
is_laying: boolean;
population_can_change: boolean;
transfer_executed: boolean;
} & ProductionMetrics;
export type RecordingDepletion = {
product_warehouse_id: number;
source_product_warehouse_id?: number;
qty: number;
product_warehouse: ProductWarehouse;
};
@@ -115,7 +114,6 @@ export type CreateGrowingRecordingPayload = {
}[];
depletions?: {
product_warehouse_id?: number;
source_product_warehouse_id?: number;
qty?: number;
}[];
};
-6
View File
@@ -144,9 +144,3 @@ export type DeletePurchaseRequestItemPayload = {
};
export type UpdatePurchaseRequestPayload = CreatePurchaseRequestPayload;
export type PurchaseFilter = {
poDate: string;
category: string[];
status: string[];
};