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
26 changed files with 235 additions and 1350 deletions
+2 -30
View File
@@ -15,7 +15,7 @@ default:
# ========================================================== # ==========================================================
.build_template: &build_template .build_template: &build_template
stage: build stage: build
image: public.ecr.aws/docker/library/node:20-alpine image: node:20-alpine
cache: cache:
key: npm-cache key: npm-cache
paths: paths:
@@ -56,7 +56,7 @@ default:
.deploy_template: &deploy_template .deploy_template: &deploy_template
stage: deploy stage: deploy
image: image:
name: public.ecr.aws/aws-cli/aws-cli:latest name: amazon/aws-cli:latest
entrypoint: ['/bin/sh', '-c'] entrypoint: ['/bin/sh', '-c']
script: script:
- set -e - set -e
@@ -183,31 +183,3 @@ deploy:staging:
environment: environment:
name: staging name: staging
url: https://stg-lti-erp.mbugroup.id url: https://stg-lti-erp.mbugroup.id
# ==========================================================
# ====== STAGING (Branch production) ======
# ==========================================================
build:production:
<<: *build_template
rules:
- if: '$CI_COMMIT_BRANCH == "production"'
environment:
name: staging
variables:
NEXT_PUBLIC_LTI_URL: 'https://lti-erp.mbugroup.id'
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://auth-erp.mbugroup.id'
NEXT_PUBLIC_API_BASE_URL: 'https://api-lti.mbugroup.id/api'
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
deploy:production:
<<: *deploy_template
needs: ['build:production']
rules:
- if: '$CI_COMMIT_BRANCH == "production"'
variables:
S3_BUCKET: 'production-lti-erp.mbugroup.id'
AWS_REGION: 'ap-southeast-3'
CLOUDFRONT_DISTRIBUTION_ID: 'E1SSLXKYYITASJ'
environment:
name: staging
url: https://lti-erp.mbugroup.id
+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 RUN apk add --no-cache git bash build-base curl
@@ -22,4 +22,4 @@ RUN mkdir -p .next/server/app/_next && \
EXPOSE 3000 EXPOSE 3000
CMD ["npx", "serve", ".next/server/app", "-l", "3000"] CMD ["npx", "serve", ".next/server/app", "-l", "3000"]
+1 -2
View File
@@ -8,8 +8,7 @@
"start": "next start", "start": "next start",
"lint": "eslint", "lint": "eslint",
"prepare": "husky", "prepare": "husky",
"format": "prettier --write .", "format": "prettier --write ."
"pre-commit": "npm run format && npm run lint && npx tsc --noEmit && npm run build"
}, },
"dependencies": { "dependencies": {
"@react-pdf/renderer": "^4.3.1", "@react-pdf/renderer": "^4.3.1",
@@ -1,11 +0,0 @@
import { MasterKandangContent } from '@/figma-make/components/pages/master-data/kandang/MasterKandangContent';
const MasterKandangPage = () => {
return (
<section className='w-full'>
<MasterKandangContent />
</section>
);
};
export default MasterKandangPage;
@@ -314,10 +314,6 @@ const KandangsTable = () => {
accessorFn: (row) => row.pic?.name ?? '-', accessorFn: (row) => row.pic?.name ?? '-',
header: 'PIC', header: 'PIC',
}, },
{
accessorFn: (row) => row.kandang_group?.name ?? '-',
header: 'Kandang Group',
},
{ {
header: 'Aksi', header: 'Aksi',
cell: (props: CellContext<Kandang, unknown>) => { cell: (props: CellContext<Kandang, unknown>) => {
@@ -1,4 +1,3 @@
import { OptionType } from '@/components/input/SelectInput';
import * as Yup from 'yup'; import * as Yup from 'yup';
type KandangFormSchemaType = { type KandangFormSchemaType = {
@@ -20,7 +19,6 @@ type KandangFormSchemaType = {
} }
| undefined | undefined
| null; | null;
group?: OptionType;
}; };
export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> = export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
@@ -44,11 +42,6 @@ export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
value: Yup.number().min(1).required(), value: Yup.number().min(1).required(),
label: Yup.string().required(), label: Yup.string().required(),
}).nullable(), }).nullable(),
group: Yup.object({
value: Yup.number().min(1).required('Kandang Grup wajib diisi!'),
label: Yup.string().required('Kandang Grup wajib diisi!'),
}).required('Kandang Grup wajib diisi!'),
}); });
export const UpdateKandangFormSchema = KandangFormSchema; export const UpdateKandangFormSchema = KandangFormSchema;
@@ -2,7 +2,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { getIn, useFormik } from 'formik'; import { useFormik } from 'formik';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
@@ -34,8 +34,6 @@ import NumberInput from '@/components/input/NumberInput';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import AlertErrorList from '@/components/helper/form/FormErrors'; import AlertErrorList from '@/components/helper/form/FormErrors';
import { User } from '@/types/api/api-general'; import { User } from '@/types/api/api-general';
import { DailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
interface KandangFormProps { interface KandangFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -98,12 +96,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
label: initialValues.pic.name, label: initialValues.pic.name,
} }
: null, : null,
group: initialValues?.kandang_group
? {
value: initialValues.kandang_group.id,
label: initialValues.kandang_group.name,
}
: undefined,
}; };
}, [initialValues]); }, [initialValues]);
@@ -119,7 +111,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
location_id: values.locationId!, location_id: values.locationId!,
capacity: values.capacity ? parseInt(values.capacity.toString()) : 0, capacity: values.capacity ? parseInt(values.capacity.toString()) : 0,
pic_id: values.picId!, pic_id: values.picId!,
group_id: values.group?.value as number,
}; };
switch (type) { switch (type) {
@@ -171,23 +162,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
formik.setFieldValue('picId', (val as OptionType)?.value); formik.setFieldValue('picId', (val as OptionType)?.value);
}; };
// Kandang Group
const {
setInputValue: setKandangGroupSelectInputValue,
options: kandangGroupOptions,
isLoadingOptions: isLoadingKandangGroupOptions,
loadMore: loadMoreKandangGroups,
} = useSelect<DailyChecklistKandang>(
DailyChecklistKandangApi.basePath,
'id',
'name'
);
const kandangGroupChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('group', true);
formik.setFieldValue('group', val);
};
const deleteKandangClickHandler = () => { const deleteKandangClickHandler = () => {
deleteModal.openModal(); deleteModal.openModal();
}; };
@@ -295,24 +269,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
isDisabled={type === 'detail'} isDisabled={type === 'detail'}
isClearable isClearable
/> />
<SelectInput
required
label='Kandang Group'
value={formik.values.group ?? undefined}
onChange={kandangGroupChangeHandler}
options={kandangGroupOptions}
onInputChange={setKandangGroupSelectInputValue}
onMenuScrollToBottom={loadMoreKandangGroups}
isLoading={isLoadingKandangGroupOptions}
isError={formik.touched.group && Boolean(formik.errors.group)}
errorMessage={
getIn(formik.errors.group, 'value') ??
(formik.errors.group as string)
}
isDisabled={type === 'detail'}
isClearable
/>
</div> </div>
<div className='flex flex-row justify-between gap-2 flex-wrap'> <div className='flex flex-row justify-between gap-2 flex-wrap'>
@@ -21,7 +21,6 @@ import SelectInput, { useSelect } from '@/components/input/SelectInput';
import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import PopoverButton from '@/components/popover/PopoverButton'; import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent'; import PopoverContent from '@/components/popover/PopoverContent';
import Tooltip from '@/components/Tooltip';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { AreaApi } from '@/services/api/master-data'; import { AreaApi } from '@/services/api/master-data';
import { LocationApi } from '@/services/api/master-data'; import { LocationApi } from '@/services/api/master-data';
@@ -37,7 +36,6 @@ import {
import RecordingTableSkeleton from '@/components/pages/production/recording/skeleton/RecordingTableSkeleton'; import RecordingTableSkeleton from '@/components/pages/production/recording/skeleton/RecordingTableSkeleton';
import Table from '@/components/Table'; import Table from '@/components/Table';
import { type Recording } from '@/types/api/production/recording'; import { type Recording } from '@/types/api/production/recording';
import { getRecordingRestriction } from './recording-utils';
import { RecordingApi } from '@/services/api/production'; import { RecordingApi } from '@/services/api/production';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
@@ -107,75 +105,30 @@ const RowOptionsMenu = ({
}; };
const isRecordingEditable = (recording: Recording) => { const isRecordingEditable = (recording: Recording) => {
const isGrowingCategory = if (recording.project_flock?.project_flock_category === 'GROWING') {
recording.project_flock?.project_flock_category === 'GROWING'; if (recording.transfer_executed) {
const isGrowingLockedByLaying = isGrowingCategory && recording.is_laying; return false;
if (isGrowingLockedByLaying) { }
return false; return recording.population_can_change === true;
}
const currentIsLaying =
recording.project_flock?.project_flock_category === 'LAYING';
const restriction = getRecordingRestriction(
recording.is_laying,
recording.is_transition,
currentIsLaying
);
if (restriction.isLocked) {
return false;
} }
return true; return true;
}; };
const getRecordingRestrictionInfo = (recording: Recording) => {
const isGrowingCategory =
recording.project_flock?.project_flock_category === 'GROWING';
const isGrowingLockedByLaying = isGrowingCategory && recording.is_laying;
if (isGrowingLockedByLaying) {
return {
canEditStock: false,
canEditDepletion: false,
canEditEgg: false,
isLocked: true,
lockReason:
'Recording Growing tidak dapat diubah karena sudah masuk fase laying dan dipakai pada recording laying',
};
}
const currentIsLaying =
recording.project_flock?.project_flock_category === 'LAYING';
return getRecordingRestriction(
recording.is_laying,
recording.is_transition,
currentIsLaying
);
};
const isApproved = isRecordingApproved(props.row.original); const isApproved = isRecordingApproved(props.row.original);
const isRejected = isRecordingRejected(props.row.original); const isRejected = isRecordingRejected(props.row.original);
const isEditable = isRecordingEditable(props.row.original); const isEditable = isRecordingEditable(props.row.original);
const restrictionInfo = getRecordingRestrictionInfo(props.row.original);
return ( return (
<div className='relative'> <div className='relative'>
<Tooltip <PopoverButton
content={restrictionInfo.isLocked ? restrictionInfo.lockReason : ''} tabIndex={0}
position='top' variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
> >
<PopoverButton <Icon icon='material-symbols:more-vert' width={16} height={16} />
tabIndex={0} </PopoverButton>
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>
<PopoverContent <PopoverContent
id={popoverId} id={popoverId}
@@ -210,7 +163,7 @@ const RowOptionsMenu = ({
</Button> </Button>
</RequirePermission> </RequirePermission>
)} )}
{!restrictionInfo.isLocked && !isApproved && !isRejected && ( {!isApproved && !isRejected && (
<RequirePermission permissions='lti.production.recording.approve'> <RequirePermission permissions='lti.production.recording.approve'>
<Button <Button
onClick={() => { onClick={() => {
@@ -226,7 +179,7 @@ const RowOptionsMenu = ({
</Button> </Button>
</RequirePermission> </RequirePermission>
)} )}
{!restrictionInfo.isLocked && !isApproved && !isRejected && ( {!isApproved && !isRejected && (
<RequirePermission permissions='lti.production.recording.approve'> <RequirePermission permissions='lti.production.recording.approve'>
<Button <Button
onClick={() => { onClick={() => {
@@ -607,17 +560,12 @@ const RecordingTable = () => {
const singleDeleteHandler = async () => { const singleDeleteHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
const response = await RecordingApi.delete(selectedRecording?.id as number); await RecordingApi.delete(selectedRecording?.id as number);
refreshRecordings();
singleDeleteModal.closeModal(); singleDeleteModal.closeModal();
toast.success('Successfully delete Recording!');
setIsDeleteLoading(false); setIsDeleteLoading(false);
if (isResponseSuccess(response)) {
toast.success(response?.message || 'Successfully delete Recording!');
refreshRecordings();
} else {
toast.error(response?.message || 'Failed to delete Recording');
}
}; };
const approveHandler = async (notes: string) => { const approveHandler = async (notes: string) => {
@@ -813,21 +761,11 @@ const RecordingTable = () => {
{ {
header: 'Kategori', header: 'Kategori',
cell: (props) => { cell: (props) => {
const isTransition = props.row.original.is_transition;
const category = const category =
props.row.original.project_flock?.project_flock_category || props.row.original.project_flock?.project_flock_category;
'GROWING'; if (!category) return '-';
const color = category === 'LAYING' ? 'info' : 'warning'; const color = category === 'LAYING' ? 'info' : 'warning';
return ( return <StatusBadge color={color} text={formatTitleCase(category)} />;
<div className='flex flex-col gap-1'>
<StatusBadge color={color} text={formatTitleCase(category)} />
{isTransition && (
<span className='text-xs text-warning font-medium'>
(Transisi)
</span>
)}
</div>
);
}, },
}, },
{ {
@@ -29,8 +29,8 @@ type RecordingGrowingFormSchemaType = {
} | null; } | null;
project_flock_kandang_id: number; project_flock_kandang_id: number;
stocks: { stocks: {
product_warehouse_id: number; product_warehouse_id?: number;
qty: number | string; qty?: number | string;
}[]; }[];
depletions: { depletions: {
product_warehouse_id?: number; product_warehouse_id?: number;
@@ -73,6 +73,18 @@ const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
.typeError('Jumlah penggunaan harus berupa angka!'), .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!'),
});
const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({ const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({
product_warehouse_id: Yup.number() product_warehouse_id: Yup.number()
.optional() .optional()
@@ -90,7 +102,9 @@ const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
weight: Yup.number().optional().typeError('Berat telur harus berupa angka!'), 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({ Yup.object({
record_date: Yup.string() record_date: Yup.string()
.required('Tanggal recording wajib diisi!') .required('Tanggal recording wajib diisi!')
@@ -150,20 +164,24 @@ export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSc
return true; return true;
} }
), ),
stocks: Yup.array() stocks: isTransitioningToLaying
.of(StockObjectSchema) ? Yup.array().of(OptionalStockObjectSchema).default([])
.min(1, 'Minimal harus ada 1 data stok!') : Yup.array()
.required('Data stok wajib diisi!'), .of(StockObjectSchema)
.min(1, 'Minimal harus ada 1 data stok!')
.required('Data stok wajib diisi!'),
depletions: Yup.array().of(DepletionObjectSchema).default([]), depletions: Yup.array().of(DepletionObjectSchema).default([]),
}); });
export const RecordingLayingFormSchema: Yup.ObjectSchema<RecordingLayingFormSchemaType> = export const RecordingLayingFormSchema: Yup.ObjectSchema<RecordingLayingFormSchemaType> =
RecordingGrowingFormSchema.shape({ RecordingGrowingFormSchema().shape({
eggs: Yup.array().of(EggObjectSchema).default([]), eggs: Yup.array().of(EggObjectSchema).default([]),
}); });
export const UpdateRecordingGrowingFormSchema = export const UpdateRecordingGrowingFormSchema = (
RecordingGrowingFormSchema.shape({ isTransitioningToLaying = false
) =>
RecordingGrowingFormSchema(isTransitioningToLaying).shape({
location_id: Yup.number().nullable().optional(), location_id: Yup.number().nullable().optional(),
project_flock_id: Yup.number().nullable().optional(), project_flock_id: Yup.number().nullable().optional(),
kandang_id: Yup.number().nullable().optional(), kandang_id: Yup.number().nullable().optional(),
@@ -193,10 +211,13 @@ export const UpdateRecordingLayingFormSchema = RecordingLayingFormSchema.shape({
.required('Project Flock Kandang wajib diisi!'), .required('Project Flock Kandang wajib diisi!'),
}); });
export type RecordingGrowingFormValues = Yup.InferType< type RecordingGrowingFormSchemaFn = ReturnType<
typeof RecordingGrowingFormSchema typeof RecordingGrowingFormSchema
>; >;
export type RecordingGrowingFormValues =
Yup.InferType<RecordingGrowingFormSchemaFn>;
export type RecordingLayingFormValues = Yup.InferType< export type RecordingLayingFormValues = Yup.InferType<
typeof RecordingLayingFormSchema typeof RecordingLayingFormSchema
>; >;
@@ -70,7 +70,7 @@ import {
} from '@/components/pages/production/recording/form/RecordingForm.schema'; } from '@/components/pages/production/recording/form/RecordingForm.schema';
import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
import { formatDate, formatNumber, cn } from '@/lib/helper'; import { formatDate, formatNumber } from '@/lib/helper';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import ApprovalSteps, { import ApprovalSteps, {
useApprovalSteps, useApprovalSteps,
@@ -80,7 +80,6 @@ import {
LAYING_RECORDING_APPROVAL_LINE, LAYING_RECORDING_APPROVAL_LINE,
} from '@/config/approval-line'; } from '@/config/approval-line';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import { getRecordingRestriction } from '../recording-utils';
interface RecordingFormProps { interface RecordingFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -243,23 +242,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const [isProductionStandardModalOpen, setIsProductionStandardModalOpen] = const [isProductionStandardModalOpen, setIsProductionStandardModalOpen] =
useState(false); useState(false);
const calculateWeek = useCallback(
(day: number): number => {
if (
productionStandards?.details &&
productionStandards.details.length > 0
) {
const firstWeek = productionStandards.details[0].week;
const weekOffset = Math.ceil(day / 7) - 1;
return firstWeek + weekOffset;
}
return Math.ceil(day / 7);
},
[productionStandards]
);
useEffect(() => { useEffect(() => {
const checkProductionStandardModalOpen = () => { const checkProductionStandardModalOpen = () => {
const isOpen = productionStandardModal.ref.current?.open || false; const isOpen = productionStandardModal.ref.current?.open || false;
@@ -290,6 +272,16 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
return recording?.approval?.action === 'REJECTED'; return recording?.approval?.action === 'REJECTED';
}, []); }, []);
const isRecordingEditable = useCallback((recording?: Recording) => {
if (recording?.project_flock?.project_flock_category === 'GROWING') {
if (recording?.transfer_executed) {
return false;
}
return recording?.population_can_change === true;
}
return true;
}, []);
// ===== PAYLOAD CREATION HELPERS ===== // ===== PAYLOAD CREATION HELPERS =====
const createGrowingPayload = useCallback( const createGrowingPayload = useCallback(
(values: RecordingGrowingFormValues) => { (values: RecordingGrowingFormValues) => {
@@ -388,17 +380,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
if (!initialValues?.id) return; if (!initialValues?.id) return;
setIsDeleteLoading(true); setIsDeleteLoading(true);
const response = await RecordingApi.delete(initialValues.id); await RecordingApi.delete(initialValues.id);
deleteModal.closeModal(); deleteModal.closeModal();
toast.success('Successfully delete Recording!');
setIsDeleteLoading(false); setIsDeleteLoading(false);
router.push('/production/recording');
if (isResponseSuccess(response)) {
toast.success(response?.message || 'Successfully delete Recording!');
router.push('/production/recording');
} else {
toast.error(response?.message || 'Failed to delete Recording');
}
}, [deleteModal, initialValues?.id, router]); }, [deleteModal, initialValues?.id, router]);
// ===== API DATA FETCHING ===== // ===== API DATA FETCHING =====
@@ -420,15 +406,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
}); });
const projectFlockKandangLookupUrl = useMemo(() => { const projectFlockKandangLookupUrl = useMemo(() => {
if (!selectedProjectFlock || !selectedKandang || !selectedRecordDate) if (!selectedProjectFlock || !selectedKandang) return null;
return null;
const params = new URLSearchParams({ const params = new URLSearchParams({
project_flock_id: selectedProjectFlock.value.toString(), project_flock_id: selectedProjectFlock.value.toString(),
kandang_id: selectedKandang.value.toString(), kandang_id: selectedKandang.value.toString(),
record_date: selectedRecordDate,
}); });
return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`; return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`;
}, [selectedProjectFlock, selectedKandang, selectedRecordDate]); }, [selectedProjectFlock, selectedKandang]);
const { data: projectFlockKandangLookupData } = useSWR( const { data: projectFlockKandangLookupData } = useSWR(
projectFlockKandangLookupUrl, projectFlockKandangLookupUrl,
@@ -460,24 +444,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
() => ProductionStandardApi.getSingle(productionStandardId!) () => ProductionStandardApi.getSingle(productionStandardId!)
); );
const { data: productionStandardForAdd } = useSWR(
type === 'add' && productionStandardId
? `production-standard-add-${productionStandardId}`
: null,
() => ProductionStandardApi.getSingle(productionStandardId!)
);
useEffect(() => { useEffect(() => {
if (productionStandard?.status === 'success') { if (productionStandard?.status === 'success') {
setProductionStandards( setProductionStandards(
productionStandard.data as ProductionStandard | null productionStandard.data as ProductionStandard | null
); );
} else if (productionStandardForAdd?.status === 'success') {
setProductionStandards(
productionStandardForAdd.data as ProductionStandard | null
);
} }
}, [productionStandard, productionStandardForAdd]); }, [productionStandard]);
const projectFlockKandangDetailUrl = useMemo(() => { const projectFlockKandangDetailUrl = useMemo(() => {
if ( if (
@@ -503,74 +476,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
? projectFlockKandangDetailData.data ? projectFlockKandangDetailData.data
: undefined; : undefined;
// ===== TRANSITION RESTRICTION LOGIC =====
const isTransitionPeriod = useMemo(() => {
return (
initialValues?.is_transition ??
projectFlockKandangLookup?.is_transition ??
false
);
}, [initialValues, projectFlockKandangLookup]);
const recordingRestriction = useMemo(() => {
// Determine isLaying primarily from transition flags.
let isLaying: boolean;
if (initialValues?.is_laying !== undefined) {
isLaying = initialValues.is_laying;
} else if (projectFlockKandangLookup?.is_laying !== undefined) {
isLaying = projectFlockKandangLookup.is_laying;
} else {
isLaying =
projectFlockKandangDetail?.project_flock?.category === 'LAYING' ||
false;
}
const isTransition =
initialValues?.is_transition ??
projectFlockKandangLookup?.is_transition ??
false;
const currentIsLaying =
projectFlockKandangDetail?.project_flock?.category === 'LAYING';
return getRecordingRestriction(
isLaying,
isTransition,
type === 'edit' ? currentIsLaying : undefined
);
}, [
initialValues,
projectFlockKandangLookup,
projectFlockKandangDetail,
type,
]);
const isRecordingEditable = useCallback((recording?: Recording) => {
if (!recording) return true;
const isGrowingCategory =
recording.project_flock?.project_flock_category === 'GROWING';
const isGrowingLockedByLaying = isGrowingCategory && recording.is_laying;
if (isGrowingLockedByLaying) {
return false;
}
const currentIsLaying =
recording.project_flock?.project_flock_category === 'LAYING';
const restriction = getRecordingRestriction(
recording.is_laying,
recording.is_transition,
currentIsLaying
);
if (restriction.isLocked) {
return false;
}
return true;
}, []);
const { const {
options: stockProductOptions, options: stockProductOptions,
rawData: stockProducts, rawData: stockProducts,
@@ -676,28 +581,23 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
return approvedProjectFlockKandangsData.data; return approvedProjectFlockKandangsData.data;
}, [approvedProjectFlockKandangsData]); }, [approvedProjectFlockKandangsData]);
const isLayingCategory = useMemo(() => { const isLayingCategory =
// Priority 1: initialValues (for edit/detail mode) initialValues?.project_flock?.project_flock_category === 'LAYING' ||
if (initialValues?.is_laying !== undefined) { projectFlockKandangLookup?.project_flock?.category === 'LAYING' ||
return initialValues.is_laying; projectFlockKandangDetail?.project_flock?.category === 'LAYING';
}
// Priority 2: projectFlockKandangLookup flag (for add mode) const isGrowingCategory =
if (projectFlockKandangLookup?.is_laying !== undefined) { initialValues?.project_flock?.project_flock_category === 'GROWING' ||
return projectFlockKandangLookup.is_laying; projectFlockKandangLookup?.project_flock?.category === 'GROWING' ||
} projectFlockKandangDetail?.project_flock?.category === 'GROWING';
// Priority 3: projectFlockKandangDetail (fallback for edit/detail mode) const isTransitioningToLaying = useMemo(() => {
if (!isGrowingCategory) return false;
return ( return (
projectFlockKandangDetail?.project_flock?.category === 'LAYING' || false initialValues?.population_can_change === true ||
initialValues?.transfer_executed === true
); );
}, [ }, [initialValues, isGrowingCategory]);
initialValues?.is_laying,
projectFlockKandangLookup,
projectFlockKandangDetail,
]);
const isGrowingCategory = !isLayingCategory;
const recordingApprovalLines = useMemo(() => { const recordingApprovalLines = useMemo(() => {
if (isLayingCategory) { if (isLayingCategory) {
@@ -1059,8 +959,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
} else { } else {
schema = schema =
type === 'edit' type === 'edit'
? UpdateRecordingGrowingFormSchema ? UpdateRecordingGrowingFormSchema(isTransitioningToLaying)
: RecordingGrowingFormSchema; : RecordingGrowingFormSchema(isTransitioningToLaying);
} }
return schema.clone().concat( return schema.clone().concat(
Yup.object().shape({ Yup.object().shape({
@@ -1407,8 +1307,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
setSelectedLocation(location); setSelectedLocation(location);
setSelectedProjectFlock(null); setSelectedProjectFlock(null);
setSelectedKandang(null); setSelectedKandang(null);
setProductionStandards(null);
setNextDayRecording(null);
if (duplicateErrorShown) { if (duplicateErrorShown) {
toast.dismiss(); toast.dismiss();
setDuplicateErrorShown(false); setDuplicateErrorShown(false);
@@ -1433,8 +1331,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
setSelectedProjectFlock(projectFlock); setSelectedProjectFlock(projectFlock);
setSelectedKandang(null); setSelectedKandang(null);
setProductionStandards(null);
setNextDayRecording(null);
if (duplicateErrorShown) { if (duplicateErrorShown) {
toast.dismiss(); toast.dismiss();
setDuplicateErrorShown(false); setDuplicateErrorShown(false);
@@ -1455,8 +1351,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
formik.setFieldValue('kandang_id', kandangId); formik.setFieldValue('kandang_id', kandangId);
setSelectedKandang(kandang); setSelectedKandang(kandang);
setProductionStandards(null);
setNextDayRecording(null);
if (duplicateErrorShown) { if (duplicateErrorShown) {
toast.dismiss(); toast.dismiss();
setDuplicateErrorShown(false); setDuplicateErrorShown(false);
@@ -1993,10 +1887,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<p className='font-semibold'> <p className='font-semibold'>
{type === 'add' {type === 'add'
? nextDayRecording ? nextDayRecording
? `Hari ke-${nextDayRecording.next_day} (Minggu ke-${calculateWeek(nextDayRecording.next_day)})` ? `Hari ke-${nextDayRecording.next_day} (Minggu ke-${Math.ceil(nextDayRecording.next_day / 7)})`
: '-' : '-'
: initialValues?.day : initialValues?.day
? `Hari ke-${initialValues.day} (Minggu ke-${calculateWeek(initialValues.day)})` ? `Hari ke-${initialValues.day} (Minggu ke-${Math.ceil(initialValues.day / 7)})`
: '-'} : '-'}
</p> </p>
</div> </div>
@@ -2070,18 +1964,18 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<div> <div>
<span className='text-sm text-gray-600'>Kategori</span> <span className='text-sm text-gray-600'>Kategori</span>
<p className='font-semibold'> <p className='font-semibold'>
{(() => { <Badge
const category = variant='soft'
initialValues.project_flock?.project_flock_category || color={
'GROWING'; initialValues.project_flock
const color = ?.project_flock_category === 'LAYING'
category === 'LAYING' ? 'info' : 'warning'; ? 'info'
return ( : 'warning'
<Badge variant='soft' color={color} size='sm'> }
{category} size='sm'
</Badge> >
); {initialValues.project_flock?.project_flock_category}
})()} </Badge>
</p> </p>
</div> </div>
<div> <div>
@@ -2217,7 +2111,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{type === 'detail' && initialValues && ( {type === 'detail' && initialValues && (
<div <div
className={`grid gap-6 mb-6 grid-cols-1 ${ className={`grid gap-6 mb-6 grid-cols-1 ${
initialValues.is_laying ? 'xl:grid-cols-3' : 'xl:grid-cols-2' initialValues.project_flock?.project_flock_category === 'LAYING'
? 'xl:grid-cols-3'
: 'xl:grid-cols-2'
}`} }`}
> >
{/* FCR Section */} {/* FCR Section */}
@@ -2308,7 +2204,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{/* Egg Production Section - Only for LAYING category */} {/* Egg Production Section - Only for LAYING category */}
{type === 'detail' && {type === 'detail' &&
initialValues && initialValues &&
initialValues.is_laying && ( initialValues.project_flock?.project_flock_category ===
'LAYING' && (
<div className='border border-gray-200 rounded-lg bg-white'> <div className='border border-gray-200 rounded-lg bg-white'>
<div className='px-4 py-3 border-b border-gray-200'> <div className='px-4 py-3 border-b border-gray-200'>
<span className='card-title font-bold text-xl'> <span className='card-title font-bold text-xl'>
@@ -2435,7 +2332,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
setSelectedStocks([]); setSelectedStocks([]);
} }
}} }}
disabled={!recordingRestriction.canEditStock}
classNames={{ classNames={{
wrapper: 'flex justify-center', wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm', checkbox: 'checkbox checkbox-sm',
@@ -2445,21 +2341,25 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
)} )}
<th> <th>
Persediaan Persediaan
<span {!isTransitioningToLaying && (
className='tooltip tooltip-error tooltip-bottom ' <span
data-tip='required' className='tooltip tooltip-error tooltip-bottom '
> data-tip='required'
<span className='text-error'>*</span> >
</span> <span className='text-error'>*</span>
</span>
)}
</th> </th>
<th> <th>
Jumlah Pakai Jumlah Pakai
<span {!isTransitioningToLaying && (
className='tooltip tooltip-error tooltip-bottom ' <span
data-tip='required' className='tooltip tooltip-error tooltip-bottom '
> data-tip='required'
<span className='text-error'>*</span> >
</span> <span className='text-error'>*</span>
</span>
)}
</th> </th>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<th>Action</th> <th>Action</th>
@@ -2485,7 +2385,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
); );
} }
}} }}
disabled={!recordingRestriction.canEditStock}
classNames={{ classNames={{
wrapper: 'flex justify-center', wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm', checkbox: 'checkbox checkbox-sm',
@@ -2495,7 +2394,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
)} )}
<td> <td>
<SelectInput <SelectInput
required required={!isTransitioningToLaying}
key={`stock-product-${idx}-${stock.product_warehouse_id}`} key={`stock-product-${idx}-${stock.product_warehouse_id}`}
value={ value={
unifiedStockProducts.find( unifiedStockProducts.find(
@@ -2538,8 +2437,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
isSearchable isSearchable
isDisabled={ isDisabled={
type === 'detail' || type === 'detail' ||
!formik.values.project_flock_kandang_id || !formik.values.project_flock_kandang_id
!recordingRestriction.canEditStock
} }
isClearable={type !== 'detail'} isClearable={type !== 'detail'}
inputPrefix={ inputPrefix={
@@ -2554,7 +2452,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<td> <td>
<div className='flex flex-col gap-1'> <div className='flex flex-col gap-1'>
<NumberInput <NumberInput
required required={!isTransitioningToLaying}
name={`stocks.${idx}.qty`} name={`stocks.${idx}.qty`}
value={stock.qty ?? ''} value={stock.qty ?? ''}
onChange={handleStockUsageQtyChangeWrapper(idx)} onChange={handleStockUsageQtyChangeWrapper(idx)}
@@ -2586,10 +2484,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
) )
: null : null
} }
disabled={ disabled={type === 'detail'}
type === 'detail' ||
!recordingRestriction.canEditStock
}
/> />
{getStockUsageAdornment(idx)} {getStockUsageAdornment(idx)}
</div> </div>
@@ -2601,7 +2496,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
type='button' type='button'
color='error' color='error'
onClick={() => removeStock(idx)} onClick={() => removeStock(idx)}
disabled={!recordingRestriction.canEditStock}
> >
<Icon <Icon
icon='mdi:trash-can' icon='mdi:trash-can'
@@ -2619,81 +2513,38 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</div> </div>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<div className='flex justify-center items-center mt-4 gap-4'> <div className='flex justify-center items-center mt-4 gap-4'>
{selectedStocks.length > 0 && {selectedStocks.length > 0 && (
recordingRestriction.canEditStock && (
<Button
type='button'
color='error'
onClick={removeSelectedStocks}
disabled={selectedStocks.length === 0}
className='w-fit'
>
<Icon icon='mdi:trash-can' width={24} height={24} />
Hapus Terpilih ({selectedStocks.length})
</Button>
)}
<Tooltip
content={
!recordingRestriction.canEditStock
? 'Stock tidak dapat ditambahkan pada masa transisi Laying'
: ''
}
position='top'
>
<Button <Button
type='button' type='button'
color='success' color='error'
onClick={addStock} onClick={removeSelectedStocks}
disabled={selectedStocks.length === 0}
className='w-fit' className='w-fit'
disabled={!recordingRestriction.canEditStock}
> >
<Icon icon='ic:round-plus' width={24} height={24} /> <Icon icon='mdi:trash-can' width={24} height={24} />
Tambah Stok Hapus Terpilih ({selectedStocks.length})
</Button> </Button>
</Tooltip> )}
<Button
type='button'
color='success'
onClick={addStock}
className='w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah Stok
</Button>
</div> </div>
)} )}
</Card> </Card>
{/* Transition Warning Banner -- MOVED UP -- */}
{isTransitionPeriod && (
<div className='alert alert-warning mb-4'>
<Icon
icon='material-symbols:warning-outline'
width={24}
height={24}
/>
<span>
{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.'}
</span>
</div>
)}
{/* Locked Recording Warning */}
{recordingRestriction.isLocked && (
<div className='alert alert-error mb-4'>
<Icon
icon='material-symbols:lock-outline'
width={24}
height={24}
/>
<span>{recordingRestriction.lockReason}</span>
</div>
)}
{/* Depletions Table */} {/* Depletions Table */}
{((type as 'add' | 'edit' | 'detail') !== 'detail' || {((type as 'add' | 'edit' | 'detail') !== 'detail' ||
(formik.values.depletions?.length ?? 0) > 0) && ( (formik.values.depletions?.length ?? 0) > 0) && (
<Card <Card
title='Deplesi' title='Deplesi'
className={{ className={{
wrapper: cn('w-full mb-4 shadow', { wrapper: 'w-full mb-4 shadow',
'opacity-60':
!recordingRestriction.canEditDepletion &&
(type as 'add' | 'edit' | 'detail') !== 'detail',
}),
title: 'mb-4', title: 'mb-4',
}} }}
> >
@@ -2723,7 +2574,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
setSelectedDepletions([]); setSelectedDepletions([]);
} }
}} }}
disabled={!recordingRestriction.canEditDepletion}
classNames={{ classNames={{
wrapper: 'flex justify-center', wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm', checkbox: 'checkbox checkbox-sm',
@@ -2760,7 +2610,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
); );
} }
}} }}
disabled={!recordingRestriction.canEditDepletion}
classNames={{ classNames={{
wrapper: 'flex justify-center', wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm', checkbox: 'checkbox checkbox-sm',
@@ -2803,10 +2652,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
idx idx
).errorMessage ).errorMessage
} }
isDisabled={ isDisabled={type === 'detail'}
type === 'detail' ||
!recordingRestriction.canEditDepletion
}
className={{ className={{
wrapper: 'w-full min-w-48', wrapper: 'w-full min-w-48',
}} }}
@@ -2845,10 +2691,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
) )
: null : null
} }
disabled={ disabled={type === 'detail'}
type === 'detail' ||
!recordingRestriction.canEditDepletion
}
/> />
</td> </td>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
@@ -2858,9 +2701,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
type='button' type='button'
color='error' color='error'
onClick={() => removeDepletion(idx)} onClick={() => removeDepletion(idx)}
disabled={
!recordingRestriction.canEditDepletion
}
> >
<Icon <Icon
icon='mdi:trash-can' icon='mdi:trash-can'
@@ -2878,38 +2718,27 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</div> </div>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<div className='flex justify-center items-center mt-4 gap-4'> <div className='flex justify-center items-center mt-4 gap-4'>
{selectedDepletions.length > 0 && {selectedDepletions.length > 0 && (
recordingRestriction.canEditDepletion && (
<Button
type='button'
color='error'
onClick={removeSelectedDepletions}
disabled={selectedDepletions.length === 0}
className='w-fit'
>
<Icon icon='mdi:trash-can' width={24} height={24} />
Hapus Terpilih ({selectedDepletions.length})
</Button>
)}
<Tooltip
content={
!recordingRestriction.canEditDepletion
? 'Deplesi tidak dapat ditambahkan pada masa transisi Growing'
: ''
}
position='top'
>
<Button <Button
type='button' type='button'
color='success' color='error'
onClick={addDepletion} onClick={removeSelectedDepletions}
disabled={selectedDepletions.length === 0}
className='w-fit' className='w-fit'
disabled={!recordingRestriction.canEditDepletion}
> >
<Icon icon='ic:round-plus' width={24} height={24} /> <Icon icon='mdi:trash-can' width={24} height={24} />
Tambah Depletion Hapus Terpilih ({selectedDepletions.length})
</Button> </Button>
</Tooltip> )}
<Button
type='button'
color='success'
onClick={addDepletion}
className='w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah Depletion
</Button>
</div> </div>
)} )}
</Card> </Card>
@@ -1,61 +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 (currentIsLaying && !isLaying) {
return {
canEditStock: false,
canEditDepletion: false,
canEditEgg: false,
isLocked: true,
lockReason:
'Recording Growing telah terkunci karena Project Flock sudah masuk fase Laying',
};
}
if (isTransition && !isLaying) {
return {
canEditStock: true,
canEditDepletion: false,
canEditEgg: false,
isLocked: false,
lockReason: undefined,
};
}
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',
};
};
-6
View File
@@ -20,7 +20,6 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
'lti.daily_checklist.master_data.employee', 'lti.daily_checklist.master_data.employee',
'lti.daily_checklist.master_data.activity', 'lti.daily_checklist.master_data.activity',
'lti.daily_checklist.master_data.configuration', 'lti.daily_checklist.master_data.configuration',
'lti.daily_checklist.master_data.kandang',
], ],
submenu: [ submenu: [
{ {
@@ -67,11 +66,6 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
link: '/daily-checklist/master-data/activity', link: '/daily-checklist/master-data/activity',
permission: ['lti.daily_checklist.master_data.activity'], permission: ['lti.daily_checklist.master_data.activity'],
}, },
{
text: 'Kandang',
link: '/daily-checklist/master-data/kandang',
permission: ['lti.daily_checklist.master_data.kandang'],
},
{ {
text: 'Konfigurasi', text: 'Konfigurasi',
link: '/daily-checklist/master-data/configuration', link: '/daily-checklist/master-data/configuration',
-3
View File
@@ -21,9 +21,6 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
'/daily-checklist/master-data/configuration/': [ '/daily-checklist/master-data/configuration/': [
'lti.daily_checklist.master_data.configuration', 'lti.daily_checklist.master_data.configuration',
], ],
'/daily-checklist/master-data/kandang/': [
'lti.daily_checklist.master_data.kandang',
],
// Production // Production
// Production - Project Flock // Production - Project Flock
@@ -1,7 +1,7 @@
'use client'; 'use client';
import * as React from 'react'; import * as React from 'react';
import { Check, ChevronsUpDown, X, Loader2 } from 'lucide-react'; import { Check, ChevronsUpDown, X } from 'lucide-react';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import { Button } from '@/figma-make/components/base/button'; import { Button } from '@/figma-make/components/base/button';
import { import {
@@ -29,8 +29,6 @@ interface MultiSelectProps {
selected: string[]; selected: string[];
onChange: (selected: string[]) => void; onChange: (selected: string[]) => void;
onSearchChange?: (value: string) => void; onSearchChange?: (value: string) => void;
onLoadMore?: () => void;
isLoadingMore?: boolean;
placeholder?: string; placeholder?: string;
className?: string; className?: string;
disabled?: boolean; disabled?: boolean;
@@ -41,8 +39,6 @@ export function MultiSelect({
selected, selected,
onChange, onChange,
onSearchChange, onSearchChange,
onLoadMore,
isLoadingMore,
placeholder = 'Select items...', placeholder = 'Select items...',
className, className,
disabled, disabled,
@@ -119,18 +115,7 @@ export function MultiSelect({
onValueChange={onSearchChange} onValueChange={onSearchChange}
/> />
<CommandEmpty>No item found.</CommandEmpty> <CommandEmpty>No item found.</CommandEmpty>
<CommandList <CommandList className='max-h-[300px] overflow-y-auto'>
className='max-h-[300px] overflow-y-auto'
onScroll={(e) => {
const target = e.currentTarget;
if (
target.scrollHeight - target.scrollTop <=
target.clientHeight + 1
) {
onLoadMore?.();
}
}}
>
<CommandGroup className='overflow-visible'> <CommandGroup className='overflow-visible'>
{options.map((option) => ( {options.map((option) => (
<CommandItem <CommandItem
@@ -149,11 +134,6 @@ export function MultiSelect({
{option.label} {option.label}
</CommandItem> </CommandItem>
))} ))}
{isLoadingMore && (
<div className='py-4 flex justify-center w-full'>
<Loader2 className='h-4 w-4 animate-spin text-muted-foreground' />
</div>
)}
</CommandGroup> </CommandGroup>
</CommandList> </CommandList>
</Command> </Command>
+2 -7
View File
@@ -55,11 +55,7 @@ function SelectContent({
children, children,
position = 'popper', position = 'popper',
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Content> & { }: React.ComponentProps<typeof SelectPrimitive.Content>) {
onScroll?: React.UIEventHandler<HTMLDivElement>;
}) {
const { onScroll, ...restProps } = props;
return ( return (
<SelectPrimitive.Portal> <SelectPrimitive.Portal>
<SelectPrimitive.Content <SelectPrimitive.Content
@@ -71,7 +67,7 @@ function SelectContent({
className className
)} )}
position={position} position={position}
{...restProps} {...props}
> >
<SelectScrollUpButton /> <SelectScrollUpButton />
<SelectPrimitive.Viewport <SelectPrimitive.Viewport
@@ -80,7 +76,6 @@ function SelectContent({
position === 'popper' && position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1' 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1'
)} )}
onScroll={onScroll}
> >
{children} {children}
</SelectPrimitive.Viewport> </SelectPrimitive.Viewport>
@@ -2,16 +2,7 @@
import * as React from 'react'; import * as React from 'react';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { import { Plus, X, Save, Send, Info, FilePlus, ListChecks } from 'lucide-react';
Plus,
X,
Save,
Send,
Info,
FilePlus,
ListChecks,
Loader2,
} from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card'; import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button'; import { Button } from '@/figma-make/components/base/button';
import { Label } from '@/figma-make/components/base/label'; import { Label } from '@/figma-make/components/base/label';
@@ -35,6 +26,7 @@ import {
import { DatePicker } from '@/figma-make/components/base/date-picker'; import { DatePicker } from '@/figma-make/components/base/date-picker';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useSelect } from '@/components/input/SelectInput'; import { useSelect } from '@/components/input/SelectInput';
import { KandangApi } from '@/services/api/master-data';
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist'; import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import useSWR from 'swr'; import useSWR from 'swr';
@@ -51,7 +43,6 @@ import DropFileInput from '@/components/input/DropFileInput';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter, useSearchParams, usePathname } from 'next/navigation'; import { useRouter, useSearchParams, usePathname } from 'next/navigation';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
// Static categories // Static categories
const CATEGORIES = [ const CATEGORIES = [
@@ -95,11 +86,16 @@ export function DailyChecklistContent() {
searchParams.get('category') || '' searchParams.get('category') || ''
); );
const { const { options: kandangOptions } = useSelect(
options: kandangOptions, KandangApi.basePath,
isLoadingMore: isLoadingMoreKandang, 'id',
loadMore: loadMoreKandang, 'name',
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name'); 'search',
{
page: '1',
limit: '100',
}
);
const { data: phases } = useSWR< const { data: phases } = useSWR<
BaseApiResponse<Phase[] | undefined>, BaseApiResponse<Phase[] | undefined>,
@@ -172,16 +168,6 @@ export function DailyChecklistContent() {
const [documents, setDocuments] = useState<File[]>([]); const [documents, setDocuments] = useState<File[]>([]);
const [deletedDocumentIds, setDeletedDocumentIds] = useState<number[]>([]); const [deletedDocumentIds, setDeletedDocumentIds] = useState<number[]>([]);
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement;
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
}
};
// Sync state to URL query params // Sync state to URL query params
useEffect(() => { useEffect(() => {
const params = new URLSearchParams(searchParams.toString()); const params = new URLSearchParams(searchParams.toString());
@@ -1008,7 +994,7 @@ export function DailyChecklistContent() {
> >
<SelectValue placeholder='Pilih kandang' /> <SelectValue placeholder='Pilih kandang' />
</SelectTrigger> </SelectTrigger>
<SelectContent onScroll={handleKandangScroll}> <SelectContent>
{kandangOptions.map((kandang) => ( {kandangOptions.map((kandang) => (
<SelectItem <SelectItem
key={kandang.value} key={kandang.value}
@@ -1017,12 +1003,6 @@ export function DailyChecklistContent() {
{kandang.label} {kandang.label}
</SelectItem> </SelectItem>
))} ))}
{isLoadingMoreKandang && (
<div className='flex justify-center p-2'>
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
</div>
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@@ -16,7 +16,7 @@ import {
SelectValue, SelectValue,
} from '@/figma-make/components/base/select'; } from '@/figma-make/components/base/select';
import { Badge } from '@/figma-make/components/base/badge'; import { Badge } from '@/figma-make/components/base/badge';
import { Users, AlertCircle, Info, Loader2 } from 'lucide-react'; import { Users, AlertCircle, Info } from 'lucide-react';
import { DateRangePicker } from '@/figma-make/components/base/date-range-picker'; import { DateRangePicker } from '@/figma-make/components/base/date-range-picker';
import { import {
BarChart, BarChart,
@@ -36,10 +36,10 @@ import { DailyChecklistSummary } from '@/types/api/daily-checklist/daily-checkli
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist'; import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
import { KandangApi } from '@/services/api/master-data';
import { useSelect } from '@/components/input/SelectInput'; import { useSelect } from '@/components/input/SelectInput';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { formatDate } from '@/lib/helper'; import { formatDate } from '@/lib/helper';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
const KANDANG_COLORS = [ const KANDANG_COLORS = [
'#0069e0', // Blue (primary) '#0069e0', // Blue (primary)
@@ -77,20 +77,16 @@ export function Dashboard() {
httpClientFetcher httpClientFetcher
); );
const { const { options: kandangOptions } = useSelect(
options: kandangOptions, KandangApi.basePath,
loadMore: loadMoreKandang, 'id',
isLoadingMore: isLoadingMoreKandang, 'name',
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name'); 'search',
{
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => { page: '1',
const target = e.target as HTMLDivElement; limit: '100',
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
} }
}; );
const kandangColorMap: { [key: string]: string } = {}; const kandangColorMap: { [key: string]: string } = {};
(kandangOptions || []).forEach((k, index) => { (kandangOptions || []).forEach((k, index) => {
@@ -168,7 +164,7 @@ export function Dashboard() {
> >
<SelectValue placeholder='Semua Kandang' /> <SelectValue placeholder='Semua Kandang' />
</SelectTrigger> </SelectTrigger>
<SelectContent onScroll={handleKandangScroll}> <SelectContent>
<SelectItem value='ALL'>Semua Kandang</SelectItem> <SelectItem value='ALL'>Semua Kandang</SelectItem>
{kandangOptions.map((kandang) => ( {kandangOptions.map((kandang) => (
<SelectItem <SelectItem
@@ -178,11 +174,6 @@ export function Dashboard() {
{kandang.label} {kandang.label}
</SelectItem> </SelectItem>
))} ))}
{isLoadingMoreKandang && (
<div className='flex justify-center p-2'>
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
</div>
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@@ -1,15 +1,7 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { import { Eye, CheckCircle, XCircle, Search, Trash2, Edit } from 'lucide-react';
Eye,
CheckCircle,
XCircle,
Search,
Trash2,
Edit,
Loader2,
} from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card'; import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button'; import { Button } from '@/figma-make/components/base/button';
import { Badge } from '@/figma-make/components/base/badge'; import { Badge } from '@/figma-make/components/base/badge';
@@ -42,9 +34,9 @@ import { DailyChecklist } from '@/types/api/daily-checklist/daily-checklist';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import { ColumnDef } from '@tanstack/react-table'; import { ColumnDef } from '@tanstack/react-table';
import { useSelect } from '@/components/input/SelectInput'; import { useSelect } from '@/components/input/SelectInput';
import { KandangApi } from '@/services/api/master-data';
import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
const STATUS_OPTIONS = [ const STATUS_OPTIONS = [
{ value: 'ALL', label: 'Semua Status' }, { value: 'ALL', label: 'Semua Status' },
@@ -101,25 +93,21 @@ export function ListDailyChecklistContent() {
} }
); );
const { const { options: kandangOptions } = useSelect(
options: kandangOptions, KandangApi.basePath,
isLoadingMore: isLoadingMoreKandang, 'id',
loadMore: loadMoreKandang, 'name',
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name'); 'search',
{
page: '1',
limit: '100',
}
);
const checklistList = isResponseSuccess(checklistListRes) const checklistList = isResponseSuccess(checklistListRes)
? checklistListRes.data || [] ? checklistListRes.data || []
: []; : [];
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement;
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
}
};
// Modals // Modals
const [showApproveModal, setShowApproveModal] = useState(false); const [showApproveModal, setShowApproveModal] = useState(false);
const [showRejectModal, setShowRejectModal] = useState(false); const [showRejectModal, setShowRejectModal] = useState(false);
@@ -502,7 +490,7 @@ export function ListDailyChecklistContent() {
> >
<SelectValue placeholder='Semua Kandang' /> <SelectValue placeholder='Semua Kandang' />
</SelectTrigger> </SelectTrigger>
<SelectContent onScroll={handleKandangScroll}> <SelectContent>
<SelectItem value='ALL'>Semua Kandang</SelectItem> <SelectItem value='ALL'>Semua Kandang</SelectItem>
{kandangOptions.map((kandang) => ( {kandangOptions.map((kandang) => (
<SelectItem <SelectItem
@@ -512,11 +500,6 @@ export function ListDailyChecklistContent() {
{kandang.label} {kandang.label}
</SelectItem> </SelectItem>
))} ))}
{isLoadingMoreKandang && (
<div className='flex justify-center p-2'>
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
</div>
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@@ -1,14 +1,7 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { import { Plus, MoreVertical, Pencil, Trash2, Search } from 'lucide-react';
Plus,
MoreVertical,
Pencil,
Trash2,
Search,
Loader2,
} from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card'; import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button'; import { Button } from '@/figma-make/components/base/button';
import { Label } from '@/figma-make/components/base/label'; import { Label } from '@/figma-make/components/base/label';
@@ -56,8 +49,8 @@ import { cn } from '@/lib/helper';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ColumnDef } from '@tanstack/react-table'; import { ColumnDef } from '@tanstack/react-table';
import { useSelect } from '@/components/input/SelectInput'; import { useSelect } from '@/components/input/SelectInput';
import { KandangApi } from '@/services/api/master-data';
import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
export function MasterEmployeeContent() { export function MasterEmployeeContent() {
const { const {
@@ -92,20 +85,16 @@ export function MasterEmployeeContent() {
keepPreviousData: true, keepPreviousData: true,
} }
); );
const { const { options: kandangOptions } = useSelect(
options: kandangOptions, KandangApi.basePath,
loadMore: loadMoreKandang, 'id',
isLoadingMore: isLoadingMoreKandang, 'name',
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name'); 'search',
{
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => { page: '1',
const target = e.target as HTMLDivElement; limit: '100',
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
} }
}; );
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
@@ -362,7 +351,7 @@ export function MasterEmployeeContent() {
<SelectTrigger className='w-[180px] border-gray-200'> <SelectTrigger className='w-[180px] border-gray-200'>
<SelectValue placeholder='Semua Kandang' /> <SelectValue placeholder='Semua Kandang' />
</SelectTrigger> </SelectTrigger>
<SelectContent onScroll={handleKandangScroll}> <SelectContent>
<SelectItem value='all'>Semua Kandang</SelectItem> <SelectItem value='all'>Semua Kandang</SelectItem>
{kandangOptions.map((kandang) => ( {kandangOptions.map((kandang) => (
<SelectItem <SelectItem
@@ -372,11 +361,6 @@ export function MasterEmployeeContent() {
{kandang.label} {kandang.label}
</SelectItem> </SelectItem>
))} ))}
{isLoadingMoreKandang && (
<div className='flex justify-center p-2'>
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
</div>
)}
</SelectContent> </SelectContent>
</Select> </Select>
@@ -487,12 +471,6 @@ export function MasterEmployeeContent() {
kandang_ids: selected.map((id) => Number(id)), kandang_ids: selected.map((id) => Number(id)),
}) })
} }
onLoadMore={() => {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
}}
isLoadingMore={isLoadingMoreKandang}
placeholder='Pilih kandang' placeholder='Pilih kandang'
className='mt-1.5' className='mt-1.5'
/> />
@@ -1,585 +0,0 @@
'use client';
import { useState } from 'react';
import { Plus, MoreVertical, Pencil, Trash2, Search } from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button';
import { Label } from '@/figma-make/components/base/label';
import { Input } from '@/figma-make/components/base/input';
import { Badge } from '@/figma-make/components/base/badge';
import { MultiSelect } from '@/figma-make/components/base/multi-select';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/figma-make/components/base/select';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/figma-make/components/base/dialog';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/figma-make/components/base/alert-dialog';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/figma-make/components/base/dropdown-menu';
import { toast } from 'sonner';
import useSWR from 'swr';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
import Table from '@/components/Table';
import { DailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { cn } from '@/lib/helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ColumnDef } from '@tanstack/react-table';
import { useSelect } from '@/components/input/SelectInput';
import { KandangApi, LocationApi } from '@/services/api/master-data';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import { BaseDailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
import { UserApi } from '@/services/api/user';
export function MasterKandangContent() {
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
location_id: '',
status: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
search: 'search',
location_id: 'location_id',
},
});
const {
data: dailyChecklistKandangs,
isLoading: isLoadingDailyChecklistKandangs,
mutate: refreshDailyChecklistKandangs,
} = useSWR(
`${DailyChecklistKandangApi.basePath}${getTableFilterQueryString()}`,
DailyChecklistKandangApi.getAllFetcher,
{
keepPreviousData: true,
}
);
const { options: locationOptions } = useSelect(
LocationApi.basePath,
'id',
'name',
'search',
{
page: '1',
limit: '100',
}
);
const { options: picOptions } = useSelect(
UserApi.basePath,
'id',
'name',
'search',
{
page: '1',
limit: '100',
}
);
const {
options: kandangOptions,
isLoadingMore: isLoadingKandangOptionsMore,
loadMore: loadMoreKandang,
} = useSelect(KandangApi.basePath, 'id', 'name');
const [showModal, setShowModal] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [kandangToDelete, setKandangToDelete] = useState<number | null>(null);
const [loading, setLoading] = useState(false);
const [modalMode, setModalMode] = useState<'create' | 'edit'>('create');
const [kandangForm, setKandangForm] = useState({
id: 0,
name: '',
location_id: 0,
pic_id: 0,
// recording_kandangs: [] as number[],
});
const dailyChecklistKandangColumns: ColumnDef<DailyChecklistKandang>[] = [
{
id: 'name',
header: 'Nama',
accessorKey: 'name',
enableSorting: false,
},
{
id: 'location',
header: 'Lokasi',
accessorKey: 'location',
enableSorting: false,
cell: ({ row }) => row.original.location.name ?? '-',
},
{
id: 'pic',
header: 'PIC',
accessorKey: 'pic',
enableSorting: false,
cell: ({ row }) => row.original.pic.name ?? '-',
},
{
id: 'recording_kandangs',
header: 'Kandang Recording',
accessorKey: 'recording_kandangs',
enableSorting: false,
cell: ({ row }) =>
row.original.recording_kandangs?.length > 0
? row.original.recording_kandangs.map((item) => item.name).join(', ')
: '-',
},
{
id: 'action',
header: 'Aksi',
accessorKey: 'action',
enableSorting: false,
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='ghost'
size='sm'
className='h-8 w-8 p-0 hover:bg-gray-100'
>
<MoreVertical className='h-4 w-4 text-gray-600' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem onClick={() => handleEdit(row.original)}>
<Pencil className='mr-2 h-4 w-4' />
Edit
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDeleteClick(row.original.id)}
className='text-red-600'
>
<Trash2 className='mr-2 h-4 w-4' />
Hapus
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
},
];
const handleAdd = () => {
setModalMode('create');
setKandangForm({
id: 0,
name: '',
location_id: 0,
pic_id: 0,
// recording_kandangs: []
});
setShowModal(true);
};
const handleEdit = (dailyChecklistKandang: DailyChecklistKandang) => {
setModalMode('edit');
setKandangForm({
id: dailyChecklistKandang.id,
name: dailyChecklistKandang.name,
location_id: dailyChecklistKandang.location.id,
pic_id: dailyChecklistKandang.pic.id,
// recording_kandangs:
// dailyChecklistKandang.recording_kandangs.map((item) => item.id) ?? [],
});
setShowModal(true);
};
const handleSave = async () => {
if (!kandangForm.name.trim()) {
toast.error('Nama harus diisi');
return;
}
if (!kandangForm.location_id) {
toast.error('Lokasi wajib diisi');
return;
}
// if (!kandangForm.recording_kandangs.length) {
// toast.error('Kandang recording wajib diisi');
// return;
// }
setLoading(true);
try {
if (modalMode === 'create') {
const createDailyChecklistKandangResponse =
await DailyChecklistKandangApi.create({
name: kandangForm.name.trim(),
location_id: kandangForm.location_id,
pic_id: kandangForm.pic_id,
// recording_kandang_ids: kandangForm.recording_kandangs,
});
if (isResponseError(createDailyChecklistKandangResponse)) {
console.error(
'Error creating kandang:',
createDailyChecklistKandangResponse.message
);
toast.error('Gagal menambahkan kandang');
return;
}
refreshDailyChecklistKandangs();
toast.success('Kandang berhasil ditambahkan');
} else {
const updateDailyChecklistKandangResponse =
await DailyChecklistKandangApi.update(kandangForm.id, {
name: kandangForm.name.trim(),
location_id: kandangForm.location_id,
pic_id: kandangForm.pic_id,
// recording_kandang_ids: kandangForm.recording_kandangs,
});
if (isResponseError(updateDailyChecklistKandangResponse)) {
console.error(
'Error updating kandang:',
updateDailyChecklistKandangResponse.message
);
toast.error('Gagal menambahkan Kandang');
return;
}
refreshDailyChecklistKandangs();
toast.success('Kandang berhasil diubah');
}
setShowModal(false);
setKandangForm({
id: 0,
name: '',
location_id: 0,
pic_id: 0,
// recording_kandangs: [],
});
} catch (error) {
console.error('Error saving kandang:', error);
toast.error('Terjadi kesalahan saat menyimpan kandang');
} finally {
setLoading(false);
}
};
const handleDeleteClick = (kandangId: number) => {
setKandangToDelete(kandangId);
setShowDeleteConfirm(true);
};
const handleConfirmDelete = async () => {
if (!kandangToDelete) return;
setLoading(true);
try {
const deleteKandangResponse =
await DailyChecklistKandangApi.delete(kandangToDelete);
if (isResponseError(deleteKandangResponse)) {
console.error('Error deleting kandang:', deleteKandangResponse.message);
toast.error('Gagal menghapus kandang');
return;
}
refreshDailyChecklistKandangs();
toast.success('Kandang berhasil dihapus');
setShowDeleteConfirm(false);
setKandangToDelete(null);
} catch (error) {
console.error('Error deleting kandang:', error);
toast.error('Terjadi kesalahan saat menghapus kandang');
} finally {
setLoading(false);
}
};
if (isLoadingDailyChecklistKandangs && !dailyChecklistKandangs) {
return (
<div className='min-h-screen'>
<div className='p-6'>
<div className='mb-6'>
<h1 className='text-2xl font-semibold text-gray-900'>
Master Kandang
</h1>
<p className='text-sm text-gray-600 mt-1'>
Master Data <span className='text-[#0069e0]'>Kandang</span>
</p>
</div>
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
<CardContent className='p-12 text-center text-gray-500'>
Memuat data...
</CardContent>
</Card>
</div>
</div>
);
}
return (
<div className='min-h-screen'>
<div className='p-6'>
{/* Page Title */}
<div className='mb-6'>
<h1 className='text-2xl font-semibold text-gray-900'>
Master Kandang
</h1>
<p className='text-sm text-gray-600 mt-1'>
Master Data <span className='text-[#0069e0]'>Kandang</span>
</p>
</div>
{/* Main Card */}
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
<CardContent className='p-0'>
{/* Single Toolbar Row */}
<div className='flex flex-wrap items-center justify-between gap-4 p-6 border-b border-gray-200/60'>
{/* LEFT: Search + Filters */}
<div className='flex items-center gap-3 flex-wrap'>
<div className='relative'>
<Search className='absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4' />
<DebouncedTextInput
name='search'
placeholder='Cari kandang...'
value={tableFilterState.search}
onChange={(e) => updateFilter('search', e.target.value)}
className={{
wrapper: 'w-full sm:w-[280px] border-gray-200',
inputWrapper: 'px-3 py-2 h-fit rounded-md',
input: 'text-sm',
}}
startAdornment={
<Search className='text-gray-400 w-4 h-4' />
}
/>
</div>
<Select
value={tableFilterState.location_id}
onValueChange={(value) =>
updateFilter('location_id', value === 'all' ? '' : value)
}
>
<SelectTrigger className='w-[180px] border-gray-200'>
<SelectValue placeholder='Semua Lokasi' />
</SelectTrigger>
<SelectContent>
<SelectItem value='all'>Semua Lokasi</SelectItem>
{locationOptions.map((kandang) => (
<SelectItem
key={kandang.value}
value={String(kandang.value)}
>
{kandang.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* RIGHT: Export + Add */}
<div className='flex items-center gap-2 flex-wrap'>
<Button
onClick={handleAdd}
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
>
<Plus className='w-4 h-4 mr-2' />
Tambah Kandang
</Button>
</div>
</div>
{/* Table */}
<Table<DailyChecklistKandang>
data={
isResponseSuccess(dailyChecklistKandangs)
? dailyChecklistKandangs?.data
: []
}
columns={dailyChecklistKandangColumns}
pageSize={tableFilterState.pageSize}
onPageSizeChange={setPageSize}
rowOptions={[10, 20, 50, 100]}
page={
isResponseSuccess(dailyChecklistKandangs)
? dailyChecklistKandangs?.meta?.page
: 0
}
totalItems={
isResponseSuccess(dailyChecklistKandangs)
? dailyChecklistKandangs?.meta?.total_results
: 0
}
onPageChange={setPage}
isLoading={isLoadingDailyChecklistKandangs}
className={{
containerClassName: cn({
'w-full mb-20':
isResponseSuccess(dailyChecklistKandangs) &&
dailyChecklistKandangs?.data?.length === 0,
}),
tableWrapperClassName:
'overflow-x-auto border border-solid border-base-content/10 rounded-none',
headerRowClassName: 'bg-gray-50/50',
headerColumnClassName:
'text-left py-3.5 px-6 text-sm font-semibold text-gray-700',
paginationClassName: 'px-4',
}}
/>
</CardContent>
</Card>
</div>
{/* Add/Edit Modal */}
<Dialog open={showModal} onOpenChange={setShowModal}>
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
<DialogHeader>
<DialogTitle>
{modalMode === 'create' ? 'Tambah Kandang' : 'Edit Kandang'}
</DialogTitle>
<DialogDescription>
{modalMode === 'create'
? 'Masukkan detail Kandang baru'
: 'Ubah detail Kandang'}
</DialogDescription>
</DialogHeader>
<div className='space-y-4 py-4'>
<div>
<Label htmlFor='nama-kandang'>
Nama Kandang <span className='text-red-500'>*</span>
</Label>
<Input
id='nama-kandang'
value={kandangForm.name}
onChange={(e) =>
setKandangForm({ ...kandangForm, name: e.target.value })
}
placeholder='Masukkan nama Kandang'
className='mt-1.5'
disabled={loading}
/>
</div>
<div>
<Label htmlFor='category'>
Lokasi <span className='text-red-500'>*</span>
</Label>
<Select
value={
kandangForm.location_id ? String(kandangForm.location_id) : ''
}
onValueChange={(value) =>
setKandangForm({ ...kandangForm, location_id: Number(value) })
}
>
<SelectTrigger id='category' className='mt-1.5'>
<SelectValue placeholder='Pilih lokasi' />
</SelectTrigger>
<SelectContent>
{locationOptions.map((cat) => (
<SelectItem key={cat.value} value={String(cat.value)}>
{cat.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor='pic'>
PIC <span className='text-red-500'>*</span>
</Label>
<Select
value={kandangForm.pic_id ? String(kandangForm.pic_id) : ''}
onValueChange={(value) =>
setKandangForm({ ...kandangForm, pic_id: Number(value) })
}
>
<SelectTrigger id='pic' className='mt-1.5'>
<SelectValue placeholder='Pilih PIC' />
</SelectTrigger>
<SelectContent>
{picOptions.map((cat) => (
<SelectItem key={cat.value} value={String(cat.value)}>
{cat.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button
variant='outline'
onClick={() => setShowModal(false)}
disabled={loading}
>
Batal
</Button>
<Button
onClick={handleSave}
disabled={loading}
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
>
{loading ? 'Menyimpan...' : 'Simpan'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Confirmation */}
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
<AlertDialogContent className='bg-white rounded-xl shadow-lg sm:max-w-md'>
<AlertDialogHeader>
<AlertDialogTitle>Hapus Kandang?</AlertDialogTitle>
<AlertDialogDescription>
Data Kandang akan dihapus secara permanen.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={loading}>Batal</AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirmDelete}
disabled={loading}
className='bg-red-600 hover:bg-red-700 text-white'
>
{loading ? 'Menghapus...' : 'Hapus'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
@@ -11,7 +11,7 @@ import {
SelectValue, SelectValue,
} from '@/figma-make/components/base/select'; } from '@/figma-make/components/base/select';
import { useSelect } from '@/components/input/SelectInput'; import { useSelect } from '@/components/input/SelectInput';
import { AreaApi, LocationApi } from '@/services/api/master-data'; import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data';
import useSWR from 'swr'; import useSWR from 'swr';
import { BaseApiResponse } from '@/types/api/api-general'; import { BaseApiResponse } from '@/types/api/api-general';
import { DailyChecklistReport } from '@/types/api/daily-checklist/daily-checklist'; import { DailyChecklistReport } from '@/types/api/daily-checklist/daily-checklist';
@@ -26,8 +26,7 @@ import { ColumnDef } from '@tanstack/react-table';
import { PhaseApi } from '@/services/api/daily-checklist/phase'; import { PhaseApi } from '@/services/api/daily-checklist/phase';
import { EmployeeApi } from '@/services/api/daily-checklist/employee'; import { EmployeeApi } from '@/services/api/daily-checklist/employee';
import { Button } from '@/figma-make/components/base/button'; import { Button } from '@/figma-make/components/base/button';
import { Download, Loader2 } from 'lucide-react'; import { Download } from 'lucide-react';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
const MONTH_OPTIONS = [ const MONTH_OPTIONS = [
{ value: '1', label: 'Januari' }, { value: '1', label: 'Januari' },
@@ -130,23 +129,18 @@ export function DailyChecklistReportsContent() {
} }
); );
const { const { options: kandangOptions } = useSelect(
options: kandangOptions, KandangApi.basePath,
loadMore: loadMoreKandang, 'id',
isLoadingMore: isLoadingMoreKandang, 'name',
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name', 'search', { 'search',
area_id: tableFilterState.area_id, {
location_id: tableFilterState.location_id, page: '1',
}); limit: '100',
area_id: tableFilterState.area_id,
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => { location_id: tableFilterState.location_id,
const target = e.target as HTMLDivElement;
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
} }
}; );
const { options: phaseOptions } = useSelect( const { options: phaseOptions } = useSelect(
PhaseApi.basePath, PhaseApi.basePath,
@@ -441,7 +435,7 @@ export function DailyChecklistReportsContent() {
> >
<SelectValue placeholder='Semua Kandang' /> <SelectValue placeholder='Semua Kandang' />
</SelectTrigger> </SelectTrigger>
<SelectContent onScroll={handleKandangScroll}> <SelectContent>
<SelectItem value='ALL'>Semua Kandang</SelectItem> <SelectItem value='ALL'>Semua Kandang</SelectItem>
{kandangOptions.map((kandang) => ( {kandangOptions.map((kandang) => (
<SelectItem <SelectItem
@@ -451,11 +445,6 @@ export function DailyChecklistReportsContent() {
{kandang.label} {kandang.label}
</SelectItem> </SelectItem>
))} ))}
{isLoadingMoreKandang && (
<div className='flex justify-center p-2'>
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
</div>
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@@ -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'
);
-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;
-3
View File
@@ -1,7 +1,6 @@
import { BaseMetadata } from '@/types/api/api-general'; import { BaseMetadata } from '@/types/api/api-general';
import { BaseLocation } from '@/types/api/master-data/location'; import { BaseLocation } from '@/types/api/master-data/location';
import { BaseUser } from '@/types/api/user'; import { BaseUser } from '@/types/api/user';
import { BaseDailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
export type BaseKandang = { export type BaseKandang = {
id: number; id: number;
@@ -11,7 +10,6 @@ export type BaseKandang = {
capacity: number; capacity: number;
pic: BaseUser; pic: BaseUser;
project_flock_kandang_id?: number; project_flock_kandang_id?: number;
kandang_group: Pick<BaseDailyChecklistKandang, 'id' | 'name'>;
}; };
export type Kandang = BaseMetadata & BaseKandang; export type Kandang = BaseMetadata & BaseKandang;
@@ -21,7 +19,6 @@ export type CreateKandangPayload = {
location_id: number; location_id: number;
capacity: number; capacity: number;
pic_id: number; pic_id: number;
group_id: number;
}; };
export type UpdateKandangPayload = CreateKandangPayload; export type UpdateKandangPayload = CreateKandangPayload;
-2
View File
@@ -74,8 +74,6 @@ export type ProjectFlockKandangLookup = {
available_quantity?: number; available_quantity?: number;
population: number; population: number;
chick_in_date: string; chick_in_date: string;
is_transition: boolean;
is_laying: boolean;
}; };
export type ProjectFlockAvailableQuantity = { export type ProjectFlockAvailableQuantity = {
+2 -2
View File
@@ -49,8 +49,8 @@ export type BaseRecording = {
project_flock: ProjectFlock; project_flock: ProjectFlock;
record_datetime: string; record_datetime: string;
day: number; day: number;
is_transition: boolean; population_can_change: boolean;
is_laying: boolean; transfer_executed: boolean;
} & ProductionMetrics; } & ProductionMetrics;
export type RecordingDepletion = { export type RecordingDepletion = {