From 4a1464185b0cb52d39817bc4550b7715369401b2 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Feb 2026 10:30:34 +0700 Subject: [PATCH 01/15] refactor(FE): Replace approval history modal with status badge --- .../production/recording/RecordingTable.tsx | 310 ++++-------------- 1 file changed, 61 insertions(+), 249 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index cd98b597..6beb0954 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -7,14 +7,12 @@ import React, { useEffect, useRef, } from 'react'; -import { RefObject } from 'react'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; import { SortingState, CellContext } from '@tanstack/react-table'; import { cn, formatDate, formatNumber } from '@/lib/helper'; import RequirePermission from '@/components/helper/RequirePermission'; import { useModal } from '@/components/Modal'; -import Modal from '@/components/Modal'; import Button from '@/components/Button'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; @@ -28,14 +26,55 @@ import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import { type Recording } from '@/types/api/production/recording'; import { RecordingApi } from '@/services/api/production'; -import { ApprovalApi } from '@/services/api/approval'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import toast from 'react-hot-toast'; import Badge from '@/components/Badge'; +import StatusBadge from '@/components/helper/StatusBadge'; import CheckboxInput from '@/components/input/CheckboxInput'; import { useUiStore } from '@/stores/ui/ui.store'; -import { BaseApproval, BaseApiResponse } from '@/types/api/api-general'; + +// ===== STATUS BADGE UTILITIES ===== +const statusTextMap: Record = { + APPROVED: 'Disetujui', + Disetujui: 'Disetujui', + REJECTED: 'Ditolak', + Ditolak: 'Ditolak', + CREATED: 'Dibuat', + UPDATED: 'Diperbarui', +}; + +const getStatusText = (status: string): string => { + return statusTextMap[status] || status; +}; + +const statusBadgeColorMap: Record< + string, + 'success' | 'error' | 'neutral' | 'info' | 'warning' +> = { + APPROVED: 'success', + Disetujui: 'success', + approved: 'success', + disetujui: 'success', + REJECTED: 'error', + Ditolak: 'error', + rejected: 'error', + ditolak: 'error', + CREATED: 'neutral', + Dibuat: 'neutral', + created: 'neutral', + dibuat: 'neutral', + UPDATED: 'warning', + Diperbarui: 'warning', + updated: 'warning', + diperbarui: 'warning', +}; + +const getStatusBadgeColor = ( + status: string +): 'success' | 'error' | 'neutral' | 'info' | 'warning' => { + return statusBadgeColorMap[status] || 'neutral'; +}; const RowOptionsMenu = ({ type = 'dropdown', @@ -135,221 +174,6 @@ const RowOptionsMenu = ({ ); }; -const ApprovalHistoryModal = ({ - ref, - currentApproval, - module_name = 'RECORDINGS', - module_id, -}: { - ref: RefObject; - currentApproval?: BaseApproval; - module_name?: string; - module_id?: number | undefined; -}) => { - const [isModalOpen, setIsModalOpen] = useState(false); - - const approvalHistoryUrl = useMemo(() => { - if (!isModalOpen) return null; - const params = new URLSearchParams({ - module_name: module_name, - group_step_number: 'true', - }); - - if (module_id) { - params.append('module_id', module_id.toString()); - } - - return `${ApprovalApi.basePath}?${params.toString()}`; - }, [module_name, module_id, isModalOpen]); - - type GroupedApprovalResponse = { - step_number: number; - step_name: string; - approvals: BaseApproval[]; - }; - - const fetchGroupedApprovals = async ( - url: string - ): Promise> => { - return (await ApprovalApi.getAllFetcher(url)) as BaseApiResponse< - GroupedApprovalResponse[] - >; - }; - - const { data: approvalHistoryData, isLoading } = useSWR< - BaseApiResponse - >(approvalHistoryUrl, fetchGroupedApprovals); - - useEffect(() => { - const checkModalOpen = () => { - const isOpen = ref.current?.open || false; - setIsModalOpen(isOpen); - }; - - checkModalOpen(); - - const observer = new MutationObserver(checkModalOpen); - if (ref.current) { - observer.observe(ref.current, { - attributes: true, - attributeFilter: ['open'], - }); - } - - return () => observer.disconnect(); - }, [ref]); - - const approvalHistory = useMemo(() => { - if (!approvalHistoryData || approvalHistoryData.status !== 'success') - return []; - - const groupedData = approvalHistoryData.data || []; - const flattenedApprovals: BaseApproval[] = []; - - groupedData.forEach((group) => { - group.approvals.forEach((approval) => { - flattenedApprovals.push(approval); - }); - }); - - return flattenedApprovals; - }, [approvalHistoryData]); - - const closeModalHandler = () => { - ref.current?.close(); - }; - - return ( - -
- {/* Header */} -
-

Riwayat Approval

