mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-23 06:45:46 +00:00
Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into dev/hotfix/restu
This commit is contained in:
+7
-3
@@ -1,5 +1,6 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
@plugin "daisyui";
|
@plugin "daisyui";
|
||||||
|
@import '../styles/tailwind.css';
|
||||||
@import '../styles/daisyui.css';
|
@import '../styles/daisyui.css';
|
||||||
@import '../figma-make/styles/theme.css';
|
@import '../figma-make/styles/theme.css';
|
||||||
|
|
||||||
@@ -34,11 +35,11 @@
|
|||||||
/* Status/Utility Colors */
|
/* Status/Utility Colors */
|
||||||
--color-info: oklch(67.4% 0.176 238.9);
|
--color-info: oklch(67.4% 0.176 238.9);
|
||||||
--color-info-content: oklch(0% 0 0); /* #000000 */
|
--color-info-content: oklch(0% 0 0); /* #000000 */
|
||||||
--color-success: oklch(62.3% 0.147 149);
|
--color-success: #00d390;
|
||||||
--color-success-content: oklch(100% 0 0); /* #ffffff */
|
--color-success-content: oklch(100% 0 0); /* #ffffff */
|
||||||
--color-warning: oklch(82.2% 0.165 91.9);
|
--color-warning: oklch(82.2% 0.165 91.9);
|
||||||
--color-warning-content: oklch(0% 0 0); /* #000000 */
|
--color-warning-content: oklch(0% 0 0); /* #000000 */
|
||||||
--color-error: oklch(61.8% 0.203 27.8);
|
--color-error: #ff3a3a;
|
||||||
--color-error-content: oklch(100% 0 0); /* #fffffff */
|
--color-error-content: oklch(100% 0 0); /* #fffffff */
|
||||||
|
|
||||||
--radius-selector: 0rem;
|
--radius-selector: 0rem;
|
||||||
@@ -52,7 +53,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--color-primary: #1f74bf;
|
--color-primary: #0069e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
@@ -64,6 +65,9 @@
|
|||||||
--container-lg: 64rem;
|
--container-lg: 64rem;
|
||||||
--container-xl: 80rem;
|
--container-xl: 80rem;
|
||||||
--container-2xl: 96rem;
|
--container-2xl: 96rem;
|
||||||
|
|
||||||
|
--shadow-button-soft:
|
||||||
|
0 3px 2px -2px var(--color-base-200), 0 4px 3px -2px var(--color-base-200);
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import TransferToLayingsTable from '@/components/pages/production/transfer-to-laying/TransferToLayingsTable';
|
import TransferToLayingsTable from '@/components/pages/production/transfer-to-laying/TransferToLayingsTable';
|
||||||
|
import TransferToLayingFormModal from '@/components/pages/production/transfer-to-laying/TransferToLayingFormModal';
|
||||||
|
|
||||||
const TransferToLaying = () => {
|
const TransferToLaying = () => {
|
||||||
return (
|
return (
|
||||||
<section className='w-full p-4'>
|
<section className='w-full'>
|
||||||
<TransferToLayingsTable />
|
<TransferToLayingsTable />
|
||||||
|
|
||||||
|
<TransferToLayingFormModal />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,263 @@
|
|||||||
|
import React, { useId } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { cn, findMenuPath } from '@/lib/helper';
|
||||||
|
import { Size } from '@/types/theme';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import { MAIN_DRAWER_LINKS } from '@/config/constant';
|
||||||
|
|
||||||
|
interface BreadcrumbItem {
|
||||||
|
label: string;
|
||||||
|
href?: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
isActive?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BreadcrumbsProps extends React.HTMLAttributes<HTMLElement> {
|
||||||
|
items: BreadcrumbItem[];
|
||||||
|
size?: Size;
|
||||||
|
maxVisibleItems?: number;
|
||||||
|
showEllipsisDropdown?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildBreadcrumbs(pathname: string): BreadcrumbItem[] {
|
||||||
|
const menuPath = findMenuPath(MAIN_DRAWER_LINKS, pathname);
|
||||||
|
|
||||||
|
if (!menuPath) return [];
|
||||||
|
|
||||||
|
return menuPath.map((menu, index) => {
|
||||||
|
const isLast = index === menuPath.length - 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: menu.text,
|
||||||
|
href: isLast ? menu.link : undefined,
|
||||||
|
isActive: isLast,
|
||||||
|
icon: menu.icon ? (
|
||||||
|
<Icon icon={menu.icon} width={16} height={16} />
|
||||||
|
) : undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const EllipsisDropdown = ({
|
||||||
|
hiddenItems,
|
||||||
|
}: {
|
||||||
|
hiddenItems: BreadcrumbItem[];
|
||||||
|
}) => {
|
||||||
|
const dropdownId = useId();
|
||||||
|
const anchorId = useId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
{/* Ellipsis Button */}
|
||||||
|
<Button
|
||||||
|
popoverTarget={dropdownId}
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
anchorName: `--breadcrumb-ellipsis-anchor-${anchorId}`,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon icon='material-symbols:more-horiz' width={16} height={16} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Dropdown Menu using popover API */}
|
||||||
|
<ul
|
||||||
|
className='dropdown menu rounded-box bg-base-100 border border-base-300 shadow-lg z-[9999] [&_a:hover]:no-underline [&_a:focus]:no-underline [&&]:no-underline [&&_a]:no-underline [&&]:hover:no-underline [&&]:flex [&&]:items-start [&&]:justify-start w-max'
|
||||||
|
popover='auto'
|
||||||
|
id={dropdownId}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
positionAnchor: `--breadcrumb-ellipsis-anchor-${anchorId}`,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{hiddenItems.map((item, index) => {
|
||||||
|
const itemStyles = cn(
|
||||||
|
'[&]:flex [&]:items-center [&]:justify-start py-1 text-sm',
|
||||||
|
// Disabled state
|
||||||
|
item.isDisabled && 'text-base-content/40 opacity-50',
|
||||||
|
// Active/Last state
|
||||||
|
(item.isActive || item.isDisabled) && 'text-primary',
|
||||||
|
// Regular clickable state
|
||||||
|
!item.isDisabled && 'text-base-content/50'
|
||||||
|
);
|
||||||
|
|
||||||
|
const itemContent = (
|
||||||
|
<div className={itemStyles}>
|
||||||
|
{item.icon && (
|
||||||
|
<span className='inline-flex mr-2'>{item.icon}</span>
|
||||||
|
)}
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={`ellipsis-${index}`}
|
||||||
|
className='[&&]:text-left [&&]:block w-full'
|
||||||
|
>
|
||||||
|
{item.href && !item.isDisabled ? (
|
||||||
|
<Link
|
||||||
|
href={item.href}
|
||||||
|
className='block !no-underline [&&]:text-left w-full'
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{itemContent}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<div className='block !no-underline [&&]:cursor-default [&&]:hover:cursor-default [&&]:hover:bg-base-100 [&&]:text-left'>
|
||||||
|
{itemContent}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Breadcrumb = ({
|
||||||
|
items,
|
||||||
|
size = 'md',
|
||||||
|
maxVisibleItems = 3,
|
||||||
|
showEllipsisDropdown = true,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: BreadcrumbsProps) => {
|
||||||
|
const sizeClasses = {
|
||||||
|
xs: 'text-xs',
|
||||||
|
sm: 'text-sm',
|
||||||
|
md: 'text-base',
|
||||||
|
lg: 'text-lg',
|
||||||
|
xl: 'text-xl',
|
||||||
|
};
|
||||||
|
|
||||||
|
const getItemStyles = (
|
||||||
|
item: BreadcrumbItem,
|
||||||
|
position: 'first' | 'middle' | 'last' = 'middle'
|
||||||
|
) => {
|
||||||
|
const baseClasses = 'inline-flex items-center gap-2';
|
||||||
|
|
||||||
|
// Disabled state
|
||||||
|
if (item.isDisabled) {
|
||||||
|
return `${baseClasses} text-base-content/40 !cursor-default opacity-50 hover:!no-underline`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active/Last state (no underline)
|
||||||
|
if (item.isActive || position === 'last') {
|
||||||
|
return `${baseClasses} text-primary !cursor-pointer hover:!no-underline`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular clickable state
|
||||||
|
return `${baseClasses} text-base-content/60`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderItem = (
|
||||||
|
item: BreadcrumbItem,
|
||||||
|
position: 'first' | 'middle' | 'last' = 'middle'
|
||||||
|
) => {
|
||||||
|
const styles = getItemStyles(item, position);
|
||||||
|
|
||||||
|
// Disabled items
|
||||||
|
if (item.isDisabled) {
|
||||||
|
return (
|
||||||
|
<span className={styles}>
|
||||||
|
{item.icon && item.icon}
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active/Last items
|
||||||
|
if (item.isActive || position === 'last') {
|
||||||
|
if (item.href) {
|
||||||
|
return (
|
||||||
|
<Link href={item.href} className={styles}>
|
||||||
|
{item.icon && (
|
||||||
|
<span className='inline-flex gap-2'>{item.icon}</span>
|
||||||
|
)}
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span className={styles}>
|
||||||
|
{item.icon && item.icon}
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular items
|
||||||
|
if (item.href) {
|
||||||
|
return (
|
||||||
|
<Link href={item.href} className={styles}>
|
||||||
|
{item.icon && <span className='inline-flex gap-2'>{item.icon}</span>}
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={styles}>
|
||||||
|
{item.icon && item.icon}
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderBreadcrumbList = () => {
|
||||||
|
// Show all items if within limit
|
||||||
|
if (items.length <= maxVisibleItems) {
|
||||||
|
return items.map((item, index) => {
|
||||||
|
const position =
|
||||||
|
index === 0
|
||||||
|
? 'first'
|
||||||
|
: index === items.length - 1
|
||||||
|
? 'last'
|
||||||
|
: 'middle';
|
||||||
|
return <li key={index}>{renderItem(item, position)}</li>;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collapsed items indexing when exceeding limit
|
||||||
|
const firstItem = items[0];
|
||||||
|
const lastItem = items[items.length - 1];
|
||||||
|
const visibleMiddleItems = items.slice(1, -1).slice(-(maxVisibleItems - 2));
|
||||||
|
const hiddenItems = items.slice(1, -1).slice(0, -(maxVisibleItems - 2));
|
||||||
|
const showEllipsis = showEllipsisDropdown && hiddenItems.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<li>{renderItem(firstItem, 'first')}</li>
|
||||||
|
|
||||||
|
{/* Ellipsis for hidden items with dropdown */}
|
||||||
|
{showEllipsis && <EllipsisDropdown hiddenItems={hiddenItems} />}
|
||||||
|
|
||||||
|
{/* Middle items */}
|
||||||
|
{visibleMiddleItems.map((item, index) => (
|
||||||
|
<li key={`middle-${index}`}>{renderItem(item, 'middle')}</li>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<li>{renderItem(lastItem, 'last')}</li>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
aria-label='Breadcrumb'
|
||||||
|
className={cn('breadcrumbs', sizeClasses[size], className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ul className='text-sm'>{renderBreadcrumbList()}</ul>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Breadcrumb;
|
||||||
@@ -2,11 +2,12 @@ import react from 'react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { Color } from '@/types/theme';
|
import { Color } from '@/types/theme';
|
||||||
|
import { UrlObject } from 'url';
|
||||||
|
|
||||||
export interface ButtonProps extends react.ComponentProps<'button'> {
|
export interface ButtonProps extends react.ComponentProps<'button'> {
|
||||||
variant?: 'soft' | 'outline' | 'dash' | 'ghost' | 'link' | 'active';
|
variant?: 'soft' | 'outline' | 'dash' | 'ghost' | 'link' | 'active';
|
||||||
color?: Color;
|
color?: Color;
|
||||||
href?: string;
|
href?: string | UrlObject;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
target?: string;
|
target?: string;
|
||||||
rel?: string;
|
rel?: string;
|
||||||
|
|||||||
@@ -78,40 +78,6 @@ const MainDrawer = ({
|
|||||||
permissionCheck(permission)
|
permissionCheck(permission)
|
||||||
);
|
);
|
||||||
|
|
||||||
const getPageTitle = useCallback(() => {
|
|
||||||
let title = '';
|
|
||||||
|
|
||||||
const activeMenu = MAIN_DRAWER_LINKS.find((item) =>
|
|
||||||
isPathActive(pathname, item.link)
|
|
||||||
);
|
|
||||||
|
|
||||||
const traverseMenuTitle = (menu: typeof activeMenu) => {
|
|
||||||
if (!menu) return;
|
|
||||||
|
|
||||||
const hasSubmenu = menu?.submenu && menu?.submenu.length > 0;
|
|
||||||
|
|
||||||
if (!title) {
|
|
||||||
title += menu?.text;
|
|
||||||
} else {
|
|
||||||
title += ' - ' + menu?.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasSubmenu || !menu.submenu) return;
|
|
||||||
|
|
||||||
const activeSubmenu = menu.submenu?.find((item) =>
|
|
||||||
isPathActive(pathname, item.link)
|
|
||||||
);
|
|
||||||
|
|
||||||
traverseMenuTitle(activeSubmenu);
|
|
||||||
};
|
|
||||||
|
|
||||||
traverseMenuTitle(activeMenu);
|
|
||||||
|
|
||||||
return title;
|
|
||||||
}, [pathname]);
|
|
||||||
|
|
||||||
const pageTitle = getPageTitle();
|
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
setMainDrawerOpen(!mainDrawerOpen);
|
setMainDrawerOpen(!mainDrawerOpen);
|
||||||
};
|
};
|
||||||
@@ -132,7 +98,7 @@ const MainDrawer = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<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 toggleSidebar={toggleSidebar} />
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -53,15 +53,25 @@ interface ModalProps {
|
|||||||
ref: RefObject<HTMLDialogElement | null>;
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
closeOnBackdrop?: boolean;
|
closeOnBackdrop?: boolean;
|
||||||
|
onBackdropClick?: () => void;
|
||||||
|
position?: 'top' | 'middle' | 'bottom' | 'start' | 'end';
|
||||||
className?: {
|
className?: {
|
||||||
modal?: string;
|
modal?: string;
|
||||||
modalBox?: string;
|
modalBox?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const Modal = ({ ref, children, closeOnBackdrop, className }: ModalProps) => {
|
const Modal = ({
|
||||||
|
ref,
|
||||||
|
children,
|
||||||
|
closeOnBackdrop,
|
||||||
|
onBackdropClick,
|
||||||
|
position = 'middle',
|
||||||
|
className,
|
||||||
|
}: ModalProps) => {
|
||||||
const handleBackdropClick = (e: React.MouseEvent<HTMLDialogElement>) => {
|
const handleBackdropClick = (e: React.MouseEvent<HTMLDialogElement>) => {
|
||||||
if (closeOnBackdrop && e.target === ref.current) {
|
if (closeOnBackdrop && e.target === ref.current) {
|
||||||
|
onBackdropClick?.();
|
||||||
ref.current?.close();
|
ref.current?.close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -69,7 +79,17 @@ const Modal = ({ ref, children, closeOnBackdrop, className }: ModalProps) => {
|
|||||||
return (
|
return (
|
||||||
<dialog
|
<dialog
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('modal', className?.modal)}
|
className={cn(
|
||||||
|
'modal',
|
||||||
|
{
|
||||||
|
'modal-top': position === 'top',
|
||||||
|
'modal-middle': position === 'middle',
|
||||||
|
'modal-bottom': position === 'bottom',
|
||||||
|
'modal-start': position === 'start',
|
||||||
|
'modal-end': position === 'end',
|
||||||
|
},
|
||||||
|
className?.modal
|
||||||
|
)}
|
||||||
onClick={handleBackdropClick}
|
onClick={handleBackdropClick}
|
||||||
>
|
>
|
||||||
<div className={cn('modal-box', className?.modalBox)}>{children}</div>
|
<div className={cn('modal-box', className?.modalBox)}>{children}</div>
|
||||||
|
|||||||
+41
-31
@@ -1,26 +1,26 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Menu from '@/components/menu/Menu';
|
|
||||||
import MenuItem from '@/components/menu/MenuItem';
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import Dropdown from '@/components/Dropdown';
|
import Breadcrumb, { buildBreadcrumbs } from '@/components/Breadcrumb';
|
||||||
|
import PopoverButton from '@/components/popover/PopoverButton';
|
||||||
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
|
|
||||||
import { useAuth } from '@/services/hooks/useAuth';
|
import { useAuth } from '@/services/hooks/useAuth';
|
||||||
import { AuthApi } from '@/services/api/auth';
|
import { AuthApi } from '@/services/api/auth';
|
||||||
import { isResponseError } from '@/lib/api-helper';
|
import { isResponseError } from '@/lib/api-helper';
|
||||||
|
|
||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
title: string;
|
|
||||||
toggleSidebar?: () => void;
|
toggleSidebar?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
|
const Navbar = ({ toggleSidebar }: NavbarProps) => {
|
||||||
const { setUser } = useAuth();
|
const { setUser } = useAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const logoutClickHandler = async () => {
|
const logoutClickHandler = async () => {
|
||||||
const logoutRes = await AuthApi.logout();
|
const logoutRes = await AuthApi.logout();
|
||||||
@@ -35,42 +35,52 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='navbar px-4 bg-base-100 shadow-sm'>
|
<div className='navbar p-3 bg-base-100 border-b border-base-content/10'>
|
||||||
<div className='flex-1'>
|
<div className='flex-1'>
|
||||||
<div className='flex flex-row items-center gap-4'>
|
<div className='flex flex-row items-center gap-4'>
|
||||||
{toggleSidebar && (
|
{toggleSidebar && (
|
||||||
<Button onClick={toggleSidebar} className='block lg:hidden'>
|
<Button
|
||||||
<Icon
|
variant='ghost'
|
||||||
icon='material-symbols:menu-rounded'
|
color='none'
|
||||||
width={24}
|
onClick={toggleSidebar}
|
||||||
height={24}
|
className='block lg:hidden p-[9px] text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||||
/>
|
>
|
||||||
|
<Icon icon='heroicons:bars-3' width={20} height={20} />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<span className='font-bold text-xl text-primary'>{title}</span>
|
<Breadcrumb items={buildBreadcrumbs(pathname)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<Dropdown
|
<PopoverButton
|
||||||
align='end'
|
tabIndex={0}
|
||||||
direction='bottom'
|
variant='ghost'
|
||||||
trigger={
|
color='none'
|
||||||
<div className='btn btn-ghost btn-circle avatar'>
|
popoverTarget='accountNavbar'
|
||||||
<div className='w-10 rounded-full border flex justify-center items-center'>
|
anchorName='--account-navbar'
|
||||||
<Icon icon='uil:user' width={40} height={40} />
|
className='p-[9px] text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
className={{
|
|
||||||
content: 'w-52 mt-3',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Menu>
|
<Icon icon='heroicons:user' width={20} height={20} />
|
||||||
<MenuItem title='Logout' onClick={logoutClickHandler} />
|
</PopoverButton>
|
||||||
</Menu>
|
|
||||||
</Dropdown>
|
<PopoverContent
|
||||||
|
id='accountNavbar'
|
||||||
|
anchorName='--account-navbar'
|
||||||
|
position='bottom-start'
|
||||||
|
className='rounded-xl border border-base-content/5 shadow-sm'
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={logoutClickHandler}
|
||||||
|
variant='ghost'
|
||||||
|
color='error'
|
||||||
|
className='p-3 justify-start text-sm font-semibold w-full'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons-outline:logout' width={20} height={20} />
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
</PopoverContent>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ interface TableClassNames {
|
|||||||
headerColumnClassName?: string;
|
headerColumnClassName?: string;
|
||||||
tableBodyClassName?: string;
|
tableBodyClassName?: string;
|
||||||
bodyRowClassName?: string;
|
bodyRowClassName?: string;
|
||||||
|
selectedBodyRowClassName?: string;
|
||||||
bodyColumnClassName?: string;
|
bodyColumnClassName?: string;
|
||||||
tableFooterClassName?: string;
|
tableFooterClassName?: string;
|
||||||
footerRowClassName?: string;
|
footerRowClassName?: string;
|
||||||
@@ -88,9 +89,11 @@ export const TABLE_DEFAULT_STYLING = {
|
|||||||
headerColumnClassName:
|
headerColumnClassName:
|
||||||
'px-4 py-3 border-base-content/10 text-base-content/50 text-sm font-medium',
|
'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:
|
||||||
|
'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',
|
||||||
paginationClassName: '',
|
paginationClassName: 'px-3',
|
||||||
tableFooterClassName: 'font-semibold border-base-content/10',
|
tableFooterClassName: 'font-semibold border-base-content/10',
|
||||||
footerRowClassName: 'bg-base-200 border-t-2 border-base-content/10',
|
footerRowClassName: 'bg-base-200 border-t-2 border-base-content/10',
|
||||||
footerColumnClassName: 'p-4 text-base-content whitespace-nowrap',
|
footerColumnClassName: 'p-4 text-base-content whitespace-nowrap',
|
||||||
@@ -353,7 +356,11 @@ const Table = <TData extends object>({
|
|||||||
key={row.id}
|
key={row.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
TABLE_DEFAULT_STYLING.bodyRowClassName,
|
TABLE_DEFAULT_STYLING.bodyRowClassName,
|
||||||
tableClassNames.bodyRowClassName
|
tableClassNames.bodyRowClassName,
|
||||||
|
{
|
||||||
|
[tableClassNames.selectedBodyRowClassName]:
|
||||||
|
row.getIsSelected(),
|
||||||
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import Badge from '@/components/Badge';
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
import { Color } from '@/types/theme';
|
||||||
|
|
||||||
|
interface StatusBadgeProps {
|
||||||
|
color: Color;
|
||||||
|
text: string;
|
||||||
|
className?: {
|
||||||
|
badge?: string;
|
||||||
|
status?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusBadge = ({
|
||||||
|
color = 'neutral',
|
||||||
|
text,
|
||||||
|
className,
|
||||||
|
}: StatusBadgeProps) => {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant='soft'
|
||||||
|
className={{
|
||||||
|
badge: cn(
|
||||||
|
'px-2 py-1 w-full flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content',
|
||||||
|
{
|
||||||
|
'bg-base-content/5': color === 'neutral',
|
||||||
|
'bg-success/30': color === 'success',
|
||||||
|
'bg-error/20': color === 'error',
|
||||||
|
'bg-primary/20': color === 'info',
|
||||||
|
},
|
||||||
|
className?.badge
|
||||||
|
),
|
||||||
|
status: cn(className?.status),
|
||||||
|
}}
|
||||||
|
color={color}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
height='12'
|
||||||
|
width='12'
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
className={cn({
|
||||||
|
'text-base-content/10': color === 'neutral',
|
||||||
|
'text-success': color === 'success',
|
||||||
|
'text-error': color === 'error',
|
||||||
|
'text-primary': color === 'info',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<circle r='6' cx='6' cy='6' fill='currentColor' />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{text}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatusBadge;
|
||||||
@@ -204,17 +204,12 @@ const DateInput = ({
|
|||||||
const finalErrorMessage = internalError || externalErrorMessage;
|
const finalErrorMessage = internalError || externalErrorMessage;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={cn('w-full flex flex-col text-start', className?.wrapper)}>
|
||||||
className={cn(
|
|
||||||
'w-full flex flex-col gap-2 text-start',
|
|
||||||
className?.wrapper
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{label && (
|
{label && (
|
||||||
<label
|
<label
|
||||||
htmlFor={name}
|
htmlFor={name}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full text-sm font-normal leading-5',
|
'w-full py-2 text-xs font-semibold leading-5',
|
||||||
{ 'text-error': finalIsError },
|
{ 'text-error': finalIsError },
|
||||||
className?.label
|
className?.label
|
||||||
)}
|
)}
|
||||||
@@ -231,7 +226,7 @@ const DateInput = ({
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'input h-12 bg-inherit px-4 py-2 text-base font-normal leading-6 w-full rounded transition-all duration-200 flex items-center border',
|
'input h-12 bg-inherit px-3 py-2.5 text-base font-normal leading-6 w-full rounded-lg transition-all duration-200 flex items-center border border-base-content/10',
|
||||||
{
|
{
|
||||||
'border-error': finalIsError,
|
'border-error': finalIsError,
|
||||||
'border-success': externalValid && !finalIsError,
|
'border-success': externalValid && !finalIsError,
|
||||||
@@ -261,10 +256,10 @@ const DateInput = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Icon
|
<Icon
|
||||||
icon='uil:calendar'
|
icon='heroicons:calendar-date-range'
|
||||||
width={24}
|
width={14}
|
||||||
height={24}
|
height={14}
|
||||||
className='cursor-pointer text-dark'
|
className='cursor-pointer text-base-content/20'
|
||||||
onClick={(e) =>
|
onClick={(e) =>
|
||||||
handleClick(e as unknown as React.MouseEvent<HTMLInputElement>)
|
handleClick(e as unknown as React.MouseEvent<HTMLInputElement>)
|
||||||
}
|
}
|
||||||
@@ -272,10 +267,10 @@ const DateInput = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!finalIsError && bottomLabel && (
|
{!finalIsError && bottomLabel && (
|
||||||
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
<p className='w-full mt-1.5 text-xs opacity-60'>{bottomLabel}</p>
|
||||||
)}
|
)}
|
||||||
{finalIsError && finalErrorMessage && (
|
{finalIsError && finalErrorMessage && (
|
||||||
<p className='w-full text-sm text-error'>{finalErrorMessage}</p>
|
<p className='w-full mt-1.5 text-xs text-error'>{finalErrorMessage}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ const CustomControl = <
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactSelectComponents.Control {...props}>
|
<ReactSelectComponents.Control {...props}>
|
||||||
<div className='flex-1 px-4! py-1.5 gap-1 flex items-center'>
|
<div className='flex-1 p-3! py-1.5 gap-1 flex items-center'>
|
||||||
{shouldShowAdornment && startAdornment}
|
{shouldShowAdornment && startAdornment}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
@@ -118,7 +118,7 @@ const CustomMenuList = <
|
|||||||
{children}
|
{children}
|
||||||
|
|
||||||
{options.length > 0 && isLoading && (
|
{options.length > 0 && isLoading && (
|
||||||
<div className='mt-1 px-3 py-2 rounded-md text-center text-gray-400'>
|
<div className='px-3 py-2 rounded-md text-center text-gray-400'>
|
||||||
<span className='loading loading-spinner loading-md' />
|
<span className='loading loading-spinner loading-md' />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -204,16 +204,11 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={cn('w-full flex flex-col text-start', className?.wrapper)}>
|
||||||
className={cn(
|
|
||||||
'w-full flex flex-col gap-2 text-start',
|
|
||||||
className?.wrapper
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{label && (
|
{label && (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full text-sm font-normal leading-5',
|
'w-full py-2 text-xs font-semibold leading-5',
|
||||||
{ 'text-error': isError },
|
{ 'text-error': isError },
|
||||||
className?.label
|
className?.label
|
||||||
)}
|
)}
|
||||||
@@ -253,33 +248,36 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
...(!startAdornment && {
|
...(!startAdornment && {
|
||||||
control: ({ isFocused, isDisabled }) =>
|
control: ({ isFocused, isDisabled }) =>
|
||||||
cn(
|
cn(
|
||||||
'w-full min-h-12! rounded border bg-white transition-shadow cursor-pointer!',
|
'w-full min-h-12! rounded-lg! border bg-white transition-shadow cursor-pointer!',
|
||||||
{
|
{
|
||||||
'border-red-500! ring-2 ring-red-200': isError,
|
'border-red-500! ring-2 ring-red-200': isError,
|
||||||
'border-indigo-500 ring-2 ring-indigo-200': isFocused,
|
'border-indigo-500 ring-2 ring-indigo-200': isFocused,
|
||||||
'border-gray-300': !isError && !isFocused,
|
'border-base-content/10!': !isError && !isFocused,
|
||||||
'bg-gray-100 text-gray-400 cursor-not-allowed': isDisabled,
|
'bg-gray-100 text-gray-400 cursor-not-allowed': isDisabled,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
valueContainer: () => cn('flex-1 px-4! py-2! gap-1'),
|
valueContainer: () => cn('flex-1 p-3! py-2! gap-1'),
|
||||||
}),
|
}),
|
||||||
placeholder: () =>
|
placeholder: () =>
|
||||||
cn({ 'text-gray-400': !isError, 'text-red-300!': isError }),
|
cn({ 'text-gray-400': !isError, 'text-red-300!': isError }),
|
||||||
singleValue: () =>
|
singleValue: () =>
|
||||||
cn({ 'text-gray-900': !isError, 'text-error!': isError }),
|
cn({ 'text-gray-900': !isError, 'text-error!': isError }),
|
||||||
input: () => cn('text-gray-900'),
|
input: () => cn('text-gray-900 m-0! p-0!'),
|
||||||
indicatorsContainer: () => cn('flex items-center gap-1 pr-2'),
|
indicatorsContainer: () => cn('flex items-center gap-1 pr-2'),
|
||||||
dropdownIndicator: ({ isFocused }) =>
|
dropdownIndicator: ({ isFocused }) =>
|
||||||
cn('p-1 rounded hover:bg-gray-100', {
|
cn('p-1! rounded hover:bg-gray-100', {
|
||||||
'text-gray-900': isFocused,
|
'text-gray-900': isFocused,
|
||||||
'text-gray-500': !isFocused,
|
'text-gray-500': !isFocused,
|
||||||
'text-error!': isError,
|
'text-error!': isError,
|
||||||
}),
|
}),
|
||||||
|
clearIndicator: () => cn('p-1! rounded hover:bg-gray-100'),
|
||||||
menu: () =>
|
menu: () =>
|
||||||
cn('border border-gray-200 rounded! bg-base-100 shadow-lg!'),
|
cn(
|
||||||
menuList: () => cn('p-2! max-h-60 overflow-auto'),
|
'border border-base-content/5 rounded-xl! bg-base-100 shadow-lg! my-1.5!'
|
||||||
|
),
|
||||||
|
menuList: () => cn('p-0! max-h-60 overflow-auto'),
|
||||||
option: ({ isFocused, isSelected }) =>
|
option: ({ isFocused, isSelected }) =>
|
||||||
cn('mt-1 px-3 py-2 rounded-md cursor-pointer!', {
|
cn('px-3 py-2 rounded-md cursor-pointer!', {
|
||||||
'bg-indigo-600 text-white': isFocused,
|
'bg-indigo-600 text-white': isFocused,
|
||||||
'bg-blue-500!': isSelected,
|
'bg-blue-500!': isSelected,
|
||||||
'text-gray-700': !isFocused && !isSelected,
|
'text-gray-700': !isFocused && !isSelected,
|
||||||
@@ -287,13 +285,17 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
multiValue: ({ getValue, index }) => {
|
multiValue: ({ getValue, index }) => {
|
||||||
const selectedValues = getValue() as T[];
|
const selectedValues = getValue() as T[];
|
||||||
return cn(
|
return cn(
|
||||||
'bg-indigo-50 rounded py-0.5 pl-2 pr-1 flex items-center gap-1!',
|
'bg-base-200! rounded-lg! py-[3px] px-2.5 m-0! flex items-center gap-1! w-fit gap-2!',
|
||||||
selectedValues[index]?.className
|
selectedValues[index]?.className
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
multiValueRemove: () => cn('p-0! w-3 h-3'),
|
||||||
multiValueLabel: ({ getValue, index }) => {
|
multiValueLabel: ({ getValue, index }) => {
|
||||||
const selectedValues = getValue() as T[];
|
const selectedValues = getValue() as T[];
|
||||||
return cn('text-indigo-700', selectedValues[index]?.labelClassName);
|
return cn(
|
||||||
|
'p-0! text-base-content! text-xs!',
|
||||||
|
selectedValues[index]?.labelClassName
|
||||||
|
);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
components={{
|
components={{
|
||||||
|
|||||||
@@ -25,14 +25,18 @@ const CheckboxOption = <
|
|||||||
>(
|
>(
|
||||||
props: OptionProps<T, IsMulti, Group>
|
props: OptionProps<T, IsMulti, Group>
|
||||||
) => {
|
) => {
|
||||||
const { isSelected, label, innerRef, innerProps, className } = props;
|
const { isSelected, label, innerRef, innerProps, className, isFocused } =
|
||||||
|
props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={innerRef}
|
ref={innerRef}
|
||||||
{...innerProps}
|
{...innerProps}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-2 px-3 py-2 cursor-pointer',
|
'flex items-center gap-3 p-3 cursor-pointer transition-all hover:bg-primary/5',
|
||||||
|
{
|
||||||
|
'bg-primary/5': isFocused,
|
||||||
|
},
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -40,9 +44,12 @@ const CheckboxOption = <
|
|||||||
type='checkbox'
|
type='checkbox'
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
onChange={() => null}
|
onChange={() => null}
|
||||||
className='checkbox checkbox-sm rounded checkbox-primary pointer-events-none'
|
className='checkbox checkbox-sm rounded-md checkbox-primary pointer-events-none border-base-content/10'
|
||||||
/>
|
/>
|
||||||
<label className='cursor-pointer flex-1 select-none'>{label}</label>
|
|
||||||
|
<label className='cursor-pointer flex-1 select-none text-sm text-base-content/50 font-medium'>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,14 +21,18 @@ const RadioOption = <
|
|||||||
>(
|
>(
|
||||||
props: OptionProps<T, IsMulti, Group>
|
props: OptionProps<T, IsMulti, Group>
|
||||||
) => {
|
) => {
|
||||||
const { isSelected, label, innerRef, innerProps, className } = props;
|
const { isSelected, label, innerRef, innerProps, className, isFocused } =
|
||||||
|
props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={innerRef}
|
ref={innerRef}
|
||||||
{...innerProps}
|
{...innerProps}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-2 px-3 py-2 cursor-pointer',
|
'flex items-center gap-3 p-3 cursor-pointer transition-all hover:bg-primary/5',
|
||||||
|
{
|
||||||
|
'bg-primary/5': isFocused,
|
||||||
|
},
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -36,9 +40,12 @@ const RadioOption = <
|
|||||||
type='radio'
|
type='radio'
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
onChange={() => null}
|
onChange={() => null}
|
||||||
className='radio radio-sm radio-primary pointer-events-none'
|
className='radio radio-md radio-primary pointer-events-none'
|
||||||
/>
|
/>
|
||||||
<label className='cursor-pointer flex-1 select-none'>{label}</label>
|
|
||||||
|
<label className='cursor-pointer flex-1 select-none text-sm text-base-content/50 font-medium'>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const TextArea = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full flex flex-col gap-2 text-start',
|
'w-full flex flex-col gap-0 text-start',
|
||||||
className?.wrapper
|
className?.wrapper
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -61,7 +61,7 @@ const TextArea = ({
|
|||||||
<label
|
<label
|
||||||
htmlFor={name}
|
htmlFor={name}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full text-sm font-normal leading-5',
|
'w-full py-2 text-xs font-semibold leading-5',
|
||||||
{
|
{
|
||||||
'text-error': isError,
|
'text-error': isError,
|
||||||
},
|
},
|
||||||
@@ -83,7 +83,7 @@ const TextArea = ({
|
|||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
className={cn(
|
className={cn(
|
||||||
'textarea h-auto px-4 py-2 text-base font-normal leading-6 w-full rounded outline-none! transition-all bg-white',
|
'textarea h-auto px-3 py-2.5 text-base font-normal leading-6 w-full rounded-lg outline-none! transition-all bg-white border-base-content/10',
|
||||||
{
|
{
|
||||||
'border-error': isError,
|
'border-error': isError,
|
||||||
'border-success!': isValid,
|
'border-success!': isValid,
|
||||||
@@ -110,9 +110,11 @@ const TextArea = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!isError && bottomLabel && (
|
{!isError && bottomLabel && (
|
||||||
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
<p className='w-full mt-1.5 text-xs opacity-60'>{bottomLabel}</p>
|
||||||
|
)}
|
||||||
|
{isError && (
|
||||||
|
<p className='w-full mt-1.5 text-xs text-error'>{errorMessage}</p>
|
||||||
)}
|
)}
|
||||||
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export interface TextInputProps {
|
|||||||
label?: string;
|
label?: string;
|
||||||
inputWrapper?: string;
|
inputWrapper?: string;
|
||||||
input?: string;
|
input?: string;
|
||||||
|
inputPrefix?: string;
|
||||||
|
inputSuffix?: string;
|
||||||
|
inputPrefixSuffixWrapper?: string;
|
||||||
};
|
};
|
||||||
isError?: boolean;
|
isError?: boolean;
|
||||||
isValid?: boolean;
|
isValid?: boolean;
|
||||||
@@ -62,7 +65,7 @@ const TextInput = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full flex flex-col gap-2 text-start',
|
'w-full flex flex-col gap-0 text-start rounded-lg',
|
||||||
className?.wrapper
|
className?.wrapper
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -70,7 +73,7 @@ const TextInput = ({
|
|||||||
<label
|
<label
|
||||||
htmlFor={name}
|
htmlFor={name}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full text-sm font-normal leading-5',
|
'w-full py-2 text-xs font-semibold leading-5',
|
||||||
{
|
{
|
||||||
'text-error': isError,
|
'text-error': isError,
|
||||||
},
|
},
|
||||||
@@ -90,15 +93,23 @@ const TextInput = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{inputPrefix || inputSuffix ? (
|
{inputPrefix || inputSuffix ? (
|
||||||
<div className='relative flex'>
|
<div
|
||||||
|
className={cn(
|
||||||
|
'relative flex text-sm',
|
||||||
|
className?.inputPrefixSuffixWrapper
|
||||||
|
)}
|
||||||
|
>
|
||||||
{inputPrefix && (
|
{inputPrefix && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center px-4 py-2 border border-r-0 rounded-l-md transition-all duration-200',
|
'inline-flex items-center px-3 py-2.5 border border-r-0 border-base-content/10 rounded-l-lg transition-all duration-200',
|
||||||
{
|
{
|
||||||
'bg-gray-100 border-gray-300': !disabled,
|
'bg-gray-100 border-gray-300': !disabled,
|
||||||
'bg-gray-50 border-gray-200': disabled,
|
'bg-gray-50 border-gray-200': disabled,
|
||||||
}
|
'border-error': isError,
|
||||||
|
'border-success!': isValid,
|
||||||
|
},
|
||||||
|
className?.inputPrefix
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{inputPrefix}
|
{inputPrefix}
|
||||||
@@ -107,7 +118,7 @@ const TextInput = ({
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'input h-12 text-base font-normal leading-6 flex-1 rounded-lg! outline-none! transition-all duration-200 flex items-center bg-white',
|
'input h-12 px-3 py-2.5 text-sm font-normal leading-6 flex-1 rounded-lg! outline-none! transition-all duration-200 flex items-center bg-white border-base-content/10',
|
||||||
{
|
{
|
||||||
'border-error': isError,
|
'border-error': isError,
|
||||||
'border-success!': isValid,
|
'border-success!': isValid,
|
||||||
@@ -154,11 +165,14 @@ const TextInput = ({
|
|||||||
{inputSuffix && (
|
{inputSuffix && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center px-4 py-2 border border-l-0 rounded-r-md transition-all duration-200',
|
'inline-flex items-center px-3 py-2.5 border border-l-0 border-base-content/10 rounded-r-lg transition-all duration-200',
|
||||||
{
|
{
|
||||||
'bg-gray-100 border-gray-300': !disabled,
|
'bg-gray-100 border-gray-300': !disabled,
|
||||||
'bg-gray-50 border-gray-200': disabled,
|
'bg-gray-50 border-gray-200': disabled,
|
||||||
}
|
'border-error': isError,
|
||||||
|
'border-success!': isValid,
|
||||||
|
},
|
||||||
|
className?.inputSuffix
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{inputSuffix}
|
{inputSuffix}
|
||||||
@@ -168,7 +182,7 @@ const TextInput = ({
|
|||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'input h-12 px-4 py-2 text-base font-normal leading-6 w-full rounded-lg! outline-none! transition-all duration-200 bg-white',
|
'input h-12 px-3 py-2.5 text-sm font-normal leading-6 w-full rounded-lg! outline-none! transition-all duration-200 bg-white border-base-content/10',
|
||||||
{
|
{
|
||||||
'border-error': isError,
|
'border-error': isError,
|
||||||
'border-success!': isValid,
|
'border-success!': isValid,
|
||||||
@@ -202,10 +216,10 @@ const TextInput = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!isError && bottomLabel && (
|
{!isError && bottomLabel && (
|
||||||
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
<p className='w-full mt-1.5 text-xs opacity-60'>{bottomLabel}</p>
|
||||||
)}
|
)}
|
||||||
{isError && errorMessage && (
|
{isError && errorMessage && (
|
||||||
<p className='w-full text-sm text-error'>{errorMessage}</p>
|
<p className='w-full mt-1.5 text-xs text-error'>{errorMessage}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -35,29 +35,29 @@ const iconConfig = {
|
|||||||
info: {
|
info: {
|
||||||
icon: 'material-symbols:info-outline-rounded',
|
icon: 'material-symbols:info-outline-rounded',
|
||||||
iconClassName: 'text-info-content',
|
iconClassName: 'text-info-content',
|
||||||
bgClassName: 'bg-info',
|
innerRingClassName: 'bg-info',
|
||||||
outerRingClassName: 'bg-info/20',
|
middleRingClassName: 'bg-info/12',
|
||||||
borderClassName: 'border-info',
|
outerRingClassName: 'border-info/12 bg-info/8',
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
icon: 'heroicons:check',
|
icon: 'heroicons:check',
|
||||||
iconClassName: 'text-white',
|
iconClassName: 'text-white',
|
||||||
bgClassName: 'bg-[#00D390]',
|
innerRingClassName: 'bg-success',
|
||||||
outerRingClassName: 'bg-[#00D3901F]',
|
middleRingClassName: 'bg-success/12',
|
||||||
borderClassName: 'border-[#CCF7EB]',
|
outerRingClassName: 'border-success/12 bg-success/8',
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
icon: 'solar:danger-triangle-linear',
|
icon: 'heroicons:exclamation-triangle',
|
||||||
iconClassName: 'text-error-content',
|
iconClassName: 'text-error-content',
|
||||||
bgClassName: 'bg-[#f03338]',
|
innerRingClassName: 'bg-error',
|
||||||
outerRingClassName: 'bg-[#f3cdcd]',
|
middleRingClassName: 'bg-error/12',
|
||||||
borderClassName: 'border-[#fff0ef]',
|
outerRingClassName: 'border-error/12 bg-error/8',
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const ConfirmationModalIcon = ({
|
const ConfirmationModalIcon = ({
|
||||||
type,
|
type,
|
||||||
size = 24,
|
size = 16,
|
||||||
}: {
|
}: {
|
||||||
type: 'info' | 'success' | 'error';
|
type: 'info' | 'success' | 'error';
|
||||||
size?: number;
|
size?: number;
|
||||||
@@ -65,28 +65,22 @@ const ConfirmationModalIcon = ({
|
|||||||
const config = iconConfig[type];
|
const config = iconConfig[type];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center justify-center p-2'>
|
<div
|
||||||
<div
|
className={cn('rounded-full border p-[5px]', config.outerRingClassName)}
|
||||||
className={cn(
|
>
|
||||||
'rounded-full border-4 p-1',
|
<div className={cn('rounded-full p-2', config.middleRingClassName)}>
|
||||||
config.outerRingClassName,
|
<div
|
||||||
config.borderClassName
|
className={cn(
|
||||||
)}
|
'rounded-full p-1 flex items-center justify-center',
|
||||||
>
|
config.innerRingClassName
|
||||||
<div className={cn('rounded-full p-1', config.outerRingClassName)}>
|
)}
|
||||||
<div
|
>
|
||||||
className={cn(
|
<Icon
|
||||||
'rounded-full p-3 flex items-center justify-center',
|
icon={config.icon}
|
||||||
config.bgClassName
|
width={size}
|
||||||
)}
|
height={size}
|
||||||
>
|
className={config.iconClassName}
|
||||||
<Icon
|
/>
|
||||||
icon={config.icon}
|
|
||||||
width={size}
|
|
||||||
height={size}
|
|
||||||
className={config.iconClassName}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,7 +97,7 @@ const ConfirmationModal = ({
|
|||||||
secondaryButton,
|
secondaryButton,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
iconSize = 32,
|
iconSize = 16,
|
||||||
iconPosition = 'center',
|
iconPosition = 'center',
|
||||||
}: ConfirmationModalProps) => {
|
}: ConfirmationModalProps) => {
|
||||||
const [isPrimaryButtonLoading, setIsPrimaryButtonLoading] = useState(false);
|
const [isPrimaryButtonLoading, setIsPrimaryButtonLoading] = useState(false);
|
||||||
@@ -123,7 +117,14 @@ const ConfirmationModal = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal ref={ref} closeOnBackdrop={closeOnBackdrop} className={className}>
|
<Modal
|
||||||
|
ref={ref}
|
||||||
|
closeOnBackdrop={closeOnBackdrop}
|
||||||
|
className={{
|
||||||
|
...className,
|
||||||
|
modalBox: cn('rounded-xl p-4', className?.modalBox),
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className='w-full flex flex-col gap-4'>
|
<div className='w-full flex flex-col gap-4'>
|
||||||
{iconPosition === 'center' ? (
|
{iconPosition === 'center' ? (
|
||||||
<>
|
<>
|
||||||
@@ -143,7 +144,7 @@ const ConfirmationModal = ({
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className={cn('flex flex-row items-center gap-4', {
|
className={cn('flex flex-row items-center gap-3', {
|
||||||
'flex-row': iconPosition === 'left',
|
'flex-row': iconPosition === 'left',
|
||||||
'flex-row-reverse': iconPosition === 'right',
|
'flex-row-reverse': iconPosition === 'right',
|
||||||
})}
|
})}
|
||||||
@@ -153,12 +154,12 @@ const ConfirmationModal = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col gap-1'>
|
||||||
<p className='font-medium'>
|
<p className='text-sm font-semibold'>
|
||||||
{text ?? 'Apakah anda yakin ingin melakukan hal ini?'}
|
{text ?? 'Apakah anda yakin ingin melakukan hal ini?'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{subtitleText && (
|
{subtitleText && (
|
||||||
<p className='text-sm text-gray-400'>{subtitleText}</p>
|
<p className='text-xs text-base-content/50'>{subtitleText}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,7 +167,7 @@ const ConfirmationModal = ({
|
|||||||
|
|
||||||
{children && <div className='w-full'>{children}</div>}
|
{children && <div className='w-full'>{children}</div>}
|
||||||
|
|
||||||
<div className='w-full flex flex-row gap-2'>
|
<div className='w-full grid grid-cols-2 gap-3'>
|
||||||
{secondaryButton && secondaryButton.text && (
|
{secondaryButton && secondaryButton.text && (
|
||||||
<Button
|
<Button
|
||||||
{...secondaryButton}
|
{...secondaryButton}
|
||||||
@@ -179,7 +180,10 @@ const ConfirmationModal = ({
|
|||||||
: isPrimaryButtonLoading
|
: isPrimaryButtonLoading
|
||||||
}
|
}
|
||||||
onClick={closeModalHandler}
|
onClick={closeModalHandler}
|
||||||
className='grow'
|
className={cn(
|
||||||
|
'p-2 rounded-xl text-sm',
|
||||||
|
secondaryButton?.className
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{secondaryButton?.text ?? 'Tidak'}
|
{secondaryButton?.text ?? 'Tidak'}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -200,7 +204,7 @@ const ConfirmationModal = ({
|
|||||||
? primaryButton?.isLoading
|
? primaryButton?.isLoading
|
||||||
: isPrimaryButtonLoading
|
: isPrimaryButtonLoading
|
||||||
}
|
}
|
||||||
className='grow'
|
className={cn('p-2 rounded-xl text-sm', primaryButton?.className)}
|
||||||
>
|
>
|
||||||
{primaryButton?.text ?? 'Ya'}
|
{primaryButton?.text ?? 'Ya'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
|
|||||||
className,
|
className,
|
||||||
rows = 3,
|
rows = 3,
|
||||||
placeholder = 'Catatan...',
|
placeholder = 'Catatan...',
|
||||||
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const randomId = useId();
|
const randomId = useId();
|
||||||
const [notes, setNotes] = useState('');
|
const [notes, setNotes] = useState('');
|
||||||
@@ -55,6 +56,7 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
|
|||||||
}}
|
}}
|
||||||
secondaryButton={secondaryButton}
|
secondaryButton={secondaryButton}
|
||||||
className={className}
|
className={className}
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
<TextArea
|
<TextArea
|
||||||
name={randomId}
|
name={randomId}
|
||||||
|
|||||||
@@ -405,7 +405,9 @@ const FinanceTable = () => {
|
|||||||
{
|
{
|
||||||
header: 'Bank',
|
header: 'Bank',
|
||||||
accessorFn: (finance: Finance) =>
|
accessorFn: (finance: Finance) =>
|
||||||
`${finance.bank?.alias} - ${finance.bank?.account_number} - ${finance.bank?.owner}`,
|
finance.bank
|
||||||
|
? `${finance.bank?.alias} - ${finance.bank?.account_number} - ${finance.bank?.owner}`
|
||||||
|
: '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Pengeluaran (Rp)',
|
header: 'Pengeluaran (Rp)',
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { RefObject } from 'react';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Modal from '@/components/Modal';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import DateInput from '@/components/input/DateInput';
|
||||||
|
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
||||||
|
|
||||||
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||||
|
import { ProjectFlockApi } from '@/services/api/production';
|
||||||
|
import { Flock } from '@/types/api/master-data/flock';
|
||||||
|
import { TransferToLayingFilter } from '@/types/api/production/transfer-to-laying';
|
||||||
|
|
||||||
|
interface TransferToLayingFilterModal {
|
||||||
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
|
onSubmit?: (values: TransferToLayingFilter) => void;
|
||||||
|
onReset?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TransferToLayingFilterModal = ({
|
||||||
|
ref,
|
||||||
|
onSubmit,
|
||||||
|
onReset,
|
||||||
|
}: TransferToLayingFilterModal) => {
|
||||||
|
const closeModalHandler = () => {
|
||||||
|
ref.current?.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Flock Source
|
||||||
|
const {
|
||||||
|
setInputValue: setFlockSourceInputValue,
|
||||||
|
options: flockSourceOptions,
|
||||||
|
isLoadingOptions: isLoadingFlockSourceOptions,
|
||||||
|
loadMore: loadMoreFlockSource,
|
||||||
|
} = useSelect<Flock>(ProjectFlockApi.basePath, 'id', 'flock_name', 'search', {
|
||||||
|
category: 'GROWING',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Flock Destination
|
||||||
|
const {
|
||||||
|
setInputValue: setFlockDestinationInputValue,
|
||||||
|
options: flockDestinationOptions,
|
||||||
|
isLoadingOptions: isLoadingFlockDestinationOptions,
|
||||||
|
loadMore: loadMoreFlockDestination,
|
||||||
|
} = useSelect<Flock>(ProjectFlockApi.basePath, 'id', 'flock_name', 'search', {
|
||||||
|
category: 'LAYING',
|
||||||
|
});
|
||||||
|
|
||||||
|
const formik = useFormik<{
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
flockSource: { value: number; label: string }[];
|
||||||
|
flockDestination: { value: number; label: string }[];
|
||||||
|
status: { value: number; label: string }[];
|
||||||
|
}>({
|
||||||
|
initialValues: {
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
flockSource: [],
|
||||||
|
flockDestination: [],
|
||||||
|
status: [],
|
||||||
|
},
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
const formattedValues = {
|
||||||
|
...values,
|
||||||
|
flockSource: values.flockSource.map((item) => item.value),
|
||||||
|
flockDestination: values.flockDestination.map((item) => item.value),
|
||||||
|
status: values.status.map((item) => item.value),
|
||||||
|
};
|
||||||
|
|
||||||
|
onSubmit?.(formattedValues);
|
||||||
|
closeModalHandler();
|
||||||
|
},
|
||||||
|
onReset: () => {
|
||||||
|
onReset?.();
|
||||||
|
closeModalHandler();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const flockSourceChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
formik.setFieldValue('flockSource', val as OptionType[]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const flockDestinationChangeHandler = (
|
||||||
|
val: OptionType | OptionType[] | null
|
||||||
|
) => {
|
||||||
|
formik.setFieldValue('flockDestination', val as OptionType[]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
formik.setFieldValue('status', val as OptionType[]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
ref={ref}
|
||||||
|
className={{
|
||||||
|
modalBox: 'p-0 rounded-xl',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
onReset={formik.handleReset}
|
||||||
|
className='w-full flex flex-col'
|
||||||
|
>
|
||||||
|
{/* Modal Header */}
|
||||||
|
<div className='p-4 flex items-center justify-between gap-2 border-b border-gray-300'>
|
||||||
|
<div className='flex items-center gap-2 text-primary'>
|
||||||
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||||
|
<h3 className='text-sm font-medium'>Filter Data</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={closeModalHandler}
|
||||||
|
className='p-0 text-base-content/50 hover:text-base-content'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Body */}
|
||||||
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<span className='py-2 text-xs font-semibold'>Tanggal</span>
|
||||||
|
<div className='flex flex-row items-center gap-1.5'>
|
||||||
|
<DateInput
|
||||||
|
name='startDate'
|
||||||
|
placeholder='Tanggal Awal'
|
||||||
|
value={formik.values.startDate}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
/>
|
||||||
|
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
||||||
|
<DateInput
|
||||||
|
name='endDate'
|
||||||
|
placeholder='Tanggal Akhir'
|
||||||
|
value={formik.values.endDate}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SelectInputCheckbox
|
||||||
|
label='Flock Asal'
|
||||||
|
placeholder='Flock Asal'
|
||||||
|
value={formik.values.flockSource}
|
||||||
|
onChange={flockSourceChangeHandler}
|
||||||
|
options={flockSourceOptions}
|
||||||
|
isLoading={isLoadingFlockSourceOptions}
|
||||||
|
onInputChange={setFlockSourceInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreFlockSource}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInputCheckbox
|
||||||
|
label='Flock Tujuan'
|
||||||
|
placeholder='Flock Tujuan'
|
||||||
|
value={formik.values.flockDestination}
|
||||||
|
onChange={flockDestinationChangeHandler}
|
||||||
|
options={flockDestinationOptions}
|
||||||
|
isLoading={isLoadingFlockDestinationOptions}
|
||||||
|
onInputChange={setFlockDestinationInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreFlockDestination}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInputCheckbox
|
||||||
|
label='Status'
|
||||||
|
placeholder='Pilih Status'
|
||||||
|
options={[
|
||||||
|
{ value: 'PENDING', label: 'Pengajuan' },
|
||||||
|
{ value: 'APPROVED', label: 'Disetujui' },
|
||||||
|
{ value: 'REJECTED', label: 'Ditolak' },
|
||||||
|
]}
|
||||||
|
value={formik.values.status}
|
||||||
|
onChange={statusChangeHandler}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Footer */}
|
||||||
|
<div className='p-4 flex justify-between gap-4 border-t border-gray-300 bg-gray-100'>
|
||||||
|
<Button
|
||||||
|
type='reset'
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
className='p-3 rounded-lg text-base-content/65'
|
||||||
|
>
|
||||||
|
Reset Filter
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
className='p-3 rounded-lg w-fit sm:w-full max-w-40 text-base-100 text-sm'
|
||||||
|
>
|
||||||
|
Apply Filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TransferToLayingFilterModal;
|
||||||
@@ -0,0 +1,975 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
FormEventHandler,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import useSWR, { useSWRConfig } from 'swr';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import DateInput from '@/components/input/DateInput';
|
||||||
|
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||||
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||||
|
import NumberInput from '@/components/input/NumberInput';
|
||||||
|
import TextArea from '@/components/input/TextArea';
|
||||||
|
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import { ProjectFlockApi } from '@/services/api/production';
|
||||||
|
import { getIn, useFormik } from 'formik';
|
||||||
|
import {
|
||||||
|
getFilledTransferToLayingFormInitialValues,
|
||||||
|
getTransferToLayingFormInitialValues,
|
||||||
|
TransferToLayingFormSchema,
|
||||||
|
TransferToLayingFormValues,
|
||||||
|
} from '@/components/pages/production/transfer-to-laying/form/TransferToLayingForm.schema';
|
||||||
|
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
|
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||||
|
import { cn, formatNumber } from '@/lib/helper';
|
||||||
|
import {
|
||||||
|
CreateTransferToLayingPayload,
|
||||||
|
UpdateTransferToLayingPayload,
|
||||||
|
} from '@/types/api/production/transfer-to-laying';
|
||||||
|
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||||
|
|
||||||
|
const TransferToLayingFormModal = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const modalAction = searchParams.get('action');
|
||||||
|
const transferToLayingId = searchParams.get('id');
|
||||||
|
|
||||||
|
const { mutate } = useSWRConfig();
|
||||||
|
|
||||||
|
const refreshTransferToLayings = () => {
|
||||||
|
mutate(
|
||||||
|
(key) =>
|
||||||
|
typeof key === 'string' && key.includes(TransferToLayingApi.basePath)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: transferToLaying, isLoading: isLoadingTransferToLaying } =
|
||||||
|
useSWR(transferToLayingId ? transferToLayingId : undefined, (id: number) =>
|
||||||
|
TransferToLayingApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step 1: General Information
|
||||||
|
* Step 2: Select source and destination kandang
|
||||||
|
* Step 3: Enter transfered quantity
|
||||||
|
* Step 4: Submit
|
||||||
|
*/
|
||||||
|
const [step, setStep] = useState(1);
|
||||||
|
|
||||||
|
const formModal = useModal();
|
||||||
|
|
||||||
|
const [formErrorMessage, setFormErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Flock Source
|
||||||
|
const {
|
||||||
|
setInputValue: setFlockSourceInputValue,
|
||||||
|
options: flockSourceOptions,
|
||||||
|
isLoadingOptions: isLoadingFlockSourceOptions,
|
||||||
|
loadMore: loadMoreFlockSource,
|
||||||
|
rawData: flockSourceRawData,
|
||||||
|
} = useSelect<ProjectFlock>(
|
||||||
|
ProjectFlockApi.basePath,
|
||||||
|
'id',
|
||||||
|
'flock_name',
|
||||||
|
'search',
|
||||||
|
{
|
||||||
|
category: 'GROWING',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Flock Destination
|
||||||
|
const {
|
||||||
|
setInputValue: setFlockDestinationInputValue,
|
||||||
|
options: flockDestinationOptions,
|
||||||
|
isLoadingOptions: isLoadingFlockDestinationOptions,
|
||||||
|
loadMore: loadMoreFlockDestination,
|
||||||
|
rawData: flockDestinationRawData,
|
||||||
|
} = useSelect<ProjectFlock>(
|
||||||
|
ProjectFlockApi.basePath,
|
||||||
|
'id',
|
||||||
|
'flock_name',
|
||||||
|
'search',
|
||||||
|
{
|
||||||
|
category: 'LAYING',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const closeModalHandler = (shouldPushToRoute: boolean = true) => {
|
||||||
|
if (shouldPushToRoute) {
|
||||||
|
router.push('/production/transfer-to-laying');
|
||||||
|
}
|
||||||
|
|
||||||
|
formik.resetForm();
|
||||||
|
setStep(1);
|
||||||
|
setFormErrorMessage('');
|
||||||
|
formModal.closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTransferToLayingHandler = useCallback(
|
||||||
|
async (payload: CreateTransferToLayingPayload) => {
|
||||||
|
const createTransferToLayingRes =
|
||||||
|
await TransferToLayingApi.create(payload);
|
||||||
|
|
||||||
|
if (isResponseError(createTransferToLayingRes)) {
|
||||||
|
setFormErrorMessage(createTransferToLayingRes.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshTransferToLayings();
|
||||||
|
toast.success(createTransferToLayingRes?.message as string);
|
||||||
|
router.push('/production/transfer-to-laying');
|
||||||
|
closeModalHandler(false);
|
||||||
|
},
|
||||||
|
[router]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateTransferToLayingHandler = useCallback(
|
||||||
|
async (
|
||||||
|
transferToLayingId: number,
|
||||||
|
payload: UpdateTransferToLayingPayload
|
||||||
|
) => {
|
||||||
|
const updateKandangRes = await TransferToLayingApi.update(
|
||||||
|
transferToLayingId,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
|
||||||
|
if (updateKandangRes?.status === 'error') {
|
||||||
|
setFormErrorMessage(updateKandangRes.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshTransferToLayings();
|
||||||
|
toast.success(updateKandangRes?.message as string);
|
||||||
|
router.push('/production/transfer-to-laying');
|
||||||
|
closeModalHandler(false);
|
||||||
|
},
|
||||||
|
[router]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [formikInitialValues, setFormikInitialValues] = useState(
|
||||||
|
getTransferToLayingFormInitialValues()
|
||||||
|
);
|
||||||
|
|
||||||
|
const formik = useFormik<TransferToLayingFormValues>({
|
||||||
|
initialValues: formikInitialValues,
|
||||||
|
validationSchema: TransferToLayingFormSchema,
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
const transferToLayingPayload: CreateTransferToLayingPayload = {
|
||||||
|
transfer_date: values.transfer_date as string,
|
||||||
|
source_project_flock_id: values.flockSource?.value as number,
|
||||||
|
target_project_flock_id: values.flockDestination?.value as number,
|
||||||
|
totalQuantity: values.totalQuantity as number,
|
||||||
|
|
||||||
|
source_kandangs: values.flockSourceKandangs?.map((kandang) => ({
|
||||||
|
project_flock_kandang_id: kandang.kandang.value,
|
||||||
|
quantity: parseFloat(kandang.quantity as string),
|
||||||
|
})) as CreateTransferToLayingPayload['source_kandangs'],
|
||||||
|
|
||||||
|
target_kandangs: values.flockDestinationKandangs?.map((kandang) => ({
|
||||||
|
project_flock_kandang_id: kandang.kandang.value,
|
||||||
|
quantity: parseFloat(kandang.quantity as string),
|
||||||
|
})) as CreateTransferToLayingPayload['target_kandangs'],
|
||||||
|
|
||||||
|
reason: values.reason as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (modalAction) {
|
||||||
|
case 'add':
|
||||||
|
await createTransferToLayingHandler(transferToLayingPayload);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'edit':
|
||||||
|
await updateTransferToLayingHandler(
|
||||||
|
Number(transferToLayingId),
|
||||||
|
transferToLayingPayload
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
||||||
|
|
||||||
|
const [selectedFlockSourceRawData, setSelectedFlockSourceRawData] = useState<
|
||||||
|
ProjectFlock | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
const selectedFlockDestinationRawData = isResponseSuccess(
|
||||||
|
flockDestinationRawData
|
||||||
|
)
|
||||||
|
? flockDestinationRawData.data.find(
|
||||||
|
(item) => item.id === formik.values.flockDestination?.value
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: flockSourceKandangsAvailability,
|
||||||
|
isLoading: isLoadingFlockSourceKandangsAvailability,
|
||||||
|
} = useSWR(
|
||||||
|
formik.values.flockSource
|
||||||
|
? [
|
||||||
|
'transfer-to-laying',
|
||||||
|
'available-qty',
|
||||||
|
String(formik.values.flockSource.value),
|
||||||
|
]
|
||||||
|
: undefined,
|
||||||
|
([, , id]: string[]) =>
|
||||||
|
TransferToLayingApi.getMappedFlockKandangsAvailability(Number(id))
|
||||||
|
);
|
||||||
|
|
||||||
|
const mappedFlockSourceKandangsAvailability: {
|
||||||
|
kandang_name: string;
|
||||||
|
available_qty: number;
|
||||||
|
project_flock_kandang_id: number;
|
||||||
|
}[] = useMemo(() => {
|
||||||
|
if (!flockSourceKandangsAvailability || !selectedFlockSourceRawData)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return selectedFlockSourceRawData
|
||||||
|
? selectedFlockSourceRawData.kandangs.map((kandang) => {
|
||||||
|
const availability =
|
||||||
|
flockSourceKandangsAvailability[kandang.project_flock_kandang_id]
|
||||||
|
.available_qty;
|
||||||
|
|
||||||
|
return {
|
||||||
|
kandang_name: kandang.name,
|
||||||
|
available_qty: availability,
|
||||||
|
project_flock_kandang_id: kandang.project_flock_kandang_id,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
}, [flockSourceKandangsAvailability, selectedFlockSourceRawData]);
|
||||||
|
|
||||||
|
const mappedFlockSourceKandangsAvailabilityInfo: {
|
||||||
|
available: number;
|
||||||
|
unavailable: number;
|
||||||
|
} = useMemo(() => {
|
||||||
|
if (!mappedFlockSourceKandangsAvailability)
|
||||||
|
return { available: 0, unavailable: 0 };
|
||||||
|
|
||||||
|
let countAvailable = 0;
|
||||||
|
let countUnavailable = 0;
|
||||||
|
|
||||||
|
mappedFlockSourceKandangsAvailability.forEach((item) => {
|
||||||
|
if (item.available_qty > 0) {
|
||||||
|
countAvailable += 1;
|
||||||
|
} else {
|
||||||
|
countUnavailable += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { available: countAvailable, unavailable: countUnavailable };
|
||||||
|
}, [mappedFlockSourceKandangsAvailability]);
|
||||||
|
|
||||||
|
const mappedFlockDestinationKandangsAvailabilityInfo: {
|
||||||
|
available: number;
|
||||||
|
unavailable: number;
|
||||||
|
} = useMemo(() => {
|
||||||
|
if (!selectedFlockDestinationRawData)
|
||||||
|
return { available: 0, unavailable: 0 };
|
||||||
|
|
||||||
|
let countAvailable = 0;
|
||||||
|
let countUnavailable = 0;
|
||||||
|
|
||||||
|
selectedFlockDestinationRawData?.kandangs.forEach((item) => {
|
||||||
|
// TODO: change this to real available quota later
|
||||||
|
if (item.capacity > 0) {
|
||||||
|
countAvailable += 1;
|
||||||
|
} else {
|
||||||
|
countUnavailable += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { available: countAvailable, unavailable: countUnavailable };
|
||||||
|
}, [selectedFlockDestinationRawData]);
|
||||||
|
|
||||||
|
const totalEnteredChickenForTransfer =
|
||||||
|
formik.values.flockSourceKandangs.reduce(
|
||||||
|
(acc, item) => acc + Number(item.quantity),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalTransferedChicken = formik.values.flockDestinationKandangs.reduce(
|
||||||
|
(acc, item) => acc + Number(item.quantity),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalAvailableChickenForTransfer =
|
||||||
|
totalEnteredChickenForTransfer - totalTransferedChicken;
|
||||||
|
|
||||||
|
const isNextButtonDisabled = useMemo(() => {
|
||||||
|
if (step === 1) {
|
||||||
|
return Boolean(
|
||||||
|
!formik.values.transfer_date ||
|
||||||
|
!formik.values.flockSource ||
|
||||||
|
!formik.values.flockDestination
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step === 2) {
|
||||||
|
return Boolean(
|
||||||
|
!formik.values.flockSourceKandangs.length ||
|
||||||
|
!formik.values.flockDestinationKandangs.length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, [step, formik.values]);
|
||||||
|
|
||||||
|
const nextButtonHandler = () => {
|
||||||
|
setStep(step + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteEnteredKandangHandler = () => {
|
||||||
|
formik.setFieldValue('flockSourceKandangs', []);
|
||||||
|
formik.setFieldValue('flockDestinationKandangs', []);
|
||||||
|
formik.setFieldValue('totalQuantity', '');
|
||||||
|
formik.setFieldValue('maxTotalQuantity', '');
|
||||||
|
formik.setFieldValue('reason', '');
|
||||||
|
formik.setFieldTouched('reason', false);
|
||||||
|
|
||||||
|
setStep(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const flockSourceChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
formik.setFieldValue('flockSource', val);
|
||||||
|
formik.setFieldValue('flockSourceKandangs', []);
|
||||||
|
};
|
||||||
|
|
||||||
|
const flockDestinationChangeHandler = (
|
||||||
|
val: OptionType | OptionType[] | null
|
||||||
|
) => {
|
||||||
|
formik.setFieldValue('flockDestination', val);
|
||||||
|
formik.setFieldValue('flockDestinationKandangs', []);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (modalAction === 'add' || modalAction === 'edit') {
|
||||||
|
formModal.openModal();
|
||||||
|
}
|
||||||
|
}, [modalAction]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getFilledInitialValues = async () => {
|
||||||
|
if (transferToLayingId && isResponseSuccess(transferToLaying)) {
|
||||||
|
const filledInitialValues =
|
||||||
|
await getFilledTransferToLayingFormInitialValues(
|
||||||
|
transferToLaying.data
|
||||||
|
);
|
||||||
|
|
||||||
|
formik.setValues(filledInitialValues);
|
||||||
|
setStep(3);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFlockSourceData = async () => {
|
||||||
|
if (transferToLayingId && isResponseSuccess(transferToLaying)) {
|
||||||
|
const singleFlockSourceRawData = await ProjectFlockApi.getSingle(
|
||||||
|
transferToLaying.data.from_project_flock.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseSuccess(singleFlockSourceRawData)) {
|
||||||
|
setSelectedFlockSourceRawData(singleFlockSourceRawData.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getFlockSourceData();
|
||||||
|
getFilledInitialValues();
|
||||||
|
}, [transferToLayingId, transferToLaying]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isResponseSuccess(flockSourceRawData)) {
|
||||||
|
const selectedFlockSourceRawData = flockSourceRawData.data.find(
|
||||||
|
(item) => item.id === formik.values.flockSource?.value
|
||||||
|
);
|
||||||
|
|
||||||
|
setSelectedFlockSourceRawData(selectedFlockSourceRawData);
|
||||||
|
}
|
||||||
|
}, [flockSourceRawData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
formik.setFieldValue('totalQuantity', totalTransferedChicken);
|
||||||
|
formik.setFieldValue('maxTotalQuantity', totalTransferedChicken);
|
||||||
|
}, [totalTransferedChicken]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
ref={formModal.ref}
|
||||||
|
position='end'
|
||||||
|
className={{
|
||||||
|
modalBox: 'w-full sm:w-fit p-3 rounded-xl bg-transparent shadow-none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
onSubmit={handleFormSubmit}
|
||||||
|
className='w-full min-h-full flex flex-col sm:flex-row items-stretch bg-base-100 rounded-xl overflow-y-auto'
|
||||||
|
>
|
||||||
|
{/* 1st Section */}
|
||||||
|
<div className='w-full sm:w-[446px]'>
|
||||||
|
<div className='w-full p-4 flex flex-row items-stretch gap-3 border-b border-base-content/10'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={() => closeModalHandler()}
|
||||||
|
className='p-0 text-black hover:text-base-content'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className='w-px border-none bg-base-content/10' />
|
||||||
|
|
||||||
|
<h4 className='text-sm font-medium text-base-content/50'>
|
||||||
|
Add Transfer to Laying
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='w-full p-4 flex flex-col'>
|
||||||
|
<h4 className='text-base font-medium text-base-content/50 font-roboto'>
|
||||||
|
Informasi Umum
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<DateInput
|
||||||
|
name='transfer_date'
|
||||||
|
label='Tanggal'
|
||||||
|
placeholder='Tanggal'
|
||||||
|
value={formik.values.transfer_date ?? ''}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={
|
||||||
|
formik.touched.transfer_date &&
|
||||||
|
Boolean(formik.errors.transfer_date)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.transfer_date}
|
||||||
|
disabled={step > 2}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInputRadio
|
||||||
|
label='Flock Asal'
|
||||||
|
placeholder='Pilih Flock Asal'
|
||||||
|
value={formik.values.flockSource}
|
||||||
|
onChange={flockSourceChangeHandler}
|
||||||
|
options={flockSourceOptions}
|
||||||
|
isLoading={isLoadingFlockSourceOptions}
|
||||||
|
onInputChange={setFlockSourceInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreFlockSource}
|
||||||
|
isDisabled={step > 2}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInputRadio
|
||||||
|
label='Flock Tujuan'
|
||||||
|
placeholder='Pilih Flock Tujuan'
|
||||||
|
value={formik.values.flockDestination}
|
||||||
|
onChange={flockDestinationChangeHandler}
|
||||||
|
options={flockDestinationOptions}
|
||||||
|
isLoading={isLoadingFlockDestinationOptions}
|
||||||
|
onInputChange={setFlockDestinationInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreFlockDestination}
|
||||||
|
isDisabled={step > 2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{step >= 2 && (
|
||||||
|
<>
|
||||||
|
<div className='w-full p-4 flex flex-col gap-3 border-y border-base-content/10'>
|
||||||
|
<h4 className='text-base font-medium text-base-content/50 font-roboto'>
|
||||||
|
Pilih Kandang Asal
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div className='w-fit flex flex-row items-stretch gap-3'>
|
||||||
|
<StatusBadge
|
||||||
|
color='info'
|
||||||
|
text={`Tersedia (${mappedFlockSourceKandangsAvailabilityInfo.available})`}
|
||||||
|
className={{ badge: 'text-nowrap' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='w-px border-none bg-base-content/10' />
|
||||||
|
|
||||||
|
<StatusBadge
|
||||||
|
color='neutral'
|
||||||
|
text={`Tidak Tersedia (${mappedFlockSourceKandangsAvailabilityInfo.unavailable})`}
|
||||||
|
className={{ badge: 'text-nowrap' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='w-full rounded-xl border border-base-content/10'>
|
||||||
|
{mappedFlockSourceKandangsAvailability.map(
|
||||||
|
(item, itemIdx) => {
|
||||||
|
const isAvailable = item.available_qty > 0;
|
||||||
|
const isChecked =
|
||||||
|
formik.values.flockSourceKandangs.some(
|
||||||
|
(k) =>
|
||||||
|
k.kandang.value === item.project_flock_kandang_id
|
||||||
|
);
|
||||||
|
|
||||||
|
const flockSourceKandangCheckboxChangeHandler: FormEventHandler<
|
||||||
|
HTMLInputElement
|
||||||
|
> = (e) => {
|
||||||
|
const checked = (e.target as HTMLInputElement)
|
||||||
|
.checked;
|
||||||
|
if (checked) {
|
||||||
|
formik.setFieldValue('flockSourceKandangs', [
|
||||||
|
...formik.values.flockSourceKandangs,
|
||||||
|
{
|
||||||
|
kandang: {
|
||||||
|
value: item.project_flock_kandang_id,
|
||||||
|
label: item.kandang_name,
|
||||||
|
},
|
||||||
|
quantity: '',
|
||||||
|
maxQuantity: item.available_qty,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
formik.setFieldValue(
|
||||||
|
'flockSourceKandangs',
|
||||||
|
formik.values.flockSourceKandangs.filter(
|
||||||
|
(k) =>
|
||||||
|
k.kandang.value !==
|
||||||
|
item.project_flock_kandang_id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={itemIdx}
|
||||||
|
className='w-full p-3 flex flex-row items-center justify-between'
|
||||||
|
>
|
||||||
|
<div className='flex flex-row items-center gap-3'>
|
||||||
|
<CheckboxInput
|
||||||
|
name={`flockSourceKandang.${itemIdx}.value`}
|
||||||
|
value={item.project_flock_kandang_id}
|
||||||
|
checked={isChecked}
|
||||||
|
onChange={
|
||||||
|
flockSourceKandangCheckboxChangeHandler
|
||||||
|
}
|
||||||
|
size='md'
|
||||||
|
disabled={!isAvailable}
|
||||||
|
classNames={{
|
||||||
|
checkbox: cn({
|
||||||
|
'bg-base-200 border border-base-content/10 opacity-100':
|
||||||
|
!isAvailable,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label
|
||||||
|
htmlFor={`flockSourceKandang.${itemIdx}.value`}
|
||||||
|
className={cn('text-sm text-base-content/50', {
|
||||||
|
'cursor-pointer': isAvailable,
|
||||||
|
'cursor-not-allowed': !isAvailable,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{item.kandang_name}{' '}
|
||||||
|
<span className='text-base-content/20'>{`(Max: ${item.available_qty})`}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StatusBadge
|
||||||
|
color={isAvailable ? 'info' : 'neutral'}
|
||||||
|
text={isAvailable ? 'Tersedia' : 'Tidak Tersedia'}
|
||||||
|
className={{ badge: 'w-fit' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='w-full p-4 flex flex-col gap-3'>
|
||||||
|
<div className='flex flex-row items-center justify-between'>
|
||||||
|
<h4 className='text-base font-medium text-base-content/50 font-roboto'>
|
||||||
|
Pilih Kandang Tujuan
|
||||||
|
</h4>
|
||||||
|
{formik.touched.flockDestinationKandangs &&
|
||||||
|
formik.errors.flockDestinationKandangs &&
|
||||||
|
typeof formik.errors.flockDestinationKandangs ===
|
||||||
|
'string' && (
|
||||||
|
<span className='text-xs text-error'>
|
||||||
|
{formik.errors.flockDestinationKandangs}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='w-fit flex flex-row items-stretch gap-3'>
|
||||||
|
<StatusBadge
|
||||||
|
color='info'
|
||||||
|
text={`Tersedia (${mappedFlockDestinationKandangsAvailabilityInfo.available})`}
|
||||||
|
className={{ badge: 'text-nowrap' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='w-px border-none bg-base-content/10' />
|
||||||
|
|
||||||
|
<StatusBadge
|
||||||
|
color='neutral'
|
||||||
|
text={`Tidak Tersedia (${mappedFlockDestinationKandangsAvailabilityInfo.unavailable})`}
|
||||||
|
className={{ badge: 'text-nowrap' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='w-full rounded-xl border border-base-content/10'>
|
||||||
|
{selectedFlockDestinationRawData?.kandangs.map(
|
||||||
|
(item, itemIdx) => {
|
||||||
|
// TODO: change this to real available quota later
|
||||||
|
const isAvailable = item.capacity > 0;
|
||||||
|
const isChecked =
|
||||||
|
formik.values.flockDestinationKandangs.some(
|
||||||
|
(k) =>
|
||||||
|
k.kandang.value === item.project_flock_kandang_id
|
||||||
|
);
|
||||||
|
|
||||||
|
const flockDestinationKandangCheckboxChangeHandler: FormEventHandler<
|
||||||
|
HTMLInputElement
|
||||||
|
> = (e) => {
|
||||||
|
const checked = (e.target as HTMLInputElement)
|
||||||
|
.checked;
|
||||||
|
if (checked) {
|
||||||
|
formik.setFieldValue('flockDestinationKandangs', [
|
||||||
|
...formik.values.flockDestinationKandangs,
|
||||||
|
{
|
||||||
|
kandang: {
|
||||||
|
value: item.project_flock_kandang_id,
|
||||||
|
label: item.name,
|
||||||
|
},
|
||||||
|
quantity: '',
|
||||||
|
// TODO: change this to real available quota later
|
||||||
|
maxQuantity: item.capacity,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
formik.setFieldValue(
|
||||||
|
'flockDestinationKandangs',
|
||||||
|
formik.values.flockDestinationKandangs.filter(
|
||||||
|
(k) =>
|
||||||
|
k.kandang.value !==
|
||||||
|
item.project_flock_kandang_id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={itemIdx}
|
||||||
|
className='w-full p-3 flex flex-row items-center justify-between'
|
||||||
|
>
|
||||||
|
<div className='flex flex-row items-center gap-3'>
|
||||||
|
<CheckboxInput
|
||||||
|
name={`flockDestinationKandang.${itemIdx}.value`}
|
||||||
|
value={item.project_flock_kandang_id}
|
||||||
|
checked={isChecked}
|
||||||
|
onChange={
|
||||||
|
flockDestinationKandangCheckboxChangeHandler
|
||||||
|
}
|
||||||
|
size='md'
|
||||||
|
disabled={!isAvailable}
|
||||||
|
classNames={{
|
||||||
|
checkbox: cn({
|
||||||
|
'bg-base-200 border border-base-content/10 opacity-100':
|
||||||
|
!isAvailable,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label
|
||||||
|
htmlFor={`flockDestinationKandang.${itemIdx}.value`}
|
||||||
|
className={cn('text-sm text-base-content/50', {
|
||||||
|
'cursor-pointer': isAvailable,
|
||||||
|
'cursor-not-allowed': !isAvailable,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{item.name}{' '}
|
||||||
|
{/* TODO: change this to real available quota later */}
|
||||||
|
<span className='text-base-content/20'>{`(Max: ${item.capacity})`}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StatusBadge
|
||||||
|
color={isAvailable ? 'info' : 'neutral'}
|
||||||
|
text={isAvailable ? 'Tersedia' : 'Tidak Tersedia'}
|
||||||
|
className={{ badge: 'w-fit' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='w-full p-4 border-t border-base-content/10'>
|
||||||
|
{step < 3 && (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
onClick={nextButtonHandler}
|
||||||
|
disabled={isNextButtonDisabled}
|
||||||
|
className='w-full p-3 rounded-lg text-sm text-base-100'
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 2nd Section */}
|
||||||
|
{step === 3 && (
|
||||||
|
<div className='w-full sm:w-[446px] border-l border-base-content/10'>
|
||||||
|
<div className='w-full p-4 flex flex-row items-center justify-between gap-3 border-b border-base-content/10'>
|
||||||
|
<h4 className='text-sm font-medium text-base-content/50'>
|
||||||
|
Tambah Kuantitas
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={deleteEnteredKandangHandler}
|
||||||
|
className='p-0 text-error'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:trash' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='w-full p-4 flex flex-col'>
|
||||||
|
<h4 className='text-base font-medium text-base-content/50 font-roboto'>
|
||||||
|
Informasi Kandang
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{/* Source Kandang */}
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<span className='w-full py-2 text-xs font-semibold'>
|
||||||
|
Kandang Asal
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{formik.values.flockSourceKandangs.length === 0 && (
|
||||||
|
<span className='text-sm text-base-content/50 italic'>
|
||||||
|
Belum ada kandang asal yang dipilih
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{formik.values.flockSourceKandangs.length > 0 && (
|
||||||
|
<div className='flex flex-col gap-3'>
|
||||||
|
{formik.values.flockSourceKandangs.map((item, index) => {
|
||||||
|
const isInvalid =
|
||||||
|
item.quantity === ''
|
||||||
|
? false
|
||||||
|
: Boolean(
|
||||||
|
getIn(
|
||||||
|
formik.errors,
|
||||||
|
`flockSourceKandangs[${index}].quantity`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const errorMessage = getIn(
|
||||||
|
formik.errors,
|
||||||
|
`flockSourceKandangs[${index}].quantity`
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NumberInput
|
||||||
|
key={`flockSourceKandangs-${item.kandang.value}-${index}`}
|
||||||
|
name={`flockSourceKandangs.${index}.quantity`}
|
||||||
|
placeholder='Masukkan Kuantitas'
|
||||||
|
value={item.quantity}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
isError={isInvalid}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
inputPrefix={
|
||||||
|
<div className='w-full h-full py-1 flex flex-row items-stretch justify-between gap-5'>
|
||||||
|
<span
|
||||||
|
title={item.kandang.label}
|
||||||
|
className='text-sm text-base-content self-center text-nowrap truncate'
|
||||||
|
>
|
||||||
|
{item.kandang.label}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className='w-px bg-base-content/10' />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
className={{
|
||||||
|
inputPrefix:
|
||||||
|
'py-0 px-0 pl-3 text-base-content/50 bg-transparent border-r-0',
|
||||||
|
inputPrefixSuffixWrapper: 'grid grid-cols-2',
|
||||||
|
inputWrapper: 'border-l-0 pl-5',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Destination Kandang */}
|
||||||
|
<div className='mt-3 flex flex-col'>
|
||||||
|
<span className='w-fit py-2 text-xs font-semibold flex flex-row items-center gap-3'>
|
||||||
|
<span className='text-nowrap'>Kandang Tujuan</span>
|
||||||
|
|
||||||
|
<div className='w-px h-5 bg-base-content/10' />
|
||||||
|
|
||||||
|
<StatusBadge
|
||||||
|
color={
|
||||||
|
totalAvailableChickenForTransfer < 0
|
||||||
|
? 'error'
|
||||||
|
: 'neutral'
|
||||||
|
}
|
||||||
|
text={`Sisa transfer: ${formatNumber(
|
||||||
|
totalAvailableChickenForTransfer
|
||||||
|
)} ekor`}
|
||||||
|
className={{
|
||||||
|
badge: 'text-nowrap',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{formik.values.flockDestinationKandangs.length === 0 && (
|
||||||
|
<span className='text-sm text-base-content/50 italic'>
|
||||||
|
Belum ada kandang tujuan yang dipilih
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{formik.values.flockDestinationKandangs.length > 0 && (
|
||||||
|
<div className='flex flex-col gap-3'>
|
||||||
|
{formik.values.flockDestinationKandangs.map(
|
||||||
|
(item, index) => {
|
||||||
|
const isInvalid =
|
||||||
|
item.quantity === ''
|
||||||
|
? false
|
||||||
|
: Boolean(
|
||||||
|
getIn(
|
||||||
|
formik.errors,
|
||||||
|
`flockDestinationKandangs[${index}].quantity`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const errorMessage = getIn(
|
||||||
|
formik.errors,
|
||||||
|
`flockDestinationKandangs[${index}].quantity`
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NumberInput
|
||||||
|
key={`flockDestinationKandangs-${item.kandang.value}-${index}`}
|
||||||
|
name={`flockDestinationKandangs.${index}.quantity`}
|
||||||
|
placeholder='Masukkan Kuantitas'
|
||||||
|
value={item.quantity}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
isError={isInvalid}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
inputPrefix={
|
||||||
|
<div className='w-full h-full py-1 flex flex-row items-stretch justify-between gap-5'>
|
||||||
|
<span
|
||||||
|
title={item.kandang.label}
|
||||||
|
className='text-sm text-base-content self-center text-nowrap truncate'
|
||||||
|
>
|
||||||
|
{item.kandang.label}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className='w-px bg-base-content/10' />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
className={{
|
||||||
|
inputPrefix:
|
||||||
|
'py-0 px-0 pl-3 text-base-content/50 bg-transparent border-r-0',
|
||||||
|
inputPrefixSuffixWrapper: 'grid grid-cols-2',
|
||||||
|
inputWrapper: 'border-l-0 pl-5',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='w-full p-4 flex flex-col border-y border-base-content/10'>
|
||||||
|
<h4 className='text-base font-medium text-base-content/50 font-roboto'>
|
||||||
|
Informasi Umum
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<NumberInput
|
||||||
|
name='totalQuantity'
|
||||||
|
label='Jumlah Transfer'
|
||||||
|
placeholder='Total Kuantitas Transfer'
|
||||||
|
value={formik.values.totalQuantity}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={totalAvailableChickenForTransfer < 0}
|
||||||
|
errorMessage={
|
||||||
|
totalAvailableChickenForTransfer < 0
|
||||||
|
? 'Jumlah transfer melebihi ketersediaan'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
name='reason'
|
||||||
|
label='Catatan'
|
||||||
|
placeholder='Alasan Transfer'
|
||||||
|
rows={4}
|
||||||
|
value={formik.values.reason}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={
|
||||||
|
Boolean(formik.touched.reason) &&
|
||||||
|
Boolean(formik.errors.reason)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.reason}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='w-full p-4 self-end flex flex-col gap-3'>
|
||||||
|
{formErrorMessage && (
|
||||||
|
<div role='alert' className='alert alert-error w-full'>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:error-outline'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
<span>{formErrorMessage}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
disabled={
|
||||||
|
formik.isSubmitting || totalAvailableChickenForTransfer < 0
|
||||||
|
}
|
||||||
|
isLoading={formik.isSubmitting}
|
||||||
|
className='w-full p-3 rounded-lg text-sm text-base-100'
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TransferToLayingFormModal;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import {
|
import {
|
||||||
CellContext,
|
CellContext,
|
||||||
@@ -12,30 +12,28 @@ import toast from 'react-hot-toast';
|
|||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import SelectInput, {
|
|
||||||
OptionType,
|
|
||||||
useSelect,
|
|
||||||
} from '@/components/input/SelectInput';
|
|
||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
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 PopoverButton from '@/components/popover/PopoverButton';
|
||||||
|
import Badge from '@/components/Badge';
|
||||||
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
|
import Dropdown from '@/components/Dropdown';
|
||||||
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
|
import TransferToLayingFilterModal from '@/components/pages/production/transfer-to-laying/TransferToLayingFilterModal';
|
||||||
|
|
||||||
import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
|
import {
|
||||||
|
TransferToLaying,
|
||||||
|
TransferToLayingFilter,
|
||||||
|
} 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 { isResponseError, 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 { Flock } from '@/types/api/master-data/flock';
|
|
||||||
import { ProjectFlockApi } from '@/services/api/production';
|
|
||||||
import Badge from '@/components/Badge';
|
|
||||||
import { Color } from '@/types/theme';
|
import { Color } from '@/types/theme';
|
||||||
import PopoverContent from '@/components/popover/PopoverContent';
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
props,
|
props,
|
||||||
@@ -56,8 +54,8 @@ const RowOptionsMenu = ({
|
|||||||
|
|
||||||
const showDeleteButton = showEditButton;
|
const showDeleteButton = showEditButton;
|
||||||
|
|
||||||
const showApproveButton = showEditButton;
|
// const showApproveButton = showEditButton;
|
||||||
const showRejectButton = showEditButton;
|
// const showRejectButton = showEditButton;
|
||||||
|
|
||||||
const popoverId = `transferToLaying#${props.row.original.id}`;
|
const popoverId = `transferToLaying#${props.row.original.id}`;
|
||||||
const popoverAnchorName = `--anchor-transferToLaying#${props.row.original.id}`;
|
const popoverAnchorName = `--anchor-transferToLaying#${props.row.original.id}`;
|
||||||
@@ -78,7 +76,7 @@ const RowOptionsMenu = ({
|
|||||||
id={popoverId}
|
id={popoverId}
|
||||||
anchorName={popoverAnchorName}
|
anchorName={popoverAnchorName}
|
||||||
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
|
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
|
||||||
className='rounded-xl border border-base-content/5 shadow-sm'
|
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
|
||||||
>
|
>
|
||||||
<div className='flex flex-col bg-base-100 rounded-xl'>
|
<div className='flex flex-col bg-base-100 rounded-xl'>
|
||||||
<RequirePermission permissions='lti.production.transfer_to_laying.detail'>
|
<RequirePermission permissions='lti.production.transfer_to_laying.detail'>
|
||||||
@@ -96,7 +94,7 @@ const RowOptionsMenu = ({
|
|||||||
{showEditButton && (
|
{showEditButton && (
|
||||||
<RequirePermission permissions='lti.production.transfer_to_laying.update'>
|
<RequirePermission permissions='lti.production.transfer_to_laying.update'>
|
||||||
<Button
|
<Button
|
||||||
href={`/production/transfer-to-laying/detail/edit/?transferToLayingId=${props.row.original.id}`}
|
href={`/production/transfer-to-laying/?action=edit&id=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
color='none'
|
color='none'
|
||||||
className='p-3 justify-start text-sm font-semibold w-full'
|
className='p-3 justify-start text-sm font-semibold w-full'
|
||||||
@@ -107,34 +105,6 @@ const RowOptionsMenu = ({
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showApproveButton && (
|
|
||||||
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
|
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
color='success'
|
|
||||||
onClick={approveClickHandler}
|
|
||||||
className='p-3 justify-start text-sm font-semibold w-full'
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:check' width={20} height={20} />
|
|
||||||
Approve
|
|
||||||
</Button>
|
|
||||||
</RequirePermission>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showRejectButton && (
|
|
||||||
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
|
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
color='error'
|
|
||||||
onClick={rejectClickHandler}
|
|
||||||
className='p-3 justify-start text-sm font-semibold w-full'
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
|
||||||
Reject
|
|
||||||
</Button>
|
|
||||||
</RequirePermission>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showDeleteButton && (
|
{showDeleteButton && (
|
||||||
<RequirePermission permissions='lti.production.transfer_to_laying.delete'>
|
<RequirePermission permissions='lti.production.transfer_to_laying.delete'>
|
||||||
<hr className='mx-3 border-base-content/10 h-px' />
|
<hr className='mx-3 border-base-content/10 h-px' />
|
||||||
@@ -165,18 +135,22 @@ const TransferToLayingsTable = () => {
|
|||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
transferDate: '',
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
flockSource: '',
|
flockSource: '',
|
||||||
flockDestination: '',
|
flockDestination: '',
|
||||||
|
status: '',
|
||||||
filter_by: '',
|
filter_by: '',
|
||||||
sort_by: '',
|
sort_by: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
transferDate: 'transfer_date',
|
startDate: 'start_date',
|
||||||
|
endDate: 'end_date',
|
||||||
flockSource: 'flock_source',
|
flockSource: 'flock_source',
|
||||||
flockDestination: 'flock_destination',
|
flockDestination: 'flock_destination',
|
||||||
|
status: 'status',
|
||||||
filter_by: 'filter_by',
|
filter_by: 'filter_by',
|
||||||
sort_by: 'sort_by',
|
sort_by: 'sort_by',
|
||||||
},
|
},
|
||||||
@@ -191,34 +165,36 @@ const TransferToLayingsTable = () => {
|
|||||||
TransferToLayingApi.getAllFetcher
|
TransferToLayingApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const filterCount = useMemo(() => {
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
if (tableFilterState.startDate && tableFilterState.endDate) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tableFilterState.flockSource.length > 0) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tableFilterState.flockDestination.length > 0) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tableFilterState.status.length > 0) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}, [tableFilterState]);
|
||||||
|
|
||||||
|
const isFilterActive = filterCount > 0;
|
||||||
|
|
||||||
// Modal hooks
|
// Modal hooks
|
||||||
|
const filterModal = useModal();
|
||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
const approveModal = useModal();
|
const approveModal = useModal();
|
||||||
const rejectModal = useModal();
|
const rejectModal = useModal();
|
||||||
|
|
||||||
// Flocks data
|
|
||||||
const {
|
|
||||||
setInputValue: setFlockSourceInputValue,
|
|
||||||
options: flockSourceOptions,
|
|
||||||
isLoadingOptions: isLoadingFlockSourceOptions,
|
|
||||||
loadMore: loadMoreFlockSource,
|
|
||||||
hasMore: hasMoreFlockSource,
|
|
||||||
} = useSelect<Flock>(ProjectFlockApi.basePath, 'id', 'flock_name');
|
|
||||||
|
|
||||||
const {
|
|
||||||
setInputValue: setFlockDestinationInputValue,
|
|
||||||
options: flockDestinationOptions,
|
|
||||||
isLoadingOptions: isLoadingFlockDestinationOptions,
|
|
||||||
loadMore: loadMoreFlockDestination,
|
|
||||||
hasMore: hasMoreFlockDestination,
|
|
||||||
} = useSelect<Flock>(ProjectFlockApi.basePath, 'id', 'flock_name');
|
|
||||||
|
|
||||||
// Flocks value
|
|
||||||
const [selectedFlockSource, setSelectedFlockSource] =
|
|
||||||
useState<OptionType | null>(null);
|
|
||||||
const [selectedFlockDestination, setSelectedFlockDestination] =
|
|
||||||
useState<OptionType | null>(null);
|
|
||||||
|
|
||||||
const [selectedTransferToLaying, setSelectedTransferToLaying] = useState<
|
const [selectedTransferToLaying, setSelectedTransferToLaying] = useState<
|
||||||
TransferToLaying | undefined
|
TransferToLaying | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
@@ -315,18 +291,7 @@ const TransferToLayingsTable = () => {
|
|||||||
latestApprovalStepName = 'Ditolak';
|
latestApprovalStepName = 'Ditolak';
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <StatusBadge color={badgeColor} text={latestApprovalStepName} />;
|
||||||
<Badge
|
|
||||||
variant='soft'
|
|
||||||
className={{
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -337,7 +302,7 @@ const TransferToLayingsTable = () => {
|
|||||||
const currentRowRelativeIndex =
|
const currentRowRelativeIndex =
|
||||||
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
||||||
|
|
||||||
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 3;
|
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
||||||
|
|
||||||
const approveClickHandler = () => {
|
const approveClickHandler = () => {
|
||||||
setSelectedTransferToLaying(props.row.original);
|
setSelectedTransferToLaying(props.row.original);
|
||||||
@@ -472,38 +437,25 @@ const TransferToLayingsTable = () => {
|
|||||||
setIsRejectLoading(false);
|
setIsRejectLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const filterSubmitHandler = (values: TransferToLayingFilter) => {
|
||||||
updateFilter('search', e.target.value);
|
updateFilter('startDate', values.startDate);
|
||||||
|
updateFilter('endDate', values.endDate);
|
||||||
|
updateFilter('flockSource', values.flockSource.join(','));
|
||||||
|
updateFilter('flockDestination', values.flockDestination.join(','));
|
||||||
|
updateFilter('status', values.status.join(','));
|
||||||
};
|
};
|
||||||
|
|
||||||
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const filterResetHandler = () => {
|
||||||
const newVal = val as OptionType;
|
updateFilter('startDate', '');
|
||||||
|
updateFilter('endDate', '');
|
||||||
setPageSize(newVal.value as number);
|
updateFilter('flockSource', '');
|
||||||
|
updateFilter('flockDestination', '');
|
||||||
|
updateFilter('status', '');
|
||||||
};
|
};
|
||||||
|
|
||||||
const transferDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (
|
// TODO: add export to excel functionality
|
||||||
e
|
const exportToExcelHandler = () => {
|
||||||
) => {
|
toast.error('Not implemented yet');
|
||||||
updateFilter('transferDate', e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const flockSourceChangeHandler = (val: OptionType | OptionType[] | null) => {
|
|
||||||
setSelectedFlockSource(val as OptionType);
|
|
||||||
updateFilter(
|
|
||||||
'flockSource',
|
|
||||||
val ? ((val as OptionType).value as string) : ''
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const flockDestinationChangeHandler = (
|
|
||||||
val: OptionType | OptionType[] | null
|
|
||||||
) => {
|
|
||||||
setSelectedFlockDestination(val as OptionType);
|
|
||||||
updateFilter(
|
|
||||||
'flockDestination',
|
|
||||||
val ? ((val as OptionType).value as string) : ''
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -518,111 +470,137 @@ const TransferToLayingsTable = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='w-full p-0'>
|
<div className='@container w-full'>
|
||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
|
||||||
<div className='w-full flex flex-col xl:flex-row justify-between items-end xl:items-center gap-2'>
|
<div className='w-fit flex flex-row gap-3 flex-wrap'>
|
||||||
<div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'>
|
<RequirePermission permissions='lti.production.transfer_to_laying.create'>
|
||||||
<RequirePermission permissions='lti.production.transfer_to_laying.create'>
|
<Button
|
||||||
<Button
|
href={{
|
||||||
href='/production/transfer-to-laying/add'
|
pathname: '/production/transfer-to-laying',
|
||||||
variant='outline'
|
query: {
|
||||||
color='primary'
|
action: 'add',
|
||||||
className='w-full sm:w-fit'
|
},
|
||||||
>
|
}}
|
||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
color='primary'
|
||||||
Tambah
|
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
||||||
</Button>
|
>
|
||||||
</RequirePermission>
|
<Icon icon='heroicons:plus' width={20} height={20} />
|
||||||
|
Add Transfer to Laying
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
{selectedRowIds.length > 0 && (
|
{selectedRowIds.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
|
<hr className='w-px h-full border-none bg-base-content/10 hidden @sm:block' />
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
color='success'
|
|
||||||
onClick={bulkApproveClickHandler}
|
|
||||||
disabled={selectedRowIds.length === 0}
|
|
||||||
className='w-full sm:w-fit'
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon='material-symbols:check'
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
/>
|
|
||||||
Approve
|
|
||||||
</Button>
|
|
||||||
</RequirePermission>
|
|
||||||
|
|
||||||
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
|
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='error'
|
color='none'
|
||||||
onClick={bulkRejectClickHandler}
|
onClick={bulkRejectClickHandler}
|
||||||
disabled={selectedRowIds.length === 0}
|
disabled={selectedRowIds.length === 0}
|
||||||
className='w-full sm:w-fit'
|
className='px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon='material-symbols:close'
|
icon='heroicons:x-mark'
|
||||||
width={24}
|
width={20}
|
||||||
height={24}
|
height={20}
|
||||||
/>
|
className='text-error'
|
||||||
Reject
|
/>
|
||||||
</Button>
|
Reject
|
||||||
</RequirePermission>
|
</Button>
|
||||||
</>
|
</RequirePermission>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DebouncedTextInput
|
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
|
||||||
name='search'
|
<Button
|
||||||
placeholder='Cari TransferToLaying'
|
variant='outline'
|
||||||
value={tableFilterState.search}
|
color='none'
|
||||||
onChange={searchChangeHandler}
|
onClick={bulkApproveClickHandler}
|
||||||
className={{
|
disabled={selectedRowIds.length === 0}
|
||||||
wrapper: 'sm:max-w-3xs',
|
className='px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||||
}}
|
>
|
||||||
/>
|
<Icon
|
||||||
|
icon='heroicons:check'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className='text-success'
|
||||||
|
/>
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='grid grid-cols-12 justify-end gap-4'>
|
<div className='flex flex-row justify-center items-center gap-3'>
|
||||||
<DateInput
|
<Button
|
||||||
name='transfer_date'
|
variant='outline'
|
||||||
label='Tanggal Transfer'
|
color='none'
|
||||||
placeholder='Tanggal Transfer'
|
onClick={filterModal.openModal}
|
||||||
value={tableFilterState.transferDate}
|
className={cn(
|
||||||
onChange={transferDateChangeHandler}
|
'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all',
|
||||||
className={{
|
{
|
||||||
wrapper: 'col-span-12 sm:col-span-3',
|
'border-primary-gradient text-primary': isFilterActive,
|
||||||
}}
|
}
|
||||||
/>
|
)}
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||||
|
Filter
|
||||||
|
{isFilterActive && (
|
||||||
|
<Badge
|
||||||
|
className={{
|
||||||
|
badge:
|
||||||
|
'p-1.5 bg-[#FF3535] text-xs text-base-100 border border-base-300 rounded-lg',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{filterCount}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
<SelectInput
|
<Dropdown
|
||||||
label='Flock Asal'
|
align='end'
|
||||||
options={flockSourceOptions}
|
direction='bottom'
|
||||||
isLoading={isLoadingFlockSourceOptions}
|
|
||||||
value={selectedFlockSource}
|
|
||||||
onChange={flockSourceChangeHandler}
|
|
||||||
onInputChange={setFlockSourceInputValue}
|
|
||||||
onMenuScrollToBottom={loadMoreFlockSource}
|
|
||||||
isClearable
|
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'col-span-12 sm:col-span-3',
|
content:
|
||||||
|
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
|
||||||
}}
|
}}
|
||||||
/>
|
trigger={
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='none'
|
||||||
|
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||||
|
>
|
||||||
|
<div className='flex flex-row items-center gap-1.5'>
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:cloud-arrow-down'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
|
||||||
<SelectInput
|
<span>Export</span>
|
||||||
label='Flock Tujuan'
|
|
||||||
options={flockDestinationOptions}
|
<div className='w-px self-stretch bg-base-content/10' />
|
||||||
isLoading={isLoadingFlockDestinationOptions}
|
|
||||||
value={selectedFlockDestination}
|
<Icon
|
||||||
onChange={flockDestinationChangeHandler}
|
icon='heroicons:chevron-down'
|
||||||
onInputChange={setFlockDestinationInputValue}
|
width={14}
|
||||||
onMenuScrollToBottom={loadMoreFlockDestination}
|
height={14}
|
||||||
isClearable
|
/>
|
||||||
className={{
|
</div>
|
||||||
wrapper: 'col-span-12 sm:col-span-3',
|
</Button>
|
||||||
}}
|
}
|
||||||
/>
|
>
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={exportToExcelHandler}
|
||||||
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
|
Export to Excel
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -652,7 +630,7 @@ const TransferToLayingsTable = () => {
|
|||||||
enableRowSelection={tableEnableRowSelectionHandler}
|
enableRowSelection={tableEnableRowSelectionHandler}
|
||||||
withCheckbox
|
withCheckbox
|
||||||
className={{
|
className={{
|
||||||
containerClassName: cn({
|
containerClassName: cn('p-3', {
|
||||||
'w-full mb-20':
|
'w-full mb-20':
|
||||||
isResponseSuccess(transferToLayings) &&
|
isResponseSuccess(transferToLayings) &&
|
||||||
transferToLayings?.data?.length === 0,
|
transferToLayings?.data?.length === 0,
|
||||||
@@ -662,15 +640,23 @@ const TransferToLayingsTable = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<TransferToLayingFilterModal
|
||||||
|
ref={filterModal.ref}
|
||||||
|
onSubmit={filterSubmitHandler}
|
||||||
|
onReset={filterResetHandler}
|
||||||
|
/>
|
||||||
|
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
ref={deleteModal.ref}
|
ref={deleteModal.ref}
|
||||||
|
iconPosition='left'
|
||||||
type='error'
|
type='error'
|
||||||
text={`Apakah anda yakin ingin menghapus data transfer ke laying ini?`}
|
text='Delete This Data?'
|
||||||
|
subtitleText='Are you sure you want to delete this data? '
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Tidak',
|
text: 'Cancel',
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya',
|
text: 'Delete',
|
||||||
color: 'error',
|
color: 'error',
|
||||||
isLoading: isDeleteLoading,
|
isLoading: isDeleteLoading,
|
||||||
onClick: confirmationModalDeleteClickHandler,
|
onClick: confirmationModalDeleteClickHandler,
|
||||||
@@ -680,12 +666,14 @@ const TransferToLayingsTable = () => {
|
|||||||
<ConfirmationModalWithNotes
|
<ConfirmationModalWithNotes
|
||||||
ref={approveModal.ref}
|
ref={approveModal.ref}
|
||||||
type='success'
|
type='success'
|
||||||
text={`Apakah anda yakin ingin approve data transfer ke laying ini (${selectedRowIds.length} data)?`}
|
iconPosition='left'
|
||||||
|
text='Approve This Submission?'
|
||||||
|
subtitleText='Are you sure you want to approve this submission?'
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Tidak',
|
text: 'Cancel',
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya',
|
text: 'Approve',
|
||||||
color: 'success',
|
color: 'success',
|
||||||
isLoading: isApproveLoading,
|
isLoading: isApproveLoading,
|
||||||
onClick: confirmationModalApproveClickHandler,
|
onClick: confirmationModalApproveClickHandler,
|
||||||
@@ -695,12 +683,14 @@ const TransferToLayingsTable = () => {
|
|||||||
<ConfirmationModalWithNotes
|
<ConfirmationModalWithNotes
|
||||||
ref={rejectModal.ref}
|
ref={rejectModal.ref}
|
||||||
type='error'
|
type='error'
|
||||||
text={`Apakah anda yakin ingin reject data transfer ke laying ini (${selectedRowIds.length} data)?`}
|
iconPosition='left'
|
||||||
|
text='Reject This Submission?'
|
||||||
|
subtitleText='Are you sure you want to reject this submission?'
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Tidak',
|
text: 'Cancel',
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya',
|
text: 'Reject',
|
||||||
color: 'error',
|
color: 'error',
|
||||||
isLoading: isRejectLoading,
|
isLoading: isRejectLoading,
|
||||||
onClick: confirmationModalRejectClickHandler,
|
onClick: confirmationModalRejectClickHandler,
|
||||||
|
|||||||
+42
-27
@@ -1,7 +1,10 @@
|
|||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
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 { formatDate } from '@/lib/helper';
|
import { formatDate, formatNumber } from '@/lib/helper';
|
||||||
|
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||||
|
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
type TransferToLayingFormSchemaType = {
|
type TransferToLayingFormSchemaType = {
|
||||||
transfer_date?: string;
|
transfer_date?: string;
|
||||||
@@ -14,7 +17,7 @@ type TransferToLayingFormSchemaType = {
|
|||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
totalQuantity?: number;
|
totalQuantity?: number | string;
|
||||||
maxTotalQuantity?: number; // original cap (hidden), helper
|
maxTotalQuantity?: number; // original cap (hidden), helper
|
||||||
|
|
||||||
flockSourceKandangs: {
|
flockSourceKandangs: {
|
||||||
@@ -53,15 +56,15 @@ export const TransferToLayingFormSchema: Yup.ObjectSchema<TransferToLayingFormSc
|
|||||||
}).required('Flock tujuan wajib diisi!'),
|
}).required('Flock tujuan wajib diisi!'),
|
||||||
|
|
||||||
totalQuantity: Yup.number()
|
totalQuantity: Yup.number()
|
||||||
.min(1, 'Jumlah transfer minimal 1')
|
.min(0, 'Jumlah transfer minimal 0')
|
||||||
.max(
|
.max(
|
||||||
Yup.ref('maxTotalQuantity'),
|
Yup.ref('maxTotalQuantity'),
|
||||||
({ max }) => `Kuantitas maksimal ${max}!`
|
({ max }) => `Kuantitas maksimal ${formatNumber(max)}!`
|
||||||
)
|
)
|
||||||
.required('Jumlah transfer wajib diisi!'),
|
.required('Jumlah transfer wajib diisi!'),
|
||||||
|
|
||||||
maxTotalQuantity: Yup.number()
|
maxTotalQuantity: Yup.number()
|
||||||
.min(1, 'Jumlah transfer minimal 1')
|
.min(0, 'Jumlah transfer minimal 0')
|
||||||
.required('Jumlah transfer wajib diisi!'),
|
.required('Jumlah transfer wajib diisi!'),
|
||||||
|
|
||||||
flockSourceKandangs: Yup.array()
|
flockSourceKandangs: Yup.array()
|
||||||
@@ -76,7 +79,7 @@ export const TransferToLayingFormSchema: Yup.ObjectSchema<TransferToLayingFormSc
|
|||||||
.min(0, 'Kuantitas minimal 0!')
|
.min(0, 'Kuantitas minimal 0!')
|
||||||
.max(
|
.max(
|
||||||
Yup.ref('maxQuantity'),
|
Yup.ref('maxQuantity'),
|
||||||
({ max }) => `Kuantitas maksimal ${max}!`
|
({ max }) => `Kuantitas maksimal ${formatNumber(max)}!`
|
||||||
)
|
)
|
||||||
.required('Kuantitas wajib diisi!'),
|
.required('Kuantitas wajib diisi!'),
|
||||||
|
|
||||||
@@ -98,7 +101,7 @@ export const TransferToLayingFormSchema: Yup.ObjectSchema<TransferToLayingFormSc
|
|||||||
.min(0, 'Kuantitas minimal 0!')
|
.min(0, 'Kuantitas minimal 0!')
|
||||||
.max(
|
.max(
|
||||||
Yup.ref('maxQuantity'),
|
Yup.ref('maxQuantity'),
|
||||||
({ max }) => `Kuantitas maksimal ${max}!`
|
({ max }) => `Kuantitas maksimal ${formatNumber(max)}!`
|
||||||
)
|
)
|
||||||
.required('Kuantitas wajib diisi!'),
|
.required('Kuantitas wajib diisi!'),
|
||||||
|
|
||||||
@@ -137,12 +140,12 @@ export const getTransferToLayingFormInitialValues = (
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
totalQuantity:
|
totalQuantity:
|
||||||
initialValues?.usage_qty ?? initialValues?.pending_usage_qty ?? undefined,
|
initialValues?.usage_qty ?? initialValues?.pending_usage_qty ?? '',
|
||||||
|
|
||||||
flockSourceKandangs: initialValues?.sources
|
flockSourceKandangs: initialValues?.sources
|
||||||
? initialValues.sources.map((sourceKandang) => ({
|
? initialValues.sources.map((sourceKandang) => ({
|
||||||
kandang: {
|
kandang: {
|
||||||
value: sourceKandang.source_project_flock_kandang.kandang.id,
|
value: sourceKandang.source_project_flock_kandang.id,
|
||||||
label: sourceKandang.source_project_flock_kandang.kandang.name,
|
label: sourceKandang.source_project_flock_kandang.kandang.name,
|
||||||
},
|
},
|
||||||
quantity: sourceKandang.qty,
|
quantity: sourceKandang.qty,
|
||||||
@@ -152,7 +155,7 @@ export const getTransferToLayingFormInitialValues = (
|
|||||||
flockDestinationKandangs: initialValues?.targets
|
flockDestinationKandangs: initialValues?.targets
|
||||||
? initialValues.targets.map((targetKandang) => ({
|
? initialValues.targets.map((targetKandang) => ({
|
||||||
kandang: {
|
kandang: {
|
||||||
value: targetKandang.target_project_flock_kandang.kandang.id,
|
value: targetKandang.target_project_flock_kandang.id,
|
||||||
label: targetKandang.target_project_flock_kandang.kandang.name,
|
label: targetKandang.target_project_flock_kandang.kandang.name,
|
||||||
},
|
},
|
||||||
quantity: targetKandang.qty,
|
quantity: targetKandang.qty,
|
||||||
@@ -174,7 +177,7 @@ export const getFilledTransferToLayingFormInitialValues = async (
|
|||||||
const formattedFlockSourceKandangs = initialValues?.sources
|
const formattedFlockSourceKandangs = initialValues?.sources
|
||||||
? initialValues.sources.map((sourceKandang) => ({
|
? initialValues.sources.map((sourceKandang) => ({
|
||||||
kandang: {
|
kandang: {
|
||||||
value: sourceKandang.source_project_flock_kandang.kandang.id,
|
value: sourceKandang.source_project_flock_kandang.id,
|
||||||
label: sourceKandang.source_project_flock_kandang.kandang.name,
|
label: sourceKandang.source_project_flock_kandang.kandang.name,
|
||||||
},
|
},
|
||||||
quantity: sourceKandang.qty,
|
quantity: sourceKandang.qty,
|
||||||
@@ -189,9 +192,35 @@ export const getFilledTransferToLayingFormInitialValues = async (
|
|||||||
|
|
||||||
let maxTotalQuantity = 0;
|
let maxTotalQuantity = 0;
|
||||||
formattedFlockSourceKandangs.forEach((item) => {
|
formattedFlockSourceKandangs.forEach((item) => {
|
||||||
maxTotalQuantity += item.maxQuantity;
|
maxTotalQuantity += item.quantity;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const flockDestination = await ProjectFlockApi.getSingle(
|
||||||
|
initialValues?.to_project_flock.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
const formattedFlockDestinationKandangs = initialValues?.targets
|
||||||
|
? initialValues.targets.map((targetKandang) => {
|
||||||
|
const kandang = isResponseSuccess(flockDestination)
|
||||||
|
? flockDestination?.data?.kandangs.find(
|
||||||
|
(kandang) =>
|
||||||
|
String(kandang.project_flock_kandang_id) ===
|
||||||
|
String(targetKandang.target_project_flock_kandang.id)
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
kandang: {
|
||||||
|
value: targetKandang.target_project_flock_kandang.id,
|
||||||
|
label: targetKandang.target_project_flock_kandang.kandang.name,
|
||||||
|
},
|
||||||
|
quantity: targetKandang.qty,
|
||||||
|
|
||||||
|
maxQuantity: kandang?.capacity ?? 0,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
transfer_date: initialValues?.transfer_date
|
transfer_date: initialValues?.transfer_date
|
||||||
? formatDate(initialValues.transfer_date, 'YYYY-MM-DD')
|
? formatDate(initialValues.transfer_date, 'YYYY-MM-DD')
|
||||||
@@ -214,21 +243,7 @@ export const getFilledTransferToLayingFormInitialValues = async (
|
|||||||
|
|
||||||
flockSourceKandangs: formattedFlockSourceKandangs,
|
flockSourceKandangs: formattedFlockSourceKandangs,
|
||||||
|
|
||||||
flockDestinationKandangs: initialValues?.targets
|
flockDestinationKandangs: formattedFlockDestinationKandangs,
|
||||||
? initialValues.targets.map((targetKandang) => ({
|
|
||||||
kandang: {
|
|
||||||
value: targetKandang.target_project_flock_kandang.kandang.id,
|
|
||||||
label: targetKandang.target_project_flock_kandang.kandang.name,
|
|
||||||
},
|
|
||||||
quantity: targetKandang.qty,
|
|
||||||
|
|
||||||
// maxQuantity:
|
|
||||||
// targetKandang.target_project_flock_kandang.kandang.capacity,
|
|
||||||
|
|
||||||
// TODO: integrate this to real API kandang capacity
|
|
||||||
maxQuantity: Infinity,
|
|
||||||
}))
|
|
||||||
: [],
|
|
||||||
|
|
||||||
reason: initialValues?.notes ?? undefined,
|
reason: initialValues?.notes ?? undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import moment from 'moment';
|
|||||||
import 'moment/locale/id';
|
import 'moment/locale/id';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
import clsx, { ClassValue } from 'clsx';
|
import clsx, { ClassValue } from 'clsx';
|
||||||
|
import { SidebarMenuItem } from '@/components/molecules/SidebarMenu';
|
||||||
|
|
||||||
// set locale globally
|
// set locale globally
|
||||||
moment.locale('id');
|
moment.locale('id');
|
||||||
@@ -147,3 +148,33 @@ export const isPathActive = (pathname: string, link?: string) => {
|
|||||||
|
|
||||||
return pathname.startsWith(link) && isActiveLinkValid;
|
return pathname.startsWith(link) && isActiveLinkValid;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function findMenuPath(
|
||||||
|
menus: readonly SidebarMenuItem[],
|
||||||
|
pathname: string,
|
||||||
|
parents: SidebarMenuItem[] = []
|
||||||
|
): SidebarMenuItem[] | null {
|
||||||
|
for (const menu of menus) {
|
||||||
|
const currentPath = [...parents, menu];
|
||||||
|
|
||||||
|
// Exact match
|
||||||
|
if (menu.link === pathname) {
|
||||||
|
return currentPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefix match (useful for pages like /add, /edit, etc.)
|
||||||
|
if (pathname.startsWith(menu.link + '/')) {
|
||||||
|
if (!menu.submenu) {
|
||||||
|
return currentPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search children
|
||||||
|
if (menu.submenu) {
|
||||||
|
const found = findMenuPath(menu.submenu, pathname, currentPath);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
@layer utilities {
|
||||||
|
.border-primary-gradient {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 12px;
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
180deg,
|
||||||
|
var(--color-primary) -250%,
|
||||||
|
var(--color-base-100) 100%
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
var(--color-primary),
|
||||||
|
color-mix(in srgb, var(--color-primary) 40%, transparent)
|
||||||
|
);
|
||||||
|
background-clip: padding-box, border-box;
|
||||||
|
background-origin: padding-box, border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -89,3 +89,11 @@ export type CreateTransferToLayingPayload = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateTransferToLayingPayload = CreateTransferToLayingPayload;
|
export type UpdateTransferToLayingPayload = CreateTransferToLayingPayload;
|
||||||
|
|
||||||
|
export type TransferToLayingFilter = {
|
||||||
|
flockSource: number[];
|
||||||
|
flockDestination: number[];
|
||||||
|
status: number[];
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user