diff --git a/src/app/globals.css b/src/app/globals.css index 386e7620..0fb52327 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,6 +1,43 @@ @import 'tailwindcss'; @plugin "daisyui"; +@plugin "daisyui/theme" { + name: "corporate"; + default: false; + prefersdark: false; + color-scheme: "light"; + --color-base-100: oklch(98% 0.001 106.423); + --color-base-200: oklch(97% 0.001 106.424); + --color-base-300: oklch(92% 0.003 48.717); + --color-base-content: oklch(22.389% 0.031 278.072); + --color-primary: oklch(60% 0.126 221.723); + --color-primary-content: oklch(100% 0 0); + --color-secondary: oklch(52% 0.105 223.128); + --color-secondary-content: oklch(100% 0 0); + --color-accent: oklch(45% 0.085 224.283); + --color-accent-content: oklch(100% 0 0); + --color-neutral: oklch(39% 0.07 227.392); + --color-neutral-content: oklch(100% 0 0); + --color-info: oklch(58% 0.158 241.966); + --color-info-content: oklch(100% 0 0); + --color-success: oklch(62% 0.194 149.214); + --color-success-content: oklch(100% 0 0); + --color-warning: oklch(85% 0.199 91.936); + --color-warning-content: oklch(0% 0 0); + --color-error: oklch(57% 0.245 27.325); + --color-error-content: oklch(100% 0 0); + --radius-selector: 0rem; + --radius-field: 0.25rem; + --radius-box: 0.25rem; + --size-selector: 0.21875rem; + --size-field: 0.1875rem; + --border: 1px; + --depth: 0; + --noise: 0; +} + + + :root { --color-primary: #1f74bf; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index ef28da38..c19b8a77 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -28,7 +28,7 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + {children} diff --git a/src/app/master-data/flock/add/page.tsx b/src/app/master-data/flock/add/page.tsx new file mode 100644 index 00000000..5ee3958e --- /dev/null +++ b/src/app/master-data/flock/add/page.tsx @@ -0,0 +1,11 @@ +import FlockForm from "@/components/pages/master-data/flock/form/FlockForm"; + +const AddFlock = () => { + return ( +
+ +
+ ); +} + +export default AddFlock; diff --git a/src/app/master-data/flock/detail/edit/page.tsx b/src/app/master-data/flock/detail/edit/page.tsx new file mode 100644 index 00000000..c3903555 --- /dev/null +++ b/src/app/master-data/flock/detail/edit/page.tsx @@ -0,0 +1,47 @@ +import FlockForm from "@/components/pages/master-data/flock/form/FlockForm"; +import { isResponseError, isResponseSuccess } from "@/lib/api-helper"; +import { FlockApi } from "@/services/api/master-data"; +import { useRouter, useSearchParams } from "next/navigation"; +import useSWR from "swr"; + +const FlockEdit = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + // Get Query Params + const flockId = searchParams.get('flockId'); + + // Fetch Data + const { data: flock, isLoading: isLoadingFlock } = useSWR( + flockId, + (id: number) => FlockApi.getSingle(id) + ); + + if (!flockId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingFlock && (!flock || isResponseError(flock))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingFlock && ( + + )} + {!isLoadingFlock && isResponseSuccess(flock) && ( + + )} +
+ ); +} + +export default FlockEdit; \ No newline at end of file diff --git a/src/app/master-data/flock/detail/page.tsx b/src/app/master-data/flock/detail/page.tsx new file mode 100644 index 00000000..cedc3243 --- /dev/null +++ b/src/app/master-data/flock/detail/page.tsx @@ -0,0 +1,44 @@ +import FlockForm from "@/components/pages/master-data/flock/form/FlockForm"; +import { isResponseError, isResponseSuccess } from "@/lib/api-helper"; +import { FlockApi } from "@/services/api/master-data"; +import { useRouter, useSearchParams } from "next/navigation"; +import useSWR from "swr"; + +const FlockDetail = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + // Get Query Params + const flockId = searchParams.get('flockId'); + + // Fetch Data + const { data: flock, isLoading: isLoadingFlock } = useSWR(flockId, (id: number) => FlockApi.getSingle(id)); + + if(!flockId){ + router.back(); + + return ( +
+ +
+ ); + } + + if(!isLoadingFlock && (!flock || isResponseError(flock))){ + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingFlock && ( + + )} + {!isLoadingFlock && isResponseSuccess(flock) && ( + + )} +
+ ); +} + +export default FlockDetail; \ No newline at end of file diff --git a/src/app/master-data/flock/page.tsx b/src/app/master-data/flock/page.tsx new file mode 100644 index 00000000..b317091a --- /dev/null +++ b/src/app/master-data/flock/page.tsx @@ -0,0 +1,11 @@ +import FlockTable from "@/components/pages/master-data/flock/FlocksTable"; + +const Flock = () => { + return ( +
+ +
+ ); +} + +export default Flock; diff --git a/src/components/Button.tsx b/src/components/Button.tsx index c67a29c2..5da6e5ad 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -43,7 +43,7 @@ const Button = ({ 'btn-warning': color === 'warning', 'btn-error': color === 'error', }, - 'h-fit justify-center items-center gap-2 rounded-xl p-2 text-base transition-all' + 'h-fit justify-center items-center gap-2 rounded p-2 text-base transition-all' ); return ( diff --git a/src/components/pages/master-data/flock/FlocksTable.tsx b/src/components/pages/master-data/flock/FlocksTable.tsx new file mode 100644 index 00000000..817eff40 --- /dev/null +++ b/src/components/pages/master-data/flock/FlocksTable.tsx @@ -0,0 +1,264 @@ +'use client'; + +import { CellContext, ColumnDef } from '@tanstack/react-table'; +import { Flock } from '@/types/api/master-data/flock'; +import { cn } from '@/lib/helper'; +import Button from '@/components/Button'; +import { Icon } from '@iconify/react'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { use, useState } from 'react'; +import useSWR from 'swr'; +import { FlockApi } from '@/services/api/master-data'; +import { useModal } from '@/components/Modal'; +import RowDropdownOptions from '@/components/table/RowDropdownOptions'; +import RowCollapseOptions from '@/components/table/RowCollapseOptions'; +import toast from 'react-hot-toast'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import { ROWS_OPTIONS } from '@/config/constant'; +import Table from '@/components/Table'; +import { isResponseSuccess } from '@/lib/api-helper'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; + +const RowsOptions = ({ + type = 'dropdown', + props, + deleteClickHandler, +}: { + type: 'dropdown' | 'collapse'; + props: CellContext; + deleteClickHandler: () => void; +}) => { + return ( +
+ + +
+ ); +}; + +const FlockTable = () => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { search: '', nameSort: '' }, + paramMap: { + page: 'page', + pageSize: 'limit', + nameSort: 'sort_name', + }, + }); + + // Fetch Data + const { + data: flocks, + isLoading, + mutate: refreshFlocks, + } = useSWR( + `${FlockApi.basePath}${getTableFilterQueryString()}`, + FlockApi.getAllFetcher + ); + + // State + const deleteModal = useModal(); + const [selectedFlock, setSelectedFlock] = useState( + undefined + ); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + // Columns Definition + const flocksColumns: ColumnDef[] = [ + { + header: '#', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + accessorKey: 'name', + header: 'Nama', + }, + { + accessorKey: 'created_at', + header: 'Dibuat pada', + cell: (props) => + new Date(props.row.original.created_at).toLocaleDateString(), + }, + { + header: 'Aksi', + cell: (props) => { + const currentPageSize = props.table.getPaginationRowModel().rows.length; + const currentPageRows = props.table.getPaginationRowModel().flatRows; + const currentRowRelativeIndex = + currentPageRows.findIndex((r) => r.id === props.row.id) + 1; + + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; + + const deleteClickHandler = () => { + setSelectedFlock(props.row.original); + deleteModal.openModal(); + }; + + return ( + <> + {currentPageSize > 2 && ( + + + + )} + {currentPageSize <= 2 && ( + + + + )} + + ); + }, + }, + ]; + + // Handler + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + + await FlockApi.delete(selectedFlock?.id as number); + refreshFlocks(); + + deleteModal.closeModal(); + toast.success('Successfully delete Flock!'); + setIsDeleteLoading(false); + }; + const searchChangeHandler = (e: React.ChangeEvent) => { + updateFilter('search', e.target.value); + }; + const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { + const newVal = val as OptionType; + setPageSize(newVal.value as number); + }; + + return ( + <> +
+
+
+
+ +
+ + +
+ +
+ +
+
+ + + data={isResponseSuccess(flocks) ? flocks?.data : []} + columns={flocksColumns} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(flocks) ? flocks?.meta?.page : 0} + totalItems={ + isResponseSuccess(flocks) ? flocks?.meta?.total_results : 0 + } + onPageChange={setPage} + isLoading={isLoading} + className={{ + containerClassName: cn({ + 'mb-20': isResponseSuccess(flocks) && flocks?.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', + }} + /> +
+ + + ); +}; + +export default FlockTable; \ No newline at end of file diff --git a/src/components/pages/master-data/flock/form/FlockForm.schema.ts b/src/components/pages/master-data/flock/form/FlockForm.schema.ts new file mode 100644 index 00000000..0a85b0fc --- /dev/null +++ b/src/components/pages/master-data/flock/form/FlockForm.schema.ts @@ -0,0 +1,14 @@ +import * as Yup from 'yup'; + +export const FlockFormSchema = Yup.object({ + name: Yup.string() + .required('Nama wajib diisi!') + .matches( + /^[a-zA-Z0-9]+$/, + 'Nama hanya boleh berisi huruf dan angka (tanpa spasi atau simbol)' + ), +}); + +export const UpdateFlockFormSchema = FlockFormSchema; + +export type FlockFormValues = Yup.InferType; \ No newline at end of file diff --git a/src/components/pages/master-data/flock/form/FlockForm.tsx b/src/components/pages/master-data/flock/form/FlockForm.tsx new file mode 100644 index 00000000..f73d47f0 --- /dev/null +++ b/src/components/pages/master-data/flock/form/FlockForm.tsx @@ -0,0 +1,217 @@ +'use client' + +import { useModal } from '@/components/Modal'; +import { FlockApi } from '@/services/api/master-data'; +import { Flock } from '@/types/api/master-data/flock'; +import { useRouter } from 'next/navigation'; +import { useEffect, useMemo, useState } from 'react'; +import { FlockFormSchema, FlockFormValues, UpdateFlockFormSchema } from './FlockForm.schema'; +import { useFormik } from 'formik'; +import Button from '@/components/Button'; +import { Icon } from '@iconify/react'; +import TextInput from '@/components/input/TextInput'; +import { cn } from '@/lib/helper'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; + +interface FlockCustomProps { + formType?: 'add' | 'edit' | 'detail'; + initialValues?: Flock; +} + +const FlockForm = ({ formType = 'add', initialValues }: FlockCustomProps) => { + const router = useRouter(); + const deleteModal = useModal(); + + // State + const [flockFormErrorMessage, setFlockFormErrorMessage] = useState(''); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + // Handler + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + + await FlockApi.delete(initialValues?.id as number); + + deleteModal.closeModal(); + setIsDeleteLoading(false); + router.push('/master-data/flock'); + }; + + // Initital Value + const formikInitialValue = useMemo(() => { + return { + name: initialValues?.name ?? '', + }; + }, [initialValues]); + + // Formik + const formik = useFormik({ + initialValues: formikInitialValue, + enableReinitialize: true, + validationSchema: formType === 'edit' ? UpdateFlockFormSchema : FlockFormSchema, + onSubmit: async (values) => { + // reset error message + setFlockFormErrorMessage(''); + + // create payload + const payload = { + name: values.name, + }; + + // cek type form yang disubmit + switch (formType) { + case 'add': + await FlockApi.create(payload); + break; + case 'edit': + await FlockApi.update(initialValues?.id as number, payload); + break; + default: + break; + } + + router.push('/master-data/flock'); + }, + }); + + // Initialize Formik + const { setValues: formikSetValues } = formik; + useEffect(() => { + formikSetValues(formikInitialValue); + }, [formikSetValues, formikInitialValue]); + + // Render + return ( + <> +
+
+ + +

+ {formType === 'add' && 'Tambah Flock'} + {formType === 'edit' && 'Ubah Flock'} + {formType === 'detail' && 'Detail Flock'} +

+
+
+ {/* Fields Form */} +
+ +
+ + {/* Action Button */} +
+ {formType !== 'add' && ( +
+ + {formType !== 'edit' && ( + + )} +
+ )} + + {formType !== 'detail' && ( +
+ + + +
+ )} +
+ + {flockFormErrorMessage && ( +
+ + {flockFormErrorMessage} +
+ )} +
+
+ + {formType !== 'add' && ( + + )} + + ); +}; + +export default FlockForm; diff --git a/src/config/constant.ts b/src/config/constant.ts index ed68adb5..97e4c285 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -77,6 +77,11 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [ link: '/master-data/supplier', icon: 'material-symbols:add-business-outline-rounded', }, + { + title: 'Flock', + link: '/master-data/flock', + icon: 'material-symbols:raven-outline-rounded', + }, ], }, { diff --git a/src/services/api/master-data.ts b/src/services/api/master-data.ts index dce528e7..854bb8f3 100644 --- a/src/services/api/master-data.ts +++ b/src/services/api/master-data.ts @@ -59,6 +59,11 @@ import { Fcr, UpdateFcrPayload, } from '@/types/api/master-data/fcr'; +import { + CreateFlockPayload, + Flock, + UpdateFlockPayload, +} from '@/types/api/master-data/flock'; export const UomApi = new BaseApiService< Uom, @@ -130,3 +135,9 @@ export const FcrApi = new BaseApiService< CreateFcrPayload, UpdateFcrPayload >('/master-data/fcrs'); + +export const FlockApi = new BaseApiService< + Flock, + CreateFlockPayload, + UpdateFlockPayload +>('/master-data/flocks'); \ No newline at end of file diff --git a/src/types/api/master-data/flock.d.ts b/src/types/api/master-data/flock.d.ts new file mode 100644 index 00000000..0c59b84c --- /dev/null +++ b/src/types/api/master-data/flock.d.ts @@ -0,0 +1,14 @@ +import { BaseMetadata } from "../api-general"; + +export type BaseFlock = { + id: number; + name: string; +} + +export type Flock = BaseMetadata & BaseFlock; + +export type CreateFlockPayload = { + name: string; +} + +export type UpdateFlockPayload = CreateFlockPayload; \ No newline at end of file