chore: prettier format

This commit is contained in:
rstubryan
2025-11-13 14:01:07 +07:00
parent ac8c39324b
commit b3f4e42f1a
15 changed files with 522 additions and 719 deletions
+11 -12
View File
@@ -10,15 +10,15 @@ stages:
paths: paths:
- node_modules/ - node_modules/
variables: variables:
NPM_CONFIG_PRODUCTION: "false" NPM_CONFIG_PRODUCTION: 'false'
NODE_ENV: "" NODE_ENV: ''
script: script:
- echo "Installing dependencies..." - echo "Installing dependencies..."
- npm ci --no-audit --no-fund - npm ci --no-audit --no-fund
- echo "Building Next.js static export..." - echo "Building Next.js static export..."
- npx next build - npx next build
artifacts: artifacts:
name: "out-$CI_COMMIT_SHORT_SHA" name: 'out-$CI_COMMIT_SHORT_SHA'
paths: paths:
- out/ - out/
expire_in: 1 week expire_in: 1 week
@@ -27,7 +27,7 @@ stages:
stage: deploy stage: deploy
image: image:
name: amazon/aws-cli:latest name: amazon/aws-cli:latest
entrypoint: ["/bin/sh", "-c"] entrypoint: ['/bin/sh', '-c']
script: script:
- set -e - set -e
- aws --version - aws --version
@@ -106,22 +106,21 @@ build:dev:
environment: environment:
name: development name: development
variables: variables:
NEXT_PUBLIC_API_BASE_URL: "https://dev-api-lti.mbugroup.id" NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id'
NEXT_PUBLIC_SSO_LOGIN_URL: "https://dev-api-sso.mbugroup.id" NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-api-sso.mbugroup.id'
deploy:dev: deploy:dev:
<<: *deploy_template <<: *deploy_template
needs: ["build:dev"] needs: ['build:dev']
rules: rules:
- if: '$CI_COMMIT_BRANCH == "development"' - if: '$CI_COMMIT_BRANCH == "development"'
variables: variables:
S3_BUCKET: "dev-lti-erp.mbugroup.id" S3_BUCKET: 'dev-lti-erp.mbugroup.id'
AWS_REGION: "ap-southeast-3" AWS_REGION: 'ap-southeast-3'
CLOUDFRONT_DISTRIBUTION_ID: "E1Z8XTA8XF1GIV" CLOUDFRONT_DISTRIBUTION_ID: 'E1Z8XTA8XF1GIV'
environment: environment:
name: development name: development
url: https://dev-lti-erp.mbugroup.id url: https://dev-lti-erp.mbugroup.id
# ====== PRODUCTION ====== # ====== PRODUCTION ======
# build:production: # build:production:
# <<: *build_template # <<: *build_template
@@ -143,5 +142,5 @@ deploy:dev:
# CLOUDFRONT_DISTRIBUTION_ID: "ddfd" # CLOUDFRONT_DISTRIBUTION_ID: "ddfd"
# environment: # environment:
# name: production # name: production
# url: https://royalgoldcapital.com # url: https://royalgoldcapital.com
+1
View File
@@ -1,2 +1,3 @@
npm run format
npm run lint npm run lint
npm run build npm run build
+5 -5
View File
@@ -1,4 +1,4 @@
version: "3.9" version: '3.9'
services: services:
dev-web-lti: dev-web-lti:
@@ -7,7 +7,7 @@ services:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
ports: ports:
- "3002:3000" - '3002:3000'
env_file: env_file:
- .env - .env
environment: environment:
@@ -19,13 +19,13 @@ services:
deploy: deploy:
resources: resources:
limits: limits:
cpus: "3.0" cpus: '3.0'
memory: 3G memory: 3G
reservations: reservations:
cpus: "1.0" cpus: '1.0'
memory: 512M memory: 512M
extra_hosts: extra_hosts:
- "host.docker.internal:host-gateway" - 'host.docker.internal:host-gateway'
# Optional: aktifkan healthcheck jika punya endpoint # Optional: aktifkan healthcheck jika punya endpoint
# healthcheck: # healthcheck:
# test: ["CMD-SHELL", "curl -fsS http://localhost:3000/api/healthz || exit 1"] # test: ["CMD-SHELL", "curl -fsS http://localhost:3000/api/healthz || exit 1"]
+3 -1
View File
@@ -64,7 +64,9 @@ export const FormActions = <T,>({
Edit Edit
</Button> </Button>
)} )}
{type === 'detail' && showApproveReject && (onApprove || onReject) && ( {type === 'detail' &&
showApproveReject &&
(onApprove || onReject) && (
<> <>
{onApprove && ( {onApprove && (
<Button <Button
+1 -1
View File
@@ -27,7 +27,7 @@ const PatternInput = ({
patternChar = '#', patternChar = '#',
onChange, onChange,
...restProps ...restProps
}: PatternInputProps) => { }: PatternInputProps) => {
const valueChangeHandler: OnValueChange = ( const valueChangeHandler: OnValueChange = (
patternFormatValues, patternFormatValues,
sourceInfo sourceInfo
@@ -150,7 +150,8 @@ const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
.required('Produk wajib diisi!'), .required('Produk wajib diisi!'),
}); });
export const MovementFormSchema: Yup.ObjectSchema<MovementFormSchemaType> = Yup.object({ export const MovementFormSchema: Yup.ObjectSchema<MovementFormSchemaType> =
Yup.object({
transfer_reason: Yup.string().required('Alasan transfer wajib diisi!'), transfer_reason: Yup.string().required('Alasan transfer wajib diisi!'),
transfer_date: Yup.string().required('Tanggal transfer wajib diisi!'), transfer_date: Yup.string().required('Tanggal transfer wajib diisi!'),
source_warehouse: Yup.object({ source_warehouse: Yup.object({
@@ -179,7 +180,7 @@ export const MovementFormSchema: Yup.ObjectSchema<MovementFormSchemaType> = Yup.
.of(DeliveryObjectSchema) .of(DeliveryObjectSchema)
.min(1, 'Minimal harus ada 1 pengiriman!') .min(1, 'Minimal harus ada 1 pengiriman!')
.required('Pengiriman wajib diisi!'), .required('Pengiriman wajib diisi!'),
}); });
export type MovementFormValues = Yup.InferType<typeof MovementFormSchema>; export type MovementFormValues = Yup.InferType<typeof MovementFormSchema>;
@@ -15,4 +15,6 @@ export const ProductCategoryFormSchema: Yup.ObjectSchema<ProductCategoryFormSche
export const UpdateProductCategoryFormSchema = ProductCategoryFormSchema; export const UpdateProductCategoryFormSchema = ProductCategoryFormSchema;
export type ProductCategoryFormValues = Yup.InferType<typeof ProductCategoryFormSchema>; export type ProductCategoryFormValues = Yup.InferType<
typeof ProductCategoryFormSchema
>;
@@ -31,7 +31,9 @@ export const ProductFormSchema: Yup.ObjectSchema<ProductFormSchemaType> =
uom: Yup.object({ uom: Yup.object({
value: Yup.number().min(1).required(), value: Yup.number().min(1).required(),
label: Yup.string().required(), label: Yup.string().required(),
}).nullable().required('Satuan wajib diisi!'), })
.nullable()
.required('Satuan wajib diisi!'),
uom_id: Yup.number() uom_id: Yup.number()
.required('Satuan wajib diisi!') .required('Satuan wajib diisi!')
@@ -40,7 +42,9 @@ export const ProductFormSchema: Yup.ObjectSchema<ProductFormSchemaType> =
product_category: Yup.object({ product_category: Yup.object({
value: Yup.number().min(1).required(), value: Yup.number().min(1).required(),
label: Yup.string().required(), label: Yup.string().required(),
}).nullable().required('Kategori produk wajib diisi!'), })
.nullable()
.required('Kategori produk wajib diisi!'),
product_category_id: Yup.number() product_category_id: Yup.number()
.required('Kategori produk wajib diisi!') .required('Kategori produk wajib diisi!')
@@ -121,9 +121,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
supplier_ids: values.supplier_ids.filter( supplier_ids: values.supplier_ids.filter(
(id): id is number => typeof id === 'number' (id): id is number => typeof id === 'number'
), ),
flags: values.flags.filter( flags: values.flags.filter((f): f is string => typeof f === 'string'),
(f): f is string => typeof f === 'string'
),
}; };
switch (type) { switch (type) {
case 'add': case 'add':
@@ -1,7 +1,6 @@
'use client'; 'use client';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import useSWR from 'swr';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { SortingState } from '@tanstack/react-table'; import { SortingState } from '@tanstack/react-table';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
@@ -9,9 +8,7 @@ import { useModal } from '@/components/Modal';
import Button from '@/components/Button'; import Button from '@/components/Button';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import { OptionType } from '@/components/input/SelectInput'; import { OptionType } from '@/components/input/SelectInput';
import SelectInput from '@/components/input/SelectInput';
import { ROWS_OPTIONS } from '@/config/constant'; import { ROWS_OPTIONS } from '@/config/constant';
import CheckboxInput from '@/components/input/CheckboxInput';
import { TableToolbar } from '@/components/table/TableToolbar'; import { TableToolbar } from '@/components/table/TableToolbar';
import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector'; import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector';
import Table from '@/components/Table'; import Table from '@/components/Table';
@@ -20,14 +17,106 @@ import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { type CellContext } from '@tanstack/react-table'; import { type CellContext } from '@tanstack/react-table';
import { type Recording } from '@/types/api/production/recording'; import { type Recording } from '@/types/api/production/recording';
import { type BaseApiResponse } from '@/types/api/api-general';
import { RecordingApi } from '@/services/api/production'; const dummyRecordings: Recording[] = [
import { AreaApi } from '@/services/api/master-data'; {
import { LocationApi } from '@/services/api/master-data'; id: 1,
import { KandangApi } from '@/services/api/master-data'; flock: {
import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; id: 1,
import { useTableFilter } from '@/services/hooks/useTableFilter'; name: 'Flock Recording 1',
import toast from 'react-hot-toast'; created_at: '2024-01-01',
updated_at: '2024-01-01',
created_user: {
id: 1,
id_user: 1,
email: 'admin@example.com',
name: 'Admin',
},
},
recording_date: '2024-01-01',
location: {
id: 1,
name: 'Location 1',
address: 'Jl. Contoh No. 1',
area: {
id: 1,
name: 'Area 1',
},
created_at: '2024-01-01',
updated_at: '2024-01-01',
created_user: {
id: 1,
id_user: 1,
email: 'admin@example.com',
name: 'Admin',
},
},
coop: {
id: 1,
name: 'Coop 1',
status: 'ACTIVE',
location: {
id: 1,
name: 'Location 1',
address: 'Jl. Contoh No. 1',
area: {
id: 1,
name: 'Area 1',
},
},
pic: {
id: 1,
id_user: 1,
email: 'pic@example.com',
name: 'PIC User',
},
created_at: '2024-01-01',
updated_at: '2024-01-01',
created_user: {
id: 1,
id_user: 1,
email: 'admin@example.com',
name: 'Admin',
},
capacity: 100000,
},
feed_data: [
{
feed_name: 'Feed 1',
feed_qty: 100,
feed_stock: 500,
},
],
body_weight: [
{
chicken_weight: 2.5,
chicken_count: 1000,
average_chicken_weight: 2.5,
},
],
vaccination: [
{
vaccine_name: 'Vaccine 1',
total_stock: 200,
used_stock: 150,
},
],
mortality: [
{
condition: 'NORMAL',
count: 5,
},
],
created_at: '2024-01-01',
updated_at: '2024-01-01',
created_user: {
id: 1,
id_user: 1,
email: 'admin@example.com',
name: 'Admin',
},
},
];
const RowOptionsMenu = ({ const RowOptionsMenu = ({
type = 'dropdown', type = 'dropdown',
@@ -77,34 +166,12 @@ const RowOptionsMenu = ({
}; };
const RecordingTable = () => { const RecordingTable = () => {
const { const [search, setSearch] = useState('');
state: tableFilterState, const [page, setPage] = useState(1);
updateFilter, const [pageSize, setPageSize] = useState(10);
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
areaFilter: '',
locationFilter: '',
kandangFilter: '',
periodFilter: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
search: 'search',
areaFilter: 'area_id',
locationFilter: 'location_id',
kandangFilter: 'kandang_id',
periodFilter: 'period',
},
});
const [sorting, setSorting] = useState<SortingState>([]); const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({}); const [selectedRecordings, setSelectedRecordings] = useState<number[]>([]);
const [selectedRecording, setSelectedRecording] = useState<Recording | undefined>(undefined); const [, setSelectedRecording] = useState<Recording | undefined>(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [isBulkApproveLoading, setIsBulkApproveLoading] = useState(false); const [isBulkApproveLoading, setIsBulkApproveLoading] = useState(false);
const [isBulkRejectLoading, setIsBulkRejectLoading] = useState(false); const [isBulkRejectLoading, setIsBulkRejectLoading] = useState(false);
@@ -113,81 +180,12 @@ const RecordingTable = () => {
const bulkApproveModal = useModal(); const bulkApproveModal = useModal();
const bulkRejectModal = useModal(); const bulkRejectModal = useModal();
// State for dropdown search
const [locationSelectInputValue, setLocationSelectInputValue] = useState('');
const [areaSelectInputValue, setAreaSelectInputValue] = useState('');
const [kandangSelectInputValue, setKandangSelectInputValue] = useState('');
const [selectedArea, setSelectedArea] = useState<OptionType | null>(null);
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(null);
const [selectedKandang, setSelectedKandang] = useState<OptionType | null>(null);
const {
data: recordings,
isLoading,
mutate: refreshRecordings,
} = useSWR(
`${RecordingApi.basePath}${getTableFilterQueryString()}`,
RecordingApi.getAllFetcher
);
// Fetch data for dropdowns
const areaUrl = `${AreaApi.basePath}?${new URLSearchParams({
search: areaSelectInputValue,
limit: '100',
}).toString()}`;
const {
data: areas,
isLoading: isLoadingAreas,
} = useSWR(areaUrl, AreaApi.getAllFetcher);
const locationUrl = `${LocationApi.basePath}?${new URLSearchParams({
search: locationSelectInputValue,
area_id: selectedArea != null ? selectedArea.value.toString() : '',
limit: '100',
}).toString()}`;
const {
data: locations,
isLoading: isLoadingLocations,
} = useSWR(locationUrl, LocationApi.getAllFetcher);
const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({
search: kandangSelectInputValue,
location_id:
selectedLocation != null ? selectedLocation.value.toString() : '',
limit: '100',
}).toString()}`;
const {
data: kandangs,
isLoading: isLoadingKandang,
} = useSWR(kandangUrl, KandangApi.getAllFetcher);
// Data to Options Mapping
const optionsArea = isResponseSuccess(areas)
? areas?.data.map((area) => ({
value: area.id,
label: area.name,
}))
: [];
const optionsLocation = isResponseSuccess(locations)
? locations?.data.map((location) => ({
value: location.id,
label: location.name,
}))
: [];
const optionsKandang = isResponseSuccess(kandangs)
? kandangs?.data.map((kandang) => ({
value: kandang.id,
label: kandang.name,
}))
: [];
const searchChangeHandler = useCallback( const searchChangeHandler = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => { (e: React.ChangeEvent<HTMLInputElement>) => {
updateFilter('search', e.target.value); setSearch(e.target.value);
setPage(1); setPage(1);
}, },
[updateFilter, setPage] []
); );
const pageSizeChangeHandler = useCallback( const pageSizeChangeHandler = useCallback(
@@ -196,80 +194,52 @@ const RecordingTable = () => {
setPageSize(newVal.value as number); setPageSize(newVal.value as number);
setPage(1); setPage(1);
}, },
[setPageSize, setPage] []
); );
const paginatedData = useMemo(() => { const paginatedData = useMemo(() => {
if (!recordings || recordings.status !== 'success') return []; const filteredData = dummyRecordings.filter(
(recording) =>
return recordings.data; recording.flock.name.toLowerCase().includes(search.toLowerCase()) ||
}, [recordings]); recording.location.name.toLowerCase().includes(search.toLowerCase()) ||
recording.coop.name.toLowerCase().includes(search.toLowerCase())
const selectedRowIds = Object.keys(rowSelection).map((item) => parseInt(item)); );
const start = (page - 1) * pageSize;
return filteredData.slice(start, start + pageSize);
}, [page, pageSize, search]);
const bulkApproveHandler = async () => { const bulkApproveHandler = async () => {
setIsBulkApproveLoading(true); setIsBulkApproveLoading(true);
console.log(
const approveResponse = await RecordingApi.customRequest< 'Approved recordings:',
BaseApiResponse<Recording[]> paginatedData.filter((_, idx) => selectedRecordings.includes(idx))
>('approvals', { );
method: 'POST', setTimeout(() => {
payload: {
action: 'APPROVED',
approvable_ids: selectedRowIds,
notes: 'Bulk Approved',
},
});
if (isResponseSuccess(approveResponse)) {
await refreshRecordings();
setRowSelection({});
bulkApproveModal.closeModal();
toast.success(`Successfully approved ${selectedRowIds.length} recordings!`);
}
if (isResponseError(approveResponse)) {
toast.error(approveResponse?.message as string);
bulkApproveModal.closeModal();
}
setIsBulkApproveLoading(false); setIsBulkApproveLoading(false);
setSelectedRecordings([]);
bulkApproveModal.closeModal();
}, 1000);
}; };
const bulkRejectHandler = async () => { const bulkRejectHandler = async () => {
setIsBulkRejectLoading(true); setIsBulkRejectLoading(true);
console.log(
const rejectResponse = await RecordingApi.customRequest< 'Rejected recordings:',
BaseApiResponse<Recording[]> paginatedData.filter((_, idx) => selectedRecordings.includes(idx))
>('approvals', { );
method: 'POST', setTimeout(() => {
payload: {
action: 'REJECTED',
approvable_ids: selectedRowIds,
notes: 'Bulk Rejected',
},
});
if (isResponseSuccess(rejectResponse)) {
refreshRecordings();
setRowSelection({});
bulkRejectModal.closeModal();
toast.success(`Successfully rejected ${selectedRowIds.length} recordings!`);
}
if (isResponseError(rejectResponse)) {
toast.error(rejectResponse?.message as string);
bulkRejectModal.closeModal();
}
setIsBulkRejectLoading(false); setIsBulkRejectLoading(false);
setSelectedRecordings([]);
bulkRejectModal.closeModal();
}, 1000);
}; };
const singleDeleteHandler = async () => { const singleDeleteHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
setTimeout(() => {
await RecordingApi.delete(selectedRecording?.id as number);
refreshRecordings();
singleDeleteModal.closeModal();
toast.success('Successfully delete Recording!');
setIsDeleteLoading(false); setIsDeleteLoading(false);
singleDeleteModal.closeModal();
}, 1000);
}; };
return ( return (
@@ -281,189 +251,21 @@ const RecordingTable = () => {
label: 'Tambah', label: 'Tambah',
}} }}
search={{ search={{
value: tableFilterState.search, value: search,
onChange: searchChangeHandler, onChange: searchChangeHandler,
placeholder: 'Cari Recording', placeholder: 'Cari Recording',
}} }}
/> />
<TableRowSizeSelector <TableRowSizeSelector
value={tableFilterState.pageSize} value={pageSize}
onChange={pageSizeChangeHandler} onChange={pageSizeChangeHandler}
options={ROWS_OPTIONS} options={ROWS_OPTIONS}
/> />
{/* Filter Dropdowns - Desktop */}
<div className='hidden sm:grid sm:grid-cols-4 gap-4 mt-4'>
<SelectInput
label='Area'
placeholder='Pilih Area'
options={optionsArea}
value={selectedArea}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
setSelectedArea(selectedValue);
setSelectedLocation(null);
setSelectedKandang(null);
updateFilter('areaFilter', selectedValue ? selectedValue.value.toString() : '');
updateFilter('locationFilter', '');
updateFilter('kandangFilter', '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
onInputChange={(value) => setAreaSelectInputValue(value)}
isLoading={isLoadingAreas}
isClearable
/>
<SelectInput
label='Lokasi'
placeholder='Pilih Lokasi'
options={optionsLocation}
value={selectedLocation}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
setSelectedLocation(selectedValue);
setSelectedKandang(null);
updateFilter('locationFilter', selectedValue ? selectedValue.value.toString() : '');
updateFilter('kandangFilter', '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
onInputChange={(value) => setLocationSelectInputValue(value)}
isLoading={isLoadingLocations}
isClearable
isDisabled={!selectedArea}
/>
<SelectInput
label='Kandang'
placeholder='Pilih Kandang'
options={optionsKandang}
value={selectedKandang}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
setSelectedKandang(selectedValue);
updateFilter('kandangFilter', selectedValue ? selectedValue.value.toString() : '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
onInputChange={(value) => setKandangSelectInputValue(value)}
isLoading={isLoadingKandang}
isClearable
isDisabled={!selectedLocation}
/>
<SelectInput
label='Periode'
placeholder='Pilih Periode'
options={[
{ value: '1', label: 'Periode 1' },
{ value: '2', label: 'Periode 2' },
{ value: '3', label: 'Periode 3' },
]}
value={
tableFilterState.periodFilter
? { value: tableFilterState.periodFilter, label: `Periode ${tableFilterState.periodFilter}` }
: null
}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
updateFilter('periodFilter', selectedValue ? selectedValue.value.toString() : '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
isClearable
/>
</div>
{/* Filter Dropdowns - Mobile */}
<div className='sm:hidden flex flex-col gap-3 mt-4'>
<SelectInput
label='Area'
placeholder='Pilih Area'
options={optionsArea}
value={selectedArea}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
setSelectedArea(selectedValue);
setSelectedLocation(null);
setSelectedKandang(null);
updateFilter('areaFilter', selectedValue ? selectedValue.value.toString() : '');
updateFilter('locationFilter', '');
updateFilter('kandangFilter', '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
onInputChange={(value) => setAreaSelectInputValue(value)}
isLoading={isLoadingAreas}
isClearable
/>
<SelectInput
label='Lokasi'
placeholder='Pilih Lokasi'
options={optionsLocation}
value={selectedLocation}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
setSelectedLocation(selectedValue);
setSelectedKandang(null);
updateFilter('locationFilter', selectedValue ? selectedValue.value.toString() : '');
updateFilter('kandangFilter', '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
onInputChange={(value) => setLocationSelectInputValue(value)}
isLoading={isLoadingLocations}
isClearable
isDisabled={!selectedArea}
/>
<SelectInput
label='Kandang'
placeholder='Pilih Kandang'
options={optionsKandang}
value={selectedKandang}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
setSelectedKandang(selectedValue);
updateFilter('kandangFilter', selectedValue ? selectedValue.value.toString() : '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
onInputChange={(value) => setKandangSelectInputValue(value)}
isLoading={isLoadingKandang}
isClearable
isDisabled={!selectedLocation}
/>
<SelectInput
label='Periode'
placeholder='Pilih Periode'
options={[
{ value: '1', label: 'Periode 1' },
{ value: '2', label: 'Periode 2' },
{ value: '3', label: 'Periode 3' },
]}
value={
tableFilterState.periodFilter
? { value: tableFilterState.periodFilter, label: `Periode ${tableFilterState.periodFilter}` }
: null
}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
updateFilter('periodFilter', selectedValue ? selectedValue.value.toString() : '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
isClearable
/>
</div>
</div> </div>
{/* Bulk action buttons */} {/* Bulk action buttons */}
<div className={'flex justify-end items-center'}> <div className={'flex justify-end items-center'}>
{selectedRowIds.length > 0 && ( {selectedRecordings.length > 0 && (
<div className='flex gap-2 mb-4'> <div className='flex gap-2 mb-4'>
<Button <Button
type='button' type='button'
@@ -476,7 +278,7 @@ const RecordingTable = () => {
width={20} width={20}
height={20} height={20}
/> />
Approve ({selectedRowIds.length}) Approve ({selectedRecordings.length})
</Button> </Button>
<Button <Button
type='button' type='button'
@@ -489,7 +291,7 @@ const RecordingTable = () => {
width={20} width={20}
height={20} height={20}
/> />
Reject ({selectedRowIds.length}) Reject ({selectedRecordings.length})
</Button> </Button>
</div> </div>
)} )}
@@ -497,7 +299,7 @@ const RecordingTable = () => {
<ConfirmationModal <ConfirmationModal
ref={bulkApproveModal.ref} ref={bulkApproveModal.ref}
type='success' type='success'
text={`Apakah anda yakin ingin menyetujui ${selectedRowIds.length} data Recording yang dipilih?`} text={`Apakah anda yakin ingin menyetujui ${selectedRecordings.length} data Recording yang dipilih?`}
secondaryButton={{ secondaryButton={{
text: 'Tidak', text: 'Tidak',
}} }}
@@ -512,7 +314,7 @@ const RecordingTable = () => {
<ConfirmationModal <ConfirmationModal
ref={bulkRejectModal.ref} ref={bulkRejectModal.ref}
type='error' type='error'
text={`Apakah anda yakin ingin menolak ${selectedRowIds.length} data Recording yang dipilih?`} text={`Apakah anda yakin ingin menolak ${selectedRecordings.length} data Recording yang dipilih?`}
secondaryButton={{ secondaryButton={{
text: 'Tidak', text: 'Tidak',
}} }}
@@ -530,83 +332,75 @@ const RecordingTable = () => {
columns={[ columns={[
{ {
id: 'select', id: 'select',
accessorKey: 'id',
header: ({ table }) => ( header: ({ table }) => (
<div className='w-full flex flex-row justify-center'> <input
<CheckboxInput type='checkbox'
name='allRow' className='checkbox'
checked={table.getIsAllRowsSelected()} checked={
indeterminate={table.getIsSomeRowsSelected()} table.getRowModel().rows.length > 0 &&
onChange={table.getToggleAllRowsSelectedHandler()} table
.getRowModel()
.rows.every((row) => selectedRecordings.includes(row.index))
}
onChange={(e) => {
if (e.target.checked) {
setSelectedRecordings(
table.getRowModel().rows.map((row) => row.index)
);
} else {
setSelectedRecordings([]);
}
}}
/> />
</div>
), ),
cell: ({ row }) => ( cell: ({ row }) => (
<div> <input
<CheckboxInput type='checkbox'
name='row' className='checkbox'
checked={row.getIsSelected()} checked={selectedRecordings.includes(row.index)}
disabled={!row.getCanSelect()} onChange={(e) => {
indeterminate={row.getIsSomeSelected()} if (e.target.checked) {
onChange={row.getToggleSelectedHandler()} setSelectedRecordings([...selectedRecordings, row.index]);
} else {
setSelectedRecordings(
selectedRecordings.filter((i) => i !== row.index)
);
}
}}
/> />
</div>
), ),
}, },
{ {
header: '#', header: '#',
cell: (props) => tableFilterState.pageSize * (tableFilterState.page - 1) + props.row.index + 1, cell: (props) => pageSize * (page - 1) + props.row.index + 1,
}, },
{ {
header: 'Nama Project', accessorKey: 'flock.name',
cell: (props) => `Project ${props.row.original.project_flock_kandang_id}`, header: 'Flock',
}, },
{ {
header: 'Umur (hari)', accessorKey: 'recording_date',
cell: (props) => props.row.original.day, header: 'Tanggal Recording',
},
{
accessorKey: 'record_date',
header: 'Waktu Recording',
cell: (props) => cell: (props) =>
new Date(props.row.original.record_date).toLocaleDateString(), new Date(props.row.original.recording_date).toLocaleDateString(),
}, },
{ {
header: 'Populasi Awal', accessorKey: 'location.name',
cell: (props) => props.row.original.total_chick?.toLocaleString() || '-', header: 'Lokasi',
}, },
{ {
header: 'BW', accessorKey: 'coop.name',
cell: (props) => props.row.original.avg_daily_gain?.toFixed(2) || '-', header: 'Kandang',
}, },
{ {
header: 'Pakan', accessorKey: 'mortality',
cell: (props) => props.row.original.cum_intake?.toLocaleString() || '-', header: 'Total Mortality',
},
{
header: 'FCR',
cell: (props) => props.row.original.fcr_value?.toFixed(2) || '-',
},
{
accessorKey: 'total_depletion',
header: 'Total Deplesi',
cell: (props) => props.row.original.total_depletion,
},
{
header: 'Deplesi (%)',
cell: (props) => props.row.original.daily_depletion_rate?.toFixed(2) || '-',
},
{
header: 'Populasi Akhir',
cell: (props) => (props.row.original.total_chick - props.row.original.total_depletion)?.toLocaleString() || '-',
},
{
header: 'Ketepatan Waktu',
cell: (props) => props.row.original.ontime ? 'Tepat Waktu' : 'Terlambat',
},
{
header: 'Tanggal Submit',
cell: (props) => cell: (props) =>
new Date(props.row.original.created_at).toLocaleString(), props.row.original.mortality.reduce(
(acc, curr) => acc + curr.count,
0
),
}, },
{ {
header: 'Aksi', header: 'Aksi',
@@ -651,15 +445,13 @@ const RecordingTable = () => {
}, },
}, },
]} ]}
pageSize={tableFilterState.pageSize} pageSize={pageSize}
page={recordings?.status === 'success' ? recordings.meta?.page : tableFilterState.page} page={page}
totalItems={recordings?.status === 'success' ? recordings.meta?.total_results : 0} totalItems={dummyRecordings.length}
onPageChange={setPage} onPageChange={setPage}
isLoading={isLoading} isLoading={false}
sorting={sorting} sorting={sorting}
setSorting={setSorting} setSorting={setSorting}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
className={{ className={{
containerClassName: cn({ containerClassName: cn({
'mb-20': paginatedData.length === 0, 'mb-20': paginatedData.length === 0,
@@ -678,7 +470,7 @@ const RecordingTable = () => {
<ConfirmationModal <ConfirmationModal
ref={singleDeleteModal.ref} ref={singleDeleteModal.ref}
type='error' type='error'
text={`Apakah anda yakin ingin menghapus data Recording ini (ID: ${selectedRecording?.id})?`} text={`Apakah anda yakin ingin menghapus data Recording ini?`}
secondaryButton={{ secondaryButton={{
text: 'Tidak', text: 'Tidak',
}} }}
@@ -1,222 +1,212 @@
import * as Yup from 'yup'; import * as Yup from 'yup';
import { RECORDING_FLAG_OPTIONS } from '@/config/constant'; import { RECORDING_FLAG_OPTIONS } from '@/config/constant';
import { import { Recording } from '@/types/api/production/recording';
Recording,
CreateRecordingPayload,
} from '@/types/api/production/recording';
export const RecordingFormSchema = Yup.object({ export const RecordingFormSchema = Yup.object({
project_flock_kandang: Yup.object({ flock: Yup.object({
value: Yup.number().min(1).required(), value: Yup.number().min(1).required(),
label: Yup.string().required(), label: Yup.string().required(),
}).nullable(), }).nullable(),
project_flock_kandang_id: Yup.number() flock_id: Yup.number()
.default(0) .default(0)
.typeError('Project Flock Kandang wajib diisi!') .typeError('Flock wajib diisi!')
.test( .test(
'is-valid-project-flock-kandang', 'is-valid-flock',
'Project Flock Kandang wajib diisi!', 'Flock wajib diisi!',
(value) => value !== undefined && value !== null && value > 0 (value) => value !== undefined && value !== null && value > 0
) )
.required('Project Flock Kandang wajib diisi!') .required('Flock wajib diisi!'),
location: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
location_id: Yup.number()
.default(0)
.typeError('Lokasi wajib diisi!')
.test( .test(
'not-already-recorded', 'is-valid-location',
'Project Flock ini sudah direcord hari ini!', 'Lokasi wajib diisi!',
function(value) { (value) => value !== undefined && value !== null && value > 0
const recordedProjectFlockIds = this.options.context?.recordedProjectFlockIds as Set<number>; )
const formType = this.options.context?.type as 'add' | 'edit' | 'detail'; .required('Lokasi wajib diisi!'),
if (formType !== 'add') return true; coop: Yup.object({
if (value && recordedProjectFlockIds?.has(value)) { value: Yup.number().min(1).required(),
return false; label: Yup.string().required(),
} }).nullable(),
coop_id: Yup.number()
.default(0)
.typeError('Kandang wajib diisi!')
.test(
'is-valid-coop',
'Kandang wajib diisi!',
(value) => value !== undefined && value !== null && value > 0
)
.required('Kandang wajib diisi!'),
recording_date: Yup.date()
.required('Tanggal recording wajib diisi')
.typeError('Format tanggal tidak valid'),
feed_data: Yup.array()
.of(
Yup.object({
feed_id: Yup.string().required('Nama pakan wajib diisi!'),
feed_qty: Yup.mixed<number | ''>().notRequired(),
feed_stock: Yup.number()
.required('Jumlah pakan yang digunakan wajib diisi!')
.min(1, 'Jumlah pakan minimal 1!')
.typeError('Jumlah pakan yang digunakan harus berupa angka!')
.test(
'is-not-exceed-qty',
'Jumlah pakan yang digunakan tidak boleh melebihi stok tersedia!',
function (value) {
const { feed_qty } = this.parent;
if (value === undefined) return true;
if (
feed_qty === undefined ||
feed_qty === '' ||
typeof feed_qty !== 'number'
)
return true; return true;
return value <= feed_qty;
} }
), ),
body_weights: Yup.array() })
)
.min(1, 'Minimal harus ada 1 data pakan!')
.required('Data pakan wajib diisi!'),
body_weight: Yup.array()
.of( .of(
Yup.object({ Yup.object({
weight: Yup.number() chicken_weight: Yup.number()
.required('Berat ayam wajib diisi!') .required('Berat ayam wajib diisi!')
.min(1, 'Berat ayam minimal 1 gram!') .min(1, 'Berat ayam minimal 1 gram!')
.typeError('Berat ayam harus berupa angka!'), .typeError('Berat ayam harus berupa angka!'),
qty: Yup.number() chicken_count: Yup.number()
.required('Jumlah ayam wajib diisi!') .required('Jumlah ayam wajib diisi!')
.min(1, 'Jumlah ayam minimal 1 ekor!') .min(1, 'Jumlah ayam minimal 1 ekor!')
.typeError('Jumlah ayam harus berupa angka!') .typeError('Jumlah ayam harus berupa angka!'),
.default(1), average_chicken_weight: Yup.number()
average_weight: Yup.number() .required('Rata-rata berat ayam wajib diisi!')
.optional() .min(1, 'Rata-rata berat ayam minimal 1 gram!')
.min(0, 'Rata-rata berat tidak boleh negatif!') .typeError('Rata-rata berat ayam harus berupa angka!'),
.typeError('Rata-rata berat harus berupa angka!')
.default(0),
}) })
) )
.min(1, 'Minimal harus ada 1 data bobot badan!') .min(1, 'Minimal harus ada 1 data bobot badan!')
.required('Data bobot badan wajib diisi!'), .required('Data bobot badan wajib diisi!'),
stocks: Yup.array() vaccination: Yup.array()
.of( .of(
Yup.object({ Yup.object({
product_warehouse_id: Yup.number() vaccine_id: Yup.string().required('Nama vaksin wajib diisi!'),
.required('Produk wajib diisi!') total_stock: Yup.mixed<number | ''>().notRequired(),
.min(1, 'Produk wajib diisi!') used_stock: Yup.number()
.typeError('Produk harus berupa angka!'), .required('Jumlah vaksin yang digunakan wajib diisi!')
usage_amount: Yup.number() .min(1, 'Jumlah vaksin minimal 1!')
.required('Jumlah penggunaan wajib diisi!') .typeError('Jumlah vaksin yang digunakan harus berupa angka!')
.min(0, 'Jumlah penggunaan tidak boleh negatif!') .test(
.typeError('Jumlah penggunaan harus berupa angka!'), 'is-not-exceed-total',
notes: Yup.string().optional(), 'Jumlah vaksin yang digunakan tidak boleh melebihi stok tersedia!',
function (value) {
const { total_stock } = this.parent;
if (value === undefined) return true;
if (
total_stock === undefined ||
total_stock === '' ||
typeof total_stock !== 'number'
)
return true;
return value <= total_stock;
}
),
}) })
) )
.min(1, 'Minimal harus ada 1 data stok!') .min(1, 'Minimal harus ada 1 data vaksinasi!')
.required('Data stok wajib diisi!'), .required('Data vaksinasi wajib diisi!'),
depletions: Yup.array() mortality: Yup.array()
.of( .of(
Yup.object({ Yup.object({
total: Yup.number() condition: Yup.mixed<string>()
.required('Jumlah depletions wajib diisi!')
.min(1, 'Jumlah depletions minimal 1!')
.typeError('Jumlah depletions harus berupa angka!'),
notes: Yup.string()
.required('Kondisi depletions wajib diisi!')
.oneOf( .oneOf(
RECORDING_FLAG_OPTIONS.map((option) => option.value), RECORDING_FLAG_OPTIONS.map((opt) => opt.value),
'Kondisi depletions tidak valid!' 'Kondisi tidak valid!'
) )
.typeError('Kondisi depletions harus berupa teks!') .required('Kondisi wajib diisi!'),
.min(1, 'Kondisi depletions wajib diisi!'), count: Yup.number()
.required('Jumlah mortalitas wajib diisi!')
.min(1, 'Jumlah mortalitas minimal 1 ekor!')
.typeError('Jumlah mortalitas harus berupa angka!'),
}) })
) )
.min(1, 'Minimal harus ada 1 data depletions!') .min(1, 'Minimal harus ada 1 data mortalitas!')
.required('Data depletions wajib diisi!'), .required('Data mortalitas wajib diisi!'),
}); });
export const UpdateRecordingFormSchema = Yup.object({ export const UpdateRecordingFormSchema = RecordingFormSchema;
project_flock_kandang: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
project_flock_kandang_id: Yup.number()
.default(0)
.typeError('Project Flock Kandang wajib diisi!')
.test(
'is-valid-project-flock-kandang',
'Project Flock Kandang wajib diisi!',
(value) => value !== undefined && value !== null && value > 0
)
.required('Project Flock Kandang wajib diisi!'),
body_weights: Yup.array()
.of(
Yup.object({
weight: Yup.number()
.required('Berat ayam wajib diisi!')
.min(1, 'Berat ayam minimal 1 gram!')
.typeError('Berat ayam harus berupa angka!'),
qty: Yup.number()
.required('Jumlah ayam wajib diisi!')
.min(1, 'Jumlah ayam minimal 1 ekor!')
.typeError('Jumlah ayam harus berupa angka!')
.default(1),
average_weight: Yup.number()
.optional()
.min(0, 'Rata-rata berat tidak boleh negatif!')
.typeError('Rata-rata berat harus berupa angka!')
.default(0),
})
)
.min(1, 'Minimal harus ada 1 data bobot badan!')
.required('Data bobot badan wajib diisi!'),
stocks: Yup.array()
.of(
Yup.object({
product_warehouse_id: Yup.number()
.required('Produk wajib diisi!')
.min(1, 'Produk wajib diisi!')
.typeError('Produk harus berupa angka!'),
usage_amount: Yup.number()
.required('Jumlah penggunaan wajib diisi!')
.min(0, 'Jumlah penggunaan tidak boleh negatif!')
.typeError('Jumlah penggunaan harus berupa angka!'),
notes: Yup.string().optional(),
})
)
.min(1, 'Minimal harus ada 1 data stok!')
.required('Data stok wajib diisi!'),
depletions: Yup.array()
.of(
Yup.object({
total: Yup.number()
.required('Jumlah depletions wajib diisi!')
.min(1, 'Jumlah depletions minimal 1!')
.typeError('Jumlah depletions harus berupa angka!'),
notes: Yup.string()
.required('Kondisi depletions wajib diisi!')
.oneOf(
RECORDING_FLAG_OPTIONS.map((option) => option.value),
'Kondisi depletions tidak valid!'
)
.typeError('Kondisi depletions harus berupa teks!')
.min(1, 'Kondisi depletions wajib diisi!'),
})
)
.min(1, 'Minimal harus ada 1 data depletions!')
.required('Data depletions wajib diisi!'),
});
export type RecordingFormValues = Yup.InferType<typeof RecordingFormSchema>; export type RecordingFormValues = Yup.InferType<typeof RecordingFormSchema>;
type RecordingFormData = Partial<Recording> & {
body_weights?: CreateRecordingPayload['body_weights'];
stocks?: CreateRecordingPayload['stocks'];
depletions?: CreateRecordingPayload['depletions'];
};
export const getRecordingFormInitialValues = ( export const getRecordingFormInitialValues = (
initialValues?: RecordingFormData initialValues?: Recording
): RecordingFormValues => ({ ): RecordingFormValues => ({
project_flock_kandang: initialValues?.project_flock_kandang_id flock: initialValues?.flock
? { ? {
value: initialValues.project_flock_kandang_id, value: initialValues.flock.id,
label: `Project Flock #${initialValues.project_flock_kandang_id}`, label: initialValues.flock.name,
} }
: null, : null,
project_flock_kandang_id: initialValues?.project_flock_kandang_id ?? 0, flock_id: initialValues?.flock?.id ?? 0,
body_weights: initialValues?.body_weights?.map( location: initialValues?.location
(bw: NonNullable<CreateRecordingPayload['body_weights']>[0]) => ({ ? {
weight: bw.weight, value: initialValues.location.id,
qty: bw.qty, label: initialValues.location.name,
average_weight: bw.qty > 0 ? Math.round(bw.weight / bw.qty) : 0, }
}) : null,
) ?? [ location_id: initialValues?.location?.id ?? 0,
coop: initialValues?.coop
? {
value: initialValues.coop.id,
label: initialValues.coop.name,
}
: null,
coop_id: initialValues?.coop?.id ?? 0,
recording_date: initialValues?.recording_date
? new Date(initialValues.recording_date)
: new Date(),
feed_data: initialValues?.feed_data
? initialValues.feed_data.map((feed) => ({
feed_id: feed.feed_name,
feed_qty: feed.feed_qty,
feed_stock: feed.feed_stock,
}))
: [
{ {
weight: 0, feed_id: '',
qty: 0, feed_qty: '',
average_weight: 0, feed_stock: 0,
}, },
], ],
stocks: initialValues?.stocks?.map( body_weight: initialValues?.body_weight ?? [
(stock: NonNullable<CreateRecordingPayload['stocks']>[0]) => ({
product_warehouse_id: stock.product_warehouse_id,
usage_amount: stock.usage_amount,
notes: stock.notes,
})
) ?? [
{ {
product_warehouse_id: 0, chicken_weight: 0,
usage_amount: 0, chicken_count: 0,
notes: '', average_chicken_weight: 0,
}, },
], ],
depletions: initialValues?.depletions?.map( vaccination: initialValues?.vaccination
(depletion: NonNullable<CreateRecordingPayload['depletions']>[0]) => ({ ? initialValues.vaccination.map((vaccine) => ({
product_warehouse_id: depletion.product_warehouse_id, vaccine_id: vaccine.vaccine_name,
total: depletion.total, total_stock: vaccine.total_stock,
notes: depletion.notes, used_stock: vaccine.used_stock,
}) }))
) ?? [ : [
{ {
product_warehouse_id: 0, vaccine_id: '',
total: 0, total_stock: '',
notes: '', used_stock: 0,
},
],
mortality: initialValues?.mortality ?? [
{
condition: '',
count: 0,
}, },
], ],
}); });
@@ -24,7 +24,7 @@ export const useRecordingFormHandlers = (initialValuesId?: number) => {
return; return;
} }
toast.success(res?.message as string); toast.success(res?.message as string);
router.push('/production/recording'); router.push('/flock/recording');
}, },
[router] [router]
); );
@@ -38,7 +38,7 @@ export const useRecordingFormHandlers = (initialValuesId?: number) => {
} }
toast.success(res?.message as string); toast.success(res?.message as string);
router.refresh(); router.refresh();
router.push('/production/recording'); router.push('/flock/recording');
}, },
[router] [router]
); );
@@ -55,7 +55,7 @@ export const useRecordingFormHandlers = (initialValuesId?: number) => {
deleteModal.closeModal(); deleteModal.closeModal();
toast.success('Successfully delete Recording!'); toast.success('Successfully delete Recording!');
setIsDeleteLoading(false); setIsDeleteLoading(false);
router.push('/production/recording'); router.push('/flock/recording');
}, [deleteModal, initialValuesId, router]); }, [deleteModal, initialValuesId, router]);
return { return {
+1 -2
View File
@@ -71,8 +71,7 @@ export class StaffApprovalService extends BaseApiService<
} }
); );
} }
}
}
export class AcceptApprovalService extends BaseApiService< export class AcceptApprovalService extends BaseApiService<
Purchase, Purchase,
+48 -33
View File
@@ -1,45 +1,60 @@
import { BaseMetadata, User } from '@/types/api/api-general'; import { BaseMetadata } from '@/types/api/api-general';
import { Location } from '@/types/api/master-data/location';
export type ProductionMetrics = { import { Kandang } from '@/types/api/master-data/kandang';
total_depletion: number; import { Flock } from '@/types/api/master-data/flock';
cum_depletion_rate: number;
daily_gain: number;
avg_daily_gain: number;
cum_intake: number;
fcr_value: number;
total_chick: number;
daily_depletion_rate: number;
cum_depletion: number;
};
export type BaseRecording = { export type BaseRecording = {
id: number; id: number;
project_flock_kandang_id: number; flock: Flock;
record_datetime: string; recording_date: string;
record_date: string; location: Location;
status: number; coop: Kandang;
ontime: boolean; feed_data: {
day: number; feed_name: string;
created_user: User; feed_qty: number;
} & ProductionMetrics; feed_stock: number;
}[];
body_weight: {
chicken_weight: number;
chicken_count: number;
average_chicken_weight: number;
}[];
vaccination: {
vaccine_name: string;
total_stock: number;
used_stock: number;
}[];
mortality: {
condition: string;
count: number;
}[];
};
export type Recording = BaseMetadata & BaseRecording; export type Recording = BaseMetadata & BaseRecording;
export type CreateRecordingPayload = { export type CreateRecordingPayload = {
project_flock_kandang_id: number; flock_id: number;
body_weights: { recording_date: string;
weight: number; location_id: number;
qty: number; coop_id: number;
feed_data: {
feed_id: string;
feed_qty: number;
feed_stock: number;
}[]; }[];
stocks?: { body_weight: {
product_warehouse_id: number; chicken_weight: number;
usage_amount: number; chicken_count: number;
notes: string; average_chicken_weight: number;
}[]; }[];
depletions?: { vaccination: {
product_warehouse_id?: number; vaccine_id: string;
total: number; total_stock: number;
notes: string; used_stock: number;
}[];
mortality: {
condition: string;
count: number;
}[]; }[];
}; };