mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 05:22:02 +00:00
fix(FE): resolve merge conflict
This commit is contained in:
+139
-69
@@ -1,76 +1,146 @@
|
|||||||
stages: [notify]
|
stages:
|
||||||
|
- build
|
||||||
|
- deploy
|
||||||
|
|
||||||
# --- Notify when MR is opened/updated ---
|
.build_template: &build_template
|
||||||
notify_discord_mr:
|
stage: build
|
||||||
stage: notify
|
image: node:20-alpine
|
||||||
image: alpine:3.20
|
cache:
|
||||||
rules:
|
key: npm-cache
|
||||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
paths:
|
||||||
|
- node_modules/
|
||||||
variables:
|
variables:
|
||||||
WEBHOOK_URL: $DISCORD_WEBHOOK_URL
|
NPM_CONFIG_PRODUCTION: 'false'
|
||||||
before_script:
|
NODE_ENV: ''
|
||||||
- apk add --no-cache curl jq
|
script:
|
||||||
script: |
|
- echo "Installing dependencies..."
|
||||||
MR_URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}"
|
- 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
|
||||||
|
|
||||||
jq -n \
|
.deploy_template: &deploy_template
|
||||||
--arg repo "$CI_PROJECT_PATH" \
|
stage: deploy
|
||||||
--arg mr "#${CI_MERGE_REQUEST_IID}" \
|
image:
|
||||||
--arg url "$MR_URL" \
|
name: amazon/aws-cli:latest
|
||||||
--arg requestor "${GITLAB_USER_LOGIN:-$GITLAB_USER_NAME}" \
|
entrypoint: ['/bin/sh', '-c']
|
||||||
--arg source "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" \
|
script:
|
||||||
--arg target "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" \
|
- set -e
|
||||||
--arg title "$CI_MERGE_REQUEST_TITLE" \
|
- aws --version
|
||||||
'{
|
- echo "Cleaning up newline characters in AWS credentials..."
|
||||||
username: "CI Bot - FE",
|
- export AWS_ACCESS_KEY_ID=$(echo $AWS_ACCESS_KEY_ID | tr -d '\r\n')
|
||||||
embeds: [{
|
- export AWS_SECRET_ACCESS_KEY=$(echo $AWS_SECRET_ACCESS_KEY | tr -d '\r\n')
|
||||||
title: "📣 [LTI WEB CLIENT] Merge Request Opened/Updated",
|
- echo "Deploying to s3://$S3_BUCKET in region $AWS_REGION"
|
||||||
description: ($mr + " in " + $repo),
|
- 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"
|
||||||
url: $url,
|
- aws s3 sync ./out "s3://$S3_BUCKET" --delete --region "$AWS_REGION" --endpoint-url "https://s3.ap-southeast-3.amazonaws.com"
|
||||||
color: 3447003,
|
|
||||||
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"
|
|
||||||
|
|
||||||
# --- Notify when MR is merged ---
|
# CloudFront invalidation
|
||||||
notify_discord_merge:
|
- |
|
||||||
stage: notify
|
STATUS="success"
|
||||||
image: alpine:3.20
|
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 actor "$GITLAB_USER_LOGIN" \
|
||||||
|
--arg commit "$CI_COMMIT_SHA" \
|
||||||
|
--arg run_url "$RUN_URL" \
|
||||||
|
'{
|
||||||
|
username: "CI Bot - LTI WEB",
|
||||||
|
embeds: [{
|
||||||
|
title: $title,
|
||||||
|
description: $desc,
|
||||||
|
color: ($color|tonumber),
|
||||||
|
fields: [
|
||||||
|
{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}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}' > payload.json
|
||||||
|
|
||||||
|
curl -sS -H "Content-Type: application/json" -d @payload.json "$DISCORD_WEBHOOK_URL"
|
||||||
|
|
||||||
|
# ====== DEVELOPMENT (Branch development) ======
|
||||||
|
build:dev:
|
||||||
|
<<: *build_template
|
||||||
rules:
|
rules:
|
||||||
# Only run for merge request pipelines that are in merged state
|
- if: '$CI_COMMIT_BRANCH == "development"'
|
||||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_STATE == "merged"'
|
environment:
|
||||||
|
name: development
|
||||||
variables:
|
variables:
|
||||||
WEBHOOK_URL: $DISCORD_WEBHOOK_URL
|
NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id'
|
||||||
before_script:
|
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-api-sso.mbugroup.id'
|
||||||
- apk add --no-cache curl jq
|
|
||||||
script: |
|
deploy:dev:
|
||||||
MR_URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}"
|
<<: *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,2 +1,3 @@
|
|||||||
|
npm run format
|
||||||
npm run lint
|
npm run lint
|
||||||
npm run build
|
npm run build
|
||||||
|
|||||||
+25
@@ -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"]
|
||||||
@@ -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 { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
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 TransferToLayingEdit = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -116,33 +29,33 @@ const TransferToLayingEdit = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: remove dummy data and integrate with real API
|
|
||||||
if (
|
if (
|
||||||
!isLoadingTransferToLaying &&
|
!isLoadingTransferToLaying &&
|
||||||
(!transferToLaying ||
|
(!transferToLaying || isResponseError(transferToLaying))
|
||||||
(isResponseError(transferToLaying) && !DUMMY_TRANSFER_TO_LAYING_EDIT))
|
|
||||||
) {
|
) {
|
||||||
router.replace('/404');
|
router.replace('/404');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isResponseSuccess(transferToLaying) &&
|
||||||
|
transferToLaying.data.approval.step_number === 2
|
||||||
|
) {
|
||||||
|
router.replace('/production/transfer-to-laying');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full p-4 flex flex-row justify-center'>
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
{isLoadingTransferToLaying && (
|
{isLoadingTransferToLaying && (
|
||||||
<span className='loading loading-spinner loading-xl' />
|
<span className='loading loading-spinner loading-xl' />
|
||||||
)}
|
)}
|
||||||
{/* {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
|
{!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
|
||||||
<TransferToLayingForm
|
<TransferToLayingForm
|
||||||
type='detail'
|
type='edit'
|
||||||
initialValues={transferToLaying.data}
|
initialValues={transferToLaying.data}
|
||||||
/>
|
/>
|
||||||
)} */}
|
)}
|
||||||
|
|
||||||
{/* TODO: remove this dummy data and integrate to real API */}
|
|
||||||
<TransferToLayingForm
|
|
||||||
type='edit'
|
|
||||||
initialValues={DUMMY_TRANSFER_TO_LAYING_EDIT}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,93 +8,6 @@ import TransferToLayingForm from '@/components/pages/production/transfer-to-layi
|
|||||||
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
|
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
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 TransferToLayingDetail = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -116,11 +29,9 @@ const TransferToLayingDetail = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: remove dummy data and integrate with real API
|
|
||||||
if (
|
if (
|
||||||
!isLoadingTransferToLaying &&
|
!isLoadingTransferToLaying &&
|
||||||
(!transferToLaying ||
|
(!transferToLaying || isResponseError(transferToLaying))
|
||||||
(isResponseError(transferToLaying) && !DUMMY_TRANSFER_TO_LAYING_DETAIL))
|
|
||||||
) {
|
) {
|
||||||
router.replace('/404');
|
router.replace('/404');
|
||||||
return;
|
return;
|
||||||
@@ -131,18 +42,13 @@ const TransferToLayingDetail = () => {
|
|||||||
{isLoadingTransferToLaying && (
|
{isLoadingTransferToLaying && (
|
||||||
<span className='loading loading-spinner loading-xl' />
|
<span className='loading loading-spinner loading-xl' />
|
||||||
)}
|
)}
|
||||||
{/* {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
|
|
||||||
|
{!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
|
||||||
<TransferToLayingForm
|
<TransferToLayingForm
|
||||||
type='detail'
|
type='detail'
|
||||||
initialValues={transferToLaying.data}
|
initialValues={transferToLaying.data}
|
||||||
/>
|
/>
|
||||||
)} */}
|
)}
|
||||||
|
|
||||||
{/* TODO: remove this dummy data and integrate to real API */}
|
|
||||||
<TransferToLayingForm
|
|
||||||
type='detail'
|
|
||||||
initialValues={DUMMY_TRANSFER_TO_LAYING_DETAIL}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
FilterFn,
|
FilterFn,
|
||||||
SortingState,
|
SortingState,
|
||||||
OnChangeFn,
|
OnChangeFn,
|
||||||
|
Row,
|
||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
import { rankItem } from '@tanstack/match-sorter-utils';
|
import { rankItem } from '@tanstack/match-sorter-utils';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
@@ -50,6 +51,7 @@ export interface TableProps<TData extends object> {
|
|||||||
manualSorting?: boolean;
|
manualSorting?: boolean;
|
||||||
rowSelection?: Record<string, boolean>;
|
rowSelection?: Record<string, boolean>;
|
||||||
setRowSelection?: OnChangeFn<Record<string, boolean>>;
|
setRowSelection?: OnChangeFn<Record<string, boolean>>;
|
||||||
|
enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
|
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
|
||||||
@@ -90,6 +92,7 @@ const Table = <TData extends object>({
|
|||||||
manualSorting = false,
|
manualSorting = false,
|
||||||
rowSelection,
|
rowSelection,
|
||||||
setRowSelection,
|
setRowSelection,
|
||||||
|
enableRowSelection,
|
||||||
}: TableProps<TData>) => {
|
}: TableProps<TData>) => {
|
||||||
const isServerSideTable =
|
const isServerSideTable =
|
||||||
totalItems !== undefined &&
|
totalItems !== undefined &&
|
||||||
@@ -150,6 +153,10 @@ const Table = <TData extends object>({
|
|||||||
tableOptions.getRowId = (row) => (row as { id: string }).id;
|
tableOptions.getRowId = (row) => (row as { id: string }).id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (enableRowSelection !== undefined) {
|
||||||
|
tableOptions.enableRowSelection = enableRowSelection;
|
||||||
|
}
|
||||||
|
|
||||||
const table = useReactTable(tableOptions);
|
const table = useReactTable(tableOptions);
|
||||||
const { setPageSize } = table;
|
const { setPageSize } = table;
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { cn, formatDate } from '@/lib/helper';
|
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 { DateRange, DayPicker, Matcher } from 'react-day-picker';
|
||||||
import 'react-day-picker/dist/style.css';
|
import 'react-day-picker/dist/style.css';
|
||||||
import Button from '../Button';
|
import Button from '@/components/Button';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
export interface DateInputProps {
|
export interface DateInputProps {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import Button from '@/components/Button';
|
|||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { Color } from '@/types/theme';
|
import { Color } from '@/types/theme';
|
||||||
|
|
||||||
interface ConfirmationModalProps {
|
export interface ConfirmationModalProps {
|
||||||
ref: RefObject<HTMLDialogElement | null>;
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
type?: 'info' | 'success' | 'error';
|
type?: 'info' | 'success' | 'error';
|
||||||
text?: string;
|
text?: string;
|
||||||
@@ -30,6 +30,7 @@ interface ConfirmationModalProps {
|
|||||||
modal?: string;
|
modal?: string;
|
||||||
modalBox?: string;
|
modalBox?: string;
|
||||||
};
|
};
|
||||||
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConfirmationModal = ({
|
const ConfirmationModal = ({
|
||||||
@@ -40,6 +41,7 @@ const ConfirmationModal = ({
|
|||||||
primaryButton,
|
primaryButton,
|
||||||
secondaryButton,
|
secondaryButton,
|
||||||
className,
|
className,
|
||||||
|
children,
|
||||||
}: ConfirmationModalProps) => {
|
}: ConfirmationModalProps) => {
|
||||||
const closeModalHandler = () => {
|
const closeModalHandler = () => {
|
||||||
ref.current?.close();
|
ref.current?.close();
|
||||||
@@ -90,6 +92,8 @@ const ConfirmationModal = ({
|
|||||||
{text ?? 'Apakah anda yakin ingin melakukan hal ini?'}
|
{text ?? 'Apakah anda yakin ingin melakukan hal ini?'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{children && <div className='w-full'>{children}</div>}
|
||||||
|
|
||||||
<div className='w-full flex flex-row gap-2'>
|
<div className='w-full flex flex-row gap-2'>
|
||||||
{secondaryButton && secondaryButton.text && (
|
{secondaryButton && secondaryButton.text && (
|
||||||
<Button
|
<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;
|
||||||
@@ -138,10 +138,17 @@ export const formatGroupedApprovalsToApprovalSteps = (
|
|||||||
|
|
||||||
if (!approvalGroup) {
|
if (!approvalGroup) {
|
||||||
const isWaiting = currentStepNumber === latestApproval.step_number + 1;
|
const isWaiting = currentStepNumber === latestApproval.step_number + 1;
|
||||||
|
const isPreviousApprovalRejected =
|
||||||
|
groupedApprovals[groupedApprovals.length - 1].approvals[0].action ===
|
||||||
|
'REJECTED';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: approvalLineItem.step_name,
|
name: approvalLineItem.step_name,
|
||||||
status: isWaiting ? 'WAITING' : 'IDLE',
|
status: isPreviousApprovalRejected
|
||||||
|
? 'IDLE'
|
||||||
|
: isWaiting
|
||||||
|
? 'WAITING'
|
||||||
|
: 'IDLE',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,12 @@
|
|||||||
|
|
||||||
import { ChangeEventHandler, useState } from 'react';
|
import { ChangeEventHandler, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
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 toast from 'react-hot-toast';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
@@ -20,6 +25,7 @@ import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
|||||||
import TextInput from '@/components/input/TextInput';
|
import TextInput from '@/components/input/TextInput';
|
||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
|
|
||||||
import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
|
import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
|
||||||
import { TransferToLayingApi } from '@/services/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 { ROWS_OPTIONS } from '@/config/constant';
|
||||||
import { Flock } from '@/types/api/master-data/flock';
|
import { Flock } from '@/types/api/master-data/flock';
|
||||||
import { FlockApi } from '@/services/api/master-data';
|
import { FlockApi } from '@/services/api/master-data';
|
||||||
|
import PillBadge from '@/components/PillBadge';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
type = 'dropdown',
|
type = 'dropdown',
|
||||||
@@ -43,6 +50,16 @@ const RowOptionsMenu = ({
|
|||||||
rejectClickHandler: () => void;
|
rejectClickHandler: () => void;
|
||||||
deleteClickHandler: () => 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 (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
<Button
|
<Button
|
||||||
@@ -55,50 +72,57 @@ const RowOptionsMenu = ({
|
|||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
{showEditButton && (
|
||||||
href={`/production/transfer-to-laying/detail/edit/?transferToLayingId=${props.row.original.id}`}
|
<Button
|
||||||
variant='ghost'
|
href={`/production/transfer-to-laying/detail/edit/?transferToLayingId=${props.row.original.id}`}
|
||||||
color='warning'
|
variant='ghost'
|
||||||
className='justify-start text-sm'
|
color='warning'
|
||||||
>
|
|
||||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
color='success'
|
|
||||||
onClick={approveClickHandler}
|
|
||||||
className='justify-start text-sm'
|
|
||||||
>
|
|
||||||
<Icon icon='material-symbols:check' width={24} height={24} />
|
|
||||||
Approve
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
color='error'
|
|
||||||
onClick={rejectClickHandler}
|
|
||||||
className='justify-start text-sm'
|
|
||||||
>
|
|
||||||
<Icon icon='material-symbols:close' width={24} height={24} />
|
|
||||||
Reject
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={deleteClickHandler}
|
|
||||||
variant='ghost'
|
|
||||||
color='error'
|
|
||||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon='material-symbols:delete-outline-rounded'
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
className='justify-start text-sm'
|
className='justify-start text-sm'
|
||||||
/>
|
>
|
||||||
Delete
|
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||||
</Button>
|
Edit
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* TODO: apply RBAC */}
|
||||||
|
{showApproveButton && (
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='success'
|
||||||
|
onClick={approveClickHandler}
|
||||||
|
className='justify-start text-sm'
|
||||||
|
>
|
||||||
|
<Icon icon='material-symbols:check' width={24} height={24} />
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{showRejectButton && (
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='error'
|
||||||
|
onClick={rejectClickHandler}
|
||||||
|
className='justify-start text-sm'
|
||||||
|
>
|
||||||
|
<Icon icon='material-symbols:close' width={24} height={24} />
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{showDeleteButton && (
|
||||||
|
<Button
|
||||||
|
onClick={deleteClickHandler}
|
||||||
|
variant='ghost'
|
||||||
|
color='error'
|
||||||
|
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:delete-outline-rounded'
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className='justify-start text-sm'
|
||||||
|
/>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</RowOptionsMenuWrapper>
|
</RowOptionsMenuWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -187,17 +211,24 @@ const TransferToLayingsTable = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => {
|
||||||
<div>
|
const isCheckboxDisabled =
|
||||||
<CheckboxInput
|
!row.getCanSelect() ||
|
||||||
name='row'
|
row.original.approval.action === 'APPROVED' ||
|
||||||
checked={row.getIsSelected()}
|
row.original.approval.action === 'REJECTED';
|
||||||
disabled={!row.getCanSelect()}
|
|
||||||
indeterminate={row.getIsSomeSelected()}
|
return (
|
||||||
onChange={row.getToggleSelectedHandler()}
|
<div>
|
||||||
/>
|
<CheckboxInput
|
||||||
</div>
|
name='row'
|
||||||
),
|
checked={row.getIsSelected()}
|
||||||
|
disabled={isCheckboxDisabled}
|
||||||
|
indeterminate={row.getIsSomeSelected()}
|
||||||
|
onChange={row.getToggleSelectedHandler()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: '#',
|
header: '#',
|
||||||
@@ -214,21 +245,55 @@ const TransferToLayingsTable = () => {
|
|||||||
{
|
{
|
||||||
accessorKey: 'flock_source',
|
accessorKey: 'flock_source',
|
||||||
header: 'Flock Asal',
|
header: 'Flock Asal',
|
||||||
cell: (props) => props.row.original.flock_source.name,
|
cell: (props) => props.row.original.from_project_flock.flock_name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'flock_destination',
|
accessorKey: 'flock_destination',
|
||||||
header: 'Flock Tujuan',
|
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',
|
header: 'Kuantitas',
|
||||||
|
cell: (props) => props.getValue() ?? props.row.original.pending_usage_qty,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'reason',
|
accessorKey: 'notes',
|
||||||
header: 'Alasan Transfer',
|
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',
|
header: 'Aksi',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
@@ -237,7 +302,7 @@ const TransferToLayingsTable = () => {
|
|||||||
const currentRowRelativeIndex =
|
const currentRowRelativeIndex =
|
||||||
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
||||||
|
|
||||||
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 3;
|
||||||
|
|
||||||
const approveClickHandler = () => {
|
const approveClickHandler = () => {
|
||||||
setSelectedTransferToLaying(props.row.original);
|
setSelectedTransferToLaying(props.row.original);
|
||||||
@@ -268,7 +333,7 @@ const TransferToLayingsTable = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{currentPageSize > 2 && (
|
{currentPageSize > 3 && (
|
||||||
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
||||||
<RowOptionsMenu
|
<RowOptionsMenu
|
||||||
type='dropdown'
|
type='dropdown'
|
||||||
@@ -280,7 +345,7 @@ const TransferToLayingsTable = () => {
|
|||||||
</RowDropdownOptions>
|
</RowDropdownOptions>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentPageSize <= 2 && (
|
{currentPageSize <= 3 && (
|
||||||
<RowCollapseOptions>
|
<RowCollapseOptions>
|
||||||
<RowOptionsMenu
|
<RowOptionsMenu
|
||||||
type='collapse'
|
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 = () => {
|
const bulkApproveClickHandler = () => {
|
||||||
approveModal.openModal();
|
approveModal.openModal();
|
||||||
};
|
};
|
||||||
@@ -309,27 +383,31 @@ const TransferToLayingsTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await TransferToLayingApi.delete(selectedTransferToLaying?.id as number);
|
try {
|
||||||
refreshTransferToLayings();
|
await TransferToLayingApi.delete(selectedTransferToLaying?.id as number);
|
||||||
|
|
||||||
deleteModal.closeModal();
|
toast.success('Berhasil menghapus data transfer ke laying!');
|
||||||
toast.success('Berhasil menghapus data transfer ke laying!');
|
refreshTransferToLayings();
|
||||||
setIsDeleteLoading(false);
|
} 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);
|
setIsApproveLoading(true);
|
||||||
|
|
||||||
const bulkApproveResponse =
|
const bulkApproveResponse = await TransferToLayingApi.bulkApprove(
|
||||||
await TransferToLayingApi.bulkApprove(selectedRowIds);
|
selectedRowIds,
|
||||||
|
notes
|
||||||
|
);
|
||||||
|
|
||||||
if (isResponseSuccess(bulkApproveResponse)) {
|
if (isResponseSuccess(bulkApproveResponse)) {
|
||||||
refreshTransferToLayings();
|
refreshTransferToLayings();
|
||||||
approveModal.closeModal();
|
approveModal.closeModal();
|
||||||
|
|
||||||
// TODO: remove console.log
|
|
||||||
console.log('Approved data:', selectedRowIds);
|
|
||||||
|
|
||||||
toast.success(
|
toast.success(
|
||||||
`Berhasil approve ${selectedRowIds.length} data transfer ke laying!`
|
`Berhasil approve ${selectedRowIds.length} data transfer ke laying!`
|
||||||
);
|
);
|
||||||
@@ -346,19 +424,18 @@ const TransferToLayingsTable = () => {
|
|||||||
setIsApproveLoading(false);
|
setIsApproveLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalRejectClickHandler = async () => {
|
const confirmationModalRejectClickHandler = async (notes: string) => {
|
||||||
setIsRejectLoading(true);
|
setIsRejectLoading(true);
|
||||||
|
|
||||||
const bulkRejectResponse =
|
const bulkRejectResponse = await TransferToLayingApi.bulkReject(
|
||||||
await TransferToLayingApi.bulkReject(selectedRowIds);
|
selectedRowIds,
|
||||||
|
notes
|
||||||
|
);
|
||||||
|
|
||||||
if (isResponseSuccess(bulkRejectResponse)) {
|
if (isResponseSuccess(bulkRejectResponse)) {
|
||||||
refreshTransferToLayings();
|
refreshTransferToLayings();
|
||||||
rejectModal.closeModal();
|
rejectModal.closeModal();
|
||||||
|
|
||||||
// TODO: remove console.log
|
|
||||||
console.log('Rejected data:', selectedRowIds);
|
|
||||||
|
|
||||||
toast.success(
|
toast.success(
|
||||||
`Berhasil reject ${selectedRowIds.length} data transfer ke laying!`
|
`Berhasil reject ${selectedRowIds.length} data transfer ke laying!`
|
||||||
);
|
);
|
||||||
@@ -559,6 +636,7 @@ const TransferToLayingsTable = () => {
|
|||||||
setSorting={setSorting}
|
setSorting={setSorting}
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
setRowSelection={setRowSelection}
|
setRowSelection={setRowSelection}
|
||||||
|
enableRowSelection={tableEnableRowSelectionHandler}
|
||||||
className={{
|
className={{
|
||||||
containerClassName: cn({
|
containerClassName: cn({
|
||||||
'mb-20':
|
'mb-20':
|
||||||
@@ -592,7 +670,7 @@ const TransferToLayingsTable = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConfirmationModal
|
<ConfirmationModalWithNotes
|
||||||
ref={approveModal.ref}
|
ref={approveModal.ref}
|
||||||
type='success'
|
type='success'
|
||||||
text={`Apakah anda yakin ingin approve data transfer ke laying ini (${selectedRowIds.length} data)?`}
|
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}
|
ref={rejectModal.ref}
|
||||||
type='error'
|
type='error'
|
||||||
text={`Apakah anda yakin ingin reject data transfer ke laying ini (${selectedRowIds.length} data)?`}
|
text={`Apakah anda yakin ingin reject data transfer ke laying ini (${selectedRowIds.length} data)?`}
|
||||||
|
|||||||
+156
-2
@@ -1,4 +1,7 @@
|
|||||||
import * as Yup from 'yup';
|
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 = {
|
type TransferToLayingFormSchemaType = {
|
||||||
transfer_date?: string;
|
transfer_date?: string;
|
||||||
@@ -14,7 +17,7 @@ type TransferToLayingFormSchemaType = {
|
|||||||
totalQuantity?: number;
|
totalQuantity?: number;
|
||||||
maxTotalQuantity?: number; // original cap (hidden), helper
|
maxTotalQuantity?: number; // original cap (hidden), helper
|
||||||
|
|
||||||
kandangs: {
|
flockSourceKandangs: {
|
||||||
kandang: {
|
kandang: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -22,6 +25,16 @@ type TransferToLayingFormSchemaType = {
|
|||||||
quantity: number | string; // editable
|
quantity: number | string; // editable
|
||||||
maxQuantity?: number; // original cap (hidden), helper
|
maxQuantity?: number; // original cap (hidden), helper
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
|
flockDestinationKandangs: {
|
||||||
|
kandang: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
quantity: number | string; // editable
|
||||||
|
maxQuantity?: number; // original cap (hidden), helper
|
||||||
|
}[];
|
||||||
|
|
||||||
reason?: string;
|
reason?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,7 +64,29 @@ export const TransferToLayingFormSchema: Yup.ObjectSchema<TransferToLayingFormSc
|
|||||||
.min(1, 'Jumlah transfer minimal 1')
|
.min(1, 'Jumlah transfer minimal 1')
|
||||||
.required('Jumlah transfer wajib diisi!'),
|
.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(
|
.of(
|
||||||
Yup.object({
|
Yup.object({
|
||||||
kandang: Yup.object({
|
kandang: Yup.object({
|
||||||
@@ -81,3 +116,122 @@ export const UpdateTransferToLayingFormSchema = TransferToLayingFormSchema;
|
|||||||
export type TransferToLayingFormValues = Yup.InferType<
|
export type TransferToLayingFormValues = Yup.InferType<
|
||||||
typeof TransferToLayingFormSchema
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
+404
-187
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
@@ -8,16 +8,23 @@ import useSWR from 'swr';
|
|||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import TextInput from '@/components/input/TextInput';
|
|
||||||
import SelectInput, {
|
import SelectInput, {
|
||||||
OptionType,
|
OptionType,
|
||||||
// useSelect,
|
useSelect,
|
||||||
} from '@/components/input/SelectInput';
|
} from '@/components/input/SelectInput';
|
||||||
import TextArea from '@/components/input/TextArea';
|
import TextArea from '@/components/input/TextArea';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
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 {
|
import {
|
||||||
|
getFilledTransferToLayingFormInitialValues,
|
||||||
|
getTransferToLayingFormInitialValues,
|
||||||
TransferToLayingFormSchema,
|
TransferToLayingFormSchema,
|
||||||
TransferToLayingFormValues,
|
TransferToLayingFormValues,
|
||||||
UpdateTransferToLayingFormSchema,
|
UpdateTransferToLayingFormSchema,
|
||||||
@@ -31,6 +38,8 @@ import {
|
|||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
|
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 {
|
interface TransferToLayingFormProps {
|
||||||
type?: 'add' | 'edit' | 'detail';
|
type?: 'add' | 'edit' | 'detail';
|
||||||
@@ -55,11 +64,23 @@ const TransferToLayingForm = ({
|
|||||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
const [isRejectLoading, setIsRejectLoading] = 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(
|
const createTransferToLayingHandler = useCallback(
|
||||||
async (payload: CreateTransferToLayingPayload) => {
|
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]
|
[router]
|
||||||
);
|
);
|
||||||
@@ -69,46 +90,30 @@ const TransferToLayingForm = ({
|
|||||||
transferToLayingId: number,
|
transferToLayingId: number,
|
||||||
payload: UpdateTransferToLayingPayload
|
payload: UpdateTransferToLayingPayload
|
||||||
) => {
|
) => {
|
||||||
console.log(
|
const updateKandangRes = await TransferToLayingApi.update(
|
||||||
`Update transfer to laying with ID of ${transferToLayingId}:`,
|
transferToLayingId,
|
||||||
{ payload }
|
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]
|
[router]
|
||||||
);
|
);
|
||||||
|
|
||||||
const formikInitialValues = useMemo<TransferToLayingFormValues>(() => {
|
// const formikInitialValues = useMemo<TransferToLayingFormValues>(() => {
|
||||||
return {
|
// return getTransferToLayingFormInitialValues(initialValues);
|
||||||
transfer_date: initialValues?.transfer_date ?? '',
|
// }, [initialValues]);
|
||||||
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,
|
|
||||||
|
|
||||||
kandangs: initialValues?.kandangs
|
const [formikInitialValues, setFormikInitialValues] = useState(
|
||||||
? initialValues.kandangs.map((kandang) => ({
|
getTransferToLayingFormInitialValues()
|
||||||
kandang: {
|
);
|
||||||
value: kandang.kandang.id,
|
|
||||||
label: kandang.kandang.name,
|
|
||||||
},
|
|
||||||
quantity: kandang.quantity,
|
|
||||||
}))
|
|
||||||
: [],
|
|
||||||
|
|
||||||
reason: initialValues?.reason ?? undefined,
|
|
||||||
};
|
|
||||||
}, [initialValues]);
|
|
||||||
|
|
||||||
const formik = useFormik<TransferToLayingFormValues>({
|
const formik = useFormik<TransferToLayingFormValues>({
|
||||||
initialValues: formikInitialValues,
|
initialValues: formikInitialValues,
|
||||||
@@ -117,23 +122,23 @@ const TransferToLayingForm = ({
|
|||||||
? UpdateTransferToLayingFormSchema
|
? UpdateTransferToLayingFormSchema
|
||||||
: TransferToLayingFormSchema,
|
: TransferToLayingFormSchema,
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
console.log({ values });
|
|
||||||
|
|
||||||
setFormErrorMessage('');
|
setFormErrorMessage('');
|
||||||
|
|
||||||
const transferToLayingPayload: CreateTransferToLayingPayload = {
|
const transferToLayingPayload: CreateTransferToLayingPayload = {
|
||||||
transfer_date: values.transfer_date as string,
|
transfer_date: values.transfer_date as string,
|
||||||
flock_source_id: values.flockSource?.value as number,
|
source_project_flock_id: values.flockSource?.value as number,
|
||||||
flock_destination_id: values.flockDestination?.value as number,
|
target_project_flock_id: values.flockDestination?.value as number,
|
||||||
totalQuantity: values.totalQuantity as number,
|
totalQuantity: values.totalQuantity as number,
|
||||||
|
|
||||||
kandangs: values.kandangs?.map((kandang) => ({
|
source_kandangs: values.flockSourceKandangs?.map((kandang) => ({
|
||||||
kandang_id: kandang.kandang.value,
|
project_flock_kandang_id: kandang.kandang.value,
|
||||||
quantity: kandang.quantity,
|
quantity: parseFloat(kandang.quantity as string),
|
||||||
})) as {
|
})) as CreateTransferToLayingPayload['source_kandangs'],
|
||||||
kandang_id: number;
|
|
||||||
quantity: number;
|
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,
|
reason: values.reason as string,
|
||||||
};
|
};
|
||||||
@@ -154,7 +159,11 @@ const TransferToLayingForm = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { setValues: formikSetValues, values: formikValues } = formik;
|
const { setValues: formikSetValues, values: formikValues } = formik;
|
||||||
const { kandangs: kandangsValue } = formikValues;
|
const {
|
||||||
|
flockSourceKandangs: flockSourceKandangsValue,
|
||||||
|
flockDestinationKandangs: flockDestinationKandangsValue,
|
||||||
|
totalQuantity,
|
||||||
|
} = formikValues;
|
||||||
|
|
||||||
const deleteTransferToLayingClickHandler = () => {
|
const deleteTransferToLayingClickHandler = () => {
|
||||||
deleteModal.openModal();
|
deleteModal.openModal();
|
||||||
@@ -172,24 +181,32 @@ const TransferToLayingForm = ({
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
// TODO: delete data and integrate to real API
|
try {
|
||||||
deleteModal.closeModal();
|
await TransferToLayingApi.delete(initialValues?.id as number);
|
||||||
toast.success('Berhasil menghapus data transfer ke laying!');
|
|
||||||
|
|
||||||
setIsDeleteLoading(false);
|
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);
|
setIsApproveLoading(true);
|
||||||
|
|
||||||
const approveResponse = await TransferToLayingApi.approve(
|
const approveResponse = await TransferToLayingApi.approve(
|
||||||
initialValues?.id as number
|
initialValues?.id as number,
|
||||||
|
notes
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isResponseSuccess(approveResponse)) {
|
if (isResponseSuccess(approveResponse)) {
|
||||||
approveModal.closeModal();
|
approveModal.closeModal();
|
||||||
|
|
||||||
toast.success('Berhasil approve data transfer ke laying!');
|
toast.success('Berhasil approve data transfer ke laying!');
|
||||||
|
router.push('/production/transfer-to-laying');
|
||||||
} else {
|
} else {
|
||||||
approveModal.closeModal();
|
approveModal.closeModal();
|
||||||
|
|
||||||
@@ -199,17 +216,19 @@ const TransferToLayingForm = ({
|
|||||||
setIsApproveLoading(false);
|
setIsApproveLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalRejectClickHandler = async () => {
|
const confirmationModalRejectClickHandler = async (notes: string) => {
|
||||||
setIsRejectLoading(true);
|
setIsRejectLoading(true);
|
||||||
|
|
||||||
const rejectResponse = await TransferToLayingApi.reject(
|
const rejectResponse = await TransferToLayingApi.reject(
|
||||||
initialValues?.id as number
|
initialValues?.id as number,
|
||||||
|
notes
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isResponseSuccess(rejectResponse)) {
|
if (isResponseSuccess(rejectResponse)) {
|
||||||
rejectModal.closeModal();
|
rejectModal.closeModal();
|
||||||
|
|
||||||
toast.success('Berhasil reject data transfer ke laying!');
|
toast.success('Berhasil reject data transfer ke laying!');
|
||||||
|
router.push('/production/transfer-to-laying');
|
||||||
} else {
|
} else {
|
||||||
rejectModal.closeModal();
|
rejectModal.closeModal();
|
||||||
|
|
||||||
@@ -219,49 +238,47 @@ const TransferToLayingForm = ({
|
|||||||
setIsRejectLoading(false);
|
setIsRejectLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isRepeaterInputError = (
|
// flock source
|
||||||
column: keyof TransferToLayingFormValues['kandangs'][0],
|
const isFlockSourceKandangsRepeaterInputError = (
|
||||||
|
column: keyof TransferToLayingFormValues['flockSourceKandangs'][0],
|
||||||
idx: number
|
idx: number
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
formik.touched.kandangs?.[idx]?.[column] &&
|
formik.touched.flockSourceKandangs?.[idx]?.[column] &&
|
||||||
Boolean(
|
Boolean(
|
||||||
formik.errors.kandangs?.[idx] instanceof Object &&
|
formik.errors.flockSourceKandangs?.[idx] instanceof Object &&
|
||||||
formik.errors.kandangs?.[idx]?.[column]
|
formik.errors.flockSourceKandangs?.[idx]?.[column]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const repeaterInputErrorMessage = (
|
const flockSourceKandangsRepeaterInputErrorMessage = (
|
||||||
column: keyof TransferToLayingFormValues['kandangs'][0],
|
column: keyof TransferToLayingFormValues['flockSourceKandangs'][0],
|
||||||
idx: number
|
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
|
const {
|
||||||
// Flock Source
|
setInputValue: setFlockSourceInputValue,
|
||||||
// const {
|
options: flockSourceOptions,
|
||||||
// inputValue: flockSourceInputValue,
|
isLoadingOptions: isLoadingFlockSourceOptions,
|
||||||
// setInputValue: setFlockSourceInputValue,
|
rawData: flockSources,
|
||||||
// options: flockSourceOptions,
|
} = useSelect<ProjectFlock>(
|
||||||
// isLoadingOptions: isLoadingFlockSourceOptions,
|
'/production/project-flocks',
|
||||||
// } = useSelect<FlockWithKandangs>('/transfer-to-laying/production/get-flock-source', 'id', 'name');
|
'id',
|
||||||
|
'flock_name',
|
||||||
// TODO: remove this dummy data
|
'search',
|
||||||
const { data: flockSources, isLoading: isLoadingFlockSourceOptions } = useSWR(
|
{
|
||||||
'test',
|
category: 'GROWING',
|
||||||
() => TransferToLayingApi.getFlockSource()
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const flockSourceOptions = isResponseSuccess(flockSources)
|
const flockSourceChangeHandler = async (
|
||||||
? flockSources?.data.map((flockSource) => ({
|
val: OptionType | OptionType[] | null
|
||||||
value: flockSource.id,
|
) => {
|
||||||
label: flockSource.name,
|
|
||||||
}))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const flockSourceChangeHandler = (val: OptionType | OptionType[] | null) => {
|
|
||||||
// Get flock source data for total quantity and kandang
|
// Get flock source data for total quantity and kandang
|
||||||
const flockSource =
|
const flockSource =
|
||||||
isResponseSuccess(flockSources) && val !== null
|
isResponseSuccess(flockSources) && val !== null
|
||||||
@@ -272,21 +289,38 @@ const TransferToLayingForm = ({
|
|||||||
|
|
||||||
// Set total quantity and kandangs
|
// Set total quantity and kandangs
|
||||||
if (flockSource) {
|
if (flockSource) {
|
||||||
|
const mappedFlockKandangsAvailableQty =
|
||||||
|
await TransferToLayingApi.getMappedFlockKandangsAvailability(
|
||||||
|
flockSource.id
|
||||||
|
);
|
||||||
|
|
||||||
const formattedKandangs = flockSource.kandangs.map((item) => ({
|
const formattedKandangs = flockSource.kandangs.map((item) => ({
|
||||||
kandang: {
|
kandang: {
|
||||||
value: item.kandang.id,
|
value: item.project_flock_kandang_id,
|
||||||
label: item.kandang.name,
|
label: item.name,
|
||||||
},
|
},
|
||||||
quantity: '',
|
quantity: '',
|
||||||
maxQuantity: item.quantity,
|
maxQuantity:
|
||||||
|
(mappedFlockKandangsAvailableQty &&
|
||||||
|
mappedFlockKandangsAvailableQty[item.project_flock_kandang_id]
|
||||||
|
.available_qty) ??
|
||||||
|
0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
formik.setFieldValue('totalQuantity', flockSource.totalQuantity);
|
let maxTotalQuantity = 0;
|
||||||
formik.setFieldValue('maxTotalQuantity', flockSource.totalQuantity);
|
// flockSource.kandangs.forEach((item) => {
|
||||||
formik.setFieldValue('kandangs', formattedKandangs);
|
// maxTotalQuantity += item.capacity;
|
||||||
|
// });
|
||||||
|
formattedKandangs.forEach((item) => {
|
||||||
|
maxTotalQuantity += item.maxQuantity;
|
||||||
|
});
|
||||||
|
|
||||||
|
formik.setFieldValue('totalQuantity', '');
|
||||||
|
formik.setFieldValue('maxTotalQuantity', maxTotalQuantity);
|
||||||
|
formik.setFieldValue('flockSourceKandangs', formattedKandangs);
|
||||||
} else {
|
} else {
|
||||||
formik.setFieldValue('totalQuantity', undefined);
|
formik.setFieldValue('totalQuantity', undefined);
|
||||||
formik.setFieldValue('kandangs', undefined);
|
formik.setFieldValue('flockSourceKandangs', undefined);
|
||||||
formik.setFieldValue('reason', '');
|
formik.setFieldValue('reason', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,52 +328,137 @@ const TransferToLayingForm = ({
|
|||||||
formik.setFieldValue('flockSource', val);
|
formik.setFieldValue('flockSource', val);
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: remove dummy data and use real data
|
// flock destination
|
||||||
// Flock Destination
|
const isFlockDestinationKandangsRepeaterInputError = (
|
||||||
// const {
|
column: keyof TransferToLayingFormValues['flockDestinationKandangs'][0],
|
||||||
// inputValue: flockDestinationInputValue,
|
idx: number
|
||||||
// setInputValue: setFlockDestinationInputValue,
|
) => {
|
||||||
// options: flockDestinationOptions,
|
return (
|
||||||
// isLoadingOptions: isLoadingFlockDestinationOptions,
|
formik.touched.flockDestinationKandangs?.[idx]?.[column] &&
|
||||||
// } = useSelect<FlockWithKandangs>('/transfer-to-laying/production/get-flock-destination', 'id', 'name');
|
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 {
|
const {
|
||||||
data: flockDestinations,
|
setInputValue: setFlockDestinationInputValue,
|
||||||
isLoading: isLoadingFlockDestinationOptions,
|
options: flockDestinationOptions,
|
||||||
} = useSWR('test', () => TransferToLayingApi.getFlockSource());
|
isLoadingOptions: isLoadingFlockDestinationOptions,
|
||||||
|
rawData: flockDestinations,
|
||||||
const flockDestinationOptions = isResponseSuccess(flockDestinations)
|
} = useSelect<ProjectFlock>(
|
||||||
? flockDestinations?.data.map((flockDestination) => ({
|
'/production/project-flocks',
|
||||||
value: flockDestination.id,
|
'id',
|
||||||
label: flockDestination.name,
|
'flock_name',
|
||||||
}))
|
'search',
|
||||||
: [];
|
{
|
||||||
|
category: 'LAYING',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const flockDestinationChangeHandler = (
|
const flockDestinationChangeHandler = (
|
||||||
val: OptionType | OptionType[] | null
|
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.setFieldTouched('flockDestination', true);
|
||||||
formik.setFieldValue('flockDestination', val);
|
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(() => {
|
useEffect(() => {
|
||||||
formikSetValues(formikInitialValues);
|
formikSetValues(formikInitialValues);
|
||||||
}, [formikSetValues, formikInitialValues]);
|
}, [formikSetValues, formikInitialValues]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// calculate total quantity if kandangs quantity change
|
// calculate total quantity if kandangs quantity change
|
||||||
if (kandangsValue && kandangsValue.length > 0) {
|
if (flockSourceKandangsValue && flockSourceKandangsValue.length > 0) {
|
||||||
let newTotalQuantity = 0;
|
let newTotalQuantity = 0;
|
||||||
|
|
||||||
kandangsValue.forEach((item) => {
|
flockSourceKandangsValue.forEach((item) => {
|
||||||
newTotalQuantity += item.quantity as number;
|
newTotalQuantity += parseFloat(item.quantity as string);
|
||||||
});
|
});
|
||||||
|
|
||||||
formik.setFieldValue('totalQuantity', newTotalQuantity);
|
formik.setFieldValue('totalQuantity', newTotalQuantity);
|
||||||
formik.validateField('totalQuantity');
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -361,30 +480,56 @@ const TransferToLayingForm = ({
|
|||||||
</h1>
|
</h1>
|
||||||
</header>
|
</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' && (
|
{type === 'detail' && (
|
||||||
<>
|
<>
|
||||||
<Button
|
{isShowApproveRejectButton && (
|
||||||
variant='outline'
|
<div className='w-full flex flex-row justify-end gap-2'>
|
||||||
color='success'
|
{/* TODO: apply RBAC */}
|
||||||
onClick={approveClickHandler}
|
<Button
|
||||||
// disabled={selectedRowIds.length === 0}
|
variant='outline'
|
||||||
className='w-full sm:w-fit'
|
color='success'
|
||||||
>
|
onClick={approveClickHandler}
|
||||||
<Icon icon='material-symbols:check' width={24} height={24} />
|
className='w-full sm:w-fit'
|
||||||
Approve
|
>
|
||||||
</Button>
|
<Icon
|
||||||
|
icon='material-symbols:check'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='error'
|
color='error'
|
||||||
onClick={rejectClickHandler}
|
onClick={rejectClickHandler}
|
||||||
// disabled={selectedRowIds.length === 0}
|
className='w-full sm:w-fit'
|
||||||
className='w-full sm:w-fit'
|
>
|
||||||
>
|
<Icon
|
||||||
<Icon icon='material-symbols:close' width={24} height={24} />
|
icon='material-symbols:close'
|
||||||
Reject
|
width={24}
|
||||||
</Button>
|
height={24}
|
||||||
|
/>
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -395,13 +540,12 @@ const TransferToLayingForm = ({
|
|||||||
className='w-full flex flex-col gap-6'
|
className='w-full flex flex-col gap-6'
|
||||||
>
|
>
|
||||||
<div className='flex flex-col gap-4'>
|
<div className='flex flex-col gap-4'>
|
||||||
<TextInput
|
<DateInput
|
||||||
required
|
required
|
||||||
type='date'
|
|
||||||
label='Tanggal Transfer'
|
label='Tanggal Transfer'
|
||||||
name='transfer_date'
|
name='transfer_date'
|
||||||
placeholder='Masukkan tanggal transfer'
|
placeholder='Masukkan tanggal transfer'
|
||||||
value={formik.values.transfer_date}
|
value={formik.values.transfer_date ?? ''}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
isError={
|
isError={
|
||||||
@@ -421,7 +565,7 @@ const TransferToLayingForm = ({
|
|||||||
options={flockSourceOptions}
|
options={flockSourceOptions}
|
||||||
onChange={flockSourceChangeHandler}
|
onChange={flockSourceChangeHandler}
|
||||||
isLoading={isLoadingFlockSourceOptions}
|
isLoading={isLoadingFlockSourceOptions}
|
||||||
// onInputChange={setFlockSourceInputValue}
|
onInputChange={setFlockSourceInputValue}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.flockSource &&
|
formik.touched.flockSource &&
|
||||||
Boolean(typeof formik.errors.flockSource === 'string')
|
Boolean(typeof formik.errors.flockSource === 'string')
|
||||||
@@ -439,7 +583,7 @@ const TransferToLayingForm = ({
|
|||||||
options={flockDestinationOptions}
|
options={flockDestinationOptions}
|
||||||
onChange={flockDestinationChangeHandler}
|
onChange={flockDestinationChangeHandler}
|
||||||
isLoading={isLoadingFlockDestinationOptions}
|
isLoading={isLoadingFlockDestinationOptions}
|
||||||
// onInputChange={setFlockDestinationInputValue}
|
onInputChange={setFlockDestinationInputValue}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.flockDestination &&
|
formik.touched.flockDestination &&
|
||||||
Boolean(typeof formik.errors.flockDestination === 'string')
|
Boolean(typeof formik.errors.flockDestination === 'string')
|
||||||
@@ -450,9 +594,8 @@ const TransferToLayingForm = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TextInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
type='number'
|
|
||||||
name='totalQuantity'
|
name='totalQuantity'
|
||||||
label='Jumlah Transfer'
|
label='Jumlah Transfer'
|
||||||
bottomLabel={
|
bottomLabel={
|
||||||
@@ -461,7 +604,9 @@ const TransferToLayingForm = ({
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
placeholder='Masukkan jumlah transfer'
|
placeholder='Masukkan jumlah transfer'
|
||||||
value={formik.values.totalQuantity ?? ''}
|
value={
|
||||||
|
formik.values.totalQuantity ? formik.values.totalQuantity : ''
|
||||||
|
}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
isError={
|
isError={
|
||||||
@@ -469,24 +614,22 @@ const TransferToLayingForm = ({
|
|||||||
Boolean(formik.errors.totalQuantity)
|
Boolean(formik.errors.totalQuantity)
|
||||||
}
|
}
|
||||||
errorMessage={formik.errors.totalQuantity}
|
errorMessage={formik.errors.totalQuantity}
|
||||||
// readOnly={type === 'detail'}
|
|
||||||
// disabled={Boolean(formik.errors.flockSource)}
|
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div className='flex flex-col gap-4'>
|
||||||
<div className='overflow-x-auto'>
|
<div className='overflow-x-auto'>
|
||||||
<table className='table'>
|
<table className='table'>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Kandang</th>
|
<th>Kandang Flock Asal</th>
|
||||||
<th>Kuantitas</th>
|
<th>Kuantitas</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
{(!formik.values.kandangs ||
|
{(!formik.values.flockSourceKandangs ||
|
||||||
formik.values.kandangs.length === 0) && (
|
formik.values.flockSourceKandangs.length === 0) && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={2}>
|
<td colSpan={2}>
|
||||||
<p className='w-full text-center text-gray-400'>
|
<p className='w-full text-center text-gray-400'>
|
||||||
@@ -496,8 +639,8 @@ const TransferToLayingForm = ({
|
|||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{formik.values.kandangs &&
|
{formik.values.flockSourceKandangs &&
|
||||||
formik.values.kandangs.map((kandang, idx) => (
|
formik.values.flockSourceKandangs.map((kandang, idx) => (
|
||||||
<tr key={idx}>
|
<tr key={idx}>
|
||||||
<td>
|
<td>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
@@ -511,10 +654,9 @@ const TransferToLayingForm = ({
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<TextInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
type='number'
|
name={`flockSourceKandangs[${idx}].quantity`}
|
||||||
name={`kandangs[${idx}].quantity`}
|
|
||||||
bottomLabel={
|
bottomLabel={
|
||||||
kandang.maxQuantity
|
kandang.maxQuantity
|
||||||
? `Max: ${kandang.maxQuantity}`
|
? `Max: ${kandang.maxQuantity}`
|
||||||
@@ -524,8 +666,11 @@ const TransferToLayingForm = ({
|
|||||||
value={kandang.quantity}
|
value={kandang.quantity}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
isError={isRepeaterInputError('quantity', idx)}
|
isError={isFlockSourceKandangsRepeaterInputError(
|
||||||
errorMessage={repeaterInputErrorMessage(
|
'quantity',
|
||||||
|
idx
|
||||||
|
)}
|
||||||
|
errorMessage={flockSourceKandangsRepeaterInputErrorMessage(
|
||||||
'quantity',
|
'quantity',
|
||||||
idx
|
idx
|
||||||
)}
|
)}
|
||||||
@@ -540,6 +685,76 @@ const TransferToLayingForm = ({
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<TextArea
|
<TextArea
|
||||||
@@ -558,25 +773,38 @@ const TransferToLayingForm = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
{type !== 'add' && (
|
{type !== 'add' && (
|
||||||
<div className='flex flex-row justify-start gap-2'>
|
<div className='flex flex-row justify-start gap-2'>
|
||||||
<Button
|
{isShowDeleteButton && (
|
||||||
type='button'
|
<Button
|
||||||
color='error'
|
type='button'
|
||||||
onClick={deleteTransferToLayingClickHandler}
|
color='error'
|
||||||
className='px-4'
|
onClick={deleteTransferToLayingClickHandler}
|
||||||
>
|
className='px-4'
|
||||||
<Icon
|
>
|
||||||
icon='material-symbols:delete-outline-rounded'
|
<Icon
|
||||||
width={24}
|
icon='material-symbols:delete-outline-rounded'
|
||||||
height={24}
|
width={24}
|
||||||
className='justify-start text-sm'
|
height={24}
|
||||||
/>
|
className='justify-start text-sm'
|
||||||
Delete
|
/>
|
||||||
</Button>
|
Delete
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{type !== 'edit' && (
|
{type !== 'edit' && isShowEditButton && (
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
@@ -617,17 +845,6 @@ const TransferToLayingForm = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -650,7 +867,7 @@ const TransferToLayingForm = ({
|
|||||||
|
|
||||||
{type === 'detail' && (
|
{type === 'detail' && (
|
||||||
<>
|
<>
|
||||||
<ConfirmationModal
|
<ConfirmationModalWithNotes
|
||||||
ref={approveModal.ref}
|
ref={approveModal.ref}
|
||||||
type='success'
|
type='success'
|
||||||
text='Apakah anda yakin ingin approve data transfer ke laying ini?'
|
text='Apakah anda yakin ingin approve data transfer ke laying ini?'
|
||||||
@@ -665,7 +882,7 @@ const TransferToLayingForm = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConfirmationModal
|
<ConfirmationModalWithNotes
|
||||||
ref={rejectModal.ref}
|
ref={rejectModal.ref}
|
||||||
type='error'
|
type='error'
|
||||||
text='Apakah anda yakin ingin reject data transfer ke laying ini?'
|
text='Apakah anda yakin ingin reject data transfer ke laying ini?'
|
||||||
|
|||||||
@@ -21,3 +21,14 @@ export const PROJECT_FLOCK_KANDANG_APPROVAL_LINE: ApprovalLine = [
|
|||||||
step_name: 'Disetujui',
|
step_name: 'Disetujui',
|
||||||
},
|
},
|
||||||
] as const;
|
] 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
Vendored
-12
@@ -12,15 +12,3 @@ export type CreateFlockPayload = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateFlockPayload = 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
@@ -10,6 +10,7 @@ export type BaseKandang = {
|
|||||||
capacity: number;
|
capacity: number;
|
||||||
pic: BaseUser;
|
pic: BaseUser;
|
||||||
project_flock_kandang_id?: number;
|
project_flock_kandang_id?: number;
|
||||||
|
capacity: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Kandang = BaseMetadata & BaseKandang;
|
export type Kandang = BaseMetadata & BaseKandang;
|
||||||
|
|||||||
+14
-1
@@ -8,6 +8,7 @@ import { BaseApproval, BaseMetadata } from '@/types/api/api-general';
|
|||||||
export type BaseProjectFlock = {
|
export type BaseProjectFlock = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
flock_name: string;
|
||||||
status: string;
|
status: string;
|
||||||
flock?: Flock;
|
flock?: Flock;
|
||||||
flock_i?: number;
|
flock_i?: number;
|
||||||
@@ -21,7 +22,9 @@ export type BaseProjectFlock = {
|
|||||||
location_id: number;
|
location_id: number;
|
||||||
period: number;
|
period: number;
|
||||||
kandang_ids: number[];
|
kandang_ids: number[];
|
||||||
kandangs: Kandang[];
|
kandangs: (Kandang & {
|
||||||
|
project_flock_kandang_id: number;
|
||||||
|
})[];
|
||||||
approval: BaseApproval;
|
approval: BaseApproval;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,3 +51,13 @@ export type ProjectFlockApprovalPayload = {
|
|||||||
action: 'APPROVED' | 'REJECTED';
|
action: 'APPROVED' | 'REJECTED';
|
||||||
approvable_ids: number[];
|
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;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|||||||
+69
-15
@@ -1,34 +1,88 @@
|
|||||||
import { BaseApiResponse, BaseMetadata, flags } from '@/types/api/api-general';
|
import {
|
||||||
import { Kandang } from '@/types/api/master-data/kandang';
|
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 = {
|
export type BaseTransferToLaying = {
|
||||||
id: number;
|
id: number;
|
||||||
|
transfer_number: string;
|
||||||
transfer_date: string;
|
transfer_date: string;
|
||||||
flock_source: {
|
notes: string;
|
||||||
|
from_project_flock: {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
flock_name: string;
|
||||||
|
category: 'GROWING' | 'LAYING';
|
||||||
};
|
};
|
||||||
flock_destination: {
|
to_project_flock: {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
flock_name: string;
|
||||||
|
category: 'GROWING' | 'LAYING';
|
||||||
};
|
};
|
||||||
quantity: number;
|
pending_usage_qty: number | null;
|
||||||
kandangs: {
|
usage_qty: number | null;
|
||||||
kandang: Kandang;
|
|
||||||
quantity: number;
|
sources: {
|
||||||
|
source_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;
|
||||||
|
};
|
||||||
|
};
|
||||||
}[];
|
}[];
|
||||||
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 TransferToLaying = BaseMetadata & BaseTransferToLaying;
|
||||||
|
|
||||||
export type CreateTransferToLayingPayload = {
|
export type CreateTransferToLayingPayload = {
|
||||||
transfer_date: string;
|
transfer_date: string;
|
||||||
flock_source_id: number;
|
source_project_flock_id: number;
|
||||||
flock_destination_id: number;
|
target_project_flock_id: number;
|
||||||
totalQuantity: number;
|
totalQuantity: number;
|
||||||
kandangs: {
|
source_kandangs: {
|
||||||
kandang_id: number;
|
project_flock_kandang_id: number;
|
||||||
|
quantity: number;
|
||||||
|
}[];
|
||||||
|
target_kandangs: {
|
||||||
|
project_flock_kandang_id: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
}[];
|
}[];
|
||||||
reason: string;
|
reason: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user