From 731bec5a94fc962a37b8730a4a49550d3a13c33f Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sun, 28 Dec 2025 04:28:02 +0700 Subject: [PATCH] feat(FE-337): slicing ui form finance and API integration --- src/app/finance/{ => add}/adjust/page.tsx | 0 src/app/finance/add/initial-balance/page.tsx | 8 +- src/app/finance/add/injection/page.tsx | 7 + .../detail/edit/initial-balance/page.tsx | 51 +++ .../finance/detail/edit/injection/page.tsx | 51 +++ src/app/finance/detail/edit/page.tsx | 52 +++ .../pages/finance/FinanceDetail.tsx | 83 +++- src/components/pages/finance/FinanceTable.tsx | 248 ++++++++++-- .../finance/add/FormFinanceAdd.schema.ts | 56 ++- .../pages/finance/add/FormFinanceAdd.tsx | 130 +++--- .../FormFinanceAddInitialBalance.schema.ts | 62 +++ .../FormFinanceAddInitialBalance.tsx | 378 ++++++++++++++++++ .../injection/FormFinanceInjection.schema.ts | 25 ++ .../add/injection/FormFinanceInjection.tsx | 251 ++++++++++++ .../adjust/FormFinanceAdjust.schema.ts | 0 .../finance/adjust/FormFinanceAdjust.tsx | 5 - .../adjustment/InventoryAdjustmentTable.tsx | 20 - src/config/constant.ts | 11 + src/config/route-permission.ts | 22 + src/services/api/finance.ts | 211 ++++++++-- src/types/api/finance/finance.d.ts | 43 +- 21 files changed, 1522 insertions(+), 192 deletions(-) rename src/app/finance/{ => add}/adjust/page.tsx (100%) create mode 100644 src/app/finance/add/injection/page.tsx create mode 100644 src/app/finance/detail/edit/initial-balance/page.tsx create mode 100644 src/app/finance/detail/edit/injection/page.tsx create mode 100644 src/app/finance/detail/edit/page.tsx create mode 100644 src/components/pages/finance/add/injection/FormFinanceInjection.schema.ts create mode 100644 src/components/pages/finance/add/injection/FormFinanceInjection.tsx delete mode 100644 src/components/pages/finance/adjust/FormFinanceAdjust.schema.ts delete mode 100644 src/components/pages/finance/adjust/FormFinanceAdjust.tsx diff --git a/src/app/finance/adjust/page.tsx b/src/app/finance/add/adjust/page.tsx similarity index 100% rename from src/app/finance/adjust/page.tsx rename to src/app/finance/add/adjust/page.tsx diff --git a/src/app/finance/add/initial-balance/page.tsx b/src/app/finance/add/initial-balance/page.tsx index 036cb049..fb3114ad 100644 --- a/src/app/finance/add/initial-balance/page.tsx +++ b/src/app/finance/add/initial-balance/page.tsx @@ -1,5 +1,7 @@ -const FinanceAddInitialBalance = () => { - return
Initial Balance
; +import FormFinanceAddInitialBalance from '@/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance'; + +const FinanceAddInitialBalancePage = () => { + return ; }; -export default FinanceAddInitialBalance; +export default FinanceAddInitialBalancePage; diff --git a/src/app/finance/add/injection/page.tsx b/src/app/finance/add/injection/page.tsx new file mode 100644 index 00000000..502df04b --- /dev/null +++ b/src/app/finance/add/injection/page.tsx @@ -0,0 +1,7 @@ +import FormFinanceInjection from '@/components/pages/finance/add/injection/FormFinanceInjection'; + +const FinanceAddInjectionPage = () => { + return ; +}; + +export default FinanceAddInjectionPage; diff --git a/src/app/finance/detail/edit/initial-balance/page.tsx b/src/app/finance/detail/edit/initial-balance/page.tsx new file mode 100644 index 00000000..fddb46d9 --- /dev/null +++ b/src/app/finance/detail/edit/initial-balance/page.tsx @@ -0,0 +1,51 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; +import { FinanceApi } from '@/services/api/finance'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import FormFinanceAddInitialBalance from '@/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance'; + +const EditFinanceInitialBalancePage = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const financeId = searchParams.get('financeId'); + + const { data: finance, isLoading: isLoadingFinance } = useSWR( + financeId, + (id: number) => FinanceApi.getSingle(id) + ); + + if (!financeId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingFinance && (!finance || isResponseError(finance))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingFinance && ( + + )} + + {!isLoadingFinance && ( + + )} +
+ ); +}; + +export default EditFinanceInitialBalancePage; diff --git a/src/app/finance/detail/edit/injection/page.tsx b/src/app/finance/detail/edit/injection/page.tsx new file mode 100644 index 00000000..a538ffd1 --- /dev/null +++ b/src/app/finance/detail/edit/injection/page.tsx @@ -0,0 +1,51 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; +import { FinanceApi } from '@/services/api/finance'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import FormFinanceInjection from '@/components/pages/finance/add/injection/FormFinanceInjection'; + +const EditFinanceInjectionPage = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const financeId = searchParams.get('financeId'); + + const { data: finance, isLoading: isLoadingFinance } = useSWR( + financeId, + (id: number) => FinanceApi.getSingle(id) + ); + + if (!financeId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingFinance && (!finance || isResponseError(finance))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingFinance && ( + + )} + + {!isLoadingFinance && ( + + )} +
+ ); +}; + +export default EditFinanceInjectionPage; diff --git a/src/app/finance/detail/edit/page.tsx b/src/app/finance/detail/edit/page.tsx new file mode 100644 index 00000000..93a0daea --- /dev/null +++ b/src/app/finance/detail/edit/page.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; +import { FinanceApi } from '@/services/api/finance'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import FormFinanceAdd from '@/components/pages/finance/add/FormFinanceAdd'; +import FormFinanceAddInitialBalance from '@/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance'; + +const EditFinanceTransactionPage = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const financeId = searchParams.get('financeId'); + + const { data: finance, isLoading: isLoadingFinance } = useSWR( + financeId, + (id: number) => FinanceApi.getSingle(id) + ); + + if (!financeId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingFinance && (!finance || isResponseError(finance))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingFinance && ( + + )} + + {!isLoadingFinance && ( + + )} +
+ ); +}; + +export default EditFinanceTransactionPage; diff --git a/src/components/pages/finance/FinanceDetail.tsx b/src/components/pages/finance/FinanceDetail.tsx index d022d20e..c7057efa 100644 --- a/src/components/pages/finance/FinanceDetail.tsx +++ b/src/components/pages/finance/FinanceDetail.tsx @@ -1,11 +1,27 @@ +import Button from '@/components/Button'; import Card from '@/components/Card'; import { FormHeader } from '@/components/helper/form/FormHeader'; +import RequirePermission from '@/components/helper/RequirePermission'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; import Table from '@/components/Table'; -import { formatCurrency, formatTitleCase } from '@/lib/helper'; +import { + FINANCE_INITIAL_BALANCE_STATUS, + FINANCE_TRANSACTION_STATUS, +} from '@/config/constant'; +import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper'; +import { FinanceApi } from '@/services/api/finance'; import { Finance } from '@/types/api/finance/finance'; +import { Icon } from '@iconify/react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import toast from 'react-hot-toast'; const FinanceDetail = ({ finance }: { finance: Finance }) => { + const router = useRouter(); + const deleteModal = useModal(); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); const informasiUmum = [ { label: 'ID', @@ -21,7 +37,7 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => { }, { label: 'Tanggal', - value: finance.payment_date, + value: formatDate(finance.payment_date, 'DD MMM yyyy'), }, { label: 'Metode Pembayaran', @@ -54,6 +70,18 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => { value: formatCurrency(finance.income_amount), }, ]; + + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + + await FinanceApi.delete(finance.id as number); + router.back(); + + deleteModal.closeModal(); + toast.success('Successfully delete Finance!'); + setIsDeleteLoading(false); + }; + return (
@@ -108,6 +136,57 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => { />
+ +
+ {FINANCE_TRANSACTION_STATUS.includes(finance.transaction_type) && ( + + + + )} + {FINANCE_INITIAL_BALANCE_STATUS.includes(finance.transaction_type) && ( + + + + )} + + + +
+ ); }; diff --git a/src/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index b4b4e60c..71ed6c84 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -1,5 +1,5 @@ import { ChangeEventHandler, useMemo, useState } from 'react'; -import { Row } from '@tanstack/react-table'; +import { CellContext, Row } from '@tanstack/react-table'; import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; @@ -16,14 +16,118 @@ import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; import Table from '@/components/Table'; import Tooltip from '@/components/Tooltip'; -import { formatCurrency, formatDate } from '@/lib/helper'; +import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { Finance } from '@/types/api/finance/finance'; -import { ROWS_OPTIONS } from '@/config/constant'; +import { + FINANCE_INITIAL_BALANCE_STATUS, + FINANCE_INJECTION_STATUS, + FINANCE_TRANSACTION_STATUS, + ROWS_OPTIONS, +} from '@/config/constant'; import { FinanceApi } from '@/services/api/finance'; import { isResponseSuccess } from '@/lib/api-helper'; import { BankApi, CustomerApi, SupplierApi } from '@/services/api/master-data'; import { Bank } from '@/types/api/master-data/bank'; +import { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import toast from 'react-hot-toast'; +import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import RequirePermission from '@/components/helper/RequirePermission'; +import { Icon } from '@iconify/react'; +import RowDropdownOptions from '@/components/table/RowDropdownOptions'; +import RowCollapseOptions from '@/components/table/RowCollapseOptions'; + +const RowOptionsMenu = ({ + type = 'dropdown', + props, + deleteClickHandler, +}: { + type: 'dropdown' | 'collapse'; + props: CellContext; + deleteClickHandler: () => void; +}) => { + return ( + + + + + + {FINANCE_TRANSACTION_STATUS.includes( + props.row.original.transaction_type + ) && ( + + + + )} + + {FINANCE_INITIAL_BALANCE_STATUS.includes( + props.row.original.transaction_type + ) && ( + + + + )} + + {FINANCE_INJECTION_STATUS.includes( + props.row.original.transaction_type + ) && ( + + + + )} + + + + + + ); +}; const FinanceTable = () => { const { @@ -56,6 +160,7 @@ const FinanceTable = () => { // ===== State ===== const [searchParams, setSearchParams] = useSearchParams(); + const deleteModal = useModal(); const [pendingFilters, setPendingFilters] = useState({ search: '', transactionType: '', @@ -72,6 +177,8 @@ const FinanceTable = () => { null ); const [selectedSortBy, setSelectedSortBy] = useState(null); + const [selectedFinance, setSelectedFinance] = useState(null); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); // ===== Fetch Data ===== const { @@ -79,7 +186,7 @@ const FinanceTable = () => { isLoading, mutate: refreshFinances, } = useSWR( - `${FinanceApi.basePath}${getTableFilterQueryString()}`, + `${FinanceApi.basePath}/transactions${getTableFilterQueryString()}`, FinanceApi.getAllFetcher ); @@ -193,6 +300,16 @@ const FinanceTable = () => { updateFilter('startDate', ''); updateFilter('endDate', ''); }; + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + + await FinanceApi.delete(selectedFinance?.id as number); + refreshFinances(); + + deleteModal.closeModal(); + toast.success('Successfully delete Finance!'); + setIsDeleteLoading(false); + }; const columns = useMemo(() => { return [ @@ -203,14 +320,30 @@ const FinanceTable = () => { { header: 'References Number', accessorKey: 'reference_number', + cell: (props: CellContext) => { + const value = props.row.original.reference_number; + return {value ?? '-'}; + }, }, { header: 'Jenis Transaksi', accessorKey: 'transaction_type', + cell: (props: CellContext) => { + const value = props.row.original.transaction_type + .split('_') + .join(' '); + return {formatTitleCase(value)}; + }, }, { header: 'Pihak', accessorFn: (finance: Finance) => finance.party.name, + cell: (props: CellContext) => { + if (props.row.original.party.id) { + return {props.row.original.party.name}; + } + return {'-'}; + }, }, { header: 'Tanggal', @@ -220,6 +353,10 @@ const FinanceTable = () => { { header: 'Metode Pembayaran', accessorKey: 'payment_method', + cell: (props: CellContext) => { + const value = props.row.original.payment_method.split('_').join(' '); + return {formatTitleCase(value)}; + }, }, { header: 'Bank', @@ -237,36 +374,73 @@ const FinanceTable = () => { }, { header: 'Aksi', - cell: ({ row }: { row: Row }) => ( - ...} - direction='bottom' - align='end' - > - - - - - ), + cell: (props: CellContext) => { + 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 = () => { + setSelectedFinance(props.row.original); + deleteModal.openModal(); + }; + + return ( + <> + {currentPageSize > 2 && ( + + + + )} + + {currentPageSize <= 2 && ( + + + + )} + + ); + }, }, ]; }, []); return (
- - - + + + + + + + + +
{ pageSize={tableFilterState.pageSize} page={tableFilterState.page} onPageChange={setPage} + onPageSizeChange={setPageSize} + totalItems={ + isResponseSuccess(finances) ? finances.meta?.total_results : 0 + } isLoading={isLoading} /> +
); }; diff --git a/src/components/pages/finance/add/FormFinanceAdd.schema.ts b/src/components/pages/finance/add/FormFinanceAdd.schema.ts index c12a3159..9aff81b9 100644 --- a/src/components/pages/finance/add/FormFinanceAdd.schema.ts +++ b/src/components/pages/finance/add/FormFinanceAdd.schema.ts @@ -15,18 +15,6 @@ import { OptionType } from '@/components/input/SelectInput'; } */ -// Type for API payload (what gets sent to the server) -export type FinancePayload = { - party_id: number; - party_type: string; - payment_date: string; - payment_method: string; - bank_id: number; - reference_number: string; - nominal: number; - notes: string; -}; - // Type for form values (includes option objects for SelectInput) export type FinanceFormValues = { party_type_option: OptionType | null; @@ -41,32 +29,36 @@ export type FinanceFormValues = { }; export const FinanceFormSchema = Yup.object().shape({ - party_type_option: Yup.object({ - label: Yup.string().required('Wajib Diisi'), - value: Yup.string().required('Wajib Diisi'), - }) + party_type_option: Yup.mixed() .nullable() - .required('Jenis transaksi wajib diisi'), - party_id_option: Yup.object({ - label: Yup.string().required('Wajib Diisi'), - value: Yup.number().required('Wajib Diisi'), - }) + .test( + 'is-valid-option', + 'Jenis transaksi wajib diisi', + (value) => value !== null && value !== undefined + ), + party_id_option: Yup.mixed() .nullable() - .required('Pihak wajib diisi'), + .test( + 'is-valid-option', + 'Pihak wajib diisi', + (value) => value !== null && value !== undefined + ), party_account_number: Yup.string().required('Nomor rekening wajib diisi'), payment_date: Yup.string().required('Tanggal pembayaran wajib diisi'), - payment_method_option: Yup.object({ - label: Yup.string().required('Wajib Diisi'), - value: Yup.string().required('Wajib Diisi'), - }) + payment_method_option: Yup.mixed() .nullable() - .required('Metode pembayaran wajib diisi'), - bank_id_option: Yup.object({ - label: Yup.string().required('Wajib Diisi'), - value: Yup.number().required('Wajib Diisi'), - }) + .test( + 'is-valid-option', + 'Metode pembayaran wajib diisi', + (value) => value !== null && value !== undefined + ), + bank_id_option: Yup.mixed() .nullable() - .required('Bank wajib diisi'), + .test( + 'is-valid-option', + 'Bank wajib diisi', + (value) => value !== null && value !== undefined + ), reference_number: Yup.string().required('Nomor referensi wajib diisi'), nominal: Yup.string().required('Nominal wajib diisi'), notes: Yup.string().required('Catatan wajib diisi'), diff --git a/src/components/pages/finance/add/FormFinanceAdd.tsx b/src/components/pages/finance/add/FormFinanceAdd.tsx index 9a55665d..9b8259be 100644 --- a/src/components/pages/finance/add/FormFinanceAdd.tsx +++ b/src/components/pages/finance/add/FormFinanceAdd.tsx @@ -14,16 +14,20 @@ import TextInput from '@/components/input/TextInput'; import { FinanceFormSchema, FinanceFormValues, - FinancePayload, } from '@/components/pages/finance/add/FormFinanceAdd.schema'; import { FINANCE_PARTY_TYPE_OPTIONS, FINANCE_PAYMENT_METHOD_OPTIONS, } from '@/config/constant'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { formatTitleCase } from '@/lib/helper'; +import { formatDate, formatTitleCase } from '@/lib/helper'; import { FinanceApi } from '@/services/api/finance'; import { BankApi, CustomerApi, SupplierApi } from '@/services/api/master-data'; +import { + CreateFinancePayment, + Finance, + UpdateFinancePayment, +} from '@/types/api/finance/finance'; import { Bank } from '@/types/api/master-data/bank'; import { useFormik } from 'formik'; import { useRouter } from 'next/navigation'; @@ -32,7 +36,7 @@ import toast from 'react-hot-toast'; interface FormFinanceAddProps { type?: 'add' | 'edit'; - initialValues?: FinanceFormValues & { id?: number }; + initialValues?: Finance; } const FormFinanceAdd = ({ @@ -44,14 +48,28 @@ const FormFinanceAdd = ({ // ===== Formik ===== const formikInitialValues = useMemo((): FinanceFormValues => { return { - party_type_option: initialValues?.party_type_option || null, - party_id_option: initialValues?.party_id_option || null, + party_type_option: + FINANCE_PARTY_TYPE_OPTIONS.find( + (option) => option.value === initialValues?.party.type + ) || null, + party_id_option: { + label: initialValues?.party.name || '', + value: initialValues?.party.id || 0, + }, payment_date: initialValues?.payment_date || '', - payment_method_option: initialValues?.payment_method_option || null, - bank_id_option: initialValues?.bank_id_option || null, - party_account_number: initialValues?.party_account_number || '', + payment_method_option: + FINANCE_PAYMENT_METHOD_OPTIONS.find( + (option) => option.value === initialValues?.payment_method + ) || null, + bank_id_option: initialValues?.bank + ? { + label: initialValues.bank.name, + value: initialValues.bank.id, + } + : null, + party_account_number: initialValues?.party.account_number || '', reference_number: initialValues?.reference_number || '', - nominal: initialValues?.nominal || '', + nominal: initialValues?.nominal.toString() || '', notes: initialValues?.notes || '', }; }, [initialValues]); @@ -59,6 +77,8 @@ const FormFinanceAdd = ({ const formik = useFormik({ initialValues: formikInitialValues, validationSchema: FinanceFormSchema, + validateOnChange: true, + validateOnBlur: true, onSubmit: async (values) => { const payload = transformFormValuesToPayload(values); @@ -96,11 +116,11 @@ const FormFinanceAdd = ({ // ===== Helper Functions ===== const transformFormValuesToPayload = ( values: FinanceFormValues - ): FinancePayload => { + ): CreateFinancePayment => { return { party_id: Number(values.party_id_option?.value) || 0, party_type: (values.party_type_option?.value as string) || '', - payment_date: values.payment_date, + payment_date: formatDate(values.payment_date, 'YYYY-MM-DD'), payment_method: (values.payment_method_option?.value as string) || '', bank_id: Number(values.bank_id_option?.value) || 0, reference_number: values.reference_number, @@ -111,7 +131,7 @@ const FormFinanceAdd = ({ // ===== Handler ===== const createFinance = useCallback( - async (payload: FinancePayload) => { + async (payload: CreateFinancePayment) => { const response = await FinanceApi.create(payload); if (isResponseError(response)) { @@ -126,7 +146,7 @@ const FormFinanceAdd = ({ [router] ); const updateFinance = useCallback( - async (financeId: number, payload: FinancePayload) => { + async (financeId: number, payload: UpdateFinancePayment) => { const response = await FinanceApi.update(financeId, payload); if (isResponseError(response)) { @@ -145,7 +165,10 @@ const FormFinanceAdd = ({ <>
- +
{ formik.setFieldValue('party_type_option', value); - formik.setFieldTouched('party_type_option', true); }} - isError={ - !!( - formik.touched.party_type_option && + isError={Boolean( + formik.touched.party_type_option && formik.errors.party_type_option - ) - } + )} errorMessage={ formik.touched.party_type_option && formik.errors.party_type_option - ? String(formik.errors.party_type_option) + ? formik.errors.party_type_option : '' } required @@ -184,18 +204,14 @@ const FormFinanceAdd = ({ value={formik.values.party_id_option} onChange={(value) => { formik.setFieldValue('party_id_option', value); - formik.setFieldTouched('party_id_option', true); }} isLoading={isLoadingPartyOptions} - isError={ - !!( - formik.touched.party_id_option && - formik.errors.party_id_option - ) - } + isError={Boolean( + formik.touched.party_id_option && formik.errors.party_id_option + )} errorMessage={ formik.touched.party_id_option && formik.errors.party_id_option - ? String(formik.errors.party_id_option) + ? formik.errors.party_id_option : '' } required @@ -209,9 +225,9 @@ const FormFinanceAdd = ({ value={formik.values.payment_date} onChange={formik.handleChange} onBlur={formik.handleBlur} - isError={ - !!(formik.touched.payment_date && formik.errors.payment_date) - } + isError={Boolean( + formik.touched.payment_date && formik.errors.payment_date + )} errorMessage={ formik.touched.payment_date && formik.errors.payment_date ? formik.errors.payment_date @@ -226,18 +242,15 @@ const FormFinanceAdd = ({ value={formik.values.payment_method_option} onChange={(value) => { formik.setFieldValue('payment_method_option', value); - formik.setFieldTouched('payment_method_option', true); }} - isError={ - !!( - formik.touched.payment_method_option && + isError={Boolean( + formik.touched.payment_method_option && formik.errors.payment_method_option - ) - } + )} errorMessage={ formik.touched.payment_method_option && formik.errors.payment_method_option - ? String(formik.errors.payment_method_option) + ? formik.errors.payment_method_option : '' } required @@ -268,17 +281,14 @@ const FormFinanceAdd = ({ value={formik.values.bank_id_option} onChange={(value) => { formik.setFieldValue('bank_id_option', value); - formik.setFieldTouched('bank_id_option', true); }} isLoading={isLoadingBankOptions} - isError={ - !!( - formik.touched.bank_id_option && formik.errors.bank_id_option - ) - } + isError={Boolean( + formik.touched.bank_id_option && formik.errors.bank_id_option + )} errorMessage={ formik.touched.bank_id_option && formik.errors.bank_id_option - ? String(formik.errors.bank_id_option) + ? formik.errors.bank_id_option : '' } required @@ -291,12 +301,10 @@ const FormFinanceAdd = ({ value={formik.values.party_account_number} onChange={formik.handleChange} onBlur={formik.handleBlur} - isError={ - !!( - formik.touched.party_account_number && + isError={Boolean( + formik.touched.party_account_number && formik.errors.party_account_number - ) - } + )} errorMessage={ formik.touched.party_account_number && formik.errors.party_account_number @@ -312,12 +320,10 @@ const FormFinanceAdd = ({ value={formik.values.reference_number} onChange={formik.handleChange} onBlur={formik.handleBlur} - isError={ - !!( - formik.touched.reference_number && + isError={Boolean( + formik.touched.reference_number && formik.errors.reference_number - ) - } + )} errorMessage={ formik.touched.reference_number && formik.errors.reference_number @@ -333,7 +339,7 @@ const FormFinanceAdd = ({ value={formik.values.nominal} onChange={formik.handleChange} onBlur={formik.handleBlur} - isError={!!(formik.touched.nominal && formik.errors.nominal)} + isError={Boolean(formik.touched.nominal && formik.errors.nominal)} errorMessage={ formik.touched.nominal && formik.errors.nominal ? formik.errors.nominal @@ -348,12 +354,13 @@ const FormFinanceAdd = ({ value={formik.values.notes} onChange={formik.handleChange} onBlur={formik.handleBlur} - isError={!!(formik.touched.notes && formik.errors.notes)} + isError={Boolean(formik.touched.notes && formik.errors.notes)} errorMessage={ formik.touched.notes && formik.errors.notes ? formik.errors.notes : '' } + required />
-
diff --git a/src/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance.schema.ts b/src/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance.schema.ts index e69de29b..c700f973 100644 --- a/src/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance.schema.ts +++ b/src/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance.schema.ts @@ -0,0 +1,62 @@ +import * as Yup from 'yup'; +import { OptionType } from '@/components/input/SelectInput'; + +/** + * API Payload format for Initial Balance: + * { + "party_type": "CUSTOMER", + "party_id": 1, + "bank_id": 1, + "reference_number": "IB.MBU.001", + "initial_balance_type": "DEBIT", + "nominal": 5000000, + "note": "Saldo awal piutang customer" + } + */ + +// Type for form values (includes option objects for SelectInput) +export type InitialBalanceFormValues = { + party_type_option: OptionType | null; + party_id_option: OptionType | null; + bank_id_option: OptionType | null; + reference_number: string; + initial_balance_type_option: OptionType | null; + nominal: string; + note: string; +}; + +export const InitialBalanceFormSchema = Yup.object().shape({ + party_type_option: Yup.mixed() + .nullable() + .test( + 'is-valid-option', + 'Jenis pihak wajib diisi', + (value) => value !== null && value !== undefined + ), + party_id_option: Yup.mixed() + .nullable() + .test( + 'is-valid-option', + 'Pihak wajib diisi', + (value) => value !== null && value !== undefined + ), + bank_id_option: Yup.mixed() + .nullable() + .test( + 'is-valid-option', + 'Bank wajib diisi', + (value) => value !== null && value !== undefined + ), + reference_number: Yup.string().required('Nomor referensi wajib diisi'), + initial_balance_type_option: Yup.mixed() + .nullable() + .test( + 'is-valid-option', + 'Tipe saldo awal wajib diisi', + (value) => value !== null && value !== undefined + ), + nominal: Yup.string().required('Nominal wajib diisi'), + note: Yup.string().required('Catatan wajib diisi'), +}); + +export const UpdateInitialBalanceFormSchema = InitialBalanceFormSchema; diff --git a/src/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance.tsx b/src/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance.tsx index e69de29b..aa185647 100644 --- a/src/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance.tsx +++ b/src/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance.tsx @@ -0,0 +1,378 @@ +'use client'; + +import Button from '@/components/Button'; +import { FormHeader } from '@/components/helper/form/FormHeader'; +import NumberInput from '@/components/input/NumberInput'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; +import TextArea from '@/components/input/TextArea'; +import TextInput from '@/components/input/TextInput'; +import { + InitialBalanceFormSchema, + InitialBalanceFormValues, +} from '@/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance.schema'; +import { + FINANCE_INITIAL_BALANCE_TYPE_OPTIONS, + FINANCE_PARTY_TYPE_OPTIONS, +} from '@/config/constant'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { formatTitleCase } from '@/lib/helper'; +import { FinanceApi } from '@/services/api/finance'; +import { BankApi, CustomerApi, SupplierApi } from '@/services/api/master-data'; +import { + CreateInitialBalance, + Finance, + UpdateInitialBalance, +} from '@/types/api/finance/finance'; +import { Bank } from '@/types/api/master-data/bank'; +import { Icon } from '@iconify/react'; +import { useFormik } from 'formik'; +import { useRouter } from 'next/navigation'; +import { useCallback, useMemo } from 'react'; +import toast from 'react-hot-toast'; + +interface FormFinanceAddInitialBalanceProps { + type?: 'add' | 'edit'; + initialValues?: Finance; +} + +const FormFinanceAddInitialBalance = ({ + type = 'add', + initialValues, +}: FormFinanceAddInitialBalanceProps) => { + const router = useRouter(); + + // ===== Formik ===== + const formikInitialValues = useMemo((): InitialBalanceFormValues => { + // Type assertion to handle potential initial_balance_type field + const extendedInitialValues = initialValues as Finance & { + initial_balance_type?: string; + }; + + return { + party_type_option: + FINANCE_PARTY_TYPE_OPTIONS.find( + (option) => option.value === initialValues?.party.type + ) || null, + party_id_option: initialValues?.party + ? { + label: initialValues.party.name, + value: initialValues.party.id, + } + : null, + bank_id_option: initialValues?.bank + ? { + label: initialValues.bank.name, + value: initialValues.bank.id, + } + : null, + reference_number: initialValues?.reference_number || '', + initial_balance_type_option: + (initialValues?.nominal ?? 0) < 0 + ? FINANCE_INITIAL_BALANCE_TYPE_OPTIONS.find( + (option) => option.value === 'NEGATIVE' + ) || null + : FINANCE_INITIAL_BALANCE_TYPE_OPTIONS.find( + (option) => option.value === 'POSITIVE' + ) || null, + nominal: initialValues?.nominal?.toString() || '', + note: initialValues?.notes || '', + }; + }, [initialValues]); + + const formik = useFormik({ + initialValues: formikInitialValues, + validationSchema: InitialBalanceFormSchema, + validateOnChange: true, + validateOnBlur: true, + onSubmit: async (values) => { + const payload = transformFormValuesToPayload(values); + + switch (type) { + case 'add': + await createInitialBalance(payload); + break; + + case 'edit': + if (initialValues?.id) { + await updateInitialBalance(initialValues.id, payload); + } + break; + } + }, + }); + + // ===== Options ===== + const { options: partyOptions, isLoadingOptions: isLoadingPartyOptions } = + useSelect( + formik.values.party_type_option?.value === 'CUSTOMER' + ? CustomerApi.basePath + : SupplierApi.basePath, + 'id', + 'name', + '', + { limit: 'limit' } + ); + const { + options: bankOptions, + rawData: bankRawData, + isLoadingOptions: isLoadingBankOptions, + } = useSelect(BankApi.basePath, 'id', 'name', '', { limit: 'limit' }); + + // ===== Helper Functions ===== + const transformFormValuesToPayload = ( + values: InitialBalanceFormValues + ): CreateInitialBalance => { + return { + party_type: (values.party_type_option?.value as string) || '', + party_id: Number(values.party_id_option?.value) || 0, + bank_id: Number(values.bank_id_option?.value) || 0, + reference_number: values.reference_number, + initial_balance_type: + (values.initial_balance_type_option?.value as string) || '', + nominal: Number(values.nominal.replace(/\D/g, '')) || 0, + note: values.note, + }; + }; + + // ===== Handler ===== + const createInitialBalance = useCallback( + async (payload: CreateInitialBalance) => { + const response = await FinanceApi.createInitialBalances(payload); + + if (isResponseError(response)) { + toast.error(response.message); + return; + } + + toast.success('Saldo awal berhasil ditambahkan'); + router.refresh(); + router.push('/finance'); + }, + [router] + ); + + const updateInitialBalance = useCallback( + async (financeId: number, payload: UpdateInitialBalance) => { + const response = await FinanceApi.updateInitialBalances( + financeId, + payload + ); + + if (isResponseError(response)) { + toast.error(response.message); + return; + } + + toast.success('Saldo awal berhasil diperbarui'); + router.refresh(); + router.push('/finance'); + }, + [router] + ); + + return ( + <> +
+
+ + + { + formik.setFieldValue('party_type_option', value); + }} + isError={Boolean( + formik.touched.party_type_option && + formik.errors.party_type_option + )} + errorMessage={ + formik.touched.party_type_option && + formik.errors.party_type_option + ? formik.errors.party_type_option + : '' + } + required + isClearable + /> + { + formik.setFieldValue('party_id_option', value); + }} + isLoading={isLoadingPartyOptions} + isError={Boolean( + formik.touched.party_id_option && formik.errors.party_id_option + )} + errorMessage={ + formik.touched.party_id_option && formik.errors.party_id_option + ? formik.errors.party_id_option + : '' + } + required + isClearable + isDisabled={!formik.values.party_type_option?.value} + /> + ({ + label: + bankRawData.data?.find( + (item) => item.id === option.value + )?.alias + + ' - ' + + bankRawData.data?.find( + (item) => item.id === option.value + )?.account_number + + ' - ' + + bankRawData.data?.find( + (item) => item.id === option.value + )?.owner, + value: option.value, + })) + : [] + } + value={formik.values.bank_id_option} + onChange={(value) => { + formik.setFieldValue('bank_id_option', value); + }} + isLoading={isLoadingBankOptions} + isError={Boolean( + formik.touched.bank_id_option && formik.errors.bank_id_option + )} + errorMessage={ + formik.touched.bank_id_option && formik.errors.bank_id_option + ? formik.errors.bank_id_option + : '' + } + required + isClearable + /> + + { + formik.setFieldValue('initial_balance_type_option', value); + }} + isError={Boolean( + formik.touched.initial_balance_type_option && + formik.errors.initial_balance_type_option + )} + errorMessage={ + formik.touched.initial_balance_type_option && + formik.errors.initial_balance_type_option + ? formik.errors.initial_balance_type_option + : '' + } + required + isClearable + /> + + ) : formik.values.initial_balance_type_option?.value === + 'NEGATIVE' ? ( + + ) : ( + '' + ) + } + required + /> +