diff --git a/src/app/page.tsx b/src/app/page.tsx index 9cc0177d..33d01de7 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -25,5 +25,9 @@ export default function Home() { ); } - return <>Loading...; + return ( +
+ +
+ ); } diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 0e095c1f..0be39fb5 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -1,11 +1,12 @@ 'use client'; -import { ReactNode, useCallback, useEffect, useState } from 'react'; +import { Fragment, ReactNode, useCallback, useEffect, useState } from 'react'; import { flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, + getExpandedRowModel, getSortedRowModel, TableOptions, useReactTable, @@ -15,6 +16,7 @@ import { OnChangeFn, Row, HeaderContext, + ExpandedState, } from '@tanstack/react-table'; import { rankItem } from '@tanstack/match-sorter-utils'; import { Icon } from '@iconify/react'; @@ -33,6 +35,9 @@ interface TableClassNames { bodyRowClassName?: string; selectedBodyRowClassName?: string; bodyColumnClassName?: string; + bodySubRowClassName?: (depth: number) => string; + selectedBodySubRowClassName?: (depth: number) => string; + bodySubRowColumnClassName?: (depth: number) => string; tableFooterClassName?: string; footerRowClassName?: string; footerColumnClassName?: string; @@ -60,6 +65,7 @@ export interface TableProps { enableRowSelection?: boolean | ((row: Row) => boolean); renderFooter?: boolean; withCheckbox?: boolean; + withPagination?: boolean; rowOptions?: number[]; /** * Custom row renderer. Should return a complete element or null. @@ -67,6 +73,10 @@ export interface TableProps { * Return null to render the default row. */ renderCustomRow?: (row: Row) => ReactNode | null; + getRowCanExpand?: (row: Row) => boolean; + renderSubComponent?: (props: { row: Row }) => React.ReactElement; + expanded?: ExpandedState; + getSubRows?: (originalRow: TData, index: number) => TData[] | undefined; } const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; @@ -92,7 +102,12 @@ export const TABLE_DEFAULT_STYLING = { bodyRowClassName: 'transition-all duration-200 border-t border-base-content/10 bg-transparent', selectedBodyRowClassName: 'bg-primary/5', - bodyColumnClassName: 'px-4 py-3 text-base-content', + bodyColumnClassName: 'px-4 py-3 text-base-content font-medium', + bodySubRowClassName: (depth: number) => + 'transition-all duration-200 border-t border-base-content/10 bg-transparent', + selectedBodySubRowClassName: (depth: number) => 'bg-primary/5', + bodySubRowColumnClassName: (depth: number) => + 'px-4 py-3 text-base-content font-medium', paginationClassName: 'px-3', tableFooterClassName: 'font-semibold border-base-content/10', footerRowClassName: 'bg-base-200 border-t-2 border-base-content/10', @@ -120,8 +135,13 @@ const Table = ({ enableRowSelection, renderFooter = false, withCheckbox = false, + withPagination = true, rowOptions = [10, 20, 50, 100], renderCustomRow, + getRowCanExpand, + renderSubComponent, + expanded = {}, + getSubRows, }: TableProps) => { const isServerSideTable = totalItems !== undefined && @@ -154,10 +174,14 @@ const Table = ({ getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel(), onPaginationChange: setPagination, + getExpandedRowModel: getExpandedRowModel(), + getRowCanExpand: getRowCanExpand ?? (getSubRows ? undefined : () => false), + getSubRows, manualSorting, state: { pagination, globalFilter: fuzzySearchValue, + expanded, }, filterFns: { fuzzy: fuzzyFilter, @@ -228,7 +252,10 @@ const Table = ({
({ } return ( - - {row.getVisibleCells().map((cell) => ( - - {!isLoading && - flexRender( - cell.column.columnDef.cell, - cell.getContext() + + 0 + ? tableClassNames.bodySubRowClassName(row.depth) + : tableClassNames.bodyRowClassName, + { + [tableClassNames.selectedBodyRowClassName!]: + row.getIsSelected() && row.depth === 0, + [tableClassNames.selectedBodySubRowClassName( + row.depth + )!]: row.getIsSelected() && row.depth > 0, + } + )} + > + {row.getVisibleCells().map((cell) => ( + 0 + ? tableClassNames.bodySubRowColumnClassName( + row.depth + ) + : tableClassNames.bodyColumnClassName )} + > + {!isLoading && + flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} - {isLoading &&
} - - ))} - + {isLoading &&
} + + ))} + + + {row.getIsExpanded() && ( + <> + {renderSubComponent && ( + + + {renderSubComponent({ row })} + + + )} + + )} + ); })} @@ -425,30 +483,33 @@ const Table = ({ !isLoading && emptyContent} - {data.length > 0 && table.getRowModel().rows.length > 0 && !isLoading && ( -
- -
- )} + {data.length > 0 && + table.getRowModel().rows.length > 0 && + !isLoading && + withPagination && ( +
+ +
+ )}
); }; diff --git a/src/components/helper/StatusBadge.tsx b/src/components/helper/StatusBadge.tsx index 46b36559..c4f99593 100644 --- a/src/components/helper/StatusBadge.tsx +++ b/src/components/helper/StatusBadge.tsx @@ -40,7 +40,7 @@ const StatusBadge = ({ xmlns='http://www.w3.org/2000/svg' className={cn({ 'text-base-content/10': color === 'neutral', - 'text-success': color === 'success', + 'text-[#008000]': color === 'success', 'text-error': color === 'error', 'text-primary': color === 'info', })} diff --git a/src/components/modal/ConfirmationModal.tsx b/src/components/modal/ConfirmationModal.tsx index 98a0b51d..89b8fbd9 100644 --- a/src/components/modal/ConfirmationModal.tsx +++ b/src/components/modal/ConfirmationModal.tsx @@ -167,49 +167,67 @@ const ConfirmationModal = ({ {children &&
{children}
} -
- {secondaryButton && secondaryButton.text && ( - - )} + {(secondaryButton || primaryButton) && ( +
+ {secondaryButton && secondaryButton.text && ( + + )} - {primaryButton && primaryButton.text && ( - - )} -
+ {primaryButton && primaryButton.text && ( + + )} +
+ )}
); diff --git a/src/components/pages/finance/FinanceDetail.tsx b/src/components/pages/finance/FinanceDetail.tsx index 3ee9a7df..622fff6f 100644 --- a/src/components/pages/finance/FinanceDetail.tsx +++ b/src/components/pages/finance/FinanceDetail.tsx @@ -30,7 +30,7 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => { }, { label: 'Jenis Transaksi', - value: finance.transaction_type, + value: formatTitleCase(finance.transaction_type.split('_').join(' ')), }, { label: 'Pihak', @@ -56,7 +56,9 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => { }, { label: 'Nomor Rekening', - value: `${finance.bank?.alias} - ${finance.bank?.account_number} - ${finance.bank?.owner}`, + value: finance.bank?.alias + ? `${finance.bank?.alias} - ${finance.bank?.account_number} - ${finance.bank?.owner}` + : '-', }, { label: `Rekening ${formatTitleCase(finance.party?.type)}`, @@ -64,7 +66,11 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => { }, { label: 'Nominal', - value: formatCurrency(Math.abs(finance.nominal)), + value: formatCurrency( + finance.transaction_type === 'INJECTION' + ? finance.nominal + : Math.abs(finance.nominal) + ), }, ].filter((item) => { // Hide party account number row if transaction type is INJECTION diff --git a/src/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index d189195a..c1c7f079 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -416,7 +416,8 @@ const FinanceTable = () => { }, { header: 'Pemasukan (Rp)', - accessorFn: (finance: Finance) => formatCurrency(finance.income_amount), + accessorFn: (finance: Finance) => + formatCurrency(Math.abs(finance.income_amount)), }, { header: 'Aksi', diff --git a/src/components/pages/finance/add/injection/FormFinanceInjection.schema.ts b/src/components/pages/finance/add/injection/FormFinanceInjection.schema.ts index c6118af6..304bac18 100644 --- a/src/components/pages/finance/add/injection/FormFinanceInjection.schema.ts +++ b/src/components/pages/finance/add/injection/FormFinanceInjection.schema.ts @@ -5,6 +5,7 @@ import * as Yup from 'yup'; export type InjectionFormValues = { bank_id_option: OptionType | null; adjustment_date: string; + injection_type?: OptionType | null | undefined; nominal: string; note: string; }; @@ -18,6 +19,7 @@ export const InjectionFormSchema = Yup.object({ (value) => value !== null && value !== undefined ), adjustment_date: Yup.string().required('Tanggal penyesuaian wajib diisi'), + injection_type: Yup.mixed().nullable().required('Tipe injeksi wajib diisi'), nominal: Yup.string().required('Nominal wajib diisi'), note: Yup.string().required('Catatan wajib diisi'), }); diff --git a/src/components/pages/finance/add/injection/FormFinanceInjection.tsx b/src/components/pages/finance/add/injection/FormFinanceInjection.tsx index ff49005c..a4b77baf 100644 --- a/src/components/pages/finance/add/injection/FormFinanceInjection.tsx +++ b/src/components/pages/finance/add/injection/FormFinanceInjection.tsx @@ -28,6 +28,10 @@ import { useCallback, useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import Alert from '@/components/Alert'; import { Icon } from '@iconify/react'; +import { + FINANCE_INJECTION_STATUS, + FINANCE_INJECTION_TYPE_OPTIONS, +} from '@/config/constant'; interface FormFinanceInjectionProps { type?: 'add' | 'edit'; @@ -51,6 +55,11 @@ const FormFinanceInjection = ({ } : null, adjustment_date: initialValues?.payment_date || '', + injection_type: initialValues?.nominal + ? initialValues.nominal < 0 + ? FINANCE_INJECTION_TYPE_OPTIONS[1] + : FINANCE_INJECTION_TYPE_OPTIONS[0] + : FINANCE_INJECTION_TYPE_OPTIONS[0], nominal: initialValues?.nominal?.toString() || '', note: initialValues?.notes || '', }; @@ -94,7 +103,10 @@ const FormFinanceInjection = ({ return { bank_id: Number(values.bank_id_option?.value) || 0, adjustment_date: formatDate(values.adjustment_date, 'YYYY-MM-DD'), - nominal: Number(values.nominal.replace(/\D/g, '')) || 0, + nominal: + values.injection_type?.value == 'POSITIVE' + ? Math.abs(Number(values.nominal)) + : -Math.abs(Number(values.nominal)), notes: values.note, }; }; @@ -203,6 +215,24 @@ const FormFinanceInjection = ({ } required /> + { + formik.setFieldValue('injection_type', value); + }} + isError={Boolean( + formik.touched.injection_type && formik.errors.injection_type + )} + errorMessage={ + formik.touched.injection_type && formik.errors.injection_type + ? formik.errors.injection_type + : '' + } + required + /> + ) : formik.values.injection_type?.value === 'NEGATIVE' ? ( + + ) : ( + '' + ) + } />