fix(FE): resolve merge conflict

This commit is contained in:
randy-ar
2025-11-13 14:12:25 +07:00
20 changed files with 1280 additions and 1410 deletions
+132 -62
View File
@@ -1,76 +1,146 @@
stages: [notify]
stages:
- build
- deploy
# --- Notify when MR is opened/updated ---
notify_discord_mr:
stage: notify
image: alpine:3.20
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
.build_template: &build_template
stage: build
image: node:20-alpine
cache:
key: npm-cache
paths:
- node_modules/
variables:
WEBHOOK_URL: $DISCORD_WEBHOOK_URL
before_script:
- apk add --no-cache curl jq
script: |
MR_URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}"
NPM_CONFIG_PRODUCTION: 'false'
NODE_ENV: ''
script:
- echo "Installing dependencies..."
- npm ci --no-audit --no-fund
- echo "Building Next.js static export..."
- npx next build
artifacts:
name: 'out-$CI_COMMIT_SHORT_SHA'
paths:
- out/
expire_in: 1 week
.deploy_template: &deploy_template
stage: deploy
image:
name: amazon/aws-cli:latest
entrypoint: ['/bin/sh', '-c']
script:
- set -e
- aws --version
- echo "Cleaning up newline characters in AWS credentials..."
- export AWS_ACCESS_KEY_ID=$(echo $AWS_ACCESS_KEY_ID | tr -d '\r\n')
- export AWS_SECRET_ACCESS_KEY=$(echo $AWS_SECRET_ACCESS_KEY | tr -d '\r\n')
- echo "Deploying to s3://$S3_BUCKET in region $AWS_REGION"
- aws s3api head-bucket --bucket "$S3_BUCKET" --region "$AWS_REGION" || aws s3api create-bucket --bucket "$S3_BUCKET" --region "$AWS_REGION" --create-bucket-configuration LocationConstraint="$AWS_REGION"
- aws s3 sync ./out "s3://$S3_BUCKET" --delete --region "$AWS_REGION" --endpoint-url "https://s3.ap-southeast-3.amazonaws.com"
# CloudFront invalidation
- |
STATUS="success"
if [ -n "$CLOUDFRONT_DISTRIBUTION_ID" ]; then
echo "Invalidating CloudFront cache..."
if ! aws cloudfront create-invalidation --distribution-id "$CLOUDFRONT_DISTRIBUTION_ID" --paths "/*"; then
echo "CloudFront invalidation failed."
STATUS="failed"
fi
else
echo "No CloudFront distribution specified — skipping invalidation"
fi
# Notifikasi Discord
- |
RUN_URL="${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}"
if [ "$CI_COMMIT_BRANCH" = "development" ]; then
ENVIRONMENT_NAME="WEB-LTI-DEV"
elif [ "$CI_COMMIT_BRANCH" = "master" ]; then
ENVIRONMENT_NAME="WEB-LTI-PROD"
else
ENVIRONMENT_NAME="UNKNOWN"
fi
if [ "$STATUS" = "success" ]; then
COLOR=3066993
TITLE="✅ Deployment ${ENVIRONMENT_NAME} Succeeded"
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` completed successfully."
else
COLOR=15158332
TITLE="❌ Deployment ${ENVIRONMENT_NAME} Failed"
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` encountered issues."
fi
jq -n \
--arg title "$TITLE" \
--arg desc "$DESC" \
--arg color "$COLOR" \
--arg repo "$CI_PROJECT_PATH" \
--arg mr "#${CI_MERGE_REQUEST_IID}" \
--arg url "$MR_URL" \
--arg requestor "${GITLAB_USER_LOGIN:-$GITLAB_USER_NAME}" \
--arg source "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" \
--arg target "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" \
--arg title "$CI_MERGE_REQUEST_TITLE" \
--arg actor "$GITLAB_USER_LOGIN" \
--arg commit "$CI_COMMIT_SHA" \
--arg run_url "$RUN_URL" \
'{
username: "CI Bot - FE",
username: "CI Bot - LTI WEB",
embeds: [{
title: "📣 [LTI WEB CLIENT] Merge Request Opened/Updated",
description: ($mr + " in " + $repo),
url: $url,
color: 3447003,
title: $title,
description: $desc,
color: ($color|tonumber),
fields: [
{name: "Author", value: $requestor, inline: true},
{name: "Source → Target", value: ($source + " → " + $target), inline: true},
{name: "Title", value: $title}
{name: "Repository", value: $repo, inline: true},
{name: "Actor", value: $actor, inline: true},
{name: "Commit", value: $commit, inline: false},
{name: "Pipeline", value: ("[Open run](" + $run_url + ")"), inline: false}
]
}]
}' \
| curl -sS -H "Content-Type: application/json" -d @- "$WEBHOOK_URL"
}' > payload.json
# --- Notify when MR is merged ---
notify_discord_merge:
stage: notify
image: alpine:3.20
curl -sS -H "Content-Type: application/json" -d @payload.json "$DISCORD_WEBHOOK_URL"
# ====== DEVELOPMENT (Branch development) ======
build:dev:
<<: *build_template
rules:
# Only run for merge request pipelines that are in merged state
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_STATE == "merged"'
- if: '$CI_COMMIT_BRANCH == "development"'
environment:
name: development
variables:
WEBHOOK_URL: $DISCORD_WEBHOOK_URL
before_script:
- apk add --no-cache curl jq
script: |
MR_URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}"
NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id'
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-api-sso.mbugroup.id'
deploy:dev:
<<: *deploy_template
needs: ['build:dev']
rules:
- if: '$CI_COMMIT_BRANCH == "development"'
variables:
S3_BUCKET: 'dev-lti-erp.mbugroup.id'
AWS_REGION: 'ap-southeast-3'
CLOUDFRONT_DISTRIBUTION_ID: 'E1Z8XTA8XF1GIV'
environment:
name: development
url: https://dev-lti-erp.mbugroup.id
# ====== PRODUCTION ======
# build:production:
# <<: *build_template
# rules:
# # pilih salah satu: pakai branch master ATAU pakai tags rilis
# - if: '$CI_COMMIT_BRANCH == "master"'
# # - if: '$CI_COMMIT_TAG' # kalau mau rilis via tag, uncomment ini dan hapus baris di atas
# environment:
# name: production
# deploy:production:
# <<: *deploy_template
# needs: ["build:production"]
# rules:
# - if: '$CI_COMMIT_BRANCH == "master"'
# # - if: '$CI_COMMIT_TAG' # selaras dengan rule di build:production
# variables:
# S3_BUCKET: "lti-erp.mbugroup.id"
# CLOUDFRONT_DISTRIBUTION_ID: "ddfd"
# environment:
# name: production
# url: https://royalgoldcapital.com
jq -n \
--arg repo "$CI_PROJECT_PATH" \
--arg mr "#${CI_MERGE_REQUEST_IID}" \
--arg url "$MR_URL" \
--arg requestor "${CI_MERGE_REQUEST_AUTHOR}" \
--arg source "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" \
--arg target "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" \
--arg title "$CI_MERGE_REQUEST_TITLE" \
'{
username: "CI Bot - FE",
embeds: [{
title: "✅ [LTI WEB CLIENT] Merge Request Merged",
description: ($mr + " has been merged into " + $repo),
url: $url,
color: 3066993,
fields: [
{name: "Author", value: $requestor, inline: true},
{name: "Source → Target", value: ($source + " → " + $target), inline: true},
{name: "Title", value: $title}
]
}]
}' \
| curl -sS -H "Content-Type: application/json" -d @- "$WEBHOOK_URL"
+1
View File
@@ -1,2 +1,3 @@
npm run format
npm run lint
npm run build
+25
View File
@@ -0,0 +1,25 @@
FROM node:20-alpine
RUN apk add --no-cache git bash build-base curl
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# Buat config agar Next tahu output: export
RUN echo "const config = { output: 'export', images: { unoptimized: true } }; export default config;" > next.config.mjs
# Build project (Next.js 15 otomatis static export)
RUN NEXT_DISABLE_TURBOPACK=1 npx next build
# Copy static assets dan hasil build agar bisa diakses
RUN mkdir -p .next/server/app/_next && \
cp -r .next/static .next/server/app/_next/static && \
cp -r public/* .next/server/app/
EXPOSE 3000
CMD ["npx", "serve", ".next/server/app", "-l", "3000"]
+39
View File
@@ -0,0 +1,39 @@
version: '3.9'
services:
dev-web-lti:
container_name: dev-web-lti
build:
context: .
dockerfile: Dockerfile
ports:
- '3002:3000'
env_file:
- .env
environment:
NODE_ENV: production
APP_ENV: production
networks:
- dev-lti-network
restart: always
deploy:
resources:
limits:
cpus: '3.0'
memory: 3G
reservations:
cpus: '1.0'
memory: 512M
extra_hosts:
- 'host.docker.internal:host-gateway'
# Optional: aktifkan healthcheck jika punya endpoint
# healthcheck:
# test: ["CMD-SHELL", "curl -fsS http://localhost:3000/api/healthz || exit 1"]
# interval: 10s
# timeout: 3s
# retries: 10
# start_period: 15s
networks:
dev-lti-network:
external: true
@@ -8,93 +8,6 @@ import TransferToLayingForm from '@/components/pages/production/transfer-to-layi
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
// TODO: delete dummy data
const DUMMY_TRANSFER_TO_LAYING_EDIT: TransferToLaying = {
id: 1,
transfer_date: '2025-10-14',
flock_source: {
id: 1,
name: 'Flock asal test',
},
flock_destination: {
id: 2,
name: 'Flock tujuan destination',
},
quantity: 10,
kandangs: [
{
kandang: {
id: 1,
capacity: 1000,
name: 'Kandang test',
status: 'ACTIVE',
location: {
id: 1,
name: 'test location',
address: 'test address 1',
area: { id: 1, name: 'test area 1' },
},
pic: {
id: 1,
id_user: 2,
email: 'test@gmail.com',
name: 'test',
},
created_user: {
id: 1,
id_user: 2,
email: 'test@gmail.com',
name: 'test',
},
created_at: '14-10-2025',
updated_at: '14-10-2025',
},
quantity: 8,
},
{
kandang: {
id: 1,
name: 'Kandang test 2',
capacity: 3000,
status: 'ACTIVE',
location: {
id: 1,
name: 'test location',
address: 'test address 1',
area: { id: 1, name: 'test area 1' },
},
pic: {
id: 1,
id_user: 2,
email: 'test@gmail.com',
name: 'test',
},
created_user: {
id: 1,
id_user: 2,
email: 'test@gmail.com',
name: 'test',
},
created_at: '14-10-2025',
updated_at: '14-10-2025',
},
quantity: 2,
},
],
reason: 'Test alasan',
created_user: {
id: 1,
id_user: 2,
email: 'test@gmail.com',
name: 'test',
},
created_at: '14-10-2025',
updated_at: '14-10-2025',
};
const TransferToLayingEdit = () => {
const router = useRouter();
const searchParams = useSearchParams();
@@ -116,33 +29,33 @@ const TransferToLayingEdit = () => {
);
}
// TODO: remove dummy data and integrate with real API
if (
!isLoadingTransferToLaying &&
(!transferToLaying ||
(isResponseError(transferToLaying) && !DUMMY_TRANSFER_TO_LAYING_EDIT))
(!transferToLaying || isResponseError(transferToLaying))
) {
router.replace('/404');
return;
}
if (
isResponseSuccess(transferToLaying) &&
transferToLaying.data.approval.step_number === 2
) {
router.replace('/production/transfer-to-laying');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingTransferToLaying && (
<span className='loading loading-spinner loading-xl' />
)}
{/* {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
<TransferToLayingForm
type='detail'
initialValues={transferToLaying.data}
/>
)} */}
{/* TODO: remove this dummy data and integrate to real API */}
{!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
<TransferToLayingForm
type='edit'
initialValues={DUMMY_TRANSFER_TO_LAYING_EDIT}
initialValues={transferToLaying.data}
/>
)}
</div>
);
};
@@ -8,93 +8,6 @@ import TransferToLayingForm from '@/components/pages/production/transfer-to-layi
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
// TODO: delete dummy data
const DUMMY_TRANSFER_TO_LAYING_DETAIL: TransferToLaying = {
id: 1,
transfer_date: '2025-10-14',
flock_source: {
id: 1,
name: 'Flock asal test',
},
flock_destination: {
id: 2,
name: 'Flock tujuan destination',
},
quantity: 10,
kandangs: [
{
kandang: {
id: 1,
capacity: 1000,
name: 'Kandang test',
status: 'ACTIVE',
location: {
id: 1,
name: 'test location',
address: 'test address 1',
area: { id: 1, name: 'test area 1' },
},
pic: {
id: 1,
id_user: 2,
email: 'test@gmail.com',
name: 'test',
},
created_user: {
id: 1,
id_user: 2,
email: 'test@gmail.com',
name: 'test',
},
created_at: '14-10-2025',
updated_at: '14-10-2025',
},
quantity: 8,
},
{
kandang: {
id: 1,
capacity: 3000,
name: 'Kandang test 2',
status: 'ACTIVE',
location: {
id: 1,
name: 'test location',
address: 'test address 1',
area: { id: 1, name: 'test area 1' },
},
pic: {
id: 1,
id_user: 2,
email: 'test@gmail.com',
name: 'test',
},
created_user: {
id: 1,
id_user: 2,
email: 'test@gmail.com',
name: 'test',
},
created_at: '14-10-2025',
updated_at: '14-10-2025',
},
quantity: 2,
},
],
reason: 'Test alasan',
created_user: {
id: 1,
id_user: 2,
email: 'test@gmail.com',
name: 'test',
},
created_at: '14-10-2025',
updated_at: '14-10-2025',
};
const TransferToLayingDetail = () => {
const router = useRouter();
const searchParams = useSearchParams();
@@ -116,11 +29,9 @@ const TransferToLayingDetail = () => {
);
}
// TODO: remove dummy data and integrate with real API
if (
!isLoadingTransferToLaying &&
(!transferToLaying ||
(isResponseError(transferToLaying) && !DUMMY_TRANSFER_TO_LAYING_DETAIL))
(!transferToLaying || isResponseError(transferToLaying))
) {
router.replace('/404');
return;
@@ -131,18 +42,13 @@ const TransferToLayingDetail = () => {
{isLoadingTransferToLaying && (
<span className='loading loading-spinner loading-xl' />
)}
{/* {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
{!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
<TransferToLayingForm
type='detail'
initialValues={transferToLaying.data}
/>
)} */}
{/* TODO: remove this dummy data and integrate to real API */}
<TransferToLayingForm
type='detail'
initialValues={DUMMY_TRANSFER_TO_LAYING_DETAIL}
/>
)}
</div>
);
};
+7
View File
@@ -13,6 +13,7 @@ import {
FilterFn,
SortingState,
OnChangeFn,
Row,
} from '@tanstack/react-table';
import { rankItem } from '@tanstack/match-sorter-utils';
import { Icon } from '@iconify/react';
@@ -50,6 +51,7 @@ export interface TableProps<TData extends object> {
manualSorting?: boolean;
rowSelection?: Record<string, boolean>;
setRowSelection?: OnChangeFn<Record<string, boolean>>;
enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
}
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
@@ -90,6 +92,7 @@ const Table = <TData extends object>({
manualSorting = false,
rowSelection,
setRowSelection,
enableRowSelection,
}: TableProps<TData>) => {
const isServerSideTable =
totalItems !== undefined &&
@@ -150,6 +153,10 @@ const Table = <TData extends object>({
tableOptions.getRowId = (row) => (row as { id: string }).id;
}
if (enableRowSelection !== undefined) {
tableOptions.enableRowSelection = enableRowSelection;
}
const table = useReactTable(tableOptions);
const { setPageSize } = table;
+2 -2
View File
@@ -7,10 +7,10 @@ import {
useState,
} from 'react';
import { cn, formatDate } from '@/lib/helper';
import Modal, { useModal } from '../Modal';
import Modal, { useModal } from '@/components/Modal';
import { DateRange, DayPicker, Matcher } from 'react-day-picker';
import 'react-day-picker/dist/style.css';
import Button from '../Button';
import Button from '@/components/Button';
import { Icon } from '@iconify/react';
export interface DateInputProps {
+5 -1
View File
@@ -9,7 +9,7 @@ import Button from '@/components/Button';
import { cn } from '@/lib/helper';
import { Color } from '@/types/theme';
interface ConfirmationModalProps {
export interface ConfirmationModalProps {
ref: RefObject<HTMLDialogElement | null>;
type?: 'info' | 'success' | 'error';
text?: string;
@@ -30,6 +30,7 @@ interface ConfirmationModalProps {
modal?: string;
modalBox?: string;
};
children?: React.ReactNode;
}
const ConfirmationModal = ({
@@ -40,6 +41,7 @@ const ConfirmationModal = ({
primaryButton,
secondaryButton,
className,
children,
}: ConfirmationModalProps) => {
const closeModalHandler = () => {
ref.current?.close();
@@ -90,6 +92,8 @@ const ConfirmationModal = ({
{text ?? 'Apakah anda yakin ingin melakukan hal ini?'}
</p>
{children && <div className='w-full'>{children}</div>}
<div className='w-full flex flex-row gap-2'>
{secondaryButton && secondaryButton.text && (
<Button
@@ -0,0 +1,69 @@
'use client';
import { ChangeEventHandler, useId, useState } from 'react';
import ConfirmationModal, {
ConfirmationModalProps,
} from '@/components/modal/ConfirmationModal';
import TextArea from '@/components/input/TextArea';
import { Color } from '@/types/theme';
interface ConfirmationModalWithNotesProps
extends Omit<ConfirmationModalProps, 'children' | 'primaryButton'> {
rows?: number;
placeholder?: string;
primaryButton?: {
text?: string;
color?: Color;
isLoading?: boolean;
onClick?: (notes: string) => void;
};
}
const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
ref,
type = 'info',
text,
closeOnBackdrop,
primaryButton,
secondaryButton,
className,
rows = 3,
placeholder = 'Catatan...',
}) => {
const randomId = useId();
const [notes, setNotes] = useState('');
const notesChangeHandler: ChangeEventHandler<HTMLTextAreaElement> = (e) => {
setNotes(e.target.value);
};
return (
<ConfirmationModal
ref={ref}
type={type}
text={text}
closeOnBackdrop={closeOnBackdrop}
primaryButton={{
...primaryButton,
onClick: () => {
primaryButton?.onClick?.(notes);
},
}}
secondaryButton={secondaryButton}
className={className}
>
<TextArea
name={randomId}
placeholder={placeholder}
value={notes}
onChange={notesChangeHandler}
rows={rows}
/>
</ConfirmationModal>
);
};
export default ConfirmationModalWithNotes;
+8 -1
View File
@@ -138,10 +138,17 @@ export const formatGroupedApprovalsToApprovalSteps = (
if (!approvalGroup) {
const isWaiting = currentStepNumber === latestApproval.step_number + 1;
const isPreviousApprovalRejected =
groupedApprovals[groupedApprovals.length - 1].approvals[0].action ===
'REJECTED';
return {
name: approvalLineItem.step_name,
status: isWaiting ? 'WAITING' : 'IDLE',
status: isPreviousApprovalRejected
? 'IDLE'
: isWaiting
? 'WAITING'
: 'IDLE',
};
}
@@ -2,7 +2,12 @@
import { ChangeEventHandler, useState } from 'react';
import useSWR from 'swr';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import {
CellContext,
ColumnDef,
Row,
SortingState,
} from '@tanstack/react-table';
import toast from 'react-hot-toast';
import { Icon } from '@iconify/react';
@@ -20,6 +25,7 @@ import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import TextInput from '@/components/input/TextInput';
import CheckboxInput from '@/components/input/CheckboxInput';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
@@ -29,6 +35,7 @@ import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ROWS_OPTIONS } from '@/config/constant';
import { Flock } from '@/types/api/master-data/flock';
import { FlockApi } from '@/services/api/master-data';
import PillBadge from '@/components/PillBadge';
const RowOptionsMenu = ({
type = 'dropdown',
@@ -43,6 +50,16 @@ const RowOptionsMenu = ({
rejectClickHandler: () => void;
deleteClickHandler: () => void;
}) => {
const showEditButton =
props.row.original.approval.action !== 'APPROVED' &&
props.row.original.approval.action !== 'REJECTED';
const showDeleteButton = showEditButton;
// TODO: apply RBAC
const showApproveButton = showEditButton;
const showRejectButton = showEditButton;
return (
<RowOptionsMenuWrapper type={type}>
<Button
@@ -55,6 +72,7 @@ const RowOptionsMenu = ({
Detail
</Button>
{showEditButton && (
<Button
href={`/production/transfer-to-laying/detail/edit/?transferToLayingId=${props.row.original.id}`}
variant='ghost'
@@ -64,7 +82,10 @@ const RowOptionsMenu = ({
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
)}
{/* TODO: apply RBAC */}
{showApproveButton && (
<Button
variant='ghost'
color='success'
@@ -74,7 +95,8 @@ const RowOptionsMenu = ({
<Icon icon='material-symbols:check' width={24} height={24} />
Approve
</Button>
)}
{showRejectButton && (
<Button
variant='ghost'
color='error'
@@ -84,7 +106,8 @@ const RowOptionsMenu = ({
<Icon icon='material-symbols:close' width={24} height={24} />
Reject
</Button>
)}
{showDeleteButton && (
<Button
onClick={deleteClickHandler}
variant='ghost'
@@ -99,6 +122,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
)}
</RowOptionsMenuWrapper>
);
};
@@ -187,17 +211,24 @@ const TransferToLayingsTable = () => {
/>
</div>
),
cell: ({ row }) => (
cell: ({ row }) => {
const isCheckboxDisabled =
!row.getCanSelect() ||
row.original.approval.action === 'APPROVED' ||
row.original.approval.action === 'REJECTED';
return (
<div>
<CheckboxInput
name='row'
checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
disabled={isCheckboxDisabled}
indeterminate={row.getIsSomeSelected()}
onChange={row.getToggleSelectedHandler()}
/>
</div>
),
);
},
},
{
header: '#',
@@ -214,21 +245,55 @@ const TransferToLayingsTable = () => {
{
accessorKey: 'flock_source',
header: 'Flock Asal',
cell: (props) => props.row.original.flock_source.name,
cell: (props) => props.row.original.from_project_flock.flock_name,
},
{
accessorKey: 'flock_destination',
header: 'Flock Tujuan',
cell: (props) => props.row.original.flock_destination.name,
cell: (props) => props.row.original.to_project_flock.flock_name,
},
{
accessorKey: 'quantity',
accessorKey: 'usage_qty',
header: 'Kuantitas',
cell: (props) => props.getValue() ?? props.row.original.pending_usage_qty,
},
{
accessorKey: 'reason',
accessorKey: 'notes',
header: 'Alasan Transfer',
},
{
header: 'Status',
cell: (props) => {
const isLatestApprovalRejected =
props.row.original.approval.action === 'REJECTED';
let latestApprovalStepName = props.row.original.approval.step_name;
let pillBadgeColor: 'yellow' | 'green' | 'gray' | 'red' = 'gray';
switch (latestApprovalStepName.toLowerCase()) {
case 'pengajuan':
pillBadgeColor = 'yellow';
break;
case 'disetujui':
pillBadgeColor = 'green';
break;
}
if (isLatestApprovalRejected) {
pillBadgeColor = 'red';
latestApprovalStepName = 'Ditolak';
}
return (
<PillBadge
content={latestApprovalStepName}
color={pillBadgeColor}
className='text-sm'
/>
);
},
},
{
header: 'Aksi',
cell: (props) => {
@@ -237,7 +302,7 @@ const TransferToLayingsTable = () => {
const currentRowRelativeIndex =
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 3;
const approveClickHandler = () => {
setSelectedTransferToLaying(props.row.original);
@@ -268,7 +333,7 @@ const TransferToLayingsTable = () => {
return (
<>
{currentPageSize > 2 && (
{currentPageSize > 3 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu
type='dropdown'
@@ -280,7 +345,7 @@ const TransferToLayingsTable = () => {
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
{currentPageSize <= 3 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
@@ -297,6 +362,15 @@ const TransferToLayingsTable = () => {
},
];
const tableEnableRowSelectionHandler: (
row: Row<TransferToLaying>
) => boolean = (row) => {
return (
row.original.approval.action !== 'APPROVED' &&
row.original.approval.action !== 'REJECTED'
);
};
const bulkApproveClickHandler = () => {
approveModal.openModal();
};
@@ -309,27 +383,31 @@ const TransferToLayingsTable = () => {
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
try {
await TransferToLayingApi.delete(selectedTransferToLaying?.id as number);
refreshTransferToLayings();
deleteModal.closeModal();
toast.success('Berhasil menghapus data transfer ke laying!');
refreshTransferToLayings();
} catch (error) {
toast.success('Gagal menghapus data transfer ke laying!');
} finally {
deleteModal.closeModal();
setIsDeleteLoading(false);
}
};
const confirmationModalApproveClickHandler = async () => {
const confirmationModalApproveClickHandler = async (notes: string) => {
setIsApproveLoading(true);
const bulkApproveResponse =
await TransferToLayingApi.bulkApprove(selectedRowIds);
const bulkApproveResponse = await TransferToLayingApi.bulkApprove(
selectedRowIds,
notes
);
if (isResponseSuccess(bulkApproveResponse)) {
refreshTransferToLayings();
approveModal.closeModal();
// TODO: remove console.log
console.log('Approved data:', selectedRowIds);
toast.success(
`Berhasil approve ${selectedRowIds.length} data transfer ke laying!`
);
@@ -346,19 +424,18 @@ const TransferToLayingsTable = () => {
setIsApproveLoading(false);
};
const confirmationModalRejectClickHandler = async () => {
const confirmationModalRejectClickHandler = async (notes: string) => {
setIsRejectLoading(true);
const bulkRejectResponse =
await TransferToLayingApi.bulkReject(selectedRowIds);
const bulkRejectResponse = await TransferToLayingApi.bulkReject(
selectedRowIds,
notes
);
if (isResponseSuccess(bulkRejectResponse)) {
refreshTransferToLayings();
rejectModal.closeModal();
// TODO: remove console.log
console.log('Rejected data:', selectedRowIds);
toast.success(
`Berhasil reject ${selectedRowIds.length} data transfer ke laying!`
);
@@ -559,6 +636,7 @@ const TransferToLayingsTable = () => {
setSorting={setSorting}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
enableRowSelection={tableEnableRowSelectionHandler}
className={{
containerClassName: cn({
'mb-20':
@@ -592,7 +670,7 @@ const TransferToLayingsTable = () => {
}}
/>
<ConfirmationModal
<ConfirmationModalWithNotes
ref={approveModal.ref}
type='success'
text={`Apakah anda yakin ingin approve data transfer ke laying ini (${selectedRowIds.length} data)?`}
@@ -607,7 +685,7 @@ const TransferToLayingsTable = () => {
}}
/>
<ConfirmationModal
<ConfirmationModalWithNotes
ref={rejectModal.ref}
type='error'
text={`Apakah anda yakin ingin reject data transfer ke laying ini (${selectedRowIds.length} data)?`}
@@ -1,4 +1,7 @@
import * as Yup from 'yup';
import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
import { formatDate } from '@/lib/helper';
type TransferToLayingFormSchemaType = {
transfer_date?: string;
@@ -14,7 +17,7 @@ type TransferToLayingFormSchemaType = {
totalQuantity?: number;
maxTotalQuantity?: number; // original cap (hidden), helper
kandangs: {
flockSourceKandangs: {
kandang: {
value: number;
label: string;
@@ -22,6 +25,16 @@ type TransferToLayingFormSchemaType = {
quantity: number | string; // editable
maxQuantity?: number; // original cap (hidden), helper
}[];
flockDestinationKandangs: {
kandang: {
value: number;
label: string;
};
quantity: number | string; // editable
maxQuantity?: number; // original cap (hidden), helper
}[];
reason?: string;
};
@@ -51,7 +64,29 @@ export const TransferToLayingFormSchema: Yup.ObjectSchema<TransferToLayingFormSc
.min(1, 'Jumlah transfer minimal 1')
.required('Jumlah transfer wajib diisi!'),
kandangs: Yup.array()
flockSourceKandangs: Yup.array()
.of(
Yup.object({
kandang: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).required('Kandang wajib diisi!'),
quantity: Yup.number()
.min(0, 'Kuantitas minimal 0!')
.max(
Yup.ref('maxQuantity'),
({ max }) => `Kuantitas maksimal ${max}!`
)
.required('Kuantitas wajib diisi!'),
maxQuantity: Yup.number().min(1).required(), // internal helper field
})
)
.min(1, 'Minimal 1 kandang terisi!')
.required('Kandang wajib diisi!'),
flockDestinationKandangs: Yup.array()
.of(
Yup.object({
kandang: Yup.object({
@@ -81,3 +116,122 @@ export const UpdateTransferToLayingFormSchema = TransferToLayingFormSchema;
export type TransferToLayingFormValues = Yup.InferType<
typeof TransferToLayingFormSchema
>;
export const getTransferToLayingFormInitialValues = (
initialValues?: TransferToLaying
): TransferToLayingFormValues => {
return {
transfer_date: initialValues?.transfer_date
? formatDate(initialValues.transfer_date, 'YYYY-MM-DD')
: '',
flockSource: initialValues?.from_project_flock
? {
value: initialValues?.from_project_flock.id,
label: initialValues?.from_project_flock.flock_name,
}
: undefined,
flockDestination: initialValues?.to_project_flock
? {
value: initialValues?.to_project_flock.id,
label: initialValues?.to_project_flock.flock_name,
}
: undefined,
totalQuantity:
initialValues?.usage_qty ?? initialValues?.pending_usage_qty ?? undefined,
flockSourceKandangs: initialValues?.sources
? initialValues.sources.map((sourceKandang) => ({
kandang: {
value: sourceKandang.source_project_flock_kandang.kandang.id,
label: sourceKandang.source_project_flock_kandang.kandang.name,
},
quantity: sourceKandang.qty,
}))
: [],
flockDestinationKandangs: initialValues?.targets
? initialValues.targets.map((targetKandang) => ({
kandang: {
value: targetKandang.target_project_flock_kandang.kandang.id,
label: targetKandang.target_project_flock_kandang.kandang.name,
},
quantity: targetKandang.qty,
}))
: [],
reason: initialValues?.notes ?? undefined,
};
};
export const getFilledTransferToLayingFormInitialValues = async (
initialValues?: TransferToLaying
): Promise<TransferToLayingFormValues> => {
const mappedFlockSourceKandangsAvailableQty =
await TransferToLayingApi.getMappedFlockKandangsAvailability(
initialValues?.from_project_flock.id as number
);
const formattedFlockSourceKandangs = initialValues?.sources
? initialValues.sources.map((sourceKandang) => ({
kandang: {
value: sourceKandang.source_project_flock_kandang.kandang.id,
label: sourceKandang.source_project_flock_kandang.kandang.name,
},
quantity: sourceKandang.qty,
maxQuantity:
(mappedFlockSourceKandangsAvailableQty &&
mappedFlockSourceKandangsAvailableQty[
sourceKandang.source_project_flock_kandang.id
].available_qty) ??
0,
}))
: [];
let maxTotalQuantity = 0;
formattedFlockSourceKandangs.forEach((item) => {
maxTotalQuantity += item.maxQuantity;
});
return {
transfer_date: initialValues?.transfer_date
? formatDate(initialValues.transfer_date, 'YYYY-MM-DD')
: '',
flockSource: initialValues?.from_project_flock
? {
value: initialValues?.from_project_flock.id,
label: initialValues?.from_project_flock.flock_name,
}
: undefined,
flockDestination: initialValues?.to_project_flock
? {
value: initialValues?.to_project_flock.id,
label: initialValues?.to_project_flock.flock_name,
}
: undefined,
totalQuantity:
initialValues?.usage_qty ?? initialValues?.pending_usage_qty ?? undefined,
maxTotalQuantity: maxTotalQuantity,
flockSourceKandangs: formattedFlockSourceKandangs,
flockDestinationKandangs: initialValues?.targets
? initialValues.targets.map((targetKandang) => ({
kandang: {
value: targetKandang.target_project_flock_kandang.kandang.id,
label: targetKandang.target_project_flock_kandang.kandang.name,
},
quantity: targetKandang.qty,
// maxQuantity:
// targetKandang.target_project_flock_kandang.kandang.capacity,
// TODO: integrate this to real API kandang capacity
maxQuantity:
targetKandang.target_project_flock_kandang.kandang.capacity ??
Infinity,
}))
: [],
reason: initialValues?.notes ?? undefined,
};
};
@@ -1,6 +1,6 @@
'use client';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useFormik } from 'formik';
import { toast } from 'react-hot-toast';
@@ -8,16 +8,23 @@ import useSWR from 'swr';
import { Icon } from '@iconify/react';
import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput';
import SelectInput, {
OptionType,
// useSelect,
useSelect,
} from '@/components/input/SelectInput';
import TextArea from '@/components/input/TextArea';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import DateInput from '@/components/input/DateInput';
import NumberInput from '@/components/input/NumberInput';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import ApprovalSteps, {
formatGroupedApprovalsToApprovalSteps,
} from '@/components/pages/ApprovalSteps';
import {
getFilledTransferToLayingFormInitialValues,
getTransferToLayingFormInitialValues,
TransferToLayingFormSchema,
TransferToLayingFormValues,
UpdateTransferToLayingFormSchema,
@@ -31,6 +38,8 @@ import {
import { cn } from '@/lib/helper';
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
import { ProjectFlock } from '@/types/api/production/project-flock';
import { TRANSFER_TO_LAYING_APPROVAL_LINE } from '@/config/approval-line';
interface TransferToLayingFormProps {
type?: 'add' | 'edit' | 'detail';
@@ -55,11 +64,23 @@ const TransferToLayingForm = ({
const [isApproveLoading, setIsApproveLoading] = useState(false);
const [isRejectLoading, setIsRejectLoading] = useState(false);
const { data: approvalHistory, isLoading: isLoadingApprovalHistory } = useSWR(
type === 'detail' && initialValues ? [String(initialValues.id)] : null,
([id]: string[]) => TransferToLayingApi.getApprovalHistory(Number(id))
);
const createTransferToLayingHandler = useCallback(
async (payload: CreateTransferToLayingPayload) => {
console.log('Create transfer to laying:', { payload });
const createTransferToLayingRes =
await TransferToLayingApi.create(payload);
toast.success('Berhasil menambahkan data transfer ke laying!');
if (isResponseError(createTransferToLayingRes)) {
setFormErrorMessage(createTransferToLayingRes.message);
return;
}
toast.success(createTransferToLayingRes?.message as string);
router.push('/production/transfer-to-laying');
},
[router]
);
@@ -69,46 +90,30 @@ const TransferToLayingForm = ({
transferToLayingId: number,
payload: UpdateTransferToLayingPayload
) => {
console.log(
`Update transfer to laying with ID of ${transferToLayingId}:`,
{ payload }
const updateKandangRes = await TransferToLayingApi.update(
transferToLayingId,
payload
);
toast.success('Berhasil mengubah data transfer ke laying!');
if (updateKandangRes?.status === 'error') {
setFormErrorMessage(updateKandangRes.message);
return;
}
toast.success(updateKandangRes?.message as string);
router.refresh();
router.push('/production/transfer-to-laying');
},
[router]
);
const formikInitialValues = useMemo<TransferToLayingFormValues>(() => {
return {
transfer_date: initialValues?.transfer_date ?? '',
flockSource: initialValues?.flock_source
? {
value: initialValues?.flock_source.id,
label: initialValues?.flock_source.name,
}
: undefined,
flockDestination: initialValues?.flock_destination
? {
value: initialValues?.flock_destination.id,
label: initialValues?.flock_destination.name,
}
: undefined,
totalQuantity: initialValues?.quantity ?? undefined,
// const formikInitialValues = useMemo<TransferToLayingFormValues>(() => {
// return getTransferToLayingFormInitialValues(initialValues);
// }, [initialValues]);
kandangs: initialValues?.kandangs
? initialValues.kandangs.map((kandang) => ({
kandang: {
value: kandang.kandang.id,
label: kandang.kandang.name,
},
quantity: kandang.quantity,
}))
: [],
reason: initialValues?.reason ?? undefined,
};
}, [initialValues]);
const [formikInitialValues, setFormikInitialValues] = useState(
getTransferToLayingFormInitialValues()
);
const formik = useFormik<TransferToLayingFormValues>({
initialValues: formikInitialValues,
@@ -117,23 +122,23 @@ const TransferToLayingForm = ({
? UpdateTransferToLayingFormSchema
: TransferToLayingFormSchema,
onSubmit: async (values) => {
console.log({ values });
setFormErrorMessage('');
const transferToLayingPayload: CreateTransferToLayingPayload = {
transfer_date: values.transfer_date as string,
flock_source_id: values.flockSource?.value as number,
flock_destination_id: values.flockDestination?.value as number,
source_project_flock_id: values.flockSource?.value as number,
target_project_flock_id: values.flockDestination?.value as number,
totalQuantity: values.totalQuantity as number,
kandangs: values.kandangs?.map((kandang) => ({
kandang_id: kandang.kandang.value,
quantity: kandang.quantity,
})) as {
kandang_id: number;
quantity: number;
}[],
source_kandangs: values.flockSourceKandangs?.map((kandang) => ({
project_flock_kandang_id: kandang.kandang.value,
quantity: parseFloat(kandang.quantity as string),
})) as CreateTransferToLayingPayload['source_kandangs'],
target_kandangs: values.flockDestinationKandangs?.map((kandang) => ({
project_flock_kandang_id: kandang.kandang.value,
quantity: parseFloat(kandang.quantity as string),
})) as CreateTransferToLayingPayload['target_kandangs'],
reason: values.reason as string,
};
@@ -154,7 +159,11 @@ const TransferToLayingForm = ({
});
const { setValues: formikSetValues, values: formikValues } = formik;
const { kandangs: kandangsValue } = formikValues;
const {
flockSourceKandangs: flockSourceKandangsValue,
flockDestinationKandangs: flockDestinationKandangsValue,
totalQuantity,
} = formikValues;
const deleteTransferToLayingClickHandler = () => {
deleteModal.openModal();
@@ -172,24 +181,32 @@ const TransferToLayingForm = ({
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
// TODO: delete data and integrate to real API
deleteModal.closeModal();
toast.success('Berhasil menghapus data transfer ke laying!');
try {
await TransferToLayingApi.delete(initialValues?.id as number);
toast.success('Berhasil menghapus data transfer ke laying!');
router.push('/production/transfer-to-laying');
} catch (error) {
toast.success('Gagal menghapus data transfer ke laying!');
} finally {
deleteModal.closeModal();
setIsDeleteLoading(false);
}
};
const confirmationModalApproveClickHandler = async () => {
const confirmationModalApproveClickHandler = async (notes: string) => {
setIsApproveLoading(true);
const approveResponse = await TransferToLayingApi.approve(
initialValues?.id as number
initialValues?.id as number,
notes
);
if (isResponseSuccess(approveResponse)) {
approveModal.closeModal();
toast.success('Berhasil approve data transfer ke laying!');
router.push('/production/transfer-to-laying');
} else {
approveModal.closeModal();
@@ -199,17 +216,19 @@ const TransferToLayingForm = ({
setIsApproveLoading(false);
};
const confirmationModalRejectClickHandler = async () => {
const confirmationModalRejectClickHandler = async (notes: string) => {
setIsRejectLoading(true);
const rejectResponse = await TransferToLayingApi.reject(
initialValues?.id as number
initialValues?.id as number,
notes
);
if (isResponseSuccess(rejectResponse)) {
rejectModal.closeModal();
toast.success('Berhasil reject data transfer ke laying!');
router.push('/production/transfer-to-laying');
} else {
rejectModal.closeModal();
@@ -219,49 +238,47 @@ const TransferToLayingForm = ({
setIsRejectLoading(false);
};
const isRepeaterInputError = (
column: keyof TransferToLayingFormValues['kandangs'][0],
// flock source
const isFlockSourceKandangsRepeaterInputError = (
column: keyof TransferToLayingFormValues['flockSourceKandangs'][0],
idx: number
) => {
return (
formik.touched.kandangs?.[idx]?.[column] &&
formik.touched.flockSourceKandangs?.[idx]?.[column] &&
Boolean(
formik.errors.kandangs?.[idx] instanceof Object &&
formik.errors.kandangs?.[idx]?.[column]
formik.errors.flockSourceKandangs?.[idx] instanceof Object &&
formik.errors.flockSourceKandangs?.[idx]?.[column]
)
);
};
const repeaterInputErrorMessage = (
column: keyof TransferToLayingFormValues['kandangs'][0],
const flockSourceKandangsRepeaterInputErrorMessage = (
column: keyof TransferToLayingFormValues['flockSourceKandangs'][0],
idx: number
) => {
return (formik.errors.kandangs?.[idx] as Record<string, string>)?.[column];
return (
formik.errors.flockSourceKandangs?.[idx] as Record<string, string>
)?.[column];
};
// TODO: remove dummy data and use real data
// Flock Source
// const {
// inputValue: flockSourceInputValue,
// setInputValue: setFlockSourceInputValue,
// options: flockSourceOptions,
// isLoadingOptions: isLoadingFlockSourceOptions,
// } = useSelect<FlockWithKandangs>('/transfer-to-laying/production/get-flock-source', 'id', 'name');
// TODO: remove this dummy data
const { data: flockSources, isLoading: isLoadingFlockSourceOptions } = useSWR(
'test',
() => TransferToLayingApi.getFlockSource()
const {
setInputValue: setFlockSourceInputValue,
options: flockSourceOptions,
isLoadingOptions: isLoadingFlockSourceOptions,
rawData: flockSources,
} = useSelect<ProjectFlock>(
'/production/project-flocks',
'id',
'flock_name',
'search',
{
category: 'GROWING',
}
);
const flockSourceOptions = isResponseSuccess(flockSources)
? flockSources?.data.map((flockSource) => ({
value: flockSource.id,
label: flockSource.name,
}))
: [];
const flockSourceChangeHandler = (val: OptionType | OptionType[] | null) => {
const flockSourceChangeHandler = async (
val: OptionType | OptionType[] | null
) => {
// Get flock source data for total quantity and kandang
const flockSource =
isResponseSuccess(flockSources) && val !== null
@@ -272,21 +289,38 @@ const TransferToLayingForm = ({
// Set total quantity and kandangs
if (flockSource) {
const mappedFlockKandangsAvailableQty =
await TransferToLayingApi.getMappedFlockKandangsAvailability(
flockSource.id
);
const formattedKandangs = flockSource.kandangs.map((item) => ({
kandang: {
value: item.kandang.id,
label: item.kandang.name,
value: item.project_flock_kandang_id,
label: item.name,
},
quantity: '',
maxQuantity: item.quantity,
maxQuantity:
(mappedFlockKandangsAvailableQty &&
mappedFlockKandangsAvailableQty[item.project_flock_kandang_id]
.available_qty) ??
0,
}));
formik.setFieldValue('totalQuantity', flockSource.totalQuantity);
formik.setFieldValue('maxTotalQuantity', flockSource.totalQuantity);
formik.setFieldValue('kandangs', formattedKandangs);
let maxTotalQuantity = 0;
// flockSource.kandangs.forEach((item) => {
// maxTotalQuantity += item.capacity;
// });
formattedKandangs.forEach((item) => {
maxTotalQuantity += item.maxQuantity;
});
formik.setFieldValue('totalQuantity', '');
formik.setFieldValue('maxTotalQuantity', maxTotalQuantity);
formik.setFieldValue('flockSourceKandangs', formattedKandangs);
} else {
formik.setFieldValue('totalQuantity', undefined);
formik.setFieldValue('kandangs', undefined);
formik.setFieldValue('flockSourceKandangs', undefined);
formik.setFieldValue('reason', '');
}
@@ -294,52 +328,137 @@ const TransferToLayingForm = ({
formik.setFieldValue('flockSource', val);
};
// TODO: remove dummy data and use real data
// Flock Destination
// const {
// inputValue: flockDestinationInputValue,
// setInputValue: setFlockDestinationInputValue,
// options: flockDestinationOptions,
// isLoadingOptions: isLoadingFlockDestinationOptions,
// } = useSelect<FlockWithKandangs>('/transfer-to-laying/production/get-flock-destination', 'id', 'name');
// flock destination
const isFlockDestinationKandangsRepeaterInputError = (
column: keyof TransferToLayingFormValues['flockDestinationKandangs'][0],
idx: number
) => {
return (
formik.touched.flockDestinationKandangs?.[idx]?.[column] &&
Boolean(
formik.errors.flockDestinationKandangs?.[idx] instanceof Object &&
formik.errors.flockDestinationKandangs?.[idx]?.[column]
)
);
};
const flockDestinationKandangsRepeaterInputErrorMessage = (
column: keyof TransferToLayingFormValues['flockDestinationKandangs'][0],
idx: number
) => {
return (
formik.errors.flockDestinationKandangs?.[idx] as Record<string, string>
)?.[column];
};
// TODO: remove this dummy data
const {
data: flockDestinations,
isLoading: isLoadingFlockDestinationOptions,
} = useSWR('test', () => TransferToLayingApi.getFlockSource());
const flockDestinationOptions = isResponseSuccess(flockDestinations)
? flockDestinations?.data.map((flockDestination) => ({
value: flockDestination.id,
label: flockDestination.name,
}))
: [];
setInputValue: setFlockDestinationInputValue,
options: flockDestinationOptions,
isLoadingOptions: isLoadingFlockDestinationOptions,
rawData: flockDestinations,
} = useSelect<ProjectFlock>(
'/production/project-flocks',
'id',
'flock_name',
'search',
{
category: 'LAYING',
}
);
const flockDestinationChangeHandler = (
val: OptionType | OptionType[] | null
) => {
// Get flock destination data for total quantity and kandang
const flockDestination =
isResponseSuccess(flockDestinations) && val !== null
? flockDestinations.data.find(
(item) => item.id === (val as OptionType).value
)
: undefined;
// Set total quantity and kandangs
if (flockDestination) {
const formattedKandangs = flockDestination.kandangs.map((item) => ({
kandang: {
value: item.project_flock_kandang_id,
label: item.name,
},
quantity: '',
// TODO: integrate this later to real kandang capacity API
// maxQuantity: item.capacity ?? 0,
maxQuantity: item.capacity ?? Infinity,
}));
formik.setFieldValue('flockDestinationKandangs', formattedKandangs);
}
formik.setFieldTouched('flockDestination', true);
formik.setFieldValue('flockDestination', val);
};
const isShowApproveRejectButton =
initialValues &&
initialValues?.approval?.step_number === 1 &&
initialValues?.approval.action !== 'REJECTED';
const isShowDeleteButton =
initialValues &&
initialValues?.approval.action !== 'REJECTED' &&
initialValues?.approval.action !== 'APPROVED';
const isShowEditButton = isShowDeleteButton;
useEffect(() => {
const getFilledInitialValues = async () => {
if (initialValues) {
const filledInitialValues =
await getFilledTransferToLayingFormInitialValues(initialValues);
setFormikInitialValues(filledInitialValues);
}
};
getFilledInitialValues();
}, [initialValues, setFormikInitialValues]);
useEffect(() => {
formikSetValues(formikInitialValues);
}, [formikSetValues, formikInitialValues]);
useEffect(() => {
// calculate total quantity if kandangs quantity change
if (kandangsValue && kandangsValue.length > 0) {
if (flockSourceKandangsValue && flockSourceKandangsValue.length > 0) {
let newTotalQuantity = 0;
kandangsValue.forEach((item) => {
newTotalQuantity += item.quantity as number;
flockSourceKandangsValue.forEach((item) => {
newTotalQuantity += parseFloat(item.quantity as string);
});
formik.setFieldValue('totalQuantity', newTotalQuantity);
formik.validateField('totalQuantity');
}
}, [formikSetValues, kandangsValue]);
}, [formikSetValues, flockSourceKandangsValue]);
useEffect(() => {
// calculate total quantity if kandangs quantity change
if (
flockDestinationKandangsValue &&
flockDestinationKandangsValue.length > 0
) {
let destinationKandangsTotalQuantity = 0;
flockDestinationKandangsValue.forEach((item) => {
destinationKandangsTotalQuantity += parseFloat(item.quantity as string);
});
if (
destinationKandangsTotalQuantity > parseFloat(String(totalQuantity))
) {
}
}
}, [formikSetValues, flockDestinationKandangsValue]);
return (
<>
@@ -361,17 +480,38 @@ const TransferToLayingForm = ({
</h1>
</header>
<div className='w-full my-4 flex flex-row justify-end gap-2'>
{type === 'detail' &&
initialValues &&
!isLoadingApprovalHistory &&
isResponseSuccess(approvalHistory) && (
<div className='w-full my-4'>
<ApprovalSteps
approvals={formatGroupedApprovalsToApprovalSteps(
TRANSFER_TO_LAYING_APPROVAL_LINE,
approvalHistory.data,
initialValues.approval
)}
/>
</div>
)}
<div className='w-full my-4 flex flex-row justify-between gap-2'>
{type === 'detail' && (
<>
{isShowApproveRejectButton && (
<div className='w-full flex flex-row justify-end gap-2'>
{/* TODO: apply RBAC */}
<Button
variant='outline'
color='success'
onClick={approveClickHandler}
// disabled={selectedRowIds.length === 0}
className='w-full sm:w-fit'
>
<Icon icon='material-symbols:check' width={24} height={24} />
<Icon
icon='material-symbols:check'
width={24}
height={24}
/>
Approve
</Button>
@@ -379,12 +519,17 @@ const TransferToLayingForm = ({
variant='outline'
color='error'
onClick={rejectClickHandler}
// disabled={selectedRowIds.length === 0}
className='w-full sm:w-fit'
>
<Icon icon='material-symbols:close' width={24} height={24} />
<Icon
icon='material-symbols:close'
width={24}
height={24}
/>
Reject
</Button>
</div>
)}
</>
)}
</div>
@@ -395,13 +540,12 @@ const TransferToLayingForm = ({
className='w-full flex flex-col gap-6'
>
<div className='flex flex-col gap-4'>
<TextInput
<DateInput
required
type='date'
label='Tanggal Transfer'
name='transfer_date'
placeholder='Masukkan tanggal transfer'
value={formik.values.transfer_date}
value={formik.values.transfer_date ?? ''}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={
@@ -421,7 +565,7 @@ const TransferToLayingForm = ({
options={flockSourceOptions}
onChange={flockSourceChangeHandler}
isLoading={isLoadingFlockSourceOptions}
// onInputChange={setFlockSourceInputValue}
onInputChange={setFlockSourceInputValue}
isError={
formik.touched.flockSource &&
Boolean(typeof formik.errors.flockSource === 'string')
@@ -439,7 +583,7 @@ const TransferToLayingForm = ({
options={flockDestinationOptions}
onChange={flockDestinationChangeHandler}
isLoading={isLoadingFlockDestinationOptions}
// onInputChange={setFlockDestinationInputValue}
onInputChange={setFlockDestinationInputValue}
isError={
formik.touched.flockDestination &&
Boolean(typeof formik.errors.flockDestination === 'string')
@@ -450,9 +594,8 @@ const TransferToLayingForm = ({
/>
</div>
<TextInput
<NumberInput
required
type='number'
name='totalQuantity'
label='Jumlah Transfer'
bottomLabel={
@@ -461,7 +604,9 @@ const TransferToLayingForm = ({
: undefined
}
placeholder='Masukkan jumlah transfer'
value={formik.values.totalQuantity ?? ''}
value={
formik.values.totalQuantity ? formik.values.totalQuantity : ''
}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={
@@ -469,24 +614,22 @@ const TransferToLayingForm = ({
Boolean(formik.errors.totalQuantity)
}
errorMessage={formik.errors.totalQuantity}
// readOnly={type === 'detail'}
// disabled={Boolean(formik.errors.flockSource)}
disabled
/>
<div>
<div className='flex flex-col gap-4'>
<div className='overflow-x-auto'>
<table className='table'>
<thead>
<tr>
<th>Kandang</th>
<th>Kandang Flock Asal</th>
<th>Kuantitas</th>
</tr>
</thead>
<tbody>
{(!formik.values.kandangs ||
formik.values.kandangs.length === 0) && (
{(!formik.values.flockSourceKandangs ||
formik.values.flockSourceKandangs.length === 0) && (
<tr>
<td colSpan={2}>
<p className='w-full text-center text-gray-400'>
@@ -496,8 +639,8 @@ const TransferToLayingForm = ({
</tr>
)}
{formik.values.kandangs &&
formik.values.kandangs.map((kandang, idx) => (
{formik.values.flockSourceKandangs &&
formik.values.flockSourceKandangs.map((kandang, idx) => (
<tr key={idx}>
<td>
<SelectInput
@@ -511,10 +654,9 @@ const TransferToLayingForm = ({
</td>
<td>
<TextInput
<NumberInput
required
type='number'
name={`kandangs[${idx}].quantity`}
name={`flockSourceKandangs[${idx}].quantity`}
bottomLabel={
kandang.maxQuantity
? `Max: ${kandang.maxQuantity}`
@@ -524,8 +666,11 @@ const TransferToLayingForm = ({
value={kandang.quantity}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={isRepeaterInputError('quantity', idx)}
errorMessage={repeaterInputErrorMessage(
isError={isFlockSourceKandangsRepeaterInputError(
'quantity',
idx
)}
errorMessage={flockSourceKandangsRepeaterInputErrorMessage(
'quantity',
idx
)}
@@ -540,6 +685,76 @@ const TransferToLayingForm = ({
</tbody>
</table>
</div>
<div className='overflow-x-auto'>
<table className='table'>
<thead>
<tr>
<th>Kandang Flock Tujuan</th>
<th>Kuantitas</th>
</tr>
</thead>
<tbody>
{(!formik.values.flockDestinationKandangs ||
formik.values.flockDestinationKandangs.length === 0) && (
<tr>
<td colSpan={2}>
<p className='w-full text-center text-gray-400'>
Pilih flock tujuan terlebih dahulu!
</p>
</td>
</tr>
)}
{formik.values.flockDestinationKandangs &&
formik.values.flockDestinationKandangs.map(
(kandang, idx) => (
<tr key={idx}>
<td>
<SelectInput
value={kandang.kandang}
options={[]}
isDisabled
className={{
wrapper: 'min-w-52',
}}
/>
</td>
<td>
<NumberInput
required
name={`flockDestinationKandangs[${idx}].quantity`}
bottomLabel={
kandang.maxQuantity
? `Max: ${kandang.maxQuantity}`
: undefined
}
placeholder='Masukkan kuantitas'
value={kandang.quantity}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={isFlockDestinationKandangsRepeaterInputError(
'quantity',
idx
)}
errorMessage={flockDestinationKandangsRepeaterInputErrorMessage(
'quantity',
idx
)}
readOnly={type === 'detail'}
className={{
wrapper: 'min-w-52',
}}
/>
</td>
</tr>
)
)}
</tbody>
</table>
</div>
</div>
<TextArea
@@ -558,9 +773,21 @@ const TransferToLayingForm = ({
/>
</div>
{formErrorMessage && (
<div role='alert' className='alert alert-error w-full'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{formErrorMessage}</span>
</div>
)}
<div className='flex flex-row justify-between gap-2 flex-wrap'>
{type !== 'add' && (
<div className='flex flex-row justify-start gap-2'>
{isShowDeleteButton && (
<Button
type='button'
color='error'
@@ -575,8 +802,9 @@ const TransferToLayingForm = ({
/>
Delete
</Button>
)}
{type !== 'edit' && (
{type !== 'edit' && isShowEditButton && (
<Button
type='button'
color='warning'
@@ -617,17 +845,6 @@ const TransferToLayingForm = ({
</div>
)}
</div>
{formErrorMessage && (
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{formErrorMessage}</span>
</div>
)}
</form>
</section>
@@ -650,7 +867,7 @@ const TransferToLayingForm = ({
{type === 'detail' && (
<>
<ConfirmationModal
<ConfirmationModalWithNotes
ref={approveModal.ref}
type='success'
text='Apakah anda yakin ingin approve data transfer ke laying ini?'
@@ -665,7 +882,7 @@ const TransferToLayingForm = ({
}}
/>
<ConfirmationModal
<ConfirmationModalWithNotes
ref={rejectModal.ref}
type='error'
text='Apakah anda yakin ingin reject data transfer ke laying ini?'
+11
View File
@@ -21,3 +21,14 @@ export const PROJECT_FLOCK_KANDANG_APPROVAL_LINE: ApprovalLine = [
step_name: 'Disetujui',
},
] as const;
export const TRANSFER_TO_LAYING_APPROVAL_LINE: ApprovalLine = [
{
step_number: 1,
step_name: 'Pengajuan',
},
{
step_number: 2,
step_name: 'Disetujui',
},
] as const;
File diff suppressed because it is too large Load Diff
-12
View File
@@ -12,15 +12,3 @@ export type CreateFlockPayload = {
};
export type UpdateFlockPayload = CreateFlockPayload;
// ---------------------------------------
// TODO: adjust this later after Transfer to Laying API done
import { BaseKandang } from '@/types/api/master-data/kandang';
export type FlockWithKandangs = BaseFlock & {
totalQuantity: number;
kandangs: {
kandang: BaseKandang;
quantity: number;
}[];
};
+1
View File
@@ -10,6 +10,7 @@ export type BaseKandang = {
capacity: number;
pic: BaseUser;
project_flock_kandang_id?: number;
capacity: number;
};
export type Kandang = BaseMetadata & BaseKandang;
+14 -1
View File
@@ -8,6 +8,7 @@ import { BaseApproval, BaseMetadata } from '@/types/api/api-general';
export type BaseProjectFlock = {
id: number;
name: string;
flock_name: string;
status: string;
flock?: Flock;
flock_i?: number;
@@ -21,7 +22,9 @@ export type BaseProjectFlock = {
location_id: number;
period: number;
kandang_ids: number[];
kandangs: Kandang[];
kandangs: (Kandang & {
project_flock_kandang_id: number;
})[];
approval: BaseApproval;
};
@@ -48,3 +51,13 @@ export type ProjectFlockApprovalPayload = {
action: 'APPROVED' | 'REJECTED';
approvable_ids: number[];
};
export type ProjectFlockAvailableQuantity = {
project_flock_id: number;
flock_name: string;
category: 'LAYING' | 'GROWING';
kandangs: {
project_flock_kandang_id: number;
available_qty: number;
}[];
};
+67 -13
View File
@@ -1,34 +1,88 @@
import { BaseApiResponse, BaseMetadata, flags } from '@/types/api/api-general';
import { Kandang } from '@/types/api/master-data/kandang';
import {
BaseApiResponse,
BaseMetadata,
CreatedUser,
flags,
} from '@/types/api/api-general';
import { BaseKandang, Kandang } from '@/types/api/master-data/kandang';
import { WarehouseType } from '@/types/api/master-data/warehouse';
export type BaseTransferToLaying = {
id: number;
transfer_number: string;
transfer_date: string;
flock_source: {
notes: string;
from_project_flock: {
id: number;
flock_name: string;
category: 'GROWING' | 'LAYING';
};
to_project_flock: {
id: number;
flock_name: string;
category: 'GROWING' | 'LAYING';
};
pending_usage_qty: number | null;
usage_qty: number | null;
sources: {
source_project_flock_kandang: {
id: number;
kandang: Omit<BaseKandang, 'status' | 'location' | 'pic'>;
};
qty: number;
product_warehouse: {
product: {
id: number;
name: string;
};
flock_destination: {
warehouse: {
id: number;
name: string;
type: WarehouseType;
};
};
quantity: number;
kandangs: {
kandang: Kandang;
quantity: number;
}[];
reason: string;
targets: {
target_project_flock_kandang: {
id: number;
kandang: Omit<BaseKandang, 'status' | 'location' | 'pic'>;
};
qty: number;
product_warehouse: {
product: {
id: number;
name: string;
};
warehouse: {
id: number;
name: string;
type: WarehouseType;
};
};
}[];
created_by: number;
created_user: CreatedUser;
created_at: string;
approval: BaseApproval;
};
export type TransferToLaying = BaseMetadata & BaseTransferToLaying;
export type CreateTransferToLayingPayload = {
transfer_date: string;
flock_source_id: number;
flock_destination_id: number;
source_project_flock_id: number;
target_project_flock_id: number;
totalQuantity: number;
kandangs: {
kandang_id: number;
source_kandangs: {
project_flock_kandang_id: number;
quantity: number;
}[];
target_kandangs: {
project_flock_kandang_id: number;
quantity: number;
}[];
reason: string;