diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index efda72f0..951e5472 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,76 +1,146 @@
-stages: [notify]
+stages:
+ - build
+ - deploy
-# --- Notify when MR is opened/updated ---
-notify_discord_mr:
- stage: notify
- image: alpine:3.20
- rules:
- - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
+.build_template: &build_template
+ stage: build
+ image: node:20-alpine
+ cache:
+ key: npm-cache
+ paths:
+ - node_modules/
variables:
- WEBHOOK_URL: $DISCORD_WEBHOOK_URL
- before_script:
- - apk add --no-cache curl jq
- script: |
- MR_URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}"
+ NPM_CONFIG_PRODUCTION: 'false'
+ NODE_ENV: ''
+ script:
+ - echo "Installing dependencies..."
+ - npm ci --no-audit --no-fund
+ - echo "Building Next.js static export..."
+ - npx next build
+ artifacts:
+ name: 'out-$CI_COMMIT_SHORT_SHA'
+ paths:
+ - out/
+ expire_in: 1 week
- jq -n \
- --arg repo "$CI_PROJECT_PATH" \
- --arg mr "#${CI_MERGE_REQUEST_IID}" \
- --arg url "$MR_URL" \
- --arg requestor "${GITLAB_USER_LOGIN:-$GITLAB_USER_NAME}" \
- --arg source "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" \
- --arg target "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" \
- --arg title "$CI_MERGE_REQUEST_TITLE" \
- '{
- username: "CI Bot - FE",
- embeds: [{
- title: "📣 [LTI WEB CLIENT] Merge Request Opened/Updated",
- description: ($mr + " in " + $repo),
- url: $url,
- 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"
+.deploy_template: &deploy_template
+ stage: deploy
+ image:
+ name: amazon/aws-cli:latest
+ entrypoint: ['/bin/sh', '-c']
+ script:
+ - set -e
+ - aws --version
+ - echo "Cleaning up newline characters in AWS credentials..."
+ - export AWS_ACCESS_KEY_ID=$(echo $AWS_ACCESS_KEY_ID | tr -d '\r\n')
+ - export AWS_SECRET_ACCESS_KEY=$(echo $AWS_SECRET_ACCESS_KEY | tr -d '\r\n')
+ - echo "Deploying to s3://$S3_BUCKET in region $AWS_REGION"
+ - aws s3api head-bucket --bucket "$S3_BUCKET" --region "$AWS_REGION" || aws s3api create-bucket --bucket "$S3_BUCKET" --region "$AWS_REGION" --create-bucket-configuration LocationConstraint="$AWS_REGION"
+ - aws s3 sync ./out "s3://$S3_BUCKET" --delete --region "$AWS_REGION" --endpoint-url "https://s3.ap-southeast-3.amazonaws.com"
-# --- Notify when MR is merged ---
-notify_discord_merge:
- stage: notify
- image: alpine:3.20
+ # CloudFront invalidation
+ - |
+ STATUS="success"
+ if [ -n "$CLOUDFRONT_DISTRIBUTION_ID" ]; then
+ echo "Invalidating CloudFront cache..."
+ if ! aws cloudfront create-invalidation --distribution-id "$CLOUDFRONT_DISTRIBUTION_ID" --paths "/*"; then
+ echo "CloudFront invalidation failed."
+ STATUS="failed"
+ fi
+ else
+ echo "No CloudFront distribution specified — skipping invalidation"
+ fi
+
+ # Notifikasi Discord
+ - |
+ RUN_URL="${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}"
+
+ if [ "$CI_COMMIT_BRANCH" = "development" ]; then
+ ENVIRONMENT_NAME="WEB-LTI-DEV"
+ elif [ "$CI_COMMIT_BRANCH" = "master" ]; then
+ ENVIRONMENT_NAME="WEB-LTI-PROD"
+ else
+ ENVIRONMENT_NAME="UNKNOWN"
+ fi
+
+ if [ "$STATUS" = "success" ]; then
+ COLOR=3066993
+ TITLE="✅ Deployment ${ENVIRONMENT_NAME} Succeeded"
+ DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` completed successfully."
+ else
+ COLOR=15158332
+ TITLE="❌ Deployment ${ENVIRONMENT_NAME} Failed"
+ DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` encountered issues."
+ fi
+
+ jq -n \
+ --arg title "$TITLE" \
+ --arg desc "$DESC" \
+ --arg color "$COLOR" \
+ --arg repo "$CI_PROJECT_PATH" \
+ --arg 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:
- # Only run for merge request pipelines that are in merged state
- - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_STATE == "merged"'
+ - if: '$CI_COMMIT_BRANCH == "development"'
+ environment:
+ name: development
variables:
- WEBHOOK_URL: $DISCORD_WEBHOOK_URL
- before_script:
- - apk add --no-cache curl jq
- script: |
- MR_URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}"
+ NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id'
+ NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-api-sso.mbugroup.id'
+
+deploy:dev:
+ <<: *deploy_template
+ needs: ['build:dev']
+ rules:
+ - if: '$CI_COMMIT_BRANCH == "development"'
+ variables:
+ S3_BUCKET: 'dev-lti-erp.mbugroup.id'
+ AWS_REGION: 'ap-southeast-3'
+ CLOUDFRONT_DISTRIBUTION_ID: 'E1Z8XTA8XF1GIV'
+ environment:
+ name: development
+ url: https://dev-lti-erp.mbugroup.id
+# ====== PRODUCTION ======
+# build:production:
+# <<: *build_template
+# rules:
+# # pilih salah satu: pakai branch master ATAU pakai tags rilis
+# - if: '$CI_COMMIT_BRANCH == "master"'
+# # - if: '$CI_COMMIT_TAG' # kalau mau rilis via tag, uncomment ini dan hapus baris di atas
+# environment:
+# name: production
+
+# deploy:production:
+# <<: *deploy_template
+# needs: ["build:production"]
+# rules:
+# - if: '$CI_COMMIT_BRANCH == "master"'
+# # - if: '$CI_COMMIT_TAG' # selaras dengan rule di build:production
+# variables:
+# S3_BUCKET: "lti-erp.mbugroup.id"
+# CLOUDFRONT_DISTRIBUTION_ID: "ddfd"
+# environment:
+# name: production
+# url: https://royalgoldcapital.com
- jq -n \
- --arg repo "$CI_PROJECT_PATH" \
- --arg mr "#${CI_MERGE_REQUEST_IID}" \
- --arg url "$MR_URL" \
- --arg requestor "${CI_MERGE_REQUEST_AUTHOR}" \
- --arg source "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" \
- --arg target "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" \
- --arg title "$CI_MERGE_REQUEST_TITLE" \
- '{
- username: "CI Bot - FE",
- embeds: [{
- title: "✅ [LTI WEB CLIENT] Merge Request Merged",
- description: ($mr + " has been merged into " + $repo),
- url: $url,
- color: 3066993,
- fields: [
- {name: "Author", value: $requestor, inline: true},
- {name: "Source → Target", value: ($source + " → " + $target), inline: true},
- {name: "Title", value: $title}
- ]
- }]
- }' \
- | curl -sS -H "Content-Type: application/json" -d @- "$WEBHOOK_URL"
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..a3a2e197
--- /dev/null
+++ b/Dockerfile
@@ -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"]
\ No newline at end of file
diff --git a/docker-compose.yaml b/docker-compose.yaml
new file mode 100644
index 00000000..b89f441b
--- /dev/null
+++ b/docker-compose.yaml
@@ -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
diff --git a/src/app/globals.css b/src/app/globals.css
index c3d05c67..e50e020d 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -48,3 +48,8 @@
html {
scrollbar-gutter: initial;
}
+
+.react-select__menu-portal {
+ position: relative;
+ z-index: 99999 !important;
+}
diff --git a/src/app/marketing/sales-orders/add/page.tsx b/src/app/marketing/sales-orders/add/page.tsx
new file mode 100644
index 00000000..e60085ef
--- /dev/null
+++ b/src/app/marketing/sales-orders/add/page.tsx
@@ -0,0 +1,11 @@
+import SalesForm from '@/components/pages/marketing/sales-orders/form/SalesForm';
+
+const AddSalesOrder = () => {
+ return (
+
+
+
+ );
+};
+
+export default AddSalesOrder;
diff --git a/src/app/marketing/sales-orders/detail/edit/page.tsx b/src/app/marketing/sales-orders/detail/edit/page.tsx
new file mode 100644
index 00000000..86cafcb6
--- /dev/null
+++ b/src/app/marketing/sales-orders/detail/edit/page.tsx
@@ -0,0 +1,42 @@
+'use client';
+
+import SalesForm from '@/components/pages/marketing/sales-orders/form/SalesForm';
+import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
+import { MarketingApi } from '@/services/api/marketing/marketing';
+import { useRouter, useSearchParams } from 'next/navigation';
+import useSWR from 'swr';
+
+const EditSalesOrder = () => {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const soId = searchParams.get('salesOrderId');
+
+ const { data: marketing, isLoading: isLoading } = useSWR(soId, (id: number) =>
+ MarketingApi.getSingle(id)
+ );
+
+ if (!soId) {
+ router.back();
+
+ return (
+
+
+
+ );
+ }
+
+ if (!isLoading && (!marketing || isResponseError(marketing))) {
+ router.replace('/404');
+ return;
+ }
+ return (
+
+ {isLoading && }
+ {!isLoading && isResponseSuccess(marketing) && (
+
+ )}
+
+ );
+};
+export default EditSalesOrder;
diff --git a/src/app/production/chickin/add/layout.tsx b/src/app/marketing/sales-orders/detail/layout.tsx
similarity index 100%
rename from src/app/production/chickin/add/layout.tsx
rename to src/app/marketing/sales-orders/detail/layout.tsx
diff --git a/src/app/marketing/sales-orders/detail/page.tsx b/src/app/marketing/sales-orders/detail/page.tsx
new file mode 100644
index 00000000..22d2651c
--- /dev/null
+++ b/src/app/marketing/sales-orders/detail/page.tsx
@@ -0,0 +1,44 @@
+'use client';
+
+import SalesOrderDetail from '@/components/pages/marketing/sales-orders/detail/SalesOrderDetail';
+import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
+import { MarketingApi } from '@/services/api/marketing/marketing';
+import { useRouter, useSearchParams } from 'next/navigation';
+import useSWR from 'swr';
+
+const DetailSalesOrder = () => {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const soId = searchParams.get('salesOrderId');
+
+ const { data: marketing, isLoading: isLoading } = useSWR(soId, (id: number) =>
+ MarketingApi.getSingle(id)
+ );
+
+ if (!soId) {
+ router.back();
+
+ return (
+
+
+
+ );
+ }
+
+ if (!isLoading && (!marketing || isResponseError(marketing))) {
+ router.replace('/404');
+ return;
+ }
+
+ return (
+
+ {isLoading && }
+ {!isLoading && isResponseSuccess(marketing) && (
+
+ )}
+
+ );
+};
+
+export default DetailSalesOrder;
diff --git a/src/app/marketing/sales-orders/page.tsx b/src/app/marketing/sales-orders/page.tsx
new file mode 100644
index 00000000..3494b6a1
--- /dev/null
+++ b/src/app/marketing/sales-orders/page.tsx
@@ -0,0 +1,10 @@
+import SalesOrderTable from '@/components/pages/marketing/sales-orders/SalesOrderTable';
+
+const SalesOrder = () => {
+ return (
+
+
+
+ );
+};
+export default SalesOrder;
diff --git a/src/app/production/chickin/add/page.tsx b/src/app/production/chickin/add/page.tsx
deleted file mode 100644
index 3ef73396..00000000
--- a/src/app/production/chickin/add/page.tsx
+++ /dev/null
@@ -1,270 +0,0 @@
-'use client';
-
-import Button from '@/components/Button';
-import SelectInput, { OptionType } from '@/components/input/SelectInput';
-import Modal, { useModal } from '@/components/Modal';
-import ConfirmationModal from '@/components/modal/ConfirmationModal';
-import ChickinForm from '@/components/pages/production/chickin/form/ChickinForm';
-import Table from '@/components/Table';
-import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
-import { cn } from '@/lib/helper';
-import { ProjectFlockApi } from '@/services/api/production';
-import { useTableFilter } from '@/services/hooks/useTableFilter';
-import { BaseApiResponse } from '@/types/api/api-general';
-import { Kandang } from '@/types/api/master-data/kandang';
-import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
-import { Icon } from '@iconify/react';
-import { useRouter, useSearchParams } from 'next/navigation';
-import { useState } from 'react';
-
-import useSWR from 'swr';
-
-const AddChickin = () => {
- const router = useRouter();
- const searchParams = useSearchParams();
- const projectFlockId = searchParams.get('projectFlockId');
-
- // Tables Props
- const { state: tableFilterState } = useTableFilter({
- initial: { search: '' },
- paramMap: { page: 'page', pageSize: 'limit' },
- });
-
- // States
- const [selectedKandang, setSelectedKandang] = useState(
- undefined
- );
- const [projectFlockKandang, setProjectFlockKandang] =
- useState>();
- const [isLoadingProjectFlockKandang, setIsLoadingProjectFlockKandang] =
- useState(false);
- const [searchProjectFlock, setSearchProjectFlock] = useState('');
-
- // Fetch Data
- const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR(
- projectFlockId,
- (id: number) => ProjectFlockApi.getSingle(id)
- );
- const { data: listProjectFlock, isLoading: isLoadingListProjectFlock } =
- useSWR(
- `${ProjectFlockApi.basePath}?${new URLSearchParams({
- search: searchProjectFlock,
- }).toString()}`,
- ProjectFlockApi.getAllFetcher
- );
-
- const getProjectFlockKandangUrl = `/kandangs/lookup`;
- // Mapping Options
- const options = isResponseSuccess(listProjectFlock)
- ? listProjectFlock?.data.map((projectFlock) => {
- return {
- value: projectFlock.id,
- label: `${projectFlock?.flock?.name} - ${projectFlock?.category} - Periode ${projectFlock.period}`,
- };
- })
- : [];
-
- const chickinModal = useModal();
- const alertModal = useModal();
-
- if (!projectFlockId) {
- router.back();
-
- return (
-
-
-
- );
- }
-
- if (
- !isLoadingProjectFlock &&
- (!projectFlock || isResponseError(projectFlock))
- ) {
- router.replace('/404');
- return;
- }
-
- // Handle Function
- const handleChickinClick = async (kandang: Kandang) => {
- setIsLoadingProjectFlockKandang(true);
- setSelectedKandang(kandang);
- const ProjectFlockKandangRes = await ProjectFlockApi.customRequest<
- BaseApiResponse,
- 'GET'
- >(getProjectFlockKandangUrl, {
- method: 'GET',
- params: {
- project_flock_id: projectFlockId ?? 0,
- kandang_id: kandang.id,
- },
- });
- if (isResponseSuccess(ProjectFlockKandangRes)) {
- setProjectFlockKandang(ProjectFlockKandangRes);
- setIsLoadingProjectFlockKandang(false);
- if (
- ProjectFlockKandangRes.data.available_quantity &&
- ProjectFlockKandangRes.data.available_quantity > 0
- ) {
- chickinModal.openModal();
- } else {
- alertModal.openModal();
- }
- }
- };
- const handleAfterSubmit = () => {
- chickinModal.closeModal();
- router.push('/production/chickin');
- };
-
- return (
- <>
- {isResponseSuccess(projectFlock) && (
- <>
-
-
-
- data={projectFlock.data?.kandangs}
- columns={[
- {
- header: '#',
- cell: (props) =>
- tableFilterState.pageSize * (tableFilterState.page - 1) +
- props.row.index +
- 1,
- },
- {
- accessorKey: 'name',
- header: 'Nama Kandang',
- },
- {
- header: 'Aksi',
- cell: (props) => {
- return (
- <>
-
- >
- );
- },
- },
- ]}
- page={undefined}
- className={{
- containerClassName: cn({
- 'mb-20':
- isResponseSuccess(projectFlock) &&
- projectFlock.data?.kandangs?.length === 0,
- }),
- tableWrapperClassName: 'overflow-x-auto min-h-full!',
- tableClassName: 'font-inter w-full table-auto min-h-full!',
- headerRowClassName: 'border-b border-b-gray-200',
- headerColumnClassName:
- 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
- bodyRowClassName: 'border-b border-b-gray-200',
- bodyColumnClassName:
- 'px-6 py-3 last:flex last:flex-row last:justify-end',
- paginationClassName: 'hidden',
- }}
- />
-
-
-
-
- Chickin Kandang - {selectedKandang?.name}
-
-
-
- {isResponseSuccess(projectFlockKandang) &&
- !isLoadingProjectFlockKandang && (
-
- )}
-
- {
- alertModal.closeModal();
- },
- }}
- />
- >
- )}
- >
- );
-};
-
-export default AddChickin;
diff --git a/src/app/production/chickin/detail/layout.tsx b/src/app/production/project-flock/chickin/add/kandang/layout.tsx
similarity index 100%
rename from src/app/production/chickin/detail/layout.tsx
rename to src/app/production/project-flock/chickin/add/kandang/layout.tsx
diff --git a/src/app/production/project-flock/chickin/add/kandang/page.tsx b/src/app/production/project-flock/chickin/add/kandang/page.tsx
new file mode 100644
index 00000000..a22039d1
--- /dev/null
+++ b/src/app/production/project-flock/chickin/add/kandang/page.tsx
@@ -0,0 +1,60 @@
+'use client';
+
+import ChickinForm from '@/components/pages/production/chickin/form/ChickinForm';
+import { isResponseSuccess } from '@/lib/api-helper';
+import { ProjectFlockKandangApi } from '@/services/api/production';
+import { useRouter, useSearchParams } from 'next/navigation';
+import useSWR from 'swr';
+
+export default function AddChickinKandang() {
+ const searchParams = useSearchParams();
+ const projectFlockKandangId = searchParams.get('projectFlockKandangId');
+ const projectFlockId = searchParams.get('projectFlockId');
+ const router = useRouter();
+
+ const {
+ data: projectFlockKandang,
+ isLoading: isLoading,
+ mutate: refreshProjectFlockKandang,
+ } = useSWR(
+ `get-single-project-flock-kandang/${projectFlockKandangId}`,
+ async () =>
+ ProjectFlockKandangApi.getSingle(
+ parseInt(projectFlockKandangId as string)
+ )
+ );
+
+ if (!projectFlockKandangId) {
+ router.push(`/production/chickin/add?projectFlockId=${projectFlockId}`);
+ return (
+
+
+
+ );
+ }
+
+ if (!isLoading && !projectFlockKandang) {
+ router.replace('/404');
+ return;
+ }
+
+ const handleAfterSubmit = () => {
+ refreshProjectFlockKandang();
+ };
+
+ return (
+ <>
+
+ {isLoading && }
+ {!isLoading &&
+ isResponseSuccess(projectFlockKandang) &&
+ projectFlockId && (
+
+ )}
+
+ >
+ );
+}
diff --git a/src/app/production/project-flock/chickin/add/layout.tsx b/src/app/production/project-flock/chickin/add/layout.tsx
new file mode 100644
index 00000000..7220dfa1
--- /dev/null
+++ b/src/app/production/project-flock/chickin/add/layout.tsx
@@ -0,0 +1,11 @@
+import SuspenseHelper from '@/components/helper/SuspenseHelper';
+
+const Layout = ({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) => {
+ return {children};
+};
+
+export default Layout;
diff --git a/src/app/production/project-flock/chickin/add/page.tsx b/src/app/production/project-flock/chickin/add/page.tsx
new file mode 100644
index 00000000..3ca09c89
--- /dev/null
+++ b/src/app/production/project-flock/chickin/add/page.tsx
@@ -0,0 +1,24 @@
+'use client';
+
+import { FormHeader } from '@/components/helper/form/FormHeader';
+import ProjectFlockChickinDetail from '@/components/pages/production/project-flock/chickin/ProjectFlockChickinDetail';
+import { useSearchParams } from 'next/navigation';
+
+const AddChickin = () => {
+ const searchParams = useSearchParams();
+ const projectFlockId = searchParams.get('projectFlockId');
+
+ return (
+ <>
+
+ >
+ );
+};
+
+export default AddChickin;
diff --git a/src/app/production/project-flock/chickin/detail/layout.tsx b/src/app/production/project-flock/chickin/detail/layout.tsx
new file mode 100644
index 00000000..7220dfa1
--- /dev/null
+++ b/src/app/production/project-flock/chickin/detail/layout.tsx
@@ -0,0 +1,11 @@
+import SuspenseHelper from '@/components/helper/SuspenseHelper';
+
+const Layout = ({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) => {
+ return {children};
+};
+
+export default Layout;
diff --git a/src/app/production/chickin/detail/page.tsx b/src/app/production/project-flock/chickin/detail/page.tsx
similarity index 94%
rename from src/app/production/chickin/detail/page.tsx
rename to src/app/production/project-flock/chickin/detail/page.tsx
index be8c5332..daea0f0a 100644
--- a/src/app/production/chickin/detail/page.tsx
+++ b/src/app/production/project-flock/chickin/detail/page.tsx
@@ -6,7 +6,7 @@ import Modal, { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import ChickinForm from '@/components/pages/production/chickin/form/ChickinForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
-import { ChickinApi } from '@/services/api/production';
+import { ChickinApi } from '@/services/api/production/chickin';
import { BaseApiResponse } from '@/types/api/api-general';
import {
Chickin,
@@ -170,8 +170,8 @@ const DetailChickin = () => {
Flock
{
- chickin.data.project_flock_kandang?.project_flock.flock
- .name
+ chickin?.data?.project_flock_kandang?.project_flock?.flock
+ ?.name
}
@@ -225,8 +225,8 @@ const DetailChickin = () => {
Flock Kandang
{
- chickin.data.project_flock_kandang?.project_flock.flock
- .name
+ chickin?.data?.project_flock_kandang?.project_flock?.flock
+ ?.name
}{' '}
- {chickin.data.project_flock_kandang?.kandang.name}
@@ -280,7 +280,7 @@ const DetailChickin = () => {
{
/>
- {
- refreshChickin();
- chickinModal.closeModal();
- }}
- />
{
text={`Apakah anda yakin ingin ${
approvalAction == 'APPROVED' ? 'approve' : 'reject'
} chickin berikut? (${
- chickin?.data.project_flock_kandang?.project_flock.flock.name
+ chickin?.data?.project_flock_kandang?.project_flock?.flock?.name
} - ${chickin?.data.project_flock_kandang?.kandang.name})?`}
secondaryButton={{
text: 'Tidak',
diff --git a/src/app/production/chickin/page.tsx b/src/app/production/project-flock/chickin/page.tsx
similarity index 100%
rename from src/app/production/chickin/page.tsx
rename to src/app/production/project-flock/chickin/page.tsx
diff --git a/src/app/production/project-flock/detail/edit/page.tsx b/src/app/production/project-flock/detail/edit/page.tsx
index 7576cc27..f55ce601 100644
--- a/src/app/production/project-flock/detail/edit/page.tsx
+++ b/src/app/production/project-flock/detail/edit/page.tsx
@@ -2,7 +2,7 @@
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
-import { ProjectFlockApi } from '@/services/api/production';
+import { ProjectFlockApi } from '@/services/api/production/project-flock';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
@@ -12,10 +12,11 @@ const ProjectFlockEdit = () => {
const projectFlockId = searchParams.get('projectFlockId');
- const { data: projectFlock, isLoading: isLoadingCostumer } = useSWR(
- projectFlockId,
- (id: number) => ProjectFlockApi.getSingle(id)
- );
+ const {
+ data: projectFlock,
+ isLoading: isLoadingProjectFlock,
+ mutate: refreshProjectFlocks,
+ } = useSWR(projectFlockId, (id: number) => ProjectFlockApi.getSingle(id));
if (!projectFlockId) {
router.back();
@@ -27,17 +28,20 @@ const ProjectFlockEdit = () => {
);
}
- if (!isLoadingCostumer && (!projectFlock || isResponseError(projectFlock))) {
+ if (
+ !isLoadingProjectFlock &&
+ (!projectFlock || isResponseError(projectFlock))
+ ) {
router.replace('/404');
return;
}
return (
-
- {isLoadingCostumer && (
+
+ {isLoadingProjectFlock && (
)}
- {!isLoadingCostumer && isResponseSuccess(projectFlock) && (
+ {!isLoadingProjectFlock && isResponseSuccess(projectFlock) && (
)}
diff --git a/src/app/production/project-flock/detail/page.tsx b/src/app/production/project-flock/detail/page.tsx
index 6cf694c0..91d4dfd5 100644
--- a/src/app/production/project-flock/detail/page.tsx
+++ b/src/app/production/project-flock/detail/page.tsx
@@ -2,7 +2,7 @@
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
-import { ProjectFlockApi } from '@/services/api/production';
+import { ProjectFlockApi } from '@/services/api/production/project-flock';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
@@ -37,11 +37,11 @@ const ProjectFlockDetail = () => {
}
return (
-
+
{isLoadingProjectFlock && (
)}
- {!isLoadingProjectFlock && isResponseSuccess(projectFlock) && (
+ {isResponseSuccess(projectFlock) && (
{
const router = useRouter();
const searchParams = useSearchParams();
@@ -114,33 +29,33 @@ const TransferToLayingEdit = () => {
);
}
- // TODO: remove dummy data and integrate with real API
if (
!isLoadingTransferToLaying &&
- (!transferToLaying ||
- (isResponseError(transferToLaying) && !DUMMY_TRANSFER_TO_LAYING_EDIT))
+ (!transferToLaying || isResponseError(transferToLaying))
) {
router.replace('/404');
return;
}
+ if (
+ isResponseSuccess(transferToLaying) &&
+ transferToLaying.data.approval.step_number === 2
+ ) {
+ router.replace('/production/transfer-to-laying');
+ return;
+ }
+
return (
{isLoadingTransferToLaying && (
)}
- {/* {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
+ {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
- )} */}
-
- {/* TODO: remove this dummy data and integrate to real API */}
-
+ )}
);
};
diff --git a/src/app/production/transfer-to-laying/detail/page.tsx b/src/app/production/transfer-to-laying/detail/page.tsx
index de5426c8..9ff6ed5e 100644
--- a/src/app/production/transfer-to-laying/detail/page.tsx
+++ b/src/app/production/transfer-to-laying/detail/page.tsx
@@ -8,91 +8,6 @@ import TransferToLayingForm from '@/components/pages/production/transfer-to-layi
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
-import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
-
-// TODO: delete dummy data
-const DUMMY_TRANSFER_TO_LAYING_DETAIL: TransferToLaying = {
- id: 1,
- transfer_date: '2025-10-14',
- flock_source: {
- id: 1,
- name: 'Flock asal test',
- },
- flock_destination: {
- id: 2,
- name: 'Flock tujuan destination',
- },
- quantity: 10,
- kandangs: [
- {
- kandang: {
- id: 1,
- 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',
- status: 'ACTIVE',
- location: {
- id: 1,
- name: 'test location',
- address: 'test address 1',
- area: { id: 1, name: 'test area 1' },
- },
- pic: {
- id: 1,
- id_user: 2,
- email: 'test@gmail.com',
- name: 'test',
- },
- created_user: {
- id: 1,
- id_user: 2,
- email: 'test@gmail.com',
- name: 'test',
- },
- created_at: '14-10-2025',
- updated_at: '14-10-2025',
- },
- quantity: 2,
- },
- ],
- reason: 'Test alasan',
-
- created_user: {
- id: 1,
- id_user: 2,
- email: 'test@gmail.com',
- name: 'test',
- },
- created_at: '14-10-2025',
- updated_at: '14-10-2025',
-};
-
const TransferToLayingDetail = () => {
const router = useRouter();
const searchParams = useSearchParams();
@@ -114,11 +29,9 @@ const TransferToLayingDetail = () => {
);
}
- // TODO: remove dummy data and integrate with real API
if (
!isLoadingTransferToLaying &&
- (!transferToLaying ||
- (isResponseError(transferToLaying) && !DUMMY_TRANSFER_TO_LAYING_DETAIL))
+ (!transferToLaying || isResponseError(transferToLaying))
) {
router.replace('/404');
return;
@@ -129,18 +42,13 @@ const TransferToLayingDetail = () => {
{isLoadingTransferToLaying && (
)}
- {/* {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
+
+ {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
- )} */}
-
- {/* TODO: remove this dummy data and integrate to real API */}
-
+ )}
);
};
diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx
index a84c1827..a242b1e4 100644
--- a/src/components/Modal.tsx
+++ b/src/components/Modal.tsx
@@ -1,6 +1,13 @@
'use client';
-import { ReactNode, RefObject, useCallback, useRef, useState } from 'react';
+import {
+ ReactNode,
+ RefObject,
+ useCallback,
+ useEffect,
+ useRef,
+ useState,
+} from 'react';
import { cn } from '@/lib/helper';
export const useModal = () => {
@@ -8,31 +15,34 @@ export const useModal = () => {
const [open, setOpen] = useState(false);
const openModal = useCallback(() => {
+ if (!ref.current) return;
+ ref.current.show();
setOpen(true);
-
- ref.current?.showModal();
}, []);
const closeModal = useCallback(() => {
+ if (!ref.current) return;
+ ref.current.close();
setOpen(false);
- ref.current?.close();
}, []);
const toggle = useCallback(() => {
- if (open) {
- closeModal();
- } else {
- openModal();
- }
+ open ? closeModal() : openModal();
}, [open, closeModal, openModal]);
- if (ref.current) {
- ref.current.addEventListener('close', () => {
- closeModal();
- });
- }
+ useEffect(() => {
+ const dialog = ref.current;
+ if (!dialog) return;
- return { ref, open, setOpen, openModal, closeModal, toggle } as const;
+ const handleClose = () => setOpen(false);
+ dialog.addEventListener('close', handleClose);
+
+ return () => {
+ dialog.removeEventListener('close', handleClose);
+ };
+ }, []);
+
+ return { ref, open, openModal, closeModal, toggle } as const;
};
interface ModalProps {
@@ -46,15 +56,19 @@ interface ModalProps {
}
const Modal = ({ ref, children, closeOnBackdrop, className }: ModalProps) => {
- return (
-