diff --git a/src/app/production/project-flock/layout.tsx b/src/app/production/project-flock/layout.tsx
index 698064cf..b74ef612 100644
--- a/src/app/production/project-flock/layout.tsx
+++ b/src/app/production/project-flock/layout.tsx
@@ -52,6 +52,7 @@ export default function ProjectFlockLayout({
closeOnBackdropClick={isDetail ? true : false}
onBackdropClick={handleBackdropClick}
variant='right'
+ zIndex='99999'
sidebarContent={isOpen &&
{children}
}
/>
>
diff --git a/src/components/FloatingActionsButton.tsx b/src/components/FloatingActionsButton.tsx
index c0033d72..c9ca3454 100644
--- a/src/components/FloatingActionsButton.tsx
+++ b/src/components/FloatingActionsButton.tsx
@@ -54,7 +54,7 @@ const FloatingActionsButton = ({
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index 973bf031..bee92a57 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -7,6 +7,7 @@ import { Icon } from '@iconify/react';
import Menu from '@/components/menu/Menu';
import MenuItem from '@/components/menu/MenuItem';
import Button from '@/components/Button';
+import Dropdown from '@/components/dropdown/Dropdown';
import { useAuth } from '@/services/hooks/useAuth';
import { AuthApi } from '@/services/api/auth';
@@ -52,21 +53,21 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
-
-
-
-
+
);
diff --git a/src/components/dropdown/Dropdown.tsx b/src/components/dropdown/Dropdown.tsx
new file mode 100644
index 00000000..4489231d
--- /dev/null
+++ b/src/components/dropdown/Dropdown.tsx
@@ -0,0 +1,116 @@
+'use client';
+
+import { ReactNode, useRef, useEffect, useState } from 'react';
+import { cn } from '@/lib/helper';
+
+interface DropdownProps {
+ trigger: ReactNode;
+ children: ReactNode;
+ position?:
+ | 'top'
+ | 'bottom'
+ | 'left'
+ | 'right'
+ | 'top-start'
+ | 'top-end'
+ | 'bottom-start'
+ | 'bottom-end'
+ | 'left-start'
+ | 'left-end'
+ | 'right-start'
+ | 'right-end';
+ align?: 'start' | 'center' | 'end';
+ hover?: boolean;
+ className?: string;
+ contentClassName?: string;
+}
+
+const Dropdown = ({
+ trigger,
+ children,
+ position = 'bottom',
+ align = 'start',
+ hover = false,
+ className,
+ contentClassName,
+}: DropdownProps) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const dropdownRef = useRef(null);
+
+ // Handle click outside to close dropdown
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (
+ dropdownRef.current &&
+ !dropdownRef.current.contains(event.target as Node)
+ ) {
+ setIsOpen(false);
+ }
+ };
+
+ if (isOpen) {
+ document.addEventListener('mousedown', handleClickOutside);
+ }
+
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [isOpen]);
+
+ // Build position classes
+ const getPositionClasses = () => {
+ const classes: string[] = [];
+
+ // Handle combined positions like 'top-start'
+ if (position.includes('-')) {
+ const [pos, al] = position.split('-');
+ classes.push(`dropdown-${pos}`);
+ classes.push(`dropdown-${al}`);
+ } else {
+ classes.push(`dropdown-${position}`);
+ if (align !== 'start') {
+ classes.push(`dropdown-${align}`);
+ }
+ }
+
+ return classes.join(' ');
+ };
+
+ const handleToggle = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ // alert('clicked');
+ setIsOpen(!isOpen);
+ };
+
+ return (
+
+ {/* Trigger Button */}
+
+ {trigger}
+
+
+ {/* Dropdown Content - Only render when open */}
+ {isOpen && (
+
setIsOpen(false)} // Close on item click
+ >
+ {children}
+
+ )}
+
+ );
+};
+
+export default Dropdown;
diff --git a/src/components/dropdown/README.md b/src/components/dropdown/README.md
new file mode 100644
index 00000000..e682682a
--- /dev/null
+++ b/src/components/dropdown/README.md
@@ -0,0 +1,83 @@
+# Dropdown Component
+
+Komponen Dropdown reusable berdasarkan DaisyUI yang mengatasi issue children component yang ter-render sebelum dropdown dibuka.
+
+## Features
+
+- ✅ **Conditional Rendering**: Children hanya di-render ketika dropdown aktif/terbuka
+- ✅ **Click Outside to Close**: Otomatis menutup dropdown ketika klik di luar area dropdown
+- ✅ **Multiple Positions**: Support berbagai posisi (top, bottom, left, right) dengan alignment (start, center, end)
+- ✅ **Hover Support**: Optional hover mode untuk membuka dropdown
+- ✅ **Customizable**: Mendukung custom className untuk container dan content
+
+## Usage
+
+### Basic Example
+
+```tsx
+import Dropdown from '@/components/dropdown/Dropdown';
+import Menu from '@/components/menu/Menu';
+import MenuItem from '@/components/menu/MenuItem';
+
+Click Me
+ }
+>
+
+
+
+```
+
+### With Position
+
+```tsx
+Dropdown}
+ contentClassName="w-52 mt-3"
+>
+ {/* Your content */}
+
+```
+
+### Hover Mode
+
+```tsx
+Hover Me}
+>
+ {/* Your content */}
+
+```
+
+## Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `trigger` | `ReactNode` | - | **Required**. Element yang akan men-trigger dropdown |
+| `children` | `ReactNode` | - | **Required**. Content dropdown yang akan ditampilkan |
+| `position` | `'top' \| 'bottom' \| 'left' \| 'right' \| 'top-start' \| 'top-end' \| 'bottom-start' \| 'bottom-end' \| 'left-start' \| 'left-end' \| 'right-start' \| 'right-end'` | `'bottom'` | Posisi dropdown relatif terhadap trigger |
+| `align` | `'start' \| 'center' \| 'end'` | `'start'` | Alignment dropdown (digunakan jika position tidak mengandung alignment) |
+| `hover` | `boolean` | `false` | Aktifkan mode hover untuk membuka dropdown |
+| `className` | `string` | - | Custom className untuk container dropdown |
+| `contentClassName` | `string` | - | Custom className untuk content dropdown |
+
+## Position Examples
+
+- `bottom` - Dropdown muncul di bawah, align ke start
+- `bottom-end` - Dropdown muncul di bawah, align ke end
+- `bottom-center` - Dropdown muncul di bawah, align ke center
+- `top-start` - Dropdown muncul di atas, align ke start
+- `left-end` - Dropdown muncul di kiri, align ke end
+- Dan seterusnya...
+
+## Key Benefits
+
+1. **Performance**: Children tidak di-render sampai dropdown dibuka, menghemat resources
+2. **Clean State**: Setiap kali dropdown dibuka, children di-render fresh
+3. **DaisyUI Compatible**: Menggunakan class DaisyUI yang sudah ada
+4. **Accessible**: Menggunakan proper ARIA attributes dan keyboard navigation
diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx
index 119d74cb..dbd4b6bc 100644
--- a/src/components/helper/RequireAuth.tsx
+++ b/src/components/helper/RequireAuth.tsx
@@ -6,9 +6,147 @@ import useSWRImmutable from 'swr/immutable';
import { useAuth } from '@/services/hooks/useAuth';
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
-import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
-import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general';
-import { AxiosError } from 'axios';
+import { isResponseSuccess } from '@/lib/api-helper';
+import { GetMeResponse } from '@/types/api/api-general';
+
+// TODO: delete this later, DONT HARDCODE USER DATA
+const DUMMY_USER = {
+ id: 1,
+ email: 'admin@mbugroup.id',
+ npk: '0001',
+ name: 'Super Admin',
+ image: null,
+ created_at: '2025-09-30T03:24:20.899229Z',
+ updated_at: '2025-09-30T03:24:20.899229Z',
+ roles: [
+ {
+ id: 1,
+ key: 'mbu.super_admin',
+ name: 'MBU Administrator',
+ client: {
+ id: 1,
+ name: 'PT Mitra Berlian Unggas',
+ alias: 'MBU',
+ },
+ permissions: [
+ {
+ id: 1,
+ name: 'mbu:purchase:read',
+ action: 'read',
+ client: {
+ id: 1,
+ name: 'PT Mitra Berlian Unggas',
+ alias: 'MBU',
+ },
+ },
+ {
+ id: 2,
+ name: 'mbu:purchase:create',
+ action: 'create',
+ client: {
+ id: 1,
+ name: 'PT Mitra Berlian Unggas',
+ alias: 'MBU',
+ },
+ },
+ {
+ id: 3,
+ name: 'mbu:purchase:approve',
+ action: 'approve',
+ client: {
+ id: 1,
+ name: 'PT Mitra Berlian Unggas',
+ alias: 'MBU',
+ },
+ },
+ ],
+ },
+ {
+ id: 2,
+ key: 'lti.super_admin',
+ name: 'LTI Administrator',
+ client: {
+ id: 2,
+ name: 'PT Lumbung Telur Indonesia',
+ alias: 'LTI',
+ },
+ permissions: [
+ {
+ id: 4,
+ name: 'lti:purchase:read',
+ action: 'read',
+ client: {
+ id: 2,
+ name: 'PT Lumbung Telur Indonesia',
+ alias: 'LTI',
+ },
+ },
+ {
+ id: 5,
+ name: 'lti:purchase:create',
+ action: 'create',
+ client: {
+ id: 2,
+ name: 'PT Lumbung Telur Indonesia',
+ alias: 'LTI',
+ },
+ },
+ {
+ id: 6,
+ name: 'lti:purchase:approve',
+ action: 'approve',
+ client: {
+ id: 2,
+ name: 'PT Lumbung Telur Indonesia',
+ alias: 'LTI',
+ },
+ },
+ ],
+ },
+ {
+ id: 3,
+ key: 'manbu.super_admin',
+ name: 'MANBU Administrator',
+ client: {
+ id: 3,
+ name: 'PT Mandiri Berlian Unggas',
+ alias: 'MANBU',
+ },
+ permissions: [
+ {
+ id: 7,
+ name: 'manbu:purchase:read',
+ action: 'read',
+ client: {
+ id: 3,
+ name: 'PT Mandiri Berlian Unggas',
+ alias: 'MANBU',
+ },
+ },
+ {
+ id: 8,
+ name: 'manbu:purchase:create',
+ action: 'create',
+ client: {
+ id: 3,
+ name: 'PT Mandiri Berlian Unggas',
+ alias: 'MANBU',
+ },
+ },
+ {
+ id: 9,
+ name: 'manbu:purchase:approve',
+ action: 'approve',
+ client: {
+ id: 3,
+ name: 'PT Mandiri Berlian Unggas',
+ alias: 'MANBU',
+ },
+ },
+ ],
+ },
+ ],
+};
interface RequireAuthProps {
children?: ReactNode;
@@ -18,20 +156,17 @@ const RequireAuth = ({ children }: RequireAuthProps) => {
const router = useRouter();
const { setUser, setIsLoadingUser } = useAuth();
- const {
- data: userResponse,
- isLoading: isLoadingUserResponse,
- error: userErrorResponse,
- } = useSWRImmutable<
- GetMeResponse & { ok?: boolean },
- AxiosError,
- SWRHttpKey
- >('/sso/userinfo', httpClientFetcher, {
- shouldRetryOnError: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshInterval: 0,
- });
+ const { data: userResponse, isLoading: isLoadingUserResponse } =
+ useSWRImmutable(
+ '/auth/sso/userinfo',
+ httpClientFetcher,
+ {
+ shouldRetryOnError: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshInterval: 0,
+ }
+ );
useEffect(() => {
setIsLoadingUser(isLoadingUserResponse);
@@ -40,25 +175,23 @@ const RequireAuth = ({ children }: RequireAuthProps) => {
useEffect(() => {
if (isResponseSuccess(userResponse)) {
setUser(userResponse.data);
- } else if (
- isResponseError(userErrorResponse?.response?.data) &&
- typeof window !== 'undefined'
- ) {
- router.replace(
- `${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}`
- );
+ } else {
+ // router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string);
+ // TODO: remove this later, DONT HARDCODE USER DATA
+ setUser(DUMMY_USER);
}
- }, [userResponse, userErrorResponse, setIsLoadingUser, setUser]);
+ }, [userResponse, setIsLoadingUser, setUser]);
- if (isLoadingUserResponse && !userResponse && !userErrorResponse) {
- return (
-
-
-
- );
- }
+ // TODO: uncomment this later
+ // if (isLoadingUserResponse && !userResponse) {
+ // return (
+ //
+ //
+ //
+ // );
+ // }
- return <>{isResponseSuccess(userResponse) && children}>;
+ return <>{children}>;
};
export default RequireAuth;
diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts
index 041108d0..dc0d804a 100644
--- a/src/services/api/closing.ts
+++ b/src/services/api/closing.ts
@@ -51,4 +51,4 @@ export class ClosingApiService extends BaseApiService {
}
}
-export const ClosingApi = new ClosingApiService('/closing');
+export const ClosingApi = new ClosingApiService('/closings');