mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b9bd3c5bd | |||
| 9dc30c1f58 | |||
| 671fd72141 | |||
| d236138aa7 |
+2
-30
@@ -15,7 +15,7 @@ default:
|
||||
# ==========================================================
|
||||
.build_template: &build_template
|
||||
stage: build
|
||||
image: public.ecr.aws/docker/library/node:20-alpine
|
||||
image: node:20-alpine
|
||||
cache:
|
||||
key: npm-cache
|
||||
paths:
|
||||
@@ -56,7 +56,7 @@ default:
|
||||
.deploy_template: &deploy_template
|
||||
stage: deploy
|
||||
image:
|
||||
name: public.ecr.aws/aws-cli/aws-cli:latest
|
||||
name: amazon/aws-cli:latest
|
||||
entrypoint: ['/bin/sh', '-c']
|
||||
script:
|
||||
- set -e
|
||||
@@ -183,31 +183,3 @@ deploy:staging:
|
||||
environment:
|
||||
name: staging
|
||||
url: https://stg-lti-erp.mbugroup.id
|
||||
|
||||
# ==========================================================
|
||||
# ====== STAGING (Branch production) ======
|
||||
# ==========================================================
|
||||
build:production:
|
||||
<<: *build_template
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "production"'
|
||||
environment:
|
||||
name: staging
|
||||
variables:
|
||||
NEXT_PUBLIC_LTI_URL: 'https://lti-erp.mbugroup.id'
|
||||
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://auth-erp.mbugroup.id'
|
||||
NEXT_PUBLIC_API_BASE_URL: 'https://api-lti.mbugroup.id/api'
|
||||
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
||||
|
||||
deploy:production:
|
||||
<<: *deploy_template
|
||||
needs: ['build:production']
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "production"'
|
||||
variables:
|
||||
S3_BUCKET: 'production-lti-erp.mbugroup.id'
|
||||
AWS_REGION: 'ap-southeast-3'
|
||||
CLOUDFRONT_DISTRIBUTION_ID: 'E1SSLXKYYITASJ'
|
||||
environment:
|
||||
name: staging
|
||||
url: https://lti-erp.mbugroup.id
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
FROM public.ecr.aws/docker/library/node:20-alpine
|
||||
FROM node:20-alpine
|
||||
|
||||
RUN apk add --no-cache git bash build-base curl
|
||||
|
||||
@@ -22,4 +22,4 @@ RUN mkdir -p .next/server/app/_next && \
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["npx", "serve", ".next/server/app", "-l", "3000"]
|
||||
CMD ["npx", "serve", ".next/server/app", "-l", "3000"]
|
||||
+1
-2
@@ -8,8 +8,7 @@
|
||||
"start": "next start",
|
||||
"lint": "eslint",
|
||||
"prepare": "husky",
|
||||
"format": "prettier --write .",
|
||||
"pre-commit": "npm run format && npm run lint && npx tsc --noEmit && npm run build"
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-pdf/renderer": "^4.3.1",
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { MasterKandangContent } from '@/figma-make/components/pages/master-data/kandang/MasterKandangContent';
|
||||
|
||||
const MasterKandangPage = () => {
|
||||
return (
|
||||
<section className='w-full'>
|
||||
<MasterKandangContent />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default MasterKandangPage;
|
||||
@@ -24,8 +24,8 @@ import {
|
||||
} from '@/types/api/api-general';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
|
||||
export interface OptionType<T = string | number> {
|
||||
value: T;
|
||||
export interface OptionType {
|
||||
value: string | number;
|
||||
label: string;
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import useSWR from 'swr';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table';
|
||||
import { useFormik } from 'formik';
|
||||
@@ -26,10 +26,6 @@ import { InventoryAdjustmentApi } from '@/services/api/inventory';
|
||||
import { WarehouseApi, ProductApi } from '@/services/api/master-data';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import PopoverButton from '@/components/popover/PopoverButton';
|
||||
import PopoverContent from '@/components/popover/PopoverContent';
|
||||
import toast from 'react-hot-toast';
|
||||
import { InventoryAdjustment } from '@/types/api/inventory/adjustment';
|
||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||
import { TRANSACTION_SUBTYPE_OPTIONS } from '@/config/constant';
|
||||
@@ -42,62 +38,6 @@ import {
|
||||
AdjustmentFilterType,
|
||||
} from '@/components/pages/inventory/adjustment/filter/AdjustmentFilter';
|
||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||
import { CellContext } from '@tanstack/react-table';
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
popoverPosition = 'bottom',
|
||||
props,
|
||||
deleteClickHandler,
|
||||
}: {
|
||||
popoverPosition: 'bottom' | 'top';
|
||||
props: CellContext<InventoryAdjustment, unknown>;
|
||||
deleteClickHandler: () => void;
|
||||
}) => {
|
||||
const popoverId = `adjustment#${props.row.original.id}`;
|
||||
const popoverAnchorName = `--anchor-adjustment#${props.row.original.id}`;
|
||||
|
||||
const closePopover = () => {
|
||||
document.getElementById(popoverId)?.hidePopover();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<PopoverButton
|
||||
tabIndex={0}
|
||||
variant='ghost'
|
||||
color='none'
|
||||
popoverTarget={popoverId}
|
||||
anchorName={popoverAnchorName}
|
||||
>
|
||||
<Icon icon='material-symbols:more-vert' width={16} height={16} />
|
||||
</PopoverButton>
|
||||
|
||||
<PopoverContent
|
||||
id={popoverId}
|
||||
anchorName={popoverAnchorName}
|
||||
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
|
||||
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
|
||||
>
|
||||
<div className='flex flex-col bg-base-100 rounded-xl'>
|
||||
<RequirePermission permissions='lti.inventory.delete'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
deleteClickHandler();
|
||||
closePopover();
|
||||
}}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='p-3 justify-start text-sm font-semibold w-full focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon icon='mdi:delete-outline' width={20} height={20} />
|
||||
Delete
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const InventoryAdjustmentTable = () => {
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
@@ -140,13 +80,13 @@ const InventoryAdjustmentTable = () => {
|
||||
const formik = useFormik<AdjustmentFilterType>({
|
||||
initialValues: {
|
||||
product_id: null,
|
||||
warehouse: null,
|
||||
warehouse_id: null,
|
||||
transaction_type: null,
|
||||
},
|
||||
validationSchema: AdjustmentFilterSchema,
|
||||
onSubmit: (values, { setSubmitting }) => {
|
||||
updateFilter('productFilter', values.product_id || '');
|
||||
updateFilter('warehouseFilter', String(values.warehouse?.value) || '');
|
||||
updateFilter('warehouseFilter', values.warehouse_id || '');
|
||||
updateFilter('transactionTypeFilter', values.transaction_type || '');
|
||||
filterModal.closeModal();
|
||||
setSubmitting(false);
|
||||
@@ -202,11 +142,14 @@ const InventoryAdjustmentTable = () => {
|
||||
[formik]
|
||||
);
|
||||
|
||||
const handleFilterWarehouseChange = (
|
||||
val: OptionType | OptionType[] | null
|
||||
) => {
|
||||
formik.setFieldValue('warehouse', val);
|
||||
};
|
||||
const handleFilterWarehouseChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const warehouse = val as OptionType | null;
|
||||
const warehouseId = warehouse?.value ? String(warehouse.value) : null;
|
||||
formik.setFieldValue('warehouse_id', warehouseId);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
|
||||
const handleFilterTransactionTypeChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
@@ -227,6 +170,15 @@ const InventoryAdjustmentTable = () => {
|
||||
);
|
||||
}, [formik.values.product_id, productOptions]);
|
||||
|
||||
const warehouseIdValue = useMemo(() => {
|
||||
if (!formik.values.warehouse_id) return null;
|
||||
return (
|
||||
warehouseOptions.find(
|
||||
(opt) => String(opt.value) === formik.values.warehouse_id
|
||||
) || null
|
||||
);
|
||||
}, [formik.values.warehouse_id, warehouseOptions]);
|
||||
|
||||
const transactionTypeValue = useMemo(() => {
|
||||
if (!formik.values.transaction_type) return null;
|
||||
return (
|
||||
@@ -242,39 +194,12 @@ const InventoryAdjustmentTable = () => {
|
||||
formik.validateForm();
|
||||
};
|
||||
|
||||
const {
|
||||
data: inventoryAdjustments,
|
||||
isLoading,
|
||||
mutate: refreshAdjustments,
|
||||
} = useSWR(
|
||||
const { data: inventoryAdjustments, isLoading } = useSWR(
|
||||
`${InventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`,
|
||||
InventoryAdjustmentApi.getAllFetcher
|
||||
);
|
||||
|
||||
const singleDeleteHandler = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
const response = await InventoryAdjustmentApi.delete(
|
||||
selectedAdjustment?.id as number
|
||||
);
|
||||
|
||||
singleDeleteModal.closeModal();
|
||||
setIsDeleteLoading(false);
|
||||
|
||||
if (isResponseSuccess(response)) {
|
||||
toast.success(response?.message || 'Successfully delete Adjustment!');
|
||||
refreshAdjustments();
|
||||
} else {
|
||||
toast.error(response?.message || 'Failed to delete Adjustment');
|
||||
}
|
||||
};
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [selectedAdjustment, setSelectedAdjustment] = useState<
|
||||
InventoryAdjustment | undefined
|
||||
>(undefined);
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
const singleDeleteModal = useModal();
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
@@ -389,39 +314,8 @@ const InventoryAdjustmentTable = () => {
|
||||
header: 'Oleh',
|
||||
accessorFn: (row) => row.created_user?.name ?? '-',
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Aksi',
|
||||
cell: (props: CellContext<InventoryAdjustment, unknown>) => {
|
||||
const currentPageSize =
|
||||
props.table.getPaginationRowModel().rows.length;
|
||||
const currentPageRows = props.table.getPaginationRowModel().flatRows;
|
||||
const currentRowRelativeIndex =
|
||||
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
||||
|
||||
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
||||
|
||||
const deleteClickHandler = () => {
|
||||
setSelectedAdjustment(props.row.original);
|
||||
singleDeleteModal.openModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<RowOptionsMenu
|
||||
props={props}
|
||||
deleteClickHandler={deleteClickHandler}
|
||||
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
tableFilterState.pageSize,
|
||||
tableFilterState.page,
|
||||
singleDeleteModal,
|
||||
setSelectedAdjustment,
|
||||
]
|
||||
[tableFilterState.pageSize, tableFilterState.page]
|
||||
);
|
||||
|
||||
const updateSortingFilter = useCallback(
|
||||
@@ -608,7 +502,7 @@ const InventoryAdjustmentTable = () => {
|
||||
label='Gudang'
|
||||
placeholder='Pilih Gudang'
|
||||
options={warehouseOptions}
|
||||
value={formik.values.warehouse}
|
||||
value={warehouseIdValue}
|
||||
onChange={handleFilterWarehouseChange}
|
||||
onInputChange={setWarehouseInputValue}
|
||||
isLoading={isLoadingWarehouseOptions}
|
||||
@@ -650,21 +544,6 @@ const InventoryAdjustmentTable = () => {
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
<ConfirmationModal
|
||||
ref={singleDeleteModal.ref}
|
||||
type='error'
|
||||
text={`Apakah anda yakin ingin menghapus data Adjustment ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'error',
|
||||
isLoading: isDeleteLoading,
|
||||
onClick: singleDeleteHandler,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { string, object } from 'yup';
|
||||
import { OptionType } from '@/components/input/SelectInput';
|
||||
|
||||
export const AdjustmentFilterSchema = object().shape({
|
||||
product_id: string().nullable(),
|
||||
@@ -9,6 +8,6 @@ export const AdjustmentFilterSchema = object().shape({
|
||||
|
||||
export type AdjustmentFilterType = {
|
||||
product_id: string | null;
|
||||
warehouse_id: string | null;
|
||||
transaction_type: string | null;
|
||||
warehouse: OptionType<number> | null;
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
InventoryAdjustmentFormSchema,
|
||||
InventoryAdjustmentFormValues,
|
||||
} from '@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema';
|
||||
import { LocationApi } from '@/services/api/master-data';
|
||||
import { KandangApi, LocationApi } from '@/services/api/master-data';
|
||||
import {
|
||||
ProjectFlockApi,
|
||||
ProjectFlockKandangApi,
|
||||
@@ -32,6 +32,8 @@ import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||
import { Location } from '@/types/api/master-data/location';
|
||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { Product } from '@/types/api/master-data/product';
|
||||
import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
@@ -117,19 +119,40 @@ const InventoryAdjustmentForm = ({
|
||||
}
|
||||
);
|
||||
|
||||
const { rawData: approvedProjectFlockKandangsRawData } =
|
||||
useSelect<ProjectFlockKandang>(
|
||||
ProjectFlockKandangApi.basePath,
|
||||
'id',
|
||||
'id',
|
||||
'search',
|
||||
{
|
||||
step_name: 'Disetujui',
|
||||
limit: '100',
|
||||
}
|
||||
);
|
||||
|
||||
const approvedProjectFlockKandangs = useMemo(() => {
|
||||
if (
|
||||
approvedProjectFlockKandangsRawData &&
|
||||
'data' in approvedProjectFlockKandangsRawData
|
||||
) {
|
||||
return approvedProjectFlockKandangsRawData.data as ProjectFlockKandang[];
|
||||
}
|
||||
return [];
|
||||
}, [approvedProjectFlockKandangsRawData]);
|
||||
|
||||
const {
|
||||
options: projectFlockKandangOptions,
|
||||
loadMore: loadMoreProjectFlockKandangs,
|
||||
setInputValue: setProjectFlockKandangInputValue,
|
||||
isLoadingOptions: isLoadingProjectFlockKandangOptions,
|
||||
} = useSelect(
|
||||
selectedProjectFlock ? ProjectFlockKandangApi.basePath : '',
|
||||
'kandang.id',
|
||||
'kandang.name',
|
||||
setInputValue: setKandangInputValue,
|
||||
options: kandangOptionsFromApi,
|
||||
isLoadingOptions: isLoadingKandangOptions,
|
||||
loadMore: loadMoreKandangs,
|
||||
} = useSelect<Kandang>(
|
||||
selectedProjectFlock ? KandangApi.basePath : '',
|
||||
'id',
|
||||
'name',
|
||||
'search',
|
||||
{
|
||||
step_name: 'Disetujui',
|
||||
project_flock_id: String(selectedProjectFlock?.value),
|
||||
location_id: selectedProjectFlockLocationId,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -199,6 +222,26 @@ const InventoryAdjustmentForm = ({
|
||||
return (product?.flags as string[]) || [];
|
||||
}, [selectedProduct, productOptions]);
|
||||
|
||||
const kandangOptions = useMemo(() => {
|
||||
let options: OptionType[] = [];
|
||||
|
||||
if (selectedProjectFlock) {
|
||||
const approvedKandangIds = approvedProjectFlockKandangs
|
||||
.filter((pfk) => pfk.project_flock_id === selectedProjectFlock.value)
|
||||
.map((pfk) => pfk.kandang_id);
|
||||
|
||||
options = kandangOptionsFromApi.filter((kandang) =>
|
||||
approvedKandangIds.includes(kandang.value as number)
|
||||
);
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [
|
||||
selectedProjectFlock,
|
||||
kandangOptionsFromApi,
|
||||
approvedProjectFlockKandangs,
|
||||
]);
|
||||
|
||||
const formikInitialValues = useMemo<Partial<InventoryAdjustmentFormValues>>(
|
||||
() => ({
|
||||
location: null,
|
||||
@@ -650,10 +693,10 @@ const InventoryAdjustmentForm = ({
|
||||
label='Kandang'
|
||||
value={selectedKandang}
|
||||
onChange={kandangChangeHandler}
|
||||
onInputChange={setProjectFlockKandangInputValue}
|
||||
options={projectFlockKandangOptions}
|
||||
onMenuScrollToBottom={loadMoreProjectFlockKandangs}
|
||||
isLoading={isLoadingProjectFlockKandangOptions}
|
||||
onInputChange={setKandangInputValue}
|
||||
options={kandangOptions}
|
||||
onMenuScrollToBottom={loadMoreKandangs}
|
||||
isLoading={isLoadingKandangOptions}
|
||||
isError={
|
||||
formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import useSWR from 'swr';
|
||||
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
||||
import { useFormik } from 'formik';
|
||||
|
||||
@@ -21,8 +21,6 @@ import { cn } from '@/lib/helper';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import toast from 'react-hot-toast';
|
||||
import Button from '@/components/Button';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||
@@ -43,11 +41,9 @@ import {
|
||||
const RowOptionsMenu = ({
|
||||
popoverPosition = 'bottom',
|
||||
props,
|
||||
deleteClickHandler,
|
||||
}: {
|
||||
popoverPosition: 'bottom' | 'top';
|
||||
props: CellContext<Movement, unknown>;
|
||||
deleteClickHandler: () => void;
|
||||
}) => {
|
||||
const popoverId = `movement#${props.row.original.id}`;
|
||||
const popoverAnchorName = `--anchor-movement#${props.row.original.id}`;
|
||||
@@ -87,20 +83,6 @@ const RowOptionsMenu = ({
|
||||
Detail
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<RequirePermission permissions='lti.inventory.transfer.delete'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
deleteClickHandler();
|
||||
closePopover();
|
||||
}}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='p-3 justify-start text-sm font-semibold w-full focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon icon='mdi:delete-outline' width={20} height={20} />
|
||||
Delete
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</div>
|
||||
@@ -224,37 +206,12 @@ const MovementTable = () => {
|
||||
};
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [selectedMovement, setSelectedMovement] = useState<
|
||||
Movement | undefined
|
||||
>(undefined);
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
const singleDeleteModal = useModal();
|
||||
|
||||
const {
|
||||
data: movements,
|
||||
isLoading,
|
||||
mutate: refreshMovements,
|
||||
} = useSWR(
|
||||
const { data: movements, isLoading } = useSWR(
|
||||
`${MovementApi.basePath}${getTableFilterQueryString()}`,
|
||||
MovementApi.getAllFetcher
|
||||
);
|
||||
|
||||
const singleDeleteHandler = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
const response = await MovementApi.delete(selectedMovement?.id as number);
|
||||
|
||||
singleDeleteModal.closeModal();
|
||||
setIsDeleteLoading(false);
|
||||
|
||||
if (isResponseSuccess(response)) {
|
||||
toast.success(response?.message || 'Successfully delete Movement!');
|
||||
refreshMovements();
|
||||
} else {
|
||||
toast.error(response?.message || 'Failed to delete Movement');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
@@ -318,27 +275,16 @@ const MovementTable = () => {
|
||||
|
||||
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
||||
|
||||
const deleteClickHandler = () => {
|
||||
setSelectedMovement(props.row.original);
|
||||
singleDeleteModal.openModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<RowOptionsMenu
|
||||
props={props}
|
||||
deleteClickHandler={deleteClickHandler}
|
||||
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
tableFilterState.pageSize,
|
||||
tableFilterState.page,
|
||||
singleDeleteModal,
|
||||
setSelectedMovement,
|
||||
]
|
||||
[tableFilterState.pageSize, tableFilterState.page]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -509,21 +455,6 @@ const MovementTable = () => {
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
<ConfirmationModal
|
||||
ref={singleDeleteModal.ref}
|
||||
type='error'
|
||||
text={`Apakah anda yakin ingin menghapus data Movement ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'error',
|
||||
isLoading: isDeleteLoading,
|
||||
onClick: singleDeleteHandler,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -82,7 +82,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
warehouse_id: number;
|
||||
warehouse_name: string;
|
||||
quantity: number;
|
||||
transfer_available_qty?: number;
|
||||
}
|
||||
|
||||
// ===== USE SELECT HOOKS =====
|
||||
@@ -380,8 +379,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
warehouse_id: formik.values.source_warehouse_id
|
||||
? formik.values.source_warehouse_id.toString()
|
||||
: '',
|
||||
transfer_context: 'inventory_transfer',
|
||||
stock_mode: 'exclude_chickin',
|
||||
}
|
||||
);
|
||||
|
||||
@@ -394,7 +391,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
warehouse_id: pw.warehouse.id,
|
||||
warehouse_name: pw.warehouse.name,
|
||||
quantity: pw.quantity,
|
||||
transfer_available_qty: pw.transfer_available_qty,
|
||||
}))
|
||||
: [];
|
||||
}, [productWarehouses]);
|
||||
@@ -838,22 +834,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
}, [formik.values.products, formik.values.deliveries]);
|
||||
|
||||
const getAvailableStock = useCallback(
|
||||
(productId: number) => {
|
||||
if (type === 'detail') return 0;
|
||||
const productWarehouse = productWarehouseOptions.find(
|
||||
(pw) => pw.product_id === productId
|
||||
);
|
||||
|
||||
return (
|
||||
productWarehouse?.transfer_available_qty ??
|
||||
productWarehouse?.quantity ??
|
||||
0
|
||||
);
|
||||
},
|
||||
[productWarehouseOptions, type]
|
||||
);
|
||||
|
||||
const getTotalStock = useCallback(
|
||||
(productId: number) => {
|
||||
if (type === 'detail') return 0;
|
||||
const productWarehouse = productWarehouseOptions.find(
|
||||
@@ -864,16 +844,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
[productWarehouseOptions, type]
|
||||
);
|
||||
|
||||
const hasAvailableQty = useCallback(
|
||||
(productId: number) => {
|
||||
const productWarehouse = productWarehouseOptions.find(
|
||||
(pw) => pw.product_id === productId
|
||||
);
|
||||
return productWarehouse?.transfer_available_qty !== undefined;
|
||||
},
|
||||
[productWarehouseOptions]
|
||||
);
|
||||
|
||||
const getProductQtyBottomLabel = useCallback(
|
||||
(productIdx: number) => {
|
||||
if (type === 'detail') return undefined;
|
||||
@@ -881,31 +851,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
if (!product || !product.product_id) return undefined;
|
||||
|
||||
const availableStock = getAvailableStock(product.product_id);
|
||||
const totalStock = getTotalStock(product.product_id);
|
||||
const requestedQty = Number(product.product_qty) || 0;
|
||||
const remainingStock = availableStock - requestedQty;
|
||||
const isAyamProduct = hasAvailableQty(product.product_id);
|
||||
|
||||
if (requestedQty > 0) {
|
||||
if (isAyamProduct) {
|
||||
return `Sisa: ${formatNumber(remainingStock)} (Total: ${formatNumber(totalStock)})`;
|
||||
}
|
||||
return `Sisa: ${formatNumber(remainingStock)}`;
|
||||
}
|
||||
|
||||
if (isAyamProduct) {
|
||||
return `Tersedia: ${formatNumber(availableStock)} (Total: ${formatNumber(totalStock)})`;
|
||||
}
|
||||
|
||||
return `Tersedia: ${formatNumber(availableStock)}`;
|
||||
},
|
||||
[
|
||||
formik.values.products,
|
||||
getAvailableStock,
|
||||
getTotalStock,
|
||||
hasAvailableQty,
|
||||
type,
|
||||
]
|
||||
[formik.values.products, getAvailableStock, type]
|
||||
);
|
||||
|
||||
const getDeliveryProductQtyBottomLabel = useCallback(
|
||||
@@ -967,26 +922,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
if (!product || !product.product_id) return null;
|
||||
|
||||
const availableStock = getAvailableStock(product.product_id);
|
||||
const totalStock = getTotalStock(product.product_id);
|
||||
const requestedQty = Number(product.product_qty) || 0;
|
||||
const isAyamProduct = hasAvailableQty(product.product_id);
|
||||
|
||||
if (requestedQty > availableStock) {
|
||||
if (isAyamProduct) {
|
||||
return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)} (Total: ${formatNumber(totalStock)}, terpakai untuk chickin: ${formatNumber(totalStock - availableStock)})`;
|
||||
}
|
||||
return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[
|
||||
formik.values.products,
|
||||
getAvailableStock,
|
||||
getTotalStock,
|
||||
hasAvailableQty,
|
||||
type,
|
||||
]
|
||||
[formik.values.products, getAvailableStock, type]
|
||||
);
|
||||
|
||||
const validateDeliveryQty = useCallback(
|
||||
|
||||
@@ -314,10 +314,6 @@ const KandangsTable = () => {
|
||||
accessorFn: (row) => row.pic?.name ?? '-',
|
||||
header: 'PIC',
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.kandang_group?.name ?? '-',
|
||||
header: 'Kandang Group',
|
||||
},
|
||||
{
|
||||
header: 'Aksi',
|
||||
cell: (props: CellContext<Kandang, unknown>) => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { OptionType } from '@/components/input/SelectInput';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
type KandangFormSchemaType = {
|
||||
@@ -20,7 +19,6 @@ type KandangFormSchemaType = {
|
||||
}
|
||||
| undefined
|
||||
| null;
|
||||
group?: OptionType;
|
||||
};
|
||||
|
||||
export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
|
||||
@@ -44,11 +42,6 @@ export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
}).nullable(),
|
||||
|
||||
group: Yup.object({
|
||||
value: Yup.number().min(1).required('Kandang Grup wajib diisi!'),
|
||||
label: Yup.string().required('Kandang Grup wajib diisi!'),
|
||||
}).required('Kandang Grup wajib diisi!'),
|
||||
});
|
||||
|
||||
export const UpdateKandangFormSchema = KandangFormSchema;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { getIn, useFormik } from 'formik';
|
||||
import { useFormik } from 'formik';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
@@ -34,8 +34,6 @@ import NumberInput from '@/components/input/NumberInput';
|
||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||
import { User } from '@/types/api/api-general';
|
||||
import { DailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
|
||||
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||||
|
||||
interface KandangFormProps {
|
||||
type?: 'add' | 'edit' | 'detail';
|
||||
@@ -98,12 +96,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
||||
label: initialValues.pic.name,
|
||||
}
|
||||
: null,
|
||||
group: initialValues?.kandang_group
|
||||
? {
|
||||
value: initialValues.kandang_group.id,
|
||||
label: initialValues.kandang_group.name,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}, [initialValues]);
|
||||
|
||||
@@ -119,7 +111,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
||||
location_id: values.locationId!,
|
||||
capacity: values.capacity ? parseInt(values.capacity.toString()) : 0,
|
||||
pic_id: values.picId!,
|
||||
group_id: values.group?.value as number,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
@@ -171,23 +162,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
||||
formik.setFieldValue('picId', (val as OptionType)?.value);
|
||||
};
|
||||
|
||||
// Kandang Group
|
||||
const {
|
||||
setInputValue: setKandangGroupSelectInputValue,
|
||||
options: kandangGroupOptions,
|
||||
isLoadingOptions: isLoadingKandangGroupOptions,
|
||||
loadMore: loadMoreKandangGroups,
|
||||
} = useSelect<DailyChecklistKandang>(
|
||||
DailyChecklistKandangApi.basePath,
|
||||
'id',
|
||||
'name'
|
||||
);
|
||||
|
||||
const kandangGroupChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldTouched('group', true);
|
||||
formik.setFieldValue('group', val);
|
||||
};
|
||||
|
||||
const deleteKandangClickHandler = () => {
|
||||
deleteModal.openModal();
|
||||
};
|
||||
@@ -295,24 +269,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
||||
isDisabled={type === 'detail'}
|
||||
isClearable
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
required
|
||||
label='Kandang Group'
|
||||
value={formik.values.group ?? undefined}
|
||||
onChange={kandangGroupChangeHandler}
|
||||
options={kandangGroupOptions}
|
||||
onInputChange={setKandangGroupSelectInputValue}
|
||||
onMenuScrollToBottom={loadMoreKandangGroups}
|
||||
isLoading={isLoadingKandangGroupOptions}
|
||||
isError={formik.touched.group && Boolean(formik.errors.group)}
|
||||
errorMessage={
|
||||
getIn(formik.errors.group, 'value') ??
|
||||
(formik.errors.group as string)
|
||||
}
|
||||
isDisabled={type === 'detail'}
|
||||
isClearable
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||
|
||||
@@ -21,7 +21,6 @@ import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import PopoverButton from '@/components/popover/PopoverButton';
|
||||
import PopoverContent from '@/components/popover/PopoverContent';
|
||||
import Tooltip from '@/components/Tooltip';
|
||||
import { useFormik } from 'formik';
|
||||
import { AreaApi } from '@/services/api/master-data';
|
||||
import { LocationApi } from '@/services/api/master-data';
|
||||
@@ -37,7 +36,6 @@ import {
|
||||
import RecordingTableSkeleton from '@/components/pages/production/recording/skeleton/RecordingTableSkeleton';
|
||||
import Table from '@/components/Table';
|
||||
import { type Recording } from '@/types/api/production/recording';
|
||||
import { getRecordingRestriction } from './recording-utils';
|
||||
import { RecordingApi } from '@/services/api/production';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
@@ -107,75 +105,30 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const isRecordingEditable = (recording: Recording) => {
|
||||
const isGrowingCategory =
|
||||
recording.project_flock?.project_flock_category === 'GROWING';
|
||||
const isGrowingLockedByLaying = isGrowingCategory && recording.is_laying;
|
||||
if (isGrowingLockedByLaying) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentIsLaying =
|
||||
recording.project_flock?.project_flock_category === 'LAYING';
|
||||
|
||||
const restriction = getRecordingRestriction(
|
||||
recording.is_laying,
|
||||
recording.is_transition,
|
||||
currentIsLaying
|
||||
);
|
||||
|
||||
if (restriction.isLocked) {
|
||||
return false;
|
||||
if (recording.project_flock?.project_flock_category === 'GROWING') {
|
||||
if (recording.transfer_executed) {
|
||||
return false;
|
||||
}
|
||||
return recording.population_can_change === true;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const getRecordingRestrictionInfo = (recording: Recording) => {
|
||||
const isGrowingCategory =
|
||||
recording.project_flock?.project_flock_category === 'GROWING';
|
||||
const isGrowingLockedByLaying = isGrowingCategory && recording.is_laying;
|
||||
if (isGrowingLockedByLaying) {
|
||||
return {
|
||||
canEditStock: false,
|
||||
canEditDepletion: false,
|
||||
canEditEgg: false,
|
||||
isLocked: true,
|
||||
lockReason:
|
||||
'Recording Growing tidak dapat diubah karena sudah masuk fase laying dan dipakai pada recording laying',
|
||||
};
|
||||
}
|
||||
|
||||
const currentIsLaying =
|
||||
recording.project_flock?.project_flock_category === 'LAYING';
|
||||
|
||||
return getRecordingRestriction(
|
||||
recording.is_laying,
|
||||
recording.is_transition,
|
||||
currentIsLaying
|
||||
);
|
||||
};
|
||||
|
||||
const isApproved = isRecordingApproved(props.row.original);
|
||||
const isRejected = isRecordingRejected(props.row.original);
|
||||
const isEditable = isRecordingEditable(props.row.original);
|
||||
const restrictionInfo = getRecordingRestrictionInfo(props.row.original);
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<Tooltip
|
||||
content={restrictionInfo.isLocked ? restrictionInfo.lockReason : ''}
|
||||
position='top'
|
||||
<PopoverButton
|
||||
tabIndex={0}
|
||||
variant='ghost'
|
||||
color='none'
|
||||
popoverTarget={popoverId}
|
||||
anchorName={popoverAnchorName}
|
||||
>
|
||||
<PopoverButton
|
||||
tabIndex={0}
|
||||
variant='ghost'
|
||||
color='none'
|
||||
popoverTarget={popoverId}
|
||||
anchorName={popoverAnchorName}
|
||||
className={restrictionInfo.isLocked ? 'text-error' : ''}
|
||||
>
|
||||
<Icon icon='material-symbols:more-vert' width={16} height={16} />
|
||||
</PopoverButton>
|
||||
</Tooltip>
|
||||
<Icon icon='material-symbols:more-vert' width={16} height={16} />
|
||||
</PopoverButton>
|
||||
|
||||
<PopoverContent
|
||||
id={popoverId}
|
||||
@@ -607,17 +560,12 @@ const RecordingTable = () => {
|
||||
const singleDeleteHandler = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
const response = await RecordingApi.delete(selectedRecording?.id as number);
|
||||
await RecordingApi.delete(selectedRecording?.id as number);
|
||||
refreshRecordings();
|
||||
|
||||
singleDeleteModal.closeModal();
|
||||
toast.success('Successfully delete Recording!');
|
||||
setIsDeleteLoading(false);
|
||||
|
||||
if (isResponseSuccess(response)) {
|
||||
toast.success(response?.message || 'Successfully delete Recording!');
|
||||
refreshRecordings();
|
||||
} else {
|
||||
toast.error(response?.message || 'Failed to delete Recording');
|
||||
}
|
||||
};
|
||||
|
||||
const approveHandler = async (notes: string) => {
|
||||
@@ -813,30 +761,11 @@ const RecordingTable = () => {
|
||||
{
|
||||
header: 'Kategori',
|
||||
cell: (props) => {
|
||||
const isTransition = props.row.original.is_transition;
|
||||
const category =
|
||||
props.row.original.project_flock?.project_flock_category ||
|
||||
'GROWING';
|
||||
props.row.original.project_flock?.project_flock_category;
|
||||
if (!category) return '-';
|
||||
const color = category === 'LAYING' ? 'info' : 'warning';
|
||||
|
||||
const isGrowingLocked =
|
||||
category === 'GROWING' && props.row.original.is_laying;
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-1'>
|
||||
<StatusBadge color={color} text={formatTitleCase(category)} />
|
||||
{isTransition && (
|
||||
<span className='text-xs text-warning font-medium'>
|
||||
(Transisi)
|
||||
</span>
|
||||
)}
|
||||
{isGrowingLocked && (
|
||||
<span className='text-xs text-error font-medium'>
|
||||
(Penguncian)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return <StatusBadge color={color} text={formatTitleCase(category)} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -29,8 +29,8 @@ type RecordingGrowingFormSchemaType = {
|
||||
} | null;
|
||||
project_flock_kandang_id: number;
|
||||
stocks: {
|
||||
product_warehouse_id: number;
|
||||
qty: number | string;
|
||||
product_warehouse_id?: number;
|
||||
qty?: number | string;
|
||||
}[];
|
||||
depletions: {
|
||||
product_warehouse_id?: number;
|
||||
@@ -73,6 +73,18 @@ const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
|
||||
.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({
|
||||
product_warehouse_id: Yup.number()
|
||||
.optional()
|
||||
@@ -90,7 +102,9 @@ const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
|
||||
weight: Yup.number().optional().typeError('Berat telur harus berupa angka!'),
|
||||
});
|
||||
|
||||
export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSchemaType> =
|
||||
export const RecordingGrowingFormSchema = (
|
||||
isTransitioningToLaying = false
|
||||
): Yup.ObjectSchema<RecordingGrowingFormSchemaType> =>
|
||||
Yup.object({
|
||||
record_date: Yup.string()
|
||||
.required('Tanggal recording wajib diisi!')
|
||||
@@ -150,20 +164,24 @@ export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSc
|
||||
return true;
|
||||
}
|
||||
),
|
||||
stocks: Yup.array()
|
||||
.of(StockObjectSchema)
|
||||
.min(1, 'Minimal harus ada 1 data stok!')
|
||||
.required('Data stok wajib diisi!'),
|
||||
stocks: isTransitioningToLaying
|
||||
? Yup.array().of(OptionalStockObjectSchema).default([])
|
||||
: Yup.array()
|
||||
.of(StockObjectSchema)
|
||||
.min(1, 'Minimal harus ada 1 data stok!')
|
||||
.required('Data stok wajib diisi!'),
|
||||
depletions: Yup.array().of(DepletionObjectSchema).default([]),
|
||||
});
|
||||
|
||||
export const RecordingLayingFormSchema: Yup.ObjectSchema<RecordingLayingFormSchemaType> =
|
||||
RecordingGrowingFormSchema.shape({
|
||||
RecordingGrowingFormSchema().shape({
|
||||
eggs: Yup.array().of(EggObjectSchema).default([]),
|
||||
});
|
||||
|
||||
export const UpdateRecordingGrowingFormSchema =
|
||||
RecordingGrowingFormSchema.shape({
|
||||
export const UpdateRecordingGrowingFormSchema = (
|
||||
isTransitioningToLaying = false
|
||||
) =>
|
||||
RecordingGrowingFormSchema(isTransitioningToLaying).shape({
|
||||
location_id: Yup.number().nullable().optional(),
|
||||
project_flock_id: Yup.number().nullable().optional(),
|
||||
kandang_id: Yup.number().nullable().optional(),
|
||||
@@ -193,10 +211,13 @@ export const UpdateRecordingLayingFormSchema = RecordingLayingFormSchema.shape({
|
||||
.required('Project Flock Kandang wajib diisi!'),
|
||||
});
|
||||
|
||||
export type RecordingGrowingFormValues = Yup.InferType<
|
||||
type RecordingGrowingFormSchemaFn = ReturnType<
|
||||
typeof RecordingGrowingFormSchema
|
||||
>;
|
||||
|
||||
export type RecordingGrowingFormValues =
|
||||
Yup.InferType<RecordingGrowingFormSchemaFn>;
|
||||
|
||||
export type RecordingLayingFormValues = Yup.InferType<
|
||||
typeof RecordingLayingFormSchema
|
||||
>;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,73 +0,0 @@
|
||||
export type RecordingRestriction = {
|
||||
canEditStock: boolean;
|
||||
canEditDepletion: boolean;
|
||||
canEditEgg: boolean;
|
||||
isLocked: boolean;
|
||||
lockReason?: string;
|
||||
};
|
||||
|
||||
export const getRecordingRestriction = (
|
||||
isLaying: boolean,
|
||||
isTransition: boolean,
|
||||
currentIsLaying?: boolean
|
||||
): RecordingRestriction => {
|
||||
if (isTransition && !isLaying) {
|
||||
const isLayingKandangInTransition = currentIsLaying === true;
|
||||
|
||||
if (isLayingKandangInTransition) {
|
||||
return {
|
||||
canEditStock: false,
|
||||
canEditDepletion: true,
|
||||
canEditEgg: true,
|
||||
isLocked: false,
|
||||
lockReason: undefined,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
canEditStock: true,
|
||||
canEditDepletion: false,
|
||||
canEditEgg: false,
|
||||
isLocked: false,
|
||||
lockReason: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLaying && !isTransition && currentIsLaying) {
|
||||
return {
|
||||
canEditStock: false,
|
||||
canEditDepletion: false,
|
||||
canEditEgg: false,
|
||||
isLocked: true,
|
||||
lockReason:
|
||||
'Recording Growing telah terkunci karena Project Flock sudah masuk fase Laying',
|
||||
};
|
||||
}
|
||||
|
||||
if (!isLaying && !isTransition) {
|
||||
return {
|
||||
canEditStock: true,
|
||||
canEditDepletion: true,
|
||||
canEditEgg: false,
|
||||
isLocked: false,
|
||||
lockReason: undefined,
|
||||
};
|
||||
}
|
||||
if (isLaying && !isTransition) {
|
||||
return {
|
||||
canEditStock: true,
|
||||
canEditDepletion: true,
|
||||
canEditEgg: true,
|
||||
isLocked: false,
|
||||
lockReason: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
canEditStock: false,
|
||||
canEditDepletion: false,
|
||||
canEditEgg: false,
|
||||
isLocked: true,
|
||||
lockReason: 'Kondisi transisi tidak valid',
|
||||
};
|
||||
};
|
||||
@@ -20,7 +20,6 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
||||
'lti.daily_checklist.master_data.employee',
|
||||
'lti.daily_checklist.master_data.activity',
|
||||
'lti.daily_checklist.master_data.configuration',
|
||||
'lti.daily_checklist.master_data.kandang',
|
||||
],
|
||||
submenu: [
|
||||
{
|
||||
@@ -67,11 +66,6 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
||||
link: '/daily-checklist/master-data/activity',
|
||||
permission: ['lti.daily_checklist.master_data.activity'],
|
||||
},
|
||||
{
|
||||
text: 'Kandang',
|
||||
link: '/daily-checklist/master-data/kandang',
|
||||
permission: ['lti.daily_checklist.master_data.kandang'],
|
||||
},
|
||||
{
|
||||
text: 'Konfigurasi',
|
||||
link: '/daily-checklist/master-data/configuration',
|
||||
@@ -555,12 +549,6 @@ export const APPROVAL_WORKFLOWS = {
|
||||
],
|
||||
};
|
||||
|
||||
export const PROJECT_FLOCK_STATUS = {
|
||||
PENGAJUAN: APPROVAL_WORKFLOWS.PROJECT_FLOCKS[0].step_name,
|
||||
AKTIF: APPROVAL_WORKFLOWS.PROJECT_FLOCKS[1].step_name,
|
||||
SELESAI: APPROVAL_WORKFLOWS.PROJECT_FLOCKS[2].step_name,
|
||||
} as const;
|
||||
|
||||
export const ACCEPTED_FILE_TYPE = {
|
||||
PDF: {
|
||||
'application/pdf': ['.pdf'],
|
||||
|
||||
@@ -21,9 +21,6 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
||||
'/daily-checklist/master-data/configuration/': [
|
||||
'lti.daily_checklist.master_data.configuration',
|
||||
],
|
||||
'/daily-checklist/master-data/kandang/': [
|
||||
'lti.daily_checklist.master_data.kandang',
|
||||
],
|
||||
|
||||
// Production
|
||||
// Production - Project Flock
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Check, ChevronsUpDown, X, Loader2 } from 'lucide-react';
|
||||
import { Check, ChevronsUpDown, X } from 'lucide-react';
|
||||
import { cn } from '@/lib/helper';
|
||||
import { Button } from '@/figma-make/components/base/button';
|
||||
import {
|
||||
@@ -29,8 +29,6 @@ interface MultiSelectProps {
|
||||
selected: string[];
|
||||
onChange: (selected: string[]) => void;
|
||||
onSearchChange?: (value: string) => void;
|
||||
onLoadMore?: () => void;
|
||||
isLoadingMore?: boolean;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
@@ -41,8 +39,6 @@ export function MultiSelect({
|
||||
selected,
|
||||
onChange,
|
||||
onSearchChange,
|
||||
onLoadMore,
|
||||
isLoadingMore,
|
||||
placeholder = 'Select items...',
|
||||
className,
|
||||
disabled,
|
||||
@@ -119,18 +115,7 @@ export function MultiSelect({
|
||||
onValueChange={onSearchChange}
|
||||
/>
|
||||
<CommandEmpty>No item found.</CommandEmpty>
|
||||
<CommandList
|
||||
className='max-h-[300px] overflow-y-auto'
|
||||
onScroll={(e) => {
|
||||
const target = e.currentTarget;
|
||||
if (
|
||||
target.scrollHeight - target.scrollTop <=
|
||||
target.clientHeight + 1
|
||||
) {
|
||||
onLoadMore?.();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CommandList className='max-h-[300px] overflow-y-auto'>
|
||||
<CommandGroup className='overflow-visible'>
|
||||
{options.map((option) => (
|
||||
<CommandItem
|
||||
@@ -149,11 +134,6 @@ export function MultiSelect({
|
||||
{option.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
{isLoadingMore && (
|
||||
<div className='py-4 flex justify-center w-full'>
|
||||
<Loader2 className='h-4 w-4 animate-spin text-muted-foreground' />
|
||||
</div>
|
||||
)}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
|
||||
@@ -55,11 +55,7 @@ function SelectContent({
|
||||
children,
|
||||
position = 'popper',
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content> & {
|
||||
onScroll?: React.UIEventHandler<HTMLDivElement>;
|
||||
}) {
|
||||
const { onScroll, ...restProps } = props;
|
||||
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
@@ -71,7 +67,7 @@ function SelectContent({
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...restProps}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
@@ -80,7 +76,6 @@ function SelectContent({
|
||||
position === 'popper' &&
|
||||
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1'
|
||||
)}
|
||||
onScroll={onScroll}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
|
||||
@@ -2,16 +2,7 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Plus,
|
||||
X,
|
||||
Save,
|
||||
Send,
|
||||
Info,
|
||||
FilePlus,
|
||||
ListChecks,
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import { Plus, X, Save, Send, Info, FilePlus, ListChecks } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/figma-make/components/base/card';
|
||||
import { Button } from '@/figma-make/components/base/button';
|
||||
import { Label } from '@/figma-make/components/base/label';
|
||||
@@ -35,6 +26,7 @@ import {
|
||||
import { DatePicker } from '@/figma-make/components/base/date-picker';
|
||||
import { toast } from 'sonner';
|
||||
import { useSelect } from '@/components/input/SelectInput';
|
||||
import { KandangApi } from '@/services/api/master-data';
|
||||
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import useSWR from 'swr';
|
||||
@@ -51,7 +43,6 @@ import DropFileInput from '@/components/input/DropFileInput';
|
||||
import Link from 'next/link';
|
||||
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||||
|
||||
// Static categories
|
||||
const CATEGORIES = [
|
||||
@@ -95,11 +86,16 @@ export function DailyChecklistContent() {
|
||||
searchParams.get('category') || ''
|
||||
);
|
||||
|
||||
const {
|
||||
options: kandangOptions,
|
||||
isLoadingMore: isLoadingMoreKandang,
|
||||
loadMore: loadMoreKandang,
|
||||
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
|
||||
const { options: kandangOptions } = useSelect(
|
||||
KandangApi.basePath,
|
||||
'id',
|
||||
'name',
|
||||
'search',
|
||||
{
|
||||
page: '1',
|
||||
limit: '100',
|
||||
}
|
||||
);
|
||||
|
||||
const { data: phases } = useSWR<
|
||||
BaseApiResponse<Phase[] | undefined>,
|
||||
@@ -172,16 +168,6 @@ export function DailyChecklistContent() {
|
||||
const [documents, setDocuments] = useState<File[]>([]);
|
||||
const [deletedDocumentIds, setDeletedDocumentIds] = useState<number[]>([]);
|
||||
|
||||
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||
const target = e.target as HTMLDivElement;
|
||||
|
||||
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
|
||||
if (!isLoadingMoreKandang) {
|
||||
loadMoreKandang();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Sync state to URL query params
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
@@ -1008,7 +994,7 @@ export function DailyChecklistContent() {
|
||||
>
|
||||
<SelectValue placeholder='Pilih kandang' />
|
||||
</SelectTrigger>
|
||||
<SelectContent onScroll={handleKandangScroll}>
|
||||
<SelectContent>
|
||||
{kandangOptions.map((kandang) => (
|
||||
<SelectItem
|
||||
key={kandang.value}
|
||||
@@ -1017,12 +1003,6 @@ export function DailyChecklistContent() {
|
||||
{kandang.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
{isLoadingMoreKandang && (
|
||||
<div className='flex justify-center p-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
|
||||
</div>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/figma-make/components/base/select';
|
||||
import { Badge } from '@/figma-make/components/base/badge';
|
||||
import { Users, AlertCircle, Info, Loader2 } from 'lucide-react';
|
||||
import { Users, AlertCircle, Info } from 'lucide-react';
|
||||
import { DateRangePicker } from '@/figma-make/components/base/date-range-picker';
|
||||
import {
|
||||
BarChart,
|
||||
@@ -36,10 +36,10 @@ import { DailyChecklistSummary } from '@/types/api/daily-checklist/daily-checkli
|
||||
import { AxiosError } from 'axios';
|
||||
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
|
||||
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
|
||||
import { KandangApi } from '@/services/api/master-data';
|
||||
import { useSelect } from '@/components/input/SelectInput';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { formatDate } from '@/lib/helper';
|
||||
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||||
|
||||
const KANDANG_COLORS = [
|
||||
'#0069e0', // Blue (primary)
|
||||
@@ -77,20 +77,16 @@ export function Dashboard() {
|
||||
httpClientFetcher
|
||||
);
|
||||
|
||||
const {
|
||||
options: kandangOptions,
|
||||
loadMore: loadMoreKandang,
|
||||
isLoadingMore: isLoadingMoreKandang,
|
||||
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
|
||||
|
||||
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||
const target = e.target as HTMLDivElement;
|
||||
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
|
||||
if (!isLoadingMoreKandang) {
|
||||
loadMoreKandang();
|
||||
}
|
||||
const { options: kandangOptions } = useSelect(
|
||||
KandangApi.basePath,
|
||||
'id',
|
||||
'name',
|
||||
'search',
|
||||
{
|
||||
page: '1',
|
||||
limit: '100',
|
||||
}
|
||||
};
|
||||
);
|
||||
|
||||
const kandangColorMap: { [key: string]: string } = {};
|
||||
(kandangOptions || []).forEach((k, index) => {
|
||||
@@ -168,7 +164,7 @@ export function Dashboard() {
|
||||
>
|
||||
<SelectValue placeholder='Semua Kandang' />
|
||||
</SelectTrigger>
|
||||
<SelectContent onScroll={handleKandangScroll}>
|
||||
<SelectContent>
|
||||
<SelectItem value='ALL'>Semua Kandang</SelectItem>
|
||||
{kandangOptions.map((kandang) => (
|
||||
<SelectItem
|
||||
@@ -178,11 +174,6 @@ export function Dashboard() {
|
||||
{kandang.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
{isLoadingMoreKandang && (
|
||||
<div className='flex justify-center p-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
|
||||
</div>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
+13
-30
@@ -1,15 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Eye,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Search,
|
||||
Trash2,
|
||||
Edit,
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import { Eye, CheckCircle, XCircle, Search, Trash2, Edit } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/figma-make/components/base/card';
|
||||
import { Button } from '@/figma-make/components/base/button';
|
||||
import { Badge } from '@/figma-make/components/base/badge';
|
||||
@@ -42,9 +34,9 @@ import { DailyChecklist } from '@/types/api/daily-checklist/daily-checklist';
|
||||
import { cn } from '@/lib/helper';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { useSelect } from '@/components/input/SelectInput';
|
||||
import { KandangApi } from '@/services/api/master-data';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: 'ALL', label: 'Semua Status' },
|
||||
@@ -101,25 +93,21 @@ export function ListDailyChecklistContent() {
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
options: kandangOptions,
|
||||
isLoadingMore: isLoadingMoreKandang,
|
||||
loadMore: loadMoreKandang,
|
||||
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
|
||||
const { options: kandangOptions } = useSelect(
|
||||
KandangApi.basePath,
|
||||
'id',
|
||||
'name',
|
||||
'search',
|
||||
{
|
||||
page: '1',
|
||||
limit: '100',
|
||||
}
|
||||
);
|
||||
|
||||
const checklistList = isResponseSuccess(checklistListRes)
|
||||
? checklistListRes.data || []
|
||||
: [];
|
||||
|
||||
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||
const target = e.target as HTMLDivElement;
|
||||
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
|
||||
if (!isLoadingMoreKandang) {
|
||||
loadMoreKandang();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Modals
|
||||
const [showApproveModal, setShowApproveModal] = useState(false);
|
||||
const [showRejectModal, setShowRejectModal] = useState(false);
|
||||
@@ -502,7 +490,7 @@ export function ListDailyChecklistContent() {
|
||||
>
|
||||
<SelectValue placeholder='Semua Kandang' />
|
||||
</SelectTrigger>
|
||||
<SelectContent onScroll={handleKandangScroll}>
|
||||
<SelectContent>
|
||||
<SelectItem value='ALL'>Semua Kandang</SelectItem>
|
||||
{kandangOptions.map((kandang) => (
|
||||
<SelectItem
|
||||
@@ -512,11 +500,6 @@ export function ListDailyChecklistContent() {
|
||||
{kandang.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
{isLoadingMoreKandang && (
|
||||
<div className='flex justify-center p-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
|
||||
</div>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Plus,
|
||||
MoreVertical,
|
||||
Pencil,
|
||||
Trash2,
|
||||
Search,
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import { Plus, MoreVertical, Pencil, Trash2, Search } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/figma-make/components/base/card';
|
||||
import { Button } from '@/figma-make/components/base/button';
|
||||
import { Label } from '@/figma-make/components/base/label';
|
||||
@@ -56,8 +49,8 @@ import { cn } from '@/lib/helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { useSelect } from '@/components/input/SelectInput';
|
||||
import { KandangApi } from '@/services/api/master-data';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||||
|
||||
export function MasterEmployeeContent() {
|
||||
const {
|
||||
@@ -92,20 +85,16 @@ export function MasterEmployeeContent() {
|
||||
keepPreviousData: true,
|
||||
}
|
||||
);
|
||||
const {
|
||||
options: kandangOptions,
|
||||
loadMore: loadMoreKandang,
|
||||
isLoadingMore: isLoadingMoreKandang,
|
||||
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
|
||||
|
||||
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||
const target = e.target as HTMLDivElement;
|
||||
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
|
||||
if (!isLoadingMoreKandang) {
|
||||
loadMoreKandang();
|
||||
}
|
||||
const { options: kandangOptions } = useSelect(
|
||||
KandangApi.basePath,
|
||||
'id',
|
||||
'name',
|
||||
'search',
|
||||
{
|
||||
page: '1',
|
||||
limit: '100',
|
||||
}
|
||||
};
|
||||
);
|
||||
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
@@ -362,7 +351,7 @@ export function MasterEmployeeContent() {
|
||||
<SelectTrigger className='w-[180px] border-gray-200'>
|
||||
<SelectValue placeholder='Semua Kandang' />
|
||||
</SelectTrigger>
|
||||
<SelectContent onScroll={handleKandangScroll}>
|
||||
<SelectContent>
|
||||
<SelectItem value='all'>Semua Kandang</SelectItem>
|
||||
{kandangOptions.map((kandang) => (
|
||||
<SelectItem
|
||||
@@ -372,11 +361,6 @@ export function MasterEmployeeContent() {
|
||||
{kandang.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
{isLoadingMoreKandang && (
|
||||
<div className='flex justify-center p-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
|
||||
</div>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -487,12 +471,6 @@ export function MasterEmployeeContent() {
|
||||
kandang_ids: selected.map((id) => Number(id)),
|
||||
})
|
||||
}
|
||||
onLoadMore={() => {
|
||||
if (!isLoadingMoreKandang) {
|
||||
loadMoreKandang();
|
||||
}
|
||||
}}
|
||||
isLoadingMore={isLoadingMoreKandang}
|
||||
placeholder='Pilih kandang'
|
||||
className='mt-1.5'
|
||||
/>
|
||||
|
||||
@@ -1,585 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Plus, MoreVertical, Pencil, Trash2, Search } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/figma-make/components/base/card';
|
||||
import { Button } from '@/figma-make/components/base/button';
|
||||
import { Label } from '@/figma-make/components/base/label';
|
||||
import { Input } from '@/figma-make/components/base/input';
|
||||
import { Badge } from '@/figma-make/components/base/badge';
|
||||
import { MultiSelect } from '@/figma-make/components/base/multi-select';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/figma-make/components/base/select';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from '@/figma-make/components/base/dialog';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/figma-make/components/base/alert-dialog';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/figma-make/components/base/dropdown-menu';
|
||||
import { toast } from 'sonner';
|
||||
import useSWR from 'swr';
|
||||
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||||
import Table from '@/components/Table';
|
||||
import { DailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { cn } from '@/lib/helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { useSelect } from '@/components/input/SelectInput';
|
||||
import { KandangApi, LocationApi } from '@/services/api/master-data';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import { BaseDailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
|
||||
import { UserApi } from '@/services/api/user';
|
||||
|
||||
export function MasterKandangContent() {
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
setPage,
|
||||
setPageSize,
|
||||
toQueryString: getTableFilterQueryString,
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
search: '',
|
||||
location_id: '',
|
||||
status: '',
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
pageSize: 'limit',
|
||||
search: 'search',
|
||||
location_id: 'location_id',
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
data: dailyChecklistKandangs,
|
||||
isLoading: isLoadingDailyChecklistKandangs,
|
||||
mutate: refreshDailyChecklistKandangs,
|
||||
} = useSWR(
|
||||
`${DailyChecklistKandangApi.basePath}${getTableFilterQueryString()}`,
|
||||
DailyChecklistKandangApi.getAllFetcher,
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
);
|
||||
const { options: locationOptions } = useSelect(
|
||||
LocationApi.basePath,
|
||||
'id',
|
||||
'name',
|
||||
'search',
|
||||
{
|
||||
page: '1',
|
||||
limit: '100',
|
||||
}
|
||||
);
|
||||
|
||||
const { options: picOptions } = useSelect(
|
||||
UserApi.basePath,
|
||||
'id',
|
||||
'name',
|
||||
'search',
|
||||
{
|
||||
page: '1',
|
||||
limit: '100',
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
options: kandangOptions,
|
||||
isLoadingMore: isLoadingKandangOptionsMore,
|
||||
loadMore: loadMoreKandang,
|
||||
} = useSelect(KandangApi.basePath, 'id', 'name');
|
||||
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [kandangToDelete, setKandangToDelete] = useState<number | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalMode, setModalMode] = useState<'create' | 'edit'>('create');
|
||||
const [kandangForm, setKandangForm] = useState({
|
||||
id: 0,
|
||||
name: '',
|
||||
location_id: 0,
|
||||
pic_id: 0,
|
||||
// recording_kandangs: [] as number[],
|
||||
});
|
||||
|
||||
const dailyChecklistKandangColumns: ColumnDef<DailyChecklistKandang>[] = [
|
||||
{
|
||||
id: 'name',
|
||||
header: 'Nama',
|
||||
accessorKey: 'name',
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
id: 'location',
|
||||
header: 'Lokasi',
|
||||
accessorKey: 'location',
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => row.original.location.name ?? '-',
|
||||
},
|
||||
{
|
||||
id: 'pic',
|
||||
header: 'PIC',
|
||||
accessorKey: 'pic',
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => row.original.pic.name ?? '-',
|
||||
},
|
||||
{
|
||||
id: 'recording_kandangs',
|
||||
header: 'Kandang Recording',
|
||||
accessorKey: 'recording_kandangs',
|
||||
enableSorting: false,
|
||||
cell: ({ row }) =>
|
||||
row.original.recording_kandangs?.length > 0
|
||||
? row.original.recording_kandangs.map((item) => item.name).join(', ')
|
||||
: '-',
|
||||
},
|
||||
{
|
||||
id: 'action',
|
||||
header: 'Aksi',
|
||||
accessorKey: 'action',
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='h-8 w-8 p-0 hover:bg-gray-100'
|
||||
>
|
||||
<MoreVertical className='h-4 w-4 text-gray-600' />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end'>
|
||||
<DropdownMenuItem onClick={() => handleEdit(row.original)}>
|
||||
<Pencil className='mr-2 h-4 w-4' />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDeleteClick(row.original.id)}
|
||||
className='text-red-600'
|
||||
>
|
||||
<Trash2 className='mr-2 h-4 w-4' />
|
||||
Hapus
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handleAdd = () => {
|
||||
setModalMode('create');
|
||||
setKandangForm({
|
||||
id: 0,
|
||||
name: '',
|
||||
location_id: 0,
|
||||
pic_id: 0,
|
||||
// recording_kandangs: []
|
||||
});
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleEdit = (dailyChecklistKandang: DailyChecklistKandang) => {
|
||||
setModalMode('edit');
|
||||
setKandangForm({
|
||||
id: dailyChecklistKandang.id,
|
||||
name: dailyChecklistKandang.name,
|
||||
location_id: dailyChecklistKandang.location.id,
|
||||
pic_id: dailyChecklistKandang.pic.id,
|
||||
// recording_kandangs:
|
||||
// dailyChecklistKandang.recording_kandangs.map((item) => item.id) ?? [],
|
||||
});
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!kandangForm.name.trim()) {
|
||||
toast.error('Nama harus diisi');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!kandangForm.location_id) {
|
||||
toast.error('Lokasi wajib diisi');
|
||||
return;
|
||||
}
|
||||
|
||||
// if (!kandangForm.recording_kandangs.length) {
|
||||
// toast.error('Kandang recording wajib diisi');
|
||||
// return;
|
||||
// }
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
if (modalMode === 'create') {
|
||||
const createDailyChecklistKandangResponse =
|
||||
await DailyChecklistKandangApi.create({
|
||||
name: kandangForm.name.trim(),
|
||||
location_id: kandangForm.location_id,
|
||||
pic_id: kandangForm.pic_id,
|
||||
// recording_kandang_ids: kandangForm.recording_kandangs,
|
||||
});
|
||||
|
||||
if (isResponseError(createDailyChecklistKandangResponse)) {
|
||||
console.error(
|
||||
'Error creating kandang:',
|
||||
createDailyChecklistKandangResponse.message
|
||||
);
|
||||
toast.error('Gagal menambahkan kandang');
|
||||
return;
|
||||
}
|
||||
|
||||
refreshDailyChecklistKandangs();
|
||||
toast.success('Kandang berhasil ditambahkan');
|
||||
} else {
|
||||
const updateDailyChecklistKandangResponse =
|
||||
await DailyChecklistKandangApi.update(kandangForm.id, {
|
||||
name: kandangForm.name.trim(),
|
||||
location_id: kandangForm.location_id,
|
||||
pic_id: kandangForm.pic_id,
|
||||
// recording_kandang_ids: kandangForm.recording_kandangs,
|
||||
});
|
||||
|
||||
if (isResponseError(updateDailyChecklistKandangResponse)) {
|
||||
console.error(
|
||||
'Error updating kandang:',
|
||||
updateDailyChecklistKandangResponse.message
|
||||
);
|
||||
toast.error('Gagal menambahkan Kandang');
|
||||
return;
|
||||
}
|
||||
|
||||
refreshDailyChecklistKandangs();
|
||||
toast.success('Kandang berhasil diubah');
|
||||
}
|
||||
|
||||
setShowModal(false);
|
||||
setKandangForm({
|
||||
id: 0,
|
||||
name: '',
|
||||
location_id: 0,
|
||||
pic_id: 0,
|
||||
// recording_kandangs: [],
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error saving kandang:', error);
|
||||
toast.error('Terjadi kesalahan saat menyimpan kandang');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = (kandangId: number) => {
|
||||
setKandangToDelete(kandangId);
|
||||
setShowDeleteConfirm(true);
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
if (!kandangToDelete) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const deleteKandangResponse =
|
||||
await DailyChecklistKandangApi.delete(kandangToDelete);
|
||||
|
||||
if (isResponseError(deleteKandangResponse)) {
|
||||
console.error('Error deleting kandang:', deleteKandangResponse.message);
|
||||
toast.error('Gagal menghapus kandang');
|
||||
return;
|
||||
}
|
||||
|
||||
refreshDailyChecklistKandangs();
|
||||
toast.success('Kandang berhasil dihapus');
|
||||
setShowDeleteConfirm(false);
|
||||
setKandangToDelete(null);
|
||||
} catch (error) {
|
||||
console.error('Error deleting kandang:', error);
|
||||
toast.error('Terjadi kesalahan saat menghapus kandang');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoadingDailyChecklistKandangs && !dailyChecklistKandangs) {
|
||||
return (
|
||||
<div className='min-h-screen'>
|
||||
<div className='p-6'>
|
||||
<div className='mb-6'>
|
||||
<h1 className='text-2xl font-semibold text-gray-900'>
|
||||
Master Kandang
|
||||
</h1>
|
||||
<p className='text-sm text-gray-600 mt-1'>
|
||||
Master Data • <span className='text-[#0069e0]'>Kandang</span>
|
||||
</p>
|
||||
</div>
|
||||
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
|
||||
<CardContent className='p-12 text-center text-gray-500'>
|
||||
Memuat data...
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='min-h-screen'>
|
||||
<div className='p-6'>
|
||||
{/* Page Title */}
|
||||
<div className='mb-6'>
|
||||
<h1 className='text-2xl font-semibold text-gray-900'>
|
||||
Master Kandang
|
||||
</h1>
|
||||
<p className='text-sm text-gray-600 mt-1'>
|
||||
Master Data • <span className='text-[#0069e0]'>Kandang</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Main Card */}
|
||||
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
|
||||
<CardContent className='p-0'>
|
||||
{/* Single Toolbar Row */}
|
||||
<div className='flex flex-wrap items-center justify-between gap-4 p-6 border-b border-gray-200/60'>
|
||||
{/* LEFT: Search + Filters */}
|
||||
<div className='flex items-center gap-3 flex-wrap'>
|
||||
<div className='relative'>
|
||||
<Search className='absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4' />
|
||||
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari kandang...'
|
||||
value={tableFilterState.search}
|
||||
onChange={(e) => updateFilter('search', e.target.value)}
|
||||
className={{
|
||||
wrapper: 'w-full sm:w-[280px] border-gray-200',
|
||||
inputWrapper: 'px-3 py-2 h-fit rounded-md',
|
||||
input: 'text-sm',
|
||||
}}
|
||||
startAdornment={
|
||||
<Search className='text-gray-400 w-4 h-4' />
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
value={tableFilterState.location_id}
|
||||
onValueChange={(value) =>
|
||||
updateFilter('location_id', value === 'all' ? '' : value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className='w-[180px] border-gray-200'>
|
||||
<SelectValue placeholder='Semua Lokasi' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='all'>Semua Lokasi</SelectItem>
|
||||
{locationOptions.map((kandang) => (
|
||||
<SelectItem
|
||||
key={kandang.value}
|
||||
value={String(kandang.value)}
|
||||
>
|
||||
{kandang.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* RIGHT: Export + Add */}
|
||||
<div className='flex items-center gap-2 flex-wrap'>
|
||||
<Button
|
||||
onClick={handleAdd}
|
||||
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
|
||||
>
|
||||
<Plus className='w-4 h-4 mr-2' />
|
||||
Tambah Kandang
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<Table<DailyChecklistKandang>
|
||||
data={
|
||||
isResponseSuccess(dailyChecklistKandangs)
|
||||
? dailyChecklistKandangs?.data
|
||||
: []
|
||||
}
|
||||
columns={dailyChecklistKandangColumns}
|
||||
pageSize={tableFilterState.pageSize}
|
||||
onPageSizeChange={setPageSize}
|
||||
rowOptions={[10, 20, 50, 100]}
|
||||
page={
|
||||
isResponseSuccess(dailyChecklistKandangs)
|
||||
? dailyChecklistKandangs?.meta?.page
|
||||
: 0
|
||||
}
|
||||
totalItems={
|
||||
isResponseSuccess(dailyChecklistKandangs)
|
||||
? dailyChecklistKandangs?.meta?.total_results
|
||||
: 0
|
||||
}
|
||||
onPageChange={setPage}
|
||||
isLoading={isLoadingDailyChecklistKandangs}
|
||||
className={{
|
||||
containerClassName: cn({
|
||||
'w-full mb-20':
|
||||
isResponseSuccess(dailyChecklistKandangs) &&
|
||||
dailyChecklistKandangs?.data?.length === 0,
|
||||
}),
|
||||
tableWrapperClassName:
|
||||
'overflow-x-auto border border-solid border-base-content/10 rounded-none',
|
||||
headerRowClassName: 'bg-gray-50/50',
|
||||
headerColumnClassName:
|
||||
'text-left py-3.5 px-6 text-sm font-semibold text-gray-700',
|
||||
paginationClassName: 'px-4',
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Add/Edit Modal */}
|
||||
<Dialog open={showModal} onOpenChange={setShowModal}>
|
||||
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{modalMode === 'create' ? 'Tambah Kandang' : 'Edit Kandang'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{modalMode === 'create'
|
||||
? 'Masukkan detail Kandang baru'
|
||||
: 'Ubah detail Kandang'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className='space-y-4 py-4'>
|
||||
<div>
|
||||
<Label htmlFor='nama-kandang'>
|
||||
Nama Kandang <span className='text-red-500'>*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id='nama-kandang'
|
||||
value={kandangForm.name}
|
||||
onChange={(e) =>
|
||||
setKandangForm({ ...kandangForm, name: e.target.value })
|
||||
}
|
||||
placeholder='Masukkan nama Kandang'
|
||||
className='mt-1.5'
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor='category'>
|
||||
Lokasi <span className='text-red-500'>*</span>
|
||||
</Label>
|
||||
<Select
|
||||
value={
|
||||
kandangForm.location_id ? String(kandangForm.location_id) : ''
|
||||
}
|
||||
onValueChange={(value) =>
|
||||
setKandangForm({ ...kandangForm, location_id: Number(value) })
|
||||
}
|
||||
>
|
||||
<SelectTrigger id='category' className='mt-1.5'>
|
||||
<SelectValue placeholder='Pilih lokasi' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{locationOptions.map((cat) => (
|
||||
<SelectItem key={cat.value} value={String(cat.value)}>
|
||||
{cat.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor='pic'>
|
||||
PIC <span className='text-red-500'>*</span>
|
||||
</Label>
|
||||
<Select
|
||||
value={kandangForm.pic_id ? String(kandangForm.pic_id) : ''}
|
||||
onValueChange={(value) =>
|
||||
setKandangForm({ ...kandangForm, pic_id: Number(value) })
|
||||
}
|
||||
>
|
||||
<SelectTrigger id='pic' className='mt-1.5'>
|
||||
<SelectValue placeholder='Pilih PIC' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{picOptions.map((cat) => (
|
||||
<SelectItem key={cat.value} value={String(cat.value)}>
|
||||
{cat.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={() => setShowModal(false)}
|
||||
disabled={loading}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={loading}
|
||||
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
|
||||
>
|
||||
{loading ? 'Menyimpan...' : 'Simpan'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Confirmation */}
|
||||
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
||||
<AlertDialogContent className='bg-white rounded-xl shadow-lg sm:max-w-md'>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Hapus Kandang?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Data Kandang akan dihapus secara permanen.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={loading}>Batal</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
disabled={loading}
|
||||
className='bg-red-600 hover:bg-red-700 text-white'
|
||||
>
|
||||
{loading ? 'Menghapus...' : 'Hapus'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/figma-make/components/base/select';
|
||||
import { useSelect } from '@/components/input/SelectInput';
|
||||
import { AreaApi, LocationApi } from '@/services/api/master-data';
|
||||
import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data';
|
||||
import useSWR from 'swr';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import { DailyChecklistReport } from '@/types/api/daily-checklist/daily-checklist';
|
||||
@@ -26,8 +26,7 @@ import { ColumnDef } from '@tanstack/react-table';
|
||||
import { PhaseApi } from '@/services/api/daily-checklist/phase';
|
||||
import { EmployeeApi } from '@/services/api/daily-checklist/employee';
|
||||
import { Button } from '@/figma-make/components/base/button';
|
||||
import { Download, Loader2 } from 'lucide-react';
|
||||
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||||
import { Download } from 'lucide-react';
|
||||
|
||||
const MONTH_OPTIONS = [
|
||||
{ value: '1', label: 'Januari' },
|
||||
@@ -130,23 +129,18 @@ export function DailyChecklistReportsContent() {
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
options: kandangOptions,
|
||||
loadMore: loadMoreKandang,
|
||||
isLoadingMore: isLoadingMoreKandang,
|
||||
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name', 'search', {
|
||||
area_id: tableFilterState.area_id,
|
||||
location_id: tableFilterState.location_id,
|
||||
});
|
||||
|
||||
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||
const target = e.target as HTMLDivElement;
|
||||
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
|
||||
if (!isLoadingMoreKandang) {
|
||||
loadMoreKandang();
|
||||
}
|
||||
const { options: kandangOptions } = useSelect(
|
||||
KandangApi.basePath,
|
||||
'id',
|
||||
'name',
|
||||
'search',
|
||||
{
|
||||
page: '1',
|
||||
limit: '100',
|
||||
area_id: tableFilterState.area_id,
|
||||
location_id: tableFilterState.location_id,
|
||||
}
|
||||
};
|
||||
);
|
||||
|
||||
const { options: phaseOptions } = useSelect(
|
||||
PhaseApi.basePath,
|
||||
@@ -441,7 +435,7 @@ export function DailyChecklistReportsContent() {
|
||||
>
|
||||
<SelectValue placeholder='Semua Kandang' />
|
||||
</SelectTrigger>
|
||||
<SelectContent onScroll={handleKandangScroll}>
|
||||
<SelectContent>
|
||||
<SelectItem value='ALL'>Semua Kandang</SelectItem>
|
||||
{kandangOptions.map((kandang) => (
|
||||
<SelectItem
|
||||
@@ -451,11 +445,6 @@ export function DailyChecklistReportsContent() {
|
||||
{kandang.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
{isLoadingMoreKandang && (
|
||||
<div className='flex justify-center p-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
|
||||
</div>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -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
@@ -1,24 +0,0 @@
|
||||
import { BaseMetadata } from '@/types/api/api-general';
|
||||
import { BaseKandang } from '@/types/api/master-data/kandang';
|
||||
import { BaseLocation } from '@/types/api/master-data/location';
|
||||
import { BaseUser } from '@/types/api/user';
|
||||
|
||||
export type BaseDailyChecklistKandang = {
|
||||
id: number;
|
||||
name: string;
|
||||
location: BaseLocation;
|
||||
recording_kandangs: Pick<BaseKandang, 'id' | 'name'>[];
|
||||
pic: BaseUser;
|
||||
};
|
||||
|
||||
export type DailyChecklistKandang = BaseMetadata & BaseDailyChecklistKandang;
|
||||
|
||||
export type CreateDailyChecklistKandangPayload = {
|
||||
name: string;
|
||||
location_id: number;
|
||||
pic_id: number;
|
||||
// recording_kandang_ids: number[];
|
||||
};
|
||||
|
||||
export type UpdateDailyChecklistKandangPayload =
|
||||
CreateDailyChecklistKandangPayload;
|
||||
-11
@@ -9,19 +9,8 @@ export type BaseProductWarehouse = {
|
||||
warehouse_id: number;
|
||||
uom: Uom;
|
||||
quantity: number;
|
||||
transfer_available_qty?: number;
|
||||
product: Product;
|
||||
warehouse: Warehouse;
|
||||
project_flock_kandang?: {
|
||||
id: number;
|
||||
project_flock_id: number;
|
||||
kandang_id: number;
|
||||
period: number;
|
||||
project_flock?: {
|
||||
id: number;
|
||||
flock_name: string;
|
||||
};
|
||||
};
|
||||
week?: number | null;
|
||||
};
|
||||
|
||||
|
||||
-3
@@ -1,7 +1,6 @@
|
||||
import { BaseMetadata } from '@/types/api/api-general';
|
||||
import { BaseLocation } from '@/types/api/master-data/location';
|
||||
import { BaseUser } from '@/types/api/user';
|
||||
import { BaseDailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
|
||||
|
||||
export type BaseKandang = {
|
||||
id: number;
|
||||
@@ -11,7 +10,6 @@ export type BaseKandang = {
|
||||
capacity: number;
|
||||
pic: BaseUser;
|
||||
project_flock_kandang_id?: number;
|
||||
kandang_group: Pick<BaseDailyChecklistKandang, 'id' | 'name'>;
|
||||
};
|
||||
|
||||
export type Kandang = BaseMetadata & BaseKandang;
|
||||
@@ -21,7 +19,6 @@ export type CreateKandangPayload = {
|
||||
location_id: number;
|
||||
capacity: number;
|
||||
pic_id: number;
|
||||
group_id: number;
|
||||
};
|
||||
|
||||
export type UpdateKandangPayload = CreateKandangPayload;
|
||||
|
||||
-2
@@ -74,8 +74,6 @@ export type ProjectFlockKandangLookup = {
|
||||
available_quantity?: number;
|
||||
population: number;
|
||||
chick_in_date: string;
|
||||
is_transition: boolean;
|
||||
is_laying: boolean;
|
||||
};
|
||||
|
||||
export type ProjectFlockAvailableQuantity = {
|
||||
|
||||
+2
-2
@@ -49,8 +49,8 @@ export type BaseRecording = {
|
||||
project_flock: ProjectFlock;
|
||||
record_datetime: string;
|
||||
day: number;
|
||||
is_transition: boolean;
|
||||
is_laying: boolean;
|
||||
population_can_change: boolean;
|
||||
transfer_executed: boolean;
|
||||
} & ProductionMetrics;
|
||||
|
||||
export type RecordingDepletion = {
|
||||
|
||||
Reference in New Issue
Block a user