mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 05:22:02 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4376eeea85 | |||
| f83474ffbf | |||
| 981755ff68 | |||
| dbbc421ce6 | |||
| cadd5b09ba | |||
| 0ced3f3bac | |||
| 5b368ffb78 | |||
| d1b918bcbf | |||
| e6c4a731b1 | |||
| 3dd3172738 | |||
| fa09e140c0 | |||
| 3951f197e3 |
@@ -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;
|
||||||
@@ -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
@@ -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' },
|
||||||
|
];
|
||||||
|
|||||||
@@ -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');
|
||||||
Vendored
+30
@@ -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;
|
||||||
Reference in New Issue
Block a user