From 66c537ec101922798265771e5bcce329ac8b6f42 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 26 Jan 2026 20:56:12 +0700 Subject: [PATCH 1/9] chore: change loading text to loading spinner --- src/app/page.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 9cc0177d..33d01de7 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -25,5 +25,9 @@ export default function Home() { ); } - return <>Loading...; + return ( +
+ +
+ ); } From 34f01abb322a6451dd390ffdfb5d67baf859e1df Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 26 Jan 2026 20:57:05 +0700 Subject: [PATCH 2/9] chore: add new props (withPagination, getRowCanExpand, renderSubComponent, expanded, and getSubRows) --- src/components/Table.tsx | 171 ++++++++++++++++++++++++++------------- 1 file changed, 116 insertions(+), 55 deletions(-) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 0e095c1f..0be39fb5 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -1,11 +1,12 @@ 'use client'; -import { ReactNode, useCallback, useEffect, useState } from 'react'; +import { Fragment, ReactNode, useCallback, useEffect, useState } from 'react'; import { flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, + getExpandedRowModel, getSortedRowModel, TableOptions, useReactTable, @@ -15,6 +16,7 @@ import { OnChangeFn, Row, HeaderContext, + ExpandedState, } from '@tanstack/react-table'; import { rankItem } from '@tanstack/match-sorter-utils'; import { Icon } from '@iconify/react'; @@ -33,6 +35,9 @@ interface TableClassNames { bodyRowClassName?: string; selectedBodyRowClassName?: string; bodyColumnClassName?: string; + bodySubRowClassName?: (depth: number) => string; + selectedBodySubRowClassName?: (depth: number) => string; + bodySubRowColumnClassName?: (depth: number) => string; tableFooterClassName?: string; footerRowClassName?: string; footerColumnClassName?: string; @@ -60,6 +65,7 @@ export interface TableProps { enableRowSelection?: boolean | ((row: Row) => boolean); renderFooter?: boolean; withCheckbox?: boolean; + withPagination?: boolean; rowOptions?: number[]; /** * Custom row renderer. Should return a complete element or null. @@ -67,6 +73,10 @@ export interface TableProps { * Return null to render the default row. */ renderCustomRow?: (row: Row) => ReactNode | null; + getRowCanExpand?: (row: Row) => boolean; + renderSubComponent?: (props: { row: Row }) => React.ReactElement; + expanded?: ExpandedState; + getSubRows?: (originalRow: TData, index: number) => TData[] | undefined; } const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; @@ -92,7 +102,12 @@ export const TABLE_DEFAULT_STYLING = { bodyRowClassName: 'transition-all duration-200 border-t border-base-content/10 bg-transparent', selectedBodyRowClassName: 'bg-primary/5', - bodyColumnClassName: 'px-4 py-3 text-base-content', + bodyColumnClassName: 'px-4 py-3 text-base-content font-medium', + bodySubRowClassName: (depth: number) => + 'transition-all duration-200 border-t border-base-content/10 bg-transparent', + selectedBodySubRowClassName: (depth: number) => 'bg-primary/5', + bodySubRowColumnClassName: (depth: number) => + 'px-4 py-3 text-base-content font-medium', paginationClassName: 'px-3', tableFooterClassName: 'font-semibold border-base-content/10', footerRowClassName: 'bg-base-200 border-t-2 border-base-content/10', @@ -120,8 +135,13 @@ const Table = ({ enableRowSelection, renderFooter = false, withCheckbox = false, + withPagination = true, rowOptions = [10, 20, 50, 100], renderCustomRow, + getRowCanExpand, + renderSubComponent, + expanded = {}, + getSubRows, }: TableProps) => { const isServerSideTable = totalItems !== undefined && @@ -154,10 +174,14 @@ const Table = ({ getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel(), onPaginationChange: setPagination, + getExpandedRowModel: getExpandedRowModel(), + getRowCanExpand: getRowCanExpand ?? (getSubRows ? undefined : () => false), + getSubRows, manualSorting, state: { pagination, globalFilter: fuzzySearchValue, + expanded, }, filterFns: { fuzzy: fuzzyFilter, @@ -228,7 +252,10 @@ const Table = ({
({ } return ( - - {row.getVisibleCells().map((cell) => ( - - {!isLoading && - flexRender( - cell.column.columnDef.cell, - cell.getContext() + + 0 + ? tableClassNames.bodySubRowClassName(row.depth) + : tableClassNames.bodyRowClassName, + { + [tableClassNames.selectedBodyRowClassName!]: + row.getIsSelected() && row.depth === 0, + [tableClassNames.selectedBodySubRowClassName( + row.depth + )!]: row.getIsSelected() && row.depth > 0, + } + )} + > + {row.getVisibleCells().map((cell) => ( + 0 + ? tableClassNames.bodySubRowColumnClassName( + row.depth + ) + : tableClassNames.bodyColumnClassName )} + > + {!isLoading && + flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} - {isLoading &&
} - - ))} - + {isLoading &&
} + + ))} + + + {row.getIsExpanded() && ( + <> + {renderSubComponent && ( + + + {renderSubComponent({ row })} + + + )} + + )} + ); })} @@ -425,30 +483,33 @@ const Table = ({ !isLoading && emptyContent} - {data.length > 0 && table.getRowModel().rows.length > 0 && !isLoading && ( -
- -
- )} + {data.length > 0 && + table.getRowModel().rows.length > 0 && + !isLoading && + withPagination && ( +
+ +
+ )}
); }; From a7958166bfb34acc26b2d0c1762b3025af75e475 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 26 Jan 2026 20:57:27 +0700 Subject: [PATCH 3/9] chore: change circle success color --- src/components/helper/StatusBadge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/helper/StatusBadge.tsx b/src/components/helper/StatusBadge.tsx index 46b36559..c4f99593 100644 --- a/src/components/helper/StatusBadge.tsx +++ b/src/components/helper/StatusBadge.tsx @@ -40,7 +40,7 @@ const StatusBadge = ({ xmlns='http://www.w3.org/2000/svg' className={cn({ 'text-base-content/10': color === 'neutral', - 'text-success': color === 'success', + 'text-[#008000]': color === 'success', 'text-error': color === 'error', 'text-primary': color === 'info', })} From 1389cb7ed695223c45fc22aa133a6fce698a4469 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 26 Jan 2026 20:57:50 +0700 Subject: [PATCH 4/9] chore: render button only if primary or secondary button is used --- src/components/modal/ConfirmationModal.tsx | 96 ++++++++++++---------- 1 file changed, 54 insertions(+), 42 deletions(-) diff --git a/src/components/modal/ConfirmationModal.tsx b/src/components/modal/ConfirmationModal.tsx index 98a0b51d..1486b6d5 100644 --- a/src/components/modal/ConfirmationModal.tsx +++ b/src/components/modal/ConfirmationModal.tsx @@ -167,49 +167,61 @@ const ConfirmationModal = ({ {children &&
{children}
} -
- {secondaryButton && secondaryButton.text && ( - - )} + {(secondaryButton || primaryButton) && ( +
+ {secondaryButton && secondaryButton.text && ( + + )} - {primaryButton && primaryButton.text && ( - - )} -
+ {primaryButton && primaryButton.text && ( + + )} +
+ )}
); From f98e9d693020ec52eacc7246a40f78e253636bcf Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 26 Jan 2026 21:22:57 +0700 Subject: [PATCH 5/9] feat: create TransferToLayingConfirmationModal component --- .../TransferToLayingConfirmationModal.tsx | 276 ++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 src/components/pages/production/transfer-to-laying/TransferToLayingConfirmationModal.tsx diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingConfirmationModal.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingConfirmationModal.tsx new file mode 100644 index 00000000..3cedf839 --- /dev/null +++ b/src/components/pages/production/transfer-to-laying/TransferToLayingConfirmationModal.tsx @@ -0,0 +1,276 @@ +'use client'; + +import { ChangeEventHandler, RefObject, useId, useState } from 'react'; +import useSWR from 'swr'; + +import ConfirmationModal, { + ConfirmationModalProps, +} from '@/components/modal/ConfirmationModal'; +import Table from '@/components/Table'; +import TextArea from '@/components/input/TextArea'; + +import { ColumnDef } from '@tanstack/react-table'; +import { cn, formatDate, formatNumber } from '@/lib/helper'; +import { TransferToLayingFormValues } from '@/components/pages/production/transfer-to-laying/form/TransferToLayingForm.schema'; +import { Color } from '@/types/theme'; +import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying'; +import { isResponseSuccess } from '@/lib/api-helper'; + +interface TransferToLayingConfirmationModalProps + extends Omit { + ref: RefObject; + type?: 'info' | 'success' | 'error'; + transferToLayingIds?: number[]; + transferToLayingForm?: TransferToLayingFormValues; + onClose?: () => void; + + withNote?: boolean; + noteLabel?: string; + rows?: number; + placeholder?: string; + + primaryButton?: { + text?: string; + color?: Color; + isLoading?: boolean; + onClick?: (notes: string) => void; + }; +} + +interface TransferToLayingConfirmationTableDataType { + label: string; + value: string; + subRows?: TransferToLayingConfirmationTableDataType[]; +} + +const TransferToLayingConfirmationModalTable = ({ + transferToLayingForm, + transferToLayingId, +}: { + transferToLayingForm?: TransferToLayingFormValues; + transferToLayingId?: number; +}) => { + const { data: transferToLaying, isLoading: isLoadingTransferToLaying } = + useSWR( + transferToLayingId + ? ['detail-transfer-to-laying', String(transferToLayingId)] + : undefined, + ([_, id]) => TransferToLayingApi.getSingle(Number(id)) + ); + + const confirmationTableColumns: ColumnDef[] = + [ + { + header: 'Label', + accessorKey: 'label', + enableSorting: false, + cell: ({ row }) => { + const isSubRow = row.depth > 0; + + return ( + <> + {!isSubRow && row.original.label} + + {isSubRow && ( +
+
+ {row.original.label} +
+ )} + + ); + }, + }, + { + header: 'Value', + accessorKey: 'value', + enableSorting: false, + }, + ]; + + const confirmationTableData: TransferToLayingConfirmationTableDataType[] = [ + { + label: 'Tanggal', + value: formatDate( + transferToLayingId && isResponseSuccess(transferToLaying) + ? transferToLaying.data.transfer_date + : transferToLayingForm?.transfer_date, + 'DD MMMM YYYY' + ), + }, + { + label: 'Flock Asal', + value: + transferToLayingId && isResponseSuccess(transferToLaying) + ? transferToLaying.data.from_project_flock.flock_name + : (transferToLayingForm?.flockSource?.label ?? '-'), + subRows: + transferToLayingId && isResponseSuccess(transferToLaying) + ? (transferToLaying.data.sources?.map( + (sourceProjectFlockKandang) => ({ + label: + sourceProjectFlockKandang.source_project_flock_kandang.kandang + .name, + value: formatNumber( + Number(sourceProjectFlockKandang.qty), + 'en-US' + ), + }) + ) ?? []) + : (transferToLayingForm?.flockSourceKandangs?.map((kandang) => ({ + label: kandang.kandang.label, + value: formatNumber(Number(kandang.quantity), 'en-US'), + })) ?? []), + }, + { + label: 'Flock Tujuan', + value: + transferToLayingId && isResponseSuccess(transferToLaying) + ? transferToLaying.data.to_project_flock.flock_name + : (transferToLayingForm?.flockDestination?.label ?? '-'), + subRows: + transferToLayingId && isResponseSuccess(transferToLaying) + ? (transferToLaying.data.targets?.map( + (targetProjectFlockKandang) => ({ + label: + targetProjectFlockKandang.target_project_flock_kandang.kandang + .name, + value: formatNumber( + Number(targetProjectFlockKandang.qty), + 'en-US' + ), + }) + ) ?? []) + : (transferToLayingForm?.flockDestinationKandangs?.map((kandang) => ({ + label: kandang.kandang.label, + value: formatNumber(Number(kandang.quantity), 'en-US'), + })) ?? []), + }, + { + label: 'Jumlah Transfer', + value: formatNumber( + transferToLayingId && isResponseSuccess(transferToLaying) + ? Number( + transferToLaying.data.sources.reduce( + (total, source) => total + Number(source.qty), + 0 + ) + ) + : Number(transferToLayingForm?.totalQuantity), + 'en-US' + ), + }, + { + label: 'Notes', + value: + transferToLayingId && isResponseSuccess(transferToLaying) + ? (transferToLaying.data.notes ?? '-') + : (transferToLayingForm?.reason ?? '-'), + }, + ]; + + return ( + + columns={confirmationTableColumns} + data={confirmationTableData} + withPagination={false} + pageSize={10000} + expanded={true} + getSubRows={(row) => row.subRows} + className={{ + bodyRowClassName: 'border-none', + bodySubRowClassName: () => 'border-none', + bodySubRowColumnClassName: () => 'first:p-0', + }} + /> + ); +}; + +const TransferToLayingConfirmationModal = ({ + ref, + type = 'success', + transferToLayingForm, + transferToLayingIds, + onClose, + withNote, + rows = 4, + noteLabel, + placeholder = 'Alasan Transfer', + primaryButton, + ...props +}: TransferToLayingConfirmationModalProps) => { + const randomId = useId(); + + const [notes, setNotes] = useState(''); + + const notesChangeHandler: ChangeEventHandler = (e) => { + setNotes(e.target.value); + }; + + const closeModalHandler = () => { + onClose?.(); + ref.current?.close(); + }; + + return ( + { + if (withNote) { + primaryButton?.onClick?.(notes); + } else { + closeModalHandler(); + } + }, + }} + secondaryButton={{ + text: 'Cancel', + color: 'none', + onClick: closeModalHandler, + }} + className={{ + modalBox: 'max-h-full', + }} + {...props} + > +
+ {!transferToLayingIds && transferToLayingForm && ( + + )} + + {transferToLayingIds && + !transferToLayingForm && + transferToLayingIds.map((transferToLayingId, idx) => ( + + ))} + + {withNote && ( +