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