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
-
-
-
-
- | Tahap |
- Aksi |
- Catatan |
- Oleh |
- Waktu |
-
-
-
- {approvalHistory
- .sort(
- (a: BaseApproval, b: BaseApproval) =>
- new Date(b.action_at).getTime() -
- new Date(a.action_at).getTime()
- )
- .map((approval: BaseApproval, index: number) => (
-
- | {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)})`
: '-'}