diff --git a/src/app/expense/page.tsx b/src/app/expense/page.tsx index d6b00286..0bd5d406 100644 --- a/src/app/expense/page.tsx +++ b/src/app/expense/page.tsx @@ -2,7 +2,7 @@ import ExpensesTable from '@/components/pages/expense/ExpensesTable'; const Expense = () => { return ( -
+
); diff --git a/src/app/globals.css b/src/app/globals.css index 0eb04a09..96339bf2 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -30,14 +30,14 @@ --color-base-100: oklch(100% 0 0); /* #ffffff */ --color-base-200: oklch(97.2% 0 0); /* #f2f2f2 */ --color-base-300: oklch(93.1% 0.002 249.7); /* #e5e6e6 */ - --color-base-content: oklch(18.6% 0.024 257.7); /* #1f2937 */ + --color-base-content: #18181b; /* Status/Utility Colors */ --color-info: oklch(67.4% 0.176 238.9); --color-info-content: oklch(0% 0 0); /* #000000 */ --color-success: #00d390; --color-success-content: oklch(100% 0 0); /* #ffffff */ - --color-warning: oklch(82.2% 0.165 91.9); + --color-warning: #fcb700; --color-warning-content: oklch(0% 0 0); /* #000000 */ --color-error: #ff3a3a; --color-error-content: oklch(100% 0 0); /* #fffffff */ diff --git a/src/app/inventory/movement/page.tsx b/src/app/inventory/movement/page.tsx index a2c25612..10717059 100644 --- a/src/app/inventory/movement/page.tsx +++ b/src/app/inventory/movement/page.tsx @@ -2,7 +2,7 @@ import MovementTable from '@/components/pages/inventory/movement/MovementTable'; const Movement = () => { return ( -
+
); diff --git a/src/app/production/recording/page.tsx b/src/app/production/recording/page.tsx index f31ac19a..471ef648 100644 --- a/src/app/production/recording/page.tsx +++ b/src/app/production/recording/page.tsx @@ -2,7 +2,7 @@ import RecordingTable from '@/components/pages/production/recording/RecordingTab const Recording = () => { return ( -
+
); diff --git a/src/app/production/transfer-to-laying/add/page.tsx b/src/app/production/transfer-to-laying/add/page.tsx deleted file mode 100644 index 6f29085f..00000000 --- a/src/app/production/transfer-to-laying/add/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import TransferToLayingForm from '@/components/pages/production/transfer-to-laying/form/TransferToLayingForm'; - -const AddTransferToLaying = () => { - return ( -
- -
- ); -}; - -export default AddTransferToLaying; diff --git a/src/app/production/transfer-to-laying/detail/edit/page.tsx b/src/app/production/transfer-to-laying/detail/edit/page.tsx deleted file mode 100644 index d5498e08..00000000 --- a/src/app/production/transfer-to-laying/detail/edit/page.tsx +++ /dev/null @@ -1,63 +0,0 @@ -'use client'; - -import { useRouter, useSearchParams } from 'next/navigation'; -import useSWR from 'swr'; - -import TransferToLayingForm from '@/components/pages/production/transfer-to-laying/form/TransferToLayingForm'; - -import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; - -const TransferToLayingEdit = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - - const transferToLayingId = searchParams.get('transferToLayingId'); - - const { data: transferToLaying, isLoading: isLoadingTransferToLaying } = - useSWR(transferToLayingId, (id: number) => - TransferToLayingApi.getSingle(id) - ); - - if (!transferToLayingId) { - router.back(); - - return ( -
- -
- ); - } - - if ( - !isLoadingTransferToLaying && - (!transferToLaying || isResponseError(transferToLaying)) - ) { - router.replace('/404'); - return; - } - - if ( - isResponseSuccess(transferToLaying) && - transferToLaying.data.approval.step_number === 2 - ) { - router.replace('/production/transfer-to-laying'); - return; - } - - return ( -
- {isLoadingTransferToLaying && ( - - )} - {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && ( - - )} -
- ); -}; - -export default TransferToLayingEdit; diff --git a/src/app/production/transfer-to-laying/detail/layout.tsx b/src/app/production/transfer-to-laying/detail/layout.tsx deleted file mode 100644 index 7220dfa1..00000000 --- a/src/app/production/transfer-to-laying/detail/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import SuspenseHelper from '@/components/helper/SuspenseHelper'; - -const Layout = ({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) => { - return {children}; -}; - -export default Layout; diff --git a/src/app/production/transfer-to-laying/detail/page.tsx b/src/app/production/transfer-to-laying/detail/page.tsx deleted file mode 100644 index 9ff6ed5e..00000000 --- a/src/app/production/transfer-to-laying/detail/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -'use client'; - -import { useRouter, useSearchParams } from 'next/navigation'; -import useSWR from 'swr'; - -import TransferToLayingForm from '@/components/pages/production/transfer-to-laying/form/TransferToLayingForm'; - -import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; - -const TransferToLayingDetail = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - - const transferToLayingId = searchParams.get('transferToLayingId'); - - const { data: transferToLaying, isLoading: isLoadingTransferToLaying } = - useSWR(transferToLayingId, (id: number) => - TransferToLayingApi.getSingle(id) - ); - - if (!transferToLayingId) { - router.back(); - - return ( -
- -
- ); - } - - if ( - !isLoadingTransferToLaying && - (!transferToLaying || isResponseError(transferToLaying)) - ) { - router.replace('/404'); - return; - } - - return ( -
- {isLoadingTransferToLaying && ( - - )} - - {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && ( - - )} -
- ); -}; - -export default TransferToLayingDetail; diff --git a/src/app/production/transfer-to-laying/page.tsx b/src/app/production/transfer-to-laying/page.tsx index 048ae005..5d790345 100644 --- a/src/app/production/transfer-to-laying/page.tsx +++ b/src/app/production/transfer-to-laying/page.tsx @@ -1,5 +1,6 @@ import TransferToLayingsTable from '@/components/pages/production/transfer-to-laying/TransferToLayingsTable'; import TransferToLayingFormModal from '@/components/pages/production/transfer-to-laying/TransferToLayingFormModal'; +import TransferToLayingDetailModal from '@/components/pages/production/transfer-to-laying/TransferToLayingDetailModal'; const TransferToLaying = () => { return ( @@ -7,6 +8,8 @@ const TransferToLaying = () => { + +
); }; diff --git a/src/app/purchase/page.tsx b/src/app/purchase/page.tsx index dc25a99d..ea2bb95b 100644 --- a/src/app/purchase/page.tsx +++ b/src/app/purchase/page.tsx @@ -2,7 +2,7 @@ import PurchaseTable from '@/components/pages/purchase/PurchaseTable'; const Purchase = () => { return ( -
+
); diff --git a/src/components/helper/ApprovalStepsV2.tsx b/src/components/helper/ApprovalStepsV2.tsx new file mode 100644 index 00000000..dc21453e --- /dev/null +++ b/src/components/helper/ApprovalStepsV2.tsx @@ -0,0 +1,203 @@ +'use client'; + +import { useEffect, useMemo, useState } from 'react'; + +import { Icon } from '@iconify/react'; +import { BaseApproval } from '@/types/api/api-general'; +import Button from '@/components/Button'; + +import { cn, formatDate } from '@/lib/helper'; + +interface ApprovalStepsV2Props { + approvals?: BaseApproval[]; + steps: { + step_number: number; + step_name: string; + }[]; + maxVisibleSteps?: number; + className?: { + wrapper?: string; + stepsWrapper?: string; + stepsContainer?: string; + }; +} + +const ApprovalStepsV2 = ({ + approvals, + steps, + maxVisibleSteps = 2, + className, +}: ApprovalStepsV2Props) => { + const [isSeeAll, setIsSeeAll] = useState(false); + const [formattedApprovals, setFormattedApprovals] = useState< + (BaseApproval & { isActive: boolean })[] + >([]); + + const latestApprovalStepNumber = + approvals?.[approvals.length - 1].step_number ?? 0; + + const lastStepNumber = steps[steps.length - 1].step_number; + + const isLatestApprovalStepNumberLessThanLastStepNumber = + latestApprovalStepNumber < lastStepNumber; + + const slicedFormattedApprovals = useMemo(() => { + return formattedApprovals.slice(0, isSeeAll ? undefined : maxVisibleSteps); + }, [formattedApprovals, isSeeAll]); + + const seeMoreClickHandler = () => { + setIsSeeAll((prevVal) => !prevVal); + }; + + useEffect(() => { + if (approvals) { + const tempFormattedApprovals: (BaseApproval & { isActive: boolean })[] = + []; + + approvals.forEach((approval) => { + tempFormattedApprovals.push({ + ...approval, + isActive: true, + }); + }); + + if (isLatestApprovalStepNumberLessThanLastStepNumber) { + const latestApprovalStepNumberIndexInSteps = steps.findIndex( + (step) => step.step_number === latestApprovalStepNumber + ); + + const slicedSteps = steps.slice( + latestApprovalStepNumberIndexInSteps + 1 + ); + + slicedSteps.forEach((step) => { + tempFormattedApprovals.push({ + action: 'APPROVED', + action_at: new Date().toISOString(), + action_by: { + id: 0, + id_user: 0, + email: '', + name: '', + }, + step_name: step.step_name, + step_number: step.step_number, + isActive: false, + }); + }); + } + + setFormattedApprovals(tempFormattedApprovals); + } + }, [approvals]); + + return ( +
+

+ Progress Details +

+ +
+ {slicedFormattedApprovals.map((approval, idx) => { + const isApprovalActionCreated = approval.action === 'CREATED'; + const isApprovalActionUpdated = approval.action === 'UPDATED'; + const isApprovalActionRejected = approval.action === 'REJECTED'; + const isApprovalActionApproved = approval.action === 'APPROVED'; + + const approvalIcon = + isApprovalActionCreated || isApprovalActionUpdated + ? 'heroicons:clock-solid' + : isApprovalActionRejected + ? 'heroicons:x-circle-solid' + : isApprovalActionApproved + ? 'heroicons:check-badge-solid' + : 'heroicons:check-badge-solid'; + + return ( +
+
+
+ + + {idx < formattedApprovals.length - 1 && ( +
+ )} +
+
+ +
+
+ {approval.step_name} + + {(isApprovalActionCreated || isApprovalActionUpdated) && + 'Diajukan oleh '} + {isApprovalActionRejected && 'Ditolak oleh '} + {isApprovalActionApproved && 'Disetujui oleh '} + {approval.isActive ? approval.action_by.name : '...'} + +
+ + {approval.isActive && ( +

+ Created at :{' '} + {formatDate(approval.action_at, 'DD-MM-YYYY, HH:mm')} +
+ Notes : {approval.notes ?? '-'} +

+ )} +
+
+ ); + })} +
+ + {formattedApprovals.length > maxVisibleSteps && ( + + )} +
+ ); +}; + +export default ApprovalStepsV2; diff --git a/src/components/input/DateInput.tsx b/src/components/input/DateInput.tsx index 558779c7..da1a4d81 100644 --- a/src/components/input/DateInput.tsx +++ b/src/components/input/DateInput.tsx @@ -226,7 +226,7 @@ const DateInput = ({
@@ -257,8 +260,8 @@ const DateInput = ({ )} handleClick(e as unknown as React.MouseEvent) diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx index 1eb3ccd8..419ed314 100644 --- a/src/components/input/SelectInput.tsx +++ b/src/components/input/SelectInput.tsx @@ -42,6 +42,7 @@ interface SelectInputBaseProps { optionComponent?: OptionComponent; components?: Partial; isDisabled?: boolean; + readOnly?: boolean; isLoading?: boolean; isClearable?: boolean; isRtl?: boolean; @@ -156,6 +157,7 @@ const SelectInput = (props: SelectInputProps) => { closeMenuOnSelect, hideSelectedOptions, onMenuScrollToBottom, + readOnly, } = props; const [internalInputValue, setInternalInputValue] = useState(''); @@ -235,7 +237,7 @@ const SelectInput = (props: SelectInputProps) => { onInputChange={internalInputChangeHandler} onMenuClose={() => setInternalInputValue('')} isMulti={isMulti} - isDisabled={isDisabled} + isDisabled={isDisabled || readOnly} isLoading={isLoading} isClearable={isClearable} isRtl={isRtl} @@ -247,30 +249,37 @@ const SelectInput = (props: SelectInputProps) => { classNames={{ ...(!startAdornment && { control: ({ isFocused, isDisabled }) => - cn( - 'w-full min-h-12! rounded-lg! border bg-white transition-shadow cursor-pointer!', - { - 'border-red-500! ring-2 ring-red-200': isError, - 'border-indigo-500 ring-2 ring-indigo-200': isFocused, - 'border-base-content/10!': !isError && !isFocused, - 'bg-gray-100 text-gray-400 cursor-not-allowed': isDisabled, - } - ), - valueContainer: () => cn('flex-1 p-3! py-2! gap-1'), + cn('w-full rounded-lg! border bg-white transition-shadow', { + 'cursor-pointer!': !readOnly && !isDisabled, + 'border-red-500! ring-2 ring-red-200': isError, + 'border-indigo-500 ring-2 ring-indigo-200': isFocused, + 'border-base-content/10!': !isError && !isFocused, + 'bg-gray-100 text-gray-400 cursor-not-allowed': + isDisabled && !readOnly, + 'bg-transparent! cursor-not-allowed!': readOnly, + }), + valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'), }), placeholder: () => - cn({ 'text-gray-400': !isError, 'text-red-300!': isError }), + cn({ + 'text-gray-400 text-sm leading-tight': !isError, + 'text-red-300!': isError, + }), singleValue: () => - cn({ 'text-gray-900': !isError, 'text-error!': isError }), - input: () => cn('text-gray-900 m-0! p-0!'), - indicatorsContainer: () => cn('flex items-center gap-1 pr-2'), + cn({ + 'm-0! text-gray-900 text-sm leading-tight': !isError, + 'text-error!': isError, + 'text-gray-900!': readOnly, + }), + input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'), + indicatorsContainer: () => cn('flex items-center gap-1 pr-3 py-2'), dropdownIndicator: ({ isFocused }) => - cn('p-1! rounded hover:bg-gray-100', { + cn('p-0! rounded hover:bg-gray-100', { 'text-gray-900': isFocused, 'text-gray-500': !isFocused, 'text-error!': isError, }), - clearIndicator: () => cn('p-1! rounded hover:bg-gray-100'), + clearIndicator: () => cn('p-0! rounded hover:bg-gray-100'), menu: () => cn( 'border border-base-content/5 rounded-xl! bg-base-100 shadow-lg! my-1.5!' diff --git a/src/components/input/TextArea.tsx b/src/components/input/TextArea.tsx index 65bd57d3..b3dd2663 100644 --- a/src/components/input/TextArea.tsx +++ b/src/components/input/TextArea.tsx @@ -83,7 +83,7 @@ const TextArea = ({