@@ -2103,9 +2376,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{type === 'detail' && initialValues && (
{/* FCR Section */}
@@ -2196,8 +2467,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{/* Egg Production Section - Only for LAYING category */}
{type === 'detail' &&
initialValues &&
- initialValues.project_flock?.project_flock_category ===
- 'LAYING' && (
+ initialValues.is_laying && (
@@ -2292,239 +2562,293 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
)}
- {/* Stocks Table */}
-
-
-
-
-
- {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
-
- 0
- }
- onChange={(
- e: React.ChangeEvent
- ) => {
- if (e.target.checked) {
- setSelectedStocks(
- formik.values.stocks?.map((_, idx) => idx) ?? []
- );
- } else {
- setSelectedStocks([]);
- }
- }}
- classNames={{
- wrapper: 'flex justify-center',
- checkbox: 'checkbox checkbox-sm',
- }}
- />
-
- )}
-
- Persediaan
-
- *
-
-
-
- Jumlah Pakai
-
- *
-
-
- {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
- Action
- )}
-
-
-
- {formik.values.stocks?.map((stock, idx) => (
-
+ {/* Stocks Table - Only show if can edit stock or has data */}
+ {(recordingRestriction.canEditStock ||
+ (type === 'detail' && formik.values.stocks?.length > 0)) && (
+
+
+
+
+
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
-
+
0
+ }
onChange={(
e: React.ChangeEvent
) => {
if (e.target.checked) {
- setSelectedStocks([...selectedStocks, idx]);
- } else {
setSelectedStocks(
- selectedStocks.filter((i) => i !== idx)
+ formik.values.stocks?.map((_, idx) => idx) ??
+ []
);
+ } else {
+ setSelectedStocks([]);
}
}}
+ disabled={!recordingRestriction.canEditStock}
classNames={{
wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm',
}}
/>
-
+
)}
-
-
- product.value === stock.product_warehouse_id
- ) || null
- }
- onChange={(selectedOption) => {
- const option = selectedOption as OptionType | null;
- formik.setFieldValue(
- `stocks.${idx}.product_warehouse_id`,
- option?.value || 0
- );
- }}
- options={getAvailableStockProductOptions(idx)}
- placeholder={
- !formik.values.project_flock_kandang_id
- ? 'Pilih kandang terlebih dahulu'
- : 'Pilih Produk'
- }
- isLoading={isLoadingStockProducts}
- onMenuScrollToBottom={loadMoreStockProducts}
- isError={
- isRepeaterInputError(
- 'stocks',
- 'product_warehouse_id',
- idx
- ).isError
- }
- errorMessage={
- isRepeaterInputError(
- 'stocks',
- 'product_warehouse_id',
- idx
- ).errorMessage
- }
- className={{
- wrapper: 'w-full min-w-48',
- }}
- isSearchable
- isDisabled={
- type === 'detail' ||
- !formik.values.project_flock_kandang_id
- }
- isClearable={type !== 'detail'}
- inputPrefix={
- stock.product_warehouse_id
- ? getProductFlagBadgeAdornment(
- stock.product_warehouse_id
- )
- : undefined
- }
- />
-
-
-
-
- {getStockUsageAdornment(idx)}
-
-
+
+ Persediaan
+
+ *
+
+
+
+ Jumlah Pakai
+
+ *
+
+
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
-
-
- removeStock(idx)}
- >
-
-
-
-
+ Action
)}
- ))}
-
-
-
- {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
-
- {selectedStocks.length > 0 && (
-
-
- Hapus Terpilih ({selectedStocks.length})
-
- )}
-
-
- Tambah Stok
-
+
+
+ {formik.values.stocks?.map((stock, idx) => (
+
+ {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
+
+
+ ) => {
+ if (e.target.checked) {
+ setSelectedStocks([...selectedStocks, idx]);
+ } else {
+ setSelectedStocks(
+ selectedStocks.filter((i) => i !== idx)
+ );
+ }
+ }}
+ disabled={!recordingRestriction.canEditStock}
+ classNames={{
+ wrapper: 'flex justify-center',
+ checkbox: 'checkbox checkbox-sm',
+ }}
+ />
+
+ )}
+
+
+ product.value === stock.product_warehouse_id
+ ) || null
+ }
+ onChange={(selectedOption) => {
+ const option =
+ selectedOption as OptionType | null;
+ formik.setFieldValue(
+ `stocks.${idx}.product_warehouse_id`,
+ option?.value || 0
+ );
+ }}
+ options={getAvailableStockProductOptions(idx)}
+ placeholder={
+ !formik.values.project_flock_kandang_id
+ ? 'Pilih kandang terlebih dahulu'
+ : 'Pilih Produk'
+ }
+ isLoading={isLoadingStockProducts}
+ onMenuScrollToBottom={loadMoreStockProducts}
+ isError={
+ isRepeaterInputError(
+ 'stocks',
+ 'product_warehouse_id',
+ idx
+ ).isError
+ }
+ errorMessage={
+ isRepeaterInputError(
+ 'stocks',
+ 'product_warehouse_id',
+ idx
+ ).errorMessage
+ }
+ className={{
+ wrapper: 'w-full min-w-48',
+ }}
+ isSearchable
+ isDisabled={
+ type === 'detail' ||
+ !formik.values.project_flock_kandang_id ||
+ !recordingRestriction.canEditStock
+ }
+ isClearable={type !== 'detail'}
+ inputPrefix={
+ stock.product_warehouse_id
+ ? getProductFlagBadgeAdornment(
+ stock.product_warehouse_id
+ )
+ : undefined
+ }
+ />
+
+
+
+
+ {getStockUsageAdornment(idx)}
+
+
+ {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
+
+
+ removeStock(idx)}
+ disabled={!recordingRestriction.canEditStock}
+ >
+
+
+
+
+ )}
+
+ ))}
+
+
- )}
-
+ {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
+
+ {selectedStocks.length > 0 &&
+ recordingRestriction.canEditStock && (
+
+
+ Hapus Terpilih ({selectedStocks.length})
+
+ )}
+
+
+
+ Tambah Stok
+
+
+
+ )}
+
+ )}
+
+ {/* Transition Warning Banner -- MOVED UP -- */}
+ {isTransitionPeriod && (
+
+
+
+ {isLayingCategory
+ ? 'Masa Transisi Laying: Hanya Deplesi yang dapat diisi. Stock (Pakan/OVK) tidak dapat diinput.'
+ : 'Masa Transisi Growing: Hanya Stock (Pakan/OVK) yang dapat diisi. Deplesi tidak dapat diinput.'}
+
+
+ )}
+
+ {/* Locked Recording Warning */}
+ {recordingRestriction.isLocked && (
+
+
+ {recordingRestriction.lockReason}
+
+ )}
{/* Depletions Table */}
{((type as 'add' | 'edit' | 'detail') !== 'detail' ||
@@ -2532,7 +2856,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
@@ -2562,6 +2890,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
setSelectedDepletions([]);
}
}}
+ disabled={!recordingRestriction.canEditDepletion}
classNames={{
wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm',
@@ -2598,6 +2927,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
);
}
}}
+ disabled={!recordingRestriction.canEditDepletion}
classNames={{
wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm',
@@ -2623,7 +2953,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
);
}}
options={getAvailableDepletionProductOptions(idx)}
- placeholder='Pilih Kondisi'
+ placeholder={
+ !formik.values.project_flock_kandang_id
+ ? 'Pilih kandang terlebih dahulu'
+ : 'Pilih Kondisi'
+ }
isLoading={isLoadingDepletionProducts}
onMenuScrollToBottom={loadMoreDepletionProducts}
isError={
@@ -2640,7 +2974,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
idx
).errorMessage
}
- isDisabled={type === 'detail'}
+ isDisabled={
+ type === 'detail' ||
+ !formik.values.project_flock_kandang_id ||
+ !recordingRestriction.canEditDepletion
+ }
className={{
wrapper: 'w-full min-w-48',
}}
@@ -2679,7 +3017,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
)
: null
}
- disabled={type === 'detail'}
+ disabled={
+ type === 'detail' ||
+ !recordingRestriction.canEditDepletion
+ }
/>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
@@ -2689,6 +3030,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
type='button'
color='error'
onClick={() => removeDepletion(idx)}
+ disabled={
+ !recordingRestriction.canEditDepletion
+ }
>
{
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
- {selectedDepletions.length > 0 && (
+ {selectedDepletions.length > 0 &&
+ recordingRestriction.canEditDepletion && (
+
+
+ Hapus Terpilih ({selectedDepletions.length})
+
+ )}
+
-
- Hapus Terpilih ({selectedDepletions.length})
+
+ Tambah Depletion
- )}
-
-
- Tambah Depletion
-
+
)}
@@ -2847,7 +3205,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
);
}}
options={getAvailableEggProductOptions(idx)}
- placeholder='Pilih Kondisi Telur'
+ placeholder={
+ !formik.values.project_flock_kandang_id
+ ? 'Pilih kandang terlebih dahulu'
+ : 'Pilih Kondisi Telur'
+ }
isLoading={isLoadingEggProducts}
onMenuScrollToBottom={loadMoreEggProducts}
isError={
@@ -2864,7 +3226,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
idx
).errorMessage
}
- isDisabled={type === 'detail'}
+ isDisabled={
+ type === 'detail' ||
+ !formik.values.project_flock_kandang_id
+ }
className={{
wrapper: 'w-full min-w-48',
}}
@@ -2969,6 +3334,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
color='success'
onClick={addEgg}
className='w-fit'
+ disabled={!formik.values.project_flock_kandang_id}
>
Tambah Telur
diff --git a/src/components/pages/production/recording/recording-utils.ts b/src/components/pages/production/recording/recording-utils.ts
new file mode 100644
index 00000000..fe7d07f8
--- /dev/null
+++ b/src/components/pages/production/recording/recording-utils.ts
@@ -0,0 +1,73 @@
+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',
+ };
+};
diff --git a/src/config/constant.ts b/src/config/constant.ts
index 99c5ff9d..99594b65 100644
--- a/src/config/constant.ts
+++ b/src/config/constant.ts
@@ -20,6 +20,7 @@ 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: [
{
@@ -66,6 +67,11 @@ 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',
@@ -549,6 +555,12 @@ 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'],
diff --git a/src/config/route-permission.ts b/src/config/route-permission.ts
index dc638b29..8c65a611 100644
--- a/src/config/route-permission.ts
+++ b/src/config/route-permission.ts
@@ -21,6 +21,9 @@ export const ROUTE_PERMISSIONS: Record
= {
'/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
diff --git a/src/figma-make/components/base/multi-select.tsx b/src/figma-make/components/base/multi-select.tsx
index 63ebdd36..656073c6 100644
--- a/src/figma-make/components/base/multi-select.tsx
+++ b/src/figma-make/components/base/multi-select.tsx
@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
-import { Check, ChevronsUpDown, X } from 'lucide-react';
+import { Check, ChevronsUpDown, X, Loader2 } from 'lucide-react';
import { cn } from '@/lib/helper';
import { Button } from '@/figma-make/components/base/button';
import {
@@ -29,6 +29,8 @@ interface MultiSelectProps {
selected: string[];
onChange: (selected: string[]) => void;
onSearchChange?: (value: string) => void;
+ onLoadMore?: () => void;
+ isLoadingMore?: boolean;
placeholder?: string;
className?: string;
disabled?: boolean;
@@ -39,6 +41,8 @@ export function MultiSelect({
selected,
onChange,
onSearchChange,
+ onLoadMore,
+ isLoadingMore,
placeholder = 'Select items...',
className,
disabled,
@@ -115,7 +119,18 @@ export function MultiSelect({
onValueChange={onSearchChange}
/>
No item found.
-
+ {
+ const target = e.currentTarget;
+ if (
+ target.scrollHeight - target.scrollTop <=
+ target.clientHeight + 1
+ ) {
+ onLoadMore?.();
+ }
+ }}
+ >
{options.map((option) => (
))}
+ {isLoadingMore && (
+
+
+
+ )}
diff --git a/src/figma-make/components/base/select.tsx b/src/figma-make/components/base/select.tsx
index 625cf3d7..16725c04 100644
--- a/src/figma-make/components/base/select.tsx
+++ b/src/figma-make/components/base/select.tsx
@@ -55,7 +55,11 @@ function SelectContent({
children,
position = 'popper',
...props
-}: React.ComponentProps) {
+}: React.ComponentProps & {
+ onScroll?: React.UIEventHandler;
+}) {
+ const { onScroll, ...restProps } = props;
+
return (
{children}
diff --git a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx
index 601025ad..1a9b4406 100644
--- a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx
+++ b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx
@@ -2,7 +2,16 @@
import * as React from 'react';
import { useState, useEffect } from 'react';
-import { Plus, X, Save, Send, Info, FilePlus, ListChecks } from 'lucide-react';
+import {
+ Plus,
+ X,
+ Save,
+ Send,
+ Info,
+ FilePlus,
+ ListChecks,
+ Loader2,
+} 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';
@@ -26,7 +35,6 @@ 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';
@@ -43,6 +51,7 @@ 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 = [
@@ -86,16 +95,11 @@ export function DailyChecklistContent() {
searchParams.get('category') || ''
);
- const { options: kandangOptions } = useSelect(
- KandangApi.basePath,
- 'id',
- 'name',
- 'search',
- {
- page: '1',
- limit: '100',
- }
- );
+ const {
+ options: kandangOptions,
+ isLoadingMore: isLoadingMoreKandang,
+ loadMore: loadMoreKandang,
+ } = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
const { data: phases } = useSWR<
BaseApiResponse,
@@ -168,6 +172,16 @@ export function DailyChecklistContent() {
const [documents, setDocuments] = useState([]);
const [deletedDocumentIds, setDeletedDocumentIds] = useState([]);
+ const handleKandangScroll = (e: React.UIEvent) => {
+ 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());
@@ -994,7 +1008,7 @@ export function DailyChecklistContent() {
>
-
+
{kandangOptions.map((kandang) => (
))}
+
+ {isLoadingMoreKandang && (
+
+
+
+ )}
diff --git a/src/figma-make/components/pages/dashboard/Dashboard.tsx b/src/figma-make/components/pages/dashboard/Dashboard.tsx
index a924c2b3..f3e4b6d0 100644
--- a/src/figma-make/components/pages/dashboard/Dashboard.tsx
+++ b/src/figma-make/components/pages/dashboard/Dashboard.tsx
@@ -16,7 +16,7 @@ import {
SelectValue,
} from '@/figma-make/components/base/select';
import { Badge } from '@/figma-make/components/base/badge';
-import { Users, AlertCircle, Info } from 'lucide-react';
+import { Users, AlertCircle, Info, Loader2 } from 'lucide-react';
import { DateRangePicker } from '@/figma-make/components/base/date-range-picker';
import {
BarChart,
@@ -36,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)
@@ -77,16 +77,20 @@ export function Dashboard() {
httpClientFetcher
);
- const { options: kandangOptions } = useSelect(
- KandangApi.basePath,
- 'id',
- 'name',
- 'search',
- {
- page: '1',
- limit: '100',
+ const {
+ options: kandangOptions,
+ loadMore: loadMoreKandang,
+ isLoadingMore: isLoadingMoreKandang,
+ } = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
+
+ const handleKandangScroll = (e: React.UIEvent