diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 345f305f..951e5472 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,7 +1,7 @@
stages:
- build
- deploy
-
+
.build_template: &build_template
stage: build
image: node:20-alpine
@@ -10,15 +10,15 @@ stages:
paths:
- node_modules/
variables:
- NPM_CONFIG_PRODUCTION: "false"
- NODE_ENV: ""
+ NPM_CONFIG_PRODUCTION: 'false'
+ NODE_ENV: ''
script:
- echo "Installing dependencies..."
- npm ci --no-audit --no-fund
- echo "Building Next.js static export..."
- npx next build
artifacts:
- name: "out-$CI_COMMIT_SHORT_SHA"
+ name: 'out-$CI_COMMIT_SHORT_SHA'
paths:
- out/
expire_in: 1 week
@@ -27,7 +27,7 @@ stages:
stage: deploy
image:
name: amazon/aws-cli:latest
- entrypoint: ["/bin/sh", "-c"]
+ entrypoint: ['/bin/sh', '-c']
script:
- set -e
- aws --version
@@ -106,22 +106,21 @@ build:dev:
environment:
name: development
variables:
- NEXT_PUBLIC_API_BASE_URL: "https://dev-api-lti.mbugroup.id"
- NEXT_PUBLIC_SSO_LOGIN_URL: "https://dev-api-sso.mbugroup.id"
+ NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id'
+ NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-api-sso.mbugroup.id'
deploy:dev:
<<: *deploy_template
- needs: ["build:dev"]
+ needs: ['build:dev']
rules:
- if: '$CI_COMMIT_BRANCH == "development"'
variables:
- S3_BUCKET: "dev-lti-erp.mbugroup.id"
- AWS_REGION: "ap-southeast-3"
- CLOUDFRONT_DISTRIBUTION_ID: "E1Z8XTA8XF1GIV"
+ S3_BUCKET: 'dev-lti-erp.mbugroup.id'
+ AWS_REGION: 'ap-southeast-3'
+ CLOUDFRONT_DISTRIBUTION_ID: 'E1Z8XTA8XF1GIV'
environment:
name: development
url: https://dev-lti-erp.mbugroup.id
-
# ====== PRODUCTION ======
# build:production:
# <<: *build_template
@@ -143,5 +142,5 @@ deploy:dev:
# CLOUDFRONT_DISTRIBUTION_ID: "ddfd"
# environment:
# name: production
- # url: https://royalgoldcapital.com
+# url: https://royalgoldcapital.com
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 8d658170..b89f441b 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -1,4 +1,4 @@
-version: "3.9"
+version: '3.9'
services:
dev-web-lti:
@@ -7,7 +7,7 @@ services:
context: .
dockerfile: Dockerfile
ports:
- - "3002:3000"
+ - '3002:3000'
env_file:
- .env
environment:
@@ -19,13 +19,13 @@ services:
deploy:
resources:
limits:
- cpus: "3.0"
+ cpus: '3.0'
memory: 3G
reservations:
- cpus: "1.0"
+ cpus: '1.0'
memory: 512M
extra_hosts:
- - "host.docker.internal:host-gateway"
+ - 'host.docker.internal:host-gateway'
# Optional: aktifkan healthcheck jika punya endpoint
# healthcheck:
# test: ["CMD-SHELL", "curl -fsS http://localhost:3000/api/healthz || exit 1"]
@@ -36,4 +36,4 @@ services:
networks:
dev-lti-network:
- external: true
\ No newline at end of file
+ external: true
diff --git a/src/app/globals.css b/src/app/globals.css
index c3d05c67..e50e020d 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -48,3 +48,8 @@
html {
scrollbar-gutter: initial;
}
+
+.react-select__menu-portal {
+ position: relative;
+ z-index: 99999 !important;
+}
diff --git a/src/app/marketing/sales-orders/add/page.tsx b/src/app/marketing/sales-orders/add/page.tsx
new file mode 100644
index 00000000..e60085ef
--- /dev/null
+++ b/src/app/marketing/sales-orders/add/page.tsx
@@ -0,0 +1,11 @@
+import SalesForm from '@/components/pages/marketing/sales-orders/form/SalesForm';
+
+const AddSalesOrder = () => {
+ return (
+
+
+
+ );
+};
+
+export default AddSalesOrder;
diff --git a/src/app/marketing/sales-orders/detail/edit/page.tsx b/src/app/marketing/sales-orders/detail/edit/page.tsx
new file mode 100644
index 00000000..86cafcb6
--- /dev/null
+++ b/src/app/marketing/sales-orders/detail/edit/page.tsx
@@ -0,0 +1,42 @@
+'use client';
+
+import SalesForm from '@/components/pages/marketing/sales-orders/form/SalesForm';
+import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
+import { MarketingApi } from '@/services/api/marketing/marketing';
+import { useRouter, useSearchParams } from 'next/navigation';
+import useSWR from 'swr';
+
+const EditSalesOrder = () => {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const soId = searchParams.get('salesOrderId');
+
+ const { data: marketing, isLoading: isLoading } = useSWR(soId, (id: number) =>
+ MarketingApi.getSingle(id)
+ );
+
+ if (!soId) {
+ router.back();
+
+ return (
+
+
+
+ );
+ }
+
+ if (!isLoading && (!marketing || isResponseError(marketing))) {
+ router.replace('/404');
+ return;
+ }
+ return (
+
+ {isLoading && }
+ {!isLoading && isResponseSuccess(marketing) && (
+
+ )}
+
+ );
+};
+export default EditSalesOrder;
diff --git a/src/app/production/chickin/add/layout.tsx b/src/app/marketing/sales-orders/detail/layout.tsx
similarity index 100%
rename from src/app/production/chickin/add/layout.tsx
rename to src/app/marketing/sales-orders/detail/layout.tsx
diff --git a/src/app/marketing/sales-orders/detail/page.tsx b/src/app/marketing/sales-orders/detail/page.tsx
new file mode 100644
index 00000000..22d2651c
--- /dev/null
+++ b/src/app/marketing/sales-orders/detail/page.tsx
@@ -0,0 +1,44 @@
+'use client';
+
+import SalesOrderDetail from '@/components/pages/marketing/sales-orders/detail/SalesOrderDetail';
+import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
+import { MarketingApi } from '@/services/api/marketing/marketing';
+import { useRouter, useSearchParams } from 'next/navigation';
+import useSWR from 'swr';
+
+const DetailSalesOrder = () => {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const soId = searchParams.get('salesOrderId');
+
+ const { data: marketing, isLoading: isLoading } = useSWR(soId, (id: number) =>
+ MarketingApi.getSingle(id)
+ );
+
+ if (!soId) {
+ router.back();
+
+ return (
+
+
+
+ );
+ }
+
+ if (!isLoading && (!marketing || isResponseError(marketing))) {
+ router.replace('/404');
+ return;
+ }
+
+ return (
+
+ {isLoading && }
+ {!isLoading && isResponseSuccess(marketing) && (
+
+ )}
+
+ );
+};
+
+export default DetailSalesOrder;
diff --git a/src/app/marketing/sales-orders/page.tsx b/src/app/marketing/sales-orders/page.tsx
new file mode 100644
index 00000000..3494b6a1
--- /dev/null
+++ b/src/app/marketing/sales-orders/page.tsx
@@ -0,0 +1,10 @@
+import SalesOrderTable from '@/components/pages/marketing/sales-orders/SalesOrderTable';
+
+const SalesOrder = () => {
+ return (
+
+
+
+ );
+};
+export default SalesOrder;
diff --git a/src/app/production/chickin/add/page.tsx b/src/app/production/chickin/add/page.tsx
deleted file mode 100644
index 3ef73396..00000000
--- a/src/app/production/chickin/add/page.tsx
+++ /dev/null
@@ -1,270 +0,0 @@
-'use client';
-
-import Button from '@/components/Button';
-import SelectInput, { OptionType } from '@/components/input/SelectInput';
-import Modal, { useModal } from '@/components/Modal';
-import ConfirmationModal from '@/components/modal/ConfirmationModal';
-import ChickinForm from '@/components/pages/production/chickin/form/ChickinForm';
-import Table from '@/components/Table';
-import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
-import { cn } from '@/lib/helper';
-import { ProjectFlockApi } from '@/services/api/production';
-import { useTableFilter } from '@/services/hooks/useTableFilter';
-import { BaseApiResponse } from '@/types/api/api-general';
-import { Kandang } from '@/types/api/master-data/kandang';
-import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
-import { Icon } from '@iconify/react';
-import { useRouter, useSearchParams } from 'next/navigation';
-import { useState } from 'react';
-
-import useSWR from 'swr';
-
-const AddChickin = () => {
- const router = useRouter();
- const searchParams = useSearchParams();
- const projectFlockId = searchParams.get('projectFlockId');
-
- // Tables Props
- const { state: tableFilterState } = useTableFilter({
- initial: { search: '' },
- paramMap: { page: 'page', pageSize: 'limit' },
- });
-
- // States
- const [selectedKandang, setSelectedKandang] = useState(
- undefined
- );
- const [projectFlockKandang, setProjectFlockKandang] =
- useState>();
- const [isLoadingProjectFlockKandang, setIsLoadingProjectFlockKandang] =
- useState(false);
- const [searchProjectFlock, setSearchProjectFlock] = useState('');
-
- // Fetch Data
- const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR(
- projectFlockId,
- (id: number) => ProjectFlockApi.getSingle(id)
- );
- const { data: listProjectFlock, isLoading: isLoadingListProjectFlock } =
- useSWR(
- `${ProjectFlockApi.basePath}?${new URLSearchParams({
- search: searchProjectFlock,
- }).toString()}`,
- ProjectFlockApi.getAllFetcher
- );
-
- const getProjectFlockKandangUrl = `/kandangs/lookup`;
- // Mapping Options
- const options = isResponseSuccess(listProjectFlock)
- ? listProjectFlock?.data.map((projectFlock) => {
- return {
- value: projectFlock.id,
- label: `${projectFlock?.flock?.name} - ${projectFlock?.category} - Periode ${projectFlock.period}`,
- };
- })
- : [];
-
- const chickinModal = useModal();
- const alertModal = useModal();
-
- if (!projectFlockId) {
- router.back();
-
- return (
-
-
-
- );
- }
-
- if (
- !isLoadingProjectFlock &&
- (!projectFlock || isResponseError(projectFlock))
- ) {
- router.replace('/404');
- return;
- }
-
- // Handle Function
- const handleChickinClick = async (kandang: Kandang) => {
- setIsLoadingProjectFlockKandang(true);
- setSelectedKandang(kandang);
- const ProjectFlockKandangRes = await ProjectFlockApi.customRequest<
- BaseApiResponse,
- 'GET'
- >(getProjectFlockKandangUrl, {
- method: 'GET',
- params: {
- project_flock_id: projectFlockId ?? 0,
- kandang_id: kandang.id,
- },
- });
- if (isResponseSuccess(ProjectFlockKandangRes)) {
- setProjectFlockKandang(ProjectFlockKandangRes);
- setIsLoadingProjectFlockKandang(false);
- if (
- ProjectFlockKandangRes.data.available_quantity &&
- ProjectFlockKandangRes.data.available_quantity > 0
- ) {
- chickinModal.openModal();
- } else {
- alertModal.openModal();
- }
- }
- };
- const handleAfterSubmit = () => {
- chickinModal.closeModal();
- router.push('/production/chickin');
- };
-
- return (
- <>
- {isResponseSuccess(projectFlock) && (
- <>
-
-
-
- data={projectFlock.data?.kandangs}
- columns={[
- {
- header: '#',
- cell: (props) =>
- tableFilterState.pageSize * (tableFilterState.page - 1) +
- props.row.index +
- 1,
- },
- {
- accessorKey: 'name',
- header: 'Nama Kandang',
- },
- {
- header: 'Aksi',
- cell: (props) => {
- return (
- <>
-
- >
- );
- },
- },
- ]}
- page={undefined}
- className={{
- containerClassName: cn({
- 'mb-20':
- isResponseSuccess(projectFlock) &&
- projectFlock.data?.kandangs?.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',
- paginationClassName: 'hidden',
- }}
- />
-
-
-
-
- Chickin Kandang - {selectedKandang?.name}
-
-
-
- {isResponseSuccess(projectFlockKandang) &&
- !isLoadingProjectFlockKandang && (
-
- )}
-
- {
- alertModal.closeModal();
- },
- }}
- />
- >
- )}
- >
- );
-};
-
-export default AddChickin;
diff --git a/src/app/production/chickin/detail/layout.tsx b/src/app/production/project-flock/chickin/add/kandang/layout.tsx
similarity index 100%
rename from src/app/production/chickin/detail/layout.tsx
rename to src/app/production/project-flock/chickin/add/kandang/layout.tsx
diff --git a/src/app/production/project-flock/chickin/add/kandang/page.tsx b/src/app/production/project-flock/chickin/add/kandang/page.tsx
new file mode 100644
index 00000000..a22039d1
--- /dev/null
+++ b/src/app/production/project-flock/chickin/add/kandang/page.tsx
@@ -0,0 +1,60 @@
+'use client';
+
+import ChickinForm from '@/components/pages/production/chickin/form/ChickinForm';
+import { isResponseSuccess } from '@/lib/api-helper';
+import { ProjectFlockKandangApi } from '@/services/api/production';
+import { useRouter, useSearchParams } from 'next/navigation';
+import useSWR from 'swr';
+
+export default function AddChickinKandang() {
+ const searchParams = useSearchParams();
+ const projectFlockKandangId = searchParams.get('projectFlockKandangId');
+ const projectFlockId = searchParams.get('projectFlockId');
+ const router = useRouter();
+
+ const {
+ data: projectFlockKandang,
+ isLoading: isLoading,
+ mutate: refreshProjectFlockKandang,
+ } = useSWR(
+ `get-single-project-flock-kandang/${projectFlockKandangId}`,
+ async () =>
+ ProjectFlockKandangApi.getSingle(
+ parseInt(projectFlockKandangId as string)
+ )
+ );
+
+ if (!projectFlockKandangId) {
+ router.push(`/production/chickin/add?projectFlockId=${projectFlockId}`);
+ return (
+
+
+
+ );
+ }
+
+ if (!isLoading && !projectFlockKandang) {
+ router.replace('/404');
+ return;
+ }
+
+ const handleAfterSubmit = () => {
+ refreshProjectFlockKandang();
+ };
+
+ return (
+ <>
+
+ {isLoading && }
+ {!isLoading &&
+ isResponseSuccess(projectFlockKandang) &&
+ projectFlockId && (
+
+ )}
+
+ >
+ );
+}
diff --git a/src/app/production/project-flock/chickin/add/layout.tsx b/src/app/production/project-flock/chickin/add/layout.tsx
new file mode 100644
index 00000000..7220dfa1
--- /dev/null
+++ b/src/app/production/project-flock/chickin/add/layout.tsx
@@ -0,0 +1,11 @@
+import SuspenseHelper from '@/components/helper/SuspenseHelper';
+
+const Layout = ({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) => {
+ return {children};
+};
+
+export default Layout;
diff --git a/src/app/production/project-flock/chickin/add/page.tsx b/src/app/production/project-flock/chickin/add/page.tsx
new file mode 100644
index 00000000..3ca09c89
--- /dev/null
+++ b/src/app/production/project-flock/chickin/add/page.tsx
@@ -0,0 +1,24 @@
+'use client';
+
+import { FormHeader } from '@/components/helper/form/FormHeader';
+import ProjectFlockChickinDetail from '@/components/pages/production/project-flock/chickin/ProjectFlockChickinDetail';
+import { useSearchParams } from 'next/navigation';
+
+const AddChickin = () => {
+ const searchParams = useSearchParams();
+ const projectFlockId = searchParams.get('projectFlockId');
+
+ return (
+ <>
+
+ >
+ );
+};
+
+export default AddChickin;
diff --git a/src/app/production/project-flock/chickin/detail/layout.tsx b/src/app/production/project-flock/chickin/detail/layout.tsx
new file mode 100644
index 00000000..7220dfa1
--- /dev/null
+++ b/src/app/production/project-flock/chickin/detail/layout.tsx
@@ -0,0 +1,11 @@
+import SuspenseHelper from '@/components/helper/SuspenseHelper';
+
+const Layout = ({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) => {
+ return {children};
+};
+
+export default Layout;
diff --git a/src/app/production/chickin/detail/page.tsx b/src/app/production/project-flock/chickin/detail/page.tsx
similarity index 94%
rename from src/app/production/chickin/detail/page.tsx
rename to src/app/production/project-flock/chickin/detail/page.tsx
index be8c5332..daea0f0a 100644
--- a/src/app/production/chickin/detail/page.tsx
+++ b/src/app/production/project-flock/chickin/detail/page.tsx
@@ -6,7 +6,7 @@ import Modal, { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import ChickinForm from '@/components/pages/production/chickin/form/ChickinForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
-import { ChickinApi } from '@/services/api/production';
+import { ChickinApi } from '@/services/api/production/chickin';
import { BaseApiResponse } from '@/types/api/api-general';
import {
Chickin,
@@ -170,8 +170,8 @@ const DetailChickin = () => {
Flock
{
- chickin.data.project_flock_kandang?.project_flock.flock
- .name
+ chickin?.data?.project_flock_kandang?.project_flock?.flock
+ ?.name
}
@@ -225,8 +225,8 @@ const DetailChickin = () => {
Flock Kandang
{
- chickin.data.project_flock_kandang?.project_flock.flock
- .name
+ chickin?.data?.project_flock_kandang?.project_flock?.flock
+ ?.name
}{' '}
- {chickin.data.project_flock_kandang?.kandang.name}
@@ -280,7 +280,7 @@ const DetailChickin = () => {
{
/>
- {
- refreshChickin();
- chickinModal.closeModal();
- }}
- />
{
text={`Apakah anda yakin ingin ${
approvalAction == 'APPROVED' ? 'approve' : 'reject'
} chickin berikut? (${
- chickin?.data.project_flock_kandang?.project_flock.flock.name
+ chickin?.data?.project_flock_kandang?.project_flock?.flock?.name
} - ${chickin?.data.project_flock_kandang?.kandang.name})?`}
secondaryButton={{
text: 'Tidak',
diff --git a/src/app/production/chickin/page.tsx b/src/app/production/project-flock/chickin/page.tsx
similarity index 100%
rename from src/app/production/chickin/page.tsx
rename to src/app/production/project-flock/chickin/page.tsx
diff --git a/src/app/production/project-flock/detail/edit/page.tsx b/src/app/production/project-flock/detail/edit/page.tsx
index 7576cc27..f55ce601 100644
--- a/src/app/production/project-flock/detail/edit/page.tsx
+++ b/src/app/production/project-flock/detail/edit/page.tsx
@@ -2,7 +2,7 @@
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
-import { ProjectFlockApi } from '@/services/api/production';
+import { ProjectFlockApi } from '@/services/api/production/project-flock';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
@@ -12,10 +12,11 @@ const ProjectFlockEdit = () => {
const projectFlockId = searchParams.get('projectFlockId');
- const { data: projectFlock, isLoading: isLoadingCostumer } = useSWR(
- projectFlockId,
- (id: number) => ProjectFlockApi.getSingle(id)
- );
+ const {
+ data: projectFlock,
+ isLoading: isLoadingProjectFlock,
+ mutate: refreshProjectFlocks,
+ } = useSWR(projectFlockId, (id: number) => ProjectFlockApi.getSingle(id));
if (!projectFlockId) {
router.back();
@@ -27,17 +28,20 @@ const ProjectFlockEdit = () => {
);
}
- if (!isLoadingCostumer && (!projectFlock || isResponseError(projectFlock))) {
+ if (
+ !isLoadingProjectFlock &&
+ (!projectFlock || isResponseError(projectFlock))
+ ) {
router.replace('/404');
return;
}
return (
-
- {isLoadingCostumer && (
+
+ {isLoadingProjectFlock && (
)}
- {!isLoadingCostumer && isResponseSuccess(projectFlock) && (
+ {!isLoadingProjectFlock && isResponseSuccess(projectFlock) && (
)}
diff --git a/src/app/production/project-flock/detail/page.tsx b/src/app/production/project-flock/detail/page.tsx
index 6cf694c0..91d4dfd5 100644
--- a/src/app/production/project-flock/detail/page.tsx
+++ b/src/app/production/project-flock/detail/page.tsx
@@ -2,7 +2,7 @@
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
-import { ProjectFlockApi } from '@/services/api/production';
+import { ProjectFlockApi } from '@/services/api/production/project-flock';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
@@ -37,11 +37,11 @@ const ProjectFlockDetail = () => {
}
return (
-
+
{isLoadingProjectFlock && (
)}
- {!isLoadingProjectFlock && isResponseSuccess(projectFlock) && (
+ {isResponseSuccess(projectFlock) && (
, 'className'> {
+ tabs: TabItem[];
+ variant?: 'bordered' | 'lifted' | 'boxed';
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+ placement?: 'top' | 'bottom';
+ /** Tab yang aktif secara default (uncontrolled mode) */
+ defaultActiveId?: string;
+ /** Tab yang aktif (controlled mode, dikontrol parent) */
+ activeTabId?: string;
+ className?:
+ | string
+ | {
+ wrapper?: string;
+ tab?: string;
+ content?: string;
+ };
+ onTabChange?: (tabId: string) => void;
+}
+
+const Tabs = ({
+ tabs,
+ variant,
+ size = 'md',
+ placement = 'top',
+ defaultActiveId,
+ activeTabId: controlledActiveId,
+ className,
+ onTabChange,
+ ...props
+}: TabsProps) => {
+ // State internal hanya dipakai kalau `activeTabId` (controlled) tidak diset
+ const [uncontrolledActiveId, setUncontrolledActiveId] = useState(
+ defaultActiveId || tabs[0]?.id || ''
+ );
+
+ const isControlled = controlledActiveId !== undefined;
+ const activeTabId = isControlled ? controlledActiveId : uncontrolledActiveId;
+
+ const handleTabChange = (tabId: string) => {
+ if (tabId === activeTabId) return;
+ if (!isControlled) setUncontrolledActiveId(tabId);
+ onTabChange?.(tabId);
+ };
+
+ const { wrapper: wrapperClassName, tab: tabClassName } =
+ typeof className === 'object'
+ ? className
+ : { wrapper: className, tab: undefined };
+
+ const getTabsClasses = () => {
+ const variantClasses: Record = {
+ bordered: 'tabs-bordered',
+ lifted: 'tabs-lift',
+ boxed: 'tabs-box',
+ };
+
+ const sizeClasses: Record = {
+ xs: 'tabs-xs',
+ sm: 'tabs-sm',
+ md: '',
+ lg: 'tabs-lg',
+ xl: 'tabs-xl',
+ };
+
+ const placementClasses: Record = {
+ top: '',
+ bottom: 'tabs-bottom',
+ };
+
+ return cn(
+ 'tabs',
+ variant && variantClasses[variant],
+ sizeClasses[size],
+ placementClasses[placement],
+ wrapperClassName
+ );
+ };
+
+ const getTabClasses = (isActive: boolean, isDisabled?: boolean) =>
+ cn(
+ 'tab',
+ {
+ 'tab-active': isActive,
+ 'tab-disabled': isDisabled,
+ },
+ tabClassName
+ );
+
+ const activeContent = tabs.find((tab) => tab.id === activeTabId)?.content;
+
+ return (
+
+
+ {tabs.map(({ id, label, disabled }) => (
+
+ ))}
+
+
+ {activeContent &&
{activeContent}
}
+
+ );
+};
+
+export default Tabs;
diff --git a/src/components/helper/form/FormHeader.tsx b/src/components/helper/form/FormHeader.tsx
index ebc1d7ae..de7ec882 100644
--- a/src/components/helper/form/FormHeader.tsx
+++ b/src/components/helper/form/FormHeader.tsx
@@ -2,15 +2,27 @@ import Button from '@/components/Button';
import { Icon } from '@iconify/react';
interface FormHeaderProps {
- type: 'add' | 'edit' | 'detail';
+ type?: 'add' | 'edit' | 'detail';
title: string;
- backUrl: string;
+ backUrl?: string;
+ onBackClick?: () => void;
}
-export const FormHeader = ({ type, title, backUrl }: FormHeaderProps) => {
+export const FormHeader = ({
+ type,
+ title,
+ backUrl,
+ onBackClick,
+}: FormHeaderProps) => {
return (
-
);
diff --git a/src/components/input/PatternInput.tsx b/src/components/input/PatternInput.tsx
new file mode 100644
index 00000000..9af1b68e
--- /dev/null
+++ b/src/components/input/PatternInput.tsx
@@ -0,0 +1,90 @@
+'use client';
+
+import { ChangeEvent } from 'react';
+import {
+ PatternFormat,
+ NumberFormatBase,
+ NumberFormatBaseProps,
+ OnValueChange,
+} from 'react-number-format';
+import TextInput, { TextInputProps } from '@/components/input/TextInput';
+
+interface PatternInputProps extends Omit {
+ /**
+ * Format pattern, contoh: "##/##/####", "(###) ###-####", "####-####-####"
+ */
+ format: string;
+ /** Mask karakter kosong, misal "_" */
+ mask?: string;
+ /** Menampilkan mask walau value kosong */
+ allowEmptyFormatting?: boolean;
+ /** Placeholder karakter format, default: "#" */
+ patternChar?: string;
+ /** Jika true, izinkan huruf (A-Z) selain angka */
+ inputVehicleNumber?: boolean;
+ type?: 'text' | 'password' | 'tel';
+}
+
+/**
+ * PatternInput – tetap backward-compatible dengan Storybook
+ * tapi bisa menerima huruf jika `allowCharacters={true}`
+ */
+const PatternInput = ({
+ type = 'text',
+ format,
+ mask = '_',
+ allowEmptyFormatting = false,
+ patternChar = '#',
+ inputVehicleNumber = false,
+ onChange,
+ ...restProps
+}: PatternInputProps) => {
+ const handleValueChange: OnValueChange = (values, { event }) => {
+ const newEvent = event as ChangeEvent | undefined;
+ if (newEvent) {
+ newEvent.target.value = values.value.toUpperCase();
+ onChange?.(newEvent);
+ }
+ };
+
+ if (inputVehicleNumber) {
+ return (
+ {
+ const clean = value.replace(/[^a-z0-9]/gi, '').toUpperCase();
+
+ const match = clean.match(/^([A-Z]{0,2})(\d{0,4})([A-Z]{0,3})$/);
+ if (!match) return clean;
+ const [, prefix, number, suffix] = match;
+ return [prefix, number, suffix].filter(Boolean).join(' ');
+ }}
+ removeFormatting={(val) => val.replace(/\s+/g, '')}
+ isValidInputCharacter={(char) => /^[a-z0-9]$/i.test(char)}
+ getCaretBoundary={(val) =>
+ Array(val.length + 1)
+ .fill(true)
+ .map(Boolean)
+ }
+ onValueChange={handleValueChange}
+ />
+ );
+ }
+
+ return (
+
+ );
+};
+
+export default PatternInput;
diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx
index 6a8d0ac8..8fa8b555 100644
--- a/src/components/input/SelectInput.tsx
+++ b/src/components/input/SelectInput.tsx
@@ -1,22 +1,23 @@
'use client';
import { ComponentType, ReactNode, useEffect, useMemo, useState } from 'react';
-import useSWR from 'swr';
-
import Select, {
OptionProps,
GroupBase,
InputActionMeta,
MultiValue,
SingleValue,
+ components as ReactSelectComponents,
+ ControlProps,
} from 'react-select';
import CreatableSelect from 'react-select/creatable';
import makeAnimated from 'react-select/animated';
import { useDebounce } from 'use-debounce';
import { cn, getByPath } from '@/lib/helper';
+import useSWR from 'swr';
import { httpClientFetcher } from '@/services/http/client';
-import { isResponseSuccess } from '@/lib/api-helper';
import { BaseApiResponse } from '@/types/api/api-general';
+import { isResponseSuccess } from '@/lib/api-helper';
export interface OptionType {
value: string | number;
@@ -53,6 +54,8 @@ interface SelectInputBaseProps {
openMenu?: boolean;
delay?: number;
onInputChange?: (search: string) => void;
+ startAdornment?: ReactNode;
+ menuPortalTarget?: HTMLElement | null;
}
interface SelectInputProps extends SelectInputBaseProps {
@@ -63,6 +66,33 @@ interface SelectInputProps extends SelectInputBaseProps {
const animatedComponents = makeAnimated();
+const CustomControl = <
+ Option,
+ IsMulti extends boolean,
+ Group extends GroupBase