Compare commits

...

12 Commits

8 changed files with 1282 additions and 1 deletions
+16
View File
@@ -0,0 +1,16 @@
import FinanceForm from '@/components/pages/finance/form/FinanceForm';
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Finance | Add',
};
const FinanceAddPage = () => {
return (
<div className='w-full p-4 flex flex-row justify-center'>
<FinanceForm formType='add' />
</div>
);
};
export default FinanceAddPage;
+11
View File
@@ -0,0 +1,11 @@
import FinancesTable from '@/components/pages/finance/FinancesTable';
const Finance = () => {
return (
<section className='w-full p-4'>
<FinancesTable />
</section>
);
};
export default Finance;
@@ -0,0 +1,472 @@
'use client';
import { ChangeEventHandler, useEffect, useState } from 'react';
import useSWR from 'swr';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import Button from '@/components/Button';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import DateInput from '@/components/input/DateInput';
import { cn, formatCurrency, formatDate } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { FinanceApi } from '@/services/api/finance';
import { Finance } from '@/types/api/finance';
import {
BankApi,
CustomerApi,
KandangApi,
SupplierApi,
} from '@/services/api/master-data';
import { Customer } from '@/types/api/master-data/customer';
import { Supplier } from '@/types/api/master-data/supplier';
import { Kandang } from '@/types/api/master-data/kandang';
import { Bank } from '@/types/api/master-data/bank';
type FinanceTableFilter = {
search: string;
nameSort: string;
transactionType: string;
customerId: string;
supplierId: string;
kandangId: string;
bankId: string;
sortBy: string;
startDate: string;
endDate: string;
};
const TRANSACTION_TYPE_OPTIONS = [
{ value: 'REVENUE', label: 'Pemasukan' },
{ value: 'EXPENSE', label: 'Pengeluaran' },
];
const SORT_OPTIONS = [
{ value: 'payment_date', label: 'Tanggal Pembayaran' },
{ value: 'created_date', label: 'Tanggal Dibuat' },
];
const RowOptionsMenu = ({
type = 'dropdown',
props,
}: {
type: 'dropdown' | 'collapse';
props: CellContext<Finance, unknown>;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<div className='w-full max-h-40 overflow-auto flex flex-col gap-1'>
<Button
href={`/finance/edit/${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit
</Button>
<Button
variant='ghost'
color='error'
className='justify-start text-sm'
// Implement delete handler later
>
<Icon icon='mdi:delete-outline' width={16} height={16} />
Delete
</Button>
</div>
</RowOptionsMenuWrapper>
);
};
const FinancesTable = () => {
const {
state: tableFilterState,
updateFilter,
reset: resetFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter<FinanceTableFilter>({
initial: {
search: '',
nameSort: '',
transactionType: '',
customerId: '',
supplierId: '',
kandangId: '',
bankId: '',
sortBy: '',
startDate: '',
endDate: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
nameSort: 'sort_name',
transactionType: 'transaction_type',
customerId: 'customer_id',
supplierId: 'supplier_id',
kandangId: 'kandang_id',
bankId: 'bank_id',
sortBy: 'sort_by',
startDate: 'start_date',
endDate: 'end_date',
},
});
const { data: finances, isLoading: isLoadingFinances } = useSWR(
`${FinanceApi.basePath}${getTableFilterQueryString()}`,
FinanceApi.getAllFetcher
);
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
// Filter Selection States
const [selectedTransactionType, setSelectedTransactionType] =
useState<OptionType | null>(null);
const [selectedCustomer, setSelectedCustomer] = useState<OptionType | null>(
null
);
const [selectedSupplier, setSelectedSupplier] = useState<OptionType | null>(
null
);
const [selectedKandang, setSelectedKandang] = useState<OptionType | null>(
null
);
const [selectedBank, setSelectedBank] = useState<OptionType | null>(null);
const [selectedSortBy, setSelectedSortBy] = useState<OptionType | null>(null);
// APIs for SelectInputs
const {
options: customerOptions,
isLoadingOptions: isLoadingCustomerOptions,
setInputValue: setCustomerInputValue,
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
const {
options: supplierOptions,
isLoadingOptions: isLoadingSupplierOptions,
setInputValue: setSupplierInputValue,
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
const {
options: kandangOptions,
isLoadingOptions: isLoadingKandangOptions,
setInputValue: setKandangInputValue,
} = useSelect<Kandang>(KandangApi.basePath, 'id', 'name');
const {
options: bankOptions,
isLoadingOptions: isLoadingBankOptions,
setInputValue: setBankInputValue,
} = useSelect<Bank>(BankApi.basePath, 'id', 'name');
const financeColumns: ColumnDef<Finance>[] = [
{
header: '#',
cell: (props) => props.row.index + 1,
},
{
accessorKey: 'reference_number',
header: 'No Referensi',
},
{
accessorKey: 'transaction_type',
header: 'Jenis Transaksi',
},
{
accessorKey: 'customer_name',
header: 'Pelanggan',
},
{
accessorKey: 'payment_date',
header: 'Tanggal Pembayaran',
cell: (props) =>
formatDate(props.row.original.payment_date, 'DD MMM YYYY'),
},
{
accessorKey: 'created_date',
header: 'Tanggal Dibuat',
cell: (props) =>
formatDate(props.row.original.created_date, 'DD MMM YYYY'),
},
{
accessorKey: 'payment_method',
header: 'Metode Pembayaran',
},
{
accessorKey: 'bank_name',
header: 'Bank',
},
{
accessorKey: 'expense_amount',
header: 'Pengeluaran',
cell: (props) => (
<span className='text-error'>
{formatCurrency(props.row.original.expense_amount)}
</span>
),
},
{
accessorKey: 'revenue_amount',
header: 'Pemasukan',
cell: (props) => (
<span className='text-success'>
{formatCurrency(props.row.original.revenue_amount)}
</span>
),
},
{
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 - 3;
return (
<>
{currentPageSize > 3 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu type='dropdown' props={props} />
</RowDropdownOptions>
)}
{currentPageSize <= 3 && (
<RowCollapseOptions>
<RowOptionsMenu type='collapse' props={props} />
</RowCollapseOptions>
)}
</>
);
},
},
];
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value);
};
const handleFilterChange = (
value: OptionType | null,
setter: (val: OptionType | null) => void,
filterKey: keyof FinanceTableFilter
) => {
setter(value);
updateFilter(filterKey, value ? String(value.value) : '');
};
const resetFilters = () => {
setSelectedTransactionType(null);
setSelectedCustomer(null);
setSelectedSupplier(null);
setSelectedKandang(null);
setSelectedBank(null);
setSelectedSortBy(null);
resetFilter();
};
// track sorting
useEffect(() => {
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
if (!isNameSorted) {
updateFilter('nameSort', '');
} else {
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
}
}, [sorting, updateFilter]);
return (
<div className='w-full p-0 sm:p-4'>
<div className='flex flex-col gap-4 mb-4'>
{/* Row 1: Search and Add Button */}
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-4'>
<Button
href='/finance/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
<DebouncedTextInput
name='search'
placeholder='Search'
value={tableFilterState.search}
onChange={searchChangeHandler}
className={{ wrapper: 'w-full sm:max-w-xs' }}
/>
</div>
{/* Row 2: Transaction Type, Customer, Supplier, Mitra */}
<div className='grid grid-cols-12 gap-4'>
<SelectInput
label='Jenis Transaksi'
placeholder='Semua Jenis Transaksi'
options={TRANSACTION_TYPE_OPTIONS}
value={selectedTransactionType}
onChange={(val) =>
handleFilterChange(
val as OptionType,
setSelectedTransactionType,
'transactionType'
)
}
isClearable
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
/>
<SelectInput
label='Customer'
placeholder='Pilih Customer'
options={customerOptions}
isLoading={isLoadingCustomerOptions}
onInputChange={setCustomerInputValue}
value={selectedCustomer}
onChange={(val) =>
handleFilterChange(
val as OptionType,
setSelectedCustomer,
'customerId'
)
}
isClearable
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
/>
<SelectInput
label='Supplier'
placeholder='Pilih Supplier'
options={supplierOptions}
isLoading={isLoadingSupplierOptions}
onInputChange={setSupplierInputValue}
value={selectedSupplier}
onChange={(val) =>
handleFilterChange(
val as OptionType,
setSelectedSupplier,
'supplierId'
)
}
isClearable
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
/>
<SelectInput
label='Mitra'
placeholder='Pilih Kandang Mitra'
options={kandangOptions}
isLoading={isLoadingKandangOptions}
onInputChange={setKandangInputValue}
value={selectedKandang}
onChange={(val) =>
handleFilterChange(
val as OptionType,
setSelectedKandang,
'kandangId'
)
}
isClearable
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
/>
</div>
{/* Row 3: Bank, Sort By, Start Date, End Date */}
<div className='grid grid-cols-12 gap-4'>
<SelectInput
label='Bank'
placeholder='Pilih Bank'
options={bankOptions}
isLoading={isLoadingBankOptions}
onInputChange={setBankInputValue}
value={selectedBank}
onChange={(val) =>
handleFilterChange(val as OptionType, setSelectedBank, 'bankId')
}
isClearable
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
/>
<SelectInput
label='Urutkan Berdasarkan'
placeholder='Tanggal Pembayaran'
options={SORT_OPTIONS}
value={selectedSortBy}
onChange={(val) =>
handleFilterChange(val as OptionType, setSelectedSortBy, 'sortBy')
}
isClearable
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
/>
<DateInput
name='startDate'
label='Tanggal Mulai'
placeholder='Pilih Tanggal Mulai'
value={tableFilterState.startDate}
onChange={(e) => updateFilter('startDate', e.target.value)}
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
/>
<DateInput
name='endDate'
label='Tanggal Akhir '
placeholder='Pilih Tanggal Selesai'
value={tableFilterState.endDate}
onChange={(e) => updateFilter('endDate', e.target.value)}
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
/>
</div>
<div className='flex flex-row justify-end items-center'>
<div className='flex flex-row gap-2'>
<Button
onClick={resetFilters}
variant='outline'
color='warning'
className='min-w-20'
>
Reset
</Button>
</div>
</div>
</div>
<Table<Finance>
data={isResponseSuccess(finances) ? finances?.data : []}
columns={financeColumns}
pageSize={tableFilterState.pageSize}
onPageSizeChange={setPageSize}
rowOptions={[10, 20, 50, 100]}
page={isResponseSuccess(finances) ? finances?.meta?.page : 0}
totalItems={
isResponseSuccess(finances) ? finances?.meta?.total_results : 0
}
onPageChange={setPage}
isLoading={isLoadingFinances}
sorting={sorting}
setSorting={setSorting}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
className={{
containerClassName: cn({
'w-full mb-20':
isResponseSuccess(finances) && finances?.data?.length === 0,
}),
}}
/>
</div>
);
};
export default FinancesTable;
@@ -0,0 +1,71 @@
'use client';
import * as Yup from 'yup';
import { OptionType } from '@/components/input/SelectInput';
import { CreateFinancePayload } from '@/types/api/finance';
export const FinanceFormSchema = Yup.object().shape({
transactionType: Yup.object()
.shape({
value: Yup.string().required(),
label: Yup.string().required(),
})
.nullable()
.required('Jenis Transaksi Wajib diisi'),
customerId: Yup.number()
.nullable()
.when('transactionType', {
is: (val: { value: string }) => val?.value === 'REVENUE',
then: (schema) => schema.required('Customer Wajib diisi'),
otherwise: (schema) => schema.notRequired(),
}),
paymentDate: Yup.string().required('Tanggal Pembayaran Wajib diisi'),
paymentMethod: Yup.object()
.shape({
value: Yup.string().required(),
label: Yup.string().required(),
})
.nullable()
.required('Metode Pembayaran Wajib diisi'),
bankId: Yup.number()
.nullable()
.test('required-if-transfer', 'Bank Wajib diisi', function (value) {
const paymentMethod = this.parent.paymentMethod;
if (paymentMethod?.value === 'TRANSFER' && !value) {
return false;
}
return true;
}),
supplierBankAccountNumber: Yup.string()
.nullable()
.test(
'required-if-transfer',
'Nomor Rekening Customer Wajib diisi',
function (value) {
const paymentMethod = this.parent.paymentMethod;
if (paymentMethod?.value === 'TRANSFER' && !value) {
return false;
}
return true;
}
),
referenceNumber: Yup.string().nullable(),
amount: Yup.number()
.typeError('Nominal harus berupa angka')
.required('Nominal Wajib diisi'),
notes: Yup.string().nullable(),
});
export type FinanceFormValues = {
transactionType: OptionType | null;
customerId: number | null;
customer: OptionType | null;
paymentDate: string;
paymentMethod: OptionType | null;
bankId: number | null;
bank: OptionType | null;
supplierBankAccountNumber: string;
referenceNumber: string;
amount: string;
notes: string;
};
@@ -0,0 +1,386 @@
'use client';
import { useModal } from '@/components/Modal';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { FinanceApi } from '@/services/api/finance';
import {
Finance,
CreateFinancePayload,
UpdateFinancePayload,
} from '@/types/api/finance';
import { useRouter } from 'next/navigation';
import { useCallback, useEffect, useMemo, useState } from 'react';
import toast from 'react-hot-toast';
import { useFormik } from 'formik';
import Button from '@/components/Button';
import { Icon } from '@iconify/react';
import TextInput from '@/components/input/TextInput';
import NumberInput from '@/components/input/NumberInput';
import { cn } from '@/lib/helper';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import TextArea from '@/components/input/TextArea';
import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import DateInput from '@/components/input/DateInput';
import { BankApi, CustomerApi } from '@/services/api/master-data';
import { Customer } from '@/types/api/master-data/customer';
import { Bank } from '@/types/api/master-data/bank';
import {
FINANCE_PAYMENT_METHOD_OPTIONS,
FINANCE_TRANSACTION_TYPE_OPTIONS,
} from '@/config/constant';
import { FinanceFormSchema, FinanceFormValues } from './FinanceForm.schema';
import Link from 'next/link';
interface FinanceFormProps {
formType?: 'add' | 'edit' | 'detail';
initialValues?: Finance;
}
const FinanceForm = ({ formType = 'add', initialValues }: FinanceFormProps) => {
const router = useRouter();
const deleteModal = useModal();
const [financeFormErrorMessage, fileFormErrorMessage] = useState('');
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
// API Options
const {
options: customerOptions,
isLoadingOptions: isLoadingCustomerOptions,
setInputValue: setCustomerInputValue,
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
const {
options: bankOptions,
isLoadingOptions: isLoadingBankOptions,
setInputValue: setBankInputValue,
} = useSelect<Bank>(BankApi.basePath, 'id', 'name');
const createFinanceHandler = useCallback(
async (payload: CreateFinancePayload) => {
const createFinanceRes = await FinanceApi.create(payload);
if (isResponseError(createFinanceRes)) {
fileFormErrorMessage(createFinanceRes.message);
return;
}
toast.success(createFinanceRes?.message as string);
router.push('/finance');
},
[router]
);
// Note: Update and Delete handlers are not strictly needed for "add" page but good practice to structure for future
const deleteFinanceHandler = () => {
deleteModal.openModal();
};
const confirmationModalDeleteclickHandler = async () => {
// Implement delete logic if needed
setIsDeleteLoading(true);
// await FinanceApi.delete(initialValues?.id as number);
deleteModal.closeModal();
setIsDeleteLoading(false);
router.push('/finance');
};
const formikInitialValues = useMemo<FinanceFormValues>(() => {
return {
transactionType: initialValues
? {
value: initialValues.transaction_type,
label: initialValues.transaction_type,
} // Should map properly if labels differ
: null,
customerId: initialValues ? null : null, // ID not directly in BaseFinance without processing, assume add mode mostly
customer: initialValues
? { value: 0, label: initialValues.customer_name }
: null,
paymentDate: initialValues?.payment_date ?? '',
paymentMethod: প্রিMethods(initialValues?.payment_method),
bankId: null,
bank: initialValues ? { value: 0, label: initialValues.bank_name } : null,
supplierBankAccountNumber: '',
referenceNumber: initialValues?.reference_number ?? '',
amount: initialValues
? String(initialValues.revenue_amount || initialValues.expense_amount)
: '',
notes: '',
};
}, [initialValues]);
function িMethods(method?: string) {
if (!method) return null;
return (
FINANCE_PAYMENT_METHOD_OPTIONS.find((o) => o.value === method) ?? {
value: method,
label: method,
}
);
}
const formik = useFormik<FinanceFormValues>({
initialValues: formikInitialValues,
validationSchema: FinanceFormSchema,
onSubmit: async (values) => {
fileFormErrorMessage('');
const payload: CreateFinancePayload = {
transaction_type: (values.transactionType as OptionType)
.value as string,
customer_id: values.customer ? (values.customer.value as number) : 0,
payment_date: values.paymentDate,
payment_method: (values.paymentMethod as OptionType).value as string,
bank_id: values.bank ? (values.bank.value as number) : 0,
supplier_bank_account_number: values.supplierBankAccountNumber,
reference_number: values.referenceNumber,
amount: Number(values.amount),
notes: values.notes,
};
if (formType === 'add') {
await createFinanceHandler(payload);
}
},
});
const { setValues: formikSetValues } = formik;
useEffect(() => {
if (formType !== 'add' && initialValues) {
// Hydrate logic would go here if editing
}
}, [formType, initialValues]);
// Helper for select changes
const handleSelectChange = (
fieldName: keyof FinanceFormValues,
val: OptionType | null
) => {
formik.setFieldValue(fieldName, val);
};
return (
<>
<section className='w-full max-w-xl'>
<header className='flex flex-col gap-4'>
<Button
href='/finance'
variant='link'
className='w-fit p-0 text-primary'
>
<Icon icon='uil:arrow-left' width={24} height={24} />
Kembali
</Button>
<h1 className='text-2xl font-bold text-center'>
{formType === 'add' && 'Tambah Keuangan'}
{formType === 'edit' && 'Ubah Keuangan'}
{formType === 'detail' && 'Detail Keuangan'}
</h1>
</header>
<form
onSubmit={formik.handleSubmit}
className='w-full mt-8 flex flex-col gap-6'
>
<div className='flex flex-col gap-4'>
<SelectInput
required
label='Pilih Jenis Transaksi'
placeholder='Pilih Jenis Transaksi'
options={FINANCE_TRANSACTION_TYPE_OPTIONS}
value={formik.values.transactionType}
onChange={(val) =>
handleSelectChange('transactionType', val as OptionType)
}
isError={
formik.touched.transactionType &&
Boolean(formik.errors.transactionType)
}
errorMessage={formik.errors.transactionType as string}
isDisabled={formType === 'detail'}
/>
<SelectInput
required
label='Customer'
placeholder='Pilih Customer'
options={customerOptions}
isLoading={isLoadingCustomerOptions}
onInputChange={setCustomerInputValue}
value={formik.values.customer}
onChange={(val) =>
handleSelectChange('customer', val as OptionType)
}
isError={
formik.touched.customer && Boolean(formik.errors.customer)
}
errorMessage={formik.errors.customer as string} // Schema logic handles this validation conditionally
isDisabled={formType === 'detail'}
/>
<DateInput
required
label='Tanggal Pembayaran'
name='paymentDate'
placeholder='Pilih Tanggal Pembayaran'
value={formik.values.paymentDate}
onChange={(e) =>
formik.setFieldValue('paymentDate', e.target.value)
}
isError={
formik.touched.paymentDate && Boolean(formik.errors.paymentDate)
}
errorMessage={formik.errors.paymentDate}
readOnly={formType === 'detail'}
/>
<SelectInput
required
label='Metode Pembayaran'
placeholder='Pilih Metode Pembayaran'
options={FINANCE_PAYMENT_METHOD_OPTIONS}
value={formik.values.paymentMethod}
onChange={(val) =>
handleSelectChange('paymentMethod', val as OptionType)
}
isError={
formik.touched.paymentMethod &&
Boolean(formik.errors.paymentMethod)
}
errorMessage={formik.errors.paymentMethod as string}
isDisabled={formType === 'detail'}
/>
<SelectInput
label='Nomor Rekening'
placeholder='Pilih Bank'
options={bankOptions}
isLoading={isLoadingBankOptions}
onInputChange={setBankInputValue}
value={formik.values.bank}
onChange={(val) => handleSelectChange('bank', val as OptionType)}
isError={formik.touched.bank && Boolean(formik.errors.bank)} // Actually controlled by bankId in schema but logic applies
errorMessage={formik.errors.bankId as string} // bankId error mapping
isDisabled={formType === 'detail'}
/>
<TextInput
required
label='Nomor Rekening Customer' // As per screenshot
name='supplierBankAccountNumber'
placeholder='Nomor Rekening'
value={formik.values.supplierBankAccountNumber}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={
formik.touched.supplierBankAccountNumber &&
Boolean(formik.errors.supplierBankAccountNumber)
}
errorMessage={formik.errors.supplierBankAccountNumber}
readOnly={formType === 'detail'}
/>
<TextInput
label='Nomor Referensi'
name='referenceNumber'
placeholder='Nomor Referensi'
value={formik.values.referenceNumber}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={
formik.touched.referenceNumber &&
Boolean(formik.errors.referenceNumber)
}
errorMessage={formik.errors.referenceNumber}
readOnly={formType === 'detail'}
/>
<NumberInput
required
label='Nominal'
name='amount'
placeholder='Nominal'
value={Number(formik.values.amount)}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={formik.touched.amount && Boolean(formik.errors.amount)}
errorMessage={formik.errors.amount}
readOnly={formType === 'detail'}
inputPrefix={
<span className='text-gray-600 font-medium'>Rp</span>
}
/>
<TextArea
required
label='Catatan'
name='notes'
placeholder='Catatan'
value={formik.values.notes}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={formik.touched.notes && Boolean(formik.errors.notes)}
errorMessage={formik.errors.notes}
readOnly={formType === 'detail'}
/>
</div>
<div className='flex flex-row gap-2'>
<Button
type='submit'
color='primary'
isLoading={formik.isSubmitting}
disabled={!formik.isValid || !formik.dirty || formik.isSubmitting}
className='w-24'
>
Simpan
</Button>
<Button
type='button'
variant='outline'
color='warning'
href='/finance'
className='w-24'
>
Batal
</Button>
</div>
{financeFormErrorMessage && (
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{financeFormErrorMessage}</span>
</div>
)}
</form>
</section>
{formType !== 'add' && (
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data ini?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
onClick: confirmationModalDeleteclickHandler,
isLoading: isDeleteLoading,
}}
/>
)}
</>
);
};
export default FinanceForm;
+17 -1
View File
@@ -36,10 +36,15 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
icon: 'heroicons-outline:currency-dollar', icon: 'heroicons-outline:currency-dollar',
}, },
{ {
text: 'Biaya Operasional', text: 'Biaya',
link: '/expense', link: '/expense',
icon: 'heroicons:wallet', icon: 'heroicons:wallet',
}, },
{
text: 'Keuangan',
link: '/finance',
icon: 'heroicons:wallet-solid',
},
{ {
text: 'Closing', text: 'Closing',
link: '/closing', link: '/closing',
@@ -251,3 +256,14 @@ export const ACCEPTED_FILE_TYPE = {
'image/*': [], 'image/*': [],
}, },
}; };
export const FINANCE_TRANSACTION_TYPE_OPTIONS = [
{ value: 'REVENUE', label: 'Pemasukan' },
{ value: 'EXPENSE', label: 'Pengeluaran' },
];
export const FINANCE_PAYMENT_METHOD_OPTIONS = [
{ value: 'TRANSFER', label: 'Transfer' },
{ value: 'CASH', label: 'Cash' },
{ value: 'GIRO', label: 'Giro' },
];
+279
View File
@@ -0,0 +1,279 @@
import { BaseApiService } from '@/services/api/base';
import {
CreateFinancePayload,
Finance,
UpdateFinancePayload,
} from '@/types/api/finance';
import { BaseApiResponse } from '@/types/api/api-general';
import { httpClientFetcher } from '@/services/http/client';
import { sleep } from '@/lib/helper';
export const dummyFinanceListResponse: BaseApiResponse<Finance[]> = {
code: 200,
status: 'success',
message: 'Finance list fetched successfully',
meta: {
page: 1,
limit: 15,
total_pages: 1,
total_results: 15,
},
data: [
{
id: 1,
reference_number: 'FIN-202501-001',
transaction_type: 'REVENUE',
customer_name: 'PT Sumber Makmur',
payment_date: '2025-01-05',
created_date: '2025-01-03',
payment_method: 'TRANSFER',
bank_name: 'BCA',
expense_amount: 0,
revenue_amount: 5000000,
created_user: {
id: 1,
id_user: 101,
email: 'admin@example.com',
name: 'Admin',
},
created_at: '2025-01-03T10:00:00Z',
updated_at: '2025-01-04T09:00:00Z',
},
{
id: 2,
reference_number: 'FIN-202501-002',
transaction_type: 'EXPENSE',
customer_name: 'UD Sentosa Farm',
payment_date: '2025-01-06',
created_date: '2025-01-04',
payment_method: 'CASH',
bank_name: '-',
expense_amount: 1200000,
revenue_amount: 0,
created_user: {
id: 2,
id_user: 102,
email: 'user1@example.com',
name: 'Budi Santoso',
},
created_at: '2025-01-04T08:20:00Z',
updated_at: '2025-01-04T08:20:00Z',
},
{
id: 3,
reference_number: 'FIN-202501-003',
transaction_type: 'REVENUE',
customer_name: 'PT Golden Egg',
payment_date: '2025-01-07',
created_date: '2025-01-05',
payment_method: 'TRANSFER',
bank_name: 'Mandiri',
expense_amount: 0,
revenue_amount: 7800000,
created_user: {
id: 3,
id_user: 103,
email: 'user2@example.com',
name: 'Clara Wijaya',
},
created_at: '2025-01-05T07:00:00Z',
updated_at: '2025-01-06T15:00:00Z',
},
{
id: 4,
reference_number: 'FIN-202501-004',
transaction_type: 'EXPENSE',
customer_name: 'PT Logistic Nusantara',
payment_date: '2025-01-07',
created_date: '2025-01-05',
payment_method: 'TRANSFER',
bank_name: 'BRI',
expense_amount: 950000,
revenue_amount: 0,
created_user: {
id: 4,
id_user: 104,
email: 'user3@example.com',
name: 'Dedi Pratama',
},
created_at: '2025-01-05T09:00:00Z',
updated_at: '2025-01-05T09:00:00Z',
},
{
id: 5,
reference_number: 'FIN-202501-005',
transaction_type: 'REVENUE',
customer_name: 'CV Agro Sentosa',
payment_date: '2025-01-08',
created_date: '2025-01-06',
payment_method: 'TRANSFER',
bank_name: 'BCA',
expense_amount: 0,
revenue_amount: 3200000,
created_user: {
id: 5,
id_user: 105,
email: 'user4@example.com',
name: 'Erika Tan',
},
created_at: '2025-01-06T10:00:00Z',
updated_at: '2025-01-07T11:00:00Z',
},
{
id: 6,
reference_number: 'FIN-202501-006',
transaction_type: 'EXPENSE',
customer_name: 'PT Pakan Jaya',
payment_date: '2025-01-08',
created_date: '2025-01-06',
payment_method: 'TRANSFER',
bank_name: 'BNI',
expense_amount: 2300000,
revenue_amount: 0,
created_user: {
id: 1,
id_user: 101,
email: 'admin@example.com',
name: 'Admin',
},
created_at: '2025-01-06T12:00:00Z',
updated_at: '2025-01-06T12:00:00Z',
},
{
id: 7,
reference_number: 'FIN-202501-007',
transaction_type: 'REVENUE',
customer_name: 'PT Telur Sejahtera',
payment_date: '2025-01-09',
created_date: '2025-01-07',
payment_method: 'CASH',
bank_name: '-',
expense_amount: 0,
revenue_amount: 5400000,
created_user: {
id: 2,
id_user: 102,
email: 'user1@example.com',
name: 'Budi Santoso',
},
created_at: '2025-01-07T11:00:00Z',
updated_at: '2025-01-07T11:00:00Z',
},
{
id: 8,
reference_number: 'FIN-202501-008',
transaction_type: 'EXPENSE',
customer_name: 'CV Transport Mulia',
payment_date: '2025-01-09',
created_date: '2025-01-07',
payment_method: 'TRANSFER',
bank_name: 'Mandiri',
expense_amount: 750000,
revenue_amount: 0,
created_user: {
id: 3,
id_user: 103,
email: 'user2@example.com',
name: 'Clara Wijaya',
},
created_at: '2025-01-07T14:00:00Z',
updated_at: '2025-01-07T14:00:00Z',
},
{
id: 9,
reference_number: 'FIN-202501-009',
transaction_type: 'REVENUE',
customer_name: 'PT IndoEgg',
payment_date: '2025-01-10',
created_date: '2025-01-08',
payment_method: 'TRANSFER',
bank_name: 'BCA',
expense_amount: 0,
revenue_amount: 4100000,
created_user: {
id: 4,
id_user: 104,
email: 'user3@example.com',
name: 'Dedi Pratama',
},
created_at: '2025-01-08T08:00:00Z',
updated_at: '2025-01-09T16:00:00Z',
},
{
id: 10,
reference_number: 'FIN-202501-010',
transaction_type: 'EXPENSE',
customer_name: 'PT Listrik Jaya',
payment_date: '2025-01-10',
created_date: '2025-01-08',
payment_method: 'TRANSFER',
bank_name: 'BRI',
expense_amount: 1800000,
revenue_amount: 0,
created_user: {
id: 5,
id_user: 105,
email: 'user4@example.com',
name: 'Erika Tan',
},
created_at: '2025-01-08T09:00:00Z',
updated_at: '2025-01-08T09:00:00Z',
},
// Remaining 5 items for total 15:
...Array.from({ length: 5 }).map((_, i) => {
const idx = i + 11;
return {
id: idx,
reference_number: `FIN-202501-${String(idx).padStart(3, '0')}`,
transaction_type: idx % 2 === 0 ? 'EXPENSE' : 'REVENUE',
customer_name: `Customer ${idx}`,
payment_date: `2025-01-${10 + i}`,
created_date: `2025-01-${9 + i}`,
payment_method: idx % 2 === 0 ? 'TRANSFER' : 'CASH',
bank_name: idx % 2 === 0 ? 'Mandiri' : '-',
expense_amount: idx % 2 === 0 ? 1000000 + i * 150000 : 0,
revenue_amount: idx % 2 !== 0 ? 3000000 + i * 200000 : 0,
created_user: {
id: idx % 5,
id_user: 100 + (idx % 5),
email: `user${idx % 5}@example.com`,
name: `User ${idx % 5}`,
},
created_at: `2025-01-${9 + i}T10:00:00Z`,
updated_at: `2025-01-${9 + i}T12:00:00Z`,
};
}),
],
};
export class FinanceApiService extends BaseApiService<
Finance,
CreateFinancePayload,
UpdateFinancePayload
> {
constructor(basePath: string) {
super(basePath);
}
async getAllFetcher(endpoint: string): Promise<BaseApiResponse<Finance[]>> {
// return await httpClientFetcher<BaseApiResponse<Finance[]>>(endpoint);
await sleep(1000);
return dummyFinanceListResponse;
}
}
export const FinanceApi = new FinanceApiService('/finance');
+30
View File
@@ -0,0 +1,30 @@
import { BaseMetadata } from '@/types/api/api-general';
export type BaseFinance = {
id: number;
reference_number: string;
transaction_type: string;
customer_name: string;
payment_date: string;
created_date: string;
payment_method: string;
bank_name: string;
expense_amount: number;
revenue_amount: number;
};
export type Finance = BaseMetadata & BaseFinance;
export type CreateFinancePayload = {
transaction_type: string;
customer_id: number;
payment_date: string;
payment_method: string;
bank_id: number;
supplier_bank_account_number: string;
reference_number: string;
amount: number;
notes: string;
};
export type UpdateFinancePayload = CreateFinancePayload;