Merge branch 'fix/transfer-to-laying' into 'development'

[FIX/FE] Transfer to Laying

See merge request mbugroup/lti-web-client!219
This commit is contained in:
Rivaldi A N S
2026-01-20 11:03:38 +00:00
17 changed files with 391 additions and 219 deletions
+4 -4
View File
@@ -52,7 +52,7 @@
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"daisyui": "^5.5.8", "daisyui": "^5.5.14",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "^15.5.7", "eslint-config-next": "^15.5.7",
"husky": "^9.1.7", "husky": "^9.1.7",
@@ -5855,9 +5855,9 @@
} }
}, },
"node_modules/daisyui": { "node_modules/daisyui": {
"version": "5.5.8", "version": "5.5.14",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.8.tgz", "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.14.tgz",
"integrity": "sha512-6psL9jIEOFOw68V10j/BKCWcRgx8dh81mmNxShr+g7HDM6UHNoPharlp9zq/PQkHNuGU1ZQsajR3HgpvavbRKQ==", "integrity": "sha512-L47rvw7I7hK68TA97VB8Ee0woHew+/ohR6Lx6Ah/krfISOqcG4My7poNpX5Mo5/ytMxiR40fEaz6njzDi7cuSg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
+1 -1
View File
@@ -55,7 +55,7 @@
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"daisyui": "^5.5.8", "daisyui": "^5.5.14",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "^15.5.7", "eslint-config-next": "^15.5.7",
"husky": "^9.1.7", "husky": "^9.1.7",
+1
View File
@@ -57,6 +57,7 @@
@theme { @theme {
--font-inter: var(--font-inter); --font-inter: var(--font-inter);
--font-roboto: var(--font-roboto);
--container-sm: 40rem; --container-sm: 40rem;
--container-md: 48rem; --container-md: 48rem;
+10 -2
View File
@@ -1,5 +1,5 @@
import type { Metadata, Viewport } from 'next'; import type { Metadata, Viewport } from 'next';
import { Inter } from 'next/font/google'; import { Inter, Roboto } from 'next/font/google';
import '@/app/globals.css'; import '@/app/globals.css';
import { Toaster } from 'react-hot-toast'; import { Toaster } from 'react-hot-toast';
@@ -12,6 +12,12 @@ const inter = Inter({
subsets: ['latin'], subsets: ['latin'],
}); });
const roboto = Roboto({
variable: '--font-roboto',
subsets: ['latin'],
weight: ['200', '300', '400', '500', '600', '700', '900'],
});
export const viewport: Viewport = { export const viewport: Viewport = {
themeColor: '#1f74bf', themeColor: '#1f74bf',
colorScheme: 'light', colorScheme: 'light',
@@ -30,7 +36,9 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html lang='en' data-theme='lti'> <html lang='en' data-theme='lti'>
<body className={`${inter.variable} antialiased font-inter`}> <body
className={`${inter.variable} ${roboto.variable} antialiased font-inter`}
>
<RequireAuth> <RequireAuth>
<MainDrawer>{children}</MainDrawer> <MainDrawer>{children}</MainDrawer>
</RequireAuth> </RequireAuth>
+1 -1
View File
@@ -162,7 +162,7 @@ const Drawer = ({
<div <div
className={cn( className={cn(
varianClassName?.drawerSidebarContent, varianClassName?.drawerSidebarContent,
className?.drawerContent, className?.drawerSidebarContent,
'overflow-y-auto' 'overflow-y-auto'
)} )}
> >
+22 -13
View File
@@ -26,29 +26,34 @@ const MainDrawerContent = () => {
}; };
return ( return (
<div className='w-full p-4 flex flex-col gap-4'> <div className='w-full flex flex-col'>
<div className='flex flex-row items-center gap-4'> <div className='p-3 flex flex-row items-center gap-4 border-b border-base-content/10'>
<Image <div className='flex flex-row items-center gap-2'>
src='/assets/img/lti-logo.png' <Image
alt='MBU Logo' src='/assets/img/lti-logo.png'
width={256} alt='LTI Logo'
height={256} width={40}
className='w-full max-w-16 h-auto' height={40}
/> className='w-full max-w-10 h-auto'
/>
<h1 className='text-xl font-bold'>LTI ERP</h1> <div className='font-roboto'>
<h1 className='text-sm font-semibold'>LTI ERP</h1>
<p className='text-sm text-black/50'>Lumbung Telur Indonesia</p>
</div>
</div>
<div className='grow flex flex-row justify-end sm:hidden'> <div className='grow flex flex-row justify-end sm:hidden'>
<Button <Button
variant='soft' variant='soft'
color='error' color='error'
onClick={closeMainDrawerHandler} onClick={closeMainDrawerHandler}
className='rounded-full' className='p-1 rounded-full'
> >
<Icon <Icon
icon='material-symbols:close-rounded' icon='material-symbols:close-rounded'
width={24} width={16}
height={24} height={16}
/> />
</Button> </Button>
</div> </div>
@@ -121,6 +126,10 @@ const MainDrawer = ({
setOpen={setMainDrawerOpen} setOpen={setMainDrawerOpen}
openOnLarge openOnLarge
sidebarContent={<MainDrawerContent />} sidebarContent={<MainDrawerContent />}
className={{
drawerSide: 'border-r border-base-content/10',
drawerSidebarContent: 'min-w-[244px] lg:w-[244px]',
}}
> >
<main className='w-full h-full flex flex-col'> <main className='w-full h-full flex flex-col'>
<Navbar title={pageTitle as string} toggleSidebar={toggleSidebar} /> <Navbar title={pageTitle as string} toggleSidebar={toggleSidebar} />
+64 -11
View File
@@ -86,7 +86,7 @@ export const TABLE_DEFAULT_STYLING = {
tableHeaderClassName: '', tableHeaderClassName: '',
headerRowClassName: '', headerRowClassName: '',
headerColumnClassName: headerColumnClassName:
'px-4 py-3 border-base-content/10 text-base-content/50', 'px-4 py-3 border-base-content/10 text-base-content/50 text-sm font-medium',
tableBodyClassName: '', tableBodyClassName: '',
bodyRowClassName: 'border-t border-base-content/10', bodyRowClassName: 'border-t border-base-content/10',
bodyColumnClassName: 'px-4 py-3 text-base-content', bodyColumnClassName: 'px-4 py-3 text-base-content',
@@ -222,14 +222,37 @@ const Table = <TData extends object>({
}, [pageSize, setPageSize]); }, [pageSize, setPageSize]);
return ( return (
<div className={tableClassNames.containerClassName}> <div
<div className={tableClassNames.tableWrapperClassName}> className={cn(
<table className={tableClassNames.tableClassName}> TABLE_DEFAULT_STYLING.containerClassName,
<thead className={tableClassNames.tableHeaderClassName}> tableClassNames.containerClassName
)}
>
<div
className={cn(
TABLE_DEFAULT_STYLING.tableWrapperClassName,
tableClassNames.tableWrapperClassName
)}
>
<table
className={cn(
TABLE_DEFAULT_STYLING.tableClassName,
tableClassNames.tableClassName
)}
>
<thead
className={cn(
TABLE_DEFAULT_STYLING.tableHeaderClassName,
tableClassNames.tableHeaderClassName
)}
>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<tr <tr
key={headerGroup.id} key={headerGroup.id}
className={tableClassNames.headerRowClassName} className={cn(
TABLE_DEFAULT_STYLING.headerRowClassName,
tableClassNames.headerRowClassName
)}
> >
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => {
const columnRelativeDepth = const columnRelativeDepth =
@@ -262,6 +285,7 @@ const Table = <TData extends object>({
{ {
'border-b': header.colSpan > 1, 'border-b': header.colSpan > 1,
}, },
TABLE_DEFAULT_STYLING.headerColumnClassName,
tableClassNames.headerColumnClassName tableClassNames.headerColumnClassName
)} )}
> >
@@ -311,7 +335,12 @@ const Table = <TData extends object>({
))} ))}
</thead> </thead>
<tbody className={tableClassNames.tableBodyClassName}> <tbody
className={cn(
TABLE_DEFAULT_STYLING.tableBodyClassName,
tableClassNames.tableBodyClassName
)}
>
{table.getRowModel().rows.map((row) => { {table.getRowModel().rows.map((row) => {
const customRowContent = renderCustomRow?.(row); const customRowContent = renderCustomRow?.(row);
@@ -320,12 +349,19 @@ const Table = <TData extends object>({
} }
return ( return (
<tr key={row.id} className={tableClassNames.bodyRowClassName}> <tr
key={row.id}
className={cn(
TABLE_DEFAULT_STYLING.bodyRowClassName,
tableClassNames.bodyRowClassName
)}
>
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<td <td
key={cell.id} key={cell.id}
className={cn( className={cn(
{ 'first:w-9 first:pr-0': withCheckbox }, { 'first:w-9 first:pr-0': withCheckbox },
TABLE_DEFAULT_STYLING.bodyColumnClassName,
tableClassNames.bodyColumnClassName tableClassNames.bodyColumnClassName
)} )}
> >
@@ -342,14 +378,25 @@ const Table = <TData extends object>({
); );
})} })}
</tbody> </tbody>
<tfoot className={cn(tableClassNames.tableFooterClassName)}> <tfoot
className={cn(
TABLE_DEFAULT_STYLING.tableFooterClassName,
tableClassNames.tableFooterClassName
)}
>
{renderFooter && ( {renderFooter && (
<tr className={cn(tableClassNames.footerRowClassName)}> <tr
className={cn(
TABLE_DEFAULT_STYLING.footerRowClassName,
tableClassNames.footerRowClassName
)}
>
{table.getAllLeafColumns().map((column) => ( {table.getAllLeafColumns().map((column) => (
<td <td
key={column.id} key={column.id}
className={cn( className={cn(
{ 'first:w-9 first:pr-0': withCheckbox }, { 'first:w-9 first:pr-0': withCheckbox },
TABLE_DEFAULT_STYLING.footerColumnClassName,
tableClassNames.footerColumnClassName tableClassNames.footerColumnClassName
)} )}
> >
@@ -372,7 +419,13 @@ const Table = <TData extends object>({
emptyContent} emptyContent}
{data.length > 0 && table.getRowModel().rows.length > 0 && !isLoading && ( {data.length > 0 && table.getRowModel().rows.length > 0 && !isLoading && (
<div className={cn('mt-5', tableClassNames.paginationClassName)}> <div
className={cn(
'mt-5',
TABLE_DEFAULT_STYLING.paginationClassName,
tableClassNames.paginationClassName
)}
>
<Pagination <Pagination
totalItems={isServerSideTable ? totalItems : table.getRowCount()} totalItems={isServerSideTable ? totalItems : table.getRowCount()}
itemsPerPage={table.getState().pagination.pageSize} itemsPerPage={table.getState().pagination.pageSize}
+9 -9
View File
@@ -39,16 +39,15 @@ const SidebarMenuItem = ({ item, activeLink }: SidebarMenuItemProps) => {
<li> <li>
<Link <Link
href={item.link} href={item.link}
className={cn( className={cn('px-3 py-1.5', {
{ 'text-base-content/60': !isItemActive,
'menu-active border-2 border-solid border-base-300': isItemActive, 'menu-active border-[1.5px] border-solid border-base-300':
}, isItemActive,
'px-3 py-1.5' })}
)}
> >
{item.icon && <Icon icon={item.icon} width={20} height={20} />} {item.icon && <Icon icon={item.icon} width={20} height={20} />}
<span className='text-base'>{item.text}</span> <span className='text-sm'>{item.text}</span>
</Link> </Link>
</li> </li>
); );
@@ -62,12 +61,13 @@ const SidebarMenuItem = ({ item, activeLink }: SidebarMenuItemProps) => {
<details open={isItemActive}> <details open={isItemActive}>
<summary <summary
className={cn({ className={cn({
'text-base-content/60': !isItemActive,
'text-primary': isItemActive, 'text-primary': isItemActive,
})} })}
> >
{item.icon && <Icon icon={item.icon} width={20} height={20} />} {item.icon && <Icon icon={item.icon} width={20} height={20} />}
<span className='text-base'>{item.text}</span> <span className='text-sm'>{item.text}</span>
</summary> </summary>
<ul> <ul>
@@ -88,7 +88,7 @@ const SidebarMenuItem = ({ item, activeLink }: SidebarMenuItemProps) => {
const SidebarMenu = ({ menu, activeLink }: SidebarMenuProps) => { const SidebarMenu = ({ menu, activeLink }: SidebarMenuProps) => {
return ( return (
<Menu> <Menu className='p-3'>
{menu.map((menuItem, menuIdx) => { {menu.map((menuItem, menuIdx) => {
return ( return (
<SidebarMenuItem <SidebarMenuItem
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { ChangeEventHandler, useState } from 'react'; import { ChangeEventHandler, useEffect, useState } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { import {
CellContext, CellContext,
@@ -20,33 +20,32 @@ import SelectInput, {
OptionType, OptionType,
useSelect, useSelect,
} from '@/components/input/SelectInput'; } from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import TextInput from '@/components/input/TextInput';
import CheckboxInput from '@/components/input/CheckboxInput'; import CheckboxInput from '@/components/input/CheckboxInput';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import DateInput from '@/components/input/DateInput';
import PopoverButton from '@/components/popover/PopoverButton';
import { TransferToLaying } from '@/types/api/production/transfer-to-laying'; import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying'; import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
import { cn, formatDate } from '@/lib/helper'; import { cn, formatDate } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ROWS_OPTIONS } from '@/config/constant';
import { Flock } from '@/types/api/master-data/flock'; import { Flock } from '@/types/api/master-data/flock';
import { FlockApi } from '@/services/api/master-data'; import { ProjectFlockApi } from '@/services/api/production';
import PillBadge from '@/components/PillBadge'; import Badge from '@/components/Badge';
import { Color } from '@/types/theme';
import PopoverContent from '@/components/popover/PopoverContent';
const RowOptionsMenu = ({ const RowOptionsMenu = ({
type = 'dropdown',
props, props,
popoverPosition = 'bottom',
approveClickHandler, approveClickHandler,
rejectClickHandler, rejectClickHandler,
deleteClickHandler, deleteClickHandler,
}: { }: {
type: 'dropdown' | 'collapse';
props: CellContext<TransferToLaying, unknown>; props: CellContext<TransferToLaying, unknown>;
popoverPosition: 'bottom' | 'top';
approveClickHandler: () => void; approveClickHandler: () => void;
rejectClickHandler: () => void; rejectClickHandler: () => void;
deleteClickHandler: () => void; deleteClickHandler: () => void;
@@ -60,80 +59,99 @@ const RowOptionsMenu = ({
const showApproveButton = showEditButton; const showApproveButton = showEditButton;
const showRejectButton = showEditButton; const showRejectButton = showEditButton;
const popoverId = `transferToLaying#${props.row.original.id}`;
const popoverAnchorName = `--anchor-transferToLaying#${props.row.original.id}`;
return ( return (
<RowOptionsMenuWrapper type={type}> <div className='relative'>
<RequirePermission permissions='lti.production.transfer_to_laying.detail'> <PopoverButton
<Button tabIndex={0}
href={`/production/transfer-to-laying/detail/?transferToLayingId=${props.row.original.id}`} variant='ghost'
variant='ghost' color='none'
color='primary' popoverTarget={popoverId}
className='justify-start text-sm' anchorName={popoverAnchorName}
> >
<Icon icon='mdi:eye-outline' width={16} height={16} /> <Icon icon='material-symbols:more-vert' width={16} height={16} />
Detail </PopoverButton>
</Button>
</RequirePermission>
{showEditButton && ( <PopoverContent
<RequirePermission permissions='lti.production.transfer_to_laying.update'> id={popoverId}
<Button anchorName={popoverAnchorName}
href={`/production/transfer-to-laying/detail/edit/?transferToLayingId=${props.row.original.id}`} position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
variant='ghost' className='rounded-xl border border-base-content/5 shadow-sm'
color='warning' >
className='justify-start text-sm' <div className='flex flex-col bg-base-100 rounded-xl'>
> <RequirePermission permissions='lti.production.transfer_to_laying.detail'>
<Icon icon='material-symbols:edit-outline' width={16} height={16} /> <Button
Edit href={`/production/transfer-to-laying/detail/?transferToLayingId=${props.row.original.id}`}
</Button> variant='ghost'
</RequirePermission> color='none'
)} className='p-3 justify-start text-sm font-semibold w-full'
>
<Icon icon='heroicons:eye' width={20} height={20} />
View Details
</Button>
</RequirePermission>
{/* TODO: apply RBAC */} {showEditButton && (
{showApproveButton && ( <RequirePermission permissions='lti.production.transfer_to_laying.update'>
<RequirePermission permissions='lti.production.transfer_to_laying.approve'> <Button
<Button href={`/production/transfer-to-laying/detail/edit/?transferToLayingId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='success' color='none'
onClick={approveClickHandler} className='p-3 justify-start text-sm font-semibold w-full'
className='justify-start text-sm' >
> <Icon icon='heroicons:pencil-square' width={20} height={20} />
<Icon icon='material-symbols:check' width={24} height={24} /> Edit
Approve </Button>
</Button> </RequirePermission>
</RequirePermission> )}
)}
{showRejectButton && ( {showApproveButton && (
<RequirePermission permissions='lti.production.transfer_to_laying.approve'> <RequirePermission permissions='lti.production.transfer_to_laying.approve'>
<Button <Button
variant='ghost' variant='ghost'
color='error' color='success'
onClick={rejectClickHandler} onClick={approveClickHandler}
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
> >
<Icon icon='material-symbols:close' width={24} height={24} /> <Icon icon='heroicons:check' width={20} height={20} />
Reject Approve
</Button> </Button>
</RequirePermission> </RequirePermission>
)} )}
{showDeleteButton && (
<RequirePermission permissions='lti.production.transfer_to_laying.delete'> {showRejectButton && (
<Button <RequirePermission permissions='lti.production.transfer_to_laying.approve'>
onClick={deleteClickHandler} <Button
variant='ghost' variant='ghost'
color='error' color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content' onClick={rejectClickHandler}
> className='p-3 justify-start text-sm font-semibold w-full'
<Icon >
icon='material-symbols:delete-outline-rounded' <Icon icon='heroicons:x-mark' width={20} height={20} />
width={16} Reject
height={16} </Button>
className='justify-start text-sm' </RequirePermission>
/> )}
Delete
</Button> {showDeleteButton && (
</RequirePermission> <RequirePermission permissions='lti.production.transfer_to_laying.delete'>
)} <hr className='mx-3 border-base-content/10 h-px' />
</RowOptionsMenuWrapper> <Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='p-3 justify-start text-sm font-semibold w-full'
>
<Icon icon='heroicons:trash' width={20} height={20} />
Delete
</Button>
</RequirePermission>
)}
</div>
</PopoverContent>
</div>
); );
}; };
@@ -150,6 +168,8 @@ const TransferToLayingsTable = () => {
transferDate: '', transferDate: '',
flockSource: '', flockSource: '',
flockDestination: '', flockDestination: '',
filter_by: '',
sort_by: '',
}, },
paramMap: { paramMap: {
page: 'page', page: 'page',
@@ -157,6 +177,8 @@ const TransferToLayingsTable = () => {
transferDate: 'transfer_date', transferDate: 'transfer_date',
flockSource: 'flock_source', flockSource: 'flock_source',
flockDestination: 'flock_destination', flockDestination: 'flock_destination',
filter_by: 'filter_by',
sort_by: 'sort_by',
}, },
}); });
@@ -181,7 +203,7 @@ const TransferToLayingsTable = () => {
isLoadingOptions: isLoadingFlockSourceOptions, isLoadingOptions: isLoadingFlockSourceOptions,
loadMore: loadMoreFlockSource, loadMore: loadMoreFlockSource,
hasMore: hasMoreFlockSource, hasMore: hasMoreFlockSource,
} = useSelect<Flock>(FlockApi.basePath, 'id', 'name'); } = useSelect<Flock>(ProjectFlockApi.basePath, 'id', 'flock_name');
const { const {
setInputValue: setFlockDestinationInputValue, setInputValue: setFlockDestinationInputValue,
@@ -189,7 +211,7 @@ const TransferToLayingsTable = () => {
isLoadingOptions: isLoadingFlockDestinationOptions, isLoadingOptions: isLoadingFlockDestinationOptions,
loadMore: loadMoreFlockDestination, loadMore: loadMoreFlockDestination,
hasMore: hasMoreFlockDestination, hasMore: hasMoreFlockDestination,
} = useSelect<Flock>(FlockApi.basePath, 'id', 'name'); } = useSelect<Flock>(ProjectFlockApi.basePath, 'id', 'flock_name');
// Flocks value // Flocks value
const [selectedFlockSource, setSelectedFlockSource] = const [selectedFlockSource, setSelectedFlockSource] =
@@ -244,13 +266,6 @@ const TransferToLayingsTable = () => {
); );
}, },
}, },
{
header: '#',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{ {
accessorKey: 'transfer_date', accessorKey: 'transfer_date',
header: 'Tanggal Transfer', header: 'Tanggal Transfer',
@@ -274,6 +289,7 @@ const TransferToLayingsTable = () => {
{ {
accessorKey: 'notes', accessorKey: 'notes',
header: 'Alasan Transfer', header: 'Alasan Transfer',
enableSorting: false,
}, },
{ {
header: 'Status', header: 'Status',
@@ -282,34 +298,39 @@ const TransferToLayingsTable = () => {
props.row.original.approval.action === 'REJECTED'; props.row.original.approval.action === 'REJECTED';
let latestApprovalStepName = props.row.original.approval.step_name; let latestApprovalStepName = props.row.original.approval.step_name;
let pillBadgeColor: 'yellow' | 'green' | 'gray' | 'red' = 'gray'; let badgeColor: Color = 'neutral';
switch (latestApprovalStepName.toLowerCase()) { switch (latestApprovalStepName.toLowerCase()) {
case 'pengajuan': case 'pengajuan':
pillBadgeColor = 'yellow'; badgeColor = 'neutral';
break; break;
case 'disetujui': case 'disetujui':
pillBadgeColor = 'green'; badgeColor = 'success';
break; break;
} }
if (isLatestApprovalRejected) { if (isLatestApprovalRejected) {
pillBadgeColor = 'red'; badgeColor = 'error';
latestApprovalStepName = 'Ditolak'; latestApprovalStepName = 'Ditolak';
} }
return ( return (
<PillBadge <Badge
content={latestApprovalStepName} variant='soft'
color={pillBadgeColor} className={{
className='text-sm' badge: 'rounded-lg px-2 w-full flex flex-row justify-start',
/> }}
color={badgeColor}
>
<Icon icon='mdi:circle' width={12} height={12} color={badgeColor} />
{latestApprovalStepName}
</Badge>
); );
}, },
}, },
{ {
header: 'Aksi', id: 'actions',
cell: (props) => { cell: (props) => {
const currentPageSize = props.table.getPaginationRowModel().rows.length; const currentPageSize = props.table.getPaginationRowModel().rows.length;
const currentPageRows = props.table.getPaginationRowModel().flatRows; const currentPageRows = props.table.getPaginationRowModel().flatRows;
@@ -346,31 +367,13 @@ const TransferToLayingsTable = () => {
}; };
return ( return (
<> <RowOptionsMenu
{currentPageSize > 3 && ( props={props}
<RowDropdownOptions isLast2Rows={isLast2Rows}> approveClickHandler={approveClickHandler}
<RowOptionsMenu rejectClickHandler={rejectClickHandler}
type='dropdown' deleteClickHandler={deleteClickHandler}
props={props} popoverPosition={isLast2Rows ? 'top' : 'bottom'}
approveClickHandler={approveClickHandler} />
rejectClickHandler={rejectClickHandler}
deleteClickHandler={deleteClickHandler}
/>
</RowDropdownOptions>
)}
{currentPageSize <= 3 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
props={props}
approveClickHandler={approveClickHandler}
rejectClickHandler={rejectClickHandler}
deleteClickHandler={deleteClickHandler}
/>
</RowCollapseOptions>
)}
</>
); );
}, },
}, },
@@ -397,17 +400,21 @@ const TransferToLayingsTable = () => {
const confirmationModalDeleteClickHandler = async () => { const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
try { const deleteResponse = await TransferToLayingApi.delete(
await TransferToLayingApi.delete(selectedTransferToLaying?.id as number); selectedTransferToLaying?.id as number
);
toast.success('Berhasil menghapus data transfer ke laying!'); if (isResponseError(deleteResponse)) {
refreshTransferToLayings(); toast.error(deleteResponse.message);
} catch (error) {
toast.success('Gagal menghapus data transfer ke laying!');
} finally {
deleteModal.closeModal();
setIsDeleteLoading(false); setIsDeleteLoading(false);
return;
} }
refreshTransferToLayings();
deleteModal.closeModal();
toast.success('Berhasil menghapus data transfer ke laying!');
setIsDeleteLoading(false);
}; };
const confirmationModalApproveClickHandler = async (notes: string) => { const confirmationModalApproveClickHandler = async (notes: string) => {
@@ -499,20 +506,19 @@ const TransferToLayingsTable = () => {
); );
}; };
// track sorting useEffect(() => {
// useEffect(() => { if (sorting.length === 1) {
// const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); updateFilter('filter_by', sorting[0].id);
updateFilter('sort_by', sorting[0].desc ? 'desc' : 'asc');
// if (!isNameSorted) { } else {
// updateFilter('nameSort', ''); updateFilter('filter_by', '');
// } else { updateFilter('sort_by', '');
// updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); }
// } }, [sorting]);
// }, [sorting, updateFilter]);
return ( return (
<> <>
<div className='w-full p-0 sm:p-4'> <div className='w-full p-0'>
<div className='flex flex-col gap-2 mb-4'> <div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col xl:flex-row justify-between items-end xl:items-center gap-2'> <div className='w-full flex flex-col xl:flex-row justify-between items-end xl:items-center gap-2'>
<div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'> <div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'>
@@ -579,12 +585,10 @@ const TransferToLayingsTable = () => {
</div> </div>
<div className='grid grid-cols-12 justify-end gap-4'> <div className='grid grid-cols-12 justify-end gap-4'>
<TextInput <DateInput
required
type='date'
label='Tanggal Transfer'
name='transfer_date' name='transfer_date'
placeholder='Masukkan tanggal transfer' label='Tanggal Transfer'
placeholder='Tanggal Transfer'
value={tableFilterState.transferDate} value={tableFilterState.transferDate}
onChange={transferDateChangeHandler} onChange={transferDateChangeHandler}
className={{ className={{
@@ -619,20 +623,6 @@ const TransferToLayingsTable = () => {
wrapper: 'col-span-12 sm:col-span-3', wrapper: 'col-span-12 sm:col-span-3',
}} }}
/> />
<SelectInput
label='Baris'
options={ROWS_OPTIONS}
value={{
label: String(tableFilterState.pageSize),
value: tableFilterState.pageSize,
}}
onChange={pageSizeChangeHandler}
className={{
wrapper:
'col-span-6 sm:col-span-3 max-w-28 sm:justify-self-end',
}}
/>
</div> </div>
</div> </div>
@@ -653,26 +643,21 @@ const TransferToLayingsTable = () => {
: 0 : 0
} }
onPageChange={setPage} onPageChange={setPage}
onPageSizeChange={setPageSize}
isLoading={isLoading} isLoading={isLoading}
sorting={sorting} sorting={sorting}
setSorting={setSorting} setSorting={setSorting}
rowSelection={rowSelection} rowSelection={rowSelection}
setRowSelection={setRowSelection} setRowSelection={setRowSelection}
enableRowSelection={tableEnableRowSelectionHandler} enableRowSelection={tableEnableRowSelectionHandler}
withCheckbox
className={{ className={{
containerClassName: cn({ containerClassName: cn({
'mb-20': 'w-full mb-20':
isResponseSuccess(transferToLayings) && isResponseSuccess(transferToLayings) &&
transferToLayings?.data?.length === 0, transferToLayings?.data?.length === 0,
}), }),
tableWrapperClassName: 'overflow-x-auto min-h-full!', headerColumnClassName: 'text-nowrap',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
}} }}
/> />
</div> </div>
@@ -80,7 +80,7 @@ export const TransferToLayingFormSchema: Yup.ObjectSchema<TransferToLayingFormSc
) )
.required('Kuantitas wajib diisi!'), .required('Kuantitas wajib diisi!'),
maxQuantity: Yup.number().min(1).required(), // internal helper field maxQuantity: Yup.number().min(0).required(), // internal helper field
}) })
) )
.min(1, 'Minimal 1 kandang terisi!') .min(1, 'Minimal 1 kandang terisi!')
@@ -102,7 +102,7 @@ export const TransferToLayingFormSchema: Yup.ObjectSchema<TransferToLayingFormSc
) )
.required('Kuantitas wajib diisi!'), .required('Kuantitas wajib diisi!'),
maxQuantity: Yup.number().min(1).required(), // internal helper field maxQuantity: Yup.number().min(0).required(), // internal helper field
}) })
) )
.min(1, 'Minimal 1 kandang terisi!') .min(1, 'Minimal 1 kandang terisi!')
@@ -123,6 +123,13 @@ const DailyMarketingsTable = ({
accessorKey: 'average_weight', accessorKey: 'average_weight',
header: 'Bobot Rata-Rata (Kg)', header: 'Bobot Rata-Rata (Kg)',
cell: (props) => formatNumber(props.row.original.average_weight_kg), cell: (props) => formatNumber(props.row.original.average_weight_kg),
footer: () => {
const totalAverageWeightKg = isResponseSuccess(dailyMarketings)
? dailyMarketings?.total?.total_average_weight
: 0;
return totalAverageWeightKg ? formatNumber(totalAverageWeightKg) : '-';
},
}, },
{ {
accessorKey: 'total_weight', accessorKey: 'total_weight',
@@ -140,6 +147,13 @@ const DailyMarketingsTable = ({
accessorKey: 'sales_price', accessorKey: 'sales_price',
header: 'Harga Jual (Rp)', header: 'Harga Jual (Rp)',
cell: (props) => formatCurrency(props.row.original.sales_price_per_kg), cell: (props) => formatCurrency(props.row.original.sales_price_per_kg),
footer: () => {
const totalSalesPrice = isResponseSuccess(dailyMarketings)
? dailyMarketings?.total?.total_sales_price
: 0;
return totalSalesPrice ? formatNumber(totalSalesPrice) : '-';
},
}, },
{ {
accessorKey: 'hpp_price', accessorKey: 'hpp_price',
+29
View File
@@ -0,0 +1,29 @@
import Button, { ButtonProps } from '@/components/Button';
export interface PopoverButtonProps extends ButtonProps {
popoverTarget: string;
anchorName: string;
}
const PopoverButton = ({
children,
popoverTarget,
anchorName,
...props
}: PopoverButtonProps) => {
return (
<Button
{...props}
popoverTarget={popoverTarget}
style={
{
anchorName: anchorName,
} as React.CSSProperties
}
>
{children}
</Button>
);
};
export default PopoverButton;
+71
View File
@@ -0,0 +1,71 @@
import { cn } from '@/lib/helper';
export interface PopoverContentProps {
children: React.ReactNode;
id: string;
anchorName: string; // Must include `--` like "--menu-anchor"
popover?: 'auto' | 'hint' | 'manual';
position?:
| 'top'
| 'bottom'
| 'left'
| 'right'
| 'top-start'
| 'top-end'
| 'bottom-start'
| 'bottom-end'
| 'left-start'
| 'left-end'
| 'right-start'
| 'right-end';
className?: string;
}
const positionAreaMap: Record<
NonNullable<PopoverContentProps['position']>,
string
> = {
top: 'top center',
bottom: 'bottom center',
left: 'left center',
right: 'right center',
'top-start': 'top left',
'top-end': 'top right',
'bottom-start': 'bottom left',
'bottom-end': 'bottom right',
'left-start': 'left top',
'left-end': 'left bottom',
'right-start': 'right top',
'right-end': 'right bottom',
};
const PopoverContent = ({
children,
id,
anchorName,
popover = 'auto',
position = 'bottom-start',
className,
}: PopoverContentProps) => {
return (
<div
className={cn(className)}
id={id}
popover={popover}
style={
{
inset: 'unset',
positionAnchor: anchorName,
positionArea: positionAreaMap[position],
} as React.CSSProperties
}
>
{children}
</div>
);
};
export default PopoverContent;
+1 -1
View File
@@ -12,7 +12,7 @@ const RowCollapseOptions = ({ children }: RowCollapseOptionsProps) => {
return ( return (
<Collapse <Collapse
title={ title={
<Button> <Button variant='ghost' color='none'>
<Icon icon='material-symbols:more-vert' width={16} height={16} /> <Icon icon='material-symbols:more-vert' width={16} height={16} />
</Button> </Button>
} }
+1 -1
View File
@@ -21,7 +21,7 @@ const RowDropdownOptions = ({
'dropdown-end': isLast2Rows, 'dropdown-end': isLast2Rows,
})} })}
> >
<Button tabIndex={0}> <Button tabIndex={0} variant='ghost' color='none'>
<Icon icon='material-symbols:more-vert' width={16} height={16} /> <Icon icon='material-symbols:more-vert' width={16} height={16} />
</Button> </Button>
+1 -1
View File
@@ -94,7 +94,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
permission: ['lti.production.recording.list'], permission: ['lti.production.recording.list'],
}, },
{ {
text: 'Transfer to Laying', text: 'Transfer ke Laying',
link: '/production/transfer-to-laying', link: '/production/transfer-to-laying',
}, },
{ {
+2
View File
@@ -41,7 +41,9 @@ export type DailyMarketingRow = BaseMetadata & BaseDailyMarketingRow;
export interface SalesSummary { export interface SalesSummary {
total_qty: number; total_qty: number;
total_average_weight: number;
total_weight_kg: number; total_weight_kg: number;
total_sales_price: number;
total_sales_amount: number; total_sales_amount: number;
total_hpp_amount: number; total_hpp_amount: number;
total_hpp_price_per_kg: number; total_hpp_price_per_kg: number;