From b3f4e42f1a0631b13943e87cc0e8772b88ecf244 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 13 Nov 2025 14:01:07 +0700 Subject: [PATCH] chore: prettier format --- .gitlab-ci.yml | 25 +- .husky/pre-commit | 3 +- docker-compose.yaml | 12 +- src/app/purchase/{detail => }/layout.tsx | 0 src/components/helper/form/FormActions.tsx | 78 +-- src/components/input/PatternInput.tsx | 16 +- .../movement/form/MovementForm.schema.ts | 61 +- .../form/ProductCategoryForm.schema.ts | 4 +- .../product/form/ProductForm.schema.ts | 8 +- .../master-data/product/form/ProductForm.tsx | 4 +- .../production/recording/RecordingTable.tsx | 606 ++++++------------ .../recording/form/RecordingForm.schema.ts | 334 +++++----- .../form/useRecordingFormHandlers.ts | 6 +- src/services/api/purchase.ts | 3 +- src/types/api/production/recording.d.ts | 81 ++- 15 files changed, 522 insertions(+), 719 deletions(-) rename src/app/purchase/{detail => }/layout.tsx (100%) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 345f305f..951e5472 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,7 @@ stages: - build - deploy - + .build_template: &build_template stage: build image: node:20-alpine @@ -10,15 +10,15 @@ stages: paths: - node_modules/ variables: - NPM_CONFIG_PRODUCTION: "false" - NODE_ENV: "" + NPM_CONFIG_PRODUCTION: 'false' + NODE_ENV: '' script: - echo "Installing dependencies..." - npm ci --no-audit --no-fund - echo "Building Next.js static export..." - npx next build artifacts: - name: "out-$CI_COMMIT_SHORT_SHA" + name: 'out-$CI_COMMIT_SHORT_SHA' paths: - out/ expire_in: 1 week @@ -27,7 +27,7 @@ stages: stage: deploy image: name: amazon/aws-cli:latest - entrypoint: ["/bin/sh", "-c"] + entrypoint: ['/bin/sh', '-c'] script: - set -e - aws --version @@ -106,22 +106,21 @@ build:dev: environment: name: development variables: - NEXT_PUBLIC_API_BASE_URL: "https://dev-api-lti.mbugroup.id" - NEXT_PUBLIC_SSO_LOGIN_URL: "https://dev-api-sso.mbugroup.id" + NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id' + NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-api-sso.mbugroup.id' deploy:dev: <<: *deploy_template - needs: ["build:dev"] + needs: ['build:dev'] rules: - if: '$CI_COMMIT_BRANCH == "development"' variables: - S3_BUCKET: "dev-lti-erp.mbugroup.id" - AWS_REGION: "ap-southeast-3" - CLOUDFRONT_DISTRIBUTION_ID: "E1Z8XTA8XF1GIV" + S3_BUCKET: 'dev-lti-erp.mbugroup.id' + AWS_REGION: 'ap-southeast-3' + CLOUDFRONT_DISTRIBUTION_ID: 'E1Z8XTA8XF1GIV' environment: name: development url: https://dev-lti-erp.mbugroup.id - # ====== PRODUCTION ====== # build:production: # <<: *build_template @@ -143,5 +142,5 @@ deploy:dev: # CLOUDFRONT_DISTRIBUTION_ID: "ddfd" # environment: # name: production - # url: https://royalgoldcapital.com +# url: https://royalgoldcapital.com diff --git a/.husky/pre-commit b/.husky/pre-commit index 66ff6a67..3782914b 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,3 @@ +npm run format npm run lint -npm run build +npm run build \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 8d658170..b89f441b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,4 +1,4 @@ -version: "3.9" +version: '3.9' services: dev-web-lti: @@ -7,7 +7,7 @@ services: context: . dockerfile: Dockerfile ports: - - "3002:3000" + - '3002:3000' env_file: - .env environment: @@ -19,13 +19,13 @@ services: deploy: resources: limits: - cpus: "3.0" + cpus: '3.0' memory: 3G reservations: - cpus: "1.0" + cpus: '1.0' memory: 512M extra_hosts: - - "host.docker.internal:host-gateway" + - 'host.docker.internal:host-gateway' # Optional: aktifkan healthcheck jika punya endpoint # healthcheck: # test: ["CMD-SHELL", "curl -fsS http://localhost:3000/api/healthz || exit 1"] @@ -36,4 +36,4 @@ services: networks: dev-lti-network: - external: true \ No newline at end of file + external: true diff --git a/src/app/purchase/detail/layout.tsx b/src/app/purchase/layout.tsx similarity index 100% rename from src/app/purchase/detail/layout.tsx rename to src/app/purchase/layout.tsx diff --git a/src/components/helper/form/FormActions.tsx b/src/components/helper/form/FormActions.tsx index 111b052f..4968f93e 100644 --- a/src/components/helper/form/FormActions.tsx +++ b/src/components/helper/form/FormActions.tsx @@ -64,44 +64,46 @@ export const FormActions = ({ Edit )} - {type === 'detail' && showApproveReject && (onApprove || onReject) && ( - <> - {onApprove && ( - - )} - {onReject && ( - - )} - - )} + {type === 'detail' && + showApproveReject && + (onApprove || onReject) && ( + <> + {onApprove && ( + + )} + {onReject && ( + + )} + + )} )} {type !== 'detail' && ( diff --git a/src/components/input/PatternInput.tsx b/src/components/input/PatternInput.tsx index 1905d2e3..b5f4f65d 100644 --- a/src/components/input/PatternInput.tsx +++ b/src/components/input/PatternInput.tsx @@ -20,14 +20,14 @@ interface PatternInputProps extends Omit { } const PatternInput = ({ - type = 'text', - format, - mask = '_', - allowEmptyFormatting = false, - patternChar = '#', - onChange, - ...restProps - }: PatternInputProps) => { + type = 'text', + format, + mask = '_', + allowEmptyFormatting = false, + patternChar = '#', + onChange, + ...restProps +}: PatternInputProps) => { const valueChangeHandler: OnValueChange = ( patternFormatValues, sourceInfo diff --git a/src/components/pages/inventory/movement/form/MovementForm.schema.ts b/src/components/pages/inventory/movement/form/MovementForm.schema.ts index 195873b7..39f7c669 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.schema.ts +++ b/src/components/pages/inventory/movement/form/MovementForm.schema.ts @@ -150,36 +150,37 @@ const DeliveryObjectSchema: Yup.ObjectSchema = Yup.object({ .required('Produk wajib diisi!'), }); -export const MovementFormSchema: Yup.ObjectSchema = Yup.object({ - transfer_reason: Yup.string().required('Alasan transfer wajib diisi!'), - transfer_date: Yup.string().required('Tanggal transfer wajib diisi!'), - source_warehouse: Yup.object({ - value: Yup.number().min(1).required(), - label: Yup.string().required(), - area: Yup.string().optional(), - location: Yup.string().optional(), - }).nullable(), - source_warehouse_id: Yup.number() - .required('Gudang asal wajib diisi!') - .typeError('Gudang asal wajib diisi!'), - destination_warehouse: Yup.object({ - value: Yup.number().min(1).required(), - label: Yup.string().required(), - area: Yup.string().optional(), - location: Yup.string().optional(), - }).nullable(), - destination_warehouse_id: Yup.number() - .required('Gudang tujuan wajib diisi!') - .typeError('Gudang tujuan wajib diisi!'), - products: Yup.array() - .of(ProductObjectSchema) - .min(1, 'Minimal harus ada 1 produk!') - .required('Produk wajib diisi!'), - deliveries: Yup.array() - .of(DeliveryObjectSchema) - .min(1, 'Minimal harus ada 1 pengiriman!') - .required('Pengiriman wajib diisi!'), -}); +export const MovementFormSchema: Yup.ObjectSchema = + Yup.object({ + transfer_reason: Yup.string().required('Alasan transfer wajib diisi!'), + transfer_date: Yup.string().required('Tanggal transfer wajib diisi!'), + source_warehouse: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + area: Yup.string().optional(), + location: Yup.string().optional(), + }).nullable(), + source_warehouse_id: Yup.number() + .required('Gudang asal wajib diisi!') + .typeError('Gudang asal wajib diisi!'), + destination_warehouse: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + area: Yup.string().optional(), + location: Yup.string().optional(), + }).nullable(), + destination_warehouse_id: Yup.number() + .required('Gudang tujuan wajib diisi!') + .typeError('Gudang tujuan wajib diisi!'), + products: Yup.array() + .of(ProductObjectSchema) + .min(1, 'Minimal harus ada 1 produk!') + .required('Produk wajib diisi!'), + deliveries: Yup.array() + .of(DeliveryObjectSchema) + .min(1, 'Minimal harus ada 1 pengiriman!') + .required('Pengiriman wajib diisi!'), + }); export type MovementFormValues = Yup.InferType; diff --git a/src/components/pages/master-data/product-category/form/ProductCategoryForm.schema.ts b/src/components/pages/master-data/product-category/form/ProductCategoryForm.schema.ts index d97e755a..c9cb2b7b 100644 --- a/src/components/pages/master-data/product-category/form/ProductCategoryForm.schema.ts +++ b/src/components/pages/master-data/product-category/form/ProductCategoryForm.schema.ts @@ -15,4 +15,6 @@ export const ProductCategoryFormSchema: Yup.ObjectSchema; \ No newline at end of file +export type ProductCategoryFormValues = Yup.InferType< + typeof ProductCategoryFormSchema +>; diff --git a/src/components/pages/master-data/product/form/ProductForm.schema.ts b/src/components/pages/master-data/product/form/ProductForm.schema.ts index 5db141a3..37881636 100644 --- a/src/components/pages/master-data/product/form/ProductForm.schema.ts +++ b/src/components/pages/master-data/product/form/ProductForm.schema.ts @@ -31,7 +31,9 @@ export const ProductFormSchema: Yup.ObjectSchema = uom: Yup.object({ value: Yup.number().min(1).required(), label: Yup.string().required(), - }).nullable().required('Satuan wajib diisi!'), + }) + .nullable() + .required('Satuan wajib diisi!'), uom_id: Yup.number() .required('Satuan wajib diisi!') @@ -40,7 +42,9 @@ export const ProductFormSchema: Yup.ObjectSchema = product_category: Yup.object({ value: Yup.number().min(1).required(), label: Yup.string().required(), - }).nullable().required('Kategori produk wajib diisi!'), + }) + .nullable() + .required('Kategori produk wajib diisi!'), product_category_id: Yup.number() .required('Kategori produk wajib diisi!') diff --git a/src/components/pages/master-data/product/form/ProductForm.tsx b/src/components/pages/master-data/product/form/ProductForm.tsx index 20527398..44457c81 100644 --- a/src/components/pages/master-data/product/form/ProductForm.tsx +++ b/src/components/pages/master-data/product/form/ProductForm.tsx @@ -121,9 +121,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { supplier_ids: values.supplier_ids.filter( (id): id is number => typeof id === 'number' ), - flags: values.flags.filter( - (f): f is string => typeof f === 'string' - ), + flags: values.flags.filter((f): f is string => typeof f === 'string'), }; switch (type) { case 'add': diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index d6edaa87..e02e3e56 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -1,7 +1,6 @@ 'use client'; import { useCallback, useMemo, useState } from 'react'; -import useSWR from 'swr'; import { Icon } from '@iconify/react'; import { SortingState } from '@tanstack/react-table'; import { cn } from '@/lib/helper'; @@ -9,9 +8,7 @@ import { useModal } from '@/components/Modal'; import Button from '@/components/Button'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import { OptionType } from '@/components/input/SelectInput'; -import SelectInput from '@/components/input/SelectInput'; import { ROWS_OPTIONS } from '@/config/constant'; -import CheckboxInput from '@/components/input/CheckboxInput'; import { TableToolbar } from '@/components/table/TableToolbar'; import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector'; import Table from '@/components/Table'; @@ -20,14 +17,106 @@ import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import { type CellContext } from '@tanstack/react-table'; import { type Recording } from '@/types/api/production/recording'; -import { type BaseApiResponse } from '@/types/api/api-general'; -import { RecordingApi } from '@/services/api/production'; -import { AreaApi } from '@/services/api/master-data'; -import { LocationApi } from '@/services/api/master-data'; -import { KandangApi } from '@/services/api/master-data'; -import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; -import { useTableFilter } from '@/services/hooks/useTableFilter'; -import toast from 'react-hot-toast'; + +const dummyRecordings: Recording[] = [ + { + id: 1, + flock: { + id: 1, + name: 'Flock Recording 1', + 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 = ({ type = 'dropdown', @@ -77,34 +166,12 @@ const RowOptionsMenu = ({ }; const RecordingTable = () => { - const { - state: tableFilterState, - updateFilter, - 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 [search, setSearch] = useState(''); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); const [sorting, setSorting] = useState([]); - const [rowSelection, setRowSelection] = useState>({}); - const [selectedRecording, setSelectedRecording] = useState(undefined); + const [selectedRecordings, setSelectedRecordings] = useState([]); + const [, setSelectedRecording] = useState(undefined); const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isBulkApproveLoading, setIsBulkApproveLoading] = useState(false); const [isBulkRejectLoading, setIsBulkRejectLoading] = useState(false); @@ -113,81 +180,12 @@ const RecordingTable = () => { const bulkApproveModal = useModal(); const bulkRejectModal = useModal(); - // State for dropdown search - const [locationSelectInputValue, setLocationSelectInputValue] = useState(''); - const [areaSelectInputValue, setAreaSelectInputValue] = useState(''); - const [kandangSelectInputValue, setKandangSelectInputValue] = useState(''); - - const [selectedArea, setSelectedArea] = useState(null); - const [selectedLocation, setSelectedLocation] = useState(null); - const [selectedKandang, setSelectedKandang] = useState(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( (e: React.ChangeEvent) => { - updateFilter('search', e.target.value); + setSearch(e.target.value); setPage(1); }, - [updateFilter, setPage] + [] ); const pageSizeChangeHandler = useCallback( @@ -196,80 +194,52 @@ const RecordingTable = () => { setPageSize(newVal.value as number); setPage(1); }, - [setPageSize, setPage] + [] ); const paginatedData = useMemo(() => { - if (!recordings || recordings.status !== 'success') return []; - - return recordings.data; - }, [recordings]); - - const selectedRowIds = Object.keys(rowSelection).map((item) => parseInt(item)); + const filteredData = dummyRecordings.filter( + (recording) => + recording.flock.name.toLowerCase().includes(search.toLowerCase()) || + recording.location.name.toLowerCase().includes(search.toLowerCase()) || + recording.coop.name.toLowerCase().includes(search.toLowerCase()) + ); + const start = (page - 1) * pageSize; + return filteredData.slice(start, start + pageSize); + }, [page, pageSize, search]); const bulkApproveHandler = async () => { setIsBulkApproveLoading(true); - - const approveResponse = await RecordingApi.customRequest< - BaseApiResponse - >('approvals', { - method: 'POST', - payload: { - action: 'APPROVED', - approvable_ids: selectedRowIds, - notes: 'Bulk Approved', - }, - }); - - if (isResponseSuccess(approveResponse)) { - await refreshRecordings(); - setRowSelection({}); + console.log( + 'Approved recordings:', + paginatedData.filter((_, idx) => selectedRecordings.includes(idx)) + ); + setTimeout(() => { + setIsBulkApproveLoading(false); + setSelectedRecordings([]); bulkApproveModal.closeModal(); - toast.success(`Successfully approved ${selectedRowIds.length} recordings!`); - } - if (isResponseError(approveResponse)) { - toast.error(approveResponse?.message as string); - bulkApproveModal.closeModal(); - } - setIsBulkApproveLoading(false); + }, 1000); }; const bulkRejectHandler = async () => { setIsBulkRejectLoading(true); - - const rejectResponse = await RecordingApi.customRequest< - BaseApiResponse - >('approvals', { - method: 'POST', - payload: { - action: 'REJECTED', - approvable_ids: selectedRowIds, - notes: 'Bulk Rejected', - }, - }); - - if (isResponseSuccess(rejectResponse)) { - refreshRecordings(); - setRowSelection({}); + console.log( + 'Rejected recordings:', + paginatedData.filter((_, idx) => selectedRecordings.includes(idx)) + ); + setTimeout(() => { + setIsBulkRejectLoading(false); + setSelectedRecordings([]); bulkRejectModal.closeModal(); - toast.success(`Successfully rejected ${selectedRowIds.length} recordings!`); - } - if (isResponseError(rejectResponse)) { - toast.error(rejectResponse?.message as string); - bulkRejectModal.closeModal(); - } - setIsBulkRejectLoading(false); + }, 1000); }; const singleDeleteHandler = async () => { setIsDeleteLoading(true); - - await RecordingApi.delete(selectedRecording?.id as number); - refreshRecordings(); - - singleDeleteModal.closeModal(); - toast.success('Successfully delete Recording!'); - setIsDeleteLoading(false); + setTimeout(() => { + setIsDeleteLoading(false); + singleDeleteModal.closeModal(); + }, 1000); }; return ( @@ -281,189 +251,21 @@ const RecordingTable = () => { label: 'Tambah', }} search={{ - value: tableFilterState.search, + value: search, onChange: searchChangeHandler, placeholder: 'Cari Recording', }} /> - - {/* Filter Dropdowns - Desktop */} -
- { - 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 - /> - - { - 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} - /> - - { - 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} - /> - - { - const selectedValue = selected as OptionType | null; - updateFilter('periodFilter', selectedValue ? selectedValue.value.toString() : ''); - setPage(1); - }} - className={{ wrapper: 'w-full' }} - isClearable - /> -
- - {/* Filter Dropdowns - Mobile */} -
- { - 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 - /> - - { - 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} - /> - - { - 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} - /> - - { - const selectedValue = selected as OptionType | null; - updateFilter('periodFilter', selectedValue ? selectedValue.value.toString() : ''); - setPage(1); - }} - className={{ wrapper: 'w-full' }} - isClearable - /> -
{/* Bulk action buttons */}
- {selectedRowIds.length > 0 && ( + {selectedRecordings.length > 0 && (
)} @@ -497,7 +299,7 @@ const RecordingTable = () => { { { columns={[ { id: 'select', + accessorKey: 'id', header: ({ table }) => ( -
- -
+ 0 && + 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([]); + } + }} + /> ), cell: ({ row }) => ( -
- -
+ { + if (e.target.checked) { + setSelectedRecordings([...selectedRecordings, row.index]); + } else { + setSelectedRecordings( + selectedRecordings.filter((i) => i !== row.index) + ); + } + }} + /> ), }, { header: '#', - cell: (props) => tableFilterState.pageSize * (tableFilterState.page - 1) + props.row.index + 1, + cell: (props) => pageSize * (page - 1) + props.row.index + 1, }, { - header: 'Nama Project', - cell: (props) => `Project ${props.row.original.project_flock_kandang_id}`, + accessorKey: 'flock.name', + header: 'Flock', }, { - header: 'Umur (hari)', - cell: (props) => props.row.original.day, - }, - { - accessorKey: 'record_date', - header: 'Waktu Recording', + accessorKey: 'recording_date', + header: 'Tanggal Recording', cell: (props) => - new Date(props.row.original.record_date).toLocaleDateString(), + new Date(props.row.original.recording_date).toLocaleDateString(), }, { - header: 'Populasi Awal', - cell: (props) => props.row.original.total_chick?.toLocaleString() || '-', + accessorKey: 'location.name', + header: 'Lokasi', }, { - header: 'BW', - cell: (props) => props.row.original.avg_daily_gain?.toFixed(2) || '-', + accessorKey: 'coop.name', + header: 'Kandang', }, { - header: 'Pakan', - cell: (props) => props.row.original.cum_intake?.toLocaleString() || '-', - }, - { - 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', + accessorKey: 'mortality', + header: 'Total Mortality', cell: (props) => - new Date(props.row.original.created_at).toLocaleString(), + props.row.original.mortality.reduce( + (acc, curr) => acc + curr.count, + 0 + ), }, { header: 'Aksi', @@ -651,15 +445,13 @@ const RecordingTable = () => { }, }, ]} - pageSize={tableFilterState.pageSize} - page={recordings?.status === 'success' ? recordings.meta?.page : tableFilterState.page} - totalItems={recordings?.status === 'success' ? recordings.meta?.total_results : 0} + pageSize={pageSize} + page={page} + totalItems={dummyRecordings.length} onPageChange={setPage} - isLoading={isLoading} + isLoading={false} sorting={sorting} setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} className={{ containerClassName: cn({ 'mb-20': paginatedData.length === 0, @@ -678,7 +470,7 @@ const RecordingTable = () => { 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( - 'not-already-recorded', - 'Project Flock ini sudah direcord hari ini!', - function(value) { - const recordedProjectFlockIds = this.options.context?.recordedProjectFlockIds as Set; - const formType = this.options.context?.type as 'add' | 'edit' | 'detail'; - if (formType !== 'add') return true; - if (value && recordedProjectFlockIds?.has(value)) { - return false; - } - return true; - } - ), - body_weights: Yup.array() + 'is-valid-location', + 'Lokasi wajib diisi!', + (value) => value !== undefined && value !== null && value > 0 + ) + .required('Lokasi wajib diisi!'), + coop: Yup.object({ + value: Yup.number().min(1).required(), + 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({ - weight: Yup.number() + feed_id: Yup.string().required('Nama pakan wajib diisi!'), + feed_qty: Yup.mixed().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 value <= feed_qty; + } + ), + }) + ) + .min(1, 'Minimal harus ada 1 data pakan!') + .required('Data pakan wajib diisi!'), + body_weight: Yup.array() + .of( + Yup.object({ + chicken_weight: Yup.number() .required('Berat ayam wajib diisi!') .min(1, 'Berat ayam minimal 1 gram!') .typeError('Berat ayam harus berupa angka!'), - qty: Yup.number() + chicken_count: 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), + .typeError('Jumlah ayam harus berupa angka!'), + average_chicken_weight: Yup.number() + .required('Rata-rata berat ayam wajib diisi!') + .min(1, 'Rata-rata berat ayam minimal 1 gram!') + .typeError('Rata-rata berat ayam harus berupa angka!'), }) ) .min(1, 'Minimal harus ada 1 data bobot badan!') .required('Data bobot badan wajib diisi!'), - stocks: Yup.array() + vaccination: 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(), + vaccine_id: Yup.string().required('Nama vaksin wajib diisi!'), + total_stock: Yup.mixed().notRequired(), + used_stock: Yup.number() + .required('Jumlah vaksin yang digunakan wajib diisi!') + .min(1, 'Jumlah vaksin minimal 1!') + .typeError('Jumlah vaksin yang digunakan harus berupa angka!') + .test( + 'is-not-exceed-total', + '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!') - .required('Data stok wajib diisi!'), - depletions: Yup.array() + .min(1, 'Minimal harus ada 1 data vaksinasi!') + .required('Data vaksinasi wajib diisi!'), + mortality: 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!') + condition: Yup.mixed() .oneOf( - RECORDING_FLAG_OPTIONS.map((option) => option.value), - 'Kondisi depletions tidak valid!' + RECORDING_FLAG_OPTIONS.map((opt) => opt.value), + 'Kondisi tidak valid!' ) - .typeError('Kondisi depletions harus berupa teks!') - .min(1, 'Kondisi depletions wajib diisi!'), + .required('Kondisi 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!') - .required('Data depletions wajib diisi!'), + .min(1, 'Minimal harus ada 1 data mortalitas!') + .required('Data mortalitas wajib diisi!'), }); -export const UpdateRecordingFormSchema = Yup.object({ - 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 const UpdateRecordingFormSchema = RecordingFormSchema; export type RecordingFormValues = Yup.InferType; -type RecordingFormData = Partial & { - body_weights?: CreateRecordingPayload['body_weights']; - stocks?: CreateRecordingPayload['stocks']; - depletions?: CreateRecordingPayload['depletions']; -}; - export const getRecordingFormInitialValues = ( - initialValues?: RecordingFormData + initialValues?: Recording ): RecordingFormValues => ({ - project_flock_kandang: initialValues?.project_flock_kandang_id + flock: initialValues?.flock ? { - value: initialValues.project_flock_kandang_id, - label: `Project Flock #${initialValues.project_flock_kandang_id}`, + value: initialValues.flock.id, + label: initialValues.flock.name, } : null, - project_flock_kandang_id: initialValues?.project_flock_kandang_id ?? 0, - body_weights: initialValues?.body_weights?.map( - (bw: NonNullable[0]) => ({ - weight: bw.weight, - qty: bw.qty, - average_weight: bw.qty > 0 ? Math.round(bw.weight / bw.qty) : 0, - }) - ) ?? [ + flock_id: initialValues?.flock?.id ?? 0, + location: initialValues?.location + ? { + value: initialValues.location.id, + label: initialValues.location.name, + } + : 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, + })) + : [ + { + feed_id: '', + feed_qty: '', + feed_stock: 0, + }, + ], + body_weight: initialValues?.body_weight ?? [ { - weight: 0, - qty: 0, - average_weight: 0, + chicken_weight: 0, + chicken_count: 0, + average_chicken_weight: 0, }, ], - stocks: initialValues?.stocks?.map( - (stock: NonNullable[0]) => ({ - product_warehouse_id: stock.product_warehouse_id, - usage_amount: stock.usage_amount, - notes: stock.notes, - }) - ) ?? [ + vaccination: initialValues?.vaccination + ? initialValues.vaccination.map((vaccine) => ({ + vaccine_id: vaccine.vaccine_name, + total_stock: vaccine.total_stock, + used_stock: vaccine.used_stock, + })) + : [ + { + vaccine_id: '', + total_stock: '', + used_stock: 0, + }, + ], + mortality: initialValues?.mortality ?? [ { - product_warehouse_id: 0, - usage_amount: 0, - notes: '', - }, - ], - depletions: initialValues?.depletions?.map( - (depletion: NonNullable[0]) => ({ - product_warehouse_id: depletion.product_warehouse_id, - total: depletion.total, - notes: depletion.notes, - }) - ) ?? [ - { - product_warehouse_id: 0, - total: 0, - notes: '', + condition: '', + count: 0, }, ], }); diff --git a/src/components/pages/production/recording/form/useRecordingFormHandlers.ts b/src/components/pages/production/recording/form/useRecordingFormHandlers.ts index 58893ce1..334b791d 100644 --- a/src/components/pages/production/recording/form/useRecordingFormHandlers.ts +++ b/src/components/pages/production/recording/form/useRecordingFormHandlers.ts @@ -24,7 +24,7 @@ export const useRecordingFormHandlers = (initialValuesId?: number) => { return; } toast.success(res?.message as string); - router.push('/production/recording'); + router.push('/flock/recording'); }, [router] ); @@ -38,7 +38,7 @@ export const useRecordingFormHandlers = (initialValuesId?: number) => { } toast.success(res?.message as string); router.refresh(); - router.push('/production/recording'); + router.push('/flock/recording'); }, [router] ); @@ -55,7 +55,7 @@ export const useRecordingFormHandlers = (initialValuesId?: number) => { deleteModal.closeModal(); toast.success('Successfully delete Recording!'); setIsDeleteLoading(false); - router.push('/production/recording'); + router.push('/flock/recording'); }, [deleteModal, initialValuesId, router]); return { diff --git a/src/services/api/purchase.ts b/src/services/api/purchase.ts index 56590975..4768d94f 100644 --- a/src/services/api/purchase.ts +++ b/src/services/api/purchase.ts @@ -71,8 +71,7 @@ export class StaffApprovalService extends BaseApiService< } ); } - - } +} export class AcceptApprovalService extends BaseApiService< Purchase, diff --git a/src/types/api/production/recording.d.ts b/src/types/api/production/recording.d.ts index dcbbeed6..6fac0bc8 100644 --- a/src/types/api/production/recording.d.ts +++ b/src/types/api/production/recording.d.ts @@ -1,45 +1,60 @@ -import { BaseMetadata, User } from '@/types/api/api-general'; - -export type ProductionMetrics = { - total_depletion: number; - 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; -}; +import { BaseMetadata } from '@/types/api/api-general'; +import { Location } from '@/types/api/master-data/location'; +import { Kandang } from '@/types/api/master-data/kandang'; +import { Flock } from '@/types/api/master-data/flock'; export type BaseRecording = { id: number; - project_flock_kandang_id: number; - record_datetime: string; - record_date: string; - status: number; - ontime: boolean; - day: number; - created_user: User; -} & ProductionMetrics; + flock: Flock; + recording_date: string; + location: Location; + coop: Kandang; + feed_data: { + feed_name: string; + feed_qty: number; + 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 CreateRecordingPayload = { - project_flock_kandang_id: number; - body_weights: { - weight: number; - qty: number; + flock_id: number; + recording_date: string; + location_id: number; + coop_id: number; + feed_data: { + feed_id: string; + feed_qty: number; + feed_stock: number; }[]; - stocks?: { - product_warehouse_id: number; - usage_amount: number; - notes: string; + body_weight: { + chicken_weight: number; + chicken_count: number; + average_chicken_weight: number; }[]; - depletions?: { - product_warehouse_id?: number; - total: number; - notes: string; + vaccination: { + vaccine_id: string; + total_stock: number; + used_stock: number; + }[]; + mortality: { + condition: string; + count: number; }[]; };