diff --git a/.gitignore b/.gitignore index 2b4315f8..82965e2d 100644 --- a/.gitignore +++ b/.gitignore @@ -40,5 +40,8 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts +# prettier +.prettierrc + # idea -.idea \ No newline at end of file +.idea diff --git a/src/app/master-data/customer/add/page.tsx b/src/app/master-data/customer/add/page.tsx new file mode 100644 index 00000000..a1096f02 --- /dev/null +++ b/src/app/master-data/customer/add/page.tsx @@ -0,0 +1,11 @@ +import CustomerForm from "@/components/pages/master-data/customer/form/CustomerForm"; + +const AddCustomer = () => { + return ( +
+ +
+ ); +} + +export default AddCustomer; \ No newline at end of file diff --git a/src/app/master-data/customer/detail/edit/page.tsx b/src/app/master-data/customer/detail/edit/page.tsx new file mode 100644 index 00000000..3fe8de52 --- /dev/null +++ b/src/app/master-data/customer/detail/edit/page.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; +import { CustomerApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import CustomerForm from '@/components/pages/master-data/customer/form/CustomerForm'; + +const CustomerEdit = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const costumerId = searchParams.get('customerId'); + + const { data: costumer, isLoading: isLoadingCostumer } = useSWR( + costumerId, + (id: number) => CustomerApi.getSingle(id) + ); + + if (!costumerId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingCostumer && (!costumer || isResponseError(costumer))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingCostumer && ( + + )} + {!isLoadingCostumer && isResponseSuccess(costumer) && ( + + )} +
+ ); +}; + +export default CustomerEdit; diff --git a/src/app/master-data/customer/detail/page.tsx b/src/app/master-data/customer/detail/page.tsx new file mode 100644 index 00000000..263458c2 --- /dev/null +++ b/src/app/master-data/customer/detail/page.tsx @@ -0,0 +1,45 @@ +'use client' + +import { useRouter, useSearchParams } from "next/navigation"; +import useSWR from "swr"; +import { CustomerApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from "@/lib/api-helper"; +import CustomerForm from "@/components/pages/master-data/customer/form/CustomerForm"; + +const CustomerDetail = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const costumerId = searchParams.get("customerId"); + + const { data: costumer, isLoading: isLoadingCostumer } = useSWR( + costumerId, + (id: number) => CustomerApi.getSingle(id) + ); + + if(!costumerId){ + router.back(); + + return ( +
+ +
+ ); + } + + if(!isLoadingCostumer && (!costumer || isResponseError(costumer))){ + router.replace("/404"); + return; + } + + return ( +
+ {isLoadingCostumer && } + {!isLoadingCostumer && isResponseSuccess(costumer) && ( + + )} +
+ ) +}; + +export default CustomerDetail; diff --git a/src/app/master-data/customer/page.tsx b/src/app/master-data/customer/page.tsx new file mode 100644 index 00000000..b80401f1 --- /dev/null +++ b/src/app/master-data/customer/page.tsx @@ -0,0 +1,11 @@ +import CustomersTable from "@/components/pages/master-data/customer/CustomersTable"; + +const Customer = () => { + return ( +
+ +
+ ) +}; + +export default Customer; \ No newline at end of file diff --git a/src/app/master-data/supplier/add/page.tsx b/src/app/master-data/supplier/add/page.tsx new file mode 100644 index 00000000..8a95c3c6 --- /dev/null +++ b/src/app/master-data/supplier/add/page.tsx @@ -0,0 +1,11 @@ +import SupplierForm from '@/components/pages/master-data/supplier/form/SupplierForm'; + +const AddSupplier = () => { + return ( +
+ +
+ ); +}; + +export default AddSupplier; \ No newline at end of file diff --git a/src/app/master-data/supplier/detail/edit/page.tsx b/src/app/master-data/supplier/detail/edit/page.tsx new file mode 100644 index 00000000..103db73d --- /dev/null +++ b/src/app/master-data/supplier/detail/edit/page.tsx @@ -0,0 +1,49 @@ +'use client'; + +import SupplierForm from '@/components/pages/master-data/supplier/form/SupplierForm'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { SupplierApi } from '@/services/api/master-data'; +import { useSearchParams, useRouter } from 'next/navigation'; +import useSWR from 'swr'; + +const SupplierEdit = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + // Get Query Params + const supplierId = searchParams.get('supplierId'); + + // Fetch Data + const { data: supplier, isLoading: isLoadingSupplier } = useSWR( + supplierId, + (id: number) => SupplierApi.getSingle(id) + ); + + if (!supplierId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingSupplier && (!supplier || isResponseError(supplier))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingSupplier && ( + + )} + {!isLoadingSupplier && isResponseSuccess(supplier) && ( + + )} +
+ ); +}; + +export default SupplierEdit; diff --git a/src/app/master-data/supplier/detail/page.tsx b/src/app/master-data/supplier/detail/page.tsx new file mode 100644 index 00000000..433fa043 --- /dev/null +++ b/src/app/master-data/supplier/detail/page.tsx @@ -0,0 +1,49 @@ +'use client'; + +import SupplierForm from '@/components/pages/master-data/supplier/form/SupplierForm'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { SupplierApi } from '@/services/api/master-data'; +import { useSearchParams, useRouter } from 'next/navigation'; +import useSWR from 'swr'; + +const SupplierDetail = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + // Get Query Params + const supplierId = searchParams.get('supplierId'); + + // Fetch Data + const { data: supplier, isLoading: isLoadingSupplier } = useSWR( + supplierId, + (id: number) => SupplierApi.getSingle(id) + ); + + if (!supplierId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingSupplier && (!supplier || isResponseError(supplier))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingSupplier && ( + + )} + {!isLoadingSupplier && isResponseSuccess(supplier) && ( + + )} +
+ ); +}; + +export default SupplierDetail; \ No newline at end of file diff --git a/src/app/master-data/supplier/page.tsx b/src/app/master-data/supplier/page.tsx new file mode 100644 index 00000000..1f54bd0d --- /dev/null +++ b/src/app/master-data/supplier/page.tsx @@ -0,0 +1,11 @@ +import SuppliersTable from "@/components/pages/master-data/supplier/SupplierTable"; + +const Supplier = () => { + return ( +
+ +
+ ); +}; + +export default Supplier; diff --git a/src/components/MainDrawer.tsx b/src/components/MainDrawer.tsx index 309cddf2..be87f069 100644 --- a/src/components/MainDrawer.tsx +++ b/src/components/MainDrawer.tsx @@ -189,6 +189,8 @@ const MainDrawer = ({ ); const traverseMenuTitle = (menu: typeof activeMenu) => { + if (!menu) return; + const hasSubmenu = menu?.submenu && menu?.submenu.length > 0; if (!title) { @@ -197,7 +199,7 @@ const MainDrawer = ({ title += ' - ' + menu?.title; } - if (!hasSubmenu) return; + if (!hasSubmenu || !menu.submenu) return; const activeSubmenu = menu.submenu?.find((item) => isPathActive(pathname, item.link) diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index 9bc199f9..1d9d86b4 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -9,6 +9,145 @@ import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; 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; } @@ -37,19 +176,22 @@ const RequireAuth = ({ children }: RequireAuthProps) => { if (isResponseSuccess(userResponse)) { setUser(userResponse.data); } else { - router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string); + // router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string); + // TODO: remove this later, DONT HARDCODE USER DATA + setUser(DUMMY_USER); } }, [userResponse, setIsLoadingUser, setUser]); - if (isLoadingUserResponse && !userResponse) { - return ( -
- -
- ); - } + // TODO: uncomment this later + // if (isLoadingUserResponse && !userResponse) { + // return ( + //
+ // + //
+ // ); + // } return <>{children}; }; -export default RequireAuth; +export default RequireAuth; \ No newline at end of file diff --git a/src/components/input/TagInput.tsx b/src/components/input/TagInput.tsx new file mode 100644 index 00000000..a14b2f63 --- /dev/null +++ b/src/components/input/TagInput.tsx @@ -0,0 +1,169 @@ +'use client'; + +import React, { useState, KeyboardEvent, ChangeEvent, useEffect } from 'react'; +import { cn } from '@/lib/helper'; + +export interface TagInputProps { + label?: string; + bottomLabel?: string; + name: string; + value?: string; + placeholder?: string; + className?: { + wrapper?: string; + label?: string; + inputWrapper?: string; + input?: string; + }; + isError?: boolean; + isValid?: boolean; + disabled?: boolean; + readOnly?: boolean; + required?: boolean; + isLoading?: boolean; + errorMessage?: string; + onChange?: (value: string) => void; +} + +const TagInput: React.FC = ({ + label, + bottomLabel, + name, + value = '', + placeholder, + className, + isError, + isValid, + errorMessage, + disabled = false, + readOnly = false, + required = false, + onChange, +}) => { + const [tags, setTags] = useState(value ? value.split(',') : []); + const [inputValue, setInputValue] = useState(''); + + useEffect(() => { + if (value !== undefined && value !== tags.join(',')) { + setTags(value ? value.split(',') : []); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]); + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ',') { + e.preventDefault(); + const newTag = inputValue.trim(); + if (newTag && !tags.includes(newTag)) { + const updatedTags = [...tags, newTag]; + setTags(updatedTags); + onChange?.(updatedTags.join(',')); + } + setInputValue(''); + } + }; + + const handleRemoveTag = (tagToRemove: string) => { + const updatedTags = tags.filter((t) => t !== tagToRemove); + setTags(updatedTags); + onChange?.(updatedTags.join(',')); + }; + + const handleInputChange = (e: ChangeEvent) => { + setInputValue(e.target.value); + }; + + return ( +
+ {/* Label */} + {label && ( + + )} + + {/* Input wrapper */} +
{ + // Fokuskan input saat area diklik + const inputEl = document.getElementById(name); + inputEl?.focus(); + }} + > + {tags.map((tag) => ( +
+ {tag} + {!readOnly && ( + + )} +
+ ))} + + {!readOnly && ( + + )} +
+ + {/* Bottom label or error message */} + {!isError && bottomLabel && ( +

{bottomLabel}

+ )} + {isError &&

{errorMessage}

} +
+ ); +}; + +export default TagInput; diff --git a/src/components/input/TextArea.tsx b/src/components/input/TextArea.tsx new file mode 100644 index 00000000..b4a6c9f5 --- /dev/null +++ b/src/components/input/TextArea.tsx @@ -0,0 +1,124 @@ +'use client'; + +import { + ChangeEventHandler, + FocusEventHandler, + ReactNode, +} from 'react'; + +import { cn } from '@/lib/helper'; + +export interface TextAreaProps { + label?: string; + bottomLabel?: string; + name: string; + value?: string | number; + placeholder?: string; + className?: { + wrapper?: string; + label?: string; + inputWrapper?: string; + input?: string; + }; + isError?: boolean; + isValid?: boolean; + disabled?: boolean; + readOnly?: boolean; + required?: boolean; + isLoading?: boolean; + errorMessage?: string; + startAdornment?: ReactNode; + endAdornment?: ReactNode; + onChange?: ChangeEventHandler; + onBlur?: FocusEventHandler; + cols?: number; +} + +const TextArea = ({ + label, + bottomLabel, + name, + value, + placeholder, + className, + isError, + isValid, + errorMessage, + startAdornment, + endAdornment, + disabled = false, + required = false, + onChange, + onBlur, + readOnly = false, + isLoading = false, + cols = 3 +}: TextAreaProps) => { + return ( +
+ {label && ( + + )} + {startAdornment && startAdornment} + +