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/.husky/pre-commit b/.husky/pre-commit index 66ff6a67..e7bb3165 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,3 @@ +npm run format npm run lint npm run build 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/production/transfer-to-laying/detail/edit/page.tsx b/src/app/production/transfer-to-laying/detail/edit/page.tsx index 20c7373f..d5498e08 100644 --- a/src/app/production/transfer-to-laying/detail/edit/page.tsx +++ b/src/app/production/transfer-to-laying/detail/edit/page.tsx @@ -8,93 +8,6 @@ import TransferToLayingForm from '@/components/pages/production/transfer-to-layi import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { TransferToLaying } from '@/types/api/production/transfer-to-laying'; - -// TODO: delete dummy data -const DUMMY_TRANSFER_TO_LAYING_EDIT: TransferToLaying = { - id: 1, - transfer_date: '2025-10-14', - flock_source: { - id: 1, - name: 'Flock asal test', - }, - flock_destination: { - id: 2, - name: 'Flock tujuan destination', - }, - quantity: 10, - kandangs: [ - { - kandang: { - id: 1, - capacity: 1000, - name: 'Kandang test', - status: 'ACTIVE', - location: { - id: 1, - name: 'test location', - address: 'test address 1', - area: { id: 1, name: 'test area 1' }, - }, - pic: { - id: 1, - id_user: 2, - email: 'test@gmail.com', - name: 'test', - }, - created_user: { - id: 1, - id_user: 2, - email: 'test@gmail.com', - name: 'test', - }, - created_at: '14-10-2025', - updated_at: '14-10-2025', - }, - quantity: 8, - }, - { - kandang: { - id: 1, - name: 'Kandang test 2', - capacity: 3000, - status: 'ACTIVE', - location: { - id: 1, - name: 'test location', - address: 'test address 1', - area: { id: 1, name: 'test area 1' }, - }, - pic: { - id: 1, - id_user: 2, - email: 'test@gmail.com', - name: 'test', - }, - created_user: { - id: 1, - id_user: 2, - email: 'test@gmail.com', - name: 'test', - }, - created_at: '14-10-2025', - updated_at: '14-10-2025', - }, - quantity: 2, - }, - ], - reason: 'Test alasan', - - created_user: { - id: 1, - id_user: 2, - email: 'test@gmail.com', - name: 'test', - }, - created_at: '14-10-2025', - updated_at: '14-10-2025', -}; - const TransferToLayingEdit = () => { const router = useRouter(); const searchParams = useSearchParams(); @@ -116,33 +29,33 @@ const TransferToLayingEdit = () => { ); } - // TODO: remove dummy data and integrate with real API if ( !isLoadingTransferToLaying && - (!transferToLaying || - (isResponseError(transferToLaying) && !DUMMY_TRANSFER_TO_LAYING_EDIT)) + (!transferToLaying || isResponseError(transferToLaying)) ) { router.replace('/404'); return; } + if ( + isResponseSuccess(transferToLaying) && + transferToLaying.data.approval.step_number === 2 + ) { + router.replace('/production/transfer-to-laying'); + return; + } + return (
{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 3f7dbe56..9ff6ed5e 100644 --- a/src/app/production/transfer-to-laying/detail/page.tsx +++ b/src/app/production/transfer-to-laying/detail/page.tsx @@ -8,93 +8,6 @@ import TransferToLayingForm from '@/components/pages/production/transfer-to-layi import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { TransferToLaying } from '@/types/api/production/transfer-to-laying'; - -// TODO: delete dummy data -const DUMMY_TRANSFER_TO_LAYING_DETAIL: TransferToLaying = { - id: 1, - transfer_date: '2025-10-14', - flock_source: { - id: 1, - name: 'Flock asal test', - }, - flock_destination: { - id: 2, - name: 'Flock tujuan destination', - }, - quantity: 10, - kandangs: [ - { - kandang: { - id: 1, - capacity: 1000, - name: 'Kandang test', - status: 'ACTIVE', - location: { - id: 1, - name: 'test location', - address: 'test address 1', - area: { id: 1, name: 'test area 1' }, - }, - pic: { - id: 1, - id_user: 2, - email: 'test@gmail.com', - name: 'test', - }, - created_user: { - id: 1, - id_user: 2, - email: 'test@gmail.com', - name: 'test', - }, - created_at: '14-10-2025', - updated_at: '14-10-2025', - }, - quantity: 8, - }, - { - kandang: { - id: 1, - capacity: 3000, - name: 'Kandang test 2', - status: 'ACTIVE', - location: { - id: 1, - name: 'test location', - address: 'test address 1', - area: { id: 1, name: 'test area 1' }, - }, - pic: { - id: 1, - id_user: 2, - email: 'test@gmail.com', - name: 'test', - }, - created_user: { - id: 1, - id_user: 2, - email: 'test@gmail.com', - name: 'test', - }, - created_at: '14-10-2025', - updated_at: '14-10-2025', - }, - quantity: 2, - }, - ], - reason: 'Test alasan', - - created_user: { - id: 1, - id_user: 2, - email: 'test@gmail.com', - name: 'test', - }, - created_at: '14-10-2025', - updated_at: '14-10-2025', -}; - const TransferToLayingDetail = () => { const router = useRouter(); const searchParams = useSearchParams(); @@ -116,11 +29,9 @@ const TransferToLayingDetail = () => { ); } - // TODO: remove dummy data and integrate with real API if ( !isLoadingTransferToLaying && - (!transferToLaying || - (isResponseError(transferToLaying) && !DUMMY_TRANSFER_TO_LAYING_DETAIL)) + (!transferToLaying || isResponseError(transferToLaying)) ) { router.replace('/404'); return; @@ -131,18 +42,13 @@ const TransferToLayingDetail = () => { {isLoadingTransferToLaying && ( )} - {/* {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && ( + + {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && ( - )} */} - - {/* TODO: remove this dummy data and integrate to real API */} - + )} ); }; diff --git a/src/components/Table.tsx b/src/components/Table.tsx index d3498e33..b02dd3b5 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -13,6 +13,7 @@ import { FilterFn, SortingState, OnChangeFn, + Row, } from '@tanstack/react-table'; import { rankItem } from '@tanstack/match-sorter-utils'; import { Icon } from '@iconify/react'; @@ -50,6 +51,7 @@ export interface TableProps { manualSorting?: boolean; rowSelection?: Record; setRowSelection?: OnChangeFn>; + enableRowSelection?: boolean | ((row: Row) => boolean); } const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; @@ -90,6 +92,7 @@ const Table = ({ manualSorting = false, rowSelection, setRowSelection, + enableRowSelection, }: TableProps) => { const isServerSideTable = totalItems !== undefined && @@ -150,6 +153,10 @@ const Table = ({ tableOptions.getRowId = (row) => (row as { id: string }).id; } + if (enableRowSelection !== undefined) { + tableOptions.enableRowSelection = enableRowSelection; + } + const table = useReactTable(tableOptions); const { setPageSize } = table; diff --git a/src/components/input/DateInput.tsx b/src/components/input/DateInput.tsx index 92d28397..7317b038 100644 --- a/src/components/input/DateInput.tsx +++ b/src/components/input/DateInput.tsx @@ -7,10 +7,10 @@ import { useState, } from 'react'; import { cn, formatDate } from '@/lib/helper'; -import Modal, { useModal } from '../Modal'; +import Modal, { useModal } from '@/components/Modal'; import { DateRange, DayPicker, Matcher } from 'react-day-picker'; import 'react-day-picker/dist/style.css'; -import Button from '../Button'; +import Button from '@/components/Button'; import { Icon } from '@iconify/react'; export interface DateInputProps { diff --git a/src/components/modal/ConfirmationModal.tsx b/src/components/modal/ConfirmationModal.tsx index 04c221e6..683345f5 100644 --- a/src/components/modal/ConfirmationModal.tsx +++ b/src/components/modal/ConfirmationModal.tsx @@ -9,7 +9,7 @@ import Button from '@/components/Button'; import { cn } from '@/lib/helper'; import { Color } from '@/types/theme'; -interface ConfirmationModalProps { +export interface ConfirmationModalProps { ref: RefObject; type?: 'info' | 'success' | 'error'; text?: string; @@ -30,6 +30,7 @@ interface ConfirmationModalProps { modal?: string; modalBox?: string; }; + children?: React.ReactNode; } const ConfirmationModal = ({ @@ -40,6 +41,7 @@ const ConfirmationModal = ({ primaryButton, secondaryButton, className, + children, }: ConfirmationModalProps) => { const closeModalHandler = () => { ref.current?.close(); @@ -90,6 +92,8 @@ const ConfirmationModal = ({ {text ?? 'Apakah anda yakin ingin melakukan hal ini?'}

+ {children &&
{children}
} +
{secondaryButton && secondaryButton.text && (