- -
- - {isLoading ? ( -
- -
- ) : ( - <> - {/* Current Status */} - {currentApproval && ( -
-

Status Saat Ini

-
- - {currentApproval.step_name} - - - {currentApproval.action === 'APPROVED' && 'Disetujui'} - {currentApproval.action === 'REJECTED' && 'Ditolak'} - {currentApproval.action === 'CREATED' && 'Dibuat'} - {currentApproval.action === 'UPDATED' && 'Diperbarui'} - -
- {currentApproval.notes && ( -

- Catatan:{' '} - {currentApproval.notes} -

- )} -

- Oleh: {currentApproval.action_by.name} โ€ข{' '} - {formatDate(currentApproval.action_at, 'DD MMMM YYYY HH:mm')} -

-
- )} - - {/* Full History */} - {approvalHistory.length > 0 && ( -
-

Riwayat Lengkap

-
- - - - - - - - - - - - {approvalHistory - .sort( - (a: BaseApproval, b: BaseApproval) => - new Date(b.action_at).getTime() - - new Date(a.action_at).getTime() - ) - .map((approval: BaseApproval, index: number) => ( - - - - - - - - ))} - -
TahapAksiCatatanOlehWaktu
{approval.step_name} - - {approval.action === 'APPROVED' && 'Disetujui'} - {approval.action === 'REJECTED' && 'Ditolak'} - {approval.action === 'CREATED' && 'Dibuat'} - {approval.action === 'UPDATED' && 'Diperbarui'} - - -
- {approval.notes || '-'} -
-
{approval.action_by.name} - {formatDate( - approval.action_at, - 'DD MMMM YYYY HH:mm' - )} -
-
-
- )} - - )} -
-
- ); -}; - const RecordingTable = () => { const { searchValue, setSearchValue, resetSearchValue } = useUiStore(); const previousPathRef = useRef(null); @@ -395,7 +219,6 @@ const RecordingTable = () => { const singleDeleteModal = useModal(); const approveModal = useModal(); const rejectModal = useModal(); - const approvalHistoryModal = useModal(); const { data: recordings, @@ -1032,32 +855,22 @@ const RecordingTable = () => { const approval = props.row.original.approval; if (!approval) return '-'; - const statusColor = - approval.action === 'APPROVED' - ? 'success' - : approval.action === 'REJECTED' - ? 'error' - : approval.action === 'UPDATED' - ? 'warning' - : 'info'; + const status = approval.action; + const statusColor = getStatusBadgeColor(status); - const openApprovalHistory = () => { - setSelectedRecording(props.row.original); - approvalHistoryModal.openModal(); - }; + const statusText = + status === 'REJECTED' + ? 'Ditolak' + : approval.step_name || getStatusText(status); return ( - - {approval.step_name || approval.action} - + /> ); }, }, @@ -1208,7 +1021,10 @@ const RecordingTable = () => { text={`Apakah anda yakin ingin approve data recording ini (${eligibleRowIds.length} data dari ${selectedRowIds.length} yang dipilih)?`} secondaryButton={{ text: 'Tidak', - onClick: () => setApprovalNotes(''), + onClick: () => { + setApprovalNotes(''); + approveModal.closeModal(); + }, }} primaryButton={{ text: 'Ya', @@ -1226,7 +1042,10 @@ const RecordingTable = () => { text={`Apakah anda yakin ingin reject data recording ini (${eligibleRowIds.length} data dari ${selectedRowIds.length} yang dipilih)?`} secondaryButton={{ text: 'Tidak', - onClick: () => setApprovalNotes(''), + onClick: () => { + setApprovalNotes(''); + rejectModal.closeModal(); + }, }} primaryButton={{ text: 'Ya', @@ -1237,13 +1056,6 @@ const RecordingTable = () => { placeholder='(Opsional) Tambahkan catatan untuk reject ini...' rows={3} /> - - ); }; From 8fa2a444f0ae455d19dce59a0d79b8d4397f3620 Mon Sep 17 00:00:00 2001 From: kris Date: Mon, 2 Feb 2026 03:42:31 +0000 Subject: [PATCH 02/15] Update .gitlab-ci.yml file --- .gitlab-ci.yml | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e80a7e02..cfef03d0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -26,14 +26,23 @@ default: script: - echo "Installing dependencies..." - npm ci --no-audit --no-fund + - echo "Build env used:" - echo "NEXT_PUBLIC_LTI_URL=$NEXT_PUBLIC_LTI_URL" - echo "NEXT_PUBLIC_SSO_LOGIN_URL=$NEXT_PUBLIC_SSO_LOGIN_URL" - echo "NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL" - - echo "Building Next.js static export..." + - echo "NEXT_PUBLIC_CLIENT_ID=$NEXT_PUBLIC_CLIENT_ID" + + # โœ… Build Next + - echo "Building Next.js..." - npx next build + + # โœ… Export static to out/ (ini yang sebelumnya missing) + - echo "Exporting Next.js static site to ./out ..." + - npx next export -o out + + # build-info.json tetap kamu simpan di out/ - | - mkdir -p out cat < out/build-info.json { "commit": "$CI_COMMIT_SHORT_SHA", @@ -41,9 +50,17 @@ default: "built_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")", "NEXT_PUBLIC_LTI_URL": "$NEXT_PUBLIC_LTI_URL", "NEXT_PUBLIC_SSO_LOGIN_URL": "$NEXT_PUBLIC_SSO_LOGIN_URL", - "NEXT_PUBLIC_API_BASE_URL": "$NEXT_PUBLIC_API_BASE_URL" + "NEXT_PUBLIC_API_BASE_URL": "$NEXT_PUBLIC_API_BASE_URL", + "NEXT_PUBLIC_CLIENT_ID": "$NEXT_PUBLIC_CLIENT_ID" } EOF + + # โœ… Verifikasi cepat (biar ketahuan out/ isinya bener) + - echo "===== out/ preview =====" + - ls -lah out | head -n 50 + - echo "===== _next assets preview =====" + - ls -lah out/_next/static/chunks 2>/dev/null | head -n 30 || true + artifacts: name: 'out-$CI_COMMIT_SHORT_SHA' paths: From 3ca4b324d316f470708803e5da79f1dc77977585 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Feb 2026 10:46:20 +0700 Subject: [PATCH 03/15] refactor(FE): Unify status badge logic and use StatusBadge --- .../production/recording/RecordingTable.tsx | 5 +- .../pages/purchase/PurchaseTable.tsx | 99 +++++++++++++------ 2 files changed, 72 insertions(+), 32 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 6beb0954..a461e2d8 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -858,10 +858,7 @@ const RecordingTable = () => { const status = approval.action; const statusColor = getStatusBadgeColor(status); - const statusText = - status === 'REJECTED' - ? 'Ditolak' - : approval.step_name || getStatusText(status); + const statusText = approval.step_name || getStatusText(status); return ( = { + APPROVED: 'Disetujui', + Disetujui: 'Disetujui', + REJECTED: 'Ditolak', + Ditolak: 'Ditolak', + CREATED: 'Dibuat', + UPDATED: 'Diperbarui', +}; + +const getStatusText = (status: string): string => { + return statusTextMap[status] || status; +}; + +const statusBadgeColorMap: Record< + string, + 'success' | 'error' | 'neutral' | 'info' | 'warning' | 'primary' +> = { + APPROVED: 'success', + Disetujui: 'success', + approved: 'success', + disetujui: 'success', + REJECTED: 'error', + Ditolak: 'error', + rejected: 'error', + ditolak: 'error', + CREATED: 'neutral', + Dibuat: 'neutral', + created: 'neutral', + dibuat: 'neutral', + UPDATED: 'warning', + Diperbarui: 'warning', + updated: 'warning', + diperbarui: 'warning', +}; + +const getStatusBadgeColor = ( + status: string +): 'success' | 'error' | 'neutral' | 'info' | 'warning' | 'primary' => { + return statusBadgeColorMap[status] || 'neutral'; +}; + // ===== INTERFACES ===== interface RowOptionsMenuProps { type: 'dropdown' | 'collapse'; @@ -160,48 +202,48 @@ const PurchaseTable = () => { const approval = props.row.original.latest_approval; if (!approval) return '-'; - const isRejected = approval.action === 'REJECTED'; + const status = approval.action; let statusColor: | 'warning' | 'success' | 'neutral' | 'error' - | 'primary' - | 'info' = 'neutral'; + | 'info' + | 'primary' = 'neutral'; - switch (approval.step_number) { - case 1: - statusColor = 'neutral'; - break; - case 2: - statusColor = 'primary'; - break; - case 3: - statusColor = 'info'; - break; - case 4: - statusColor = 'warning'; - break; - case 5: - statusColor = 'success'; - break; + if (status === 'REJECTED') { + statusColor = getStatusBadgeColor(status); + } else { + switch (approval.step_number) { + case 1: + statusColor = 'neutral'; + break; + case 2: + statusColor = 'primary'; + break; + case 3: + statusColor = 'info'; + break; + case 4: + statusColor = 'warning'; + break; + case 5: + statusColor = 'success'; + break; + } } - if (isRejected) { - statusColor = 'error'; - } + const statusText = approval.step_name || getStatusText(status); return ( - - {isRejected ? 'Ditolak' : approval.step_name} - + /> ); }, }, @@ -369,6 +411,7 @@ const PurchaseTable = () => { text={`Apakah anda yakin ingin menghapus data permintaan pembelian ini?`} secondaryButton={{ text: 'Tidak', + onClick: () => deleteModal.closeModal(), }} primaryButton={{ text: 'Ya', From 995d20bdf354ef1720bc3ebc09b7dec083c02fdb Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 2 Feb 2026 10:54:34 +0700 Subject: [PATCH 04/15] chore: implement permission guard for edit, approve/reject, and delete button --- .../ListDailyChecklistContent.tsx | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/figma-make/components/pages/list-daily-checklist/ListDailyChecklistContent.tsx b/src/figma-make/components/pages/list-daily-checklist/ListDailyChecklistContent.tsx index f54ad41f..634d8716 100644 --- a/src/figma-make/components/pages/list-daily-checklist/ListDailyChecklistContent.tsx +++ b/src/figma-make/components/pages/list-daily-checklist/ListDailyChecklistContent.tsx @@ -36,6 +36,7 @@ import { ColumnDef } from '@tanstack/react-table'; import { useSelect } from '@/components/input/SelectInput'; import { KandangApi } from '@/services/api/master-data'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import RequirePermission from '@/components/helper/RequirePermission'; interface Kandang { id: string; @@ -389,19 +390,21 @@ export function ListDailyChecklistContent() { {row.original.status === 'DRAFT' && ( - + + + )} {row.original.status === 'SUBMITTED' && ( - <> + - + )} {row.original.status === 'DRAFT' && ( - + + + )} ), From 448cf5ceae71b73f8cff47278e8f6ea4dde0a103 Mon Sep 17 00:00:00 2001 From: kris Date: Mon, 2 Feb 2026 03:57:20 +0000 Subject: [PATCH 05/15] Update .gitlab-ci.yml file --- .gitlab-ci.yml | 47 +++++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cfef03d0..9bdfffa4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,32 +16,46 @@ default: .build_template: &build_template stage: build image: node:20-alpine + cache: key: npm-cache paths: - node_modules/ + variables: - NPM_CONFIG_PRODUCTION: 'false' - NODE_ENV: '' + NODE_ENV: "" + NPM_CONFIG_PRODUCTION: "false" + script: - - echo "Installing dependencies..." + # Install dependencies + - echo "๐Ÿ“ฆ Installing dependencies..." - npm ci --no-audit --no-fund - - echo "Build env used:" + # Print env used + - echo "โœ… Build env used:" - echo "NEXT_PUBLIC_LTI_URL=$NEXT_PUBLIC_LTI_URL" - echo "NEXT_PUBLIC_SSO_LOGIN_URL=$NEXT_PUBLIC_SSO_LOGIN_URL" - echo "NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL" - - echo "NEXT_PUBLIC_CLIENT_ID=$NEXT_PUBLIC_CLIENT_ID" - # โœ… Build Next - - echo "Building Next.js..." + # Clean old output + - echo "๐Ÿงน Cleaning old build..." + - rm -rf .next out + + # Build Next.js + - echo "๐Ÿ—๏ธ Running Next.js build..." - npx next build - # โœ… Export static to out/ (ini yang sebelumnya missing) - - echo "Exporting Next.js static site to ./out ..." - - npx next export -o out + # Export static site + - echo "๐Ÿ“ค Exporting static site..." + - npx next export - # build-info.json tetap kamu simpan di out/ + # Validate export result + - echo "๐Ÿ” Validating export output..." + - test -f out/index.html || (echo "โŒ out/index.html missing" && exit 1) + - test -d out/_next/static || (echo "โŒ out/_next/static missing" && exit 1) + + # Build metadata + - echo "๐Ÿ“ Writing build-info.json..." - | cat < out/build-info.json { @@ -50,19 +64,12 @@ default: "built_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")", "NEXT_PUBLIC_LTI_URL": "$NEXT_PUBLIC_LTI_URL", "NEXT_PUBLIC_SSO_LOGIN_URL": "$NEXT_PUBLIC_SSO_LOGIN_URL", - "NEXT_PUBLIC_API_BASE_URL": "$NEXT_PUBLIC_API_BASE_URL", - "NEXT_PUBLIC_CLIENT_ID": "$NEXT_PUBLIC_CLIENT_ID" + "NEXT_PUBLIC_API_BASE_URL": "$NEXT_PUBLIC_API_BASE_URL" } EOF - # โœ… Verifikasi cepat (biar ketahuan out/ isinya bener) - - echo "===== out/ preview =====" - - ls -lah out | head -n 50 - - echo "===== _next assets preview =====" - - ls -lah out/_next/static/chunks 2>/dev/null | head -n 30 || true - artifacts: - name: 'out-$CI_COMMIT_SHORT_SHA' + name: "out-$CI_COMMIT_SHORT_SHA" paths: - out/ expire_in: 1 week From 7ac92ff451e93670cbdbb549cb98acc8c419080e Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 2 Feb 2026 10:59:44 +0700 Subject: [PATCH 06/15] chore: implement permission guard for approve/reject button --- .../detail/DetailDailyChecklistContent.tsx | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx b/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx index d8723df0..bc9653d4 100644 --- a/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx +++ b/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx @@ -23,6 +23,7 @@ import { isResponseError } from '@/lib/api-helper'; import Link from 'next/link'; import { Icon } from '@iconify/react'; import { Document } from '@/types/api/api-general'; +import RequirePermission from '@/components/helper/RequirePermission'; interface ChecklistDetailRow { checklist_id: string; @@ -593,25 +594,27 @@ export function DetailDailyChecklistContent() {

{header.status === 'SUBMITTED' && ( -
- - -
+ +
+ + +
+
)} From 85556c0db0eae30b21a431747ebaff91d31fb6e0 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Feb 2026 11:10:58 +0700 Subject: [PATCH 07/15] refactor(FE): Use Color type for badge color typings --- .../production/recording/RecordingTable.tsx | 10 +++------- .../pages/purchase/PurchaseTable.tsx | 18 ++++-------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index a461e2d8..36a4e69a 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -33,6 +33,7 @@ import Badge from '@/components/Badge'; import StatusBadge from '@/components/helper/StatusBadge'; import CheckboxInput from '@/components/input/CheckboxInput'; import { useUiStore } from '@/stores/ui/ui.store'; +import { Color } from '@/types/theme'; // ===== STATUS BADGE UTILITIES ===== const statusTextMap: Record = { @@ -48,10 +49,7 @@ const getStatusText = (status: string): string => { return statusTextMap[status] || status; }; -const statusBadgeColorMap: Record< - string, - 'success' | 'error' | 'neutral' | 'info' | 'warning' -> = { +const statusBadgeColorMap: Record = { APPROVED: 'success', Disetujui: 'success', approved: 'success', @@ -70,9 +68,7 @@ const statusBadgeColorMap: Record< diperbarui: 'warning', }; -const getStatusBadgeColor = ( - status: string -): 'success' | 'error' | 'neutral' | 'info' | 'warning' => { +const getStatusBadgeColor = (status: string): Color => { return statusBadgeColorMap[status] || 'neutral'; }; diff --git a/src/components/pages/purchase/PurchaseTable.tsx b/src/components/pages/purchase/PurchaseTable.tsx index 6773ba99..d0c72dac 100644 --- a/src/components/pages/purchase/PurchaseTable.tsx +++ b/src/components/pages/purchase/PurchaseTable.tsx @@ -25,6 +25,7 @@ import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ROWS_OPTIONS } from '@/config/constant'; import { Purchase } from '@/types/api/purchase/purchase'; import { PurchaseApi } from '@/services/api/purchase'; +import { Color } from '@/types/theme'; // ===== STATUS BADGE UTILITIES ===== const statusTextMap: Record = { @@ -40,10 +41,7 @@ const getStatusText = (status: string): string => { return statusTextMap[status] || status; }; -const statusBadgeColorMap: Record< - string, - 'success' | 'error' | 'neutral' | 'info' | 'warning' | 'primary' -> = { +const statusBadgeColorMap: Record = { APPROVED: 'success', Disetujui: 'success', approved: 'success', @@ -62,9 +60,7 @@ const statusBadgeColorMap: Record< diperbarui: 'warning', }; -const getStatusBadgeColor = ( - status: string -): 'success' | 'error' | 'neutral' | 'info' | 'warning' | 'primary' => { +const getStatusBadgeColor = (status: string): Color => { return statusBadgeColorMap[status] || 'neutral'; }; @@ -204,13 +200,7 @@ const PurchaseTable = () => { const status = approval.action; - let statusColor: - | 'warning' - | 'success' - | 'neutral' - | 'error' - | 'info' - | 'primary' = 'neutral'; + let statusColor: Color = 'neutral'; if (status === 'REJECTED') { statusColor = getStatusBadgeColor(status); From 0efae3089d43990e2f4a1db14b764b246534cc5d Mon Sep 17 00:00:00 2001 From: kris Date: Mon, 2 Feb 2026 04:35:36 +0000 Subject: [PATCH 08/15] Update .gitlab-ci.yml file --- .gitlab-ci.yml | 38 +++++++------------------------------- 1 file changed, 7 insertions(+), 31 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9bdfffa4..e80a7e02 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,47 +16,24 @@ default: .build_template: &build_template stage: build image: node:20-alpine - cache: key: npm-cache paths: - node_modules/ - variables: - NODE_ENV: "" - NPM_CONFIG_PRODUCTION: "false" - + NPM_CONFIG_PRODUCTION: 'false' + NODE_ENV: '' script: - # Install dependencies - - echo "๐Ÿ“ฆ Installing dependencies..." + - echo "Installing dependencies..." - npm ci --no-audit --no-fund - - # Print env used - - echo "โœ… Build env used:" + - echo "Build env used:" - echo "NEXT_PUBLIC_LTI_URL=$NEXT_PUBLIC_LTI_URL" - echo "NEXT_PUBLIC_SSO_LOGIN_URL=$NEXT_PUBLIC_SSO_LOGIN_URL" - echo "NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL" - - # Clean old output - - echo "๐Ÿงน Cleaning old build..." - - rm -rf .next out - - # Build Next.js - - echo "๐Ÿ—๏ธ Running Next.js build..." + - echo "Building Next.js static export..." - npx next build - - # Export static site - - echo "๐Ÿ“ค Exporting static site..." - - npx next export - - # Validate export result - - echo "๐Ÿ” Validating export output..." - - test -f out/index.html || (echo "โŒ out/index.html missing" && exit 1) - - test -d out/_next/static || (echo "โŒ out/_next/static missing" && exit 1) - - # Build metadata - - echo "๐Ÿ“ Writing build-info.json..." - | + mkdir -p out cat < out/build-info.json { "commit": "$CI_COMMIT_SHORT_SHA", @@ -67,9 +44,8 @@ default: "NEXT_PUBLIC_API_BASE_URL": "$NEXT_PUBLIC_API_BASE_URL" } EOF - artifacts: - name: "out-$CI_COMMIT_SHORT_SHA" + name: 'out-$CI_COMMIT_SHORT_SHA' paths: - out/ expire_in: 1 week From 2425316fea3a029666a86e3b44f021101a8a398d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Feb 2026 11:54:19 +0700 Subject: [PATCH 09/15] refactor(FE): Exclude already selected products from options --- .../recording/form/RecordingForm.tsx | 66 ++++++++++++++++++- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index f3d6d2f9..3b3ce302 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -1196,6 +1196,66 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { [stockProducts, depletionProductsData, eggProductsData, initialValues, type] ); + const getAvailableStockProductOptions = useCallback( + (currentIdx: number) => { + const selectedProductIds = + formik.values.stocks + ?.filter((s, idx) => { + return ( + idx !== currentIdx && + s.product_warehouse_id && + s.product_warehouse_id !== 0 + ); + }) + .map((s) => s.product_warehouse_id) || []; + + return unifiedStockProducts.filter( + (opt) => !selectedProductIds.includes(Number(opt.value)) + ); + }, + [formik.values.stocks, unifiedStockProducts] + ); + + const getAvailableDepletionProductOptions = useCallback( + (currentIdx: number) => { + const selectedProductIds = + formik.values.depletions + ?.filter((d, idx) => { + return ( + idx !== currentIdx && + d.product_warehouse_id && + d.product_warehouse_id !== 0 + ); + }) + .map((d) => d.product_warehouse_id) || []; + + return depletionProducts.filter( + (opt) => !selectedProductIds.includes(Number(opt.value)) + ); + }, + [formik.values.depletions, depletionProducts] + ); + + const getAvailableEggProductOptions = useCallback( + (currentIdx: number) => { + const selectedProductIds = + (formik.values as RecordingLayingFormValues).eggs + ?.filter((e, idx) => { + return ( + idx !== currentIdx && + e.product_warehouse_id && + e.product_warehouse_id !== 0 + ); + }) + .map((e) => e.product_warehouse_id) || []; + + return eggProducts.filter( + (opt) => !selectedProductIds.includes(Number(opt.value)) + ); + }, + [formik.values, eggProducts] + ); + const hasExceededStock = useMemo(() => { if ((type as 'add' | 'edit' | 'detail') === 'detail') return false; return ( @@ -2398,7 +2458,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { option?.value || 0 ); }} - options={unifiedStockProducts} + options={getAvailableStockProductOptions(idx)} placeholder={ !formik.values.project_flock_kandang_id ? 'Pilih kandang terlebih dahulu' @@ -2619,7 +2679,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { option?.value || 0 ); }} - options={depletionProducts} + options={getAvailableDepletionProductOptions(idx)} placeholder='Pilih Kondisi' isLoading={isLoadingDepletionProducts} onMenuScrollToBottom={loadMoreDepletionProducts} @@ -2837,7 +2897,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { option?.value || 0 ); }} - options={eggProducts} + options={getAvailableEggProductOptions(idx)} placeholder='Pilih Kondisi Telur' isLoading={isLoadingEggProducts} onMenuScrollToBottom={loadMoreEggProducts} From dc26da7404e06585b4d29582d48859adb7215e7a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Feb 2026 12:11:24 +0700 Subject: [PATCH 10/15] refactor(FE): Remove next-day duplicate recording check --- .../recording/form/RecordingForm.tsx | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 3b3ce302..bd937ab8 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -1410,28 +1410,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { setDuplicateErrorShown(false); } } - - if ( - nextDayRecording && - nextDayRecording.project_flock_kandang_id === projectFlockKandangId - ) { - const hasSameDayRecording = isResponseSuccess(existingRecordings) - ? existingRecordings.data?.some( - (recording: Recording) => - recording.project_flock.project_flock_kandang_id === - projectFlockKandangId && - recording.day === nextDayRecording.next_day - ) - : false; - - if (hasSameDayRecording) { - toast.error( - `Recording untuk hari ke-${nextDayRecording.next_day} sudah ada datanya. - Tidak bisa membuat recording di hari yang sama dengan project flock yang sama, mohon perbarui recording yang sudah ada terlebih dahulu.` - ); - return; - } - } } if (formik.values.project_flock_kandang_id !== projectFlockKandangId) { From d776c73a03dec6c899ed3d2b7309db7a5e8fcc90 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Feb 2026 14:32:40 +0700 Subject: [PATCH 11/15] refactor(FE): Add onClose handler and improve button behavior --- .../modal/ConfirmationModalWithNotes.tsx | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/components/modal/ConfirmationModalWithNotes.tsx b/src/components/modal/ConfirmationModalWithNotes.tsx index a5551571..e862dffc 100644 --- a/src/components/modal/ConfirmationModalWithNotes.tsx +++ b/src/components/modal/ConfirmationModalWithNotes.tsx @@ -13,6 +13,7 @@ interface ConfirmationModalWithNotesProps extends Omit { rows?: number; placeholder?: string; + onClose?: () => void; primaryButton?: { text?: string; @@ -32,6 +33,7 @@ const ConfirmationModalWithNotes: React.FC = ({ className, rows = 3, placeholder = 'Catatan...', + onClose, ...props }) => { const randomId = useId(); @@ -41,6 +43,11 @@ const ConfirmationModalWithNotes: React.FC = ({ setNotes(e.target.value); }; + const closeModalHandler = () => { + onClose?.(); + ref.current?.close(); + }; + return ( = ({ closeOnBackdrop={closeOnBackdrop} primaryButton={{ ...primaryButton, - onClick: () => { - primaryButton?.onClick?.(notes); + onClick: (e) => { + if (primaryButton && primaryButton?.onClick) { + primaryButton?.onClick?.(notes); + } else { + closeModalHandler(); + } + setNotes(''); }, }} - secondaryButton={secondaryButton} + secondaryButton={ + secondaryButton + ? { + text: secondaryButton?.text ?? 'Tidak', + onClick: (e) => { + if (secondaryButton && secondaryButton?.onClick) { + secondaryButton.onClick?.(e); + } else { + closeModalHandler(); + } + + setNotes(''); + }, + } + : undefined + } className={className} {...props} > From 4dec97b57c5d800ed48c22c6ac25fb502cb4dd04 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Feb 2026 14:33:48 +0700 Subject: [PATCH 12/15] refactor(FE): Reset approval notes when opening/closing modals --- .../pages/expense/ExpenseRequestContent.tsx | 13 +++++++++ .../pages/expense/ExpensesTable.tsx | 15 +++++++++++ .../recording/form/RecordingForm.tsx | 10 +++++-- .../purchase/order/PurchaseOrderDetail.tsx | 27 +++++++++++++++++++ 4 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/components/pages/expense/ExpenseRequestContent.tsx b/src/components/pages/expense/ExpenseRequestContent.tsx index 6513956e..8fbc81d7 100644 --- a/src/components/pages/expense/ExpenseRequestContent.tsx +++ b/src/components/pages/expense/ExpenseRequestContent.tsx @@ -100,6 +100,7 @@ const ExpenseRequestContent = ({ const [isCompleteLoading, setIsCompleteLoading] = useState(false); const [isApproveLoading, setIsApproveLoading] = useState(false); const [isRejectLoading, setIsRejectLoading] = useState(false); + const [, setApprovalNotes] = useState(''); const formik = useFormik({ initialValues: { @@ -130,10 +131,12 @@ const ExpenseRequestContent = ({ }; const approveClickHandler = () => { + setApprovalNotes(''); approveModal.openModal(); }; const rejectClickHandler = () => { + setApprovalNotes(''); rejectModal.openModal(); }; @@ -200,6 +203,7 @@ const ExpenseRequestContent = ({ approveModal.closeModal(); toast.success(approveResponse?.message); + setApprovalNotes(''); router.push('/expense'); } else { approveModal.closeModal(); @@ -234,6 +238,7 @@ const ExpenseRequestContent = ({ rejectModal.closeModal(); toast.success(rejectResponse.message); + setApprovalNotes(''); router.push('/expense'); } else { rejectModal.closeModal(); @@ -710,6 +715,10 @@ const ExpenseRequestContent = ({ text='Apakah anda yakin ingin approve data biaya operasional ini?' secondaryButton={{ text: 'Tidak', + onClick: () => { + setApprovalNotes(''); + approveModal.closeModal(); + }, }} primaryButton={{ text: 'Ya', @@ -725,6 +734,10 @@ const ExpenseRequestContent = ({ text='Apakah anda yakin ingin reject data biaya operasional ini?' secondaryButton={{ text: 'Tidak', + onClick: () => { + setApprovalNotes(''); + rejectModal.closeModal(); + }, }} primaryButton={{ text: 'Ya', diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index 69376992..d2d4754c 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -185,6 +185,7 @@ const ExpensesTable = () => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isApproveLoading, setIsApproveLoading] = useState(false); const [isRejectLoading, setIsRejectLoading] = useState(false); + const [, setApprovalNotes] = useState(''); const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); @@ -342,6 +343,7 @@ const ExpensesTable = () => { [String(props.row.original.id)]: true, }); + setApprovalNotes(''); approveModal.openModal(); }; @@ -353,6 +355,7 @@ const ExpensesTable = () => { [String(props.row.original.id)]: true, }); + setApprovalNotes(''); rejectModal.openModal(); }; @@ -412,10 +415,12 @@ const ExpensesTable = () => { // }; const bulkApproveClickHandler = () => { + setApprovalNotes(''); approveModal.openModal(); }; const bulkRejectClickHandler = () => { + setApprovalNotes(''); rejectModal.openModal(); }; @@ -468,6 +473,7 @@ const ExpensesTable = () => { `Berhasil approve ${selectedRowIds.length} data biaya operasional!` ); + setApprovalNotes(''); setRowSelection({}); } else { approveModal.closeModal(); @@ -509,6 +515,7 @@ const ExpensesTable = () => { toast.success( `Berhasil reject ${selectedRowIds.length} data biaya operasional!` ); + setApprovalNotes(''); setRowSelection({}); } else { rejectModal.closeModal(); @@ -787,6 +794,10 @@ const ExpensesTable = () => { text='Apakah anda yakin ingin approve data biaya operasional ini?' secondaryButton={{ text: 'Tidak', + onClick: () => { + setApprovalNotes(''); + approveModal.closeModal(); + }, }} primaryButton={{ text: 'Ya', @@ -802,6 +813,10 @@ const ExpensesTable = () => { text='Apakah anda yakin ingin reject data biaya operasional ini?' secondaryButton={{ text: 'Tidak', + onClick: () => { + setApprovalNotes(''); + rejectModal.closeModal(); + }, }} primaryButton={{ text: 'Ya', diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index bd937ab8..8f619463 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -3154,7 +3154,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { text='Apakah anda yakin ingin menyetujui data Recording ini?' secondaryButton={{ text: 'Tidak', - onClick: () => setApprovalNotes(''), + onClick: () => { + setApprovalNotes(''); + approveModal.closeModal(); + }, }} primaryButton={{ text: 'Ya', @@ -3176,7 +3179,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { text='Apakah anda yakin ingin menolak data Recording ini?' secondaryButton={{ text: 'Tidak', - onClick: () => setApprovalNotes(''), + onClick: () => { + setApprovalNotes(''); + rejectModal.closeModal(); + }, }} primaryButton={{ text: 'Ya', diff --git a/src/components/pages/purchase/order/PurchaseOrderDetail.tsx b/src/components/pages/purchase/order/PurchaseOrderDetail.tsx index 1f8dd3c9..02bf443d 100644 --- a/src/components/pages/purchase/order/PurchaseOrderDetail.tsx +++ b/src/components/pages/purchase/order/PurchaseOrderDetail.tsx @@ -105,6 +105,7 @@ const PurchaseOrderDetail = ({ const [rowSelection, setRowSelection] = useState>({}); const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [selectedItem, setSelectedItem] = useState(null); + const [, setApprovalNotes] = useState(''); const selectedRowIds = Object.keys(rowSelection).map((item) => parseInt(item) @@ -207,12 +208,15 @@ const PurchaseOrderDetail = ({ switch (approvalStep) { case 1: + setApprovalNotes(''); staffApprovalModal.openModal(); break; case 2: + setApprovalNotes(''); confirmationModalWithNotes.openModal(); break; case 3: + setApprovalNotes(''); acceptApprovalModal.openModal(); break; default: @@ -225,12 +229,15 @@ const PurchaseOrderDetail = ({ switch (approvalStep) { case 1: + setApprovalNotes(''); staffRejectionModal.openModal(); break; case 2: + setApprovalNotes(''); managerRejectionModal.openModal(); break; case 3: + setApprovalNotes(''); acceptRejectionModal.openModal(); break; default: @@ -978,11 +985,16 @@ const PurchaseOrderDetail = ({ await createManagerApprovalHandler(payload); await refreshApprovals(); await refetchData?.(); + setApprovalNotes(''); confirmationModalWithNotes.closeModal(); }, }} secondaryButton={{ text: 'Batal', + onClick: () => { + setApprovalNotes(''); + confirmationModalWithNotes.closeModal(); + }, }} /> @@ -1079,11 +1091,16 @@ const PurchaseOrderDetail = ({ await createStaffApprovalHandler(payload); await refetchData?.(); + setApprovalNotes(''); staffRejectionModal.closeModal(); }, }} secondaryButton={{ text: 'Batal', + onClick: () => { + setApprovalNotes(''); + staffRejectionModal.closeModal(); + }, }} /> @@ -1106,11 +1123,16 @@ const PurchaseOrderDetail = ({ await createAcceptApprovalHandler(payload); await refetchData?.(); + setApprovalNotes(''); acceptRejectionModal.closeModal(); }, }} secondaryButton={{ text: 'Batal', + onClick: () => { + setApprovalNotes(''); + acceptRejectionModal.closeModal(); + }, }} /> @@ -1133,11 +1155,16 @@ const PurchaseOrderDetail = ({ await createManagerApprovalHandler(payload); await refetchData?.(); + setApprovalNotes(''); managerRejectionModal.closeModal(); }, }} secondaryButton={{ text: 'Batal', + onClick: () => { + setApprovalNotes(''); + managerRejectionModal.closeModal(); + }, }} /> From 68feef77fcc57251ff07107a4145eccdbbe773fd Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Feb 2026 14:43:18 +0700 Subject: [PATCH 13/15] refactpr(FE): Extract approval and rejection handlers --- .../purchase/order/PurchaseOrderDetail.tsx | 99 ++++++++++--------- 1 file changed, 54 insertions(+), 45 deletions(-) diff --git a/src/components/pages/purchase/order/PurchaseOrderDetail.tsx b/src/components/pages/purchase/order/PurchaseOrderDetail.tsx index 02bf443d..5324db03 100644 --- a/src/components/pages/purchase/order/PurchaseOrderDetail.tsx +++ b/src/components/pages/purchase/order/PurchaseOrderDetail.tsx @@ -413,6 +413,56 @@ const PurchaseOrderDetail = ({ refetchData, ]); + // ===== APPROVAL/REJECTION HANDLERS ===== + const managerApprovalHandler = async (notes: string) => { + const payload: CreateManagerApprovalRequestPayload = { + action: 'APPROVED', + notes: notes || null, + }; + + await createManagerApprovalHandler(payload); + await refreshApprovals(); + await refetchData?.(); + setApprovalNotes(''); + confirmationModalWithNotes.closeModal(); + }; + + const staffRejectionHandler = async (notes: string) => { + const payload: CreateStaffApprovalRequestPayload = { + action: 'REJECTED', + notes: notes || null, + }; + + await createStaffApprovalHandler(payload); + await refetchData?.(); + setApprovalNotes(''); + staffRejectionModal.closeModal(); + }; + + const acceptRejectionHandler = async (notes: string) => { + const payload: CreateAcceptApprovalRequestPayload = { + action: 'REJECTED', + notes: notes || null, + }; + + await createAcceptApprovalHandler(payload); + await refetchData?.(); + setApprovalNotes(''); + acceptRejectionModal.closeModal(); + }; + + const managerRejectionHandler = async (notes: string) => { + const payload: CreateManagerApprovalRequestPayload = { + action: 'REJECTED', + notes: notes || null, + }; + + await createManagerApprovalHandler(payload); + await refetchData?.(); + setApprovalNotes(''); + managerRejectionModal.closeModal(); + }; + if (!initialValues) { return null; } @@ -976,18 +1026,7 @@ const PurchaseOrderDetail = ({ primaryButton={{ text: 'Ya, Lanjutkan', color: 'success', - onClick: async (notes) => { - const payload: CreateManagerApprovalRequestPayload = { - action: 'APPROVED', - notes: notes || null, - }; - - await createManagerApprovalHandler(payload); - await refreshApprovals(); - await refetchData?.(); - setApprovalNotes(''); - confirmationModalWithNotes.closeModal(); - }, + onClick: managerApprovalHandler, }} secondaryButton={{ text: 'Batal', @@ -1083,17 +1122,7 @@ const PurchaseOrderDetail = ({ primaryButton={{ text: 'Ya, Tolak', color: 'error', - onClick: async (notes) => { - const payload: CreateStaffApprovalRequestPayload = { - action: 'REJECTED', - notes: notes || null, - }; - - await createStaffApprovalHandler(payload); - await refetchData?.(); - setApprovalNotes(''); - staffRejectionModal.closeModal(); - }, + onClick: staffRejectionHandler, }} secondaryButton={{ text: 'Batal', @@ -1115,17 +1144,7 @@ const PurchaseOrderDetail = ({ primaryButton={{ text: 'Ya, Tolak', color: 'error', - onClick: async (notes) => { - const payload: CreateAcceptApprovalRequestPayload = { - action: 'REJECTED', - notes: notes || null, - }; - - await createAcceptApprovalHandler(payload); - await refetchData?.(); - setApprovalNotes(''); - acceptRejectionModal.closeModal(); - }, + onClick: acceptRejectionHandler, }} secondaryButton={{ text: 'Batal', @@ -1147,17 +1166,7 @@ const PurchaseOrderDetail = ({ primaryButton={{ text: 'Ya, Tolak', color: 'error', - onClick: async (notes) => { - const payload: CreateManagerApprovalRequestPayload = { - action: 'REJECTED', - notes: notes || null, - }; - - await createManagerApprovalHandler(payload); - await refetchData?.(); - setApprovalNotes(''); - managerRejectionModal.closeModal(); - }, + onClick: managerRejectionHandler, }} secondaryButton={{ text: 'Batal', From 8637d1c2c21db93041c210314e9e203728282275 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Feb 2026 15:05:47 +0700 Subject: [PATCH 14/15] refactor(FE): Handle next-day error toast visibility --- .../recording/form/RecordingForm.tsx | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 8f619463..6929e2f5 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -241,6 +241,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { new Date().toISOString().split('T')[0] ); const [duplicateErrorShown, setDuplicateErrorShown] = useState(false); + const [nextDayErrorShown, setNextDayErrorShown] = useState(false); useEffect(() => { return () => { @@ -575,10 +576,24 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { setNextDayRecording( nextDayRecordingData.data as unknown as NextDayRecording ); + if (nextDayErrorShown) { + toast.dismiss(); + setNextDayErrorShown(false); + } + } else if (nextDayRecordingData?.status === 'error') { + setNextDayRecording(null); + if (!nextDayErrorShown) { + toast.error(nextDayRecordingData.message || 'Terjadi kesalahan saat memuat data hari berikutnya', { duration: Infinity }); + setNextDayErrorShown(true); + } } else { setNextDayRecording(null); + if (nextDayErrorShown) { + toast.dismiss(); + setNextDayErrorShown(false); + } } - }, [nextDayRecordingData]); + }, [nextDayRecordingData, nextDayErrorShown]); const { rawData: eggProductsData, @@ -1315,6 +1330,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { toast.dismiss(); setDuplicateErrorShown(false); } + if (nextDayErrorShown) { + toast.dismiss(); + setNextDayErrorShown(false); + } setSelectedProjectFlockLocationId( location ? location.value.toString() : '' ); @@ -1335,6 +1354,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { toast.dismiss(); setDuplicateErrorShown(false); } + if (nextDayErrorShown) { + toast.dismiss(); + setNextDayErrorShown(false); + } }; const kandangChangeHandler = (val: OptionType | OptionType[] | null) => { @@ -1351,6 +1374,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { toast.dismiss(); setDuplicateErrorShown(false); } + if (nextDayErrorShown) { + toast.dismiss(); + setNextDayErrorShown(false); + } if (selectedLocation && kandang) { setStockProductsLocationId(selectedLocation.value.toString()); setStockProductsKandangId(kandang.value.toString()); @@ -1380,11 +1407,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { toast.dismiss(); setDuplicateErrorShown(false); } + if (nextDayErrorShown) { + toast.dismiss(); + setNextDayErrorShown(false); + } setTimeout(() => { formik.validateField('project_flock_kandang_id'); }, 0); }, - [formik, duplicateErrorShown] + [formik, duplicateErrorShown, nextDayErrorShown] ); const { formErrorList, handleFormSubmit, close } = useFormikErrorList(formik); From d327af814f0fb3df7495924b8f467d0ee40f9975 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Feb 2026 15:13:59 +0700 Subject: [PATCH 15/15] refactor(FE): Handle missing selectedRecordDate and next-day data --- .../production/recording/form/RecordingForm.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 6929e2f5..94f078c1 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -554,6 +554,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const nextDayRecordingUrl = useMemo(() => { if (!projectFlockKandangLookup) return null; + if (!selectedRecordDate) return null; const projectFlockKandangId = projectFlockKandangLookup.id; return `${RecordingApi.basePath}/next-day?project_flock_kandang_id=${projectFlockKandangId}&record_date=${selectedRecordDate}`; }, [projectFlockKandangLookup, selectedRecordDate]); @@ -583,7 +584,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } else if (nextDayRecordingData?.status === 'error') { setNextDayRecording(null); if (!nextDayErrorShown) { - toast.error(nextDayRecordingData.message || 'Terjadi kesalahan saat memuat data hari berikutnya', { duration: Infinity }); + toast.error( + nextDayRecordingData.message || + 'Terjadi kesalahan saat memuat data hari berikutnya', + { duration: Infinity } + ); setNextDayErrorShown(true); } } else { @@ -1900,8 +1905,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
Umur

- {nextDayRecording - ? `Hari ke-${nextDayRecording.next_day} (Minggu ke-${Math.ceil(nextDayRecording.next_day / 7)})` + {type === 'add' + ? nextDayRecording + ? `Hari ke-${nextDayRecording.next_day} (Minggu ke-${Math.ceil(nextDayRecording.next_day / 7)})` + : '-' : initialValues?.day ? `Hari ke-${initialValues.day} (Minggu ke-${Math.ceil(initialValues.day / 7)})` : '-'}