diff --git a/src/components/Card.tsx b/src/components/Card.tsx index e04fa4c7..ce7c1c57 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -22,6 +22,7 @@ export interface CardProps onCollapsedChange?: (collapsed: boolean) => void; className?: { wrapper?: string; + wrapperContent?: string; image?: string; body?: string; title?: string; @@ -122,6 +123,10 @@ const Card = ({ return cn(baseClasses, 'p-6', className?.body); }; + const getCollapsibleClasses = () => { + return cn('', className?.collapsible); + }; + const getTitleClasses = () => { const sizeClasses = { sm: 'text-lg', @@ -144,6 +149,10 @@ const Card = ({ return cn('border-t border-base-300 mt-4 pt-4', className?.footer); }; + const getWrapperContentClasses = () => { + return cn('space-y-4', className?.wrapperContent); + }; + const renderCardContent = () => { const hasContent = children || actions || footer; @@ -177,7 +186,7 @@ const Card = ({ ); const cardContent = ( -
+
{children} {actions &&
{actions}
} {footer &&
{footer}
} @@ -208,6 +217,7 @@ const Card = ({ titleClassName='w-full cursor-pointer' contentClassName='p-0' fullWidth={true} + className={getCollapsibleClasses()} > {cardContent} diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 4998ca66..ce17f6b8 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -12,6 +12,7 @@ import PopoverContent from '@/components/popover/PopoverContent'; import { useAuth } from '@/services/hooks/useAuth'; import { AuthApi } from '@/services/api/auth'; import { isResponseError } from '@/lib/api-helper'; +import { useUiStore } from '@/stores/ui/ui.store'; interface NavbarProps { toggleSidebar?: () => void; @@ -21,6 +22,7 @@ const Navbar = ({ toggleSidebar }: NavbarProps) => { const { setUser } = useAuth(); const router = useRouter(); const pathname = usePathname(); + const navbarActions = useUiStore((state) => state.navbarActions); const logoutClickHandler = async () => { const logoutRes = await AuthApi.logout(); @@ -53,7 +55,9 @@ const Navbar = ({ toggleSidebar }: NavbarProps) => {
-
+
+ {/* Page-specific actions */} + {navbarActions &&
{navbarActions}
} { @@ -79,7 +80,9 @@ export interface TableProps { getSubRows?: (originalRow: TData, index: number) => TData[] | undefined; } -const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; +const DUMMY_SKELETON_DATA = Array.from({ length: 10 }, (_, index) => ({ + id: index, +})); const emptyContentDefaultValue = (
@@ -414,7 +417,14 @@ const Table = ({ cell.getContext() )} - {isLoading &&
} + {isLoading && ( +
+ )} ))} diff --git a/src/components/Tabs.tsx b/src/components/Tabs.tsx index 8f685452..8a06f9ed 100644 --- a/src/components/Tabs.tsx +++ b/src/components/Tabs.tsx @@ -25,8 +25,10 @@ export interface TabsProps wrapper?: string; tab?: string; content?: string; + tabHeaderWrapper?: string; }; onTabChange?: (tabId: string) => void; + sideContent?: ReactNode; } const Tabs = ({ @@ -38,6 +40,7 @@ const Tabs = ({ activeTabId: controlledActiveId, className, onTabChange, + sideContent, ...props }: TabsProps) => { // State internal hanya dipakai kalau `activeTabId` (controlled) tidak diset @@ -59,6 +62,7 @@ const Tabs = ({ wrapper: wrapperClassName, tab: tabClassName, content: contentClassName, + tabHeaderWrapper: tabHeaderWrapperClassName, } = typeof className === 'object' ? className : { wrapper: className, tab: undefined }; @@ -102,6 +106,10 @@ const Tabs = ({ tabClassName ); + const getSideContentClasses = () => { + return cn('flex flex-row', tabHeaderWrapperClassName); + }; + const activeContent = tabs.find((tab) => tab.id === activeTabId)?.content; return ( @@ -112,18 +120,21 @@ const Tabs = ({ typeof className === 'string' ? className : containerClassName )} > -
- {tabs.map(({ id, label, disabled }) => ( - - ))} +
+
+ {tabs.map(({ id, label, disabled }) => ( + + ))} +
+ {sideContent && sideContent}
{activeContent && ( diff --git a/src/components/helper/ButtonFilter.tsx b/src/components/helper/ButtonFilter.tsx index 90343fed..cff1d167 100644 --- a/src/components/helper/ButtonFilter.tsx +++ b/src/components/helper/ButtonFilter.tsx @@ -9,15 +9,21 @@ export type ButtonFilterProps = ButtonProps & { onClick: () => void; }; +// 'bg-gradient-to-t from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200 + const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => { return (
); }; diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index a4c9f5e0..535bcce5 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -72,8 +72,10 @@ const RequireAuth = ({ children }: RequireAuthProps) => { await AuthApi.refresh(); }; - refreshUserSession(); - }, []); + if (user) { + refreshUserSession(); + } + }, [user]); if ( (isLoadingUserResponse && !userResponse && !userErrorResponse) || diff --git a/src/components/helper/StatusBadge.tsx b/src/components/helper/StatusBadge.tsx index c4f99593..f9725fff 100644 --- a/src/components/helper/StatusBadge.tsx +++ b/src/components/helper/StatusBadge.tsx @@ -27,6 +27,7 @@ const StatusBadge = ({ 'bg-success/30': color === 'success', 'bg-error/20': color === 'error', 'bg-primary/20': color === 'info', + 'bg-[#FF9A20]/12': color === 'warning', }, className?.badge ), @@ -43,6 +44,7 @@ const StatusBadge = ({ 'text-[#008000]': color === 'success', 'text-error': color === 'error', 'text-primary': color === 'info', + 'text-[#FF9A20]': color === 'warning', })} > diff --git a/src/components/helper/drawer/DrawerHeader.tsx b/src/components/helper/drawer/DrawerHeader.tsx index f9d70a04..8bb635ae 100644 --- a/src/components/helper/drawer/DrawerHeader.tsx +++ b/src/components/helper/drawer/DrawerHeader.tsx @@ -58,6 +58,7 @@ const DrawerHeader = ({ if (leftIconOnClick) { return ( {isRange && ( - )} diff --git a/src/components/input/FileInput.tsx b/src/components/input/FileInput.tsx index 5f3a8610..f285f687 100644 --- a/src/components/input/FileInput.tsx +++ b/src/components/input/FileInput.tsx @@ -41,7 +41,7 @@ const FileInput = ({ return (
@@ -49,7 +49,7 @@ const FileInput = ({
); }; diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx index 419ed314..a79054dd 100644 --- a/src/components/input/SelectInput.tsx +++ b/src/components/input/SelectInput.tsx @@ -54,6 +54,9 @@ interface SelectInputBaseProps { wrapper?: string; label?: string; select?: string; + inputPrefix?: string; + inputSuffix?: string; + inputPrefixSuffixWrapper?: string; }; isError?: boolean; errorMessage?: string; @@ -62,6 +65,8 @@ interface SelectInputBaseProps { delay?: number; onInputChange?: (search: string) => void; startAdornment?: ReactNode; + inputPrefix?: ReactNode; + inputSuffix?: ReactNode; menuPortalTarget?: HTMLElement | null; closeMenuOnSelect?: boolean; hideSelectedOptions?: boolean; @@ -84,7 +89,7 @@ const CustomControl = < >( props: ControlProps ) => { - const { children } = props; + const { children, innerProps } = props; const customProps = props.selectProps as unknown as { shouldShowAdornment?: boolean; @@ -96,7 +101,7 @@ const CustomControl = < return ( -
+
{shouldShowAdornment && startAdornment} {children}
@@ -153,6 +158,8 @@ const SelectInput = (props: SelectInputProps) => { createables = false, onInputChange, startAdornment, + inputPrefix, + inputSuffix, menuPortalTarget, closeMenuOnSelect, hideSelectedOptions, @@ -227,111 +234,264 @@ const SelectInput = (props: SelectInputProps) => { )} - > - instanceId='select' - value={value ?? (isMulti ? [] : null)} - onChange={onChange ? handleChange : undefined} - options={options} - menuIsOpen={openMenu} - inputValue={internalInputValue} - onInputChange={internalInputChangeHandler} - onMenuClose={() => setInternalInputValue('')} - isMulti={isMulti} - isDisabled={isDisabled || readOnly} - isLoading={isLoading} - isClearable={isClearable} - isRtl={isRtl} - isSearchable={isSearchable} - placeholder={placeholder} - closeMenuOnSelect={closeMenuOnSelect} - hideSelectedOptions={hideSelectedOptions} - className={cn('w-full', className?.select)} - classNames={{ - ...(!startAdornment && { + {inputPrefix || inputSuffix ? ( +
+ {inputPrefix && ( +
+ {inputPrefix} +
+ )} + + > + instanceId='select' + value={value ?? (isMulti ? [] : null)} + onChange={onChange ? handleChange : undefined} + options={options} + menuIsOpen={openMenu} + inputValue={internalInputValue} + onInputChange={internalInputChangeHandler} + onMenuClose={() => setInternalInputValue('')} + isMulti={isMulti} + isDisabled={isDisabled || readOnly} + isLoading={isLoading} + isClearable={isClearable} + isRtl={isRtl} + isSearchable={isSearchable} + placeholder={placeholder} + closeMenuOnSelect={closeMenuOnSelect} + hideSelectedOptions={hideSelectedOptions} + className={cn('w-full flex-1', className?.select)} + classNames={{ + control: ({ isFocused, isDisabled }) => + cn('w-full border bg-white transition-shadow', 'rounded-lg!', { + 'cursor-pointer!': !readOnly && !isDisabled, + 'border-red-500! ring-2 ring-red-200': isError, + 'border-indigo-500 ring-2 ring-indigo-200': + isFocused && !startAdornment, + 'border-base-content/10!': !isError && !isFocused, + 'bg-gray-100 text-gray-400 cursor-not-allowed': + isDisabled && !readOnly, + 'bg-transparent! cursor-not-allowed!': readOnly, + 'rounded-l-none!': inputPrefix && !startAdornment, + 'rounded-r-none!': inputSuffix && !startAdornment, + }), + valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'), + placeholder: () => + cn({ + 'text-gray-400 text-sm leading-tight': !isError, + 'text-red-300!': isError, + }), + singleValue: () => + cn({ + 'm-0! text-gray-900 text-sm leading-tight': !isError, + 'text-error!': isError, + 'text-gray-900!': readOnly, + }), + input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'), + indicatorsContainer: () => + cn('flex items-center gap-1 pr-3 py-2'), + dropdownIndicator: ({ isFocused }) => + cn('p-0! rounded hover:bg-gray-100', { + 'text-gray-900': isFocused, + 'text-gray-500': !isFocused, + 'text-error!': isError, + }), + clearIndicator: () => cn('p-0! rounded hover:bg-gray-100'), + menu: () => + cn( + 'border border-base-content/5 rounded-xl! bg-base-100 shadow-lg! my-1.5!' + ), + menuList: () => cn('p-0! max-h-60 overflow-auto'), + option: ({ isFocused, isSelected }) => + cn('px-3 py-2 rounded-md cursor-pointer!', { + 'bg-indigo-600 text-white': isFocused, + 'bg-blue-500!': isSelected, + 'text-gray-700': !isFocused && !isSelected, + }), + multiValue: ({ getValue, index }) => { + const selectedValues = getValue() as T[]; + return cn( + 'bg-base-200! rounded-lg! py-[3px] px-2.5 m-0! flex items-center gap-1! w-fit gap-2!', + selectedValues[index]?.className + ); + }, + multiValueRemove: () => cn('p-0! w-3 h-3'), + multiValueLabel: ({ getValue, index }) => { + const selectedValues = getValue() as T[]; + return cn( + 'p-0! text-base-content! text-xs!', + selectedValues[index]?.labelClassName + ); + }, + }} + components={{ + ...components, + ...(optionComponent ? { Option: optionComponent } : {}), + MenuList: CustomMenuList, + }} + {...(startAdornment && { + shouldShowAdornment, + startAdornment, + })} + menuPortalTarget={ + typeof document !== 'undefined' + ? (menuPortalTarget ?? document.body) + : undefined + } + styles={{ + menuPortal: (base) => ({ ...base, zIndex: 9999 }), + multiValue(base) { + return { + ...base, + borderRadius: '8px', + }; + }, + }} + onMenuScrollToBottom={onMenuScrollToBottom} + /> + + {inputSuffix && ( +
+ {inputSuffix} +
+ )} +
+ ) : ( + > + instanceId='select' + value={value ?? (isMulti ? [] : null)} + onChange={onChange ? handleChange : undefined} + options={options} + menuIsOpen={openMenu} + inputValue={internalInputValue} + onInputChange={internalInputChangeHandler} + onMenuClose={() => setInternalInputValue('')} + isMulti={isMulti} + isDisabled={isDisabled || readOnly} + isLoading={isLoading} + isClearable={isClearable} + isRtl={isRtl} + isSearchable={isSearchable} + placeholder={placeholder} + closeMenuOnSelect={closeMenuOnSelect} + hideSelectedOptions={hideSelectedOptions} + className={cn('w-full', className?.select)} + classNames={{ control: ({ isFocused, isDisabled }) => - cn('w-full rounded-lg! border bg-white transition-shadow', { - 'cursor-pointer!': !readOnly && !isDisabled, - 'border-red-500! ring-2 ring-red-200': isError, - 'border-indigo-500 ring-2 ring-indigo-200': isFocused, - 'border-base-content/10!': !isError && !isFocused, - 'bg-gray-100 text-gray-400 cursor-not-allowed': - isDisabled && !readOnly, - 'bg-transparent! cursor-not-allowed!': readOnly, - }), + cn( + 'w-full border bg-white transition-shadow', + // Gunakan rounded-lg untuk semua kasus + 'rounded-lg!', + { + 'cursor-pointer!': !readOnly && !isDisabled, + 'border-red-500! ring-2 ring-red-200': isError, + 'border-indigo-500 ring-2 ring-indigo-200': + isFocused && !startAdornment, + 'border-base-content/10!': !isError && !isFocused, + 'bg-gray-100 text-gray-400 cursor-not-allowed': + isDisabled && !readOnly, + 'bg-transparent! cursor-not-allowed!': readOnly, + } + ), valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'), - }), - placeholder: () => - cn({ - 'text-gray-400 text-sm leading-tight': !isError, - 'text-red-300!': isError, - }), - singleValue: () => - cn({ - 'm-0! text-gray-900 text-sm leading-tight': !isError, - 'text-error!': isError, - 'text-gray-900!': readOnly, - }), - input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'), - indicatorsContainer: () => cn('flex items-center gap-1 pr-3 py-2'), - dropdownIndicator: ({ isFocused }) => - cn('p-0! rounded hover:bg-gray-100', { - 'text-gray-900': isFocused, - 'text-gray-500': !isFocused, - 'text-error!': isError, - }), - clearIndicator: () => cn('p-0! rounded hover:bg-gray-100'), - menu: () => - cn( - 'border border-base-content/5 rounded-xl! bg-base-100 shadow-lg! my-1.5!' - ), - menuList: () => cn('p-0! max-h-60 overflow-auto'), - option: ({ isFocused, isSelected }) => - cn('px-3 py-2 rounded-md cursor-pointer!', { - 'bg-indigo-600 text-white': isFocused, - 'bg-blue-500!': isSelected, - 'text-gray-700': !isFocused && !isSelected, - }), - multiValue: ({ getValue, index }) => { - const selectedValues = getValue() as T[]; - return cn( - 'bg-base-200! rounded-lg! py-[3px] px-2.5 m-0! flex items-center gap-1! w-fit gap-2!', - selectedValues[index]?.className - ); - }, - multiValueRemove: () => cn('p-0! w-3 h-3'), - multiValueLabel: ({ getValue, index }) => { - const selectedValues = getValue() as T[]; - return cn( - 'p-0! text-base-content! text-xs!', - selectedValues[index]?.labelClassName - ); - }, - }} - components={{ - ...components, - ...(optionComponent ? { Option: optionComponent } : {}), - MenuList: CustomMenuList, - }} - {...(startAdornment && { - shouldShowAdornment, - startAdornment, - })} - menuPortalTarget={ - typeof document !== 'undefined' - ? (menuPortalTarget ?? document.body) - : undefined - } - styles={{ - menuPortal: (base) => ({ ...base, zIndex: 9999 }), - multiValue(base) { - return { - ...base, - borderRadius: '8px', - }; - }, - }} - onMenuScrollToBottom={onMenuScrollToBottom} - /> + placeholder: () => + cn({ + 'text-gray-400 text-sm leading-tight': !isError, + 'text-red-300!': isError, + }), + singleValue: () => + cn({ + 'm-0! text-gray-900 text-sm leading-tight': !isError, + 'text-error!': isError, + 'text-gray-900!': readOnly, + }), + input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'), + indicatorsContainer: () => cn('flex items-center gap-1 pr-3 py-2'), + dropdownIndicator: ({ isFocused }) => + cn('p-0! rounded hover:bg-gray-100', { + 'text-gray-900': isFocused, + 'text-gray-500': !isFocused, + 'text-error!': isError, + }), + clearIndicator: () => cn('p-0! rounded hover:bg-gray-100'), + menu: () => + cn( + 'border border-base-content/5 rounded-xl! bg-base-100 shadow-lg! my-1.5!' + ), + menuList: () => cn('p-0! max-h-60 overflow-auto'), + option: ({ isFocused, isSelected }) => + cn('px-3 py-2 rounded-md cursor-pointer!', { + 'bg-indigo-600 text-white': isFocused, + 'bg-blue-500!': isSelected, + 'text-gray-700': !isFocused && !isSelected, + }), + multiValue: ({ getValue, index }) => { + const selectedValues = getValue() as T[]; + return cn( + 'bg-base-200! rounded-lg! py-[3px] px-2.5 m-0! flex items-center gap-1! w-fit gap-2!', + selectedValues[index]?.className + ); + }, + multiValueRemove: () => cn('p-0! w-3 h-3'), + multiValueLabel: ({ getValue, index }) => { + const selectedValues = getValue() as T[]; + return cn( + 'p-0! text-base-content! text-xs!', + selectedValues[index]?.labelClassName + ); + }, + }} + components={{ + ...components, + ...(optionComponent ? { Option: optionComponent } : {}), + MenuList: CustomMenuList, + }} + {...(startAdornment && { + shouldShowAdornment, + startAdornment, + })} + menuPortalTarget={ + typeof document !== 'undefined' + ? (menuPortalTarget ?? document.body) + : undefined + } + styles={{ + menuPortal: (base) => ({ ...base, zIndex: 9999 }), + multiValue(base) { + return { + ...base, + borderRadius: '8px', + }; + }, + }} + onMenuScrollToBottom={onMenuScrollToBottom} + /> + )} {isError &&

{errorMessage}

} {!isError && bottomLabel && ( diff --git a/src/components/input/TextInput.tsx b/src/components/input/TextInput.tsx index 2365d73d..89d4f059 100644 --- a/src/components/input/TextInput.tsx +++ b/src/components/input/TextInput.tsx @@ -102,7 +102,7 @@ const TextInput = ({ {inputPrefix && (
{ const { filterValues, setFilterValues, resetFilterValues } = useDashboardStore(); + // ===== UI STORE (for navbar actions) ===== + const setNavbarActions = useUiStore((state) => state.setNavbarActions); + const clearNavbarActions = useUiStore((state) => state.clearNavbarActions); + const [analysisMode, setAnalysisMode] = useState<'OVERVIEW' | 'COMPARISON'>( (filterValues.analysisMode as 'OVERVIEW' | 'COMPARISON') || 'OVERVIEW' ); @@ -67,9 +73,8 @@ const DashboardProduction = () => { normalizeToArray(filterValues.location) ); const [exporting, setExporting] = useState(false); - const statsRef = useRef(null); - const chartRef = useRef(null); - const allChartsRef = useRef(null); + const allChartsRef = useRef(null); + const allStatsRef = useRef(null); // ===== FETCH DATA ===== const { @@ -194,12 +199,69 @@ const DashboardProduction = () => { const handleExportPDF = async () => { await generateDashboardPDF({ filterValues: formik.values, - statsRef, + allStatsRef, allChartsRef, setExporting, }); }; + // ===== Register Navbar Actions ===== + const openFilterModalRef = useRef(filterModal.openModal); + openFilterModalRef.current = filterModal.openModal; + + useEffect(() => { + setNavbarActions( +
+ openFilterModalRef.current()} + /> + + + Export +
+ +
+ + } + className={{ + content: 'w-full mt-1 p-0', + }} + > + + + +
+
+ ); + }, [formik.values, exporting, setNavbarActions]); + + // Cleanup only on unmount + useEffect(() => { + return () => { + clearNavbarActions(); + }; + }, [clearNavbarActions]); + if (isLoadingDashboardProductionData) { return (
@@ -210,48 +272,62 @@ const DashboardProduction = () => { return ( <> -
-
-
- -
- filterModal.openModal()} - /> - - - Export - - - } - className={{ - content: 'w-full', - }} +
+
+ openFilterModalRef.current()} + /> + + + Export +
+ +
+ + } + className={{ + content: + 'w-full mt-1 p-0 shadow-button-soft border border-base-content/10 rounded-lg', + }} + > + - - - - -
+ + +
- {/* Dashboard Stats */} -
+
{/* Use DashboardLineChart component or skeleton */} -
+
{isLoadingDashboardProductionData ? ( ) : dashboardProductionData && @@ -287,28 +363,46 @@ const DashboardProduction = () => { {/* Hidden container for all charts (used for PDF export in OVERVIEW mode) */} {dashboardProductionData && ( -
- + {/* Export Stats Charts */} +
+ +
+ + {/* Export ALL Charts */} +
+ -
+ } + /> +
+ )}
@@ -316,102 +410,106 @@ const DashboardProduction = () => { ref={filterModal.ref} className={{ modal: 'p-0', - modalBox: 'p-0 rounded-xl', + modalBox: 'p-0 rounded-[0.875rem]', }} > -
+
{/* Modal Header */} -
-
+
+
-

Filter Data

+

Filter Data

- {/* Rentang Waktu */} -
- -
- -
- +
+ {/* Rentang Waktu */} +
+ +
+ +
+ +
-
- {/* Analysis Mode */} -
- - { - formik.handleChange(e); - setAnalysisMode(e.target.value as 'OVERVIEW' | 'COMPARISON'); - // Reset all dependent fields when analysis mode changes - formik.setFieldValue('location', []); - formik.setFieldValue('flock', []); - formik.setFieldValue('kandang', []); - formik.setFieldValue('comparisonType', ''); - setSelectedLocationIds([]); - }} - color='primary' - className={{ - wrapper: 'w-full my-6 font-semibold text-neutral-500', - }} - > - + + { + formik.handleChange(e); + setAnalysisMode( + e.target.value as 'OVERVIEW' | 'COMPARISON' + ); + // Reset all dependent fields when analysis mode changes + formik.setFieldValue('location', []); + formik.setFieldValue('flock', []); + formik.setFieldValue('kandang', []); + formik.setFieldValue('comparisonType', ''); + setSelectedLocationIds([]); + }} color='primary' - value='OVERVIEW' - label='Performance Overview' - /> - - -
+ className={{ + wrapper: + 'w-full flex flex-row items-center font-medium text-base-content/50', + radioWrapper: 'w-full grid grid-cols-2 gap-0 p-0', + }} + > + + + +
- {formik.values.analysisMode === 'COMPARISON' && ( -
+ {formik.values.analysisMode === 'COMPARISON' && ( { Boolean(formik.errors.comparisonType) && Boolean(formik.touched.comparisonType) } + className={{ + select: 'rounded-lg text-sm border-base-content/10', + }} /> -
- )} + )} - {/* Location */} -
+ {/* Location */} {comparisonTypeOptions.find( (option) => option.value === formik.values.comparisonType )?.value === 'FARM' ? ( @@ -465,6 +564,9 @@ const DashboardProduction = () => { Boolean(formik.errors.location) && Boolean(formik.touched.location) } + className={{ + select: 'rounded-lg text-sm border-base-content/10', + }} /> ) : ( { Boolean(formik.errors.location) && Boolean(formik.touched.location) } + className={{ + select: 'rounded-lg text-sm border-base-content/10', + }} /> )} + + {/* Flock */} + {!( + formik.values.analysisMode === 'COMPARISON' && + !( + formik.values.comparisonType === 'FLOCK' || + formik.values.comparisonType === 'KANDANG' + ) + ) && ( + <> + {comparisonTypeOptions.find( + (option) => option.value === formik.values.comparisonType + )?.value === 'FLOCK' ? ( + + formik.setFieldValue('flock', selected) + } + errorMessage={formik.errors.flock as string} + onInputChange={setInputValueFlock} + onMenuScrollToBottom={loadMoreFlock} + options={flockOptions} + isLoading={isLoadingFlockOptions} + isError={ + Boolean(formik.errors.flock) && + Boolean(formik.touched.flock) + } + className={{ + select: 'rounded-lg text-sm border-base-content/10', + }} + /> + ) : ( + + formik.setFieldValue('flock', selected) + } + errorMessage={formik.errors.flock as string} + onInputChange={setInputValueFlock} + onMenuScrollToBottom={loadMoreFlock} + options={flockOptions} + isLoading={isLoadingFlockOptions} + isError={ + Boolean(formik.errors.flock) && + Boolean(formik.touched.flock) + } + className={{ + select: 'rounded-lg text-sm border-base-content/10', + }} + /> + )} + + )} + + {/* Kandang */} + {!( + formik.values.analysisMode === 'COMPARISON' && + !(formik.values.comparisonType === 'KANDANG') + ) && ( + <> + {comparisonTypeOptions.find( + (option) => option.value === formik.values.comparisonType + )?.value === 'KANDANG' ? ( + + formik.setFieldValue('kandang', selected) + } + errorMessage={formik.errors.kandang as string} + onInputChange={setInputValueKandang} + onMenuScrollToBottom={loadMoreKandang} + options={kandangOptions} + isLoading={isLoadingKandangOptions} + isError={ + Boolean(formik.errors.kandang) && + Boolean(formik.touched.kandang) + } + className={{ + select: 'rounded-lg text-sm border-base-content/10', + }} + /> + ) : ( + + formik.setFieldValue('kandang', selected) + } + errorMessage={formik.errors.kandang as string} + onInputChange={setInputValueKandang} + onMenuScrollToBottom={loadMoreKandang} + options={kandangOptions} + isLoading={isLoadingKandangOptions} + isError={ + Boolean(formik.errors.kandang) && + Boolean(formik.touched.kandang) + } + className={{ + select: 'rounded-lg text-sm border-base-content/10', + }} + /> + )} + + )} + + {formErrorList.length > 0 && ( +
+ +
+ )}
- {/* Flock */} - {!( - formik.values.analysisMode === 'COMPARISON' && - !( - formik.values.comparisonType === 'FLOCK' || - formik.values.comparisonType === 'KANDANG' - ) - ) && ( -
- {comparisonTypeOptions.find( - (option) => option.value === formik.values.comparisonType - )?.value === 'FLOCK' ? ( - - formik.setFieldValue('flock', selected) - } - errorMessage={formik.errors.flock as string} - onInputChange={setInputValueFlock} - onMenuScrollToBottom={loadMoreFlock} - options={flockOptions} - isLoading={isLoadingFlockOptions} - isError={ - Boolean(formik.errors.flock) && - Boolean(formik.touched.flock) - } - /> - ) : ( - - formik.setFieldValue('flock', selected) - } - errorMessage={formik.errors.flock as string} - onInputChange={setInputValueFlock} - onMenuScrollToBottom={loadMoreFlock} - options={flockOptions} - isLoading={isLoadingFlockOptions} - isError={ - Boolean(formik.errors.flock) && - Boolean(formik.touched.flock) - } - /> - )} -
- )} - - {/* Kandang */} - {!( - formik.values.analysisMode === 'COMPARISON' && - !(formik.values.comparisonType === 'KANDANG') - ) && ( -
- {comparisonTypeOptions.find( - (option) => option.value === formik.values.comparisonType - )?.value === 'KANDANG' ? ( - - formik.setFieldValue('kandang', selected) - } - errorMessage={formik.errors.kandang as string} - onInputChange={setInputValueKandang} - onMenuScrollToBottom={loadMoreKandang} - options={kandangOptions} - isLoading={isLoadingKandangOptions} - isError={ - Boolean(formik.errors.kandang) && - Boolean(formik.touched.kandang) - } - /> - ) : ( - - formik.setFieldValue('kandang', selected) - } - errorMessage={formik.errors.kandang as string} - onInputChange={setInputValueKandang} - onMenuScrollToBottom={loadMoreKandang} - options={kandangOptions} - isLoading={isLoadingKandangOptions} - isError={ - Boolean(formik.errors.kandang) && - Boolean(formik.touched.kandang) - } - /> - )} -
- )} - -
- -
- {/* Action Buttons */} -
+
-
diff --git a/src/components/pages/dashboard/chart/DashboardLineChart.tsx b/src/components/pages/dashboard/chart/DashboardLineChart.tsx index f2449795..58749cf2 100644 --- a/src/components/pages/dashboard/chart/DashboardLineChart.tsx +++ b/src/components/pages/dashboard/chart/DashboardLineChart.tsx @@ -1,6 +1,7 @@ import Button from '@/components/Button'; import Card from '@/components/Card'; import Dropdown from '@/components/Dropdown'; +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; import { OptionType } from '@/components/input/SelectInput'; import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; @@ -147,11 +148,12 @@ const DashboardLineChart = ({ return ( -
+
Performance{' '} setOpen(!open)} > {chartTypeLabels[chartData]}{' '} -
- +
+ +
} - className={{ - content: 'w-52 mt-3', - }} controlled={open} > - + { setChartData('body_weight'); setOpen(!open); @@ -192,6 +196,7 @@ const DashboardLineChart = ({ /> { setChartData('performance'); setOpen(!open); @@ -199,6 +204,7 @@ const DashboardLineChart = ({ /> { setChartData('fcr'); setOpen(!open); @@ -206,6 +212,7 @@ const DashboardLineChart = ({ /> { setChartData('quality_control'); setOpen(!open); @@ -213,6 +220,7 @@ const DashboardLineChart = ({ /> { setChartData('deplesi'); setOpen(!open); @@ -248,8 +256,8 @@ const DashboardLineChart = ({ .includes('std'); return ( - + ); }); })()} @@ -335,20 +345,68 @@ const DashboardLineChart = ({ { // Calculate dynamic domain based on visible data let seriesData: DashboardChartsSeries[] = []; @@ -399,14 +457,12 @@ const DashboardLineChart = ({ })()} ticks={(() => { // Calculate dynamic ticks based on domain - let seriesData: DashboardChartsSeries[] = []; let dataset: DashboardChartsDataset[] = []; if ( analysisMode === 'OVERVIEW' && isOverviewCharts(data.charts) ) { - seriesData = data.charts[chartData]?.series || []; dataset = data.charts[chartData]?.dataset || []; } else if ( analysisMode === 'COMPARISON' && @@ -416,7 +472,6 @@ const DashboardLineChart = ({ data.charts.farm || data.charts.flock || data.charts.kandang; - seriesData = comparisonChart?.series || []; dataset = comparisonChart?.dataset || []; } @@ -436,6 +491,20 @@ const DashboardLineChart = ({ const minValue = Math.min(...allValues); const maxValue = Math.max(...allValues); + + // Handle edge case where min equals max + if (minValue === maxValue) { + const value = Math.round(minValue); + const padding = Math.max(10, Math.abs(value) * 0.2); + return [ + Math.floor(value - padding), + Math.floor(value - padding / 2), + value, + Math.ceil(value + padding / 2), + Math.ceil(value + padding), + ]; + } + const padding = (maxValue - minValue) * 0.1; const domainMin = Math.floor(Math.max(0, minValue - padding)); const domainMax = Math.ceil(maxValue + padding); @@ -444,21 +513,25 @@ const DashboardLineChart = ({ const range = domainMax - domainMin; const step = range / 4; - return [ + // Use Set to ensure unique values + const tickSet = new Set([ domainMin, Math.round(domainMin + step), Math.round(domainMin + step * 2), Math.round(domainMin + step * 3), domainMax, - ]; + ]); + + return Array.from(tickSet).sort((a, b) => a - b); })()} + tickFormatter={(value) => formatNumber(Number(value))} /> `Week ${value}`} content={(props) => { return ( -
-

+

+

{analysisMode === 'OVERVIEW' ? selectedKandang ? selectedKandang.label || 'Overview Performance' @@ -506,12 +579,12 @@ const DashboardLineChart = ({ return (

  • - +
    -

    +

    Week {props.label}

    @@ -598,7 +671,7 @@ const DashboardLineChart = ({ return ( {/* Chart icon */} -
    - -
    - - {/* Empty state text */} -

    - Data Not Yet Available -

    -

    - Please change your filters to get the data. -

    + + } + title='Data Not Yet Available' + description='Please change your filters to get the data.' + />
  • ); } diff --git a/src/components/pages/dashboard/chart/DashboardStats.tsx b/src/components/pages/dashboard/chart/DashboardStats.tsx index dcb0707f..331547b0 100644 --- a/src/components/pages/dashboard/chart/DashboardStats.tsx +++ b/src/components/pages/dashboard/chart/DashboardStats.tsx @@ -21,7 +21,7 @@ const CARD_CONFIG = [ key: 'Avg. Selling Price', icon: 'heroicons:document-currency-dollar', alertColor: 'success' as const, - suffix: ' /Kg', + suffix: ' /Kg Telur', prefix: '', }, { @@ -48,7 +48,7 @@ const DashboardStats = ({ data }: DashboardStatsProps) => { icon: isPositive ? 'heroicons:arrow-trending-up' : 'heroicons:arrow-trending-down', - color: isPositive ? 'text-success' : 'text-error', + color: isPositive ? 'text-[#008000]' : 'text-[#FF3A3A]', value: Math.abs(percent), }; }; @@ -60,14 +60,16 @@ const DashboardStats = ({ data }: DashboardStatsProps) => { {prefix} {formatNumber(value)} {suffix && ( - {suffix} + + {suffix} + )} ); }; return ( -
    +
    {CARD_CONFIG.map((config) => { // Find matching data from API const cardData = data.find((item) => item.label === config.key); @@ -78,35 +80,41 @@ const DashboardStats = ({ data }: DashboardStatsProps) => { -
    +
    +
    From last month
    -
    +
    Filter Required
    } > -
    - +
    +
    -

    +

    {config.key}

    -

    +

    ********

    @@ -121,17 +129,20 @@ const DashboardStats = ({ data }: DashboardStatsProps) => { -
    +
    From last month
    {trend.value}% @@ -143,15 +154,15 @@ const DashboardStats = ({ data }: DashboardStatsProps) => { - + -
    -

    +
    +

    {cardData.label}

    -

    +

    {formatValue(cardData.value, config.prefix, config.suffix)}

    diff --git a/src/components/pages/dashboard/chart/DashboardAllCharts.tsx b/src/components/pages/dashboard/export/DashboardExportCharts.tsx similarity index 96% rename from src/components/pages/dashboard/chart/DashboardAllCharts.tsx rename to src/components/pages/dashboard/export/DashboardExportCharts.tsx index fe0db0a7..ba0b2fe2 100644 --- a/src/components/pages/dashboard/chart/DashboardAllCharts.tsx +++ b/src/components/pages/dashboard/export/DashboardExportCharts.tsx @@ -17,12 +17,12 @@ import { YAxis, } from 'recharts'; -type DashboardAllChartsProps = { +type DashboardExportChartsProps = { data: Dashboard; analysisMode: string; }; -export type DashboardAllChartsRef = { +export type DashboardExportChartsRef = { getChartRefs: () => { key: string; ref: HTMLDivElement | null; @@ -99,9 +99,9 @@ const chartTypeLabels: Record = { deplesi: 'Deplesi', }; -const DashboardAllCharts = forwardRef< - DashboardAllChartsRef, - DashboardAllChartsProps +const DashboardExportCharts = forwardRef< + DashboardExportChartsRef, + DashboardExportChartsProps >(({ data, analysisMode }, ref) => { // Create refs for charts - use string keys for flexibility const chartRefs = useRef<{ @@ -189,7 +189,8 @@ const DashboardAllCharts = forwardRef< > @@ -338,6 +339,6 @@ const DashboardAllCharts = forwardRef< ); }); -DashboardAllCharts.displayName = 'DashboardAllCharts'; +DashboardExportCharts.displayName = 'DashboardExportCharts'; -export default DashboardAllCharts; +export default DashboardExportCharts; diff --git a/src/components/pages/dashboard/export/DashboardExportStats.tsx b/src/components/pages/dashboard/export/DashboardExportStats.tsx new file mode 100644 index 00000000..aebdc751 --- /dev/null +++ b/src/components/pages/dashboard/export/DashboardExportStats.tsx @@ -0,0 +1,197 @@ +import Alert from '@/components/Alert'; +import Card from '@/components/Card'; +import { formatNumber } from '@/lib/helper'; +import { DashboardStatisticsData } from '@/types/api/dashboard/dashboard'; +import { Icon } from '@iconify/react'; +import { forwardRef, useImperativeHandle, useRef } from 'react'; +interface DashboardStatsProps { + data: DashboardStatisticsData[]; +} +export type DashboardExportStatsRef = { + getStatsRefs: () => { + key: string; + ref: HTMLDivElement | null; + label: string; + }[]; + getContainerRef: () => HTMLDivElement | null; +}; + +// Konfigurasi untuk setiap kartu +const CARD_CONFIG = [ + { + key: 'HPP Global', + icon: 'heroicons:banknotes', + alertColor: 'warning' as const, + suffix: ' /Kg', + prefix: 'RP ', + }, + { + key: 'Avg. Selling Price', + icon: 'heroicons:document-currency-dollar', + alertColor: 'success' as const, + suffix: ' /Kg', + prefix: '', + }, + { + key: 'FCR', + icon: 'heroicons:clipboard-document-list', + alertColor: 'info' as const, + suffix: '', + prefix: '', + }, + { + key: 'Mortality', + icon: 'heroicons:exclamation-triangle', + alertColor: 'error' as const, + suffix: ' %', + prefix: '', + }, +]; + +const DashboardExportStats = forwardRef< + DashboardExportStatsRef, + DashboardStatsProps +>(({ data }, ref) => { + const containerRef = useRef(null); + // Helper to get trend icon and color + const getTrendDisplay = (percent: number) => { + const isPositive = percent >= 0; + return { + icon: isPositive + ? 'heroicons:arrow-trending-up' + : 'heroicons:arrow-trending-down', + color: isPositive ? 'text-[#008000]' : 'text-[#FF3A3A]', + value: Math.abs(percent), + }; + }; + + // Helper to format value + const formatValue = (value: number, prefix: string, suffix: string) => { + return ( + <> + {prefix} + {formatNumber(value)} + {suffix && ( + + {suffix} + + )} + + ); + }; + + // Expose container ref through imperative handle + useImperativeHandle(ref, () => ({ + getStatsRefs: () => [], + getContainerRef: () => containerRef.current, + })); + + return ( +
    + {CARD_CONFIG.map((config) => { + // Find matching data from API + const cardData = data.find((item) => item.label === config.key); + + if (!cardData) { + // Show placeholder card for missing data (FCR & Mortality) + return ( + +
    + From last month +
    +
    + Filter Required +
    +
    + } + > +
    + + + +
    +

    + {config.key} +

    +

    + ******** +

    +
    +
    +
    + ); + } + + const trend = getTrendDisplay(cardData.percent_last_month); + + return ( + +
    + From last month +
    +
    + + {trend.value}% +
    +

    + } + > +
    + + + +
    +

    + {cardData.label} +

    +

    + {formatValue(cardData.value, config.prefix, config.suffix)} +

    +
    +
    + + ); + })} +
    + ); +}); + +DashboardExportStats.displayName = 'DashboardExportStats'; + +export default DashboardExportStats; diff --git a/src/components/pages/dashboard/export/DashboardPDF.ts b/src/components/pages/dashboard/export/DashboardPDF.ts index 17c5bde4..8b4c7e6a 100644 --- a/src/components/pages/dashboard/export/DashboardPDF.ts +++ b/src/components/pages/dashboard/export/DashboardPDF.ts @@ -3,18 +3,19 @@ import { toPng } from 'html-to-image'; import toast from 'react-hot-toast'; import { formatDate } from '@/lib/helper'; import { DashboardFilterType } from '@/components/pages/dashboard/filter/DashboardProductionFilter.schema'; -import { DashboardAllChartsRef } from '@/components/pages/dashboard/chart/DashboardAllCharts'; +import { DashboardExportChartsRef } from '@/components/pages/dashboard/export/DashboardExportCharts'; +import { DashboardExportStatsRef } from '@/components/pages/dashboard/export/DashboardExportStats'; interface DashboardPDFExportParams { filterValues: DashboardFilterType; - statsRef: React.RefObject; - allChartsRef: React.RefObject; + allStatsRef: React.RefObject; + allChartsRef: React.RefObject; setExporting: (value: boolean) => void; } export const generateDashboardPDF = async ({ filterValues, - statsRef, + allStatsRef, allChartsRef, setExporting, }: DashboardPDFExportParams): Promise => { @@ -168,31 +169,34 @@ export const generateDashboardPDF = async ({ yPosition += 10; // Capture and add stats if available - if (statsRef.current) { - const statsImage = await toPng(statsRef.current, { - quality: 1, - pixelRatio: 2, - }); - const statsImgProps = pdf.getImageProperties(statsImage); - const statsWidth = pageWidth - 2 * margin; - const statsHeight = - (statsImgProps.height * statsWidth) / statsImgProps.width; + if (allStatsRef.current) { + const statsContainer = allStatsRef.current.getContainerRef(); + if (statsContainer) { + const statsImage = await toPng(statsContainer, { + quality: 1, + pixelRatio: 2, + }); + const statsImgProps = pdf.getImageProperties(statsImage); + const statsWidth = pageWidth - 2 * margin; + const statsHeight = + (statsImgProps.height * statsWidth) / statsImgProps.width; - // Check if we need a new page - if (yPosition + statsHeight > pageHeight - margin) { - pdf.addPage(); - yPosition = margin; + // Check if we need a new page + if (yPosition + statsHeight > pageHeight - margin) { + pdf.addPage(); + yPosition = margin; + } + + pdf.addImage( + statsImage, + 'PNG', + margin, + yPosition, + statsWidth, + statsHeight + ); + yPosition += statsHeight + 10; } - - pdf.addImage( - statsImage, - 'PNG', - margin, - yPosition, - statsWidth, - statsHeight - ); - yPosition += statsHeight + 10; } if (allChartsRef.current) { diff --git a/src/components/pages/dashboard/skeleton/DashboardLineChartSkeleton.tsx b/src/components/pages/dashboard/skeleton/DashboardLineChartSkeleton.tsx index b479eced..7f19a6d0 100644 --- a/src/components/pages/dashboard/skeleton/DashboardLineChartSkeleton.tsx +++ b/src/components/pages/dashboard/skeleton/DashboardLineChartSkeleton.tsx @@ -1,96 +1,89 @@ import { Icon } from '@iconify/react'; import { DashboardMeta } from '@/types/api/dashboard/dashboard'; +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; const DashboardLineChartSkeleton = ({ meta }: { meta?: DashboardMeta }) => { return ( -
    +
    {/* Header with title skeleton */} -
    +
    Performance{' '}
    {/* Chart area with axes skeleton */} -
    - {/* Main chart container */} -
    - {/* Y-axis skeleton (left side) */} -
    - {[1, 2, 3, 4, 5, 6].map((item) => ( -
    - ))} -
    - - {/* Chart content area */} -
    - {/* Empty state centered in chart area */} -
    - {!meta?.filters && ( - <> - {/* Filter icon */} -
    +
    + {/* Chart content area */} +
    + {/* Empty state centered in chart area */} +
    + {!meta?.filters && ( + <> + {/* Filter icon */} + -
    - - {/* Empty state text */} -

    - No Filters Selected -

    -

    - Please choose filters to narrow down your results and make - your search easier. -

    - - )} - {meta?.filters && ( - <> - {/* Filter icon */} -
    + } + title='No Filters Selected' + description='Please choose filters to narrow down your results and make your search easier.' + /> + + )} + {meta?.filters && ( + <> + {/* Filter icon */} + -
    + } + title='Data Not Yet Available' + description='Please change your filters to get the data.' + /> + + )} +
    - {/* Empty state text */} -

    - Data Not Yet Available -

    -

    - Please change your filters to get the data. -

    - - )} +
    +
    +
    - - {/* Placeholder for chart height */} -
    - - {/* X-axis skeleton (bottom) */} -
    - {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => ( -
    +
    + {[1, 2, 3, 4].map((item) => ( +
    +
    +
    +
    ))}
    + + {/* X-axis skeleton (bottom) */} +
    + {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => ( +
    + ))} +
    +
    +
    +
    diff --git a/src/components/pages/expense/form/ExpenseRequestForm.tsx b/src/components/pages/expense/form/ExpenseRequestForm.tsx index ed30de3d..733204d9 100644 --- a/src/components/pages/expense/form/ExpenseRequestForm.tsx +++ b/src/components/pages/expense/form/ExpenseRequestForm.tsx @@ -372,23 +372,6 @@ const ExpenseRequestForm = ({ onReset={formik.handleReset} className='w-full mt-8 flex flex-col gap-6' > - {expenseFormErrorMessage && ( -
    - - {expenseFormErrorMessage} -
    - )} - - {formErrorList.length > 0 && ( - setFormErrorList([])} - /> - )}
    + {expenseFormErrorMessage && ( +
    + + {expenseFormErrorMessage} +
    + )} + + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} +
    {type !== 'add' && (
    diff --git a/src/components/pages/inventory/movement/form/MovementForm.schema.ts b/src/components/pages/inventory/movement/form/MovementForm.schema.ts index 18161974..1de5747e 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.schema.ts +++ b/src/components/pages/inventory/movement/form/MovementForm.schema.ts @@ -27,12 +27,12 @@ type MovementFormSchemaType = { product_qty: number | string; }[]; deliveries: { - delivery_cost?: number | string; - delivery_cost_per_item?: number | string; + delivery_cost?: number | string | null; + delivery_cost_per_item?: number | string | null; document?: File | MovementDocument | null; document_path?: string | null; - driver_name: string; - vehicle_plate: string; + driver_name?: string | null; + vehicle_plate?: string | null; supplier?: { value: number; label: string; @@ -59,12 +59,12 @@ export type ProductSchema = { }; export type DeliverySchema = { - delivery_cost?: number | string; - delivery_cost_per_item?: number | string; + delivery_cost?: number | string | null; + delivery_cost_per_item?: number | string | null; document?: File | MovementDocument | null; document_path?: string | null; - driver_name: string; - vehicle_plate: string; + driver_name?: string | null; + vehicle_plate?: string | null; supplier?: { value: number; label: string; @@ -120,32 +120,64 @@ const DeliveryDocumentSchema = Yup.mixed() const DeliveryObjectSchema: Yup.ObjectSchema = Yup.object({ delivery_cost: Yup.number() - .transform((value) => (isNaN(value) || value === 0 ? undefined : value)) - .min(1, 'Biaya minimal 1!') - .typeError('Biaya harus berupa angka!') - .test('one-of-cost-fields', 'Wajib diisi salah satu!', function (value) { - const { delivery_cost_per_item } = this.parent; - return ( - (value !== undefined && value > 0) || - (delivery_cost_per_item !== undefined && delivery_cost_per_item > 0) - ); + .transform((value) => + isNaN(value) || value === '' || value === null ? undefined : value + ) + .when('supplier_id', { + is: (supplier_id: number | null | undefined) => + supplier_id !== null && supplier_id !== undefined && supplier_id > 0, + then: (schema) => + schema + .required('Biaya pengiriman wajib diisi!') + .min(1, 'Biaya minimal 1!') + .typeError('Biaya harus berupa angka!'), + otherwise: (schema) => + schema + .optional() + .nullable() + .min(1, 'Biaya minimal 1!') + .typeError('Biaya harus berupa angka!'), }), delivery_cost_per_item: Yup.number() - .transform((value) => (isNaN(value) || value === 0 ? undefined : value)) - .min(1, 'Biaya per item minimal 1!') - .typeError('Biaya per item harus berupa angka!') - .test('one-of-cost-fields', 'Wajib diisi salah satu!', function (value) { - const { delivery_cost } = this.parent; - return ( - (value !== undefined && value > 0) || - (delivery_cost !== undefined && delivery_cost > 0) - ); + .transform((value) => + isNaN(value) || value === '' || value === null ? undefined : value + ) + .when('supplier_id', { + is: (supplier_id: number | null | undefined) => + supplier_id !== null && supplier_id !== undefined && supplier_id > 0, + then: (schema) => + schema + .required('Biaya per item wajib diisi!') + .min(1, 'Biaya per item minimal 1!') + .typeError('Biaya per item harus berupa angka!'), + otherwise: (schema) => + schema + .optional() + .nullable() + .min(1, 'Biaya per item minimal 1!') + .typeError('Biaya per item harus berupa angka!'), }), document_path: Yup.string().nullable().optional(), document_index: Yup.number().optional(), document: DeliveryDocumentSchema, - driver_name: Yup.string().required('Nama sopir wajib diisi!'), - vehicle_plate: Yup.string().required('Plat nomor wajib diisi!'), + driver_name: Yup.string().when('supplier_id', { + is: (supplier_id: number | null | undefined) => + supplier_id !== null && supplier_id !== undefined && supplier_id > 0, + then: (schema) => + schema + .required('Nama sopir wajib diisi!') + .min(1, 'Nama sopir wajib diisi!'), + otherwise: (schema) => schema.optional().nullable(), + }), + vehicle_plate: Yup.string().when('supplier_id', { + is: (supplier_id: number | null | undefined) => + supplier_id !== null && supplier_id !== undefined && supplier_id > 0, + then: (schema) => + schema + .required('Plat nomor wajib diisi!') + .min(1, 'Plat nomor wajib diisi!'), + otherwise: (schema) => schema.optional().nullable(), + }), supplier: Yup.object({ value: Yup.number().min(1).required(), label: Yup.string().required(), @@ -279,12 +311,12 @@ export const getMovementFormInitialValues = ( }) ?? [], })) ?? [ { - delivery_cost: undefined, - delivery_cost_per_item: undefined, + delivery_cost: null, + delivery_cost_per_item: null, document: null, document_path: null, - driver_name: '', - vehicle_plate: '', + driver_name: null, + vehicle_plate: null, supplier: null, supplier_id: 0, products: [ diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index ff3edbfc..dbb30314 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -86,6 +86,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { } // ===== USE SELECT HOOKS ===== + const { + setInputValue: setSourceWarehouseSelectInputValue, + isLoadingOptions: isLoadingSourceWarehouses, + loadMore: loadMoreSourceWarehouses, + rawData: sourceWarehouses, + } = useSelect(WarehouseApi.basePath, 'id', 'name', 'search', { + transfer_context: 'inventory_transfer', + }); + const { setInputValue: setWarehouseSelectInputValue, isLoadingOptions: isLoadingWarehouses, @@ -136,6 +145,25 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { return stockMap; }, [allProductWarehouses]); + const sourceWarehouseOptions = useMemo(() => { + if (!isResponseSuccess(sourceWarehouses)) return []; + + return ( + sourceWarehouses?.data.map((w) => { + warehouseStockMap.get(w.id); + return { + value: w.id, + label: w.name, + area: w.area?.name, + location: + 'type' in w && (w.type === 'LOKASI' || w.type === 'KANDANG') + ? w.location?.name + : undefined, + }; + }) || [] + ); + }, [sourceWarehouses, warehouseStockMap]); + const warehouseOptions = useMemo(() => { if (!isResponseSuccess(warehouses)) return []; @@ -228,19 +256,49 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { } } - return { - delivery_cost: parseInt((d.delivery_cost || '').toString()) || 0, - delivery_cost_per_item: - parseInt((d.delivery_cost_per_item || '').toString()) || 0, - document_index: documentIndex, - driver_name: d.driver_name, - vehicle_plate: d.vehicle_plate, - supplier_id: d.supplier_id, + const deliveryObj: { + products: Array<{ product_id: number; product_qty: number }>; + delivery_cost?: number; + delivery_cost_per_item?: number; + document_index?: number; + driver_name?: string; + vehicle_plate?: string; + supplier_id?: number; + } = { products: d.products.map((p) => ({ product_id: p.product_id, product_qty: parseInt(p.product_qty.toString()) || 0, })), }; + + const deliveryCost = parseInt((d.delivery_cost || '').toString()) || 0; + if (deliveryCost > 0) { + deliveryObj.delivery_cost = deliveryCost; + } + + const deliveryCostPerItem = + parseInt((d.delivery_cost_per_item || '').toString()) || 0; + if (deliveryCostPerItem > 0) { + deliveryObj.delivery_cost_per_item = deliveryCostPerItem; + } + + if (documentIndex >= 0) { + deliveryObj.document_index = documentIndex; + } + + if (d.driver_name) { + deliveryObj.driver_name = d.driver_name; + } + + if (d.vehicle_plate) { + deliveryObj.vehicle_plate = d.vehicle_plate; + } + + if (d.supplier_id) { + deliveryObj.supplier_id = d.supplier_id; + } + + return deliveryObj; }); const payload: CreateMovementPayload = { @@ -844,32 +902,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { (warehouseId: number) => { const stockInfo = warehouseStockMap.get(warehouseId); if (!stockInfo) { - return ( - - Kosong - - ); + return Kosong; } const { productCount } = stockInfo; - let color: 'neutral' | 'success' | 'warning' = 'neutral'; - if (productCount === 0) color = 'warning'; - else if (productCount > 0) color = 'success'; return ( - + Tersedia {productCount} produk - + ); }, [warehouseStockMap] @@ -971,6 +1012,28 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { [formik.values.deliveries, formik.values.products, type] ); + const isSupplierSelected = useCallback( + (deliveryIdx: number) => { + const delivery = formik.values.deliveries?.[deliveryIdx]; + return ( + delivery && + delivery.supplier_id !== null && + delivery.supplier_id !== undefined && + delivery.supplier_id > 0 + ); + }, + [formik.values.deliveries] + ); + + const hasAnySupplierSelected = useMemo(() => { + return formik.values.deliveries?.some( + (delivery) => + delivery.supplier_id !== null && + delivery.supplier_id !== undefined && + delivery.supplier_id > 0 + ); + }, [formik.values.deliveries]); + // ===== COMPUTED VALUES ===== const invalidQtyRows = useMemo( () => @@ -1263,25 +1326,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { onReset={formik.handleReset} className='w-full mt-8 flex flex-col gap-6' > - {movementFormErrorMessage && ( -
    - - {movementFormErrorMessage} -
    - )} - - {/* Error List Alert */} - {formErrorList.length > 0 && ( - setFormErrorList([])} - /> - )} - {/* Top card - Movement details */} { placeholder='Pilih gudang asal...' value={formik.values.source_warehouse} onChange={handleSourceWarehouseChange} - options={warehouseOptions} - onInputChange={setWarehouseSelectInputValue} - onMenuScrollToBottom={loadMoreWarehouses} - isLoading={isLoadingWarehouses} + options={sourceWarehouseOptions} + onInputChange={setSourceWarehouseSelectInputValue} + onMenuScrollToBottom={loadMoreSourceWarehouses} + isLoading={isLoadingSourceWarehouses} isError={ formik.touched.source_warehouse_id && Boolean(formik.errors.source_warehouse_id) @@ -1349,7 +1393,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { errorMessage={formik.errors.source_warehouse_id as string} isDisabled={type === 'detail'} isClearable - startAdornment={ + inputPrefix={ formik.values.source_warehouse_id ? getWarehouseStockAdornment( formik.values.source_warehouse_id @@ -1407,7 +1451,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { errorMessage={formik.errors.destination_warehouse_id as string} isDisabled={type === 'detail'} isClearable - startAdornment={ + inputPrefix={ formik.values.destination_warehouse_id ? getWarehouseStockAdornment( formik.values.destination_warehouse_id @@ -1665,43 +1709,61 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { * - Supplier + + Supplier + {hasAnySupplierSelected && ( + + * + + )} + Plat Nomor - - * - + {hasAnySupplierSelected && ( + + * + + )} Dokumen Biaya Pengiriman (Rp.) - - * - + {hasAnySupplierSelected && ( + + * + + )} Biaya Per Item (Rp.) - - * - + {hasAnySupplierSelected && ( + + * + + )} Nama Sopir - - * - + {hasAnySupplierSelected && ( + + * + + )} {type !== 'detail' && Aksi} @@ -1799,10 +1861,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { { idx )} readOnly={type === 'detail'} + required={isSupplierSelected(idx)} className={{ wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80', }} @@ -1890,10 +1952,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { { idx )} readOnly={type === 'detail'} + required={isSupplierSelected(idx)} className={{ wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80', }} @@ -1914,10 +1976,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { { idx )} readOnly={type === 'detail'} + required={isSupplierSelected(idx)} className={{ wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80', }} @@ -1938,10 +2000,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { { idx )} readOnly={type === 'detail'} + required={isSupplierSelected(idx)} className={{ wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80', }} @@ -2007,6 +2069,27 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { )} +
    + {movementFormErrorMessage && ( +
    + + {movementFormErrorMessage} +
    + )} + + {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} +
    + {/* Action buttons */}
    {type !== 'detail' && ( diff --git a/src/components/pages/master-data/product-category/form/ProductCategoryForm.tsx b/src/components/pages/master-data/product-category/form/ProductCategoryForm.tsx index 1e61879c..b75795b1 100644 --- a/src/components/pages/master-data/product-category/form/ProductCategoryForm.tsx +++ b/src/components/pages/master-data/product-category/form/ProductCategoryForm.tsx @@ -159,19 +159,6 @@ const ProductCategoryForm = ({ onReset={formik.handleReset} className='w-full mt-8 flex flex-col gap-6' > - {formErrorMessage && ( -
    - - {formErrorMessage} -
    - )} - - -
    )} +
    + {formErrorMessage && ( +
    + + {formErrorMessage} +
    + )} + + +
    + {type !== 'detail' && (
    { onReset={formik.handleReset} className='w-full mt-8 flex flex-col gap-6' > - {productFormErrorMessage && ( -
    - - {productFormErrorMessage} -
    - )} - - -
    { )}
    )} + +
    + {productFormErrorMessage && ( +
    + + {productFormErrorMessage} +
    + )} + + +
    + {type !== 'detail' && (
    { } return ( - + Periode {projectFlockKandangLookup.project_flock?.period} - + ); }, [recordedProjectFlockKandangIds, projectFlockKandangLookup]); @@ -1150,33 +1143,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const hasOvkFlag = productWarehouse.product.flags?.includes('OVK'); if (hasPakanFlag) { - return ( - - PAKAN - - ); + return PAKAN; } if (hasOvkFlag) { - return ( - - OVK - - ); + return OVK; } return null; @@ -1734,22 +1705,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { onSubmit={handleFormSubmit} className='w-full mt-8 flex flex-col gap-6' > - {recordingFormErrorMessage && ( -
    - - {recordingFormErrorMessage} -
    - )} - - {/* Error List Alert */} - {formErrorList.length > 0 && ( - - )} - {/* Basic Info Card */} {(type === 'add' || type === 'edit') && ( { Boolean(formik.errors.kandang_id) } errorMessage={formik.errors.kandang_id as string} - startAdornment={ + inputPrefix={ projectFlockKandangLookup || projectFlockKandangDetail ? getProjectFlockBadgeAdornment() : undefined @@ -2474,7 +2429,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { !formik.values.project_flock_kandang_id } isClearable={type !== 'detail'} - startAdornment={ + inputPrefix={ stock.product_warehouse_id ? getProductFlagBadgeAdornment( stock.product_warehouse_id @@ -3010,6 +2965,24 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { )} +
    + {recordingFormErrorMessage && ( +
    + + {recordingFormErrorMessage} +
    + )} + + {/* Error List Alert */} + {formErrorList.length > 0 && ( + + )} +
    + {/* Action buttons */}
    {/* Left side - Detail & Edit actions */} diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx index 87541d8c..399468c7 100644 --- a/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx +++ b/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx @@ -98,6 +98,7 @@ const TransferToLayingFormModal = () => { 'search', { category: 'GROWING', + transfer_context: 'transfer_to_laying', } ); diff --git a/src/components/pages/production/uniformity/UniformityChart.tsx b/src/components/pages/production/uniformity/UniformityChart.tsx index 6ddf50d3..8c9eab84 100644 --- a/src/components/pages/production/uniformity/UniformityChart.tsx +++ b/src/components/pages/production/uniformity/UniformityChart.tsx @@ -5,6 +5,7 @@ import UniformityGaugeChart from '@/components/pages/production/uniformity/chart import UniformityBarChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton'; import UniformityGaugeChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton'; import { Uniformity, type ChartData } from '@/types/api/production/uniformity'; +import { Icon } from '@iconify/react'; interface UniformityChartProps { uniformityData?: Uniformity | null; @@ -101,15 +102,26 @@ const UniformityChart = ({ const shouldShowEmptyState = !isFiltered; return ( -
    +
    +
    +
    + Performance Overview{' '} + +
    +
    {shouldShowEmptyState || !uniformityData || @@ -120,26 +132,31 @@ const UniformityChart = ({ )}
    - {shouldShowEmptyState || !uniformityData || !gaugeChartData ? ( - + +
    +
    + Weekly Performance{' '} + +
    +
    + {shouldShowEmptyState || !uniformityData || !gaugeChartData ? ( -
    - ) : ( - + ) : ( - - )} + )} +
    ); }; diff --git a/src/components/pages/production/uniformity/UniformityPageWrapper.tsx b/src/components/pages/production/uniformity/UniformityPageWrapper.tsx index ac14ebb5..07f313b6 100644 --- a/src/components/pages/production/uniformity/UniformityPageWrapper.tsx +++ b/src/components/pages/production/uniformity/UniformityPageWrapper.tsx @@ -3,7 +3,7 @@ import { usePathname, useRouter } from 'next/navigation'; import Drawer from '@/components/Drawer'; import React, { ReactNode } from 'react'; -import UniformityTable from '@/components/pages/production/uniformity/UniformityTable'; +import Uniformity from '@/app/production/uniformity/page'; import { useUiStore } from '@/stores/ui/ui.store'; export default function UniformityPageWrapper({ @@ -40,8 +40,8 @@ export default function UniformityPageWrapper({ return ( <> -
    - +
    +
    [] = [ { - accessorKey: 'label', header: 'Label', cell: (props) => props.row.original.label, }, { - accessorKey: 'value', header: 'Value', cell: (props) => { const id = props.row.original.id; @@ -819,7 +816,7 @@ const UniformityTable = () => { ); return ( -
    +
    { }, cell: ({ row }) => { return ( -
    +
    { { accessorKey: 'week', header: 'Tanggal (Week)', - cell: (props) => - `${formatDate(props.row.original.applied_at, 'DD MMM YYYY')} (${props.row.original.week})`, + cell: (props) => ( + + {`${formatDate(props.row.original.applied_at, 'DD MMM YYYY')} (${props.row.original.week})`} + + ), }, { accessorKey: 'status', @@ -872,20 +872,11 @@ const UniformityTable = () => { const uniformity = props.row.original; const status = uniformity.latest_approval?.action ?? uniformity.status; - return ( -
    - - {getStatusText(status)} - -
    - ); + + const badgeColor = getStatusBadgeColor(status); + const statusText = getStatusText(status); + + return ; }, }, { @@ -900,334 +891,410 @@ const UniformityTable = () => { [] ); + // ===== CALCULATE FILTER COUNT ===== + const filterCount = useMemo(() => { + let count = 0; + + if (filterStartDate && filterEndDate) { + count += 1; + } + + if (filterLocation) { + count += 1; + } + + if (filterProjectFlock) { + count += 1; + } + + if (filterKandang) { + count += 1; + } + + return count; + }, [ + filterStartDate, + filterEndDate, + filterLocation, + filterProjectFlock, + filterKandang, + ]); + + const isFilterActive = filterCount > 0; + return ( <> -
    -
    - - - -
    - -
    - - - - - Export +
    +
    +
    + + - } - align='end' - > - - - - - + +
    + +
    + + + +
    + + + Export + +
    + + +
    + + } + > + + + +
    -
    -
    +
    + +
    +
    -
    - -
    - - + data={isResponseSuccess(uniformities) ? uniformities?.data : []} + columns={uniformityColumns} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(uniformities) ? uniformities?.meta?.page : 0} + totalItems={ + isResponseSuccess(uniformities) + ? uniformities?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} className={{ - wrapper: 'my-4 w-full relative', + containerClassName: cn('p-3 pt-0', { + 'mb-20': + isResponseSuccess(uniformities) && + uniformities?.data?.length === 0, + }), + headerColumnClassName: + 'first:pl-3 first:pr-0 xl:first:pl-3 py-3 text-nowrap', + bodyColumnClassName: + 'first:pl-3 first:pr-0 xl:first:pl-3 py-3 text-nowrap', + }} + emptyContent={} + /> + + - - data={isResponseSuccess(uniformities) ? uniformities?.data : []} - columns={uniformityColumns} - pageSize={tableFilterState.pageSize} - page={isResponseSuccess(uniformities) ? uniformities?.meta?.page : 0} - totalItems={ - isResponseSuccess(uniformities) - ? uniformities?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'mb-20': - isResponseSuccess(uniformities) && - uniformities?.data?.length === 0, - }), - tableWrapperClassName: 'overflow-x-auto min-h-full ', - tableClassName: 'font-inter w-full table-auto min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', - bodyColumnClassName: - 'px-6 py-3 last:flex last:flex-row last:justify-end', - }} - emptyContent={} - /> - - -
    - {createdUniformity ? ( - - ) : selectedRowIds.length === 1 ? ( - - ) : ( -
    - {selectedRowIds.length} data dipilih -
    - )} -
    -
    - - -
    - -
    -
    - - -
    - {selectedRowIds.length === 1 ? ( - - ) : ( -
    - {selectedRowIds.length} data dipilih -
    - )} -
    -
    - - -
    - -
    -
    - - -
    - {selectedRowIds.length === 1 ? ( - - ) : ( -
    - {selectedRowIds.length} data dipilih -
    - )} -
    -
    - - -
    - {selectedRowIds.length === 1 ? ( - - ) : ( -
    - {selectedRowIds.length} data dipilih -
    - )} -
    -
    - - {/* Filter Modal */} - -
    - {/* Modal Header */} -
    -
    - -

    Filter Data

    -
    - +
    + {createdUniformity ? ( + + ) : selectedRowIds.length === 1 ? ( + + ) : ( +
    + {selectedRowIds.length} data dipilih
    + )} +
    + - {/* Error List Alert */} - {formErrorList.length > 0 && ( -
    - -
    - )} + +
    + +
    +
    -
    -
    -
    + +
    + {selectedRowIds.length === 1 ? ( + + ) : ( +
    + {selectedRowIds.length} data dipilih +
    + )} +
    +
    + + +
    + +
    +
    + + +
    + {selectedRowIds.length === 1 ? ( + + ) : ( +
    + {selectedRowIds.length} data dipilih +
    + )} +
    +
    + + +
    + {selectedRowIds.length === 1 ? ( + + ) : ( +
    + {selectedRowIds.length} data dipilih +
    + )} +
    +
    + + {/* Filter Modal */} + +
    + {/* Modal Header */} +
    +
    + +

    Filter Data

    +
    + +
    + +
    +
    + {/* Rentang Waktu */} +
    + +
    -
    - -
    +
    @@ -1299,74 +1366,83 @@ const UniformityTable = () => { className={{ wrapper: 'w-full' }} />
    + + {formErrorList.length > 0 && ( +
    + +
    + )}
    {/* Action Buttons */} -
    +
    -
    -
    + +
    + - {/* Floating Actions Button */} - - + permissions: 'lti.production.uniformity.detail', + }, + { + action: 'DELETE', + icon: 'mdi:delete-outline', + label: 'Delete', + hidden: selectedRowIds.length !== 1, + onClick: handleDelete, + permissions: 'lti.production.uniformity.delete', + }, + ]} + approvals={[ + { + action: 'APPROVED', + icon: 'mdi:check-circle-outline', + label: 'Approve', + onClick: handleBulkApprove, + permissions: 'lti.production.uniformity.approve', + disabled: !canApproveReject, + }, + { + action: 'REJECTED', + icon: 'mdi:close-circle-outline', + label: 'Reject', + onClick: handleBulkReject, + permissions: 'lti.production.uniformity.approve', + disabled: !canApproveReject, + }, + ]} + selectedRowIds={selectedRowIds} + onClose={handleCloseFab} + /> ); }; diff --git a/src/components/pages/production/uniformity/detail/UniformityDetail.tsx b/src/components/pages/production/uniformity/detail/UniformityDetail.tsx index e8e5f1b3..11087e44 100644 --- a/src/components/pages/production/uniformity/detail/UniformityDetail.tsx +++ b/src/components/pages/production/uniformity/detail/UniformityDetail.tsx @@ -319,18 +319,19 @@ const UniformityDetail: React.FC = ({ {/* Form Section */} -
    -
    +
    {initialValues ? (
    {/* Info Umum */}
    -

    Informasi Umum

    +

    + Informasi Umum +

    data={infoUmumTableData} columns={columnsInfoUmum} @@ -345,7 +346,9 @@ const UniformityDetail: React.FC = ({ {/* Sampling and Range */} {initialValues.sampling && (
    -

    Sampling and Range

    +

    + Sampling and Range +

    data={samplingTableData} columns={columnsSampling} @@ -361,7 +364,9 @@ const UniformityDetail: React.FC = ({ {/* Result */} {initialValues.result && (
    -

    Result

    +

    + Result +

    data={resultTableData} columns={resultColumns} diff --git a/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx b/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx index 05d21535..04c43a02 100644 --- a/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx +++ b/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx @@ -10,11 +10,10 @@ import { UniformityInfoUmum, } from '@/types/api/production/uniformity'; import Table from '@/components/Table'; -import Badge from '@/components/Badge'; +import StatusBadge from '@/components/helper/StatusBadge'; import { - getWeightStatusColor, - getWeightStatusIndicatorColor, getWeightStatusText, + getWeightStatusBadgeColor, } from '@/components/pages/production/uniformity/uniformity-utils'; import { BodyWeightData } from '@/types/api/production/uniformity'; @@ -51,7 +50,7 @@ const UniformityDetailsPreview = ({ () => [ { accessorKey: 'number', - header: 'No', + header: 'Number', cell: (props) => props.row.original.number, }, { @@ -65,30 +64,12 @@ const UniformityDetailsPreview = ({ cell: (props) => { const status = props.row.original.status; return status ? ( -
    - - {getWeightStatusText(status)} - -
    + ) : ( - - Unknown - + ); }, }, @@ -102,7 +83,7 @@ const UniformityDetailsPreview = ({ - + <> +
    + + + + )}
    diff --git a/src/components/pages/production/uniformity/form/UniformityPreviewForm.tsx b/src/components/pages/production/uniformity/form/UniformityPreviewForm.tsx index 63ba34fd..314eabfd 100644 --- a/src/components/pages/production/uniformity/form/UniformityPreviewForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityPreviewForm.tsx @@ -50,7 +50,7 @@ const UniformityPreviewForm = () => { () => [ { accessorKey: 'number', - header: 'No', + header: 'Number', cell: (props) => props.row.original.number, }, { @@ -68,19 +68,18 @@ const UniformityPreviewForm = () => { {/* Form Section */} -
    -
    +
    {verifyUniformityResult ? (
    diff --git a/src/components/pages/production/uniformity/form/UniformityResultForm.tsx b/src/components/pages/production/uniformity/form/UniformityResultForm.tsx index df144c64..98db02fc 100644 --- a/src/components/pages/production/uniformity/form/UniformityResultForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityResultForm.tsx @@ -14,12 +14,11 @@ import { useRouter } from 'next/navigation'; import toast from 'react-hot-toast'; import { UniformityApi } from '@/services/api/uniformity'; import { isResponseError } from '@/lib/api-helper'; -import Badge from '@/components/Badge'; +import StatusBadge from '@/components/helper/StatusBadge'; import { formatNumber } from '@/lib/helper'; import { - getWeightStatusColor, - getWeightStatusIndicatorColor, getWeightStatusText, + getWeightStatusBadgeColor, } from '@/components/pages/production/uniformity/uniformity-utils'; import { DetailOptionType } from '@/types/api/production/uniformity'; import { @@ -190,7 +189,7 @@ const UniformityResultForm = () => { () => [ { accessorKey: 'number', - header: 'No', + header: 'Number', cell: (props) => props.row.original.number, }, { @@ -204,30 +203,12 @@ const UniformityResultForm = () => { cell: (props) => { const status = props.row.original.status; return status ? ( -
    - - {getWeightStatusText(status)} - -
    + ) : ( - - Unknown - + ); }, }, @@ -241,23 +222,24 @@ const UniformityResultForm = () => { {/* Form Section */} -
    -
    +
    {verifyUniformityResult ? (
    -

    Sampling and Range

    +

    + Sampling and Range +

    data={samplingTableData} columns={columnsSampling} @@ -270,7 +252,9 @@ const UniformityResultForm = () => {
    -

    Result

    +

    + Result +

    data={resultTableData} columns={resultColumns} diff --git a/src/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton.tsx b/src/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton.tsx index 123a456a..3ce40a3a 100644 --- a/src/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton.tsx +++ b/src/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton.tsx @@ -1,4 +1,3 @@ -import Button from '@/components/Button'; import { Icon } from '@iconify/react'; const LeftLegend = () => { @@ -45,11 +44,11 @@ const ChartArea = () => { ))}
    -
    +
    {ranges.map((range) => (
    ))}
    @@ -65,28 +64,38 @@ const ChartArea = () => { const EmptyState = () => { return ( - <> -
    -
    - +
    +
    + {/* Filter icon */} +
    +
    +
    + +
    +
    - + + {/* Empty state text */} +

    No Filters Selected - - +

    +

    Please choose filters to narrow down your results and make your search easier. - +

    - +
    ); }; const UniformityBarChartSkeleton = () => { return ( -
    +
    diff --git a/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx b/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx index 17ed7ee9..2ff7155b 100644 --- a/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx +++ b/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx @@ -1,4 +1,3 @@ -import Button from '@/components/Button'; import { Icon } from '@iconify/react'; import React from 'react'; import { Cell, Pie, PieChart, ResponsiveContainer } from 'recharts'; @@ -55,22 +54,29 @@ const UniformityGaugeChartSkeleton: React.FC< -
    -
    - +
    + {/* Filter icon */} +
    +
    +
    + +
    +
    - + + {/* Empty state text */} +

    No Filters Selected - - +

    +

    Please choose filters to narrow down your results and make your search easier. - +

    diff --git a/src/components/pages/production/uniformity/skeleton/UniformityTableSkeleton.tsx b/src/components/pages/production/uniformity/skeleton/UniformityTableSkeleton.tsx index b90048a1..e1bbd4a9 100644 --- a/src/components/pages/production/uniformity/skeleton/UniformityTableSkeleton.tsx +++ b/src/components/pages/production/uniformity/skeleton/UniformityTableSkeleton.tsx @@ -1,24 +1,30 @@ -import Button from '@/components/Button'; import { Icon } from '@iconify/react'; const UniformityTableSkeleton = () => { return ( -
    -
    - +
    + {/* Document icon */} +
    +
    +
    + +
    +
    - + + {/* Empty state text */} +

    No Data Available - - +

    +

    There is no uniformity data displayed. Enter uniformity check data to get started. - +

    ); }; diff --git a/src/components/pages/production/uniformity/uniformity-utils.ts b/src/components/pages/production/uniformity/uniformity-utils.ts index 340bb555..1b513de4 100644 --- a/src/components/pages/production/uniformity/uniformity-utils.ts +++ b/src/components/pages/production/uniformity/uniformity-utils.ts @@ -25,6 +25,20 @@ export const getWeightStatusText = (status: string): string => { return weightStatusTextMap[status] || status; }; +export const weightStatusBadgeColorMap: Record< + string, + 'success' | 'error' | 'neutral' | 'info' +> = { + ideal: 'success', + outside: 'error', +}; + +export const getWeightStatusBadgeColor = ( + status: string +): 'success' | 'error' | 'neutral' | 'info' => { + return weightStatusBadgeColorMap[status] || 'neutral'; +}; + export const statusColorMap: Record = { APPROVED: 'bg-[#00D39033]', Disetujui: 'bg-[#00D39033]', @@ -63,3 +77,29 @@ export const getStatusIndicatorColor = (status: string): string => { export const getStatusText = (status: string): string => { return statusTextMap[status] || status; }; + +export const statusBadgeColorMap: Record< + string, + 'success' | 'error' | 'neutral' | 'info' +> = { + APPROVED: 'success', + Disetujui: 'success', + approved: 'success', + disetujui: 'success', + REJECTED: 'error', + Ditolak: 'error', + rejected: 'error', + ditolak: 'error', + CREATED: 'neutral', + Pengajuan: 'neutral', + created: 'neutral', + pengajuan: 'neutral', + PENDING: 'neutral', + pending: 'neutral', +}; + +export const getStatusBadgeColor = ( + status: string +): 'success' | 'error' | 'neutral' | 'info' => { + return statusBadgeColorMap[status] || 'neutral'; +}; diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index de27a169..c7b196a2 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -370,25 +370,6 @@ const PurchaseOrderAcceptApprovalForm = ({ ? 'Konfirmasi Penerimaan Produk' : 'Edit Penerimaan Produk'} - {purchaseOrderFormErrorMessage && ( -
    - - {purchaseOrderFormErrorMessage} -
    - )} - - {/* Error List Alert */} - {formErrorList.length > 0 && ( - setFormErrorList([])} - /> - )} -
    @@ -709,6 +690,27 @@ const PurchaseOrderAcceptApprovalForm = ({ /> +
    + {purchaseOrderFormErrorMessage && ( +
    + + {purchaseOrderFormErrorMessage} +
    + )} + + {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} +
    + {/* Action buttons */}
    diff --git a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx index a232347d..000c212b 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx @@ -688,25 +688,6 @@ const PurchaseOrderStaffApprovalForm = ({ ? 'Konfirmasi Item Pembelian' : 'Edit Item Pembelian'} - {purchaseOrderFormErrorMessage && ( -
    - - {purchaseOrderFormErrorMessage} -
    - )} - - {/* Error List Alert */} - {formErrorList.length > 0 && ( - setFormErrorList([])} - /> - )} -
    {groupedPurchaseItems.length > 0 ? (
    @@ -1186,6 +1167,27 @@ const PurchaseOrderStaffApprovalForm = ({ />
    +
    + {purchaseOrderFormErrorMessage && ( +
    + + {purchaseOrderFormErrorMessage} +
    + )} + + {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} +
    + {/* Action buttons */}
    diff --git a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx index 83d9e2d7..90f79e6d 100644 --- a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx +++ b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx @@ -494,25 +494,6 @@ const PurchaseRequestForm = ({ onReset={formik.handleReset} className='w-full mt-8 flex flex-col gap-6' > - {purchaseRequestFormErrorMessage && ( -
    - - {purchaseRequestFormErrorMessage} -
    - )} - - {/* Error List Alert */} - {formErrorList.length > 0 && ( - setFormErrorList([])} - /> - )} - {/* Basic Info Card */} +
    + {purchaseRequestFormErrorMessage && ( +
    + + {purchaseRequestFormErrorMessage} +
    + )} + + {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} +
    + {/* Action buttons */}
    {type !== 'detail' && ( diff --git a/src/components/pages/report/DailyMarketingsTable.tsx b/src/components/pages/report/DailyMarketingsTable.tsx index 67702035..2ed5a9cb 100644 --- a/src/components/pages/report/DailyMarketingsTable.tsx +++ b/src/components/pages/report/DailyMarketingsTable.tsx @@ -10,7 +10,13 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Card from '@/components/Card'; import Collapse from '@/components/Collapse'; -import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import { + cn, + formatCurrency, + formatDate, + formatNumber, + formatVechicleNumber, +} from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { DailyMarketingRow } from '@/types/api/report/marketing'; import { MarketingReportApi } from '@/services/api/report/marketing-report'; @@ -94,7 +100,9 @@ const DailyMarketingsTable = ({ accessorKey: 'vehicle_number', header: 'No. Polisi', cell: (props) => ( - {props.row.original.vehicle_number} + + {formatVechicleNumber(props.row.original.vehicle_number)} + ), }, { diff --git a/src/components/pages/report/finance/FinanceTabs.tsx b/src/components/pages/report/finance/FinanceTabs.tsx index 58d1e78b..ffb0d3f1 100644 --- a/src/components/pages/report/finance/FinanceTabs.tsx +++ b/src/components/pages/report/finance/FinanceTabs.tsx @@ -1,28 +1,43 @@ 'use client'; +import { useState } from 'react'; import Tabs from '@/components/Tabs'; import CustomerPaymentTab from '@/components/pages/report/finance/tab/CustomerPaymentTab'; import DebtSupplierTab from '@/components/pages/report/finance/tab/DebtSupplierTab'; +import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store'; const FinanceTabs = () => { + const [activeTabId, setActiveTabId] = useState('1'); + const tabActions = useFinanceTabStore((state) => state.tabActions); + const tabs = [ { id: '1', label: 'Rekapitulasi Hutang Ke Supplier', - - content: , + content: , }, { id: '2', label: 'Kontrol Pembayaran Customer', - - content: , + content: , }, ]; return ( -
    - +
    +
    ); }; diff --git a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx index 869430b0..edcd360f 100644 --- a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx +++ b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx @@ -281,16 +281,16 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { No - + No. PR - + No. PO - + Tgl Terima/Bayar - + Tgl PO @@ -320,7 +320,12 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { Status - + No. Perjalanan @@ -330,16 +335,16 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { {/* NO */} - + {/* No. PR */} - + {/* No. PO */} - + {/* Tgl Terima/Bayar */} - + {/* Tgl PO */} @@ -381,8 +386,13 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { {/* Status */} - - {/* No. Perjalanan */} + + @@ -400,13 +410,13 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { {index + 1} - + {item.pr_number || '-'} - + {item.po_number || '-'} - + {item.received_date ? item.received_date != '-' @@ -415,7 +425,7 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { : '-'} - + {item.po_date ? item.po_date != '-' @@ -526,7 +536,12 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { - )} - + {item.travel_number || '-'} @@ -538,18 +553,18 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { Total - - - - - - + + + + + + {formatNumber(supplierReport.total.aging)} Hari diff --git a/src/components/pages/report/finance/skeleton/CustomerSupplierSkeleton.tsx b/src/components/pages/report/finance/skeleton/CustomerSupplierSkeleton.tsx new file mode 100644 index 00000000..8ab0ffd3 --- /dev/null +++ b/src/components/pages/report/finance/skeleton/CustomerSupplierSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { CustomerPaymentRow } from '@/types/api/report/customer-payment'; +import { ColumnDef } from '@tanstack/react-table'; + +const CustomerSupplierSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
    +
    +
    + +
    + + ); +}; + +export default CustomerSupplierSkeleton; diff --git a/src/components/pages/report/finance/skeleton/DebtSupplierSkeleton.tsx b/src/components/pages/report/finance/skeleton/DebtSupplierSkeleton.tsx new file mode 100644 index 00000000..b9397f8f --- /dev/null +++ b/src/components/pages/report/finance/skeleton/DebtSupplierSkeleton.tsx @@ -0,0 +1,38 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { DebtRow } from '@/types/api/report/debt-supplier'; +import { Icon } from '@iconify/react'; +import { ColumnDef } from '@tanstack/react-table'; + +const DebtSupplierSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
    +
    +
    + +
    + + ); +}; + +export default DebtSupplierSkeleton; diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index dc1705ed..2987455a 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useCallback } from 'react'; +import { useState, useMemo, useCallback, useEffect } from 'react'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Card from '@/components/Card'; @@ -11,9 +11,10 @@ import { FinanceApi } from '@/services/api/report/finance-report'; import { UserApi } from '@/services/api/user'; import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; -import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import { formatCurrency, formatDate, formatNumber, cn } from '@/lib/helper'; import { CustomerPaymentReport, + CustomerPaymentRow, CustomerPaymentSummary, } from '@/types/api/report/customer-payment'; import { isResponseSuccess } from '@/lib/api-helper'; @@ -26,8 +27,14 @@ import { useModal } from '@/components/Modal'; import toast from 'react-hot-toast'; import { generateCustomerPaymentExcel } from '@/components/pages/report/finance/export/CustomerPaymentExportXLSX'; import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF'; +import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store'; +import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton'; -const CustomerPaymentTab = () => { +interface CustomerPaymentTabProps { + tabId: string; +} + +const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // ===== STATE MANAGEMENT ===== const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); @@ -111,6 +118,10 @@ const CustomerPaymentTab = () => { }; // ===== FILTER HANDLERS ===== + const handleFilterModalOpen = useCallback(() => { + filterModal.openModal(); + }, [filterModal]); + const handleResetFilters = useCallback(() => { setIsSubmitted(false); setFilterCustomer([]); @@ -298,6 +309,92 @@ const CustomerPaymentTab = () => { } }, [customerPaymentExport]); + // ===== REGISTER TAB ACTIONS TO STORE ===== + const setTabActions = useFinanceTabStore((state) => state.setTabActions); + const clearTabActions = useFinanceTabStore((state) => state.clearTabActions); + + useEffect(() => { + setTabActions( + tabId, +
    + + + + + Export +
    + +
    + + } + align='end' + className={{ + content: + 'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg', + }} + > + + + + +
    +
    + ); + }, [ + tabId, + hasFilters, + activeFiltersCount, + isAnyExportLoading, + handleExportExcel, + handleExportPdf, + filterModal.open, + setTabActions, + ]); + + // Cleanup on unmount + useEffect(() => { + return () => { + clearTabActions(tabId); + }; + }, [tabId, clearTabActions]); + const getTableColumns = ( summary: CustomerPaymentSummary ): ColumnDef[] => { @@ -552,192 +649,40 @@ const CustomerPaymentTab = () => { }; return ( -
    - -
    - - - - - Export - - } - align='end' - > - - - - - -
    - - {/* Filter Modal */} - -
    - {/* Modal Header */} -
    -
    - -

    Filter Data

    -
    - -
    -
    -
    -
    - { - setFilterStartDate(e.target.value); - }} - className={{ wrapper: 'w-full' }} - /> -
    - -
    - { - setFilterEndDate(e.target.value); - }} - className={{ wrapper: 'w-full' }} - /> -
    -
    - -
    - { - setFilterCustomer( - Array.isArray(val) ? val : val ? [val] : [] - ); - }} - onInputChange={setCustomerInputValue} - isLoading={isLoadingCustomers} - isClearable - onMenuScrollToBottom={loadMoreCustomers} - className={{ wrapper: 'w-full' }} - /> -
    - - {/* TODO: Uncomment when BE is ready */} - {/*
    - { - setFilterSales(Array.isArray(val) ? val : val ? [val] : []); - }} - onInputChange={setSalesInputValue} - isLoading={isLoadingSales} - isClearable - onMenuScrollToBottom={loadMoreSales} - className={{ wrapper: 'w-full' }} - /> -
    */} - - {/* TODO: Uncomment when BE is ready */} - {/*
    - -
    */} -
    - - {/* Action Buttons */} -
    - - -
    -
    -
    - + <> +
    {!isSubmitted ? ( -
    - Silakan klik tombol Filter untuk mengatur filter dan menampilkan - data. -
    + + } + title='No Filters Selected' + subtitle='Please choose filters to narrow down your results and make your search easier.' + /> ) : isLoading ? (
    ) : data.length === 0 ? ( -
    - Tidak ada data yang dapat ditampilkan... -
    + + } + title='Data Not Yet Available' + subtitle='Please change your filters to get the data.' + /> ) : ( data.map((customerReport) => { const summary = customerReport.summary || { @@ -757,15 +702,17 @@ const CustomerPaymentTab = () => { title={customerReport.customer.name} subtitle={`(${customerReport.customer.address})`} className={{ - wrapper: 'w-full rounded-2xl', + wrapper: 'w-full rounded-lg border-none', body: 'p-0', title: - 'py-1.5 px-3 bg-primary text-white text-lg font-normal', + 'px-2 py-1.5 font-normal text-sm bg-primary text-white', subtitle: - 'px-3 pb-1 bg-primary text-white text-sm font-normal', + 'px-2 pb-1.5 bg-primary text-white text-xs font-normal', + collapsible: 'rounded-lg', }} variant='bordered' collapsible={true} + defaultCollapsed={true} >
    { renderFooter={customerReport.rows.length > 0} className={{ containerClassName: 'w-full mb-0!', - tableWrapperClassName: 'overflow-x-auto', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', tableClassName: 'w-full table-auto text-sm', headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', headerColumnClassName: @@ -799,8 +747,8 @@ const CustomerPaymentTab = () => { if (row.index === 0) { return ( - {selectedEmployees.map((emp) => ( + {sortedSelectedEmployees.map((emp) => ( {activity.employees.length > 0 && - activity.employees[0].note ? ( + activity.employees[ + activity.employees.length - 1 + ].note ? (

    - {activity.employees[0].note} + { + activity.employees[ + activity.employees.length - 1 + ].note + }

    ) : (

    diff --git a/src/figma-make/components/pages/master-data/configuration/MasterConfigurationContent.tsx b/src/figma-make/components/pages/master-data/configuration/MasterConfigurationContent.tsx index 9fa75c33..33ad2608 100644 --- a/src/figma-make/components/pages/master-data/configuration/MasterConfigurationContent.tsx +++ b/src/figma-make/components/pages/master-data/configuration/MasterConfigurationContent.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Plus, MoreVertical, Pencil, Trash2 } from 'lucide-react'; import { Card, CardContent } from '@/figma-make/components/base/card'; import { Button } from '@/figma-make/components/base/button'; @@ -404,7 +404,22 @@ export function MasterConfigurationContent() { {/* Add/Edit Modal */} -

    + { + if (!open) { + setIsFormInvalid(false); + setConfigurationForm({ + id: 0, + date: '', + percentage_threshold_bad: '', + percentage_threshold_enough: '', + }); + } + + setShowModal(open); + }} + > diff --git a/src/stores/finance-tab/finance-tab.store.ts b/src/stores/finance-tab/finance-tab.store.ts new file mode 100644 index 00000000..9b5cf096 --- /dev/null +++ b/src/stores/finance-tab/finance-tab.store.ts @@ -0,0 +1,51 @@ +'use client'; + +import { ReactNode } from 'react'; +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; + +export type FinanceTabActionsSlice = { + // State - actions per tab ID + tabActions: Record; + + // Actions + setTabActions: (tabId: string, actions: ReactNode) => void; + clearTabActions: (tabId: string) => void; + clearAllTabActions: () => void; +}; + +export const useFinanceTabStore = create()( + devtools( + (set) => ({ + tabActions: {}, + + setTabActions: (tabId, actions) => + set( + (state) => ({ + tabActions: { + ...state.tabActions, + [tabId]: actions, + }, + }), + false, + 'setTabActions' + ), + + clearTabActions: (tabId) => + set( + (state) => { + const { [tabId]: _, ...rest } = state.tabActions; + return { tabActions: rest }; + }, + false, + 'clearTabActions' + ), + + clearAllTabActions: () => + set({ tabActions: {} }, false, 'clearAllTabActions'), + }), + { + name: 'FinanceTabStore', + } + ) +); diff --git a/src/stores/ui/slices/navbar.slice.ts b/src/stores/ui/slices/navbar.slice.ts new file mode 100644 index 00000000..2db6f547 --- /dev/null +++ b/src/stores/ui/slices/navbar.slice.ts @@ -0,0 +1,22 @@ +'use client'; + +import { ReactNode } from 'react'; +import { StateCreator } from 'zustand'; +import { UIStore } from '@/types/stores'; + +type NavbarActionsSlice = { + navbarActions: ReactNode | null; + setNavbarActions: (actions: ReactNode) => void; + clearNavbarActions: () => void; +}; + +export const createNavbarActionsSlice: StateCreator< + UIStore, + [], + [], + NavbarActionsSlice +> = (set) => ({ + navbarActions: null, + setNavbarActions: (actions) => set({ navbarActions: actions }), + clearNavbarActions: () => set({ navbarActions: null }), +}); diff --git a/src/stores/ui/ui.store.ts b/src/stores/ui/ui.store.ts index 05adbb9b..46c70283 100644 --- a/src/stores/ui/ui.store.ts +++ b/src/stores/ui/ui.store.ts @@ -7,6 +7,7 @@ import { UIStore } from '@/types/stores'; import { createMainUiSlice } from '@/stores/ui/slices/main.slice'; import { createDrawerUISlice } from '@/stores/ui/slices/drawer.slice'; import { createTableUISlice } from '@/stores/ui/slices/table.slice'; +import { createNavbarActionsSlice } from '@/stores/ui/slices/navbar.slice'; export const useUiStore = create()( devtools( @@ -15,6 +16,7 @@ export const useUiStore = create()( ...createMainUiSlice(...args), ...createDrawerUISlice(...args), ...createTableUISlice(...args), + ...createNavbarActionsSlice(...args), }), { name: 'ui-cache', diff --git a/src/types/api/inventory/movement.d.ts b/src/types/api/inventory/movement.d.ts index 10a62e5b..e84dead9 100644 --- a/src/types/api/inventory/movement.d.ts +++ b/src/types/api/inventory/movement.d.ts @@ -68,11 +68,11 @@ export type CreateMovementPayloadData = { product_qty: number; }[]; deliveries: { - delivery_cost: number; - delivery_cost_per_item: number; + delivery_cost?: number; + delivery_cost_per_item?: number; document_index?: number; - driver_name: string; - vehicle_plate: string; + driver_name?: string; + vehicle_plate?: string; supplier_id?: number | null; products: { product_id: number; diff --git a/src/types/stores.d.ts b/src/types/stores.d.ts index 5b0be6f3..47d2c1fd 100644 --- a/src/types/stores.d.ts +++ b/src/types/stores.d.ts @@ -32,7 +32,17 @@ type TableUISlice = { resetSearchValue: () => void; }; -export type UIStore = MainUiSlice & DrawerUISlice & TableUISlice; +// Navbar Actions Slice +type NavbarActionsSlice = { + navbarActions: ReactNode | null; + setNavbarActions: (actions: ReactNode) => void; + clearNavbarActions: () => void; +}; + +export type UIStore = MainUiSlice & + DrawerUISlice & + TableUISlice & + NavbarActionsSlice; type ProductionStandardFormSlice = { formData: {
    { ); }) )} - - + + + {/* Filter Modal */} + + {/* Modal Header */} +
    +
    + +

    Filter Data

    +
    + +
    +
    +
    + +
    + { + setFilterStartDate(e.target.value); + }} + className={{ wrapper: 'w-full' }} + isNestedModal + /> +
    + + { + setFilterEndDate(e.target.value); + }} + className={{ wrapper: 'w-full' }} + isNestedModal + /> +
    +
    + + { + setFilterCustomer(Array.isArray(val) ? val : val ? [val] : []); + }} + onInputChange={setCustomerInputValue} + isLoading={isLoadingCustomers} + isClearable + onMenuScrollToBottom={loadMoreCustomers} + className={{ wrapper: 'w-full' }} + /> + + {/* TODO: Uncomment when BE is ready */} + {/*
    + { + setFilterSales(Array.isArray(val) ? val : val ? [val] : []); + }} + onInputChange={setSalesInputValue} + isLoading={isLoadingSales} + isClearable + onMenuScrollToBottom={loadMoreSales} + className={{ wrapper: 'w-full' }} + /> +
    */} + + {/* TODO: Uncomment when BE is ready */} + {/*
    + +
    */} + + {/* Action Buttons */} +
    +
    + + +
    +
    + ); }; diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index c5065d29..646cfb15 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -2,10 +2,7 @@ import Button from '@/components/Button'; import Card from '@/components/Card'; import Dropdown from '@/components/Dropdown'; import DateInput from '@/components/input/DateInput'; -import SelectInput, { - OptionType, - useSelect, -} from '@/components/input/SelectInput'; +import { OptionType, useSelect } from '@/components/input/SelectInput'; import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; import Modal, { useModal } from '@/components/Modal'; @@ -22,7 +19,7 @@ import { generateDebtSupplierExcel } from '@/components/pages/report/finance/exp import { generateDebtSupplierPDF } from '@/components/pages/report/finance/export/DebtSupllierExportPDF'; import { Icon } from '@iconify/react'; import { ColumnDef } from '@tanstack/react-table'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import useSWR from 'swr'; import { DebtSupplierApi } from '@/services/api/report/debt-supplier'; @@ -32,11 +29,14 @@ import { DebtSupplierFilterType, } from '@/components/pages/report/finance/filter/DebtSupplierFilter'; import ButtonFilter from '@/components/helper/ButtonFilter'; -import Badge from '@/components/Badge'; import { Color } from '@/types/theme'; import { Supplier } from '@/types/api/master-data/supplier'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; +import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store'; +import StatusBadge from '@/components/helper/StatusBadge'; +import DebtSupplierSkeleton from '@/components/pages/report/finance/skeleton/DebtSupplierSkeleton'; +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; const dueStatus: Record = { 'Sudah Jatuh Tempo': 'error', @@ -60,22 +60,14 @@ const getPillBadge = ( ? dueStatus[statusText] || 'neutral' : paymentStatus[statusText] || 'neutral'; - return ( - - {statusText} - - ); + return ; }; -const DebtSupplierTab = () => { +interface DebtSupplierTabProps { + tabId: string; +} + +const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { // ===== STATE MANAGEMENT ===== const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); @@ -271,7 +263,78 @@ const DebtSupplierTab = () => { } }, [debtSupplierExport]); - const getTableColumns = (supplier: DebtSupplier): ColumnDef[] => [ + // ===== REGISTER TAB ACTIONS TO STORE ===== + const setTabActions = useFinanceTabStore((state) => state.setTabActions); + const clearTabActions = useFinanceTabStore((state) => state.clearTabActions); + + useEffect(() => { + setTabActions( + tabId, +
    + + + + + Export +
    + +
    + + } + align='end' + className={{ + content: + 'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg', + }} + > + + + + +
    +
    + ); + }, [ + tabId, + formik.values, + isAnyExportLoading, + handleExportExcel, + handleExportPdf, + setTabActions, + ]); + + // Cleanup on unmount + useEffect(() => { + return () => { + clearTabActions(tabId); + }; + }, [tabId, clearTabActions]); + + const getTableColumns = (supplier?: DebtSupplier): ColumnDef[] => [ { id: 'no', header: 'No', @@ -337,8 +400,10 @@ const DebtSupplierTab = () => { return
    {formatNumber(value)} Hari
    ; }, footer: () => { - const value = supplier.total.aging; - return
    {formatNumber(value)} Hari
    ; + const value = supplier?.total.aging; + return ( +
    {formatNumber(value || 0)} Hari
    + ); }, }, { @@ -399,10 +464,10 @@ const DebtSupplierTab = () => { ); }, footer: () => { - const value = supplier.total.total_price; + const value = supplier?.total.total_price; return ( -
    - {formatCurrency(value)} +
    + {formatCurrency(value || 0)}
    ); }, @@ -421,10 +486,10 @@ const DebtSupplierTab = () => { ); }, footer: () => { - const value = supplier.total.payment_price; + const value = supplier?.total.payment_price; return ( -
    - {formatCurrency(value)} +
    + {formatCurrency(value || 0)}
    ); }, @@ -443,10 +508,10 @@ const DebtSupplierTab = () => { ); }, footer: () => { - const value = supplier.total.debt_price; + const value = supplier?.total.debt_price; return ( -
    - {formatCurrency(value)} +
    + {formatCurrency(value || 0)}
    ); }, @@ -478,52 +543,39 @@ const DebtSupplierTab = () => { ]; return ( <> -
    - -
    - - - - - Export - - } - align='end' - > - - - - - -
    -
    - +
    {!isSubmitted ? ( -
    - Silakan klik tombol Filter untuk mengatur filter dan menampilkan - data. -
    + + } + title='No Filters Selected' + subtitle='Please choose filters to narrow down your results and make your search easier.' + /> ) : isLoading ? (
    ) : data.length === 0 ? ( -
    - Tidak ada data yang dapat ditampilkan... -
    + + } + title='Data Not Yet Available' + subtitle='Please change your filters to get the data.' + /> ) : ( data.map((supplierReport) => { return ( @@ -531,10 +583,11 @@ const DebtSupplierTab = () => { key={supplierReport.supplier.id} title={supplierReport.supplier.name} className={{ - wrapper: 'w-full !rounded-lg', - body: 'p-0 rounded-lg', + wrapper: 'w-full rounded-lg border-none', + body: 'p-0', title: - 'ps-2 pt-1 pb-1 font-normal text-md bg-primary text-white', + 'px-2 py-1.5 font-normal text-sm bg-primary text-white', + collapsible: 'rounded-lg', }} variant='bordered' collapsible={true} @@ -551,8 +604,9 @@ const DebtSupplierTab = () => { pageSize={supplierReport.rows.length + 1} renderFooter={supplierReport.rows.length > 0} className={{ - containerClassName: 'w-full', - tableWrapperClassName: 'overflow-x-auto', + containerClassName: 'w-full mb-0', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', headerColumnClassName: cn( TABLE_DEFAULT_STYLING.headerColumnClassName, 'whitespace-nowrap' @@ -617,33 +671,34 @@ const DebtSupplierTab = () => { ref={filterModal.ref} className={{ modal: 'p-0', - modalBox: 'p-0 rounded-2xl xl:max-w-4/12 max-w-sm', + modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm', }} > -
    + {/* Modal Header */} -
    +
    -

    Filter Data

    +

    Filter Data

    -
    -
    -
    + + {/* Modal Body */} +
    +
    + +
    { @@ -654,12 +709,10 @@ const DebtSupplierTab = () => { formik.touched.startDate && !!formik.errors.startDate } errorMessage={formik.errors.startDate} + isNestedModal /> -
    - -
    +
    { @@ -668,6 +721,7 @@ const DebtSupplierTab = () => { className={{ wrapper: 'w-full' }} isError={formik.touched.endDate && !!formik.errors.endDate} errorMessage={formik.errors.endDate} + isNestedModal />
    @@ -730,15 +784,19 @@ const DebtSupplierTab = () => {
    {/* Action Buttons */} -
    +
    -
    diff --git a/src/config/constant.ts b/src/config/constant.ts index fff9fa92..120b6b6a 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -5,6 +5,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ text: 'Dashboard', link: '/dashboard', icon: 'heroicons-outline:chart-bar-square', + permission: ['lti.dashboard.list'], }, { text: 'Daily Checklist', @@ -114,11 +115,13 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ text: 'Penjualan', link: '/marketing', icon: 'heroicons-outline:currency-dollar', + permission: ['lti.marketing.delivery_order.list'], }, { text: 'Keuangan', link: '/finance', icon: 'heroicons-outline:banknotes', + permission: ['lti.finance.transactions.list'], }, { text: 'Biaya', @@ -136,26 +139,46 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ text: 'Laporan', link: '/report', icon: 'mdi:chart-box-outline', + permission: [ + 'lti.repport.debtsupplier.list', + 'lti.repport.customerpayment.list', + 'lti.repport.purchasesupplier.list', + 'lti.repport.expense.list', + 'lti.repport.delivery.list', + 'lti.repport.gethppperkandang.list', + 'lti.repport.production_result.list', + ], submenu: [ { text: 'Keuangan', link: '/report/finance', + permission: [ + 'lti.repport.debtsupplier.list', + 'lti.repport.customerpayment.list', + ], }, { text: 'Logistik & Persediaan', link: '/report/logistic-stock', + permission: ['lti.repport.purchasesupplier.list'], }, { text: 'Biaya Operasional', link: '/report/expense', + permission: ['lti.repport.expense.list'], }, { text: 'Penjualan', link: '/report/marketing', + permission: [ + 'lti.repport.delivery.list', + 'lti.repport.gethppperkandang.list', + ], }, { text: 'Hasil Produksi', link: '/report/production-result', + permission: ['lti.repport.production_result.list'], }, ], }, @@ -204,6 +227,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ 'lti.master.suppliers.list', 'lti.master.uoms.list', 'lti.master.warehouses.list', + 'lti.master.production_standards.list', ], submenu: [ { @@ -274,6 +298,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ { text: 'Standar Produksi', link: '/master-data/production-standard', + permission: ['lti.master.production_standards.list'], }, ], }, diff --git a/src/config/route-permission.ts b/src/config/route-permission.ts index 44f3728e..20ee5292 100644 --- a/src/config/route-permission.ts +++ b/src/config/route-permission.ts @@ -116,7 +116,10 @@ export const ROUTE_PERMISSIONS: Record = { // Report '/report/logistic-stock/': ['lti.repport.purchasesupplier.list'], '/report/expense/': ['lti.repport.expense.list'], - '/report/marketing/': ['lti.repport.delivery.list'], + '/report/marketing/': [ + 'lti.repport.delivery.list', + 'lti.repport.gethppperkandang.list', + ], '/report/production-result/': ['lti.repport.production_result.list'], '/report/finance/': [ 'lti.repport.finance.list', diff --git a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx index 37430354..266b8740 100644 --- a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx +++ b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx @@ -127,6 +127,10 @@ export function DailyChecklistContent() { { id: number; name: string }[] >([]); + const sortedSelectedEmployees = selectedEmployees.toSorted((a, b) => + a.name.localeCompare(b.name) + ); + const [dailyChecklistId, setDailyChecklistId] = useState(null); const [checklistStatus, setChecklistStatus] = useState('DRAFT'); // const [isEditMode, setIsEditMode] = useState(false); @@ -486,6 +490,11 @@ export function DailyChecklistContent() { return; } + if (!tempSelectedPhaseIds.length) { + toast.error('Pilih minimal satu fase'); + return; + } + try { // Insert new phase links const setDailyChecklistPhaseRes = @@ -535,14 +544,6 @@ export function DailyChecklistContent() { } }; - const toggleSelectAllAbk = () => { - if (tempSelectedEmployees.length === employees.length) { - setTempSelectedEmployees([]); - } else { - setTempSelectedEmployees([...employees]); - } - }; - const applyAbkSelection = async () => { if (!dailyChecklistId) { toast.error('Checklist belum tersedia'); @@ -853,10 +854,34 @@ export function DailyChecklistContent() { ); const isAllAbkSelected = - tempSelectedEmployees.length === employees.length && employees.length > 0; + tempSelectedEmployees.length === filteredEmployees.length && + filteredEmployees.length > 0 && + tempSelectedEmployees.every((tempSelectedEmployee) => { + return ( + filteredEmployees.findIndex( + (filteredEmployee) => filteredEmployee.id === tempSelectedEmployee.id + ) >= 0 + ); + }); const isAllPhasesSelected = - tempSelectedPhaseIds.length === availablePhases.length && - availablePhases.length > 0; + tempSelectedPhaseIds.length === filteredPhases.length && + filteredPhases.length > 0 && + tempSelectedPhaseIds.every((tempSelectedPhaseId) => { + return ( + filteredPhases.findIndex( + (filteredPhase) => + String(filteredPhase.id) === String(tempSelectedPhaseId) + ) >= 0 + ); + }); + + const toggleSelectAllAbk = () => { + if (isAllAbkSelected) { + setTempSelectedEmployees([]); + } else { + setTempSelectedEmployees([...filteredEmployees]); + } + }; // Group activities by PHASE → TIME_TYPE → ACTIVITIES const groupActivitiesByPhase = () => { @@ -1130,7 +1155,7 @@ export function DailyChecklistContent() {
    Aktivitas + a.name.localeCompare(b.name, undefined, { + sensitivity: 'base', + }) + ); + + console.log(activities); + activities.forEach((activity, index) => { const taskId = taskIdsByPhaseActivityId[activity.id]; @@ -1244,7 +1277,7 @@ export function DailyChecklistContent() {

    )} - {selectedEmployees.map((emp) => ( + {sortedSelectedEmployees.map((emp) => (
    )} @@ -1519,14 +1554,14 @@ export function DailyChecklistContent() { setTempSelectedPhaseIds([]); } else { setTempSelectedPhaseIds( - availablePhases.map((p) => String(p.id)) + filteredPhases.map((p) => String(p.id)) ); } }} className='checkbox-clean' /> - Pilih Semua ({availablePhases.length} Fase) + Pilih Semua ({filteredPhases.length} Fase) @@ -1621,7 +1656,7 @@ export function DailyChecklistContent() { /> - {employees.length > 0 && ( + {filteredEmployees.length > 0 && (
    diff --git a/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx b/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx index 0b3ece27..d8723df0 100644 --- a/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx +++ b/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx @@ -275,6 +275,13 @@ export function DetailDailyChecklistContent() { ]) ).values() ); + + uniqueEmployees.sort((a, b) => + a.name.localeCompare(b.name, undefined, { + sensitivity: 'base', + }) + ); + setEmployees(uniqueEmployees); // Group data by Phase → Time Type → Activity @@ -779,11 +786,23 @@ export function DetailDailyChecklistContent() { } // ACTIVITY rows - timeGroup.activities.forEach((activity, index) => { + const activities = timeGroup.activities; + + activities.sort((a, b) => + a.name.localeCompare(b.name, undefined, { + sensitivity: 'base', + }) + ); + + activities.forEach((activity, index) => { const indentClass = hasMultipleTimeTypes ? 'pl-12' : 'pl-8'; + console.log({ + activity, + }); + rows.push(